@mikrojs/firmware 0.17.0-pr-276.20260721233726 → 0.17.0
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 +21 -1
- package/components/mikrojs/include/mikrojs_esp32.h +21 -6
- package/components/mikrojs/mik_deploy.cpp +10 -7
- package/components/mikrojs/mik_main.cpp +24 -1
- package/components/mikrojs/mik_ota.cpp +169 -74
- package/index.d.ts +1 -0
- package/index.js +13 -1
- package/package.json +3 -3
- package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32/firmware.json +1 -0
- package/prebuilds/esp32/mikrojs.bin +0 -0
- package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c3/firmware.json +1 -0
- package/prebuilds/esp32c3/mikrojs.bin +0 -0
- package/prebuilds/esp32c5/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c5/firmware.json +1 -0
- package/prebuilds/esp32c5/mikrojs.bin +0 -0
- package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32c6/firmware.json +1 -0
- package/prebuilds/esp32c6/mikrojs.bin +0 -0
- package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
- package/prebuilds/esp32s3/firmware.json +1 -0
- package/prebuilds/esp32s3/mikrojs.bin +0 -0
- package/project.cmake +46 -4
- package/resolve.js +10 -0
|
@@ -134,6 +134,26 @@ target_compile_definitions(${COMPONENT_LIB} PRIVATE "MIK_PACKAGE_NAME=\"mikro\""
|
|
|
134
134
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
|
135
135
|
"${CMAKE_CURRENT_LIST_DIR}/../../package.json")
|
|
136
136
|
|
|
137
|
+
# Firmware identity: the consuming project's package.json name, reported in
|
|
138
|
+
# MSG_READY as `fw`. The CLI only auto-flashes its bundled prebuilt over a
|
|
139
|
+
# device whose identity matches the prebuilt's own recorded name, so custom
|
|
140
|
+
# firmware is protected from the auto-reflash without any project marker.
|
|
141
|
+
# Absent (no package.json, e.g. on-device test apps) the field is omitted.
|
|
142
|
+
execute_process(
|
|
143
|
+
COMMAND node ${_MIK_RESOLVE} projectName ${CMAKE_SOURCE_DIR}
|
|
144
|
+
OUTPUT_VARIABLE _MIK_FW_NAME
|
|
145
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
146
|
+
)
|
|
147
|
+
# STREQUAL, not if(_MIK_FW_NAME): a project named "0", "off", or "false"
|
|
148
|
+
# would read as a false constant and silently lose its identity.
|
|
149
|
+
if(NOT _MIK_FW_NAME STREQUAL "")
|
|
150
|
+
target_compile_definitions(${COMPONENT_LIB} PRIVATE "MIK_FW_NAME=\"${_MIK_FW_NAME}\"")
|
|
151
|
+
endif()
|
|
152
|
+
if(EXISTS "${CMAKE_SOURCE_DIR}/package.json")
|
|
153
|
+
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
|
154
|
+
"${CMAKE_SOURCE_DIR}/package.json")
|
|
155
|
+
endif()
|
|
156
|
+
|
|
137
157
|
# UTC build timestamp. `string(TIMESTAMP ... UTC)` honors SOURCE_DATE_EPOCH
|
|
138
158
|
# when set. Refreshes on re-configure only (idf.py triggers this on each
|
|
139
159
|
# build via its own reconfigure step, so in practice it's close to
|
|
@@ -151,7 +171,7 @@ include("${MIK_BYTECODE_CMAKE}")
|
|
|
151
171
|
|
|
152
172
|
# Force linker to include self-registering native modules
|
|
153
173
|
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)
|
|
174
|
+
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 pin pwm reader sleep spi sntp stdio stream sys test uart udp wifi)
|
|
155
175
|
if(CONFIG_BT_ENABLED)
|
|
156
176
|
list(APPEND _MIK_MODULES ble)
|
|
157
177
|
list(APPEND _MIK_BYTECODE_MODULES ble)
|
|
@@ -25,13 +25,28 @@ void MIK_DeployRecover(void);
|
|
|
25
25
|
* (may esp_restart). Call right after MIK_DeployRecover(). */
|
|
26
26
|
void mik__ota_boot_reconcile(void);
|
|
27
27
|
|
|
28
|
-
/*
|
|
29
|
-
* rollback baseline
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
/* Stage a streamed .tgz for install at the next boot with adopt semantics:
|
|
29
|
+
* no trial, no rollback baseline, one install attempt. Backs
|
|
30
|
+
* MIK_CMD_DEPLOY_BUILD. Verifies the build against `checksum` (lowercase hex,
|
|
31
|
+
* may be empty to skip) so a corrupt upload still fails synchronously over
|
|
32
|
+
* serial, then arms the boot reconcile's adopt install. Returns false with
|
|
33
33
|
* *err pointing at a static reason string on failure. */
|
|
34
|
-
bool
|
|
34
|
+
bool mik__ota_stage_adopt(const char* tgz_path, const char* checksum, const char** err);
|
|
35
|
+
|
|
36
|
+
/* Outcome of the last adopt-mode boot install, for MIK_CMD_DEPLOY_RESULT.
|
|
37
|
+
* Kept separate from the reconcile record on purpose: that record reaches the
|
|
38
|
+
* registry as `lastInstall` and would blacklist a checksum a developer is
|
|
39
|
+
* about to legitimately re-push over the cable. */
|
|
40
|
+
typedef struct MIKDeployResult {
|
|
41
|
+
uint8_t status; /* 0 none, 1 ok, 2 fail */
|
|
42
|
+
char checksum[96];
|
|
43
|
+
char reason[32];
|
|
44
|
+
char detail[160];
|
|
45
|
+
} MIKDeployResult;
|
|
46
|
+
|
|
47
|
+
/* Read and clear the recorded adopt-install outcome (mik_ota.cpp). status 0
|
|
48
|
+
* with empty strings when nothing is recorded. */
|
|
49
|
+
void mik__ota_take_deploy_result(MIKDeployResult* out);
|
|
35
50
|
|
|
36
51
|
/* True while the running build is an unconfirmed OTA trial (mik_ota.cpp). Lets
|
|
37
52
|
* the boot path treat a JS-level fatal during a trial as a failed trial. */
|
|
@@ -20,7 +20,7 @@ static const char* DEPLOY_TMP = "/appfs/.deploy-tmp";
|
|
|
20
20
|
static const char* APP_DIR = "/appfs/app";
|
|
21
21
|
static const char* CHECKSUMS_PATH = "/appfs/app/.checksums";
|
|
22
22
|
/* Where DEPLOY_PUT("/.build.tgz", ...) streams the OTA build (DEPLOY_TMP +
|
|
23
|
-
* "/.build.tgz").
|
|
23
|
+
* "/.build.tgz"). Staged for boot install by MIK_CMD_DEPLOY_BUILD. */
|
|
24
24
|
static const char* BUILD_TGZ_PATH = "/appfs/.deploy-tmp/.build.tgz";
|
|
25
25
|
|
|
26
26
|
/* ── Checksums manifest ─────────────────────────────────────────── */
|
|
@@ -550,9 +550,12 @@ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
550
550
|
}
|
|
551
551
|
|
|
552
552
|
case MIK_CMD_DEPLOY_BUILD: {
|
|
553
|
-
/*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
553
|
+
/* Stage a streamed .tgz for install at the next boot. The install
|
|
554
|
+
* runs from the boot reconcile on a clean heap; installing here
|
|
555
|
+
* with the app live needs a 32 KB contiguous inflate window a
|
|
556
|
+
* fragmented heap cannot provide. The checksum is verified now so
|
|
557
|
+
* a corrupt upload still fails synchronously. The build was
|
|
558
|
+
* streamed via PUT("/.build.tgz") + chunks into BUILD_TGZ_PATH.
|
|
556
559
|
* Payload: u16le checksum_len | checksum. */
|
|
557
560
|
if (payload_len < 2) {
|
|
558
561
|
mik__proto_drain(transport, payload_len);
|
|
@@ -576,14 +579,14 @@ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
|
|
|
576
579
|
uint32_t consumed = 2 + (uint32_t)chk_len;
|
|
577
580
|
if (payload_len > consumed) mik__proto_drain(transport, payload_len - consumed);
|
|
578
581
|
|
|
579
|
-
/* Flush the staged .tgz to disk before
|
|
582
|
+
/* Flush the staged .tgz to disk before staging verifies it. */
|
|
580
583
|
put_close_and_clear();
|
|
581
584
|
|
|
582
585
|
const char* err = nullptr;
|
|
583
|
-
if (
|
|
586
|
+
if (mik__ota_stage_adopt(BUILD_TGZ_PATH, checksum, &err)) {
|
|
584
587
|
mik__proto_send_ok(transport);
|
|
585
588
|
} else {
|
|
586
|
-
mik__proto_send_err(transport, err ? err : "build
|
|
589
|
+
mik__proto_send_err(transport, err ? err : "build stage failed");
|
|
587
590
|
}
|
|
588
591
|
deploy_cleanup();
|
|
589
592
|
return true;
|
|
@@ -198,12 +198,35 @@ static bool platform_command_handler(MIKReplTransport* transport, uint8_t cmd_ty
|
|
|
198
198
|
return true;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/* Deploy commands (0x20-0x27) plus the build-
|
|
201
|
+
/* Deploy commands (0x20-0x27) plus the build-stage command (0x2D) */
|
|
202
202
|
if ((cmd_type >= MIK_CMD_DEPLOY_PUT && cmd_type <= MIK_CMD_DEPLOY_CHECKSUM) ||
|
|
203
203
|
cmd_type == MIK_CMD_DEPLOY_BUILD) {
|
|
204
204
|
return mik__handle_deploy_command(transport, cmd_type, payload_len);
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/* Install outcome (0x2E): report + clear the result of the last staged
|
|
208
|
+
* build install. Handled here, not in the deploy handler: it is a
|
|
209
|
+
* standalone read after the post-deploy reboot and must not start a
|
|
210
|
+
* deploy session (pause the runtime, clear the staging dir). */
|
|
211
|
+
if (cmd_type == MIK_CMD_DEPLOY_RESULT) {
|
|
212
|
+
mik__proto_drain(transport, payload_len);
|
|
213
|
+
MIKDeployResult res;
|
|
214
|
+
mik__ota_take_deploy_result(&res);
|
|
215
|
+
uint8_t buf[1 + 3 * 2 + sizeof(res.checksum) + sizeof(res.reason) + sizeof(res.detail)];
|
|
216
|
+
size_t n = 0;
|
|
217
|
+
buf[n++] = res.status;
|
|
218
|
+
const char* fields[] = {res.checksum, res.reason, res.detail};
|
|
219
|
+
for (const char* s : fields) {
|
|
220
|
+
size_t len = strlen(s);
|
|
221
|
+
buf[n++] = (uint8_t)(len & 0xFF);
|
|
222
|
+
buf[n++] = (uint8_t)((len >> 8) & 0xFF);
|
|
223
|
+
memcpy(buf + n, s, len);
|
|
224
|
+
n += len;
|
|
225
|
+
}
|
|
226
|
+
mik__proto_send(transport, MIK_MSG_OK, buf, n);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
207
230
|
/* File pull (0x2B) */
|
|
208
231
|
if (cmd_type == MIK_CMD_FS_GET) {
|
|
209
232
|
return mik__handle_fs_get(transport, payload_len);
|
|
@@ -696,6 +696,7 @@ constexpr const char* kKeyTrialLeft = "trialLeft"; // u8
|
|
|
696
696
|
constexpr const char* kKeyTrialN = "trialN"; // u8: trial budget for a pending install
|
|
697
697
|
constexpr const char* kKeyConfirm = "confirm"; // u8: trial requires markValid() to promote
|
|
698
698
|
constexpr const char* kKeyPending = "pending"; // u8: 1 = .ota-pending.tgz awaits clean-heap install
|
|
699
|
+
constexpr const char* kKeyPendMode = "pendMode"; // u8: how to install the pending build (see below)
|
|
699
700
|
constexpr const char* kKeyInstLeft = "instLeft"; // u8: install-attempt budget
|
|
700
701
|
constexpr const char* kKeyPromLeft = "promLeft"; // u8: promote-attempt budget
|
|
701
702
|
constexpr const char* kKeyRbLeft = "rbLeft"; // u8: rollback-attempt budget
|
|
@@ -712,9 +713,22 @@ constexpr const char* kKeyOInst = "oInst"; // str: checksum just installe
|
|
|
712
713
|
constexpr const char* kKeyORevert = "oRevert"; // u8
|
|
713
714
|
constexpr const char* kKeyORsn = "oRsn"; // str: diagnostic reason
|
|
714
715
|
constexpr const char* kKeyODetail = "oDetail"; // str: diagnostic detail
|
|
716
|
+
// Adopt-install outcome, read once by MIK_CMD_DEPLOY_RESULT and then cleared.
|
|
717
|
+
// Separate from the reconcile record above: that one reaches the registry as
|
|
718
|
+
// `lastInstall` and would blacklist a checksum the developer is re-pushing.
|
|
719
|
+
constexpr const char* kKeyDRes = "dRes"; // u8: 1 ok, 2 fail
|
|
720
|
+
constexpr const char* kKeyDChk = "dChk"; // str: staged checksum
|
|
721
|
+
constexpr const char* kKeyDRsn = "dRsn"; // str: failure reason
|
|
722
|
+
constexpr const char* kKeyDDetail = "dDetail"; // str: failure detail
|
|
715
723
|
|
|
716
724
|
constexpr uint8_t kStateGood = 0;
|
|
717
725
|
constexpr uint8_t kStateTrial = 1;
|
|
726
|
+
// kKeyPendMode values. Trial is the OTA path (trialBoots, rollback, abandon
|
|
727
|
+
// policy); adopt is the cable-deploy path (straight to GOOD, no trial, no
|
|
728
|
+
// rollback baseline, one install attempt). Absent defaults to trial so a
|
|
729
|
+
// pending build staged by older firmware keeps its semantics.
|
|
730
|
+
constexpr uint8_t kPendModeTrial = 0;
|
|
731
|
+
constexpr uint8_t kPendModeAdopt = 1;
|
|
718
732
|
constexpr uint8_t kInstallBudget = 3; // boot install attempts before abandoning a pending build
|
|
719
733
|
// Boot attempts to make a survived trial the revert target before giving up on
|
|
720
734
|
// the revert target rather than on the device: a trial that cannot promote and
|
|
@@ -867,37 +881,6 @@ bool nvs_str(nvs_handle_t h, const char* key, char* buf, size_t cap) {
|
|
|
867
881
|
return buf[0] != 0;
|
|
868
882
|
}
|
|
869
883
|
|
|
870
|
-
// Copy src to dst (truncating dst). Used to move the staged build to the
|
|
871
|
-
// rollback slot before install_build rmtrees the staging tree it lives under.
|
|
872
|
-
bool copy_file(const char* src, const char* dst) {
|
|
873
|
-
FILE* in = fopen(src, "rb");
|
|
874
|
-
if (!in) return false;
|
|
875
|
-
FILE* out = fopen(dst, "wb");
|
|
876
|
-
if (!out) {
|
|
877
|
-
fclose(in);
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
unsigned char* buf = (unsigned char*)malloc(kChunk);
|
|
881
|
-
if (!buf) {
|
|
882
|
-
fclose(in);
|
|
883
|
-
fclose(out);
|
|
884
|
-
return false;
|
|
885
|
-
}
|
|
886
|
-
bool ok = true;
|
|
887
|
-
size_t n;
|
|
888
|
-
while ((n = fread(buf, 1, kChunk, in)) > 0) {
|
|
889
|
-
if (fwrite(buf, 1, n, out) != n) {
|
|
890
|
-
ok = false;
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (ok && ferror(in)) ok = false;
|
|
895
|
-
free(buf);
|
|
896
|
-
if (fclose(out) != 0) ok = false;
|
|
897
|
-
fclose(in);
|
|
898
|
-
return ok;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
884
|
// ── install (gunzip -> untar -> atomic swap) ─────────────────────────────────
|
|
902
885
|
// Unpacks `tgz` into <base>/.deploy-tmp/app and promotes it to /app via the
|
|
903
886
|
// shared app-store engine (crash-safe: an interrupted swap is rolled back by
|
|
@@ -1197,6 +1180,9 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
|
|
|
1197
1180
|
nvs_erase_key(h, kKeyStgChk);
|
|
1198
1181
|
nvs_erase_key(h, kKeyStgSize);
|
|
1199
1182
|
nvs_set_str(h, kKeyPendChk, want_chk);
|
|
1183
|
+
// Explicit even though trial is the default: a leftover adopt marker from
|
|
1184
|
+
// an earlier cable deploy must not turn this OTA build into an adopt.
|
|
1185
|
+
nvs_set_u8(h, kKeyPendMode, kPendModeTrial);
|
|
1200
1186
|
nvs_set_u8(h, kKeyTrialN, (uint8_t)trial_boots);
|
|
1201
1187
|
nvs_set_u8(h, kKeyConfirm, require_confirm ? 1 : 0);
|
|
1202
1188
|
|
|
@@ -1477,21 +1463,37 @@ bool attempt_rollback(nvs_handle_t h, const char* reason, const char* detail) {
|
|
|
1477
1463
|
return true;
|
|
1478
1464
|
}
|
|
1479
1465
|
|
|
1466
|
+
// Resolve a failed adopt install: drop the pending build, record the outcome
|
|
1467
|
+
// for MIK_CMD_DEPLOY_RESULT, and leave the previous /app running. No rollback
|
|
1468
|
+
// and no retry -- the developer is on the serial cable and re-pushes. Never
|
|
1469
|
+
// touches the reconcile record (oRsn/oDetail), which reaches the registry.
|
|
1470
|
+
void adopt_fail(nvs_handle_t h, const char* chk, const char* reason, const char* detail) {
|
|
1471
|
+
nvs_set_u8(h, kKeyPending, 0);
|
|
1472
|
+
nvs_erase_key(h, kKeyPendChk);
|
|
1473
|
+
nvs_erase_key(h, kKeyPendMode);
|
|
1474
|
+
nvs_erase_key(h, kKeyInstLeft);
|
|
1475
|
+
unlink(kPending);
|
|
1476
|
+
nvs_set_u8(h, kKeyDRes, 2);
|
|
1477
|
+
nvs_set_str(h, kKeyDChk, chk);
|
|
1478
|
+
nvs_set_str(h, kKeyDRsn, reason);
|
|
1479
|
+
nvs_set_str(h, kKeyDDetail, detail);
|
|
1480
|
+
nvs_commit(h);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1480
1483
|
} // namespace
|
|
1481
1484
|
|
|
1482
|
-
//
|
|
1483
|
-
// path, distinct from an OTA download which runs a trial).
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1485
|
+
// Stage a streamed .tgz for adopt install at the next boot (the cable-deploy
|
|
1486
|
+
// path, distinct from an OTA download which runs a trial). The install itself
|
|
1487
|
+
// runs from mik__ota_boot_reconcile() on a clean heap: doing it here, with the
|
|
1488
|
+
// app live, needs a 32 KB contiguous inflate window a fragmented heap cannot
|
|
1489
|
+
// provide. Verification stays synchronous so a corrupt upload still fails over
|
|
1490
|
+
// serial; empty checksum skips the check. NOT JS-exposed — called from the
|
|
1491
|
+
// serial DEPLOY_BUILD handler.
|
|
1492
|
+
bool mik__ota_stage_adopt(const char* tgz_path, const char* checksum, const char** err) {
|
|
1489
1493
|
if (!path_exists(tgz_path)) {
|
|
1490
1494
|
*err = "build not staged";
|
|
1491
1495
|
return false;
|
|
1492
1496
|
}
|
|
1493
|
-
// Verify SHA-256 before install so a corrupt upload can't become the
|
|
1494
|
-
// rollback baseline. Empty checksum skips the check.
|
|
1495
1497
|
if (checksum && checksum[0]) {
|
|
1496
1498
|
char got[65];
|
|
1497
1499
|
if (!sha256_file(tgz_path, got)) {
|
|
@@ -1503,43 +1505,74 @@ bool mik__ota_adopt_build(const char* tgz_path, const char* checksum, const char
|
|
|
1503
1505
|
return false;
|
|
1504
1506
|
}
|
|
1505
1507
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
// kLastGood up front would, on a failed install, leave the revert target
|
|
1510
|
-
// naming a build that is not installed and never was, so a later revert()
|
|
1511
|
-
// would roll *forward* onto it while reporting the old checksum.
|
|
1512
|
-
unlink(kLastGoodTmp);
|
|
1513
|
-
if (!copy_file(tgz_path, kLastGoodTmp)) {
|
|
1514
|
-
*err = "stage rollback baseline";
|
|
1508
|
+
nvs_handle_t h;
|
|
1509
|
+
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) {
|
|
1510
|
+
*err = "nvs open failed";
|
|
1515
1511
|
return false;
|
|
1516
1512
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1513
|
+
// Move the .tgz out of the deploy tmp tree before acking: MIK_DeployRecover
|
|
1514
|
+
// cleans that tree at boot, before the reconcile would read it. Rename
|
|
1515
|
+
// before anything else mutates: a failed rename (e.g. littlefs metadata
|
|
1516
|
+
// ENOSPC) must error back over serial having left trial state untouched.
|
|
1517
|
+
unlink(kPending);
|
|
1518
|
+
if (rename(tgz_path, kPending) != 0) {
|
|
1519
|
+
nvs_close(h);
|
|
1520
|
+
*err = "stage pending build";
|
|
1520
1521
|
return false;
|
|
1521
1522
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1523
|
+
// A cable deploy supersedes an in-flight trial. Resolve it to GOOD now,
|
|
1524
|
+
// while the trial's checksum is still in pendChk (overwritten below): the
|
|
1525
|
+
// trial build is what /app holds, so it becomes the installed build, the
|
|
1526
|
+
// same accounting the reflash guard does. Without this, the next boot's
|
|
1527
|
+
// trial verdict would roll back or promote against the pending slot this
|
|
1528
|
+
// deploy just replaced.
|
|
1529
|
+
if (nvs_u8(h, kKeyState, kStateGood) == kStateTrial) {
|
|
1530
|
+
char trial_chk[96];
|
|
1531
|
+
if (nvs_str(h, kKeyPendChk, trial_chk, sizeof(trial_chk))) {
|
|
1532
|
+
nvs_set_str(h, kKeyInstChk, trial_chk);
|
|
1533
|
+
}
|
|
1531
1534
|
nvs_set_u8(h, kKeyState, kStateGood);
|
|
1532
1535
|
nvs_set_u8(h, kKeyTrialLeft, 0);
|
|
1533
|
-
|
|
1536
|
+
nvs_erase_key(h, kKeyPromLeft);
|
|
1537
|
+
nvs_erase_key(h, kKeyRbLeft);
|
|
1538
|
+
nvs_erase_key(h, kKeyNeutLeft);
|
|
1534
1539
|
clear_trial_failure(h);
|
|
1535
|
-
unlink(kPending);
|
|
1536
|
-
nvs_erase_key(h, kKeyPendChk);
|
|
1537
|
-
nvs_commit(h);
|
|
1538
|
-
nvs_close(h);
|
|
1539
1540
|
}
|
|
1541
|
+
nvs_set_str(h, kKeyPendChk, checksum ? checksum : "");
|
|
1542
|
+
nvs_set_u8(h, kKeyPendMode, kPendModeAdopt);
|
|
1543
|
+
nvs_set_u8(h, kKeyPending, 1);
|
|
1544
|
+
nvs_set_u8(h, kKeyInstLeft, 1); // one attempt: the developer is on the cable
|
|
1545
|
+
// Drop any unread outcome from an earlier adopt (e.g. a --no-restart deploy
|
|
1546
|
+
// that installed at a natural boot), so the post-restart result read can
|
|
1547
|
+
// only ever see this deploy's outcome.
|
|
1548
|
+
nvs_erase_key(h, kKeyDRes);
|
|
1549
|
+
nvs_erase_key(h, kKeyDChk);
|
|
1550
|
+
nvs_erase_key(h, kKeyDRsn);
|
|
1551
|
+
nvs_erase_key(h, kKeyDDetail);
|
|
1552
|
+
nvs_commit(h);
|
|
1553
|
+
nvs_close(h);
|
|
1540
1554
|
return true;
|
|
1541
1555
|
}
|
|
1542
1556
|
|
|
1557
|
+
// Read and clear the adopt-install outcome for MIK_CMD_DEPLOY_RESULT.
|
|
1558
|
+
void mik__ota_take_deploy_result(MIKDeployResult* out) {
|
|
1559
|
+
memset(out, 0, sizeof(*out));
|
|
1560
|
+
nvs_handle_t h;
|
|
1561
|
+
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return;
|
|
1562
|
+
out->status = nvs_u8(h, kKeyDRes, 0);
|
|
1563
|
+
nvs_str(h, kKeyDChk, out->checksum, sizeof(out->checksum));
|
|
1564
|
+
nvs_str(h, kKeyDRsn, out->reason, sizeof(out->reason));
|
|
1565
|
+
nvs_str(h, kKeyDDetail, out->detail, sizeof(out->detail));
|
|
1566
|
+
if (out->status != 0) {
|
|
1567
|
+
nvs_erase_key(h, kKeyDRes);
|
|
1568
|
+
nvs_erase_key(h, kKeyDChk);
|
|
1569
|
+
nvs_erase_key(h, kKeyDRsn);
|
|
1570
|
+
nvs_erase_key(h, kKeyDDetail);
|
|
1571
|
+
nvs_commit(h);
|
|
1572
|
+
}
|
|
1573
|
+
nvs_close(h);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1543
1576
|
// True while an unconfirmed OTA trial is the running build.
|
|
1544
1577
|
bool mik__ota_in_trial(void) {
|
|
1545
1578
|
nvs_handle_t h;
|
|
@@ -1564,15 +1597,16 @@ void mik__ota_note_trial_failure(const char* detail) {
|
|
|
1564
1597
|
}
|
|
1565
1598
|
|
|
1566
1599
|
// Boot reconcile: NOT JS-exposed. Called from MIK_Main() before the JS app
|
|
1567
|
-
// loads, after MIK_DeployRecover(). Runs the reflash guard,
|
|
1568
|
-
// and a deferred install in that
|
|
1600
|
+
// loads, after MIK_DeployRecover(). Runs the reflash guard, an adopt install
|
|
1601
|
+
// (cable deploy), the trial verdict, and a deferred trial install in that
|
|
1602
|
+
// order.
|
|
1569
1603
|
void mik__ota_boot_reconcile(void) {
|
|
1570
1604
|
nvs_handle_t h;
|
|
1571
1605
|
if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return;
|
|
1572
1606
|
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
//
|
|
1607
|
+
// Legacy leftover: firmware that installed cable deploys synchronously
|
|
1608
|
+
// staged its install source here and could die before promoting it. Never
|
|
1609
|
+
// valid across a boot, and a whole build's worth of flash.
|
|
1576
1610
|
unlink(kLastGoodTmp);
|
|
1577
1611
|
|
|
1578
1612
|
// 1. Reflash guard. An out-of-band `idf.py flash` swaps the firmware without
|
|
@@ -1602,6 +1636,7 @@ void mik__ota_boot_reconcile(void) {
|
|
|
1602
1636
|
}
|
|
1603
1637
|
}
|
|
1604
1638
|
nvs_erase_key(h, kKeyPendChk);
|
|
1639
|
+
nvs_erase_key(h, kKeyPendMode);
|
|
1605
1640
|
nvs_set_u8(h, kKeyState, kStateGood);
|
|
1606
1641
|
nvs_set_u8(h, kKeyTrialLeft, 0);
|
|
1607
1642
|
nvs_set_u8(h, kKeyPending, 0);
|
|
@@ -1613,7 +1648,65 @@ void mik__ota_boot_reconcile(void) {
|
|
|
1613
1648
|
nvs_commit(h);
|
|
1614
1649
|
}
|
|
1615
1650
|
|
|
1616
|
-
// 2.
|
|
1651
|
+
// 2. Adopt install: a cable deploy staged by MIK_CMD_DEPLOY_BUILD. No
|
|
1652
|
+
// trial, no rollback baseline, one attempt (charged before the attempt,
|
|
1653
|
+
// same crash reasoning as the deferred install below). Runs before the
|
|
1654
|
+
// trial verdict as a backstop: stage_adopt resolves an in-flight trial
|
|
1655
|
+
// at stage time, but stale trial state must never roll back or promote
|
|
1656
|
+
// against the pending slot the deploy overwrote.
|
|
1657
|
+
if (nvs_u8(h, kKeyPending, 0) == 1 &&
|
|
1658
|
+
nvs_u8(h, kKeyPendMode, kPendModeTrial) == kPendModeAdopt) {
|
|
1659
|
+
char chk[96] = {0};
|
|
1660
|
+
nvs_str(h, kKeyPendChk, chk, sizeof(chk));
|
|
1661
|
+
if (!path_exists(kPending)) {
|
|
1662
|
+
adopt_fail(h, chk, "install-failed", "staged build missing");
|
|
1663
|
+
nvs_close(h);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
uint8_t left = nvs_u8(h, kKeyInstLeft, 1);
|
|
1667
|
+
if (left == 0) {
|
|
1668
|
+
// The charged attempt never came back: the install panicked.
|
|
1669
|
+
adopt_fail(h, chk, "install-failed", "install did not complete (device restarted)");
|
|
1670
|
+
nvs_close(h);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
nvs_set_u8(h, kKeyInstLeft, (uint8_t)(left - 1));
|
|
1674
|
+
nvs_commit(h);
|
|
1675
|
+
|
|
1676
|
+
const char* err = nullptr;
|
|
1677
|
+
OtaErr kind = OtaErr::kTransient;
|
|
1678
|
+
if (!install_build(kPending, &err, &kind)) {
|
|
1679
|
+
adopt_fail(h, chk, kind == OtaErr::kCorrupt ? "install-corrupt" : "install-failed",
|
|
1680
|
+
err != nullptr ? err : "install failed");
|
|
1681
|
+
nvs_close(h);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
// Installed. A cable deploy resets the OTA safety story like a reflash
|
|
1685
|
+
// does: nothing to roll back to until the next OTA trial promotes.
|
|
1686
|
+
unlink(kPending);
|
|
1687
|
+
unlink(kLastGood);
|
|
1688
|
+
nvs_set_str(h, kKeyInstChk, chk);
|
|
1689
|
+
nvs_set_u8(h, kKeyState, kStateGood);
|
|
1690
|
+
nvs_set_u8(h, kKeyTrialLeft, 0);
|
|
1691
|
+
nvs_set_u8(h, kKeyPending, 0);
|
|
1692
|
+
nvs_erase_key(h, kKeyPendChk);
|
|
1693
|
+
nvs_erase_key(h, kKeyPendMode);
|
|
1694
|
+
nvs_erase_key(h, kKeyInstLeft);
|
|
1695
|
+
nvs_erase_key(h, kKeyPromLeft);
|
|
1696
|
+
nvs_erase_key(h, kKeyRbLeft);
|
|
1697
|
+
nvs_erase_key(h, kKeyNeutLeft);
|
|
1698
|
+
clear_trial_failure(h);
|
|
1699
|
+
nvs_set_u8(h, kKeyDRes, 1);
|
|
1700
|
+
nvs_set_str(h, kKeyDChk, chk);
|
|
1701
|
+
nvs_erase_key(h, kKeyDRsn);
|
|
1702
|
+
nvs_erase_key(h, kKeyDDetail);
|
|
1703
|
+
nvs_commit(h);
|
|
1704
|
+
nvs_close(h);
|
|
1705
|
+
esp_restart(); // boot into the deployed app on a clean heap
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// 3. Trial verdict. Evaluate why we just reset while a trial was in flight.
|
|
1617
1710
|
if (nvs_u8(h, kKeyState, kStateGood) == kStateTrial) {
|
|
1618
1711
|
esp_reset_reason_t reason = esp_reset_reason();
|
|
1619
1712
|
TrialVerdict verdict = classify_reset(reason);
|
|
@@ -1715,8 +1808,10 @@ void mik__ota_boot_reconcile(void) {
|
|
|
1715
1808
|
// kNeutral: don't penalize — leave the trial untouched and proceed.
|
|
1716
1809
|
}
|
|
1717
1810
|
|
|
1718
|
-
//
|
|
1719
|
-
// installed here, on a clean heap, then we restart into its
|
|
1811
|
+
// 4. Deferred trial install. A build verified by stageFinish(installNow=
|
|
1812
|
+
// false) is installed here, on a clean heap, then we restart into its
|
|
1813
|
+
// trial. (An adopt-mode pending build never reaches this: step 2
|
|
1814
|
+
// returns.)
|
|
1720
1815
|
if (nvs_u8(h, kKeyPending, 0) == 1 && nvs_u8(h, kKeyState, kStateGood) == kStateGood) {
|
|
1721
1816
|
if (!path_exists(kPending)) {
|
|
1722
1817
|
nvs_set_u8(h, kKeyPending, 0);
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {existsSync} from 'node:fs'
|
|
1
|
+
import {existsSync, readFileSync} from 'node:fs'
|
|
2
2
|
import {dirname, join} from 'node:path'
|
|
3
3
|
import {fileURLToPath} from 'node:url'
|
|
4
4
|
|
|
@@ -18,3 +18,15 @@ export function prebuiltFirmwareDir(chip) {
|
|
|
18
18
|
export function hasPrebuiltFirmware(chip) {
|
|
19
19
|
return existsSync(join(prebuildsRoot, chip, 'flasher_args.json'))
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/** Identity of the bundled prebuilt for `chip` (the package name of the
|
|
23
|
+
* firmware project it was built from), or undefined when no prebuilt or no
|
|
24
|
+
* recorded identity exists. */
|
|
25
|
+
export function prebuiltFirmwareName(chip) {
|
|
26
|
+
try {
|
|
27
|
+
const {name} = JSON.parse(readFileSync(join(prebuildsRoot, chip, 'firmware.json'), 'utf8'))
|
|
28
|
+
return typeof name === 'string' ? name : undefined
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikrojs/firmware",
|
|
3
|
-
"version": "0.17.0
|
|
3
|
+
"version": "0.17.0",
|
|
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/
|
|
51
|
-
"@mikrojs/
|
|
50
|
+
"@mikrojs/quickjs": "0.17.0",
|
|
51
|
+
"@mikrojs/native": "0.17.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.0.0"
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mikrojs/firmware-dev"}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mikrojs/firmware-dev"}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mikrojs/firmware-dev"}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mikrojs/firmware-dev"}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@mikrojs/firmware-dev"}
|
|
Binary file
|
package/project.cmake
CHANGED
|
@@ -81,16 +81,58 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/sdkconfig.defaults.${IDF_TARGET}")
|
|
|
81
81
|
list(APPEND _SDKCONFIG_LIST "${CMAKE_SOURCE_DIR}/sdkconfig.defaults.${IDF_TARGET}")
|
|
82
82
|
endif()
|
|
83
83
|
|
|
84
|
-
# Partition table:
|
|
84
|
+
# Partition table: copy the source csv into the build dir and point the
|
|
85
|
+
# config at it, project-relative when possible (ESP-IDF resolves a relative
|
|
86
|
+
# value against the project dir). Writing the resolved package path directly
|
|
87
|
+
# would freeze an absolute node_modules/store path into sdkconfig — sdkconfig
|
|
88
|
+
# wins over defaults files, so every @mikrojs/firmware upgrade would then
|
|
89
|
+
# break the build with a missing-partitions.csv error pointing at a version
|
|
90
|
+
# that is no longer installed.
|
|
85
91
|
if(EXISTS "${CMAKE_SOURCE_DIR}/partitions.csv")
|
|
86
|
-
set(
|
|
92
|
+
set(_PARTITION_SRC "${CMAKE_SOURCE_DIR}/partitions.csv")
|
|
87
93
|
else()
|
|
88
|
-
set(
|
|
94
|
+
set(_PARTITION_SRC "${_MIK_CONFIG_DIR}/partitions.csv")
|
|
89
95
|
endif()
|
|
90
|
-
|
|
96
|
+
configure_file("${_PARTITION_SRC}" "${CMAKE_BINARY_DIR}/partitions.csv" COPYONLY)
|
|
97
|
+
file(RELATIVE_PATH _PARTITION_CSV "${CMAKE_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/partitions.csv")
|
|
98
|
+
if(_PARTITION_CSV MATCHES "^\\.\\.")
|
|
99
|
+
# Build dir outside the project (idf.py -B): absolute path, still stable
|
|
100
|
+
# across package upgrades, and the heal below re-points it if it moves.
|
|
101
|
+
set(_PARTITION_CSV "${CMAKE_BINARY_DIR}/partitions.csv")
|
|
102
|
+
endif()
|
|
103
|
+
set(_PARTITION_FRAGMENT "${CMAKE_BINARY_DIR}/sdkconfig.partitions")
|
|
91
104
|
file(WRITE "${_PARTITION_FRAGMENT}" "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"${_PARTITION_CSV}\"\n")
|
|
92
105
|
list(APPEND _SDKCONFIG_LIST "${_PARTITION_FRAGMENT}")
|
|
93
106
|
|
|
107
|
+
# Heal an sdkconfig whose frozen path points outside the project: a stale
|
|
108
|
+
# resolved package path, or a build dir that moved. Must fire even when the
|
|
109
|
+
# frozen file still exists — an older package version often is still in the
|
|
110
|
+
# store, and its old partition table would be built silently. Paths inside
|
|
111
|
+
# the project are deliberate menuconfig overrides and are left alone.
|
|
112
|
+
if(EXISTS "${CMAKE_SOURCE_DIR}/sdkconfig")
|
|
113
|
+
file(READ "${CMAKE_SOURCE_DIR}/sdkconfig" _SDKCONFIG_CONTENT)
|
|
114
|
+
string(REGEX MATCH "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"([^\"]*)\"" _PARTITION_MATCH "${_SDKCONFIG_CONTENT}")
|
|
115
|
+
if(_PARTITION_MATCH AND NOT CMAKE_MATCH_1 STREQUAL "${_PARTITION_CSV}")
|
|
116
|
+
set(_PARTITION_FROZEN "${CMAKE_MATCH_1}")
|
|
117
|
+
get_filename_component(_PARTITION_FROZEN_ABS "${_PARTITION_FROZEN}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
|
|
118
|
+
string(FIND "${_PARTITION_FROZEN_ABS}" "${CMAKE_SOURCE_DIR}/" _PARTITION_INSIDE)
|
|
119
|
+
if(NOT _PARTITION_INSIDE EQUAL 0)
|
|
120
|
+
string(REGEX REPLACE "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"[^\"]*\""
|
|
121
|
+
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"${_PARTITION_CSV}\""
|
|
122
|
+
_SDKCONFIG_CONTENT "${_SDKCONFIG_CONTENT}")
|
|
123
|
+
# The derived twin should be recomputed by kconfgen, but rewrite it
|
|
124
|
+
# too: if it kept the saved value the build breaks the same way.
|
|
125
|
+
string(REGEX REPLACE "CONFIG_PARTITION_TABLE_FILENAME=\"[^\"]*\""
|
|
126
|
+
"CONFIG_PARTITION_TABLE_FILENAME=\"${_PARTITION_CSV}\""
|
|
127
|
+
_SDKCONFIG_CONTENT "${_SDKCONFIG_CONTENT}")
|
|
128
|
+
file(WRITE "${CMAKE_SOURCE_DIR}/sdkconfig" "${_SDKCONFIG_CONTENT}")
|
|
129
|
+
message(WARNING
|
|
130
|
+
"mikrojs: sdkconfig CONFIG_PARTITION_TABLE_CUSTOM_FILENAME pointed outside "
|
|
131
|
+
"the project (\"${_PARTITION_FROZEN}\"); repointed at \"${_PARTITION_CSV}\"")
|
|
132
|
+
endif()
|
|
133
|
+
endif()
|
|
134
|
+
endif()
|
|
135
|
+
|
|
94
136
|
if(_BOARD_JSON)
|
|
95
137
|
string(JSON _BOARD_COMPONENT_DIRS GET "${_BOARD_JSON}" "components")
|
|
96
138
|
string(JSON _BOARD_SDKCONFIG_DEFAULTS GET "${_BOARD_JSON}" "sdkconfigs")
|
package/resolve.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* quickjs — path to quickjs.cmake
|
|
13
13
|
* native — JSON with native package paths
|
|
14
14
|
* version — firmware package version
|
|
15
|
+
* projectName <dir> — package.json name of the consuming project (empty if none)
|
|
15
16
|
* discover <dir> — discover board/driver components from project dir
|
|
16
17
|
*/
|
|
17
18
|
import {componentDir, projectCmakePath} from './cmake.js'
|
|
@@ -44,6 +45,15 @@ if (query === 'componentDir') {
|
|
|
44
45
|
} else if (query === 'version') {
|
|
45
46
|
const {default: pkg} = await import('./package.json', {with: {type: 'json'}})
|
|
46
47
|
process.stdout.write(pkg.version)
|
|
48
|
+
} else if (query === 'projectName') {
|
|
49
|
+
const {readFileSync} = await import('node:fs')
|
|
50
|
+
const {join} = await import('node:path')
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(readFileSync(join(process.argv[3], 'package.json'), 'utf8'))
|
|
53
|
+
if (typeof pkg.name === 'string') process.stdout.write(pkg.name)
|
|
54
|
+
} catch {
|
|
55
|
+
// No package.json (e.g. on-device test apps): no identity, empty output.
|
|
56
|
+
}
|
|
47
57
|
} else if (query === 'discover') {
|
|
48
58
|
const projectDir = process.argv[3]
|
|
49
59
|
const {discover} = await import('./discover.js')
|