Merge pull request #73 from prabathbr/main
Adding observer-eastmesh-bridge-mqtt track to bridge over a MQTT broker🤖🤖
This commit is contained in:
@@ -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/*
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)`.
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">↻</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">↻</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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user