From 92b1944dd5aac8cb5bfe31905428c2b36a9607a0 Mon Sep 17 00:00:00 2001
From: iceice400
Date: Tue, 2 Jun 2026 13:10:58 -0400
Subject: [PATCH] feat: RAK SX1303 HAL guard, UART onboard GPS, and operator
docs
---
README.md | 2 +
config/default.yaml | 4 +-
docs/CHANGELOG.md | 4 +
docs/COMMON-ERRORS.md | 50 +++++++-
docs/CONFIGURATION.md | 37 +++++-
docs/HARDWARE-MATRIX.md | 7 +-
frontend/js/configuration/gps_card.js | 76 +++++++++++-
requirements.txt | 1 +
src/api/routes/config_enrichment.py | 3 +
src/api/routes/device_config_routes.py | 33 ++++-
src/capture/concentrator_source.py | 3 +
src/cli/setup_wizard.py | 20 ++-
src/config.py | 28 +++--
src/coordinator.py | 5 +-
src/hal/gps_reader.py | 12 +-
src/hal/location/factory.py | 6 +-
src/hal/location/uart_source.py | 137 +++++++++++++++++----
src/hal/sx1302_signatures.py | 4 +
src/hal/sx1302_types.py | 10 ++
src/hal/sx1302_wrapper.py | 75 ++++++++++-
tests/test_location_config.py | 9 +-
tests/test_location_static_uart_sources.py | 39 ++++--
tests/test_sx1302_wrapper_hal_guard.py | 91 ++++++++++++++
tests/test_uart_location_source.py | 110 +++++++++++++++++
24 files changed, 684 insertions(+), 82 deletions(-)
create mode 100644 tests/test_sx1302_wrapper_hal_guard.py
create mode 100644 tests/test_uart_location_source.py
diff --git a/README.md b/README.md
index 407813f..27cc915 100644
--- a/README.md
+++ b/README.md
@@ -247,6 +247,8 @@ See [docs/COMMON-ERRORS.md](docs/COMMON-ERRORS.md#upgrades) if the service fails
**Chip version 0x00:** Concentrator not responding. Check that the concentrator module is seated, SPI is enabled (`raspi-config` → Interface Options → SPI), and try a full power cycle (unplug for 10+ seconds). Normal chip versions are `0x10` (SX1302) and `0x12` (SX1303).
+**`sx1261_check_status` / `lgw_start() failed` on RAK V2:** Clear `radio.sx1261_spi_path` in `local.yaml` (leave it `""`). The SX1261 is not Pi-visible on RAK/SenseCap boards. Use `location.source: uart` for onboard HAT GPS or `gpsd` for USB GPS. See [Common Errors](docs/COMMON-ERRORS.md).
+
**No packets:** Verify antenna is connected and frequency matches your region. Check `meshpoint logs` for `lgw_receive returned N packet(s)`.
**Upstream 401:** Bad API key. Get a free one at [meshradar.io](https://meshradar.io) and re-run `sudo meshpoint setup`.
diff --git a/config/default.yaml b/config/default.yaml
index 2f7e30d..13b4c2e 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -120,12 +120,14 @@ location:
# Where the Meshpoint's reported lat/lon/alt comes from.
# static : use device.latitude/longitude/altitude (default)
# gpsd : poll a local or remote gpsd daemon for live fixes
- # uart : reserved (RAK Pi HAT GPS, not yet wired)
+ # uart : on-board RAK Pi HAT GPS via /dev/ttyAMA0 (NMEA GGA)
source: "static"
# gpsd connection. Defaults match the well-known local socket; only
# change when running gpsd on a peer machine on the LAN.
gpsd_host: "127.0.0.1"
gpsd_port: 2947
+ uart_path: "/dev/ttyAMA0"
+ uart_baud: 9600
# How often the coordinator wakes to read the active source.
update_interval_seconds: 5
# Minimum acceptable fix mode: 0=any (incl. no-fix), 1=2D, 2=3D.
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 7cb7fd8..81dcde7 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,6 +4,10 @@
Queued for the next version bump (v0.7.6 mesh participant RC on `feat/v0.7.6`).
+- **UART GPS.** `location.source: uart` reads NMEA GGA from the RAK Pi HAT (`/dev/ttyAMA0` by default). Configuration → GPS UART fieldset; `meshpoint setup` can select uart when the wizard probe gets a fix. Requires `pyserial` in the venv (`pip install -r requirements.txt` after upgrade).
+- **Concentrator model ID.** Startup logs SX1302 vs SX1303 when `libloragw` exposes `sx1302_get_model_id` (e.g. `Concentrator model ID: 0x12 (SX1303)`).
+- **SX1261 guard.** Non-empty `radio.sx1261_spi_path` on RAK/SenseCap carriers (`radio.carrier_type`) is cleared at configure time with an explicit warning, preventing `lgw_start()` failures from misconfigured spectral scan on Pi-invisible SX1261 wiring.
+- **SPI preflight.** Missing `/dev/spidev0.0` raises a clear error before `lgw_start()` (raspi-config / spi group hints).
- **MQTT broker TLS (deferred).** Transport TLS (`mqtts`, CA bundle, cert validation) is not implemented on `mqtt_publisher.py` (plain TCP only). Planned for v0.7.6. Until then use plain port 1883 or a LAN broker without TLS.
### v0.7.5.1 (May 2026)
diff --git a/docs/COMMON-ERRORS.md b/docs/COMMON-ERRORS.md
index d269f56..df03d58 100644
--- a/docs/COMMON-ERRORS.md
+++ b/docs/COMMON-ERRORS.md
@@ -401,16 +401,54 @@ disabled in `raspi-config`, or the SPI bus latched after a hard power cut.
3. Full power cycle: `sudo poweroff`, wait for green LED to stop, unplug for
10+ seconds, then plug back in.
-Normal chip versions are `0x10` (SX1302) and `0x12` (SX1303).
+Normal chip versions are `0x10` (SX1302) and `0x12` (SX1303). Startup
+also logs `Concentrator model ID: 0x12 (SX1303)` when the HAL exposes
+`sx1302_get_model_id`.
+
+### `sx1261_check_status: got:0x00 expected:0x22` / `failed to patch sx1261`
+
+**Cause:** `radio.sx1261_spi_path` is set on a carrier where the SX1261
+companion chip is **not** wired to a Pi-visible SPI bus (RAK2287, RAK5146,
+SenseCap M1, most RAK Hotspot V2 units). The HAL probes a chip that is not
+there and may refuse `lgw_start()`.
+
+**Fix:**
+
+1. Edit `config/local.yaml` and clear the path:
+
+ ```yaml
+ radio:
+ sx1261_spi_path: ""
+ ```
+
+2. Restart: `sudo systemctl restart meshpoint`.
+
+3. Confirm the journal shows `SX1261 spi_path empty; spectral scan disabled`
+ and `Application startup complete`.
+
+On current Meshpoint builds with `radio.carrier_type: rak` or
+`sensecap_m1` (written by `meshpoint setup`), a mistaken path is cleared
+automatically at startup with a warning. Re-run `sudo meshpoint setup` if
+`carrier_type` is missing.
+
+See [Configuration > Spectral Scan](CONFIGURATION.md#spectral-scan-noise-floor).
### `lgw_start() failed` or `Failed to set SX1250_0 in STANDBY_RC mode`
-**Cause:** SPI bus latch from a hard power cut. The Meshpoint shutdown
-handler holds the concentrator in reset on `sudo reboot` and
-`sudo systemctl restart`, so this only appears after yanked-cable shutdowns,
-breaker trips, or outages.
+**Cause:** Usually one of:
+
+1. **SX1261 misconfiguration** — see the entry above if the log mentions
+ `sx1261_check_status` or `failed to patch sx1261`.
+2. **SPI bus latch** from a hard power cut. The Meshpoint shutdown handler
+ holds the concentrator in reset on `sudo reboot` and
+ `sudo systemctl restart`, so latch typically follows yanked-cable
+ shutdowns, breaker trips, or outages.
+3. **SPI disabled or device missing** — `/dev/spidev0.0` not present;
+ enable SPI in `raspi-config` and confirm the `meshpoint` user is in
+ the `spi` group.
-**Fix:** Full power cycle:
+**Fix:** If SX1261 lines appear in the log, fix `sx1261_spi_path` first.
+Otherwise full power cycle:
```bash
sudo poweroff
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 089403c..825772b 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -26,6 +26,7 @@ radio:
tx_power_dbm: 22 # SX1302 concentrator output power
spectral_scan_interval_seconds: 60 # noise floor sampler cadence (0 disables)
sx1261_spi_path: "" # SX1261 SPI device for spectral scan (empty = disabled)
+ carrier_type: "" # rak | sensecap_m1 (set by meshpoint setup; guards SX1261 path)
```
The region sets the base frequency, spreading factor, and bandwidth automatically. You only need `region` in most cases. Override `frequency_mhz`, `spreading_factor`, or `bandwidth_khz` individually to tune for non-default presets (MediumFast, ShortFast, etc.) or custom frequency slots.
@@ -76,6 +77,8 @@ ERROR: failed to patch sx1261 radio for LBT/Spectral Scan
…and `lgw_start()` may then refuse to bring up the concentrator. If you see that, revert `sx1261_spi_path` to `""`, restart the service, and stay on the packet-derived fallback.
+On RAK and SenseCap carriers, `meshpoint setup` writes `radio.carrier_type`. When that field is `rak` or `sensecap_m1`, Meshpoint **clears** a non-empty `sx1261_spi_path` at startup and logs a warning instead of calling `lgw_sx1261_setconf`.
+
If your `libloragw` build does not expose the spectral scan symbols at all (older HAL revisions), the service logs a single info line at startup and falls back automatically.
### Standard Meshtastic Presets
@@ -169,8 +172,10 @@ location:
source: "static" # static | gpsd | uart
gpsd_host: "127.0.0.1" # gpsd TCP host (only when source=gpsd)
gpsd_port: 2947 # gpsd TCP port
+ uart_path: "/dev/ttyAMA0" # serial device (only when source=uart)
+ uart_baud: 9600 # NMEA baud (uart only)
update_interval_seconds: 5 # how often the coordinator polls the source
- min_fix_quality: 1 # minimum NMEA fix quality (1=2D, 3=3D)
+ min_fix_quality: 1 # minimum fix quality (1=2D, 2=3D for gpsd/uart)
```
`location.source` selects where the Meshpoint reads its current
@@ -183,9 +188,9 @@ changes require a service restart; everything else hot-reloads.
|---|---|
| `static` (default) | Uses `device.latitude` / `device.longitude` / `device.altitude` exactly as set during the wizard. No GPS hardware required. |
| `gpsd` | Reads live fixes from the system `gpsd` daemon over TCP (`127.0.0.1:2947`). Recommended for any USB GPS receiver (u-blox 7, u-blox 8, VFAN puck, generic CDC ACM sticks). |
-| `uart` | Reserved for direct-serial reads from a Pi HAT GPS (e.g. RAK 7248). Currently a placeholder; falls back to static and surfaces an explanatory error in the dashboard. |
+| `uart` | Reads NMEA GGA from the on-board RAK Pi HAT GPS on `/dev/ttyAMA0` (or `uart_path`). Fix and satellite count update live; full skyplot az/el/SNR needs `gpsd` + USB receiver. |
-When `source: gpsd` is active and the daemon has a 2D or 3D fix,
+When `source` is `gpsd` or `uart` and a 2D or 3D fix is available,
the coordinator updates `_config.device.latitude` / `.longitude` /
`.altitude` in place every `update_interval_seconds`. Anything that
reads from `device.*` (NodeInfo broadcasts, MQTT, the dashboard map,
@@ -228,13 +233,35 @@ in `gpsd-clients`) or `gpsmon`.
| u-blox 7 USB stick | USB CDC ACM, NMEA + UBX | yes (RAK V2 .141) |
| u-blox 8 USB stick | USB CDC ACM, NMEA + UBX | yes |
| VFAN ublox 7 USB puck | USB CDC ACM, NMEA + UBX | yes |
-| RAK 7248 onboard u-blox via UART (`/dev/ttyAMA0`) | NMEA over UART | placeholder (`source: uart`, not yet wired) |
+| RAK 7248 onboard u-blox via UART (`/dev/ttyAMA0`) | NMEA over UART | yes (`source: uart`; `install.sh` enables UART) |
Other USB receivers should work as long as `gpsd` recognizes the
device's VID. If `cgps` shows data but the dashboard does not,
check `journalctl -u meshpoint | grep -i gpsd` for connection
errors and confirm `source: gpsd` in `local.yaml`.
+### Using uart (RAK Pi HAT onboard GPS)
+
+`scripts/install.sh` enables the primary UART (`/dev/ttyAMA0`), disables
+the serial console, and turns off Bluetooth on the UART pins so the HAT
+GPS module can talk to the Pi.
+
+1. Run `sudo meshpoint setup` outdoors and accept **Use live UART GPS**
+ when the wizard acquires a fix, **or** set in `local.yaml`:
+
+ ```yaml
+ location:
+ source: "uart"
+ uart_path: "/dev/ttyAMA0"
+ uart_baud: 9600
+ ```
+
+2. Restart: `sudo systemctl restart meshpoint`.
+
+3. Open **Configuration → GPS** → **UART**. Coordinates and satellite
+ count update every few seconds outdoors. The skyplot stays empty
+ until GSV parsing is added (use `gpsd` for a full skyplot today).
+
### Privacy
Live GPS coordinates flow through the same surfaces as the static
@@ -663,6 +690,8 @@ location: # GPS / location source
source: "static" # static | gpsd | uart
gpsd_host: "127.0.0.1"
gpsd_port: 2947
+ uart_path: "/dev/ttyAMA0"
+ uart_baud: 9600
update_interval_seconds: 5
min_fix_quality: 1
diff --git a/docs/HARDWARE-MATRIX.md b/docs/HARDWARE-MATRIX.md
index e06708d..c6dd747 100644
--- a/docs/HARDWARE-MATRIX.md
+++ b/docs/HARDWARE-MATRIX.md
@@ -17,6 +17,7 @@ Application code is plain Python (v0.7.0+); **aarch64** is required.
|---|---|---|---|---|
| **Host** | Pi 4 (SD) | Pi 4 (SD) | CM4 (eMMC) | Pi 4 (SD) |
| **Concentrator** | RAK2287 (SX1302) | WM1303 (SX1303) | SX1302 (onboard) | RAK2287 (SX1302) |
+| **HAL chip version log** | `0x10` (SX1302) | `0x12` (SX1303) | `0x10` (SX1302) | `0x10` (SX1302) |
| **TX support** | Yes (native, with HAL patch) | Yes (native, with HAL patch) | Yes (native, with HAL patch) | Yes (native, with HAL patch) |
| **RX channels** | 8 simultaneous | 8 simultaneous | 8 simultaneous | 8 simultaneous |
| **Spreading factors** | SF7-SF12 simultaneous | SF7-SF12 simultaneous | SF7-SF12 simultaneous | SF7-SF12 simultaneous |
@@ -177,8 +178,10 @@ window-mounted deployments. For better coverage:
GPS antenna (u.FL to SMA pigtail) is optional. If your carrier board has a
u-blox GPS module, plugging in a GPS antenna gives you automatic
-positioning during the setup wizard. Otherwise enter coordinates manually
-(right-click any spot in Google Maps to copy in decimal format).
+positioning during the setup wizard. Set `location.source: uart` for the
+on-board RAK HAT module, or `location.source: gpsd` for a USB GPS stick.
+Otherwise enter coordinates manually (right-click any spot in Google Maps
+to copy in decimal format).
---
diff --git a/frontend/js/configuration/gps_card.js b/frontend/js/configuration/gps_card.js
index cacac0b..969c8ae 100644
--- a/frontend/js/configuration/gps_card.js
+++ b/frontend/js/configuration/gps_card.js
@@ -33,8 +33,8 @@ class GpsConfigCard {
GPS and placement
- Live fix from a USB GPS via gpsd, or static coordinates
- you enter manually. Used by the local map and
+ Live fix from gpsd (USB GPS), UART (RAK Pi HAT), or static
+ coordinates you enter manually. Used by the local map and
Meshradar fleet view.