Smart tab5keyboard (#533)

This commit is contained in:
Shadowtrance
2026-06-20 06:14:07 +10:00
committed by GitHub
parent e8b9a1f2a9
commit 594b8bd27e
34 changed files with 528 additions and 137 deletions
@@ -38,8 +38,10 @@ static std::shared_ptr<tt::hal::touch::TouchDevice> createGt911Touch() {
}
static std::shared_ptr<tt::hal::touch::TouchDevice> createSt7123Touch() {
auto* i2c = device_find_by_name("i2c0");
check(i2c, "i2c0 not found");
auto configuration = std::make_unique<St7123Touch::Configuration>(
I2C_NUM_0,
i2c,
720,
1280,
false, // swapXY
@@ -102,7 +102,7 @@ bool Ili9881cDisplay::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, cons
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
.in_color_format = LCD_COLOR_FMT_RGB565,
.out_color_format = LCD_COLOR_FMT_RGB565,
.num_fbs = 1, // TODO: 2?
.num_fbs = 2,
.video_timing =
{
.h_size = 720,
@@ -1,26 +0,0 @@
#include "SdCard.h"
#include <tactility/device.h>
#include <Tactility/hal/sdcard/SpiSdCardDevice.h>
constexpr auto SDCARD_PIN_CS = GPIO_NUM_42;
using tt::hal::sdcard::SpiSdCardDevice;
std::shared_ptr<SdCardDevice> createSdCard() {
auto configuration = std::make_unique<SpiSdCardDevice::Config>(
SDCARD_PIN_CS,
GPIO_NUM_NC,
GPIO_NUM_NC,
GPIO_NUM_NC,
SdCardDevice::MountBehaviour::AtBoot
);
auto* spi_controller = device_find_by_name("spi0");
check(spi_controller, "spi0 not found");
return std::make_shared<SpiSdCardDevice>(
std::move(configuration),
spi_controller
);
}
@@ -1,8 +0,0 @@
#pragma once
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <memory>
using tt::hal::sdcard::SdCardDevice;
std::shared_ptr<SdCardDevice> createSdCard();
@@ -99,7 +99,7 @@ bool St7123Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = 70,
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
.num_fbs = 1,
.num_fbs = 2,
.video_timing = {
.h_size = 720,
.v_size = 1280,
@@ -1,6 +1,7 @@
#include "St7123Touch.h"
#include <Tactility/Logger.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <esp_lcd_touch_st7123.h>
#include <esp_err.h>
@@ -8,11 +9,9 @@ static const auto LOGGER = tt::Logger("ST7123Touch");
bool St7123Touch::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_ST7123_CONFIG();
return esp_lcd_new_panel_io_i2c(
static_cast<esp_lcd_i2c_bus_handle_t>(configuration->port),
&io_config,
&outHandle
) == ESP_OK;
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(configuration->controller);
i2c_master_bus_handle_t bus = esp32_i2c_master_get_bus_handle(configuration->controller);
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, &outHandle) == ESP_OK;
}
bool St7123Touch::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& config, esp_lcd_touch_handle_t& touchHandle) {
@@ -2,7 +2,7 @@
#include <EspLcdTouch.h>
#include <Tactility/TactilityCore.h>
#include <driver/i2c.h>
#include <tactility/device.h>
class St7123Touch final : public EspLcdTouch {
@@ -12,14 +12,14 @@ public:
public:
Configuration(
i2c_port_t port,
::Device* controller,
uint16_t xMax,
uint16_t yMax,
bool swapXy = false,
bool mirrorX = false,
bool mirrorY = false,
gpio_num_t pinInterrupt = GPIO_NUM_NC
) : port(port),
) : controller(controller),
xMax(xMax),
yMax(yMax),
swapXy(swapXy),
@@ -28,7 +28,7 @@ public:
pinInterrupt(pinInterrupt)
{}
i2c_port_t port;
::Device* controller;
uint16_t xMax;
uint16_t yMax;
bool swapXy;
@@ -1,5 +1,7 @@
#include "Tab5Keyboard.h"
#include <Tactility/app/App.h>
#include <Tactility/lvgl/Keyboard.h>
#include <Tactility/lvgl/LvglSync.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <esp_timer.h>
@@ -329,6 +331,113 @@ void Tab5Keyboard::processKeyboard() {
}
}
}
checkAttachState();
}
// ---------------------------------------------------------------------------
// applyAutoRotation - on attach, switches to landscape if not already (saving
// the prior rotation); on detach, restores the saved rotation if we were the
// ones who changed it. Only affects the live LVGL rotation, never persisted
// display settings.
// ---------------------------------------------------------------------------
bool Tab5Keyboard::applyAutoRotation(bool keyboardAttached) {
auto* display = lv_indev_get_display(kbHandle);
if (display == nullptr) {
return false;
}
if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) {
return false; // retry next poll
}
if (keyboardAttached) {
if (lv_display_get_rotation(display) != LV_DISPLAY_ROTATION_90) {
savedRotation = lv_display_get_rotation(display);
rotationOverrideActive = true;
lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90);
}
} else {
// Only restore if rotation is still what we set it to - if the user manually
// changed it since attaching, respect their choice instead.
if (rotationOverrideActive && lv_display_get_rotation(display) == LV_DISPLAY_ROTATION_90) {
lv_display_set_rotation(display, savedRotation);
}
rotationOverrideActive = false;
}
tt::lvgl::unlock();
return true;
}
// ---------------------------------------------------------------------------
// checkAttachState - throttled (~1s) hot-plug detection. Reapplies device
// register configuration and auto-rotation on detach/attach transitions.
// ---------------------------------------------------------------------------
void Tab5Keyboard::checkAttachState() {
static constexpr uint32_t ATTACH_CHECK_TICKS = 50; // ~1s at 20ms/tick
if (++attachCheckTickCounter < ATTACH_CHECK_TICKS) {
return;
}
attachCheckTickCounter = 0;
const bool attached = isAttached();
if (attached == wasAttached) {
pendingAttachConfirmCount = 0;
return;
}
// Require the new state to be confirmed on a second consecutive check before acting -
// a single probe on a floating/half-connected bus (e.g. mid-unplug) can false-positive.
if (attached != pendingAttachState || pendingAttachConfirmCount == 0) {
pendingAttachState = attached;
pendingAttachConfirmCount = 1;
return;
}
pendingAttachConfirmCount = 0;
if (attached) {
reinitDevice();
}
if (!applyAutoRotation(attached)) {
return; // keep prior state so transition is retried
}
wasAttached = attached;
}
// ---------------------------------------------------------------------------
// lateStart - see header comment. Brings up LVGL input handling for a keyboard
// that wasn't attached at boot (startLvgl() wasn't called from attachDevices()).
// ---------------------------------------------------------------------------
bool Tab5Keyboard::lateStart() {
if (kbHandle != nullptr) {
return true; // already started
}
auto* display = lv_display_get_default();
if (display == nullptr) {
return false; // LVGL not ready yet
}
if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) {
return false; // try again on the next attach-state check
}
bool started = startLvgl(display);
if (started) {
tt::lvgl::hardware_keyboard_set_indev(kbHandle);
// redraw() assigns every indev that exists at the time to the active screen's
// input group. This indev didn't exist yet at the last redraw(), so it has no
// group and won't deliver key events until the next app switch. Join the
// current default group now so input works immediately on the visible screen.
lv_indev_set_group(kbHandle, lv_group_get_default());
}
tt::lvgl::unlock();
return started;
}
// ---------------------------------------------------------------------------
@@ -359,21 +468,31 @@ Tab5Keyboard::~Tab5Keyboard() {
}
}
// ---------------------------------------------------------------------------
// reinitDevice - (re)applies the device register configuration. Used at
// startLvgl() and again on hot-plug reattach, since the device's RGB mode and
// interrupt configuration are volatile and reset to power-on defaults when
// the keyboard is unplugged and reconnected.
// ---------------------------------------------------------------------------
void Tab5Keyboard::reinitDevice() {
writeReg(REG_KEYBOARD_MODE, 0x00); // Normal mode
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
updateLeds(); // restore current LED state
if (irqConfigured) {
writeReg(REG_INT_CFG, 0x01); // re-enable Normal-mode interrupt (bit 0)
}
}
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;
@@ -383,13 +502,14 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) {
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;
}
configureIrqPin(); // best-effort; falls back to polling if it fails. Must run before
// reinitDevice() so REG_INT_CFG is written if IRQ setup succeeded.
// Best-effort: if the keyboard isn't attached yet (e.g. started speculatively at
// boot so it can be detected later via hot-plug), these I2C writes fail silently
// and reinitDevice() runs again once attach is detected.
reinitDevice();
kbHandle = lv_indev_create();
lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD);
@@ -397,7 +517,8 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) {
lv_indev_set_display(kbHandle, display);
lv_indev_set_user_data(kbHandle, this);
configureIrqPin(); // best-effort; falls back to polling if it fails
wasAttached = isAttached();
rotationOverrideActive = false;
assert(inputTimer == nullptr);
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, pdMS_TO_TICKS(20), [this] {
@@ -405,6 +526,10 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) {
});
inputTimer->start();
if (wasAttached) {
applyAutoRotation(true);
}
return true;
}
@@ -29,6 +29,16 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice {
volatile bool irqPending = false;
bool irqConfigured = false;
// Hot-plug attach-state polling (piggybacks on the 20ms inputTimer)
bool wasAttached = false;
uint32_t attachCheckTickCounter = 0;
// I2C probes can false-positive on a floating/half-connected bus (e.g. mid-unplug), so a
// state change is only acted on once it's seen on two consecutive ~1s checks in a row.
bool pendingAttachState = false;
uint8_t pendingAttachConfirmCount = 0;
lv_display_rotation_t savedRotation = LV_DISPLAY_ROTATION_0;
bool rotationOverrideActive = false;
// Software key-repeat state (tracked by position to survive modifier changes)
uint32_t repeatKey = 0;
uint8_t repeatRow = 0xFF;
@@ -44,6 +54,10 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice {
void removeIrqPin();
static void IRAM_ATTR irqHandler(void* arg);
void reinitDevice();
bool applyAutoRotation(bool keyboardAttached);
void checkAttachState();
void drainEvents();
void processKeyboard();
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
@@ -62,4 +76,10 @@ public:
bool stopLvgl() override;
bool isAttached() const override;
lv_indev_t* getLvglIndev() override { return kbHandle; }
// Starts LVGL input handling and registers the hardware keyboard indev for a device
// that wasn't attached at boot (so startLvgl() was never called from Lvgl.cpp's
// attachDevices()). Called from the device module's attach-detection timer once the
// keyboard is first detected post-boot. No-op if LVGL input is already started.
bool lateStart();
};