Added basic Public channel only view message history and compose - bugs still present

This commit is contained in:
pelgraine
2026-01-29 21:33:14 +11:00
parent 5bdcbb25b6
commit c5df40cefd
5 changed files with 778 additions and 2 deletions

View File

@@ -4,6 +4,22 @@
#include "variant.h" // Board-specific defines (HAS_GPS, etc.)
#include "target.h" // For sensors, board, etc.
// T-Deck Pro Keyboard support
#if defined(LilyGo_TDeck_Pro)
#include "TCA8418Keyboard.h"
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
// Compose mode state
static bool composeMode = false;
static char composeBuffer[138]; // 137 chars max + null terminator
static int composePos = 0;
void initKeyboard();
void handleKeyboardInput();
void drawComposeScreen();
void sendComposedMessage();
#endif
// Believe it or not, this std C function is busted on some platforms!
static uint32_t _atoi(const char* sp) {
uint32_t n = 0;
@@ -303,6 +319,11 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - ui_task.begin() done");
#endif
// Initialize T-Deck Pro keyboard
#if defined(LilyGo_TDeck_Pro)
initKeyboard();
#endif
// Enable GPS by default on T-Deck Pro
#if HAS_GPS
// Set GPS enabled in both sensor manager and node prefs
@@ -319,7 +340,232 @@ void loop() {
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
// Skip UITask rendering when in compose mode to prevent flickering
#if defined(LilyGo_TDeck_Pro)
if (!composeMode) {
ui_task.loop();
}
#else
ui_task.loop();
#endif
#endif
rtc_clock.tick();
}
// Handle T-Deck Pro keyboard input
#if defined(LilyGo_TDeck_Pro)
handleKeyboardInput();
#endif
}
// ============================================================================
// T-DECK PRO KEYBOARD FUNCTIONS
// ============================================================================
#if defined(LilyGo_TDeck_Pro)
void initKeyboard() {
// Keyboard uses the same I2C bus as other peripherals (already initialized)
if (keyboard.begin()) {
MESH_DEBUG_PRINTLN("setup() - Keyboard initialized");
composeBuffer[0] = '\0';
composePos = 0;
composeMode = false;
} else {
MESH_DEBUG_PRINTLN("setup() - Keyboard initialization failed!");
}
}
void handleKeyboardInput() {
if (!keyboard.isReady()) return;
char key = keyboard.readKey();
if (key == 0) return;
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
key >= 32 ? key : '?', key, composeMode);
if (composeMode) {
// In compose mode - handle text input
if (key == '\r') {
// Enter - send the message
Serial.println("Compose: Enter pressed, sending...");
if (composePos > 0) {
sendComposedMessage();
}
composeMode = false;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
return;
}
if (key == '\b') {
// Backspace - check if shift was recently pressed for cancel combo
if (keyboard.wasShiftRecentlyPressed(500)) {
// Shift+Backspace = Cancel
Serial.println("Compose: Shift+Backspace, cancelling...");
composeMode = false;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
return;
}
// Regular backspace - delete last character
if (composePos > 0) {
composePos--;
composeBuffer[composePos] = '\0';
Serial.printf("Compose: Backspace, pos now %d\n", composePos);
drawComposeScreen();
}
return;
}
// Regular character input
if (key >= 32 && key < 127 && composePos < 137) {
composeBuffer[composePos++] = key;
composeBuffer[composePos] = '\0';
Serial.printf("Compose: Added '%c', pos now %d\n", key, composePos);
drawComposeScreen();
}
return;
}
// Normal mode - not composing
switch (key) {
case 'c':
case 'C':
// Enter compose mode
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
Serial.println("Entering compose mode");
drawComposeScreen();
break;
case 'm':
case 'M':
// Go to channel message screen
Serial.println("Opening channel messages");
ui_task.gotoChannelScreen();
break;
case 'w':
case 'W':
case 'a':
case 'A':
// Navigate left/previous
Serial.println("Nav: Previous");
ui_task.injectKey(0xF2); // KEY_PREV
break;
case 's':
case 'S':
case 'd':
case 'D':
// Navigate right/next
Serial.println("Nav: Next");
ui_task.injectKey(0xF1); // KEY_NEXT
break;
case '\r':
// Select/Enter
Serial.println("Nav: Enter/Select");
ui_task.injectKey(13); // KEY_ENTER
break;
case 'q':
case 'Q':
case '\b':
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
break;
case ' ':
// Space - also acts as next/select
Serial.println("Nav: Space (Next)");
ui_task.injectKey(0xF1); // KEY_NEXT
break;
default:
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
break;
}
}
void drawComposeScreen() {
#ifdef DISPLAY_CLASS
display.startFrame();
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, 0);
display.print("Compose - Public Channel");
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, 11, display.width(), 1);
display.setCursor(0, 14);
display.setColor(DisplayDriver::LIGHT);
// Word wrap the compose buffer
int x = 0;
int y = 14;
int charsPerLine = display.width() / 6;
char charStr[2] = {0, 0}; // Buffer for single character as string
for (int i = 0; i < composePos; i++) {
charStr[0] = composeBuffer[i];
display.print(charStr);
x++;
if (x >= charsPerLine) {
x = 0;
y += 11;
display.setCursor(0, y);
}
}
// Show cursor
display.print("_");
// Status bar
int statusY = display.height() - 12;
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, statusY - 2, display.width(), 1);
display.setCursor(0, statusY);
display.setColor(DisplayDriver::YELLOW);
char status[50];
sprintf(status, "%d/137 Enter:Send Sh+Del:Cancel", composePos);
display.print(status);
display.endFrame();
#endif
}
void sendComposedMessage() {
if (composePos == 0) return;
MESH_DEBUG_PRINTLN("Sending message: %s", composeBuffer);
// Get the Public channel (index 0)
ChannelDetails channel;
if (the_mesh.getChannel(0, channel)) {
uint32_t timestamp = rtc_clock.getCurrentTime();
// Send to channel
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
the_mesh.getNodePrefs()->node_name,
composeBuffer, composePos)) {
MESH_DEBUG_PRINTLN("Message sent to Public channel");
ui_task.showAlert("Sent!", 1500);
} else {
MESH_DEBUG_PRINTLN("Failed to send message");
ui_task.showAlert("Send failed!", 1500);
}
} else {
MESH_DEBUG_PRINTLN("Could not get Public channel");
ui_task.showAlert("No channel!", 1500);
}
}
#endif // LilyGo_TDeck_Pro

View File

@@ -0,0 +1,238 @@
#pragma once
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <MeshCore.h>
// Maximum messages to store in history
#define CHANNEL_MSG_HISTORY_SIZE 20
#define CHANNEL_MSG_TEXT_LEN 160
class UITask; // Forward declaration
class ChannelScreen : public UIScreen {
public:
struct ChannelMessage {
uint32_t timestamp;
uint8_t path_len;
char text[CHANNEL_MSG_TEXT_LEN];
bool valid;
};
private:
UITask* _task;
mesh::RTCClock* _rtc;
ChannelMessage _messages[CHANNEL_MSG_HISTORY_SIZE];
int _msgCount; // Total messages stored
int _newestIdx; // Index of newest message (circular buffer)
int _scrollPos; // Current scroll position (0 = newest)
int _msgsPerPage; // Messages that fit on screen
public:
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0), _msgsPerPage(3) {
// Initialize all messages as invalid
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
_messages[i].valid = false;
}
}
// Add a new message to the history
void addMessage(uint8_t path_len, const char* sender, const char* text) {
// Move to next slot in circular buffer
_newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE;
ChannelMessage* msg = &_messages[_newestIdx];
msg->timestamp = _rtc->getCurrentTime();
msg->path_len = path_len;
msg->valid = true;
// The text already contains "Sender: message" format, just store it
strncpy(msg->text, text, CHANNEL_MSG_TEXT_LEN - 1);
msg->text[CHANNEL_MSG_TEXT_LEN - 1] = '\0';
if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) {
_msgCount++;
}
// Reset scroll to show newest message
_scrollPos = 0;
}
int getMessageCount() const { return _msgCount; }
int render(DisplayDriver& display) override {
char tmp[32];
// Header
display.setCursor(0, 0);
display.setTextSize(1);
display.setColor(DisplayDriver::GREEN);
display.print("Public Channel");
// Message count on right
sprintf(tmp, "[%d]", _msgCount);
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
display.print(tmp);
// Divider line
display.drawRect(0, 11, display.width(), 1);
if (_msgCount == 0) {
display.setCursor(0, 25);
display.setColor(DisplayDriver::LIGHT);
display.print("No messages yet");
display.setCursor(0, 40);
display.print("Press C to compose");
} else {
int lineHeight = 10;
int headerHeight = 14;
int footerHeight = 14;
int availableHeight = display.height() - headerHeight - footerHeight;
// Calculate chars per line based on display width
int charsPerLine = display.width() / 6;
int y = headerHeight;
// Display messages from scroll position
int msgsDrawn = 0;
for (int i = 0; i + _scrollPos < _msgCount && y < display.height() - footerHeight - lineHeight; i++) {
// Calculate index in circular buffer
int idx = _newestIdx - _scrollPos - i;
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
if (!_messages[idx].valid) continue;
ChannelMessage* msg = &_messages[idx];
// Time indicator with hop count
display.setCursor(0, y);
display.setColor(DisplayDriver::YELLOW);
uint32_t age = _rtc->getCurrentTime() - msg->timestamp;
if (age < 60) {
sprintf(tmp, "(%d) %ds", msg->path_len == 0xFF ? 0 : msg->path_len, age);
} else if (age < 3600) {
sprintf(tmp, "(%d) %dm", msg->path_len == 0xFF ? 0 : msg->path_len, age / 60);
} else if (age < 86400) {
sprintf(tmp, "(%d) %dh", msg->path_len == 0xFF ? 0 : msg->path_len, age / 3600);
} else {
sprintf(tmp, "(%d) %dd", msg->path_len == 0xFF ? 0 : msg->path_len, age / 86400);
}
display.print(tmp);
y += lineHeight;
// Message text with word wrap - the text already contains "Sender: message"
display.setColor(DisplayDriver::LIGHT);
int textLen = strlen(msg->text);
int pos = 0;
int linesForThisMsg = 0;
int maxLinesPerMsg = 3; // Allow up to 3 lines per message
while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y < display.height() - footerHeight - 2) {
display.setCursor(0, y);
// Find how much text fits on this line
int lineEnd = pos + charsPerLine;
if (lineEnd >= textLen) {
lineEnd = textLen;
} else {
// Try to break at a space
int lastSpace = -1;
for (int j = pos; j < lineEnd && j < textLen; j++) {
if (msg->text[j] == ' ') lastSpace = j;
}
if (lastSpace > pos) lineEnd = lastSpace;
}
// Print this line segment
char lineBuf[42];
int lineLen = lineEnd - pos;
if (lineLen > 40) lineLen = 40;
strncpy(lineBuf, msg->text + pos, lineLen);
lineBuf[lineLen] = '\0';
display.print(lineBuf);
pos = lineEnd;
// Skip space at start of next line
while (pos < textLen && msg->text[pos] == ' ') pos++;
y += lineHeight;
linesForThisMsg++;
}
// If we truncated, show ellipsis indicator
if (pos < textLen && linesForThisMsg >= maxLinesPerMsg) {
// Message was truncated - could add "..." but space is tight
}
y += 2; // Small gap between messages
msgsDrawn++;
_msgsPerPage = msgsDrawn;
}
}
// Footer with scroll indicator and controls
int footerY = display.height() - 12;
display.drawRect(0, footerY - 2, display.width(), 1);
display.setCursor(0, footerY);
display.setColor(DisplayDriver::YELLOW);
// Left side: Q:Exit
display.print("Q:Exit");
// Middle: scroll position indicator
if (_msgCount > _msgsPerPage) {
int endMsg = _scrollPos + _msgsPerPage;
if (endMsg > _msgCount) endMsg = _msgCount;
sprintf(tmp, "%d-%d/%d", _scrollPos + 1, endMsg, _msgCount);
// Center it roughly
int midX = display.width() / 2 - 15;
display.setCursor(midX, footerY);
display.print(tmp);
}
// Right side: controls
sprintf(tmp, "W/S:Scrl C:New");
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, footerY);
display.print(tmp);
#if AUTO_OFF_MILLIS == 0 // e-ink
return 5000; // Refresh every 5s
#else
return 1000; // Refresh every 1s for time updates
#endif
}
bool handleInput(char c) override {
// KEY_PREV (0xF2) or 'w' - scroll up (older messages)
if (c == 0xF2 || c == 'w' || c == 'W') {
if (_scrollPos + _msgsPerPage < _msgCount) {
_scrollPos++;
return true;
}
}
// KEY_NEXT (0xF1) or 's' - scroll down (newer messages)
if (c == 0xF1 || c == 's' || c == 'S') {
if (_scrollPos > 0) {
_scrollPos--;
return true;
}
}
// KEY_ENTER or 'c' - compose (handled by main.cpp keyboard handler)
// 'q' - go back (handled by main.cpp keyboard handler)
return false;
}
// Reset scroll position to newest
void resetScroll() {
_scrollPos = 0;
}
};

View File

@@ -30,6 +30,7 @@
#endif
#include "icons.h"
#include "ChannelScreen.h"
class SplashScreen : public UIScreen {
UITask* _task;
@@ -580,6 +581,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
channel_screen = new ChannelScreen(this, &rtc_clock);
setCurrScreen(splash);
}
@@ -628,7 +630,12 @@ void UITask::msgRead(int msgcount) {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
// Add to preview screen (for notifications)
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
// Also add to channel history screen
((ChannelScreen *) channel_screen)->addMessage(path_len, from_name, text);
setCurrScreen(msg_preview);
if (_display != NULL) {
@@ -922,3 +929,25 @@ void UITask::toggleBuzzer() {
_next_refresh = 0; // trigger refresh
#endif
}
void UITask::injectKey(char c) {
if (c != 0 && curr) {
// Turn on display if it's off
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 100; // trigger refresh
}
}
void UITask::gotoChannelScreen() {
((ChannelScreen *) channel_screen)->resetScroll();
setCurrScreen(channel_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}

View File

@@ -51,6 +51,7 @@ class UITask : public AbstractUITask {
UIScreen* splash;
UIScreen* home;
UIScreen* msg_preview;
UIScreen* channel_screen; // Channel message history screen
UIScreen* curr;
void userLedHandler();
@@ -73,15 +74,23 @@ public:
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void gotoChannelScreen(); // Navigate to channel message screen
void showAlert(const char* text, int duration_millis);
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;
bool isOnChannelScreen() const { return curr == channel_screen; }
void toggleBuzzer();
bool getGPSState();
void toggleGPS();
// Inject a key press from external source (e.g., keyboard)
void injectKey(char c);
// Get current screen for checking state
UIScreen* getCurrentScreen() const { return curr; }
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
// from AbstractUITask
void msgRead(int msgcount) override;
@@ -90,4 +99,4 @@ public:
void loop() override;
void shutdown(bool restart = false);
};
};

View File

@@ -0,0 +1,254 @@
#pragma once
#include <Arduino.h>
#include <Wire.h>
// TCA8418 Register addresses
#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_DEBOUNCE 0x29
#define TCA8418_REG_GPI_EM1 0x20
#define TCA8418_REG_GPI_EM2 0x21
#define TCA8418_REG_GPI_EM3 0x22
// Key codes for special keys
#define KB_KEY_NONE 0
#define KB_KEY_BACKSPACE '\b'
#define KB_KEY_ENTER '\r'
#define KB_KEY_SPACE ' '
class TCA8418Keyboard {
private:
uint8_t _addr;
TwoWire* _wire;
bool _initialized;
bool _shiftActive; // Sticky shift (one-shot)
bool _altActive; // Sticky alt (one-shot)
unsigned long _lastShiftTime; // For Shift+key combos
uint8_t readReg(uint8_t reg) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->endTransmission();
_wire->requestFrom(_addr, (uint8_t)1);
return _wire->available() ? _wire->read() : 0;
}
void writeReg(uint8_t reg, uint8_t val) {
_wire->beginTransmission(_addr);
_wire->write(reg);
_wire->write(val);
_wire->endTransmission();
}
// Map raw key codes to characters (from working reader firmware)
char getKeyChar(uint8_t keyCode) {
switch (keyCode) {
// Row 1 - QWERTYUIOP
case 10: return 'q'; // Q (was 97 on different hardware)
case 9: return 'w';
case 8: return 'e';
case 7: return 'r';
case 6: return 't';
case 5: return 'y';
case 4: return 'u';
case 3: return 'i';
case 2: return 'o';
case 1: return 'p';
// Row 2 - ASDFGHJKL + Backspace
case 20: return 'a'; // A (was 98 on different hardware)
case 19: return 's';
case 18: return 'd';
case 17: return 'f';
case 16: return 'g';
case 15: return 'h';
case 14: return 'j';
case 13: return 'k';
case 12: return 'l';
case 11: return '\b'; // Backspace
// Row 3 - Alt ZXCVBNM Sym Enter
case 30: return 0; // Alt - handled separately
case 29: return 'z';
case 28: return 'x';
case 27: return 'c';
case 26: return 'v';
case 25: return 'b';
case 24: return 'n';
case 23: return 'm';
case 22: return 0; // Symbol/volume
case 21: return '\r'; // Enter
// Row 4 - Shift Mic Space Sym Shift
case 35: return 0; // Left shift - handled separately
case 34: return 0; // Mic
case 33: return ' '; // Space
case 32: return 0; // Sym
case 31: return 0; // Right shift - handled separately
default: return 0;
}
}
// Map key with Alt modifier for numbers/symbols
char getAltChar(uint8_t keyCode) {
switch (keyCode) {
// Top row with Alt -> numbers
case 10: return '1'; // Q -> 1
case 9: return '2'; // W -> 2
case 8: return '3'; // E -> 3
case 7: return '4'; // R -> 4
case 6: return '5'; // T -> 5
case 5: return '6'; // Y -> 6
case 4: return '7'; // U -> 7
case 3: return '8'; // I -> 8
case 2: return '9'; // O -> 9
case 1: return '0'; // P -> 0
// Common symbols with Alt
case 20: return '@'; // A -> @
case 19: return '#'; // S -> #
case 18: return '$'; // D -> $
case 17: return '%'; // F -> %
case 16: return '&'; // G -> &
case 15: return '*'; // H -> *
case 14: return '-'; // J -> -
case 13: return '+'; // K -> +
case 12: return '='; // L -> =
// More symbols
case 29: return '!'; // Z -> !
case 28: return '?'; // X -> ?
case 27: return ':'; // C -> :
case 26: return ';'; // V -> ;
case 25: return '\''; // B -> '
case 24: return '"'; // N -> "
case 23: return ','; // M -> ,
default: return 0;
}
}
public:
TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire)
: _addr(addr), _wire(wire), _initialized(false),
_shiftActive(false), _altActive(false), _lastShiftTime(0) {}
bool begin() {
// Check if device responds
_wire->beginTransmission(_addr);
if (_wire->endTransmission() != 0) {
Serial.println("TCA8418: Device not found");
return false;
}
// Configure keyboard matrix (8 rows x 10 cols)
writeReg(TCA8418_REG_KP_GPIO1, 0xFF); // Rows 0-7 as keypad
writeReg(TCA8418_REG_KP_GPIO2, 0xFF); // Cols 0-7 as keypad
writeReg(TCA8418_REG_KP_GPIO3, 0x03); // Cols 8-9 as keypad
// Enable keypad with FIFO overflow detection
writeReg(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG
// Set debounce
writeReg(TCA8418_REG_DEBOUNCE, 0x03);
// Clear any pending interrupts
writeReg(TCA8418_REG_INT_STAT, 0x1F);
// Flush the FIFO
while (readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F) {
readReg(TCA8418_REG_KEY_EVENT_A);
}
_initialized = true;
Serial.println("TCA8418: Keyboard initialized OK");
return true;
}
// Read a key press - returns character or 0 if no key
char readKey() {
if (!_initialized) return 0;
// Check for key events in FIFO
uint8_t keyCount = readReg(TCA8418_REG_KEY_LCK_EC) & 0x0F;
if (keyCount == 0) return 0;
// Read key event from FIFO
uint8_t keyEvent = readReg(TCA8418_REG_KEY_EVENT_A);
// Bit 7: 1 = press, 0 = release
bool pressed = (keyEvent & 0x80) != 0;
uint8_t keyCode = keyEvent & 0x7F;
// Clear interrupt
writeReg(TCA8418_REG_INT_STAT, 0x1F);
Serial.printf("KB raw: event=0x%02X code=%d pressed=%d count=%d\n",
keyEvent, keyCode, pressed, keyCount);
// Only act on key press, not release
if (!pressed || keyCode == 0) {
return 0;
}
// Handle modifier keys - set sticky state and return 0
if (keyCode == 35 || keyCode == 31) { // Shift keys
_shiftActive = true;
_lastShiftTime = millis();
Serial.println("KB: Shift activated");
return 0;
}
if (keyCode == 30) { // Alt key
_altActive = true;
Serial.println("KB: Alt activated");
return 0;
}
if (keyCode == 22 || keyCode == 32 || keyCode == 34) { // Sym/Mic - ignore
return 0;
}
// Get the character
char c = 0;
if (_altActive) {
c = getAltChar(keyCode);
_altActive = false; // Reset sticky alt
if (c != 0) {
Serial.printf("KB: Alt+key -> '%c'\n", c);
return c;
}
}
c = getKeyChar(keyCode);
if (c != 0 && _shiftActive) {
// Apply shift - uppercase letters
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
_shiftActive = false; // Reset sticky shift
}
if (c != 0) {
Serial.printf("KB: code %d -> '%c' (0x%02X)\n", keyCode, c >= 32 ? c : '?', c);
} else {
Serial.printf("KB: code %d -> UNMAPPED\n", keyCode);
}
return c;
}
bool isReady() const { return _initialized; }
// Check if shift was pressed within the last N milliseconds
bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const {
return (millis() - _lastShiftTime) < withinMs;
}
};