From f00355c10050b1a5db177af43794b5c07e5669d7 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sun, 17 May 2026 12:11:02 -0600 Subject: [PATCH] =?UTF-8?q?forge:=20add=20patch=2014=20=E2=80=94=20stabili?= =?UTF-8?q?ze=20bot=20response=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...on-radio-firmware-bot-command-parity.patch | 2 +- ...n-firmware-bot-response-coordination.patch | 2 +- ...03-Allow-prefixless-firmware-bot-DMs.patch | 2 +- ...-bot-commands-with-upstream-behavior.patch | 2 +- ...t-coordination-utility-commands-and-.patch | 2 +- ...-speed-up-initial-advert-default-con.patch | 2 +- ...nses-with-request-token-for-inter-bo.patch | 2 +- ...t-suffix-and-auto-learn-known-bot-pa.patch | 2 +- ...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 +- ...abilize-firmware-bot-response-tokens.patch | 342 ++++++++++++++++++ tests/firmware_bot/test_firmware_bot.cpp | 37 ++ 15 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch 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 b434341..0cda401 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/13] Add companion radio firmware bot command parity +Subject: [PATCH 01/14] Add companion radio firmware bot command parity --- .../companion_radio/BotCommandRegistry.cpp | 121 ++ diff --git a/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch index 4a050f8..898564b 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/13] Harden firmware bot response coordination +Subject: [PATCH 02/14] Harden firmware bot response coordination --- examples/companion_radio/BotCommands.cpp | 2 +- diff --git a/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch b/patches/meshcore/0003-Allow-prefixless-firmware-bot-DMs.patch index 84777f8..a1936fa 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/13] Allow prefixless firmware bot DMs +Subject: [PATCH 03/14] 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 f4bdb1a..ed99bd6 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/13] Align firmware bot commands with upstream behavior +Subject: [PATCH 04/14] Align firmware bot commands with upstream behavior --- .../companion_radio/BotCommandRegistry.cpp | 25 +- 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 5c55eb6..1e2f6ad 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/13] Add hop-aware bot coordination, utility commands, and +Subject: [PATCH 05/14] Add hop-aware bot coordination, utility commands, and bounded delays Adds new utility commands (TIME, LORA, ID, NEIGHBORS), neighbor tracking, 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 c21e39d..2a06bdf 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/13] Mark bot adverts, speed up initial advert, default +Subject: [PATCH 06/14] Mark bot adverts, speed up initial advert, default contacts auto-overwrite Three coordination/discoverability fixes: 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 93f8909..a202645 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/13] Prefix bot responses with request token for inter-bot +Subject: [PATCH 07/14] 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 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 92e7157..1b9120f 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/13] Drop [MCBOT] advert suffix and auto-learn known-bot +Subject: [PATCH 08/14] Drop [MCBOT] advert suffix and auto-learn known-bot path Removes BOT_ADVERT_MARKER / writeBotAdvertName / isBotAdvertName from 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 bebf530..d4a3bed 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/13] Widen hop-step + drop "ack " prefix from test response +Subject: [PATCH 09/14] 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 05e68ac..f9ecded 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/13] Widen coordinator delays a tiny bit more +Subject: [PATCH 10/14] 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 3691435..c946d9e 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/13] Auto-advert on startup (initial flood at 5s instead of +Subject: [PATCH 11/14] 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 0f0aa61..388b18b 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/13] Spell out hops/byte in test+path; drop channel name +Subject: [PATCH 12/14] 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 bbd4ef7..a6e8d21 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/13] =?UTF-8?q?Hop=20delay=20grows=20quadratically=20?= +Subject: [PATCH 13/14] =?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-firmware-bot-response-tokens.patch b/patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch new file mode 100644 index 0000000..70cb13b --- /dev/null +++ b/patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch @@ -0,0 +1,342 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: cj-vana +Date: Sun, 17 May 2026 12:06:00 -0600 +Subject: [PATCH 14/14] Stabilize firmware bot response tokens + +--- + examples/companion_radio/FirmwareBot.cpp | 52 ++++++++++++------ + examples/companion_radio/FirmwareBot.h | 1 + + examples/companion_radio/MyMesh.cpp | 67 +++++++++++++++++++++--- + examples/companion_radio/MyMesh.h | 1 + + 4 files changed, 96 insertions(+), 25 deletions(-) + +diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp +index afd44d41..21312c55 100644 +--- a/examples/companion_radio/FirmwareBot.cpp ++++ b/examples/companion_radio/FirmwareBot.cpp +@@ -366,8 +366,17 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com + } + + BotFingerprint fingerprintFor(const BotMessage& message) { ++ return fingerprintFor(message, NULL, 0); ++} ++ ++BotFingerprint fingerprintFor(const BotMessage& message, const uint8_t* channel_id, size_t channel_id_len) { + uint64_t hash = 1469598103934665603ULL; +- hash = fnv1aUpdateChannel(hash, message); ++ if (message.channel_kind == BOT_CHANNEL_DM || !channel_id || channel_id_len == 0) { ++ hash = fnv1aUpdateChannel(hash, message); ++ } else { ++ hash = fnv1aUpdate(hash, 0xC7); ++ hash = fnv1aUpdateBytes(hash, channel_id, channel_id_len); ++ } + hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, sizeof(message.sender_key_prefix)); + hash = fnv1aUpdateTextLower(hash, message.sender_name, boundedStrLen(message.sender_name, sizeof(message.sender_name))); + hash = fnv1aUpdateU32(hash, message.sender_timestamp); +@@ -381,6 +390,24 @@ BotFingerprint fingerprintFor(const BotMessage& message) { + return fingerprint; + } + ++static int hexNibble(char c); ++ ++bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) { ++ if (token) *token = 0; ++ if (prefix_len) *prefix_len = 0; ++ if (!text || text_len < 7) return false; ++ if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false; ++ uint16_t value = 0; ++ for (int i = 0; i < 4; i++) { ++ int n = hexNibble(text[1 + i]); ++ if (n < 0) return false; ++ value = (uint16_t)((value << 4) | (uint16_t)n); ++ } ++ if (token) *token = value; ++ if (prefix_len) *prefix_len = 7; ++ return true; ++} ++ + BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) { + uint64_t hash = 1469598103934665603ULL; + hash = fnv1aUpdateChannel(hash, message); +@@ -389,6 +416,13 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res + hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len); + } + ++ uint16_t token = 0; ++ size_t prefix_len = 0; ++ if (parseRequestTokenPrefix(response_text, response_text_len, &token, &prefix_len)) { ++ response_text += prefix_len; ++ response_text_len -= prefix_len; ++ } ++ + char normalized[BOT_MAX_RESPONSE_LEN + 1]; + size_t normalized_len = 0; + normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len); +@@ -423,22 +457,6 @@ static int hexNibble(char c) { + return -1; + } + +-bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len) { +- if (token) *token = 0; +- if (prefix_len) *prefix_len = 0; +- if (!text || text_len < 7) return false; // "[XXXX] " = 7 chars minimum +- if (text[0] != '[' || text[5] != ']' || text[6] != ' ') return false; +- uint16_t value = 0; +- for (int i = 0; i < 4; i++) { +- int n = hexNibble(text[1 + i]); +- if (n < 0) return false; +- value = (uint16_t)((value << 4) | (uint16_t)n); +- } +- if (token) *token = value; +- if (prefix_len) *prefix_len = 7; +- return true; +-} +- + BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len, + size_t* new_len) { + if (new_len) *new_len = text_len; +diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h +index 708432a2..f1726712 100644 +--- a/examples/companion_radio/FirmwareBot.h ++++ b/examples/companion_radio/FirmwareBot.h +@@ -17,6 +17,7 @@ BotWriteResult writeResponseForChannel(BotChannelKind channel_kind, bool allow_p + BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& command, char* output, size_t output_len, + size_t* written); + BotFingerprint fingerprintFor(const BotMessage& message); ++BotFingerprint fingerprintFor(const BotMessage& message, const uint8_t* channel_id, size_t channel_id_len); + BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len); + uint16_t requestToken(BotFingerprint request_fingerprint); + void formatRequestToken(uint16_t token, char out[5]); +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 9121d42c..1a7ddb7a 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -121,6 +121,16 @@ + #define LAZY_CONTACTS_WRITE_DELAY 5000 + + #if CMESH_BOT_ENABLED ++#ifndef CMESH_BOT_DEBUG ++#define CMESH_BOT_DEBUG 0 ++#endif ++#if CMESH_BOT_DEBUG ++#define BOT_DEBUG_PRINTF(...) Serial.printf(__VA_ARGS__) ++#define BOT_DEBUG_PRINTLN(value) Serial.println(value) ++#else ++#define BOT_DEBUG_PRINTF(...) ++#define BOT_DEBUG_PRINTLN(value) ++#endif + #endif + + #define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" +@@ -1055,6 +1065,7 @@ bool MyMesh::handleBotCLI(const char *args) { + + void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix, + size_t sender_prefix_len, const char *text, const mesh::Packet *packet) { ++ BOT_DEBUG_PRINTF("CBOT-DBG: DM rx from=%s text=%.40s enabled=%d\r\n", from.name, text, bot_prefs.enabled); + BotMessage message; + memset(&message, 0, sizeof(message)); + message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs); +@@ -1090,6 +1101,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti + + void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text, + uint32_t sender_timestamp, const mesh::Packet *packet) { ++ BOT_DEBUG_PRINTF("CBOT-DBG: CH rx ch=%s text=%.40s enabled=%d\r\n", channel_name ? channel_name : "?", text, bot_prefs.enabled); + BotMessage message; + memset(&message, 0, sizeof(message)); + size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN); +@@ -1429,7 +1441,7 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact + return true; + } + +- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx); + BotFingerprint response_fingerprint = + FirmwareBot::responseFingerprintFor(message, final_response, final_response_len); + BotFingerprint fingerprint; +@@ -1503,7 +1515,7 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo + } + if (!have_path || !botTracePathShapeValid(path_len, flags)) return false; + +- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx); + 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]; +@@ -1737,6 +1749,16 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati + return false; + } + ++BotFingerprint MyMesh::botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx) { ++ if (message.channel_kind != BOT_CHANNEL_DM && channel_idx != 0xFF) { ++ ChannelDetails channel; ++ if (getChannel(channel_idx, channel)) { ++ return FirmwareBot::fingerprintFor(message, channel.channel.hash, sizeof(channel.channel.hash)); ++ } ++ } ++ return FirmwareBot::fingerprintFor(message); ++} ++ + bool MyMesh::observeBotGroupResponse(const BotMessage &message) { + if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false; + +@@ -1749,6 +1771,9 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { + if (FirmwareBot::parseRequestTokenPrefix(message.text, message.text_len, &token, &token_prefix_len)) { + if (ResponseCoordinator::suppressByRequestToken(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, token)) { + token_suppressed = true; ++ BOT_DEBUG_PRINTF("CBOT-DBG: token suppress %04x\r\n", (unsigned)token); ++ } else { ++ BOT_DEBUG_PRINTF("CBOT-DBG: token miss %04x\r\n", (unsigned)token); + } + } + +@@ -1763,9 +1788,12 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { + } + + void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) { ++ BOT_DEBUG_PRINTF("CBOT-DBG: record kind=%d direct=%d enabled=%d\r\n", (int)message.channel_kind, direct_recipient != NULL, bot_prefs.enabled); + bot_stats.observed_messages++; + BotPolicyDecision decision = BotPolicy::decide(message.channel_kind); ++ BOT_DEBUG_PRINTF("CBOT-DBG: policy decision=%d\r\n", (int)decision); + if (decision == BOT_POLICY_IGNORE) { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: IGNORE -> drop"); + bot_stats.ignored_messages++; + return; + } +@@ -1776,24 +1804,28 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + } + + if (!bot_prefs.enabled) { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: bot disabled -> drop"); + bot_stats.ignored_messages++; + return; + } + +- if (observeKnownBotResponse(message, direct_recipient != NULL)) return; +- if (!direct_recipient && observeBotGroupResponse(message)) return; ++ if (observeKnownBotResponse(message, direct_recipient != NULL)) { BOT_DEBUG_PRINTLN("CBOT-DBG: known-bot suppress"); return; } ++ if (!direct_recipient && observeBotGroupResponse(message)) { BOT_DEBUG_PRINTLN("CBOT-DBG: group-response suppress"); return; } + sendQueuedBotResponses(); + + BotCommand command; + if (!FirmwareBot::parseCommand(message.text, message.text_len, &command, + BotPolicy::isPrefixlessCommandAllowed(message.channel_kind))) { ++ BOT_DEBUG_PRINTF("CBOT-DBG: parseCommand FAILED text=%.40s prefixless=%d\r\n", message.text, BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)); + if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++; + return; + } ++ BOT_DEBUG_PRINTF("CBOT-DBG: parsed cmd.id=%d name=%s args=%.30s\r\n", (int)command.id, command.name, command.args); + + 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())) { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: cmd disabled or on cooldown -> drop"); + bot_stats.ignored_messages++; + return; + } +@@ -1868,7 +1900,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + return; + } + +- BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ BotFingerprint request_fingerprint = botRequestFingerprintFor(message, channel_idx); + BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len); + BotFingerprint fingerprint; + uint32_t due_at_millis = 0; +@@ -1884,7 +1916,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + queue_depth, bot_prefs.normal_delay_ms, + bot_prefs.normal_jitter_ms, bot_prefs.hop_step_ms, + &fingerprint, &due_at_millis); ++ BOT_DEBUG_PRINTF("CBOT-DBG: schedule result=%d due=%lu now=%lu reqfp=%08lx%08lx\r\n", ++ (int)schedule, (unsigned long)due_at_millis, (unsigned long)_ms->getMillis(), ++ (unsigned long)(request_fingerprint.value >> 32), (unsigned long)(request_fingerprint.value & 0xFFFFFFFFul)); + if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: schedule FAILED -> drop"); + bot_stats.send_failures++; + return; + } +@@ -1894,8 +1930,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(), + BOT_COMMAND_COOLDOWN_MILLIS); + if (!enqueueBotResponse(message, direct_recipient, channel_idx, final_response, final_response_len, fingerprint, response_fingerprint)) { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: enqueueBotResponse FAILED -> drop"); + ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint); + bot_stats.send_failures++; ++ } else { ++ BOT_DEBUG_PRINTF("CBOT-DBG: queued response, text=%.40s\r\n", final_response); + } + } + +@@ -1904,6 +1943,8 @@ 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; ++ BOT_DEBUG_PRINTF("CBOT-DBG: poll result=%d reqfp=%08lx\r\n", (int)ready.result, ++ (unsigned long)(ready.request_fingerprint.value & 0xFFFFFFFFul)); + + if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) { + bot_stats.suppressed_responses++; +@@ -1945,12 +1986,14 @@ void MyMesh::sendQueuedBotResponses() { + bool success = false; + if (pending->direct) { + ContactInfo *recipient = lookupContactByPubKey(pending->recipient_pub_key, PUB_KEY_SIZE); ++ BOT_DEBUG_PRINTF("CBOT-DBG: send DM lookup=%s text=%.40s\r\n", recipient ? "FOUND" : "NULL", pending->text); + if (recipient) { + uint32_t expected_ack = 0; + uint32_t est_timeout = 0; + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); + int result = sendMessage(*recipient, timestamp, 0, pending->text, expected_ack, est_timeout); + success = result != MSG_SEND_FAILED; ++ BOT_DEBUG_PRINTF("CBOT-DBG: sendMessage result=%d success=%d\r\n", result, success); + if (success && expected_ack) { + expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); + expected_ack_table[next_ack_idx].ack = expected_ack; +@@ -1960,9 +2003,11 @@ void MyMesh::sendQueuedBotResponses() { + } + } else if (pending->channel_idx != 0xFF) { + ChannelDetails channel; ++ bool have_ch = getChannel(pending->channel_idx, channel); + uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); +- success = getChannel(pending->channel_idx, channel) && +- sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len); ++ success = have_ch && sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len); ++ BOT_DEBUG_PRINTF("CBOT-DBG: send CH idx=%u have=%d success=%d text=%.40s\r\n", ++ (unsigned)pending->channel_idx, have_ch, success, pending->text); + } + + if (success) { +@@ -1970,6 +2015,7 @@ void MyMesh::sendQueuedBotResponses() { + ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, + pending->response_fingerprint, now); + } else { ++ BOT_DEBUG_PRINTLN("CBOT-DBG: send FAILED -> bot stays silent"); + bot_stats.send_failures++; + } + pending->active = false; +@@ -2521,9 +2567,14 @@ void MyMesh::begin(bool has_display) { + MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", + radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); + #if CMESH_BOT_ENABLED +- if (!_store->loadBotPrefs(bot_prefs)) bot_prefs.prefs_load_failures++; ++ bool loaded = _store->loadBotPrefs(bot_prefs); ++ if (!loaded) bot_prefs.prefs_load_failures++; + BotPrefsCodec::validate(bot_prefs); + applyBotPrefs(); ++ BOT_DEBUG_PRINTF("CBOT-DBG: boot: loaded=%d enabled=%d bot_ch=%s testing_ch=%s mask=0x%08lx delay=%u jitter=%u hop_step=%u\r\n", ++ loaded, bot_prefs.enabled, bot_prefs.bot_channel, bot_prefs.testing_channel, ++ (unsigned long)bot_prefs.command_mask, bot_prefs.normal_delay_ms, bot_prefs.normal_jitter_ms, ++ bot_prefs.hop_step_ms); + #endif + } + +diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h +index 46c3ff3c..cfd402df 100644 +--- a/examples/companion_radio/MyMesh.h ++++ b/examples/companion_radio/MyMesh.h +@@ -215,6 +215,7 @@ private: + void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx); + bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender); + bool observeBotGroupResponse(const BotMessage &message); ++ BotFingerprint botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx); + void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id); + bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, + const char *text, size_t text_len, BotFingerprint request_fingerprint, diff --git a/tests/firmware_bot/test_firmware_bot.cpp b/tests/firmware_bot/test_firmware_bot.cpp index db5fbb4..5c9a7a5 100644 --- a/tests/firmware_bot/test_firmware_bot.cpp +++ b/tests/firmware_bot/test_firmware_bot.cpp @@ -1138,6 +1138,26 @@ static void test_fingerprint() { assert(fa.value != FirmwareBot::fingerprintFor(b).value); } +static void test_channel_id_fingerprint() { + uint8_t channel_id_a[4] = { 0x10, 0x20, 0x30, 0x40 }; + uint8_t channel_id_b[4] = { 0x10, 0x20, 0x30, 0x41 }; + BotMessage a = make_message("#bot", "!PING"); + BotMessage b = make_message("#testing", "!ping"); + BotMessage c = make_message("#ops", "!ping"); + c.channel_kind = BOT_CHANNEL_BOT; + BotFingerprint fa = FirmwareBot::fingerprintFor(a, channel_id_a, sizeof(channel_id_a)); + BotFingerprint fb = FirmwareBot::fingerprintFor(b, channel_id_a, sizeof(channel_id_a)); + BotFingerprint fc = FirmwareBot::fingerprintFor(c, channel_id_a, sizeof(channel_id_a)); + assert(fa.value == fb.value); + assert(fa.value == fc.value); + assert(fa.value != FirmwareBot::fingerprintFor(a, channel_id_b, sizeof(channel_id_b)).value); + + BotMessage dm = a; + dm.channel_kind = BOT_CHANNEL_DM; + dm.channel_name[0] = 0; + assert(FirmwareBot::fingerprintFor(dm, channel_id_a, sizeof(channel_id_a)).value == FirmwareBot::fingerprintFor(dm).value); +} + static BotFingerprint final_response_fingerprint_for(const BotMessage& message, const char* text) { char response[BOT_MAX_RESPONSE_LEN + 1]; size_t written = 0; @@ -1171,6 +1191,21 @@ static void test_response_fingerprint() { FirmwareBot::responseFingerprintFor(other_dm, "pong!", 5).value); } +static void test_response_fingerprint_ignores_request_token() { + BotMessage message = make_message("#bot", "ping"); + BotFingerprint bare = FirmwareBot::responseFingerprintFor(message, "Pong!", 5); + assert(bare.value == FirmwareBot::responseFingerprintFor(message, "[1a2b] Pong!", 12).value); + assert(bare.value == FirmwareBot::responseFingerprintFor(message, "[1A2B] Pong!", 12).value); + assert(bare.value != FirmwareBot::responseFingerprintFor(message, "[bad] Pong!", 11).value); + + BotMessage dm = message; + dm.channel_kind = BOT_CHANNEL_DM; + dm.channel_name[0] = 0; + dm.sender_key_prefix_len = BOT_SENDER_KEY_PREFIX_LEN; + assert(FirmwareBot::responseFingerprintFor(dm, "Pong!", 5).value == + FirmwareBot::responseFingerprintFor(dm, "[1a2b] Pong!", 12).value); +} + static void test_authoritative_suppression_flow() { BotKnownBotEntry entries[1]; uint8_t known_key[BOT_SENDER_KEY_PREFIX_LEN] = { 2, 4, 6, 8, 10, 12 }; @@ -1665,7 +1700,9 @@ int main() { test_known_bot_registry(); test_known_bot_registry_ambiguous_short_prefix(); test_fingerprint(); + test_channel_id_fingerprint(); test_response_fingerprint(); + test_response_fingerprint_ignores_request_token(); test_authoritative_suppression_flow(); test_response_coordinator_schedule_poll(); test_response_coordinator_distinct_requests_same_response();