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