Files
MeshCore-Solo/examples/companion_radio/ui-new/GpioScreen.h
Jakub 5bfebc6559 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>
2026-07-21 20:30:03 +02:00

154 lines
5.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// Tools GPIO — manual control for the 4 user-assignable pins (see
// PIN_GPIO1..4 in the board's variant header). Board-specific: this whole
// file only compiles where PIN_GPIO1 is defined (currently Wio Tracker L1).
// Mirrors the bot's !gpio1..!gpio4 commands (MyMeshBot.h) — both paths go
// through UITask::setGpioMode()/botGetGPIO()/botGetGPIOAnalog(), so UI and
// bot state never disagree. Only GPIO1/GPIO2 (the nRF52840's AIN0/AIN5)
// offer the Analog mode step in the cycle -- GPIO3/GPIO4 have no ADC
// channel (see UITask::gpioSupportsAnalog).
// Included by UITask.cpp.
#if defined(PIN_GPIO1)
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/UIScreen.h>
#include "icons.h"
#include "../NodePrefs.h"
class GpioScreen : public UIScreen {
UITask* _task;
NodePrefs* _prefs;
int _sel;
int _scroll;
static const int PIN_COUNT = 4;
enum Kind : uint8_t { ROW_MODE, ROW_STATE };
struct Row { uint8_t pin; Kind kind; }; // pin is 0-based here, 1-based for UITask/bot calls
Row _items[PIN_COUNT * 2];
int _item_count;
// Direction (Off/Input/Output) and on/off state are separate concerns, so
// they're separate rows: a Mode row per pin, always shown, plus a State
// row right after it that only appears once that pin's Mode is Output —
// Input has nothing to toggle (its live level is read-only, shown inline
// on the Mode row instead).
void buildItems() {
_item_count = 0;
for (uint8_t i = 0; i < PIN_COUNT; i++) {
_items[_item_count++] = { i, ROW_MODE };
uint8_t m = gpioModeOf(i);
if (m == 2 || m == 3) _items[_item_count++] = { i, ROW_STATE }; // Output only -- Analog has nothing to toggle
}
if (_sel >= _item_count) _sel = _item_count - 1;
if (_sel < 0) _sel = 0;
}
uint8_t gpioModeOf(int idx) const {
if (!_prefs) return 0;
switch (idx) {
case 0: return _prefs->gpio1_mode;
case 1: return _prefs->gpio2_mode;
case 2: return _prefs->gpio3_mode;
default: return _prefs->gpio4_mode;
}
}
void modeRowValue(int pin, char* buf, size_t n) const {
switch (gpioModeOf(pin)) {
case 1: {
bool is_out = false, val = false;
if (_task->botGetGPIO(pin + 1, is_out, val)) strncpy(buf, val ? "Input (High)" : "Input (Low)", n);
else strncpy(buf, "Input", n);
break;
}
case 2:
case 3: strncpy(buf, "Output", n); break;
case 4: {
int mv = 0;
if (_task->botGetGPIOAnalog(pin + 1, mv)) snprintf(buf, n, "%dmV", mv);
else strncpy(buf, "Analog", n);
break;
}
default: strncpy(buf, "Off", n); break;
}
buf[n - 1] = '\0';
}
// Off -> Input -> Output (starts OFF) -> [Analog, GPIO1/GPIO2 only] -> Off
// ... one press per step. Output's ON/OFF split lives on the State row
// below, not in this cycle; Analog is read-only so it has no State row.
void cycleMode(int pin) {
uint8_t mode = gpioModeOf(pin);
bool analog_ok = _task->gpioSupportsAnalog(pin + 1);
uint8_t next;
if (mode == 0) next = 1;
else if (mode == 1) next = 2;
else if (mode == 2 || mode == 3) next = analog_ok ? 4 : 0;
else next = 0; // was Analog -> back to Off
_task->setGpioMode(pin + 1, next);
const char* name = (next == 0) ? "Off" : (next == 1) ? "Input" : (next == 2) ? "Output" : "Analog";
char msg[24];
snprintf(msg, sizeof(msg), "GPIO%d: %s", pin + 1, name);
_task->showAlert(msg, 800);
buildItems();
}
void toggleState(int pin) {
bool on = (gpioModeOf(pin) == 3);
_task->setGpioMode(pin + 1, on ? 2 : 3);
char msg[24];
snprintf(msg, sizeof(msg), "GPIO%d: %s", pin + 1, on ? "OFF" : "ON");
_task->showAlert(msg, 800);
}
public:
GpioScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs), _sel(0), _scroll(0), _item_count(1) {}
void onShow() override { _sel = 0; _scroll = 0; buildItems(); }
int render(DisplayDriver& display) override {
buildItems();
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("GPIO");
bool any_live = false;
drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
const Row& it = _items[row];
drawRowSelection(display, y, sel, reserve);
display.setCursor(2, y);
char val[16];
if (it.kind == ROW_MODE) {
char lbl[8];
snprintf(lbl, sizeof(lbl), "GPIO%d", it.pin + 1);
display.print(lbl);
modeRowValue(it.pin, val, sizeof(val));
uint8_t m = gpioModeOf(it.pin);
if (m == 1 || m == 4) any_live = true; // Input / Analog need a live-refreshed reading
} else {
display.print(" State"); // indented: reads as GPIOn's sub-row
strncpy(val, gpioModeOf(it.pin) == 3 ? "ON" : "OFF", sizeof(val));
}
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
display.setColor(DisplayDriver::LIGHT);
});
return any_live ? 500 : 2000; // faster refresh only while a row needs a live reading
}
bool handleInput(char c) override {
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : _item_count - 1; return true; }
if (c == KEY_DOWN) { _sel = (_sel < _item_count - 1) ? _sel + 1 : 0; return true; }
if (!_prefs) return false;
if (c == KEY_ENTER || keyIsNext(c) || keyIsPrev(c)) {
const Row& it = _items[_sel];
if (it.kind == ROW_MODE) cycleMode(it.pin);
else toggleState(it.pin);
return true;
}
return false;
}
};
#endif // PIN_GPIO1