- Auto-Reply Bot gains Actions (!buzz/!gps/!advert) behind a new per-target
toggle nested under Commands (bot_actions_dm/ch/room); off by default.
- Bot Trigger fields accept comma-separated multiple phrases, matching any
one fires the reply.
- New user-assignable GPIO feature (Wio Tracker L1): !gpio1..!gpio4 bot
commands plus a Tools > GPIO screen. Each pin cycles Off/Input/Output;
GPIO1/GPIO2 (P0.02/P0.29, the nRF52840's AIN0/AIN5) also offer a read-only
Analog mode via direct SAADC access. GPIO3/GPIO4 (P0.09/P0.10) are the
chip's NFC1/NFC2 pins, repurposed as plain GPIO via a one-time UICR
NFCPINS bit-clear in initVariant() (adapted from Adafruit's own
nfc_to_gpio example) -- confirmed working on real hardware.
- Fix: DM/room reply-prefix ("@[nick] ") stripping happened at the wrong
layer, hiding the "To:" header on DM replies and leaking the raw prefix
into room messages' list view; a related mismatch had the history
scrollbar's sizing pass wrap room messages with the sender name still
attached, disagreeing with the actual rendered text.
Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean
(sizeof(NodePrefs) confirmed 2720 via real build, not guessed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renamed the vestigial Lemon/default font-switch naming (setLemonFont/
isLemonFont/drawLemonChar/lemonXAdvance/_use_lemon -> setSingleFont/
isSingleFont/drawGlyph/glyphXAdvance/_single_font) across DisplayDriver.h
and both concrete drivers -- both have been permanently single-font for
several commits, so the old names invited a future reader to think a
real switch still existed. Pure identifier rename, no logic changed.
Also extracted the byte-identical "SenderName: " prefix-splitting in
MyMeshBot.h's tryBotReplyChannel()/tryBotChannelCommand() into a shared
botChannelSenderSplit(), mirroring the existing botRoomSenderName().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Trigger is now stored as typed (e.g. "Ping" stays "Ping").
Case-insensitive matching is preserved by lowercasing both
the trigger and incoming message at comparison time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For a 140-byte channel msg with a 64-byte trigger, the old loop did
~8960 tolower() calls per inbound message; now it's 140 tolower calls
plus one strstr.
perf(buzzer): skip nRF52 STOPPED wait when PWM was not running
TASKS_STOP on a disabled PWM never fires EVENTS_STOPPED, so the wait
always timed out at 2 ms. Gating on _pwm_on removes the wait on the
first note of a melody and after pauses, where there's nothing to wait
for.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tryBotReplyChannel checked bot_channel_enabled but not bot_enabled,
so disabling the bot via the main Enable toggle had no effect on
channel replies. Added bot_enabled to the guard condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single shared _bot_last_reply_ms caused a DM reply to block the
channel bot for 10s and vice versa. Split into _bot_last_dm_reply_ms
and _bot_last_ch_reply_ms so each cooldown is independent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- with_sender[160] could hold only ~160 chars but node_name(32) +
expanded(200) needs up to 234; snprintf would silently truncate
the UI display entry. Increased to 240.
- FS_LINE_H was 9 but Adafruit 5x7 font line height is 8, wasting
1px per line and showing 5 lines instead of 6.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Channel messages have the format "sender_name: body". The trigger was
being searched across the whole string, so a nick containing the trigger
word would fire a reply even with an unrelated message. Now the match
skips past the ": " separator and only checks the message body.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expandMsg now expands {temp}, {hum}, {pres}, {batt}, {alt}, {lux}, {dist},
{co2} from live sensor readings. The [{}] picker in the on-screen keyboard
shows only sensors currently detected by SensorManager and scrolls when
more than 3 items fit the overlay.
New SensorPlaceholders.h helper keeps sensor-to-keyboard wiring out of
UITask.cpp. MsgExpand.h handles all expansion logic centrally; MyMeshBot
and QuickMsgScreen pass &sensors and batt_volts to each call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add DM_HIST phase in QuickMsgScreen: 32-entry ring buffer tracks incoming
and outgoing DMs per contact; selecting a contact now opens history view
before compose
- Per-contact DM unread badge in contact list (replaces hop count display)
- Bot replies (DM and channel) stored in history so they appear on-screen
- Fix contact index truncation: _sorted was uint8_t (max 255), causing
contacts at indices 256-349 to map to wrong entries; changed to uint16_t
- Add addDMMsg() virtual to AbstractUITask; wire through MyMesh.cpp
- Incoming chat DMs call addDMMsg(); outgoing after successful send too
- msgRead(0) from companion app clears per-contact unread table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bot redesign: DM bot active for all private messages (no pubkey filter),
channel bot optional with separate enable flag; both can run simultaneously
- Bot: one shared trigger, separate reply texts for DM and channel
- Buzzer: add Auto mode (mutes when BT connected, unmutes on disconnect);
per-channel force-on overrides still work as intended
- Settings: defer flash writes to menu exit (only when dirty)
- Settings: add Seconds toggle (hide clock seconds, refresh drops 1s→60s)
- Fullscreen msg preview: fix truncation (fmsg buf 79→140 chars)
- RingtoneEditor: fix melody playback cut after first note (RTTTL lib holds
pointer — moved play buffer to member variable)
- BotScreen: remove key hint footer
- .gitattributes: protect README.md from upstream merge conflicts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without the check, a DM-mode bot (bot_target_type==1) would also reply
to channel messages whenever the trigger phrase and channel index happened
to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move tryBotReplyDM() and tryBotReplyChannel() implementations to
MyMeshBot.h (included at end of MyMesh.cpp). onMessageRecv and
onChannelMessageRecv now contain only a single-line call each,
minimising the diff against upstream in those methods.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>