diff --git a/patches/meshcore/0005-Add-response-coordinator.patch b/patches/meshcore/0005-Add-response-coordinator.patch new file mode 100644 index 0000000..b7f95bb --- /dev/null +++ b/patches/meshcore/0005-Add-response-coordinator.patch @@ -0,0 +1,766 @@ +diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h +index 1e6f72c..d44aadb 100644 +--- a/examples/companion_radio/BotTypes.h ++++ b/examples/companion_radio/BotTypes.h +@@ -17,7 +17,16 @@ + #define BOT_EMERGENCY_RATE_LIMIT_MILLIS 60000UL + #define BOT_EMERGENCY_RATE_LIMIT_COUNT 3 + #define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS ++#define BOT_COORDINATOR_PENDING_SLOTS 8 ++#define BOT_COORDINATOR_RECENT_SLOTS 16 ++#define BOT_KNOWN_BOT_SLOTS 8 ++#define BOT_RESPONSE_DELAY_BASE_MILLIS 1200UL ++#define BOT_RESPONSE_DELAY_JITTER_MILLIS 1800UL ++#define BOT_RESPONSE_PENDING_TTL_MILLIS 15000UL ++#define BOT_RESPONSE_RECENT_TTL_MILLIS 30000UL ++#define BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL 0x01 + #define BOT_SENDER_KEY_PREFIX_LEN 6 ++#define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4 + + enum BotChannelKind : uint8_t { + BOT_CHANNEL_DM = 0, +@@ -60,6 +69,20 @@ enum BotWriteResult : uint8_t { + BOT_WRITE_NO_SPACE + }; + ++enum BotCoordinatorScheduleResult : uint8_t { ++ BOT_COORDINATOR_SCHEDULED = 0, ++ BOT_COORDINATOR_REPLACED, ++ BOT_COORDINATOR_NO_SPACE, ++ BOT_COORDINATOR_NOT_NORMAL ++}; ++ ++enum BotCoordinatorReadyResult : uint8_t { ++ BOT_COORDINATOR_READY_NONE = 0, ++ BOT_COORDINATOR_READY_SEND, ++ BOT_COORDINATOR_READY_SUPPRESSED, ++ BOT_COORDINATOR_READY_EXPIRED ++}; ++ + struct BotFingerprint { + uint64_t value; + }; +@@ -69,6 +92,7 @@ struct BotMessage { + char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1]; + char sender_name[BOT_MAX_SENDER_NAME_LEN + 1]; + uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN]; ++ uint8_t sender_key_prefix_len; + bool text_truncated; + uint32_t sender_timestamp; + char text[BOT_MAX_TEXT_LEN + 1]; +@@ -115,6 +139,34 @@ struct BotCommandCooldown { + uint32_t expires_at_millis; + }; + ++struct BotKnownBotEntry { ++ bool active; ++ uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]; ++ uint8_t flags; ++ char label[12]; ++}; ++ ++struct BotCoordinatorPending { ++ bool active; ++ bool suppressed; ++ BotFingerprint request_fingerprint; ++ BotFingerprint response_fingerprint; ++ uint32_t due_at_millis; ++ uint32_t expires_at_millis; ++}; ++ ++struct BotCoordinatorRecent { ++ bool active; ++ BotFingerprint response_fingerprint; ++ uint32_t expires_at_millis; ++}; ++ ++struct BotCoordinatorReady { ++ BotCoordinatorReadyResult result; ++ BotFingerprint request_fingerprint; ++ BotFingerprint response_fingerprint; ++}; ++ + struct BotEmergencyForward { + uint8_t part_count; + bool truncated; +@@ -141,16 +193,24 @@ struct BotStats { + uint32_t emergency_forwards; + uint32_t emergency_forward_failures; + uint32_t parse_errors; ++ uint32_t pending_responses; ++ uint32_t suppressed_responses; ++ uint32_t expired_responses; ++ uint32_t known_bot_messages; + uint32_t sent_messages; + uint32_t send_failures; + }; + +-static_assert(sizeof(BotMessage) <= 240, "BotMessage RAM budget exceeded"); ++static_assert(sizeof(BotMessage) <= 248, "BotMessage RAM budget exceeded"); + static_assert(sizeof(BotCommand) <= 120, "BotCommand RAM budget exceeded"); + static_assert(sizeof(BotResponse) <= 184, "BotResponse RAM budget exceeded"); + static_assert(sizeof(BotCommandContext) <= 96, "BotCommandContext RAM budget exceeded"); + static_assert(sizeof(BotCommandResult) <= 16, "BotCommandResult RAM budget exceeded"); + static_assert(sizeof(BotCommandCooldown) <= 8, "BotCommandCooldown RAM budget exceeded"); ++static_assert(sizeof(BotKnownBotEntry) <= 24, "BotKnownBotEntry RAM budget exceeded"); ++static_assert(sizeof(BotCoordinatorPending) <= 32, "BotCoordinatorPending RAM budget exceeded"); ++static_assert(sizeof(BotCoordinatorRecent) <= 24, "BotCoordinatorRecent RAM budget exceeded"); ++static_assert(sizeof(BotCoordinatorReady) <= 24, "BotCoordinatorReady RAM budget exceeded"); + static_assert(sizeof(BotEmergencyForward) <= 480, "BotEmergencyForward RAM budget exceeded"); + static_assert(sizeof(BotPrefs) <= 128, "BotPrefs RAM budget exceeded"); +-static_assert(sizeof(BotStats) <= 48, "BotStats RAM budget exceeded"); ++static_assert(sizeof(BotStats) <= 64, "BotStats RAM budget exceeded"); +diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp +index 78b6693..56a3395 100644 +--- a/examples/companion_radio/FirmwareBot.cpp ++++ b/examples/companion_radio/FirmwareBot.cpp +@@ -25,6 +25,19 @@ uint64_t fnv1aUpdateTextLower(uint64_t hash, const char* value, size_t len) { + return hash; + } + ++size_t boundedStrLen(const char* value, size_t max_len); ++ ++uint64_t fnv1aUpdateChannel(uint64_t hash, const BotMessage& message) { ++ hash = fnv1aUpdate(hash, (uint8_t)message.channel_kind); ++ const char* channel_name = message.channel_name; ++ size_t channel_name_len = boundedStrLen(message.channel_name, sizeof(message.channel_name)); ++ if (channel_name_len > 0 && channel_name[0] == '#') { ++ channel_name++; ++ channel_name_len--; ++ } ++ return fnv1aUpdateTextLower(hash, channel_name, channel_name_len); ++} ++ + bool isSpaceByte(char ch) { + return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'; + } +@@ -229,15 +242,9 @@ BotWriteResult writeResponse(char* output, size_t output_len, const char* text, + + BotFingerprint fingerprintFor(const BotMessage& message) { + uint64_t hash = 1469598103934665603ULL; +- hash = fnv1aUpdate(hash, (uint8_t)message.channel_kind); +- const char* channel_name = message.channel_name; +- size_t channel_name_len = boundedStrLen(message.channel_name, sizeof(message.channel_name)); +- if (channel_name_len > 0 && channel_name[0] == '#') { +- channel_name++; +- channel_name_len--; +- } +- hash = fnv1aUpdateTextLower(hash, channel_name, channel_name_len); ++ 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); + + char normalized[BOT_MAX_TEXT_LEN + 1]; +@@ -249,4 +256,21 @@ BotFingerprint fingerprintFor(const BotMessage& message) { + return fingerprint; + } + ++BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) { ++ uint64_t hash = 1469598103934665603ULL; ++ hash = fnv1aUpdateChannel(hash, message); ++ if (message.channel_kind == BOT_CHANNEL_DM) { ++ hash = fnv1aUpdate(hash, message.sender_key_prefix_len); ++ hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_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); ++ hash = fnv1aUpdateTextLower(hash, normalized, normalized_len); ++ ++ BotFingerprint fingerprint = { hash }; ++ return fingerprint; ++} ++ + } +diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h +index b718985..42b9c5b 100644 +--- a/examples/companion_radio/FirmwareBot.h ++++ b/examples/companion_radio/FirmwareBot.h +@@ -10,6 +10,7 @@ bool splitChannelText(const char* text, size_t text_len, char* sender, size_t se + size_t* body_len); + BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written); + 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); + size_t maxResponseLenForChannel(BotChannelKind channel_kind); + bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id, +diff --git a/examples/companion_radio/KnownBotRegistry.cpp b/examples/companion_radio/KnownBotRegistry.cpp +new file mode 100644 +index 0000000..a3e764e +--- /dev/null ++++ b/examples/companion_radio/KnownBotRegistry.cpp +@@ -0,0 +1,87 @@ ++#include "KnownBotRegistry.h" ++ ++#include ++ ++namespace { ++ ++bool keyEqual(const uint8_t a[BOT_SENDER_KEY_PREFIX_LEN], const uint8_t b[BOT_SENDER_KEY_PREFIX_LEN]) { ++ return memcmp(a, b, BOT_SENDER_KEY_PREFIX_LEN) == 0; ++} ++ ++bool keyPrefixEqual(const uint8_t a[BOT_SENDER_KEY_PREFIX_LEN], const uint8_t b[BOT_SENDER_KEY_PREFIX_LEN], size_t len) { ++ return memcmp(a, b, len) == 0; ++} ++ ++void copyLabel(char dest[12], const char* label) { ++ size_t i = 0; ++ if (label) { ++ while (i + 1 < 12 && label[i] != 0) { ++ dest[i] = label[i]; ++ i++; ++ } ++ } ++ dest[i] = 0; ++} ++ ++} ++ ++namespace KnownBotRegistry { ++ ++void clear(BotKnownBotEntry entries[], size_t entry_count) { ++ if (!entries) return; ++ memset(entries, 0, sizeof(BotKnownBotEntry) * entry_count); ++} ++ ++const BotKnownBotEntry* find(const BotKnownBotEntry entries[], size_t entry_count, ++ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len) { ++ if (!entries || !key_prefix || key_prefix_len < BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN) return NULL; ++ if (key_prefix_len > BOT_SENDER_KEY_PREFIX_LEN) key_prefix_len = BOT_SENDER_KEY_PREFIX_LEN; ++ ++ const BotKnownBotEntry* match = NULL; ++ for (size_t i = 0; i < entry_count; i++) { ++ if (!entries[i].active || !keyPrefixEqual(entries[i].key_prefix, key_prefix, key_prefix_len)) continue; ++ if (match) return NULL; ++ match = &entries[i]; ++ } ++ return match; ++} ++ ++bool add(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], ++ uint8_t flags, const char* label) { ++ if (!entries || !key_prefix || entry_count == 0) return false; ++ ++ size_t slot = entry_count; ++ for (size_t i = 0; i < entry_count; i++) { ++ if (entries[i].active && keyEqual(entries[i].key_prefix, key_prefix)) { ++ slot = i; ++ break; ++ } ++ if (slot == entry_count && !entries[i].active) slot = i; ++ } ++ if (slot == entry_count) return false; ++ ++ entries[slot].active = true; ++ memcpy(entries[slot].key_prefix, key_prefix, BOT_SENDER_KEY_PREFIX_LEN); ++ entries[slot].flags = flags; ++ copyLabel(entries[slot].label, label); ++ return true; ++} ++ ++bool remove(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]) { ++ if (!entries || !key_prefix) return false; ++ for (size_t i = 0; i < entry_count; i++) { ++ if (entries[i].active && keyEqual(entries[i].key_prefix, key_prefix)) { ++ memset(&entries[i], 0, sizeof(entries[i])); ++ return true; ++ } ++ } ++ return false; ++} ++ ++bool canSuppressNormal(const BotKnownBotEntry entries[], size_t entry_count, ++ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len) { ++ const BotKnownBotEntry* entry = find(entries, entry_count, key_prefix, key_prefix_len); ++ return entry && (entry->flags & BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL) != 0; ++} ++ ++} +diff --git a/examples/companion_radio/KnownBotRegistry.h b/examples/companion_radio/KnownBotRegistry.h +new file mode 100644 +index 0000000..5e93dad +--- /dev/null ++++ b/examples/companion_radio/KnownBotRegistry.h +@@ -0,0 +1,16 @@ ++#pragma once ++ ++#include "BotTypes.h" ++ ++namespace KnownBotRegistry { ++ ++void clear(BotKnownBotEntry entries[], size_t entry_count); ++bool add(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], ++ uint8_t flags, const char* label); ++bool remove(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]); ++const BotKnownBotEntry* find(const BotKnownBotEntry entries[], size_t entry_count, ++ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len); ++bool canSuppressNormal(const BotKnownBotEntry entries[], size_t entry_count, ++ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len); ++ ++} +diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp +index ad72ccc..31e9bad 100644 +--- a/examples/companion_radio/MyMesh.cpp ++++ b/examples/companion_radio/MyMesh.cpp +@@ -8,6 +8,8 @@ + #include "BotPolicy.h" + #include "EmergencyForwarder.h" + #include "FirmwareBot.h" ++#include "KnownBotRegistry.h" ++#include "ResponseCoordinator.h" + #endif + + #define CMD_APP_START 1 +@@ -566,6 +568,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti + size_t prefix_len = sender_prefix_len; + if (prefix_len > sizeof(message.sender_key_prefix)) prefix_len = sizeof(message.sender_key_prefix); + if (sender_prefix && prefix_len > 0) memcpy(message.sender_key_prefix, sender_prefix, prefix_len); ++ message.sender_key_prefix_len = prefix_len; + message.sender_timestamp = sender_timestamp; + message.text_truncated = FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN + 1), message.text, + sizeof(message.text), &message.text_len) == BOT_WRITE_TRUNCATED; +@@ -618,28 +621,36 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com + } + + bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx, +- const char *text, size_t text_len) { ++ const char *text, size_t text_len, BotFingerprint request_fingerprint, ++ BotFingerprint response_fingerprint) { ++ size_t slot = BOT_PENDING_RESPONSE_SLOTS; + for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) { +- PendingBotResponse *pending = &pending_bot_responses[i]; +- if (!pending->active) { +- pending->direct = message.channel_kind == BOT_CHANNEL_DM; +- if (pending->direct) { +- if (!direct_recipient) return false; +- memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key)); +- } else { +- memset(pending->recipient_pub_key, 0, sizeof(pending->recipient_pub_key)); +- } +- pending->channel_idx = channel_idx; +- 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; +- pending->active = true; +- return true; ++ if (pending_bot_responses[i].active && pending_bot_responses[i].request_fingerprint.value == request_fingerprint.value) { ++ slot = i; ++ break; + } ++ if (slot == BOT_PENDING_RESPONSE_SLOTS && !pending_bot_responses[i].active) slot = i; + } +- return false; ++ if (slot == BOT_PENDING_RESPONSE_SLOTS) return false; ++ ++ PendingBotResponse *pending = &pending_bot_responses[slot]; ++ pending->direct = message.channel_kind == BOT_CHANNEL_DM; ++ if (pending->direct) { ++ if (!direct_recipient) return false; ++ memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key)); ++ } else { ++ memset(pending->recipient_pub_key, 0, sizeof(pending->recipient_pub_key)); ++ } ++ 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; ++ pending->active = true; ++ return true; + } + + bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) { +@@ -699,6 +710,22 @@ bool MyMesh::enqueueEmergencyForward(const BotMessage &message) { + return true; + } + ++bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritative_sender) { ++ if (!authoritative_sender || message.channel_kind != BOT_CHANNEL_DM) return false; ++ if (!KnownBotRegistry::canSuppressNormal(known_bot_entries, BOT_KNOWN_BOT_SLOTS, message.sender_key_prefix, ++ message.sender_key_prefix_len)) return false; ++ ++ bot_stats.known_bot_messages++; ++ 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); +@@ -712,6 +739,9 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + return; + } + ++ if (observeKnownBotResponse(message, direct_recipient != NULL)) return; ++ sendQueuedBotResponses(); ++ + BotCommand command; + if (!FirmwareBot::parseCommand(message.text, message.text_len, &command)) { + if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++; +@@ -732,18 +762,58 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo * + return; + } + ++ BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message); ++ BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, response, result.text_len); ++ BotFingerprint fingerprint; ++ uint32_t due_at_millis = 0; ++ uint32_t bot_identity_seed; ++ memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed)); ++ uint8_t queue_depth = (uint8_t)_mgr->getOutboundTotal(); ++ BotCoordinatorScheduleResult schedule = ResponseCoordinator::schedule(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, ++ message, command.id, request_fingerprint, ++ response_fingerprint, _ms->getMillis(), ++ context.random_seed, bot_identity_seed, ++ queue_depth, &fingerprint, &due_at_millis); ++ if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) { ++ bot_stats.send_failures++; ++ return; ++ } ++ + bot_stats.eligible_messages++; ++ 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)) { ++ if (!enqueueBotResponse(message, direct_recipient, channel_idx, response, result.text_len, fingerprint, response_fingerprint)) { ++ ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint); + bot_stats.send_failures++; + } + } + + void MyMesh::sendQueuedBotResponses() { +- for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) { +- PendingBotResponse *pending = &pending_bot_responses[i]; +- if (!pending->active) continue; ++ uint32_t now = _ms->getMillis(); ++ 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) { ++ bot_stats.expired_responses++; ++ } ++ ++ PendingBotResponse *pending = NULL; ++ for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) { ++ if (pending_bot_responses[i].active && pending_bot_responses[i].request_fingerprint.value == ready.request_fingerprint.value) { ++ pending = &pending_bot_responses[i]; ++ break; ++ } ++ } ++ if (!pending) continue; ++ ++ if (ready.result != BOT_COORDINATOR_READY_SEND) { ++ pending->active = false; ++ continue; ++ } + + bool success = false; + if (pending->direct) { +@@ -770,6 +840,8 @@ void MyMesh::sendQueuedBotResponses() { + + if (success) { + bot_stats.sent_messages++; ++ ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, ++ pending->response_fingerprint, now); + } else { + bot_stats.send_failures++; + } +@@ -1176,6 +1248,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe + memset(pending_bot_responses, 0, sizeof(pending_bot_responses)); + memset(pending_emergency_forwards, 0, sizeof(pending_emergency_forwards)); + memset(bot_command_cooldowns, 0, sizeof(bot_command_cooldowns)); ++ ResponseCoordinator::clear(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS); ++ ResponseCoordinator::clearRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS); ++ KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS); + emergency_rate_window_started = 0; + emergency_rate_count = 0; + next_bot_local_advert = 0; +diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h +index 10b27fe..698ecad 100644 +--- a/examples/companion_radio/MyMesh.h ++++ b/examples/companion_radio/MyMesh.h +@@ -9,7 +9,7 @@ + #endif + #if CMESH_BOT_ENABLED + #include "BotTypes.h" +-#define BOT_PENDING_RESPONSE_SLOTS 2 ++#define BOT_PENDING_RESPONSE_SLOTS BOT_COORDINATOR_PENDING_SLOTS + #define BOT_COMMAND_COOLDOWN_SLOTS 9 + #endif + +@@ -207,9 +207,11 @@ private: + void observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text, + uint32_t sender_timestamp); + void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx); ++ bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender); + 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); ++ const char *text, size_t text_len, BotFingerprint request_fingerprint, ++ BotFingerprint response_fingerprint); + bool enqueueEmergencyForward(const BotMessage &message); + bool findBotChannel(BotChannelKind kind, uint8_t &channel_idx); + bool isEmergencyRateLimited(); +@@ -259,6 +261,8 @@ private: + bool direct; + uint8_t recipient_pub_key[PUB_KEY_SIZE]; + uint8_t channel_idx; ++ BotFingerprint request_fingerprint; ++ BotFingerprint response_fingerprint; + char text[BOT_MAX_RESPONSE_LEN + 1]; + size_t text_len; + }; +@@ -273,6 +277,9 @@ private: + PendingBotResponse pending_bot_responses[BOT_PENDING_RESPONSE_SLOTS]; + PendingEmergencyForward pending_emergency_forwards[BOT_PENDING_EMERGENCY_SLOTS]; + BotCommandCooldown bot_command_cooldowns[BOT_COMMAND_COOLDOWN_SLOTS]; ++ BotCoordinatorPending bot_coordinator_pending[BOT_COORDINATOR_PENDING_SLOTS]; ++ BotCoordinatorRecent bot_coordinator_recent[BOT_COORDINATOR_RECENT_SLOTS]; ++ BotKnownBotEntry known_bot_entries[BOT_KNOWN_BOT_SLOTS]; + unsigned long emergency_rate_window_started; + uint8_t emergency_rate_count; + unsigned long next_bot_local_advert; +diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp +new file mode 100644 +index 0000000..c5c8fc3 +--- /dev/null ++++ b/examples/companion_radio/ResponseCoordinator.cpp +@@ -0,0 +1,190 @@ ++#include "ResponseCoordinator.h" ++ ++#include "BotPolicy.h" ++ ++#include ++ ++namespace { ++ ++bool isNormalChannel(BotChannelKind kind) { ++ return BotPolicy::isNormalAllowed(kind); ++} ++ ++bool sameFingerprint(BotFingerprint a, BotFingerprint b) { ++ return a.value == b.value; ++} ++ ++uint32_t channelDelayBias(BotChannelKind kind) { ++ if (kind == BOT_CHANNEL_DM) return 0; ++ if (kind == BOT_CHANNEL_BOT) return 200; ++ if (kind == BOT_CHANNEL_TESTING) return 400; ++ return 800; ++} ++ ++uint32_t commandDelayBias(BotCommandId command_id) { ++ if (command_id == BOT_COMMAND_PING || command_id == BOT_COMMAND_TEST) return 0; ++ if (command_id == BOT_COMMAND_DICE) return 200; ++ if (command_id == BOT_COMMAND_STATUS || command_id == BOT_COMMAND_CHANNELS) return 400; ++ return 100; ++} ++ ++uint32_t tieBreakBias(BotFingerprint request_fingerprint, uint32_t bot_identity_seed) { ++ uint32_t mixed = (uint32_t)request_fingerprint.value ^ (uint32_t)(request_fingerprint.value >> 32) ^ bot_identity_seed; ++ mixed ^= mixed >> 16; ++ mixed *= 0x7feb352dUL; ++ mixed ^= mixed >> 15; ++ return mixed % 900UL; ++} ++ ++uint32_t queueDelayBias(uint8_t queue_depth) { ++ return (uint32_t)queue_depth * 150UL; ++} ++ ++bool millisDue(uint32_t now_millis, uint32_t then_millis) { ++ return (int32_t)(now_millis - then_millis) >= 0; ++} ++ ++} ++ ++namespace ResponseCoordinator { ++ ++void clear(BotCoordinatorPending pending[], size_t pending_count) { ++ if (!pending) return; ++ memset(pending, 0, sizeof(BotCoordinatorPending) * pending_count); ++} ++ ++void clearRecent(BotCoordinatorRecent recent[], size_t recent_count) { ++ if (!recent) return; ++ memset(recent, 0, sizeof(BotCoordinatorRecent) * recent_count); ++} ++ ++uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id, BotFingerprint request_fingerprint, ++ uint32_t bot_identity_seed, uint8_t queue_depth, uint32_t jitter_seed) { ++ uint32_t jitter = BOT_RESPONSE_DELAY_JITTER_MILLIS ? jitter_seed % BOT_RESPONSE_DELAY_JITTER_MILLIS : 0; ++ return BOT_RESPONSE_DELAY_BASE_MILLIS + channelDelayBias(message.channel_kind) + commandDelayBias(command_id) + ++ queueDelayBias(queue_depth) + tieBreakBias(request_fingerprint, bot_identity_seed) + jitter; ++} ++ ++BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pending_count, ++ const BotMessage& message, BotCommandId command_id, ++ BotFingerprint request_fingerprint, BotFingerprint response_fingerprint, ++ uint32_t now_millis, uint32_t jitter_seed, uint32_t bot_identity_seed, ++ uint8_t queue_depth, BotFingerprint* fingerprint, uint32_t* due_at_millis) { ++ if (fingerprint) fingerprint->value = 0; ++ if (due_at_millis) *due_at_millis = 0; ++ if (!pending || pending_count == 0 || !isNormalChannel(message.channel_kind) || request_fingerprint.value == 0 || response_fingerprint.value == 0) return BOT_COORDINATOR_NOT_NORMAL; ++ ++ uint32_t due = now_millis + responseDelayMillis(message, command_id, request_fingerprint, bot_identity_seed, queue_depth, jitter_seed); ++ size_t slot = pending_count; ++ ++ for (size_t i = 0; i < pending_count; i++) { ++ if (pending[i].active && sameFingerprint(pending[i].request_fingerprint, request_fingerprint)) { ++ slot = i; ++ break; ++ } ++ if (slot == pending_count && !pending[i].active) slot = i; ++ } ++ if (slot == pending_count) return BOT_COORDINATOR_NO_SPACE; ++ ++ bool replaced = pending[slot].active; ++ pending[slot].active = true; ++ pending[slot].suppressed = false; ++ pending[slot].request_fingerprint = request_fingerprint; ++ pending[slot].response_fingerprint = response_fingerprint; ++ pending[slot].due_at_millis = due; ++ pending[slot].expires_at_millis = now_millis + BOT_RESPONSE_PENDING_TTL_MILLIS; ++ if (fingerprint) *fingerprint = request_fingerprint; ++ if (due_at_millis) *due_at_millis = due; ++ return replaced ? BOT_COORDINATOR_REPLACED : BOT_COORDINATOR_SCHEDULED; ++} ++ ++bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint) { ++ if (!pending || response_fingerprint.value == 0) return 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; ++ } ++ } ++ return false; ++} ++ ++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++) { ++ if (pending[i].active && sameFingerprint(pending[i].request_fingerprint, request_fingerprint)) { ++ pending[i].active = false; ++ return true; ++ } ++ } ++ return false; ++} ++ ++BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis) { ++ BotCoordinatorReady ready; ++ ready.result = BOT_COORDINATOR_READY_NONE; ++ ready.request_fingerprint.value = 0; ++ ready.response_fingerprint.value = 0; ++ if (!pending) return ready; ++ ++ for (size_t i = 0; i < pending_count; i++) { ++ if (!pending[i].active) continue; ++ if (pending[i].suppressed) { ++ ready.result = BOT_COORDINATOR_READY_SUPPRESSED; ++ ready.request_fingerprint = pending[i].request_fingerprint; ++ ready.response_fingerprint = pending[i].response_fingerprint; ++ pending[i].active = false; ++ return ready; ++ } ++ if (millisDue(now_millis, pending[i].expires_at_millis)) { ++ ready.result = BOT_COORDINATOR_READY_EXPIRED; ++ ready.request_fingerprint = pending[i].request_fingerprint; ++ ready.response_fingerprint = pending[i].response_fingerprint; ++ pending[i].active = false; ++ return ready; ++ } ++ if (millisDue(now_millis, pending[i].due_at_millis)) { ++ ready.result = BOT_COORDINATOR_READY_SEND; ++ ready.request_fingerprint = pending[i].request_fingerprint; ++ ready.response_fingerprint = pending[i].response_fingerprint; ++ pending[i].active = false; ++ return ready; ++ } ++ } ++ ++ return ready; ++} ++ ++void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, ++ uint32_t now_millis) { ++ if (!recent || recent_count == 0 || response_fingerprint.value == 0) return; ++ ++ size_t slot = recent_count; ++ for (size_t i = 0; i < recent_count; i++) { ++ if (recent[i].active && sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) { ++ slot = i; ++ break; ++ } ++ if (slot == recent_count && (!recent[i].active || millisDue(now_millis, recent[i].expires_at_millis))) 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; ++} ++ ++bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, ++ uint32_t now_millis) { ++ 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 (sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true; ++ } ++ return false; ++} ++ ++} +diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h +new file mode 100644 +index 0000000..d900633 +--- /dev/null ++++ b/examples/companion_radio/ResponseCoordinator.h +@@ -0,0 +1,24 @@ ++#pragma once ++ ++#include "BotTypes.h" ++ ++namespace ResponseCoordinator { ++ ++void clear(BotCoordinatorPending pending[], size_t pending_count); ++void clearRecent(BotCoordinatorRecent recent[], size_t recent_count); ++uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id, BotFingerprint request_fingerprint, ++ uint32_t bot_identity_seed, uint8_t queue_depth, uint32_t jitter_seed); ++BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pending_count, ++ const BotMessage& message, BotCommandId command_id, ++ BotFingerprint request_fingerprint, BotFingerprint response_fingerprint, ++ uint32_t now_millis, uint32_t jitter_seed, uint32_t bot_identity_seed, ++ uint8_t queue_depth, BotFingerprint* fingerprint, uint32_t* due_at_millis); ++bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint); ++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, ++ uint32_t now_millis); ++bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint, ++ uint32_t now_millis); ++ ++} diff --git a/tests/firmware_bot/run_tests.py b/tests/firmware_bot/run_tests.py index efdc892..17385c2 100644 --- a/tests/firmware_bot/run_tests.py +++ b/tests/firmware_bot/run_tests.py @@ -24,6 +24,8 @@ def main(): str(SRC_DIR / "BotPolicy.cpp"), str(SRC_DIR / "BotCommands.cpp"), str(SRC_DIR / "EmergencyForwarder.cpp"), + str(SRC_DIR / "KnownBotRegistry.cpp"), + str(SRC_DIR / "ResponseCoordinator.cpp"), "-o", str(binary), ] diff --git a/tests/firmware_bot/test_firmware_bot.cpp b/tests/firmware_bot/test_firmware_bot.cpp index e8349cb..3502629 100644 --- a/tests/firmware_bot/test_firmware_bot.cpp +++ b/tests/firmware_bot/test_firmware_bot.cpp @@ -6,6 +6,8 @@ #include "BotPolicy.h" #include "EmergencyForwarder.h" #include "FirmwareBot.h" +#include "KnownBotRegistry.h" +#include "ResponseCoordinator.h" static void test_channel_policy() { assert(BotPolicy::classifyChannel(NULL, 0, true) == BOT_CHANNEL_DM); @@ -320,6 +322,53 @@ static void test_emergency_forward_truncation() { assert(strstr(forward.parts[forward.part_count - 1], "...") != NULL); } +static void test_known_bot_registry() { + BotKnownBotEntry entries[2]; + uint8_t key1[BOT_SENDER_KEY_PREFIX_LEN] = { 1, 2, 3, 4, 5, 6 }; + uint8_t key2[BOT_SENDER_KEY_PREFIX_LEN] = { 7, 8, 9, 10, 11, 12 }; + uint8_t key3[BOT_SENDER_KEY_PREFIX_LEN] = { 13, 14, 15, 16, 17, 18 }; + KnownBotRegistry::clear(entries, 2); + + assert(!KnownBotRegistry::find(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::add(entries, 2, key1, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "alpha-bot")); + const BotKnownBotEntry* entry = KnownBotRegistry::find(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN); + assert(entry != NULL); + assert(entry->active); + assert(strcmp(entry->label, "alpha-bot") == 0); + assert(KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN)); + assert(!KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN - 1)); + assert(!KnownBotRegistry::canSuppressNormal(entries, 2, key2, BOT_SENDER_KEY_PREFIX_LEN)); + + assert(KnownBotRegistry::add(entries, 2, key1, 0, "renamed-bot")); + entry = KnownBotRegistry::find(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN); + assert(entry != NULL); + assert(strcmp(entry->label, "renamed-bot") == 0); + assert(!KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN)); + + assert(KnownBotRegistry::add(entries, 2, key2, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "beta")); + assert(!KnownBotRegistry::add(entries, 2, key3, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "gamma")); + assert(KnownBotRegistry::remove(entries, 2, key1)); + assert(!KnownBotRegistry::find(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::add(entries, 2, key3, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, NULL)); + assert(KnownBotRegistry::canSuppressNormal(entries, 2, key3, BOT_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::remove(entries, 2, key3)); + assert(!KnownBotRegistry::remove(entries, 2, key3)); +} + +static void test_known_bot_registry_ambiguous_short_prefix() { + BotKnownBotEntry entries[2]; + uint8_t key1[BOT_SENDER_KEY_PREFIX_LEN] = { 1, 2, 3, 4, 5, 6 }; + uint8_t key2[BOT_SENDER_KEY_PREFIX_LEN] = { 1, 2, 3, 4, 7, 8 }; + KnownBotRegistry::clear(entries, 2); + + assert(KnownBotRegistry::add(entries, 2, key1, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "alpha")); + assert(KnownBotRegistry::add(entries, 2, key2, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "beta")); + assert(!KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::canSuppressNormal(entries, 2, key1, BOT_SENDER_KEY_PREFIX_LEN)); + assert(KnownBotRegistry::canSuppressNormal(entries, 2, key2, BOT_SENDER_KEY_PREFIX_LEN)); +} + static void test_fingerprint() { BotMessage a = make_message("#bot", "!PING"); BotMessage b = make_message("bot", "!ping"); @@ -332,6 +381,283 @@ static void test_fingerprint() { b.sender_key_prefix[0] = 99; assert(fa.value != FirmwareBot::fingerprintFor(b).value); + b.sender_key_prefix[0] = a.sender_key_prefix[0]; + strncpy(b.sender_name, "bob", sizeof(b.sender_name) - 1); + assert(fa.value != FirmwareBot::fingerprintFor(b).value); +} + +static void test_response_fingerprint() { + BotMessage a = make_message("#bot", "!ping"); + BotMessage b = make_message("bot", "!ping"); + BotMessage c = make_message("#testing", "!ping"); + BotFingerprint fa = FirmwareBot::responseFingerprintFor(a, " Pong! ", 7); + BotFingerprint fb = FirmwareBot::responseFingerprintFor(b, "pong!", 5); + BotFingerprint fc = FirmwareBot::responseFingerprintFor(c, "pong!", 5); + assert(fa.value == fb.value); + assert(fa.value != fc.value); + + a.sender_key_prefix[0] = 99; + a.sender_timestamp++; + assert(fa.value == FirmwareBot::responseFingerprintFor(a, "pong!", 5).value); + assert(fa.value != FirmwareBot::responseFingerprintFor(a, "status", 6).value); + + BotMessage dm = make_message("#bot", "!ping"); + dm.channel_kind = BOT_CHANNEL_DM; + dm.channel_name[0] = 0; + dm.sender_key_prefix_len = BOT_SENDER_KEY_PREFIX_LEN; + BotMessage other_dm = dm; + other_dm.sender_key_prefix[0] ^= 0x55; + assert(FirmwareBot::responseFingerprintFor(dm, "pong!", 5).value != + FirmwareBot::responseFingerprintFor(other_dm, "pong!", 5).value); +} + +static void test_authoritative_suppression_flow() { + BotKnownBotEntry entries[1]; + uint8_t known_key[BOT_SENDER_KEY_PREFIX_LEN] = { 2, 4, 6, 8, 10, 12 }; + KnownBotRegistry::clear(entries, 1); + assert(KnownBotRegistry::add(entries, 1, known_key, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, "known")); + + BotCoordinatorPending pending[1]; + ResponseCoordinator::clear(pending, 1); + BotMessage dm = make_message("#bot", "!ping"); + dm.channel_kind = BOT_CHANNEL_DM; + dm.channel_name[0] = 0; + memcpy(dm.sender_key_prefix, known_key, sizeof(known_key)); + dm.sender_key_prefix_len = BOT_SENDER_KEY_PREFIX_LEN; + BotFingerprint request = FirmwareBot::fingerprintFor(dm); + BotFingerprint response = FirmwareBot::responseFingerprintFor(dm, "Pong!", 5); + BotFingerprint scheduled; + uint32_t due = 0; + + assert(ResponseCoordinator::schedule(pending, 1, dm, BOT_COMMAND_PING, request, response, 1000, 0, + 0x01020304UL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(KnownBotRegistry::canSuppressNormal(entries, 1, dm.sender_key_prefix, dm.sender_key_prefix_len)); + assert(ResponseCoordinator::suppress(pending, 1, FirmwareBot::responseFingerprintFor(dm, "Pong!", 5))); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 1, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.request_fingerprint.value == request.value); + + ResponseCoordinator::clear(pending, 1); + BotMessage group = make_message("#bot", "cm-bot: Pong!"); + group.sender_key_prefix_len = 0; + assert(!KnownBotRegistry::canSuppressNormal(entries, 1, group.sender_key_prefix, group.sender_key_prefix_len)); +} + +static void test_response_coordinator_schedule_poll() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage message = make_message("#bot", "!ping"); + BotFingerprint request = FirmwareBot::fingerprintFor(message); + BotFingerprint response = FirmwareBot::responseFingerprintFor(message, "Pong!", 5); + BotFingerprint scheduled; + uint32_t due = 0; + uint32_t identity_seed = 0xA5A55A5AUL; + uint8_t queue_depth = 1; + uint32_t jitter_seed = 17; + + assert(ResponseCoordinator::schedule(pending, 2, message, BOT_COMMAND_PING, request, response, 1000, jitter_seed, + identity_seed, queue_depth, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(scheduled.value == request.value); + assert(due == 1000 + ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, identity_seed, + queue_depth, jitter_seed)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, due - 1); + assert(ready.result == BOT_COORDINATOR_READY_NONE); + ready = ResponseCoordinator::poll(pending, 2, due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == request.value); + assert(ready.response_fingerprint.value == response.value); + assert(ResponseCoordinator::poll(pending, 2, due).result == BOT_COORDINATOR_READY_NONE); + + assert(ResponseCoordinator::schedule(pending, 2, message, BOT_COMMAND_PING, request, response, 2000, 0, + identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::schedule(pending, 2, message, BOT_COMMAND_PING, request, response, 3000, 0, + identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_REPLACED); + ready = ResponseCoordinator::poll(pending, 2, due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == request.value); + assert(ready.response_fingerprint.value == response.value); +} + +static void test_response_coordinator_distinct_requests_same_response() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage first = make_message("#bot", "!ping"); + BotMessage second = first; + second.sender_timestamp++; + BotFingerprint first_request = FirmwareBot::fingerprintFor(first); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second); + BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); + 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(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, + identity_seed, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED); + assert(scheduled.value == second_request.value); + + bool first_is_early = first_due <= second_due; + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, first_is_early ? first_due : second_due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == (first_is_early ? first_request.value : second_request.value)); + assert(ready.response_fingerprint.value == response.value); + ready = ResponseCoordinator::poll(pending, 2, first_is_early ? second_due : first_due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == (first_is_early ? second_request.value : first_request.value)); + assert(ready.response_fingerprint.value == response.value); +} + +static void test_response_coordinator_suppression_uses_response_fingerprint() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage first = make_message("#bot", "!ping"); + BotMessage second = first; + second.sender_timestamp++; + BotFingerprint first_request = FirmwareBot::fingerprintFor(first); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second); + BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); + BotFingerprint scheduled; + uint32_t due = 0; + + 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, + 0x0A0B0C0DUL, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::suppress(pending, 2, response)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 2, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + 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_SEND); + assert(ready.request_fingerprint.value == second_request.value); + assert(ready.response_fingerprint.value == response.value); +} + +static void test_response_coordinator_suppress_expire_full() { + BotCoordinatorPending pending[1]; + ResponseCoordinator::clear(pending, 1); + BotMessage message = make_message("#bot", "!ping"); + BotMessage second_message = make_message("#bot", "!test"); + BotFingerprint first_request = FirmwareBot::fingerprintFor(message); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second_message); + BotFingerprint first_response = FirmwareBot::responseFingerprintFor(message, "Pong!", 5); + BotFingerprint second_response = FirmwareBot::responseFingerprintFor(second_message, "Bot test OK", 11); + BotFingerprint scheduled; + uint32_t due = 0; + uint32_t identity_seed = 0x11223344UL; + + assert(ResponseCoordinator::schedule(pending, 1, message, BOT_COMMAND_PING, first_request, first_response, 1000, 0, + identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::schedule(pending, 1, second_message, BOT_COMMAND_TEST, second_request, second_response, 1000, + 0, identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_NO_SPACE); + assert(ResponseCoordinator::suppress(pending, 1, first_response)); + BotCoordinatorReady ready = ResponseCoordinator::poll(pending, 1, 1000); + assert(ready.result == BOT_COORDINATOR_READY_SUPPRESSED); + assert(ready.request_fingerprint.value == first_request.value); + assert(ready.response_fingerprint.value == first_response.value); + assert(!ResponseCoordinator::suppress(pending, 1, first_response)); + + assert(ResponseCoordinator::schedule(pending, 1, message, BOT_COMMAND_PING, first_request, first_response, 1000, 0, + identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::schedule(pending, 1, second_message, BOT_COMMAND_TEST, second_request, second_response, + 1000 + BOT_RESPONSE_PENDING_TTL_MILLIS, 0, identity_seed, 0, &scheduled, + &due) == BOT_COORDINATOR_NO_SPACE); + ready = ResponseCoordinator::poll(pending, 1, 1000 + BOT_RESPONSE_PENDING_TTL_MILLIS); + assert(ready.result == BOT_COORDINATOR_READY_EXPIRED); + assert(ready.request_fingerprint.value == first_request.value); + assert(ready.response_fingerprint.value == first_response.value); + assert(ResponseCoordinator::schedule(pending, 1, second_message, BOT_COMMAND_TEST, second_request, second_response, + 1000 + BOT_RESPONSE_PENDING_TTL_MILLIS, 0, identity_seed, 0, &scheduled, + &due) == BOT_COORDINATOR_SCHEDULED); + + ResponseCoordinator::clear(pending, 1); + assert(ResponseCoordinator::schedule(pending, 1, message, BOT_COMMAND_PING, first_request, first_response, + 0xFFFFFF00UL, 0, identity_seed, 0, &scheduled, &due) == BOT_COORDINATOR_SCHEDULED); + ready = ResponseCoordinator::poll(pending, 1, 0xFFFFFF00UL); + assert(ready.result == BOT_COORDINATOR_READY_NONE); + ready = ResponseCoordinator::poll(pending, 1, due); + assert(ready.result == BOT_COORDINATOR_READY_SEND); + assert(ready.request_fingerprint.value == first_request.value); + assert(ready.response_fingerprint.value == first_response.value); +} + +static void test_response_coordinator_cancel_by_request() { + BotCoordinatorPending pending[2]; + ResponseCoordinator::clear(pending, 2); + BotMessage first = make_message("#bot", "!ping"); + BotMessage second = first; + second.sender_timestamp++; + BotFingerprint first_request = FirmwareBot::fingerprintFor(first); + BotFingerprint second_request = FirmwareBot::fingerprintFor(second); + BotFingerprint response = FirmwareBot::responseFingerprintFor(first, "Pong!", 5); + BotFingerprint scheduled; + uint32_t first_due = 0; + uint32_t second_due = 0; + + 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, + 0x01020304UL, 0, &scheduled, &second_due) == BOT_COORDINATOR_SCHEDULED); + assert(ResponseCoordinator::cancel(pending, 2, first_request)); + assert(!ResponseCoordinator::cancel(pending, 2, first_request)); + BotCoordinatorReady 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 == response.value); +} + +static void test_response_coordinator_delay_biases() { + BotMessage message = make_message("#bot", "!ping"); + BotFingerprint request = { 0x0123456789ABCDEFULL }; + uint32_t first = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0); + uint32_t second = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x05060708UL, 0, 0); + assert(first != second); + assert(ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x01020304UL, 2, + BOT_RESPONSE_DELAY_JITTER_MILLIS + 17) == first + 300 + 17); +} + +static void test_response_coordinator_recent_responses() { + BotCoordinatorRecent recent[1]; + ResponseCoordinator::clearRecent(recent, 1); + BotFingerprint response = { 0xFEDCBA9876543210ULL }; + BotFingerprint zero = { 0 }; + + assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000)); + ResponseCoordinator::recordRecent(recent, 1, zero, 1000); + assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000)); + 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)); + assert(!ResponseCoordinator::recentlySent(recent, 1, response, 1000 + BOT_RESPONSE_RECENT_TTL_MILLIS)); +} + +static void test_response_coordinator_rejects_non_normal() { + BotCoordinatorPending pending[1]; + ResponseCoordinator::clear(pending, 1); + BotMessage bot_message = make_message("#bot", "!ping"); + BotMessage public_message = make_message("Public", "!ping"); + BotMessage emergency_message = make_message("#emergency", "need help"); + BotFingerprint request = FirmwareBot::fingerprintFor(bot_message); + BotFingerprint response = FirmwareBot::responseFingerprintFor(bot_message, "Pong!", 5); + BotFingerprint zero = { 0 }; + BotFingerprint scheduled; + uint32_t due = 99; + + assert(ResponseCoordinator::schedule(pending, 1, public_message, BOT_COMMAND_PING, request, response, 1000, 0, + 0, 0, &scheduled, &due) == BOT_COORDINATOR_NOT_NORMAL); + assert(scheduled.value == 0); + assert(due == 0); + assert(ResponseCoordinator::schedule(pending, 1, emergency_message, BOT_COMMAND_PING, request, response, 1000, 0, + 0, 0, &scheduled, &due) == BOT_COORDINATOR_NOT_NORMAL); + assert(ResponseCoordinator::schedule(pending, 1, bot_message, BOT_COMMAND_PING, zero, response, 1000, 0, + 0, 0, &scheduled, &due) == BOT_COORDINATOR_NOT_NORMAL); + assert(ResponseCoordinator::schedule(pending, 1, bot_message, BOT_COMMAND_PING, request, zero, 1000, 0, + 0, 0, &scheduled, &due) == BOT_COORDINATOR_NOT_NORMAL); } int main() { @@ -349,7 +675,19 @@ int main() { test_emergency_forward_loop_prevention(); test_emergency_forward_multipart(); test_emergency_forward_truncation(); + test_known_bot_registry(); + test_known_bot_registry_ambiguous_short_prefix(); test_fingerprint(); + test_response_fingerprint(); + test_authoritative_suppression_flow(); + test_response_coordinator_schedule_poll(); + test_response_coordinator_distinct_requests_same_response(); + test_response_coordinator_suppression_uses_response_fingerprint(); + test_response_coordinator_suppress_expire_full(); + test_response_coordinator_cancel_by_request(); + test_response_coordinator_delay_biases(); + test_response_coordinator_recent_responses(); + test_response_coordinator_rejects_non_normal(); printf("firmware_bot tests passed\n"); return 0; }