forge: step 7 — add response coordinator

This commit is contained in:
cj-vana
2026-05-14 20:03:13 -06:00
parent e78dcbbce8
commit c37e006732
3 changed files with 1106 additions and 0 deletions

View File

@@ -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 <string.h>
+
+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 <string.h>
+
+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);
+
+}

View File

@@ -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),
]

View File

@@ -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;
}