mirror of
https://github.com/Colorado-Mesh/meshcore-bot-firmware.git
synced 2026-08-11 16:20:29 +00:00
forge: step 4 — wire bot runtime observation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: cj-vana <cj@depth23.online>
|
||||
Date: Thu, 14 May 2026 13:36:50 -0600
|
||||
Subject: [PATCH] Add companion firmware bot core
|
||||
Date: Thu, 14 May 2026 14:03:07 -0600
|
||||
Subject: [PATCH 1/2] Add companion firmware bot core
|
||||
|
||||
---
|
||||
examples/companion_radio/BotPolicy.cpp | 58 +++++++
|
||||
|
||||
316
patches/meshcore/0002-Wire-companion-firmware-bot-runtime.patch
Normal file
316
patches/meshcore/0002-Wire-companion-firmware-bot-runtime.patch
Normal file
@@ -0,0 +1,316 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user