An SPI radio's GPIO chip is found by its label, not trusted by its number
The AIO's pins sit on whichever /dev/gpiochip carries the Raspberry Pi's 40-pin header, and that number is no fact about the board: chip 0 on a uConsole's CM4, chip 4 on a CM5 under its early kernels, chip 0 once Raspberry Pi aliased the RP1 there, and another number again on some later kernels. gpio_chip now defaults to -1, which asks each chip for its label and takes the one called pinctrl-rp1, pinctrl-bcm2711 or pinctrl-bcm2835, falling back to chip 0 where none is; a number in the profile still pins it. The chip is settled once before the node starts, so the node, every error and the lsof hint all name the same one. The uConsole guide says CM4 and CM5 both work, that ClockworkPi's own A-04, A-06 and R-01 cores are not supported, and explains which chip and why; config.example.toml and the configuration guide say the same, and the guide's test record now names the CM5 it has run on since 09-24. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
05ede530bd
commit
22398128f5
6 files changed
+180
-24
No files matched your search
@@ -66,6 +66,14 @@ description = "Basestation over WiFi"
|
||||
transport = "spi"
|
||||
# [profiles.aio.spi]
|
||||
# en_pins = [27] # the AIO v2 powers its radio from pin 27
|
||||
#
|
||||
# Which /dev/gpiochip<n> carries the pins is not a fact about the board: a uConsole's CM4
|
||||
# has its 40-pin header on chip 0, a CM5 had it on chip 4 under its early kernels, chip 0
|
||||
# once Raspberry Pi aliased it there, and another number again on some later kernels. So
|
||||
# gpio_chip defaults to -1, which finds the chip by its label (pinctrl-rp1 on a CM5,
|
||||
# pinctrl-bcm2711 on a CM4; `gpiodetect` lists them) and falls back to chip 0 where none
|
||||
# carries either. Give it a number only to override that:
|
||||
# gpio_chip = 4
|
||||
|
||||
# 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
|
||||
|
||||
@@ -106,11 +106,16 @@ map one to one:
|
||||
| `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` |
|
||||
| `gpiochip: 4` | usually nothing — see below; `gpio_chip = 4` to pin it |
|
||||
|
||||
`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.
|
||||
`Module` must be `sx1262`; that is the chip MeshTerm's node drives. **Leave `gpio_chip` out**
|
||||
unless you need to override it: a `meshtasticd` file pins a number because Meshtastic can't
|
||||
look the chip up, but the number that carries a Raspberry Pi's 40-pin header differs
|
||||
between a Pi 4/CM4 and a Pi 5/CM5 and has moved between kernel releases on the latter.
|
||||
Left out, it defaults to `-1`, and MeshTerm finds the header's chip by its label
|
||||
(`pinctrl-rp1`, `pinctrl-bcm2711`, `pinctrl-bcm2835`) — falling back to chip `0` on a board
|
||||
whose chips carry none of them. `gpiodetect` lists every chip with its label. The
|
||||
[uConsole guide](uconsole.md#which-gpio-chip) has the whole story.
|
||||
|
||||
**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
|
||||
|
||||
+50
-15
@@ -35,6 +35,7 @@ That's [further down](#always-on-the-bridge) — most people want the section ab
|
||||
- [Step 4 — Connect](#step-4--connect)
|
||||
- [Moving over from the bridge (0.9.0 and earlier)](#moving-over-from-the-bridge-090-and-earlier)
|
||||
- [Board wiring](#board-wiring)
|
||||
- [Which GPIO chip](#which-gpio-chip)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Always on: the bridge](#always-on-the-bridge)
|
||||
- [What this has been tested on](#what-this-has-been-tested-on)
|
||||
@@ -47,7 +48,7 @@ That's [further down](#always-on-the-bridge) — most people want the section ab
|
||||
|
||||
| Part | Notes |
|
||||
| --- | --- |
|
||||
| ClockworkPi **uConsole** (CM4 module) | Any uConsole with a Compute Module 4. |
|
||||
| ClockworkPi **uConsole** with a Raspberry Pi **CM4 or CM5** | Both cores work the same way; MeshTerm finds the GPIO chip behind the header on either (see [Which GPIO chip](#which-gpio-chip)). ClockworkPi's own A-04, A-06 and R-01 cores are not supported. |
|
||||
| hackergadgets **AIO** expansion board | The board this guide is written for and the one we recommend: an SX1262 on SPI bus 1, plus GPS, an RTL-SDR and a USB hub you don't need here. MeshTerm's SPI defaults are its wiring (AIO v1; the v2 needs one extra setting — see [Board wiring](#board-wiring)). |
|
||||
| An antenna for your region's LoRa band | Required. |
|
||||
|
||||
@@ -61,8 +62,8 @@ for that part.
|
||||
Linux-only — driving a radio on the host's own SPI bus is not something Windows or macOS
|
||||
can do.
|
||||
- A `/dev/spidev*` node. This needs `dtoverlay=spi1-1cs` in `/boot/firmware/config.txt`.
|
||||
- `/dev/gpiochip0`, and membership of the `spi` and `gpio` groups so you can read and write
|
||||
both without root.
|
||||
- The GPIO character devices (`/dev/gpiochip*`), and membership of the `spi` and `gpio`
|
||||
groups so you can read and write both without root.
|
||||
- The radio library, **`openhop-core[hardware]` 1.1.3 or newer** — [Step
|
||||
2](#step-2--install-the-radio-library) gets you there. It doesn't have to be in
|
||||
MeshTerm's own Python; MeshTerm looks in several places for it.
|
||||
@@ -99,12 +100,14 @@ sudo reboot
|
||||
After it comes back, check the device nodes exist and that you can use them:
|
||||
|
||||
```bash
|
||||
ls -l /dev/spidev* /dev/gpiochip0
|
||||
ls -l /dev/spidev* /dev/gpiochip*
|
||||
groups
|
||||
```
|
||||
|
||||
You should see `/dev/spidev1.0` (or another `spidev*` node) and `/dev/gpiochip0` listed,
|
||||
and `spi` and `gpio` in your `groups` output.
|
||||
You should see `/dev/spidev1.0` (or another `spidev*` node) and at least one `gpiochip`
|
||||
listed, and `spi` and `gpio` in your `groups` output. Which `gpiochip` carries the radio's
|
||||
pins depends on your core and your kernel; MeshTerm works that out itself (see [Which GPIO
|
||||
chip](#which-gpio-chip)).
|
||||
|
||||
---
|
||||
|
||||
@@ -275,7 +278,7 @@ en_pins = [27] # the AIO v2 needs this
|
||||
| --- | --- | --- |
|
||||
| `bus_id`, `cs_id` | `1`, `0` | Which SPI bus and chip-select the radio is on (`/dev/spidev<bus_id>.<cs_id>`). |
|
||||
| `cs_pin` | `-1` | A GPIO driven as chip select by hand, instead of the bus's own. |
|
||||
| `gpio_chip` | `0` | Which `/dev/gpiochip<n>` the pins below are on. |
|
||||
| `gpio_chip` | `-1` | Which `/dev/gpiochip<n>` the pins below are on. `-1` finds the chip behind the 40-pin header by its label — see [Which GPIO chip](#which-gpio-chip). |
|
||||
| `use_gpiod_backend` | `false` | Drive the pins through `gpiod` instead of `python-periphery`. |
|
||||
| `reset_pin`, `busy_pin`, `irq_pin` | `25`, `24`, `26` | The chip's reset, busy and interrupt (DIO1) lines. |
|
||||
| `txen_pin`, `rxen_pin` | `-1`, `-1` | An external RF switch's transmit/receive-enable lines, if the board has one. |
|
||||
@@ -291,6 +294,38 @@ Radio settings — frequency, bandwidth, spreading factor, coding rate, TX power
|
||||
**not** in this table. They're the node's own, on Device config, as noted in
|
||||
[What you need](#what-you-need).
|
||||
|
||||
### Which GPIO chip
|
||||
|
||||
The AIO's reset, busy and interrupt lines are GPIO 25, 24 and 26 on either core — the pin
|
||||
numbers don't change. What changes is *which* `/dev/gpiochip<n>` Linux puts them on:
|
||||
|
||||
| Core | The header's GPIO controller | Its chip number |
|
||||
| --- | --- | --- |
|
||||
| CM4 | `pinctrl-bcm2711` | `0` |
|
||||
| CM5, early kernels | `pinctrl-rp1` | `4` |
|
||||
| CM5, kernels since Raspberry Pi aliased the RP1 to chip 0 ([raspberrypi/linux#6144](https://github.com/raspberrypi/linux/pull/6144)) | `pinctrl-rp1` | `0` |
|
||||
| CM5, some later kernels | `pinctrl-rp1` | another number again, as the order the drivers load in moved it |
|
||||
|
||||
So a chip number is not a fact about the board. **MeshTerm looks the chip up by its label
|
||||
instead**: with `gpio_chip` left at its default of `-1`, it asks each `/dev/gpiochip<n>` for
|
||||
its label and uses the one called `pinctrl-rp1`, `pinctrl-bcm2711` or `pinctrl-bcm2835`, so
|
||||
the same profile works on a CM4, a CM5, and across kernel updates. Where no chip carries one
|
||||
of those labels, it falls back to chip `0`.
|
||||
|
||||
To see what your system has, `gpiodetect` (from the `gpiod` package) lists every chip with
|
||||
its label:
|
||||
|
||||
```
|
||||
gpiochip0 [pinctrl-rp1] (54 lines)
|
||||
gpiochip10 [gpio-brcmstb@107d508500] (32 lines)
|
||||
…
|
||||
```
|
||||
|
||||
Set `gpio_chip` to a number only to override the lookup — on a board whose header
|
||||
controller has some other label, say. The [bridge](#always-on-the-bridge) does not look
|
||||
chips up by label: on a CM5 it needs `MESHCORE_GPIO_CHIP` set to whatever `gpiodetect` shows
|
||||
for `pinctrl-rp1`, if that isn't `0`.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
@@ -302,7 +337,7 @@ Radio settings — frequency, bandwidth, spreading factor, coding rate, TX power
|
||||
| Permission denied opening the SPI or GPIO device | `sudo usermod -aG spi,gpio $USER`, then log out and back in. |
|
||||
| "the meshterm-spi-bridge service has the radio" | Stop it: `systemctl --user stop meshterm-spi-bridge` (and `disable` it to keep it stopped). |
|
||||
| "meshcore-console has the radio open" | Close the GUI and connect again. |
|
||||
| Some other program has the radio | `sudo lsof /dev/gpiochip0` shows which; only one program may hold the radio at a time. |
|
||||
| Some other program has the radio | The message names the chip: `sudo lsof /dev/gpiochip<n>` on it shows which program; only one may hold the radio at a time. |
|
||||
| The node starts but never answers | Check its log: `~/.meshterm/radio/spidev1.0/node.log`. |
|
||||
| An AIO v2 radio never comes up | It needs `en_pins = [27]` in the profile's `spi` table — see [Board wiring](#board-wiring). |
|
||||
|
||||
@@ -400,7 +435,7 @@ what to do if it fails:
|
||||
| --- | --- | --- |
|
||||
| `node runtime` | Neither `openhop_core` nor `pymc_core` is importable by any interpreter the bridge tried. | Build the venv from [step 1](#bridge-step-1--install-the-radio-runtime), or set `MESHTERM_PYMC_PYTHON` to a python that has one. |
|
||||
| `SPI device` | No `/dev/spidev*` node exists, or it exists but you can't read/write it. | No node at all: add `dtoverlay=spi1-1cs` to `/boot/firmware/config.txt` and reboot. Node present but no permission: `sudo usermod -aG spi $USER`, then log out and back in. |
|
||||
| `GPIO chip` | `/dev/gpiochip0` is missing or not read/write for you. | `sudo usermod -aG gpio $USER`, then log out and back in. |
|
||||
| `GPIO chip` | `/dev/gpiochip0` (or the chip `MESHCORE_GPIO_CHIP` names) is missing or not read/write for you. | `sudo usermod -aG gpio $USER`, then log out and back in. On a CM5, check [which chip](#which-gpio-chip) the header is on. |
|
||||
| `bridge service` / `radio in use` / `port 5000 busy` / `radio is free` | Tells you whether something already holds the radio: the bridge's own boot service, a running `meshcore-console`, or something else already listening on the TCP port. | Only one program may use the radio at a time — see [step 6](#bridge-step-6--connect-meshterm-to-it). If `meshcore-console` is running, close it before running the bridge. |
|
||||
|
||||
If the first three checks all pass, you're ready to run it.
|
||||
@@ -568,7 +603,7 @@ Then `systemctl --user restart meshterm-spi-bridge.service`.
|
||||
| `MESHCORE_SPREADING_FACTOR`, `MESHCORE_BANDWIDTH`, `MESHCORE_CODING_RATE` | the modem preset — all three must match the mesh you are joining. Whole numbers only: bandwidth in Hz (`62500`), coding rate as the denominator (`5` for 4/5). A decimal or `4/5` stops the bridge at startup. |
|
||||
| `MESHCORE_TXEN_PIN`, `MESHCORE_RXEN_PIN`, `MESHCORE_EN_PINS` | the RF-switch and power-enable lines a board may need (`-1` for none; `EN_PINS` is a comma list — the AIO v2 wants `27`) |
|
||||
| `MESHCORE_USE_DIO2_RF`, `MESHCORE_USE_DIO3_TCXO`, `MESHCORE_IS_WAVESHARE` | whether DIO2 drives the RF switch and DIO3 the TCXO (both on for the AIO), and a flag for a different vendor's wiring the runtime knows about |
|
||||
| `MESHCORE_GPIO_CHIP`, `MESHCORE_USE_GPIOD_BACKEND`, `MESHCORE_PREAMBLE_LENGTH` | which gpiochip, whether to drive it through `gpiod`, and the LoRa preamble (leave it unset: the bridge follows MeshCore, 32 symbols up to SF8 and 16 above, and a shorter one leaves the radio deaf to most of the mesh) |
|
||||
| `MESHCORE_GPIO_CHIP`, `MESHCORE_USE_GPIOD_BACKEND`, `MESHCORE_PREAMBLE_LENGTH` | which gpiochip (`0` unless set — the bridge doesn't look it up by label, so on a CM5 see [Which GPIO chip](#which-gpio-chip)), whether to drive it through `gpiod`, and the LoRa preamble (leave it unset: the bridge follows MeshCore, 32 symbols up to SF8 and 16 above, and a shorter one leaves the radio deaf to most of the mesh) |
|
||||
| `MESHTERM_PYMC_PYTHON` | force a specific interpreter instead of letting the bridge discover one |
|
||||
|
||||
The bridge passes a knob only when the runtime's radio constructor accepts it, so the same
|
||||
@@ -597,9 +632,10 @@ install the table above is the whole of it. Either way, an AIO v2 without
|
||||
|
||||
## What this has been tested on
|
||||
|
||||
**The direct connection** has been tested against the real radio library
|
||||
(`openhop-core`) with a simulated radio; it has **not yet been verified on uConsole
|
||||
hardware**.
|
||||
**The direct connection** has run on a bookworm uConsole with a **CM5** and an AIO v1
|
||||
since 2026-09-24, on MeshTerm's defaults. It has also been tested against the real radio
|
||||
library (`openhop-core`) with a simulated radio. A CM4 has not been tried with it; its
|
||||
header chip is found by the same label lookup (see [Which GPIO chip](#which-gpio-chip)).
|
||||
|
||||
**The bridge** has been verified on a bookworm uConsole with `openhop-core` 1.1.3: the
|
||||
radio comes up, contacts are restored across a restart, and `info` and `contacts` answer
|
||||
@@ -609,8 +645,7 @@ live feed. MeshTerm's test suite does not cover the bridge.
|
||||
|
||||
What has not been confirmed:
|
||||
|
||||
- **The direct connection on real hardware** — everything above has only run against a
|
||||
simulated radio under the real library.
|
||||
- **The direct connection on a CM4** — only the CM5 has run it.
|
||||
- **The bridge under `openhop_core` beyond `info` and `contacts`.** Startup, identity,
|
||||
contact restore and those two reads were exercised; a trace, a message and the live feed
|
||||
under the new runtime were not, though the library's own trace push is what the bridge
|
||||
|
||||
@@ -81,7 +81,11 @@ class SpiWiring:
|
||||
bus_id: SPI bus (``/dev/spidev<bus_id>.<cs_id>``).
|
||||
cs_id: SPI chip-select device on that bus.
|
||||
cs_pin: A GPIO driven as chip select by hand, or ``-1`` for the bus's own.
|
||||
gpio_chip: Which ``/dev/gpiochip<n>`` the pins below are on.
|
||||
gpio_chip: Which ``/dev/gpiochip<n>`` the pins below are on, or ``-1`` (the default)
|
||||
to find the Raspberry Pi header's controller by its label — ``pinctrl-bcm2711``
|
||||
on a CM4, ``pinctrl-rp1`` on a CM5 — rather than trust a number that differs
|
||||
between the two and has moved between kernel releases on the CM5 (see
|
||||
:func:`~meshterm.core.spiradio.gpio_chip`). A number pins it.
|
||||
use_gpiod_backend: Drive the pins through ``gpiod`` instead of ``python-periphery``.
|
||||
reset_pin: The chip's reset line.
|
||||
busy_pin: The chip's busy line.
|
||||
@@ -100,7 +104,7 @@ class SpiWiring:
|
||||
bus_id: int = 1
|
||||
cs_id: int = 0
|
||||
cs_pin: int = -1
|
||||
gpio_chip: int = 0
|
||||
gpio_chip: int = -1
|
||||
use_gpiod_backend: bool = False
|
||||
reset_pin: int = 25
|
||||
busy_pin: int = 24
|
||||
|
||||
@@ -40,7 +40,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from pathlib import Path
|
||||
|
||||
from .config import DeviceProfile, SpiWiring
|
||||
@@ -139,6 +139,71 @@ def spi_present(wiring: SpiWiring) -> bool:
|
||||
return _on_linux() and os.path.exists(wiring.spidev)
|
||||
|
||||
|
||||
#: The labels the kernel gives the GPIO controller behind a Raspberry Pi's 40-pin header, in
|
||||
#: the order they are looked for: the RP1 a Pi 5 or CM5 carries, then the BCM2711 of a Pi 4
|
||||
#: or CM4, then the BCM2835 family of the older boards. Its *number* is no fact about the
|
||||
#: board: chip 0 on a CM4, chip 4 on a CM5 under the kernels it shipped with, chip 0 again
|
||||
#: once Raspberry Pi aliased the RP1 there (raspberrypi/linux#6144), and on some later
|
||||
#: kernels another number still, as probe order moved it. The label is what stays put.
|
||||
HEADER_GPIO_LABELS = ("pinctrl-rp1", "pinctrl-bcm2711", "pinctrl-bcm2835")
|
||||
|
||||
#: ``GPIO_GET_CHIPINFO_IOCTL``: ``_IOR(0xB4, 0x01, struct gpiochip_info)``, which fills a
|
||||
#: 68-byte ``{char name[32]; char label[32]; u32 lines}`` for the chip the fd is open on.
|
||||
_GPIO_GET_CHIPINFO_IOCTL = 0x8044B401
|
||||
|
||||
|
||||
def gpio_chip_labels(dev: Path = Path("/dev")) -> dict[int, str]:
|
||||
"""Each ``/dev/gpiochip<n>``'s label as the kernel reports it, by ``n``.
|
||||
|
||||
Asked of the GPIO character device itself, which needs nothing installed and only the
|
||||
read access the radio's pins need anyway. Empty off Linux, and a chip that can't be
|
||||
opened (no ``gpio`` group yet) is simply left out — the radio's own start reports that.
|
||||
"""
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError: # not a POSIX system: no GPIO character devices to ask
|
||||
return {}
|
||||
labels: dict[int, str] = {}
|
||||
for path in dev.glob("gpiochip[0-9]*"):
|
||||
try:
|
||||
number = int(path.name.removeprefix("gpiochip"))
|
||||
fd = os.open(path, os.O_RDONLY)
|
||||
except (ValueError, OSError):
|
||||
continue
|
||||
try:
|
||||
info = bytearray(68)
|
||||
fcntl.ioctl(fd, _GPIO_GET_CHIPINFO_IOCTL, info)
|
||||
except OSError:
|
||||
continue
|
||||
finally:
|
||||
os.close(fd)
|
||||
labels[number] = bytes(info[32:64]).split(b"\0", 1)[0].decode("ascii", "replace")
|
||||
return labels
|
||||
|
||||
|
||||
def header_gpio_chip(labels: Mapping[int, str]) -> int | None:
|
||||
"""The chip carrying a Pi's 40-pin header, by its label (:data:`HEADER_GPIO_LABELS`)."""
|
||||
for wanted in HEADER_GPIO_LABELS:
|
||||
for number in sorted(labels):
|
||||
if labels[number] == wanted:
|
||||
return number
|
||||
return None
|
||||
|
||||
|
||||
def gpio_chip(wiring: SpiWiring) -> int:
|
||||
"""The ``/dev/gpiochip<n>`` the wiring's pins are on.
|
||||
|
||||
The profile's ``gpio_chip`` where it names one. Otherwise the Raspberry Pi header's
|
||||
controller, found by its label — so a uConsole's CM4 and CM5 both work on the AIO's
|
||||
defaults, whatever number the kernel gave the chip this boot — and chip 0 where no
|
||||
controller carries a header label, which is what the default meant before it looked.
|
||||
"""
|
||||
if wiring.gpio_chip >= 0:
|
||||
return wiring.gpio_chip
|
||||
found = header_gpio_chip(gpio_chip_labels())
|
||||
return 0 if found is None else found
|
||||
|
||||
|
||||
def spi_radios(
|
||||
profiles: Mapping[str, DeviceProfile] | None, listed: Iterable[DiscoveredDevice] = ()
|
||||
) -> list[DiscoveredDevice]:
|
||||
@@ -308,7 +373,7 @@ def explain(kind: str, detail: str, wiring: SpiWiring, holder: str | None = None
|
||||
return "meshcore-console has the radio open — close it and connect again"
|
||||
return (
|
||||
"another program has the radio's pins "
|
||||
f"(`sudo lsof /dev/gpiochip{wiring.gpio_chip}` shows which)"
|
||||
f"(`sudo lsof /dev/gpiochip{gpio_chip(wiring)}` shows which)"
|
||||
)
|
||||
if kind == "runtime":
|
||||
return (
|
||||
@@ -321,7 +386,7 @@ def explain(kind: str, detail: str, wiring: SpiWiring, holder: str | None = None
|
||||
"`dtoverlay=spi1-1cs` to /boot/firmware/config.txt) and reboot"
|
||||
)
|
||||
if kind == "no-gpio":
|
||||
return f"/dev/gpiochip{wiring.gpio_chip} does not exist — check `gpio_chip` in the profile"
|
||||
return f"/dev/gpiochip{gpio_chip(wiring)} does not exist — check `gpio_chip` in the profile"
|
||||
if kind == "permission":
|
||||
return (
|
||||
f"{detail} — add yourself to the spi and gpio groups "
|
||||
@@ -482,6 +547,9 @@ async def start_node(wiring: SpiWiring, state: Path, *, node_name: str) -> NodeP
|
||||
"""
|
||||
if not _on_linux():
|
||||
raise SpiRadioError("unsupported", "a radio on the SPI bus needs Linux")
|
||||
# The chip the pins are on, found by label where the profile names none: settled once
|
||||
# here, so the node, every error message and the `lsof` hint all name the same chip.
|
||||
wiring = replace(wiring, gpio_chip=await asyncio.to_thread(gpio_chip, wiring))
|
||||
# Each of these runs a short subprocess or two; none of them belongs on the event loop
|
||||
# that is drawing the "connecting" card.
|
||||
holder = await asyncio.to_thread(radio_holder)
|
||||
|
||||
@@ -308,6 +308,42 @@ def test_no_bridge_means_nothing_carried(tmp_path: Path, monkeypatch: pytest.Mon
|
||||
# --- the error a person reads --------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("labels", "chip"),
|
||||
[
|
||||
({0: "pinctrl-bcm2711", 1: "raspberrypi-exp-gpio"}, 0), # a CM4
|
||||
({0: "gpio-brcmstb@107d508500", 4: "pinctrl-rp1"}, 4), # a CM5 on its first kernels
|
||||
({0: "pinctrl-rp1", 10: "gpio-brcmstb@107d508500"}, 0), # a CM5 after the alias
|
||||
({0: "gpio-brcmstb@107d508500", 15: "pinctrl-rp1"}, 15), # a CM5 whose chip moved
|
||||
({0: "gpio-mockup-A"}, None), # no Raspberry Pi header at all
|
||||
],
|
||||
)
|
||||
def test_the_header_s_gpio_chip_is_found_by_its_label(labels: dict, chip: int | None) -> None:
|
||||
"""The header's controller is known by its label, whatever number this kernel gave it.
|
||||
|
||||
A CM4 and a CM5 put their 40-pin header on chips numbered differently, and on the CM5
|
||||
the number has moved between kernel releases, so the AIO's defaults find it by name.
|
||||
"""
|
||||
assert spiradio.header_gpio_chip(labels) == chip
|
||||
|
||||
|
||||
def test_a_profile_s_gpio_chip_pins_it_and_the_default_looks(monkeypatch) -> None:
|
||||
"""A number in the profile wins; left out, the label decides, and chip 0 is the fallback."""
|
||||
monkeypatch.setattr(spiradio, "gpio_chip_labels", lambda: {15: "pinctrl-rp1"})
|
||||
assert SpiWiring().gpio_chip == -1
|
||||
assert spiradio.gpio_chip(SpiWiring()) == 15
|
||||
assert spiradio.gpio_chip(SpiWiring(gpio_chip=4)) == 4
|
||||
assert "sudo lsof /dev/gpiochip15" in spiradio.explain("busy", "", SpiWiring())
|
||||
monkeypatch.setattr(spiradio, "gpio_chip_labels", lambda: {})
|
||||
assert spiradio.gpio_chip(SpiWiring()) == 0
|
||||
|
||||
|
||||
def test_reading_the_chips_labels_never_fails(tmp_path: Path) -> None:
|
||||
"""Off Linux, or where a chip can't be asked, there is simply nothing to report."""
|
||||
(tmp_path / "gpiochip0").write_bytes(b"") # a plain file: the ioctl can't answer it
|
||||
assert spiradio.gpio_chip_labels(tmp_path) == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("kind", "holder", "says"),
|
||||
[
|
||||
|
||||
Reference in new issue
Block a user