Files
MeshCore-mqtt-observer/MQTT_IMPLEMENTATION.md
agessaman f339beb22e docs: refresh observer docs and document ota commands
Add ota check/update/branch, display, and diagnostic commands to the
observer guide; condense MQTT_IMPLEMENTATION.md and ALERTS.md; fix the
nonexistent 'set bridge off' instruction; mark the T-LoRa observer envs
parked; update the SNMP build-target list; move local-testing notes to
MQTT_INTERNALS.md; drop MEMORY_MONITORING.md (its script never existed).
2026-10-18 15:00:47 -07:00

25 KiB
Raw Permalink Blame History

MQTT Observer Firmware

Observer firmware turns a MeshCore repeater or room server into an uplink that publishes the packets it hears to up to six MQTT brokers.

Quick Start Guide

You need the device, WiFi credentials, and a console: USB serial at 115200 baud, or a repeater admin login from the companion app.

1. Flash. Use the MeshCore Observer Flasher: pick MQTT Observer Firmware and your device, then flash from Chrome or Edge. To build it yourself, see Build Configuration.

2. Set the radio on a fresh flash. These must match the rest of your mesh:

set radio 910.525,62.5,7,5    # <freq_MHz>,<bw_kHz>,<sf>,<cr>
set tx 22

3. Set the identity:

set name MyObserver
set mqtt.iata SEA
set prv.key <64_hex_chars>    # optional: keep the identity of a node you are replacing

4. Set WiFi. The value is the rest of the line; do not add quotes.

set wifi.ssid YourWiFiNetwork
set wifi.pwd YourWiFiPassword

5. Optional settings:

set mqtt3.preset meshmapper   # add a broker (slots 1 and 2 default to Let's Mesh US and EU)
set timezone America/New_York # published timestamps stay UTC either way
set repeat off                # receive-only observer

6. Reboot and verify:

reboot
get link.status
get mqtt.status

The node joins WiFi, connects to each configured broker, and starts publishing. Its MQTT origin is the node name.

Broker Presets

Each of the six slots holds one preset:

set mqtt3.preset meshmapper
set mqtt3.preset none         # disable the slot

Broker address, transport, and credentials ship in the firmware. The Extra setup column lists the presets that need more. Every preset except meshrank uses the meshcore/{iata}/… topic layout, so mqtt.iata must be set.

get mqtt.presets lists what the running firmware ships. The table matches 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 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 (built in)
nashmesh mqtt://mqtt.nashme.sh:1883 user/pass (built in)
ctmesh mqtt://mqtt.ctmesh.org:1883 user/pass (built in)
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 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
marwoj mqtts://mqtt.marwoj.net:8883 user/pass (built in)
custom your broker user/pass, or JWT when mqttN.audience is set see Custom Brokers
none slot disabled

wss:// is WebSocket over TLS, mqtts:// is MQTT over TLS, and mqtt:// is unencrypted.

Slots and Memory Limits

Fresh installs use analyzer-us in slot 1, analyzer-eu in slot 2, and none in 3-6.

  • With PSRAM: all six slots can be active.
  • Without PSRAM: at most two TLS slots (wss:// or mqtts://); each needs about 40 KB of internal heap. Extra slots show as (inactive) in get mqtt.status and stay saved, so they activate if the settings move to a PSRAM board.
  • Classic ESP32 without PSRAM (Heltec V2): plan on one TLS slot. A second usually fails with mbedtls_ssl_setup / esp-tls 0x8017. Set the other slot to none.

Neighbors publication (WITH_MQTT_NEIGHBORS) is on for every PSRAM board, and for the ESP32-S3 boards without PSRAM that set -D MQTT_NEIGHBORS_WITHOUT_PSRAM=1 (Heltec V3/WSL3, RAK3112, Heltec Tracker v1.1/v2). Those builds cap the table at 20 entries and spend about 7.4 KB of static DRAM (9.6 KB on room servers) plus up to 13 KB per publish, from the same heap TLS uses. Classic ESP32 builds leave it off.

CLI Commands

Commands work over serial and over a remote admin login unless marked serial only.

MQTT Slots

N is the slot number, 1-6.

Command Purpose
get/set mqttN.preset <name> Broker preset, custom, or none
get/set mqttN.server <host or URL> Custom broker address
get/set mqttN.port <1-65535> Custom broker port (optional when the URL has a scheme)
get/set mqttN.username <user> For custom and per-slot presets such as inwmesh
get/set mqttN.password <pass> Same; get shows the value on serial only
get/set mqttN.token <token> Per-slot token, e.g. MeshRank; get shows the value on serial only
get/set mqttN.topic <template> Topic template, custom slots only
get/set mqttN.audience <aud> Switches a custom slot to JWT auth; bare set clears it
get/set mqttN.filter <all|none|list> Packet types sent to this slot
get mqttN.diag Last connection error and timing for the slot

Server and port apply only to custom slots. The username and password apply to custom and to presets that take per-device credentials (inwmesh, mesh-chaun14).

Packet filters

Each slot has its own allowlist of payload types, by name or number:

set mqtt1.filter txt_msg,advert   # same as: set mqtt1.filter 2,4
set mqtt1.filter none             # stay connected for status/neighbors, send no packets
set mqtt1.filter                  # back to all
Type Name Type Name
0 req 8 path
1 response 9 trace
2 txt_msg 10 multipart
3 ack 11 control
4 advert 12-14 reserved (number only)
5 grp_txt 15 raw_custom
6 grp_data
7 anon_req
  • The filter covers packets and raw publishes for RX and TX. It does not affect forwarding, status, or neighbors. Changes apply live.
  • get mqttN.filter answers in numeric form (all, none, or an ascending list).
  • Filtered packets are dropped before they reach the publish queue. get mqtt.stats counts them as filt=<n>, and get mqttN.diag shows a slot's filter when it is not all. A long diag reply shortens it to filter:<n>/16.
  • WebConfig shows the filter as a checkbox per type.

Rolling back: builds from the release that added filters onward ignore filters they don't know about. Older builds reject the longer settings file and fall back to defaults, losing WiFi and broker settings. Set every slot back to all before rolling back that far.

MeshRank

set mqtt3.preset meshrank
set mqtt3.token <token from the MeshRank website>

MeshRank receives status, packets, and neighbors under meshrank/uplink/{token}/{device}/. It never receives raw, and its publishes are not retained.

Custom Brokers

set mqtt3.preset custom
set mqtt3.server your-broker.example.com
set mqtt3.port 1883
set mqtt3.username your-username
set mqtt3.password your-password

JWT: setting audience switches the slot to the same Ed25519 JWT scheme the built-in presets use. The node connects as v1_{PUBLIC_KEY}, renews the token before it expires (24 h lifetime), and includes mqtt.owner / mqtt.email in the token when set.

set mqtt3.server wss://my-broker.example.com:443/mqtt
set mqtt3.audience my-broker.example.com

LAN broker without TLS: set mqtt3.server ws://192.168.1.50:9001/mqtt. Non-TLS URLs skip certificate checks.

With a full URL (mqtt://, mqtts://, ws://, wss://), the port comes from the URL or the scheme's default.

Topic templates

set mqttN.topic applies to custom slots only. The default is meshcore/{iata}/{device}/{type}.

Placeholder Value
{iata} mqtt.iata
{device} public key, 64 hex chars
{token} mqttN.token
{type} status, packets, raw, or neighbors

MQTT Shared Settings

Command Purpose Default
get/set mqtt.origin <name> Origin name in payloads node name
get/set mqtt.iata <code> 3-character location code, uppercased; bare set clears blank
set mqtt.status on|off Status messages (periodic and on connect) on
get/set mqtt.packets on|off packets topic on
get/set mqtt.raw on|off raw topic off
get/set mqtt.rx on|off Publish received packets on
get/set mqtt.tx on|off|advert Publish transmitted packets; advert sends only this node's own adverts advert
get/set mqtt.advert.verify on|off Drop received adverts with bad signatures (counted as badsig= in mqtt.stats) off
get/set mqtt.interval <1-60> Status interval, minutes 5
get/set mqtt.neighbors on|off Periodic neighbors publish (neighbors builds) off
get/set mqtt.neighbors.interval <12-336> Neighbors interval, hours 24
get/set mqtt.ntp <host|none> Primary NTP server; set syncs once to test it pool.ntp.org
get mqtt.ntp.diag Probe every NTP server without changing the clock
get/set mqtt.owner <64 hex> Owner public key for JWT (get is serial only)
get/set mqtt.email <email> Owner email for JWT (get is serial only)
get mqtt.presets [start] List presets, paged; continue from next:<idx>
get mqtt.status Per-slot connection state

RX/TX, filter, and neighbors changes apply live. get mqtt.analyzer.us and get mqtt.analyzer.eu remain for old scripts and report whether slot 1 or 2 holds that preset.

Network and WiFi

Command Purpose
get/set wifi.ssid <ssid> Up to 31 characters
get/set wifi.pwd <password> Up to 63 characters; get shows it on serial only. set wifi.pwd (trailing space) for an open network
get/set wifi.powersave none|min|max Default none
get link.status Active medium, IP, signal, uptime, or the last disconnect reason
get link.diag Why Ethernet or WiFi was chosen
get link.dns Live DNS servers and what each medium's DHCP lease supplied
get wifi.status Older WiFi-only form of link.status

The SSID and password take everything after the first space, so spaces are fine and quotes are stored literally. New credentials are used on the next reconnect, no reboot needed.

Timezone

Command Purpose
get/set timezone <tz> IANA name (America/Los_Angeles), abbreviation (PST, CET, BST, ...), or offset (UTC-8, +5)
get/set timezone.offset <-12..14> Fallback hours when timezone is blank

The timezone affects local display only. Published timestamps are always UTC.

Firmware Updates (OTA)

Observer builds update themselves from the release manifest on observer.gessaman.com.

Command Purpose
ota check Report whether a newer build exists. Does not flash
ota update Download and flash the newer build, then reboot (about 30 s offline)
ota branch Show the selected channel, the channel this build came from, and the manifest URL
ota branch prod Follow production (alias stable)
ota branch beta Follow beta (alias dev)
ota branch default Follow the channel this build came from
start ota Serve a manual upload page (ElegantOTA) on the node's IP, or a MeshCore-OTA hotspot when offline. Stops after 15 minutes idle
start ota ap Always use the MeshCore-OTA hotspot, e.g. on networks with client isolation
stop ota Stop the manual upload page

ota branch only saves the choice. Run ota update afterward to switch:

ota branch beta
ota check     # update available: v1.18.0.3 (...) -> v1.18.0.7 (...) (channel switch)
ota update

ota check replies:

  • up to date: <version>
  • update available: <current> -> <new> (<n> behind | new base | channel switch)
  • ... [partition change: cable flash]: the new build changes the partition table, which OTA can't write. Flash it by cable (see Partition Table Changes)

ota update refuses a channel switch to a build that can't read this node's saved settings or lacks a transport the node uses (Ethernet). The reply says channel switch refused ... cable flash. If fault alerts are on, the node also announces the start of an update and any abort on the alert channel.

Web Configuration Portal

The portal configures WiFi, radio, MQTT slots, and filters from a browser. It is off by default on a configured node.

Command Purpose
start webconfig Serve the portal on the node's LAN IP (admin password required). With no WiFi configured it raises the setup AP instead
start webconfig ap Force the setup AP. Run set bridge.enabled off first when the bridge is on WiFi
stop webconfig Stop the portal

Setup AP. A node with no WiFi configured starts an open AP named MeshCore-Setup-XXXX with a captive portal at http://192.168.4.1/. The display shows both. The wizard steps through WiFi, radio, and MQTT, then saves and reboots. The AP stops after 10 minutes with no client. Build with -D WEBCONFIG_AP_PASSWORD='"..."' to add WPA2.

Security. The setup AP has no login and sends credentials over plain HTTP, so provision somewhere private and use LAN mode afterward. LAN mode uses the admin password, a 20-minute idle session, and a 30-second lockout after five failed logins.

ThinkNode M7 on Ethernet with no WiFi configured opens the portal on the LAN at first boot. The serial log and display show a one-time login code, and you must set a new admin password.

Applying changes. MQTT settings apply live. Radio settings apply at reboot. Changing WiFi saves and reboots; find the node's new IP on your router. If the portal is unreachable, the serial CLI always works.

Display

Observer builds with a screen:

Command Purpose Default
get/set display.timeout <0-3600> Seconds before the screen blanks; 0 keeps it on 60
get/set display.flip on|off Rotate 180 degrees. Survives updates, so check it if a screen looks upside down off

Tap the panel or press USER to wake or blank the screen.

Radio Watchdog

get/set radio.watchdog <0-120> (minutes, default 5, 0 disables). If the radio sees no packet, interrupt, or successful TX for that long, the firmware restarts receive mode. This recovers radios that stop reporting interrupts without a reboot. On meshes that are silent for long stretches, raise the interval or set 0.

Bridge

Command Purpose
get/set bridge.enabled on|off Master switch for the bridge
get/set bridge.source rx|tx For RS232 and ESP-NOW bridges. For MQTT, use mqtt.rx / mqtt.tx

SNMP

set snmp on|off and set snmp.community <string> (default public) take effect after a restart. See MQTT_SNMP.md.

Diagnostics

Command Shows
get mqtt.status Each slot's state; with neighbors on, nbr: <next>/<last>
get mqttN.diag Last error for slot N (TLS, socket, clock) and its filter
get mqtt.stats Heap, queue depth, outbox, drops, filt=, badsig=
get mqtt.config.valid valid when the saved MQTT settings pass validation
memory Heap, internal RAM, largest free block, PSRAM
tls.bundletest <host[:port]> Test a TLS handshake against the built-in certificate bundle

MQTT Topics

Topic Contents
meshcore/{IATA}/{PUBLIC_KEY}/status Node status and stats. QoS 1, retained except on meshrank and waev
meshcore/{IATA}/{PUBLIC_KEY}/packets Decoded packet with RF data
meshcore/{IATA}/{PUBLIC_KEY}/raw Raw packet hex
meshcore/{IATA}/{PUBLIC_KEY}/neighbors Zero-hop repeater neighbors with SNR, age, and scopes. QoS 0

{PUBLIC_KEY} is 64 hex characters. MeshRank uses meshrank/uplink/{token}/{PUBLIC_KEY}/<type>.

Neighbors publish after discover.scopes, or every mqtt.neighbors.interval hours when mqtt.neighbors is on. A periodic run first refreshes the neighbor table for 60 s (like discover.neighbors), then queries each neighbor's scopes. A manual discover.scopes during a refresh waits for it and says so: OK - scopes queued (47s discovery remaining).

JSON Message Formats

All timestamps are UTC with an explicit +00:00 offset.

Status

{
  "status": "online|offline",
  "timestamp": "2024-01-01T12:00:00.000000+00:00",
  "origin": "Device Name",
  "origin_id": "DEVICE_PUBLIC_KEY",
  "model": "device_model",
  "firmware_version": "firmware_version",
  "radio": "radio_info",
  "client_version": "meshcore/{firmware_version}",
  "repeat": "on|off",
  "stats": {
    "battery_mv": 4100,
    "uptime_secs": 3600,
    "packets_sent": 42,
    "packets_received": 128,
    "errors": 0,
    "queue_len": 0,
    "noise_floor": -110,
    "tx_air_secs": 12,
    "rx_air_secs": 340,
    "recv_errors": 2,
    "internal_heap": 102400
  }
}

stats and its fields are omitted when no value is available. Packet counts are totals since boot.

Packet

{
  "origin": "MeshCore-HOWL",
  "origin_id": "A1B2C3D4E5F67890...",
  "timestamp": "2024-01-01T12:00:00.000000+00:00",
  "type": "PACKET",
  "direction": "rx|tx",
  "time": "12:00:00",
  "date": "01/01/2024",
  "len": "45",
  "packet_type": "4",
  "route": "F|D|T|U",
  "payload_len": "32",
  "raw": "F5930103807E5F1E...",
  "SNR": "12.5",
  "RSSI": "-65",
  "score": "234",
  "hash": "A1B2C3D4E5F67890",
  "path": ["aa", "bb", "cc"]
}
  • Numeric fields are JSON strings. time and date are UTC.
  • SNR, RSSI, and score appear on RX packets only. score is the rebroadcast score ×1000 (0-1000), the same number the serial RX log prints.
  • path appears on direct-route packets: one lowercase hex token per hop, matching meshcore-packet-capture.

Raw

{
  "origin": "MeshCore-HOWL",
  "origin_id": "A1B2C3D4E5F67890...",
  "timestamp": "2024-01-01T12:00:00.000000+00:00",
  "type": "RAW",
  "data": "F5930103807E5F1E..."
}

Neighbors

{
  "timestamp": "2024-01-01T12:00:00.000000+00:00",
  "origin": "MeshCore-HOWL",
  "origin_id": "A1B2C3D4E5F67890...",
  "total_neighbors": 2,
  "queried_neighbors": 2,
  "truncated": false,
  "self": { "scopes": "DEN,APRS", "default_scope": "*" },
  "neighbors": [
    { "pubkey": "0011223344556677...", "snr": 9.75, "heard_secs_ago": 42,
      "scopes": "DEN,APRS", "status": "responded" },
    { "pubkey": "8899AABBCCDDEEFF...", "snr": 12.5, "heard_secs_ago": null,
      "scopes": "DEN", "status": "responded" }
  ]
}
  • Entries are sorted by usefulness. The list stops early when the 10 KB buffer fills or the 20-entry cap applies; compare its length with total_neighbors.
  • queried_neighbors counts scope requests actually sent. status is responded, timeout, or send_failed.
  • heard_secs_ago: null means unknown, never zero: the neighbor was last heard before the clock was set. It usually clears on the next cycle.
  • self.default_scope is the node's region default, or * when unset.

Behavior

Connections. Each slot reconnects with exponential backoff, then drops to a slow periodic retry. Packets queue while a slot is down. When the network route changes (Ethernet to WiFi or back), every slot disconnects cleanly and retries at once on the new route. Each medium keeps its own DHCP DNS servers.

Ethernet (ThinkNode M7). The observer uses Ethernet when it has a DHCP lease at boot, otherwise WiFi, and switches at runtime. Without a cable, boot waits about 1 s for Ethernet. The DHCP hostname is meshcore-<node name> on both.

NTP. JWT auth needs the correct time. The node syncs hourly from mqtt.ntp, then pool.ntp.org, time.google.com, time.cloudflare.com, time.aws.com, and time.nist.gov. Replies from unsynced servers, the wrong address, or with a mismatched request stamp are ignored.

Authentication is fixed per preset: Ed25519 JWT (username v1_{PUBLIC_KEY}, renewed before expiry), username/password (built in, or per slot), or none.

Build Configuration

Observer environments are named <board>_repeater_observer_mqtt and <board>_room_server_observer_mqtt. List them all with:

grep -rhoE '^\[env:[^]]*observer_mqtt\]' variants
pio run -e heltec_v4_repeater_observer_mqtt

The LilyGo T-LoRa V2.1-1.6 observer envs are parked (trailing _ in the env name) because the image no longer fits its 4 MB flash.

Partition Table Changes

Some observer builds use a different partition table than the board's other MeshCore firmware. The first time you move to one of these, flash the merged image (*-merged.bin) so the bootloader and partition table are written too. OTA works normally after that.

Environment (repeater and room server) Partition table App slot
LilyGo_T3S3_sx1262_*_observer_mqtt min_spiffs.csv (4 MB) 1.875 MB
LilyGo_T3S3_sx1276_*_observer_mqtt min_spiffs.csv (4 MB) 1.875 MB
Ebyte_EoRa-S3_*_observer_mqtt min_spiffs.csv (4 MB) 1.875 MB
Station_G2_*_observer_mqtt default_16MB.csv 6.25 MB
Station_G3_ESP32_*_observer_mqtt (both RF slots) default_16MB.csv 6.25 MB
LilyGo_TBeam_1W_*_observer_mqtt default_16MB.csv 6.25 MB

A partition change usually erases settings (WiFi, MQTT, name, admin password), so plan to reconfigure. A device coming from non-MeshCore firmware is always erased.

To flash: use the web flasher, whose Download menu also has the merged, erase, and bootloader files, or:

pio run -t mergebin -e LilyGo_T3S3_sx1262_repeater_observer_mqtt
esptool.py write_flash 0x0 .pio/build/LilyGo_T3S3_sx1262_repeater_observer_mqtt/firmware-merged.bin

Build Flags

Flag Purpose
WITH_MQTT_BRIDGE=1 MQTT bridge (required)
WITH_SNMP=1 SNMP agent
MQTT_DEBUG=1 Debug logging
MQTT_WIFI_TX_POWER WiFi TX power (default WIFI_POWER_11dBm)

These set fresh-install defaults (see src/helpers/MQTTDefaults.h). They don't change a node that already has /mqtt.json.

Macro Default
MQTT_DEFAULT_SLOT1_PRESET … SLOT6_PRESET analyzer-us, analyzer-eu, then none
MQTT_DEFAULT_IATA blank
MQTT_DEFAULT_TIMEZONE blank
MQTT_DEFAULT_TIMEZONE_OFFSET 0
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"'

WiFi credentials can't be set at build time.

Upgrading Settings

The first boot after an update converts older settings formats, including the pre-slot mqtt.analyzer.us / mqtt.server layout, and keeps brokers, origin, IATA, WiFi, and timezone. Firmware old enough to predate the separate observer settings file is the exception: re-enter MQTT and WiFi settings after that upgrade. Details are in MQTT_INTERNALS.md.

Troubleshooting

No WiFi:

get wifi.ssid
get link.status            # shows the disconnect reason
set wifi.powersave none

No MQTT messages:

get bridge.enabled         # must be on
get mqtt.rx                # must be on
get mqtt.iata              # must be set for meshcore/{iata}/... presets
get mqtt.status
get mqtt1.diag             # TLS, socket, or clock error for slot 1
get mqtt1.filter           # none sends no packets

JWT brokers reject the login: check the clock with get mqtt.ntp.diag.

Fault Alerts

The node can post a message on a LoRa group channel when WiFi or a broker stays down. See ALERTS.md.

Developer Documentation

Source layout, upstream integration points, settings file format, and local testing: MQTT_INTERNALS.md.