MeshCom integration (design)¶
Built and planned
The receive side is built: the pure core in packages/aprs/src/meshcom/ decodes the datagrams,
apps/ingest/src/meshcom.ts listens for them, and operator setup is under
MeshCom. Transmit is built for one use: answering radio commands heard on
the mesh (see Transmit). Telemetry, replies from the Messages surface, and a
browser-direct path are planned, tracked in
TODO.md.
What MeshCom is¶
MeshCom is a LoRa text-and-position mesh for licensed radio amateurs, developed by the
Institute of Citizen Science for Space & Wireless (ICSSW) and widely
deployed in Austria (ÖVSV), Germany (DARC) and Switzerland. Its firmware is
MIT-licensed, which makes it an open specification in
the sense of the project's IP rule and lets the decoder live in the MIT packages/aprs.
| Property | Value |
|---|---|
| Frequency | 433.175 MHz (D-A-CH, EU); 439.9125 MHz (UK); 433.925 MHz (Norway) |
| Modulation | LoRa, SF11, 250 kHz bandwidth, CR 4/6 (the EU profile); the UK, ON (Belgium) and LA (Norway) profiles use SF10 at 125 kHz |
| Framing | APRS source / destination / digipeater path and APRS payload (: message, ! position) inside a small binary header: 32-bit message id, hop byte (hop count + flags), hardware id, modulation id, 16-bit checksum (protocol) |
| Addressing | Direct message to a callsign, group message to a group number 1–99999 (group 9 = emergency), broadcast to * |
| Payload types | Text, position, telemetry (temperature, humidity, pressure) |
| Identity | Real amateur callsigns with SSID (SSIDs above 15 occur, e.g. -99) |
| Gateways | A node in gateway mode links its local mesh to the MeshCom servers over HAMNET or the internet; dashboards at meshcom.oevsv.at |
| LoRa-APRS side channel | With --track on a node also beacons its position on 433.775 MHz (LoRa-APRS) to destination APRSMC; --aprsmc APRS sets the destination many LoRa-APRS IGates expect, and they put it on APRS-IS (details) |
| Hardware | T-Beam, Heltec V3, RAK4631, T-Echo, T-Deck and T-Deck Plus (a standalone node with display and keyboard) |
The external UDP interface¶
The UDP interface for external client software
is the integration point. A node joined to Wi-Fi and configured with --extudpip <ip> and
--extudp on exchanges JSON datagrams on UDP port 1799:
{"src_type":"lora","type":"msg","src":"DH1FR-1","dst":"DH1FR-2","msg":"Hello{034","msg_id":"5DFC7187","rssi":-95,"snr":12}
{"src_type":"lora","type":"pos","src":"9V1LH-1,OE1KBC-12","msg":"","lat":1.3773,"lat_dir":"N","long":103.942,"long_dir":"E","aprs_symbol":"#","aprs_symbol_group":"/","hw_id":4,"msg_id":"AE48D54D","alt":161,"batt":5,"firmware":17,"fw_sub":"p","rssi":-95,"snr":12}
{"src_type":"lora","type":"tele","src":"9V1LH-1,OE1KBC-12","batt":5,"temp1":28.9,"temp2":0,"hum":40.2,"qfe":1004.9,"qnh":1005.4}
- A client sends with
{"type":"msg","dst":"<call | group | *>","msg":"<text>"}(UTF-8, at most 150 bytes). The full datagram set, verified against the firmware, is the ExtUDP reference. lat/longare unsigned degrees (truncated to four decimals) with the hemisphere inlat_dir/long_dir.altholds the raw/A=digits, which are feet or metres depending on a per-node setting, so the listener does not forward it.src_typenames where the node got the datagram:lora(heard on air),node(the node itself),udp(relayed from the MeshCom server over the internet).- The trailing
{034on a message is the APRS message number, used for acknowledgement. - The interface carries no authentication and no encryption.
How it fits the platform¶
MeshCom sits between APRS and Meshtastic. Like APRS it carries real callsigns on every frame and APRS payloads, so a MeshCom position maps onto a station directly — Meshtastic shows a callsign only for nodes in licensed mode, learned from their node info. Like Meshtastic it is a LoRa mesh reached through a node on the operator's LAN, not through a TNC.
flowchart LR
lora["LoRa 433.175 MHz"] --> node["MeshCom node"]
node -- "UDP :1799, JSON" --> listener["apps/ingest<br/>MeshcomListener"]
listener -- "Packet, port meshcom<br/>batched POST /ingest" --> gateway["gateway<br/>map · MHeard · Messages"]
listener -. "a message out<br/>(transmit is gated)" .-> node
Where each piece lands¶
| MeshCom piece | Home in the repo | State |
|---|---|---|
| Pure core | packages/aprs/src/meshcom/: parse (total, structured rejection reasons), normalize (hemisphere, callsigns, path, locator, provenance), dedup (bounded frame-id window, stronger copies upgrade), encode (direct-callsign text, 150 UTF-8 bytes), aprs (APRS mapping and transport hint). A position becomes an uncompressed ! position; a direct message an APRS :ADDRESSEE:text{nnn message; group and * text an APRS user-defined {MG packet, which the decoder classifies as other so it stays out of the callsign message log; meshcomGroupOf reads the group and text back, and the gateway stores them as group chat (meshcom_group_messages, one row per sender and msg_id). Conformance runs the golden fixtures on Node and Bun (pnpm conformance:meshcom) |
built |
| UDP listener | MeshcomListener in apps/ingest/src/meshcom.ts: accepts only configured node addresses, bounds size and per-node rate, passes raw datagrams to MESHCOM_FANOUT targets, dedups, stamps the transport hint, logs counters and warns on a silent node or crash-prone firmware |
built |
| Configuration | MESHCOM_NODE (ip[=CALL] list; enables the listener), MESHCOM_PORT, MESHCOM_BIND, MESHCOM_FANOUT, MESHCOM_RATE, MESHCOM_STALE_MIN |
built |
| Transport enum | "meshcom" in Transport (packages/shared/src/packet.ts). Stored positions record it in positions.transport, which transportOf() in workers/gateway/src/provenance.ts reads back; display and statistics only, since trust gates on the attestation flag, not the transport |
built |
| Positions | kind:"position" with parsed.lat/lon → live map, MHeard, GET /api/ports |
built |
| Messages | a direct message → the messages log (shack.ts); the Messages surface shows it |
built |
| Node and link store | meshcom_nodes and meshcom_links (workers/gateway/src/meshcom.ts): per node the latest device, firmware, battery, way of hearing, receiver and a rolling signal average; per link the last direct hearing or relay leg. Written only from the operator's own attested MeshCom port, when something shown changes or MESHCOM_META_MIN_S has passed; pruned nightly, skipped while the write budget sheds. Display only: never touches the A/B/C find tiers |
built |
| Read API | GET /api/meshcom/nodes and GET /api/meshcom/links (API): buckets for visitors, exact battery, RSSI and SNR for signed-in members |
built |
| Map | a MeshCom layer (on by default) marks nodes with an "M" tag, dashed when heard only via the server; a links sub-layer (off by default) draws the last 24 hours of links, solid for direct, dashed for relay legs, wider for a stronger signal, fainter with age; the station panel shows how the node was heard, its device, battery and signal, and links to its page on MeshMap. Device names come from a table of the firmware's hardware ids (packages/aprs/src/meshcom/hardware.ts) |
built |
| Via lists | the destination is the last token of dst; the via list rides as display metadata (sent_via on the node row) and never becomes a link or a trust input (Via-Calls) |
built |
| Telemetry | tele → the observational weather path (sensor_readings); never touches the A/B/C find tiers. The firmware reports an absent sensor as 0, so the mapping needs a per-field presence rule |
planned |
Callsigns and paths¶
src is a source path. The origin is its first element; later elements are the relaying nodes and go
into Packet.path (the firmware builds it that way: the source call ends at the first comma). The
callsign check accepts SSIDs outside the AX.25 0–15 range, as APRS-IS does for TNC2 text, and requires
both a letter and a digit in the base call, which separates calls from group numbers and from the
firmware's internal non-call sources.
Trust¶
MeshCom frames are unsigned, and a udp-sourced datagram crossed the internet. The design follows the
standing rule that transport is not trust:
- Only a direct LoRa hearing names a receiving site. A frame is RF only when
src_typeisloraand its origin is not the receiving node (the firmware labels the node's own back-pressure noticeslora). An RF frame whose source path is the originator alone is direct. The listener forwards a direct frame asheardVia: "rf"with the node's call asigateCall; a relayed RF frame asrfwith no gate; everything else asaprs_is, all on themeshcomport. The gateway's provenance derivation is then the only Tier A gate:meshcomis one of the two on-air transports it can attest (with a local TNC), and it attests the frame only when the node's call is inFIRST_PARTY_SITES, and the usual independence rule stops an operator's own node from corroborating the operator's own find. - Relayed and server frames never corroborate presence. A relay proves the originator was near the relay, not near the receiving node, and a server copy proves nothing about the air.
- Finds logged over the mesh (Logging finds over radio messages) stay Tier C unless corroborated by the usual A or B evidence; the message itself proves nothing about presence.
Security¶
- The listener binds to the LAN (
MESHCOM_BIND) and drops datagrams whose source address is notMESHCOM_NODE. Any host on the segment can otherwise inject JSON. - Port 1799 is never exposed to the internet. Pointing
--extudpipat a cloud gateway would couple RF ingest to a cloud host and send unauthenticated traffic across the internet; the operator docs say so explicitly. - Datagrams over 2 KiB are refused before parsing; every field is type- and range-checked, text is collapsed to one line and length-bounded, and a malformed datagram is dropped, never thrown.
- The listener never transmits and never registers with the node, so it cannot change the node's state.
Locality¶
The node talks to the ingest box over the operator's own LAN, which is exactly the shape
the ingest-locality rule
asks for: a Pi next to the node works off-grid with a local gateway, and the same box can forward to a
LAN or cloud gateway through INGEST_URL.
The browser cannot open UDP sockets. A browser-direct path goes over Web Serial or Web Bluetooth to the
node, the way RfBrowser.tsx reaches Meshtastic; it depends on the node's serial and BLE
protocols, which are listed under open questions.
Transmit¶
MeshcomSender (apps/ingest/src/meshcom-send.ts) is the one transmit path. It is off unless enabled
with the operator's callsign, and that call must match the call the target node transmits under — the
node sends every message as itself, so software can only transmit under the licensed operator's call.
It sends direct messages to a callsign only (the encoder refuses groups and *), only to configured
node addresses, through a token bucket (one per minute, burst three, by default) because an SF11 /
250 kHz frame is long on air and the channel is shared. Every attempt is audited — time, node,
destination, byte length, requesting feature and outcome, never the text. The best outcome is handed to
node: from firmware 4.40a ExtUDP reports an ack only when it reaches the node through the MeshCom server
(type:"ack"), never one the node hears over LoRa. The node reports its own refusals (QRS/QRT) back
through the listener.
The box enables the sender with MESHCOM_TX=1. It is used by:
- Answers to radio commands — the ack and opt-in reply to a
FOUND/DNF/NOTE/HELPdirect message that one of the box's nodes heard, queued by the gateway as ameshcom_msgbox command (Logging finds over radio messages).
Planned: replies from the Messages surface answering a direct message, tracked in
TODO.md.
Group announcements stay out of scope: software never originates group or broadcast traffic.
Via-Calls¶
A node can name the relays allowed to forward what it sends, instead of letting every node flood it — source routing to save airtime. Via-Calls are in the firmware from v4.35p.06.13, which carries the via list in the destination path.
- Commands.
--via <CALL,CALL>sets the list (upper-cased, at most 39 characters),--via NONEclears it,--via on|offswitches the function,--viadebug on|offadds debug output. The node settings show the list asVIACALL. - Sending. With Via on and a list set,
checkVia()writes the destination path as<via-list>,<destination>on every send path, messages that arrive from an ExtUDP client included. - Relaying. A frame without a via list is relayed by every node with
--mesh on. A frame with one is relayed only by a node whose call appears in it — compared token by token at full length, soDK5EN-9is notDK5EN-90— and only if that node has--mesh on. Order is not enforced: any named node that hears the frame relays it. Every node still receives it. - A relay writes its own list. Before relaying, a node resets the destination path to the destination and applies its own via list, if it has one; a gateway does the same to a frame from the MeshCom server before it goes on air. So a copy's via list is that of the node that last transmitted it — the originator's only when the source path names no relay.
- Automatic selection is commented out in the firmware since 22 July 2026 (still so in 4.40a); only lists
an operator sets take effect. The disabled code picks the gateway token
HGon a gateway and, on any other node, the direct neighbour heard within the last 60 minutes that reports the most neighbours of its own.
Three rules hold in APRScaching:
- The destination is the last token of
dst, everything before it the via list (ExtUDP). A node's via list is recorded only from a copy whose source path is the originator alone; a relayed or server copy carries another node's list. - A via list is never a link. The map draws links from the source path and the receiving node only; a via list names relays a transmitting node allowed, not ones the frame passed.
- A via list is never trust. It bears on nothing in provenance,
director the A/B/C tiers. It is display information: the station panel shows a node's latest own list ("sent via relays …"), and the operator's own node's list — which every message the box sends through it carries — shows in the station status, the Pocket notification and the box log.
Sources: firmware 4.40a e1e2ace — src/via_functions.cpp (checkVia, checkMesh, pathNamesCall),
src/lora_functions.cpp (the relay), src/esp32/udp_frame_esp32.cpp (server frames put on air),
src/command_functions.cpp (--via), src/aprs_functions.cpp (the destination split),
src/extudp_functions.cpp (dst), docs/hey-supp.md (the routing design).
Positions that already arrive¶
Nodes running --track on beacon on the LoRa-APRS frequency, and LoRa-APRS IGates forward those
beacons to APRS-IS. They reach every instance through the APRS-IS feed, as ordinary
Tier C positions, with no MeshCom-specific code. The listener adds what that path cannot: MeshCom-only
nodes, messages, telemetry, and signal reports from the operator's own node.
Open questions¶
- Serial and BLE protocols. Whether the node's serial console emits machine-readable frames, and what the BLE service the phone apps use looks like. Both are answered from the MIT firmware source before a browser-direct path is designed.
- Acknowledgements over LoRa. From 4.40a ExtUDP reports an ack that reaches the node through the
MeshCom server (
type:"ack"), but none the node hears over LoRa. How to learn of a LoRa-path ack is open, so a transmitted message ends at handed to node and the Messages surface cannot yet show delivery. - Gateway server protocol. Whether an instance should ever talk to the MeshCom servers directly; the default answer is no — the local node is the integration point.
- Reference clients. MeshcomWebDesk and meshcom-udp-logger are useful behavioural references for the UDP interface. Their code is not reused; the ICSSW interface description and the firmware are the specifications.
References¶
- ICSSW — MeshCom · MeshCom 4.0 FAQ
- MeshCom protocol
- UDP interface for external client software
- MeshCom & LoRa-APRS
- MeshCom firmware (MIT)
- ÖVSV wiki — MeshCom
- DARC — Einstieg in MeshCom auf 433 MHz (PDF)
Next¶
- MeshCom: setting up a node.
- MeshCom ExtUDP protocol: the datagrams on the wire.