13 KiB
Linux ESP-NOW Team integration
CM0 and M5Stack Cardputer Zero use the shared Team coordinator, protocol and service to pair with an unchanged ESP32 TrailMate. ESP-NOW handles enrollment; subsequent Team status, chat and map data use the selected mesh bearer.
Qualified pairing hardware
| Device | OS / architecture | Kernel | Wi-Fi |
|---|---|---|---|
| T-Display-CM0 | Raspbian / ARMv7 hard float | 6.18.50-trailmate-irq-v7 | CYW43439 |
| M5Stack Cardputer Zero | Debian / AArch64 | 6.18.39+rpt-rpi-v8 | CYW43439 |
The qualified devices retain brcmfmac firmware 7.95.75, FWID 01-bb9b5b66.
The helper uses the existing Broadcom vendor DCMD actframe transmit operation
and cfg80211 management-frame receive registration. These combinations do not
require monitor mode, firmware replacement or kernel patches. This does not
qualify arbitrary Linux Wi-Fi adapters.
The vendor action body contains category 127, Espressif OUI 18:fe:34, four
random bytes and a vendor element with ESP-NOW type 4/version 1. Application
payloads are bounded to 250 bytes. The firmware reconstructs parts of the
received MAC header; decoding uses its source address and action payload,
without treating that reconstructed header as an exact air capture.
Pairing temporarily detaches wlan0 from NetworkManager and selects channel 1 through the firmware setter with readback. Reception registration follows that transition. Firmware MPC/PM sleep is disabled for the connectionless window; the previous policy is saved durably before modification and restored afterward. Concurrent ordinary Wi-Fi and ESP-NOW operation has not been qualified.
Installation and privilege boundary
From the repository root on a qualified device:
sudo sh scripts/install_linux_espnow_helper.sh
The installer requires systemd, NetworkManager, Python 3, iw, ip, rfkill,
/usr/include/linux/nl80211.h and the local pi account. Build and install the
Linux application separately. The helper and units currently have a standalone
installer; they are not automatically included in the Debian package lifecycle.
The application runs as the ordinary user. LinuxEspNowPairing connects the
shared TeamPairingCoordinator to a bounded Unix-domain protocol. Root radio
operations stay in the socket-activated Python helper. Its root:pi socket is
mode 0660 and checks peer credentials. Clients may send bounded ESP-NOW payloads
to peer MACs; the protocol does not expose arbitrary firmware commands or channel
changes. Production pairing is available only when the installed control socket
is accessible. Demo mode retains its separate loopback transport.
Recovery independent of the application
Before changing wlan0, the helper verifies the independent recovery timer, a saved active connection UUID and an IPv4 default route. It fsyncs a root-only lease containing the boot ID, deadline and preceding radio policy.
| Limit | Duration |
|---|---|
| Active enrollment exchange | 125 seconds |
| Recovery lease | 150 seconds |
| Independent timer interval | 15 seconds |
| Helper service runtime limit | 165 seconds |
Normal completion, cancellation and client EOF expire the lease and request recovery in a separate unit. Killing the application or helper cannot stop the independent timer. Recovery restores NetworkManager ownership, the saved power policy, connection UUID and default route. A failed activation retains the record for retry; only verified recovery clears it. A changed boot ID triggers recovery of an interrupted lease after reboot.
Physical SIGKILL acceptance restored ordinary Wi-Fi, SSH, route and the original
mpc=1, pm=2 policy at 154 seconds on CM0 and 155 seconds on Zero, measured
from arming the lease. Reboot detection and reconnect retries also have automated
coverage. These measurements do not promise a fixed reconnection time for every
network.
Durable Team state and compatible crypto
Team ID, PSK, key round, membership, name and transient pairing flags persist
atomically in a private mode-0600 team.snapshot. Startup restores controller
keys from that snapshot. There is no time-based expiration of membership or
credentials. A missed member status does not clear them; an explicit leave does.
The normal device paths are:
- CM0:
~/.local/state/trailmate/team.snapshot - Zero:
~/.trailmate_cardputer_zero/team.snapshot
A local snapshot member with node ID zero represents “You”, not a zero radio identity. The ESP-NOW key exchange does not carry the full roster; authenticated mesh status messages subsequently supply the leader's roster.
Native Linux crypto matches the ESP implementation: SHA-256 over PSK followed by the derivation label, then ChaCha20-Poly1305 with a 12-byte nonce and an appended 16-byte tag. It fails closed without OpenSSL. The former Linux demo cipher is not used for native Team traffic.
Chat display history is currently process-local on Linux. Restarting an application clears its displayed Team conversation history while retaining membership and keys.
Common Linux mesh dispatch
Both devices use the same Team service, snapshot store, crypto implementation, mesh worker, presence scheduler and Meshtastic relay planner. Their physical drivers differ: CM0 uses LR2021; Zero uses the SX1262 CAP. SX1262 transmission enters checked standby before changing packet parameters or writing the shared RX/TX FIFO, then restores reception and reports errors.
The shared Team background dispatcher sends authenticated status every 30 seconds after a successful enqueue, with a five-second startup/retry delay. This schedule continues after the finite enrollment broadcasts have drained and does not depend on keeping the Team page open. Leaders send their complete roster; members send presence without a roster, retaining the leader's list. Linux uses the same dispatcher rather than a second platform scheduler. A single 32-bit deadline is added to the dispatcher; periodic sends do not allocate queue entries. Existing finite enrollment retries retain their compatibility messages, while periodic presence is authenticated only.
Protocol, region, actual frequency, modulation and channel keys must agree across participants. The live CN / LongFast exercises used 478.875 MHz, BW 250 kHz, SF11, CR 4/5, sync 0x2B and a 16-symbol preamble.
For physical Meshtastic application data, the worker observes completed RX/TX and damaged-frame counters plus instant RSSI. It defers local data through the active packet and delayed relay exchange. The guard derives from configured LoRa airtime and relay delay. Every new attempt, including a command that wakes an idle worker, first enters randomized contention.
Callers enqueue without sleeping. Receive processing, due relays and the UI
continue while application data waits. A continuously busy channel expires an
operation after 30 seconds, with an action error and a
Meshtastic queued TX expired packet log entry. It does not clear Team
credentials. MQTT and other mesh protocols retain their own transport policies.
The bounded relay planner preserves origin, packet ID and encrypted payload, decrements hops and marks the relay node. It does not forward MQTT ingress to RF. Retries and queued frames have bounded lifetimes. A separate delivery cache prevents original RF, relay and MQTT copies from generating duplicate application events; reliable direct duplicates still receive routing ACKs.
The position-ring reader projects the newest timestamp per member. An older relayed sample cannot replace a newer displayed location.
Team relative-time presentation uses wall-clock seconds when available and does not subtract a process uptime from a Unix timestamp. A persisted timestamp from a different clock domain is shown as unknown. The local zero-ID alias is the running observer, not a stale RF peer; returned local radio identities map back to that same alias instead of adding another member. Snapshot reload also restores the encoded last-update timestamp.
Verification
Native fixtures cover compatible crypto and tamper rejection, private snapshot save/reload and truncation rejection, authenticated presence, relay/delivery deduplication, newest-position projection, SX1262 standby/FIFO sequencing and RX restoration, rejection of simultaneous RX_DONE/CRC/header error IRQs, Contacts Team visibility and conversation routing, and real worker queueing under carrier activity. Queue tests also cover idle-worker wake-up, eventual transmission, timer rollover and CPU-spin avoidance.
Run the recovery tests on Linux:
python3 -m unittest discover -s tests/linux -p test_espnow_recovery.py
Physical pairing acceptance runs trailmate_linux_team_espnow_hardware_test
explicitly, outside CTest: it temporarily changes wlan0 and requires the installed
recovery helper. Use isolated settings roots and validation identities for that
executable. Normal installed-app acceptance must use the user's actual identity
and preserve its current snapshot.
Enqueue success or a local “Sent” bubble alone does not prove peer reception. Correlate actual RF packet IDs at the receiver, authenticate the current Team envelope, and inspect the peer conversation. Enrollment success and matching keys alone do not qualify subsequent chat or map traffic.
Map acceptance needs a usable GNSS fix or an explicitly labeled test waypoint. Manual test coordinates must not be presented as a live fix. A “Team location not ready” result when no fix exists is distinct from a radio delivery failure. Independent application-resume timers must protect any exclusive-radio diagnostic that temporarily stops the normal app.
Shared protocol ownership
Linux is a useful test host for the protocol code that also runs on ESP32. The same protocol must not acquire a second wire implementation merely because its radio or operating system changes. Team enrollment and mesh transport remain separate; successful ESP-NOW enrollment does not qualify a mesh bearer.
| Protocol | Canonical shared implementation | Platform responsibility |
|---|---|---|
| Meshtastic | core_chat packet wire format, nonce, channel framing, protobuf helpers, PKI primitives and runtime effects |
Radio I/O, scheduler, persistent identity, event/store integration; the AES primitive selects the existing ESP implementation or Linux OpenSSL backend |
| MeshCore | core_chat crypto, payload helpers and identity primitives; core_mesh::MeshCoreProtocolStrategy builds both group and private AppData |
Resolve channel/peer keys and paths, maintain bounded pending sends, persist identities, and project received events |
| Reticulum / LXMF | core_chat packet/token format, signature and handshake calculations, packet router, path/session/resource owners and delivery planning |
Interface sockets/radio, clock/random/crypto providers, persistence, allocation budget and application acceptance |
LinuxRawLoraMeshAdapter calls Meshtastic buildWirePacket, parseWirePacket
and decryptPayload; it does not maintain its own AES-CTR, nonce or packet
codec. The Linux OpenSSL primitive operates in place. ESP preprocessing excludes
that backend, including its OpenSSL context allocation.
Linux MeshCore group/private AppData calls the same strategy used by the ESP adapter. Explicit public-key hashes take precedence in both V1 and V2, including a valid zero first byte. Oversized AppData fails instead of silently transmitting a truncated application envelope. Linux admission checks still reject ambiguous zero-padded application tails and disabled channel slots. Existing text and PATH ACK composition, platform relay scheduling and adapter orchestration have not all been consolidated; sharing AppData does not claim that the whole adapter is platform independent.
Reticulum's ESP compatibility headers already forward to canonical core_chat
owners. Keep that direction of dependency. Linux's heap buffers, SQLite
transactions and socket lifecycle must not become ESP task-stack buffers or a
second copy of the shared session/resource runtime. Remaining platform envelope
orchestration can be reduced only while preserving its caller-owned storage and
durable acceptance boundary.
Memory and compatibility checks
The Meshtastic backend change produces byte-identical ESP preprocessed code.
An ESP32-S3 -Os -fstack-usage compile of the MeshCore strategy before/after the
AppData convergence reports 3,266 bytes of code and zero static data/BSS for both
translation units. The send implementation's reported stack frame changes from
688 to 704 bytes; its wrapper remains 48 bytes and RX remains 416 bytes. These
are isolated compiler checks, not whole-firmware RAM or runtime high-water
measurements. No strategy member, queue, allocation or buffer capacity is added.
Native contracts independently decrypt actual Linux Meshtastic AES-128/256 packets with OpenSSL and compare their complete bytes with the shared codec. MeshCore contracts compare group/private V1/V2 AppData with the shared strategy, retain independent encrypted text vectors, and test explicit zero hashes and oversize rejection. Cardputer Zero registers those existing MeshCore fixtures in its native suite, so they run against the same common library as the device. The repository ESP stack hygiene check remains required. Software contracts do not establish new MeshCore or Reticulum on-air qualification.