From 58755eb715f639ccc2a799d6ea380b3eb667d960 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sat, 20 Jun 2026 13:07:39 -0400 Subject: [PATCH 1/2] Add exact MC0 MeshCore DM routing --- README.md | 10 +- docs/tdeck-feature-inventory.md | 2 +- docs/tdeck-firmware-roadmap.md | 14 +- docs/tdeck-meshcore-companion-protocol.md | 37 ++- scripts/mc_companion_usb_smoke.py | 35 ++- scripts/tdeck_smoke.py | 12 +- sim/backend_sim.c | 46 ++- sim/main_sim.c | 75 ++++- src/backend_sx1262.cpp | 65 ++++- src/serial_cli.cpp | 7 +- src/services/mesh.h | 3 + src/services/mesh_core.c | 329 ++++++++++++++++------ 12 files changed, 489 insertions(+), 146 deletions(-) diff --git a/README.md b/README.md index bfcf95d..be5602e 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,11 @@ iPhone-style dark look (status bar, battery glyph, grouped settings cards). enabling one frees the other. ### 🔭 Later -- **MeshCore companion bridge** — V0 protocol foundation is drafted, and an - initial `companion mc ...` USB serial-console smoke surface can report - snapshots and exercise send boundaries; the formal USB/BLE bridge remains - planned, and it is not official MeshCore app compatible unless the real - MeshCore app protocol is confirmed. +- **MeshCore companion bridge** - V0 protocol foundation is drafted, with + `companion mc ...` diagnostics and formal USB `MC0` mode for snapshots, + public send, private send, and exact public-key DM routing. BLE transport, + live events, and official MeshCore app compatibility remain planned until the + real MeshCore app protocol is confirmed. - **Roll the iPhone look everywhere** — grouped cards / dividers across Messages, Nodes, Contacts. - **Local app platform** - scan local app manifests from `/sd/limitlezz/apps`, `/sd/apps`, `/appfs/apps`, and simulator data dirs, then show accepted apps diff --git a/docs/tdeck-feature-inventory.md b/docs/tdeck-feature-inventory.md index ad3fb6e..1ae2005 100644 --- a/docs/tdeck-feature-inventory.md +++ b/docs/tdeck-feature-inventory.md @@ -75,7 +75,7 @@ Status labels: | MeshCore self-advert TX | Partial, needs validation | Ed25519 identity, self-advert builder, serial/UI advert commands | Needs interop proof with real MeshCore nodes. | | MeshCore public channel / rooms | Planned | README says receive/default Public channel still ahead | V0.6: implement group text decode/send, room model, and split airtime config. | | MeshCore DMs | Planned | MeshCore contacts are non-messageable while gated | V0.7: implement key/session model, send path, ACKs, and UI routing. | -| MeshCore companion bridge | Planned/In progress | `docs/tdeck-meshcore-companion-protocol.md` drafts the V0 USB serial line protocol; `companion mc ...` provides an initial firmware smoke surface for snapshots, sends, and self-test | V0.8: formalize USB first, mirror to BLE later, and do not claim external MeshCore app compatibility until the real app protocol is confirmed. | +| MeshCore companion bridge | Partial, needs validation | `docs/tdeck-meshcore-companion-protocol.md` defines the V0 USB serial line protocol; `companion mc ...` and formal `MC0` mode provide snapshot, public-send, private-send, exact public-key DM routing, and self-test surfaces | V0.8: hardware-validate USB MC0, add live events, mirror to BLE later, and do not claim external MeshCore app compatibility until the real app protocol is confirmed. | ## User Interface diff --git a/docs/tdeck-firmware-roadmap.md b/docs/tdeck-firmware-roadmap.md index e04f738..0edd07d 100644 --- a/docs/tdeck-firmware-roadmap.md +++ b/docs/tdeck-firmware-roadmap.md @@ -190,16 +190,20 @@ Exit criteria: Goal: expose MeshCore companion functionality only after native MeshCore messaging is stable. **Status:** protocol foundation in progress with an initial USB serial-console -smoke surface. `companion mc hello|status|nodes|threads|send|dm|test` now -exercises the firmware-owned MeshCore snapshots and send boundaries for COM8 -validation, while the formal `MC0` bridge, BLE transport, events, and real -external MeshCore app compatibility are still planned work. +smoke surface plus formal USB `MC0` mode. `companion mc +hello|status|nodes|threads|send|dm|test` and `MC0` +`HELLO|IDENTITY|STATUS|NODES|THREADS|SEND_PUBLIC|SEND_DM|EXIT` exercise the +firmware-owned MeshCore snapshots and send boundaries for COM8 validation. +`MC0` uses full public-key addresses for exact DM routing. BLE transport, live +events, and real external MeshCore app compatibility are still planned work. Deliverables: - Define the MeshCore companion protocol surface for node DB, public chat, private chats, and send/receive forwarding. Drafted as `docs/tdeck-meshcore-companion-protocol.md`. - Add a USB serial-console smoke surface that reports MeshCore companion status, nodes, threads, Public send, DM send, and self-test. -- Implement MeshCore USB companion mode. +- Implement MeshCore USB companion mode. USB `MC0` is implemented for + snapshots, public send, private send, exact full-public-key DM routing, and + explicit exit back to console; live event streaming is still a later step. - Implement MeshCore BLE companion mode. - Add UI and serial commands that distinguish Meshtastic companion from MeshCore companion. - Decide whether one companion session or one network can own the external-app bridge at a time. diff --git a/docs/tdeck-meshcore-companion-protocol.md b/docs/tdeck-meshcore-companion-protocol.md index e5cea30..800d9eb 100644 --- a/docs/tdeck-meshcore-companion-protocol.md +++ b/docs/tdeck-meshcore-companion-protocol.md @@ -140,14 +140,16 @@ MC0 2 IDENTITY Response: ```text -MC0 2 OK enabled=1 name=Jess addr=4f8e21a0 role=chat pubkey=6b1d... addr_format=meshcore-hex advert_ready=1 +MC0 2 OK enabled=1 name=Jess addr=6b1d000000000000000000000000000000000000000000000000000000000000 short_id=MC-6b1d0000 role=chat pubkey=6b1d000000000000000000000000000000000000000000000000000000000000 addr_format=meshcore-pubkey-hex advert_ready=1 ``` Required fields: - `enabled`: whether MeshCore is currently enabled. - `name`: percent-encoded local display name. -- `addr`: local MeshCore address, lowercase hex, opaque to the host. +- `addr`: local MeshCore address, lowercase 64-hex MeshCore public key, opaque + to the host except for exact equality. +- `short_id`: optional display-only short identifier. Hosts must not route by it. - `role`: current local MeshCore role, such as `chat`, `router`, or `unknown`. - `addr_format`: the address encoding advertised for this session. @@ -197,8 +199,8 @@ Response: ```text MC0 4 BEGIN type=nodes rev=42 count=2 more=0 cursor=end -MC0 4 NODE addr=4f8e21a0 name=Limitlezz role=chat seen_ms=12000 snr=-9 rssi=-112 public_key=present dm=ready -MC0 4 NODE addr=12ab9001 name=Hilltop role=router seen_ms=180000 snr=-14 rssi=-118 public_key=missing dm=not_messageable +MC0 4 NODE addr=4f8e21a000000000000000000000000000000000000000000000000000000000 short_id=MC-4f8e21a0 name=Limitlezz role=chat seen_ms=12000 snr=-9 rssi=-112 public_key=present dm=ready +MC0 4 NODE addr=- short_id=MC-12ab9001 name=Hilltop role=router seen_ms=180000 snr=-14 rssi=-118 public_key=missing dm=not_messageable MC0 4 END type=nodes rev=42 count=2 more=0 cursor=end ``` @@ -206,12 +208,15 @@ Request fields: - `since`: last `nodes_rev` known by the host, or `0` for a full snapshot. - `limit`: maximum rows requested. Firmware may cap this below the requested - value. + value; V0 USB mode caps node snapshots at five rows to keep line responses + bounded. - `cursor`: optional opaque cursor from a previous `NODES` response. Node fields: -- `addr`: MeshCore address, lowercase hex, opaque to the host. +- `addr`: MeshCore address, lowercase 64-hex public key when known. `-` means + the firmware has no usable key yet, so `SEND_DM to_addr=...` is impossible. +- `short_id`: display-only short identifier for compact UI labels. - `name`: percent-encoded display name, if known. - `role`: `chat`, `router`, `repeater`, `sensor`, or `unknown`. - `seen_ms`: milliseconds since last heard, or `-1` if unknown. @@ -263,7 +268,7 @@ name. Address is preferred because display names can collide. By address: ```text -MC0 6 SEND_DM to_addr=4f8e21a0 text=Meet%20at%20camp client_mid=pc-0002 +MC0 6 SEND_DM to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 text=Meet%20at%20camp client_mid=pc-0002 ``` By known name: @@ -275,13 +280,13 @@ MC0 7 SEND_DM to_name=Limitlezz text=Copy%20that client_mid=pc-0003 Immediate response: ```text -MC0 6 OK accepted=1 msg_id=mc-805 to_addr=4f8e21a0 status=queued +MC0 6 OK accepted=1 msg_id=mc-805 to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 status=queued ``` Later event: ```text -MC0 EVT 123 tx_status client_mid=pc-0002 msg_id=mc-805 kind=dm to_addr=4f8e21a0 status=delivered +MC0 EVT 123 tx_status client_mid=pc-0002 msg_id=mc-805 kind=dm to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 status=delivered ``` V0 name matching rules: @@ -292,6 +297,8 @@ V0 name matching rules: - If more than one node matches, return `ERR code=ambiguous_name`. - If the matching node lacks a usable session/key, return `ERR code=no_key`. - If the node role is not messageable, return `ERR code=not_messageable`. +- If both `to_addr` and `to_name` are supplied, they must identify the same + node or firmware returns `ERR code=target_mismatch`. The host does not manage MeshCore private keys or sessions in V0. @@ -315,10 +322,10 @@ MC0 8 OK events=on types=nodes,messages,tx,status event_seq=123 Supported event types: ```text -MC0 EVT 124 node_upsert addr=4f8e21a0 nodes_rev=43 +MC0 EVT 124 node_upsert addr=4f8e21a000000000000000000000000000000000000000000000000000000000 nodes_rev=43 MC0 EVT 125 snapshot_dirty type=nodes rev=43 reason=node_upsert -MC0 EVT 126 rx_public msg_id=mc-806 from_addr=4f8e21a0 from_name=Limitlezz room=public text=Copy%20CH0. -MC0 EVT 127 rx_dm msg_id=mc-807 from_addr=4f8e21a0 from_name=Limitlezz text=Direct%20copy. +MC0 EVT 126 rx_public msg_id=mc-806 from_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 from_name=Limitlezz room=public text=Copy%20CH0. +MC0 EVT 127 rx_dm msg_id=mc-807 from_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 from_name=Limitlezz text=Direct%20copy. MC0 EVT 128 tx_status client_mid=pc-0002 msg_id=mc-805 kind=dm status=failed reason=ack_timeout retry=1 MC0 EVT 129 status mc=on tdm=active airtime=balanced queue=0 ``` @@ -390,9 +397,9 @@ Rules: `companion mc test`. - Formal USB MC0 smoke is opt-in: `python scripts/mc_companion_usb_smoke.py --mc0-usb` enters the configured - USB mode, sends `HELLO`, `STATUS`, and `NODES`, asserts `MC0 ... OK`, - `BEGIN`, and `END` response markers, then exits through the configured - `MC0 EXIT` line. + USB mode, sends `HELLO`, `IDENTITY`, `STATUS`, and `NODES`, asserts + `MC0 ... OK`, the public-key address format, `BEGIN`, and `END` response + markers, then exits through the configured `MC0 EXIT` line. - If firmware lands different command names, use the smoke helper's `--mc0-enter-command`, `--mc0-*-template`, `--mc0-*-marker`, and `--mc0-exit-template` flags instead of editing firmware or weakening the diff --git a/scripts/mc_companion_usb_smoke.py b/scripts/mc_companion_usb_smoke.py index 1d7f61f..764fb40 100644 --- a/scripts/mc_companion_usb_smoke.py +++ b/scripts/mc_companion_usb_smoke.py @@ -33,10 +33,12 @@ DEFAULT_PUBLIC_TEMPLATE = "companion mc send {text}" DEFAULT_MC0_ENTER_COMMAND = "companion mc usb on" DEFAULT_MC0_HELLO_ID = "1" -DEFAULT_MC0_STATUS_ID = "2" -DEFAULT_MC0_NODES_ID = "3" +DEFAULT_MC0_IDENTITY_ID = "2" +DEFAULT_MC0_STATUS_ID = "3" +DEFAULT_MC0_NODES_ID = "4" DEFAULT_MC0_EXIT_ID = "99" DEFAULT_MC0_HELLO_TEMPLATE = "MC0 {id} HELLO proto=0 app=limitlezz-smoke host=windows want=none" +DEFAULT_MC0_IDENTITY_TEMPLATE = "MC0 {id} IDENTITY" DEFAULT_MC0_STATUS_TEMPLATE = "MC0 {id} STATUS" DEFAULT_MC0_NODES_TEMPLATE = "MC0 {id} NODES since=0 limit=5" DEFAULT_MC0_EXIT_TEMPLATE = "MC0 {id} EXIT" @@ -180,6 +182,9 @@ def build_mc0_specs(args: argparse.Namespace) -> list[CommandSpec]: hello_command = format_mc0_template( "--mc0-hello-template", args.mc0_hello_template, args.mc0_hello_id ) + identity_command = format_mc0_template( + "--mc0-identity-template", args.mc0_identity_template, args.mc0_identity_id + ) status_command = format_mc0_template( "--mc0-status-template", args.mc0_status_template, args.mc0_status_id ) @@ -195,6 +200,18 @@ def build_mc0_specs(args: argparse.Namespace) -> list[CommandSpec]: [f"MC0 {args.mc0_hello_id} OK"], ), ), + CommandSpec( + "mc0 identity", + identity_command, + default_marker_override( + args.mc0_identity_marker, + [ + f"MC0 {args.mc0_identity_id} OK", + "addr_format=meshcore-pubkey-hex", + "pubkey=", + ], + ), + ), CommandSpec( "mc0 status", status_command, @@ -549,6 +566,7 @@ def main() -> int: help="Seconds of quiet serial input that ends an optional MC0 read.", ) mc0.add_argument("--mc0-hello-id", default=DEFAULT_MC0_HELLO_ID) + mc0.add_argument("--mc0-identity-id", default=DEFAULT_MC0_IDENTITY_ID) mc0.add_argument("--mc0-status-id", default=DEFAULT_MC0_STATUS_ID) mc0.add_argument("--mc0-nodes-id", default=DEFAULT_MC0_NODES_ID) mc0.add_argument("--mc0-exit-id", default=DEFAULT_MC0_EXIT_ID) @@ -557,6 +575,11 @@ def main() -> int: default=DEFAULT_MC0_HELLO_TEMPLATE, help="MC0 HELLO line template; supports {id}.", ) + mc0.add_argument( + "--mc0-identity-template", + default=DEFAULT_MC0_IDENTITY_TEMPLATE, + help="MC0 IDENTITY line template; supports {id}.", + ) mc0.add_argument( "--mc0-status-template", default=DEFAULT_MC0_STATUS_TEMPLATE, @@ -572,6 +595,14 @@ def main() -> int: action="append", help="Override HELLO expected marker(s). Defaults to 'MC0 OK'.", ) + mc0.add_argument( + "--mc0-identity-marker", + action="append", + help=( + "Override IDENTITY expected marker(s). Defaults to OK, " + "addr_format=meshcore-pubkey-hex, and pubkey=." + ), + ) mc0.add_argument( "--mc0-status-marker", action="append", diff --git a/scripts/tdeck_smoke.py b/scripts/tdeck_smoke.py index 06d493d..4bb5e35 100644 --- a/scripts/tdeck_smoke.py +++ b/scripts/tdeck_smoke.py @@ -115,16 +115,16 @@ def nostub_upload(project_dir: Path, env_name: str, port: str, baud: int, artifa "--baud", str(baud), "--before", - "default-reset", + "default_reset", "--after", - "hard-reset", + "hard_reset", "--no-stub", - "write-flash", - "--flash-mode", + "write_flash", + "--flash_mode", "dio", - "--flash-freq", + "--flash_freq", "80m", - "--flash-size", + "--flash_size", "16MB", "0x0", str(bootloader), diff --git a/sim/backend_sim.c b/sim/backend_sim.c index a4aa37a..2191b6b 100644 --- a/sim/backend_sim.c +++ b/sim/backend_sim.c @@ -15,6 +15,7 @@ #include "sim_radio.h" #include #include +#include void lz_backend_init(void) { sim_radio_init(); } @@ -36,11 +37,54 @@ bool lz_backend_mc_advert_now(bool flood) { return sim_radio_mc_advert_now(flood void lz_backend_mc_addr(char *buf, int n) { snprintf(buf, n, "MC-1ec77175"); } +bool lz_backend_mc_pubkey(uint8_t out32[32]) +{ + if(!out32) return false; + for(int i = 0; i < 32; i++) out32[i] = (uint8_t)(0x10 + i); + return true; +} + /* MeshCore outbound API. sim_radio models inbound MeshCore (sim_inject_mc_*); * the outbound contract still needs these symbols defined or the native build * fails to link on macOS (weak externs in mesh_core.c don't resolve to NULL). */ bool lz_backend_mc_send_public(const char *text) { (void)text; return true; } -bool lz_backend_mc_dm(const char *name, const char *text) { (void)name; (void)text; return true; } + +static bool g_last_mc_dm; +static uint8_t g_last_mc_dm_key[32]; +static char g_last_mc_dm_name[28]; +static char g_last_mc_dm_text[160]; + +bool lz_backend_mc_dm_key(const uint8_t peer_pub[32], const char *name_hint, const char *text) +{ + if(!peer_pub || !text || !text[0]) return false; + g_last_mc_dm = true; + memcpy(g_last_mc_dm_key, peer_pub, 32); + snprintf(g_last_mc_dm_name, sizeof g_last_mc_dm_name, "%s", + name_hint ? name_hint : ""); + snprintf(g_last_mc_dm_text, sizeof g_last_mc_dm_text, "%s", text); + return true; +} + +bool lz_backend_mc_dm(const char *name, const char *text) { (void)name; (void)text; return false; } + +void sim_backend_mc_clear_last_dm(void) +{ + g_last_mc_dm = false; + memset(g_last_mc_dm_key, 0, sizeof g_last_mc_dm_key); + g_last_mc_dm_name[0] = 0; + g_last_mc_dm_text[0] = 0; +} + +bool sim_backend_mc_last_dm(uint8_t out32[32], char *name, int name_cap, + char *text, int text_cap) +{ + if(!g_last_mc_dm) return false; + if(out32) memcpy(out32, g_last_mc_dm_key, 32); + if(name && name_cap > 0) snprintf(name, (size_t)name_cap, "%s", g_last_mc_dm_name); + if(text && text_cap > 0) snprintf(text, (size_t)text_cap, "%s", g_last_mc_dm_text); + return true; +} + int lz_backend_mc_peers(char *buf, int n) { snprintf(buf, n, "no MeshCore peers (sim)\n"); return 0; } static bool g_sim_companion; diff --git a/sim/main_sim.c b/sim/main_sim.c index 3073a12..34d94a4 100644 --- a/sim/main_sim.c +++ b/sim/main_sim.c @@ -62,6 +62,15 @@ static SDL_Window *win; static SDL_Renderer *ren; static SDL_Texture *tex; +extern void sim_backend_mc_clear_last_dm(void); +extern bool sim_backend_mc_last_dm(uint8_t out32[32], char *name, int name_cap, + char *text, int text_cap); +extern lz_node_rt *lz_seed_node(uint32_t num, const char *id, lz_net_t net, + const char *name, const char *sc, + const char *role, float snr, int batt, + const char *hw, const char *dist, + uint32_t ago_s, bool contact); + uint32_t lz_tick_ms(void) { return SDL_GetTicks(); } /* sim_reset_dir() lives in sim_fs.c (recursive, Windows-safe) — included via sim_fs.h */ @@ -2063,10 +2072,12 @@ static int codec_selftest(void) lz_svc_init(NULL, false); uint8_t pub[32] = {0}; pub[0] = 0x42; + char pub_addr[65]; + for(int i = 0; i < 32; i++) sprintf(pub_addr + i * 2, "%02x", pub[i]); lz_core_on_mc_node(pub, "CompanionPeer", 1, -7.5f); lz_core_on_mc_channel_text("CompanionPeer", "public hello", -7.5f); lz_core_on_mc_dm(pub, "CompanionPeer", "dm hello", -7.5f); - char hello[180], status[220], nodes[420], threads[520]; + char hello[240], status[280], nodes[700], threads[520]; lz_svc_mc_companion_hello(hello, sizeof hello); lz_svc_mc_companion_status(status, sizeof status); lz_svc_mc_companion_nodes(nodes, sizeof nodes); @@ -2075,14 +2086,26 @@ static int codec_selftest(void) "MeshCore companion v0 hello reports protocol"); CHECK(strstr(status, "nodes=1") != NULL && strstr(status, "threads=2") != NULL, "MeshCore companion v0 status counts snapshots"); - CHECK(strstr(nodes, "CompanionPeer") != NULL && strstr(nodes, "dm=yes") != NULL, - "MeshCore companion v0 node snapshot lists messageable peer"); + CHECK(strstr(nodes, "CompanionPeer") != NULL && + strstr(nodes, pub_addr) != NULL && + strstr(nodes, "short_id=MC-42000000") != NULL && + strstr(nodes, "public_key=present") != NULL && + strstr(nodes, "dm=yes") != NULL, + "MeshCore companion v0 node snapshot lists exact-address peer"); CHECK(strstr(threads, "public hello") != NULL && strstr(threads, "dm hello") != NULL, "MeshCore companion v0 thread snapshot lists public and DM threads"); CHECK(lz_svc_mc_companion_send_public("public from companion"), "MeshCore companion v0 public send uses service boundary"); + sim_backend_mc_clear_last_dm(); CHECK(lz_svc_mc_companion_send_dm("CompanionPeer", "dm from companion"), "MeshCore companion v0 DM send uses service boundary"); + uint8_t last_key[32]; char last_name[28], last_text[160]; + CHECK(sim_backend_mc_last_dm(last_key, last_name, sizeof last_name, + last_text, sizeof last_text) && + memcmp(last_key, pub, 32) == 0 && + strcmp(last_name, "CompanionPeer") == 0 && + strcmp(last_text, "dm from companion") == 0, + "MeshCore companion v0 DM sends exact public key"); char mc0[900], proto[120]; bool mc0_exit = false; lz_svc_mc_companion_handle_line("MC0 1 HELLO proto=0 app=selftest", mc0, sizeof mc0, &mc0_exit); @@ -2094,9 +2117,13 @@ static int codec_selftest(void) "MeshCore MC0 STATUS counts snapshots"); lz_svc_mc_companion_handle_line("MC0 3 NODES since=0 limit=5", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "MC0 3 BEGIN type=nodes") != NULL && + strstr(mc0, pub_addr) != NULL && + strstr(mc0, "short_id=MC-42000000") != NULL && + strstr(mc0, "public_key=present") != NULL && + strstr(mc0, "dm=ready") != NULL && strstr(mc0, "name=CompanionPeer") != NULL && strstr(mc0, "MC0 3 END type=nodes") != NULL, - "MeshCore MC0 NODES snapshot lists peer"); + "MeshCore MC0 NODES snapshot lists full-address peer"); lz_svc_mc_companion_handle_line("MC0 4 THREADS", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "MC0 4 BEGIN type=threads") != NULL && strstr(mc0, "text=CompanionPeer%3A%20public%20hello") != NULL && @@ -2105,10 +2132,42 @@ static int codec_selftest(void) lz_svc_mc_companion_handle_line("MC0 5 SEND_PUBLIC text=mc0%20public", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "MC0 5 OK accepted=1") != NULL, "MeshCore MC0 SEND_PUBLIC uses service boundary"); - lz_svc_mc_companion_handle_line("MC0 6 SEND_DM to_name=companionpeer text=mc0%20dm", mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 6 OK accepted=1") != NULL, - "MeshCore MC0 SEND_DM uses service boundary"); - lz_svc_mc_companion_handle_line("MC0 7 EXIT", mc0, sizeof mc0, &mc0_exit); + sim_backend_mc_clear_last_dm(); + char mc0_cmd[180]; + snprintf(mc0_cmd, sizeof mc0_cmd, "MC0 6 SEND_DM to_addr=%s text=mc0%%20dm", pub_addr); + lz_svc_mc_companion_handle_line(mc0_cmd, mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 6 OK accepted=1") != NULL && + strstr(mc0, pub_addr) != NULL, + "MeshCore MC0 SEND_DM accepts exact public-key address"); + CHECK(sim_backend_mc_last_dm(last_key, last_name, sizeof last_name, + last_text, sizeof last_text) && + memcmp(last_key, pub, 32) == 0 && + strcmp(last_text, "mc0 dm") == 0, + "MeshCore MC0 SEND_DM routes by exact public key"); + lz_svc_mc_companion_handle_line("MC0 7 SEND_DM to_name=companionpeer text=mc0%20dm%20name", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 7 OK accepted=1") != NULL, + "MeshCore MC0 SEND_DM keeps unambiguous name fallback"); + lz_seed_node(0x00009999u, "MC-nokey", LZ_NET_MC, "NoKey", "NOK", + "Chat", -5.0f, 90, "MeshCore", "1.0 km", 10, false); + lz_svc_mc_companion_handle_line("MC0 8 SEND_DM to_name=NoKey text=x", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "ERR code=no_key") != NULL, + "MeshCore MC0 SEND_DM rejects chat nodes without a key"); + uint8_t other[32] = {0}; + other[0] = 0x44; other[3] = 0x01; + lz_core_on_mc_node(other, "OtherPeer", 1, -7.5f); + lz_svc_mc_companion_handle_line("MC0 9 SEND_DM to_addr=4200000000000000000000000000000000000000000000000000000000000000 to_name=OtherPeer text=x", + mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "ERR code=target_mismatch") != NULL, + "MeshCore MC0 SEND_DM rejects mismatched address/name targets"); + uint8_t dup1[32] = {0}, dup2[32] = {0}; + dup1[0] = 0x50; dup1[3] = 0x01; + dup2[0] = 0x51; dup2[3] = 0x02; + lz_core_on_mc_node(dup1, "DupePeer", 1, -7.5f); + lz_core_on_mc_node(dup2, "DupePeer", 1, -7.5f); + lz_svc_mc_companion_handle_line("MC0 10 SEND_DM to_name=dupepeer text=x", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "ERR code=ambiguous_name") != NULL, + "MeshCore MC0 SEND_DM rejects duplicate display names"); + lz_svc_mc_companion_handle_line("MC0 11 EXIT", mc0, sizeof mc0, &mc0_exit); CHECK(mc0_exit && strstr(mc0, "state=detached") != NULL, "MeshCore MC0 EXIT returns to console"); lz_svc_mc_companion_selftest(proto, sizeof proto); diff --git a/src/backend_sx1262.cpp b/src/backend_sx1262.cpp index 298d9c2..b097562 100644 --- a/src/backend_sx1262.cpp +++ b/src/backend_sx1262.cpp @@ -801,20 +801,32 @@ static void handle_mc_path(const mc_pkt_t *p) handle_mc_ack(p->payload + i); } -/* send a direct message to a known MeshCore peer (matched by node-name substring). */ -extern "C" bool lz_backend_mc_dm(const char *name, const char *text) +static bool mc_name_eq_ci(const char *a, const char *b) { - if(!g_ok || !g_net_mc || !name || !text || !text[0]) return false; + if(!a || !b) return false; + while(*a && *b) { + char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a - 'A' + 'a') : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? (char)(*b - 'A' + 'a') : *b; + if(ca != cb) return false; + a++; b++; + } + return *a == 0 && *b == 0; +} + +static bool send_mc_dm_to_key(const uint8_t ppub[32], const char *name_hint, const char *text) +{ + if(!g_ok || !g_net_mc || !ppub || !text || !text[0]) return false; + char pname[24]; + snprintf(pname, sizeof pname, "%s", (name_hint && name_hint[0]) ? name_hint : "MeshCore peer"); + const lz_node_rt *nodes; int nn = lz_svc_nodes(&nodes); - uint8_t ppub[32]; char pname[24]; bool found = false; - for(int i = 0; i < nn; i++) - if(nodes[i].net == LZ_NET_MC && nodes[i].has_key && nodes[i].name[0] && - strstr(nodes[i].name, name)) { - memcpy(ppub, nodes[i].pubkey, 32); - snprintf(pname, sizeof pname, "%s", nodes[i].name); - found = true; break; + for(int i = 0; i < nn; i++) { + if(nodes[i].net == LZ_NET_MC && nodes[i].has_key && + memcmp(nodes[i].pubkey, ppub, 32) == 0) { + if(nodes[i].name[0]) snprintf(pname, sizeof pname, "%s", nodes[i].name); + break; } - if(!found) return false; /* unknown peer: need their advert first */ + } retune_guarded(PROF_MC, millis()); uint8_t shared[32]; @@ -840,6 +852,30 @@ extern "C" bool lz_backend_mc_dm(const char *name, const char *text) return sent; } +/* send a direct message to an exact MeshCore public key selected by the core. */ +extern "C" bool lz_backend_mc_dm_key(const uint8_t peer_pub[32], + const char *name_hint, + const char *text) +{ + return send_mc_dm_to_key(peer_pub, name_hint, text); +} + +/* legacy serial-console wrapper: exact unique display-name match only. */ +extern "C" bool lz_backend_mc_dm(const char *name, const char *text) +{ + if(!name || !name[0]) return false; + const lz_node_rt *nodes; int nn = lz_svc_nodes(&nodes); + const lz_node_rt *match = NULL; + for(int i = 0; i < nn; i++) { + if(nodes[i].net != LZ_NET_MC || !nodes[i].has_key || !nodes[i].name[0]) + continue; + if(!mc_name_eq_ci(nodes[i].name, name)) continue; + if(match) return false; + match = &nodes[i]; + } + return match && send_mc_dm_to_key(match->pubkey, match->name, text); +} + /* list known MeshCore peers (serial `mc peers`) */ extern "C" int lz_backend_mc_peers(char *buf, int n) { @@ -1099,6 +1135,13 @@ extern "C" int lz_backend_mc_id(char *buf, int n) return k; } +extern "C" bool lz_backend_mc_pubkey(uint8_t out32[32]) +{ + if(!g_mc_id_ok || !out32) return false; + memcpy(out32, g_mc_pub, 32); + return true; +} + /* self-test: build our advert, parse + Ed25519-verify it exactly as a remote * MeshCore node would. VALID here == a real node will accept our advert. */ extern "C" int lz_backend_mc_selftest(char *buf, int n) diff --git a/src/serial_cli.cpp b/src/serial_cli.cpp index 26510bc..e1d177e 100644 --- a/src/serial_cli.cpp +++ b/src/serial_cli.cpp @@ -27,7 +27,6 @@ extern "C" void lz_backend_mc_tune(float freq, float bw, int sf, int cr, int syn extern "C" int lz_backend_mc_id(char *buf, int n) __attribute__((weak)); extern "C" bool lz_backend_mc_advert_now(bool flood) __attribute__((weak)); extern "C" bool lz_backend_mc_send_public(const char *text) __attribute__((weak)); -extern "C" bool lz_backend_mc_dm(const char *name, const char *text) __attribute__((weak)); extern "C" int lz_backend_mc_peers(char *buf, int n) __attribute__((weak)); extern "C" int lz_backend_mc_selftest(char *buf, int n) __attribute__((weak)); extern "C" int lz_mtc_selftest(char *buf, int n) __attribute__((weak)); @@ -234,10 +233,10 @@ static void cmd_mc(char *args) char *p = args + 3; char *sp = strchr(p, ' '); if(!sp) { Serial.println("usage: mc dm "); return; } *sp = 0; const char *text = sp + 1; - if(lz_backend_mc_dm && lz_backend_mc_dm(p, text)) + if(lz_svc_mc_companion_send_dm(p, text)) Serial.printf("[ok] DM sent to %s\n", p); else - Serial.println("[err] DM not sent (unknown peer? try 'mc peers')"); + Serial.println("[err] DM not sent (unknown, ambiguous, no key, or not messageable)"); return; } if(args && strcmp(args, "peers") == 0) { @@ -377,7 +376,7 @@ static void cmd_companion(char *args) return; } if(strcmp(sub, "nodes") == 0) { - char b[760]; lz_svc_mc_companion_nodes(b, sizeof b); Serial.print(b); + char b[1200]; lz_svc_mc_companion_nodes(b, sizeof b); Serial.print(b); return; } if(strcmp(sub, "threads") == 0) { diff --git a/src/services/mesh.h b/src/services/mesh.h index 81cb0b1..c364d9f 100644 --- a/src/services/mesh.h +++ b/src/services/mesh.h @@ -476,6 +476,9 @@ void lz_backend_set_airtime(int mode); /* choose the both-networks split */ void lz_backend_request_nodeinfo(uint32_t to); /* ask a node for its NodeInfo (PKI key) */ bool lz_backend_mc_advert_now(bool flood); /* send a MeshCore self-advert (flood/zero-hop) */ void lz_backend_mc_addr(char *buf, int n); /* our MeshCore address, e.g. "MC-978bbe5f" */ +bool lz_backend_mc_pubkey(uint8_t out32[32]); /* our 32-byte MeshCore public key */ +bool lz_backend_mc_dm_key(const uint8_t peer_pub[32], const char *name_hint, + const char *text); /* MeshCore DM to exact public key */ /* companion bridge: USB serial speaks the Meshtastic app protocol when active */ bool lz_mtc_active(void); void lz_mtc_set_active(bool on); diff --git a/src/services/mesh_core.c b/src/services/mesh_core.c index afd12ea..6952cc7 100644 --- a/src/services/mesh_core.c +++ b/src/services/mesh_core.c @@ -63,7 +63,10 @@ extern uint32_t lz_tick_ms(void); /* MeshCore TX (real backend on the T-Deck; absent in the sim -> weak/NULL) */ extern bool lz_backend_mc_send_public(const char *text) __attribute__((weak)); -extern bool lz_backend_mc_dm(const char *name, const char *text) __attribute__((weak)); +extern bool lz_backend_mc_dm_key(const uint8_t peer_pub[32], + const char *name_hint, + const char *text) __attribute__((weak)); +extern bool lz_backend_mc_pubkey(uint8_t out32[32]) __attribute__((weak)); static lz_node_rt *g_nodes; /* 45 KB node DB — PSRAM-backed on T-Deck (alloc in lz_svc_init) */ static int g_node_count; @@ -989,6 +992,150 @@ lz_thread_rt *lz_svc_mc_channel_thread(void) return t; } +#define LZ_MC_ADDR_HEX_CHARS 64 + +static int mc_hex_val(char c) +{ + if(c >= '0' && c <= '9') return c - '0'; + if(c >= 'a' && c <= 'f') return c - 'a' + 10; + if(c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static void mc_key_hex(const uint8_t key[32], char *out, int cap) +{ + static const char hex[] = "0123456789abcdef"; + if(!out || cap <= 0) return; + if(!key || cap < LZ_MC_ADDR_HEX_CHARS + 1) { + out[0] = 0; + return; + } + for(int i = 0; i < 32; i++) { + out[i * 2] = hex[(key[i] >> 4) & 0x0F]; + out[i * 2 + 1] = hex[key[i] & 0x0F]; + } + out[LZ_MC_ADDR_HEX_CHARS] = 0; +} + +static bool mc_key_from_hex(const char *s, uint8_t out[32]) +{ + if(!s || !out || strlen(s) != LZ_MC_ADDR_HEX_CHARS) return false; + for(int i = 0; i < 32; i++) { + int hi = mc_hex_val(s[i * 2]); + int lo = mc_hex_val(s[i * 2 + 1]); + if(hi < 0 || lo < 0) return false; + out[i] = (uint8_t)((hi << 4) | lo); + } + return true; +} + +static bool mc_node_addr(const lz_node_rt *nd, char *out, int cap) +{ + if(!nd || nd->net != LZ_NET_MC || !nd->has_key) { + if(out && cap > 0) out[0] = 0; + return false; + } + mc_key_hex(nd->pubkey, out, cap); + return out && out[0] != 0; +} + +static void mc_node_short_id(const lz_node_rt *nd, char *out, int cap) +{ + if(!out || cap <= 0) return; + if(nd && nd->net == LZ_NET_MC && nd->has_key) { + snprintf(out, (size_t)cap, "MC-%02x%02x%02x%02x", + nd->pubkey[0], nd->pubkey[1], nd->pubkey[2], nd->pubkey[3]); + } else if(nd && nd->id[0]) { + snprintf(out, (size_t)cap, "%s", nd->id); + } else { + snprintf(out, (size_t)cap, "MC-unknown"); + } +} + +static bool mc_local_addr(char *out, int cap) +{ + uint8_t pub[32]; + if(lz_backend_mc_pubkey && lz_backend_mc_pubkey(pub)) { + mc_key_hex(pub, out, cap); + return out && out[0] != 0; + } + if(out && cap > 0) out[0] = 0; + return false; +} + +static lz_node_rt *mc_node_by_pubkey(const uint8_t key[32]) +{ + if(!key) return NULL; + for(int i = 0; i < g_node_count; i++) + if(g_nodes[i].net == LZ_NET_MC && g_nodes[i].has_key && + memcmp(g_nodes[i].pubkey, key, 32) == 0) + return &g_nodes[i]; + return NULL; +} + +static lz_node_rt *mc_node_by_addr(const char *addr, bool *bad_addr) +{ + uint8_t key[32]; + if(bad_addr) *bad_addr = false; + if(!addr || !addr[0]) return NULL; + if(!mc_key_from_hex(addr, key)) { + if(bad_addr) *bad_addr = true; + return NULL; + } + return mc_node_by_pubkey(key); +} + +static char mc_fold_char(char c) +{ + return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; +} + +static bool mc_name_eq(const char *a, const char *b) +{ + if(!a || !b) return false; + while(*a && *b) { + if(mc_fold_char(*a++) != mc_fold_char(*b++)) return false; + } + return *a == 0 && *b == 0; +} + +static lz_node_rt *mc_node_by_name_unique(const char *name, bool *ambiguous) +{ + lz_node_rt *match = NULL; + if(ambiguous) *ambiguous = false; + if(!name || !name[0]) return NULL; + for(int i = 0; i < g_node_count; i++) { + if(g_nodes[i].net != LZ_NET_MC || !mc_name_eq(g_nodes[i].name, name)) continue; + if(match) { + if(ambiguous) *ambiguous = true; + return match; + } + match = &g_nodes[i]; + } + return match; +} + +static const char *mc_companion_dm_state(const lz_node_rt *n) +{ + if(!n || n->net != LZ_NET_MC) return "not_messageable"; + if(strcmp(n->role, "Chat") != 0) return "not_messageable"; + if(!n->has_key) return "no_key"; + return "ready"; +} + +static bool mc_companion_dm_target(const lz_node_rt *n) +{ + return strcmp(mc_companion_dm_state(n), "ready") == 0; +} + +static bool mc_companion_send_dm_node(lz_node_rt *target, const char *text) +{ + if(!target || !text || !text[0] || !mc_companion_dm_target(target)) + return false; + return lz_backend_mc_dm_key && + lz_backend_mc_dm_key(target->pubkey, target->name, text); +} + static void track_loaded_delivery(lz_thread_rt *t); void lz_svc_open_thread(lz_thread_rt *t) @@ -1128,7 +1275,8 @@ bool lz_svc_send_text(lz_thread_rt *t, const char *text) if(!t || !text[0] || !t->messageable) return false; if(t->net == LZ_NET_MC) { /* MeshCore: backend sends + echoes into the thread */ if(t->is_channel) return lz_backend_mc_send_public && lz_backend_mc_send_public(text); - return lz_backend_mc_dm && lz_backend_mc_dm(t->name, text); + lz_node_rt *n = find_node(t->node_num); + return mc_companion_send_dm_node(n, text); } uint32_t ts = now_epoch(); uint32_t pid = next_packet_id(); @@ -1274,8 +1422,9 @@ int lz_svc_delivery_diag(char *buf, int n) int lz_svc_mc_companion_hello(char *buf, int n) { if(!buf || n <= 0) return 0; - char addr[24]; - lz_backend_mc_addr(addr, sizeof addr); + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + if(!mc_local_addr(addr, sizeof addr)) lz_backend_mc_addr(addr, sizeof addr); + lz_backend_mc_addr(short_id, sizeof short_id); const char *build = #if LZ_MESHCORE_ENABLED "enabled"; @@ -1283,8 +1432,8 @@ int lz_svc_mc_companion_hello(char *buf, int n) "gated"; #endif return snprintf(buf, (size_t)n, - "mccomp: hello v0 build=%s meshcore=%s addr=%s protocol=line\n", - build, build, addr); + "mccomp: hello v0 build=%s meshcore=%s addr=%s short_id=%s protocol=line\n", + build, build, addr, short_id); } int lz_svc_mc_companion_status(char *buf, int n) @@ -1298,19 +1447,14 @@ int lz_svc_mc_companion_status(char *buf, int n) mc_threads++; unread += g_threads[i].unread; } - char addr[24]; - lz_backend_mc_addr(addr, sizeof addr); + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + if(!mc_local_addr(addr, sizeof addr)) lz_backend_mc_addr(addr, sizeof addr); + lz_backend_mc_addr(short_id, sizeof short_id); return snprintf(buf, (size_t)n, - "mccomp: status v0 addr=%s nodes=%d threads=%d unread=%d public=%s dm=%s\n", - addr, mc_nodes, mc_threads, unread, + "mccomp: status v0 addr=%s short_id=%s nodes=%d threads=%d unread=%d public=%s dm=%s\n", + addr, short_id, mc_nodes, mc_threads, unread, lz_backend_mc_send_public ? "sendable" : "unavailable", - lz_backend_mc_dm ? "sendable" : "unavailable"); -} - -static bool mc_companion_dm_target(const lz_node_rt *n) -{ - if(!n || n->net != LZ_NET_MC) return false; - return strcmp(n->role, "Chat") == 0; + lz_backend_mc_dm_key ? "sendable" : "unavailable"); } int lz_svc_mc_companion_nodes(char *buf, int n) @@ -1322,9 +1466,14 @@ int lz_svc_mc_companion_nodes(char *buf, int n) if(nd->net != LZ_NET_MC) continue; char ago[12]; lz_fmt_ago(nd->last_heard, ago, sizeof ago); + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + bool has_addr = mc_node_addr(nd, addr, sizeof addr); + mc_node_short_id(nd, short_id, sizeof short_id); pos = buf_appendf(buf, n, pos, - "mccomp-node: name=\"%s\" id=%s role=%s snr=%.1f last=%s dm=%s\n", - nd->name, nd->id, nd->role, (double)nd->snr, ago, + "mccomp-node: name=\"%s\" addr=%s short_id=%s role=%s snr=%.1f last=%s public_key=%s dm=%s\n", + nd->name, has_addr ? addr : "-", short_id, nd->role, + (double)nd->snr, ago, + nd->has_key ? "present" : "missing", mc_companion_dm_target(nd) ? "yes" : "no"); listed++; } @@ -1365,9 +1514,10 @@ bool lz_svc_mc_companion_send_public(const char *text) bool lz_svc_mc_companion_send_dm(const char *name, const char *text) { if(!name || !name[0] || !text || !text[0]) return false; - lz_node_rt *n = lz_svc_node_by_name(name); - if(!mc_companion_dm_target(n)) return false; - return lz_backend_mc_dm && lz_backend_mc_dm(name, text); + bool ambiguous = false; + lz_node_rt *n = mc_node_by_name_unique(name, &ambiguous); + if(ambiguous) return false; + return mc_companion_send_dm_node(n, text); } static const char *mc0_skip_ws(const char *p) @@ -1500,57 +1650,84 @@ static int mc0_hello(char *buf, int n, const char *id) static int mc0_identity(char *buf, int n, const char *id) { - char addr[24]; - lz_backend_mc_addr(addr, sizeof addr); + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + bool have_addr = mc_local_addr(addr, sizeof addr); + if(!have_addr) lz_backend_mc_addr(addr, sizeof addr); + lz_backend_mc_addr(short_id, sizeof short_id); int pos = mc0_ok_prefix(buf, n, id); pos = buf_appendf(buf, n, pos, "enabled=%d name=", LZ_MESHCORE_ENABLED ? 1 : 0); pos = mc0_append_pct(buf, n, pos, g_id.long_name); return buf_appendf(buf, n, pos, - " addr=%s role=chat addr_format=meshcore-id advert_ready=1\n", - addr); + " addr=%s short_id=%s role=chat pubkey=%s addr_format=meshcore-pubkey-hex advert_ready=%d\n", + addr, short_id, have_addr ? addr : "-", + have_addr ? 1 : 0); } static int mc0_status(char *buf, int n, const char *id) { - char addr[24]; + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; int unread = 0; int nodes = mc0_count_nodes(); int threads = mc0_count_threads(&unread); - lz_backend_mc_addr(addr, sizeof addr); + if(!mc_local_addr(addr, sizeof addr)) lz_backend_mc_addr(addr, sizeof addr); + lz_backend_mc_addr(short_id, sizeof short_id); int pos = mc0_ok_prefix(buf, n, id); return buf_appendf(buf, n, pos, - "proto=0 mc=%s bridge=usb mc_companion=attached mt_companion=%s addr=%s nodes=%d threads=%d unread=%d public=%d dm=%d event_seq=0 nodes_rev=0 messages_rev=0\n", + "proto=0 mc=%s bridge=usb mc_companion=attached mt_companion=%s addr=%s short_id=%s nodes=%d threads=%d unread=%d public=%d dm=%d event_seq=0 nodes_rev=0 messages_rev=0\n", LZ_MESHCORE_ENABLED ? "on" : "disabled", lz_mtc_active() ? "on" : "off", - addr, nodes, threads, unread, + addr, short_id, nodes, threads, unread, lz_backend_mc_send_public ? 1 : 0, - lz_backend_mc_dm ? 1 : 0); + lz_backend_mc_dm_key ? 1 : 0); } -static int mc0_nodes(char *buf, int n, const char *id) +static int mc0_limit_arg(const char *args, int def, int max) +{ + char raw[12]; + int limit = def; + if(mc0_get_arg(args, "limit", raw, sizeof raw)) { + limit = atoi(raw); + if(limit <= 0) limit = def; + } + if(limit > max) limit = max; + return limit; +} + +static int mc0_nodes(char *buf, int n, const char *id, const char *args) { int count = mc0_count_nodes(); + int limit = mc0_limit_arg(args, 5, 5); + int listed = count < limit ? count : limit; + int more = count > listed ? 1 : 0; int pos = buf_appendf(buf, n, 0, - "MC0 %s BEGIN type=nodes rev=0 count=%d more=0 cursor=end\n", - id, count); + "MC0 %s BEGIN type=nodes rev=0 count=%d more=%d cursor=%s\n", + id, listed, more, more ? "next" : "end"); + int emitted = 0; for(int i = 0; i < g_node_count; i++) { const lz_node_rt *nd = &g_nodes[i]; if(nd->net != LZ_NET_MC) continue; + if(emitted >= listed) break; uint32_t seen_ms = 0; uint32_t now_s = now_epoch(); if(nd->last_heard && now_s >= nd->last_heard) seen_ms = (now_s - nd->last_heard) * 1000u; - pos = buf_appendf(buf, n, pos, "MC0 %s NODE addr=%s name=", id, nd->id); + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + bool has_addr = mc_node_addr(nd, addr, sizeof addr); + mc_node_short_id(nd, short_id, sizeof short_id); + pos = buf_appendf(buf, n, pos, "MC0 %s NODE addr=%s short_id=%s name=", + id, has_addr ? addr : "-", short_id); pos = mc0_append_pct(buf, n, pos, nd->name); pos = buf_appendf(buf, n, pos, " role="); pos = mc0_append_pct(buf, n, pos, nd->role); - pos = buf_appendf(buf, n, pos, " seen_ms=%lu snr=%.1f dm=%s\n", + pos = buf_appendf(buf, n, pos, " seen_ms=%lu snr=%.1f public_key=%s dm=%s\n", (unsigned long)seen_ms, (double)nd->snr, - mc_companion_dm_target(nd) ? "ready" : "not_messageable"); + nd->has_key ? "present" : "missing", + mc_companion_dm_state(nd)); + emitted++; } return buf_appendf(buf, n, pos, - "MC0 %s END type=nodes rev=0 count=%d more=0 cursor=end\n", - id, count); + "MC0 %s END type=nodes rev=0 count=%d more=%d cursor=%s\n", + id, emitted, more, more ? "next" : "end"); } static int mc0_threads(char *buf, int n, const char *id) @@ -1577,45 +1754,6 @@ static int mc0_threads(char *buf, int n, const char *id) id, count); } -static lz_node_rt *mc0_node_by_addr(const char *addr) -{ - if(!addr || !addr[0]) return NULL; - for(int i = 0; i < g_node_count; i++) - if(g_nodes[i].net == LZ_NET_MC && strcmp(g_nodes[i].id, addr) == 0) - return &g_nodes[i]; - return NULL; -} - -static char mc0_fold_char(char c) -{ - return (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; -} - -static bool mc0_name_eq(const char *a, const char *b) -{ - if(!a || !b) return false; - while(*a && *b) { - if(mc0_fold_char(*a++) != mc0_fold_char(*b++)) return false; - } - return *a == 0 && *b == 0; -} - -static lz_node_rt *mc0_node_by_name_unique(const char *name, bool *ambiguous) -{ - lz_node_rt *match = NULL; - if(ambiguous) *ambiguous = false; - if(!name || !name[0]) return NULL; - for(int i = 0; i < g_node_count; i++) { - if(g_nodes[i].net != LZ_NET_MC || !mc0_name_eq(g_nodes[i].name, name)) continue; - if(match) { - if(ambiguous) *ambiguous = true; - return match; - } - match = &g_nodes[i]; - } - return match; -} - static int mc0_send_public(char *buf, int n, const char *id, const char *args) { char text[LZ_TEXT_MAX + 1]; @@ -1631,7 +1769,7 @@ static int mc0_send_public(char *buf, int n, const char *id, const char *args) static int mc0_send_dm(char *buf, int n, const char *id, const char *args) { - char text[LZ_TEXT_MAX + 1], name[64], addr[32]; + char text[LZ_TEXT_MAX + 1], name[64], addr[LZ_MC_ADDR_HEX_CHARS + 1]; if(!mc0_get_arg(args, "text", text, sizeof text)) return mc0_err(buf, n, id, "bad_request", false, "missing text"); if((int)strlen(text) > LZ_TEXT_MAX) @@ -1642,22 +1780,36 @@ static int mc0_send_dm(char *buf, int n, const char *id, const char *args) lz_node_rt *target = NULL; bool ambiguous = false; if(have_addr) { - target = mc0_node_by_addr(addr); + bool bad_addr = false; + target = mc_node_by_addr(addr, &bad_addr); + if(bad_addr) return mc0_err(buf, n, id, "bad_addr", false, "to_addr must be 64 hex chars"); if(!target) return mc0_err(buf, n, id, "not_found", false, "node not found"); - } else if(have_name) { - target = mc0_node_by_name_unique(name, &ambiguous); + } + if(have_name) { + lz_node_rt *by_name = mc_node_by_name_unique(name, &ambiguous); if(ambiguous) return mc0_err(buf, n, id, "ambiguous_name", false, "name is ambiguous"); - } else { + if(!by_name) return mc0_err(buf, n, id, "not_found", false, "node not found"); + if(target && by_name != target) + return mc0_err(buf, n, id, "target_mismatch", false, "to_addr and to_name identify different nodes"); + target = by_name; + } + if(!have_addr && !have_name) { return mc0_err(buf, n, id, "bad_request", false, "missing to_name or to_addr"); } if(!target) return mc0_err(buf, n, id, "not_found", false, "node not found"); - if(!mc_companion_dm_target(target)) + const char *dm_state = mc_companion_dm_state(target); + if(strcmp(dm_state, "no_key") == 0) + return mc0_err(buf, n, id, "no_key", false, "node key is missing"); + if(strcmp(dm_state, "ready") != 0) return mc0_err(buf, n, id, "not_messageable", false, "node cannot receive DMs"); - if(!lz_svc_mc_companion_send_dm(target->name, text)) + if(!mc_companion_send_dm_node(target, text)) return mc0_err(buf, n, id, "send_failed", true, "DM send failed"); + char target_addr[LZ_MC_ADDR_HEX_CHARS + 1]; + mc_node_addr(target, target_addr, sizeof target_addr); int pos = mc0_ok_prefix(buf, n, id); - pos = buf_appendf(buf, n, pos, "accepted=1 kind=dm to_name="); + pos = buf_appendf(buf, n, pos, "accepted=1 kind=dm to_addr=%s to_name=", + target_addr); pos = mc0_append_pct(buf, n, pos, target->name); return buf_appendf(buf, n, pos, " status=queued\n"); } @@ -1679,7 +1831,7 @@ int lz_svc_mc_companion_handle_line(const char *line, char *buf, int n, bool *ex if(strcmp(verb, "HELLO") == 0) return mc0_hello(buf, n, id); if(strcmp(verb, "IDENTITY") == 0) return mc0_identity(buf, n, id); if(strcmp(verb, "STATUS") == 0) return mc0_status(buf, n, id); - if(strcmp(verb, "NODES") == 0) return mc0_nodes(buf, n, id); + if(strcmp(verb, "NODES") == 0) return mc0_nodes(buf, n, id, p); if(strcmp(verb, "THREADS") == 0) return mc0_threads(buf, n, id); if(strcmp(verb, "SEND_PUBLIC") == 0) return mc0_send_public(buf, n, id, p); if(strcmp(verb, "SEND_DM") == 0) return mc0_send_dm(buf, n, id, p); @@ -1782,7 +1934,8 @@ void lz_core_on_mc_node(const uint8_t *pubkey, const char *name, int adv_type, f uint32_t num = ((uint32_t)pubkey[0] << 24) | ((uint32_t)pubkey[1] << 16) | ((uint32_t)pubkey[2] << 8) | (uint32_t)pubkey[3]; char id[16]; - snprintf(id, sizeof id, "MC-%02x%02x", pubkey[0], pubkey[1]); + snprintf(id, sizeof id, "MC-%02x%02x%02x%02x", + pubkey[0], pubkey[1], pubkey[2], pubkey[3]); lz_node_rt *n = ensure_node(num, id, LZ_NET_MC); if(!n) return; n->net = LZ_NET_MC; From 8742c3169f58bda494a8fcf876ca195392698b32 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sat, 20 Jun 2026 14:13:44 -0400 Subject: [PATCH 2/2] Add MC0 live event streaming --- README.md | 13 +- docs/tdeck-feature-inventory.md | 2 +- docs/tdeck-firmware-roadmap.md | 24 +- docs/tdeck-meshcore-companion-protocol.md | 83 +++-- scripts/mc_companion_usb_smoke.py | 168 +++++++++- scripts/serial_harness.py | 5 + sim/main_sim.c | 78 ++++- src/mc_companion.cpp | 3 + src/services/mesh.h | 3 + src/services/mesh_core.c | 389 +++++++++++++++++++--- 10 files changed, 651 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index be5602e..d9a6a29 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,12 @@ iPhone-style dark look (status bar, battery glyph, grouped settings cards). enabling one frees the other. ### 🔭 Later -- **MeshCore companion bridge** - V0 protocol foundation is drafted, with - `companion mc ...` diagnostics and formal USB `MC0` mode for snapshots, - public send, private send, and exact public-key DM routing. BLE transport, - live events, and official MeshCore app compatibility remain planned until the - real MeshCore app protocol is confirmed. +- **MeshCore companion bridge** — V0 USB foundation is in progress: + `companion mc ...` covers console smoke, and `companion mc usb on` enters + formal MC0 mode for identity/status/node/thread snapshots, send boundaries, + revision counters, event controls, and bounded node/message/TX event draining. + BLE transport remains planned, and it is not official MeshCore app compatible + unless the real MeshCore app protocol is confirmed. - **Roll the iPhone look everywhere** — grouped cards / dividers across Messages, Nodes, Contacts. - **Local app platform** - scan local app manifests from `/sd/limitlezz/apps`, `/sd/apps`, `/appfs/apps`, and simulator data dirs, then show accepted apps @@ -185,7 +186,7 @@ iPhone-style dark look (status bar, battery glyph, grouped settings cards). - [`docs/tdeck-firmware-audit.md`](docs/tdeck-firmware-audit.md) - current firmware audit and risk list. - [`docs/tdeck-feature-inventory.md`](docs/tdeck-feature-inventory.md) - feature-by-feature implementation inventory. - [`docs/tdeck-firmware-roadmap.md`](docs/tdeck-firmware-roadmap.md) - roadmap to a complete T-Deck firmware. -- [`docs/tdeck-meshcore-companion-protocol.md`](docs/tdeck-meshcore-companion-protocol.md) - draft Phase 5/V0.8 MeshCore companion line protocol. +- [`docs/tdeck-meshcore-companion-protocol.md`](docs/tdeck-meshcore-companion-protocol.md) - Phase 5/V0.8 MeshCore companion V0 protocol. - [`docs/tdeck-hardware-dogfood-checklist.md`](docs/tdeck-hardware-dogfood-checklist.md) - stock-device hardware proof checklist. - [`docs/tdeck-release-checklist.md`](docs/tdeck-release-checklist.md) - slow-host Actions artifact and COM8 release evidence checklist. - [`docs/tdeck-troubleshooting.md`](docs/tdeck-troubleshooting.md) - build, flash, boot, radio, storage, Wi-Fi, and companion troubleshooting. diff --git a/docs/tdeck-feature-inventory.md b/docs/tdeck-feature-inventory.md index 1ae2005..29a3038 100644 --- a/docs/tdeck-feature-inventory.md +++ b/docs/tdeck-feature-inventory.md @@ -75,7 +75,7 @@ Status labels: | MeshCore self-advert TX | Partial, needs validation | Ed25519 identity, self-advert builder, serial/UI advert commands | Needs interop proof with real MeshCore nodes. | | MeshCore public channel / rooms | Planned | README says receive/default Public channel still ahead | V0.6: implement group text decode/send, room model, and split airtime config. | | MeshCore DMs | Planned | MeshCore contacts are non-messageable while gated | V0.7: implement key/session model, send path, ACKs, and UI routing. | -| MeshCore companion bridge | Partial, needs validation | `docs/tdeck-meshcore-companion-protocol.md` defines the V0 USB serial line protocol; `companion mc ...` and formal `MC0` mode provide snapshot, public-send, private-send, exact public-key DM routing, and self-test surfaces | V0.8: hardware-validate USB MC0, add live events, mirror to BLE later, and do not claim external MeshCore app compatibility until the real app protocol is confirmed. | +| MeshCore companion bridge | Partial/In progress | `docs/tdeck-meshcore-companion-protocol.md` defines the V0 USB serial line protocol; `companion mc ...` provides console smoke coverage, and `companion mc usb on` enters formal MC0 mode for identity/status/node/thread snapshots, send boundaries, event controls, revision counters, and bounded node/message/TX event drain | V0.8: hardware-validate USB MC0 with TX-event smoke, mirror to BLE later, and do not claim external MeshCore app compatibility until the real app protocol is confirmed. | ## User Interface diff --git a/docs/tdeck-firmware-roadmap.md b/docs/tdeck-firmware-roadmap.md index 0edd07d..bbde54f 100644 --- a/docs/tdeck-firmware-roadmap.md +++ b/docs/tdeck-firmware-roadmap.md @@ -47,7 +47,7 @@ These maintainer-provided beta labels are the canonical near-term sequence. The | V0.6 | MeshCore public chat and split airtime config | 🚧 Public chat send/receive hardware-verified; **split airtime may not be working — needs re-verification**; config UI still TODO | | V0.6 | MeshCore public chat and split airtime config | In progress - public chat send/receive hardware-verified; split-airtime serial dwell/switch smoke passed on COM8; packet-loss, latency, and real dual-network traffic soak still open | | V0.7 | MeshCore DMs and private chats | ✅ Encrypted DMs (X25519 ECDH + AES) send/receive hardware-verified against a real MeshCore peer | -| V0.8 | MeshCore USB companion and MeshCore BLE companion | 🚧 Protocol foundation drafted; USB/BLE implementation still planned and not external-app compatible yet | +| V0.8 | MeshCore USB companion and MeshCore BLE companion | In progress - USB MC0 mode now covers identity/status/node/thread snapshots, send boundaries, revision counters, event controls, and COM8 smoke hooks; BLE and external-app compatibility are still planned | | V0.9 | Code review, optimization, and emoji polish | ⬜ Not started | | V0.95 | Basic app SDK and infrastructure; Home UI supports adding apps and multiple home screens | 🚧 Local manifest scanner, Home paging, and detail shell started; runtime/catalog still TODO | | V0.96 | Upgraded Wi-Fi password storage | ✅ Implemented on T-Deck hardware: credentials use ESP32 NVS, legacy `wifi.cfg` migrates/removes, and diagnostics do not print passwords | @@ -189,21 +189,23 @@ Exit criteria: Goal: expose MeshCore companion functionality only after native MeshCore messaging is stable. -**Status:** protocol foundation in progress with an initial USB serial-console -smoke surface plus formal USB `MC0` mode. `companion mc -hello|status|nodes|threads|send|dm|test` and `MC0` -`HELLO|IDENTITY|STATUS|NODES|THREADS|SEND_PUBLIC|SEND_DM|EXIT` exercise the -firmware-owned MeshCore snapshots and send boundaries for COM8 validation. -`MC0` uses full public-key addresses for exact DM routing. BLE transport, live -events, and real external MeshCore app compatibility are still planned work. +**Status:** USB-first protocol foundation in progress. `companion mc +hello|status|nodes|threads|send|dm|test` still exercises the firmware-owned +MeshCore snapshots and send boundaries for COM8 validation, and formal +`companion mc usb on` MC0 mode now handles `HELLO`, `IDENTITY`, `STATUS`, +`NODES`, `THREADS`, `SEND_PUBLIC`, `SEND_DM`, `EVENTS`, and `EXIT` with +snapshot revisions plus bounded node/message/TX event draining. BLE transport +and real external MeshCore app compatibility are still planned work. Deliverables: - Define the MeshCore companion protocol surface for node DB, public chat, private chats, and send/receive forwarding. Drafted as `docs/tdeck-meshcore-companion-protocol.md`. - Add a USB serial-console smoke surface that reports MeshCore companion status, nodes, threads, Public send, DM send, and self-test. -- Implement MeshCore USB companion mode. USB `MC0` is implemented for - snapshots, public send, private send, exact full-public-key DM routing, and - explicit exit back to console; live event streaming is still a later step. +- Implement MeshCore USB companion mode. In progress: MC0 USB request/response + mode is implemented for identity, status, node/thread snapshots, send + boundaries, exact full-public-key DM routing, event controls, bounded event + draining, and reconnect/resync counters; BLE and external protocol mapping + remain. - Implement MeshCore BLE companion mode. - Add UI and serial commands that distinguish Meshtastic companion from MeshCore companion. - Decide whether one companion session or one network can own the external-app bridge at a time. diff --git a/docs/tdeck-meshcore-companion-protocol.md b/docs/tdeck-meshcore-companion-protocol.md index 800d9eb..6ffad72 100644 --- a/docs/tdeck-meshcore-companion-protocol.md +++ b/docs/tdeck-meshcore-companion-protocol.md @@ -1,10 +1,11 @@ # T-Deck MeshCore Companion V0 Protocol -Status: Phase 5 / V0.8 protocol foundation plus an initial firmware serial -smoke surface. The current firmware exposes `companion mc hello`, `status`, -`nodes`, `threads`, `send`, `dm`, and `test` for USB console validation; the -formal `MC0` request/response bridge, live events, BLE transport, and external -app compatibility are still planned. +Status: Phase 5 / V0.8 USB-first protocol foundation. The firmware exposes +`companion mc hello`, `status`, `nodes`, `threads`, `send`, `dm`, and `test` +for USB console validation, plus formal `companion mc usb on` MC0 mode for +`HELLO`, `IDENTITY`, `STATUS`, `NODES`, `THREADS`, `SEND_PUBLIC`, `SEND_DM`, +`EVENTS`, and `EXIT`. Snapshot revision counters and a bounded event drain are +implemented; BLE transport and external app compatibility are still planned. ## Goal @@ -49,8 +50,12 @@ the normal console prompt. The firmware enters MC0 mode with: ```text companion mc usb on MC0 1 HELLO proto=0 app=limitlezz-smoke host=windows want=none -MC0 2 STATUS -MC0 3 NODES since=0 limit=5 +MC0 2 IDENTITY +MC0 3 STATUS +MC0 4 NODES since=0 limit=5 +MC0 5 THREADS since=0 limit=5 +MC0 6 EVENTS mode=on types=nodes,messages,tx,status +MC0 7 EVENTS mode=off MC0 99 EXIT ``` @@ -111,7 +116,7 @@ MC0 1 HELLO proto=0 app=limitlezz-test host=windows want=events Response: ```text -MC0 1 OK proto=0 fw=0.8-draft device=tdeck session=84 caps=identity,nodes,status,send_public,send_dm,events max_line=512 max_text=180 event_seq=120 nodes_rev=42 messages_rev=77 +MC0 1 OK proto=0 fw=0.8-draft device=tdeck caps=identity,nodes,status,threads,send_public,send_dm,events,exit max_line=512 max_text=180 event_seq=120 nodes_rev=42 messages_rev=77 ``` Required fields: @@ -171,18 +176,19 @@ MC0 3 STATUS Response: ```text -MC0 3 OK mc=on bridge=usb mc_companion=idle mt_companion=off tdm=active airtime=balanced queue=0 event_seq=120 nodes_rev=42 messages_rev=77 +MC0 3 OK proto=0 mc=on bridge=usb mc_companion=attached mt_companion=off addr=6b1d000000000000000000000000000000000000000000000000000000000000 short_id=MC-6b1d0000 nodes=2 threads=2 unread=1 public=1 dm=1 events=off event_seq=120 nodes_rev=42 messages_rev=77 ``` Suggested fields: - `mc`: `on`, `off`, or `disabled`. - `bridge`: active transport, usually `usb` for V0. -- `mc_companion`: `idle`, `attached`, or `streaming`. +- `mc_companion`: `attached` or `streaming`. - `mt_companion`: Meshtastic companion state so clients can detect conflicts. -- `tdm`: `active`, `mc_only`, `mt_only`, or `idle`. -- `airtime`: current split-airtime preset. -- `queue`: queued outbound MeshCore sends owned by the firmware. +- `addr` and `short_id`: local MeshCore identity fields, matching `IDENTITY`. +- `nodes`, `threads`, and `unread`: current snapshot counts. +- `public` and `dm`: whether firmware-backed public and DM sends are available. +- `events`: `on` when typed event streaming is enabled. - `event_seq`, `nodes_rev`, `messages_rev`: resync counters. ### `NODES` @@ -199,8 +205,8 @@ Response: ```text MC0 4 BEGIN type=nodes rev=42 count=2 more=0 cursor=end -MC0 4 NODE addr=4f8e21a000000000000000000000000000000000000000000000000000000000 short_id=MC-4f8e21a0 name=Limitlezz role=chat seen_ms=12000 snr=-9 rssi=-112 public_key=present dm=ready -MC0 4 NODE addr=- short_id=MC-12ab9001 name=Hilltop role=router seen_ms=180000 snr=-14 rssi=-118 public_key=missing dm=not_messageable +MC0 4 NODE addr=4f8e21a000000000000000000000000000000000000000000000000000000000 short_id=MC-4f8e21a0 name=Limitlezz role=chat seen_ms=12000 snr=-9 public_key=present dm=ready +MC0 4 NODE addr=- short_id=MC-12ab9001 name=Hilltop role=router seen_ms=180000 snr=-14 public_key=missing dm=not_messageable MC0 4 END type=nodes rev=42 count=2 more=0 cursor=end ``` @@ -220,7 +226,7 @@ Node fields: - `name`: percent-encoded display name, if known. - `role`: `chat`, `router`, `repeater`, `sensor`, or `unknown`. - `seen_ms`: milliseconds since last heard, or `-1` if unknown. -- `snr`, `rssi`: last RF quality values, or omitted when unknown. +- `snr`: last RF quality value, or omitted when unknown. - `public_key`: `present`, `missing`, or `unknown`. - `dm`: `ready`, `no_key`, `not_messageable`, or `unknown`. @@ -241,14 +247,13 @@ MC0 5 SEND_PUBLIC room=public text=Hello%20mesh client_mid=pc-0001 Immediate response: ```text -MC0 5 OK accepted=1 msg_id=mc-804 queue=1 status=queued +MC0 5 OK accepted=1 kind=public status=queued event_seq=121 client_mid=pc-0001 ``` Later events: ```text -MC0 EVT 121 tx_status client_mid=pc-0001 msg_id=mc-804 kind=public status=sent -MC0 EVT 122 tx_status client_mid=pc-0001 msg_id=mc-804 kind=public status=delivered +MC0 EVT 121 tx_status kind=public status=queued room=public client_mid=pc-0001 ``` Request fields: @@ -280,18 +285,18 @@ MC0 7 SEND_DM to_name=Limitlezz text=Copy%20that client_mid=pc-0003 Immediate response: ```text -MC0 6 OK accepted=1 msg_id=mc-805 to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 status=queued +MC0 6 OK accepted=1 kind=dm to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 to_name=Limitlezz status=queued event_seq=122 client_mid=pc-0002 ``` Later event: ```text -MC0 EVT 123 tx_status client_mid=pc-0002 msg_id=mc-805 kind=dm to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 status=delivered +MC0 EVT 122 tx_status kind=dm status=queued to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 client_mid=pc-0002 ``` V0 name matching rules: -- Match against the firmware's known MeshCore display name and short name. +- Match against the firmware's known MeshCore display name. - Case-insensitive exact match only. - If zero nodes match, return `ERR code=not_found`. - If more than one node matches, return `ERR code=ambiguous_name`. @@ -316,22 +321,22 @@ MC0 8 EVENTS mode=on types=nodes,messages,tx,status Response: ```text -MC0 8 OK events=on types=nodes,messages,tx,status event_seq=123 +MC0 8 OK events=on types=nodes,messages,tx,status event_seq=123 nodes_rev=42 messages_rev=77 ``` Supported event types: ```text -MC0 EVT 124 node_upsert addr=4f8e21a000000000000000000000000000000000000000000000000000000000 nodes_rev=43 -MC0 EVT 125 snapshot_dirty type=nodes rev=43 reason=node_upsert -MC0 EVT 126 rx_public msg_id=mc-806 from_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 from_name=Limitlezz room=public text=Copy%20CH0. -MC0 EVT 127 rx_dm msg_id=mc-807 from_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 from_name=Limitlezz text=Direct%20copy. -MC0 EVT 128 tx_status client_mid=pc-0002 msg_id=mc-805 kind=dm status=failed reason=ack_timeout retry=1 -MC0 EVT 129 status mc=on tdm=active airtime=balanced queue=0 +MC0 EVT 124 node_upsert addr=4f8e21a000000000000000000000000000000000000000000000000000000000 short_id=MC-4f8e21a0 nodes_rev=43 name=Limitlezz role=chat public_key=present dm=ready +MC0 EVT 125 rx_public messages_rev=78 kind=public room=public from_name=Limitlezz text=Copy%20CH0. +MC0 EVT 126 rx_dm messages_rev=79 kind=dm from_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 from_name=Limitlezz text=Direct%20copy. +MC0 EVT 127 tx_status kind=dm status=queued to_addr=4f8e21a000000000000000000000000000000000000000000000000000000000 client_mid=pc-0002 ``` Snapshot events are hints. The host should use `STATUS`, `NODES`, or later message snapshot commands for authoritative state after reconnect. +The current V0 firmware accepts `status` in `types`, but it has no live status +event producer yet; poll `STATUS` for authoritative radio and bridge state. ## Error Semantics @@ -380,12 +385,14 @@ Rules: 2. Add an initial USB serial-console smoke surface for `hello`, `status`, `nodes`, `threads`, Public send, DM send, and self-test. 3. Add a USB-only MeshCore companion mode with `HELLO`, `IDENTITY`, `STATUS`, - and `NODES`. + and `NODES`. Implemented. 4. Add `SEND_PUBLIC` and `SEND_DM` using the existing firmware-owned MeshCore - send paths. + send paths. Implemented. 5. Add event streaming for receive, send-status, node-change, and status - changes. -6. Add snapshot revision counters and reconnect/resync behavior. + changes. In progress: event controls and a bounded event drain are + implemented for node, message, and TX events. +6. Add snapshot revision counters and reconnect/resync behavior. Implemented + for node/thread snapshots and `STATUS`/`HELLO` resync. 7. Mirror the same logical protocol over BLE only after USB behavior is stable. 8. Revisit external-app compatibility only after the real MeshCore app protocol is confirmed. @@ -397,9 +404,13 @@ Rules: `companion mc test`. - Formal USB MC0 smoke is opt-in: `python scripts/mc_companion_usb_smoke.py --mc0-usb` enters the configured - USB mode, sends `HELLO`, `IDENTITY`, `STATUS`, and `NODES`, asserts - `MC0 ... OK`, the public-key address format, `BEGIN`, and `END` response - markers, then exits through the configured `MC0 EXIT` line. + USB mode, sends `HELLO`, `IDENTITY`, `STATUS`, `NODES`, `THREADS`, and + `EVENTS` on/off, asserts `MC0 ... OK`, the public-key address format, + revision markers, `BEGIN`, and `END` response markers, then exits through + the configured `MC0 EXIT` line. +- Add `--mc0-tx-smoke` to the formal USB smoke when a hardware run should prove + live event draining. It sends a public `SEND_PUBLIC` with a known + `client_mid` and waits for a matching `MC0 EVT tx_status`. - If firmware lands different command names, use the smoke helper's `--mc0-enter-command`, `--mc0-*-template`, `--mc0-*-marker`, and `--mc0-exit-template` flags instead of editing firmware or weakening the diff --git a/scripts/mc_companion_usb_smoke.py b/scripts/mc_companion_usb_smoke.py index 764fb40..08098b7 100644 --- a/scripts/mc_companion_usb_smoke.py +++ b/scripts/mc_companion_usb_smoke.py @@ -36,11 +36,21 @@ DEFAULT_MC0_IDENTITY_ID = "2" DEFAULT_MC0_STATUS_ID = "3" DEFAULT_MC0_NODES_ID = "4" +DEFAULT_MC0_THREADS_ID = "5" +DEFAULT_MC0_EVENTS_ON_ID = "6" +DEFAULT_MC0_EVENTS_OFF_ID = "7" +DEFAULT_MC0_SEND_PUBLIC_ID = "8" DEFAULT_MC0_EXIT_ID = "99" DEFAULT_MC0_HELLO_TEMPLATE = "MC0 {id} HELLO proto=0 app=limitlezz-smoke host=windows want=none" DEFAULT_MC0_IDENTITY_TEMPLATE = "MC0 {id} IDENTITY" DEFAULT_MC0_STATUS_TEMPLATE = "MC0 {id} STATUS" DEFAULT_MC0_NODES_TEMPLATE = "MC0 {id} NODES since=0 limit=5" +DEFAULT_MC0_THREADS_TEMPLATE = "MC0 {id} THREADS since=0 limit=5" +DEFAULT_MC0_EVENTS_ON_TEMPLATE = "MC0 {id} EVENTS mode=on types=nodes,messages,tx,status" +DEFAULT_MC0_EVENTS_OFF_TEMPLATE = "MC0 {id} EVENTS mode=off" +DEFAULT_MC0_SEND_PUBLIC_TEMPLATE = ( + "MC0 {id} SEND_PUBLIC room=public text={text} client_mid={client_mid}" +) DEFAULT_MC0_EXIT_TEMPLATE = "MC0 {id} EXIT" DEFAULT_STATUS_MARKERS = ["mccomp: status", "MeshCore", "MC companion"] DEFAULT_TEST_MARKERS = ["PASS"] @@ -120,6 +130,19 @@ def format_mc0_template(option: str, template: str, request_id: str) -> str: return template.format(id=request_id) +def format_mc0_send_public_template(args: argparse.Namespace) -> str: + validate_template_fields( + "--mc0-send-public-template", + args.mc0_send_public_template, + {"id", "text", "client_mid"}, + ) + return args.mc0_send_public_template.format( + id=args.mc0_send_public_id, + text=args.mc0_send_public_text, + client_mid=args.mc0_send_public_client_mid, + ) + + def add_markers( specs: list[CommandSpec], command: str, @@ -191,13 +214,27 @@ def build_mc0_specs(args: argparse.Namespace) -> list[CommandSpec]: nodes_command = format_mc0_template( "--mc0-nodes-template", args.mc0_nodes_template, args.mc0_nodes_id ) - return [ + threads_command = format_mc0_template( + "--mc0-threads-template", args.mc0_threads_template, args.mc0_threads_id + ) + events_on_command = format_mc0_template( + "--mc0-events-on-template", args.mc0_events_on_template, args.mc0_events_on_id + ) + events_off_command = format_mc0_template( + "--mc0-events-off-template", args.mc0_events_off_template, args.mc0_events_off_id + ) + specs = [ CommandSpec( "mc0 hello", hello_command, default_marker_override( args.mc0_hello_marker, - [f"MC0 {args.mc0_hello_id} OK"], + [ + f"MC0 {args.mc0_hello_id} OK", + "event_seq=", + "nodes_rev=", + "messages_rev=", + ], ), ), CommandSpec( @@ -217,7 +254,7 @@ def build_mc0_specs(args: argparse.Namespace) -> list[CommandSpec]: status_command, default_marker_override( args.mc0_status_marker, - [f"MC0 {args.mc0_status_id} OK"], + [f"MC0 {args.mc0_status_id} OK", "event_seq=", "nodes_rev=", "messages_rev="], ), ), CommandSpec( @@ -228,7 +265,66 @@ def build_mc0_specs(args: argparse.Namespace) -> list[CommandSpec]: [f"MC0 {args.mc0_nodes_id} BEGIN", f"MC0 {args.mc0_nodes_id} END"], ), ), + CommandSpec( + "mc0 threads", + threads_command, + default_marker_override( + args.mc0_threads_marker, + [f"MC0 {args.mc0_threads_id} BEGIN", f"MC0 {args.mc0_threads_id} END"], + ), + ), + CommandSpec( + "mc0 events on", + events_on_command, + default_marker_override( + args.mc0_events_on_marker, + [ + f"MC0 {args.mc0_events_on_id} OK", + "events=on", + "types=", + "event_seq=", + "nodes_rev=", + "messages_rev=", + ], + ), + ), ] + if args.mc0_tx_smoke: + specs.append( + CommandSpec( + "mc0 send public tx event", + format_mc0_send_public_template(args), + default_marker_override( + args.mc0_send_public_marker, + [ + f"MC0 {args.mc0_send_public_id} OK", + "accepted=1", + "event_seq=", + f"client_mid={args.mc0_send_public_client_mid}", + "MC0 EVT", + "tx_status", + "kind=public", + ], + ), + ) + ) + specs.append( + CommandSpec( + "mc0 events off", + events_off_command, + default_marker_override( + args.mc0_events_off_marker, + [ + f"MC0 {args.mc0_events_off_id} OK", + "events=off", + "event_seq=", + "nodes_rev=", + "messages_rev=", + ], + ), + ), + ) + return specs def open_console(args: argparse.Namespace) -> tuple[serial.Serial, str]: @@ -569,6 +665,10 @@ def main() -> int: mc0.add_argument("--mc0-identity-id", default=DEFAULT_MC0_IDENTITY_ID) mc0.add_argument("--mc0-status-id", default=DEFAULT_MC0_STATUS_ID) mc0.add_argument("--mc0-nodes-id", default=DEFAULT_MC0_NODES_ID) + mc0.add_argument("--mc0-threads-id", default=DEFAULT_MC0_THREADS_ID) + mc0.add_argument("--mc0-events-on-id", default=DEFAULT_MC0_EVENTS_ON_ID) + mc0.add_argument("--mc0-events-off-id", default=DEFAULT_MC0_EVENTS_OFF_ID) + mc0.add_argument("--mc0-send-public-id", default=DEFAULT_MC0_SEND_PUBLIC_ID) mc0.add_argument("--mc0-exit-id", default=DEFAULT_MC0_EXIT_ID) mc0.add_argument( "--mc0-hello-template", @@ -590,10 +690,45 @@ def main() -> int: default=DEFAULT_MC0_NODES_TEMPLATE, help="MC0 NODES line template; supports {id}.", ) + mc0.add_argument( + "--mc0-threads-template", + default=DEFAULT_MC0_THREADS_TEMPLATE, + help="MC0 THREADS line template; supports {id}.", + ) + mc0.add_argument( + "--mc0-events-on-template", + default=DEFAULT_MC0_EVENTS_ON_TEMPLATE, + help="MC0 EVENTS-on line template; supports {id}.", + ) + mc0.add_argument( + "--mc0-events-off-template", + default=DEFAULT_MC0_EVENTS_OFF_TEMPLATE, + help="MC0 EVENTS-off line template; supports {id}.", + ) + mc0.add_argument( + "--mc0-tx-smoke", + action="store_true", + help="After EVENTS on, send a public MC0 message and require its tx_status event.", + ) + mc0.add_argument( + "--mc0-send-public-template", + default=DEFAULT_MC0_SEND_PUBLIC_TEMPLATE, + help="MC0 SEND_PUBLIC template; supports {id}, {text}, and {client_mid}.", + ) + mc0.add_argument( + "--mc0-send-public-text", + default="mc0%20usb%20smoke", + help="Percent-encoded text used by --mc0-tx-smoke.", + ) + mc0.add_argument( + "--mc0-send-public-client-mid", + default="mc0-smoke-tx", + help="client_mid used by --mc0-tx-smoke.", + ) mc0.add_argument( "--mc0-hello-marker", action="append", - help="Override HELLO expected marker(s). Defaults to 'MC0 OK'.", + help="Override HELLO expected marker(s). Defaults to OK plus revision fields.", ) mc0.add_argument( "--mc0-identity-marker", @@ -606,7 +741,7 @@ def main() -> int: mc0.add_argument( "--mc0-status-marker", action="append", - help="Override STATUS expected marker(s). Defaults to 'MC0 OK'.", + help="Override STATUS expected marker(s). Defaults to OK plus revision fields.", ) mc0.add_argument( "--mc0-nodes-marker", @@ -616,6 +751,29 @@ def main() -> int: "'MC0 BEGIN' and 'MC0 END'." ), ) + mc0.add_argument( + "--mc0-threads-marker", + action="append", + help=( + "Override THREADS expected marker(s). Defaults to both " + "'MC0 BEGIN' and 'MC0 END'." + ), + ) + mc0.add_argument( + "--mc0-events-on-marker", + action="append", + help="Override EVENTS-on expected marker(s). Defaults to OK plus events=on.", + ) + mc0.add_argument( + "--mc0-events-off-marker", + action="append", + help="Override EVENTS-off expected marker(s). Defaults to OK plus events=off.", + ) + mc0.add_argument( + "--mc0-send-public-marker", + action="append", + help="Override TX-smoke SEND_PUBLIC/event marker(s).", + ) mc0.add_argument( "--mc0-exit-template", default=DEFAULT_MC0_EXIT_TEMPLATE, diff --git a/scripts/serial_harness.py b/scripts/serial_harness.py index 1e2cba4..77a6bab 100644 --- a/scripts/serial_harness.py +++ b/scripts/serial_harness.py @@ -12,6 +12,11 @@ import sys import time +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="backslashreplace") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8", errors="backslashreplace") + try: import serial from serial.tools import list_ports diff --git a/sim/main_sim.c b/sim/main_sim.c index 34d94a4..55a51ae 100644 --- a/sim/main_sim.c +++ b/sim/main_sim.c @@ -2109,53 +2109,89 @@ static int codec_selftest(void) char mc0[900], proto[120]; bool mc0_exit = false; lz_svc_mc_companion_handle_line("MC0 1 HELLO proto=0 app=selftest", mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 1 OK proto=0") != NULL && strstr(mc0, "caps=") != NULL, + const char *event_seq_s = strstr(mc0, "event_seq="); + const char *nodes_rev_s = strstr(mc0, "nodes_rev="); + const char *messages_rev_s = strstr(mc0, "messages_rev="); + CHECK(strstr(mc0, "MC0 1 OK proto=0") != NULL && strstr(mc0, "caps=") != NULL && + event_seq_s != NULL && nodes_rev_s != NULL && messages_rev_s != NULL, "MeshCore MC0 HELLO reports protocol capabilities"); lz_svc_mc_companion_handle_line("MC0 2 STATUS", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "MC0 2 OK") != NULL && strstr(mc0, "nodes=1") != NULL && - strstr(mc0, "threads=2") != NULL, - "MeshCore MC0 STATUS counts snapshots"); + strstr(mc0, "threads=2") != NULL && strstr(mc0, "events=off") != NULL && + strstr(mc0, "event_seq=") != NULL && + strstr(mc0, "nodes_rev=") != NULL && + strstr(mc0, "messages_rev=") != NULL, + "MeshCore MC0 STATUS counts snapshots and event state"); lz_svc_mc_companion_handle_line("MC0 3 NODES since=0 limit=5", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "MC0 3 BEGIN type=nodes") != NULL && strstr(mc0, pub_addr) != NULL && strstr(mc0, "short_id=MC-42000000") != NULL && + strstr(mc0, "rev=") != NULL && strstr(mc0, "public_key=present") != NULL && strstr(mc0, "dm=ready") != NULL && strstr(mc0, "name=CompanionPeer") != NULL && strstr(mc0, "MC0 3 END type=nodes") != NULL, - "MeshCore MC0 NODES snapshot lists full-address peer"); - lz_svc_mc_companion_handle_line("MC0 4 THREADS", mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 4 BEGIN type=threads") != NULL && + "MeshCore MC0 NODES snapshot lists full-address peer with revision"); + lz_svc_mc_companion_handle_line("MC0 4 NODES since=999999 limit=5", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "count=0") != NULL && strstr(mc0, "rev=") != NULL, + "MeshCore MC0 NODES since current revision returns empty snapshot"); + lz_svc_mc_companion_handle_line("MC0 5 THREADS", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 5 BEGIN type=threads") != NULL && strstr(mc0, "text=CompanionPeer%3A%20public%20hello") != NULL && strstr(mc0, "text=dm%20hello") != NULL, "MeshCore MC0 THREADS snapshot lists encoded thread text"); - lz_svc_mc_companion_handle_line("MC0 5 SEND_PUBLIC text=mc0%20public", mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 5 OK accepted=1") != NULL, + lz_svc_mc_companion_handle_line("MC0 6 EVENTS mode=on types=nodes,messages,tx", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 6 OK events=on") != NULL && + strstr(mc0, "types=nodes,messages,tx") != NULL && + strstr(mc0, "event_seq=") != NULL && + strstr(mc0, "nodes_rev=") != NULL && + strstr(mc0, "messages_rev=") != NULL, + "MeshCore MC0 EVENTS enables typed streaming"); + lz_core_on_mc_channel_text("CompanionPeer", "event hello", -7.5f); + char ev[360]; + lz_svc_mc_companion_drain_events(ev, sizeof ev); + CHECK(strstr(ev, "MC0 EVT") != NULL && strstr(ev, "rx_public") != NULL && + strstr(ev, "event%20hello") != NULL, + "MeshCore MC0 event drain emits enabled message events"); + lz_svc_mc_companion_handle_line("MC0 7 SEND_PUBLIC text=mc0%20public client_mid=pc-1", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 7 OK accepted=1") != NULL && + strstr(mc0, "event_seq=") != NULL && + strstr(mc0, "client_mid=pc-1") != NULL, "MeshCore MC0 SEND_PUBLIC uses service boundary"); + lz_svc_mc_companion_drain_events(ev, sizeof ev); + CHECK(strstr(ev, "tx_status") != NULL && strstr(ev, "client_mid=pc-1") != NULL, + "MeshCore MC0 SEND_PUBLIC emits queued tx_status event"); sim_backend_mc_clear_last_dm(); - char mc0_cmd[180]; - snprintf(mc0_cmd, sizeof mc0_cmd, "MC0 6 SEND_DM to_addr=%s text=mc0%%20dm", pub_addr); + char mc0_cmd[220]; + snprintf(mc0_cmd, sizeof mc0_cmd, + "MC0 8 SEND_DM to_addr=%s text=mc0%%20dm client_mid=dm-1", + pub_addr); lz_svc_mc_companion_handle_line(mc0_cmd, mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 6 OK accepted=1") != NULL && - strstr(mc0, pub_addr) != NULL, + CHECK(strstr(mc0, "MC0 8 OK accepted=1") != NULL && + strstr(mc0, pub_addr) != NULL && + strstr(mc0, "client_mid=dm-1") != NULL, "MeshCore MC0 SEND_DM accepts exact public-key address"); CHECK(sim_backend_mc_last_dm(last_key, last_name, sizeof last_name, last_text, sizeof last_text) && memcmp(last_key, pub, 32) == 0 && strcmp(last_text, "mc0 dm") == 0, "MeshCore MC0 SEND_DM routes by exact public key"); - lz_svc_mc_companion_handle_line("MC0 7 SEND_DM to_name=companionpeer text=mc0%20dm%20name", mc0, sizeof mc0, &mc0_exit); - CHECK(strstr(mc0, "MC0 7 OK accepted=1") != NULL, + lz_svc_mc_companion_drain_events(ev, sizeof ev); + CHECK(strstr(ev, "tx_status") != NULL && strstr(ev, pub_addr) != NULL && + strstr(ev, "client_mid=dm-1") != NULL, + "MeshCore MC0 SEND_DM emits queued tx_status for exact address"); + lz_svc_mc_companion_handle_line("MC0 9 SEND_DM to_name=companionpeer text=mc0%20dm%20name", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 9 OK accepted=1") != NULL, "MeshCore MC0 SEND_DM keeps unambiguous name fallback"); lz_seed_node(0x00009999u, "MC-nokey", LZ_NET_MC, "NoKey", "NOK", "Chat", -5.0f, 90, "MeshCore", "1.0 km", 10, false); - lz_svc_mc_companion_handle_line("MC0 8 SEND_DM to_name=NoKey text=x", mc0, sizeof mc0, &mc0_exit); + lz_svc_mc_companion_handle_line("MC0 10 SEND_DM to_name=NoKey text=x", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "ERR code=no_key") != NULL, "MeshCore MC0 SEND_DM rejects chat nodes without a key"); uint8_t other[32] = {0}; other[0] = 0x44; other[3] = 0x01; lz_core_on_mc_node(other, "OtherPeer", 1, -7.5f); - lz_svc_mc_companion_handle_line("MC0 9 SEND_DM to_addr=4200000000000000000000000000000000000000000000000000000000000000 to_name=OtherPeer text=x", + lz_svc_mc_companion_handle_line("MC0 11 SEND_DM to_addr=4200000000000000000000000000000000000000000000000000000000000000 to_name=OtherPeer text=x", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "ERR code=target_mismatch") != NULL, "MeshCore MC0 SEND_DM rejects mismatched address/name targets"); @@ -2164,10 +2200,16 @@ static int codec_selftest(void) dup2[0] = 0x51; dup2[3] = 0x02; lz_core_on_mc_node(dup1, "DupePeer", 1, -7.5f); lz_core_on_mc_node(dup2, "DupePeer", 1, -7.5f); - lz_svc_mc_companion_handle_line("MC0 10 SEND_DM to_name=dupepeer text=x", mc0, sizeof mc0, &mc0_exit); + lz_svc_mc_companion_handle_line("MC0 12 SEND_DM to_name=dupepeer text=x", mc0, sizeof mc0, &mc0_exit); CHECK(strstr(mc0, "ERR code=ambiguous_name") != NULL, "MeshCore MC0 SEND_DM rejects duplicate display names"); - lz_svc_mc_companion_handle_line("MC0 11 EXIT", mc0, sizeof mc0, &mc0_exit); + lz_svc_mc_companion_handle_line("MC0 13 EVENTS mode=off", mc0, sizeof mc0, &mc0_exit); + CHECK(strstr(mc0, "MC0 13 OK events=off") != NULL && + strstr(mc0, "event_seq=") != NULL && + strstr(mc0, "nodes_rev=") != NULL && + strstr(mc0, "messages_rev=") != NULL, + "MeshCore MC0 EVENTS disables streaming"); + lz_svc_mc_companion_handle_line("MC0 14 EXIT", mc0, sizeof mc0, &mc0_exit); CHECK(mc0_exit && strstr(mc0, "state=detached") != NULL, "MeshCore MC0 EXIT returns to console"); lz_svc_mc_companion_selftest(proto, sizeof proto); diff --git a/src/mc_companion.cpp b/src/mc_companion.cpp index 14719b5..0c389f4 100644 --- a/src/mc_companion.cpp +++ b/src/mc_companion.cpp @@ -87,6 +87,9 @@ extern "C" void lz_mcc_usb_poll(void) g_mc_len = 0; } } + char events[700]; + if(lz_svc_mc_companion_drain_events(events, sizeof events) > 0) + Serial.print(events); } #endif /* LZ_TARGET_TDECK */ diff --git a/src/services/mesh.h b/src/services/mesh.h index c364d9f..059244a 100644 --- a/src/services/mesh.h +++ b/src/services/mesh.h @@ -180,6 +180,8 @@ typedef struct { char name[28]; lz_net_t net; uint32_t node_num; /* LZ_BROADCAST for the broadcast channel */ + uint8_t mc_pubkey[32]; /* MeshCore DM peer key when known */ + bool has_mc_pubkey; char last_text[72]; uint32_t last_ts; int unread; @@ -380,6 +382,7 @@ int lz_svc_mc_companion_threads(char *buf, int n); bool lz_svc_mc_companion_send_public(const char *text); bool lz_svc_mc_companion_send_dm(const char *name, const char *text); int lz_svc_mc_companion_handle_line(const char *line, char *buf, int n, bool *exit_mode); +int lz_svc_mc_companion_drain_events(char *buf, int n); int lz_svc_mc_companion_selftest(char *buf, int n); /* ---- radio stats (airtime accounting) ---- */ diff --git a/src/services/mesh_core.c b/src/services/mesh_core.c index 6952cc7..33e3a1b 100644 --- a/src/services/mesh_core.c +++ b/src/services/mesh_core.c @@ -80,6 +80,16 @@ static void (*g_dirty)(void); static uint32_t g_pkt_seq; static bool g_nodes_dirty; /* node DB has unsaved high-freq fields */ static uint32_t g_nodes_dirty_ms; /* tick when it first became dirty */ +static uint32_t g_mc_event_seq; /* MC0 companion resync/event anchor */ +static uint32_t g_mc_nodes_rev; /* MeshCore node snapshot revision */ +static uint32_t g_mc_messages_rev; /* MeshCore thread/message snapshot revision */ +static bool g_mc_events_enabled; +static char g_mc_event_types[40] = "none"; +#define MC0_EVENT_QUEUE 6 +#define MC0_EVENT_LINE 512 +static char g_mc_events[MC0_EVENT_QUEUE][MC0_EVENT_LINE]; +static uint8_t g_mc_event_head; +static uint8_t g_mc_event_count; /* placeholder until onboarding sets the real name (and MAC sets the num) */ static lz_identity_t g_id = { 0x7c3af1d0, "!7c3af1d0", "Node", "NODE" }; static bool g_have_identity; /* false until onboarding done */ @@ -100,6 +110,11 @@ static struct { } g_delivery[LZ_DELIVERY_PEND]; static lz_thread_rt *find_thread(uint32_t num); +static void mc0_note_node_change(const lz_node_rt *n); +static void mc0_note_message_change(const char *kind, const lz_thread_rt *t, + const char *text, bool self); +static void mc0_note_tx_status(const char *kind, const char *target, + const char *client_mid, const char *status); /* ---------- helpers ---------- */ @@ -908,6 +923,13 @@ static lz_thread_rt *find_thread_by_addr(const char *addr) return NULL; } +static void note_thread_mc_key(lz_thread_rt *t, const lz_node_rt *n) +{ + if(!t || !n || n->net != LZ_NET_MC || !n->has_key) return; + memcpy(t->mc_pubkey, n->pubkey, sizeof t->mc_pubkey); + t->has_mc_pubkey = true; +} + static void touch_thread_meta(lz_thread_rt *t, const char *text, uint32_t ts, bool inc_unread) { snprintf(t->last_text, sizeof t->last_text, "%s", text); @@ -918,7 +940,10 @@ static void touch_thread_meta(lz_thread_rt *t, const char *text, uint32_t ts, bo static lz_thread_rt *ensure_thread(lz_node_rt *n) { lz_thread_rt *t = find_thread(n->num); - if(t) return t; + if(t) { + note_thread_mc_key(t, n); + return t; + } if(g_thread_count >= LZ_MAX_THREADS) return NULL; /* full: do not clobber slot 0 */ t = &g_threads[g_thread_count++]; memset(t, 0, sizeof *t); @@ -926,6 +951,7 @@ static lz_thread_rt *ensure_thread(lz_node_rt *n) t->net = n->net; snprintf(t->addr, sizeof t->addr, "%s", n->id); snprintf(t->name, sizeof t->name, "%s", n->name); + note_thread_mc_key(t, n); t->messageable = lz_node_messageable(n); t->is_channel = false; snprintf(t->path, sizeof t->path, "direct"); @@ -1591,6 +1617,175 @@ static int mc0_append_pct(char *buf, int n, int pos, const char *s) return pos; } +static bool mc_thread_peer_addr(const lz_thread_rt *t, char *out, int cap) +{ + if(out && cap > 0) out[0] = 0; + if(!t || t->net != LZ_NET_MC || t->is_channel) return false; + if(t->has_mc_pubkey) { + mc_key_hex(t->mc_pubkey, out, cap); + return out && out[0] != 0; + } + lz_node_rt *peer = find_node(t->node_num); + return peer && mc_node_addr(peer, out, cap); +} + +static void mc0_event_finish(char *line, int n) +{ + if(!line || n <= 0) return; + int len = 0; + while(len < n && line[len]) len++; + if(len >= n - 1) { + static const char suffix[] = " truncated=1\n"; + int slen = (int)strlen(suffix); + int start = n - slen - 1; + if(start < 0) start = 0; + memcpy(line + start, suffix, (size_t)slen + 1); + return; + } + if(len == 0 || line[len - 1] != '\n') { + if(len >= n - 2) len = n - 2; + line[len++] = '\n'; + line[len] = 0; + } +} + +static bool mc0_event_type_match(const char *want, const char *token) +{ + size_t tl = strlen(token); + const char *p = want ? want : ""; + while(*p) { + while(*p == ',' || *p == ' ') p++; + const char *s = p; + while(*p && *p != ',') p++; + if((size_t)(p - s) == tl && strncmp(s, token, tl) == 0) return true; + if((size_t)(p - s) == 3 && strncmp(s, "all", 3) == 0) return true; + } + return false; +} + +static bool mc0_event_enabled_for(const char *bucket) +{ + return g_mc_events_enabled && mc0_event_type_match(g_mc_event_types, bucket); +} + +static void mc0_event_push(const char *line) +{ + if(!line || !line[0]) return; + uint8_t slot; + if(g_mc_event_count < MC0_EVENT_QUEUE) { + slot = (uint8_t)((g_mc_event_head + g_mc_event_count) % MC0_EVENT_QUEUE); + g_mc_event_count++; + } else { + slot = g_mc_event_head; + g_mc_event_head = (uint8_t)((g_mc_event_head + 1) % MC0_EVENT_QUEUE); + } + snprintf(g_mc_events[slot], sizeof g_mc_events[slot], "%s", line); +} + +static void mc0_note_node_change(const lz_node_rt *n) +{ + if(!n || n->net != LZ_NET_MC) return; + uint32_t seq = ++g_mc_event_seq; + g_mc_nodes_rev++; + if(!mc0_event_enabled_for("nodes")) return; + + char addr[LZ_MC_ADDR_HEX_CHARS + 1], short_id[24]; + bool has_addr = mc_node_addr(n, addr, sizeof addr); + mc_node_short_id(n, short_id, sizeof short_id); + char line[MC0_EVENT_LINE]; + int pos = snprintf(line, sizeof line, + "MC0 EVT %lu node_upsert addr=%s short_id=%s nodes_rev=%lu name=", + (unsigned long)seq, has_addr ? addr : "-", short_id, + (unsigned long)g_mc_nodes_rev); + pos = mc0_append_pct(line, sizeof line, pos, n->name); + pos = buf_appendf(line, sizeof line, pos, " role="); + pos = mc0_append_pct(line, sizeof line, pos, n->role); + pos = buf_appendf(line, sizeof line, pos, " public_key=%s dm=%s", + n->has_key ? "present" : "missing", + mc_companion_dm_state(n)); + mc0_event_finish(line, sizeof line); + mc0_event_push(line); +} + +static void mc0_note_message_change(const char *kind, const lz_thread_rt *t, + const char *text, bool self) +{ + if(!t || t->net != LZ_NET_MC) return; + uint32_t seq = ++g_mc_event_seq; + g_mc_messages_rev++; + if(!mc0_event_enabled_for(self ? "tx" : "messages")) return; + + char line[MC0_EVENT_LINE]; + int pos = snprintf(line, sizeof line, "MC0 EVT %lu %s messages_rev=%lu kind=%s ", + (unsigned long)seq, kind ? kind : "snapshot_dirty", + (unsigned long)g_mc_messages_rev, + t->is_channel ? "public" : "dm"); + if(self) { + if(t->is_channel) { + pos = buf_appendf(line, sizeof line, pos, "room=public "); + } else { + char peer_addr[LZ_MC_ADDR_HEX_CHARS + 1]; + if(mc_thread_peer_addr(t, peer_addr, sizeof peer_addr)) + pos = buf_appendf(line, sizeof line, pos, "to_addr=%s ", peer_addr); + } + pos = buf_appendf(line, sizeof line, pos, "status=sent text="); + } else { + const char *body = text; + char public_sender[28] = ""; + if(t->is_channel) { + const char *sep = text ? strstr(text, ": ") : NULL; + if(sep) { + size_t sl = (size_t)(sep - text); + if(sl >= sizeof public_sender) sl = sizeof public_sender - 1; + memcpy(public_sender, text, sl); + public_sender[sl] = 0; + body = sep + 2; + } + pos = buf_appendf(line, sizeof line, pos, "room=public from_name="); + pos = mc0_append_pct(line, sizeof line, pos, + public_sender[0] ? public_sender : "-"); + } else { + char peer_addr[LZ_MC_ADDR_HEX_CHARS + 1]; + if(mc_thread_peer_addr(t, peer_addr, sizeof peer_addr)) + pos = buf_appendf(line, sizeof line, pos, "from_addr=%s ", peer_addr); + pos = buf_appendf(line, sizeof line, pos, "from_name="); + pos = mc0_append_pct(line, sizeof line, pos, t->name); + } + pos = buf_appendf(line, sizeof line, pos, " text="); + text = body; + } + pos = mc0_append_pct(line, sizeof line, pos, text); + mc0_event_finish(line, sizeof line); + mc0_event_push(line); +} + +static void mc0_note_tx_status(const char *kind, const char *target, + const char *client_mid, const char *status) +{ + uint32_t seq = ++g_mc_event_seq; + if(!mc0_event_enabled_for("tx")) return; + + char line[MC0_EVENT_LINE]; + int pos = snprintf(line, sizeof line, "MC0 EVT %lu tx_status kind=%s status=%s", + (unsigned long)seq, kind ? kind : "unknown", + status ? status : "queued"); + if(target && target[0]) { + if(kind && strcmp(kind, "public") == 0) + pos = buf_appendf(line, sizeof line, pos, " room="); + else if(kind && strcmp(kind, "dm") == 0) + pos = buf_appendf(line, sizeof line, pos, " to_addr="); + else + pos = buf_appendf(line, sizeof line, pos, " target="); + pos = mc0_append_pct(line, sizeof line, pos, target); + } + if(client_mid && client_mid[0]) { + pos = buf_appendf(line, sizeof line, pos, " client_mid="); + pos = mc0_append_pct(line, sizeof line, pos, client_mid); + } + mc0_event_finish(line, sizeof line); + mc0_event_push(line); +} + static bool mc0_get_arg(const char *p, const char *key, char *out, int cap) { char tok[220]; @@ -1644,8 +1839,10 @@ static int mc0_hello(char *buf, int n, const char *id) { int pos = mc0_ok_prefix(buf, n, id); return buf_appendf(buf, n, pos, - "proto=0 fw=0.8-draft device=tdeck caps=identity,nodes,status,threads,send_public,send_dm,events,exit max_line=512 max_text=%d event_seq=0 nodes_rev=0 messages_rev=0\n", - LZ_TEXT_MAX); + "proto=0 fw=0.8-draft device=tdeck caps=identity,nodes,status,threads,send_public,send_dm,events,exit max_line=512 max_text=%d event_seq=%lu nodes_rev=%lu messages_rev=%lu\n", + LZ_TEXT_MAX, (unsigned long)g_mc_event_seq, + (unsigned long)g_mc_nodes_rev, + (unsigned long)g_mc_messages_rev); } static int mc0_identity(char *buf, int n, const char *id) @@ -1673,12 +1870,17 @@ static int mc0_status(char *buf, int n, const char *id) lz_backend_mc_addr(short_id, sizeof short_id); int pos = mc0_ok_prefix(buf, n, id); return buf_appendf(buf, n, pos, - "proto=0 mc=%s bridge=usb mc_companion=attached mt_companion=%s addr=%s short_id=%s nodes=%d threads=%d unread=%d public=%d dm=%d event_seq=0 nodes_rev=0 messages_rev=0\n", + "proto=0 mc=%s bridge=usb mc_companion=%s mt_companion=%s addr=%s short_id=%s nodes=%d threads=%d unread=%d public=%d dm=%d events=%s event_seq=%lu nodes_rev=%lu messages_rev=%lu\n", LZ_MESHCORE_ENABLED ? "on" : "disabled", + g_mc_events_enabled ? "streaming" : "attached", lz_mtc_active() ? "on" : "off", addr, short_id, nodes, threads, unread, lz_backend_mc_send_public ? 1 : 0, - lz_backend_mc_dm_key ? 1 : 0); + lz_backend_mc_dm_key ? 1 : 0, + g_mc_events_enabled ? "on" : "off", + (unsigned long)g_mc_event_seq, + (unsigned long)g_mc_nodes_rev, + (unsigned long)g_mc_messages_rev); } static int mc0_limit_arg(const char *args, int def, int max) @@ -1695,13 +1897,18 @@ static int mc0_limit_arg(const char *args, int def, int max) static int mc0_nodes(char *buf, int n, const char *id, const char *args) { - int count = mc0_count_nodes(); + int total = mc0_count_nodes(); + char since_s[16]; + uint32_t since = mc0_get_arg(args, "since", since_s, sizeof since_s) + ? (uint32_t)strtoul(since_s, NULL, 10) : 0; + if(since && since >= g_mc_nodes_rev) total = 0; int limit = mc0_limit_arg(args, 5, 5); - int listed = count < limit ? count : limit; - int more = count > listed ? 1 : 0; + int listed = total < limit ? total : limit; + int more = total > listed ? 1 : 0; int pos = buf_appendf(buf, n, 0, - "MC0 %s BEGIN type=nodes rev=0 count=%d more=%d cursor=%s\n", - id, listed, more, more ? "next" : "end"); + "MC0 %s BEGIN type=nodes rev=%lu count=%d more=%d cursor=%s\n", + id, (unsigned long)g_mc_nodes_rev, listed, + more, more ? "next" : "end"); int emitted = 0; for(int i = 0; i < g_node_count; i++) { const lz_node_rt *nd = &g_nodes[i]; @@ -1726,50 +1933,75 @@ static int mc0_nodes(char *buf, int n, const char *id, const char *args) emitted++; } return buf_appendf(buf, n, pos, - "MC0 %s END type=nodes rev=0 count=%d more=%d cursor=%s\n", - id, emitted, more, more ? "next" : "end"); + "MC0 %s END type=nodes rev=%lu count=%d more=%d cursor=%s\n", + id, (unsigned long)g_mc_nodes_rev, emitted, + more, more ? "next" : "end"); } -static int mc0_threads(char *buf, int n, const char *id) +static int mc0_threads(char *buf, int n, const char *id, const char *args) { int count = mc0_count_threads(NULL); + char since_s[16]; + uint32_t since = mc0_get_arg(args, "since", since_s, sizeof since_s) + ? (uint32_t)strtoul(since_s, NULL, 10) : 0; + if(since && since >= g_mc_messages_rev) count = 0; int pos = buf_appendf(buf, n, 0, - "MC0 %s BEGIN type=threads rev=0 count=%d more=0 cursor=end\n", - id, count); - for(int oi = 0; oi < g_thread_count; oi++) { - int idx = (oi < LZ_MAX_THREADS) ? g_order[oi] : -1; - if(idx < 0 || idx >= g_thread_count) continue; - const lz_thread_rt *t = &g_threads[idx]; - if(t->net != LZ_NET_MC) continue; - pos = buf_appendf(buf, n, pos, "MC0 %s THREAD addr=%s name=", id, t->addr); - pos = mc0_append_pct(buf, n, pos, t->name); - pos = buf_appendf(buf, n, pos, " kind=%s unread=%d last=%lu text=", - t->is_channel ? "public" : "dm", t->unread, - (unsigned long)t->last_ts); - pos = mc0_append_pct(buf, n, pos, t->last_text); - pos = buf_appendf(buf, n, pos, "\n"); + "MC0 %s BEGIN type=threads rev=%lu count=%d more=0 cursor=end\n", + id, (unsigned long)g_mc_messages_rev, count); + if(count) { + for(int oi = 0; oi < g_thread_count; oi++) { + int idx = (oi < LZ_MAX_THREADS) ? g_order[oi] : -1; + if(idx < 0 || idx >= g_thread_count) continue; + const lz_thread_rt *t = &g_threads[idx]; + if(t->net != LZ_NET_MC) continue; + const char *thread_addr = t->addr; + char peer_addr[LZ_MC_ADDR_HEX_CHARS + 1]; + if(!t->is_channel) { + thread_addr = mc_thread_peer_addr(t, peer_addr, sizeof peer_addr) + ? peer_addr : "-"; + } + pos = buf_appendf(buf, n, pos, "MC0 %s THREAD addr=%s name=", id, thread_addr); + pos = mc0_append_pct(buf, n, pos, t->name); + if(!t->is_channel) { + pos = buf_appendf(buf, n, pos, " short_id="); + pos = mc0_append_pct(buf, n, pos, t->addr); + } + pos = buf_appendf(buf, n, pos, " kind=%s unread=%d last=%lu text=", + t->is_channel ? "public" : "dm", t->unread, + (unsigned long)t->last_ts); + pos = mc0_append_pct(buf, n, pos, t->last_text); + pos = buf_appendf(buf, n, pos, "\n"); + } } return buf_appendf(buf, n, pos, - "MC0 %s END type=threads rev=0 count=%d more=0 cursor=end\n", - id, count); + "MC0 %s END type=threads rev=%lu count=%d more=0 cursor=end\n", + id, (unsigned long)g_mc_messages_rev, count); } static int mc0_send_public(char *buf, int n, const char *id, const char *args) { - char text[LZ_TEXT_MAX + 1]; + char text[LZ_TEXT_MAX + 1], client_mid[40]; if(!mc0_get_arg(args, "text", text, sizeof text)) return mc0_err(buf, n, id, "bad_request", false, "missing text"); if((int)strlen(text) > LZ_TEXT_MAX) return mc0_err(buf, n, id, "text_too_long", false, "text too long"); if(!lz_svc_mc_companion_send_public(text)) return mc0_err(buf, n, id, "send_failed", true, "public send failed"); + mc0_get_arg(args, "client_mid", client_mid, sizeof client_mid); + mc0_note_tx_status("public", "public", client_mid, "queued"); int pos = mc0_ok_prefix(buf, n, id); - return buf_appendf(buf, n, pos, "accepted=1 kind=public status=queued\n"); + pos = buf_appendf(buf, n, pos, "accepted=1 kind=public status=queued event_seq=%lu", + (unsigned long)g_mc_event_seq); + if(client_mid[0]) { + pos = buf_appendf(buf, n, pos, " client_mid="); + pos = mc0_append_pct(buf, n, pos, client_mid); + } + return buf_appendf(buf, n, pos, "\n"); } static int mc0_send_dm(char *buf, int n, const char *id, const char *args) { - char text[LZ_TEXT_MAX + 1], name[64], addr[LZ_MC_ADDR_HEX_CHARS + 1]; + char text[LZ_TEXT_MAX + 1], name[64], addr[LZ_MC_ADDR_HEX_CHARS + 1], client_mid[40]; if(!mc0_get_arg(args, "text", text, sizeof text)) return mc0_err(buf, n, id, "bad_request", false, "missing text"); if((int)strlen(text) > LZ_TEXT_MAX) @@ -1807,11 +2039,53 @@ static int mc0_send_dm(char *buf, int n, const char *id, const char *args) char target_addr[LZ_MC_ADDR_HEX_CHARS + 1]; mc_node_addr(target, target_addr, sizeof target_addr); + mc0_get_arg(args, "client_mid", client_mid, sizeof client_mid); + mc0_note_tx_status("dm", target_addr, client_mid, "queued"); int pos = mc0_ok_prefix(buf, n, id); pos = buf_appendf(buf, n, pos, "accepted=1 kind=dm to_addr=%s to_name=", target_addr); pos = mc0_append_pct(buf, n, pos, target->name); - return buf_appendf(buf, n, pos, " status=queued\n"); + pos = buf_appendf(buf, n, pos, " status=queued event_seq=%lu", (unsigned long)g_mc_event_seq); + if(client_mid[0]) { + pos = buf_appendf(buf, n, pos, " client_mid="); + pos = mc0_append_pct(buf, n, pos, client_mid); + } + return buf_appendf(buf, n, pos, "\n"); +} + +static int mc0_events(char *buf, int n, const char *id, const char *args) +{ + char mode[12], types[40]; + if(!mc0_get_arg(args, "mode", mode, sizeof mode)) { + int pos = mc0_ok_prefix(buf, n, id); + return buf_appendf(buf, n, pos, + "events=%s types=%s event_seq=%lu nodes_rev=%lu messages_rev=%lu\n", + g_mc_events_enabled ? "on" : "off", g_mc_event_types, + (unsigned long)g_mc_event_seq, + (unsigned long)g_mc_nodes_rev, + (unsigned long)g_mc_messages_rev); + } + if(strcmp(mode, "on") == 0) { + g_mc_events_enabled = true; + if(mc0_get_arg(args, "types", types, sizeof types)) + snprintf(g_mc_event_types, sizeof g_mc_event_types, "%s", types); + else + snprintf(g_mc_event_types, sizeof g_mc_event_types, "nodes,messages,tx,status"); + } else if(strcmp(mode, "off") == 0) { + g_mc_events_enabled = false; + snprintf(g_mc_event_types, sizeof g_mc_event_types, "none"); + g_mc_event_head = 0; + g_mc_event_count = 0; + } else { + return mc0_err(buf, n, id, "bad_request", false, "bad events mode"); + } + int pos = mc0_ok_prefix(buf, n, id); + return buf_appendf(buf, n, pos, + "events=%s types=%s event_seq=%lu nodes_rev=%lu messages_rev=%lu\n", + g_mc_events_enabled ? "on" : "off", g_mc_event_types, + (unsigned long)g_mc_event_seq, + (unsigned long)g_mc_nodes_rev, + (unsigned long)g_mc_messages_rev); } int lz_svc_mc_companion_handle_line(const char *line, char *buf, int n, bool *exit_mode) @@ -1832,13 +2106,10 @@ int lz_svc_mc_companion_handle_line(const char *line, char *buf, int n, bool *ex if(strcmp(verb, "IDENTITY") == 0) return mc0_identity(buf, n, id); if(strcmp(verb, "STATUS") == 0) return mc0_status(buf, n, id); if(strcmp(verb, "NODES") == 0) return mc0_nodes(buf, n, id, p); - if(strcmp(verb, "THREADS") == 0) return mc0_threads(buf, n, id); + if(strcmp(verb, "THREADS") == 0) return mc0_threads(buf, n, id, p); if(strcmp(verb, "SEND_PUBLIC") == 0) return mc0_send_public(buf, n, id, p); if(strcmp(verb, "SEND_DM") == 0) return mc0_send_dm(buf, n, id, p); - if(strcmp(verb, "EVENTS") == 0) { - int pos = mc0_ok_prefix(buf, n, id); - return buf_appendf(buf, n, pos, "events=off types=none event_seq=0\n"); - } + if(strcmp(verb, "EVENTS") == 0) return mc0_events(buf, n, id, p); if(strcmp(verb, "EXIT") == 0) { if(exit_mode) *exit_mode = true; int pos = mc0_ok_prefix(buf, n, id); @@ -1847,6 +2118,22 @@ int lz_svc_mc_companion_handle_line(const char *line, char *buf, int n, bool *ex return mc0_err(buf, n, id, "unknown_command", false, "unknown MC0 command"); } +int lz_svc_mc_companion_drain_events(char *buf, int n) +{ + int pos = 0; + if(!buf || n <= 0) return 0; + buf[0] = 0; + while(g_mc_event_count && pos + 1 < n) { + const char *line = g_mc_events[g_mc_event_head]; + int len = (int)strlen(line); + if(pos && pos + len >= n) break; + pos = buf_appendf(buf, n, pos, "%s", line); + g_mc_event_head = (uint8_t)((g_mc_event_head + 1) % MC0_EVENT_QUEUE); + g_mc_event_count--; + } + return pos; +} + int lz_svc_mc_companion_selftest(char *buf, int n) { char out[900]; @@ -1859,7 +2146,11 @@ int lz_svc_mc_companion_selftest(char *buf, int n) lz_svc_mc_companion_handle_line("MC0 3 NODES", out, sizeof out, &exit_mode); ok = ok && strstr(out, "MC0 3 BEGIN type=nodes") != NULL && strstr(out, "MC0 3 END type=nodes") != NULL; - lz_svc_mc_companion_handle_line("MC0 4 EXIT", out, sizeof out, &exit_mode); + lz_svc_mc_companion_handle_line("MC0 4 EVENTS mode=on types=nodes,messages,tx", out, sizeof out, &exit_mode); + ok = ok && strstr(out, "events=on") != NULL && strstr(out, "types=nodes,messages,tx") != NULL; + lz_svc_mc_companion_handle_line("MC0 5 EVENTS mode=off", out, sizeof out, &exit_mode); + ok = ok && strstr(out, "events=off") != NULL; + lz_svc_mc_companion_handle_line("MC0 6 EXIT", out, sizeof out, &exit_mode); ok = ok && exit_mode && strstr(out, "state=detached") != NULL; return snprintf(buf, (size_t)n, "MeshCore MC0 protocol selftest: %s", ok ? "PASS" : "FAIL"); } @@ -1923,7 +2214,9 @@ void lz_core_on_nodeinfo(uint32_t from, const char *id, const char *long_name, if(role >= 0) snprintf(n->role, sizeof n->role, "%s", lz_svc_mt_role_label(role)); if(!isnan(snr)) n->snr = snr; n->last_heard = now_epoch(); + note_thread_mc_key(find_thread(n->num), n); lz_store_save_nodes(g_nodes, g_node_count); + mc0_note_node_change(n); mark_dirty(); } @@ -1949,6 +2242,7 @@ void lz_core_on_mc_node(const uint8_t *pubkey, const char *name, int adv_type, f if(!isnan(snr)) n->snr = snr; n->last_heard = now_epoch(); lz_store_save_nodes(g_nodes, g_node_count); + mc0_note_node_change(n); mark_dirty(); } @@ -1964,6 +2258,10 @@ static void mc_thread_append(lz_thread_rt *t, bool self, const char *text) touch_thread_meta(t, text, ts, !self && g_open != t); /* unread only for inbound */ reorder_threads(); lz_store_save_threads(g_threads, g_thread_count); + if(t->net == LZ_NET_MC) { + const char *kind = self ? "tx_message" : (t->is_channel ? "rx_public" : "rx_dm"); + mc0_note_message_change(kind, t, text, self); + } mark_dirty(); } @@ -1977,7 +2275,11 @@ static lz_thread_rt *mc_dm_thread(const uint8_t *pubkey, const char *name, float n->net = LZ_NET_MC; if(name && name[0]) snprintf(n->name, sizeof n->name, "%s", name); lz_thread_rt *t = ensure_thread(n); - if(t) { t->net = LZ_NET_MC; t->messageable = true; } /* a chat peer we can DM back */ + if(t) { + t->net = LZ_NET_MC; + t->messageable = true; /* a chat peer we can DM back */ + note_thread_mc_key(t, n); + } return t; } @@ -2289,6 +2591,13 @@ void lz_svc_init(const char *datadir, bool seed_demo) if(LZ_MESHCORE_ENABLED) lz_svc_mc_channel_thread(); /* MeshCore Public always in Channels too */ track_stored_delivery(); reorder_threads(); + g_mc_event_seq = 0; + g_mc_nodes_rev = 0; + g_mc_messages_rev = 0; + g_mc_events_enabled = false; + snprintf(g_mc_event_types, sizeof g_mc_event_types, "none"); + g_mc_event_head = 0; + g_mc_event_count = 0; lz_backend_init(); }