This commit is contained in:
Michael A. Cojocari committed 2026-08-27 20:37:59 -04:00
1 parent 799756cd94
commit fe6741b26d
5 files changed
+236 -21

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
| LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs |
| Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported |
| LilyGo T-Display P4 | ESP32-P4 + ESP32-C6, SX1262 | AMOLED or TFT-LCD, touch | Web flasher (AMOLED) or the .bin for the LCD SKU | Beta (new in beta_45) | Two screen SKUs; fuel-gauge battery reporting, full TX power |
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported |
| Attaky Mesh Series | ESP32-S3, SX1262 | Touch display + front D-pad | Web flasher | Beta (new in beta_47) | Community port by attakygit (#158/#169); detachable keyboard supported; the front D-pad + SELECT navigate the UI |
## Feature notes per board
+121 -2
View File
@@ -1917,6 +1917,18 @@ static bool tbFingerTouchOnTabBarBlocked(uint16_t y) {
static bool s_kbd_nav = true;
#endif
#if defined(ATTAKY_MESH_SERIES)
// Attaky Mesh Deck: touch is the primary input, and the front D-pad + SELECT are
// a second one — so this takes the same CAP_KEYPAD_NAV "secondary indev" path as
// the M9 block below, fed by attakyNavPump() draining the @0x59 expander queue.
// The three symbols are the ones that path needs (see the M9's note); nav is
// always on because the buttons are soldered to the board — there is nothing to
// toggle off, and with no press the group just sits unfocused and invisible.
static bool s_kbd_nav = true;
static bool s_tb_nav = false; // no trackball — read by the shared nav-rebuild gate, never set
static lv_indev_drv_t s_nav_keypad_drv;
#endif
#if defined(HAS_THINKNODE_M9)
// Keyboard-only device (no touch, no trackball): nav is always on, same as Tanmatsu above.
// Unlike Tanmatsu (which registers its KEYPAD indev as the PRIMARY one, driven by navPump()
@@ -36910,11 +36922,14 @@ static bool popupRegistryBlocksSwipe();
// True if any popup/modal is currently up (rows flagged PF_COUNT).
static bool anyPopupOpen() { return popupRegistryAny(); }
#if CAP_KEYBOARD
#if CAP_KEYBOARD || CAP_KEYPAD_NAV
// Close the topmost popup/modal (registry order = front-to-back priority),
// like tapping its X / close button. Returns true if one was dismissed.
// Widened past CAP_KEYBOARD for the Attaky: navGoToMainTab() (inside the
// CAP_KEYPAD_NAV block) calls this, and that board navigates with a D-pad but
// has no physical keyboard, so the old gate left the declaration undefined.
static bool hwKeyDismissTopPopup() { return popupRegistryDismissTop(); }
#endif // HAS_TDECK_KEYBOARD || HAS_TANMATSU (hwKeyDismissTopPopup)
#endif // CAP_KEYBOARD || CAP_KEYPAD_NAV (hwKeyDismissTopPopup)
#if defined(HAS_TDECK_KEYBOARD) || defined(HAS_PAGER_KEYBOARD) || defined(HAS_M9_KEYBOARD)
// Keys that act as "close the popup" when no text field is focused.
@@ -38965,6 +38980,89 @@ if (g_lv.task && g_lv.task->isManualLock()) {
}
#endif
#if defined(ATTAKY_MESH_SERIES)
// ---- Attaky front D-pad -> focus navigation ---------------------------------
// The five nav buttons on the @0x59 expander (P00 down, P01 left, P02 select,
// P03 right, P04 up) drive the SAME focus group the Tanmatsu arrows and the
// T-Deck trackball use, so they reach every screen with no per-screen handling.
// Touch stays this board's primary input and the D-pad is a second one — which
// is why the focus ring starts hidden (s_nav_show is false until a press) and
// nothing here is toggleable: the buttons are soldered on, so "off" would just
// make them dead.
//
// This board compiles no handleHwKey() (its keyboard module types straight into
// the focused textarea), so the nav routing the T-Deck and M9 do from there
// lives here instead. Deliberately narrower than the M9's: that board has no
// touch, so it also needs d-pad paths for Back, Home, the chat-bubble action
// menu and map panning — all of which are still a tap away here.
static void attakyNavPump() {
// The field currently bound to the keyboard, and whether the D-pad should be
// editing it: focus has to be really ON it (a tap sets s_nav_ta_editing), so
// arrows moving through a page don't get swallowed as caret moves.
lv_obj_t* const ta_focused = g_lv.keyboard ? lv_keyboard_get_textarea(g_lv.keyboard) : nullptr;
lv_obj_t* const ta = (ta_focused && s_nav_ta_editing) ? ta_focused : nullptr;
for (int i = 0; i < 8; ++i) {
const int ev = attakyNavKeyRead();
if (ev == ATTAKY_NAV_NONE) break;
switch (ev) {
case ATTAKY_NAV_UP:
case ATTAKY_NAV_DOWN: {
const bool up = (ev == ATTAKY_NAV_UP);
// The @-mention picker owns up/down while it shows: its cells carry
// NAV_SKIP_FLAG (they're tap targets on touch boards), so the focus
// group cannot reach them at all.
if (mentionNavActive()) { mentionNavMove(up ? -1 : +1); break; }
// An open dropdown LIST owns them too — navMoveDir would move focus
// off the dropdown, which closes it. The FIFO'd LV_KEY_UP/DOWN reach
// the dropdown's own handler, which moves the highlighted option.
if (navOpenDropdown()) { navPushTap(up ? LV_KEY_UP : LV_KEY_DOWN); break; }
lv_obj_t* const was = s_nav_group ? lv_group_get_focused(s_nav_group) : nullptr;
navMoveDir(up ? NAV_UP : NAV_DOWN);
// Focus had nowhere to go: page-scroll instead. A display-only Lua app
// scrolls its own body; everything else falls to navScrollFocused, so
// pages with few or no focusable rows still move under the D-pad.
if (s_nav_group && lv_group_get_focused(s_nav_group) == was) {
if (!(luaAppIsOpen() && luaAppScroll(up))) navScrollFocused(up);
}
break;
}
case ATTAKY_NAV_LEFT:
case ATTAKY_NAV_RIGHT: {
const bool left = (ev == ATTAKY_NAV_LEFT);
if (mentionNavActive() || navOpenDropdown()) break; // vertical-only: swallow
if (ta) {
// Caret already at the edge: fall through to a focus move, so the
// pad always eventually LEAVES the field instead of no-oping.
const uint32_t p = lv_textarea_get_cursor_pos(ta);
if (left) lv_textarea_cursor_left(ta); else lv_textarea_cursor_right(ta);
if (lv_textarea_get_cursor_pos(ta) == p) navMoveDir(left ? NAV_LEFT : NAV_RIGHT);
}
else if (navOnTabBar()) navSwitchTab(left ? -1 : +1);
else navMoveDir(left ? NAV_LEFT : NAV_RIGHT);
break;
}
case ATTAKY_NAV_SELECT:
if (mentionNavActive()) { mentionNavConfirm(); break; }
if (navOnTabBar()) { navSwitchTab(+1); break; }
// First SELECT on a focused field starts typing (the caret appears and
// left/right walk it); the next one activates whatever is focused.
if (ta_focused && !ta && s_nav_group && lv_group_get_focused(s_nav_group) == ta_focused) {
s_nav_ta_editing = true;
break;
}
navMarkEntered(s_nav_group ? lv_group_get_focused(s_nav_group) : nullptr);
navPushTap(LV_KEY_ENTER);
break;
default: break;
}
s_nav_show = true;
if (g_lv.task) g_lv.task->noteUserInput();
}
}
#endif
static void refreshLiveDiag(unsigned long now) {
if (!s_live_diag_label) return;
if (now < s_live_diag_next_ms) return;
@@ -52004,6 +52102,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
#if defined(ESP32)
#if defined(HAS_TANMATSU) || defined(HAS_THINKNODE_M9)
s_kbd_nav = true; // keyboard-only device: nav is always on (no touch to fall back to)
#elif defined(ATTAKY_MESH_SERIES)
// Soldered-on D-pad, and no settings row toggles it (the "Keyboard navigation"
// switch is CAP_TRACKBALL-only) — reading the pref here would let a value left
// over from another board leave the buttons dead with no way to turn them back on.
s_kbd_nav = true;
#else
s_kbd_nav = touchPrefsGetKbdNav();
#endif
@@ -54980,6 +55083,22 @@ void UITask::loop() {
if (_screen_off) wakeScreen();
else sleepScreen();
}
// Front D-pad (P00..P04) -> focus navigation. Two things have to happen before
// the drain: with the screen dark a press only WAKES (and is swallowed, so a
// wake never also activates whatever the ring happened to be on — the pager's
// encoder-wake bug), and in remote mode the panel is a placeholder the browser
// drives. Otherwise sync the focus group to the current tree first: Enter's
// tree mutations only land inside lv_timer_handler at the END of a tick, so a
// drain against a stale mirror navigates the screen behind a just-opened modal
// (the same fix the M9's branch above carries).
if (_screen_off || s_remote_mode) {
const bool any = (attakyNavKeyRead() != ATTAKY_NAV_NONE);
attakyNavKeysDiscard();
if (any && _screen_off) wakeScreen();
} else {
navMaybeRebuild();
attakyNavPump();
}
#endif
#if defined(HAS_ATTAKY_MESH_KEYBOARD)
{
+10 -7
View File
@@ -163,13 +163,16 @@
// Focus-group D-pad navigation (no pointer): Tanmatsu keypad, T-Deck trackball,
// the pager (no touch at all — the rotary encoder is its only nav input, so
// like Tanmatsu this is always-on, not an optional toggle like the T-Deck's),
// or the ThinkNode M9's d-pad. The underlying machinery (navFifo, navMoveDir,
// the focus group, the secondary KEYPAD indev) is generic — only the *pump*
// that feeds it differs per board: Tanmatsu's navPump() reads bsp-input events;
// T-Deck's WASDZ-letter nav and the M9's raw d-pad bytes are both fed straight
// from handleHwKey() instead (see UITask.cpp's `#elif defined(HAS_M9_KEYBOARD)`
// block, parallel to the T-Deck's `#if CAP_TRACKBALL` block).
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_TRACKBALL) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
// the ThinkNode M9's d-pad, or the Attaky's front D-pad + SELECT. The underlying
// machinery (navFifo, navMoveDir, the focus group, the secondary KEYPAD indev)
// is generic — only the *pump* that feeds it differs per board: Tanmatsu's
// navPump() reads bsp-input events; T-Deck's WASDZ-letter nav and the M9's raw
// d-pad bytes are both fed straight from handleHwKey() instead (see UITask.cpp's
// `#elif defined(HAS_M9_KEYBOARD)` block, parallel to the T-Deck's `#if
// CAP_TRACKBALL` block); the Attaky drains its expander queue in attakyNavPump().
// NOTE: the Attaky is the first board here with CAP_KEYBOARD == 0, so anything
// this flag pulls in must not assume a physical keyboard is also compiled.
#if defined(HAS_TANMATSU) || defined(HAS_TDECK_TRACKBALL) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(ATTAKY_MESH_SERIES)
#define CAP_KEYPAD_NAV 1
#else
#define CAP_KEYPAD_NAV 0
@@ -20,15 +20,50 @@ static const uint8_t POWER_BIT = (1u << 7); // P07 = POWER_BTN
static const uint8_t DEBOUNCE_POLLS = 2;
static const uint32_t POLL_INTERVAL_MS = 30;
// Auto-repeat for the four directions (never SELECT — one press must stay one
// activation). Without it, walking a long contact list means one press per row.
static const uint32_t REPEAT_DELAY_MS = 450;
static const uint32_t REPEAT_RATE_MS = 130;
// P0 bit -> nav event. 0 = not a nav button (L1/R2 are read but unbound, POWER
// has its own edge flag below).
static const uint8_t BIT_NAV[8] = {
ATTAKY_NAV_DOWN, // P00
ATTAKY_NAV_LEFT, // P01
ATTAKY_NAV_SELECT, // P02
ATTAKY_NAV_RIGHT, // P03
ATTAKY_NAV_UP, // P04
ATTAKY_NAV_NONE, // P05 BTN_L1 — unbound
ATTAKY_NAV_NONE, // P06 BTN_R2 — unbound
ATTAKY_NAV_NONE, // P07 POWER_BTN — attakyPowerKeyPressed()
};
static bool s_inited = false;
static bool s_present = false;
static uint32_t s_next_ms = 0;
static bool s_power_down = false; // debounced state
static uint8_t s_pending_n = 0; // consecutive polls agreeing with s_pending_down
static bool s_pending_down = false;
// Debounced whole-port state, active-low (a 0 bit = that button is down).
static uint8_t s_stable = 0xFF;
static uint8_t s_pending = 0xFF; // candidate awaiting DEBOUNCE_POLLS agreement
static uint8_t s_pending_n = 0;
static volatile bool s_power_event = false;
// Nav press queue. Small ring: the poll runs at ~30 ms and the UI drains every
// tick, so it only has to absorb a burst, never buffer.
static const uint8_t NAV_Q_LEN = 8;
static uint8_t s_navq[NAV_Q_LEN];
static uint8_t s_navq_head = 0, s_navq_tail = 0;
static int8_t s_repeat_bit = -1; // direction currently held, or -1
static uint32_t s_repeat_ms = 0; // when it next repeats
static void navPush(uint8_t ev) {
const uint8_t next = (uint8_t)((s_navq_head + 1) % NAV_Q_LEN);
if (next == s_navq_tail) return; // full — drop the newest rather than stall
s_navq[s_navq_head] = ev;
s_navq_head = next;
}
static bool awWrite(uint8_t reg, uint8_t val) {
Wire.beginTransmission(KEYS_ADDR);
Wire.write(reg);
@@ -75,14 +110,37 @@ void attakyKeysPoll() {
}
if (!ok) return; // bus busy or read failed — hold the previous state
const bool down = (p0 & POWER_BIT) == 0; // active low
// Every button reading pressed at once is a bad read, not a hand: acting on
// it would inject a burst of nav keys AND a panel toggle from one glitch.
// Resync the debouncer instead of treating the recovery as presses.
if (p0 == 0x00) { s_pending = s_stable; s_pending_n = 0; return; }
if (down != s_pending_down) { s_pending_down = down; s_pending_n = 1; return; }
if (p0 != s_pending) { s_pending = p0; s_pending_n = 1; return; }
if (s_pending_n < DEBOUNCE_POLLS) { s_pending_n++; return; }
if (down != s_power_down) {
s_power_down = down;
if (down) s_power_event = true; // fire on the press edge
const uint8_t changed = (uint8_t)(s_stable ^ p0);
if (changed) {
const uint8_t went_down = (uint8_t)(changed & ~p0); // active low: 1 -> 0
s_stable = p0;
if (went_down & POWER_BIT) s_power_event = true; // fire on the press edge
for (uint8_t b = 0; b < 8; b++) {
if (!(went_down & (1u << b)) || BIT_NAV[b] == ATTAKY_NAV_NONE) continue;
navPush(BIT_NAV[b]);
// Only the directions repeat, and only the most recent one: rolling a
// thumb across the pad should follow the finger, not fire two axes.
if (BIT_NAV[b] != ATTAKY_NAV_SELECT) {
s_repeat_bit = (int8_t)b;
s_repeat_ms = now + REPEAT_DELAY_MS;
}
}
// The repeating direction was released (or another button's edge arrived
// while it was already up) — stop repeating.
if (s_repeat_bit >= 0 && (s_stable & (1u << (uint8_t)s_repeat_bit))) s_repeat_bit = -1;
}
if (s_repeat_bit >= 0 && (int32_t)(now - s_repeat_ms) >= 0) {
navPush(BIT_NAV[(uint8_t)s_repeat_bit]);
s_repeat_ms = now + REPEAT_RATE_MS;
}
}
@@ -92,12 +150,26 @@ bool attakyPowerKeyPressed() {
return true;
}
int attakyNavKeyRead() {
if (s_navq_tail == s_navq_head) return ATTAKY_NAV_NONE;
const uint8_t ev = s_navq[s_navq_tail];
s_navq_tail = (uint8_t)((s_navq_tail + 1) % NAV_Q_LEN);
return (int)ev;
}
void attakyNavKeysDiscard() {
s_navq_tail = s_navq_head;
s_repeat_bit = -1;
}
bool attakyKeysPresent() { return s_present; }
#else
void attakyKeysPoll() {}
bool attakyPowerKeyPressed() { return false; }
int attakyNavKeyRead() { return ATTAKY_NAV_NONE; }
void attakyNavKeysDiscard() {}
bool attakyKeysPresent() { return false; }
#endif
@@ -3,9 +3,24 @@
#include <stdbool.h>
// Front buttons on the board's AW9523 @0x59 (P00-P07, active-low). Only
// POWER_BTN (P07) is used: short-press toggles the panel. A long press is the
// board's hardware power-cut, outside firmware control.
// Front buttons on the board's AW9523 @0x59 (P00-P07, active-low), per Attaky's
// hardware catalog:
// P00 DOWN P01 LEFT P02 SELECT P03 RIGHT P04 UP
// P05 BTN_L1 (top-left shoulder) P06 BTN_R2 (top-right shoulder)
// P07 POWER_BTN
// POWER_BTN short-press toggles the panel (a long press is the board's hardware
// power-cut, outside firmware control). The D-pad + SELECT feed the UI's focus
// navigation. L1/R2 are read but deliberately unbound — nothing claims them yet.
/** D-pad events drained by attakyNavKeyRead(). */
enum AttakyNavKey {
ATTAKY_NAV_NONE = 0,
ATTAKY_NAV_UP,
ATTAKY_NAV_DOWN,
ATTAKY_NAV_LEFT,
ATTAKY_NAV_RIGHT,
ATTAKY_NAV_SELECT,
};
/** Poll the expander. Call once per UI tick; rate-limits its own I2C traffic. */
void attakyKeysPoll();
@@ -13,5 +28,11 @@ void attakyKeysPoll();
/** True once per POWER_BTN press edge (consumes the event). */
bool attakyPowerKeyPressed();
/** Next queued D-pad event, or ATTAKY_NAV_NONE when the queue is empty. */
int attakyNavKeyRead();
/** Drop every queued D-pad event (screen-off wake, remote mode). */
void attakyNavKeysDiscard();
/** Whether the @0x59 expander answered at init (false = keys unavailable). */
bool attakyKeysPresent();