From e706d9044ceafe4f8d26f028f05fc5655c58f402 Mon Sep 17 00:00:00 2001 From: prabathbr Date: Wed, 24 Jun 2026 23:04:42 +1000 Subject: [PATCH] test with single radio - mqtt success at broker --- .../setup-build-environment/action.yml | 7 + ...h-build-observer-mqtt-bridge-firmwares.yml | 41 ++++++ AGENTS.md | 4 +- README.md | 123 ++++++++++++++++++ RELEASE.md | 9 +- eastmesh-build.sh | 12 ++ eastmesh-docs/boards.md | 4 +- eastmesh-docs/custom-cli.md | 12 +- eastmesh-docs/index.md | 9 +- eastmesh-docs/local-builds.md | 17 ++- eastmesh-docs/releases.md | 24 +++- eastmesh-docs/web-panel.md | 4 +- release-notes.yml | 25 ++++ src/helpers/CommonCLI.cpp | 26 +++- src/helpers/CommonCLI.h | 2 + src/helpers/bridges/MQTTBridge.cpp | 45 +++++-- src/helpers/bridges/MQTTBridge.h | 2 + src/helpers/mqtt/MQTTPrefs.cpp | 12 ++ src/helpers/mqtt/MQTTPrefs.h | 1 + src/helpers/web/WebPanelServer.cpp | 21 ++- variants/eastmesh_mqtt/platformio.ini | 9 ++ 21 files changed, 383 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index 3354c260..01c79f2e 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -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 diff --git a/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml b/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml new file mode 100644 index 00000000..3b47c631 --- /dev/null +++ b/.github/workflows/eastmesh-build-observer-mqtt-bridge-firmwares.yml @@ -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/* diff --git a/AGENTS.md b/AGENTS.md index 56908eaf..e38df7ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: diff --git a/README.md b/README.md index ecbbed2f..8988e468 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ CoreScope () 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//...` (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) diff --git a/RELEASE.md b/RELEASE.md index cec3c0e1..2adab788 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -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-` - `repeater-bridge-espnow`: `v1.15.0-` - `observer-eastmesh-bridge-espnow`: `v1.15.0-eastmesh-v2026.5.1-` +- `observer-eastmesh-bridge-mqtt`: `v1.15.0-eastmesh-v2026.7.0-` - `observer-eastmesh`: `v1.15.0-eastmesh-v2026.5.1-` 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` diff --git a/eastmesh-build.sh b/eastmesh-build.sh index 48955a21..b27002c6 100755 --- a/eastmesh-build.sh +++ b/eastmesh-build.sh @@ -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 diff --git a/eastmesh-docs/boards.md b/eastmesh-docs/boards.md index 9fff8bc8..a6e8ec63 100644 --- a/eastmesh-docs/boards.md +++ b/eastmesh-docs/boards.md @@ -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)`. diff --git a/eastmesh-docs/custom-cli.md b/eastmesh-docs/custom-cli.md index 4d06fab4..0e9be13e 100644 --- a/eastmesh-docs/custom-cli.md +++ b/eastmesh-docs/custom-cli.md @@ -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 `: 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 `: sets the peer MQTT broker port and restarts the bridge. +- `get bridge.peer.username` / `set bridge.peer.username `: optional peer MQTT broker username. +- `get bridge.peer.password` / `set bridge.peer.password `: optional peer MQTT broker password (`get` returns `set` or `-`, not the stored value). - `get bridge.secret` / `set bridge.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 ``` diff --git a/eastmesh-docs/index.md b/eastmesh-docs/index.md index 70c40711..51c91753 100644 --- a/eastmesh-docs/index.md +++ b/eastmesh-docs/index.md @@ -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: diff --git a/eastmesh-docs/local-builds.md b/eastmesh-docs/local-builds.md index b988f1e2..1acd29af 100644 --- a/eastmesh-docs/local-builds.md +++ b/eastmesh-docs/local-builds.md @@ -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 diff --git a/eastmesh-docs/releases.md b/eastmesh-docs/releases.md index 2c33e1d5..174de088 100644 --- a/eastmesh-docs/releases.md +++ b/eastmesh-docs/releases.md @@ -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. diff --git a/eastmesh-docs/web-panel.md b/eastmesh-docs/web-panel.md index 399cf135..77f84f0f 100644 --- a/eastmesh-docs/web-panel.md +++ b/eastmesh-docs/web-panel.md @@ -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 diff --git a/release-notes.yml b/release-notes.yml index ed261b0a..258c34b3 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -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: [] diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 7afb705d..f2eedcf1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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 diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index a7673766..fe092e85 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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 { diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 362fca4f..49577f5f 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -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(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) { diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index bfa9880a..d9dcf276 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -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(); diff --git a/src/helpers/mqtt/MQTTPrefs.cpp b/src/helpers/mqtt/MQTTPrefs.cpp index c276d8db..93a279aa 100644 --- a/src/helpers/mqtt/MQTTPrefs.cpp +++ b/src/helpers/mqtt/MQTTPrefs.cpp @@ -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; } diff --git a/src/helpers/mqtt/MQTTPrefs.h b/src/helpers/mqtt/MQTTPrefs.h index 337ad5af..1112fd18 100644 --- a/src/helpers/mqtt/MQTTPrefs.h +++ b/src/helpers/mqtt/MQTTPrefs.h @@ -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 { diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 4c463283..2d051923 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -1033,7 +1033,24 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( -
Both bridge nodes must use the same peer broker, port, and bridge secret.
+
Both bridge nodes must use the same peer broker, port, credentials, and bridge secret.
+
+
+ +
+ + + +
+
+
+ +
+ + +
+
+
@@ -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"); diff --git a/variants/eastmesh_mqtt/platformio.ini b/variants/eastmesh_mqtt/platformio.ini index 694fd1eb..ac815ebe 100644 --- a/variants/eastmesh_mqtt/platformio.ini +++ b/variants/eastmesh_mqtt/platformio.ini @@ -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} + + + +[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} +