diff --git a/App/idf_component.yml b/App/idf_component.yml index 06b8eb02..335109d9 100644 --- a/App/idf_component.yml +++ b/App/idf_component.yml @@ -4,4 +4,8 @@ dependencies: espressif/esp_lcd_touch_gt911: "1.1.1~2" espressif/esp_lcd_touch_ft5x06: "1.0.6~1" espressif/esp_lcd_touch: "1.1.2" + espressif/esp_tinyusb: + version: "1.5.0" + rules: + - if: "target == esp32s3" idf: '5.3.2' diff --git a/CMakeLists.txt b/CMakeLists.txt index 2232815f..d26df98c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,13 +32,13 @@ if (DEFINED ENV{ESP_IDF_VERSION}) set(EXCLUDE_COMPONENTS "Simulator") - # ESP32 boards + # Non-ESP32 boards if(NOT "${IDF_TARGET}" STREQUAL "esp32") set(EXCLUDE_COMPONENTS "YellowBoard") set(EXCLUDE_COMPONENTS "M5stackCore2") endif() - # ESP32-S3 boards + # Non-ESP32-S3 boards if(NOT "${IDF_TARGET}" STREQUAL "esp32s3") set(EXCLUDE_COMPONENTS "LilygoTdeck") set(EXCLUDE_COMPONENTS "M5stackCoreS3") diff --git a/Data/assets/boot_logo_usb.png b/Data/assets/boot_logo_usb.png new file mode 100644 index 00000000..ae3dd613 Binary files /dev/null and b/Data/assets/boot_logo_usb.png differ diff --git a/Data/assets_sources/boot_logo_usb.svg b/Data/assets_sources/boot_logo_usb.svg new file mode 100644 index 00000000..5be88072 --- /dev/null +++ b/Data/assets_sources/boot_logo_usb.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index aaaaf295..087f2799 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -54,6 +54,7 @@ namespace app { namespace settings { extern const AppManifest manifest; } namespace systeminfo { extern const AppManifest manifest; } namespace textviewer { extern const AppManifest manifest; } + namespace usbsettings { extern const AppManifest manifest; } namespace wifiapsettings { extern const AppManifest manifest; } namespace wificonnect { extern const AppManifest manifest; } namespace wifimanage { extern const AppManifest manifest; } @@ -86,6 +87,7 @@ static const std::vector system_apps = { &app::selectiondialog::manifest, &app::systeminfo::manifest, &app::textviewer::manifest, + &app::usbsettings::manifest, &app::wifiapsettings::manifest, &app::wificonnect::manifest, &app::wifimanage::manifest, diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 5317f53b..1aaf8c43 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -9,6 +9,7 @@ #include "lvgl.h" #include "Tactility.h" +#include "hal/usb/Usb.h" #ifdef ESP_PLATFORM #include "kernel/PanicHandler.h" @@ -31,29 +32,33 @@ struct Data { }; static int32_t bootThreadCallback(TT_UNUSED void* context) { - TickType_t start_time = tt::kernel::getTicks(); + TickType_t start_time = kernel::getTicks(); auto* lvgl_display = lv_display_get_default(); tt_assert(lvgl_display != nullptr); - auto* hal_display = (tt::hal::Display*)lv_display_get_user_data(lvgl_display); + auto* hal_display = (hal::Display*)lv_display_get_user_data(lvgl_display); tt_assert(hal_display != nullptr); if (hal_display->supportsBacklightDuty()) { int32_t backlight_duty = app::display::getBacklightDuty(); hal_display->setBacklightDuty(backlight_duty); } - TickType_t end_time = tt::kernel::getTicks(); - TickType_t ticks_passed = end_time - start_time; - TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS); - if (minimum_ticks > ticks_passed) { - tt::kernel::delayTicks(minimum_ticks - ticks_passed); + if (hal::usb::isUsbBootMode()) { + TT_LOG_I(TAG, "Rebooting into mass storage device mode"); + hal::usb::resetUsbBootMode(); + hal::usb::startMassStorageWithSdmmc(); + } else { + TickType_t end_time = tt::kernel::getTicks(); + TickType_t ticks_passed = end_time - start_time; + TickType_t minimum_ticks = (CONFIG_TT_SPLASH_DURATION / portTICK_PERIOD_MS); + if (minimum_ticks > ticks_passed) { + kernel::delayTicks(minimum_ticks - ticks_passed); + } + + tt::service::loader::stopApp(); + startNextApp(); } - tt::service::loader::stopApp(); - - - startNextApp(); - return 0; } @@ -84,7 +89,13 @@ static void onShow(TT_UNUSED AppContext& app, lv_obj_t* parent) { lv_obj_t* image = lv_image_create(parent); lv_obj_set_size(image, LV_PCT(100), LV_PCT(100)); - lv_image_set_src(image, TT_ASSETS_BOOT_LOGO); + + if (hal::usb::isUsbBootMode()) { + lv_image_set_src(image, TT_ASSETS_BOOT_LOGO_USB); + } else { + lv_image_set_src(image, TT_ASSETS_BOOT_LOGO); + } + lvgl::obj_set_style_bg_blacken(parent); data->thread.start(); diff --git a/Tactility/Source/app/usbsettings/UsbSettings.cpp b/Tactility/Source/app/usbsettings/UsbSettings.cpp new file mode 100644 index 00000000..fa59148c --- /dev/null +++ b/Tactility/Source/app/usbsettings/UsbSettings.cpp @@ -0,0 +1,45 @@ +#include "lvgl.h" +#include "lvgl/Toolbar.h" +#include "hal/usb/Usb.h" + +#define TAG "usb_settings" + +namespace tt::app::usbsettings { + +static void onRebootMassStorage(TT_UNUSED lv_event_t* event) { + hal::usb::rebootIntoMassStorageSdmmc(); +} + +static void onShow(AppContext& app, lv_obj_t* parent) { + auto* toolbar = lvgl::toolbar_create(parent, app); + lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); + + if (hal::usb::canRebootIntoMassStorageSdmmc()) { + auto* button = lv_button_create(parent); + auto* label = lv_label_create(button); + lv_label_set_text(label, "Reboot as USB storage"); + lv_obj_align(button, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(button, onRebootMassStorage, LV_EVENT_SHORT_CLICKED, nullptr); + } else { + bool supported = hal::usb::isSupported(); + const char* first = supported ? "USB storage not available:" : "USB driver not supported"; + const char* second = supported ? "SD card not mounted" : "on this hardware"; + auto* label_a = lv_label_create(parent); + lv_label_set_text(label_a, first); + lv_obj_align(label_a, LV_ALIGN_CENTER, 0, 0); + auto* label_b = lv_label_create(parent); + lv_label_set_text(label_b, second); + lv_obj_align_to(label_b, label_a, LV_ALIGN_OUT_BOTTOM_MID, 0, 4); + } + +} + +extern const AppManifest manifest = { + .id = "UsbSettings", + .name = "USB", + .icon = LV_SYMBOL_USB, + .type = TypeSettings, + .onShow = onShow +}; + +} // namespace diff --git a/TactilityHeadless/CMakeLists.txt b/TactilityHeadless/CMakeLists.txt index 0a092d7d..6ce8bcda 100644 --- a/TactilityHeadless/CMakeLists.txt +++ b/TactilityHeadless/CMakeLists.txt @@ -6,11 +6,16 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) if (DEFINED ENV{ESP_IDF_VERSION}) file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + list(APPEND REQUIRES_LIST TactilityCore esp_wifi nvs_flash driver spiffs vfs fatfs ) + if("${IDF_TARGET}" STREQUAL "esp32s3") + list(APPEND REQUIRES_LIST esp_tinyusb) + endif() + idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source/" PRIV_INCLUDE_DIRS "Private/" - REQUIRES TactilityCore esp_wifi nvs_flash driver spiffs vfs fatfs + REQUIRES ${REQUIRES_LIST} ) if (NOT DEFINED TACTILITY_SKIP_SPIFFS) diff --git a/TactilityHeadless/Source/Assets.h b/TactilityHeadless/Source/Assets.h index 5cb52342..8c3e715a 100644 --- a/TactilityHeadless/Source/Assets.h +++ b/TactilityHeadless/Source/Assets.h @@ -5,6 +5,7 @@ // Splash #define TT_ASSETS_BOOT_LOGO TT_ASSET("boot_logo.png") +#define TT_ASSETS_BOOT_LOGO_USB TT_ASSET("boot_logo_usb.png") // UI #define TT_ASSETS_UI_SPINNER TT_ASSET("spinner.png") diff --git a/TactilityHeadless/Source/hal/SpiSdCard.h b/TactilityHeadless/Source/hal/SpiSdCard.h index eae1aaaa..c7067e18 100644 --- a/TactilityHeadless/Source/hal/SpiSdCard.h +++ b/TactilityHeadless/Source/hal/SpiSdCard.h @@ -73,6 +73,8 @@ public: bool mount(const char* mountPath) override; bool unmount() override; State getState() const override; + + sdmmc_card_t* _Nullable getCard() { return card; } }; } diff --git a/TactilityHeadless/Source/hal/usb/Usb.cpp b/TactilityHeadless/Source/hal/usb/Usb.cpp new file mode 100644 index 00000000..f4edfcd4 --- /dev/null +++ b/TactilityHeadless/Source/hal/usb/Usb.cpp @@ -0,0 +1,108 @@ +#ifdef ESP_PLATFORM + +#include +#include "Usb.h" +#include "UsbTusb.h" +#include "TactilityHeadless.h" +#include "hal/SpiSdCard.h" + +namespace tt::hal::usb { + +#define TAG "usb" + +#define BOOT_FLAG 42 + +struct BootMode { + uint32_t flag = 0; +}; + +static Mode currentMode = ModeDefault; +static RTC_NOINIT_ATTR BootMode bootMode; + +sdmmc_card_t* _Nullable getCard() { + auto sdcard = getConfiguration().sdcard; + if (sdcard == nullptr) { + TT_LOG_W(TAG, "No SD card configuration found"); + return nullptr; + } + + if (!sdcard->isMounted()) { + TT_LOG_W(TAG, "SD card not mounted"); + return nullptr; + } + + auto spi_sdcard = std::static_pointer_cast(sdcard); + if (spi_sdcard == nullptr) { + TT_LOG_W(TAG, "SD card interface is not supported (must be SpiSdCard)"); + return nullptr; + } + + auto* card = spi_sdcard->getCard(); + if (card == nullptr) { + TT_LOG_W(TAG, "SD card has no card object available"); + return nullptr; + } + + return card; +} + +static bool canStartNewMode() { + return isSupported() && (currentMode == ModeDefault || currentMode == ModeNone); +} + +bool isSupported() { + return tusbIsSupported(); +} + +bool startMassStorageWithSdmmc() { + if (!canStartNewMode()) { + TT_LOG_E(TAG, "Can't start"); + return false; + } + + auto result = tusbStartMassStorageWithSdmmc(); + if (result != ESP_OK) { + TT_LOG_E(TAG, "Failed to init mass storage: %s", esp_err_to_name(result)); + return false; + } else { + currentMode = ModeMassStorageSdmmc; + return true; + } +} + +void stop() { + if (canStartNewMode()) { + return; + } + + tusbStop(); + + currentMode = ModeNone; +} + +Mode getMode() { + return currentMode; +} + +bool canRebootIntoMassStorageSdmmc() { + return tusbIsSupported() && getCard() != nullptr; +} + +void rebootIntoMassStorageSdmmc() { + if (tusbIsSupported()) { + bootMode.flag = BOOT_FLAG; + esp_restart(); + } +} + +bool isUsbBootMode() { + return bootMode.flag == BOOT_FLAG; +} + +void resetUsbBootMode() { + bootMode.flag = 0; +} + +} + +#endif diff --git a/TactilityHeadless/Source/hal/usb/Usb.h b/TactilityHeadless/Source/hal/usb/Usb.h new file mode 100644 index 00000000..aab33d5c --- /dev/null +++ b/TactilityHeadless/Source/hal/usb/Usb.h @@ -0,0 +1,21 @@ +#pragma once + +namespace tt::hal::usb { + +enum Mode { + ModeDefault, // Default state of USB stack + ModeNone, // State after TinyUSB was used and (partially) deinitialized + ModeMassStorageSdmmc +}; + +bool startMassStorageWithSdmmc(); +void stop(); +Mode getMode(); +bool isSupported(); + +bool canRebootIntoMassStorageSdmmc(); +void rebootIntoMassStorageSdmmc(); +bool isUsbBootMode(); +void resetUsbBootMode(); + +} \ No newline at end of file diff --git a/TactilityHeadless/Source/hal/usb/UsbMock.cpp b/TactilityHeadless/Source/hal/usb/UsbMock.cpp new file mode 100644 index 00000000..431520db --- /dev/null +++ b/TactilityHeadless/Source/hal/usb/UsbMock.cpp @@ -0,0 +1,21 @@ +#ifndef ESP_PLATFORM + +#include "Usb.h" + +#define TAG "usb" + +namespace tt::hal::usb { + +bool startMassStorageWithSdmmc() { return false; } +void stop() {} +Mode getMode() { return ModeDefault; } +bool isSupported() { return false; } + +bool canRebootIntoMassStorageSdmmc() { return false; } +void rebootIntoMassStorageSdmmc() {} +bool isUsbBootMode() { return false; } +void resetUsbBootMode() {} + +} + +#endif diff --git a/TactilityHeadless/Source/hal/usb/UsbTusb.cpp b/TactilityHeadless/Source/hal/usb/UsbTusb.cpp new file mode 100644 index 00000000..368a6ec3 --- /dev/null +++ b/TactilityHeadless/Source/hal/usb/UsbTusb.cpp @@ -0,0 +1,167 @@ +#ifdef ESP_PLATFORM + +#include "UsbTusb.h" +#include "sdkconfig.h" + +#if CONFIG_TINYUSB_MSC_ENABLED == 1 + +#include "Log.h" +#include "tinyusb.h" +#include "tusb_msc_storage.h" + +#define TAG "usb" +#define EPNUM_MSC 1 +#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN) + +namespace tt::hal::usb { + extern sdmmc_card_t* _Nullable getCard(); +} + +enum { + ITF_NUM_MSC = 0, + ITF_NUM_TOTAL +}; + +enum { + EDPT_CTRL_OUT = 0x00, + EDPT_CTRL_IN = 0x80, + + EDPT_MSC_OUT = 0x01, + EDPT_MSC_IN = 0x81, +}; + +static bool driverInstalled = false; + +static tusb_desc_device_t descriptor_config = { + .bLength = sizeof(descriptor_config), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x303A, // TODO: Espressif VID. Do we need to change this? + .idProduct = 0x4002, + .bcdDevice = 0x100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 +}; + +static char const* string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "Espressif", // 1: Manufacturer + "Tactility Device", // 2: Product + "42", // 3: Serials + "Tactility Mass Storage", // 4. MSC +}; + +static uint8_t const msc_fs_configuration_desc[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64), +}; + +#if (TUD_OPT_HIGH_SPEED) + static const tusb_desc_device_qualifier_t device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0 +}; + +static uint8_t const msc_hs_configuration_desc[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 512), +}; +#endif // TUD_OPT_HIGH_SPEED + +static void storage_mount_changed_cb(tinyusb_msc_event_t* event) { + if (event->mount_changed_data.is_mounted) { + TT_LOG_I(TAG, "Mounted"); + } else { + TT_LOG_I(TAG, "Unmounted"); + } +} + +static bool ensureDriverInstalled() { + if (driverInstalled) { + return true; + } + + const tinyusb_config_t tusb_cfg = { + .device_descriptor = &descriptor_config, + .string_descriptor = string_desc_arr, + .string_descriptor_count = sizeof(string_desc_arr) / sizeof(string_desc_arr[0]), + .external_phy = false, +#if (TUD_OPT_HIGH_SPEED) + .fs_configuration_descriptor = msc_fs_configuration_desc, + .hs_configuration_descriptor = msc_hs_configuration_desc, + .qualifier_descriptor = &device_qualifier, +#else + .configuration_descriptor = msc_fs_configuration_desc, +#endif // TUD_OPT_HIGH_SPEED + .self_powered = false, + .vbus_monitor_io = 0 + }; + + if (tinyusb_driver_install(&tusb_cfg) != ESP_OK) { + TT_LOG_E(TAG, "Failed to install TinyUSB driver"); + return false; + } + + driverInstalled = true; + return true; +} + +bool tusbIsSupported() { return true; } + +bool tusbStartMassStorageWithSdmmc() { + ensureDriverInstalled(); + + auto* card = tt::hal::usb::getCard(); + if (card == nullptr) { + TT_LOG_E(TAG, "SD card not mounted"); + return false; + } + + const tinyusb_msc_sdmmc_config_t config_sdmmc = { + .card = card, + .callback_mount_changed = storage_mount_changed_cb, + .callback_premount_changed = nullptr, + .mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 0, + .disk_status_check_enable = false, + .use_one_fat = false + } + }; + + return tinyusb_msc_storage_init_sdmmc(&config_sdmmc) == ESP_OK; +} + +void tusbStop() { + tinyusb_msc_storage_deinit(); +} + +#else + +bool tusbIsSupported() { return false; } +bool tusbStartMassStorageWithSdmmc() { return false; } +void tusbStop() {} + +#endif // TinyUSB enabled + +#endif // ESP_PLATFORM diff --git a/TactilityHeadless/Source/hal/usb/UsbTusb.h b/TactilityHeadless/Source/hal/usb/UsbTusb.h new file mode 100644 index 00000000..a9f9a3ee --- /dev/null +++ b/TactilityHeadless/Source/hal/usb/UsbTusb.h @@ -0,0 +1,5 @@ +#pragma once + +bool tusbIsSupported(); +bool tusbStartMassStorageWithSdmmc(); +void tusbStop(); diff --git a/sdkconfig.board.lilygo-tdeck b/sdkconfig.board.lilygo-tdeck index 41a00457..9ed497c8 100644 --- a/sdkconfig.board.lilygo-tdeck +++ b/sdkconfig.board.lilygo-tdeck @@ -24,6 +24,8 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS=y CONFIG_FATFS_LFN_HEAP=y +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" # Hardware: Main CONFIG_TT_BOARD_LILYGO_TDECK=y diff --git a/sdkconfig.board.m5stack-cores3 b/sdkconfig.board.m5stack-cores3 index 667b98e2..9cc6b8cb 100644 --- a/sdkconfig.board.m5stack-cores3 +++ b/sdkconfig.board.m5stack-cores3 @@ -24,6 +24,8 @@ CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS=y CONFIG_FATFS_LFN_HEAP=y +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" # Hardware: Main CONFIG_TT_BOARD_M5STACK_CORES3=y diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 94e17271..97e96523 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -23,6 +23,8 @@ CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_ELF_LOADER_CUSTOMER_SYMBOLS=y +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" # Hardware defaults CONFIG_TT_BOARD_CUSTOM=y