mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-23 08:32:28 +00:00
Merge PR #320
This commit is contained in:
commit
9fb65f91c4
13 files changed
+850
-154
No files matched your search
@@ -23,6 +23,10 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
- **T-Deck**: the everything device: touch, physical keyboard, trackball cursor
|
||||
or d-pad navigation, microSD (deep 5000-message chat history, map tile packs,
|
||||
data storage), GPS on the Plus, notification sounds through the I2S speaker.
|
||||
Tap Sym or Alt for one symbol, or double-tap either to lock the symbol layer;
|
||||
this needs [LilyGO keyboard-controller firmware with raw matrix mode](https://github.com/Xinyuan-LilyGO/T-Deck/tree/master/examples/Keyboard_ESP32C3)
|
||||
(June 2025 or newer). Older controller firmware keeps normal typing and
|
||||
reports the unavailable latch mode on Serial.
|
||||
- **Heltec V4 + TFT**: touch UI with the on-screen keyboard; the optional
|
||||
Expansion Kit adds environment sensors (home-screen chart) and a piezo
|
||||
buzzer. V4.3 boards get the switchable high-gain receive LNA toggle.
|
||||
@@ -33,6 +37,8 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
- **T-Lora Pager**: no touchscreen at all — the QWERTY keyboard and the rotary
|
||||
encoder drive everything (Alt+turn free-scrolls a page; see
|
||||
[TLORA_PAGER_SHORTCUTS.md](TLORA_PAGER_SHORTCUTS.md) for the full key map).
|
||||
Tap Fn/Alt for one symbol or double-tap it to lock the symbol layer; physical
|
||||
hold combinations keep their existing behavior.
|
||||
GPS, microSD, keyboard backlight, lock screen and notification sound through
|
||||
the onboard codec and amp all work. Two builds, one per radio: LR1121 and
|
||||
SX1262 — flashing the wrong one leaves you with no radio, so check the label
|
||||
|
||||
+6
-6
@@ -225,8 +225,8 @@ WASD-panning).
|
||||
`updatePagerAltShiftChord()` toggles Caps Lock ONLY while a text field is
|
||||
actually being edited (same "ta" derivation `handleHwKey()`'s TLORA_PAGER
|
||||
branch already uses to tell a bound-but-unfocused field apart from one
|
||||
actually being edited), and jumps straight to Home (`navGoToMainTab`)
|
||||
everywhere else.
|
||||
actually being edited), and is a no-op everywhere else. Alt+Backspace is
|
||||
the context-independent Home shortcut.
|
||||
3. **@-mention contact picker made encoder-navigable** (commit `57802eb`) —
|
||||
the same unreachable-without-touch problem the accent box had, fixed the
|
||||
same way (`mentionNavRestyle()`/`mentionNavConfirm()` mirror
|
||||
@@ -622,9 +622,9 @@ existing boards. Notes:
|
||||
`pagerKeyboardConsumeAltShiftChord()`; `UITask.cpp`'s
|
||||
`updatePagerAltShiftChord()` toggles persistent Caps Lock ONLY while a text
|
||||
field is actually being edited (same "ta" derivation as `handleHwKey()`'s
|
||||
TLORA_PAGER branch), and jumps straight Home (`navGoToMainTab`) everywhere
|
||||
else — toggling Caps Lock with no field to see it change in was reported as
|
||||
surprising/purposeless outside of typing.
|
||||
TLORA_PAGER branch), and is a no-op everywhere else — toggling Caps Lock
|
||||
with no field to see it change in was reported as surprising/purposeless
|
||||
outside of typing. Alt+Backspace owns the Home shortcut.
|
||||
See `PagerKeyboard.cpp`'s matrix-legend comment for the one documented
|
||||
edge case (Alt+Shift+O or Alt+Shift+L all three held phantom-ghosts a
|
||||
stray 'q'/'a' — a diode-less-matrix limitation, not a bug, and not the
|
||||
@@ -751,7 +751,7 @@ existing boards. Notes:
|
||||
| Accent-variant popup keyboard nav | Fn+Space arms it; encoder walks variants; encoder click/Enter confirms; Backspace cancels (`s_accentnav_active`/`accentNavRestyle()`/`accentNavConfirm()`) | medium | done |
|
||||
| @-mention picker keyboard nav | Auto-arms the instant the list appears (no Fn+Space needed); same encoder walk/confirm/cancel as the accent popup (`s_mentionnav_active`/`mentionNavRestyle()`/`mentionNavConfirm()`) | medium | done |
|
||||
| Encoder short click on a focused bubble | Mirrors keyboard Enter — opens the same action menu instead of a plain ENTER a bubble doesn't react to | small | done |
|
||||
| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; jumps to Home (`navGoToMainTab`) everywhere else — `PagerKeyboard.cpp` only reports the chord, `UITask.cpp` decides the effect | small | done |
|
||||
| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; no-op everywhere else. Fn+Backspace jumps Home — `PagerKeyboard.cpp` only reports the chords, `UITask.cpp` decides the effects | small | done |
|
||||
| Map pan (WASD) + slider nudge (Q/E) | WASD pans the Map tab (`mapNudge()`, shared with Tanmatsu's Ctrl+Arrow); Q/E adjust a focused slider (moved off D/F to avoid colliding with WASD) | small | done |
|
||||
| App-drawer tile focus highlight | `NAV_ACCENTFOCUS_FLAG` makes `navFocusCb` use the tile's own accent tint instead of the generic white reverse-fill, on every board | small | done |
|
||||
| SX1262-variant env | `tlora_pager_sx1262_companion_radio_touch` — same board/pins, `CustomSX1262`/`CustomSX1262Wrapper` instead of the LR1121 classes | medium | done (worklist ⑩) |
|
||||
|
||||
+15
-22
@@ -40,7 +40,7 @@ while editing a text field, where the keys type normally.
|
||||
|
||||
| Gesture | Action | Keyboard equivalent |
|
||||
|---|---|---|
|
||||
| Turn | Move focus to the next/previous item on screen | **Fn (Alt) tapped alone** moves forward one step (NEXT only — no keyboard way to go backward) |
|
||||
| Turn | Move focus to the next/previous item on screen | none — encoder only |
|
||||
| Short click | Select / confirm the focused item | **Enter** |
|
||||
| Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** |
|
||||
| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly |
|
||||
@@ -93,29 +93,19 @@ text box → Send**, in that order, in both directions — nothing is skipped.
|
||||
Turning past the △ chip (toward the messages) always jumps straight to the
|
||||
newest message, regardless of channel or DM.
|
||||
|
||||
### Backspace and Fn (Alt) — leaving vs. catching up
|
||||
### Backspace — catching up
|
||||
|
||||
These two keys are your shortcuts in and out of "reading mode" on this
|
||||
screen, and they do different things depending on whether you've got unread
|
||||
messages:
|
||||
**Backspace (tap)** jumps to whichever message needs your attention:
|
||||
|
||||
- **Fn (Alt) tapped alone** — jumps straight to the **newest** message and
|
||||
moves focus into the text box, ready to type a reply. This is the
|
||||
deliberate "I'm done reading, let me respond" gesture — it works regardless
|
||||
of where your focus currently is in the chat.
|
||||
- **Backspace (tap)** — jumps to whichever message needs your attention:
|
||||
- If there are unread messages, it jumps to the first one — right below the
|
||||
**"NEW ----"** divider — and selects it, so you can then turn forward
|
||||
through your unread messages in order, oldest-of-the-unread first.
|
||||
- If nothing is unread, it jumps to the newest message instead (same
|
||||
destination as Fn+Alt, but leaves focus on the message itself rather than
|
||||
the text box).
|
||||
- If there are unread messages, it jumps to the first one — right below the
|
||||
**"NEW ----"** divider — and selects it, so you can then turn forward
|
||||
through your unread messages in order, oldest-of-the-unread first.
|
||||
- If nothing is unread, it jumps to the newest message instead (same
|
||||
destination, with focus left on the message).
|
||||
|
||||
Backspace's override above only applies while you're inside an open chat and
|
||||
*not* actively editing the text box — the usual rule from the top of this
|
||||
guide; with a field focused, Backspace deletes a character as normal.
|
||||
Fn (Alt) tap's override fires the same way whether or not the text box
|
||||
already has focus, since landing there is the whole point of the gesture.
|
||||
|
||||
## Keyboard layout
|
||||
|
||||
@@ -128,7 +118,9 @@ a s d f g h j k l [Enter]
|
||||
[Space]
|
||||
```
|
||||
|
||||
Hold **Fn (Alt)** for numbers/symbols instead:
|
||||
Hold **Fn (Alt)** for numbers/symbols, or tap it once to apply this layer to
|
||||
the next key only. Double-tap Fn to lock the symbol layer; tap it again to
|
||||
return to letters.
|
||||
|
||||
```
|
||||
1 2 3 4 5 6 7 8 9 0
|
||||
@@ -162,7 +154,7 @@ Hold **Fn (Alt)** for numbers/symbols instead:
|
||||
| **Space** (tap) | Types a space | — |
|
||||
| **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — |
|
||||
| **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) |
|
||||
| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder. In an open chat: jumps to the newest message and focuses the text box instead — see [Chat screen](#chat-screen) |
|
||||
| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same |
|
||||
|
||||
The **BOOT** button (top of the device) instantly wakes the screen from
|
||||
idle-dim. It does *not* unlock a screen you've manually locked with the
|
||||
@@ -249,13 +241,14 @@ that changes keyboard language.
|
||||
| Input | Action |
|
||||
|---|---|
|
||||
| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings |
|
||||
| Turn encoder (or Fn tap = NEXT only) | Move focus |
|
||||
| Turn encoder | Move focus |
|
||||
| Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message |
|
||||
| Click encoder (or Enter) | Select / confirm |
|
||||
| Hold encoder ~1s (or hold Backspace ~1s) | Back |
|
||||
| Fn + turn (main tab) | Switch tabs |
|
||||
| Fn + turn (page/chat) | Scroll |
|
||||
| Fn tap alone | Next field (in a chat: jump to newest message + focus the text box) |
|
||||
| Fn tap alone | Use the symbol layer for the next key |
|
||||
| Fn double-tap | Lock the symbol layer; tap Fn again to unlock |
|
||||
| Hold Shift + letter | Momentary uppercase |
|
||||
| Fn + Shift (editing a field) | Toggle Caps Lock |
|
||||
| Fn + Shift (not editing a field) | Nothing |
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class LatchedModifier {
|
||||
public:
|
||||
static constexpr uint32_t DOUBLE_TAP_MS = 400;
|
||||
|
||||
void press() {
|
||||
held_ = true;
|
||||
used_while_held_ = false;
|
||||
}
|
||||
|
||||
void release(uint32_t now_ms) {
|
||||
held_ = false;
|
||||
if (discard_release_) {
|
||||
discard_release_ = false;
|
||||
used_while_held_ = false;
|
||||
return;
|
||||
}
|
||||
if (used_while_held_) return;
|
||||
tap(now_ms);
|
||||
}
|
||||
|
||||
void tap(uint32_t now_ms) {
|
||||
if (locked_) {
|
||||
clearLatched();
|
||||
return;
|
||||
}
|
||||
if (one_shot_ && (uint32_t)(now_ms - last_tap_ms_) <= DOUBLE_TAP_MS) {
|
||||
one_shot_ = false;
|
||||
locked_ = true;
|
||||
last_tap_ms_ = 0;
|
||||
return;
|
||||
}
|
||||
one_shot_ = true;
|
||||
last_tap_ms_ = now_ms;
|
||||
}
|
||||
|
||||
bool consumeForKey() {
|
||||
const bool active_now = held_ || one_shot_ || locked_;
|
||||
if (held_) used_while_held_ = true;
|
||||
if (one_shot_) {
|
||||
one_shot_ = false;
|
||||
last_tap_ms_ = 0;
|
||||
}
|
||||
return active_now;
|
||||
}
|
||||
|
||||
void markHeldUsed() {
|
||||
if (!held_) return;
|
||||
used_while_held_ = true;
|
||||
one_shot_ = false;
|
||||
last_tap_ms_ = 0;
|
||||
}
|
||||
|
||||
void clearLatched() {
|
||||
one_shot_ = false;
|
||||
locked_ = false;
|
||||
last_tap_ms_ = 0;
|
||||
}
|
||||
|
||||
void discard() {
|
||||
clearLatched();
|
||||
if (held_) {
|
||||
used_while_held_ = true;
|
||||
discard_release_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void baseline(bool physically_held) {
|
||||
clearLatched();
|
||||
held_ = physically_held;
|
||||
used_while_held_ = physically_held;
|
||||
discard_release_ = physically_held;
|
||||
}
|
||||
|
||||
bool held() const { return held_; }
|
||||
bool latched() const { return one_shot_ || locked_; }
|
||||
bool active() const { return held_ || one_shot_ || locked_; }
|
||||
bool locked() const { return locked_; }
|
||||
|
||||
private:
|
||||
bool held_ = false;
|
||||
bool used_while_held_ = false;
|
||||
bool one_shot_ = false;
|
||||
bool locked_ = false;
|
||||
bool discard_release_ = false;
|
||||
uint32_t last_tap_ms_ = 0;
|
||||
};
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <Adafruit_TCA8418.h>
|
||||
#include <cctype>
|
||||
#include "PagerKeyboardState.h"
|
||||
|
||||
#ifndef KB_INT
|
||||
#define KB_INT 6
|
||||
@@ -14,30 +14,10 @@
|
||||
#ifndef KB_BACKLIGHT
|
||||
#define KB_BACKLIGHT 46
|
||||
#endif
|
||||
#define KB_ROWS 4
|
||||
#define KB_COLS 10
|
||||
|
||||
// Matrix legend, s_keymap[row][col] — same physical keyboard PCB as
|
||||
// trail-mate's working LR1121 pager build, cross-checked there. '\0' = no
|
||||
// character at that position (dead cell, or intercepted as a modifier below).
|
||||
static constexpr char s_keymap[KB_ROWS][KB_COLS] = {
|
||||
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\r'},
|
||||
{'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', '\0'},
|
||||
{' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'},
|
||||
};
|
||||
// Alt layer (symbols/numbers) — this hardware has no separate physical Symbol
|
||||
// key, so Alt alone drives it (matches trail-mate's has_symbol_key=false path).
|
||||
static constexpr char s_symbolMap[KB_ROWS][KB_COLS] = {
|
||||
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'*', '/', '+', '-', '=', ':', '\'', '"', '@', '\0'},
|
||||
{'\0', '_', '$', ';', '?', '!', ',', '.', '\0', '\0'},
|
||||
{' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'},
|
||||
};
|
||||
|
||||
// Modifier/special-key positions: 0-based (row*KB_COLS + col), matching the
|
||||
// TCA8418 raw event's (code & 0x7F) - 1. Alt is a hold (symbol layer while
|
||||
// held); Shift is a hold too (momentary uppercase on the base layer, real
|
||||
// TCA8418 raw event's (code & 0x7F) - 1. Alt selects symbols while held, a
|
||||
// solo tap selects them for one key, and a double tap locks the layer. Shift
|
||||
// is a hold too (momentary uppercase on the base layer, real
|
||||
// Shift-key semantics) — held Alt THEN a Shift press instead chords into
|
||||
// Alt+Shift, reported via s_alt_shift_chord_pending. What that chord DOES is
|
||||
// a UI-level decision (UITask.cpp): Caps Lock toggle while editing a text
|
||||
@@ -55,29 +35,21 @@ static constexpr char s_symbolMap[KB_ROWS][KB_COLS] = {
|
||||
// harmless in practice since the intended gesture is
|
||||
// hold-Alt-tap-Shift-release-both, not holding all three simultaneously.
|
||||
// Backspace (row2,col9) sits one column over from Shift, so Alt+Backspace
|
||||
// doesn't share this exact ghosting risk with any base-layer letter.
|
||||
static constexpr uint8_t kAltPos = 2 * KB_COLS + 0; // row2,col0 ('\0' in both layers)
|
||||
static constexpr uint8_t kShiftPos = 2 * KB_COLS + 8; // row2,col8 ('\0' in both layers)
|
||||
static constexpr uint8_t kBackspacePos = 2 * KB_COLS + 9; // row2,col9 ('\0' in both layers)
|
||||
static constexpr uint8_t kSpacePos = 3 * KB_COLS + 0; // row3,col0 (' ' in both layers)
|
||||
// doesn't share this exact ghosting risk with any base-layer letter. The maps
|
||||
// and positions live in PagerKeyboardState so host tests exercise the exact
|
||||
// production translation logic.
|
||||
|
||||
static Adafruit_TCA8418 s_kb;
|
||||
static bool s_inited = false;
|
||||
static bool s_alt = false;
|
||||
static bool s_alt_used = false; // Alt consumed as a modifier since it was last pressed
|
||||
static bool s_alt_tap_pending = false; // Alt pressed+released with nothing else happening meanwhile
|
||||
static bool s_caps = false;
|
||||
static bool s_shift_held = false; // momentary Shift, mirrors s_backspace_held/s_space_held
|
||||
static bool s_alt_shift_chord_pending = false; // one-shot, see pagerKeyboardConsumeAltShiftChord()
|
||||
static bool s_alt_backspace_chord_pending = false; // one-shot, see pagerKeyboardConsumeAltBackspaceChord()
|
||||
static bool s_backspace_held = false;
|
||||
static bool s_space_held = false;
|
||||
static PagerKeyboardState s_state;
|
||||
|
||||
// Single-producer (poll) / single-consumer (UI thread) ring — same pattern as
|
||||
// TDeckKeyboard.cpp; byte indices are atomic enough for SPSC without a lock.
|
||||
static volatile uint8_t s_ring[16];
|
||||
static volatile uint8_t s_head = 0;
|
||||
static volatile uint8_t s_tail = 0;
|
||||
// Single-producer (poll) / single-consumer ring. Polling currently happens on
|
||||
// the UI task, but the short critical section preserves the header's contract
|
||||
// if a future board moves I2C polling to another core.
|
||||
static portMUX_TYPE s_ring_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static uint8_t s_ring[16];
|
||||
static uint8_t s_head = 0;
|
||||
static uint8_t s_tail = 0;
|
||||
|
||||
static bool s_bl_ready = false;
|
||||
// This framework's Arduino-ESP32 core only has the channel-based LEDC API
|
||||
@@ -88,16 +60,19 @@ static bool s_bl_ready = false;
|
||||
static constexpr uint8_t kKbBacklightPwmChannel = 0;
|
||||
|
||||
static void ringPush(uint8_t c) {
|
||||
portENTER_CRITICAL(&s_ring_mux);
|
||||
const uint8_t nh = (uint8_t)((s_head + 1) & 15);
|
||||
if (nh != s_tail) { // drop if the ring is full
|
||||
s_ring[s_head] = c;
|
||||
s_head = nh;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_ring_mux);
|
||||
}
|
||||
|
||||
void pagerKeyboardBegin() {
|
||||
if (s_inited) return;
|
||||
s_inited = s_kb.begin(TCA8418_DEFAULT_ADDR, &Wire) && s_kb.matrix(KB_ROWS, KB_COLS);
|
||||
s_inited = s_kb.begin(TCA8418_DEFAULT_ADDR, &Wire) &&
|
||||
s_kb.matrix(PagerKeyboardState::ROWS, PagerKeyboardState::COLS);
|
||||
if (!s_inited) return;
|
||||
s_kb.flush();
|
||||
pinMode(KB_INT, INPUT_PULLUP); // TCA8418 INT is open-drain active-low; not ISR-driven here (see .h)
|
||||
@@ -115,58 +90,20 @@ void pagerKeyboardPoll() {
|
||||
const bool pressed = (raw & 0x80) != 0;
|
||||
const uint8_t code = (uint8_t)((raw & 0x7F) - 1);
|
||||
|
||||
if (code == kAltPos) {
|
||||
// Solo tap (press+release, nothing else in between) vs. a modifier hold
|
||||
// (symbol-layer typing, or the rotary encoder's Alt+turn via
|
||||
// pagerKeyboardMarkAltUsed()) — only the former queues a pending tap.
|
||||
if (pressed) { s_alt = true; s_alt_used = false; }
|
||||
else { if (!s_alt_used) s_alt_tap_pending = true; s_alt = false; }
|
||||
continue;
|
||||
}
|
||||
// Any other key event while Alt is held means Alt is being used as a
|
||||
// modifier, not tapped solo — cancels the pending-tap interpretation.
|
||||
if (s_alt && pressed) s_alt_used = true;
|
||||
if (code == kShiftPos) {
|
||||
if (pressed) {
|
||||
if (s_alt) s_alt_shift_chord_pending = true; // Alt (Fn) held + Shift press = chord (UI decides the effect)
|
||||
else s_shift_held = true;
|
||||
} else {
|
||||
s_shift_held = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code == kBackspacePos) {
|
||||
if (pressed) {
|
||||
// Alt+Backspace is a distinct chord (jump Home, everywhere -- see
|
||||
// pagerKeyboardConsumeAltBackspaceChord()), not a delete: suppress
|
||||
// both the '\b' ring-push AND s_backspace_held, so the plain-
|
||||
// Backspace hold gestures (back / unlock) never also see this press.
|
||||
if (s_alt) s_alt_backspace_chord_pending = true;
|
||||
else { s_backspace_held = true; ringPush('\b'); }
|
||||
} else {
|
||||
s_backspace_held = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code == kSpacePos) { s_space_held = pressed; if (pressed) ringPush(' '); continue; }
|
||||
if (!pressed) continue; // base/symbol keys only emit on press
|
||||
|
||||
const uint8_t row = code / KB_COLS;
|
||||
const uint8_t col = code % KB_COLS;
|
||||
if (row >= KB_ROWS) continue; // a GPIO event outside the matrix, not a key
|
||||
|
||||
char c = s_alt ? s_symbolMap[row][col] : s_keymap[row][col];
|
||||
if (c == '\0') continue;
|
||||
// Caps Lock and a held Shift both mean "uppercase," base layer only.
|
||||
if ((s_caps || s_shift_held) && !s_alt) c = (char)toupper((unsigned char)c);
|
||||
ringPush((uint8_t)c);
|
||||
const uint8_t key = s_state.event(code, pressed, millis());
|
||||
if (key) ringPush(key);
|
||||
}
|
||||
}
|
||||
|
||||
int pagerKeyboardReadKey() {
|
||||
if (s_tail == s_head) return 0;
|
||||
portENTER_CRITICAL(&s_ring_mux);
|
||||
if (s_tail == s_head) {
|
||||
portEXIT_CRITICAL(&s_ring_mux);
|
||||
return 0;
|
||||
}
|
||||
const uint8_t c = s_ring[s_tail];
|
||||
s_tail = (uint8_t)((s_tail + 1) & 15);
|
||||
portEXIT_CRITICAL(&s_ring_mux);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -179,11 +116,12 @@ void pagerKeyboardSetBacklight(uint8_t level) {
|
||||
ledcWrite(kKbBacklightPwmChannel, level);
|
||||
}
|
||||
|
||||
bool pagerKeyboardAltHeld() { return s_alt; }
|
||||
bool pagerKeyboardAltHeld() { return s_state.altHeld(); }
|
||||
|
||||
void pagerKeyboardMarkAltUsed() { s_alt_used = true; }
|
||||
void pagerKeyboardMarkAltUsed() { s_state.markAltUsed(); }
|
||||
|
||||
void pagerKeyboardDiscardAlt() {
|
||||
<<<<<<< HEAD
|
||||
s_alt = false;
|
||||
s_alt_used = true;
|
||||
s_alt_tap_pending = false;
|
||||
@@ -195,24 +133,23 @@ bool pagerKeyboardConsumeAltTap() {
|
||||
if (!s_alt_tap_pending) return false;
|
||||
s_alt_tap_pending = false;
|
||||
return true;
|
||||
=======
|
||||
s_state.discardAlt();
|
||||
>>>>>>> pr/320
|
||||
}
|
||||
|
||||
bool pagerKeyboardBackspaceHeld() { return s_backspace_held; }
|
||||
bool pagerKeyboardBackspaceHeld() { return s_state.backspaceHeld(); }
|
||||
|
||||
bool pagerKeyboardSpaceHeld() { return s_space_held; }
|
||||
bool pagerKeyboardSpaceHeld() { return s_state.spaceHeld(); }
|
||||
|
||||
bool pagerKeyboardConsumeAltShiftChord() {
|
||||
if (!s_alt_shift_chord_pending) return false;
|
||||
s_alt_shift_chord_pending = false;
|
||||
return true;
|
||||
return s_state.consumeAltShiftChord();
|
||||
}
|
||||
|
||||
void pagerKeyboardToggleCaps() { s_caps = !s_caps; }
|
||||
void pagerKeyboardToggleCaps() { s_state.toggleCaps(); }
|
||||
|
||||
bool pagerKeyboardConsumeAltBackspaceChord() {
|
||||
if (!s_alt_backspace_chord_pending) return false;
|
||||
s_alt_backspace_chord_pending = false;
|
||||
return true;
|
||||
return s_state.consumeAltBackspaceChord();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// T-LoRa Pager physical QWERTY keyboard: a TCA8418 I2C matrix controller (4
|
||||
// rows x 10 cols, addr 0x34) on the shared I2C bus (SDA 3 / SCL 2). Unlike
|
||||
// the T-Deck's keyboard (a second MCU that resolves ASCII itself before we
|
||||
// ever see a byte), the TCA8418 only reports raw row/col matrix events — the
|
||||
// the T-Deck's keyboard (a second MCU, with raw mode only on newer firmware),
|
||||
// the TCA8418 always reports raw row/col matrix events — the
|
||||
// keymap + shift/sym/alt state machine lives HERE, so pagerKeyboardReadKey()
|
||||
// produces the same final ASCII/control-code stream handleHwKey() already
|
||||
// expects from the T-Deck; no UI-side changes needed to consume it.
|
||||
@@ -13,7 +13,7 @@
|
||||
// must be called from a single, consistent context each tick (whichever task
|
||||
// ends up owning it — wired in a later milestone; this board has no
|
||||
// pre-existing shared-bus task the way the T-Deck's touch poll does).
|
||||
// pagerKeyboardReadKey() only pops from a lock-free ring and is safe to call
|
||||
// pagerKeyboardReadKey() only pops from a critical-section-protected ring and is safe to call
|
||||
// from the UI thread regardless of which context polls.
|
||||
#if defined(HAS_PAGER_KEYBOARD) && defined(ESP32)
|
||||
|
||||
@@ -46,10 +46,10 @@ bool pagerKeyboardAltHeld();
|
||||
|
||||
/** Mark the currently-held Alt as "used as a modifier" — call this when some
|
||||
* other gesture (e.g. the rotary encoder's Alt+turn) consumes the hold, so
|
||||
* releasing Alt afterward isn't also read as a solo tap by
|
||||
* pagerKeyboardConsumeAltTap(). */
|
||||
* releasing Alt afterward does not arm the one-shot symbol layer. */
|
||||
void pagerKeyboardMarkAltUsed();
|
||||
|
||||
<<<<<<< HEAD
|
||||
/** Clear held/tapped Alt and pending Alt chords when keyboard input is
|
||||
* intentionally discarded during a UI mode transition. */
|
||||
void pagerKeyboardDiscardAlt();
|
||||
@@ -59,6 +59,11 @@ void pagerKeyboardDiscardAlt();
|
||||
* pagerKeyboardMarkAltUsed() call) — a "solo tap", distinct from a
|
||||
* symbol-layer or Alt+turn hold. Consumes the pending flag on read. */
|
||||
bool pagerKeyboardConsumeAltTap();
|
||||
=======
|
||||
/** Cancel one-shot/locked Alt, suppress a later release from a held Alt, and
|
||||
* discard pending Alt chords. Used when input is intentionally discarded. */
|
||||
void pagerKeyboardDiscardAlt();
|
||||
>>>>>>> pr/320
|
||||
|
||||
/** True while Backspace is physically held WITHOUT Alt (raw state, mirrors
|
||||
* pagerKeyboardAltHeld()). A plain press still immediately ring-pushes '\b'
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "LatchedModifier.h"
|
||||
|
||||
class PagerKeyboardState {
|
||||
public:
|
||||
static constexpr uint8_t ROWS = 4;
|
||||
static constexpr uint8_t COLS = 10;
|
||||
static constexpr uint8_t ALT_POS = 2 * COLS;
|
||||
static constexpr uint8_t SHIFT_POS = 2 * COLS + 8;
|
||||
static constexpr uint8_t BACKSPACE_POS = 2 * COLS + 9;
|
||||
static constexpr uint8_t SPACE_POS = 3 * COLS;
|
||||
|
||||
uint8_t event(uint8_t code, bool pressed, uint32_t now_ms) {
|
||||
static const char base[ROWS][COLS] = {
|
||||
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
|
||||
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\r'},
|
||||
{'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', '\0'},
|
||||
{' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'},
|
||||
};
|
||||
static const char symbols[ROWS][COLS] = {
|
||||
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'*', '/', '+', '-', '=', ':', '\'', '"', '@', '\0'},
|
||||
{'\0', '_', '$', ';', '?', '!', ',', '.', '\0', '\0'},
|
||||
{' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'},
|
||||
};
|
||||
|
||||
if (code == ALT_POS) {
|
||||
if (pressed) alt_.press();
|
||||
else alt_.release(now_ms);
|
||||
return 0;
|
||||
}
|
||||
if (code == SHIFT_POS) {
|
||||
if (pressed) {
|
||||
if (alt_.held()) {
|
||||
alt_.markHeldUsed();
|
||||
alt_shift_chord_pending_ = true;
|
||||
} else {
|
||||
shift_held_ = true;
|
||||
}
|
||||
} else {
|
||||
shift_held_ = false;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (code == BACKSPACE_POS) {
|
||||
if (pressed) {
|
||||
if (alt_.held()) {
|
||||
alt_.markHeldUsed();
|
||||
alt_backspace_chord_pending_ = true;
|
||||
} else {
|
||||
alt_.consumeForKey();
|
||||
backspace_held_ = true;
|
||||
return '\b';
|
||||
}
|
||||
} else {
|
||||
backspace_held_ = false;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (code == SPACE_POS) {
|
||||
space_held_ = pressed;
|
||||
if (pressed) {
|
||||
alt_.consumeForKey();
|
||||
return ' ';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (!pressed) return 0;
|
||||
|
||||
const uint8_t row = code / COLS;
|
||||
const uint8_t col = code % COLS;
|
||||
if (row >= ROWS) return 0;
|
||||
const bool symbol_layer = alt_.consumeForKey();
|
||||
char key = symbol_layer ? symbols[row][col] : base[row][col];
|
||||
if (key == '\0') return 0;
|
||||
if ((caps_ || shift_held_) && !symbol_layer && key >= 'a' && key <= 'z') key -= 32;
|
||||
return (uint8_t)key;
|
||||
}
|
||||
|
||||
bool altHeld() const { return alt_.held(); }
|
||||
void markAltUsed() { alt_.markHeldUsed(); }
|
||||
void discardAlt() {
|
||||
alt_.discard();
|
||||
alt_shift_chord_pending_ = false;
|
||||
alt_backspace_chord_pending_ = false;
|
||||
}
|
||||
bool backspaceHeld() const { return backspace_held_; }
|
||||
bool spaceHeld() const { return space_held_; }
|
||||
bool consumeAltShiftChord() {
|
||||
const bool pending = alt_shift_chord_pending_;
|
||||
alt_shift_chord_pending_ = false;
|
||||
return pending;
|
||||
}
|
||||
void toggleCaps() { caps_ = !caps_; }
|
||||
bool consumeAltBackspaceChord() {
|
||||
const bool pending = alt_backspace_chord_pending_;
|
||||
alt_backspace_chord_pending_ = false;
|
||||
return pending;
|
||||
}
|
||||
|
||||
private:
|
||||
LatchedModifier alt_;
|
||||
bool caps_ = false;
|
||||
bool shift_held_ = false;
|
||||
bool alt_shift_chord_pending_ = false;
|
||||
bool alt_backspace_chord_pending_ = false;
|
||||
bool backspace_held_ = false;
|
||||
bool space_held_ = false;
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
#if defined(HAS_TDECK_KEYBOARD) && defined(ESP32)
|
||||
|
||||
#include "TDeckKeyboard.h"
|
||||
#include "TDeckKeyboardState.h"
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
@@ -9,19 +10,91 @@
|
||||
#define PIN_KB_ADDR 0x55
|
||||
#endif
|
||||
|
||||
// Single-producer (core-0 poll) / single-consumer (UI thread) ring. Byte indices
|
||||
// on a 32-bit MCU are atomic enough for SPSC without a lock.
|
||||
static volatile uint8_t s_ring[16];
|
||||
static volatile uint8_t s_head = 0; // written by producer
|
||||
static volatile uint8_t s_tail = 0; // written by consumer
|
||||
// Core-0 produces and the UI task consumes. One short cross-core critical
|
||||
// section publishes each byte together with its index and also protects the
|
||||
// desired modifier-input mode below.
|
||||
static portMUX_TYPE s_keyboard_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
static uint8_t s_ring[16];
|
||||
static uint8_t s_head = 0;
|
||||
static uint8_t s_tail = 0;
|
||||
static bool s_inited = false;
|
||||
enum class KeyboardMode : uint8_t { Probe, Raw, Legacy };
|
||||
static KeyboardMode s_mode = KeyboardMode::Probe;
|
||||
static TDeckKeyboardState s_raw_state;
|
||||
static bool s_modifier_input_allowed = true; // guarded by s_keyboard_mux
|
||||
static uint32_t s_modifier_mode_generation = 0; // guarded by s_keyboard_mux
|
||||
static uint32_t s_modifier_generation_applied = 0; // core-0 owner only
|
||||
|
||||
// Backlight: the UI thread requests a level; the actual I2C write happens in the
|
||||
// poll (core 0). The keyboard's C3 firmware sets the backlight on an I2C write.
|
||||
static volatile uint8_t s_bl_desired = 0;
|
||||
static volatile bool s_bl_dirty = false;
|
||||
|
||||
static void ringPushLocked(uint8_t key) {
|
||||
if (!key) return;
|
||||
const uint8_t next = (uint8_t)((s_head + 1) & 15);
|
||||
if (next != s_tail) {
|
||||
s_ring[s_head] = key;
|
||||
s_head = next;
|
||||
}
|
||||
}
|
||||
|
||||
static void keyboardCommand(uint8_t command) {
|
||||
Wire.beginTransmission(PIN_KB_ADDR);
|
||||
Wire.write(command);
|
||||
Wire.endTransmission();
|
||||
}
|
||||
|
||||
static int keyboardRead(uint8_t* out, size_t count) {
|
||||
Wire.requestFrom((int)PIN_KB_ADDR, (int)count);
|
||||
int read = 0;
|
||||
while (Wire.available() && read < (int)count) out[read++] = (uint8_t)Wire.read();
|
||||
while (Wire.available()) Wire.read();
|
||||
return read;
|
||||
}
|
||||
|
||||
static void processRawFrame(const uint8_t frame[TDeckKeyboardState::COLS], uint32_t now_ms) {
|
||||
uint8_t keys[16];
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
const uint32_t generation = s_modifier_mode_generation;
|
||||
if (generation != s_modifier_generation_applied) {
|
||||
// Transition frame: establish current physical state, publish nothing.
|
||||
// The mode setter already cleared older queued bytes under this same lock.
|
||||
s_raw_state.baseline(frame);
|
||||
s_modifier_generation_applied = generation;
|
||||
} else {
|
||||
const size_t key_count = s_raw_state.update(frame, now_ms, keys, sizeof keys,
|
||||
s_modifier_input_allowed);
|
||||
for (size_t i = 0; i < key_count; ++i) ringPushLocked(keys[i]);
|
||||
}
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
|
||||
static void processLegacyKey(uint8_t key) {
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
const uint32_t generation = s_modifier_mode_generation;
|
||||
if (generation != s_modifier_generation_applied) {
|
||||
// No raw modifier state exists, and the C3 exposes one latest-byte mailbox
|
||||
// (comdata/comdata_flag), not one response per host mode transition. Any
|
||||
// number of transitions before this read therefore coalesce deliberately:
|
||||
// drop the sole pending byte once so it cannot cross into the final mode.
|
||||
s_modifier_generation_applied = generation;
|
||||
} else {
|
||||
ringPushLocked(key);
|
||||
}
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
|
||||
void tdeckKeyboardBegin() {
|
||||
s_mode = KeyboardMode::Probe;
|
||||
s_raw_state = TDeckKeyboardState{};
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
s_head = s_tail = 0;
|
||||
// Keep any desired mode the UI published immediately after starting this
|
||||
// task. Setting applied one generation behind forces the first response to
|
||||
// baseline (or drain the legacy controller's one-byte mailbox).
|
||||
s_modifier_generation_applied = s_modifier_mode_generation - 1;
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
s_inited = true; // Wire was configured (18/8, 400k, 20ms timeout) by the touch driver
|
||||
}
|
||||
|
||||
@@ -49,25 +122,75 @@ void tdeckKeyboardFlushBacklight() {
|
||||
void tdeckKeyboardPoll() {
|
||||
if (!s_inited) return;
|
||||
tdeckKeyboardFlushBacklight();
|
||||
if (Wire.requestFrom((int)PIN_KB_ADDR, 1) != 1) return;
|
||||
uint8_t key = Wire.read();
|
||||
if (key == 0) return; // no key this scan
|
||||
const uint8_t nh = (uint8_t)((s_head + 1) & 15);
|
||||
if (nh != s_tail) { // drop if the ring is full
|
||||
s_ring[s_head] = key;
|
||||
s_head = nh;
|
||||
if (s_mode == KeyboardMode::Probe) {
|
||||
// LilyGO keyboard firmware from June 2025 onward accepts 0x03 and returns
|
||||
// five column-state bytes. Older controllers ignore it and keep returning
|
||||
// one resolved ASCII byte; restore 0x04 key mode immediately in that case.
|
||||
keyboardCommand(0x03);
|
||||
uint8_t frame[TDeckKeyboardState::COLS] = {};
|
||||
const int count = keyboardRead(frame, sizeof frame);
|
||||
bool valid_raw = count == (int)sizeof frame;
|
||||
for (size_t col = 0; col < sizeof frame && valid_raw; ++col)
|
||||
valid_raw = (frame[col] & 0x80) == 0;
|
||||
if (valid_raw) {
|
||||
s_mode = KeyboardMode::Raw;
|
||||
Serial.println("[keyboard] T-Deck raw mode: modifier latching enabled");
|
||||
processRawFrame(frame, millis());
|
||||
return;
|
||||
}
|
||||
keyboardCommand(0x04);
|
||||
s_mode = KeyboardMode::Legacy;
|
||||
Serial.println("[keyboard] T-Deck legacy mode: update keyboard C3 firmware for modifier latching");
|
||||
if (count == 1) processLegacyKey(frame[0]);
|
||||
return;
|
||||
}
|
||||
if (s_mode == KeyboardMode::Raw) {
|
||||
uint8_t frame[TDeckKeyboardState::COLS] = {};
|
||||
if (keyboardRead(frame, sizeof frame) != (int)sizeof frame) return;
|
||||
for (size_t col = 0; col < sizeof frame; ++col) if (frame[col] & 0x80) return;
|
||||
processRawFrame(frame, millis());
|
||||
return;
|
||||
}
|
||||
uint8_t key = 0;
|
||||
if (keyboardRead(&key, 1) == 1) processLegacyKey(key);
|
||||
}
|
||||
|
||||
int tdeckKeyboardReadKey() {
|
||||
if (s_tail == s_head) return 0; // empty
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
if (s_tail == s_head) {
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
return 0;
|
||||
}
|
||||
const uint8_t key = s_ring[s_tail];
|
||||
s_tail = (uint8_t)((s_tail + 1) & 15);
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
return key;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
void tdeckKeyboardDiscardModifiers() {}
|
||||
|
||||
void tdeckKeyboardAllowModifiers() {}
|
||||
=======
|
||||
void tdeckKeyboardDiscardModifiers() {
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
if (s_modifier_input_allowed) {
|
||||
s_modifier_input_allowed = false;
|
||||
++s_modifier_mode_generation;
|
||||
s_tail = s_head;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
|
||||
void tdeckKeyboardAllowModifiers() {
|
||||
portENTER_CRITICAL(&s_keyboard_mux);
|
||||
if (!s_modifier_input_allowed) {
|
||||
s_modifier_input_allowed = true;
|
||||
++s_modifier_mode_generation;
|
||||
s_tail = s_head;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_keyboard_mux);
|
||||
}
|
||||
>>>>>>> pr/320
|
||||
|
||||
#endif
|
||||
@@ -1,14 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
// LilyGo T-Deck physical keyboard. It's a separate ESP32-C3 running the
|
||||
// T-Keyboard firmware on the shared I2C bus (SDA 18 / SCL 8, addr 0x55): each
|
||||
// 1-byte read returns the ASCII code of the last key press (0 = none). The C3
|
||||
// resolves shift / sym layers itself, so we just receive final characters.
|
||||
// T-Keyboard firmware on the shared I2C bus (SDA 18 / SCL 8, addr 0x55).
|
||||
// Current controller firmware supports a five-byte raw matrix mode, which lets
|
||||
// this driver preserve held modifiers and add tap/double-tap latching. Older
|
||||
// firmware returns one resolved ASCII byte; startup detects that and restores
|
||||
// the original controller-side key mode automatically.
|
||||
//
|
||||
// CRITICAL: the keyboard shares the I2C bus with the GT911 touch controller,
|
||||
// which is polled from a core-0 task. To avoid two cores hitting Wire at once,
|
||||
// tdeckKeyboardPoll() must be called from THAT task; the UI thread only drains
|
||||
// the lock-free ring via tdeckKeyboardReadKey().
|
||||
// the critical-section-protected ring via tdeckKeyboardReadKey().
|
||||
#if defined(HAS_TDECK_KEYBOARD) && defined(ESP32)
|
||||
|
||||
#include <stdint.h>
|
||||
@@ -23,10 +25,12 @@ void tdeckKeyboardPoll();
|
||||
/** Pop the next buffered key (ASCII), or 0 if none. Safe from the UI thread. */
|
||||
int tdeckKeyboardReadKey();
|
||||
|
||||
/** Compatibility hooks for UI mode transitions. Modifier state belongs to
|
||||
* the keyboard's C3 firmware in this legacy ASCII driver, so there is no
|
||||
* host-side state to clear or re-enable. */
|
||||
/** Cancel one-shot/locked modifiers and suppress a later release from a
|
||||
* currently held modifier. Call when queued input is intentionally discarded. */
|
||||
void tdeckKeyboardDiscardModifiers();
|
||||
|
||||
/** Stop continuously discarding raw modifier state after the UI returns to a
|
||||
* mode where keyboard input is meaningful. */
|
||||
void tdeckKeyboardAllowModifiers();
|
||||
|
||||
/** Request a keyboard-backlight level (0 = off, 0xFF = on). Safe from the UI
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "LatchedModifier.h"
|
||||
|
||||
class TDeckKeyboardState {
|
||||
public:
|
||||
static constexpr size_t COLS = 5;
|
||||
|
||||
size_t update(const uint8_t current[COLS], uint32_t now_ms,
|
||||
uint8_t* out, size_t out_capacity, bool modifiers_enabled = true) {
|
||||
static const char base[COLS][7] = {
|
||||
{'q', 'w', '\0', 'a', '\0', ' ', '\0'},
|
||||
{'e', 's', 'd', 'p', 'x', 'z', '\0'},
|
||||
{'r', 'g', 't', '\0', 'v', 'c', 'f'},
|
||||
{'u', 'h', 'y', '\0', 'b', 'n', 'j'},
|
||||
{'o', 'l', 'i', '\0', '$', 'm', 'k'},
|
||||
};
|
||||
static const char symbols[COLS][7] = {
|
||||
{'#', '1', '\0', '*', '\0', '\0', '0'},
|
||||
{'2', '4', '5', '@', '8', '7', '\0'},
|
||||
{'3', '/', '(', '\0', '?', '9', '6'},
|
||||
{'_', ':', ')', '\0', '!', ',', ';'},
|
||||
{'+', '"', '-', '\0', '\0', '.', '\''},
|
||||
};
|
||||
auto down = [&](size_t col, uint8_t row) {
|
||||
return (current[col] & (uint8_t)(1U << row)) != 0;
|
||||
};
|
||||
auto pressed = [&](size_t col, uint8_t row) {
|
||||
return down(col, row) && (previous_[col] & (uint8_t)(1U << row)) == 0;
|
||||
};
|
||||
auto released = [&](size_t col, uint8_t row) {
|
||||
return !down(col, row) && (previous_[col] & (uint8_t)(1U << row)) != 0;
|
||||
};
|
||||
|
||||
if (!modifiers_enabled) {
|
||||
symbol_.baseline(down(0, 2));
|
||||
alt_.baseline(down(0, 4));
|
||||
}
|
||||
if (modifiers_enabled && pressed(0, 2)) symbol_.press();
|
||||
if (modifiers_enabled && pressed(0, 4)) alt_.press();
|
||||
if (modifiers_enabled && released(0, 2)) symbol_.release(now_ms);
|
||||
if (modifiers_enabled && released(0, 4)) alt_.release(now_ms);
|
||||
|
||||
const bool shift = modifiers_enabled && (down(1, 6) || down(2, 3));
|
||||
size_t written = 0;
|
||||
auto emit = [&](uint8_t key) {
|
||||
if (written < out_capacity) out[written++] = key;
|
||||
};
|
||||
for (uint8_t row = 0; row < 7; ++row) {
|
||||
for (size_t col = 0; col < COLS; ++col) {
|
||||
if (!pressed(col, row)) continue;
|
||||
if ((col == 0 && (row == 2 || row == 4)) ||
|
||||
(col == 1 && row == 6) || (col == 2 && row == 3)) continue;
|
||||
const bool physical_alt = modifiers_enabled && alt_.held();
|
||||
if (physical_alt) alt_.markHeldUsed();
|
||||
// Preserve LilyGO controller shortcuts while raw mode moves ordinary
|
||||
// matrix translation into the host. The C3 still handles Alt+B's
|
||||
// backlight toggle; suppress its character exactly as key mode does.
|
||||
if (physical_alt && col == 3 && row == 4) {
|
||||
symbol_.consumeForKey();
|
||||
continue;
|
||||
}
|
||||
if (physical_alt && col == 2 && row == 5) {
|
||||
symbol_.consumeForKey();
|
||||
emit(0x0C);
|
||||
continue;
|
||||
}
|
||||
if (col == 3 && row == 3) {
|
||||
symbol_.consumeForKey(); alt_.consumeForKey(); emit('\r'); continue;
|
||||
}
|
||||
if (col == 4 && row == 3) {
|
||||
symbol_.consumeForKey(); alt_.consumeForKey(); emit('\b'); continue;
|
||||
}
|
||||
|
||||
const bool use_symbols = modifiers_enabled && (symbol_.active() || alt_.latched());
|
||||
const char key = use_symbols ? symbols[col][row] : base[col][row];
|
||||
if (modifiers_enabled) {
|
||||
symbol_.consumeForKey();
|
||||
if (!physical_alt) alt_.consumeForKey();
|
||||
}
|
||||
if (key == '\0') continue;
|
||||
char resolved = key;
|
||||
if (!use_symbols && shift && resolved >= 'a' && resolved <= 'z') resolved -= 32;
|
||||
emit((uint8_t)resolved);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t col = 0; col < COLS; ++col) previous_[col] = current[col] & 0x7F;
|
||||
return written;
|
||||
}
|
||||
|
||||
void baseline(const uint8_t current[COLS]) {
|
||||
symbol_.baseline((current[0] & (uint8_t)(1U << 2)) != 0);
|
||||
alt_.baseline((current[0] & (uint8_t)(1U << 4)) != 0);
|
||||
for (size_t col = 0; col < COLS; ++col) previous_[col] = current[col] & 0x7F;
|
||||
}
|
||||
|
||||
void discardModifiers() {
|
||||
symbol_.discard();
|
||||
alt_.discard();
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t previous_[COLS] = {};
|
||||
LatchedModifier symbol_;
|
||||
LatchedModifier alt_;
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "helpers/input/LatchedModifier.h"
|
||||
|
||||
int main() {
|
||||
LatchedModifier modifier;
|
||||
|
||||
modifier.press();
|
||||
assert(modifier.consumeForKey());
|
||||
modifier.release(100);
|
||||
assert(!modifier.consumeForKey());
|
||||
|
||||
modifier.press();
|
||||
modifier.release(200);
|
||||
assert(modifier.consumeForKey());
|
||||
assert(!modifier.consumeForKey());
|
||||
|
||||
modifier.press();
|
||||
modifier.release(400);
|
||||
modifier.press();
|
||||
modifier.release(700);
|
||||
assert(modifier.locked());
|
||||
assert(modifier.consumeForKey());
|
||||
assert(modifier.consumeForKey());
|
||||
|
||||
modifier.press();
|
||||
modifier.release(800);
|
||||
assert(!modifier.active());
|
||||
|
||||
modifier.tap(1000);
|
||||
modifier.tap(1500);
|
||||
assert(!modifier.locked());
|
||||
assert(modifier.consumeForKey());
|
||||
|
||||
modifier.tap(2000);
|
||||
modifier.clearLatched();
|
||||
assert(!modifier.active());
|
||||
|
||||
modifier.tap(2100);
|
||||
modifier.discard();
|
||||
assert(!modifier.active());
|
||||
|
||||
modifier.press();
|
||||
modifier.discard();
|
||||
modifier.release(2200);
|
||||
assert(!modifier.active());
|
||||
|
||||
LatchedModifier wrapped;
|
||||
wrapped.tap(UINT32_MAX - 100);
|
||||
wrapped.tap(100);
|
||||
assert(wrapped.locked());
|
||||
|
||||
wrapped.baseline(true);
|
||||
assert(wrapped.held());
|
||||
wrapped.release(200);
|
||||
assert(!wrapped.active());
|
||||
wrapped.baseline(false);
|
||||
assert(!wrapped.active());
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "helpers/input/PagerKeyboardState.h"
|
||||
|
||||
int main() {
|
||||
PagerKeyboardState keyboard;
|
||||
|
||||
assert(keyboard.event(0, true, 10) == 'q');
|
||||
keyboard.event(0, false, 20);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 100);
|
||||
assert(keyboard.altHeld());
|
||||
assert(keyboard.event(0, true, 110) == '1');
|
||||
keyboard.event(0, false, 120);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 130);
|
||||
assert(keyboard.event(0, true, 140) == 'q');
|
||||
keyboard.event(0, false, 150);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 200);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 220);
|
||||
assert(keyboard.event(1, true, 230) == '2');
|
||||
keyboard.event(1, false, 240);
|
||||
assert(keyboard.event(1, true, 250) == 'w');
|
||||
keyboard.event(1, false, 260);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 300);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 320);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 400);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 420);
|
||||
assert(keyboard.event(0, true, 430) == '1');
|
||||
keyboard.event(0, false, 440);
|
||||
assert(keyboard.event(1, true, 450) == '2');
|
||||
keyboard.event(1, false, 460);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 470);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 480);
|
||||
assert(keyboard.event(0, true, 490) == 'q');
|
||||
keyboard.event(0, false, 500);
|
||||
|
||||
keyboard.event(PagerKeyboardState::SHIFT_POS, true, 510);
|
||||
assert(keyboard.event(0, true, 520) == 'Q');
|
||||
keyboard.event(0, false, 530);
|
||||
keyboard.event(PagerKeyboardState::SHIFT_POS, false, 540);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 600);
|
||||
keyboard.event(PagerKeyboardState::SHIFT_POS, true, 610);
|
||||
assert(keyboard.consumeAltShiftChord());
|
||||
assert(!keyboard.consumeAltShiftChord());
|
||||
keyboard.toggleCaps();
|
||||
keyboard.event(PagerKeyboardState::SHIFT_POS, false, 620);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 630);
|
||||
assert(keyboard.event(0, true, 640) == 'Q');
|
||||
keyboard.event(0, false, 650);
|
||||
keyboard.toggleCaps();
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 700);
|
||||
assert(keyboard.event(PagerKeyboardState::BACKSPACE_POS, true, 710) == 0);
|
||||
assert(keyboard.consumeAltBackspaceChord());
|
||||
assert(!keyboard.backspaceHeld());
|
||||
keyboard.event(PagerKeyboardState::BACKSPACE_POS, false, 720);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 730);
|
||||
assert(keyboard.event(0, true, 740) == 'q');
|
||||
keyboard.event(0, false, 750);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 800);
|
||||
keyboard.discardAlt();
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 810);
|
||||
assert(keyboard.event(0, true, 820) == 'q');
|
||||
keyboard.event(0, false, 830);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 900);
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 920);
|
||||
keyboard.markAltUsed(); // no effect without a physical hold
|
||||
assert(keyboard.event(PagerKeyboardState::SPACE_POS, true, 930) == ' ');
|
||||
assert(keyboard.spaceHeld());
|
||||
keyboard.event(PagerKeyboardState::SPACE_POS, false, 940);
|
||||
assert(!keyboard.spaceHeld());
|
||||
assert(keyboard.event(0, true, 950) == 'q');
|
||||
keyboard.event(0, false, 960);
|
||||
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, true, 1000);
|
||||
keyboard.markAltUsed(); // physical Alt+encoder equivalent
|
||||
keyboard.event(PagerKeyboardState::ALT_POS, false, 1010);
|
||||
assert(keyboard.event(0, true, 1020) == 'q');
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "helpers/input/TDeckKeyboardState.h"
|
||||
|
||||
static size_t update(TDeckKeyboardState& keyboard, uint8_t state[5], uint32_t now,
|
||||
uint8_t* out) {
|
||||
return keyboard.update(state, now, out, 8);
|
||||
}
|
||||
|
||||
static void setKey(uint8_t state[5], int col, int row, bool down) {
|
||||
if (down) state[col] |= (uint8_t)(1U << row);
|
||||
else state[col] &= (uint8_t)~(1U << row);
|
||||
}
|
||||
|
||||
int main() {
|
||||
TDeckKeyboardState keyboard;
|
||||
uint8_t state[5] = {};
|
||||
uint8_t out[8] = {};
|
||||
|
||||
setKey(state, 0, 2, true); // hold Sym
|
||||
assert(update(keyboard, state, 10, out) == 0);
|
||||
setKey(state, 0, 0, true); // q -> #
|
||||
assert(update(keyboard, state, 20, out) == 1 && out[0] == '#');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 30, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 40, out);
|
||||
setKey(state, 0, 0, true); // hold use must not latch
|
||||
assert(update(keyboard, state, 50, out) == 1 && out[0] == 'q');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 60, out);
|
||||
|
||||
setKey(state, 0, 2, true); // tap Sym
|
||||
update(keyboard, state, 100, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 120, out);
|
||||
setKey(state, 0, 1, true); // w -> 1 once
|
||||
assert(update(keyboard, state, 130, out) == 1 && out[0] == '1');
|
||||
setKey(state, 0, 1, false);
|
||||
update(keyboard, state, 140, out);
|
||||
setKey(state, 0, 1, true);
|
||||
assert(update(keyboard, state, 150, out) == 1 && out[0] == 'w');
|
||||
setKey(state, 0, 1, false);
|
||||
update(keyboard, state, 160, out);
|
||||
|
||||
setKey(state, 0, 2, true); // an unmapped symbol still uses the hold
|
||||
update(keyboard, state, 170, out);
|
||||
setKey(state, 0, 5, true); // Space has no T-Deck symbol mapping
|
||||
assert(update(keyboard, state, 175, out) == 0);
|
||||
setKey(state, 0, 5, false);
|
||||
update(keyboard, state, 180, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 185, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 190, out) == 1 && out[0] == 'q');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 195, out);
|
||||
|
||||
setKey(state, 0, 4, true); // held Alt preserves base layer
|
||||
update(keyboard, state, 200, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 210, out) == 1 && out[0] == 'q');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 215, out);
|
||||
setKey(state, 3, 4, true); // Alt+B stays controller-only
|
||||
assert(update(keyboard, state, 216, out) == 0);
|
||||
setKey(state, 3, 4, false);
|
||||
update(keyboard, state, 217, out);
|
||||
setKey(state, 2, 5, true); // Alt+C keeps its legacy control code
|
||||
assert(update(keyboard, state, 218, out) == 1 && out[0] == 0x0C);
|
||||
setKey(state, 2, 5, false);
|
||||
update(keyboard, state, 219, out);
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 220, out);
|
||||
|
||||
setKey(state, 0, 4, true); // solo Alt is a one-shot symbol latch
|
||||
update(keyboard, state, 225, out);
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 230, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 235, out) == 1 && out[0] == '#');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 240, out);
|
||||
|
||||
setKey(state, 0, 4, true); // double-tap Alt locks symbols
|
||||
update(keyboard, state, 250, out);
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 270, out);
|
||||
setKey(state, 0, 4, true);
|
||||
update(keyboard, state, 300, out);
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 320, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 330, out) == 1 && out[0] == '#');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 340, out);
|
||||
setKey(state, 0, 1, true);
|
||||
assert(update(keyboard, state, 350, out) == 1 && out[0] == '1');
|
||||
setKey(state, 0, 1, false);
|
||||
update(keyboard, state, 360, out);
|
||||
setKey(state, 0, 4, true); // tap Alt unlocks
|
||||
update(keyboard, state, 400, out);
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 420, out);
|
||||
|
||||
setKey(state, 0, 2, true); // double-tap Sym locks symbols
|
||||
update(keyboard, state, 430, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 440, out);
|
||||
setKey(state, 0, 2, true);
|
||||
update(keyboard, state, 450, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 460, out);
|
||||
setKey(state, 0, 1, true);
|
||||
assert(update(keyboard, state, 470, out) == 1 && out[0] == '1');
|
||||
setKey(state, 0, 1, false);
|
||||
update(keyboard, state, 475, out);
|
||||
setKey(state, 0, 2, true);
|
||||
update(keyboard, state, 480, out);
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 490, out);
|
||||
|
||||
setKey(state, 1, 6, true); // held Shift still uppercases base
|
||||
update(keyboard, state, 500, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 510, out) == 1 && out[0] == 'Q');
|
||||
|
||||
setKey(state, 0, 0, false);
|
||||
setKey(state, 1, 6, false);
|
||||
update(keyboard, state, 520, out);
|
||||
setKey(state, 0, 2, true); // discarded physical hold
|
||||
update(keyboard, state, 600, out);
|
||||
keyboard.discardModifiers();
|
||||
setKey(state, 0, 2, false);
|
||||
update(keyboard, state, 620, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 630, out) == 1 && out[0] == 'q');
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 640, out);
|
||||
|
||||
setKey(state, 0, 4, true); // discarded Alt hold cannot latch on release
|
||||
update(keyboard, state, 650, out);
|
||||
keyboard.discardModifiers();
|
||||
setKey(state, 0, 4, false);
|
||||
update(keyboard, state, 660, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(update(keyboard, state, 670, out) == 1 && out[0] == 'q');
|
||||
|
||||
setKey(state, 0, 0, false);
|
||||
update(keyboard, state, 680, out);
|
||||
setKey(state, 0, 2, true); // modifiers disabled: base key still routes
|
||||
update(keyboard, state, 690, out);
|
||||
setKey(state, 0, 0, true);
|
||||
assert(keyboard.update(state, 700, out, 8, false) == 1 && out[0] == 'q');
|
||||
|
||||
uint8_t baseline[5] = {};
|
||||
setKey(baseline, 0, 1, true); // transition frame produces no edge later
|
||||
keyboard.baseline(baseline);
|
||||
assert(keyboard.update(baseline, 710, out, 8) == 0);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user