mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
- 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>
154 lines
5.6 KiB
C++
154 lines
5.6 KiB
C++
#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
|