mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 08:10:29 +00:00
forge: step 7 — add response coordinator
This commit is contained in:
766
patches/meshcore/0005-Add-response-coordinator.patch
Normal file
766
patches/meshcore/0005-Add-response-coordinator.patch
Normal 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);
|
||||||
|
+
|
||||||
|
+}
|
||||||
@@ -24,6 +24,8 @@ def main():
|
|||||||
str(SRC_DIR / "BotPolicy.cpp"),
|
str(SRC_DIR / "BotPolicy.cpp"),
|
||||||
str(SRC_DIR / "BotCommands.cpp"),
|
str(SRC_DIR / "BotCommands.cpp"),
|
||||||
str(SRC_DIR / "EmergencyForwarder.cpp"),
|
str(SRC_DIR / "EmergencyForwarder.cpp"),
|
||||||
|
str(SRC_DIR / "KnownBotRegistry.cpp"),
|
||||||
|
str(SRC_DIR / "ResponseCoordinator.cpp"),
|
||||||
"-o",
|
"-o",
|
||||||
str(binary),
|
str(binary),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "BotPolicy.h"
|
#include "BotPolicy.h"
|
||||||
#include "EmergencyForwarder.h"
|
#include "EmergencyForwarder.h"
|
||||||
#include "FirmwareBot.h"
|
#include "FirmwareBot.h"
|
||||||
|
#include "KnownBotRegistry.h"
|
||||||
|
#include "ResponseCoordinator.h"
|
||||||
|
|
||||||
static void test_channel_policy() {
|
static void test_channel_policy() {
|
||||||
assert(BotPolicy::classifyChannel(NULL, 0, true) == BOT_CHANNEL_DM);
|
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);
|
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() {
|
static void test_fingerprint() {
|
||||||
BotMessage a = make_message("#bot", "!PING");
|
BotMessage a = make_message("#bot", "!PING");
|
||||||
BotMessage b = make_message("bot", "!ping");
|
BotMessage b = make_message("bot", "!ping");
|
||||||
@@ -332,6 +381,283 @@ static void test_fingerprint() {
|
|||||||
|
|
||||||
b.sender_key_prefix[0] = 99;
|
b.sender_key_prefix[0] = 99;
|
||||||
assert(fa.value != FirmwareBot::fingerprintFor(b).value);
|
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() {
|
int main() {
|
||||||
@@ -349,7 +675,19 @@ int main() {
|
|||||||
test_emergency_forward_loop_prevention();
|
test_emergency_forward_loop_prevention();
|
||||||
test_emergency_forward_multipart();
|
test_emergency_forward_multipart();
|
||||||
test_emergency_forward_truncation();
|
test_emergency_forward_truncation();
|
||||||
|
test_known_bot_registry();
|
||||||
|
test_known_bot_registry_ambiguous_short_prefix();
|
||||||
test_fingerprint();
|
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");
|
printf("firmware_bot tests passed\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user