Skip to content

Commit 65fef12

Browse files
committed
Add companion radio debug bot with multi-board support
A separate firmware type layering a mesh debug bot on top of the stock BLE companion example without modifying upstream sources. Replies to !-prefixed commands (!ping, !rf, !path, !status, !stats, !neighbors, !uptime, !time, !ver) from direct messages or allow-listed channels, with rate limiting. Runtime config rides the companion protocol's custom vars ("bot", "bot.channels") settable via meshcore-cli or the phone app, persisted to flash. Envs for Xiao S3 WIO, Xiao nRF52, Heltec V3, Heltec V4, RAK4631, RAK3401 (WisMesh 1W Booster), T1000-E and Heltec T114 in variants/bot_envs/platformio.ini.
1 parent 5f3b7f2 commit 65fef12

11 files changed

Lines changed: 1051 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Companion Radio + Built-in Debug Bot
2+
3+
This is the standard MeshCore **BLE Companion** firmware with a built-in
4+
"debug bot". When the node receives a **direct text message** over the
5+
LoRa mesh whose text begins with `!`, it auto-replies over the mesh with useful
6+
debugging info — while still behaving as a normal companion (the incoming
7+
message is still delivered to the phone app over BLE).
8+
9+
Supported boards (PlatformIO env names):
10+
11+
| Board | Env |
12+
|-------|-----|
13+
| Seeed XIAO ESP32-S3 + Wio-SX1262 | `Xiao_S3_WIO_companion_radio_ble_bot` |
14+
| Seeed XIAO nRF52840 + Wio-SX1262 | `Xiao_nrf52_companion_radio_ble_bot` |
15+
| Heltec LoRa32 V3 | `Heltec_v3_companion_radio_ble_bot` |
16+
| Heltec LoRa32 V4 | `heltec_v4_companion_radio_ble_bot` |
17+
| RAK4631 (WisBlock) | `RAK_4631_companion_radio_ble_bot` |
18+
| RAK WisMesh 1W Booster (RAK3401 + RAK13302) | `RAK_3401_companion_radio_ble_bot` |
19+
| Seeed T1000-E Card Tracker | `t1000e_companion_radio_ble_bot` |
20+
| Heltec T114 | `Heltec_t114_companion_radio_ble_bot` |
21+
22+
Adding another board is ~12 lines of `platformio.ini` (see
23+
`variants/bot_envs/platformio.ini`) — all bot code is board-agnostic.
24+
25+
## Commands
26+
27+
Send any of these as a normal direct message to the bot node from another
28+
MeshCore client:
29+
30+
| Command | Reply |
31+
|----------------|-------|
32+
| `!ping` | Reports the requester's signal into us: `<name>: heard you at SNR …, RSSI …, N hop(s)` |
33+
| `!rf` | SNR + RSSI of the received request packet |
34+
| `!path` | Route the request arrived on, with hop hashes resolved to contact names: `flood, N hop(s): AA(RptAlpha) BB(?) …` or `direct route` |
35+
| `!status` | name, freq/SF/BW/CR, TX power, contact count, free heap |
36+
| `!stats` | Packet RX/TX counts (flood/direct) + TX/RX airtime — mesh-health snapshot |
37+
| `!neighbors` | Recently-heard nodes + hop counts (alias `!seen`) |
38+
| `!uptime` | Time since boot (`2d 4h 13m 7s`) |
39+
| `!time` | Node clock (UTC epoch) if the RTC is set |
40+
| `!ver` | Firmware version + build date |
41+
42+
`!path` and `!rf` describe the **request packet as this node received it**,
43+
which is exactly what you want when probing a path through the mesh. `!path`
44+
resolves each hop's hash against this node's contact list; hops it doesn't
45+
have a contact for show as `(?)`.
46+
47+
Unrecognised `!` commands are silently ignored (no reply), so typos and other
48+
bots' command prefixes don't generate mesh traffic.
49+
50+
## Where it listens
51+
52+
- **Direct messages:** the bot replies to a `!`-command sent to it as a direct
53+
message from any contact (subject to rate limiting).
54+
- **Group channels:** the bot also replies to `!`-commands posted in an
55+
**allow-listed** channel, posting the reply back to the **same channel**.
56+
Channels not on the list (e.g. `Public`) are ignored, so the bot can't spam
57+
busy channels. The allow-list is **empty by default** (DM-only) and is
58+
configured at runtime — see below. Replies never start with `!`, so they
59+
cannot re-trigger the bot.
60+
61+
## Runtime configuration
62+
63+
The bot is configured **locally** over the companion protocol's custom-vars
64+
frames — from meshcore-cli or any client that exposes custom variables — and
65+
NOT via mesh messages, so only the paired/local user can change it. Settings
66+
persist to flash (`/bot_prefs`) and survive reboots.
67+
68+
| Var | Values | Meaning |
69+
|-----|--------|---------|
70+
| `bot` | `1` / `0` (also `on`/`off`) | Enable/disable all bot replies |
71+
| `bot.channels` | `#name[,#name...]` or `none` | Channel allow-list (default: `none`, DM-only) |
72+
73+
With [meshcore-cli](https://github.com/meshcore-dev/meshcore-cli) (unknown
74+
`set`/`get` names fall through to custom vars):
75+
76+
```sh
77+
meshcore-cli -d "XiaoS3 Bot" set bot 0 # mute the bot
78+
meshcore-cli -d "XiaoS3 Bot" set bot 1 # re-enable it
79+
meshcore-cli -d "XiaoS3 Bot" set bot.channels "#test,#bots" # allow channels
80+
meshcore-cli -d "XiaoS3 Bot" set bot.channels none # DM-only again
81+
meshcore-cli -d "XiaoS3 Bot" get custom # show all vars
82+
```
83+
84+
Channel names must match the stored channel name exactly, including the
85+
leading `#`, and the node must have those channels added
86+
(key = `SHA256(name)[:16]`).
87+
88+
## How it works
89+
90+
This is a **separate firmware type** that layers on top of the stock companion
91+
example without modifying it. The upstream tree (`examples/companion_radio/`,
92+
`variants/<board>/`) is untouched, so pulling from upstream never conflicts
93+
with the bot. Everything lives in `examples/companion_radio_bot/` plus one
94+
env file holding all the board envs (`variants/bot_envs/platformio.ini`):
95+
96+
- `BotCommands.{h,cpp}` — all the bot logic (commands, rate limiting).
97+
- `BotConfig.{h,cpp}` — the persisted runtime settings (`/bot_prefs` on the
98+
node's filesystem): enabled flag + channel allow-list.
99+
- `BotSensorManager.h` / `BotTarget.cpp` — expose those settings as the
100+
`bot` / `bot.channels` custom vars. `BotTarget.cpp` compiles the board's
101+
stock `target.cpp` with the global `sensors` object swapped for a wrapper
102+
subclass that adds the two settings to the custom-vars get/set hooks.
103+
- `BotMyMesh.cpp` — compiles the stock `MyMesh.cpp` with uses of `sensors`
104+
routed through an opaque reference accessor. Necessary because the
105+
compiler devirtualizes calls on a global object of known type, which
106+
would silently bypass the wrapper's overrides (it did, until this shim).
107+
- `BotMesh.h` — a `MyMesh` subclass; the only coupling point. It overrides the
108+
two virtual receive callbacks (`onMessageRecv`, `onChannelMessageRecv`) to
109+
hand messages to the bot, then delegates to the stock behaviour, and
110+
shadows `begin()` to start the bot's uptime clock.
111+
- `main.cpp` — a shim that `#include`s the stock companion `main.cpp` verbatim
112+
(no copy to keep in sync), with `MyMesh` renamed to `BotMesh` so the global
113+
mesh object is our subclass. Because `MyMesh.h` declares
114+
`extern MyMesh the_mesh;`, the bot instance is named `the_bot_mesh` and the
115+
env links it back under the original symbol with
116+
`-Wl,--defsym,the_mesh=the_bot_mesh` (other translation units like
117+
`UITask.cpp` resolve against it normally).
118+
119+
The bot uses only `MyMesh`'s **public** API (`sendMessage`, `getNodePrefs`,
120+
`getRecentlyHeard`, `advert`, `getRTCClock`), so it stays decoupled. Stock
121+
companion builds are completely unaffected — they never compile these files.
122+
123+
### Abuse protection (rate limiting)
124+
125+
A node receiving a flood of `!` messages must not be tricked into hammering the
126+
airwaves. `BotCommands` enforces three limits (all overridable via `-D`):
127+
128+
| Define | Default | Meaning |
129+
|--------|---------|---------|
130+
| `BOT_MIN_REPLY_INTERVAL_MS` | `1200` | Min gap between any two bot replies |
131+
| `BOT_SENDER_COOLDOWN_MS` | `5000` | Per-sender cooldown |
132+
| `BOT_MAX_REPLIES_PER_MIN` | `20` | Hard cap on replies per rolling 60 s |
133+
134+
Dropped requests are silently ignored (no reply), but are still forwarded to the
135+
phone app.
136+
137+
### Other build options
138+
139+
| Define | Default | Meaning |
140+
|--------|---------|---------|
141+
| `BOT_CMD_PREFIX` | `'!'` | Command prefix character |
142+
| `BOT_FORWARD_TO_APP` | `1` | `0` = hide bot commands from the phone app |
143+
144+
## Build & flash
145+
146+
Use the env for your board from the table at the top, e.g.:
147+
148+
```sh
149+
# build
150+
pio run -e Xiao_S3_WIO_companion_radio_ble_bot
151+
152+
# build + flash (board on USB)
153+
pio run -e Xiao_S3_WIO_companion_radio_ble_bot -t upload
154+
155+
# serial monitor
156+
pio device monitor -b 115200
157+
```
158+
159+
nRF52 boards (RAK4631, T1000-E, T114) also produce a `firmware.uf2` in
160+
`.pio/build/<env>/` for drag-and-drop flashing via the UF2 bootloader.
161+
162+
Default BLE pairing PIN is `123456` (`BLE_PIN_CODE`). Pair with any MeshCore
163+
client (phone app, web client) as you would the normal companion firmware.

0 commit comments

Comments
 (0)