mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-23 23:44:30 +00:00
Work
This commit is contained in:
1 parent
799756cd94
commit
fe6741b26d
5 files changed
+236
-21
No files matched your search
+1
-1
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
Reference in new issue
Block a user