mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
t5s3 cardkb support; update firmware build date
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
98
examples/companion_radio/ui-new/CardKBKeyboard.h
Normal file
98
examples/companion_radio/ui-new/CardKBKeyboard.h
Normal 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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user