@mikrojs/firmware 0.18.1 → 0.18.2
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/README.md +1 -1
- package/components/mikrojs/mik_ble.cpp +1 -1
- package/components/mikrojs/mik_http.cpp +8 -7
- package/components/mikrojs/mik_http_server.cpp +1 -2
- package/components/mikrojs/mik_i2s.cpp +1 -1
- package/components/mikrojs/mik_ota_client_module.cpp +181 -1
- package/components/mikrojs/mik_pwm.cpp +1 -2
- package/components/mikrojs/mik_sntp.cpp +1 -1
- package/components/mikrojs/mik_uart.cpp +1 -1
- package/components/mikrojs/mik_wifi.cpp +1 -1
- package/components/mikrojs/test/http_test.cpp +104 -0
- package/package.json +3 -3
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c5/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/project.cmake +2 -2
- package/sdkconfig.defaults +1 -6
package/README.md
CHANGED
|
@@ -26,6 +26,6 @@ See the [Custom Firmware](https://mikrojs.dev/develop/custom-firmware) docs for
|
|
|
26
26
|
## Requirements
|
|
27
27
|
|
|
28
28
|
- Node.js >= 24
|
|
29
|
-
- ESP-IDF >= 6.
|
|
29
|
+
- ESP-IDF >= 6.1 (installed via [EIM](https://docs.espressif.com/projects/idf-im-ui/en/latest/))
|
|
30
30
|
|
|
31
31
|
To build, activate ESP-IDF in your shell first: run `eim select` and source the activation script it prints, then use `idf.py` as usual.
|
|
@@ -1480,7 +1480,7 @@ static int mik__ble_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
1480
1480
|
static JSModuleDef* mik__ble_init(JSContext* ctx) {
|
|
1481
1481
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1482
1482
|
CHECK_NOT_NULL(mik_rt);
|
|
1483
|
-
mik__ble_slot =
|
|
1483
|
+
if (mik__ble_slot < 0) mik__ble_slot = MIK_ReserveModuleSlot();
|
|
1484
1484
|
|
|
1485
1485
|
auto* state = new MIKBleState();
|
|
1486
1486
|
mik__ble_st(mik_rt) = state;
|
|
@@ -923,7 +923,7 @@ static void mik__http_ensure_initialized(JSContext* ctx) {
|
|
|
923
923
|
void mik__http_ensure_native(JSContext* ctx) {
|
|
924
924
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
925
925
|
if (!mik_rt) return;
|
|
926
|
-
if (mik__http_slot < 0) mik__http_slot =
|
|
926
|
+
if (mik__http_slot < 0) mik__http_slot = MIK_ReserveModuleSlot();
|
|
927
927
|
mik__http_ensure_initialized(ctx);
|
|
928
928
|
MIK_RegisterLoopConsumer(mik_rt, mik__http_consume, mik__http_destroy);
|
|
929
929
|
}
|
|
@@ -941,10 +941,9 @@ static int mik__http_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
941
941
|
}
|
|
942
942
|
|
|
943
943
|
static JSModuleDef* mik__http_init(JSContext* ctx) {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if (mik__http_slot < 0) mik__http_slot = MIK_AllocModuleSlot(mik_rt);
|
|
944
|
+
/* The slot may already be reserved: a native consumer can bring the
|
|
945
|
+
* transport up before anything imports the JS module. */
|
|
946
|
+
if (mik__http_slot < 0) mik__http_slot = MIK_ReserveModuleSlot();
|
|
948
947
|
|
|
949
948
|
JSModuleDef* m = JS_NewCModule(ctx, "native:mikro/http", mik__http_module_init);
|
|
950
949
|
if (!m) return nullptr;
|
|
@@ -973,10 +972,12 @@ void mik__http_consume(JSContext* ctx) {
|
|
|
973
972
|
if (!p->sink.done || p->deadline_us == 0 || now_us < p->deadline_us) continue;
|
|
974
973
|
MIKHttpNativeSink sink = p->sink;
|
|
975
974
|
/* Stop delivering and let the task's terminal message drop the entry;
|
|
976
|
-
* the sink hears about it once, here.
|
|
975
|
+
* the sink hears about it once, here. `done` must stay set: it is what
|
|
976
|
+
* marks the entry native, and js_cancelled is what suppresses the
|
|
977
|
+
* second call. Clearing it would route the terminal message into the
|
|
978
|
+
* JS branch, onto promises a native entry never created. */
|
|
977
979
|
p->sink.headers = nullptr;
|
|
978
980
|
p->sink.data = nullptr;
|
|
979
|
-
p->sink.done = nullptr;
|
|
980
981
|
p->js_cancelled = true;
|
|
981
982
|
p->deadline_us = 0;
|
|
982
983
|
if (p->cancelled) p->cancelled->store(true, std::memory_order_relaxed);
|
|
@@ -671,8 +671,7 @@ static int mik__hs_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
671
671
|
}
|
|
672
672
|
|
|
673
673
|
static JSModuleDef* mik__http_server_init(JSContext* ctx) {
|
|
674
|
-
|
|
675
|
-
mik__http_server_slot = MIK_AllocModuleSlot(rt);
|
|
674
|
+
if (mik__http_server_slot < 0) mik__http_server_slot = MIK_ReserveModuleSlot();
|
|
676
675
|
|
|
677
676
|
JSModuleDef* m = JS_NewCModule(ctx, "native:mikro/http_server", mik__hs_module_init);
|
|
678
677
|
if (!m) return nullptr;
|
|
@@ -663,7 +663,7 @@ static int mik__i2s_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
663
663
|
|
|
664
664
|
static JSModuleDef* mik__i2s_init(JSContext* ctx) {
|
|
665
665
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
666
|
-
mik__i2s_slot =
|
|
666
|
+
if (mik__i2s_slot < 0) mik__i2s_slot = MIK_ReserveModuleSlot();
|
|
667
667
|
|
|
668
668
|
auto* slot_data = static_cast<MIKI2sSlot*>(calloc(1, sizeof(MIKI2sSlot)));
|
|
669
669
|
if (!slot_data) return nullptr;
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
#include "esp_log.h"
|
|
15
15
|
#include "mik_http_internal.h"
|
|
16
16
|
#include "mik_ota_native.h"
|
|
17
|
+
#include "mikrojs/cbor_helpers.h"
|
|
17
18
|
#include "mikrojs/ota_client.h"
|
|
18
19
|
#include "mikrojs/ota_config.h"
|
|
19
20
|
#include "mikrojs/ota_js_hooks.h"
|
|
20
21
|
#include "mikrojs/ota_policy.h"
|
|
22
|
+
#include "mikrojs/ota_slots.h"
|
|
21
23
|
#include "mikrojs/private.h"
|
|
22
24
|
#include "mikrojs/utils.h"
|
|
23
25
|
|
|
@@ -26,6 +28,7 @@ using mikrojs::MIKOtaCheckResult;
|
|
|
26
28
|
using mikrojs::MIKOtaCheckStatus;
|
|
27
29
|
using mikrojs::MIKOtaClient;
|
|
28
30
|
using mikrojs::MIKOtaConfigReader;
|
|
31
|
+
using mikrojs::MIKOtaConfigWrite;
|
|
29
32
|
using mikrojs::MIKOtaJsHooks;
|
|
30
33
|
using mikrojs::MIKOtaApplyOutcome;
|
|
31
34
|
using mikrojs::MIKOtaApplySession;
|
|
@@ -628,6 +631,174 @@ JSValue ota_apply_offer(JSContext* ctx, JSValue, int argc, JSValue* argv) {
|
|
|
628
631
|
return promise;
|
|
629
632
|
}
|
|
630
633
|
|
|
634
|
+
// ── config delivery ─────────────────────────────────────────────────────────
|
|
635
|
+
//
|
|
636
|
+
// The write side of config sync, for a client that received a document over its
|
|
637
|
+
// own transport. Where the slot policy lives is src/mik_ota_config.cpp, shared
|
|
638
|
+
// with the built-in client, so the two cannot disagree about the trial or the
|
|
639
|
+
// rollback baseline. What is here is the marshalling.
|
|
640
|
+
|
|
641
|
+
/* Copy a string field into a fixed buffer. False when it is the wrong type or
|
|
642
|
+
* too long to hold: a truncated rev never matches the one the registry issued,
|
|
643
|
+
* so the device would be served the same document forever. */
|
|
644
|
+
bool take_config_field(JSContext* ctx, JSValue obj, const char* key, char* out, size_t out_len,
|
|
645
|
+
bool* out_present) {
|
|
646
|
+
*out_present = false;
|
|
647
|
+
JSValue value = JS_GetPropertyStr(ctx, obj, key);
|
|
648
|
+
if (JS_IsUndefined(value) || JS_IsNull(value)) {
|
|
649
|
+
JS_FreeValue(ctx, value);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
if (!JS_IsString(value)) {
|
|
653
|
+
JS_FreeValue(ctx, value);
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
const char* text = JS_ToCString(ctx, value);
|
|
657
|
+
JS_FreeValue(ctx, value);
|
|
658
|
+
if (!text) return false;
|
|
659
|
+
bool fits = strlen(text) < out_len;
|
|
660
|
+
if (fits) {
|
|
661
|
+
snprintf(out, out_len, "%s", text);
|
|
662
|
+
*out_present = text[0] != '\0';
|
|
663
|
+
}
|
|
664
|
+
JS_FreeCString(ctx, text);
|
|
665
|
+
return fits;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* Validate a raw value into the fields a stored config carries. `*out_doc` is
|
|
669
|
+
* the document to store, or JS_UNDEFINED for the clear; the caller owns it. */
|
|
670
|
+
bool config_from_js(JSContext* ctx, JSValue raw, MIKOtaStoredConfig* out, JSValue* out_doc) {
|
|
671
|
+
*out_doc = JS_UNDEFINED;
|
|
672
|
+
if (!JS_IsObject(raw) || JS_IsArray(raw) || JS_IsFunction(ctx, raw)) return false;
|
|
673
|
+
|
|
674
|
+
/* The stamp is what says which release the document was computed for, and
|
|
675
|
+
* every read is decided by it. A document without one cannot be placed. */
|
|
676
|
+
bool has_version = false;
|
|
677
|
+
if (!take_config_field(ctx, raw, "version", out->version, sizeof(out->version),
|
|
678
|
+
&has_version)) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
if (!has_version) return false;
|
|
682
|
+
|
|
683
|
+
bool has_rev = false;
|
|
684
|
+
if (!take_config_field(ctx, raw, "rev", out->rev, sizeof(out->rev), &has_rev)) return false;
|
|
685
|
+
|
|
686
|
+
JSValue doc = JS_GetPropertyStr(ctx, raw, "doc");
|
|
687
|
+
if (JS_IsUndefined(doc) || JS_IsNull(doc)) {
|
|
688
|
+
/* An absent document is the clear, not a malformed config. */
|
|
689
|
+
JS_FreeValue(ctx, doc);
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
/* The document is spread over the manifest defaults, top level only.
|
|
693
|
+
* Anything that is not a plain object has no keys to spread, so storing one
|
|
694
|
+
* would read back as an empty overlay rather than as the error it is. */
|
|
695
|
+
if (!JS_IsObject(doc) || JS_IsArray(doc) || JS_IsFunction(ctx, doc)) {
|
|
696
|
+
JS_FreeValue(ctx, doc);
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
*out_doc = doc;
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* Encode a document to the CBOR the slots store. False for a value CBOR cannot
|
|
704
|
+
* carry (a function, a cycle, past the depth limit). */
|
|
705
|
+
bool encode_config_doc(JSContext* ctx, JSValue doc, std::vector<uint8_t>* out) {
|
|
706
|
+
/* Pass 1 sizes it against a zero-capacity buffer; the base pointer must be
|
|
707
|
+
* non-null because a zero-length append still reaches memcpy. */
|
|
708
|
+
static uint8_t measure_base;
|
|
709
|
+
nanocbor_encoder_t enc;
|
|
710
|
+
nanocbor_encoder_init(&enc, &measure_base, 0);
|
|
711
|
+
bool ok = mik__cbor_encode_value(ctx, &enc, doc, 0) >= 0;
|
|
712
|
+
size_t needed = nanocbor_encoded_len(&enc);
|
|
713
|
+
if (ok && needed > 0) {
|
|
714
|
+
out->resize(needed);
|
|
715
|
+
nanocbor_encoder_init(&enc, out->data(), needed);
|
|
716
|
+
ok = mik__cbor_encode_value(ctx, &enc, doc, 0) >= 0;
|
|
717
|
+
} else {
|
|
718
|
+
ok = false;
|
|
719
|
+
}
|
|
720
|
+
/* Property enumeration can throw on the way out, and an exception left on
|
|
721
|
+
* the context would surface at whatever ran next. */
|
|
722
|
+
if (JS_HasException(ctx)) JS_FreeValue(ctx, JS_GetException(ctx));
|
|
723
|
+
return ok;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* parseConfig(raw) -> StoredConfig | undefined */
|
|
727
|
+
JSValue ota_parse_config(JSContext* ctx, JSValue, int argc, JSValue* argv) {
|
|
728
|
+
MIKOtaStoredConfig cfg = {};
|
|
729
|
+
JSValue doc = JS_UNDEFINED;
|
|
730
|
+
if (argc < 1 || !config_from_js(ctx, argv[0], &cfg, &doc)) {
|
|
731
|
+
JS_FreeValue(ctx, doc);
|
|
732
|
+
return JS_UNDEFINED;
|
|
733
|
+
}
|
|
734
|
+
JSValue obj = JS_NewObject(ctx);
|
|
735
|
+
if (cfg.rev[0]) JS_SetPropertyStr(ctx, obj, "rev", JS_NewString(ctx, cfg.rev));
|
|
736
|
+
JS_SetPropertyStr(ctx, obj, "version", JS_NewString(ctx, cfg.version));
|
|
737
|
+
if (JS_IsUndefined(doc)) return obj;
|
|
738
|
+
/* The document travels on as it arrived: whether it survives CBOR is
|
|
739
|
+
* settled by applyConfig, which is where the bytes are actually made. */
|
|
740
|
+
JS_SetPropertyStr(ctx, obj, "doc", doc);
|
|
741
|
+
return obj;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* applyConfig(cfg, {trialBoots}) -> ConfigWrite */
|
|
745
|
+
JSValue ota_apply_config(JSContext* ctx, JSValue, int argc, JSValue* argv) {
|
|
746
|
+
MIKOtaClientState* state = state_of(ctx);
|
|
747
|
+
CHECK_NOT_NULL(state);
|
|
748
|
+
|
|
749
|
+
MIKOtaStoredConfig cfg = {};
|
|
750
|
+
JSValue doc = JS_UNDEFINED;
|
|
751
|
+
if (argc < 1 || !config_from_js(ctx, argv[0], &cfg, &doc)) {
|
|
752
|
+
JS_FreeValue(ctx, doc);
|
|
753
|
+
return JS_NewString(ctx, "invalid");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/* Held until the write is done: the stored config's span points into it. */
|
|
757
|
+
std::vector<uint8_t> bytes;
|
|
758
|
+
if (!JS_IsUndefined(doc)) {
|
|
759
|
+
bool encoded = encode_config_doc(ctx, doc, &bytes);
|
|
760
|
+
JS_FreeValue(ctx, doc);
|
|
761
|
+
if (!encoded) return JS_NewString(ctx, "invalid");
|
|
762
|
+
cfg.doc_cbor = bytes.data();
|
|
763
|
+
cfg.doc_cbor_len = bytes.size();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
MIKOtaCheckOptions defaults;
|
|
767
|
+
int trial_boots = defaults.trial_boots;
|
|
768
|
+
if (argc > 1 && JS_IsObject(argv[1])) {
|
|
769
|
+
trial_boots = (int)opt_u32(ctx, argv[1], "trialBoots", (uint32_t)trial_boots);
|
|
770
|
+
}
|
|
771
|
+
/* A budget of zero would roll the document back on the first read that
|
|
772
|
+
* serves it, which is never what an app meant by delivering one. */
|
|
773
|
+
if (trial_boots < 1) trial_boots = 1;
|
|
774
|
+
|
|
775
|
+
MIKOtaConfigWrite write = mikrojs::mik__ota_deliver_config(state->env, &cfg, trial_boots);
|
|
776
|
+
return JS_NewString(ctx, mikrojs::mik__ota_config_write_to_str(write));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/* configState() -> {rev?, error?} */
|
|
780
|
+
JSValue ota_config_state(JSContext* ctx, JSValue, int, JSValue*) {
|
|
781
|
+
MIKOtaClientState* state = state_of(ctx);
|
|
782
|
+
CHECK_NOT_NULL(state);
|
|
783
|
+
|
|
784
|
+
JSValue obj = JS_NewObject(ctx);
|
|
785
|
+
MIKOtaConfigErrorReport report = {};
|
|
786
|
+
bool has_error = mikrojs::mik__ota_load_config_error(state->env, &report);
|
|
787
|
+
if (has_error) {
|
|
788
|
+
JSValue error = JS_NewObject(ctx);
|
|
789
|
+
JS_SetPropertyStr(ctx, error, "rev", JS_NewString(ctx, report.rev));
|
|
790
|
+
JS_SetPropertyStr(ctx, error, "message", JS_NewString(ctx, report.message));
|
|
791
|
+
JS_SetPropertyStr(ctx, obj, "error", error);
|
|
792
|
+
}
|
|
793
|
+
/* After a rollback the FAILED document's rev is the one to echo, not the
|
|
794
|
+
* restored one's: echoed-equals-held is what stops the registry serving the
|
|
795
|
+
* document that just failed, until an operator changes it. */
|
|
796
|
+
mikrojs::MIKOtaLoadedConfig held = mikrojs::mik__ota_load_slot(state->env, MIK_OTA_CFG_CURRENT);
|
|
797
|
+
const char* rev = has_error ? report.rev : (held.present ? held.cfg.rev : "");
|
|
798
|
+
if (rev[0]) JS_SetPropertyStr(ctx, obj, "rev", JS_NewString(ctx, rev));
|
|
799
|
+
return obj;
|
|
800
|
+
}
|
|
801
|
+
|
|
631
802
|
int mik__ota_client_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
632
803
|
JS_SetModuleExport(ctx, m, "check", JS_NewCFunction(ctx, mik__ota_client_check, "check", 1));
|
|
633
804
|
JS_SetModuleExport(ctx, m, "watch", JS_NewCFunction(ctx, mik__ota_client_watch, "watch", 1));
|
|
@@ -642,6 +813,12 @@ int mik__ota_client_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
642
813
|
JS_NewCFunction(ctx, ota_parse_offer, "parseOffer", 2));
|
|
643
814
|
JS_SetModuleExport(ctx, m, "applyOffer",
|
|
644
815
|
JS_NewCFunction(ctx, ota_apply_offer, "applyOffer", 3));
|
|
816
|
+
JS_SetModuleExport(ctx, m, "parseConfig",
|
|
817
|
+
JS_NewCFunction(ctx, ota_parse_config, "parseConfig", 1));
|
|
818
|
+
JS_SetModuleExport(ctx, m, "applyConfig",
|
|
819
|
+
JS_NewCFunction(ctx, ota_apply_config, "applyConfig", 2));
|
|
820
|
+
JS_SetModuleExport(ctx, m, "configState",
|
|
821
|
+
JS_NewCFunction(ctx, ota_config_state, "configState", 0));
|
|
645
822
|
return 0;
|
|
646
823
|
}
|
|
647
824
|
|
|
@@ -650,7 +827,7 @@ int mik__ota_client_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
650
827
|
JSModuleDef* mik__ota_client_init(JSContext* ctx) {
|
|
651
828
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
652
829
|
CHECK_NOT_NULL(mik_rt);
|
|
653
|
-
mik__ota_client_slot =
|
|
830
|
+
if (mik__ota_client_slot < 0) mik__ota_client_slot = MIK_ReserveModuleSlot();
|
|
654
831
|
|
|
655
832
|
/* The bytecode version needs a context to derive (JS_WriteObject's first
|
|
656
833
|
* byte), so it is read here and handed to the env, which has none. */
|
|
@@ -689,6 +866,9 @@ JSModuleDef* mik__ota_client_init(JSContext* ctx) {
|
|
|
689
866
|
JS_AddModuleExport(ctx, m, "registry");
|
|
690
867
|
JS_AddModuleExport(ctx, m, "parseOffer");
|
|
691
868
|
JS_AddModuleExport(ctx, m, "applyOffer");
|
|
869
|
+
JS_AddModuleExport(ctx, m, "parseConfig");
|
|
870
|
+
JS_AddModuleExport(ctx, m, "applyConfig");
|
|
871
|
+
JS_AddModuleExport(ctx, m, "configState");
|
|
692
872
|
return m;
|
|
693
873
|
}
|
|
694
874
|
|
|
@@ -464,8 +464,7 @@ static int mik__pwm_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
static JSModuleDef* mik__pwm_init(JSContext* ctx) {
|
|
467
|
-
|
|
468
|
-
mik__pwm_slot = MIK_AllocModuleSlot(mik_rt);
|
|
467
|
+
if (mik__pwm_slot < 0) mik__pwm_slot = MIK_ReserveModuleSlot();
|
|
469
468
|
|
|
470
469
|
JSRuntime* rt = JS_GetRuntime(ctx);
|
|
471
470
|
|
|
@@ -196,7 +196,7 @@ static int mik__sntp_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
196
196
|
static JSModuleDef* mik__sntp_init(JSContext* ctx) {
|
|
197
197
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
198
198
|
CHECK_NOT_NULL(mik_rt);
|
|
199
|
-
mik__sntp_slot =
|
|
199
|
+
if (mik__sntp_slot < 0) mik__sntp_slot = MIK_ReserveModuleSlot();
|
|
200
200
|
|
|
201
201
|
auto* state = new MIKSntpState();
|
|
202
202
|
state->event_queue = xQueueCreate(4, sizeof(struct timeval));
|
|
@@ -465,7 +465,7 @@ static int mik__uart_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
465
465
|
|
|
466
466
|
static JSModuleDef* mik__uart_init(JSContext* ctx) {
|
|
467
467
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
468
|
-
mik__uart_slot =
|
|
468
|
+
if (mik__uart_slot < 0) mik__uart_slot = MIK_ReserveModuleSlot();
|
|
469
469
|
|
|
470
470
|
/* Allocate per-runtime slot data */
|
|
471
471
|
auto* slot_data = static_cast<MIKUartSlot*>(calloc(1, sizeof(MIKUartSlot)));
|
|
@@ -1236,7 +1236,7 @@ static int mik__wifi_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
1236
1236
|
static JSModuleDef* mik__wifi_init(JSContext* ctx) {
|
|
1237
1237
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
1238
1238
|
CHECK_NOT_NULL(mik_rt);
|
|
1239
|
-
mik__wifi_slot =
|
|
1239
|
+
if (mik__wifi_slot < 0) mik__wifi_slot = MIK_ReserveModuleSlot();
|
|
1240
1240
|
|
|
1241
1241
|
auto* state = new MIKWifiState();
|
|
1242
1242
|
state->event_queue = xQueueCreate(8, sizeof(MIKWifiEvent));
|
|
@@ -473,3 +473,107 @@ TEST_CASE("cancel sets the pending cancelled flag", "[http]") {
|
|
|
473
473
|
TEST_ASSERT_TRUE_MESSAGE(flagAfterCancel, "cancel sets the shared flag");
|
|
474
474
|
TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "pending dropped after cancellation settles");
|
|
475
475
|
}
|
|
476
|
+
|
|
477
|
+
/* ── Native sink (OTA client) ─────────────────────────────────────── */
|
|
478
|
+
|
|
479
|
+
static int native_done_calls;
|
|
480
|
+
static int native_done_status;
|
|
481
|
+
static char native_done_error[64];
|
|
482
|
+
|
|
483
|
+
static void native_sink_done(void*, int status, const char* error_msg) {
|
|
484
|
+
native_done_calls++;
|
|
485
|
+
native_done_status = status;
|
|
486
|
+
snprintf(native_done_error, sizeof(native_done_error), "%s", error_msg ? error_msg : "");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Install a native pending whose deadline has already passed, the way
|
|
490
|
+
* mik__http_start_native would minus the background task. Ownership of
|
|
491
|
+
* `cancelled` transfers to the entry; mik__pending_drop frees it. */
|
|
492
|
+
static uint32_t seed_expired_native_pending() {
|
|
493
|
+
native_done_calls = 0;
|
|
494
|
+
native_done_status = -1;
|
|
495
|
+
native_done_error[0] = '\0';
|
|
496
|
+
|
|
497
|
+
MIKHttpState* state = mik__http(rt);
|
|
498
|
+
MIKHttpPending p = {};
|
|
499
|
+
p.id = state->next_id++;
|
|
500
|
+
p.cancelled = new std::atomic<bool>(false);
|
|
501
|
+
p.sink.done = native_sink_done;
|
|
502
|
+
p.deadline_us = 1; /* in the past for any running device */
|
|
503
|
+
state->pending[state->pending_count++] = p;
|
|
504
|
+
return p.id;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
TEST_CASE("a timed-out native request stays tagged native", "[http]") {
|
|
508
|
+
setup();
|
|
509
|
+
ensure_http_initialized();
|
|
510
|
+
|
|
511
|
+
uint32_t id = seed_expired_native_pending();
|
|
512
|
+
|
|
513
|
+
mik__http_consume(ctx);
|
|
514
|
+
int callsAfterTimeout = native_done_calls;
|
|
515
|
+
int statusAfterTimeout = native_done_status;
|
|
516
|
+
char errorAfterTimeout[64];
|
|
517
|
+
snprintf(errorAfterTimeout, sizeof(errorAfterTimeout), "%s", native_done_error);
|
|
518
|
+
size_t countAfterTimeout = mik__http(rt)->pending_count;
|
|
519
|
+
/* The entry outlives the timeout waiting for the task's terminal message.
|
|
520
|
+
* sink.done is what marks it native; clearing it would send that message
|
|
521
|
+
* into the JS branch and onto promises this entry never created. */
|
|
522
|
+
bool stillTaggedNative = mik__http(rt)->pending[0].sink.done != nullptr;
|
|
523
|
+
|
|
524
|
+
push_error(id, "connection closed");
|
|
525
|
+
mik__http_consume(ctx);
|
|
526
|
+
int callsAfterTerminal = native_done_calls;
|
|
527
|
+
size_t countAfterTerminal = mik__http(rt)->pending_count;
|
|
528
|
+
|
|
529
|
+
teardown();
|
|
530
|
+
|
|
531
|
+
TEST_ASSERT_EQUAL_MESSAGE(1, callsAfterTimeout, "deadline sweep reports once");
|
|
532
|
+
TEST_ASSERT_EQUAL(0, statusAfterTimeout);
|
|
533
|
+
TEST_ASSERT_EQUAL_STRING("timeout", errorAfterTimeout);
|
|
534
|
+
TEST_ASSERT_EQUAL_MESSAGE(1, countAfterTimeout, "entry waits for the terminal message");
|
|
535
|
+
TEST_ASSERT_TRUE_MESSAGE(stillTaggedNative, "sink.done survives the timeout");
|
|
536
|
+
TEST_ASSERT_EQUAL_MESSAGE(1, callsAfterTerminal, "done is terminal, not repeated");
|
|
537
|
+
TEST_ASSERT_EQUAL_MESSAGE(0, countAfterTerminal, "terminal message drops the entry");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
TEST_CASE("destroy skips promises on a timed-out native request", "[http]") {
|
|
541
|
+
setup();
|
|
542
|
+
ensure_http_initialized();
|
|
543
|
+
|
|
544
|
+
uint32_t id = seed_expired_native_pending();
|
|
545
|
+
mik__http_consume(ctx);
|
|
546
|
+
|
|
547
|
+
/* Leave the entry in place and let destroy drain the terminal message.
|
|
548
|
+
* It must take the native branch and free no promises: a native entry's
|
|
549
|
+
* headers_promise was never built. */
|
|
550
|
+
push_error(id, "connection closed");
|
|
551
|
+
int callsBeforeTeardown = native_done_calls;
|
|
552
|
+
|
|
553
|
+
teardown();
|
|
554
|
+
|
|
555
|
+
TEST_ASSERT_EQUAL(1, callsBeforeTeardown);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
TEST_CASE("the http module slot is stable across runtimes", "[http]") {
|
|
559
|
+
setup();
|
|
560
|
+
ensure_http_initialized();
|
|
561
|
+
int slotInFirstRuntime = mik__http_slot;
|
|
562
|
+
teardown();
|
|
563
|
+
|
|
564
|
+
/* A second runtime must find http at the same index, and no other module
|
|
565
|
+
* may be handed that index here. */
|
|
566
|
+
setup();
|
|
567
|
+
int burned = MIK_ReserveModuleSlot();
|
|
568
|
+
ensure_http_initialized();
|
|
569
|
+
int slotInSecondRuntime = mik__http_slot;
|
|
570
|
+
MIKHttpState* state = mik__http(rt);
|
|
571
|
+
size_t pendingCount = state ? state->pending_count : 999;
|
|
572
|
+
teardown();
|
|
573
|
+
|
|
574
|
+
TEST_ASSERT_EQUAL_MESSAGE(slotInFirstRuntime, slotInSecondRuntime,
|
|
575
|
+
"the same module keeps the same slot");
|
|
576
|
+
TEST_ASSERT_NOT_EQUAL_MESSAGE(burned, slotInSecondRuntime,
|
|
577
|
+
"http must not share a slot with another module");
|
|
578
|
+
TEST_ASSERT_EQUAL_MESSAGE(0, pendingCount, "fresh state, not another module's bytes");
|
|
579
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/firmware",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "Mikro.js ESP32 firmware: ESP-IDF component, build tools, and project template",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"esp-idf",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"esbuild": "^0.28.0",
|
|
50
|
-
"@mikrojs/native": "0.18.
|
|
51
|
-
"@mikrojs/quickjs": "0.18.
|
|
50
|
+
"@mikrojs/native": "0.18.2",
|
|
51
|
+
"@mikrojs/quickjs": "0.18.2"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.0.0"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/project.cmake
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
# ── Validate ESP-IDF version ─────────────────────────────────────────
|
|
13
13
|
if(IDF_VERSION_MAJOR LESS 6 OR
|
|
14
|
-
(IDF_VERSION_MAJOR EQUAL 6 AND IDF_VERSION_MINOR
|
|
14
|
+
(IDF_VERSION_MAJOR EQUAL 6 AND IDF_VERSION_MINOR LESS 1))
|
|
15
15
|
message(FATAL_ERROR
|
|
16
|
-
"mikrojs requires ESP-IDF >= 6.
|
|
16
|
+
"mikrojs requires ESP-IDF >= 6.1, found ${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}. "
|
|
17
17
|
"Install a supported version via EIM: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html"
|
|
18
18
|
)
|
|
19
19
|
endif()
|
package/sdkconfig.defaults
CHANGED
|
@@ -185,7 +185,7 @@ CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=n
|
|
|
185
185
|
|
|
186
186
|
# Use smaller cert bundle (common CAs only)
|
|
187
187
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
|
|
188
|
-
# Deliberate deviation from IDF
|
|
188
|
+
# Deliberate deviation from IDF, which defaults this to n to save ~700 B
|
|
189
189
|
# of heap. We need it: with n, esp_crt_bundle looks up only the top served
|
|
190
190
|
# cert's issuer, so any chain that serves a cross-signed root fails outright.
|
|
191
191
|
# That is ~2 in 5 real hosts, mostly Google Trust Services roots cross-signed
|
|
@@ -193,10 +193,5 @@ CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y
|
|
|
193
193
|
# Device-confirmed against registry.npmjs.org (MBEDTLS_ERR_X509_FATAL_ERROR,
|
|
194
194
|
# -0x3000). Neither cross-signing root ships in the CMN or the full bundle, so
|
|
195
195
|
# bundle selection is not an escape either.
|
|
196
|
-
#
|
|
197
|
-
# The cost on 6.0.2 is a leak: ~215 B of issuer-name copies per successful
|
|
198
|
-
# handshake, ~10 KB/day at a 30-minute check-in cadence. Fixed upstream by
|
|
199
|
-
# 0d8ff68f8a47 and a1f1d9072900, landing in an upcoming release. Until our
|
|
200
|
-
# IDF floor gets there, patch IDF rather than turning this off.
|
|
201
196
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y
|
|
202
197
|
|