From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: cj-vana 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 // needed for PlatformIO #include +#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 #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;