@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.
- package/CMakeLists.txt +3 -1
- package/cmake/mikrojs_bytecode.cmake +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/result/native-result.node-shim.js.map +1 -1
- package/dist/runtime/result/types.d.ts +5 -5
- package/dist/runtime/result/types.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/mikrojs.h +26 -2
- package/include/mikrojs/ota_env.h +8 -6
- package/include/mikrojs/ota_policy.h +16 -0
- package/include/mikrojs/private.h +13 -0
- package/include/mikrojs/utils.h +24 -0
- package/package.json +10 -10
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/gpio/types.ts +116 -0
- package/runtime/i2c/types.ts +25 -31
- package/runtime/i2s/types.ts +23 -12
- package/runtime/internal.d.ts +1 -174
- package/runtime/kv/shared.ts +13 -11
- package/runtime/neopixel/types.ts +24 -6
- package/runtime/observable/native-observable.node-shim.ts +73 -37
- package/runtime/observable/native-operators.node-shim.ts +417 -0
- package/runtime/observable/observable.ts +76 -1
- package/runtime/observable/operators.ts +156 -89
- package/runtime/observable/types.ts +61 -6
- package/runtime/pwm/types.ts +31 -11
- package/runtime/result/native-result.node-shim.ts +1 -1
- package/runtime/result/types.ts +5 -5
- package/runtime/sleep/types.ts +10 -8
- package/runtime/spi/types.ts +16 -14
- package/runtime/uart/types.ts +27 -15
- package/src/gpio_claim.cpp +63 -0
- package/src/mik_abort.cpp +1 -2
- package/src/mik_console.cpp +192 -115
- package/src/mik_http_client.cpp +3 -14
- package/src/mik_inspect.cpp +86 -9
- package/src/mik_observable.cpp +1028 -80
- package/src/mik_ota_client.cpp +5 -2
- package/src/mik_ota_policy.cpp +65 -18
- package/src/mik_result.cpp +10 -9
- package/src/mikrojs.cpp +26 -13
- package/src/modules.cpp +17 -6
- package/src/timers.cpp +6 -2
- package/src/utils.cpp +106 -0
- package/runtime/i2c/i2c.ts +0 -35
- package/runtime/i2s/i2s.ts +0 -46
- package/runtime/neopixel/neopixel.ts +0 -38
- package/runtime/pin/pin.ts +0 -51
- package/runtime/pin/types.ts +0 -49
- package/runtime/pwm/pwm.ts +0 -32
- package/runtime/spi/spi.ts +0 -31
- package/runtime/uart/uart.ts +0 -52
package/src/mik_ota_client.cpp
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
}
|
package/src/mik_ota_policy.cpp
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
96
|
+
if (ReadI32("ota.tries", &val) == MIK_OTA_KV_OK) {
|
|
81
97
|
return val;
|
|
82
98
|
}
|
|
83
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
404
|
+
if (!url_matches || offer.checksum != attempt) {
|
|
358
405
|
store.SetUrl(offer.url);
|
|
359
406
|
store.SetAttempt(offer.checksum);
|
|
360
407
|
store.SetTries(0);
|
package/src/mik_result.cpp
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
if (
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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);
|
package/runtime/i2c/i2c.ts
DELETED
|
@@ -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
|
-
}
|
package/runtime/i2s/i2s.ts
DELETED
|
@@ -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
|
-
}
|
package/runtime/pin/pin.ts
DELETED
|
@@ -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
|
-
}
|