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 +#include + +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 +#include + +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 #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 #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 // needed for PlatformIO #include +#include +#include #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 "); + } + 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 "); + } + } else { + Serial.println(" Error: usage bot delay "); + } + 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 "); + } + } else { + Serial.println(" Error: usage bot advert "); + } + 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 [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);