diff --git a/Devices/m5stack-tab5/Source/Configuration.cpp b/Devices/m5stack-tab5/Source/Configuration.cpp index dd9dec53a..88c003464 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" @@ -8,6 +7,10 @@ #include #include +#include +#include +#include +#include using namespace tt::hal; @@ -18,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/Source/devices/Tab5Keyboard.cpp b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp index 1901fae84..875a00bcb 100644 --- a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.cpp @@ -1,5 +1,7 @@ #include "Tab5Keyboard.h" #include +#include +#include #include #include #include @@ -329,6 +331,103 @@ 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) { + return; + } + + 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 +458,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 +492,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 +507,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::Type::Periodic, pdMS_TO_TICKS(20), [this] { @@ -405,6 +516,10 @@ bool Tab5Keyboard::startLvgl(lv_display_t* display) { }); inputTimer->start(); + if (wasAttached) { + applyAutoRotation(true); + } + return true; } diff --git a/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h b/Devices/m5stack-tab5/Source/devices/Tab5Keyboard.h index d05538434..0538ec75d 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(); + bool applyAutoRotation(bool keyboardAttached); + void checkAttachState(); + void drainEvents(); void processKeyboard(); static void readCallback(lv_indev_t* indev, lv_indev_data_t* data); @@ -62,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..d4d0984d5 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(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(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(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; } 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..c9c313a05 100644 --- a/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h +++ b/Drivers/EspLcdCompat/Source/EspLcdDisplayDriver.h @@ -3,6 +3,9 @@ #include #include +#if CONFIG_SOC_MIPI_DSI_SUPPORTED +#include +#endif class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { @@ -32,4 +35,13 @@ class EspLcdDisplayDriver : public tt::hal::display::DisplayDriver { 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 }; diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml index e2acf679f..0de6815d7 100644 --- a/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml +++ b/Platforms/platform-esp32/bindings/espressif,esp32-sdmmc.yaml @@ -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 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/Tactility/Include/Tactility/hal/usb/Usb.h b/Tactility/Include/Tactility/hal/usb/Usb.h index cc04d1796..9cc3d461f 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(); @@ -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 3b3c746d1..68cda7623 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 tusbStartMassStorageWithFlash(); +bool tusbStartMassStorageWithSdmmc(bool fromBootMode = false); +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 3bb48f770..39a38c356 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -16,9 +16,12 @@ #include +#include + #ifdef ESP_PLATFORM #include "Tactility/app/crashdiagnostics/CrashDiagnostics.h" #include +#include #include #else #define CONFIG_TT_SPLASH_DURATION 0 @@ -36,6 +39,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, @@ -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 @@ 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); 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 BootApp::isUsbBootSplash = false; + extern const AppManifest manifest = { .appId = "Boot", .appName = "Boot", diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 4d362778f..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) { @@ -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_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 @@ 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); 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); diff --git a/Tactility/Source/hal/usb/Usb.cpp b/Tactility/Source/hal/usb/Usb.cpp index c52a0c52a..6b2bb399c 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 { @@ -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/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; } diff --git a/Tactility/Source/hal/usb/UsbTusb.cpp b/Tactility/Source/hal/usb/UsbTusb.cpp index c48862ccd..e68afcd29 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"); } @@ -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; } 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/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_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..da2d97f68 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -62,9 +62,15 @@ #include #include +#ifdef CONFIG_IDF_TARGET_ESP32P4 +#include +#include +#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 };