Files
MeshCore-Solo/examples/companion_radio/ui-new/GpioScreen.h
T
Jakub 6d1e71cd3f fix(ui): one rule for value rows, one meaning for Hold Enter
Three interaction inconsistencies found while auditing the favourites work,
all of the same shape: the same gesture meaning different things depending
on which screen you were on.

Value rows in popup menus. Rows like "Notif: ON" or "Sort: Dist" show a
value the user steps through with LEFT/RIGHT, but Enter treated them as
ordinary menu picks and closed the popup, so changing two of them meant
reopening the menu in between. Trail's settings submenu was the lone
exception, working around it by rebuilding and re-selecting after each
Enter. PopupMenu now knows the difference: addValueItem() marks a row, and
Enter on it returns the new VALUE_NEXT instead of SELECTED, leaving the
menu open. Only Back closes a menu now. Applied to the Messages
contact/room/channel menus, Nodes, the Ringtone editor and Trail, which
drops its reopenSettingsAt() workaround. The LEFT/RIGHT cycling bodies
moved into one helper per menu, since Enter and RIGHT now share them.

Nodes' Fav row was the worst case: LEFT/RIGHT did nothing there at all, so
the only way to toggle a favourite was an Enter that dismissed the menu on
every flip. Its label moved to a member buffer (as the Pin row already had)
so it can be retitled in place.

Settings rows Auto-off, Low battery, GPS pwr and Battery ignored Enter,
though their options wrap exactly like the melody/keyboard/clock rows
beside them, where Enter has always stepped forward. They accept it now.
Rows that ramp between fixed ends (Brightness, Volume, TX Pwr, Timezone,
SF/BW/CR) stay LEFT/RIGHT-only -- there is nothing to wrap to.

Hold Enter no longer doubles as Back. It quietly meant "go back" on Tools,
Locator, Live Share, Repeater, Bot, Auto-Advert, GPIO, Compass, the
Dashboard config and the Messages navigate view, while elsewhere the same
long press opens a context menu. It now only ever opens a menu, or does
nothing where there is none. Same for dismissing an open popup, which it
used to do. Checked that this strands nobody: every board that can reach
these screens has a real Back key (back_btn on joystick boards, Esc on
CardKB/TCA8418/T-Deck). Single-button boards produce no KEY_ENTER at all,
so they never leave the home pages in the first place.
2026-08-31 15:43:10 +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) { _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