Group documentation and move research into its own section
This commit is contained in:
1 parent
5d5f76688a
commit
9b01a5319b
30 files changed
+1776
-1670
No files matched your search
+1
-1
@@ -560,7 +560,7 @@ meaning can change when the service reorders or adds presets.
|
||||
Repeater profiles use up to 254 neighbors. Selected Generic E22, Heltec V2,
|
||||
Meshadventurer, T-Beam OTA/MQTT, and TLora MQTT profiles use 50 to preserve
|
||||
internal-DRAM reserves. Consult the artifact capability manifest and
|
||||
[ESP32 memory budget](esp32_memory_budget.md) for the exact target.
|
||||
[ESP32 memory budget](research/esp32_memory_budget.md) for the exact target.
|
||||
|
||||
The interactive Option 1 **FULL everything** choice and the standalone FULL
|
||||
command select the unified USB + WiFi image when a matching MQTT target exists;
|
||||
|
||||
@@ -93,7 +93,7 @@ administrator.
|
||||
Repeater profiles use up to 254 neighbor entries. Measured internal-DRAM
|
||||
limits reduce selected Generic E22, Heltec V2, Meshadventurer, T-Beam OTA/MQTT,
|
||||
and TLora MQTT profiles to 50. Check the artifact capability manifest and
|
||||
[ESP32 memory budget](esp32_memory_budget.md) for the exact target.
|
||||
[ESP32 memory budget](research/esp32_memory_budget.md) for the exact target.
|
||||
|
||||
See [local maintenance commands](terminal_chat_cli.md#local-maintenance-commands)
|
||||
for USB, BLE/binary Companion, TCP/Ethernet, and browser CLI privileges.
|
||||
|
||||
@@ -1714,7 +1714,7 @@ OTAFIX bootloader still uses its own address, which is visible only while the
|
||||
board is in BLE DFU/recovery mode. USB and LoRa updates are unaffected.
|
||||
Bluetooth update tools must follow that separate bootloader address after the
|
||||
handoff; they must not assume that a custom application address remains the
|
||||
DFU address. See the [hardware observations](hardware_validation_bluetooth_stealth_2026-09-07.md)
|
||||
DFU address. See the [hardware observations](test-results/hardware_validation_bluetooth_stealth_2026-09-07.md)
|
||||
for the tested XIAO handoff and host-cache caveats.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,190 +1,5 @@
|
||||
# Companion contact caches
|
||||
# Document moved
|
||||
|
||||
Full Companions on ESP32 and nRF52 without PSRAM, plus the high-contact RAK4631
|
||||
USB/BLE profiles, keep 16 outgoing paths
|
||||
and 16 shared secrets in RAM. Every contact remains in the contact table;
|
||||
selecting a different contact loads its saved path as needed. This recovers
|
||||
about 25.4 KiB of internal RAM in the 350-contact ESP32 qualification builds.
|
||||
<script>window.location.replace(new URL("../research/companion_contact_cache/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
See the [qualification results](test-results/companion_contact_cache_results.md) for the
|
||||
six-board RAM comparison, physical tests, and shared-secret timings.
|
||||
|
||||
PSRAM boards retain their existing complete contact table in external RAM.
|
||||
Most legacy USB/BLE/WiFi Companion profiles retain inline paths and secrets.
|
||||
The high-contact RAK4631 USB/BLE profiles also enable the same cache,
|
||||
keeping all 350 contacts while making room for the shared 256-frame offline/DM
|
||||
pool. The build flag `MESH_CONTACT_CACHE=0` or `1` overrides this policy for
|
||||
qualification; it is not a runtime feature switch.
|
||||
|
||||
## Paths and persistence
|
||||
|
||||
The 16 resident paths use least-recently-used replacement. Contact records
|
||||
keep small handles to their saved paths, including 1-, 2-, and 3-byte hop
|
||||
hashes. The existing 152-byte contact record and Companion app frame formats
|
||||
are unchanged. ESP32 uses `/contacts3`; nRF52 also supports its paged contact
|
||||
store and migration from `/contacts3`. An erase is not required.
|
||||
|
||||
A hot path needs no flash read. A cold path is read from its existing contact
|
||||
record. ESP32 reuses one open contact-file reader to avoid repeated SPIFFS
|
||||
metadata scans during synchronization and saving. It closes that reader
|
||||
before replacing the file or recovering a saved transaction.
|
||||
|
||||
Changing a copied contact preserves the old path for a pending app response
|
||||
or rollback. Dirty entries are saved before eviction. If storage cannot be
|
||||
read, written, or represented completely, the operation fails instead of
|
||||
silently losing a contact or sending along a different route. Reboot retries
|
||||
an incomplete load from the preserved files. The path checksum also rejects
|
||||
a cold path changed underneath a live handle; it does not add a new checksum
|
||||
to the legacy ESP32 on-disk format.
|
||||
|
||||
## Shared secrets and power
|
||||
|
||||
Both platforms reuse the 16 most recent secrets from RAM. Cache misses use a
|
||||
platform-specific policy:
|
||||
|
||||
| Platform | Miss behavior |
|
||||
| --- | --- |
|
||||
| ESP32 | Recalculate the secret without reading or writing a secret file. |
|
||||
| nRF52 | Try a saved LittleFS entry, then recalculate if it is unavailable. |
|
||||
|
||||
On the V4, a measured SPIFFS secret lookup took 132,427 microseconds while
|
||||
key exchange took 21,932 microseconds. ESP32 therefore uses recalculation on
|
||||
a RAM-cache miss. It avoids secret-file writes and reduces time awake for
|
||||
this measured workload. These are elapsed times, not measurements of energy
|
||||
in joules; other ESP32 hardware has not been physically benchmarked.
|
||||
|
||||
On a RAK3401, a LittleFS lookup took 7,812 microseconds versus 31,250
|
||||
microseconds for key exchange. The nRF52 backend therefore uses saved flash
|
||||
entries to avoid repeat key exchange when a usable entry exists, including
|
||||
after reboot. Saved entries match the full
|
||||
peer public key and a fingerprint of the local key pair, and have a record
|
||||
checksum. Importing a different private or public key invalidates old results.
|
||||
A corrupt or missing saved entry is recalculated. The native tests use the
|
||||
firmware's actual Ed25519 key-exchange library and verify agreement with the
|
||||
peer's independently calculated secret.
|
||||
|
||||
Derived secrets are expendable. Their storage keeps a reserve for contacts,
|
||||
preferences and filesystem metadata; a full filesystem leaves the calculated
|
||||
secret usable in RAM. nRF52 packs 56 entries into a 3,844-byte page to use its
|
||||
4 KiB flash blocks efficiently. Small filesystems may not have space to persist
|
||||
secrets for all 350 contacts. A miss without a usable saved entry still
|
||||
performs key exchange.
|
||||
|
||||
`MESH_CONTACT_SECRET_FLASH_CACHE=0` or `1` overrides the miss policy for
|
||||
qualification builds. The optional ESP32 flash backend groups eight entries
|
||||
per file. The normal ESP32 policy does not create or use those files.
|
||||
|
||||
## Inspect the cache
|
||||
|
||||
From the Full Companion's ASCII USB terminal or authenticated local CLI:
|
||||
|
||||
```text
|
||||
get contact.cache
|
||||
```
|
||||
|
||||
Cached builds report `paths=16 secrets=16`, `miss=calculate` or `miss=flash`,
|
||||
plus these boot-session counters:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `ram_hits` | Shared secret reused from RAM. |
|
||||
| `flash_hits` | Shared secret loaded from flash without recalculation. |
|
||||
| `calculations` | Shared-secret key exchanges performed. |
|
||||
| `save_skips` | Calculated secrets that could not be persisted, including storage reserve/backoff. |
|
||||
|
||||
Uncached builds report `paths=inline secrets=inline`. The normal `memory`
|
||||
command on supported ESP32 Full Companions shows live heap and queue capacity.
|
||||
|
||||
`get contact.cache.timing` reports the last successful flash lookup and last
|
||||
key calculation in microseconds. Zero means no such operation has completed
|
||||
since boot. These timings exclude secret writes and are useful for comparing
|
||||
the work avoided on a valid flash hit.
|
||||
|
||||
## Offline messages during mOTA
|
||||
|
||||
Contact caches are independent of the offline message queue. Builds using
|
||||
the shared mOTA queue retain **256 frames normally and 128 while mOTA owns
|
||||
its workspace**, then restore 256. No unread frames are discarded to start
|
||||
mOTA: synchronize the queue and retry if it exceeds the retained capacity.
|
||||
PSRAM-backed queues keep their existing capacity and allocation policy.
|
||||
|
||||
Each queue frame occupies 177 bytes. Retaining 32 instead of 128 would make
|
||||
another 16,992 bytes (16.6 KiB) available inside the shared storage. It would
|
||||
not increase free heap by itself: the normal 256-frame storage is statically
|
||||
reserved, and the current mOTA context already fits in the loaned half. More
|
||||
mOTA scratch data would have to share that space to obtain an additional RAM
|
||||
saving. These changes keep the existing 128-frame mOTA limit.
|
||||
|
||||
## Build the six capacity trials
|
||||
|
||||
The optional environments in `platformio.nimble.ini` combine NimBLE, these
|
||||
contact caches, and the shared mOTA queue at **350 contacts, 40 channels and
|
||||
256 normal offline frames**. They remain outside the ordinary release matrix.
|
||||
The ordinary six constrained release recipes still use their established
|
||||
150-contact limits pending wider hardware qualification.
|
||||
|
||||
Enable `platformio.nimble.ini` as described in the
|
||||
[NimBLE trial guide](test-results/nimble_companion_trial.md), then build one environment at
|
||||
a time. For example:
|
||||
|
||||
```sh
|
||||
OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \
|
||||
Heltec_v3_ram_trial_companion_radio_full_nimble \
|
||||
--firmware-version v1.17.1.5-halo-keymind-cascade-cache-trial \
|
||||
--radio-preset usa-cascadia --profile cascade --standard --require-ota
|
||||
```
|
||||
|
||||
| Hardware | Trial environment |
|
||||
| --- | --- |
|
||||
| Heltec V3 | `Heltec_v3_ram_trial_companion_radio_full_nimble` |
|
||||
| Wireless Tracker | `Heltec_Wireless_Tracker_ram_trial_companion_radio_full_nimble` |
|
||||
| Tracker V2, FEM on | `heltec_tracker_v2_ram_trial_companion_radio_full_femon_nimble` |
|
||||
| Heltec CT62 | `Heltec_ct62_ram_trial_companion_radio_full_nimble` |
|
||||
| XIAO ESP32-C3 | `Xiao_C3_ram_trial_companion_radio_full_nimble` |
|
||||
| Generic ESP-NOW | `Generic_ESPNOW_ram_trial_companion_radio_full_nimble` |
|
||||
|
||||
The firmware RAM guard remains enabled. A successful linked budget is distinct
|
||||
from live free heap and physical Bluetooth/LoRa qualification on each board.
|
||||
|
||||
The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW
|
||||
trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and
|
||||
coredump stay at the same addresses as the ordinary `huge_app.csv` Full
|
||||
layout. Install the trial's merged image over USB first to install its
|
||||
partition table. Subsequent WiFi updates must fit the new slots. An
|
||||
application-only update from the ordinary single-slot Full layout cannot
|
||||
change that layout or enable WiFi OTA.
|
||||
|
||||
## Regression checks
|
||||
|
||||
```sh
|
||||
python3 -B test/test_contact_cache.py
|
||||
python3 -B test/test_companion_contact_persistence_contract.py
|
||||
python3 -B test/test_companion_contact_stream_contract.py
|
||||
python3 -B test/test_nrf52_extrafs_contract.py
|
||||
python3 -B test/test_shared_mota_queue.py
|
||||
python3 -B test/test_t096_full_memory.py
|
||||
python3 -B test/test_firmware_ram.py
|
||||
```
|
||||
|
||||
The cache tests compile the production path/secret cache code and extracted
|
||||
production persistence, routing and request functions with filesystem/radio
|
||||
adapters. They exercise 350 routes, eviction, snapshot rollback, page
|
||||
migration, identity changes, read/write/rename faults, handle exhaustion,
|
||||
packet release on a failed path read, and recovery after a simulated reset
|
||||
between file renames. They do not simulate a physical flash power cut.
|
||||
|
||||
`tools/hil/contact_cache_serial_stress.py` checks all 350 paths over USB,
|
||||
replaces them with a different generation, and optionally exercises secret
|
||||
eviction. Use an otherwise empty test node and exclusive access to its
|
||||
Companion data port. `--populate` writes test contacts; `--secrets` transmits
|
||||
23 directed LoRa datagrams to synthetic peers. For an ESP32 cache trial:
|
||||
|
||||
```sh
|
||||
python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
|
||||
--populate --generation 1 --secrets --expect-calculate --reboot
|
||||
python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
|
||||
--generation 1 --secrets --expect-calculate
|
||||
```
|
||||
|
||||
Use the actual device path. Omit `--expect-calculate` for the nRF52 flash
|
||||
policy. The second invocation verifies the persisted paths after reboot.
|
||||
This document is now in [Research](research/companion_contact_cache.md).
|
||||
@@ -43,7 +43,7 @@ app first. While the loan is active, the overflow policy below applies at 128
|
||||
frames. The original `26303793` 1.17.1.5 builds reserve the queue and mOTA state
|
||||
separately. Sharing recovers about 19 KiB on nRF52 Full. The ST7735 color-display
|
||||
Full profiles require at least 72 KiB of heap space at link time; OLED and
|
||||
headless profiles use their own [memory budgets](firmware_memory_budget.md).
|
||||
headless profiles use their own [memory budgets](research/firmware_memory_budget.md).
|
||||
|
||||
Standard, logging, MQTT, and Cascade build overlays retain the selected target
|
||||
capacity. On cache-enabled high-contact nRF52 Companions, including the RAK4631
|
||||
|
||||
@@ -1,69 +1,5 @@
|
||||
# Classic ESP32 image memory budget
|
||||
# Document moved
|
||||
|
||||
Classic ESP32 has 320 KiB of internal DRAM, but at most 160 KiB can hold
|
||||
statically allocated data. The remaining DRAM is available only through the
|
||||
runtime heap. Bluetooth, tracing, and SDK reservations further constrain the
|
||||
usable region. See Espressif's [memory types documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/memory-types.html#dram-data-ram).
|
||||
<script>window.location.replace(new URL("../research/esp32_memory_budget/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
An image can therefore approach its static limit while PlatformIO reports
|
||||
less than 40% of the 320 KiB total in use. PSRAM does not expand the region used
|
||||
by ordinary internal `.data` and `.bss` allocations.
|
||||
|
||||
## Build enforcement
|
||||
|
||||
Every classic ESP32 environment inheriting `esp32_base` runs
|
||||
`scripts/check_esp32_dram.py`. The release builder also adds the script when
|
||||
an ESP32 profile replaces the inherited extra scripts. The check runs before
|
||||
image generation, merging, and uploading, including cached `nobuild` uploads.
|
||||
It requires the ELF and linker map to be present for those cached operations.
|
||||
The check script must load after `merge-bin.py` so the merge target retains its
|
||||
pre-action. A merge also requires the bootloader/partition flash layout;
|
||||
`nobuild` cannot create a bootable merged image when that layout is absent.
|
||||
|
||||
The check reads `dram0_0_seg` from the final linker map. SDK reservations are
|
||||
already reflected in that region; Bluetooth's reservation is not subtracted
|
||||
a second time. The occupied span runs from the region origin to `_heap_start`,
|
||||
including `.data`, `.bss`, `.noinit`, and alignment gaps. The allowed region is
|
||||
the smaller of the linker's region and 160 KiB.
|
||||
|
||||
Builds must leave at least **8 KiB free within that static region**. This is
|
||||
an additional project margin against near-full images, not an Espressif
|
||||
hardware limit or a prediction of free heap. A profile may require a larger
|
||||
margin with `custom_esp32_static_dram_reserve` in bytes; values below 8192 are
|
||||
rejected. Missing or unrecognized map boundaries also fail the check.
|
||||
|
||||
For the Heltec V2 build used to investigate the repeater report:
|
||||
|
||||
| Shared repeater defaults | Static span | Static region | Remaining | Result |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Previous 255 scope slots | 124,020 B | 124,580 B | 560 B | Rejected |
|
||||
| Tuned 31 scope slots | 108,308 B | 124,580 B | 16,272 B | Passed |
|
||||
|
||||
These measurements remove the board's existing scope-capacity override to
|
||||
exercise the shared defaults. The normal Heltec V2 profile already used 31
|
||||
scope slots. Exact sizes vary with build flags and SDK versions.
|
||||
|
||||
To inspect an existing classic ESP32 build:
|
||||
|
||||
```sh
|
||||
python3 scripts/check_esp32_dram.py .pio/build/Heltec_v2_repeater/firmware.map
|
||||
python3 test/test_esp32_dram.py
|
||||
```
|
||||
|
||||
ESP32-S2, S3, and C-series chips have different memory maps and do not inherit
|
||||
this classic ESP32 check.
|
||||
|
||||
## Boot versus running memory
|
||||
|
||||
The build gate checks static placement needed to load and start the image.
|
||||
It never counts heap-only RAM toward that static budget. Allocations made with
|
||||
`new` or `malloc` in global constructors consume heap and are not included in
|
||||
the static span. Moving a table there changes its placement, not total RAM
|
||||
consumption.
|
||||
|
||||
Successful build checks do not prove that every runtime configuration will
|
||||
boot. Hardware validation must still cover startup allocations and the enabled
|
||||
Wi-Fi/Bluetooth services, recording minimum free internal heap and the largest
|
||||
free internal block. Available heap changes during initialization; for example,
|
||||
the Arduino 2.x repeater releases unused Bluetooth controller RAM in
|
||||
`initArduino()`, after global constructors have run.
|
||||
This document is now in [Research](research/esp32_memory_budget.md).
|
||||
@@ -1,104 +1,5 @@
|
||||
# Firmware memory checks
|
||||
# Document moved
|
||||
|
||||
Every firmware environment runs `scripts/check_firmware_ram.py` against its
|
||||
linked ELF before producing or uploading an image. `build.sh`, including
|
||||
option 3, also requires a passing report before collecting release files.
|
||||
Native host tests do not use a microcontroller RAM budget.
|
||||
<script>window.location.replace(new URL("../research/firmware_memory_budget/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
The check reserves room for enabled runtime allocations as well as static
|
||||
data. A firmware image fitting its board's reported RAM total is insufficient:
|
||||
the display, packet pool, USB, Bluetooth workers and WiFi can allocate after
|
||||
startup. The T096 Full 1.17.1.5 report exposed this distinction.
|
||||
|
||||
## What is counted
|
||||
|
||||
| Platform | Source of available runtime RAM |
|
||||
| --- | --- |
|
||||
| nRF52 | Actual `__HeapBase` and `__HeapLimit`; excludes SoftDevice, retained state, ISR stack and the dedicated 64 KiB mOTA arena where present |
|
||||
| ESP32, S3, C3, C6 | Linked ESP-IDF memory-region, capability and reservation tables; only internal, byte-addressable heap counts |
|
||||
| RP2040/RP2350 | `__end__` to `__HeapLimit`, according to the selected linker |
|
||||
| STM32 | `_end` to `_estack`, minus `_Min_Stack_Size` |
|
||||
|
||||
ESP32 PSRAM, instruction-only RAM and RTC RAM never increase the internal
|
||||
budget. On chips other than classic ESP32, the late-reclaimed ROM stack region
|
||||
is excluded because its silicon-specific reservations are only known at boot.
|
||||
Classic ESP32 additionally retains its existing 8 KiB **static DRAM** check.
|
||||
|
||||
The policy adds allowances for task stacks, radio packet pools, screen objects
|
||||
and pixel buffers, filesystem/sensor allocations, enabled wireless stacks,
|
||||
MQTT connections, OTA scratch and transient allocations. A 160x80 ST7735
|
||||
framebuffer needs 25,602 bytes; an OLED allowance is 4 KiB. nRF52 Full with that
|
||||
color framebuffer must have at least 72 KiB available before startup allocations.
|
||||
Headless and OLED devices use their own smaller totals. The JSON lists each
|
||||
component and checks the largest available region against the largest planned
|
||||
single allocation.
|
||||
|
||||
The [small-screen message layout](test-results/v4_pixel5_font_trial.md) retains complete
|
||||
160-byte messages. Its expanded preview records add 2,816 bytes to the
|
||||
startup allowance and increase the contiguous history allocation budget.
|
||||
The V4 can allocate that history in PSRAM; the guard conservatively reserves
|
||||
internal capacity so that PSRAM availability cannot hide a RAM shortage. A
|
||||
target may replace the generic UI allowance only with
|
||||
`MESH_COMPANION_SCREEN_STARTUP_BYTES`; a source-side assertion then covers its
|
||||
actual startup screens plus ESP32 allocator overhead.
|
||||
|
||||
These are engineering allowances for supported configurations, not measured
|
||||
free heap after boot or a guarantee against every future allocation failure.
|
||||
Unknown platforms, unknown display drivers and missing linker metadata fail
|
||||
closed. `MESH_MIN_RUNTIME_HEAP` can raise a profile's requirement; it cannot
|
||||
lower the calculated requirement. Add an allocation allowance when adding a
|
||||
display, transport or other substantial feature.
|
||||
|
||||
Wireless Paper Full keeps 350 contacts and 256 offline frames by lending the
|
||||
upper 128 queue slots to its mOTA workspace during a session. An idle WiFi
|
||||
mOTA listener leaves all 256 slots available. More than 128 unread frames
|
||||
refuses the loan; sync messages with an app first. USB/TCP source detach or
|
||||
disconnect returns all 256 slots and releases the ESP32 proof/leaf scratch
|
||||
buffers. The display and simultaneous USB, Bluetooth and WiFi remain enabled.
|
||||
|
||||
## Release evidence and regression tests
|
||||
|
||||
Full Companions without PSRAM also use
|
||||
[16-entry path and shared-secret caches](companion_contact_cache.md).
|
||||
The optional NimBLE capacity trials retain the same RAM guards at 350
|
||||
contacts and 256 normal offline frames.
|
||||
|
||||
Each newly built firmware has a matching `.memory.json` report. It records
|
||||
the linked ELF SHA-256, available internal RAM, required RAM, largest region,
|
||||
and SHA-256 hashes for the actual firmware files and capability manifest.
|
||||
Packaging and resumed builds reject absent reports, failures, stale ELFs,
|
||||
missing files and changed firmware. Do not reuse a report for another build.
|
||||
|
||||
Run PlatformIO commands sequentially in this checkout:
|
||||
|
||||
```sh
|
||||
python3 -B test/test_firmware_ram.py
|
||||
python3 -B test/test_t096_full_memory.py
|
||||
python3 -B test/test_nrf52_ble_startup.py
|
||||
python3 -B test/test_shared_mota_queue.py
|
||||
python3 -B test/test_cascade_release_package.py
|
||||
pio test -e native -f test_ota
|
||||
```
|
||||
|
||||
Tests cover all resolved firmware environments' hooks, real ELF parsing,
|
||||
allocator table formats, excluded memory, allocation failure, package/report
|
||||
binding and the published T096 failing budget. Shared mOTA tests exercise
|
||||
complete transfers, queue wraparound, unread-message order, source ownership,
|
||||
stop/disconnect and repeated reuse. Bluetooth tests inject task and service
|
||||
startup failures. The manual staging buffer also has allocation-failure and
|
||||
repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source
|
||||
framing through complete transfers, idle polling, queue-full refusal, network
|
||||
loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers.
|
||||
|
||||
For older releases without saved ELFs, an audit can compare their ESP allocator
|
||||
tables against a matching pinned SDK ELF and read reservations from the
|
||||
**published application itself** using `scripts/audit_esp32_image_ram.py`.
|
||||
Unrecognized layouts require another matching reference or a historical rebuild.
|
||||
An audit must identify original-log/linker calculations separately from new
|
||||
ELF checks and verify the published firmware hashes.
|
||||
|
||||
Physical validation remains necessary: boot with and without USB, pair and
|
||||
exchange Bluetooth messages, visit every screen, wake with the button, enable
|
||||
logging/MQTT, transfer mOTA, and monitor heap during a sustained workload.
|
||||
See [memory monitoring](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MEMORY_MONITORING.md)
|
||||
for runtime diagnostics.
|
||||
This document is now in [Research](research/firmware_memory_budget.md).
|
||||
@@ -1,200 +1,5 @@
|
||||
# XIAO Bluetooth stealth hardware validation - 2026-09-07
|
||||
# Report moved
|
||||
|
||||
Historical command syntax: the initial run used stealth as a MAC mode.
|
||||
Current firmware uses the independent `bluetooth.stealth on|off` flag instead
|
||||
and does not migrate that development mode. See [current commands](cli_commands.md)
|
||||
and the independent-flag follow-up below. Each observation applies only to
|
||||
the exact artifact listed in its section.
|
||||
<script>window.location.replace(new URL("../test-results/hardware_validation_bluetooth_stealth_2026-09-07/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
This is a hardware observation report, not an all-board or phone qualification.
|
||||
The test host was the MercerWoodMesh Raspberry Pi Zero 2 W, accessed remotely
|
||||
over Tailscale. Tests used its built-in Bluetooth adapter, not USB resets or
|
||||
hub power switching. Local timestamps below use America/Los_Angeles.
|
||||
|
||||
## Exact target and firmware
|
||||
|
||||
- Board: Seeed XIAO nRF52840; application name `MeshCore-4610BBD2`.
|
||||
- Factory/bootloader address: `E5:C3:A8:B0:60:66`.
|
||||
- Fresh stealth test address: `DC:8D:5E:45:89:6F`.
|
||||
- Bonded test central: Mercer Pi, public address `B8:27:EB:F9:6E:8E`.
|
||||
- Environment: `Xiao_nrf52_companion_radio_ble`, supported build wrapper,
|
||||
target radio preset, default profile, GCC 14.2.1.
|
||||
- Firmware: `dev-stealth3-4b400ea5`, including the uncommitted Bluetooth
|
||||
identity changes on top of `4b400ea5`.
|
||||
- Application ZIP SHA-256:
|
||||
`acdce0af2334349c6e0a5786db5525b00d19befef340411d2e99008ce4d316ae`.
|
||||
- Linked RAM: 183,232 / 237,568 bytes; flash: 616,504 / 708,608 bytes.
|
||||
|
||||
The application was installed with Nordic Legacy BLE DFU. The tool verified
|
||||
the exact ZIP hash, bootloader advertisement and live DIS model
|
||||
`XIAO nRF52840` before transmitting 616,584 application bytes. Target image
|
||||
validation and target-initiated activation disconnect passed. The transfer
|
||||
reported about 2,223 bytes/second (about 277 seconds for payload transmission).
|
||||
Authenticated MeshCore device queries subsequently returned protocol 14,
|
||||
board `Seeed Xiao-nrf52`, and the expected unique test-version prefix.
|
||||
|
||||
## Observed results
|
||||
|
||||
| Check | Result and evidence |
|
||||
| --- | --- |
|
||||
| First-pair discovery | `set bluetooth.mac stealth` saved a new random-static address; reboot returned that exact address with the normal name and Nordic UART advertisement. |
|
||||
| First authenticated pairing | The Pi received a passkey request and completed pairing. BlueZ reported both Paired and Bonded. MITM-protected Nordic UART notifications and real command/reply exchanges worked at MTU 247. |
|
||||
| Pairing transition keeps the session alive | The initial connection returned `stealth; bonded-peer-only advertising`, remained usable for five more seconds, and answered another command without disconnecting or pairing again. |
|
||||
| Advertising after disconnect | A public-address passive scan captured `ADV_DIRECT_IND` from the exact target, with advertising data length zero. The capture contained no undirected advertisement or scan response from that address. |
|
||||
| Bonded reconnect | Subsequent direct connections by saved BlueZ device identity succeeded without a new passkey, including live version, core-statistics and Bluetooth-mode replies. |
|
||||
| Warm reboot persistence | Two commanded reboots retained the address and saved bond. Directed advertising was captured after reboot, followed by authenticated reconnect. Device uptime decreased from 61 seconds before the second reboot to 35 seconds at the next query; core error flags were zero. |
|
||||
| Slow advertising interval | A successful bonded reconnect occurred about 61 seconds after reboot, beyond the 30-second fast-advertising interval. |
|
||||
| Reopen pairing | Setting `stealth` again from an existing bonded connection generated a new address, and the next boot allowed a fresh first pairing. |
|
||||
| Restore default | `set bluetooth.mac default` plus reboot restored `E5:C3:A8:B0:60:66` and normal discovery. Fresh PIN pairing, version/core-statistics queries and repeated `factory address (default)` replies passed. The bench target was left in this discoverable mode. |
|
||||
|
||||
The newly completed pairing reaches Bluefruit's secured callback before its
|
||||
bonded flag is necessarily ready. Recording the completed pairing in the
|
||||
pair-complete callback as well, waiting for the deferred bond write, and
|
||||
deferring identity-list reconfiguration until disconnect are covered by this
|
||||
fresh-pair hardware run. Static source contracts also check those paths.
|
||||
|
||||
The binary reboot command can reset the target before BlueZ receives its ATT
|
||||
write acknowledgement. One initial harness run reported `Unlikely Error`
|
||||
after successful pre-reboot commands. Later checks require an observed
|
||||
disconnect, a live post-reboot query and decreased device uptime; they do not
|
||||
treat the write exception or tool exit code alone as a successful reboot.
|
||||
|
||||
## Discovery and DFU cautions found during testing
|
||||
|
||||
An ordinary BlueZ discovery scan used a temporary random local address and
|
||||
did not show the directed target. A passive scan using the Pi's public
|
||||
identity did show it. Reconnection must use the saved peer/device identity;
|
||||
absence from an ordinary scan is not proof that the node stopped working.
|
||||
Conversely, BlueZ can retain a cached name after pairing, so a displayed name
|
||||
is not proof that the name is still being advertised. The packet-type and
|
||||
zero-length-payload capture is the relevant evidence here.
|
||||
|
||||
The existing DFU handoff helper assumed the application and bootloader shared
|
||||
one address. That assumption is false with a custom application address:
|
||||
the authenticated handoff worked, but the helper timed out looking for DFU
|
||||
at the application address. A fresh exact-address scan found the known
|
||||
factory-address `XIAO_DFU` instead. An old factory-address host bond also
|
||||
caused the first DFU service-discovery attempt to disconnect; clearing only
|
||||
that old lab record allowed DFU. No bootloader code was changed for this test.
|
||||
|
||||
An earlier application-address host pairing record was also removed before
|
||||
its saved stealth state was known. A direct, known-address connection from
|
||||
the same Pi identity allowed fresh PIN pairing and protected commands again.
|
||||
This does not prove that an arbitrary replacement phone can rediscover or
|
||||
recover a stealth node after forgetting its bond.
|
||||
|
||||
## Remaining hardware gates
|
||||
|
||||
- True cold boot with all XIAO power sources, including battery, disconnected.
|
||||
The XIAO reported a battery voltage and did not enumerate as USB on the Pi;
|
||||
a Pi-only reboot or power cycle would not prove this gate.
|
||||
- Unused-boot retention with `random-after-connect`, using a reboot trigger
|
||||
that does not itself authenticate over BLE. The policy has native tests;
|
||||
the remote BLE reboot command cannot prove this hardware gate.
|
||||
- Android and iOS bonded reconnect, including a phone changing its resolvable
|
||||
private address, and direct-connection behavior in the actual MeshCore app.
|
||||
- Rejection of a connection from a second, unbonded physical central.
|
||||
- ESP32 Bluedroid and NimBLE hardware. Those use allowlisted minimal
|
||||
advertising rather than nRF52 directed advertisements.
|
||||
- Node-side missing/corrupt bond and failed settings/bond-write fault injection.
|
||||
|
||||
## Repeatable regression procedure
|
||||
|
||||
1. Record the exact board, application version, ZIP hash, local/remote BLE
|
||||
identities and original Bluetooth setting. Keep a working recovery path.
|
||||
2. Set the desired custom or random MAC, enable `bluetooth.stealth on`, reboot,
|
||||
and capture normal discovery before pairing.
|
||||
3. Pair once. Require a stored bond and successful protected MeshCore commands,
|
||||
then keep the original session open and issue another command.
|
||||
4. Disconnect. Capture the actual advertising type and payload with an
|
||||
appropriate receiving identity; do not use cached scan names as evidence.
|
||||
5. Reconnect by the saved peer without another PIN and exchange commands.
|
||||
6. For custom and saved-random addresses, repeat `bluetooth.stealth on`, reboot
|
||||
twice, and verify unchanged identity/bond, decreased uptime and live
|
||||
commands. Include a reconnect after the fast-advertising period ends.
|
||||
7. Separately remove all target power and repeat the persistence checks.
|
||||
8. Test `bluetooth.stealth off`, then `on`, as a pairing reset, and `off` as
|
||||
the exit from stealth. Verify live discovery, pairing, commands and an
|
||||
unchanged MAC policy. Changing the address to `default` is a separate step.
|
||||
9. With each rotating MAC policy, verify that its rotation trigger still
|
||||
works, stealth stays enabled, and the new address permits fresh pairing.
|
||||
10. Repeat relevant gates for a phone with address privacy and each BLE backend.
|
||||
|
||||
Raw VM-side logs, the exact artifact and bounded test helper are in
|
||||
`/home/mesh/mercer-stealth-validation.jUDW0v/`. Raw Bluetooth captures are
|
||||
local diagnostics and should not be published without privacy review.
|
||||
|
||||
## Mercer crash capture
|
||||
|
||||
During this run the Pi used its existing `dwc_otg.speed=1` full-speed USB
|
||||
configuration. Kernel and health logs were streamed to this VM. No new kernel
|
||||
errors or Pi lockup were observed during these tests; that does not establish
|
||||
the cause of the earlier lockup.
|
||||
|
||||
The live test streams and all temporary scans were stopped at approximately
|
||||
22:56 PDT. Journald remained active, the restore service remained enabled,
|
||||
and the test XIAO was disconnected in default mode. Disk journals occupied
|
||||
about 4.9 MiB at the final check.
|
||||
|
||||
At 22:35 PDT, temporary persistent journaling was enabled with a 16 MiB
|
||||
configured disk-use limit, 2 MiB journal files, one-day retention and a
|
||||
30-second normal sync interval. Recent buffered messages can still be lost
|
||||
in a sudden failure. The original `Storage=volatile` configuration is intact.
|
||||
|
||||
`mercer-crash-capture-restore.service` is enabled for the next Pi boot. It
|
||||
checks the armed boot ID, removes only the unchanged managed override,
|
||||
switches journald back to RAM and leaves the old disk journals for collection.
|
||||
Its same-boot no-op guard and unit syntax passed. The next-boot restoration
|
||||
has not yet been exercised; no Pi crash or reboot was deliberately induced.
|
||||
|
||||
## Independent-flag follow-up - dev-stflag
|
||||
|
||||
The same XIAO and Pi were used later on 2026-09-07 to test the separate
|
||||
`bluetooth.stealth on|off` flag, with no development-mode migration. This run
|
||||
used `Xiao_nrf52_companion_radio_ble`, target radio preset, default profile,
|
||||
and firmware `dev-stflag-4b400ea5` with the uncommitted flag changes.
|
||||
|
||||
- Application ZIP SHA-256:
|
||||
`48fc0ff2c716281771f8b525c62a0015c7605b07e7c513ab854c8b3ff3113f42`.
|
||||
- Linked RAM: 183,232 / 237,568 bytes; flash: 617,192 / 708,608 bytes.
|
||||
- Nordic Legacy BLE DFU verified the exact target, hash and model, transferred
|
||||
617,272 application bytes at a reported 3,522 bytes/second, passed target
|
||||
validation, and observed the target activation disconnect.
|
||||
- Protected live device queries returned `dev-stflag-4b400ea5`, protocol 14,
|
||||
and `Seeed Xiao-nrf52`; every queried core error-flags value was zero.
|
||||
|
||||
| Check | Observed result |
|
||||
| --- | --- |
|
||||
| Default and rejected input | Initial mode was factory address with stealth off. `set bluetooth.mac stealth` and `set bluetooth.stealth maybe` returned errors, leaving both settings unchanged. |
|
||||
| Custom plus stealth | `D2:27:5A:40:06:99` advertised normally before first pairing. Fresh PIN pairing, stored bond, protected commands, and a five-second held session passed with stealth on. |
|
||||
| Repeated on and warm reboot | Repeating `set bluetooth.stealth on` retained the paired state. After reboot, the same custom address accepted protected commands without another PIN. Uptime decreased from 84 to 67 seconds. |
|
||||
| Actual advertising after reboot | A public-address passive capture showed `ADV_DIRECT_IND` for the custom address, data length zero, and no undirected advertisement or scan response from that address. The scan was explicitly disabled afterward. |
|
||||
| Off preserves custom address | `set ble.stealth off` and reboot restored normal discovery at the same custom address. A live connection returned stealth off and the unchanged custom policy. |
|
||||
| Saved random plus stealth | `F2:2E:E5:65:18:5E` passed first pairing and held commands. After reboot, it retained its address, saved-random policy and stealth bond; protected reconnect needed no PIN. Uptime decreased from 55 to 30 seconds. |
|
||||
| Random-after-connect plus stealth | Selecting this policy kept stealth on and reopened pairing at `C7:FD:29:44:4F:3E`. Pairing produced both bonded-peer-only status and an armed next-boot rotation. Reboot changed the address to `F5:F3:89:B7:B5:26`, where normal discovery, fresh pairing and protected commands passed with stealth still on. |
|
||||
| Random-every-boot plus stealth | The first boot used `F9:B2:03:DD:CC:43`, completed fresh pairing, and answered commands with stealth on. The next commanded reboot advertised `F8:C7:C0:96:1E:00`; fresh PIN pairing and protected commands passed again with the rotating policy and stealth on. |
|
||||
| Restore the bench target | Setting the MAC to `default` kept stealth on until the separate `set bluetooth.stealth off`. After reboot, factory-address discovery, fresh PIN pairing and protected commands confirmed default MAC and stealth off. The obsolete factory-address host bond was replaced during this final pairing. |
|
||||
|
||||
These checks used actual encrypted BLE connections at MTU 247, not scan
|
||||
sightings alone. Two first-pair harness attempts stopped before connecting
|
||||
because their unpaired BlueZ discovery object no longer existed. A fresh
|
||||
bounded scan immediately followed by pairing was used for retries; this was
|
||||
not a firmware GATT command failure. Reboot-write/disconnect races were
|
||||
accepted only with an observed disconnect and subsequent live device checks.
|
||||
|
||||
Local automated verification passed 39 native cases, 32 source/persistence/UI
|
||||
contracts, and 11 real-browser cases. Supported-wrapper builds passed for
|
||||
XIAO nRF52, XIAO C3, M5Stack Unit C6L and XIAO S3 WIO Full Companion, covering
|
||||
nRF52, ESP32 NimBLE and ESP32 Bluedroid integration. The contracts and browser
|
||||
suite are included in the unit-test workflow; a browser must be available for
|
||||
the optional browser suite to execute instead of skip.
|
||||
|
||||
Exact artifacts, build logs, command logs and the private packet capture are
|
||||
in `/home/mesh/bluetooth-stealth-flag.ApLB8o/` on the VM. The remaining hardware
|
||||
gates above still apply; these results do not qualify every board or phone.
|
||||
|
||||
At 23:42 PDT the XIAO was disconnected in normal discoverable mode. The Pi
|
||||
retained the same boot ID throughout, remained responsive, and had no new
|
||||
kernel messages in the streamed log. Journald was active, the next-boot
|
||||
volatile-log restoration service was still enabled, and disk journals used
|
||||
about 5.9 MiB. No USB reset, Pi reboot, or target battery removal was used.
|
||||
This report is now in [Test Results](test-results/hardware_validation_bluetooth_stealth_2026-09-07.md).
|
||||
+21
-35
@@ -1,40 +1,26 @@
|
||||
# Introduction
|
||||
# MeshCore documentation
|
||||
|
||||
Welcome to the MeshCore documentation.
|
||||
Choose firmware with the [firmware picker](firmware_picker.md), then read the [current 1.17.1.8 release notes](releases/1.17.1.8.md) before installing. For help with a device already in service, start with the [FAQ](faq.md).
|
||||
|
||||
Below are a few quick start guides.
|
||||
## Documentation
|
||||
|
||||
- [1.17.1.8 USA Cascade release](./releases/1.17.1.8.md)
|
||||
- [Test Results](./test-results/index.md)
|
||||
- [Old Releases](./old-releases/index.md)
|
||||
- [Feature switches by role: Companion, Repeater, Room Server, Sensor](./role_feature_switches.md)
|
||||
- [USB web console](https://flasher.meshcore.io/console)
|
||||
- [Full Companion feature switches](./full_companion_features.md)
|
||||
- [Frequently Asked Questions](./faq.md)
|
||||
- [Firmware Picker](./firmware_picker.md)
|
||||
- [Companion Notification Builder](./notifications.md)
|
||||
- [Temporary Radio Test Command Generator](./preset_test.md)
|
||||
- [CLI Commands](./cli_commands.md)
|
||||
- [LoRa CLI Host Service](./host_cli_service.md)
|
||||
- [Filter Policy Playground](./filter_tool.md)
|
||||
- [Telemetry Decoder](./telemetry_decoder.md)
|
||||
- [Management Report Decoder](./management_decoder.md)
|
||||
- [CLI Availability by Firmware Build](./cli_build_matrix.md)
|
||||
- [Classic ESP32 image memory budget](./esp32_memory_budget.md)
|
||||
- [Full Companion contact caches and capacity trials](./companion_contact_cache.md)
|
||||
- [Easy LoRa OTA: ESP32 and nRF52 firmware updates](./ota_easy.md)
|
||||
- [Scripted LoRa OTA: Bash and PowerShell](./lora_ota_automation.md)
|
||||
- [nRF52 repeater OTA with external QSPI](./ota_nrf52_qspi.md)
|
||||
- [nRF52 bootloader updates over LoRa](./ota_nrf52_bootloader_update.md)
|
||||
- [MeshTower V2 microSD self-updates](./ota_meshtower_v2_sdcard.md)
|
||||
- [GPS Tracking](./gps_tracking.md)
|
||||
- [Companion Protocol](./companion_protocol.md)
|
||||
- [Companion Offline Message Queue](./companion_offline_queue.md)
|
||||
- [Full Companion: ESP32 and nRF52](./companion_radio_full.md)
|
||||
- [Full Companion USB CLI and binary switcher](./full_companion_usb_switcher.md)
|
||||
- [Packet Format](./packet_format.md)
|
||||
- [QR Codes](./qr_codes.md)
|
||||
- [Feature switches by role](role_feature_switches.md) and [Full Companion features](full_companion_features.md)
|
||||
- [Firmware update guide](ota_user_guide.md), [easy LoRa OTA](ota_easy.md), and [scripted LoRa OTA](lora_ota_automation.md)
|
||||
- [CLI commands](cli_commands.md), [availability by build](cli_build_matrix.md), and [radio profiles](radio_profiles.md)
|
||||
- [Companion protocol](companion_protocol.md), [packet format](packet_format.md), and [QR codes](qr_codes.md)
|
||||
|
||||
If you find a mistake in any of our documentation, or find something is missing, please feel free to open a pull request for us to review.
|
||||
The navigation groups the remaining guides under Documentation, including board-specific updates, networking, power, and protocol references.
|
||||
|
||||
- [Documentation Source](https://github.com/mikecarper/MeshCore/tree/keymindCascade/docs)
|
||||
## Tools
|
||||
|
||||
- [Firmware picker](firmware_picker.md) and [USB web console](https://flasher.meshcore.io/console)
|
||||
- [Notification builder](notifications.md) and [temporary radio test builder](preset_test.md)
|
||||
- [Filter policy playground](filter_tool.md), [telemetry decoder](telemetry_decoder.md), and [management report decoder](management_decoder.md)
|
||||
|
||||
## Engineering records
|
||||
|
||||
- [Research](research/index.md) — design notes, diagnostics, and historical update-chain studies
|
||||
- [Test Results](test-results/index.md) — measured validation reports and Preview 1 qualification evidence
|
||||
- [Old Releases](old-releases/index.md) — historical release notes and supporting files
|
||||
|
||||
Found a mistake or missing information? [Edit the documentation](https://github.com/mikecarper/MeshCore/tree/keymindCascade/docs).
|
||||
@@ -9,7 +9,7 @@ describe compatibility/migration history, not the current recommended download.
|
||||
Full Companion is a MOTA source and normally updates itself over USB.
|
||||
|
||||
|
||||
The dedicated [RAK3401 chain report](rak3401_mota_chain.md) records the
|
||||
The dedicated [RAK3401 chain report](research/rak3401_mota_chain.md) records the
|
||||
physical failures of withdrawn migrations and the physically qualified compact
|
||||
nine-step replacement. Its runner blocks the withdrawn chains and pins every
|
||||
accepted bridge image by SHA-256.
|
||||
|
||||
@@ -1,96 +1,5 @@
|
||||
# nRF52 Companion Bluetooth diagnostic firmware
|
||||
# Document moved
|
||||
|
||||
The T1000-E diagnostic image is Full Companion firmware with the normal radio,
|
||||
sensor, pairing and recovery settings. Diagnostics are enabled with
|
||||
MESH_NRF52_BLE_TRACE=1. Ordinary builds compile them out. Diagnostic builds
|
||||
reserve about 6.3 KB of RAM for a 256-event ring and counters.
|
||||
<script>window.location.replace(new URL("../research/nrf52_bluetooth_debug/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
The trace records connection timing, security results, UART notification
|
||||
subscriptions, app receive/transmit lengths, queue overflow, local disconnect
|
||||
requests and each recovery trigger. It brackets Bluefruit's real event
|
||||
dispatcher so a handler that stalls can be distinguished from an event that
|
||||
never reached the framework. Maximum dispatch duration and Companion loop
|
||||
gaps are also reported. Callbacks write bounded RAM records without serial
|
||||
printing or allocation. PINs, keys, peer addresses and message contents are
|
||||
not stored.
|
||||
|
||||
## Capture a phone failure
|
||||
|
||||
1. Install the diagnostic UF2 for your exact board, or use its application DFU
|
||||
ZIP. This does not require replacing the bootloader.
|
||||
2. Connect with the phone normally and reproduce the failure. Keep the node
|
||||
powered. Do not forget the Bluetooth device, reboot, or factory reset
|
||||
before collecting the trace.
|
||||
3. Connect the node's USB cable to a computer and close other serial clients.
|
||||
Install Python and pyserial if needed.
|
||||
4. Export the diagnostic report:
|
||||
|
||||
~~~sh
|
||||
python3 scripts/capture_nrf52_ble_debug.py --port /dev/ttyACM0 --output bluetooth-report.json
|
||||
~~~
|
||||
|
||||
On Windows use the correct COM port instead of /dev/ttyACM0. The exporter
|
||||
enters the Full Companion USB terminal at 115200 baud, reads diagnostics,
|
||||
then returns it to binary mode. It does not reset or change settings.
|
||||
5. Send the JSON report with the approximate failure time and what the phone
|
||||
displayed. Export soon after failure because old events roll out of the
|
||||
ring. Reboot or loss of power clears the RAM history.
|
||||
|
||||
If the Pi's attached T1000-E is used, its serial identity is
|
||||
34A9141999729D5D. Identify the physical board before flashing; an upstream
|
||||
application may have the misleading USB product name T1000-E-BOOT.
|
||||
|
||||
## Manual USB commands
|
||||
|
||||
- get bluetooth.trace: format, latest sequence, capacity and dropped writes.
|
||||
- get bluetooth.trace.state: advertising, connection, security, UART
|
||||
notification state, MTU, app traffic and queues.
|
||||
- get bluetooth.trace.stats: cumulative connects, disconnects, secured
|
||||
sessions, RX/TX operations and last disconnect reason/time.
|
||||
- get bluetooth.trace.timing: uptime, maximum loop gap, maximum stack
|
||||
dispatch time and any currently executing event.
|
||||
- get bluetooth.trace N: one record by sequence.
|
||||
|
||||
The debug commands are restricted to local Companion transport requests and
|
||||
are not exposed through remote mesh management. Replies and raw records are
|
||||
decoded by the exporter. Sequence gaps and overwritten records are reported.
|
||||
Diagnostic contention is counted and dropped rather than blocking BLE.
|
||||
|
||||
## Reading the result
|
||||
|
||||
| Evidence | Stage identified |
|
||||
| --- | --- |
|
||||
| No stack connection event; advertising remains active | Connection has not reached the application's Bluetooth event layer. |
|
||||
| Connection but security error or timeout | Pairing or encryption setup. |
|
||||
| Secured link but UART notifications disabled | Notification subscription or restoration. |
|
||||
| Secured link with no app RX; secured_no_app_watchdog | App never opened its Companion UART session; the 15-second recovery fired. |
|
||||
| App RX followed by blocked/partial TX | Reply delivery; the report identifies the queue/watchdog result. |
|
||||
| Dispatch entry with no matching exit and increasing age | Software stack handler may be stuck; correlate with a later snapshot. |
|
||||
|
||||
Reason 0x08 is connection timeout, 0x16 is local termination and 0x3e
|
||||
is connection establishment failure. Do not confuse decimal 22 (0x16)
|
||||
with hexadecimal 0x22 (link-layer response timeout).
|
||||
|
||||
The report can identify the failing software stage, but cannot guarantee the
|
||||
physical cause. No connection event alone cannot separate a central
|
||||
controller problem, RF interference and a peripheral controller problem.
|
||||
Dispatch timing starts after the SoftDevice delivers an event; it does not
|
||||
measure radio interrupt latency. A bench failure is not proof of the same
|
||||
failure on an iPhone. Use the diagnostic image on the affected node.
|
||||
|
||||
## Build
|
||||
|
||||
Run one PlatformIO command at a time in this checkout:
|
||||
|
||||
~~~sh
|
||||
PLATFORMIO_BUILD_FLAGS='-DMESH_NRF52_BLE_TRACE=1 -DBLE_DEBUG_LOGGING=0 -g3' \
|
||||
OUTPUT_DIR=out/t1000e-ble-debug \
|
||||
bash build.sh build-firmware t1000e_companion_radio_full \
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev-ble-debug --full
|
||||
~~~
|
||||
|
||||
The Bluefruit dispatch hooks are applied to private build copies of framework
|
||||
sources. The shared PlatformIO framework cache is not modified. Retain the
|
||||
ELF alongside the UF2/ZIP for future SWD inspection if hardware access becomes
|
||||
available. Do not halt the CPU during a Bluetooth connection: that disrupts
|
||||
SoftDevice radio timing and invalidates the test.
|
||||
This document is now in [Research](research/nrf52_bluetooth_debug.md).
|
||||
@@ -77,7 +77,7 @@ it does not certify all historical statements as current hardware behavior.
|
||||
| [docs/companion_protocol.md](../companion_protocol.md) | Scanned; retained |
|
||||
| [docs/companion_radio_full.md](../companion_radio_full.md) | Corrected or clarified |
|
||||
| [docs/docs.md](../docs.md) | Scanned; retained |
|
||||
| [docs/esp32_memory_budget.md](../esp32_memory_budget.md) | Scanned; retained |
|
||||
| [docs/esp32_memory_budget.md](../research/esp32_memory_budget.md) | Scanned; retained |
|
||||
| [docs/faq.md](../faq.md) | Corrected or clarified |
|
||||
| [docs/filter_tool.md](../filter_tool.md) | Scanned; retained |
|
||||
| [docs/firmware_picker.md](../firmware_picker.md) | Corrected or clarified |
|
||||
@@ -104,7 +104,7 @@ it does not certify all historical statements as current hardware behavior.
|
||||
| [docs/packet_format.md](../packet_format.md) | Scanned; retained |
|
||||
| [docs/payloads.md](../payloads.md) | Scanned; retained |
|
||||
| [docs/qr_codes.md](../qr_codes.md) | Scanned; retained |
|
||||
| [docs/rak3401_mota_chain.md](../rak3401_mota_chain.md) | Historical record retained |
|
||||
| [docs/rak3401_mota_chain.md](../research/rak3401_mota_chain.md) | Historical record retained |
|
||||
| [docs/releases/1.17.1.5.md](1.17.1.5.md) | Corrected or clarified |
|
||||
| [docs/spiffs_regular_file_reads.md](../test-results/spiffs_regular_file_reads.md) | Scanned; retained |
|
||||
| [docs/stats_binary_frames.md](../stats_binary_frames.md) | Scanned; retained |
|
||||
|
||||
@@ -22,7 +22,7 @@ SF7, CR5. Saved node settings survive ordinary firmware updates.
|
||||
|
||||
All 583 published profiles (the 540-profile matrix plus 43 supplemental Full
|
||||
repeaters) were checked against the new
|
||||
[runtime RAM policy](../firmware_memory_budget.md). **All 11 failing profiles
|
||||
[runtime RAM policy](../research/firmware_memory_budget.md). **All 11 failing profiles
|
||||
have replacements**, and the same queue sharing has been applied to the other
|
||||
41 nRF52 Full profiles: **52 corrected profiles in total**, initially built from
|
||||
[`aa20e927`](https://github.com/mikecarper/MeshCore/tree/aa20e9278ebdd574c4769fdb58d46fd9d4face68).
|
||||
|
||||
@@ -97,7 +97,7 @@ each inbox mode. The sleep harness checks both MQTT and dual-profile scanning.
|
||||
|
||||
The release packager checks each published profile's capability manifest,
|
||||
firmware-file hashes, native file pairs and
|
||||
[runtime RAM budget](../firmware_memory_budget.md). Infrastructure must have a
|
||||
[runtime RAM budget](../research/firmware_memory_budget.md). Infrastructure must have a
|
||||
verified wireless update path. The target manifests and exclusion report describe
|
||||
the actual published matrix; a build check is not a physical-board test.
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ built with `-DTELEM_INA3221_ADDRESS=0x43` when both are installed.
|
||||
|
||||
ESP32 siblings retain the compact browser WiFi updater and use up to
|
||||
254 neighbors. Internal-DRAM-constrained targets use 50 as recorded in the
|
||||
artifact capability manifest; see [ESP32 memory budget](esp32_memory_budget.md). RP2040 and STM32 targets are not offered because
|
||||
artifact capability manifest; see [ESP32 memory budget](research/esp32_memory_budget.md). RP2040 and STM32 targets are not offered because
|
||||
those platforms do not yet have a safe bootloader/apply path.
|
||||
|
||||
nRF52 LoRa-OTA siblings use size optimization rather than the Adafruit platform's default `-Ofast`. This
|
||||
|
||||
@@ -1,272 +1,5 @@
|
||||
# RAK3401: 1.16.07 to 1.17.1.7 without a bootloader update
|
||||
# Document moved
|
||||
|
||||
Qualified on MercerWoodMesh on 27 September 2026: all 15 application updates
|
||||
passed over LoRa, from the exact supplied 1.16.07 image, without a bootloader
|
||||
update. Use `RAK3401-1.16.07-to-1.17.1.7-legacy24-mOTA.zip` and verify it against
|
||||
the accompanying `.zip.sha256` file before extracting. Follow the exact
|
||||
before/after hashes in its `CHAIN.csv`; this is not a single-step stock update.
|
||||
<script>window.location.replace(new URL("../research/rak3401_1.17.1.7_legacy24/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
## Exact compatibility boundary
|
||||
|
||||
This is an application-only migration for target `2FA509C1`, hardware
|
||||
`RAK_3401_repeater_lora_ota_no_external_sensors`. It is not a generic RAK4631
|
||||
package. The installed application must have body hash `71F4026CBE4B8B74`
|
||||
(`1.16.07`, full image SHA-256
|
||||
`4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb`).
|
||||
|
||||
The bootloader remains `0.9.2-OTAFIX2.4`, with ABI 2 and codec mask `0x4`.
|
||||
Its application staging ceiling is `0xD4000`. Every package is a codec-2
|
||||
CRLE in-place delta. DEFLATE is used only for transport by compatible
|
||||
intermediate applications; the old bootloader still applies CRLE.
|
||||
|
||||
The destination is built from source commit
|
||||
`2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1` with the explicit
|
||||
`MESHCORE_NRF52_LEGACY_FLASH_OTA=1` build selection. This preserves the
|
||||
ordinary S140 linker and disables hybrid flash/RAM staging and bootloader
|
||||
self-update. It is deliberately different from the stock hybrid 1.17.1.7
|
||||
release. The endpoint has:
|
||||
|
||||
- Version: `v1.17.1.7-halo-keymind-cascade-legacy24-2d03e098`
|
||||
- Image: 521,496 bytes
|
||||
- Body hash: `D437D1F17AF5B9F3`
|
||||
- SHA-256: `c9e63a94f58f881b1853fc5d9d348cd75a6cdd454c5b572341d652a6141e3157`
|
||||
|
||||
Do not replace this endpoint with the stock ZIP. Do not send a bootloader or
|
||||
SoftDevice update. The ZIP/UF2 files used to construct and qualify the chain
|
||||
are not themselves LoRa update containers.
|
||||
|
||||
Future application updates for this node must also be qualified against the
|
||||
unchanged OTAFIX2.4 contract. Keep the legacy build selection; do not assume
|
||||
that a later stock release is an interchangeable target for this chain.
|
||||
|
||||
## What "smallest" means
|
||||
|
||||
The search compares an explicit, hash-pinned set of application images and
|
||||
every allowed page-aligned in-place workspace for each increasing-version
|
||||
transition. The starting bootstrap is fixed: its 89,844-byte package is the
|
||||
only admitted first edge. Synthetic bridge version numbers are real compiled
|
||||
versions below 1.17.1.7; their binaries are not relabeled after compilation.
|
||||
|
||||
The `bytes` objective minimizes ideal serialized MeshCore traffic first,
|
||||
then install count. The `packets` objective minimizes ideal originated
|
||||
packets first, then serialized traffic. The older `transport` objective
|
||||
minimizes install count before traffic and is not equivalent to either.
|
||||
|
||||
Accounting includes manifests, block requests, proof requests/responses,
|
||||
DATA fragments, and MeshCore framing. It uses the actual pinned encoder for
|
||||
compressed blocks. It excludes discovery, retries, admin/control commands,
|
||||
other mesh traffic, flood fan-out, and LoRa preamble/coding overhead. A fixed linear relay path
|
||||
can be accounted for separately. These are finite-search minima, not a proof
|
||||
that no possible custom firmware or protocol could ever use fewer bytes.
|
||||
The ideal compressed counts also assume negotiation remains in that mode.
|
||||
An older receiver can fall back to raw DATA after stalls; the physical-test
|
||||
transmission totals include that extra traffic.
|
||||
|
||||
Best lossless byte count is not a guarantee of the fastest real radio link.
|
||||
Use the fastest preset that remains reliable over the actual route. SF5 /
|
||||
500 kHz is a direct bench measurement, not a recommendation to switch an
|
||||
unreachable field node or unconfigured relay chain to that preset.
|
||||
|
||||
## Selected route
|
||||
|
||||
The completed search covers 52 images and 47,920 flash geometries. The
|
||||
packet-first route has 15 packages, totaling 2,340,160 container bytes. Its
|
||||
ideal direct-link traffic is **12,621 packets / 1,808,961 serialized MeshCore
|
||||
bytes**. These totals include traffic in both directions, not just DATA.
|
||||
Fifteen is also the fewest installs among the searched candidates.
|
||||
|
||||
The byte-first route uses the same sequence of application images, with
|
||||
four different patch workspaces: 12,625 packets / 1,808,787 bytes. Saving
|
||||
174 bytes costs four packets. The delivered packages use the packet-first
|
||||
choice; do not substitute packages from the comparison route.
|
||||
|
||||
| Step | Resulting version | Container bytes | Ideal packets | Ideal MeshCore bytes |
|
||||
| ---: | --- | ---: | ---: | ---: |
|
||||
| 1 | 1.16.7.9 | 89,844 | 878 | 101,203 |
|
||||
| 2 | 1.16.7.10 | 62,838 | 551 | 70,031 |
|
||||
| 3 | 1.16.8.7 | 106,125 | 496 | 70,134 |
|
||||
| 4 | 1.16.9.105 | 149,542 | 728 | 104,992 |
|
||||
| 5 | 1.16.9.110 | 112,540 | 489 | 67,887 |
|
||||
| 6 | 1.16.9.111 | 71,070 | 268 | 35,453 |
|
||||
| 7 | 1.16.9.116 | 138,545 | 692 | 101,308 |
|
||||
| 8 | 1.16.9.122 | 177,536 | 995 | 148,671 |
|
||||
| 9 | 1.16.10.1 | 258,398 | 1,477 | 223,042 |
|
||||
| 10 | 1.16.11.0 | 245,426 | 1,450 | 219,953 |
|
||||
| 11 | 1.16.12.1 | 217,608 | 1,167 | 172,794 |
|
||||
| 12 | 1.16.13.1 | 199,395 | 1,024 | 150,005 |
|
||||
| 13 | 1.16.14.0 | 199,527 | 989 | 143,160 |
|
||||
| 14 | 1.16.15.0 | 139,188 | 618 | 86,721 |
|
||||
| 15 | 1.17.1.7 | 172,578 | 799 | 113,607 |
|
||||
|
||||
Some bridge versions also exist as different binaries. `CHAIN.csv`'s exact
|
||||
body hashes and package checksums, not this version-only table, determine
|
||||
which package is valid at each step. The first two transfers use legacy
|
||||
1 KiB blocks; subsequent transfers use negotiated 2 KiB blocks with
|
||||
transport DEFLATE and the unchanged CRLE bootloader format.
|
||||
The second transfer also omits explicit proof requests: that historical
|
||||
receiver waits for the current sender's proactive Merkle proofs. The original
|
||||
1.16.07 receiver does not have this optimization.
|
||||
|
||||
## Measured MercerWoodMesh replay
|
||||
|
||||
All 15 selected packages passed in order on the Pi's debug-wired RAK3401,
|
||||
starting with the exact 1.16.07 image and OTAFIX2.4. Every application payload
|
||||
crossed LoRa from the Heltec T096. There were no debug writes, application
|
||||
substitutions, failed installs, or resumed downloads between the 15 steps.
|
||||
|
||||
The direct bench used 909.950 MHz, BW 500 kHz, SF5, CR5, continuous receive,
|
||||
and a 128-symbol source preamble. Observations for both radios totaled:
|
||||
|
||||
- 14,069 transmitted frames, including discovery and retries.
|
||||
- 22 minutes 31 seconds of measured download time.
|
||||
- 47 minutes 35 seconds summed active test-step time, including configuration,
|
||||
checks, installs, and reboots; deliberate gaps between runs are excluded.
|
||||
|
||||
These are bench measurements, not a field-link completion-time promise. The
|
||||
control commands were sent over USB; a remote field console adds LoRa control
|
||||
traffic. `BENCHMARK.csv` separates each step's ideal cost from its observed
|
||||
frame count and timing. Actual serialized radio bytes were not measured.
|
||||
|
||||
Independent debug-port readback matched the complete final application,
|
||||
unchanged bootloader, and unchanged MBR/SoftDevice. UICR matched the normal
|
||||
post-startup baseline throughout this replay. The initial pre-first-boot
|
||||
UICR dump differs because normal Nordic/RAK startup configures the reset pins
|
||||
and 3.3 V regulator; this is documented in the evidence, not a bootloader
|
||||
upgrade.
|
||||
|
||||
After a fresh reboot, the watchdog reported `on`. Both radios returned to
|
||||
910.525 MHz / BW 62.5 kHz / SF7 / CR5, and a nonce-matched normal-channel trace
|
||||
through the exact target key prefix completed in 423 ms. The inaccessible
|
||||
field node has not been touched. The lab RAK remains on this repeater image;
|
||||
the Pi's `mctomqtt.service` remains stopped because it previously owned that
|
||||
serial port. Radios other than the RAK3401 and sending Heltec T096 were left
|
||||
untouched.
|
||||
|
||||
## Field preparation
|
||||
|
||||
All destination commands below can be sent through the authenticated remote
|
||||
repeater console; USB/Bluetooth at the destination is not required. Keep a
|
||||
separate control connection available while the sender owns its companion's
|
||||
serial port. A local
|
||||
Full Companion radio attached to the sender runs the bundled `motatool`
|
||||
seeder. Resolve ports by device identity, not a remembered `ttyACM` number.
|
||||
|
||||
The qualified source is the Pi's Heltec T096, USB serial
|
||||
`651F8E496197F882`, running Full Companion
|
||||
`v1.17.1.8-halo-keymind-cascade-dev-d4b3bb56`. It supports the extended
|
||||
2 KiB transport and host-backed DEFLATE. The destination is RAK3401 USB
|
||||
serial `0B81C9C68D8D01B4`. Older companion firmware or an older `motatool`
|
||||
is not an interchangeable sender for this bundle.
|
||||
|
||||
Before any change, record `ver`, `ota self`, `ota status`,
|
||||
`get bootloader.ver`, `get system.watchdog`, `get radio.rxps`,
|
||||
`get powersaving`, `get rxdelay`, `get af`, and `ota config`. Match the exact
|
||||
starting body, hardware, bootloader, and signing-key policy. An unsigned
|
||||
bundle is not suitable for a node requiring signed packages.
|
||||
Choose a stable-power maintenance window long enough for all 15 installs;
|
||||
do not begin an install while the destination is at risk of losing power.
|
||||
|
||||
Disable the destination system watchdog with `set system.watchdog off`.
|
||||
Allow its one-time hardware reset and wait at least 90 seconds; reconnect
|
||||
and prove that it reports `off`. A quick software reboot is not a substitute.
|
||||
|
||||
Use continuous receive on the destination and sender throughout the bridge
|
||||
chain (`set radio.rxps off`). Record and disable a second scanning radio
|
||||
profile on the sender if it would take the radio away from the OTA channel.
|
||||
Record `rxdelay`, `af`, power-saving, reach, and relay settings before changing
|
||||
them. Use `hops 0` only when there are no intermediate relays. Obey applicable
|
||||
airtime/duty restrictions and restore all saved settings when finished.
|
||||
|
||||
The direct bench also uses `set rxdelay 0` and `set af 0` on both radios,
|
||||
`ota speed 1` on the sender, and `set powersaving off` where supported.
|
||||
The earliest applications do not recognize the last command; continuous
|
||||
receive is still selected through `set radio.rxps off`. The source has
|
||||
`set radio2 off` throughout the transfer. Wait for configuration replies to
|
||||
finish before handing its serial connection to the seeder.
|
||||
|
||||
For the qualified direct bench configuration only, the destination uses
|
||||
`tempradio 909.950,500,5,5,120`; the tested current Full Companion accepts
|
||||
`tempradio 909.950,500,5,5,120,128` for an explicit 128-symbol preamble.
|
||||
Historical destination parsers do not accept the sixth argument. The sender
|
||||
and destination must be on the same live radio preset. Do not assume the
|
||||
temporary preset survives a bridge reboot.
|
||||
|
||||
The newer historical receivers performed poorly with a 16-symbol source
|
||||
preamble in this test. Use the qualified longer setting. Also avoid frequent
|
||||
`ota status` or `ota stats` polling: these versions rehash the complete running
|
||||
application while answering, which can interrupt receive processing. The lab
|
||||
runner observes the sender's local log and checks the receiver near completion
|
||||
or at a 60-second interval. Set a field maintenance lease appropriate to the
|
||||
actual preset and route; the 120-minute value above is a bench setting.
|
||||
|
||||
## Applying a qualified bundle
|
||||
|
||||
Verify the outer archive hash and every entry in `SHA256SUMS.txt`. Verify
|
||||
each `.mota` using the included, hash-pinned `motatool`. `CHAIN.csv` is the
|
||||
authoritative ordered list of packages, manifest IDs, and before/after hashes.
|
||||
On the Pi, use `tools/motatool-linux-aarch64` (make it executable after
|
||||
extracting if necessary). The x86-64 binary is for Linux/WSL, not native
|
||||
Windows. The provenance images, tools, and evidence are not sent over LoRa;
|
||||
only the selected `.mota` in the current step's serving directory is sent.
|
||||
|
||||
For every row, including after an interrupted run:
|
||||
|
||||
1. Prove that `ota self` matches that row's exact before hash and that the
|
||||
bootloader reports `apply OK`. Do not select a row by version alone.
|
||||
2. Reconcile `ota status`. Resume only that row's known partial MID. If it is
|
||||
already ready, do not redownload it. Stop on an unrelated or ambiguous
|
||||
session. Never send a blind `ota cancel`.
|
||||
3. Reapply the agreed radio/receive guardrails after reboot. Start the sender
|
||||
with only this row's package in its serving directory:
|
||||
`motatool serve --dir STEP_DIRECTORY --serial COMPANION_PORT --baud 115200 -v`.
|
||||
4. Run `ota ls` until the expected version is advertised. The old listing
|
||||
shows a version, not a MID. Use `ota pull EXPECTED_MID flash` and poll
|
||||
`ota status` at a restrained interval.
|
||||
5. Require `ready to install` with the exact MID. Stop the seeder gracefully
|
||||
with Ctrl-C. Recheck watchdog `off`, then send `ota install`.
|
||||
6. Allow up to five minutes for the application to return. Require the exact
|
||||
after hash, expected version, and `bootloader: apply OK` before continuing.
|
||||
Stop if any check fails; do not try the next package against another base.
|
||||
|
||||
A retained previous container may be re-discovered after boot. If the exact
|
||||
previous MID is verifying, wait for it to become ready or idle. Only after
|
||||
re-proving the current running hash may that attached, completed previous
|
||||
MID be cancelled. An idle `ota cancel` does not prove that flash was erased.
|
||||
|
||||
After the final hash is proved, restore the destination, sender, and relays
|
||||
to their recorded normal settings and confirm normal-channel reachability.
|
||||
Re-enable the system watchdog and require it to report `on`. The current
|
||||
endpoint supports `normalradio`; coordinate channel restoration so that the
|
||||
remote node does not become unreachable during the handoff.
|
||||
The endpoint requires a temporary radio channel for OTA operations, so a
|
||||
normal-channel `ota ls` is not a reachability test. Use an ordinary mesh trace
|
||||
or authenticated remote command after restoring normal radio.
|
||||
|
||||
The existing `rak3401_mota_chain.py` pins older release archives. It must not
|
||||
be pointed at this new bundle or have its safety checks bypassed.
|
||||
|
||||
## Evidence and reproducibility
|
||||
|
||||
`build_rak3401_legacy24_bundle.py` checks every base/target hash, hardware
|
||||
identity, increasing version, block capability, flash workspace, and staging
|
||||
margin. It reconstructs every target with both zero- and erased-flash-filled
|
||||
scratch space, and applies every package with the actual OTAFIX2.4 release
|
||||
implementation compiled as a flash simulator. It remeasures transport with
|
||||
the pinned encoder. Its output remains explicitly offline-only until a
|
||||
separate physical qualification record is attached.
|
||||
|
||||
`ACCOUNTING-CORRECTION.json` records the proactive-proof accounting correction.
|
||||
It changed the ideal totals only; all package checksums and the selected
|
||||
hardware-test sequence remained unchanged.
|
||||
|
||||
The archive includes the 52 frozen search images, all 47,920 measured
|
||||
geometries, encoder binaries, per-step simulator logs, and raw hardware-test
|
||||
records. Only the selected `.mota` containers are served over LoRa. Lab
|
||||
scripts in `evidence/` are tied to the Pi's USB/debug fixture and are not a
|
||||
remote field deployment controller.
|
||||
|
||||
Regression checks: 534 LoRa OTA tests ran successfully with one opt-in
|
||||
dependency-install test skipped; all 11 linker-selection tests passed.
|
||||
|
||||
All PlatformIO builds must be serialized, including builds in separate
|
||||
checkout directories that share the same PlatformIO package installation.
|
||||
This document is now in [Research](research/rak3401_1.17.1.7_legacy24.md).
|
||||
+3
-702
@@ -1,704 +1,5 @@
|
||||
# RAK3401 1W repeater compact LoRa update chain
|
||||
# Document moved
|
||||
|
||||
> Status: **published prerelease for controlled, recoverable lab use.** On
|
||||
> 29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte
|
||||
> identical to the corresponding transitions in the physically passed
|
||||
> `fd98bc90` chain. The replacement step 10 installs the current `3f6eddd5`
|
||||
> endpoint. That replacement package and the hardened current runner have
|
||||
> passed offline reconstruction plus the exact deployed bootloader simulator, but
|
||||
> have not had a new physical end-to-end run. Use the lab gate and keep local
|
||||
> DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates
|
||||
> remain planning data, not physical qualification.
|
||||
<script>window.location.replace(new URL("../research/rak3401_mota_chain/" + window.location.hash, window.location.href));</script>
|
||||
|
||||
The prerelease is
|
||||
[`rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5`](https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5).
|
||||
Use this asset:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
```
|
||||
|
||||
- prerelease ZIP SHA-256: `135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4`
|
||||
- endpoint image SHA-256: `cd1e9b819f9e918f9f2ceaf37b8a74bf242e2be78f319af2cf02a42d7729d0f4`
|
||||
- endpoint EndF body hash: `9BD7CF682EE065AE`
|
||||
|
||||
`--verify-only` remains the safe default. Live preflight and installation of
|
||||
this prerelease require `--accept-test-candidate`. That switch bypasses only
|
||||
the prerelease-status gate; it does not bypass the archive, identity,
|
||||
bootloader, route, watchdog, reachability, or post-boot checks.
|
||||
|
||||
## Exact destination
|
||||
|
||||
This chain is intentionally specific to one firmware lineage and hardware
|
||||
target:
|
||||
|
||||
- target ID: `2FA509C1`
|
||||
- hardware: `RAK_3401`
|
||||
- role: `RAK_3401_repeater_lora_ota_no_external_sensors`
|
||||
- start: `v1.16.7.0-c1caa5ad`, EndF `71F4026CBE4B8B74`
|
||||
- endpoint source: MeshCore `3f6eddd5c20f1fa3df6ba1cc208efbe3e2fffb43`
|
||||
- endpoint label: `v1.17.1.6-halo-keymind-cascade-dev-3f6eddd5`
|
||||
- endpoint packed version: `0x01110106`
|
||||
- deployment target key: `63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d`
|
||||
|
||||
The exact starting application is the
|
||||
[`lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf`](https://github.com/mikecarper/MeshCore/releases/tag/lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf)
|
||||
RAK3401 asset. Its outer ZIP SHA-256 is
|
||||
`58ead2ea18efafda186bc9d983fe08cdf65f69b0e5a3a1c72c18fe35948a254c`;
|
||||
its 566,912-byte `firmware.bin` SHA-256 is
|
||||
`4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb`.
|
||||
The endpoint build ZIP SHA-256 is
|
||||
`a9fca9be145a96feb650b57c43b5ed173d74d2f3194da12e31b1c54352635b56`.
|
||||
|
||||
### Retained and omitted hardware support
|
||||
|
||||
Despite the legacy `no_external_sensors` target name, the endpoint retains the
|
||||
RAK12501 GPS provider and the common INA3221, INA219, INA226, and INA260 I2C
|
||||
voltage/current monitors. Install RAK12501 in sensor slot A. Do not use slot D
|
||||
with the RAK13302 1 W radio because the GPS reset/PPS signals overlap the
|
||||
radio's BUSY/DIO1 wiring.
|
||||
|
||||
The reduced profile omits optional external environmental and ranging devices:
|
||||
AHTX0, BME280, BMP280, SHTC3, SHT4x, LPS22HB, MLX90614, VL53L0X, BME680,
|
||||
BMP085, RAK12035, and BME680 BSEC. Board radio, display, buttons, battery
|
||||
telemetry, GPS, and the INA monitor family remain. No additional “remove I2C
|
||||
temperature” image is needed: the optional I2C temperature/environment drivers
|
||||
are already in the omitted set, while removing the remaining I2C/INA support
|
||||
would discard desired voltage/current monitoring and create another firmware
|
||||
identity without helping this route.
|
||||
|
||||
Do not use the chain on another target ID, hardware family, starting image, or
|
||||
firmware body hash. The runner checks all four.
|
||||
|
||||
## Why the deployed OTAFIX2.4 and old staging ceiling work
|
||||
|
||||
The deployed bootloader is not changed and no package uses the newer expanded
|
||||
`0xED000` ceiling. Each package remains bottom-aligned below the old
|
||||
bootloader's `0xD4000` scan ceiling. Its detools workspace is page-aligned and
|
||||
must satisfy:
|
||||
|
||||
```text
|
||||
0x26000 + inplace_memory <= staged_package_start < 0xD4000
|
||||
```
|
||||
|
||||
The old bootloader already reads and validates that encoded detools geometry
|
||||
before its first destructive write. The previous application receiver was the
|
||||
limiting part: it reserved a fixed `0x98000` workspace before accepting a
|
||||
download. Step 1 therefore remains byte-for-byte identical to the physically
|
||||
passed 89,844-byte package. Step 2 is a 46,364-byte fixed-workspace package
|
||||
that installs the compatibility receiver. Later receivers stage above the
|
||||
real EndF image extent and let the bootloader enforce each package's selected
|
||||
workspace.
|
||||
|
||||
Every package in this candidate is an ordinary application container using
|
||||
format ABI 2 and in-place codec 2. The historical bridge applications scan the
|
||||
bootloader capability marker byte-by-byte, so they recognize the released
|
||||
`0.9.2-OTAFIX2.4` marker at absolute address `0xFCD16`. That immutable marker
|
||||
advertises ABI 2, codec mask `0x0004`, and internal storage profile 0. The
|
||||
`3f6eddd5` endpoint retains the matching legacy application-update fallback.
|
||||
Its separately compiled internal bootloader-update feature remains unavailable
|
||||
because privileged self-update requires a current ABI 3 marker with the exact
|
||||
storage profile.
|
||||
Installing this chain therefore does not replace the bootloader, and the final
|
||||
application can still accept a future valid ABI-2/codec-2 application update.
|
||||
|
||||
The exact installed bootloader is the
|
||||
[`0.9.2-OTAFIX2.4` RAK4631 asset](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4_s140_6.1.1.zip).
|
||||
Its outer ZIP SHA-256 is
|
||||
`5e29e7a8982cc2ed5f8868435556eb09387250fe5b9a7e8443bac325247dfed1`;
|
||||
its 191,040-byte `sd_bl.bin` SHA-256 is
|
||||
`a97f834388db0f1da6c29f6d8f458ff6d3e6a26034709044566384838919bb23`.
|
||||
The exact simulator was compiled from release commit
|
||||
`d73de8372e89b8ef352747c8bc7a1aaeab80fbfe`; its executable SHA-256 is
|
||||
`9dacf24b1023fe2f4c620419417649ccc9538dd4a611b47bc19b7fedb97ceba6`.
|
||||
|
||||
The schema-2 exhaustive search checked 1,799 candidate geometries and found 12
|
||||
feasible forward edges in the declared exact 12-node inventory. It found one
|
||||
shortest ten-package route and selected the smallest total container bytes as
|
||||
the secondary objective. This minimum applies only to that declared inventory
|
||||
and geometry table, not every conceivable new bridge image. The selected route
|
||||
is:
|
||||
|
||||
| Step | From | To | Workspace | Package | Margin |
|
||||
|---:|---|---|---:|---:|---:|
|
||||
| 1 | 1.16.7.0 | 1.16.7.9 | `0x98000` | 89,844 | 0 |
|
||||
| 2 | 1.16.7.9 | 1.16.7.10 | `0x98000` | 46,364 | 40,960 |
|
||||
| 3 | 1.16.7.10 | 1.16.8.7 | `0x91000` | 106,029 | 12,288 |
|
||||
| 4 | 1.16.8.7 | 1.16.9.105 | `0x89000` | 149,927 | 0 |
|
||||
| 5 | 1.16.9.105 | 1.16.9.110 | `0x8E000` | 111,312 | 16,384 |
|
||||
| 6 | 1.16.9.110 | 1.16.9.113 | `0x8F000` | 120,624 | 4,096 |
|
||||
| 7 | 1.16.9.113 | 1.16.9.117 | `0x8B000` | 139,637 | 0 |
|
||||
| 8 | 1.16.9.117 | 1.16.10.0 | `0x7E000` | 174,202 | 20,480 |
|
||||
| 9 | 1.16.10.0 | 1.17.1.3 | `0x6B000` | 271,569 | 0 |
|
||||
| 10 | 1.17.1.3 | 1.17.1.6 | `0x76000` | 197,825 | 28,672 |
|
||||
|
||||
Total mOTA transfer data is 1,407,333 bytes. `ROUTE.json`, `CHAIN.csv`, and
|
||||
`validation-results.json` in the bundle pin the exact geometry and image hash
|
||||
for every transition.
|
||||
|
||||
Steps 1-9 are byte-for-byte identical to the transitions that passed on the
|
||||
physical RAK3401. The replacement step 10, from the physically reached
|
||||
`FE65A6135A1E7B3F` v1.17.1.3 body to `9BD7CF682EE065AE`, has offline
|
||||
qualification only. All ten packages passed independent container checks,
|
||||
zero-filled and erased-workspace reconstruction, and the exact deployed
|
||||
`0.9.2-OTAFIX2.4` simulator. Those results prove the package bytes and apply
|
||||
geometry; they are not a physical claim for the replacement step 10 or the
|
||||
hardened runner.
|
||||
|
||||
The 171-byte transport DEFLATE support lands only in the final `3f6eddd5`
|
||||
application. The starting receiver and every receiver that accepts steps 1-10
|
||||
lack that inflate path, so all ten legacy-chain transfers remain raw and the
|
||||
1,407,333-byte total above is not automatically reduced over LoRa. A later
|
||||
update can negotiate transport compression after this endpoint is running;
|
||||
the application inflates before staging, and the immutable bootloader still
|
||||
applies an ordinary ABI-2/codec-2 container.
|
||||
|
||||
## Host requirements
|
||||
|
||||
Install Python 3.9 or newer, `meshcli` 1.6.0 or newer, the current `motatool`,
|
||||
and an OTA-enabled Full Companion or repeater that can seed mOTA files.
|
||||
|
||||
## Verify offline
|
||||
|
||||
No password or device is needed:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--motatool /path/to/motatool \
|
||||
--verify-only
|
||||
```
|
||||
|
||||
When given the ZIP, the runner checks its pinned outer hash and binds the
|
||||
extraction cache to that exact archive. An explicitly supplied extracted root
|
||||
starts at the pinned inner checksum list. In either case it checks complete
|
||||
inner coverage, all ten manifests, continuity, image anchors, both recovery
|
||||
images, and every container with `motatool`. Passing this command means offline
|
||||
and simulator qualification only; it does not unlock live use.
|
||||
|
||||
## Guarded prerelease lab command
|
||||
|
||||
These direct-link arguments match the physical transition setup, but the
|
||||
current package replaces step 10 and the current host runner contains later
|
||||
cleanup and recovery fixes. Neither has been cleanly rerun through the whole
|
||||
chain. Keep local USB/SWD recovery available. The candidate switch bypasses
|
||||
only the prerelease-status block; all identity, checksum, bootloader, route,
|
||||
watchdog, reachability, and post-boot gates still run.
|
||||
|
||||
Restore the test start locally with
|
||||
`recovery/test-start/RAK3401-test-start-v1.16.7-c1caa5ad.uf2`, then run:
|
||||
|
||||
```bash
|
||||
export MESHCORE_ADMIN_PASSWORD='password'
|
||||
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp 192.168.1.51:5001 \
|
||||
--source-cli-tcp 192.168.1.51:5002 \
|
||||
--source-shares-controller \
|
||||
--target-key 63d8df63 \
|
||||
--temp-radio 909.950,500,5,5,120 \
|
||||
--ota-hops 0 \
|
||||
--legacy-full-airtime \
|
||||
--motatool /path/to/motatool \
|
||||
--accept-test-candidate \
|
||||
--yes
|
||||
```
|
||||
|
||||
`--target-key` accepts a full contact public key or a unique hexadecimal prefix
|
||||
of at least eight characters, such as the 12-character prefix shown by
|
||||
`meshcli lc`. The chain resolves it against the controller's saved contacts
|
||||
and uses the full key for every remote command and transfer step. Emoji or a
|
||||
later change in the repeater's display name does not affect selection. Each
|
||||
`--relay` can likewise use a full key or unique prefix instead of a name.
|
||||
The target still must be a repeater contact with the exact hardware, firmware,
|
||||
and bootloader required by this chain.
|
||||
|
||||
Keep the work directory. Rerunning the same command resumes only when the live
|
||||
EndF body hash matches an exact chain node. Never manually skip a package.
|
||||
`--legacy-full-airtime` temporarily sets the destination airtime factor to
|
||||
zero and restores it at the endpoint. Use that option only where the selected
|
||||
frequency and local duty-cycle rules permit a full transmit budget; omit it
|
||||
otherwise.
|
||||
|
||||
## Superseded `b40d2e6c` prerelease
|
||||
|
||||
The immediately preceding prerelease remains pinned for offline provenance:
|
||||
|
||||
- release: [`rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c`](https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c)
|
||||
- asset: `RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip`
|
||||
- outer ZIP SHA-256: `8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a`
|
||||
- endpoint image SHA-256: `a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905`
|
||||
- endpoint EndF body hash: `1FA6AAA3C28D8BD8`
|
||||
|
||||
Its first nine packages are byte-for-byte identical to the current release and
|
||||
the physical `fd98bc90` evidence. Its different step 10 had offline
|
||||
qualification only. The runner recognizes this exact archive separately and
|
||||
keeps live use gated; it is not interchangeable with the current `3f6eddd5`
|
||||
asset.
|
||||
|
||||
## Legacy `fd98bc90` physical and SWD qualification
|
||||
|
||||
The earlier local candidate remains the exact physical evidence source for
|
||||
this route:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
```
|
||||
|
||||
- candidate ZIP SHA-256: `c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881`
|
||||
- endpoint image SHA-256: `31c182c888ceb1135e5afb2376610d93cee2e807b556c838e07fd4486c79d095`
|
||||
- endpoint EndF body hash: `9179B98432895924`
|
||||
|
||||
All ten exact `fd98bc90` transitions completed on the RAK3401 with its
|
||||
deployed `0.9.2-OTAFIX2.4` bootloader and a Heltec V4 source at 909.950 MHz /
|
||||
500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD
|
||||
readback then matched the endpoint application, original bootloader,
|
||||
SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine
|
||||
packages in the `b40d2e6c` and `3f6eddd5` prereleases. All three candidates
|
||||
have different step 10 packages or endpoints, so the physical result and
|
||||
step-10 timing must not be attributed to either later prerelease.
|
||||
|
||||
### Measured direct-link timing for `fd98bc90`
|
||||
|
||||
The first `ready to install` transfer ages were:
|
||||
|
||||
| Step | Blocks | Transfer |
|
||||
|---:|---:|---:|
|
||||
| 1 | 88 | 598 s |
|
||||
| 2 | 45 | 42 s |
|
||||
| 3 | 103 | 81 s |
|
||||
| 4 | 146 | 105 s |
|
||||
| 5 | 109 | 76 s |
|
||||
| 6 | 118 | 94 s |
|
||||
| 7 | 136 | 102 s |
|
||||
| 8 | 170 | 127 s |
|
||||
| 9 | 264 | 187 s |
|
||||
| 10 | 169 | 193 s |
|
||||
|
||||
The physically observed bulk-transfer total for those exact legacy packages is
|
||||
1,605 seconds (26m45s). The log came from the pre-cleanup-fix runner revision.
|
||||
Step 1 was measured before the host began forcing source RXPS off and is
|
||||
therefore a conservative outlier. A new complete run with that fix is projected
|
||||
at roughly 18–19 minutes of bulk transfer, but that projection is not a
|
||||
substitute for the measured total. Allow about 70–75 minutes direct at BW500
|
||||
for validation, ten transfers, installs/reboots, retained-store checks, and
|
||||
final restoration.
|
||||
|
||||
The legacy step 10's longer per-block time was not a V4 TempRadio delay: the
|
||||
source RXPS readback was off. Its v1.17.1.3 requester uses non-overlapping
|
||||
adaptive flights and a five-second recovery floor and logged more block reloads
|
||||
than step 8. The V4 itself intentionally schedules its local radio handoff
|
||||
after 1.5 seconds so the normal-channel command reply can drain; the host waits
|
||||
three seconds and then checks the live TempRadio state.
|
||||
|
||||
### Live progress and speed sanity checks
|
||||
|
||||
The runner watches `motatool serve -v` locally, so its quarter-progress reports
|
||||
do not consume LoRa airtime. At BW500 direct, physically measured steps 2–9
|
||||
averaged about 1.1–1.4 newly requested payload blocks per second; the legacy
|
||||
step 10 averaged about 0.88 blocks/s, and step 1's 0.15 blocks/s was the known
|
||||
pre-RXPS-fix outlier. Use those only as bench sanity ranges, not guarantees.
|
||||
Relays, narrower bandwidths, interference, and recovery reads lower the rate.
|
||||
|
||||
Healthy output keeps advancing from `[download] passive source progress x/y`
|
||||
to `source read all y payload blocks`, followed by destination status `ready to
|
||||
install y/y (100%)` for the same manifest ID. Aggregate reads above the unique
|
||||
block count indicate recovery/retry traffic. A radio-scaled no-progress window
|
||||
causes one destination status check; the transfer timeout leaves the partial
|
||||
download staged for an exact-package resume. Stop and diagnose instead of
|
||||
installing if the manifest changes, progress remains flat through those checks,
|
||||
the final receiver count is not `y/y`, or the measured rate is far below the
|
||||
matching bandwidth/hop estimate without an understood RF cause.
|
||||
|
||||
For planning only, scale the conservative measured `fd98bc90` 26m45s bulk
|
||||
baseline by the qualified adaptive-preamble airtime and by each additional
|
||||
relay transmission:
|
||||
|
||||
| Bandwidth, SF5/CR5 | Direct / 0 relays | 1 relay | 2 relays |
|
||||
|---:|---:|---:|---:|
|
||||
| 500 kHz | 26m45s | 53m30s | 1h20m15s |
|
||||
| 250 kHz | 46m51s | 1h33m42s | 2h20m33s |
|
||||
| 125 kHz | 1h27m03s | 2h54m07s | 4h21m11s |
|
||||
| 62.5 kHz | 2h54m07s | 5h48m15s | 8h42m23s |
|
||||
|
||||
These are transfer-only estimates, not measured alternate-bandwidth or relay
|
||||
results. Packet loss, relay contention, discovery, administrative commands,
|
||||
staged verification, and ten reboot cycles add time. The bandwidth factors are
|
||||
not simple powers of two because the adaptive wire preamble is 128 symbols at
|
||||
500 kHz, 64 at 250 kHz, and 32 at 125/62.5 kHz.
|
||||
|
||||
## Manual operation
|
||||
|
||||
The automated runner is preferred because it binds every transition to the
|
||||
expected MID and body hash. The manual details below document the checks used
|
||||
during qualification and recovery; they are not permission to skip the
|
||||
runner's candidate gate. Keep a written copy of each original setting and
|
||||
never skip a step even when a later package appears in `ota ls`.
|
||||
|
||||
### 1. Verify and extract the asset
|
||||
|
||||
```bash
|
||||
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5
|
||||
sha256sum -c SHA256SUMS.txt
|
||||
for package in motas/*.mota; do motatool verify "$package" || exit 1; done
|
||||
```
|
||||
|
||||
The outer hash must be
|
||||
`135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4`,
|
||||
and the SHA-256 of the extracted `SHA256SUMS.txt` must be
|
||||
`93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4`.
|
||||
Do not continue after any checksum or verification error.
|
||||
|
||||
### 2. Record and prepare the destination
|
||||
|
||||
Before changing anything, save the complete replies from:
|
||||
|
||||
```text
|
||||
ver
|
||||
ota self
|
||||
get bootloader.ver
|
||||
get system.watchdog
|
||||
get radio.rxps
|
||||
get powersaving
|
||||
get rxdelay
|
||||
get af
|
||||
ota config
|
||||
```
|
||||
|
||||
The starting `ota self` hash must be `71F4026CBE4B8B74`, the target must be
|
||||
`2FA509C1`, and the hardware must be `RAK_3401`. Disable the nRF52 system
|
||||
watchdog before the chain:
|
||||
|
||||
```text
|
||||
set system.watchdog off
|
||||
```
|
||||
|
||||
The hardware watchdog cannot stop immediately. The RAK will reset once within
|
||||
about 60 seconds. Reconnect, wait at least 90 seconds from the command, and
|
||||
require `get system.watchdog` to report `> off` before continuing.
|
||||
|
||||
For a direct bench link, apply these temporary transfer guardrails to the RAK
|
||||
after every bridge reboot:
|
||||
|
||||
```text
|
||||
set powersaving off
|
||||
set radio.rxps off
|
||||
set rxdelay 0
|
||||
ota config hops 0
|
||||
tempradio 909.950,500,5,5,120
|
||||
```
|
||||
|
||||
Where local duty-cycle rules permit it, `set af 0` can also remove the legacy
|
||||
firmware's saved airtime wait during this bounded maintenance window. Record
|
||||
`get af` first and restore that exact value after the final step. Current
|
||||
firmware grants the bounded TempRadio transfer budget without overwriting the
|
||||
saved airtime factor.
|
||||
|
||||
`set powersaving off` is an isolation guardrail, not the fix for the observed
|
||||
failure. The actual source failure was an oversized USB CDC reply; current
|
||||
firmware splits those reads. Some historical bridge builds may report RXPS as
|
||||
unsupported while already operating continuously. Verify the reported state
|
||||
and do not substitute a normal reboot for the one-time watchdog reset. Direct
|
||||
tests use `hops 0`: `hops 1` makes the target echo each source response despite
|
||||
there being no intermediate relay, increasing half-duplex loss and legacy
|
||||
three-second retries.
|
||||
|
||||
The exact candidate cannot use fast RXPS throughout because its historical
|
||||
bridge applications predate the v1.17.1.5 adaptive-preamble contract. Keep
|
||||
RXPS off for all ten steps whether using SF5/BW250 or the faster direct-bench
|
||||
SF5/BW500 tuple. After the final endpoint is proven, a later update whose
|
||||
target, controller, source, and relays are all v1.17.1.5 or newer can use the
|
||||
generic LoRa OTA runner and its qualified adaptive-preamble boundaries.
|
||||
|
||||
Put the source on the identical TempRadio tuple. A current ASCII-first Full
|
||||
Companion recognizes `motatool`'s initial `ota folder on` line directly:
|
||||
|
||||
```bash
|
||||
motatool serve \
|
||||
--dir ./motas \
|
||||
--serial /dev/ttyACM1 \
|
||||
--baud 115200 \
|
||||
-v
|
||||
```
|
||||
|
||||
The same command also works with older Full Companion firmware in binary mode,
|
||||
where the idle parser recognizes the identical preamble. Restart `motatool` for
|
||||
every step so the source emits a fresh catalog advert. Leave it running during
|
||||
the download and stop it with Ctrl-C only after the destination reports `ready
|
||||
to install`.
|
||||
|
||||
### 3. Install all ten packages in order
|
||||
|
||||
| Step | Manifest ID | Before hash | Expected version | After hash |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `C147BCEF` | `71F4026CBE4B8B74` | `1.16.7.9` | `42BC53A64288E845` |
|
||||
| 2 | `BEB578FC` | `42BC53A64288E845` | `1.16.7.10` | `1AD2AA8B6C478DA6` |
|
||||
| 3 | `6D8EF814` | `1AD2AA8B6C478DA6` | `1.16.8.7` | `D709A25308BC1831` |
|
||||
| 4 | `40AC4CEB` | `D709A25308BC1831` | `1.16.9.105` | `457BEDA5E6406C81` |
|
||||
| 5 | `F6C8890B` | `457BEDA5E6406C81` | `1.16.9.110` | `E76BFA205634CAB8` |
|
||||
| 6 | `82405E89` | `E76BFA205634CAB8` | `1.16.9.113` | `65328FC8A1FBED2D` |
|
||||
| 7 | `80F40DB6` | `65328FC8A1FBED2D` | `1.16.9.117` | `AF7532E13337FADD` |
|
||||
| 8 | `DBF6310D` | `AF7532E13337FADD` | `1.16.10.0` | `22002359BBDFA76E` |
|
||||
| 9 | `C2979E08` | `22002359BBDFA76E` | `1.17.1.3` | `FE65A6135A1E7B3F` |
|
||||
| 10 | `B5011420` | `FE65A6135A1E7B3F` | `1.17.1.5` | `1FA6AAA3C28D8BD8` |
|
||||
|
||||
For each row, first prove that `ota self` exactly matches its **Before hash**,
|
||||
then inspect the manager before changing it:
|
||||
|
||||
```text
|
||||
ota status
|
||||
```
|
||||
|
||||
Proceed to discovery only after that status is reconciled. When the manager is
|
||||
idle, run:
|
||||
|
||||
```text
|
||||
ota ls
|
||||
ota pull MANIFEST_ID flash
|
||||
ota status
|
||||
```
|
||||
|
||||
If the first status reports this row's MID as partial, use `ota pull` with that
|
||||
same MID to resume it. If this row's MID is already ready, do not pull or
|
||||
cancel it; continue with the install checks below. If status reports the
|
||||
immediately previous row's MID as `verifying staged blocks`, wait through the
|
||||
bounded discovery window; require either that exact MID to become `ready to
|
||||
install` or the manager to become `no download`, and re-prove the current
|
||||
**Before hash**. Cancel only the same previous MID while it is visibly
|
||||
attached and ready. If it has become idle, send no cancel. Stop on any other
|
||||
MID, failed or incomplete state, ambiguous reply, or timeout. Never issue a
|
||||
blind `ota cancel`: it can discard a resumable or unrelated session.
|
||||
|
||||
Repeat `ota status` at a restrained interval until it says `ready to install`.
|
||||
If the manifest is initially absent, wait for a fresh source advert, run
|
||||
`ota ls` again, or restart `motatool`; do not pull a different ID. Stop the
|
||||
seeder and install:
|
||||
|
||||
```text
|
||||
get system.watchdog
|
||||
ota install
|
||||
```
|
||||
|
||||
The watchdog reply must still be `> off`. Allow the current default five-minute
|
||||
readiness window for the USB/LoRa identity to return; automation probes every
|
||||
10 seconds and returns early as soon as the exact body is visible. Then run
|
||||
`ver`, `ota self`, and `ota help`.
|
||||
Require the row's exact **After hash**, require `bootloader: apply OK`, and
|
||||
require `rescue install <hash16>` in the help before exposing that bridge to
|
||||
the next package. This rescue-help gate applies to steps 1-9; step 10 is the
|
||||
endpoint and is not exposed to another package in this chain. A normal-radio
|
||||
`ota status` response of `no download` proves only that the manager is idle;
|
||||
legacy internal-flash firmware does not durably erase a retained container by
|
||||
accepting an IDLE `ota cancel`. Do not claim otherwise. When the same expected
|
||||
manifest reports `verifying staged blocks` after the next TempRadio starts,
|
||||
wait only for the bounded discovery window and require that it become `ready
|
||||
to install` or `no download`; stop on a changed ID, failed/incomplete state, or
|
||||
timeout. Prove the exact running body before cancelling an attached completed
|
||||
session. OTAFIX consumes the approval word during a successful install, so a
|
||||
retained endpoint container is inert and a later valid pull replaces it.
|
||||
Reapply the transfer
|
||||
guardrails and TempRadio tuple, restart `motatool`, and continue with the next
|
||||
row.
|
||||
|
||||
If the board boots but `ota self` says the EndF is invalid, stop. Only when
|
||||
`ota status` still identifies that row's complete staged package may the
|
||||
guarded `ota rescue install BEFORE_HASH` command be used. It is not a force
|
||||
option and must use that row's exact Before hash. If the board does not boot,
|
||||
recover the documented start/recovery UF2 locally over USB. Step 10 is the
|
||||
endpoint and has no successor package in this chain. If its EndF is invalid,
|
||||
use local DFU/SWD rather than attempting an unqualified LoRa package.
|
||||
|
||||
### 4. Intermediate relays and restoration
|
||||
|
||||
On every managed intermediate relay, save `get rxdelay` and `get txdelay`, then
|
||||
use the airtime-scaled collision window during the maintenance session:
|
||||
|
||||
```text
|
||||
set rxdelay 0
|
||||
set txdelay 0.3
|
||||
```
|
||||
|
||||
Current OTA transfer packets honor that configured relay `txdelay`; setting
|
||||
`0.3` gives competing relays a small randomized, airtime-scaled forwarding
|
||||
window. The source's manifest-fragment gap independently follows active packet
|
||||
airtime and duty spacing, clamped to 100-1000 ms. These two delays solve
|
||||
different problems.
|
||||
|
||||
After step 10 is proven, restore each saved destination and relay value exactly,
|
||||
including `rxdelay`, RXPS, CPU power saving, `af`, `ota config hops`, and relay
|
||||
timing. Current full-parser repeater firmware accepts `normalradio` and restores
|
||||
the saved tuple after replying on the temporary channel:
|
||||
|
||||
```text
|
||||
normalradio
|
||||
```
|
||||
|
||||
The `b40d2e6c` endpoint supports that command. If a recovery build unexpectedly
|
||||
replies `Unknown command`, shorten its lease to one minute with the same tuple:
|
||||
|
||||
```text
|
||||
tempradio 909.950,500,5,5,1
|
||||
```
|
||||
|
||||
Wait for the lease to expire, return the controller and ordinary source to
|
||||
their saved normal channels, and then re-enable the RAK system watchdog:
|
||||
|
||||
```text
|
||||
set system.watchdog on
|
||||
get system.watchdog
|
||||
```
|
||||
|
||||
A Full Companion source is the exception: its local TCP console supports
|
||||
`normalradio` and the automated runner uses it before restoring the shared
|
||||
Binary API radio tuple.
|
||||
|
||||
Final lab success requires version
|
||||
`v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c`, body hash
|
||||
`1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, and
|
||||
`get system.watchdog` reporting `> on`. If a relay cannot be restored before
|
||||
its TempRadio lease ends, wait for it to return to the normal channel and
|
||||
restore its saved `rxdelay` and `txdelay` there.
|
||||
|
||||
## Two-relay deployment
|
||||
|
||||
The first nine direct package transitions above were physically observed as
|
||||
the byte-identical first nine transitions of the legacy chain. Replacement
|
||||
step 10, this hardened host-runner revision, and multi-hop operation are not
|
||||
cleanly end-to-end physically qualified. A controlled, recoverable multi-hop
|
||||
lab can exercise two intermediate relays by
|
||||
listing them farthest-to-nearest and using three OTA hops. Run a non-mutating
|
||||
preflight first; even preflight requires the prerelease candidate override
|
||||
because the command connects to live devices:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp SOURCE_IP:5001 \
|
||||
--source-cli-tcp SOURCE_IP:5002 \
|
||||
--source-shares-controller \
|
||||
--target-key 63d8df63 \
|
||||
--relay 'Far Repeater' \
|
||||
--relay 'Near Repeater' \
|
||||
--temp-radio 909.950,250,5,5,120 \
|
||||
--ota-hops 3 \
|
||||
--motatool /path/to/motatool \
|
||||
--accept-test-candidate \
|
||||
--preflight-only
|
||||
```
|
||||
|
||||
After preflight succeeds, rerun with `--yes` in place of
|
||||
`--preflight-only`. Confirm that the selected frequency and bandwidth are
|
||||
legal at the deployment location.
|
||||
|
||||
## Rescue and completion
|
||||
|
||||
The guarded `ota rescue install <base_body_hash>` command is present after
|
||||
step 1 and remains available through the step-9 bridge. It is not a force
|
||||
command: it refuses a valid normal EndF, a base mismatch, wrong
|
||||
target/hardware, or invalid payload. A failure before step 1 completes still
|
||||
requires local USB recovery because the deployed start image predates the
|
||||
rescue command. The final `b40d2e6c` endpoint uses the stricter shared-slot
|
||||
profile; its invalid-EndF recovery is local DFU/SWD, not the rescue command.
|
||||
|
||||
Before the first mutation, the runner saves the destination's RXPS periods,
|
||||
CPU power-saving state, RX flood delay, airtime factor, and OTA hop reach in
|
||||
the persistent work directory, flushing both the file and its directory entry
|
||||
before changing a guardrail. It also saves the managed source's exact RXPS
|
||||
preference in the chain-root `source-rxps-settings.json` before disabling it.
|
||||
Every step reuses that protected record, so a rerun after a killed host process
|
||||
restores the chain-start preference instead of treating the temporary off state
|
||||
as original. The record is accepted only for the same CLI endpoint. The runner
|
||||
retires that chain-root record only after it has proved the source is back on
|
||||
its normal radio and restored the exact preference at the verified endpoint.
|
||||
The runner
|
||||
reads the destination, controller, source, and relay versions before applying
|
||||
the same RXPS policy as the generic runner.
|
||||
The mixed historical chain therefore verifies `radio.rxps off` after every
|
||||
bridge reboot; a future all-v1.17.1.5-or-newer SF5/BW250 chain would instead
|
||||
keep RXPS on under the qualified level-8/preamble-64 boundary. It also verifies
|
||||
`set powersaving off` and `rxdelay 0`, plus `af 0` when
|
||||
`--legacy-full-airtime` was explicitly selected. It restores every original
|
||||
value only after the exact endpoint is proven. An interrupted run deliberately
|
||||
leaves those transfer guardrails active; rerun the same command with the same
|
||||
work directory to resume and restore them.
|
||||
CPU power saving was not the cause of the observed folder-source failure: that
|
||||
was a USB CDC receive-ring overrun fixed by bounded serial reads. The runtime
|
||||
settings remain conservative isolation and fast-link reliability guardrails.
|
||||
|
||||
For every named `--relay`, the per-step runner also captures `rxdelay` and
|
||||
`txdelay`, writes them to `relay-timing-settings.json`, applies `rxdelay 0` and
|
||||
`txdelay 0.3` during transfer, verifies the readback, and restores both before
|
||||
the relay leaves TempRadio.
|
||||
|
||||
The runner also keeps the watchdog off across the chain, rechecks identity and
|
||||
OTA reach before every package, requires the exact post-boot EndF hash, and
|
||||
only re-enables the watchdog after step 10. Lab success requires endpoint body
|
||||
hash `1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, normal radio
|
||||
`910.525 MHz / 62.5 kHz / SF7 / CR5`, and the watchdog verified on.
|
||||
|
||||
## External-radio startup and manual USB recovery
|
||||
|
||||
RAK3401 is a distinct target from RAK4631. Its RAK13300/RAK13302 radio is on
|
||||
the WisBlock SPI bus (`SPI1` in the Adafruit nRF52 core), with BUSY and DIO1 on
|
||||
P0.09/P0.10. Current builds explicitly enable those NFC-capable pads as GPIO
|
||||
and do not advertise the radio bus as an on-board QSPI flash device.
|
||||
|
||||
The RAK13302's FEM and boost supply use switched `3V3_S`, but the SX1262 core
|
||||
uses unswitched 3V3. An MCU reset, a 1200-baud DFU touch, or disconnecting only
|
||||
USB may therefore leave a battery-powered SX1262 in its previous state. At
|
||||
startup, current firmware quiesces the FEM, reinitializes the dedicated SPI
|
||||
instance, asserts NRESET, and, if BUSY remains high, sends the Semtech
|
||||
NSS/GET_STATUS wake sequence without waiting on RadioLib first. The one-time
|
||||
board startup cold-starts `3V3_S`; later radio retries leave that shared rail
|
||||
enabled so they do not reset GPS or sensor modules.
|
||||
|
||||
If BUSY still cannot be released, firmware must not enter a reset loop:
|
||||
|
||||
- Companion starts its USB/BLE management services after three bounded probes,
|
||||
uses nRF52 hardware entropy if it needs a first-boot identity, and retries the
|
||||
radio every 60 seconds. Radio statistics remain zero until recovery.
|
||||
- Repeater keeps the same USB CDC session open and prints
|
||||
`Radio unavailable; retrying in 60 seconds`. It repeats the board-level probe
|
||||
in place. The normal repeater CLI and mesh transport cannot start until the
|
||||
radio responds.
|
||||
|
||||
A permanently high BUSY after NRESET and the direct wake transaction indicates
|
||||
an electrically unavailable radio, loose module, or power-domain fault; an MCU
|
||||
reboot cannot manufacture a response. If local access is possible, disconnect
|
||||
the battery as well as USB and reseat the WisBlock module. Remote firmware will
|
||||
continue retrying without requiring that physical intervention or churning the
|
||||
USB device.
|
||||
|
||||
For an existing deployed tower using Preview 5, do not change the bootloader
|
||||
as part of this application chain. Its exact local recovery package is the
|
||||
[Preview 5 RAK4631 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4.1-preview.5/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4.1-preview.5_s140_6.1.1.zip).
|
||||
For a new RAK3401 installation or local recovery, use the
|
||||
[OTAFIX 2.4.3 RAK3401 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.11.0-OTAFIX2.4.3/wiscore_rak3401_bootloader-0.11.0-OTAFIX2.4.3_s140_6.1.1.zip)
|
||||
from the
|
||||
[OTAFIX 2.4.3 release](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.3).
|
||||
These are local DFU/SWD recovery assets, not mOTA chain steps.
|
||||
|
||||
For a local Serial DFU recovery with the repository helper:
|
||||
|
||||
```bash
|
||||
cd /path/to/meshfirmware
|
||||
MCFIRMWARE_NO_SUDO=1 ./mcfirmware.sh
|
||||
```
|
||||
|
||||
Select the serial entry whose USB identity is `WisCore RAK3401 Board`, then
|
||||
choose **Custom**, **nrf52**, and the exact RAK3401 `.zip`. Use **flash-update**
|
||||
to preserve InternalFS preferences and identity. Use **flash-wipe + flash** only
|
||||
when erasing application data is intentional. The helper follows the same USB
|
||||
device into bootloader mode, so do not substitute another `/dev/ttyACM*` merely
|
||||
because its number appears first.
|
||||
This document is now in [Research](research/rak3401_mota_chain.md).
|
||||
@@ -0,0 +1,190 @@
|
||||
# Companion contact caches
|
||||
|
||||
Full Companions on ESP32 and nRF52 without PSRAM, plus the high-contact RAK4631
|
||||
USB/BLE profiles, keep 16 outgoing paths
|
||||
and 16 shared secrets in RAM. Every contact remains in the contact table;
|
||||
selecting a different contact loads its saved path as needed. This recovers
|
||||
about 25.4 KiB of internal RAM in the 350-contact ESP32 qualification builds.
|
||||
|
||||
See the [qualification results](../test-results/companion_contact_cache_results.md) for the
|
||||
six-board RAM comparison, physical tests, and shared-secret timings.
|
||||
|
||||
PSRAM boards retain their existing complete contact table in external RAM.
|
||||
Most legacy USB/BLE/WiFi Companion profiles retain inline paths and secrets.
|
||||
The high-contact RAK4631 USB/BLE profiles also enable the same cache,
|
||||
keeping all 350 contacts while making room for the shared 256-frame offline/DM
|
||||
pool. The build flag `MESH_CONTACT_CACHE=0` or `1` overrides this policy for
|
||||
qualification; it is not a runtime feature switch.
|
||||
|
||||
## Paths and persistence
|
||||
|
||||
The 16 resident paths use least-recently-used replacement. Contact records
|
||||
keep small handles to their saved paths, including 1-, 2-, and 3-byte hop
|
||||
hashes. The existing 152-byte contact record and Companion app frame formats
|
||||
are unchanged. ESP32 uses `/contacts3`; nRF52 also supports its paged contact
|
||||
store and migration from `/contacts3`. An erase is not required.
|
||||
|
||||
A hot path needs no flash read. A cold path is read from its existing contact
|
||||
record. ESP32 reuses one open contact-file reader to avoid repeated SPIFFS
|
||||
metadata scans during synchronization and saving. It closes that reader
|
||||
before replacing the file or recovering a saved transaction.
|
||||
|
||||
Changing a copied contact preserves the old path for a pending app response
|
||||
or rollback. Dirty entries are saved before eviction. If storage cannot be
|
||||
read, written, or represented completely, the operation fails instead of
|
||||
silently losing a contact or sending along a different route. Reboot retries
|
||||
an incomplete load from the preserved files. The path checksum also rejects
|
||||
a cold path changed underneath a live handle; it does not add a new checksum
|
||||
to the legacy ESP32 on-disk format.
|
||||
|
||||
## Shared secrets and power
|
||||
|
||||
Both platforms reuse the 16 most recent secrets from RAM. Cache misses use a
|
||||
platform-specific policy:
|
||||
|
||||
| Platform | Miss behavior |
|
||||
| --- | --- |
|
||||
| ESP32 | Recalculate the secret without reading or writing a secret file. |
|
||||
| nRF52 | Try a saved LittleFS entry, then recalculate if it is unavailable. |
|
||||
|
||||
On the V4, a measured SPIFFS secret lookup took 132,427 microseconds while
|
||||
key exchange took 21,932 microseconds. ESP32 therefore uses recalculation on
|
||||
a RAM-cache miss. It avoids secret-file writes and reduces time awake for
|
||||
this measured workload. These are elapsed times, not measurements of energy
|
||||
in joules; other ESP32 hardware has not been physically benchmarked.
|
||||
|
||||
On a RAK3401, a LittleFS lookup took 7,812 microseconds versus 31,250
|
||||
microseconds for key exchange. The nRF52 backend therefore uses saved flash
|
||||
entries to avoid repeat key exchange when a usable entry exists, including
|
||||
after reboot. Saved entries match the full
|
||||
peer public key and a fingerprint of the local key pair, and have a record
|
||||
checksum. Importing a different private or public key invalidates old results.
|
||||
A corrupt or missing saved entry is recalculated. The native tests use the
|
||||
firmware's actual Ed25519 key-exchange library and verify agreement with the
|
||||
peer's independently calculated secret.
|
||||
|
||||
Derived secrets are expendable. Their storage keeps a reserve for contacts,
|
||||
preferences and filesystem metadata; a full filesystem leaves the calculated
|
||||
secret usable in RAM. nRF52 packs 56 entries into a 3,844-byte page to use its
|
||||
4 KiB flash blocks efficiently. Small filesystems may not have space to persist
|
||||
secrets for all 350 contacts. A miss without a usable saved entry still
|
||||
performs key exchange.
|
||||
|
||||
`MESH_CONTACT_SECRET_FLASH_CACHE=0` or `1` overrides the miss policy for
|
||||
qualification builds. The optional ESP32 flash backend groups eight entries
|
||||
per file. The normal ESP32 policy does not create or use those files.
|
||||
|
||||
## Inspect the cache
|
||||
|
||||
From the Full Companion's ASCII USB terminal or authenticated local CLI:
|
||||
|
||||
```text
|
||||
get contact.cache
|
||||
```
|
||||
|
||||
Cached builds report `paths=16 secrets=16`, `miss=calculate` or `miss=flash`,
|
||||
plus these boot-session counters:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `ram_hits` | Shared secret reused from RAM. |
|
||||
| `flash_hits` | Shared secret loaded from flash without recalculation. |
|
||||
| `calculations` | Shared-secret key exchanges performed. |
|
||||
| `save_skips` | Calculated secrets that could not be persisted, including storage reserve/backoff. |
|
||||
|
||||
Uncached builds report `paths=inline secrets=inline`. The normal `memory`
|
||||
command on supported ESP32 Full Companions shows live heap and queue capacity.
|
||||
|
||||
`get contact.cache.timing` reports the last successful flash lookup and last
|
||||
key calculation in microseconds. Zero means no such operation has completed
|
||||
since boot. These timings exclude secret writes and are useful for comparing
|
||||
the work avoided on a valid flash hit.
|
||||
|
||||
## Offline messages during mOTA
|
||||
|
||||
Contact caches are independent of the offline message queue. Builds using
|
||||
the shared mOTA queue retain **256 frames normally and 128 while mOTA owns
|
||||
its workspace**, then restore 256. No unread frames are discarded to start
|
||||
mOTA: synchronize the queue and retry if it exceeds the retained capacity.
|
||||
PSRAM-backed queues keep their existing capacity and allocation policy.
|
||||
|
||||
Each queue frame occupies 177 bytes. Retaining 32 instead of 128 would make
|
||||
another 16,992 bytes (16.6 KiB) available inside the shared storage. It would
|
||||
not increase free heap by itself: the normal 256-frame storage is statically
|
||||
reserved, and the current mOTA context already fits in the loaned half. More
|
||||
mOTA scratch data would have to share that space to obtain an additional RAM
|
||||
saving. These changes keep the existing 128-frame mOTA limit.
|
||||
|
||||
## Build the six capacity trials
|
||||
|
||||
The optional environments in `platformio.nimble.ini` combine NimBLE, these
|
||||
contact caches, and the shared mOTA queue at **350 contacts, 40 channels and
|
||||
256 normal offline frames**. They remain outside the ordinary release matrix.
|
||||
The ordinary six constrained release recipes still use their established
|
||||
150-contact limits pending wider hardware qualification.
|
||||
|
||||
Enable `platformio.nimble.ini` as described in the
|
||||
[NimBLE trial guide](../test-results/nimble_companion_trial.md), then build one environment at
|
||||
a time. For example:
|
||||
|
||||
```sh
|
||||
OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \
|
||||
Heltec_v3_ram_trial_companion_radio_full_nimble \
|
||||
--firmware-version v1.17.1.5-halo-keymind-cascade-cache-trial \
|
||||
--radio-preset usa-cascadia --profile cascade --standard --require-ota
|
||||
```
|
||||
|
||||
| Hardware | Trial environment |
|
||||
| --- | --- |
|
||||
| Heltec V3 | `Heltec_v3_ram_trial_companion_radio_full_nimble` |
|
||||
| Wireless Tracker | `Heltec_Wireless_Tracker_ram_trial_companion_radio_full_nimble` |
|
||||
| Tracker V2, FEM on | `heltec_tracker_v2_ram_trial_companion_radio_full_femon_nimble` |
|
||||
| Heltec CT62 | `Heltec_ct62_ram_trial_companion_radio_full_nimble` |
|
||||
| XIAO ESP32-C3 | `Xiao_C3_ram_trial_companion_radio_full_nimble` |
|
||||
| Generic ESP-NOW | `Generic_ESPNOW_ram_trial_companion_radio_full_nimble` |
|
||||
|
||||
The firmware RAM guard remains enabled. A successful linked budget is distinct
|
||||
from live free heap and physical Bluetooth/LoRa qualification on each board.
|
||||
|
||||
The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW
|
||||
trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and
|
||||
coredump stay at the same addresses as the ordinary `huge_app.csv` Full
|
||||
layout. Install the trial's merged image over USB first to install its
|
||||
partition table. Subsequent WiFi updates must fit the new slots. An
|
||||
application-only update from the ordinary single-slot Full layout cannot
|
||||
change that layout or enable WiFi OTA.
|
||||
|
||||
## Regression checks
|
||||
|
||||
```sh
|
||||
python3 -B test/test_contact_cache.py
|
||||
python3 -B test/test_companion_contact_persistence_contract.py
|
||||
python3 -B test/test_companion_contact_stream_contract.py
|
||||
python3 -B test/test_nrf52_extrafs_contract.py
|
||||
python3 -B test/test_shared_mota_queue.py
|
||||
python3 -B test/test_t096_full_memory.py
|
||||
python3 -B test/test_firmware_ram.py
|
||||
```
|
||||
|
||||
The cache tests compile the production path/secret cache code and extracted
|
||||
production persistence, routing and request functions with filesystem/radio
|
||||
adapters. They exercise 350 routes, eviction, snapshot rollback, page
|
||||
migration, identity changes, read/write/rename faults, handle exhaustion,
|
||||
packet release on a failed path read, and recovery after a simulated reset
|
||||
between file renames. They do not simulate a physical flash power cut.
|
||||
|
||||
`tools/hil/contact_cache_serial_stress.py` checks all 350 paths over USB,
|
||||
replaces them with a different generation, and optionally exercises secret
|
||||
eviction. Use an otherwise empty test node and exclusive access to its
|
||||
Companion data port. `--populate` writes test contacts; `--secrets` transmits
|
||||
23 directed LoRa datagrams to synthetic peers. For an ESP32 cache trial:
|
||||
|
||||
```sh
|
||||
python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
|
||||
--populate --generation 1 --secrets --expect-calculate --reboot
|
||||
python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
|
||||
--generation 1 --secrets --expect-calculate
|
||||
```
|
||||
|
||||
Use the actual device path. Omit `--expect-calculate` for the nRF52 flash
|
||||
policy. The second invocation verifies the persisted paths after reboot.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Classic ESP32 image memory budget
|
||||
|
||||
Classic ESP32 has 320 KiB of internal DRAM, but at most 160 KiB can hold
|
||||
statically allocated data. The remaining DRAM is available only through the
|
||||
runtime heap. Bluetooth, tracing, and SDK reservations further constrain the
|
||||
usable region. See Espressif's [memory types documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/memory-types.html#dram-data-ram).
|
||||
|
||||
An image can therefore approach its static limit while PlatformIO reports
|
||||
less than 40% of the 320 KiB total in use. PSRAM does not expand the region used
|
||||
by ordinary internal `.data` and `.bss` allocations.
|
||||
|
||||
## Build enforcement
|
||||
|
||||
Every classic ESP32 environment inheriting `esp32_base` runs
|
||||
`scripts/check_esp32_dram.py`. The release builder also adds the script when
|
||||
an ESP32 profile replaces the inherited extra scripts. The check runs before
|
||||
image generation, merging, and uploading, including cached `nobuild` uploads.
|
||||
It requires the ELF and linker map to be present for those cached operations.
|
||||
The check script must load after `merge-bin.py` so the merge target retains its
|
||||
pre-action. A merge also requires the bootloader/partition flash layout;
|
||||
`nobuild` cannot create a bootable merged image when that layout is absent.
|
||||
|
||||
The check reads `dram0_0_seg` from the final linker map. SDK reservations are
|
||||
already reflected in that region; Bluetooth's reservation is not subtracted
|
||||
a second time. The occupied span runs from the region origin to `_heap_start`,
|
||||
including `.data`, `.bss`, `.noinit`, and alignment gaps. The allowed region is
|
||||
the smaller of the linker's region and 160 KiB.
|
||||
|
||||
Builds must leave at least **8 KiB free within that static region**. This is
|
||||
an additional project margin against near-full images, not an Espressif
|
||||
hardware limit or a prediction of free heap. A profile may require a larger
|
||||
margin with `custom_esp32_static_dram_reserve` in bytes; values below 8192 are
|
||||
rejected. Missing or unrecognized map boundaries also fail the check.
|
||||
|
||||
For the Heltec V2 build used to investigate the repeater report:
|
||||
|
||||
| Shared repeater defaults | Static span | Static region | Remaining | Result |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Previous 255 scope slots | 124,020 B | 124,580 B | 560 B | Rejected |
|
||||
| Tuned 31 scope slots | 108,308 B | 124,580 B | 16,272 B | Passed |
|
||||
|
||||
These measurements remove the board's existing scope-capacity override to
|
||||
exercise the shared defaults. The normal Heltec V2 profile already used 31
|
||||
scope slots. Exact sizes vary with build flags and SDK versions.
|
||||
|
||||
To inspect an existing classic ESP32 build:
|
||||
|
||||
```sh
|
||||
python3 scripts/check_esp32_dram.py .pio/build/Heltec_v2_repeater/firmware.map
|
||||
python3 test/test_esp32_dram.py
|
||||
```
|
||||
|
||||
ESP32-S2, S3, and C-series chips have different memory maps and do not inherit
|
||||
this classic ESP32 check.
|
||||
|
||||
## Boot versus running memory
|
||||
|
||||
The build gate checks static placement needed to load and start the image.
|
||||
It never counts heap-only RAM toward that static budget. Allocations made with
|
||||
`new` or `malloc` in global constructors consume heap and are not included in
|
||||
the static span. Moving a table there changes its placement, not total RAM
|
||||
consumption.
|
||||
|
||||
Successful build checks do not prove that every runtime configuration will
|
||||
boot. Hardware validation must still cover startup allocations and the enabled
|
||||
Wi-Fi/Bluetooth services, recording minimum free internal heap and the largest
|
||||
free internal block. Available heap changes during initialization; for example,
|
||||
the Arduino 2.x repeater releases unused Bluetooth controller RAM in
|
||||
`initArduino()`, after global constructors have run.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Firmware memory checks
|
||||
|
||||
Every firmware environment runs `scripts/check_firmware_ram.py` against its
|
||||
linked ELF before producing or uploading an image. `build.sh`, including
|
||||
option 3, also requires a passing report before collecting release files.
|
||||
Native host tests do not use a microcontroller RAM budget.
|
||||
|
||||
The check reserves room for enabled runtime allocations as well as static
|
||||
data. A firmware image fitting its board's reported RAM total is insufficient:
|
||||
the display, packet pool, USB, Bluetooth workers and WiFi can allocate after
|
||||
startup. The T096 Full 1.17.1.5 report exposed this distinction.
|
||||
|
||||
## What is counted
|
||||
|
||||
| Platform | Source of available runtime RAM |
|
||||
| --- | --- |
|
||||
| nRF52 | Actual `__HeapBase` and `__HeapLimit`; excludes SoftDevice, retained state, ISR stack and the dedicated 64 KiB mOTA arena where present |
|
||||
| ESP32, S3, C3, C6 | Linked ESP-IDF memory-region, capability and reservation tables; only internal, byte-addressable heap counts |
|
||||
| RP2040/RP2350 | `__end__` to `__HeapLimit`, according to the selected linker |
|
||||
| STM32 | `_end` to `_estack`, minus `_Min_Stack_Size` |
|
||||
|
||||
ESP32 PSRAM, instruction-only RAM and RTC RAM never increase the internal
|
||||
budget. On chips other than classic ESP32, the late-reclaimed ROM stack region
|
||||
is excluded because its silicon-specific reservations are only known at boot.
|
||||
Classic ESP32 additionally retains its existing 8 KiB **static DRAM** check.
|
||||
|
||||
The policy adds allowances for task stacks, radio packet pools, screen objects
|
||||
and pixel buffers, filesystem/sensor allocations, enabled wireless stacks,
|
||||
MQTT connections, OTA scratch and transient allocations. A 160x80 ST7735
|
||||
framebuffer needs 25,602 bytes; an OLED allowance is 4 KiB. nRF52 Full with that
|
||||
color framebuffer must have at least 72 KiB available before startup allocations.
|
||||
Headless and OLED devices use their own smaller totals. The JSON lists each
|
||||
component and checks the largest available region against the largest planned
|
||||
single allocation.
|
||||
|
||||
The [small-screen message layout](../test-results/v4_pixel5_font_trial.md) retains complete
|
||||
160-byte messages. Its expanded preview records add 2,816 bytes to the
|
||||
startup allowance and increase the contiguous history allocation budget.
|
||||
The V4 can allocate that history in PSRAM; the guard conservatively reserves
|
||||
internal capacity so that PSRAM availability cannot hide a RAM shortage. A
|
||||
target may replace the generic UI allowance only with
|
||||
`MESH_COMPANION_SCREEN_STARTUP_BYTES`; a source-side assertion then covers its
|
||||
actual startup screens plus ESP32 allocator overhead.
|
||||
|
||||
These are engineering allowances for supported configurations, not measured
|
||||
free heap after boot or a guarantee against every future allocation failure.
|
||||
Unknown platforms, unknown display drivers and missing linker metadata fail
|
||||
closed. `MESH_MIN_RUNTIME_HEAP` can raise a profile's requirement; it cannot
|
||||
lower the calculated requirement. Add an allocation allowance when adding a
|
||||
display, transport or other substantial feature.
|
||||
|
||||
Wireless Paper Full keeps 350 contacts and 256 offline frames by lending the
|
||||
upper 128 queue slots to its mOTA workspace during a session. An idle WiFi
|
||||
mOTA listener leaves all 256 slots available. More than 128 unread frames
|
||||
refuses the loan; sync messages with an app first. USB/TCP source detach or
|
||||
disconnect returns all 256 slots and releases the ESP32 proof/leaf scratch
|
||||
buffers. The display and simultaneous USB, Bluetooth and WiFi remain enabled.
|
||||
|
||||
## Release evidence and regression tests
|
||||
|
||||
Full Companions without PSRAM also use
|
||||
[16-entry path and shared-secret caches](companion_contact_cache.md).
|
||||
The optional NimBLE capacity trials retain the same RAM guards at 350
|
||||
contacts and 256 normal offline frames.
|
||||
|
||||
Each newly built firmware has a matching `.memory.json` report. It records
|
||||
the linked ELF SHA-256, available internal RAM, required RAM, largest region,
|
||||
and SHA-256 hashes for the actual firmware files and capability manifest.
|
||||
Packaging and resumed builds reject absent reports, failures, stale ELFs,
|
||||
missing files and changed firmware. Do not reuse a report for another build.
|
||||
|
||||
Run PlatformIO commands sequentially in this checkout:
|
||||
|
||||
```sh
|
||||
python3 -B test/test_firmware_ram.py
|
||||
python3 -B test/test_t096_full_memory.py
|
||||
python3 -B test/test_nrf52_ble_startup.py
|
||||
python3 -B test/test_shared_mota_queue.py
|
||||
python3 -B test/test_cascade_release_package.py
|
||||
pio test -e native -f test_ota
|
||||
```
|
||||
|
||||
Tests cover all resolved firmware environments' hooks, real ELF parsing,
|
||||
allocator table formats, excluded memory, allocation failure, package/report
|
||||
binding and the published T096 failing budget. Shared mOTA tests exercise
|
||||
complete transfers, queue wraparound, unread-message order, source ownership,
|
||||
stop/disconnect and repeated reuse. Bluetooth tests inject task and service
|
||||
startup failures. The manual staging buffer also has allocation-failure and
|
||||
repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source
|
||||
framing through complete transfers, idle polling, queue-full refusal, network
|
||||
loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers.
|
||||
|
||||
For older releases without saved ELFs, an audit can compare their ESP allocator
|
||||
tables against a matching pinned SDK ELF and read reservations from the
|
||||
**published application itself** using `scripts/audit_esp32_image_ram.py`.
|
||||
Unrecognized layouts require another matching reference or a historical rebuild.
|
||||
An audit must identify original-log/linker calculations separately from new
|
||||
ELF checks and verify the published firmware hashes.
|
||||
|
||||
Physical validation remains necessary: boot with and without USB, pair and
|
||||
exchange Bluetooth messages, visit every screen, wake with the button, enable
|
||||
logging/MQTT, transfer mOTA, and monitor heap during a sustained workload.
|
||||
See [memory monitoring](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MEMORY_MONITORING.md)
|
||||
for runtime diagnostics.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Research
|
||||
|
||||
Engineering investigations, design notes, diagnostic methods, and historical update-chain studies live here. For measured outcomes, see [Test Results](../test-results/index.md); for current installation instructions, use the [documentation homepage](../index.md).
|
||||
|
||||
## Memory and storage
|
||||
|
||||
- [Classic ESP32 image memory budget](esp32_memory_budget.md)
|
||||
- [Firmware memory checks](firmware_memory_budget.md)
|
||||
- [Companion contact caches](companion_contact_cache.md)
|
||||
|
||||
## Diagnostics and update-chain studies
|
||||
|
||||
- [nRF52 Companion Bluetooth diagnostic firmware](nrf52_bluetooth_debug.md)
|
||||
- [RAK3401 compact LoRa update chain](rak3401_mota_chain.md)
|
||||
- [RAK3401 1.16.07 to 1.17.1.7 with legacy bootloader](rak3401_1.17.1.7_legacy24.md)
|
||||
@@ -0,0 +1,96 @@
|
||||
# nRF52 Companion Bluetooth diagnostic firmware
|
||||
|
||||
The T1000-E diagnostic image is Full Companion firmware with the normal radio,
|
||||
sensor, pairing and recovery settings. Diagnostics are enabled with
|
||||
MESH_NRF52_BLE_TRACE=1. Ordinary builds compile them out. Diagnostic builds
|
||||
reserve about 6.3 KB of RAM for a 256-event ring and counters.
|
||||
|
||||
The trace records connection timing, security results, UART notification
|
||||
subscriptions, app receive/transmit lengths, queue overflow, local disconnect
|
||||
requests and each recovery trigger. It brackets Bluefruit's real event
|
||||
dispatcher so a handler that stalls can be distinguished from an event that
|
||||
never reached the framework. Maximum dispatch duration and Companion loop
|
||||
gaps are also reported. Callbacks write bounded RAM records without serial
|
||||
printing or allocation. PINs, keys, peer addresses and message contents are
|
||||
not stored.
|
||||
|
||||
## Capture a phone failure
|
||||
|
||||
1. Install the diagnostic UF2 for your exact board, or use its application DFU
|
||||
ZIP. This does not require replacing the bootloader.
|
||||
2. Connect with the phone normally and reproduce the failure. Keep the node
|
||||
powered. Do not forget the Bluetooth device, reboot, or factory reset
|
||||
before collecting the trace.
|
||||
3. Connect the node's USB cable to a computer and close other serial clients.
|
||||
Install Python and pyserial if needed.
|
||||
4. Export the diagnostic report:
|
||||
|
||||
~~~sh
|
||||
python3 scripts/capture_nrf52_ble_debug.py --port /dev/ttyACM0 --output bluetooth-report.json
|
||||
~~~
|
||||
|
||||
On Windows use the correct COM port instead of /dev/ttyACM0. The exporter
|
||||
enters the Full Companion USB terminal at 115200 baud, reads diagnostics,
|
||||
then returns it to binary mode. It does not reset or change settings.
|
||||
5. Send the JSON report with the approximate failure time and what the phone
|
||||
displayed. Export soon after failure because old events roll out of the
|
||||
ring. Reboot or loss of power clears the RAM history.
|
||||
|
||||
If the Pi's attached T1000-E is used, its serial identity is
|
||||
34A9141999729D5D. Identify the physical board before flashing; an upstream
|
||||
application may have the misleading USB product name T1000-E-BOOT.
|
||||
|
||||
## Manual USB commands
|
||||
|
||||
- get bluetooth.trace: format, latest sequence, capacity and dropped writes.
|
||||
- get bluetooth.trace.state: advertising, connection, security, UART
|
||||
notification state, MTU, app traffic and queues.
|
||||
- get bluetooth.trace.stats: cumulative connects, disconnects, secured
|
||||
sessions, RX/TX operations and last disconnect reason/time.
|
||||
- get bluetooth.trace.timing: uptime, maximum loop gap, maximum stack
|
||||
dispatch time and any currently executing event.
|
||||
- get bluetooth.trace N: one record by sequence.
|
||||
|
||||
The debug commands are restricted to local Companion transport requests and
|
||||
are not exposed through remote mesh management. Replies and raw records are
|
||||
decoded by the exporter. Sequence gaps and overwritten records are reported.
|
||||
Diagnostic contention is counted and dropped rather than blocking BLE.
|
||||
|
||||
## Reading the result
|
||||
|
||||
| Evidence | Stage identified |
|
||||
| --- | --- |
|
||||
| No stack connection event; advertising remains active | Connection has not reached the application's Bluetooth event layer. |
|
||||
| Connection but security error or timeout | Pairing or encryption setup. |
|
||||
| Secured link but UART notifications disabled | Notification subscription or restoration. |
|
||||
| Secured link with no app RX; secured_no_app_watchdog | App never opened its Companion UART session; the 15-second recovery fired. |
|
||||
| App RX followed by blocked/partial TX | Reply delivery; the report identifies the queue/watchdog result. |
|
||||
| Dispatch entry with no matching exit and increasing age | Software stack handler may be stuck; correlate with a later snapshot. |
|
||||
|
||||
Reason 0x08 is connection timeout, 0x16 is local termination and 0x3e
|
||||
is connection establishment failure. Do not confuse decimal 22 (0x16)
|
||||
with hexadecimal 0x22 (link-layer response timeout).
|
||||
|
||||
The report can identify the failing software stage, but cannot guarantee the
|
||||
physical cause. No connection event alone cannot separate a central
|
||||
controller problem, RF interference and a peripheral controller problem.
|
||||
Dispatch timing starts after the SoftDevice delivers an event; it does not
|
||||
measure radio interrupt latency. A bench failure is not proof of the same
|
||||
failure on an iPhone. Use the diagnostic image on the affected node.
|
||||
|
||||
## Build
|
||||
|
||||
Run one PlatformIO command at a time in this checkout:
|
||||
|
||||
~~~sh
|
||||
PLATFORMIO_BUILD_FLAGS='-DMESH_NRF52_BLE_TRACE=1 -DBLE_DEBUG_LOGGING=0 -g3' \
|
||||
OUTPUT_DIR=out/t1000e-ble-debug \
|
||||
bash build.sh build-firmware t1000e_companion_radio_full \
|
||||
--firmware-version v1.18.0.1-halo-keymind-cascade-dev-ble-debug --full
|
||||
~~~
|
||||
|
||||
The Bluefruit dispatch hooks are applied to private build copies of framework
|
||||
sources. The shared PlatformIO framework cache is not modified. Retain the
|
||||
ELF alongside the UF2/ZIP for future SWD inspection if hardware access becomes
|
||||
available. Do not halt the CPU during a Bluetooth connection: that disrupts
|
||||
SoftDevice radio timing and invalidates the test.
|
||||
@@ -0,0 +1,272 @@
|
||||
# RAK3401: 1.16.07 to 1.17.1.7 without a bootloader update
|
||||
|
||||
Qualified on MercerWoodMesh on 27 September 2026: all 15 application updates
|
||||
passed over LoRa, from the exact supplied 1.16.07 image, without a bootloader
|
||||
update. Use `RAK3401-1.16.07-to-1.17.1.7-legacy24-mOTA.zip` and verify it against
|
||||
the accompanying `.zip.sha256` file before extracting. Follow the exact
|
||||
before/after hashes in its `CHAIN.csv`; this is not a single-step stock update.
|
||||
|
||||
## Exact compatibility boundary
|
||||
|
||||
This is an application-only migration for target `2FA509C1`, hardware
|
||||
`RAK_3401_repeater_lora_ota_no_external_sensors`. It is not a generic RAK4631
|
||||
package. The installed application must have body hash `71F4026CBE4B8B74`
|
||||
(`1.16.07`, full image SHA-256
|
||||
`4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb`).
|
||||
|
||||
The bootloader remains `0.9.2-OTAFIX2.4`, with ABI 2 and codec mask `0x4`.
|
||||
Its application staging ceiling is `0xD4000`. Every package is a codec-2
|
||||
CRLE in-place delta. DEFLATE is used only for transport by compatible
|
||||
intermediate applications; the old bootloader still applies CRLE.
|
||||
|
||||
The destination is built from source commit
|
||||
`2d03e098cf75d58adcf35fe3d4dbbeaf0af114e1` with the explicit
|
||||
`MESHCORE_NRF52_LEGACY_FLASH_OTA=1` build selection. This preserves the
|
||||
ordinary S140 linker and disables hybrid flash/RAM staging and bootloader
|
||||
self-update. It is deliberately different from the stock hybrid 1.17.1.7
|
||||
release. The endpoint has:
|
||||
|
||||
- Version: `v1.17.1.7-halo-keymind-cascade-legacy24-2d03e098`
|
||||
- Image: 521,496 bytes
|
||||
- Body hash: `D437D1F17AF5B9F3`
|
||||
- SHA-256: `c9e63a94f58f881b1853fc5d9d348cd75a6cdd454c5b572341d652a6141e3157`
|
||||
|
||||
Do not replace this endpoint with the stock ZIP. Do not send a bootloader or
|
||||
SoftDevice update. The ZIP/UF2 files used to construct and qualify the chain
|
||||
are not themselves LoRa update containers.
|
||||
|
||||
Future application updates for this node must also be qualified against the
|
||||
unchanged OTAFIX2.4 contract. Keep the legacy build selection; do not assume
|
||||
that a later stock release is an interchangeable target for this chain.
|
||||
|
||||
## What "smallest" means
|
||||
|
||||
The search compares an explicit, hash-pinned set of application images and
|
||||
every allowed page-aligned in-place workspace for each increasing-version
|
||||
transition. The starting bootstrap is fixed: its 89,844-byte package is the
|
||||
only admitted first edge. Synthetic bridge version numbers are real compiled
|
||||
versions below 1.17.1.7; their binaries are not relabeled after compilation.
|
||||
|
||||
The `bytes` objective minimizes ideal serialized MeshCore traffic first,
|
||||
then install count. The `packets` objective minimizes ideal originated
|
||||
packets first, then serialized traffic. The older `transport` objective
|
||||
minimizes install count before traffic and is not equivalent to either.
|
||||
|
||||
Accounting includes manifests, block requests, proof requests/responses,
|
||||
DATA fragments, and MeshCore framing. It uses the actual pinned encoder for
|
||||
compressed blocks. It excludes discovery, retries, admin/control commands,
|
||||
other mesh traffic, flood fan-out, and LoRa preamble/coding overhead. A fixed linear relay path
|
||||
can be accounted for separately. These are finite-search minima, not a proof
|
||||
that no possible custom firmware or protocol could ever use fewer bytes.
|
||||
The ideal compressed counts also assume negotiation remains in that mode.
|
||||
An older receiver can fall back to raw DATA after stalls; the physical-test
|
||||
transmission totals include that extra traffic.
|
||||
|
||||
Best lossless byte count is not a guarantee of the fastest real radio link.
|
||||
Use the fastest preset that remains reliable over the actual route. SF5 /
|
||||
500 kHz is a direct bench measurement, not a recommendation to switch an
|
||||
unreachable field node or unconfigured relay chain to that preset.
|
||||
|
||||
## Selected route
|
||||
|
||||
The completed search covers 52 images and 47,920 flash geometries. The
|
||||
packet-first route has 15 packages, totaling 2,340,160 container bytes. Its
|
||||
ideal direct-link traffic is **12,621 packets / 1,808,961 serialized MeshCore
|
||||
bytes**. These totals include traffic in both directions, not just DATA.
|
||||
Fifteen is also the fewest installs among the searched candidates.
|
||||
|
||||
The byte-first route uses the same sequence of application images, with
|
||||
four different patch workspaces: 12,625 packets / 1,808,787 bytes. Saving
|
||||
174 bytes costs four packets. The delivered packages use the packet-first
|
||||
choice; do not substitute packages from the comparison route.
|
||||
|
||||
| Step | Resulting version | Container bytes | Ideal packets | Ideal MeshCore bytes |
|
||||
| ---: | --- | ---: | ---: | ---: |
|
||||
| 1 | 1.16.7.9 | 89,844 | 878 | 101,203 |
|
||||
| 2 | 1.16.7.10 | 62,838 | 551 | 70,031 |
|
||||
| 3 | 1.16.8.7 | 106,125 | 496 | 70,134 |
|
||||
| 4 | 1.16.9.105 | 149,542 | 728 | 104,992 |
|
||||
| 5 | 1.16.9.110 | 112,540 | 489 | 67,887 |
|
||||
| 6 | 1.16.9.111 | 71,070 | 268 | 35,453 |
|
||||
| 7 | 1.16.9.116 | 138,545 | 692 | 101,308 |
|
||||
| 8 | 1.16.9.122 | 177,536 | 995 | 148,671 |
|
||||
| 9 | 1.16.10.1 | 258,398 | 1,477 | 223,042 |
|
||||
| 10 | 1.16.11.0 | 245,426 | 1,450 | 219,953 |
|
||||
| 11 | 1.16.12.1 | 217,608 | 1,167 | 172,794 |
|
||||
| 12 | 1.16.13.1 | 199,395 | 1,024 | 150,005 |
|
||||
| 13 | 1.16.14.0 | 199,527 | 989 | 143,160 |
|
||||
| 14 | 1.16.15.0 | 139,188 | 618 | 86,721 |
|
||||
| 15 | 1.17.1.7 | 172,578 | 799 | 113,607 |
|
||||
|
||||
Some bridge versions also exist as different binaries. `CHAIN.csv`'s exact
|
||||
body hashes and package checksums, not this version-only table, determine
|
||||
which package is valid at each step. The first two transfers use legacy
|
||||
1 KiB blocks; subsequent transfers use negotiated 2 KiB blocks with
|
||||
transport DEFLATE and the unchanged CRLE bootloader format.
|
||||
The second transfer also omits explicit proof requests: that historical
|
||||
receiver waits for the current sender's proactive Merkle proofs. The original
|
||||
1.16.07 receiver does not have this optimization.
|
||||
|
||||
## Measured MercerWoodMesh replay
|
||||
|
||||
All 15 selected packages passed in order on the Pi's debug-wired RAK3401,
|
||||
starting with the exact 1.16.07 image and OTAFIX2.4. Every application payload
|
||||
crossed LoRa from the Heltec T096. There were no debug writes, application
|
||||
substitutions, failed installs, or resumed downloads between the 15 steps.
|
||||
|
||||
The direct bench used 909.950 MHz, BW 500 kHz, SF5, CR5, continuous receive,
|
||||
and a 128-symbol source preamble. Observations for both radios totaled:
|
||||
|
||||
- 14,069 transmitted frames, including discovery and retries.
|
||||
- 22 minutes 31 seconds of measured download time.
|
||||
- 47 minutes 35 seconds summed active test-step time, including configuration,
|
||||
checks, installs, and reboots; deliberate gaps between runs are excluded.
|
||||
|
||||
These are bench measurements, not a field-link completion-time promise. The
|
||||
control commands were sent over USB; a remote field console adds LoRa control
|
||||
traffic. `BENCHMARK.csv` separates each step's ideal cost from its observed
|
||||
frame count and timing. Actual serialized radio bytes were not measured.
|
||||
|
||||
Independent debug-port readback matched the complete final application,
|
||||
unchanged bootloader, and unchanged MBR/SoftDevice. UICR matched the normal
|
||||
post-startup baseline throughout this replay. The initial pre-first-boot
|
||||
UICR dump differs because normal Nordic/RAK startup configures the reset pins
|
||||
and 3.3 V regulator; this is documented in the evidence, not a bootloader
|
||||
upgrade.
|
||||
|
||||
After a fresh reboot, the watchdog reported `on`. Both radios returned to
|
||||
910.525 MHz / BW 62.5 kHz / SF7 / CR5, and a nonce-matched normal-channel trace
|
||||
through the exact target key prefix completed in 423 ms. The inaccessible
|
||||
field node has not been touched. The lab RAK remains on this repeater image;
|
||||
the Pi's `mctomqtt.service` remains stopped because it previously owned that
|
||||
serial port. Radios other than the RAK3401 and sending Heltec T096 were left
|
||||
untouched.
|
||||
|
||||
## Field preparation
|
||||
|
||||
All destination commands below can be sent through the authenticated remote
|
||||
repeater console; USB/Bluetooth at the destination is not required. Keep a
|
||||
separate control connection available while the sender owns its companion's
|
||||
serial port. A local
|
||||
Full Companion radio attached to the sender runs the bundled `motatool`
|
||||
seeder. Resolve ports by device identity, not a remembered `ttyACM` number.
|
||||
|
||||
The qualified source is the Pi's Heltec T096, USB serial
|
||||
`651F8E496197F882`, running Full Companion
|
||||
`v1.17.1.8-halo-keymind-cascade-dev-d4b3bb56`. It supports the extended
|
||||
2 KiB transport and host-backed DEFLATE. The destination is RAK3401 USB
|
||||
serial `0B81C9C68D8D01B4`. Older companion firmware or an older `motatool`
|
||||
is not an interchangeable sender for this bundle.
|
||||
|
||||
Before any change, record `ver`, `ota self`, `ota status`,
|
||||
`get bootloader.ver`, `get system.watchdog`, `get radio.rxps`,
|
||||
`get powersaving`, `get rxdelay`, `get af`, and `ota config`. Match the exact
|
||||
starting body, hardware, bootloader, and signing-key policy. An unsigned
|
||||
bundle is not suitable for a node requiring signed packages.
|
||||
Choose a stable-power maintenance window long enough for all 15 installs;
|
||||
do not begin an install while the destination is at risk of losing power.
|
||||
|
||||
Disable the destination system watchdog with `set system.watchdog off`.
|
||||
Allow its one-time hardware reset and wait at least 90 seconds; reconnect
|
||||
and prove that it reports `off`. A quick software reboot is not a substitute.
|
||||
|
||||
Use continuous receive on the destination and sender throughout the bridge
|
||||
chain (`set radio.rxps off`). Record and disable a second scanning radio
|
||||
profile on the sender if it would take the radio away from the OTA channel.
|
||||
Record `rxdelay`, `af`, power-saving, reach, and relay settings before changing
|
||||
them. Use `hops 0` only when there are no intermediate relays. Obey applicable
|
||||
airtime/duty restrictions and restore all saved settings when finished.
|
||||
|
||||
The direct bench also uses `set rxdelay 0` and `set af 0` on both radios,
|
||||
`ota speed 1` on the sender, and `set powersaving off` where supported.
|
||||
The earliest applications do not recognize the last command; continuous
|
||||
receive is still selected through `set radio.rxps off`. The source has
|
||||
`set radio2 off` throughout the transfer. Wait for configuration replies to
|
||||
finish before handing its serial connection to the seeder.
|
||||
|
||||
For the qualified direct bench configuration only, the destination uses
|
||||
`tempradio 909.950,500,5,5,120`; the tested current Full Companion accepts
|
||||
`tempradio 909.950,500,5,5,120,128` for an explicit 128-symbol preamble.
|
||||
Historical destination parsers do not accept the sixth argument. The sender
|
||||
and destination must be on the same live radio preset. Do not assume the
|
||||
temporary preset survives a bridge reboot.
|
||||
|
||||
The newer historical receivers performed poorly with a 16-symbol source
|
||||
preamble in this test. Use the qualified longer setting. Also avoid frequent
|
||||
`ota status` or `ota stats` polling: these versions rehash the complete running
|
||||
application while answering, which can interrupt receive processing. The lab
|
||||
runner observes the sender's local log and checks the receiver near completion
|
||||
or at a 60-second interval. Set a field maintenance lease appropriate to the
|
||||
actual preset and route; the 120-minute value above is a bench setting.
|
||||
|
||||
## Applying a qualified bundle
|
||||
|
||||
Verify the outer archive hash and every entry in `SHA256SUMS.txt`. Verify
|
||||
each `.mota` using the included, hash-pinned `motatool`. `CHAIN.csv` is the
|
||||
authoritative ordered list of packages, manifest IDs, and before/after hashes.
|
||||
On the Pi, use `tools/motatool-linux-aarch64` (make it executable after
|
||||
extracting if necessary). The x86-64 binary is for Linux/WSL, not native
|
||||
Windows. The provenance images, tools, and evidence are not sent over LoRa;
|
||||
only the selected `.mota` in the current step's serving directory is sent.
|
||||
|
||||
For every row, including after an interrupted run:
|
||||
|
||||
1. Prove that `ota self` matches that row's exact before hash and that the
|
||||
bootloader reports `apply OK`. Do not select a row by version alone.
|
||||
2. Reconcile `ota status`. Resume only that row's known partial MID. If it is
|
||||
already ready, do not redownload it. Stop on an unrelated or ambiguous
|
||||
session. Never send a blind `ota cancel`.
|
||||
3. Reapply the agreed radio/receive guardrails after reboot. Start the sender
|
||||
with only this row's package in its serving directory:
|
||||
`motatool serve --dir STEP_DIRECTORY --serial COMPANION_PORT --baud 115200 -v`.
|
||||
4. Run `ota ls` until the expected version is advertised. The old listing
|
||||
shows a version, not a MID. Use `ota pull EXPECTED_MID flash` and poll
|
||||
`ota status` at a restrained interval.
|
||||
5. Require `ready to install` with the exact MID. Stop the seeder gracefully
|
||||
with Ctrl-C. Recheck watchdog `off`, then send `ota install`.
|
||||
6. Allow up to five minutes for the application to return. Require the exact
|
||||
after hash, expected version, and `bootloader: apply OK` before continuing.
|
||||
Stop if any check fails; do not try the next package against another base.
|
||||
|
||||
A retained previous container may be re-discovered after boot. If the exact
|
||||
previous MID is verifying, wait for it to become ready or idle. Only after
|
||||
re-proving the current running hash may that attached, completed previous
|
||||
MID be cancelled. An idle `ota cancel` does not prove that flash was erased.
|
||||
|
||||
After the final hash is proved, restore the destination, sender, and relays
|
||||
to their recorded normal settings and confirm normal-channel reachability.
|
||||
Re-enable the system watchdog and require it to report `on`. The current
|
||||
endpoint supports `normalradio`; coordinate channel restoration so that the
|
||||
remote node does not become unreachable during the handoff.
|
||||
The endpoint requires a temporary radio channel for OTA operations, so a
|
||||
normal-channel `ota ls` is not a reachability test. Use an ordinary mesh trace
|
||||
or authenticated remote command after restoring normal radio.
|
||||
|
||||
The existing `rak3401_mota_chain.py` pins older release archives. It must not
|
||||
be pointed at this new bundle or have its safety checks bypassed.
|
||||
|
||||
## Evidence and reproducibility
|
||||
|
||||
`build_rak3401_legacy24_bundle.py` checks every base/target hash, hardware
|
||||
identity, increasing version, block capability, flash workspace, and staging
|
||||
margin. It reconstructs every target with both zero- and erased-flash-filled
|
||||
scratch space, and applies every package with the actual OTAFIX2.4 release
|
||||
implementation compiled as a flash simulator. It remeasures transport with
|
||||
the pinned encoder. Its output remains explicitly offline-only until a
|
||||
separate physical qualification record is attached.
|
||||
|
||||
`ACCOUNTING-CORRECTION.json` records the proactive-proof accounting correction.
|
||||
It changed the ideal totals only; all package checksums and the selected
|
||||
hardware-test sequence remained unchanged.
|
||||
|
||||
The archive includes the 52 frozen search images, all 47,920 measured
|
||||
geometries, encoder binaries, per-step simulator logs, and raw hardware-test
|
||||
records. Only the selected `.mota` containers are served over LoRa. Lab
|
||||
scripts in `evidence/` are tied to the Pi's USB/debug fixture and are not a
|
||||
remote field deployment controller.
|
||||
|
||||
Regression checks: 534 LoRa OTA tests ran successfully with one opt-in
|
||||
dependency-install test skipped; all 11 linker-selection tests passed.
|
||||
|
||||
All PlatformIO builds must be serialized, including builds in separate
|
||||
checkout directories that share the same PlatformIO package installation.
|
||||
@@ -0,0 +1,704 @@
|
||||
# RAK3401 1W repeater compact LoRa update chain
|
||||
|
||||
> Status: **published prerelease for controlled, recoverable lab use.** On
|
||||
> 29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte
|
||||
> identical to the corresponding transitions in the physically passed
|
||||
> `fd98bc90` chain. The replacement step 10 installs the current `3f6eddd5`
|
||||
> endpoint. That replacement package and the hardened current runner have
|
||||
> passed offline reconstruction plus the exact deployed bootloader simulator, but
|
||||
> have not had a new physical end-to-end run. Use the lab gate and keep local
|
||||
> DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates
|
||||
> remain planning data, not physical qualification.
|
||||
|
||||
The prerelease is
|
||||
[`rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5`](https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5).
|
||||
Use this asset:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
```
|
||||
|
||||
- prerelease ZIP SHA-256: `135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4`
|
||||
- endpoint image SHA-256: `cd1e9b819f9e918f9f2ceaf37b8a74bf242e2be78f319af2cf02a42d7729d0f4`
|
||||
- endpoint EndF body hash: `9BD7CF682EE065AE`
|
||||
|
||||
`--verify-only` remains the safe default. Live preflight and installation of
|
||||
this prerelease require `--accept-test-candidate`. That switch bypasses only
|
||||
the prerelease-status gate; it does not bypass the archive, identity,
|
||||
bootloader, route, watchdog, reachability, or post-boot checks.
|
||||
|
||||
## Exact destination
|
||||
|
||||
This chain is intentionally specific to one firmware lineage and hardware
|
||||
target:
|
||||
|
||||
- target ID: `2FA509C1`
|
||||
- hardware: `RAK_3401`
|
||||
- role: `RAK_3401_repeater_lora_ota_no_external_sensors`
|
||||
- start: `v1.16.7.0-c1caa5ad`, EndF `71F4026CBE4B8B74`
|
||||
- endpoint source: MeshCore `3f6eddd5c20f1fa3df6ba1cc208efbe3e2fffb43`
|
||||
- endpoint label: `v1.17.1.6-halo-keymind-cascade-dev-3f6eddd5`
|
||||
- endpoint packed version: `0x01110106`
|
||||
- deployment target key: `63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d`
|
||||
|
||||
The exact starting application is the
|
||||
[`lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf`](https://github.com/mikecarper/MeshCore/releases/tag/lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf)
|
||||
RAK3401 asset. Its outer ZIP SHA-256 is
|
||||
`58ead2ea18efafda186bc9d983fe08cdf65f69b0e5a3a1c72c18fe35948a254c`;
|
||||
its 566,912-byte `firmware.bin` SHA-256 is
|
||||
`4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb`.
|
||||
The endpoint build ZIP SHA-256 is
|
||||
`a9fca9be145a96feb650b57c43b5ed173d74d2f3194da12e31b1c54352635b56`.
|
||||
|
||||
### Retained and omitted hardware support
|
||||
|
||||
Despite the legacy `no_external_sensors` target name, the endpoint retains the
|
||||
RAK12501 GPS provider and the common INA3221, INA219, INA226, and INA260 I2C
|
||||
voltage/current monitors. Install RAK12501 in sensor slot A. Do not use slot D
|
||||
with the RAK13302 1 W radio because the GPS reset/PPS signals overlap the
|
||||
radio's BUSY/DIO1 wiring.
|
||||
|
||||
The reduced profile omits optional external environmental and ranging devices:
|
||||
AHTX0, BME280, BMP280, SHTC3, SHT4x, LPS22HB, MLX90614, VL53L0X, BME680,
|
||||
BMP085, RAK12035, and BME680 BSEC. Board radio, display, buttons, battery
|
||||
telemetry, GPS, and the INA monitor family remain. No additional “remove I2C
|
||||
temperature” image is needed: the optional I2C temperature/environment drivers
|
||||
are already in the omitted set, while removing the remaining I2C/INA support
|
||||
would discard desired voltage/current monitoring and create another firmware
|
||||
identity without helping this route.
|
||||
|
||||
Do not use the chain on another target ID, hardware family, starting image, or
|
||||
firmware body hash. The runner checks all four.
|
||||
|
||||
## Why the deployed OTAFIX2.4 and old staging ceiling work
|
||||
|
||||
The deployed bootloader is not changed and no package uses the newer expanded
|
||||
`0xED000` ceiling. Each package remains bottom-aligned below the old
|
||||
bootloader's `0xD4000` scan ceiling. Its detools workspace is page-aligned and
|
||||
must satisfy:
|
||||
|
||||
```text
|
||||
0x26000 + inplace_memory <= staged_package_start < 0xD4000
|
||||
```
|
||||
|
||||
The old bootloader already reads and validates that encoded detools geometry
|
||||
before its first destructive write. The previous application receiver was the
|
||||
limiting part: it reserved a fixed `0x98000` workspace before accepting a
|
||||
download. Step 1 therefore remains byte-for-byte identical to the physically
|
||||
passed 89,844-byte package. Step 2 is a 46,364-byte fixed-workspace package
|
||||
that installs the compatibility receiver. Later receivers stage above the
|
||||
real EndF image extent and let the bootloader enforce each package's selected
|
||||
workspace.
|
||||
|
||||
Every package in this candidate is an ordinary application container using
|
||||
format ABI 2 and in-place codec 2. The historical bridge applications scan the
|
||||
bootloader capability marker byte-by-byte, so they recognize the released
|
||||
`0.9.2-OTAFIX2.4` marker at absolute address `0xFCD16`. That immutable marker
|
||||
advertises ABI 2, codec mask `0x0004`, and internal storage profile 0. The
|
||||
`3f6eddd5` endpoint retains the matching legacy application-update fallback.
|
||||
Its separately compiled internal bootloader-update feature remains unavailable
|
||||
because privileged self-update requires a current ABI 3 marker with the exact
|
||||
storage profile.
|
||||
Installing this chain therefore does not replace the bootloader, and the final
|
||||
application can still accept a future valid ABI-2/codec-2 application update.
|
||||
|
||||
The exact installed bootloader is the
|
||||
[`0.9.2-OTAFIX2.4` RAK4631 asset](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4_s140_6.1.1.zip).
|
||||
Its outer ZIP SHA-256 is
|
||||
`5e29e7a8982cc2ed5f8868435556eb09387250fe5b9a7e8443bac325247dfed1`;
|
||||
its 191,040-byte `sd_bl.bin` SHA-256 is
|
||||
`a97f834388db0f1da6c29f6d8f458ff6d3e6a26034709044566384838919bb23`.
|
||||
The exact simulator was compiled from release commit
|
||||
`d73de8372e89b8ef352747c8bc7a1aaeab80fbfe`; its executable SHA-256 is
|
||||
`9dacf24b1023fe2f4c620419417649ccc9538dd4a611b47bc19b7fedb97ceba6`.
|
||||
|
||||
The schema-2 exhaustive search checked 1,799 candidate geometries and found 12
|
||||
feasible forward edges in the declared exact 12-node inventory. It found one
|
||||
shortest ten-package route and selected the smallest total container bytes as
|
||||
the secondary objective. This minimum applies only to that declared inventory
|
||||
and geometry table, not every conceivable new bridge image. The selected route
|
||||
is:
|
||||
|
||||
| Step | From | To | Workspace | Package | Margin |
|
||||
|---:|---|---|---:|---:|---:|
|
||||
| 1 | 1.16.7.0 | 1.16.7.9 | `0x98000` | 89,844 | 0 |
|
||||
| 2 | 1.16.7.9 | 1.16.7.10 | `0x98000` | 46,364 | 40,960 |
|
||||
| 3 | 1.16.7.10 | 1.16.8.7 | `0x91000` | 106,029 | 12,288 |
|
||||
| 4 | 1.16.8.7 | 1.16.9.105 | `0x89000` | 149,927 | 0 |
|
||||
| 5 | 1.16.9.105 | 1.16.9.110 | `0x8E000` | 111,312 | 16,384 |
|
||||
| 6 | 1.16.9.110 | 1.16.9.113 | `0x8F000` | 120,624 | 4,096 |
|
||||
| 7 | 1.16.9.113 | 1.16.9.117 | `0x8B000` | 139,637 | 0 |
|
||||
| 8 | 1.16.9.117 | 1.16.10.0 | `0x7E000` | 174,202 | 20,480 |
|
||||
| 9 | 1.16.10.0 | 1.17.1.3 | `0x6B000` | 271,569 | 0 |
|
||||
| 10 | 1.17.1.3 | 1.17.1.6 | `0x76000` | 197,825 | 28,672 |
|
||||
|
||||
Total mOTA transfer data is 1,407,333 bytes. `ROUTE.json`, `CHAIN.csv`, and
|
||||
`validation-results.json` in the bundle pin the exact geometry and image hash
|
||||
for every transition.
|
||||
|
||||
Steps 1-9 are byte-for-byte identical to the transitions that passed on the
|
||||
physical RAK3401. The replacement step 10, from the physically reached
|
||||
`FE65A6135A1E7B3F` v1.17.1.3 body to `9BD7CF682EE065AE`, has offline
|
||||
qualification only. All ten packages passed independent container checks,
|
||||
zero-filled and erased-workspace reconstruction, and the exact deployed
|
||||
`0.9.2-OTAFIX2.4` simulator. Those results prove the package bytes and apply
|
||||
geometry; they are not a physical claim for the replacement step 10 or the
|
||||
hardened runner.
|
||||
|
||||
The 171-byte transport DEFLATE support lands only in the final `3f6eddd5`
|
||||
application. The starting receiver and every receiver that accepts steps 1-10
|
||||
lack that inflate path, so all ten legacy-chain transfers remain raw and the
|
||||
1,407,333-byte total above is not automatically reduced over LoRa. A later
|
||||
update can negotiate transport compression after this endpoint is running;
|
||||
the application inflates before staging, and the immutable bootloader still
|
||||
applies an ordinary ABI-2/codec-2 container.
|
||||
|
||||
## Host requirements
|
||||
|
||||
Install Python 3.9 or newer, `meshcli` 1.6.0 or newer, the current `motatool`,
|
||||
and an OTA-enabled Full Companion or repeater that can seed mOTA files.
|
||||
|
||||
## Verify offline
|
||||
|
||||
No password or device is needed:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--motatool /path/to/motatool \
|
||||
--verify-only
|
||||
```
|
||||
|
||||
When given the ZIP, the runner checks its pinned outer hash and binds the
|
||||
extraction cache to that exact archive. An explicitly supplied extracted root
|
||||
starts at the pinned inner checksum list. In either case it checks complete
|
||||
inner coverage, all ten manifests, continuity, image anchors, both recovery
|
||||
images, and every container with `motatool`. Passing this command means offline
|
||||
and simulator qualification only; it does not unlock live use.
|
||||
|
||||
## Guarded prerelease lab command
|
||||
|
||||
These direct-link arguments match the physical transition setup, but the
|
||||
current package replaces step 10 and the current host runner contains later
|
||||
cleanup and recovery fixes. Neither has been cleanly rerun through the whole
|
||||
chain. Keep local USB/SWD recovery available. The candidate switch bypasses
|
||||
only the prerelease-status block; all identity, checksum, bootloader, route,
|
||||
watchdog, reachability, and post-boot gates still run.
|
||||
|
||||
Restore the test start locally with
|
||||
`recovery/test-start/RAK3401-test-start-v1.16.7-c1caa5ad.uf2`, then run:
|
||||
|
||||
```bash
|
||||
export MESHCORE_ADMIN_PASSWORD='password'
|
||||
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp 192.168.1.51:5001 \
|
||||
--source-cli-tcp 192.168.1.51:5002 \
|
||||
--source-shares-controller \
|
||||
--target-key 63d8df63 \
|
||||
--temp-radio 909.950,500,5,5,120 \
|
||||
--ota-hops 0 \
|
||||
--legacy-full-airtime \
|
||||
--motatool /path/to/motatool \
|
||||
--accept-test-candidate \
|
||||
--yes
|
||||
```
|
||||
|
||||
`--target-key` accepts a full contact public key or a unique hexadecimal prefix
|
||||
of at least eight characters, such as the 12-character prefix shown by
|
||||
`meshcli lc`. The chain resolves it against the controller's saved contacts
|
||||
and uses the full key for every remote command and transfer step. Emoji or a
|
||||
later change in the repeater's display name does not affect selection. Each
|
||||
`--relay` can likewise use a full key or unique prefix instead of a name.
|
||||
The target still must be a repeater contact with the exact hardware, firmware,
|
||||
and bootloader required by this chain.
|
||||
|
||||
Keep the work directory. Rerunning the same command resumes only when the live
|
||||
EndF body hash matches an exact chain node. Never manually skip a package.
|
||||
`--legacy-full-airtime` temporarily sets the destination airtime factor to
|
||||
zero and restores it at the endpoint. Use that option only where the selected
|
||||
frequency and local duty-cycle rules permit a full transmit budget; omit it
|
||||
otherwise.
|
||||
|
||||
## Superseded `b40d2e6c` prerelease
|
||||
|
||||
The immediately preceding prerelease remains pinned for offline provenance:
|
||||
|
||||
- release: [`rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c`](https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c)
|
||||
- asset: `RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip`
|
||||
- outer ZIP SHA-256: `8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a`
|
||||
- endpoint image SHA-256: `a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905`
|
||||
- endpoint EndF body hash: `1FA6AAA3C28D8BD8`
|
||||
|
||||
Its first nine packages are byte-for-byte identical to the current release and
|
||||
the physical `fd98bc90` evidence. Its different step 10 had offline
|
||||
qualification only. The runner recognizes this exact archive separately and
|
||||
keeps live use gated; it is not interchangeable with the current `3f6eddd5`
|
||||
asset.
|
||||
|
||||
## Legacy `fd98bc90` physical and SWD qualification
|
||||
|
||||
The earlier local candidate remains the exact physical evidence source for
|
||||
this route:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
```
|
||||
|
||||
- candidate ZIP SHA-256: `c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881`
|
||||
- endpoint image SHA-256: `31c182c888ceb1135e5afb2376610d93cee2e807b556c838e07fd4486c79d095`
|
||||
- endpoint EndF body hash: `9179B98432895924`
|
||||
|
||||
All ten exact `fd98bc90` transitions completed on the RAK3401 with its
|
||||
deployed `0.9.2-OTAFIX2.4` bootloader and a Heltec V4 source at 909.950 MHz /
|
||||
500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD
|
||||
readback then matched the endpoint application, original bootloader,
|
||||
SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine
|
||||
packages in the `b40d2e6c` and `3f6eddd5` prereleases. All three candidates
|
||||
have different step 10 packages or endpoints, so the physical result and
|
||||
step-10 timing must not be attributed to either later prerelease.
|
||||
|
||||
### Measured direct-link timing for `fd98bc90`
|
||||
|
||||
The first `ready to install` transfer ages were:
|
||||
|
||||
| Step | Blocks | Transfer |
|
||||
|---:|---:|---:|
|
||||
| 1 | 88 | 598 s |
|
||||
| 2 | 45 | 42 s |
|
||||
| 3 | 103 | 81 s |
|
||||
| 4 | 146 | 105 s |
|
||||
| 5 | 109 | 76 s |
|
||||
| 6 | 118 | 94 s |
|
||||
| 7 | 136 | 102 s |
|
||||
| 8 | 170 | 127 s |
|
||||
| 9 | 264 | 187 s |
|
||||
| 10 | 169 | 193 s |
|
||||
|
||||
The physically observed bulk-transfer total for those exact legacy packages is
|
||||
1,605 seconds (26m45s). The log came from the pre-cleanup-fix runner revision.
|
||||
Step 1 was measured before the host began forcing source RXPS off and is
|
||||
therefore a conservative outlier. A new complete run with that fix is projected
|
||||
at roughly 18–19 minutes of bulk transfer, but that projection is not a
|
||||
substitute for the measured total. Allow about 70–75 minutes direct at BW500
|
||||
for validation, ten transfers, installs/reboots, retained-store checks, and
|
||||
final restoration.
|
||||
|
||||
The legacy step 10's longer per-block time was not a V4 TempRadio delay: the
|
||||
source RXPS readback was off. Its v1.17.1.3 requester uses non-overlapping
|
||||
adaptive flights and a five-second recovery floor and logged more block reloads
|
||||
than step 8. The V4 itself intentionally schedules its local radio handoff
|
||||
after 1.5 seconds so the normal-channel command reply can drain; the host waits
|
||||
three seconds and then checks the live TempRadio state.
|
||||
|
||||
### Live progress and speed sanity checks
|
||||
|
||||
The runner watches `motatool serve -v` locally, so its quarter-progress reports
|
||||
do not consume LoRa airtime. At BW500 direct, physically measured steps 2–9
|
||||
averaged about 1.1–1.4 newly requested payload blocks per second; the legacy
|
||||
step 10 averaged about 0.88 blocks/s, and step 1's 0.15 blocks/s was the known
|
||||
pre-RXPS-fix outlier. Use those only as bench sanity ranges, not guarantees.
|
||||
Relays, narrower bandwidths, interference, and recovery reads lower the rate.
|
||||
|
||||
Healthy output keeps advancing from `[download] passive source progress x/y`
|
||||
to `source read all y payload blocks`, followed by destination status `ready to
|
||||
install y/y (100%)` for the same manifest ID. Aggregate reads above the unique
|
||||
block count indicate recovery/retry traffic. A radio-scaled no-progress window
|
||||
causes one destination status check; the transfer timeout leaves the partial
|
||||
download staged for an exact-package resume. Stop and diagnose instead of
|
||||
installing if the manifest changes, progress remains flat through those checks,
|
||||
the final receiver count is not `y/y`, or the measured rate is far below the
|
||||
matching bandwidth/hop estimate without an understood RF cause.
|
||||
|
||||
For planning only, scale the conservative measured `fd98bc90` 26m45s bulk
|
||||
baseline by the qualified adaptive-preamble airtime and by each additional
|
||||
relay transmission:
|
||||
|
||||
| Bandwidth, SF5/CR5 | Direct / 0 relays | 1 relay | 2 relays |
|
||||
|---:|---:|---:|---:|
|
||||
| 500 kHz | 26m45s | 53m30s | 1h20m15s |
|
||||
| 250 kHz | 46m51s | 1h33m42s | 2h20m33s |
|
||||
| 125 kHz | 1h27m03s | 2h54m07s | 4h21m11s |
|
||||
| 62.5 kHz | 2h54m07s | 5h48m15s | 8h42m23s |
|
||||
|
||||
These are transfer-only estimates, not measured alternate-bandwidth or relay
|
||||
results. Packet loss, relay contention, discovery, administrative commands,
|
||||
staged verification, and ten reboot cycles add time. The bandwidth factors are
|
||||
not simple powers of two because the adaptive wire preamble is 128 symbols at
|
||||
500 kHz, 64 at 250 kHz, and 32 at 125/62.5 kHz.
|
||||
|
||||
## Manual operation
|
||||
|
||||
The automated runner is preferred because it binds every transition to the
|
||||
expected MID and body hash. The manual details below document the checks used
|
||||
during qualification and recovery; they are not permission to skip the
|
||||
runner's candidate gate. Keep a written copy of each original setting and
|
||||
never skip a step even when a later package appears in `ota ls`.
|
||||
|
||||
### 1. Verify and extract the asset
|
||||
|
||||
```bash
|
||||
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
|
||||
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5
|
||||
sha256sum -c SHA256SUMS.txt
|
||||
for package in motas/*.mota; do motatool verify "$package" || exit 1; done
|
||||
```
|
||||
|
||||
The outer hash must be
|
||||
`135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4`,
|
||||
and the SHA-256 of the extracted `SHA256SUMS.txt` must be
|
||||
`93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4`.
|
||||
Do not continue after any checksum or verification error.
|
||||
|
||||
### 2. Record and prepare the destination
|
||||
|
||||
Before changing anything, save the complete replies from:
|
||||
|
||||
```text
|
||||
ver
|
||||
ota self
|
||||
get bootloader.ver
|
||||
get system.watchdog
|
||||
get radio.rxps
|
||||
get powersaving
|
||||
get rxdelay
|
||||
get af
|
||||
ota config
|
||||
```
|
||||
|
||||
The starting `ota self` hash must be `71F4026CBE4B8B74`, the target must be
|
||||
`2FA509C1`, and the hardware must be `RAK_3401`. Disable the nRF52 system
|
||||
watchdog before the chain:
|
||||
|
||||
```text
|
||||
set system.watchdog off
|
||||
```
|
||||
|
||||
The hardware watchdog cannot stop immediately. The RAK will reset once within
|
||||
about 60 seconds. Reconnect, wait at least 90 seconds from the command, and
|
||||
require `get system.watchdog` to report `> off` before continuing.
|
||||
|
||||
For a direct bench link, apply these temporary transfer guardrails to the RAK
|
||||
after every bridge reboot:
|
||||
|
||||
```text
|
||||
set powersaving off
|
||||
set radio.rxps off
|
||||
set rxdelay 0
|
||||
ota config hops 0
|
||||
tempradio 909.950,500,5,5,120
|
||||
```
|
||||
|
||||
Where local duty-cycle rules permit it, `set af 0` can also remove the legacy
|
||||
firmware's saved airtime wait during this bounded maintenance window. Record
|
||||
`get af` first and restore that exact value after the final step. Current
|
||||
firmware grants the bounded TempRadio transfer budget without overwriting the
|
||||
saved airtime factor.
|
||||
|
||||
`set powersaving off` is an isolation guardrail, not the fix for the observed
|
||||
failure. The actual source failure was an oversized USB CDC reply; current
|
||||
firmware splits those reads. Some historical bridge builds may report RXPS as
|
||||
unsupported while already operating continuously. Verify the reported state
|
||||
and do not substitute a normal reboot for the one-time watchdog reset. Direct
|
||||
tests use `hops 0`: `hops 1` makes the target echo each source response despite
|
||||
there being no intermediate relay, increasing half-duplex loss and legacy
|
||||
three-second retries.
|
||||
|
||||
The exact candidate cannot use fast RXPS throughout because its historical
|
||||
bridge applications predate the v1.17.1.5 adaptive-preamble contract. Keep
|
||||
RXPS off for all ten steps whether using SF5/BW250 or the faster direct-bench
|
||||
SF5/BW500 tuple. After the final endpoint is proven, a later update whose
|
||||
target, controller, source, and relays are all v1.17.1.5 or newer can use the
|
||||
generic LoRa OTA runner and its qualified adaptive-preamble boundaries.
|
||||
|
||||
Put the source on the identical TempRadio tuple. A current ASCII-first Full
|
||||
Companion recognizes `motatool`'s initial `ota folder on` line directly:
|
||||
|
||||
```bash
|
||||
motatool serve \
|
||||
--dir ./motas \
|
||||
--serial /dev/ttyACM1 \
|
||||
--baud 115200 \
|
||||
-v
|
||||
```
|
||||
|
||||
The same command also works with older Full Companion firmware in binary mode,
|
||||
where the idle parser recognizes the identical preamble. Restart `motatool` for
|
||||
every step so the source emits a fresh catalog advert. Leave it running during
|
||||
the download and stop it with Ctrl-C only after the destination reports `ready
|
||||
to install`.
|
||||
|
||||
### 3. Install all ten packages in order
|
||||
|
||||
| Step | Manifest ID | Before hash | Expected version | After hash |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `C147BCEF` | `71F4026CBE4B8B74` | `1.16.7.9` | `42BC53A64288E845` |
|
||||
| 2 | `BEB578FC` | `42BC53A64288E845` | `1.16.7.10` | `1AD2AA8B6C478DA6` |
|
||||
| 3 | `6D8EF814` | `1AD2AA8B6C478DA6` | `1.16.8.7` | `D709A25308BC1831` |
|
||||
| 4 | `40AC4CEB` | `D709A25308BC1831` | `1.16.9.105` | `457BEDA5E6406C81` |
|
||||
| 5 | `F6C8890B` | `457BEDA5E6406C81` | `1.16.9.110` | `E76BFA205634CAB8` |
|
||||
| 6 | `82405E89` | `E76BFA205634CAB8` | `1.16.9.113` | `65328FC8A1FBED2D` |
|
||||
| 7 | `80F40DB6` | `65328FC8A1FBED2D` | `1.16.9.117` | `AF7532E13337FADD` |
|
||||
| 8 | `DBF6310D` | `AF7532E13337FADD` | `1.16.10.0` | `22002359BBDFA76E` |
|
||||
| 9 | `C2979E08` | `22002359BBDFA76E` | `1.17.1.3` | `FE65A6135A1E7B3F` |
|
||||
| 10 | `B5011420` | `FE65A6135A1E7B3F` | `1.17.1.5` | `1FA6AAA3C28D8BD8` |
|
||||
|
||||
For each row, first prove that `ota self` exactly matches its **Before hash**,
|
||||
then inspect the manager before changing it:
|
||||
|
||||
```text
|
||||
ota status
|
||||
```
|
||||
|
||||
Proceed to discovery only after that status is reconciled. When the manager is
|
||||
idle, run:
|
||||
|
||||
```text
|
||||
ota ls
|
||||
ota pull MANIFEST_ID flash
|
||||
ota status
|
||||
```
|
||||
|
||||
If the first status reports this row's MID as partial, use `ota pull` with that
|
||||
same MID to resume it. If this row's MID is already ready, do not pull or
|
||||
cancel it; continue with the install checks below. If status reports the
|
||||
immediately previous row's MID as `verifying staged blocks`, wait through the
|
||||
bounded discovery window; require either that exact MID to become `ready to
|
||||
install` or the manager to become `no download`, and re-prove the current
|
||||
**Before hash**. Cancel only the same previous MID while it is visibly
|
||||
attached and ready. If it has become idle, send no cancel. Stop on any other
|
||||
MID, failed or incomplete state, ambiguous reply, or timeout. Never issue a
|
||||
blind `ota cancel`: it can discard a resumable or unrelated session.
|
||||
|
||||
Repeat `ota status` at a restrained interval until it says `ready to install`.
|
||||
If the manifest is initially absent, wait for a fresh source advert, run
|
||||
`ota ls` again, or restart `motatool`; do not pull a different ID. Stop the
|
||||
seeder and install:
|
||||
|
||||
```text
|
||||
get system.watchdog
|
||||
ota install
|
||||
```
|
||||
|
||||
The watchdog reply must still be `> off`. Allow the current default five-minute
|
||||
readiness window for the USB/LoRa identity to return; automation probes every
|
||||
10 seconds and returns early as soon as the exact body is visible. Then run
|
||||
`ver`, `ota self`, and `ota help`.
|
||||
Require the row's exact **After hash**, require `bootloader: apply OK`, and
|
||||
require `rescue install <hash16>` in the help before exposing that bridge to
|
||||
the next package. This rescue-help gate applies to steps 1-9; step 10 is the
|
||||
endpoint and is not exposed to another package in this chain. A normal-radio
|
||||
`ota status` response of `no download` proves only that the manager is idle;
|
||||
legacy internal-flash firmware does not durably erase a retained container by
|
||||
accepting an IDLE `ota cancel`. Do not claim otherwise. When the same expected
|
||||
manifest reports `verifying staged blocks` after the next TempRadio starts,
|
||||
wait only for the bounded discovery window and require that it become `ready
|
||||
to install` or `no download`; stop on a changed ID, failed/incomplete state, or
|
||||
timeout. Prove the exact running body before cancelling an attached completed
|
||||
session. OTAFIX consumes the approval word during a successful install, so a
|
||||
retained endpoint container is inert and a later valid pull replaces it.
|
||||
Reapply the transfer
|
||||
guardrails and TempRadio tuple, restart `motatool`, and continue with the next
|
||||
row.
|
||||
|
||||
If the board boots but `ota self` says the EndF is invalid, stop. Only when
|
||||
`ota status` still identifies that row's complete staged package may the
|
||||
guarded `ota rescue install BEFORE_HASH` command be used. It is not a force
|
||||
option and must use that row's exact Before hash. If the board does not boot,
|
||||
recover the documented start/recovery UF2 locally over USB. Step 10 is the
|
||||
endpoint and has no successor package in this chain. If its EndF is invalid,
|
||||
use local DFU/SWD rather than attempting an unqualified LoRa package.
|
||||
|
||||
### 4. Intermediate relays and restoration
|
||||
|
||||
On every managed intermediate relay, save `get rxdelay` and `get txdelay`, then
|
||||
use the airtime-scaled collision window during the maintenance session:
|
||||
|
||||
```text
|
||||
set rxdelay 0
|
||||
set txdelay 0.3
|
||||
```
|
||||
|
||||
Current OTA transfer packets honor that configured relay `txdelay`; setting
|
||||
`0.3` gives competing relays a small randomized, airtime-scaled forwarding
|
||||
window. The source's manifest-fragment gap independently follows active packet
|
||||
airtime and duty spacing, clamped to 100-1000 ms. These two delays solve
|
||||
different problems.
|
||||
|
||||
After step 10 is proven, restore each saved destination and relay value exactly,
|
||||
including `rxdelay`, RXPS, CPU power saving, `af`, `ota config hops`, and relay
|
||||
timing. Current full-parser repeater firmware accepts `normalradio` and restores
|
||||
the saved tuple after replying on the temporary channel:
|
||||
|
||||
```text
|
||||
normalradio
|
||||
```
|
||||
|
||||
The `b40d2e6c` endpoint supports that command. If a recovery build unexpectedly
|
||||
replies `Unknown command`, shorten its lease to one minute with the same tuple:
|
||||
|
||||
```text
|
||||
tempradio 909.950,500,5,5,1
|
||||
```
|
||||
|
||||
Wait for the lease to expire, return the controller and ordinary source to
|
||||
their saved normal channels, and then re-enable the RAK system watchdog:
|
||||
|
||||
```text
|
||||
set system.watchdog on
|
||||
get system.watchdog
|
||||
```
|
||||
|
||||
A Full Companion source is the exception: its local TCP console supports
|
||||
`normalradio` and the automated runner uses it before restoring the shared
|
||||
Binary API radio tuple.
|
||||
|
||||
Final lab success requires version
|
||||
`v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c`, body hash
|
||||
`1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, and
|
||||
`get system.watchdog` reporting `> on`. If a relay cannot be restored before
|
||||
its TempRadio lease ends, wait for it to return to the normal channel and
|
||||
restore its saved `rxdelay` and `txdelay` there.
|
||||
|
||||
## Two-relay deployment
|
||||
|
||||
The first nine direct package transitions above were physically observed as
|
||||
the byte-identical first nine transitions of the legacy chain. Replacement
|
||||
step 10, this hardened host-runner revision, and multi-hop operation are not
|
||||
cleanly end-to-end physically qualified. A controlled, recoverable multi-hop
|
||||
lab can exercise two intermediate relays by
|
||||
listing them farthest-to-nearest and using three OTA hops. Run a non-mutating
|
||||
preflight first; even preflight requires the prerelease candidate override
|
||||
because the command connects to live devices:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp SOURCE_IP:5001 \
|
||||
--source-cli-tcp SOURCE_IP:5002 \
|
||||
--source-shares-controller \
|
||||
--target-key 63d8df63 \
|
||||
--relay 'Far Repeater' \
|
||||
--relay 'Near Repeater' \
|
||||
--temp-radio 909.950,250,5,5,120 \
|
||||
--ota-hops 3 \
|
||||
--motatool /path/to/motatool \
|
||||
--accept-test-candidate \
|
||||
--preflight-only
|
||||
```
|
||||
|
||||
After preflight succeeds, rerun with `--yes` in place of
|
||||
`--preflight-only`. Confirm that the selected frequency and bandwidth are
|
||||
legal at the deployment location.
|
||||
|
||||
## Rescue and completion
|
||||
|
||||
The guarded `ota rescue install <base_body_hash>` command is present after
|
||||
step 1 and remains available through the step-9 bridge. It is not a force
|
||||
command: it refuses a valid normal EndF, a base mismatch, wrong
|
||||
target/hardware, or invalid payload. A failure before step 1 completes still
|
||||
requires local USB recovery because the deployed start image predates the
|
||||
rescue command. The final `b40d2e6c` endpoint uses the stricter shared-slot
|
||||
profile; its invalid-EndF recovery is local DFU/SWD, not the rescue command.
|
||||
|
||||
Before the first mutation, the runner saves the destination's RXPS periods,
|
||||
CPU power-saving state, RX flood delay, airtime factor, and OTA hop reach in
|
||||
the persistent work directory, flushing both the file and its directory entry
|
||||
before changing a guardrail. It also saves the managed source's exact RXPS
|
||||
preference in the chain-root `source-rxps-settings.json` before disabling it.
|
||||
Every step reuses that protected record, so a rerun after a killed host process
|
||||
restores the chain-start preference instead of treating the temporary off state
|
||||
as original. The record is accepted only for the same CLI endpoint. The runner
|
||||
retires that chain-root record only after it has proved the source is back on
|
||||
its normal radio and restored the exact preference at the verified endpoint.
|
||||
The runner
|
||||
reads the destination, controller, source, and relay versions before applying
|
||||
the same RXPS policy as the generic runner.
|
||||
The mixed historical chain therefore verifies `radio.rxps off` after every
|
||||
bridge reboot; a future all-v1.17.1.5-or-newer SF5/BW250 chain would instead
|
||||
keep RXPS on under the qualified level-8/preamble-64 boundary. It also verifies
|
||||
`set powersaving off` and `rxdelay 0`, plus `af 0` when
|
||||
`--legacy-full-airtime` was explicitly selected. It restores every original
|
||||
value only after the exact endpoint is proven. An interrupted run deliberately
|
||||
leaves those transfer guardrails active; rerun the same command with the same
|
||||
work directory to resume and restore them.
|
||||
CPU power saving was not the cause of the observed folder-source failure: that
|
||||
was a USB CDC receive-ring overrun fixed by bounded serial reads. The runtime
|
||||
settings remain conservative isolation and fast-link reliability guardrails.
|
||||
|
||||
For every named `--relay`, the per-step runner also captures `rxdelay` and
|
||||
`txdelay`, writes them to `relay-timing-settings.json`, applies `rxdelay 0` and
|
||||
`txdelay 0.3` during transfer, verifies the readback, and restores both before
|
||||
the relay leaves TempRadio.
|
||||
|
||||
The runner also keeps the watchdog off across the chain, rechecks identity and
|
||||
OTA reach before every package, requires the exact post-boot EndF hash, and
|
||||
only re-enables the watchdog after step 10. Lab success requires endpoint body
|
||||
hash `1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, normal radio
|
||||
`910.525 MHz / 62.5 kHz / SF7 / CR5`, and the watchdog verified on.
|
||||
|
||||
## External-radio startup and manual USB recovery
|
||||
|
||||
RAK3401 is a distinct target from RAK4631. Its RAK13300/RAK13302 radio is on
|
||||
the WisBlock SPI bus (`SPI1` in the Adafruit nRF52 core), with BUSY and DIO1 on
|
||||
P0.09/P0.10. Current builds explicitly enable those NFC-capable pads as GPIO
|
||||
and do not advertise the radio bus as an on-board QSPI flash device.
|
||||
|
||||
The RAK13302's FEM and boost supply use switched `3V3_S`, but the SX1262 core
|
||||
uses unswitched 3V3. An MCU reset, a 1200-baud DFU touch, or disconnecting only
|
||||
USB may therefore leave a battery-powered SX1262 in its previous state. At
|
||||
startup, current firmware quiesces the FEM, reinitializes the dedicated SPI
|
||||
instance, asserts NRESET, and, if BUSY remains high, sends the Semtech
|
||||
NSS/GET_STATUS wake sequence without waiting on RadioLib first. The one-time
|
||||
board startup cold-starts `3V3_S`; later radio retries leave that shared rail
|
||||
enabled so they do not reset GPS or sensor modules.
|
||||
|
||||
If BUSY still cannot be released, firmware must not enter a reset loop:
|
||||
|
||||
- Companion starts its USB/BLE management services after three bounded probes,
|
||||
uses nRF52 hardware entropy if it needs a first-boot identity, and retries the
|
||||
radio every 60 seconds. Radio statistics remain zero until recovery.
|
||||
- Repeater keeps the same USB CDC session open and prints
|
||||
`Radio unavailable; retrying in 60 seconds`. It repeats the board-level probe
|
||||
in place. The normal repeater CLI and mesh transport cannot start until the
|
||||
radio responds.
|
||||
|
||||
A permanently high BUSY after NRESET and the direct wake transaction indicates
|
||||
an electrically unavailable radio, loose module, or power-domain fault; an MCU
|
||||
reboot cannot manufacture a response. If local access is possible, disconnect
|
||||
the battery as well as USB and reseat the WisBlock module. Remote firmware will
|
||||
continue retrying without requiring that physical intervention or churning the
|
||||
USB device.
|
||||
|
||||
For an existing deployed tower using Preview 5, do not change the bootloader
|
||||
as part of this application chain. Its exact local recovery package is the
|
||||
[Preview 5 RAK4631 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4.1-preview.5/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4.1-preview.5_s140_6.1.1.zip).
|
||||
For a new RAK3401 installation or local recovery, use the
|
||||
[OTAFIX 2.4.3 RAK3401 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.11.0-OTAFIX2.4.3/wiscore_rak3401_bootloader-0.11.0-OTAFIX2.4.3_s140_6.1.1.zip)
|
||||
from the
|
||||
[OTAFIX 2.4.3 release](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.3).
|
||||
These are local DFU/SWD recovery assets, not mOTA chain steps.
|
||||
|
||||
For a local Serial DFU recovery with the repository helper:
|
||||
|
||||
```bash
|
||||
cd /path/to/meshfirmware
|
||||
MCFIRMWARE_NO_SUDO=1 ./mcfirmware.sh
|
||||
```
|
||||
|
||||
Select the serial entry whose USB identity is `WisCore RAK3401 Board`, then
|
||||
choose **Custom**, **nrf52**, and the exact RAK3401 `.zip`. Use **flash-update**
|
||||
to preserve InternalFS preferences and identity. Use **flash-wipe + flash** only
|
||||
when erasing application data is intentional. The helper follows the same USB
|
||||
device into bootloader mode, so do not substitute another `/dev/ttyACM*` merely
|
||||
because its number appears first.
|
||||
@@ -7,7 +7,7 @@ at **350 contacts, 40 channels and 256 normal offline frames**. Their final
|
||||
capability manifests confirm compiled Bluetooth/mOTA support and valid WiFi
|
||||
OTA layouts.
|
||||
These are the optional environments in the
|
||||
[contact-cache guide](../companion_contact_cache.md). Ordinary release recipes
|
||||
[contact-cache guide](../research/companion_contact_cache.md). Ordinary release recipes
|
||||
retain their established contact limits pending qualification on those boards.
|
||||
|
||||
## Linked internal RAM
|
||||
@@ -125,7 +125,7 @@ The native queue/store suites passed 27 cases, the radio-liveness suite passed
|
||||
eight, and the partition-selection suite passed 11.
|
||||
|
||||
Hardware reproduction commands are in the
|
||||
[contact-cache guide](../companion_contact_cache.md#regression-checks). The tracked
|
||||
[contact-cache guide](../research/companion_contact_cache.md#regression-checks). The tracked
|
||||
harness is `tools/hil/contact_cache_serial_stress.py`. It requires exclusive
|
||||
access to the specified Companion data port.
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# XIAO Bluetooth stealth hardware validation - 2026-09-07
|
||||
|
||||
Historical command syntax: the initial run used stealth as a MAC mode.
|
||||
Current firmware uses the independent `bluetooth.stealth on|off` flag instead
|
||||
and does not migrate that development mode. See [current commands](../cli_commands.md)
|
||||
and the independent-flag follow-up below. Each observation applies only to
|
||||
the exact artifact listed in its section.
|
||||
|
||||
This is a hardware observation report, not an all-board or phone qualification.
|
||||
The test host was the MercerWoodMesh Raspberry Pi Zero 2 W, accessed remotely
|
||||
over Tailscale. Tests used its built-in Bluetooth adapter, not USB resets or
|
||||
hub power switching. Local timestamps below use America/Los_Angeles.
|
||||
|
||||
## Exact target and firmware
|
||||
|
||||
- Board: Seeed XIAO nRF52840; application name `MeshCore-4610BBD2`.
|
||||
- Factory/bootloader address: `E5:C3:A8:B0:60:66`.
|
||||
- Fresh stealth test address: `DC:8D:5E:45:89:6F`.
|
||||
- Bonded test central: Mercer Pi, public address `B8:27:EB:F9:6E:8E`.
|
||||
- Environment: `Xiao_nrf52_companion_radio_ble`, supported build wrapper,
|
||||
target radio preset, default profile, GCC 14.2.1.
|
||||
- Firmware: `dev-stealth3-4b400ea5`, including the uncommitted Bluetooth
|
||||
identity changes on top of `4b400ea5`.
|
||||
- Application ZIP SHA-256:
|
||||
`acdce0af2334349c6e0a5786db5525b00d19befef340411d2e99008ce4d316ae`.
|
||||
- Linked RAM: 183,232 / 237,568 bytes; flash: 616,504 / 708,608 bytes.
|
||||
|
||||
The application was installed with Nordic Legacy BLE DFU. The tool verified
|
||||
the exact ZIP hash, bootloader advertisement and live DIS model
|
||||
`XIAO nRF52840` before transmitting 616,584 application bytes. Target image
|
||||
validation and target-initiated activation disconnect passed. The transfer
|
||||
reported about 2,223 bytes/second (about 277 seconds for payload transmission).
|
||||
Authenticated MeshCore device queries subsequently returned protocol 14,
|
||||
board `Seeed Xiao-nrf52`, and the expected unique test-version prefix.
|
||||
|
||||
## Observed results
|
||||
|
||||
| Check | Result and evidence |
|
||||
| --- | --- |
|
||||
| First-pair discovery | `set bluetooth.mac stealth` saved a new random-static address; reboot returned that exact address with the normal name and Nordic UART advertisement. |
|
||||
| First authenticated pairing | The Pi received a passkey request and completed pairing. BlueZ reported both Paired and Bonded. MITM-protected Nordic UART notifications and real command/reply exchanges worked at MTU 247. |
|
||||
| Pairing transition keeps the session alive | The initial connection returned `stealth; bonded-peer-only advertising`, remained usable for five more seconds, and answered another command without disconnecting or pairing again. |
|
||||
| Advertising after disconnect | A public-address passive scan captured `ADV_DIRECT_IND` from the exact target, with advertising data length zero. The capture contained no undirected advertisement or scan response from that address. |
|
||||
| Bonded reconnect | Subsequent direct connections by saved BlueZ device identity succeeded without a new passkey, including live version, core-statistics and Bluetooth-mode replies. |
|
||||
| Warm reboot persistence | Two commanded reboots retained the address and saved bond. Directed advertising was captured after reboot, followed by authenticated reconnect. Device uptime decreased from 61 seconds before the second reboot to 35 seconds at the next query; core error flags were zero. |
|
||||
| Slow advertising interval | A successful bonded reconnect occurred about 61 seconds after reboot, beyond the 30-second fast-advertising interval. |
|
||||
| Reopen pairing | Setting `stealth` again from an existing bonded connection generated a new address, and the next boot allowed a fresh first pairing. |
|
||||
| Restore default | `set bluetooth.mac default` plus reboot restored `E5:C3:A8:B0:60:66` and normal discovery. Fresh PIN pairing, version/core-statistics queries and repeated `factory address (default)` replies passed. The bench target was left in this discoverable mode. |
|
||||
|
||||
The newly completed pairing reaches Bluefruit's secured callback before its
|
||||
bonded flag is necessarily ready. Recording the completed pairing in the
|
||||
pair-complete callback as well, waiting for the deferred bond write, and
|
||||
deferring identity-list reconfiguration until disconnect are covered by this
|
||||
fresh-pair hardware run. Static source contracts also check those paths.
|
||||
|
||||
The binary reboot command can reset the target before BlueZ receives its ATT
|
||||
write acknowledgement. One initial harness run reported `Unlikely Error`
|
||||
after successful pre-reboot commands. Later checks require an observed
|
||||
disconnect, a live post-reboot query and decreased device uptime; they do not
|
||||
treat the write exception or tool exit code alone as a successful reboot.
|
||||
|
||||
## Discovery and DFU cautions found during testing
|
||||
|
||||
An ordinary BlueZ discovery scan used a temporary random local address and
|
||||
did not show the directed target. A passive scan using the Pi's public
|
||||
identity did show it. Reconnection must use the saved peer/device identity;
|
||||
absence from an ordinary scan is not proof that the node stopped working.
|
||||
Conversely, BlueZ can retain a cached name after pairing, so a displayed name
|
||||
is not proof that the name is still being advertised. The packet-type and
|
||||
zero-length-payload capture is the relevant evidence here.
|
||||
|
||||
The existing DFU handoff helper assumed the application and bootloader shared
|
||||
one address. That assumption is false with a custom application address:
|
||||
the authenticated handoff worked, but the helper timed out looking for DFU
|
||||
at the application address. A fresh exact-address scan found the known
|
||||
factory-address `XIAO_DFU` instead. An old factory-address host bond also
|
||||
caused the first DFU service-discovery attempt to disconnect; clearing only
|
||||
that old lab record allowed DFU. No bootloader code was changed for this test.
|
||||
|
||||
An earlier application-address host pairing record was also removed before
|
||||
its saved stealth state was known. A direct, known-address connection from
|
||||
the same Pi identity allowed fresh PIN pairing and protected commands again.
|
||||
This does not prove that an arbitrary replacement phone can rediscover or
|
||||
recover a stealth node after forgetting its bond.
|
||||
|
||||
## Remaining hardware gates
|
||||
|
||||
- True cold boot with all XIAO power sources, including battery, disconnected.
|
||||
The XIAO reported a battery voltage and did not enumerate as USB on the Pi;
|
||||
a Pi-only reboot or power cycle would not prove this gate.
|
||||
- Unused-boot retention with `random-after-connect`, using a reboot trigger
|
||||
that does not itself authenticate over BLE. The policy has native tests;
|
||||
the remote BLE reboot command cannot prove this hardware gate.
|
||||
- Android and iOS bonded reconnect, including a phone changing its resolvable
|
||||
private address, and direct-connection behavior in the actual MeshCore app.
|
||||
- Rejection of a connection from a second, unbonded physical central.
|
||||
- ESP32 Bluedroid and NimBLE hardware. Those use allowlisted minimal
|
||||
advertising rather than nRF52 directed advertisements.
|
||||
- Node-side missing/corrupt bond and failed settings/bond-write fault injection.
|
||||
|
||||
## Repeatable regression procedure
|
||||
|
||||
1. Record the exact board, application version, ZIP hash, local/remote BLE
|
||||
identities and original Bluetooth setting. Keep a working recovery path.
|
||||
2. Set the desired custom or random MAC, enable `bluetooth.stealth on`, reboot,
|
||||
and capture normal discovery before pairing.
|
||||
3. Pair once. Require a stored bond and successful protected MeshCore commands,
|
||||
then keep the original session open and issue another command.
|
||||
4. Disconnect. Capture the actual advertising type and payload with an
|
||||
appropriate receiving identity; do not use cached scan names as evidence.
|
||||
5. Reconnect by the saved peer without another PIN and exchange commands.
|
||||
6. For custom and saved-random addresses, repeat `bluetooth.stealth on`, reboot
|
||||
twice, and verify unchanged identity/bond, decreased uptime and live
|
||||
commands. Include a reconnect after the fast-advertising period ends.
|
||||
7. Separately remove all target power and repeat the persistence checks.
|
||||
8. Test `bluetooth.stealth off`, then `on`, as a pairing reset, and `off` as
|
||||
the exit from stealth. Verify live discovery, pairing, commands and an
|
||||
unchanged MAC policy. Changing the address to `default` is a separate step.
|
||||
9. With each rotating MAC policy, verify that its rotation trigger still
|
||||
works, stealth stays enabled, and the new address permits fresh pairing.
|
||||
10. Repeat relevant gates for a phone with address privacy and each BLE backend.
|
||||
|
||||
Raw VM-side logs, the exact artifact and bounded test helper are in
|
||||
`/home/mesh/mercer-stealth-validation.jUDW0v/`. Raw Bluetooth captures are
|
||||
local diagnostics and should not be published without privacy review.
|
||||
|
||||
## Mercer crash capture
|
||||
|
||||
During this run the Pi used its existing `dwc_otg.speed=1` full-speed USB
|
||||
configuration. Kernel and health logs were streamed to this VM. No new kernel
|
||||
errors or Pi lockup were observed during these tests; that does not establish
|
||||
the cause of the earlier lockup.
|
||||
|
||||
The live test streams and all temporary scans were stopped at approximately
|
||||
22:56 PDT. Journald remained active, the restore service remained enabled,
|
||||
and the test XIAO was disconnected in default mode. Disk journals occupied
|
||||
about 4.9 MiB at the final check.
|
||||
|
||||
At 22:35 PDT, temporary persistent journaling was enabled with a 16 MiB
|
||||
configured disk-use limit, 2 MiB journal files, one-day retention and a
|
||||
30-second normal sync interval. Recent buffered messages can still be lost
|
||||
in a sudden failure. The original `Storage=volatile` configuration is intact.
|
||||
|
||||
`mercer-crash-capture-restore.service` is enabled for the next Pi boot. It
|
||||
checks the armed boot ID, removes only the unchanged managed override,
|
||||
switches journald back to RAM and leaves the old disk journals for collection.
|
||||
Its same-boot no-op guard and unit syntax passed. The next-boot restoration
|
||||
has not yet been exercised; no Pi crash or reboot was deliberately induced.
|
||||
|
||||
## Independent-flag follow-up - dev-stflag
|
||||
|
||||
The same XIAO and Pi were used later on 2026-09-07 to test the separate
|
||||
`bluetooth.stealth on|off` flag, with no development-mode migration. This run
|
||||
used `Xiao_nrf52_companion_radio_ble`, target radio preset, default profile,
|
||||
and firmware `dev-stflag-4b400ea5` with the uncommitted flag changes.
|
||||
|
||||
- Application ZIP SHA-256:
|
||||
`48fc0ff2c716281771f8b525c62a0015c7605b07e7c513ab854c8b3ff3113f42`.
|
||||
- Linked RAM: 183,232 / 237,568 bytes; flash: 617,192 / 708,608 bytes.
|
||||
- Nordic Legacy BLE DFU verified the exact target, hash and model, transferred
|
||||
617,272 application bytes at a reported 3,522 bytes/second, passed target
|
||||
validation, and observed the target activation disconnect.
|
||||
- Protected live device queries returned `dev-stflag-4b400ea5`, protocol 14,
|
||||
and `Seeed Xiao-nrf52`; every queried core error-flags value was zero.
|
||||
|
||||
| Check | Observed result |
|
||||
| --- | --- |
|
||||
| Default and rejected input | Initial mode was factory address with stealth off. `set bluetooth.mac stealth` and `set bluetooth.stealth maybe` returned errors, leaving both settings unchanged. |
|
||||
| Custom plus stealth | `D2:27:5A:40:06:99` advertised normally before first pairing. Fresh PIN pairing, stored bond, protected commands, and a five-second held session passed with stealth on. |
|
||||
| Repeated on and warm reboot | Repeating `set bluetooth.stealth on` retained the paired state. After reboot, the same custom address accepted protected commands without another PIN. Uptime decreased from 84 to 67 seconds. |
|
||||
| Actual advertising after reboot | A public-address passive capture showed `ADV_DIRECT_IND` for the custom address, data length zero, and no undirected advertisement or scan response from that address. The scan was explicitly disabled afterward. |
|
||||
| Off preserves custom address | `set ble.stealth off` and reboot restored normal discovery at the same custom address. A live connection returned stealth off and the unchanged custom policy. |
|
||||
| Saved random plus stealth | `F2:2E:E5:65:18:5E` passed first pairing and held commands. After reboot, it retained its address, saved-random policy and stealth bond; protected reconnect needed no PIN. Uptime decreased from 55 to 30 seconds. |
|
||||
| Random-after-connect plus stealth | Selecting this policy kept stealth on and reopened pairing at `C7:FD:29:44:4F:3E`. Pairing produced both bonded-peer-only status and an armed next-boot rotation. Reboot changed the address to `F5:F3:89:B7:B5:26`, where normal discovery, fresh pairing and protected commands passed with stealth still on. |
|
||||
| Random-every-boot plus stealth | The first boot used `F9:B2:03:DD:CC:43`, completed fresh pairing, and answered commands with stealth on. The next commanded reboot advertised `F8:C7:C0:96:1E:00`; fresh PIN pairing and protected commands passed again with the rotating policy and stealth on. |
|
||||
| Restore the bench target | Setting the MAC to `default` kept stealth on until the separate `set bluetooth.stealth off`. After reboot, factory-address discovery, fresh PIN pairing and protected commands confirmed default MAC and stealth off. The obsolete factory-address host bond was replaced during this final pairing. |
|
||||
|
||||
These checks used actual encrypted BLE connections at MTU 247, not scan
|
||||
sightings alone. Two first-pair harness attempts stopped before connecting
|
||||
because their unpaired BlueZ discovery object no longer existed. A fresh
|
||||
bounded scan immediately followed by pairing was used for retries; this was
|
||||
not a firmware GATT command failure. Reboot-write/disconnect races were
|
||||
accepted only with an observed disconnect and subsequent live device checks.
|
||||
|
||||
Local automated verification passed 39 native cases, 32 source/persistence/UI
|
||||
contracts, and 11 real-browser cases. Supported-wrapper builds passed for
|
||||
XIAO nRF52, XIAO C3, M5Stack Unit C6L and XIAO S3 WIO Full Companion, covering
|
||||
nRF52, ESP32 NimBLE and ESP32 Bluedroid integration. The contracts and browser
|
||||
suite are included in the unit-test workflow; a browser must be available for
|
||||
the optional browser suite to execute instead of skip.
|
||||
|
||||
Exact artifacts, build logs, command logs and the private packet capture are
|
||||
in `/home/mesh/bluetooth-stealth-flag.ApLB8o/` on the VM. The remaining hardware
|
||||
gates above still apply; these results do not qualify every board or phone.
|
||||
|
||||
At 23:42 PDT the XIAO was disconnected in normal discoverable mode. The Pi
|
||||
retained the same boot ID throughout, remained responsive, and had no new
|
||||
kernel messages in the streamed log. Journald was active, the next-boot
|
||||
volatile-log restoration service was still enabled, and disk journals used
|
||||
about 5.9 MiB. No USB reset, Pi reboot, or target battery removal was used.
|
||||
@@ -12,7 +12,7 @@ exit code when the tool has a documented false-success mode.
|
||||
|
||||
## Bluetooth stealth qualification - 2026-09-07
|
||||
|
||||
The [XIAO stealth report](../hardware_validation_bluetooth_stealth_2026-09-07.md)
|
||||
The [XIAO stealth report](hardware_validation_bluetooth_stealth_2026-09-07.md)
|
||||
records exact artifacts for the initial implementation and independent-flag
|
||||
follow-up. The flag run covers custom/saved-random pairing and bonded reboot
|
||||
reconnect, idempotent `on`, `off` preserving the MAC, directed advertising,
|
||||
|
||||
@@ -52,6 +52,7 @@ Completed validation reports, experiments, and test guides are collected here. O
|
||||
- [Stock nRF52 bootloader version audit](bootloader_version_stock_audit.md)
|
||||
- [CLI setting dispatch audit](cli_settings_audit.md)
|
||||
- [Hardware validation checklist](hardware_validation_checklist.md)
|
||||
- [XIAO Bluetooth stealth hardware validation — 2026-09-07](hardware_validation_bluetooth_stealth_2026-09-07.md)
|
||||
- [Shrinking the per-connection TLS footprint on non-PSRAM observers](mbedtls-tls-footprint.md)
|
||||
- [nRF52 Companion automatic ExtraFS recovery](nrf52_companion_storage_recovery.md)
|
||||
- [PR #7 review and validation](pr7_review_validation.md)
|
||||
|
||||
@@ -7,7 +7,7 @@ release matrix and firmware picker.
|
||||
See the [2026-09-08 hardware results](nimble_companion_trial_results.md) for
|
||||
measured RAM, MAC-policy checks, WiFi updates, and remaining qualification.
|
||||
|
||||
The [contact-cache guide](../companion_contact_cache.md) adds six optional
|
||||
The [contact-cache guide](../research/companion_contact_cache.md) adds six optional
|
||||
350-contact trials for ESP32 boards without PSRAM, including Heltec V3.
|
||||
Their [qualification results](companion_contact_cache_results.md) include
|
||||
linked RAM margins, V4/RAK hardware tests and the mOTA queue policy.
|
||||
|
||||
+69
@@ -26,6 +26,75 @@ not_in_nav: |
|
||||
/releases/1.17.1.5.md
|
||||
/releases/1.17.1.6.md
|
||||
/releases/1.17.1.7.md
|
||||
/companion_contact_cache.md
|
||||
/esp32_memory_budget.md
|
||||
/firmware_memory_budget.md
|
||||
/hardware_validation_bluetooth_stealth_2026-09-07.md
|
||||
/nrf52_bluetooth_debug.md
|
||||
/rak3401_1.17.1.7_legacy24.md
|
||||
/rak3401_mota_chain.md
|
||||
/research/*.md
|
||||
/test-results/*.md
|
||||
/test-results/preview-1.17.1.8/*.md
|
||||
/old-releases/*.md
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Firmware and tools:
|
||||
- firmware_picker.md
|
||||
- notifications.md
|
||||
- preset_test.md
|
||||
- filter_tool.md
|
||||
- telemetry_decoder.md
|
||||
- management_decoder.md
|
||||
- Documentation:
|
||||
- Getting started and features:
|
||||
- faq.md
|
||||
- role_feature_switches.md
|
||||
- full_companion_features.md
|
||||
- companion_radio_full.md
|
||||
- full_companion_usb_switcher.md
|
||||
- companion_offline_queue.md
|
||||
- halo_keymind_settings.md
|
||||
- Firmware updates and partitions:
|
||||
- ota_user_guide.md
|
||||
- ota_easy.md
|
||||
- lora_ota_automation.md
|
||||
- ota_nrf52_bootloader_update.md
|
||||
- ota_nrf52_qspi.md
|
||||
- ota_meshtower_v2_sdcard.md
|
||||
- esp32_partition_catalog.md
|
||||
- esp32_wifi_partition_migration.md
|
||||
- heltec_v4_wifi_partition_migration.md
|
||||
- host_cli_service.md
|
||||
- Radio, networking, and power:
|
||||
- radio_profiles.md
|
||||
- carrier_wave.md
|
||||
- espnow_bridge_setup.md
|
||||
- WiFi.md
|
||||
- flood_filtering.md
|
||||
- gps_tracking.md
|
||||
- nrf52_power_management.md
|
||||
- CLI and protocol reference:
|
||||
- cli_commands.md
|
||||
- cli_build_matrix.md
|
||||
- cli_command_availability.md
|
||||
- terminal_chat_cli.md
|
||||
- companion_protocol.md
|
||||
- kiss_modem_protocol.md
|
||||
- packet_format.md
|
||||
- payloads.md
|
||||
- ota_protocol.md
|
||||
- management_reports.md
|
||||
- stats_binary_frames.md
|
||||
- number_allocations.md
|
||||
- qr_codes.md
|
||||
- docs.md
|
||||
- Research: research/index.md
|
||||
- Test Results: test-results/index.md
|
||||
- Releases:
|
||||
- Current release: releases/1.17.1.8.md
|
||||
- Old Releases: old-releases/index.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
|
||||
Reference in new issue
Block a user