+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Full Companions on ESP32 and nRF52 without PSRAM now keep 16 outgoing paths
+and 16 shared secrets in RAM. Every contact remains in the contact table;
+selecting a different contact loads its saved path as needed. This recovers
+about 25.4 KiB of internal RAM in the 350-contact ESP32 qualification builds.
+See the qualification results for the
+six-board RAM comparison, physical tests, and shared-secret timings.
+PSRAM boards retain their existing complete contact table in external RAM.
+Legacy USB/BLE/WiFi Companion profiles also retain their inline paths and
+secrets. The build flag MESH_CONTACT_CACHE=0 or 1 overrides this policy for
+qualification; it is not a runtime feature switch.
+Paths and persistence
+The 16 resident paths use least-recently-used replacement. Contact records
+keep small handles to their saved paths, including 1-, 2-, and 3-byte hop
+hashes. The existing 152-byte contact record and Companion app frame formats
+are unchanged. ESP32 uses /contacts3; nRF52 also supports its paged contact
+store and migration from /contacts3. An erase is not required.
+A hot path needs no flash read. A cold path is read from its existing contact
+record. ESP32 reuses one open contact-file reader to avoid repeated SPIFFS
+metadata scans during synchronization and saving. It closes that reader
+before replacing the file or recovering a saved transaction.
+Changing a copied contact preserves the old path for a pending app response
+or rollback. Dirty entries are saved before eviction. If storage cannot be
+read, written, or represented completely, the operation fails instead of
+silently losing a contact or sending along a different route. Reboot retries
+an incomplete load from the preserved files. The path checksum also rejects
+a cold path changed underneath a live handle; it does not add a new checksum
+to the legacy ESP32 on-disk format.
+Shared secrets and power
+Both platforms reuse the 16 most recent secrets from RAM. Cache misses use a
+platform-specific policy:
+
+
+
+Platform
+Miss behavior
+
+
+
+
+ESP32
+Recalculate the secret without reading or writing a secret file.
+
+
+nRF52
+Try a saved LittleFS entry, then recalculate if it is unavailable.
+
+
+
+On the V4, a measured SPIFFS secret lookup took 132,427 microseconds while
+key exchange took 21,932 microseconds. ESP32 therefore uses recalculation on
+a RAM-cache miss. It avoids secret-file writes and reduces time awake for
+this measured workload. These are elapsed times, not measurements of energy
+in joules; other ESP32 hardware has not been physically benchmarked.
+On a RAK3401, a LittleFS lookup took 7,812 microseconds versus 31,250
+microseconds for key exchange. The nRF52 backend therefore uses saved flash
+entries to avoid repeat key exchange when a usable entry exists, including
+after reboot. Saved entries match the full
+peer public key and a fingerprint of the local key pair, and have a record
+checksum. Importing a different private or public key invalidates old results.
+A corrupt or missing saved entry is recalculated. The native tests use the
+firmware's actual Ed25519 key-exchange library and verify agreement with the
+peer's independently calculated secret.
+Derived secrets are expendable. Their storage keeps a reserve for contacts,
+preferences and filesystem metadata; a full filesystem leaves the calculated
+secret usable in RAM. nRF52 packs 56 entries into a 3,844-byte page to use its
+4 KiB flash blocks efficiently. Small filesystems may not have space to persist
+secrets for all 350 contacts. A miss without a usable saved entry still
+performs key exchange.
+MESH_CONTACT_SECRET_FLASH_CACHE=0 or 1 overrides the miss policy for
+qualification builds. The optional ESP32 flash backend groups eight entries
+per file. The normal ESP32 policy does not create or use those files.
+Inspect the cache
+From the Full Companion's ASCII USB terminal or authenticated local CLI:
+get contact.cache
+
+Cached builds report paths=16 secrets=16, miss=calculate or miss=flash,
+plus these boot-session counters:
+
+
+
+Field
+Meaning
+
+
+
+
+ram_hits
+Shared secret reused from RAM.
+
+
+flash_hits
+Shared secret loaded from flash without recalculation.
+
+
+calculations
+Shared-secret key exchanges performed.
+
+
+save_skips
+Calculated secrets that could not be persisted, including storage reserve/backoff.
+
+
+
+Uncached builds report paths=inline secrets=inline. The normal memory
+command on supported ESP32 Full Companions shows live heap and queue capacity.
+get contact.cache.timing reports the last successful flash lookup and last
+key calculation in microseconds. Zero means no such operation has completed
+since boot. These timings exclude secret writes and are useful for comparing
+the work avoided on a valid flash hit.
+Offline messages during mOTA
+Contact caches are independent of the offline message queue. Builds using
+the shared mOTA queue retain 256 frames normally and 128 while mOTA owns
+its workspace , then restore 256. No unread frames are discarded to start
+mOTA: synchronize the queue and retry if it exceeds the retained capacity.
+PSRAM-backed queues keep their existing capacity and allocation policy.
+Each queue frame occupies 177 bytes. Retaining 32 instead of 128 would make
+another 16,992 bytes (16.6 KiB) available inside the shared storage. It would
+not increase free heap by itself: the normal 256-frame storage is statically
+reserved, and the current mOTA context already fits in the loaned half. More
+mOTA scratch data would have to share that space to obtain an additional RAM
+saving. These changes keep the existing 128-frame mOTA limit.
+Build the six capacity trials
+The optional environments in platformio.nimble.ini combine NimBLE, these
+contact caches, and the shared mOTA queue at 350 contacts, 40 channels and
+256 normal offline frames . They remain outside the ordinary release matrix.
+The ordinary six constrained release recipes still use their established
+150-contact limits pending wider hardware qualification.
+Enable platformio.nimble.ini as described in the
+NimBLE trial guide , then build one environment at
+a time. For example:
+OUTPUT_DIR=.releases/contact-cache-v3 bash build.sh build-firmware \
+ Heltec_v3_ram_trial_companion_radio_full_nimble \
+ --firmware-version v1.17.1.5-halo-keymind-cascade-cache-trial \
+ --radio-preset usa-cascadia --profile cascade --standard --require-ota
+
+
+
+
+Hardware
+Trial environment
+
+
+
+
+Heltec V3
+Heltec_v3_ram_trial_companion_radio_full_nimble
+
+
+Wireless Tracker
+Heltec_Wireless_Tracker_ram_trial_companion_radio_full_nimble
+
+
+Tracker V2, FEM on
+heltec_tracker_v2_ram_trial_companion_radio_full_femon_nimble
+
+
+Heltec CT62
+Heltec_ct62_ram_trial_companion_radio_full_nimble
+
+
+XIAO ESP32-C3
+Xiao_C3_ram_trial_companion_radio_full_nimble
+
+
+Generic ESP-NOW
+Generic_ESPNOW_ram_trial_companion_radio_full_nimble
+
+
+
+The firmware RAM guard remains enabled. A successful linked budget is distinct
+from live free heap and physical Bluetooth/LoRa qualification on each board.
+The smaller NimBLE applications let the CT62, XIAO C3 and Generic ESP-NOW
+trials use two 1.5 MiB WiFi OTA slots on 4 MiB flash. Their 896 KiB SPIFFS and
+coredump stay at the same addresses as the ordinary huge_app.csv Full
+layout. Install the trial's merged image over USB first to install its
+partition table. Subsequent WiFi updates must fit the new slots. An
+application-only update from the ordinary single-slot Full layout cannot
+change that layout or enable WiFi OTA.
+Regression checks
+python3 -B test/test_contact_cache.py
+python3 -B test/test_companion_contact_persistence_contract.py
+python3 -B test/test_companion_contact_stream_contract.py
+python3 -B test/test_nrf52_extrafs_contract.py
+python3 -B test/test_shared_mota_queue.py
+python3 -B test/test_t096_full_memory.py
+python3 -B test/test_firmware_ram.py
+
+The cache tests compile the production path/secret cache code and extracted
+production persistence, routing and request functions with filesystem/radio
+adapters. They exercise 350 routes, eviction, snapshot rollback, page
+migration, identity changes, read/write/rename faults, handle exhaustion,
+packet release on a failed path read, and recovery after a simulated reset
+between file renames. They do not simulate a physical flash power cut.
+tools/hil/contact_cache_serial_stress.py checks all 350 paths over USB,
+replaces them with a different generation, and optionally exercises secret
+eviction. Use an otherwise empty test node and exclusive access to its
+Companion data port. --populate writes test contacts; --secrets transmits
+23 directed LoRa datagrams to synthetic peers. For an ESP32 cache trial:
+python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
+ --populate --generation 1 --secrets --expect-calculate --reboot
+python3 tools/hil/contact_cache_serial_stress.py --port /dev/ttyACM0 \
+ --generation 1 --secrets --expect-calculate
+
+Use the actual device path. Omit --expect-calculate for the nRF52 flash
+policy. The second invocation verifies the persisted paths after reboot.
+
+
+
+
+
+
+
+
+
+
+
+
+
+