@mikrojs/native 0.20.1 → 0.21.0-next.20260913195055

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CMakeLists.txt +3 -1
  2. package/cmake/mikrojs_bytecode.cmake +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/runtime/result/native-result.node-shim.js.map +1 -1
  6. package/dist/runtime/result/types.d.ts +5 -5
  7. package/dist/runtime/result/types.d.ts.map +1 -1
  8. package/dist/types.d.ts +1 -1
  9. package/dist/types.d.ts.map +1 -1
  10. package/include/mikrojs/mikrojs.h +26 -2
  11. package/include/mikrojs/ota_env.h +8 -6
  12. package/include/mikrojs/ota_policy.h +16 -0
  13. package/include/mikrojs/private.h +13 -0
  14. package/include/mikrojs/utils.h +24 -0
  15. package/package.json +10 -10
  16. package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
  17. package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
  18. package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
  19. package/runtime/gpio/types.ts +116 -0
  20. package/runtime/i2c/types.ts +25 -31
  21. package/runtime/i2s/types.ts +23 -12
  22. package/runtime/internal.d.ts +1 -174
  23. package/runtime/kv/shared.ts +13 -11
  24. package/runtime/neopixel/types.ts +24 -6
  25. package/runtime/observable/native-observable.node-shim.ts +73 -37
  26. package/runtime/observable/native-operators.node-shim.ts +417 -0
  27. package/runtime/observable/observable.ts +76 -1
  28. package/runtime/observable/operators.ts +156 -89
  29. package/runtime/observable/types.ts +61 -6
  30. package/runtime/pwm/types.ts +31 -11
  31. package/runtime/result/native-result.node-shim.ts +1 -1
  32. package/runtime/result/types.ts +5 -5
  33. package/runtime/sleep/types.ts +10 -8
  34. package/runtime/spi/types.ts +16 -14
  35. package/runtime/uart/types.ts +27 -15
  36. package/src/gpio_claim.cpp +63 -0
  37. package/src/mik_abort.cpp +1 -2
  38. package/src/mik_console.cpp +192 -115
  39. package/src/mik_http_client.cpp +3 -14
  40. package/src/mik_inspect.cpp +86 -9
  41. package/src/mik_observable.cpp +1028 -80
  42. package/src/mik_ota_client.cpp +5 -2
  43. package/src/mik_ota_policy.cpp +65 -18
  44. package/src/mik_result.cpp +10 -9
  45. package/src/mikrojs.cpp +26 -13
  46. package/src/modules.cpp +17 -6
  47. package/src/timers.cpp +6 -2
  48. package/src/utils.cpp +106 -0
  49. package/runtime/i2c/i2c.ts +0 -35
  50. package/runtime/i2s/i2s.ts +0 -46
  51. package/runtime/neopixel/neopixel.ts +0 -38
  52. package/runtime/pin/pin.ts +0 -51
  53. package/runtime/pin/types.ts +0 -49
  54. package/runtime/pwm/pwm.ts +0 -32
  55. package/runtime/spi/spi.ts +0 -31
  56. package/runtime/uart/uart.ts +0 -52
@@ -354,10 +354,13 @@ bool MIKOtaClient::Enrollment(std::string* out_url, std::string* out_bearer) con
354
354
  char url[256] = {};
355
355
  char bearer[128] = {};
356
356
  if (!env_ || !env_->kv_get_str) return false;
357
- if (!env_->kv_get_str(env_->opaque, "ota.registry", url, sizeof(url)) || url[0] == '\0') {
357
+ /* A read that failed reads as "not enrolled", which only makes the client
358
+ * skip the round. Erring the other way would send a half-read update key. */
359
+ if (env_->kv_get_str(env_->opaque, "ota.registry", url, sizeof(url)) != MIK_OTA_KV_OK ||
360
+ url[0] == '\0') {
358
361
  return false;
359
362
  }
360
- if (!env_->kv_get_str(env_->opaque, "ota.updateKey", bearer, sizeof(bearer)) ||
363
+ if (env_->kv_get_str(env_->opaque, "ota.updateKey", bearer, sizeof(bearer)) != MIK_OTA_KV_OK ||
361
364
  bearer[0] == '\0') {
362
365
  return false;
363
366
  }
@@ -13,6 +13,11 @@ namespace mikrojs {
13
13
  /* Staging attempts for one url before a checksum is abandoned. */
14
14
  constexpr int32_t MIK__OTA_MAX_TRIES = 3;
15
15
 
16
+ /* The contract is 64 lowercase hex (see decline(), which enforces the format).
17
+ * Only the length is checked here, because a shorter checksum round-trips
18
+ * fine and the registry is free to use one. */
19
+ constexpr size_t MIK__OTA_CHECKSUM_MAX = 64;
20
+
16
21
  const char* mik__ota_outcome_to_str(MIKOtaApplyOutcome outcome) {
17
22
  switch (outcome) {
18
23
  case MIKOtaApplyOutcome::kStaged:
@@ -48,10 +53,23 @@ std::string url_digest(const std::string& url) {
48
53
 
49
54
  } // namespace
50
55
 
56
+ MIKOtaKvStatus MIKOtaStore::ReadStr(const char* key, char* out, size_t max_len) const {
57
+ if (!env_ || !env_->kv_get_str) return MIK_OTA_KV_ABSENT;
58
+ MIKOtaKvStatus status = env_->kv_get_str(env_->opaque, key, out, max_len);
59
+ if (status == MIK_OTA_KV_ERROR) read_failed_ = true;
60
+ return status;
61
+ }
62
+
63
+ MIKOtaKvStatus MIKOtaStore::ReadI32(const char* key, int32_t* out) const {
64
+ if (!env_ || !env_->kv_get_i32) return MIK_OTA_KV_ABSENT;
65
+ MIKOtaKvStatus status = env_->kv_get_i32(env_->opaque, key, out);
66
+ if (status == MIK_OTA_KV_ERROR) read_failed_ = true;
67
+ return status;
68
+ }
69
+
51
70
  bool MIKOtaStore::UrlMatches(const std::string& url) const {
52
- if (!env_ || !env_->kv_get_str) return false;
53
71
  char buf[17] = {};
54
- if (!env_->kv_get_str(env_->opaque, "ota.url", buf, sizeof(buf))) return false;
72
+ if (ReadStr("ota.url", buf, sizeof(buf)) != MIK_OTA_KV_OK) return false;
55
73
  return url_digest(url) == buf;
56
74
  }
57
75
 
@@ -61,9 +79,8 @@ void MIKOtaStore::SetUrl(const std::string& url) {
61
79
  }
62
80
 
63
81
  std::string MIKOtaStore::GetAttempt() const {
64
- if (!env_ || !env_->kv_get_str) return "";
65
82
  char buf[128] = {};
66
- if (env_->kv_get_str(env_->opaque, "ota.att", buf, sizeof(buf))) {
83
+ if (ReadStr("ota.att", buf, sizeof(buf)) == MIK_OTA_KV_OK) {
67
84
  return buf;
68
85
  }
69
86
  return "";
@@ -75,12 +92,14 @@ void MIKOtaStore::SetAttempt(const std::string& checksum) {
75
92
  }
76
93
 
77
94
  int32_t MIKOtaStore::GetTries() const {
78
- if (!env_ || !env_->kv_get_i32) return 0;
79
95
  int32_t val = 0;
80
- if (env_->kv_get_i32(env_->opaque, "ota.tries", &val)) {
96
+ if (ReadI32("ota.tries", &val) == MIK_OTA_KV_OK) {
81
97
  return val;
82
98
  }
83
- return 0;
99
+ /* An unreadable budget counts as spent, which is what stops the attempt in
100
+ * apply_begin. Defaulting to "fully available" is the one mistake here that
101
+ * ends in a build reinstalling itself forever. */
102
+ return read_failed_ ? MIK__OTA_MAX_TRIES : 0;
84
103
  }
85
104
 
86
105
  void MIKOtaStore::SetTries(int32_t n) {
@@ -89,9 +108,8 @@ void MIKOtaStore::SetTries(int32_t n) {
89
108
  }
90
109
 
91
110
  std::string MIKOtaStore::GetBad() const {
92
- if (!env_ || !env_->kv_get_str) return "";
93
111
  char buf[128] = {};
94
- if (env_->kv_get_str(env_->opaque, "ota.bad", buf, sizeof(buf))) {
112
+ if (ReadStr("ota.bad", buf, sizeof(buf)) == MIK_OTA_KV_OK) {
95
113
  return buf;
96
114
  }
97
115
  return "";
@@ -103,9 +121,8 @@ void MIKOtaStore::SetBad(const std::string& checksum) {
103
121
  }
104
122
 
105
123
  bool MIKOtaStore::GetInFlight() const {
106
- if (!env_ || !env_->kv_get_i32) return false;
107
124
  int32_t val = 0;
108
- if (env_->kv_get_i32(env_->opaque, "ota.inflight", &val)) {
125
+ if (ReadI32("ota.inflight", &val) == MIK_OTA_KV_OK) {
109
126
  return val == 1;
110
127
  }
111
128
  return false;
@@ -117,16 +134,15 @@ void MIKOtaStore::SetInFlight(bool in_flight) {
117
134
  }
118
135
 
119
136
  bool MIKOtaStore::GetDecline(MIKOtaDeclineRecord* out) const {
120
- if (!env_ || !env_->kv_get_str) return false;
121
- if (!env_->kv_get_str(env_->opaque, "ota.declined", out->checksum, sizeof(out->checksum)) ||
137
+ if (ReadStr("ota.declined", out->checksum, sizeof(out->checksum)) != MIK_OTA_KV_OK ||
122
138
  out->checksum[0] == '\0') {
123
139
  return false;
124
140
  }
125
- if (!env_->kv_get_str(env_->opaque, "ota.declReason", out->reason, sizeof(out->reason)) ||
141
+ if (ReadStr("ota.declReason", out->reason, sizeof(out->reason)) != MIK_OTA_KV_OK ||
126
142
  out->reason[0] == '\0') {
127
143
  return false;
128
144
  }
129
- if (!env_->kv_get_str(env_->opaque, "ota.declDetail", out->detail, sizeof(out->detail))) {
145
+ if (ReadStr("ota.declDetail", out->detail, sizeof(out->detail)) != MIK_OTA_KV_OK) {
130
146
  out->detail[0] = '\0';
131
147
  }
132
148
  return true;
@@ -181,6 +197,18 @@ bool mik__ota_parse_offer(const char* url, const char* checksum, int64_t size, b
181
197
  return false;
182
198
  }
183
199
 
200
+ /* ota.att and ota.bad read back through a 128-byte buffer, and a stored
201
+ * value too long to decode reports a failed read, not an absent one. That
202
+ * stops every later offer, including a good one, and nothing rewrites the
203
+ * key. Bound it here, at the one place untrusted registry fields become an
204
+ * offer, rather than letting an unreadable value into the store. */
205
+ if (strlen(checksum) > MIK__OTA_CHECKSUM_MAX) {
206
+ if (out_warn_reason) {
207
+ *out_warn_reason = "checksum too long";
208
+ }
209
+ return false;
210
+ }
211
+
184
212
  if (size <= 0) {
185
213
  if (out_warn_reason) {
186
214
  *out_warn_reason = "invalid size";
@@ -202,7 +230,10 @@ MIKOtaReconcileOutcome mik__ota_policy_reconcile(const MIKOtaEnv* env) {
202
230
  MIKOtaStore store(env);
203
231
  if (store.GetInFlight()) {
204
232
  store.SetInFlight(false);
205
- } else {
233
+ } else if (!store.ReadFailed()) {
234
+ /* Only a boot we can prove was clean hands the budget back. An
235
+ * unreadable flag reads as "not in flight", and the attempt it would
236
+ * forget is the one that just took the device down. */
206
237
  store.SetTries(0);
207
238
  }
208
239
  MIKOtaReconcileOutcome outcome = {};
@@ -345,7 +376,23 @@ bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
345
376
 
346
377
  // (d) device has given up on this build
347
378
  MIKOtaStore store(env);
348
- if (!offer.checksum.empty() && offer.checksum == store.GetBad()) {
379
+ std::string bad = store.GetBad();
380
+ bool url_matches = store.UrlMatches(offer.url);
381
+ std::string attempt = store.GetAttempt();
382
+
383
+ // Read first, then decide: every value above reads as absent when the store
384
+ // could not answer, and each of those defaults grants the attempt
385
+ // something: no abandoned build, a url that never matched (a fresh
386
+ // budget). A store this device cannot read is one it has no business
387
+ // re-keying, and the heap pressure behind the failure is no state to start
388
+ // a download in anyway. Nothing is written here, so the next boot decides
389
+ // on the real values.
390
+ if (store.ReadFailed()) {
391
+ if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kExhausted;
392
+ return true;
393
+ }
394
+
395
+ if (!offer.checksum.empty() && offer.checksum == bad) {
349
396
  if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kAbandoned;
350
397
  return true;
351
398
  }
@@ -354,7 +401,7 @@ bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
354
401
  // attempts against this exact build at this exact url; the checksum is not
355
402
  // abandoned, because everything counted here is transient by construction
356
403
  // (a corrupt build is abandoned in apply_end instead).
357
- if (!store.UrlMatches(offer.url) || offer.checksum != store.GetAttempt()) {
404
+ if (!url_matches || offer.checksum != attempt) {
358
405
  store.SetUrl(offer.url);
359
406
  store.SetAttempt(offer.checksum);
360
407
  store.SetTries(0);
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Motivation: the previous TS implementation (runtime/result/result.ts)
18
18
  * compiled to ~7 KB of per-runtime JS heap because every public runtime
19
- * module (mikrojs/pin, mikrojs/wifi, …) top-level-imported it, so both
19
+ * module (mikrojs/wifi, mikrojs/fs, …) top-level-imported it, so both
20
20
  * OkImpl/ErrImpl class bodies and their ~16 method closures were paid once
21
21
  * per runtime whether the app used them or not. Moving the prototype to C
22
22
  * keeps the fluent API while collapsing that to a single shared proto +
@@ -230,20 +230,21 @@ static JSValue js_result_or_default(JSContext* ctx, JSValue this_val, int argc,
230
230
  return argc > 0 ? JS_DupValue(ctx, argv[0]) : JS_UNDEFINED;
231
231
  }
232
232
 
233
- /* .orPanic(msg) — value if ok, else throw an Error(name=PanicError, cause=error) */
233
+ /* .orPanic(msg?) — value if ok, else throw an Error(name=PanicError, cause=error).
234
+ * With no message the report reads "PanicError" followed by the cause. */
234
235
  static JSValue js_result_or_panic(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
235
236
  if (result_is_ok(ctx, this_val)) {
236
237
  return result_get_value(ctx, this_val);
237
238
  }
238
- JSValue panic = JS_NewError(ctx);
239
- const char* msg = NULL;
240
- if (argc > 0) {
239
+ const char* msg = "";
240
+ bool has_msg = argc > 0 && !JS_IsUndefined(argv[0]);
241
+ if (has_msg) {
241
242
  msg = JS_ToCString(ctx, argv[0]);
243
+ if (!msg) return JS_EXCEPTION;
242
244
  }
243
- JS_DefinePropertyValueStr(ctx, panic, "message",
244
- JS_NewString(ctx, msg ? msg : "panic"),
245
- JS_PROP_C_W_E);
246
- if (msg) JS_FreeCString(ctx, msg);
245
+ JSValue panic = JS_NewError(ctx);
246
+ JS_DefinePropertyValueStr(ctx, panic, "message", JS_NewString(ctx, msg), JS_PROP_C_W_E);
247
+ if (has_msg) JS_FreeCString(ctx, msg);
247
248
  JS_DefinePropertyValueStr(ctx, panic, "name", JS_NewString(ctx, "PanicError"),
248
249
  JS_PROP_C_W_E);
249
250
  JSValue cause = result_get_error(ctx, this_val);
package/src/mikrojs.cpp CHANGED
@@ -1211,20 +1211,33 @@ int MIK_RunEntryErr(MIKRuntime* mik_rt, const char* entry, char* err_buf, size_t
1211
1211
  rejected = true;
1212
1212
  }
1213
1213
  }
1214
- if (failed && err_buf && err_buf_size > 0) {
1215
- /* Capture the failure's string form for the caller. Without an
1216
- * err_buf the pending exception is intentionally left on ctx,
1217
- * matching MIK_RunEntry's historical behavior. */
1214
+ if (failed) {
1218
1215
  JSValue exc = rejected ? JS_PromiseResult(ctx, result) : JS_GetException(ctx);
1219
- const char* msg = JS_ToCString(ctx, exc);
1220
- if (msg) {
1221
- snprintf(err_buf, err_buf_size, "%s", msg);
1222
- JS_FreeCString(ctx, msg);
1223
- } else {
1224
- /* Stringifying the exception itself threw (e.g. OOM) — drop
1225
- * the secondary exception so it can't leak into later evals. */
1226
- JSValue stray = JS_GetException(ctx);
1227
- JS_FreeValue(ctx, stray);
1216
+ if (err_buf && err_buf_size > 0) {
1217
+ /* Capture the failure's string form for the caller. */
1218
+ const char* msg = JS_ToCString(ctx, exc);
1219
+ if (msg) {
1220
+ snprintf(err_buf, err_buf_size, "%s", msg);
1221
+ JS_FreeCString(ctx, msg);
1222
+ } else {
1223
+ /* Stringifying the exception itself threw (e.g. OOM) — drop
1224
+ * the secondary exception so it can't leak into later evals. */
1225
+ JSValue stray = JS_GetException(ctx);
1226
+ JS_FreeValue(ctx, stray);
1227
+ }
1228
+ }
1229
+ /* A synchronous throw (a missing import, a syntax error) exists only
1230
+ * as the pending exception consumed above, so report it here the way
1231
+ * MIK_Loop reports one it finds pending: handler, dump, stop. Left
1232
+ * unreported, the device idles in silence. A rejected eval promise is
1233
+ * still queued for the loop's rejection flush, which reports it the
1234
+ * same way. */
1235
+ if (!rejected) {
1236
+ if (mik_rt->error_handler_fn) {
1237
+ mik_rt->error_handler_fn(ctx, exc, mik_rt->error_handler_opaque);
1238
+ }
1239
+ mik_dump_error1(ctx, exc);
1240
+ MIK_Stop(mik_rt);
1228
1241
  }
1229
1242
  JS_FreeValue(ctx, exc);
1230
1243
  }
package/src/modules.cpp CHANGED
@@ -234,8 +234,9 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
234
234
  }
235
235
  }
236
236
  /* Native module not found — this is a device-only module that isn't
237
- * available in the current build (e.g. running on desktop). */
238
- JS_ThrowReferenceError(ctx, "Native module '%s' is not available", module_name);
237
+ * available in the current build (e.g. running on desktop). Same
238
+ * wording as every other unresolvable specifier (and browsers). */
239
+ JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
239
240
  return nullptr;
240
241
  }
241
242
 
@@ -250,11 +251,22 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
250
251
  } c_modules[] = {
251
252
  {"mikro/http/helpers", mik__http_helpers_load},
252
253
  {"mikro/http/request", mik__http_request_load},
254
+ {"mikro/observable/operators", mik__observable_operators_load},
253
255
  {"mikro/wifi", mik__wifi_client_load},
254
256
  };
255
257
  for (const auto& cm : c_modules) {
256
258
  if (strcmp(cm.name, module_name) == 0) return cm.load(ctx);
257
259
  }
260
+ /* Platform C modules registered under their public name (mikro/gpio on ESP-IDF). */
261
+ for (mik_module_desc_t* d = mik__module_registry_head; d != nullptr; d = d->next) {
262
+ if (strcmp(d->name, module_name) == 0) {
263
+ JSModuleDef* reg_m = d->init(ctx);
264
+ if (reg_m && mik_rt && d->consume) {
265
+ MIK_RegisterLoopConsumer(mik_rt, d->consume, d->destroy);
266
+ }
267
+ return reg_m;
268
+ }
269
+ }
258
270
  JSModuleDef* builtin_m = mik__load_builtin(ctx, module_name);
259
271
  if (builtin_m) return builtin_m;
260
272
  /* mik__load_builtin returns NULL either because the module isn't in
@@ -265,8 +277,7 @@ static JSModuleDef* mik_module_loader_inner(JSContext* ctx, const char* module_n
265
277
  * returns JS_UNINITIALIZED from an empty exception slot, and
266
278
  * re-throwing that surfaced as a bare "[uninitialized]" error. */
267
279
  if (!JS_HasException(ctx)) {
268
- JS_ThrowReferenceError(ctx, "Builtin module '%s' is not available in this build",
269
- module_name);
280
+ JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", module_name);
270
281
  }
271
282
  return NULL;
272
283
  }
@@ -996,7 +1007,7 @@ JSValue mik__load_module_ns(JSContext* ctx, const char* importer, const char* sp
996
1007
  JSValue p = JS_LoadModule(ctx, importer, specifier);
997
1008
  if (JS_IsException(p)) {
998
1009
  /* Propagate the loader's real error (e.g. a syntax error in a
999
- * virtual stub), not a generic not-available message. */
1010
+ * virtual stub), not a generic resolution failure. */
1000
1011
  JS_FreeAtom(ctx, name);
1001
1012
  return p;
1002
1013
  }
@@ -1016,7 +1027,7 @@ JSValue mik__load_module_ns(JSContext* ctx, const char* importer, const char* sp
1016
1027
  }
1017
1028
  JS_FreeAtom(ctx, name);
1018
1029
  if (!m) {
1019
- return JS_ThrowReferenceError(ctx, "Native module '%s' is not available", specifier);
1030
+ return JS_ThrowTypeError(ctx, "Failed to resolve module specifier '%s'", specifier);
1020
1031
  }
1021
1032
  /* 5 == EVALUATED; quickjs.h documents the numbering but exports no enum. */
1022
1033
  if (require_evaluated && JS_GetModuleStatus(ctx, m) != 5) {
package/src/timers.cpp CHANGED
@@ -155,9 +155,13 @@ void mik__timers_consume(JSContext* ctx) {
155
155
  if (due_count < MIK_MAX_DUE_TIMERS) {
156
156
  due_ids[due_count++] = entry.id;
157
157
  }
158
- // Update interval deadlines before executing callbacks
158
+ // Drift-free: advance from the previous deadline, not from now, so the rate
159
+ // stays exact. After a stall of a whole period, resume from now; never burst.
159
160
  if (entry.is_interval) {
160
- entry.next_deadline = entry.timeout + platform->get_boot_us();
161
+ entry.next_deadline += entry.timeout;
162
+ if (entry.next_deadline <= now) {
163
+ entry.next_deadline = now + entry.timeout;
164
+ }
161
165
  }
162
166
  }
163
167
  }
package/src/utils.cpp CHANGED
@@ -1,6 +1,7 @@
1
1
  #include "mikrojs/utils.h"
2
2
 
3
3
  #include <errno.h>
4
+ #include <math.h>
4
5
  #include <stdarg.h>
5
6
  #include <stdio.h>
6
7
  #include <stdlib.h>
@@ -58,6 +59,111 @@ void mik__print_error_line(const char* fmt, ...) {
58
59
  MIK_GetPlatform()->stderr_write(buf, len);
59
60
  }
60
61
 
62
+ JSAtom mik__async_iterator_atom(JSContext* ctx) {
63
+ JSValue g = JS_GetGlobalObject(ctx);
64
+ JSValue sym_ctor = JS_GetPropertyStr(ctx, g, "Symbol");
65
+ JSValue sym = JS_GetPropertyStr(ctx, sym_ctor, "asyncIterator");
66
+ JSAtom atom = JS_ValueToAtom(ctx, sym);
67
+ JS_FreeValue(ctx, sym);
68
+ JS_FreeValue(ctx, sym_ctor);
69
+ JS_FreeValue(ctx, g);
70
+ return atom;
71
+ }
72
+
73
+ void mik__warn_after_end(bool* warned, const char* owner, int id, const char* call,
74
+ const char* resource) {
75
+ if (*warned) return;
76
+ *warned = true;
77
+ mik__print_error_line("%s %d: %s after end(); the handle no longer owns the %s", owner, id,
78
+ call, resource);
79
+ }
80
+
81
+ int mik__to_int_arg(JSContext* ctx, JSValueConst v, const char* name, int32_t* out) {
82
+ double d = 0;
83
+ if (!JS_IsNumber(v) || JS_ToFloat64(ctx, &d, v) || trunc(d) != d || fabs(d) > INT32_MAX) {
84
+ /* Reject NaN and fractions instead of truncating them. */
85
+ JS_ThrowTypeError(ctx, "%s must be an integer", name);
86
+ return -1;
87
+ }
88
+ *out = (int32_t)d;
89
+ return 0;
90
+ }
91
+
92
+ int mik__to_number_arg(JSContext* ctx, JSValueConst v, const char* name, double* out) {
93
+ if (!JS_IsNumber(v) || JS_ToFloat64(ctx, out, v)) {
94
+ JS_ThrowTypeError(ctx, "%s must be a number", name);
95
+ return -1;
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ uint8_t* mik__bytes_arg(JSContext* ctx, JSValueConst v, const char* name, size_t* len) {
101
+ size_t offset, elem_size, buf_len;
102
+ JSValue ab = JS_GetTypedArrayBuffer(ctx, v, &offset, len, &elem_size);
103
+ uint8_t* data;
104
+ if (!JS_IsException(ab)) {
105
+ /* len keeps the view length; the full backing buffer goes to a
106
+ * throwaway so a subarray view isn't over-read. */
107
+ data = JS_GetArrayBuffer(ctx, &buf_len, ab);
108
+ JS_FreeValue(ctx, ab);
109
+ if (data) data += offset;
110
+ } else {
111
+ JSValue exc = JS_GetException(ctx);
112
+ JS_FreeValue(ctx, exc);
113
+ data = JS_GetArrayBuffer(ctx, len, v);
114
+ }
115
+ if (!data) JS_ThrowTypeError(ctx, "%s must be a Uint8Array", name);
116
+ return data;
117
+ }
118
+
119
+ int mik__options_arg(JSContext* ctx, int argc, JSValueConst* argv, int index, bool required,
120
+ JSValueConst* out) {
121
+ JSValueConst v = index < argc ? argv[index] : JS_UNDEFINED;
122
+ if (JS_IsObject(v) || (!required && JS_IsUndefined(v))) {
123
+ *out = v;
124
+ return 0;
125
+ }
126
+ JS_ThrowTypeError(ctx, "options must be an object");
127
+ return -1;
128
+ }
129
+
130
+ int mik__int_option(JSContext* ctx, JSValueConst options, const char* name, bool required,
131
+ int32_t* out) {
132
+ if (JS_IsUndefined(options)) return required ? mik__to_int_arg(ctx, options, name, out) : 0;
133
+ JSValue v = JS_GetPropertyStr(ctx, options, name);
134
+ if (JS_IsException(v)) return -1;
135
+ int rc = 0;
136
+ if (required || !JS_IsUndefined(v)) rc = mik__to_int_arg(ctx, v, name, out);
137
+ JS_FreeValue(ctx, v);
138
+ return rc;
139
+ }
140
+
141
+ int mik__number_option(JSContext* ctx, JSValueConst options, const char* name, bool required,
142
+ double* out) {
143
+ if (JS_IsUndefined(options)) return required ? mik__to_number_arg(ctx, options, name, out) : 0;
144
+ JSValue v = JS_GetPropertyStr(ctx, options, name);
145
+ if (JS_IsException(v)) return -1;
146
+ int rc = 0;
147
+ if (required || !JS_IsUndefined(v)) rc = mik__to_number_arg(ctx, v, name, out);
148
+ JS_FreeValue(ctx, v);
149
+ return rc;
150
+ }
151
+
152
+ int mik__bool_option(JSContext* ctx, JSValueConst options, const char* name, bool* out) {
153
+ if (JS_IsUndefined(options)) return 0;
154
+ JSValue v = JS_GetPropertyStr(ctx, options, name);
155
+ if (JS_IsException(v)) return -1;
156
+ int rc = 0;
157
+ if (JS_IsBool(v)) {
158
+ *out = JS_ToBool(ctx, v);
159
+ } else if (!JS_IsUndefined(v)) {
160
+ JS_ThrowTypeError(ctx, "%s must be a boolean", name);
161
+ rc = -1;
162
+ }
163
+ JS_FreeValue(ctx, v);
164
+ return rc;
165
+ }
166
+
61
167
  void mik_dump_error(JSContext* ctx) {
62
168
  mik__watchdog_report_blocking(MIK_GetRuntime(ctx));
63
169
  JSValue exception_val = JS_GetException(ctx);
@@ -1,35 +0,0 @@
1
- import * as native from 'native:mikro/i2c'
2
-
3
- import type {Result} from '../result/types.js'
4
- import type {I2c as PlatformI2c, I2cError, I2cOptions} from './types.js'
5
-
6
- /**
7
- * @public
8
- */
9
- export class I2c implements PlatformI2c {
10
- #native: native.I2c
11
-
12
- constructor(busNo: 0 | 1, options?: I2cOptions) {
13
- this.#native = new native.I2c(busNo, options)
14
- }
15
-
16
- begin(): Result<void, I2cError> {
17
- return this.#native.begin()
18
- }
19
-
20
- end(): Result<void, I2cError> {
21
- return this.#native.end()
22
- }
23
-
24
- scan(): Result<Uint8Array, I2cError> {
25
- return this.#native.scan()
26
- }
27
-
28
- write(address: number, data: Uint8Array, stop?: boolean): Result<void, I2cError> {
29
- return this.#native.write(address, data, stop)
30
- }
31
-
32
- read(address: number, bytes: number): Result<Uint8Array, I2cError> {
33
- return this.#native.read(address, bytes)
34
- }
35
- }
@@ -1,46 +0,0 @@
1
- import * as native from 'native:mikro/i2s'
2
-
3
- import type {Result} from '../result/types.js'
4
- import type {
5
- I2sBase,
6
- I2sError,
7
- I2sPdmRxOptions,
8
- I2sSamples,
9
- I2sStdRxOptions,
10
- I2sStdTxOptions,
11
- I2sStdTxRxOptions,
12
- } from './types.js'
13
-
14
- /**
15
- * @public
16
- */
17
- export class I2s implements I2sBase {
18
- #native: native.I2s
19
-
20
- constructor(port: number, options: I2sStdTxRxOptions)
21
- constructor(port: number, options: I2sStdTxOptions)
22
- constructor(port: number, options: I2sStdRxOptions)
23
- constructor(port: number, options: I2sPdmRxOptions)
24
- constructor(
25
- port: number,
26
- options: I2sStdTxRxOptions | I2sStdTxOptions | I2sStdRxOptions | I2sPdmRxOptions,
27
- ) {
28
- this.#native = new native.I2s(port, options)
29
- }
30
-
31
- begin(): Result<void, I2sError> {
32
- return this.#native.begin()
33
- }
34
-
35
- end(): Result<void, I2sError> {
36
- return this.#native.end()
37
- }
38
-
39
- write(data: I2sSamples | Uint8Array): Promise<Result<void, I2sError>> {
40
- return this.#native.write(data)
41
- }
42
-
43
- capture(frames: number, options?: {gainBits?: number}): Result<Int16Array, I2sError> {
44
- return this.#native.capture(frames, options)
45
- }
46
- }
@@ -1,38 +0,0 @@
1
- import {NeoPixel as NativeNeoPixel} from 'native:mikro/neopixel'
2
-
3
- import type {Result} from '../result/types.js'
4
- import type {NeoPixelError, NeoPixelOptions} from './types.js'
5
-
6
- export class NeoPixel {
7
- #native: InstanceType<typeof NativeNeoPixel>
8
-
9
- constructor(pin: number, options: NeoPixelOptions) {
10
- this.#native = new NativeNeoPixel(pin, options.count, options.rgbw ? 4 : 3)
11
- }
12
-
13
- setPixel(
14
- index: number,
15
- r: number,
16
- g: number,
17
- b: number,
18
- w?: number,
19
- ): Result<void, NeoPixelError> {
20
- return this.#native.setPixel(index, r, g, b, w ?? 0)
21
- }
22
-
23
- fill(r: number, g: number, b: number, w?: number): Result<void, NeoPixelError> {
24
- return this.#native.fill(r, g, b, w ?? 0)
25
- }
26
-
27
- show(): Result<void, NeoPixelError> {
28
- return this.#native.show()
29
- }
30
-
31
- clear(): Result<void, NeoPixelError> {
32
- return this.#native.clear()
33
- }
34
-
35
- end(): Result<void, NeoPixelError> {
36
- return this.#native.end()
37
- }
38
- }
@@ -1,51 +0,0 @@
1
- import * as native from 'native:mikro/pin'
2
-
3
- import type {Result} from '../result/types.js'
4
- import type {AnalogReadOptions, Attenuation, PinError, PinMode} from './types.js'
5
-
6
- // Public mode lookup. Kept as a runtime value (rather than inlined) because
7
- // user code occasionally reads it to build a mode-picker UI on host-side
8
- // tooling; the native:mikro/pin layer uses the numeric constants directly.
9
- const PIN_MODE_INPUT = 0x01
10
- const PIN_MODE_OUTPUT = 0x03
11
- const PIN_MODE_INPUT_PULLUP = 0x05
12
-
13
- export const NativePinModes = {
14
- INPUT: PIN_MODE_INPUT,
15
- OUTPUT: PIN_MODE_OUTPUT,
16
- INPUT_PULLUP: PIN_MODE_INPUT_PULLUP,
17
- } as const
18
-
19
- const NativeAttenuation: Record<Attenuation, number> = {
20
- '0db': 0,
21
- '2.5db': 1,
22
- '6db': 2,
23
- '11db': 3,
24
- } as const
25
-
26
- // Wrappers translate the string/enum JS-idiom args to the numeric codes the
27
- // native layer expects. Errors flow through unchanged: native:mikro/pin returns
28
- // {ok, error:{name,message}} directly (see mik_pin.cpp), so no remapping.
29
-
30
- export function pinMode(pin: number, mode: PinMode): Result<void, PinError> {
31
- return native.pinMode(pin, NativePinModes[mode])
32
- }
33
-
34
- export function digitalWrite(pin: number, value: 0 | 1): Result<void, PinError> {
35
- return native.digitalWrite(pin, value)
36
- }
37
-
38
- export function digitalRead(pin: number): 0 | 1 {
39
- return native.digitalRead(pin) as 0 | 1
40
- }
41
-
42
- export function analogRead(pin: number, options?: AnalogReadOptions): Result<number, PinError> {
43
- return native.analogRead(pin, NativeAttenuation[options?.attenuation ?? '11db'])
44
- }
45
-
46
- export function analogReadMillivolts(
47
- pin: number,
48
- options?: AnalogReadOptions,
49
- ): Result<number, PinError> {
50
- return native.analogReadMillivolts(pin, NativeAttenuation[options?.attenuation ?? '11db'])
51
- }