feat(bot): Actions commands, multi-trigger, and user GPIO pins

- 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>
This commit is contained in:
Jakub
2026-07-21 20:30:03 +02:00
parent 7113d34ea1
commit 5bfebc6559
15 changed files with 729 additions and 55 deletions

View File

@@ -526,6 +526,31 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
rd(&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet));
if (_prefs.keyboard_main_alphabet >= NodePrefs::KB_ALPHABET_COUNT) _prefs.keyboard_main_alphabet = 0;
// → 0xC0DE0021: append the per-target bot-actions toggles at the tail. A
// pre-0x21 file has neither byte here; clamp to 0 (off) -- these gate
// state-changing bot commands (!buzz/!gps/!advert), so an upgrader must
// opt in deliberately rather than get them silently enabled.
rd(&_prefs.bot_actions_dm, sizeof(_prefs.bot_actions_dm));
if (_prefs.bot_actions_dm > 1) _prefs.bot_actions_dm = 0;
rd(&_prefs.bot_actions_ch, sizeof(_prefs.bot_actions_ch));
if (_prefs.bot_actions_ch > 1) _prefs.bot_actions_ch = 0;
rd(&_prefs.bot_actions_room, sizeof(_prefs.bot_actions_room));
if (_prefs.bot_actions_room > 1) _prefs.bot_actions_room = 0;
// → 0xC0DE0022: user-assignable GPIO pin modes (0=Off 1=In 2=Out-low
// 3=Out-high 4=Analog). A pre-0x22 file has none of these bytes; clamp to
// 0 (off). gpio1/gpio2 (AIN0/AIN5) allow mode 4; gpio3/gpio4 have no ADC
// channel, so their clamp stops at 3 -- a stray 4 there (corrupt file,
// schema mismatch) falls back to Off rather than doing something undefined.
rd(&_prefs.gpio1_mode, sizeof(_prefs.gpio1_mode));
if (_prefs.gpio1_mode > 4) _prefs.gpio1_mode = 0;
rd(&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode));
if (_prefs.gpio2_mode > 4) _prefs.gpio2_mode = 0;
rd(&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode));
if (_prefs.gpio3_mode > 3) _prefs.gpio3_mode = 0;
rd(&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
if (_prefs.gpio4_mode > 3) _prefs.gpio4_mode = 0;
// Schema sentinel: bumped on layout changes. Mismatch means an older file
// (or a different schema); rd() already zero-inits any fields not present,
// so we just log it — next savePrefs writes the current sentinel.
@@ -730,6 +755,13 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.bot_commands_ch, sizeof(_prefs.bot_commands_ch));
file.write((uint8_t *)&_prefs.bot_commands_room, sizeof(_prefs.bot_commands_room));
file.write((uint8_t *)&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet));
file.write((uint8_t *)&_prefs.bot_actions_dm, sizeof(_prefs.bot_actions_dm));
file.write((uint8_t *)&_prefs.bot_actions_ch, sizeof(_prefs.bot_actions_ch));
file.write((uint8_t *)&_prefs.bot_actions_room, sizeof(_prefs.bot_actions_room));
file.write((uint8_t *)&_prefs.gpio1_mode, sizeof(_prefs.gpio1_mode));
file.write((uint8_t *)&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode));
file.write((uint8_t *)&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode));
file.write((uint8_t *)&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
// the one we check: once the flash fills, writes return 0, so a good