Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,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`.
Expand Down
4 changes: 3 additions & 1 deletion config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,14 @@ location:
# and are not overwritten by gpsd.
# 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.
Expand Down
5 changes: 5 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- **MQTT broker TLS.** Transport TLS (`mqtts`, CA bundle, cert validation) is not implemented on `mqtt_publisher.py` (plain TCP only). Until then use plain port 1883 or a LAN broker without TLS.

- **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).

### v0.7.6 (June 2026)

Meshtastic mesh participant release on `main` (merge `feat/v0.7.6`). Edge-only, pure Python, no concentrator recompile. **Upgrade:** Settings → Updates → **Stable**, or the full SSH block in `docs/COMMON-ERRORS.md` (`git fetch`, `checkout main`, `pull`, `scripts/install.sh`, `restart`). Required this release: new `cryptography` dependency for PKI and an updated `meshpoint.service` unit (RAK V2 reset fix). Pull-only upgrades can miss both. Witness-tested on RAK V2. Settings → Updates RC picker now points at **v0.7.7** on `feat/v0.7.7`.
Expand Down
50 changes: 44 additions & 6 deletions docs/COMMON-ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -430,16 +430,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
Expand Down
35 changes: 32 additions & 3 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 **live GPS fixes**
Expand All @@ -185,7 +190,7 @@ coordinates and mesh position settings hot-reload from the dashboard.
|---|---|
| `static` (default) | No live GPS hardware. Registered coordinates live in `device.*` only. Skyplot shows the static pin. |
| `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). Skyplot and stats update from the live fix. |
| `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. |

### Mesh position broadcasts (LoRa / Meshtastic app map)

Expand Down Expand Up @@ -253,13 +258,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

Three independent surfaces:
Expand Down Expand Up @@ -693,6 +720,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

Expand Down
7 changes: 5 additions & 2 deletions docs/HARDWARE-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -212,8 +213,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).

---

Expand Down
79 changes: 72 additions & 7 deletions frontend/js/configuration/gps_card.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ class GpsConfigCard {
<header class="cfg-card__head">
<h3 class="cfg-card__title">GPS and placement</h3>
<p class="cfg-card__hint">
Registered coordinates go to Meshradar. Choose whether
the LoRa mesh hears your registered pin or a live GPS
fix, with optional privacy rounding on live.
Registered coordinates go to Meshradar. Live fixes from
gpsd (USB), UART (RAK Pi HAT), or static entry feed the
skyplot. Mesh position broadcasts on the LoRa mesh are
configured separately below.
</p>
</header>

Expand Down Expand Up @@ -132,6 +133,42 @@ class GpsConfigCard {
</p>
</fieldset>

<fieldset class="cfg-fieldset" data-uart-fields hidden>
<legend class="cfg-fieldset__legend">UART (on-board GPS)</legend>
<div class="cfg-row">
<label class="cfg-field">
<span class="cfg-field__label">Serial device</span>
<input class="cfg-field__input" type="text"
data-uart-path placeholder="/dev/ttyAMA0">
</label>
<label class="cfg-field cfg-field--narrow">
<span class="cfg-field__label">Baud</span>
<input class="cfg-field__input" type="number"
min="4800" max="115200"
data-uart-baud placeholder="9600">
</label>
</div>
<div class="cfg-row">
<label class="cfg-field cfg-field--narrow">
<span class="cfg-field__label">Update interval (s)</span>
<input class="cfg-field__input" type="number"
min="1" max="300" data-uart-interval>
</label>
<label class="cfg-field cfg-field--narrow">
<span class="cfg-field__label">Min fix quality</span>
<select class="cfg-field__input" data-uart-quality>
<option value="1">1 — accept any reading</option>
<option value="2" selected>2 — require 2D fix</option>
<option value="3">3 — require 3D fix</option>
</select>
</label>
</div>
<p class="cfg-field__hint">
RAK Pi HAT GPS on /dev/ttyAMA0 (install.sh enables UART).
Fix and satellite count update live; full skyplot needs gpsd.
</p>
</fieldset>

<fieldset class="cfg-fieldset" data-mesh-position-fields>
<legend class="cfg-fieldset__legend">Mesh position broadcasts</legend>
<p class="cfg-field__hint">
Expand Down Expand Up @@ -188,6 +225,11 @@ class GpsConfigCard {

this._staticFields = this._root.querySelector('[data-static-fields]');
this._gpsdFields = this._root.querySelector('[data-gpsd-fields]');
this._uartFields = this._root.querySelector('[data-uart-fields]');
this._uartPath = this._root.querySelector('[data-uart-path]');
this._uartBaud = this._root.querySelector('[data-uart-baud]');
this._uartInterval = this._root.querySelector('[data-uart-interval]');
this._uartQuality = this._root.querySelector('[data-uart-quality]');
this._meshLiveChip = this._root.querySelector('[data-mesh-live-chip]');
this._meshPrecisionWrap = this._root.querySelector('[data-mesh-precision-wrap]');
this._meshPrecision = this._root.querySelector('[data-mesh-precision]');
Expand Down Expand Up @@ -233,6 +275,19 @@ class GpsConfigCard {
this._gpsdQuality.value = String(location.min_fix_quality);
}

if (this._uartPath) {
this._uartPath.value = location.uart_path || '/dev/ttyAMA0';
}
if (this._uartBaud) {
this._uartBaud.value = location.uart_baud || 9600;
}
if (this._uartInterval && location.update_interval_seconds) {
this._uartInterval.value = location.update_interval_seconds;
}
if (this._uartQuality && location.min_fix_quality) {
this._uartQuality.value = String(location.min_fix_quality);
}

const position = (config && config.transmit && config.transmit.position) || {};
const meshSource = (position.coordinate_source || 'static').toLowerCase();
const meshRadio = this._root.querySelector(
Expand Down Expand Up @@ -299,8 +354,10 @@ class GpsConfigCard {
_showFieldsetForSource(source) {
if (!this._staticFields || !this._gpsdFields) return;
const isGpsd = source === 'gpsd';
const isUart = source === 'uart';
this._staticFields.hidden = false;
this._gpsdFields.hidden = !isGpsd;
if (this._uartFields) this._uartFields.hidden = !isUart;
}

_updateSourceHint(source) {
Expand All @@ -312,9 +369,8 @@ class GpsConfigCard {
+ 'to the daemon.';
} else if (source === 'uart') {
this._sourceHint.textContent =
'Reserved for the on-board RAK Pi HAT GPS module. Not yet '
+ 'wired in v0.7.5; falls back to the static coordinates '
+ 'on save.';
'Live NMEA from the on-board RAK Pi HAT GPS (/dev/ttyAMA0). '
+ 'Switching source requires a service restart.';
} else {
this._sourceHint.textContent =
'Coordinates are entered manually and stay fixed until '
Expand All @@ -324,7 +380,7 @@ class GpsConfigCard {

_restartPolling(source) {
this._stopPolling();
const interval = source === 'gpsd' ? 2000 : 30000;
const interval = (source === 'gpsd' || source === 'uart') ? 2000 : 30000;
this._pollOnce();
this._timer = window.setInterval(() => this._pollOnce(), interval);
}
Expand Down Expand Up @@ -383,6 +439,15 @@ class GpsConfigCard {
if (intervalRaw) payload.update_interval_seconds = Number(intervalRaw);
const qualityRaw = this._gpsdQuality.value;
if (qualityRaw) payload.min_fix_quality = Number(qualityRaw);
} else if (source === 'uart') {
const path = this._uartPath.value.trim();
if (path) payload.uart_path = path;
const baudRaw = this._uartBaud.value.trim();
if (baudRaw) payload.uart_baud = Number(baudRaw);
const intervalRaw = this._uartInterval.value.trim();
if (intervalRaw) payload.update_interval_seconds = Number(intervalRaw);
const qualityRaw = this._uartQuality.value;
if (qualityRaw) payload.min_fix_quality = Number(qualityRaw);
}

const gpsResult = await this._api.put('/api/config/gps', payload);
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ meshcore>=2.1.0
paho-mqtt>=2.1.0
bcrypt>=4.2.0
PyJWT>=2.10.0
pyserial>=3.5
3 changes: 3 additions & 0 deletions src/api/routes/config_enrichment.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ def enrich_config_payload(cfg: AppConfig, base: dict) -> dict:
base["radio_advanced"] = {
"spectral_scan_interval_seconds": radio.spectral_scan_interval_seconds,
"sx1261_spi_path": radio.sx1261_spi_path or "",
"carrier_type": radio.carrier_type or "",
}
base["location"] = {
"source": location.source,
"gpsd_host": location.gpsd_host,
"gpsd_port": location.gpsd_port,
"uart_path": location.uart_path,
"uart_baud": location.uart_baud,
"update_interval_seconds": location.update_interval_seconds,
"min_fix_quality": location.min_fix_quality,
}
Expand Down
Loading
Loading