docs: trim MQTT guide redundancy and split out internals

Port the observer doc cleanup from observer-firmware (4beb5472) onto dev,
keeping dev-specific content intact.

- move internal mechanics into MQTT_INTERNALS.md: deferred construction,
  runtime slot memory, backoff/circuit breaker, message building, command
  namespacing, and the legacy prefs key mapping
- drop the duplicated First-Time Setup, Command Architecture, and SNMP
  Monitoring sections, folding their unique steps into Quick Start
- condense the partition/NVS prose and merge the five custom-broker
  examples into one Custom Brokers section
- promote the preset list to a top-level Broker Presets section with an
  Extra setup column, and document the presets the table was missing:
  meshcore-fi, corecomms, mesh-chaun14, wcmesh, and meshtexas
- fix the coloradomesh port (443, not 1883)
- point flasher and changelog links at observer.gessaman.com
- restore the nbr: status field, which the code still emits, and correct
  the neighbors topic to QoS 0 retained per allow_retain

Dev-specific content is preserved as-is: per-slot packet filters, the Web
Configuration Portal, local testing without hardware, MeshRank's raw
exclusion, and neighbors default_scope.
This commit is contained in:
agessaman
2026-07-30 12:06:18 -07:00
parent 616fe96d1e
commit dd7d13497b
4 changed files with 242 additions and 236 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# MeshCore Observer — Changelog
Changes to the MQTT observer / bridge work that powers the firmware offered on
[flasher.meshcore.io](https://flasher.meshcore.io). Newest changes are at the top.
[observer.gessaman.com](https://observer.gessaman.com/). Newest changes are at the top.
**Legend:** **New** = new capability · **Fix** = bug fix · **Improvement** = enhancement to
existing behavior · **Internal** = refactor / under-the-hood · **Docs** = documentation ·
+172 -234
View File
@@ -4,11 +4,20 @@ This document describes the MQTT bridge implementation that allows MeshCore repe
## Quick Start Guide
**Prerequisites:** a MeshCore device flashed with observer MQTT firmware, WiFi network
credentials, and console access — either serial (115200 baud) or repeater login via the
companion app.
### Essential Commands to Get MQTT Observer Running
**1. Flash the observer firmware to your device**
Use one of the observer build targets (e.g., `heltec_v4_repeater_observer_mqtt`). After flashing, connect to the device console via serial (115200 baud) or repeater login.
The easiest route is the [MeshCore Observer Flasher](https://observer.gessaman.com/) — pick
**MQTT Observer Firmware**, select your device, and flash from the browser (Chrome or Edge).
To build it yourself instead, use one of the observer build targets (e.g.
`heltec_v4_repeater_observer_mqtt`) — see [Build Configuration](#build-configuration).
After flashing, connect to the device console via serial (115200 baud) or repeater login.
**2. Configure radio settings**
@@ -39,21 +48,39 @@ set wifi.ssid YourWiFiNetwork
set wifi.pwd YourWiFiPassword
```
**5. (Optional) Disable packet repeating**
**5. (Optional) Choose which brokers to publish to**
Slots 1 and 2 default to Let's Mesh Analyzer US and EU. To add or change a broker, pick a
name from [Broker Presets](#broker-presets):
```bash
set mqtt3.preset meshmapper
```
**6. (Optional) Configure timezone**
```bash
set timezone America/New_York
```
Or use a plain offset as a fallback: `set timezone.offset -5`. Published timestamps are
always UTC either way — see [Timezone Commands](#timezone-commands).
**7. (Optional) Disable packet repeating**
If this observer is receive-only (e.g., using a PCB antenna in a location where repeating would be harmful), disable forwarding:
```bash
set repeat off
```
**6. Reboot to connect**
**8. Reboot to connect**
```bash
reboot
```
**7. Verify configuration**
**9. Verify configuration**
```bash
get wifi.ssid
get wifi.status
get bridge.enabled
get mqtt.rx
get mqtt.tx
@@ -76,63 +103,74 @@ get mqtt.status
## Overview
The MQTT bridge implementation provides:
- Up to 6 MQTT connection slots with built-in presets
- Built-in presets for many community brokers (see the [preset table](#slot-based-preset-system) for the full list)
- Custom broker support with username/password authentication
- JWT (Ed25519 device signing) authentication for most preset brokers; TennMesh uses a fixed username/password (plain MQTT)
- WSS (WebSocket Secure), direct MQTT/TLS, and plain MQTT (TennMesh) transport
- Up to 6 MQTT connection slots, each holding a built-in preset for a community broker or a custom broker of your own — see [Broker Presets](#broker-presets)
- Per-preset authentication over WSS, MQTT/TLS, or plain MQTT: Ed25519-signed JWT, fixed or per-slot username/password, or none — see [Authentication](#authentication)
- Automatic reconnection with exponential backoff
- JSON message formatting for status, packets, and raw data
- JSON message formatting for status, packet, raw, and neighbors data
- Packet queuing during connection issues
- Automatic migration from old configuration format
## Architecture
## Broker Presets
### Slot-Based Preset System
Each of the 6 slots holds one preset. Point a slot at a community broker with:
The MQTT bridge uses a slot-based architecture with up to 6 concurrent connections. Each slot can be configured with a built-in preset or custom broker settings.
```bash
set mqtt3.preset meshmapper # slot 3 → MeshMapper
```
**Built-in Presets:**
Most presets need nothing else — the broker address, transport, and credentials all ship
in the firmware. The **Extra setup** column below lists the exceptions. Presets using the
`meshcore/{iata}/…` topic layout (every built-in except `meshrank`) also need `set mqtt.iata`.
| Preset | Server | Auth | Transport |
|--------|--------|------|-----------|
| `analyzer-us` | mqtt-us-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
| `analyzer-eu` | mqtt-eu-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
| `nz-analyzer` | meshcore-mqtt-1.baird.io:443 | JWT (Ed25519) | WSS |
| `meshmapper` | mqtt.meshmapper.net:443 | JWT (Ed25519) | WSS |
| `meshrank` | meshrank.net:8883 | None (token in topic) | MQTT over TLS |
| `waev` | mqtt.waev.app:443 | JWT (Ed25519) | WSS |
| `meshomatic` | us-east.meshomatic.net:443 | JWT (Ed25519) | WSS |
| `cascadiamesh` | mqtt-v1.cascadiamesh.org:443 | JWT (Ed25519) | WSS |
| `tennmesh` | mqtt.tennmesh.com:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `nashmesh` | mqtt://mqtt.nashme.sh:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `ctmesh` | mqtt.ctmesh.org:1883 | Username/password (fixed in firmware) | Plain MQTT |
| `chimesh` | wss://mqtt.chimesh.org:443 | JWT (Ed25519) | WSS |
| `meshat.se` | meshcore-mqtt.meshat.se:443 | JWT (Ed25519) | WSS |
| `eastidahomesh` | mqtt://live.eastidahomesh.com:1883 | None | Plain MQTT |
| `coloradomesh` | wss://mqtt.meshcore.coloradomesh.org:1883 | JWT (Ed25519) | WSS |
| `dutchmeshcore-1` | collector1.dutchmeshcore.nl:443 | JWT (Ed25519) | WSS |
| `dutchmeshcore-2` | collector2.dutchmeshcore.nl:443 | JWT (Ed25519) | WSS |
| `meshcore-ca-1` | mqtt1.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `meshcore-ca-2` | mqtt2.meshcore.ca:443 | JWT (Ed25519) | WSS |
| `bostonmesh` | mqttmc01.bostonme.sh:443 | JWT (Ed25519) | WSS |
| `ipnt.uk` | mqtt.ipnt.uk:443 | JWT (Ed25519) | WSS |
| `flmesh` | mcmqtt.jntconnections.com:443 | JWT (Ed25519) | WSS |
| `inwmesh` | scope.inwmesh.org:8883 | Username/password (per slot via `mqttN.username` / `mqttN.password`) | MQTT over TLS |
| `rflab` | mqtt.rflab.io:443 | JWT (Ed25519) | WSS |
| `custom` | User-configured | Username/Password | MQTT or WSS |
| `none` | (disabled) | — | — |
Run `get mqtt.presets` on the device for the list this firmware actually ships; the table
below documents the current build.
**Default Configuration:**
- Slot 1: `analyzer-us`
- Slot 2: `analyzer-eu`
- Slots 3-6: `none`
| Preset | Broker | Auth | Extra setup |
|--------|--------|------|-------------|
| `analyzer-us` | `wss://mqtt-us-v1.letsmesh.net:443/mqtt` | JWT | — (default slot 1) |
| `analyzer-eu` | `wss://mqtt-eu-v1.letsmesh.net:443/mqtt` | JWT | — (default slot 2) |
| `nz-analyzer` | `wss://meshcore-mqtt-1.baird.io:443` | JWT | — |
| `meshmapper` | `wss://mqtt.meshmapper.net:443/mqtt` | JWT | — |
| `meshrank` | `mqtts://meshrank.net:8883` | None (token in topic) | `set mqttN.token <token>` |
| `waev` | `wss://mqtt.waev.app:443/mqtt` | JWT | — |
| `meshomatic` | `wss://us-east.meshomatic.net:443/mqtt` | JWT | — |
| `cascadiamesh` | `wss://mqtt-v1.cascadiamesh.org:443/mqtt` | JWT | — |
| `tennmesh` | `mqtt://mqtt.tennmesh.com:1883` | User/pass (in firmware) | — |
| `nashmesh` | `mqtt://mqtt.nashme.sh:1883` | User/pass (in firmware) | — |
| `ctmesh` | `mqtt://mqtt.ctmesh.org:1883` | User/pass (in firmware) | — |
| `chimesh` | `wss://mqtt.chimesh.org:443` | JWT | — |
| `meshat.se` | `wss://meshcore-mqtt.meshat.se:443` | JWT | — |
| `eastidahomesh` | `mqtt://live.eastidahomesh.com:1883` | None | — |
| `coloradomesh` | `wss://mqtt.meshcore.coloradomesh.org:443` | JWT | — |
| `dutchmeshcore-1` | `wss://collector1.dutchmeshcore.nl:443/mqtt` | JWT | — |
| `dutchmeshcore-2` | `wss://collector2.dutchmeshcore.nl:443/mqtt` | JWT | — |
| `meshcore-ca-1` | `wss://mqtt1.meshcore.ca:443/mqtt` | JWT | — |
| `meshcore-ca-2` | `wss://mqtt2.meshcore.ca:443/mqtt` | JWT | — |
| `meshcore-fi` | `wss://mc-mqtt.meshcore.fi:443/` | JWT | — |
| `inwmesh` | `mqtts://scope.inwmesh.org:8883` | User/pass (per slot) | `set mqttN.username` + `set mqttN.password` |
| `bostonmesh` | `wss://mqttmc01.bostonme.sh:443/mqtt` | JWT | — |
| `rflab` | `wss://mqtt.rflab.io:443` | JWT | — |
| `ipnt.uk` | `wss://mqtt.ipnt.uk:443` | JWT | — |
| `flmesh` | `wss://mcmqtt.jntconnections.com:443` | JWT | — |
| `corecomms` | `wss://mqtt.corecomms.net:443/mqtt` | JWT | — |
| `meshtexas` | `wss://mqtt.meshtexas.org:443/mqtt` | JWT | — |
| `mesh-chaun14` | `mqtt://mqtt.mesh.chaun14.fr:1884` | User/pass (username is the device public key) | `set mqttN.password` |
| `wcmesh` | `wss://mqtt.wcmesh.com:443` | JWT | — |
| `custom` | your own broker | User/pass, or JWT when `mqttN.audience` is set | `set mqttN.server` (see [custom broker setup](#custom-brokers)) |
| `none` | (slot disabled) | — | — |
**Memory Limits:**
- With PSRAM: All slots can be active simultaneously
- Without PSRAM: Maximum 2 active TLS/WSS slots (each WSS/TLS connection requires ~40KB internal heap)
- If more slots are configured than the device supports, excess slots show as `(inactive)` in `get mqtt.status`
- Slot configurations are preserved in preferences — moving firmware to a PSRAM device activates all slots
Transport is the URL scheme: `wss://` is WebSocket Secure, `mqtts://` is MQTT over TLS,
and `mqtt://` is plain unencrypted MQTT. The two TLS schemes are what count against the
non-PSRAM slot limit below.
### Slots and Memory Limits
Fresh installs default to slot 1 `analyzer-us`, slot 2 `analyzer-eu`, and slots 36 `none`.
- **With PSRAM:** all 6 slots can be active simultaneously
- **Without PSRAM:** maximum 2 active TLS/WSS slots (each WSS/TLS connection requires ~40KB internal heap)
- Slots configured beyond what the device supports show as `(inactive)` in `get mqtt.status`
- Slot configuration is preserved in preferences — moving the firmware to a PSRAM device activates the rest
## Build Configuration
@@ -171,25 +209,30 @@ Some MQTT observer builds use a non-default partition table to accommodate the l
|-------------|----------------|------------|---------------|-------|
| `LilyGo_T3S3_sx1262_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
| `LilyGo_T3S3_sx1262_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | Changed from default (1.25 MB) |
| `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | TTGO LoRa32 V1.0; observer omits `sensor_base`. **One active WSS broker** recommended (no PSRAM; dual TLS usually fails on the second slot). |
| `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | TTGO LoRa32 V1.0; observer omits `sensor_base`. See the one-WSS-broker note above. |
| `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` | `min_spiffs.csv` | 4 MB | 1.875 MB | same |
| `Station_G2_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
| `Station_G2_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board |
| `LilyGo_TBeam_1W_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | Set in `boards/t_beam_1w.json`; required vs implicit `default.csv` |
| `LilyGo_TBeam_1W_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | same |
**NVS / settings when the partition layout changes**
**Settings loss when the layout changes**
Flashing a **full merged image** (`*-merged.bin` at offset `0x0`) writes a new bootloader **and** partition table. If that layout **differs** from what is already on the device, **NVS is typically wiped or invalidated** — expect to lose stored configuration (admin preferences, WiFi, MQTT slots, name, etc.) and reconfigure from scratch.
A merged image writes a new bootloader **and** partition table at offset `0x0`. If that
layout differs from what the device already has, **NVS is typically wiped** — expect to lose
stored configuration (admin preferences, WiFi, MQTT slots, name) and reconfigure from
scratch. If the layout is unchanged, NVS is usually retained, though Bluetooth pairings may
still be cleared on some upgrade paths.
- **`LilyGo_TLora_V2_1_1_6_*_observer_mqtt`:** These use the **same** `min_spiffs.csv` layout as other MeshCore TLora builds, so moving between repeater / room server / MQTT observer does **not** require a different partition table for normal upgrades. **If you previously installed an older TLora MQTT observer that used `huge_app.csv`,** flashing this firmware switches back to `min_spiffs` — treat that as a **partition layout change** (merged flash; NVS may be reset). **If you install MeshCore on a device that used a non-MeshCore partition map,** the first merged flash can still **wipe** settings.
- **`Station_G2_*_observer_mqtt`** and **`LilyGo_TBeam_1W_*_observer_mqtt`**: These use `default_16MB.csv` to accomodate the larger size of the MQTT observer firmware. Installing MQTT observer firmware on these devices requires a **merged** flash the first time. The same applies if you move **from** firmware that was built with a **different** partition table—the first merged flash that installs this layout will **wipe** stored settings.
Two cases catch people out: the TLora observer builds use the same `min_spiffs.csv` as other
MeshCore TLora builds, so normal repeater ↔ room server ↔ observer moves are fine — but
coming *from* an older TLora observer that used `huge_app.csv` is a layout change. And any
device previously running a non-MeshCore partition map will be wiped by its first merged
flash regardless of target.
**How to flash the merged firmware:**
You can flash the merged firmware using either the web flasher or the command line:
- **Web flasher (recommended):** Use the [MeshCore Web Flasher](https://meshcore.io/flasher) to flash the `*-merged.bin` file directly from your browser — no tools to install.
- **Web flasher (recommended):** Use the [MeshCore Observer Flasher](https://observer.gessaman.com/) to flash from your browser — no tools to install. Pick **MQTT Observer Firmware** and your device. Its Download menu also serves the individual `*-merged.bin`, erase, and bootloader files if you would rather flash with something else. Requires Chrome or Edge.
- **Command line:**
```bash
# Build the merged binary
@@ -199,8 +242,6 @@ You can flash the merged firmware using either the web flasher or the command li
esptool.py write_flash 0x0 .pio/build/LilyGo_T3S3_sx1262_repeater_observer_mqtt/firmware-merged.bin
```
> **Note:** If the **partition layout is unchanged** (e.g. updating the MQTT observer build in place), device configuration in NVS is usually retained; Bluetooth pairings may still be cleared on some upgrade paths. If the **partition table is new to the device**, see **NVS / settings when the partition layout changes** above — stored settings are typically lost. After the first merged flash **for a given layout**, subsequent updates on that board can use OTA or the standard non-merged binary when applicable.
### Build Flags
- `WITH_MQTT_BRIDGE=1` - Enable MQTT bridge (required)
- `WITH_SNMP=1` - Enable SNMP agent (optional, see [MQTT_SNMP.md](MQTT_SNMP.md))
@@ -276,9 +317,7 @@ Each slot (1-6) supports the following commands:
- `get mqttN.filter` - Get the slot's packet-type allowlist (`all`, `none`, or numeric CSV)
#### Set Commands
- `set mqttN.preset <name>` - Set slot N to a built-in preset. Use any `name` from the [preset table](#slot-based-preset-system) (run `get mqtt.presets` on-device for the full list). Most presets need no further configuration; the exceptions are:
- `meshrank` - requires a per-slot token (`set mqttN.token <token>`)
- `inwmesh` - requires per-slot credentials (`set mqttN.username` / `set mqttN.password`)
- `set mqttN.preset <name>` - Set slot N to a built-in preset. Use any `name` from [Broker Presets](#broker-presets), which also lists the few presets needing extra setup.
- `set mqttN.preset custom` - Set slot N to custom broker (configure server/port/username/password)
- `set mqttN.preset none` - Disable slot N
- `set mqttN.server <hostname>` - Set custom server hostname for slot N
@@ -354,20 +393,27 @@ losing the stored WiFi credentials along with the broker config. Slots left at
the `all` default keep the file in the shorter layout those builds can read, so
if you may need to roll a node back that far, reset every slot to `all` first.
#### Example: Configure MeshRank on Slot 3
#### Example: MeshRank
MeshRank needs an account token, generated on the MeshRank website and tied to your account:
```bash
set mqtt3.preset meshrank
set mqtt3.token FE1B34242C5938C39225310081FD6718
```
The token is generated on the MeshRank website and is tied to your account. MeshRank receives status, packets, and neighbors under `meshrank/uplink/{token}/{device}/`, using the same type suffixes as the MeshCore layout. Raw is **not** sent to MeshRank — it is the highest-volume topic and the broker does not consume it — so `set mqtt.raw on` has no effect on a MeshRank slot.
It receives status, packets, and neighbors under `meshrank/uplink/{token}/{device}/`, using
the same type suffixes as the MeshCore layout. Raw is **not** sent to MeshRank — it is the
highest-volume topic and the broker does not consume it — so `set mqtt.raw on` has no effect
on a MeshRank slot. Its broker does not accept the retain flag, so those publishes go out
unretained.
#### Example: Configure MeshMapper on Slot 3
```bash
set mqtt3.preset meshmapper
```
### Custom Brokers
#### Example: Configure Custom Broker on Slot 3
Set the preset to `custom` and supply the broker address, plus credentials in whichever style
the broker expects.
**Username/password:**
```bash
set mqtt3.preset custom
set mqtt3.server your-broker.example.com
@@ -376,46 +422,35 @@ set mqtt3.username your-username
set mqtt3.password your-password
```
#### Example: Custom Broker with JWT Authentication (Ed25519)
For community brokers that support the MeshCore JWT auth protocol (same as the built-in presets), set the `audience` field to enable Ed25519-signed JWT authentication:
**Ed25519 JWT** — for community brokers implementing the same JWT auth protocol as the
built-in presets. Setting `audience` is what switches the slot to JWT:
```bash
set mqtt3.preset custom
set mqtt3.server wss://my-broker.example.com:443/mqtt
set mqtt3.audience my-broker.example.com
```
When the server is given as a full URL with a scheme (`mqtt://`, `mqtts://`, `ws://`, `wss://`), `set mqttN.port` is optional — an explicit port in the URL is used as-is, and without one the scheme's default port applies.
With `audience` set, the device connects as `v1_{PUBLIC_KEY}` with an Ed25519-signed JWT as
the password, renews tokens before expiry (default 24h lifetime), and includes the owner
public key and email in the JWT payload if `set mqtt.owner` / `set mqtt.email` are
configured. Clear it with a bare `set mqtt3.audience` to revert to username/password.
When `audience` is set, the device will:
- Connect with username `v1_{PUBLIC_KEY}` and an Ed25519-signed JWT as the password
- Automatically renew tokens before expiry (default 24h lifetime)
- Include owner public key and email in the JWT payload (if configured via `set mqtt.owner` / `set mqtt.email`)
To revert a slot back to username/password auth, clear the audience:
```bash
set mqtt3.audience
```
#### Example: Local Development Broker (plain WebSocket, no TLS)
For local development (e.g. a LAN broker without SSL termination), use a full `ws://` URL. Non-TLS transports (`ws://`, `mqtt://`) skip certificate verification entirely:
**Local development broker** — a LAN broker with no SSL termination. Non-TLS transports
(`ws://`, `mqtt://`) skip certificate verification entirely:
```bash
set mqtt3.preset custom
set mqtt3.server ws://192.168.1.50:9001/mqtt
set mqtt3.audience my-local-broker
```
The `audience` line is optional — set it if your local broker uses the same JWT auth as the production presets, or use `set mqtt3.username` / `set mqtt3.password` instead.
#### Example: Custom Broker with Custom Topic Template
**Custom topic layout** — see [Custom Topic Templates](#custom-topic-templates) below:
```bash
set mqtt3.preset custom
set mqtt3.server my-broker.local
set mqtt3.port 1883
set mqtt3.topic mynetwork/{device}/{type}
```
When the server is given as a full URL with a scheme (`mqtt://`, `mqtts://`, `ws://`,
`wss://`), `set mqttN.port` is optional — an explicit port in the URL is used as-is, and
without one the scheme's default port applies.
### Custom Topic Templates
When a slot's preset is `custom`, you can define a custom topic template using placeholders:
@@ -440,7 +475,7 @@ These settings apply across all MQTT slots:
- `get mqtt.iata` - Get IATA code
- `get mqtt.presets` - List available MQTT presets (paginated, comma-separated)
- `get mqtt.presets <start>` - Continue list from index shown in `... next:<idx>`
- `get mqtt.status` - Get MQTT status summary (connection info per slot)
- `get mqtt.status` - Get MQTT status summary (connection info per slot, plus the periodic neighbors schedule when `mqtt.neighbors` is on)
- `get mqtt.packets` - Get packet message setting (on/off)
- `get mqtt.raw` - Get raw message setting (on/off)
- `get mqtt.rx` - Get RX packet uplinking setting (on/off)
@@ -530,8 +565,15 @@ These are standard MeshCore commands, not MQTT-specific, but important for obser
- `set bridge.source rx|tx` - Set packet source (rx for received, tx for transmitted)
- `set bridge.enabled on|off` - Enable/disable bridge
> **Note:** `bridge.enabled` is the master switch for the whole bridge system. `bridge.source`
> applies to non-MQTT bridges (RS232, ESP-NOW) only — for MQTT use `mqtt.rx` and `mqtt.tx`,
> which control each direction independently.
### SNMP Commands
Observer nodes include an optional SNMP v2c agent that exposes radio stats, MQTT
connectivity, memory usage, and network information to standard monitoring tools.
#### Get Commands
- `get snmp` - Get SNMP agent status (on/off)
- `get snmp.community` - Get SNMP community string
@@ -540,7 +582,7 @@ These are standard MeshCore commands, not MQTT-specific, but important for obser
- `set snmp on|off` - Enable/disable SNMP agent (restart required)
- `set snmp.community <string>` - Set SNMP community string (restart required, default: `public`)
See [MQTT_SNMP.md](MQTT_SNMP.md) for full SNMP documentation.
See [MQTT_SNMP.md](MQTT_SNMP.md) for setup and the full OID reference.
### Web Configuration Portal
@@ -622,29 +664,13 @@ Two ways to iterate on observer/WiFi functionality without flashing a device:
Backend handler logic is covered by host unit tests under `test/` (`pio test -e
native`); see [test/README.md](test/README.md) for the suites and how to run them.
## Command Architecture
The CLI commands are organized into two levels:
### Bridge Commands (`bridge.*`)
**Low-level bridge control** - These settings apply to all bridge types (MQTT, RS232, ESP-NOW, etc.):
- `bridge.enabled` - Master switch for the entire bridge system
- `bridge.source` - Controls which packet events to capture for non-MQTT bridges (RS232, ESP-NOW). For MQTT, use `mqtt.rx` and `mqtt.tx` instead.
### Bridge-Specific Commands (`mqtt.*`, `mqttN.*`, `wifi.*`, `timezone.*`)
**Implementation-specific settings** - These only apply to the MQTT bridge:
- `mqtt.rx` / `mqtt.tx` - Independent per-direction packet uplinking control
- `mqttN.*` - Per-slot MQTT broker configuration (N = 1-6)
- `mqtt.*` - Shared MQTT settings (message types, origin, IATA, etc.)
- `wifi.*` - WiFi connection settings for MQTT connectivity
- `timezone.*` - Timezone configuration for accurate timestamps
## MQTT Topics
The bridge publishes to three main topics with the following structure:
The bridge publishes to four main topics with the following structure:
### Status Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/status`
Device connection status and metadata (retained messages).
Device connection status and metadata, QoS 1. Retained, except on presets whose broker rejects the retain flag (`meshrank`, `waev`).
### Packets Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/packets`
Full packet data with RF characteristics and metadata.
@@ -653,12 +679,15 @@ Full packet data with RF characteristics and metadata.
Minimal raw packet data for map integration.
### Neighbors Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/neighbors`
Periodic snapshot of this node's zero-hop neighbor table plus each neighbor's
region scopes (PSRAM boards only; disabled by default). Published non-retained at
QoS 1 on the interval set by `mqtt.neighbors.interval` (12336 h, default 24 h).
MeshRank slots receive this topic too, as `meshrank/uplink/{token}/{device}/neighbors`.
Cached zero-hop repeater neighbors with SNR, last-heard age, and flood-allowed scopes. Published on `discover.scopes` or periodically when `mqtt.neighbors` is enabled (PSRAM observer builds only). Goes to every configured slot's `neighbors` topic at QoS 0, retained only where the preset allows it.
**Note**: `{DEVICE_PUBLIC_KEY}` is the device's public key in hexadecimal format (64 characters).
Periodic publishing first runs a 60-second zero-hop neighbor refresh equivalent to `discover.neighbors`, then queries the refreshed table for scopes and publishes when the scope-query phase completes.
Manual `discover.scopes` normally queries the current cache in one shot. If a `discover.neighbors` refresh is already collecting responses — whether started from the CLI or by the periodic timer — the scope queries are queued behind its 60-second window instead, so they run against the refreshed table. The reply reports the wait, e.g. `OK - scopes queued (47s discovery remaining)`. A queued one-shot request survives `set mqtt.neighbors off`; only the periodic timer's own refresh is cancelled by it.
While `mqtt.neighbors` is on, `get mqtt.status` appends `nbr: <next>/<last>` — time to the next automatic publish (`3h12m`, `12m`, `45s`, or `active`/`due`) and the last publish result (`ok`, `failed`, or `none`).
**Note**: `{DEVICE_PUBLIC_KEY}` is the device's public key in hexadecimal format (64 characters). MeshRank slots use `meshrank/uplink/{token}/{DEVICE_PUBLIC_KEY}/neighbors` instead.
## JSON Message Formats
@@ -722,7 +751,7 @@ MeshRank slots receive this topic too, as `meshrank/uplink/{token}/{device}/neig
- All numeric fields (`len`, `packet_type`, `payload_len`, `SNR`, `RSSI`, `score`) are formatted as JSON strings.
- `time` and `date` are always UTC (`HH:MM:SS` and `DD/MM/YYYY`); `timestamp` is UTC with an explicit `+00:00` offset.
- `SNR`, `RSSI`, and `score` are only present for RX packets (received from radio). TX packets omit these fields since the packet originates from this node.
- `score` is the firmware's rebroadcast score for the received packet (the same value used to compute flood-rebroadcast delay), scaled ×1000 to match the integer printed in the serial RX log — e.g. a score of `0.234` is emitted as `"234"` (range `0``1000`). It is recomputed at publish time from the packet's SNR and length via the radio's `packetScore()`, so it matches what the firmware used on receive. Omitted when unavailable (e.g. the non-PSRAM reconstruction-less fallback path).
- `score` is the firmware's rebroadcast score for the received packet (the same value used to compute flood-rebroadcast delay), scaled ×1000 to match the integer printed in the serial RX log — e.g. a score of `0.234` is emitted as `"234"` (range `0``1000`). Omitted when unavailable (e.g. the non-PSRAM reconstruction-less fallback path).
- `path` is only present for direct-route packets that carry path data. It is a JSON array of lowercase hex hop tokens, one element per hop — e.g. `["aa","bb","cc"]` for single-byte hashes, or `["aaaa","bbbb"]` for multi-byte hashes. This matches the `path` representation emitted by [meshcore-packet-capture](https://github.com/agessaman/meshcore-packet-capture).
### Raw Message
@@ -736,7 +765,7 @@ MeshRank slots receive this topic too, as `meshrank/uplink/{token}/{device}/neig
}
```
### Neighbors Message
### Neighbors Message (PSRAM observer builds)
```json
{
"timestamp": "2024-01-01T12:00:00.000000+00:00",
@@ -763,29 +792,20 @@ the radio actually performs in that case.
## Key Features
### Slot-Based Preset System
- Up to 6 concurrent MQTT connections (with PSRAM), 2 without PSRAM
- Built-in presets for many community brokers (see the [preset table](#slot-based-preset-system))
- Custom broker support with username/password auth and custom topic templates
- JWT (Ed25519) for most preset brokers; MeshRank uses token-in-topic; TennMesh uses fixed username/password over plain MQTT
- WSS (WebSocket Secure), direct MQTT over TLS, and plain MQTT (TennMesh)
- Automatic reconnection with exponential backoff per slot
- Circuit breaker pattern with periodic probes for recovery from prolonged outages
- JWT token buffers only allocated for JWT-auth slots (memory efficient)
- Deferred construction: MQTTBridge is heap-allocated in `begin()` to avoid ESP32 static init crashes
### Connection Handling
- Automatic reconnection with exponential backoff per slot; a slot that stays down through
the full backoff ladder is retried on a slow periodic probe instead of hammering the broker
- Packets are queued while a slot is disconnected and flushed when it recovers
### Raw Radio Data Capture
- Captures actual raw radio transmission data (including radio headers)
- Uses proper MeshCore packet hashing (SHA256-based)
- Provides accurate SNR/RSSI values from actual radio reception (RX packets only)
- Independent RX and TX packet uplinking — both can be active simultaneously
- TX advert mode: selectively uplink only this node's own advert packets
### Timezone Support
- Full timezone support with automatic DST handling
- Supports IANA timezone strings, common abbreviations, and UTC offsets
- Separates local time (for timestamps) and UTC time (for time/date fields)
- Uses JChristensen/Timezone library for accurate timezone conversions
- Accepts IANA timezone strings, common abbreviations, and UTC offsets, with automatic DST handling
- Note: all published MQTT timestamps are UTC regardless of the configured timezone
### WiFi Configuration
- Runtime WiFi credential management via CLI
@@ -795,15 +815,11 @@ the radio actually performs in that case.
### NTP Time Synchronization
- Automatic time synchronization with NTP servers (required for JWT authentication)
- Default primary: `pool.ntp.org`; built-in fallbacks (tried sequentially on failure): `time.google.com`, `time.cloudflare.com`, `time.aws.com`, `time.nist.gov`
- Custom primary via `set mqtt.ntp <hostname>`; `set mqtt.ntp none` reverts to default
- `set mqtt.ntp` runs an immediate sync (primary only, so a typo fails fast) when WiFi is connected and the bridge is running
- `get mqtt.ntp` returns the effective primary hostname
- `get mqtt.ntp.diag` probes every configured server (primary + fallbacks) for connectivity and reports each server's time without changing the system clock — a pure diagnostic
- Periodic time updates (every hour) on the effective primary only
- Proper UTC system time handling
- Periodic time updates (every hour) on the effective primary only; system time is kept in UTC
- Configure and diagnose with `set mqtt.ntp` / `get mqtt.ntp` / `get mqtt.ntp.diag` — see [MQTT Shared Commands](#mqtt-shared-commands)
### Authentication
The auth mode is fixed per preset (see the [preset table](#slot-based-preset-system)). Three modes are used:
The auth mode is fixed per preset (see [Broker Presets](#broker-presets)). Three modes are used:
- **JWT Authentication**: Ed25519-signed tokens for brokers that expect JWT (most WSS presets). For `custom` slots, JWT is used when `audience` is set.
- **Username/Password**: Some presets ship fixed credentials embedded in firmware (`tennmesh`, `nashmesh`, `ctmesh` — plain MQTT, no TLS); others (`inwmesh`, `custom`) take per-slot credentials via `mqttN.username` / `mqttN.password`.
- **None**: `meshrank` (account token carried in the topic) and `eastidahomesh` connect without broker auth.
@@ -812,90 +828,17 @@ The auth mode is fixed per preset (see the [preset table](#slot-based-preset-sys
## Migration from Old Configuration
When upgrading from a firmware version that used the old MQTT configuration format (`mqtt.analyzer.us`, `mqtt.analyzer.eu`, `mqtt.server`, `mqtt.port`, `mqtt.username`, `mqtt.password`), the device automatically migrates settings:
Upgrading from firmware that used an older settings layout — including the pre-slot format
(`mqtt.analyzer.us`, `mqtt.server`, …) — needs no manual intervention: the device converts
its stored configuration on the first boot after the update and keeps your brokers, origin,
IATA, message types, WiFi, and timezone. Verify with `get mqtt.status` afterwards.
- `mqtt.analyzer.us = on` → Slot 1 preset: `analyzer-us`
- `mqtt.analyzer.eu = on` → Slot 2 preset: `analyzer-eu`
- Custom server configured → Slot 3 preset: `custom` with host/port/username/password preserved
- All other settings (origin, IATA, message types, WiFi, timezone) are preserved as-is
The one exception is firmware old enough to predate the separate observer settings file: on
that upgrade path the MQTT slot and WiFi configuration cannot be recovered and must be
re-entered. For the per-format details, see
[MQTT_INTERNALS.md](MQTT_INTERNALS.md#settings-upgrade--migration).
The migration happens automatically on first boot after firmware update. No manual intervention is needed.
## First-Time Setup
### Prerequisites
- MeshCore device with observer MQTT firmware flashed
- WiFi network credentials
- Serial console access (115200 baud) or repeater login via companion app
### Step 1: Configure Radio (after fresh flash/full erase)
If this is a fresh flash, radio parameters must be set to match your mesh network:
```
set radio 910.525,62.5,7,5
set tx 22
```
### Step 2: Configure Device Identity
```
set name MyObserver
set mqtt.iata SEA
```
If migrating from an existing device, restore the private key to keep the same identity:
```
set prv.key <your_64_hex_char_private_key>
```
### Step 3: Configure WiFi
Use the rest of the line as the value (spaces allowed; no quotes). See [WiFi Commands](#wifi-commands).
```
set wifi.ssid YourWiFiNetwork
set wifi.pwd YourWiFiPassword
reboot
```
### Step 4: Configure Timezone (optional)
```
set timezone America/New_York
```
Or use an offset as a fallback:
```
set timezone.offset -5
```
### Step 5: (Optional) Disable Repeating
For receive-only observers (e.g., using a PCB antenna or in a location where repeating is not desired):
```
set repeat off
```
### Step 6: Verify Slot Configuration
```
get mqtt1.preset # Should show: analyzer-us
get mqtt2.preset # Should show: analyzer-eu
get mqtt3.preset # Should show: none
```
### Step 7: (Optional) Add Additional Presets
```
set mqtt3.preset meshmapper
```
### Step 8: Verify Connection
```
get bridge.enabled
get mqtt.rx
get mqtt.tx
get mqtt.status
get wifi.status
```
### Troubleshooting
## Troubleshooting
#### Device Won't Connect to WiFi
```
@@ -922,15 +865,10 @@ get mqtt.iata # IATA must be set for MeshCore-topic presets (e.g. A
#### Timezone Issues
```
get timezone
set timezone America/New_York # IANA format
set timezone EST # Abbreviation
set timezone UTC-5 # UTC offset
get timezone.offset
```
## SNMP Monitoring
Observer nodes include an optional SNMP v2c agent that exposes radio stats, MQTT connectivity, memory usage, and network information to standard monitoring tools. See [MQTT_SNMP.md](MQTT_SNMP.md) for setup and OID reference.
See [Supported Timezone Formats](#supported-timezone-formats) for the accepted values.
Note that published timestamps are UTC regardless of this setting.
## Fault Alerts
+65
View File
@@ -89,6 +89,66 @@ The JSON builder lives in the pure, host-tested `MQTTPayloadBuilder`
(`test/test_mqtt_payload_builder`); the topic type in `MQTTTopicRouter`
(`test/test_mqtt_topic_router`). The mesh↔bridge orchestration above is on-target only.
### Runtime construction and slot memory
- **Deferred construction**`MQTTBridge` is heap-allocated in each app's `begin()`
(`bridge = new MQTTBridge(...)` in `MyMesh.cpp`) rather than held as a static member,
because constructing it at static-init time crashes on ESP32 classic.
- **Runtime slot array**`RUNTIME_MQTT_SLOTS` (`MQTTPresets.h`) is 6 with PSRAM and 3
without, saving ~1.2 KB of heap on non-PSRAM boards. `MAX_MQTT_SLOTS` stays 6 on every
build because it fixes the persisted `MQTTPrefs` layout, so slot config survives moving
firmware between board classes. Three runtime slots suffice without PSRAM:
`_max_active_slots` caps those boards at 2 live connections, leaving one spare for
reconfiguration. Configured slots past the cap report `(inactive)`.
- **Buffers** — the 768-byte JWT `auth_token` is inline in every `MQTTSlot`, not allocated
per JWT-auth slot. What varies is the MQTT client's TX/RX buffer: 896 bytes (the minimum
that fits a CONNECT plus a 768-byte JWT) uniformly on PSRAM boards to limit
fragmentation from mixed allocations, and 896 or 512 per slot on non-PSRAM boards so
non-JWT slots leave smaller holes across teardown/recreate cycles. The large
JSON/raw-packet buffers go through `psram_malloc()`, which prefers PSRAM and falls back
to internal heap.
### Reconnection, backoff, and circuit breaker
The client's own auto-reconnect is disabled (`setAutoReconnect(false)`); the bridge drives
reconnection per slot.
- Backoff ladder: 10 s → 30 s → 60 s → 120 s → 300 s, staggered by 3 s × slot index so
slots don't all handshake at once.
- The ladder resets only after a connection has held for 2 minutes
(`BACKOFF_STABLE_RESET_MS`), which is longer than the 75 s keepalive — a link that can't
survive one keepalive round-trip keeps its earned rung instead of hammering TLS
handshakes at the 10 s rung. CONNACK alone does not reset it.
- After 3 more failures at the top rung (~15 min) the slot's circuit breaker trips and
routine reconnects stop. A tripped slot is probed once every 30 minutes (with a fresh
JWT where applicable); a successful connect clears the breaker, as does reconfiguring
the slot.
- Message retransmit timeout is 15 s — one retry inside esp-mqtt's 30 s outbox expiry,
preserving at-least-once delivery while capping duplicates at one.
### Message building
- The `hash` field in `packets` messages is MeshCore's own packet hash,
`Packet::calculatePacketHash()` — SHA256 over the payload type and payload (plus
`path_len` for TRACE), truncated to `MAX_HASH_SIZE`. It is the same value the dispatcher
uses, so uplinked hashes match the mesh.
- `score` is recomputed at publish time from the packet's SNR and length via the radio's
`packetScore()`, so it matches the value the firmware used on receive.
- Timezone: the JChristensen/Timezone object (`_timezone_storage`, inline since the
memory-defrag work) is kept current from `timezone_string` via `setRules()`, but
`formatIsoTimestampForMqtt()` explicitly ignores it — every published timestamp, time,
and date field is UTC off `gmtime()`, matching Python's
`datetime.now(timezone.utc).isoformat()`. The timezone prefs therefore do not affect
MQTT message content.
### Command namespacing
CLI commands sit at two levels. `bridge.*` is low-level and shared by all bridge types
(MQTT, RS232, ESP-NOW): `bridge.enabled` is the master switch, and `bridge.source` selects
which packet events non-MQTT bridges capture. The MQTT bridge ignores `bridge.source` in
favour of independent `mqtt.rx` / `mqtt.tx` controls. Everything MQTT-specific lives under
`mqtt.*` (shared settings), `mqttN.*` (per-slot broker config), `wifi.*`, and `timezone.*`.
### `/mqtt_prefs` file format
`/mqtt_prefs` is written with an 8-byte `MQTTPrefsHeader` (`magic`, `version`,
@@ -158,6 +218,11 @@ no checksum and an arbitrary short size cannot be trusted to mean anything.
drops the vestigial `_legacy_*` fields the flex layout carried mid-struct. This is a
one-time rewrite; every deployed device performs it on its first boot of versioned
firmware, after which all reads take the header path.
The pre-slot (`OldMQTTPrefs`) copy maps the old single-broker keys onto slots:
`mqtt.analyzer.us = on` → slot 1 `analyzer-us`, `mqtt.analyzer.eu = on` → slot 2
`analyzer-eu`, and a configured `mqtt.server` / `mqtt.port` / `mqtt.username` /
`mqtt.password` → slot 3 `custom` with those values preserved. Origin, IATA, message
types, WiFi, and timezone carry over as-is.
- **`/com_prefs`** — a file written by fork firmware that predates the `MQTTPrefs` split
(a zero-filled MQTT gap plus a trailing observer block) is detected by size; the
trailing SNMP / radio-watchdog / fault-alert settings and the `rx_boosted_gain` /
+4 -1
View File
@@ -7,7 +7,10 @@ MeshCore is a lightweight, portable C++ library that enables multi-hop packet ro
MeshCore now supports a range of LoRa devices, allowing for easy flashing without the need to compile firmware manually. Users can flash a pre-built binary using tools like Adafruit ESPTool and interact with the network through a serial console.
MeshCore provides the ability to create wireless mesh networks, similar to Meshtastic and Reticulum but with a focus on lightweight multi-hop packet routing for embedded projects. Unlike Meshtastic, which is tailored for casual LoRa communication, or Reticulum, which offers advanced networking, MeshCore balances simplicity with scalability, making it ideal for custom embedded solutions, where devices (nodes) can communicate over long distances by relaying messages through intermediate nodes. This is especially useful in off-grid, emergency, or tactical situations where traditional communication infrastructure is unavailable.
> **MQTT Observer Setup**If you're deploying an observer node with MQTT, see the [MQTT Implementation Guide](./MQTT_IMPLEMENTATION.md) for configuration, CLI commands, and troubleshooting.
> **MQTT Observer Setup**Prebuilt observer firmware, docs, and a changelog are at
> [observer.gessaman.com](https://observer.gessaman.com/). See the
> [MQTT Implementation Guide](./MQTT_IMPLEMENTATION.md) for configuration, CLI commands, and
> troubleshooting.
## ⚡ Key Features