mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(bot): a Log tab with the bot's last 32 replies (ESP32 and the L2 sim)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,9 @@ channel and one room, each switched on separately:
|
||||
Fav** (Other tab) lets only your favourites run them; on a channel the
|
||||
sender is just a name, so there it's a weak check.
|
||||
|
||||
On ESP32 boards (L2, Heltec, ...) the **Log** tab lists the bot's last 32
|
||||
replies: when, who, what they asked and what it answered. Kept until a restart.
|
||||
|
||||
**DM allow = Fav** limits the DM bot to your favourites, and **quiet hours**
|
||||
stop trigger replies at night (commands still answer). Replies are rate
|
||||
limited so two bots can't answer each other forever. The room bot uses the
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
// The bot's recent replies -- who asked what, what it answered, when --
|
||||
// newest first, for the Bot screen's Log tab. ESP32 only (BOT_LOG): the
|
||||
// nRF52's heap is short -- and the L2's simulator (BOT_LOG_SIM), which
|
||||
// stands for one. The ring goes in PSRAM where the board has it, allocated on
|
||||
// the first reply.
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#if defined(ESP32) || defined(BOT_LOG_SIM)
|
||||
#define BOT_LOG 1
|
||||
#endif
|
||||
#if defined(ESP32)
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
struct BotLogEntry {
|
||||
uint32_t ts; // RTC time of the reply
|
||||
uint8_t where; // BotLog::DM / CHANNEL / ROOM
|
||||
char who[24]; // who asked
|
||||
char req[48]; // what they wrote (cut)
|
||||
char reply[80]; // what the bot sent (cut)
|
||||
};
|
||||
|
||||
class BotLog {
|
||||
public:
|
||||
enum : uint8_t { DM, CHANNEL, ROOM };
|
||||
#ifdef BOT_LOG
|
||||
static const int SIZE = 32;
|
||||
|
||||
void add(uint32_t ts, uint8_t where, const char* who, const char* req, const char* reply) {
|
||||
if (!_ring) {
|
||||
#if defined(ESP32)
|
||||
_ring = (BotLogEntry*)heap_caps_calloc(SIZE, sizeof(BotLogEntry), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
if (!_ring) _ring = (BotLogEntry*)calloc(SIZE, sizeof(BotLogEntry));
|
||||
if (!_ring) return;
|
||||
}
|
||||
BotLogEntry& e = _ring[_head];
|
||||
e.ts = ts;
|
||||
e.where = where;
|
||||
copy(e.who, sizeof(e.who), who);
|
||||
copy(e.req, sizeof(e.req), req);
|
||||
copy(e.reply, sizeof(e.reply), reply);
|
||||
_head = (_head + 1) % SIZE;
|
||||
if (_n < SIZE) _n++;
|
||||
}
|
||||
int count() const { return _n; }
|
||||
const BotLogEntry& at(int i) const { return _ring[(_head + SIZE - 1 - i) % SIZE]; } // 0: the newest
|
||||
|
||||
private:
|
||||
static void copy(char* out, size_t n, const char* s) {
|
||||
strncpy(out, s ? s : "", n - 1);
|
||||
out[n - 1] = '\0';
|
||||
}
|
||||
BotLogEntry* _ring = nullptr;
|
||||
int _head = 0, _n = 0;
|
||||
#else
|
||||
void add(uint32_t, uint8_t, const char*, const char*, const char*) {}
|
||||
int count() const { return 0; }
|
||||
const BotLogEntry& at(int) const { static BotLogEntry e = {}; return e; }
|
||||
#endif
|
||||
};
|
||||
@@ -43,6 +43,7 @@ class UITask;
|
||||
#include "DataStore.h"
|
||||
#include "NodePrefs.h"
|
||||
#include "ScopeList.h"
|
||||
#include "BotLog.h"
|
||||
|
||||
#include <RTClib.h>
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
@@ -587,6 +588,7 @@ public:
|
||||
|
||||
// Number of auto-replies sent since boot (DM + channel + room). Shown on BotScreen.
|
||||
uint16_t botReplyCount() const { return _bot_reply_count; }
|
||||
const BotLog& botLog() const { return _bot_log; } // the recent replies (ESP32)
|
||||
|
||||
// Whether a "!gps fix" request is acquiring/averaging right now -- used by
|
||||
// UITask's GPS duty-cycle "is anything live using GPS right now" check, so
|
||||
@@ -726,6 +728,7 @@ private:
|
||||
static const int BOT_DM_LOG_SIZE = 8;
|
||||
BotReplyLog _bot_dm_log[BOT_DM_LOG_SIZE];
|
||||
uint16_t _bot_reply_count; // total auto-replies sent since boot
|
||||
BotLog _bot_log;
|
||||
|
||||
// !gps fix state -- one global slot (one physical GPS): botCommandReply()
|
||||
// rejects a second request outright while one is active, so this never
|
||||
|
||||
@@ -188,6 +188,7 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t ho
|
||||
if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED) {
|
||||
botDmRecord(from.id.pub_key);
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::DM, from.name, text, expanded);
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_listener) _listener->addDMMsg(from.id.pub_key, true, expanded);
|
||||
#endif
|
||||
@@ -233,6 +234,7 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text, uint8_t h
|
||||
if (sendGroupMessage(ts, ch.channel, _prefs.node_name, expanded, rlen)) {
|
||||
_bot_last_ch_reply_ms = millis();
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::CHANNEL, sender_name, msg, expanded);
|
||||
#ifdef DISPLAY_CLASS
|
||||
mirrorOwnChannelMsg(channel_idx, expanded);
|
||||
#endif
|
||||
@@ -277,6 +279,7 @@ void MyMesh::tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_pref
|
||||
if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED) {
|
||||
_bot_last_room_reply_ms = millis();
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::ROOM, sender_name, text, expanded);
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_listener) _listener->addDMMsg(from.id.pub_key, true, expanded);
|
||||
#endif
|
||||
@@ -512,6 +515,7 @@ bool MyMesh::tryBotCommand(const ContactInfo& from, const char* text, uint8_t ho
|
||||
if (sendMessage(from, ts, 0, out, expected_ack, est_timeout) != MSG_SEND_FAILED) {
|
||||
botDmRecord(from.id.pub_key);
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::DM, from.name, text, out);
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_listener) _listener->addDMMsg(from.id.pub_key, true, out);
|
||||
#endif
|
||||
@@ -547,6 +551,7 @@ bool MyMesh::tryBotChannelCommand(uint8_t channel_idx, const char* text, uint8_t
|
||||
if (sendGroupMessage(ts, ch.channel, _prefs.node_name, out, rlen)) {
|
||||
_bot_last_ch_reply_ms = millis();
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::CHANNEL, sender_name, msg, out);
|
||||
#ifdef DISPLAY_CLASS
|
||||
mirrorOwnChannelMsg(channel_idx, out);
|
||||
#endif
|
||||
@@ -580,6 +585,7 @@ bool MyMesh::tryBotRoomCommand(const ContactInfo& from, const uint8_t* sender_pr
|
||||
if (sendMessage(from, ts, 0, out, expected_ack, est_timeout) != MSG_SEND_FAILED) {
|
||||
_bot_last_room_reply_ms = millis();
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::ROOM, sender_name, text, out);
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_listener) _listener->addDMMsg(from.id.pub_key, true, out);
|
||||
#endif
|
||||
@@ -703,6 +709,7 @@ void MyMesh::sendLocFixResult(const char* msg) {
|
||||
if (sendGroupMessage(ts, ch.channel, _prefs.node_name, msg, mlen)) {
|
||||
_bot_last_ch_reply_ms = millis();
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, BotLog::CHANNEL, "", "!gps fix", msg);
|
||||
#ifdef DISPLAY_CLASS
|
||||
mirrorOwnChannelMsg(_loc_fix.channel_idx, msg);
|
||||
#endif
|
||||
@@ -715,6 +722,7 @@ void MyMesh::sendLocFixResult(const char* msg) {
|
||||
if (c->type == ADV_TYPE_ROOM) _bot_last_room_reply_ms = millis();
|
||||
else botDmRecord(c->id.pub_key);
|
||||
_bot_reply_count++;
|
||||
_bot_log.add(ts, c->type == ADV_TYPE_ROOM ? BotLog::ROOM : BotLog::DM, c->name, "!gps fix", msg);
|
||||
#ifdef DISPLAY_CLASS
|
||||
if (_listener) _listener->addDMMsg(c->id.pub_key, true, msg);
|
||||
#endif
|
||||
|
||||
@@ -10,8 +10,17 @@
|
||||
|
||||
namespace botcfg {
|
||||
|
||||
// Log: the bot's recent replies (the_mesh.botLog()), where there's the
|
||||
// memory for them (BOT_LOG, BotLog.h). A tab of its own, no setting rows.
|
||||
#ifdef BOT_LOG
|
||||
enum Tab : uint8_t { TAB_CHANNEL, TAB_ROOM, TAB_DIRECT, TAB_OTHER, TAB_LOG, TAB_COUNT };
|
||||
static const char* const TAB_LABELS[TAB_COUNT] = { "Channel", "Room", "Direct", "Other", "Log" };
|
||||
static bool isLog(int tab) { return tab == TAB_LOG; }
|
||||
#else
|
||||
enum Tab : uint8_t { TAB_CHANNEL, TAB_ROOM, TAB_DIRECT, TAB_OTHER, TAB_COUNT };
|
||||
static const char* const TAB_LABELS[TAB_COUNT] = { "Channel", "Room", "Direct", "Other" };
|
||||
static bool isLog(int) { return false; }
|
||||
#endif
|
||||
|
||||
enum Kind : uint8_t { ENABLE_DM, DM_SCOPE, COMMANDS_DM, ACTIONS_DM, TRIGGER_DM, REPLY_DM,
|
||||
ENABLE_CH, CHANNEL, COMMANDS_CH, ACTIONS_CH, TRIGGER_CH, REPLY_CH,
|
||||
@@ -56,7 +65,8 @@ static int rowCount(int tab) {
|
||||
case TAB_CHANNEL: return sizeof(CHANNEL_ROWS) / sizeof(Row);
|
||||
case TAB_ROOM: return sizeof(ROOM_ROWS) / sizeof(Row);
|
||||
case TAB_DIRECT: return sizeof(DIRECT_ROWS) / sizeof(Row);
|
||||
default: return sizeof(OTHER_ROWS) / sizeof(Row);
|
||||
case TAB_OTHER: return sizeof(OTHER_ROWS) / sizeof(Row);
|
||||
default: return 0; // Log
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +121,15 @@ static bool quietOff(const NodePrefs* p) { return p->bot_quiet_start == p->bot_q
|
||||
// Placeholders a reply can use: the bot fills {name} / {hops} / {path} from the
|
||||
// message it answers; {loc} / {time} and the sensor ones come from here.
|
||||
// Most useful first (a frontend short of room shows a prefix).
|
||||
// A log entry's one-line form: "14:30 Bob: !snr > SNR 6.5dB ...", the time
|
||||
// local (tz hours); a room / channel entry marked with # / @.
|
||||
static void logLine(const BotLogEntry& e, int8_t tz, char* out, size_t n) {
|
||||
uint32_t t = e.ts + (int32_t)tz * 3600;
|
||||
const char* mark = e.where == BotLog::CHANNEL ? "#" : e.where == BotLog::ROOM ? "@" : "";
|
||||
snprintf(out, n, "%02u:%02u %s%s: %s > %s", (unsigned)(t / 3600 % 24), (unsigned)(t / 60 % 60), mark,
|
||||
e.who[0] ? e.who : "?", e.req, e.reply);
|
||||
}
|
||||
|
||||
static const char* const REPLY_PLACEHOLDERS[] = { "{name}", "{hops}", "{path}", "{snr}", "{loc}", "{time}", "{batt}" };
|
||||
static const int REPLY_PLACEHOLDER_COUNT = sizeof(REPLY_PLACEHOLDERS) / sizeof(REPLY_PLACEHOLDERS[0]);
|
||||
|
||||
|
||||
@@ -93,6 +93,31 @@ void UITask::buildBot() {
|
||||
lv_obj_t* list = scrollList(body);
|
||||
lv_obj_t* g = group(list, nullptr);
|
||||
|
||||
if (botcfg::isLog(s_tab)) { // the replies, newest first: when and who, then what was asked and answered
|
||||
const BotLog& log = the_mesh.botLog();
|
||||
lv_obj_set_style_pad_hor(g, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_ver(g, 6, 0);
|
||||
if (log.count() == 0) label(g, "No replies yet", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
for (int i = 0; i < log.count(); i++) {
|
||||
const BotLogEntry& e = log.at(i);
|
||||
const uint32_t t = e.ts + (int32_t)p->tz_offset_hours * 3600;
|
||||
static const char* const WHERE[] = { "DM", "channel", "room" };
|
||||
char hd[64];
|
||||
snprintf(hd, sizeof(hd), "%02u:%02u %s (%s)", (unsigned)(t / 3600 % 24), (unsigned)(t / 60 % 60),
|
||||
e.who[0] ? e.who : "?", WHERE[e.where < 3 ? e.where : 0]);
|
||||
label(g, hd, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* q = label(g, e.req, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(q, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(q, lv_pct(100));
|
||||
lv_obj_t* a = label(g, "", THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_label_set_text_fmt(a, LV_SYMBOL_RIGHT " %s", e.reply);
|
||||
lv_label_set_long_mode(a, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(a, lv_pct(100));
|
||||
if (i + 1 < log.count()) lv_obj_set_style_pad_bottom(a, 8, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < botcfg::rowCount(s_tab); i++) {
|
||||
const botcfg::Row& r = botcfg::row(s_tab, i);
|
||||
uint8_t* fl = botcfg::flag(p, r.kind);
|
||||
|
||||
@@ -106,6 +106,8 @@ public:
|
||||
display.setColor(UIColor::primary_txt);
|
||||
}
|
||||
|
||||
if (botcfg::isLog(_tab)) return renderLog(display);
|
||||
|
||||
int n = botcfg::rowCount(_tab);
|
||||
int mq_delay = 0;
|
||||
drawList(display, n, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
@@ -143,6 +145,29 @@ public:
|
||||
return 2000;
|
||||
}
|
||||
|
||||
// Log tab: the bot's replies, newest first, one line each (the selected
|
||||
// one scrolls to show the rest).
|
||||
int renderLog(DisplayDriver& display) {
|
||||
const BotLog& log = the_mesh.botLog();
|
||||
if (log.count() == 0) {
|
||||
display.drawTextCentered(display.width() / 2, display.height() / 2, "No replies yet");
|
||||
return 2000;
|
||||
}
|
||||
const int8_t tz = _prefs ? _prefs->tz_offset_hours : 0;
|
||||
int mq_delay = 0;
|
||||
drawList(display, log.count(), _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
char line[200];
|
||||
botcfg::logLine(log.at(i), tz, line, sizeof(line));
|
||||
char filt[200];
|
||||
display.translateUTF8ToBlocks(filt, line, sizeof(filt));
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 2 - reserve, filt, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
display.setColor(UIColor::primary_txt);
|
||||
});
|
||||
return mq_delay > 0 ? mq_delay : 2000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
bool up = (c == KEY_UP);
|
||||
bool down = (c == KEY_DOWN);
|
||||
@@ -180,12 +205,14 @@ public:
|
||||
if (keyIsPrev(c)) { tabbar::turn(_task->getDisplay(), botcfg::TAB_LABELS, _tab, -1); _tab = (_tab + botcfg::TAB_COUNT - 1) % botcfg::TAB_COUNT; _sel = _scroll = 0; return true; }
|
||||
if (keyIsNext(c)) { tabbar::turn(_task->getDisplay(), botcfg::TAB_LABELS, _tab, +1); _tab = (_tab + 1) % botcfg::TAB_COUNT; _sel = _scroll = 0; return true; }
|
||||
|
||||
int n = botcfg::rowCount(_tab);
|
||||
int n = botcfg::isLog(_tab) ? the_mesh.botLog().count() : botcfg::rowCount(_tab);
|
||||
if (n == 0) return up || down || enter;
|
||||
// drawList() reclamps _scroll from _sel every render.
|
||||
if (up) { _sel = (_sel > 0) ? _sel - 1 : n - 1; return true; }
|
||||
if (down) { _sel = (_sel < n - 1) ? _sel + 1 : 0; return true; }
|
||||
|
||||
if (!enter) return false;
|
||||
if (botcfg::isLog(_tab)) return true; // only read
|
||||
Kind k = tabRow(_tab, _sel).kind;
|
||||
|
||||
if (uint8_t* fl = botcfg::flag(_prefs, k)) { *fl ^= 1; _dirty = true; return true; }
|
||||
|
||||
@@ -190,7 +190,7 @@ if [ "$SIM_UI" = "lvgl" ]; then
|
||||
done
|
||||
# The L2's PSRAM-sized limits (solo/wio-tracker-l2/platformio.ini).
|
||||
DEFINES=("${kept[@]}" -DDISPLAY_CLASS=SimLcdDisplay -DMAX_GROUP_CHANNELS=40 -DUI_ZOOM=1 -DLV_CONF_INCLUDE_SIMPLE
|
||||
-DTRAIL_CAPACITY=32768 -DTRAIL_SIMPLIFY=0 -DTRAIL_FIXED_MIN_DELTA_M=5 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE)
|
||||
-DTRAIL_CAPACITY=32768 -DTRAIL_SIMPLIFY=0 -DTRAIL_FIXED_MIN_DELTA_M=5 -DWAYPOINT_CAPACITY=64 -DHIST_CH_MAX=256 -DHIST_DM_MAX=128 -DNEARBY_MAX=64 -DHIST_ARCHIVE -DBOT_LOG_SIM)
|
||||
INCLUDES+=("-I$LVGL_DIR")
|
||||
LINK_EXTRA=(-sFETCH=1) # map downloads (LvglPort.h) go through emscripten_fetch
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user