Skip to content

Draft: native radio driver for meshtasticd hardware (SX126x, SX127x, SX1280, LR11x0, CH341 USB) - #3

Merged
A13xB0 merged 21 commits into
mainfrom
native-spi-radio
Sep 16, 2026
Merged

A13xB0 merged 21 commits into
mainfrom
native-spi-radio

Conversation

@A13xB0

@A13xB0 A13xB0 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Native radio driver (option D in the meshtasticd plan): RepeaterTastic drives the LoRa chip on meshtasticd hardware itself, with no USB KISS modem and no meshtasticd. It's a new radio.Radio implementation, so identities, relay, plugins and MQTT don't change.

Not yet run on hardware. This PR stays a draft until testers have worked through docs/spi-radio-testing.md. Test binaries are on the spi-draft-2 prerelease.

Coverage

Chip Buses Example boards
SX1262, SX1268, LLCC68 spidev + GPIO, CH341 USB MeshAdv, Waveshare, RAK6421, Nebra/Zebra, PiMesh, Femtofox, Luckfox, MeshStick, Meshtoad, uMesh
LR1110, LR1120, LR1121 spidev + GPIO, CH341 USB Femtofox E80, PiggyStick
SX1276/SX1278 (RF95) spidev + GPIO Adafruit RFM9x
SX1280 (2.4 GHz) spidev + GPIO Module: sx1280 boards

All 61 of meshtasticd's bin/config.d/lora-*.yaml files are built in, copied unchanged under GPL-3.0 (see internal/radio/spi/boards/README.md), and all of them parse. LR2021 is left out because no meshtasticd board uses it.

What's in it

  • Config: radio.driver: spi. radio.device is one of:
    • a board file path;
    • a built-in board name (MeshAdv-900M30S);
    • auto: detect a CH341 stick's product string, a Pi HAT+ EEPROM or a RAK I²C EEPROM, following meshtasticd's autoconf.
  • internal/radio/spi: one radio loop (IRQ or polling, RX frames with RSSI/SNR, TX with TxDone, noise floor, channel busy) plus a driver per chip family:
    • SX126x: sync word readback, TCXO, DIO2 RF switch, errata 15.1/15.2/15.4, OCP, boosted gain;
    • SX127x: version check, PA_BOOST/DAC, errata 2.1, packet-RSSI offsets, modem-status channel busy;
    • SX1280: version string check, mantissa/exponent preamble, SF register;
    • LR11x0: two-phase SPI reads, TCXO, calibration, rfswitch_table, HP/LP/HF PA selection, bootloader detection.
  • Buses, pure Go (no CGO):
    • spidev SPI_IOC_MESSAGE plus GPIO v2 chardev lines and edge events;
    • CH341 over usbfs (bit-reversed SPI stream packets, UIO pins, polled IRQ).
  • Board file keys, following meshtasticd:
    • pins as numbers or {pin, gpiochip, line}, and RADIOLIB_NC;
    • Enable_Pins, SX126X_ANT_SW, DIO2_AS_RF_SWITCH, DIO3_TCXO_VOLTAGE;
    • per-chip *_MAX_POWER with meshtasticd's defaults;
    • TX_GAIN_LORA, with the same power-limit walk as RadioInterface::limitPower;
    • USB_VID/USB_PID/USB_Serialnum;
    • a repeated key resolves to its first value, as yaml-cpp does.
  • kisstool: boards lists the built-in boards; --board works with info (chip diagnostics, noise floor), listen and send-text.
  • Setup wizard: keeps an spi radio's board; editing a radio in the GUI no longer forces kiss.
  • Tests: a fake chip model per family (configure, RX, TX), board parsing, every built-in board, autoconf mapping, EEPROM CRC, CH341 encoding.

GUI

  • Modem switch in Add radio and Edit radio: USB modem | Board. The board list (GET /api/v1/boards) is grouped by host computer from each file's Meta.compatible, with /etc/meshtasticd boards first and a custom path option.
  • Setup wizard: detected serial ports, Serial port by name for an unlisted one, and LoRa board on SPI or a USB stick; Test modem opens a board and shows the chip's diagnostics. Picking a board switches the driver without a restart. The relay step offers all five roles.
  • Built-in board file lora-usb-xiao-sx1262-ch341.yaml for the XIAO nRF52840 + Wio-SX1262 CH341 bridge, so auto finds it.

meshtasticd's raw modem mode (PR #4) was merged in here and then removed again: it won't go upstream, and RepeaterTastic will drive meshtasticd over its client API instead (epic PR to follow).

Known gaps

  • Channel busy uses preamble/header IRQs (and SX127x modem status) rather than CAD.
  • LR1110 firmware updates aren't handled; a chip in its bootloader is reported.

random-thought and others added 3 commits September 15, 2026 21:35
radio.driver: spi drives an SX126x on a Linux SPI bus directly, with radio.device set to the
meshtasticd board file (config.d/lora-*.yaml). Pure Go: spidev and GPIO v2 chardev ioctls, no CGO.
49 of meshtasticd's 61 board files parse; LR1121, RF95 and CH341 USB adapters are rejected.

- internal/radio/sx126x: board parser (pins, gpiochip maps, Enable_Pins, SX126X_ANT_SW, TCXO,
  DIO2 RF switch, SX126X_MAX_POWER, TX_GAIN_LORA), Linux HAL, driver with datasheet errata,
  tests against a fake chip
- kisstool --board: info (SPI readback, device errors, noise floor), listen and send-text
- setup wizard keeps an spi radio's board file; editing a radio no longer forces kiss
- docs/spi-radio-testing.md: step-by-step guide for testers with hardware

Untested on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…, board names and auto

The native radio driver moves to internal/radio/spi and splits into a shared radio loop and chip
drivers, so it now covers every chip and bus meshtasticd's board files use:

- chips: SX1262/SX1268/LLCC68, SX1276/SX1278 (RF95), SX1280 (2.4 GHz) and LR1110/LR1120/LR1121,
  each from the datasheet and RadioLib's command sequences, with fake-chip tests for RX and TX
- CH341 USB-to-SPI sticks (MeshStick, Meshtoad, RAK19714, uMesh, PiNedio, PiggyStick) over usbfs,
  in pure Go
- boards: meshtasticd's 61 board files are built in; radio.device takes a file path, a board name,
  or auto (CH341 product string, Pi HAT+ EEPROM, RAK I2C EEPROM, as meshtasticd's autoconf)
- board file keys: per-chip power limits and defaults, LR11x0 rfswitch_table, optional BUSY,
  repeated YAML keys resolved like yaml-cpp (first wins)
- kisstool boards lists them; kisstool info prints each chip's diagnostics

LR2021 is left out: no meshtasticd board uses it. Untested on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
@A13xB0 A13xB0 changed the title Draft: native SPI radio driver for meshtasticd Pi HATs Draft: native radio driver for meshtasticd hardware (SX126x, SX127x, SX1280, LR11x0, CH341 USB) Sep 15, 2026
A kiss radio's device can now be tcp://host:port. The driver dials it with a
connect timeout and TCP keepalives instead of opening a serial port, and
reconnects and re-applies the config exactly as for a serial modem. This is
how RepeaterTastic uses meshtasticd's raw modem mode, which serves a Linux LoRa
HAT or CH341 stick with the Mesh KISS v2 protocol (currently the raw-modem
branch of A13xB0/firmware).

- config: a malformed tcp:// device is rejected, and two radios can't share
  one (host names compared case-insensitively).
- kisstool: --dev tcp://host:port works.
- web: the setup "Test modem" probe accepts tcp:// devices; before a password
  is set it only dials loopback addresses.
- docs: meshtasticd-raw-modem.md, linked from the README, configuration and
  hardware pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
random-thought and others added 17 commits September 15, 2026 23:07
Edit radio and Add radio get a USB modem / meshtasticd switch: meshtasticd takes a host and port
and saves tcp://host:port, with a hint about RawModemPort. The setup wizard lists meshtasticd's raw
modem next to the serial ports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
FESC FESC TFEND decoded to FEND; the second FESC is an invalid escaped byte and
is dropped, so TFEND is then a literal 0xDC, matching the modem firmware.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
GET /boards (setup-level auth) lists what radio.device can be with driver
spi: auto, meshtasticd's board files in /etc/meshtasticd/config.d and
available.d, then the built-in copies, each with its module, bus and
whether this driver supports it.

POST /setup/probe takes driver: spi and opens the board, answering with
the chip's diagnostic lines in a new details field; a board a running
radio already drives is reported, not opened twice, and board files are
limited to /etc/meshtasticd since the probe runs before a password is set.

POST /setup takes driver (kiss or spi) and applies it with device; an
older wizard that sends only a serial port still can't overwrite an spi
board.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
ModemDeviceField switches between "USB modem" (the serial port as before)
and "Board (SPI or USB stick)": a select of the boards from GET /boards,
grouped by where they come from, with a board file path as the last
choice. It sets the radio's driver (kiss or spi) with the device, so Edit
radio and Add radio save both.

The setup wizard gets a "LoRa board on SPI or a USB stick" card under the
serial ports; Test modem probes the board and shows the chip's
diagnostics, and finishing sends driver with device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
One lazy radio serves both drivers, so picking a Board in first-time setup
takes effect at once instead of showing the kiss driver until a restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
ModemDeviceField now offers all three ways to reach a radio: a USB KISS
modem on a serial port, meshtasticd's raw modem over TCP (from kiss-tcp),
and a LoRa board the spi driver runs itself. The setup wizard shows the
meshtasticd card and the board card under the serial ports. Picking
meshtasticd or a port sets driver kiss; picking a board sets spi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…uto finds it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A reply left in the adapter by an interrupted session is longer than the next
short read expects, and usbfs answers EOVERFLOW ('value too large for defined
data type'). Read full 32-byte packets, and throw away leftovers when opening.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…ial port by name

experimental.meshtasticd_raw_modem gates radio.device tcp://…, the setup probe and
the meshtasticd choice in the modem pickers, until raw modem mode lands upstream
(meshtastic/firmware#11863). The setup wizard gets a card for typing a serial
port that isn't listed, and Windows COM ports pass the serial-path check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…ial port by name

experimental.meshtasticd_raw_modem gates radio.device tcp://…, the setup probe and
the meshtasticd choice in the modem pickers, until raw modem mode lands upstream
(meshtastic/firmware#11863). The setup wizard gets a card for typing a serial
port that isn't listed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…tible

The same HAT has one meshtasticd file per host (Raspberry Pi, Luckfox Lyra
Zero W, OK3506…) and they all carry the same name, so the dropdown groups by
host and files found on this machine say where they came from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
The raw modem mode isn't going upstream: meshtasticd stays a full node and
RepeaterTastic will drive meshtasticd instances over the client API
instead. Remove the tcp:// KISS transport, the experimental switch, the
meshtasticd choice in the modem pickers and setup wizard, and the guide.
The KISS decoder escape fix and its test stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
@A13xB0
A13xB0 marked this pull request as ready for review September 16, 2026 10:27
@A13xB0

A13xB0 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

This ships as part of the real-nodes epic, #5: epic/real-nodes is built on this branch, so please don't merge this one on its own. It will show as merged when #5 lands on main.

@A13xB0
A13xB0 merged commit 8e35a33 into main Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants