Compare commits

..

2 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
543390a977
Fix for CoreS3 touch (#529)
The FT6 driver does a hardware ID check, which fails on the CoreS3 but
not on Stackchan for some reason. FT5 works on Stackchan, so we'll use
that for all devices, just like M5GFX does.
2026-06-10 00:24:18 +02:00
Shadowtrance
62266dff58
Tab5 Keyboard (#528) 2026-06-09 23:19:56 +02:00
7 changed files with 583 additions and 9 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 ILI934x FT6x36 AXP2101 AW9523 driver vfs fatfs
REQUIRES Tactility esp_lvgl_port ILI934x FT5x06 AXP2101 AW9523 driver vfs fatfs
)

View File

@ -1,7 +1,7 @@
#include "Display.h"
#include <Axp2101.h>
#include <Ft6x36Touch.h>
#include <Ft5x06Touch.h>
#include <Ili934xDisplay.h>
#include <Tactility/Logger.h>
#include <Tactility/hal/i2c/I2c.h>
@ -17,16 +17,13 @@ static void setBacklightDuty(uint8_t backlightDuty) {
}
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto configuration = std::make_unique<Ft6x36Touch::Configuration>(
auto configuration = std::make_unique<Ft5x06Touch::Configuration>(
I2C_NUM_0,
319,//LCD_HORIZONTAL_RESOLUTION,
239,//LCD_VERTICAL_RESOLUTION,
false,
false,
false
239//LCD_VERTICAL_RESOLUTION,
);
return std::make_shared<Ft6x36Touch>(std::move(configuration));
return std::make_shared<Ft5x06Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {

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