From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana Date: Sat, 16 May 2026 19:33:04 -0600 Subject: [PATCH 07/15] Prefix bot responses with request token for inter-bot suppression Adds a 4-hex request token (low 16 bits of the request fingerprint) as a leading "[XXXX] " prefix on every bot response. Two bots receiving the same channel request compute the same fingerprint -> same token, so a peer bot can correlate an observed response back to its own pending response even when the response text differs (per-bot data like hop count, SNR, recv time, etc.). New FirmwareBot helpers: - requestToken(BotFingerprint) -> uint16_t - formatRequestToken(uint16_t, char[5]) - parseRequestTokenPrefix(text, len, *token, *prefix_len) - prependRequestToken(request_fingerprint, text, text_len, buf_len, *new_len) New ResponseCoordinator function: - suppressByRequestToken(pending, count, token16) suppresses entries where (request_fingerprint.value & 0xFFFF) == token16 Wiring: - MyMesh::enqueueBotResponse prepends the token to every queued response - MyMesh::sendBotTraceText prepends the token to async trace results - MyMesh::observeBotGroupResponse parses the token from incoming peer responses and suppresses matching pending responses, in addition to the existing response-fingerprint match (which still handles deterministic commands like ping->Pong where both bots produce identical text) Fixes inter-bot duplication on #bot for ack/test/hello/status/roll/dice/ path/trace/tracer/stats/time/lora/id/neighbors observed on the Colorado mesh when two bots both queued responses to the same command. --- examples/companion_radio/FirmwareBot.cpp | 65 +++++++++++++++++++ examples/companion_radio/FirmwareBot.h | 5 ++ examples/companion_radio/MyMesh.cpp | 40 +++++++++--- .../companion_radio/ResponseCoordinator.cpp | 15 +++++ .../companion_radio/ResponseCoordinator.h | 1 + 5 files changed, 118 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp index 6aac8a1b..24c79e81 100644 --- a/examples/companion_radio/FirmwareBot.cpp +++ b/examples/companion_radio/FirmwareBot.cpp @@ -422,4 +422,69 @@ BotFingerprint responseFingerprintFor(const BotMessage& message, const char* res return fingerprint; } +uint16_t requestToken(BotFingerprint request_fingerprint) { + // Take the low 16 bits of the request fingerprint. Two bots computing + // fingerprintFor() against the same request derive the same token, so + // a bot can recognise another bot's response to a request it also queued + // even when the response text differs (different hop count, SNR, recv time). + return (uint16_t)(request_fingerprint.value & 0xFFFFu); +} + +void formatRequestToken(uint16_t token, char out[5]) { + if (!out) return; + static const char hex[] = "0123456789abcdef"; + out[0] = hex[(token >> 12) & 0xF]; + out[1] = hex[(token >> 8) & 0xF]; + out[2] = hex[(token >> 4) & 0xF]; + out[3] = hex[token & 0xF]; + out[4] = 0; +} + +static int hexNibble(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + (c - 'a'); + if (c >= 'A' && c <= 'F') return 10 + (c - 'A'); + 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; + if (!text || buf_len == 0) return BOT_WRITE_NO_SPACE; + if (text_len + 7 + 1 > buf_len) { + // No room for "[XXXX] " prefix + null terminator without dropping content. + return BOT_WRITE_NO_SPACE; + } + char hex[5]; + formatRequestToken(requestToken(request_fingerprint), hex); + memmove(text + 7, text, text_len); + text[0] = '['; + text[1] = hex[0]; + text[2] = hex[1]; + text[3] = hex[2]; + text[4] = hex[3]; + text[5] = ']'; + text[6] = ' '; + size_t total = text_len + 7; + if (total < buf_len) text[total] = 0; + if (new_len) *new_len = total; + return BOT_WRITE_OK; +} + } diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h index 9eff340b..6e5cfd9a 100644 --- a/examples/companion_radio/FirmwareBot.h +++ b/examples/companion_radio/FirmwareBot.h @@ -20,6 +20,11 @@ BotWriteResult writeBotAdvertName(const char* node_name, char* output, size_t ou bool isBotAdvertName(const char* name, size_t name_len); BotFingerprint fingerprintFor(const BotMessage& message); 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]); +bool parseRequestTokenPrefix(const char* text, size_t text_len, uint16_t* token, size_t* prefix_len); +BotWriteResult prependRequestToken(BotFingerprint request_fingerprint, char* text, size_t text_len, size_t buf_len, + size_t* new_len); BotCommandId commandIdForName(const char* name, size_t len); size_t maxResponseLenForChannel(BotChannelKind channel_kind); bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id, diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b2c47df4..d4bca6ce 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1218,6 +1218,11 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di if (!botFormatResponseForChannel(message, text, text_len, pending->text, sizeof(pending->text), &pending->text_len)) { return false; } + // Prefix every bot response with the request token so peer bots can correlate + // their pending response to ours (their response text may differ from ours when + // the command includes per-bot data like hop count, SNR, recv time, etc.). + FirmwareBot::prependRequestToken(request_fingerprint, pending->text, pending->text_len, sizeof(pending->text), + &pending->text_len); pending->active = true; return true; } @@ -1622,6 +1627,17 @@ BotFingerprint MyMesh::traceResponseFingerprintFor(const PendingBotTrace &pendin bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, BotFingerprint response_fingerprint, uint32_t now_millis) { + // Prefix with the request token so peer bots can correlate this trace result + // back to their own pending trace for the same request and suppress duplicates. + char tokened[BOT_MAX_RESPONSE_LEN + 1]; + size_t tokened_len = text_len; + if (tokened_len > BOT_MAX_RESPONSE_LEN) tokened_len = BOT_MAX_RESPONSE_LEN; + if (tokened_len > 0) memcpy(tokened, text, tokened_len); + tokened[tokened_len] = 0; + FirmwareBot::prependRequestToken(pending.request_fingerprint, tokened, tokened_len, sizeof(tokened), &tokened_len); + const char *send_text = tokened; + size_t send_len = tokened_len; + bool success = false; if (pending.direct) { ContactInfo *recipient = lookupContactByPubKey(pending.recipient_pub_key, PUB_KEY_SIZE); @@ -1629,12 +1645,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, uint32_t expected_ack = 0; uint32_t est_timeout = 0; uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); - char response[BOT_MAX_RESPONSE_LEN + 1]; - size_t copy_len = text_len; - if (copy_len > BOT_MAX_RESPONSE_LEN) copy_len = BOT_MAX_RESPONSE_LEN; - if (copy_len > 0) memcpy(response, text, copy_len); - response[copy_len] = 0; - int result = sendMessage(*recipient, timestamp, 0, response, expected_ack, est_timeout); + int result = sendMessage(*recipient, timestamp, 0, send_text, expected_ack, est_timeout); success = result != MSG_SEND_FAILED; if (success && expected_ack) { expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); @@ -1647,7 +1658,7 @@ bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, ChannelDetails channel; uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); success = getChannel(pending.channel_idx, channel) && - sendGroupMessage(timestamp, channel.channel, _prefs.node_name, text, text_len); + sendGroupMessage(timestamp, channel.channel, _prefs.node_name, send_text, send_len); } if (success) { @@ -1753,6 +1764,19 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati bool MyMesh::observeBotGroupResponse(const BotMessage &message) { if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false; + + // If the incoming channel message carries a request token from a peer bot, + // suppress our own pending response for that same request even when the + // response text differs (hop count, SNR, recv time, etc.). + uint16_t token = 0; + size_t token_prefix_len = 0; + bool token_suppressed = false; + 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; + } + } + BotFingerprint fingerprint = FirmwareBot::responseFingerprintFor(message, message.text, message.text_len); if (ResponseCoordinator::recentlySent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, fingerprint, _ms->getMillis())) { return true; @@ -1760,7 +1784,7 @@ bool MyMesh::observeBotGroupResponse(const BotMessage &message) { if (ResponseCoordinator::suppress(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint)) { return true; } - return false; + return token_suppressed; } void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) { diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp index 7afdab24..3640f8b6 100644 --- a/examples/companion_radio/ResponseCoordinator.cpp +++ b/examples/companion_radio/ResponseCoordinator.cpp @@ -153,6 +153,21 @@ bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerpr return suppressed; } +bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_count, uint16_t request_token) { + if (!pending) return false; + bool suppressed = false; + 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); + if (token == request_token) { + pending[i].suppressed = true; + suppressed = true; + } + } + return suppressed; +} + bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint) { if (!pending || request_fingerprint.value == 0) return false; for (size_t i = 0; i < pending_count; i++) { diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h index f549b887..7e053b44 100644 --- a/examples/companion_radio/ResponseCoordinator.h +++ b/examples/companion_radio/ResponseCoordinator.h @@ -32,6 +32,7 @@ BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pe uint8_t queue_depth, uint16_t base_delay_millis, uint16_t jitter_millis, uint16_t hop_step_millis, BotFingerprint* fingerprint, uint32_t* due_at_millis); bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint); +bool suppressByRequestToken(BotCoordinatorPending pending[], size_t pending_count, uint16_t request_token); bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint); BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis); void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,