Files
meshcore-bot-firmware/patches/meshcore/0002-Wire-companion-firmware-bot-runtime.patch
2026-05-14 15:56:48 -06:00

317 lines
12 KiB
Diff

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;