From 74dcf979a98fc69b432081ed899e0b4ce33516b3 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 10 Jul 2026 15:34:07 -0600 Subject: [PATCH] Update MeshCore base to v1.16.0; add sig/air/coin; ship 139 boards - Bump vendor/MeshCore pin 910b1bee -> bbb58cce (upstream v1.16.0, 272 commits) and rebase the 14-patch bot queue onto it; one trivial conflict in the MyMesh constructor. - New patch 15: sig/air/coin commands plus registry-derived help and cmd listings ('cmd diag' for the diagnostic set). BOT_PREFS_VERSION bumped to 7. Host tests cover the new commands and assert every discoverable registry entry appears in a listing. - Re-include six boards fixed upstream (Pico W, RAK 11310, Waveshare RP2040 LoRa, Xiao RP2040, Nibble USB+BLE) after local test builds; remaining exclusions are BLE flash-size only. - Fix release env enumeration to strip CR line endings so envs in CRLF variant files (Minewsemi ME25LS01, Wio WM1110, Nibble) are counted and built; verified all three build locally. - Refresh README/CHANGELOG/RELEASE/CONTRIBUTING for the new base and command set. Verified: host tests, safety checks, 4 representative builds, and 9 additional env builds all pass; the 15-patch queue applies cleanly to pristine bbb58cce. --- CHANGELOG.md | 42 ++- CONTRIBUTING.md | 29 +- README.md | 42 +-- RELEASE.md | 17 +- ...on-radio-firmware-bot-command-parity.patch | 58 ++-- ...n-firmware-bot-response-coordination.patch | 44 +-- ...03-Allow-prefixless-firmware-bot-DMs.patch | 2 +- ...-bot-commands-with-upstream-behavior.patch | 40 +-- ...t-coordination-utility-commands-and-.patch | 54 ++-- ...-speed-up-initial-advert-default-con.patch | 22 +- ...nses-with-request-token-for-inter-bo.patch | 16 +- ...t-suffix-and-auto-learn-known-bot-pa.patch | 24 +- ...p-drop-ack-prefix-from-test-response.patch | 2 +- ...n-coordinator-delays-a-tiny-bit-more.patch | 2 +- ...tartup-initial-flood-at-5s-instead-o.patch | 2 +- ...yte-in-test-path-drop-channel-name-f.patch | 2 +- ...quadratically-wider-gaps-at-higher-h.patch | 2 +- ...iagnostics-and-duplicate-suppression.patch | 26 +- ...-commands-derive-help-listings-from-.patch | 264 ++++++++++++++++++ scripts/list-release-companion-envs.sh | 14 +- tests/firmware_bot/test_firmware_bot.cpp | 95 ++++++- vendor/MeshCore | 2 +- 22 files changed, 585 insertions(+), 216 deletions(-) create mode 100644 patches/meshcore/0015-Add-sig-air-coin-commands-derive-help-listings-from-.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d85398..4d06cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,29 +6,53 @@ follows the `cmesh-bot-vX.Y.Z` scheme described in [RELEASE.md](RELEASE.md). ## [Unreleased] +## [cmesh-bot-v0.1.0] - 2026-07-10 + +First tagged release. + ### Added +- Signal report command `sig` (aliases `snr`, `rssi`, `signal`): shows the + SNR the bot heard your request at, plus its last RSSI and noise floor. + Made for range testing. +- Airtime command `air` (alias `airtime`): TX/RX airtime and flood/direct + packet counters. +- Coin flip command `coin` (aliases `flip`, `coinflip`). +- `cmd diag` lists the diagnostic command set separately, since the full + command list no longer fits one group-channel message. - Hop-aware response coordination: bot delays its reply proportional to - the sender's hop count so the closest bot wins; far bots suppress. + the sender's hop count (2 s per hop plus a quadratic term, wide caps) + so the closest bot wins; far bots suppress via response fingerprints + and request tokens. - Utility commands: `time`, `lora`, `id`, `neighbors`. - Neighbor tracking: bot remembers nodes heard directly within the last hour for the `neighbors` command. - `received_at_timestamp` field on bot messages; personalized ack target in command context. -- Comprehensive repository scaffolding: README, CONTRIBUTING, LICENSE, - CHANGELOG, RELEASE doc, issue/PR templates, release workflow building - all 133 companion USB+BLE environments. +- Full repository scaffolding: README, CONTRIBUTING, LICENSE, CHANGELOG, + RELEASE doc, issue/PR templates, release workflow building every + non-excluded companion USB+BLE environment. ### Changed -- `BOT_HOP_STEP_MILLIS_DEFAULT` lowered from 5000 to 1500 ms per hop. -- Added `BOT_HOP_BIAS_MAX_MILLIS` cap of 8000 ms on the total hop bias. -- `BOT_RESPONSE_PENDING_TTL_MILLIS` raised from 15000 to 45000 ms. -- `BOT_PREFS_VERSION` bumped 2 → 3 to force re-default of in-the-wild - bots that saved the broken 5000 ms hop-step value. +- MeshCore base updated from `910b1bee` to `bbb58cce` (upstream v1.16.0, + 272 commits: 5-byte ACKs, flood scope keys, NRF52 power saving, RP2040 + build fixes, contacts sync fix, and more). +- `help` and `cmd` listings are now derived from the command registry + instead of hand-maintained lists that had drifted (`magic8` was missing + from both). +- `BOT_PREFS_VERSION` bumped to 7 so deployed bots re-default their + command mask and pick up the commands added in this release. +- Re-included six boards that build again on the new base after passing + local test builds: Pico W, RAK 11310, Waveshare RP2040 LoRa, Xiao + RP2040, and Nibble Screen Connect (USB and BLE). The remaining + exclusions are BLE variants that don't fit flash. ### Fixed +- Release env enumeration now strips CR line endings, so companion + environments defined in variant files with CRLF (Minewsemi ME25LS01, + Wio WM1110, Nibble) are no longer silently missing from releases. - Multi-hop senders on `#bot` no longer get silently ignored. Previously 3+ hop responses scheduled past the 15 s TTL and were dropped by the coordinator's `poll()` as `READY_EXPIRED`. Verified on a Heltec V3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a8f0eb..d3fbd1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,19 +23,18 @@ The submodule is a real git repo. Iterate inside it like normal: 2. **Apply patches** with `bash scripts/apply-patches.sh`. This commits the patch queue on top of `origin/main` in the submodule, leaving you with a clean tree pinned to a known state. -3. **Make changes inside `vendor/MeshCore/`** — edit files, run - `pio run -e ` to compile, commit your changes in the submodule - with descriptive messages. +3. **Edit inside `vendor/MeshCore/`.** Compile with `pio run -e ` + and commit your changes in the submodule with descriptive messages. 4. **Re-export the patch queue** with `bash scripts/export-patches.sh origin/main`. This deletes `patches/meshcore/*.patch` and regenerates it from the submodule commits since `origin/main`. Each patch corresponds to one submodule commit, so keep commits small and focused. -5. **Run host tests** (`python3 tests/firmware_bot/run_tests.py`) — fast, +5. **Run host tests** (`python3 tests/firmware_bot/run_tests.py`). Fast, no hardware needed. -6. **Run safety checks** (`bash scripts/check-bot-safety.sh`) — verifies - the firmware doesn't expose private keys, network bridges, or other - debug-only features in release builds. +6. **Run safety checks** (`bash scripts/check-bot-safety.sh`), which + verify the firmware doesn't expose private keys, network bridges, or + other debug-only features in release builds. 7. **(Optional) Build representative envs** with `bash scripts/build-representative.sh --baseline` to confirm the binaries still link and to inspect size impact in `out/size/summary.json`. @@ -44,14 +43,13 @@ The submodule is a real git repo. Iterate inside it like normal: ## What goes where -- **Firmware/library code that MUST live inside MeshCore** for PlatformIO - builds — develop inside `vendor/MeshCore/`, export as a patch. -- **Test code (host-side C++ tests)** — `tests/firmware_bot/` at the - wrapper level. They `#include` MeshCore headers via the script's `-I` - flag. -- **Tooling, build scripts, CI** — `scripts/` and `.github/` at the +- Firmware and library code that must live inside MeshCore for PlatformIO + builds is developed inside `vendor/MeshCore/` and exported as a patch. +- Host-side C++ tests live in `tests/firmware_bot/` at the wrapper level. + They `#include` MeshCore headers via the script's `-I` flag. +- Tooling, build scripts, and CI live in `scripts/` and `.github/` at the wrapper level. -- **Colorado-specific docs, fixtures, board notes** — `colorado/`. +- Colorado-specific docs, fixtures, and board notes live in `colorado/`. ## Patch hygiene @@ -76,7 +74,8 @@ PRs run `.github/workflows/firmware-build.yml`, which: 3. Uploads firmware + size reports as workflow artifacts. Releases run `.github/workflows/release.yml` on `cmesh-bot-v*` tags and -build all 133 companion USB+BLE environments — see [RELEASE.md](RELEASE.md). +build every non-excluded companion USB+BLE environment (139 at the current +pin); see [RELEASE.md](RELEASE.md). ## Pull request expectations diff --git a/README.md b/README.md index 98d5a3f..510e503 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ Companion-radio firmware for the Colorado Mesh community, built on top of [MeshCore](https://github.com/meshcore-dev/MeshCore). It adds an in-firmware "firmware bot" that responds to chat commands like `ping`, `trace`, `path`, -`status`, `neighbors`, and `magic8`, with multi-bot response coordination so -nearby Colorado bots don't all reply at once. +`status`, `neighbors`, `sig`, and `magic8`, with multi-bot response +coordination so nearby Colorado bots don't all reply at once. -The bot runs entirely on-device — no internet, no companion-app bridge, no +The bot runs entirely on-device: no internet, no companion-app bridge, no cloud. Useful as a low-friction probe / utility node on the Colorado mesh. ## What's different from upstream MeshCore @@ -19,8 +19,9 @@ The release-built firmware applies a patch queue (`patches/meshcore/`) on top of a pinned MeshCore commit. The patches add: - A bot command registry (`help`, `cmd`, `ping`, `test`, `hello`, `about`, - `roll`, `dice`, `status`, `channels`, `version`, `stats`, `magic8`, `path`, - `trace`, `tracer`, `prefix`, `time`, `lora`, `id`, `neighbors`) + `roll`, `dice`, `coin`, `status`, `channels`, `version`, `stats`, `magic8`, + `path`, `trace`, `tracer`, `prefix`, `time`, `lora`, `id`, `neighbors`, + `sig`, `air`) - Channel-aware response policy (`#bot`, `#testing` for normal commands; `#emergency` for emergency forwards to `Public`; all other channels ignored) - Multi-bot response coordinator with hop-aware delay, bounded TTL, and @@ -37,10 +38,12 @@ re-exported from the submodule. ## Supported boards The release workflow builds **every** `*_companion_radio_usb` and -`*_companion_radio_ble` PlatformIO environment exposed by upstream MeshCore -— 133 boards across ESP32, NRF52, RP2040, and STM32 platforms (Heltec V3, -RAK 4631, LilyGo T-Echo, T-Beam, T-Deck, Xiao S3, Nano G2, ThinkNode, -Meshtiny, Pico W, and many more). See the +`*_companion_radio_ble` PlatformIO environment exposed by upstream MeshCore, +minus a short exclusion list of BLE variants that don't fit flash (see +`scripts/list-release-companion-envs.sh`). That is 139 boards at the current +pin, across ESP32, NRF52, RP2040, and STM32 platforms (Heltec V3, RAK 4631, +LilyGo T-Echo, T-Beam, T-Deck, Xiao S3, Nano G2, ThinkNode, Meshtiny, Pico W, +and many more). See the [latest release assets](https://github.com/Colorado-Mesh/meshcore-bot-firmware/releases/latest) for the full list. @@ -84,19 +87,22 @@ use `adafruit-nrfutil dfu serial -pkg .zip -p `. Once flashed, the bot joins the channels configured in its prefs. By default those are `#bot`, `#testing`, `#emergency`, and `Public`. Send commands on -`#bot` (or DM the bot directly) without any prefix — for example: +`#bot` (or DM the bot directly) without any prefix, for example: | Command | Response | |---|---| | `ping` | `Pong` | | `hello` | `Hello @[], from ` | -| `path` | Compact route summary back to you, e.g. `Path 3h@2B SNR -6.25 \| 2751 -> ea4d -> 430d` | +| `path` | Compact route summary back to you, e.g. `Path 3 hops, 2-byte hashes, SNR -6.25 \| 2751 -> ea4d -> 430d` | | `trace` | Active trace request along your reverse path | +| `sig` | How the bot heard you: request SNR, plus local RSSI and noise floor | | `status` | Bot uptime, battery, storage, send counters | | `neighbors` | Nodes heard directly within the last hour | +| `air` | TX/RX airtime and flood/direct packet counters | | `version` | Firmware version + build date | | `magic8 ` | Classic 8-ball answer | -| `help` | List all commands | +| `roll`, `dice`, `coin` | Random numbers, D&D dice, coin flips | +| `help` | List chat commands (`cmd diag` lists the diagnostic set) | In any other channel the bot stays silent (except `#emergency`, which it re-forwards to `Public`). @@ -107,14 +113,14 @@ wins the race and others suppress. This avoids spam on the channel. ## Layout -- `vendor/MeshCore/` — pinned upstream MeshCore submodule. -- `patches/meshcore/` — ordered patch queue applied to the submodule. -- `colorado/` — Colorado Mesh overlay files, fixtures, and notes. -- `scripts/` — wrapper scripts for patch, build, verify, and size-report +- `vendor/MeshCore/`: pinned upstream MeshCore submodule. +- `patches/meshcore/`: ordered patch queue applied to the submodule. +- `colorado/`: Colorado Mesh overlay files, fixtures, and notes. +- `scripts/`: wrapper scripts for patch, build, verify, and size-report workflows. -- `tests/firmware_bot/` — host-side C++ unit tests for the bot code +- `tests/firmware_bot/`: host-side C++ unit tests for the bot code (compile and run on your machine, no hardware required). -- `.github/workflows/` — PR CI (`firmware-build.yml`) and tag-driven release +- `.github/workflows/`: PR CI (`firmware-build.yml`) and tag-driven release (`release.yml`). ## Local development diff --git a/RELEASE.md b/RELEASE.md index a16d919..230d60b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,7 +4,8 @@ Releases are produced by pushing a `cmesh-bot-vX.Y.Z` tag. The `.github/workflows/release.yml` workflow then: 1. Builds **every** companion USB+BLE environment exposed by the pinned - MeshCore submodule (133 boards as of this writing). + MeshCore submodule, minus the exclusion list in + `scripts/list-release-companion-envs.sh` (139 boards at the current pin). 2. Splits the build across 8 parallel matrix shards for wall-time. 3. Embeds the tag version (`vX.Y.Z`) into the firmware via `FIRMWARE_VERSION`. @@ -44,9 +45,9 @@ Review it, paste in the CHANGELOG entry as the description, and publish. ## Tag scheme -- `cmesh-bot-vX.Y.Z` — production release. -- `cmesh-bot-vX.Y.Z-rc.N` — release candidate (still publishes as draft). -- `cmesh-bot-vX.Y.Z-alpha.N` / `-beta.N` — pre-release, mark as +- `cmesh-bot-vX.Y.Z`: production release. +- `cmesh-bot-vX.Y.Z-rc.N`: release candidate (still publishes as draft). +- `cmesh-bot-vX.Y.Z-alpha.N` / `-beta.N`: pre-release, mark as pre-release when publishing. The leading `cmesh-bot-` namespace leaves room for future variants @@ -57,14 +58,14 @@ The leading `cmesh-bot-` namespace leaves room for future variants Semantic-ish: bump major for breaking pref/protocol changes, minor for new commands or boards, patch for fixes. Bump `BOT_PREFS_VERSION` in `vendor/MeshCore/examples/companion_radio/BotTypes.h` whenever an -existing pref field's meaning or default changes — that forces deployed +existing pref field's meaning or default changes. That forces deployed bots to reset prefs on first boot. ## Hotfix / unpublish If a published release turns out to be broken: -1. Mark it as a pre-release (don't delete — flashed devices may still +1. Mark it as a pre-release (don't delete it; flashed devices may still reference the URL). 2. Tag and publish `cmesh-bot-vX.Y.(Z+1)` immediately. 3. Edit the broken release's notes to point at the fix. @@ -77,5 +78,5 @@ git push --delete origin cmesh-bot-vX.Y.Z # fix, re-tag, re-push ``` -Avoid deleting tags after the workflow has published artifacts — users -may have already downloaded them. +Avoid deleting tags after the workflow has published artifacts, since +users may have already downloaded them. diff --git a/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch b/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch index 0cda401..c248e70 100644 --- a/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch +++ b/patches/meshcore/0001-Add-companion-radio-firmware-bot-command-parity.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 11:44:21 -0600 -Subject: [PATCH 01/14] Add companion radio firmware bot command parity +Subject: [PATCH 01/15] Add companion radio firmware bot command parity --- .../companion_radio/BotCommandRegistry.cpp | 121 ++ @@ -1428,7 +1428,7 @@ index 00000000..5c7430a9 +static_assert(sizeof(BotPrefs) <= 320, "BotPrefs RAM budget exceeded"); +static_assert(sizeof(BotStats) <= 64, "BotStats RAM budget exceeded"); diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp -index c7988bb3..9a134389 100644 +index bf2f36c3..e6f52e09 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1,6 +1,10 @@ @@ -1482,7 +1482,7 @@ index c7988bb3..9a134389 100644 File file = openRead(_fs, filename); if (file) { diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h -index 58b4d5d2..187cd771 100644 +index af5ee7af..702316cf 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -5,6 +5,13 @@ @@ -1508,7 +1508,7 @@ index 58b4d5d2..187cd771 100644 + bool saveBotPrefs(const BotPrefs& prefs); +#endif void loadContacts(DataStoreHost* host); - void saveContacts(DataStoreHost* host); + void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); void loadChannels(DataStoreHost* host); diff --git a/examples/companion_radio/EmergencyForwarder.cpp b/examples/companion_radio/EmergencyForwarder.cpp new file mode 100644 @@ -2072,7 +2072,7 @@ index 00000000..5e93dad0 + +} diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index e8c1914b..44fcd1ca 100644 +index 6fbb0f74..19963f38 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2,6 +2,21 @@ @@ -2097,7 +2097,7 @@ index e8c1914b..44fcd1ca 100644 #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 -@@ -105,8 +120,244 @@ +@@ -106,8 +121,244 @@ #define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 #define LAZY_CONTACTS_WRITE_DELAY 5000 @@ -2342,7 +2342,7 @@ index e8c1914b..44fcd1ca 100644 // these are _pushed_ to client app at any time #define PUSH_CODE_ADVERT 0x80 #define PUSH_CODE_PATH_UPDATED 0x81 -@@ -514,6 +765,9 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t +@@ -523,6 +774,9 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t const char *text) { markConnectionActive(from); // in case this is from a server, and we have a connection queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); @@ -2352,7 +2352,7 @@ index e8c1914b..44fcd1ca 100644 } void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, -@@ -528,8 +782,840 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin +@@ -537,8 +791,840 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin // from.sync_since change needs to be persisted dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); @@ -2399,8 +2399,8 @@ index e8c1914b..44fcd1ca 100644 + (unsigned)bot_prefs.normal_jitter_ms); + Serial.printf(" > advert local=%lu flood=%lu\n", (unsigned long)bot_prefs.local_advert_interval_ms, + (unsigned long)bot_prefs.flood_advert_interval_ms); -+} -+ + } + +bool MyMesh::handleBotCLI(const char *args) { + if (!args) return false; + botSkipSpaces(&args); @@ -2705,8 +2705,8 @@ index e8c1914b..44fcd1ca 100644 + pending->text[pending->text_len] = 0; + pending->active = true; + return true; - } - ++} ++ +bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) { + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails channel; @@ -3193,7 +3193,7 @@ index e8c1914b..44fcd1ca 100644 void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) { int i = 0; -@@ -566,15 +1652,17 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe +@@ -575,15 +1661,17 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (_ui) _ui->notify(UIEventType::channelMessage); #endif } @@ -3213,7 +3213,7 @@ index e8c1914b..44fcd1ca 100644 } void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type, -@@ -829,6 +1917,23 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, +@@ -838,6 +1926,23 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } @@ -3237,10 +3237,10 @@ index e8c1914b..44fcd1ca 100644 } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { -@@ -856,6 +1961,19 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe - dirty_contacts_expiry = 0; +@@ -866,6 +1971,19 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe memset(advert_paths, 0, sizeof(advert_paths)); memset(send_scope.key, 0, sizeof(send_scope.key)); + send_unscoped = false; +#if CMESH_BOT_ENABLED + BotPrefsCodec::defaults(bot_prefs); + memset(&bot_stats, 0, sizeof(bot_stats)); @@ -3257,7 +3257,7 @@ index e8c1914b..44fcd1ca 100644 // defaults memset(&_prefs, 0, sizeof(_prefs)); -@@ -868,6 +1986,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe +@@ -878,6 +1996,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default @@ -3267,7 +3267,7 @@ index e8c1914b..44fcd1ca 100644 //_prefs.rx_delay_base = 10.0f; enable once new algo fixed #if defined(USE_SX1262) || defined(USE_SX1268) #ifdef SX126X_RX_BOOSTED_GAIN -@@ -956,6 +2077,11 @@ void MyMesh::begin(bool has_display) { +@@ -966,6 +2087,11 @@ void MyMesh::begin(bool has_display) { radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); @@ -3279,7 +3279,7 @@ index e8c1914b..44fcd1ca 100644 } const char *MyMesh::getNodeName() { -@@ -1975,7 +3101,13 @@ void MyMesh::checkCLIRescueCmd() { +@@ -2023,7 +3149,13 @@ void MyMesh::checkCLIRescueCmd() { if (len > 0 && cli_command[len - 1] == '\r') { // received complete line cli_command[len - 1] = 0; // replace newline with C string null terminator @@ -3294,7 +3294,7 @@ index e8c1914b..44fcd1ca 100644 const char* config = &cli_command[4]; if (memcmp(config, "pin ", 4) == 0) { _prefs.ble_pin = atoi(&config[4]); -@@ -2172,6 +3304,10 @@ void MyMesh::loop() { +@@ -2228,6 +3360,10 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } @@ -3306,7 +3306,7 @@ index e8c1914b..44fcd1ca 100644 if (_ui) _ui->setHasConnection(_serial->isConnected()); #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h -index aeff591c..8d848fef 100644 +index 43d3950b..c1159a31 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -4,6 +4,15 @@ @@ -3323,9 +3323,9 @@ index aeff591c..8d848fef 100644 +#endif + /*------------ Frame Protocol --------------*/ - #define FIRMWARE_VER_CODE 11 + #define FIRMWARE_VER_CODE 13 -@@ -192,6 +201,43 @@ private: +@@ -195,6 +204,43 @@ private: return _store->putBlobByKey(key, key_len, src_buf, len); } @@ -3369,7 +3369,7 @@ index aeff591c..8d848fef 100644 void checkCLIRescueCmd(); void checkSerialInterface(); bool isValidClientRepeatFreq(uint32_t f) const; -@@ -223,6 +269,53 @@ private: +@@ -227,6 +273,53 @@ private: TransportKey send_scope; @@ -3679,7 +3679,7 @@ index 00000000..7717bee7 + +} diff --git a/platformio.ini b/platformio.ini -index 864e5e1f..dfed7dbd 100644 +index e16f7b83..e96147b9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -54,6 +54,14 @@ build_src_filter = @@ -3698,7 +3698,7 @@ index 864e5e1f..dfed7dbd 100644 extends = arduino_base platform = platformio/espressif32@6.11.0 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini -index 803ee683..ca9cf00d 100644 +index a70a93a5..8164bc78 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -144,6 +144,7 @@ build_flags = @@ -3718,10 +3718,10 @@ index 803ee683..ca9cf00d 100644 -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini -index ea7e49c3..d6610098 100644 +index 2bbba314..a1226fe3 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini -@@ -122,6 +122,7 @@ build_flags = +@@ -127,6 +127,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 @@ -3729,7 +3729,7 @@ index ea7e49c3..d6610098 100644 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} -@@ -143,6 +144,7 @@ build_flags = +@@ -148,6 +149,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 diff --git a/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch index 898564b..56ae820 100644 --- a/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch +++ b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 19:58:42 -0600 -Subject: [PATCH 02/14] Harden firmware bot response coordination +Subject: [PATCH 02/15] Harden firmware bot response coordination --- examples/companion_radio/BotCommands.cpp | 2 +- @@ -153,10 +153,10 @@ index e248adc1..38d4416d 100644 BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len); BotCommandId commandIdForName(const char* name, size_t len); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 44fcd1ca..2ee5139b 100644 +index 19963f38..cb557f5d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -139,6 +139,18 @@ static void botCopyString(char *dest, size_t dest_len, const char *src) { +@@ -140,6 +140,18 @@ static void botCopyString(char *dest, size_t dest_len, const char *src) { dest[len] = 0; } @@ -175,7 +175,7 @@ index 44fcd1ca..2ee5139b 100644 static bool botParseU32(const char *text, uint32_t *value, const char **end_out) { if (!text || !value || !isdigit((unsigned char)text[0])) return false; uint32_t parsed = 0; -@@ -598,6 +610,10 @@ void MyMesh::onContactsFull() { +@@ -599,6 +611,10 @@ void MyMesh::onContactsFull() { } void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { @@ -186,7 +186,7 @@ index 44fcd1ca..2ee5139b 100644 if (_serial->isConnected()) { if (is_new) { writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact); -@@ -1040,11 +1056,9 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n +@@ -1049,11 +1065,9 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n message.path = packet->path; } @@ -201,7 +201,7 @@ index 44fcd1ca..2ee5139b 100644 recordBotObservation(message, NULL, channel_idx); } -@@ -1100,6 +1114,21 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com +@@ -1109,6 +1123,21 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com } } @@ -223,7 +223,7 @@ index 44fcd1ca..2ee5139b 100644 bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const char *text, size_t text_len, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint) { -@@ -1124,11 +1153,9 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di +@@ -1133,11 +1162,9 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di pending->channel_idx = channel_idx; pending->request_fingerprint = request_fingerprint; pending->response_fingerprint = response_fingerprint; @@ -238,7 +238,7 @@ index 44fcd1ca..2ee5139b 100644 pending->active = true; return true; } -@@ -1204,7 +1231,14 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1213,7 +1240,14 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); char response[BOT_MAX_RESPONSE_LEN + 1]; size_t response_len = botFormatTraceSent(response, sizeof(response)); @@ -254,7 +254,7 @@ index 44fcd1ca..2ee5139b 100644 BotFingerprint fingerprint; uint32_t due_at_millis = 0; uint32_t now = _ms->getMillis(); -@@ -1264,6 +1298,10 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc +@@ -1273,6 +1307,10 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key)); } pending->channel_idx = channel_idx; @@ -265,7 +265,7 @@ index 44fcd1ca..2ee5139b 100644 pending->request_fingerprint = request_fingerprint; pending->response_fingerprint = response_fingerprint; pending->tag = tag; -@@ -1275,6 +1313,16 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc +@@ -1284,6 +1322,16 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc return true; } @@ -282,7 +282,7 @@ index 44fcd1ca..2ee5139b 100644 bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, BotFingerprint response_fingerprint, uint32_t now_millis) { bool success = false; -@@ -1325,7 +1373,13 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) +@@ -1334,7 +1382,13 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) char response[BOT_MAX_RESPONSE_LEN + 1]; size_t response_len = botFormatTraceSent(response, sizeof(response)); @@ -297,7 +297,7 @@ index 44fcd1ca..2ee5139b 100644 } void MyMesh::expirePendingBotTraces(uint32_t now_millis) { -@@ -1334,10 +1388,13 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { +@@ -1343,10 +1397,13 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { if (!pending->active || !pending->sent || (int32_t)(now_millis - pending->expires_at_millis) < 0) continue; const char *response = "Trace timed out"; @@ -315,7 +315,7 @@ index 44fcd1ca..2ee5139b 100644 pending->active = false; } } -@@ -1384,6 +1441,18 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati +@@ -1393,6 +1450,18 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati return false; } @@ -334,7 +334,7 @@ index 44fcd1ca..2ee5139b 100644 void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) { bot_stats.observed_messages++; BotPolicyDecision decision = BotPolicy::decide(message.channel_kind); -@@ -1403,6 +1472,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1412,6 +1481,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * } if (observeKnownBotResponse(message, direct_recipient != NULL)) return; @@ -342,7 +342,7 @@ index 44fcd1ca..2ee5139b 100644 sendQueuedBotResponses(); BotCommand command; -@@ -1439,8 +1509,15 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1448,8 +1518,15 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * return; } @@ -359,7 +359,7 @@ index 44fcd1ca..2ee5139b 100644 BotFingerprint fingerprint; uint32_t due_at_millis = 0; uint32_t bot_identity_seed; -@@ -1462,7 +1539,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1471,7 +1548,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * bot_stats.pending_responses++; FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(), BOT_COMMAND_COOLDOWN_MILLIS); @@ -368,7 +368,7 @@ index 44fcd1ca..2ee5139b 100644 ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint); bot_stats.send_failures++; } -@@ -1572,10 +1649,12 @@ void MyMesh::sendQueuedEmergencyForwards() { +@@ -1581,10 +1658,12 @@ void MyMesh::sendQueuedEmergencyForwards() { bool MyMesh::sendBotSelfAdvert(bool flood) { mesh::Packet* pkt; @@ -383,7 +383,7 @@ index 44fcd1ca..2ee5139b 100644 } if (!pkt) return false; -@@ -1927,9 +2006,13 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, +@@ -1936,9 +2015,13 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, char response[BOT_MAX_RESPONSE_LEN + 1]; size_t response_len = botFormatTraceResult(response, sizeof(response), tag, flags, path_hashes, path_len, (int8_t)(packet->getSNR() * 4)); @@ -401,10 +401,10 @@ index 44fcd1ca..2ee5139b 100644 break; } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h -index 8d848fef..67bdc010 100644 +index c1159a31..1197caff 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h -@@ -214,7 +214,9 @@ private: +@@ -217,7 +217,9 @@ private: uint32_t sender_timestamp, const mesh::Packet *packet); void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx); bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender); @@ -414,7 +414,7 @@ index 8d848fef..67bdc010 100644 bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const char *text, size_t text_len, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint); -@@ -226,6 +228,7 @@ private: +@@ -229,6 +231,7 @@ private: BotFingerprint response_fingerprint, uint32_t tag, uint32_t auth_code); bool sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, BotFingerprint response_fingerprint, uint32_t now_millis); @@ -422,7 +422,7 @@ index 8d848fef..67bdc010 100644 bool sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis); void expirePendingBotTraces(uint32_t now_millis); bool enqueueEmergencyForward(const BotMessage &message); -@@ -291,8 +294,12 @@ private: +@@ -295,8 +298,12 @@ private: bool active; bool direct; bool sent; diff --git a/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch b/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch index a1936fa..a96db60 100644 --- a/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch +++ b/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Fri, 15 May 2026 20:09:58 -0600 -Subject: [PATCH 03/14] Allow prefixless firmware bot DMs +Subject: [PATCH 03/15] Allow prefixless firmware bot DMs --- examples/companion_radio/BotPolicy.cpp | 2 +- diff --git a/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch b/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch index ed99bd6..89d8b8a 100644 --- a/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch +++ b/patches/meshcore/0004-Align-firmware-bot-commands-with-upstream-behavior.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 04:12:02 +0000 -Subject: [PATCH 04/14] Align firmware bot commands with upstream behavior +Subject: [PATCH 04/15] Align firmware bot commands with upstream behavior --- .../companion_radio/BotCommandRegistry.cpp | 25 +- @@ -816,10 +816,10 @@ index 38d4416d..9eff340b 100644 bool isBotAdvertName(const char* name, size_t name_len); BotFingerprint fingerprintFor(const BotMessage& message); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 2ee5139b..7b87f14f 100644 +index cb557f5d..e84b3a52 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -223,7 +223,7 @@ static uint8_t botTraceHashSize(uint8_t flags) { +@@ -224,7 +224,7 @@ static uint8_t botTraceHashSize(uint8_t flags) { static bool botTracePathShapeValid(uint8_t path_len, uint8_t flags) { uint8_t hash_size = botTraceHashSize(flags); @@ -828,7 +828,7 @@ index 2ee5139b..7b87f14f 100644 (path_len % hash_size) == 0 && (path_len / hash_size) <= MAX_PATH_SIZE; } -@@ -236,17 +236,42 @@ static void botCopyReversedPath(uint8_t *dest, const uint8_t *src, uint8_t hop_c +@@ -237,17 +237,42 @@ static void botCopyReversedPath(uint8_t *dest, const uint8_t *src, uint8_t hop_c static bool botParseTraceHexPath(const BotCommand &command, uint8_t flags, uint8_t path[BOT_MAX_PATH_BYTES], uint8_t *path_len) { if (!path || !path_len || command.args_len == 0) return false; @@ -880,7 +880,7 @@ index 2ee5139b..7b87f14f 100644 *path_len = parsed_len; return true; } -@@ -284,7 +309,7 @@ static BotCommandResult botWriteFormatted(char *output, size_t output_len, const +@@ -285,7 +310,7 @@ static BotCommandResult botWriteFormatted(char *output, size_t output_len, const static bool botParsePubKeyPrefixHex(const BotCommand &command, uint8_t prefix[PUB_KEY_SIZE], uint8_t *prefix_len) { if (!prefix || !prefix_len || command.args_len == 0 || (command.args_len & 1) != 0) return false; size_t byte_len = command.args_len / 2; @@ -889,7 +889,7 @@ index 2ee5139b..7b87f14f 100644 for (size_t i = 0; i < byte_len; i++) { int high = botHexValue(command.args[i * 2]); int low = botHexValue(command.args[i * 2 + 1]); -@@ -341,14 +366,32 @@ static void botAppendHex(char *output, size_t output_len, size_t *pos, const uin +@@ -342,14 +367,32 @@ static void botAppendHex(char *output, size_t output_len, size_t *pos, const uin if (output_len > 0) output[*pos < output_len ? *pos : output_len - 1] = 0; } @@ -929,7 +929,7 @@ index 2ee5139b..7b87f14f 100644 } static size_t botFormatTraceResult(char *output, size_t output_len, uint32_t tag, uint8_t flags, -@@ -358,14 +401,22 @@ static size_t botFormatTraceResult(char *output, size_t output_len, uint32_t tag +@@ -359,14 +402,22 @@ static size_t botFormatTraceResult(char *output, size_t output_len, uint32_t tag uint8_t hop_count = path_len / hash_size; char snr[8]; botFormatQuarters(snr_quarters, snr, sizeof(snr)); @@ -955,7 +955,7 @@ index 2ee5139b..7b87f14f 100644 return botBoundedStrLen(output, output_len); } #endif -@@ -1032,6 +1083,19 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti +@@ -1041,6 +1092,19 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti if (sender_prefix && prefix_len > 0) memcpy(message.sender_key_prefix, sender_prefix, prefix_len); message.sender_key_prefix_len = prefix_len; message.sender_timestamp = sender_timestamp; @@ -975,7 +975,7 @@ index 2ee5139b..7b87f14f 100644 message.text_truncated = FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN + 1), message.text, sizeof(message.text), &message.text_len) == BOT_WRITE_TRUNCATED; recordBotObservation(message, &from, 0xFF); -@@ -1048,6 +1112,7 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n +@@ -1057,6 +1121,7 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n message.channel_name[channel_len] = 0; } message.sender_timestamp = sender_timestamp; @@ -983,7 +983,7 @@ index 2ee5139b..7b87f14f 100644 if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) { message.path_len = (uint8_t)packet->path_len; message.path_hash_size = packet->getPathHashSize(); -@@ -1178,7 +1243,7 @@ BotCommandResult MyMesh::executeBotPrefixCommand(const BotCommand &command, char +@@ -1187,7 +1252,7 @@ BotCommandResult MyMesh::executeBotPrefixCommand(const BotCommand &command, char uint8_t prefix[PUB_KEY_SIZE]; uint8_t prefix_len = 0; if (!botParsePubKeyPrefixHex(command, prefix, &prefix_len)) { @@ -992,7 +992,7 @@ index 2ee5139b..7b87f14f 100644 } ContactInfo match; -@@ -1217,10 +1282,17 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1226,10 +1291,17 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo bool have_path = false; if (command.args_len > 0) { have_path = botParseTraceHexPath(command, flags, path, &path_len); @@ -1012,7 +1012,7 @@ index 2ee5139b..7b87f14f 100644 path_len = raw_len; have_path = true; } -@@ -1230,7 +1302,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1239,7 +1311,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); char response[BOT_MAX_RESPONSE_LEN + 1]; @@ -1021,7 +1021,7 @@ index 2ee5139b..7b87f14f 100644 char final_response[BOT_MAX_RESPONSE_LEN + 1]; size_t final_response_len = 0; if (!botFormatResponseForChannel(message, response, response_len, final_response, sizeof(final_response), -@@ -1247,7 +1319,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1256,7 +1328,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo uint32_t jitter_seed = request_fingerprint.value ? (uint32_t)request_fingerprint.value : 1; uint8_t queue_depth = (uint8_t)_mgr->getOutboundTotal(); BotCoordinatorScheduleResult schedule = ResponseCoordinator::schedule(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, @@ -1030,7 +1030,7 @@ index 2ee5139b..7b87f14f 100644 response_fingerprint, now, jitter_seed, bot_identity_seed, queue_depth, bot_prefs.normal_delay_ms, bot_prefs.normal_jitter_ms, &fingerprint, -@@ -1264,7 +1336,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1273,7 +1345,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo bot_stats.eligible_messages++; bot_stats.pending_responses++; @@ -1039,7 +1039,7 @@ index 2ee5139b..7b87f14f 100644 BOT_TRACE_COOLDOWN_MILLIS); if (!enqueueBotTrace(message, direct_recipient, channel_idx, path, path_len, flags, fingerprint, response_fingerprint, tag, auth_code)) { -@@ -1278,7 +1350,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc +@@ -1287,7 +1359,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc const uint8_t *path, uint8_t path_len, uint8_t flags, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint, uint32_t tag, uint32_t auth_code) { @@ -1048,7 +1048,7 @@ index 2ee5139b..7b87f14f 100644 size_t slot = BOT_PENDING_TRACE_SLOTS; for (size_t i = 0; i < BOT_PENDING_TRACE_SLOTS; i++) { -@@ -1308,7 +1380,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc +@@ -1317,7 +1389,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc pending->auth_code = auth_code; pending->flags = flags; pending->path_len = path_len; @@ -1057,7 +1057,7 @@ index 2ee5139b..7b87f14f 100644 pending->active = true; return true; } -@@ -1372,7 +1444,7 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) +@@ -1381,7 +1453,7 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) pending.expires_at_millis = now_millis + BOT_TRACE_TIMEOUT_MILLIS; char response[BOT_MAX_RESPONSE_LEN + 1]; @@ -1066,7 +1066,7 @@ index 2ee5139b..7b87f14f 100644 char final_response[BOT_MAX_RESPONSE_LEN + 1]; size_t final_response_len = 0; if (!botFormatResponseForChannelKind(pending.channel_kind, response, response_len, final_response, sizeof(final_response), -@@ -1387,7 +1459,9 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { +@@ -1396,7 +1468,9 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { PendingBotTrace *pending = &pending_bot_traces[i]; if (!pending->active || !pending->sent || (int32_t)(now_millis - pending->expires_at_millis) < 0) continue; @@ -1077,7 +1077,7 @@ index 2ee5139b..7b87f14f 100644 char final_response[BOT_MAX_RESPONSE_LEN + 1]; size_t final_response_len = 0; if (botFormatResponseForChannelKind(pending->channel_kind, response, botBoundedStrLen(response, BOT_MAX_RESPONSE_LEN + 1), -@@ -1491,19 +1565,43 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1500,19 +1574,43 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * char response[BOT_MAX_RESPONSE_LEN + 1]; BotCommandResult result; @@ -1127,7 +1127,7 @@ index 2ee5139b..7b87f14f 100644 bot_stats.parse_errors++; return; diff --git a/platformio.ini b/platformio.ini -index dfed7dbd..e3e4b30f 100644 +index e96147b9..e724ad35 100644 --- a/platformio.ini +++ b/platformio.ini @@ -57,6 +57,7 @@ build_src_filter = diff --git a/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch b/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch index 1e2f6ad..13dfd95 100644 --- a/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch +++ b/patches/meshcore/0005-Add-hop-aware-bot-coordination-utility-commands-and-.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 12:38:13 -0600 -Subject: [PATCH 05/14] Add hop-aware bot coordination, utility commands, and +Subject: [PATCH 05/15] Add hop-aware bot coordination, utility commands, and bounded delays Adds new utility commands (TIME, LORA, ID, NEIGHBORS), neighbor tracking, @@ -551,10 +551,10 @@ index de6dd92f..6aac8a1b 100644 size_t actual = boundedStrLen(output, output_len); if (written) *written = actual; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 7b87f14f..34517c44 100644 +index e84b3a52..27ff37c1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -394,31 +394,6 @@ static size_t botFormatTraceSent(char *output, size_t output_len, uint8_t hop_co +@@ -395,31 +395,6 @@ static size_t botFormatTraceSent(char *output, size_t output_len, uint8_t hop_co return botBoundedStrLen(output, output_len); } @@ -586,7 +586,7 @@ index 7b87f14f..34517c44 100644 #endif // these are _pushed_ to client app at any time -@@ -663,6 +638,13 @@ void MyMesh::onContactsFull() { +@@ -664,6 +639,13 @@ void MyMesh::onContactsFull() { void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { #if CMESH_BOT_ENABLED autoLearnKnownBotAdvert(contact); @@ -600,7 +600,7 @@ index 7b87f14f..34517c44 100644 #endif if (_serial->isConnected()) { -@@ -833,7 +815,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t +@@ -842,7 +824,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t markConnectionActive(from); // in case this is from a server, and we have a connection queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); #if CMESH_BOT_ENABLED @@ -609,7 +609,7 @@ index 7b87f14f..34517c44 100644 #endif } -@@ -850,7 +832,7 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin +@@ -859,7 +841,7 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); #if CMESH_BOT_ENABLED @@ -618,7 +618,7 @@ index 7b87f14f..34517c44 100644 #endif } -@@ -1073,7 +1055,7 @@ bool MyMesh::handleBotCLI(const char *args) { +@@ -1082,7 +1064,7 @@ bool MyMesh::handleBotCLI(const char *args) { } void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix, @@ -627,7 +627,7 @@ index 7b87f14f..34517c44 100644 BotMessage message; memset(&message, 0, sizeof(message)); message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs); -@@ -1084,7 +1066,13 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti +@@ -1093,7 +1075,13 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti message.sender_key_prefix_len = prefix_len; message.sender_timestamp = sender_timestamp; message.received_at_timestamp = getRTCClock()->getCurrentTime(); @@ -642,7 +642,7 @@ index 7b87f14f..34517c44 100644 if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) { message.path_len = from.out_path_len; message.path_hash_size = (from.out_path_len >> 6) + 1; -@@ -1270,6 +1258,220 @@ BotCommandResult MyMesh::executeBotPrefixCommand(const BotCommand &command, char +@@ -1279,6 +1267,220 @@ BotCommandResult MyMesh::executeBotPrefixCommand(const BotCommand &command, char return botWriteFormatted(output, output_len, "Prefix %s %s", key_hex, name); } @@ -863,7 +863,7 @@ index 7b87f14f..34517c44 100644 bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const BotCommand &command) { uint8_t hash_size = command.args_len > 0 ? botConfiguredTraceHashSize(_prefs.path_hash_mode) : message.path_hash_size; -@@ -1277,9 +1479,14 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1286,9 +1488,14 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo uint8_t flags = 0; if (!botTraceFlagForHashSize(hash_size, &flags)) return false; @@ -878,7 +878,7 @@ index 7b87f14f..34517c44 100644 if (command.args_len > 0) { have_path = botParseTraceHexPath(command, flags, path, &path_len); } else if (message.path_hash_size == hash_size) { -@@ -1292,13 +1499,20 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1301,13 +1508,20 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo } else { botCopyReversedPath(path, message.path, message.path_hash_count, hash_size); } @@ -900,7 +900,7 @@ index 7b87f14f..34517c44 100644 BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); char response[BOT_MAX_RESPONSE_LEN + 1]; -@@ -1316,14 +1530,15 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1325,14 +1539,15 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo uint32_t now = _ms->getMillis(); uint32_t bot_identity_seed; memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed)); @@ -919,7 +919,7 @@ index 7b87f14f..34517c44 100644 if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) { bot_stats.send_failures++; return true; -@@ -1372,6 +1587,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc +@@ -1381,6 +1596,7 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc pending->channel_idx = channel_idx; pending->channel_kind = message.channel_kind; StrHelper::strzcpy(pending->channel_name, message.channel_name, sizeof(pending->channel_name)); @@ -927,7 +927,7 @@ index 7b87f14f..34517c44 100644 memcpy(pending->sender_key_prefix, message.sender_key_prefix, sizeof(pending->sender_key_prefix)); pending->sender_key_prefix_len = message.sender_key_prefix_len; pending->request_fingerprint = request_fingerprint; -@@ -1442,16 +1658,9 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) +@@ -1451,16 +1667,9 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) sendDirect(pkt, pending.path, pending.path_len); pending.sent = true; pending.expires_at_millis = now_millis + BOT_TRACE_TIMEOUT_MILLIS; @@ -947,7 +947,7 @@ index 7b87f14f..34517c44 100644 } void MyMesh::expirePendingBotTraces(uint32_t now_millis) { -@@ -1460,8 +1669,26 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { +@@ -1469,8 +1678,26 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { if (!pending->active || !pending->sent || (int32_t)(now_millis - pending->expires_at_millis) < 0) continue; char response[BOT_MAX_RESPONSE_LEN + 1]; @@ -976,7 +976,7 @@ index 7b87f14f..34517c44 100644 char final_response[BOT_MAX_RESPONSE_LEN + 1]; size_t final_response_len = 0; if (botFormatResponseForChannelKind(pending->channel_kind, response, botBoundedStrLen(response, BOT_MAX_RESPONSE_LEN + 1), -@@ -1579,27 +1806,46 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1588,27 +1815,46 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * BotCommandContext context; buildBotCommandContext(context, command.id); @@ -1036,7 +1036,7 @@ index 7b87f14f..34517c44 100644 } } if (result.code == BOT_COMMAND_RESULT_NOT_HANDLED || result.code == BOT_COMMAND_RESULT_NO_SPACE || result.text_len == 0) { -@@ -1620,14 +1866,16 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1629,14 +1875,16 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * uint32_t due_at_millis = 0; uint32_t bot_identity_seed; memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed)); @@ -1056,7 +1056,7 @@ index 7b87f14f..34517c44 100644 if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) { bot_stats.send_failures++; return; -@@ -2102,8 +2350,11 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, +@@ -2111,8 +2359,11 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, if (!pending->active || !pending->sent || pending->tag != tag || pending->auth_code != auth_code) continue; char response[BOT_MAX_RESPONSE_LEN + 1]; @@ -1070,7 +1070,7 @@ index 7b87f14f..34517c44 100644 char final_response[BOT_MAX_RESPONSE_LEN + 1]; size_t final_response_len = 0; if (botFormatResponseForChannelKind(pending->channel_kind, response, response_len, final_response, -@@ -2149,6 +2400,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe +@@ -2159,6 +2410,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe memset(pending_bot_traces, 0, sizeof(pending_bot_traces)); memset(pending_emergency_forwards, 0, sizeof(pending_emergency_forwards)); memset(bot_command_cooldowns, 0, sizeof(bot_command_cooldowns)); @@ -1079,10 +1079,10 @@ index 7b87f14f..34517c44 100644 ResponseCoordinator::clearRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS); KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h -index 67bdc010..9685e89a 100644 +index 1197caff..488987c0 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h -@@ -209,7 +209,7 @@ private: +@@ -212,7 +212,7 @@ private: void printBotPrefs(); bool handleBotCLI(const char *args); void observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix, @@ -1091,7 +1091,7 @@ index 67bdc010..9685e89a 100644 void observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text, uint32_t sender_timestamp, const mesh::Packet *packet); void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx); -@@ -222,7 +222,14 @@ private: +@@ -225,7 +225,14 @@ private: BotFingerprint response_fingerprint); bool handleBotTraceCommand(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const BotCommand &command); @@ -1106,7 +1106,7 @@ index 67bdc010..9685e89a 100644 bool enqueueBotTrace(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const uint8_t *path, uint8_t path_len, uint8_t flags, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint, uint32_t tag, uint32_t auth_code); -@@ -298,6 +305,7 @@ private: +@@ -302,6 +309,7 @@ private: uint8_t recipient_pub_key[PUB_KEY_SIZE]; uint8_t channel_idx; char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1]; @@ -1114,7 +1114,7 @@ index 67bdc010..9685e89a 100644 uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN]; uint8_t sender_key_prefix_len; BotFingerprint request_fingerprint; -@@ -319,6 +327,7 @@ private: +@@ -323,6 +331,7 @@ private: BotCoordinatorPending bot_coordinator_pending[BOT_COORDINATOR_PENDING_SLOTS]; BotCoordinatorRecent bot_coordinator_recent[BOT_COORDINATOR_RECENT_SLOTS]; BotKnownBotEntry known_bot_entries[BOT_KNOWN_BOT_SLOTS]; @@ -1246,11 +1246,11 @@ index 7717bee7..f549b887 100644 bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint); BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis); diff --git a/src/Mesh.cpp b/src/Mesh.cpp -index 57fee140..9c512b20 100644 +index 87ad61af..4a3cc567 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp -@@ -53,12 +53,22 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { - uint8_t offset = pkt->path_len << path_sz; +@@ -55,12 +55,22 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { + uint16_t offset = (uint16_t)pkt->path_len << path_sz; if (offset >= len) { // TRACE has reached end of given path onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { diff --git a/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch b/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch index 2a06bdf..bc7cd0b 100644 --- a/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch +++ b/patches/meshcore/0006-Mark-bot-adverts-speed-up-initial-advert-default-con.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 18:58:28 -0600 -Subject: [PATCH 06/14] Mark bot adverts, speed up initial advert, default +Subject: [PATCH 06/15] Mark bot adverts, speed up initial advert, default contacts auto-overwrite Three coordination/discoverability fixes: @@ -42,10 +42,10 @@ index 4fdd4bdd..d30fb7f2 100644 #define BOT_PREFS_MAX_ADVERT_MILLIS (7UL * 24UL * 60UL * 60UL * 1000UL) #define BOT_PREFS_SERIALIZED_SIZE 296 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 34517c44..9209dd86 100644 +index 27ff37c1..b2c47df4 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -848,7 +848,7 @@ void MyMesh::applyBotPrefs() { +@@ -857,7 +857,7 @@ void MyMesh::applyBotPrefs() { } if (bot_prefs.enabled) { scheduleBotLocalAdvert(bot_prefs.local_advert_interval_ms ? BOT_PREFS_INITIAL_LOCAL_ADVERT_MILLIS : 0); @@ -54,7 +54,7 @@ index 34517c44..9209dd86 100644 } else { scheduleBotLocalAdvert(0); scheduleBotFloodAdvert(0); -@@ -1996,11 +1996,11 @@ void MyMesh::sendQueuedEmergencyForwards() { +@@ -2005,11 +2005,11 @@ void MyMesh::sendQueuedEmergencyForwards() { bool MyMesh::sendBotSelfAdvert(bool flood) { mesh::Packet* pkt; char bot_advert_name[sizeof(((ContactInfo*)0)->name)]; @@ -69,7 +69,7 @@ index 34517c44..9209dd86 100644 } if (!pkt) return false; -@@ -2041,6 +2041,18 @@ void MyMesh::tickBot() { +@@ -2050,6 +2050,18 @@ void MyMesh::tickBot() { } #endif @@ -88,7 +88,7 @@ index 34517c44..9209dd86 100644 void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) { int i = 0; -@@ -2479,6 +2491,9 @@ void MyMesh::begin(bool has_display) { +@@ -2489,6 +2501,9 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours @@ -98,7 +98,7 @@ index 34517c44..9209dd86 100644 #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { -@@ -2780,10 +2795,12 @@ void MyMesh::handleCmdFrame(size_t len) { +@@ -2794,10 +2809,12 @@ void MyMesh::handleCmdFrame(size_t len) { } } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { mesh::Packet* pkt; @@ -113,7 +113,7 @@ index 34517c44..9209dd86 100644 } if (pkt) { if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) -@@ -2864,10 +2881,12 @@ void MyMesh::handleCmdFrame(size_t len) { +@@ -2878,10 +2895,12 @@ void MyMesh::handleCmdFrame(size_t len) { if (len < 1 + PUB_KEY_SIZE) { // export SELF mesh::Packet* pkt; @@ -128,7 +128,7 @@ index 34517c44..9209dd86 100644 } if (pkt) { pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode -@@ -3748,10 +3767,12 @@ void MyMesh::loop() { +@@ -3804,10 +3823,12 @@ void MyMesh::loop() { bool MyMesh::advert() { mesh::Packet* pkt; @@ -144,10 +144,10 @@ index 34517c44..9209dd86 100644 if (pkt) { sendZeroHop(pkt); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h -index 9685e89a..711bb69f 100644 +index 488987c0..efc5240a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h -@@ -248,6 +248,9 @@ private: +@@ -251,6 +251,9 @@ private: bool sendBotSelfAdvert(bool flood); #endif diff --git a/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch b/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch index a202645..65cab65 100644 --- a/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch +++ b/patches/meshcore/0007-Prefix-bot-responses-with-request-token-for-inter-bo.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 19:33:04 -0600 -Subject: [PATCH 07/14] Prefix bot responses with request token for inter-bot +Subject: [PATCH 07/15] Prefix bot responses with request token for inter-bot suppression Adds a 4-hex request token (low 16 bits of the request fingerprint) as a @@ -131,10 +131,10 @@ index 9eff340b..6e5cfd9a 100644 size_t maxResponseLenForChannel(BotChannelKind channel_kind); bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id, diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 9209dd86..02339124 100644 +index b2c47df4..d4bca6ce 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -1209,6 +1209,11 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di +@@ -1218,6 +1218,11 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di if (!botFormatResponseForChannel(message, text, text_len, pending->text, sizeof(pending->text), &pending->text_len)) { return false; } @@ -146,7 +146,7 @@ index 9209dd86..02339124 100644 pending->active = true; return true; } -@@ -1613,6 +1618,17 @@ BotFingerprint MyMesh::traceResponseFingerprintFor(const PendingBotTrace &pendin +@@ -1622,6 +1627,17 @@ BotFingerprint MyMesh::traceResponseFingerprintFor(const PendingBotTrace &pendin bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, BotFingerprint response_fingerprint, uint32_t now_millis) { @@ -164,7 +164,7 @@ index 9209dd86..02339124 100644 bool success = false; if (pending.direct) { ContactInfo *recipient = lookupContactByPubKey(pending.recipient_pub_key, PUB_KEY_SIZE); -@@ -1620,12 +1636,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, +@@ -1629,12 +1645,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, uint32_t expected_ack = 0; uint32_t est_timeout = 0; uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); @@ -178,7 +178,7 @@ index 9209dd86..02339124 100644 success = result != MSG_SEND_FAILED; if (success && expected_ack) { expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); -@@ -1638,7 +1649,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, +@@ -1647,7 +1658,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, ChannelDetails channel; uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); success = getChannel(pending.channel_idx, channel) && @@ -187,7 +187,7 @@ index 9209dd86..02339124 100644 } if (success) { -@@ -1744,6 +1755,19 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati +@@ -1753,6 +1764,19 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati bool MyMesh::observeBotGroupResponse(const BotMessage &message) { if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false; @@ -207,7 +207,7 @@ index 9209dd86..02339124 100644 BotFingerprint fingerprint = FirmwareBot::responseFingerprintFor(message, message.text, message.text_len); if (ResponseCoordinator::recentlySent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, fingerprint, _ms->getMillis())) { return true; -@@ -1751,7 +1775,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { +@@ -1760,7 +1784,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { if (ResponseCoordinator::suppress(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint)) { return true; } diff --git a/patches/meshcore/0008-Drop-MCBOT-advert-suffix-and-auto-learn-known-bot-pa.patch b/patches/meshcore/0008-Drop-MCBOT-advert-suffix-and-auto-learn-known-bot-pa.patch index 1b9120f..a2e46db 100644 --- a/patches/meshcore/0008-Drop-MCBOT-advert-suffix-and-auto-learn-known-bot-pa.patch +++ b/patches/meshcore/0008-Drop-MCBOT-advert-suffix-and-auto-learn-known-bot-pa.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 19:55:36 -0600 -Subject: [PATCH 08/14] Drop [MCBOT] advert suffix and auto-learn known-bot +Subject: [PATCH 08/15] Drop [MCBOT] advert suffix and auto-learn known-bot path Removes BOT_ADVERT_MARKER / writeBotAdvertName / isBotAdvertName from @@ -93,10 +93,10 @@ index 6e5cfd9a..708432a2 100644 BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len); uint16_t requestToken(BotFingerprint request_fingerprint); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 02339124..9121d42c 100644 +index d4bca6ce..a8b4680b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -637,7 +637,6 @@ void MyMesh::onContactsFull() { +@@ -638,7 +638,6 @@ void MyMesh::onContactsFull() { void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { #if CMESH_BOT_ENABLED @@ -104,7 +104,7 @@ index 02339124..9121d42c 100644 if ((path_len & 63) == 0) { float rssi = radio_driver.getLastRSSI(); if (rssi > 32767.0f) rssi = 32767.0f; -@@ -1167,21 +1166,6 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com +@@ -1176,21 +1175,6 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com } } @@ -126,7 +126,7 @@ index 02339124..9121d42c 100644 bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const char *text, size_t text_len, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint) { -@@ -2019,12 +2003,10 @@ void MyMesh::sendQueuedEmergencyForwards() { +@@ -2028,12 +2012,10 @@ void MyMesh::sendQueuedEmergencyForwards() { bool MyMesh::sendBotSelfAdvert(bool flood) { mesh::Packet* pkt; @@ -141,7 +141,7 @@ index 02339124..9121d42c 100644 } if (!pkt) return false; -@@ -2065,17 +2047,6 @@ void MyMesh::tickBot() { +@@ -2074,17 +2056,6 @@ void MyMesh::tickBot() { } #endif @@ -159,7 +159,7 @@ index 02339124..9121d42c 100644 void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, const char *text) { -@@ -2819,12 +2790,10 @@ void MyMesh::handleCmdFrame(size_t len) { +@@ -2833,12 +2804,10 @@ void MyMesh::handleCmdFrame(size_t len) { } } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { mesh::Packet* pkt; @@ -174,7 +174,7 @@ index 02339124..9121d42c 100644 } if (pkt) { if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) -@@ -2905,12 +2874,10 @@ void MyMesh::handleCmdFrame(size_t len) { +@@ -2919,12 +2888,10 @@ void MyMesh::handleCmdFrame(size_t len) { if (len < 1 + PUB_KEY_SIZE) { // export SELF mesh::Packet* pkt; @@ -189,7 +189,7 @@ index 02339124..9121d42c 100644 } if (pkt) { pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode -@@ -3791,12 +3758,10 @@ void MyMesh::loop() { +@@ -3847,12 +3814,10 @@ void MyMesh::loop() { bool MyMesh::advert() { mesh::Packet* pkt; @@ -205,10 +205,10 @@ index 02339124..9121d42c 100644 if (pkt) { sendZeroHop(pkt); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h -index 711bb69f..46c3ff3c 100644 +index efc5240a..7aa56326 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h -@@ -216,7 +216,6 @@ private: +@@ -219,7 +219,6 @@ private: bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender); bool observeBotGroupResponse(const BotMessage &message); void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id); @@ -216,7 +216,7 @@ index 711bb69f..46c3ff3c 100644 bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, const char *text, size_t text_len, BotFingerprint request_fingerprint, BotFingerprint response_fingerprint); -@@ -248,9 +247,6 @@ private: +@@ -251,9 +250,6 @@ private: bool sendBotSelfAdvert(bool flood); #endif diff --git a/patches/meshcore/0009-Widen-hop-step-drop-ack-prefix-from-test-response.patch b/patches/meshcore/0009-Widen-hop-step-drop-ack-prefix-from-test-response.patch index d4a3bed..68e20f2 100644 --- a/patches/meshcore/0009-Widen-hop-step-drop-ack-prefix-from-test-response.patch +++ b/patches/meshcore/0009-Widen-hop-step-drop-ack-prefix-from-test-response.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 20:05:19 -0600 -Subject: [PATCH 09/14] Widen hop-step + drop "ack " prefix from test response +Subject: [PATCH 09/15] Widen hop-step + drop "ack " prefix from test response Two changes: diff --git a/patches/meshcore/0010-Widen-coordinator-delays-a-tiny-bit-more.patch b/patches/meshcore/0010-Widen-coordinator-delays-a-tiny-bit-more.patch index f9ecded..b5a887d 100644 --- a/patches/meshcore/0010-Widen-coordinator-delays-a-tiny-bit-more.patch +++ b/patches/meshcore/0010-Widen-coordinator-delays-a-tiny-bit-more.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 20:31:12 -0600 -Subject: [PATCH 10/14] Widen coordinator delays a tiny bit more +Subject: [PATCH 10/15] Widen coordinator delays a tiny bit more ~25% bump across the main coordination knobs: - BOT_RESPONSE_DELAY_BASE_MILLIS 1200 -> 1500 diff --git a/patches/meshcore/0011-Auto-advert-on-startup-initial-flood-at-5s-instead-o.patch b/patches/meshcore/0011-Auto-advert-on-startup-initial-flood-at-5s-instead-o.patch index c946d9e..9d33755 100644 --- a/patches/meshcore/0011-Auto-advert-on-startup-initial-flood-at-5s-instead-o.patch +++ b/patches/meshcore/0011-Auto-advert-on-startup-initial-flood-at-5s-instead-o.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 20:49:51 -0600 -Subject: [PATCH 11/14] Auto-advert on startup (initial flood at 5s instead of +Subject: [PATCH 11/15] Auto-advert on startup (initial flood at 5s instead of 120s) BOT_PREFS_INITIAL_FLOOD_ADVERT_MILLIS 120000 -> 5000. The bot now sends diff --git a/patches/meshcore/0012-Spell-out-hops-byte-in-test-path-drop-channel-name-f.patch b/patches/meshcore/0012-Spell-out-hops-byte-in-test-path-drop-channel-name-f.patch index 388b18b..63c623a 100644 --- a/patches/meshcore/0012-Spell-out-hops-byte-in-test-path-drop-channel-name-f.patch +++ b/patches/meshcore/0012-Spell-out-hops-byte-in-test-path-drop-channel-name-f.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 21:32:04 -0600 -Subject: [PATCH 12/14] Spell out hops/byte in test+path; drop channel name +Subject: [PATCH 12/15] Spell out hops/byte in test+path; drop channel name from test ack Test response (writeAckResponse): diff --git a/patches/meshcore/0013-Hop-delay-grows-quadratically-wider-gaps-at-higher-h.patch b/patches/meshcore/0013-Hop-delay-grows-quadratically-wider-gaps-at-higher-h.patch index a6e8d21..2073251 100644 --- a/patches/meshcore/0013-Hop-delay-grows-quadratically-wider-gaps-at-higher-h.patch +++ b/patches/meshcore/0013-Hop-delay-grows-quadratically-wider-gaps-at-higher-h.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 22:18:02 -0600 -Subject: [PATCH 13/14] =?UTF-8?q?Hop=20delay=20grows=20quadratically=20?= +Subject: [PATCH 13/15] =?UTF-8?q?Hop=20delay=20grows=20quadratically=20?= =?UTF-8?q?=E2=80=94=20wider=20gaps=20at=20higher=20hop=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 diff --git a/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch b/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch index 2871f56..879d20d 100644 --- a/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch +++ b/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Mon, 18 May 2026 08:02:27 -0600 -Subject: [PATCH] Stabilize bot diagnostics and duplicate suppression +Subject: [PATCH 14/15] Stabilize bot diagnostics and duplicate suppression --- examples/companion_radio/BotTypes.h | 9 ++- @@ -128,10 +128,10 @@ index afd44d41..d256e61a 100644 size_t* new_len) { if (new_len) *new_len = text_len; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 9121d42c..5d333328 100644 +index a8b4680b..5e00c9d2 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp -@@ -120,9 +120,6 @@ +@@ -121,9 +121,6 @@ #define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 #define LAZY_CONTACTS_WRITE_DELAY 5000 @@ -141,7 +141,7 @@ index 9121d42c..5d333328 100644 #define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" #if CMESH_BOT_ENABLED -@@ -1072,7 +1069,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti +@@ -1081,7 +1078,12 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti if (rssi < -32768.0f) rssi = -32768.0f; recordBotNeighbor(from.id.pub_key, (int16_t)rssi, (int8_t)(packet->getSNR() * 4)); } @@ -155,7 +155,7 @@ index 9121d42c..5d333328 100644 message.path_len = from.out_path_len; message.path_hash_size = (from.out_path_len >> 6) + 1; message.path_hash_count = from.out_path_len & 63; -@@ -1430,11 +1432,16 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact +@@ -1439,11 +1441,16 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact } BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); @@ -173,7 +173,7 @@ index 9121d42c..5d333328 100644 uint32_t bot_identity_seed; memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed)); uint32_t jitter_seed = (uint32_t)request_fingerprint.value ^ bot_identity_seed; -@@ -1504,6 +1511,11 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo +@@ -1513,6 +1520,11 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo if (!have_path || !botTracePathShapeValid(path_len, flags)) return false; BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); @@ -185,7 +185,7 @@ index 9121d42c..5d333328 100644 char response[BOT_MAX_RESPONSE_LEN + 1]; size_t response_len = botFormatTraceSent(response, sizeof(response), (uint8_t)(path_len / hash_size)); char final_response[BOT_MAX_RESPONSE_LEN + 1]; -@@ -1640,6 +1652,10 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, +@@ -1649,6 +1661,10 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, bot_stats.sent_messages++; ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, response_fingerprint, now_millis); @@ -196,7 +196,7 @@ index 9121d42c..5d333328 100644 } else { bot_stats.send_failures++; } -@@ -1747,6 +1763,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { +@@ -1756,6 +1772,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { size_t token_prefix_len = 0; bool token_suppressed = false; if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) { @@ -204,7 +204,7 @@ index 9121d42c..5d333328 100644 if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) { token_suppressed = true; } -@@ -1790,7 +1807,6 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1799,7 +1816,6 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++; return; } @@ -212,7 +212,7 @@ index 9121d42c..5d333328 100644 if ((command.id != BOT_COMMAND_UNKNOWN && command.id != BOT_COMMAND_UNSUPPORTED && !BotPrefsCodec::commandEnabled(bot_prefs, command.id)) || FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) { -@@ -1869,6 +1885,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * +@@ -1878,6 +1894,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * } BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); @@ -224,7 +224,7 @@ index 9121d42c..5d333328 100644 BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len); BotFingerprint fingerprint; uint32_t due_at_millis = 0; -@@ -1904,7 +1925,6 @@ void MyMesh::sendQueuedBotResponses() { +@@ -1913,7 +1934,6 @@ void MyMesh::sendQueuedBotResponses() { while (true) { BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now); if (ready.result == BOT_COORDINATOR_READY_NONE) return; @@ -232,7 +232,7 @@ index 9121d42c..5d333328 100644 if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) { bot_stats.suppressed_responses++; } else if (ready.result == BOT_COORDINATOR_READY_EXPIRED) { -@@ -1941,6 +1961,12 @@ void MyMesh::sendQueuedBotResponses() { +@@ -1950,6 +1970,12 @@ void MyMesh::sendQueuedBotResponses() { pending->active = false; continue; } @@ -245,7 +245,7 @@ index 9121d42c..5d333328 100644 bool success = false; if (pending->direct) { -@@ -1960,15 +1986,19 @@ void MyMesh::sendQueuedBotResponses() { +@@ -1969,15 +1995,19 @@ void MyMesh::sendQueuedBotResponses() { } } else if (pending->channel_idx != 0xFF) { ChannelDetails channel; diff --git a/patches/meshcore/0015-Add-sig-air-coin-commands-derive-help-listings-from-.patch b/patches/meshcore/0015-Add-sig-air-coin-commands-derive-help-listings-from-.patch new file mode 100644 index 0000000..7b00158 --- /dev/null +++ b/patches/meshcore/0015-Add-sig-air-coin-commands-derive-help-listings-from-.patch @@ -0,0 +1,264 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: CJ Vana +Date: Fri, 10 Jul 2026 15:19:21 -0600 +Subject: [PATCH 15/15] Add sig/air/coin commands; derive help listings from + the registry + +- sig (aliases snr/rssi/signal): reports the SNR the bot heard the + request at, plus local last-RSSI and noise floor - range testing aid +- air (alias airtime): TX/RX airtime and flood/direct packet counters +- coin (aliases flip/coinflip): coin flip alongside roll/dice/magic8 +- help and cmd listings now walk the command registry filtered by + context class (chat vs 'cmd diag') instead of hand-picked id arrays + that had drifted (magic8 was missing from both) +- BOT_PREFS_VERSION 6 -> 7 so deployed bots re-default command_mask + and pick up the mask bits for the added commands +--- + .../companion_radio/BotCommandRegistry.cpp | 12 +++ + examples/companion_radio/BotCommands.cpp | 81 ++++++++++++++----- + examples/companion_radio/BotTypes.h | 15 +++- + examples/companion_radio/MyMesh.cpp | 8 +- + 4 files changed, 89 insertions(+), 27 deletions(-) + +diff --git a/examples/companion_radio/BotCommandRegistry.cpp b/examples/companion_radio/BotCommandRegistry.cpp +index c429259c..65fed686 100644 +--- a/examples/companion_radio/BotCommandRegistry.cpp ++++ b/examples/companion_radio/BotCommandRegistry.cpp +@@ -14,6 +14,9 @@ const char* const kChannelsAliases[] = { "channel" }; + const char* const kPathAliases[] = { "p", "decode", "route" }; + const char* const kPrefixAliases[] = { "lookup" }; + const char* const kNeighborsAliases[] = { "near" }; ++const char* const kSigAliases[] = { "snr", "rssi", "signal" }; ++const char* const kAirAliases[] = { "airtime" }; ++const char* const kCoinAliases[] = { "flip", "coinflip" }; + + size_t boundedStrLen(const char* value, size_t max_len) { + size_t len = 0; +@@ -74,6 +77,15 @@ const BotCommandMetadata kCommands[] = { + { BOT_COMMAND_NEIGHBORS, "neighbors", kNeighborsAliases, 1, BOT_COMMAND_MASK_NEIGHBORS, BOT_COMMAND_VISIBILITY_DISCOVERABLE, + BOT_COMMAND_CONTEXT_DIAGNOSTIC, "Show recent direct neighbors", "neighbors", + "Show nodes heard directly within the last hour with RSSI and SNR." }, ++ { BOT_COMMAND_SIG, "sig", kSigAliases, 3, BOT_COMMAND_MASK_SIG, BOT_COMMAND_VISIBILITY_DISCOVERABLE, ++ BOT_COMMAND_CONTEXT_DIAGNOSTIC, "Report received signal", "sig", ++ "Show how the bot heard your request: SNR plus local RSSI and noise floor." }, ++ { BOT_COMMAND_AIR, "air", kAirAliases, 1, BOT_COMMAND_MASK_AIR, BOT_COMMAND_VISIBILITY_DISCOVERABLE, ++ BOT_COMMAND_CONTEXT_DIAGNOSTIC, "Show radio airtime", "air", ++ "Show local TX/RX airtime and flood/direct packet counters." }, ++ { BOT_COMMAND_COIN, "coin", kCoinAliases, 2, BOT_COMMAND_MASK_COIN, BOT_COMMAND_VISIBILITY_DISCOVERABLE, ++ BOT_COMMAND_CONTEXT_NORMAL, "Flip a coin", "coin", ++ "Flip a coin and report heads or tails." }, + { BOT_COMMAND_UNKNOWN, "unknown", NULL, 0, 0, BOT_COMMAND_VISIBILITY_INTERNAL, + BOT_COMMAND_CONTEXT_INTERNAL, "Unknown command", "unknown", "Internal unknown-command handler." } + }; +diff --git a/examples/companion_radio/BotCommands.cpp b/examples/companion_radio/BotCommands.cpp +index 198dc32f..4fbbbee0 100644 +--- a/examples/companion_radio/BotCommands.cpp ++++ b/examples/companion_radio/BotCommands.cpp +@@ -313,45 +313,55 @@ void formatQuarters(int8_t quarters, char* output, size_t output_len) { + snprintf(output, output_len, "%s%d.%02d", sign, value / 4, (value % 4) * 25); + } + +-void appendCommandList(char* output, size_t output_len, size_t* pos, const BotCommandId* ids, size_t id_count) { ++bool commandInClasses(const BotCommandMetadata* command, const BotCommandContextClass* classes, size_t class_count) { ++ for (size_t i = 0; i < class_count; i++) { ++ if (command->context_class == classes[i]) return true; ++ } ++ return false; ++} ++ ++// Listings are derived from the registry so an added command can never ++// be missed; classes split the list because all commands do not fit one ++// group-channel message. ++void appendCommandsByClass(char* output, size_t output_len, size_t* pos, const BotCommandContextClass* classes, ++ size_t class_count) { + bool first = true; +- for (size_t i = 0; i < id_count; i++) { +- const BotCommandMetadata* command = BotCommandRegistry::findById(ids[i]); ++ for (size_t i = 0; i < BotCommandRegistry::commandCount(); i++) { ++ const BotCommandMetadata* command = BotCommandRegistry::commandAt(i); + if (!command || command->visibility != BOT_COMMAND_VISIBILITY_DISCOVERABLE) continue; +- if (!first) appendText(output, output_len, pos, ", "); ++ if (!commandInClasses(command, classes, class_count)) continue; ++ if (!first) appendText(output, output_len, pos, " "); + appendText(output, output_len, pos, command->name); + first = false; + } + } + +-BotCommandResult executeCmd(char* output, size_t output_len) { +- static const BotCommandId ids[] = { +- BOT_COMMAND_TEST, BOT_COMMAND_PING, BOT_COMMAND_HELP, BOT_COMMAND_HELLO, BOT_COMMAND_ABOUT, +- BOT_COMMAND_ROLL, BOT_COMMAND_DICE, BOT_COMMAND_STATUS, BOT_COMMAND_CHANNELS, BOT_COMMAND_VERSION, +- BOT_COMMAND_STATS, BOT_COMMAND_PATH, BOT_COMMAND_TRACE, BOT_COMMAND_TRACER, BOT_COMMAND_PREFIX, +- BOT_COMMAND_TIME, BOT_COMMAND_LORA, BOT_COMMAND_ID, BOT_COMMAND_NEIGHBORS +- }; ++const BotCommandContextClass kChatClasses[] = { BOT_COMMAND_CONTEXT_NORMAL, BOT_COMMAND_CONTEXT_TRACE }; ++const BotCommandContextClass kDiagClasses[] = { BOT_COMMAND_CONTEXT_DIAGNOSTIC, BOT_COMMAND_CONTEXT_LOCAL_CONTACT }; ++ ++BotCommandResult executeCmd(const BotCommand& command, char* output, size_t output_len) { + if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0); + output[0] = 0; + size_t pos = 0; ++ if (textEqualsIgnoreCase(command.args, command.args_len, "diag")) { ++ appendText(output, output_len, &pos, "Diag: "); ++ appendCommandsByClass(output, output_len, &pos, kDiagClasses, 2); ++ return resultForAppend(output, output_len, pos); ++ } + appendText(output, output_len, &pos, "Commands: "); +- appendCommandList(output, output_len, &pos, ids, sizeof(ids) / sizeof(ids[0])); ++ appendCommandsByClass(output, output_len, &pos, kChatClasses, 2); ++ appendText(output, output_len, &pos, " | cmd diag"); + return resultForAppend(output, output_len, pos); + } + + BotCommandResult executeHelp(const BotCommand& command, char* output, size_t output_len) { + if (command.args_len == 0) { +- static const BotCommandId ids[] = { +- BOT_COMMAND_HELP, BOT_COMMAND_CMD, BOT_COMMAND_PING, BOT_COMMAND_TEST, BOT_COMMAND_HELLO, BOT_COMMAND_ROLL, +- BOT_COMMAND_DICE, BOT_COMMAND_TRACE, BOT_COMMAND_TRACER, BOT_COMMAND_PREFIX, BOT_COMMAND_TIME, +- BOT_COMMAND_LORA, BOT_COMMAND_ID, BOT_COMMAND_NEIGHBORS +- }; + if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0); + output[0] = 0; + size_t pos = 0; + appendText(output, output_len, &pos, "Commands: "); +- appendCommandList(output, output_len, &pos, ids, sizeof(ids) / sizeof(ids[0])); +- appendText(output, output_len, &pos, "; help "); ++ appendCommandsByClass(output, output_len, &pos, kChatClasses, 2); ++ appendText(output, output_len, &pos, " | cmd diag | help "); + return resultForAppend(output, output_len, pos); + } + +@@ -531,6 +541,31 @@ uint8_t batteryPercentFromMillivolts(uint16_t mv) { + return (uint8_t)(((uint32_t)(mv - kEmpty) * 100UL) / (uint32_t)(kFull - kEmpty)); + } + ++BotCommandResult executeSig(const BotCommandContext& context, char* output, size_t output_len) { ++ char snr[8]; ++ formatQuarters(context.path_snr_quarters, snr, sizeof(snr)); ++ const char* target = context.response_target[0] ? context.response_target : NULL; ++ if (target) { ++ return writeFormatted(output, output_len, "Sig @[%s]: heard you at SNR %s | last RSSI %d dBm, noise %d dBm", target, ++ snr, (int)context.last_rssi, (int)context.noise_floor); ++ } ++ return writeFormatted(output, output_len, "Sig: heard you at SNR %s | last RSSI %d dBm, noise %d dBm", snr, ++ (int)context.last_rssi, (int)context.noise_floor); ++} ++ ++BotCommandResult executeAir(const BotCommandContext& context, char* output, size_t output_len) { ++ return writeFormatted(output, output_len, "Air: tx %lus rx %lus | rx flood %lu direct %lu | tx flood %lu direct %lu", ++ (unsigned long)context.tx_airtime_seconds, (unsigned long)context.rx_airtime_seconds, ++ (unsigned long)context.flood_recv, (unsigned long)context.direct_recv, ++ (unsigned long)context.flood_sent, (unsigned long)context.direct_sent); ++} ++ ++BotCommandResult executeCoin(const BotCommandContext& context, char* output, size_t output_len) { ++ uint32_t state = context.random_seed ^ 0x636F696EUL; ++ if (state == 0) state = 1; ++ return writeFormatted(output, output_len, "Coin: %s", rollOnce(&state, 2) == 1 ? "Heads" : "Tails"); ++} ++ + BotCommandResult executeTest(const BotCommand& command, const BotCommandContext& context, char* output, size_t output_len) { + const char* name = context.node_name[0] ? context.node_name : "local"; + char received_at[9]; +@@ -591,7 +626,7 @@ BotCommandResult executeCommand(const BotCommand& command, const BotCommandConte + case BOT_COMMAND_HELP: + return executeHelp(command, output, output_len); + case BOT_COMMAND_CMD: +- return executeCmd(output, output_len); ++ return executeCmd(command, output, output_len); + case BOT_COMMAND_PING: + return writeText(output, output_len, "Pong!"); + case BOT_COMMAND_TEST: +@@ -642,6 +677,12 @@ BotCommandResult executeCommand(const BotCommand& command, const BotCommandConte + (unsigned long)context.packets_recv_errors, (unsigned)context.queue_depth); + case BOT_COMMAND_MAGIC8: + return executeMagic8(context, output, output_len); ++ case BOT_COMMAND_SIG: ++ return executeSig(context, output, output_len); ++ case BOT_COMMAND_AIR: ++ return executeAir(context, output, output_len); ++ case BOT_COMMAND_COIN: ++ return executeCoin(context, output, output_len); + case BOT_COMMAND_PATH: + return executePath(command, context, output, output_len); + case BOT_COMMAND_TRACE: +diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h +index a54949d9..b91b291d 100644 +--- a/examples/companion_radio/BotTypes.h ++++ b/examples/companion_radio/BotTypes.h +@@ -39,7 +39,7 @@ + #define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4 + #define BOT_KNOWN_BOT_LABEL_LEN 12 + #define BOT_PREFS_MAGIC 0x31504642UL +-#define BOT_PREFS_VERSION 6 ++#define BOT_PREFS_VERSION 7 + #define BOT_PREFS_DEFAULT_LOCAL_ADVERT_MILLIS (24UL * 60UL * 60UL * 1000UL) + #define BOT_PREFS_DEFAULT_FLOOD_ADVERT_MILLIS (24UL * 60UL * 60UL * 1000UL) + #define BOT_PREFS_INITIAL_LOCAL_ADVERT_MILLIS 60000UL +@@ -90,8 +90,11 @@ enum BotCommandId : uint8_t { + BOT_COMMAND_LORA = 19, + BOT_COMMAND_ID = 20, + BOT_COMMAND_NEIGHBORS = 21, +- BOT_COMMAND_UNSUPPORTED = 22, +- BOT_COMMAND_UNKNOWN = 23 ++ BOT_COMMAND_SIG = 22, ++ BOT_COMMAND_AIR = 23, ++ BOT_COMMAND_COIN = 24, ++ BOT_COMMAND_UNSUPPORTED = 25, ++ BOT_COMMAND_UNKNOWN = 26 + }; + + enum BotCommandVisibility : uint8_t { +@@ -143,13 +146,17 @@ struct BotCommandMetadata { + #define BOT_COMMAND_MASK_LORA (1UL << BOT_COMMAND_LORA) + #define BOT_COMMAND_MASK_ID (1UL << BOT_COMMAND_ID) + #define BOT_COMMAND_MASK_NEIGHBORS (1UL << BOT_COMMAND_NEIGHBORS) ++#define BOT_COMMAND_MASK_SIG (1UL << BOT_COMMAND_SIG) ++#define BOT_COMMAND_MASK_AIR (1UL << BOT_COMMAND_AIR) ++#define BOT_COMMAND_MASK_COIN (1UL << BOT_COMMAND_COIN) + #define BOT_COMMAND_MASK_ALL (BOT_COMMAND_MASK_HELP | BOT_COMMAND_MASK_CMD | BOT_COMMAND_MASK_PING | \ + BOT_COMMAND_MASK_TEST | BOT_COMMAND_MASK_HELLO | BOT_COMMAND_MASK_ABOUT | \ + BOT_COMMAND_MASK_ROLL | BOT_COMMAND_MASK_DICE | BOT_COMMAND_MASK_STATUS | \ + BOT_COMMAND_MASK_CHANNELS | BOT_COMMAND_MASK_VERSION | BOT_COMMAND_MASK_STATS | \ + BOT_COMMAND_MASK_MAGIC8 | BOT_COMMAND_MASK_PATH | BOT_COMMAND_MASK_TRACE | \ + BOT_COMMAND_MASK_TRACER | BOT_COMMAND_MASK_PREFIX | BOT_COMMAND_MASK_TIME | \ +- BOT_COMMAND_MASK_LORA | BOT_COMMAND_MASK_ID | BOT_COMMAND_MASK_NEIGHBORS) ++ BOT_COMMAND_MASK_LORA | BOT_COMMAND_MASK_ID | BOT_COMMAND_MASK_NEIGHBORS | \ ++ BOT_COMMAND_MASK_SIG | BOT_COMMAND_MASK_AIR | BOT_COMMAND_MASK_COIN) + + enum BotCommandResultCode : uint8_t { + BOT_COMMAND_RESULT_NOT_HANDLED = 0, +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 5e00c9d2..fc37823c 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -1142,7 +1142,8 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com + context.pending_responses = bot_stats.pending_responses; + context.emergency_forwards = bot_stats.emergency_forwards; + context.emergency_forward_failures = bot_stats.emergency_forward_failures; +- if (command_id == BOT_COMMAND_ROLL || command_id == BOT_COMMAND_DICE || command_id == BOT_COMMAND_MAGIC8) { ++ if (command_id == BOT_COMMAND_ROLL || command_id == BOT_COMMAND_DICE || command_id == BOT_COMMAND_MAGIC8 || ++ command_id == BOT_COMMAND_COIN) { + getRNG()->random((uint8_t *)&context.random_seed, sizeof(context.random_seed)); + } + if (command_id == BOT_COMMAND_STATUS || command_id == BOT_COMMAND_STATS) { +@@ -1160,7 +1161,7 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com + StrHelper::strzcpy(context.firmware_version, FIRMWARE_VERSION, sizeof(context.firmware_version)); + StrHelper::strzcpy(context.firmware_build_date, FIRMWARE_BUILD_DATE, sizeof(context.firmware_build_date)); + } +- if (command_id == BOT_COMMAND_STATS) { ++ if (command_id == BOT_COMMAND_STATS || command_id == BOT_COMMAND_SIG || command_id == BOT_COMMAND_AIR) { + context.queue_depth = (uint8_t)_mgr->getOutboundTotal(); + context.noise_floor = (int16_t)_radio->getNoiseFloor(); + context.last_rssi = (int8_t)radio_driver.getLastRSSI(); +@@ -1842,7 +1843,8 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + if (message.sender_name[0]) { + StrHelper::strzcpy(context.response_target, message.sender_name, sizeof(context.response_target)); + } +- if (command.id == BOT_COMMAND_PATH || command.id == BOT_COMMAND_TRACE || command.id == BOT_COMMAND_TRACER) { ++ if (command.id == BOT_COMMAND_PATH || command.id == BOT_COMMAND_TRACE || command.id == BOT_COMMAND_TRACER || ++ command.id == BOT_COMMAND_SIG) { + context.path_len = message.path_len; + context.path_hash_size = command.args_len > 0 ? botConfiguredTraceHashSize(_prefs.path_hash_mode) : message.path_hash_size; + context.path_hash_count = message.path_hash_count; diff --git a/scripts/list-release-companion-envs.sh b/scripts/list-release-companion-envs.sh index b5a52f9..6697150 100755 --- a/scripts/list-release-companion-envs.sh +++ b/scripts/list-release-companion-envs.sh @@ -5,18 +5,15 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" MESHCORE_DIR="${MESHCORE_DIR:-${ROOT_DIR}/vendor/MeshCore}" is_excluded() { + # BLE variants that do not fit on these boards. The RP2040 and Nibble + # envs that used to sit here build again as of upstream bbb58cce + # (v1.16.0) and were re-included after passing local test builds. case "$1" in Heltec_v2_companion_radio_ble|\ LilyGo_TLora_V2_1_1_6_companion_radio_ble|\ - nibble_screen_connect_companion_radio_ble|\ - nibble_screen_connect_companion_radio_usb|\ - PicoW_companion_radio_usb|\ - RAK_11310_companion_radio_usb|\ Station_G2_companion_radio_ble|\ Tbeam_SX1262_companion_radio_ble|\ - Tbeam_SX1276_companion_radio_ble|\ - waveshare_rp2040_lora_companion_radio_usb|\ - Xiao_rp2040_companion_radio_usb) + Tbeam_SX1276_companion_radio_ble) return 0 ;; *) @@ -25,7 +22,10 @@ is_excluded() { esac } +# The sed strips CR so variant files with CRLF line endings (a few exist +# upstream) match; without it their envs silently vanish from releases. grep -rhE '^\[env:' "${MESHCORE_DIR}/variants/" \ + | sed -e 's/\r$//' \ | sort -u \ | grep -E '_companion_radio_(usb|ble)\]$' \ | sed -e 's/\[env://' -e 's/\]//' \ diff --git a/tests/firmware_bot/test_firmware_bot.cpp b/tests/firmware_bot/test_firmware_bot.cpp index a3a66de..a47f6cf 100644 --- a/tests/firmware_bot/test_firmware_bot.cpp +++ b/tests/firmware_bot/test_firmware_bot.cpp @@ -657,31 +657,49 @@ static void test_command_outputs() { result = run_command("!help", out, sizeof(out)); assert(result.code == BOT_COMMAND_RESULT_OK); - assert_starts_with(out, "Commands: help, cmd, ping"); + assert_starts_with(out, "Commands: help cmd ping"); assert(result.text_len <= BOT_MAX_GROUP_RESPONSE_LEN); assert_contains(out, "roll"); assert_contains(out, "dice"); assert_contains(out, "trace"); assert_contains(out, "tracer"); - assert_contains(out, "prefix"); + assert_contains(out, "magic8"); + assert_contains(out, "coin"); + assert_contains(out, "cmd diag"); assert_contains(out, "help "); assert(strstr(out, "weather") == NULL); result = run_command("!cmd", out, sizeof(out)); assert(result.code == BOT_COMMAND_RESULT_OK); - assert_starts_with(out, "Commands: test, ping, help, hello, about"); - assert_contains(out, "prefix"); + assert_starts_with(out, "Commands: help cmd ping test hello about"); + assert(result.text_len <= BOT_MAX_GROUP_RESPONSE_LEN); assert_contains(out, "path"); assert_contains(out, "dice"); assert_contains(out, "roll"); - assert_contains(out, "stats"); - assert_contains(out, "time"); - assert_contains(out, "lora"); - assert_contains(out, "id"); - assert_contains(out, "neighbors"); - assert_contains(out, ", "); + assert_contains(out, "magic8"); + assert_contains(out, "coin"); + assert_contains(out, "cmd diag"); assert(strstr(out, "weather") == NULL); + result = run_command("!cmd diag", out, sizeof(out)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert_starts_with(out, "Diag: status channels stats prefix time lora id neighbors sig air"); + assert(result.text_len <= BOT_MAX_GROUP_RESPONSE_LEN); + + // Every discoverable command must appear in either the chat or the diag + // listing, so a new registry entry can never silently miss discovery. + { + char chat[BOT_MAX_RESPONSE_LEN + 1]; + char diag[BOT_MAX_RESPONSE_LEN + 1]; + assert(run_command("!cmd", chat, sizeof(chat)).code == BOT_COMMAND_RESULT_OK); + assert(run_command("!cmd diag", diag, sizeof(diag)).code == BOT_COMMAND_RESULT_OK); + for (size_t i = 0; i < BotCommandRegistry::commandCount(); i++) { + const BotCommandMetadata* meta = BotCommandRegistry::commandAt(i); + if (!meta || meta->visibility != BOT_COMMAND_VISIBILITY_DISCOVERABLE) continue; + assert(strstr(chat, meta->name) != NULL || strstr(diag, meta->name) != NULL); + } + } + result = run_command("!help roll", out, sizeof(out)); assert(result.code == BOT_COMMAND_RESULT_OK); assert_starts_with(out, "roll: Roll a random number between 1 and X"); @@ -791,6 +809,62 @@ static void test_command_outputs() { assert(strcmp(out, "Unknown command. Try help") == 0); } +static void test_sig_air_coin_commands() { + char out[BOT_MAX_RESPONSE_LEN + 1]; + BotCommand command; + + assert(FirmwareBot::parseCommand("!sig", 4, &command)); + assert(command.id == BOT_COMMAND_SIG); + BotCommandContext context = make_context(); + context.path_snr_quarters = 23; + context.last_rssi = -92; + context.noise_floor = -105; + BotCommandResult result = BotCommands::executeCommand(command, context, out, sizeof(out)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert(strcmp(out, "Sig: heard you at SNR 5.75 | last RSSI -92 dBm, noise -105 dBm") == 0); + + strncpy(context.response_target, "alice", sizeof(context.response_target) - 1); + result = BotCommands::executeCommand(command, context, out, sizeof(out)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert(strcmp(out, "Sig @[alice]: heard you at SNR 5.75 | last RSSI -92 dBm, noise -105 dBm") == 0); + context.response_target[0] = 0; + + assert(FirmwareBot::parseCommand("!snr", 4, &command)); + assert(command.id == BOT_COMMAND_SIG); + assert(FirmwareBot::parseCommand("!rssi", 5, &command)); + assert(command.id == BOT_COMMAND_SIG); + + assert(FirmwareBot::parseCommand("!air", 4, &command)); + assert(command.id == BOT_COMMAND_AIR); + context.tx_airtime_seconds = 12; + context.rx_airtime_seconds = 345; + context.flood_recv = 10; + context.direct_recv = 3; + context.flood_sent = 5; + context.direct_sent = 2; + result = BotCommands::executeCommand(command, context, out, sizeof(out)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert(strcmp(out, "Air: tx 12s rx 345s | rx flood 10 direct 3 | tx flood 5 direct 2") == 0); + + assert(FirmwareBot::parseCommand("!airtime", 8, &command)); + assert(command.id == BOT_COMMAND_AIR); + + assert(FirmwareBot::parseCommand("!coin", 5, &command)); + assert(command.id == BOT_COMMAND_COIN); + result = BotCommands::executeCommand(command, context, out, sizeof(out)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert(strcmp(out, "Coin: Heads") == 0 || strcmp(out, "Coin: Tails") == 0); + + // Same seed must flip the same way; a different seed exercises the other path. + char again[BOT_MAX_RESPONSE_LEN + 1]; + result = BotCommands::executeCommand(command, context, again, sizeof(again)); + assert(result.code == BOT_COMMAND_RESULT_OK); + assert(strcmp(out, again) == 0); + + assert(FirmwareBot::parseCommand("!flip", 5, &command)); + assert(command.id == BOT_COMMAND_COIN); +} + static void test_path_command() { char out[BOT_MAX_RESPONSE_LEN + 1]; BotCommand command; @@ -1757,6 +1831,7 @@ int main() { test_channel_text_and_response_guards(); test_ack_response_format(); test_command_outputs(); + test_sig_air_coin_commands(); test_path_command(); test_trace_result_format(); test_dice_command(); diff --git a/vendor/MeshCore b/vendor/MeshCore index 910b1be..bbb58cc 160000 --- a/vendor/MeshCore +++ b/vendor/MeshCore @@ -1 +1 @@ -Subproject commit 910b1bee5b0ccffc472c7684d4165fc85c681896 +Subproject commit bbb58cceb852a9190b46d5f6984e8e5140e6991e