Compare commits

...

2 Commits

Author SHA1 Message Date
Shadowtrance
594b8bd27e
Smart tab5keyboard (#533) 2026-06-19 22:14:07 +02:00
Ken Van Hoeylandt
e8b9a1f2a9
Fixes and improvements (#534) 2026-06-19 20:46:56 +02:00
46 changed files with 596 additions and 186 deletions

View File

@ -26,7 +26,6 @@ macro(tactility_project project_name)
set(COMPONENTS
TactilityFreeRtos
bm8563-module
bm8563-module
bmi270-module
mpu6886-module
pi4ioe5v6408-module

View File

@ -40,8 +40,8 @@
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaRx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 1 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 1 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;

View File

@ -26,8 +26,8 @@
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaRx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 1 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 1 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_0>;
i2cClockFrequency = <400000>;

View File

@ -41,8 +41,8 @@
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaRx = <&gpio0 32 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 33 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 32 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 33 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;

View File

@ -46,8 +46,8 @@
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaRx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 1 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 1 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
@ -56,8 +56,8 @@
port_b: grove1 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_UART>;
pinSdaRx = <&gpio0 9 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 8 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 9 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 8 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
@ -66,8 +66,8 @@
port_c: grove2 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_UART>;
pinSdaRx = <&gpio0 17 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 18 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 17 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 18 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_2>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;

View File

@ -80,8 +80,8 @@
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaRx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 1 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 2 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 1 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_2>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
@ -90,8 +90,8 @@
port_b: grove1 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_UART>;
pinSdaRx = <&gpio0 9 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 8 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 9 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 8 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_2>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
@ -100,8 +100,8 @@
port_c: grove2 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_DISABLED>;
pinSdaRx = <&gpio0 17 GPIO_FLAG_NONE>;
pinSclTx = <&gpio0 18 GPIO_FLAG_NONE>;
pinSdaTx = <&gpio0 17 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 18 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_2>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;

View File

@ -1,5 +1,4 @@
#include "devices/Display.h"
#include "devices/SdCard.h"
#include "devices/Power.h"
#include "devices/Tab5Keyboard.h"
@ -13,11 +12,11 @@ using namespace tt::hal;
static constexpr auto* TAG = "Tab5";
static DeviceVector createDevices() {
::Device* i2c2 = device_find_by_name("i2c2");
auto* i2c2 = device_find_by_name("i2c2");
check(i2c2, "i2c2 not found");
return {
createPower(),
createDisplay(),
createSdCard(),
std::make_shared<Tab5Keyboard>(i2c2)
};
}

View File

@ -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

View File

@ -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,

View File

@ -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
);
}

View File

@ -1,8 +0,0 @@
#pragma once
#include <Tactility/hal/sdcard/SdCardDevice.h>
#include <memory>
using tt::hal::sdcard::SdCardDevice;
std::shared_ptr<SdCardDevice> createSdCard();

View File

@ -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,

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}

View File

@ -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();
};

View File

@ -3,6 +3,10 @@
#include <tactility/drivers/gpio_controller.h>
#include <tactility/log.h>
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include "devices/Tab5Keyboard.h"
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
@ -13,14 +17,17 @@
constexpr auto GPIO_EXP0_PIN_SPEAKER_ENABLE = 1;
constexpr auto GPIO_EXP0_PIN_HEADPHONE_DETECT = 7;
constexpr auto HP_DETECT_POLL_MS = 1000;
constexpr auto KB_DETECT_POLL_MS = 1000;
// hp_detect_timer is only touched from start()/stop(), which are called serially
// by the module manager — no atomic needed for the handle itself.
// hp_detect_timer and kb_detect_timer are only touched from start()/stop(), which are called
// serially by the module manager — no atomic needed for the handles themselves.
static TimerHandle_t hp_detect_timer = nullptr;
static TimerHandle_t kb_detect_timer = nullptr;
static std::atomic<Device*> io_expander0_cached { nullptr };
// Flags are written by the timer daemon task and read by start()/stop() — use atomics.
static std::atomic<bool> hp_detect_last { false };
static std::atomic<bool> hp_detect_initialized { false };
static std::atomic<bool> kb_late_started { false };
static void headphoneDetectCallback(TimerHandle_t /*timer*/) {
Device* cached = io_expander0_cached.load(std::memory_order_acquire);
@ -68,6 +75,40 @@ static void headphoneDetectCallback(TimerHandle_t /*timer*/) {
}
}
// Detects a Tab5 Keyboard add-on that was plugged in after boot (so it wasn't started by
// Lvgl.cpp's attachDevices()). Once lateStart() succeeds, this stops polling for good — there's
// no support for re-detecting after the indev is torn down again.
static void keyboardDetectCallback(TimerHandle_t timer) {
if (kb_late_started.load(std::memory_order_acquire)) {
xTimerStop(timer, 0);
return;
}
using namespace tt::hal;
auto keyboard = findFirstDevice<keyboard::KeyboardDevice>(tt::hal::Device::Type::Keyboard);
if (!keyboard) {
return; // Not registered yet, will retry on next tick
}
if (keyboard->getLvglIndev() != nullptr) {
// Already started (boot-time attach) — nothing left to do.
kb_late_started.store(true, std::memory_order_release);
xTimerStop(timer, 0);
return;
}
if (!keyboard->isAttached()) {
return; // Not plugged in yet, will retry on next tick
}
auto tab5_keyboard = std::static_pointer_cast<Tab5Keyboard>(keyboard);
if (tab5_keyboard->lateStart()) {
LOG_I(TAG, "kb_detect: keyboard attached post-boot, LVGL input started");
kb_late_started.store(true, std::memory_order_release);
xTimerStop(timer, 0);
}
}
extern "C" {
static error_t start() {
@ -91,23 +132,48 @@ static error_t start() {
hp_detect_timer = nullptr;
return ERROR_RESOURCE;
}
kb_late_started = false;
kb_detect_timer = xTimerCreate("kb_detect", pdMS_TO_TICKS(KB_DETECT_POLL_MS), pdTRUE, nullptr, keyboardDetectCallback);
if (!kb_detect_timer) {
LOG_E(TAG, "Failed to create kb_detect timer");
return ERROR_RESOURCE;
}
if (xTimerStart(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to start kb_detect timer");
xTimerDelete(kb_detect_timer, pdMS_TO_TICKS(100));
kb_detect_timer = nullptr;
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t stop() {
if (hp_detect_timer == nullptr) {
return ERROR_NONE;
if (hp_detect_timer != nullptr) {
if (xTimerStop(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop hp_detect timer");
}
if (xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to delete hp_detect timer");
}
// Always clear the handle — stale non-null handle is worse than a resource leak,
// as it would cause start() to silently skip re-creating the timer.
hp_detect_timer = nullptr;
io_expander0_cached.store(nullptr, std::memory_order_release);
}
if (xTimerStop(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop hp_detect timer");
if (kb_detect_timer != nullptr) {
if (xTimerStop(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_W(TAG, "Failed to stop kb_detect timer");
}
if (xTimerDelete(kb_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to delete kb_detect timer");
}
kb_detect_timer = nullptr;
}
if (xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) {
LOG_E(TAG, "Failed to delete hp_detect timer");
}
// Always clear the handle — stale non-null handle is worse than a resource leak,
// as it would cause start() to silently skip re-creating the timer.
hp_detect_timer = nullptr;
io_expander0_cached.store(nullptr, std::memory_order_release);
return ERROR_NONE;
}

View File

@ -3,10 +3,10 @@
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_grove.h>
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbhost.h>
#include <bindings/bmi270.h>
@ -28,11 +28,11 @@
};
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c";
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <100000>;
pin-sda = <&gpio0 31 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 32 GPIO_FLAG_NONE>;
pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>;
io_expander0 {
compatible = "diodes,pi4ioe5v6408";
@ -61,12 +61,14 @@
};
};
i2c_port_a: i2c1 {
compatible = "espressif,esp32-i2c";
port = <I2C_NUM_1>;
clock-frequency = <100000>;
pin-sda = <&gpio0 53 GPIO_FLAG_NONE>;
pin-scl = <&gpio0 54 GPIO_FLAG_NONE>;
port_a: grove0 {
compatible = "espressif,esp32-grove";
defaultMode = <GROVE_MODE_I2C>;
pinSdaTx = <&gpio0 53 GPIO_FLAG_NONE>;
pinSclRx = <&gpio0 54 GPIO_FLAG_NONE>;
uartPort = <UART_NUM_1>;
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <100000>;
};
i2c_keyboard: i2c2 {
@ -78,12 +80,18 @@
pin-scl = <&gpio0 1 GPIO_FLAG_PULL_UP>;
};
sdcard_spi: spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
pin-mosi = <&gpio0 44 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 39 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 43 GPIO_FLAG_NONE>;
sdmmc0 {
compatible = "espressif,esp32-sdmmc";
pin-clk = <&gpio0 43 GPIO_FLAG_NONE>;
pin-cmd = <&gpio0 44 GPIO_FLAG_NONE>;
pin-d0 = <&gpio0 39 GPIO_FLAG_NONE>;
pin-d1 = <&gpio0 40 GPIO_FLAG_NONE>;
pin-d2 = <&gpio0 41 GPIO_FLAG_NONE>;
pin-d3 = <&gpio0 42 GPIO_FLAG_NONE>;
bus-width = <4>;
slot = <SDMMC_HOST_SLOT_0>;
max-freq-khz = <SDMMC_FREQ_HIGHSPEED>;
on-chip-ldo-chan = <4>;
};
// ES8388 and ES7210
@ -97,14 +105,6 @@
pin-mclk = <&gpio0 30 GPIO_FLAG_NONE>;
};
uart_port_a: uart1 {
compatible = "espressif,esp32-uart";
status = "disabled";
port = <UART_NUM_1>;
pin-tx = <&gpio0 53 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 54 GPIO_FLAG_NONE>;
};
usbhost0 {
compatible = "espressif,esp32-usbhost";
peripheral-map = <0>;

View File

@ -4,12 +4,16 @@
### Beitian BH-222Q
Currently not working.
115200 baud, U-Blox 10 clone, defaults to binary messages
https://www.beitian.com/en/pd.jsp?id=1677
### Beitian BN-357ZF
Configure as MTK L76B in GPS settings, or auto-detect as MTK L76K.
9600 baud, L76K clone
https://www.beitian.com/en/sys-pd/879.html
@ -20,7 +24,9 @@ https://www.beitian.com/en/sys-pd/879.html
https://www.datasheethub.com/gy-neo6mv2-flight-control-gps-module/
### M5Stack GPS
### M5Stack Mini GPS (AT6558)
Configure as ATGM336H.
9600 baud, AT6558 + MAX2659 (LNA)

View File

@ -12,8 +12,8 @@
## Higher Priority
- Make a root device type so it can be discovered more easily.
- When device.py selects a new device, it should automatically delete the build dirs (build/, cmake-*/) when it detects that the platform has changed.
- Require either SD card present or at least 8MB of flash. This way we can increase firmware size, ensure performance, and have a single location for storing data.
- Consider storing data on SD card in `/tactility` folder (for usage with Launcher)
- Add font design tokens such as "regular", "title" and "smaller". Perhaps via the LVGL kernel module.
- Add kernel listening mechanism so that the root device init can be notified when a device becomes available:
Callback for device/start stop with filtering on device type:
@ -24,13 +24,11 @@
- DTS: support for #defines
- DTS: support for aliases
- SPI kernel driver
- iomux kernel driver
- Kernel concepts for ELF loading (generic approach for GUI apps, console apps, libraries).
- Fix glitches when installing app via App Hub with 4.3" Waveshare
- TCA9534 keyboards should use interrupts
- GT911 drivers should use interrupts if it's stable
- Fix Cardputer (original): use LV_KEY_NEXT and _PREV in keyboard mapping instead of encoder driver hack (and check GPIO app if it then hangs too)
- Logging with a function that uses std::format
- Expose http::download() and main dispatcher to TactiltyC.
- External app loading: Check the version of Tactility and check ESP target hardware to check for compatibility
Check during installation process, but also when starting (SD card might have old app install from before Tactility OS update)
@ -59,15 +57,11 @@
- Make WiFi setup app that starts an access point and hosts a webpage to set up the device.
This will be useful for devices without a screen, a small screen or a non-touch screen.
- Unify the way displays are dimmed. Some implementations turn off the display when it's fully dimmed. Make this a separate functionality.
- Try out ILI9342 https://github.com/jbrilha/esp_lcd_ili9342
- All drivers (e.g. display, touch, etc.) should call stop() in their destructor, or at least assert that they should not be running.
- Bug: Turn on WiFi (when testing it wasn't connected/connecting - just active). Open chat. Observe crash.
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
- I2C app should show error when I2C port is disabled when the scan button was manually pressed
## Lower Priority
- The boot button on some devices can be used as GPIO_NUM_0 at runtime
- Localize all apps
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
- Explore LVGL9's FreeRTOS functionality
@ -97,7 +91,6 @@
- Audio recording app
- OTA updates
- T-Deck Plus: Create separate device config?
- Support for displays with different DPI. Consider the layer-based system like on Android.
- If present, use LED to show boot/wifi status
- Capacity based on voltage: estimation for various devices uses a linear voltage curve, but it should use a battery discharge curve.
- Wrapper for lvgl slider widget that shows "+" and "-" buttons, and also the value in a label.
@ -121,14 +114,5 @@
- IR transceiver app
- GPS app
- Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/
- Compile unix tools to ELF apps?
- Todo list
- Calendar
- Display touch calibration
- RSS reader
- Static file web server (with option to specify path and port)
- Port TamaFi https://github.com/cifertech/TamaFi
# Notes on firmware size
- adding esp_http_client (with esp_event) added about 100kB

View File

@ -3,6 +3,9 @@
#include <Tactility/hal/display/DisplayDriver.h>
#include <esp_lcd_panel_ops.h>
#if CONFIG_SOC_MIPI_DSI_SUPPORTED
#include <esp_lcd_mipi_dsi.h>
#endif
class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver {
@ -32,4 +35,13 @@ public:
uint16_t getPixelWidth() const override { return hRes; }
uint16_t getPixelHeight() const override { return vRes; }
#if CONFIG_SOC_MIPI_DSI_SUPPORTED
uint8_t getFrameBuffers(void* outBuffers[2]) const override {
if (outBuffers == nullptr) {
return 0;
}
return (esp_lcd_dpi_panel_get_frame_buffer(panelHandle, 2, &outBuffers[0], &outBuffers[1]) == ESP_OK) ? 2 : 0;
}
#endif
};

View File

@ -7,14 +7,14 @@ properties:
type: int
required: true
description: "One of enum Esp32GroveMode"
pinSdaRx:
pinSdaTx:
type: phandle-array
required: true
description: SDA (I2C) or RX (UART) pin
pinSclTx:
description: SDA (I2C) or TX (UART) pin
pinSclRx:
type: phandle-array
required: true
description: SCL (I2C) or TX (UART) pin
description: SCL (I2C) or RX (UART) pin
uartPort:
type: int
required: true

View File

@ -43,6 +43,16 @@ properties:
type: int
required: true
description: Bus width in bits
slot:
type: int
default: 1
enum: [0, 1]
description: SDMMC host slot number (SDMMC_HOST_SLOT_0 or SDMMC_HOST_SLOT_1). On ESP32-P4, slot 0 uses the dedicated (non-GPIO-matrix) pins.
max-freq-khz:
type: int
default: 20000
minimum: 1
description: Maximum SDMMC clock frequency in kHz (e.g. 40000 for SDMMC_FREQ_HIGHSPEED)
wp-active-high:
type: boolean
default: false

View File

@ -12,8 +12,8 @@ extern "C" {
struct Esp32GroveConfig {
enum GroveMode defaultMode;
struct GpioPinSpec pinSdaRx;
struct GpioPinSpec pinSclTx;
struct GpioPinSpec pinSdaTx;
struct GpioPinSpec pinSclRx;
uart_port_t uartPort;
i2c_port_t i2cPort;
uint32_t i2cClockFrequency;

View File

@ -2,6 +2,8 @@
#pragma once
#include <tactility/drivers/gpio.h>
#include <tactility/device.h>
#include <driver/i2c_types.h>
#include <driver/i2c_master.h>
#ifdef __cplusplus
@ -18,8 +20,18 @@ struct Esp32I2cMasterConfig {
struct GpioPinSpec pinScl;
};
/**
* Returns the i2c_master_bus_handle_t for an esp32_i2c_master Device.
* The device must be started. Returns NULL if the device has no driver data.
*/
i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(struct Device* device);
/**
* Returns the SCL clock frequency (Hz) configured for an esp32_i2c_master Device.
* Reads directly from the device tree config, so the device does not need to be started.
*/
uint32_t esp32_i2c_master_get_clock_frequency(struct Device* device);
#ifdef __cplusplus
}
#endif

View File

@ -3,6 +3,7 @@
#include <soc/soc_caps.h>
#if SOC_SDMMC_HOST_SUPPORTED
#include <driver/sdmmc_default_configs.h>
#include <sd_protocol_types.h>
#include <stdbool.h>
#include <stdint.h>
@ -26,6 +27,8 @@ struct Esp32SdmmcConfig {
struct GpioPinSpec pin_cd;
struct GpioPinSpec pin_wp;
uint8_t bus_width;
int32_t slot;
int32_t max_freq_khz;
bool wp_active_high;
bool enable_uhs;
bool pullups;

View File

@ -108,8 +108,8 @@ static error_t start_child(Device* device, GroveMode mode) {
}
std::memset(uart_cfg, 0, sizeof(Esp32UartConfig));
uart_cfg->port = config->uartPort;
uart_cfg->pin_tx = config->pinSclTx;
uart_cfg->pin_rx = config->pinSdaRx;
uart_cfg->pin_rx = config->pinSclRx;
uart_cfg->pin_tx = config->pinSdaTx;
uart_cfg->pin_cts = GPIO_PIN_SPEC_NONE;
uart_cfg->pin_rts = GPIO_PIN_SPEC_NONE;
data->child_config = uart_cfg;
@ -131,8 +131,8 @@ static error_t start_child(Device* device, GroveMode mode) {
std::memset(i2c_cfg, 0, sizeof(Esp32I2cMasterConfig));
i2c_cfg->port = static_cast<i2c_port_num_t>(config->i2cPort);
i2c_cfg->clockFrequency = config->i2cClockFrequency;
i2c_cfg->pinSda = config->pinSdaRx;
i2c_cfg->pinScl = config->pinSclTx;
i2c_cfg->pinSda = config->pinSdaTx;
i2c_cfg->pinScl = config->pinSclRx;
// New driver seems to require pull-up setting
i2c_cfg->pinSda.flags |= GPIO_FLAG_PULL_UP;
i2c_cfg->pinScl.flags |= GPIO_FLAG_PULL_UP;

View File

@ -286,6 +286,14 @@ static error_t stop(Device* device) {
return ERROR_NONE;
}
i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(struct Device* device) {
return GET_DATA(device)->bus_handle;
}
uint32_t esp32_i2c_master_get_clock_frequency(struct Device* device) {
return GET_CONFIG(device)->clockFrequency;
}
static constexpr I2cControllerApi ESP32_I2C_MASTER_API = {
.read = read,
.write = write,
@ -308,8 +316,4 @@ Driver esp32_i2c_master_driver = {
.internal = nullptr
};
i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(Device* device) {
return GET_DATA(device)->bus_handle;
}
} // extern "C"

View File

@ -10,6 +10,8 @@
#include <driver/sdmmc_host.h>
#include <esp_vfs_fat.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <sdmmc_cmd.h>
#include <string>
@ -76,6 +78,8 @@ static error_t mount(void* data) {
};
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
host.slot = config->slot;
host.max_freq_khz = config->max_freq_khz;
#if SOC_SD_PWR_CTRL_SUPPORTED
// Treat non-positive values as disabled to remain safe with zero-initialized configs.
@ -89,6 +93,10 @@ static error_t mount(void* data) {
return ERROR_NOT_SUPPORTED;
}
host.pwr_ctrl_handle = fs_data->pwr_ctrl_handle;
// On cold boot the SD card needs time for its supply rail to ramp up after the
// on-chip LDO is enabled, otherwise the initial ACMD41 (send_op_cond) times out.
vTaskDelay(pdMS_TO_TICKS(10));
}
#endif

View File

@ -24,6 +24,14 @@ public:
virtual uint16_t getPixelWidth() const = 0;
virtual uint16_t getPixelHeight() const = 0;
virtual bool drawBitmap(int xStart, int yStart, int xEnd, int yEnd, const void* pixelData) = 0;
/**
* Returns direct pointers to the panel's hardware frame buffer(s), if the
* underlying driver supports it (DPI/MIPI-DSI panels only).
* @param[out] outBuffers receives up to 2 frame buffer pointers
* @return number of buffers written to outBuffers (0 if unsupported)
*/
virtual uint8_t getFrameBuffers(void* outBuffers[2]) const { return 0; }
};
}

View File

@ -15,7 +15,7 @@ enum class BootMode {
Flash
};
bool startMassStorageWithSdmmc();
bool startMassStorageWithSdmmc(bool fromBootMode = false);
void stop();
Mode getMode();
bool isSupported();
@ -28,7 +28,7 @@ void resetUsbBootMode();
BootMode getUsbBootMode();
// Flash-based mass storage
bool startMassStorageWithFlash();
bool startMassStorageWithFlash(bool fromBootMode = false);
bool canRebootIntoMassStorageFlash();
void rebootIntoMassStorageFlash();

View File

@ -1,7 +1,7 @@
#pragma once
bool tusbIsSupported();
bool tusbStartMassStorageWithSdmmc();
bool tusbStartMassStorageWithFlash();
bool tusbStartMassStorageWithSdmmc(bool fromBootMode = false);
bool tusbStartMassStorageWithFlash(bool fromBootMode = false);
void tusbStop();
bool tusbCanStartMassStorageWithFlash();

View File

@ -34,7 +34,6 @@
#include <tactility/lvgl_module.h>
#ifdef ESP_PLATFORM
#include <tactility/drivers/root.h>
#include <Tactility/InitEsp.h>
#endif

View File

@ -16,9 +16,12 @@
#include <lvgl.h>
#include <atomic>
#ifdef ESP_PLATFORM
#include "Tactility/app/crashdiagnostics/CrashDiagnostics.h"
#include <Tactility/kernel/PanicHandler.h>
#include <esp_system.h>
#include <sdkconfig.h>
#else
#define CONFIG_TT_SPLASH_DURATION 0
@ -36,6 +39,11 @@ static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
class BootApp : public App {
// Snapshot of hal::usb::isUsbBootMode(), taken before the boot thread starts and
// potentially clears the underlying flag via setupUsbBootMode()/resetUsbBootMode().
// onShow() reads this instead of the live flag to avoid a race between the two.
static std::atomic<bool> isUsbBootSplash;
Thread thread = Thread(
"boot",
5120,
@ -76,12 +84,12 @@ class BootApp : public App {
auto mode = hal::usb::getUsbBootMode(); // Get mode before reset
hal::usb::resetUsbBootMode();
if (mode == hal::usb::BootMode::Flash) {
if (!hal::usb::startMassStorageWithFlash()) {
if (!hal::usb::startMassStorageWithFlash(true)) {
LOGGER.error("Unable to start flash mass storage");
return false;
}
} else if (mode == hal::usb::BootMode::Sdmmc) {
if (!hal::usb::startMassStorageWithSdmmc()) {
if (!hal::usb::startMassStorageWithSdmmc(true)) {
LOGGER.error("Unable to start SD mass storage");
return false;
}
@ -174,6 +182,9 @@ class BootApp : public App {
public:
void onCreate(AppContext& app) override {
// Snapshot before the boot thread potentially clears the flag via setupUsbBootMode()
isUsbBootSplash = hal::usb::isUsbBootMode();
// Just in case this app is somehow resumed
if (thread.getState() == Thread::State::Stopped) {
thread.start();
@ -197,16 +208,31 @@ public:
const char* logo;
// TODO: Replace with automatic asset buckets like on Android
if (getSmallestDimension() < 150) { // e.g. Cardputer
logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo_small.png";
logo = isUsbBootSplash ? "logo_usb.png" : "logo_small.png";
} else {
logo = hal::usb::isUsbBootMode() ? "logo_usb.png" : "logo.png";
logo = isUsbBootSplash ? "logo_usb.png" : "logo.png";
}
const auto logo_path = lvgl::PATH_PREFIX + paths->getAssetsPath(logo);
LOGGER.info("{}", logo_path);
lv_image_set_src(image, logo_path.c_str());
#ifdef ESP_PLATFORM
if (isUsbBootSplash) {
auto* button = lv_button_create(parent);
lv_obj_align(button, LV_ALIGN_BOTTOM_MID, 0, -16);
auto* label = lv_label_create(button);
lv_label_set_text(label, "Return to OS");
lv_obj_add_event_cb(button, [](lv_event_t*) {
hal::usb::stop();
esp_restart();
}, LV_EVENT_SHORT_CLICKED, nullptr);
}
#endif
}
};
std::atomic<bool> BootApp::isUsbBootSplash = false;
extern const AppManifest manifest = {
.appId = "Boot",
.appName = "Boot",

View File

@ -232,7 +232,7 @@ void I2cScannerApp::onScanTimer() {
return;
}
for (uint8_t address = 0; address < 128; ++address) {
for (uint8_t address = 1; address < 128; ++address) {
if (i2c_controller_has_device_at_address(safe_port, address, 10 / portTICK_PERIOD_MS) == ERROR_NONE) {
logger.info("Found device at address 0x{:02X}", address);
if (!shouldStopScanTimer()) {

View File

@ -26,6 +26,11 @@ static uint32_t getButtonPadding(UiDensity density, uint32_t buttonSize) {
}
}
static int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
const int32_t usable = std::max<int32_t>(0, available_span - (3 * total_button_size));
return std::min<int32_t>(usable / 16, total_button_size / 2);
}
class LauncherApp final : public App {
static lv_obj_t* createAppButton(lv_obj_t* parent, UiDensity uiDensity, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) {
@ -84,6 +89,50 @@ class LauncherApp final : public App {
}
}
// The screen object outlives the launcher's views (it's recreated by GuiService::redraw()
// via lv_obj_clean() on every app switch), so the LV_EVENT_SIZE_CHANGED callback registered
// on it must be removed once buttons_wrapper is destroyed, to avoid a dangling user-data
// pointer on the next rotation while a different app is visible.
static void onButtonsWrapperDeleted(lv_event_t* e) {
auto* buttons_wrapper = lv_event_get_target_obj(e);
auto* screen = lv_obj_get_screen(buttons_wrapper);
lv_obj_remove_event_cb_with_user_data(screen, onButtonsWrapperResized, buttons_wrapper);
}
// Re-applies the flex direction and per-button margins when the display orientation
// changes while the launcher is the visible app (these are decided once at onShow()
// based on the resolution at that time, so a later rotation needs this to catch up).
static void onButtonsWrapperResized(lv_event_t* e) {
auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
const auto* display = lv_obj_get_display(buttons_wrapper);
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(lvgl_get_ui_density(), button_size);
const auto total_button_size = button_size + (button_padding * 2);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
const auto current_flow = lv_obj_get_style_flex_flow(buttons_wrapper, LV_PART_MAIN);
const bool was_landscape = current_flow == LV_FLEX_FLOW_ROW;
if (is_landscape_display == was_landscape) {
return;
}
lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
const int32_t margin = is_landscape_display
? computeButtonMargin(horizontal_px, total_button_size)
: computeButtonMargin(vertical_px, total_button_size);
const uint32_t child_count = lv_obj_get_child_count(buttons_wrapper);
for (uint32_t i = 0; i < child_count; i++) {
auto* button = lv_obj_get_child(buttons_wrapper, i);
lv_obj_set_style_margin_hor(button, is_landscape_display ? margin : 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_ver(button, is_landscape_display ? 0 : margin, LV_STATE_DEFAULT);
}
}
public:
void onCreate(AppContext& app) override {
@ -133,19 +182,20 @@ public:
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
}
int32_t margin;
if (is_landscape_display) {
const int32_t available_width = std::max<int32_t>(0, lv_display_get_horizontal_resolution(display) - (3 * total_button_size));
margin = std::min<int32_t>(available_width / 16, total_button_size / 2);
} else {
const int32_t available_height = std::max<int32_t>(0, lv_display_get_vertical_resolution(display) - (3 * total_button_size));
margin = std::min<int32_t>(available_height / 16, total_button_size / 2);
}
const int32_t margin = is_landscape_display
? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
: computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "AppList", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "Files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "Settings", margin, is_landscape_display);
// The launcher's container is several levels below the screen, and LVGL only sends
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
// handler is attached there, with buttons_wrapper passed through as user data.
lv_obj_add_event_cb(lv_obj_get_screen(parent), onButtonsWrapperResized, LV_EVENT_SIZE_CHANGED, buttons_wrapper);
lv_obj_add_event_cb(buttons_wrapper, onButtonsWrapperDeleted, LV_EVENT_DELETE, nullptr);
if (shouldShowPowerButton()) {
auto* power_button = lv_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);

View File

@ -69,13 +69,13 @@ bool isSupported() {
return tusbIsSupported();
}
bool startMassStorageWithSdmmc() {
bool startMassStorageWithSdmmc(bool fromBootMode) {
if (!canStartNewMode()) {
LOGGER.error("Can't start");
return false;
}
if (tusbStartMassStorageWithSdmmc()) {
if (tusbStartMassStorageWithSdmmc(fromBootMode)) {
currentMode = Mode::MassStorageSdmmc;
return true;
} else {
@ -110,13 +110,13 @@ void rebootIntoMassStorageSdmmc() {
}
// NEW: Flash mass storage functions
bool startMassStorageWithFlash() {
bool startMassStorageWithFlash(bool fromBootMode) {
if (!canStartNewMode()) {
LOGGER.error("Can't start flash mass storage");
return false;
}
if (tusbStartMassStorageWithFlash()) {
if (tusbStartMassStorageWithFlash(fromBootMode)) {
currentMode = Mode::MassStorageFlash;
return true;
} else {

View File

@ -4,7 +4,7 @@
namespace tt::hal::usb {
bool startMassStorageWithSdmmc() { return false; }
bool startMassStorageWithSdmmc(bool /*fromBootMode*/) { return false; }
void stop() {}
Mode getMode() { return Mode::Default; }
BootMode getUsbBootMode() { return BootMode::None; }
@ -12,7 +12,7 @@ bool isSupported() { return false; }
bool canRebootIntoMassStorageSdmmc() { return false; }
void rebootIntoMassStorageSdmmc() {}
bool startMassStorageWithFlash() { return false; }
bool startMassStorageWithFlash(bool /*fromBootMode*/) { return false; }
bool canRebootIntoMassStorageFlash() { return false; }
void rebootIntoMassStorageFlash() {}
bool isUsbBootMode() { return false; }

View File

@ -8,6 +8,9 @@
#if CONFIG_TINYUSB_MSC_ENABLED == 1
#include <Tactility/Logger.h>
#include <esp_system.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <wear_levelling.h>
@ -26,6 +29,10 @@ namespace tt::hal::usb {
extern sdmmc_card_t* getCard();
}
// Set when mass storage was started as part of the dedicated reboot-into-MSC boot flow.
// Used to decide whether ejecting the volume should automatically reboot back to normal OS.
static bool startedFromBootMode = false;
enum {
ITF_NUM_MSC = 0,
ITF_NUM_TOTAL
@ -99,6 +106,15 @@ static uint8_t const msc_hs_configuration_desc[] = {
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
if (event->mount_changed_data.is_mounted) {
LOGGER.info("MSC Mounted");
// Storage is only (re)mounted into our own filesystem after the host sends a SCSI
// START STOP UNIT eject (see tud_msc_start_stop_cb() in tusb_msc_storage.c). Windows
// is known not to send this reliably, so this is a best-effort path for hosts that do
// (e.g. Linux/macOS) - the "Return to OS" button on the boot screen is the primary one.
// If we got here while booted into MSC mode, it's safe to reboot back into normal OS now.
if (startedFromBootMode) {
LOGGER.info("MSC ejected by host, rebooting into normal OS");
esp_restart();
}
} else {
LOGGER.info("MSC Unmounted");
}
@ -147,8 +163,11 @@ static bool ensureDriverInstalled() {
bool tusbIsSupported() { return true; }
bool tusbStartMassStorageWithSdmmc() {
ensureDriverInstalled();
bool tusbStartMassStorageWithSdmmc(bool fromBootMode) {
if (!ensureDriverInstalled()) {
return false;
}
startedFromBootMode = fromBootMode;
auto* card = tt::hal::usb::getCard();
if (card == nullptr) {
@ -179,9 +198,12 @@ bool tusbStartMassStorageWithSdmmc() {
return result == ESP_OK;
}
bool tusbStartMassStorageWithFlash() {
bool tusbStartMassStorageWithFlash(bool fromBootMode) {
LOGGER.info("Starting flash MSC");
ensureDriverInstalled();
if (!ensureDriverInstalled()) {
return false;
}
startedFromBootMode = fromBootMode;
wl_handle_t handle = tt::getDataPartitionWlHandle();
if (handle == WL_INVALID_HANDLE) {
@ -212,6 +234,12 @@ bool tusbStartMassStorageWithFlash() {
}
void tusbStop() {
// Actively signal a disconnect to the host before tearing down the peripheral, otherwise
// a subsequent esp_restart() resets the chip too fast for the host to notice the device
// went away, leaving it stuck showing the old MSC device until the cable is replugged.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(250));
tinyusb_msc_storage_deinit();
#if CONFIG_IDF_TARGET_ESP32P4
usb_wrap_ll_phy_select(&USB_WRAP, 1);
@ -225,8 +253,8 @@ bool tusbCanStartMassStorageWithFlash() {
#else
bool tusbIsSupported() { return false; }
bool tusbStartMassStorageWithSdmmc() { return false; }
bool tusbStartMassStorageWithFlash() { return false; }
bool tusbStartMassStorageWithSdmmc(bool /*fromBootMode*/) { return false; }
bool tusbStartMassStorageWithFlash(bool /*fromBootMode*/) { return false; }
void tusbStop() {}
bool tusbCanStartMassStorageWithFlash() { return false; }

View File

@ -17,6 +17,10 @@ if (DEFINED ENV{ESP_IDF_VERSION})
list(APPEND PRIV_REQUIRES_LIST elf_loader)
endif ()
if (IDF_TARGET STREQUAL "esp32p4")
list(APPEND PRIV_REQUIRES_LIST esp_driver_ppa esp_mm)
endif ()
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
tactility_add_module(TactilityC

View File

@ -86,6 +86,15 @@ uint16_t tt_hal_display_driver_get_pixel_height(DisplayDriverHandle handle);
*/
void tt_hal_display_driver_draw_bitmap(DisplayDriverHandle handle, int xStart, int yStart, int xEnd, int yEnd, const void* pixelData);
/**
* Get direct pointers to the display's hardware frame buffer(s), if supported.
* Only available for panels with direct CPU-addressable frame buffers (e.g. MIPI-DSI/DPI).
* @param[in] handle the display driver handle
* @param[out] outBuffers receives up to 2 frame buffer pointers
* @return number of buffers written to outBuffers (0 if unsupported)
*/
uint8_t tt_hal_display_driver_get_frame_buffers(DisplayDriverHandle handle, void* outBuffers[2]);
#ifdef __cplusplus
}
#endif

View File

@ -135,6 +135,10 @@ int __gtdf2(double a, double b);
// GCC integer/bitwise helpers (compiler-rt)
int __clzsi2(unsigned int x);
// GCC 64-bit integer arithmetic helpers (needed for 64-bit div on 32-bit RISC-V)
long long __divdi3(long long a, long long b);
unsigned long long __udivdi3(unsigned long long a, unsigned long long b);
} // extern "C"
const esp_elfsym gcc_soft_float_symbols[] = {
@ -261,6 +265,10 @@ const esp_elfsym gcc_soft_float_symbols[] = {
// GCC integer/bitwise helpers
ESP_ELFSYM_EXPORT(__clzsi2),
// GCC 64-bit integer arithmetic helpers
ESP_ELFSYM_EXPORT(__divdi3),
ESP_ELFSYM_EXPORT(__udivdi3),
ESP_ELFSYM_END
};

View File

@ -85,4 +85,9 @@ void tt_hal_display_driver_draw_bitmap(DisplayDriverHandle handle, int xStart, i
wrapper->driver->drawBitmap(xStart, yStart, xEnd, yEnd, pixelData);
}
uint8_t tt_hal_display_driver_get_frame_buffers(DisplayDriverHandle handle, void* outBuffers[2]) {
auto wrapper = static_cast<DriverWrapper*>(handle);
return wrapper->driver->getFrameBuffers(outBuffers);
}
}

View File

@ -62,9 +62,15 @@
#include <driver/i2s_std.h>
#include <driver/gpio.h>
#ifdef CONFIG_IDF_TARGET_ESP32P4
#include <driver/ppa.h>
#include <esp_cache.h>
#endif
extern "C" {
extern double __floatsidf(int x);
extern void _esp_error_check_failed(esp_err_t rc, const char *file, int line, const char *function, const char *expression);
const esp_elfsym main_symbols[] {
// stdlib.h
@ -77,11 +83,13 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(rand_r),
ESP_ELFSYM_EXPORT(atoi),
ESP_ELFSYM_EXPORT(atol),
ESP_ELFSYM_EXPORT(system),
// esp random
ESP_ELFSYM_EXPORT(esp_random),
ESP_ELFSYM_EXPORT(esp_fill_random),
// esp other
ESP_ELFSYM_EXPORT(__floatsidf),
ESP_ELFSYM_EXPORT(_esp_error_check_failed),
// unistd.h
ESP_ELFSYM_EXPORT(usleep),
ESP_ELFSYM_EXPORT(sleep),
@ -205,6 +213,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(fwrite),
ESP_ELFSYM_EXPORT(getc),
ESP_ELFSYM_EXPORT(putc),
ESP_ELFSYM_EXPORT(putchar),
ESP_ELFSYM_EXPORT(puts),
ESP_ELFSYM_EXPORT(printf),
ESP_ELFSYM_EXPORT(sscanf),
@ -212,6 +221,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(sprintf),
ESP_ELFSYM_EXPORT(vsprintf),
ESP_ELFSYM_EXPORT(vsnprintf),
ESP_ELFSYM_EXPORT(vfprintf),
// cstring
ESP_ELFSYM_EXPORT(strlen),
ESP_ELFSYM_EXPORT(strcmp),
@ -236,6 +246,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(memcmp),
ESP_ELFSYM_EXPORT(memchr),
ESP_ELFSYM_EXPORT(memmove),
ESP_ELFSYM_EXPORT(strdup),
// ctype
ESP_ELFSYM_EXPORT(isalnum),
@ -296,6 +307,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(tt_hal_display_driver_lock),
ESP_ELFSYM_EXPORT(tt_hal_display_driver_unlock),
ESP_ELFSYM_EXPORT(tt_hal_display_driver_supported),
ESP_ELFSYM_EXPORT(tt_hal_display_driver_get_frame_buffers),
ESP_ELFSYM_EXPORT(tt_hal_touch_driver_supported),
ESP_ELFSYM_EXPORT(tt_hal_touch_driver_alloc),
ESP_ELFSYM_EXPORT(tt_hal_touch_driver_free),
@ -363,6 +375,8 @@ const esp_elfsym main_symbols[] {
// stdio.h
ESP_ELFSYM_EXPORT(rename),
ESP_ELFSYM_EXPORT(rewind),
ESP_ELFSYM_EXPORT(remove),
// dirent.h
ESP_ELFSYM_EXPORT(opendir),
ESP_ELFSYM_EXPORT(closedir),
@ -439,6 +453,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(heap_caps_get_allocated_size),
ESP_ELFSYM_EXPORT(heap_caps_get_free_size),
ESP_ELFSYM_EXPORT(heap_caps_get_largest_free_block),
ESP_ELFSYM_EXPORT(heap_caps_aligned_alloc),
ESP_ELFSYM_EXPORT(heap_caps_malloc),
ESP_ELFSYM_EXPORT(heap_caps_calloc),
ESP_ELFSYM_EXPORT(heap_caps_free),
@ -449,6 +464,14 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(esp_timer_start_periodic),
ESP_ELFSYM_EXPORT(esp_timer_start_once),
ESP_ELFSYM_EXPORT(esp_timer_get_time),
#ifdef CONFIG_IDF_TARGET_ESP32P4
// driver/ppa.h
ESP_ELFSYM_EXPORT(ppa_register_client),
ESP_ELFSYM_EXPORT(ppa_unregister_client),
ESP_ELFSYM_EXPORT(ppa_do_scale_rotate_mirror),
// esp_cache.h
ESP_ELFSYM_EXPORT(esp_cache_msync),
#endif
// delimiter
ESP_ELFSYM_END
};

View File

@ -12,6 +12,7 @@
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/drivers/usb_host_msc.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/grove.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/i2s_controller.h>
#include <tactility/drivers/root.h>
@ -85,6 +86,10 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(gpio_controller_init_descriptors),
DEFINE_MODULE_SYMBOL(gpio_controller_deinit_descriptors),
DEFINE_MODULE_SYMBOL(GPIO_CONTROLLER_TYPE),
// drivers/grove
DEFINE_MODULE_SYMBOL(grove_set_mode),
DEFINE_MODULE_SYMBOL(grove_get_mode),
DEFINE_MODULE_SYMBOL(GROVE_TYPE),
// drivers/i2c_controller
DEFINE_MODULE_SYMBOL(i2c_controller_read),
DEFINE_MODULE_SYMBOL(i2c_controller_write),

View File

@ -1,5 +1,7 @@
import configparser
import glob
import os
import shutil
import sys
from configparser import ConfigParser
@ -392,14 +394,42 @@ def write_properties(output_file, device_properties: ConfigParser, device_id: st
write_custom_sdkconfig(output_file, device_properties)
write_lvgl_variables(output_file, device_properties)
def get_current_sdkconfig_target(sdkconfig_path: str):
if not os.path.isfile(sdkconfig_path):
return None
with open(sdkconfig_path, "r") as f:
for line in f:
if line.startswith("CONFIG_IDF_TARGET="):
return line.split("=", 1)[1].strip().strip('"')
return None
def clean_build_dirs_on_platform_change(previous_target: str, new_target: str):
if previous_target is None or previous_target == new_target:
return
dirs_to_remove = []
if os.path.isdir("build"):
dirs_to_remove.append("build")
for d in glob.glob("cmake-build-*/"):
dirs_to_remove.append(d.rstrip("/"))
if not dirs_to_remove:
return
print(f"Platform changed ({previous_target} -> {new_target}), removing build dirs:")
for d in dirs_to_remove:
print(f" {d}")
shutil.rmtree(d)
def main(device_id: str, is_dev: bool):
device_properties_path = get_properties_file_path(device_id)
if not os.path.isfile(device_properties_path):
exit_with_error(f"{device_id} is not a valid device identifier (could not found {device_properties_path})")
output_file_path = "sdkconfig"
# Clean build dirs if target changes
device_properties = read_device_properties(device_id)
new_target = get_property_or_exit(device_properties, "hardware", "target").lower()
sdkconfig_target = get_current_sdkconfig_target(output_file_path)
clean_build_dirs_on_platform_change(sdkconfig_target, new_target)
if os.path.isfile(output_file_path):
os.remove(output_file_path)
device_properties = read_device_properties(device_id)
with open(output_file_path, "w") as output_file:
write_properties(output_file, device_properties, device_id, is_dev)
if is_dev: