Add MeshTower SD bootloader updates over LoRa

This commit is contained in:
mikecarper committed 2026-08-21 09:55:15 -07:00
1 parent 9e4d246ac6
commit 3d78d9c846
26 files changed
+763 -114

No files matched your search

+1 -1
View File
@@ -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 |
+2 -2
View File
@@ -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
View File
@@ -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
+64 -4
View File
@@ -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.
+53 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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.