Organize test results and historical release docs

This commit is contained in:
mikecarper committed 2026-10-01 11:32:23 -07:00
1 parent d44910865c
commit e96918cd91
130 files changed
+8975 -7736

No files matched your search

+3 -171
View File
@@ -1,173 +1,5 @@
# Stock nRF52 bootloader version audit
# Document moved
Checked **2026-09-29**, against the 40 nRF52 variant families resolving to 26
board configurations in this checkout. This concerns `get bootloader.ver`, not
bootloader installation or OTA eligibility. No devices were flashed, and no
features, storage sizes, or partitions were changed.
<script>window.location.replace(new URL("../test-results/bootloader_version_stock_audit/" + window.location.hash, window.location.href));</script>
## Result
The real firmware reader successfully reads **26 distinct manufacturer-published
bootloader images** from 32 artifact records, including older RAK revisions,
current and historical LilyGO images, and vendor HEX, UF2 and DFU ZIP packages.
Repeated copies and identical normalized bootloader images are counted once.
These are publicly supplied stock/recovery images, **not proof that every
manufacturing batch shipped identical bytes**. No untouched live unit was read
in this audit.
One format gap was fixed: Seeed's T1000-E **LoRaWAN development-board** bootloader
declares `UF2 Bootloader 1.00`. The reader now accepts that two-component version
and returns `1.00` unchanged. It does not invent `1.00.0`. That development image
is separate from Seeed's Meshtastic/MeshCore recovery image, which reports
`0.9.1-5-g488711a` and already worked.
All other tested images already expose a readable UF2 bootloader record.
Generic bare dotted-number searches remain deliberately unsupported: TinyUSB,
nrfx, and SoftDevice can embed their own versions inside the same bootloader.
The bounded standalone fallback still requires the explicit OTAFIX version
shape. Ambiguous records, invalid vectors, and corrupt canonical OTAFIX metadata
retain the existing rejection rules.
## Published images actually tested
The exact source URLs, download hashes, normalized-image hashes, extraction
layout and expected strings are pinned in
[`stock_images.json`](../test/fixtures/nrf52_bootloader_version/stock_images.json).
| Manufacturer image family | Version captured from the image |
| --- | --- |
| RAK4631/RAK4630 Arduino bootloader, historical through current | `0.3.2-109-gd6b28e6`, `0.4.1-13-g5e6690e-dirty`, `0.4.2`, `0.4.3`, `0.4.4` |
| Heltec HT-n5262, SDK and T114 vendor recovery HEX | `0.9.0-2-g836c8dc-dirty` |
| LilyGO T-Echo | `0.6.1-2-g1224915` |
| LilyGO T-Echo Lite, Card, T-Impulse Plus (including historical builds) | `0.9.2-dirty` |
| Seeed XIAO nRF52840 and Sense | `0.6.1` |
| Seeed XIAO nRF52840 Plus and Sense Plus | `0.9.2-29-g6a9a6a3` |
| Seeed Wio Tracker 1110 SDK/conversion bootloader | `0.6.2-26-g949425a-dirty` |
| Seeed Wio Tracker L1 vendor recovery ZIP | `0.7.0-22-g277a0c8` |
| Seeed T1000-E Meshtastic/MeshCore recovery ZIP | `0.9.1-5-g488711a` |
| Seeed T1000-E LoRaWAN development-board HEX | `1.00` (newly supported) |
| Seeed MeshTracker X1 recovery ZIP | `0.10.0-18-gb93789f` |
| MTools Tec MeshTiny vendor bootloader UF2 | `0.4.5` |
| BQ/Unit Engineering Nano G2 Ultra vendor HEX ZIP | `0.6.4-dirty` |
| Muzi Works R1 Neo release HEX | `0.9.2-31-g990aa7f-dirty` |
**Use the embedded version, not the filename.** For example, Seeed's XIAO file
named `...bootloader-0.6.2...hex` reports `0.6.1`, and its L1 ZIP named
`...bootloader-0.10.0...zip` reports `0.7.0-22-g277a0c8`. These are actual tested
bytes, not renamed or inferred release numbers.
All these normalized images use the Adafruit nRF52840 `F4000..FE000` bootloader
window. HEX/UF2 address records identify the bytes. The boot-only Seeed DFU
packages omit MBR/UICR address words; their bootloader binary is extracted using
the manifest's `sd_size`/`bl_size` and the
[Adafruit nRF52840 linker layout](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/linker/nrf52840.ld).
The runtime reader independently selects the installed MBR/UICR boot address;
this audit does not hardcode that layout into the firmware getter.
## Coverage of every variant family
- **Image verified:** a published bootloader for that named product/module was
tested. Factory batch identification still requires reading the actual unit.
- **Representative only:** a related module/SDK bootloader was tested; its exact
factory image was not obtained. Shared build configuration alone does not
establish identical factory bootloaders.
- **Unverified:** no accessible factory bootloader image was obtained. This is
not a demonstrated reader failure.
| Variant family | Status | Evidence or remaining gap |
| --- | --- | --- |
| `gat562_30s_mesh_kit` | Unverified | GAT documentation/firmware repository checked; no product-specific stock bootloader found. |
| `gat562_mesh_evb_pro` | Unverified | Same GAT gap; RAK board configuration is not factory-image proof. |
| `gat562_mesh_tracker_pro` | Unverified | Same GAT gap; custom vendor/OTAFIX builds also exist. |
| `gat562_mesh_watch13` | Unverified | Same GAT gap. |
| `heltec_mesh_solar` | Representative only | HT-n5262 SDK image passes; Solar downloads found application images, not a board-specific stock bootloader. |
| `heltec_t096` | Representative only | HT-n5262 SDK image passes; T096 factory revision not established. |
| `heltec_t1` | Representative only | SDK image passes; vendor F&T archive contains application-only UF2s. |
| `heltec_t114` | Image verified | Both SDK HEX and T114-specific vendor recovery HEX pass. |
| `heltec_tower_v2` | Representative only | SDK image passes; vendor matching-firmware archive contains applications, not the stock bootloader. |
| `ikoka_handheld_nrf` | Representative only | XIAO board configuration; XIAO module images pass, exact installed factory image not obtained. |
| `ikoka_nano_nrf` | Representative only | Same XIAO representative coverage. |
| `ikoka_stick_nrf` | Representative only | Same XIAO representative coverage. |
| `keepteen_lt1` | Unverified | Manufacturer product/download search did not yield a factory bootloader binary. |
| `lilygo_t_impulse_plus` | Image verified | Both historical and newer vendor bootloader HEX images pass. |
| `lilygo_techo` | Image verified | Vendor T-Echo bootloader passes. |
| `lilygo_techo_card` | Image verified | Three distinct vendor historical/current bootloader images pass. NFC UICR repair application excluded. |
| `lilygo_techo_lite` | Image verified | Vendor bootloader HEX passes. |
| `mesh_pocket` | Representative only | HT-n5262 SDK image passes; exact Pocket factory revision not established. |
| `meshtiny` | Image verified | Direct manufacturer bootloader UF2 passes; newer ADV/factory revisions require their actual image. |
| `meshtracker_x1` | Image verified | Seeed product-specific recovery ZIP passes. |
| `minewsemi_me25ls01` | Unverified | Manufacturer module page lists firmware as none; no stock bootloader binary found. OEM/dev-kit installations can differ. |
| `muzi_base` | Unverified | Manufacturer publishes bootloader source, but its release assets found were for R1 Neo, not BASE. |
| `muziworks_r1_neo` | Image verified | Manufacturer R1 Neo release HEX passes; do not substitute a generic RAK version. |
| `nano_g2_ultra` | Image verified | Manufacturer's preinstalled-bootloader recovery HEX ZIP passes. |
| `promicro` | Unverified | Nologo documents a Nice!Nano UF2 drive; its linked download requires Baidu access. Generic clones need separate factory images. |
| `rak3401` | Representative only | RAK4631 Arduino images pass; RAK3401-specific untouched image not obtained. |
| `rak4631` | Image verified | Five distinct published stock Arduino bootloader revisions pass. RAK4631-R/RUI3 is a different bootloader family. |
| `rak_wismesh_tag` | Representative only | RAK core images pass; exact Tag factory image not obtained. |
| `sensecap_solar` | Representative only | Seeed documentation identifies XIAO DFU; XIAO images pass, exact Solar factory image not obtained. |
| `t1000-e` | Image verified | Both vendor recovery ZIP and separate LoRaWAN development-board HEX pass. |
| `thinknode_m1` | Unverified | Manufacturer repository checked; no factory bootloader binary found. |
| `thinknode_m3` | Unverified | Published factory UF2 is application-only (`26000..52BFF`), not a bootloader image. |
| `thinknode_m4` | Unverified | Published factory UF2 is application-only (`26000..521FF`). |
| `thinknode_m6` | Unverified | Published factory UF2 is application-only (`26000..46CFF`). |
| `thinknode_m8` | Unverified | No accessible product-specific factory bootloader binary found. |
| `wio-tracker-l1` | Image verified | Seeed's L1 recovery ZIP passes. |
| `wio-tracker-l1-1w` | Representative only | L1 recovery image passes; exact 1W/ADV factory revision not established. |
| `wio-tracker-l1-eink` | Representative only | L1 recovery image passes; exact e-ink factory revision not established. |
| `wio_wm1110` | Representative only | Published Wio Tracker 1110 and XIAO SDK images pass; the variant's XIAO config does not prove the shipped module's image. |
| `xiao_nrf52` | Image verified | XIAO, Sense, Plus and Sense Plus vendor images pass. |
Representative/unverified products use the same shared reader and should report
a version **if their installed bootloader exposes one of the recognized records**.
There is no evidence yet for claiming an exact version or success on every
untouched unit. ESP32, RP2040 and STM32 are outside this nRF52 flash reader;
their getters were not changed and this report does not claim stock-version
capture on those architectures.
## Reproduce the artifact tests
Normal tests are offline and include all observed version strings, malformed and
conflicting versions, dependency-version false positives, region/vector bounds,
and the existing OTAFIX metadata/CRC/fallback cases:
```sh
python3 test/test_nrf52_bootloader_version.py -v
```
To replay the real images, download each `url` in `stock_images.json` into a
cache directory using its `file` name, then run:
```sh
MESHCORE_STOCK_BOOTLOADER_CACHE=/absolute/path/to/cache \
python3 test/test_nrf52_bootloader_version.py -v
```
This checks the exact download SHA-256, extracts only the bootloader window,
checks the normalized-image SHA-256, and invokes the actual C++ reader with an
exact expected version. It never downloads automatically or flashes a device.
The ordinary regression cases cover every captured format even without the
optional cached artifacts. RAK4631 and T1000-E repeater firmware builds passed.
## What is needed to close the remaining gaps
For each untouched unit, collect its DFU drive's **`INFO_UF2.TXT`** if present.
To prove the application's flash reader can capture that exact revision, also
obtain a read-only bootloader dump plus the MBR word at `0xFF8`, UICR `NRFFW[0]`,
and flash geometry (or a full vendor HEX including those address records).
Keep the bootloader unchanged while collecting this evidence. Only the identified
bootloader region is needed; private application keys/configuration are not.
If no version bytes or startup register value exist, report `unknown`; a regex
cannot recover a version that was never embedded.
## Manufacturer sources
- [RAK stock bootloader catalog](https://github.com/RAKWireless/WisBlock/tree/master/bootloader/RAK4630)
- [Heltec SDK](https://github.com/HelTecAutomation/Heltec_nRF52), [vendor downloads](https://resource.heltec.cn/download)
- LilyGO [T-Echo](https://github.com/Xinyuan-LilyGO/T-Echo), [T-Echo Lite](https://github.com/Xinyuan-LilyGO/T-Echo-Lite), [T-Echo Card](https://github.com/Xinyuan-LilyGO/T-Echo-Card), [T-Impulse Plus](https://github.com/Xinyuan-LilyGO/T-Impulse-Plus)
- [Seeed Arduino bootloader catalog](https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/tree/master/bootloader), [T1000-E recovery](https://wiki.seeedstudio.com/sensecap_t1000_e/), [T1000-E development board](https://github.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board), [MeshTracker X1 recovery](https://wiki.seeedstudio.com/sensecap_meshtracker_x1_meshcore/), [Wio L1 bootloader guidance](https://wiki.seeedstudio.com/get_started_with_other_mesh_firmware/)
- [MeshTiny manufacturer bootloader download](https://shop.mtoolstec.com/bootloader-update-on-meshtiny.html)
- [Nano G2 Ultra manufacturer bootloader documentation](https://wiki.bqvoy.com/en/meshtastic/nano-g2-ultra)
- [Muzi bootloader source/releases](https://github.com/muzi-works/Adafruit_nRF52_Bootloader), [BASE source guidance](https://muzi.works/pages/base-system-downloads)
- [GAT manufacturer documentation](https://github.com/gat-iot/GAT562-family), [Elecrow manufacturer repositories](https://github.com/Elecrow-RD), [Keepteen](https://www.keepteen.com/)
- [Nologo ProMicro documentation](https://www.nologo.tech/product/otherboard/NRF52840.html)
- [Minewsemi ME25LS01 module specifications](https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01)
This document is now in [Test Results](test-results/bootloader_version_stock_audit.md).
+3 -61
View File
@@ -1,63 +1,5 @@
# SX1262 channel-scanning hardware results
# Document moved
Hardware test on September 12, 2026 (America/Los_Angeles). Heltec V4 transmitter; XIAO ESP32-S3 with Wio SX1262 receiver. Both used standalone diagnostic firmware.
<script>window.location.replace(new URL("../test-results/cad_scan_validation/" + window.location.hash, window.location.href));</script>
The best tested scan setting was **four channels with four-symbol RX-preamble windows**. Three-symbol windows were unreliable even in the single-channel re-arm control. In the longer four-symbol runs, four channels received 80/80 packets, five received 79/80, and six received 52/60. These are measured limits of this implementation and test setup, not universal SX1262 channel-count limits.
## Configuration and counting
SF5, 125 kHz, coding rate 4/5, explicit header, payload CRC, and 32 preamble symbols. The programmed preamble is 8.192 ms. Channels start at 909.5 MHz and increase in 1 MHz steps; tests use the first N channels, up to 920.5 MHz. Test packets are transmitted individually with varied timing relative to the scanner. There are no overlapping transmitters in this experiment.
A success requires a CRC-valid packet with the expected sequence, channel, length, and test payload. Every included transmission has its own successful transmitter acknowledgement; delayed serial acknowledgements are recovered by sequence, without retransmitting. The one exploratory RX scan trial with shifted acknowledgements is excluded.
## Completed trials
| Trial | Method/window | Channels | Payload bytes | Received / sent | Mean complete scan |
| --- | --- | ---: | ---: | ---: | ---: |
| baseline-fixed | Fixed-channel RX | 4 | 64 | 16 / 16 | — |
| cad-standard | CAD + software RX | 4 | 64 | 7 / 32 | 14.469 ms |
| cad-auto | CAD + automatic RX | 4 | 64 | 16 / 32 | 14.468 ms |
| rxfast-4 | rxfast 4 | 4 | 64 | 32 / 32 | 6.100 ms |
| rxfast-4-maxpayload | rxfast 4 | 4 | 255 | 35 / 35 | 6.098 ms |
| matrix-cad1-1 | cad 1 | 1 | 64 | 7 / 8 | 2.643 ms |
| matrix-cad1-10 | cad 1 | 10 | 64 | 2 / 20 | 28.389 ms |
| matrix-cad1-11 | cad 1 | 11 | 64 | 5 / 22 | 31.297 ms |
| matrix-cad1-12 | cad 1 | 12 | 64 | 3 / 24 | 34.145 ms |
| matrix-cad2-10 | cad 2 | 10 | 64 | 3 / 40 | 31.040 ms |
| matrix-fixed-12 | Fixed-channel RX | 12 | 64 | 24 / 24 | — |
| matrix-rx1-10 | rxfast 1 | 10 | 64 | 0 / 20 | 7.555 ms |
| matrix-rx1-11 | rxfast 1 | 11 | 64 | 0 / 22 | 8.316 ms |
| matrix-rx1-12 | rxfast 1 | 12 | 64 | 0 / 24 | 9.072 ms |
| matrix-rx2-10 | rxfast 2 | 10 | 64 | 0 / 40 | 10.133 ms |
| matrix-rx3-7 | rxfast 3 | 7 | 64 | 2 / 35 | 8.892 ms |
| matrix-rx3-8 | rxfast 3 | 8 | 64 | 1 / 40 | 10.160 ms |
| matrix-rx4-5 | rxfast 4 | 5 | 64 | 39 / 40 | 7.630 ms |
| boundary-rx3-1 | rxfast 3 | 1 | 64 | 4 / 24 | 1.268 ms |
| boundary-rx3-4 | rxfast 3 | 4 | 64 | 2 / 24 | 5.080 ms |
| boundary-rx3-5 | rxfast 3 | 5 | 64 | 0 / 25 | 6.350 ms |
| boundary-rx3-6 | rxfast 3 | 6 | 64 | 2 / 30 | 7.621 ms |
| boundary-rx4-4 | rxfast 4 | 4 | 64 | 80 / 80 | 6.104 ms |
| boundary-rx4-5 | rxfast 4 | 5 | 64 | 79 / 80 | 7.630 ms |
| boundary-rx4-6 | rxfast 4 | 6 | 64 | 52 / 60 | 9.158 ms |
`rxfast N` means a continuous-RX preamble observation window of N symbols, with minimal reconfiguration between frequencies. It is not CAD. `cad N` uses the SX1262 CAD detector with N symbols and automatic handoff to RX. CAD has no three-symbol mode.
Scan times describe uninterrupted sweeps. When reception starts, the radio stays on that frequency through the packet and cannot hear packets on the other frequencies. Serial status requests can add occasional timing outliers.
The maximum-payload trial ended after 35 confirmed transmissions when a serial acknowledgement was delayed. Its last TX acknowledgement and matching RX were subsequently recovered from the serial logs; all 35 were received. It is not a completed 64-packet trial.
## Interpretation and limits
The initial four-channel fast-RX test received 32/32 64-byte packets and 35/35 255-byte packets. That is a successful strong-signal bench result, not a guarantee of zero loss. Ordinary four-symbol CAD took approximately 14.47 ms to cover four frequencies and was unreliable with an 8.192 ms preamble.
Reducing the observation window also reduces the time available for the modem to recognize a preamble. Dividing preamble duration by scan duration alone overestimates the number of usable channels. One- and two-symbol RX windows produced no preamble detections in the tested high-channel-count cases.
The three-symbol single-channel control repeatedly re-arms RX, just as a scan would, and received only 4/24 packets. It is distinct from continuous fixed-channel reception, which passed its baseline. This isolates a detection-window limitation even without a long multi-channel sweep.
CAD detection peak 18 and minimum 10 were held constant. These are the datasheet four-symbol SF5 thresholds; the one- and two-symbol CAD tests do not establish optimized thresholds or weak-signal sensitivity. CAD activity followed by no decoded packet can be a late packet detection, not necessarily a noise false positive.
Received signals were strong. These tests do not establish weak-signal range, adjacent-channel rejection, simultaneous-packet capacity, production firmware scheduling performance, power consumption, FCC compliance, or months of uptime.
## Reproduction and evidence
See [the test harness](../tools/hil/cad_scan/README.md). Compact validated results, timing counters, and final firmware/source hashes are in [cad_scan_results.json](../tools/hil/cad_scan/cad_scan_results.json). Detailed sequence-level logs and private lab transport helpers are retained locally under `out/cad-scan/`.
This document is now in [Test Results](test-results/cad_scan_validation.md).
+1 -1
View File
@@ -56,5 +56,5 @@ ordinary LoRa packets to verify both RX and TX recovery.
The reproducible bench receiver that provides this live reading is
`tools/hil/cw_observer.cpp`, built with `tools/hil/cw_observer.ini` on a
separate RAK4631. It never transmits on its own.
See the [hardware validation](carrier_wave_validation.md) for measured results,
See the [hardware validation](test-results/carrier_wave_validation.md) for measured results,
reproduction steps, and the tested preamble settings.
+3 -110
View File
@@ -1,112 +1,5 @@
# CW and CW2 validation
# Document moved
Tested on the MercerWoodMesh lab Pi on September 14, 2026 (Pacific), using
base commit `9241ccc253ad2f8ac2c9a2fe756f59ba2f52505d` plus the CW changes.
Test images are marked `v1.17.1.6-cw-test-9241ccc2`; they are development images.
<script>window.location.replace(new URL("../test-results/carrier_wave_validation/" + window.location.hash, window.location.href));</script>
A separate RAK4631 sampled live received signal strength (RSSI) about every
10 ms, without needing to decode a packet.
The transmitting boards ran the Full V4 repeater and Full T1000-E Companion
builds. Both were configured for −9 dBm chip TX power, with profiles at
909.5 and 919.5 MHz, BW125, SF7, CR5, and 32-symbol preambles.
| Check | V4 / SX1262 | T1000-E / LR1110 |
| --- | --- | --- |
| `cw on 0.25` measured duration | 0.25 s | 0.25 s |
| `cw on 1.25` measured duration | 1.25 s | 1.25 s |
| Manual stop approximately 1 s into a 5 s request | 1.01 s | 1.01 s |
| `cw on 1.25` after enabling RXPS | 1.26 s | 1.25 s |
| `cw2 on 1.25` on the second frequency | 1.25 s | 1.25 s |
| Primary-frequency RSSI change while CW2 transmits | 0 dB | 0 dB |
| Paired RX and TX recovery checks | 5/5 passed | 5/5 passed |
The observer saw roughly −56 to −57 dBm during V4 carriers and −67 to
−71 dBm during T1000-E carriers, against a −112 to −113 dBm baseline.
Readings returned to baseline after timeout and manual stop. These are
received signal readings, **not calibrated transmitter output measurements**.
RX recovery used CRC-valid LoRa test payloads and the DUT's receive counter.
TX recovery used each DUT's MeshCore advertisement, received with valid CRC
by the observer. Both DUTs finished in RX, with CW and radio2 disabled and
no outbound packet pending. The observer has no autonomous transmission.
## Software checks
- 1,436 native tests passed, including new checks that CW holds queued packets,
services its deadline, and prevents transmission in the same loop where a
received command starts CW.
- Compiled production-method tests cover fractional input, malformed durations,
bounds, timer rollover, busy/IRQ rejection, RXPS cleanup, board TX/RX callbacks,
profile selection and replacement, failed TX entry, stop retries, and failed
RX restart. An ESP32 sleep test covers a carrier with USB disconnected.
- Full V4 repeater, Full T1000-E Companion, and ordinary STM32
`RAK_3x72_companion_radio_usb` builds passed. The STM32 build is a compilation
check, not an RF test of that board.
- An additional existing OTA sleep test still expects the old
`ota::ota_ctx().apply_pending` spelling. It fails against unchanged HEAD
`src/Mesh.cpp` and test-file contents; the actual implementation uses
`ota_context_if_active()`. This is separate from the CW changes.
## SX1276 follow-up
The SX1276 carrier feature from [PR #11](https://github.com/mikecarper/MeshCore/pull/11)
is included alongside SX1262 and LR1110 support. It switches to FSK with zero
deviation, applies the board's calibrated TX power, and rebuilds the runtime
LoRa configuration when stopped. External-PA settings pass through the same
DAC/drive-power mapping used for packets, including RFO selection.
The normal TX power command can adjust a running carrier for meter sweeps.
RadioLib's SX1276 power setter enters standby, so the carrier is restarted
afterward without extending the deadline. Failed power changes stop the
carrier and restore the previous cached power.
Follow-up checks use base `9dd9881d520b4b791600a00fff1f8e1436921054` plus the
SX1276 changes. Production-method regression tests cover both profiles,
zero deviation, external-PA calibration, RFO, power sweeps, timeout, failed
FSK entry, failed restoration, retry guards, and return to LoRa. The native
CW paths and existing radio/profile/sleep/PA checks pass as well.
Both `GEPRC_Linkflow_900_repeater` (external PA) and
`Heltec_v2_companion_radio_usb` firmware builds pass, including size checks.
SX1276 RF output has not been measured in this follow-up. The physical RF
results above apply to SX1262 and LR1110.
## Bench qualifications
The first CW2 request arrived before radio2's existing delayed activation.
The harness now polls `get radio2.status` for the active mode before testing.
An initial V4 dual-profile packet probe used a 120-symbol preamble against
32-symbol profiles. A comparison around another CW2 burst received **0/8
before and 1/8 afterward** with that mismatched probe, versus **8/8 before
and 8/8 afterward** with a matching 32-symbol probe. The configured preamble
also sets the driver's preamble-detection hold timeout, so a much longer
incoming preamble can outlast that hold and be interrupted by scanning.
The final dual-profile recovery checks use matched preambles. This finding
does not establish general packet-loss performance for every scan combination.
These checks establish RF activity, selected-frequency behavior, and recovery
on the tested boards. They do not measure spectral purity, frequency error,
SWR, calibrated power, or months of uptime. Carrier timeouts are serviced by
the main loop and depend on that loop remaining responsive.
## Reproduction
Append `tools/hil/cw_observer.ini` to `extra_configs` in `platformio.local.ini`,
preserving existing entries, and build `pio run -e cw_observer_rak4631`.
Flash that application onto a separate lab RAK4631. Do not run PlatformIO
concurrently with another build or upload in the checkout.
Configure the lab DUT's primary profile as above, disable radio2, set TX power
to −9 dBm, and reboot to apply saved primary settings. Then run:
```sh
python3 tools/hil/carrier_wave.py --dut /dev/serial/by-id/DUT \
--observer /dev/serial/by-id/OBSERVER --role repeater --output cw-results.json
```
Use `--role companion` for the T1000-E. The harness changes lab radio2/RXPS
settings and emits short RF bursts and packet probes. It is for test boards.
Compact evidence: [carrier_wave_results.json](../tools/hil/carrier_wave_results.json).
Command guide: [carrier-wave RF checks](carrier_wave.md).
This document is now in [Test Results](test-results/carrier_wave_validation.md).
+5 -5
View File
@@ -1044,7 +1044,7 @@ set usb.logging on
Verify with `get powersaving` (expect `off`) and `get usb.logging` (expect `on`).
Both settings are saved; disabling logging later does not restore power
saving. This works around the [released USB sleep bug](releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled).
saving. This works around the [released USB sleep bug](old-releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled).
nRF52 does not need this ESP32 workaround.
These commands are compiled into ordinary USB-loggable artifacts and every
@@ -1443,7 +1443,7 @@ Station G2/G3 targets default to `off`.
**Notes:**
- `get rxps.wd` reports the radio watchdogs' soft and hard recovery counts.
See [receive calibration and recovery](radio_receive_calibration.md) for
See [receive calibration and recovery](test-results/radio_receive_calibration.md) for
noise sampling, CAD handling, and the continuous-RX mode check.
- `get radio.rxps.config` adds the persisted level and preamble assumption to
the on/off and timing values. Deployment tools use it to restore a
@@ -2154,7 +2154,7 @@ minutes when the terminal is closed, even with a computer attached. The
source fix blocks ESP32 light sleep while a native USB host is attached or
live USB logging is enabled; ordinary CPU idle/yield remains available.
The G3 button also wakes the device for at least two minutes. See the
[G3 release note](releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled)
[G3 release note](old-releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled)
for verification steps and the distinction between the fix and the published
binaries.
@@ -2257,7 +2257,7 @@ The pin number is the Arduino pin number used by that target (the normal GPIO nu
**1.17.1.5 infrastructure bug:** affected Repeater, Room Server, and Sensor
images accept `get path.hash.mode` but report `unknown config` for the setter.
The command was not renamed. The source correction restores the setter;
installed images need an updated build. See the [setting-dispatch audit](cli_settings_audit.md).
installed images need an updated build. See the [setting-dispatch audit](test-results/cli_settings_audit.md).
**Usage:**
- `get path.hash.mode`
@@ -4770,7 +4770,7 @@ displaying a version neither grants OTA capability nor bypasses install checks.
The fix is in MeshCore application firmware; installing another bootloader alone
does not update an older application's version reader.
See the [stock bootloader audit](bootloader_version_stock_audit.md) for tested
See the [stock bootloader audit](test-results/bootloader_version_stock_audit.md) for tested
manufacturer images and the boards still requiring untouched factory dumps.
---
+3 -72
View File
@@ -1,74 +1,5 @@
# CLI setting dispatch audit
# Document moved
The 1.17.1.5 source retained `get path.hash.mode` in `CommonCLI` but lost its
setter when radio preferences moved into `CommonRadioPrefs`. Infrastructure
roles call `CommonCLI` directly, so the shared parser's setter was unreachable.
The correction restores `set path.hash.mode 0|1|2` there, validates the value,
and saves accepted changes. Missing/invalid values report a value error and
leave the setting unchanged.
<script>window.location.replace(new URL("../test-results/cli_settings_audit/" + window.location.hash, window.location.href));</script>
The other radio setters removed by the original refactor had already been
restored. A feature-guarded check found a second mismatch: `get extra.sf` exposed
stored data on non-LR2021 radios while its setter was compiled out. Both
commands now return `Error: extra.sf requires an LR2021 radio` there. Supported
LR2021 set/get behavior is preserved. These are source corrections; existing
release binaries need rebuilding/updating to receive them.
## Coverage
| Surface | Check |
| --- | --- |
| Common infrastructure CLI | 72 literal setter keys and 84 query keys; every query has a setter or an explicitly reviewed read-only/alternate-command exception |
| MQTT/observer CLI | 33 literal setter keys and 39 query keys, including Wi-Fi, timezone, alerts, display and watchdog settings |
| MQTT slots | All nine writable slot subkeys have query/set coverage; `diag` is intentionally read-only |
| Repeater, Room Server, Sensor | Shared CLI delegation and role-specific query/set coverage |
| Companion | Shared-radio allowlist and parser agreement, persistence wiring, terminal-specific setters, and role query exceptions |
| Radio settings moved by the refactor | All 13 shared setters remain present in infrastructure dispatch |
| Literal command comparisons | Checks key lengths so a setter cannot accidentally compare its value against the key's terminating NUL |
| Feature guards | Preprocesses common and observer handlers for minimal, nRF52 GPS/SD, ESP32 WebConfig, ESP32 MQTT, RS232/GPS, ESP-NOW, and LR2021 profiles before comparing query/set coverage |
Read-only exceptions include runtime connection status, diagnostics, firmware
role, bootloader identity and reset information. Alternate-command exceptions
include the password command and `set battery.alert on <region>`. These
exceptions are explicit in the test, rather than silently allowing all missing
setters. Dynamic/delegated command families and hardware-specific features
continue to use their dedicated tests; the inventory is not a claim that every
build has every feature enabled.
## Executed behavior tests
The native C++ fixture compiles the production get/set dispatch branches and
the real setting bodies for 14 radio-related settings: `radio`, `freq`, `af`,
`dutycycle`, `int.thresh`, `cad`, `radio.rxgain`, `tx`, `rxdelay`,
`agc.reset.interval`, `path.hash.mode`, `multi.acks`, `txdelay`, and
`direct.txdelay`. It runs local and authenticated on-air caller timestamps,
checks round-trip values and save calls, preserves AGC interval rounding, and
checks that rejected hardware applies do not save or change preferences.
Hardware callbacks are mocked; these are not live-radio or flash-durability
tests. The fixture also runs with and without `USE_LR2021`, checking supported
`extra.sf` set/query/clear behavior, invalid values, and the unsupported-radio
error.
The focused hash-mode test checks all valid modes, missing values, negative and
out-of-range values, overflow, malformed numbers, whitespace, and similarly
named unknown keys. Existing transport tests exercise Companion local/framed
and on-air dispatch, local-only secret reads, and browser/stream routing under
AddressSanitizer and UndefinedBehaviorSanitizer on Linux.
Run the CI checks locally with a host C++ compiler (Linux is required for the
existing sanitizer-enabled transport suite):
```text
python3 -B test/test_path_hash_cli.py
python3 -B test/test_cli_settings_contract.py
python3 -B test/test_local_cli_access.py
```
All 14 tests passed on the Linux VM. The workflow runs these commands on future
pushes and pull requests. The new inventory and native radio tests also passed
with the Windows host compiler.
`pio run -e heltec_v4_repeater` passed after both corrections, including the
firmware's flash-size and runtime-RAM checks. The initial audit used host tests
and build checks; subsequent hardware checks are recorded in the
[dual-profile validation](radio_profiles_validation.md).
This document is now in [Test Results](test-results/cli_settings_audit.md).
+2 -2
View File
@@ -6,7 +6,7 @@ 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](companion_contact_cache_results.md) for the
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.
@@ -124,7 +124,7 @@ 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](nimble_companion_trial.md), then build one environment at
[NimBLE trial guide](test-results/nimble_companion_trial.md), then build one environment at
a time. For example:
```sh
+3 -144
View File
@@ -1,146 +1,5 @@
# Contact-cache and NimBLE RAM qualification
# Document moved
Test date: 2026-09-08. Firmware source: `b68aa1c6`.
<script>window.location.replace(new URL("../test-results/companion_contact_cache_results/" + window.location.hash, window.location.href));</script>
All six constrained ESP32 Full Companion trials pass the linked RAM budget
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
retain their established contact limits pending qualification on those boards.
## Linked internal RAM
Every column below uses 350 contacts, 40 channels and 256 normal offline
frames. Values are bytes available for runtime allocations after linking.
The final margin subtracts the existing conservative runtime reservation;
it is not a measurement of live free heap.
| Hardware | Bluedroid | NimBLE | NimBLE + mOTA queue loan | Plus contact caches | Final margin |
| --- | ---: | ---: | ---: | ---: | ---: |
| Heltec V3 | 143,776 | 158,896 | 170,152 | 196,168 | 23,112 |
| Wireless Tracker | 148,432 | 163,536 | 174,792 | 200,824 | 30,838 |
| Tracker V2, FEM on | 133,064 | 148,168 | 159,416 | 185,456 | 15,470 |
| Heltec CT62 | 99,040 | 114,096 | 125,312 | 151,344 | 15,152 |
| XIAO ESP32-C3 | 98,816 | 113,888 | 125,120 | 151,152 | 14,960 |
| Generic ESP-NOW | 99,776 | 114,800 | 126,032 | 152,064 | 15,872 |
NimBLE recovers about 14.7 KiB. Contact caches add about 25.4 KiB on top of
NimBLE and the shared mOTA workspace. NimBLE alone fails the unchanged budget
at this capacity on all six; adding the queue loan passes only on the original
Wireless Tracker. Adding the contact caches passes all six, with 14.6-30.1 KiB
of margin. The same Bluetooth runtime allowance is used in every comparison.
The shared mOTA queue stays at 256 frames normally and retains 128 while
mOTA borrows its workspace, then returns to 256. A queue above 128 must be
synchronized before that workspace can be borrowed; unread frames are not
discarded to start mOTA. PSRAM-backed queues retain their existing policy.
The baseline builds use `b041fc7f`; the final builds use `b68aa1c6`, with the
same ESP32 Arduino 2.0.17 toolchain and NimBLE-Arduino 2.5.1 where applicable.
The CT62, XIAO C3 and Generic ESP-NOW applications fit two 1.5 MiB OTA slots
on 4 MiB flash. Their explicit trial layouts retain 896 KiB SPIFFS and the
existing SPIFFS/coredump addresses. Install the merged image over USB when
changing from the ordinary single-slot Full layout. The other three trials
also retain verified dual WiFi OTA slots. All six remain mOTA senders.
Additional builds check the platform and default-policy branches:
| Build | Available bytes | Reserved runtime bytes | Margin bytes | Policy |
| --- | ---: | ---: | ---: | --- |
| T096 Full, FEM on | 100,076 | 73,728 | 26,348 | nRF52 path/secret caches |
| RAK3401 Full | 99,900 | 50,976 | 48,924 | nRF52 path/secret caches |
| Wireless Paper Full | 187,360 | 152,576 | 34,784 | ESP32 caches, existing Bluedroid |
| V4 Full NimBLE | 263,944 | 173,056 | 90,888 | Complete contacts in PSRAM |
| XIAO nRF52 legacy BLE Companion | 84,368 | 38,688 | 45,680 | Existing inline contact storage |
The V4 test-only forced-cache build also passes its RAM budget. The V4 normally
keeps complete contacts in PSRAM; forcing caches adds internal RAM overhead.
The six-board savings above apply to boards without PSRAM.
## Physical checks
The VM V4 used the test-only cache override. USB tests verified all 350
contacts and all 64 saved path bytes per contact, cold individual lookups,
replacement of the complete path set, and persistence after reboot. A secret
workload contacted 21 distinct synthetic peers, then the first peer twice:
22 calculations, one RAM hit, and no secret-file access under the ESP32 policy.
A sustained send test exposed a false receive watchdog recovery. RX could
occur inside the main loop between two successful transmissions, while the
watchdog observed TX at both loop boundaries. It counted from the start of
the whole burst and could reset a working radio during a later CAD pause.
`468cc3cf` restarts the receive recovery allowance after each successful TX.
The native regression fails before that change and passes afterward; it also
checks that a stuck receiver still gets recovered.
The corrected V4 passed the same 23-datagram workload with zero radio error
flags. It also passed a complete 350-path replacement, another send workload
and a post-reboot path check with zero radio error flags. The replacement
took about 127 seconds: dirty-cache eviction still performs synchronous
SPIFFS contact-file saves, so bulk path updates remain a latency workload
to qualify on the other ESP32 boards. After that run it reported 133,828 bytes free internal heap, a minimum
of 130,976 bytes, and a largest free block of 122,868 bytes. These readings
belong to the forced-cache V4 workload, not the six other boards.
After qualification, the VM V4 was erased to remove synthetic contacts and
installed with the normal PSRAM-backed V4 NimBLE trial from `b68aa1c6`. It is
named `NimBLE-V4-VM`, uses the PIN shown on its display, and has powersaving
off for USB use. That clean setup reported 143,612 bytes of free internal heap
and passed 201 local USB requests with zero radio error flags. The Mercer V4 and
XIAO S3 were left on their previously qualified NimBLE builds.
The Mercer RAK3401 Full Companion verified 350 paths, eviction and persistence.
Its first secret workload produced 21 calculations, one flash hit and one RAM
hit with no skipped saves. After reboot, the same workload produced 22 flash
hits, one RAM hit and **zero calculations**. Both completed with zero radio
error flags. The final `b68aa1c6` RAK image repeated the 350-path and saved-secret
checks with zero radio errors and zero calculations. The RAK was returned to
the published `26303793` repeater
application with its original repeater name and USA Cascade settings; the
Pi's MQTT logger was restarted after the exclusive USB tests.
## Shared-secret timing
| Hardware/filesystem | Successful saved-secret lookup | Key exchange | Selected miss policy |
| --- | ---: | ---: | --- |
| V4 / SPIFFS | 132,427 us | 21,932 us | Recalculate |
| RAK3401 / LittleFS | 7,812-8,789 us | 31,250 us | Read saved entry, calculate on miss |
The SPIFFS timing comes from the earlier forced flash-cache experiment. The
final ESP32 policy does no secret-file I/O. Both platforms reuse the 16 most
recent secrets from RAM. These are elapsed-time measurements, not energy
measurements; timing on other ESP32/nRF52 boards remains to be measured.
## Regression checks and evidence
The contact-cache suite exercises the production cache and persistence code
under address/undefined-behavior sanitizers, with the real Ed25519 key-exchange
implementation. Its cases include 350 paths, identity changes, snapshots,
legacy migration, storage faults, handle exhaustion and packet release when
a cold path cannot be read. The related contract suites passed 62 checks.
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
harness is `tools/hil/contact_cache_serial_stress.py`. It requires exclusive
access to the specified Companion data port.
Local evidence in the qualification workspace:
- `.releases/nimble-six-capacity/control/results.json`: 18 baseline builds.
- `.releases/contact-cache/qualified/control/results.json`: final build results.
- `.releases/contact-cache/qualified/built/`: firmware, ELFs, maps, RAM reports,
partition tables and capability manifests.
- `.releases/contact-cache/qualified/control/`: final V4 USB and flash checks.
- `.releases/contact-cache/control/`: earlier experiments, RAK results and
regression logs, including the watchdog's failing/passing test runs.
Each final RAM report is checked against its ELF hash. All 12 final builds pass
their RAM and OTA capability checks. Physical testing here covers the V4 and
RAK3401; the six constrained ESP32 boards still need pairing, WiFi/BLE/LoRa
coexistence and sustained-load testing on their own hardware. No GitHub release
assets or firmware-picker entries were replaced by these experimental builds.
This document is now in [Test Results](test-results/companion_contact_cache_results.md).
+1 -1
View File
@@ -28,7 +28,7 @@ Meshadventurer SX1262 and SX1268 Full Companion use 16 frames together with 100
contacts and 30 group channels; their ordinary transport-specific images keep
128 frames and 40 channels.
The [nRF52 Full memory correction](releases/1.17.1.5.md#t096-full-companion-bluetooth-and-menu-freeze-report)
The [nRF52 Full memory correction](old-releases/1.17.1.5.md#t096-full-companion-bluetooth-and-menu-freeze-report)
applies queue sharing to every nRF52 Full Companion. It keeps 256 frames
normally, retaining each board's contacts, channels, and all
Full transports. The upper 128 slots temporarily hold the mOTA context when
+1 -1
View File
@@ -398,7 +398,7 @@ defaults and full-queue behavior.
Every corrected nRF52 Full profile keeps **256 offline frames normally** and
temporarily lends 128 slots to mOTA to leave room for
Bluetooth tasks, displays and UI allocations. Queue sharing preserves the
board's contacts, channels, and USB/Bluetooth mOTA sending. See the [memory correction](releases/1.17.1.5.md#t096-full-companion-bluetooth-and-menu-freeze-report).
board's contacts, channels, and USB/Bluetooth mOTA sending. See the [memory correction](old-releases/1.17.1.5.md#t096-full-companion-bluetooth-and-menu-freeze-report).
If an explicit OTA source, discovery, download, install, or temporary-radio
request needs that workspace while more than 128 unread frames are queued, the
oldest volatile frames are consumed until 128 remain. The command reports the
+3 -66
View File
@@ -1,68 +1,5 @@
# USB Companion ASCII default validation
# Document moved
The updated [1.17.1.6 Companion release](https://github.com/mikecarper/MeshCore/releases/tag/v1.17.1.6-halo-keymind-cascade-dev-306feebe)
covers 103 USB-enabled profiles: 52 ESP32 Full, 43 nRF52 Full, four RP2040 USB,
and four STM32 USB. The BLE-only WM1110 image has no USB interface and is outside
this change.
<script>window.location.replace(new URL("../test-results/companion_usb_ascii_validation/" + window.location.hash, window.location.href));</script>
## Behavior and cause
Ordinary USB Companions previously started in Binary mode. Full Companions
started in ASCII, but host-session cleanup could select or retain Binary mode
for the next connection.
All USB Companions now select ASCII before framed dispatch begins. An
observable USB session boundary cancels old USB operations and clears partial
input, queued protocol output, frame ownership, and mOTA attachment before restoring
ASCII. A network terminal can defer that restoration; a new USB binary client
that connects during the deferral keeps its chosen mode. BLE and network
Companion transports keep their existing binary protocol.
A complete framed app command still selects Binary mode automatically. An
incomplete initial probe returns to ASCII after one second. Explicit terminal
start/stop controls remain available.
Session detection depends on the hardware. Native USB with DTR can detect a
terminal closing. ESP32 hardware USB Serial/JTAG observes bus resets and host
loss, but cannot reliably observe a program closing its handle. UART bridges
usually cannot observe that close either. Idle time never forces an active
binary app back into ASCII; use the start token on a port that remains Binary.
See the [USB mode guide](full_companion_usb_switcher.md).
## Automated validation
- The real firmware startup and reset functions are compiled and executed for
nRF52, ESP32 TinyUSB, ESP32 hardware USB, and RP2040, with and without the
Full macro. Cases include reconnecting from Binary or ASCII, delayed cleanup,
mOTA cancellation, network ownership, and a new binary client during deferral.
- 55 native serial-mode/flow-control tests and 11 terminal-session/diagnostic
tests passed locally, along with 49 USB Python tests and the picker tests.
- STM32 number formatting is tested against an Arduino API without `ltoa`.
CI also compiles an ordinary RAK 3x72 USB Companion with the real core and
RadioLib, so missing packages and GPIO API incompatibilities fail the build.
The 99 ESP32/nRF52/RP2040 profiles use
[`d4a641ff`](https://github.com/mikecarper/MeshCore/commit/d4a641ff8af1b413c8fb96a670282b799be1f6e8).
The four STM32 profiles additionally use the compatible Arduino 2.12 platform
pin and build test in
[`d2c52e4d`](https://github.com/mikecarper/MeshCore/commit/d2c52e4d047c4b1068a1053364d9acaddefa64a1).
Both source revisions passed GitHub checks:
[main repair](https://github.com/mikecarper/MeshCore/actions/runs/34792631229) and
[STM32 repair](https://github.com/mikecarper/MeshCore/actions/runs/34793043594).
All 103 builds passed capability and RAM qualification. Packaging checks bind
each application to its capability and memory manifests, retain the original
profile capabilities and download formats, and verify ESP32 application/merged
image consistency. Replacement publication verifies public SHA-256 digests
before deleting old images and checks that unrelated firmware assets retain
their original hashes. Release manifests, checksum lists, picker links, and
capacity-note source references follow the new binaries.
Publication completed with all 430 uploaded files verified through their public
download URLs. The 412 superseded firmware/manifest files were removed. The
remaining 1,837 assets kept their original hashes, and all five release checksum
lists matched the final GitHub asset digests.
No USB Companion was attached to the local computer for this repair. These
results are compilation and automated protocol tests, not physical USB tests
across the board matrix.
This document is now in [Test Results](test-results/companion_usb_ascii_validation.md).
+3 -115
View File
@@ -1,117 +1,5 @@
# USB Companion client compatibility
# Document moved
The RAK4631 report was reproduced on the MercerWoodMesh Pi with the published
`RAK_4631_companion_radio_full-v1.17.1.6-halo-keymind-cascade-dev-d4a641ff`
image. The fixes described here are in the source and test firmware;
the existing GitHub release assets have not yet been replaced with this fix.
Hardware validation was completed on 2026-09-14 with test builds labeled
`v1.17.1.6-usb-client-test-0e5955f8` (base commit plus these USB fixes).
<script>window.location.replace(new URL("../test-results/companion_usb_client_validation/" + window.location.hash, window.location.href));</script>
## Port selection
| Hardware | Companion/ASCII port | USB logging |
| --- | --- | --- |
| RAK4631 and other nRF52 Full Companions | Primary CDC, Linux `*-if00` | Optional second CDC, `*-if02` |
| ESP32 Full Companions | Primary USB serial port | Shares that port; must be off for Binary Companion |
`+++MESHCORE-TERM-STOP` changes the primary port to Binary Companion. Its
`OK - Binary mode` reply does not move the client to a second port. Close the
terminal before opening that same port with `meshcli -s PORT infos`. Do not use
MeshCLI's repeater-mode `-r` option for a binary Companion connection.
## Two reproduced failures
1. Stock `meshcore-cli` 1.6.3 was tested with `meshcore` 2.3.10, whose serial client
deliberately deasserts DTR. The nRF52 primary USB gate and response route
both required DTR. A raw battery query returned a valid 14-byte response
with DTR high and no bytes with DTR low.
2. The automatic ASCII banner ended in `> `. MeshCLI recognized that `>` as a
binary frame marker and consumed the real response's leading `>` as a
length byte. Even forcing DTR high in a diagnostic client did not recover
the first `APP_START` response. Stock MeshCLI exited without node information.
The nRF52 transport now accepts real USB input as evidence of a client, tagged
with the current session generation. Close, line-coding, and device-reset
events invalidate that evidence; an old reader racing cleanup cannot grant
access to a newer session. The write path remains bounded and does not wait
for an unread host. CDC1 logging retains its own DTR requirement.
The default ASCII terminal now suppresses its banner and unsolicited terminal
events until ASCII input arrives. Enter or a command reveals the terminal.
A binary probe receives a clean first response. Explicit START/STOP remains
available, and an incomplete binary probe still times out back to ASCII.
On a shared logging port, STOP now reports
`ERROR: set usb.logging off before Binary mode` while logging is enabled.
It no longer acknowledges Binary mode and immediately reclaims the port for
ASCII logging.
## Hardware validation
The RAK4631 Full test firmware passed with an unmodified installed MeshCLI and
its normal DTR-low serial library:
- ASCII `ver` from the default mode, without START;
- explicit STOP acknowledgement followed by 10 binary battery queries;
- 10 immediate close/reopen queries each with DTR low and high;
- 10 stock-library reconnects and three separate `meshcli ... infos` runs;
- all of the above while the second CDC streamed logs: 598 bytes received,
no logging disconnections.
The Heltec V4 Full Companion also passed using its ESP32-S3 USB Serial/JTAG
interface, with USB logging disabled:
- default ASCII `ver`, then STOP and 10 clean binary battery replies;
- 10 stock-library reconnects and three unmodified `meshcli ... infos` runs;
- a stock-client connection directly after reboot, without either mode token.
The shared-port logging policy was exercised on the V4 as well. With logging
on, STOP returned the explicit error, a binary probe could not claim the port,
and stock MeshCLI could not connect. After `set usb.logging off`, STOP worked,
10 binary replies were clean, and three stock-client reconnects succeeded.
After testing, the V4's complete original flash snapshot was restored and
verified by the flasher. Its original `v1.17.1-soak-v4-B` firmware answered
again, and the previously active memory-soak collector was restarted. This
test interrupted that board's uptime; the restored firmware starts a new run.
Fresh successful V4 telemetry was verified at `2026-09-14T19:46:01Z`.
The RAK4631 remains on the patched Full Companion test build with its separate
logging interface enabled. The Pi's previously active services were restored.
The nRF52 DTR stress sequence is deliberately not used on ESP32. Changing those
control lines during immediate close/reopen triggered the V4's hardware
bootloader and produced an `ESP` boot message. Preselecting RTS before opening
did not prevent that transition on this host. ESP32 reconnect results above
use the unmodified client's normal connection sequence instead.
The test issues local information queries only. It does not transmit test chat
messages over LoRa. Firmware installation and logging configuration are
separate from the reusable test runner:
```sh
python3 tools/hil/companion_usb_client.py \
--port /dev/serial/by-id/PRIMARY-if00 \
--logging-port /dev/serial/by-id/LOGGING-if02 \
--test-dtr --cycles 10 --output usb-client-results.json
```
Replace the example paths with the device's actual stable paths. Install
`meshcore-cli` in the Python environment used for the test. Start after a fresh
reboot so the default-ASCII check has a new session. On ESP32, omit both
`--logging-port` and `--test-dtr`, and disable USB logging first.
## Automated checks
- 53 Python USB checks pass, including compiled execution of the real nRF52
callbacks and transport gate with DTR-low clients, cleanup races, early
input, line-coding changes, physical disconnects, full TX FIFO, CDC1 isolation,
and the 1200-baud bootloader entry.
- The real terminal methods are compiled for ordinary and Full modes to verify
that unsolicited text is suppressed before the first binary response.
- The actual STOP branch is exercised for logging off, shared-port logging on,
and dedicated-port logging on.
- 63 native serial-switching, flow-control, and terminal-session tests pass.
- RAK4631 Full and Heltec V4 Full firmware compile and pass their packaged
capability checks.
The USB workflow runs the new compiled regression tests on subsequent changes.
This document is now in [Test Results](test-results/companion_usb_client_validation.md).
+3 -112
View File
@@ -1,114 +1,5 @@
# Double center-frequency write: V4 transmitter / XIAO receiver
# Document moved
2026-09-14. **Completed bounded comparison: neither single nor duplicate
frequency writes reached 400/400. The duplicate adds about 86 us per hop.**
No production default was changed. Both radios are idle with HIL installed;
the duplicate-write toggle is off. The earlier descending-SF sweep and its
follow-up remain paused; no new exhaustive settling sweep was launched.
<script>window.location.replace(new URL("../test-results/double_frequency_v4_validation/" + window.location.hash, window.location.href));</script>
## Hardware and control
- RX: XIAO ESP32-S3/WIO SX1262, COM31, MAC 28:84:85:b4:09:80. Normal gain
register 0x94, unchanged-modulation cache enabled, buffered 8 MHz SPI.
- TX: newly connected Heltec V4, COM7, MAC 44:1b:f6:6a:e8:44. Hardware probe
found ESP32-S3, 2 MB embedded PSRAM and 16 MB flash. HIL detected KCT8103L
FEM and reported the amplified TX path; chip RX gain 0x96.
- User explicitly authorized erasing the V4 without backup. Its full flash
was erased and a verified HIL upload completed. XIAO's previous HIL image
was replaced with a verified same-image A/B-capable build. No private
backup contents were read; RAK and other boards were not touched.
- SF10/125 kHz, CR4/5, four centers 909.500/909.750/910.000/910.250 MHz,
5.1-symbol visits (41,780 us), 32-symbol TX preamble, zero added settling,
synthetic 16-byte packets, -9 dBm **chip request**. No extra IRQ polling.
The V4 FEM uses the repository's production detection and TX/RX control.
HIL restores RX after success or error on each direct transmit path. The
GC1109 branch (not detected here) uses bypass according to
[GC1109 table 4](https://resource.heltec.cn/download/WiFi_LoRa_32_V4/datasheet/GC1109_EN_V0.9.2.pdf).
The detected KCT8103L uses its amplified path; antenna-port TX power was not
measured. This is not a power-matched Indicator/V4 comparison.
## Exact experiment
`scanfreqrepeat 1` makes the HIL `ExperimentalSX1262` repeat the same
SetRfFrequency (0x86) immediately after the first successful frequency call,
before modulation, packet parameters and RX restart. The second call skips
the image-calibration check, but keeps normal BUSY/status handling. Only an
identified RX-to-RX hop may repeat; ordinary setup/TX does not. Either write's
failure propagates through the existing failed profile-apply path. The toggle
defaults off at reboot and cannot change during an active scan.
Both modes use the same XIAO binary. Actual SPI command counts, not only
software policy labels, are checked against one or two 0x86 writes per hop.
The modulation cache remains on in both modes.
## RX-only timing
Six-second windows in single/double/double/single order, with reboot/clean
setup between windows. No reference packets transmitted during this phase.
| Frequency writes | Hops | Mean switch | Actual 0x86 | Actual 0x8B |
| --- | ---: | ---: | ---: | ---: |
| Single, first | 142 | 451.965 us | 142 | 0 |
| Double, first | 141 | 538.213 us | 282 | 0 |
| Double, second | 141 | 538.170 us | 282 | 0 |
| Single, second | 142 | 451.930 us | 142 | 0 |
Weighted means: **451.948 us single, 538.192 us double**: +86.244 us / +19.1%.
All 566 hops had zero retune, RX-mode and cache errors. Maximum observed
switch costs were 594 us single and 674 us double; these are samples, not
hard latency guarantees. Unheld full-cycle means were about 168.942 ms versus
169.285 ms.
## Packet checks
Single first, then double, each up to 100 packets/channel and stopping on its
first miss; a 400/400 result would have stopped all RF testing. Neither passed.
| Frequency writes | Valid before first miss | Failed packet | RF-run mean switch | Hops / 0x86 writes |
| --- | ---: | ---: | ---: | ---: |
| Single | 1 | 2, channel 2 | 452.719 us | 235 / 235 |
| Double | 4 | 5, channel 2 | 542.063 us | 255 / 510 |
Both failed packets were receiver timeouts, with TX success acknowledged,
zero device-error flags and RX mode still active. Both settings reported zero
retune, RX-mode, cache and RX errors. Ordinary processing after received
packets caused one/four modulation rewrites respectively; pure RX-only timing
above is the cleaner comparison of the duplicate cost.
All five good V4 packets reported **-19 dBm RSSI**. SNR was +9.0 dB in the
single-write sample and +8.2..+9.2 dB (mean +8.675 dB) for four double-write
samples. The previous Indicator run had 547 good packets at average -40.547
dBm RSSI, +8.311 dB SNR. Thus this V4 setup was about 21.5 dB stronger as seen
by the receiver. Hardware/antenna/placement/power differences remain
confounded; neither the Indicator nor close-range effects are proven causes.
One versus four successes before a miss is too small and stop-conditioned to
establish a reliability improvement or packet-error rate. Repeating center
frequency did not remove the observed failure at this setting. No results for
lower SFs or added settling delays are claimed for the V4. A weaker received
signal is needed before treating a transmitter swap as a clean comparison.
## Artifacts and checks
Base checkout `0e5955f889788a4317863e731b38ffe09e3d5441`, branch
`keymindCascade`, plus local uncommitted changes. RadioLib 7.7.1 pinned at
`187ef24791c3d844939b2be13a68bd890bd04e4c`, Espressif32 6.11.0 / Arduino
ESP32 2.0.17. Both builds/uploads succeeded and uploader verified written
data. The 59 selected host/native tests passed (39 collectors, ten profile,
four SX1262, five radio and one receive-mode), including FEM restore on error
and frequency-repeat scope/error propagation. Temporary build overlay removed.
Actually loaded firmware SHA-256:
- XIAO: `9f82d86ea78cfa6bc03fef4925f10c918c8b9fc7a96a7d2b565a04bd2f281288`
- V4: `2ea2ce37d9d66fb4f8ec9cf3cedfbe7d4eb6d08bfe9abe6c817e5afb04885636`
Raw evidence and byte-preserving SHA-256:
- [RX-only ABBA](../tools/hil/profile_switch_frequency_sf10_rx_only_results.json):
`38ded968a60ff19f45618ec35d75ce46354d81f163879185e9d6ff0e68da3ae0`
- [Single-write packets](../tools/hil/profile_switch_frequency_v4_sf10_off_results.json):
`4cadeba0d22b3ec6158ff295f1becfb57fde7ef8bf7fabbdbc6295f274619557`
- [Double-write packets](../tools/hil/profile_switch_frequency_v4_sf10_on_results.json):
`46bea8d9c095847ef8ccf4a4ed51ab5404eb5bb87d9cf62b832c3298159d1525`
This document is now in [Test Results](test-results/double_frequency_v4_validation.md).
+1 -1
View File
@@ -33,7 +33,7 @@ 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](v4_pixel5_font_trial.md) retains complete
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
+1 -1
View File
@@ -534,5 +534,5 @@ release version. Its installation directions identify the nRF52 queue-sharing
behavior, Wireless Paper Full's 350 contacts with a shared 256/128-slot queue,
and the 150-contact limit on the other six affected ESP32 Full profiles.
Those notices apply only to the corrected files. See the
[memory correction details](releases/1.17.1.5.md#memory-corrections-in-aa20e927)
[memory correction details](old-releases/1.17.1.5.md#memory-corrections-in-aa20e927)
and each replacement's `.memory.json` report before updating.
+3 -141
View File
@@ -1,143 +1,5 @@
# Four fixed transmitters: longer dwell repeats
# Document moved
Same physical MercerMesh fixture and installed firmware as
[the completed 5.1-symbol run](four_fixed_tx_single_pass_validation.md):
RAK4631, Heltec T096, MeshTower V2 and T1000-E fixed transmitters; Heltec V4 RX.
SF10 / BW125 kHz / CR4/5, four channels spaced 1 MHz apart, 32-symbol preamble,
normal SX1262 RX gain, -9 dBm chip TX setting, zero added settling and one
frequency write per hop. No firmware was rebuilt or reflashed for these repeats.
<script>window.location.replace(new URL("../test-results/four_fixed_tx_dwell_validation/" + window.location.hash, window.location.href));</script>
## 6.1-symbol run: user-stopped, incomplete sample
Started 2026-09-14 22:44:47 UTC. Dwell was 49,972 us per channel.
All four stationary controls passed 100/100 again.
The user requested stopping this run and proceeding to 7.7 symbols. The
collector was interrupted only after verifying its PID, command line and owner.
At the last saved checkpoint, **342/348 scored scan packets were received**
(98.28%), with six timeouts:
| TX / channel | Scored attempts | Received | Timeouts |
| --- | ---: | ---: | ---: |
| RAK4631 / 0 | 87 | 87 | 0 |
| Heltec T096 / 1 | 87 | 86 | 1 |
| MeshTower V2 / 2 | 87 | 85 | 2 |
| T1000-E / 3 | 87 | 84 | 3 |
This is not a completed 400-packet run or a validated full-run timing capture.
An interrupted in-flight packet can exist beyond the last saved checkpoint;
it is not scored as another success or failure. No final switch timing was
collected before reboot. The raw capture remains incomplete, and the separate
user-stop record documents why. All five radios were then independently
verified idle, and ModemManager was restored. Gateway services stayed active;
the V4 memory-soak service stayed inactive.
- [Partial raw capture](../tools/hil/profile_four_tx_sf10_6p1_results.json)
- [User stop record](../tools/hil/profile_four_tx_sf10_6p1_user-stop.json)
- [Verified cleanup](../tools/hil/profile_four_tx_sf10_6p1_cleanup.json)
- [Run provenance](../tools/hil/profile_four_tx_sf10_6p1_launch.json)
Partial raw capture SHA-256:
`ad199124930da19fe56c78a4ea8280a6d94b95ef408589b19a9e165155dbf7fd`.
## 7.7-symbol run
Launched 2026-09-14 22:58:14 UTC; collector ran from 22:58:15 to 23:11:45 UTC
with 63,079 us visits. All four fixed-channel controls passed 100/100.
The full scan completed with **395/400 received (98.75%)**, five timeouts and
zero wrong-channel or invalid payloads. Thus **7.7 symbols did not achieve a
400/400 pass**.
| TX / channel | Scanning result | Timeouts | Mean received RSSI (dBm) | Mean received SNR (dB) |
| --- | ---: | ---: | ---: | ---: |
| RAK4631 / 0 | 100/100 | 0 | -21.05 | +8.14 |
| Heltec T096 / 1 | 99/100 | 1 | -4.35 | +8.98 |
| MeshTower V2 / 2 | 97/100 | 3 | -44.86 | +8.63 |
| T1000-E / 3 | 99/100 | 1 | -18.56 | +8.26 |
All 800 baseline/scan TX sequences were unique, all transmit calls succeeded,
and every TX packet reported zero frequency and modulation configuration writes.
There were zero packet-response transport replays. All four final TX lifetime
checks reported 200 packets and a prepared, non-failed transmitter.
The five timeouts occurred at scan attempts 158 (T1000-E), 184 (T096), and
200, 293, 314 (MeshTower). Each reported RX mode 1 and zero device errors.
Final RX failure, RX-mode-error, cache-error and receive-error counters were zero.
### Measured switch and sweep timing
| Hop population | Count | Minimum (us) | Mean (us) | Maximum (us) |
| --- | ---: | ---: | ---: | ---: |
| All single-pass switches | 5,046 | 438 | 524.146 | 804 |
| Cached modulation; no 0x8B | 4,651 | 438 | 502.147 | 686 |
| Post-reception modulation refresh | 395 | 760 | 783.180 | 804 |
Exactly 5,046 RF-frequency commands and 5,046 optimized RX resumes were counted,
with one retune pass and zero second passes/detours. The 395 modulation writes
match the 395 post-reception cache refreshes. This is the same cache behavior
as the earlier run, not double-setting the radio. The slightly higher overall
mean than the 5.1 run partly reflects a larger fraction of refresh hops.
Across 860 measured idle cycles, the four-channel sweep averaged **254.351 ms**,
minimum 254.208 ms and maximum **255.482 ms**. Relative to the 262.144 ms
programmed preamble, that leaves 7.793 ms at the mean and 6.662 ms at the maximum.
Nominal dwell was 63.079 ms; measured idle dwell averaged 63.084 ms. Packet-held
visits include payload/guard time and are not comparable to idle cycles.
The 7.7 run received three more packets than the earlier 5.1 run (395 versus
392 out of 400). This small, sequential comparison does not establish a reliable
improvement or a lossless dwell threshold. The 6.1 result is partial and must
not be treated as an equal-size completed comparison.
### Evidence and cleanup
Unlike the original 5.1 run's overly strict host assertion, this collector
completed normally with `complete: true` and exit code 0. Independent offline
validation also passed. The same collector SHA-256 was used for both repeats:
`cceb8a570f4aedeaf45d28b4b4590b23df3c1efd490c8e8e9cd74a23ba00024f`.
The copied deployment/manifest files are provenance for reused firmware, not
records of a new flash operation. Host options and acknowledged dwell match.
- [Completed raw capture](../tools/hil/profile_four_tx_sf10_7p7_results.json)
- [Validated summary](../tools/hil/profile_four_tx_sf10_7p7_validated.json)
- [Run provenance](../tools/hil/profile_four_tx_sf10_7p7_launch.json)
- [Cleanup readbacks and warnings](../tools/hil/profile_four_tx_sf10_7p7_cleanup.json)
Raw capture SHA-256:
`5e3f9a80af1384c3072303428b75c8550ac42d2ac95bd1234bf92cb0aea625e8`.
The cleanup wrapper retained three errors from its reboot phase: the T096
primary USB interface was temporarily missing/ambiguous, and attempts to open
the MeshTower/T1000-E ports returned permission denied. Its subsequent final
readbacks nevertheless succeeded for all five radios: each TX was unprepared,
non-failed, packet count zero and autonomous TX disabled; RX scanning was
inactive with zero channels. No further radio access or permission changes
were attempted after those results returned. The wrapper's `verified_idle`
flag remains **false** because it retains any earlier cleanup error, and the
outer operation returned nonzero for that reason. These warnings happened
after the complete RF sample; they are not counted as RF packet losses or
silently removed from the record.
ModemManager was restored active; gateway services remained active. The V4
memory-soak service remains inactive with test firmware installed. No firmware
restore, commit, push or pull was performed. Previous raw captures are unchanged.
## Dwell budget
One chirp at SF10/125 lasts 8.192 ms. For the experiment's nominal budget of
one complete four-channel idle sweep inside 32 programmed preamble symbols:
`maximum dwell symbols = 32 / 4 - switching_ms / 8.192`.
The 5.1-symbol run's mean switch (0.513980 ms) gives about 7.937 symbols;
its maximum switch (0.801 ms) gives about 7.902 symbols, before other loop
overhead. These are timing ceilings, not proven lossless dwell limits or a
complete acquisition model. Near the ceiling there is little timing margin.
At 7.7 symbols, four nominal dwell-plus-average-switch intervals total about
254.4 ms, versus a 262.144 ms programmed preamble.
The host keeps the same seed and arrival-randomization rule, spanning about
two nominal scan cycles; absolute arrival pauses therefore scale with dwell.
The fixed-channel controls, sample counts, switching policy and firmware stay
unchanged. As before, external PA/LNA paths and unrelated RF traffic mean this
is a finite bench comparison, not a calibrated sensitivity or interference test.
This document is now in [Test Results](test-results/four_fixed_tx_dwell_validation.md).
+3 -183
View File
@@ -1,185 +1,5 @@
# Four fixed transmitters / fast single-pass receiver
# Document moved
## Result
<script>window.location.replace(new URL("../test-results/four_fixed_tx_single_pass_validation/" + window.location.hash, window.location.href));</script>
MercerMesh Pi bench run, started **2026-09-14 22:18:49 UTC** (15:18:49 PDT):
four physically separate, fixed-channel transmitters and the Heltec V4 receiver.
The full stationary controls passed **400/400**. The subsequent four-channel
scan received **392/400 correctly (98%)**, with **8 timeouts (2%)** and **zero
wrong-channel payloads**. No RF retries were used.
| TX / assigned channel | Frequency (MHz) | Fixed-RX control | Scanning RX | Scan mean RSSI (dBm) | Scan mean SNR (dB) |
| --- | ---: | ---: | ---: | ---: | ---: |
| RAK4631 / 0 | 909.5 | 100/100 | 96/100 | -20.69 | +8.63 |
| Heltec T096 / 1 | 910.5 | 100/100 | 98/100 | -3.84 | +8.45 |
| Heltec MeshTower V2 / 2 | 911.5 | 100/100 | 100/100 | -45.48 | +8.41 |
| Seeed T1000-E (LR1110) / 3 | 912.5 | 100/100 | 98/100 | -18.27 | +8.51 |
RSSI/SNR describe received packets only. Each transmitter sent 200 unique
packets across its baseline and scan. All 800 transmit calls returned success,
with zero frequency or modulation commands during those packet calls. There
were zero recovered/replayed USB responses in the packet records.
## Switching timing and command evidence
| Hop population | Count | Minimum (us) | Mean (us) | Maximum (us) |
| --- | ---: | ---: | ---: | ---: |
| All single-pass switches | 7,206 | 429 | 513.980 | 801 |
| Cached modulation; no 0x8B | 6,814 | 429 | 498.602 | 686 |
| Modulation cache refresh | 392 | 763 | 781.291 | 801 |
The receiver recorded exactly **7,206 frequency commands and 7,206 optimized
RX resumes**: one frequency write and one resume per hop. Second-pass count
was zero. Duplicate frequency writes, frequency/CR detours and added settling
were disabled. Warm XOSC, bulk 8 MHz SPI and cached modulation were enabled.
The average switch was **0.514 ms**, but not every switch was below 0.6 ms.
After each received packet, ordinary RX staging invalidates the modulation
cache; the next hop refreshes it. Subsequent unchanged hops omit 0x8B.
The 392 refreshes match the 392 received packets. The source path is
`RadioLibWrapper::recvRaw()` -> `startRecv()` / `startReceiveMode()` ->
`PhysicalLayer::startReceive()` -> `CustomSX1262::stageMode()` ->
`SX1262ProfileSwitchState::invalidate()`. No production cache-invalidation
behavior was changed for this test.
Mean switch-plus-settling-instrumentation time was 515.698 us, maximum 803 us.
The settling measurement averaged 1.718 us of timer/measurement overhead;
the configured settling delay was zero. The idle four-channel sweep averaged
169.135 ms (maximum 170.470 ms), versus 262.144 ms for the 32-symbol preamble.
Packet-held visits include reception/guard time and are not idle sweep timings.
The `deferred` counter counts guard-loop deferrals, not lost packets.
## Method and fixture
- SF10 / 125 kHz / CR4/5; 32-symbol preamble; synthetic 16-byte CHS1 packets.
- Four channels spaced 1 MHz apart; 5.1-symbol RX visits (41,780 us).
- Each physical transmitter was configured once and remained on its assigned
frequency throughout both the controls and scanning.
- Receiver was reinitialized for each fixed-channel control and once before
the scan. All 100 control packets per transmitter were retained.
- Scan used 100 packets per channel, randomized channel order and arrival
pauses (seed 606125). Transmissions were staggered, not concurrent.
- Each expected packet had a 10-second scan timeout. Misses did not stop the
run, restart the scanner, force it onto the TX channel or trigger a retry.
- SX1262 normal RX gain was verified as register 0x94. The V4's external
KCT8103L front end remains distinct from the chip's boosted-gain setting.
- Requested TX power was -9 dBm at each radio chip. The Heltec TX boards have
external PA paths, held in TX mode by this HIL firmware; these are **not
calibrated equal antenna-port powers**.
- High-rate channel trace was disabled; command counters and timing remained
enabled. Gateway services remained running, so unrelated RF was not excluded.
| Role | Hardware serial / MAC | Pi USB topology |
| --- | --- | --- |
| TX0 RAK4631 | 9AB3B64C641BA927 | 1-1.3.1 |
| TX1 Heltec T096 | 651F8E496197F882 | 1-1.2.3 |
| TX2 Heltec MeshTower V2 | 9352162A72082314 | 1-1.2.2 |
| TX3 Seeed T1000-E LR1110 | 34A9141999729D5D | 1-1.2.1 |
| RX Heltec V4 | 44:1B:F6:69:CF:98 | 1-1.3.3 |
Board selection checked serial plus topology, not transient tty names or CDC
interface count. The RAK3401 gateway, serial 0B81C9C68D8D01B4, was excluded.
The earlier local XIAO radios were not modified during this run.
## Remaining losses and interpretation
Timeouts occurred at overall scan attempts 10, 14, 55, 65, 145, 299, 304 and
384. Thus they were not confined to startup. Each timeout reported RX mode 1
and zero device errors; TX calls returned success and still reported no
configuration writes. Final RX failure, RX-mode, cache and receive-error
counters were all zero.
The wrong-channel behavior did **not** reproduce in this configuration.
The remaining misses do not require a transmitter that retunes between packets.
However, this is not a controlled A/B against the earlier XIAO setup: the RX
board, TX boards/chips, antennas and geometry changed. These results do not
prove that the earlier transmitter was faulty or establish the cause of the
eight remaining losses.
Several received signals were very strong, particularly the T096 at about
-4 dBm. The weakest link, MeshTower V2 at about -45 dBm, passed 100/100 while
scanning. That makes a controlled spacing/attenuation comparison worth
considering, but this sample does not prove receiver overload. The observed
98% is a finite bench result, not a reliability guarantee or sensitivity test.
## Capture integrity and post-run validation
All 400 scan probes finished and final radio telemetry was returned. The
original host checker then rejected that telemetry because it incorrectly
required zero modulation writes over the entire run, including post-packet
RX staging. Its raw capture therefore remains `complete: false`, with final
status preserved verbatim inside its error string. The normal `sender_final`
collector step did not run. **The raw failure record was not rewritten.**
Source review established the cache-refresh behavior above. The host checker
now validates modulation writes against the measured refresh-hop count, checks
that cached plus refresh hops equal total hops, and still requires single
frequency writes, single fast resumes, zero added delay and the other policies.
It also saves final telemetry before checking it, so future failures retain
that structured record.
Separate offline validation parsed the saved telemetry, checked the full
400-probe scan and all four 100-probe controls, verified 800 unique sequences,
and checked every transmitter's packet counter from 1 through 200 and its
zero-retune acknowledgments. A capture regression additionally checks that
each TX/RX response ID matches its probe and every accepted payload/channel
matches the assigned channel. No radio was rerun to replace the failed host
assertion. The derived summary explicitly reports
`original_collector_complete: false` and `post_run_validation_passed: true`.
Evidence files:
- [Immutable raw capture](../tools/hil/profile_four_tx_sf10_single_results.json)
- [Independently validated summary](../tools/hil/profile_four_tx_sf10_single_validated.json)
- [Deployment record](../tools/hil/profile_four_tx_sf10_single_deployment.json)
- [Build manifest](../tools/hil/profile_four_tx_sf10_single_manifest.json)
- [Verified idle/service state](../tools/hil/profile_four_tx_sf10_single_cleanup.json)
Raw capture SHA-256:
`42de3ac0c335b2c59f35c1cab217a01ef242c9f2e7b0010c8000a908a59232e6`.
Derived summary SHA-256:
`316bd551eb7306dfc576f4b88ad23e82cb1d701481d1e022067c961806d31bfe`.
## Build, deployment and final state
Built from the dirty `keymindCascade` worktree based on
`2b5a6f209835f0f67ed73fee78d36447a4566ce6`. This is not a claim that HEAD
alone reproduces the images. The manifest records image and HIL-source hashes.
All five PlatformIO builds succeeded and ran sequentially.
After the host-checker correction, 25 focused unit/regression tests passed,
including the preserved-capture check. `git diff --check` passed; its only
warning concerned a pre-existing CRLF conversion in `MyMesh.cpp`.
RadioLib was pinned to `187ef24791c3d844939b2be13a68bd890bd04e4c` (7.7.1).
The nRF framework was `d5413016` (1.10701.0); the ESP build used platform
6.11.0 / Arduino 2.0.17. The first three nRF targets retained S140 6.1.1
(FWID 0xB6, app base 0x26000); T1000-E retained S140 7.3.0 (FWID 0x123,
app base 0x27000). nRF DFU packages were application-only, with their matching
SoftDevice requirements intact. No nRF bootloader/SoftDevice replacement or
cross-board override was used. Runtime FWID and application base were verified.
V4 received the normal ESP bootloader, partition table, boot_app0 and HIL app
at 0x0, 0x8000, 0xE000 and 0x10000, respectively, with esptool hash checks.
No whole-flash bulk erase was issued. No backup was requested for these
authorized bench deployments.
The initial bundle's collector/deployer hashes are not the final scripts used:
the actual run used `profile_four_tx_v2.py` with SHA-256
`b762e412636076902174c76b71961cc8ab3059c2b6de3dd3b4123bafcb06545c`
(startup-greeting fix, original strict final assertion), and deployment used
`profile_four_tx_deploy_v3.py` with SHA-256
`8a660788e0215e6ac1aece0a5d033521f9f376c9e51c00e760e918ebfd72cca1`.
The current local collector includes the later validation correction; firmware
images were not changed after the run. Remote evidence remains under
`/home/mikec/hwtest/runs/four-tx-sf10-20260914`.
All five radios were subsequently rebooted and independently verified idle.
All TXs reported unprepared, packet count zero, no failure and autonomous TX
disabled; V4 reported scanning inactive and zero hops after reboot. HIL
firmware remains installed. ModemManager was restored active, gateway services
were left active, and the V4 memory-soak service remains inactive because its
firmware is now the bench receiver. No Pi USB hub power cycle was performed.
No commit, push or pull was performed for this test. Unrelated worktree changes
were preserved.
This document is now in [Test Results](test-results/four_fixed_tx_single_pass_validation.md).
+1 -1
View File
@@ -15,7 +15,7 @@ queued messages normally. During an mOTA session, 128 message slots are
available while the other half holds its workspace. If more than 128 unread
messages are queued, sync them with an app before starting mOTA. Disconnecting
the source or turning it off returns all 256 slots. Idle WiFi mOTA listening
does not borrow the queue. See the [1.17.1.5 release guide](releases/1.17.1.5.md)
does not borrow the queue. See the [1.17.1.5 release guide](old-releases/1.17.1.5.md)
for the corrected download; the earlier `aa20e927` Wireless Paper image has
150 contacts.
+1 -1
View File
@@ -14,7 +14,7 @@ frame automatically hands the interface to the binary parser.
Current source keeps the default terminal quiet until the host sends ASCII
input. Press Enter or type a command to see the banner. This prevents the ASCII
`>` prompt from being mistaken for the beginning of an app's first binary
response. See [USB client compatibility validation](companion_usb_client_validation.md)
response. See [USB client compatibility validation](test-results/companion_usb_client_validation.md)
for the tested firmware and release status.
These mode controls apply to Full and ordinary USB Companion builds. The
+3 -76
View File
@@ -1,78 +1,5 @@
# Full USB logging repair — 1.17.1.6
# Document moved
Audit date: 2026-09-13. Release firmware source: `306feebe6d648a247f925894f876be9d747fc8d2`.
<script>window.location.replace(new URL("../test-results/full_usb_logging_validation/" + window.location.hash, window.location.href));</script>
The original Station G2 Full Repeater and Full Room Server images omitted USB
packet logging. Both inherited `-UMESH_PACKET_LOGGING` from their ordinary MQTT
observer recipes. Full added `-DMESH_PACKET_LOGGING=1`, but PlatformIO's
`ProcessFlags` moves undefines after defines in the compiler command. The
resulting image compiled successfully with its packet logger removed.
The previous release gate checked OTA and other features without requiring
USB logging in Full infrastructure. An old passing manifest could also be
reused by `--resume` without the new logging evidence.
## Published-image audit
Each downloaded/cached artifact was matched to GitHub's published SHA-256.
The check inspected application bytes, including the application inside nRF52
DFU ZIPs. It required the packet-log formatter and USB logging controls.
| Release group | Application images | Missing USB packet logging |
| --- | ---: | ---: |
| ESP32 Full infrastructure/utility | 157 | 2 |
| ESP32 Full Companion | 52 | 0 |
| nRF52 Full Companion | 43 | 0 |
| Total | 252 | 2 |
Affected targets:
- `Station_G2_repeater_observer_mqtt`
- `Station_G2_room_server_observer_mqtt`
V4/R8 Full builds and Station G2's Full ESP-NOW bridge passed. This audit does
not count ordinary MQTT profiles that intentionally omit USB logging as
broken. Artifact inspection proves compiled support, not hardware operation
on every board.
## Fix and regression coverage
- Full builds that require logging remove inherited disables of
`MESH_PACKET_LOGGING` before PlatformIO parses flags. Other macros and
explicit logging-off profiles retain their policy.
- Full capability checks require the packet logger and USB controls in the
packaged application. Combined USB/WiFi profiles additionally require
the `logging.output` setter. ELF symbols or debug text cannot satisfy these
checks.
- Resumed builds must have passing application evidence for the current
logging contract, along with the existing artifact hashes and RAM checks.
- CI runs the flag, real PlatformIO/compiler, artifact, resume, and build-profile
regression suites.
Focused verification (after installing PlatformIO and its tool-scons package):
```sh
python3 -B test/test_full_build_flag_dedup.py
python3 -B test/test_full_logging_compile.py
python3 -B test/test_firmware_capabilities.py
python3 -B test/test_full_logging_resume.py
bash test/test_build_profiles.sh
python3 -B test/test_cascade_release_package.py
```
Both Station G2 profiles were rebuilt through `build.sh --full` with the
original observer recipes, including their inherited logging undefines. They
passed the new application checks, runtime RAM budgets, and both OTA slot
size checks. The new checker rejects both original published images.
On the Bellevue Station G2, the corrected Full repeater implementation also
passed serial CLI and passive traffic checks: four RX and two TX packet log
lines were captured. Its active temporary profile retained preamble 120 and
the original schedule end time; the Pi relay services were restored.
Corrected application SHA-256:
```text
Repeater: 81a0a2926d4e567c069d0b10f316047881bcedce062afaa42119dddb2baadf23
Room Server: 7ee81f1cb65fc274bd363397c3c80ce9006d575efae50fe952c8ca246613a18c
```
This document is now in [Test Results](test-results/full_usb_logging_validation.md).
File diff suppressed because it is too large. Load diff
+3 -21
View File
@@ -1,23 +1,5 @@
# Home-screen text spacing
# Document moved
On non-Indicator companions, a visible Bluetooth PIN or connection status owns
a separate bottom-aligned block. It replaces the ordinary inbox instruction,
clock and WiFi rows while displayed. The PIN stays below the inbox title; the
label and value use measured font heights, including the MeshPocket's larger
e-paper font. An unusually short viewport may omit the label, never overlap
the title or clip the PIN. Indicator's dedicated pairing layout is unchanged.
<script>window.location.replace(new URL("../test-results/home_text_spacing/" + window.location.hash, window.location.href));</script>
The repeater home page also stacks measured font-height rows instead of fixed
ten-pixel offsets. Long names and status lines are ellipsized horizontally.
Run `python3 -m unittest discover -s test -p test_home_text_spacing.py -v` on a
host with a C++ compiler. This renders the actual companion and repeater home
branches, recording every text rectangle and rejecting overlaps or clipping.
It covers 128x64 OLED, 160x80 T096, 250x122 MeshPocket and larger viewports,
PIN/connected/disabled/prompt states, WiFi variants, long strings and a sweep
of font heights. It also checks the static reader hint. The separate native
display and T096 tests exercise the driver drawing/measurement methods.
Hardware acceptance: check that the full six-digit PIN is near the bottom,
with no text over it; connect/disconnect Bluetooth and verify no stale digits;
then check the repeater's five rows and the V4's non-blinking reader footer.
This document is now in [Test Results](test-results/home_text_spacing.md).
+2 -2
View File
@@ -5,8 +5,8 @@ Welcome to the MeshCore documentation.
Below are a few quick start guides.
- [1.17.1.8 USA Cascade release](./releases/1.17.1.8.md)
- [1.17.1.5 Markdown audit](./releases/1.17.1.5-doc-audit.md)
- [1.17.1.5 USA Cascade release](./releases/1.17.1.5.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)
+3 -181
View File
@@ -1,183 +1,5 @@
# Shrinking the per-connection TLS footprint on non-PSRAM observers
# Document moved
## Why
<script>window.location.replace(new URL("../test-results/mbedtls-tls-footprint/" + window.location.hash, window.location.href));</script>
On a non-PSRAM Heltec V3 running two WSS/JWT broker slots, the largest allocatable block in
internal DRAM walks down in ~16 KiB steps at every TLS reconnect while total free heap stays
flat. Measured on hardware over 50 reconnect cycles: 62,452 -> 16,372 bytes, permanently.
The step size is not a coincidence. `framework-arduinoespressif32 3.20017` (Arduino 2.0.17,
IDF 4.4) builds mbedTLS with the **symmetric** buffer configuration:
```
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN 16384 # sizes BOTH the in and out record buffers
CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE 1
# CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN -- not defined
# CONFIG_MBEDTLS_DYNAMIC_BUFFER -- not defined
# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH -- not defined
```
Read from `packages/framework-arduinoespressif32/tools/sdk/esp32s3/qio_qspi/include/sdkconfig.h`.
Note the separate `framework-arduinoespressif32-libs` package in `~/.platformio` belongs to
the esp32c6 env (pioarduino, IDF 5.3) and is **not** what this env links against -- though it
happens to carry identical mbedTLS values.
So each broker slot costs **2 x 16 KiB = 32 KiB** of contiguous internal DRAM in record
buffers alone, and two slots cost 64 KiB on a board with roughly 80 KiB free. Every reconnect
frees and re-allocates those buffers, and anything that lands in the vacated hole in between
prevents them from going back, which is the ratchet.
Confirmed by two independent observations: losing a whole TLS session returned exactly 16,384
bytes of contiguity on one device and exactly 32,768 on another, and per-connection teardown
frees ~41.6-44.7 KB total.
## What the firmware could already do, and its limit
`softDisconnect()` (branch `perf/mqtt-renewal-no-stop`, commit `6c51e468`) stops the JWT
renewal bounce from destroying and recreating the esp-mqtt task, keeping its 6 KiB stack out
of the hole. Measured: the staircase arrests after 2 steps at 36,852 through cycle 16, where
the baseline took 4 steps and settled at 16,372 by cycle 11 -- about 20 KB better.
That is as far as the application layer reaches. MQTT 3.1.1 has no re-authentication packet,
so presenting a fresh JWT *requires* a transport reconnect; mbedTLS's internal allocation
order during the handshake is not controllable from the application. The remaining cost is
the record buffers themselves.
## The changes
All three are compile-time in mbedTLS, and the Arduino framework ships precompiled `.a`
archives (`tools/sdk/esp32s3/lib/libmbedtls.a`), so a project-level `-D` cannot change them.
A custom framework build is required.
| Setting | From | To | Saving per connection |
|---|---|---|---|
| `CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN` | unset | `y` | enables the two below |
| `CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN` | 16384 (implied) | 16384 | none -- keep it |
| `CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN` | 16384 (implied) | 4096 | **~12 KiB** |
| `CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE` | `1` | `n` | ~4 KiB |
Roughly 16 KiB per connection, 32 KiB across two slots.
### Why inbound stays at 16 KiB
A peer may legitimately send a 16 KiB TLS record. Lowering the inbound limit only works if
both brokers negotiate the max-fragment-length extension or provably never send larger, and
getting it wrong produces invalid-record and handshake failures rather than a clean error.
Outbound is ours to choose: this firmware's MQTT and WebSocket frames are far below 4 KiB
(`MAX_TRANS_UNIT`-bounded packets plus small JSON), so 4 KiB is comfortable.
### Risk on the peer certificate
Dropping `KEEP_PEER_CERTIFICATE` means `mbedtls_ssl_get_peer_cert()` returns NULL after the
handshake. Chain validation still happens -- only retention of the parsed leaf changes. This
firmware verifies against a CA (`GTS_ROOT_R4` / the bundle) and never inspects the peer
certificate or a fingerprint, so it should be safe. It does change `mbedtls_ssl_session`
layout, which is exactly why the whole framework must be rebuilt together rather than
swapping a single archive in.
## Build procedure
### Do not use esp32-arduino-lib-builder for this
`release/v4.4` is the branch matching Arduino 2.0.x, but its `update-components.sh` clones
every dependency at **master**, so it no longer resolves. Five successive failures, each a
different repo: the arduino branch name it passes to `-A` does not exist; `jq` is absent
from the IDF image and its absence makes `build.sh`'s target loop a **silent no-op that
still exits 0**; `esp_littlefs` and `esp32-camera` master require IDF >=5.0/>=5.1;
`esp32-camera` later needs an `esp_jpeg` version the 4.4 registry cannot satisfy; and
tinyusb's source layout no longer matches `arduino_tinyusb/CMakeLists.txt`. Pinning each
one in turn just surfaces the next.
### Rebuild only the mbedTLS archives
More rigorous anyway, because it reuses the shipped `sdkconfig` verbatim -- so the archives
differ from stock *only* by the intended change, with no arduino-version or
`DYNAMIC_BUFFER` drift.
This is ABI-safe for the content-length change specifically: `ssl.h` declares `in_buf` and
`out_buf` as `unsigned char *`, allocated in `ssl_setup()`, and no public struct embeds a
CONTENT_LEN-sized array. The other precompiled archives (esp-tls, esp_http_client,
esp-mqtt) therefore remain compatible. **It is not safe for
`CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE`**, which changes `mbedtls_ssl_session` layout --
that one needs everything rebuilt together, so it is excluded here.
1. Minimal IDF project whose only component requirement is `mbedtls`.
2. `sdkconfig.defaults` = the shipped
`packages/framework-arduinoespressif32/tools/sdk/esp32s3/sdkconfig`, with
`CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=16384` replaced by the three asymmetric lines.
Verify the diff is exactly 5 lines before building.
3. `docker run --rm -v $PWD:/project -w /project espressif/idf:v4.4.7 idf.py -DIDF_TARGET=esp32s3 build`
4. Confirm `build/config/sdkconfig.h` carries `OUT_CONTENT_LEN 4096`.
5. Stage the four archives under the framework's names -- note the rename:
| built | framework name | stock size | rebuilt |
|---|---|---:|---:|
| `esp-idf/mbedtls/libmbedtls.a` | `libmbedtls.a` | 113,914 | 113,338 |
| `esp-idf/mbedtls/mbedtls/library/libmbedtls.a` | **`libmbedtls_2.a`** | 1,252,150 | 1,245,718 |
| `.../libmbedcrypto.a` | `libmbedcrypto.a` | 4,302,698 | 4,259,458 |
| `.../libmbedx509.a` | `libmbedx509.a` | 676,590 | 673,318 |
All within ~1% of stock, which is a good check that only the config differs.
### Wire it in with -L, never platform_packages
```
PLATFORMIO_BUILD_FLAGS="-L/path/to/staged/archives" pio run -e Heltec_v3_repeater_observer_mqtt
```
Verify with `grep -oE "[^ ]*libmbed[a-z0-9_]*\.a" .pio/build/<env>/firmware.map | sort -u` --
every path must be the staged directory.
**Do not** point `platform_packages` at a `file://` copy of the framework. PlatformIO
installs it *over* the shared `~/.platformio/packages/framework-arduinoespressif32`,
silently changing mbedTLS for every other ESP32 env and project on the machine. It does this
even when the copy's `package.json` version differs -- verified twice here, and both times the
fix was `rm -rf` the package and `pio pkg install` to re-download stock. A prepended library
search path keeps the change scoped to one env, because the linker takes each archive member
from the first archive that satisfies an undefined symbol.
### How the archives are distributed
They are not committed: ~6 MB per architecture, and they have to be rebuilt for every
`platformio/espressif32` bump, so committing them would grow history permanently and go
stale silently. Instead they are published as a release asset and fetched on demand:
```
scripts/fetch_mbedtls_4k.sh esp32s3 # download + verify against the manifest
MESHCORE_REDUCED_TLS=1 pio run -e Heltec_v3_repeater_observer_mqtt
```
- `scripts/mbedtls_4k_manifest.txt` — per-arch sha256 of each archive. **Update it on every
platform bump**, together with the published asset.
- `scripts/fetch_mbedtls_4k.sh` — downloads into `.mbedtls-4k/<arch>/` (gitignored) and
verifies. `MBEDTLS_4K_LOCAL=<dir>` copies from a local build tree instead of downloading.
- `scripts/mbedtls_4k.py` — wired into `esp32_base.extra_scripts`, but a **no-op unless
`MESHCORE_REDUCED_TLS=1`**, so ordinary builds need no artifact and behave as before.
The opt-in path is deliberately loud, because both ways this can go wrong produce a
firmware that looks correct and silently lacks the change:
| failure | what happens without a guard | guard |
|---|---|---|
| directory missing or partial | linker ignores an unusable `-L` and resolves mbedTLS from the framework | pre-build: hard error |
| archives stale after a platform bump | links the wrong build | pre-build: sha256 vs manifest |
| `-L` present but outranked | framework archives win, flag is inert | post-link: `firmware.map` must resolve every `libmbed*.a` to `.mbedtls-4k/` |
That last one is the reason the post-link check exists rather than trusting the flag: a
build flag reaching the compiler proves nothing about what got linked.
## How to verify it worked
1. `strings`/`grep` the new `sdkconfig.h` for the four settings.
2. Build and check the RAM figure; static usage should be unchanged (these are heap buffers).
3. On hardware, `get mqtt.stats` at boot with two slots connected: the largest free block
should start roughly 24-32 KiB higher than the current 62-67 KiB.
4. Soak across reconnect cycles and compare the floor against the two recorded runs:
baseline settled 16,372 (cycle 11); `softDisconnect` holds 36,852 (cycle 16).
## Prior art in this investigation
`.scratch/mqtt-non-psram-heap-staircase-analysis-2026-08-05.md` (untracked -- `.scratch/` is
globally gitignored) holds the full allocation inventory. `~/mqtt-soak/STATE.md` holds the
soak evidence, including two retracted hypotheses worth not repeating: the perf commits were
not the cause, and waev does not cap connections per IP.
This document is now in [Test Results](test-results/mbedtls-tls-footprint.md).
+3 -25
View File
@@ -1,27 +1,5 @@
# Mixed-bandwidth two-channel reception test
# Document moved
V4 transmitter on MercerWoodMesh; XIAO ESP32-S3/Wio SX1262 receiver. Channel 0: 909.5 MHz, SF7/62.5 kHz, preamble 32. Channel 1: 910.5 MHz, 500 kHz, SF and preamble below. Both use CR4/5, explicit header, CRC and 64-byte test payloads. Fast RX-preamble scanning uses four symbols of the active profile per visit, not four simultaneous channels or hardware CAD.
<script>window.location.replace(new URL("../test-results/mixed_scan_validation/" + window.location.hash, window.location.href));</script>
Each setting starts with a five-packet-per-channel continuous-RX baseline. Scanning sends 50 packets per channel in shuffled order with random host delays. Only CRC-valid, payload-validated, sequence-matched packets count. Any scan loss escalates the 500 kHz preamble by 16, capped at 128. SF9 and SF8 start at 32; SF7 starts at 48. An additional SF8/40 test was requested after SF8/48; its firmware adds acceptance of preamble 40 without changing radio timing or scanning behavior.
| 500 kHz SF | Preamble | Mode | SF7/62.5 received | 500 kHz received | Mean scan cycle |
|---|---:|---|---:|---:|---:|
| SF9 | 32 | Fixed baseline | 5/5 | 5/5 | — |
| SF9 | 32 | Scan | 50/50 | 50/50 | 14.676 ms |
| SF8 | 32 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 32 | Scan | 50/50 | 49/50 | 12.628 ms |
| SF8 | 48 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 48 | Scan | 50/50 | 50/50 | 12.629 ms |
| SF8 | 40 | Fixed baseline | 5/5 | 5/5 | — |
| SF8 | 40 | Scan | 50/50 | 49/50 | 12.630 ms |
| SF7 | 48 | Fixed baseline | 5/5 | 5/5 | — |
| SF7 | 48 | Scan | 50/50 | 50/50 | 11.603 ms |
These are strong-signal, sequential-transmission lab results. Successful reception of 50 packets is not proof of zero loss over months. The receiver remains on one channel through an entire detected packet, so overlapping traffic on the other channel can be lost. Scan-cycle measurements exclude time occupied by packet reception; USB command handling can add timing outliers.
Machine-readable counts, timings and firmware/source hashes: [mixed_scan_results.json](../tools/hil/cad_scan/mixed_scan_results.json).
After testing, both boards were restored to their original B soak firmware using
app-only flashes. Version and normal radio settings were verified on both boards;
Wi-Fi was connected and MQTT reported `custom (ok)`. Both persistent soak loggers
were restarted in phase `B-post-CAD-soak`, marking the intentional uptime reset.
This document is now in [Test Results](test-results/mixed_scan_validation.md).
+3 -134
View File
@@ -1,136 +1,5 @@
# Equal-symbol-time mixed SF/BW, four-channel test
# Document moved
2026-09-14. **Promising, but no strict 400/400 pass.** With no added settling,
the mixed scanner received 61 consecutive packets on the correct channels,
then timed out on packet 62. The near-ceiling +22.4 ms setting received four
packets and timed out on the fifth. There were no wrong-channel payloads in
either run. Both stopped on the first miss without retries.
<script>window.location.replace(new URL("../test-results/mixed_sf_bw_validation/" + window.location.hash, window.location.href));</script>
## Configuration
| Channel | Center (MHz) | SF | BW (kHz) | Symbol time (ms) |
| --- | ---: | ---: | ---: | ---: |
| 0 | 909.5 | 9 | 62.5 | 8.192 |
| 1 | 910.5 | 10 | 125 | 8.192 |
| 2 | 911.5 | 11 | 250 | 8.192 |
| 3 | 912.5 | 12 | 500 | 8.192 |
These preserve the preceding SF10/125 symbol duration exactly, rather than
approximately: `symbol_us = 2^SF * 1000 / BW_kHz`. The supported SF/BW values
and symbol-rate equation are specified in Semtech's
[SX1261/2 datasheet, sections 6.1.1.1–2](https://cdn.sparkfun.com/assets/6/b/5/1/4/SX1262_datasheet.pdf).
Equal symbol durations do not imply equal payload airtime or sensitivity.
- RX: XIAO ESP32-S3 / Wio SX1262, COM31, MAC 28:84:85:B4:09:80.
- TX: XIAO nRF52840 / Wio SX1262, COM15, serial B35E71C1C3726CE7.
- Normal RX gain 0x94, -9 dBm TX chip request, CR4/5, 32-symbol preamble,
16-byte CHS1 packets, 1 MHz channel spacing. No extra IRQ tracing/polling.
- 5.1-symbol visits = 41,780 us; programmed preamble = 262,144 us.
- Double frequency write retained, warm XOSC, bulk SPI and fast RX restart.
SF/BW changes on every hop, so **one 0x8B command is required on every hop**.
- Goal per setting: 100 packets/channel, 400 total, strict channel identity.
No relaxed off-channel acceptance and no automatic channel-count expansion.
The lab scanner stages four channels through the existing two production
profile slots; this does not change the public two-profile protocol.
## Positive controls
Before scanning, each profile received ten fixed-frequency probes after full
normal initialization. **All four passed 10/10, forty total**. Zero frequency
commands occurred after each setup; no stationary receiver hardware failure.
These are fresh ten-packet controls, not claims of 100/100 on each new profile.
Both devices acknowledge the same exact modulation table through `mixinfo`.
Transmitter results verify SF, fractional BW, center, sequence and packet size.
RX records verify center-command word and SF/BW at packet read. Full expected
payload identity and CRC remain required. Mode/cache errors are treated as
fixture failures rather than ordinary RF misses.
## Timing controls and settling budget
Six-second RX-only window at zero added delay: 141 hops, mean 637.922 us,
maximum 828 us. The largest 100 us delay step with a 500 us per-hop reserve is
`floor((262144/4 - 41780 - 828 - 500)/100)*100 = 22400 us`.
The +22,400 us RX-only verification window measured 87 hops, mean 638.241 us,
maximum 833 us. Nineteen complete idle cycles measured mean 259,292.789 us,
maximum 259,310 us, below the 262,144 us preamble. Every hop had two observed
frequency commands and one modulation command, with no mode/cache/RX errors.
This is an empirical near-ceiling budget, not a guarantee of acquisition
for every arrival phase. Longer packet holds and software jitter can defeat
the idle-cycle calculation. RX is active throughout added settling.
## Four-channel results
| Added settling | Strict received / attempted before first miss | Failed TX profile | Mean switch (us) | Max switch (us) | Max idle cycle (us) |
| --- | ---: | --- | ---: | ---: | ---: |
| 22,400 us | 4 / 5 | Channel 2, SF11/250 | 660.951 | 892 | 259391 |
| 0 us | 61 / 62 | Channel 0, SF9/62.5 | 674.819 | 899 | 169798 |
Both failures were timeouts, with TX rc=0, RX mode=1, device errors=0.
Both runs had zero mode/cache/receive errors and zero off-channel payloads.
The +22.4 ms run exercised 163 hops, 326 frequency commands and 163 modulation
commands. Zero-delay exercised 718 hops, 1,436 frequency commands and 718
modulation commands. Optimized RX resume counts equaled hop counts.
No-delay per-channel counts at stop:
| Channel | Received | Attempted | Good-packet mean RSSI (dBm) | Good-packet mean SNR (dB) |
| --- | ---: | ---: | ---: | ---: |
| 0 | 15 | 16 | -34.00 | 11.15 |
| 1 | 16 | 16 | -33.69 | 8.24 |
| 2 | 15 | 15 | -34.33 | 5.78 |
| 3 | 15 | 15 | -33.27 | 3.47 |
The receiver stayed longer on some visits: maximum held visit 582,993 us
with settling, 580,577 us without. Held-visit statistics include successful
packet reception, so these maxima alone do **not** prove false off-channel
preamble holds in this experiment. No IRQ trace was collected for the two
failures; their precise mechanism is not established.
The longer success sequence and absence of wrong-channel payloads are
encouraging relative to the earlier uniform-SF/BW tests, but do not establish
a statistically measured improvement or zero-loss reliability. Each setting
was censored at first failure; do not present 61/62 as an unbiased measured
packet-delivery rate or either run as a completed 400-packet test. The random
arrival method is seeded, not RF-phase synchronized across configurations.
## Implementation, verification and final state
HIL-only `ProfileMixedChannels.h`, `basemixed`, `mixtx`, `scanmixed` and
`profile_switch_mixed.py` add the bounded experiment. Uniform modes preserve
their defaults. Post-hop checks use the actual per-channel modulation;
the uniform collector and settling evidence checks reject mixed-mode results.
Receive ownership, packet guards, BUSY and command-error handling remain intact.
17 native profile tests and 48 HIL host tests passed. Tests cover fractional
62.5 kHz, equal symbol duration, table identity, normal uniform mapping,
required modulation writes, TX replay, and wrong-policy rejection. Both
firmware builds and normal uploads succeeded. The nRF52 ZIP is application-
only, SoftDevice ID 0x0123; no bootloader or SoftDevice replacement. RAK COM29
was not opened. Production settings/source were not changed this turn.
Both boards rebooted idle after the second failed setting, with test firmware
retained. No autonomous TX, background collectors or restoration. Temporary
PlatformIO overlay removed. No commit or push was requested. Existing unrelated
working-tree changes remain intact. `git diff --check` passed apart from
unrelated pre-existing CRLF warnings.
## Provenance
Raw capture: `tools/hil/profile_switch_mixed_equal_symbols_results.json`.
It includes the four baselines, two timing controls and two strict scans,
with `complete:true`, `stopped:both_delay_controls_failed`, and no fixture or
cleanup errors. Earlier captures were not rewritten.
- Raw SHA-256: `49de5c6415b1becdca02bc6e3c72d5e55127e92e758bbe3ac6c342c89e3723dd`
- RX BIN SHA-256: `20d3008ef3d92610510748f64ac90c327f37c91d96cd3ac55272a918867fd5d0`
- TX ZIP SHA-256: `915d507d0feec1712fad2bd4be62c2fa8d769d11400711de0da1cf98a50a4b36`
- TX HEX SHA-256: `1225bcc70854a224f2862443d816b0e9216a253da87c42e96569a933204af941`
RX build reported 94,844 bytes RAM / 367,537 bytes flash. TX reported
18,220 / 95,908 bytes. Source is keymindCascade commit
`0e5955f889788a4317863e731b38ffe09e3d5441` plus existing uncommitted work and
these HIL changes; this is not a clean-release comparison.
This document is now in [Test Results](test-results/mixed_sf_bw_validation.md).
+3 -131
View File
@@ -1,133 +1,5 @@
# ESP32-S3 NimBLE Full Companion trial
# Document moved
These optional builds use NimBLE-Arduino 2.5.1 with the existing ESP32 Arduino
2.0.17 toolchain. They are hardware qualification builds, outside the normal
release matrix and firmware picker.
<script>window.location.replace(new URL("../test-results/nimble_companion_trial/" + window.location.hash, window.location.href));</script>
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
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.
| Hardware | Trial environment | Contacts | Channels | Offline queue |
| --- | --- | ---: | ---: | ---: |
| Heltec V4.2/V4.3 OLED, FEM on | `heltec_v4_2_v4_3_companion_radio_full_femon_nimble` | 350 | 40 | 512 |
| XIAO ESP32-S3 with WIO SX1262 | `Xiao_S3_WIO_companion_radio_full_nimble` | 350 | 40 | 256 |
The XIAO profile uses WIO radio pins CS 41, DIO1 39, BUSY 40 and RESET 42.
It is not the generic XIAO profile for separately wired radio modules. Both
boards retain their parent Full Companion features, USB mOTA sender, WiFi
update slots, and PSRAM-backed offline queue. The V4 also retains direct MQTT.
The [small-screen message layout](v4_pixel5_font_trial.md) uses 6-pixel capitals
on 128x64 panels and 5-pixel capitals on very tiny panels. It includes a compact
channel/sender line and full message previews. Font selection is automatic.
## Build
Enable the optional configuration in the ignored `platformio.local.ini`:
```ini
[platformio]
extra_configs =
variants/*/platformio.ini
platformio.nimble.ini
```
If that file already contains local settings, merge this list with them. Run
one PlatformIO/build.sh command at a time. Both commands below use USA Cascade:
```sh
OUTPUT_DIR=.releases/nimble-v4 bash build.sh build-firmware \
heltec_v4_2_v4_3_companion_radio_full_femon_nimble \
--firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \
--radio-preset usa-cascadia --profile cascade --standard --require-ota
OUTPUT_DIR=.releases/nimble-xiao bash build.sh build-firmware \
Xiao_S3_WIO_companion_radio_full_nimble \
--firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \
--radio-preset usa-cascadia --profile cascade --standard --require-ota
```
Use the merged image at address 0 for a USB installation. The V4 uses 16 MB
flash; the XIAO uses 8 MB. Both use DIO flash mode. Use the application-only
`.bin` for an existing, matching WiFi OTA layout. Remove the optional config
entry when finished to return to the ordinary release matrix.
## Bluetooth identity and pairing
All existing MAC settings remain available from the ASCII USB terminal and
the app's authenticated local CLI. A setting takes effect after reboot.
| Command | Behavior |
| --- | --- |
| `get bluetooth.mac` | Show the saved MAC policy. |
| `set bluetooth.mac C2:17:15:04:00:01` | Use this custom random-static address. |
| `set bluetooth.mac random` | Generate one address and retain it across boots. |
| `set bluetooth.mac random-every-boot` | Generate a new address on each boot. |
| `set bluetooth.mac random-after-connect` | Rotate on the next boot after an authenticated connection; otherwise retain the address. |
| `set bluetooth.mac default` | Restore the factory Bluetooth address. |
| `set bluetooth.stealth on` | Pair once, then accept the saved bonded peer. |
| `set bluetooth.stealth off` | Restore ordinary discovery without changing the MAC policy. |
| `get bluetooth.stealth` | Show pairing or bonded-peer-only state. |
The `ble.mac` and `ble.stealth` aliases also work. A literal custom address must
be a valid BLE random-static address (first byte C0-FF). Forget the old entry
in the phone's Bluetooth settings and pair again after an address change or
switching Bluetooth libraries. Existing Bluedroid bonds are not migrated.
Stealth does not disable the selected rotation policy. If rotation changes
the address on reboot, first pairing opens again for the new identity.
Pairing requires encryption, PIN authentication and a stored bond. NimBLE's
controller byte order is converted explicitly, so a custom address appears
over the air in the order entered. Failed identity setup stops advertising
and retries initialization instead of advertising the wrong address.
## V4 WiFi setup QR
The active WiFi setup page displays a compact QR code with a one-pixel white
border. The normal open setup network fits a 44 x 44 pixel code, with 2 x 2
pixel modules. The SSID, portal address and `HOLD STOP` hint remain beside it.
Short-click to leave the page; hold the button on this page to start or stop
the setup AP. From USB, `start webconfig ap` starts the same setup session.
When the setup AP is inactive and the node has joined WiFi, this page shows
`WIFI CONNECTED`, `IP <address>`, `SETUP AP INACTIVE`, then `HOLD START AP` from
top to bottom. The address appears once assigned and clears when the station
disconnects. While the setup AP is active, the QR page shows the setup portal's
address instead.
## Regression checks
```sh
python3 -B test/test_nimble_companion.py
python3 -B test/test_bluetooth_mac_contract.py
python3 -B test/test_bluetooth_pairing_ui.py
python3 -B test/test_heltec_v4_wifi_setup_page.py
python3 -B test/test_firmware_ram.py
python3 -B test/test_esp32_dram.py
python3 -B test/test_shared_mota_queue.py
python3 -B test/test_companion_mqtt_stack.py
pio test -e native -f test_companion_node_prefs \
-f test_ble_tx_stall_watchdog -f test_companion_frame_queue \
-f test_display_driver
```
The adapter test compiles the actual transport under address/undefined-behavior
sanitizers with API doubles. It injects allocation, identity and bond failures;
checks PIN authentication and stealth; and exercises 176-byte frames, MTU and
notification subscription gating, retry and duplicate prevention. Embedded
builds compile against the pinned real library. RAM reports retain the existing
wireless budget; linked headroom is not a measurement of live free heap.
The MQTT save callback also has a compiler-enforced stack-frame regression
check: nested preference copies previously overflowed the V4 loop task when
saving MQTT settings through WebConfig, despite sufficient free heap.
Hardware qualification must additionally verify pairing and reconnection,
advertised addresses for every MAC policy, contact synchronization, WiFi/BLE
coexistence, and uptime under load. BlueZ results do not establish iOS or
Android camera/pairing compatibility.
This document is now in [Test Results](test-results/nimble_companion_trial.md).
+3 -115
View File
@@ -1,117 +1,5 @@
# ESP32-S3 NimBLE trial results, 2026-09-08
# Document moved
These are local hardware trial results for USA Cascade Full Companions on a
Heltec V4.3 OLED and a Seeed XIAO ESP32-S3 with WIO SX1262. The V4.2 shares the
profile but was not physically tested. Build and command instructions are in
[the trial guide](nimble_companion_trial.md).
<script>window.location.replace(new URL("../test-results/nimble_companion_trial_results/" + window.location.hash, window.location.href));</script>
The final source revision is `9dbab431`, using NimBLE-Arduino 2.5.1 and the
existing Arduino-ESP32 2.0.17 toolchain. These optional profiles are outside the
ordinary release matrix. This trial does not change GitHub release assets or
the firmware picker.
## Capacity
| Measurement | Heltec V4 | XIAO S3 WIO |
| --- | ---: | ---: |
| Contacts / channels | 350 / 40 | 350 / 40 |
| PSRAM-backed offline queue | 512 | 256 |
| Application image, bytes | 1,774,600 | 1,510,536 |
| OTA application slot, bytes | 6,553,600 | 3,342,336 |
| Linked internal capacity before runtime allocation, bytes | 263,944 | 265,576 |
| Required runtime RAM budget, bytes | 173,056 | 148,480 |
| Headroom beyond that budget, bytes | 90,888 | 117,096 |
| Live free heap with Bluetooth and 350 test contacts, bytes | 142,644 | 147,444 |
| Minimum free heap during that contact test, bytes | 138,024 | 142,788 |
The live contact-test readings are from revision `7f1bd8b1`. Revision
`9dbab431` retains the same Bluetooth adapter, contact allocation and queue
sizes, and fixes the MQTT save stack use described below. Linked capacity is
not live free heap; the firmware memory reports label that distinction.
A matching V4 build using Bluedroid had 248,840 bytes of linked internal
capacity. NimBLE recovers 15,104 bytes at link time. The matching Bluedroid
application was 2,169,848 bytes. There was no matching physical Bluedroid heap
measurement, so these figures do not establish a live heap saving.
## Bluetooth hardware checks
Both boards passed the following cases with BlueZ on the Mercerwood Pi:
- PIN authentication, encryption, stored bonding, and bonded reconnection.
- MTU 179; accepted 176-byte writes and complete 148-byte contact notifications.
- Insert and synchronize all 350 synthetic contacts without missing or
duplicate entries.
- Custom MAC address appears over the air in the entered byte order and
persists across reboots.
- Saved random MAC persists; random-every-boot changes on each boot.
- Random-after-connect retains its address through an unauthenticated boot
and rotates on the boot following an authenticated connection.
- Stealth first pairing, bonded-peer reconnection, and reconnection after
reboot with a stable identity.
- Restore factory MAC and ordinary discovery.
- Three reconnect samples with no observed downward free-heap trend.
The complete MAC suite ran on `7f1bd8b1`. Final-image connection and update
checks also verify the unchanged adapter after the MQTT stack fix. Synthetic
contacts were removed after testing; factory MAC mode and stealth off were
restored.
## MQTT save stack overflow
The V4 test reproduced an actual reset while saving MQTT settings through
WebConfig. Serial output identified `Stack canary watchpoint triggered
(loopTask)`. `onConfigBatchEnd()` kept a second 2.8 KB preference copy while
the loader and NVS code also used stack space. Free heap was not the limiting
resource.
The loader already updates its destination only after validation, so the
callback now passes the live preference object directly. The actual Xtensa
callback frame shrank from 2,928 bytes to 32 bytes. The regression test
compiles the real callback with a 512-byte frame ceiling and demonstrates that
the previous nested-copy implementation fails the check.
On the corrected `9dbab431` image, the 11-setting WebConfig batch completed
with all entries accepted and no reboot. The V4 connected to a local
Mosquitto broker, published MQTT traffic, and kept its authenticated Bluetooth
session alive through a 30-second load interval with continuous uptime and
zero reported error flags. Free heap measured 120,684 then 122,140 bytes;
the minimum was 118,332 bytes and the largest free block was 110,580 bytes.
Disabling the test MQTT slot also completed successfully without reboot.
## WiFi updates and QR
Both boards completed real application-image uploads over WiFi with Bluetooth
connected, followed by reboot and reconnection using the saved bond. Repeated
HTTP page requests and Bluetooth status requests ran together before upload.
The image hash was checked before transmission, and the running firmware
version was checked after reboot.
The V4 WiFi setup page again contains a QR code. The default open setup SSID
fits a 44 x 44 pixel code: 2 x 2 pixel modules and an exact one-pixel white
border. The native test rendered the actual display/QRCode code and decoded
the result with ZXing. SSID, portal address and the hold-to-stop hint fit
beside it. Physical phone-camera scanning has not been verified.
## Observed limits
One XIAO warm reboot left its USB endpoint unresponsive while Bluetooth still
answered. The user's physical unplug/replug restored USB; the later full MAC
suite completed its commanded reboots successfully. The cause of that one
USB failure is unconfirmed.
The XIAO initially failed to associate with the saved WiFi network. Reapplying
the SSID scheduled a fresh connection, after which it joined at -53 dBm and
completed WiFi OTA. No antenna change was needed for that recovery.
This was functional hardware testing, not a long-duration soak. iOS/Android
app pairing, phone camera scanning, multiple TLS MQTT brokers, and an
end-to-end LoRa mOTA transfer were not qualified in this trial. The XIAO parent
profile does not include direct MQTT; the V4 does.
## Final application hashes
| Hardware | SHA-256 |
| --- | --- |
| V4 | `e6a1522ced4ed38ddd00e7cdf8393857224adbb622888a86370c0f7a3ee28d88` |
| XIAO S3 WIO | `ecf7afe6222e3897556a04e022515f9dcfcb825172dae9eaaa72eb92396dd004` |
This document is now in [Test Results](test-results/nimble_companion_trial_results.md).
+3 -44
View File
@@ -1,46 +1,5 @@
# nRF52 USB READY hang
# Document moved
The RAK3401 on Mercer was found stuck in the framework's TinyUSB power handler:
USBD was enabled and attached, but its write-one-to-clear READY event was
already consumed. The duplicate-event guard also required HFCLK to be running.
If that guard missed the completed initialization, the following unbounded
READY wait could stop application startup. A bootloader update alone cannot
replace this USB driver in an already installed MeshCore application.
<script>window.location.replace(new URL("../test-results/nrf52-usb-ready-fix/" + window.location.hash, window.location.href));</script>
The wait and guard were inherited through the pinned Adafruit nRF52 Arduino
framework, not introduced by the recent OTAFIX upstream merge. TinyUSB's wait
dates back at least to 2019; the guard was added in
[upstream commit 7d9efd0697](https://github.com/hathach/tinyusb/commit/7d9efd06979f6cdde2a4093f0c26e8100312c92c).
OTAFIX's August 24 HFCLK backport addressed a later clock wait, leaving this
earlier READY wait uncovered. The exact live interrupt ordering is not known;
the stalled program counter and consumed event were read directly over SWD.
All nRF52 environments inherit `pre:scripts/nrf52_usb_power_fix.py`. It compiles
a build-local replacement for the matching framework driver, without changing
PlatformIO's shared SDK or affecting ESP32 builds. A changed/unrecognized
framework implementation stops the build for review. The backport:
- requests HFCLK before checking the consumed READY event;
- rechecks attachment while waiting, including completion by another callback;
- bounds both waits with one 100,000-poll budget, independent of OS ticks;
- ignores stale READY on a disabled peripheral and exits if it is removed.
- replays an already asserted regulator READY state when the application USB
task starts with VBUS present, as the bootloader does. The framework's
original startup replayed DETECTED only, so a missing later power edge could
leave the new application's USB transport unattached after a UF2 handoff.
The poll budget is an iteration limit, not a promised elapsed-time timeout.
If the clock/peripheral never becomes ready, the caller returns instead of
freezing the application. A later power event can retry; permanent electrical
failure can still require reconnecting USB. This does not establish or fix the
separate Pi hub/controller fault.
Run `python3 -B test/test_nrf52_usb_power.py -v`. The tests compile the actual
patched handler from the pinned-framework fixture, model W1C event semantics,
and cover duplicate/nested callbacks, delayed/missing clocks and READY, removal,
retry, and detached USB. Restoring the inherited READY prefix must reproduce
the infinite wait under a subprocess deadline. Additional tests check SDK
isolation, idempotence, fail-closed patching, the startup DETECTED/READY order,
and all nRF52 environment hooks.
The harness is shared with OTAFIX's TinyUSB fork at
`test/otafix/nrf5x_power_test.py`; keep the two copies in sync when extending it.
This document is now in [Test Results](test-results/nrf52-usb-ready-fix.md).
+3 -66
View File
@@ -1,68 +1,5 @@
# nRF52 Companion automatic ExtraFS recovery
# Document moved
Companion builds using internal ExtraFS reserve 100 KiB at
`0xD4000..0xED000`. Identity and preferences remain in the separate 28 KiB
primary store at `0xED000..0xF4000`. This applies to internal-ExtraFS Companion
builds, including Full, not repeaters or external QSPI storage.
<script>window.location.replace(new URL("../test-results/nrf52_companion_storage_recovery/" + window.location.hash, window.location.href));</script>
At startup the firmware:
1. Mounts primary storage without erasing nonblank media. Only proven-blank
primary storage may be initialized automatically.
2. Checks the exact ExtraFS address, size, block geometry, and application
linker reservation, then attempts a non-formatting secondary mount.
3. Validates primary filesystem metadata before any destructive secondary
recovery. An unavailable primary store blocks recovery and identity replacement.
4. Keeps an already mounted, metadata-valid ExtraFS unchanged. If its initial
mount or validation failed, unmounts and retries once without formatting.
5. If the retry remains unusable, automatically formats only the reserved
100 KiB region, then remounts and validates it. There is one format attempt
per boot; failure keeps the contact/channel write quarantine active.
6. Runs the existing verified migration from primary into ExtraFS. Primary
contact/channel sources are retired only after every destination is copied,
read back, and the migration transaction is committed. Identity and
preferences stay on primary.
No terminal repair command, phone intervention, or bootloader update is needed
for this recovery path when a compatible application/layout is already used.
After successful recovery, the Companion storage API reports 100 KiB total
rather than the 28 KiB primary fallback.
## Data-loss boundary
Automatic rebuilding prioritizes a usable contact/channel store over retaining
an unreadable secondary filesystem. Data stored only in that damaged region
can be lost. Surviving primary files are migrated, but may not contain the
latest secondary-only changes. An API backup remains advisable before flashing.
Primary storage is never formatted by this recovery path.
This is mount/metadata recovery, not forensic file recovery. An invalid
migration journal, unreadable individual contact page, primary-source error,
full filesystem, or failed migration cleanup does **not** on its own trigger a
format of a mountable, metadata-valid secondary store. Those cases retain the
existing validation, retry, and incomplete-load protections.
## Regression coverage
`test/test_internal_secondary_fs_repair` exercises healthy/no-format startup,
successful remount, failed remount/traversal followed by one repair, and
format/remount/validation failure. `test/test_nrf52_extrafs_contract.py` checks
primary-first ordering, internal-only geometry guards, quarantine handling,
and the separation between migration failures and destructive recovery.
## T1000-E hardware qualification (2026-09-03)
A device reporting 25/28 KiB primary fallback and contact enumeration error 5
was flashed with the recovery build. With no explicit repair command, startup
activated 100 KiB ExtraFS and loaded all 125 contacts and 40 channel slots.
Identity, preferences, radio settings, and channels matched the pre-update API
snapshot. Add, edit, and delete of an owned temporary contact each survived a
separate normal reboot; the device ended with the original 125 contacts and
27/100 KiB usage. The 161 offline/native regression tests and target build passed.
This was not lossless recovery of every historical RAM value: 21 contacts had
older advertisement/local-modification timestamps, including one older advertised
location. No contact was missing, and no computer-backup restoration was used.
A subsequent read-only scan found no readable cached advert for any of the 125
contacts. Without a surviving newer copy, firmware cannot infer those earlier
values; receiving new verified advertisements can refresh them normally.
This document is now in [Test Results](test-results/nrf52_companion_storage_recovery.md).
+123
View File
@@ -0,0 +1,123 @@
# Markdown audit — 1.17.1.5 USA Cascade
Audited on 2026-09-05 against firmware source `26303793`, the resolved build
configuration, and the qualified 1.17.1.5 staging manifests. The scope was all
**71 tracked Markdown files** present at the start, including root documents,
docs, examples, tools, tests, variants, and vendored library documentation.
Generated/ignored build directories and external repositories were outside that scope.
## Corrections
- Added [role-specific feature switches](../role_feature_switches.md), exact
Companion/infrastructure MQTT and logging controls, board exceptions, SNMP,
and the [USB web console](https://flasher.meshcore.io/console).
- Added an [exact-name variant map](1.17.1.5-variant-map.tsv); every one of its
1,325 covered entries resolves to a staged firmware file. The 36 excluded
entries are identified rather than linked to substitute hardware.
- Corrected universal 254-neighbor claims, Full Companion MQTT filename/CLI
assumptions, TLora Full WebConfig availability, and partition/NVS guidance.
- Documented the power-saving guards present at audit time; current instructions
now use the shared `get powersaving` / `set powersaving on|off` command set.
Corrected the RS232 baud range and SNMP environment list.
- Linked OTAFIX 2.4.6 and its board/storage requirements from operator guides;
aligned the two manual USA OTA quickstarts with 910.525 MHz.
- Marked old provider catalogs and dated hardware results as historical;
documented the legacy tag workflow that also matches `repeater-room-*`.
- Fixed the vendored MQTT documentation link and documentation-site links
that incorrectly treated repository source files as generated site pages.
## Verification and limits
All original Markdown files were scanned for relative links, section anchors,
source-path references, version/default claims, and role/OTA terminology.
Flagged operator instructions were checked against CommonCLI, its observer
extension, Companion command handlers, board configuration, and release manifests.
Brace-expanded source references and SDK-relative paths were retained as such.
Historical measurements, changelogs, upstream findings, contribution/security
policies, and licenses were not rewritten as new validation results.
The documentation site passed a strict MkDocs Material build. Relative repository
links and section anchors were checked separately, including documents outside
the MkDocs source directory. Console and current bootloader links were checked
on their primary sites. This was not a crawl of every third-party URL or a
new hardware/firmware qualification; no claim is made that every board was
physically tested or every protocol statement independently re-proved.
## Original file coverage
“Retained” means no correction was identified by this documentation audit;
it does not certify all historical statements as current hardware behavior.
| File | Disposition |
| --- | --- |
| [AGENTS.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/AGENTS.md) | Scanned; retained |
| [ALERTS.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/ALERTS.md) | Scanned; retained |
| [CHANGELOG.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/CHANGELOG.md) | Historical record retained |
| [CONTRIBUTING.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/CONTRIBUTING.md) | Scanned; retained |
| [MEMORY_MONITORING.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MEMORY_MONITORING.md) | Scanned; retained |
| [MQTT_IMPLEMENTATION.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_IMPLEMENTATION.md) | Corrected or clarified |
| [MQTT_INTERNALS.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_INTERNALS.md) | Corrected or clarified |
| [MQTT_OWNERSHIP.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_OWNERSHIP.md) | Historical record retained |
| [MQTT_SNMP.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/MQTT_SNMP.md) | Corrected or clarified |
| [README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/README.md) | Corrected or clarified |
| [RELEASE.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/RELEASE.md) | Corrected or clarified |
| [RESTORE_UPSTREAM_NOTES.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/RESTORE_UPSTREAM_NOTES.md) | Historical record retained |
| [SECURITY.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/SECURITY.md) | Scanned; retained |
| [STABILITY_TESTABILITY_HANDOFF.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/STABILITY_TESTABILITY_HANDOFF.md) | Historical record retained |
| [UPSTREAM_BUGS.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/UPSTREAM_BUGS.md) | Scanned; retained |
| [WEB_CONFIG_REVIEW.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/WEB_CONFIG_REVIEW.md) | Corrected or clarified |
| [arch/stm32/Adafruit_LittleFS_stm32/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/arch/stm32/Adafruit_LittleFS_stm32/README.md) | Vendored documentation/license retained |
| [arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/LICENSE.md) | Vendored documentation/license retained |
| [arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/arch/stm32/Adafruit_LittleFS_stm32/src/littlefs/README.md) | Vendored documentation/license retained |
| [docs/WiFi.md](../WiFi.md) | Corrected or clarified |
| [docs/cli_build_matrix.md](../cli_build_matrix.md) | Corrected or clarified |
| [docs/cli_command_availability.md](../cli_command_availability.md) | Corrected or clarified |
| [docs/cli_commands.md](../cli_commands.md) | Corrected or clarified |
| [docs/companion_offline_queue.md](../companion_offline_queue.md) | Scanned; retained |
| [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/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 |
| [docs/flood_filtering.md](../flood_filtering.md) | Scanned; retained |
| [docs/full_companion_features.md](../full_companion_features.md) | Corrected or clarified |
| [docs/full_companion_usb_switcher.md](../full_companion_usb_switcher.md) | Corrected or clarified |
| [docs/gps_tracking.md](../gps_tracking.md) | Scanned; retained |
| [docs/halo_keymind_settings.md](../halo_keymind_settings.md) | Corrected or clarified |
| [docs/hardware_validation_checklist.md](../test-results/hardware_validation_checklist.md) | Corrected or clarified |
| [docs/host_cli_service.md](../host_cli_service.md) | Scanned; retained |
| [docs/index.md](../index.md) | Corrected or clarified |
| [docs/kiss_modem_protocol.md](../kiss_modem_protocol.md) | Scanned; retained |
| [docs/lora_ota_automation.md](../lora_ota_automation.md) | Corrected or clarified |
| [docs/mbedtls-tls-footprint.md](../test-results/mbedtls-tls-footprint.md) | Scanned; retained |
| [docs/nrf52_companion_storage_recovery.md](../test-results/nrf52_companion_storage_recovery.md) | Scanned; retained |
| [docs/nrf52_power_management.md](../nrf52_power_management.md) | Scanned; retained |
| [docs/number_allocations.md](../number_allocations.md) | Scanned; retained |
| [docs/ota_easy.md](../ota_easy.md) | Corrected or clarified |
| [docs/ota_meshtower_v2_sdcard.md](../ota_meshtower_v2_sdcard.md) | Corrected or clarified |
| [docs/ota_nrf52_bootloader_update.md](../ota_nrf52_bootloader_update.md) | Corrected or clarified |
| [docs/ota_nrf52_qspi.md](../ota_nrf52_qspi.md) | Corrected or clarified |
| [docs/ota_protocol.md](../ota_protocol.md) | Corrected or clarified |
| [docs/ota_user_guide.md](../ota_user_guide.md) | Corrected or clarified |
| [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/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 |
| [docs/telemetry_decoder.md](../telemetry_decoder.md) | Scanned; retained |
| [docs/terminal_chat_cli.md](../terminal_chat_cli.md) | Scanned; retained |
| [docs/usb_serial_backpressure.md](../test-results/usb_serial_backpressure.md) | Scanned; retained |
| [examples/host_cli_service/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/README.md) | Scanned; retained |
| [lib/PsychicMqttClient/CHANGELOG.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/lib/PsychicMqttClient/CHANGELOG.md) | Vendored documentation/license retained |
| [lib/PsychicMqttClient/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/lib/PsychicMqttClient/README.md) | Corrected or clarified |
| [mesh-america/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/mesh-america/README.md) | Corrected or clarified |
| [test/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/test/README.md) | Scanned; retained |
| [test/test_mqtt_prefs_codec/FIXTURES.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/test/test_mqtt_prefs_codec/FIXTURES.md) | Historical record retained |
| [tools/mota/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/mota/README.md) | Scanned; retained |
| [tools/sensecap_indicator_font/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/sensecap_indicator_font/README.md) | Scanned; retained |
| [tools/sensecap_indicator_rp2040/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/tools/sensecap_indicator_rp2040/README.md) | Scanned; retained |
| [variants/sensecap_indicator-espnow/README.md](https://github.com/mikecarper/MeshCore/blob/keymindCascade/variants/sensecap_indicator-espnow/README.md) | Scanned; retained |
File renamed without changes.
Internal Server Error - Gitea: Git with a cup of tea
500 Internal Server Error

An error occurred:

An error occurred

Gitea Version: 28.2.0