mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 08:10:29 +00:00
1168 lines
52 KiB
Diff
1168 lines
52 KiB
Diff
diff --git a/examples/companion_radio/BotCommands.cpp b/examples/companion_radio/BotCommands.cpp
|
|
index 5b13fbfe..2c206a9f 100644
|
|
--- a/examples/companion_radio/BotCommands.cpp
|
|
+++ b/examples/companion_radio/BotCommands.cpp
|
|
@@ -159,8 +159,11 @@ BotCommandResult executeCommand(const BotCommand& command, const BotCommandConte
|
|
(unsigned long)context.storage_total_kb, (unsigned long)context.observed_messages,
|
|
(unsigned long)context.sent_messages, (unsigned long)context.send_failures);
|
|
case BOT_COMMAND_CHANNELS:
|
|
- return writeFormatted(output, output_len, "Configured channels: %u. Normal bot replies: DM, #bot, #testing.",
|
|
- (unsigned)context.channel_count);
|
|
+ return writeFormatted(output, output_len, "Channels: %s %s emergency=%s public=%s (%u configured)",
|
|
+ context.bot_channel[0] ? context.bot_channel : "#bot",
|
|
+ context.testing_channel[0] ? context.testing_channel : "#testing",
|
|
+ context.emergency_channel[0] ? context.emergency_channel : "#emergency",
|
|
+ context.public_channel[0] ? context.public_channel : "Public", (unsigned)context.channel_count);
|
|
case BOT_COMMAND_UNKNOWN:
|
|
return writeText(output, output_len, "Unknown command. Try !help");
|
|
default:
|
|
diff --git a/examples/companion_radio/BotPolicy.cpp b/examples/companion_radio/BotPolicy.cpp
|
|
index 1cf75c73..66d338ef 100644
|
|
--- a/examples/companion_radio/BotPolicy.cpp
|
|
+++ b/examples/companion_radio/BotPolicy.cpp
|
|
@@ -49,6 +49,15 @@ BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message
|
|
return BOT_CHANNEL_OTHER;
|
|
}
|
|
|
|
+BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message, const BotPrefs& prefs) {
|
|
+ if (direct_message) return BOT_CHANNEL_DM;
|
|
+ if (equalsExact(name, len, prefs.public_channel)) return BOT_CHANNEL_PUBLIC;
|
|
+ if (equalsIgnoreCase(name, len, prefs.bot_channel)) return BOT_CHANNEL_BOT;
|
|
+ if (equalsIgnoreCase(name, len, prefs.testing_channel)) return BOT_CHANNEL_TESTING;
|
|
+ if (equalsExact(name, len, prefs.emergency_channel)) return BOT_CHANNEL_EMERGENCY;
|
|
+ return BOT_CHANNEL_OTHER;
|
|
+}
|
|
+
|
|
BotPolicyDecision decide(BotChannelKind kind) {
|
|
if (kind == BOT_CHANNEL_DM || kind == BOT_CHANNEL_BOT || kind == BOT_CHANNEL_TESTING) {
|
|
return BOT_POLICY_ALLOW_NORMAL;
|
|
diff --git a/examples/companion_radio/BotPolicy.h b/examples/companion_radio/BotPolicy.h
|
|
index e074e3eb..418ae3ac 100644
|
|
--- a/examples/companion_radio/BotPolicy.h
|
|
+++ b/examples/companion_radio/BotPolicy.h
|
|
@@ -5,6 +5,7 @@
|
|
namespace BotPolicy {
|
|
|
|
BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message);
|
|
+BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message, const BotPrefs& prefs);
|
|
BotPolicyDecision decide(BotChannelKind kind);
|
|
bool isNormalAllowed(BotChannelKind kind);
|
|
bool isEmergency(BotChannelKind kind);
|
|
diff --git a/examples/companion_radio/BotPrefs.cpp b/examples/companion_radio/BotPrefs.cpp
|
|
new file mode 100644
|
|
index 00000000..1d0810a8
|
|
--- /dev/null
|
|
+++ b/examples/companion_radio/BotPrefs.cpp
|
|
@@ -0,0 +1,389 @@
|
|
+#include "BotPrefs.h"
|
|
+
|
|
+#include <ctype.h>
|
|
+#include <string.h>
|
|
+
|
|
+namespace {
|
|
+
|
|
+const size_t SERIALIZED_SIZE = BOT_PREFS_SERIALIZED_SIZE;
|
|
+
|
|
+size_t boundedStrLen(const char* value, size_t max_len) {
|
|
+ size_t len = 0;
|
|
+ while (value && len < max_len && value[len] != 0) len++;
|
|
+ return len;
|
|
+}
|
|
+
|
|
+void copyString(char* dest, size_t dest_len, const char* src) {
|
|
+ if (!dest || dest_len == 0) return;
|
|
+ size_t len = boundedStrLen(src, dest_len - 1);
|
|
+ if (len > 0) memcpy(dest, src, len);
|
|
+ dest[len] = 0;
|
|
+}
|
|
+
|
|
+bool channelNameEqual(const char* lhs, const char* rhs) {
|
|
+ size_t lhs_len = boundedStrLen(lhs, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ size_t rhs_len = boundedStrLen(rhs, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ if (lhs_len != rhs_len || lhs_len == 0) return false;
|
|
+ return memcmp(lhs, rhs, lhs_len) == 0;
|
|
+}
|
|
+
|
|
+bool channelHasShape(const char* value, bool public_channel) {
|
|
+ size_t len = boundedStrLen(value, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ if (len == 0 || len > BOT_MAX_CHANNEL_NAME_LEN) return false;
|
|
+ if (public_channel) return value[0] != '#';
|
|
+ return value[0] == '#' && len > 1;
|
|
+}
|
|
+
|
|
+uint32_t checksumBytes(const uint8_t* data, size_t len) {
|
|
+ uint32_t hash = 2166136261UL;
|
|
+ for (size_t i = 0; i < len; i++) {
|
|
+ hash ^= data[i];
|
|
+ hash *= 16777619UL;
|
|
+ }
|
|
+ return hash;
|
|
+}
|
|
+
|
|
+void put8(uint8_t* data, size_t& pos, uint8_t value) {
|
|
+ data[pos++] = value;
|
|
+}
|
|
+
|
|
+void put16(uint8_t* data, size_t& pos, uint16_t value) {
|
|
+ data[pos++] = (uint8_t)(value & 0xFF);
|
|
+ data[pos++] = (uint8_t)(value >> 8);
|
|
+}
|
|
+
|
|
+void put32(uint8_t* data, size_t& pos, uint32_t value) {
|
|
+ data[pos++] = (uint8_t)(value & 0xFF);
|
|
+ data[pos++] = (uint8_t)((value >> 8) & 0xFF);
|
|
+ data[pos++] = (uint8_t)((value >> 16) & 0xFF);
|
|
+ data[pos++] = (uint8_t)((value >> 24) & 0xFF);
|
|
+}
|
|
+
|
|
+uint8_t get8(const uint8_t* data, size_t& pos) {
|
|
+ return data[pos++];
|
|
+}
|
|
+
|
|
+uint16_t get16(const uint8_t* data, size_t& pos) {
|
|
+ uint16_t value = data[pos];
|
|
+ value |= ((uint16_t)data[pos + 1]) << 8;
|
|
+ pos += 2;
|
|
+ return value;
|
|
+}
|
|
+
|
|
+uint32_t get32(const uint8_t* data, size_t& pos) {
|
|
+ uint32_t value = data[pos];
|
|
+ value |= ((uint32_t)data[pos + 1]) << 8;
|
|
+ value |= ((uint32_t)data[pos + 2]) << 16;
|
|
+ value |= ((uint32_t)data[pos + 3]) << 24;
|
|
+ pos += 4;
|
|
+ return value;
|
|
+}
|
|
+
|
|
+void putFixedString(uint8_t* data, size_t& pos, const char* value, size_t fixed_len) {
|
|
+ memset(&data[pos], 0, fixed_len);
|
|
+ size_t len = boundedStrLen(value, fixed_len);
|
|
+ if (len > 0) memcpy(&data[pos], value, len);
|
|
+ pos += fixed_len;
|
|
+}
|
|
+
|
|
+void getFixedString(const uint8_t* data, size_t& pos, char* value, size_t fixed_len) {
|
|
+ memcpy(value, &data[pos], fixed_len);
|
|
+ value[fixed_len - 1] = 0;
|
|
+ pos += fixed_len;
|
|
+}
|
|
+
|
|
+int hexValue(char c) {
|
|
+ if (c >= '0' && c <= '9') return c - '0';
|
|
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
+ return -1;
|
|
+}
|
|
+
|
|
+bool sameKeyPrefix(const uint8_t lhs[BOT_SENDER_KEY_PREFIX_LEN], const uint8_t rhs[BOT_SENDER_KEY_PREFIX_LEN]) {
|
|
+ return memcmp(lhs, rhs, BOT_SENDER_KEY_PREFIX_LEN) == 0;
|
|
+}
|
|
+
|
|
+}
|
|
+
|
|
+namespace BotPrefsCodec {
|
|
+
|
|
+size_t serializedSize() {
|
|
+ return SERIALIZED_SIZE;
|
|
+}
|
|
+
|
|
+void defaults(BotPrefs& prefs) {
|
|
+ memset(&prefs, 0, sizeof(prefs));
|
|
+ prefs.enabled = true;
|
|
+ prefs.normal_delay_ms = BOT_RESPONSE_DELAY_BASE_MILLIS;
|
|
+ prefs.normal_jitter_ms = BOT_RESPONSE_DELAY_JITTER_MILLIS;
|
|
+ prefs.local_advert_interval_ms = BOT_PREFS_DEFAULT_LOCAL_ADVERT_MILLIS;
|
|
+ prefs.flood_advert_interval_ms = BOT_PREFS_DEFAULT_FLOOD_ADVERT_MILLIS;
|
|
+ prefs.command_mask = BOT_COMMAND_MASK_ALL;
|
|
+ prefs.max_response_parts = BOT_EMERGENCY_MAX_PARTS;
|
|
+ copyString(prefs.bot_channel, sizeof(prefs.bot_channel), "#bot");
|
|
+ copyString(prefs.testing_channel, sizeof(prefs.testing_channel), "#testing");
|
|
+ copyString(prefs.emergency_channel, sizeof(prefs.emergency_channel), "#emergency");
|
|
+ copyString(prefs.public_channel, sizeof(prefs.public_channel), "Public");
|
|
+}
|
|
+
|
|
+void validate(BotPrefs& prefs) {
|
|
+ prefs.normal_delay_ms = prefs.normal_delay_ms > BOT_PREFS_MAX_DELAY_MILLIS ? BOT_PREFS_MAX_DELAY_MILLIS : prefs.normal_delay_ms;
|
|
+ prefs.normal_jitter_ms = prefs.normal_jitter_ms > BOT_PREFS_MAX_DELAY_MILLIS ? BOT_PREFS_MAX_DELAY_MILLIS : prefs.normal_jitter_ms;
|
|
+ if (prefs.local_advert_interval_ms > BOT_PREFS_MAX_ADVERT_MILLIS) prefs.local_advert_interval_ms = BOT_PREFS_MAX_ADVERT_MILLIS;
|
|
+ if (prefs.flood_advert_interval_ms > BOT_PREFS_MAX_ADVERT_MILLIS) prefs.flood_advert_interval_ms = BOT_PREFS_MAX_ADVERT_MILLIS;
|
|
+ prefs.command_mask &= BOT_COMMAND_MASK_ALL;
|
|
+ if (prefs.max_response_parts == 0 || prefs.max_response_parts > BOT_EMERGENCY_MAX_PARTS) {
|
|
+ prefs.max_response_parts = BOT_EMERGENCY_MAX_PARTS;
|
|
+ }
|
|
+ if (!channelHasShape(prefs.bot_channel, false)) copyString(prefs.bot_channel, sizeof(prefs.bot_channel), "#bot");
|
|
+ if (!channelHasShape(prefs.testing_channel, false)) copyString(prefs.testing_channel, sizeof(prefs.testing_channel), "#testing");
|
|
+ if (!channelHasShape(prefs.emergency_channel, false)) copyString(prefs.emergency_channel, sizeof(prefs.emergency_channel), "#emergency");
|
|
+ if (!channelHasShape(prefs.public_channel, true)) copyString(prefs.public_channel, sizeof(prefs.public_channel), "Public");
|
|
+ prefs.bot_channel[BOT_MAX_CHANNEL_NAME_LEN] = 0;
|
|
+ prefs.testing_channel[BOT_MAX_CHANNEL_NAME_LEN] = 0;
|
|
+ prefs.emergency_channel[BOT_MAX_CHANNEL_NAME_LEN] = 0;
|
|
+ prefs.public_channel[BOT_MAX_CHANNEL_NAME_LEN] = 0;
|
|
+ if (!channelConfigValid(prefs)) {
|
|
+ copyString(prefs.bot_channel, sizeof(prefs.bot_channel), "#bot");
|
|
+ copyString(prefs.testing_channel, sizeof(prefs.testing_channel), "#testing");
|
|
+ copyString(prefs.emergency_channel, sizeof(prefs.emergency_channel), "#emergency");
|
|
+ copyString(prefs.public_channel, sizeof(prefs.public_channel), "Public");
|
|
+ }
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ prefs.known_bots[i].flags &= BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL;
|
|
+ prefs.known_bots[i].label[BOT_KNOWN_BOT_LABEL_LEN - 1] = 0;
|
|
+ }
|
|
+}
|
|
+
|
|
+bool channelNameValid(const char* value, bool public_channel) {
|
|
+ return channelHasShape(value, public_channel);
|
|
+}
|
|
+
|
|
+bool channelConfigValid(const BotPrefs& prefs) {
|
|
+ return channelHasShape(prefs.bot_channel, false) && channelHasShape(prefs.testing_channel, false) &&
|
|
+ channelHasShape(prefs.emergency_channel, false) && channelHasShape(prefs.public_channel, true) &&
|
|
+ !channelNameEqual(prefs.bot_channel, prefs.testing_channel) &&
|
|
+ !channelNameEqual(prefs.bot_channel, prefs.emergency_channel) &&
|
|
+ !channelNameEqual(prefs.testing_channel, prefs.emergency_channel) &&
|
|
+ !channelNameEqual(prefs.bot_channel, prefs.public_channel) &&
|
|
+ !channelNameEqual(prefs.testing_channel, prefs.public_channel) &&
|
|
+ !channelNameEqual(prefs.emergency_channel, prefs.public_channel);
|
|
+}
|
|
+
|
|
+bool serialize(const BotPrefs& prefs, uint8_t* output, size_t output_len) {
|
|
+ if (!output || output_len < SERIALIZED_SIZE) return false;
|
|
+
|
|
+ BotPrefs clean = prefs;
|
|
+ validate(clean);
|
|
+
|
|
+ memset(output, 0, output_len);
|
|
+ size_t pos = 0;
|
|
+ put32(output, pos, BOT_PREFS_MAGIC);
|
|
+ put16(output, pos, BOT_PREFS_VERSION);
|
|
+ put16(output, pos, (uint16_t)SERIALIZED_SIZE);
|
|
+ size_t checksum_pos = pos;
|
|
+ put32(output, pos, 0);
|
|
+ put8(output, pos, clean.enabled ? 1 : 0);
|
|
+ put16(output, pos, clean.normal_delay_ms);
|
|
+ put16(output, pos, clean.normal_jitter_ms);
|
|
+ put32(output, pos, clean.local_advert_interval_ms);
|
|
+ put32(output, pos, clean.flood_advert_interval_ms);
|
|
+ put32(output, pos, clean.command_mask);
|
|
+ put8(output, pos, clean.max_response_parts);
|
|
+ putFixedString(output, pos, clean.bot_channel, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ putFixedString(output, pos, clean.testing_channel, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ putFixedString(output, pos, clean.emergency_channel, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ putFixedString(output, pos, clean.public_channel, BOT_MAX_CHANNEL_NAME_LEN + 1);
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ put8(output, pos, clean.known_bots[i].active ? 1 : 0);
|
|
+ memcpy(&output[pos], clean.known_bots[i].key_prefix, BOT_SENDER_KEY_PREFIX_LEN);
|
|
+ pos += BOT_SENDER_KEY_PREFIX_LEN;
|
|
+ put8(output, pos, clean.known_bots[i].flags);
|
|
+ putFixedString(output, pos, clean.known_bots[i].label, BOT_KNOWN_BOT_LABEL_LEN);
|
|
+ }
|
|
+ put32(output, pos, clean.prefs_load_failures);
|
|
+ put32(output, pos, clean.prefs_save_failures);
|
|
+
|
|
+ if (pos != SERIALIZED_SIZE) return false;
|
|
+ uint32_t checksum = checksumBytes(&output[12], SERIALIZED_SIZE - 12);
|
|
+ size_t write_pos = checksum_pos;
|
|
+ put32(output, write_pos, checksum);
|
|
+ return true;
|
|
+}
|
|
+
|
|
+bool deserialize(const uint8_t* data, size_t data_len, BotPrefs& prefs) {
|
|
+ if (!data || data_len != SERIALIZED_SIZE) {
|
|
+ defaults(prefs);
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ size_t pos = 0;
|
|
+ uint32_t magic = get32(data, pos);
|
|
+ uint16_t version = get16(data, pos);
|
|
+ uint16_t length = get16(data, pos);
|
|
+ uint32_t checksum = get32(data, pos);
|
|
+ if (magic != BOT_PREFS_MAGIC || version != BOT_PREFS_VERSION || length != SERIALIZED_SIZE) {
|
|
+ defaults(prefs);
|
|
+ return false;
|
|
+ }
|
|
+ if (checksumBytes(&data[12], SERIALIZED_SIZE - 12) != checksum) {
|
|
+ defaults(prefs);
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ BotPrefs loaded;
|
|
+ memset(&loaded, 0, sizeof(loaded));
|
|
+ loaded.enabled = get8(data, pos) != 0;
|
|
+ loaded.normal_delay_ms = get16(data, pos);
|
|
+ loaded.normal_jitter_ms = get16(data, pos);
|
|
+ loaded.local_advert_interval_ms = get32(data, pos);
|
|
+ loaded.flood_advert_interval_ms = get32(data, pos);
|
|
+ loaded.command_mask = get32(data, pos);
|
|
+ loaded.max_response_parts = get8(data, pos);
|
|
+ getFixedString(data, pos, loaded.bot_channel, sizeof(loaded.bot_channel));
|
|
+ getFixedString(data, pos, loaded.testing_channel, sizeof(loaded.testing_channel));
|
|
+ getFixedString(data, pos, loaded.emergency_channel, sizeof(loaded.emergency_channel));
|
|
+ getFixedString(data, pos, loaded.public_channel, sizeof(loaded.public_channel));
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ loaded.known_bots[i].active = get8(data, pos) != 0;
|
|
+ memcpy(loaded.known_bots[i].key_prefix, &data[pos], BOT_SENDER_KEY_PREFIX_LEN);
|
|
+ pos += BOT_SENDER_KEY_PREFIX_LEN;
|
|
+ loaded.known_bots[i].flags = get8(data, pos);
|
|
+ getFixedString(data, pos, loaded.known_bots[i].label, sizeof(loaded.known_bots[i].label));
|
|
+ }
|
|
+ loaded.prefs_load_failures = get32(data, pos);
|
|
+ loaded.prefs_save_failures = get32(data, pos);
|
|
+ if (pos != SERIALIZED_SIZE) {
|
|
+ defaults(prefs);
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ validate(loaded);
|
|
+ prefs = loaded;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+uint32_t commandMaskFor(BotCommandId command_id) {
|
|
+ if (command_id <= BOT_COMMAND_NONE || command_id > BOT_COMMAND_UNKNOWN) return 0;
|
|
+ return 1UL << command_id;
|
|
+}
|
|
+
|
|
+bool commandEnabled(const BotPrefs& prefs, BotCommandId command_id) {
|
|
+ uint32_t mask = commandMaskFor(command_id);
|
|
+ return mask != 0 && (prefs.command_mask & mask) != 0;
|
|
+}
|
|
+
|
|
+void setCommandEnabled(BotPrefs& prefs, BotCommandId command_id, bool enabled) {
|
|
+ uint32_t mask = commandMaskFor(command_id);
|
|
+ if (mask == 0) return;
|
|
+ if (enabled) {
|
|
+ prefs.command_mask |= mask;
|
|
+ } else {
|
|
+ prefs.command_mask &= ~mask;
|
|
+ }
|
|
+ validate(prefs);
|
|
+}
|
|
+
|
|
+const char* commandName(BotCommandId command_id) {
|
|
+ switch (command_id) {
|
|
+ case BOT_COMMAND_HELP: return "help";
|
|
+ case BOT_COMMAND_PING: return "ping";
|
|
+ case BOT_COMMAND_TEST: return "test";
|
|
+ case BOT_COMMAND_HELLO: return "hello";
|
|
+ case BOT_COMMAND_ABOUT: return "about";
|
|
+ case BOT_COMMAND_DICE: return "roll";
|
|
+ case BOT_COMMAND_STATUS: return "status";
|
|
+ case BOT_COMMAND_CHANNELS: return "channels";
|
|
+ case BOT_COMMAND_UNKNOWN: return "unknown";
|
|
+ default: return "";
|
|
+ }
|
|
+}
|
|
+
|
|
+bool commandIdForName(const char* name, BotCommandId* command_id) {
|
|
+ if (!name || !command_id) return false;
|
|
+ for (uint8_t id = BOT_COMMAND_HELP; id <= BOT_COMMAND_UNKNOWN; id++) {
|
|
+ const char* candidate = commandName((BotCommandId)id);
|
|
+ if (candidate[0] == 0) continue;
|
|
+ size_t len = boundedStrLen(name, BOT_MAX_COMMAND_NAME_LEN + 1);
|
|
+ size_t candidate_len = boundedStrLen(candidate, BOT_MAX_COMMAND_NAME_LEN + 1);
|
|
+ if (len != candidate_len) continue;
|
|
+ bool match = true;
|
|
+ for (size_t i = 0; i < len; i++) {
|
|
+ if (tolower((unsigned char)name[i]) != tolower((unsigned char)candidate[i])) {
|
|
+ match = false;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ if (match) {
|
|
+ *command_id = (BotCommandId)id;
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+}
|
|
+
|
|
+bool parseKeyPrefixHex(const char* text, uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]) {
|
|
+ if (!text || !key_prefix) return false;
|
|
+ for (size_t i = 0; i < BOT_SENDER_KEY_PREFIX_LEN; i++) {
|
|
+ int high = hexValue(text[i * 2]);
|
|
+ int low = hexValue(text[i * 2 + 1]);
|
|
+ if (high < 0 || low < 0) return false;
|
|
+ key_prefix[i] = (uint8_t)((high << 4) | low);
|
|
+ }
|
|
+ return text[BOT_SENDER_KEY_PREFIX_LEN * 2] == 0;
|
|
+}
|
|
+
|
|
+void formatKeyPrefixHex(const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], char* output, size_t output_len) {
|
|
+ static const char hex[] = "0123456789abcdef";
|
|
+ if (!output || output_len == 0) return;
|
|
+ if (!key_prefix || output_len < BOT_SENDER_KEY_PREFIX_LEN * 2 + 1) {
|
|
+ output[0] = 0;
|
|
+ return;
|
|
+ }
|
|
+ for (size_t i = 0; i < BOT_SENDER_KEY_PREFIX_LEN; i++) {
|
|
+ output[i * 2] = hex[key_prefix[i] >> 4];
|
|
+ output[i * 2 + 1] = hex[key_prefix[i] & 0x0F];
|
|
+ }
|
|
+ output[BOT_SENDER_KEY_PREFIX_LEN * 2] = 0;
|
|
+}
|
|
+
|
|
+const BotKnownBotEntry* findKnownBot(const BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]) {
|
|
+ if (!key_prefix) return NULL;
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ if (prefs.known_bots[i].active && sameKeyPrefix(prefs.known_bots[i].key_prefix, key_prefix)) return &prefs.known_bots[i];
|
|
+ }
|
|
+ return NULL;
|
|
+}
|
|
+
|
|
+bool addKnownBot(BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], uint8_t flags,
|
|
+ const char* label) {
|
|
+ if (!key_prefix) return false;
|
|
+ size_t slot = BOT_KNOWN_BOT_SLOTS;
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ if (prefs.known_bots[i].active && sameKeyPrefix(prefs.known_bots[i].key_prefix, key_prefix)) {
|
|
+ slot = i;
|
|
+ break;
|
|
+ }
|
|
+ if (slot == BOT_KNOWN_BOT_SLOTS && !prefs.known_bots[i].active) slot = i;
|
|
+ }
|
|
+ if (slot == BOT_KNOWN_BOT_SLOTS) return false;
|
|
+
|
|
+ prefs.known_bots[slot].active = true;
|
|
+ memcpy(prefs.known_bots[slot].key_prefix, key_prefix, BOT_SENDER_KEY_PREFIX_LEN);
|
|
+ prefs.known_bots[slot].flags = flags & BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL;
|
|
+ copyString(prefs.known_bots[slot].label, sizeof(prefs.known_bots[slot].label), label ? label : "bot");
|
|
+ return true;
|
|
+}
|
|
+
|
|
+bool removeKnownBot(BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]) {
|
|
+ if (!key_prefix) return false;
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ if (prefs.known_bots[i].active && sameKeyPrefix(prefs.known_bots[i].key_prefix, key_prefix)) {
|
|
+ memset(&prefs.known_bots[i], 0, sizeof(prefs.known_bots[i]));
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+}
|
|
+
|
|
+}
|
|
diff --git a/examples/companion_radio/BotPrefs.h b/examples/companion_radio/BotPrefs.h
|
|
new file mode 100644
|
|
index 00000000..676f7de2
|
|
--- /dev/null
|
|
+++ b/examples/companion_radio/BotPrefs.h
|
|
@@ -0,0 +1,31 @@
|
|
+#pragma once
|
|
+
|
|
+#include "BotTypes.h"
|
|
+
|
|
+#include <stddef.h>
|
|
+#include <stdint.h>
|
|
+
|
|
+namespace BotPrefsCodec {
|
|
+
|
|
+size_t serializedSize();
|
|
+void defaults(BotPrefs& prefs);
|
|
+void validate(BotPrefs& prefs);
|
|
+bool serialize(const BotPrefs& prefs, uint8_t* output, size_t output_len);
|
|
+bool deserialize(const uint8_t* data, size_t data_len, BotPrefs& prefs);
|
|
+bool channelNameValid(const char* value, bool public_channel);
|
|
+bool channelConfigValid(const BotPrefs& prefs);
|
|
+
|
|
+uint32_t commandMaskFor(BotCommandId command_id);
|
|
+bool commandEnabled(const BotPrefs& prefs, BotCommandId command_id);
|
|
+void setCommandEnabled(BotPrefs& prefs, BotCommandId command_id, bool enabled);
|
|
+const char* commandName(BotCommandId command_id);
|
|
+bool commandIdForName(const char* name, BotCommandId* command_id);
|
|
+
|
|
+bool parseKeyPrefixHex(const char* text, uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]);
|
|
+void formatKeyPrefixHex(const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], char* output, size_t output_len);
|
|
+bool addKnownBot(BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], uint8_t flags,
|
|
+ const char* label);
|
|
+bool removeKnownBot(BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]);
|
|
+const BotKnownBotEntry* findKnownBot(const BotPrefs& prefs, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]);
|
|
+
|
|
+}
|
|
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
|
index d44aadb2..98d4a583 100644
|
|
--- a/examples/companion_radio/BotTypes.h
|
|
+++ b/examples/companion_radio/BotTypes.h
|
|
@@ -27,6 +27,27 @@
|
|
#define BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL 0x01
|
|
#define BOT_SENDER_KEY_PREFIX_LEN 6
|
|
#define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4
|
|
+#define BOT_KNOWN_BOT_LABEL_LEN 12
|
|
+#define BOT_PREFS_MAGIC 0x31504642UL
|
|
+#define BOT_PREFS_VERSION 1
|
|
+#define BOT_PREFS_DEFAULT_LOCAL_ADVERT_MILLIS (24UL * 60UL * 60UL * 1000UL)
|
|
+#define BOT_PREFS_DEFAULT_FLOOD_ADVERT_MILLIS (24UL * 60UL * 60UL * 1000UL)
|
|
+#define BOT_PREFS_INITIAL_LOCAL_ADVERT_MILLIS 60000UL
|
|
+#define BOT_PREFS_MAX_DELAY_MILLIS 60000U
|
|
+#define BOT_PREFS_MAX_ADVERT_MILLIS (7UL * 24UL * 60UL * 60UL * 1000UL)
|
|
+#define BOT_COMMAND_MASK_HELP (1UL << BOT_COMMAND_HELP)
|
|
+#define BOT_COMMAND_MASK_PING (1UL << BOT_COMMAND_PING)
|
|
+#define BOT_COMMAND_MASK_TEST (1UL << BOT_COMMAND_TEST)
|
|
+#define BOT_COMMAND_MASK_HELLO (1UL << BOT_COMMAND_HELLO)
|
|
+#define BOT_COMMAND_MASK_ABOUT (1UL << BOT_COMMAND_ABOUT)
|
|
+#define BOT_COMMAND_MASK_DICE (1UL << BOT_COMMAND_DICE)
|
|
+#define BOT_COMMAND_MASK_STATUS (1UL << BOT_COMMAND_STATUS)
|
|
+#define BOT_COMMAND_MASK_CHANNELS (1UL << BOT_COMMAND_CHANNELS)
|
|
+#define BOT_COMMAND_MASK_UNKNOWN (1UL << BOT_COMMAND_UNKNOWN)
|
|
+#define BOT_COMMAND_MASK_ALL (BOT_COMMAND_MASK_HELP | BOT_COMMAND_MASK_PING | BOT_COMMAND_MASK_TEST | \
|
|
+ BOT_COMMAND_MASK_HELLO | BOT_COMMAND_MASK_ABOUT | BOT_COMMAND_MASK_DICE | \
|
|
+ BOT_COMMAND_MASK_STATUS | BOT_COMMAND_MASK_CHANNELS | BOT_COMMAND_MASK_UNKNOWN)
|
|
+#define BOT_PREFS_SERIALIZED_SIZE 294
|
|
|
|
enum BotChannelKind : uint8_t {
|
|
BOT_CHANNEL_DM = 0,
|
|
@@ -116,6 +137,10 @@ struct BotResponse {
|
|
|
|
struct BotCommandContext {
|
|
char node_name[BOT_MAX_SENDER_NAME_LEN + 1];
|
|
+ char bot_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char testing_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char emergency_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char public_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
uint32_t uptime_seconds;
|
|
uint16_t battery_millivolts;
|
|
uint32_t storage_used_kb;
|
|
@@ -143,7 +168,7 @@ struct BotKnownBotEntry {
|
|
bool active;
|
|
uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN];
|
|
uint8_t flags;
|
|
- char label[12];
|
|
+ char label[BOT_KNOWN_BOT_LABEL_LEN];
|
|
};
|
|
|
|
struct BotCoordinatorPending {
|
|
@@ -178,11 +203,17 @@ struct BotPrefs {
|
|
bool enabled;
|
|
uint16_t normal_delay_ms;
|
|
uint16_t normal_jitter_ms;
|
|
+ uint32_t local_advert_interval_ms;
|
|
+ uint32_t flood_advert_interval_ms;
|
|
+ uint32_t command_mask;
|
|
uint8_t max_response_parts;
|
|
char bot_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
char testing_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
char emergency_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
char public_channel[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ BotKnownBotEntry known_bots[BOT_KNOWN_BOT_SLOTS];
|
|
+ uint32_t prefs_load_failures;
|
|
+ uint32_t prefs_save_failures;
|
|
};
|
|
|
|
struct BotStats {
|
|
@@ -204,7 +235,7 @@ struct BotStats {
|
|
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(BotCommandContext) <= 192, "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");
|
|
@@ -212,5 +243,5 @@ static_assert(sizeof(BotCoordinatorPending) <= 32, "BotCoordinatorPending RAM bu
|
|
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(BotPrefs) <= 320, "BotPrefs RAM budget exceeded");
|
|
static_assert(sizeof(BotStats) <= 64, "BotStats RAM budget exceeded");
|
|
diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp
|
|
index c7988bb3..9a134389 100644
|
|
--- a/examples/companion_radio/DataStore.cpp
|
|
+++ b/examples/companion_radio/DataStore.cpp
|
|
@@ -1,6 +1,10 @@
|
|
#include <Arduino.h>
|
|
#include "DataStore.h"
|
|
|
|
+#if CMESH_BOT_ENABLED
|
|
+#include "BotPrefs.h"
|
|
+#endif
|
|
+
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
#define MAX_BLOBRECS 100
|
|
#else
|
|
@@ -199,6 +203,38 @@ void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon)
|
|
}
|
|
}
|
|
|
|
+#if CMESH_BOT_ENABLED
|
|
+bool DataStore::loadBotPrefs(BotPrefs& prefs) {
|
|
+ if (!_fs->exists("/bot_prefs_v1")) {
|
|
+ BotPrefsCodec::defaults(prefs);
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ uint8_t data[BOT_PREFS_SERIALIZED_SIZE];
|
|
+ File file = openRead(_fs, "/bot_prefs_v1");
|
|
+ bool success = false;
|
|
+ if (file) {
|
|
+ success = file.size() == sizeof(data) && file.read(data, sizeof(data)) == sizeof(data) &&
|
|
+ BotPrefsCodec::deserialize(data, sizeof(data), prefs);
|
|
+ file.close();
|
|
+ }
|
|
+ if (!success) BotPrefsCodec::defaults(prefs);
|
|
+ return success;
|
|
+}
|
|
+
|
|
+bool DataStore::saveBotPrefs(const BotPrefs& prefs) {
|
|
+ uint8_t data[BOT_PREFS_SERIALIZED_SIZE];
|
|
+ if (!BotPrefsCodec::serialize(prefs, data, sizeof(data))) return false;
|
|
+
|
|
+ File file = openWrite(_fs, "/bot_prefs_v1");
|
|
+ if (!file) return false;
|
|
+ bool success = file.write(data, sizeof(data)) == sizeof(data);
|
|
+ file.close();
|
|
+ if (!success) _fs->remove("/bot_prefs_v1");
|
|
+ return success;
|
|
+}
|
|
+#endif
|
|
+
|
|
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
|
|
File file = openRead(_fs, filename);
|
|
if (file) {
|
|
diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h
|
|
index 58b4d5d2..187cd771 100644
|
|
--- a/examples/companion_radio/DataStore.h
|
|
+++ b/examples/companion_radio/DataStore.h
|
|
@@ -5,6 +5,13 @@
|
|
#include <helpers/ChannelDetails.h>
|
|
#include "NodePrefs.h"
|
|
|
|
+#ifndef CMESH_BOT_ENABLED
|
|
+#define CMESH_BOT_ENABLED 0
|
|
+#endif
|
|
+#if CMESH_BOT_ENABLED
|
|
+#include "BotTypes.h"
|
|
+#endif
|
|
+
|
|
class DataStoreHost {
|
|
public:
|
|
virtual bool onContactLoaded(const ContactInfo& contact) =0;
|
|
@@ -35,6 +42,10 @@ public:
|
|
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
|
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
|
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
|
|
+#if CMESH_BOT_ENABLED
|
|
+ bool loadBotPrefs(BotPrefs& prefs);
|
|
+ bool saveBotPrefs(const BotPrefs& prefs);
|
|
+#endif
|
|
void loadContacts(DataStoreHost* host);
|
|
void saveContacts(DataStoreHost* host);
|
|
void loadChannels(DataStoreHost* host);
|
|
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
|
index 31e9bad8..13ead485 100644
|
|
--- a/examples/companion_radio/MyMesh.cpp
|
|
+++ b/examples/companion_radio/MyMesh.cpp
|
|
@@ -2,10 +2,13 @@
|
|
|
|
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
+#include <ctype.h>
|
|
+#include <stdio.h>
|
|
|
|
#if CMESH_BOT_ENABLED
|
|
#include "BotCommands.h"
|
|
#include "BotPolicy.h"
|
|
+#include "BotPrefs.h"
|
|
#include "EmergencyForwarder.h"
|
|
#include "FirmwareBot.h"
|
|
#include "KnownBotRegistry.h"
|
|
@@ -115,8 +118,6 @@
|
|
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
|
|
|
#if CMESH_BOT_ENABLED
|
|
-#define BOT_AUTO_LOCAL_FIRST_DELAY_MILLIS 60000UL
|
|
-#define BOT_AUTO_ADVERT_INTERVAL_MILLIS (24UL * 60UL * 60UL * 1000UL)
|
|
#endif
|
|
|
|
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
|
|
@@ -127,6 +128,49 @@ static size_t botBoundedStrLen(const char *value, size_t max_len) {
|
|
while (value && len < max_len && value[len] != 0) len++;
|
|
return len;
|
|
}
|
|
+
|
|
+static void botCopyString(char *dest, size_t dest_len, const char *src) {
|
|
+ if (!dest || dest_len == 0) return;
|
|
+ size_t len = botBoundedStrLen(src, dest_len - 1);
|
|
+ if (len > 0) memcpy(dest, src, len);
|
|
+ dest[len] = 0;
|
|
+}
|
|
+
|
|
+static bool botParseU32(const char *text, uint32_t *value, const char **end_out) {
|
|
+ if (!text || !value || !isdigit((unsigned char)text[0])) return false;
|
|
+ uint32_t parsed = 0;
|
|
+ while (isdigit((unsigned char)*text)) {
|
|
+ uint32_t next = parsed * 10UL + (uint32_t)(*text - '0');
|
|
+ if (next < parsed) return false;
|
|
+ parsed = next;
|
|
+ text++;
|
|
+ }
|
|
+ *value = parsed;
|
|
+ if (end_out) *end_out = text;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+static void botSkipSpaces(const char **text) {
|
|
+ while (text && *text && **text == ' ') (*text)++;
|
|
+}
|
|
+
|
|
+static bool botReadToken(const char **text, char *output, size_t output_len) {
|
|
+ if (!text || !*text || !output || output_len == 0) return false;
|
|
+ botSkipSpaces(text);
|
|
+ const char *start = *text;
|
|
+ size_t len = 0;
|
|
+ while (start[len] != 0 && start[len] != ' ') len++;
|
|
+ if (len == 0 || len + 1 > output_len) return false;
|
|
+ memcpy(output, start, len);
|
|
+ output[len] = 0;
|
|
+ *text = start + len;
|
|
+ return true;
|
|
+}
|
|
+
|
|
+static bool botNoMoreTokens(const char *text) {
|
|
+ botSkipSpaces(&text);
|
|
+ return text && *text == 0;
|
|
+}
|
|
#endif
|
|
|
|
// these are _pushed_ to client app at any time
|
|
@@ -559,11 +603,226 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin
|
|
}
|
|
|
|
#if CMESH_BOT_ENABLED
|
|
+void MyMesh::applyBotPrefs() {
|
|
+ BotPrefsCodec::validate(bot_prefs);
|
|
+ KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS);
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ if (bot_prefs.known_bots[i].active) {
|
|
+ KnownBotRegistry::add(known_bot_entries, BOT_KNOWN_BOT_SLOTS, bot_prefs.known_bots[i].key_prefix,
|
|
+ bot_prefs.known_bots[i].flags, bot_prefs.known_bots[i].label);
|
|
+ }
|
|
+ }
|
|
+ if (bot_prefs.enabled) {
|
|
+ scheduleBotLocalAdvert(bot_prefs.local_advert_interval_ms ? BOT_PREFS_INITIAL_LOCAL_ADVERT_MILLIS : 0);
|
|
+ scheduleBotFloodAdvert(bot_prefs.flood_advert_interval_ms);
|
|
+ } else {
|
|
+ scheduleBotLocalAdvert(0);
|
|
+ scheduleBotFloodAdvert(0);
|
|
+ }
|
|
+}
|
|
+
|
|
+bool MyMesh::saveBotPrefs() {
|
|
+ BotPrefsCodec::validate(bot_prefs);
|
|
+ bool success = _store->saveBotPrefs(bot_prefs);
|
|
+ if (!success) bot_prefs.prefs_save_failures++;
|
|
+ return success;
|
|
+}
|
|
+
|
|
+static void printBotPrefsSaveResult(const char *success_message, bool saved) {
|
|
+ Serial.println(saved ? success_message : " Error: bot prefs save failed");
|
|
+}
|
|
+
|
|
+void MyMesh::printBotPrefs() {
|
|
+ Serial.printf(" > bot %s\n", bot_prefs.enabled ? "enabled" : "disabled");
|
|
+ Serial.printf(" > channels bot=%s testing=%s emergency=%s public=%s\n", bot_prefs.bot_channel,
|
|
+ bot_prefs.testing_channel, bot_prefs.emergency_channel, bot_prefs.public_channel);
|
|
+ Serial.printf(" > delay base=%u jitter=%u\n", (unsigned)bot_prefs.normal_delay_ms,
|
|
+ (unsigned)bot_prefs.normal_jitter_ms);
|
|
+ Serial.printf(" > advert local=%lu flood=%lu\n", (unsigned long)bot_prefs.local_advert_interval_ms,
|
|
+ (unsigned long)bot_prefs.flood_advert_interval_ms);
|
|
+}
|
|
+
|
|
+bool MyMesh::handleBotCLI(const char *args) {
|
|
+ if (!args) return false;
|
|
+ botSkipSpaces(&args);
|
|
+ if (*args == 0) {
|
|
+ printBotPrefs();
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "enable") == 0) {
|
|
+ bot_prefs.enabled = true;
|
|
+ applyBotPrefs();
|
|
+ printBotPrefsSaveResult(" > bot enabled", saveBotPrefs());
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "disable") == 0) {
|
|
+ bot_prefs.enabled = false;
|
|
+ ResponseCoordinator::clear(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS);
|
|
+ memset(pending_bot_responses, 0, sizeof(pending_bot_responses));
|
|
+ applyBotPrefs();
|
|
+ printBotPrefsSaveResult(" > bot disabled", saveBotPrefs());
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "channels") == 0) {
|
|
+ Serial.printf(" > %s %s %s %s\n", bot_prefs.bot_channel, bot_prefs.testing_channel,
|
|
+ bot_prefs.emergency_channel, bot_prefs.public_channel);
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "channels ", 9) == 0) {
|
|
+ const char *pos = args + 9;
|
|
+ char bot[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char testing[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char emergency[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ char public_name[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
|
+ BotPrefs updated = bot_prefs;
|
|
+ if (botReadToken(&pos, bot, sizeof(bot)) && botReadToken(&pos, testing, sizeof(testing)) &&
|
|
+ botReadToken(&pos, emergency, sizeof(emergency)) && botReadToken(&pos, public_name, sizeof(public_name)) &&
|
|
+ botNoMoreTokens(pos) && BotPrefsCodec::channelNameValid(bot, false) &&
|
|
+ BotPrefsCodec::channelNameValid(testing, false) && BotPrefsCodec::channelNameValid(emergency, false) &&
|
|
+ BotPrefsCodec::channelNameValid(public_name, true)) {
|
|
+ botCopyString(updated.bot_channel, sizeof(updated.bot_channel), bot);
|
|
+ botCopyString(updated.testing_channel, sizeof(updated.testing_channel), testing);
|
|
+ botCopyString(updated.emergency_channel, sizeof(updated.emergency_channel), emergency);
|
|
+ botCopyString(updated.public_channel, sizeof(updated.public_channel), public_name);
|
|
+ if (BotPrefsCodec::channelConfigValid(updated)) {
|
|
+ bot_prefs = updated;
|
|
+ printBotPrefsSaveResult(" > bot channels saved", saveBotPrefs());
|
|
+ } else {
|
|
+ Serial.println(" Error: duplicate bot channel names");
|
|
+ }
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot channels <bot> <testing> <emergency> <public>");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "delay ", 6) == 0) {
|
|
+ const char *pos = args + 6;
|
|
+ uint32_t base = 0;
|
|
+ uint32_t jitter = 0;
|
|
+ if (botParseU32(pos, &base, &pos)) {
|
|
+ botSkipSpaces(&pos);
|
|
+ if (botParseU32(pos, &jitter, &pos) && *pos == 0 && base <= BOT_PREFS_MAX_DELAY_MILLIS &&
|
|
+ jitter <= BOT_PREFS_MAX_DELAY_MILLIS) {
|
|
+ bot_prefs.normal_delay_ms = (uint16_t)base;
|
|
+ bot_prefs.normal_jitter_ms = (uint16_t)jitter;
|
|
+ printBotPrefsSaveResult(" > bot delay saved", saveBotPrefs());
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot delay <base_ms> <jitter_ms>");
|
|
+ }
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot delay <base_ms> <jitter_ms>");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "advert ", 7) == 0) {
|
|
+ const char *pos = args + 7;
|
|
+ uint32_t local = 0;
|
|
+ uint32_t flood = 0;
|
|
+ if (botParseU32(pos, &local, &pos)) {
|
|
+ botSkipSpaces(&pos);
|
|
+ if (botParseU32(pos, &flood, &pos) && *pos == 0 && local <= BOT_PREFS_MAX_ADVERT_MILLIS &&
|
|
+ flood <= BOT_PREFS_MAX_ADVERT_MILLIS) {
|
|
+ bot_prefs.local_advert_interval_ms = local;
|
|
+ bot_prefs.flood_advert_interval_ms = flood;
|
|
+ applyBotPrefs();
|
|
+ printBotPrefsSaveResult(" > bot advert saved", saveBotPrefs());
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot advert <local_ms> <flood_ms>");
|
|
+ }
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot advert <local_ms> <flood_ms>");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "known list") == 0) {
|
|
+ char hex[BOT_SENDER_KEY_PREFIX_LEN * 2 + 1];
|
|
+ for (size_t i = 0; i < BOT_KNOWN_BOT_SLOTS; i++) {
|
|
+ if (!bot_prefs.known_bots[i].active) continue;
|
|
+ BotPrefsCodec::formatKeyPrefixHex(bot_prefs.known_bots[i].key_prefix, hex, sizeof(hex));
|
|
+ Serial.printf(" > %s %s flags=%u\n", hex, bot_prefs.known_bots[i].label,
|
|
+ (unsigned)bot_prefs.known_bots[i].flags);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "known add ", 10) == 0) {
|
|
+ const char *pos = args + 10;
|
|
+ char key_hex[BOT_SENDER_KEY_PREFIX_LEN * 2 + 1];
|
|
+ char label[BOT_KNOWN_BOT_LABEL_LEN];
|
|
+ label[0] = 0;
|
|
+ if (botReadToken(&pos, key_hex, sizeof(key_hex))) {
|
|
+ bool have_label = botReadToken(&pos, label, sizeof(label));
|
|
+ uint8_t key[BOT_SENDER_KEY_PREFIX_LEN];
|
|
+ if (botNoMoreTokens(pos) && (!have_label || label[0] != 0) && BotPrefsCodec::parseKeyPrefixHex(key_hex, key) &&
|
|
+ BotPrefsCodec::addKnownBot(bot_prefs, key, BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL, have_label ? label : "bot")) {
|
|
+ applyBotPrefs();
|
|
+ printBotPrefsSaveResult(" > known bot saved", saveBotPrefs());
|
|
+ } else {
|
|
+ Serial.println(" Error: known bot table full or invalid key");
|
|
+ }
|
|
+ } else {
|
|
+ Serial.println(" Error: usage bot known add <hex-prefix> [label]");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "known remove ", 13) == 0) {
|
|
+ const char *pos = args + 13;
|
|
+ char key_hex[BOT_SENDER_KEY_PREFIX_LEN * 2 + 1];
|
|
+ uint8_t key[BOT_SENDER_KEY_PREFIX_LEN];
|
|
+ if (botReadToken(&pos, key_hex, sizeof(key_hex)) && botNoMoreTokens(pos) &&
|
|
+ BotPrefsCodec::parseKeyPrefixHex(key_hex, key) && BotPrefsCodec::removeKnownBot(bot_prefs, key)) {
|
|
+ applyBotPrefs();
|
|
+ printBotPrefsSaveResult(" > known bot removed", saveBotPrefs());
|
|
+ } else {
|
|
+ Serial.println(" Error: known bot not found");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "commands") == 0) {
|
|
+ for (uint8_t id = BOT_COMMAND_HELP; id <= BOT_COMMAND_UNKNOWN; id++) {
|
|
+ BotCommandId command_id = (BotCommandId)id;
|
|
+ Serial.printf(" > %s %s\n", BotPrefsCodec::commandName(command_id),
|
|
+ BotPrefsCodec::commandEnabled(bot_prefs, command_id) ? "enabled" : "disabled");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (memcmp(args, "commands enable ", 16) == 0 || memcmp(args, "commands disable ", 17) == 0) {
|
|
+ bool enable = memcmp(args, "commands enable ", 16) == 0;
|
|
+ const char *name = args + (enable ? 16 : 17);
|
|
+ BotCommandId command_id;
|
|
+ if (BotPrefsCodec::commandIdForName(name, &command_id)) {
|
|
+ BotPrefsCodec::setCommandEnabled(bot_prefs, command_id, enable);
|
|
+ if (saveBotPrefs()) {
|
|
+ Serial.printf(" > command %s %s\n", BotPrefsCodec::commandName(command_id), enable ? "enabled" : "disabled");
|
|
+ } else {
|
|
+ Serial.println(" Error: bot prefs save failed");
|
|
+ }
|
|
+ } else {
|
|
+ Serial.println(" Error: unknown bot command");
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "stats") == 0) {
|
|
+ Serial.printf(" > observed=%lu ignored=%lu eligible=%lu sent=%lu failed=%lu suppressed=%lu emergency=%lu/%lu\n",
|
|
+ (unsigned long)bot_stats.observed_messages, (unsigned long)bot_stats.ignored_messages,
|
|
+ (unsigned long)bot_stats.eligible_messages, (unsigned long)bot_stats.sent_messages,
|
|
+ (unsigned long)bot_stats.send_failures, (unsigned long)bot_stats.suppressed_responses,
|
|
+ (unsigned long)bot_stats.emergency_forwards, (unsigned long)bot_stats.emergency_forward_failures);
|
|
+ Serial.printf(" > prefs load_failures=%lu save_failures=%lu\n", (unsigned long)bot_prefs.prefs_load_failures,
|
|
+ (unsigned long)bot_prefs.prefs_save_failures);
|
|
+ return true;
|
|
+ }
|
|
+ if (strcmp(args, "save") == 0) {
|
|
+ Serial.println(saveBotPrefs() ? " > bot prefs saved" : " Error: bot prefs save failed");
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+}
|
|
+
|
|
void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
|
size_t sender_prefix_len, const char *text) {
|
|
BotMessage message;
|
|
memset(&message, 0, sizeof(message));
|
|
- message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true);
|
|
+ message.channel_kind = BotPolicy::classifyChannel(NULL, 0, true, bot_prefs);
|
|
StrHelper::strzcpy(message.sender_name, from.name, sizeof(message.sender_name));
|
|
size_t prefix_len = sender_prefix_len;
|
|
if (prefix_len > sizeof(message.sender_key_prefix)) prefix_len = sizeof(message.sender_key_prefix);
|
|
@@ -580,7 +839,7 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n
|
|
BotMessage message;
|
|
memset(&message, 0, sizeof(message));
|
|
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
|
|
- message.channel_kind = BotPolicy::classifyChannel(channel_name, channel_len, false);
|
|
+ message.channel_kind = BotPolicy::classifyChannel(channel_name, channel_len, false, bot_prefs);
|
|
if (channel_name && channel_len > 0) {
|
|
memcpy(message.channel_name, channel_name, channel_len);
|
|
message.channel_name[channel_len] = 0;
|
|
@@ -598,6 +857,10 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n
|
|
void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId command_id) {
|
|
memset(&context, 0, sizeof(context));
|
|
StrHelper::strzcpy(context.node_name, _prefs.node_name, sizeof(context.node_name));
|
|
+ StrHelper::strzcpy(context.bot_channel, bot_prefs.bot_channel, sizeof(context.bot_channel));
|
|
+ StrHelper::strzcpy(context.testing_channel, bot_prefs.testing_channel, sizeof(context.testing_channel));
|
|
+ StrHelper::strzcpy(context.emergency_channel, bot_prefs.emergency_channel, sizeof(context.emergency_channel));
|
|
+ StrHelper::strzcpy(context.public_channel, bot_prefs.public_channel, sizeof(context.public_channel));
|
|
context.uptime_seconds = _ms->getMillis() / 1000;
|
|
context.observed_messages = bot_stats.observed_messages;
|
|
context.ignored_messages = bot_stats.ignored_messages;
|
|
@@ -658,7 +921,7 @@ bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) {
|
|
ChannelDetails channel;
|
|
if (getChannel(i, channel)) {
|
|
size_t name_len = botBoundedStrLen(channel.name, BOT_MAX_CHANNEL_NAME_LEN);
|
|
- if (BotPolicy::classifyChannel(channel.name, name_len, false) == kind) {
|
|
+ if (BotPolicy::classifyChannel(channel.name, name_len, false, bot_prefs) == kind) {
|
|
channel_idx = i;
|
|
return true;
|
|
}
|
|
@@ -739,6 +1002,11 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
return;
|
|
}
|
|
|
|
+ if (!bot_prefs.enabled) {
|
|
+ bot_stats.ignored_messages++;
|
|
+ return;
|
|
+ }
|
|
+
|
|
if (observeKnownBotResponse(message, direct_recipient != NULL)) return;
|
|
sendQueuedBotResponses();
|
|
|
|
@@ -748,7 +1016,8 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
return;
|
|
}
|
|
|
|
- if (FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
|
+ if (!BotPrefsCodec::commandEnabled(bot_prefs, command.id) ||
|
|
+ FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
|
bot_stats.ignored_messages++;
|
|
return;
|
|
}
|
|
@@ -773,7 +1042,9 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
|
message, command.id, request_fingerprint,
|
|
response_fingerprint, _ms->getMillis(),
|
|
context.random_seed, bot_identity_seed,
|
|
- queue_depth, &fingerprint, &due_at_millis);
|
|
+ queue_depth, bot_prefs.normal_delay_ms,
|
|
+ bot_prefs.normal_jitter_ms, &fingerprint,
|
|
+ &due_at_millis);
|
|
if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) {
|
|
bot_stats.send_failures++;
|
|
return;
|
|
@@ -903,14 +1174,15 @@ void MyMesh::scheduleBotFloodAdvert(unsigned long interval_millis) {
|
|
|
|
void MyMesh::tickBot() {
|
|
sendQueuedEmergencyForwards();
|
|
- sendQueuedBotResponses();
|
|
+ if (bot_prefs.enabled) sendQueuedBotResponses();
|
|
+ if (!bot_prefs.enabled) return;
|
|
if (next_bot_local_advert && millisHasNowPassed(next_bot_local_advert)) {
|
|
sendBotSelfAdvert(false);
|
|
- scheduleBotLocalAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
|
+ scheduleBotLocalAdvert(bot_prefs.local_advert_interval_ms);
|
|
}
|
|
if (next_bot_flood_advert && millisHasNowPassed(next_bot_flood_advert)) {
|
|
sendBotSelfAdvert(true);
|
|
- scheduleBotFloodAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
|
+ scheduleBotFloodAdvert(bot_prefs.flood_advert_interval_ms);
|
|
}
|
|
}
|
|
#endif
|
|
@@ -1244,6 +1516,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|
memset(advert_paths, 0, sizeof(advert_paths));
|
|
memset(send_scope.key, 0, sizeof(send_scope.key));
|
|
#if CMESH_BOT_ENABLED
|
|
+ BotPrefsCodec::defaults(bot_prefs);
|
|
memset(&bot_stats, 0, sizeof(bot_stats));
|
|
memset(pending_bot_responses, 0, sizeof(pending_bot_responses));
|
|
memset(pending_emergency_forwards, 0, sizeof(pending_emergency_forwards));
|
|
@@ -1360,8 +1633,9 @@ void MyMesh::begin(bool has_display) {
|
|
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
|
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
|
#if CMESH_BOT_ENABLED
|
|
- scheduleBotLocalAdvert(BOT_AUTO_LOCAL_FIRST_DELAY_MILLIS);
|
|
- scheduleBotFloodAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
|
+ if (!_store->loadBotPrefs(bot_prefs)) bot_prefs.prefs_load_failures++;
|
|
+ BotPrefsCodec::validate(bot_prefs);
|
|
+ applyBotPrefs();
|
|
#endif
|
|
}
|
|
|
|
@@ -2382,7 +2656,13 @@ void MyMesh::checkCLIRescueCmd() {
|
|
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
|
|
cli_command[len - 1] = 0; // replace newline with C string null terminator
|
|
|
|
- if (memcmp(cli_command, "set ", 4) == 0) {
|
|
+ if (memcmp(cli_command, "bot", 3) == 0 && (cli_command[3] == 0 || cli_command[3] == ' ')) {
|
|
+#if CMESH_BOT_ENABLED
|
|
+ if (!handleBotCLI(&cli_command[3])) Serial.println(" Error: unknown bot command");
|
|
+#else
|
|
+ Serial.println(" Error: bot support is disabled in this build");
|
|
+#endif
|
|
+ } else if (memcmp(cli_command, "set ", 4) == 0) {
|
|
const char* config = &cli_command[4];
|
|
if (memcmp(config, "pin ", 4) == 0) {
|
|
_prefs.ble_pin = atoi(&config[4]);
|
|
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
|
index 698ecad8..6f8d4329 100644
|
|
--- a/examples/companion_radio/MyMesh.h
|
|
+++ b/examples/companion_radio/MyMesh.h
|
|
@@ -202,6 +202,10 @@ private:
|
|
}
|
|
|
|
#if CMESH_BOT_ENABLED
|
|
+ void applyBotPrefs();
|
|
+ bool saveBotPrefs();
|
|
+ void printBotPrefs();
|
|
+ bool handleBotCLI(const char *args);
|
|
void observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
|
size_t sender_prefix_len, const char *text);
|
|
void observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
|
@@ -273,6 +277,7 @@ private:
|
|
size_t text_len;
|
|
};
|
|
|
|
+ BotPrefs bot_prefs;
|
|
BotStats bot_stats;
|
|
PendingBotResponse pending_bot_responses[BOT_PENDING_RESPONSE_SLOTS];
|
|
PendingEmergencyForward pending_emergency_forwards[BOT_PENDING_EMERGENCY_SLOTS];
|
|
diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp
|
|
index c5c8fc35..74ae3150 100644
|
|
--- a/examples/companion_radio/ResponseCoordinator.cpp
|
|
+++ b/examples/companion_radio/ResponseCoordinator.cpp
|
|
@@ -60,8 +60,15 @@ 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) {
|
|
- 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) +
|
|
+ return responseDelayMillis(message, command_id, request_fingerprint, bot_identity_seed, queue_depth, jitter_seed,
|
|
+ BOT_RESPONSE_DELAY_BASE_MILLIS, BOT_RESPONSE_DELAY_JITTER_MILLIS);
|
|
+}
|
|
+
|
|
+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,
|
|
+ uint16_t base_delay_millis, uint16_t jitter_millis) {
|
|
+ uint32_t jitter = jitter_millis ? jitter_seed % jitter_millis : 0;
|
|
+ return (uint32_t)base_delay_millis + channelDelayBias(message.channel_kind) + commandDelayBias(command_id) +
|
|
queueDelayBias(queue_depth) + tieBreakBias(request_fingerprint, bot_identity_seed) + jitter;
|
|
}
|
|
|
|
@@ -70,11 +77,23 @@ BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pe
|
|
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) {
|
|
+ return schedule(pending, pending_count, message, command_id, request_fingerprint, response_fingerprint, now_millis,
|
|
+ jitter_seed, bot_identity_seed, queue_depth, BOT_RESPONSE_DELAY_BASE_MILLIS,
|
|
+ BOT_RESPONSE_DELAY_JITTER_MILLIS, fingerprint, due_at_millis);
|
|
+}
|
|
+
|
|
+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, uint16_t base_delay_millis, uint16_t jitter_millis,
|
|
+ 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);
|
|
+ uint32_t due = now_millis + responseDelayMillis(message, command_id, request_fingerprint, bot_identity_seed, queue_depth,
|
|
+ jitter_seed, base_delay_millis, jitter_millis);
|
|
size_t slot = pending_count;
|
|
|
|
for (size_t i = 0; i < pending_count; i++) {
|
|
diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h
|
|
index d900633e..7717bee7 100644
|
|
--- a/examples/companion_radio/ResponseCoordinator.h
|
|
+++ b/examples/companion_radio/ResponseCoordinator.h
|
|
@@ -8,11 +8,20 @@ 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);
|
|
+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,
|
|
+ uint16_t base_delay_millis, uint16_t jitter_millis);
|
|
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);
|
|
+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, uint16_t base_delay_millis, uint16_t jitter_millis,
|
|
+ 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);
|