diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f32bb4d..6c6b237 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,10 +56,7 @@ jobs: - name: Enumerate companion USB+BLE environments and shard id: shards run: | - envs=$(grep -rhE '^\[env:' vendor/MeshCore/variants/ \ - | sort -u \ - | grep -E '_companion_radio_(usb|ble)\]$' \ - | sed -e 's/\[env://' -e 's/\]//') + envs=$(bash scripts/list-release-companion-envs.sh) count=$(printf '%s\n' "$envs" | wc -l | tr -d ' ') echo "env_count=$count" >> "$GITHUB_OUTPUT" echo "Found $count companion USB+BLE environments." @@ -107,10 +104,7 @@ jobs: - name: Select envs for this shard id: select run: | - envs=$(grep -rhE '^\[env:' vendor/MeshCore/variants/ \ - | sort -u \ - | grep -E '_companion_radio_(usb|ble)\]$' \ - | sed -e 's/\[env://' -e 's/\]//') + envs=$(bash scripts/list-release-companion-envs.sh) shard_envs=$(printf '%s\n' "$envs" \ | awk -v shard=${{ matrix.shard }} -v total=8 'NR%total==shard') printf 'envs<> "$GITHUB_OUTPUT" diff --git a/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch b/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch new file mode 100644 index 0000000..2871f56 --- /dev/null +++ b/patches/meshcore/0014-Stabilize-bot-diagnostics-and-duplicate-suppression.patch @@ -0,0 +1,416 @@ +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 + +--- + examples/companion_radio/BotTypes.h | 9 ++- + examples/companion_radio/FirmwareBot.cpp | 49 +++++++------ + examples/companion_radio/MyMesh.cpp | 48 +++++++++--- + .../companion_radio/ResponseCoordinator.cpp | 73 +++++++++++++++---- + .../companion_radio/ResponseCoordinator.h | 4 + + 5 files changed, 136 insertions(+), 47 deletions(-) + +diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h +index f3afa21f..a54949d9 100644 +--- a/examples/companion_radio/BotTypes.h ++++ b/examples/companion_radio/BotTypes.h +@@ -32,6 +32,8 @@ + #define BOT_RESPONSE_DELAY_JITTER_MILLIS 2200UL + #define BOT_RESPONSE_PENDING_TTL_MILLIS 95000UL + #define BOT_RESPONSE_RECENT_TTL_MILLIS 30000UL ++#define BOT_TIE_BREAK_SPREAD_MILLIS 900UL ++#define BOT_HOP_TIER_GUARD_MILLIS 600UL + #define BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL 0x01 + #define BOT_SENDER_KEY_PREFIX_LEN 6 + #define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4 +@@ -292,9 +294,12 @@ struct BotCoordinatorPending { + }; + + struct BotCoordinatorRecent { +- bool active; + BotFingerprint response_fingerprint; +- uint32_t expires_at_millis; ++ uint32_t response_expires_at_millis; ++ uint32_t request_expires_at_millis; ++ uint16_t request_token; ++ bool active; ++ bool request_active; + }; + + struct BotCoordinatorReady { +diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp +index afd44d41..d256e61a 100644 +--- a/examples/companion_radio/FirmwareBot.cpp ++++ b/examples/companion_radio/FirmwareBot.cpp +@@ -320,11 +320,7 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com + + const char* sender = message.sender_name[0] ? message.sender_name : "unknown"; + int n; +- if (message.channel_kind == BOT_CHANNEL_DM) { +- n = snprintf(output, output_len, "@[%s] direct", sender); +- } else { +- n = snprintf(output, output_len, "@[%s]", sender); +- } ++ n = snprintf(output, output_len, "@[%s]", sender); + if (n < 0) { + output[0] = 0; + return BOT_WRITE_NO_SPACE; +@@ -370,7 +366,7 @@ BotFingerprint fingerprintFor(const BotMessage& message) { + hash = fnv1aUpdateChannel(hash, message); + 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); ++ if (message.channel_kind == BOT_CHANNEL_DM) hash = fnv1aUpdateU32(hash, message.sender_timestamp); + + char normalized[BOT_MAX_TEXT_LEN + 1]; + size_t normalized_len = 0; +@@ -381,6 +377,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 +403,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 +444,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/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 9121d42c..5d333328 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -120,9 +120,6 @@ + #define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 + #define LAZY_CONTACTS_WRITE_DELAY 5000 + +-#if CMESH_BOT_ENABLED +-#endif +- + #define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" + + #if CMESH_BOT_ENABLED +@@ -1072,7 +1069,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)); + } +- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) { ++ 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(); ++ message.path_hash_count = packet->getPathHashCount(); ++ message.path = packet->path; ++ } else 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; + message.path_hash_count = from.out_path_len & 63; +@@ -1430,11 +1432,16 @@ bool MyMesh::dispatchBotTraceDirectLink(const BotMessage &message, const Contact + } + + BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ uint32_t now = _ms->getMillis(); ++ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(request_fingerprint), now)) { ++ bot_stats.suppressed_responses++; ++ return true; ++ } + BotFingerprint response_fingerprint = + FirmwareBot::responseFingerprintFor(message, final_response, final_response_len); + BotFingerprint fingerprint; + uint32_t due_at_millis = 0; +- uint32_t now = _ms->getMillis(); + 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 + if (!have_path || !botTracePathShapeValid(path_len, flags)) return false; + + BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(request_fingerprint), _ms->getMillis())) { ++ bot_stats.suppressed_responses++; ++ return true; ++ } + 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, + bot_stats.sent_messages++; + ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, response_fingerprint, + now_millis); ++ if (!pending.direct) { ++ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(pending.request_fingerprint), now_millis); ++ } + } else { + bot_stats.send_failures++; + } +@@ -1747,6 +1763,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)) { ++ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, token, _ms->getMillis()); + 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 * + if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++; + return; + } +- + 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 * + } + + BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ if (!direct_recipient && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(request_fingerprint), _ms->getMillis())) { ++ bot_stats.suppressed_responses++; ++ return; ++ } + 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() { + while (true) { + BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now); + if (ready.result == BOT_COORDINATOR_READY_NONE) return; +- + 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() { + pending->active = false; + continue; + } ++ if (!pending->direct && ResponseCoordinator::recentlyAnswered(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(ready.request_fingerprint), now)) { ++ bot_stats.suppressed_responses++; ++ pending->active = false; ++ continue; ++ } + + bool success = false; + if (pending->direct) { +@@ -1960,15 +1986,19 @@ 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); + } + + if (success) { + bot_stats.sent_messages++; + ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, + pending->response_fingerprint, now); ++ if (!pending->direct) { ++ ResponseCoordinator::recordRequestToken(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ FirmwareBot::requestToken(pending->request_fingerprint), now); ++ } + } else { + bot_stats.send_failures++; + } +diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp +index 837b3f12..888cdf49 100644 +--- a/examples/companion_radio/ResponseCoordinator.cpp ++++ b/examples/companion_radio/ResponseCoordinator.cpp +@@ -14,6 +14,10 @@ bool sameFingerprint(BotFingerprint a, BotFingerprint b) { + return a.value == b.value; + } + ++uint16_t requestToken(BotFingerprint fingerprint) { ++ return (uint16_t)(fingerprint.value & 0xFFFFu); ++} ++ + uint32_t channelDelayBias(BotChannelKind kind) { + if (kind == BOT_CHANNEL_DM) return 0; + if (kind == BOT_CHANNEL_BOT) return 200; +@@ -21,13 +25,11 @@ uint32_t channelDelayBias(BotChannelKind kind) { + return 800; + } + +-uint32_t hopDelayBias(BotChannelKind kind, uint8_t path_hash_count, uint16_t hop_step_millis) { ++uint32_t hopDelayBias(BotChannelKind kind, uint8_t path_hash_count, uint16_t hop_step_millis, uint16_t jitter_millis) { + if (kind == BOT_CHANNEL_DM) return 0; +- // Linear + quadratic growth: gap between consecutive hops widens with hop +- // count, so a far bot always has more slack than a near bot to receive +- // the near bot's reply before its own scheduled fire time. + uint32_t hop = (uint32_t)path_hash_count; +- uint32_t bias = hop * (uint32_t)hop_step_millis + hop * hop * BOT_HOP_GROW_MILLIS; ++ uint32_t tier_step = (uint32_t)hop_step_millis + (uint32_t)jitter_millis + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS; ++ uint32_t bias = hop * tier_step + hop * hop * BOT_HOP_GROW_MILLIS; + if (bias > BOT_HOP_BIAS_MAX_MILLIS) bias = BOT_HOP_BIAS_MAX_MILLIS; + return bias; + } +@@ -37,7 +39,7 @@ uint32_t tieBreakBias(BotFingerprint request_fingerprint, uint32_t bot_identity_ + mixed ^= mixed >> 16; + mixed *= 0x7feb352dUL; + mixed ^= mixed >> 15; +- return mixed % 900UL; ++ return mixed % BOT_TIE_BREAK_SPREAD_MILLIS; + } + + uint32_t queueDelayBias(uint8_t queue_depth) { +@@ -82,7 +84,7 @@ uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id, + (void)command_id; + uint32_t jitter = jitter_millis ? jitter_seed % jitter_millis : 0; + return (uint32_t)base_delay_millis + channelDelayBias(message.channel_kind) + +- hopDelayBias(message.channel_kind, message.path_hash_count, hop_step_millis) + ++ hopDelayBias(message.channel_kind, message.path_hash_count, hop_step_millis, jitter_millis) + + queueDelayBias(queue_depth) + tieBreakBias(request_fingerprint, bot_identity_seed) + jitter; + } + +@@ -163,7 +165,7 @@ bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_coun + for (size_t i = 0; i < pending_count; i++) { + if (!pending[i].active) continue; + if (pending[i].request_fingerprint.value == 0) continue; +- uint16_t token = (uint16_t)(pending[i].request_fingerprint.value & 0xFFFFu); ++ uint16_t token = requestToken(pending[i].request_fingerprint); + if (token == request_token) { + pending[i].suppressed = true; + suppressed = true; +@@ -228,12 +230,14 @@ void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp + slot = i; + break; + } +- if (slot == recent_count && (!recent[i].active || millisDue(now_millis, recent[i].expires_at_millis))) slot = i; ++ bool expired_response = !recent[i].active || recent[i].response_fingerprint.value == 0 || millisDue(now_millis, recent[i].response_expires_at_millis); ++ bool expired_request = !recent[i].active || !recent[i].request_active || millisDue(now_millis, recent[i].request_expires_at_millis); ++ if (slot == recent_count && expired_response && expired_request) slot = i; + } + if (slot == recent_count) slot = 0; + recent[slot].active = true; + recent[slot].response_fingerprint = response_fingerprint; +- recent[slot].expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS; ++ recent[slot].response_expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS; + } + + bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, +@@ -241,11 +245,52 @@ bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp + if (!recent || response_fingerprint.value == 0) return false; + for (size_t i = 0; i < recent_count; i++) { + if (!recent[i].active) continue; +- if (millisDue(now_millis, recent[i].expires_at_millis)) { +- recent[i].active = false; +- continue; ++ if (millisDue(now_millis, recent[i].response_expires_at_millis)) { ++ recent[i].response_fingerprint.value = 0; ++ } ++ if (recent[i].response_fingerprint.value != 0 && sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true; ++ if (recent[i].response_fingerprint.value == 0 && !recent[i].request_active) recent[i].active = false; ++ } ++ return false; ++} ++ ++void recordRequestToken(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token, ++ uint32_t now_millis) { ++ if (!recent || recent_count == 0) return; ++ ++ size_t slot = recent_count; ++ for (size_t i = 0; i < recent_count; i++) { ++ if (recent[i].active && millisDue(now_millis, recent[i].response_expires_at_millis)) { ++ recent[i].response_fingerprint.value = 0; ++ } ++ if (recent[i].active && recent[i].request_active && millisDue(now_millis, recent[i].request_expires_at_millis)) { ++ recent[i].request_active = false; ++ } ++ if (recent[i].active && recent[i].request_active && recent[i].request_token == request_token) { ++ slot = i; ++ break; ++ } ++ bool expired_response = !recent[i].active || recent[i].response_fingerprint.value == 0; ++ bool expired_request = !recent[i].active || !recent[i].request_active; ++ if (slot == recent_count && expired_response && expired_request) slot = i; ++ } ++ if (slot == recent_count) slot = 0; ++ recent[slot].active = true; ++ recent[slot].request_active = true; ++ recent[slot].request_token = request_token; ++ recent[slot].request_expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS; ++} ++ ++bool recentlyAnswered(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token, ++ uint32_t now_millis) { ++ if (!recent) return false; ++ for (size_t i = 0; i < recent_count; i++) { ++ if (!recent[i].active) continue; ++ if (recent[i].request_active && millisDue(now_millis, recent[i].request_expires_at_millis)) { ++ recent[i].request_active = false; + } +- if (sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true; ++ if (recent[i].request_active && recent[i].request_token == request_token) return true; ++ if (recent[i].response_fingerprint.value == 0 && !recent[i].request_active) recent[i].active = false; + } + return false; + } +diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h +index 7e053b44..c1b5ca04 100644 +--- a/examples/companion_radio/ResponseCoordinator.h ++++ b/examples/companion_radio/ResponseCoordinator.h +@@ -39,5 +39,9 @@ void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerp + uint32_t now_millis); + bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, + uint32_t now_millis); ++void recordRequestToken(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token, ++ uint32_t now_millis); ++bool recentlyAnswered(BotCoordinatorRecent recent[], size_t recent_count, uint16_t request_token, ++ uint32_t now_millis); + + } diff --git a/patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch b/patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch deleted file mode 100644 index 70cb13b..0000000 --- a/patches/meshcore/0014-Stabilize-firmware-bot-response-tokens.patch +++ /dev/null @@ -1,342 +0,0 @@ -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/patches/meshcore/0015-Report-flood-paths-in-bot-DM-diagnostics.patch b/patches/meshcore/0015-Report-flood-paths-in-bot-DM-diagnostics.patch deleted file mode 100644 index 3205fb7..0000000 --- a/patches/meshcore/0015-Report-flood-paths-in-bot-DM-diagnostics.patch +++ /dev/null @@ -1,45 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: cj-vana -Date: Sun, 17 May 2026 21:07:25 -0600 -Subject: [PATCH] Report flood paths in bot DM diagnostics - ---- - examples/companion_radio/FirmwareBot.cpp | 6 +----- - examples/companion_radio/MyMesh.cpp | 7 ++++++- - 2 files changed, 7 insertions(+), 6 deletions(-) - -diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp -index 21312c55..3b4a008e 100644 ---- a/examples/companion_radio/FirmwareBot.cpp -+++ b/examples/companion_radio/FirmwareBot.cpp -@@ -320,11 +320,7 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com - - const char* sender = message.sender_name[0] ? message.sender_name : "unknown"; - int n; -- if (message.channel_kind == BOT_CHANNEL_DM) { -- n = snprintf(output, output_len, "@[%s] direct", sender); -- } else { -- n = snprintf(output, output_len, "@[%s]", sender); -- } -+ n = snprintf(output, output_len, "@[%s]", sender); - if (n < 0) { - output[0] = 0; - return BOT_WRITE_NO_SPACE; -diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 1a7ddb7a..bfed7f98 100644 ---- a/examples/companion_radio/MyMesh.cpp -+++ b/examples/companion_radio/MyMesh.cpp -@@ -1083,7 +1083,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)); - } -- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) { -+ 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(); -+ message.path_hash_count = packet->getPathHashCount(); -+ message.path = packet->path; -+ } else 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; - message.path_hash_count = from.out_path_len & 63; diff --git a/patches/meshcore/0016-Stabilize-group-bot-request-tokens.patch b/patches/meshcore/0016-Stabilize-group-bot-request-tokens.patch deleted file mode 100644 index 0b357ad..0000000 --- a/patches/meshcore/0016-Stabilize-group-bot-request-tokens.patch +++ /dev/null @@ -1,331 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: cj-vana -Date: Sun, 17 May 2026 21:55:45 -0600 -Subject: [PATCH] Stabilize group bot request tokens - ---- - examples/companion_radio/FirmwareBot.cpp | 51 ++++++++++-------- - examples/companion_radio/MyMesh.cpp | 69 ++++++++++++++++++++---- - 2 files changed, 89 insertions(+), 31 deletions(-) - -diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp -index e228b7c8..3d3ad96f 100644 ---- a/examples/companion_radio/FirmwareBot.cpp -+++ b/examples/companion_radio/FirmwareBot.cpp -@@ -319,10 +319,8 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com - if (!output || output_len == 0) return BOT_WRITE_NO_SPACE; - - const char* sender = message.sender_name[0] ? message.sender_name : "unknown"; -- const char* connection = message.channel_kind == BOT_CHANNEL_DM -- ? "direct" -- : (message.channel_name[0] ? message.channel_name : "channel"); -- int n = snprintf(output, output_len, "@[%s] %s", sender, connection); -+ int n; -+ n = snprintf(output, output_len, "@[%s]", sender); - if (n < 0) { - output[0] = 0; - return BOT_WRITE_NO_SPACE; -@@ -337,9 +335,9 @@ BotWriteResult writeAckResponse(const BotMessage& message, const BotCommand& com - char path_block[40]; - int path_n; - if (message.path_hash_count == 0 || message.path_hash_size == 0) { -- path_n = snprintf(path_block, sizeof(path_block), " | 0h SNR %s%d.%02d", sign, value / 4, (value % 4) * 25); -+ path_n = snprintf(path_block, sizeof(path_block), " | 0 hops, SNR %s%d.%02d", sign, value / 4, (value % 4) * 25); - } else { -- path_n = snprintf(path_block, sizeof(path_block), " | %uh@%uB SNR %s%d.%02d", -+ path_n = snprintf(path_block, sizeof(path_block), " | %u hops, %u-byte hashes, SNR %s%d.%02d", - (unsigned)message.path_hash_count, (unsigned)message.path_hash_size, sign, value / 4, - (value % 4) * 25); - } -@@ -379,6 +377,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); -@@ -387,6 +403,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); -@@ -421,22 +444,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/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp -index 9121d42c..9aad730d 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); -@@ -1072,7 +1083,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)); - } -- if (from.out_path_len != OUT_PATH_UNKNOWN && mesh::Packet::isValidPathLen(from.out_path_len)) { -+ 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(); -+ message.path_hash_count = packet->getPathHashCount(); -+ message.path = packet->path; -+ } else 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; - message.path_hash_count = from.out_path_len & 63; -@@ -1090,6 +1106,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 +1446,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 +1520,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 +1754,11 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati - return false; - } - -+BotFingerprint MyMesh::botRequestFingerprintFor(const BotMessage &message, uint8_t channel_idx) { -+ (void)channel_idx; -+ 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/scripts/list-release-companion-envs.sh b/scripts/list-release-companion-envs.sh new file mode 100755 index 0000000..ee5f4b9 --- /dev/null +++ b/scripts/list-release-companion-envs.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MESHCORE_DIR="${MESHCORE_DIR:-${ROOT_DIR}/vendor/MeshCore}" + +is_excluded() { + case "$1" in + Heltec_v2_companion_radio_ble|\ + LilyGo_TLora_V2_1_1_6_companion_radio_ble|\ + Station_G2_companion_radio_ble|\ + Tbeam_SX1262_companion_radio_ble|\ + Tbeam_SX1276_companion_radio_ble) + return 0 + ;; + *) + return 1 + ;; + esac +} + +grep -rhE '^\[env:' "${MESHCORE_DIR}/variants/" \ + | sort -u \ + | grep -E '_companion_radio_(usb|ble)\]$' \ + | sed -e 's/\[env://' -e 's/\]//' \ + | while IFS= read -r env; do + if ! is_excluded "$env"; then + printf '%s\n' "$env" + fi + done diff --git a/tests/firmware_bot/test_firmware_bot.cpp b/tests/firmware_bot/test_firmware_bot.cpp index 9a21a8b..a3a66de 100644 --- a/tests/firmware_bot/test_firmware_bot.cpp +++ b/tests/firmware_bot/test_firmware_bot.cpp @@ -1136,6 +1136,17 @@ static void test_fingerprint() { assert(fa.value == fb.value); assert(fa.value != fc.value); + BotMessage shifted = a; + shifted.sender_timestamp++; + assert(fa.value == FirmwareBot::fingerprintFor(shifted).value); + + BotMessage dm = a; + dm.channel_kind = BOT_CHANNEL_DM; + dm.channel_name[0] = 0; + BotMessage later_dm = dm; + later_dm.sender_timestamp++; + assert(FirmwareBot::fingerprintFor(dm).value != FirmwareBot::fingerprintFor(later_dm).value); + b.sender_key_prefix[0] = 99; assert(fa.value != FirmwareBot::fingerprintFor(b).value); b.sender_key_prefix[0] = a.sender_key_prefix[0]; @@ -1155,6 +1166,7 @@ static void test_group_request_token_uses_stable_channel_name() { c.channel_name[3] = 'T'; c.channel_name[4] = 0; c.path_hash_count = 1; + b.sender_timestamp += 99; BotFingerprint fa = FirmwareBot::fingerprintFor(a); BotFingerprint fb = FirmwareBot::fingerprintFor(b); @@ -1290,7 +1302,8 @@ static void test_response_coordinator_distinct_requests_same_response() { ResponseCoordinator::clear(pending, 2); BotMessage first = make_message("#bot", "!ping"); BotMessage second = first; - second.sender_timestamp++; + strncpy(second.text, "!status", sizeof(second.text) - 1); + second.text_len = strlen(second.text); BotFingerprint first_request = FirmwareBot::fingerprintFor(first); BotFingerprint second_request = FirmwareBot::fingerprintFor(second); BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); @@ -1303,7 +1316,7 @@ static void test_response_coordinator_distinct_requests_same_response() { assert(response.value == FirmwareBot::responseFingerprintFor(second, "Pong!", 5).value); assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0, identity_seed, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED); - assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0, + assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0, identity_seed, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED); assert(scheduled.value == second_request.value); @@ -1323,7 +1336,8 @@ static void test_response_coordinator_suppression_uses_response_fingerprint() { ResponseCoordinator::clear(pending, 2); BotMessage first = make_message("#bot", "!ping"); BotMessage second = first; - second.sender_timestamp++; + strncpy(second.text, "!status", sizeof(second.text) - 1); + second.text_len = strlen(second.text); BotFingerprint first_request = FirmwareBot::fingerprintFor(first); BotFingerprint second_request = FirmwareBot::fingerprintFor(second); BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); @@ -1332,7 +1346,7 @@ static void test_response_coordinator_suppression_uses_response_fingerprint() { assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0, 0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); - assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0, + assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0, 0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); assert(ResponseCoordinator::suppress(pending, 2, response)); BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000); @@ -1363,7 +1377,8 @@ static void test_response_coordinator_distinct_path_outputs() { ResponseCoordinator::clear(pending, 2); BotMessage first = make_message("#bot", "path"); BotMessage second = first; - second.sender_timestamp++; + strncpy(second.text, "status", sizeof(second.text) - 1); + second.text_len = strlen(second.text); uint8_t path_a[] = { 0x12, 0x34, 0x56, 0x78 }; uint8_t path_b[] = { 0xab, 0xcd, 0xef, 0x01 }; first.path = path_a; @@ -1505,7 +1520,8 @@ static void test_response_coordinator_cancel_by_request() { ResponseCoordinator::clear(pending, 2); BotMessage first = make_message("#bot", "!ping"); BotMessage second = first; - second.sender_timestamp++; + strncpy(second.text, "!status", sizeof(second.text) - 1); + second.text_len = strlen(second.text); BotFingerprint first_request = FirmwareBot::fingerprintFor(first); BotFingerprint second_request = FirmwareBot::fingerprintFor(second); BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); @@ -1515,7 +1531,7 @@ static void test_response_coordinator_cancel_by_request() { assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PING, first_request, response, 1000, 0, 0x01020304UL, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED); - assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PING, second_request, response, 1000, 0, + assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_STATUS, second_request, response, 1000, 0, 0x01020304UL, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED); assert(ResponseCoordinator::cancel(pending, 2, first_request)); assert(!ResponseCoordinator::cancel(pending, 2, first_request)); @@ -1580,12 +1596,12 @@ static void test_response_coordinator_hop_count_ranking() { base_delay, jitter, hop_step); uint32_t d2 = ResponseCoordinator::responseDelayMillis(two_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0, base_delay, jitter, hop_step); - // Linear + quadratic growth: bias(h) = h*step + h*h*grow - assert(d1 == d0 + (uint32_t)hop_step + BOT_HOP_GROW_MILLIS); - assert(d2 == d0 + (uint32_t)hop_step * 2 + 4 * BOT_HOP_GROW_MILLIS); - // Gap between hops widens as hop count rises: + uint32_t tier_step = (uint32_t)hop_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS; + assert(d1 == d0 + tier_step + BOT_HOP_GROW_MILLIS); + assert(d2 == d0 + tier_step * 2 + 4 * BOT_HOP_GROW_MILLIS); uint32_t gap_0_to_1 = d1 - d0; uint32_t gap_1_to_2 = d2 - d1; + assert(gap_0_to_1 > jitter + BOT_TIE_BREAK_SPREAD_MILLIS); assert(gap_1_to_2 > gap_0_to_1); uint32_t alt_step = 2000; @@ -1593,8 +1609,9 @@ static void test_response_coordinator_hop_count_ranking() { base_delay, jitter, alt_step); uint32_t d1_alt = ResponseCoordinator::responseDelayMillis(one_hop, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0, base_delay, jitter, alt_step); + uint32_t alt_tier_step = alt_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS; assert(d0_alt == d0); - assert(d1_alt == d0 + alt_step + BOT_HOP_GROW_MILLIS); + assert(d1_alt == d0 + alt_tier_step + BOT_HOP_GROW_MILLIS); BotMessage dm = make_message("dm", "!ping"); dm.channel_kind = BOT_CHANNEL_DM; @@ -1623,7 +1640,8 @@ static void test_response_coordinator_hop_bias_cap() { base_delay, jitter, hop_step); assert(dmany == d0 + BOT_HOP_BIAS_MAX_MILLIS); uint32_t hop_u32 = (uint32_t)many_hops.path_hash_count; - assert(hop_u32 * (uint32_t)hop_step + hop_u32 * hop_u32 * BOT_HOP_GROW_MILLIS > BOT_HOP_BIAS_MAX_MILLIS); + uint32_t tier_step = (uint32_t)hop_step + jitter + BOT_TIE_BREAK_SPREAD_MILLIS + BOT_HOP_TIER_GUARD_MILLIS; + assert(hop_u32 * tier_step + hop_u32 * hop_u32 * BOT_HOP_GROW_MILLIS > BOT_HOP_BIAS_MAX_MILLIS); assert(dmany + BOT_RESPONSE_DELAY_JITTER_MILLIS < BOT_RESPONSE_PENDING_TTL_MILLIS); } @@ -1640,13 +1658,41 @@ static void test_response_coordinator_same_hop_tiebreak() { BOT_RESPONSE_DELAY_JITTER_MILLIS, hop_step); assert(bot_a != bot_b); uint32_t spread = bot_a > bot_b ? bot_a - bot_b : bot_b - bot_a; - assert(spread < 900); + assert(spread < BOT_TIE_BREAK_SPREAD_MILLIS); +} + +static void test_response_coordinator_request_token_suppression() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage first = make_message("#bot", "path"); + BotMessage second = first; + strncpy(second.text, "test", sizeof(second.text) - 1); + second.text_len = strlen(second.text); + BotFingerprint first_request = FirmwareBot::fingerprintFor(first); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second); + BotFingerprint first_response = FirmwareBot::responseFingerprintFor(first, "Path 1 hop", 10); + BotFingerprint second_response = FirmwareBot::responseFingerprintFor(second, "@[alice] | 1 hop", 16); + BotFingerprint scheduled; + uint32_t due = 0; + + assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PATH, first_request, first_response, 1000, 0, + 0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_TEST, second_request, second_response, 1000, 0, + 0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::suppressByRequestToken(pending, 2, FirmwareBot::requestToken(first_request))); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.request_fingerprint.value == first_request.value); + ready = ResponseCoordinator::poll(pending, 2, due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == second_request.value); } static void test_response_coordinator_recent_responses() { BotCoordinatorRecent recent[1]; ResponseCoordinator::clearRecent(recent, 1); BotFingerprint response = { 0xFEDCBA9876543210ULL }; + BotFingerprint other_response = { 0x0123456789ABCDEFULL }; BotFingerprint zero = { 0 }; assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000)); @@ -1655,7 +1701,19 @@ static void test_response_coordinator_recent_responses() { ResponseCoordinator::recordRecent(recent, 1, response, 1000); assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000)); assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS - 1)); + + ResponseCoordinator::recordRequestToken(recent, 1, 0x1234, 1000 + 10); + assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + 10)); + assert(ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS - 1)); assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS)); + assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS)); + assert(!ResponseCoordinator::recentlyAnswered(recent, 1, 0x1234, 1000 + 10 + BOT_RESPONSE_RECENT_TTL_MILLIS)); + + ResponseCoordinator::recordRequestToken(recent, 1, 0x5678, 2000); + assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x5678, 2000)); + ResponseCoordinator::recordRecent(recent, 1, other_response, 2000 + 1); + assert(ResponseCoordinator::recentlyAnswered(recent, 1, 0x5678, 2000 + 1)); + assert(ResponseCoordinator::recentlySent(recent, 1, other_response, 2000 + 1)); } static void test_response_coordinator_rejects_non_normal() { @@ -1724,6 +1782,7 @@ int main() { test_response_coordinator_schedule_preserves_suppression(); test_response_coordinator_group_observed_response_suppresses(); test_response_coordinator_group_guarded_path_output_suppresses(); + test_response_coordinator_request_token_suppression(); test_response_coordinator_suppress_expire_full(); test_response_coordinator_cancel_by_request(); test_response_coordinator_delay_biases();