Files
meshcore-bot-firmware/patches/meshcore/0009-Add-firmware-bot-utility-commands.patch

411 lines
20 KiB
Diff

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);