Contact-cache and NimBLE RAM qualification
Test date: 2026-09-08. Firmware source: b68aa1c6.
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. 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. 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.