feat(bot): hardening + auto-responder, query commands, quiet hours

Reply-bot overhaul on top of the trigger/reply auto-reply:

Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
  (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
  while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
  (was: any reply containing the trigger word — which silently killed channel
  replies for the common "trigger word in reply" setup).

Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
  on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
  tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
  combined reply. DM = per-contact throttle, ignores quiet hours; channel =
  broadcast, per-channel cooldown, respects quiet hours. !hops uses
  getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.

UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
  fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
  persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
  (bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-15 11:57:27 +02:00
co-authored by Claude Opus 4.8
parent 8c0ccaed73
commit 8bd6fbf1cb
8 changed files with 410 additions and 100 deletions
+5 -10
View File
@@ -493,14 +493,9 @@ Tools Stats:
Read-only. UP/DOWN switches between metrics. Bottom shows current value as text.
### Auto-reply trigger words with sensor placeholders
### Auto-reply query commands with live data
Bot already has a single trigger word. Extend to a small table of (trigger, reply-with-placeholder) pairs:
- "temp?" → replies with `{temp}`
- "loc?" → replies with `{loc}`
- "batt?" → replies with `{batt}`
Stored in NodePrefs as fixed-size table (say 4 pairs × 16 B). UI: edit pairs in Tools Auto-Reply Bot.
Realised as a **command bot** rather than a trigger/reply table: with **Commands** ON, a DM is scanned for `!word` tokens and answered with live node data via `expandMsg``!batt`/`!loc`/`!time`/`!temp`/`!status`/`!ping`/`!help`, plus `!hops` (per-message hop count via `getPathHashCount()`, `direct` if heard directly). Multiple commands in one message are merged into a single ` | `-joined reply (one transmission/throttle/counter tick) via the shared `botScanCommands`. Works in DMs (per-contact throttle, ignores quiet hours — a pull) and on the bot's **monitored channel** (broadcast: per-channel cooldown, respects quiet hours). Toggled independently of the trigger bot. See [`MyMeshBot.h`](examples/companion_radio/MyMeshBot.h) `tryBotCommand` / `tryBotChannelCommand` / `botCommandReply`.
### ✅ Lock-screen unread count
@@ -774,7 +769,7 @@ Pub-key line is skipped entirely when `max_chars < 4` instead of feeding a negat
### 📋 `expandMsg` GPS validity test treats (0, 0) as invalid
[`MyMeshBot.h:23, 60`](examples/companion_radio/MyMeshBot.h#L23)
[`MyMeshBot.h:95, 132, 182`](examples/companion_radio/MyMeshBot.h#L95)
```cpp
sensors.node_lat != 0.0 || sensors.node_lon != 0.0 // proxy for "valid GPS"
@@ -798,9 +793,9 @@ Safe today (CAPACITY=512 fits), fragile if CAPACITY grows past 65535.
### 📋 Bot `strstr` on truncated 199-char buffer
[`MyMeshBot.h:14-17`](examples/companion_radio/MyMeshBot.h#L14-L17)
[`MyMeshBot.h:22-25`](examples/companion_radio/MyMeshBot.h#L22-L25)
Trigger word near the end of a >199-character message will not match.
Trigger word near the end of a >199-character message will not match. Matching is now centralised in `botTriggerMatches()` (single `BOT_SCRATCH`-sized buffer), so a fix would land in one place.
### ✅ `strncpy("?", buf, sizeof(buf))` replaced with `strcpy`