Tab5 Keyboard (#528)

This commit is contained in:
Shadowtrance 2026-06-10 07:19:56 +10:00 committed by GitHub
parent 3dcc95f6f3
commit 62266dff58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 578 additions and 1 deletions

View File

@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver vfs fatfs ina226-module
REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911 PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module
)

View File

@ -1,6 +1,7 @@
#include "devices/Display.h"
#include "devices/SdCard.h"
#include "devices/Power.h"
#include "devices/Tab5Keyboard.h"
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>
@ -17,6 +18,7 @@ static DeviceVector createDevices() {
createPower(),
createDisplay(),
createSdCard(),
std::make_shared<Tab5Keyboard>()
};
}

View File

@ -0,0 +1,507 @@
#include "Tab5Keyboard.h"
#include <Tactility/app/App.h>
#include <esp_timer.h>
#include <lvgl.h>
// ---------------------------------------------------------------------------
// Register addresses
// ---------------------------------------------------------------------------
static constexpr uint8_t REG_INT_CFG = 0x00;
static constexpr uint8_t REG_INT_STAT = 0x01;
static constexpr uint8_t REG_EVENT_NUM = 0x02;
static constexpr uint8_t REG_BRIGHTNESS = 0x03;
static constexpr uint8_t REG_KEYBOARD_MODE = 0x10;
static constexpr uint8_t REG_RGB_MODE = 0x11;
static constexpr uint8_t REG_KEY_EVENT = 0x20;
static constexpr uint8_t REG_RGB_BASE = 0x60;
static constexpr uint8_t KEY_EVENT_EMPTY = 0xFF;
// ---------------------------------------------------------------------------
// Modifier key positions in the 5x14 matrix
// ---------------------------------------------------------------------------
static constexpr uint8_t MOD_ROW_SYM = 3, MOD_COL_SYM = 0;
static constexpr uint8_t MOD_ROW_AA = 3, MOD_COL_AA = 1;
static constexpr uint8_t MOD_ROW_CTRL = 4, MOD_COL_CTRL = 0;
static constexpr uint8_t MOD_ROW_ALT = 4, MOD_COL_ALT = 1;
// ---------------------------------------------------------------------------
// HID lookup tables
// Row-major: index = row * 14 + col, 5 rows x 14 cols = 70 entries.
// modifier 0x02 = Left Shift (pre-baked by firmware for shifted characters).
// ---------------------------------------------------------------------------
struct HidMapping {
uint8_t keycode;
uint8_t modifier;
};
static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = {
// Row 0: Esc 1 2 3 4 5 6 7 8 9 0 - + Del
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: ` ! @ # $ % ^ & * ( ) [ ] backslash
{0x35, 0x00}, {0x1E, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x25, 0x02}, {0x26, 0x02}, {0x27, 0x02}, {0x2F, 0x00},
{0x30, 0x00}, {0x31, 0x00},
// Row 2: Tab q w e r t y u i o p ; ' Backspace
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x00},
{0x34, 0x00}, {0x2A, 0x00},
// Row 3: Sym Aa a s d f g h j k l ↑ _ Enter
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2D, 0x02}, {0x28, 0x00},
// Row 4: Ctrl Alt z x c v b n m . ← ↓ → Space
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x37, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
// Row 0: identical to base
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00},
{0x2E, 0x02}, {0x4C, 0x00},
// Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → |
{0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02},
{0x30, 0x02}, {0x31, 0x02},
// Row 2: Sym deltas: ; → :, ' → "
{0x2B, 0x00}, {0x14, 0x00}, {0x1A, 0x00}, {0x08, 0x00}, {0x15, 0x00}, {0x17, 0x00},
{0x1C, 0x00}, {0x18, 0x00}, {0x0C, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x33, 0x02},
{0x34, 0x02}, {0x2A, 0x00},
// Row 3: Sym delta: _ → =
{0x00, 0x00}, {0x00, 0x00}, {0x04, 0x00}, {0x16, 0x00}, {0x07, 0x00}, {0x09, 0x00},
{0x0A, 0x00}, {0x0B, 0x00}, {0x0D, 0x00}, {0x0E, 0x00}, {0x0F, 0x00}, {0x52, 0x00},
{0x2E, 0x00}, {0x28, 0x00},
// Row 4: Sym delta: . → ,
{0x00, 0x00}, {0x00, 0x00}, {0x1D, 0x00}, {0x1B, 0x00}, {0x06, 0x00}, {0x19, 0x00},
{0x05, 0x00}, {0x11, 0x00}, {0x10, 0x00}, {0x36, 0x00}, {0x50, 0x00}, {0x51, 0x00},
{0x4F, 0x00}, {0x2C, 0x00},
};
// ---------------------------------------------------------------------------
// HID usage code + modifier → LVGL key
// Covers all codes present in the Tab5 matrix tables above.
// ---------------------------------------------------------------------------
static uint32_t tab5TranslateKey(uint8_t keycode, uint8_t modifier, bool ctrl) {
const bool shift = (modifier & 0x22U) != 0U;
// Navigation → LVGL key constants
switch (keycode) {
case 0x29: return LV_KEY_ESC;
case 0x28: return LV_KEY_ENTER;
case 0x2A: return LV_KEY_BACKSPACE;
case 0x4C: return LV_KEY_DEL;
case 0x2B: return '\t';
// Arrows: Ctrl+arrow = focus navigation, plain arrow = raw cursor movement
case 0x52: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_UP;
case 0x51: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_DOWN;
case 0x50: return ctrl ? (uint32_t)LV_KEY_PREV : (uint32_t)LV_KEY_LEFT;
case 0x4F: return ctrl ? (uint32_t)LV_KEY_NEXT : (uint32_t)LV_KEY_RIGHT;
default: break;
}
// Letters az / AZ
if (keycode >= 0x04U && keycode <= 0x1DU) {
uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
return shift ? (c - 0x20U) : c;
}
// Numbers 10 and their shifted symbols
if (keycode >= 0x1EU && keycode <= 0x27U) {
static constexpr char nums[] = "1234567890";
static constexpr char snums[] = "!@#$%^&*()";
return shift ? static_cast<uint32_t>(snums[keycode - 0x1EU])
: static_cast<uint32_t>(nums[keycode - 0x1EU]);
}
// Space and punctuation - all codes present in the Tab5 matrix
switch (keycode) {
case 0x2C: return ' ';
case 0x2D: return shift ? '_' : '-';
case 0x2E: return shift ? '+' : '=';
case 0x2F: return shift ? '{' : '[';
case 0x30: return shift ? '}' : ']';
case 0x31: return shift ? '|' : '\\';
case 0x33: return shift ? ':' : ';';
case 0x34: return shift ? '"' : '\'';
case 0x35: return shift ? '~' : '`';
case 0x36: return shift ? '<' : ',';
case 0x37: return shift ? '>' : '.';
case 0x38: return shift ? '?' : '/';
default: return 0;
}
}
// ---------------------------------------------------------------------------
// I2C helpers - direct i2c_master API (LP_I2C_NUM_0, GPIO 0/1)
// ---------------------------------------------------------------------------
bool Tab5Keyboard::readReg(uint8_t reg, uint8_t& value) const {
if (!i2cDev) return false;
const esp_err_t err = i2c_master_transmit_receive(i2cDev, &reg, 1, &value, 1, pdMS_TO_TICKS(50));
return err == ESP_OK;
}
bool Tab5Keyboard::writeReg(uint8_t reg, uint8_t value) const {
if (!i2cDev) return false;
const uint8_t buf[2] = { reg, value };
const esp_err_t err = i2c_master_transmit(i2cDev, buf, 2, pdMS_TO_TICKS(50));
return err == ESP_OK;
}
// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (amber)
// RGB register layout: [B, G, R] per LED, stride 4 (byte 3 reserved)
// ---------------------------------------------------------------------------
void Tab5Keyboard::updateLeds() {
// [LED0: B,G,R, reserved, LED1: B,G,R]
uint8_t buf[7] = {
0x00, symActive ? uint8_t(0xA0) : uint8_t(0x00), 0x00, 0x00, // LED0: green if Sym
0x00, 0x00, aaSticky ? uint8_t(0xA0) : uint8_t(0x00), // LED1: red if Aa latched
};
// Write 7-byte block starting at REG_RGB_BASE
const uint8_t reg = REG_RGB_BASE;
uint8_t tx[8];
tx[0] = reg;
for (int i = 0; i < 7; i++) tx[i + 1] = buf[i];
i2c_master_transmit(i2cDev, tx, 8, pdMS_TO_TICKS(50));
}
// ---------------------------------------------------------------------------
// IRQ pin - GPIO 50, active-low, falling edge
// ---------------------------------------------------------------------------
void IRAM_ATTR Tab5Keyboard::irqHandler(void* arg) {
auto* self = static_cast<Tab5Keyboard*>(arg);
self->irqPending = true;
}
bool Tab5Keyboard::configureIrqPin() {
gpio_config_t io_conf{};
io_conf.pin_bit_mask = (1ULL << INT_PIN);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.intr_type = GPIO_INTR_NEGEDGE;
if (gpio_config(&io_conf) != ESP_OK) {
return false;
}
const esp_err_t svc = gpio_install_isr_service(0);
if (svc != ESP_OK && svc != ESP_ERR_INVALID_STATE) {
return false;
}
if (gpio_isr_handler_add(INT_PIN, irqHandler, this) != ESP_OK) {
gpio_set_intr_type(INT_PIN, GPIO_INTR_DISABLE);
return false;
}
irqConfigured = true;
return true;
}
void Tab5Keyboard::removeIrqPin() {
if (!irqConfigured) return;
gpio_isr_handler_remove(INT_PIN);
irqConfigured = false;
irqPending = false;
}
// ---------------------------------------------------------------------------
// drainEvents - reads all pending events from the device queue
// ---------------------------------------------------------------------------
void Tab5Keyboard::drainEvents() {
uint8_t count = 0;
if (!readReg(REG_EVENT_NUM, count) || count == 0) {
return;
}
while (count > 0) {
uint8_t raw = 0;
if (!readReg(REG_KEY_EVENT, raw) || raw == KEY_EVENT_EMPTY) {
break;
}
const bool pressed = (raw & 0x80U) != 0U;
const uint8_t row = (raw >> 4U) & 0x07U;
const uint8_t col = raw & 0x0FU;
// Modifier keys: update state, no key output
if (row == MOD_ROW_SYM && col == MOD_COL_SYM) {
symActive = pressed;
updateLeds();
count--;
continue;
}
if (row == MOD_ROW_AA && col == MOD_COL_AA) {
if (pressed) {
aaHeld = true;
aaTapped = true; // assume tap until a real key is pressed while held
} else {
// Only latch sticky if no non-modifier key was pressed during this hold
if (aaTapped) {
aaSticky = !aaSticky;
}
aaHeld = false;
aaTapped = false;
}
updateLeds();
count--;
continue;
}
if (row == MOD_ROW_CTRL && col == MOD_COL_CTRL) {
ctrlHeld = pressed;
count--;
continue;
}
if (row == MOD_ROW_ALT && col == MOD_COL_ALT) {
count--;
continue;
}
if (row < 5U && col < 14U) {
const bool aaActive = aaHeld || aaSticky;
const HidMapping& m = symActive
? KEY_MATRIX_HID_SYM[row * 14U + col]
: KEY_MATRIX_HID_BASE[row * 14U + col];
if (m.keycode != 0U) {
const uint8_t modifier = static_cast<uint8_t>(m.modifier | (aaActive ? 0x02U : 0U));
const uint32_t lv_key = tab5TranslateKey(m.keycode, modifier, ctrlHeld);
if (lv_key != 0U) {
if (pressed) {
// A real key was pressed — this hold is a chord, not a tap
aaTapped = false;
if (lv_key == LV_KEY_ESC) {
tt::app::stop();
} else {
xQueueSend(queue, &lv_key, 0);
// Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now_ms = static_cast<uint32_t>(esp_timer_get_time() / 1000);
repeatKey = lv_key;
repeatRow = row;
repeatCol = col;
repeatStartMs = now_ms;
repeatLastMs = 0;
// Consume sticky Aa after one keypress
if (aaSticky) {
aaSticky = false;
aaHeld = false;
updateLeds();
}
}
} else if (row == repeatRow && col == repeatCol) {
// Match release by position, not translated value — survives sticky Aa clear
repeatKey = 0;
}
}
}
}
count--;
}
// Clear INT status after draining so the line de-asserts
writeReg(REG_INT_STAT, 0x00);
}
// ---------------------------------------------------------------------------
// processKeyboard - called from 20ms Timer; IRQ-gated when INT is wired
// ---------------------------------------------------------------------------
void Tab5Keyboard::processKeyboard() {
bool shouldDrain = false;
if (irqConfigured) {
if (irqPending) {
irqPending = false;
shouldDrain = true;
}
} else {
// Polling: check INT_STA first — bit 0 = Normal mode event pending
uint8_t status = 0;
if (readReg(REG_INT_STAT, status) && (status & 0x01U)) {
shouldDrain = true;
}
}
if (shouldDrain) {
drainEvents();
}
// Software key-repeat (runs every tick regardless of IRQ)
if (repeatKey != 0U) {
const uint32_t now_ms = static_cast<uint32_t>(esp_timer_get_time() / 1000);
if ((now_ms - repeatStartMs) >= REPEAT_INITIAL_MS) {
const uint32_t last = repeatLastMs;
if (last == 0 || (now_ms - last) >= REPEAT_RATE_MS) {
repeatLastMs = now_ms;
xQueueSend(queue, &repeatKey, 0);
}
}
}
}
// ---------------------------------------------------------------------------
// LVGL read callback - called from the LVGL task
// ---------------------------------------------------------------------------
void Tab5Keyboard::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto* self = static_cast<Tab5Keyboard*>(lv_indev_get_user_data(indev));
uint32_t lv_key = 0;
if (xQueueReceive(self->queue, &lv_key, 0) == pdTRUE) {
data->key = lv_key;
data->state = LV_INDEV_STATE_PRESSED;
data->continue_reading = (uxQueueMessagesWaiting(self->queue) > 0);
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
// ---------------------------------------------------------------------------
// KeyboardDevice interface
// ---------------------------------------------------------------------------
Tab5Keyboard::~Tab5Keyboard() {
if (inputTimer) {
stopLvgl(); // tears down LVGL indev, IRQ, I2C bus
}
if (queue) {
vQueueDelete(queue);
queue = nullptr;
}
}
bool Tab5Keyboard::startLvgl(lv_display_t* display) {
if (!queue) {
LOG_E("Tab5Keyboard", "Input queue allocation failed — cannot start");
return false;
}
// Create LP I2C master bus (LP_I2C_NUM_0, GPIO 0/1) via new i2c_master API
i2c_master_bus_config_t bus_cfg = {
.i2c_port = LP_I2C_NUM_0,
.sda_io_num = GPIO_NUM_0,
.scl_io_num = GPIO_NUM_1,
.clk_source = static_cast<i2c_clock_source_t>(LP_I2C_SCLK_DEFAULT),
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = { .enable_internal_pullup = true },
};
if (i2c_new_master_bus(&bus_cfg, &i2cBus) != ESP_OK) {
LOG_E("Tab5Keyboard", "Failed to create LP I2C master bus");
return false;
}
i2c_device_config_t dev_cfg = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = I2C_ADDRESS,
.scl_speed_hz = 100000,
};
if (i2c_master_bus_add_device(i2cBus, &dev_cfg, &i2cDev) != ESP_OK) {
LOG_E("Tab5Keyboard", "Failed to add keyboard device to LP I2C bus");
i2c_del_master_bus(i2cBus);
i2cBus = nullptr;
return false;
}
// Set Normal mode explicitly — device may power up in a different mode
if (!writeReg(REG_KEYBOARD_MODE, 0x00)) {
LOG_E("Tab5Keyboard", "Failed to set keyboard mode");
i2c_master_bus_rm_device(i2cDev);
i2c_del_master_bus(i2cBus);
i2cDev = nullptr;
i2cBus = nullptr;
return false;
}
writeReg(REG_EVENT_NUM, 0x00); // flush event queue
writeReg(REG_INT_STAT, 0x00); // clear pending INT
writeReg(REG_RGB_MODE, 0x01); // Custom RGB mode (manual LED control)
writeReg(REG_BRIGHTNESS, 50); // 50% brightness
symActive = false;
aaSticky = false;
aaHeld = false;
aaTapped = false;
ctrlHeld = false;
repeatKey = 0;
repeatRow = 0xFF;
repeatCol = 0xFF;
repeatLastMs = 0;
updateLeds(); // both LEDs off initially
// Enable Normal-mode interrupt (bit 0)
if (!writeReg(REG_INT_CFG, 0x01)) {
LOG_E("Tab5Keyboard", "Failed to configure interrupt register");
i2c_master_bus_rm_device(i2cDev);
i2c_del_master_bus(i2cBus);
i2cDev = nullptr;
i2cBus = nullptr;
return false;
}
kbHandle = lv_indev_create();
lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(kbHandle, readCallback);
lv_indev_set_display(kbHandle, display);
lv_indev_set_user_data(kbHandle, this);
configureIrqPin(); // best-effort; falls back to polling if it fails
assert(inputTimer == nullptr);
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, pdMS_TO_TICKS(20), [this] {
processKeyboard();
});
inputTimer->start();
return true;
}
bool Tab5Keyboard::stopLvgl() {
if (!inputTimer) {
return false; // Not started
}
inputTimer->stop();
inputTimer = nullptr;
removeIrqPin();
if (queue) {
xQueueReset(queue); // discard unread keycodes so a restart begins with an empty buffer
}
writeReg(REG_INT_CFG, 0x00); // disable all interrupts
symActive = false;
aaSticky = false;
aaHeld = false;
updateLeds(); // turn LEDs off
lv_indev_delete(kbHandle);
kbHandle = nullptr;
if (i2cDev) {
i2c_master_bus_rm_device(i2cDev);
i2cDev = nullptr;
}
if (i2cBus) {
i2c_del_master_bus(i2cBus);
i2cBus = nullptr;
}
return true;
}
bool Tab5Keyboard::isAttached() const {
// If already started, just probe via the open bus handle
if (i2cBus) {
return i2c_master_probe(i2cBus, I2C_ADDRESS, pdMS_TO_TICKS(100)) == ESP_OK;
}
// Otherwise open a temporary bus to probe (LP I2C is not accessible via legacy API)
i2c_master_bus_config_t bus_cfg = {
.i2c_port = LP_I2C_NUM_0,
.sda_io_num = GPIO_NUM_0,
.scl_io_num = GPIO_NUM_1,
.clk_source = static_cast<i2c_clock_source_t>(LP_I2C_SCLK_DEFAULT),
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = { .enable_internal_pullup = true },
};
i2c_master_bus_handle_t probe_bus = nullptr;
if (i2c_new_master_bus(&bus_cfg, &probe_bus) != ESP_OK) {
return false;
}
const esp_err_t ret = i2c_master_probe(probe_bus, I2C_ADDRESS, pdMS_TO_TICKS(100));
i2c_del_master_bus(probe_bus);
return ret == ESP_OK;
}

View File

@ -0,0 +1,66 @@
#pragma once
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tactility/Timer.h>
#include <driver/i2c_master.h>
#include <driver/gpio.h>
#include <freertos/queue.h>
class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice {
static constexpr uint8_t I2C_ADDRESS = 0x6D;
static constexpr gpio_num_t INT_PIN = GPIO_NUM_50;
// Software key-repeat timing
static constexpr uint32_t REPEAT_INITIAL_MS = 400;
static constexpr uint32_t REPEAT_RATE_MS = 80;
i2c_master_bus_handle_t i2cBus = nullptr;
i2c_master_dev_handle_t i2cDev = nullptr;
lv_indev_t* kbHandle = nullptr;
QueueHandle_t queue = nullptr;
std::unique_ptr<tt::Timer> inputTimer;
bool symActive = false; // held while Sym is physically down
bool aaSticky = false; // latched on single Aa tap, cleared after next non-modifier key
bool aaHeld = false; // true while Aa is physically held
bool aaTapped = false; // no non-modifier key was pressed while Aa was held
bool ctrlHeld = false; // true while Ctrl is physically held
// IRQ-driven event gating
volatile bool irqPending = false;
bool irqConfigured = false;
// Software key-repeat state (tracked by position to survive modifier changes)
uint32_t repeatKey = 0;
uint8_t repeatRow = 0xFF;
uint8_t repeatCol = 0xFF;
uint32_t repeatStartMs = 0;
uint32_t repeatLastMs = 0;
bool readReg(uint8_t reg, uint8_t& value) const;
bool writeReg(uint8_t reg, uint8_t value) const;
void updateLeds();
bool configureIrqPin();
void removeIrqPin();
static void IRAM_ATTR irqHandler(void* arg);
void drainEvents();
void processKeyboard();
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
public:
Tab5Keyboard() {
queue = xQueueCreate(20, sizeof(uint32_t));
// queue == nullptr on OOM; startLvgl() checks and refuses to start
}
~Tab5Keyboard() override; // defined in .cpp: stops active session, then vQueueDelete(queue)
std::string getName() const override { return "Tab5Keyboard"; }
std::string getDescription() const override { return "M5Stack Tab5 Keyboard addon"; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
bool isAttached() const override;
lv_indev_t* getLvglIndev() override { return kbHandle; }
};

View File

@ -48,3 +48,5 @@ CONFIG_CACHE_L2_CACHE_256KB=y
CONFIG_LVGL_PORT_ENABLE_PPA=y
CONFIG_LV_DRAW_BUF_ALIGN=64
CONFIG_LV_DEF_REFR_PERIOD=15
# Allow new i2c_master API (used by Tab5Keyboard for LP_I2C_NUM_0) to coexist with legacy i2c driver
CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK=y