From 58755eb715f639ccc2a799d6ea380b3eb667d960 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sat, 20 Jun 2026 13:07:39 -0400 Subject: [PATCH] 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;