Files
HaloKeymind/docs/radio_profile_switch_validation.md
T
mikecarper 091c8d8324 Preserve radio lab experiments and reuse unchanged SX1262 modulation
Skip redundant modulation writes during owned fast RX retunes when the last
acknowledged SF/BW/CR/LDRO tuple matches. Invalidate that cache after ordinary
setters, failed writes, and lifecycle changes.

Include the remaining scan, preamble, settling, and memory-soak experiments,
their collectors, validation notes, and original capture records. Preserve
capture bytes across checkouts and keep private soak credentials local.

Run lab collector and compiled contract tests in CI. Update the expectation,
profile mapping, and result-buffer tests for the extended lab tools, and make
the private WiFi override header optional for ordinary soak diagnostics.

Validation: 145 host tests passed from the staged source snapshot. Clean
heltec_v4_repeater and Xiao_S3_WIO_companion_radio_usb builds passed their
RAM/flash gates. All 229 staged capture files retain their original bytes;
all 75 local documentation links resolve in the clean snapshot.
2026-09-14 22:34:17 -07:00

26 KiB

SX1262 profile-switch validation

Measured locally on 2026-09-13 using an XIAO ESP32-S3 with WIO SX1262. Later tests also use the SenseCAP Indicator ESP32-S3 LoRa processor. The V8 integration is the current implementation; V1-V7 sections/captures document the development history.

Initial result (V1)

The production retune path averaged 1.434 ms with XOSC held, versus 3.166 ms with RC standby. Holding the oscillator saved 1.732 ms per retune (54.7%). Warm-mode maximum was 1.467 ms across 6,000 measured retunes. The 6 ms switch allowance and 4 ms loop allowance are unchanged.

Each row combines both directions and two reversed-order trials, with 2,000 measured retunes per standby policy. Times run from production tuneProfile entry until BUSY is observed low. GetStatus confirmed RX after every timed hop.

Secondary profile RC mean / maximum XOSC mean / maximum
SF7 / 500 kHz 3.161 / 3.199 ms 1.434 / 1.467 ms
SF8 / 500 kHz 3.166 / 3.209 ms 1.435 / 1.464 ms
SF9 / 500 kHz 3.171 / 3.193 ms 1.433 / 1.437 ms
  • Primary: 909.5 MHz, SF7 / 62.5 kHz, CR 4/5.
  • Secondary: 910.5 MHz, CR 4/5, automatic production preamble calculation.
  • TCXO configuration remained 1.8 V and 1,600 us startup delay.
  • 12,000 measured retunes; zero retune failures, BUSY timeouts, or RX-mode errors.
  • 109 packet/busy deferrals were excluded from hop timings (57 RC, 52 XOSC). Deferral preserves the existing packet and radio ownership checks.
  • The first eight successful hops of every trial were excluded from steady-state statistics, including the initial oscillator transition.
  • All 12 trials returned to RC standby after disabling the second profile.

Scope and reproducibility

The timing harness compiles the actual production wrapper, not the earlier fast/minimal RX benchmark. RC and XOSC use the same executable: the baseline changes only the standby policy after entering dual-profile mode. No TCXO supply or initial startup delay is disabled. The normal guard checks and frequency/SF/BW/CR/preamble setters remain active.

This isolates radio switching from UI, Wi-Fi, BLE, mesh dispatch, and scan-loop scheduling. BUSY polling/timer overhead is included; the RX status query is outside the timing. This is not a packet-delivery test, first-startup timing, power measurement, wide-frequency image-recalibration test, or proof of analog RF settling beyond BUSY/RX status. Indicator I/O-expander latency and nRF52 timing still need their own measurements before reducing common budgets.

Raw results: profile_switch_results.json. The collector requires all requested samples, zero failure/timeout/mode-error counts, and successful RC restoration before accepting a trial.

Build provenance:

  • Base commit: c329cf1fc1e7f2e8c8733ab0d000094730458fe3, with the local warm-standby patch.
  • Environment: profile_switch_xiao; Espressif32 6.11.0 / Arduino 2.0.17.
  • RadioLib: pinned 187ef24791c3d844939b2be13a68bd890bd04e4c (7.7.1).
  • Test firmware.bin SHA-256: 629d39bba80ad97c434fbaab61bc7c3c302b5ac8180cb2cb12806508b2d111e0.
  • Existing unrelated Companion/repeater/Wi-Fi soak changes were not included in this minimal timing executable.

Host regression checks also passed: profile scan/CLI (3 tests), receive contract (2 tests), and SX126x RX-status decoding (1 test). A normal heltec_v4_companion_radio_usb build and the XIAO timing build succeeded.

The XIAO's original 8 MiB flash snapshot was verified against the device while held in the bootloader before the timing firmware was uploaded. Raw flash backups stay in the current user's private backup directory, outside Git. After testing, the entire original snapshot was restored and verified again against the device before reboot. The live USB CLI then confirmed Xiao S3 WIO running v1.17.1-soak-xiao-B (11-Sep-2026). Its soak logger was left stopped; existing logs were preserved. The Indicator and RAK Tag were not flashed.

Follow-up: batched modulation

On the same date, a second 12,000-retune experiment held XOSC warm in both cases and compared the old three-setter sequence with one combined LoRa modulation update. Both paths ran in the same V2 executable, with the second round reversing their order. Each mode completed 6,000 timed hops.

Secondary profile Separate setters: mean / maximum Batched: mean / maximum
SF7 / 500 kHz 1.442 / 1.473 ms 1.159 / 1.198 ms
SF8 / 500 kHz 1.444 / 1.480 ms 1.158 / 1.192 ms
SF9 / 500 kHz 1.446 / 1.478 ms 1.156 / 1.163 ms

Overall: 1.444 ms to 1.158 ms, saving 0.286 ms (19.8%) per retune. Use this matched V2 baseline for the batching comparison; the earlier V1 warm-standby numbers came from a different executable layout.

There were zero retune failures, BUSY timeouts, RX-mode errors, or SF/BW/CR cache mismatches. All 12 trials restored RC standby after disabling profile 2. 84 packet/busy deferrals were excluded (54 separate-setter, 30 batched). First eight successful hops per trial were excluded as before.

The batching removes two repeated modem checks and two modulation writes. The helper validates a complete MeshCore bandwidth/SF/CR tuple, updates RadioLib's cached SF/BW/CR before its automatic LDRO calculation, and restores the old cache if the command fails. The wrapper's existing physical rollback and safe-transition paths are retained. Frequency changes, preamble updates, standby, IRQ/buffer setup, packet parameters, and RX restart remain unchanged. This does not qualify eliminating those other repeated operations.

Raw results: profile_switch_batch_results.json. V2 test firmware SHA-256: b4403f6f5eb0a2b0a31c2d79859122225f247296699a578ad2596ef17eaed0fd. Toolchain and RadioLib pin match the first experiment. The normal heltec_v4_companion_radio_usb build also succeeded with batching enabled.

The two new host tests execute the production method over 1,200 valid bandwidth/SF/CR/LDRO combinations, invalid inputs and wrong modem, command failures with cache rollback/retry, and the wrapper's failure short-circuit. All eight relevant host tests passed. RF packet delivery and application/UI scheduling remain outside this timing experiment; shared budgets are unchanged.

Expanded screening: redundant RX setup and SPI (V5)

Corrected V5 measurements on 2026-09-13 cover 28,800 timed retunes in 300 trials across XIAO S3/WIO SX1262 and the Indicator ESP32/SX1262. Each sweep uses SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, both directions, reversed case order in round two, and eight excluded initial hops per trial. All 300 trials completed their sample count and restored RC standby on exit. There were zero timed switch failures, BUSY timeouts, RX-mode errors or SF/BW/CR cache mismatches. The normal packet guards deferred 175 attempted hops; these were not counted as timed retunes.

Twenty-one distinct cases were screened: seven individual redundant-operation changes, a full manual RX control, combinations, 2/4/8 MHz SPI and whole-buffer ESP32 SPI transfers. Shared controls appear in both groups. All new shortcuts are HIL-only, not included in normal targets.

Individual and combined RX changes

Means in milliseconds, with warm oscillator and batched modulation throughout. Each XIAO row has 768 timed hops; each Indicator row has 384.

RX/SPI case XIAO mean Indicator mean
Production batching, 2 MHz SPI 1.182 15.190
Manual RX sequence, no omissions 1.181 15.135
Skip second standby 1.124 14.195
Skip repeated IRQ setup 1.096 14.399
Skip repeated buffer-base setup 1.146 14.437
Skip repeated packet-parameter setup 0.991 12.364
Skip RX modem query 1.145 14.427
Defer preamble to the one RX packet-parameter write 0.956 11.659
Skip wake NOP when already awake 1.146 14.576
Combined omissions, early preamble write 0.771 9.096
Combined omissions, deferred preamble 0.732 8.384
4 MHz SPI only 0.985 14.996
8 MHz SPI only 0.889 14.889
Combined/deferred, 4 MHz SPI 0.627 8.280
Combined/deferred, 8 MHz SPI 0.579 8.224

The largest individual saving is programming preamble/packet parameters once. Compare individual omissions with the manual full control when attributing their isolated costs: replacing RadioLib's staging dispatch also has overhead. Individual savings are not additive; some remove overlapping work.

Whole-buffer SPI follow-up (same V5 binaries)

Case XIAO mean / maximum Indicator mean / maximum
Production batching, byte loop, 2 MHz 1.184 / 1.221 ms 15.185 / 15.785 ms
Bulk transfer only, 2 MHz 1.070 / 1.073 ms 15.092 / 15.134 ms
Bulk transfer only, 4 MHz 0.890 / 0.903 ms 14.924 / 14.983 ms
Bulk transfer only, 8 MHz 0.797 / 0.806 ms 14.836 / 14.908 ms
Combined/deferred, byte loop, 8 MHz 0.579 / 0.588 ms 8.222 / 8.843 ms
Combined/deferred + bulk, 2 MHz 0.671 / 0.700 ms 8.369 / 9.027 ms
Combined/deferred + bulk, 4 MHz 0.582 / 0.595 ms 8.252 / 8.315 ms
Combined/deferred + bulk, 8 MHz 0.535 / 0.545 ms 8.203 / 8.233 ms

The fastest XIAO candidate saves 0.649 ms (54.8%) versus that sweep's already-warm, already-batched production control. At 8 MHz, bulk transfers alone add about 0.043 ms of benefit to the combined RX shortcut. The Indicator remains dominated by I2C-mediated chip-select/BUSY/DIO access; the final bulk step improves its combined mean by only 0.019 ms. Small HAL effects varied between executable layouts in preliminary screens; do not generalize them.

The Indicator exceeds the generic 6 ms switch allowance even in its fastest experimental path. These are board measurements, not a justification for reducing the shared 6 ms switch / 4 ms loop constants. Indicator scheduling and effective preambles need board-specific treatment before a production rollout. Application/display/network scheduling is still excluded here.

Raw accepted sweeps:

Test corrections and provenance

V3/V4 broad screening accidentally called SX1262::startReceive() instead of CustomSX1262::startReceive() in the fallback, omitting MeshCore's extra preamble-detection IRQ flag. Their raw files are marked superseded and are not included in the counts or conclusions above. The V4 packet sweep was stopped on discovery. V5 preserves that flag in both normal and manual RX paths; a host regression test checks the Custom fallback and mapped flags. The earlier V1/V2 warm/batching experiments did not use that experimental subclass and are unaffected.

An initial V5 XIAO capture stopped on a partial serial JSON line during guarded mode exit. It is retained as incomplete, not counted above. The host now accumulates reads until newline, with regression coverage. Initial setup rejections are retried only for the explicit pre-measurement packet-guard reply, bounded to three seconds and counted; packet losses and timed failures are never retried away.

Final V5 firmware SHA-256:

  • XIAO: 5506a40ffa8240201aca519e8e24db3647f897e7a1290bba22be6e4986d8f02f
  • Indicator: dd595b2e7fa8c1ccb14fac9223f1a57277cd6a602864ee9bf090672f494fa2f6

Base commit/toolchain/RadioLib pin match the earlier experiments. Thirteen unique host tests passed (eight production regressions, two HIL boundary/buffer tests, three serial-reader/setup-guard tests). The HIL tests cover all 255 nonzero omission masks, normal-path fallbacks, preserved IRQ clear/SetRx, error short-circuit and full-duplex buffer lengths 1..260. They are not full driver lifecycle tests. An additional normal t1000e_companion_radio_usb nRF52 build passed with the production warm/batching changes: 591,484-byte app, runtime RAM guard passed. It was not flashed to the RAK or any nRF52 board.

Qualification boundary

No BUSY wait, command status validation, stale-IRQ clear, RX command or wrapper packet-ownership guard is removed. The fastest path relies on a known RX-to-RX window and established RX setup. Correct invalidation after TX, CAD, sleep, reset, failed commands, and recovery must be designed and tested before promoting these shortcuts. The per-byte/bulk HAL experiment is ESP32-specific. Sensitivity/PER, oscillator power draw, large-frequency image recalibration, and application-driven continuous scanning remain unqualified.

Actual packet checks and limitations

Final forward matrix: Indicator transmitter to XIAO receiver, -9 dBm, 21 cases x SF7/8/9 x both profiles x 16/64/255-byte payloads = 378 probes. 359 sequence/length/payload/profile checks succeeded, 13 receiver-declared wait timeouts occurred, and six receiver USB replies did not arrive before the host deadline. Transmitter replies returned success. Successfully read packets had RSSI -42 to -39 dBm and SNR 10.2 to 14.2 dB. Timeout diagnostics reported RX mode and no device errors; some retained a preamble IRQ. These observations do not establish why packets were missed.

The production-batched baseline delivered 17/18 in that matrix; the fastest combined/bulk/8 MHz candidate delivered 18/18. An earlier reversed-order smoke test of the fastest candidate had one receiver timeout, so the clean 18-packet batch is not a reliability qualification. Earlier 0 dBm checks also had baseline and candidate misses. Reducing power did not eliminate them.

Reverse direction: XIAO transmitter to Indicator receiver, -9 dBm, baseline, combined/8 MHz and combined/bulk/8 MHz, with the same SF/profile/length matrix. All 54 payloads arrived intact, 18/18 per case. However, one XIAO USB TX acknowledgement was delayed past the host deadline, after which 32 later records picked up the previous command's acknowledgement. Thus the raw file has only 21 fully matched transactions despite 54 correctly received payloads. This is sequence-correlated evidence of a host acknowledgement problem, not 33 RF losses. Subsequent collector code rejects/stores stale sequence replies instead of associating them with a new command, and now asserts DTR on Windows to match the existing s3_memory_soak.py HWCDC session policy. The captures above used DTR deasserted; a causal DTR A/B test was not performed. Sequence handling has host regression coverage, but these last host changes were not rerun on HIL firmware after the boards were restored.

USB acknowledgements and actual receiver payload validity are recorded separately. Missing acknowledgements never trigger an automatic retransmit. The host drains the receiver concurrently with the transmitter's command and records timeouts without hiding them. These tests establish some real packet functionality, not a controlled packet-error-rate comparison. Fix/qualify the test transport, repeat with a stable RF reference, then test TX/CAD/reset/RXPS and continuous application scanning before promoting the shortcuts.

Earlier incomplete packet captures and superseded screens are retained for audit, not counted in the 378/54 final matrices. No identity or channel secrets were transmitted: probes used synthetic sequence-tagged bytes.

Production integration and USB recovery (V8)

Normal XIAO S3 WIO and Indicator LoRa variants now opt in to buffered 8 MHz SPI and a guarded fast RX-to-RX path. Other SX1262 variants retain default SPI/full RX setup; modulation batching and warm standby remain shared.

The fast path keeps the TCXO warm, applies one SF/BW/CR tuple and one packet tuple, clears stale IRQs and starts RX with the original BUSY handling. It reuses an established LoRa IRQ map and buffer bases only within an owned continuous-RX retune. Ordinary standby, staging/TX, CAD, sleep, reset/init, RX duty cycling and command failures invalidate that reuse. RX startup failure now fails the whole profile transaction and restores the old profile/caches with full setup; failed recovery keeps the profile marked for refresh.

Sleep recovery discovered during integration

Initial lifecycle captures found XOSC_START_ERR (0x20) after retained sleep, despite successful RX status. RC-policy controls reproduced the same flag: this was not established as a regression caused by the fast path. Simply waking in RC and entering standby before sleep did not clear it. Reapplying the already configured TCXO voltage and 1,600 us delay resolved it.

The production sleep override now enters standby first, wakes in RC, and restores TCXO configuration on wake. It refuses to clear unrelated device faults. The warm policy resumes after full RX setup. This follows the standby entry requirement in section 13.1.1 of the Semtech SX1261/2 datasheet. Section 13.3.6 describes TCXO control and the XOSC error flag; it does not by itself prove the cause of this retained-sleep observation.

Diagnostic files profile_switch_v6_recovery_xiao.json, profile_switch_v7_recovery_xiao.json, profile_switch_v7_recovery_xiao_complete.json (despite its filename, its complete field is false), and profile_switch_v7_sleep_diagnostic_xiao.json retain these failures. They are not counted as passing lifecycle tests.

USB fix and scope

The HIL host distinguishes Espressif native CDC (DTR asserted) from CH340 bridges (DTR/RTS deasserted). Replies are newline-assembled and sequence-matched. Firmware caches completed packet and timing reports before emitting and flushing them. A timeout or malformed report triggers a sequence-scoped result query, never a repeated TX or timing run. Randomized session sequence starts avoid a cached result from an earlier host session.

V7 had successfully fixed/retested packet result replay but a long native-USB timing sweep still lost one response. V8 adds whole-report timing buffering and replay. The incomplete V7 XIAO timing capture is retained, not silently completed. The public MeshCore Companion USB protocol is unchanged; these USB fixes are for the radio-lab transport. The previous broad Windows-DTR policy described in the V5 history above has been replaced by native-device identification.

Both boards passed 20 reopen cycles, deliberate lost timing replies, deliberate lost TX replies, stale TX reply isolation, and 18 lifecycle cases each. The cases cover TX, CAD, sleep, reset/init, RC-policy sleep and explicit TCXO-restoration controls, repeated three times. Fast counters prove one full RX fallback after invalidation and reuse only on the following retune. All command results, RX-mode checks and device-error checks passed.

V8 test firmware SHA-256 (same pinned RadioLib/toolchain as above):

  • XIAO: e1806bbf4af5a88b31423131806a682713221082235507ff95e5beb9ef3ac067
  • Indicator: bab4009078a4fff68dfb7aa73a5afc83e84f5c47589354d8aeb8e5b8d8798442

Normal USB Companion builds passed for Xiao_S3_WIO_companion_radio_usb (813,901 app bytes / 63,012 static RAM), SenseCapIndicator-LoRa_comp_radio_usb (879,809 / 62,960), and t1000e_companion_radio_usb (591,980 / 179,868). Runtime RAM budgets passed on all three. These normal application images were compiled, not substituted for the user's configured soak firmware during the HIL tests.

Host/native regression coverage includes production modulation caches/LDRO, standby failure short-circuit, RX-start rollback failure, ownership guards, fast-state invalidation, TCXO wake fault preservation, buffered SPI semantics, USB result overflow/replay, partial reads and stale-sequence handling.

Final steady-state timing

V8 ran 256 timed retunes per trial, four production configurations, SF7/8/9 at 500 kHz paired with SF7/62.5 kHz, and two reversed-order rounds on each board: 12,288 measured retunes / 48 trials. The first eight hops per trial are excluded. There were zero retune failures, BUSY timeouts, RX-mode errors or modulation-cache errors. All 48 trials restored RC policy on exit. The normal packet guards deferred 21 XIAO and 59 Indicator hops; no setup retries or naturally missing/replayed USB reports occurred in these final sweeps.

Production configuration XIAO mean / maximum Indicator mean / maximum
Warm + batching, per-byte SPI 2 MHz 1.193 / 1.227 ms 15.174 / 15.836 ms
Fast RX, per-byte SPI 2 MHz 0.744 / 0.770 ms 8.426 / 8.471 ms
Full RX, buffered SPI 8 MHz 0.828 / 0.840 ms 14.816 / 15.386 ms
Fast RX + buffered SPI 8 MHz 0.549 / 0.556 ms 8.263 / 8.879 ms

The combined production path saves 54.0% on XIAO and 45.5% on Indicator relative to each same-image warm/batched control. Small differences from V5-V7 reflect different binaries/code layout; only within-image A/Bs are compared. Both fast configurations must increment the production success counter; full RX controls must not. Mode/cache verification happens after the timed region.

The common 6 ms switch and 4 ms loop allowances are unchanged, not delays inserted by the driver. Indicator still exceeds 6 ms. These measurements do not qualify continuous application scheduling, scan-overlap reception, sensitivity/PER, power use, temperature extremes, or image recalibration on frequency changes larger than this 1 MHz pair. Do not shorten common transmit preambles or advertise the XIAO number for other boards.

Final over-air packet matrix

288/288 payloads and command transactions passed: 144 Indicator-to-XIAO and 144 XIAO-to-Indicator. Each direction covers all four production cases, SF7/8/9 secondary profiles, both target profiles, 16/64/255-byte synthetic payloads and two reversed-order rounds. TX used buffered 8 MHz SPI at -9 dBm. Fast receiver readiness reports had to show actual production fast resumes.

There were zero RX errors, receiver timeouts, missing replies, stale replies, or natural transport-recovery queries in these matrices. Deliberately lost and stale replies were separately exercised by the recovery tests above. RSSI ranged -43 to -39 dBm and SNR 10.2 to 14.5 dB. This clean short-range matrix addresses the failures seen in the earlier V5 harness; it does not identify which single change caused each historical miss or establish a field packet-error rate.

All 18 host/native regression tests passed. The HIL builds do not include the unrelated pre-existing Companion/repeater/Wi-Fi soak edits; the normal application compile checks include the current working tree and preserve them.

A subsequent requested SF6/125 kHz multi-channel test used 32-symbol preambles and 4.8-symbol visits. It stopped on the third packet at the initial four channels (two received, one timeout); no higher count was tried. That continuous scan experiment is separate from the 288/288 stationary-after-retune matrix above.

Unchanged modulation tuple follow-up

The initial software-only checks below were followed by same-image hardware timing and separated-radio tests. Frequency-only means improved from 548.880 to 451.897 us on XIAO and from 8,263.438 to 7,601.193 us on Indicator. Four-channel SF6 reception was not loss-free. The following paragraph records the initial implementation stage, not the current hardware-test status.

Implemented 2026-09-14 after commit 0e5955f8, without restarting hardware tests. On opted-in fast RX-to-RX hops, setLoRaModulationParams() now compares the requested SF, encoded bandwidth, encoded CR and effective LDRO against the last successfully acknowledged modulation write. An exact match within a valid owned standby window omits only SetModulationParams (0x8B). Frequency programming, the modem query, preamble/packet parameters, IRQ clears, RF-switch control and BUSY waits remain unchanged.

The acknowledgement is separate from RadioLib's mutable software fields. Auto/manual LDRO compares the resulting command byte, with the pinned RadioLib >=16 ms symbol rule. A failed write revokes the whole fast context before rollback. Ordinary SF/BW/CR/forced-LDRO setters invalidate the saved tuple even on failure. Existing full-RX staging, ordinary standby, TX/CAD staging, reset/init, sleep, duty cycling and failed RX/standby operations also invalidate it. The first hop after lost context therefore writes again; frequency-only hops that retain the context can skip subsequent writes.

All 30 selected host/native tests passed, including repeat-write counts for all supported bandwidth/SF/CR combinations and auto/forced LDRO modes, each independently changed field, LDRO policy transitions, failed-write retry, modem-query failure, ownership gates, ordinary setters and lifecycle invalidation. Both profile_switch_xiao and profile_switch_indicator compiled successfully against the pinned RadioLib. No firmware was uploaded, no radio command was issued, and no new RF result was collected. The temporary build overlay was removed. The earlier ~0.549 ms XIAO and ~8.263 ms Indicator timings do not measure this added optimization; its time saving and RF behavior await testing.