mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 08:10:29 +00:00
forge: step 11 — add firmware bot utility commands
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
}
|
||||
410
patches/meshcore/0009-Add-firmware-bot-utility-commands.patch
Normal file
410
patches/meshcore/0009-Add-firmware-bot-utility-commands.patch
Normal file
@@ -0,0 +1,410 @@
|
||||
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);
|
||||
@@ -137,6 +137,14 @@ static void test_bot_prefs_validation_and_command_mask() {
|
||||
BotCommandId id = BOT_COMMAND_NONE;
|
||||
assert(BotPrefsCodec::commandIdForName("ROLL", &id));
|
||||
assert(id == BOT_COMMAND_DICE);
|
||||
assert(BotPrefsCodec::commandIdForName("t", &id));
|
||||
assert(id == BOT_COMMAND_TEST);
|
||||
assert(BotPrefsCodec::commandIdForName("ver", &id));
|
||||
assert(id == BOT_COMMAND_VERSION);
|
||||
assert(BotPrefsCodec::commandIdForName("8ball", &id));
|
||||
assert(id == BOT_COMMAND_MAGIC8);
|
||||
assert(BotPrefsCodec::commandIdForName("p", &id));
|
||||
assert(id == BOT_COMMAND_PATH);
|
||||
assert(!BotPrefsCodec::commandIdForName("nope", &id));
|
||||
}
|
||||
|
||||
@@ -230,6 +238,14 @@ static void test_parse_command() {
|
||||
|
||||
BotCommand command;
|
||||
assert(!FirmwareBot::parseCommand("hello bot", 9, &command));
|
||||
assert(!FirmwareBot::parseCommand("ping", 4, &command));
|
||||
assert(FirmwareBot::parseCommand("ping", 4, &command, true));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(command.args_len == 0);
|
||||
assert(FirmwareBot::parseCommand("status please", 13, &command, true));
|
||||
assert(command.id == BOT_COMMAND_STATUS);
|
||||
assert(strcmp(command.args, "please") == 0);
|
||||
assert(!FirmwareBot::parseCommand("random prose", 12, &command, true));
|
||||
assert(FirmwareBot::parseCommand("!PING", 5, &command));
|
||||
assert(command.id == BOT_COMMAND_PING);
|
||||
assert(strcmp(command.name, "ping") == 0);
|
||||
@@ -253,6 +269,17 @@ static void test_parse_command() {
|
||||
assert(FirmwareBot::parseCommand("!hi", 3, &command));
|
||||
assert(command.id == BOT_COMMAND_HELLO);
|
||||
|
||||
assert(FirmwareBot::parseCommand("t", 1, &command, true));
|
||||
assert(command.id == BOT_COMMAND_TEST);
|
||||
assert(FirmwareBot::parseCommand("ver", 3, &command, true));
|
||||
assert(command.id == BOT_COMMAND_VERSION);
|
||||
assert(FirmwareBot::parseCommand("8ball", 5, &command, true));
|
||||
assert(command.id == BOT_COMMAND_MAGIC8);
|
||||
assert(FirmwareBot::parseCommand("p", 1, &command, true));
|
||||
assert(command.id == BOT_COMMAND_PATH);
|
||||
assert(FirmwareBot::parseCommand("decode", 6, &command, true));
|
||||
assert(command.id == BOT_COMMAND_PATH);
|
||||
|
||||
assert(FirmwareBot::parseCommand("!wat", 4, &command));
|
||||
assert(command.id == BOT_COMMAND_UNKNOWN);
|
||||
|
||||
@@ -307,6 +334,16 @@ static BotCommandContext make_context() {
|
||||
context.send_failures = 1;
|
||||
context.random_seed = 0x12345678;
|
||||
context.channel_count = 4;
|
||||
strncpy(context.firmware_version, "v1.test", sizeof(context.firmware_version) - 1);
|
||||
strncpy(context.firmware_build_date, "14 May 2026", sizeof(context.firmware_build_date) - 1);
|
||||
context.suppressed_responses = 3;
|
||||
context.pending_responses = 6;
|
||||
context.emergency_forwards = 1;
|
||||
context.emergency_forward_failures = 0;
|
||||
context.packets_recv = 77;
|
||||
context.packets_sent = 44;
|
||||
context.packets_recv_errors = 2;
|
||||
context.queue_depth = 3;
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -338,8 +375,25 @@ 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, "!channels") != NULL);
|
||||
assert(strstr(out, "ping") != NULL);
|
||||
assert(strstr(out, "path") != NULL);
|
||||
|
||||
result = run_command("!version", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Firmware v1.test built 14 May 2026") == 0);
|
||||
|
||||
result = run_command("!stats", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "Bot seen 9 ok 5 sent 4 fail 1") != NULL);
|
||||
assert(strstr(out, "rf rx/tx 77/44") != NULL);
|
||||
|
||||
result = run_command("!magic8", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strstr(out, "Magic 8-ball:") == out);
|
||||
|
||||
result = run_command("!path", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path unavailable") == 0);
|
||||
|
||||
result = run_command("!channels", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
@@ -353,7 +407,28 @@ static void test_command_outputs() {
|
||||
|
||||
result = run_command("!wat", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Unknown command. Try !help") == 0);
|
||||
assert(strcmp(out, "Unknown command. Try help") == 0);
|
||||
}
|
||||
|
||||
static void test_path_command() {
|
||||
char out[BOT_MAX_RESPONSE_LEN + 1];
|
||||
BotCommand command;
|
||||
assert(FirmwareBot::parseCommand("!path", 5, &command));
|
||||
BotCommandContext context = make_context();
|
||||
uint8_t path[] = { 0x12, 0x34, 0xab, 0xcd, 0x00, 0x01 };
|
||||
context.path = path;
|
||||
context.path_len = (uint8_t)(((2 - 1) << 6) | 3);
|
||||
context.path_hash_size = 2;
|
||||
context.path_hash_count = 3;
|
||||
context.path_snr_quarters = 23;
|
||||
BotCommandResult result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path 3h x 2B snr 5.75: 1234abcd0001") == 0);
|
||||
|
||||
context.path = NULL;
|
||||
result = BotCommands::executeCommand(command, context, out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strcmp(out, "Path unavailable") == 0);
|
||||
}
|
||||
|
||||
static void test_dice_command() {
|
||||
@@ -372,17 +447,25 @@ static void test_dice_command() {
|
||||
assert(strchr(out, '+') != NULL);
|
||||
assert(strchr(out, '=') != NULL);
|
||||
|
||||
result = run_command("!roll d7", out, sizeof(out));
|
||||
result = run_command("!roll 20", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: !roll", 12) == 0);
|
||||
assert(strncmp(out, "Rolled d20: ", 12) == 0);
|
||||
|
||||
result = run_command("!roll d100", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Rolled d100: ", 13) == 0);
|
||||
|
||||
result = run_command("!roll d1", 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));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: !roll", 12) == 0);
|
||||
assert(strncmp(out, "Usage: roll", 11) == 0);
|
||||
|
||||
result = run_command("!roll 11d6", out, sizeof(out));
|
||||
assert(result.code == BOT_COMMAND_RESULT_OK);
|
||||
assert(strncmp(out, "Usage: !roll", 12) == 0);
|
||||
assert(strncmp(out, "Usage: roll", 11) == 0);
|
||||
}
|
||||
|
||||
static void test_command_truncation() {
|
||||
@@ -826,6 +909,7 @@ int main() {
|
||||
test_parse_command();
|
||||
test_response_write();
|
||||
test_command_outputs();
|
||||
test_path_command();
|
||||
test_dice_command();
|
||||
test_command_truncation();
|
||||
test_command_cooldown();
|
||||
|
||||
Reference in New Issue
Block a user