# 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 ### Browser setup (recommended) Normal ESP32 MQTT repeater-, room-server-, and WiFi-companion builds include an all-in-one WebConfig portal. The same page is used by non-MQTT ESP32 builds, where the MQTT tab and wizard step are removed at runtime. 1. Flash an observer build such as `heltec_v4_repeater_observer_mqtt`. 2. On a fresh node with no saved WiFi SSID, join the open `MeshCore-Setup-XXXX` access point. The captive page should open automatically; otherwise browse to . 3. Complete the wizard, review the settings, and choose **Save & Reboot**. The MQTT bridge remains stopped while the setup AP owns WiFi and starts normally after the reboot. 4. Verify the connections on the portal's status page or with `get mqtt.status` from the CLI. The setup AP stops after 10 minutes with no connected client. WiFi companion builds enable the WebUI by default. Repeater and room-server builds default it off; use `set webui on` for a persistent start or `start webconfig` for only the current boot. `get webui` reports the active URL. To change an already-configured repeater/room-server observer temporarily, log in through its CLI and run: ```text start webconfig ``` Open the reported LAN URL and sign in with the node's admin password. The LAN portal runs until `stop webconfig` or a reboot. To force the captive setup AP, first stop the MQTT bridge, then start the portal in AP mode: ```text set bridge off start webconfig ap ``` `start webconfig ap` intentionally refuses to take WiFi away from a running bridge. The forced AP is open, so stop it when finished and re-enable the bridge if you did not reboot: ```text stop webconfig set bridge on ``` The classic 4 MB ESP32 `LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt` and `LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt` targets omit the browser portal because the async web-server code does not fit while retaining two app slots for LoRa OTA. Configure those two builds with the CLI below. MQTT WiFi companions use this same wizard instead of the former two-page setup. The portal remains on the companion's station IP, alongside the companion protocol on TCP port 5000. Because companions have no admin CLI password, their LAN page is intentionally unauthenticated; use a trusted WiFi network. ### CLI setup and fallback **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 they retain dual-app OTA on the 4 MB flash; **all other** `LilyGo_TLora_V2_1_1_6_*` targets still use optional I2C environmental sensors as before. The repeater observer also keeps 256 recent-repeater entries instead of the normal ESP32 default of 2,048. 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` | `dual_ota_1984k.csv` | 4 MB | 1.9375 MB | 64 KB SPIFFS; no coredump partition. **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 | TTGO LoRa32 V1.0; observer omits `sensor_base`; one active WSS broker recommended. | | `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_repeater_observer_mqtt`:** This uses the custom `dual_ota_1984k.csv` layout. Install its merged image when coming from a standard TLora build, the room-server observer, or any older `huge_app.csv` build; expect to reconfigure after that partition change. - **`LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt`:** This retains the normal `min_spiffs.csv` layout. Moving from another `min_spiffs` TLora build does not itself require a partition change, but coming from the repeater observer's custom layout, `huge_app.csv`, or a non-MeshCore layout does. - **`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