From 2d5e8bb67d4be01c04067d2520588b6a328a4879 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Thu, 24 Sep 2026 17:44:44 -0400 Subject: [PATCH] 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) --- README.md | 5 ++- config.example.toml | 20 +++++++++- docs/cli.md | 2 + docs/configuration.md | 89 +++++++++++++++++++++++++++++++++++++++++++ docs/cookbook.md | 2 +- docs/uconsole.md | 5 ++- 6 files changed, 118 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dc488ba..f9770cb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/config.example.toml b/config.example.toml index 29666da..0b4ca41 100644 --- a/config.example.toml +++ b/config.example.toml @@ -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 diff --git a/docs/cli.md b/docs/cli.md index f42ef59..bb82daa 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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//` | 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. | diff --git a/docs/configuration.md b/docs/configuration.md index 9cbe7ce..226f747 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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..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//`, 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. diff --git a/docs/cookbook.md b/docs/cookbook.md index 450757b..8ff2884 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -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. diff --git a/docs/uconsole.md b/docs/uconsole.md index 760ca15..b48a2d2 100644 --- a/docs/uconsole.md +++ b/docs/uconsole.md @@ -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..spi]`: +all. State only what differs, under `[profiles..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]