# MQTT Bridge Implementation for MeshCore This document describes the MQTT bridge implementation that allows MeshCore repeaters to uplink packet data to multiple MQTT brokers. ## Quick Start Guide ### 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. **2. Configure radio settings** If this is a fresh flash or full erase, configure your radio parameters first. These must match other nodes in your mesh: ```bash set radio 910.525,62.5,7,5 set tx 22 ``` Format: `set radio ,,,` **3. Configure device identity** ```bash set name MyObserver set mqtt.iata SEA ``` If migrating from an existing node (e.g., a Raspberry Pi gateway), restore the private key to keep the same identity: ```bash set prv.key ``` **4. Configure WiFi credentials** (value is the rest of the line; do not use quotes — see [WiFi Commands](#wifi-commands)) ```bash set wifi.ssid YourWiFiNetwork set wifi.pwd YourWiFiPassword ``` **5. (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** ```bash reboot ``` **7. Verify configuration** ```bash get wifi.ssid get bridge.enabled get mqtt.rx get mqtt.tx get mqtt.origin get mqtt.iata get mqtt1.preset get mqtt2.preset get mqtt3.preset get mqtt.status ``` **That's it!** The device will now: - Connect to WiFi automatically - Start uplinking mesh packets to configured MQTT brokers - By default, publish to Let's Mesh Analyzer US (slot 1) and EU (slot 2) - Use device name as MQTT origin (set automatically) --- ## 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 - Automatic reconnection with exponential backoff - JSON message formatting for status, packets, and raw data - Packet queuing during connection issues - Automatic migration from old configuration format ## Architecture ### Slot-Based Preset System 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. **Built-in Presets:** | 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` | wss://broker.eastidahomesh.net:443 | None | WSS | | `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) | — | — | **Default Configuration:** - Slot 1: `analyzer-us` - Slot 2: `analyzer-eu` - Slots 3-6: `none` **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 ## Build Configuration To build the MQTT bridge firmware: ```bash # Heltec V3 pio run -e Heltec_v3_repeater_observer_mqtt # Heltec V4 pio run -e heltec_v4_repeater_observer_mqtt # Heltec Wireless Tracker v1.1 / v2 pio run -e heltec_tracker_v1_1_repeater_observer_mqtt pio run -e heltec_tracker_v1_1_room_server_observer_mqtt pio run -e heltec_tracker_v2_repeater_observer_mqtt pio run -e heltec_tracker_v2_room_server_observer_mqtt # Station G2 pio run -e Station_G2_repeater_observer_mqtt # LilyGo T-LoRa V2.1-1.6 (TTGO LoRa32 V1.0) pio run -e LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt pio run -e LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt ``` **TLora naming:** The env prefix `LilyGo_TLora_V2_1_1_6` is LilyGo’s **T-LoRa V2.1–1.6** board (SX1276); PlatformIO selects **`ttgo-lora32-v1`** (TTGO LoRa32 V1.0). **MQTT observer** envs extend a slim base **without** `sensor_base` so the image fits `min_spiffs`; **all other** `LilyGo_TLora_V2_1_1_6_*` targets still use optional I2C environmental sensors as before. The **`lilygo_tlora_c6`** variant is separate hardware (ESP32-C6). **T-LoRa V2.1–1.6 MQTT observer — one WSS broker:** This hardware is **classic ESP32 without PSRAM**. Each WSS preset uses a full TLS stack and large contiguous heap allocations; **two active broker presets at once** typically fails the second connection (`mbedtls_ssl_setup` / `esp-tls` `0x8017`, low `IntMax` in `memory`). **Treat these observer builds as supporting one active cloud preset:** configure the broker you need in `mqtt1` or `mqtt2`, and set the other slot to `none` (e.g. `set mqtt2.preset none`). Use PSRAM-capable boards if you need multiple simultaneous MQTT uplinks. ### Partition Table Changes — Merged Firmware Required Some MQTT observer builds use a non-default partition table to accommodate the larger firmware size (MQTT libraries, TLS, cert bundle, etc.). **When a board's partition table changes, you must flash the merged firmware (`*-merged.bin`) the first time** so the new partition layout and bootloader are written together. After that initial flash, standard OTA or non-merged updates will work normally. | Environment | Partition Table | Flash Size | App Slot Size | Notes | |-------------|----------------|------------|---------------|-------| | `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_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** 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. - **`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. **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. - **Command line:** ```bash # Build the merged binary pio run -t mergebin -e LilyGo_T3S3_sx1262_repeater_observer_mqtt # Flash at offset 0x0 (overwrites bootloader + partition table) 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)) - `MQTT_DEBUG=1` - Enable debug logging (optional) - `MQTT_WIFI_TX_POWER` - WiFi TX power level (default: `WIFI_POWER_11dBm`) - ~~`MQTT_WIFI_POWER_SAVE_DEFAULT`~~ - Removed; all builds now default to `none` (no power save) #### Compile-time fresh-install defaults (`src/helpers/MQTTDefaults.h`) Optional PlatformIO `build_flags` override defaults written when `/mqtt_prefs` is first created. They do **not** change existing saved prefs on upgrade or reflash (unless `/mqtt_prefs` is erased). | Macro | Default | Notes | |-------|---------|-------| | `MQTT_DEFAULT_SLOT1_PRESET` … `MQTT_DEFAULT_SLOT6_PRESET` | slots 1–2: `analyzer-us` / `analyzer-eu`; slots 3–6: `none` | Must be a built-in preset name, `none`, or `custom` | | `MQTT_DEFAULT_IATA` | (empty) | e.g. `'"YYZ"'` | | `MQTT_DEFAULT_TIMEZONE` | (empty) | e.g. `'"America/Toronto"'` | | `MQTT_DEFAULT_TIMEZONE_OFFSET` | `0` | Fallback hours when TZ string is empty | Example community build: ```ini build_flags = -D MQTT_DEFAULT_SLOT1_PRESET='"meshcore-ca-1"' -D MQTT_DEFAULT_SLOT2_PRESET='"meshcore-ca-2"' -D MQTT_DEFAULT_IATA='"YYZ"' -D MQTT_DEFAULT_TIMEZONE='"America/Toronto"' -D MQTT_DEFAULT_TIMEZONE_OFFSET=-5 ``` WiFi SSID/password are not compile-time configurable (operators set them per device via CLI). Legacy `get mqtt.analyzer_us` / `set mqtt.analyzer_us` still refer to the preset name `analyzer-us`, not “whatever slot 1 default is”. ## Default Configuration The MQTT bridge comes with the following defaults for fresh installs (unless overridden by the macros above): - **Origin**: Device name (set automatically from `set name`) - **IATA**: (blank — must be configured for MeshCore-style topic presets such as Analyzer and TennMesh, unless `MQTT_DEFAULT_IATA` is set at build time) - **Status Messages**: Enabled - **Packet Messages**: Enabled - **Raw Messages**: Disabled - **RX Packets**: Enabled (uplink received packets) - **TX Packets**: `advert` by default (uplink this node's own adverts; set to `on` for all TX or `off` to disable) - **Status Interval**: 5 minutes (300000 ms) - **Slot 1**: `analyzer-us` - **Slot 2**: `analyzer-eu` - **Slots 3-6**: `none` (disabled) - **WiFi SSID**: (blank — must be configured) - **WiFi Password**: (blank — optional for open networks) - **WiFi Power Save**: `none` (no power save) - **Timezone**: (blank — uses UTC until configured, unless `MQTT_DEFAULT_TIMEZONE` is set at build time) - **Timezone Offset**: 0 (fallback, no offset, unless `MQTT_DEFAULT_TIMEZONE_OFFSET` is set) - **Repeat (forwarding)**: On (set `repeat off` for receive-only observers) ## CLI Commands ### MQTT Slot Commands Each slot (1-6) supports the following commands: #### Get Commands - `get mqtt1.preset` - Get slot 1 preset name - `get mqtt2.preset` - Get slot 2 preset name - `get mqttN.preset` - Get slot N preset name (N = 1-6) - `get mqttN.server` - Get custom server hostname for slot N - `get mqttN.port` - Get custom server port for slot N - `get mqttN.username` - Get custom username for slot N - `get mqttN.password` - Get custom password for slot N - `get mqttN.token` - Get per-slot token (e.g., MeshRank account token) - `get mqttN.topic` - Get custom topic template for slot N - `get mqttN.audience` - Get JWT audience for slot N (custom slots only) #### Set Commands - `set mqttN.preset ` - 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 `) - `inwmesh` - requires per-slot credentials (`set mqttN.username` / `set mqttN.password`) - `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 ` - Set custom server hostname for slot N - `set mqttN.port ` - Set custom server port for slot N (1-65535) - `set mqttN.username ` - Set username for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials) - `set mqttN.password ` - Set password for slot N (`custom` preset, or presets like `inwmesh` that require per-device credentials) - `set mqttN.token ` - Set per-slot token (required for MeshRank preset) - `set mqttN.topic