Merge pull request #73 from prabathbr/main

Adding observer-eastmesh-bridge-mqtt track to bridge over a MQTT broker🤖🤖
This commit is contained in:
xJARiD
2026-06-26 12:29:03 +10:00
committed by GitHub
23 changed files with 887 additions and 16 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)
@@ -159,6 +163,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.
@@ -198,6 +317,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`
@@ -210,6 +330,7 @@ The current release workflows intentionally focus only on:
- `repeater-bridge-espnow`
- `observer-eastmesh`
- `observer-eastmesh-bridge-espnow`
- `observer-eastmesh-bridge-mqtt`
## Release Tags
@@ -220,6 +341,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.
@@ -243,3 +365,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)`.
+35 -1
View File
@@ -129,7 +129,7 @@ Default servers are `au.pool.ntp.org`, `time.google.com`, and `time.cloudflare.c
### ESP-NOW Bridge Settings For Observer ESP-NOW Builds
These commands are available on `*_repeater_observer_espnow` firmware targets.
These commands are available on `*_repeater_observer_espnow` firmware targets that use the ESP-NOW bridge transport.
Bridge commands are for local ESP-NOW bridge use between nearby repeaters, such as linking repeaters on `Australia (Narrow)` and `Australia (Mid)`. They are not MQTT-over-WAN, VPN, or internet bridge controls.
@@ -159,6 +159,40 @@ Example:
OK
```
### MQTT Bridge Settings For Observer MQTT Bridge Builds
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.
- `get bridge.type`: returns `mqtt` on MQTT bridge builds.
- `get bridge.peer.host`: shows the configured peer MQTT broker host.
- `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, 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:
```text
> set bridge.peer.host 192.168.1.10
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
```
### Web Panel Controls
- `get web`
+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
View File
@@ -255,6 +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`, 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.
@@ -269,6 +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, credentials, and `bridge.secret`
## `/stats` Overview
+8
View File
@@ -533,6 +533,8 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
reply_data[8] |= 0x01; // is bridge, type UART
#elif WITH_ESPNOW_BRIDGE
reply_data[8] |= 0x03; // is bridge, type ESP-NOW
#elif WITH_MQTT_BRIDGE
reply_data[8] |= 0x04; // is bridge, type MQTT
#endif
if (_prefs.disable_fwd) { // is this repeater currently disabled
reply_data[8] |= 0x80; // is disabled
@@ -1198,6 +1200,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#if defined(WITH_ESPNOW_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
#endif
#if defined(WITH_MQTT_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
#endif
#if defined(WITH_MQTT_UPLINK)
, mqtt(rtc, self_id)
#endif
@@ -1254,6 +1259,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.bridge_channel = 1; // channel 1
StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret));
#if defined(WITH_MQTT_BRIDGE)
_prefs.bridge_peer_port = 1883;
#endif
// GPS defaults
_prefs.gps_enabled = 0;
+7
View File
@@ -23,6 +23,11 @@
#define WITH_BRIDGE
#endif
#ifdef WITH_MQTT_BRIDGE
#include "helpers/bridges/MQTTBridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_MQTT_UPLINK
#include <helpers/mqtt/MQTTUplink.h>
#endif
@@ -170,6 +175,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm
RS232Bridge bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#elif defined(WITH_MQTT_BRIDGE)
MQTTBridge bridge;
#endif
#ifdef WITH_MQTT_UPLINK
MQTTUplink mqtt;
+26
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:
@@ -418,6 +419,30 @@ releases:
- "`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."
<<<<<<< HEAD
- 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."
=======
- track: observer-eastmesh-bridge-espnow
version: "2026.6.5"
tag: "observer-eastmesh-bridge-espnow-v2026.6.5"
@@ -440,4 +465,5 @@ releases:
- type: docs
area: docs
text: "Documented the Waev broker in the README and custom CLI guides."
>>>>>>> cdf2cc6fa283615a04c488ca3ad324a33b72966d
breaking_changes: []
+81 -2
View File
@@ -103,7 +103,19 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->flood_max_advert)) {
file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
}
// next: 296
if (file.available() >= (int)sizeof(_prefs->bridge_peer_host)) {
file.read((uint8_t *)&_prefs->bridge_peer_host, sizeof(_prefs->bridge_peer_host)); // 296
}
if (file.available() >= (int)sizeof(_prefs->bridge_peer_port)) {
file.read((uint8_t *)&_prefs->bridge_peer_port, sizeof(_prefs->bridge_peer_port)); // 360
}
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);
@@ -125,6 +137,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
if (_prefs->bridge_peer_port > 65535) {
_prefs->bridge_peer_port = 1883;
}
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
@@ -200,7 +215,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292
file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
// next: 296
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
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();
}
@@ -770,6 +789,52 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
#endif
#ifdef WITH_MQTT_BRIDGE
} else if (memcmp(config, "bridge.peer.host ", 17) == 0) {
const char* host = &config[17];
size_t oi = 0;
char cleaned[sizeof(_prefs->bridge_peer_host)];
memset(cleaned, 0, sizeof(cleaned));
for (size_t i = 0; host[i] != 0 && oi + 1 < sizeof(cleaned); ++i) {
unsigned char c = static_cast<unsigned char>(host[i]);
if (c <= ' ' || c == '/' || c == ':' || c == '\\') {
strcpy(reply, "Error: invalid host");
return;
}
cleaned[oi++] = static_cast<char>(c);
}
cleaned[oi] = 0;
StrHelper::strncpy(_prefs->bridge_peer_host, cleaned, sizeof(_prefs->bridge_peer_host));
_callbacks->restartBridge();
savePrefs();
strcpy(reply, "OK");
} else if (memcmp(config, "bridge.peer.port ", 17) == 0) {
char* end = nullptr;
unsigned long parsed = strtoul(&config[17], &end, 10);
if (end == &config[17] || *end != 0 || parsed == 0 || parsed > 65535UL) {
strcpy(reply, "Error: port must be 1-65535");
return;
}
_prefs->bridge_peer_port = static_cast<uint16_t>(parsed);
_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();
savePrefs();
strcpy(reply, "OK");
#endif
} else if (memcmp(config, "adc.multiplier ", 15) == 0) {
_prefs->adc_multiplier = atof(&config[15]);
@@ -884,6 +949,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
#elif WITH_MQTT_BRIDGE
"mqtt"
#else
"none"
#endif
@@ -905,6 +972,18 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel);
} else if (memcmp(config, "bridge.secret", 13) == 0) {
sprintf(reply, "> %s", _prefs->bridge_secret);
#endif
#ifdef WITH_MQTT_BRIDGE
} else if (memcmp(config, "bridge.peer.host", 16) == 0) {
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
} else if (memcmp(config, "bootloader.ver", 14) == 0) {
#ifdef NRF52_PLATFORM
+6 -2
View File
@@ -6,7 +6,7 @@
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE)
#define WITH_BRIDGE
#endif
@@ -50,7 +50,7 @@ struct NodePrefs { // persisted to file
uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx)
uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200)
uint8_t bridge_channel; // 1-14 (ESP-NOW only)
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW / MQTT)
// Power setting
uint8_t powersaving_enabled; // boolean
// Gps settings
@@ -65,6 +65,10 @@ struct NodePrefs { // persisted to file
uint8_t loop_detect;
uint8_t fan_mode;
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 {
+335
View File
@@ -0,0 +1,335 @@
#include "MQTTBridge.h"
#ifdef WITH_MQTT_BRIDGE
#if defined(ESP_PLATFORM)
#include <Arduino.h>
#include <WiFi.h>
#include <ctype.h>
#include <string.h>
#ifndef BRIDGE_DEBUG
#define BRIDGE_DEBUG 0
#endif
#if BRIDGE_DEBUG
#define BRIDGE_DEBUG_PRINTLN(...) Serial.printf(__VA_ARGS__)
#else
#define BRIDGE_DEBUG_PRINTLN(...) do { } while (0)
#endif
namespace {
constexpr unsigned long kConnectRetryBaseMillis = 10000;
constexpr unsigned long kConnectRetryMaxMillis = 120000;
unsigned long connectRetryDelayMillis(uint8_t failures) {
unsigned long delay_ms = kConnectRetryBaseMillis;
if (failures > 0) {
uint8_t shifts = min<uint8_t>(failures - 1, 3);
delay_ms <<= shifts;
}
if (delay_ms > kConnectRetryMaxMillis) {
delay_ms = kConnectRetryMaxMillis;
}
return delay_ms;
}
bool peerHostConfigured(const NodePrefs *prefs) {
return prefs != nullptr && prefs->bridge_peer_host[0] != 0;
}
uint16_t peerPort(const NodePrefs *prefs) {
return prefs->bridge_peer_port != 0 ? prefs->bridge_peer_port : 1883;
}
} // namespace
MQTTBridge *MQTTBridge::_instance = nullptr;
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),
_pending_destroy(false), _next_connect_attempt(0), _reconnect_failures(0) {
_instance = this;
_client_id[0] = 0;
}
void MQTTBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = strlen(_prefs->bridge_secret);
if (keyLen == 0) {
return;
}
for (size_t i = 0; i < len; i++) {
data[i] ^= _prefs->bridge_secret[i % keyLen];
}
}
void MQTTBridge::destroyClient() {
if (_client != nullptr) {
esp_mqtt_client_stop(_client);
esp_mqtt_client_destroy(_client);
_client = nullptr;
}
_connected = false;
_started = false;
_pending_destroy = false;
}
void MQTTBridge::scheduleClientDestroy() {
_connected = false;
_started = false;
_pending_destroy = true;
}
void MQTTBridge::begin() {
BRIDGE_DEBUG_PRINTLN("MQTT bridge initializing\n");
if (!peerHostConfigured(_prefs)) {
BRIDGE_DEBUG_PRINTLN("MQTT bridge peer host not configured\n");
return;
}
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(_client_id, sizeof(_client_id), "mc-br-%02x%02x%02x", mac[3], mac[4], mac[5]);
_initialized = true;
_next_connect_attempt = 0;
_reconnect_failures = 0;
}
void MQTTBridge::end() {
BRIDGE_DEBUG_PRINTLN("MQTT bridge stopping\n");
destroyClient();
_initialized = false;
}
void MQTTBridge::onMqttConnected() {
_connected = true;
_reconnect_failures = 0;
_next_connect_attempt = 0;
if (_client != nullptr) {
esp_mqtt_client_subscribe(_client, kBridgeTopic, 0);
}
BRIDGE_DEBUG_PRINTLN("MQTT bridge connected to %s:%u\n", _prefs->bridge_peer_host,
static_cast<unsigned>(peerPort(_prefs)));
}
void MQTTBridge::onMqttDisconnected() {
scheduleClientDestroy();
_next_connect_attempt = millis() + connectRetryDelayMillis(_reconnect_failures);
if (_reconnect_failures < 255) {
++_reconnect_failures;
}
}
void MQTTBridge::mqttEventHandler(void *handler_args, esp_event_base_t, int32_t event_id, void *event_data) {
auto *bridge = static_cast<MQTTBridge *>(handler_args);
if (bridge == nullptr) {
return;
}
switch (event_id) {
case MQTT_EVENT_CONNECTED:
bridge->onMqttConnected();
break;
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) {
bridge->handleMqttData(reinterpret_cast<const uint8_t *>(event->data),
static_cast<size_t>(event->data_len));
}
break;
}
default:
break;
}
}
bool MQTTBridge::ensureClient() {
if (_client != nullptr) {
return true;
}
if (!peerHostConfigured(_prefs)) {
return false;
}
if (WiFi.status() != WL_CONNECTED) {
return false;
}
esp_mqtt_client_config_t cfg = {};
#if ESP_IDF_VERSION_MAJOR >= 5
cfg.broker.address.hostname = _prefs->bridge_peer_host;
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;
cfg.network.disable_auto_reconnect = true;
cfg.buffer.size = MAX_MQTT_PACKET_SIZE;
cfg.buffer.out_size = MAX_MQTT_PACKET_SIZE;
#else
cfg.host = _prefs->bridge_peer_host;
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;
cfg.reconnect_timeout_ms = 10000;
cfg.network_timeout_ms = 10000;
cfg.disable_auto_reconnect = true;
#endif
_client = esp_mqtt_client_init(&cfg);
if (_client == nullptr) {
BRIDGE_DEBUG_PRINTLN("MQTT bridge client init failed\n");
return false;
}
esp_mqtt_client_register_event(_client, MQTT_EVENT_ANY, &MQTTBridge::mqttEventHandler, this);
if (esp_mqtt_client_start(_client) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("MQTT bridge client start failed\n");
destroyClient();
return false;
}
_started = true;
return true;
}
void MQTTBridge::loop() {
if (!_initialized) {
return;
}
if (_pending_destroy) {
destroyClient();
return;
}
if (WiFi.status() != WL_CONNECTED) {
if (_client != nullptr) {
destroyClient();
}
return;
}
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) {
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
BRIDGE_DEBUG_PRINTLN("MQTT RX packet too small, len=%u\n", static_cast<unsigned>(len));
return;
}
if (len > MAX_MQTT_PACKET_SIZE) {
BRIDGE_DEBUG_PRINTLN("MQTT RX packet too large, len=%u\n", static_cast<unsigned>(len));
return;
}
uint16_t received_magic = (data[0] << 8) | data[1];
if (received_magic != BRIDGE_PACKET_MAGIC) {
BRIDGE_DEBUG_PRINTLN("MQTT RX invalid magic 0x%04X\n", received_magic);
return;
}
uint8_t decrypted[MAX_MQTT_PACKET_SIZE];
const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE;
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
xorCrypt(decrypted, encryptedDataLen);
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE;
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
BRIDGE_DEBUG_PRINTLN("MQTT RX checksum mismatch, rcv=0x%04X\n", received_checksum);
return;
}
mesh::Packet *pkt = _mgr->allocNew();
if (!pkt) {
return;
}
if (pkt->readFrom(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen)) {
onPacketReceived(pkt);
} else {
_mgr->free(pkt);
}
}
void MQTTBridge::sendPacket(mesh::Packet *packet) {
if (!_initialized || !_connected || _client == nullptr || packet == nullptr) {
return;
}
if (_seen_packets.hasSeen(packet)) {
return;
}
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
BRIDGE_DEBUG_PRINTLN("MQTT TX packet too large (payload=%u, max=%u)\n", meshPacketLen,
static_cast<unsigned>(MAX_PAYLOAD_SIZE));
return;
}
uint8_t buffer[MAX_MQTT_PACKET_SIZE];
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;
const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE;
memcpy(buffer + packetOffset, sizingBuffer, meshPacketLen);
uint16_t checksum = fletcher16(buffer + packetOffset, meshPacketLen);
buffer[2] = (checksum >> 8) & 0xFF;
buffer[3] = checksum & 0xFF;
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
int msg_id = esp_mqtt_client_publish(_client, kBridgeTopic, reinterpret_cast<const char *>(buffer),
static_cast<int>(totalPacketSize), 0, 0);
if (msg_id >= 0) {
BRIDGE_DEBUG_PRINTLN("MQTT TX, len=%u\n", meshPacketLen);
} else {
BRIDGE_DEBUG_PRINTLN("MQTT TX FAILED\n");
}
}
void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif // ESP_PLATFORM
#endif // WITH_MQTT_BRIDGE
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "helpers/bridges/BridgeBase.h"
#ifdef WITH_MQTT_BRIDGE
#if defined(ESP_PLATFORM)
#include <mqtt_client.h>
#endif
/**
* @brief Bridge implementation using MQTT for bidirectional mesh packet transport
*
* Publishes and subscribes on a shared topic at a peer MQTT broker. Uses the same
* binary framing and XOR encryption as other bridge types for network isolation.
*/
class MQTTBridge : public BridgeBase {
private:
static MQTTBridge *_instance;
static void mqttEventHandler(void *handler_args, esp_event_base_t base, int32_t event_id,
void *event_data);
static const char *kBridgeTopic;
static const size_t MAX_MQTT_PACKET_SIZE = 512;
static const size_t MAX_PAYLOAD_SIZE = MAX_MQTT_PACKET_SIZE - (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE);
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();
void onMqttDisconnected();
public:
MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
void begin() override;
void end() override;
void loop() override;
void onPacketReceived(mesh::Packet *packet) override;
void sendPacket(mesh::Packet *packet) override;
};
#endif
+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 {
+71 -1
View File
@@ -1027,6 +1027,32 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<div class="panel-note">A maximum of two MQTT brokers can be enabled at once.</div>
<div id="mqttBrokerWarning" class="panel-warning"></div>
</div>
<div class="field-card" id="bridgePeerConfig" style="display:none">
<label class="label" for="bridgePeerEndpoint">Mesh bridge peer MQTT host:port</label>
<div class="inline-actions">
<input id="bridgePeerEndpoint" placeholder="192.168.1.10:1883" maxlength="80">
<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, 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>
@@ -1222,6 +1248,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" },
@@ -2835,6 +2862,40 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
if (!portResult.ok) return;
input.value = `${parsed.host}:${parsed.port}`;
}
async function loadBridgePeerEndpoint(options = {}) {
const config = document.getElementById("bridgePeerConfig");
if (!config) return;
const typeResult = await runCommand("get bridge.type", options);
if (!typeResult.ok || parseReplyValue(typeResult.text) !== "mqtt") {
config.style.display = "none";
return;
}
config.style.display = "";
const hostResult = await runCommand("get bridge.peer.host", options);
if (!hostResult.ok) return;
const portResult = await runCommand("get bridge.peer.port", options);
if (!portResult.ok) return;
const host = parseReplyValue(hostResult.text);
const port = parseReplyValue(portResult.text);
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");
if (!input) return;
const parsed = parseCustomEndpoint(input.value);
if (!parsed) {
statusEl.textContent = "Use host:port, for example 192.168.1.10:1883";
return;
}
const hostResult = await runCommand("set bridge.peer.host " + parsed.host);
if (!hostResult.ok) return;
const portResult = await runCommand("set bridge.peer.port " + parsed.port);
if (!portResult.ok) return;
input.value = `${parsed.host}:${parsed.port}`;
}
async function loadRadioConfig(options = {}) {
const result = await runCommand("get radio", options);
if (!result.ok) {
@@ -3017,6 +3078,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
});
document.getElementById("refreshCustomEndpointBtn").onclick = () => loadCustomEndpoint();
document.getElementById("saveCustomEndpointBtn").onclick = () => saveCustomEndpoint();
const refreshBridgePeerEndpointBtn = document.getElementById("refreshBridgePeerEndpointBtn");
if (refreshBridgePeerEndpointBtn) {
refreshBridgePeerEndpointBtn.onclick = () => loadBridgePeerEndpoint();
}
const saveBridgePeerEndpointBtn = document.getElementById("saveBridgePeerEndpointBtn");
if (saveBridgePeerEndpointBtn) {
saveBridgePeerEndpointBtn.onclick = () => saveBridgePeerEndpoint();
}
const customTransportSlider = document.getElementById("mqttCustomTransport");
if (customTransportSlider) {
customTransportSlider.addEventListener("input", () => {
@@ -3200,7 +3269,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadBrokerState("get mqtt.custom", "mqttCustom", quiet),
() => loadCustomEndpoint(quiet),
() => loadCustomTransport(quiet),
() => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet)
() => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet),
() => loadBridgePeerEndpoint(quiet)
]);
if (!isCurrentPageLoad(generation)) return;
statusEl.textContent = "Ready";
+9
View File
@@ -1159,3 +1159,12 @@ 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>