@mikrojs/firmware 0.16.0 → 0.17.0-next.20260722220623
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 +9 -2
- package/components/mikrojs/include/mikrojs_esp32.h +23 -0
- package/components/mikrojs/mik_config.cpp +159 -5
- package/components/mikrojs/mik_deploy.cpp +54 -35
- package/components/mikrojs/mik_main.cpp +66 -5
- package/components/mikrojs/mik_nvs_kv.cpp +64 -18
- package/components/mikrojs/mik_ota.cpp +1791 -0
- package/components/mikrojs/platform_esp32.cpp +59 -0
- package/components/mikrojs/test/CMakeLists.txt +1 -0
- package/components/mikrojs/test/ble_test.cpp +2 -2
- package/components/mikrojs/test/fs_js_test.cpp +1 -1
- package/components/mikrojs/test/fs_pub_test.cpp +23 -23
- package/components/mikrojs/test/http_server_test.cpp +12 -2
- package/components/mikrojs/test/http_test.cpp +2 -2
- package/components/mikrojs/test/modules_extended_test.cpp +2 -2
- package/components/mikrojs/test/modules_test.cpp +2 -2
- package/components/mikrojs/test/nvs_kv_test.cpp +245 -0
- package/components/mikrojs/test/ota_host/gunzip_host_test.cpp +118 -0
- package/components/mikrojs/test/ota_host/run.sh +174 -0
- package/components/mikrojs/test/rtc_test.cpp +1 -1
- package/components/mikrojs/test/sleep_test.cpp +1 -1
- package/components/mikrojs/test/sntp_test.cpp +1 -1
- package/components/mikrojs/test/stdio_test.cpp +1 -1
- package/components/mikrojs/test/sys_test.cpp +1 -1
- package/components/mikrojs/test/udp_test.cpp +5 -5
- package/components/mikrojs/test/wifi_test.cpp +2 -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
|
@@ -41,6 +41,11 @@ if(CONFIG_BT_ENABLED)
|
|
|
41
41
|
list(APPEND _MIK_BLE_SRCS "mik_ble.cpp" "mik_ble_c_shim.c")
|
|
42
42
|
endif()
|
|
43
43
|
|
|
44
|
+
# mik_ota.cpp includes <miniz.h> for tinfl (gunzip). On the esp32c6 the
|
|
45
|
+
# implementation is in the chip mask ROM and the header comes from the
|
|
46
|
+
# always-available `esp_rom` common dependency, so no REQUIRES entry is needed.
|
|
47
|
+
# A target without ROM miniz needs the `miniz` managed component adding to
|
|
48
|
+
# idf_component.yml.
|
|
44
49
|
idf_component_register(
|
|
45
50
|
INCLUDE_DIRS "include" "${MIK_INCLUDE_DIR}"
|
|
46
51
|
REQUIRES bt littlefs esp_adc esp_app_format esp_driver_gpio esp_driver_i2c esp_driver_i2s esp_driver_ledc esp_driver_rmt esp_driver_spi esp_psram esp_timer esp_http_client esp_http_server esp-tls esp_netif esp_event esp_wifi nvs_flash esp_driver_uart esp_driver_usb_serial_jtag spi_flash
|
|
@@ -55,6 +60,7 @@ idf_component_register(
|
|
|
55
60
|
"${MIK_SRC_DIR}/fs.cpp"
|
|
56
61
|
"${MIK_SRC_DIR}/mem.cpp"
|
|
57
62
|
"${MIK_SRC_DIR}/mik_app_config.cpp"
|
|
63
|
+
"${MIK_SRC_DIR}/mik_app_store.cpp"
|
|
58
64
|
"${MIK_SRC_DIR}/mik_color.cpp"
|
|
59
65
|
"${MIK_SRC_DIR}/mik_console.cpp"
|
|
60
66
|
"${MIK_SRC_DIR}/mik_inspect.cpp"
|
|
@@ -90,6 +96,7 @@ idf_component_register(
|
|
|
90
96
|
"mik_pin.cpp"
|
|
91
97
|
"mik_pwm.cpp"
|
|
92
98
|
"mik_nvs_kv.cpp"
|
|
99
|
+
"mik_ota.cpp"
|
|
93
100
|
"mik_recovery.cpp"
|
|
94
101
|
"mik_rtc.cpp"
|
|
95
102
|
"mik_serial_io.cpp"
|
|
@@ -143,8 +150,8 @@ endif()
|
|
|
143
150
|
include("${MIK_BYTECODE_CMAKE}")
|
|
144
151
|
|
|
145
152
|
# Force linker to include self-registering native modules
|
|
146
|
-
set(_MIK_MODULES cbor pin i2c i2s spi http http_server wifi rtc nvs_kv sntp sleep neopixel pwm uart)
|
|
147
|
-
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 pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
153
|
+
set(_MIK_MODULES cbor pin i2c i2s spi http http_server wifi rtc nvs_kv sntp sleep neopixel pwm uart ota)
|
|
154
|
+
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 pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
148
155
|
if(CONFIG_BT_ENABLED)
|
|
149
156
|
list(APPEND _MIK_MODULES ble)
|
|
150
157
|
list(APPEND _MIK_BYTECODE_MODULES ble)
|
|
@@ -20,6 +20,29 @@ void MIK_Main(void);
|
|
|
20
20
|
/* Deploy recovery (mik_deploy.cpp) */
|
|
21
21
|
void MIK_DeployRecover(void);
|
|
22
22
|
|
|
23
|
+
/* OTA boot reconcile (mik_ota.cpp). Runs before the JS app loads: reflash
|
|
24
|
+
* guard, trial verdict, and a deferred clean-heap install of a staged build
|
|
25
|
+
* (may esp_restart). Call right after MIK_DeployRecover(). */
|
|
26
|
+
void mik__ota_boot_reconcile(void);
|
|
27
|
+
|
|
28
|
+
/* Adopt a streamed .tgz as the live app with no trial, recording it as the OTA
|
|
29
|
+
* rollback baseline (.ota-last-good.tgz). Backs MIK_CMD_DEPLOY_BUILD: a deploy
|
|
30
|
+
* (not an OTA download) becomes the known-good build. Verifies the build
|
|
31
|
+
* against `checksum` (lowercase hex, may be empty to skip), installs it, sets
|
|
32
|
+
* state GOOD with no trial, and clears any pending build. Returns false with
|
|
33
|
+
* *err pointing at a static reason string on failure. */
|
|
34
|
+
bool mik__ota_adopt_build(const char* tgz_path, const char* checksum, const char** err);
|
|
35
|
+
|
|
36
|
+
/* True while the running build is an unconfirmed OTA trial (mik_ota.cpp). Lets
|
|
37
|
+
* the boot path treat a JS-level fatal during a trial as a failed trial. */
|
|
38
|
+
bool mik__ota_in_trial(void);
|
|
39
|
+
|
|
40
|
+
/* Record that the running trial build hit a fatal JS error (an uncaught
|
|
41
|
+
* exception or unhandled rejection), so the next reconcile reverts it even
|
|
42
|
+
* though the reboot looks like a clean software reset. No-op outside a trial.
|
|
43
|
+
* `detail` is a short message kept for the revert diagnostic. */
|
|
44
|
+
void mik__ota_note_trial_failure(const char* detail);
|
|
45
|
+
|
|
23
46
|
/* Safe-mode recovery window (mik_recovery.cpp).
|
|
24
47
|
* Opens a brief window (typ. 500ms) right after console init that watches
|
|
25
48
|
* for two triggers and returns true if either fires:
|
|
@@ -96,6 +96,11 @@ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
96
96
|
|
|
97
97
|
case MIK_CMD_CONFIG_SET: {
|
|
98
98
|
/* Payload: u8 flags | u16le key_len | key | u16le val_len | value */
|
|
99
|
+
if (payload_len < 3) {
|
|
100
|
+
mik__proto_drain(transport, payload_len);
|
|
101
|
+
mik__proto_send_err(transport, "config header too short");
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
99
104
|
uint8_t flags;
|
|
100
105
|
if (!mik__proto_read_exact(transport, &flags, 1)) return false;
|
|
101
106
|
|
|
@@ -104,26 +109,35 @@ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
104
109
|
uint16_t key_len = kl[0] | (kl[1] << 8);
|
|
105
110
|
|
|
106
111
|
char key[64];
|
|
107
|
-
if (key_len >= sizeof(key)) {
|
|
112
|
+
if (key_len >= sizeof(key) || (uint32_t)key_len + 3 > payload_len) {
|
|
108
113
|
mik__proto_drain(transport, payload_len - 3);
|
|
109
114
|
mik__proto_send_err(transport, "key too long");
|
|
110
115
|
return true;
|
|
111
116
|
}
|
|
112
117
|
if (!mik__proto_read_exact(transport, key, key_len)) return false;
|
|
113
118
|
key[key_len] = '\0';
|
|
119
|
+
uint32_t consumed = 3 + (uint32_t)key_len;
|
|
114
120
|
|
|
121
|
+
if (consumed + 2 > payload_len) {
|
|
122
|
+
mik__proto_drain(transport, payload_len - consumed);
|
|
123
|
+
mik__proto_send_err(transport, "config value header truncated");
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
115
126
|
uint8_t vl[2];
|
|
116
127
|
if (!mik__proto_read_exact(transport, vl, 2)) return false;
|
|
117
128
|
uint16_t val_len = vl[0] | (vl[1] << 8);
|
|
129
|
+
consumed += 2;
|
|
118
130
|
|
|
119
131
|
char value[512];
|
|
120
|
-
if (val_len >= sizeof(value)) {
|
|
121
|
-
mik__proto_drain(transport,
|
|
132
|
+
if (val_len >= sizeof(value) || consumed + (uint32_t)val_len > payload_len) {
|
|
133
|
+
mik__proto_drain(transport, payload_len - consumed);
|
|
122
134
|
mik__proto_send_err(transport, "value too long");
|
|
123
135
|
return true;
|
|
124
136
|
}
|
|
125
137
|
if (!mik__proto_read_exact(transport, value, val_len)) return false;
|
|
126
138
|
value[val_len] = '\0';
|
|
139
|
+
consumed += val_len;
|
|
140
|
+
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
127
141
|
|
|
128
142
|
if (key_len > 15) {
|
|
129
143
|
mik__proto_send_err(transport, "key exceeds NVS 15-char limit");
|
|
@@ -142,24 +156,164 @@ bool mik__handle_config_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
142
156
|
|
|
143
157
|
case MIK_CMD_CONFIG_DELETE: {
|
|
144
158
|
/* Payload: u16le key_len | key */
|
|
159
|
+
if (payload_len < 2) {
|
|
160
|
+
mik__proto_drain(transport, payload_len);
|
|
161
|
+
mik__proto_send_err(transport, "config header too short");
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
145
164
|
uint8_t kl[2];
|
|
146
165
|
if (!mik__proto_read_exact(transport, kl, 2)) return false;
|
|
147
166
|
uint16_t key_len = kl[0] | (kl[1] << 8);
|
|
148
167
|
|
|
149
168
|
char key[64];
|
|
150
|
-
if (key_len >= sizeof(key)) {
|
|
151
|
-
mik__proto_drain(transport,
|
|
169
|
+
if (key_len >= sizeof(key) || (uint32_t)key_len + 2 > payload_len) {
|
|
170
|
+
mik__proto_drain(transport, payload_len - 2);
|
|
152
171
|
mik__proto_send_err(transport, "key too long");
|
|
153
172
|
return true;
|
|
154
173
|
}
|
|
155
174
|
if (!mik__proto_read_exact(transport, key, key_len)) return false;
|
|
156
175
|
key[key_len] = '\0';
|
|
176
|
+
uint32_t consumed = 2 + (uint32_t)key_len;
|
|
177
|
+
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
157
178
|
|
|
158
179
|
uint8_t byte = mik__nvs_env_delete(key) ? 1 : 0;
|
|
159
180
|
mik__proto_send(transport, MIK_MSG_OK, &byte, 1);
|
|
160
181
|
return true;
|
|
161
182
|
}
|
|
162
183
|
|
|
184
|
+
case MIK_CMD_KV_SET: {
|
|
185
|
+
/* Payload: u8 ns | u16le key_len | key | u16le val_len | value.
|
|
186
|
+
* ns selects the namespace (0 = mik.kv, 1 = mik.sys). The value is
|
|
187
|
+
* stored as a CBOR text-string blob, matching what `mikro/kv/nvs`
|
|
188
|
+
* writes, so JS reads it back as a plain string. */
|
|
189
|
+
if (payload_len < 3) {
|
|
190
|
+
mik__proto_drain(transport, payload_len);
|
|
191
|
+
mik__proto_send_err(transport, "kv header too short");
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
uint8_t ns;
|
|
195
|
+
if (!mik__proto_read_exact(transport, &ns, 1)) return false;
|
|
196
|
+
if (ns > 1) {
|
|
197
|
+
mik__proto_drain(transport, payload_len - 1);
|
|
198
|
+
mik__proto_send_err(transport, "unknown kv namespace");
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
uint8_t kl[2];
|
|
203
|
+
if (!mik__proto_read_exact(transport, kl, 2)) return false;
|
|
204
|
+
uint16_t key_len = kl[0] | (kl[1] << 8);
|
|
205
|
+
|
|
206
|
+
char key[64];
|
|
207
|
+
if (key_len >= sizeof(key) || (uint32_t)key_len + 3 > payload_len) {
|
|
208
|
+
mik__proto_drain(transport, payload_len - 3);
|
|
209
|
+
mik__proto_send_err(transport, "key too long");
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (!mik__proto_read_exact(transport, key, key_len)) return false;
|
|
213
|
+
key[key_len] = '\0';
|
|
214
|
+
uint32_t consumed = 3 + (uint32_t)key_len;
|
|
215
|
+
|
|
216
|
+
if (consumed + 2 > payload_len) {
|
|
217
|
+
mik__proto_drain(transport, payload_len - consumed);
|
|
218
|
+
mik__proto_send_err(transport, "kv value header truncated");
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
uint8_t vl[2];
|
|
222
|
+
if (!mik__proto_read_exact(transport, vl, 2)) return false;
|
|
223
|
+
uint16_t val_len = vl[0] | (vl[1] << 8);
|
|
224
|
+
consumed += 2;
|
|
225
|
+
|
|
226
|
+
char value[512];
|
|
227
|
+
if (val_len >= sizeof(value) || consumed + (uint32_t)val_len > payload_len) {
|
|
228
|
+
mik__proto_drain(transport, payload_len - consumed);
|
|
229
|
+
mik__proto_send_err(transport, "value too long");
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (!mik__proto_read_exact(transport, value, val_len)) return false;
|
|
233
|
+
value[val_len] = '\0';
|
|
234
|
+
consumed += val_len;
|
|
235
|
+
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
236
|
+
|
|
237
|
+
if (key_len == 0 || key_len > 15) {
|
|
238
|
+
mik__proto_send_err(transport, "key exceeds NVS 15-char limit");
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
nanocbor_encoder_t enc;
|
|
243
|
+
uint8_t blob[sizeof(value) + 4];
|
|
244
|
+
nanocbor_encoder_init(&enc, blob, sizeof(blob));
|
|
245
|
+
if (nanocbor_put_tstrn(&enc, value, val_len) < 0) {
|
|
246
|
+
mik__proto_send_err(transport, "cbor encode failed");
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
size_t blob_len = nanocbor_encoded_len(&enc);
|
|
250
|
+
|
|
251
|
+
nvs_handle_t handle;
|
|
252
|
+
esp_err_t err = nvs_open(ns == 1 ? "mik.sys" : "mik.kv", NVS_READWRITE, &handle);
|
|
253
|
+
if (err == ESP_OK) {
|
|
254
|
+
err = nvs_set_blob(handle, key, blob, blob_len);
|
|
255
|
+
if (err == ESP_OK) err = nvs_commit(handle);
|
|
256
|
+
nvs_close(handle);
|
|
257
|
+
}
|
|
258
|
+
if (err == ESP_OK) {
|
|
259
|
+
mik__proto_send_ok(transport);
|
|
260
|
+
} else {
|
|
261
|
+
mik__proto_send_err(transport, "nvs write failed");
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case MIK_CMD_KV_DELETE: {
|
|
267
|
+
/* Payload: u8 ns | u16le key_len | key */
|
|
268
|
+
if (payload_len < 3) {
|
|
269
|
+
mik__proto_drain(transport, payload_len);
|
|
270
|
+
mik__proto_send_err(transport, "kv header too short");
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
uint8_t ns;
|
|
274
|
+
if (!mik__proto_read_exact(transport, &ns, 1)) return false;
|
|
275
|
+
if (ns > 1) {
|
|
276
|
+
mik__proto_drain(transport, payload_len - 1);
|
|
277
|
+
mik__proto_send_err(transport, "unknown kv namespace");
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
uint8_t kl[2];
|
|
282
|
+
if (!mik__proto_read_exact(transport, kl, 2)) return false;
|
|
283
|
+
uint16_t key_len = kl[0] | (kl[1] << 8);
|
|
284
|
+
|
|
285
|
+
char key[64];
|
|
286
|
+
if (key_len >= sizeof(key) || (uint32_t)key_len + 3 > payload_len) {
|
|
287
|
+
mik__proto_drain(transport, payload_len - 3);
|
|
288
|
+
mik__proto_send_err(transport, "key too long");
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (!mik__proto_read_exact(transport, key, key_len)) return false;
|
|
292
|
+
key[key_len] = '\0';
|
|
293
|
+
uint32_t consumed = 3 + (uint32_t)key_len;
|
|
294
|
+
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
295
|
+
|
|
296
|
+
bool existed = false;
|
|
297
|
+
esp_err_t err = ESP_FAIL;
|
|
298
|
+
nvs_handle_t handle;
|
|
299
|
+
if (nvs_open(ns == 1 ? "mik.sys" : "mik.kv", NVS_READWRITE, &handle) == ESP_OK) {
|
|
300
|
+
size_t len = 0;
|
|
301
|
+
existed = nvs_get_blob(handle, key, nullptr, &len) == ESP_OK;
|
|
302
|
+
err = nvs_erase_key(handle, key);
|
|
303
|
+
/* Deleting a key that was never there is a successful no-op. */
|
|
304
|
+
if (err == ESP_ERR_NVS_NOT_FOUND) err = ESP_OK;
|
|
305
|
+
if (err == ESP_OK) err = nvs_commit(handle);
|
|
306
|
+
nvs_close(handle);
|
|
307
|
+
}
|
|
308
|
+
if (err != ESP_OK) {
|
|
309
|
+
mik__proto_send_err(transport, "nvs delete failed");
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
uint8_t byte = existed ? 1 : 0;
|
|
313
|
+
mik__proto_send(transport, MIK_MSG_OK, &byte, 1);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
163
317
|
default:
|
|
164
318
|
mik__proto_drain(transport, payload_len);
|
|
165
319
|
return false;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
#include <freertos/FreeRTOS.h>
|
|
10
10
|
#include <freertos/task.h>
|
|
11
11
|
|
|
12
|
+
#include "mikrojs/app_store.h"
|
|
12
13
|
#include "mikrojs/mikrojs.h"
|
|
13
14
|
#include "mikrojs/platform.h"
|
|
14
15
|
#include "mikrojs/private.h"
|
|
@@ -16,9 +17,11 @@
|
|
|
16
17
|
|
|
17
18
|
static const char* FS_BASE = "/appfs";
|
|
18
19
|
static const char* DEPLOY_TMP = "/appfs/.deploy-tmp";
|
|
19
|
-
static const char* DEPLOY_OLD = "/appfs/.deploy-old";
|
|
20
20
|
static const char* APP_DIR = "/appfs/app";
|
|
21
21
|
static const char* CHECKSUMS_PATH = "/appfs/app/.checksums";
|
|
22
|
+
/* Where DEPLOY_PUT("/.build.tgz", ...) streams the OTA build (DEPLOY_TMP +
|
|
23
|
+
* "/.build.tgz"). Adopted by MIK_CMD_DEPLOY_BUILD. */
|
|
24
|
+
static const char* BUILD_TGZ_PATH = "/appfs/.deploy-tmp/.build.tgz";
|
|
22
25
|
|
|
23
26
|
/* ── Checksums manifest ─────────────────────────────────────────── */
|
|
24
27
|
|
|
@@ -239,19 +242,7 @@ static bool copy_file(const char* src, const char* dst) {
|
|
|
239
242
|
/* ── Deploy recovery ─────────────────────────────────────────────── */
|
|
240
243
|
|
|
241
244
|
void MIK_DeployRecover(void) {
|
|
242
|
-
|
|
243
|
-
bool has_old = path_exists(DEPLOY_OLD);
|
|
244
|
-
bool has_tmp = path_exists(DEPLOY_TMP);
|
|
245
|
-
|
|
246
|
-
if (!has_app && has_old) {
|
|
247
|
-
rename(DEPLOY_OLD, APP_DIR);
|
|
248
|
-
} else if (has_old) {
|
|
249
|
-
rmdir_recursive(DEPLOY_OLD);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (has_tmp) {
|
|
253
|
-
rmdir_recursive(DEPLOY_TMP);
|
|
254
|
-
}
|
|
245
|
+
mik__app_recover(FS_BASE);
|
|
255
246
|
}
|
|
256
247
|
|
|
257
248
|
/* ── Unified protocol deploy handler ────────────────────────────── */
|
|
@@ -540,36 +531,64 @@ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
540
531
|
}
|
|
541
532
|
|
|
542
533
|
/* Atomic swap: staging dir → app dir */
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return true;
|
|
549
|
-
}
|
|
534
|
+
MIKAppCommitResult commit = mik__app_commit(FS_BASE, s_deploy_erased);
|
|
535
|
+
if (commit == MIK_APP_COMMIT_STASH_FAILED) {
|
|
536
|
+
mik__proto_send_err(transport, "rename app to old failed");
|
|
537
|
+
deploy_cleanup();
|
|
538
|
+
return true;
|
|
550
539
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (rename(staged_app, APP_DIR) != 0) {
|
|
556
|
-
if (path_exists(DEPLOY_OLD)) {
|
|
557
|
-
rename(DEPLOY_OLD, APP_DIR);
|
|
558
|
-
}
|
|
559
|
-
mik__proto_send_err(transport, "rename staged to app failed");
|
|
560
|
-
deploy_cleanup();
|
|
561
|
-
return true;
|
|
562
|
-
}
|
|
540
|
+
if (commit == MIK_APP_COMMIT_SWAP_FAILED) {
|
|
541
|
+
mik__proto_send_err(transport, "rename staged to app failed");
|
|
542
|
+
deploy_cleanup();
|
|
543
|
+
return true;
|
|
563
544
|
}
|
|
564
545
|
|
|
565
|
-
rmdir_recursive(DEPLOY_OLD);
|
|
566
|
-
rmdir_recursive(DEPLOY_TMP);
|
|
567
546
|
stamp_checksums_manifest();
|
|
568
547
|
mik__proto_send_ok(transport);
|
|
569
548
|
deploy_cleanup();
|
|
570
549
|
return true;
|
|
571
550
|
}
|
|
572
551
|
|
|
552
|
+
case MIK_CMD_DEPLOY_BUILD: {
|
|
553
|
+
/* Adopt a streamed .tgz as the live app via the OTA install path,
|
|
554
|
+
* establishing the rollback baseline. The build was streamed via
|
|
555
|
+
* PUT("/.build.tgz") + chunks into BUILD_TGZ_PATH.
|
|
556
|
+
* Payload: u16le checksum_len | checksum. */
|
|
557
|
+
if (payload_len < 2) {
|
|
558
|
+
mik__proto_drain(transport, payload_len);
|
|
559
|
+
mik__proto_send_err(transport, "build header too short");
|
|
560
|
+
deploy_cleanup();
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
uint8_t cl[2];
|
|
564
|
+
if (!mik__proto_read_exact(transport, cl, 2)) return false;
|
|
565
|
+
uint16_t chk_len = cl[0] | (cl[1] << 8);
|
|
566
|
+
|
|
567
|
+
char checksum[96];
|
|
568
|
+
if (chk_len >= sizeof(checksum) || (uint32_t)chk_len + 2 > payload_len) {
|
|
569
|
+
mik__proto_drain(transport, payload_len - 2);
|
|
570
|
+
mik__proto_send_err(transport, "build checksum too long");
|
|
571
|
+
deploy_cleanup();
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
if (!mik__proto_read_exact(transport, checksum, chk_len)) return false;
|
|
575
|
+
checksum[chk_len] = '\0';
|
|
576
|
+
uint32_t consumed = 2 + (uint32_t)chk_len;
|
|
577
|
+
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
578
|
+
|
|
579
|
+
/* Flush the staged .tgz to disk before adopt reads it back. */
|
|
580
|
+
put_close_and_clear();
|
|
581
|
+
|
|
582
|
+
const char* err = nullptr;
|
|
583
|
+
if (mik__ota_adopt_build(BUILD_TGZ_PATH, checksum, &err)) {
|
|
584
|
+
mik__proto_send_ok(transport);
|
|
585
|
+
} else {
|
|
586
|
+
mik__proto_send_err(transport, err ? err : "build install failed");
|
|
587
|
+
}
|
|
588
|
+
deploy_cleanup();
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
573
592
|
case MIK_CMD_DEPLOY_ABORT:
|
|
574
593
|
mik__proto_drain(transport, payload_len);
|
|
575
594
|
rmdir_recursive(DEPLOY_TMP);
|
|
@@ -21,6 +21,41 @@
|
|
|
21
21
|
|
|
22
22
|
static const char* TAG = "mikrojs";
|
|
23
23
|
|
|
24
|
+
/* Error handler armed during a normal boot: when the app hits a fatal JS error
|
|
25
|
+
* (uncaught exception or unhandled rejection) while running an OTA trial build,
|
|
26
|
+
* flag the trial so the next reconcile reverts it. A JS error reboots as a clean
|
|
27
|
+
* software reset, which the trial state machine can't otherwise tell from a
|
|
28
|
+
* healthy restart. No-op outside a trial. */
|
|
29
|
+
static void ota_trial_error_handler(JSContext* ctx, JSValue error, void* /*opaque*/) {
|
|
30
|
+
if (!mik__ota_in_trial()) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
char detail[160] = "uncaught error";
|
|
34
|
+
if (JS_IsObject(error)) {
|
|
35
|
+
JSValue name_v = JS_GetPropertyStr(ctx, error, "name");
|
|
36
|
+
JSValue msg_v = JS_GetPropertyStr(ctx, error, "message");
|
|
37
|
+
const char* name = JS_ToCString(ctx, name_v);
|
|
38
|
+
const char* msg = JS_ToCString(ctx, msg_v);
|
|
39
|
+
if (name && name[0] && msg && msg[0]) {
|
|
40
|
+
snprintf(detail, sizeof(detail), "%s: %s", name, msg);
|
|
41
|
+
} else if (msg && msg[0]) {
|
|
42
|
+
snprintf(detail, sizeof(detail), "%s", msg);
|
|
43
|
+
} else if (name && name[0]) {
|
|
44
|
+
snprintf(detail, sizeof(detail), "%s", name);
|
|
45
|
+
}
|
|
46
|
+
/* A throwing getter (or a toString that throws) leaves an exception
|
|
47
|
+
* pending on ctx that nothing downstream would ever clear. */
|
|
48
|
+
if (!name || !msg) {
|
|
49
|
+
JS_FreeValue(ctx, JS_GetException(ctx));
|
|
50
|
+
}
|
|
51
|
+
if (name) JS_FreeCString(ctx, name);
|
|
52
|
+
if (msg) JS_FreeCString(ctx, msg);
|
|
53
|
+
JS_FreeValue(ctx, name_v);
|
|
54
|
+
JS_FreeValue(ctx, msg_v);
|
|
55
|
+
}
|
|
56
|
+
mik__ota_note_trial_failure(detail);
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
/* Apply the MIK_LOG_LEVEL env var (if set in NVS) to our log tags.
|
|
25
60
|
*
|
|
26
61
|
* The firmware compiles in all levels up to DEBUG
|
|
@@ -163,8 +198,9 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
|
|
|
163
198
|
return true;
|
|
164
199
|
}
|
|
165
200
|
|
|
166
|
-
/* Deploy commands (0x20-0x27) */
|
|
167
|
-
if (cmd_type >= MIK_CMD_DEPLOY_PUT && cmd_type <= MIK_CMD_DEPLOY_CHECKSUM)
|
|
201
|
+
/* Deploy commands (0x20-0x27) plus the build-install command (0x2D) */
|
|
202
|
+
if ((cmd_type >= MIK_CMD_DEPLOY_PUT && cmd_type <= MIK_CMD_DEPLOY_CHECKSUM) ||
|
|
203
|
+
cmd_type == MIK_CMD_DEPLOY_BUILD) {
|
|
168
204
|
return mik__handle_deploy_command(transport, cmd_type, payload_len);
|
|
169
205
|
}
|
|
170
206
|
|
|
@@ -181,8 +217,8 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
|
|
|
181
217
|
return true;
|
|
182
218
|
}
|
|
183
219
|
|
|
184
|
-
/* Config commands (0x40-
|
|
185
|
-
if (cmd_type >= MIK_CMD_CONFIG_LIST && cmd_type <=
|
|
220
|
+
/* Config + kv provisioning commands (0x40-0x44) */
|
|
221
|
+
if (cmd_type >= MIK_CMD_CONFIG_LIST && cmd_type <= MIK_CMD_KV_DELETE) {
|
|
186
222
|
return mik__handle_config_command(transport, cmd_type, payload_len);
|
|
187
223
|
}
|
|
188
224
|
|
|
@@ -291,6 +327,20 @@ void MIK_Main(void) {
|
|
|
291
327
|
/* Recover from incomplete deploys */
|
|
292
328
|
MIK_DeployRecover();
|
|
293
329
|
|
|
330
|
+
/* Reconcile OTA state before the JS app loads: reflash guard, trial
|
|
331
|
+
* verdict, and a deferred clean-heap install of a staged build (may
|
|
332
|
+
* restart). Runs after DeployRecover so an interrupted unpack's staging
|
|
333
|
+
* dir is already cleaned.
|
|
334
|
+
*
|
|
335
|
+
* Skipped in safe mode. The install budget bounds a crashing install, but
|
|
336
|
+
* safe mode is what a developer reaches for while it is still burning
|
|
337
|
+
* through that budget, and reconcile runs long before the REPL comes up:
|
|
338
|
+
* without this, the recovery window opens onto an install that panics
|
|
339
|
+
* before anything can be typed into it. */
|
|
340
|
+
if (!safe_mode) {
|
|
341
|
+
mik__ota_boot_reconcile();
|
|
342
|
+
}
|
|
343
|
+
|
|
294
344
|
/* Load app config (mikro.config.json) if present */
|
|
295
345
|
MIKConfig app_config;
|
|
296
346
|
MIK_LoadConfig("/appfs", &app_config);
|
|
@@ -565,13 +615,24 @@ void MIK_Main(void) {
|
|
|
565
615
|
if (safe_mode) {
|
|
566
616
|
ESP_LOGW(TAG, "Safe mode: skipping entry point %s", app_config.entry_point);
|
|
567
617
|
} else {
|
|
568
|
-
|
|
618
|
+
/* Catch async fatals (unhandled rejections) during an OTA trial. */
|
|
619
|
+
MIK_SetErrorHandler(mik_rt, ota_trial_error_handler, nullptr);
|
|
620
|
+
char entry_err[160] = {0};
|
|
621
|
+
int rc = MIK_RunEntryErr(mik_rt, app_config.entry_point, entry_err, sizeof(entry_err));
|
|
569
622
|
if (rc == -EINVAL) {
|
|
570
623
|
ESP_LOGW(TAG, "No entry point configured (no \"main\" field in package.json)");
|
|
571
624
|
} else if (rc == -ENOENT) {
|
|
572
625
|
ESP_LOGW(TAG, "Entry point not found: %s", app_config.entry_point);
|
|
573
626
|
} else if (rc == -EFAULT) {
|
|
574
627
|
ESP_LOGE(TAG, "Failed to evaluate %s", app_config.entry_point);
|
|
628
|
+
/* A synchronous top-level throw doesn't reach the rejection handler
|
|
629
|
+
* above. If this is a trial build, flag it and arm the panic restart
|
|
630
|
+
* so reconcile reverts on the next (clean-looking) boot. */
|
|
631
|
+
if (mik__ota_in_trial()) {
|
|
632
|
+
mik__ota_note_trial_failure(entry_err[0] ? entry_err
|
|
633
|
+
: "entry failed to evaluate");
|
|
634
|
+
MIK_Stop(mik_rt);
|
|
635
|
+
}
|
|
575
636
|
}
|
|
576
637
|
}
|
|
577
638
|
}
|