Enable contact caching for high-capacity nRF52 companions

This commit is contained in:
mikecarper committed 2026-10-17 10:14:17 -07:00
1 parent 8a5f735bba
commit 50428f0fb7
4 files changed
+69 -8

No files matched your search

+12 -4
View File
@@ -4,8 +4,8 @@
# Companion contact caches
Full Companions on ESP32 and nRF52 without PSRAM, plus the high-contact RAK4631
USB/BLE profiles, keep 16 outgoing paths
Full Companions on ESP32 and nRF52 without PSRAM, plus nRF52 USB/BLE
profiles with more than 300 contacts, 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.
@@ -14,12 +14,20 @@ See the [qualification results](../test-results/companion_contact_cache_results.
six-board RAM comparison, physical tests, and shared-secret timings.
PSRAM boards retain their existing complete contact table in external RAM.
Most legacy USB/BLE/WiFi Companion profiles retain inline paths and secrets.
The high-contact RAK4631 USB/BLE profiles also enable the same cache,
Lower-contact legacy profiles and ordinary ESP32 USB/BLE/WiFi Companion
profiles retain inline paths and secrets. High-contact nRF52 USB/BLE profiles
(including Wio Tracker L1) enable the same cache,
keeping all 350 contacts while making room for the shared 256-frame offline/DM
pool. The build flag `MESH_CONTACT_CACHE=0` or `1` overrides this policy for
qualification; it is not a runtime feature switch.
The ordinary Wio Tracker L1 BLE build reproduced a runtime RAM failure with
48,964 bytes available against 59,968 required. Enabling the existing cache
for high-contact nRF52 profiles recovered 30,200 bytes: the linked build has
79,164 bytes available and a 19,196-byte margin against the unchanged budget.
Its 350 contacts, 40 channels and configured 256-frame offline/held-DM pool
are retained. This is a linked-capacity result, not a physical load/soak test.
## Paths and persistence
The 16 resident paths use least-recently-used replacement. Contact records
+6 -2
View File
@@ -1,10 +1,14 @@
#pragma once
// PSRAM boards already keep the complete contact table outside internal RAM.
// High-contact nRF52 USB/BLE recipes need the same cache as Full Companion:
// inline paths/secrets can otherwise leave too little heap for startup.
// An explicit 0/1 override is useful for matched qualification builds.
#ifndef MESH_CONTACT_CACHE
#if defined(COMPANION_RADIO_FULL) && !defined(BOARD_HAS_PSRAM) && \
(defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))
#if !defined(BOARD_HAS_PSRAM) && \
((defined(COMPANION_RADIO_FULL) && \
(defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))) || \
(defined(NRF52_PLATFORM) && defined(MAX_CONTACTS) && MAX_CONTACTS > 300))
#define MESH_CONTACT_CACHE 1
#else
#define MESH_CONTACT_CACHE 0
+32 -2
View File
@@ -22,6 +22,30 @@ def crypto_objects(temp):
class ContactCacheTest(unittest.TestCase):
def test_default_policy_covers_high_contact_nrf52_without_full(self):
cases = (
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350}, 1),
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 301}, 1),
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 300}, 0),
({"NRF52_PLATFORM": 1}, 0),
({"ESP32_PLATFORM": 1, "MAX_CONTACTS": 350}, 0),
({"NRF52_PLATFORM": 1, "COMPANION_RADIO_FULL": 1}, 1),
({"ESP32_PLATFORM": 1, "COMPANION_RADIO_FULL": 1}, 1),
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350, "BOARD_HAS_PSRAM": 1}, 0),
({"ESP32_PLATFORM": 1, "COMPANION_RADIO_FULL": 1, "BOARD_HAS_PSRAM": 1}, 0),
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350, "MESH_CONTACT_CACHE": 0}, 0),
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 100, "MESH_CONTACT_CACHE": 1}, 1),
)
for defines, expected in cases:
with self.subTest(defines=defines):
result = subprocess.run([
"c++", "-E", "-P", "-x", "c++", "-",
"-I", str(ROOT / "src"),
*("-D%s=%s" % item for item in defines.items()),
], input='#include <helpers/ContactCachePolicy.h>\nMESH_CONTACT_CACHE\n',
text=True, capture_output=True, check=True)
self.assertEqual(result.stdout.strip(), str(expected))
def test_esp32_misses_recalculate_without_flash_io(self):
with tempfile.TemporaryDirectory(prefix="mesh-secret-power-") as temp:
temp = Path(temp)
@@ -44,6 +68,11 @@ class ContactCacheTest(unittest.TestCase):
def test_nrf52_paged_store_migration_and_snapshots(self):
self.run_cache_test("NRF52_PLATFORM")
def test_standard_nrf52_policy_preserves_migration_and_snapshots(self):
# No Full or explicit cache flag: exercise the ordinary 350-contact
# recipe's default through the same real cache/persistence harness.
self.run_cache_test("NRF52_PLATFORM", standard_nrf52=True)
def test_esp32_default_crc_cadence_is_detected(self):
self.run_cache_test("ESP32_PLATFORM", disable_crc_batch=True)
@@ -66,7 +95,8 @@ class ContactCacheTest(unittest.TestCase):
disable_raw_recovery=False,
unsafe_legacy_discovery=False, unsafe_page_discovery=False,
fixture_transform=None, expected_failure=None,
expected_marker='PASS: 114 shared request/login payload and routing traces'):
expected_marker='PASS: 114 shared request/login payload and routing traces',
standard_nrf52=False):
with tempfile.TemporaryDirectory(prefix="mesh-contact-cache-") as temp:
temp = Path(temp)
store = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
@@ -188,7 +218,7 @@ class ContactCacheTest(unittest.TestCase):
result = subprocess.run([
"c++", "-std=c++17", "-O1", "-g", "-Wall", "-Wextra",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-DMESH_CONTACT_CACHE=1", "-DCOMPANION_RADIO_FULL=1",
*([] if standard_nrf52 else ["-DMESH_CONTACT_CACHE=1", "-DCOMPANION_RADIO_FULL=1"]),
"-DMESH_CONTACT_SECRET_FLASH_CACHE=1",
"-D" + platform + "=1", "-DMAX_CONTACTS=350",
"-I", str(ROOT / "test/fixtures/contact_cache/mocks"),
+19
View File
@@ -22,6 +22,25 @@ def method(source: str, signature: str) -> str:
class OneKeyDMSharedQueueTest(unittest.TestCase):
def test_standard_high_contact_nrf52_selects_shared_pool_by_default(self):
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
start = header.index("#if defined(NRF52_PLATFORM) && MAX_CONTACTS > 300")
end = header.index("#ifndef ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE", start)
policy = '#include <helpers/ContactCachePolicy.h>\n' + header[start:end]
policy += '\nstatic_assert(MESH_CONTACT_CACHE == 1);\n'
policy += 'static_assert(ONE_KEY_DM_SHARED_OFFLINE_QUEUE == 1);\n'
policy += 'static_assert(OFFLINE_QUEUE_SIZE == 256);\nint main() {}\n'
with tempfile.TemporaryDirectory(prefix="meshcore-dm-policy-") as temp:
binary = Path(temp) / "policy.exe"
for capacity in (301, 350):
with self.subTest(capacity=capacity):
result = subprocess.run([
"c++", "-std=c++17", "-x", "c++", "-",
"-DNRF52_PLATFORM=1", f"-DMAX_CONTACTS={capacity}",
"-I", str(ROOT / "src"), "-o", str(binary),
], input=policy, text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
def compile_and_run(self, source: str):
with tempfile.TemporaryDirectory(prefix="meshcore-shared-dm-") as temp:
path = Path(temp) / "shared_dm.cpp"