mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 08:10:29 +00:00
forge: step 13 — validate bot parity and export patch
This commit is contained in:
@@ -1,423 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: cj-vana <cj@depth23.online>
|
||||
Date: Thu, 14 May 2026 14:03:07 -0600
|
||||
Subject: [PATCH 1/2] Add companion firmware bot core
|
||||
|
||||
---
|
||||
examples/companion_radio/BotPolicy.cpp | 58 +++++++
|
||||
examples/companion_radio/BotPolicy.h | 12 ++
|
||||
examples/companion_radio/BotTypes.h | 99 ++++++++++++
|
||||
examples/companion_radio/FirmwareBot.cpp | 193 +++++++++++++++++++++++
|
||||
examples/companion_radio/FirmwareBot.h | 13 ++
|
||||
5 files changed, 375 insertions(+)
|
||||
create mode 100644 examples/companion_radio/BotPolicy.cpp
|
||||
create mode 100644 examples/companion_radio/BotPolicy.h
|
||||
create mode 100644 examples/companion_radio/BotTypes.h
|
||||
create mode 100644 examples/companion_radio/FirmwareBot.cpp
|
||||
create mode 100644 examples/companion_radio/FirmwareBot.h
|
||||
|
||||
diff --git a/examples/companion_radio/BotPolicy.cpp b/examples/companion_radio/BotPolicy.cpp
|
||||
new file mode 100644
|
||||
index 00000000..bd3fc6c7
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/BotPolicy.cpp
|
||||
@@ -0,0 +1,58 @@
|
||||
+#include "BotPolicy.h"
|
||||
+
|
||||
+#include <ctype.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+bool equalsIgnoreCase(const char* value, size_t len, const char* expected) {
|
||||
+ if (!value || !expected) return false;
|
||||
+
|
||||
+ if (len > 0 && value[0] == '#') {
|
||||
+ value++;
|
||||
+ len--;
|
||||
+ }
|
||||
+
|
||||
+ size_t expected_len = strlen(expected);
|
||||
+ if (expected_len > 0 && expected[0] == '#') {
|
||||
+ expected++;
|
||||
+ expected_len--;
|
||||
+ }
|
||||
+ if (len != expected_len) return false;
|
||||
+
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ if (tolower((unsigned char)value[i]) != tolower((unsigned char)expected[i])) return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace BotPolicy {
|
||||
+
|
||||
+BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message) {
|
||||
+ if (direct_message) return BOT_CHANNEL_DM;
|
||||
+ if (equalsIgnoreCase(name, len, "Public")) return BOT_CHANNEL_PUBLIC;
|
||||
+ if (equalsIgnoreCase(name, len, "#bot")) return BOT_CHANNEL_BOT;
|
||||
+ if (equalsIgnoreCase(name, len, "#testing")) return BOT_CHANNEL_TESTING;
|
||||
+ if (equalsIgnoreCase(name, len, "#emergency")) 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;
|
||||
+ }
|
||||
+ if (kind == BOT_CHANNEL_EMERGENCY) return BOT_POLICY_EMERGENCY_FORWARD;
|
||||
+ return BOT_POLICY_IGNORE;
|
||||
+}
|
||||
+
|
||||
+bool isNormalAllowed(BotChannelKind kind) {
|
||||
+ return decide(kind) == BOT_POLICY_ALLOW_NORMAL;
|
||||
+}
|
||||
+
|
||||
+bool isEmergency(BotChannelKind kind) {
|
||||
+ return decide(kind) == BOT_POLICY_EMERGENCY_FORWARD;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/BotPolicy.h b/examples/companion_radio/BotPolicy.h
|
||||
new file mode 100644
|
||||
index 00000000..e074e3eb
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/BotPolicy.h
|
||||
@@ -0,0 +1,12 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace BotPolicy {
|
||||
+
|
||||
+BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message);
|
||||
+BotPolicyDecision decide(BotChannelKind kind);
|
||||
+bool isNormalAllowed(BotChannelKind kind);
|
||||
+bool isEmergency(BotChannelKind kind);
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
new file mode 100644
|
||||
index 00000000..869ae3ae
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -0,0 +1,99 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include <stddef.h>
|
||||
+#include <stdint.h>
|
||||
+
|
||||
+#define BOT_MAX_TEXT_LEN 160
|
||||
+#define BOT_MAX_RESPONSE_LEN 144
|
||||
+#define BOT_MAX_COMMAND_NAME_LEN 15
|
||||
+#define BOT_MAX_COMMAND_ARGS_LEN 79
|
||||
+#define BOT_MAX_CHANNEL_NAME_LEN 23
|
||||
+#define BOT_MAX_SENDER_NAME_LEN 31
|
||||
+#define BOT_SENDER_KEY_PREFIX_LEN 6
|
||||
+
|
||||
+enum BotChannelKind : uint8_t {
|
||||
+ BOT_CHANNEL_DM = 0,
|
||||
+ BOT_CHANNEL_PUBLIC,
|
||||
+ BOT_CHANNEL_BOT,
|
||||
+ BOT_CHANNEL_TESTING,
|
||||
+ BOT_CHANNEL_EMERGENCY,
|
||||
+ BOT_CHANNEL_OTHER
|
||||
+};
|
||||
+
|
||||
+enum BotPolicyDecision : uint8_t {
|
||||
+ BOT_POLICY_IGNORE = 0,
|
||||
+ BOT_POLICY_ALLOW_NORMAL,
|
||||
+ BOT_POLICY_EMERGENCY_FORWARD
|
||||
+};
|
||||
+
|
||||
+enum BotCommandId : uint8_t {
|
||||
+ BOT_COMMAND_NONE = 0,
|
||||
+ BOT_COMMAND_HELP,
|
||||
+ BOT_COMMAND_PING,
|
||||
+ BOT_COMMAND_TEST,
|
||||
+ BOT_COMMAND_HELLO,
|
||||
+ BOT_COMMAND_ABOUT,
|
||||
+ BOT_COMMAND_DICE,
|
||||
+ BOT_COMMAND_STATUS,
|
||||
+ BOT_COMMAND_UNKNOWN
|
||||
+};
|
||||
+
|
||||
+enum BotWriteResult : uint8_t {
|
||||
+ BOT_WRITE_OK = 0,
|
||||
+ BOT_WRITE_TRUNCATED,
|
||||
+ BOT_WRITE_NO_SPACE
|
||||
+};
|
||||
+
|
||||
+struct BotFingerprint {
|
||||
+ uint64_t value;
|
||||
+};
|
||||
+
|
||||
+struct BotMessage {
|
||||
+ BotChannelKind channel_kind;
|
||||
+ char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
||||
+ char sender_name[BOT_MAX_SENDER_NAME_LEN + 1];
|
||||
+ uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN];
|
||||
+ uint32_t sender_timestamp;
|
||||
+ char text[BOT_MAX_TEXT_LEN + 1];
|
||||
+ size_t text_len;
|
||||
+};
|
||||
+
|
||||
+struct BotCommand {
|
||||
+ BotCommandId id;
|
||||
+ char name[BOT_MAX_COMMAND_NAME_LEN + 1];
|
||||
+ char args[BOT_MAX_COMMAND_ARGS_LEN + 1];
|
||||
+ size_t args_len;
|
||||
+};
|
||||
+
|
||||
+struct BotResponse {
|
||||
+ BotPolicyDecision decision;
|
||||
+ BotFingerprint fingerprint;
|
||||
+ char text[BOT_MAX_RESPONSE_LEN + 1];
|
||||
+ size_t text_len;
|
||||
+ bool truncated;
|
||||
+};
|
||||
+
|
||||
+struct BotPrefs {
|
||||
+ bool enabled;
|
||||
+ uint16_t normal_delay_ms;
|
||||
+ uint16_t normal_jitter_ms;
|
||||
+ 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];
|
||||
+};
|
||||
+
|
||||
+struct BotStats {
|
||||
+ uint32_t observed_messages;
|
||||
+ uint32_t ignored_messages;
|
||||
+ uint32_t eligible_messages;
|
||||
+ uint32_t emergency_messages;
|
||||
+ uint32_t parse_errors;
|
||||
+};
|
||||
+
|
||||
+static_assert(sizeof(BotMessage) <= 240, "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(BotPrefs) <= 128, "BotPrefs RAM budget exceeded");
|
||||
+static_assert(sizeof(BotStats) <= 32, "BotStats RAM budget exceeded");
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
new file mode 100644
|
||||
index 00000000..c9447282
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -0,0 +1,193 @@
|
||||
+#include "FirmwareBot.h"
|
||||
+
|
||||
+#include <ctype.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+uint64_t fnv1aUpdate(uint64_t hash, uint8_t value) {
|
||||
+ hash ^= value;
|
||||
+ hash *= 1099511628211ULL;
|
||||
+ return hash;
|
||||
+}
|
||||
+
|
||||
+uint64_t fnv1aUpdateBytes(uint64_t hash, const uint8_t* data, size_t len) {
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ hash = fnv1aUpdate(hash, data[i]);
|
||||
+ }
|
||||
+ return hash;
|
||||
+}
|
||||
+
|
||||
+uint64_t fnv1aUpdateTextLower(uint64_t hash, const char* value, size_t len) {
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)tolower((unsigned char)value[i]));
|
||||
+ }
|
||||
+ return hash;
|
||||
+}
|
||||
+
|
||||
+bool isSpaceByte(char ch) {
|
||||
+ return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
||||
+}
|
||||
+
|
||||
+bool isControlByte(char ch) {
|
||||
+ unsigned char value = (unsigned char)ch;
|
||||
+ return value < 0x20 || value == 0x7F;
|
||||
+}
|
||||
+
|
||||
+bool namesEqual(const char* name, size_t len, const char* expected) {
|
||||
+ size_t expected_len = strlen(expected);
|
||||
+ if (len != expected_len) return false;
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ if (tolower((unsigned char)name[i]) != expected[i]) return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+bool isCommandDelimiter(char ch) {
|
||||
+ return ch == ' ' || ch == ':' || ch == ',' || ch == '.' || ch == ';' || ch == '?' || ch == '!';
|
||||
+}
|
||||
+
|
||||
+uint64_t fnv1aUpdateU32(uint64_t hash, uint32_t value) {
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)(value & 0xFF));
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)((value >> 8) & 0xFF));
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)((value >> 16) & 0xFF));
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)((value >> 24) & 0xFF));
|
||||
+ return hash;
|
||||
+}
|
||||
+
|
||||
+size_t boundedStrLen(const char* value, size_t max_len) {
|
||||
+ size_t len = 0;
|
||||
+ while (len < max_len && value[len] != 0) len++;
|
||||
+ return len;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace FirmwareBot {
|
||||
+
|
||||
+BotWriteResult normalizeText(const char* input, size_t input_len, char* output, size_t output_len, size_t* written) {
|
||||
+ if (written) *written = 0;
|
||||
+ if (!output || output_len == 0) return BOT_WRITE_NO_SPACE;
|
||||
+
|
||||
+ size_t out = 0;
|
||||
+ bool pending_space = false;
|
||||
+ bool truncated = false;
|
||||
+
|
||||
+ for (size_t i = 0; i < input_len; i++) {
|
||||
+ char ch = input ? input[i] : 0;
|
||||
+ if (ch == 0) break;
|
||||
+
|
||||
+ if (isSpaceByte(ch) || isControlByte(ch)) {
|
||||
+ pending_space = out > 0;
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ if (pending_space) {
|
||||
+ if (out + 1 >= output_len) {
|
||||
+ truncated = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ output[out++] = ' ';
|
||||
+ pending_space = false;
|
||||
+ }
|
||||
+
|
||||
+ if (out + 1 >= output_len) {
|
||||
+ truncated = true;
|
||||
+ break;
|
||||
+ }
|
||||
+ output[out++] = ch;
|
||||
+ }
|
||||
+
|
||||
+ output[out] = 0;
|
||||
+ if (written) *written = out;
|
||||
+ return truncated ? BOT_WRITE_TRUNCATED : BOT_WRITE_OK;
|
||||
+}
|
||||
+
|
||||
+BotCommandId commandIdForName(const char* name, size_t len) {
|
||||
+ if (namesEqual(name, len, "help") || namesEqual(name, len, "cmd") || namesEqual(name, len, "commands")) return BOT_COMMAND_HELP;
|
||||
+ if (namesEqual(name, len, "ping")) return BOT_COMMAND_PING;
|
||||
+ if (namesEqual(name, len, "test")) return BOT_COMMAND_TEST;
|
||||
+ if (namesEqual(name, len, "hello") || namesEqual(name, len, "hi")) return BOT_COMMAND_HELLO;
|
||||
+ if (namesEqual(name, len, "about")) return BOT_COMMAND_ABOUT;
|
||||
+ if (namesEqual(name, len, "dice") || namesEqual(name, len, "roll")) return BOT_COMMAND_DICE;
|
||||
+ if (namesEqual(name, len, "status")) return BOT_COMMAND_STATUS;
|
||||
+ return BOT_COMMAND_UNKNOWN;
|
||||
+}
|
||||
+
|
||||
+bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
+ if (!command) return false;
|
||||
+ memset(command, 0, sizeof(*command));
|
||||
+ command->id = BOT_COMMAND_NONE;
|
||||
+
|
||||
+ char normalized[BOT_MAX_TEXT_LEN + 1];
|
||||
+ size_t normalized_len = 0;
|
||||
+ normalizeText(text, text_len, normalized, sizeof(normalized), &normalized_len);
|
||||
+
|
||||
+ if (normalized_len < 2 || (normalized[0] != '!' && normalized[0] != '/')) return false;
|
||||
+
|
||||
+ size_t pos = 1;
|
||||
+ while (pos < normalized_len && normalized[pos] == ' ') pos++;
|
||||
+ size_t name_start = pos;
|
||||
+ while (pos < normalized_len && !isCommandDelimiter(normalized[pos])) pos++;
|
||||
+ size_t name_len = pos - name_start;
|
||||
+ if (name_len == 0) return false;
|
||||
+
|
||||
+ size_t copy_name_len = name_len;
|
||||
+ if (copy_name_len > BOT_MAX_COMMAND_NAME_LEN) copy_name_len = BOT_MAX_COMMAND_NAME_LEN;
|
||||
+ for (size_t i = 0; i < copy_name_len; i++) {
|
||||
+ command->name[i] = (char)tolower((unsigned char)normalized[name_start + i]);
|
||||
+ }
|
||||
+ command->name[copy_name_len] = 0;
|
||||
+ command->id = name_len > BOT_MAX_COMMAND_NAME_LEN ? BOT_COMMAND_UNKNOWN : commandIdForName(command->name, copy_name_len);
|
||||
+
|
||||
+ while (pos < normalized_len && isCommandDelimiter(normalized[pos])) pos++;
|
||||
+ size_t args_len = normalized_len - pos;
|
||||
+ if (args_len > BOT_MAX_COMMAND_ARGS_LEN) args_len = BOT_MAX_COMMAND_ARGS_LEN;
|
||||
+ if (args_len > 0) memcpy(command->args, &normalized[pos], args_len);
|
||||
+ command->args[args_len] = 0;
|
||||
+ command->args_len = args_len;
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written) {
|
||||
+ if (written) *written = 0;
|
||||
+ if (!output || output_len == 0) return BOT_WRITE_NO_SPACE;
|
||||
+
|
||||
+ if (!text && text_len > 0) {
|
||||
+ output[0] = 0;
|
||||
+ return BOT_WRITE_NO_SPACE;
|
||||
+ }
|
||||
+
|
||||
+ size_t copy_len = text_len;
|
||||
+ if (copy_len + 1 > output_len) copy_len = output_len - 1;
|
||||
+ if (copy_len > 0) memcpy(output, text, copy_len);
|
||||
+ output[copy_len] = 0;
|
||||
+ if (written) *written = copy_len;
|
||||
+
|
||||
+ return copy_len < text_len ? BOT_WRITE_TRUNCATED : BOT_WRITE_OK;
|
||||
+}
|
||||
+
|
||||
+BotFingerprint fingerprintFor(const BotMessage& message) {
|
||||
+ uint64_t hash = 1469598103934665603ULL;
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)message.channel_kind);
|
||||
+ const char* channel_name = message.channel_name;
|
||||
+ size_t channel_name_len = boundedStrLen(message.channel_name, sizeof(message.channel_name));
|
||||
+ if (channel_name_len > 0 && channel_name[0] == '#') {
|
||||
+ channel_name++;
|
||||
+ channel_name_len--;
|
||||
+ }
|
||||
+ hash = fnv1aUpdateTextLower(hash, channel_name, channel_name_len);
|
||||
+ hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, sizeof(message.sender_key_prefix));
|
||||
+ hash = fnv1aUpdateU32(hash, message.sender_timestamp);
|
||||
+
|
||||
+ char normalized[BOT_MAX_TEXT_LEN + 1];
|
||||
+ size_t normalized_len = 0;
|
||||
+ normalizeText(message.text, message.text_len, normalized, sizeof(normalized), &normalized_len);
|
||||
+ hash = fnv1aUpdateTextLower(hash, normalized, normalized_len);
|
||||
+
|
||||
+ BotFingerprint fingerprint = { hash };
|
||||
+ return fingerprint;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
||||
new file mode 100644
|
||||
index 00000000..8fce16ab
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/FirmwareBot.h
|
||||
@@ -0,0 +1,13 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace FirmwareBot {
|
||||
+
|
||||
+BotWriteResult normalizeText(const char* input, size_t input_len, char* output, size_t output_len, size_t* written);
|
||||
+bool parseCommand(const char* text, size_t text_len, BotCommand* command);
|
||||
+BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written);
|
||||
+BotFingerprint fingerprintFor(const BotMessage& message);
|
||||
+BotCommandId commandIdForName(const char* name, size_t len);
|
||||
+
|
||||
+}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,316 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: cj-vana <cj@depth23.online>
|
||||
Date: Thu, 14 May 2026 14:03:07 -0600
|
||||
Subject: [PATCH 2/2] Wire companion firmware bot runtime
|
||||
|
||||
---
|
||||
examples/companion_radio/FirmwareBot.cpp | 24 ++++
|
||||
examples/companion_radio/FirmwareBot.h | 2 +
|
||||
examples/companion_radio/MyMesh.cpp | 136 ++++++++++++++++++++++-
|
||||
examples/companion_radio/MyMesh.h | 24 ++++
|
||||
4 files changed, 184 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
index c9447282..2d4b7395 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -150,6 +150,30 @@ bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+bool splitChannelText(const char* text, size_t text_len, char* sender, size_t sender_len, const char** body,
|
||||
+ size_t* body_len) {
|
||||
+ if (body) *body = text;
|
||||
+ if (body_len) *body_len = text_len;
|
||||
+ if (!text) return false;
|
||||
+
|
||||
+ for (size_t i = 0; i < text_len; i++) {
|
||||
+ if (text[i] == 0) break;
|
||||
+ if (text[i] == ':' && i + 1 < text_len && text[i + 1] == ' ') {
|
||||
+ if (sender && sender_len > 0) {
|
||||
+ size_t copy_len = i;
|
||||
+ if (copy_len >= sender_len) copy_len = sender_len - 1;
|
||||
+ if (copy_len > 0) memcpy(sender, text, copy_len);
|
||||
+ sender[copy_len] = 0;
|
||||
+ }
|
||||
+ size_t start = i + 2;
|
||||
+ if (body) *body = &text[start];
|
||||
+ if (body_len) *body_len = text_len - start;
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written) {
|
||||
if (written) *written = 0;
|
||||
if (!output || output_len == 0) return BOT_WRITE_NO_SPACE;
|
||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
||||
index 8fce16ab..8b267b89 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.h
|
||||
+++ b/examples/companion_radio/FirmwareBot.h
|
||||
@@ -6,6 +6,8 @@ namespace FirmwareBot {
|
||||
|
||||
BotWriteResult normalizeText(const char* input, size_t input_len, char* output, size_t output_len, size_t* written);
|
||||
bool parseCommand(const char* text, size_t text_len, BotCommand* command);
|
||||
+bool splitChannelText(const char* text, size_t text_len, char* sender, size_t sender_len, const char** body,
|
||||
+ size_t* body_len);
|
||||
BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written);
|
||||
BotFingerprint fingerprintFor(const BotMessage& message);
|
||||
BotCommandId commandIdForName(const char* name, size_t len);
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index e8c1914b..481038f4 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -3,6 +3,11 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+#include "BotPolicy.h"
|
||||
+#include "FirmwareBot.h"
|
||||
+#endif
|
||||
+
|
||||
#define CMD_APP_START 1
|
||||
#define CMD_SEND_TXT_MSG 2
|
||||
#define CMD_SEND_CHANNEL_TXT_MSG 3
|
||||
@@ -105,8 +110,21 @@
|
||||
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
||||
#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=="
|
||||
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+static size_t botBoundedStrLen(const char *value, size_t max_len) {
|
||||
+ size_t len = 0;
|
||||
+ while (value && len < max_len && value[len] != 0) len++;
|
||||
+ return len;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
// these are _pushed_ to client app at any time
|
||||
#define PUSH_CODE_ADVERT 0x80
|
||||
#define PUSH_CODE_PATH_UPDATED 0x81
|
||||
@@ -514,6 +532,9 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t
|
||||
const char *text) {
|
||||
markConnectionActive(from); // in case this is from a server, and we have a connection
|
||||
queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text);
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ observeBotDirectMessage(from, sender_timestamp, from.id.pub_key, BOT_SENDER_KEY_PREFIX_LEN, text);
|
||||
+#endif
|
||||
}
|
||||
|
||||
void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
|
||||
@@ -528,8 +549,104 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin
|
||||
// from.sync_since change needs to be persisted
|
||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||
queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text);
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ observeBotDirectMessage(from, sender_timestamp, sender_prefix, 4, text);
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+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);
|
||||
+ 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);
|
||||
+ if (sender_prefix && prefix_len > 0) memcpy(message.sender_key_prefix, sender_prefix, prefix_len);
|
||||
+ message.sender_timestamp = sender_timestamp;
|
||||
+ FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN), message.text, sizeof(message.text),
|
||||
+ &message.text_len);
|
||||
+ recordBotObservation(message);
|
||||
+}
|
||||
+
|
||||
+void MyMesh::observeBotChannelMessage(const char *channel_name, const char *text, uint32_t sender_timestamp) {
|
||||
+ 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);
|
||||
+ if (channel_name && channel_len > 0) {
|
||||
+ memcpy(message.channel_name, channel_name, channel_len);
|
||||
+ message.channel_name[channel_len] = 0;
|
||||
+ }
|
||||
+ message.sender_timestamp = sender_timestamp;
|
||||
+
|
||||
+ const char *body = text;
|
||||
+ size_t body_len = botBoundedStrLen(text, BOT_MAX_TEXT_LEN);
|
||||
+ FirmwareBot::splitChannelText(text, body_len, message.sender_name, sizeof(message.sender_name), &body, &body_len);
|
||||
+ FirmwareBot::normalizeText(body, body_len, message.text, sizeof(message.text), &message.text_len);
|
||||
+ recordBotObservation(message);
|
||||
+}
|
||||
+
|
||||
+void MyMesh::recordBotObservation(const BotMessage &message) {
|
||||
+ bot_stats.observed_messages++;
|
||||
+ BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
||||
+ if (decision == BOT_POLICY_IGNORE) {
|
||||
+ bot_stats.ignored_messages++;
|
||||
+ return;
|
||||
+ }
|
||||
+ if (decision == BOT_POLICY_EMERGENCY_FORWARD) {
|
||||
+ bot_stats.emergency_messages++;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ BotCommand command;
|
||||
+ if (FirmwareBot::parseCommand(message.text, message.text_len, &command)) {
|
||||
+ bot_stats.eligible_messages++;
|
||||
+ } else if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) {
|
||||
+ bot_stats.parse_errors++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+bool MyMesh::sendBotSelfAdvert(bool flood) {
|
||||
+ mesh::Packet* pkt;
|
||||
+ if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
|
||||
+ pkt = createSelfAdvert(_prefs.node_name);
|
||||
+ } else {
|
||||
+ pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon);
|
||||
+ }
|
||||
+ if (!pkt) return false;
|
||||
+
|
||||
+ if (flood) {
|
||||
+ TransportKey default_scope;
|
||||
+ memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
|
||||
+ sendFloodScoped(default_scope, pkt, 0);
|
||||
+ } else {
|
||||
+ sendZeroHop(pkt);
|
||||
+ }
|
||||
+ return true;
|
||||
}
|
||||
|
||||
+void MyMesh::scheduleBotLocalAdvert(unsigned long interval_millis) {
|
||||
+ next_bot_local_advert = interval_millis > 0 ? futureMillis(interval_millis) : 0;
|
||||
+}
|
||||
+
|
||||
+void MyMesh::scheduleBotFloodAdvert(unsigned long interval_millis) {
|
||||
+ next_bot_flood_advert = interval_millis > 0 ? futureMillis(interval_millis) : 0;
|
||||
+}
|
||||
+
|
||||
+void MyMesh::tickBot() {
|
||||
+ if (next_bot_local_advert && millisHasNowPassed(next_bot_local_advert)) {
|
||||
+ sendBotSelfAdvert(false);
|
||||
+ scheduleBotLocalAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
||||
+ }
|
||||
+ if (next_bot_flood_advert && millisHasNowPassed(next_bot_flood_advert)) {
|
||||
+ sendBotSelfAdvert(true);
|
||||
+ scheduleBotFloodAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
||||
+ }
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp,
|
||||
const char *text) {
|
||||
int i = 0;
|
||||
@@ -566,15 +683,17 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
if (_ui) _ui->notify(UIEventType::channelMessage);
|
||||
#endif
|
||||
}
|
||||
-#ifdef DISPLAY_CLASS
|
||||
- // Get the channel name from the channel index
|
||||
const char *channel_name = "Unknown";
|
||||
ChannelDetails channel_details;
|
||||
if (getChannel(channel_idx, channel_details)) {
|
||||
channel_name = channel_details.name;
|
||||
}
|
||||
+#ifdef DISPLAY_CLASS
|
||||
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
|
||||
#endif
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ observeBotChannelMessage(channel_name, text, timestamp);
|
||||
+#endif
|
||||
}
|
||||
|
||||
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
|
||||
@@ -856,6 +975,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
dirty_contacts_expiry = 0;
|
||||
memset(advert_paths, 0, sizeof(advert_paths));
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ memset(&bot_stats, 0, sizeof(bot_stats));
|
||||
+ next_bot_local_advert = 0;
|
||||
+ next_bot_flood_advert = 0;
|
||||
+#endif
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
@@ -956,6 +1080,10 @@ void MyMesh::begin(bool has_display) {
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
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);
|
||||
+#endif
|
||||
}
|
||||
|
||||
const char *MyMesh::getNodeName() {
|
||||
@@ -2172,6 +2300,10 @@ void MyMesh::loop() {
|
||||
dirty_contacts_expiry = 0;
|
||||
}
|
||||
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ tickBot();
|
||||
+#endif
|
||||
+
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_ui) _ui->setHasConnection(_serial->isConnected());
|
||||
#endif
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index aeff591c..d72a496a 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -4,6 +4,13 @@
|
||||
#include <Mesh.h>
|
||||
#include "AbstractUITask.h"
|
||||
|
||||
+#ifndef CMESH_BOT_ENABLED
|
||||
+#define CMESH_BOT_ENABLED 0
|
||||
+#endif
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+#include "BotTypes.h"
|
||||
+#endif
|
||||
+
|
||||
/*------------ Frame Protocol --------------*/
|
||||
#define FIRMWARE_VER_CODE 11
|
||||
|
||||
@@ -192,6 +199,17 @@ private:
|
||||
return _store->putBlobByKey(key, key_len, src_buf, len);
|
||||
}
|
||||
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ void observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
||||
+ size_t sender_prefix_len, const char *text);
|
||||
+ void observeBotChannelMessage(const char *channel_name, const char *text, uint32_t sender_timestamp);
|
||||
+ void recordBotObservation(const BotMessage &message);
|
||||
+ void tickBot();
|
||||
+ void scheduleBotLocalAdvert(unsigned long interval_millis);
|
||||
+ void scheduleBotFloodAdvert(unsigned long interval_millis);
|
||||
+ bool sendBotSelfAdvert(bool flood);
|
||||
+#endif
|
||||
+
|
||||
void checkCLIRescueCmd();
|
||||
void checkSerialInterface();
|
||||
bool isValidClientRepeatFreq(uint32_t f) const;
|
||||
@@ -223,6 +241,12 @@ private:
|
||||
|
||||
TransportKey send_scope;
|
||||
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ BotStats bot_stats;
|
||||
+ unsigned long next_bot_local_advert;
|
||||
+ unsigned long next_bot_flood_advert;
|
||||
+#endif
|
||||
+
|
||||
uint8_t cmd_frame[MAX_FRAME_SIZE + 1];
|
||||
uint8_t out_frame[MAX_FRAME_SIZE + 1];
|
||||
CayenneLPP telemetry;
|
||||
@@ -1,596 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: cj-vana <cj@depth23.online>
|
||||
Date: Thu, 14 May 2026 14:03:07 -0600
|
||||
Subject: [PATCH 3/3] Add firmware bot commands
|
||||
|
||||
---
|
||||
examples/companion_radio/BotCommands.cpp | 171 +++++++++++++++++++++++++++++++
|
||||
examples/companion_radio/BotCommands.h | 10 ++
|
||||
examples/companion_radio/BotTypes.h | 43 +++++++-
|
||||
examples/companion_radio/FirmwareBot.cpp | 35 +++++++
|
||||
examples/companion_radio/FirmwareBot.h | 5 +
|
||||
examples/companion_radio/MyMesh.cpp | 131 +++++++++++++++++++++--
|
||||
examples/companion_radio/MyMesh.h | 22 +++-
|
||||
7 files changed, 406 insertions(+), 11 deletions(-)
|
||||
diff --git a/examples/companion_radio/BotCommands.cpp b/examples/companion_radio/BotCommands.cpp
|
||||
new file mode 100644
|
||||
index 00000000..5b13fbfe
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/BotCommands.cpp
|
||||
@@ -0,0 +1,171 @@
|
||||
+#include "BotCommands.h"
|
||||
+
|
||||
+#include <ctype.h>
|
||||
+#include <stdarg.h>
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+BotCommandResult makeResult(BotCommandResultCode code, size_t text_len) {
|
||||
+ BotCommandResult result = { code, text_len };
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+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;
|
||||
+}
|
||||
+
|
||||
+BotCommandResult writeText(char* output, size_t output_len, const char* text) {
|
||||
+ if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+
|
||||
+ size_t text_len = boundedStrLen(text, BOT_MAX_RESPONSE_LEN + 1);
|
||||
+ size_t copy_len = text_len;
|
||||
+ if (copy_len + 1 > output_len) copy_len = output_len - 1;
|
||||
+ if (copy_len > 0) memcpy(output, text, copy_len);
|
||||
+ output[copy_len] = 0;
|
||||
+
|
||||
+ return makeResult(copy_len < text_len ? BOT_COMMAND_RESULT_TRUNCATED : BOT_COMMAND_RESULT_OK, copy_len);
|
||||
+}
|
||||
+
|
||||
+BotCommandResult writeFormatted(char* output, size_t output_len, const char* format, ...) {
|
||||
+ if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+
|
||||
+ va_list args;
|
||||
+ va_start(args, format);
|
||||
+ int n = vsnprintf(output, output_len, format, args);
|
||||
+ va_end(args);
|
||||
+
|
||||
+ if (n < 0) {
|
||||
+ output[0] = 0;
|
||||
+ return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+ }
|
||||
+
|
||||
+ size_t written = (size_t)n;
|
||||
+ if (written >= output_len) written = output_len - 1;
|
||||
+ return makeResult((size_t)n >= output_len ? BOT_COMMAND_RESULT_TRUNCATED : BOT_COMMAND_RESULT_OK, written);
|
||||
+}
|
||||
+
|
||||
+bool parseUInt(const char* text, size_t len, size_t* pos, uint16_t* value) {
|
||||
+ uint32_t parsed = 0;
|
||||
+ size_t start = *pos;
|
||||
+ while (*pos < len && isdigit((unsigned char)text[*pos])) {
|
||||
+ parsed = parsed * 10 + (uint32_t)(text[*pos] - '0');
|
||||
+ if (parsed > 1000) return false;
|
||||
+ (*pos)++;
|
||||
+ }
|
||||
+ if (*pos == start) return false;
|
||||
+ *value = (uint16_t)parsed;
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+bool isSupportedSides(uint16_t sides) {
|
||||
+ return sides == 4 || sides == 6 || sides == 8 || sides == 10 || sides == 12 || sides == 16 || sides == 20;
|
||||
+}
|
||||
+
|
||||
+bool parseDice(const BotCommand& command, uint16_t* count, uint16_t* sides) {
|
||||
+ *count = 1;
|
||||
+ *sides = 6;
|
||||
+ if (command.args_len == 0) return true;
|
||||
+
|
||||
+ const char* text = command.args;
|
||||
+ size_t len = command.args_len;
|
||||
+ size_t pos = 0;
|
||||
+
|
||||
+ if (text[pos] == 'd' || text[pos] == 'D') {
|
||||
+ pos++;
|
||||
+ if (!parseUInt(text, len, &pos, sides)) return false;
|
||||
+ } else {
|
||||
+ uint16_t first = 0;
|
||||
+ if (!parseUInt(text, len, &pos, &first)) return false;
|
||||
+ if (pos < len && (text[pos] == 'd' || text[pos] == 'D')) {
|
||||
+ *count = first;
|
||||
+ pos++;
|
||||
+ if (!parseUInt(text, len, &pos, sides)) return false;
|
||||
+ } else {
|
||||
+ *sides = first;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return pos == len && *count >= 1 && *count <= 10 && isSupportedSides(*sides);
|
||||
+}
|
||||
+
|
||||
+uint16_t rollOnce(uint32_t* state, uint16_t sides) {
|
||||
+ *state = (*state * 1664525UL) + 1013904223UL;
|
||||
+ return (uint16_t)((*state >> 16) % sides) + 1;
|
||||
+}
|
||||
+
|
||||
+BotCommandResult executeDice(const BotCommand& command, const BotCommandContext& context, char* output, size_t output_len) {
|
||||
+ uint16_t count = 1;
|
||||
+ uint16_t sides = 6;
|
||||
+ if (!parseDice(command, &count, &sides)) {
|
||||
+ return writeText(output, output_len, "Usage: !roll [d6|d20|2d6], sides: d4 d6 d8 d10 d12 d16 d20");
|
||||
+ }
|
||||
+
|
||||
+ uint32_t state = context.random_seed ^ ((uint32_t)count << 16) ^ sides;
|
||||
+ if (state == 0) state = 1;
|
||||
+
|
||||
+ if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+ int written = count == 1 ? snprintf(output, output_len, "Rolled d%u: ", (unsigned)sides)
|
||||
+ : snprintf(output, output_len, "Rolled %ud%u: ", (unsigned)count, (unsigned)sides);
|
||||
+ if (written < 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+
|
||||
+ size_t pos = (size_t)written;
|
||||
+ uint16_t total = 0;
|
||||
+ for (uint16_t i = 0; i < count; i++) {
|
||||
+ uint16_t roll = rollOnce(&state, sides);
|
||||
+ total += roll;
|
||||
+ if (pos < output_len) {
|
||||
+ int n = snprintf(&output[pos], output_len - pos, i == 0 ? "%u" : "+%u", (unsigned)roll);
|
||||
+ if (n < 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+ pos += (size_t)n;
|
||||
+ }
|
||||
+ }
|
||||
+ if (count > 1 && pos < output_len) {
|
||||
+ int n = snprintf(&output[pos], output_len - pos, "=%u", (unsigned)total);
|
||||
+ if (n < 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+ pos += (size_t)n;
|
||||
+ }
|
||||
+
|
||||
+ size_t actual = boundedStrLen(output, output_len);
|
||||
+ return makeResult(pos >= output_len ? BOT_COMMAND_RESULT_TRUNCATED : BOT_COMMAND_RESULT_OK, actual);
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace BotCommands {
|
||||
+
|
||||
+BotCommandResult executeCommand(const BotCommand& command, const BotCommandContext& context, char* output,
|
||||
+ size_t output_len) {
|
||||
+ switch (command.id) {
|
||||
+ case BOT_COMMAND_HELP:
|
||||
+ return writeText(output, output_len, "Commands: !ping !test !hello !about !roll [d20|2d6] !status !channels");
|
||||
+ case BOT_COMMAND_PING:
|
||||
+ return writeText(output, output_len, "Pong!");
|
||||
+ case BOT_COMMAND_TEST:
|
||||
+ return writeText(output, output_len, "Bot test OK");
|
||||
+ case BOT_COMMAND_HELLO:
|
||||
+ return writeFormatted(output, output_len, "Hello from %s", context.node_name[0] ? context.node_name : "MeshCore bot");
|
||||
+ case BOT_COMMAND_ABOUT:
|
||||
+ return writeText(output, output_len, "Colorado Mesh firmware bot: local commands only, no internet required.");
|
||||
+ case BOT_COMMAND_DICE:
|
||||
+ return executeDice(command, context, output, output_len);
|
||||
+ case BOT_COMMAND_STATUS:
|
||||
+ return writeFormatted(output, output_len, "%s up %lus batt %umV storage %lu/%luKB seen %lu sent %lu fail %lu",
|
||||
+ context.node_name[0] ? context.node_name : "bot", (unsigned long)context.uptime_seconds,
|
||||
+ (unsigned)context.battery_millivolts, (unsigned long)context.storage_used_kb,
|
||||
+ (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);
|
||||
+ case BOT_COMMAND_UNKNOWN:
|
||||
+ return writeText(output, output_len, "Unknown command. Try !help");
|
||||
+ default:
|
||||
+ return makeResult(BOT_COMMAND_RESULT_NOT_HANDLED, 0);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/BotCommands.h b/examples/companion_radio/BotCommands.h
|
||||
new file mode 100644
|
||||
index 00000000..b0b4cddb
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/BotCommands.h
|
||||
@@ -0,0 +1,10 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace BotCommands {
|
||||
+
|
||||
+BotCommandResult executeCommand(const BotCommand& command, const BotCommandContext& context, char* output,
|
||||
+ size_t output_len);
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
index 869ae3ae..a7575a7c 100644
|
||||
--- a/examples/companion_radio/BotTypes.h
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -9,6 +9,9 @@
|
||||
#define BOT_MAX_COMMAND_ARGS_LEN 79
|
||||
#define BOT_MAX_CHANNEL_NAME_LEN 23
|
||||
#define BOT_MAX_SENDER_NAME_LEN 31
|
||||
+#define BOT_GROUP_RESPONSE_PREFIX_RESERVE (BOT_MAX_SENDER_NAME_LEN + 2)
|
||||
+#define BOT_MAX_GROUP_RESPONSE_LEN (BOT_MAX_TEXT_LEN - BOT_GROUP_RESPONSE_PREFIX_RESERVE)
|
||||
+#define BOT_COMMAND_COOLDOWN_MILLIS 5000UL
|
||||
#define BOT_SENDER_KEY_PREFIX_LEN 6
|
||||
|
||||
enum BotChannelKind : uint8_t {
|
||||
@@ -35,9 +38,17 @@ enum BotCommandId : uint8_t {
|
||||
BOT_COMMAND_ABOUT,
|
||||
BOT_COMMAND_DICE,
|
||||
BOT_COMMAND_STATUS,
|
||||
+ BOT_COMMAND_CHANNELS,
|
||||
BOT_COMMAND_UNKNOWN
|
||||
};
|
||||
|
||||
+enum BotCommandResultCode : uint8_t {
|
||||
+ BOT_COMMAND_RESULT_NOT_HANDLED = 0,
|
||||
+ BOT_COMMAND_RESULT_OK,
|
||||
+ BOT_COMMAND_RESULT_TRUNCATED,
|
||||
+ BOT_COMMAND_RESULT_NO_SPACE
|
||||
+};
|
||||
+
|
||||
enum BotWriteResult : uint8_t {
|
||||
BOT_WRITE_OK = 0,
|
||||
BOT_WRITE_TRUNCATED,
|
||||
@@ -73,6 +84,31 @@ struct BotResponse {
|
||||
bool truncated;
|
||||
};
|
||||
|
||||
+struct BotCommandContext {
|
||||
+ char node_name[BOT_MAX_SENDER_NAME_LEN + 1];
|
||||
+ uint32_t uptime_seconds;
|
||||
+ uint16_t battery_millivolts;
|
||||
+ uint32_t storage_used_kb;
|
||||
+ uint32_t storage_total_kb;
|
||||
+ uint32_t observed_messages;
|
||||
+ uint32_t ignored_messages;
|
||||
+ uint32_t eligible_messages;
|
||||
+ uint32_t sent_messages;
|
||||
+ uint32_t send_failures;
|
||||
+ uint32_t random_seed;
|
||||
+ uint8_t channel_count;
|
||||
+};
|
||||
+
|
||||
+struct BotCommandResult {
|
||||
+ BotCommandResultCode code;
|
||||
+ size_t text_len;
|
||||
+};
|
||||
+
|
||||
+struct BotCommandCooldown {
|
||||
+ BotCommandId command_id;
|
||||
+ uint32_t expires_at_millis;
|
||||
+};
|
||||
+
|
||||
struct BotPrefs {
|
||||
bool enabled;
|
||||
uint16_t normal_delay_ms;
|
||||
@@ -90,10 +126,15 @@ struct BotStats {
|
||||
uint32_t eligible_messages;
|
||||
uint32_t emergency_messages;
|
||||
uint32_t parse_errors;
|
||||
+ uint32_t sent_messages;
|
||||
+ uint32_t send_failures;
|
||||
};
|
||||
|
||||
static_assert(sizeof(BotMessage) <= 240, "BotMessage RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommand) <= 120, "BotCommand RAM budget exceeded");
|
||||
static_assert(sizeof(BotResponse) <= 184, "BotResponse RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCommandContext) <= 96, "BotCommandContext RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCommandResult) <= 16, "BotCommandResult RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCommandCooldown) <= 8, "BotCommandCooldown RAM budget exceeded");
|
||||
static_assert(sizeof(BotPrefs) <= 128, "BotPrefs RAM budget exceeded");
|
||||
-static_assert(sizeof(BotStats) <= 32, "BotStats RAM budget exceeded");
|
||||
+static_assert(sizeof(BotStats) <= 48, "BotStats RAM budget exceeded");
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
index 2d4b7395..78b66937 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -111,9 +111,44 @@ BotCommandId commandIdForName(const char* name, size_t len) {
|
||||
if (namesEqual(name, len, "about")) return BOT_COMMAND_ABOUT;
|
||||
if (namesEqual(name, len, "dice") || namesEqual(name, len, "roll")) return BOT_COMMAND_DICE;
|
||||
if (namesEqual(name, len, "status")) return BOT_COMMAND_STATUS;
|
||||
+ if (namesEqual(name, len, "channels")) return BOT_COMMAND_CHANNELS;
|
||||
return BOT_COMMAND_UNKNOWN;
|
||||
}
|
||||
|
||||
+size_t maxResponseLenForChannel(BotChannelKind channel_kind) {
|
||||
+ return channel_kind == BOT_CHANNEL_DM ? BOT_MAX_RESPONSE_LEN : BOT_MAX_GROUP_RESPONSE_LEN;
|
||||
+}
|
||||
+
|
||||
+bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id,
|
||||
+ uint32_t now_millis) {
|
||||
+ if (!cooldowns || command_id == BOT_COMMAND_NONE) return false;
|
||||
+ for (size_t i = 0; i < cooldown_count; i++) {
|
||||
+ if (cooldowns[i].command_id == command_id && (int32_t)(cooldowns[i].expires_at_millis - now_millis) > 0) return true;
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+void recordCommandCooldown(BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id,
|
||||
+ uint32_t now_millis, uint32_t cooldown_millis) {
|
||||
+ if (!cooldowns || cooldown_count == 0 || command_id == BOT_COMMAND_NONE || cooldown_millis == 0) return;
|
||||
+
|
||||
+ size_t slot = cooldown_count;
|
||||
+ for (size_t i = 0; i < cooldown_count; i++) {
|
||||
+ if (cooldowns[i].command_id == command_id) {
|
||||
+ slot = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (slot == cooldown_count &&
|
||||
+ (cooldowns[i].command_id == BOT_COMMAND_NONE || (int32_t)(cooldowns[i].expires_at_millis - now_millis) <= 0)) {
|
||||
+ slot = i;
|
||||
+ }
|
||||
+ }
|
||||
+ if (slot == cooldown_count) slot = 0;
|
||||
+
|
||||
+ cooldowns[slot].command_id = command_id;
|
||||
+ cooldowns[slot].expires_at_millis = now_millis + cooldown_millis;
|
||||
+}
|
||||
+
|
||||
bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
if (!command) return false;
|
||||
memset(command, 0, sizeof(*command));
|
||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
||||
index 8b267b89..b718985f 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.h
|
||||
+++ b/examples/companion_radio/FirmwareBot.h
|
||||
@@ -11,5 +11,10 @@ bool splitChannelText(const char* text, size_t text_len, char* sender, size_t se
|
||||
BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written);
|
||||
BotFingerprint fingerprintFor(const BotMessage& message);
|
||||
BotCommandId commandIdForName(const char* name, size_t len);
|
||||
+size_t maxResponseLenForChannel(BotChannelKind channel_kind);
|
||||
+bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id,
|
||||
+ uint32_t now_millis);
|
||||
+void recordCommandCooldown(BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id,
|
||||
+ uint32_t now_millis, uint32_t cooldown_millis);
|
||||
|
||||
}
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index 1596875a..72873d37 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <Mesh.h>
|
||||
|
||||
#if CMESH_BOT_ENABLED
|
||||
+#include "BotCommands.h"
|
||||
#include "BotPolicy.h"
|
||||
#include "FirmwareBot.h"
|
||||
#endif
|
||||
@@ -567,10 +568,11 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
||||
message.sender_timestamp = sender_timestamp;
|
||||
FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN), message.text, sizeof(message.text),
|
||||
&message.text_len);
|
||||
- recordBotObservation(message);
|
||||
+ recordBotObservation(message, &from, 0xFF);
|
||||
}
|
||||
|
||||
-void MyMesh::observeBotChannelMessage(const char *channel_name, const char *text, uint32_t sender_timestamp) {
|
||||
+void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
||||
+ uint32_t sender_timestamp) {
|
||||
BotMessage message;
|
||||
memset(&message, 0, sizeof(message));
|
||||
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
|
||||
@@ -585,10 +587,60 @@ void MyMesh::observeBotChannelMessage(const char *channel_name, const char *text
|
||||
size_t body_len = botBoundedStrLen(text, BOT_MAX_TEXT_LEN);
|
||||
FirmwareBot::splitChannelText(text, body_len, message.sender_name, sizeof(message.sender_name), &body, &body_len);
|
||||
FirmwareBot::normalizeText(body, body_len, message.text, sizeof(message.text), &message.text_len);
|
||||
- recordBotObservation(message);
|
||||
+ recordBotObservation(message, NULL, channel_idx);
|
||||
+}
|
||||
+
|
||||
+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));
|
||||
+ context.uptime_seconds = _ms->getMillis() / 1000;
|
||||
+ context.observed_messages = bot_stats.observed_messages;
|
||||
+ context.ignored_messages = bot_stats.ignored_messages;
|
||||
+ context.eligible_messages = bot_stats.eligible_messages;
|
||||
+ context.sent_messages = bot_stats.sent_messages;
|
||||
+ context.send_failures = bot_stats.send_failures;
|
||||
+ if (command_id == BOT_COMMAND_DICE) {
|
||||
+ getRNG()->random((uint8_t *)&context.random_seed, sizeof(context.random_seed));
|
||||
+ }
|
||||
+ if (command_id == BOT_COMMAND_STATUS) {
|
||||
+ context.battery_millivolts = board.getBattMilliVolts();
|
||||
+ context.storage_used_kb = _store->getStorageUsedKb();
|
||||
+ context.storage_total_kb = _store->getStorageTotalKb();
|
||||
+ }
|
||||
+ if (command_id == BOT_COMMAND_CHANNELS) {
|
||||
+ for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
+ ChannelDetails channel;
|
||||
+ if (getChannel(i, channel) && channel.name[0]) context.channel_count++;
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
|
||||
-void MyMesh::recordBotObservation(const BotMessage &message) {
|
||||
+bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
+ const char *text, size_t text_len) {
|
||||
+ for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) {
|
||||
+ PendingBotResponse *pending = &pending_bot_responses[i];
|
||||
+ if (!pending->active) {
|
||||
+ pending->direct = message.channel_kind == BOT_CHANNEL_DM;
|
||||
+ if (pending->direct) {
|
||||
+ if (!direct_recipient) return false;
|
||||
+ memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key));
|
||||
+ } else {
|
||||
+ memset(pending->recipient_pub_key, 0, sizeof(pending->recipient_pub_key));
|
||||
+ }
|
||||
+ pending->channel_idx = channel_idx;
|
||||
+ pending->text_len = text_len;
|
||||
+ size_t max_text_len = FirmwareBot::maxResponseLenForChannel(message.channel_kind);
|
||||
+ if (pending->text_len > max_text_len) pending->text_len = max_text_len;
|
||||
+ if (pending->text_len > 0) memcpy(pending->text, text, pending->text_len);
|
||||
+ pending->text[pending->text_len] = 0;
|
||||
+ pending->active = true;
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
|
||||
bot_stats.observed_messages++;
|
||||
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
||||
if (decision == BOT_POLICY_IGNORE) {
|
||||
@@ -601,10 +653,67 @@ void MyMesh::recordBotObservation(const BotMessage &message) {
|
||||
}
|
||||
|
||||
BotCommand command;
|
||||
- if (FirmwareBot::parseCommand(message.text, message.text_len, &command)) {
|
||||
- bot_stats.eligible_messages++;
|
||||
- } else if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) {
|
||||
+ if (!FirmwareBot::parseCommand(message.text, message.text_len, &command)) {
|
||||
+ if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (FirmwareBot::isCommandOnCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis())) {
|
||||
+ bot_stats.ignored_messages++;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ BotCommandContext context;
|
||||
+ buildBotCommandContext(context, command.id);
|
||||
+ char response[BOT_MAX_RESPONSE_LEN + 1];
|
||||
+ BotCommandResult result = BotCommands::executeCommand(command, context, response, sizeof(response));
|
||||
+ if (result.code == BOT_COMMAND_RESULT_NOT_HANDLED || result.code == BOT_COMMAND_RESULT_NO_SPACE || result.text_len == 0) {
|
||||
bot_stats.parse_errors++;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ bot_stats.eligible_messages++;
|
||||
+ FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(),
|
||||
+ BOT_COMMAND_COOLDOWN_MILLIS);
|
||||
+ if (!enqueueBotResponse(message, direct_recipient, channel_idx, response, result.text_len)) {
|
||||
+ bot_stats.send_failures++;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+void MyMesh::sendQueuedBotResponses() {
|
||||
+ for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) {
|
||||
+ PendingBotResponse *pending = &pending_bot_responses[i];
|
||||
+ if (!pending->active) continue;
|
||||
+
|
||||
+ bool success = false;
|
||||
+ if (pending->direct) {
|
||||
+ ContactInfo *recipient = lookupContactByPubKey(pending->recipient_pub_key, PUB_KEY_SIZE);
|
||||
+ if (recipient) {
|
||||
+ uint32_t expected_ack = 0;
|
||||
+ uint32_t est_timeout = 0;
|
||||
+ uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
+ int result = sendMessage(*recipient, timestamp, 0, pending->text, expected_ack, est_timeout);
|
||||
+ success = result != MSG_SEND_FAILED;
|
||||
+ if (success && expected_ack) {
|
||||
+ expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis();
|
||||
+ expected_ack_table[next_ack_idx].ack = expected_ack;
|
||||
+ expected_ack_table[next_ack_idx].contact = recipient;
|
||||
+ next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
|
||||
+ }
|
||||
+ }
|
||||
+ } else if (pending->channel_idx != 0xFF) {
|
||||
+ ChannelDetails channel;
|
||||
+ uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
+ success = getChannel(pending->channel_idx, channel) &&
|
||||
+ sendGroupMessage(timestamp, channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
||||
+ }
|
||||
+
|
||||
+ if (success) {
|
||||
+ bot_stats.sent_messages++;
|
||||
+ } else {
|
||||
+ bot_stats.send_failures++;
|
||||
+ }
|
||||
+ pending->active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,6 +745,7 @@ void MyMesh::scheduleBotFloodAdvert(unsigned long interval_millis) {
|
||||
}
|
||||
|
||||
void MyMesh::tickBot() {
|
||||
+ sendQueuedBotResponses();
|
||||
if (next_bot_local_advert && millisHasNowPassed(next_bot_local_advert)) {
|
||||
sendBotSelfAdvert(false);
|
||||
scheduleBotLocalAdvert(BOT_AUTO_ADVERT_INTERVAL_MILLIS);
|
||||
@@ -692,7 +802,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
|
||||
#endif
|
||||
#if CMESH_BOT_ENABLED
|
||||
- observeBotChannelMessage(channel_name, text, timestamp);
|
||||
+ observeBotChannelMessage(channel_idx, channel_name, text, timestamp);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -977,6 +1087,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||
#if CMESH_BOT_ENABLED
|
||||
memset(&bot_stats, 0, sizeof(bot_stats));
|
||||
+ memset(pending_bot_responses, 0, sizeof(pending_bot_responses));
|
||||
+ memset(bot_command_cooldowns, 0, sizeof(bot_command_cooldowns));
|
||||
next_bot_local_advert = 0;
|
||||
next_bot_flood_advert = 0;
|
||||
#endif
|
||||
@@ -992,6 +1104,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.tx_power_dbm = LORA_TX_POWER;
|
||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
||||
+#if CMESH_BOT_ENABLED
|
||||
+ _prefs.path_hash_mode = 1;
|
||||
+#endif
|
||||
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
|
||||
#if defined(USE_SX1262) || defined(USE_SX1268)
|
||||
#ifdef SX126X_RX_BOOSTED_GAIN
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index dc375c5b..ed915b28 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -9,6 +9,8 @@
|
||||
#endif
|
||||
#if CMESH_BOT_ENABLED
|
||||
#include "BotTypes.h"
|
||||
+#define BOT_PENDING_RESPONSE_SLOTS 2
|
||||
+#define BOT_COMMAND_COOLDOWN_SLOTS 9
|
||||
#endif
|
||||
|
||||
/*------------ Frame Protocol --------------*/
|
||||
@@ -202,8 +204,13 @@ private:
|
||||
#if CMESH_BOT_ENABLED
|
||||
void observeBotDirectMessage(const ContactInfo &from, uint32_t sender_timestamp, const uint8_t *sender_prefix,
|
||||
size_t sender_prefix_len, const char *text);
|
||||
- void observeBotChannelMessage(const char *channel_name, const char *text, uint32_t sender_timestamp);
|
||||
- void recordBotObservation(const BotMessage &message);
|
||||
+ void observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
||||
+ uint32_t sender_timestamp);
|
||||
+ void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx);
|
||||
+ void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id);
|
||||
+ bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
+ const char *text, size_t text_len);
|
||||
+ void sendQueuedBotResponses();
|
||||
void tickBot();
|
||||
void scheduleBotLocalAdvert(unsigned long interval_millis);
|
||||
void scheduleBotFloodAdvert(unsigned long interval_millis);
|
||||
@@ -242,7 +249,18 @@ private:
|
||||
TransportKey send_scope;
|
||||
|
||||
#if CMESH_BOT_ENABLED
|
||||
+ struct PendingBotResponse {
|
||||
+ bool active;
|
||||
+ bool direct;
|
||||
+ uint8_t recipient_pub_key[PUB_KEY_SIZE];
|
||||
+ uint8_t channel_idx;
|
||||
+ char text[BOT_MAX_RESPONSE_LEN + 1];
|
||||
+ size_t text_len;
|
||||
+ };
|
||||
+
|
||||
BotStats bot_stats;
|
||||
+ PendingBotResponse pending_bot_responses[BOT_PENDING_RESPONSE_SLOTS];
|
||||
+ BotCommandCooldown bot_command_cooldowns[BOT_COMMAND_COOLDOWN_SLOTS];
|
||||
unsigned long next_bot_local_advert;
|
||||
unsigned long next_bot_flood_advert;
|
||||
#endif
|
||||
@@ -1,419 +0,0 @@
|
||||
diff --git a/examples/companion_radio/BotPolicy.cpp b/examples/companion_radio/BotPolicy.cpp
|
||||
index bd3fc6c7..1cf75c73 100644
|
||||
--- a/examples/companion_radio/BotPolicy.cpp
|
||||
+++ b/examples/companion_radio/BotPolicy.cpp
|
||||
@@ -26,16 +26,26 @@ bool equalsIgnoreCase(const char* value, size_t len, const char* expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+bool equalsExact(const char* value, size_t len, const char* expected) {
|
||||
+ if (!value || !expected) return false;
|
||||
+ size_t expected_len = strlen(expected);
|
||||
+ if (len != expected_len) return false;
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ if (value[i] != expected[i]) return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
}
|
||||
|
||||
namespace BotPolicy {
|
||||
|
||||
BotChannelKind classifyChannel(const char* name, size_t len, bool direct_message) {
|
||||
if (direct_message) return BOT_CHANNEL_DM;
|
||||
- if (equalsIgnoreCase(name, len, "Public")) return BOT_CHANNEL_PUBLIC;
|
||||
+ if (equalsExact(name, len, "Public")) return BOT_CHANNEL_PUBLIC;
|
||||
if (equalsIgnoreCase(name, len, "#bot")) return BOT_CHANNEL_BOT;
|
||||
if (equalsIgnoreCase(name, len, "#testing")) return BOT_CHANNEL_TESTING;
|
||||
- if (equalsIgnoreCase(name, len, "#emergency")) return BOT_CHANNEL_EMERGENCY;
|
||||
+ if (equalsExact(name, len, "#emergency")) return BOT_CHANNEL_EMERGENCY;
|
||||
return BOT_CHANNEL_OTHER;
|
||||
}
|
||||
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
index a7575a7c..1e6f72c1 100644
|
||||
--- a/examples/companion_radio/BotTypes.h
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -12,6 +12,11 @@
|
||||
#define BOT_GROUP_RESPONSE_PREFIX_RESERVE (BOT_MAX_SENDER_NAME_LEN + 2)
|
||||
#define BOT_MAX_GROUP_RESPONSE_LEN (BOT_MAX_TEXT_LEN - BOT_GROUP_RESPONSE_PREFIX_RESERVE)
|
||||
#define BOT_COMMAND_COOLDOWN_MILLIS 5000UL
|
||||
+#define BOT_EMERGENCY_PREFIX "EMERGENCY MESSAGE FROM "
|
||||
+#define BOT_EMERGENCY_MAX_PARTS 3
|
||||
+#define BOT_EMERGENCY_RATE_LIMIT_MILLIS 60000UL
|
||||
+#define BOT_EMERGENCY_RATE_LIMIT_COUNT 3
|
||||
+#define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS
|
||||
#define BOT_SENDER_KEY_PREFIX_LEN 6
|
||||
|
||||
enum BotChannelKind : uint8_t {
|
||||
@@ -64,6 +69,7 @@ struct BotMessage {
|
||||
char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
||||
char sender_name[BOT_MAX_SENDER_NAME_LEN + 1];
|
||||
uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN];
|
||||
+ bool text_truncated;
|
||||
uint32_t sender_timestamp;
|
||||
char text[BOT_MAX_TEXT_LEN + 1];
|
||||
size_t text_len;
|
||||
@@ -109,6 +115,13 @@ struct BotCommandCooldown {
|
||||
uint32_t expires_at_millis;
|
||||
};
|
||||
|
||||
+struct BotEmergencyForward {
|
||||
+ uint8_t part_count;
|
||||
+ bool truncated;
|
||||
+ char parts[BOT_EMERGENCY_MAX_PARTS][BOT_MAX_GROUP_RESPONSE_LEN + 1];
|
||||
+ size_t part_lens[BOT_EMERGENCY_MAX_PARTS];
|
||||
+};
|
||||
+
|
||||
struct BotPrefs {
|
||||
bool enabled;
|
||||
uint16_t normal_delay_ms;
|
||||
@@ -125,6 +138,8 @@ struct BotStats {
|
||||
uint32_t ignored_messages;
|
||||
uint32_t eligible_messages;
|
||||
uint32_t emergency_messages;
|
||||
+ uint32_t emergency_forwards;
|
||||
+ uint32_t emergency_forward_failures;
|
||||
uint32_t parse_errors;
|
||||
uint32_t sent_messages;
|
||||
uint32_t send_failures;
|
||||
@@ -136,5 +151,6 @@ static_assert(sizeof(BotResponse) <= 184, "BotResponse RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandContext) <= 96, "BotCommandContext RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandResult) <= 16, "BotCommandResult RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandCooldown) <= 8, "BotCommandCooldown RAM budget exceeded");
|
||||
+static_assert(sizeof(BotEmergencyForward) <= 480, "BotEmergencyForward RAM budget exceeded");
|
||||
static_assert(sizeof(BotPrefs) <= 128, "BotPrefs RAM budget exceeded");
|
||||
static_assert(sizeof(BotStats) <= 48, "BotStats RAM budget exceeded");
|
||||
diff --git a/examples/companion_radio/EmergencyForwarder.cpp b/examples/companion_radio/EmergencyForwarder.cpp
|
||||
new file mode 100644
|
||||
index 00000000..b228a8fa
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/EmergencyForwarder.cpp
|
||||
@@ -0,0 +1,118 @@
|
||||
+#include "EmergencyForwarder.h"
|
||||
+
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+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;
|
||||
+}
|
||||
+
|
||||
+bool prefixEqual(const char* text, size_t text_len, const char* prefix) {
|
||||
+ size_t prefix_len = strlen(prefix);
|
||||
+ if (text_len < prefix_len) return false;
|
||||
+ for (size_t i = 0; i < prefix_len; i++) {
|
||||
+ if (text[i] != prefix[i]) return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+size_t appendText(char* output, size_t output_len, size_t pos, const char* text, size_t text_len) {
|
||||
+ if (!output || output_len == 0) return 0;
|
||||
+ while (pos + 1 < output_len && text_len > 0) {
|
||||
+ output[pos++] = *text++;
|
||||
+ text_len--;
|
||||
+ }
|
||||
+ output[pos] = 0;
|
||||
+ return pos;
|
||||
+}
|
||||
+
|
||||
+size_t appendRepeated(char* output, size_t output_len, size_t pos, char ch, size_t count) {
|
||||
+ while (pos + 1 < output_len && count > 0) {
|
||||
+ output[pos++] = ch;
|
||||
+ count--;
|
||||
+ }
|
||||
+ output[pos] = 0;
|
||||
+ return pos;
|
||||
+}
|
||||
+
|
||||
+void writePart(BotEmergencyForward& forward, uint8_t part_idx, const char* header, size_t header_len,
|
||||
+ const char* text, size_t text_len, bool multipart) {
|
||||
+ char* output = forward.parts[part_idx];
|
||||
+ size_t output_len = sizeof(forward.parts[part_idx]);
|
||||
+ output[0] = 0;
|
||||
+ size_t pos = appendText(output, output_len, 0, header, header_len);
|
||||
+
|
||||
+ if (multipart) {
|
||||
+ char marker[8];
|
||||
+ int n = snprintf(marker, sizeof(marker), "[%u/%u] ", (unsigned)(part_idx + 1), (unsigned)forward.part_count);
|
||||
+ if (n > 0) pos = appendText(output, output_len, pos, marker, (size_t)n);
|
||||
+ }
|
||||
+
|
||||
+ pos = appendText(output, output_len, pos, text, text_len);
|
||||
+ forward.part_lens[part_idx] = pos;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace EmergencyForwarder {
|
||||
+
|
||||
+bool isForwardedEmergencyText(const char* text, size_t text_len) {
|
||||
+ if (!text) return false;
|
||||
+ size_t len = boundedStrLen(text, text_len);
|
||||
+ return prefixEqual(text, len, BOT_EMERGENCY_PREFIX);
|
||||
+}
|
||||
+
|
||||
+bool format(const BotMessage& message, BotEmergencyForward& forward) {
|
||||
+ memset(&forward, 0, sizeof(forward));
|
||||
+ if (message.channel_kind != BOT_CHANNEL_EMERGENCY) return false;
|
||||
+ if (isForwardedEmergencyText(message.text, message.text_len)) return false;
|
||||
+
|
||||
+ char header[BOT_MAX_GROUP_RESPONSE_LEN + 1];
|
||||
+ const char* sender = message.sender_name[0] ? message.sender_name : "unknown";
|
||||
+ int header_len_int = snprintf(header, sizeof(header), BOT_EMERGENCY_PREFIX "%s: ", sender);
|
||||
+ if (header_len_int < 0) return false;
|
||||
+ size_t header_len = (size_t)header_len_int;
|
||||
+ if (header_len >= sizeof(header)) header_len = sizeof(header) - 1;
|
||||
+ if (header_len >= BOT_MAX_GROUP_RESPONSE_LEN) return false;
|
||||
+
|
||||
+ size_t text_len = boundedStrLen(message.text, message.text_len);
|
||||
+ size_t one_part_capacity = BOT_MAX_GROUP_RESPONSE_LEN - header_len;
|
||||
+ if (text_len <= one_part_capacity) {
|
||||
+ forward.part_count = 1;
|
||||
+ forward.truncated = message.text_truncated;
|
||||
+ writePart(forward, 0, header, header_len, message.text, text_len, false);
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ size_t multipart_header_extra = 6;
|
||||
+ if (header_len + multipart_header_extra >= BOT_MAX_GROUP_RESPONSE_LEN) return false;
|
||||
+ size_t part_capacity = BOT_MAX_GROUP_RESPONSE_LEN - header_len - multipart_header_extra;
|
||||
+ size_t needed_parts = (text_len + part_capacity - 1) / part_capacity;
|
||||
+ forward.part_count = needed_parts > BOT_EMERGENCY_MAX_PARTS ? BOT_EMERGENCY_MAX_PARTS : (uint8_t)needed_parts;
|
||||
+ forward.truncated = message.text_truncated || needed_parts > BOT_EMERGENCY_MAX_PARTS;
|
||||
+
|
||||
+ size_t offset = 0;
|
||||
+ for (uint8_t i = 0; i < forward.part_count; i++) {
|
||||
+ size_t chunk_len = text_len - offset;
|
||||
+ if (chunk_len > part_capacity) chunk_len = part_capacity;
|
||||
+ writePart(forward, i, header, header_len, &message.text[offset], chunk_len, true);
|
||||
+ offset += chunk_len;
|
||||
+ }
|
||||
+
|
||||
+ if (forward.truncated && forward.part_count > 0) {
|
||||
+ uint8_t last = forward.part_count - 1;
|
||||
+ size_t pos = forward.part_lens[last];
|
||||
+ if (pos > 3) pos -= 3;
|
||||
+ forward.parts[last][pos] = 0;
|
||||
+ pos = appendRepeated(forward.parts[last], sizeof(forward.parts[last]), pos, '.', 3);
|
||||
+ forward.part_lens[last] = pos;
|
||||
+ }
|
||||
+
|
||||
+ return forward.part_count > 0;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/EmergencyForwarder.h b/examples/companion_radio/EmergencyForwarder.h
|
||||
new file mode 100644
|
||||
index 00000000..e59d29e2
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/EmergencyForwarder.h
|
||||
@@ -0,0 +1,10 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace EmergencyForwarder {
|
||||
+
|
||||
+bool isForwardedEmergencyText(const char* text, size_t text_len);
|
||||
+bool format(const BotMessage& message, BotEmergencyForward& forward);
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index 72873d37..ad72ccc5 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -6,6 +6,7 @@
|
||||
#if CMESH_BOT_ENABLED
|
||||
#include "BotCommands.h"
|
||||
#include "BotPolicy.h"
|
||||
+#include "EmergencyForwarder.h"
|
||||
#include "FirmwareBot.h"
|
||||
#endif
|
||||
|
||||
@@ -566,8 +567,8 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
||||
if (prefix_len > sizeof(message.sender_key_prefix)) prefix_len = sizeof(message.sender_key_prefix);
|
||||
if (sender_prefix && prefix_len > 0) memcpy(message.sender_key_prefix, sender_prefix, prefix_len);
|
||||
message.sender_timestamp = sender_timestamp;
|
||||
- FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN), message.text, sizeof(message.text),
|
||||
- &message.text_len);
|
||||
+ message.text_truncated = FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN + 1), message.text,
|
||||
+ sizeof(message.text), &message.text_len) == BOT_WRITE_TRUNCATED;
|
||||
recordBotObservation(message, &from, 0xFF);
|
||||
}
|
||||
|
||||
@@ -586,7 +587,8 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n
|
||||
const char *body = text;
|
||||
size_t body_len = botBoundedStrLen(text, BOT_MAX_TEXT_LEN);
|
||||
FirmwareBot::splitChannelText(text, body_len, message.sender_name, sizeof(message.sender_name), &body, &body_len);
|
||||
- FirmwareBot::normalizeText(body, body_len, message.text, sizeof(message.text), &message.text_len);
|
||||
+ message.text_truncated = FirmwareBot::normalizeText(body, body_len, message.text, sizeof(message.text),
|
||||
+ &message.text_len) == BOT_WRITE_TRUNCATED;
|
||||
recordBotObservation(message, NULL, channel_idx);
|
||||
}
|
||||
|
||||
@@ -640,6 +642,63 @@ bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *di
|
||||
return false;
|
||||
}
|
||||
|
||||
+bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) {
|
||||
+ for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
+ 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) {
|
||||
+ channel_idx = i;
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+bool MyMesh::isEmergencyRateLimited() {
|
||||
+ unsigned long now = _ms->getMillis();
|
||||
+ if (!emergency_rate_window_started || millisHasNowPassed(emergency_rate_window_started + BOT_EMERGENCY_RATE_LIMIT_MILLIS)) {
|
||||
+ emergency_rate_window_started = now;
|
||||
+ emergency_rate_count = 0;
|
||||
+ return false;
|
||||
+ }
|
||||
+ return emergency_rate_count >= BOT_EMERGENCY_RATE_LIMIT_COUNT;
|
||||
+}
|
||||
+
|
||||
+void MyMesh::recordEmergencyRateLimitEvent() {
|
||||
+ if (emergency_rate_count < 0xFF) emergency_rate_count++;
|
||||
+}
|
||||
+
|
||||
+bool MyMesh::enqueueEmergencyForward(const BotMessage &message) {
|
||||
+ if (isEmergencyRateLimited()) return false;
|
||||
+
|
||||
+ BotEmergencyForward forward;
|
||||
+ if (!EmergencyForwarder::format(message, forward)) return false;
|
||||
+
|
||||
+ uint8_t free_slots = 0;
|
||||
+ for (size_t i = 0; i < BOT_PENDING_EMERGENCY_SLOTS; i++) {
|
||||
+ if (!pending_emergency_forwards[i].active) free_slots++;
|
||||
+ }
|
||||
+ if (free_slots < forward.part_count) return false;
|
||||
+
|
||||
+ uint8_t part_idx = 0;
|
||||
+ for (size_t i = 0; i < BOT_PENDING_EMERGENCY_SLOTS && part_idx < forward.part_count; i++) {
|
||||
+ PendingEmergencyForward *pending = &pending_emergency_forwards[i];
|
||||
+ if (!pending->active) {
|
||||
+ pending->text_len = forward.part_lens[part_idx];
|
||||
+ if (pending->text_len > BOT_MAX_GROUP_RESPONSE_LEN) pending->text_len = BOT_MAX_GROUP_RESPONSE_LEN;
|
||||
+ if (pending->text_len > 0) memcpy(pending->text, forward.parts[part_idx], pending->text_len);
|
||||
+ pending->text[pending->text_len] = 0;
|
||||
+ pending->active = true;
|
||||
+ part_idx++;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ recordEmergencyRateLimitEvent();
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
|
||||
bot_stats.observed_messages++;
|
||||
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
||||
@@ -649,6 +708,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||
}
|
||||
if (decision == BOT_POLICY_EMERGENCY_FORWARD) {
|
||||
bot_stats.emergency_messages++;
|
||||
+ if (!enqueueEmergencyForward(message)) bot_stats.emergency_forward_failures++;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -717,6 +777,31 @@ void MyMesh::sendQueuedBotResponses() {
|
||||
}
|
||||
}
|
||||
|
||||
+void MyMesh::sendQueuedEmergencyForwards() {
|
||||
+ uint8_t public_channel_idx = 0xFF;
|
||||
+ bool have_public = findBotChannel(BOT_CHANNEL_PUBLIC, public_channel_idx);
|
||||
+ ChannelDetails public_channel;
|
||||
+ if (have_public) have_public = getChannel(public_channel_idx, public_channel);
|
||||
+
|
||||
+ for (size_t i = 0; i < BOT_PENDING_EMERGENCY_SLOTS; i++) {
|
||||
+ PendingEmergencyForward *pending = &pending_emergency_forwards[i];
|
||||
+ if (!pending->active) continue;
|
||||
+
|
||||
+ bool success = false;
|
||||
+ if (have_public) {
|
||||
+ uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
+ success = sendGroupMessage(timestamp, public_channel.channel, _prefs.node_name, pending->text, pending->text_len);
|
||||
+ }
|
||||
+
|
||||
+ if (success) {
|
||||
+ bot_stats.emergency_forwards++;
|
||||
+ } else {
|
||||
+ bot_stats.emergency_forward_failures++;
|
||||
+ }
|
||||
+ pending->active = false;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
bool MyMesh::sendBotSelfAdvert(bool flood) {
|
||||
mesh::Packet* pkt;
|
||||
if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) {
|
||||
@@ -745,6 +830,7 @@ void MyMesh::scheduleBotFloodAdvert(unsigned long interval_millis) {
|
||||
}
|
||||
|
||||
void MyMesh::tickBot() {
|
||||
+ sendQueuedEmergencyForwards();
|
||||
sendQueuedBotResponses();
|
||||
if (next_bot_local_advert && millisHasNowPassed(next_bot_local_advert)) {
|
||||
sendBotSelfAdvert(false);
|
||||
@@ -1088,7 +1174,10 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
#if CMESH_BOT_ENABLED
|
||||
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));
|
||||
memset(bot_command_cooldowns, 0, sizeof(bot_command_cooldowns));
|
||||
+ emergency_rate_window_started = 0;
|
||||
+ emergency_rate_count = 0;
|
||||
next_bot_local_advert = 0;
|
||||
next_bot_flood_advert = 0;
|
||||
#endif
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index ed915b28..10b27feb 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -210,7 +210,12 @@ private:
|
||||
void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id);
|
||||
bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
const char *text, size_t text_len);
|
||||
+ bool enqueueEmergencyForward(const BotMessage &message);
|
||||
+ bool findBotChannel(BotChannelKind kind, uint8_t &channel_idx);
|
||||
+ bool isEmergencyRateLimited();
|
||||
+ void recordEmergencyRateLimitEvent();
|
||||
void sendQueuedBotResponses();
|
||||
+ void sendQueuedEmergencyForwards();
|
||||
void tickBot();
|
||||
void scheduleBotLocalAdvert(unsigned long interval_millis);
|
||||
void scheduleBotFloodAdvert(unsigned long interval_millis);
|
||||
@@ -258,9 +263,18 @@ private:
|
||||
size_t text_len;
|
||||
};
|
||||
|
||||
+ struct PendingEmergencyForward {
|
||||
+ bool active;
|
||||
+ char text[BOT_MAX_GROUP_RESPONSE_LEN + 1];
|
||||
+ size_t text_len;
|
||||
+ };
|
||||
+
|
||||
BotStats bot_stats;
|
||||
PendingBotResponse pending_bot_responses[BOT_PENDING_RESPONSE_SLOTS];
|
||||
+ PendingEmergencyForward pending_emergency_forwards[BOT_PENDING_EMERGENCY_SLOTS];
|
||||
BotCommandCooldown bot_command_cooldowns[BOT_COMMAND_COOLDOWN_SLOTS];
|
||||
+ unsigned long emergency_rate_window_started;
|
||||
+ uint8_t emergency_rate_count;
|
||||
unsigned long next_bot_local_advert;
|
||||
unsigned long next_bot_flood_advert;
|
||||
#endif
|
||||
@@ -1,766 +0,0 @@
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
index 1e6f72c..d44aadb 100644
|
||||
--- a/examples/companion_radio/BotTypes.h
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -17,7 +17,16 @@
|
||||
#define BOT_EMERGENCY_RATE_LIMIT_MILLIS 60000UL
|
||||
#define BOT_EMERGENCY_RATE_LIMIT_COUNT 3
|
||||
#define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS
|
||||
+#define BOT_COORDINATOR_PENDING_SLOTS 8
|
||||
+#define BOT_COORDINATOR_RECENT_SLOTS 16
|
||||
+#define BOT_KNOWN_BOT_SLOTS 8
|
||||
+#define BOT_RESPONSE_DELAY_BASE_MILLIS 1200UL
|
||||
+#define BOT_RESPONSE_DELAY_JITTER_MILLIS 1800UL
|
||||
+#define BOT_RESPONSE_PENDING_TTL_MILLIS 15000UL
|
||||
+#define BOT_RESPONSE_RECENT_TTL_MILLIS 30000UL
|
||||
+#define BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL 0x01
|
||||
#define BOT_SENDER_KEY_PREFIX_LEN 6
|
||||
+#define BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN 4
|
||||
|
||||
enum BotChannelKind : uint8_t {
|
||||
BOT_CHANNEL_DM = 0,
|
||||
@@ -60,6 +69,20 @@ enum BotWriteResult : uint8_t {
|
||||
BOT_WRITE_NO_SPACE
|
||||
};
|
||||
|
||||
+enum BotCoordinatorScheduleResult : uint8_t {
|
||||
+ BOT_COORDINATOR_SCHEDULED = 0,
|
||||
+ BOT_COORDINATOR_REPLACED,
|
||||
+ BOT_COORDINATOR_NO_SPACE,
|
||||
+ BOT_COORDINATOR_NOT_NORMAL
|
||||
+};
|
||||
+
|
||||
+enum BotCoordinatorReadyResult : uint8_t {
|
||||
+ BOT_COORDINATOR_READY_NONE = 0,
|
||||
+ BOT_COORDINATOR_READY_SEND,
|
||||
+ BOT_COORDINATOR_READY_SUPPRESSED,
|
||||
+ BOT_COORDINATOR_READY_EXPIRED
|
||||
+};
|
||||
+
|
||||
struct BotFingerprint {
|
||||
uint64_t value;
|
||||
};
|
||||
@@ -69,6 +92,7 @@ struct BotMessage {
|
||||
char channel_name[BOT_MAX_CHANNEL_NAME_LEN + 1];
|
||||
char sender_name[BOT_MAX_SENDER_NAME_LEN + 1];
|
||||
uint8_t sender_key_prefix[BOT_SENDER_KEY_PREFIX_LEN];
|
||||
+ uint8_t sender_key_prefix_len;
|
||||
bool text_truncated;
|
||||
uint32_t sender_timestamp;
|
||||
char text[BOT_MAX_TEXT_LEN + 1];
|
||||
@@ -115,6 +139,34 @@ struct BotCommandCooldown {
|
||||
uint32_t expires_at_millis;
|
||||
};
|
||||
|
||||
+struct BotKnownBotEntry {
|
||||
+ bool active;
|
||||
+ uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN];
|
||||
+ uint8_t flags;
|
||||
+ char label[12];
|
||||
+};
|
||||
+
|
||||
+struct BotCoordinatorPending {
|
||||
+ bool active;
|
||||
+ bool suppressed;
|
||||
+ BotFingerprint request_fingerprint;
|
||||
+ BotFingerprint response_fingerprint;
|
||||
+ uint32_t due_at_millis;
|
||||
+ uint32_t expires_at_millis;
|
||||
+};
|
||||
+
|
||||
+struct BotCoordinatorRecent {
|
||||
+ bool active;
|
||||
+ BotFingerprint response_fingerprint;
|
||||
+ uint32_t expires_at_millis;
|
||||
+};
|
||||
+
|
||||
+struct BotCoordinatorReady {
|
||||
+ BotCoordinatorReadyResult result;
|
||||
+ BotFingerprint request_fingerprint;
|
||||
+ BotFingerprint response_fingerprint;
|
||||
+};
|
||||
+
|
||||
struct BotEmergencyForward {
|
||||
uint8_t part_count;
|
||||
bool truncated;
|
||||
@@ -141,16 +193,24 @@ struct BotStats {
|
||||
uint32_t emergency_forwards;
|
||||
uint32_t emergency_forward_failures;
|
||||
uint32_t parse_errors;
|
||||
+ uint32_t pending_responses;
|
||||
+ uint32_t suppressed_responses;
|
||||
+ uint32_t expired_responses;
|
||||
+ uint32_t known_bot_messages;
|
||||
uint32_t sent_messages;
|
||||
uint32_t send_failures;
|
||||
};
|
||||
|
||||
-static_assert(sizeof(BotMessage) <= 240, "BotMessage RAM budget exceeded");
|
||||
+static_assert(sizeof(BotMessage) <= 248, "BotMessage RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommand) <= 120, "BotCommand RAM budget exceeded");
|
||||
static_assert(sizeof(BotResponse) <= 184, "BotResponse RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandContext) <= 96, "BotCommandContext RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandResult) <= 16, "BotCommandResult RAM budget exceeded");
|
||||
static_assert(sizeof(BotCommandCooldown) <= 8, "BotCommandCooldown RAM budget exceeded");
|
||||
+static_assert(sizeof(BotKnownBotEntry) <= 24, "BotKnownBotEntry RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCoordinatorPending) <= 32, "BotCoordinatorPending RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCoordinatorRecent) <= 24, "BotCoordinatorRecent RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCoordinatorReady) <= 24, "BotCoordinatorReady RAM budget exceeded");
|
||||
static_assert(sizeof(BotEmergencyForward) <= 480, "BotEmergencyForward RAM budget exceeded");
|
||||
static_assert(sizeof(BotPrefs) <= 128, "BotPrefs RAM budget exceeded");
|
||||
-static_assert(sizeof(BotStats) <= 48, "BotStats RAM budget exceeded");
|
||||
+static_assert(sizeof(BotStats) <= 64, "BotStats RAM budget exceeded");
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
index 78b6693..56a3395 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -25,6 +25,19 @@ uint64_t fnv1aUpdateTextLower(uint64_t hash, const char* value, size_t len) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
+size_t boundedStrLen(const char* value, size_t max_len);
|
||||
+
|
||||
+uint64_t fnv1aUpdateChannel(uint64_t hash, const BotMessage& message) {
|
||||
+ hash = fnv1aUpdate(hash, (uint8_t)message.channel_kind);
|
||||
+ const char* channel_name = message.channel_name;
|
||||
+ size_t channel_name_len = boundedStrLen(message.channel_name, sizeof(message.channel_name));
|
||||
+ if (channel_name_len > 0 && channel_name[0] == '#') {
|
||||
+ channel_name++;
|
||||
+ channel_name_len--;
|
||||
+ }
|
||||
+ return fnv1aUpdateTextLower(hash, channel_name, channel_name_len);
|
||||
+}
|
||||
+
|
||||
bool isSpaceByte(char ch) {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
||||
}
|
||||
@@ -229,15 +242,9 @@ BotWriteResult writeResponse(char* output, size_t output_len, const char* text,
|
||||
|
||||
BotFingerprint fingerprintFor(const BotMessage& message) {
|
||||
uint64_t hash = 1469598103934665603ULL;
|
||||
- hash = fnv1aUpdate(hash, (uint8_t)message.channel_kind);
|
||||
- const char* channel_name = message.channel_name;
|
||||
- size_t channel_name_len = boundedStrLen(message.channel_name, sizeof(message.channel_name));
|
||||
- if (channel_name_len > 0 && channel_name[0] == '#') {
|
||||
- channel_name++;
|
||||
- channel_name_len--;
|
||||
- }
|
||||
- hash = fnv1aUpdateTextLower(hash, channel_name, channel_name_len);
|
||||
+ hash = fnv1aUpdateChannel(hash, message);
|
||||
hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, sizeof(message.sender_key_prefix));
|
||||
+ hash = fnv1aUpdateTextLower(hash, message.sender_name, boundedStrLen(message.sender_name, sizeof(message.sender_name)));
|
||||
hash = fnv1aUpdateU32(hash, message.sender_timestamp);
|
||||
|
||||
char normalized[BOT_MAX_TEXT_LEN + 1];
|
||||
@@ -249,4 +256,21 @@ BotFingerprint fingerprintFor(const BotMessage& message) {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
+BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len) {
|
||||
+ uint64_t hash = 1469598103934665603ULL;
|
||||
+ hash = fnv1aUpdateChannel(hash, message);
|
||||
+ if (message.channel_kind == BOT_CHANNEL_DM) {
|
||||
+ hash = fnv1aUpdate(hash, message.sender_key_prefix_len);
|
||||
+ hash = fnv1aUpdateBytes(hash, message.sender_key_prefix, message.sender_key_prefix_len);
|
||||
+ }
|
||||
+
|
||||
+ char normalized[BOT_MAX_RESPONSE_LEN + 1];
|
||||
+ size_t normalized_len = 0;
|
||||
+ normalizeText(response_text, response_text_len, normalized, sizeof(normalized), &normalized_len);
|
||||
+ hash = fnv1aUpdateTextLower(hash, normalized, normalized_len);
|
||||
+
|
||||
+ BotFingerprint fingerprint = { hash };
|
||||
+ return fingerprint;
|
||||
+}
|
||||
+
|
||||
}
|
||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
||||
index b718985..42b9c5b 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.h
|
||||
+++ b/examples/companion_radio/FirmwareBot.h
|
||||
@@ -10,6 +10,7 @@ bool splitChannelText(const char* text, size_t text_len, char* sender, size_t se
|
||||
size_t* body_len);
|
||||
BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written);
|
||||
BotFingerprint fingerprintFor(const BotMessage& message);
|
||||
+BotFingerprint responseFingerprintFor(const BotMessage& message, const char* response_text, size_t response_text_len);
|
||||
BotCommandId commandIdForName(const char* name, size_t len);
|
||||
size_t maxResponseLenForChannel(BotChannelKind channel_kind);
|
||||
bool isCommandOnCooldown(const BotCommandCooldown* cooldowns, size_t cooldown_count, BotCommandId command_id,
|
||||
diff --git a/examples/companion_radio/KnownBotRegistry.cpp b/examples/companion_radio/KnownBotRegistry.cpp
|
||||
new file mode 100644
|
||||
index 0000000..a3e764e
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/KnownBotRegistry.cpp
|
||||
@@ -0,0 +1,87 @@
|
||||
+#include "KnownBotRegistry.h"
|
||||
+
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+bool keyEqual(const uint8_t a[BOT_SENDER_KEY_PREFIX_LEN], const uint8_t b[BOT_SENDER_KEY_PREFIX_LEN]) {
|
||||
+ return memcmp(a, b, BOT_SENDER_KEY_PREFIX_LEN) == 0;
|
||||
+}
|
||||
+
|
||||
+bool keyPrefixEqual(const uint8_t a[BOT_SENDER_KEY_PREFIX_LEN], const uint8_t b[BOT_SENDER_KEY_PREFIX_LEN], size_t len) {
|
||||
+ return memcmp(a, b, len) == 0;
|
||||
+}
|
||||
+
|
||||
+void copyLabel(char dest[12], const char* label) {
|
||||
+ size_t i = 0;
|
||||
+ if (label) {
|
||||
+ while (i + 1 < 12 && label[i] != 0) {
|
||||
+ dest[i] = label[i];
|
||||
+ i++;
|
||||
+ }
|
||||
+ }
|
||||
+ dest[i] = 0;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace KnownBotRegistry {
|
||||
+
|
||||
+void clear(BotKnownBotEntry entries[], size_t entry_count) {
|
||||
+ if (!entries) return;
|
||||
+ memset(entries, 0, sizeof(BotKnownBotEntry) * entry_count);
|
||||
+}
|
||||
+
|
||||
+const BotKnownBotEntry* find(const BotKnownBotEntry entries[], size_t entry_count,
|
||||
+ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len) {
|
||||
+ if (!entries || !key_prefix || key_prefix_len < BOT_MIN_AUTH_SENDER_KEY_PREFIX_LEN) return NULL;
|
||||
+ if (key_prefix_len > BOT_SENDER_KEY_PREFIX_LEN) key_prefix_len = BOT_SENDER_KEY_PREFIX_LEN;
|
||||
+
|
||||
+ const BotKnownBotEntry* match = NULL;
|
||||
+ for (size_t i = 0; i < entry_count; i++) {
|
||||
+ if (!entries[i].active || !keyPrefixEqual(entries[i].key_prefix, key_prefix, key_prefix_len)) continue;
|
||||
+ if (match) return NULL;
|
||||
+ match = &entries[i];
|
||||
+ }
|
||||
+ return match;
|
||||
+}
|
||||
+
|
||||
+bool add(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN],
|
||||
+ uint8_t flags, const char* label) {
|
||||
+ if (!entries || !key_prefix || entry_count == 0) return false;
|
||||
+
|
||||
+ size_t slot = entry_count;
|
||||
+ for (size_t i = 0; i < entry_count; i++) {
|
||||
+ if (entries[i].active && keyEqual(entries[i].key_prefix, key_prefix)) {
|
||||
+ slot = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (slot == entry_count && !entries[i].active) slot = i;
|
||||
+ }
|
||||
+ if (slot == entry_count) return false;
|
||||
+
|
||||
+ entries[slot].active = true;
|
||||
+ memcpy(entries[slot].key_prefix, key_prefix, BOT_SENDER_KEY_PREFIX_LEN);
|
||||
+ entries[slot].flags = flags;
|
||||
+ copyLabel(entries[slot].label, label);
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+bool remove(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]) {
|
||||
+ if (!entries || !key_prefix) return false;
|
||||
+ for (size_t i = 0; i < entry_count; i++) {
|
||||
+ if (entries[i].active && keyEqual(entries[i].key_prefix, key_prefix)) {
|
||||
+ memset(&entries[i], 0, sizeof(entries[i]));
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+bool canSuppressNormal(const BotKnownBotEntry entries[], size_t entry_count,
|
||||
+ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len) {
|
||||
+ const BotKnownBotEntry* entry = find(entries, entry_count, key_prefix, key_prefix_len);
|
||||
+ return entry && (entry->flags & BOT_KNOWN_BOT_FLAG_SUPPRESS_NORMAL) != 0;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/KnownBotRegistry.h b/examples/companion_radio/KnownBotRegistry.h
|
||||
new file mode 100644
|
||||
index 0000000..5e93dad
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/KnownBotRegistry.h
|
||||
@@ -0,0 +1,16 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace KnownBotRegistry {
|
||||
+
|
||||
+void clear(BotKnownBotEntry entries[], size_t entry_count);
|
||||
+bool add(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN],
|
||||
+ uint8_t flags, const char* label);
|
||||
+bool remove(BotKnownBotEntry entries[], size_t entry_count, const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN]);
|
||||
+const BotKnownBotEntry* find(const BotKnownBotEntry entries[], size_t entry_count,
|
||||
+ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len);
|
||||
+bool canSuppressNormal(const BotKnownBotEntry entries[], size_t entry_count,
|
||||
+ const uint8_t key_prefix[BOT_SENDER_KEY_PREFIX_LEN], size_t key_prefix_len);
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index ad72ccc..31e9bad 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "BotPolicy.h"
|
||||
#include "EmergencyForwarder.h"
|
||||
#include "FirmwareBot.h"
|
||||
+#include "KnownBotRegistry.h"
|
||||
+#include "ResponseCoordinator.h"
|
||||
#endif
|
||||
|
||||
#define CMD_APP_START 1
|
||||
@@ -566,6 +568,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
||||
size_t prefix_len = sender_prefix_len;
|
||||
if (prefix_len > sizeof(message.sender_key_prefix)) prefix_len = sizeof(message.sender_key_prefix);
|
||||
if (sender_prefix && prefix_len > 0) memcpy(message.sender_key_prefix, sender_prefix, prefix_len);
|
||||
+ message.sender_key_prefix_len = prefix_len;
|
||||
message.sender_timestamp = sender_timestamp;
|
||||
message.text_truncated = FirmwareBot::normalizeText(text, botBoundedStrLen(text, BOT_MAX_TEXT_LEN + 1), message.text,
|
||||
sizeof(message.text), &message.text_len) == BOT_WRITE_TRUNCATED;
|
||||
@@ -618,28 +621,36 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com
|
||||
}
|
||||
|
||||
bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
- const char *text, size_t text_len) {
|
||||
+ const char *text, size_t text_len, BotFingerprint request_fingerprint,
|
||||
+ BotFingerprint response_fingerprint) {
|
||||
+ size_t slot = BOT_PENDING_RESPONSE_SLOTS;
|
||||
for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) {
|
||||
- PendingBotResponse *pending = &pending_bot_responses[i];
|
||||
- if (!pending->active) {
|
||||
- pending->direct = message.channel_kind == BOT_CHANNEL_DM;
|
||||
- if (pending->direct) {
|
||||
- if (!direct_recipient) return false;
|
||||
- memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key));
|
||||
- } else {
|
||||
- memset(pending->recipient_pub_key, 0, sizeof(pending->recipient_pub_key));
|
||||
- }
|
||||
- pending->channel_idx = channel_idx;
|
||||
- pending->text_len = text_len;
|
||||
- size_t max_text_len = FirmwareBot::maxResponseLenForChannel(message.channel_kind);
|
||||
- if (pending->text_len > max_text_len) pending->text_len = max_text_len;
|
||||
- if (pending->text_len > 0) memcpy(pending->text, text, pending->text_len);
|
||||
- pending->text[pending->text_len] = 0;
|
||||
- pending->active = true;
|
||||
- return true;
|
||||
+ if (pending_bot_responses[i].active && pending_bot_responses[i].request_fingerprint.value == request_fingerprint.value) {
|
||||
+ slot = i;
|
||||
+ break;
|
||||
}
|
||||
+ if (slot == BOT_PENDING_RESPONSE_SLOTS && !pending_bot_responses[i].active) slot = i;
|
||||
}
|
||||
- return false;
|
||||
+ if (slot == BOT_PENDING_RESPONSE_SLOTS) return false;
|
||||
+
|
||||
+ PendingBotResponse *pending = &pending_bot_responses[slot];
|
||||
+ pending->direct = message.channel_kind == BOT_CHANNEL_DM;
|
||||
+ if (pending->direct) {
|
||||
+ if (!direct_recipient) return false;
|
||||
+ memcpy(pending->recipient_pub_key, direct_recipient->id.pub_key, sizeof(pending->recipient_pub_key));
|
||||
+ } else {
|
||||
+ memset(pending->recipient_pub_key, 0, sizeof(pending->recipient_pub_key));
|
||||
+ }
|
||||
+ pending->channel_idx = channel_idx;
|
||||
+ pending->request_fingerprint = request_fingerprint;
|
||||
+ pending->response_fingerprint = response_fingerprint;
|
||||
+ pending->text_len = text_len;
|
||||
+ size_t max_text_len = FirmwareBot::maxResponseLenForChannel(message.channel_kind);
|
||||
+ if (pending->text_len > max_text_len) pending->text_len = max_text_len;
|
||||
+ if (pending->text_len > 0) memcpy(pending->text, text, pending->text_len);
|
||||
+ pending->text[pending->text_len] = 0;
|
||||
+ pending->active = true;
|
||||
+ return true;
|
||||
}
|
||||
|
||||
bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) {
|
||||
@@ -699,6 +710,22 @@ bool MyMesh::enqueueEmergencyForward(const BotMessage &message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
+bool MyMesh::observeKnownBotResponse(const BotMessage &message, bool authoritative_sender) {
|
||||
+ if (!authoritative_sender || message.channel_kind != BOT_CHANNEL_DM) return false;
|
||||
+ if (!KnownBotRegistry::canSuppressNormal(known_bot_entries, BOT_KNOWN_BOT_SLOTS, message.sender_key_prefix,
|
||||
+ message.sender_key_prefix_len)) return false;
|
||||
+
|
||||
+ bot_stats.known_bot_messages++;
|
||||
+ BotFingerprint fingerprint = FirmwareBot::responseFingerprintFor(message, message.text, message.text_len);
|
||||
+ if (ResponseCoordinator::recentlySent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS, fingerprint, _ms->getMillis())) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (ResponseCoordinator::suppress(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint)) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx) {
|
||||
bot_stats.observed_messages++;
|
||||
BotPolicyDecision decision = BotPolicy::decide(message.channel_kind);
|
||||
@@ -712,6 +739,9 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||
return;
|
||||
}
|
||||
|
||||
+ if (observeKnownBotResponse(message, direct_recipient != NULL)) return;
|
||||
+ sendQueuedBotResponses();
|
||||
+
|
||||
BotCommand command;
|
||||
if (!FirmwareBot::parseCommand(message.text, message.text_len, &command)) {
|
||||
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
||||
@@ -732,18 +762,58 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||
return;
|
||||
}
|
||||
|
||||
+ BotFingerprint request_fingerprint = FirmwareBot::fingerprintFor(message);
|
||||
+ BotFingerprint response_fingerprint = FirmwareBot::responseFingerprintFor(message, response, result.text_len);
|
||||
+ BotFingerprint fingerprint;
|
||||
+ uint32_t due_at_millis = 0;
|
||||
+ uint32_t bot_identity_seed;
|
||||
+ memcpy(&bot_identity_seed, self_id.pub_key, sizeof(bot_identity_seed));
|
||||
+ uint8_t queue_depth = (uint8_t)_mgr->getOutboundTotal();
|
||||
+ BotCoordinatorScheduleResult schedule = ResponseCoordinator::schedule(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS,
|
||||
+ message, command.id, request_fingerprint,
|
||||
+ response_fingerprint, _ms->getMillis(),
|
||||
+ context.random_seed, bot_identity_seed,
|
||||
+ queue_depth, &fingerprint, &due_at_millis);
|
||||
+ if (schedule == BOT_COORDINATOR_NO_SPACE || schedule == BOT_COORDINATOR_NOT_NORMAL) {
|
||||
+ bot_stats.send_failures++;
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
bot_stats.eligible_messages++;
|
||||
+ bot_stats.pending_responses++;
|
||||
FirmwareBot::recordCommandCooldown(bot_command_cooldowns, BOT_COMMAND_COOLDOWN_SLOTS, command.id, _ms->getMillis(),
|
||||
BOT_COMMAND_COOLDOWN_MILLIS);
|
||||
- if (!enqueueBotResponse(message, direct_recipient, channel_idx, response, result.text_len)) {
|
||||
+ if (!enqueueBotResponse(message, direct_recipient, channel_idx, response, result.text_len, fingerprint, response_fingerprint)) {
|
||||
+ ResponseCoordinator::cancel(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, fingerprint);
|
||||
bot_stats.send_failures++;
|
||||
}
|
||||
}
|
||||
|
||||
void MyMesh::sendQueuedBotResponses() {
|
||||
- for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) {
|
||||
- PendingBotResponse *pending = &pending_bot_responses[i];
|
||||
- if (!pending->active) continue;
|
||||
+ uint32_t now = _ms->getMillis();
|
||||
+ while (true) {
|
||||
+ BotCoordinatorReady ready = ResponseCoordinator::poll(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS, now);
|
||||
+ if (ready.result == BOT_COORDINATOR_READY_NONE) return;
|
||||
+
|
||||
+ if (ready.result == BOT_COORDINATOR_READY_SUPPRESSED) {
|
||||
+ bot_stats.suppressed_responses++;
|
||||
+ } else if (ready.result == BOT_COORDINATOR_READY_EXPIRED) {
|
||||
+ bot_stats.expired_responses++;
|
||||
+ }
|
||||
+
|
||||
+ PendingBotResponse *pending = NULL;
|
||||
+ for (size_t i = 0; i < BOT_PENDING_RESPONSE_SLOTS; i++) {
|
||||
+ if (pending_bot_responses[i].active && pending_bot_responses[i].request_fingerprint.value == ready.request_fingerprint.value) {
|
||||
+ pending = &pending_bot_responses[i];
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ if (!pending) continue;
|
||||
+
|
||||
+ if (ready.result != BOT_COORDINATOR_READY_SEND) {
|
||||
+ pending->active = false;
|
||||
+ continue;
|
||||
+ }
|
||||
|
||||
bool success = false;
|
||||
if (pending->direct) {
|
||||
@@ -770,6 +840,8 @@ void MyMesh::sendQueuedBotResponses() {
|
||||
|
||||
if (success) {
|
||||
bot_stats.sent_messages++;
|
||||
+ ResponseCoordinator::recordRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS,
|
||||
+ pending->response_fingerprint, now);
|
||||
} else {
|
||||
bot_stats.send_failures++;
|
||||
}
|
||||
@@ -1176,6 +1248,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
memset(pending_bot_responses, 0, sizeof(pending_bot_responses));
|
||||
memset(pending_emergency_forwards, 0, sizeof(pending_emergency_forwards));
|
||||
memset(bot_command_cooldowns, 0, sizeof(bot_command_cooldowns));
|
||||
+ ResponseCoordinator::clear(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS);
|
||||
+ ResponseCoordinator::clearRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS);
|
||||
+ KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS);
|
||||
emergency_rate_window_started = 0;
|
||||
emergency_rate_count = 0;
|
||||
next_bot_local_advert = 0;
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index 10b27fe..698ecad 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -9,7 +9,7 @@
|
||||
#endif
|
||||
#if CMESH_BOT_ENABLED
|
||||
#include "BotTypes.h"
|
||||
-#define BOT_PENDING_RESPONSE_SLOTS 2
|
||||
+#define BOT_PENDING_RESPONSE_SLOTS BOT_COORDINATOR_PENDING_SLOTS
|
||||
#define BOT_COMMAND_COOLDOWN_SLOTS 9
|
||||
#endif
|
||||
|
||||
@@ -207,9 +207,11 @@ private:
|
||||
void observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
||||
uint32_t sender_timestamp);
|
||||
void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx);
|
||||
+ bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender);
|
||||
void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id);
|
||||
bool enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
- const char *text, size_t text_len);
|
||||
+ const char *text, size_t text_len, BotFingerprint request_fingerprint,
|
||||
+ BotFingerprint response_fingerprint);
|
||||
bool enqueueEmergencyForward(const BotMessage &message);
|
||||
bool findBotChannel(BotChannelKind kind, uint8_t &channel_idx);
|
||||
bool isEmergencyRateLimited();
|
||||
@@ -259,6 +261,8 @@ private:
|
||||
bool direct;
|
||||
uint8_t recipient_pub_key[PUB_KEY_SIZE];
|
||||
uint8_t channel_idx;
|
||||
+ BotFingerprint request_fingerprint;
|
||||
+ BotFingerprint response_fingerprint;
|
||||
char text[BOT_MAX_RESPONSE_LEN + 1];
|
||||
size_t text_len;
|
||||
};
|
||||
@@ -273,6 +277,9 @@ private:
|
||||
PendingBotResponse pending_bot_responses[BOT_PENDING_RESPONSE_SLOTS];
|
||||
PendingEmergencyForward pending_emergency_forwards[BOT_PENDING_EMERGENCY_SLOTS];
|
||||
BotCommandCooldown bot_command_cooldowns[BOT_COMMAND_COOLDOWN_SLOTS];
|
||||
+ BotCoordinatorPending bot_coordinator_pending[BOT_COORDINATOR_PENDING_SLOTS];
|
||||
+ BotCoordinatorRecent bot_coordinator_recent[BOT_COORDINATOR_RECENT_SLOTS];
|
||||
+ BotKnownBotEntry known_bot_entries[BOT_KNOWN_BOT_SLOTS];
|
||||
unsigned long emergency_rate_window_started;
|
||||
uint8_t emergency_rate_count;
|
||||
unsigned long next_bot_local_advert;
|
||||
diff --git a/examples/companion_radio/ResponseCoordinator.cpp b/examples/companion_radio/ResponseCoordinator.cpp
|
||||
new file mode 100644
|
||||
index 0000000..c5c8fc3
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/ResponseCoordinator.cpp
|
||||
@@ -0,0 +1,190 @@
|
||||
+#include "ResponseCoordinator.h"
|
||||
+
|
||||
+#include "BotPolicy.h"
|
||||
+
|
||||
+#include <string.h>
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+bool isNormalChannel(BotChannelKind kind) {
|
||||
+ return BotPolicy::isNormalAllowed(kind);
|
||||
+}
|
||||
+
|
||||
+bool sameFingerprint(BotFingerprint a, BotFingerprint b) {
|
||||
+ return a.value == b.value;
|
||||
+}
|
||||
+
|
||||
+uint32_t channelDelayBias(BotChannelKind kind) {
|
||||
+ if (kind == BOT_CHANNEL_DM) return 0;
|
||||
+ if (kind == BOT_CHANNEL_BOT) return 200;
|
||||
+ if (kind == BOT_CHANNEL_TESTING) return 400;
|
||||
+ return 800;
|
||||
+}
|
||||
+
|
||||
+uint32_t commandDelayBias(BotCommandId command_id) {
|
||||
+ if (command_id == BOT_COMMAND_PING || command_id == BOT_COMMAND_TEST) return 0;
|
||||
+ if (command_id == BOT_COMMAND_DICE) return 200;
|
||||
+ if (command_id == BOT_COMMAND_STATUS || command_id == BOT_COMMAND_CHANNELS) return 400;
|
||||
+ return 100;
|
||||
+}
|
||||
+
|
||||
+uint32_t tieBreakBias(BotFingerprint request_fingerprint, uint32_t bot_identity_seed) {
|
||||
+ uint32_t mixed = (uint32_t)request_fingerprint.value ^ (uint32_t)(request_fingerprint.value >> 32) ^ bot_identity_seed;
|
||||
+ mixed ^= mixed >> 16;
|
||||
+ mixed *= 0x7feb352dUL;
|
||||
+ mixed ^= mixed >> 15;
|
||||
+ return mixed % 900UL;
|
||||
+}
|
||||
+
|
||||
+uint32_t queueDelayBias(uint8_t queue_depth) {
|
||||
+ return (uint32_t)queue_depth * 150UL;
|
||||
+}
|
||||
+
|
||||
+bool millisDue(uint32_t now_millis, uint32_t then_millis) {
|
||||
+ return (int32_t)(now_millis - then_millis) >= 0;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
+
|
||||
+namespace ResponseCoordinator {
|
||||
+
|
||||
+void clear(BotCoordinatorPending pending[], size_t pending_count) {
|
||||
+ if (!pending) return;
|
||||
+ memset(pending, 0, sizeof(BotCoordinatorPending) * pending_count);
|
||||
+}
|
||||
+
|
||||
+void clearRecent(BotCoordinatorRecent recent[], size_t recent_count) {
|
||||
+ if (!recent) return;
|
||||
+ memset(recent, 0, sizeof(BotCoordinatorRecent) * recent_count);
|
||||
+}
|
||||
+
|
||||
+uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id, BotFingerprint request_fingerprint,
|
||||
+ uint32_t bot_identity_seed, uint8_t queue_depth, uint32_t jitter_seed) {
|
||||
+ uint32_t jitter = BOT_RESPONSE_DELAY_JITTER_MILLIS ? jitter_seed % BOT_RESPONSE_DELAY_JITTER_MILLIS : 0;
|
||||
+ return BOT_RESPONSE_DELAY_BASE_MILLIS + channelDelayBias(message.channel_kind) + commandDelayBias(command_id) +
|
||||
+ queueDelayBias(queue_depth) + tieBreakBias(request_fingerprint, bot_identity_seed) + jitter;
|
||||
+}
|
||||
+
|
||||
+BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pending_count,
|
||||
+ const BotMessage& message, BotCommandId command_id,
|
||||
+ BotFingerprint request_fingerprint, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis, uint32_t jitter_seed, uint32_t bot_identity_seed,
|
||||
+ uint8_t queue_depth, BotFingerprint* fingerprint, uint32_t* due_at_millis) {
|
||||
+ if (fingerprint) fingerprint->value = 0;
|
||||
+ if (due_at_millis) *due_at_millis = 0;
|
||||
+ if (!pending || pending_count == 0 || !isNormalChannel(message.channel_kind) || request_fingerprint.value == 0 || response_fingerprint.value == 0) return BOT_COORDINATOR_NOT_NORMAL;
|
||||
+
|
||||
+ uint32_t due = now_millis + responseDelayMillis(message, command_id, request_fingerprint, bot_identity_seed, queue_depth, jitter_seed);
|
||||
+ size_t slot = pending_count;
|
||||
+
|
||||
+ for (size_t i = 0; i < pending_count; i++) {
|
||||
+ if (pending[i].active && sameFingerprint(pending[i].request_fingerprint, request_fingerprint)) {
|
||||
+ slot = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (slot == pending_count && !pending[i].active) slot = i;
|
||||
+ }
|
||||
+ if (slot == pending_count) return BOT_COORDINATOR_NO_SPACE;
|
||||
+
|
||||
+ bool replaced = pending[slot].active;
|
||||
+ pending[slot].active = true;
|
||||
+ pending[slot].suppressed = false;
|
||||
+ pending[slot].request_fingerprint = request_fingerprint;
|
||||
+ pending[slot].response_fingerprint = response_fingerprint;
|
||||
+ pending[slot].due_at_millis = due;
|
||||
+ pending[slot].expires_at_millis = now_millis + BOT_RESPONSE_PENDING_TTL_MILLIS;
|
||||
+ if (fingerprint) *fingerprint = request_fingerprint;
|
||||
+ if (due_at_millis) *due_at_millis = due;
|
||||
+ return replaced ? BOT_COORDINATOR_REPLACED : BOT_COORDINATOR_SCHEDULED;
|
||||
+}
|
||||
+
|
||||
+bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint) {
|
||||
+ if (!pending || response_fingerprint.value == 0) return false;
|
||||
+ for (size_t i = 0; i < pending_count; i++) {
|
||||
+ if (pending[i].active && sameFingerprint(pending[i].response_fingerprint, response_fingerprint)) {
|
||||
+ pending[i].suppressed = true;
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint) {
|
||||
+ if (!pending || request_fingerprint.value == 0) return false;
|
||||
+ for (size_t i = 0; i < pending_count; i++) {
|
||||
+ if (pending[i].active && sameFingerprint(pending[i].request_fingerprint, request_fingerprint)) {
|
||||
+ pending[i].active = false;
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis) {
|
||||
+ BotCoordinatorReady ready;
|
||||
+ ready.result = BOT_COORDINATOR_READY_NONE;
|
||||
+ ready.request_fingerprint.value = 0;
|
||||
+ ready.response_fingerprint.value = 0;
|
||||
+ if (!pending) return ready;
|
||||
+
|
||||
+ for (size_t i = 0; i < pending_count; i++) {
|
||||
+ if (!pending[i].active) continue;
|
||||
+ if (pending[i].suppressed) {
|
||||
+ ready.result = BOT_COORDINATOR_READY_SUPPRESSED;
|
||||
+ ready.request_fingerprint = pending[i].request_fingerprint;
|
||||
+ ready.response_fingerprint = pending[i].response_fingerprint;
|
||||
+ pending[i].active = false;
|
||||
+ return ready;
|
||||
+ }
|
||||
+ if (millisDue(now_millis, pending[i].expires_at_millis)) {
|
||||
+ ready.result = BOT_COORDINATOR_READY_EXPIRED;
|
||||
+ ready.request_fingerprint = pending[i].request_fingerprint;
|
||||
+ ready.response_fingerprint = pending[i].response_fingerprint;
|
||||
+ pending[i].active = false;
|
||||
+ return ready;
|
||||
+ }
|
||||
+ if (millisDue(now_millis, pending[i].due_at_millis)) {
|
||||
+ ready.result = BOT_COORDINATOR_READY_SEND;
|
||||
+ ready.request_fingerprint = pending[i].request_fingerprint;
|
||||
+ ready.response_fingerprint = pending[i].response_fingerprint;
|
||||
+ pending[i].active = false;
|
||||
+ return ready;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return ready;
|
||||
+}
|
||||
+
|
||||
+void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis) {
|
||||
+ if (!recent || recent_count == 0 || response_fingerprint.value == 0) return;
|
||||
+
|
||||
+ size_t slot = recent_count;
|
||||
+ for (size_t i = 0; i < recent_count; i++) {
|
||||
+ if (recent[i].active && sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) {
|
||||
+ slot = i;
|
||||
+ break;
|
||||
+ }
|
||||
+ if (slot == recent_count && (!recent[i].active || millisDue(now_millis, recent[i].expires_at_millis))) slot = i;
|
||||
+ }
|
||||
+ if (slot == recent_count) slot = 0;
|
||||
+ recent[slot].active = true;
|
||||
+ recent[slot].response_fingerprint = response_fingerprint;
|
||||
+ recent[slot].expires_at_millis = now_millis + BOT_RESPONSE_RECENT_TTL_MILLIS;
|
||||
+}
|
||||
+
|
||||
+bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis) {
|
||||
+ if (!recent || response_fingerprint.value == 0) return false;
|
||||
+ for (size_t i = 0; i < recent_count; i++) {
|
||||
+ if (!recent[i].active) continue;
|
||||
+ if (millisDue(now_millis, recent[i].expires_at_millis)) {
|
||||
+ recent[i].active = false;
|
||||
+ continue;
|
||||
+ }
|
||||
+ if (sameFingerprint(recent[i].response_fingerprint, response_fingerprint)) return true;
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+}
|
||||
diff --git a/examples/companion_radio/ResponseCoordinator.h b/examples/companion_radio/ResponseCoordinator.h
|
||||
new file mode 100644
|
||||
index 0000000..d900633
|
||||
--- /dev/null
|
||||
+++ b/examples/companion_radio/ResponseCoordinator.h
|
||||
@@ -0,0 +1,24 @@
|
||||
+#pragma once
|
||||
+
|
||||
+#include "BotTypes.h"
|
||||
+
|
||||
+namespace ResponseCoordinator {
|
||||
+
|
||||
+void clear(BotCoordinatorPending pending[], size_t pending_count);
|
||||
+void clearRecent(BotCoordinatorRecent recent[], size_t recent_count);
|
||||
+uint32_t responseDelayMillis(const BotMessage& message, BotCommandId command_id, BotFingerprint request_fingerprint,
|
||||
+ uint32_t bot_identity_seed, uint8_t queue_depth, uint32_t jitter_seed);
|
||||
+BotCoordinatorScheduleResult schedule(BotCoordinatorPending pending[], size_t pending_count,
|
||||
+ const BotMessage& message, BotCommandId command_id,
|
||||
+ BotFingerprint request_fingerprint, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis, uint32_t jitter_seed, uint32_t bot_identity_seed,
|
||||
+ uint8_t queue_depth, BotFingerprint* fingerprint, uint32_t* due_at_millis);
|
||||
+bool suppress(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint response_fingerprint);
|
||||
+bool cancel(BotCoordinatorPending pending[], size_t pending_count, BotFingerprint request_fingerprint);
|
||||
+BotCoordinatorReady poll(BotCoordinatorPending pending[], size_t pending_count, uint32_t now_millis);
|
||||
+void recordRecent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis);
|
||||
+bool recentlySent(BotCoordinatorRecent recent[], size_t recent_count, BotFingerprint response_fingerprint,
|
||||
+ uint32_t now_millis);
|
||||
+
|
||||
+}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +0,0 @@
|
||||
diff --git a/platformio.ini b/platformio.ini
|
||||
index 864e5e1f..dfed7dbd 100644
|
||||
--- a/platformio.ini
|
||||
+++ b/platformio.ini
|
||||
@@ -54,6 +54,14 @@ build_src_filter =
|
||||
|
||||
; ----------------- ESP32 ---------------------
|
||||
|
||||
+[cmesh_bot_production]
|
||||
+build_flags =
|
||||
+ -D CMESH_BOT_ENABLED=1
|
||||
+ -UENABLE_PRIVATE_KEY_IMPORT
|
||||
+ -UENABLE_PRIVATE_KEY_EXPORT
|
||||
+ -D ENABLE_PRIVATE_KEY_IMPORT=0
|
||||
+ -D ENABLE_PRIVATE_KEY_EXPORT=0
|
||||
+
|
||||
[esp32_base]
|
||||
extends = arduino_base
|
||||
platform = platformio/espressif32@6.11.0
|
||||
diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini
|
||||
index 803ee683..ca9cf00d 100644
|
||||
--- a/variants/heltec_v3/platformio.ini
|
||||
+++ b/variants/heltec_v3/platformio.ini
|
||||
@@ -144,6 +144,7 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
+ ${cmesh_bot_production.build_flags}
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
|
||||
@@ -163,6 +164,7 @@ build_flags =
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
+ ${cmesh_bot_production.build_flags}
|
||||
-D BLE_PIN_CODE=123456 ; dynamic, random PIN
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3400
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini
|
||||
index ea7e49c3..d6610098 100644
|
||||
--- a/variants/rak4631/platformio.ini
|
||||
+++ b/variants/rak4631/platformio.ini
|
||||
@@ -122,6 +122,7 @@ build_flags =
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
+ ${cmesh_bot_production.build_flags}
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
|
||||
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
|
||||
build_src_filter = ${rak4631.build_src_filter}
|
||||
@@ -143,6 +144,7 @@ build_flags =
|
||||
-D DISPLAY_CLASS=SSD1306Display
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
+ ${cmesh_bot_production.build_flags}
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
@@ -1,64 +0,0 @@
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
index 56a3395..de44fb9 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -163,6 +163,10 @@ void recordCommandCooldown(BotCommandCooldown* cooldowns, size_t cooldown_count,
|
||||
}
|
||||
|
||||
bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
+ return parseCommand(text, text_len, command, false);
|
||||
+}
|
||||
+
|
||||
+bool parseCommand(const char* text, size_t text_len, BotCommand* command, bool allow_prefixless) {
|
||||
if (!command) return false;
|
||||
memset(command, 0, sizeof(*command));
|
||||
command->id = BOT_COMMAND_NONE;
|
||||
@@ -171,9 +175,13 @@ bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
size_t normalized_len = 0;
|
||||
normalizeText(text, text_len, normalized, sizeof(normalized), &normalized_len);
|
||||
|
||||
- if (normalized_len < 2 || (normalized[0] != '!' && normalized[0] != '/')) return false;
|
||||
+ if (normalized_len == 0) return false;
|
||||
+
|
||||
+ bool has_prefix = normalized[0] == '!' || normalized[0] == '/';
|
||||
+ if (!has_prefix && !allow_prefixless) return false;
|
||||
+ if (has_prefix && normalized_len < 2) return false;
|
||||
|
||||
- size_t pos = 1;
|
||||
+ size_t pos = has_prefix ? 1 : 0;
|
||||
while (pos < normalized_len && normalized[pos] == ' ') pos++;
|
||||
size_t name_start = pos;
|
||||
while (pos < normalized_len && !isCommandDelimiter(normalized[pos])) pos++;
|
||||
@@ -187,6 +195,7 @@ bool parseCommand(const char* text, size_t text_len, BotCommand* command) {
|
||||
}
|
||||
command->name[copy_name_len] = 0;
|
||||
command->id = name_len > BOT_MAX_COMMAND_NAME_LEN ? BOT_COMMAND_UNKNOWN : commandIdForName(command->name, copy_name_len);
|
||||
+ if (!has_prefix && command->id == BOT_COMMAND_UNKNOWN) return false;
|
||||
|
||||
while (pos < normalized_len && isCommandDelimiter(normalized[pos])) pos++;
|
||||
size_t args_len = normalized_len - pos;
|
||||
diff --git a/examples/companion_radio/FirmwareBot.h b/examples/companion_radio/FirmwareBot.h
|
||||
index 42b9c5b..e248adc 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.h
|
||||
+++ b/examples/companion_radio/FirmwareBot.h
|
||||
@@ -6,6 +6,7 @@ namespace FirmwareBot {
|
||||
|
||||
BotWriteResult normalizeText(const char* input, size_t input_len, char* output, size_t output_len, size_t* written);
|
||||
bool parseCommand(const char* text, size_t text_len, BotCommand* command);
|
||||
+bool parseCommand(const char* text, size_t text_len, BotCommand* command, bool allow_prefixless);
|
||||
bool splitChannelText(const char* text, size_t text_len, char* sender, size_t sender_len, const char** body,
|
||||
size_t* body_len);
|
||||
BotWriteResult writeResponse(char* output, size_t output_len, const char* text, size_t text_len, size_t* written);
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index 13ead48..ec6a780 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -1011,7 +1011,7 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||
sendQueuedBotResponses();
|
||||
|
||||
BotCommand command;
|
||||
- if (!FirmwareBot::parseCommand(message.text, message.text_len, &command)) {
|
||||
+ if (!FirmwareBot::parseCommand(message.text, message.text_len, &command, true)) {
|
||||
if (message.text_len > 0 && (message.text[0] == '!' || message.text[0] == '/')) bot_stats.parse_errors++;
|
||||
return;
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
diff --git a/examples/companion_radio/BotCommands.cpp b/examples/companion_radio/BotCommands.cpp
|
||||
index 2c206a9..6222be5 100644
|
||||
--- a/examples/companion_radio/BotCommands.cpp
|
||||
+++ b/examples/companion_radio/BotCommands.cpp
|
||||
@@ -62,7 +62,7 @@ bool parseUInt(const char* text, size_t len, size_t* pos, uint16_t* value) {
|
||||
}
|
||||
|
||||
bool isSupportedSides(uint16_t sides) {
|
||||
- return sides == 4 || sides == 6 || sides == 8 || sides == 10 || sides == 12 || sides == 16 || sides == 20;
|
||||
+ return sides >= 2 && sides <= 1000;
|
||||
}
|
||||
|
||||
bool parseDice(const BotCommand& command, uint16_t* count, uint16_t* sides) {
|
||||
@@ -97,11 +97,57 @@ uint16_t rollOnce(uint32_t* state, uint16_t sides) {
|
||||
return (uint16_t)((*state >> 16) % sides) + 1;
|
||||
}
|
||||
|
||||
+void appendPathHex(char* output, size_t output_len, size_t* pos, const uint8_t* path, size_t path_len) {
|
||||
+ static const char hex[] = "0123456789abcdef";
|
||||
+ for (size_t i = 0; i < path_len; i++) {
|
||||
+ if (*pos + 2 < output_len) {
|
||||
+ output[*pos] = hex[path[i] >> 4];
|
||||
+ output[*pos + 1] = hex[path[i] & 0x0F];
|
||||
+ }
|
||||
+ *pos += 2;
|
||||
+ }
|
||||
+ if (output_len > 0) output[*pos < output_len ? *pos : output_len - 1] = 0;
|
||||
+}
|
||||
+
|
||||
+void formatQuarters(int8_t quarters, char* output, size_t output_len) {
|
||||
+ if (!output || output_len == 0) return;
|
||||
+ int value = quarters;
|
||||
+ const char* sign = value < 0 ? "-" : "";
|
||||
+ if (value < 0) value = -value;
|
||||
+ snprintf(output, output_len, "%s%d.%02d", sign, value / 4, (value % 4) * 25);
|
||||
+}
|
||||
+
|
||||
+BotCommandResult executeMagic8(const BotCommandContext& context, char* output, size_t output_len) {
|
||||
+ static const char* responses[] = {
|
||||
+ "It is certain", "Looks good", "Ask again later", "Cannot predict now", "Doubtful", "Very likely",
|
||||
+ "Signs point yes", "No", "Reply hazy", "Absolutely"
|
||||
+ };
|
||||
+ uint32_t seed = context.random_seed ? context.random_seed : 1;
|
||||
+ seed = seed * 1664525UL + 1013904223UL;
|
||||
+ return writeFormatted(output, output_len, "Magic 8-ball: %s", responses[(seed >> 16) % (sizeof(responses) / sizeof(responses[0]))]);
|
||||
+}
|
||||
+
|
||||
+BotCommandResult executePath(const BotCommandContext& context, char* output, size_t output_len) {
|
||||
+ if (!context.path || context.path_len == 0 || context.path_hash_count == 0) return writeText(output, output_len, "Path unavailable");
|
||||
+ if (!output || output_len == 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+
|
||||
+ size_t byte_len = (size_t)context.path_hash_size * context.path_hash_count;
|
||||
+ char snr[8];
|
||||
+ formatQuarters(context.path_snr_quarters, snr, sizeof(snr));
|
||||
+ int written = snprintf(output, output_len, "Path %uh x %uB snr %s: ", (unsigned)context.path_hash_count,
|
||||
+ (unsigned)context.path_hash_size, snr);
|
||||
+ if (written < 0) return makeResult(BOT_COMMAND_RESULT_NO_SPACE, 0);
|
||||
+ size_t pos = (size_t)written;
|
||||
+ appendPathHex(output, output_len, &pos, context.path, byte_len);
|
||||
+ size_t actual = boundedStrLen(output, output_len);
|
||||
+ return makeResult(pos >= output_len ? BOT_COMMAND_RESULT_TRUNCATED : BOT_COMMAND_RESULT_OK, actual);
|
||||
+}
|
||||
+
|
||||
BotCommandResult executeDice(const BotCommand& command, const BotCommandContext& context, char* output, size_t output_len) {
|
||||
uint16_t count = 1;
|
||||
uint16_t sides = 6;
|
||||
if (!parseDice(command, &count, &sides)) {
|
||||
- return writeText(output, output_len, "Usage: !roll [d6|d20|2d6], sides: d4 d6 d8 d10 d12 d16 d20");
|
||||
+ return writeText(output, output_len, "Usage: roll [N|dN|NdN], max 10 dice, sides 2-1000");
|
||||
}
|
||||
|
||||
uint32_t state = context.random_seed ^ ((uint32_t)count << 16) ^ sides;
|
||||
@@ -141,7 +187,7 @@ BotCommandResult executeCommand(const BotCommand& command, const BotCommandConte
|
||||
size_t output_len) {
|
||||
switch (command.id) {
|
||||
case BOT_COMMAND_HELP:
|
||||
- return writeText(output, output_len, "Commands: !ping !test !hello !about !roll [d20|2d6] !status !channels");
|
||||
+ return writeText(output, output_len, "Commands: ping t hello about roll stats version path magic8 status channels");
|
||||
case BOT_COMMAND_PING:
|
||||
return writeText(output, output_len, "Pong!");
|
||||
case BOT_COMMAND_TEST:
|
||||
@@ -164,8 +210,22 @@ BotCommandResult executeCommand(const BotCommand& command, const BotCommandConte
|
||||
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_VERSION:
|
||||
+ return writeFormatted(output, output_len, "Firmware %s built %s", context.firmware_version[0] ? context.firmware_version : "unknown",
|
||||
+ context.firmware_build_date[0] ? context.firmware_build_date : "unknown");
|
||||
+ case BOT_COMMAND_STATS:
|
||||
+ return writeFormatted(output, output_len, "Bot seen %lu ok %lu sent %lu fail %lu sup %lu pend %lu rf rx/tx %lu/%lu err %lu q %u",
|
||||
+ (unsigned long)context.observed_messages, (unsigned long)context.eligible_messages,
|
||||
+ (unsigned long)context.sent_messages, (unsigned long)context.send_failures,
|
||||
+ (unsigned long)context.suppressed_responses, (unsigned long)context.pending_responses,
|
||||
+ (unsigned long)context.packets_recv, (unsigned long)context.packets_sent,
|
||||
+ (unsigned long)context.packets_recv_errors, (unsigned)context.queue_depth);
|
||||
+ case BOT_COMMAND_MAGIC8:
|
||||
+ return executeMagic8(context, output, output_len);
|
||||
+ case BOT_COMMAND_PATH:
|
||||
+ return executePath(context, output, output_len);
|
||||
case BOT_COMMAND_UNKNOWN:
|
||||
- return writeText(output, output_len, "Unknown command. Try !help");
|
||||
+ return writeText(output, output_len, "Unknown command. Try help");
|
||||
default:
|
||||
return makeResult(BOT_COMMAND_RESULT_NOT_HANDLED, 0);
|
||||
}
|
||||
diff --git a/examples/companion_radio/BotPrefs.cpp b/examples/companion_radio/BotPrefs.cpp
|
||||
index 1d0810a..0eff075 100644
|
||||
--- a/examples/companion_radio/BotPrefs.cpp
|
||||
+++ b/examples/companion_radio/BotPrefs.cpp
|
||||
@@ -294,27 +294,51 @@ const char* commandName(BotCommandId command_id) {
|
||||
case BOT_COMMAND_DICE: return "roll";
|
||||
case BOT_COMMAND_STATUS: return "status";
|
||||
case BOT_COMMAND_CHANNELS: return "channels";
|
||||
+ case BOT_COMMAND_VERSION: return "version";
|
||||
+ case BOT_COMMAND_STATS: return "stats";
|
||||
+ case BOT_COMMAND_MAGIC8: return "magic8";
|
||||
+ case BOT_COMMAND_PATH: return "path";
|
||||
case BOT_COMMAND_UNKNOWN: return "unknown";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
+bool namesEqual(const char* name, size_t len, const char* expected) {
|
||||
+ size_t expected_len = boundedStrLen(expected, BOT_MAX_COMMAND_NAME_LEN + 1);
|
||||
+ if (len != expected_len) return false;
|
||||
+ for (size_t i = 0; i < len; i++) {
|
||||
+ if (tolower((unsigned char)name[i]) != tolower((unsigned char)expected[i])) return false;
|
||||
+ }
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
bool commandIdForName(const char* name, BotCommandId* command_id) {
|
||||
if (!name || !command_id) return false;
|
||||
+ size_t len = boundedStrLen(name, BOT_MAX_COMMAND_NAME_LEN + 1);
|
||||
+ if (namesEqual(name, len, "t")) {
|
||||
+ *command_id = BOT_COMMAND_TEST;
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (namesEqual(name, len, "dice")) {
|
||||
+ *command_id = BOT_COMMAND_DICE;
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (namesEqual(name, len, "ver")) {
|
||||
+ *command_id = BOT_COMMAND_VERSION;
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (namesEqual(name, len, "8ball") || namesEqual(name, len, "eightball")) {
|
||||
+ *command_id = BOT_COMMAND_MAGIC8;
|
||||
+ return true;
|
||||
+ }
|
||||
+ if (namesEqual(name, len, "p") || namesEqual(name, len, "decode") || namesEqual(name, len, "route")) {
|
||||
+ *command_id = BOT_COMMAND_PATH;
|
||||
+ return true;
|
||||
+ }
|
||||
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) {
|
||||
+ if (namesEqual(name, len, candidate)) {
|
||||
*command_id = (BotCommandId)id;
|
||||
return true;
|
||||
}
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
index 98d4a58..5eb71c6 100644
|
||||
--- a/examples/companion_radio/BotTypes.h
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -9,6 +9,9 @@
|
||||
#define BOT_MAX_COMMAND_ARGS_LEN 79
|
||||
#define BOT_MAX_CHANNEL_NAME_LEN 23
|
||||
#define BOT_MAX_SENDER_NAME_LEN 31
|
||||
+#define BOT_MAX_FIRMWARE_VERSION_LEN 19
|
||||
+#define BOT_MAX_BUILD_DATE_LEN 15
|
||||
+#define BOT_MAX_PATH_BYTES 64
|
||||
#define BOT_GROUP_RESPONSE_PREFIX_RESERVE (BOT_MAX_SENDER_NAME_LEN + 2)
|
||||
#define BOT_MAX_GROUP_RESPONSE_LEN (BOT_MAX_TEXT_LEN - BOT_GROUP_RESPONSE_PREFIX_RESERVE)
|
||||
#define BOT_COMMAND_COOLDOWN_MILLIS 5000UL
|
||||
@@ -43,10 +46,16 @@
|
||||
#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_VERSION (1UL << BOT_COMMAND_VERSION)
|
||||
+#define BOT_COMMAND_MASK_STATS (1UL << BOT_COMMAND_STATS)
|
||||
+#define BOT_COMMAND_MASK_MAGIC8 (1UL << BOT_COMMAND_MAGIC8)
|
||||
+#define BOT_COMMAND_MASK_PATH (1UL << BOT_COMMAND_PATH)
|
||||
#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)
|
||||
+ BOT_COMMAND_MASK_STATUS | BOT_COMMAND_MASK_CHANNELS | BOT_COMMAND_MASK_VERSION | \
|
||||
+ BOT_COMMAND_MASK_STATS | BOT_COMMAND_MASK_MAGIC8 | BOT_COMMAND_MASK_PATH | \
|
||||
+ BOT_COMMAND_MASK_UNKNOWN)
|
||||
#define BOT_PREFS_SERIALIZED_SIZE 294
|
||||
|
||||
enum BotChannelKind : uint8_t {
|
||||
@@ -74,6 +83,10 @@ enum BotCommandId : uint8_t {
|
||||
BOT_COMMAND_DICE,
|
||||
BOT_COMMAND_STATUS,
|
||||
BOT_COMMAND_CHANNELS,
|
||||
+ BOT_COMMAND_VERSION,
|
||||
+ BOT_COMMAND_STATS,
|
||||
+ BOT_COMMAND_MAGIC8,
|
||||
+ BOT_COMMAND_PATH,
|
||||
BOT_COMMAND_UNKNOWN
|
||||
};
|
||||
|
||||
@@ -116,8 +129,13 @@ struct BotMessage {
|
||||
uint8_t sender_key_prefix_len;
|
||||
bool text_truncated;
|
||||
uint32_t sender_timestamp;
|
||||
+ uint8_t path_len;
|
||||
+ uint8_t path_hash_size;
|
||||
+ uint8_t path_hash_count;
|
||||
+ int8_t packet_snr_quarters;
|
||||
char text[BOT_MAX_TEXT_LEN + 1];
|
||||
size_t text_len;
|
||||
+ const uint8_t* path;
|
||||
};
|
||||
|
||||
struct BotCommand {
|
||||
@@ -141,6 +159,8 @@ struct BotCommandContext {
|
||||
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];
|
||||
+ char firmware_version[BOT_MAX_FIRMWARE_VERSION_LEN + 1];
|
||||
+ char firmware_build_date[BOT_MAX_BUILD_DATE_LEN + 1];
|
||||
uint32_t uptime_seconds;
|
||||
uint16_t battery_millivolts;
|
||||
uint32_t storage_used_kb;
|
||||
@@ -150,8 +170,30 @@ struct BotCommandContext {
|
||||
uint32_t eligible_messages;
|
||||
uint32_t sent_messages;
|
||||
uint32_t send_failures;
|
||||
+ uint32_t suppressed_responses;
|
||||
+ uint32_t pending_responses;
|
||||
+ uint32_t emergency_forwards;
|
||||
+ uint32_t emergency_forward_failures;
|
||||
+ uint32_t packets_recv;
|
||||
+ uint32_t packets_sent;
|
||||
+ uint32_t packets_recv_errors;
|
||||
+ uint32_t flood_recv;
|
||||
+ uint32_t flood_sent;
|
||||
+ uint32_t direct_recv;
|
||||
+ uint32_t direct_sent;
|
||||
+ uint32_t tx_airtime_seconds;
|
||||
+ uint32_t rx_airtime_seconds;
|
||||
uint32_t random_seed;
|
||||
+ int16_t noise_floor;
|
||||
+ int8_t last_rssi;
|
||||
+ int8_t last_snr_quarters;
|
||||
+ uint8_t queue_depth;
|
||||
uint8_t channel_count;
|
||||
+ uint8_t path_len;
|
||||
+ uint8_t path_hash_size;
|
||||
+ uint8_t path_hash_count;
|
||||
+ int8_t path_snr_quarters;
|
||||
+ const uint8_t* path;
|
||||
};
|
||||
|
||||
struct BotCommandResult {
|
||||
@@ -232,10 +274,10 @@ struct BotStats {
|
||||
uint32_t send_failures;
|
||||
};
|
||||
|
||||
-static_assert(sizeof(BotMessage) <= 248, "BotMessage RAM budget exceeded");
|
||||
+static_assert(sizeof(BotMessage) <= 264, "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) <= 192, "BotCommandContext RAM budget exceeded");
|
||||
+static_assert(sizeof(BotCommandContext) <= 312, "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");
|
||||
diff --git a/examples/companion_radio/FirmwareBot.cpp b/examples/companion_radio/FirmwareBot.cpp
|
||||
index de44fb9..8cdf764 100644
|
||||
--- a/examples/companion_radio/FirmwareBot.cpp
|
||||
+++ b/examples/companion_radio/FirmwareBot.cpp
|
||||
@@ -119,12 +119,16 @@ BotWriteResult normalizeText(const char* input, size_t input_len, char* output,
|
||||
BotCommandId commandIdForName(const char* name, size_t len) {
|
||||
if (namesEqual(name, len, "help") || namesEqual(name, len, "cmd") || namesEqual(name, len, "commands")) return BOT_COMMAND_HELP;
|
||||
if (namesEqual(name, len, "ping")) return BOT_COMMAND_PING;
|
||||
- if (namesEqual(name, len, "test")) return BOT_COMMAND_TEST;
|
||||
+ if (namesEqual(name, len, "test") || namesEqual(name, len, "t")) return BOT_COMMAND_TEST;
|
||||
if (namesEqual(name, len, "hello") || namesEqual(name, len, "hi")) return BOT_COMMAND_HELLO;
|
||||
if (namesEqual(name, len, "about")) return BOT_COMMAND_ABOUT;
|
||||
if (namesEqual(name, len, "dice") || namesEqual(name, len, "roll")) return BOT_COMMAND_DICE;
|
||||
if (namesEqual(name, len, "status")) return BOT_COMMAND_STATUS;
|
||||
if (namesEqual(name, len, "channels")) return BOT_COMMAND_CHANNELS;
|
||||
+ if (namesEqual(name, len, "version") || namesEqual(name, len, "ver")) return BOT_COMMAND_VERSION;
|
||||
+ if (namesEqual(name, len, "stats")) return BOT_COMMAND_STATS;
|
||||
+ if (namesEqual(name, len, "magic8") || namesEqual(name, len, "8ball") || namesEqual(name, len, "eightball")) return BOT_COMMAND_MAGIC8;
|
||||
+ if (namesEqual(name, len, "path") || namesEqual(name, len, "p") || namesEqual(name, len, "decode") || namesEqual(name, len, "route")) return BOT_COMMAND_PATH;
|
||||
return BOT_COMMAND_UNKNOWN;
|
||||
}
|
||||
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index ec6a780..895be4b 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -835,7 +835,7 @@ void MyMesh::observeBotDirectMessage(const ContactInfo &from, uint32_t sender_ti
|
||||
}
|
||||
|
||||
void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_name, const char *text,
|
||||
- uint32_t sender_timestamp) {
|
||||
+ uint32_t sender_timestamp, const mesh::Packet *packet) {
|
||||
BotMessage message;
|
||||
memset(&message, 0, sizeof(message));
|
||||
size_t channel_len = botBoundedStrLen(channel_name, BOT_MAX_CHANNEL_NAME_LEN);
|
||||
@@ -845,6 +845,13 @@ void MyMesh::observeBotChannelMessage(uint8_t channel_idx, const char *channel_n
|
||||
message.channel_name[channel_len] = 0;
|
||||
}
|
||||
message.sender_timestamp = sender_timestamp;
|
||||
+ if (packet && packet->isRouteFlood() && packet->path_len <= 0xFF && mesh::Packet::isValidPathLen((uint8_t)packet->path_len)) {
|
||||
+ message.path_len = (uint8_t)packet->path_len;
|
||||
+ message.path_hash_size = packet->getPathHashSize();
|
||||
+ message.path_hash_count = packet->getPathHashCount();
|
||||
+ message.packet_snr_quarters = (int8_t)(packet->getSNR() * 4);
|
||||
+ message.path = packet->path;
|
||||
+ }
|
||||
|
||||
const char *body = text;
|
||||
size_t body_len = botBoundedStrLen(text, BOT_MAX_TEXT_LEN);
|
||||
@@ -867,10 +874,14 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com
|
||||
context.eligible_messages = bot_stats.eligible_messages;
|
||||
context.sent_messages = bot_stats.sent_messages;
|
||||
context.send_failures = bot_stats.send_failures;
|
||||
- if (command_id == BOT_COMMAND_DICE) {
|
||||
+ context.suppressed_responses = bot_stats.suppressed_responses;
|
||||
+ context.pending_responses = bot_stats.pending_responses;
|
||||
+ context.emergency_forwards = bot_stats.emergency_forwards;
|
||||
+ context.emergency_forward_failures = bot_stats.emergency_forward_failures;
|
||||
+ if (command_id == BOT_COMMAND_DICE || command_id == BOT_COMMAND_MAGIC8) {
|
||||
getRNG()->random((uint8_t *)&context.random_seed, sizeof(context.random_seed));
|
||||
}
|
||||
- if (command_id == BOT_COMMAND_STATUS) {
|
||||
+ if (command_id == BOT_COMMAND_STATUS || command_id == BOT_COMMAND_STATS) {
|
||||
context.battery_millivolts = board.getBattMilliVolts();
|
||||
context.storage_used_kb = _store->getStorageUsedKb();
|
||||
context.storage_total_kb = _store->getStorageTotalKb();
|
||||
@@ -881,6 +892,25 @@ void MyMesh::buildBotCommandContext(BotCommandContext &context, BotCommandId com
|
||||
if (getChannel(i, channel) && channel.name[0]) context.channel_count++;
|
||||
}
|
||||
}
|
||||
+ if (command_id == BOT_COMMAND_VERSION) {
|
||||
+ StrHelper::strzcpy(context.firmware_version, FIRMWARE_VERSION, sizeof(context.firmware_version));
|
||||
+ StrHelper::strzcpy(context.firmware_build_date, FIRMWARE_BUILD_DATE, sizeof(context.firmware_build_date));
|
||||
+ }
|
||||
+ if (command_id == BOT_COMMAND_STATS) {
|
||||
+ context.queue_depth = (uint8_t)_mgr->getOutboundTotal();
|
||||
+ context.noise_floor = (int16_t)_radio->getNoiseFloor();
|
||||
+ context.last_rssi = (int8_t)radio_driver.getLastRSSI();
|
||||
+ context.last_snr_quarters = (int8_t)(radio_driver.getLastSNR() * 4);
|
||||
+ context.tx_airtime_seconds = getTotalAirTime() / 1000;
|
||||
+ context.rx_airtime_seconds = getReceiveAirTime() / 1000;
|
||||
+ context.packets_recv = radio_driver.getPacketsRecv();
|
||||
+ context.packets_sent = radio_driver.getPacketsSent();
|
||||
+ context.flood_sent = getNumSentFlood();
|
||||
+ context.direct_sent = getNumSentDirect();
|
||||
+ context.flood_recv = getNumRecvFlood();
|
||||
+ context.direct_recv = getNumRecvDirect();
|
||||
+ context.packets_recv_errors = radio_driver.getPacketsRecvErrors();
|
||||
+ }
|
||||
}
|
||||
|
||||
bool MyMesh::enqueueBotResponse(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx,
|
||||
@@ -1024,6 +1054,13 @@ void MyMesh::recordBotObservation(const BotMessage &message, const ContactInfo *
|
||||
|
||||
BotCommandContext context;
|
||||
buildBotCommandContext(context, command.id);
|
||||
+ if (command.id == BOT_COMMAND_PATH) {
|
||||
+ context.path_len = message.path_len;
|
||||
+ context.path_hash_size = message.path_hash_size;
|
||||
+ context.path_hash_count = message.path_hash_count;
|
||||
+ context.path_snr_quarters = message.packet_snr_quarters;
|
||||
+ context.path = message.path;
|
||||
+ }
|
||||
char response[BOT_MAX_RESPONSE_LEN + 1];
|
||||
BotCommandResult result = BotCommands::executeCommand(command, context, response, sizeof(response));
|
||||
if (result.code == BOT_COMMAND_RESULT_NOT_HANDLED || result.code == BOT_COMMAND_RESULT_NO_SPACE || result.text_len == 0) {
|
||||
@@ -1232,7 +1269,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
|
||||
if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len);
|
||||
#endif
|
||||
#if CMESH_BOT_ENABLED
|
||||
- observeBotChannelMessage(channel_idx, channel_name, text, timestamp);
|
||||
+ observeBotChannelMessage(channel_idx, channel_name, text, timestamp, pkt);
|
||||
#endif
|
||||
}
|
||||
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index 6f8d432..e25f9a5 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -209,7 +209,7 @@ private:
|
||||
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,
|
||||
- uint32_t sender_timestamp);
|
||||
+ uint32_t sender_timestamp, const mesh::Packet *packet);
|
||||
void recordBotObservation(const BotMessage &message, const ContactInfo *direct_recipient, uint8_t channel_idx);
|
||||
bool observeKnownBotResponse(const BotMessage &message, bool authoritative_sender);
|
||||
void buildBotCommandContext(BotCommandContext &context, BotCommandId command_id);
|
||||
@@ -1,92 +0,0 @@
|
||||
diff --git a/examples/companion_radio/BotTypes.h b/examples/companion_radio/BotTypes.h
|
||||
index 5eb71c6..7ee34c8 100644
|
||||
--- a/examples/companion_radio/BotTypes.h
|
||||
+++ b/examples/companion_radio/BotTypes.h
|
||||
@@ -17,8 +17,6 @@
|
||||
#define BOT_COMMAND_COOLDOWN_MILLIS 5000UL
|
||||
#define BOT_EMERGENCY_PREFIX "EMERGENCY MESSAGE FROM "
|
||||
#define BOT_EMERGENCY_MAX_PARTS 3
|
||||
-#define BOT_EMERGENCY_RATE_LIMIT_MILLIS 60000UL
|
||||
-#define BOT_EMERGENCY_RATE_LIMIT_COUNT 3
|
||||
#define BOT_PENDING_EMERGENCY_SLOTS BOT_EMERGENCY_MAX_PARTS
|
||||
#define BOT_COORDINATOR_PENDING_SLOTS 8
|
||||
#define BOT_COORDINATOR_RECENT_SLOTS 16
|
||||
diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp
|
||||
index 895be4b..89e130b 100644
|
||||
--- a/examples/companion_radio/MyMesh.cpp
|
||||
+++ b/examples/companion_radio/MyMesh.cpp
|
||||
@@ -960,23 +960,7 @@ bool MyMesh::findBotChannel(BotChannelKind kind, uint8_t &channel_idx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
-bool MyMesh::isEmergencyRateLimited() {
|
||||
- unsigned long now = _ms->getMillis();
|
||||
- if (!emergency_rate_window_started || millisHasNowPassed(emergency_rate_window_started + BOT_EMERGENCY_RATE_LIMIT_MILLIS)) {
|
||||
- emergency_rate_window_started = now;
|
||||
- emergency_rate_count = 0;
|
||||
- return false;
|
||||
- }
|
||||
- return emergency_rate_count >= BOT_EMERGENCY_RATE_LIMIT_COUNT;
|
||||
-}
|
||||
-
|
||||
-void MyMesh::recordEmergencyRateLimitEvent() {
|
||||
- if (emergency_rate_count < 0xFF) emergency_rate_count++;
|
||||
-}
|
||||
-
|
||||
bool MyMesh::enqueueEmergencyForward(const BotMessage &message) {
|
||||
- if (isEmergencyRateLimited()) return false;
|
||||
-
|
||||
BotEmergencyForward forward;
|
||||
if (!EmergencyForwarder::format(message, forward)) return false;
|
||||
|
||||
@@ -999,7 +983,6 @@ bool MyMesh::enqueueEmergencyForward(const BotMessage &message) {
|
||||
}
|
||||
}
|
||||
|
||||
- recordEmergencyRateLimitEvent();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1175,10 +1158,10 @@ void MyMesh::sendQueuedEmergencyForwards() {
|
||||
|
||||
if (success) {
|
||||
bot_stats.emergency_forwards++;
|
||||
+ pending->active = false;
|
||||
} else {
|
||||
bot_stats.emergency_forward_failures++;
|
||||
}
|
||||
- pending->active = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1561,8 +1544,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
ResponseCoordinator::clear(bot_coordinator_pending, BOT_COORDINATOR_PENDING_SLOTS);
|
||||
ResponseCoordinator::clearRecent(bot_coordinator_recent, BOT_COORDINATOR_RECENT_SLOTS);
|
||||
KnownBotRegistry::clear(known_bot_entries, BOT_KNOWN_BOT_SLOTS);
|
||||
- emergency_rate_window_started = 0;
|
||||
- emergency_rate_count = 0;
|
||||
next_bot_local_advert = 0;
|
||||
next_bot_flood_advert = 0;
|
||||
#endif
|
||||
diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h
|
||||
index e25f9a5..20497b7 100644
|
||||
--- a/examples/companion_radio/MyMesh.h
|
||||
+++ b/examples/companion_radio/MyMesh.h
|
||||
@@ -218,8 +218,6 @@ private:
|
||||
BotFingerprint response_fingerprint);
|
||||
bool enqueueEmergencyForward(const BotMessage &message);
|
||||
bool findBotChannel(BotChannelKind kind, uint8_t &channel_idx);
|
||||
- bool isEmergencyRateLimited();
|
||||
- void recordEmergencyRateLimitEvent();
|
||||
void sendQueuedBotResponses();
|
||||
void sendQueuedEmergencyForwards();
|
||||
void tickBot();
|
||||
@@ -285,8 +283,6 @@ private:
|
||||
BotCoordinatorPending bot_coordinator_pending[BOT_COORDINATOR_PENDING_SLOTS];
|
||||
BotCoordinatorRecent bot_coordinator_recent[BOT_COORDINATOR_RECENT_SLOTS];
|
||||
BotKnownBotEntry known_bot_entries[BOT_KNOWN_BOT_SLOTS];
|
||||
- unsigned long emergency_rate_window_started;
|
||||
- uint8_t emergency_rate_count;
|
||||
unsigned long next_bot_local_advert;
|
||||
unsigned long next_bot_flood_advert;
|
||||
#endif
|
||||
@@ -9,6 +9,8 @@ bot_sources=(
|
||||
"${MESHCORE_DIR}/examples/companion_radio/FirmwareBot.cpp"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotCommands.h"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotCommands.cpp"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotCommandRegistry.h"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotCommandRegistry.cpp"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotPolicy.h"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotPolicy.cpp"
|
||||
"${MESHCORE_DIR}/examples/companion_radio/BotPrefs.h"
|
||||
@@ -65,6 +67,8 @@ require_pattern 'recordBotObservation\(|sendQueuedEmergencyForwards\(|tickBot\('
|
||||
"${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp"
|
||||
require_pattern '_prefs\.path_hash_mode[[:space:]]*=[[:space:]]*1' 'bot firmware defaults to two-byte path hashes' \
|
||||
"${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp"
|
||||
require_pattern 'command\.args_len > 0 \? botConfiguredTraceHashSize\(_prefs\.path_hash_mode\) : message\.path_hash_size' 'explicit bot trace paths use configured hash width' \
|
||||
"${MESHCORE_DIR}/examples/companion_radio/MyMesh.cpp"
|
||||
if grep -R -n -E 'BOT_EMERGENCY_RATE_LIMIT|isEmergencyRateLimited|recordEmergencyRateLimitEvent|emergency_rate_window_started|emergency_rate_count' \
|
||||
"${MESHCORE_DIR}/examples/companion_radio"; then
|
||||
echo "Emergency forwarding must not be globally rate limited or dropped by a bot-local quota." >&2
|
||||
|
||||
@@ -21,6 +21,7 @@ def main():
|
||||
str(SRC_DIR),
|
||||
str(ROOT / "tests" / "firmware_bot" / "test_firmware_bot.cpp"),
|
||||
str(SRC_DIR / "FirmwareBot.cpp"),
|
||||
str(SRC_DIR / "BotCommandRegistry.cpp"),
|
||||
str(SRC_DIR / "BotPrefs.cpp"),
|
||||
str(SRC_DIR / "BotPolicy.cpp"),
|
||||
str(SRC_DIR / "BotCommands.cpp"),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "BotCommandRegistry.h"
|
||||
#include "BotCommands.h"
|
||||
#include "BotPolicy.h"
|
||||
#include "BotPrefs.h"
|
||||
@@ -10,6 +11,48 @@
|
||||
#include "KnownBotRegistry.h"
|
||||
#include "ResponseCoordinator.h"
|
||||
|
||||
static void test_command_registry() {
|
||||
assert(BotCommandRegistry::commandCount() >= 18);
|
||||
uint32_t seen_masks = 0;
|
||||
bool saw_hidden = false;
|
||||
bool saw_internal = false;
|
||||
for (size_t i = 0; i < BotCommandRegistry::commandCount(); i++) {
|
||||
const BotCommandMetadata* command = BotCommandRegistry::commandAt(i);
|
||||
assert(command != NULL);
|
||||
assert(BotCommandRegistry::findById(command->id) == command);
|
||||
assert(command->name != NULL);
|
||||
assert(command->name[0] != 0);
|
||||
assert(command->summary != NULL);
|
||||
assert(command->usage != NULL);
|
||||
assert(command->details != NULL);
|
||||
const BotCommandMetadata* by_name = BotCommandRegistry::findByName(command->name, strlen(command->name));
|
||||
assert(by_name == command);
|
||||
if (command->visibility == BOT_COMMAND_VISIBILITY_DISCOVERABLE) {
|
||||
assert(command->mask != 0);
|
||||
assert((seen_masks & command->mask) == 0);
|
||||
seen_masks |= command->mask;
|
||||
assert(BotCommandRegistry::isDiscoverable(command->id));
|
||||
} else {
|
||||
assert(command->mask == 0);
|
||||
assert(!BotCommandRegistry::isDiscoverable(command->id));
|
||||
saw_hidden = saw_hidden || command->visibility == BOT_COMMAND_VISIBILITY_HIDDEN;
|
||||
saw_internal = saw_internal || command->visibility == BOT_COMMAND_VISIBILITY_INTERNAL;
|
||||
}
|
||||
}
|
||||
assert((seen_masks & BOT_COMMAND_MASK_ALL) == BOT_COMMAND_MASK_ALL);
|
||||
assert(saw_hidden);
|
||||
assert(saw_internal);
|
||||
assert(BotCommandRegistry::findByName("ROLL", 4)->id == BOT_COMMAND_ROLL);
|
||||
assert(BotCommandRegistry::findByName("dice", 4)->id == BOT_COMMAND_DICE);
|
||||
assert(BotCommandRegistry::findByName("commands", 8)->id == BOT_COMMAND_CMD);
|
||||
assert(BotCommandRegistry::findByName("t", 1)->id == BOT_COMMAND_TEST);
|
||||
assert(BotCommandRegistry::findByName("tracer", 6)->id == BOT_COMMAND_TRACER);
|
||||
assert(BotCommandRegistry::findByName("weather", 7)->id == BOT_COMMAND_UNSUPPORTED);
|
||||
assert(BotCommandRegistry::findByName("nope", 4) == NULL);
|
||||
assert(strcmp(BotCommandRegistry::commandName(BOT_COMMAND_ROLL), "roll") == 0);
|
||||
assert(BotCommandRegistry::commandMask(BOT_COMMAND_UNKNOWN) == 0);
|
||||
}
|
||||
|
||||
static void test_channel_policy() {
|
||||
assert(BotPolicy::classifyChannel(NULL, 0, true) == BOT_CHANNEL_DM);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_DM) == BOT_POLICY_ALLOW_NORMAL);
|
||||
@@ -19,20 +62,26 @@ static void test_channel_policy() {
|
||||
assert(BotPolicy::classifyChannel("#Public", 7, false) == BOT_CHANNEL_OTHER);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_PUBLIC) == BOT_POLICY_IGNORE);
|
||||
assert(!BotPolicy::isNormalAllowed(BOT_CHANNEL_PUBLIC));
|
||||
assert(!BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_PUBLIC));
|
||||
assert(BotPolicy::classifyChannel("#bot", 4, false) == BOT_CHANNEL_BOT);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_BOT) == BOT_POLICY_ALLOW_NORMAL);
|
||||
assert(BotPolicy::isNormalAllowed(BOT_CHANNEL_BOT));
|
||||
assert(BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_BOT));
|
||||
assert(BotPolicy::classifyChannel("testing", 7, false) == BOT_CHANNEL_TESTING);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_TESTING) == BOT_POLICY_ALLOW_NORMAL);
|
||||
assert(BotPolicy::isNormalAllowed(BOT_CHANNEL_TESTING));
|
||||
assert(BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_TESTING));
|
||||
assert(BotPolicy::classifyChannel("#emergency", 10, false) == BOT_CHANNEL_EMERGENCY);
|
||||
assert(BotPolicy::classifyChannel("emergency", 9, false) == BOT_CHANNEL_OTHER);
|
||||
assert(BotPolicy::classifyChannel("#Emergency", 10, false) == BOT_CHANNEL_OTHER);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_EMERGENCY) == BOT_POLICY_EMERGENCY_FORWARD);
|
||||
assert(BotPolicy::isEmergency(BOT_CHANNEL_EMERGENCY));
|
||||
assert(!BotPolicy::isNormalAllowed(BOT_CHANNEL_EMERGENCY));
|
||||
assert(!BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_EMERGENCY));
|
||||
assert(BotPolicy::classifyChannel("#botnet", 7, false) == BOT_CHANNEL_OTHER);
|
||||
assert(BotPolicy::decide(BOT_CHANNEL_OTHER) == BOT_POLICY_IGNORE);
|
||||
assert(!BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_OTHER));
|
||||
assert(!BotPolicy::isPrefixlessCommandAllowed(BOT_CHANNEL_DM));
|
||||
}
|
||||
|
||||
static void test_bot_prefs_defaults() {
|
||||
@@ -49,7 +98,7 @@ static void test_bot_prefs_defaults() {
|
||||
assert(strcmp(prefs.public_channel, "Public") == 0);
|
||||
assert(BotPrefsCodec::serializedSize() == BOT_PREFS_SERIALIZED_SIZE);
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_PING));
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_UNKNOWN));
|
||||
assert(!BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_UNKNOWN));
|
||||
}
|
||||
|
||||
static void test_bot_prefs_serialization_round_trip() {
|
||||
@@ -134,9 +183,14 @@ static void test_bot_prefs_validation_and_command_mask() {
|
||||
assert(!BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_PING));
|
||||
BotPrefsCodec::setCommandEnabled(prefs, BOT_COMMAND_PING, true);
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_PING));
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_CMD));
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_TRACE));
|
||||
assert(BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_PREFIX));
|
||||
BotCommandId id = BOT_COMMAND_NONE;
|
||||
assert(BotPrefsCodec::commandIdForName("commands", &id));
|
||||
assert(id == BOT_COMMAND_CMD);
|
||||
assert(BotPrefsCodec::commandIdForName("ROLL", &id));
|
||||
assert(id == BOT_COMMAND_DICE);
|
||||
assert(id == BOT_COMMAND_ROLL);
|
||||
assert(BotPrefsCodec::commandIdForName("t", &id));
|
||||
assert(id == BOT_COMMAND_TEST);
|
||||
assert(BotPrefsCodec::commandIdForName("ver", &id));
|
||||
@@ -145,9 +199,31 @@ static void test_bot_prefs_validation_and_command_mask() {
|
||||
assert(id == BOT_COMMAND_MAGIC8);
|
||||
assert(BotPrefsCodec::commandIdForName("p", &id));
|
||||
assert(id == BOT_COMMAND_PATH);
|
||||
assert(BotPrefsCodec::commandIdForName("tracer", &id));
|
||||
assert(id == BOT_COMMAND_TRACER);
|
||||
assert(!BotPrefsCodec::commandIdForName("weather", &id));
|
||||
assert(BotPrefsCodec::commandIdForName("prefix", &id));
|
||||
assert(id == BOT_COMMAND_PREFIX);
|
||||
assert(!BotPrefsCodec::commandIdForName("unknown", &id));
|
||||
assert(!BotPrefsCodec::commandIdForName("nope", &id));
|
||||
}
|
||||
|
||||
static bool command_allowed_by_runtime_gate(const BotPrefs& prefs, BotCommandId command_id) {
|
||||
return command_id == BOT_COMMAND_UNKNOWN || command_id == BOT_COMMAND_UNSUPPORTED || BotPrefsCodec::commandEnabled(prefs, command_id);
|
||||
}
|
||||
|
||||
static void test_unknown_command_runtime_gate() {
|
||||
BotPrefs prefs;
|
||||
BotPrefsCodec::defaults(prefs);
|
||||
assert(command_allowed_by_runtime_gate(prefs, BOT_COMMAND_UNKNOWN));
|
||||
assert(command_allowed_by_runtime_gate(prefs, BOT_COMMAND_UNSUPPORTED));
|
||||
assert(!BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_UNKNOWN));
|
||||
assert(!BotPrefsCodec::commandEnabled(prefs, BOT_COMMAND_UNSUPPORTED));
|
||||
assert(!BotPrefsCodec::commandIdForName("weather", NULL));
|
||||
BotPrefsCodec::setCommandEnabled(prefs, BOT_COMMAND_PING, false);
|
||||
assert(!command_allowed_by_runtime_gate(prefs, BOT_COMMAND_PING));
|
||||
}
|
||||
|
||||
static void test_bot_prefs_known_bot_helpers() {
|
||||
BotPrefs prefs;
|
||||
BotPrefsCodec::defaults(prefs);
|
||||
@@ -226,6 +302,11 @@ static void test_normalize_truncation() {
|
||||
assert(written == BOT_MAX_TEXT_LEN);
|
||||
}
|
||||
|
||||
static bool parse_channel_command(BotChannelKind channel_kind, const char* text, BotCommand* command) {
|
||||
return BotPolicy::isNormalAllowed(channel_kind) &&
|
||||
FirmwareBot::parseCommand(text, strlen(text), command, BotPolicy::isPrefixlessCommandAllowed(channel_kind));
|
||||
}
|
||||
|
||||
static void test_parse_command() {
|
||||
const char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
@@ -242,9 +323,22 @@ static void test_parse_command() {
|
||||
assert(FirmwareBot::parseCommand("ping", 4, &command, true));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(command.args_len == 0);
|
||||
assert(parse_channel_command(BOT_CHANNEL_BOT, "ping", &command));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(parse_channel_command(BOT_CHANNEL_TESTING, "ping", &command));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(!parse_channel_command(BOT_CHANNEL_DM, "ping", &command));
|
||||
assert(!parse_channel_command(BOT_CHANNEL_PUBLIC, "ping", &command));
|
||||
assert(!parse_channel_command(BOT_CHANNEL_OTHER, "ping", &command));
|
||||
assert(!parse_channel_command(BOT_CHANNEL_EMERGENCY, "ping", &command));
|
||||
assert(parse_channel_command(BOT_CHANNEL_DM, "!ping", &command));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(FirmwareBot::parseCommand("status please", 13, &command, true));
|
||||
assert(command.id == BOT_COMMAND_STATUS);
|
||||
assert(strcmp(command.args, "please") == 0);
|
||||
assert(!parse_channel_command(BOT_CHANNEL_DM, "status please", &command));
|
||||
assert(!parse_channel_command(BOT_CHANNEL_PUBLIC, "status please", &command));
|
||||
assert(!parse_channel_command(BOT_CHANNEL_BOT, "random prose", &command));
|
||||
assert(!FirmwareBot::parseCommand("random prose", 12, &command, true));
|
||||
assert(FirmwareBot::parseCommand("!PING", 5, &command));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
@@ -252,7 +346,7 @@ static void test_parse_command() {
|
||||
assert(command.args_len == 0);
|
||||
|
||||
assert(FirmwareBot::parseCommand("/roll: 2d6", 10, &command));
|
||||
assert(command.id == BOT_COMMAND_DICE);
|
||||
assert(command.id == BOT_COMMAND_ROLL);
|
||||
assert(strcmp(command.name, "roll") == 0);
|
||||
assert(strcmp(command.args, "2d6") == 0);
|
||||
|
||||
@@ -263,8 +357,10 @@ static void test_parse_command() {
|
||||
assert(FirmwareBot::parseCommand("!channels", 9, &command));
|
||||
assert(command.id == BOT_COMMAND_CHANNELS);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!cmd", 4, &command));
|
||||
assert(command.id == BOT_COMMAND_CMD);
|
||||
assert(FirmwareBot::parseCommand("!commands", 9, &command));
|
||||
assert(command.id == BOT_COMMAND_HELP);
|
||||
assert(command.id == BOT_COMMAND_CMD);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!hi", 3, &command));
|
||||
assert(command.id == BOT_COMMAND_HELLO);
|
||||
@@ -279,6 +375,21 @@ static void test_parse_command() {
|
||||
assert(command.id == BOT_COMMAND_PATH);
|
||||
assert(FirmwareBot::parseCommand("decode", 6, &command, true));
|
||||
assert(command.id == BOT_COMMAND_PATH);
|
||||
assert(FirmwareBot::parseCommand("trace", 5, &command, true));
|
||||
assert(command.id == BOT_COMMAND_TRACE);
|
||||
assert(FirmwareBot::parseCommand("!tracer", 7, &command));
|
||||
assert(command.id == BOT_COMMAND_TRACER);
|
||||
assert(FirmwareBot::parseCommand("!prefix 01020304", 16, &command));
|
||||
assert(command.id == BOT_COMMAND_PREFIX);
|
||||
assert(strcmp(command.args, "01020304") == 0);
|
||||
assert(FirmwareBot::parseCommand("!weather", 8, &command));
|
||||
assert(command.id == BOT_COMMAND_UNSUPPORTED);
|
||||
assert(FirmwareBot::parseCommand("!reboot now", 11, &command));
|
||||
assert(command.id == BOT_COMMAND_UNSUPPORTED);
|
||||
assert(strcmp(command.name, "reboot") == 0);
|
||||
assert(strcmp(command.args, "now") == 0);
|
||||
assert(!FirmwareBot::parseCommand("weather", 7, &command, true));
|
||||
assert(!FirmwareBot::parseCommand("reboot", 6, &command, true));
|
||||
|
||||
assert(FirmwareBot::parseCommand("!wat", 4, &command));
|
||||
assert(command.id == BOT_COMMAND_UNKNOWN);
|
||||
@@ -375,8 +486,75 @@ static void test_command_outputs() {
|
||||
|
||||
result = run_command("!help", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "ping") != NULL);
|
||||
assert(strstr(out, "path") != NULL);
|
||||
assert(strstr(out, "Commands: help cmd ping") == out);
|
||||
assert(strstr(out, "roll") != NULL);
|
||||
assert(strstr(out, "dice") != NULL);
|
||||
assert(strstr(out, "trace") != NULL);
|
||||
assert(strstr(out, "tracer") != NULL);
|
||||
assert(strstr(out, "prefix") != NULL);
|
||||
assert(strstr(out, "help <command>") != NULL);
|
||||
assert(strstr(out, "weather") == NULL);
|
||||
|
||||
result = run_command("!cmd", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "help cmd ping test hello about roll dice") == out);
|
||||
assert(strstr(out, "trace") != NULL);
|
||||
assert(strstr(out, "tracer") != NULL);
|
||||
assert(strstr(out, "prefix") != NULL);
|
||||
assert(strstr(out, "weather") == NULL);
|
||||
|
||||
result = run_command("!help roll", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "roll: Roll within a numeric range.") == out);
|
||||
assert(strstr(out, "Usage: roll [max|low high]") != NULL);
|
||||
|
||||
result = run_command("!help dice", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "dice: Roll dice notation") == out);
|
||||
assert(strstr(out, "Usage: dice [dN|NdN]") != NULL);
|
||||
|
||||
result = run_command("!help trace", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "trace: Send a bounded active trace request") == out);
|
||||
assert(strstr(out, "Usage: trace [hex-path]") != NULL);
|
||||
|
||||
result = run_command("!help tracer", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "tracer: Show the current packet route hashes") == out);
|
||||
assert(strstr(out, "Usage: tracer") != NULL);
|
||||
|
||||
result = run_command("!help t", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "test: Return a short firmware bot self-test response") == out);
|
||||
|
||||
result = run_command("!help weather", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "weather is unavailable in firmware") == 0);
|
||||
|
||||
result = run_command("!help prefix", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "prefix: Look up a local contact by public-key prefix") == out);
|
||||
assert(strstr(out, "Usage: prefix <hex>") != NULL);
|
||||
|
||||
result = run_command("!prefix 01020304", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Prefix lookup unavailable") == 0);
|
||||
|
||||
result = run_command("!help nope", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "No help for nope") == 0);
|
||||
|
||||
result = run_command("!weather", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "weather is unavailable in firmware") == 0);
|
||||
|
||||
result = run_command("!reboot now", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "reboot is unavailable in firmware") == 0);
|
||||
|
||||
result = run_command("!nodes", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "nodes is unavailable in firmware") == 0);
|
||||
|
||||
result = run_command("!version", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
@@ -395,6 +573,22 @@ static void test_command_outputs() {
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path unavailable") == 0);
|
||||
|
||||
result = run_command("!trace", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Trace route unavailable") == 0);
|
||||
|
||||
result = run_command("!trace zz", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Usage: trace [hex-path]") == 0);
|
||||
|
||||
result = run_command("!tracer", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Tracer unavailable") == 0);
|
||||
|
||||
result = run_command("!tracer 1234", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Usage: tracer") == 0);
|
||||
|
||||
result = run_command("!channels", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Channels: #bot #testing emergency=#emergency public=Public (4 configured)") == 0);
|
||||
@@ -425,47 +619,95 @@ static void test_path_command() {
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path 3h x 2B snr 5.75: 1234abcd0001") == 0);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!tracer", 7, &command));
|
||||
result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Tracer 3h x 2B snr 5.75: 1234abcd0001") == 0);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!tracer 1234", 12, &command));
|
||||
result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Usage: tracer") == 0);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!path", 5, &command));
|
||||
context.path = NULL;
|
||||
result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path unavailable") == 0);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!tracer", 7, &command));
|
||||
result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Tracer unavailable") == 0);
|
||||
}
|
||||
|
||||
static void test_dice_command() {
|
||||
char out[BOT_MAX_RESPONSE_LEN + 1];
|
||||
BotCommandResult result = run_command("!roll", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled d6: ", 11) == 0);
|
||||
|
||||
result = run_command("!roll d20", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled d20: ", 12) == 0);
|
||||
|
||||
result = run_command("!roll 2d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled 2d6: ", 12) == 0);
|
||||
assert(strchr(out, '+') != NULL);
|
||||
assert(strchr(out, '=') != NULL);
|
||||
assert(strncmp(out, "Rolled 1-100: ", 14) == 0);
|
||||
|
||||
result = run_command("!roll 20", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled d20: ", 12) == 0);
|
||||
assert(strncmp(out, "Rolled 1-20: ", 13) == 0);
|
||||
|
||||
result = run_command("!roll d100", out, sizeof(out));
|
||||
result = run_command("!roll 5 10", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled d100: ", 13) == 0);
|
||||
assert(strncmp(out, "Rolled 5-10: ", 13) == 0);
|
||||
|
||||
result = run_command("!roll d1", out, sizeof(out));
|
||||
result = run_command("!roll 1 1", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Rolled 1-1: 1") == 0);
|
||||
|
||||
result = run_command("!roll 2d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Usage: roll [max|low high], range 1-1000") == 0);
|
||||
|
||||
result = run_command("!roll 0", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: roll", 11) == 0);
|
||||
|
||||
result = run_command("!roll 0d6", out, sizeof(out));
|
||||
result = run_command("!roll 10 1", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: roll", 11) == 0);
|
||||
|
||||
result = run_command("!roll 11d6", out, sizeof(out));
|
||||
result = run_command("!roll 100000", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: roll", 11) == 0);
|
||||
|
||||
result = run_command("!dice", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Dice d6: ", 9) == 0);
|
||||
|
||||
result = run_command("!dice d20", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Dice d20: ", 10) == 0);
|
||||
|
||||
result = run_command("!dice 2d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Dice 2d6: ", 10) == 0);
|
||||
assert(strchr(out, '+') != NULL);
|
||||
assert(strchr(out, '=') != NULL);
|
||||
|
||||
result = run_command("!dice 20", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Usage: dice [dN|NdN], max 10 dice, sides 2-1000") == 0);
|
||||
|
||||
result = run_command("!dice d1", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: dice", 11) == 0);
|
||||
|
||||
result = run_command("!dice 0d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: dice", 11) == 0);
|
||||
|
||||
result = run_command("!dice 11d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: dice", 11) == 0);
|
||||
|
||||
result = run_command("!dice 2d1001", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: dice", 11) == 0);
|
||||
}
|
||||
|
||||
static void test_command_truncation() {
|
||||
@@ -852,7 +1094,15 @@ static void test_response_coordinator_delay_biases() {
|
||||
BotFingerprint request = { 0x0123456789ABCDEFULL };
|
||||
uint32_t first = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x01020304UL, 0, 0);
|
||||
uint32_t second = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x05060708UL, 0, 0);
|
||||
uint32_t roll = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_ROLL, request, 0x01020304UL, 0, 0);
|
||||
uint32_t dice = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_DICE, request, 0x01020304UL, 0, 0);
|
||||
uint32_t trace = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_TRACE, request, 0x01020304UL, 0, 0);
|
||||
uint32_t tracer = ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_TRACER, request, 0x01020304UL, 0, 0);
|
||||
assert(first != second);
|
||||
assert(roll == dice);
|
||||
assert(roll == first + 200);
|
||||
assert(trace == tracer);
|
||||
assert(trace == first + 800);
|
||||
assert(ResponseCoordinator::responseDelayMillis(message, BOT_COMMAND_PING, request, 0x01020304UL, 2,
|
||||
BOT_RESPONSE_DELAY_JITTER_MILLIS + 17) == first + 300 + 17);
|
||||
}
|
||||
@@ -897,11 +1147,13 @@ static void test_response_coordinator_rejects_non_normal() {
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_command_registry();
|
||||
test_channel_policy();
|
||||
test_bot_prefs_defaults();
|
||||
test_bot_prefs_serialization_round_trip();
|
||||
test_bot_prefs_rejects_corrupt_and_wrong_version();
|
||||
test_bot_prefs_validation_and_command_mask();
|
||||
test_unknown_command_runtime_gate();
|
||||
test_bot_prefs_known_bot_helpers();
|
||||
test_prefs_aware_channel_policy();
|
||||
test_normalize_text();
|
||||
|
||||
2
vendor/MeshCore
vendored
2
vendor/MeshCore
vendored
Submodule vendor/MeshCore updated: 910b1bee5b...43a831df10
Reference in New Issue
Block a user