2026-06-12 18:46:22 +02:00

437 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Tab5Keyboard.h"
#include <Tactility/app/App.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.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 - use Tactility I2C controller API
// ---------------------------------------------------------------------------
bool Tab5Keyboard::readReg(uint8_t reg, uint8_t& value) {
return i2c_controller_read_register(i2cController, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
bool Tab5Keyboard::writeReg(uint8_t reg, uint8_t value) {
return i2c_controller_write_register(i2cController, I2C_ADDRESS, reg, &value, 1, pdMS_TO_TICKS(50)) == ERROR_NONE;
}
// ---------------------------------------------------------------------------
// LED helpers - LED0 = Sym indicator (green), LED1 = Aa indicator (red)
// 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
i2c_controller_write_register(i2cController, I2C_ADDRESS, REG_RGB_BASE, buf, 7, 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;
}
// 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");
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");
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;
return true;
}
bool Tab5Keyboard::isAttached() const {
return i2c_controller_has_device_at_address(i2cController, I2C_ADDRESS, pdMS_TO_TICKS(100)) == ERROR_NONE;
}