Files
MeshCore-Solo/variants/lilygo_techo_lite/TEchoKeyShield.cpp
T
JakubandClaude Opus 5 05e57357d3 feat(boards): add M5Stack Cardputer ADV and LilyGO T-Echo Lite KeyShield
New M5Stack Cardputer ADV variant (ESP32-S3, ST7789 TFT, built-in TCA8418
QWERTY keyboard, PI4IOE5V6408 LoRa-cap IO-expander autodetect), and a
KeyShield accessory variant for the existing LilyGO T-Echo Lite (external
TCA8418 T9 keypad + AW21009 backlight driver). Both keyboards share one
ENV_USE_TCA8418 polling block in UITask.cpp::loop(), coexisting with the
unrelated CardKB support (different chip/address/flag).

Fixes carried in from the contributed T-Echo Lite code: swapped GPS RX/TX
pins, TX-LED hooks, TCXO voltage, missing GxEPD2_122_T61 panel include.
Fixed during integration: I2C bus was probed for an RTC before Wire.begin()
configured its pins on Cardputer ADV (silent RTC autodetect failure).

Added dedicated *_solo_dual release envs for both boards (auto-picked up by
the solo-firmware release workflow). Gave the T-Echo Lite KeyShield solo
build -Os/-Ofast-unflag like every other nRF52 solo build (was missing,
cut flash usage from 90.7% to 61.4%).

Ported the shared misc-fixed 6x9 font (full Latin/Greek/Cyrillic, opt-in via
OLED_MISC_FIXED_FONT) to ST7789Display for the Cardputer's on-screen
keyboard. ST7789Spi isn't Adafruit_GFX-based like the other single-font
drivers, and this panel's logical->physical scale is non-integer, so glyphs
are re-packed to XBM and blitted through the existing drawXbm(), which
already does correct fractional-scale boundary math, rather than
duplicating that logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 01:06:24 +02:00

240 lines
7.5 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.
#ifdef LILYGO_TECHO_LITE_KEYSHIELD
#include <Arduino.h>
#include <Wire.h>
#include <helpers/ui/UIScreen.h> // KEY_PREV / KEY_NEXT / KEY_ENTER / KEY_CANCEL
// ---- TCA8418 (KeyShield keypad) --------------------------------------------
#define TCA8418_ADDR 0x34
#define TCA8418_REG_CFG 0x01
#define TCA8418_REG_INT_STAT 0x02
#define TCA8418_REG_KEY_LCK_EC 0x03
#define TCA8418_REG_KEY_EVENT_A 0x04
#define TCA8418_REG_KP_GPIO1 0x1D
#define TCA8418_REG_KP_GPIO2 0x1E
#define TCA8418_REG_KP_GPIO3 0x1F
#define TCA8418_REG_GPI_EM1 0x20
#define TCA8418_REG_GPI_EM2 0x21
#define TCA8418_REG_GPI_EM3 0x22
#define TCA8418_REG_DEBOUNCE 0x29
#define MULTI_TAP_THRESHOLD 300 // multitap threshold csúszó ablak ideje
#define LONG_PRESS_THRESHOLD 600 // longpress threshold idő
// ---- Multi-tap / long-press állapotgép -------------------------------------
// true : folyamatos mód minden koppintáskor azonnal küld backspace-t + új karaktert
// (gyors, sima kijelzőknek jó, "élőben" látszik a T9 ciklus)
// false : halasztott mód csak akkor küldi a végleges karaktert, ha lejárt a
// MULTI_TAP_THRESHOLD (e-ink-hez ajánlott)
static bool sendBackSpace = false;
static void tca_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
static uint8_t tca_read(uint8_t reg) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom((uint8_t)TCA8418_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
// Num chars per key (T9 ciklus hossz)
static uint8_t KeyTapMod[5*4] = {
1, 5, 1, 5,
1, 9, 7, 9,
1, 7, 7, 7,
1, 1, 7, 7,
1, 1, 1, 1
};
static const unsigned char keymap[5*4][9] = {
{' '},
{'.', ',', '?', '!', '*'},
{'0'},
{':', ';', '-', '+', '#'},
{'\b'},
{'p', 'q', 'r', 's', 'P', 'Q', 'R', 'S', '7'},
{'t', 'u', 'v', 'T', 'U', 'V', '8'},
{'w', 'x', 'y', 'z', 'W', 'X', 'Y', 'Z', '9'},
{KEY_DOWN},
{'g', 'h', 'i', 'G', 'H', 'I', '4'},
{'j', 'k', 'l', 'J', 'K', 'L', '5'},
{'m', 'n', 'o', 'M', 'N', 'O', '6'},
{KEY_ENTER},
{'1'},
{'a', 'b', 'c', 'A', 'B', 'C', '2'},
{'d', 'e', 'f', 'D', 'E', 'F', '3'},
{KEY_UP},
{KEY_CANCEL},
{KEY_HOME},
{KEY_CONTEXT_MENU}
};
// long press
static const unsigned char keymapLongPress[5*4] = {
0, 0, 0, 0,
0, 0, 0, 0,
KEY_LEFT, 0, 0, 0,
KEY_KB_ENTER, 0, 0, 0,
KEY_RIGHT, KEY_CANCEL, KEY_HOME, KEY_CONTEXT_MENU
};
void tca8418_keypad_set_backspace_mode(bool continuous) {
sendBackSpace = continuous;
}
// kimeneti kör-FIFO, mert egy koppintás akár 2 karaktert is eredményezhet
// (backspace + új kar) folyamatos módban
#define KEY_OUT_QUEUE_LEN 4
static char s_outQueue[KEY_OUT_QUEUE_LEN];
static uint8_t s_outHead = 0, s_outTail = 0;
static void queue_push(char c) {
uint8_t next = (s_outTail + 1) % KEY_OUT_QUEUE_LEN;
if (next != s_outHead) { s_outQueue[s_outTail] = c; s_outTail = next; }
}
static bool queue_pop(char &c) {
if (s_outHead == s_outTail) return false;
c = s_outQueue[s_outHead];
s_outHead = (s_outHead + 1) % KEY_OUT_QUEUE_LEN;
return true;
}
// aktuálisan lenyomva tartott (fizikai) billentyű, a long-press méréshez
static uint8_t s_downKeyIndex = 0xFF;
static unsigned long s_downTime = 0;
// függőben lévő (még nem lezárt) multi-tap ciklus
static uint8_t s_pendingIndex = 0xFF; // 0xFF = nincs függő ciklus
static uint8_t s_pendingTap = 0; // aktuális tap-index (0-alapú)
static unsigned long s_pendingTapTime = 0;
static void keypad_init() {
tca_write(TCA8418_REG_CFG, 0x00);
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F);
tca_write(TCA8418_REG_GPI_EM1, 0x00);
tca_write(TCA8418_REG_GPI_EM2, 0x00);
tca_write(TCA8418_REG_GPI_EM3, 0x00);
tca_write(TCA8418_REG_KP_GPIO1, 0x1F); // ROW0-ROW4 in keypad matrix 0b00011111
tca_write(TCA8418_REG_KP_GPIO2, 0x0F); // COL0-COL3 in keypad matrix 0b00001111
tca_write(TCA8418_REG_KP_GPIO3, 0x00); // COL8, COL9
tca_write(TCA8418_REG_DEBOUNCE, 0x03);
tca_write(TCA8418_REG_INT_STAT, 0x1F);
tca_write(TCA8418_REG_CFG, 0x11);
delay(5);
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F);
}
// lezár egy függő multi-tap ciklust: halasztott módban ilyenkor kell
// kiküldeni a "végleges" kart, mert eddig semmi nem ment ki érte
static void commit_pending_if_needed() {
if (s_pendingIndex == 0xFF) return;
if (!sendBackSpace) {
uint8_t idx = s_pendingTap % KeyTapMod[s_pendingIndex];
queue_push(keymap[s_pendingIndex][idx]);
}
s_pendingIndex = 0xFF;
}
char tca8418_keypad_read() {
static bool inited = false;
if (!inited) { keypad_init(); inited = true; }
unsigned long now = millis();
// 1) lejárt-e a függő multi-tap ciklus? (nem kap I2C eseményt, ezért
// minden híváskor ellenőrizni kell)
if (s_pendingIndex != 0xFF && (now - s_pendingTapTime) >= MULTI_TAP_THRESHOLD) {
commit_pending_if_needed();
}
// 2) van-e még kiküldendő karakter a pufferben?
char qc;
if (queue_pop(qc)) return qc;
// 3) hardver FIFO
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) return 0;
uint8_t ev = tca_read(TCA8418_REG_KEY_EVENT_A);
tca_write(TCA8418_REG_INT_STAT, 0x1F);
bool pressed = (ev & 0x80) != 0;
uint8_t code = ev & 0x7F;
uint8_t row = (code - 1) / 10;
uint8_t col = (code - 1) % 10;
uint8_t key_index = row * 4 + col;
if (key_index >= 5*4) return 0;
if (pressed) {
// csak eltároljuk, a döntés (rövid/hosszú) a release-nél történik
s_downKeyIndex = key_index;
s_downTime = now;
return 0;
}
// --- release ---
if (s_downKeyIndex != key_index) return 0; // eltévedt/pár nélküli release
unsigned long duration = now - s_downTime;
s_downKeyIndex = 0xFF;
bool longEligible = keymapLongPress[key_index] != 0;
bool isLong = longEligible && duration >= LONG_PRESS_THRESHOLD;
if (isLong) {
if (sendBackSpace && s_pendingIndex == key_index) {
queue_push('\b'); // az előzőleg megjelenített preview karakter törlése
} else {
commit_pending_if_needed(); // más gombra vonatkozó függő ciklus lezárása
}
s_pendingIndex = 0xFF;
queue_push(keymapLongPress[key_index]);
}
else if (KeyTapMod[key_index] <= 1) {
// nem ciklikus gomb -> mindig azonnali, önálló leütés
commit_pending_if_needed();
s_pendingIndex = 0xFF;
queue_push(keymap[key_index][0]);
}
else {
// Serial.printf("gap: %lu ms\n", now - s_pendingTapTime); // gap idő debug
bool isContinuation = (s_pendingIndex == key_index) &&
((now - s_pendingTapTime) < MULTI_TAP_THRESHOLD);
if (!isContinuation) {
commit_pending_if_needed(); // esetleges más gombos függő ciklus lezárása
s_pendingIndex = key_index;
s_pendingTap = 0;
} else {
s_pendingTap = (s_pendingTap + 1) % KeyTapMod[key_index];
}
s_pendingTapTime = now;
if (sendBackSpace) {
if (isContinuation) queue_push('\b');
queue_push(keymap[key_index][s_pendingTap]);
}
// halasztott módban itt nem küldünk semmit, a threshold lejártakor
// (1-es lépés) vagy a következő eltérő gombnál (commit_pending_if_needed)
// fog kimenni a végleges karakter
}
if (queue_pop(qc)) return qc;
return 0;
}
#endif