View this page on MeshCore Docs.
ESP32 Bluetooth shutdown and radio2 CPU idle, 2026-10-21
The ESP32 changes cover two gaps identified while qualifying nRF52 CPU sleep: reversibly stopping the Bluetooth host/controller, and allowing bounded MCU light-sleep during a settled secondary-radio receive visit. This report separates host/build coverage, SDK wait measurements and physical packet tests.
Bluetooth transitions and quiet-window SDK wait checks pass. Three earlier strict RF trials with power saving on and matched preambles received five of six packets, including the trial after the first IRQ fix. They remain failures; radio2 is not fully qualified.
The first V3 CPU-clock sweep stopped during setup, before any of its
20-packet-per-frequency cases transmitted. Switching Bluetooth off exposed a
NimBLE-Arduino 2.5.1 shutdown crash: PC zero in the host task, with the event
address resolving to ble_hs_timer + 8. This matches upstream
issue #1184 and its
merged fix #1185.
The backport removes premature timer deinitialization while leaving final
cleanup intact. The earlier V4 Bluedroid checks do not qualify V3 NimBLE.
The regression reproduces the original null callback, then passes 1,000
shutdown/cleanup cycles under ASan/UBSan for each timer backend. Library
version, source hash and build-local middleware checks also pass.
Full Companion source
6236eec3
includes that repair and a preferences-reader fix for a truncated timezone
record colliding with the new CPU footer length. Both timezone and CPU suites
pass; damaged CPU overrides still fall back to Auto. V3 and V4.3 OLED FEM-off
builds pass all 24 capability markers and their memory gates, at 2,118,680 and
2,556,504 application bytes respectively.
The V3 received one application-only update after its old application hash was verified. Complete new-image readback passed, with its partition table and OTA metadata unchanged. Starting with Bluetooth saved off, it restored Bluetooth on and completed three off/on cycles without a reboot or core error. Uptime advanced from 32 to 58 seconds across 20 health samples. Every Binary handoff revalidated board identity and source version. The final checked configuration and credential hashes matched the baseline, and the active PIN stayed unchanged. This physical check covers Bluetooth power transitions, USB control and checked preference preservation.
The recovery also exposed an already-ROM ESP32-S3 UART finish gap. The flasher
now explicitly releases the boot strap before its final reset; its regression
passes with both esptool command spellings. That separate fix is published in
meshfirmware f9d03d1.
Behavior and safeguards
Bluetooth off disconnects clients and completes host/controller teardown in the main loop. Bluetooth on rebuilds the service without releasing controller memory permanently or clearing successful bonds. Address, name, PIN and saved stealth policy remain intact. Pending or failed teardown prevents CPU sleep. The Bluedroid characteristics have deterministic TX/RX registration order; heap fragmentation must not change a paired client's cached handles. On SDKs using automatic Service Changed handling, a small asynchronous service change requests client rediscovery after authentication. A local SDK completion is not treated as proof that the client refreshed its cache. Only actual use of the current notification and receive handles permits skipping this request on a later connection from the same peer within that host session.
Companion, Repeater, Room Server and Sensor can idle during an eligible radio2 visit. The decision requires a settled, validated receive profile, working radio GPIO wake, no pending reception and physically stopped wireless drivers. An absolute scanner deadline is checked again at sleep entry. The timer budget is at most 10 ms, reserves a 4 ms early-wake scheduling guard and requires at least 2 ms remaining. Calibration, repair, ownership, queue, network, USB and role deadlines retain their existing guards. The receiver stays on. This does not enable sleep through an active Bluetooth/WiFi link.
Host tests and builds
The scheduling, board wake, dispatcher and real role integration suites pass. Coverage includes expired and wrapping deadlines, stale profile proofs, pending interrupts, failed wake setup and SDK errors. The BLE adapter tests exercise actual production code with ASan/UBSan, ownership differences, partial initialization/teardown failures and repeated off/on cycles. Existing nRF52 event/profile tests also pass; nRF52 hardware was not touched.
All 22 scripts in the affected Sensor/tracker/sleep CI step and all 57 in the
Companion contact/channel step pass locally. The latter initially encountered
a missing Python platformio module in one read-only metadata case; that
script's six cases passed with the existing PlatformIO Python environment.
No firmware build or hardware test was repeated for this environment correction.
The diagnostic fixtures exercise the current production sleep branches rather
than relying on obsolete source anchors.
The initial sleep/teardown implementation, before the cache fix, has Full V4.3 FEM-off Companion, V3 Companion, V4 Repeater, Room and Sensor builds that pass their capability and memory gates. ESP32-C6 BLE (Arduino 3.1.3), the NimBLE-Arduino V4 trial and RC32 Full (Arduino 3.3.11) also compile. RC32 exposed SDK service ownership and array-deletion defects; a version/hash-bound build-local repair fixes those sources without editing a shared SDK install.
The cache fix is published in
1087643ae.
Its V4.3 FEM-off Full and RC32 Full builds pass all 24 capability markers and
their application size gates: 2,551,592 and 2,152,840 bytes respectively, each
within a 6,553,600-byte application slot. The other earlier builds do not
constitute a full build matrix of this later source revision.
d34e65175
removes unnecessary delays between successful cache-recovery phases while
retaining bounded failure backoff. Its V4 Full build passes, at 2,551,608 bytes.
The same-size V4 build of
f8b5b8301
adds only the channel-response padding fix described below. Bluetooth, radio,
sleep-policy and role code are unchanged between those last two revisions.
At this checkpoint, Classic Heltec V2 BLE was below its required 8,192-byte static DRAM reserve. Historical checks left 2,552 bytes free on the baseline and 1,224 bytes on the initial sleep candidate. Removing an unnecessary WiFi-driver reference recovered 1,208 bytes, raising the later candidate to 2,432: 120 below baseline and 5,760 short of the reserve. Final cache/padding revisions were not remeasured on V2. The gate was retained. The later 22 October V2 cache repair closes the RAM build failure with 12,440 static bytes free and unchanged capacities; physical V2 operation was not measured by that repair.
Physical V4 observations
The test board is a Heltec V4.3 OLED using its matching FEM-off Full Companion
image. The first candidate reports source suffix 278c9ffb; its published
equivalent after history cleanup is 61eecbd97. An application-only write and
complete readback preserved the partition table, OTA metadata, identity,
settings and credential digests. No whole-flash backup, bootloader write or
other-device update was performed.
With Bluetooth/WiFi off and power saving on, only this V4's USB data was suspended. After the host-loss grace period, a 90-second diagnostic window recorded 4,635 SDK light-sleep calls and returns, zero SDK errors, zero short waits, zero vetoes and zero pending-wake returns. Cumulative call time was 35.523299 seconds; the longest call was 9,869 microseconds. The mixed profiles were 910.525 MHz / BW62.5 / SF7 / CR5 and 916 MHz / BW125 / SF9 / CR5. USB authorization and runtime policy were restored. Core errors remained zero. These are SDK wait measurements, not electrical current or residency readings.
Two initial saved-bond Bluetooth off/on sessions passed. Further testing exposed both a host-side notification-acquisition problem and a real firmware GATT-handle ordering defect. The host driver now uses ordinary StartNotify. The original failures are retained; clearing a client cache is not accepted as proof that firmware handles remain stable or an existing cache migrates.
On d34e65175, both authenticated NUS connections passed with the old opposite
handle cache retained. The test independently checked board, exact version,
role, public identity, node name, address and advertising policy. Switching off
while connected disconnected the client; the test required completed off
before re-enabling and reconnecting. No cache was cleared and no new pairing
was made. The cache changed automatically to TX value 0x002A, RX value
0x002D; saved pairing-key digests were unchanged. The image also started
successfully with Bluetooth saved off. No electrical power measurement was made.
Two-frequency packet controls
The peer is an independently identified ESP32 XIAO S3 WIO. Every transmitted packet uses a new encrypted nonce, full sender/destination identities and a zero-hop direct route. The receiver's USB data is absent during the sleeping RF window; queued packets are checked only after USB resumes. WiFi and Bluetooth are stopped, crossover is off, and physical RX counters must advance on both frequencies. No failed packet is resent automatically.
Earlier runs used a transmitter preamble of 128 symbols while the receiver reported 32. An awake two-packet control received zero packets with zero SDK sleep calls. Disabling radio2 then received the one primary test packet. Source inspection found that the receiver's 32-symbol preamble guards expire after about 99/198 ms, while a 128-symbol transmitted preamble alone lasts 262/524 ms on these profiles. The guard can clear preamble detection and permit a scan hop before the header arrives. These mismatched trials remain recorded; they do not establish a CPU-sleep regression.
With both sides at the verified 32-symbol preamble and peer TX at 2 dBm:
| Control | Unique packets received | SDK waits | Result |
|---|---|---|---|
| USB active, CPU awake | 2/2; one on each profile | 0 | Pass, including exact cleanup |
| USB suspended after 121-second host-loss grace, power saving on | 5/6; all three primary and two secondary | 777 calls/returns; zero SDK errors, short waits, vetoes or pending returns | Strict RF check failed: final secondary nonce missing |
The sleeping trial's measured profile RX deltas were 100/2, including ambient traffic. Both frequencies carried verified private packets while the MCU idle path ran, but the missing sixth packet prevents an all-pass result. These finite samples do not establish a loss rate or attribute that loss to CPU sleep.
A predeclared paired comparison then used the same d34e65175 image, settings,
32-symbol preambles, TX power and suspended USB in both modes. Each interval
sent six new nonces once. A first preparation attempt was refused before any
RF transmission; its clean restoration and zero-transmission receipt remain
preserved. Bounded admission of the power setting was moved before USB
suspension; no packet retry was introduced. Power saving also caps the CPU at 80 MHz;
switching it off restores the nominal 160 MHz clock on this V4 Full build.
This comparison measures the whole power-saving policy, so it does not isolate light sleep from foreground
CPU speed.
| Same-image control | Unique packets received | SDK calls/returns | Result |
|---|---|---|---|
| Power saving off | 6/6; three on each frequency | 0/0; all diagnostic error/veto counters zero | Pass, cleanup and PS-on restoration verified |
| Power saving on | 5/6; primary sequence 2 missing, all three secondary packets received | 820/820; zero errors, short waits or pending returns; five vetoes | Strict RF check failed; cleanup and PS-on restoration verified |
The second sleeping trial had profile RX deltas 103/3, including ambient traffic, and seven receive errors. The different missing packet rules out a deterministic final message-slot loss. This small comparison warrants a wake-path investigation; it does not isolate interference, scanner timing or an individual GPIO event as the cause. No unchanged RF trial was repeated after it.
GPIO wake and packet ownership
ESP-IDF 4.4.7 documents that GPIO interrupts are not generated during light sleep. GPIO wake is a separate mechanism. The board temporarily uses a level wake source and then restores a rising-edge interrupt; the SDK's GPIO driver and S3 wake implementation do not replay a software callback when that interrupt type is restored.
The wrapper previously checked only its software event. If a completed packet left DIO high without a callback, the loop could fail to claim its FIFO before a later rearm or retune. This is a concrete ownership gap, but the aggregate packet results do not prove which missing nonce encountered it.
1ba4e2830
adds an ESP32-only physical IRQ fallback to both pending and claim checks. It
uses that registered radio module's own pin and HAL, validates ownership,
null pointers, NC, pin range and the SoC validity mask, and adds no RAM fields.
Claiming consumes any software event too; the unchanged hardware RX_DONE
check still gates FIFO reads. It changes no frequency, preamble, wake mask or
sleep budget. Other platforms retain their software-event behavior.
All 12 interrupt-recovery host tests pass with ASan/UBSan. They execute the production receive and reconfiguration paths with an asserted DIO and no ISR, combined and delayed callbacks, independent radio pins, invalid pins and non-ESP32 variants. Removing either fallback fails an executed regression assertion. The V4 Full build passes all 24 capability markers and its memory/size gates, at 2,551,736 bytes within the same 6,553,600-byte application slot. The RC32 Full build on Arduino 3.3.11 also passes all 24 markers and its memory/size gates at 2,152,968 bytes. These are build checks, not physical RC32 qualification.
The application-only installation and full readback of this image passed, including unchanged partition table, OTA metadata, identity, saved settings and credential digests. One new six-packet qualification trial then exercised the changed IRQ code with the same radio settings, USB suspension and power-saving policy. It received 5/6, missing secondary sequence 6: another strict RF failure. All six sends completed once; none was resent. Profile RX deltas were 79/2, including ambient traffic, with five direct packets and two receive errors. The interval recorded 741 SDK calls/returns, zero SDK errors, short waits or pending returns, and two safe vetoes. Cumulative call time was 5.710857 seconds; the longest was 9,900 microseconds. Cleanup and PS-on restoration passed.
Independent source audits found no additional concrete clock/deadline defect:
ESP32 micros() and the scanner use the ESP Timer domain; the deadline is
anchored before the late query and checked again after wake setup. Pending
packets and latched preamble/header activity guard the next retune. The current
4.6-symbol scanning model is not a zero-loss guarantee. The observed losses
remain unresolved; neither interference nor the IRQ gap is established as their
cause. No further unchanged RF trial or arbitrary timing/preamble change was
used to obtain a pass. Further qualification needs a control that holds CPU
speed fixed and correlates scan/preamble/IRQ timing with individual packets.
Channel cleanup and response padding
The single-profile and sleeping reports also recorded cleanup failures. The single-profile power restore was refused while the radio was busy; a separate bounded cleanup restored 22 dBm and removed only the hash-matching test channel. The later sleeping run restored power and cleared its channel but failed a raw channel-response digest comparison.
A separate read-only audit compared every meaningful channel name and every
key byte twice on both devices. All matched, and both temporary slots were
empty with zero keys. Raw differences occurred only after the terminating NUL
in the fixed-width channel-name field: one channel differed between repeated
reads on each device and all 40 differed between devices. The getter used
strcpy without clearing the unused name bytes, exposing stale reply-buffer
padding. The original failed receipts were retained; no channel data was
rewritten to force a digest match.
The shared Companion fix zero-pads and bounds that field while retaining key offset 34 and the existing 50/66-byte frame layouts. All nine affected host tests pass. The actual-handler regression covers empty, short, maximum and unterminated names, dirty output buffers, Meshtext on/off and 16/32-byte keys; the original implementation fails its negative control.
A read-only hardware audit on 1ba4e2830 also passed: both devices' meaningful
channel names and every key byte matched across two reads, both temporary
slots were empty with zero keys, and every V4 name field had zero padding.
The peer's firmware was not changed.
V3 fixed-clock comparison with 20 packets per frequency
After the NimBLE repair, the V3 completed all four predeclared cases on
6236eec3. Every case sent 20 unique packets per frequency once, with the same
32-symbol preambles, 2 dBm peer TX power and default 10/4/2 ms sleep dials.
Power saving stayed on; CPU speed and the temporary manual sleep switch were
controlled independently. RX power saving was saved off for these comparisons;
the profile scanner itself uses continuous RX during each visit.
| CPU | Manual CPU sleep | Primary received | Secondary received | SDK calls/returns over the whole diagnostic window |
|---|---|---|---|---|
| 80 MHz | Off | 18/20 | 17/20 | 0/0 |
| 80 MHz | On | 20/20 | 19/20 | 270/270 |
| 160 MHz | On | 18/20 | 14/20 | 18/18 |
| 160 MHz | Off | 19/20 | 16/20 | 0/0 |
All 160 transmissions reached TX_DONE, with no packet retries or duplicates. All four strict delivery checks failed. Settings, channels and credential hashes were restored; both radios finished with zero core error flags. Losses with manual sleep off show that sleep is not required for the observed failure. Ambient traffic differed between cases, and these samples do not establish a CPU-speed relationship or a packet-loss cause.
The On rows do not yet prove sleep during packet traffic. The UART quiet control stopped the driver but left its Companion session logically connected. Incoming messages then queued push notifications to that stopped transport; the existing pending-output guard correctly prevented sleep. Whole-window SDK calls could have occurred before traffic began. The delivery counts remain valid, but sleep comparison qualification requires retiring that UART session and timing evidence inside the RF sequence. No pending-work guard is bypassed.
UART session repair and timed V3 comparison
The campaign continued on 2026-10-22 with
7d0709bf0.
UART quiet now retires only that USB protocol session after draining the ACK,
including its pending reply owners and USB Reader output. The offline inbox
and other transports remain active. A successful UART restart restores a
fresh silent session; failed restarts keep the interface parked and retry.
The integration regression reproduces the original stuck notification, then
retains and downloads 40 actual receive frames without weakening sleep guards.
ESP32 diagnostics now retain sixteen 16-second SDK-entry buckets, with explicit outside-window, saturation and missing-clock indications. The collector stays within its 256-byte budget; nRF52 layout and behavior are unchanged. The physical comparison requires a positive bucket wholly after the first confirmed TX_DONE and before the last TX request, including host/device clock uncertainty. This establishes SDK entries during the packet sequence, not sleep through a particular packet or electrical residency.
Both Full Companion builds passed 24 capability markers and their memory gates. V3 and V4 application sizes are 2,120,088 and 2,557,400 bytes. Application-only updates passed full readback, unchanged partition/OTA metadata and checked configuration/credential preservation on both boards. No Bluetooth lifecycle or earlier quiet-only test was repeated solely for documentation changes.
The new V3 comparison again sent 20 unique packets per frequency per case, with no RF retries, the same 32-symbol preambles and default sleep dials:
| CPU | Manual CPU sleep | Primary received | Secondary received | SDK calls/returns | SDK calls in wholly contained RF-time buckets |
|---|---|---|---|---|---|
| 80 MHz | Off | 17/20 | 15/20 | 0/0 | 0 |
| 80 MHz | On | 18/20 | 19/20 | 10,925/10,925 | 4,625 |
| 160 MHz | On | 18/20 | 16/20 | 10,164/10,164 | 4,585 |
| 160 MHz | Off | 20/20 | 16/20 | 0/0 | 0 |
All 160 sends reached TX_DONE once; there were no duplicate deliveries. All four strict delivery checks failed. On cases have positive SDK activity during the RF sequence, with zero SDK errors, short returns or pending waits. In the 80 MHz On comparison, serial blocking fell from 109,265 ms on the old image to 1,368 ms after session parking. This supports the console-fix mechanism; varied ambient traffic still prevents a causal delivery comparison. Neither CPU speed nor manual sleep consistently removed packet loss. Original settings, channels, identity and checked credential hashes were restored, with clean core flags.
V4's first new comparison stopped during setup, before Bluetooth-off was sent and before any case or RF transmission. Its strict stable-Binary check rejected one unexpected prefix byte after a valid health response. Seven valid push frames were accepted; the rejected byte was not retained, so its origin was unknown at the time. That original failure remains recorded.
Two diagnostics-only attempts on unchanged 7d0709bf0 also stopped before
Bluetooth-off was sent, with zero RF transmissions. Bounded raw capture retained
the late SDK line E (2905571) wifi:timeout when WiFi un-init, type=4.
Source inspection identified an SDK timer firing after shutdown; this message
does not establish that WiFi deinitialization timed out. The raw SDK log
bypassed disabled USB logging and broke strict Binary Companion framing.
All three attempts restored checked settings/channels and verified a fresh
MQTT PUBACK increase. The latest immutable diagnostic receipt is
v4-7d0709bf-suffix-completed.private.json. These setup failures supply no
new V4 packet-delivery evidence.
SDK logging repair and V4 scanning controls
The SDK logging fix in d73599bc6 shipped in
3fd9b392.
It prevents raw SDK logs from bypassing the disabled USB-logging policy and
corrupting the shared Binary Companion stream. All 101 USB host tests pass.
The V4 application is 2,558,856 bytes, with SHA-256
ddfd4f40a97da40bdeda2c00fb18ebc2d4d365590d4bc63496a425d16975906d.
Its installation passed full readback and checked configuration preservation.
Two new scanning cases completed on this V4 image, with 20 new nonces per profile, matched 32-symbol preambles and 2 dBm peer TX:
| CPU | Manual CPU sleep | Primary received | Secondary received | SDK calls/returns | SDK calls in wholly contained RF-time buckets |
|---|---|---|---|---|---|
| 80 MHz | Off | 19/20 | 18/20 | 0/0 | 0 |
| 80 MHz | On | 19/20 | 15/20 | 4,121/4,121 | 3,511 |
Both strict delivery checks failed. All 80 sends reached TX_DONE once, with zero retries or duplicate deliveries. SDK errors, short returns and pending waits were zero. The awake losses establish that CPU sleep is not required for this symptom; these samples do not identify the cause of individual misses.
The user redirected testing toward the scanner. The 160 MHz On case was
intentionally stopped during its USB host-loss grace period, before any RF
transmission; 160 MHz Off was not entered. This is not a completed four-case
comparison. The interrupt receipt remains recorded in
v4-3fd9b392-user-steered-completed.private.json (SHA-256
e102200af7e2503bb0362e65afad0d947d5c3aeb220425a589d0588032530c52).
Full restoration and a fresh MQTT PUBACK increase passed after that stop.
Fixed-tuple controls and receive-path defect
Two fixed-tuple controls on the same 3fd9b392 image each received 20/20
unique packets with radio2 explicitly off. The primary physical radio was
temporarily tuned first to 910.525 MHz / BW62.5 / SF7 / CR5, then separately to
916 MHz / BW125 / SF9 / CR5. Both runs used 80 MHz, manual CPU sleep off, saved
power-saving intent on, RX power saving off, 32-symbol preambles and 2 dBm peer
TX. The target's USB data was absent after the same 121-second host-loss grace.
Each run retained the original seeded 40-slot cadence, leaving the opposite
profile's 20 slots silent. SDK calls, duplicate deliveries and RF retries were
zero. The immutable control report remains failed because the peer's original
22 dBm setting was not restored within three bounded busy-response attempts;
MQTT recovery passed. A separate recovery then restored 22 dBm and verified
full peer settings, credential hashes and channel semantics without another
RF transmission. The original failure is retained in
fixed-tuples-v4-3fd9b392-completed.private.json; successful recovery is in
peer-tx22-restoration-verified.private.json.
These finite controls narrow the investigation toward scanning or overlapping traffic. Radio2 time-shares one transceiver: a valid primary reception can occupy it throughout a secondary packet's preamble. The idle-scan timing model does not guarantee zero loss under simultaneous traffic. The controls do not distinguish that limitation from a receive-path defect for a particular nonce.
A separate host regression executing the real receive functions with
ASan/UBSan proved a stale-IRQ defect. Servicing a packet through its asserted
physical DIO could leave a late software event. Subsequent empty RX_DONE probes
could then restart continuous RX over a newer packet's preamble or valid
header. The narrow ESP32 SX126x continuous-RX guard in
a5293fa50
preserves that new reception while retaining normal activity timeouts.
All 15 interrupt-recovery, five receive-contract and six RX-retry tests pass.
The V4 build passes all 24 capability markers and its memory gate at 2,558,936
application bytes. Its application SHA-256 is
cc16e44ccd24e64724a2bd17f59f684117e411077e4c0a7d04611be328b7ccfc.
The application-only installation passed complete readback, with the partition
table, OTA metadata and checked configuration preserved. The immutable receipt
is flash-v4-a5293fa5-completed.private.json (SHA-256
ef61cf8dfc77f8907d621f7f0493949ff6888b017c277eefb309247ccf88b278).
One new awake 80 MHz scanning run then sent 20 unique packets per profile on
this changed image, with manual CPU sleep off and the same matched preambles,
TX power and USB host-loss grace. The V4 received 19/20 primary and 20/20
secondary packets. All 40 sends reached TX_DONE once, with zero retries or
duplicate deliveries. SDK calls, returns and all time buckets were zero; core
error flags remained clear. The missing primary nonce keeps the strict RF
result failed. Full restoration passed, including a fresh MQTT PUBACK increase.
The immutable receipt is v4-a5293fa5-awake-completed.private.json (SHA-256
466bdf0bb08f10665007d156e1ec37c074e1c576e6ac5fc401bcd449179ee774).
The stale-IRQ regression proves a receive-path bug and the new guard repairs that case. This physical sample does not establish which historical misses encountered it or show that all scanning losses are fixed. A controlled two-transmitter overlap diagnostic is recorded below; it does not establish a cause for these historical misses.
Receive activity across RX restarts
A further host regression executing the production receive methods exposed a
separate scan hazard. Reading a completed packet and successfully rearming RX
cleared the hardware IRQs but retained the previous packet's _activityAt and
_headerSeen latches. At the next due scan, the detector could mistake the new
packet's preamble without a header for the old packet's consumed header. It
reported idle before processing that preamble, permitting a retune over the
new packet. The regression reproduces that sequence and shows that resetting
the latches on successful RX entry protects the next preamble.
The change resets those existing fields only after successful receive
starts in CustomSX1262, CustomSX1268, CustomLLCC68, CustomSTM32WLx,
CustomLR1110 and CustomLR1121. It covers normal starts and, where supported,
fast resumes and duty-cycle entry. Failed starts retain their prior state;
the change adds no fields and leaves existing activity timeouts and
receive/retune guards unchanged.
Independent source review passed. Five new regression methods cover 24
ASan/UBSan variants, including reset-removal controls that reproduce the
retune over a new packet for all six handlers. All 19 affected host-fixture
tests and 26 interrupt-recovery, receive-contract and receive-retry checks
pass. The new suite runs in the required unit-test workflow. Physical RF
results on the new image are recorded below. The host proof does not attribute
historical missing nonces or establish a complete RF pass.
RX-restart installation and overlap diagnostic
The V4 image built from pushed source
e4119a120
passes its capability and memory gates at 2,558,984 application bytes. Its
application SHA-256 is
fc1a835c45ebc5e7a9fec95c757d8606e145bbc92fa8d365ba62d96a7930286d.
The application-only installation passed full readback and checked
configuration preservation. The receipt is
flash-v4-e4119a12-completed.private.json (SHA-256
b06d8b6ef8d9df9aa881d6d1ca2f6096d19c8bcc6d4cbb2873fb20d47a9f85c5).
Two overlap conditions completed on this image at 80 MHz with manual CPU sleep off. Each condition sent 20 unique primary/secondary pairs; all 40 sends reached TX_DONE once, with zero retries or duplicate deliveries and zero SDK counters in both conditions.
| Condition | Primary received | Secondary received |
|---|---|---|
| Radio2 scanning | 16/20 | 2/20 |
| Fixed 916 MHz, radio2 off | 0/20 (outside receive frequency) | 20/20 |
All 20 scanner timing bounds were inconclusive. The nominal 140 ms secondary write occurred 227.239-263.257 ms after the primary write (median 244.363 ms), while the first counter reply confirming primary TX_DONE arrived 523.533-903.982 ms after that write. That reply bounds completion; it does not timestamp the RF event. The controller synchronously saved the report before the secondary write and after each counter reply, widening these bounds. The analysis uses 101-byte wire airtimes of 397.824/652.288 ms, a complete 148.480 ms secondary preamble and a 2 ms margin. No pair proves that the whole secondary preamble fell inside the primary payload reception. The timings do not exclude desense or attribute historical misses. The ordinary alternating-profile check is recorded below.
Three earlier setup attempts sent no RF: the first two stopped before
mutation; the third encountered a pending lease, with separate V3 automatic
cleanup recovery passing. The overlap experiment completed with an
inconclusive status: neither condition has a qualified timing pair, so masking
was not demonstrated. Full restoration of all three boards passed without
errors, and a fresh MQTT PUBACK verified recovery. The final host receipt is
/home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/overlap-v4-e4119a12-completed.private.json
(SHA-256
c1aee5132b447734df3fe9c663f4f3ae7a8b980f3165de43f96c2653d05200e1).
No firmware qualification pass is claimed.
Ordinary awake check after both receive fixes
The predeclared e4119a120 check sent 20 new packets per profile, once each,
with the ordinary alternating schedule and no deliberate overlapping sender.
It received 17/20 primary and 18/20 secondary packets. All 40 sends reached
TX_DONE once; duplicates, RF retries and SDK sleep calls were zero. The strict
RF result remains failed. The physical counters recorded 120 successful raw
receives: 35 owned direct packets and 85 ambient floods, with seven separate
receive errors. These totals do not identify the cause of each missing nonce.
The immutable receipt is v4-e4119a12-awake-completed.private.json (SHA-256
9f2f8acecc634ebaf7b9e9aa21e51079b9872316e571a886d3866bfedabab558).
That receipt also retains a cleanup failure: restoring automatic radio2
permission was rejected while the temporary primary restore was still pending.
Fresh MQTT recovery passed. A separate guarded operation subsequently restored
automatic permission with one setter and no RF transmissions, then verified
the checked settings, credentials and channel semantics against the baseline.
Its receipt is v4-auto-restoration-after-e411-awake.private.json (SHA-256
4dbf10eeac81e8897c8a4f9fa4d7b62ac20e74c91b467a501f31fa3a1d8bd0ec).
Both receive defects remain proven by executed host regressions, but the physical results show that additional losses remain. The following diagnostic records bounded activity and transmission times on the radios themselves. It distinguishes other-profile ownership, unresolved preamble activity, read/rearm gaps and delays between scanner calls without treating host counter replies as RF timestamps. No arbitrary preamble or CPU-sleep setting change or replacement packet was used to turn a failed result into a pass.
On-radio capture after the receive fixes
Source 9b70827d0 adds an opt-in, bounded RAM trace for this diagnosis.
It is compiled out of normal firmware. The private V3 sender and V4 receiver
images record physical interrupt bounds and receive activity without additional
SPI reads or asynchronous USB output. Trace capture does not qualify a release
image by itself.
Two controller failures are retained separately. The first rejected a valid pending-frequency response before sending any RF. After correcting that parser, the absolute-slot attempt sent and received one primary packet, then stopped with 39 packets unsent. Its next frequency change took 2.847 seconds, exceeding the 2.553 seconds available after transmit confirmation. Both attempts restored the original checked settings and verified a fresh MQTT PUBACK. Neither is an RF qualification pass, and no original packet was resent.
A separate, predeclared minimum-gap-v1 measurement keeps the seeded packet
order and minimum gaps, allowing slow frequency changes to stretch the cadence.
It uses 40 fresh unique packets, 20 per profile, each sent once. The entire
transmission phase completed in 114.162 seconds with zero retries or duplicate
deliveries. Its changed cadence must not be treated as a passing replacement
for the failed absolute-slot attempt or directly compared as the same loss-rate
experiment.
| V4 awake, radio2 scanning | Sent once | Received |
|---|---|---|
| Primary | 20 | 20 |
| Secondary | 20 | 12 |
Both radios recorded zero SDK sleep calls, returns and errors. Both captures had zero overflow, SPI errors and context errors. All 32 shared packet-end checks agreed with the clock model. The timing model uses causal clock-query brackets, a conditional awake ESP32-S3 rate bound of 10 ppm per radio, and an explicit conditional 2 ms PHY allowance. Its full uncertainty envelope is retained; the predeclared 50 ms quality limit never narrows that envelope.
Three of the eight secondary losses have a positive ownership classification: the entire possible preamble interval fell inside a confirmed valid primary packet reception from ambient traffic, rather than one of the owned test packets. The single transceiver could not listen on the secondary frequency during that reception. Five losses remain unresolved. Three overlap a read/rearm window, which is only a timing association; the other two have no qualifying cause in this capture. These results do not establish desense, noise, a new scanner defect, or the cause of historical missing packets.
The strict RF result remains failed. The successful fixed-frequency controls and the new timing evidence establish frequency sharing as a cause of some losses while CPU sleep is disabled. Five losses still lack a qualifying cause. A longer preamble, different scan period, or dropping a valid primary reception is not silently applied as a general fix.
The completed capture receipt is
radio-trace-minimum-gap-v1-completed.private.json (SHA-256
a2f34dc973692c9567ac7854db59446c590ab1c176375b06076fe6944ea7440a).
It retains a cleanup failure: restoring the V4's automatic radio2 permission
exceeded the controller's combined 15-second budget after a busy rejection.
The primary temporary lease had already become inactive; automatic permission
remained unrestored. Fresh MQTT PUBACK recovery passed. A separate guarded operation
restored that permission with one setter and no RF, checked channel semantics
and the PIN through a fresh handshake, and preserved the original failed
receipt. Its receipt SHA-256 is
432b1104156e2b6c4802efa5af6089d1b2ccc6bcfe70ea7acb1596fcc3cfa89f.
A subsequent read-only baseline verified both radios against all checked
original settings and credential digests, with no transmissions or preference
writes (SHA-256
f32cb54600142b2011fd9bcf51a186e95f41b70e590fabbf5e06516bb6506dc7).
Final handback and limits
V3 and V4 now run normal Full Companion 9b70827d0, with the diagnostic trace
compiled out. The peer remains on ebb59d48. Both application-only returns
passed complete readback, partition-table and OTA-metadata preservation, and
checked configuration/credential preservation. The normal application SHA-256
values are 39e15c4df674993ef1a3a809867a381a01b182af3e920ff208f28bef16178619
(V3) and dd714ad4e9f57f811647af889c8eab7e99a434f81da9541a4398624049b6361f
(V4). Return receipts have SHA-256
28a5e075ee9154c2e67b1d4bc1fbec375d45abc1a41f40ac4797dc4a23bd9528 (V3)
and 5894b59b1d3344cf88d3804a05022271c86f7b4aad29e5307be0f1af475237b9 (V4).
A read-only post-boot observation confirms V4 MQTT running with its saved
broker connected and zero core error flags.
The first V3 return invocation stopped before any ROM operation or firmware
write because the existing diagnostic install used the same source-commit
attempt marker. That failed receipt is retained (SHA-256
972e888b0d5c425676b2411ae6f7ca04cbd1b5e126fd1557667c474d0617c741).
The normal return uses a separate node/source/application-hash marker while
retaining exclusive creation and all original update checks; the diagnostic
marker is unchanged. Later source commits only repair a host fixture and
update this report. Unchanged RF tests were not repeated for the normal return.
The completed earlier V3 campaign and V4 scanning controls,
including the a5293fa50 awake run, restored their checked radio, RX power-saving,
Bluetooth, WiFi, MQTT, logging, diagnostics and USB-watchdog settings, including
credential digests, original radio2 automatic permission and peer power/channel
state. Fresh MQTT PUBACK increases verified recovery. The fixed-tuple cleanup
power failure is retained, with the later peer recovery verified separately.
Full overlap restoration is verified. The ordinary run's original cleanup
failure is retained, with its separate automatic-permission recovery verified.
No nRF52 board, hub power or other firmware was changed in this task.
The Bluetooth hardware checks pass. SDK light-sleep calls are observed without SDK errors. Strict sleeping two-frequency packet qualification remains open. The later V2 cache repair passes its existing RAM reserve gate. These results do not qualify every ESP32 board or the full thirteen-radio fleet, and do not measure electrical power savings. Unchanged Bluetooth and quiet-wait checks were not repeated for padding, version, documentation or fixture-only changes.
Private artifacts, receipts and traces are retained under
/home/mesh/hwtest/esp32-ble-radio2-sleep-cd4ea053-20261021/ and
/home/mesh/hwtest/esp32-radio2-pson-v3-v4-20261021/.
See power-saving diagnostics for the controls
and the limits of interpreting wait counters as energy savings.