From 30ebef765f52569b0b6db230aba30f5c8ca78561 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:46:28 +0100 Subject: [PATCH] feat(bot): {path} !path (#55), {snr} {rssi} !snr, !dist, Actions by All/Fav Co-Authored-By: Claude Opus 5.5 --- docs/solo/tools.md | 9 ++- examples/companion_radio/MsgExpand.h | 24 +++++- examples/companion_radio/MyMesh.cpp | 9 +++ examples/companion_radio/MyMesh.h | 3 + examples/companion_radio/MyMeshBot.h | 80 +++++++++++++++++--- examples/companion_radio/NodePrefs.h | 3 +- examples/companion_radio/ui-core/BotConfig.h | 10 ++- examples/companion_radio/ui-lvgl/BotScreen.h | 2 +- examples/companion_radio/ui-new/BotScreen.h | 5 +- 9 files changed, 127 insertions(+), 18 deletions(-) diff --git a/docs/solo/tools.md b/docs/solo/tools.md index 14fb53b5..a4db5282 100644 --- a/docs/solo/tools.md +++ b/docs/solo/tools.md @@ -41,7 +41,7 @@ channel and one room, each switched on separately: - **Trigger and reply**: when a message contains the trigger (several can be separated by commas, `*` matches any message), the bot sends the reply. The - reply can use placeholders, plus `{name}` (the sender) and `{hops}`. + reply can use placeholders, plus `{name}` (the sender), `{hops}`, `{path}` (the route in, as hop hashes oldest first: `3f:a1:c8`, or `direct`), `{snr}` and `{rssi}` (how its last hop was heard). - **Commands**: messages starting with `!` get live data back: | Command | Reply | @@ -49,13 +49,18 @@ channel and one room, each switched on separately: | `!ping` | `pong` | | `!batt`, `!temp`, `!time`, `!loc` | battery, temperature, time, position | | `!hops` | how many hops the command took | + | `!path` | which way it came: the hop hashes, oldest first (`3f:a1:c8`; 2-byte mode `3fa1:c802`), or `direct` | + | `!snr` | how its last hop was heard here: SNR and RSSI | + | `!dist` | how far away and which way the sender is (both need a position) | | `!status` | battery, position and time | | `!help` | the list of commands | Several in one message get one reply: `!batt !time` → `4.10V | 14:30`. - **Actions** (off by default) let the command change the device: `!buzz` (sounds the buzzer to find it), `!gps on` / `!gps off`, `!gps fix` (turns - the GPS on, waits for a good fix and sends it), `!advert`. + the GPS on, waits for a good fix and sends it), `!advert`. **Actions by = + 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. **DM allow = Fav** limits the DM bot to your favourites, and **quiet hours** stop trigger replies at night (commands still answer). Replies are rate diff --git a/examples/companion_radio/MsgExpand.h b/examples/companion_radio/MsgExpand.h index efa236ef..ca03360a 100644 --- a/examples/companion_radio/MsgExpand.h +++ b/examples/companion_radio/MsgExpand.h @@ -21,6 +21,15 @@ // {co2} — CO2 concentration ppm (requires sm) // {name} — sender's name (bot replies only; requires sender_name, else left literal) // {hops} — hop count: "direct" or "N hops" (bot replies only; requires hops>=0, else left literal) +// {path} — inbound route as hop hashes, oldest first: "3f:a1:c8" (bot replies only; requires `in`, else left literal) +// {snr} — the message's last hop's SNR, "6.5dB" (bot replies only) +// {rssi} — and its RSSI, "-92dBm" (bot replies only) +// How the message a bot reply answers came in (MyMesh::setBotInbound()). +struct BotInbound { + const char* path; // "direct" or the hop hashes + float snr, rssi; // the last hop's +}; + inline void expandMsg(const char* tmpl, char* out, int out_len, double lat, double lon, bool gps_valid, uint32_t utc_ts, int8_t tz_hours, @@ -28,7 +37,8 @@ inline void expandMsg(const char* tmpl, char* out, int out_len, float batt_volts = -1.0f, const char* sender_name = nullptr, int hops = -1, - float mcu_temp = NAN) { // the chip's, for {temp} without a sensor + float mcu_temp = NAN, + const BotInbound* in = nullptr) { // the chip's, for {temp} without a sensor // sv indices: 0=temp 1=hum 2=pres 3=batt 4=alt 5=lux 6=dist 7=co2 float sv[8] = {}; bool sv_ok[8] = {}; @@ -139,6 +149,18 @@ inline void expandMsg(const char* tmpl, char* out, int out_len, APPEND(b, strlen(b)); } else { APPEND("{hops}", 6); } p += 6; + } else if (strncmp(p, "{path}", 6) == 0) { + if (in && in->path) { APPEND(in->path, strlen(in->path)); } + else { APPEND("{path}", 6); } + p += 6; + } else if (strncmp(p, "{snr}", 5) == 0) { + if (in) { char b[12]; snprintf(b, sizeof(b), "%.1fdB", in->snr); APPEND(b, strlen(b)); } + else { APPEND("{snr}", 5); } + p += 5; + } else if (strncmp(p, "{rssi}", 6) == 0) { + if (in) { char b[12]; snprintf(b, sizeof(b), "%.0fdBm", in->rssi); APPEND(b, strlen(b)); } + else { APPEND("{rssi}", 6); } + p += 6; } else { out[oi++] = *p++; } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0101805e..634c7f75 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -694,6 +694,7 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t // retransmit priority. 0 = heard directly. (Raw path_len is a size/count // bitfield for transport packets, so it must not be used directly.) uint8_t hops = pkt ? pkt->getPathHashCount() : 0; + setBotInbound(pkt, &from); if (!tryBotCommand(from, text, hops)) // commands take priority; fall through to trigger reply tryBotReplyDM(from, text, hops); } @@ -753,6 +754,7 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin bool replayed = now > sender_timestamp && now - sender_timestamp > BOT_ROOM_MAX_AGE_SECS; if (from.type == ADV_TYPE_ROOM && !replayed) { uint8_t hops = pkt ? pkt->getPathHashCount() : 0; + setBotInbound(pkt, sender_prefix ? lookupContactByPubKey(sender_prefix, 4) : nullptr); if (!tryBotRoomCommand(from, sender_prefix, text, hops)) // commands take priority tryBotReplyRoom(from, sender_prefix, text, hops); } @@ -823,6 +825,12 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe // hop count for !hops (see onMessageRecv); not the wire path_len above. uint8_t ch_hops = pkt->getPathHashCount(); + { + char sender[32]; + const char* msg; + botChannelSenderSplit(text, sender, sizeof(sender), &msg); + setBotInbound(pkt, botContactByName(sender)); + } if (!tryBotChannelCommand(channel_idx, text, ch_hops)) // commands take priority tryBotReplyChannel(channel_idx, text, ch_hops); } @@ -1416,6 +1424,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.bot_commands_enabled = 0; _prefs.bot_quiet_start = 0; _prefs.bot_quiet_end = 0; // start==end → quiet hours disabled + _prefs.bot_actions_scope = 0; // actions by everyone _prefs.dm_show_all = 1; // show all contacts by default _prefs.fav_sort = 1; // favourites first in lists _prefs.msg_wake = 1; // a new message turns the screen on diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index cad385f5..a237f2ec 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -594,6 +594,9 @@ public: bool isGpsFixPending() const { return _loc_fix.active; } private: + void setBotInbound(const mesh::Packet* pkt, const ContactInfo* sender); // for {path} {snr} !path !snr !dist + const ContactInfo* botContactByName(const char* name); + bool botActionsFrom(const ContactInfo* c) const; // bot_actions_scope void tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t hops); void tryBotReplyChannel(uint8_t channel_idx, const char* text, uint8_t hops); void tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_prefix, const char* text, uint8_t hops); diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index 89446a15..3b128efe 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -128,6 +128,46 @@ bool MyMesh::botInQuietHours() const { return hourInWindow(h, _prefs.bot_quiet_start, _prefs.bot_quiet_end); } +// How the message about to be answered came in, for {path} / {snr} / {rssi} +// and !path / !snr / !dist. The route: hop hashes oldest first, ':' between +// hops, each hash as wide as the packet's own path hash mode ("3f:a1:c8", +// "3fa1:c802"); "direct" if heard directly. The sender's position: its +// contact's, when it has one (on a channel, the contact of that name). +static char s_bot_path[MAX_PATH_SIZE * 3 + 8]; +static BotInbound s_bot_in = { s_bot_path, 0, 0 }; +static bool s_bot_from_pos = false; +static int32_t s_bot_from_lat = 0, s_bot_from_lon = 0; + +void MyMesh::setBotInbound(const mesh::Packet* pkt, const ContactInfo* sender) { + const uint8_t n = pkt ? pkt->getPathHashCount() : 0, w = pkt ? pkt->getPathHashSize() : 1; + s_bot_in.snr = pkt ? pkt->getSNR() : 0; + s_bot_in.rssi = _radio->getLastRSSI(); + s_bot_from_pos = sender && (sender->gps_lat || sender->gps_lon); + if (s_bot_from_pos) { s_bot_from_lat = sender->gps_lat; s_bot_from_lon = sender->gps_lon; } + if (n == 0) { strcpy(s_bot_path, "direct"); return; } + char* o = s_bot_path; + for (uint8_t h = 0; h < n; h++) { + if (h) *o++ = ':'; + for (uint8_t b = 0; b < w; b++) { sprintf(o, "%02x", pkt->path[h * w + b]); o += 2; } + } +} + +// The contact named `name` (a channel sender: only a name, unverified). +const ContactInfo* MyMesh::botContactByName(const char* name) { + for (int idx = 0; ; idx++) { + ContactInfo c; + if (!getContactByIdx(idx, c)) return nullptr; + if (c.name[0] && strcmp(c.name, name) == 0) return lookupContactByPubKey(c.id.pub_key, PUB_KEY_SIZE); + } +} + +// Actions by (bot_actions_scope): everyone, or favourites only. On a channel +// the sender is only a name anyone can type, so there it's a weak check. +bool MyMesh::botActionsFrom(const ContactInfo* c) const { + if (_prefs.bot_actions_scope == 0) return true; + return c && (c->flags & 0x01); +} + void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t hops) { if (from.type != ADV_TYPE_CHAT) return; if (!(_prefs.bot_enabled && _prefs.bot_reply_dm[0])) return; @@ -143,7 +183,7 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t ho sensors.node_lat != 0.0 || sensors.node_lon != 0.0, ts, _prefs.tz_offset_hours, &sensors, (float)board.getBattMilliVolts() / 1000.0f, - from.name, hops, board.getMCUTemperature()); + from.name, hops, board.getMCUTemperature(), &s_bot_in); uint32_t expected_ack, est_timeout; if (sendMessage(from, ts, 0, expanded, expected_ack, est_timeout) != MSG_SEND_FAILED) { botDmRecord(from.id.pub_key); @@ -184,7 +224,7 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text, uint8_t h sensors.node_lat != 0.0 || sensors.node_lon != 0.0, ts, _prefs.tz_offset_hours, &sensors, (float)board.getBattMilliVolts() / 1000.0f, - sender_name, hops, board.getMCUTemperature()); + sender_name, hops, board.getMCUTemperature(), &s_bot_in); // Anti-loop: don't echo a message identical to our own reply (e.g. another bot // on the channel sending the same text), which would ping-pong forever. The // per-channel cooldown caps any residual back-and-forth between mismatched bots. @@ -228,7 +268,7 @@ void MyMesh::tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_pref sensors.node_lat != 0.0 || sensors.node_lon != 0.0, ts, _prefs.tz_offset_hours, &sensors, (float)board.getBattMilliVolts() / 1000.0f, - sender_name, hops, board.getMCUTemperature()); + sender_name, hops, board.getMCUTemperature(), &s_bot_in); // Anti-loop, mirrors the channel path: don't echo a message identical to // our own reply (e.g. re-synced back from the room server). if (strcmp(text, expanded) == 0) return; @@ -251,6 +291,27 @@ void MyMesh::tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_pref // into a custom reply — not used by any of the built-in templates below today. bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, bool actions_allowed, uint8_t hops, uint32_t ts, char* out, int out_len, const char* sender_name) { + if (!strcmp(cmd, "path")) { + strncpy(out, s_bot_path, out_len); // "direct" or the hop hashes, oldest first + out[out_len - 1] = '\0'; + return true; + } + if (!strcmp(cmd, "snr")) { // how the last hop was heard here + snprintf(out, out_len, "SNR %.1fdB RSSI %.0fdBm", s_bot_in.snr, s_bot_in.rssi); + return true; + } + if (!strcmp(cmd, "dist")) { // from here to the sender, when both are known + int32_t lat, lon; + LocationProvider* lp = sensors.getLocationProvider(); + if (lp && lp->isValid()) { lat = lp->getLatitude(); lon = lp->getLongitude(); } + else { lat = (int32_t)lround(sensors.node_lat * 1e6); lon = (int32_t)lround(sensors.node_lon * 1e6); } + if (!lat && !lon) { snprintf(out, out_len, "dist: no position here"); return true; } + if (!s_bot_from_pos) { snprintf(out, out_len, "dist: no position for you"); return true; } + char d[16]; + geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, s_bot_from_lat, s_bot_from_lon), _prefs.units_imperial); + snprintf(out, out_len, "%s %s", d, geo::bearingCardinal(geo::bearingDeg(lat, lon, s_bot_from_lat, s_bot_from_lon))); + return true; + } if (!strcmp(cmd, "hops")) { if (hops == 0) strncpy(out, "direct", out_len); // heard directly (0 repeaters) else snprintf(out, out_len, "%u hops", (unsigned)hops); @@ -358,8 +419,8 @@ bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, else if (!strcmp(cmd, "temp")) tmpl = "{temp}"; else if (!strcmp(cmd, "status")) tmpl = "batt {batt} loc {loc} at {time}"; else if (!strcmp(cmd, "help")) tmpl = actions_allowed - ? "cmds: !ping !batt !loc !time !temp !hops !status !gps !buzz !advert !gpio1-4" - : "cmds: !ping !batt !loc !time !temp !hops !status"; + ? "cmds: !ping !batt !loc !time !temp !hops !path !snr !dist !status !gps !buzz !advert !gpio1-4" + : "cmds: !ping !batt !loc !time !temp !hops !path !snr !dist !status"; else return false; // unknown — let the trigger bot try expandMsg(tmpl, out, out_len, @@ -367,7 +428,7 @@ bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, sensors.node_lat != 0.0 || sensors.node_lon != 0.0, ts, _prefs.tz_offset_hours, &sensors, (float)board.getBattMilliVolts() / 1000.0f, - sender_name, hops, board.getMCUTemperature()); + sender_name, hops, board.getMCUTemperature(), &s_bot_in); if (strchr(out, '{')) strcpy(out, "n/a"); // requested sensor not present return true; } @@ -444,7 +505,7 @@ bool MyMesh::tryBotCommand(const ContactInfo& from, const char* text, uint8_t ho uint32_t ts = getRTCClock()->getCurrentTime(); char out[BOT_SCRATCH]; - if (botScanCommands(text, hops, ts, out, sizeof(out), from.name, _prefs.bot_actions_dm != 0) == 0) return false; // no commands + if (botScanCommands(text, hops, ts, out, sizeof(out), from.name, _prefs.bot_actions_dm != 0 && botActionsFrom(&from)) == 0) return false; // no commands if (!botDmAllowed(from.id.pub_key)) { resetPendingBotActions(); return true; } // throttled uint32_t expected_ack, est_timeout; @@ -476,7 +537,7 @@ bool MyMesh::tryBotChannelCommand(uint8_t channel_idx, const char* text, uint8_t uint32_t ts = getRTCClock()->getCurrentTime(); char out[BOT_SCRATCH]; - if (botScanCommands(msg, hops, ts, out, sizeof(out), sender_name, _prefs.bot_actions_ch != 0) == 0) return false; // no commands + if (botScanCommands(msg, hops, ts, out, sizeof(out), sender_name, _prefs.bot_actions_ch != 0 && botActionsFrom(botContactByName(sender_name))) == 0) return false; // no commands if (botInQuietHours()) { resetPendingBotActions(); return true; } // quiet hours if (millis() - _bot_last_ch_reply_ms <= BOT_REPLY_COOLDOWN_MS) { resetPendingBotActions(); return true; } // throttled @@ -510,7 +571,8 @@ bool MyMesh::tryBotRoomCommand(const ContactInfo& from, const uint8_t* sender_pr uint32_t ts = getRTCClock()->getCurrentTime(); char out[BOT_SCRATCH]; - if (botScanCommands(text, hops, ts, out, sizeof(out), sender_name, _prefs.bot_actions_room != 0) == 0) return false; // no commands + if (botScanCommands(text, hops, ts, out, sizeof(out), sender_name, _prefs.bot_actions_room != 0 && + botActionsFrom(sender_prefix ? lookupContactByPubKey(sender_prefix, 4) : nullptr)) == 0) return false; // no commands if (botInQuietHours()) { resetPendingBotActions(); return true; } // quiet hours if (millis() - _bot_last_room_reply_ms <= BOT_REPLY_COOLDOWN_MS) { resetPendingBotActions(); return true; } // throttled diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f0625b1a..2de5f1f7 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -404,6 +404,7 @@ struct NodePrefsFields { uint8_t bot_actions_room; uint8_t bot_quiet_start; // quiet-hours start hour, local 0-23 (start==end → disabled) uint8_t bot_quiet_end; // quiet-hours end hour, local 0-23 + uint8_t bot_actions_scope; // who may run the bot's actions (!gps !buzz !gpio ...): 0 = all, 1 = favourites // ── GPS, trail & location sharing ───────────────────────────────────── uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) @@ -858,7 +859,7 @@ class NodePrefs : public NodePrefsFields, public ConfigSerializer { s.def("rm_trig", bot_trigger_room, sizeof(bot_trigger_room)); s.def("rm_reply", bot_reply_room, sizeof(bot_reply_room)); s.def("rm_cmd", bot_commands_room); s.def("rm_act", bot_actions_room); - s.def("q_start", bot_quiet_start); s.def("q_end", bot_quiet_end); + s.def("q_start", bot_quiet_start); s.def("q_end", bot_quiet_end); s.def("act_scope", bot_actions_scope); } void gpsSolo(Section& s) { s.def("imperial", units_imperial); diff --git a/examples/companion_radio/ui-core/BotConfig.h b/examples/companion_radio/ui-core/BotConfig.h index 7ff31e47..752e274c 100644 --- a/examples/companion_radio/ui-core/BotConfig.h +++ b/examples/companion_radio/ui-core/BotConfig.h @@ -16,7 +16,7 @@ static const char* const TAB_LABELS[TAB_COUNT] = { "Channel", "Room", "Direct", 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, ENABLE_ROOM, ROOM, COMMANDS_ROOM, ACTIONS_ROOM, TRIGGER_ROOM, REPLY_ROOM, - QUIET_FROM, QUIET_TO }; + QUIET_FROM, QUIET_TO, ACTIONS_SCOPE }; // hint: a one-line explanation for frontends with room for it. struct Row { Kind kind; const char* label; const char* hint; }; @@ -48,6 +48,7 @@ static const Row DIRECT_ROWS[] = { static const Row OTHER_ROWS[] = { { QUIET_FROM, "Quiet from", "No replies in these hours" }, { QUIET_TO, "Quiet to", "Same hour = always on" }, + { ACTIONS_SCOPE, "Actions by", "Everyone, or favourites only" }, }; static int rowCount(int tab) { @@ -81,6 +82,7 @@ static uint8_t* flag(NodePrefs* p, Kind k) { case ACTIONS_DM: return &p->bot_actions_dm; case ACTIONS_CH: return &p->bot_actions_ch; case ACTIONS_ROOM: return &p->bot_actions_room; + case ACTIONS_SCOPE: return &p->bot_actions_scope; // 1 = favourites only default: return nullptr; } } @@ -98,16 +100,18 @@ static char* text(NodePrefs* p, Kind k, int& cap) { } } +// All / Fav rows (a flag shown as who, not on / off). +static bool isScope(Kind k) { return k == DM_SCOPE || k == ACTIONS_SCOPE; } static bool isTrigger(Kind k) { return k == TRIGGER_DM || k == TRIGGER_CH || k == TRIGGER_ROOM; } static bool isHour(Kind k) { return k == QUIET_FROM || k == QUIET_TO; } static uint8_t& hour(NodePrefs* p, Kind k) { return k == QUIET_FROM ? p->bot_quiet_start : p->bot_quiet_end; } static bool quietOff(const NodePrefs* p) { return p->bot_quiet_start == p->bot_quiet_end; } -// Placeholders a reply can use: the bot fills {name} / {hops} from the +// 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). -static const char* const REPLY_PLACEHOLDERS[] = { "{name}", "{hops}", "{loc}", "{time}", "{batt}" }; +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]); // Shown value of a text row ("(none)", "(any msg)" for the "*" trigger). diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index ad589c47..6fbc6c36 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -97,7 +97,7 @@ void UITask::buildBot() { const botcfg::Row& r = botcfg::row(s_tab, i); uint8_t* fl = botcfg::flag(p, r.kind); lv_obj_t* row = settingRow(g, r.label, fl || botcfg::isHour(r.kind) ? r.hint : nullptr); - if (fl && r.kind == botcfg::DM_SCOPE) { + if (fl && botcfg::isScope(r.kind)) { static const char* SCOPE[] = { "All", "Fav", "" }; lv_obj_t* seg = segmented(row, SCOPE, *fl ? 1 : 0, 110, 32); lv_obj_align(seg, LV_ALIGN_RIGHT_MID, -4, 0); diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 6fbc89a8..854be4d0 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -118,7 +118,7 @@ public: char name[32]; const char* shown = ""; if (fl) { - shown = r.kind == botcfg::DM_SCOPE ? (*fl ? "Fav" : "All") : (*fl ? "ON" : "OFF"); + shown = botcfg::isScope(r.kind) ? (*fl ? "Fav" : "All") : (*fl ? "ON" : "OFF"); } else if (txt) { shown = botcfg::shownText(txt, botcfg::isTrigger(r.kind)); } else if (r.kind == botcfg::CHANNEL || r.kind == botcfg::ROOM) { @@ -203,6 +203,9 @@ public: // compose keyboard's own placeholder list. _kb->addPlaceholder("{name}"); _kb->addPlaceholder("{hops}"); + _kb->addPlaceholder("{path}"); + _kb->addPlaceholder("{snr}"); + _kb->addPlaceholder("{rssi}"); } return true; }