@mikrojs/firmware 0.18.0 → 0.18.1
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/components/mikrojs/CMakeLists.txt +12 -1
- package/components/mikrojs/include/mik_http_internal.h +47 -0
- package/components/mikrojs/include/mik_ota_native.h +19 -0
- package/components/mikrojs/mik_config.cpp +28 -5
- package/components/mikrojs/mik_http.cpp +199 -8
- package/components/mikrojs/mik_nvs_kv.cpp +28 -5
- package/components/mikrojs/mik_ota.cpp +305 -106
- package/components/mikrojs/mik_ota_client_module.cpp +743 -0
- package/components/mikrojs/mik_ota_env.cpp +370 -0
- package/components/mikrojs/test/ota_host/run.sh +7 -2
- 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/sdkconfig.defaults +58 -0
|
@@ -69,6 +69,7 @@ idf_component_register(
|
|
|
69
69
|
"${MIK_SRC_DIR}/mik_sys.cpp"
|
|
70
70
|
"${MIK_SRC_DIR}/mik_text_encoding.cpp"
|
|
71
71
|
"${MIK_SRC_DIR}/mik_cbor.cpp"
|
|
72
|
+
"${MIK_SRC_DIR}/mik_sys_codec.cpp"
|
|
72
73
|
"${MIK_SRC_DIR}/mik_result.cpp"
|
|
73
74
|
"${MIK_SRC_DIR}/mik_udp.cpp"
|
|
74
75
|
"${MIK_SRC_DIR}/mik_observable.cpp"
|
|
@@ -97,6 +98,15 @@ idf_component_register(
|
|
|
97
98
|
"mik_pwm.cpp"
|
|
98
99
|
"mik_nvs_kv.cpp"
|
|
99
100
|
"mik_ota.cpp"
|
|
101
|
+
# The OTA check-in client and config reader: policy, state machine and slot
|
|
102
|
+
# store in the portable library, platform seam and JS surface here.
|
|
103
|
+
"${MIK_SRC_DIR}/mik_ota_policy.cpp"
|
|
104
|
+
"${MIK_SRC_DIR}/mik_ota_slots.cpp"
|
|
105
|
+
"${MIK_SRC_DIR}/mik_ota_config.cpp"
|
|
106
|
+
"${MIK_SRC_DIR}/mik_ota_js_hooks.cpp"
|
|
107
|
+
"${MIK_SRC_DIR}/mik_ota_client.cpp"
|
|
108
|
+
"mik_ota_env.cpp"
|
|
109
|
+
"mik_ota_client_module.cpp"
|
|
100
110
|
"mik_recovery.cpp"
|
|
101
111
|
"mik_rtc.cpp"
|
|
102
112
|
"mik_serial_io.cpp"
|
|
@@ -182,7 +192,8 @@ include("${MIK_BYTECODE_CMAKE}")
|
|
|
182
192
|
|
|
183
193
|
# Force linker to include self-registering native modules
|
|
184
194
|
set(_MIK_MODULES cbor pin i2c i2s spi http http_server wifi rtc nvs_kv sntp sleep neopixel pwm uart ota)
|
|
185
|
-
|
|
195
|
+
list(APPEND _MIK_MODULES ota_client)
|
|
196
|
+
set(_MIK_BYTECODE_MODULES abort cbor env result schema fs http/helpers http/request http/server i2c i2s kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators ota ota/client ota/config pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
186
197
|
if(CONFIG_BT_ENABLED)
|
|
187
198
|
list(APPEND _MIK_MODULES ble)
|
|
188
199
|
list(APPEND _MIK_BYTECODE_MODULES ble)
|
|
@@ -48,10 +48,30 @@ struct MIKHttpQueuedMsg {
|
|
|
48
48
|
MIKHttpQueuedMsg* next;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/* A C consumer of one request's messages, used when the requester is native
|
|
52
|
+
* code rather than JS — the OTA client. Set on a pending entry, it replaces the
|
|
53
|
+
* two promises entirely: nothing on that entry is ever handed to JS.
|
|
54
|
+
*
|
|
55
|
+
* Callbacks run from mik__http_consume, i.e. on the JS loop thread, never on the
|
|
56
|
+
* HTTP background task. `done` fires exactly once and is terminal. */
|
|
57
|
+
struct MIKHttpNativeSink {
|
|
58
|
+
void (*headers)(void* user_data, int status);
|
|
59
|
+
void (*data)(void* user_data, const uint8_t* data, size_t len);
|
|
60
|
+
void (*done)(void* user_data, int status, const char* error_msg);
|
|
61
|
+
void* user_data;
|
|
62
|
+
};
|
|
63
|
+
|
|
51
64
|
struct MIKHttpPending {
|
|
52
65
|
uint32_t id;
|
|
53
66
|
std::atomic<bool>* cancelled;
|
|
54
67
|
bool js_cancelled;
|
|
68
|
+
/* Native requests only: when this request stops being worth waiting for,
|
|
69
|
+
* in esp_timer microseconds. 0 means no deadline. */
|
|
70
|
+
int64_t deadline_us;
|
|
71
|
+
|
|
72
|
+
/* `done == nullptr` means this is a JS request and the promises below are
|
|
73
|
+
* live. Otherwise messages route to the sink and the promises are unused. */
|
|
74
|
+
MIKHttpNativeSink sink;
|
|
55
75
|
|
|
56
76
|
MIKPromise headers_promise;
|
|
57
77
|
bool headers_resolved;
|
|
@@ -63,6 +83,33 @@ struct MIKHttpPending {
|
|
|
63
83
|
MIKHttpQueuedMsg* queue_tail;
|
|
64
84
|
};
|
|
65
85
|
|
|
86
|
+
/* One native request, as the OTA client describes it. Strings and buffers are
|
|
87
|
+
* borrowed for the duration of the start call only; the module copies them. */
|
|
88
|
+
struct MIKHttpNativeRequest {
|
|
89
|
+
const char* url;
|
|
90
|
+
const char* method;
|
|
91
|
+
const char* const* header_keys;
|
|
92
|
+
const char* const* header_values;
|
|
93
|
+
size_t header_count;
|
|
94
|
+
const uint8_t* body;
|
|
95
|
+
size_t body_len;
|
|
96
|
+
/* Whole-request bound, milliseconds; 0 for none. The 10s socket timeout
|
|
97
|
+
* only bounds a single read, so a server that dribbles is otherwise
|
|
98
|
+
* unbounded and holds the task and its TLS session indefinitely. */
|
|
99
|
+
uint32_t timeout_ms;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/* Bring the transport up for a native consumer that never imports the JS module.
|
|
103
|
+
* Must be called before mik__http_start_native. Idempotent. */
|
|
104
|
+
void mik__http_ensure_native(struct JSContext* ctx);
|
|
105
|
+
|
|
106
|
+
/* Start a request whose messages go to `sink`. Returns the request id, or 0 when
|
|
107
|
+
* it could not be started. Defined in mik_http.cpp. */
|
|
108
|
+
uint32_t mik__http_start_native(struct MIKRuntime* rt, const MIKHttpNativeRequest* req,
|
|
109
|
+
const MIKHttpNativeSink* sink);
|
|
110
|
+
/* Abandon a native request. No further sink callbacks fire for it. */
|
|
111
|
+
void mik__http_cancel_native(struct MIKRuntime* rt, uint32_t id);
|
|
112
|
+
|
|
66
113
|
/* Ceilings shared with the test harness. Must match the #defines in
|
|
67
114
|
* mik_http.cpp. */
|
|
68
115
|
#define MIK_HTTP_MAX_PENDING 4
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Internal wiring for the native OTA client (mikro/ota/client backed by C).
|
|
2
|
+
* Not part of the public ESP API: only the OTA module and its env implementation
|
|
3
|
+
* include this. */
|
|
4
|
+
#pragma once
|
|
5
|
+
|
|
6
|
+
#include "mikrojs/ota_env.h"
|
|
7
|
+
|
|
8
|
+
/* Fill the install-op slots with mik_ota.cpp's staging machinery — the same
|
|
9
|
+
* cores the native:mikro/ota JS bindings call. */
|
|
10
|
+
void mik__ota_fill_install_ops(MIKOtaEnv* env);
|
|
11
|
+
|
|
12
|
+
/* The OTA environment for this runtime, fully populated (install ops, kv, HTTP,
|
|
13
|
+
* identity, clock). Valid until the runtime is freed. `rt` is needed because the
|
|
14
|
+
* HTTP seam borrows the http module's task and queue, which live per runtime.
|
|
15
|
+
*
|
|
16
|
+
* `bytecode_version` cannot be derived without a JSContext, so it is passed in
|
|
17
|
+
* from the module init that has one. */
|
|
18
|
+
struct MIKRuntime;
|
|
19
|
+
const MIKOtaEnv* mik__ota_env_for(struct MIKRuntime* rt, int bytecode_version);
|
|
@@ -223,26 +223,47 @@ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
223
223
|
uint16_t val_len = vl[0] | (vl[1] << 8);
|
|
224
224
|
consumed += 2;
|
|
225
225
|
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
/* Sized to carry a 4 KiB config document plus its {version, doc}
|
|
227
|
+
* envelope (a config seed is the largest value this path sees);
|
|
228
|
+
* mirrored as KV_VALUE_MAX_BYTES in the CLI/sim protocol — keep
|
|
229
|
+
* the two in sync. Heap, not stack: the value and its CBOR blob
|
|
230
|
+
* together would not fit a task stack. */
|
|
231
|
+
constexpr uint32_t kValueMax = 4608;
|
|
232
|
+
if (val_len >= kValueMax || consumed + (uint32_t)val_len > payload_len) {
|
|
228
233
|
mik__proto_drain(transport, payload_len - consumed);
|
|
229
234
|
mik__proto_send_err(transport, "value too long");
|
|
230
235
|
return true;
|
|
231
236
|
}
|
|
232
|
-
|
|
237
|
+
char* value = (char*)malloc((size_t)val_len + 1);
|
|
238
|
+
uint8_t* blob = (uint8_t*)malloc((size_t)val_len + 8);
|
|
239
|
+
if (value == NULL || blob == NULL) {
|
|
240
|
+
free(value);
|
|
241
|
+
free(blob);
|
|
242
|
+
mik__proto_drain(transport, payload_len - consumed);
|
|
243
|
+
mik__proto_send_err(transport, "out of memory");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (!mik__proto_read_exact(transport, value, val_len)) {
|
|
247
|
+
free(value);
|
|
248
|
+
free(blob);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
233
251
|
value[val_len] = '\0';
|
|
234
252
|
consumed += val_len;
|
|
235
253
|
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
236
254
|
|
|
237
255
|
if (key_len == 0 || key_len > 15) {
|
|
256
|
+
free(value);
|
|
257
|
+
free(blob);
|
|
238
258
|
mik__proto_send_err(transport, "key exceeds NVS 15-char limit");
|
|
239
259
|
return true;
|
|
240
260
|
}
|
|
241
261
|
|
|
242
262
|
nanocbor_encoder_t enc;
|
|
243
|
-
|
|
244
|
-
nanocbor_encoder_init(&enc, blob, sizeof(blob));
|
|
263
|
+
nanocbor_encoder_init(&enc, blob, (size_t)val_len + 8);
|
|
245
264
|
if (nanocbor_put_tstrn(&enc, value, val_len) < 0) {
|
|
265
|
+
free(value);
|
|
266
|
+
free(blob);
|
|
246
267
|
mik__proto_send_err(transport, "cbor encode failed");
|
|
247
268
|
return true;
|
|
248
269
|
}
|
|
@@ -255,6 +276,8 @@ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
255
276
|
if (err == ESP_OK) err = nvs_commit(handle);
|
|
256
277
|
nvs_close(handle);
|
|
257
278
|
}
|
|
279
|
+
free(value);
|
|
280
|
+
free(blob);
|
|
258
281
|
if (err == ESP_OK) {
|
|
259
282
|
mik__proto_send_ok(transport);
|
|
260
283
|
} else {
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
#include "esp_crt_bundle.h"
|
|
5
5
|
#include "esp_event.h"
|
|
6
|
+
#include "esp_heap_caps.h"
|
|
6
7
|
#include "esp_http_client.h"
|
|
7
8
|
#include "esp_log.h"
|
|
8
9
|
#include "esp_netif.h"
|
|
10
|
+
#include "esp_system.h"
|
|
11
|
+
#include "esp_timer.h"
|
|
9
12
|
#include "freertos/FreeRTOS.h"
|
|
10
13
|
#include "freertos/queue.h"
|
|
11
14
|
#include "freertos/semphr.h"
|
|
@@ -162,17 +165,19 @@ static bool mik__task_cancelled(MIKHttpTaskArgs* args) {
|
|
|
162
165
|
* fragmentation that is NOT a leak in this code — it's mbedTLS scatter-alloc
|
|
163
166
|
* during handshake plus lwIP holding socket TCBs in TIME_WAIT for 2*MSL
|
|
164
167
|
* (CONFIG_LWIP_TCP_MSL). Both reclaim over time; sysFree recovers after the
|
|
165
|
-
* TIME_WAIT window. The UAF fix in commit [UAF fix] removed the real bug
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
168
|
+
* TIME_WAIT window. The UAF fix in commit [UAF fix] removed the real bug.
|
|
169
|
+
* CONFIG_MBEDTLS_DYNAMIC_BUFFER and a shorter MSL are now both in
|
|
170
|
+
* sdkconfig.defaults.
|
|
171
|
+
* Still unexplored: session reuse, or an esp-tls-direct implementation. */
|
|
169
172
|
static void mik__http_task(void* arg) {
|
|
170
173
|
auto* args = static_cast<MIKHttpTaskArgs*>(arg);
|
|
171
174
|
|
|
172
175
|
MIKHttpEventData event_data = {};
|
|
173
176
|
bool headers_posted = false;
|
|
174
177
|
bool cancelled = false;
|
|
175
|
-
|
|
178
|
+
/* 384: a build URL (64-hex checksum path) plus full TLS detail and heap
|
|
179
|
+
* figures overruns 256, and truncation eats the trailing heap figures. */
|
|
180
|
+
char error_buf[384] = {0};
|
|
176
181
|
bool have_error = false;
|
|
177
182
|
|
|
178
183
|
esp_http_client_config_t config = {};
|
|
@@ -188,6 +193,13 @@ static void mik__http_task(void* arg) {
|
|
|
188
193
|
config.event_handler = mik__http_event_handler;
|
|
189
194
|
config.user_data = &event_data;
|
|
190
195
|
config.crt_bundle_attach = esp_crt_bundle_attach;
|
|
196
|
+
// No HTTP_TLS_DYN_BUF_RX_STATIC: converting the RX buffer to a static
|
|
197
|
+
// ~16.6 KB allocation right after the handshake demands a contiguous
|
|
198
|
+
// block exactly when the heap is most fragmented, and the failure is
|
|
199
|
+
// silent (esp-tls records no error code and its log line compiles out) —
|
|
200
|
+
// observed live on a c6 as a connect failure with empty TLS slots. For
|
|
201
|
+
// check-in-sized exchanges dynamic RX allocates per record (~2 KB), which
|
|
202
|
+
// is strictly lighter than pinning the full buffer.
|
|
191
203
|
|
|
192
204
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
193
205
|
if (!client) {
|
|
@@ -225,12 +237,18 @@ static void mik__http_task(void* arg) {
|
|
|
225
237
|
* keep the plain connect error (no heap-level guessing). */
|
|
226
238
|
bool out_of_memory = err == ESP_ERR_NO_MEM || last_tls_err == ESP_ERR_NO_MEM ||
|
|
227
239
|
tls_code == 0x008d || tls_code == 0x7f00;
|
|
240
|
+
/* Heap figures settle ambiguous codes: -0x3000 (X509 fatal) is
|
|
241
|
+
* both "CA not in bundle" and "calloc failed in the bundle verify
|
|
242
|
+
* callback" — a starved largest block names the second. */
|
|
228
243
|
snprintf(error_buf, sizeof(error_buf),
|
|
229
244
|
"fetch failed: %s %s (%s, errno=%d, "
|
|
230
|
-
"esp_tls=%s, mbedtls=-0x%04x, flags=0x%x
|
|
245
|
+
"esp_tls=%s, mbedtls=-0x%04x, flags=0x%x, "
|
|
246
|
+
"sysFree=%u, largestBlock=%u)",
|
|
231
247
|
out_of_memory ? "out of memory connecting to" : "could not connect to",
|
|
232
248
|
args->req.url, esp_err_to_name(err), sock_errno,
|
|
233
|
-
esp_err_to_name(last_tls_err), tls_code, tls_flags
|
|
249
|
+
esp_err_to_name(last_tls_err), tls_code, tls_flags,
|
|
250
|
+
static_cast<unsigned>(esp_get_free_heap_size()),
|
|
251
|
+
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)));
|
|
234
252
|
have_error = true;
|
|
235
253
|
goto cleanup;
|
|
236
254
|
}
|
|
@@ -476,6 +494,104 @@ static bool mik__enqueue_msg(MIKHttpPending* p, const MIKHttpMsg& m) {
|
|
|
476
494
|
return true;
|
|
477
495
|
}
|
|
478
496
|
|
|
497
|
+
/* ── Native requests ───────────────────────────────────────────────── */
|
|
498
|
+
/* Native code (the OTA client) shares this module's task, TLS setup, inflight
|
|
499
|
+
* ceiling and chunk budget rather than standing up a second esp_http_client
|
|
500
|
+
* path: one TLS client on the device is what keeps the handshake heap spike
|
|
501
|
+
* bounded and tuned in one place. */
|
|
502
|
+
|
|
503
|
+
uint32_t mik__http_start_native(MIKRuntime* rt, const MIKHttpNativeRequest* spec,
|
|
504
|
+
const MIKHttpNativeSink* sink) {
|
|
505
|
+
if (!rt || !spec || !sink || !sink->done) return 0;
|
|
506
|
+
if (mik__http_slot < 0 || !mik__http_st(rt)) return 0;
|
|
507
|
+
MIKHttpState* state = mik__http_st(rt);
|
|
508
|
+
if (state->pending_count >= MIK_HTTP_MAX_PENDING) return 0;
|
|
509
|
+
|
|
510
|
+
MIKHttpRequest req = {};
|
|
511
|
+
req.url = strdup(spec->url ? spec->url : "");
|
|
512
|
+
if (!req.url) return 0;
|
|
513
|
+
req.method = mik__method_from_string(spec->method);
|
|
514
|
+
|
|
515
|
+
if (spec->body && spec->body_len > 0) {
|
|
516
|
+
req.body = static_cast<uint8_t*>(malloc(spec->body_len));
|
|
517
|
+
if (!req.body) {
|
|
518
|
+
mik__http_free_request(&req);
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
memcpy(req.body, spec->body, spec->body_len);
|
|
522
|
+
req.body_len = spec->body_len;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (spec->header_count > 0) {
|
|
526
|
+
req.headers =
|
|
527
|
+
static_cast<MIKHttpHeader*>(calloc(spec->header_count, sizeof(MIKHttpHeader)));
|
|
528
|
+
if (!req.headers) {
|
|
529
|
+
mik__http_free_request(&req);
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
for (size_t i = 0; i < spec->header_count; i++) {
|
|
533
|
+
char* key = strdup(spec->header_keys[i]);
|
|
534
|
+
char* value = strdup(spec->header_values[i]);
|
|
535
|
+
if (!key || !value) {
|
|
536
|
+
free(key);
|
|
537
|
+
free(value);
|
|
538
|
+
mik__http_free_request(&req);
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
req.headers[req.header_count].key = key;
|
|
542
|
+
req.headers[req.header_count].value = value;
|
|
543
|
+
req.header_count++;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
MIKHttpPending pending = {};
|
|
548
|
+
pending.id = state->next_id++;
|
|
549
|
+
pending.sink = *sink;
|
|
550
|
+
pending.deadline_us =
|
|
551
|
+
spec->timeout_ms > 0
|
|
552
|
+
? esp_timer_get_time() + static_cast<int64_t>(spec->timeout_ms) * 1000
|
|
553
|
+
: 0;
|
|
554
|
+
|
|
555
|
+
auto* cancelled = new std::atomic<bool>(false);
|
|
556
|
+
pending.cancelled = cancelled;
|
|
557
|
+
|
|
558
|
+
auto* task_args = static_cast<MIKHttpTaskArgs*>(malloc(sizeof(MIKHttpTaskArgs)));
|
|
559
|
+
if (!task_args) {
|
|
560
|
+
mik__http_free_request(&req);
|
|
561
|
+
delete cancelled;
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
task_args->id = pending.id;
|
|
565
|
+
task_args->req = req;
|
|
566
|
+
task_args->result_queue = state->result_queue;
|
|
567
|
+
task_args->inflight = state->inflight;
|
|
568
|
+
task_args->cancelled = cancelled;
|
|
569
|
+
|
|
570
|
+
if (xTaskCreate(mik__http_task, "mik_http", MIK_HTTP_TASK_STACK_SIZE, task_args,
|
|
571
|
+
tskIDLE_PRIORITY + 1, nullptr) != pdPASS) {
|
|
572
|
+
mik__http_free_request(&task_args->req);
|
|
573
|
+
free(task_args);
|
|
574
|
+
delete cancelled;
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
state->pending[state->pending_count++] = pending;
|
|
579
|
+
return pending.id;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
void mik__http_cancel_native(MIKRuntime* rt, uint32_t id) {
|
|
583
|
+
if (!rt || mik__http_slot < 0 || !mik__http_st(rt)) return;
|
|
584
|
+
MIKHttpState* state = mik__http_st(rt);
|
|
585
|
+
MIKHttpPending* p = mik__find_pending(state, id);
|
|
586
|
+
if (!p || !p->sink.done) return;
|
|
587
|
+
if (p->cancelled) p->cancelled->store(true, std::memory_order_relaxed);
|
|
588
|
+
/* Stop delivering entirely: the task still emits a terminal message, which
|
|
589
|
+
* the consume loop then drops along with the pending entry. */
|
|
590
|
+
p->sink.headers = nullptr;
|
|
591
|
+
p->sink.data = nullptr;
|
|
592
|
+
p->js_cancelled = true;
|
|
593
|
+
}
|
|
594
|
+
|
|
479
595
|
static JSValue mik__http_request(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
|
|
480
596
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
481
597
|
CHECK_NOT_NULL(mik_rt);
|
|
@@ -774,6 +890,18 @@ static void mik__ensure_netif_initialized() {
|
|
|
774
890
|
}
|
|
775
891
|
}
|
|
776
892
|
|
|
893
|
+
/* Bring the transport up for a native consumer — the OTA client, which uses this
|
|
894
|
+
* module's task and queue from C rather than importing it from JS.
|
|
895
|
+
*
|
|
896
|
+
* Without this the module is only ever initialized by an import, so a firmware
|
|
897
|
+
* whose only HTTP user is native would have no slot, no state and no loop
|
|
898
|
+
* consumer: every request would fail to start and no response would ever be
|
|
899
|
+
* drained. Idempotent, and safe either side of a JS import — the loop consumer
|
|
900
|
+
* registration de-duplicates. */
|
|
901
|
+
void mik__http_ensure_native(JSContext* ctx);
|
|
902
|
+
void mik__http_consume(JSContext* ctx);
|
|
903
|
+
void mik__http_destroy(JSContext* ctx);
|
|
904
|
+
|
|
777
905
|
static void mik__http_ensure_initialized(JSContext* ctx) {
|
|
778
906
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
779
907
|
CHECK_NOT_NULL(mik_rt);
|
|
@@ -792,6 +920,14 @@ static void mik__http_ensure_initialized(JSContext* ctx) {
|
|
|
792
920
|
mik__http_st(mik_rt) = state;
|
|
793
921
|
}
|
|
794
922
|
|
|
923
|
+
void mik__http_ensure_native(JSContext* ctx) {
|
|
924
|
+
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
925
|
+
if (!mik_rt) return;
|
|
926
|
+
if (mik__http_slot < 0) mik__http_slot = MIK_AllocModuleSlot(mik_rt);
|
|
927
|
+
mik__http_ensure_initialized(ctx);
|
|
928
|
+
MIK_RegisterLoopConsumer(mik_rt, mik__http_consume, mik__http_destroy);
|
|
929
|
+
}
|
|
930
|
+
|
|
795
931
|
static int mik__http_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
796
932
|
mik__http_ensure_initialized(ctx);
|
|
797
933
|
JS_SetModuleExport(ctx, m, "request",
|
|
@@ -806,7 +942,9 @@ static int mik__http_module_init(JSContext* ctx, JSModuleDef* m) {
|
|
|
806
942
|
|
|
807
943
|
static JSModuleDef* mik__http_init(JSContext* ctx) {
|
|
808
944
|
MIKRuntime* mik_rt = MIK_GetRuntime(ctx);
|
|
809
|
-
|
|
945
|
+
/* The slot may already exist: a native consumer can bring the transport up
|
|
946
|
+
* before anything imports the JS module. */
|
|
947
|
+
if (mik__http_slot < 0) mik__http_slot = MIK_AllocModuleSlot(mik_rt);
|
|
810
948
|
|
|
811
949
|
JSModuleDef* m = JS_NewCModule(ctx, "native:mikro/http", mik__http_module_init);
|
|
812
950
|
if (!m) return nullptr;
|
|
@@ -825,6 +963,26 @@ void mik__http_consume(JSContext* ctx) {
|
|
|
825
963
|
if (!mik__http_st(mik_rt)) return;
|
|
826
964
|
|
|
827
965
|
MIKHttpState* state = mik__http_st(mik_rt);
|
|
966
|
+
|
|
967
|
+
/* Time out native requests before draining, so a request whose deadline
|
|
968
|
+
* passed reports it even when the transport has gone quiet. The socket
|
|
969
|
+
* timeout bounds one read; this bounds the request. */
|
|
970
|
+
int64_t now_us = esp_timer_get_time();
|
|
971
|
+
for (size_t i = 0; i < state->pending_count; i++) {
|
|
972
|
+
MIKHttpPending* p = &state->pending[i];
|
|
973
|
+
if (!p->sink.done || p->deadline_us == 0 || now_us < p->deadline_us) continue;
|
|
974
|
+
MIKHttpNativeSink sink = p->sink;
|
|
975
|
+
/* Stop delivering and let the task's terminal message drop the entry;
|
|
976
|
+
* the sink hears about it once, here. */
|
|
977
|
+
p->sink.headers = nullptr;
|
|
978
|
+
p->sink.data = nullptr;
|
|
979
|
+
p->sink.done = nullptr;
|
|
980
|
+
p->js_cancelled = true;
|
|
981
|
+
p->deadline_us = 0;
|
|
982
|
+
if (p->cancelled) p->cancelled->store(true, std::memory_order_relaxed);
|
|
983
|
+
sink.done(sink.user_data, 0, "timeout");
|
|
984
|
+
}
|
|
985
|
+
|
|
828
986
|
MIKHttpMsg msg;
|
|
829
987
|
while (xQueueReceive(state->result_queue, &msg, 0) == pdTRUE) {
|
|
830
988
|
MIKHttpPending* p = mik__find_pending(state, msg.id);
|
|
@@ -841,6 +999,38 @@ void mik__http_consume(JSContext* ctx) {
|
|
|
841
999
|
continue;
|
|
842
1000
|
}
|
|
843
1001
|
|
|
1002
|
+
if (p->sink.done) {
|
|
1003
|
+
/* Native consumer: hand the message over and free the payload here,
|
|
1004
|
+
* since nothing is queued for a later JS read. */
|
|
1005
|
+
size_t idx = static_cast<size_t>(p - state->pending);
|
|
1006
|
+
if (msg.kind == MIK_HTTP_MSG_HEADERS) {
|
|
1007
|
+
if (p->sink.headers) p->sink.headers(p->sink.user_data, msg.status);
|
|
1008
|
+
mik__http_free_headers(msg.headers, msg.header_count);
|
|
1009
|
+
} else if (msg.kind == MIK_HTTP_MSG_CHUNK) {
|
|
1010
|
+
if (p->sink.data) p->sink.data(p->sink.user_data, msg.chunk_data, msg.chunk_len);
|
|
1011
|
+
xSemaphoreGive(state->inflight);
|
|
1012
|
+
free(msg.chunk_data);
|
|
1013
|
+
} else {
|
|
1014
|
+
const char* error = msg.kind == MIK_HTTP_MSG_ERROR
|
|
1015
|
+
? (msg.error_message ? msg.error_message : "HTTP error")
|
|
1016
|
+
: nullptr;
|
|
1017
|
+
/* A cancelled request never calls back, whatever the task ended
|
|
1018
|
+
* up reporting. The consumer cancels on its way out, so calling
|
|
1019
|
+
* `done` here would reach a destroyed object. */
|
|
1020
|
+
if (!p->js_cancelled) {
|
|
1021
|
+
MIKHttpNativeSink sink = p->sink;
|
|
1022
|
+
/* Drop first: the callback may start the next request, and
|
|
1023
|
+
* that would reuse this slot. */
|
|
1024
|
+
mik__pending_drop(state, idx);
|
|
1025
|
+
sink.done(sink.user_data, msg.status, error);
|
|
1026
|
+
} else {
|
|
1027
|
+
mik__pending_drop(state, idx);
|
|
1028
|
+
}
|
|
1029
|
+
if (msg.kind == MIK_HTTP_MSG_ERROR) free(msg.error_message);
|
|
1030
|
+
}
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
844
1034
|
if (msg.kind == MIK_HTTP_MSG_HEADERS) {
|
|
845
1035
|
JSValue v =
|
|
846
1036
|
mik__headers_result_ok(ctx, msg.status, msg.headers, msg.header_count);
|
|
@@ -966,6 +1156,7 @@ void mik__http_destroy(JSContext* ctx) {
|
|
|
966
1156
|
mik__free_queued_msg(m);
|
|
967
1157
|
m = next;
|
|
968
1158
|
}
|
|
1159
|
+
if (p->sink.done) continue; /* native pending: no promises to free */
|
|
969
1160
|
if (!p->headers_resolved) MIK_FreePromise(ctx, &p->headers_promise);
|
|
970
1161
|
if (p->next_promise_active) MIK_FreePromise(ctx, &p->next_promise);
|
|
971
1162
|
}
|
|
@@ -107,20 +107,36 @@ static JSValue mik__nvs_kv_get(JSContext* ctx, JSValue this_val, int argc, JSVal
|
|
|
107
107
|
return JS_UNDEFINED;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/* Absence is the only silent outcome: a missing namespace (nothing ever
|
|
111
|
+
* stored) or a missing key reads as undefined. Every other failure throws,
|
|
112
|
+
* so a read starved by heap pressure (nvs_open allocates its handle) is
|
|
113
|
+
* never mistaken for "not stored". */
|
|
110
114
|
nvs_handle_t handle;
|
|
111
|
-
|
|
115
|
+
esp_err_t err = nvs_open(mik__nvs_ns(magic), NVS_READONLY, &handle);
|
|
116
|
+
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
|
112
117
|
JS_FreeCString(ctx, key);
|
|
113
118
|
return JS_UNDEFINED;
|
|
114
119
|
}
|
|
120
|
+
if (err != ESP_OK) {
|
|
121
|
+
JS_ThrowPlainError(ctx, "nvs open failed reading \"%s\": %s", key, esp_err_to_name(err));
|
|
122
|
+
JS_FreeCString(ctx, key);
|
|
123
|
+
return JS_EXCEPTION;
|
|
124
|
+
}
|
|
115
125
|
|
|
116
126
|
/* Query size first */
|
|
117
127
|
size_t len = 0;
|
|
118
|
-
|
|
119
|
-
if (err
|
|
128
|
+
err = nvs_get_blob(handle, key, nullptr, &len);
|
|
129
|
+
if (err == ESP_ERR_NVS_NOT_FOUND || (err == ESP_OK && len == 0)) {
|
|
120
130
|
JS_FreeCString(ctx, key);
|
|
121
131
|
nvs_close(handle);
|
|
122
132
|
return JS_UNDEFINED;
|
|
123
133
|
}
|
|
134
|
+
if (err != ESP_OK) {
|
|
135
|
+
JS_ThrowPlainError(ctx, "nvs read failed for \"%s\": %s", key, esp_err_to_name(err));
|
|
136
|
+
JS_FreeCString(ctx, key);
|
|
137
|
+
nvs_close(handle);
|
|
138
|
+
return JS_EXCEPTION;
|
|
139
|
+
}
|
|
124
140
|
|
|
125
141
|
/* Read blob */
|
|
126
142
|
auto* buf = static_cast<uint8_t*>(js_malloc(ctx, len));
|
|
@@ -136,7 +152,7 @@ static JSValue mik__nvs_kv_get(JSContext* ctx, JSValue this_val, int argc, JSVal
|
|
|
136
152
|
|
|
137
153
|
if (err != ESP_OK) {
|
|
138
154
|
js_free(ctx, buf);
|
|
139
|
-
return
|
|
155
|
+
return JS_ThrowPlainError(ctx, "nvs read failed: %s", esp_err_to_name(err));
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
/* Decode CBOR */
|
|
@@ -145,7 +161,14 @@ static JSValue mik__nvs_kv_get(JSContext* ctx, JSValue this_val, int argc, JSVal
|
|
|
145
161
|
JSValue result = mik__cbor_decode_value(ctx, &decoder, 0);
|
|
146
162
|
js_free(ctx, buf);
|
|
147
163
|
|
|
148
|
-
|
|
164
|
+
/* A stored blob that does not decode is corruption, not absence. The
|
|
165
|
+
* decoder signals malformed input by value with no pending exception
|
|
166
|
+
* (only allocation failures inside it carry one). TypeError marks it as
|
|
167
|
+
* corruption: the kv layer deletes and heals on TypeError only, never on
|
|
168
|
+
* the transient (plain Error) failures above. */
|
|
169
|
+
if (JS_IsException(result) && !JS_HasException(ctx)) {
|
|
170
|
+
return JS_ThrowTypeError(ctx, "stored nvs value is not valid CBOR");
|
|
171
|
+
}
|
|
149
172
|
return result;
|
|
150
173
|
}
|
|
151
174
|
|