test with single radio - mqtt success at broker

This commit is contained in:
prabathbr
2026-06-24 23:04:42 +10:00
parent 90d2c9302f
commit e706d9044c
21 changed files with 383 additions and 26 deletions
@@ -44,6 +44,7 @@ runs:
# companion-wifi-v1.15.0
# repeater-bridge-espnow-v1.15.0
# observer-eastmesh-bridge-espnow-v2026.5.1
# observer-eastmesh-bridge-mqtt-v2026.7.0
# observer-eastmesh-v2026.5.1
# with OFFICIAL_MESHCORE_VERSION=v1.15.0 configured as a GitHub variable.
#
@@ -85,6 +86,12 @@ runs:
exit 1
fi
EASTMESH_VERSION="${BASH_REMATCH[1]}"
elif [[ "$GIT_TAG_NAME" =~ ^observer-eastmesh-bridge-mqtt-(v[[:alnum:]._-]+)$ ]]; then
if [[ -z "$STATIC_OFFICIAL_VERSION" ]]; then
echo "OFFICIAL_MESHCORE_VERSION must be set for EastMesh release tags" >&2
exit 1
fi
EASTMESH_VERSION="${BASH_REMATCH[1]}"
elif [[ "$GIT_TAG_NAME" =~ ^companion-wifi-(v[[:alnum:]._-]+)$ ]]; then
FIRMWARE_VERSION="${BASH_REMATCH[1]}"
elif [[ "$GIT_TAG_NAME" =~ ^repeater-bridge-espnow-(v[[:alnum:]._-]+)$ ]]; then
@@ -0,0 +1,41 @@
name: EastMesh Build Observer MQTT Bridge Firmwares
permissions:
contents: write
on:
workflow_dispatch:
push:
tags:
- "observer-eastmesh-bridge-mqtt-v*"
jobs:
build:
runs-on: ubuntu-latest
env:
OFFICIAL_MESHCORE_VERSION: ${{ vars.OFFICIAL_MESHCORE_VERSION }}
PLATFORMIO_BUILD_FLAGS: -UMQTT_DEBUG
steps:
- name: Clone Repo
uses: actions/checkout@v6
- name: Setup Build Environment
uses: ./.github/actions/setup-build-environment
- name: Build Firmwares
run: /usr/bin/env bash eastmesh-build.sh build-observer-mqtt-bridge-firmwares
- name: Upload Workflow Artifacts
uses: actions/upload-artifact@v7
with:
name: observer-bridge-mqtt-firmwares
path: out
- name: Create Release
uses: softprops/action-gh-release@v3
if: startsWith(github.ref, 'refs/tags/')
with:
name: Observer EastMesh Firmware with MQTT Bridge ${{ env.RELEASE_VERSION }}
body: ""
draft: true
files: out/*
+3 -1
View File
@@ -172,6 +172,7 @@ Current tag formats:
git tag companion-wifi-v1.15.0
git tag repeater-bridge-espnow-v1.15.0
git tag observer-eastmesh-bridge-espnow-v2026.5.1
git tag observer-eastmesh-bridge-mqtt-v2026.7.0
git tag observer-eastmesh-v2026.5.1
```
@@ -180,8 +181,9 @@ Rules:
- `companion-wifi` tags use the upstream MeshCore version directly
- `repeater-bridge-espnow` tags use the upstream MeshCore version directly
- `observer-eastmesh-bridge-espnow` tags use the EastMesh release version in the tag
- `observer-eastmesh-bridge-mqtt` tags use the EastMesh release version in the tag
- `observer-eastmesh` tags use the EastMesh release version in the tag
- GitHub Actions variable `OFFICIAL_MESHCORE_VERSION` supplies the upstream base version for Observer EastMesh and Observer ESP-NOW EastMesh release builds
- GitHub Actions variable `OFFICIAL_MESHCORE_VERSION` supplies the upstream base version for Observer EastMesh, Observer ESP-NOW EastMesh, and Observer MQTT Bridge EastMesh release builds
- if the upstream MeshCore release version changes, update `OFFICIAL_MESHCORE_VERSION` in GitHub before cutting release tags
Typical release flow:
+123
View File
@@ -18,6 +18,8 @@ CoreScope (<https://core.eastmesh.au>) offers visibility into the network, inclu
- native WiFi
- MQTT over WSS with JWT auth
- optional local HTTPS config panel on supported ESP32 targets
- `*_repeater_observer_mqtt_bridge` firmware targets that add a **bidirectional MQTT mesh bridge** to a peer broker (separate from MQTT uplink)
- `*_repeater_observer_espnow` firmware targets that add a local ESP-NOW mesh bridge
- `*_companion_radio_wifi` firmware targets for WiFi-connected companion devices
- EastMesh-specific release workflows and versioning on top of upstream MeshCore releases
- docs and release guidance for EastMesh users instead of the full upstream MeshCore docs set
@@ -127,6 +129,8 @@ uv run --group docs zensical build
- shared EastMesh observer env definitions
- [`examples/simple_repeater/MyMesh.cpp`](./examples/simple_repeater/MyMesh.cpp)
- repeater CLI wiring, MQTT command surface, and web allowlist integration
- [`src/helpers/bridges/MQTTBridge.cpp`](./src/helpers/bridges/MQTTBridge.cpp)
- bidirectional MQTT mesh bridge (peer broker TCP, topic `meshcore/bridge/packets`)
- [`src/helpers/mqtt/MQTTUplink.cpp`](./src/helpers/mqtt/MQTTUplink.cpp)
- MQTT uplink implementation, HTTPS web panel, WSS/JWT handling, and repeater WiFi control
- [`examples/companion_radio`](./examples/companion_radio)
@@ -158,6 +162,121 @@ uv run --group docs zensical build
- owner public key and email
- local web panel enablement
### MQTT Mesh Bridge (Observer)
The `observer-eastmesh-bridge-mqtt` release track (`*_repeater_observer_mqtt_bridge`) bridges **raw mesh packets** between repeaters through a **peer MQTT broker you run** (for example Mosquitto on a LAN PC). This is **not** the same as MQTT uplink to EastMesh/MeshMapper:
| | MQTT uplink (observer) | MQTT mesh bridge |
| --- | --- | --- |
| Purpose | Publish JSON telemetry to curated brokers | Forward encrypted mesh packets between radios |
| Brokers | `eastmesh-au`, `meshmapper`, custom WSS | Your peer broker (`host:port`, TCP) |
| Topic | `meshcore/<iata>/...` (per IATA) | `meshcore/bridge/packets` (fixed) |
| Default on bridge builds | Off by default (enable in web panel / CLI) | On when `bridge.enabled` is on |
**First supported target:** `Xiao_S3_WIO_repeater_observer_mqtt_bridge`
**Related track:** `observer-eastmesh-bridge-espnow` (`*_repeater_observer_espnow`) uses ESP-NOW instead of MQTT for local bridging.
Build example:
```bash
uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge
```
Flash (update):
```bash
uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge -t upload --upload-port COM10
```
After a **full flash erase**, flash the merged image at `0x0` instead of only `firmware.bin`:
```bash
uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge -t mergebin
uv run pio pkg exec -p tool-esptoolpy -- esptool.py --chip esp32s3 --port COM10 write_flash 0x0 .pio/build/Xiao_S3_WIO_repeater_observer_mqtt_bridge/firmware-merged.bin
```
#### Peer Mosquitto broker (LAN)
Each bridge node connects to the **same** peer broker with the **same** credentials and `bridge.secret`.
1. Install [Mosquitto](https://mosquitto.org/download/) on a machine reachable from both repeaters (for example `192.168.1.145`).
2. Edit `mosquitto.conf` (Windows service install: `C:\Program Files\Mosquitto\mosquitto.conf`) and add:
```conf
listener 1883 0.0.0.0
allow_anonymous false
password_file C:\Program Files\Mosquitto\passwd
```
For lab/testing only, `allow_anonymous true` works without a password file.
3. Create a user (admin CMD):
```cmd
cd "C:\Program Files\Mosquitto"
mosquitto_passwd -c passwd bridgeuser
```
4. Restart the **service** (do not run a second `mosquitto -v` while the service owns port 1883):
```powershell
Restart-Service mosquitto
```
5. Confirm LAN listen and open the firewall:
```cmd
netstat -ano | findstr ":1883"
```
Expect `0.0.0.0:1883`, not only `127.0.0.1:1883`.
6. Test from another machine:
```cmd
mosquitto_pub -h 192.168.1.145 -p 1883 -u bridgeuser -P your-password -t test -m hello
mosquitto_sub -h 192.168.1.145 -p 1883 -u bridgeuser -P your-password -t meshcore/bridge/packets -v
```
Binary garbage on `meshcore/bridge/packets` is normal — payloads are XOR-encrypted mesh frames, not text.
#### Repeater configuration
Configure **both** bridge nodes identically for broker access; use the same `bridge.secret` on every node in the bridge group.
Serial or web panel CLI:
```text
set wifi.ssid YourNetwork
set wifi.pwd YourWiFiPassword
set bridge.peer.host 192.168.1.145
set bridge.peer.port 1883
set bridge.peer.username bridgeuser
set bridge.peer.password your-password
set bridge.secret your-shared-bridge-secret
```
Web panel: **MQTT Settings** → **Mesh bridge peer MQTT** (`host:port`, username, password). The section appears when `get bridge.type` returns `mqtt`.
Useful checks:
```text
get bridge.type
get bridge.peer.host
get bridge.enabled
```
Default admin password on dev builds is usually `password` unless you changed it.
Packets use magic `0xC03E`, a Fletcher checksum, XOR encryption with `bridge.secret`, then publish/subscribe on `meshcore/bridge/packets`. Duplicate detection limits loops when both nodes see the same traffic.
More detail:
- [Custom CLI — MQTT bridge settings](./eastmesh-docs/custom-cli.md)
- [Web panel — MQTT settings and bridge peer fields](./eastmesh-docs/web-panel.md)
- [Releases — track comparison](./eastmesh-docs/releases.md)
### Local Web Panel
On supported `*_repeater_observer` ESP32 targets, the repeater can expose a local HTTPS config panel over WiFi.
@@ -197,6 +316,7 @@ These rescue commands are only available after entering `CLI Rescue`:
- `.github/workflows/eastmesh-build-observer-firmwares.yml`
- `.github/workflows/eastmesh-build-repeater-bridge-espnow-firmwares.yml`
- `.github/workflows/eastmesh-build-observer-espnow-firmwares.yml`
- `.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml`
- `.github/workflows/eastmesh-pr-build-check.yml`
- `.github/workflows/eastmesh-push-build-check.yml`
- `.github/workflows/eastmesh-github-pages.yml`
@@ -209,6 +329,7 @@ The current release workflows intentionally focus only on:
- `repeater-bridge-espnow`
- `observer-eastmesh`
- `observer-eastmesh-bridge-espnow`
- `observer-eastmesh-bridge-mqtt`
## Release Tags
@@ -219,6 +340,7 @@ git tag companion-wifi-v1.14.1
git tag repeater-bridge-espnow-v1.15.0
git tag observer-eastmesh-v2026.5.1
git tag observer-eastmesh-bridge-espnow-v2026.5.1
git tag observer-eastmesh-bridge-mqtt-v2026.7.0
```
Companion WiFi uses the upstream MeshCore version in the tag.
@@ -242,3 +364,4 @@ Current docs pages:
- [Download and Flash Releases](./eastmesh-docs/releases.md)
- [Build Locally With uv](./eastmesh-docs/local-builds.md)
- [Custom CLI Commands](./eastmesh-docs/custom-cli.md)
- [Web Panel](./eastmesh-docs/web-panel.md)
+8 -1
View File
@@ -19,11 +19,13 @@ Push one or more of the following tag formats to trigger the matching firmware r
- `companion-wifi-v1.15.0`
- `repeater-bridge-espnow-v1.15.0`
- `observer-eastmesh-bridge-espnow-v2026.5.1`
- `observer-eastmesh-bridge-mqtt-v2026.7.0`
- `observer-eastmesh-v2026.5.1`
Use the upstream MeshCore version in `companion-wifi-v1.15.0`.
Use the upstream MeshCore version in `repeater-bridge-espnow-v1.15.0`.
Use the EastMesh release version in `observer-eastmesh-bridge-espnow-v2026.5.1`.
Use the EastMesh release version in `observer-eastmesh-bridge-mqtt-v2026.7.0`.
Use the EastMesh release version in `observer-eastmesh-v2026.5.1`.
Each tag triggers a separate workflow:
@@ -31,6 +33,7 @@ Each tag triggers a separate workflow:
- `companion-wifi-v*` builds companion WiFi firmware
- `repeater-bridge-espnow-v*` builds repeater ESP-NOW bridge firmware
- `observer-eastmesh-bridge-espnow-v*` builds Observer ESP-NOW firmware
- `observer-eastmesh-bridge-mqtt-v*` builds Observer MQTT bridge firmware
- `observer-eastmesh-v*` builds Observer firmware
You can push one, or more tags on the same commit, and they will all build separately.
@@ -42,6 +45,7 @@ During the GitHub Actions build:
- `companion-wifi` uses the version in the tag as `FIRMWARE_VERSION`
- `repeater-bridge-espnow` uses the version in the tag as `FIRMWARE_VERSION`
- `observer-eastmesh-bridge-espnow` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION`
- `observer-eastmesh-bridge-mqtt` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION`
- `observer-eastmesh` uses `OFFICIAL_MESHCORE_VERSION` as `FIRMWARE_VERSION` and the EastMesh version from the tag as `EASTMESH_VERSION`
The resulting version string depends on the workflow:
@@ -49,6 +53,7 @@ The resulting version string depends on the workflow:
- `companion-wifi`: `v1.15.0-<commit>`
- `repeater-bridge-espnow`: `v1.15.0-<commit>`
- `observer-eastmesh-bridge-espnow`: `v1.15.0-eastmesh-v2026.5.1-<commit>`
- `observer-eastmesh-bridge-mqtt`: `v1.15.0-eastmesh-v2026.7.0-<commit>`
- `observer-eastmesh`: `v1.15.0-eastmesh-v2026.5.1-<commit>`
Example:
@@ -76,8 +81,9 @@ Example:
git tag companion-wifi-v1.15.0
git tag repeater-bridge-espnow-v1.15.0
git tag observer-eastmesh-bridge-espnow-v2026.5.1
git tag observer-eastmesh-bridge-mqtt-v2026.7.0
git tag observer-eastmesh-v2026.5.1
git push origin companion-wifi-v1.15.0 repeater-bridge-espnow-v1.15.0 observer-eastmesh-bridge-espnow-v2026.5.1 observer-eastmesh-v2026.5.1
git push origin companion-wifi-v1.15.0 repeater-bridge-espnow-v1.15.0 observer-eastmesh-bridge-espnow-v2026.5.1 observer-eastmesh-bridge-mqtt-v2026.7.0 observer-eastmesh-v2026.5.1
```
## Supported Tags
@@ -85,4 +91,5 @@ git push origin companion-wifi-v1.15.0 repeater-bridge-espnow-v1.15.0 observer-e
- `companion-wifi-v1.15.0`
- `repeater-bridge-espnow-v1.15.0`
- `observer-eastmesh-bridge-espnow-v2026.5.1`
- `observer-eastmesh-bridge-mqtt-v2026.7.0`
- `observer-eastmesh-v2026.5.1`
+12
View File
@@ -37,6 +37,7 @@ Commands:
build-repeater-bridge-espnow-firmwares: Build all repeater ESP-NOW bridge firmwares for all build targets.
build-observer-firmwares: Build all observer firmwares for all build targets.
build-observer-espnow-firmwares: Build all observer ESP-NOW firmwares for all build targets.
build-observer-mqtt-bridge-firmwares: Build all observer MQTT bridge firmwares for all build targets.
build-room-server-firmwares: Build all chat room server firmwares for all build targets.
Examples:
@@ -64,6 +65,9 @@ $ sh eastmesh-build.sh build-observer-firmwares
Build all observer ESP-NOW firmwares
$ sh eastmesh-build.sh build-observer-espnow-firmwares
Build all observer MQTT bridge firmwares
$ sh eastmesh-build.sh build-observer-mqtt-bridge-firmwares
Build all chat room server firmwares
$ sh eastmesh-build.sh build-room-server-firmwares
@@ -327,6 +331,12 @@ build_repeater_observer_espnow_firmwares() {
}
build_repeater_observer_mqtt_bridge_firmwares() {
build_all_firmwares_by_suffix "_repeater_observer_mqtt_bridge"
}
build_room_server_firmwares() {
# # build specific room server firmwares
@@ -380,6 +390,8 @@ elif [[ $1 == "build-observer-firmwares" ]]; then
build_repeater_observer_firmwares
elif [[ $1 == "build-observer-espnow-firmwares" ]]; then
build_repeater_observer_espnow_firmwares
elif [[ $1 == "build-observer-mqtt-bridge-firmwares" ]]; then
build_repeater_observer_mqtt_bridge_firmwares
elif [[ $1 == "build-room-server-firmwares" ]]; then
build_room_server_firmwares
fi
+3 -1
View File
@@ -26,7 +26,8 @@ For bridge repeaters, think local radio linking between nearby repeaters:
- use bridge firmware when two local repeaters need to exchange traffic across different MeshCore radio configs, such as `Australia (Narrow)` and `Australia (Mid)`
- pure ESP-NOW bridge repeaters do not need MQTT, the EastMesh web panel, or a screen
- prioritise stable power, suitable antennas, and placement over display features
- use `observer-eastmesh-bridge-espnow` only when the same repeater also needs MQTT uplink; otherwise `repeater-bridge-espnow` keeps the role simpler
- use `observer-eastmesh-bridge-espnow` only when the same repeater also needs MQTT uplink and a local ESP-NOW bridge; otherwise `repeater-bridge-espnow` keeps the role simpler
- use `observer-eastmesh-bridge-mqtt` when the same repeater needs MQTT uplink and bidirectional MQTT mesh bridging through a peer broker
Other notes:
@@ -52,6 +53,7 @@ The tables below are built from the repo's PlatformIO board metadata and variant
- `companion-wifi` boards are for app-connected companion devices.
- `observer-eastmesh` boards are for Wi-Fi repeaters that publish to MQTT.
- `repeater-bridge-espnow` and `observer-eastmesh-bridge-espnow` boards are for local ESP-NOW bridge use between nearby repeaters on different MeshCore radio configs.
- `observer-eastmesh-bridge-mqtt` boards bridge mesh packets through a shared MQTT topic at a peer broker you configure.
Bridge firmware is not MQTT-over-WAN or VPN bridging. Use it when two local repeaters need to exchange traffic across radio configs such as `Australia (Narrow)` and `Australia (Mid)`.
+10 -2
View File
@@ -158,7 +158,7 @@ OK
### MQTT Bridge Settings For Observer MQTT Bridge Builds
These commands are available on observer builds that compile with the MQTT mesh bridge transport (for example `Xiao_S3_WIO_repeater_observer_espnow` when built with `WITH_MQTT_BRIDGE`).
These commands are available on `*_repeater_observer_mqtt_bridge` firmware targets (release track `observer-eastmesh-bridge-mqtt`).
The MQTT bridge forwards raw mesh packets over a shared topic at a peer MQTT broker. It is separate from MQTT uplink publishing to EastMesh or MeshMapper brokers.
@@ -167,9 +167,13 @@ The MQTT bridge forwards raw mesh packets over a shared topic at a peer MQTT bro
- `set bridge.peer.host <host>`: sets the peer MQTT broker host and restarts the bridge.
- `get bridge.peer.port`: shows the configured peer MQTT broker port (defaults to `1883` when unset).
- `set bridge.peer.port <port>`: sets the peer MQTT broker port and restarts the bridge.
- `get bridge.peer.username` / `set bridge.peer.username <username>`: optional peer MQTT broker username.
- `get bridge.peer.password` / `set bridge.peer.password <password>`: optional peer MQTT broker password (`get` returns `set` or `-`, not the stored value).
- `get bridge.secret` / `set bridge.secret <secret>`: shared XOR key used by all bridge nodes on the same bridge network.
Both bridge nodes must use the same peer broker address, port, and `bridge.secret`. Mesh packets are published and subscribed on topic `meshcore/bridge/packets`.
Both bridge nodes must use the same peer broker address, port, credentials, and `bridge.secret`.
On `*_repeater_observer_mqtt_bridge` builds, MQTT uplink brokers are **disabled by default** so the mesh bridge client is not competing with EastMesh/MeshMapper WSS uplink at boot. Enable uplink brokers manually in MQTT Settings when needed.
Example:
@@ -178,6 +182,10 @@ Example:
OK
> set bridge.peer.port 1883
OK
> set bridge.peer.username meshbridge
OK
> set bridge.peer.password your-mqtt-password
OK
> set bridge.secret my-shared-secret
OK
```
+6 -3
View File
@@ -1,17 +1,20 @@
# MeshCore EastMesh Docs
MeshCore-EastMesh keeps the upstream MeshCore firmware intact and publishes four firmware tracks, depending on how the device needs to connect:
MeshCore-EastMesh keeps the upstream MeshCore firmware intact and publishes five firmware tracks, depending on how the device needs to connect:
- `companion-wifi`: use this for Wi-Fi-connected companion devices. It stays closest to upstream MeshCore and adds the EastMesh Wi-Fi rescue/configuration commands.
- `repeater-bridge-espnow`: use this when you need a plain upstream-style repeater ESP-NOW bridge without MQTT uplink or the EastMesh web panel.
- `observer-eastmesh`: use this for a Wi-Fi repeater that should publish to an MQTT broker and, on supported ESP32 boards, offer the optional local web panel for setup and troubleshooting.
- `observer-eastmesh-bridge-espnow`: use this when one repeater needs both MQTT uplink and ESP-NOW bridge duties, including bridge channel/secret controls for keeping the bridge aligned with Wi-Fi.
- `observer-eastmesh-bridge-mqtt`: use this when one repeater needs both MQTT uplink and bidirectional MQTT mesh bridging through a peer broker you configure in the web panel or CLI.
!!! note "Bridge tracks are local radio bridges"
The bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example one repeater on `Australia (Narrow)` and another on `Australia (Mid)`.
The ESP-NOW bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example one repeater on `Australia (Narrow)` and another on `Australia (Mid)`.
They are not MQTT-over-WAN, VPN, or internet bridge releases. MQTT is still the uplink/visibility path for MQTT firmware; it is not used to tunnel mesh traffic between distant sites.
The MQTT bridge track uses a shared topic at a peer MQTT broker for mesh packet bridging. It is separate from MQTT uplink publishing to EastMesh or MeshMapper.
Bridge tracks are not MQTT-over-WAN, VPN, or internet tunnel releases.
If you want guidance first, start with:
+16 -1
View File
@@ -29,6 +29,7 @@ uv run pio run -e heltec_v4_repeater_observer
uv run pio run -e heltec_v4_companion_radio_wifi
uv run pio run -e heltec_v4_repeater_bridge_espnow
uv run pio run -e heltec_v4_repeater_observer_espnow
uv run pio run -e Xiao_S3_WIO_repeater_observer_mqtt_bridge
```
Flash a target:
@@ -82,7 +83,7 @@ This produces versioned artifacts in `out/`.
Versioning rule:
- `companion-wifi` and `repeater-bridge-espnow` use the upstream MeshCore version as `FIRMWARE_VERSION`
- `observer-eastmesh` and `observer-eastmesh-bridge-espnow` use the upstream MeshCore version as `FIRMWARE_VERSION` plus the EastMesh release version as `EASTMESH_VERSION`
- `observer-eastmesh`, `observer-eastmesh-bridge-espnow`, and `observer-eastmesh-bridge-mqtt` use the upstream MeshCore version as `FIRMWARE_VERSION` plus the EastMesh release version as `EASTMESH_VERSION`
## Supported `repeater_observer` Boards
@@ -143,6 +144,20 @@ T_Beam_S3_Supreme_SX1262_repeater_bridge_espnow
T_Beam_S3_Supreme_SX1262_repeater_observer_espnow
```
## Supported `repeater_observer_mqtt_bridge` Boards
At present only the Xiao S3 WIO observer MQTT bridge target is defined:
```text
Xiao_S3_WIO_repeater_observer_mqtt_bridge
```
List all observer MQTT bridge targets:
```bash
bash eastmesh-build.sh list | grep '_repeater_observer_mqtt_bridge'
```
Bridge firmware is for local ESP-NOW bridge use between nearby repeaters. It is not MQTT-over-WAN or VPN bridging.
## Supported `companion_radio_wifi` Boards
+21 -3
View File
@@ -22,7 +22,7 @@ If you are not sure which track you need, start with `companion-wifi` for app-co
## Pick Your Track
EastMesh publishes four release tracks:
EastMesh publishes five release tracks:
| Track | Use it when | Firmware filename suffix |
| ----- | ----------- | ------------------------ |
@@ -30,12 +30,15 @@ EastMesh publishes four release tracks:
| `observer-eastmesh` | You want a repeater with Wi-Fi and MQTT uplink, usually feeding broker visibility such as EastMesh/CoreScope. | `*_repeater_observer` |
| `repeater-bridge-espnow` | You want a local ESP-NOW bridge between nearby repeaters, without MQTT uplink or the EastMesh web panel. | `*_repeater_bridge_espnow` |
| `observer-eastmesh-bridge-espnow` | You want one repeater to provide both MQTT uplink and local ESP-NOW bridge duties. | `*_repeater_observer_espnow` |
| `observer-eastmesh-bridge-mqtt` | You want one repeater to provide both MQTT uplink and bidirectional MQTT mesh bridging to a peer broker. | `*_repeater_observer_mqtt_bridge` |
!!! note "Bridge firmware is not a WAN bridge"
Bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example `Australia (Narrow)` and `Australia (Mid)`.
ESP-NOW bridge tracks are for bridging two nearby repeaters that operate on different MeshCore radio configs, for example `Australia (Narrow)` and `Australia (Mid)`.
They do not use MQTT to tunnel mesh traffic over the internet, WAN links, or VPNs.
The MQTT bridge track forwards mesh packets through a shared topic at a peer MQTT broker you configure. It is separate from MQTT uplink publishing to EastMesh or MeshMapper.
Bridge tracks do not use MQTT uplink brokers to tunnel mesh traffic over the internet, WAN links, or VPNs.
## Pick The Right Asset
@@ -48,6 +51,7 @@ Examples:
- `heltec_v4_repeater_observer-v1.15.0-eastmesh-v2026.5.1-abcdef-merged.bin`
- `heltec_v4_repeater_bridge_espnow-v1.15.0-abcdef.bin`
- `heltec_v4_repeater_observer_espnow-v1.15.0-eastmesh-v2026.5.1-abcdef.bin`
- `Xiao_S3_WIO_repeater_observer_mqtt_bridge-v1.15.0-eastmesh-v2026.7.0-abcdef.bin`
The important part is the board/env prefix:
@@ -55,6 +59,7 @@ The important part is the board/env prefix:
- `*_repeater_observer`
- `*_repeater_bridge_espnow`
- `*_repeater_observer_espnow`
- `*_repeater_observer_mqtt_bridge`
## Which File To Flash
@@ -187,6 +192,19 @@ Typical first steps after flashing:
- set `bridge.channel` to match that Wi-Fi channel
- set the same `bridge.secret` on every local ESP-NOW bridge node that should talk together
### Observer MQTT Bridge
`repeater_observer_mqtt_bridge` builds combine the observer role with bidirectional MQTT mesh bridging.
Typical first steps after flashing:
- set `wifi.ssid`
- set `wifi.pwd`
- set `mqtt.iata`
- confirm `get mqtt.status`
- set `bridge.peer.host` and `bridge.peer.port` to your peer MQTT broker
- set the same `bridge.secret` on every MQTT bridge node that should talk together
### Repeater ESP-NOW Bridge
`repeater_bridge_espnow` builds are for local ESP-NOW bridge nodes without MQTT uplink.
+2 -2
View File
@@ -255,7 +255,7 @@ This section includes:
- `mqtt.email`: owner contact email.
- MQTT brokers: **Primary MQTT** and **Secondary MQTT** dropdowns, each selecting one of `eastmesh-au`, `meshmapper`, `Custom`, the retired `letsmesh-eu`/`letsmesh-us`, or `None`. The two slots enforce the two-broker maximum, and a broker chosen in one slot is disabled in the other.
- custom MQTT `host:port`, TCP/WSS transport, username, and password fields, shown when `Custom` is selected in either slot.
- mesh bridge peer MQTT `host:port`, shown on MQTT bridge builds (`get bridge.type` returns `mqtt`). This is separate from MQTT uplink brokers and points at the shared peer broker used for bidirectional mesh packet bridging.
- mesh bridge peer MQTT `host:port`, optional username and password, shown on MQTT bridge builds (`get bridge.type` returns `mqtt`). This is separate from MQTT uplink brokers and points at the shared peer broker used for bidirectional mesh packet bridging.
`UNSET - To be configured` is the default for new observer installs until a real saved value exists.
@@ -270,7 +270,7 @@ Notes:
- turning off a connected MQTT server publishes retained offline status before the client disconnects
- changing `mqtt.iata` away from a configured value publishes retained offline status to the old status topic, restarts connected broker clients, and reconnects under the new topic path
- at most two MQTT brokers can be enabled at once
- on MQTT bridge builds, both bridge nodes must use the same peer broker host, port, and `bridge.secret`
- on MQTT bridge builds, both bridge nodes must use the same peer broker host, port, credentials, and `bridge.secret`
## `/stats` Overview
+25
View File
@@ -4,6 +4,7 @@ tracks:
- repeater-bridge-espnow
- observer-eastmesh
- observer-eastmesh-bridge-espnow
- observer-eastmesh-bridge-mqtt
generated_from: "Adjacent git tag comparisons plus non-merge commit subjects."
releases:
@@ -393,3 +394,27 @@ releases:
breaking_changes:
- "`get mqtt.status` output changed from per-broker fields to `p:`/`s:` broker slots; anything parsing the previous format must be updated."
- "Saved LetsMesh EU/US broker selections are cleared once on upgrade; re-enable a broker such as MeshMapper if a second uplink is required."
- track: observer-eastmesh-bridge-mqtt
version: "2026.7.0"
tag: "observer-eastmesh-bridge-mqtt-v2026.7.0"
date: "2026-06-24"
previous_version: null
summary: "Introduces the observer-eastmesh-bridge-mqtt release track with bidirectional MQTT mesh bridging and web-panel peer broker configuration."
changes:
- type: added
area: bridge
text: "Added `WITH_MQTT_BRIDGE` and `MQTTBridge` for bidirectional mesh packet transport over a shared MQTT topic at a peer broker."
- type: added
area: cli
text: "Added `get/set bridge.peer.host` and `get/set bridge.peer.port` for configuring the peer MQTT broker used by the mesh bridge."
- type: added
area: web
text: "Added mesh bridge peer MQTT host:port controls to the web panel on MQTT bridge builds."
- type: added
area: release
text: "Added the `observer-eastmesh-bridge-mqtt-v*` release tag format, PlatformIO `*_repeater_observer_mqtt_bridge` environments, and CI build workflow."
- type: added
area: boards
text: "Added `Xiao_S3_WIO_repeater_observer_mqtt_bridge` as the first supported observer MQTT bridge target."
breaking_changes: []
+24 -2
View File
@@ -109,7 +109,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->bridge_peer_port)) {
file.read((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 360
}
// next: 362
if (file.available() >= (int)sizeof(_prefs->bridge_peer_username)) {
file.read((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 362
}
if (file.available() >= (int)sizeof(_prefs->bridge_peer_password)) {
file.read((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 427
}
// next: 523
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -211,7 +217,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
file.write((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 296
file.write((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 360
// next: 362
file.write((uint8_t *)&_prefs->bridge_peer_username, sizeof(_prefs->bridge_peer_username)); // 362
file.write((uint8_t *)&_prefs->bridge_peer_password, sizeof(_prefs->bridge_peer_password)); // 427
// next: 523
file.close();
}
@@ -812,6 +820,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "bridge.peer.username ", 21) == 0) {
StrHelper::strncpy(_prefs->bridge_peer_username, &config[21], sizeof(_prefs->bridge_peer_username));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "bridge.peer.password ", 21) == 0) {
StrHelper::strncpy(_prefs->bridge_peer_password, &config[21], sizeof(_prefs->bridge_peer_password));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "bridge.secret ", 14) == 0) {
StrHelper::strncpy(_prefs->bridge_secret, &config[14], sizeof(_prefs->bridge_secret));
_callbacks->restartBridge();
@@ -960,6 +978,10 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", _prefs->bridge_peer_host);
} else if (memcmp(config, "bridge.peer.port", 16) == 0) {
sprintf(reply, "> %u", _prefs->bridge_peer_port != 0 ? _prefs->bridge_peer_port : 1883);
} else if (memcmp(config, "bridge.peer.username", 20) == 0) {
sprintf(reply, "> %s", _prefs->bridge_peer_username[0] ? _prefs->bridge_peer_username : "-");
} else if (memcmp(config, "bridge.peer.password", 20) == 0) {
sprintf(reply, "> %s", _prefs->bridge_peer_password[0] ? "set" : "-");
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
+2
View File
@@ -67,6 +67,8 @@ struct NodePrefs { // persisted to file
uint16_t fan_timeout_secs;
char bridge_peer_host[64]; // peer MQTT broker host (MQTT bridge only)
uint16_t bridge_peer_port; // peer MQTT broker port (MQTT bridge only, default 1883)
char bridge_peer_username[65]; // peer MQTT broker username (MQTT bridge only)
char bridge_peer_password[96]; // peer MQTT broker password (MQTT bridge only)
};
class CommonCLICallbacks {
+36 -9
View File
@@ -49,7 +49,7 @@ const char *MQTTBridge::kBridgeTopic = "meshcore/bridge/packets";
MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _client(nullptr), _connected(false), _started(false),
_next_connect_attempt(0), _reconnect_failures(0) {
_pending_destroy(false), _next_connect_attempt(0), _reconnect_failures(0) {
_instance = this;
_client_id[0] = 0;
}
@@ -72,6 +72,13 @@ void MQTTBridge::destroyClient() {
}
_connected = false;
_started = false;
_pending_destroy = false;
}
void MQTTBridge::scheduleClientDestroy() {
_connected = false;
_started = false;
_pending_destroy = true;
}
void MQTTBridge::begin() {
@@ -108,9 +115,7 @@ void MQTTBridge::onMqttConnected() {
}
void MQTTBridge::onMqttDisconnected() {
_connected = false;
_started = false;
destroyClient();
scheduleClientDestroy();
_next_connect_attempt = millis() + connectRetryDelayMillis(_reconnect_failures);
if (_reconnect_failures < 255) {
++_reconnect_failures;
@@ -130,6 +135,9 @@ void MQTTBridge::mqttEventHandler(void *handler_args, esp_event_base_t, int32_t
case MQTT_EVENT_DISCONNECTED:
bridge->onMqttDisconnected();
break;
case MQTT_EVENT_ERROR:
bridge->onMqttDisconnected();
break;
case MQTT_EVENT_DATA: {
auto *event = static_cast<esp_mqtt_event_handle_t>(event_data);
if (event != nullptr && event->data_len > 0) {
@@ -160,6 +168,12 @@ bool MQTTBridge::ensureClient() {
cfg.broker.address.port = peerPort(_prefs);
cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP;
cfg.credentials.client_id = _client_id;
if (_prefs->bridge_peer_username[0] != 0) {
cfg.credentials.username = _prefs->bridge_peer_username;
}
if (_prefs->bridge_peer_password[0] != 0) {
cfg.credentials.authentication.password = _prefs->bridge_peer_password;
}
cfg.session.keepalive = 30;
cfg.network.reconnect_timeout_ms = 10000;
cfg.network.timeout_ms = 10000;
@@ -171,6 +185,12 @@ bool MQTTBridge::ensureClient() {
cfg.port = peerPort(_prefs);
cfg.transport = MQTT_TRANSPORT_OVER_TCP;
cfg.client_id = _client_id;
if (_prefs->bridge_peer_username[0] != 0) {
cfg.username = _prefs->bridge_peer_username;
}
if (_prefs->bridge_peer_password[0] != 0) {
cfg.password = _prefs->bridge_peer_password;
}
cfg.keepalive = 30;
cfg.buffer_size = MAX_MQTT_PACKET_SIZE;
cfg.out_buffer_size = MAX_MQTT_PACKET_SIZE;
@@ -201,6 +221,11 @@ void MQTTBridge::loop() {
return;
}
if (_pending_destroy) {
destroyClient();
return;
}
if (WiFi.status() != WL_CONNECTED) {
if (_client != nullptr) {
destroyClient();
@@ -208,12 +233,14 @@ void MQTTBridge::loop() {
return;
}
if (_client == nullptr) {
if (_next_connect_attempt != 0 && millis() < _next_connect_attempt) {
return;
}
ensureClient();
if (_client != nullptr) {
return;
}
if (_next_connect_attempt != 0 && millis() < _next_connect_attempt) {
return;
}
ensureClient();
}
void MQTTBridge::handleMqttData(const uint8_t *data, size_t len) {
+2
View File
@@ -28,12 +28,14 @@ private:
esp_mqtt_client_handle_t _client;
bool _connected;
bool _started;
bool _pending_destroy;
unsigned long _next_connect_attempt;
uint8_t _reconnect_failures;
char _client_id[24];
void xorCrypt(uint8_t *data, size_t len);
void destroyClient();
void scheduleClientDestroy();
bool ensureClient();
void handleMqttData(const uint8_t *data, size_t len);
void onMqttConnected();
+12
View File
@@ -13,7 +13,12 @@ constexpr uint32_t kFixedStatusIntervalMs = 300000;
void MQTTPrefsStore::setDefaults(MQTTPrefs& prefs) {
memset(&prefs, 0, sizeof(prefs));
prefs.magic = kMagic;
#if defined(WITH_MQTT_BRIDGE)
prefs.enabled_mask = 0x00;
prefs.mqtt_bridge_uplink_migrated = 1;
#else
prefs.enabled_mask = 0x01;
#endif
prefs.packets_enabled = 1;
prefs.raw_enabled = 0;
prefs.status_enabled = 1;
@@ -83,6 +88,13 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
prefs.brokers_migrated = 1;
save(fs, prefs);
}
#if defined(WITH_MQTT_BRIDGE)
if (!prefs.mqtt_bridge_uplink_migrated) {
prefs.enabled_mask = 0;
prefs.mqtt_bridge_uplink_migrated = 1;
save(fs, prefs);
}
#endif
return true;
}
+1
View File
@@ -37,6 +37,7 @@ struct MQTTPrefs {
// Appended fields must stay at the end: older prefs files are shorter and
// read back as zero here, which drives one-time migrations (see load()).
uint8_t brokers_migrated;
uint8_t mqtt_bridge_uplink_migrated;
};
class MQTTPrefsStore {
+20 -1
View File
@@ -1033,7 +1033,24 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<button id="refreshBridgePeerEndpointBtn" class="iconbtn" title="Refresh bridge peer MQTT host and port">&#8635;</button>
<button id="saveBridgePeerEndpointBtn" class="savebtn">Save</button>
</div>
<div class="panel-note">Both bridge nodes must use the same peer broker, port, and bridge secret.</div>
<div class="panel-note">Both bridge nodes must use the same peer broker, port, credentials, and bridge secret.</div>
<div class="row">
<div class="field-card">
<label class="label" for="bridgePeerUsername">Username</label>
<div class="inline-actions">
<input id="bridgePeerUsername" placeholder="optional" maxlength="64">
<button class="iconbtn" data-load-cmd="get bridge.peer.username" data-load-input="bridgePeerUsername" title="Refresh bridge peer MQTT username">&#8635;</button>
<button class="savebtn" data-prefix="set bridge.peer.username " data-input="bridgePeerUsername">Save</button>
</div>
</div>
<div class="field-card">
<label class="label" for="bridgePeerPassword">Password</label>
<div class="inline-actions two-actions">
<input id="bridgePeerPassword" type="password" placeholder="optional" maxlength="95">
<button class="savebtn" data-prefix="set bridge.peer.password " data-input="bridgePeerPassword">Save</button>
</div>
</div>
</div>
</div>
</div>
</section>
@@ -1230,6 +1247,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
function parseClientEnv(clientEnv) {
const env = String(clientEnv || "").trim();
const suffixes = [
{ suffix:"_repeater_observer_mqtt_bridge", firmware:"repeater_observer_mqtt_bridge" },
{ suffix:"_repeater_observer_espnow", firmware:"repeater_observer_espnow" },
{ suffix:"_repeater_observer", firmware:"repeater_observer" },
{ suffix:"_repeater_bridge_espnow", firmware:"repeater_bridge_espnow" },
@@ -2860,6 +2878,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const input = document.getElementById("bridgePeerEndpoint");
if (!input) return;
input.value = host ? `${host}:${port}` : "";
await loadField("get bridge.peer.username", "bridgePeerUsername", null, options);
}
async function saveBridgePeerEndpoint() {
const input = document.getElementById("bridgePeerEndpoint");
+9
View File
@@ -1154,8 +1154,17 @@ build_src_filter = ${env:WHY2025_badge_repeater_observer.build_src_filter}
[env:Xiao_S3_WIO_repeater_observer_espnow]
extends = env:Xiao_S3_WIO_repeater_observer
build_flags =
${env:Xiao_S3_WIO_repeater_observer.build_flags}
-D WITH_ESPNOW_BRIDGE=1
build_src_filter = ${env:Xiao_S3_WIO_repeater_observer.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
[env:Xiao_S3_WIO_repeater_observer_mqtt_bridge]
extends = env:Xiao_S3_WIO_repeater_observer
build_flags =
${env:Xiao_S3_WIO_repeater_observer.build_flags}
-D WITH_MQTT_BRIDGE=1
-UMQTT_DEBUG
build_src_filter = ${env:Xiao_S3_WIO_repeater_observer.build_src_filter}
+<helpers/bridges/MQTTBridge.cpp>