# MQTT Bridge Implementation for MeshCore This document describes the on-device ESP32 MQTT bridge for observer infrastructure and MQTT-capable Companions. Shared controls use the same `get` / `set` commands on each role: `usb.logging`, `mqtt.enabled`, `logging.output`, and MQTT broker settings. Diagnostic and bridge commands that need infrastructure features are identified below. See [feature switches by role](docs/role_feature_switches.md). Use the [USB web console](https://flasher.meshcore.io/console) for ASCII commands. ## Quick Start Guide ### Browser setup (recommended) Expanded Full ESP32 observer builds and supported WiFi/Full Companions include WebConfig. Non-MQTT builds with WebConfig omit its MQTT controls. Some portable images omit the portal while retaining the compact WiFi firmware uploader; check the exact artifact capability manifest. 1. Flash an observer build such as `heltec_v4_repeater_observer_mqtt`. 2. Enable the portal with `set webui on` if it is off. 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 on either role. 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 mqtt.enabled 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 mqtt.enabled on ``` The 1.17.1.5 expanded Full TLora V2.1-1.6 MQTT artifacts include WebConfig, as verified in their capability manifests. Older/slimmer direct PlatformIO recipes can omit it; use the infrastructure CLI for those images. A board's flash size alone does not establish whether the release contains WebConfig. 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 Use the same MQTT setup commands on MQTT-capable Companions, Repeaters, and Room Servers through the USB web console at 115200 baud or the LAN WebConfig CLI tab. Infrastructure also accepts an authenticated remote admin login through the Companion app. Full Companion also has its TCP terminal on port 5002. Device identity import, neighbor discovery, and extended diagnostics remain specific to the roles that implement them. **1. Flash the observer firmware to your device** For this fork's 1.17.1.5 USA Cascade images, use the [firmware picker](docs/firmware_picker.md). The upstream [MeshCore Observer Flasher](https://observer.gessaman.com/) offers a separate Observer distribution -- 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** 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) 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 ``` **8. Enable MQTT and reboot to connect** ```bash set mqtt.enabled on reboot ``` **9. Verify configuration** ```bash get wifi.ssid get wifi.status get mqtt.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, 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, packet, raw, and neighbors data - Packet queuing during connection issues - Automatic migration from old configuration format ## Broker Presets Each of the 6 slots holds one preset. Point a slot at a community broker with: ```bash set mqtt3.preset meshmapper # slot 3 -> MeshMapper ``` 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`. Run `get mqtt.presets` on the device for the list this firmware actually ships; the table below documents the current build. | 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 ` | | `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 | -- | | `okimesh-1` | `wss://mqtt1.okimesh.org:9002/mqtt` | JWT | -- | | `okimesh-2` | `wss://mqtt2.okimesh.org:9002/mqtt` | 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 | -- | | `atvirastinklas` | `wss://mqtt-mc.atvirastinklas.lt:443` | JWT | -- | | `gomesh` | `wss://mqtt.gomesh.dev:443` | JWT | -- | | `idahomesh` | `wss://mqtt.idahomesh.org:443/mqtt` | JWT | -- | | `ntxmesh` | `wss://ntxmesh.dhovin.me:8883` | JWT | -- | | `bsmesh` | `wss://mqtt.bsmesh.de:8885` | 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) | -- | -- | 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 3-6 `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 **Neighbors publication without PSRAM.** PSRAM boards get `WITH_MQTT_NEIGHBORS` automatically. Non-PSRAM boards opt in per variant with `-D MQTT_NEIGHBORS_WITHOUT_PSRAM=1`, which is set on the ESP32-S3 observer envs (Heltec V3/WSL3, RAK3112, Heltec Tracker v1.1/v2). It costs ~7.4 KB of static DRAM (~9.6 KB on room servers) and up to ~13 KB transiently per publish, and caps the table at 20 entries -- so the feature is deliberately **not** enabled on the classic ESP32 T-LoRa V2.1-1.6 observer builds, which are already down to one active TLS slot. Publishing peaks while the table is built, so keep the slot guidance above in mind: the peak lands on the same internal heap the TLS stack draws from. ## 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 # Station G3 (ESP32) pio run -e Station_G3_ESP32_repeater_observer_mqtt pio run -e Station_G3_ESP32_room_server_observer_mqtt pio run -e Station_G3_ESP32_r2_repeater_observer_mqtt pio run -e Station_G3_ESP32_r2_room_server_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_ # Elecrow ThinkNode M7 pio run -e ThinkNode_M7_repeater_observer_mqtt pio run -e ThinkNode_M7_room_server_observer_mqtt ``` **ThinkNode M7 -- WiFi only:** the M7 has an onboard CH390 Ethernet controller, and `ThinkNode_M7_companion_radio_ethernet` uses it, but the MQTT bridge link management is bound to the WiFi station API, so observer environments uplink over WiFi. The bridge still uses WiFi even when the Companion image also includes Ethernet. The board has PSRAM, so these builds get neighbors publication (`WITH_MQTT_NEIGHBORS`) automatically. **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 The table below describes **base PlatformIO recipes**, not every `build.sh` release overlay. Option 3 promotes MQTT infrastructure to expanded Full layouts; inspect that artifact's partition/capability metadata and the installed layout before updating. To install a changed partition table, flash the matching `*-merged.bin` over USB. An application-only OTA upload does not change the partition table. Subsequent OTA updates require an image that fits the installed application slots and satisfies the updater's compatibility checks. | 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 | | `Station_G3_ESP32_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board | | `Station_G3_ESP32_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | 16 MB flash board | | `Station_G3_ESP32_r2_repeater_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | Second RF slot | | `Station_G3_ESP32_r2_room_server_observer_mqtt` | `default_16MB.csv` | 16 MB | 6.25 MB | Second RF slot | | `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 | A board absent from the base-recipe table can still need a layout migration when installing an expanded Full release image. A merged flash writes the bootloader and partition table, but it does **not inherently erase NVS**. An erase operation, relocated/resized storage, or incompatible filesystem layout can lose settings; unchanged storage can retain them. Back up identity and configuration before a layout migration and check them afterward. For example, the base TLora repeater observer uses `dual_ota_1984k.csv` while its room-server sibling uses `min_spiffs.csv`. Compare those with the actual release layout rather than assuming a filename or role change is compatible. **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 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. Requires Chrome or Edge. - **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 ``` When the installed partition layout is unchanged, normal application updates usually retain preferences. Full Companion and infrastructure can use different layouts on the same board; follow the exact image's installation directions. ### 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`) - `DEFAULT_WIFI_POWER_SAVE_MODE` - Fresh-install WiFi modem-sleep default: `0` = `min`, `1` = `none`, `2` = `max`. The Cascade profile sets `0`; target-default builds use `1`. #### 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 | | `DEFAULT_WIFI_POWER_SAVE_MODE` | `1` (`none`); Cascade profile: `0` (`min`) | Fresh MQTT and standalone WebConfig WiFi setting; saved settings take precedence | 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) - **Per-slot packet filters**: `all` (every payload type is uploaded) - **WiFi SSID**: (blank - must be configured) - **WiFi Password**: (blank - optional for open networks) - **WiFi Power Save**: `min` for Cascade-profile builds; otherwise `none` (a saved setting takes precedence) - **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) - `get mqttN.filter` - Get the slot's packet-type allowlist (`all`, `none`, or numeric CSV) - `get mqttN.password` / `get mqttN.token` - Read stored slot credentials from a local USB, TCP/Ethernet, or LAN browser CLI; Companion also accepts binary command `0x42`. LoRa callers cannot read their values. #### Set Commands - `set mqttN.preset ` - 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 ` - Set custom server hostname for slot N - `set mqttN.port ` - Set custom server port for slot N (0-65535; 0 clears the override and uses a full URI's default port) - `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