USB device-mode support (#613)

This commit is contained in:
Shadowtrance
2026-08-13 05:29:35 +10:00
committed by GitHub
parent 37c507544b
commit cc8be3faef
94 changed files with 3466 additions and 399 deletions
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
@@ -105,4 +106,12 @@
pin-tx = <&gpio0 17 GPIO_FLAG_NONE>; pin-tx = <&gpio0 17 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 18 GPIO_FLAG_NONE>; pin-rx = <&gpio0 18 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
@@ -110,4 +111,12 @@
pin-tx = <&gpio0 17 GPIO_FLAG_NONE>; pin-tx = <&gpio0 17 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 18 GPIO_FLAG_NONE>; pin-rx = <&gpio0 18 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/gpio_backlight.h> #include <tactility/bindings/gpio_backlight.h>
#include <bindings/rgb_display.h> #include <bindings/rgb_display.h>
@@ -123,4 +124,12 @@
pin-tx = <&gpio0 20 GPIO_FLAG_NONE>; pin-tx = <&gpio0 20 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 19 GPIO_FLAG_NONE>; pin-rx = <&gpio0 19 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -8,7 +8,7 @@ hardware.flashSize=4MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
@@ -119,4 +120,12 @@
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.bluetooth=true hardware.bluetooth=true
display.size=2.8" display.size=2.8"
+9
View File
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_wifi_pinned.h> #include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/battery_sense.h> #include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
@@ -139,4 +140,12 @@
slot = <SDMMC_HOST_SLOT_1>; slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>; bus-width = <4>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -8,7 +8,7 @@ apps.autoStartAppId=ApWebServer
hardware.target=ESP32S3 hardware.target=ESP32S3
hardware.flashSize=8MB hardware.flashSize=8MB
hardware.spiRam=false hardware.spiRam=false
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -6,6 +6,7 @@
#include <tactility/bindings/esp32_gpio.h> #include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h> #include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_adc_oneshot.h> #include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/battery_sense.h> #include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
#include <bindings/button_control.h> #include <bindings/button_control.h>
@@ -113,4 +114,12 @@
compatible = "tactility,button-control"; compatible = "tactility,button-control";
pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; pin-primary = <&gpio0 0 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -11,7 +11,7 @@ hardware.flashMode=DIO
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=AUTO hardware.spiRamMode=AUTO
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/sx1262.h> #include <bindings/sx1262.h>
#include <bindings/xl9555.h> #include <bindings/xl9555.h>
@@ -179,4 +180,12 @@
dio2-as-rf-switch; dio2-as-rf-switch;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -14,6 +14,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h> #include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/gt911.h> #include <bindings/gt911.h>
#include <bindings/st7789.h> #include <bindings/st7789.h>
@@ -198,4 +199,12 @@
model = <GPS_MODEL_UBLOX10>; model = <GPS_MODEL_UBLOX10>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -11,7 +11,7 @@ hardware.flashMode=DIO
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=AUTO hardware.spiRamMode=AUTO
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/cst328.h> #include <bindings/cst328.h>
#include <bindings/gdeq031t10.h> #include <bindings/gdeq031t10.h>
@@ -135,4 +136,12 @@
frequency-khz = <20000>; frequency-khz = <20000>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
+9
View File
@@ -14,6 +14,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h> #include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/gt911.h> #include <bindings/gt911.h>
#include <bindings/st7789.h> #include <bindings/st7789.h>
@@ -192,4 +193,12 @@
i2cPort = <I2C_NUM_1>; i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>; i2cClockFrequency = <400000>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/battery_sense.h> #include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/esp32_i8080.h> #include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789_i8080.h> #include <bindings/st7789_i8080.h>
#include <bindings/button_control.h> #include <bindings/button_control.h>
@@ -95,4 +96,12 @@
pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; pin-primary = <&gpio0 0 GPIO_FLAG_NONE>;
pin-secondary = <&gpio0 14 GPIO_FLAG_NONE>; pin-secondary = <&gpio0 14 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ apps.autoStartAppId=ApWebServer
hardware.target=ESP32S3 hardware.target=ESP32S3
hardware.flashSize=16MB hardware.flashSize=16MB
hardware.spiRam=false hardware.spiRam=false
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7735.h> #include <bindings/st7735.h>
@@ -95,4 +96,12 @@
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
+9
View File
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_i8080.h> #include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
#include <bindings/button_control.h> #include <bindings/button_control.h>
@@ -131,4 +132,12 @@
bus-width = <1>; bus-width = <1>;
pullups; pullups;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.flashMode=DIO
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=AUTO hardware.spiRamMode=AUTO
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=40M hardware.esptoolFlashFreq=40M
hardware.bluetooth=true hardware.bluetooth=true
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <gps_generic/bindings.h> #include <gps_generic/bindings.h>
#include <bindings/st7796.h> #include <bindings/st7796.h>
@@ -205,4 +206,12 @@
pin-b = <&gpio0 41 GPIO_FLAG_NONE>; pin-b = <&gpio0 41 GPIO_FLAG_NONE>;
pin-enter = <&gpio0 7 GPIO_FLAG_NONE>; pin-enter = <&gpio0 7 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -6,7 +6,7 @@ apps.launcherAppId=Launcher
hardware.target=ESP32S3 hardware.target=ESP32S3
hardware.flashSize=8MB hardware.flashSize=8MB
hardware.spiRam=false hardware.spiRam=false
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -11,6 +11,7 @@
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/bmi270.h> #include <bindings/bmi270.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
@@ -148,4 +149,12 @@
pin-data-out = <&gpio0 42 GPIO_FLAG_NONE>; pin-data-out = <&gpio0 42 GPIO_FLAG_NONE>;
pin-data-in = <&gpio0 46 GPIO_FLAG_NONE>; pin-data-in = <&gpio0 46 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -6,7 +6,7 @@ apps.launcherAppId=Launcher
hardware.target=ESP32S3 hardware.target=ESP32S3
hardware.flashSize=8MB hardware.flashSize=8MB
hardware.spiRam=false hardware.spiRam=false
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -11,6 +11,7 @@
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
@@ -164,4 +165,12 @@
i2s = <&i2s0>; i2s = <&i2s0>;
channels = <1>; channels = <1>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=QUAD hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
#include <bindings/aw88298.h> #include <bindings/aw88298.h>
#include <bindings/aw9523b.h> #include <bindings/aw9523b.h>
@@ -205,4 +206,12 @@
backlight = <&display_backlight>; backlight = <&display_backlight>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -10,7 +10,7 @@ hardware.spiRam=true
hardware.spiRamMode=OPI hardware.spiRamMode=OPI
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.bluetooth=true hardware.bluetooth=true
storage.userDataLocation=SD storage.userDataLocation=SD
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_adc_oneshot.h> #include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/battery_sense.h> #include <tactility/bindings/battery_sense.h>
#include <bindings/bmi270.h> #include <bindings/bmi270.h>
#include <bindings/bm8563.h> #include <bindings/bm8563.h>
@@ -114,4 +115,12 @@
// correct orientation. // correct orientation.
rotation = <EPD_ROT_INVERTED_PORTRAIT>; rotation = <EPD_ROT_INVERTED_PORTRAIT>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=QUAD hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
#include <bindings/aw88298.h> #include <bindings/aw88298.h>
#include <bindings/aw9523b.h> #include <bindings/aw9523b.h>
@@ -238,4 +239,12 @@
frequency-khz = <20000>; frequency-khz = <20000>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.bluetooth=true hardware.bluetooth=true
storage.userDataLocation=Internal storage.userDataLocation=Internal
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2s.h> #include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/bmi270.h> #include <bindings/bmi270.h>
#include <bindings/m5pm1.h> #include <bindings/m5pm1.h>
#include <bindings/es8311.h> #include <bindings/es8311.h>
@@ -131,4 +132,12 @@
pin-tx = <&gpio0 9 GPIO_FLAG_NONE>; pin-tx = <&gpio0 9 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 10 GPIO_FLAG_NONE>; pin-rx = <&gpio0 10 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -96,10 +96,10 @@ static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = {
}; };
static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = { static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = {
// Row 0: identical to base // Row 0: Esc F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 Del
{0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, {0x29, 0x00}, {0x3A, 0x00}, {0x3B, 0x00}, {0x3C, 0x00}, {0x3D, 0x00}, {0x3E, 0x00},
{0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00}, {0x3F, 0x00}, {0x40, 0x00}, {0x41, 0x00}, {0x42, 0x00}, {0x43, 0x00}, {0x44, 0x00},
{0x2E, 0x02}, {0x4C, 0x00}, {0x45, 0x00}, {0x4C, 0x00},
// Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → | // Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → |
{0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02}, {0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 0x02},
{0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02}, {0x23, 0x02}, {0x24, 0x02}, {0x38, 0x00}, {0x36, 0x02}, {0x37, 0x02}, {0x2F, 0x02},
@@ -148,6 +148,10 @@ static uint32_t tab5_translate_key(uint8_t keycode, uint8_t modifier, bool ctrl)
default: break; default: break;
} }
// F1-F12 (Sym layer over the number row) have no LVGL/ASCII representation - callers that
// want them read KeyboardKeyData::hid_keycode instead (see drain_events()), which is
// populated for every key regardless of whether `key` itself has a meaningful value.
// Letters a–z / A–Z // Letters a–z / A–Z
if (keycode >= 0x04U && keycode <= 0x1DU) { if (keycode >= 0x04U && keycode <= 0x1DU) {
uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U)); uint32_t c = static_cast<uint32_t>('a' + (keycode - 0x04U));
@@ -191,6 +195,8 @@ struct Tab5KeyEvent {
uint32_t key; uint32_t key;
bool ctrl; bool ctrl;
bool alt; bool alt;
uint8_t hid_keycode;
uint8_t hid_modifier;
}; };
struct Tab5KeyboardInternal { struct Tab5KeyboardInternal {
@@ -368,9 +374,13 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
? KEY_MATRIX_HID_SYM[row * 14U + col] ? KEY_MATRIX_HID_SYM[row * 14U + col]
: KEY_MATRIX_HID_BASE[row * 14U + col]; : KEY_MATRIX_HID_BASE[row * 14U + col];
if (m.keycode != 0U) { if (m.keycode != 0U) {
const uint8_t modifier = static_cast<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U)); uint8_t modifier = static_cast<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U));
if (internal->ctrl_held) modifier |= 0x01U; // HID LeftCtrl
if (internal->alt_held) modifier |= 0x04U; // HID LeftAlt
const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held); const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held);
if (lv_key != 0U) { // Queue whenever there's a HID keycode, even if this key has no LVGL/ASCII
// representation (e.g. F1-F12) - lv_key stays 0 for those, callers that only
// care about LVGL/ASCII text ignore a 0 key the same way they always have.
if (pressed) { if (pressed) {
// A real key was pressed — this hold is a chord, not a tap // A real key was pressed — this hold is a chord, not a tap
internal->aa_tapped = false; internal->aa_tapped = false;
@@ -379,7 +389,8 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
// no business reaching into, so ESC is now just queued as a normal key // no business reaching into, so ESC is now just queued as a normal key
// like everything else (LVGL/app code already handles ESC via focus/group // like everything else (LVGL/app code already handles ESC via focus/group
// navigation the same way a dedicated ESC key on any other keyboard would). // navigation the same way a dedicated ESC key on any other keyboard would).
const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held }; const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held,
m.keycode, modifier };
xQueueSend(internal->queue, &event, 0); xQueueSend(internal->queue, &event, 0);
// Arm software repeat tracking by row/col to survive modifier changes // Arm software repeat tracking by row/col to survive modifier changes
const uint32_t now = now_ms(); const uint32_t now = now_ms();
@@ -401,7 +412,6 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) {
} }
} }
} }
}
// Clear INT status after draining so the line de-asserts // Clear INT status after draining so the line de-asserts
write_reg(device, REG_INT_STAT, 0x00); write_reg(device, REG_INT_STAT, 0x00);
@@ -560,12 +570,16 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0;
data->ctrl = event.ctrl; data->ctrl = event.ctrl;
data->alt = event.alt; data->alt = event.alt;
data->hid_keycode = event.hid_keycode;
data->hid_modifier = event.hid_modifier;
} else { } else {
data->key = 0; data->key = 0;
data->pressed = false; data->pressed = false;
data->continue_reading = false; data->continue_reading = false;
data->ctrl = false; data->ctrl = false;
data->alt = false; data->alt = false;
data->hid_keycode = 0;
data->hid_modifier = 0;
} }
return ERROR_NONE; return ERROR_NONE;
+4 -1
View File
@@ -11,7 +11,10 @@ hardware.spiRamSpeed=200M
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.bluetooth=true hardware.bluetooth=true
hardware.usbHostEnabled=true hardware.usbHostEnabled=true
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.tinyUsbHid=true
hardware.tinyUsbMidi=true
hardware.tinyUsbCdc=true
storage.userDataLocation=SD storage.userDataLocation=SD
+21
View File
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_usbhost.h> #include <tactility/bindings/esp32_usbhost.h>
#include <tactility/bindings/esp32_wifi.h> #include <tactility/bindings/esp32_wifi.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
@@ -210,4 +211,24 @@
compatible = "espressif,esp32-usbhost-msc"; compatible = "espressif,esp32-usbhost-msc";
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicehid0 {
compatible = "espressif,esp32-usbdevice-hid";
};
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
usbdevicemidi0 {
compatible = "espressif,esp32-usbdevice-midi";
};
usbdevicecdc0 {
compatible = "espressif,esp32-usbdevice-cdc";
};
};
}; };
@@ -10,7 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=QUAD hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_sdmmc.h> #include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
@@ -103,4 +104,12 @@
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -10,7 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <bindings/qmi8658.h> #include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h> #include <bindings/st7789.h>
@@ -90,4 +91,12 @@
frequency-khz = <20000>; frequency-khz = <20000>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -10,7 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=QUAD hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <bindings/qmi8658.h> #include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/cst816t.h> #include <bindings/cst816t.h>
#include <bindings/gc9a01.h> #include <bindings/gc9a01.h>
@@ -93,4 +94,12 @@
backlight = <&display_backlight>; backlight = <&display_backlight>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -9,7 +9,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/axs5106.h> #include <bindings/axs5106.h>
#include <bindings/button_control.h> #include <bindings/button_control.h>
@@ -104,4 +105,12 @@
compatible = "tactility,button-control"; compatible = "tactility,button-control";
pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; pin-primary = <&gpio0 0 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -9,7 +9,7 @@ hardware.flashSize=8MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=OCT hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M hardware.spiRamSpeed=120M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M hardware.esptoolFlashFreq=120M
hardware.bluetooth=true hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h> #include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h> #include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h> #include <tactility/bindings/gpio_hog.h>
#include <bindings/ch422g.h> #include <bindings/ch422g.h>
#include <bindings/gt911.h> #include <bindings/gt911.h>
@@ -126,4 +127,12 @@
pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-tx = <&gpio0 43 GPIO_FLAG_NONE>;
pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>;
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true hardware.spiRam=true
hardware.spiRamMode=QUAD hardware.spiRamMode=QUAD
hardware.spiRamSpeed=80M hardware.spiRamSpeed=80M
hardware.tinyUsb=true hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=80M hardware.esptoolFlashFreq=80M
hardware.bluetooth=true hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_i8080.h> #include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h> #include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h> #include <tactility/bindings/pwm_backlight.h>
#include <bindings/ft6x36.h> #include <bindings/ft6x36.h>
#include <bindings/st7796_i8080.h> #include <bindings/st7796_i8080.h>
@@ -105,4 +106,12 @@
frequency-khz = <20000>; frequency-khz = <20000>;
}; };
}; };
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
}; };
@@ -13,6 +13,16 @@
extern "C" { extern "C" {
#endif #endif
/**
* Maximum simultaneous touch points lvgl_pointer_add() will create indev \slots for.
*
* \slots Each lv_indev_t carries one point per read, so per-finger widget interaction needs
* one indev per finger, pooled over a single device read - see pointer.cpp. Distinct from
* LVGL's own lv_indev_touch_data_t gesture path, which reports all points to one indev for
* gestures rather than per-widget press handling; unused here.
*/
#define LVGL_POINTER_MAX_SLOTS 5
/** /**
* @brief Linear per-axis calibration range for raw pointer coordinates. * @brief Linear per-axis calibration range for raw pointer coordinates.
* *
@@ -31,7 +41,8 @@ struct LvglPointerCalibration {
/** /**
* @brief Sets (or clears, when calibration is NULL) the calibration applied to raw coordinates * @brief Sets (or clears, when calibration is NULL) the calibration applied to raw coordinates
* read from the device before they are written into LVGL indev data, on an indev previously * read from the device before they are written into LVGL indev data, on an indev previously
* created with lvgl_pointer_add(). * created with lvgl_pointer_add(). Applies to every slot of the same physical device (one panel,
* one calibration) - pass any one of lvgl_pointer_add()'s out_indevs, it doesn't matter which.
* *
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h). * @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h).
* *
@@ -44,19 +55,20 @@ struct LvglPointerCalibration {
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration); error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration);
/** /**
* @brief Retrieves the calibration currently active on indev, if any. * @brief Retrieves the calibration currently active on indev's device, if any.
* @warning Caller must hold the LVGL lock. * @warning Caller must hold the LVGL lock.
* @return true when a calibration is currently set on indev (out_calibration is filled), false otherwise * @return true when a calibration is currently set on indev (out_calibration is filled), false otherwise
*/ */
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration); bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration);
/** /**
* @brief Returns the first indev created by lvgl_pointer_add() that hasn't been removed yet. * @brief Returns the first indev created by lvgl_pointer_add() that hasn't been removed yet
* (specifically, slot 0 of the first pool created).
* *
* Unlike iterating LVGL's own indev list, this only ever returns an indev created by * Unlike iterating LVGL's own indev list, this only ever returns an indev created by
* lvgl_pointer_add() — safe to pass to lvgl_pointer_set_calibration()/lvgl_pointer_get_calibration() * lvgl_pointer_add() — safe to pass to lvgl_pointer_set_calibration()/lvgl_pointer_get_calibration()
* without risking a foreign indev (e.g. one registered by the deprecated HAL layer) whose driver * without risking a foreign indev (e.g. one registered by the deprecated HAL layer) whose driver
* data isn't a struct LvglPointerCtx*. * data isn't this module's own context type.
* *
* @warning Caller must hold the LVGL lock. * @warning Caller must hold the LVGL lock.
* @return the indev, or NULL if none is currently registered. * @return the indev, or NULL if none is currently registered.
@@ -64,23 +76,47 @@ bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibrati
lv_indev_t* lvgl_pointer_get_default(void); lv_indev_t* lvgl_pointer_get_default(void);
/** /**
* @brief Creates an lv_indev_t bound to the given POINTER_TYPE device and registers a read callback * @brief Creates up to max_touch_points lv_indev_t instances bound to the given POINTER_TYPE
* that polls the device through its PointerApi. * device, each independently tracking one simultaneous touch point ("slot"). All slots share one
* underlying read of the device per LVGL polling round (see pointer.cpp) — this does not multiply
* bus traffic by max_touch_points.
*
* Each returned indev behaves like an ordinary single-touch pointer indev to LVGL: normal widget
* press/click/drag interaction (LV_EVENT_PRESSED, dragging, etc.) works independently per-indev
* out of the box. Event callbacks that need to know which finger fired them should use
* lv_event_get_indev() (never lv_indev_active()/lvgl_pointer_get_default(), which only ever
* resolve to whichever indev happens to be currently processing) and, if per-finger app state is
* needed, lvgl_pointer_get_slot_index() to index into it.
* *
* @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from * @warning Caller must hold the LVGL lock (see lvgl_lock() in lvgl_module.h) — call this from
* LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly. * LvglModuleConfig.on_start, or after calling lvgl_lock() explicitly.
* @warning Do not call lv_indev_enable(false) on individual slots. The pool's shared bus read
* only runs once every slot_count read callbacks (round_pos); disabling one slot skips its
* callback and desyncs that cadence, staling the remaining active slots' data.
* *
* @param[in] device a device of type POINTER_TYPE * @param[in] device a device of type POINTER_TYPE
* @param[in] display the display this indev should be associated with, or NULL to leave it unset * @param[in] display the display this indev should be associated with, or NULL to leave it unset
* @param[out] out_indev the created indev, valid only when ERROR_NONE is returned * @param[in] max_touch_points how many simultaneous touch slots to create (1 for classic
* single-touch behavior; clamped to [1, LVGL_POINTER_MAX_SLOTS])
* @param[out] out_indevs array of at least max_touch_points entries; filled with the created
* indevs on success, valid only when ERROR_NONE is returned
* @retval ERROR_NONE on success * @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT if device or out_indev is NULL, or device is not of type POINTER_TYPE * @retval ERROR_INVALID_ARGUMENT if device or out_indevs is NULL, or device is not of type POINTER_TYPE
* @retval ERROR_OUT_OF_MEMORY if allocation failed * @retval ERROR_OUT_OF_MEMORY if allocation failed
*/ */
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev); error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, uint8_t max_touch_points, lv_indev_t** out_indevs);
/** /**
* @brief Removes an indev previously created with lvgl_pointer_add(). * @brief Returns which slot index (0-based) of its pool the given indev is, or -1 if indev wasn't
* created by lvgl_pointer_add() (or is NULL). Useful for indexing small per-finger app state
* arrays from an event callback's lv_event_get_indev() result.
* @warning Caller must hold the LVGL lock.
*/
int8_t lvgl_pointer_get_slot_index(lv_indev_t* indev);
/**
* @brief Removes every slot indev in the same pool as the given indev (i.e. all indevs returned
* together by one lvgl_pointer_add() call).
* @warning Caller must hold the LVGL lock. * @warning Caller must hold the LVGL lock.
*/ */
void lvgl_pointer_remove(lv_indev_t* indev); void lvgl_pointer_remove(lv_indev_t* indev);
@@ -68,6 +68,16 @@ lv_obj_t* lvgl_toolbar_add_text_button_action(lv_obj_t* obj, const char* text, l
*/ */
lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj); lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj);
/**
* Create and add a dropdown to the toolbar actions.
* @param[in] obj the toolbar instance
* @param[in] options newline-separated dropdown options
* @param[in] width the dropdown's width in pixels, or 0 to leave LVGL's default dropdown width
* @param[in] text fixed text to show instead of the selected option or NULL to show the selected option as usual
* @return an instance created by lv_dropdown_create()
*/
lv_obj_t* lvgl_toolbar_add_dropdown_action(lv_obj_t* obj, const char* options, lv_coord_t width, const char* text);
/** /**
* Create and add a spinner to the toolbar actions. * Create and add a spinner to the toolbar actions.
* @param[in] obj the toolbar instance * @param[in] obj the toolbar instance
+12 -6
View File
@@ -97,15 +97,21 @@ void lvgl_devices_attach() {
continue; continue;
} }
lv_indev_t* lvgl_pointer_device; // Each physical touch device gets up to LVGL_POINTER_MAX_SLOTS independent indevs (a
if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, &lvgl_pointer_device) == ERROR_NONE) { // "pool", see lvgl_pointer_add()) so LVGL can track that many simultaneous touch points -
LOG_I(TAG, "Bound %s to LVGL", kernel_pointer_device->name); // touch controllers report multiple points but expose no per-point hardware max, so this
// is a fixed ceiling rather than a per-device query.
lv_indev_t* lvgl_pointer_slots[LVGL_POINTER_MAX_SLOTS];
if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, LVGL_POINTER_MAX_SLOTS, lvgl_pointer_slots) == ERROR_NONE) {
LOG_I(TAG, "Bound %s to LVGL (%d touch slots)", kernel_pointer_device->name, (int)LVGL_POINTER_MAX_SLOTS);
// Slow panels cause taps to be missed due to the long update time, prevent that // Slow panels cause taps to be missed due to the long update time, prevent that
if (display_updates_slowly ) { if (display_updates_slowly) {
lv_indev_set_long_press_time(lvgl_pointer_device, 2000); for (uint8_t slot = 0; slot < LVGL_POINTER_MAX_SLOTS; slot++) {
lv_indev_set_long_press_time(lvgl_pointer_slots[slot], 2000);
}
} }
} else { } else {
LOG_E(TAG, "Failed to bind %s to LVG", kernel_pointer_device->name); LOG_E(TAG, "Failed to bind %s to LVGL", kernel_pointer_device->name);
} }
} }
+249 -63
View File
@@ -2,14 +2,13 @@
#include <lvgl/devices/pointer.h> #include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h> #include <lvgl/devices/device_context.h>
#include <tactility/device.h>
#include <tactility/drivers/pointer.h> #include <tactility/drivers/pointer.h>
constexpr auto* TAG = "lvgl_pointer"; #include <cstring>
#include <new>
struct LvglPointerCtx { constexpr auto* TAG = "lvgl_pointer";
bool calibration_enabled;
struct LvglPointerCalibration calibration;
};
// Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll. // Bus reads are expected to complete quickly; bound the wait so a stalled controller can't block the LVGL indev poll.
static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10); static const TickType_t LVGL_POINTER_READ_TIMEOUT = pdMS_TO_TICKS(10);
@@ -22,6 +21,61 @@ static lv_indev_t* default_pointer_indev = NULL;
// Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid(). // Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid().
static const int32_t LVGL_POINTER_CALIBRATION_MIN_RANGE = 20; static const int32_t LVGL_POINTER_CALIBRATION_MIN_RANGE = 20;
// Caps nearest-neighbor slot tracking (lvgl_pointer_pool_assign) to a fraction of screen width,
// so a lifted finger's slot doesn't jump to grab an unrelated new touch elsewhere on screen.
// Scaled by resolution, not a flat pixel value, so it stays proportionate on any display size.
// Set generously \wide: a too-tight cap misreads a fast scroll's own motion as release+re-press,
// firing whatever is under the finger mid-drag.
//
// \wide False positives (treating one continued drag as two separate touches) are far more
// disruptive than false negatives (merging an unrelated same-spot lift+relanding), which
// favors erring toward a larger cap.
static const int32_t LVGL_POINTER_MAX_TRACK_DIST_FRACTION = 3; // 1/3 of screen width
// One physical touch device backs LVGL_POINTER_MAX_SLOTS independent lv_indev_t instances (a
// "pool"), so LVGL can track that many simultaneous fingers - LVGL v9 has no multi-point indev
// concept (confirmed against lv_indev.h/lv_indev.c: lv_indev_data_t carries exactly one
// lv_point_t/state pair), so N simultaneous independently-clickable widgets requires N indevs.
// Each slot's read callback shares one raw multi-touch read per round rather than each slot
// hitting the bus independently - see lvgl_pointer_read_cb().
struct LvglPointerPool {
struct Device* device;
bool calibration_enabled;
struct LvglPointerCalibration calibration;
uint8_t slot_count;
lv_indev_t* slot_indev[LVGL_POINTER_MAX_SLOTS];
// Per-slot "which finger" tracking. A slot with active=false reports RELEASED and is up for
// grabs by any unmatched raw point next round.
bool slot_active[LVGL_POINTER_MAX_SLOTS];
lv_point_t slot_point[LVGL_POINTER_MAX_SLOTS];
// Shared raw-read cache for this round. Refreshed by whichever slot's read callback runs
// first each round (see round_pos); the rest just consume it - one bus transaction per
// slot_count read callbacks, not one per slot.
uint8_t round_pos;
uint16_t raw_x[LVGL_POINTER_MAX_SLOTS];
uint16_t raw_y[LVGL_POINTER_MAX_SLOTS];
uint8_t raw_count;
};
// Safely narrows an arbitrary lv_indev_t's driver data down to this module's LvglPointerPool,
// or NULL if indev is NULL, wasn't created by lvgl_pointer_add() (LvglDeviceContext is shared by
// every LVGL indev/display this module creates - pointer, trackball, keyboard - so driver_data
// alone doesn't prove it's a pointer pool), or is mid-teardown (context nulled by
// lvgl_pointer_remove()/lvgl_pointer_add()'s cleanup path before the wrapper itself is freed).
static struct LvglPointerPool* lvgl_pointer_pool_from_indev(lv_indev_t* indev) {
if (indev == NULL) {
return NULL;
}
auto* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
if (wrapper == NULL || wrapper->device == NULL || device_get_type(wrapper->device) != &POINTER_TYPE) {
return NULL;
}
return (struct LvglPointerPool*)wrapper->context;
}
static bool lvgl_pointer_calibration_is_valid(const struct LvglPointerCalibration* calibration) { static bool lvgl_pointer_calibration_is_valid(const struct LvglPointerCalibration* calibration) {
return calibration->x_max > calibration->x_min && return calibration->x_max > calibration->x_min &&
calibration->y_max > calibration->y_min && calibration->y_max > calibration->y_min &&
@@ -53,77 +107,164 @@ static void lvgl_pointer_calibration_apply(
*y = (uint16_t)mapped_y; *y = (uint16_t)mapped_y;
} }
// Reads the touch controller and applies calibration entirely in the graphics driver's own // Reads all currently-touched points from the device into pool->raw_x/raw_y/raw_count, applying
// native (LV_DISPLAY_ROTATION_0) coordinate space - native_x_max/native_y_max are just the panel's // calibration in the graphics driver's own native (LV_DISPLAY_ROTATION_0) coordinate space -
// fixed pixel dimensions, not a rotation. This function has no notion of LVGL rotation at all: // native_x_max/native_y_max are just the panel's fixed pixel dimensions, not a rotation. This
// calibration corrects the raw sensor's fixed physical mapping, which never changes with on-screen // function has no notion of LVGL rotation at all: calibration corrects the raw sensor's fixed
// orientation, so it doesn't belong anywhere near rotation math. // physical mapping, which never changes with on-screen orientation, so it doesn't belong anywhere
static bool lvgl_pointer_read_calibrated(struct Device* device, struct LvglPointerCtx* ctx, int32_t native_x_max, int32_t native_y_max, uint16_t* x, uint16_t* y) { // near rotation math.
if (pointer_read_data(device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) { static void lvgl_pointer_pool_refresh(struct LvglPointerPool* pool, int32_t native_x_max, int32_t native_y_max) {
return false; pool->raw_count = 0;
if (pointer_read_data(pool->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) {
return;
} }
uint8_t point_count = 0; uint8_t point_count = 0;
if (!pointer_get_touched_points(device, x, y, NULL, &point_count, 1) || point_count == 0) { if (!pointer_get_touched_points(pool->device, pool->raw_x, pool->raw_y, NULL, &point_count, LVGL_POINTER_MAX_SLOTS) || point_count == 0) {
return false; return;
}
if (point_count > LVGL_POINTER_MAX_SLOTS) point_count = LVGL_POINTER_MAX_SLOTS;
if (pool->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
for (uint8_t i = 0; i < point_count; i++) {
lvgl_pointer_calibration_apply(&pool->calibration, native_x_max, native_y_max, &pool->raw_x[i], &pool->raw_y[i]);
}
} }
if (ctx->calibration_enabled && native_x_max > 0 && native_y_max > 0) { pool->raw_count = point_count;
lvgl_pointer_calibration_apply(&ctx->calibration, native_x_max, native_y_max, x, y);
}
return true;
} }
// The actual LVGL indev read callback: wraps lvgl_pointer_read_calibrated() and, only here, applies // Matches this round's raw points onto pool slots by nearest-neighbor to each slot's last known
// the rotation needed to place the (still native-space) point into the currently active LVGL // position, so a slot "follows" the same physical finger across rounds instead of jumping when
// logical space - unconditionally, since native-space coordinates always need this regardless of // the touch controller reports points in a different order (no touch-ID/tracking field exists
// whether calibration is enabled. // anywhere in this stack - see esp_lcd_touch_get_coordinates()/PointerApi.get_touched_points()).
// Unmatched raw points (new touches) claim the nearest inactive slot. Slots with no matching
// point this round go inactive (RELEASED).
static void lvgl_pointer_pool_assign(struct LvglPointerPool* pool, int32_t native_x_max) {
// Touch drivers clamp raw coordinates to the panel's configured native resolution regardless
// of calibration, so native_x_max is a valid scale reference for the distance cap even when calibration is disabled.
// Falls back to a conservative fixed pixel value if the display/resolution isn't available for some reason.
const int32_t max_track_dist = native_x_max > 0 ? (native_x_max / LVGL_POINTER_MAX_TRACK_DIST_FRACTION) : 150;
const int32_t max_track_dist_sq = max_track_dist * max_track_dist;
bool raw_claimed[LVGL_POINTER_MAX_SLOTS] = {};
// A slot released this round must report RELEASED for at least one round before it can host
// a new touch - otherwise pass 2 immediately reassigns it, and LVGL sees a jump instead of a
// release-then-press.
bool slot_released_now[LVGL_POINTER_MAX_SLOTS] = {};
// First pass: let already-active slots keep following their nearest raw point, so a held
// finger doesn't get reshuffled onto a different slot just because another finger moved.
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (!pool->slot_active[s]) continue;
int32_t best_dist = -1;
int8_t best_raw = -1;
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
int32_t dx = (int32_t)pool->raw_x[r] - pool->slot_point[s].x;
int32_t dy = (int32_t)pool->raw_y[r] - pool->slot_point[s].y;
int32_t dist = dx * dx + dy * dy;
if (best_raw < 0 || dist < best_dist) {
best_dist = dist;
best_raw = (int8_t)r;
}
}
if (best_raw >= 0 && best_dist <= max_track_dist_sq) {
raw_claimed[best_raw] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[best_raw];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[best_raw];
} else {
pool->slot_active[s] = false;
slot_released_now[s] = true;
}
}
// Second pass: any unclaimed raw point is a new touch - hand it to the first inactive slot
// that wasn't just released this round.
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (pool->slot_active[s] || slot_released_now[s]) continue;
pool->slot_active[s] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[r];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[r];
raw_claimed[r] = true;
break;
}
}
}
// The actual LVGL indev read callback, shared by every slot in the pool. Only the first slot to
// be read each round (round_pos wraps 0..slot_count-1) triggers the real bus read + reassignment;
// the rest just report whatever lvgl_pointer_pool_assign() decided for their slot. Which slot
// happens to run first varies (LVGL calls each indev's timer independently), but since all slots
// share one timer period they complete one full round every slot_count calls regardless of order.
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; if (pool == NULL) {
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation - no rotation lookup needed to get it.
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
uint16_t x = 0;
uint16_t y = 0;
if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) {
data->state = LV_INDEV_STATE_RELEASED; data->state = LV_INDEV_STATE_RELEASED;
return; return;
} }
data->point.x = x; uint8_t slot = 0;
data->point.y = y; for (; slot < pool->slot_count; slot++) {
if (pool->slot_indev[slot] == indev) break;
}
if (pool->round_pos == 0) {
lv_display_t* display = lv_indev_get_display(indev);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation - no rotation lookup needed to get it.
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
lvgl_pointer_pool_refresh(pool, native_x_max, native_y_max);
lvgl_pointer_pool_assign(pool, native_x_max);
}
pool->round_pos = (uint8_t)((pool->round_pos + 1) % pool->slot_count);
if (slot < pool->slot_count && pool->slot_active[slot]) {
data->point = pool->slot_point[slot];
data->state = LV_INDEV_STATE_PRESSED; data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
} }
error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) { error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, uint8_t max_touch_points, lv_indev_t** out_indevs) {
if (device == NULL || out_indev == NULL) { if (device == NULL || out_indevs == NULL) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
if (device_get_type(device) != &POINTER_TYPE) { if (device_get_type(device) != &POINTER_TYPE) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
if (max_touch_points == 0) max_touch_points = 1;
if (max_touch_points > LVGL_POINTER_MAX_SLOTS) max_touch_points = LVGL_POINTER_MAX_SLOTS;
struct LvglPointerCtx* ctx = new(std::nothrow) LvglPointerCtx(); auto* pool = new(std::nothrow) LvglPointerPool();
if (ctx == NULL) { if (pool == NULL) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }
struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx); pool->device = device;
pool->slot_count = max_touch_points;
// Every slot gets its own LvglDeviceContext wrapper, but all of them point at this same pool.
// On the way out - success or failure - every wrapper's context pointer is nulled before any
// of them are deleted, then the pool itself is freed exactly once: LvglDeviceContext's
// destructor does `::operator delete(context)`, so leaving more than one wrapper owning the
// same pool pointer would double-free it (see lvgl_pointer_remove(), same pattern).
uint8_t created = 0;
for (; created < max_touch_points; created++) {
auto* wrapper = new(std::nothrow) LvglDeviceContext(pool);
if (wrapper == NULL) { if (wrapper == NULL) {
delete ctx; break;
return ERROR_OUT_OF_MEMORY;
} }
wrapper->device = device; wrapper->device = device;
lv_indev_t* indev = lv_indev_create(); lv_indev_t* indev = lv_indev_create();
if (indev == NULL) { if (indev == NULL) {
delete wrapper; delete wrapper;
return ERROR_OUT_OF_MEMORY; break;
} }
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
@@ -133,11 +274,26 @@ error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_
lv_indev_set_display(indev, display); lv_indev_set_display(indev, display);
} }
if (default_pointer_indev == NULL) { pool->slot_indev[created] = indev;
default_pointer_indev = indev; out_indevs[created] = indev;
}
if (created < max_touch_points) {
for (uint8_t j = 0; j < created; j++) {
lv_indev_t* slot_indev = pool->slot_indev[j];
auto* slot_wrapper = (LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
slot_wrapper->context = NULL;
lv_indev_delete(slot_indev);
delete slot_wrapper;
}
delete pool;
return ERROR_OUT_OF_MEMORY;
}
if (default_pointer_indev == NULL) {
default_pointer_indev = pool->slot_indev[0];
} }
*out_indev = indev;
return ERROR_NONE; return ERROR_NONE;
} }
@@ -145,48 +301,78 @@ lv_indev_t* lvgl_pointer_get_default(void) {
return default_pointer_indev; return default_pointer_indev;
} }
// Any slot indev's wrapper->context points at the same shared pool, so calibration set through
// any one of them applies to the whole physical device/all its slots (one panel, one calibration).
error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) { error_t lvgl_pointer_set_calibration(lv_indev_t* indev, const struct LvglPointerCalibration* calibration) {
if (indev == NULL) { struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context;
if (calibration == NULL) { if (calibration == NULL) {
ctx->calibration_enabled = false; pool->calibration_enabled = false;
return ERROR_NONE; return ERROR_NONE;
} }
if (!lvgl_pointer_calibration_is_valid(calibration)) { if (!lvgl_pointer_calibration_is_valid(calibration)) {
return ERROR_INVALID_ARGUMENT; return ERROR_INVALID_ARGUMENT;
} }
ctx->calibration = *calibration; pool->calibration = *calibration;
ctx->calibration_enabled = true; pool->calibration_enabled = true;
return ERROR_NONE; return ERROR_NONE;
} }
bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) { bool lvgl_pointer_get_calibration(lv_indev_t* indev, struct LvglPointerCalibration* out_calibration) {
if (indev == NULL || out_calibration == NULL) { if (out_calibration == NULL) {
return false; return false;
} }
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; if (pool == NULL || !pool->calibration_enabled) {
if (!ctx->calibration_enabled) {
return false; return false;
} }
*out_calibration = ctx->calibration; *out_calibration = pool->calibration;
return true; return true;
} }
int8_t lvgl_pointer_get_slot_index(lv_indev_t* indev) {
struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev);
if (pool == NULL) {
return -1;
}
for (uint8_t i = 0; i < pool->slot_count; i++) {
if (pool->slot_indev[i] == indev) return (int8_t)i;
}
return -1;
}
// Removes every slot indev belonging to the same pool as `indev` (a partial pool removal isn't a
// case that comes up: apps ask for "the pointer device" and get every slot back from
// lvgl_pointer_add(), so they hold either all of a pool's indevs or none).
void lvgl_pointer_remove(lv_indev_t* indev) { void lvgl_pointer_remove(lv_indev_t* indev) {
if (indev == NULL) { if (indev == NULL) {
return; return;
} }
struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev);
if (default_pointer_indev == indev) { struct LvglPointerPool* pool = (struct LvglPointerPool*)wrapper->context;
uint8_t slot_count = pool->slot_count;
lv_indev_t* slot_indevs[LVGL_POINTER_MAX_SLOTS];
memcpy(slot_indevs, pool->slot_indev, sizeof(lv_indev_t*) * slot_count);
// Every LvglDeviceContext wrapper points at the same pool; null `context` out on all of them
// before deleting any (LvglDeviceContext's destructor frees `context`, and pool must stay
// valid for every wrapper's own delete to run safely) - then free the pool exactly once
// ourselves at the end.
for (uint8_t i = 0; i < slot_count; i++) {
lv_indev_t* slot_indev = slot_indevs[i];
if (slot_indev == NULL) continue;
if (default_pointer_indev == slot_indev) {
default_pointer_indev = NULL; default_pointer_indev = NULL;
} }
lv_indev_delete(indev); struct LvglDeviceContext* slot_wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(slot_indev);
delete wrapper; slot_wrapper->context = NULL;
lv_indev_delete(slot_indev);
delete slot_wrapper;
}
delete pool;
} }
+17
View File
@@ -45,6 +45,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_calibration), DEFINE_MODULE_SYMBOL(lvgl_pointer_get_calibration),
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_default), DEFINE_MODULE_SYMBOL(lvgl_pointer_get_default),
DEFINE_MODULE_SYMBOL(lvgl_pointer_add), DEFINE_MODULE_SYMBOL(lvgl_pointer_add),
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index),
DEFINE_MODULE_SYMBOL(lvgl_pointer_remove), DEFINE_MODULE_SYMBOL(lvgl_pointer_remove),
// lvgl_spinner // lvgl_spinner
DEFINE_MODULE_SYMBOL(lvgl_spinner_create), DEFINE_MODULE_SYMBOL(lvgl_spinner_create),
@@ -56,6 +57,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_image_button_action), DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_image_button_action),
DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_text_button_action), DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_text_button_action),
DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_switch_action), DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_switch_action),
DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_dropdown_action),
DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_spinner_action), DEFINE_MODULE_SYMBOL(lvgl_toolbar_add_spinner_action),
DEFINE_MODULE_SYMBOL(lvgl_toolbar_clear_actions), DEFINE_MODULE_SYMBOL(lvgl_toolbar_clear_actions),
// lvgl_sliderbox // lvgl_sliderbox
@@ -123,6 +125,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_obj_set_flex_flow), DEFINE_MODULE_SYMBOL(lv_obj_set_flex_flow),
DEFINE_MODULE_SYMBOL(lv_obj_set_flex_grow), DEFINE_MODULE_SYMBOL(lv_obj_set_flex_grow),
DEFINE_MODULE_SYMBOL(lv_obj_set_layout), DEFINE_MODULE_SYMBOL(lv_obj_set_layout),
DEFINE_MODULE_SYMBOL(lv_obj_set_grid_dsc_array),
DEFINE_MODULE_SYMBOL(lv_obj_set_grid_cell),
DEFINE_MODULE_SYMBOL(lv_obj_is_layout_positioned), DEFINE_MODULE_SYMBOL(lv_obj_is_layout_positioned),
DEFINE_MODULE_SYMBOL(lv_obj_mark_layout_as_dirty), DEFINE_MODULE_SYMBOL(lv_obj_mark_layout_as_dirty),
DEFINE_MODULE_SYMBOL(lv_obj_get_style_layout), DEFINE_MODULE_SYMBOL(lv_obj_get_style_layout),
@@ -192,6 +196,9 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_pivot_x), DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_pivot_x),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_pivot_y), DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_pivot_y),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_rotation), DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_rotation),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_scale),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_scale_x),
DEFINE_MODULE_SYMBOL(lv_obj_set_style_transform_scale_y),
DEFINE_MODULE_SYMBOL(lv_obj_scroll_to_y), DEFINE_MODULE_SYMBOL(lv_obj_scroll_to_y),
DEFINE_MODULE_SYMBOL(lv_obj_set_scrollbar_mode), DEFINE_MODULE_SYMBOL(lv_obj_set_scrollbar_mode),
DEFINE_MODULE_SYMBOL(lv_obj_get_child_count), DEFINE_MODULE_SYMBOL(lv_obj_get_child_count),
@@ -361,6 +368,14 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_textarea_set_max_length), DEFINE_MODULE_SYMBOL(lv_textarea_set_max_length),
DEFINE_MODULE_SYMBOL(lv_textarea_set_cursor_click_pos), DEFINE_MODULE_SYMBOL(lv_textarea_set_cursor_click_pos),
DEFINE_MODULE_SYMBOL(lv_textarea_add_text), DEFINE_MODULE_SYMBOL(lv_textarea_add_text),
DEFINE_MODULE_SYMBOL(lv_textarea_add_char),
DEFINE_MODULE_SYMBOL(lv_textarea_delete_char),
DEFINE_MODULE_SYMBOL(lv_textarea_delete_char_forward),
DEFINE_MODULE_SYMBOL(lv_textarea_set_cursor_pos),
DEFINE_MODULE_SYMBOL(lv_textarea_cursor_left),
DEFINE_MODULE_SYMBOL(lv_textarea_cursor_right),
DEFINE_MODULE_SYMBOL(lv_textarea_cursor_up),
DEFINE_MODULE_SYMBOL(lv_textarea_cursor_down),
// lv_palette // lv_palette
DEFINE_MODULE_SYMBOL(lv_palette_main), DEFINE_MODULE_SYMBOL(lv_palette_main),
DEFINE_MODULE_SYMBOL(lv_palette_darken), DEFINE_MODULE_SYMBOL(lv_palette_darken),
@@ -409,6 +424,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_indev_get_next), DEFINE_MODULE_SYMBOL(lv_indev_get_next),
DEFINE_MODULE_SYMBOL(lv_indev_set_group), DEFINE_MODULE_SYMBOL(lv_indev_set_group),
DEFINE_MODULE_SYMBOL(lv_indev_wait_release), DEFINE_MODULE_SYMBOL(lv_indev_wait_release),
DEFINE_MODULE_SYMBOL(lv_indev_enable),
// lv_timer // lv_timer
DEFINE_MODULE_SYMBOL(lv_timer_handler), DEFINE_MODULE_SYMBOL(lv_timer_handler),
DEFINE_MODULE_SYMBOL(lv_timer_handler_run_in_period), DEFINE_MODULE_SYMBOL(lv_timer_handler_run_in_period),
@@ -438,6 +454,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lv_line_set_points), DEFINE_MODULE_SYMBOL(lv_line_set_points),
DEFINE_MODULE_SYMBOL(lv_line_set_points_mutable), DEFINE_MODULE_SYMBOL(lv_line_set_points_mutable),
DEFINE_MODULE_SYMBOL(lv_tick_get), DEFINE_MODULE_SYMBOL(lv_tick_get),
DEFINE_MODULE_SYMBOL(lv_delay_ms),
DEFINE_MODULE_SYMBOL(lv_tick_elaps), DEFINE_MODULE_SYMBOL(lv_tick_elaps),
// lv_slider // lv_slider
DEFINE_MODULE_SYMBOL(lv_slider_create), DEFINE_MODULE_SYMBOL(lv_slider_create),
@@ -234,6 +234,29 @@ lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj) {
return widget; return widget;
} }
lv_obj_t* lvgl_toolbar_add_dropdown_action(lv_obj_t* obj, const char* options, lv_coord_t width, const char* text) {
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
lv_obj_set_style_pad_hor(wrapper, 4, LV_STATE_DEFAULT);
lv_obj_t* widget = lv_dropdown_create(wrapper);
lv_dropdown_set_options(widget, options);
lv_dropdown_set_selected_highlight(widget, false);
if (width > 0) {
lv_obj_set_width(widget, width);
}
if (text != nullptr) {
lv_dropdown_set_text(widget, text);
}
lv_obj_set_align(widget, LV_ALIGN_CENTER);
return widget;
}
lv_obj_t* lvgl_toolbar_add_spinner_action(lv_obj_t* obj) { lv_obj_t* lvgl_toolbar_add_spinner_action(lv_obj_t* obj) {
auto* toolbar = reinterpret_cast<Toolbar*>(obj); auto* toolbar = reinterpret_cast<Toolbar*>(obj);
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached"); check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
+9
View File
@@ -2,11 +2,20 @@ cmake_minimum_required(VERSION 3.20)
file(GLOB_RECURSE SOURCES "source/*.c**") file(GLOB_RECURSE SOURCES "source/*.c**")
set(PRIV_REQUIRES_LIST "")
if ("${IDF_TARGET}" STREQUAL "esp32s3" OR "${IDF_TARGET}" STREQUAL "esp32p4")
# esp32_usb_device_controller.cpp / esp32_usb_hid_device.cpp include tinyusb.h - only
# available on targets with TinyUSB device-mode support (mirrors Tactility/CMakeLists.txt's
# own esp_tinyusb requirement).
list(APPEND PRIV_REQUIRES_LIST esp_tinyusb)
endif ()
idf_component_register( idf_component_register(
SRCS ${SOURCES} SRCS ${SOURCES}
INCLUDE_DIRS "include/" INCLUDE_DIRS "include/"
PRIV_INCLUDE_DIRS "private/" PRIV_INCLUDE_DIRS "private/"
REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event
PRIV_REQUIRES ${PRIV_REQUIRES_LIST}
) )
if (DEFINED ENV{ESP_IDF_VERSION}) if (DEFINED ENV{ESP_IDF_VERSION})
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode CDC-ACM addon (composites a USB serial console interface into whichever primary class - HID or MIDI - is active, or stands alone with no primary; deliberately excluded from MSC, which always presents as plain mass storage with no console). Requires CONFIG_TINYUSB_CDC_ENABLED.
compatible: "espressif,esp32-usbdevice-cdc"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode HID class driver (present the board as a USB HID peripheral, e.g. a keyboard, to a host). Requires CONFIG_TINYUSB_HID_COUNT > 0.
compatible: "espressif,esp32-usbdevice-hid"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode MIDI class driver (present the board as a USB MIDI peripheral to a host). Requires CONFIG_TINYUSB_MIDI_COUNT > 0.
compatible: "espressif,esp32-usbdevice-midi"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode MSC class driver (present the board's SD card or internal flash as a USB mass storage device to a host). Requires CONFIG_TINYUSB_MSC_ENABLED.
compatible: "espressif,esp32-usbdevice-msc"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,13 @@
description: ESP32 USB device-mode (peripheral) controller. Owns the single TinyUSB device-mode slot shared by its child classes (HID, MSC, ...); only one child class may be active at a time.
compatible: "espressif,esp32-usbdevice"
properties:
_unused:
type: int
default: 0
description: |
USB device-mode (peripheral) controller: presents itself as a USB device to a host (HID keyboard, mass storage, ...).
Only one of usbdevicehid0/usbdevicemsc0/usbdevicemidi0 may be active at a time -
usbdevicecdc0 is not part of that exclusion - it's an addon composited into whichever of HID/MIDI is active (never MSC)
toggle its status to enable/disable the USB serial console independent of which primary is running.
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_usbdevice.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_usbdevice, struct Esp32UsbDeviceConfig)
DEFINE_DEVICETREE(esp32_usbdevice_hid, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_msc, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_midi, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_cdc, struct Esp32UsbDeviceChildConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
struct Esp32UsbDeviceConfig {
int _unused;
};
struct Esp32UsbDeviceChildConfig {
int _unused;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// ---- Shared HID report descriptor tables ----
//
// HID report descriptors are transport-independent (the same USB HID Usage Tables spec applies
// regardless of whether the bytes travel over USB or BLE GATT), so these tables are shared
// between esp32_usb_hid_device.cpp and esp32_ble_hid.cpp rather than each keeping its own copy.
// Report ID scheme matches BtHidDeviceMode/UsbHidDeviceMode 1:1 across both transports:
// Keyboard mode: report ID 1 = keyboard, report ID 2 = consumer control
// Mouse mode: report ID 1 = mouse
// Keyboard+Mouse mode: report ID 1 = keyboard, 2 = consumer, 3 = mouse
// Gamepad mode: report ID 1 = gamepad
/** Keyboard (report ID 1, 8 bytes: modifier, reserved, keycode[6]) + Consumer (report ID 2,
* 16-bit usage code, 2 bytes). Keyboard collection uses the standard boot-protocol byte
* layout, though the descriptor itself is Report protocol (report-ID-multiplexed with
* Consumer, which boot protocol can't express). */
extern const uint8_t hid_report_map_keyboard_consumer[];
extern const size_t hid_report_map_keyboard_consumer_len;
/** Mouse only (report ID 1, 4 bytes: buttons[3]+5 padding bits, X, Y, wheel). */
extern const uint8_t hid_report_map_mouse[];
extern const size_t hid_report_map_mouse_len;
/** Keyboard (report ID 1) + Consumer (report ID 2) + Mouse (report ID 3) combined. */
extern const uint8_t hid_report_map_keyboard_consumer_mouse[];
extern const size_t hid_report_map_keyboard_consumer_mouse_len;
/** Gamepad only (report ID 1, 8 bytes). Xbox-360-style layout: X/Y (1 byte each, left stick),
* Rx/Ry (1 byte each, right stick), Z (1 byte, triggers - L=positive, R=negative, centered=0),
* hat/dpad (1 byte, 1-8 = 8-direction clockwise from Up, 0 = centered/released), buttons[10]
* (2 bytes, 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Select 8=Start 9=L3 10=R3, 6 padding bits at the end). */
extern const uint8_t hid_report_map_gamepad[];
extern const size_t hid_report_map_gamepad_len;
#ifdef __cplusplus
}
#endif
@@ -11,6 +11,7 @@
#include <host/ble_hs_mbuf.h> #include <host/ble_hs_mbuf.h>
#include <services/gap/ble_svc_gap.h> #include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h> #include <services/gatt/ble_svc_gatt.h>
#include <tactility/drivers/hid_report_descriptors.h>
#include <cstring> #include <cstring>
@@ -58,77 +59,10 @@ static uint8_t hid_protocol_mode = 0x01; // 0x00=Boot, 0x01=Report
// ============================================================================ // ============================================================================
// Per-profile HID Report Maps // Per-profile HID Report Maps
// ============================================================================ // ============================================================================
//
// Keyboard + Consumer (IDs 1 and 2) // Shared with esp32_usb_hid_device.cpp - see hid_report_descriptors.h for the byte tables
static const uint8_t hid_rpt_map_kb_consumer[] = { // (report ID scheme, per-field layout) - HID report descriptors are transport-independent
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, // so the same bytes apply verbatim whether carried over USB or BLE GATT.
0x85, 0x01,
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
0x75, 0x08, 0x95, 0x01, 0x81, 0x01,
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02,
0x75, 0x03, 0x95, 0x01, 0x91, 0x01,
0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73,
0x75, 0x08, 0x95, 0x06, 0x81, 0x00,
0xC0,
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01,
0x85, 0x02,
0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03,
0x75, 0x10, 0x95, 0x01, 0x81, 0x00,
0xC0,
};
// Mouse only (ID 1, 4 bytes)
static const uint8_t hid_rpt_map_mouse[] = {
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01,
0x85, 0x01,
0x09, 0x01, 0xA1, 0x00,
0x05, 0x09, 0x19, 0x01, 0x29, 0x05,
0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02,
0x95, 0x01, 0x75, 0x03, 0x81, 0x01,
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38,
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06,
0xC0, 0xC0,
};
// Keyboard + Consumer + Mouse (IDs 1, 2, 3)
static const uint8_t hid_rpt_map_kb_mouse[] = {
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01,
0x85, 0x01,
0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
0x75, 0x08, 0x95, 0x01, 0x81, 0x01,
0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x75, 0x01, 0x95, 0x05, 0x91, 0x02,
0x75, 0x03, 0x95, 0x01, 0x91, 0x01,
0x15, 0x00, 0x25, 0x73, 0x05, 0x07, 0x19, 0x00, 0x29, 0x73,
0x75, 0x08, 0x95, 0x06, 0x81, 0x00,
0xC0,
0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01,
0x85, 0x02,
0x15, 0x00, 0x26, 0xFF, 0x03, 0x19, 0x00, 0x2A, 0xFF, 0x03,
0x75, 0x10, 0x95, 0x01, 0x81, 0x00,
0xC0,
0x05, 0x01, 0x09, 0x02, 0xA1, 0x01,
0x85, 0x03,
0x09, 0x01, 0xA1, 0x00,
0x05, 0x09, 0x19, 0x01, 0x29, 0x05,
0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02,
0x95, 0x01, 0x75, 0x03, 0x81, 0x01,
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x38,
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x03, 0x81, 0x06,
0xC0, 0xC0,
};
// Gamepad only (ID 1, 8 bytes)
static const uint8_t hid_rpt_map_gamepad[] = {
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01,
0x85, 0x01,
0x05, 0x09, 0x19, 0x01, 0x29, 0x10,
0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02,
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x09, 0x33, 0x09, 0x34,
0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02,
0xC0,
};
// ---- Per-profile Report Reference descriptor data ---- // ---- Per-profile Report Reference descriptor data ----
// Format: {Report ID, Report Type} — Type: 1=Input, 2=Output // Format: {Report ID, Report Type} — Type: 1=Input, 2=Output
@@ -181,6 +115,7 @@ static int hid_chr_access(uint16_t /*conn_handle*/, uint16_t attr_handle,
size_t report_len = 8; size_t report_len = 8;
if (attr_handle == hid_consumer_input_handle) report_len = 2; if (attr_handle == hid_consumer_input_handle) report_len = 2;
else if (attr_handle == hid_mouse_input_handle) report_len = 4; else if (attr_handle == hid_mouse_input_handle) report_len = 4;
else if (attr_handle == hid_gamepad_input_handle) report_len = 8;
int rc = os_mbuf_append(ctxt->om, zeros, report_len); int rc = os_mbuf_append(ctxt->om, zeros, report_len);
return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES; return (rc == 0) ? 0 : BLE_ATT_ERR_INSUFFICIENT_RES;
} }
@@ -354,10 +289,10 @@ bool ble_hid_switch_profile(struct Device* device, BleHidProfile profile) {
const uint8_t* new_rpt_map = nullptr; const uint8_t* new_rpt_map = nullptr;
size_t new_rpt_map_len = 0; size_t new_rpt_map_len = 0;
switch (profile) { switch (profile) {
case BleHidProfile::KbConsumer: svcs = gatt_svcs_kb_consumer; new_rpt_map = hid_rpt_map_kb_consumer; new_rpt_map_len = sizeof(hid_rpt_map_kb_consumer); break; case BleHidProfile::KbConsumer: svcs = gatt_svcs_kb_consumer; new_rpt_map = hid_report_map_keyboard_consumer; new_rpt_map_len = hid_report_map_keyboard_consumer_len; break;
case BleHidProfile::Mouse: svcs = gatt_svcs_mouse; new_rpt_map = hid_rpt_map_mouse; new_rpt_map_len = sizeof(hid_rpt_map_mouse); break; case BleHidProfile::Mouse: svcs = gatt_svcs_mouse; new_rpt_map = hid_report_map_mouse; new_rpt_map_len = hid_report_map_mouse_len; break;
case BleHidProfile::KbMouse: svcs = gatt_svcs_kb_mouse; new_rpt_map = hid_rpt_map_kb_mouse; new_rpt_map_len = sizeof(hid_rpt_map_kb_mouse); break; case BleHidProfile::KbMouse: svcs = gatt_svcs_kb_mouse; new_rpt_map = hid_report_map_keyboard_consumer_mouse; new_rpt_map_len = hid_report_map_keyboard_consumer_mouse_len; break;
case BleHidProfile::Gamepad: svcs = gatt_svcs_gamepad; new_rpt_map = hid_rpt_map_gamepad; new_rpt_map_len = sizeof(hid_rpt_map_gamepad); break; case BleHidProfile::Gamepad: svcs = gatt_svcs_gamepad; new_rpt_map = hid_report_map_gamepad; new_rpt_map_len = hid_report_map_gamepad_len; break;
default: svcs = gatt_svcs_none; break; default: svcs = gatt_svcs_none; break;
} }
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/hid_report_descriptors.h>
extern "C" {
// Keyboard (report ID 1) + Consumer (report ID 2)
const uint8_t hid_report_map_keyboard_consumer[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x06, // Usage: Keyboard
0xA1, 0x01, // Collection: Application
// --- Keyboard
0x85, 0x01, // Report ID: 1
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0xE0, // Usage Minimum: Left Control
0x29, 0xE7, // Usage Maximum: Right GUI
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x08, // Report Count: 8
0x81, 0x02, // Input: Data, Variable, Absolute (modifier byte)
0x75, 0x08, // Report Size: 8
0x95, 0x01, // Report Count: 1
0x81, 0x01, // Input: Constant, Array, Absolute (reserved byte)
0x05, 0x08, // Usage Page: LEDs
0x19, 0x01, // Usage Minimum: 0x01
0x29, 0x05, // Usage Maximum: 0x05
0x75, 0x01, // Report Size: 1
0x95, 0x05, // Report Count: 5
0x91, 0x02, // Output: Data, Variable, Absolute (LED state)
0x75, 0x03, // Report Size: 3
0x95, 0x01, // Report Count: 1
0x91, 0x01, // Output: Constant, Array, Absolute (LED padding)
0x15, 0x00, // Logical Minimum: 0
0x25, 0x73, // Logical Maximum: 115
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0x00, // Usage Minimum: 0x00
0x29, 0x73, // Usage Maximum: 0x73
0x75, 0x08, // Report Size: 8
0x95, 0x06, // Report Count: 6
0x81, 0x00, // Input: Data, Array, Absolute (keycode[6])
0xC0, // End Collection
// --- Consumer / Media Keys
0x05, 0x0C, // Usage Page: Consumer
0x09, 0x01, // Usage: Consumer Control
0xA1, 0x01, // Collection: Application
0x85, 0x02, // Report ID: 2
0x15, 0x00, // Logical Minimum: 0
0x26, 0xFF, 0x03, // Logical Maximum: 1023
0x19, 0x00, // Usage Minimum: 0x00
0x2A, 0xFF, 0x03, // Usage Maximum: 0x3FF
0x75, 0x10, // Report Size: 16
0x95, 0x01, // Report Count: 1
0x81, 0x00, // Input: Data, Array, Absolute
0xC0, // End Collection
};
const size_t hid_report_map_keyboard_consumer_len = sizeof(hid_report_map_keyboard_consumer);
// Mouse only (report ID 1)
const uint8_t hid_report_map_mouse[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x02, // Usage: Mouse
0xA1, 0x01, // Collection: Application
0x85, 0x01, // Report ID: 1
0x09, 0x01, // Usage: Pointer
0xA1, 0x00, // Collection: Physical
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x03, // Usage Maximum: 3
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x95, 0x03, // Report Count: 3
0x75, 0x01, // Report Size: 1
0x81, 0x02, // Input: Data, Variable, Absolute (3 buttons)
0x95, 0x01, // Report Count: 1
0x75, 0x05, // Report Size: 5
0x81, 0x01, // Input: Constant, Array, Absolute (padding to byte boundary)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x38, // Usage: Wheel
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x03, // Report Count: 3
0x81, 0x06, // Input: Data, Variable, Relative
0xC0, // End Collection (Physical)
0xC0, // End Collection (Application)
};
const size_t hid_report_map_mouse_len = sizeof(hid_report_map_mouse);
// Keyboard (report ID 1) + Consumer (report ID 2) + Mouse (report ID 3)
const uint8_t hid_report_map_keyboard_consumer_mouse[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x06, // Usage: Keyboard
0xA1, 0x01, // Collection: Application
// --- Keyboard
0x85, 0x01, // Report ID: 1
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0xE0, // Usage Minimum: Left Control
0x29, 0xE7, // Usage Maximum: Right GUI
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x08, // Report Count: 8
0x81, 0x02, // Input: Data, Variable, Absolute (modifier byte)
0x75, 0x08, // Report Size: 8
0x95, 0x01, // Report Count: 1
0x81, 0x01, // Input: Constant, Array, Absolute (reserved byte)
0x05, 0x08, // Usage Page: LEDs
0x19, 0x01, // Usage Minimum: 0x01
0x29, 0x05, // Usage Maximum: 0x05
0x75, 0x01, // Report Size: 1
0x95, 0x05, // Report Count: 5
0x91, 0x02, // Output: Data, Variable, Absolute (LED state)
0x75, 0x03, // Report Size: 3
0x95, 0x01, // Report Count: 1
0x91, 0x01, // Output: Constant, Array, Absolute (LED padding)
0x15, 0x00, // Logical Minimum: 0
0x25, 0x73, // Logical Maximum: 115
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0x00, // Usage Minimum: 0x00
0x29, 0x73, // Usage Maximum: 0x73
0x75, 0x08, // Report Size: 8
0x95, 0x06, // Report Count: 6
0x81, 0x00, // Input: Data, Array, Absolute (keycode[6])
0xC0, // End Collection
// --- Consumer / Media Keys
0x05, 0x0C, // Usage Page: Consumer
0x09, 0x01, // Usage: Consumer Control
0xA1, 0x01, // Collection: Application
0x85, 0x02, // Report ID: 2
0x15, 0x00, // Logical Minimum: 0
0x26, 0xFF, 0x03, // Logical Maximum: 1023
0x19, 0x00, // Usage Minimum: 0x00
0x2A, 0xFF, 0x03, // Usage Maximum: 0x3FF
0x75, 0x10, // Report Size: 16
0x95, 0x01, // Report Count: 1
0x81, 0x00, // Input: Data, Array, Absolute
0xC0, // End Collection
// --- Mouse
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x02, // Usage: Mouse
0xA1, 0x01, // Collection: Application
0x85, 0x03, // Report ID: 3
0x09, 0x01, // Usage: Pointer
0xA1, 0x00, // Collection: Physical
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x03, // Usage Maximum: 3
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x95, 0x03, // Report Count: 3
0x75, 0x01, // Report Size: 1
0x81, 0x02, // Input: Data, Variable, Absolute (3 buttons)
0x95, 0x01, // Report Count: 1
0x75, 0x05, // Report Size: 5
0x81, 0x01, // Input: Constant, Array, Absolute (padding to byte boundary)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x38, // Usage: Wheel
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x03, // Report Count: 3
0x81, 0x06, // Input: Data, Variable, Relative
0xC0, // End Collection (Physical)
0xC0, // End Collection (Application)
};
const size_t hid_report_map_keyboard_consumer_mouse_len = sizeof(hid_report_map_keyboard_consumer_mouse);
// Gamepad only (report ID 1):
// left stick = X/Y, right stick = Rx/Ry, triggers share a single Z axis (LT = positive, RT = negative, centered = 0,),
// hat/dpad, 10 buttons (1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Back/Select 8=Start 9=L3 10=R3).
// DirectInput assigns axis usages to lX/lY/lZ/lRx/lRy by USAGE TAG, not by declaration order, so X/Y/Rx/Ry/Z in that order (not X/Y/Z/Rx/Ry) is correct.
const uint8_t hid_report_map_gamepad[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x05, // Usage: Game Pad
0xA1, 0x01, // Collection: Application
0x85, 0x01, // Report ID: 1
// --- 8-bit X, Y (left stick), Rx, Ry (right stick), Z (triggers, LT=+, RT=-) (min -127, max 127)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x33, // Usage: Rx
0x09, 0x34, // Usage: Ry
0x09, 0x32, // Usage: Z
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x05, // Report Count: 5
0x81, 0x02, // Input: Data, Variable, Absolute
// --- 4-bit hat/dpad (0-7 = direction clockwise from Up, 8 = centered/released via Null State).
// Logical range must be 0-7 (not 1-8) and size must be a nibble (not a full byte) - both are required for DirectInput to recognize this as a POV.
0x09, 0x39, // Usage: Hat Switch
0x15, 0x00, // Logical Minimum: 0
0x25, 0x07, // Logical Maximum: 7
0x35, 0x00, // Physical Minimum: 0
0x46, 0x3B, 0x01, // Physical Maximum: 315
0x65, 0x14, // Unit: Eng Rot:Angular Pos (degrees)
0x75, 0x04, // Report Size: 4
0x95, 0x01, // Report Count: 1
0x81, 0x42, // Input: Data, Variable, Absolute, Null State (value 8 = centered/released, outside the declared 0-7 logical range)
0x65, 0x00, // Unit: None - resets the Unit global item so it doesn't leak onto the button/padding items below (Unit is global, not local, in HID).
0x75, 0x04, // Report Size: 4
0x95, 0x01, // Report Count: 1
0x81, 0x03, // Input: Constant, Variable, Absolute (pad hat nibble to a full byte)
// --- 10 one-bit buttons: 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Back/Select 8=Start 9=L3 10=R3
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x0A, // Usage Maximum: 10
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x0A, // Report Count: 10
0x81, 0x02, // Input: Data, Variable, Absolute
// --- 6-bit padding to byte-align the buttons field (10 bits -> 16 bits / 2 bytes)
0x75, 0x06, // Report Size: 6
0x95, 0x01, // Report Count: 1
0x81, 0x03, // Input: Constant, Variable, Absolute
0xC0, // End Collection
};
const size_t hid_report_map_gamepad_len = sizeof(hid_report_map_gamepad);
} // extern "C"
@@ -0,0 +1,186 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_cdc_acm.h>
#include <esp_log.h>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#define TAG "esp32_usb_cdc_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- CDC device state ----
// A single presence flag, flipped by start_device()/stop_device() - matches how the USB device
// controller checks "is this board's usbdevicecdc0 child enabled" (usb_cdc_device_is_present()),
// independent of whether the console is actually installed right now (build_contribution()/
// start_console()/stop_console() are called by the controller, not tied to this device's own
// start/stop, since CDC's console lifecycle follows whichever primary claim is active - HID or
// MIDI; MSC is deliberately excluded, see esp32_usb_device_controller.cpp's claim()).
struct UsbCdcDeviceCtx {
bool present = false;
};
// ---- CDC console (log mirroring over the CDC ACM interface) ----
static bool cdc_console_installed = false;
static vprintf_like_t previous_vprintf = nullptr;
// Mirrors every log line to the CDC ACM interface in addition to whatever the log vprintf
// hook already does (UART0, ...) - deliberately additive rather than esp_tinyusb's own
// esp_tusb_init_console(), which freopen()s stdout/stderr and would replace the existing
// console instead of adding a second one alongside it.
static int cdc_mirroring_vprintf(const char* fmt, va_list args) {
// args must be copied before the first vprintf-family call consumes it - passing an
// already-consumed va_list to vsnprintf below is undefined behavior.
va_list cdc_args;
va_copy(cdc_args, args);
int result = previous_vprintf ? previous_vprintf(fmt, args) : vprintf(fmt, args);
if (tud_cdc_connected()) {
char buf[256];
int len = vsnprintf(buf, sizeof(buf), fmt, cdc_args);
if (len > 0) {
size_t to_write = static_cast<size_t>(len) < sizeof(buf) ? static_cast<size_t>(len) : sizeof(buf) - 1;
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, reinterpret_cast<const uint8_t*>(buf), to_write);
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);
}
}
va_end(cdc_args);
return result;
}
// ---- CDC descriptor contribution ----
static char cdc_interface_string[32] = "Tactility Console";
static uint8_t cdc_descriptor_bytes[TUD_CDC_DESC_LEN]; // one CDC instance per board
// ---- CDC device API ----
static bool cdc_device_is_present(struct Device* device) {
auto* ctx = static_cast<UsbCdcDeviceCtx*>(device_get_driver_data(device));
return ctx != nullptr && ctx->present;
}
static error_t cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string) {
(void)device;
// TUD_CDC_DESCRIPTOR consumes 2 interfaces internally (control at itfnum, data at itfnum+1 -
// it emits its own IAD covering both, see usbd.h) - same shape as MIDI's 2-interface layout.
// Also needs 2 IN endpoints (notif + data-in) and 1 OUT (data-out), mirroring the old
// HID-composited layout (HID_EP_NOTIF/HID_EP_CDC_IN/HID_EP_CDC_OUT), just dynamically
// assigned now instead of fixed literals.
struct UsbInterfaceAllocation alloc;
error_t err = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/2,
/*in_endpoint_count=*/2, /*out_endpoint_count=*/1, &alloc);
if (err != ERROR_NONE) {
return err;
}
const uint8_t ep_notif = alloc.first_in_endpoint;
const uint8_t ep_data_in = (uint8_t)(alloc.first_in_endpoint + 1);
const uint8_t ep_data_out = alloc.first_out_endpoint;
const uint8_t built[] = {
TUD_CDC_DESCRIPTOR(alloc.first_interface_number, interface_string_index,
ep_notif, 8, ep_data_out, ep_data_in, 64),
};
memcpy(cdc_descriptor_bytes, built, sizeof(built));
out_contribution->descriptor_bytes = cdc_descriptor_bytes;
out_contribution->descriptor_bytes_len = sizeof(cdc_descriptor_bytes);
out_contribution->interface_count = 2;
out_contribution->in_endpoint_count = 2;
out_contribution->out_endpoint_count = 1;
*out_interface_string = cdc_interface_string;
return ERROR_NONE;
}
static error_t cdc_device_start_console(struct Device* device) {
(void)device;
const tinyusb_config_cdcacm_t acm_cfg = {
.usb_dev = TINYUSB_USBDEV_0,
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = nullptr,
.callback_rx_wanted_char = nullptr,
.callback_line_state_changed = nullptr,
.callback_line_coding_changed = nullptr,
};
if (tusb_cdc_acm_init(&acm_cfg) != ESP_OK) {
LOG_E(TAG, "tusb_cdc_acm_init failed - CDC console unavailable");
return ERROR_RESOURCE;
}
previous_vprintf = esp_log_set_vprintf(cdc_mirroring_vprintf);
cdc_console_installed = true;
return ERROR_NONE;
}
static error_t cdc_device_stop_console(struct Device* device) {
(void)device;
if (!cdc_console_installed) {
return ERROR_NONE;
}
esp_log_set_vprintf(previous_vprintf ? previous_vprintf : vprintf);
previous_vprintf = nullptr;
tusb_cdc_acm_deinit(TINYUSB_CDC_ACM_0);
cdc_console_installed = false;
return ERROR_NONE;
}
extern const UsbCdcDeviceApi esp32_usb_cdc_device_api = {
.is_present = cdc_device_is_present,
.build_contribution = cdc_device_build_contribution,
.start_console = cdc_device_start_console,
.stop_console = cdc_device_stop_console,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (usbdevicecdc0). Presence is boolean -
// this driver doesn't participate in usb_device_controller_claim()/release() itself; the
// controller discovers this device once via device_for_each_child() in its own start_device()
// and calls is_present()/build_contribution()/start_console()/stop_console() directly.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbCdcDeviceCtx();
ctx->present = true;
device_set_driver_data(device, ctx);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbCdcDeviceCtx*>(device_get_driver_data(device));
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_cdc_device_driver = {
.name = "esp32_usb_cdc_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-cdc", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_cdc_device_api,
.device_type = &USB_CDC_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
@@ -0,0 +1,414 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED)
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <tinyusb.h>
#include <tusb.h>
#include <cstring>
#if CONFIG_IDF_TARGET_ESP32P4
#include <hal/usb_wrap_ll.h>
#include <soc/usb_wrap_struct.h>
#elif CONFIG_IDF_TARGET_ESP32S3
#include <hal/usb_serial_jtag_ll.h>
#endif
#define TAG "esp32_usb_device_controller"
#define GET_CONFIG(device) ((const Esp32UsbDeviceConfig*)(device)->config)
// Composite descriptor scratch buffer, sized for worst case (any one primary + CDC). Primary
// contributions today max out at MIDI's ~54 bytes (TUD_CONFIG_DESC_LEN + 2*9 + 2*9 + ...); 96
// leaves headroom without tracking each primary's exact size here.
static constexpr size_t COMPOSITE_CONFIG_DESCRIPTOR_MAX = TUD_CONFIG_DESC_LEN + 96 + TUD_CDC_DESC_LEN;
// Worst case: primary's own table (lang/mfr/product/serial/interface = 5) + CDC's own interface string.
static constexpr size_t COMPOSITE_STRING_DESCRIPTOR_MAX = 6;
// ---- Controller state ----
// One TinyUSB device-mode slot, shared by every USB device class (each a separate child device
// under usbdevice0 in the devicetree - see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp /
// esp32_usb_midi_device.cpp). Only one primary class may be installed at a time - see
// UsbDeviceControllerApi::claim(). CDC (esp32_usb_cdc_device.cpp) is a separate, orthogonal
// devicetree-presence addon composited into whichever primary is active (HID or MIDI; MSC is
// deliberately excluded, see claim()'s cdc_enabled computation) - see claim() below.
struct UsbDeviceControllerCtx {
enum UsbDeviceClass active_class = USB_DEVICE_CLASS_NONE;
bool phy_routed = false;
bool cdc_console_started = false;
// Allocation state, live from begin_claim() through claim()'s call to tinyusb_driver_install()
// (or until the claim attempt is abandoned by a caller that never follows through with claim()).
bool allocation_open = false;
uint8_t next_interface = 0;
uint8_t next_in_endpoint = 1; // EP0 reserved
uint8_t next_out_endpoint = 1;
// Composite descriptor scratch, rebuilt on every claim() - can't be `static const` per-class
// anymore now that CDC's presence is a runtime devicetree fact, not compile-time. Two buffers
// since a primary (MSC) may need different bytes per speed (see UsbInterfaceContribution's
// hs_descriptor_bytes) - under !TUD_OPT_HIGH_SPEED only the fs buffer is ever used.
uint8_t composite_fs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
#if (TUD_OPT_HIGH_SPEED)
uint8_t composite_hs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
#endif
const char* composite_string_descriptor[COMPOSITE_STRING_DESCRIPTOR_MAX];
tusb_desc_device_t composite_device_descriptor;
#if (TUD_OPT_HIGH_SPEED)
tusb_desc_device_qualifier_t composite_device_qualifier;
#endif
tinyusb_config_t composite_tusb_cfg;
struct Device* cdc_child = nullptr; // resolved once in start_device(); nullptr if no usbdevicecdc0 node
};
static void route_phy_for_device_mode() {
#if CONFIG_IDF_TARGET_ESP32P4
// Tab5's USB-C is wired to ESP32-P4 FSLS PHY0 (GPIO24/25). ESP-IDF's default USB_WRAP
// route uses FSLS PHY1 (GPIO26/27), so switch it here before installing TinyUSB.
usb_wrap_ll_phy_select(&USB_WRAP, 0);
#endif
}
static void restore_default_phy_route() {
#if CONFIG_IDF_TARGET_ESP32P4
usb_wrap_ll_phy_select(&USB_WRAP, 1);
#elif CONFIG_IDF_TARGET_ESP32S3
// S2/S3 share one FSLS PHY between USB-OTG and USB-Serial-JTAG. esp_tinyusb's teardown
// leaves the PHY muxed to USB-OTG \afterward, so the native console never comes back
// without this.
//
// \afterward ESP-IDF's usb_del_phy() (called from tinyusb_driver_uninstall() above).
usb_serial_jtag_ll_phy_enable_external(false);
#endif
}
static bool find_cdc_child(struct Device* child, void* context) {
if (device_get_type(child) == &USB_CDC_DEVICE_TYPE) {
*static_cast<struct Device**>(context) = child;
return false; // stop iterating
}
return true;
}
// Devicetree children are constructed/added/started strictly after their parent
// (kernel_init.cpp's dts_devices[] loop starts each device in list order, parent first) - so the
// usbdevicecdc0 child does not exist yet when this controller's own start_device() runs.
// Discovering it lazily here (called from is_cdc_enabled(), well after all devicetree devices
// have finished starting) instead of once at start_device() time is the only way to actually see
// it. Cheap to re-scan every call: at most one child of this type per board.
static struct Device* find_cdc_child_lazy(struct Device* device, struct UsbDeviceControllerCtx* ctx) {
if (ctx->cdc_child == nullptr) {
device_for_each_child(device, &ctx->cdc_child, find_cdc_child);
}
return ctx->cdc_child;
}
// ---- Controller API ----
static error_t begin_claim(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
LOG_E(TAG, "begin_claim: slot busy (active_class=%d)", ctx->active_class);
return ERROR_RESOURCE_BUSY;
}
ctx->allocation_open = true;
ctx->next_interface = 0;
ctx->next_in_endpoint = 1;
ctx->next_out_endpoint = 1;
return ERROR_NONE;
}
static error_t allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (!ctx->allocation_open) {
LOG_E(TAG, "allocate_interfaces: called without a preceding begin_claim()");
return ERROR_INVALID_STATE;
}
out_allocation->first_interface_number = ctx->next_interface;
out_allocation->first_in_endpoint = in_endpoint_count > 0 ? (uint8_t)(0x80 | ctx->next_in_endpoint) : 0;
out_allocation->first_out_endpoint = out_endpoint_count > 0 ? ctx->next_out_endpoint : 0;
ctx->next_interface = (uint8_t)(ctx->next_interface + interface_count);
ctx->next_in_endpoint = (uint8_t)(ctx->next_in_endpoint + in_endpoint_count);
ctx->next_out_endpoint = (uint8_t)(ctx->next_out_endpoint + out_endpoint_count);
return ERROR_NONE;
}
static bool is_cdc_enabled(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
struct Device* cdc_child = find_cdc_child_lazy(device, ctx);
return cdc_child != nullptr && usb_cdc_device_is_present(cdc_child);
}
static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const struct UsbDeviceClaimConfig* config) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
LOG_E(TAG, "claim: slot busy (active_class=%d, requested=%d)", ctx->active_class, usb_class);
return ERROR_RESOURCE_BUSY;
}
if (config == nullptr || config->device_descriptor == nullptr || config->primary.descriptor_bytes == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
if (!ctx->allocation_open) {
LOG_E(TAG, "claim: no begin_claim()/allocate_interfaces() session in progress");
return ERROR_INVALID_STATE;
}
// MSC is deliberately excluded from CDC compositing - a MSC-presenting device is meant to
// look/behave like plain mass storage to the host's storage stack, and there's no real use
// case for a console interface while acting as a flash drive, unlike HID/MIDI which
// plausibly want a console alongside. (A Windows safe-eject hang was seen during development
// of this compositing code but traced to a stale Windows-side driver/device-cache state,
// cleared by a host reboot - not caused by CDC compositing or this exclusion. Kept the
// exclusion anyway since it matches MSC's actual intended behavior.)
const bool cdc_enabled = usb_class != USB_DEVICE_CLASS_MSC && is_cdc_enabled(device);
// ---- String table: primary's table, plus CDC's own interface string appended last.
size_t string_count = config->string_descriptor_count;
if (string_count > COMPOSITE_STRING_DESCRIPTOR_MAX) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: primary string table too large (%u > %u)", (unsigned)string_count, (unsigned)COMPOSITE_STRING_DESCRIPTOR_MAX);
return ERROR_INVALID_ARGUMENT;
}
memcpy(ctx->composite_string_descriptor, config->string_descriptor, string_count * sizeof(const char*));
struct UsbInterfaceContribution cdc_contribution = {};
if (cdc_enabled) {
const char* cdc_interface_string = nullptr;
if (string_count >= COMPOSITE_STRING_DESCRIPTOR_MAX) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: no room for CDC's interface string");
return ERROR_INVALID_ARGUMENT;
}
error_t err = usb_cdc_device_build_contribution(ctx->cdc_child, device, (uint8_t)string_count,
&cdc_contribution, &cdc_interface_string);
if (err != ERROR_NONE) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: CDC build_contribution failed");
return err;
}
ctx->composite_string_descriptor[string_count] = cdc_interface_string;
string_count++;
}
// ---- Descriptor byte assembly: primary's bytes, then CDC's (if enabled), behind one config
// header. Built twice (fs/hs) under TUD_OPT_HIGH_SPEED if the primary supplied distinct
// high-speed bytes (see UsbInterfaceContribution::hs_descriptor_bytes) - CDC never varies by
// speed, so its bytes are reused verbatim in both buffers.
auto assemble = [&](uint8_t* dest, size_t dest_capacity, const uint8_t* primary_bytes, size_t primary_len) -> error_t {
const size_t total_len = TUD_CONFIG_DESC_LEN + primary_len + (cdc_enabled ? cdc_contribution.descriptor_bytes_len : 0);
if (total_len > dest_capacity) {
LOG_E(TAG, "claim: composite descriptor too large (%u > %u)", (unsigned)total_len, (unsigned)dest_capacity);
return ERROR_INVALID_ARGUMENT;
}
const uint8_t config_header[] = {
TUD_CONFIG_DESCRIPTOR(1, ctx->next_interface, 0, total_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
};
uint8_t* p = dest;
memcpy(p, config_header, sizeof(config_header));
p += sizeof(config_header);
memcpy(p, primary_bytes, primary_len);
p += primary_len;
if (cdc_enabled) {
memcpy(p, cdc_contribution.descriptor_bytes, cdc_contribution.descriptor_bytes_len);
}
return ERROR_NONE;
};
error_t assemble_err = assemble(ctx->composite_fs_config_descriptor, sizeof(ctx->composite_fs_config_descriptor),
config->primary.descriptor_bytes, config->primary.descriptor_bytes_len);
if (assemble_err != ERROR_NONE) {
ctx->allocation_open = false;
return assemble_err;
}
#if (TUD_OPT_HIGH_SPEED)
const uint8_t* hs_primary_bytes = config->primary.hs_descriptor_bytes != nullptr
? config->primary.hs_descriptor_bytes : config->primary.descriptor_bytes;
const size_t hs_primary_len = config->primary.hs_descriptor_bytes != nullptr
? config->primary.hs_descriptor_bytes_len : config->primary.descriptor_bytes_len;
assemble_err = assemble(ctx->composite_hs_config_descriptor, sizeof(ctx->composite_hs_config_descriptor),
hs_primary_bytes, hs_primary_len);
if (assemble_err != ERROR_NONE) {
ctx->allocation_open = false;
return assemble_err;
}
#endif
ctx->allocation_open = false;
// ---- Device descriptor: primary's metadata (idVendor/idProduct/strings), controller decides
// the class triad since CDC's presence (not the primary's identity) is what needs IAD in the
// general case - except MSC, which always got MISC/IAD unconditionally even standalone in
// the pre-refactor code (see esp32_usb_device_msc.cpp history). Kept that behavior verbatim
// for MSC (regardless of cdc_enabled, which is always false for MSC anyway - see the
// cdc_enabled computation above) simply to match what shipped before rather than introduce
// an unrelated behavior change while separating CDC out of HID's descriptor.
ctx->composite_device_descriptor = *static_cast<const tusb_desc_device_t*>(config->device_descriptor);
if (cdc_enabled || usb_class == USB_DEVICE_CLASS_MSC) {
ctx->composite_device_descriptor.bDeviceClass = TUSB_CLASS_MISC;
ctx->composite_device_descriptor.bDeviceSubClass = MISC_SUBCLASS_COMMON;
ctx->composite_device_descriptor.bDeviceProtocol = MISC_PROTOCOL_IAD;
} else {
ctx->composite_device_descriptor.bDeviceClass = TUSB_CLASS_UNSPECIFIED;
ctx->composite_device_descriptor.bDeviceSubClass = 0x00;
ctx->composite_device_descriptor.bDeviceProtocol = 0x00;
}
ctx->composite_tusb_cfg = {};
ctx->composite_tusb_cfg.device_descriptor = &ctx->composite_device_descriptor;
ctx->composite_tusb_cfg.string_descriptor = ctx->composite_string_descriptor;
ctx->composite_tusb_cfg.string_descriptor_count = string_count;
ctx->composite_tusb_cfg.external_phy = false;
#if (TUD_OPT_HIGH_SPEED)
ctx->composite_tusb_cfg.fs_configuration_descriptor = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.hs_configuration_descriptor = ctx->composite_hs_config_descriptor;
// The qualifier descriptor's class triad must mirror the device descriptor's (same IAD
// reasoning) - built here rather than supplied per-primary since its content is boilerplate
// (same bMaxPacketSize0/bNumConfigurations for every class) and the controller already knows
// the class triad.
ctx->composite_device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = ctx->composite_device_descriptor.bDeviceClass,
.bDeviceSubClass = ctx->composite_device_descriptor.bDeviceSubClass,
.bDeviceProtocol = ctx->composite_device_descriptor.bDeviceProtocol,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0x00,
};
ctx->composite_tusb_cfg.qualifier_descriptor = &ctx->composite_device_qualifier;
#else
ctx->composite_tusb_cfg.configuration_descriptor = ctx->composite_fs_config_descriptor;
#endif
ctx->composite_tusb_cfg.self_powered = false;
ctx->composite_tusb_cfg.vbus_monitor_io = 0;
route_phy_for_device_mode();
ctx->phy_routed = true;
if (tinyusb_driver_install(&ctx->composite_tusb_cfg) != ESP_OK) {
LOG_E(TAG, "claim: tinyusb_driver_install failed for class %d", usb_class);
if (ctx->phy_routed) {
restore_default_phy_route();
ctx->phy_routed = false;
}
return ERROR_RESOURCE;
}
ctx->active_class = usb_class;
if (cdc_enabled) {
if (usb_cdc_device_start_console(ctx->cdc_child) == ERROR_NONE) {
ctx->cdc_console_started = true;
} else {
LOG_E(TAG, "claim: CDC start_console failed - continuing without console");
}
}
return ERROR_NONE;
}
static error_t release(struct Device* device, enum UsbDeviceClass usb_class) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != usb_class) {
LOG_E(TAG, "release: class %d does not hold the slot (active=%d)", usb_class, ctx->active_class);
return ERROR_INVALID_STATE;
}
if (ctx->cdc_console_started) {
usb_cdc_device_stop_console(ctx->cdc_child);
ctx->cdc_console_started = false;
}
// Signal disconnect before tearing down so the host notices promptly.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(250));
tinyusb_driver_uninstall();
if (ctx->phy_routed) {
restore_default_phy_route();
ctx->phy_routed = false;
}
ctx->active_class = USB_DEVICE_CLASS_NONE;
return ERROR_NONE;
}
static enum UsbDeviceClass get_active_class(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
return ctx->active_class;
}
extern const UsbDeviceControllerApi usb_device_controller_api = {
.begin_claim = begin_claim,
.allocate_interfaces = allocate_interfaces,
.claim = claim,
.release = release,
.get_active_class = get_active_class,
.is_cdc_enabled = is_cdc_enabled,
};
// ---- Driver lifecycle ----
// This device is defined in each board's .dts (usbdevice0), same pattern as usbhost0 - its
// child classes (usbdevicehid0, usbdevicemsc0, usbdevicemidi0, usbdevicecdc0) are separate .dts
// nodes with their own drivers, wired to this device as their parent by the devicetree compiler.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbDeviceControllerCtx();
device_set_driver_data(device, ctx);
// cdc_child is NOT resolved here: usbdevicecdc0 (a child of this device in the devicetree)
// hasn't been constructed/started yet at this point - kernel_init.cpp starts devicetree
// devices in list order, parent before children. Resolved lazily instead, see
// find_cdc_child_lazy() / is_cdc_enabled().
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
// Safety cleanup: if a class still holds the slot (e.g. this device is stopped while HID/
// MSC/MIDI is still active), tear TinyUSB down and restore the PHY route before freeing ctx -
// otherwise the installed TinyUSB driver and mis-routed PHY would outlive the context that
// tracks them. Mirrors the same safety-release pattern each child driver's own stop_device
// uses (see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp / esp32_usb_midi_device.cpp).
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
release(device, ctx->active_class);
}
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_device_controller_driver = {
.name = "esp32_usb_device_controller",
.compatible = (const char*[]) { "espressif,esp32-usbdevice", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &usb_device_controller_api,
.device_type = &USB_DEVICE_CONTROLLER_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT)
@@ -0,0 +1,250 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <wear_levelling.h>
#include <cstring>
#define TAG "esp32_usb_device_msc"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- MSC device descriptor set ----
// Mutable, matching the other primary classes' descriptors, even though MSC's own
// bDeviceClass/SubClass/Protocol below are never actually rewritten by the controller - MSC is
// excluded from CDC compositing (see esp32_usb_device_controller.cpp's claim(), cdc_enabled
// computation) and always keeps its own unconditional MISC/IAD triad regardless.
static tusb_desc_device_t msc_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4002,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char* msc_string_descriptor[] = {
(const char[]) { 0x09, 0x04 }, // 0: English (0x0409)
"Tactility", // 1: Manufacturer
"Tactility Device", // 2: Product
"42", // 3: Serial
"Tactility Mass Storage", // 4: MSC
};
// Interface/endpoint numbers here are the values usb_device_controller_allocate_interfaces()
// always returns for MSC in practice (MSC is always the first/only allocation for itself, per
// the controller's fixed primary-first ordering) - still requested via allocate_interfaces() at
// claim time rather than assumed, so this stays correct if that ordering ever changes.
static uint8_t msc_fs_configuration_descriptor[TUD_MSC_DESC_LEN];
#if (TUD_OPT_HIGH_SPEED)
static uint8_t msc_hs_configuration_descriptor[TUD_MSC_DESC_LEN];
#endif
// ---- MSC device state ----
struct UsbMscDeviceCtx {
UsbMscDeviceMountChangedCallback mount_changed_cb = nullptr;
void* mount_changed_context = nullptr;
bool storage_active = false;
};
// device pointer isn't threaded through the TinyUSB mount-changed callback, so this driver
// supports a single active MSC device instance at a time - matches the single TinyUSB
// device-mode slot the controller already enforces.
static struct Device* active_msc_device = nullptr;
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
if (active_msc_device == nullptr) return;
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(active_msc_device));
if (ctx == nullptr) return;
const bool mounted = event->mount_changed_data.is_mounted;
LOG_I(TAG, "%s", mounted ? "MSC mounted" : "MSC unmounted");
if (ctx->mount_changed_cb != nullptr) {
ctx->mount_changed_cb(mounted, ctx->mount_changed_context);
}
}
// ---- MSC device API ----
static error_t msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context) {
if (source_handle == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/1,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/1, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
const uint8_t msc_fs_bytes[] = {
TUD_MSC_DESCRIPTOR(alloc.first_interface_number, 4, alloc.first_out_endpoint, alloc.first_in_endpoint, 64),
};
memcpy(msc_fs_configuration_descriptor, msc_fs_bytes, sizeof(msc_fs_bytes));
#if (TUD_OPT_HIGH_SPEED)
const uint8_t msc_hs_bytes[] = {
TUD_MSC_DESCRIPTOR(alloc.first_interface_number, 4, alloc.first_out_endpoint, alloc.first_in_endpoint, 512),
};
memcpy(msc_hs_configuration_descriptor, msc_hs_bytes, sizeof(msc_hs_bytes));
#endif
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &msc_device_descriptor;
claim_config.string_descriptor = msc_string_descriptor;
claim_config.string_descriptor_count = sizeof(msc_string_descriptor) / sizeof(msc_string_descriptor[0]);
claim_config.primary.descriptor_bytes = msc_fs_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(msc_fs_configuration_descriptor);
#if (TUD_OPT_HIGH_SPEED)
claim_config.primary.hs_descriptor_bytes = msc_hs_configuration_descriptor;
claim_config.primary.hs_descriptor_bytes_len = sizeof(msc_hs_configuration_descriptor);
#endif
claim_config.primary.interface_count = 1;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 1;
error_t claim_result = usb_device_controller_claim(controller, USB_DEVICE_CLASS_MSC, &claim_config);
if (claim_result != ERROR_NONE) {
return claim_result;
}
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
ctx->mount_changed_cb = mount_changed_cb;
ctx->mount_changed_context = context;
active_msc_device = device;
esp_err_t result;
if (source == USB_MSC_DEVICE_SOURCE_SDMMC) {
const tinyusb_msc_sdmmc_config_t config_sdmmc = {
.card = static_cast<sdmmc_card_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_sdmmc(&config_sdmmc);
} else {
const tinyusb_msc_spiflash_config_t config_flash = {
.wl_handle = *static_cast<wl_handle_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_spiflash(&config_flash);
}
if (result != ESP_OK) {
LOG_E(TAG, "storage init failed: %s", esp_err_to_name(result));
active_msc_device = nullptr;
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
return ERROR_RESOURCE;
}
ctx->storage_active = true;
return ERROR_NONE;
}
static error_t msc_device_stop(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
if (ctx == nullptr || !ctx->storage_active) {
return ERROR_NONE;
}
tinyusb_msc_storage_deinit();
auto* controller = device_get_parent(device);
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
ctx->storage_active = false;
ctx->mount_changed_cb = nullptr;
ctx->mount_changed_context = nullptr;
if (active_msc_device == device) {
active_msc_device = nullptr;
}
return ERROR_NONE;
}
static bool msc_device_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
return ctx != nullptr && ctx->storage_active && tud_mounted();
}
extern const UsbMscDeviceApi esp32_usb_msc_device_api = {
.start = msc_device_start,
.stop = msc_device_stop,
.is_connected = msc_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicemsc0).
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbMscDeviceCtx();
device_set_driver_data(device, ctx);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
if (ctx != nullptr && ctx->storage_active) {
msc_device_stop(device);
}
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_msc_device_driver = {
.name = "esp32_usb_msc_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-msc", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_msc_device_api,
.device_type = &USB_MSC_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
@@ -0,0 +1,352 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/hid_report_descriptors.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstring>
#define TAG "esp32_usb_hid_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- HID Report Maps ----
// Defined once in Platforms/platform-esp32/source/drivers/hid_report_descriptors.cpp and shared
// with esp32_ble_hid.cpp - HID report descriptors are transport-independent (same USB HID Usage
// Tables spec applies to both USB and BLE HID), so the same bytes work verbatim over either
// transport.
// Report IDs, matching the shared hid_report_map_* tables.
static constexpr uint8_t REPORT_ID_KEYBOARD = 1;
static constexpr uint8_t REPORT_ID_CONSUMER = 2;
static constexpr uint8_t REPORT_ID_MOUSE_SOLO = 1; // USB_HID_DEVICE_MODE_MOUSE
static constexpr uint8_t REPORT_ID_MOUSE_COMBO = 3; // USB_HID_DEVICE_MODE_KEYBOARD_MOUSE
static constexpr uint8_t REPORT_ID_GAMEPAD = 1;
// Active mode/report-map state - the report descriptor TinyUSB serves via
// tud_hid_descriptor_report_cb() must match whichever mode start() was last called with, since
// (unlike BLE's mutable GATT) USB's descriptor is fixed for the lifetime of one claim()
// session; switching modes means stop() + start() with the new mode, re-enumerating.
static enum UsbHidDeviceMode active_mode = USB_HID_DEVICE_MODE_KEYBOARD;
static const uint8_t* active_report_map() {
switch (active_mode) {
case USB_HID_DEVICE_MODE_MOUSE: return hid_report_map_mouse;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return hid_report_map_keyboard_consumer_mouse;
case USB_HID_DEVICE_MODE_GAMEPAD: return hid_report_map_gamepad;
default: return hid_report_map_keyboard_consumer; // KEYBOARD
}
}
static size_t active_report_map_len() {
switch (active_mode) {
case USB_HID_DEVICE_MODE_MOUSE: return hid_report_map_mouse_len;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return hid_report_map_keyboard_consumer_mouse_len;
case USB_HID_DEVICE_MODE_GAMEPAD: return hid_report_map_gamepad_len;
default: return hid_report_map_keyboard_consumer_len;
}
}
// Per-mode default product name and idProduct offset. Distinct idProduct per mode matters more
// than it might seem: most hosts (Windows in particular) cache device metadata - including
// which icon/appearance to show - keyed by VID/PID/serial, and a HID descriptor swap alone
// isn't always enough to make the host re-read it on a fast re-enumeration. Giving each mode
// its own PID makes the host treat a mode switch as connecting a genuinely different device,
// which reliably clears the stale-appearance problem (was previously showing "Keyboard" for
// every mode almost all the time).
static const char* default_name_for_mode(enum UsbHidDeviceMode mode) {
switch (mode) {
case USB_HID_DEVICE_MODE_MOUSE: return "Tactility Mouse";
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return "Tactility Keyboard+Mouse";
case USB_HID_DEVICE_MODE_GAMEPAD: return "Tactility Gamepad";
default: return "Tactility Keyboard"; // KEYBOARD
}
}
static uint16_t product_id_for_mode(enum UsbHidDeviceMode mode) {
switch (mode) {
case USB_HID_DEVICE_MODE_MOUSE: return 0x4006;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return 0x4007;
case USB_HID_DEVICE_MODE_GAMEPAD: return 0x4008;
default: return 0x4004; // KEYBOARD (unchanged - existing PID)
}
}
// Set via usb_hid_device_set_name() before start(); empty means "use the mode's default name".
// Mirrors bluetooth_set_device_name()'s pattern of being set once before starting the profile.
static char custom_name[32] = {};
// HID's own interface/endpoint numbers come from allocate_interfaces() at claim time
// (hid_device_start()) - CDC, if the board's usbdevicecdc0 child is enabled,
// is composited in by the USB device controller itself after HID's own contribution,
// so HID no longer needs to know or care whether CDC exists.
// The report descriptor length varies by mode (KEYBOARD_MOUSE's combined map is the largest),
// so the config descriptor's total length is computed at claim time in hid_device_start().
// Built fresh in start() (see build_hid_device_descriptor()) so idProduct/iProduct can vary per
// mode - see default_name_for_mode()/product_id_for_mode() above for why.
// Mutable for the same reason, plus: the USB device controller patches bDeviceClass/SubClass/Protocol at
// claim() time depending on whether CDC is composited in.
static tusb_desc_device_t hid_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_UNSPECIFIED,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4004, // overwritten per-mode in start()
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char hid_langid_descriptor[] = { 0x09, 0x04 };
// index 2 (iProduct) is overwritten per-mode/per-custom-name in start(), see
// build_hid_device_descriptor().
static char hid_product_string[sizeof(custom_name)] = "Tactility Keyboard";
// index 4 is the HID interface string, read directly by some HID apps/testers (gamepad testers, joystick tools) instead of iProduct (index 2).
// Point it at the same buffer as iProduct so both stay in sync with whatever mode/custom name is active.
static const char* hid_string_descriptor[] = {
hid_langid_descriptor, "Tactility", hid_product_string, "123456", hid_product_string,
};
// Built fresh in hid_device_start(), sized for the active mode's report map. TinyUSB only reads
// this once during tinyusb_driver_install() (inside claim()), so it's safe to mutate between
// claim sessions (i.e. stop() + start() with a different mode) as long as it's finalized before
// claim() is called.
static uint8_t hid_configuration_descriptor[TUD_HID_DESC_LEN];
// ---- TinyUSB HID callbacks (required by the TinyUSB HID class driver) ----
extern "C" {
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
(void)instance;
return active_report_map();
}
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
uint8_t* buffer, uint16_t reqlen) {
(void)instance; (void)report_id; (void)report_type; (void)buffer; (void)reqlen;
return 0;
}
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
uint8_t const* buffer, uint16_t bufsize) {
(void)instance; (void)report_id; (void)report_type; (void)buffer; (void)bufsize;
// LED state (caps/num/scroll lock) - not currently surfaced to callers.
}
} // extern "C"
// ---- HID device API ----
// Fills in idProduct/iProduct for the given mode, honoring a set_name() override if one was
// set. Must run before every claim() - the name/PID need to match whatever mode is about to
// start, not whatever they were left as from a previous session.
static void build_hid_device_descriptor(enum UsbHidDeviceMode mode) {
hid_device_descriptor.idProduct = product_id_for_mode(mode);
const char* name = custom_name[0] ? custom_name : default_name_for_mode(mode);
strncpy(hid_product_string, name, sizeof(hid_product_string) - 1);
hid_product_string[sizeof(hid_product_string) - 1] = '\0';
}
static error_t hid_device_start(struct Device* device, enum UsbHidDeviceMode mode) {
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/1,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/0, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
// active_mode/hid_device_descriptor commit only once resources are reserved - reads of
// active_mode (tud_hid_descriptor_report_cb(), the mode gates in send_*) must keep matching
// whatever session is actually installed if begin_claim()/allocate_interfaces() above failed.
active_mode = mode;
build_hid_device_descriptor(mode);
const uint8_t hid_bytes[] = {
TUD_HID_DESCRIPTOR(alloc.first_interface_number, 4, HID_ITF_PROTOCOL_NONE,
active_report_map_len(), alloc.first_in_endpoint, 16, 10),
};
memcpy(hid_configuration_descriptor, hid_bytes, sizeof(hid_bytes));
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &hid_device_descriptor;
claim_config.string_descriptor = hid_string_descriptor;
claim_config.string_descriptor_count = sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]);
claim_config.primary.descriptor_bytes = hid_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(hid_configuration_descriptor);
claim_config.primary.interface_count = 1;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 0;
return usb_device_controller_claim(controller, USB_DEVICE_CLASS_HID_KEYBOARD, &claim_config);
}
static error_t hid_device_stop(struct Device* device) {
auto* controller = device_get_parent(device);
return usb_device_controller_release(controller, USB_DEVICE_CLASS_HID_KEYBOARD);
}
static error_t hid_device_set_name(struct Device* device, const char* name) {
(void)device;
if (name == nullptr) {
custom_name[0] = '\0'; // clear override, fall back to the mode's default name
return ERROR_NONE;
}
strncpy(custom_name, name, sizeof(custom_name) - 1);
custom_name[sizeof(custom_name) - 1] = '\0';
return ERROR_NONE;
}
// tud_hid_report() fails outright (no queueing) if the endpoint is still busy sending a
// previous report - a single fire-and-forget call is unsafe whenever a caller sends two reports
// back to back (e.g. press immediately followed by release: the second call can lose the race and drop the release, leaving
// the host thinking a key/button/consumer-control is still held down (which then either does
// nothing further or, for keys the host auto-repeats, keeps repeating). Retry with a short
// backoff instead of failing immediately.
static error_t hid_send_report(uint8_t report_id, const uint8_t* report, size_t len) {
if (!tud_mounted()) {
LOG_W(TAG, "hid_send_report(id=%d): tud_mounted() false - real disconnect, not just a busy endpoint", report_id);
return ERROR_INVALID_STATE;
}
for (int attempt = 0; attempt < 10; attempt++) {
if (tud_hid_ready() && tud_hid_report(report_id, report, static_cast<uint16_t>(len))) {
return ERROR_NONE;
}
vTaskDelay(pdMS_TO_TICKS(5));
}
LOG_W(TAG, "hid_send_report(id=%d): gave up after 10 retries (~50ms) - endpoint stayed busy/not-ready the whole time", report_id);
return ERROR_RESOURCE;
}
static error_t hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_KEYBOARD && active_mode != USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[8] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_KEYBOARD, buf, sizeof(buf));
}
static error_t hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_KEYBOARD && active_mode != USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[2] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_CONSUMER, buf, sizeof(buf));
}
static error_t hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
uint8_t report_id;
if (active_mode == USB_HID_DEVICE_MODE_MOUSE) {
report_id = REPORT_ID_MOUSE_SOLO;
} else if (active_mode == USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
report_id = REPORT_ID_MOUSE_COMBO;
} else {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[4] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(report_id, buf, sizeof(buf));
}
static error_t hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_GAMEPAD) {
return ERROR_NOT_SUPPORTED;
}
// 8 bytes: X,Y,Rx,Ry,Z (1 each - Xbox-360-style, Z shared by triggers), hat/dpad (1),
// buttons[2] (10 buttons + 6 padding bits). See hid_report_descriptors.cpp for the full
// layout/rationale.
uint8_t buf[8] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_GAMEPAD, buf, sizeof(buf));
}
static bool hid_device_is_connected(struct Device* device) {
(void)device;
// tud_hid_ready() deliberately NOT included here: it reflects whether the endpoint is idle
// right now (false during every in-flight report send, which is normal and momentary, not
// a disconnect), not whether a host is actually connected. Including it made this function
// flap constantly under any traffic - explains the "Connected" <-> "waiting for host..."
// flicker apps were seeing on every keypress/click even though nothing was actually wrong
// (hid_send_report()'s own retry logic was succeeding the whole time; no warning ever
// logged because there was nothing to warn about). tud_mounted() alone is the correct
// "is a host connected" signal.
return tud_mounted();
}
extern const UsbHidDeviceApi esp32_usb_hid_device_api = {
.start = hid_device_start,
.stop = hid_device_stop,
.set_name = hid_device_set_name,
.send_keyboard = hid_device_send_keyboard,
.send_consumer = hid_device_send_consumer,
.send_mouse = hid_device_send_mouse,
.send_gamepad = hid_device_send_gamepad,
.is_connected = hid_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicehid0) - the devicetree
// compiler wires device_get_parent() to the controller automatically.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
// Safety cleanup: release the slot if it's still held (e.g. this device is stopped while
// HID is still active).
auto* controller = device_get_parent(device);
if (controller != nullptr && usb_device_controller_get_active_class(controller) == USB_DEVICE_CLASS_HID_KEYBOARD) {
usb_device_controller_release(controller, USB_DEVICE_CLASS_HID_KEYBOARD);
}
return ERROR_NONE;
}
Driver esp32_usb_hid_device_driver = {
.name = "esp32_usb_hid_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-hid", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_hid_device_api,
.device_type = &USB_HID_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
@@ -0,0 +1,195 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb.h>
#include <cstring>
#define TAG "esp32_usb_midi_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- MIDI device descriptor set ----
// TUD_MIDI_DESCRIPTOR consumes 2 interfaces internally (Audio Control + MIDI Streaming,
// itfnum and itfnum+1 - see TUD_MIDI_DESC_HEAD in usbd.h), unlike HID/MSC/CDC which use 1
// each - allocate_interfaces(2, 1, 1, ...) in midi_device_start() reflects that.
// Mutable: the USB device controller patches bDeviceClass/SubClass/Protocol at
// claim() time depending on whether CDC is composited in (see esp32_usb_device_controller.cpp).
static tusb_desc_device_t midi_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4005,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char midi_langid_descriptor[] = { 0x09, 0x04 };
// index 2 (iProduct) is overwritten by set_name() before start(), see
// midi_device_set_name()/midi_device_start(). Index 4 is the jack name (see
// midi_configuration_descriptor below).
//
// The interface string index is literal 0 ("no string" - a real, well-defined USB concept: the
// host skips string lookup entirely, distinct from an empty string AT a valid index), not an
// index into this table at all. An earlier attempt used an empty "" entry at a real index
// instead of literal 0 - that crashed the device outright on Windows (Code 10,
// STATUS_DEVICE_DATA_ERROR): empty-string-at-a-valid-index and "no string" (index 0) are not
// the same thing, only the latter is safe. Both Windows' newer MIDI naming and MIDIBerry were
// confirmed (live test) to concatenate interface-string + jack-string into one displayed name,
// so leaving the interface string as "no string" (0) means only the jack string shows, instead
// of duplicating/prefixing the product name.
static char midi_product_string[32] = "Tactility MIDI Device";
static const char* midi_string_descriptor[] = {
midi_langid_descriptor, "Tactility", midi_product_string, "123456", midi_product_string,
};
static constexpr uint8_t MIDI_STRIDX_JACK = 4;
// TUD_MIDI_DESCRIPTOR (the usbd.h convenience macro) hardcodes its jack descriptors' string
// index to 0 (no string) - it isn't a parameter the macro exposes. Built by hand here with a
// real string index for the jack (instead of the macro's hardcoded 0), while keeping the
// interface string index at literal 0 (see comment above on midi_string_descriptor for why 0,
// not an empty table entry). Interface/endpoint numbers come from allocate_interfaces() at claim
// time rather than compile-time constants, since CDC may now be composited in after MIDI.
static uint8_t midi_configuration_descriptor[TUD_MIDI_DESC_LEN];
// ---- MIDI device API ----
static error_t midi_device_start(struct Device* device) {
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/2,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/1, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
const uint8_t itf_ac = alloc.first_interface_number;
const uint8_t midi_bytes[] = {
TUD_MIDI_DESC_HEAD(itf_ac, 0, 1),
TUD_MIDI_DESC_JACK_DESC(1, MIDI_STRIDX_JACK),
TUD_MIDI_DESC_EP(alloc.first_out_endpoint, 64, 1),
TUD_MIDI_JACKID_IN_EMB(1),
TUD_MIDI_DESC_EP(alloc.first_in_endpoint, 64, 1),
TUD_MIDI_JACKID_OUT_EMB(1),
};
memcpy(midi_configuration_descriptor, midi_bytes, sizeof(midi_bytes));
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &midi_device_descriptor;
claim_config.string_descriptor = midi_string_descriptor;
claim_config.string_descriptor_count = sizeof(midi_string_descriptor) / sizeof(midi_string_descriptor[0]);
claim_config.primary.descriptor_bytes = midi_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(midi_configuration_descriptor);
claim_config.primary.interface_count = 2;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 1;
return usb_device_controller_claim(controller, USB_DEVICE_CLASS_MIDI, &claim_config);
}
static error_t midi_device_stop(struct Device* device) {
auto* controller = device_get_parent(device);
return usb_device_controller_release(controller, USB_DEVICE_CLASS_MIDI);
}
static error_t midi_device_set_name(struct Device* device, const char* name) {
(void)device;
if (name == nullptr) {
strncpy(midi_product_string, "Tactility MIDI Device", sizeof(midi_product_string) - 1);
midi_product_string[sizeof(midi_product_string) - 1] = '\0';
return ERROR_NONE;
}
strncpy(midi_product_string, name, sizeof(midi_product_string) - 1);
midi_product_string[sizeof(midi_product_string) - 1] = '\0';
return ERROR_NONE;
}
static error_t midi_device_send(struct Device* device, const uint8_t* msg, size_t len) {
auto* controller = device_get_parent(device);
if (controller == nullptr || usb_device_controller_get_active_class(controller) != USB_DEVICE_CLASS_MIDI) {
return ERROR_INVALID_STATE;
}
if (!tud_mounted()) {
return ERROR_INVALID_STATE;
}
uint32_t written = tud_midi_stream_write(0, msg, static_cast<uint32_t>(len));
return written == len ? ERROR_NONE : ERROR_RESOURCE;
}
static bool midi_device_is_connected(struct Device* device) {
auto* controller = device_get_parent(device);
if (controller == nullptr || usb_device_controller_get_active_class(controller) != USB_DEVICE_CLASS_MIDI) {
return false;
}
return tud_mounted();
}
extern const UsbMidiDeviceApi esp32_usb_midi_device_api = {
.start = midi_device_start,
.stop = midi_device_stop,
.set_name = midi_device_set_name,
.send = midi_device_send,
.is_connected = midi_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicemidi0) - the devicetree
// compiler wires device_get_parent() to the controller automatically.
extern "C" {
// tud_midi_rx_cb is intentionally not implemented: this driver is send-only, matching
// bluetooth_midi.h's API shape (no receive exposed at the Tactility layer either). Incoming
// bytes are simply left in TinyUSB's RX FIFO and eventually overwritten/dropped.
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
// Safety cleanup: release the slot if it's still held (e.g. this device is stopped while
// MIDI is still active).
auto* controller = device_get_parent(device);
if (controller != nullptr && usb_device_controller_get_active_class(controller) == USB_DEVICE_CLASS_MIDI) {
usb_device_controller_release(controller, USB_DEVICE_CLASS_MIDI);
}
return ERROR_NONE;
}
Driver esp32_usb_midi_device_driver = {
.name = "esp32_usb_midi_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-midi", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_midi_device_api,
.device_type = &USB_MIDI_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
@@ -43,6 +43,21 @@ extern Driver esp32_usbhost_hid_driver;
extern Driver esp32_usbhost_midi_driver; extern Driver esp32_usbhost_midi_driver;
extern Driver esp32_usbhost_msc_driver; extern Driver esp32_usbhost_msc_driver;
#endif #endif
#if SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED)
extern Driver esp32_usb_device_controller_driver;
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
extern Driver esp32_usb_hid_device_driver;
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
extern Driver esp32_usb_msc_device_driver;
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
extern Driver esp32_usb_midi_device_driver;
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
extern Driver esp32_usb_cdc_device_driver;
#endif
static error_t start() { static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct, /* We crash when construct fails, because if a single driver fails to construct,
@@ -78,6 +93,25 @@ static error_t start() {
check(driver_construct_add(&esp32_usbhost_hid_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_midi_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE); check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE);
#endif
// usbdevice0 and its children (usbdevicehid0, usbdevicemsc0, ...) are declared per-board in
// .dts, same pattern as usbhost0 above - the devicetree compiler constructs/adds/starts their
// Device instances and wires parent/child relationships. Only driver registration happens
// here.
#if SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED)
check(driver_construct_add(&esp32_usb_device_controller_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
check(driver_construct_add(&esp32_usb_hid_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
check(driver_construct_add(&esp32_usb_msc_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
check(driver_construct_add(&esp32_usb_midi_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
check(driver_construct_add(&esp32_usb_cdc_device_driver) == ERROR_NONE);
#endif #endif
return ERROR_NONE; return ERROR_NONE;
} }
@@ -89,6 +123,21 @@ static error_t stop() {
check(driver_remove_destruct(&esp32_wifi_pinned_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_wifi_pinned_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_wifi_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_wifi_driver) == ERROR_NONE);
#endif #endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
check(driver_remove_destruct(&esp32_usb_cdc_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
check(driver_remove_destruct(&esp32_usb_midi_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
check(driver_remove_destruct(&esp32_usb_msc_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
check(driver_remove_destruct(&esp32_usb_hid_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED)
check(driver_remove_destruct(&esp32_usb_device_controller_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED #if SOC_USB_OTG_SUPPORTED
check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE); 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_midi_driver) == ERROR_NONE);
+50 -166
View File
@@ -10,102 +10,43 @@
#include <esp_system.h> #include <esp_system.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <wear_levelling.h> #include <wear_levelling.h>
#include <tactility/device.h>
#include <tactility/drivers/esp32_sdcard.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/log.h> #include <tactility/log.h>
#if CONFIG_IDF_TARGET_ESP32P4
#include "hal/usb_wrap_ll.h"
#endif
#define EPNUM_MSC 1
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN)
#define SECTOR_SIZE 512
constexpr auto* TAG = "USB"; constexpr auto* TAG = "USB";
namespace tt::hal::usb { namespace tt::hal::usb {
extern sdmmc_card_t* getCard();
}
// Set when mass storage was started as part of the dedicated reboot-into-MSC boot flow. // Set when mass storage was started as part of the dedicated reboot-into-MSC boot flow.
// Used to decide whether ejecting the volume should automatically reboot back to normal OS. // Used to decide whether ejecting the volume should automatically reboot back to normal OS.
static bool startedFromBootMode = false; static bool startedFromBootMode = false;
enum { static sdmmc_card_t* getCard() {
ITF_NUM_MSC = 0, sdmmc_card_t* card = nullptr;
ITF_NUM_TOTAL
};
enum { device_for_each(&card, [](auto* device, void* context) {
EDPT_CTRL_OUT = 0x00, auto* driver = device_get_driver(device);
EDPT_CTRL_IN = 0x80, if (driver == nullptr) return true;
if (!driver_is_compatible(driver, "espressif,esp32-sdspi") &&
!driver_is_compatible(driver, "espressif,esp32-sdmmc")) return true;
auto** out = static_cast<sdmmc_card_t**>(context);
*out = esp32_sdcard_get_card(device);
return *out == nullptr;
});
EDPT_MSC_OUT = 0x01, if (card == nullptr) {
EDPT_MSC_IN = 0x81, LOG_W(TAG, "Couldn't find a mounted SD card");
}; }
static bool driverInstalled = false; return card;
}
static tusb_desc_device_t descriptor_config = { static void onMountChanged(bool mounted, void* /*context*/) {
.bLength = sizeof(descriptor_config), if (mounted) {
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // TODO: Espressif VID. Do we need to change this?
.idProduct = 0x4002,
.bcdDevice = 0x100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
static char const* string_desc_arr[] = {
(const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
"Espressif", // 1: Manufacturer
"Tactility Device", // 2: Product
"42", // 3: Serials
"Tactility Mass Storage", // 4. MSC
};
static uint8_t const msc_fs_configuration_desc[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, EP Out & EP In address, EP size
TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64),
};
#if (TUD_OPT_HIGH_SPEED)
static const tusb_desc_device_qualifier_t device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0
};
static uint8_t const msc_hs_configuration_desc[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, EP Out & EP In address, EP size
TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 512),
};
#endif // TUD_OPT_HIGH_SPEED
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
if (event->mount_changed_data.is_mounted) {
LOG_I(TAG, "MSC Mounted"); LOG_I(TAG, "MSC Mounted");
// Storage is only (re)mounted into our own filesystem after the host sends a SCSI // Storage is only (re)mounted into our own filesystem after the host sends a SCSI
// START STOP UNIT eject (see tud_msc_start_stop_cb() in tusb_msc_storage.c). Windows // START STOP UNIT eject (see tud_msc_start_stop_cb() in tusb_msc_storage.c). Windows
@@ -121,130 +62,73 @@ static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
} }
} }
static bool ensureDriverInstalled() { } // namespace tt::hal::usb
if (driverInstalled) {
return true;
}
#if CONFIG_IDF_TARGET_ESP32P4
// Tab5's USB-C port is wired to FSLS PHY0, but the USB_WRAP (FS/FSLS) controller
// used by TinyUSB device mode defaults to FSLS PHY1. Route it to PHY0 before
// installing the TinyUSB driver.
usb_wrap_ll_phy_select(&USB_WRAP, 0);
#endif
const tinyusb_config_t tusb_cfg = {
.device_descriptor = &descriptor_config,
.string_descriptor = string_desc_arr,
.string_descriptor_count = sizeof(string_desc_arr) / sizeof(string_desc_arr[0]),
.external_phy = false,
#if (TUD_OPT_HIGH_SPEED)
.fs_configuration_descriptor = msc_fs_configuration_desc,
.hs_configuration_descriptor = msc_hs_configuration_desc,
.qualifier_descriptor = &device_qualifier,
#else
.configuration_descriptor = msc_fs_configuration_desc,
#endif // TUD_OPT_HIGH_SPEED
.self_powered = false,
.vbus_monitor_io = 0
};
if (tinyusb_driver_install(&tusb_cfg) != ESP_OK) {
LOG_E(TAG, "Failed to install TinyUSB driver");
#if CONFIG_IDF_TARGET_ESP32P4
// Roll back routing when TinyUSB did not start.
usb_wrap_ll_phy_select(&USB_WRAP, 1);
#endif
return false;
}
driverInstalled = true;
return true;
}
bool tusbIsSupported() { return true; } bool tusbIsSupported() { return true; }
bool tusbStartMassStorageWithSdmmc(bool fromBootMode) { bool tusbStartMassStorageWithSdmmc(bool fromBootMode) {
if (!ensureDriverInstalled()) { auto* device = usb_msc_device_get();
if (device == nullptr) {
LOG_E(TAG, "USB MSC device not available");
return false; return false;
} }
startedFromBootMode = fromBootMode;
auto* card = tt::hal::usb::getCard(); auto* card = tt::hal::usb::getCard();
if (card == nullptr) { if (card == nullptr) {
LOG_E(TAG, "SD card not mounted"); LOG_E(TAG, "SD card not mounted");
device_put(device);
return false; return false;
} }
const tinyusb_msc_sdmmc_config_t config_sdmmc = { tt::hal::usb::startedFromBootMode = fromBootMode;
.card = card,
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false
}
};
auto result = tinyusb_msc_storage_init_sdmmc(&config_sdmmc); auto result = usb_msc_device_start(device, USB_MSC_DEVICE_SOURCE_SDMMC, card,
if (result != ESP_OK) { tt::hal::usb::onMountChanged, nullptr);
LOG_E(TAG, "TinyUSB SDMMC init failed: %s", esp_err_to_name(result)); if (result != ERROR_NONE) {
LOG_E(TAG, "TinyUSB SDMMC init failed: %s", error_to_string(result));
} else { } else {
LOG_I(TAG, "TinyUSB SDMMC init success"); LOG_I(TAG, "TinyUSB SDMMC init success");
} }
return result == ESP_OK; device_put(device);
return result == ERROR_NONE;
} }
bool tusbStartMassStorageWithFlash(bool fromBootMode) { bool tusbStartMassStorageWithFlash(bool fromBootMode) {
LOG_I(TAG, "Starting flash MSC"); LOG_I(TAG, "Starting flash MSC");
if (!ensureDriverInstalled()) {
auto* device = usb_msc_device_get();
if (device == nullptr) {
LOG_E(TAG, "USB MSC device not available");
return false; return false;
} }
startedFromBootMode = fromBootMode;
wl_handle_t handle = tt::getDataPartitionWlHandle(); wl_handle_t handle = tt::getDataPartitionWlHandle();
if (handle == WL_INVALID_HANDLE) { if (handle == WL_INVALID_HANDLE) {
LOG_E(TAG, "WL not mounted for /data"); LOG_E(TAG, "WL not mounted for /data");
device_put(device);
return false; return false;
} }
const tinyusb_msc_spiflash_config_t config_flash = { tt::hal::usb::startedFromBootMode = fromBootMode;
.wl_handle = handle,
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false
}
};
esp_err_t result = tinyusb_msc_storage_init_spiflash(&config_flash); auto result = usb_msc_device_start(device, USB_MSC_DEVICE_SOURCE_FLASH,
if (result != ESP_OK) { &handle, tt::hal::usb::onMountChanged, nullptr);
LOG_E(TAG, "TinyUSB flash init failed: %s", esp_err_to_name(result)); if (result != ERROR_NONE) {
LOG_E(TAG, "TinyUSB flash init failed: %s", error_to_string(result));
} else { } else {
LOG_I(TAG, "TinyUSB flash init success"); LOG_I(TAG, "TinyUSB flash init success");
} }
return result == ESP_OK; device_put(device);
return result == ERROR_NONE;
} }
void tusbStop() { void tusbStop() {
// Actively signal a disconnect to the host before tearing down the peripheral, otherwise auto* device = usb_msc_device_get();
// a subsequent esp_restart() resets the chip too fast for the host to notice the device if (device != nullptr) {
// went away, leaving it stuck showing the old MSC device until the cable is replugged. usb_msc_device_stop(device);
tud_disconnect(); device_put(device);
vTaskDelay(pdMS_TO_TICKS(250)); }
tinyusb_msc_storage_deinit();
#if CONFIG_IDF_TARGET_ESP32P4
usb_wrap_ll_phy_select(&USB_WRAP, 1);
#endif
} }
bool tusbCanStartMassStorageWithFlash() { bool tusbCanStartMassStorageWithFlash() {
@@ -26,7 +26,8 @@ enum BtHidDeviceMode {
BT_HID_DEVICE_MODE_MOUSE, BT_HID_DEVICE_MODE_MOUSE,
/** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */ /** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */
BT_HID_DEVICE_MODE_KEYBOARD_MOUSE, BT_HID_DEVICE_MODE_KEYBOARD_MOUSE,
/** Gamepad (report ID 1, 8 bytes: 2-byte buttons + 6-byte axes). */ /** Gamepad (report ID 1, 8 bytes: 5-byte axes X/Y/Rx/Ry/Z, 1-byte hat/dpad, 2-byte
* buttons[10] padded). */
BT_HID_DEVICE_MODE_GAMEPAD, BT_HID_DEVICE_MODE_GAMEPAD,
}; };
@@ -90,7 +91,7 @@ struct BtHidDeviceApi {
error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len); error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len);
/** /**
* Send a gamepad HID report (8 bytes: buttons[2] + axes[6]). * Send a gamepad HID report (8 bytes: axes[5] + hat[1] + buttons[2]).
* @param[in] device the HID device child device * @param[in] device the HID device child device
* @param[in] report pointer to the 8-byte gamepad report * @param[in] report pointer to the 8-byte gamepad report
* @param[in] len number of bytes (up to 8) * @param[in] len number of bytes (up to 8)
@@ -43,6 +43,20 @@ struct KeyboardKeyData {
* separately. Drivers whose hardware cannot report Alt leave this false. * separately. Drivers whose hardware cannot report Alt leave this false.
*/ */
bool alt; bool alt;
/**
* @brief Standard USB HID keyboard usage code for this key (USB HID Usage Tables, page 0x07),
* or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers
* need). Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so
* consumers that want to mirror physical key presses as real HID reports (e.g. USB HID output)
* don't have to reverse-engineer one out of `key`'s LVGL/ASCII encoding - which is lossy for
* keys with no ASCII/LVGL representation at all, e.g. F1-F12.
*/
uint8_t hid_keycode;
/**
* @brief HID modifier bitmask (report byte 0: bit0=LeftCtrl, bit1=LeftShift, bit2=LeftAlt,
* bit3=LeftGui, bit4-7=Right variants) matching hid_keycode, or 0 if hid_keycode is 0.
*/
uint8_t hid_modifier;
}; };
/** /**
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
struct UsbInterfaceContribution;
// ---- USB device-mode CDC-ACM addon ----
/**
* USB CDC-ACM addon API (present a serial console over USB device mode). Unlike MSC/HID/MIDI,
* CDC is not a primary USB device class and does not go through
* usb_device_controller_claim()/release() - it's a devicetree-presence addon the USB device
* controller composites into whichever primary class is active (or stands alone with none
* active), toggled per-board via the usbdevicecdc0 devicetree child's status. This API is
* exposed by that child device and called by the USB device controller itself, not by primary
* class drivers.
*/
struct UsbCdcDeviceApi {
/**
* @param[in] device the CDC child device
* @return true if this board's usbdevicecdc0 child is present and enabled (status != "disabled")
*/
bool (*is_present)(struct Device* device);
/**
* Build this CDC instance's descriptor contribution, requesting interface/endpoint numbers
* from the controller via usb_device_controller_allocate_interfaces(). Called by the USB
* device controller itself during claim(), after the primary class's own contribution has
* already been allocated.
*
* `interface_string_index` is the string-descriptor index CDC's own interface string will
* land at once the controller appends it to the primary's string table (always
* primary_string_descriptor_count, since CDC's string is always appended last) - CDC bakes
* this index into its TUD_CDC_DESCRIPTOR bytes directly since it can't be patched after the
* fact. `out_interface_string` is CDC's own interface string text for the controller to
* append at that same index.
*
* @param[in] device the CDC child device
* @param[in] controller the USB device controller (to call allocate_interfaces() on)
* @param[in] interface_string_index string index this contribution's descriptor bytes must reference
* @param[out] out_contribution the built fragment
* @param[out] out_interface_string CDC's interface string text, to append at interface_string_index
* @retval ERROR_NONE on success
*/
error_t (*build_contribution)(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string);
/**
* Install the CDC ACM interface and the log-mirroring vprintf hook. Called by the USB device
* controller after claim() successfully installs the composite descriptor.
* @param[in] device the CDC child device
* @retval ERROR_NONE on success
*/
error_t (*start_console)(struct Device* device);
/**
* Uninstall the CDC ACM interface and restore the previous vprintf hook. Called by the USB
* device controller before release() uninstalls the TinyUSB driver.
* @param[in] device the CDC child device
* @retval ERROR_NONE on success
*/
error_t (*stop_console)(struct Device* device);
};
extern const struct DeviceType USB_CDC_DEVICE_TYPE;
/**
* Find the first started USB CDC device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_cdc_device_get(void);
bool usb_cdc_device_is_present(struct Device* device);
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string);
error_t usb_cdc_device_start_console(struct Device* device);
error_t usb_cdc_device_stop_console(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB device-mode (peripheral) controller ----
/**
* Which USB device-mode class currently owns the single TinyUSB device-mode slot.
* Only one class may be active at a time - see usb_device_controller_claim(). CDC is not part of
* this enum: it's an addon that composites into whichever primary class is active (or stands
* alone with none active) rather than a primary itself - see usb_cdc_device.h.
*/
enum UsbDeviceClass {
USB_DEVICE_CLASS_NONE,
USB_DEVICE_CLASS_MSC,
USB_DEVICE_CLASS_HID_KEYBOARD,
USB_DEVICE_CLASS_MIDI,
};
/**
* A block of raw USB interface-descriptor bytes contributed by one class (MSC/HID/MIDI/CDC) to
* the composite configuration descriptor the controller assembles at claim() time.
*
* Interface and endpoint numbers inside the descriptor bytes must already be renumbered by the
* contributor to the base values the controller handed back via usb_device_controller_allocate_interfaces()
* - the controller does not parse or patch the bytes, it only concatenates them behind the
* config-descriptor header it writes itself.
*
* `hs_descriptor_bytes`/`hs_descriptor_bytes_len` are optional (leave both NULL/0 if this
* contributor's bytes don't differ between full-speed and high-speed, e.g. HID/MIDI/CDC's
* endpoint sizes are already speed-independent for their traffic) - only used when
* TUD_OPT_HIGH_SPEED is set, mirroring the fs_/hs_configuration_descriptor split
* tinyusb_config_t itself has (see MSC, whose bulk endpoint max-packet-size legitimately differs:
* 64 bytes FS vs 512 bytes HS).
*/
struct UsbInterfaceContribution {
const uint8_t* descriptor_bytes; // one or more TUD_*_DESCRIPTOR blocks, back to back (full-speed)
size_t descriptor_bytes_len;
const uint8_t* hs_descriptor_bytes; // optional high-speed variant; NULL to reuse descriptor_bytes
size_t hs_descriptor_bytes_len; // must equal descriptor_bytes_len (same interface/endpoint layout, only sizes differ)
uint8_t interface_count; // interfaces consumed (MSC/HID/CDC=1, MIDI=2)
uint8_t in_endpoint_count; // IN endpoints consumed (excluding EP0)
uint8_t out_endpoint_count; // OUT endpoints consumed
};
/**
* Interface/endpoint numbers assigned to one contributor by the controller before it built its
* descriptor bytes. Endpoint numbers are direction-local (IN and OUT each number from 1), as is
* standard for USB - usb_device_controller_allocate_interfaces() hands out the next free *pair*
* per direction, not a single shared counter.
*/
struct UsbInterfaceAllocation {
uint8_t first_interface_number;
uint8_t first_in_endpoint; // e.g. 0x81, 0x82, ... or 0 if in_endpoint_count was 0
uint8_t first_out_endpoint; // e.g. 0x01, 0x02, ... or 0 if out_endpoint_count was 0
};
/**
* A fully-described primary class descriptor plus device/string descriptor metadata, submitted
* to claim(). CDC (if enabled) is appended by the controller itself - primary contributors never
* see or reference CDC's interface numbers.
*/
struct UsbDeviceClaimConfig {
// Actually a `tusb_desc_device_t*` (TinyUSB's device descriptor struct) - void* here so this
// kernel header doesn't need to depend on TinyUSB's own headers; the controller and every
// contributor already build against TinyUSB directly and cast accordingly. Mutable: the
// controller patches the class triad (bDeviceClass/SubClass/Protocol) in place at claim()
// time depending on whether CDC is composited in.
void* device_descriptor;
const char* const* string_descriptor;
size_t string_descriptor_count;
struct UsbInterfaceContribution primary; // MSC or HID or MIDI's contribution
};
/**
* Shared owner of the TinyUSB device-mode (peripheral) stack. Exactly one primary USB device
* class (mass storage, HID, MIDI) may be installed at a time - callers claim the slot before use
* and release it when done. CDC is a separate, orthogonal addon (see usb_cdc_device.h) that the
* controller composites into whichever primary is active, independent of the claim/release cycle.
* This exists so multiple independent drivers (MSC, HID, MIDI, CDC) can share the single
* underlying `tinyusb_driver_install()` call, composite descriptor assembly, and any
* board-specific PHY routing without needing to know about each other.
*/
struct UsbDeviceControllerApi {
/**
* Starts a new interface/endpoint allocation session, resetting the numbering counters
* allocate_interfaces() hands out. Call once, before building any descriptor bytes, as the
* first step of a claim() attempt (i.e. before the primary class's own allocate_interfaces()
* call) - claim() itself calls this again internally for CDC's allocation, so primary
* contributors only need to call it for their own single call.
*
* There is no explicit abort/cancel: a session abandoned after begin_claim() (e.g. a
* contributor's own allocate_interfaces() call fails and it returns early without calling
* claim()) is simply reset by the next begin_claim() call, which unconditionally reinitializes
* the allocation state regardless of whether the previous session ever finished.
*
* @param[in] device the USB device controller device
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*begin_claim)(struct Device* device);
/**
* Ask the controller for the next free interface number and endpoint pair, before building
* descriptor bytes. Call once per contributor (primary class, and CDC internally) per
* begin_claim() session, in the order the resulting descriptor should list interfaces:
* primary class first, then CDC (the controller enforces this order internally for CDC;
* primary contributors just call this once for themselves, after begin_claim()).
* @param[in] device the USB device controller device
* @param[in] interface_count number of interfaces this contributor needs
* @param[in] in_endpoint_count number of IN endpoints needed (0 if none)
* @param[in] out_endpoint_count number of OUT endpoints needed (0 if none)
* @param[out] out_allocation the assigned numbers
* @retval ERROR_INVALID_STATE if called without a preceding begin_claim()
* @retval ERROR_NONE on success
*/
error_t (*allocate_interfaces)(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation);
/**
* Claim the USB device-mode slot for the given primary class and install the composite
* descriptor (primary + CDC, if the board's usbdevicecdc0 child is enabled). Must be called
* after begin_claim() and the primary's own allocate_interfaces() call, using the same
* device-mode session (no other claim()/begin_claim() calls in between).
* @param[in] device the USB device controller device
* @param[in] usb_class the class to claim the slot for
* @param[in] config the primary class's descriptor contribution and metadata
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*claim)(struct Device* device, enum UsbDeviceClass usb_class,
const struct UsbDeviceClaimConfig* config);
/**
* Release the USB device-mode slot. Stops the CDC console (if it was composited in),
* disconnects from the host, uninstalls the TinyUSB driver, and restores any board-specific
* PHY routing. Only the current holder may release.
* @param[in] device the USB device controller device
* @param[in] usb_class the class releasing the slot; must match the current holder
* @retval ERROR_INVALID_STATE if usb_class does not hold the slot
* @retval ERROR_NONE on success
*/
error_t (*release)(struct Device* device, enum UsbDeviceClass usb_class);
/**
* @param[in] device the USB device controller device
* @return the class currently holding the slot, or USB_DEVICE_CLASS_NONE if free
*/
enum UsbDeviceClass (*get_active_class)(struct Device* device);
/**
* @param[in] device the USB device controller device
* @return true if a usbdevicecdc0 child device is present and enabled on this board
*/
bool (*is_cdc_enabled)(struct Device* device);
};
extern const struct DeviceType USB_DEVICE_CONTROLLER_TYPE;
/**
* Find the first started USB device controller and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_device_controller_get(void);
error_t usb_device_controller_begin_claim(struct Device* device);
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation);
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class,
const struct UsbDeviceClaimConfig* config);
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class);
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device);
bool usb_device_controller_is_cdc_enabled(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB HID device mode ----
/**
* Selects the HID report descriptor installed when this device operates as a USB HID
* peripheral. Mirrors BtHidDeviceMode's shape (bluetooth_hid_device.h) so callers that want to
* support both transports use the same mode taxonomy and send_* call shape.
*/
enum UsbHidDeviceMode {
/** Keyboard (report ID 1, boot-protocol-compatible 8 bytes) + Consumer (report ID 2, 2 bytes). */
USB_HID_DEVICE_MODE_KEYBOARD,
/** Mouse only (report ID 1, 4 bytes). */
USB_HID_DEVICE_MODE_MOUSE,
/** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */
USB_HID_DEVICE_MODE_KEYBOARD_MOUSE,
/** Gamepad (report ID 1, 8 bytes: 5-byte axes, 1-byte hat/dpad, 2-byte buttons[10] padded). */
USB_HID_DEVICE_MODE_GAMEPAD,
};
/**
* USB HID device profile API (present this device as a USB HID peripheral to a host).
* This API is exposed by a child device of the USB device controller.
*/
struct UsbHidDeviceApi {
/**
* Claim the USB device-mode slot and start advertising as a USB HID device with the given
* mode.
* @param[in] device the HID device child device
* @param[in] mode the HID device mode (keyboard, mouse, keyboard+mouse, gamepad)
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device, enum UsbHidDeviceMode mode);
/**
* Stop presenting as a USB HID device and release the USB device-mode slot.
* @param[in] device the HID device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* Override the USB product name string reported to the host (iProduct descriptor). Only
* takes effect on the next start() - the descriptor is fixed for the lifetime of a claimed
* session, matching bluetooth_set_device_name()'s pattern of being set before starting
* advertising. If never called, each mode falls back to its own default name (e.g.
* "Tactility Keyboard", "Tactility Mouse").
* @param[in] device the HID device child device
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
* this call returns)
* @return ERROR_NONE on success
*/
error_t (*set_name)(struct Device* device, const char* name);
/**
* Send a keyboard HID report (report ID 1: modifier, reserved, keycodes[6] - 8 bytes after
* the ID byte). Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 8-byte keyboard report (modifier, reserved, keycode[6])
* @param[in] len number of bytes (up to 8)
* @return ERROR_NONE on success
*/
error_t (*send_keyboard)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a consumer control HID report (report ID 2: 16-bit usage code, little-endian).
* Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 2-byte consumer report
* @param[in] len number of bytes (up to 2)
* @return ERROR_NONE on success
*/
error_t (*send_consumer)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a mouse HID report (buttons, X, Y, wheel - 4 bytes). Report ID 1 in
* USB_HID_DEVICE_MODE_MOUSE, report ID 3 in _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 4-byte mouse report
* @param[in] len number of bytes (up to 4)
* @return ERROR_NONE on success
*/
error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a gamepad HID report (report ID 1: axes[5] (X,Y,Rx,Ry,Z), hat/dpad[1] (low nibble),
* buttons[2] (10 buttons + 6 padding bits) - 8 bytes). Only valid in
* USB_HID_DEVICE_MODE_GAMEPAD. See hid_report_map_gamepad in hid_report_descriptors.cpp for
* the full wire layout.
* @param[in] device the HID device child device
* @param[in] report pointer to the 8-byte gamepad report
* @param[in] len number of bytes (up to 8)
* @return ERROR_NONE on success
*/
error_t (*send_gamepad)(struct Device* device, const uint8_t* report, size_t len);
/**
* @param[in] device the HID device child device
* @return true when a USB host has mounted the device and the HID interface is ready
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_HID_DEVICE_TYPE;
/**
* Find the first started USB HID device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_hid_device_get(void);
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode);
error_t usb_hid_device_stop(struct Device* device);
error_t usb_hid_device_set_name(struct Device* device, const char* name);
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len);
bool usb_hid_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB MIDI device mode ----
/**
* USB MIDI device profile API (present this device as a USB MIDI peripheral to a host).
* This API is exposed by a child device of the USB device controller. Mirrors
* bluetooth_midi.h's shape (no mode enum - unlike HID, MIDI has exactly one device profile) so
* the two transports stay symmetric for callers that want to support both.
*/
struct UsbMidiDeviceApi {
/**
* Claim the USB device-mode slot and start advertising as a USB MIDI device.
* @param[in] device the MIDI device child device
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device);
/**
* Stop presenting as a USB MIDI device and release the USB device-mode slot.
* @param[in] device the MIDI device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* Override the USB product name string reported to the host (iProduct descriptor). Only
* takes effect on the next start() - matches bluetooth_set_device_name()'s pattern of being
* set before starting advertising. If never called, falls back to "Tactility MIDI Device".
* @param[in] device the MIDI device child device
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
* this call returns)
* @return ERROR_NONE on success
*/
error_t (*set_name)(struct Device* device, const char* name);
/**
* Send raw MIDI message bytes over the USB MIDI connection.
* @param[in] device the MIDI device child device
* @param[in] msg the raw MIDI bytes
* @param[in] len the number of bytes
* @return ERROR_NONE on success
*/
error_t (*send)(struct Device* device, const uint8_t* msg, size_t len);
/**
* @param[in] device the MIDI device child device
* @return true when a USB host has mounted the device and the MIDI interface is ready
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_MIDI_DEVICE_TYPE;
/**
* Find the first started USB MIDI device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_midi_device_get(void);
error_t usb_midi_device_start(struct Device* device);
error_t usb_midi_device_stop(struct Device* device);
error_t usb_midi_device_set_name(struct Device* device, const char* name);
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len);
bool usb_midi_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB mass-storage device mode ----
/** Which backing storage is exposed to the USB host as a mass-storage volume. */
enum UsbMscDeviceSource {
USB_MSC_DEVICE_SOURCE_SDMMC,
USB_MSC_DEVICE_SOURCE_FLASH,
};
/** Fired when the exposed volume's mount state changes (host mounted/unmounted it). */
typedef void (*UsbMscDeviceMountChangedCallback)(bool mounted, void* context);
/**
* USB mass-storage device profile API (present the board's SD card or internal flash as a USB
* mass-storage device to a host).
*/
struct UsbMscDeviceApi {
/**
* Claim the USB device-mode slot and expose the given storage source as a USB mass-storage
* volume.
* @warning the caller must ensure the backing storage is unmounted/quiesced from local use
* before calling this - a source exposed to a USB host while still locally mounted
* risks filesystem corruption from two concurrent writers. Tactility's own flash-MSC
* path (see UsbTusb.cpp / UsbSettingsApp) enforces this via a dedicated reboot-into-
* MSC boot flow rather than unmounting live, so local access never overlaps with USB
* exposure; local availability is only restored by rebooting back to normal OS.
* @param[in] device the MSC device child device
* @param[in] source which backing storage to expose
* @param[in] source_handle the backing storage handle: a `sdmmc_card_t*` when source is
* USB_MSC_DEVICE_SOURCE_SDMMC, or a pointer to a `wl_handle_t` (i.e. `wl_handle_t*`)
* when source is USB_MSC_DEVICE_SOURCE_FLASH - passed by address, not cast through
* `void*` by value, since `wl_handle_t` is a plain integer type where 0 is a valid
* handle and would collide with the nullptr/"no handle" check otherwise. The caller
* resolves this - platform-esp32 has no business knowing which wear-levelling
* partition Tactility mounted as /data.
* @param[in] mount_changed_cb optional callback fired on host mount/unmount, nullable
* @param[in] context passed back to mount_changed_cb, nullable
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_INVALID_ARGUMENT if source_handle is invalid for the given source
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
/**
* Stop presenting as a USB mass-storage device and release the USB device-mode slot.
* @param[in] device the MSC device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* @param[in] device the MSC device child device
* @return true when a USB host currently has the volume mounted
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_MSC_DEVICE_TYPE;
/**
* Find the first started USB MSC device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_msc_device_get(void);
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
error_t usb_msc_device_stop(struct Device* device);
bool usb_msc_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
+4 -2
View File
@@ -10,10 +10,12 @@ extern "C" {
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
const auto* driver = device_get_driver(device); const auto* driver = device_get_driver(device);
// Default the modifier fields here rather than in each driver: only drivers whose hardware can // Default the modifier/HID fields here rather than in each driver: only drivers whose hardware
// report modifiers set them, and the rest would otherwise leave whatever the caller's stack held. // can report them set them, and the rest would otherwise leave whatever the caller's stack held.
data->ctrl = false; data->ctrl = false;
data->alt = false; data->alt = false;
data->hid_keycode = 0;
data->hid_modifier = 0;
return KEYBOARD_DRIVER_API(driver)->read_key(device, data); return KEYBOARD_DRIVER_API(driver)->read_key(device, data);
} }
@@ -0,0 +1,38 @@
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_CDC_DEVICE_API(device) ((const struct UsbCdcDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_CDC_DEVICE_TYPE = {
.name = "usb-cdc-device",
};
struct Device* usb_cdc_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_CDC_DEVICE_TYPE, &found);
return found;
}
bool usb_cdc_device_is_present(struct Device* device) {
return USB_CDC_DEVICE_API(device)->is_present(device);
}
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string) {
return USB_CDC_DEVICE_API(device)->build_contribution(device, controller, interface_string_index, out_contribution, out_interface_string);
}
error_t usb_cdc_device_start_console(struct Device* device) {
return USB_CDC_DEVICE_API(device)->start_console(device);
}
error_t usb_cdc_device_stop_console(struct Device* device) {
return USB_CDC_DEVICE_API(device)->stop_console(device);
}
} // extern "C"
@@ -0,0 +1,45 @@
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_DEVICE_CONTROLLER_API(device) ((const struct UsbDeviceControllerApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_DEVICE_CONTROLLER_TYPE = {
.name = "usb-device-controller",
};
struct Device* usb_device_controller_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_DEVICE_CONTROLLER_TYPE, &found);
return found;
}
error_t usb_device_controller_begin_claim(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->begin_claim(device);
}
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation) {
return USB_DEVICE_CONTROLLER_API(device)->allocate_interfaces(device, interface_count, in_endpoint_count, out_endpoint_count, out_allocation);
}
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class, const struct UsbDeviceClaimConfig* config) {
return USB_DEVICE_CONTROLLER_API(device)->claim(device, usb_class, config);
}
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class) {
return USB_DEVICE_CONTROLLER_API(device)->release(device, usb_class);
}
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->get_active_class(device);
}
bool usb_device_controller_is_cdc_enabled(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->is_cdc_enabled(device);
}
} // extern "C"
@@ -0,0 +1,51 @@
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_HID_DEVICE_API(device) ((const struct UsbHidDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_HID_DEVICE_TYPE = {
.name = "usb-hid-device",
};
struct Device* usb_hid_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_HID_DEVICE_TYPE, &found);
return found;
}
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode) {
return USB_HID_DEVICE_API(device)->start(device, mode);
}
error_t usb_hid_device_stop(struct Device* device) {
return USB_HID_DEVICE_API(device)->stop(device);
}
error_t usb_hid_device_set_name(struct Device* device, const char* name) {
return USB_HID_DEVICE_API(device)->set_name(device, name);
}
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_keyboard(device, report, len);
}
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_consumer(device, report, len);
}
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_mouse(device, report, len);
}
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_gamepad(device, report, len);
}
bool usb_hid_device_is_connected(struct Device* device) {
return USB_HID_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
@@ -0,0 +1,39 @@
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_MIDI_DEVICE_API(device) ((const struct UsbMidiDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_MIDI_DEVICE_TYPE = {
.name = "usb-midi-device",
};
struct Device* usb_midi_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_MIDI_DEVICE_TYPE, &found);
return found;
}
error_t usb_midi_device_start(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->start(device);
}
error_t usb_midi_device_stop(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->stop(device);
}
error_t usb_midi_device_set_name(struct Device* device, const char* name) {
return USB_MIDI_DEVICE_API(device)->set_name(device, name);
}
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len) {
return USB_MIDI_DEVICE_API(device)->send(device, msg, len);
}
bool usb_midi_device_is_connected(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
@@ -0,0 +1,32 @@
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_MSC_DEVICE_API(device) ((const struct UsbMscDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_MSC_DEVICE_TYPE = {
.name = "usb-msc-device",
};
struct Device* usb_msc_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_MSC_DEVICE_TYPE, &found);
return found;
}
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context) {
return USB_MSC_DEVICE_API(device)->start(device, source, source_handle, mount_changed_cb, context);
}
error_t usb_msc_device_stop(struct Device* device) {
return USB_MSC_DEVICE_API(device)->stop(device);
}
bool usb_msc_device_is_connected(struct Device* device) {
return USB_MSC_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
+38
View File
@@ -33,9 +33,13 @@
#include <tactility/drivers/spi_controller.h> #include <tactility/drivers/spi_controller.h>
#include <tactility/drivers/trackball.h> #include <tactility/drivers/trackball.h>
#include <tactility/drivers/uart_controller.h> #include <tactility/drivers/uart_controller.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/drivers/usb_host_hid.h> #include <tactility/drivers/usb_host_hid.h>
#include <tactility/drivers/usb_host_midi.h> #include <tactility/drivers/usb_host_midi.h>
#include <tactility/drivers/usb_host_msc.h> #include <tactility/drivers/usb_host_msc.h>
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/drivers/wifi.h> #include <tactility/drivers/wifi.h>
#include <tactility/error.h> #include <tactility/error.h>
#include <tactility/filesystem/file_mutex.h> #include <tactility/filesystem/file_mutex.h>
@@ -422,6 +426,40 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
// drivers/usb_host_msc // drivers/usb_host_msc
DEFINE_MODULE_SYMBOL(usb_msc_eject), DEFINE_MODULE_SYMBOL(usb_msc_eject),
DEFINE_MODULE_SYMBOL(USB_HOST_MSC_TYPE), DEFINE_MODULE_SYMBOL(USB_HOST_MSC_TYPE),
// drivers/usb_device_controller
DEFINE_MODULE_SYMBOL(usb_device_controller_get),
DEFINE_MODULE_SYMBOL(usb_device_controller_begin_claim),
DEFINE_MODULE_SYMBOL(usb_device_controller_allocate_interfaces),
DEFINE_MODULE_SYMBOL(usb_device_controller_claim),
DEFINE_MODULE_SYMBOL(usb_device_controller_release),
DEFINE_MODULE_SYMBOL(usb_device_controller_get_active_class),
DEFINE_MODULE_SYMBOL(usb_device_controller_is_cdc_enabled),
DEFINE_MODULE_SYMBOL(USB_DEVICE_CONTROLLER_TYPE),
// drivers/usb_hid_device
DEFINE_MODULE_SYMBOL(usb_hid_device_get),
DEFINE_MODULE_SYMBOL(usb_hid_device_start),
DEFINE_MODULE_SYMBOL(usb_hid_device_stop),
DEFINE_MODULE_SYMBOL(usb_hid_device_set_name),
DEFINE_MODULE_SYMBOL(usb_hid_device_send_keyboard),
DEFINE_MODULE_SYMBOL(usb_hid_device_send_consumer),
DEFINE_MODULE_SYMBOL(usb_hid_device_send_mouse),
DEFINE_MODULE_SYMBOL(usb_hid_device_send_gamepad),
DEFINE_MODULE_SYMBOL(usb_hid_device_is_connected),
DEFINE_MODULE_SYMBOL(USB_HID_DEVICE_TYPE),
// drivers/usb_msc_device
DEFINE_MODULE_SYMBOL(usb_msc_device_get),
DEFINE_MODULE_SYMBOL(usb_msc_device_start),
DEFINE_MODULE_SYMBOL(usb_msc_device_stop),
DEFINE_MODULE_SYMBOL(usb_msc_device_is_connected),
DEFINE_MODULE_SYMBOL(USB_MSC_DEVICE_TYPE),
// drivers/usb_midi_device
DEFINE_MODULE_SYMBOL(usb_midi_device_get),
DEFINE_MODULE_SYMBOL(usb_midi_device_start),
DEFINE_MODULE_SYMBOL(usb_midi_device_stop),
DEFINE_MODULE_SYMBOL(usb_midi_device_set_name),
DEFINE_MODULE_SYMBOL(usb_midi_device_send),
DEFINE_MODULE_SYMBOL(usb_midi_device_is_connected),
DEFINE_MODULE_SYMBOL(USB_MIDI_DEVICE_TYPE),
// concurrent/dispatcher // concurrent/dispatcher
DEFINE_MODULE_SYMBOL(dispatcher_alloc), DEFINE_MODULE_SYMBOL(dispatcher_alloc),
DEFINE_MODULE_SYMBOL(dispatcher_free), DEFINE_MODULE_SYMBOL(dispatcher_free),
+25 -2
View File
@@ -341,11 +341,34 @@ def write_touch_calibration_variables(output_file, device_properties: dict):
def write_usb_variables(output_file, device_properties: dict): def write_usb_variables(output_file, device_properties: dict):
has_tiny_usb = get_boolean_property_or_false(device_properties, "hardware.tinyUsb") has_tiny_usb_msc = get_boolean_property_or_false(device_properties, "hardware.tinyUsbMsc")
if has_tiny_usb: has_tiny_usb_hid = get_boolean_property_or_false(device_properties, "hardware.tinyUsbHid")
has_tiny_usb_midi = get_boolean_property_or_false(device_properties, "hardware.tinyUsbMidi")
has_tiny_usb_cdc = get_boolean_property_or_false(device_properties, "hardware.tinyUsbCdc")
if has_tiny_usb_msc or has_tiny_usb_hid or has_tiny_usb_midi or has_tiny_usb_cdc:
output_file.write("# TinyUSB\n") output_file.write("# TinyUSB\n")
if has_tiny_usb_msc:
output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\n") output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\n")
output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n") output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n")
if has_tiny_usb_hid:
# TinyUSB HID is gated by an interface count, not a plain enable flag - 1 interface
# is enough for the boot-protocol keyboard the USB HID device driver installs.
output_file.write("CONFIG_TINYUSB_HID_COUNT=1\n")
if has_tiny_usb_midi:
# Same "count > 0 enables it" shape as HID. MIDI is its own claim()-able
# USB_DEVICE_CLASS_MIDI (mutually exclusive with HID/MSC at runtime, like MSC
# already is) - a board acting as a MIDI controller has no reason to also be a
# keyboard, and vice versa.
output_file.write("CONFIG_TINYUSB_MIDI_COUNT=1\n")
if has_tiny_usb_cdc:
# CDC is an independent devicetree-toggleable addon (usbdevicecdc0), not tied to any
# one primary class - the USB device controller composites it into whichever of
# HID/MIDI is active (or none) so a serial console stays reachable even while the
# port is also presenting as a HID/MIDI device. MSC is deliberately excluded (a MSC
# device should look like plain mass storage, no console). See
# esp32_usb_cdc_device.cpp / esp32_usb_device_controller.cpp. This flag only needs to
# be true if the board's .dts actually has an enabled usbdevicecdc0 node.
output_file.write("CONFIG_TINYUSB_CDC_ENABLED=y\n")
idf_target = get_property_or_exit(device_properties, "hardware.target").lower() idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
if idf_target == "esp32p4": if idf_target == "esp32p4":
# P4 has two USB-DWC controllers (HS/UTMI and FS/FSLS). esp_tinyusb defaults to # P4 has two USB-DWC controllers (HS/UTMI and FS/FSLS). esp_tinyusb defaults to