From c36fe9246a7f7031a541c48088a90ba59ccb6431 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 13 Jun 2026 12:30:02 +1000 Subject: [PATCH 01/16] smart keyboard --- Devices/m5stack-tab5/Source/Configuration.cpp | 25 ++++++ .../Source/devices/Tab5Keyboard.cpp | 83 +++++++++++++++++++ .../Source/devices/Tab5Keyboard.h | 10 +++ Tactility/Source/app/launcher/Launcher.cpp | 55 ++++++++++++ 4 files changed, 173 insertions(+) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index dd9dec53a..e7d35e248 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include +#include using namespace tt::hal; @@ -273,6 +276,28 @@ static bool initBoot() { LOG_E(TAG, "Failed to init ES7210"); } + // If the keyboard is attached and no display orientation has been saved yet, default to + // landscape (the keyboard add-on is used with the device on its side). Runs at BootSplash, + // once LVGL and the keyboard's isAttached() state are both available, and only applies on + // first boot - an existing saved orientation (e.g. user preference) is left untouched. + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + auto keyboard = findFirstDevice(tt::hal::Device::Type::Keyboard); + if (keyboard == nullptr || !keyboard->isAttached()) { + return; + } + + tt::settings::display::DisplaySettings displaySettings; + if (tt::settings::display::load(displaySettings)) { + return; + } + + displaySettings = tt::settings::display::getDefault(); + displaySettings.orientation = tt::settings::display::Orientation::Landscape; + lv_display_set_rotation(lv_display_get_default(), tt::settings::display::toLvglDisplayRotation(displaySettings.orientation)); + tt::settings::display::save(displaySettings); + LOG_I(TAG, "Keyboard attached: defaulting display orientation to landscape"); + }); + return true; } diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index 1901fae84..4bd94b117 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -1,5 +1,6 @@ #include "Tab5Keyboard.h" #include +#include #include #include #include @@ -329,6 +330,66 @@ 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. +// --------------------------------------------------------------------------- +void Tab5Keyboard::applyAutoRotation(bool keyboardAttached) { + auto* display = lv_indev_get_display(kbHandle); + if (display == nullptr) { + return; + } + + if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) { + return; // try again on the next attach-state check + } + + 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(); +} + +// --------------------------------------------------------------------------- +// 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) { + return; + } + wasAttached = attached; + + if (attached) { + reinitDevice(); + } + applyAutoRotation(attached); } // --------------------------------------------------------------------------- @@ -359,6 +420,25 @@ 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"); @@ -399,6 +479,9 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) { configureIrqPin(); // best-effort; falls back to polling if it fails + wasAttached = isAttached(); + rotationOverrideActive = false; + assert(inputTimer == nullptr); inputTimer = std::make_unique(tt::Timer::Type::Periodic, pdMS_TO_TICKS(20), [this] { processKeyboard(); diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h index d05538434..25b5fb0ef 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h @@ -29,6 +29,12 @@ 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; + 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 +50,10 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice { void removeIrqPin(); static void IRAM_ATTR irqHandler(void* arg); + void reinitDevice(); + void applyAutoRotation(bool keyboardAttached); + void checkAttachState(); + void drainEvents(); void processKeyboard(); static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 4d362778f..b3ebdcfee 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -84,6 +84,55 @@ 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_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); + + int32_t margin; + if (is_landscape_display) { + const int32_t available_width = std::max(0, horizontal_px - (3 * total_button_size)); + margin = std::min(available_width / 16, total_button_size / 2); + } else { + const int32_t available_height = std::max(0, vertical_px - (3 * total_button_size)); + margin = std::min(available_height / 16, total_button_size / 2); + } + + 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 { @@ -146,6 +195,12 @@ class LauncherApp final : public App { 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); From 90ddd383d87cc8ee66ee4fc980d9864b1a8f646f Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 13 Jun 2026 12:34:18 +1000 Subject: [PATCH 02/16] detect after boot --- .../Source/devices/Tab5Keyboard.cpp | 63 +++++++++---- .../Source/devices/Tab5Keyboard.h | 6 ++ Devices/m5stack-tab5/Source/module.cpp | 92 ++++++++++++++++--- 3 files changed, 131 insertions(+), 30 deletions(-) diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index 4bd94b117..34fa25c4e 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -1,5 +1,6 @@ #include "Tab5Keyboard.h" #include +#include #include #include #include @@ -392,6 +393,44 @@ void Tab5Keyboard::checkAttachState() { applyAutoRotation(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(); + + if (started) { + applyAutoRotation(true); + } + + return started; +} + // --------------------------------------------------------------------------- // LVGL read callback - called from the LVGL task // --------------------------------------------------------------------------- @@ -445,15 +484,6 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) { 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; @@ -463,13 +493,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); @@ -477,8 +508,6 @@ 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; diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h index 25b5fb0ef..944b48891 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h @@ -72,4 +72,10 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice { 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(); }; diff --git a/Devices/m5stack-tab5/Source/module.cpp b/Devices/m5stack-tab5/Source/module.cpp index 17174a07a..3b7a1f647 100644 --- a/Devices/m5stack-tab5/Source/module.cpp +++ b/Devices/m5stack-tab5/Source/module.cpp @@ -3,6 +3,10 @@ #include #include +#include + +#include "devices/Tab5Keyboard.h" + #include #include @@ -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 io_expander0_cached { nullptr }; // Flags are written by the timer daemon task and read by start()/stop() — use atomics. static std::atomic hp_detect_last { false }; static std::atomic hp_detect_initialized { false }; +static std::atomic 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(kb_detect_timer, 0); + return; + } + + using namespace tt::hal; + auto keyboard = findFirstDevice(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(kb_detect_timer, 0); + return; + } + + if (!keyboard->isAttached()) { + return; // Not plugged in yet, will retry on next tick + } + + auto tab5_keyboard = std::static_pointer_cast(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(kb_detect_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 (xTimerStop(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) { - LOG_W(TAG, "Failed to stop hp_detect timer"); + 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 (xTimerDelete(hp_detect_timer, pdMS_TO_TICKS(100)) != pdPASS) { - LOG_E(TAG, "Failed to delete 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; } - // 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; } From 510711c25f741a22a886d6a2c2be85beaa6e3c8c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 13 Jun 2026 13:30:12 +1000 Subject: [PATCH 03/16] fixes n stuff --- Devices/m5stack-tab5/Source/Configuration.cpp | 6 ++++- Tactility/Source/app/launcher/Launcher.cpp | 27 ++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index e7d35e248..ffa8ca825 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include using namespace tt::hal; @@ -293,7 +294,10 @@ static bool initBoot() { displaySettings = tt::settings::display::getDefault(); displaySettings.orientation = tt::settings::display::Orientation::Landscape; - lv_display_set_rotation(lv_display_get_default(), tt::settings::display::toLvglDisplayRotation(displaySettings.orientation)); + if (tt::lvgl::lock()) { + lv_display_set_rotation(lv_display_get_default(), tt::settings::display::toLvglDisplayRotation(displaySettings.orientation)); + tt::lvgl::unlock(); + } tt::settings::display::save(displaySettings); LOG_I(TAG, "Keyboard attached: defaulting display orientation to landscape"); }); diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index b3ebdcfee..3d99474e8 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -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(0, available_span - (3 * total_button_size)); + return std::min(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) { @@ -116,14 +121,9 @@ class LauncherApp final : public App { lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN); - int32_t margin; - if (is_landscape_display) { - const int32_t available_width = std::max(0, horizontal_px - (3 * total_button_size)); - margin = std::min(available_width / 16, total_button_size / 2); - } else { - const int32_t available_height = std::max(0, vertical_px - (3 * total_button_size)); - margin = std::min(available_height / 16, total_button_size / 2); - } + 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++) { @@ -182,14 +182,9 @@ class LauncherApp final : public App { 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(0, lv_display_get_horizontal_resolution(display) - (3 * total_button_size)); - margin = std::min(available_width / 16, total_button_size / 2); - } else { - const int32_t available_height = std::max(0, lv_display_get_vertical_resolution(display) - (3 * total_button_size)); - margin = std::min(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); From 8b1de3917ebc0a95d887980fe873334db7a3d384 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sat, 13 Jun 2026 15:23:58 +1000 Subject: [PATCH 04/16] tab5 sdmmc, hal display get fb, symbols --- Devices/m5stack-tab5/Source/Configuration.cpp | 2 -- .../Source/devices/Ili9881cDisplay.cpp | 2 +- .../m5stack-tab5/Source/devices/SdCard.cpp | 26 ------------------ Devices/m5stack-tab5/Source/devices/SdCard.h | 8 ------ .../Source/devices/St7123Display.cpp | 2 +- Devices/m5stack-tab5/m5stack,tab5.dts | 20 +++++++++----- .../EspLcdCompat/Source/EspLcdDisplayDriver.h | 8 ++++++ .../bindings/espressif,esp32-sdmmc.yaml | 8 ++++++ .../include/tactility/drivers/esp32_sdmmc.h | 3 +++ .../source/drivers/esp32_sdmmc_fs.cpp | 8 ++++++ .../Tactility/hal/display/DisplayDriver.h | 8 ++++++ TactilityC/CMakeLists.txt | 4 +++ TactilityC/Include/tt_hal_display.h | 9 +++++++ TactilityC/Source/tt_hal_display.cpp | 5 ++++ TactilityC/Source/tt_init.cpp | 27 +++++++++++++++++++ 15 files changed, 95 insertions(+), 45 deletions(-) delete mode 100644 Devices/m5stack-tab5/Source/devices/SdCard.cpp delete mode 100644 Devices/m5stack-tab5/Source/devices/SdCard.h diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index ffa8ca825..15363ed48 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -1,5 +1,4 @@ #include "devices/Display.h" -#include "devices/SdCard.h" #include "devices/Power.h" #include "devices/Tab5Keyboard.h" @@ -22,7 +21,6 @@ static DeviceVector createDevices() { return { createPower(), createDisplay(), - createSdCard(), std::make_shared(i2c2) }; } diff --git a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp index 0ba634267..9c9d464bd 100644 --- a/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp +++ b/Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp @@ -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, diff --git a/Devices/m5stack-tab5/Source/devices/SdCard.cpp b/Devices/m5stack-tab5/Source/devices/SdCard.cpp deleted file mode 100644 index 639eed882..000000000 --- a/Devices/m5stack-tab5/Source/devices/SdCard.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "SdCard.h" - -#include -#include - -constexpr auto SDCARD_PIN_CS = GPIO_NUM_42; - -using tt::hal::sdcard::SpiSdCardDevice; - -std::shared_ptr createSdCard() { - auto configuration = std::make_unique( - 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( - std::move(configuration), - spi_controller - ); -} diff --git a/Devices/m5stack-tab5/Source/devices/SdCard.h b/Devices/m5stack-tab5/Source/devices/SdCard.h deleted file mode 100644 index 98b222fa6..000000000 --- a/Devices/m5stack-tab5/Source/devices/SdCard.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include -#include - -using tt::hal::sdcard::SdCardDevice; - -std::shared_ptr createSdCard(); diff --git a/Devices/m5stack-tab5/Source/devices/St7123Display.cpp b/Devices/m5stack-tab5/Source/devices/St7123Display.cpp index f1fd991eb..a5d0869f4 100644 --- a/Devices/m5stack-tab5/Source/devices/St7123Display.cpp +++ b/Devices/m5stack-tab5/Source/devices/St7123Display.cpp @@ -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, diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 060b15c5c..059f2939f 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include @@ -78,12 +78,18 @@ pin-scl = <&gpio0 1 GPIO_FLAG_PULL_UP>; }; - sdcard_spi: spi0 { - compatible = "espressif,esp32-spi"; - 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 = ; + max-freq-khz = ; + on-chip-ldo-chan = <4>; }; // ES8388 and ES7210 diff --git a/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h b/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h index ce4b91376..8138f33d1 100644 --- a/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h +++ b/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h @@ -3,6 +3,7 @@ #include #include +#include class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { @@ -32,4 +33,11 @@ class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { uint16_t getPixelWidth() const override { return hRes; } uint16_t getPixelHeight() const override { return vRes; } + + 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; + } }; diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml index e2acf679f..a97aafbce 100644 --- a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml +++ b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml @@ -43,6 +43,14 @@ properties: type: int required: true description: Bus width in bits + slot: + type: int + default: 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 + description: Maximum SDMMC clock frequency in kHz (e.g. 40000 for SDMMC_FREQ_HIGHSPEED) wp-active-high: type: boolean default: false diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h index 40beb383f..35ed84ca5 100644 --- a/Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_sdmmc.h @@ -3,6 +3,7 @@ #include #if SOC_SDMMC_HOST_SUPPORTED +#include #include #include #include @@ -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; diff --git a/Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp b/Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp index 6dbcb3785..c3dff17e3 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_sdmmc_fs.cpp @@ -10,6 +10,8 @@ #include #include +#include +#include #include #include @@ -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 diff --git a/Tactility/Include/Tactility/hal/display/DisplayDriver.h b/Tactility/Include/Tactility/hal/display/DisplayDriver.h index b400223cb..16cafae38 100644 --- a/Tactility/Include/Tactility/hal/display/DisplayDriver.h +++ b/Tactility/Include/Tactility/hal/display/DisplayDriver.h @@ -24,6 +24,14 @@ class DisplayDriver { 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; } }; } \ No newline at end of file diff --git a/TactilityC/CMakeLists.txt b/TactilityC/CMakeLists.txt index 3acd8d128..9f95aea46 100644 --- a/TactilityC/CMakeLists.txt +++ b/TactilityC/CMakeLists.txt @@ -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 diff --git a/TactilityC/Include/tt_hal_display.h b/TactilityC/Include/tt_hal_display.h index 3c4328eaf..1a7af6a03 100644 --- a/TactilityC/Include/tt_hal_display.h +++ b/TactilityC/Include/tt_hal_display.h @@ -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 diff --git a/TactilityC/Source/tt_hal_display.cpp b/TactilityC/Source/tt_hal_display.cpp index 48f16aff9..6eae38740 100644 --- a/TactilityC/Source/tt_hal_display.cpp +++ b/TactilityC/Source/tt_hal_display.cpp @@ -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(handle); + return wrapper->driver->getFrameBuffers(outBuffers); +} + } \ No newline at end of file diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index a742490b6..77d21b774 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -62,9 +62,17 @@ #include #include +#ifdef CONFIG_IDF_TARGET_ESP32P4 +#include +#include +#endif + extern "C" { extern double __floatsidf(int x); +extern long long __divdi3(long long a, long long b); +extern unsigned long long __udivdi3(unsigned long long a, unsigned long long b); +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 +85,15 @@ 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(__divdi3), + ESP_ELFSYM_EXPORT(__udivdi3), + ESP_ELFSYM_EXPORT(_esp_error_check_failed), // unistd.h ESP_ELFSYM_EXPORT(usleep), ESP_ELFSYM_EXPORT(sleep), @@ -205,6 +217,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 +225,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 +250,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 +311,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 +379,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 +457,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 +468,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 }; From 7d479b8c53d9234bbbf3a67340c2fc14e50bd9f6 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sun, 14 Jun 2026 13:42:27 +1000 Subject: [PATCH 05/16] Fix usb mode logo issue wrong logo was selected if rotation was changed from the default... yep it was weird. --- Tactility/Source/app/boot/Boot.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 3bb48f770..92537cde7 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -16,6 +16,8 @@ #include +#include + #ifdef ESP_PLATFORM #include "Tactility/app/crashdiagnostics/CrashDiagnostics.h" #include @@ -36,6 +38,11 @@ static std::shared_ptr 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 isUsbBootSplash; + Thread thread = Thread( "boot", 5120, @@ -174,6 +181,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,9 +207,9 @@ class BootApp : public App { 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); @@ -207,6 +217,8 @@ class BootApp : public App { } }; +std::atomic BootApp::isUsbBootSplash = false; + extern const AppManifest manifest = { .appId = "Boot", .appName = "Boot", From 8f5349dee0914ff2bea0153da027fed1f9b64747 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Sun, 14 Jun 2026 14:39:03 +1000 Subject: [PATCH 06/16] reboot from usb msc --- Tactility/Source/app/boot/Boot.cpp | 14 ++++++++++++ Tactility/Source/hal/usb/UsbTusb.cpp | 32 ++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 92537cde7..4993683f3 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -21,6 +21,7 @@ #ifdef ESP_PLATFORM #include "Tactility/app/crashdiagnostics/CrashDiagnostics.h" #include +#include #include #else #define CONFIG_TT_SPLASH_DURATION 0 @@ -214,6 +215,19 @@ class BootApp : public App { 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 } }; diff --git a/Tactility/Source/hal/usb/UsbTusb.cpp b/Tactility/Source/hal/usb/UsbTusb.cpp index c48862ccd..0ea5b4d9f 100644 --- a/Tactility/Source/hal/usb/UsbTusb.cpp +++ b/Tactility/Source/hal/usb/UsbTusb.cpp @@ -8,6 +8,9 @@ #if CONFIG_TINYUSB_MSC_ENABLED == 1 #include +#include +#include +#include #include #include #include @@ -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"); } @@ -148,7 +164,10 @@ static bool ensureDriverInstalled() { bool tusbIsSupported() { return true; } bool tusbStartMassStorageWithSdmmc() { - ensureDriverInstalled(); + if (!ensureDriverInstalled()) { + return false; + } + startedFromBootMode = true; auto* card = tt::hal::usb::getCard(); if (card == nullptr) { @@ -181,7 +200,10 @@ bool tusbStartMassStorageWithSdmmc() { bool tusbStartMassStorageWithFlash() { LOGGER.info("Starting flash MSC"); - ensureDriverInstalled(); + if (!ensureDriverInstalled()) { + return false; + } + startedFromBootMode = true; 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); From 3cb58c509718c11ccc57782f1870b4dc093a9be0 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Mon, 15 Jun 2026 12:45:12 +1000 Subject: [PATCH 07/16] Update EspLcdDisplayDriver.h --- Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h b/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h index 8138f33d1..c9c313a05 100644 --- a/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h +++ b/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h @@ -3,7 +3,9 @@ #include #include +#if CONFIG_SOC_MIPI_DSI_SUPPORTED #include +#endif class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { @@ -34,10 +36,12 @@ class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { 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 }; From 4f3634a55aff8995d015a8c52818d444a5f447e5 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 15:26:08 +1000 Subject: [PATCH 08/16] No longer needed --- Devices/m5stack-tab5/Source/Configuration.cpp | 25 ------------------- .../Source/devices/Tab5Keyboard.cpp | 4 +++ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index 15363ed48..88c003464 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -275,31 +275,6 @@ static bool initBoot() { LOG_E(TAG, "Failed to init ES7210"); } - // If the keyboard is attached and no display orientation has been saved yet, default to - // landscape (the keyboard add-on is used with the device on its side). Runs at BootSplash, - // once LVGL and the keyboard's isAttached() state are both available, and only applies on - // first boot - an existing saved orientation (e.g. user preference) is left untouched. - tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { - auto keyboard = findFirstDevice(tt::hal::Device::Type::Keyboard); - if (keyboard == nullptr || !keyboard->isAttached()) { - return; - } - - tt::settings::display::DisplaySettings displaySettings; - if (tt::settings::display::load(displaySettings)) { - return; - } - - displaySettings = tt::settings::display::getDefault(); - displaySettings.orientation = tt::settings::display::Orientation::Landscape; - if (tt::lvgl::lock()) { - lv_display_set_rotation(lv_display_get_default(), tt::settings::display::toLvglDisplayRotation(displaySettings.orientation)); - tt::lvgl::unlock(); - } - tt::settings::display::save(displaySettings); - LOG_I(TAG, "Keyboard attached: defaulting display orientation to landscape"); - }); - return true; } diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index 34fa25c4e..d2863d850 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -517,6 +517,10 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) { }); inputTimer->start(); + if (wasAttached) { + applyAutoRotation(true); + } + return true; } From e001d3aea9a60a4f45229cc2ecdd891fadb226d2 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 15:43:44 +1000 Subject: [PATCH 09/16] Update Tab5Keyboard.cpp --- Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index d2863d850..b52d677d3 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -424,10 +424,6 @@ bool Tab5Keyboard::lateStart() { tt::lvgl::unlock(); - if (started) { - applyAutoRotation(true); - } - return started; } From 0b9ea444f4a9339bec918101cb4f66168c4f91a3 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 16:11:37 +1000 Subject: [PATCH 10/16] symbols --- TactilityC/Source/symbols/gcc_soft_float_p4.cpp | 8 ++++++++ TactilityC/Source/tt_init.cpp | 4 ---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/TactilityC/Source/symbols/gcc_soft_float_p4.cpp b/TactilityC/Source/symbols/gcc_soft_float_p4.cpp index 1383679bc..4c6fab1f9 100644 --- a/TactilityC/Source/symbols/gcc_soft_float_p4.cpp +++ b/TactilityC/Source/symbols/gcc_soft_float_p4.cpp @@ -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 }; diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 77d21b774..da2d97f68 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -70,8 +70,6 @@ extern "C" { extern double __floatsidf(int x); -extern long long __divdi3(long long a, long long b); -extern unsigned long long __udivdi3(unsigned long long a, unsigned long long b); 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[] { @@ -91,8 +89,6 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(esp_fill_random), // esp other ESP_ELFSYM_EXPORT(__floatsidf), - ESP_ELFSYM_EXPORT(__divdi3), - ESP_ELFSYM_EXPORT(__udivdi3), ESP_ELFSYM_EXPORT(_esp_error_check_failed), // unistd.h ESP_ELFSYM_EXPORT(usleep), From bb49ac7fe8512cc2572e87513f2381e9b6281d9c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 17:05:07 +1000 Subject: [PATCH 11/16] fixes --- .../m5stack-tab5/Source/devices/Tab5Keyboard.cpp | 13 ++++++++----- Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h | 2 +- Devices/m5stack-tab5/Source/module.cpp | 8 ++++---- .../bindings/espressif,esp32-sdmmc.yaml | 2 ++ Tactility/Include/Tactility/hal/usb/Usb.h | 2 +- Tactility/Private/Tactility/hal/usb/UsbTusb.h | 2 +- Tactility/Source/app/boot/Boot.cpp | 2 +- Tactility/Source/hal/usb/Usb.cpp | 4 ++-- Tactility/Source/hal/usb/UsbTusb.cpp | 4 ++-- 9 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index b52d677d3..875a00bcb 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -341,14 +341,14 @@ void Tab5Keyboard::processKeyboard() { // ones who changed it. Only affects the live LVGL rotation, never persisted // display settings. // --------------------------------------------------------------------------- -void Tab5Keyboard::applyAutoRotation(bool keyboardAttached) { +bool Tab5Keyboard::applyAutoRotation(bool keyboardAttached) { auto* display = lv_indev_get_display(kbHandle); if (display == nullptr) { - return; + return false; } if (!tt::lvgl::lock(pdMS_TO_TICKS(100))) { - return; // try again on the next attach-state check + return false; // retry next poll } if (keyboardAttached) { @@ -367,6 +367,7 @@ void Tab5Keyboard::applyAutoRotation(bool keyboardAttached) { } tt::lvgl::unlock(); + return true; } // --------------------------------------------------------------------------- @@ -385,12 +386,14 @@ void Tab5Keyboard::checkAttachState() { if (attached == wasAttached) { return; } - wasAttached = attached; if (attached) { reinitDevice(); } - applyAutoRotation(attached); + if (!applyAutoRotation(attached)) { + return; // keep prior state so transition is retried + } + wasAttached = attached; } // --------------------------------------------------------------------------- diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h index 944b48891..0538ec75d 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h @@ -51,7 +51,7 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice { static void IRAM_ATTR irqHandler(void* arg); void reinitDevice(); - void applyAutoRotation(bool keyboardAttached); + bool applyAutoRotation(bool keyboardAttached); void checkAttachState(); void drainEvents(); diff --git a/Devices/m5stack-tab5/Source/module.cpp b/Devices/m5stack-tab5/Source/module.cpp index 3b7a1f647..d4d0984d5 100644 --- a/Devices/m5stack-tab5/Source/module.cpp +++ b/Devices/m5stack-tab5/Source/module.cpp @@ -78,9 +78,9 @@ 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*/) { +static void keyboardDetectCallback(TimerHandle_t timer) { if (kb_late_started.load(std::memory_order_acquire)) { - xTimerStop(kb_detect_timer, 0); + xTimerStop(timer, 0); return; } @@ -93,7 +93,7 @@ static void keyboardDetectCallback(TimerHandle_t /*timer*/) { if (keyboard->getLvglIndev() != nullptr) { // Already started (boot-time attach) — nothing left to do. kb_late_started.store(true, std::memory_order_release); - xTimerStop(kb_detect_timer, 0); + xTimerStop(timer, 0); return; } @@ -105,7 +105,7 @@ static void keyboardDetectCallback(TimerHandle_t /*timer*/) { 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(kb_detect_timer, 0); + xTimerStop(timer, 0); } } diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml index a97aafbce..0de6815d7 100644 --- a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml +++ b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml @@ -46,10 +46,12 @@ properties: 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 diff --git a/Tactility/Include/Tactility/hal/usb/Usb.h b/Tactility/Include/Tactility/hal/usb/Usb.h index cc04d1796..c2e71207b 100644 --- a/Tactility/Include/Tactility/hal/usb/Usb.h +++ b/Tactility/Include/Tactility/hal/usb/Usb.h @@ -15,7 +15,7 @@ enum class BootMode { Flash }; -bool startMassStorageWithSdmmc(); +bool startMassStorageWithSdmmc(bool fromBootMode = false); void stop(); Mode getMode(); bool isSupported(); diff --git a/Tactility/Private/Tactility/hal/usb/UsbTusb.h b/Tactility/Private/Tactility/hal/usb/UsbTusb.h index 3b3c746d1..c4a428d15 100644 --- a/Tactility/Private/Tactility/hal/usb/UsbTusb.h +++ b/Tactility/Private/Tactility/hal/usb/UsbTusb.h @@ -1,7 +1,7 @@ #pragma once bool tusbIsSupported(); -bool tusbStartMassStorageWithSdmmc(); +bool tusbStartMassStorageWithSdmmc(bool fromBootMode = false); bool tusbStartMassStorageWithFlash(); void tusbStop(); bool tusbCanStartMassStorageWithFlash(); \ No newline at end of file diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 4993683f3..61177331a 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -89,7 +89,7 @@ class BootApp : public App { 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; } diff --git a/Tactility/Source/hal/usb/Usb.cpp b/Tactility/Source/hal/usb/Usb.cpp index c52a0c52a..bd027bbea 100644 --- a/Tactility/Source/hal/usb/Usb.cpp +++ b/Tactility/Source/hal/usb/Usb.cpp @@ -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 { diff --git a/Tactility/Source/hal/usb/UsbTusb.cpp b/Tactility/Source/hal/usb/UsbTusb.cpp index 0ea5b4d9f..3229cd659 100644 --- a/Tactility/Source/hal/usb/UsbTusb.cpp +++ b/Tactility/Source/hal/usb/UsbTusb.cpp @@ -163,11 +163,11 @@ static bool ensureDriverInstalled() { bool tusbIsSupported() { return true; } -bool tusbStartMassStorageWithSdmmc() { +bool tusbStartMassStorageWithSdmmc(bool fromBootMode) { if (!ensureDriverInstalled()) { return false; } - startedFromBootMode = true; + startedFromBootMode = fromBootMode; auto* card = tt::hal::usb::getCard(); if (card == nullptr) { From 6ceeef1fc2f23968c9061b4ec6a35692a3b5c80f Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 17:18:29 +1000 Subject: [PATCH 12/16] whoops --- Tactility/Include/Tactility/hal/usb/Usb.h | 2 +- Tactility/Private/Tactility/hal/usb/UsbTusb.h | 2 +- Tactility/Source/app/boot/Boot.cpp | 2 +- Tactility/Source/hal/usb/Usb.cpp | 4 ++-- Tactility/Source/hal/usb/UsbTusb.cpp | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tactility/Include/Tactility/hal/usb/Usb.h b/Tactility/Include/Tactility/hal/usb/Usb.h index c2e71207b..9cc3d461f 100644 --- a/Tactility/Include/Tactility/hal/usb/Usb.h +++ b/Tactility/Include/Tactility/hal/usb/Usb.h @@ -28,7 +28,7 @@ void resetUsbBootMode(); BootMode getUsbBootMode(); // Flash-based mass storage -bool startMassStorageWithFlash(); +bool startMassStorageWithFlash(bool fromBootMode = false); bool canRebootIntoMassStorageFlash(); void rebootIntoMassStorageFlash(); diff --git a/Tactility/Private/Tactility/hal/usb/UsbTusb.h b/Tactility/Private/Tactility/hal/usb/UsbTusb.h index c4a428d15..68cda7623 100644 --- a/Tactility/Private/Tactility/hal/usb/UsbTusb.h +++ b/Tactility/Private/Tactility/hal/usb/UsbTusb.h @@ -2,6 +2,6 @@ bool tusbIsSupported(); bool tusbStartMassStorageWithSdmmc(bool fromBootMode = false); -bool tusbStartMassStorageWithFlash(); +bool tusbStartMassStorageWithFlash(bool fromBootMode = false); void tusbStop(); bool tusbCanStartMassStorageWithFlash(); \ No newline at end of file diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 61177331a..39a38c356 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -84,7 +84,7 @@ 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; } diff --git a/Tactility/Source/hal/usb/Usb.cpp b/Tactility/Source/hal/usb/Usb.cpp index bd027bbea..6b2bb399c 100644 --- a/Tactility/Source/hal/usb/Usb.cpp +++ b/Tactility/Source/hal/usb/Usb.cpp @@ -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 { diff --git a/Tactility/Source/hal/usb/UsbTusb.cpp b/Tactility/Source/hal/usb/UsbTusb.cpp index 3229cd659..e68afcd29 100644 --- a/Tactility/Source/hal/usb/UsbTusb.cpp +++ b/Tactility/Source/hal/usb/UsbTusb.cpp @@ -198,12 +198,12 @@ bool tusbStartMassStorageWithSdmmc(bool fromBootMode) { return result == ESP_OK; } -bool tusbStartMassStorageWithFlash() { +bool tusbStartMassStorageWithFlash(bool fromBootMode) { LOGGER.info("Starting flash MSC"); if (!ensureDriverInstalled()) { return false; } - startedFromBootMode = true; + startedFromBootMode = fromBootMode; wl_handle_t handle = tt::getDataPartitionWlHandle(); if (handle == WL_INVALID_HANDLE) { @@ -253,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; } From 4782e819b89a1db2df086ce008018a6e8cf1918a Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Tue, 16 Jun 2026 17:33:01 +1000 Subject: [PATCH 13/16] Update UsbMock.cpp --- Tactility/Source/hal/usb/UsbMock.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/hal/usb/UsbMock.cpp b/Tactility/Source/hal/usb/UsbMock.cpp index 5e602cee4..d18dddb57 100644 --- a/Tactility/Source/hal/usb/UsbMock.cpp +++ b/Tactility/Source/hal/usb/UsbMock.cpp @@ -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; } From ea2375f41662f25e53cca8de835f51d291e724f7 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 17 Jun 2026 18:30:39 +1000 Subject: [PATCH 14/16] tab5 i2c master --- Devices/m5stack-tab5/CMakeLists.txt | 2 +- Devices/m5stack-tab5/Source/Configuration.cpp | 1 - .../m5stack-tab5/Source/devices/Display.cpp | 12 ++-- .../Source/devices/St7123Touch.cpp | 9 ++- .../m5stack-tab5/Source/devices/St7123Touch.h | 8 +-- Devices/m5stack-tab5/m5stack,tab5.dts | 13 ++-- Drivers/GT911Ng/CMakeLists.txt | 5 ++ Drivers/GT911Ng/README.md | 3 + Drivers/GT911Ng/Source/Gt911TouchNg.cpp | 58 +++++++++++++++ Drivers/GT911Ng/Source/Gt911TouchNg.h | 70 +++++++++++++++++++ .../tactility/drivers/esp32_i2c_master.h | 14 ++++ .../source/drivers/esp32_i2c_master.cpp | 9 +++ 12 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 Drivers/GT911Ng/CMakeLists.txt create mode 100644 Drivers/GT911Ng/README.md create mode 100644 Drivers/GT911Ng/Source/Gt911TouchNg.cpp create mode 100644 Drivers/GT911Ng/Source/Gt911TouchNg.h diff --git a/Devices/m5stack-tab5/CMakeLists.txt b/Devices/m5stack-tab5/CMakeLists.txt index ed36355cf..f0b70c71e 100644 --- a/Devices/m5stack-tab5/CMakeLists.txt +++ b/Devices/m5stack-tab5/CMakeLists.txt @@ -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 esp_driver_i2c vfs fatfs ina226-module + REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c esp_lcd_st7123 esp_lcd_touch_st7123 GT911Ng PwmBacklight driver esp_driver_i2c vfs fatfs ina226-module ) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index 88c003464..3eea859cb 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include diff --git a/Devices/m5stack-tab5/Source/devices/Display.cpp b/Devices/m5stack-tab5/Source/devices/Display.cpp index b26fe70e2..e48010f1e 100644 --- a/Devices/m5stack-tab5/Source/devices/Display.cpp +++ b/Devices/m5stack-tab5/Source/devices/Display.cpp @@ -4,7 +4,7 @@ #include "St7123Display.h" #include "St7123Touch.h" -#include +#include #include #include #include @@ -19,8 +19,9 @@ constexpr auto LCD_PIN_RESET = GPIO_NUM_NC; constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_22; static std::shared_ptr createGt911Touch() { - auto configuration = std::make_unique( - I2C_NUM_0, + ::Device* i2c0 = device_find_by_name("i2c0"); + auto configuration = std::make_unique( + i2c0, 720, 1280, false, // swapXY @@ -30,12 +31,13 @@ static std::shared_ptr createGt911Touch() { GPIO_NUM_NC // "GPIO_NUM_23 cannot be used due to resistor to 3V3" // https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L76234 ); - return std::make_shared(std::move(configuration)); + return std::make_shared(std::move(configuration)); } static std::shared_ptr createSt7123Touch() { + ::Device* i2c0 = device_find_by_name("i2c0"); auto configuration = std::make_unique( - I2C_NUM_0, + i2c0, 720, 1280, false, // swapXY diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp b/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp index 1b2aa0c92..a34dc208c 100644 --- a/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp +++ b/Devices/m5stack-tab5/Source/devices/St7123Touch.cpp @@ -1,6 +1,7 @@ #include "St7123Touch.h" #include +#include #include #include @@ -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(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) { diff --git a/Devices/m5stack-tab5/Source/devices/St7123Touch.h b/Devices/m5stack-tab5/Source/devices/St7123Touch.h index 2c3ddb4d4..ec483cccc 100644 --- a/Devices/m5stack-tab5/Source/devices/St7123Touch.h +++ b/Devices/m5stack-tab5/Source/devices/St7123Touch.h @@ -2,7 +2,7 @@ #include #include -#include +#include class St7123Touch final : public EspLcdTouch { @@ -12,14 +12,14 @@ class St7123Touch final : public EspLcdTouch { 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 @@ class St7123Touch final : public EspLcdTouch { pinInterrupt(pinInterrupt) {} - i2c_port_t port; + ::Device* controller; uint16_t xMax; uint16_t yMax; bool swapXy; diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 059f2939f..14fc9acec 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -28,11 +27,11 @@ }; i2c_internal: i2c0 { - compatible = "espressif,esp32-i2c"; + compatible = "espressif,esp32-i2c-master"; port = ; 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"; @@ -62,11 +61,11 @@ }; i2c_port_a: i2c1 { - compatible = "espressif,esp32-i2c"; + compatible = "espressif,esp32-i2c-master"; port = ; clock-frequency = <100000>; - pin-sda = <&gpio0 53 GPIO_FLAG_NONE>; - pin-scl = <&gpio0 54 GPIO_FLAG_NONE>; + pin-sda = <&gpio0 53 GPIO_FLAG_PULL_UP>; + pin-scl = <&gpio0 54 GPIO_FLAG_PULL_UP>; }; i2c_keyboard: i2c2 { diff --git a/Drivers/GT911Ng/CMakeLists.txt b/Drivers/GT911Ng/CMakeLists.txt new file mode 100644 index 000000000..cb0bb61c4 --- /dev/null +++ b/Drivers/GT911Ng/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility EspLcdCompat esp_lcd_touch_gt911 driver +) diff --git a/Drivers/GT911Ng/README.md b/Drivers/GT911Ng/README.md new file mode 100644 index 000000000..d2eba0bb3 --- /dev/null +++ b/Drivers/GT911Ng/README.md @@ -0,0 +1,3 @@ +# GT911 + +GT911 touch driver for Tactility. \ No newline at end of file diff --git a/Drivers/GT911Ng/Source/Gt911TouchNg.cpp b/Drivers/GT911Ng/Source/Gt911TouchNg.cpp new file mode 100644 index 000000000..96d4cc32e --- /dev/null +++ b/Drivers/GT911Ng/Source/Gt911TouchNg.cpp @@ -0,0 +1,58 @@ +#include "Gt911TouchNg.h" + +#include +#include +#include + +#include +#include + +static const auto LOGGER = tt::Logger("GT911"); + +bool Gt911TouchNg::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) { + esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); + 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); + + /** + * When the interrupt pin is low, the address is 0x5D. Otherwise it is 0x14. + * There is not reset pin, and the current driver fails when you only specify the interrupt pin. + * Because of that, we don't use the interrupt pin and we'll simply scan the bus instead: + */ + if (i2c_controller_has_device_at_address(configuration->controller, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, pdMS_TO_TICKS(100))) { + io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS; + } else if (i2c_controller_has_device_at_address(configuration->controller, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP, pdMS_TO_TICKS(100))) { + io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS_BACKUP; + } else { + LOGGER.error("No device found on I2C bus"); + return false; + } + + return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, &outHandle) == ESP_OK; +} + +bool Gt911TouchNg::createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) { + return esp_lcd_touch_new_i2c_gt911(ioHandle, &configuration, &panelHandle) == ESP_OK; +} + +esp_lcd_touch_config_t Gt911TouchNg::createEspLcdTouchConfig() { + return { + .x_max = configuration->xMax, + .y_max = configuration->yMax, + .rst_gpio_num = configuration->pinReset, + .int_gpio_num = configuration->pinInterrupt, + .levels = { + .reset = configuration->pinResetLevel, + .interrupt = configuration->pinInterruptLevel, + }, + .flags = { + .swap_xy = configuration->swapXy, + .mirror_x = configuration->mirrorX, + .mirror_y = configuration->mirrorY, + }, + .process_coordinates = nullptr, + .interrupt_callback = nullptr, + .user_data = nullptr, + .driver_data = nullptr + }; +} diff --git a/Drivers/GT911Ng/Source/Gt911TouchNg.h b/Drivers/GT911Ng/Source/Gt911TouchNg.h new file mode 100644 index 000000000..b6eb0ecab --- /dev/null +++ b/Drivers/GT911Ng/Source/Gt911TouchNg.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include + +#include + +class Gt911TouchNg final : public EspLcdTouch { + +public: + + class Configuration { + public: + + Configuration( + ::Device* controller, + uint16_t xMax, + uint16_t yMax, + bool swapXy = false, + bool mirrorX = false, + bool mirrorY = false, + gpio_num_t pinReset = GPIO_NUM_NC, + gpio_num_t pinInterrupt = GPIO_NUM_NC, + unsigned int pinResetLevel = 0, + unsigned int pinInterruptLevel = 0 + ) : controller(controller), + xMax(xMax), + yMax(yMax), + swapXy(swapXy), + mirrorX(mirrorX), + mirrorY(mirrorY), + pinReset(pinReset), + pinInterrupt(pinInterrupt), + pinResetLevel(pinResetLevel), + pinInterruptLevel(pinInterruptLevel) + {} + + ::Device* controller; + uint16_t xMax; + uint16_t yMax; + bool swapXy; + bool mirrorX; + bool mirrorY; + gpio_num_t pinReset; + gpio_num_t pinInterrupt; + unsigned int pinResetLevel; + unsigned int pinInterruptLevel; + }; + +private: + + std::unique_ptr configuration; + + bool createIoHandle(esp_lcd_panel_io_handle_t& outHandle) override; + + bool createTouchHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_touch_config_t& configuration, esp_lcd_touch_handle_t& panelHandle) override; + + esp_lcd_touch_config_t createEspLcdTouchConfig() override; + +public: + + explicit Gt911TouchNg(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { + assert(configuration != nullptr); + } + + std::string getName() const override { return "GT911"; } + + std::string getDescription() const override { return "GT911 I2C touch driver"; } +}; diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_i2c_master.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_i2c_master.h index 3e302f7a5..5c4d26b93 100644 --- a/Platforms/platform-esp32/include/tactility/drivers/esp32_i2c_master.h +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_i2c_master.h @@ -2,7 +2,9 @@ #pragma once #include +#include #include +#include #ifdef __cplusplus extern "C" { @@ -16,6 +18,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 diff --git a/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp b/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp index 4d6318450..11088358d 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_i2c_master.cpp @@ -286,6 +286,15 @@ static error_t stop(Device* device) { return ERROR_NONE; } +i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(struct Device* device) { + auto* data = GET_DATA(device); + return data ? data->bus_handle : nullptr; +} + +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, From c2d293ec0886866b383bd62f3dcdc19540d7bf3c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 18 Jun 2026 05:17:22 +1000 Subject: [PATCH 15/16] fix attach/detach issues --- Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp | 10 ++++++++++ Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index 875a00bcb..e5f71f657 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -384,9 +384,19 @@ void Tab5Keyboard::checkAttachState() { 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(); } diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h index 0538ec75d..8a052d235 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h @@ -32,6 +32,10 @@ class Tab5Keyboard final : public tt::hal::keyboard::KeyboardDevice { // 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; From 34e55fd8b4cfc264975ccd0db5b0aa490598d57b Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 18 Jun 2026 23:19:21 +1000 Subject: [PATCH 16/16] check --- Devices/m5stack-tab5/Source/Configuration.cpp | 3 ++- Devices/m5stack-tab5/Source/devices/Display.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index 3eea859cb..3887c205a 100644 --- a/Devices/m5stack-tab5/Source/Configuration.cpp +++ b/Devices/m5stack-tab5/Source/Configuration.cpp @@ -16,7 +16,8 @@ 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(), diff --git a/Devices/m5stack-tab5/Source/devices/Display.cpp b/Devices/m5stack-tab5/Source/devices/Display.cpp index e48010f1e..c62d2ffd4 100644 --- a/Devices/m5stack-tab5/Source/devices/Display.cpp +++ b/Devices/m5stack-tab5/Source/devices/Display.cpp @@ -19,7 +19,8 @@ constexpr auto LCD_PIN_RESET = GPIO_NUM_NC; constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_22; static std::shared_ptr createGt911Touch() { - ::Device* i2c0 = device_find_by_name("i2c0"); + auto* i2c0 = device_find_by_name("i2c0"); + check(i2c0, "i2c0 not found"); auto configuration = std::make_unique( i2c0, 720, @@ -35,7 +36,8 @@ static std::shared_ptr createGt911Touch() { } static std::shared_ptr createSt7123Touch() { - ::Device* i2c0 = device_find_by_name("i2c0"); + auto* i2c0 = device_find_by_name("i2c0"); + check(i2c0, "i2c0 not found"); auto configuration = std::make_unique( i2c0, 720,