Adding a radio on the SPI bus has a walkthrough of its own

configuration.md now takes a non-AIO board from its meshtasticd config
to a working profile: the key-by-key mapping, a whole example, how to
check it, and what the profile deliberately doesn't hold (radio
settings, the preamble, identity). It warns that a key left out keeps
the AIO's value, unlike meshtasticd where it means off. --spi joins the
option lists in cli.md, the README, and the cookbook, radio/ joins the
state-files table, and config.example.toml carries a full wiring example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-24 17:44:44 -04:00
1 parent dee4674942
commit 2d5e8bb67d
6 files changed
+118 -5

No files matched your search

+4 -1
View File
@@ -216,6 +216,9 @@ meshterm --ble AA:BB:CC:DD:EE:FF info
# Connect over the network to a TCP companion (host[:port], port defaults to 5000)
meshterm --tcp 192.168.1.50 info
# Drive a LoRa radio on this machine's own SPI bus (the uConsole AIO; Linux only)
meshterm --spi info
# No radio attached? Use the built-in simulator for development.
meshterm --mock
@@ -253,7 +256,7 @@ does, and its scripted equivalent where one exists.
## Scripting it
Nearly every command speaks `--json`, and the global options — `--profile/-p`, `--port`,
`--ble`, `--ble-pin`, `--tcp`, `--mock`, `--db`, `--json`, `--absolute`, `--quiet/-q`,
`--ble`, `--ble-pin`, `--tcp`, `--spi`, `--mock`, `--db`, `--json`, `--absolute`, `--quiet/-q`,
`--platform` — may be typed before or after the subcommand.
```bash
+18 -2
View File
@@ -59,9 +59,25 @@ host = "192.168.1.50"
# transport = "tcp"
description = "Basestation over WiFi"
# A radio on this machine's own SPI bus (the uConsole AIO): MeshTerm runs its node.
# The pins default to the AIO v1's; a `spi` table states only what differs.
# A radio on this machine's own SPI bus: MeshTerm runs its node while it is connected.
# The pins default to the uConsole AIO v1's, which needs nothing more than this (and is
# listed on the device screen anyway, whenever /dev/spidev1.0 exists).
[profiles.aio]
transport = "spi"
# [profiles.aio.spi]
# en_pins = [27] # the AIO v2 powers its radio from pin 27
# Any other board states its whole wiring — every pin and both switches, because a key
# left out keeps the AIO's value. Read them off the board's meshtasticd config; the
# values here are an illustration only. docs/configuration.md has the key-by-key mapping.
# [profiles.hat.spi]
# bus_id = 0 # /dev/spidev0.0
# cs_id = 0
# reset_pin = 18
# busy_pin = 20
# irq_pin = 16
# txen_pin = 6
# rxen_pin = -1
# use_dio2_rf = false
# use_dio3_tcxo = false
# python = "/opt/radio/bin/python" # only to pick the Python that has openhop-core
+2
View File
@@ -55,6 +55,7 @@ With no command, MeshTerm launches the interactive menu instead.
| `--ble ADDRESS` | Bluetooth address of a companion; selects the BLE transport. |
| `--ble-pin PIN` | Pairing PIN, if the Bluetooth companion asks for one. |
| `--tcp HOST[:PORT]` | Network address of a TCP companion; selects the TCP transport. Default port 5000. |
| `--spi` | The LoRa radio on this machine's own SPI bus; selects the SPI transport. MeshTerm runs the node for the length of the command. Uses the first SPI profile's wiring, else the uConsole AIO's — see [Adding a radio on the SPI bus](configuration.md#adding-a-radio-on-the-spi-bus). Linux only. |
| `--mock` | Use the built-in simulator instead of real hardware. Nothing transmits, and the run records to `meshterm-mock.db` rather than your real history. |
| `--db PATH` | Use this SQLite database instead of `~/.meshterm/meshterm.db` (or `meshterm-mock.db`, under `--mock`). **It moves the database and nothing else** — see [Where MeshTerm keeps its state](#where-meshterm-keeps-its-state). |
| `--json` | Print the answer as JSON instead of aligned text. |
@@ -104,6 +105,7 @@ of two directories from the same shell.
| `contacts.json`, `channels.json`, `settings.json` | Per-device caches of what the radio last told us, so a screen opens without a round-trip. |
| `adverts.json` | The weekly flood advert's clock: when it was switched on, and when each device's week began. |
| `mutes.json`, `watchtower.json`, `remote.json` | Muted channels; watched nodes and their alerts; per-node remote-admin cache and CLI history. |
| `radio/<spidev>/` | An SPI radio's node, one folder per device node (`radio/spidev1.0/`): its identity key, settings, channels, and contacts, and its log, `node.log`. What firmware would keep in flash. |
| `tilecache/` | Downloaded basemap tiles. |
| `meshterm.log` | The log file. |
| `.lock` | The single-instance lock. |
+89
View File
@@ -70,3 +70,92 @@ Timestamps are stored as UTC and rendered in your local time. Heard packets olde
`history_days` preference are deleted when monitoring starts: once in each menu session,
and at the start of each `meshterm monitor` run. Nothing else is pruned.
## Adding a radio on the SPI bus
Some boards put the LoRa chip straight on the computer's SPI bus, with no firmware in front
of it: the uConsole's AIO board, and LoRa HATs for the Raspberry Pi. MeshTerm drives these
itself — it runs the node while it is connected and lets go of the radio when it quits.
It needs Linux and the radio library; [the uConsole manual](uconsole.md#step-2--install-the-radio-library)
shows how to install that.
**An AIO v1 needs no configuration.** Its wiring is MeshTerm's default, so whenever
`/dev/spidev1.0` exists the device screen lists it as **SPI radio**, and `meshterm --spi`
reaches it. Any other board is added with a profile that states how it is wired.
### 1. Find your board's wiring
The easiest place to find it is the board's `meshtasticd` configuration, which most vendors
publish (usually a `config.yaml` or a `config.d/` fragment with a `Lora:` section). Its keys
map one to one:
| `meshtasticd` (`Lora:`) | MeshTerm (`[profiles.<name>.spi]`) |
| --- | --- |
| `spidev: spidev0.0` | `bus_id = 0` and `cs_id = 0` — the two numbers of `spidevB.C` |
| `CS: 21` | `cs_pin = 21` (only when the board drives chip select from a GPIO) |
| `Reset`, `Busy`, `IRQ` | `reset_pin`, `busy_pin`, `irq_pin` |
| `TXen`, `RXen` | `txen_pin`, `rxen_pin` |
| `DIO2_AS_RF_SWITCH: true` | `use_dio2_rf = true` |
| `DIO3_TCXO_VOLTAGE: true` | `use_dio3_tcxo = true` (1.8 V) |
| `gpiochip: 4` | `gpio_chip = 4` |
`Module` must be `sx1262`; that is the chip MeshTerm's node drives. `gpio_chip` is `0` on
a Compute Module 4 but differs on a Raspberry Pi 5 and a CM5 — `gpiodetect`, or
`ls /dev/gpiochip*`, shows which chip carries the 40-pin header.
**State every pin and both switches.** Where a `meshtasticd` file leaves a key out, it means
"none" or "off". Where the `spi` table leaves a key out, it means *the AIO's value* — IRQ on
26, DIO2 and DIO3 on — which is wrong for almost any other board, and a radio wired wrong
comes up deaf rather than failing.
### 2. Write the profile
In `~/.meshterm/config.toml`, give the radio a name and its wiring. The values below are an
illustration, not any particular board's — take yours from step 1:
```toml
[profiles.hat]
description = "SX1262 HAT on the Pi header"
[profiles.hat.spi]
bus_id = 0 # /dev/spidev0.0
cs_id = 0
reset_pin = 18
busy_pin = 20
irq_pin = 16
txen_pin = 6 # this HAT switches its antenna from a GPIO…
rxen_pin = -1
use_dio2_rf = false # …so DIO2 doesn't
use_dio3_tcxo = false # no TCXO on this board
```
A `spi` table is enough to make it an SPI profile; `transport = "spi"` says so outright and
is what an AIO profile with no table uses. Every key, with the AIO defaults, is in
[the uConsole manual's wiring table](uconsole.md#board-wiring). A key MeshTerm doesn't know,
or a value of the wrong type, stops it at startup with the profile named — a misspelt pin
is never quietly ignored.
### 3. Check it
```bash
meshterm -p hat info
```
The first run starts the node, which mints the radio's identity. If something is wrong, the
error says what to fix — a missing `/dev/spidev*` (enable SPI and reboot), a permission
problem (join the `spi` and `gpio` groups), a missing radio library, or another program
already holding the radio. A node that starts but never answers leaves its log in
`~/.meshterm/radio/spidev0.0/node.log`. Once it answers, the device screen lists the radio
under the profile's name.
### What the profile doesn't hold
- **Radio settings.** Frequency, bandwidth, spreading factor, coding rate, and TX power are
the node's own settings, saved by the node and changed on **Device config** like any
companion's. A new node starts on MeshCore's **US/Canada preset** (910.525 MHz, 62.5 kHz,
SF7, CR 4/5, 22 dBm): anywhere else, change it on Device config before you send anything.
- **The preamble.** The node follows MeshCore's own rule — 32 symbols up to SF8, 16 above —
and a receiver that expects anything else misses most of the mesh. `preamble_length` was
once a wiring key and is refused now; delete the line if an old profile has it.
- **Identity.** The node's key, contacts, and channels live in
`~/.meshterm/radio/<spidev>/`, filed by the device node rather than the profile, so
renaming a profile, or reaching the same radio from the device screen, keeps the same node.
+1 -1
View File
@@ -53,7 +53,7 @@ meshterm preferences set weekly_flood_advert on # flood an advert once a week,
meshterm monitor --seconds 60
```
Global options — `--profile/-p`, `--port`, `--ble`, `--ble-pin`, `--tcp`, `--mock`,
Global options — `--profile/-p`, `--port`, `--ble`, `--ble-pin`, `--tcp`, `--spi`, `--mock`,
`--db`, `--json`, `--absolute`, `--quiet/-q`, `--platform` — may be typed **before or
after** the subcommand: `meshterm contacts --json` and `meshterm --json contacts` are the
same run.
+4 -1
View File
@@ -253,7 +253,10 @@ same two commands.
## Board wiring
The defaults match the hackergadgets uConsole AIO **v1**, which needs no `spi` table at
all. State only what differs, under `[profiles.<name>.spi]`:
all. State only what differs, under `[profiles.<name>.spi]`. For a board that isn't an
AIO — a LoRa HAT on a Raspberry Pi, say — [Adding a radio on the SPI
bus](configuration.md#adding-a-radio-on-the-spi-bus) walks through a whole profile, including
how to read the pins off the board's `meshtasticd` configuration:
```toml
[profiles.aio2]