Files changed: engine/lint-report.md vault/.obsidian/workspace.json vault/projects/meshai.md vault/projects/meshcore-transport.md
15 KiB
| title | type | tags | aliases | related | updated | status | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MeshCore transport for meshai (dual Meshtastic + MeshCore) | project |
|
|
|
2026-07-02 | proposed |
MeshCore transport for meshai (dual Meshtastic + MeshCore)
Design for adding MeshCore as a second mesh transport to the meshai LLM mesh assistant (meshai), running alongside Meshtastic.
Status
Proposed / design — not yet built. Decisions below are agreed with Matt; three prerequisites remain open (hardware, dependency, branch timing — see Open items).
Goal
Let meshai speak both Meshtastic and MeshCore, config-selectable as either or both at once. Target: full two-way parity (outbound announcements AND inbound interactive LLM replies) on both networks simultaneously.
Key decisions (agreed)
- Uniform messages across both networks. Every outbound message is formatted ONCE, sized to fit the SMALLER radio's budget (MeshCore, ~120 chars), and sent identically to both meshes. No per-network rendering, no divergent content. This is deliberately chosen over per-transport rendering (which was judged fragile: it would make every message formatter budget-polymorphic, double the test surface, run composition per-transport, and produce divergent content).
- Run mode: both simultaneously — send every announcement to Meshtastic AND MeshCore. "Both" = the existing Meshtastic path (meshai over IP to MeshMonitor's vnode fronting the radio) + a MeshCore Companion node reachable over IP/TCP, paralleling that same all-over-IP model. The MeshCore radio need NOT be on the meshai host — it just needs to be network-reachable.
- The transport is the only thing that knows its size and the only place that fragments a message (single source of truth for size =
transport.max_chars; kill the 4 hard-coded 200s + the stray [:195]). Because messages are pre-fit to the smaller budget, both radios normally send a single packet; the transport's (k/N) splitter is a safety net. - Network awareness is per-network, not uniform. Outbound announcements (weather/fire/511/alerts) are outside-world info → uniform to both. But network-status answers from the LLM ("who's on the mesh / mesh health") are inside-the-network info and must be scoped to whichever network the question arrived on. A MeshCore user gets MeshCore answers; a Meshtastic user gets the existing MeshView/MeshMonitor-based answers.
Character limits
- Meshtastic text payload: ~200–237 bytes (meshai currently budgets 200).
- MeshCore: hard cap
MAX_TEXT_LEN = 160 bytes; usable channel-message text is ~120–150 chars (channel messages embed<sender name>:+ timestamp/flags), docs round to "~133". No auto-chunking in the companion chat path — over-length text is hard-truncated, so meshai must split itself. - Practical uniform budget when both are active: ~120 chars.
Companion vs client (how we connect to MeshCore)
MeshCore firmware is role-specific (flashed, not runtime): Companion, Repeater, Room Server. The Companion node is the one a computer attaches to and drives — analogous to Meshtastic's phone+node model. meshai attaches to a Companion-firmware radio via the official meshcore Python lib (asyncio). TCP to a Companion node is the natural fit here — either native-TCP MeshCore firmware or a ser2net/serial-to-IP bridge — matching meshai's all-over-IP deployment (the Meshtastic side already runs over IP to MeshMonitor's vnode). A USB-on-host radio is not how this deployment works. Avoid BLE on Linux. Broadcast to a channel with send_chan_msg(index, text); channel 0 = "Public" (well-known PSK) = the broadcast primitive. Do NOT attach to a Repeater or Room Server for messaging.
Connecting to MeshCore via pyMC (companion TCP frame server)
- meshai's MeshCore link will be a TCP connection to the pyMC repeater's companion frame server, not a directly-attached radio. "pyMC" = pyMC_core (Python MeshCore library) + pyMC_Repeater (the repeater daemon, also branded "openHop"), which runs on aida-nebra as
pymc-repeater.service(node EchoBase, RAK4631). pyMC_core includes aCompanionFrameServerthat exposes the MeshCore Companion Protocol frames over a TCP socket — so a companion client connects over TCP as if to a radio. - Wire compatibility is confirmed: the
meshcorePython lib's TCP transport (tcp_cx.py) is byte-for-byte compatible with pyMC's frame server — outbound>(0x3E)+LE16+data, inbound<(0x3C)+LE16+data. So meshai connects withMeshCore.create_tcp(host, port); no framing shim needed. - Target: meshai connects to the companion identity named AIDA on the pyMC repeater — aida-nebra at 100.64.0.9 over Tailscale, port 5050 (the AIDA companion on aida-nebra; pyMC's out-of-box default is 5000). The repeater's radio node is EchoBase; AIDA is the companion frame-server identity meshai attaches to (set under
identities.companions[]in the repeater's config). The frame server is confirmed enabled. No authentication on the frame server. Endpoint:100.64.0.9:5050. - ⚠️ Single-client gotcha (important): pyMC's companion frame server is single-client — a new connection evicts the existing one. So meshai and MeshMonitor (which we also want on MeshCore for network awareness) cannot share one companion frame server — they'd kick each other off. Resolution: pyMC's config supports multiple companion identities (
identities.companions[]is a list), each on its owntcp_port— run a dedicated companion identity/port for meshai and a separate one for MeshMonitor. (This mirrors the Meshtastic side, where meshai and MeshMonitor coexist via MeshMonitor's vnode.) meshai's companion endpoint is the AIDA identity at100.64.0.9:5050— if MeshMonitor also needs MeshCore awareness it must use a separate companion identity/port so the two don't evict each other.
Network awareness: MeshCore vs Meshtastic
- Meshtastic (today): rich, global, passive picture via MeshView + MeshMonitor (fed by Meshtastic's MQTT firehose).
- MeshCore: no MQTT firehose and no passive "every node ever heard" nodeDB — by design (privacy/routing model). Awareness is LOCAL/contact-scoped and mostly PULL-based.
- Convenient fit: MeshMonitor (the same tool meshai already uses for Meshtastic) supports MeshCore as a first-class source since v4.5+, over USB/TCP, with a REST API (
/api/nodes). So MeshCore awareness reuses the existing MeshMonitor pattern. MeshMonitor is already central to meshai's mesh connectivity — meshai's Meshtastic link itself runs over IP through MeshMonitor's vnode — which makes reusing MeshMonitor for MeshCore awareness an especially natural fit. - What we CAN give a MeshCore user's LLM query: contact roster (name, node type, last-advert, position if shared, known path/hops via
get_contacts()), per-message SNR/RSSI, own device telemetry, on-demand telemetry from other nodes (req_telemetry/req_status, Cayenne LPP), trace/path discovery. Repeater stats (uptime/airtime/neighbors) only if the operator enabled guest access. - What's missing vs Meshtastic: no global/passive view, no firehose, advert SNR/position not inline, companion has no neighbor table (only Repeaters do).
- Verdict: less than Meshtastic's effortless global view, but a real local picture — plan MeshCore awareness around active polling of a curated contact/repeater set, not passive ingestion.
Architecture & code seams (current meshai)
- Today: one concrete
MeshConnector(meshai/connector.py) hard-wired to themeshtasticpackage (serial/TCP; no BLE), imported directly bymain.py,router.py,responder.py,channels.py, and the pipeline. No transport interface. - Send path:
MeshConnector.send_message()→interface.sendText(...); broadcast =BROADCAST_NUMon a channel index. - Inbound: meshtastic pubsub →
_on_receivebuilds aMeshMessage(carries a meshtastic-specificpacketdict) →set_message_callback→MeshAIBot._on_message→MessageRouter. - Size assumptions duplicated in 4 places:
chunker.py(max_chars=200),MeshRenderer+ its(k/N)splitter (renderers/mesh.py), digest accumulator (pipeline/digest.py), and the newersingle_packet_max_chars(adapter_config/defaults.py, used bycentral/nws_handler.py); plus a stray[:195]innotifications/router.py. - Config: dataclasses in
meshai/config.py(ConnectionConfig), loaded frommeshtastic.yamlviaconfig_loader.py. Exactly one connector is built inmain.py.
meshai config: the MeshCore menu
- There is no MeshCore code in meshai yet (greenfield; nothing to reconcile).
- meshai config is two-layer: (1) file —
meshai/config.pyConnectionConfigdataclass (typeserial|tcp,tcp_host,tcp_port), loaded frommeshtastic.yamlviaconfig_loader.py; (2) live dashboard — a React Config page (dashboard-frontend/src/pages/Config.tsx) with a "Connection" section, saved at runtime viaPUT /config/{section}(dashboard/api/config_routes.py), with a restart banner when needed. - The MeshCore menu = a new sub-form in the dashboard's Connection section (plus a matching config block). Fields it needs:
- transport enable / select (meshtastic | meshcore | both)
connection_type: tcp(the pyMC path is TCP; BLE/serial exist in the lib but aren't used here)host(aida-nebra,100.64.0.9),port(5050— the AIDA companion on aida-nebra; pyMC's out-of-box default is 5000)channel_index(index into the companion's channel table — required to send/receive on a public channel)channel_name(optional label)channel_psk/ secret — store incredentials, NOT in this doc or any graph-tracked file; only needed if meshai must provision the channel rather than relying on one pre-configured on the repeatermax_chars(MeshCore per-chunk cap — est. ~130–150; verify against companion max)auto_reconnect+max_reconnect_attempts(passed tocreate_tcp)- optional self/node name for the companion identity
Implementation plan (phased)
- Phase 1 — Transport abstraction (pure refactor, testable without new hardware). Extract
MeshTransportABC (connect/disconnect/send_message/set_message_callback/my_node_id/connected/get_node_name/get_node_position). RenameMeshConnector→MeshtasticTransport(behavior identical; assert byte-identical output in tests). GeneralizeMeshMessage(drop meshtastic-onlypacket; add atransporttag + generic reply-address token). Config-driven transport factory. Existing tests stay green. - Phase 2 — MeshCoreTransport (in + out). Wrap the
meshcorelib behind the same sync surface via a dedicated event-loop thread. Outboundsend_chan_msgfor broadcast, DM-by-pubkey for replies. Inbound: MeshCore channel/contact events → normalizedMeshMessage→ same callback. Contact/pubkey handling from adverts;my_node_idequivalent = our node pubkey (self-filter). - Phase 3 — Uniform sizing + one splitter. Consolidate the 4 size sites into
transport.max_chars; message formatting targets the minimum active budget (~120) so output is uniform; transport does final (k/N) safety split. Remove the stray [:195]. - Phase 4 — CompositeTransport ("both") + reply routing. Fan
send_messageto both children; merge both inbound streams; each reply goes back over the transport it arrived on (via theMeshMessage.transporttag). - Phase 5 — Per-network awareness + config + deploy. Scope LLM network-status context to the originating network (MeshCore via MeshMonitor REST, mirroring the Meshtastic path). Add
transport: meshtastic|meshcore|both+ ameshcoreconfig section (serial/tcp, channel index/PSK, max_chars). Add themeshcorepip dependency to the image. Deploy to CT 108 once the second radio is attached; smoke-test both meshes.
Open items / prerequisites
- Hardware: a MeshCore Companion node reachable over IP/TCP (native-TCP firmware or a
ser2net/serial-to-IP bridge). Location is flexible — it need NOT be the meshai host — and no USB/udev pinning is required since the connection is over the network. "Both" can't run live until this exists — Phases 1–4 can be built/unit-tested without it. - Dependency: adds the
meshcorepip package to the meshai image (needs approval per no-deps-without-permission policy). - Timing: Phase 1 touches core files (
connector.py,main.py,router.py,responder.py,channels.py) near the in-flightfix/fire-spam-drain-pacerwork; land this AFTER that branch merges to main to avoid a refactor conflict on the live host. - ✅ RESOLVED — pyMC companion frame server: confirmed enabled on aida-nebra, on port 5050, companion identity AIDA (repeater radio node EchoBase, RAK4631).
- ✅ RESOLVED — aida-nebra Tailscale IP =
100.64.0.9(confirmed authoritatively from Tailscale:tailscale status,tailscale ip -4 aida-nebra, MagicDNSaida-nebra.echo6.mesh, andgetent hostsall agree). The old 100.64.0.18 figure is wrong. - Single-client separation (still open): ensure meshai's AIDA companion identity/port (
100.64.0.9:5050) is not also used by MeshMonitor. If MeshMonitor needs MeshCore awareness, it must use a separate companion identity/port so the two don't evict each other. - Map the repeater's channel table (which index = which named channel/PSK).
- Confirm MeshCore max text length to set
max_chars.
Risks
- Async bridge (meshcore asyncio ↔ meshai sync send).
- Reply routing correctness (right mesh).
- MeshCore firmware/lib version churn — pin the
meshcorelib + Companion firmware together; constants (MAX_TEXT_LEN, opcodes) are firmware-defined. - Uniform-to-smaller means Meshtastic messages don't use their extra capacity (accepted tradeoff for uniformity).
References
- meshcore Python lib: github.com/meshcore-dev/meshcore_py ; pypi.org/project/meshcore
- Companion protocol: docs.meshcore.io/companion_protocol/ ; firmware: github.com/meshcore-dev/MeshCore
- MeshMonitor MeshCore support: meshmonitor.org/features/meshcore.html
- Roles overview: nodakmesh.org/blog/meshcore-how-it-works-guide
- pyMC_core (companion frame server): github.com/pyMC-dev/pyMC_core — src/pymc_core/companion/frame_server.py
- pyMC_Repeater / openHop: github.com/rightup/pyMC_Repeater
- meshcore_py TCP transport: github.com/meshcore-dev/meshcore_py — src/meshcore/tcp_cx.py
Deploy context
meshai runs on CT 108 (Tailscale 100.64.0.32) as a docker compose build in /home/zvx/meshai/work/ — NOT the Central service on CT 104. Mesh connectivity is all over IP: no radio is attached to CT 108. The Meshtastic link runs over IP to MeshMonitor's vnode (which fronts the radio), and the planned MeshCore link is likewise a Companion node reached over TCP — so neither transport requires hardware on the meshai host. See the deploy topology note.