mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 00:41:18 +00:00
Add guarded nRF52 OTA rescue and SF5 test mode
This commit is contained in:
1 parent
710a6e02c5
commit
4a3d86ef98
14 files changed
+225
-47
No files matched your search
@@ -168,6 +168,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| LoRa OTA | [`ota ls`; `ota neighbors`; `ota nbrs`; `ota updates`; `ota n`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build; nRF52 installs in-place deltas | No | No | Yes |
|
||||
| LoRa OTA | [`ota install`; `ota apply`; `ota applydelta`](ota_protocol.md#11-cli-surface-otaclicpp) | Compatible bootloader and completed update | No | No | Yes |
|
||||
| LoRa OTA | [`ota rescue install <base_hash16>`](ota_protocol.md#12-apply--bootloader-contract) | Internal-flash nRF52 LoRa OTA build with failed app-side EndF validation | No | No | Feature |
|
||||
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes |
|
||||
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes |
|
||||
@@ -324,6 +325,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| LoRa OTA | [`ota ls`; `ota neighbors`; `ota nbrs`; `ota updates`; `ota n`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota install`; `ota apply`; `ota applydelta`](ota_protocol.md#11-cli-surface-otaclicpp) | Compatible completed update | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota rescue install <base_hash16>`](ota_protocol.md#12-apply--bootloader-contract) | Internal-flash nRF52 LoRa OTA build with failed app-side EndF validation | No | No | Feature | No | No | No | No |
|
||||
| LoRa OTA | [`ota cancel`; `ota drop`; `ota stop`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota announce`; `ota adv`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build | No | No | Yes | No | No | Yes | Yes |
|
||||
| LoRa OTA | [`ota self`; `ota id`](ota_protocol.md#11-cli-surface-otaclicpp) | Firmware with EndF trailer | No | No | Yes | No | No | Yes | Yes |
|
||||
|
||||
@@ -237,7 +237,7 @@ nc 192.168.1.50 5002
|
||||
```
|
||||
|
||||
```text
|
||||
tempradio 909.950,250,7,5,120
|
||||
tempradio 909.950,250,5,5,120
|
||||
ota status
|
||||
```
|
||||
|
||||
@@ -260,7 +260,7 @@ picocom -b 115200 \
|
||||
```
|
||||
|
||||
```text
|
||||
tempradio 909.950,250,7,5,120
|
||||
tempradio 909.950,250,5,5,120
|
||||
+++MESHCORE-TERM-STOP
|
||||
```
|
||||
|
||||
|
||||
@@ -179,15 +179,13 @@ and codec mask against the selected package.
|
||||
The default TempRadio tuple is:
|
||||
|
||||
```text
|
||||
909.950,250,7,5,120
|
||||
909.950,250,5,5,120
|
||||
```
|
||||
|
||||
The 250 kHz bandwidth, SF7, and CR5 combination is supported by every current
|
||||
sub-GHz radio family used in USB Companion builds, including older SX127x
|
||||
controllers (which do not support SF5). The frequency is only a North American
|
||||
example: choose a legal frequency supported by every participating radio and
|
||||
appropriate to your location. Pass the complete replacement tuple with
|
||||
`--temp-radio`.
|
||||
The test default is 250 kHz bandwidth, SF5, and CR5. The frequency is only a
|
||||
North American example: choose a legal frequency supported by every
|
||||
participating radio and appropriate to your location. Older radios that do not
|
||||
support SF5 require a complete replacement tuple passed with `--temp-radio`.
|
||||
|
||||
## 4. Run an ESP32 update
|
||||
|
||||
|
||||
@@ -679,6 +679,7 @@ ota status | st (or bare `ota`) plain-language: running fw, the one fetch ses
|
||||
ota ls | neighbors | nbrs | updates | n [page] paged updates (queries sources; rows arrive async via OTA_HAVE)
|
||||
ota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch)
|
||||
ota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader
|
||||
ota rescue install <base_hash16> internal-flash nRF52 only: recover from failed app-side EndF validation
|
||||
ota cancel | drop | stop drop the current fetch session (frees the slot)
|
||||
ota announce | adv serve self + send a beacon now
|
||||
ota self | id print this firmware's EndF (body/image size, base_hash)
|
||||
@@ -702,6 +703,15 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the
|
||||
running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` - never trust `bank_0_size`),
|
||||
3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`.
|
||||
- **nRF52 EndF rescue:** `ota rescue install <base_hash16>` is a pre-provisioned recovery path for an
|
||||
internal-flash nRF52 application that still runs but cannot validate its own EndF identity. It refuses
|
||||
when normal EndF validation succeeds, requires the operator hash to exactly equal the staged delta's
|
||||
`base_hash`, requires the package `target_id` to match and its `hw_id` to pass the normal hardware gate,
|
||||
and retains the normal payload and signature/allowlist gates. Approval only delegates the base decision:
|
||||
OTAFIX independently
|
||||
locates the physical EndF, hashes the running app, and compares that value with the package before its
|
||||
first app write. A physically absent/corrupt EndF or wrong base therefore returns to the unchanged app;
|
||||
it still requires USB recovery if that app does not already contain this command.
|
||||
- **MeshTower V2 SD nRF52:** the application stores a contiguous `/meshcore-ota.mota` on microSD and
|
||||
publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching
|
||||
bootloader reads the card without mounting FAT, supports either a full image or an in-place delta,
|
||||
|
||||
@@ -178,6 +178,18 @@ After it reboots, run `ota status` to confirm the new version.
|
||||
### 5. If something goes wrong
|
||||
|
||||
- A download that stalls or gets interrupted just **resumes** later, or you can `ota cancel` and try again.
|
||||
- An internal-flash **nRF52** that still runs but reports `no EndF` can use the pre-provisioned rescue path
|
||||
if its physical EndF is intact and only app-side validation is failing. Fetch the exact in-place delta,
|
||||
obtain its 16-hex-digit `base_hash` from the package metadata, then run:
|
||||
|
||||
```text
|
||||
ota rescue install <base_hash16>
|
||||
```
|
||||
|
||||
This is not a force option. It refuses a normally valid EndF, a different package hash, hardware or
|
||||
target mismatch, corrupt payload, and invalid/untrusted signatures. The bootloader independently hashes
|
||||
the running app and rejects a wrong base before writing the app. If the physical EndF is absent or this
|
||||
command was not already in the running firmware, recover over USB.
|
||||
- If an **install** fails, the node won't boot a broken image - it lands in **recovery mode**:
|
||||
- **nRF52:** it appears as a USB drive; drag a known-good firmware `.uf2` for that exact board onto it
|
||||
to recover.
|
||||
@@ -301,6 +313,7 @@ that only contains what changed). You get them by:
|
||||
| Download update #1 for installation | `ota get 1 flash` |
|
||||
| Cancel a download | `ota cancel` |
|
||||
| Install a finished download | `ota install` |
|
||||
| Recover app-side `no EndF` on internal nRF52 | `ota rescue install <base_hash16>` |
|
||||
| Turn on auto-download | `ota config autofetch any` |
|
||||
| Turn on auto-install (trusted only) | `ota config autoinstall trusted` |
|
||||
| Trust a signer | `ota key add <hex>` |
|
||||
|
||||
@@ -155,7 +155,7 @@ The tested Heltec key is
|
||||
Change the IP address for a different deployment.
|
||||
|
||||
The source, controller, destination, and every intermediate relay must support
|
||||
the temporary tuple `909.950,250,7,5`. Confirm that this frequency and
|
||||
the temporary tuple `909.950,250,5,5`. Confirm that this frequency and
|
||||
bandwidth are legal at the deployment location. All nodes return to their
|
||||
ordinary radio after each step.
|
||||
|
||||
|
||||
@@ -407,6 +407,10 @@ bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len, const SignerAllowl
|
||||
bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) {
|
||||
st = ApplyState(); strcpy(msg, "nRF52-only (ESP32 uses ota_apply_detools_mota)"); return false;
|
||||
}
|
||||
bool ota_rescue_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, const uint8_t*,
|
||||
uint32_t, ApplyState& st, char* msg) {
|
||||
st = ApplyState(); strcpy(msg, "rescue is internal-flash nRF52-only"); return false;
|
||||
}
|
||||
|
||||
void ota_reboot_to_apply() { esp_restart(); } // boots the slot armed by ota_apply_detools_mota; no return
|
||||
|
||||
@@ -438,8 +442,11 @@ uint8_t ota_bootloader_last_rc() { // bootloader's last in-place-app
|
||||
return (uint8_t)v;
|
||||
}
|
||||
|
||||
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len,
|
||||
const SignerAllowlist& allow,
|
||||
const uint8_t* rescue_base_hash,
|
||||
uint32_t local_target_id,
|
||||
ApplyState& st, char* msg) {
|
||||
st = ApplyState();
|
||||
MotaManifest m;
|
||||
if (!mota_parse(buf, len, m)) { strcpy(msg, "parse failed"); return false; }
|
||||
@@ -470,12 +477,31 @@ bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlis
|
||||
strcpy(msg, "payload hash mismatch (incomplete or corrupt .mota)"); return false;
|
||||
}
|
||||
|
||||
// 2) target firmware: the delta must be built against THIS running image (base_hash == our EndF body
|
||||
// hash). The resulting image_hash is re-checked by the bootloader after the in-place decode -- a
|
||||
// single-slot device cannot produce the target image to hash it before applying.
|
||||
// 2) target firmware. Normally the delta must match the validated EndF body hash. The explicit rescue
|
||||
// path exists only for a firmware whose normal EndF validation is broken: it requires the operator's
|
||||
// exact package base hash and the package's target_id. It then delegates the actual running-image
|
||||
// base check to the bootloader, which performs that check before its first application write.
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
|
||||
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; }
|
||||
const bool self_valid = ota_self_firmware(fi) && fi.valid;
|
||||
if (rescue_base_hash) {
|
||||
switch (ota_nrf52_rescue_gate(self_valid, m.base_hash, rescue_base_hash,
|
||||
m.target_id, local_target_id)) {
|
||||
case NRF52_RESCUE_SELF_VALID:
|
||||
strcpy(msg, "EndF OK; use ota install"); return false;
|
||||
case NRF52_RESCUE_BASE_MISSING:
|
||||
case NRF52_RESCUE_BASE_MISMATCH:
|
||||
strcpy(msg, "rescue base mismatch"); return false;
|
||||
case NRF52_RESCUE_TARGET_MISMATCH:
|
||||
strcpy(msg, "rescue target mismatch"); return false;
|
||||
case NRF52_RESCUE_OK:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!self_valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
|
||||
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) {
|
||||
strcpy(msg, "not built for the running firmware (base mismatch)"); return false;
|
||||
}
|
||||
}
|
||||
st.slot_ok = true;
|
||||
|
||||
// 3) signature (only if the .mota is signed): valid Ed25519 AND signer in this device's allowlist
|
||||
@@ -493,11 +519,26 @@ bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlis
|
||||
|
||||
// Approved. Do NOT reset here - return so the caller can deliver `msg` to the operator first; the
|
||||
// deferred ota_reboot_to_apply() (after the reply is sent) does the actual handoff to the bootloader.
|
||||
sprintf(msg, "verified%s; applying - rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)");
|
||||
if (rescue_base_hash) {
|
||||
strcpy(msg, "rescue armed; bootloader checks base");
|
||||
} else {
|
||||
sprintf(msg, "verified%s; applying - rebooting into bootloader once this reply is sent",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_impl(buf, len, allow, nullptr, 0, st, msg);
|
||||
}
|
||||
|
||||
bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
|
||||
const uint8_t operator_base_hash[8], uint32_t local_target_id,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_impl(buf, len, allow, operator_base_hash, local_target_id, st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
@@ -586,6 +627,10 @@ bool ota_apply_verify_slot(ApplyState&) { return false; }
|
||||
bool ota_apply_commit() { return false; }
|
||||
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
|
||||
bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
|
||||
bool ota_rescue_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, const uint8_t*,
|
||||
uint32_t, ApplyState& st, char* msg) {
|
||||
st = ApplyState(); strcpy(msg, "unsupported"); return false;
|
||||
}
|
||||
void ota_reboot_to_apply() {}
|
||||
uint8_t ota_bootloader_last_rc() { return 0; }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
#include "SignerAllowlist.h"
|
||||
|
||||
// P6 apply (full-image, ESP32 A/B). The new image is delivered into the inactive OTA slot; the device
|
||||
@@ -23,6 +24,31 @@ struct ApplyState {
|
||||
uint8_t image_hash[32] = {0};
|
||||
};
|
||||
|
||||
// Policy gate for the nRF52 recovery-only apply path. This is intentionally separate from the normal
|
||||
// EndF/base check so it cannot become an accidental fallback: the operator must invoke the rescue
|
||||
// command with the exact base hash carried by the staged package, and the package must target this
|
||||
// build. The bootloader still recomputes and checks the actual running-image base before writing it.
|
||||
enum Nrf52RescueGate : uint8_t {
|
||||
NRF52_RESCUE_OK = 0,
|
||||
NRF52_RESCUE_SELF_VALID,
|
||||
NRF52_RESCUE_BASE_MISSING,
|
||||
NRF52_RESCUE_BASE_MISMATCH,
|
||||
NRF52_RESCUE_TARGET_MISMATCH,
|
||||
};
|
||||
|
||||
inline Nrf52RescueGate ota_nrf52_rescue_gate(bool self_valid,
|
||||
const uint8_t* manifest_base_hash,
|
||||
const uint8_t* operator_base_hash,
|
||||
uint32_t manifest_target_id,
|
||||
uint32_t local_target_id) {
|
||||
if (self_valid) return NRF52_RESCUE_SELF_VALID;
|
||||
if (!manifest_base_hash || !operator_base_hash) return NRF52_RESCUE_BASE_MISSING;
|
||||
if (memcmp(manifest_base_hash, operator_base_hash, 8) != 0) return NRF52_RESCUE_BASE_MISMATCH;
|
||||
if (!manifest_target_id || !local_target_id || manifest_target_id != local_target_id)
|
||||
return NRF52_RESCUE_TARGET_MISMATCH;
|
||||
return NRF52_RESCUE_OK;
|
||||
}
|
||||
|
||||
bool ota_apply_slot_info(uint32_t* addr, uint32_t* size); // the inactive A/B slot
|
||||
bool ota_apply_set_manifest(const uint8_t* mf, uint32_t len,
|
||||
const SignerAllowlist& allow, ApplyState& st); // parse + verify signature
|
||||
@@ -50,6 +76,11 @@ bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len,
|
||||
// Returns true (msg = "verified...") when approved, false (msg = the first failing gate) otherwise.
|
||||
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
// Recovery for failed app-side EndF validation. This is available only through the explicit
|
||||
// `ota rescue install <base_hash16>` CLI command and never through normal or automatic installation.
|
||||
bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
const SignerAllowlist& allow, const uint8_t operator_base_hash[8],
|
||||
uint32_t local_target_id, ApplyState& st, char* msg);
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
class OtaStoreSdNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
|
||||
|
||||
@@ -131,6 +131,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder: status | stats | ls=find images | get <#> folder=capture | cancel | "
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get | install | rescue install <hash16> | cancel | announce | "
|
||||
"self | folder | config | key");
|
||||
#else
|
||||
snprintf(reply, 160,
|
||||
"OTA: status | stats=admin ids/hashes | ls=find updates | get <#>=download | install | cancel | "
|
||||
@@ -395,6 +399,32 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} else if (is_cmd(a, "rescue", &rest)) {
|
||||
// Recovery-only nRF52 handoff for firmware whose normal EndF validation is broken. This command is
|
||||
// deliberately not an alias or automatic fallback: the operator must name `install` and provide
|
||||
// the exact 8-byte base hash carried by the already-fetched package. The bootloader independently
|
||||
// hashes the running app and refuses a mismatch before writing any application flash.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
const char* hash_text = nullptr;
|
||||
uint8_t operator_base_hash[8];
|
||||
if (!is_cmd(rest, "install", &hash_text) ||
|
||||
!mesh::Utils::fromHex(operator_base_hash, sizeof(operator_base_hash), hash_text)) {
|
||||
strcpy(reply, "ERR usage: ota rescue install <hash16>");
|
||||
return true;
|
||||
}
|
||||
if (c.manager.fetchState() != OtaManager::COMPLETE || c.fetch_store.staged_size() == 0) {
|
||||
sprintf(reply, "ERR no complete update fetched (fetch=%c %u/%u)",
|
||||
fstate_char(c.manager.fetchState()), (unsigned)c.manager.blocksHave(),
|
||||
(unsigned)c.manager.blocksTotal());
|
||||
return true;
|
||||
}
|
||||
char m2[100];
|
||||
bool ok = c.apply_fetched_rescue(operator_base_hash, m2);
|
||||
sprintf(reply, "%s | %s", ok ? "OK" : "ERR", m2);
|
||||
#else
|
||||
strcpy(reply, "ERR rescue requires an internal-flash nRF52 LoRa OTA build");
|
||||
#endif
|
||||
|
||||
} else if (is_cmd(a, "install|apply|applydelta", &rest)) {
|
||||
// Apply the fetched update. Destructive (reflashes + reboots) and GATED, not interactive (no "type
|
||||
// yes" round-trip - unreliable over LoRa): refuse unless the fetch is COMPLETE, then the apply path
|
||||
|
||||
@@ -140,11 +140,28 @@ struct OtaContext {
|
||||
// so the deferred-reboot path (mesh loop) takes over. Caller ensures the fetch is COMPLETE. Shared by
|
||||
// manual `ota applydelta` and the auto-install path.
|
||||
bool apply_fetched(char* msg) {
|
||||
return apply_fetched_impl(nullptr, msg);
|
||||
}
|
||||
|
||||
// Explicit recovery entry point for an internal-flash nRF52 whose normal EndF validation fails. This
|
||||
// is never called by auto-install; the CLI requires the package's exact 8-byte base hash.
|
||||
bool apply_fetched_rescue(const uint8_t operator_base_hash[8], char* msg) {
|
||||
return apply_fetched_impl(operator_base_hash, msg);
|
||||
}
|
||||
|
||||
bool apply_fetched_impl(const uint8_t* rescue_base_hash, char* msg) {
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
strncpy(msg, "refused: this build serves mOTA images but cannot install one", 96);
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
#else
|
||||
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE)
|
||||
if (rescue_base_hash) {
|
||||
strncpy(msg, "rescue is only for internal-flash nRF52 builds", 96);
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
if (sdCacheFetching()) {
|
||||
strncpy(msg, "refused: SD archive capture owns the OTA receive slot", 96);
|
||||
@@ -171,7 +188,12 @@ struct OtaContext {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg);
|
||||
if (rescue_base_hash) {
|
||||
ok = ota_rescue_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow,
|
||||
rescue_base_hash, manager.target(), apply_st, msg);
|
||||
} else {
|
||||
ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg);
|
||||
}
|
||||
#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
ok = ota_apply_detools_mota(fetch_store, allow, apply_st, msg);
|
||||
#else
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "helpers/ota/OtaStore.h"
|
||||
#include "helpers/ota/OtaProtocol.h"
|
||||
#include "helpers/ota/OtaManager.h"
|
||||
#include "helpers/ota/OtaApply.h"
|
||||
#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py
|
||||
|
||||
extern "C" {
|
||||
@@ -64,6 +65,26 @@ TEST(OtaParse, ParsesReferenceContainer) {
|
||||
EXPECT_FALSE(m.is_approved());
|
||||
}
|
||||
|
||||
TEST(OtaRescue, RequiresInvalidSelfExactBaseAndExactTarget) {
|
||||
const uint8_t package_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 4};
|
||||
const uint8_t operator_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 4};
|
||||
const uint8_t wrong_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 5};
|
||||
const uint32_t target_id = 0x2FA509C1u;
|
||||
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
||||
target_id, target_id), NRF52_RESCUE_OK);
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(true, package_base, operator_base,
|
||||
target_id, target_id), NRF52_RESCUE_SELF_VALID);
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(false, nullptr, operator_base,
|
||||
target_id, target_id), NRF52_RESCUE_BASE_MISSING);
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, wrong_base,
|
||||
target_id, target_id), NRF52_RESCUE_BASE_MISMATCH);
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
||||
target_id, target_id + 1), NRF52_RESCUE_TARGET_MISMATCH);
|
||||
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
||||
target_id, 0), NRF52_RESCUE_TARGET_MISMATCH);
|
||||
}
|
||||
|
||||
TEST(OtaParse, RejectsTampering) {
|
||||
MotaManifest m;
|
||||
// bad magic
|
||||
|
||||
@@ -2066,7 +2066,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
help="optional relay, ordered farthest-to-nearest; repeat as needed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--temp-radio", default="909.950,250,7,5,120",
|
||||
"--temp-radio", default="909.950,250,5,5,120",
|
||||
help="frequency,bw,sf,cr,minutes",
|
||||
)
|
||||
parser.add_argument(
|
||||
|
||||
@@ -866,7 +866,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
metavar="NAME[=PASSWORD]",
|
||||
help="intermediate relay, farthest-to-nearest; repeat for each relay",
|
||||
)
|
||||
parser.add_argument("--temp-radio", default="909.950,250,7,5,120")
|
||||
parser.add_argument("--temp-radio", default="909.950,250,5,5,120")
|
||||
parser.add_argument(
|
||||
"--ota-hops",
|
||||
type=int,
|
||||
|
||||
@@ -141,11 +141,17 @@ class FormatTests(unittest.TestCase):
|
||||
|
||||
def test_temp_radio_accepts_only_cli_bandwidths(self) -> None:
|
||||
self.assertEqual(
|
||||
ota.parse_temp_radio("909.950,250,7,5,120"),
|
||||
(909.95, 250.0, 7, 5, 120),
|
||||
ota.parse_temp_radio("909.950,250,5,5,120"),
|
||||
(909.95, 250.0, 5, 5, 120),
|
||||
)
|
||||
with self.assertRaisesRegex(argparse.ArgumentTypeError, "bandwidth must be"):
|
||||
ota.parse_temp_radio("909.950,200,7,5,120")
|
||||
ota.parse_temp_radio("909.950,200,5,5,120")
|
||||
|
||||
def test_ota_runners_default_to_sf5_and_250_khz(self) -> None:
|
||||
generic = ota.build_parser().parse_args(["release.mota", "remote"])
|
||||
chain = rak_chain.build_parser().parse_args([])
|
||||
self.assertEqual(generic.temp_radio, "909.950,250,5,5,120")
|
||||
self.assertEqual(chain.temp_radio, "909.950,250,5,5,120")
|
||||
|
||||
def test_offline_sd_nrf52_does_not_require_a_base_hash(self) -> None:
|
||||
parser = ota.build_parser()
|
||||
@@ -284,14 +290,14 @@ class SourceCliTests(unittest.TestCase):
|
||||
)
|
||||
with mock.patch.object(ota, "run_checked", return_value=completed) as run:
|
||||
output = ota.source_cli_command(
|
||||
args, "tempradio 909.95,250,7,5,120"
|
||||
args, "tempradio 909.95,250,5,5,120"
|
||||
)
|
||||
|
||||
wire_command = run.call_args.args[0][-1]
|
||||
self.assertEqual(
|
||||
wire_command,
|
||||
"+++MESHCORE-TERM-START\r"
|
||||
"tempradio 909.95,250,7,5,120\r"
|
||||
"tempradio 909.95,250,5,5,120\r"
|
||||
"+++MESHCORE-TERM-STOP",
|
||||
)
|
||||
self.assertIn("OK - temp params", output)
|
||||
@@ -661,13 +667,13 @@ class ReliabilityTests(unittest.TestCase):
|
||||
controller = object.__new__(ota.Controller)
|
||||
controller._run = lambda *_args: [{}]
|
||||
controller.get_radio = lambda: ota.RadioSettings(915, 250, 7, 5, False)
|
||||
requested = ota.RadioSettings(909.95, 250, 7, 5, False)
|
||||
requested = ota.RadioSettings(909.95, 250, 5, 5, False)
|
||||
with self.assertRaisesRegex(ota.OtaError, "read back"):
|
||||
controller.set_radio(requested, "set radio")
|
||||
|
||||
def test_lost_target_temp_reply_is_resolved_on_temporary_channel(self) -> None:
|
||||
normal = ota.RadioSettings(910.525, 62.5, 7, 5, False)
|
||||
temporary = ota.RadioSettings(909.95, 250.0, 7, 5, False)
|
||||
temporary = ota.RadioSettings(909.95, 250.0, 5, 5, False)
|
||||
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
@@ -689,19 +695,19 @@ class ReliabilityTests(unittest.TestCase):
|
||||
ota.arm_target_temp_radio(
|
||||
controller,
|
||||
argparse.Namespace(target="remote"),
|
||||
"tempradio 909.95,250,7,5,120",
|
||||
"tempradio 909.95,250,5,5,120",
|
||||
temporary,
|
||||
normal,
|
||||
)
|
||||
self.assertEqual(
|
||||
controller.commands,
|
||||
["tempradio 909.95,250,7,5,120", "ota self"],
|
||||
["tempradio 909.95,250,5,5,120", "ota self"],
|
||||
)
|
||||
self.assertEqual(controller.radios, [temporary, normal])
|
||||
|
||||
def test_ambiguous_target_temp_probe_is_not_replayed(self) -> None:
|
||||
normal = ota.RadioSettings(910.525, 62.5, 7, 5, False)
|
||||
temporary = ota.RadioSettings(909.95, 250.0, 7, 5, False)
|
||||
temporary = ota.RadioSettings(909.95, 250.0, 5, 5, False)
|
||||
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
@@ -722,13 +728,13 @@ class ReliabilityTests(unittest.TestCase):
|
||||
ota.arm_target_temp_radio(
|
||||
controller,
|
||||
argparse.Namespace(target="remote"),
|
||||
"tempradio 909.95,250,7,5,120",
|
||||
"tempradio 909.95,250,5,5,120",
|
||||
temporary,
|
||||
normal,
|
||||
)
|
||||
self.assertEqual(
|
||||
controller.commands,
|
||||
["tempradio 909.95,250,7,5,120", "ota self"],
|
||||
["tempradio 909.95,250,5,5,120", "ota self"],
|
||||
)
|
||||
self.assertEqual(controller.radios, [temporary, normal])
|
||||
|
||||
@@ -759,15 +765,15 @@ class ReliabilityTests(unittest.TestCase):
|
||||
|
||||
controller = Controller()
|
||||
args = argparse.Namespace(
|
||||
target="remote", temp_values=(909.95, 250.0, 7, 5, 120)
|
||||
target="remote", temp_values=(909.95, 250.0, 5, 5, 120)
|
||||
)
|
||||
with mock.patch.object(ota.time, "sleep"):
|
||||
self.assertTrue(ota.request_install(controller, args, package))
|
||||
self.assertEqual(
|
||||
controller.commands,
|
||||
[
|
||||
"ota status", "tempradio 909.95,250,7,5,3", "ota install",
|
||||
"ota status", "tempradio 909.95,250,7,5,3", "ota install",
|
||||
"ota status", "tempradio 909.95,250,5,5,3", "ota install",
|
||||
"ota status", "tempradio 909.95,250,5,5,3", "ota install",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -794,7 +800,7 @@ class ReliabilityTests(unittest.TestCase):
|
||||
controller = Controller()
|
||||
args = argparse.Namespace(
|
||||
target="remote",
|
||||
temp_values=(909.95, 250.0, 7, 5, 120),
|
||||
temp_values=(909.95, 250.0, 5, 5, 120),
|
||||
require_system_watchdog_off=True,
|
||||
)
|
||||
self.assertTrue(ota.request_install(controller, args, package))
|
||||
@@ -802,7 +808,7 @@ class ReliabilityTests(unittest.TestCase):
|
||||
controller.commands,
|
||||
[
|
||||
"ota status",
|
||||
"tempradio 909.95,250,7,5,3",
|
||||
"tempradio 909.95,250,5,5,3",
|
||||
"get system.watchdog",
|
||||
"ota install",
|
||||
],
|
||||
@@ -830,7 +836,7 @@ class ReliabilityTests(unittest.TestCase):
|
||||
controller = Controller()
|
||||
args = argparse.Namespace(
|
||||
target="remote",
|
||||
temp_values=(909.95, 250.0, 7, 5, 120),
|
||||
temp_values=(909.95, 250.0, 5, 5, 120),
|
||||
require_system_watchdog_off=True,
|
||||
)
|
||||
with self.assertRaisesRegex(ota.OtaError, "must report `> off`"):
|
||||
@@ -939,7 +945,7 @@ class ReliabilityTests(unittest.TestCase):
|
||||
"release.mota", "remote",
|
||||
"--controller-serial", "/dev/controller",
|
||||
"--source-serial", "/dev/source",
|
||||
"--temp-radio", "909.950,250,7,5,114",
|
||||
"--temp-radio", "909.950,250,5,5,114",
|
||||
])
|
||||
with (
|
||||
contextlib.redirect_stderr(io.StringIO()),
|
||||
@@ -963,15 +969,15 @@ class ReliabilityTests(unittest.TestCase):
|
||||
args = argparse.Namespace(
|
||||
target="remote",
|
||||
relay_values=[("relay", "relay-secret")],
|
||||
temp_values=(909.95, 250.0, 7, 5, 120),
|
||||
temp_values=(909.95, 250.0, 5, 5, 120),
|
||||
)
|
||||
ota.shorten_target_temp_window(controller, args)
|
||||
ota.shorten_relay_temp_windows(controller, args)
|
||||
self.assertEqual(
|
||||
controller.commands,
|
||||
[
|
||||
("remote", "tempradio 909.95,250,7,5,1", None),
|
||||
("relay", "tempradio 909.95,250,7,5,1", "relay-secret"),
|
||||
("remote", "tempradio 909.95,250,5,5,1", None),
|
||||
("relay", "tempradio 909.95,250,5,5,1", "relay-secret"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -979,14 +985,14 @@ class ReliabilityTests(unittest.TestCase):
|
||||
args = argparse.Namespace(
|
||||
source_already_temp=False,
|
||||
source_shares_controller=False,
|
||||
temp_values=(909.95, 250.0, 7, 5, 120),
|
||||
temp_values=(909.95, 250.0, 5, 5, 120),
|
||||
)
|
||||
with mock.patch.object(
|
||||
ota, "source_cli_command", return_value="OK - temp params for 1 mins"
|
||||
) as source_command:
|
||||
self.assertTrue(ota.shorten_source_temp_window(args))
|
||||
source_command.assert_called_once_with(
|
||||
args, "tempradio 909.95,250,7,5,1", check=True
|
||||
args, "tempradio 909.95,250,5,5,1", check=True
|
||||
)
|
||||
|
||||
args.source_already_temp = True
|
||||
@@ -998,7 +1004,7 @@ class ReliabilityTests(unittest.TestCase):
|
||||
args = argparse.Namespace(
|
||||
source_already_temp=False,
|
||||
source_shares_controller=True,
|
||||
temp_values=(909.95, 250.0, 7, 5, 120),
|
||||
temp_values=(909.95, 250.0, 5, 5, 120),
|
||||
)
|
||||
with (
|
||||
mock.patch.object(
|
||||
|
||||
Reference in new issue
Block a user