t5s3 cardkb support; update firmware build date

This commit is contained in:
pelgraine
2026-03-20 06:23:05 +11:00
parent 5bed26cb72
commit 3ae988c0bb
8 changed files with 301 additions and 4 deletions

View File

@@ -8,7 +8,7 @@
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "19 March 2026"
#define FIRMWARE_BUILD_DATE "20 March 2026"
#endif
#ifndef FIRMWARE_VERSION

View File

@@ -364,6 +364,13 @@
static bool gt911Ready = false;
static bool sdCardReady = false; // T5S3 SD card state
#ifdef MECK_CARDKB
#include "CardKBKeyboard.h"
static CardKBKeyboard cardkb;
static unsigned long lastCardKBProbe = 0;
#define CARDKB_PROBE_INTERVAL_MS 5000 // Re-probe for hot-plug every 5s
#endif
// Read GT911 in landscape orientation (960×540)
static bool readTouchLandscape(int16_t* outX, int16_t* outY) {
if (!gt911Ready) return false;
@@ -1397,6 +1404,16 @@ void setup() {
}
#endif
// Initialize CardKB external keyboard (if connected via QWIIC)
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
if (cardkb.begin()) {
ui_task.setCardKBDetected(true);
Serial.println("setup() - CardKB detected at 0x5F");
} else {
Serial.println("setup() - CardKB not detected (will re-probe)");
}
#endif
// RTC diagnostic + boot-time serial clock sync (T5S3 has no GPS)
#if defined(LilyGo_T5S3_EPaper_Pro)
{
@@ -1978,6 +1995,128 @@ void loop() {
#endif
#endif // MECK_TOUCH_ENABLED
// ---------------------------------------------------------------------------
// CardKB external keyboard polling (T5S3 only, via QWIIC)
// When VKB is active: typed characters feed into the VKB text buffer.
// When VKB is not active: navigation keys route through injectKey().
// ESC key maps to 'q' (back) when no VKB is active.
// ---------------------------------------------------------------------------
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
{
// Hot-plug detection: re-probe periodically
if (millis() - lastCardKBProbe >= CARDKB_PROBE_INTERVAL_MS) {
lastCardKBProbe = millis();
bool wasDetected = cardkb.isDetected();
bool nowDetected = cardkb.probe();
if (nowDetected != wasDetected) {
ui_task.setCardKBDetected(nowDetected);
Serial.printf("[CardKB] %s\n", nowDetected ? "Connected" : "Disconnected");
}
}
// Poll for keypress
char ckb = cardkb.readKey();
if (ckb != 0) {
// Block input while locked (same as touch)
if (!ui_task.isLocked()) {
cpuPower.setBoost();
ui_task.keepAlive();
if (ui_task.isVKBActive()) {
// VKB is open — feed character into VKB text buffer
ui_task.feedCardKBChar(ckb);
} else if (ckb == 0x1B) {
// ESC → back (same as 'q' on T-Deck Pro)
ui_task.injectKey('q');
} else if (ui_task.isOnHomeScreen()) {
// Home screen: letter shortcuts open tiles, arrows cycle pages
switch (ckb) {
case 'm': ui_task.gotoChannelScreen(); break;
case 'c': ui_task.gotoContactsScreen(); break;
case 'e': ui_task.gotoTextReader(); break;
case 'n': ui_task.gotoNotesScreen(); break;
case 's': ui_task.gotoSettingsScreen(); break;
case 'f': ui_task.gotoDiscoveryScreen(); break;
case 'h': ui_task.gotoLastHeardScreen(); break;
#ifdef MECK_WEB_READER
case 'b': ui_task.gotoWebReader(); break;
#endif
#if HAS_GPS
case 'g': ui_task.gotoMapScreen(); break;
#endif
default: ui_task.injectKey(ckb); break;
}
} else {
// Non-home screens: handle Enter for compose, remap arrows to WASD.
// Screens respond to w/s (scroll up/down) and a/d (prev/next channel)
// but not to KEY_LEFT/KEY_RIGHT constants.
if (ckb == '\r') {
// Enter key — screen-specific compose or select
if (ui_task.isOnChannelScreen()) {
// Open VKB for channel message compose
uint8_t chIdx = ui_task.getChannelScreenViewIdx();
ChannelDetails ch;
if (the_mesh.getChannel(chIdx, ch)) {
char label[40];
snprintf(label, sizeof(label), "To: %s", ch.name);
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
}
} else if (ui_task.isOnContactsScreen()) {
// DM compose for chat contacts, admin for repeaters
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
if (cs) {
int idx = cs->getSelectedContactIdx();
uint8_t ctype = cs->getSelectedContactType();
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
char dname[32];
cs->getSelectedContactName(dname, sizeof(dname));
char label[40];
snprintf(label, sizeof(label), "DM: %s", dname);
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
ui_task.gotoRepeaterAdmin(idx);
}
}
} else if (ui_task.isOnRepeaterAdmin()) {
// Open VKB for password or CLI entry
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen();
if (admin) {
RepeaterAdminScreen::AdminState astate = admin->getState();
if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
ui_task.showVirtualKeyboard(VKB_ADMIN_PASSWORD, "Admin Password", "", 32);
} else {
ui_task.showVirtualKeyboard(VKB_ADMIN_CLI, "Admin Command", "", 137);
}
}
} else if (ui_task.isOnNotesScreen()) {
// Open VKB for note editing
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
if (notesScr && notesScr->isEditing()) {
ui_task.showVirtualKeyboard(VKB_NOTES, "Edit Note", "", 137);
} else {
ui_task.injectKey('\r'); // File list: select/open
}
} else {
// All other screens: pass Enter through for native handling
// (settings toggle, discovery add-contact, last heard, text reader file select, etc.)
ui_task.injectKey('\r');
}
} else {
// Non-Enter keys: remap arrows to WASD, pass others through
switch (ckb) {
case (char)0xF2: ui_task.injectKey('w'); break; // Up → scroll up
case (char)0xF1: ui_task.injectKey('s'); break; // Down → scroll down
case (char)0xF3: ui_task.injectKey('a'); break; // Left → prev channel/category
case (char)0xF4: ui_task.injectKey('d'); break; // Right → next channel/category
default: ui_task.injectKey(ckb); break;
}
}
}
}
}
}
#endif
// Poll touch input for phone dialer numpad
// Hybrid debounce: finger-up detection + 150ms minimum between accepted taps.
// The CST328 INT pin is pulse-based (not level), so getPoint() can return

View File

@@ -0,0 +1,98 @@
#pragma once
// =============================================================================
// CardKBKeyboard — M5Stack CardKB (or compatible) I2C keyboard driver
//
// Polls 0x5F on the shared I2C bus via QWIIC connector.
// Maps CardKB special key codes to Meck key constants.
//
// Usage:
// CardKBKeyboard cardkb;
// if (cardkb.begin()) { /* detected */ }
// char key = cardkb.readKey(); // returns 0 if no key
// =============================================================================
#if defined(LilyGo_T5S3_EPaper_Pro) && defined(MECK_CARDKB)
#ifndef CARDKB_KEYBOARD_H
#define CARDKB_KEYBOARD_H
#include <Arduino.h>
#include <Wire.h>
// I2C address (defined in variant.h, fallback here)
#ifndef CARDKB_I2C_ADDR
#define CARDKB_I2C_ADDR 0x5F
#endif
// CardKB special key codes (from M5Stack documentation)
#define CARDKB_KEY_UP 0xB5
#define CARDKB_KEY_DOWN 0xB6
#define CARDKB_KEY_LEFT 0xB4
#define CARDKB_KEY_RIGHT 0xB7
#define CARDKB_KEY_TAB 0x09
#define CARDKB_KEY_ESC 0x1B
#define CARDKB_KEY_BS 0x08
#define CARDKB_KEY_ENTER 0x0D
#define CARDKB_KEY_DEL 0x7F
#define CARDKB_KEY_FN 0x00 // Fn modifier (swallowed by CardKB internally)
class CardKBKeyboard {
public:
CardKBKeyboard() : _detected(false) {}
// Probe for CardKB on the I2C bus. Call after Wire.begin().
bool begin() {
Wire.beginTransmission(CARDKB_I2C_ADDR);
_detected = (Wire.endTransmission() == 0);
if (_detected) {
Serial.println("[CardKB] Detected at 0x5F");
}
return _detected;
}
// Re-probe (e.g. for hot-plug detection every few seconds)
bool probe() {
Wire.beginTransmission(CARDKB_I2C_ADDR);
_detected = (Wire.endTransmission() == 0);
return _detected;
}
bool isDetected() const { return _detected; }
// Poll for a keypress. Returns 0 if no key available.
// Returns raw ASCII for printable chars, or Meck KEY_* constants for nav keys.
char readKey() {
if (!_detected) return 0;
Wire.requestFrom((uint8_t)CARDKB_I2C_ADDR, (uint8_t)1);
if (!Wire.available()) return 0;
uint8_t raw = Wire.read();
if (raw == 0) return 0;
// Map CardKB special keys to Meck constants
switch (raw) {
case CARDKB_KEY_UP: return 0xF2; // KEY_PREV
case CARDKB_KEY_DOWN: return 0xF1; // KEY_NEXT
case CARDKB_KEY_LEFT: return 0xF3; // KEY_LEFT
case CARDKB_KEY_RIGHT: return 0xF4; // KEY_RIGHT
case CARDKB_KEY_ENTER: return '\r';
case CARDKB_KEY_BS: return '\b';
case CARDKB_KEY_DEL: return '\b'; // Treat delete same as backspace
case CARDKB_KEY_ESC: return 0x1B; // ESC — handled by caller
case CARDKB_KEY_TAB: return 0x09; // Tab — available for future use
default:
// Printable ASCII — pass through unchanged
if (raw >= 0x20 && raw <= 0x7E) {
return (char)raw;
}
// Unknown code — ignore
return 0;
}
}
private:
bool _detected;
};
#endif // CARDKB_KEYBOARD_H
#endif // LilyGo_T5S3_EPaper_Pro && MECK_CARDKB

View File

@@ -2032,6 +2032,30 @@ void UITask::onVKBCancel() {
display.invalidateFrameCRC();
Serial.println("[UI] VKB cancelled");
}
#ifdef MECK_CARDKB
void UITask::feedCardKBChar(char c) {
if (_vkbActive) {
// VKB is open — feed character into its text buffer
if (_vkb.feedChar(c)) {
_next_refresh = 0; // Redraw VKB immediately
_auto_off = millis() + 120000; // Extend timeout while typing
// Check if feedChar triggered submit or cancel
if (_vkb.status() == VKB_SUBMITTED) {
onVKBSubmit();
} else if (_vkb.status() == VKB_CANCELLED) {
onVKBCancel();
}
} else {
// feedChar returned false — nav keys (arrows) while VKB is active
// Not consumed; could be used for cursor movement in future
}
} else {
// No VKB active — route as normal navigation key
injectKey(c);
}
}
#endif
#endif
bool UITask::getGPSState() {

View File

@@ -103,6 +103,9 @@ class UITask : public AbstractUITask {
bool _vkbActive = false;
UIScreen* _screenBeforeVKB = nullptr;
unsigned long _vkbOpenedAt = 0;
#ifdef MECK_CARDKB
bool _cardkbDetected = false;
#endif
#elif defined(LilyGo_TDeck_Pro)
UIScreen* lock_screen; // Lock screen (big clock + battery + unread)
UIScreen* _screenBeforeLock = nullptr;
@@ -197,6 +200,11 @@ public:
void showVirtualKeyboard(VKBPurpose purpose, const char* label, const char* initial, int maxLen, int contextIdx = 0);
void onVKBSubmit();
void onVKBCancel();
#ifdef MECK_CARDKB
void setCardKBDetected(bool v) { _cardkbDetected = v; }
bool hasCardKB() const { return _cardkbDetected; }
void feedCardKBChar(char c);
#endif
#endif
#ifdef MECK_WEB_READER
bool isOnWebReader() const { return curr == web_reader; }

View File

@@ -217,6 +217,32 @@ public:
// Swipe up on keyboard = cancel
void cancel() { _status = VKB_CANCELLED; }
// --- Feed a raw ASCII character from an external physical keyboard ---
// Maps standard ASCII control chars to internal VKB actions.
// Returns true if the character was consumed.
#ifdef MECK_CARDKB
bool feedChar(char c) {
if (_status != VKB_EDITING) return false;
switch (c) {
case '\r': processKey('>'); return true; // Enter → submit
case '\b': processKey('<'); return true; // Backspace
case 0x7F: processKey('<'); return true; // Delete → backspace
case 0x1B: _status = VKB_CANCELLED; return true; // ESC → cancel
case ' ': processKey('~'); return true; // Space
default:
// Printable ASCII → insert directly
if (c >= 0x20 && c <= 0x7E) {
if (_textLen < _maxLen) {
_text[_textLen++] = c;
_text[_textLen] = '\0';
}
return true;
}
return false; // Non-printable / nav keys — not consumed
}
}
#endif
private:
VKBStatus _status;
VKBPurpose _purpose;

View File

@@ -79,7 +79,7 @@ build_flags =
-D CHANNEL_MSG_HISTORY_SIZE=800
-D DISPLAY_CLASS=FastEPDDisplay
-D USE_EINK
; Font family: comment/uncomment to toggle (delete .indexes on SD after switching)
-D MECK_CARDKB
; -D MECK_SERIF_FONT ; FreeSerif (Times New Roman-like)
; ; Default (no flag): FreeSans (Arial-like)
build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter}
@@ -111,6 +111,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=FastEPDDisplay
-D USE_EINK
-D MECK_CARDKB
; -D MECK_SERIF_FONT
build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -144,6 +145,7 @@ build_flags =
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=FastEPDDisplay
-D USE_EINK
-D MECK_CARDKB
; -D MECK_SERIF_FONT
build_src_filter = ${LilyGo_T5S3_EPaper_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
@@ -156,5 +158,4 @@ lib_deps =
densaugeo/base64 @ ~1.4.0
adafruit/Adafruit GFX Library@^1.11.0
https://github.com/mverch67/FastEPD/archive/0df1bff329b6fc782e062f611758880762340647.zip
https://github.com/lewisxhe/SensorLib/archive/refs/tags/v0.3.4.zip
https://github.com/lewisxhe/SensorLib/archive/refs/tags/v0.3.4.zip

View File

@@ -29,6 +29,7 @@
#define I2C_ADDR_BQ27220 0x55 // Fuel gauge
#define I2C_ADDR_BQ25896 0x6B // Battery charger
#define I2C_ADDR_TPS65185 0x68 // E-ink power driver
#define CARDKB_I2C_ADDR 0x5F // M5Stack CardKB (external, via QWIIC)
// -----------------------------------------------------------------------------
// SPI Bus — shared by LoRa and SD card