mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-06-19 04:15:06 +00:00
Tab5 Keyboard (#528)
This commit is contained in:
parent
3dcc95f6f3
commit
62266dff58
@ -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
|
||||
)
|
||||
|
||||
@ -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>()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
507
Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp
Normal file
507
Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp
Normal 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 a–z / A–Z
|
||||
if (keycode >= 0x04U && keycode <= 0x1DU) {
|
||||
uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
|
||||
return shift ? (c - 0x20U) : c;
|
||||
}
|
||||
|
||||
// Numbers 1–0 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, ®, 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;
|
||||
}
|
||||
66
Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h
Normal file
66
Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h
Normal 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; }
|
||||
};
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user