diff --git a/.github/actions/build-firmware/action.yml b/.github/actions/build-firmware/action.yml index 2317b64e0..c8bc8de61 100644 --- a/.github/actions/build-firmware/action.yml +++ b/.github/actions/build-firmware/action.yml @@ -20,7 +20,7 @@ runs: - name: 'Build' uses: espressif/esp-idf-ci-action@v1 with: - esp_idf_version: v5.5 + esp_idf_version: v5.5.2 target: ${{ inputs.arch }} path: './' - name: 'Release' diff --git a/.github/actions/build-sdk/action.yml b/.github/actions/build-sdk/action.yml index 36140ceaa..4964f7039 100644 --- a/.github/actions/build-sdk/action.yml +++ b/.github/actions/build-sdk/action.yml @@ -21,14 +21,14 @@ runs: uses: espressif/esp-idf-ci-action@v1 with: # NOTE: Update with ESP-IDF! - esp_idf_version: v5.5 + esp_idf_version: v5.5.2 target: ${{ inputs.arch }} path: './' - name: 'Release' shell: bash env: # NOTE: Update with ESP-IDF! - ESP_IDF_VERSION: '5.5' + ESP_IDF_VERSION: '5.5.2' run: python Buildscripts/release-sdk.py release/TactilitySDK - name: 'Test Integration Prep' shell: bash @@ -41,7 +41,7 @@ runs: - name: 'Test Integration' uses: espressif/esp-idf-ci-action@v1 with: - esp_idf_version: v5.5 + esp_idf_version: v5.5.2 target: ${{ inputs.arch }} command: export TACTILITY_SDK_PATH=../../test_sdk && cd Tests/SdkIntegration && python tactility.py build ${{ inputs.arch }} --local-sdk - name: 'Upload Artifact' diff --git a/Buildscripts/TactilitySDK/CMakeLists.txt b/Buildscripts/TactilitySDK/CMakeLists.txt index 7e757c19e..49d6f1025 100644 --- a/Buildscripts/TactilitySDK/CMakeLists.txt +++ b/Buildscripts/TactilitySDK/CMakeLists.txt @@ -2,6 +2,7 @@ idf_component_register( INCLUDE_DIRS "Libraries/TactilityC/include" "Libraries/TactilityKernel/include" + "Libraries/TactilityFreeRtos/include" "Libraries/lvgl/include" "Modules/lvgl-module/include" "Drivers/bm8563-module/include" diff --git a/Buildscripts/TactilitySDK/TactilitySDK.cmake b/Buildscripts/TactilitySDK/TactilitySDK.cmake index 54e05c824..77a6f0849 100644 --- a/Buildscripts/TactilitySDK/TactilitySDK.cmake +++ b/Buildscripts/TactilitySDK/TactilitySDK.cmake @@ -11,7 +11,9 @@ macro(tactility_project project_name) project_elf($project_name) file(READ ${TACTILITY_SDK_PATH}/idf-version.txt TACTILITY_SDK_IDF_VERSION) - if (NOT "$ENV{ESP_IDF_VERSION}" STREQUAL "${TACTILITY_SDK_IDF_VERSION}") + string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" TACTILITY_SDK_IDF_MAJOR_MINOR "${TACTILITY_SDK_IDF_VERSION}") + string(REGEX REPLACE "^([0-9]+\\.[0-9]+).*" "\\1" CURRENT_IDF_MAJOR_MINOR "$ENV{ESP_IDF_VERSION}") + if (NOT "${CURRENT_IDF_MAJOR_MINOR}" STREQUAL "${TACTILITY_SDK_IDF_MAJOR_MINOR}") message(FATAL_ERROR "ESP-IDF version of Tactility SDK (${TACTILITY_SDK_IDF_VERSION}) does not match current ESP-IDF version ($ENV{ESP_IDF_VERSION})") endif() diff --git a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts index 36c9646b9..2cb5ef8f2 100644 --- a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts +++ b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts @@ -4,6 +4,7 @@ #include #include #include +#include // Reference: https://github.com/bigtreetech/PandaTouch_PlatformIO/blob/master/docs/PINOUT.md / { @@ -34,4 +35,23 @@ pin-sda = <&gpio0 4 GPIO_FLAG_NONE>; pin-scl = <&gpio0 3 GPIO_FLAG_NONE>; }; + + usbhost0 { + compatible = "espressif,esp32-usbhost"; + + usbhosthid0 { + compatible = "espressif,esp32-usbhost-hid"; + //status = "disabled"; + }; + + usbhostmidi0 { + compatible = "espressif,esp32-usbhost-midi"; + //status = "disabled"; + }; + + usbhostmsc0 { + compatible = "espressif,esp32-usbhost-msc"; + //status = "disabled"; + }; + }; }; diff --git a/Devices/btt-panda-touch/device.properties b/Devices/btt-panda-touch/device.properties index dd58ccb82..e6d97c7fb 100644 --- a/Devices/btt-panda-touch/device.properties +++ b/Devices/btt-panda-touch/device.properties @@ -13,6 +13,7 @@ spiRamMode=OCT spiRamSpeed=120M esptoolFlashFreq=120M bluetooth=true +usbHostEnabled=true [display] size=5" diff --git a/Devices/m5stack-tab5/Source/devices/Display.cpp b/Devices/m5stack-tab5/Source/devices/Display.cpp index b7516976f..5bc8e13f3 100644 --- a/Devices/m5stack-tab5/Source/devices/Display.cpp +++ b/Devices/m5stack-tab5/Source/devices/Display.cpp @@ -77,8 +77,8 @@ std::shared_ptr createDisplay() { .mirrorY = false, .invertColor = false, .bufferSize = 0, // 0 = default (1/10 of screen) - .swRotate = (variant == Tab5Variant::St7123), // ST7123 MIPI-DSI panel does not support hardware swap_xy - .buffSpiram = (variant == Tab5Variant::St7123), // sw_rotate needs a 3rd buffer; use PSRAM to avoid OOM in internal SRAM + .swRotate = true, + .buffSpiram = true, .touch = touch, .backlightDutyFunction = driver::pwmbacklight::setBacklightDuty, .resetPin = LCD_PIN_RESET, diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 65fe43dc3..4997e3abe 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -13,6 +13,7 @@ spiRamMode=HEX spiRamSpeed=200M esptoolFlashFreq=80M bluetooth=true +usbHostEnabled=true [display] size=5" diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 1611b1b36..0424b48b5 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -93,4 +94,24 @@ pin-tx = <&gpio0 53 GPIO_FLAG_NONE>; pin-rx = <&gpio0 54 GPIO_FLAG_NONE>; }; + + usbhost0 { + compatible = "espressif,esp32-usbhost"; + peripheral-map = <0>; + + usbhosthid0 { + compatible = "espressif,esp32-usbhost-hid"; + //status = "disabled"; + }; + + usbhostmidi0 { + compatible = "espressif,esp32-usbhost-midi"; + //status = "disabled"; + }; + + usbhostmsc0 { + compatible = "espressif,esp32-usbhost-msc"; + //status = "disabled"; + }; + }; }; diff --git a/Firmware/idf_component.yml b/Firmware/idf_component.yml index adb53d712..f60d022c7 100644 --- a/Firmware/idf_component.yml +++ b/Firmware/idf_component.yml @@ -73,5 +73,13 @@ dependencies: rules: # More hardware might be supported - enable as needed - if: "target in [esp32s3]" - idf: '5.5' + espressif/usb_host_hid: + version: "1.1.0" + rules: + - if: "target in [esp32s3, esp32p4]" + espressif/usb_host_msc: + version: "1.1.4" + rules: + - if: "target in [esp32s3, esp32p4]" + idf: '5.5.2' diff --git a/Modules/lvgl-module/assets/generate-all.py b/Modules/lvgl-module/assets/generate-all.py index cff67bcb6..3fd92cbeb 100644 --- a/Modules/lvgl-module/assets/generate-all.py +++ b/Modules/lvgl-module/assets/generate-all.py @@ -145,6 +145,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "bluetooth_searching", "bluetooth_connected", "bluetooth_disabled", + "usb", ] # Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded diff --git a/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h b/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h index a4f7967f0..d6f38b159 100644 --- a/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h +++ b/Modules/lvgl-module/include/tactility/lvgl_icon_statusbar.h @@ -24,3 +24,4 @@ #define LVGL_ICON_STATUSBAR_BLUETOOTH_SEARCHING "\xEE\x98\x8F" #define LVGL_ICON_STATUSBAR_BLUETOOTH_CONNECTED "\xEE\x86\xA8" #define LVGL_ICON_STATUSBAR_BLUETOOTH_DISABLED "\xEE\x86\xA9" +#define LVGL_ICON_STATUSBAR_USB "\xEE\x87\xA0" diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c index 880815480..d0b9d89f0 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_12.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 12 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -46,6 +46,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x3c, 0xe0, 0x0, 0x0, 0x34, 0x0, 0x0, 0x0, + /* U+E1E0 "" */ + 0x2, 0x0, 0x7, 0x80, 0x3, 0x0, 0x73, 0x3c, + 0xb3, 0x3c, 0x33, 0x18, 0x3f, 0xf4, 0x3, 0x0, + 0x3, 0x40, 0x3, 0x40, + /* U+E322 "" */ 0x0, 0x0, 0x0, 0x77, 0x40, 0x2e, 0xab, 0x3, 0x45, 0x34, 0x77, 0xf3, 0x83, 0x77, 0x34, 0x75, @@ -166,26 +171,27 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 18, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, {.bitmap_index = 43, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, {.bitmap_index = 74, .adv_w = 192, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 107, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 132, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 157, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 177, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 204, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 231, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 258, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 278, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 305, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 332, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 359, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 383, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 403, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 421, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 439, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 457, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 475, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 493, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 511, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 529, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3} + {.bitmap_index = 107, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 127, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 152, .adv_w = 192, .box_w = 10, .box_h = 10, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 177, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 197, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 224, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 251, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 278, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 298, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 325, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 352, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 379, .adv_w = 192, .box_w = 12, .box_h = 8, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 403, .adv_w = 192, .box_w = 8, .box_h = 10, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 423, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 441, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 459, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 477, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 495, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 513, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 531, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 549, .adv_w = 192, .box_w = 12, .box_h = 6, .ofs_x = 0, .ofs_y = 3} }; /*--------------------- @@ -193,9 +199,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, - 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, - 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 + 0x0, 0x1, 0x2, 0x33, 0x39, 0x17b, 0x468, 0x47c, + 0xa2f, 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, + 0x1034, 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, + 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -203,7 +210,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 25, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c index 1cdbf1a3c..e49f80e65 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_16.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 16 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -54,6 +54,13 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x1, 0x41, 0xd0, 0x0, 0x0, 0x0, 0x70, 0x0, 0x0, 0x0, 0x0, + /* U+E1E0 "" */ + 0x0, 0x0, 0x0, 0xe, 0x0, 0x2, 0xf0, 0x0, + 0xe, 0x0, 0x10, 0xd1, 0x5b, 0xcd, 0x3e, 0x78, + 0xd3, 0xe3, 0x4d, 0x1c, 0x39, 0xe6, 0xc2, 0xff, + 0xf4, 0x0, 0xd0, 0x0, 0xe, 0x0, 0x1, 0xf0, + 0x0, 0xe, 0x0, + /* U+E322 "" */ 0x0, 0x21, 0x40, 0x0, 0x6e, 0xf5, 0x0, 0xff, 0xff, 0xd0, 0x34, 0x0, 0x34, 0x3d, 0x3f, 0x8f, @@ -210,26 +217,27 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 32, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, {.bitmap_index = 74, .adv_w = 256, .box_w = 13, .box_h = 14, .ofs_x = 1, .ofs_y = 1}, {.bitmap_index = 120, .adv_w = 256, .box_w = 16, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 180, .adv_w = 256, .box_w = 13, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, - {.bitmap_index = 223, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 265, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 307, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 355, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 403, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 451, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 493, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 541, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 589, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 637, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 685, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, - {.bitmap_index = 727, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 759, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 791, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 823, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 855, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 887, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 919, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 951, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4} + {.bitmap_index = 180, .adv_w = 256, .box_w = 10, .box_h = 14, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 215, .adv_w = 256, .box_w = 13, .box_h = 13, .ofs_x = 1, .ofs_y = 1}, + {.bitmap_index = 258, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 300, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 342, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 390, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 438, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 486, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 528, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 576, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 624, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 672, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 720, .adv_w = 256, .box_w = 12, .box_h = 14, .ofs_x = 2, .ofs_y = 1}, + {.bitmap_index = 762, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 794, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 826, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 858, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 890, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 922, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 954, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 986, .adv_w = 256, .box_w = 16, .box_h = 8, .ofs_x = 0, .ofs_y = 4} }; /*--------------------- @@ -237,9 +245,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, - 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, - 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 + 0x0, 0x1, 0x2, 0x33, 0x39, 0x17b, 0x468, 0x47c, + 0xa2f, 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, + 0x1034, 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, + 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -247,7 +256,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 25, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c index 6ce319c58..15dd882d6 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_20.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 20 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -66,6 +66,15 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x0, 0x0, 0x0, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, + /* U+E1E0 "" */ + 0x0, 0x0, 0x0, 0x0, 0x28, 0x0, 0x0, 0x7d, + 0x0, 0x0, 0xff, 0x0, 0x0, 0x38, 0x0, 0x10, + 0x38, 0x14, 0xbc, 0x38, 0x7f, 0xfd, 0x38, 0x7f, + 0x7c, 0x38, 0x7f, 0x38, 0x38, 0x2c, 0x3d, 0x7d, + 0x6c, 0x2f, 0xff, 0xfc, 0x5, 0x7d, 0x50, 0x0, + 0x38, 0x0, 0x0, 0x3c, 0x0, 0x0, 0xbe, 0x0, + 0x0, 0x7d, 0x0, 0x0, 0x0, 0x0, + /* U+E322 "" */ 0x0, 0x0, 0x10, 0x0, 0x0, 0x2c, 0x34, 0x0, 0x2, 0xbe, 0xfe, 0x80, 0xf, 0xff, 0xff, 0xe0, @@ -276,26 +285,27 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 44, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, {.bitmap_index = 108, .adv_w = 320, .box_w = 17, .box_h = 17, .ofs_x = 1, .ofs_y = 1}, {.bitmap_index = 181, .adv_w = 320, .box_w = 20, .box_h = 18, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 271, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 335, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, - {.bitmap_index = 399, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 462, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 532, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 602, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 672, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, - {.bitmap_index = 735, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 805, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 875, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 945, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 1015, .adv_w = 320, .box_w = 14, .box_h = 17, .ofs_x = 3, .ofs_y = 2}, - {.bitmap_index = 1075, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1125, .adv_w = 320, .box_w = 19, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1173, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1223, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1273, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1323, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1373, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 1423, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5} + {.bitmap_index = 271, .adv_w = 320, .box_w = 12, .box_h = 18, .ofs_x = 4, .ofs_y = 1}, + {.bitmap_index = 325, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 389, .adv_w = 320, .box_w = 16, .box_h = 16, .ofs_x = 2, .ofs_y = 2}, + {.bitmap_index = 453, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 516, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 586, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 656, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 726, .adv_w = 320, .box_w = 14, .box_h = 18, .ofs_x = 3, .ofs_y = 1}, + {.bitmap_index = 789, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 859, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 929, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 999, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 1069, .adv_w = 320, .box_w = 14, .box_h = 17, .ofs_x = 3, .ofs_y = 2}, + {.bitmap_index = 1129, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1179, .adv_w = 320, .box_w = 19, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1227, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1277, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1327, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1377, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1427, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 1477, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 5} }; /*--------------------- @@ -303,9 +313,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, - 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, - 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 + 0x0, 0x1, 0x2, 0x33, 0x39, 0x17b, 0x468, 0x47c, + 0xa2f, 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, + 0x1034, 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, + 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -313,7 +324,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 25, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c index fd9675db2..93c11d286 100644 --- a/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c +++ b/Modules/lvgl-module/source-fonts/material_symbols_statusbar_30.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 30 px * Bpp: 2 - * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9 --format lvgl -o ../source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format + * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -104,6 +104,23 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x0, 0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0x0, + /* U+E1E0 "" */ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xe, 0x0, + 0x0, 0x0, 0x2, 0xf4, 0x0, 0x0, 0x0, 0x7f, + 0xc0, 0x0, 0x0, 0xf, 0xff, 0x0, 0x0, 0x0, + 0x6f, 0x90, 0x0, 0x0, 0x1, 0xf0, 0x0, 0x0, + 0x0, 0x1f, 0x0, 0x0, 0x2f, 0x1, 0xf0, 0x3f, + 0xfb, 0xfc, 0x1f, 0x3, 0xff, 0xbf, 0xc1, 0xf0, + 0x3f, 0xfb, 0xfc, 0x1f, 0x3, 0xff, 0x2f, 0x1, + 0xf0, 0x3f, 0xf2, 0xf0, 0x1f, 0x0, 0xf8, 0x2f, + 0x1, 0xf0, 0xf, 0x81, 0xfa, 0xaf, 0xaa, 0xf8, + 0x1f, 0xff, 0xff, 0xff, 0x40, 0x7f, 0xff, 0xff, + 0xe0, 0x0, 0x1, 0xf0, 0x0, 0x0, 0x0, 0x1f, + 0x0, 0x0, 0x0, 0x1, 0xf0, 0x0, 0x0, 0x0, + 0x3f, 0xc0, 0x0, 0x0, 0xb, 0xfc, 0x0, 0x0, + 0x0, 0xbf, 0xc0, 0x0, 0x0, 0x3, 0xf8, 0x0, + 0x0, 0x0, 0x5, 0x0, 0x0, + /* U+E322 "" */ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xb4, 0x2d, 0x0, 0x0, 0x0, 0x0, 0xf8, 0x3e, @@ -486,26 +503,27 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 96, .adv_w = 480, .box_w = 22, .box_h = 24, .ofs_x = 4, .ofs_y = 3}, {.bitmap_index = 228, .adv_w = 480, .box_w = 25, .box_h = 25, .ofs_x = 2, .ofs_y = 2}, {.bitmap_index = 385, .adv_w = 480, .box_w = 30, .box_h = 26, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 580, .adv_w = 480, .box_w = 24, .box_h = 24, .ofs_x = 3, .ofs_y = 3}, - {.bitmap_index = 724, .adv_w = 480, .box_w = 23, .box_h = 24, .ofs_x = 4, .ofs_y = 3}, - {.bitmap_index = 862, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 992, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1139, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1286, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1433, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, - {.bitmap_index = 1563, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1710, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 1857, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, - {.bitmap_index = 2004, .adv_w = 480, .box_w = 28, .box_h = 20, .ofs_x = 1, .ofs_y = 5}, - {.bitmap_index = 2144, .adv_w = 480, .box_w = 20, .box_h = 25, .ofs_x = 5, .ofs_y = 3}, - {.bitmap_index = 2269, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2381, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2493, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2605, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2717, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2829, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 2941, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, - {.bitmap_index = 3053, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7} + {.bitmap_index = 580, .adv_w = 480, .box_w = 18, .box_h = 26, .ofs_x = 6, .ofs_y = 2}, + {.bitmap_index = 697, .adv_w = 480, .box_w = 24, .box_h = 24, .ofs_x = 3, .ofs_y = 3}, + {.bitmap_index = 841, .adv_w = 480, .box_w = 23, .box_h = 24, .ofs_x = 4, .ofs_y = 3}, + {.bitmap_index = 979, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 1109, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1256, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1403, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1550, .adv_w = 480, .box_w = 20, .box_h = 26, .ofs_x = 5, .ofs_y = 2}, + {.bitmap_index = 1680, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1827, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 1974, .adv_w = 480, .box_w = 28, .box_h = 21, .ofs_x = 1, .ofs_y = 4}, + {.bitmap_index = 2121, .adv_w = 480, .box_w = 28, .box_h = 20, .ofs_x = 1, .ofs_y = 5}, + {.bitmap_index = 2261, .adv_w = 480, .box_w = 20, .box_h = 25, .ofs_x = 5, .ofs_y = 3}, + {.bitmap_index = 2386, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2498, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2610, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2722, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2834, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 2946, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 3058, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7}, + {.bitmap_index = 3170, .adv_w = 480, .box_w = 28, .box_h = 16, .ofs_x = 1, .ofs_y = 7} }; /*--------------------- @@ -513,9 +531,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0x1, 0x2, 0x33, 0x17b, 0x468, 0x47c, 0xa2f, - 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, 0x1034, - 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, 0x10b0 + 0x0, 0x1, 0x2, 0x33, 0x39, 0x17b, 0x468, 0x47c, + 0xa2f, 0xa3a, 0xa3d, 0xeb0, 0xebd, 0xebe, 0xf09, 0xfb5, + 0x1034, 0x10a8, 0x10a9, 0x10ab, 0x10ac, 0x10ad, 0x10ae, 0x10af, + 0x10b0 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -523,7 +542,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = { { .range_start = 57767, .range_length = 4273, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 24, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 25, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index c890546d2..f3ad3b072 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -197,6 +197,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_one_checked), DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_button_width), DEFINE_MODULE_SYMBOL(lv_buttonmatrix_set_selected_button), + DEFINE_MODULE_SYMBOL(lv_buttonmatrix_clear_button_ctrl), // lv_canvas DEFINE_MODULE_SYMBOL(lv_canvas_create), DEFINE_MODULE_SYMBOL(lv_canvas_fill_bg), diff --git a/Platforms/platform-esp32/CMakeLists.txt b/Platforms/platform-esp32/CMakeLists.txt index 80f20f8d1..406bbc412 100644 --- a/Platforms/platform-esp32/CMakeLists.txt +++ b/Platforms/platform-esp32/CMakeLists.txt @@ -9,4 +9,4 @@ idf_component_register( REQUIRES TactilityKernel driver vfs fatfs ) -idf_component_optional_requires(PRIVATE bt) +idf_component_optional_requires(PRIVATE bt usb espressif__usb_host_hid espressif__usb_host_msc) diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml new file mode 100644 index 000000000..8b59c903e --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml @@ -0,0 +1,9 @@ +description: ESP32 USB Host HID class driver + +compatible: "espressif,esp32-usbhost-hid" + +properties: + _unused: + type: int + default: 0 + description: Placeholder required by the binding schema; this driver has no device-tree configuration. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml new file mode 100644 index 000000000..878cfbe85 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml @@ -0,0 +1,9 @@ +description: ESP32 USB Host MIDI class driver + +compatible: "espressif,esp32-usbhost-midi" + +properties: + _unused: + type: int + default: 0 + description: Placeholder required by the binding schema; this driver has no device-tree configuration. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml new file mode 100644 index 000000000..b2afee470 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml @@ -0,0 +1,9 @@ +description: ESP32 USB Host MSC class driver (mass storage) + +compatible: "espressif,esp32-usbhost-msc" + +properties: + _unused: + type: int + default: 0 + description: Placeholder required by the binding schema; this driver has no device-tree configuration. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml new file mode 100644 index 000000000..80f20a1ea --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml @@ -0,0 +1,15 @@ +description: ESP32 USB Host controller + +compatible: "espressif,esp32-usbhost" + +properties: + peripheral-map: + type: int + default: 0 + description: | + Bitmask of USB OTG controllers to use. Only meaningful on ESP32-P4, + which has two independent USB DWC cores. On ESP32-S2/S3 this field + is ignored (single controller, always used). + Default 0 selects the default peripheral (P4 default: controller 0, HS/UTMI). + BIT(0) = controller 0 (HS, UTMI PHY - GPIO 49/50 aka USB2_OTG pins, e.g. USB-A on Tab5) + BIT(1) = controller 1 (FS, FSLS PHY - GPIO 24/25 aka USB1 pins, e.g. USB-C OTG on Tab5) diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h new file mode 100644 index 000000000..e3e85e43f --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +DEFINE_DEVICETREE(esp32_usbhost, struct Esp32UsbHostConfig) +DEFINE_DEVICETREE(esp32_usbhost_hid, struct Esp32UsbHostChildConfig) +DEFINE_DEVICETREE(esp32_usbhost_midi, struct Esp32UsbHostChildConfig) +DEFINE_DEVICETREE(esp32_usbhost_msc, struct Esp32UsbHostChildConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h new file mode 100644 index 000000000..0a9fdff75 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Esp32UsbHostConfig { + uint8_t peripheral_map; /**< BIT(0)=controller 0 (HS/UTMI, e.g. USB-A on Tab5), BIT(1)=controller 1 (FS/FSLS, e.g. USB-C OTG on Tab5) */ +}; + +struct Esp32UsbHostChildConfig { + int _unused; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp new file mode 100644 index 000000000..4185c3927 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp @@ -0,0 +1,161 @@ +#include +#ifdef CONFIG_SOC_USB_OTG_SUPPORTED + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define TAG "esp32_usbhost" + +#define GET_CONFIG(device) ((const Esp32UsbHostConfig*)(device)->config) + +constexpr auto USB_LIB_TASK_STACK = 4096; +constexpr auto USB_LIB_TASK_PRIORITY = 10; +constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500; +constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000; +constexpr auto USB_HOST_STOP_RETRY_MS = 1000; + +struct UsbHostContext { + TaskHandle_t lib_task = nullptr; + SemaphoreHandle_t lib_task_done = nullptr; +}; + +static void usbLibTask(void* arg) { + auto* ctx = static_cast(arg); + LOG_I(TAG, "lib task started"); + + while (true) { + uint32_t flags = 0; + esp_err_t err = usb_host_lib_handle_events(pdMS_TO_TICKS(USB_LIB_EVENT_TIMEOUT_MS), &flags); + if (err != ESP_OK && err != ESP_ERR_TIMEOUT) { + LOG_W(TAG, "usb_host_lib_handle_events: %s", esp_err_to_name(err)); + } + + if (flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + LOG_I(TAG, "no more USB clients, freeing all devices"); + usb_host_device_free_all(); + } + if (flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + LOG_I(TAG, "all USB devices freed"); + } + + if (ulTaskNotifyTake(pdFALSE, 0) > 0) { + break; + } + } + + LOG_I(TAG, "lib task stopping"); + xSemaphoreGive(ctx->lib_task_done); + vTaskDelete(nullptr); +} + +extern "C" { + +static error_t start_device(struct Device* device) { + auto* cfg = GET_CONFIG(device); + if (!cfg) { + LOG_E(TAG, "device config is null"); + return ERROR_INVALID_ARGUMENT; + } + + usb_host_config_t host_cfg = { + .skip_phy_setup = false, + .root_port_unpowered = false, + .intr_flags = ESP_INTR_FLAG_LEVEL1, + .enum_filter_cb = nullptr, + .fifo_settings_custom = {}, +#if CONFIG_IDF_TARGET_ESP32P4 + .peripheral_map = cfg->peripheral_map, +#endif + }; + + esp_err_t ret = usb_host_install(&host_cfg); + if (ret != ESP_OK) { + LOG_E(TAG, "usb_host_install failed: %s", esp_err_to_name(ret)); + return ERROR_RESOURCE; + } + + auto* ctx = new UsbHostContext(); + + ctx->lib_task_done = xSemaphoreCreateBinary(); + if (!ctx->lib_task_done) { + LOG_E(TAG, "failed to create lib_task_done semaphore"); + delete ctx; + usb_host_uninstall(); + return ERROR_RESOURCE; + } + + BaseType_t result = xTaskCreate(usbLibTask, "usb_lib", USB_LIB_TASK_STACK, + ctx, USB_LIB_TASK_PRIORITY, &ctx->lib_task); + if (result != pdPASS) { + LOG_E(TAG, "failed to create usb_lib task"); + vSemaphoreDelete(ctx->lib_task_done); + delete ctx; + usb_host_uninstall(); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, ctx); + LOG_I(TAG, "started (peripheral_map=0x%02x)", cfg->peripheral_map); + return ERROR_NONE; +} + +static error_t stop_device(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return ERROR_NONE; + + xTaskNotifyGive(ctx->lib_task); + + bool exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_TIMEOUT_MS)) == pdTRUE); + if (!exited) { + // Free all devices to unblock the NO_CLIENTS / ALL_FREE flags, then retry. + usb_host_device_free_all(); + exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_RETRY_MS)) == pdTRUE); + } + if (!exited) { + LOG_E(TAG, "lib task stop timed out after %dms, force terminating — USB host restart required", + USB_HOST_STOP_TIMEOUT_MS + USB_HOST_STOP_RETRY_MS); + vTaskDelete(ctx->lib_task); + vTaskDelay(pdMS_TO_TICKS(50)); + // Skip usb_host_uninstall — USB stack is in undefined state. + ctx->lib_task = nullptr; + vSemaphoreDelete(ctx->lib_task_done); + device_set_driver_data(device, nullptr); + delete ctx; + return ERROR_RESOURCE; + } + ctx->lib_task = nullptr; + vSemaphoreDelete(ctx->lib_task_done); + + esp_err_t uninstall_err = usb_host_uninstall(); + if (uninstall_err != ESP_OK) { + LOG_W(TAG, "usb_host_uninstall: %s", esp_err_to_name(uninstall_err)); + } + + device_set_driver_data(device, nullptr); + delete ctx; + LOG_I(TAG, "stopped"); + return ERROR_NONE; +} + +Driver esp32_usbhost_driver = { + .name = "esp32_usbhost", + .compatible = (const char*[]) { "espressif,esp32-usbhost", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = nullptr, + .device_type = nullptr, + .owner = nullptr, + .internal = nullptr, +}; + +} // extern "C" + +#endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp new file mode 100644 index 000000000..dd20bb8e6 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -0,0 +1,506 @@ +#include +#ifdef CONFIG_SOC_USB_OTG_SUPPORTED + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "esp32_usbhost_hid" + +constexpr auto HID_EVENT_QUEUE_SIZE = 8; +constexpr auto HID_PROC_TASK_STACK = 4096; +constexpr auto HID_PROC_TASK_PRIORITY = 5; +constexpr auto HID_STOP_TIMEOUT_MS = 2000; +constexpr auto MAX_SUBSCRIBERS = 4; + +typedef struct { + hid_host_device_handle_t handle; + hid_host_driver_event_t event; + void* arg; +} hid_dev_event_t; + +struct UsbHidContext { + std::atomic mouse_connected{false}; + std::atomic device_connected{false}; + QueueHandle_t hid_event_queue = nullptr; + TaskHandle_t hid_proc_task = nullptr; + SemaphoreHandle_t hid_proc_task_done = nullptr; + std::atomic hid_proc_running{false}; + + uint8_t prev_keys[HID_KEYBOARD_KEY_MAX] = {}; + uint32_t pressed_lv_keys[256] = {}; + bool caps_lock_active = false; + bool num_lock_active = true; + bool scroll_lock_active = false; + bool prev_mouse_button2 = false; + + std::atomic kb_handle{nullptr}; + std::atomic kb_led_pending{false}; + + QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {}; + SemaphoreHandle_t sub_mutex = nullptr; +}; + +static const uint8_t keycode2ascii[57][2] = { + {0, 0}, {0, 0}, {0, 0}, {0, 0}, + {'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'}, + {'f', 'F'}, {'g', 'G'}, {'h', 'H'}, {'i', 'I'}, {'j', 'J'}, + {'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'}, + {'p', 'P'}, {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'}, + {'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, {'y', 'Y'}, + {'z', 'Z'}, + {'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'}, + {'6', '^'}, {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'}, + {'\r', '\r'}, {0, 0}, {'\b', 0}, {'\t', '\t'}, {' ', ' '}, + {'-', '_'}, {'=', '+'}, {'[', '{'}, {']', '}'}, + {'\\', '|'}, {'\\', '|'}, {';', ':'}, {'\'', '"'}, + {'`', '~'}, {',', '<'}, {'.', '>'}, {'/', '?'}, +}; + +static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code, + bool caps_lock, bool num_lock) { + bool shift = (modifier & HID_LEFT_SHIFT) || (modifier & HID_RIGHT_SHIFT); + bool ctrl = (modifier & HID_LEFT_CONTROL) || (modifier & HID_RIGHT_CONTROL); + bool alt = (modifier & HID_LEFT_ALT) || (modifier & HID_RIGHT_ALT); + + switch (key_code) { + case HID_KEY_ENTER: return USB_HID_KEY_ENTER; + case HID_KEY_ESC: return USB_HID_KEY_ESC; + case HID_KEY_DEL: return USB_HID_KEY_BACKSPACE; + case HID_KEY_DELETE: return USB_HID_KEY_DEL; + case HID_KEY_TAB: return shift ? USB_HID_KEY_PREV : USB_HID_KEY_NEXT; + case HID_KEY_UP: return USB_HID_KEY_UP; + case HID_KEY_DOWN: return USB_HID_KEY_DOWN; + case HID_KEY_LEFT: return USB_HID_KEY_LEFT; + case HID_KEY_RIGHT: return USB_HID_KEY_RIGHT; + case HID_KEY_HOME: return USB_HID_KEY_HOME; + case HID_KEY_END: return USB_HID_KEY_END; + case HID_KEY_KEYPAD_ENTER: return USB_HID_KEY_ENTER; + case HID_KEY_KEYPAD_ADD: return '+'; + case HID_KEY_KEYPAD_SUB: return '-'; + case HID_KEY_KEYPAD_MUL: return '*'; + case HID_KEY_KEYPAD_DIV: return '/'; + case HID_KEY_KEYPAD_0: return num_lock ? (uint32_t)'0' : 0u; + case HID_KEY_KEYPAD_1: return num_lock ? (uint32_t)'1' : (uint32_t)USB_HID_KEY_END; + case HID_KEY_KEYPAD_2: return num_lock ? (uint32_t)'2' : (uint32_t)USB_HID_KEY_DOWN; + case HID_KEY_KEYPAD_3: return num_lock ? (uint32_t)'3' : 0u; + case HID_KEY_KEYPAD_4: return num_lock ? (uint32_t)'4' : (uint32_t)USB_HID_KEY_LEFT; + case HID_KEY_KEYPAD_5: return num_lock ? (uint32_t)'5' : 0u; + case HID_KEY_KEYPAD_6: return num_lock ? (uint32_t)'6' : (uint32_t)USB_HID_KEY_RIGHT; + case HID_KEY_KEYPAD_7: return num_lock ? (uint32_t)'7' : (uint32_t)USB_HID_KEY_HOME; + case HID_KEY_KEYPAD_8: return num_lock ? (uint32_t)'8' : (uint32_t)USB_HID_KEY_UP; + case HID_KEY_KEYPAD_9: return num_lock ? (uint32_t)'9' : 0u; + case HID_KEY_KEYPAD_DELETE: return num_lock ? (uint32_t)'.' : (uint32_t)USB_HID_KEY_DEL; + default: break; + } + + if (ctrl || alt) return 0; + + if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) { + bool is_letter = (key_code >= 0x04 && key_code <= 0x1D); + bool effective_shift = is_letter ? (shift ^ caps_lock) : shift; + uint8_t ch = keycode2ascii[key_code][effective_shift ? 1 : 0]; + if (ch != 0) return (uint32_t)ch; + } + return 0; +} + +static void publish_event(UsbHidContext* ctx, const UsbHidEvent* evt) { + if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(10)) != pdTRUE) { + LOG_W(TAG, "publish_event: sub_mutex contended, event type=%d dropped", (int)evt->type); + return; + } + bool is_release = (evt->type == USB_HID_EVENT_KEY && !evt->key.pressed); + TickType_t send_timeout = is_release ? pdMS_TO_TICKS(10) : 0; + for (int i = 0; i < MAX_SUBSCRIBERS; i++) { + if (ctx->subscribers[i]) { + xQueueSend(ctx->subscribers[i], evt, send_timeout); + } + } + xSemaphoreGive(ctx->sub_mutex); +} + +static void publish_scroll(UsbHidContext* ctx, int32_t delta) { + UsbHidEvent evt = { .type = USB_HID_EVENT_SCROLL, .scroll = { delta } }; + publish_event(ctx, &evt); +} + +static void hid_interface_callback(hid_host_device_handle_t handle, + const hid_host_interface_event_t event, + void* arg) +{ + auto* ctx = static_cast(arg); + uint8_t data[64] = {}; + size_t data_len = 0; + hid_host_dev_params_t params; + + if (hid_host_device_get_params(handle, ¶ms) != ESP_OK) return; + + switch (event) { + case HID_HOST_INTERFACE_EVENT_INPUT_REPORT: + if (hid_host_device_get_raw_input_report_data(handle, data, sizeof(data), &data_len) != ESP_OK) break; + + if (params.proto == HID_PROTOCOL_KEYBOARD) { + if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break; + auto* kb = reinterpret_cast(data); + + for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) { + uint8_t prev_hid = ctx->prev_keys[i]; + if (prev_hid > HID_KEY_ERROR_UNDEFINED) { + bool still_pressed = false; + for (int j = 0; j < HID_KEYBOARD_KEY_MAX; j++) { + if (kb->key[j] == prev_hid) { still_pressed = true; break; } + } + if (!still_pressed) { + uint32_t lv_key = ctx->pressed_lv_keys[prev_hid]; + ctx->pressed_lv_keys[prev_hid] = 0; + if (lv_key) { + UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false } }; + publish_event(ctx, &evt); + } + } + } + + uint8_t hid_code = kb->key[i]; + if (hid_code > HID_KEY_ERROR_UNDEFINED) { + bool was_pressed = false; + for (int j = 0; j < HID_KEYBOARD_KEY_MAX; j++) { + if (ctx->prev_keys[j] == hid_code) { was_pressed = true; break; } + } + if (!was_pressed) { + if (hid_code == HID_KEY_CAPS_LOCK) { + ctx->caps_lock_active = !ctx->caps_lock_active; + ctx->kb_led_pending.store(true); + continue; + } + if (hid_code == HID_KEY_NUM_LOCK) { + ctx->num_lock_active = !ctx->num_lock_active; + ctx->kb_led_pending.store(true); + continue; + } + if (hid_code == HID_KEY_SCROLL_LOCK) { + ctx->scroll_lock_active = !ctx->scroll_lock_active; + ctx->kb_led_pending.store(true); + continue; + } + bool is_pgup = (hid_code == HID_KEY_PAGEUP) || + (!ctx->num_lock_active && hid_code == HID_KEY_KEYPAD_9); + bool is_pgdn = (hid_code == HID_KEY_PAGEDOWN) || + (!ctx->num_lock_active && hid_code == HID_KEY_KEYPAD_3); + if (is_pgup || is_pgdn) { + publish_scroll(ctx, is_pgup ? -8 : 8); + continue; + } + uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code, + ctx->caps_lock_active, ctx->num_lock_active); + if (lv_key) { + UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } }; + publish_event(ctx, &evt); + ctx->pressed_lv_keys[hid_code] = lv_key; + } + } + } + } + memcpy(ctx->prev_keys, kb->key, HID_KEYBOARD_KEY_MAX); + + } else if (params.proto == HID_PROTOCOL_MOUSE) { + if (data_len < sizeof(hid_mouse_input_report_boot_t)) break; + auto* ms = reinterpret_cast(data); + + if (ms->x_displacement != 0 || ms->y_displacement != 0) { + UsbHidEvent evt = { .type = USB_HID_EVENT_MOUSE_MOVE, + .mouse_move = { (int32_t)ms->x_displacement, (int32_t)ms->y_displacement } }; + publish_event(ctx, &evt); + } + { + bool b1 = ms->buttons.button1 != 0; + bool b2 = ms->buttons.button2 != 0; + UsbHidEvent evt = { .type = USB_HID_EVENT_MOUSE_BTN, .mouse_btn = { b1, b2 } }; + publish_event(ctx, &evt); + + if (b2 != ctx->prev_mouse_button2) { + UsbHidEvent key_evt = { .type = USB_HID_EVENT_KEY, .key = { USB_HID_KEY_ESC, b2 } }; + publish_event(ctx, &key_evt); + ctx->prev_mouse_button2 = b2; + } + } + + if (data_len > sizeof(hid_mouse_input_report_boot_t)) { + int8_t wheel = (int8_t)data[sizeof(hid_mouse_input_report_boot_t)]; + if (wheel != 0) { + int32_t delta = (wheel < -8) ? -8 : (wheel > 8) ? 8 : (int32_t)wheel; + publish_scroll(ctx, delta); + } + } + } + break; + + case HID_HOST_INTERFACE_EVENT_DISCONNECTED: + if (params.proto == HID_PROTOCOL_KEYBOARD) { + memset(ctx->prev_keys, 0, sizeof(ctx->prev_keys)); + memset(ctx->pressed_lv_keys, 0, sizeof(ctx->pressed_lv_keys)); + ctx->kb_handle.store(nullptr); + ctx->kb_led_pending.store(false); + } else if (params.proto == HID_PROTOCOL_MOUSE) { + ctx->mouse_connected = false; + } + hid_host_device_close(handle); + if (!ctx->kb_handle.load() && !ctx->mouse_connected) { + ctx->device_connected = false; + } + { + UsbHidEventType disc_type = (params.proto == HID_PROTOCOL_KEYBOARD) + ? USB_HID_EVENT_KEYBOARD_DISCONNECTED + : USB_HID_EVENT_MOUSE_DISCONNECTED; + UsbHidEvent evt = { .type = disc_type }; + publish_event(ctx, &evt); + } + break; + + case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: + LOG_W(TAG, "HID transfer error (proto=%d)", params.proto); + break; + + default: + break; + } +} + +static void hid_driver_callback(hid_host_device_handle_t handle, + const hid_host_driver_event_t event, + void* arg) +{ + auto* ctx = static_cast(arg); + hid_dev_event_t evt = { handle, event, arg }; + if (ctx->hid_event_queue) { + xQueueSend(ctx->hid_event_queue, &evt, 0); + } +} + +static void hid_proc_task(void* arg) { + auto* ctx = static_cast(arg); + LOG_I(TAG, "HID proc task started"); + + while (ctx->hid_proc_running) { + hid_dev_event_t dev_evt; + if (xQueueReceive(ctx->hid_event_queue, &dev_evt, pdMS_TO_TICKS(100)) != pdTRUE) { + if (ctx->kb_led_pending.exchange(false) && ctx->kb_handle.load()) { + uint8_t leds = (ctx->num_lock_active ? 0x01 : 0) + | (ctx->caps_lock_active ? 0x02 : 0) + | (ctx->scroll_lock_active ? 0x04 : 0); + hid_class_request_set_report(ctx->kb_handle.load(), HID_REPORT_TYPE_OUTPUT, 0, &leds, 1); + } + continue; + } + if (dev_evt.event == HID_HOST_DRIVER_EVENT_CONNECTED) { + hid_host_dev_params_t params; + if (hid_host_device_get_params(dev_evt.handle, ¶ms) != ESP_OK) continue; + + if (params.proto != HID_PROTOCOL_KEYBOARD && params.proto != HID_PROTOCOL_MOUSE) { + LOG_D(TAG, "ignoring HID interface with unhandled proto=%d", params.proto); + continue; + } + LOG_I(TAG, "HID device connected (proto=%d)", params.proto); + + const hid_host_device_config_t dev_cfg = { + .callback = hid_interface_callback, + .callback_arg = ctx, + }; + if (hid_host_device_open(dev_evt.handle, &dev_cfg) != ESP_OK) { + LOG_W(TAG, "hid_host_device_open failed"); + continue; + } + hid_class_request_set_protocol(dev_evt.handle, HID_REPORT_PROTOCOL_BOOT); + if (params.proto == HID_PROTOCOL_KEYBOARD) { + hid_class_request_set_idle(dev_evt.handle, 0, 0); + vTaskDelay(pdMS_TO_TICKS(200)); + ctx->kb_handle.store(dev_evt.handle); + uint8_t leds = (ctx->num_lock_active ? 0x01 : 0) + | (ctx->caps_lock_active ? 0x02 : 0) + | (ctx->scroll_lock_active ? 0x04 : 0); + hid_class_request_set_report(dev_evt.handle, HID_REPORT_TYPE_OUTPUT, 0, &leds, 1); + } else if (params.proto == HID_PROTOCOL_MOUSE) { + ctx->mouse_connected = true; + } + ctx->device_connected = true; + { + UsbHidEventType conn_type = (params.proto == HID_PROTOCOL_KEYBOARD) + ? USB_HID_EVENT_KEYBOARD_CONNECTED + : USB_HID_EVENT_MOUSE_CONNECTED; + UsbHidEvent evt = { .type = conn_type }; + publish_event(ctx, &evt); + } + hid_host_device_start(dev_evt.handle); + } + } + + LOG_I(TAG, "HID proc task stopped"); + xSemaphoreGive(ctx->hid_proc_task_done); + vTaskDelete(nullptr); +} + +static bool api_hid_is_connected(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + return ctx && ctx->device_connected.load(); +} + +static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx || !event_queue) return false; + if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; + bool added = false; + for (int i = 0; i < MAX_SUBSCRIBERS; i++) { + if (ctx->subscribers[i] == static_cast(event_queue)) { + added = true; + break; + } + if (!ctx->subscribers[i]) { + ctx->subscribers[i] = static_cast(event_queue); + added = true; + break; + } + } + xSemaphoreGive(ctx->sub_mutex); + if (!added) LOG_W(TAG, "subscriber list full"); + return added; +} + +static void api_hid_unsubscribe(struct Device* device, UsbHidQueueHandle event_queue) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx || !event_queue) return; + if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) { + LOG_W(TAG, "unsubscribe: mutex timeout, subscriber slot may remain stale"); + return; + } + for (int i = 0; i < MAX_SUBSCRIBERS; i++) { + if (ctx->subscribers[i] == static_cast(event_queue)) { + ctx->subscribers[i] = nullptr; + break; + } + } + xSemaphoreGive(ctx->sub_mutex); +} + +static const UsbHidApi hid_api = { + .is_connected = api_hid_is_connected, + .subscribe = api_hid_subscribe, + .unsubscribe = api_hid_unsubscribe, +}; + +extern "C" { + +static error_t start_device(struct Device* device) { + auto* ctx = new UsbHidContext(); + + ctx->sub_mutex = xSemaphoreCreateMutex(); + if (!ctx->sub_mutex) { + LOG_E(TAG, "failed to create subscriber mutex"); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->hid_event_queue = xQueueCreate(HID_EVENT_QUEUE_SIZE, sizeof(hid_dev_event_t)); + if (!ctx->hid_event_queue) { + LOG_E(TAG, "failed to create HID event queue"); + vSemaphoreDelete(ctx->sub_mutex); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->hid_proc_task_done = xSemaphoreCreateBinary(); + if (!ctx->hid_proc_task_done) { + LOG_E(TAG, "failed to create task done semaphore"); + vQueueDelete(ctx->hid_event_queue); + vSemaphoreDelete(ctx->sub_mutex); + delete ctx; + return ERROR_RESOURCE; + } + + const hid_host_driver_config_t hid_cfg = { + .create_background_task = true, + .task_priority = HID_PROC_TASK_PRIORITY, + .stack_size = HID_PROC_TASK_STACK, + .core_id = tskNO_AFFINITY, + .callback = hid_driver_callback, + .callback_arg = ctx, + }; + if (hid_host_install(&hid_cfg) != ESP_OK) { + LOG_E(TAG, "hid_host_install failed"); + vQueueDelete(ctx->hid_event_queue); + vSemaphoreDelete(ctx->hid_proc_task_done); + vSemaphoreDelete(ctx->sub_mutex); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->hid_proc_running = true; + BaseType_t result = xTaskCreate(hid_proc_task, "hid_proc", HID_PROC_TASK_STACK, + ctx, HID_PROC_TASK_PRIORITY, &ctx->hid_proc_task); + if (result != pdPASS) { + LOG_E(TAG, "failed to create hid_proc task"); + ctx->hid_proc_running = false; + hid_host_uninstall(); + vQueueDelete(ctx->hid_event_queue); + vSemaphoreDelete(ctx->hid_proc_task_done); + vSemaphoreDelete(ctx->sub_mutex); + delete ctx; + return ERROR_RESOURCE; + } + + device_set_driver_data(device, ctx); + LOG_I(TAG, "started"); + return ERROR_NONE; +} + +static error_t stop_device(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return ERROR_NONE; + + ctx->hid_proc_running = false; + + if (xSemaphoreTake(ctx->hid_proc_task_done, pdMS_TO_TICKS(HID_STOP_TIMEOUT_MS)) != pdTRUE) { + LOG_W(TAG, "HID proc task stop timed out, force terminating"); + vTaskDelete(ctx->hid_proc_task); + } + ctx->hid_proc_task = nullptr; + vSemaphoreDelete(ctx->hid_proc_task_done); + + hid_host_uninstall(); + + if (ctx->hid_event_queue) { vQueueDelete(ctx->hid_event_queue); ctx->hid_event_queue = nullptr; } + if (ctx->sub_mutex) { vSemaphoreDelete(ctx->sub_mutex); ctx->sub_mutex = nullptr; } + + device_set_driver_data(device, nullptr); + delete ctx; + LOG_I(TAG, "stopped"); + return ERROR_NONE; +} + +Driver esp32_usbhost_hid_driver = { + .name = "esp32_usbhost_hid", + .compatible = (const char*[]) { "espressif,esp32-usbhost-hid", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &hid_api, + .device_type = &USB_HOST_HID_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} // extern "C" + +#endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp new file mode 100644 index 000000000..68ac3a7db --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp @@ -0,0 +1,314 @@ +#include +#ifdef CONFIG_SOC_USB_OTG_SUPPORTED + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "esp32_usbhost_midi" + +constexpr auto MIDI_TASK_STACK = 4096; +constexpr auto MIDI_TASK_PRIORITY = 5; +constexpr auto MIDI_STOP_TIMEOUT_MS = 2000; +constexpr auto MIDI_TRANSFER_BUF_SIZE = 512; + +constexpr uint8_t MIDI_INTF_CLASS = 0x01; +constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03; + +struct UsbMidiContext { + usb_host_client_handle_t client_hdl = nullptr; + usb_device_handle_t dev_hdl = nullptr; + usb_transfer_t* transfer = nullptr; + uint8_t ep_addr = 0; + uint8_t intf_num = 0; + std::atomic connected{false}; + std::atomic running{false}; + TaskHandle_t task_handle = nullptr; + SemaphoreHandle_t task_done = nullptr; + + portMUX_TYPE callback_lock = portMUX_INITIALIZER_UNLOCKED; + usb_midi_message_cb_t callback = nullptr; + void* callback_arg = nullptr; +}; + +static bool find_midi_interface(const usb_config_desc_t* cfg, uint8_t* out_intf, uint8_t* out_ep) { + int offset = 0; + const usb_standard_desc_t* cur = reinterpret_cast(cfg); + + while ((cur = usb_parse_next_descriptor_of_type( + cur, cfg->wTotalLength, USB_W_VALUE_DT_INTERFACE, &offset)) != nullptr) { + const auto* intf = reinterpret_cast(cur); + if (intf->bInterfaceClass != MIDI_INTF_CLASS || intf->bInterfaceSubClass != MIDI_INTF_SUBCLASS) continue; + + int ep_offset = offset; + const usb_standard_desc_t* ep_cur = cur; + for (int e = 0; e < intf->bNumEndpoints; e++) { + ep_cur = usb_parse_next_descriptor_of_type(ep_cur, cfg->wTotalLength, USB_W_VALUE_DT_ENDPOINT, &ep_offset); + if (!ep_cur) break; + const auto* ep = reinterpret_cast(ep_cur); + usb_transfer_type_t xtype = USB_EP_DESC_GET_XFERTYPE(ep); + bool is_in = USB_EP_DESC_GET_EP_DIR(ep) == 1; + if (is_in && (xtype == USB_TRANSFER_TYPE_BULK || xtype == USB_TRANSFER_TYPE_INTR)) { + *out_intf = intf->bInterfaceNumber; + *out_ep = ep->bEndpointAddress; + return true; + } + } + } + return false; +} + +static void dispatch_midi_packets(UsbMidiContext* ctx, const uint8_t* buf, int len) { + usb_midi_message_cb_t cb; + void* arg; + taskENTER_CRITICAL(&ctx->callback_lock); + cb = ctx->callback; + arg = ctx->callback_arg; + taskEXIT_CRITICAL(&ctx->callback_lock); + if (!cb) return; + + for (int i = 0; i + 3 < len; i += 4) { + uint8_t cin = buf[i] & 0x0F; + if (cin < 0x02) continue; + usb_midi_message_t msg = { + .cable = static_cast(buf[i] >> 4), + .status = buf[i + 1], + .data1 = buf[i + 2], + .data2 = buf[i + 3], + }; + cb(&msg, arg); + } +} + +static void midi_transfer_cb(usb_transfer_t* transfer) { + auto* ctx = static_cast(transfer->context); + if (transfer->status == USB_TRANSFER_STATUS_COMPLETED && transfer->actual_num_bytes > 0) { + dispatch_midi_packets(ctx, transfer->data_buffer, transfer->actual_num_bytes); + } + if (ctx->running && ctx->connected && transfer->status != USB_TRANSFER_STATUS_NO_DEVICE) { + transfer->num_bytes = MIDI_TRANSFER_BUF_SIZE; + if (usb_host_transfer_submit(transfer) != ESP_OK) { + LOG_E(TAG, "failed to resubmit MIDI transfer"); + } + } +} + +static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) { + auto* ctx = static_cast(arg); + + if (msg->event == USB_HOST_CLIENT_EVENT_NEW_DEV) { + if (ctx->dev_hdl != nullptr || ctx->connected.load()) { + LOG_W(TAG, "ignoring additional MIDI device while one is already active"); + return; + } + uint8_t addr = msg->new_dev.address; + usb_device_handle_t dev_hdl = nullptr; + if (usb_host_device_open(ctx->client_hdl, addr, &dev_hdl) != ESP_OK) return; + + const usb_config_desc_t* cfg = nullptr; + if (usb_host_get_active_config_descriptor(dev_hdl, &cfg) != ESP_OK) { + usb_host_device_close(ctx->client_hdl, dev_hdl); + return; + } + + uint8_t intf_num = 0, ep_addr = 0; + if (!find_midi_interface(cfg, &intf_num, &ep_addr)) { + LOG_D(TAG, "USB device addr=%d: no MIDI Streaming interface found", addr); + usb_host_device_close(ctx->client_hdl, dev_hdl); + return; + } + + if (usb_host_interface_claim(ctx->client_hdl, dev_hdl, intf_num, 0) != ESP_OK) { + LOG_E(TAG, "failed to claim MIDI interface %d", intf_num); + usb_host_device_close(ctx->client_hdl, dev_hdl); + return; + } + + ctx->dev_hdl = dev_hdl; + ctx->intf_num = intf_num; + ctx->ep_addr = ep_addr; + + ctx->transfer->device_handle = dev_hdl; + ctx->transfer->bEndpointAddress = ep_addr; + ctx->transfer->num_bytes = MIDI_TRANSFER_BUF_SIZE; + ctx->transfer->callback = midi_transfer_cb; + ctx->transfer->context = ctx; + ctx->transfer->timeout_ms = 0; + + if (usb_host_transfer_submit(ctx->transfer) == ESP_OK) { + ctx->connected = true; + LOG_I(TAG, "MIDI device connected (intf=%d ep=0x%02x)", intf_num, ep_addr); + } else { + LOG_E(TAG, "failed to submit initial MIDI transfer"); + usb_host_interface_release(ctx->client_hdl, dev_hdl, intf_num); + usb_host_device_close(ctx->client_hdl, dev_hdl); + ctx->dev_hdl = nullptr; + } + + } else if (msg->event == USB_HOST_CLIENT_EVENT_DEV_GONE) { + if (ctx->dev_hdl && msg->dev_gone.dev_hdl == ctx->dev_hdl) { + LOG_I(TAG, "MIDI device disconnected"); + ctx->connected = false; + usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num); + usb_host_device_close(ctx->client_hdl, ctx->dev_hdl); + ctx->dev_hdl = nullptr; + } + } +} + +static void midi_client_task(void* arg) { + auto* ctx = static_cast(arg); + LOG_I(TAG, "MIDI client task started"); + + while (ctx->running) { + usb_host_client_handle_events(ctx->client_hdl, pdMS_TO_TICKS(100)); + } + + if (ctx->dev_hdl) { + ctx->connected = false; + usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num); + usb_host_device_close(ctx->client_hdl, ctx->dev_hdl); + ctx->dev_hdl = nullptr; + } + + LOG_I(TAG, "MIDI client task stopped"); + xSemaphoreGive(ctx->task_done); + vTaskDelete(nullptr); +} + +static void api_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return; + taskENTER_CRITICAL(&ctx->callback_lock); + ctx->callback = callback; + ctx->callback_arg = user_data; + taskEXIT_CRITICAL(&ctx->callback_lock); +} + +static bool api_is_connected(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + return ctx && ctx->connected.load(); +} + +static const UsbMidiApi midi_api = { + .set_callback = api_set_callback, + .is_connected = api_is_connected, +}; + +extern "C" { + +static error_t start_device(struct Device* device) { + auto* ctx = new UsbMidiContext(); + + if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) { + LOG_E(TAG, "failed to allocate MIDI transfer"); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->task_done = xSemaphoreCreateBinary(); + if (!ctx->task_done) { + LOG_E(TAG, "failed to create task done semaphore"); + usb_host_transfer_free(ctx->transfer); + delete ctx; + return ERROR_RESOURCE; + } + + const usb_host_client_config_t client_cfg = { + .is_synchronous = false, + .max_num_event_msg = 5, + .async = { + .client_event_callback = client_event_cb, + .callback_arg = ctx, + }, + }; + if (usb_host_client_register(&client_cfg, &ctx->client_hdl) != ESP_OK) { + LOG_E(TAG, "failed to register USB host client"); + vSemaphoreDelete(ctx->task_done); + usb_host_transfer_free(ctx->transfer); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->running = true; + BaseType_t result = xTaskCreate(midi_client_task, "midi_client", MIDI_TASK_STACK, + ctx, MIDI_TASK_PRIORITY, &ctx->task_handle); + if (result != pdPASS) { + LOG_E(TAG, "failed to create midi_client task"); + ctx->running = false; + usb_host_client_deregister(ctx->client_hdl); + vSemaphoreDelete(ctx->task_done); + usb_host_transfer_free(ctx->transfer); + delete ctx; + return ERROR_RESOURCE; + } + + device_set_driver_data(device, ctx); + LOG_I(TAG, "started"); + return ERROR_NONE; +} + +static error_t stop_device(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return ERROR_NONE; + + ctx->running = false; + usb_host_client_unblock(ctx->client_hdl); + + if (xSemaphoreTake(ctx->task_done, pdMS_TO_TICKS(MIDI_STOP_TIMEOUT_MS)) != pdTRUE) { + LOG_E(TAG, "MIDI client task stop timed out after %dms — a full USB host restart may be required", MIDI_STOP_TIMEOUT_MS); + if (ctx->dev_hdl) { + ctx->connected = false; + if (usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num) != ESP_OK) { + LOG_W(TAG, "failed to release MIDI interface during force-stop"); + } + if (usb_host_device_close(ctx->client_hdl, ctx->dev_hdl) != ESP_OK) { + LOG_W(TAG, "failed to close MIDI device during force-stop"); + } + ctx->dev_hdl = nullptr; + } + vTaskDelete(ctx->task_handle); + vTaskDelay(pdMS_TO_TICKS(50)); + } + ctx->task_handle = nullptr; + vSemaphoreDelete(ctx->task_done); + + usb_host_client_deregister(ctx->client_hdl); + ctx->client_hdl = nullptr; + + usb_host_transfer_free(ctx->transfer); + ctx->transfer = nullptr; + + device_set_driver_data(device, nullptr); + delete ctx; + LOG_I(TAG, "stopped"); + return ERROR_NONE; +} + +Driver esp32_usbhost_midi_driver = { + .name = "esp32_usbhost_midi", + .compatible = (const char*[]) { "espressif,esp32-usbhost-midi", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &midi_api, + .device_type = &USB_HOST_MIDI_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} // extern "C" + +#endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp new file mode 100644 index 000000000..6ec3ee65a --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp @@ -0,0 +1,377 @@ +#include +#ifdef CONFIG_SOC_USB_OTG_SUPPORTED + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#define TAG "esp32_usbhost_msc" + +#define USB_MSC_MOUNT_PATH_PREFIX "/usb" + +constexpr auto MAX_MSC_DEVICES = 2; +constexpr auto MSC_EVENT_QUEUE_SIZE = 8; +constexpr auto MSC_PROC_TASK_STACK = 4096; +constexpr auto MSC_PROC_TASK_PRIORITY = 5; +constexpr auto MSC_STOP_TIMEOUT_MS = 2000; + +typedef struct { + uint8_t usb_addr; + msc_host_device_handle_t device; + msc_host_vfs_handle_t vfs; + char mount_path[16]; + struct FileSystem* fs_entry; + bool mounted; +} msc_dev_entry_t; + +// The anonymous enum inside msc_host_event_t is C-only scoped; use a local alias in C++. +using msc_event_id_t = decltype(msc_host_event_t{}.event); +static constexpr msc_event_id_t kMscDeviceConnected = static_cast(0); +static constexpr msc_event_id_t kMscDeviceDisconnected = static_cast(1); + +enum class MscMsgId : uint8_t { Connected, Disconnected }; + +typedef struct { + MscMsgId id; + union { + uint8_t address; + msc_host_device_handle_t handle; + }; +} msc_msg_t; + +struct UsbMscContext { + msc_dev_entry_t* devs[MAX_MSC_DEVICES] = {}; + portMUX_TYPE devs_lock = portMUX_INITIALIZER_UNLOCKED; + QueueHandle_t event_queue = nullptr; + TaskHandle_t proc_task = nullptr; + SemaphoreHandle_t proc_task_done = nullptr; + std::atomic proc_running{false}; +}; + +static error_t usb_fs_mount(void* /*data*/) { return ERROR_NONE; } +static error_t usb_fs_unmount(void* /*data*/) { return ERROR_NONE; } +static bool usb_fs_is_mounted(void* data) { + if (!data) return false; + return static_cast(data)->mounted; +} +static error_t usb_fs_get_path(void* data, char* out_path, size_t out_path_size) { + if (!data) return ERROR_INVALID_ARGUMENT; + snprintf(out_path, out_path_size, "%s", static_cast(data)->mount_path); + return ERROR_NONE; +} + +static const FileSystemApi usb_fs_api = { + .mount = usb_fs_mount, + .unmount = usb_fs_unmount, + .is_mounted = usb_fs_is_mounted, + .get_path = usb_fs_get_path, +}; + +static int find_free_slot(UsbMscContext* ctx) { + for (int i = 0; i < MAX_MSC_DEVICES; i++) { + if (ctx->devs[i] == nullptr) return i; + } + return -1; +} + +static int find_slot_by_handle(UsbMscContext* ctx, msc_host_device_handle_t handle) { + for (int i = 0; i < MAX_MSC_DEVICES; i++) { + if (ctx->devs[i] && ctx->devs[i]->device == handle) return i; + } + return -1; +} + +static void free_msc_device(UsbMscContext* ctx, int slot) { + if (slot < 0 || slot >= MAX_MSC_DEVICES || !ctx->devs[slot]) return; + if (ctx->devs[slot]->fs_entry) { + ctx->devs[slot]->mounted = false; + file_system_remove(ctx->devs[slot]->fs_entry); + ctx->devs[slot]->fs_entry = nullptr; + } + if (ctx->devs[slot]->vfs) { + msc_host_vfs_unregister(ctx->devs[slot]->vfs); + } + if (ctx->devs[slot]->device) { + msc_host_uninstall_device(ctx->devs[slot]->device); + } + free(ctx->devs[slot]); + ctx->devs[slot] = nullptr; +} + +static void free_all_msc_devices(UsbMscContext* ctx) { + for (int i = 0; i < MAX_MSC_DEVICES; i++) { + free_msc_device(ctx, i); + } +} + +static void msc_event_cb(const msc_host_event_t* event, void* arg) { + auto* ctx = static_cast(arg); + if (!ctx->event_queue) return; + msc_msg_t msg = {}; + if (event->event == kMscDeviceConnected) { + msg.id = MscMsgId::Connected; + msg.address = event->device.address; + if (xQueueSend(ctx->event_queue, &msg, 0) != pdTRUE) { + LOG_W(TAG, "event queue full, dropped Connected event (addr=%d)", event->device.address); + } + } else if (event->event == kMscDeviceDisconnected) { + msg.id = MscMsgId::Disconnected; + msg.handle = event->device.handle; + if (xQueueSend(ctx->event_queue, &msg, 0) != pdTRUE) { + LOG_W(TAG, "event queue full, dropped Disconnected event"); + } + } +} + +static void msc_proc_task(void* arg) { + auto* ctx = static_cast(arg); + LOG_I(TAG, "MSC proc task started"); + + while (ctx->proc_running) { + msc_msg_t msg; + if (xQueueReceive(ctx->event_queue, &msg, pdMS_TO_TICKS(100)) != pdTRUE) continue; + + if (msg.id == MscMsgId::Connected) { + LOG_I(TAG, "USB drive connected (addr=%d)", msg.address); + + // Find a free slot under the lock, then allocate outside it. + taskENTER_CRITICAL(&ctx->devs_lock); + int slot = find_free_slot(ctx); + taskEXIT_CRITICAL(&ctx->devs_lock); + if (slot < 0) { + LOG_W(TAG, "no free slots for USB drive"); + continue; + } + auto* entry = static_cast(calloc(1, sizeof(msc_dev_entry_t))); + if (!entry) { + LOG_E(TAG, "failed to allocate MSC device entry"); + continue; + } + // Re-check the slot is still free before committing. + taskENTER_CRITICAL(&ctx->devs_lock); + if (ctx->devs[slot] != nullptr) { + slot = find_free_slot(ctx); // another path claimed it; find a new one + if (slot >= 0) ctx->devs[slot] = entry; + } else { + ctx->devs[slot] = entry; + } + taskEXIT_CRITICAL(&ctx->devs_lock); + if (slot < 0) { + free(entry); + LOG_W(TAG, "no free slots for USB drive after allocation"); + continue; + } + + if (msc_host_install_device(msg.address, &ctx->devs[slot]->device) != ESP_OK) { + LOG_E(TAG, "msc_host_install_device failed"); + taskENTER_CRITICAL(&ctx->devs_lock); + free(ctx->devs[slot]); + ctx->devs[slot] = nullptr; + taskEXIT_CRITICAL(&ctx->devs_lock); + continue; + } + ctx->devs[slot]->usb_addr = msg.address; + snprintf(ctx->devs[slot]->mount_path, sizeof(ctx->devs[slot]->mount_path), + USB_MSC_MOUNT_PATH_PREFIX "%d", slot); + const char* mount_path = ctx->devs[slot]->mount_path; + + const esp_vfs_fat_mount_config_t mount_cfg = { + .format_if_mount_failed = false, + .max_files = 4, + .allocation_unit_size = 4096, + .disk_status_check_enable = false, + .use_one_fat = false, + }; + esp_err_t vfs_err = msc_host_vfs_register(ctx->devs[slot]->device, mount_path, &mount_cfg, &ctx->devs[slot]->vfs); + if (vfs_err != ESP_OK) { + LOG_E(TAG, "msc_host_vfs_register failed for %s: %s", mount_path, esp_err_to_name(vfs_err)); + msc_host_uninstall_device(ctx->devs[slot]->device); + taskENTER_CRITICAL(&ctx->devs_lock); + free(ctx->devs[slot]); + ctx->devs[slot] = nullptr; + taskEXIT_CRITICAL(&ctx->devs_lock); + continue; + } + LOG_I(TAG, "USB drive mounted at %s", mount_path); + ctx->devs[slot]->fs_entry = file_system_add(&usb_fs_api, ctx->devs[slot]); + if (!ctx->devs[slot]->fs_entry) { + LOG_E(TAG, "failed to register filesystem for %s", mount_path); + free_msc_device(ctx, slot); + continue; + } + ctx->devs[slot]->mounted = true; + + } else if (msg.id == MscMsgId::Disconnected) { + taskENTER_CRITICAL(&ctx->devs_lock); + int slot = find_slot_by_handle(ctx, msg.handle); + taskEXIT_CRITICAL(&ctx->devs_lock); + if (slot >= 0) { + LOG_I(TAG, "USB drive disconnected, unmounting slot %d", slot); + free_msc_device(ctx, slot); + } + } + } + + free_all_msc_devices(ctx); + LOG_I(TAG, "MSC proc task stopped"); + xSemaphoreGive(ctx->proc_task_done); + vTaskDelete(nullptr); +} + +static bool api_eject(struct Device* device, const char* mount_path) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return false; + + taskENTER_CRITICAL(&ctx->devs_lock); + msc_dev_entry_t* entry = nullptr; + int found = -1; + for (int i = 0; i < MAX_MSC_DEVICES; i++) { + if (ctx->devs[i] && strcmp(ctx->devs[i]->mount_path, mount_path) == 0) { + found = i; + entry = ctx->devs[i]; + ctx->devs[i] = nullptr; // claim atomically under the lock + break; + } + } + taskEXIT_CRITICAL(&ctx->devs_lock); + + if (found >= 0) { + LOG_I(TAG, "ejecting USB drive at %s (slot %d)", mount_path, found); + // Free outside the lock; slot is already claimed (nullptr) so msc_proc_task won't touch it. + if (entry->fs_entry) { + entry->mounted = false; + file_system_remove(entry->fs_entry); + entry->fs_entry = nullptr; + } + if (entry->vfs) { + msc_host_vfs_unregister(entry->vfs); + } + if (entry->device) { + msc_host_uninstall_device(entry->device); + } + free(entry); + LOG_I(TAG, "USB drive ejected, safe to remove"); + return true; + } + LOG_W(TAG, "no drive mounted at %s", mount_path); + return false; +} + +static const UsbMscApi msc_api = { + .eject = api_eject, +}; + +extern "C" { + +static error_t start_device(struct Device* device) { + auto* ctx = new UsbMscContext(); + + ctx->event_queue = xQueueCreate(MSC_EVENT_QUEUE_SIZE, sizeof(msc_msg_t)); + if (!ctx->event_queue) { + LOG_E(TAG, "failed to create MSC event queue"); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->proc_task_done = xSemaphoreCreateBinary(); + if (!ctx->proc_task_done) { + LOG_E(TAG, "failed to create task done semaphore"); + vQueueDelete(ctx->event_queue); + delete ctx; + return ERROR_RESOURCE; + } + + const msc_host_driver_config_t msc_cfg = { + .create_backround_task = true, + .task_priority = MSC_PROC_TASK_PRIORITY, + .stack_size = MSC_PROC_TASK_STACK, + .core_id = tskNO_AFFINITY, + .callback = msc_event_cb, + .callback_arg = ctx, + }; + if (msc_host_install(&msc_cfg) != ESP_OK) { + LOG_E(TAG, "msc_host_install failed"); + vQueueDelete(ctx->event_queue); + vSemaphoreDelete(ctx->proc_task_done); + delete ctx; + return ERROR_RESOURCE; + } + + ctx->proc_running = true; + BaseType_t result = xTaskCreate(msc_proc_task, "msc_proc", MSC_PROC_TASK_STACK, + ctx, MSC_PROC_TASK_PRIORITY, &ctx->proc_task); + if (result != pdPASS) { + LOG_E(TAG, "failed to create msc_proc task"); + ctx->proc_running = false; + msc_host_uninstall(); + vQueueDelete(ctx->event_queue); + vSemaphoreDelete(ctx->proc_task_done); + delete ctx; + return ERROR_RESOURCE; + } + + device_set_driver_data(device, ctx); + LOG_I(TAG, "started"); + return ERROR_NONE; +} + +static error_t stop_device(struct Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) return ERROR_NONE; + + ctx->proc_running = false; + + bool exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(MSC_STOP_TIMEOUT_MS)) == pdTRUE); + if (!exited) { + // Retry once with a short extra wait before resorting to force-delete. + exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(500)) == pdTRUE); + } + if (!exited) { + LOG_W(TAG, "MSC proc task stop timed out, force terminating"); + vTaskDelete(ctx->proc_task); + vTaskDelay(pdMS_TO_TICKS(50)); + // Task was killed mid-cleanup — free devices ourselves as best-effort. + free_all_msc_devices(ctx); + } + ctx->proc_task = nullptr; + vSemaphoreDelete(ctx->proc_task_done); + + msc_host_uninstall(); + + if (ctx->event_queue) { vQueueDelete(ctx->event_queue); ctx->event_queue = nullptr; } + + device_set_driver_data(device, nullptr); + delete ctx; + LOG_I(TAG, "stopped"); + return ERROR_NONE; +} + +Driver esp32_usbhost_msc_driver = { + .name = "esp32_usbhost_msc", + .compatible = (const char*[]) { "espressif,esp32-usbhost-msc", nullptr }, + .start_device = start_device, + .stop_device = stop_device, + .api = &msc_api, + .device_type = &USB_HOST_MSC_TYPE, + .owner = nullptr, + .internal = nullptr, +}; + +} // extern "C" + +#endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 2cd39e52a..b734841df 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -25,6 +25,12 @@ extern Driver esp32_ble_serial_driver; extern Driver esp32_ble_midi_driver; extern Driver esp32_ble_hid_device_driver; #endif +#if SOC_USB_OTG_SUPPORTED +extern Driver esp32_usbhost_driver; +extern Driver esp32_usbhost_hid_driver; +extern Driver esp32_usbhost_midi_driver; +extern Driver esp32_usbhost_msc_driver; +#endif static error_t start() { /* We crash when construct fails, because if a single driver fails to construct, @@ -42,6 +48,12 @@ static error_t start() { check(driver_construct_add(&esp32_ble_serial_driver) == ERROR_NONE); check(driver_construct_add(&esp32_ble_midi_driver) == ERROR_NONE); check(driver_construct_add(&esp32_ble_hid_device_driver) == ERROR_NONE); +#endif +#if SOC_USB_OTG_SUPPORTED + check(driver_construct_add(&esp32_usbhost_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_usbhost_hid_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_usbhost_midi_driver) == ERROR_NONE); + check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE); #endif return ERROR_NONE; } @@ -49,6 +61,12 @@ static error_t start() { static error_t stop() { /* We crash when destruct fails, because if a single driver fails to destruct, * there is no guarantee that the previously destroyed drivers can be recovered */ +#if SOC_USB_OTG_SUPPORTED + check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE); + check(driver_remove_destruct(&esp32_usbhost_midi_driver) == ERROR_NONE); + check(driver_remove_destruct(&esp32_usbhost_hid_driver) == ERROR_NONE); + check(driver_remove_destruct(&esp32_usbhost_driver) == ERROR_NONE); +#endif #if defined(CONFIG_BT_NIMBLE_ENABLED) check(driver_remove_destruct(&esp32_ble_hid_device_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_ble_midi_driver) == ERROR_NONE); diff --git a/Tactility/Include/Tactility/lvgl/UsbHidInput.h b/Tactility/Include/Tactility/lvgl/UsbHidInput.h new file mode 100644 index 000000000..a46568093 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/UsbHidInput.h @@ -0,0 +1,13 @@ +#pragma once + +namespace tt::lvgl { + +#ifdef ESP_PLATFORM +void startUsbHidInput(); +void stopUsbHidInput(); +#else +inline void startUsbHidInput() {} +inline void stopUsbHidInput() {} +#endif + +} // namespace tt::lvgl diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index c13a2c9d7..c29b9fba2 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -29,6 +29,7 @@ class View final { void showActions(); void showActionsForDirectory(); void showActionsForFile(); + void showActionsForMountPoint(); void viewFile(const std::string&path, const std::string&filename); void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry); @@ -51,6 +52,7 @@ class View final { void onCopyPressed(); void onCutPressed(); void onPastePressed(); + void onEjectPressed(); void onDirEntryListScrollBegin(); void onResult(LaunchId launchId, Result result, std::unique_ptr bundle); void deinit(const AppContext& appContext); diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 1e4423b46..37f87444c 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -16,6 +16,10 @@ #include #include +#include +#include + +#include #include #include #include @@ -84,6 +88,11 @@ static void onCutPressedCallback(lv_event_t* event) { view->onCutPressed(); } +static void onEjectPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onEjectPressed(); +} + static void onPastePressedCallback(lv_event_t* event) { auto* view = static_cast(lv_event_get_user_data(event)); view->onPastePressed(); @@ -275,18 +284,24 @@ void View::onDirEntryPressed(uint32_t index) { } void View::onDirEntryLongPressed(int32_t index) { - if (state->getCurrentPath() == "/") { - return; - } - dirent dir_entry; if (!resolveDirentFromListIndex(index, dir_entry)) { return; } - LOGGER.info("Pressed {} {}", dir_entry.d_name, dir_entry.d_type); + LOGGER.info("Long-pressed {} {}", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + if (state->getCurrentPath() == "/") { + // At root, only USB mount points support actions (eject). + // Other root-level entries intentionally have no context actions. + const char* name = dir_entry.d_name; + if (strncmp(name, "usb", 3) == 0 && isdigit((unsigned char)name[3])) { + showActionsForMountPoint(); + } + return; + } + using namespace file; switch (dir_entry.d_type) { case TT_DT_DIR: @@ -410,6 +425,30 @@ void View::showActions() { void View::showActionsForDirectory() { showActions(); } void View::showActionsForFile() { showActions(); } +void View::showActionsForMountPoint() { + lv_obj_clean(action_list); + + auto* eject_button = lv_list_add_button(action_list, LV_SYMBOL_EJECT, "Eject"); + lv_obj_add_event_cb(eject_button, onEjectPressedCallback, LV_EVENT_SHORT_CLICKED, this); + + lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN); +} + +void View::onEjectPressed() { + std::string mount_path = state->getSelectedChildPath(); + LOGGER.info("Ejecting {}", mount_path); + + struct Device* msc_dev = device_find_first_active_by_type(&USB_HOST_MSC_TYPE); + if (!msc_dev || !usb_msc_eject(msc_dev, mount_path.c_str())) { + LOGGER.warn("usb_msc_eject: {} not found", mount_path); + alertdialog::start("Eject failed", "Could not eject \"" + file::getLastPathSegment(mount_path) + "\"."); + } + + onNavigate(); + state->setEntriesForPath(state->getCurrentPath()); + update(); +} + void View::update(size_t start_index) { const bool is_root = (state->getCurrentPath() == "/"); diff --git a/Tactility/Source/lvgl/UsbHidInput.cpp b/Tactility/Source/lvgl/UsbHidInput.cpp new file mode 100644 index 000000000..63a7743d8 --- /dev/null +++ b/Tactility/Source/lvgl/UsbHidInput.cpp @@ -0,0 +1,365 @@ +#include + +#ifdef ESP_PLATFORM + +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include + +namespace tt::lvgl { + +static const auto LOGGER = Logger("UsbHidInput"); + +constexpr auto HID_EVENT_QUEUE_SIZE = 64; +constexpr auto KEY_EVENT_QUEUE_SIZE = 64; +constexpr auto TASK_STACK = 3072; +constexpr auto TASK_PRIORITY = 5; +constexpr auto STOP_TIMEOUT_MS = 2000; +constexpr uint32_t KEY_REPEAT_DELAY_MS = 500; +constexpr uint32_t KEY_REPEAT_RATE_MS = 50; +constexpr int32_t CURSOR_SIZE = 16; + +typedef struct { + uint32_t lv_key; + bool pressed; +} KeyEvent; + +struct UsbHidInputCtx { + // Receives raw UsbHidEvent items from the HID driver + QueueHandle_t hid_queue = nullptr; + // Key-only events forwarded to the keyboard read callback + QueueHandle_t key_queue = nullptr; + TaskHandle_t task = nullptr; + SemaphoreHandle_t task_done = nullptr; + std::atomic running{false}; + std::atomic subscribed{false}; + + lv_indev_t* mouse_indev = nullptr; + lv_indev_t* kb_indev = nullptr; + lv_obj_t* mouse_cursor = nullptr; + + std::atomic mouse_x{0}; + std::atomic mouse_y{0}; + std::atomic mouse_btn1{false}; + bool mouse_connected = false; + + uint32_t repeat_lv_key = 0; + uint32_t repeat_start_ms = 0; + uint32_t repeat_last_ms = 0; + bool emit_repeat_release = false; + uint32_t repeat_release_key = 0; +}; + +static UsbHidInputCtx* s_ctx = nullptr; + +static void mouse_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { + auto* ctx = static_cast(lv_indev_get_user_data(indev)); + int32_t cx = ctx->mouse_x.load(); + int32_t cy = ctx->mouse_y.load(); + + lv_display_t* disp = lv_display_get_default(); + if (disp) { + int32_t ow = lv_display_get_original_horizontal_resolution(disp); + int32_t oh = lv_display_get_original_vertical_resolution(disp); + switch (lv_display_get_rotation(disp)) { + case LV_DISPLAY_ROTATION_0: + data->point.x = (lv_coord_t)cx; + data->point.y = (lv_coord_t)cy; + break; + case LV_DISPLAY_ROTATION_90: + data->point.x = (lv_coord_t)cy; + data->point.y = (lv_coord_t)(oh - cx - 1); + break; + case LV_DISPLAY_ROTATION_180: + data->point.x = (lv_coord_t)(ow - cx - 1); + data->point.y = (lv_coord_t)(oh - cy - 1); + break; + case LV_DISPLAY_ROTATION_270: + data->point.x = (lv_coord_t)(ow - cy - 1); + data->point.y = (lv_coord_t)cx; + break; + } + } else { + data->point.x = (lv_coord_t)cx; + data->point.y = (lv_coord_t)cy; + } + + data->state = ctx->mouse_btn1.load() ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; +} + +static void keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { + auto* ctx = static_cast(lv_indev_get_user_data(indev)); + + if (ctx->emit_repeat_release) { + ctx->emit_repeat_release = false; + data->key = ctx->repeat_release_key; + data->state = LV_INDEV_STATE_RELEASED; + return; + } + + KeyEvent evt; + if (ctx->key_queue && xQueueReceive(ctx->key_queue, &evt, 0) == pdTRUE) { + data->key = evt.lv_key; + data->state = evt.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + if (evt.pressed) { + ctx->repeat_lv_key = evt.lv_key; + ctx->repeat_start_ms = lv_tick_get(); + ctx->repeat_last_ms = 0; + } else if (evt.lv_key == ctx->repeat_lv_key) { + ctx->repeat_lv_key = 0; + } + data->continue_reading = (uxQueueMessagesWaiting(ctx->key_queue) > 0); + return; + } + + uint32_t rkey = ctx->repeat_lv_key; + if (rkey != 0) { + uint32_t now_ms = lv_tick_get(); + if ((now_ms - ctx->repeat_start_ms) >= KEY_REPEAT_DELAY_MS) { + uint32_t last = ctx->repeat_last_ms; + if (last == 0 || (now_ms - last) >= KEY_REPEAT_RATE_MS) { + ctx->repeat_last_ms = now_ms; + ctx->emit_repeat_release = true; + ctx->repeat_release_key = rkey; + data->key = rkey; + data->state = LV_INDEV_STATE_PRESSED; + data->continue_reading = true; + return; + } + } + } + + data->state = LV_INDEV_STATE_RELEASED; +} + +static void usbHidInputTask(void* arg) { + auto* ctx = static_cast(arg); + LOGGER.info("started"); + + while (!lv_is_initialized()) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + + if (lock()) { + ctx->mouse_cursor = lv_image_create(lv_layer_sys()); + lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE); + lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR); + lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); + + ctx->mouse_indev = lv_indev_create(); + lv_indev_set_type(ctx->mouse_indev, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(ctx->mouse_indev, mouse_read_cb); + lv_indev_set_user_data(ctx->mouse_indev, ctx); + lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor); + + ctx->kb_indev = lv_indev_create(); + lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(ctx->kb_indev, keyboard_read_cb); + lv_indev_set_user_data(ctx->kb_indev, ctx); + lv_indev_set_group(ctx->kb_indev, lv_group_get_default()); + + unlock(); + LOGGER.info("LVGL input devices registered"); + } else { + LOGGER.warn("could not acquire LVGL lock for indev registration"); + } + + // Drain the HID event queue and route events to the appropriate destinations + while (ctx->running) { + UsbHidEvent hid_evt; + if (xQueueReceive(ctx->hid_queue, &hid_evt, pdMS_TO_TICKS(100)) != pdTRUE) { + if (!ctx->subscribed) { + struct Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE); + if (hid_dev) ctx->subscribed = usb_host_hid_subscribe(hid_dev, ctx->hid_queue); + } + continue; + } + + switch (hid_evt.type) { + case USB_HID_EVENT_KEY: { + KeyEvent key_evt = { hid_evt.key.key_code, hid_evt.key.pressed }; + xQueueSend(ctx->key_queue, &key_evt, 0); + break; + } + case USB_HID_EVENT_MOUSE_MOVE: { + lv_display_t* disp = lv_display_get_default(); + if (!disp) break; + // Use logical (post-rotation) resolution so clamping matches LVGL's coordinate space + int32_t w = lv_display_get_horizontal_resolution(disp); + int32_t h = lv_display_get_vertical_resolution(disp); + int32_t nx = ctx->mouse_x.load() + hid_evt.mouse_move.dx; + int32_t ny = ctx->mouse_y.load() + hid_evt.mouse_move.dy; + if (nx < 0) nx = 0; + if (nx > w - CURSOR_SIZE - 1) nx = w - CURSOR_SIZE - 1; + if (ny < 0) ny = 0; + if (ny > h - CURSOR_SIZE - 1) ny = h - CURSOR_SIZE - 1; + ctx->mouse_x.store(nx); + ctx->mouse_y.store(ny); + break; + } + case USB_HID_EVENT_MOUSE_BTN: + ctx->mouse_btn1.store(hid_evt.mouse_btn.button1); + break; + case USB_HID_EVENT_SCROLL: { + int32_t delta = hid_evt.scroll.delta; + uint32_t key = (delta < 0) ? USB_HID_KEY_UP : USB_HID_KEY_DOWN; + int ticks = (delta < 0) ? -delta : delta; + // Clamp to reasonable maximum to prevent queue overflow + constexpr int MAX_SCROLL_TICKS = 10; + if (ticks > MAX_SCROLL_TICKS) ticks = MAX_SCROLL_TICKS; + for (int t = 0; t < ticks; t++) { + KeyEvent press = { key, true }; + KeyEvent release = { key, false }; + xQueueSend(ctx->key_queue, &press, 0); + xQueueSend(ctx->key_queue, &release, 0); + } + break; + } + case USB_HID_EVENT_KEYBOARD_CONNECTED: + if (ctx->kb_indev && lock(pdMS_TO_TICKS(200))) { + hardware_keyboard_set_indev(ctx->kb_indev); + unlock(); + } + break; + case USB_HID_EVENT_KEYBOARD_DISCONNECTED: + if (lock(pdMS_TO_TICKS(200))) { + hardware_keyboard_set_indev(nullptr); + unlock(); + } + break; + case USB_HID_EVENT_MOUSE_CONNECTED: + ctx->mouse_connected = true; + if (ctx->mouse_cursor && lock(pdMS_TO_TICKS(200))) { + lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); + unlock(); + } + break; + case USB_HID_EVENT_MOUSE_DISCONNECTED: + ctx->mouse_connected = false; + if (ctx->mouse_cursor && lock(pdMS_TO_TICKS(200))) { + lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); + unlock(); + } + break; + default: + break; + } + } + + if (lock()) { + if (ctx->mouse_indev) { lv_indev_delete(ctx->mouse_indev); ctx->mouse_indev = nullptr; } + if (ctx->mouse_cursor) { lv_obj_delete(ctx->mouse_cursor); ctx->mouse_cursor = nullptr; } + if (ctx->kb_indev) { + hardware_keyboard_set_indev(nullptr); + lv_indev_delete(ctx->kb_indev); + ctx->kb_indev = nullptr; + } + unlock(); + } + + LOGGER.info("stopped"); + xSemaphoreGive(ctx->task_done); + vTaskDelete(nullptr); +} + +void startUsbHidInput() { + if (s_ctx != nullptr) return; + + auto* ctx = new UsbHidInputCtx(); + + ctx->hid_queue = xQueueCreate(HID_EVENT_QUEUE_SIZE, sizeof(UsbHidEvent)); + if (!ctx->hid_queue) { + LOGGER.error("failed to create HID event queue"); + delete ctx; + return; + } + + ctx->key_queue = xQueueCreate(KEY_EVENT_QUEUE_SIZE, sizeof(KeyEvent)); + if (!ctx->key_queue) { + LOGGER.error("failed to create key event queue"); + vQueueDelete(ctx->hid_queue); + delete ctx; + return; + } + + ctx->task_done = xSemaphoreCreateBinary(); + if (!ctx->task_done) { + LOGGER.error("failed to create task done semaphore"); + vQueueDelete(ctx->hid_queue); + vQueueDelete(ctx->key_queue); + delete ctx; + return; + } + + struct Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE); + if (hid_dev) ctx->subscribed = usb_host_hid_subscribe(hid_dev, ctx->hid_queue); + + ctx->running = true; + if (xTaskCreate(usbHidInputTask, "usb_hid_inp", TASK_STACK, ctx, TASK_PRIORITY, &ctx->task) != pdPASS) { + LOGGER.error("failed to create task"); + ctx->running = false; + if (hid_dev) usb_host_hid_unsubscribe(hid_dev, ctx->hid_queue); + vQueueDelete(ctx->hid_queue); + vQueueDelete(ctx->key_queue); + vSemaphoreDelete(ctx->task_done); + delete ctx; + return; + } + + s_ctx = ctx; + LOGGER.info("started"); +} + +void stopUsbHidInput() { + if (!s_ctx) return; + auto* ctx = s_ctx; + s_ctx = nullptr; + + ctx->running = false; + + if (xSemaphoreTake(ctx->task_done, pdMS_TO_TICKS(STOP_TIMEOUT_MS)) != pdTRUE) { + LOGGER.warn("task stop timed out, force terminating"); + vTaskDelete(ctx->task); + // Task was killed before it could clean up LVGL objects; do it here to + // prevent mouse_read_cb / keyboard_read_cb from running with a freed ctx. + if (lock(pdMS_TO_TICKS(200))) { + if (ctx->mouse_indev) { lv_indev_delete(ctx->mouse_indev); ctx->mouse_indev = nullptr; } + if (ctx->mouse_cursor) { lv_obj_delete(ctx->mouse_cursor); ctx->mouse_cursor = nullptr; } + if (ctx->kb_indev) { + hardware_keyboard_set_indev(nullptr); + lv_indev_delete(ctx->kb_indev); + ctx->kb_indev = nullptr; + } + unlock(); + } + } + ctx->task = nullptr; + + if (ctx->subscribed) { + struct Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE); + if (hid_dev) usb_host_hid_unsubscribe(hid_dev, ctx->hid_queue); + } + vQueueDelete(ctx->hid_queue); + vQueueDelete(ctx->key_queue); + vSemaphoreDelete(ctx->task_done); + delete ctx; + + LOGGER.info("stopped"); +} + +} // namespace tt::lvgl + +#endif // ESP_PLATFORM diff --git a/Tactility/Source/service/gui/GuiService.cpp b/Tactility/Source/service/gui/GuiService.cpp index 83b605eab..bfd255c5d 100644 --- a/Tactility/Source/service/gui/GuiService.cpp +++ b/Tactility/Source/service/gui/GuiService.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -191,12 +192,16 @@ bool GuiService::onStart(ServiceContext& service) { isStarted = true; + lvgl::startUsbHidInput(); + thread->start(); return true; } void GuiService::onStop(ServiceContext& service) { + lvgl::stopUsbHidInput(); + lock(); const auto loader = findLoaderService(); diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index fa9a156ff..7ae4619e6 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -15,12 +15,19 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#include #include +#include + namespace tt::service::statusbar { static const auto LOGGER = Logger("StatusbarService"); @@ -134,6 +141,8 @@ class StatusbarService final : public Service { const char* sdcard_last_icon = nullptr; int8_t power_icon_id; const char* power_last_icon = nullptr; + int8_t usb_icon_id; + bool usb_last_state = false; void lock() const { mutex.lock(); @@ -204,6 +213,31 @@ class StatusbarService final : public Service { } } + void updateUsbIcon() { + struct Device* hid_dev = device_find_first_active_by_type(&USB_HOST_HID_TYPE); + struct Device* midi_dev = device_find_first_active_by_type(&USB_HOST_MIDI_TYPE); + bool connected = (hid_dev && usb_host_hid_is_connected(hid_dev)) || + (midi_dev && usb_midi_is_connected(midi_dev)); + if (!connected) { + // MSC: scan filesystems for any mounted /usb* path + file_system_for_each(&connected, [](struct FileSystem* fs, void* ctx) -> bool { + if (!file_system_is_mounted(fs)) return true; + char path[64]; + if (file_system_get_path(fs, path, sizeof(path)) == ERROR_NONE) { + if (strncmp(path, USB_MSC_MOUNT_PATH_PREFIX, sizeof(USB_MSC_MOUNT_PATH_PREFIX) - 1) == 0) { + *static_cast(ctx) = true; + return false; + } + } + return true; + }); + } + if (connected != usb_last_state) { + lvgl::statusbar_icon_set_visibility(usb_icon_id, connected); + usb_last_state = connected; + } + } + void updateSdCardIcon() { auto* sdcard_fs = findSdcardFileSystem(false); // TODO: Support multiple SD cards @@ -231,6 +265,7 @@ class StatusbarService final : public Service { updateWifiIcon(); updateSdCardIcon(); updatePowerStatusIcon(); + updateUsbIcon(); lvgl::unlock(); } } @@ -241,16 +276,18 @@ class StatusbarService final : public Service { StatusbarService() { gps_icon_id = lvgl::statusbar_icon_add(); bt_icon_id = lvgl::statusbar_icon_add(); + usb_icon_id = lvgl::statusbar_icon_add(); sdcard_icon_id = lvgl::statusbar_icon_add(); wifi_icon_id = lvgl::statusbar_icon_add(); power_icon_id = lvgl::statusbar_icon_add(); } ~StatusbarService() override { + lvgl::statusbar_icon_remove(power_icon_id); lvgl::statusbar_icon_remove(wifi_icon_id); lvgl::statusbar_icon_remove(sdcard_icon_id); + lvgl::statusbar_icon_remove(usb_icon_id); lvgl::statusbar_icon_remove(bt_icon_id); - lvgl::statusbar_icon_remove(power_icon_id); lvgl::statusbar_icon_remove(gps_icon_id); } @@ -262,6 +299,8 @@ class StatusbarService final : public Service { // TODO: Make thread-safe for LVGL lvgl::statusbar_icon_set_visibility(wifi_icon_id, true); + lvgl::statusbar_icon_set_image(usb_icon_id, LVGL_ICON_STATUSBAR_USB); + lvgl::statusbar_icon_set_visibility(usb_icon_id, false); auto service = findServiceById(manifest.id); assert(service); diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index 5a2f5523b..95aeebb67 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -67,15 +67,9 @@ static const char* getChipModelName(esp_chip_model_t model) { case CHIP_ESP32C2: return "ESP32-C2"; case CHIP_ESP32C6: return "ESP32-C6"; case CHIP_ESP32H2: return "ESP32-H2"; -#ifdef CHIP_ESP32P4 case CHIP_ESP32P4: return "ESP32-P4"; -#endif -#ifdef CHIP_ESP32C5 case CHIP_ESP32C5: return "ESP32-C5"; -#endif -#ifdef CHIP_ESP32C61 case CHIP_ESP32C61: return "ESP32-C61"; -#endif default: return "Unknown"; } } diff --git a/TactilityKernel/include/tactility/device.h b/TactilityKernel/include/tactility/device.h index 104cdb8da..e5313b7ee 100644 --- a/TactilityKernel/include/tactility/device.h +++ b/TactilityKernel/include/tactility/device.h @@ -273,6 +273,14 @@ bool device_exists_of_type(const struct DeviceType* type); */ struct Device* device_find_by_name(const char* name); +/** + * Find the first started device of the given type. + * + * @param[in] type non-null device type pointer + * @return the first started device of the given type, or NULL if none found + */ +struct Device* device_find_first_active_by_type(const struct DeviceType* type); + #ifdef __cplusplus } #endif diff --git a/TactilityKernel/include/tactility/drivers/usb_host_hid.h b/TactilityKernel/include/tactility/drivers/usb_host_hid.h new file mode 100644 index 000000000..c7b2b111b --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_host_hid.h @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +typedef void* UsbHidQueueHandle; + +/** Key values — numerically identical to lv_key_t so no translation is needed. */ +typedef enum { + USB_HID_KEY_UP = 17, + USB_HID_KEY_DOWN = 18, + USB_HID_KEY_RIGHT = 19, + USB_HID_KEY_LEFT = 20, + USB_HID_KEY_ESC = 27, + USB_HID_KEY_DEL = 127, + USB_HID_KEY_BACKSPACE = 8, + USB_HID_KEY_ENTER = 10, + USB_HID_KEY_NEXT = 9, + USB_HID_KEY_PREV = 11, + USB_HID_KEY_HOME = 2, + USB_HID_KEY_END = 3, +} UsbHidKey; + +typedef enum { + USB_HID_EVENT_KEY, + USB_HID_EVENT_MOUSE_MOVE, + USB_HID_EVENT_MOUSE_BTN, + USB_HID_EVENT_SCROLL, + USB_HID_EVENT_KEYBOARD_CONNECTED, + USB_HID_EVENT_KEYBOARD_DISCONNECTED, + USB_HID_EVENT_MOUSE_CONNECTED, + USB_HID_EVENT_MOUSE_DISCONNECTED, +} UsbHidEventType; + +typedef struct { + UsbHidEventType type; + union { + struct { uint32_t key_code; bool pressed; } key; + struct { int32_t dx; int32_t dy; } mouse_move; + struct { bool button1; bool button2; } mouse_btn; + struct { int32_t delta; } scroll; + }; +} UsbHidEvent; + +struct UsbHidApi { + bool (*is_connected)(struct Device* device); + bool (*subscribe)(struct Device* device, UsbHidQueueHandle event_queue); + void (*unsubscribe)(struct Device* device, UsbHidQueueHandle event_queue); +}; + +extern const struct DeviceType USB_HOST_HID_TYPE; + +/** Returns true if any HID device (keyboard or mouse) is currently connected. */ +bool usb_host_hid_is_connected(struct Device* device); + +/** Subscribe a FreeRTOS queue to receive UsbHidEvent items from the HID driver. */ +bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue); + +/** Unsubscribe a previously subscribed queue. */ +void usb_host_hid_unsubscribe(struct Device* device, UsbHidQueueHandle event_queue); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/usb_host_midi.h b/TactilityKernel/include/tactility/drivers/usb_host_midi.h new file mode 100644 index 000000000..eb22006c2 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_host_midi.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +/** + * Decoded USB MIDI message. + * + * `status` encodes both message type and channel: + * type = status & 0xF0 (0x80=NoteOff, 0x90=NoteOn, 0xB0=CC, 0xC0=PC, 0xE0=PitchBend, ...) + * channel = status & 0x0F (0–15) + */ +typedef struct { + uint8_t cable; /**< USB cable number (0–15, almost always 0) */ + uint8_t status; /**< MIDI status byte */ + uint8_t data1; /**< First data byte */ + uint8_t data2; /**< Second data byte */ +} usb_midi_message_t; + +typedef void (*usb_midi_message_cb_t)(const usb_midi_message_t* msg, void* user_data); + +struct UsbMidiApi { + void (*set_callback)(struct Device* device, usb_midi_message_cb_t callback, void* user_data); + bool (*is_connected)(struct Device* device); +}; + +extern const struct DeviceType USB_HOST_MIDI_TYPE; + +/** + * Register a callback for incoming MIDI messages. + * Replaces any previously registered callback. Pass NULL to disable. + * @param device non-null ready USB MIDI device. + */ +// TODO: Make an interface that takes/releases control +void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data); + +/** + * Returns true if a MIDI device is currently connected and streaming. + * @param device non-null ready USB MIDI device. + */ +bool usb_midi_is_connected(struct Device* device); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/usb_host_msc.h b/TactilityKernel/include/tactility/drivers/usb_host_msc.h new file mode 100644 index 000000000..5a78184dc --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_host_msc.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +#define USB_MSC_MOUNT_PATH_PREFIX "/usb" + +struct UsbMscApi { + bool (*eject)(struct Device* device, const char* mount_path); +}; + +extern const struct DeviceType USB_HOST_MSC_TYPE; + +/** + * Safely eject a mounted USB drive. + * @param device non-null ready USB MSC device (from device_find_first_active_by_type). + * @param mount_path Full mount path (e.g. "/usb0"). + * @return true if the drive was found and ejected. + */ +bool usb_msc_eject(struct Device* device, const char* mount_path); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/source/device.cpp b/TactilityKernel/source/device.cpp index b752faae5..d01cd192c 100644 --- a/TactilityKernel/source/device.cpp +++ b/TactilityKernel/source/device.cpp @@ -370,4 +370,16 @@ Device* device_find_by_name(const char* name) { return found; } +Device* device_find_first_active_by_type(const DeviceType* type) { + Device* found = nullptr; + device_for_each_of_type(type, &found, [](Device* dev, void* ctx) -> bool { + if (device_is_ready(dev)) { + *static_cast(ctx) = dev; + return false; + } + return true; + }); + return found; +} + } // extern "C" diff --git a/TactilityKernel/source/drivers/usb_host_hid.cpp b/TactilityKernel/source/drivers/usb_host_hid.cpp new file mode 100644 index 000000000..3c64bf5f5 --- /dev/null +++ b/TactilityKernel/source/drivers/usb_host_hid.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +#define USB_HID_API(driver) ((struct UsbHidApi*)(driver)->api) + +extern "C" { + +const struct DeviceType USB_HOST_HID_TYPE = { + .name = "usb-host-hid", +}; + +bool usb_host_hid_is_connected(struct Device* device) { + auto* api = USB_HID_API(device_get_driver(device)); + return api->is_connected(device); +} + +bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) { + auto* api = USB_HID_API(device_get_driver(device)); + return api->subscribe(device, event_queue); +} + +void usb_host_hid_unsubscribe(struct Device* device, UsbHidQueueHandle event_queue) { + auto* api = USB_HID_API(device_get_driver(device)); + api->unsubscribe(device, event_queue); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/usb_host_midi.cpp b/TactilityKernel/source/drivers/usb_host_midi.cpp new file mode 100644 index 000000000..a128eb1dd --- /dev/null +++ b/TactilityKernel/source/drivers/usb_host_midi.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +#define USB_MIDI_API(driver) ((struct UsbMidiApi*)(driver)->api) + +extern "C" { + +const struct DeviceType USB_HOST_MIDI_TYPE = { + .name = "usb-host-midi", +}; + +void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) { + auto* api = USB_MIDI_API(device_get_driver(device)); + api->set_callback(device, callback, user_data); +} + +bool usb_midi_is_connected(struct Device* device) { + auto* api = USB_MIDI_API(device_get_driver(device)); + return api->is_connected(device); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/usb_host_msc.cpp b/TactilityKernel/source/drivers/usb_host_msc.cpp new file mode 100644 index 000000000..d731bac6a --- /dev/null +++ b/TactilityKernel/source/drivers/usb_host_msc.cpp @@ -0,0 +1,18 @@ +#include +#include +#include + +#define USB_MSC_API(driver) ((struct UsbMscApi*)(driver)->api) + +extern "C" { + +const struct DeviceType USB_HOST_MSC_TYPE = { + .name = "usb-host-msc", +}; + +bool usb_msc_eject(struct Device* device, const char* mount_path) { + auto* api = USB_MSC_API(device_get_driver(device)); + return api->eject && api->eject(device, mount_path); +} + +} // extern "C" diff --git a/TactilityKernel/source/kernel_symbols.c b/TactilityKernel/source/kernel_symbols.c index 59f6e2577..793d4a659 100644 --- a/TactilityKernel/source/kernel_symbols.c +++ b/TactilityKernel/source/kernel_symbols.c @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -55,6 +58,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(device_for_each_of_type), DEFINE_MODULE_SYMBOL(device_exists_of_type), DEFINE_MODULE_SYMBOL(device_find_by_name), + DEFINE_MODULE_SYMBOL(device_find_first_active_by_type), // driver DEFINE_MODULE_SYMBOL(driver_construct), DEFINE_MODULE_SYMBOL(driver_destruct), @@ -168,6 +172,16 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(bluetooth_hid_device_send_gamepad), DEFINE_MODULE_SYMBOL(bluetooth_hid_device_is_connected), DEFINE_MODULE_SYMBOL(BLUETOOTH_HID_DEVICE_TYPE), + // drivers/usb_host_hid + DEFINE_MODULE_SYMBOL(usb_host_hid_is_connected), + DEFINE_MODULE_SYMBOL(USB_HOST_HID_TYPE), + // drivers/usb_host_midi + DEFINE_MODULE_SYMBOL(usb_midi_set_callback), + DEFINE_MODULE_SYMBOL(usb_midi_is_connected), + DEFINE_MODULE_SYMBOL(USB_HOST_MIDI_TYPE), + // drivers/usb_host_msc + DEFINE_MODULE_SYMBOL(usb_msc_eject), + DEFINE_MODULE_SYMBOL(USB_HOST_MSC_TYPE), // concurrent/dispatcher DEFINE_MODULE_SYMBOL(dispatcher_alloc), DEFINE_MODULE_SYMBOL(dispatcher_free), diff --git a/device.py b/device.py index dc4b5c2ca..022419df8 100644 --- a/device.py +++ b/device.py @@ -350,6 +350,18 @@ def write_bluetooth_variables(output_file, device_properties: ConfigParser): # rapid connect/disconnect/re-pair loop. output_file.write("CONFIG_BT_NIMBLE_NVS_PERSIST=y\n") +def write_usbhost_variables(output_file, device_properties: ConfigParser): + has_usbhost = get_boolean_property_or_false(device_properties, "hardware", "usbHostEnabled") + if has_usbhost: + output_file.write("# USB Host\n") + output_file.write("CONFIG_FATFS_VOLUME_COUNT=6\n") + output_file.write("CONFIG_VFS_MAX_COUNT=10\n") + output_file.write("CONFIG_USB_HOST_HUBS_SUPPORTED=y\n") + output_file.write("CONFIG_USB_HOST_DEBOUNCE_DELAY_MS=500\n") + output_file.write("CONFIG_USB_HOST_RESET_HOLD_MS=100\n") + output_file.write("CONFIG_USB_HOST_RESET_RECOVERY_MS=100\n") + output_file.write("CONFIG_USB_HOST_SET_ADDR_RECOVERY_MS=500\n") + def write_custom_sdkconfig(output_file, device_properties: ConfigParser): if "sdkconfig" in device_properties.sections(): output_file.write("# Custom\n") @@ -369,6 +381,7 @@ def write_properties(output_file, device_properties: ConfigParser, device_id: st write_performance_improvements(output_file, device_properties) write_usb_variables(output_file, device_properties) write_bluetooth_variables(output_file, device_properties) + write_usbhost_variables(output_file, device_properties) write_custom_sdkconfig(output_file, device_properties) write_lvgl_variables(output_file, device_properties)