Architecture
How Perch & Coop fit together
Two programs, on purpose
radio ──USB/TCP/BLE──▶ ┌──────────────┐ REST + SSE ┌───────────┐
│ COOP │ ◀────────────▶ │ PERCH │──▶ your browser
mqtt broker ──────────▶│ the engine │ │ dashboard │ or the app
└──────┬───────┘ └───────────┘
▼ write-through
mesh.db (SQLite, WAL)
Coop owns everything volatile: the radio link, decode/decrypt, capture. Perch is a stateless viewer over Coop's API. The split is why viewing never risks the capture, why a crashed dashboard costs nothing, and why remote/multi-radio setups are configuration rather than architecture.
The packet's journey
- Arrive — from the radio (serial/TCP/BLE) or the MQTT observer.
- Decrypt if needed — the radio decodes its own channels; Coop's keyring retries everything else (other channels, MQTT traffic) and tags the matching channel.
- Decode — text, nodeinfo, position, telemetry, routing ACKs, neighbor info, waypoints, detection alerts; request/reply correlation wakes any waiting traceroute or position probe.
- Record — into bounded in-memory rings (recent packets/messages/signal) and the roster map, deduped against relays and MQTT echoes.
- Persist — write-through to SQLite, off the hot path.
- Broadcast — one SSE event to every subscriber; slow clients drop frames rather than block the pipeline.
Why the capture survives anything
SQLite in WAL mode, one writer, synchronous=NORMAL: power loss loses at most the final moments, never the file. The radio's own memory is irrelevant — history lives on disk from the moment a packet is heard. Restarts rehydrate the dashboard from the file; retention purges + vacuums on a 6-hour cycle.
Build & portability choices
- Pure Go engine —
modernc.org/sqlite(no cgo) means one static binary per OS/arch; cross-compile with two env vars. macOS builds add cgo only for IOKit serial enumeration and optional CoreBluetooth. - Embedded UI — Perch's React build ships inside the Go binary
(
go:embed); the macOS app is the two binaries in one signed bundle, where Perch launches its sibling Coop. - Ports with names — Coop binds 2667 ("COOP") and falls forward through mnemonic siblings (6374 "MESH"…) instead of crashing on a clash; Perch's prober knows the list, so discovery keeps working.
- Vendored protocol surface — the Meshtastic protobufs come via a Go library, with hand-rolled wire decoding where the vendored schemas lag firmware (traceroute's SNR fields) — upstream churn stays contained.