mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 08:43:48 +00:00
Add MeshTower SD bootloader updates over LoRa
This commit is contained in:
1 parent
9e4d246ac6
commit
3d78d9c846
26 files changed
+763
-114
No files matched your search
@@ -166,7 +166,7 @@ fix, no WiFi connection, an inactive bridge, or an nRF52 bootloader without
|
||||
| LoRa OTA | [`ota get`; `ota pull`; `ota download`](ota_protocol.md#11-cli-surface-otaclicpp) | LoRa OTA build; internal-store nRF52 installs in-place deltas, while matched SD/QSPI nRF52 also accepts full images | 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) | Legacy internal-flash nRF52 LoRa OTA build with failed app-side EndF validation; shared bootloader-update builds require valid EndF | No | No | Feature |
|
||||
| LoRa OTA | [`ota bootloader`; `ota bootloader install <MID8> <HASH16>`](ota_nrf52_bootloader_update.md) | Allowlisted internal-flash or XIAO-QSPI nRF52 repeater with ABI-3 exact-board OTAFIX | No | No | Feature |
|
||||
| LoRa OTA | [`ota bootloader`; `ota bootloader install <MID8> <HASH16>`](ota_nrf52_bootloader_update.md) | Allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2 microSD nRF52 repeater with ABI-3 exact-board OTAFIX | 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 |
|
||||
|
||||
@@ -17,8 +17,8 @@ The script cannot install the destination's first OTA-capable firmware. It is
|
||||
also deliberately application-only: it rejects both v3 bootloader containers
|
||||
and a v2 container carrying the bootloader flag, and it never sends the
|
||||
privileged install command. Do the first nRF52 bootloader installation over USB.
|
||||
An already-capable, explicitly supported internal-flash or XIAO QSPI repeater
|
||||
may later use the separate manual
|
||||
An already-capable, explicitly supported internal-flash, XIAO QSPI, or exact
|
||||
MeshTower V2 microSD repeater may later use the separate manual
|
||||
[`ota bootloader install`](ota_nrf52_bootloader_update.md) workflow.
|
||||
|
||||
## Required topology
|
||||
|
||||
+3
-2
@@ -335,8 +335,9 @@ or `delta` for any supported nRF52 target. Do not select `[unsupported]` (for ex
|
||||
full image on an internal-staging nRF52).
|
||||
Rows labelled `bootloader` are also outside this ordinary application flow:
|
||||
they are never automatic and `ota install` refuses them. Only an
|
||||
already-capable, allowlisted internal-flash or XIAO-QSPI nRF52 target can use
|
||||
the separate [explicit bootloader workflow](ota_nrf52_bootloader_update.md).
|
||||
already-capable, allowlisted internal-flash, XIAO-QSPI, or exact MeshTower V2
|
||||
microSD nRF52 target can use the separate
|
||||
[explicit bootloader workflow](ota_nrf52_bootloader_update.md).
|
||||
Use the row's stable eight-hex manifest ID rather than its changing list position:
|
||||
|
||||
```text
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
The `Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors` target uses the MeshTower V2 onboard
|
||||
microSD socket as persistent storage for its own LoRa OTA downloads. It accepts
|
||||
both full `.mota` images and in-place delta `.mota` images. After verification,
|
||||
the matching SD-aware OTAFIX bootloader reads the staged file from the card and
|
||||
programs the nRF52840 application region.
|
||||
full application images, in-place application deltas, and explicitly selected
|
||||
signed v3 bootloader packages. After verification, the matching SD-aware OTAFIX
|
||||
bootloader reads the staged file from the card and programs the selected
|
||||
nRF52840 application or bootloader region.
|
||||
|
||||
The pin assignment follows the
|
||||
[Heltec MeshTower V2 partial reference circuit](https://resource.heltec.cn/download/MeshTower-V2/schematic/MeshTower_V2_Partial_Reference_Circuit.pdf):
|
||||
@@ -70,6 +71,65 @@ bootloader capability marker does not advertise SD staging and the selected
|
||||
codec. Existing `Heltec_tower_v2_repeater` firmware continues to use the
|
||||
internal-flash delta path and is unchanged.
|
||||
|
||||
## Signed bootloader update
|
||||
|
||||
Only this exact SD build exposes the privileged LoRa bootloader-update command.
|
||||
It requires an already installed exact-board ABI-3 OTAFIX bootloader whose one
|
||||
unambiguous capability marker is exactly `0x09` (`SD|BOOT_UPDATE`) and whose
|
||||
codec mask is `0x0005` (`FULL|INPLACE`). Requiring both application codecs
|
||||
prevents a bootloader self-update from disabling either normal SD application
|
||||
path. It cannot bootstrap a stock or older bootloader; provision that
|
||||
prerequisite once over USB/BLE DFU or SWD.
|
||||
|
||||
The signed v3 container is exactly 41,330 bytes and carries a 40 KiB candidate
|
||||
for the installed `239A0071 / TOWER_V2_OTA` identity (boot target `1150F50E`).
|
||||
It uses the same contiguous `/meshcore-ota.mota` file and sector-1 handoff as an
|
||||
application update. GPREGRET `0x6B` plus the distinct SD source marker `0x53`
|
||||
selects the bootloader path; LoRa transport remains payload type `0x0C`.
|
||||
|
||||
The application linker still ends at `0xED000`, but OTAFIX needs
|
||||
`0xE0000..0xEA000` as temporary scratch while replacing itself. Before a boot
|
||||
package can be downloaded or approved, MeshCore requires a hash-valid live
|
||||
`EndF` proving the complete running image ends by `0xE0000`; OTAFIX repeats
|
||||
that no-overlap check before its first erase. An application extending above
|
||||
that boundary can still receive normal FULL or delta application updates from
|
||||
SD, but its bootloader must be updated locally.
|
||||
|
||||
MeshCore also checks the live boot settings before touching that scratch page.
|
||||
Erased settings and a valid-app record with CRC disabled are allowed. When a
|
||||
nonzero bank CRC is active, its recorded `bank_0_size` must cover the complete
|
||||
EndF-inclusive running image and must end by `0xE0000`; an undersized or
|
||||
oversized record refuses the operation and requires local DFU/SWD.
|
||||
|
||||
Because the card is removable, approval is bound to the exact bytes that were
|
||||
authenticated. MeshCore verifies the signature and signer allowlist over one
|
||||
exact local manifest copy, requires the streamed manifest on SD to remain
|
||||
byte-identical, and verifies the complete payload against that manifest. It
|
||||
then writes `APRV` and syncs the card. Before publishing the raw-sector
|
||||
handoff, it writes and readback-verifies a temporary 64-byte `MOTASDBL` token
|
||||
at `0xE0000` containing the exact container length and that signed manifest's
|
||||
`image_hash`. OTAFIX requires the parsed SD manifest, streamed payload, and
|
||||
final scratch image to match the same token. Replacing or changing the card
|
||||
after verification can therefore only make the boot update fail; it cannot
|
||||
authorize different bootloader bytes. The token page becomes the first
|
||||
scratch page during a successful update and is not permanently reserved.
|
||||
|
||||
Bootloader packages are never autofetched or autoinstalled. Select and confirm
|
||||
one exact package manually:
|
||||
|
||||
```text
|
||||
ota ls
|
||||
ota pull <MID8> flash
|
||||
# wait for ota status to report the bootloader download ready
|
||||
ota bootloader
|
||||
ota bootloader install <MID8> <HASH16>
|
||||
```
|
||||
|
||||
Copy the MID and first 16 image-hash hex digits from `ota bootloader`. Ordinary
|
||||
`ota install` rejects this package, and the bootloader command rejects an
|
||||
application package. Keep the SD card inserted through the reboot. A later
|
||||
`ota status` value of `blup:C8` reports a successful bootloader replacement.
|
||||
|
||||
## SD card CLI
|
||||
|
||||
The SD-backed target provides these CLI commands:
|
||||
@@ -100,7 +160,7 @@ reset when the device reboots. The `get sdcard` age queries report how long ago
|
||||
each operation completed. `get sdcard free` reports used and free filesystem
|
||||
space in human-readable binary units.
|
||||
|
||||
`get sdcard ls` and `get sdcard dir` recursively list files on the card, four
|
||||
`get sdcard ls` and `get sdcard dir` recursively list files on the card, two
|
||||
files per page. A bare command shows page 1; append a positive page number to
|
||||
move through the remaining results. Each row includes the path and a compact
|
||||
file size. The header reports the selected page, total pages, and total files.
|
||||
|
||||
@@ -12,6 +12,33 @@ over USB/BLE DFU or SWD.
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| XIAO-module raw QSPI | below `0xE0000` | external QSPI offset 0 | dedicated internal `0xE0000..0xEA000` scratch | source `0x51`, flags `0x0E` |
|
||||
| Qualified internal-flash target | normal `0xED000` limit | shared internal slot, exact start `0xE2000` | the same eleven-page slot; no second reservation | source `0xED`, flags `0x0A` |
|
||||
| MeshTower V2 microSD | normal `0xED000` limit | contiguous `/meshcore-ota.mota` | dynamic internal `0xE0000..0xEA000` scratch; live image must end by `0xE0000` | source `0x53`, flags `0x09` |
|
||||
|
||||
The exact SD target is
|
||||
`Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors`. Its normal
|
||||
application FULL and delta updates continue to use the SD file without the
|
||||
bootloader-update scratch restriction. For a bootloader package only, both
|
||||
MeshCore and OTAFIX require a hash-valid live `EndF` proving the complete
|
||||
running image ends at or below `0xE0000`. If boot settings carry a nonzero app
|
||||
bank CRC, the recorded bank size must also cover that full EndF-inclusive
|
||||
image and stop by `0xE0000`; erased or explicitly CRC-disabled settings remain
|
||||
valid. OTAFIX then copies the verified 40
|
||||
KiB payload from SD into `0xE0000..0xEA000` and uses the MBR copy operation to
|
||||
replace `0xF4000..0xFE000`. The application linker remains at `0xED000`; a
|
||||
future application extending above `0xE0000` can still use application mOTA
|
||||
but must update its bootloader through local DFU/SWD.
|
||||
|
||||
The removable SD authorization is fail-closed. MeshCore directly authenticates
|
||||
one exact manifest, requires the streamed manifest on SD to remain
|
||||
byte-identical, and verifies its complete payload. After writing and syncing
|
||||
`APRV`, but before publishing the raw-sector handoff, it writes and
|
||||
readback-verifies a 64-byte `MOTASDBL` token at `0xE0000`. The token contains
|
||||
the exact container length and the authenticated signed manifest's
|
||||
`image_hash`. OTAFIX requires that same hash in the parsed manifest, the
|
||||
streamed raw payload, and the final scratch image. A card swap or mutation
|
||||
after application verification therefore causes refusal rather than
|
||||
authorizing different bytes; the token page is consumed as scratch during a
|
||||
successful update and is not a permanent reservation.
|
||||
|
||||
The internal path does not change the application linker or permanently set
|
||||
aside separate app-OTA, boot-package, and scratch regions. The ordinary
|
||||
@@ -86,9 +113,10 @@ ordinary full-sensor target when that module is required.
|
||||
Boards with onboard external flash are not silently redirected to internal
|
||||
staging. Mesh Solar, Nano G2 Ultra, T-Impulse Plus, ThinkNode M8, T-Echo
|
||||
Lite/Card, MeshTracker X1, and Wio WM1110 have board-specific QSPI hardware and
|
||||
need a separately matched QSPI path where available. Full Companions,
|
||||
need a separately matched QSPI path where available. Full Companions, other
|
||||
SD/QSPI/ExtraFS roles, Ethernet roles, source-only roles, and unqualified
|
||||
full-sensor roles are excluded.
|
||||
full-sensor roles are excluded from the internal-flash inventory. The exact
|
||||
MeshTower V2 SD role above is separately qualified for its SD path.
|
||||
|
||||
This internal layout is limited to nRF52840 devices with 1 MiB internal flash,
|
||||
the exact S140 v6/v7 map, and the 40 KiB boot region at
|
||||
@@ -104,8 +132,9 @@ Check the installed identity and capability marker:
|
||||
ota bootloader
|
||||
```
|
||||
|
||||
The response must show a CRC-valid exact identity, ABI 3 or newer, full codec,
|
||||
and exact flags `0x0A` for internal shared storage or `0x0E` for XIAO QSPI. A
|
||||
The response must show a CRC-valid exact identity, ABI 3 or newer, both FULL
|
||||
and INPLACE application codecs (`codecs=0x5`), and exact flags `0x09` for
|
||||
MeshTower V2 SD, `0x0A` for internal shared storage, or `0x0E` for XIAO QSPI. A
|
||||
bootloader row is visible in `ota ls`, but it is never autofetched or
|
||||
autoinstalled. Fetch its exact MID, then explicitly arm it:
|
||||
|
||||
@@ -126,25 +155,31 @@ bootloader package is not automatically resumed after an application reboot.
|
||||
Before writing `APRV`, the application authenticates and authorizes the
|
||||
package: exact v3 geometry, trusted Ed25519 signer, signed/embedded identity,
|
||||
one unambiguous capability marker, embedded CRC, sane vectors, complete
|
||||
Merkle/payload/image hashes, safe live placement, and the typed MID/hash
|
||||
confirmation. OTAFIX consumes that application-written authorization and then
|
||||
Merkle/payload/image hashes, storage-specific safe live placement, and the
|
||||
typed MID/hash confirmation. On SD, `APRV` and the internal signed-image-hash
|
||||
token are synced and read back before the handoff becomes visible. OTAFIX consumes that application-written
|
||||
authorization and then
|
||||
independently rechecks the safety/integrity subset: strict v3 structure,
|
||||
canonical identity/capabilities, vectors, full payload SHA, embedded manifest
|
||||
CRC, live `EndF` no-overlap geometry, page readback, and final copy hash. It
|
||||
does not re-run Ed25519, the signer allowlist, Merkle leaves/root, or the typed
|
||||
operator confirmation. Success is reported as `blup:C8`.
|
||||
CRC, the applicable live `EndF`/bank-settings no-overlap geometry, the SD token
|
||||
binding where applicable, scratch readback, and final
|
||||
copy hash. It does not re-run Ed25519, the signer allowlist, Merkle leaves/root,
|
||||
or the typed operator confirmation. Success is reported as `blup:C8`.
|
||||
|
||||
## Failure behavior
|
||||
|
||||
The feature fails closed when the MCU/map is wrong, current valid `EndF`
|
||||
headroom is insufficient, external/ExtraFS storage owns the target, the exact
|
||||
installed capability marker is absent or ambiguous, identity cannot be derived
|
||||
unambiguously, or any package check fails. Before compaction, the application
|
||||
and bootloader are unchanged. OTAFIX consumes the trigger/approval before the
|
||||
first in-place compaction erase, so an interrupted conversion cannot retry a
|
||||
partly converted package; it boots the untouched application and old
|
||||
bootloader instead. Use USB/BLE DFU or SWD for initial provisioning and local
|
||||
recovery.
|
||||
The feature fails closed when the MCU/map is wrong, required valid `EndF`
|
||||
headroom is insufficient, an unsupported external/ExtraFS role owns the target,
|
||||
the exact installed capability marker is absent or ambiguous, identity cannot be derived
|
||||
unambiguously, or any package check fails. Before the storage-specific
|
||||
scratch/copy step, the application and bootloader are unchanged. OTAFIX
|
||||
consumes the trigger and, for internal/QSPI storage, clears approval before its
|
||||
first scratch erase, so an interrupted operation cannot retry a partly
|
||||
consumed package. The SD backend cannot rewrite raw card sectors: its `APRV`
|
||||
and checksummed handoff may persist, but the one-shot GPREGRET trigger is
|
||||
consumed before validation and they remain inert unless the running app again
|
||||
authenticates and explicitly re-arms that package. Use USB/BLE DFU or SWD for
|
||||
initial provisioning and local recovery.
|
||||
|
||||
For XIAO and ordinary external-QSPI details, see
|
||||
[nRF52 repeater LoRa OTA with external QSPI](ota_nrf52_qspi.md).
|
||||
+16
-3
@@ -271,8 +271,9 @@ A v3 bootloader package has a deliberately narrow, non-extensible profile:
|
||||
- signed `hw_id` exactly `XIAO_BL_28860044`/`XIAO_BL_28860045` for deployed XIAO, or the zero-padded
|
||||
32-byte `NRF_BL_<BOARD_ID>_<DEVICE_NAME>` for a generic target;
|
||||
- a sane nRF52840 vector table, exactly one CRC-valid embedded manifest v1 with the exact board/name pair,
|
||||
and exactly one `MOTABLDR` marker advertising ABI >= 3, full codec, boot-update continuity, and the exact
|
||||
storage flags for the application layout (`0x0E` XIAO QSPI or `0x0A` shared internal staging).
|
||||
and exactly one `MOTABLDR` marker advertising ABI >= 3, both application codecs (`FULL|INPLACE`, mask
|
||||
`0x0005`), boot-update continuity, and the exact storage flags for the application layout (`0x09`
|
||||
MeshTower V2 SD, `0x0E` XIAO QSPI, or `0x0A` shared internal staging).
|
||||
|
||||
The incoming embedded identity must exactly match the installed CRC-valid bootloader identity. Both scans
|
||||
consider every aligned structurally valid candidate so magic bytes in a literal pool cannot shadow the real
|
||||
@@ -842,7 +843,19 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
- **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,
|
||||
verifies the staged/full result hash, and never writes through `0xED000` where InternalFS begins.
|
||||
verifies the staged/full result hash, and never writes through `0xED000` where InternalFS begins. The
|
||||
exact SD repeater also accepts a manually selected, signed v3 bootloader package when installed and
|
||||
candidate markers are exactly `0x09` (`SD|BOOT_UPDATE`). MeshCore streams the same strict identity,
|
||||
CRC, vector, signature, MID/hash-confirmation, and complete-image checks from the SD file. GPREGRET
|
||||
`0x6B` plus GPREGRET2 `0x53` selects this privileged path. Both MeshCore and OTAFIX require a hash-valid
|
||||
live `EndF` ending by `0xE0000`; when a nonzero boot-settings bank CRC is active, its recorded size must
|
||||
also cover that complete live image and stop by `0xE0000`. MeshCore authenticates one exact manifest,
|
||||
verifies the streamed SD copy is byte-identical, writes and syncs `APRV`, then writes a readback-checked
|
||||
`MOTASDBL` token at `0xE0000` containing the exact total and signed `image_hash` before publishing the
|
||||
raw-sector handoff. OTAFIX binds the parsed manifest, streamed payload, and final scratch image to that
|
||||
token, so a removable-media change can only fail closed. OTAFIX then uses `0xE0000..0xEA000` as temporary scratch; the normal
|
||||
application linker remains at `0xED000` and ordinary application updates do not inherit this scratch
|
||||
headroom restriction.
|
||||
- **Matched external-QSPI nRF52 repeaters:** the application reserves the board's dedicated QSPI NOR as a
|
||||
raw store beginning at offset zero. It obtains a 1-16 MiB capacity from JEDEC, checkpoints payload before
|
||||
leaf metadata, and verifies each erased/programmed page. GPREGRET2 `0x51` selects QSPI only when the
|
||||
|
||||
+14
-3
@@ -219,7 +219,9 @@ After it reboots, run `ota status` to confirm the new version.
|
||||
### Updating an nRF52 bootloader (advanced, explicit only)
|
||||
|
||||
This is available on specially marked no-external-flash nRF52840 lean
|
||||
repeater/bridge builds and on the legacy XIAO nRF52840/Sense raw-QSPI builds,
|
||||
repeater/bridge builds, on the legacy XIAO nRF52840/Sense raw-QSPI builds, and
|
||||
on the exact `Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors`
|
||||
microSD build,
|
||||
after a one-time exact-board ABI-3 OTAFIX installation over USB/BLE DFU or
|
||||
SWD. It is not the normal firmware update path. Check support first:
|
||||
|
||||
@@ -228,8 +230,8 @@ ota bootloader
|
||||
```
|
||||
|
||||
The reply must show a CRC-valid installed identity plus ABI 3 and the exact
|
||||
storage/boot-update capability bits for that build (`0x0A` for the shared
|
||||
internal store or `0x0E` for XIAO raw QSPI). A bootloader package appears as
|
||||
storage/boot-update capability bits for that build (`0x09` for MeshTower V2
|
||||
microSD, `0x0A` for the shared internal store, or `0x0E` for XIAO raw QSPI). A bootloader package appears as
|
||||
`bootloader` in `ota ls`. It is never downloaded or installed automatically,
|
||||
even if both OTA automation settings are enabled. Use its stable ID explicitly:
|
||||
|
||||
@@ -254,6 +256,15 @@ On an internal-flash target, the package shares the ordinary store below
|
||||
bank. A valid live `EndF` must prove the current image ends at or below that
|
||||
address before any page is erased. If `EndF` is missing/corrupt or the app is
|
||||
too large, the pull is refused and local DFU/SWD is required.
|
||||
On the MeshTower V2 SD target, the application linker remains at `0xED000`, but
|
||||
bootloader replacement needs temporary scratch beginning at `0xE0000`. A
|
||||
hash-valid live `EndF` must therefore prove the complete current image ends by
|
||||
`0xE0000`. A CRC-bound boot-settings bank must cover that complete image while
|
||||
also ending by `0xE0000`. MeshCore binds approval to the exact authenticated
|
||||
signed image hash in an internal token before handing the removable SD card to
|
||||
OTAFIX, so a later card change fails instead of authorizing different bytes.
|
||||
Larger applications can continue to use normal application mOTA;
|
||||
only bootloader self-update is refused.
|
||||
See [the nRF52 bootloader-update guide](ota_nrf52_bootloader_update.md) for the
|
||||
complete target inventory, storage layouts, and safety contract.
|
||||
|
||||
|
||||
+107
-38
@@ -485,6 +485,8 @@ void ota_reboot_to_bootloader_update() {
|
||||
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
source = GPREGRET2_OTA_STAGE_EXPANDED;
|
||||
#elif defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
source = GPREGRET2_OTA_STAGE_SD;
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
@@ -749,22 +751,10 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
static bool qspi_bootloader_crc_ok(OtaStoreQspiNrf52& store, uint32_t payload_off,
|
||||
const OtaBootloaderIdentity& identity) {
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
template <typename Store>
|
||||
static bool external_bootloader_crc_ok(Store& store, uint32_t payload_off,
|
||||
const OtaBootloaderIdentity& identity) {
|
||||
uint8_t buf[512];
|
||||
uint32_t crc = UINT32_MAX;
|
||||
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += sizeof(buf)) {
|
||||
@@ -781,10 +771,10 @@ static bool qspi_bootloader_crc_ok(OtaStoreQspiNrf52& store, uint32_t payload_of
|
||||
return ~crc == identity.crc32;
|
||||
}
|
||||
|
||||
static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t payload_off,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
OtaBootloaderIdentity& candidate,
|
||||
OtaBootloaderCapsMarker& caps) {
|
||||
template <typename Store>
|
||||
static bool external_bootloader_image_metadata(Store& store, uint32_t payload_off,
|
||||
OtaBootloaderIdentity& candidate,
|
||||
OtaBootloaderCapsMarker& caps) {
|
||||
// Keep enough overlap to catch a 44-byte manifest or 16-byte capability marker split across a
|
||||
// window. Every aligned manifest candidate is considered; a magic string in a literal pool is not
|
||||
// a terminal failure.
|
||||
@@ -804,7 +794,8 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
OtaBootloaderCapsMarker parsed;
|
||||
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
|
||||
parsed.apply_abi >= MOTA_BOOT_FORMAT_VER &&
|
||||
(parsed.codec_mask & (1u << CODEC_FULL)) != 0 &&
|
||||
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
|
||||
OTA_BL_REQUIRED_APP_CODEC_MASK &&
|
||||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
|
||||
if (++valid_caps != 1u) return false;
|
||||
if (parsed.storage_flags == ota_bootloader_update_storage_flags()) caps = parsed;
|
||||
@@ -812,10 +803,8 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
}
|
||||
if ((absolute & 3u) != 0 || local + OTA_BOOT_MANIFEST_SIZE > len) continue;
|
||||
OtaBootloaderIdentity parsed;
|
||||
if (!ota_bootloader_manifest_parse(buf + local, absolute, parsed) ||
|
||||
parsed.board_id != installed.board_id ||
|
||||
memcmp(parsed.device_name, installed.device_name, OTA_BOOT_DEVICE_NAME_SIZE) != 0) continue;
|
||||
if (qspi_bootloader_crc_ok(store, payload_off, parsed)) {
|
||||
if (!ota_bootloader_manifest_parse(buf + local, absolute, parsed)) continue;
|
||||
if (external_bootloader_crc_ok(store, payload_off, parsed)) {
|
||||
parsed.crc_ok = true;
|
||||
if (!ota_bootloader_identity_add_unique(parsed, candidate, valid_identities)) return false;
|
||||
}
|
||||
@@ -825,13 +814,30 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
|
||||
candidate.present && candidate.crc_ok && caps.present;
|
||||
}
|
||||
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg) {
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
static bool external_bootloader_approve(OtaStoreSdNrf52& store,
|
||||
const uint8_t image_hash[32]) {
|
||||
return store.approve_for_bootloader(image_hash);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
static bool external_bootloader_approve(OtaStoreQspiNrf52& store,
|
||||
const uint8_t image_hash[32]) {
|
||||
(void)image_hash;
|
||||
return store.approve_for_bootloader();
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename Store>
|
||||
static bool ota_prepare_bootloader_update_external(Store& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
const char* storage_name,
|
||||
ApplyState& st, char* msg) {
|
||||
static const size_t CAP = 96;
|
||||
st = ApplyState();
|
||||
const uint32_t total = store.staged_size();
|
||||
@@ -839,12 +845,23 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
if (total < 8u + MOTA_MFL + 5u || !store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest))) {
|
||||
strcpy(msg, "QSPI bootloader container parse failed"); return false;
|
||||
snprintf(msg, CAP, "%s bootloader container parse failed", storage_name); return false;
|
||||
}
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
|
||||
strcpy(msg, "not a valid v3 bootloader package"); return false;
|
||||
}
|
||||
// Authenticate this exact local manifest copy before any removable-media
|
||||
// reread. The streaming verifier below requires byte identity with it, and
|
||||
// the SD approval token copies its signed image_hash.
|
||||
mesh::Identity authenticated_signer(m.signer_pubkey);
|
||||
if (!authenticated_signer.verify(
|
||||
m.signature, m.manifest_start, (int)m.signed_len)) {
|
||||
strcpy(msg, "bootloader package signature invalid"); return false;
|
||||
}
|
||||
if (!allow.contains(m.signer_pubkey)) {
|
||||
strcpy(msg, "bootloader signer is not in the trusted allowlist"); return false;
|
||||
}
|
||||
switch (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
|
||||
operator_hash8)) {
|
||||
case OTA_BOOT_CONFIRM_OK: break;
|
||||
@@ -859,7 +876,8 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
|
||||
const OtaBlCaps current_caps = ota_bootloader_caps();
|
||||
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
|
||||
strcpy(msg, "installed bootloader cannot safely self-update from QSPI"); return false;
|
||||
snprintf(msg, CAP, "installed bootloader cannot safely self-update from %s", storage_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
|
||||
@@ -868,7 +886,8 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
}
|
||||
const uint32_t payload_off = (uint32_t)payload_off64;
|
||||
|
||||
VerifyResult vr = ota_verify(static_cast<const OtaStore&>(store), allow);
|
||||
VerifyResult vr = ota_verify(
|
||||
static_cast<const OtaStore&>(store), allow, manifest);
|
||||
st.manifest_ok = vr.parsed;
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
@@ -890,18 +909,68 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
}
|
||||
OtaBootloaderIdentity candidate;
|
||||
OtaBootloaderCapsMarker candidate_caps;
|
||||
if (!qspi_bootloader_image_metadata(store, payload_off, installed, candidate, candidate_caps) ||
|
||||
if (!external_bootloader_image_metadata(store, payload_off, candidate, candidate_caps) ||
|
||||
!ota_bootloader_identity_matches(installed, candidate)) {
|
||||
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
|
||||
}
|
||||
st.slot_ok = true;
|
||||
if (!store.approve_for_bootloader()) {
|
||||
snprintf(msg, CAP, "QSPI bootloader handoff failed: %s", store.last_error()); return false;
|
||||
if (!external_bootloader_approve(store, m.image_hash)) {
|
||||
snprintf(msg, CAP, "%s bootloader handoff failed: %s", storage_name, store.last_error());
|
||||
return false;
|
||||
}
|
||||
strcpy(msg, "trusted bootloader verified and armed; rebooting after this reply");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg) {
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) ||
|
||||
!ota_bootloader_scratch_headroom_valid(
|
||||
fi.valid, mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START) ||
|
||||
!ota_bootloader_live_bank_preserves_scratch(
|
||||
mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START)) {
|
||||
st = ApplyState();
|
||||
strcpy(msg, "running firmware/settings do not preserve E0000 scratch");
|
||||
return false;
|
||||
}
|
||||
return ota_prepare_bootloader_update_external(
|
||||
store, allow, installed, actual_mid, operator_mid, operator_hash8, "SD", st, msg);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(OTA_QSPI_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg) {
|
||||
return ota_prepare_bootloader_update_external(
|
||||
store, allow, installed, actual_mid, operator_mid, operator_hash8, "QSPI", st, msg);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_FLASH_STORE)
|
||||
|
||||
@@ -86,6 +86,15 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
class OtaStoreSdNrf52;
|
||||
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const uint8_t actual_mid[4],
|
||||
const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8],
|
||||
ApplyState& st, char* msg);
|
||||
#endif
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
class OtaStoreQspiNrf52;
|
||||
|
||||
@@ -34,16 +34,29 @@ static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
|
||||
static const uint8_t OTA_BL_STORAGE_QSPI = 0x04;
|
||||
static const uint8_t OTA_BL_STORAGE_BOOT_UPDATE = 0x08;
|
||||
static const uint8_t OTA_BL_STORAGE_KNOWN = 0x0F;
|
||||
static const uint8_t OTA_BL_PROFILE_SD_BOOT_UPDATE =
|
||||
OTA_BL_STORAGE_SD | OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
static const uint8_t OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
static const uint8_t OTA_BL_PROFILE_QSPI_BOOT_UPDATE =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
// A successor must retain both application update paths used by qualified
|
||||
// external stores: bit 0 CODEC_FULL and bit 2 CODEC_DETOOLS_INPLACE.
|
||||
static const uint16_t OTA_BL_REQUIRED_APP_CODEC_MASK = 0x0005u;
|
||||
|
||||
inline uint8_t ota_bootloader_update_storage_flags() {
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
// No external-storage bit means the ordinary internal flash store. The
|
||||
// same store holds either an app delta or a boot package; BOOT_UPDATE marks
|
||||
// only the privileged package capability, not a second storage backend.
|
||||
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
return OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
|
||||
#elif defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
return OTA_BL_PROFILE_QSPI_BOOT_UPDATE;
|
||||
#elif defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
// The complete container remains in the contiguous SD staging file. No
|
||||
// internal stage-ceiling capability is implied by this external store.
|
||||
return OTA_BL_PROFILE_SD_BOOT_UPDATE;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
@@ -52,7 +65,9 @@ inline uint8_t ota_bootloader_update_storage_flags() {
|
||||
inline bool ota_bootloader_self_update_caps_valid(const OtaBlCaps& c) {
|
||||
const uint8_t required = ota_bootloader_update_storage_flags();
|
||||
return required != 0 && c.present && c.apply_abi >= 3u &&
|
||||
(c.codec_mask & 1u) != 0 && c.storage_flags == required;
|
||||
(c.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
|
||||
OTA_BL_REQUIRED_APP_CODEC_MASK &&
|
||||
c.storage_flags == required;
|
||||
}
|
||||
|
||||
// Prefer a continuity-capable marker over a numerically newer legacy-looking candidate. Bootloader
|
||||
@@ -88,7 +103,8 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
|
||||
if (abi == 0 || abi == 0xFFFFu || codecs == 0 || (storage & ~OTA_BL_STORAGE_KNOWN) != 0 ||
|
||||
p[13] != 0 || p[14] != 0 || p[15] != 0) continue;
|
||||
const bool exact_profile = exact_storage_flags == 0 || storage == exact_storage_flags;
|
||||
if (require_boot_update && exact_profile && abi >= 3u && (codecs & 1u) != 0 &&
|
||||
if (require_boot_update && exact_profile && abi >= 3u &&
|
||||
(codecs & OTA_BL_REQUIRED_APP_CODEC_MASK) == OTA_BL_REQUIRED_APP_CODEC_MASK &&
|
||||
(storage & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
|
||||
// A self-update build must have one unambiguous privileged marker. Do
|
||||
// not silently choose between two otherwise valid structures (including
|
||||
@@ -118,7 +134,8 @@ inline OtaBlCaps ota_bootloader_caps() {
|
||||
const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START;
|
||||
const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END;
|
||||
return ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo),
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
true, ota_bootloader_update_storage_flags()
|
||||
#else
|
||||
false
|
||||
|
||||
@@ -31,6 +31,18 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
#if !defined(NRF52_PLATFORM) || !defined(OTA_SD_STORE)
|
||||
#error "OTA_SD_BOOTLOADER_UPDATE requires nRF52 SD staging"
|
||||
#endif
|
||||
#if !defined(HELTEC_TOWER_V2_SDCARD)
|
||||
#error "OTA_SD_BOOTLOADER_UPDATE is restricted to the qualified MeshTower V2 SD target"
|
||||
#endif
|
||||
#if defined(OTA_FLASH_STORE) || defined(OTA_QSPI_STORE) || defined(QSPIFLASH)
|
||||
#error "SD bootloader staging cannot share another OTA/filesystem store"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
@@ -45,9 +57,65 @@ static const uint32_t OTA_BOOT_IMAGE_START = 0x000F4000UL;
|
||||
static const uint32_t OTA_BOOT_IMAGE_SIZE = 0x0000A000UL; // F4000..FE000, padded to exactly 40 KiB
|
||||
static const uint32_t OTA_BOOT_SCRATCH_START = 0x000E0000UL;
|
||||
static const uint32_t OTA_BOOT_SCRATCH_END = 0x000EA000UL;
|
||||
static const uint32_t OTA_NRF52840_BOOT_SETTINGS_ADDRESS = 0x000FF000UL;
|
||||
static const uint16_t OTA_BOOT_BANK_VALID_APP = 0x0001u;
|
||||
static const uint16_t OTA_BOOT_BANK_ERASED_FALLBACK = 0xFFFFu;
|
||||
static const uint32_t OTA_NRF52840_RAM_START = 0x20000000UL;
|
||||
static const uint32_t OTA_NRF52840_RAM_END = 0x20040000UL;
|
||||
|
||||
inline bool ota_bootloader_image_geometry_valid(uint32_t image_size,
|
||||
uint32_t payload_size) {
|
||||
return image_size == OTA_BOOT_IMAGE_SIZE && payload_size == OTA_BOOT_IMAGE_SIZE;
|
||||
}
|
||||
|
||||
// External SD keeps the normal ED000 application linker, but OTAFIX needs the
|
||||
// XIAO-sized E0000..EA000 internal scratch range temporarily while replacing
|
||||
// itself. A hash-valid live EndF must prove the complete running image ends
|
||||
// before that scratch range; a linker maximum alone is not sufficient.
|
||||
inline bool ota_bootloader_scratch_headroom_valid(bool self_valid,
|
||||
uint32_t app_base,
|
||||
uint32_t image_len,
|
||||
uint32_t scratch_start) {
|
||||
return self_valid && app_base < scratch_start && image_len != 0 &&
|
||||
image_len <= scratch_start - app_base;
|
||||
}
|
||||
|
||||
// The bootloader may validate BANK_VALID_APP over bank_0_size on every reset.
|
||||
// Erasing the SD authorization-token page must not change bytes covered by a
|
||||
// nonzero stored CRC. A CRC-bound bank must cover the complete hash-valid live
|
||||
// image but stop by scratch_start; a smaller recorded size would disagree with
|
||||
// OTAFIX even if it did not overlap scratch. Debugger/UF2 installs can have
|
||||
// erased settings, and a valid-app record with CRC zero explicitly disables
|
||||
// that CRC check.
|
||||
inline bool ota_bootloader_scratch_bank_geometry_valid(uint16_t bank_0,
|
||||
uint16_t bank_0_crc,
|
||||
uint32_t bank_0_size,
|
||||
uint32_t live_image_size,
|
||||
uint32_t safe_span) {
|
||||
if (bank_0 == OTA_BOOT_BANK_ERASED_FALLBACK) return true;
|
||||
if (bank_0 != OTA_BOOT_BANK_VALID_APP) return false;
|
||||
if (bank_0_crc == 0u) return true;
|
||||
return live_image_size != 0u && bank_0_size >= live_image_size &&
|
||||
bank_0_size <= safe_span;
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
inline bool ota_bootloader_live_bank_preserves_scratch(uint32_t app_base,
|
||||
uint32_t live_image_size,
|
||||
uint32_t scratch_start) {
|
||||
if (app_base >= scratch_start) return false;
|
||||
const volatile uint8_t* raw =
|
||||
(const volatile uint8_t*)(uintptr_t)OTA_NRF52840_BOOT_SETTINGS_ADDRESS;
|
||||
const uint16_t bank_0 = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
|
||||
const uint16_t bank_0_crc = (uint16_t)(raw[2] | ((uint16_t)raw[3] << 8));
|
||||
const uint32_t bank_0_size = (uint32_t)raw[8] | ((uint32_t)raw[9] << 8) |
|
||||
((uint32_t)raw[10] << 16) | ((uint32_t)raw[11] << 24);
|
||||
return ota_bootloader_scratch_bank_geometry_valid(
|
||||
bank_0, bank_0_crc, bank_0_size, live_image_size,
|
||||
scratch_start - app_base);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Only the two official XIAO nRF52840 OTAFIX identities are eligible. Runtime matching against the
|
||||
// installed, CRC-valid manifest distinguishes the base and Sense modules even for carrier-board aliases.
|
||||
static const uint32_t OTA_XIAO_BOARD_ID_BASE = 0x28860044UL;
|
||||
@@ -284,7 +352,8 @@ inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_si
|
||||
OtaBootloaderCapsMarker parsed;
|
||||
if (!ota_bootloader_caps_marker_parse(image + off, parsed) ||
|
||||
parsed.apply_abi < MOTA_BOOT_FORMAT_VER ||
|
||||
(parsed.codec_mask & (1u << CODEC_FULL)) == 0 ||
|
||||
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) !=
|
||||
OTA_BL_REQUIRED_APP_CODEC_MASK ||
|
||||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) == 0) continue;
|
||||
if (++valid_count != 1u) return false; // privileged marker identity must be unambiguous
|
||||
if (parsed.storage_flags != exact_storage_flags) continue;
|
||||
|
||||
@@ -189,6 +189,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | qspi | "
|
||||
"folder | config | key");
|
||||
#endif
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | "
|
||||
"folder | cache | config | key");
|
||||
#else
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
|
||||
"cache | config | key");
|
||||
#endif
|
||||
#else
|
||||
snprintf(reply, 160,
|
||||
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
|
||||
@@ -328,6 +338,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
!defined(OTA_QSPI_STORE)
|
||||
SelfFwInfo list_self;
|
||||
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
SelfFwInfo list_sd_self;
|
||||
const bool list_sd_headroom = ota_self_firmware(list_sd_self) &&
|
||||
ota_bootloader_scratch_headroom_valid(
|
||||
list_sd_self.valid, mota_nrf52_app_base(), list_sd_self.image_len,
|
||||
OTA_BOOT_SCRATCH_START) &&
|
||||
ota_bootloader_live_bank_preserves_scratch(
|
||||
mota_nrf52_app_base(), list_sd_self.image_len,
|
||||
OTA_BOOT_SCRATCH_START);
|
||||
#endif
|
||||
uint32_t now = millis(); int shown = 0;
|
||||
uint16_t first = (uint16_t)(page - 1) * PAGE_SIZE;
|
||||
@@ -355,12 +375,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (boot_package) {
|
||||
bool installable = false;
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
installable = h->flags == (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) &&
|
||||
h->codec == CODEC_FULL && bid.present && bid.crc_ok &&
|
||||
h->target_id == ota_bootloader_target_id(bid) &&
|
||||
ota_bootloader_self_update_caps_valid(list_bl);
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
installable = installable && list_sd_headroom;
|
||||
#endif
|
||||
#endif
|
||||
fit = installable ? "yours" : "bootloader unsupported";
|
||||
} else if (myt && h->target_id == myt) {
|
||||
@@ -489,7 +513,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#else
|
||||
if (selboot) {
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
|
||||
@@ -505,6 +530,19 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
|
||||
return true;
|
||||
}
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
SelfFwInfo sd_self;
|
||||
if (!ota_self_firmware(sd_self) ||
|
||||
!ota_bootloader_scratch_headroom_valid(
|
||||
sd_self.valid, mota_nrf52_app_base(), sd_self.image_len,
|
||||
OTA_BOOT_SCRATCH_START) ||
|
||||
!ota_bootloader_live_bank_preserves_scratch(
|
||||
mota_nrf52_app_base(), sd_self.image_len,
|
||||
OTA_BOOT_SCRATCH_START)) {
|
||||
strcpy(reply, "ERR running firmware/settings do not preserve E0000 scratch; use local DFU/SWD");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
strcpy(reply, "ERR this build cannot install bootloader packages; use folder capture or USB DFU");
|
||||
return true;
|
||||
@@ -716,7 +754,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
|
||||
} else if (is_cmd(a, "bootloader|blupdate", &rest)) {
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
if (*rest == 0 || strcmp(rest, "status") == 0) {
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh).
|
||||
// Holds the session engine, a persistent staging store (fetch), a RAM serve buffer, and the signer
|
||||
// allowlist. nRF52 uses one of three stores selected by the exact target: internal flash for in-place
|
||||
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. Flash-backed stores
|
||||
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. One exact microSD
|
||||
// target also accepts explicitly confirmed signed bootloader packages. Flash-backed stores
|
||||
// coalesce writes at their erase-page boundary and checkpoint payload before leaf metadata, so an
|
||||
// interrupted fetch can be verified and resumed without trusting stale progress markers.
|
||||
|
||||
@@ -238,7 +239,8 @@ struct OtaContext {
|
||||
bool apply_fetched_bootloader(const uint8_t operator_mid[4],
|
||||
const uint8_t operator_hash8[8], char* msg) {
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
if (fetch_to_folder) {
|
||||
strncpy(msg, "refused: bootloader package is not in local install storage", 96);
|
||||
msg[95] = 0; return false;
|
||||
@@ -518,7 +520,8 @@ struct OtaContext {
|
||||
manager.set_apply_codec2(CODEC_DETOOLS_INPLACE); // also accepted -> a single in-place .mota fits both
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
|
||||
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
|
||||
defined(OTA_SD_BOOTLOADER_UPDATE))
|
||||
manager.set_accept_bootloader(true);
|
||||
#else
|
||||
manager.set_accept_bootloader(false);
|
||||
|
||||
@@ -52,6 +52,7 @@ static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_SD = 0x53u;
|
||||
|
||||
inline uint8_t mota_nrf52_flash_stage_handoff(uint32_t effective_stage_ceiling) {
|
||||
if (effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Internal authorization token for a bootloader package staged on removable SD.
|
||||
// Keep byte-identical with OTAFIX src/ota_sd_boot_token.h.
|
||||
#ifndef OTA_SD_BOOT_TOKEN_H_
|
||||
#define OTA_SD_BOOT_TOKEN_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MOTA_SD_BOOT_TOKEN_VERSION 1u
|
||||
#define MOTA_SD_BOOT_TOKEN_LEN 64u
|
||||
#define MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET 24u
|
||||
#define MOTA_SD_BOOT_TOKEN_CRC_OFFSET 56u
|
||||
|
||||
static const uint8_t MOTA_SD_BOOT_TOKEN_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'S', 'D', 'B', 'L'
|
||||
};
|
||||
|
||||
static inline uint32_t mota_sd_boot_token_rd32(const uint8_t *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static inline void mota_sd_boot_token_wr32(uint8_t *p, uint32_t value) {
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
p[2] = (uint8_t)(value >> 16);
|
||||
p[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
static inline uint32_t mota_sd_boot_token_crc32(const uint8_t *data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & (uint32_t)-(int32_t)(crc & 1u));
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
// image_hash is copied from the exact signed manifest buffer authenticated by
|
||||
// the application. It binds the only bootloader bytes that the MBR can install.
|
||||
static inline void mota_sd_boot_token_encode(uint8_t out[MOTA_SD_BOOT_TOKEN_LEN],
|
||||
uint32_t total,
|
||||
const uint8_t image_hash[32]) {
|
||||
memset(out, 0, MOTA_SD_BOOT_TOKEN_LEN);
|
||||
memcpy(out, MOTA_SD_BOOT_TOKEN_MAGIC, sizeof(MOTA_SD_BOOT_TOKEN_MAGIC));
|
||||
mota_sd_boot_token_wr32(out + 8u, MOTA_SD_BOOT_TOKEN_VERSION);
|
||||
mota_sd_boot_token_wr32(out + 12u, MOTA_SD_BOOT_TOKEN_LEN);
|
||||
mota_sd_boot_token_wr32(out + 16u, total);
|
||||
mota_sd_boot_token_wr32(out + 20u, ~total);
|
||||
memcpy(out + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, image_hash, 32u);
|
||||
const uint32_t crc = mota_sd_boot_token_crc32(out, MOTA_SD_BOOT_TOKEN_CRC_OFFSET);
|
||||
mota_sd_boot_token_wr32(out + MOTA_SD_BOOT_TOKEN_CRC_OFFSET, crc);
|
||||
mota_sd_boot_token_wr32(out + MOTA_SD_BOOT_TOKEN_CRC_OFFSET + 4u, ~crc);
|
||||
}
|
||||
|
||||
static inline int mota_sd_boot_token_valid(const uint8_t token[MOTA_SD_BOOT_TOKEN_LEN],
|
||||
uint32_t total,
|
||||
const uint8_t image_hash[32]) {
|
||||
const uint32_t crc = mota_sd_boot_token_rd32(token + MOTA_SD_BOOT_TOKEN_CRC_OFFSET);
|
||||
return memcmp(token, MOTA_SD_BOOT_TOKEN_MAGIC, sizeof(MOTA_SD_BOOT_TOKEN_MAGIC)) == 0 &&
|
||||
mota_sd_boot_token_rd32(token + 8u) == MOTA_SD_BOOT_TOKEN_VERSION &&
|
||||
mota_sd_boot_token_rd32(token + 12u) == MOTA_SD_BOOT_TOKEN_LEN &&
|
||||
mota_sd_boot_token_rd32(token + 16u) == total &&
|
||||
mota_sd_boot_token_rd32(token + 20u) == ~total &&
|
||||
memcmp(token + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, image_hash, 32u) == 0 &&
|
||||
mota_sd_boot_token_crc32(token, MOTA_SD_BOOT_TOKEN_CRC_OFFSET) == crc &&
|
||||
mota_sd_boot_token_rd32(token + MOTA_SD_BOOT_TOKEN_CRC_OFFSET + 4u) == ~crc;
|
||||
}
|
||||
|
||||
#endif // OTA_SD_BOOT_TOKEN_H_
|
||||
@@ -9,9 +9,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "MeshCore.h"
|
||||
#include "OtaBootloaderUpdate.h"
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "OtaSdBootToken.h"
|
||||
#include "OtaSdHandoff.h"
|
||||
#include "OtaSelf.h"
|
||||
#include "flash/flash_nrf5x.h"
|
||||
|
||||
#ifndef OTA_SD_CS_PIN
|
||||
#define OTA_SD_CS_PIN PIN_SPI1_NSS
|
||||
@@ -44,6 +48,7 @@ void OtaStoreSdNrf52::resetStoreState() {
|
||||
_allocated_sectors = 0;
|
||||
_partition_start = 0;
|
||||
_partition_end = 0;
|
||||
_planned_bootloader = false;
|
||||
if (_file && *_file) _file->close();
|
||||
if (_sd) _sd->end();
|
||||
_mounted = false;
|
||||
@@ -370,7 +375,19 @@ bool OtaStoreSdNrf52::locate_file() {
|
||||
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t image_size,
|
||||
uint32_t, uint32_t payload_size,
|
||||
bool is_bootloader) {
|
||||
if (is_bootloader) return false;
|
||||
_planned_bootloader = false;
|
||||
if (is_bootloader) {
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
if (ota_bootloader_image_geometry_valid(image_size, payload_size)) {
|
||||
_planned_bootloader = true;
|
||||
return true;
|
||||
}
|
||||
fail("bootloader package geometry mismatch");
|
||||
#else
|
||||
fail("bootloader update is not enabled for this SD target");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
|
||||
image_size > MOTA_NRF52_APP_END - app_base) {
|
||||
@@ -464,6 +481,7 @@ void OtaStoreSdNrf52::checkpoint() {
|
||||
|
||||
bool OtaStoreSdNrf52::reopen() {
|
||||
_total = 0;
|
||||
_planned_bootloader = false;
|
||||
if (!mount()) return false;
|
||||
if (*_file) _file->close();
|
||||
if (!_file->open(PATH, O_RDWR)) return false;
|
||||
@@ -480,6 +498,32 @@ bool OtaStoreSdNrf52::reopen() {
|
||||
return false;
|
||||
}
|
||||
_total = total;
|
||||
uint8_t manifest[MOTA_MFL];
|
||||
MotaManifest parsed;
|
||||
if (total < 8u + sizeof(manifest) + 5u || !_file->seekSet(8u) ||
|
||||
_file->read(manifest, sizeof(manifest)) != (int)sizeof(manifest) ||
|
||||
!mota_parse_manifest(manifest, sizeof(manifest), parsed)) {
|
||||
_total = 0;
|
||||
_file->close();
|
||||
return false;
|
||||
}
|
||||
if (parsed.is_bootloader()) {
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
const uint64_t payload_off = 8u + sizeof(manifest) +
|
||||
(uint64_t)parsed.block_count * 4u;
|
||||
if (!ota_bootloader_image_geometry_valid(parsed.image_size, parsed.payload_size) ||
|
||||
payload_off + parsed.payload_size + 5u != total) {
|
||||
_total = 0;
|
||||
_file->close();
|
||||
return false;
|
||||
}
|
||||
_planned_bootloader = true;
|
||||
#else
|
||||
_total = 0;
|
||||
_file->close();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
if (!locate_file()) {
|
||||
_total = 0;
|
||||
_file->close();
|
||||
@@ -490,6 +534,7 @@ bool OtaStoreSdNrf52::reopen() {
|
||||
|
||||
void OtaStoreSdNrf52::clear() {
|
||||
_total = 0;
|
||||
_planned_bootloader = false;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
if (!mount()) return;
|
||||
@@ -498,14 +543,54 @@ void OtaStoreSdNrf52::clear() {
|
||||
_sd->remove(PATH);
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::approve_for_bootloader() {
|
||||
bool OtaStoreSdNrf52::approve_for_bootloader(
|
||||
const uint8_t expected_boot_image_hash[32]) {
|
||||
if (!_total || !finalize()) return false;
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
if (_planned_bootloader) {
|
||||
SelfFwInfo fi;
|
||||
if (!expected_boot_image_hash ||
|
||||
_total != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
|
||||
!ota_self_firmware(fi) ||
|
||||
!ota_bootloader_scratch_headroom_valid(
|
||||
fi.valid, mota_nrf52_app_base(), fi.image_len,
|
||||
OTA_BOOT_SCRATCH_START) ||
|
||||
!ota_bootloader_live_bank_preserves_scratch(
|
||||
mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START)) {
|
||||
fail("running firmware/settings do not preserve boot scratch");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!write(8 + MOTA_OFF_APPROVAL, APPROVAL_YES, sizeof(APPROVAL_YES)) ||
|
||||
!_file->sync() || !_sd->card()->syncDevice()) {
|
||||
fail("SD approval write failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
if (_planned_bootloader) {
|
||||
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
|
||||
mota_sd_boot_token_encode(token, _total, expected_boot_image_hash);
|
||||
|
||||
// This page is outside the hash-valid live app and below InternalFS. The
|
||||
// token is temporary: OTAFIX validates it against the current SD bytes,
|
||||
// then consumes the page as the first bootloader scratch page.
|
||||
flash_nrf5x_flush();
|
||||
if (!flash_nrf5x_erase(OTA_BOOT_SCRATCH_START) ||
|
||||
flash_nrf5x_write(OTA_BOOT_SCRATCH_START, token, sizeof(token)) < 0) {
|
||||
fail("SD bootloader token flash write failed");
|
||||
return false;
|
||||
}
|
||||
flash_nrf5x_flush();
|
||||
if (memcmp((const void*)(uintptr_t)OTA_BOOT_SCRATCH_START,
|
||||
token, sizeof(token)) != 0) {
|
||||
fail("SD bootloader token flash verify failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) {
|
||||
fail("SD handoff sector read failed");
|
||||
|
||||
@@ -18,6 +18,8 @@ class OtaCacheSdNrf52;
|
||||
// Persistent nRF52840 OTA store backed by the MeshTower V2 microSD socket.
|
||||
// The .mota remains a normal file, but is preallocated contiguously so the
|
||||
// bootloader can read it by raw sectors without embedding a FAT implementation.
|
||||
// The exact self-update build uses this same one-file store for either an
|
||||
// application package or an explicitly confirmed signed bootloader package.
|
||||
class OtaStoreSdNrf52 : public OtaStore {
|
||||
public:
|
||||
OtaStoreSdNrf52();
|
||||
@@ -36,9 +38,14 @@ public:
|
||||
uint32_t payload_off, uint32_t payload_size,
|
||||
bool is_bootloader) override;
|
||||
|
||||
// Called only after the app has verified payload, base, signature and trust.
|
||||
// Writes APRV into the file, then publishes the raw-sector handoff record.
|
||||
bool approve_for_bootloader();
|
||||
// Called only after the app has verified package kind, payload, applicable
|
||||
// base/identity, signature, trust, and storage-specific safety geometry.
|
||||
// For a bootloader package it writes APRV, binds the exact authenticated
|
||||
// signed image_hash to an internal-flash token, and only then publishes the
|
||||
// raw-sector handoff. Application packages retain the ordinary APRV+handoff
|
||||
// path and omit expected_boot_image_hash.
|
||||
bool approve_for_bootloader(
|
||||
const uint8_t expected_boot_image_hash[32] = nullptr);
|
||||
bool formatCard(MainBoard& board);
|
||||
bool eraseCard(MainBoard& board);
|
||||
bool getSpace(MainBoard& board, uint64_t& used_bytes, uint64_t& free_bytes);
|
||||
@@ -60,6 +67,7 @@ private:
|
||||
SdFs* _sd = nullptr;
|
||||
mutable FsFile* _file = nullptr;
|
||||
bool _mounted = false;
|
||||
bool _planned_bootloader = false;
|
||||
uint32_t _total = 0;
|
||||
uint32_t _first_sector = 0;
|
||||
uint32_t _allocated_sectors = 0;
|
||||
|
||||
@@ -28,7 +28,9 @@ VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist&
|
||||
return r;
|
||||
}
|
||||
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
|
||||
static VerifyResult ota_verify_store(const OtaStore& store,
|
||||
const SignerAllowlist& allow,
|
||||
const uint8_t* expected_manifest) {
|
||||
VerifyResult r;
|
||||
const uint32_t total = store.staged_size();
|
||||
uint8_t hdr[8], manifest[MOTA_MFL], trailer[5];
|
||||
@@ -36,6 +38,8 @@ VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
|
||||
!store.read(0, hdr, sizeof(hdr)) ||
|
||||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
|
||||
!store.read(8, manifest, sizeof(manifest)) ||
|
||||
(expected_manifest &&
|
||||
memcmp(manifest, expected_manifest, sizeof(manifest)) != 0) ||
|
||||
!store.read(total - 5, trailer, sizeof(trailer)) ||
|
||||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) return r;
|
||||
|
||||
@@ -99,5 +103,15 @@ VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
|
||||
return r;
|
||||
}
|
||||
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
|
||||
return ota_verify_store(store, allow, nullptr);
|
||||
}
|
||||
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow,
|
||||
const uint8_t expected_manifest[MOTA_MFL]) {
|
||||
if (!expected_manifest) return VerifyResult();
|
||||
return ota_verify_store(store, allow, expected_manifest);
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -31,5 +31,11 @@ VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist&
|
||||
// contiguous memory view (notably the MeshTower V2 SD-backed nRF52 store).
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow);
|
||||
|
||||
// As above, but the store's manifest must be byte-identical to an already
|
||||
// authenticated local copy. This binds removable-media payload verification
|
||||
// to the exact signed anchors later written into an internal approval token.
|
||||
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow,
|
||||
const uint8_t expected_manifest[MOTA_MFL]);
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
+1
-1
@@ -70,7 +70,7 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_user_gpio` | `src/helpers/UserGpio.cpp`, `UserGpioReplyTracker.h` | board-approved pins, get/set/reset, timed nonblocking transitions, duplicate suppression, rollover, and completion-reply routing |
|
||||
| `test_utf8_helpers` | `src/helpers/UTF8Helpers.h` | byte-limit truncation at complete code-point boundaries and rejection of malformed or truncated UTF-8 |
|
||||
| `test_wifi_ota_seeder_policy` | `src/helpers/WiFiOtaSeederPolicy.h`, `WiFiOtaSeederStatus.h` | listener state versus network availability, serial/TCP folder ownership, detach detection, and bounded status formatting |
|
||||
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO and generic embedded identity, vector, capability, explicit-confirmation, codec-isolation, shared-slot placement/no-EndF erase gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
|
||||
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO, generic internal, and exact MeshTower V2 SD embedded identity, vector, capability, explicit-confirmation, codec-isolation, scratch-headroom/shared-slot no-EndF gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
|
||||
| `test_trace_retry` | `src/Mesh.cpp` retry and relay policy | app-v2 and boot-v3 traffic sharing `PAYLOAD_TYPE_OTA=0x0C` and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing |
|
||||
| `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) |
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "helpers/ota/OtaManager.h"
|
||||
#include "helpers/ota/OtaApply.h"
|
||||
#include "helpers/ota/OtaBlInfo.h"
|
||||
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
||||
#include "helpers/ota/OtaSdBootToken.h"
|
||||
#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py
|
||||
|
||||
extern "C" {
|
||||
@@ -41,12 +43,74 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x00), 0x00);
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x4C), 0x00); // shutdown reason
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x51), 0x00); // QSPI handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x53), 0x00); // SD handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xD4), 0x00); // legacy handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xED), 0x00); // expanded handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xE0), 0x00); // retired handoff remains non-result
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0x00);
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, StorageProfilesAndSdGeometryAreExact) {
|
||||
EXPECT_EQ(OTA_BL_PROFILE_SD_BOOT_UPDATE, 0x09u);
|
||||
EXPECT_EQ(OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE, 0x0Au);
|
||||
EXPECT_EQ(OTA_BL_PROFILE_QSPI_BOOT_UPDATE, 0x0Eu);
|
||||
EXPECT_EQ(OTA_BL_REQUIRED_APP_CODEC_MASK, 0x0005u);
|
||||
EXPECT_EQ(GPREGRET2_OTA_STAGE_SD, 0x53u);
|
||||
EXPECT_TRUE(ota_bootloader_image_geometry_valid(
|
||||
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE));
|
||||
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
|
||||
OTA_BOOT_IMAGE_SIZE - 1u, OTA_BOOT_IMAGE_SIZE));
|
||||
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
|
||||
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE - 1u));
|
||||
EXPECT_TRUE(ota_bootloader_scratch_headroom_valid(
|
||||
true, MOTA_NRF52_APP_BASE_S140_V6,
|
||||
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6,
|
||||
OTA_BOOT_SCRATCH_START));
|
||||
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
|
||||
false, MOTA_NRF52_APP_BASE_S140_V6, 1u, OTA_BOOT_SCRATCH_START));
|
||||
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
|
||||
true, MOTA_NRF52_APP_BASE_S140_V6,
|
||||
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6 + 1u,
|
||||
OTA_BOOT_SCRATCH_START));
|
||||
|
||||
const uint32_t safe_span = OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6;
|
||||
const uint32_t live_size = safe_span - 0x1000u;
|
||||
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
OTA_BOOT_BANK_ERASED_FALLBACK, 0xFFFFu, UINT32_MAX, live_size, safe_span));
|
||||
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
OTA_BOOT_BANK_VALID_APP, 0u, UINT32_MAX, live_size, safe_span));
|
||||
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
OTA_BOOT_BANK_VALID_APP, 1u, live_size, live_size, safe_span));
|
||||
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
OTA_BOOT_BANK_VALID_APP, 1u, live_size - 1u, live_size, safe_span));
|
||||
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
OTA_BOOT_BANK_VALID_APP, 1u, safe_span + 1u, live_size, safe_span));
|
||||
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
||||
0x00FFu, 0u, 0u, live_size, safe_span));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, SdApprovalTokenBindsTheAuthenticatedImageHash) {
|
||||
uint8_t hash[32];
|
||||
for (uint8_t i = 0; i < sizeof(hash); i++) hash[i] = i;
|
||||
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
|
||||
mota_sd_boot_token_encode(token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash);
|
||||
EXPECT_EQ(0, memcmp(token, "MOTASDBL", 8));
|
||||
EXPECT_EQ(mota_sd_boot_token_rd32(token + 8), 1u);
|
||||
EXPECT_EQ(mota_sd_boot_token_rd32(token + 12), 64u);
|
||||
EXPECT_EQ(mota_sd_boot_token_rd32(token + 16), MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
||||
EXPECT_EQ(mota_sd_boot_token_rd32(token + 20), ~MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
||||
EXPECT_EQ(0, memcmp(token + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, hash, sizeof(hash)));
|
||||
EXPECT_TRUE(mota_sd_boot_token_valid(
|
||||
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
|
||||
|
||||
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
|
||||
EXPECT_FALSE(mota_sd_boot_token_valid(
|
||||
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
|
||||
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
|
||||
EXPECT_FALSE(mota_sd_boot_token_valid(
|
||||
token, MOTA_NRF52_BOOT_CONTAINER_SIZE + 1u, hash));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) {
|
||||
const uint32_t shared_board_id = 0x239A0029u;
|
||||
const uint8_t rak3401_name[16] = {'3','4','0','1','_','D','F','U',0,0,0,0,0,0,0,0};
|
||||
@@ -209,7 +273,7 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
image[0x300] ^= 1;
|
||||
EXPECT_FALSE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
|
||||
|
||||
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 1,0,
|
||||
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
||||
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
||||
OtaBootloaderCapsMarker caps;
|
||||
EXPECT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
@@ -218,7 +282,7 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
|
||||
std::vector<uint8_t> internal_caps_image(64, 0xFF);
|
||||
const uint8_t internal_marker[16] = {
|
||||
'M','O','T','A','B','L','D','R', 3,0, 1,0,
|
||||
'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
||||
(uint8_t)(OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
||||
memcpy(internal_caps_image.data() + 4, internal_marker, sizeof(internal_marker));
|
||||
EXPECT_TRUE(ota_bootloader_caps_from_image(
|
||||
@@ -237,6 +301,14 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
caps));
|
||||
|
||||
std::vector<uint8_t> full_only_caps(32, 0xFF);
|
||||
memcpy(full_only_caps.data() + 4, internal_marker, sizeof(internal_marker));
|
||||
full_only_caps[4 + 10] = 1;
|
||||
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
||||
full_only_caps.data(), full_only_caps.size(),
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
caps));
|
||||
|
||||
OtaBlCaps selected;
|
||||
selected.present = true;
|
||||
selected.apply_abi = 3;
|
||||
@@ -270,7 +342,7 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
||||
const uint8_t qspi_profile =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 1,0,
|
||||
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
||||
qspi_profile, 0,0,0};
|
||||
uint8_t image[64];
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
|
||||
@@ -58,17 +58,21 @@ Ordinary calls to `build_manifest()` continue to emit v2 application packages.
|
||||
Passing `bootloader=True` is a deliberately narrow reference-only path: it
|
||||
requires a nonzero version, a signed exact 40 KiB OTAFIX region, 1024-byte
|
||||
blocks, full codec, zero base hash, a sane vector table, an exact embedded
|
||||
manifest/CRC identity, and an ABI-3 marker retaining the selected storage plus
|
||||
boot-update capabilities. Deployed XIAO packages retain their
|
||||
manifest/CRC identity, and an ABI-3 marker retaining both application codecs
|
||||
(`FULL|INPLACE`, mask `0x0005`), the selected storage, and boot-update
|
||||
capabilities. Deployed XIAO packages retain their
|
||||
`XIAO_BL_28860044`/`XIAO_BL_28860045` IDs. Generic packages derive the padded
|
||||
`NRF_BL_<BOARD_ID>_<DEVICE_NAME>` ID and collision-checked wire target from the
|
||||
full embedded manifest pair. Parsing and `verify()` repeat those gates; magic
|
||||
literals that are not complete valid structures are skipped.
|
||||
|
||||
Generic parsing remains available for candidate inspection, but the signing
|
||||
builder accepts only identities in the qualified internal inventory plus the
|
||||
two deployed XIAO identities. The test suite verifies those boot target IDs are
|
||||
unique and disjoint from the generated application target table.
|
||||
builder accepts only identities in the qualified inventory. That inventory
|
||||
currently covers the shared-internal nRF52840 targets, the two deployed XIAO
|
||||
raw-QSPI identities, and the exact MeshTower V2 identity whose candidate
|
||||
marker may select either its internal (`0x0A`) or microSD (`0x09`) application
|
||||
layout. The test suite verifies those boot target IDs are unique and disjoint
|
||||
from the generated application target table.
|
||||
|
||||
This library does not authorize a device update. A capable node will only arm
|
||||
such a v3 package through the exact manual confirmation described in
|
||||
|
||||
+26
-8
@@ -92,13 +92,17 @@ XIAO_BOOT_MANIFEST_VERSION = 1
|
||||
XIAO_BOOT_MANIFEST_SIZE = 44
|
||||
XIAO_BOOT_DEVICE_NAME = b"XIAO_DFU".ljust(16, b"\0")
|
||||
XIAO_BOOT_CAPS_MAGIC = b"MOTABLDR"
|
||||
BOOT_STORAGE_SD = 0x01
|
||||
BOOT_STORAGE_STAGE_CEILING = 0x02
|
||||
XIAO_BOOT_STORAGE_QSPI = 0x04
|
||||
XIAO_BOOT_STORAGE_UPDATE = 0x08
|
||||
BOOT_STORAGE_KNOWN = 0x0F
|
||||
BOOT_STORAGE_SD_UPDATE = BOOT_STORAGE_SD | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_STORAGE_QSPI_UPDATE = (BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_QSPI |
|
||||
XIAO_BOOT_STORAGE_UPDATE)
|
||||
BOOT_STORAGE_INTERNAL_UPDATE = BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
|
||||
(1 << CODEC_DETOOLS_INPLACE))
|
||||
|
||||
# Exact embedded manifests for the qualified shared-internal nRF52840
|
||||
# bootloaders. Parsing/inspection remains generic, but package creation is
|
||||
@@ -121,6 +125,14 @@ INTERNAL_BOOTLOADER_IDENTITIES = (
|
||||
(0x239A0029, "RTAG_DFU"),
|
||||
)
|
||||
|
||||
# MeshTower V2 has both a lean internal-store application and an exact SD-store
|
||||
# application. They intentionally retain the same embedded bootloader identity
|
||||
# and signed wire target; the candidate capability marker selects the matching
|
||||
# storage contract and is checked again by the device before approval.
|
||||
SD_BOOTLOADER_IDENTITIES = (
|
||||
(0x239A0071, "TOWER_V2_OTA"),
|
||||
)
|
||||
|
||||
# MeshTower V2's SD-backed OTA target keeps the staged .mota off-chip, so the application may use the
|
||||
# complete S140 v6 application region up to InternalFS instead of leaving room for internal staging.
|
||||
# This is deliberately target-specific: other nRF52 OTA builds still need NRF52_INPLACE_MEMORY above.
|
||||
@@ -659,9 +671,11 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
continue
|
||||
abi, codecs = struct.unpack_from("<HH", image, off + 8)
|
||||
storage = image[off + 12]
|
||||
if (abi < BOOT_FORMAT_VER or abi == 0xFFFF or not codecs & (1 << CODEC_FULL) or
|
||||
if (abi < BOOT_FORMAT_VER or abi == 0xFFFF or
|
||||
codecs & BOOT_REQUIRED_APP_CODEC_MASK != BOOT_REQUIRED_APP_CODEC_MASK or
|
||||
storage & ~BOOT_STORAGE_KNOWN or image[off + 13:off + 16] != b"\0\0\0" or
|
||||
storage not in (BOOT_STORAGE_QSPI_UPDATE, BOOT_STORAGE_INTERNAL_UPDATE)):
|
||||
storage not in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
|
||||
BOOT_STORAGE_INTERNAL_UPDATE)):
|
||||
continue
|
||||
if found is not None:
|
||||
return None
|
||||
@@ -688,12 +702,16 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
expected_hw = bootloader_hw_id(identity.board_id, identity.device_name)
|
||||
if signed_hw_id is not None and bytes(signed_hw_id) != expected_hw:
|
||||
raise ValueError("bootloader signed hw_id does not match embedded identity")
|
||||
expected_storage = (BOOT_STORAGE_QSPI_UPDATE
|
||||
if identity.board_id in (XIAO_BOOT_BOARD_ID_BASE,
|
||||
XIAO_BOOT_BOARD_ID_SENSE)
|
||||
else BOOT_STORAGE_INTERNAL_UPDATE)
|
||||
if bootloader_caps_storage(image) != expected_storage:
|
||||
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities 0x{expected_storage:02X}")
|
||||
if identity.board_id in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
expected_storage = (BOOT_STORAGE_QSPI_UPDATE,)
|
||||
elif (identity.board_id, identity.device_name) in SD_BOOTLOADER_IDENTITIES:
|
||||
expected_storage = (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
|
||||
else:
|
||||
expected_storage = (BOOT_STORAGE_INTERNAL_UPDATE,)
|
||||
actual_storage = bootloader_caps_storage(image)
|
||||
if actual_storage not in expected_storage:
|
||||
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
|
||||
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities {expected}")
|
||||
return identity
|
||||
|
||||
|
||||
|
||||
@@ -181,6 +181,7 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
qspi_backed = _cppdef("OTA_QSPI_STORE") is not None
|
||||
qspi_bootloader_update = _cppdef("OTA_QSPI_BOOTLOADER_UPDATE") is not None
|
||||
internal_bootloader_update = _cppdef("OTA_INTERNAL_BOOTLOADER_UPDATE") is not None
|
||||
sd_bootloader_update = _cppdef("OTA_SD_BOOTLOADER_UPDATE") is not None
|
||||
if sd_backed and qspi_backed:
|
||||
raise RuntimeError("nRF52 build cannot enable both SD and QSPI OTA stores")
|
||||
if qspi_backed and _cppdef("QSPIFLASH") is not None:
|
||||
@@ -196,6 +197,11 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
raise RuntimeError("internal bootloader update cannot use SD/QSPI/ExtraFS")
|
||||
if linked_app_end != ml.NRF52_APP_END:
|
||||
raise RuntimeError("shared-slot bootloader update requires the normal 0xED000 linker ceiling")
|
||||
if sd_bootloader_update:
|
||||
if not sd_backed or qspi_backed or internal_extrafs or _cppdef("QSPIFLASH") is not None:
|
||||
raise RuntimeError("SD bootloader update requires the dedicated nRF52 SD OTA store")
|
||||
if linked_app_end != ml.NRF52_APP_END:
|
||||
raise RuntimeError("SD bootloader update requires the normal 0xED000 linker ceiling")
|
||||
stage_ceiling = (ml.NRF52_APP_END if (sd_backed or qspi_backed) else
|
||||
ml.nrf52_stage_ceiling_for_layout(linked_app_end, internal_extrafs))
|
||||
layout_flags = ((ml.NRF52_LAYOUT_FLAG_SD if sd_backed else 0) |
|
||||
@@ -226,7 +232,8 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
|
||||
for i, b in enumerate(suffix):
|
||||
ih[app_end + i] = b # write it right after the app's last byte
|
||||
ih.write_hex_file(path)
|
||||
print(f"EndF: appended to {os.path.basename(path)} at 0x{app_end:X} "
|
||||
endf_address = app_start + len(body)
|
||||
print(f"EndF: appended to {os.path.basename(path)} at 0x{endf_address:X} "
|
||||
f"(app=0x{app_start:X}.. body_len={len(body)} body_hash={h8.hex()} "
|
||||
f"stage=0x{stage_ceiling:X} "
|
||||
f"target={ident.target_id:#010x} hw='{ident.hw_id}' fw={ident.fw_version:#010x})")
|
||||
|
||||
+38
-6
@@ -213,7 +213,7 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE):
|
||||
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
|
||||
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
|
||||
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
|
||||
ml.BOOT_FORMAT_VER, 1 << ml.CODEC_FULL,
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.BOOT_STORAGE_QSPI_UPDATE)
|
||||
name = ml.XIAO_BOOT_DEVICE_NAME
|
||||
struct.pack_into("<8sHHIII16sI", image, 0x100, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
@@ -224,13 +224,14 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE):
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU"):
|
||||
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
|
||||
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE):
|
||||
import zlib
|
||||
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
|
||||
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
|
||||
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
|
||||
ml.BOOT_FORMAT_VER, 1 << ml.CODEC_FULL,
|
||||
ml.BOOT_STORAGE_INTERNAL_UPDATE)
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
storage)
|
||||
name = device_name.encode("ascii").ljust(16, b"\0")
|
||||
struct.pack_into("<8sHHIII16sI", image, 0x100, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
ml.XIAO_BOOT_MANIFEST_VERSION, ml.XIAO_BOOT_MANIFEST_SIZE,
|
||||
@@ -321,7 +322,7 @@ def test_xiao_bootloader_identity_skips_bad_decoy_and_rejects_two_valid_manifest
|
||||
|
||||
def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
|
||||
def caps(*, offset=0, abi=ml.BOOT_FORMAT_VER,
|
||||
codecs=1 << ml.CODEC_FULL,
|
||||
codecs=ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
storage=ml.BOOT_STORAGE_QSPI_UPDATE,
|
||||
reserved=b"\0\0\0"):
|
||||
image = bytearray(b"\xff" * 64)
|
||||
@@ -333,11 +334,12 @@ def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(offset=1))
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(abi=0xFFFF))
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(codecs=0))
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(codecs=1 << ml.CODEC_FULL))
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(storage=0x1C))
|
||||
assert not ml.xiao_bootloader_caps_ok(caps(reserved=b"\0\x01\0"))
|
||||
duplicate = bytearray(caps())
|
||||
struct.pack_into("<8sHHB3s", duplicate, 24, ml.XIAO_BOOT_CAPS_MAGIC,
|
||||
ml.BOOT_FORMAT_VER, 1 << ml.CODEC_FULL,
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.BOOT_STORAGE_INTERNAL_UPDATE, b"\0\0\0")
|
||||
assert ml.bootloader_caps_storage(bytes(duplicate)) is None
|
||||
|
||||
@@ -412,6 +414,36 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract():
|
||||
pass
|
||||
|
||||
|
||||
def test_meshtower_sd_bootloader_profile_builds_the_same_exact_identity():
|
||||
priv = Ed25519PrivateKey.generate()
|
||||
image = _generic_bootloader_image(
|
||||
board_id=0x239A0071, device_name="TOWER_V2_OTA",
|
||||
storage=ml.BOOT_STORAGE_SD_UPDATE)
|
||||
identity = ml.validate_bootloader_image(image)
|
||||
assert (identity.board_id, identity.device_name) in ml.SD_BOOTLOADER_IDENTITIES
|
||||
assert ml.bootloader_caps_storage(image) == 0x09
|
||||
target = ml.bootloader_target_id(identity.board_id, identity.device_name)
|
||||
assert target == 0x1150F50E
|
||||
manifest = ml.build_manifest(
|
||||
target_id=target, fw_version=ml.pack_version("1.0.0"),
|
||||
image_size=len(image), payload=image, block_size=1024,
|
||||
image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True,
|
||||
sign_priv=priv, bootloader=True)
|
||||
parsed = ml.parse_container(ml.build_container(manifest, image))
|
||||
assert parsed.manifest.hw_id == b"NRF_BL_239A0071_TOWER_V2_OTA".ljust(32, b"\0")
|
||||
assert ml.verify(parsed, expect_pub=priv.public_key().public_bytes_raw()) == []
|
||||
|
||||
|
||||
def test_meshtower_sd_bootloader_build_flag_is_exact_target_only():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
text = (root / "variants/heltec_tower_v2/platformio.ini").read_text(encoding="utf-8")
|
||||
section = text.split(
|
||||
"[env:Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors]", 1)[1]
|
||||
section = section.split("[env:", 1)[0]
|
||||
assert "-D OTA_SD_STORE=1" in section
|
||||
assert "-D OTA_SD_BOOTLOADER_UPDATE=1" in section
|
||||
|
||||
|
||||
def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
import re
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ build_flags =
|
||||
; persistent LoRa-OTA store. Requires the matching SD-aware OTAFIX bootloader.
|
||||
; Unlike the internal-flash target above, this accepts both complete images
|
||||
; and in-place deltas and does not reserve application flash for the container.
|
||||
; The exact SD repeater env also accepts manually confirmed signed bootloader
|
||||
; packages when OTAFIX advertises the matching SD self-update profile.
|
||||
[Heltec_tower_v2_sdcard]
|
||||
extends = Heltec_tower_v2
|
||||
build_flags =
|
||||
@@ -74,6 +76,7 @@ build_flags =
|
||||
-D MAX_NEIGHBOURS=254
|
||||
-D ENABLE_OTA=1
|
||||
-D OTA_SD_STORE=1
|
||||
-D OTA_SD_BOOTLOADER_UPDATE=1
|
||||
-D OTA_FOLDER_SERIAL
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user