|
| 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