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 7f557cd..a56783e 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] Add companion radio firmware bot command parity +Subject: [PATCH 1/2] 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 new file mode 100644 index 0000000..39fe17f --- /dev/null +++ b/patches/meshcore/0002-Harden-firmware-bot-response-coordination.patch @@ -0,0 +1,458 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: cj-vana +Date: Fri, 15 May 2026 19:58:42 -0600 +Subject: [PATCH 2/2] Harden firmware bot response coordination + +--- + examples/companion_radio/BotCommands.cpp | 2 +- + examples/companion_radio/BotTypes.h | 4 + + examples/companion_radio/FirmwareBot.cpp | 66 +++++++++ + examples/companion_radio/FirmwareBot.h | 6 + + examples/companion_radio/MyMesh.cpp | 129 ++++++++++++++---- + examples/companion_radio/MyMesh.h | 7 + + .../companion_radio/ResponseCoordinator.cpp | 5 +- + 7 files changed, 193 insertions(+), 26 deletions(-) + +diff --git a/examples/companion_radio/BotCommands.cpp b/examples/companion_radio/BotCommands.cpp +index a7d3637f..add7feed 100644 +--- a/examples/companion_radio/BotCommands.cpp ++++ b/examples/companion_radio/BotCommands.cpp +@@ -180,7 +180,7 @@ BotCommandResult executeHelp(const BotCommand& command, char* output, size_t out + if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0); + output[0] = 0; + size_t pos = 0; +- appendText(output, output_len, &pos, "Commands: "); ++ appendText(output, output_len, &pos, "Cmds: "); + bool first = true; + for (size_t i = 0; i < BotCommandRegistry::commandCount(); i++) { + const BotCommandMetadata* metadata = BotCommandRegistry::commandAt(i); +diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h +index 5c7430a9..4d48c13f 100644 +--- a/examples/companion_radio/BotTypes.h ++++ b/examples/companion_radio/BotTypes.h +@@ -14,11 +14,15 @@ + #define BOT_MAX_PATH_BYTES 64 + #define BOT_GROUP_RESPONSE_PREFIX_RESERVE (BOT_MAX_SENDER_NAME_LEN + 2) + #define BOT_MAX_GROUP_RESPONSE_LEN (BOT_MAX_TEXT_LEN - BOT_GROUP_RESPONSE_PREFIX_RESERVE) ++#define BOT_GROUP_RESPONSE_GUARD_PREFIX "# " ++#define BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN 2 + #define BOT_COMMAND_COOLDOWN_MILLIS 5000UL + #define BOT_TRACE_COOLDOWN_MILLIS 60000UL + #define BOT_TRACE_TIMEOUT_MILLIS 30000UL + #define BOT_PENDING_TRACE_SLOTS 2 + #define BOT_EMERGENCY_PREFIX "EMERGENCY MESSAGE FROM " ++#define BOT_ADVERT_MARKER " [MCBOT]" ++#define BOT_ADVERT_MARKER_LEN 8 + #define BOT_EMERGENCY_MAX_PARTS 3 + #define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS + #define BOT_COORDINATOR_PENDING_SLOTS 8 +diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp +index d1b240bb..1eed310a 100644 +--- a/examples/companion_radio/FirmwareBot.cpp ++++ b/examples/companion_radio/FirmwareBot.cpp +@@ -218,6 +218,19 @@ bool splitChannelText(const char* text, size_t text_len, char* sender, size_t se + return false; + } + ++BotWriteResult normalizeChannelText(const char* text, char* sender, size_t sender_len, char* output, size_t output_len, ++ size_t* written) { ++ if (sender && sender_len > 0) sender[0] = 0; ++ const char* body = text; ++ size_t raw_len = boundedStrLen(text, BOT_MAX_TEXT_LEN + BOT_MAX_SENDER_NAME_LEN + 3); ++ size_t body_len = raw_len; ++ splitChannelText(text, body_len, sender, sender_len, &body, &body_len); ++ bool truncated = raw_len > BOT_MAX_TEXT_LEN || body_len > BOT_MAX_TEXT_LEN; ++ if (body_len > BOT_MAX_TEXT_LEN) body_len = BOT_MAX_TEXT_LEN; ++ BotWriteResult result = normalizeText(body, body_len, output, output_len, written); ++ return truncated ? BOT_WRITE_TRUNCATED : result; ++} ++ + BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written) { + if (written) *written = 0; + if (!output || output_len == 0) return BOT_WRITE_NO_SPACE; +@@ -236,6 +249,59 @@ BotWriteResult writeResponse(char* output, size_t output_len, const char* text, + return copy_len < text_len ? BOT_WRITE_TRUNCATED : BOT_WRITE_OK; + } + ++BotWriteResult writeResponseForChannel(BotChannelKind channel_kind, bool allow_prefixless, const char* text, ++ size_t text_len, char* output, size_t output_len, size_t* written) { ++ BotCommand command; ++ bool needs_guard = channel_kind != BOT_CHANNEL_DM && allow_prefixless && ++ parseCommand(text, text_len, &command, true); ++ size_t max_len = maxResponseLenForChannel(channel_kind); ++ if (max_len + 1 < output_len) output_len = max_len + 1; ++ if (!needs_guard) return writeResponse(output, output_len, text, text_len, written); ++ ++ if (!output || output_len == 0) { ++ if (written) *written = 0; ++ return BOT_WRITE_NO_SPACE; ++ } ++ if (output_len <= BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN) { ++ output[0] = 0; ++ if (written) *written = 0; ++ return BOT_WRITE_NO_SPACE; ++ } ++ memcpy(output, BOT_GROUP_RESPONSE_GUARD_PREFIX, BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN); ++ size_t body_written = 0; ++ BotWriteResult result = writeResponse(&output[BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN], ++ output_len - BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN, text, text_len, ++ &body_written); ++ if (written) *written = BOT_GROUP_RESPONSE_GUARD_PREFIX_LEN + body_written; ++ return result; ++} ++ ++BotWriteResult writeBotAdvertName(const char* node_name, char* output, size_t output_len, size_t* written) { ++ if (written) *written = 0; ++ if (!output || output_len == 0) return BOT_WRITE_NO_SPACE; ++ if (output_len <= BOT_ADVERT_MARKER_LEN) { ++ output[0] = 0; ++ return BOT_WRITE_NO_SPACE; ++ } ++ ++ size_t base_len = boundedStrLen(node_name, output_len - 1); ++ bool truncated = false; ++ if (base_len + BOT_ADVERT_MARKER_LEN + 1 > output_len) { ++ base_len = output_len - BOT_ADVERT_MARKER_LEN - 1; ++ truncated = true; ++ } ++ if (base_len > 0) memcpy(output, node_name, base_len); ++ memcpy(&output[base_len], BOT_ADVERT_MARKER, BOT_ADVERT_MARKER_LEN); ++ output[base_len + BOT_ADVERT_MARKER_LEN] = 0; ++ if (written) *written = base_len + BOT_ADVERT_MARKER_LEN; ++ return truncated ? BOT_WRITE_TRUNCATED : BOT_WRITE_OK; ++} ++ ++bool isBotAdvertName(const char* name, size_t name_len) { ++ if (!name || name_len < BOT_ADVERT_MARKER_LEN) return false; ++ return memcmp(&name[name_len - BOT_ADVERT_MARKER_LEN], BOT_ADVERT_MARKER, BOT_ADVERT_MARKER_LEN) == 0; ++} ++ + BotFingerprint fingerprintFor(const BotMessage& message) { + uint64_t hash = 1469598103934665603ULL; + hash = fnv1aUpdateChannel(hash, message); +diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h +index e248adc1..38d4416d 100644 +--- a/examples/companion_radio/FirmwareBot.h ++++ b/examples/companion_radio/FirmwareBot.h +@@ -5,11 +5,17 @@ + namespace FirmwareBot { + + BotWriteResult normalizeText(const char* input, size_t input_len, char* output, size_t output_len, size_t* written); ++BotWriteResult normalizeChannelText(const char* text, char* sender, size_t sender_len, char* output, size_t output_len, ++ size_t* written); + bool parseCommand(const char* text, size_t text_len, BotCommand* command); + bool parseCommand(const char* text, size_t text_len, BotCommand* command, bool allow_prefixless); + bool splitChannelText(const char* text, size_t text_len, char* sender, size_t sender_len, const char** body, + size_t* body_len); + BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written); ++BotWriteResult writeResponseForChannel(BotChannelKind channel_kind, bool allow_prefixless, const char* text, ++ size_t text_len, char* output, size_t output_len, size_t* written); ++BotWriteResult writeBotAdvertName(const char* node_name, char* output, size_t output_len, size_t* written); ++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); + BotCommandId commandIdForName(const char* name, size_t len); +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index 44fcd1ca..2ee5139b 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -139,6 +139,18 @@ static void botCopyString(char *dest, size_t dest_len, const char *src) { + dest[len] = 0; + } + ++static bool botFormatResponseForChannelKind(BotChannelKind channel_kind, const char *text, size_t text_len, char *output, ++ size_t output_len, size_t *written) { ++ BotWriteResult result = FirmwareBot::writeResponseForChannel( ++ channel_kind, BotPolicy::isPrefixlessCommandAllowed(channel_kind), text, text_len, output, output_len, written); ++ return result != BOT_WRITE_NO_SPACE && written && *written > 0; ++} ++ ++static bool botFormatResponseForChannel(const BotMessage &message, const char *text, size_t text_len, char *output, ++ size_t output_len, size_t *written) { ++ return botFormatResponseForChannelKind(message.channel_kind, text, text_len, output, output_len, written); ++} ++ + static bool botParseU32(const char *text, uint32_t *value, const char **end_out) { + if (!text || !value || !isdigit((unsigned char)text[0])) return false; + uint32_t parsed = 0; +@@ -598,6 +610,10 @@ void MyMesh::onContactsFull() { + } + + void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { ++#if CMESH_BOT_ENABLED ++ autoLearnKnownBotAdvert(contact); ++#endif ++ + if (_serial->isConnected()) { + if (is_new) { + writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact); +@@ -1040,11 +1056,9 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n + message.path = packet->path; + } + +- const char *body = text; +- size_t body_len = botBoundedStrLen(text, BOT_MAX_TEXT_LEN); +- FirmwareBot::splitChannelText(text, body_len, message.sender_name, sizeof(message.sender_name), &body, &body_len); +- message.text_truncated = FirmwareBot::normalizeText(body, body_len, message.text, sizeof(message.text), +- &message.text_len) == BOT_WRITE_TRUNCATED; ++ message.text_truncated = FirmwareBot::normalizeChannelText(text, message.sender_name, sizeof(message.sender_name), ++ message.text, sizeof(message.text), ++ &message.text_len) == BOT_WRITE_TRUNCATED; + recordBotObservation(message, NULL, channel_idx); + } + +@@ -1100,6 +1114,21 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com + } + } + ++bool MyMesh::autoLearnKnownBotAdvert(const ContactInfo &contact) { ++ if (!bot_prefs.enabled || contact.type != ADV_TYPE_CHAT || ++ !FirmwareBot::isBotAdvertName(contact.name, botBoundedStrLen(contact.name, sizeof(contact.name)))) { ++ return false; ++ } ++ ++ uint8_t key[BOT_SENDER_KEY_PREFIX_LEN]; ++ memcpy(key, contact.id.pub_key, sizeof(key)); ++ if (BotPrefsCodec::findKnownBot(bot_prefs, key)) return false; ++ if (!BotPrefsCodec::addKnownBot(bot_prefs, key, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "autobot")) return false; ++ KnownBotRegistry::add(known_bot_entries, BOT_KNOWN_BOT_SLOTS, key, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "autobot"); ++ saveBotPrefs(); ++ return true; ++} ++ + bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, + const char *text, size_t text_len, BotFingerprint request_fingerprint, + BotFingerprint response_fingerprint) { +@@ -1124,11 +1153,9 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di + pending->channel_idx = channel_idx; + pending->request_fingerprint = request_fingerprint; + pending->response_fingerprint = response_fingerprint; +- pending->text_len = text_len; +- size_t max_text_len = FirmwareBot::maxResponseLenForChannel(message.channel_kind); +- if (pending->text_len > max_text_len) pending->text_len = max_text_len; +- if (pending->text_len > 0) memcpy(pending->text, text, pending->text_len); +- pending->text[pending->text_len] = 0; ++ if (!botFormatResponseForChannel(message, text, text_len, pending->text, sizeof(pending->text), &pending->text_len)) { ++ return false; ++ } + pending->active = true; + return true; + } +@@ -1204,7 +1231,14 @@ bool MyMesh::handleBotTraceCommand(const BotMessage &message, const ContactInfo + BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); + char response[BOT_MAX_RESPONSE_LEN + 1]; + size_t response_len = botFormatTraceSent(response, sizeof(response)); +- BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, response, response_len); ++ char final_response[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t final_response_len = 0; ++ if (!botFormatResponseForChannel(message, response, response_len, final_response, sizeof(final_response), ++ &final_response_len)) { ++ bot_stats.send_failures++; ++ 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(); +@@ -1264,6 +1298,10 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc + memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key)); + } + pending->channel_idx = channel_idx; ++ pending->channel_kind = message.channel_kind; ++ StrHelper::strzcpy(pending->channel_name, message.channel_name, sizeof(pending->channel_name)); ++ memcpy(pending->sender_key_prefix, message.sender_key_prefix, sizeof(pending->sender_key_prefix)); ++ pending->sender_key_prefix_len = message.sender_key_prefix_len; + pending->request_fingerprint = request_fingerprint; + pending->response_fingerprint = response_fingerprint; + pending->tag = tag; +@@ -1275,6 +1313,16 @@ bool MyMesh::enqueueBotTrace(const BotMessage &message, const ContactInfo *direc + return true; + } + ++BotFingerprint MyMesh::traceResponseFingerprintFor(const PendingBotTrace &pending, const char *text, size_t text_len) { ++ BotMessage message; ++ memset(&message, 0, sizeof(message)); ++ message.channel_kind = pending.channel_kind; ++ StrHelper::strzcpy(message.channel_name, pending.channel_name, sizeof(message.channel_name)); ++ memcpy(message.sender_key_prefix, pending.sender_key_prefix, sizeof(message.sender_key_prefix)); ++ message.sender_key_prefix_len = pending.sender_key_prefix_len; ++ return FirmwareBot::responseFingerprintFor(message, text, text_len); ++} ++ + bool MyMesh::sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, + BotFingerprint response_fingerprint, uint32_t now_millis) { + bool success = false; +@@ -1325,7 +1373,13 @@ bool MyMesh::sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis) + + char response[BOT_MAX_RESPONSE_LEN + 1]; + size_t response_len = botFormatTraceSent(response, sizeof(response)); +- return sendBotTraceText(pending, response, response_len, pending.response_fingerprint, now_millis); ++ char final_response[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t final_response_len = 0; ++ if (!botFormatResponseForChannelKind(pending.channel_kind, response, response_len, final_response, sizeof(final_response), ++ &final_response_len)) { ++ return false; ++ } ++ return sendBotTraceText(pending, final_response, final_response_len, pending.response_fingerprint, now_millis); + } + + void MyMesh::expirePendingBotTraces(uint32_t now_millis) { +@@ -1334,10 +1388,13 @@ void MyMesh::expirePendingBotTraces(uint32_t now_millis) { + if (!pending->active || !pending->sent || (int32_t)(now_millis - pending->expires_at_millis) < 0) continue; + + const char *response = "Trace timed out"; +- BotFingerprint response_fingerprint; +- response_fingerprint.value = pending->response_fingerprint.value ^ 0x74696d656f7574ULL; +- sendBotTraceText(*pending, response, botBoundedStrLen(response, BOT_MAX_RESPONSE_LEN + 1), response_fingerprint, +- now_millis); ++ char final_response[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t final_response_len = 0; ++ if (botFormatResponseForChannelKind(pending->channel_kind, response, botBoundedStrLen(response, BOT_MAX_RESPONSE_LEN + 1), ++ final_response, sizeof(final_response), &final_response_len)) { ++ BotFingerprint response_fingerprint = traceResponseFingerprintFor(*pending, final_response, final_response_len); ++ sendBotTraceText(*pending, final_response, final_response_len, response_fingerprint, now_millis); ++ } + pending->active = false; + } + } +@@ -1384,6 +1441,18 @@ bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritati + return false; + } + ++bool MyMesh::observeBotGroupResponse(const BotMessage &message) { ++ if (!BotPolicy::isPrefixlessCommandAllowed(message.channel_kind)) return false; ++ 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; ++ } ++ if (ResponseCoordinator::suppress(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint)) { ++ return true; ++ } ++ return false; ++} ++ + void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) { + bot_stats.observed_messages++; + BotPolicyDecision decision = BotPolicy::decide(message.channel_kind); +@@ -1403,6 +1472,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + } + + if (observeKnownBotResponse(message, direct_recipient != NULL)) return; ++ if (!direct_recipient && observeBotGroupResponse(message)) return; + sendQueuedBotResponses(); + + BotCommand command; +@@ -1439,8 +1509,15 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + return; + } + ++ char final_response[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t final_response_len = 0; ++ if (!botFormatResponseForChannel(message, response, result.text_len, final_response, sizeof(final_response), &final_response_len)) { ++ bot_stats.send_failures++; ++ return; ++ } ++ + BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); +- BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, response, result.text_len); ++ BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, final_response, final_response_len); + BotFingerprint fingerprint; + uint32_t due_at_millis = 0; + uint32_t bot_identity_seed; +@@ -1462,7 +1539,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + bot_stats.pending_responses++; + FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(), + BOT_COMMAND_COOLDOWN_MILLIS); +- if (!enqueueBotResponse(message, direct_recipient, channel_idx, response, result.text_len, fingerprint, response_fingerprint)) { ++ if (!enqueueBotResponse(message, direct_recipient, channel_idx, final_response, final_response_len, fingerprint, response_fingerprint)) { + ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint); + bot_stats.send_failures++; + } +@@ -1572,10 +1649,12 @@ void MyMesh::sendQueuedEmergencyForwards() { + + bool MyMesh::sendBotSelfAdvert(bool flood) { + mesh::Packet* pkt; ++ char bot_advert_name[sizeof(((ContactInfo*)0)->name)]; ++ FirmwareBot::writeBotAdvertName(_prefs.node_name, bot_advert_name, sizeof(bot_advert_name), NULL); + if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { +- pkt = createSelfAdvert(_prefs.node_name); ++ pkt = createSelfAdvert(bot_advert_name); + } else { +- pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); ++ pkt = createSelfAdvert(bot_advert_name, sensors.node_lat, sensors.node_lon); + } + if (!pkt) return false; + +@@ -1927,9 +2006,13 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, + char response[BOT_MAX_RESPONSE_LEN + 1]; + size_t response_len = botFormatTraceResult(response, sizeof(response), tag, flags, path_hashes, path_len, + (int8_t)(packet->getSNR() * 4)); +- BotFingerprint response_fingerprint; +- response_fingerprint.value = pending->response_fingerprint.value ^ 0x726573756c74ULL; +- sendBotTraceText(*pending, response, response_len, response_fingerprint, now); ++ char final_response[BOT_MAX_RESPONSE_LEN + 1]; ++ size_t final_response_len = 0; ++ if (botFormatResponseForChannelKind(pending->channel_kind, response, response_len, final_response, ++ sizeof(final_response), &final_response_len)) { ++ BotFingerprint response_fingerprint = traceResponseFingerprintFor(*pending, final_response, final_response_len); ++ sendBotTraceText(*pending, final_response, final_response_len, response_fingerprint, now); ++ } + pending->active = false; + break; + } +diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h +index 8d848fef..67bdc010 100644 +--- a/examples/companion_radio/MyMesh.h ++++ b/examples/companion_radio/MyMesh.h +@@ -214,7 +214,9 @@ private: + uint32_t sender_timestamp, const mesh::Packet *packet); + void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx); + bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender); ++ bool observeBotGroupResponse(const BotMessage &message); + void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id); ++ bool autoLearnKnownBotAdvert(const ContactInfo &contact); + bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, + const char *text, size_t text_len, BotFingerprint request_fingerprint, + BotFingerprint response_fingerprint); +@@ -226,6 +228,7 @@ private: + BotFingerprint response_fingerprint, uint32_t tag, uint32_t auth_code); + bool sendBotTraceText(const PendingBotTrace &pending, const char *text, size_t text_len, + BotFingerprint response_fingerprint, uint32_t now_millis); ++ BotFingerprint traceResponseFingerprintFor(const PendingBotTrace &pending, const char *text, size_t text_len); + bool sendPendingBotTrace(PendingBotTrace &pending, uint32_t now_millis); + void expirePendingBotTraces(uint32_t now_millis); + bool enqueueEmergencyForward(const BotMessage &message); +@@ -291,8 +294,12 @@ private: + bool active; + bool direct; + bool sent; ++ BotChannelKind channel_kind; + uint8_t recipient_pub_key[PUB_KEY_SIZE]; + uint8_t channel_idx; ++ char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1]; ++ uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN]; ++ uint8_t sender_key_prefix_len; + BotFingerprint request_fingerprint; + BotFingerprint response_fingerprint; + uint32_t tag; +diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp +index 25c03f18..484372e5 100644 +--- a/examples/companion_radio/ResponseCoordinator.cpp ++++ b/examples/companion_radio/ResponseCoordinator.cpp +@@ -120,13 +120,14 @@ BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pe + + bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint) { + if (!pending || response_fingerprint.value == 0) return false; ++ bool suppressed = false; + for (size_t i = 0; i < pending_count; i++) { + if (pending[i].active && sameFingerprint(pending[i].response_fingerprint, response_fingerprint)) { + pending[i].suppressed = true; +- return true; ++ suppressed = true; + } + } +- return false; ++ return suppressed; + } + + bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint) { diff --git a/tests/firmware_bot/test_firmware_bot.cpp b/tests/firmware_bot/test_firmware_bot.cpp index bc89366..76950fe 100644 --- a/tests/firmware_bot/test_firmware_bot.cpp +++ b/tests/firmware_bot/test_firmware_bot.cpp @@ -413,6 +413,69 @@ static void test_response_write() { assert(out[0] == 0); } +static void test_channel_text_and_response_guards() { + char sender[BOT_MAX_SENDER_NAME_LEN + 1]; + char out[BOT_MAX_TEXT_LEN + 1]; + size_t written = 0; + char input[BOT_MAX_TEXT_LEN + 8]; + strcpy(input, "alice: "); + memset(&input[7], 'x', BOT_MAX_TEXT_LEN); + input[7 + BOT_MAX_TEXT_LEN] = 0; + assert(FirmwareBot::normalizeChannelText(input, sender, sizeof(sender), out, sizeof(out), &written) == BOT_WRITE_TRUNCATED); + assert(strcmp(sender, "alice") == 0); + assert(written == BOT_MAX_TEXT_LEN); + + BotCommand command; + char response[BOT_MAX_RESPONSE_LEN + 1]; + assert(FirmwareBot::writeResponseForChannel(BOT_CHANNEL_BOT, true, "trace sent", 10, response, sizeof(response), + &written) == BOT_WRITE_OK); + assert(strcmp(response, "# trace sent") == 0); + assert(!FirmwareBot::parseCommand(response, written, &command, true)); + char normalized[BOT_MAX_TEXT_LEN + 1]; + assert(FirmwareBot::normalizeChannelText(response, sender, sizeof(sender), normalized, sizeof(normalized), &written) == + BOT_WRITE_OK); + assert(strcmp(normalized, "# trace sent") == 0); + assert(!FirmwareBot::parseCommand(normalized, written, &command, true)); + assert(FirmwareBot::writeResponseForChannel(BOT_CHANNEL_BOT, true, "# trace sent", 12, response, sizeof(response), + &written) == BOT_WRITE_OK); + assert(strcmp(response, "# trace sent") == 0); + assert(FirmwareBot::normalizeChannelText(response, sender, sizeof(sender), normalized, sizeof(normalized), &written) == + BOT_WRITE_OK); + assert(!FirmwareBot::parseCommand(normalized, written, &command, true)); + assert(FirmwareBot::writeResponseForChannel(BOT_CHANNEL_DM, false, "trace sent", 10, response, sizeof(response), + &written) == BOT_WRITE_OK); + assert(strcmp(response, "trace sent") == 0); + + strcpy(sender, "stale"); + assert(FirmwareBot::normalizeChannelText("plain text", sender, sizeof(sender), normalized, sizeof(normalized), + &written) == BOT_WRITE_OK); + assert(sender[0] == 0); + assert(strcmp(normalized, "plain text") == 0); + + char long_response[BOT_MAX_RESPONSE_LEN + 1]; + memset(long_response, 'p', sizeof(long_response)); + long_response[BOT_MAX_RESPONSE_LEN] = 0; + assert(FirmwareBot::writeResponseForChannel(BOT_CHANNEL_BOT, true, long_response, BOT_MAX_RESPONSE_LEN, response, + sizeof(response), &written) == BOT_WRITE_TRUNCATED); + assert(written == BOT_MAX_GROUP_RESPONSE_LEN); +} + +static void test_bot_advert_marker() { + char out[32]; + size_t written = 0; + assert(FirmwareBot::writeBotAdvertName("node", out, sizeof(out), &written) == BOT_WRITE_OK); + assert(strcmp(out, "node" BOT_ADVERT_MARKER) == 0); + assert(written == strlen(out)); + assert(FirmwareBot::isBotAdvertName(out, strlen(out))); + assert(!FirmwareBot::isBotAdvertName("node", 4)); + assert(!FirmwareBot::isBotAdvertName("node [bot]", 10)); + + char small[12]; + assert(FirmwareBot::writeBotAdvertName("very-long-node-name", small, sizeof(small), &written) == BOT_WRITE_TRUNCATED); + assert(FirmwareBot::isBotAdvertName(small, strlen(small))); + assert(written == sizeof(small) - 1); +} + static BotMessage make_message(const char* channel, const char* text) { BotMessage message; memset(&message, 0, sizeof(message)); @@ -486,7 +549,8 @@ static void test_command_outputs() { result = run_command("!help", out, sizeof(out)); assert(result.code == BOT_COMMAND_RESULT_OK); - assert(strstr(out, "Commands: help cmd ping") == out); + assert(strstr(out, "Cmds: help cmd ping") == out); + assert(result.text_len <= BOT_MAX_GROUP_RESPONSE_LEN); assert(strstr(out, "roll") != NULL); assert(strstr(out, "dice") != NULL); assert(strstr(out, "trace") != NULL); @@ -864,6 +928,14 @@ static void test_fingerprint() { assert(fa.value != FirmwareBot::fingerprintFor(b).value); } +static BotFingerprint final_response_fingerprint_for(const BotMessage& message, const char* text) { + char response[BOT_MAX_RESPONSE_LEN + 1]; + size_t written = 0; + assert(FirmwareBot::writeResponseForChannel(message.channel_kind, BotPolicy::isPrefixlessCommandAllowed(message.channel_kind), + text, strlen(text), response, sizeof(response), &written) != BOT_WRITE_NO_SPACE); + return FirmwareBot::responseFingerprintFor(message, response, written); +} + static void test_response_fingerprint() { BotMessage a = make_message("#bot", "!ping"); BotMessage b = make_message("bot", "!ping"); @@ -1011,8 +1083,112 @@ static void test_response_coordinator_suppression_uses_response_fingerprint() { assert(ready.request_fingerprint.value == first_request.value); assert(ready.response_fingerprint.value == response.value); ready = ResponseCoordinator::poll(pending, 2, due); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.request_fingerprint.value == second_request.value); + assert(ready.response_fingerprint.value == response.value); + assert(ResponseCoordinator::poll(pending, 2, due).result == BOT_COORDINATOR_READY_NONE); +} + +static void test_trace_final_response_fingerprints_use_final_text() { + BotMessage message = make_message("#bot", "trace"); + BotFingerprint sent = final_response_fingerprint_for(message, "Trace sent"); + BotFingerprint result_a = final_response_fingerprint_for(message, "Trace 12345678 2h x 2B snr 1.25: 11112222"); + BotFingerprint result_b = final_response_fingerprint_for(message, "Trace 12345678 2h x 2B snr 1.25: aaaabbbb"); + BotFingerprint timeout = final_response_fingerprint_for(message, "Trace timed out"); + assert(sent.value != result_a.value); + assert(result_a.value != result_b.value); + assert(timeout.value != sent.value); + assert(timeout.value != result_a.value); +} + +static void test_response_coordinator_distinct_path_outputs() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage first = make_message("#bot", "path"); + BotMessage second = first; + second.sender_timestamp++; + uint8_t path_a[] = { 0x12, 0x34, 0x56, 0x78 }; + uint8_t path_b[] = { 0xab, 0xcd, 0xef, 0x01 }; + first.path = path_a; + first.path_hash_size = 2; + first.path_hash_count = 2; + second.path = path_b; + second.path_hash_size = 2; + second.path_hash_count = 2; + BotFingerprint first_request = FirmwareBot::fingerprintFor(first); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second); + BotFingerprint first_response = FirmwareBot::responseFingerprintFor(first, "Path 2h x 2B snr 0.00: 12345678", 34); + BotFingerprint second_response = FirmwareBot::responseFingerprintFor(second, "Path 2h x 2B snr 0.00: abcdef01", 34); + BotFingerprint scheduled; + uint32_t first_due = 0; + uint32_t second_due = 0; + uint32_t identity_seed = 0x01020304UL; + + assert(first_request.value != second_request.value); + assert(first_response.value != second_response.value); + assert(ResponseCoordinator::schedule(pending, 2, first, BOT_COMMAND_PATH, first_request, first_response, 1000, 0, + identity_seed, 0, &scheduled, &first_due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::schedule(pending, 2, second, BOT_COMMAND_PATH, second_request, second_response, 1000, 0, + identity_seed, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::suppress(pending, 2, first_response)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.response_fingerprint.value == first_response.value); + ready = ResponseCoordinator::poll(pending, 2, first_due > second_due ? first_due : second_due); assert(ready.result == BOT_COORDINATOR_READY_SEND); assert(ready.request_fingerprint.value == second_request.value); + assert(ready.response_fingerprint.value == second_response.value); +} + +static void test_response_coordinator_group_observed_response_suppresses() { + BotCoordinatorPending pending[1]; + ResponseCoordinator::clear(pending, 1); + BotMessage request_message = make_message("#bot", "ping"); + BotMessage observed_response = make_message("#bot", "Pong!"); + BotFingerprint request = FirmwareBot::fingerprintFor(request_message); + BotFingerprint response = final_response_fingerprint_for(request_message, "Pong!"); + BotFingerprint observed = FirmwareBot::responseFingerprintFor(observed_response, observed_response.text, + observed_response.text_len); + BotFingerprint scheduled; + uint32_t due = 0; + + assert(response.value == observed.value); + assert(ResponseCoordinator::schedule(pending, 1, request_message, BOT_COMMAND_PING, request, response, 1000, 0, + 0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::suppress(pending, 1, observed)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 1, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.response_fingerprint.value == response.value); +} + +static void test_response_coordinator_group_guarded_path_output_suppresses() { + BotCoordinatorPending pending[1]; + ResponseCoordinator::clear(pending, 1); + BotMessage request_message = make_message("#bot", "path"); + BotFingerprint request = FirmwareBot::fingerprintFor(request_message); + const char* path_text = "Path 2h x 2B snr 0.00: 12345678"; + char final_response[BOT_MAX_RESPONSE_LEN + 1]; + size_t final_response_len = 0; + assert(FirmwareBot::writeResponseForChannel(request_message.channel_kind, + BotPolicy::isPrefixlessCommandAllowed(request_message.channel_kind), + path_text, strlen(path_text), final_response, sizeof(final_response), + &final_response_len) == BOT_WRITE_OK); + assert(strcmp(final_response, "# Path 2h x 2B snr 0.00: 12345678") == 0); + BotCommand command; + assert(!FirmwareBot::parseCommand(final_response, final_response_len, &command, true)); + BotFingerprint response = FirmwareBot::responseFingerprintFor(request_message, final_response, final_response_len); + BotMessage observed_response = make_message("#bot", final_response); + BotFingerprint observed = FirmwareBot::responseFingerprintFor(observed_response, observed_response.text, + observed_response.text_len); + BotFingerprint scheduled; + uint32_t due = 0; + + assert(response.value == observed.value); + assert(ResponseCoordinator::schedule(pending, 1, request_message, BOT_COMMAND_PATH, request, response, 1000, 0, + 0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::suppress(pending, 1, observed)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 1, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); assert(ready.response_fingerprint.value == response.value); } @@ -1160,6 +1336,8 @@ int main() { test_normalize_truncation(); test_parse_command(); test_response_write(); + test_channel_text_and_response_guards(); + test_bot_advert_marker(); test_command_outputs(); test_path_command(); test_dice_command(); @@ -1178,6 +1356,10 @@ int main() { test_response_coordinator_schedule_poll(); test_response_coordinator_distinct_requests_same_response(); test_response_coordinator_suppression_uses_response_fingerprint(); + test_trace_final_response_fingerprints_use_final_text(); + test_response_coordinator_distinct_path_outputs(); + test_response_coordinator_group_observed_response_suppresses(); + test_response_coordinator_group_guarded_path_output_suppresses(); test_response_coordinator_suppress_expire_full(); test_response_coordinator_cancel_by_request(); test_response_coordinator_delay_biases(); diff --git a/vendor/MeshCore b/vendor/MeshCore index 43a831d..b9ce1fd 160000 --- a/vendor/MeshCore +++ b/vendor/MeshCore @@ -1 +1 @@ -Subproject commit 43a831df10ffadf1c1bc44515453e666c95beed6 +Subproject commit b9ce1fd38ed7c67974643fa19c238b4387c7459c