From 406fce62ea4bfba0a79f02ff3513896c3d1c127c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 28 Sep 2026 15:04:15 -0700 Subject: [PATCH] Expose held one-key DM count in Companion CLI --- docs/companion_protocol.md | 7 ++++-- examples/companion_radio/MyMesh.cpp | 5 +++++ test/test_one_key_dm_shared_queue.py | 32 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 4b8c2e6ef..dc8142d20 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -673,10 +673,13 @@ separate from advert auto-add. Neither mode accepts an invalid signature or overwrites an existing contact. Stock receivers still need to learn the sender key by advert or import before they can decrypt an ordinary DM. The first send uses an additional radio packet and its reported timeout includes the delay. +`get dm.held` reports the current number of verified, decryptable DMs waiting +for contact acceptance (0-15); it is read-only and does not expose message text. Flash-constrained STM32WL Companion builds omit one-key DM support to retain -their existing filesystem boundary. On those builds, `get dm.one_key` and -`set dm.one_key on|off` report `Error: one-key DMs unsupported on this build`. +their existing filesystem boundary. On those builds, `get dm.one_key`, +`set dm.one_key on|off`, and `get dm.held` report +`Error: one-key DMs unsupported on this build`. Ordinary contact-based private messages are unaffected. For a two-radio hardware check, run `python3 tools/hil/one_key_dm.py --sender diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1d20d1603..e1c51c58c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2591,6 +2591,10 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* sprintf(reply, "> %s", _prefs.one_key_dm_enabled == 1 ? "on" : "off"); return true; } + if (strcmp(command, "get dm.held") == 0) { + sprintf(reply, "> %u", (unsigned)held_dm_count); + return true; + } if (memcmp(command, "set dm.one_key ", 15) == 0) { const char* value = &command[15]; if (strcmp(value, "on") == 0 || strcmp(value, "off") == 0) { @@ -2604,6 +2608,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } #else if (strcmp(command, "get dm.one_key") == 0 || + strcmp(command, "get dm.held") == 0 || strncmp(command, "set dm.one_key ", 15) == 0) { strcpy(reply, "Error: one-key DMs unsupported on this build"); return true; diff --git a/test/test_one_key_dm_shared_queue.py b/test/test_one_key_dm_shared_queue.py index 976fe446f..c58ed84ea 100644 --- a/test/test_one_key_dm_shared_queue.py +++ b/test/test_one_key_dm_shared_queue.py @@ -21,6 +21,38 @@ def method(source: str, signature: str) -> str: class SharedOneKeyQueueTest(unittest.TestCase): + def test_held_count_cli_reports_current_queue_length(self): + source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() + getter = method(source, 'if (strcmp(command, "get dm.held") == 0)') + harness = f''' +#include +#include +#include +bool query(const char* command, char* reply, unsigned held_dm_count) {{ +{getter} + return false; +}} +int main() {{ + char reply[16] = {{}}; + assert(query("get dm.held", reply, 0)); + assert(strcmp(reply, "> 0") == 0); + assert(query("get dm.held", reply, 15)); + assert(strcmp(reply, "> 15") == 0); + assert(!query("get dm.one_key", reply, 15)); +}} +''' + with tempfile.TemporaryDirectory(prefix="meshcore-held-dm-cli-") as temp: + path = Path(temp) / "held_dm_cli.cpp" + binary = Path(temp) / "held_dm_cli.exe" + path.write_text(harness) + built = subprocess.run( + ["c++", "-std=c++17", str(path), "-o", str(binary)], + text=True, capture_output=True, + ) + self.assertEqual(built.returncode, 0, built.stderr) + result = subprocess.run([str(binary)], text=True, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr) + def test_256_slots_and_fifteen_held_dms(self): source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() methods = "\n\n".join(method(source, signature) for signature in (