Radoslaw B.

Faro & sixi-observer: building an open-source handheld UI, Linux console, and SDR bridge for MeshCore LoRa radios

Radoslaw B. · 24 October 2026 · sixi-tdeck-plus (Faro) · sixi-observer

Most LoRa mesh nodes are headless radios that treat your smartphone as their screen and keyboard. That works right up until your phone battery dies, Bluetooth pairing flakes out in the rain, or you want a standalone field terminal that operates without a companion phone at all.

Over the past few months I built and open-sourced the device and telemetry layers of my off-grid packet-radio stack on top of MeshCore:

Together with Sigillo (the 87-byte Ed25519-signed EU CAMF alert frame), they form a complete, auditable stack from the wire format to the handheld screen and desktop workbench.

1. Attaching an on-device UI without forking the radio protocol

Custom firmware forks usually rot because they patch the underlying routing state machine and invent private serial opcodes. Faro takes the opposite approach: it pins unmodified upstream MeshCore (companion-v1.18.0) as a submodule and attaches solely through upstream's platformio.local.ini build hook and the AbstractUITask listener interface.

That architectural constraint buys three things immediately:

  1. Zero custom companion opcodes: The standard MeshCore BLE phone app and Wi-Fi TCP clients (port 5000) keep working simultaneously while you use the T-Deck's physical keyboard and touchscreen.
  2. One UI codebase across handheld, desktop, and CI: The exact same LVGL 9.3 screen code compiles to ESP32-S3 firmware, to a native Ubuntu Linux desktop console (faro-console over SDL2 talking to a companion radio via USB serial or TCP :5000), and to a deterministic headless simulator (faro-sim) that renders every 320×240 screen in light and dark themes to PNG.
  3. Clean upstream upgrades: Moving to a new MeshCore release means bumping the submodule commit and running the simulator and hardware checks, not rebasing a diverged radio stack.

2. Hardware targets and power/RF realities

Writing C/C++ for battery-powered ESP32-S3 + SX1262 hardware quickly surfaces bugs that never appear in a simulator:

Board Build environment Mode Input & interfaces
LilyGo T-Deck / T-Deck Plus faro_tdeck Handheld UI + Companion 320×240 IPS touch, physical keyboard, trackball, GPS, microSD, BLE, Wi-Fi TCP
LilyGo T-Deck MAX faro_tdeck_max Handheld UI + Companion 320×240 IPS touch, keyboard, trackball, microSD, BLE, Wi-Fi TCP
Elecrow ThinkNode M9 faro_thinknode_m9 Screen UI (experimental) I2C keypad (STC8H at 0x6C), piezo alarm/message tones
LilyGo T-Beam S3 Supreme faro_tbeam_supreme Headless Companion SX1262, GPS, PMU, BLE + dynamic Wi-Fi TCP (:5000) + USB serial
Heltec WiFi LoRa 32 V3 faro_heltec_v3 Headless Companion SX1262, BLE + Wi-Fi TCP (:5000) + USB serial

Two hardware lessons from the 0.10.x releases stand out:

3. Enforcing regional spectrum rules in firmware, every second

Because Faro also exposes upstream's USB serial CLI, BLE companion protocol, and TCP port 5000, an external phone app or script can issue a set radio command behind the UI's back. If a user in Switzerland or the EU accidentally applies a 915 MHz US preset or cranks transmit power past the legal ERP limit, the radio transmits out of band.

Faro enforces regional spectrum ceilings directly in a 1 Hz supervisor loop across five region definitions:

Region Allowed sub-band (MHz) Power ceiling Duty cycle
EU / UK / CH 868 869.400 – 869.650 27 dBm e.r.p. (500 mW) 10% (airtime factor 9)
EU / UK 433 433.050 – 434.790 10 dBm e.r.p. (10 mW) 10% (airtime factor 9)
USA / Canada 915 902.000 – 928.000 30 dBm conducted / 36 dBm e.i.r.p. —
Australia / NZ 915 915.000 – 928.000 30 dBm e.i.r.p. —
Brazil 915 902–907.5, 915–928 30 dBm e.i.r.p. —

Every second, Faro checks the active radio parameters (including full channel bandwidth, centre ± BW/2, and antenna gain FARO_ANTENNA_GAIN_DBI, defaulting to 3 dBi where e.r.p. = conducted + gain − 2.15 dB). If an external client moves settings inside a valid region, Faro clamps power and duty cycle to that region's ceiling; if the frequency falls outside every allowed sub-band, Faro immediately restores the last valid configuration and logs the rejection.

4. Hardening the companion surface

Reviewing the companion attack surface led to several deliberate defaults in Faro that differ from stock firmware:

On top of messaging (direct messages with ACK/RTT, #hashtag channels, QR contact sharing, store-and-forward) and remote repeater administration (login, telemetry, neighbour tables, ACLs, remote CLI), the UI gives field operators real RF instrumentation: a 2D mesh topology graph, an SX1262 link-budget and Time-on-Air calculator, per-hop SNR path traces, zero-hop neighbour scans, and offline SD-card map tiles with GPX track recording and EN/IT/DE/FR localization.

5. sixi-observer: local Ed25519 verification and Bayesian ghost-hop triangulation

While Faro turns a handheld into a self-contained field terminal, I also wanted a lightweight Python daemon for headless USB radios (T-Beam Supreme SX1262, Heltec V3/V4, RAK4631) and RTL-SDR Blog V4 dongles running on a Linux mini-PC or Raspberry Pi. That project is sixi-observer.

It solves three practical telemetry problems in a multi-hop LoRa network:

  1. Local 64-byte Ed25519 advertisement verification: MeshCore node advertisements (ADVERT frames) carry a 64-byte Ed25519 signature over the node's public key, timestamp, coordinates, and name. Rather than trusting unverified metadata, sixi-observer verifies the 64-byte signature locally on the observer host before marking a node verified_local = true.
  2. ML Bayesian ghost-hop repeater triangulation: In MeshCore flood routing, packets record a path of 1-byte repeater hashes (Origin → ~A3 → ~7F → Your Radio) alongside the final hop's sub-noise SNR and RSSI. Frequently, intermediate repeaters (~HASH) forward traffic without having broadcast a recent GPS-tagged ADVERT of their own. sixi-observer uses Bayesian spatial inference over observed multi-hop paths, known anchor coordinates, and free-space/terrain path-loss posteriors to estimate coordinates and an uncertainty radius (±km) for those unidentified "ghost-hop" repeaters.
  3. Zero-latency local WebSocket stream & strict privacy: Running sixi-observer exposes a local WebSocket server at ws://127.0.0.1:8765/ws with 0 ms cloud latency, and can be launched in one command with uvx or Docker. By design, encrypted direct messages (TXT_MSG / ECDH) and private channel keys are never decoded or transmitted off the device.
# Run sixi-observer directly via uvx against a USB-connected T-Beam Supreme or Heltec
uvx --from git+https://github.com/radek-sixi/sixi-observer sixi-observer run \
  --serial /dev/ttyACM0 \
  --name "Field Observer" \
  --lat 47.37 --lon 8.54

All three repositories in the LoRa stack are open source: sigillo (Apache-2.0 / CC BY 4.0), sixi-tdeck-plus (Faro) (Apache-2.0), and sixi-observer (MIT).