diff --git a/Devices/elecrow-crowpanel-advance-28/device.properties b/Devices/elecrow-crowpanel-advance-28/device.properties index 9d2fd7949..c17ec1404 100644 --- a/Devices/elecrow-crowpanel-advance-28/device.properties +++ b/Devices/elecrow-crowpanel-advance-28/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts b/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts index fe6881c60..78db1398e 100644 --- a/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts +++ b/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -105,4 +106,12 @@ pin-tx = <&gpio0 17 GPIO_FLAG_NONE>; pin-rx = <&gpio0 18 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/elecrow-crowpanel-advance-35/device.properties b/Devices/elecrow-crowpanel-advance-35/device.properties index 833744397..29851d8d2 100644 --- a/Devices/elecrow-crowpanel-advance-35/device.properties +++ b/Devices/elecrow-crowpanel-advance-35/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts b/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts index 173c9cd40..a687b01c6 100644 --- a/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts +++ b/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -110,4 +111,12 @@ pin-tx = <&gpio0 17 GPIO_FLAG_NONE>; pin-rx = <&gpio0 18 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/elecrow-crowpanel-advance-50/device.properties b/Devices/elecrow-crowpanel-advance-50/device.properties index 9fa787d6a..7b0e0246a 100644 --- a/Devices/elecrow-crowpanel-advance-50/device.properties +++ b/Devices/elecrow-crowpanel-advance-50/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts b/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts index 734640d04..1a0f93ba8 100644 --- a/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts +++ b/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -123,4 +124,12 @@ pin-tx = <&gpio0 20 GPIO_FLAG_NONE>; pin-rx = <&gpio0 19 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/elecrow-crowpanel-basic-50/device.properties b/Devices/elecrow-crowpanel-basic-50/device.properties index 69633d2ae..3d9425edf 100644 --- a/Devices/elecrow-crowpanel-basic-50/device.properties +++ b/Devices/elecrow-crowpanel-basic-50/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=4MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts b/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts index c9726bb1e..ebb9e0b84 100644 --- a/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts +++ b/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -119,4 +120,12 @@ pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/es3c28p/device.properties b/Devices/es3c28p/device.properties index 1d90476b8..51c42c137 100644 --- a/Devices/es3c28p/device.properties +++ b/Devices/es3c28p/device.properties @@ -9,7 +9,7 @@ hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M hardware.esptoolFlashFreq=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.bluetooth=true display.size=2.8" diff --git a/Devices/es3c28p/es3c28p.dts b/Devices/es3c28p/es3c28p.dts index 1063311be..75d5ac1dc 100644 --- a/Devices/es3c28p/es3c28p.dts +++ b/Devices/es3c28p/es3c28p.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -139,4 +140,12 @@ slot = ; bus-width = <4>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/heltec-wifi-lora-32-v3/device.properties b/Devices/heltec-wifi-lora-32-v3/device.properties index 729c48738..b656de2e2 100644 --- a/Devices/heltec-wifi-lora-32-v3/device.properties +++ b/Devices/heltec-wifi-lora-32-v3/device.properties @@ -8,7 +8,7 @@ apps.autoStartAppId=ApWebServer hardware.target=ESP32S3 hardware.flashSize=8MB hardware.spiRam=false -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts index 73c57b7b3..db8be2bf2 100644 --- a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts +++ b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -113,4 +114,12 @@ compatible = "tactility,button-control"; pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdeck-max/device.properties b/Devices/lilygo-tdeck-max/device.properties index 50604bb16..379418968 100644 --- a/Devices/lilygo-tdeck-max/device.properties +++ b/Devices/lilygo-tdeck-max/device.properties @@ -11,7 +11,7 @@ hardware.flashMode=DIO hardware.spiRam=true hardware.spiRamMode=AUTO hardware.spiRamSpeed=80M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=80M hardware.bluetooth=true diff --git a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts index 3c8ca9586..0be730b59 100644 --- a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts +++ b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -179,4 +180,12 @@ dio2-as-rf-switch; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdeck-plus/device.properties b/Devices/lilygo-tdeck-plus/device.properties index 3aaf6fbba..7ee41b8ae 100644 --- a/Devices/lilygo-tdeck-plus/device.properties +++ b/Devices/lilygo-tdeck-plus/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts index 961b9d851..5a0476c92 100644 --- a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts +++ b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -198,4 +199,12 @@ model = ; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdeck-pro/device.properties b/Devices/lilygo-tdeck-pro/device.properties index 3688af82b..63781264a 100644 --- a/Devices/lilygo-tdeck-pro/device.properties +++ b/Devices/lilygo-tdeck-pro/device.properties @@ -11,7 +11,7 @@ hardware.flashMode=DIO hardware.spiRam=true hardware.spiRamMode=AUTO hardware.spiRamSpeed=80M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=80M hardware.bluetooth=true diff --git a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts index 54dd18d55..dfa466002 100644 --- a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts +++ b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -135,4 +136,12 @@ frequency-khz = <20000>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdeck/device.properties b/Devices/lilygo-tdeck/device.properties index 7f4b9e457..b2400a4fd 100644 --- a/Devices/lilygo-tdeck/device.properties +++ b/Devices/lilygo-tdeck/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index d915f1247..9c4bd34dd 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -192,4 +193,12 @@ i2cPort = ; i2cClockFrequency = <400000>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdisplay-s3/device.properties b/Devices/lilygo-tdisplay-s3/device.properties index 759018c91..6936ecbaf 100644 --- a/Devices/lilygo-tdisplay-s3/device.properties +++ b/Devices/lilygo-tdisplay-s3/device.properties @@ -9,7 +9,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts index 2ec983f1c..2b742a3c2 100644 --- a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts +++ b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -95,4 +96,12 @@ pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; pin-secondary = <&gpio0 14 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tdongle-s3/device.properties b/Devices/lilygo-tdongle-s3/device.properties index f92ed6ebe..4b68fa89c 100644 --- a/Devices/lilygo-tdongle-s3/device.properties +++ b/Devices/lilygo-tdongle-s3/device.properties @@ -8,7 +8,7 @@ apps.autoStartAppId=ApWebServer hardware.target=ESP32S3 hardware.flashSize=16MB hardware.spiRam=false -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts index 6cfc3455e..519bade15 100644 --- a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts +++ b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -95,4 +96,12 @@ pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-thmi/device.properties b/Devices/lilygo-thmi/device.properties index 5d32e381b..46fb227c7 100644 --- a/Devices/lilygo-thmi/device.properties +++ b/Devices/lilygo-thmi/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/lilygo-thmi/lilygo,thmi.dts b/Devices/lilygo-thmi/lilygo,thmi.dts index d724d9a3a..3811c0c6c 100644 --- a/Devices/lilygo-thmi/lilygo,thmi.dts +++ b/Devices/lilygo-thmi/lilygo,thmi.dts @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -131,4 +132,12 @@ bus-width = <1>; pullups; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/lilygo-tlora-pager/device.properties b/Devices/lilygo-tlora-pager/device.properties index 1e363302f..0c820c81b 100644 --- a/Devices/lilygo-tlora-pager/device.properties +++ b/Devices/lilygo-tlora-pager/device.properties @@ -9,7 +9,7 @@ hardware.flashMode=DIO hardware.spiRam=true hardware.spiRamMode=AUTO hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=40M hardware.bluetooth=true diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index a2e55af76..48c571c7c 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -205,4 +206,12 @@ pin-b = <&gpio0 41 GPIO_FLAG_NONE>; pin-enter = <&gpio0 7 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-cardputer-adv/device.properties b/Devices/m5stack-cardputer-adv/device.properties index ab63ce706..ef8018f15 100644 --- a/Devices/m5stack-cardputer-adv/device.properties +++ b/Devices/m5stack-cardputer-adv/device.properties @@ -6,7 +6,7 @@ apps.launcherAppId=Launcher hardware.target=ESP32S3 hardware.flashSize=8MB hardware.spiRam=false -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts index e47a2cd7c..bdfa69ea2 100644 --- a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts +++ b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -148,4 +149,12 @@ pin-data-out = <&gpio0 42 GPIO_FLAG_NONE>; pin-data-in = <&gpio0 46 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-cardputer/device.properties b/Devices/m5stack-cardputer/device.properties index afcb648f0..96ba51432 100644 --- a/Devices/m5stack-cardputer/device.properties +++ b/Devices/m5stack-cardputer/device.properties @@ -6,7 +6,7 @@ apps.launcherAppId=Launcher hardware.target=ESP32S3 hardware.flashSize=8MB hardware.spiRam=false -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/m5stack-cardputer/m5stack,cardputer.dts b/Devices/m5stack-cardputer/m5stack,cardputer.dts index bd802d2f4..8af8b42e4 100644 --- a/Devices/m5stack-cardputer/m5stack,cardputer.dts +++ b/Devices/m5stack-cardputer/m5stack,cardputer.dts @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -164,4 +165,12 @@ i2s = <&i2s0>; channels = <1>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-cores3/device.properties b/Devices/m5stack-cores3/device.properties index 5af2a482d..a06fe84b9 100644 --- a/Devices/m5stack-cores3/device.properties +++ b/Devices/m5stack-cores3/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=QUAD hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/m5stack-cores3/m5stack,cores3.dts b/Devices/m5stack-cores3/m5stack,cores3.dts index bd0ecd002..130a2c41a 100644 --- a/Devices/m5stack-cores3/m5stack,cores3.dts +++ b/Devices/m5stack-cores3/m5stack,cores3.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -205,4 +206,12 @@ backlight = <&display_backlight>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-papers3/device.properties b/Devices/m5stack-papers3/device.properties index 7ca2752e7..edd194126 100644 --- a/Devices/m5stack-papers3/device.properties +++ b/Devices/m5stack-papers3/device.properties @@ -10,7 +10,7 @@ hardware.spiRam=true hardware.spiRamMode=OPI hardware.spiRamSpeed=80M hardware.esptoolFlashFreq=80M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.bluetooth=true storage.userDataLocation=SD diff --git a/Devices/m5stack-papers3/m5stack,papers3.dts b/Devices/m5stack-papers3/m5stack,papers3.dts index dc7b15c18..b2bc3ccec 100644 --- a/Devices/m5stack-papers3/m5stack,papers3.dts +++ b/Devices/m5stack-papers3/m5stack,papers3.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -114,4 +115,12 @@ // correct orientation. rotation = ; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-stackchan/device.properties b/Devices/m5stack-stackchan/device.properties index fa5fcaf99..edd65a81c 100644 --- a/Devices/m5stack-stackchan/device.properties +++ b/Devices/m5stack-stackchan/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=QUAD hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/m5stack-stackchan/m5stack,stackchan.dts b/Devices/m5stack-stackchan/m5stack,stackchan.dts index 9772e8b2b..9cbb0f4f6 100644 --- a/Devices/m5stack-stackchan/m5stack,stackchan.dts +++ b/Devices/m5stack-stackchan/m5stack,stackchan.dts @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -238,4 +239,12 @@ frequency-khz = <20000>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-sticks3/device.properties b/Devices/m5stack-sticks3/device.properties index 057115eca..f4d8df5db 100644 --- a/Devices/m5stack-sticks3/device.properties +++ b/Devices/m5stack-sticks3/device.properties @@ -9,7 +9,7 @@ hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=80M hardware.esptoolFlashFreq=80M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.bluetooth=true storage.userDataLocation=Internal diff --git a/Devices/m5stack-sticks3/m5stack,sticks3.dts b/Devices/m5stack-sticks3/m5stack,sticks3.dts index cbc4fa30f..0011b7e3e 100644 --- a/Devices/m5stack-sticks3/m5stack,sticks3.dts +++ b/Devices/m5stack-sticks3/m5stack,sticks3.dts @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -131,4 +132,12 @@ pin-tx = <&gpio0 9 GPIO_FLAG_NONE>; pin-rx = <&gpio0 10 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp index 3de918af2..82e326ce3 100644 --- a/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp +++ b/Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp @@ -96,10 +96,10 @@ static constexpr HidMapping KEY_MATRIX_HID_BASE[70] = { }; static constexpr HidMapping KEY_MATRIX_HID_SYM[70] = { - // Row 0: identical to base - {0x29, 0x00}, {0x1E, 0x00}, {0x1F, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, - {0x23, 0x00}, {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x2D, 0x00}, - {0x2E, 0x02}, {0x4C, 0x00}, + // Row 0: Esc F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 Del + {0x29, 0x00}, {0x3A, 0x00}, {0x3B, 0x00}, {0x3C, 0x00}, {0x3D, 0x00}, {0x3E, 0x00}, + {0x3F, 0x00}, {0x40, 0x00}, {0x41, 0x00}, {0x42, 0x00}, {0x43, 0x00}, {0x44, 0x00}, + {0x45, 0x00}, {0x4C, 0x00}, // Row 1: Sym deltas: ` → ~, ! → ?, * → /, ( → <, ) → >, [ → {, ] → }, backslash → | {0x35, 0x02}, {0x38, 0x02}, {0x1F, 0x02}, {0x20, 0x02}, {0x21, 0x02}, {0x22, 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; } + // 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 if (keycode >= 0x04U && keycode <= 0x1DU) { uint32_t c = static_cast('a' + (keycode - 0x04U)); @@ -191,6 +195,8 @@ struct Tab5KeyEvent { uint32_t key; bool ctrl; bool alt; + uint8_t hid_keycode; + uint8_t hid_modifier; }; struct Tab5KeyboardInternal { @@ -368,36 +374,40 @@ static void drain_events(Device* device, Tab5KeyboardInternal* internal) { ? KEY_MATRIX_HID_SYM[row * 14U + col] : KEY_MATRIX_HID_BASE[row * 14U + col]; if (m.keycode != 0U) { - const uint8_t modifier = static_cast(m.modifier | (aa_active ? 0x02U : 0U)); + uint8_t modifier = static_cast(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); - if (lv_key != 0U) { - if (pressed) { - // A real key was pressed — this hold is a chord, not a tap - internal->aa_tapped = false; - // Note: ESC used to stop the foreground app directly (tt::app::stop()) in - // the deprecated-HAL version - that's an app-layer concern the driver has - // 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 - // navigation the same way a dedicated ESC key on any other keyboard would). - const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held }; - xQueueSend(internal->queue, &event, 0); - // Arm software repeat tracking by row/col to survive modifier changes - const uint32_t now = now_ms(); - internal->repeat_event = event; - internal->repeat_row = row; - internal->repeat_col = col; - internal->repeat_start_ms = now; - internal->repeat_last_ms = 0; - // Consume sticky Aa after one keypress - if (internal->aa_sticky) { - internal->aa_sticky = false; - internal->aa_held = false; - update_leds(device, internal); - } - } else if (row == internal->repeat_row && col == internal->repeat_col) { - // Match release by position, not translated value — survives sticky Aa clear - internal->repeat_event.key = 0; + // 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) { + // A real key was pressed — this hold is a chord, not a tap + internal->aa_tapped = false; + // Note: ESC used to stop the foreground app directly (tt::app::stop()) in + // the deprecated-HAL version - that's an app-layer concern the driver has + // 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 + // navigation the same way a dedicated ESC key on any other keyboard would). + const Tab5KeyEvent event = { lv_key, internal->ctrl_held, internal->alt_held, + m.keycode, modifier }; + xQueueSend(internal->queue, &event, 0); + // Arm software repeat tracking by row/col to survive modifier changes + const uint32_t now = now_ms(); + internal->repeat_event = event; + internal->repeat_row = row; + internal->repeat_col = col; + internal->repeat_start_ms = now; + internal->repeat_last_ms = 0; + // Consume sticky Aa after one keypress + if (internal->aa_sticky) { + internal->aa_sticky = false; + internal->aa_held = false; + update_leds(device, internal); } + } else if (row == internal->repeat_row && col == internal->repeat_col) { + // Match release by position, not translated value — survives sticky Aa clear + internal->repeat_event.key = 0; } } } @@ -560,12 +570,16 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) { data->continue_reading = uxQueueMessagesWaiting(internal->queue) > 0; data->ctrl = event.ctrl; data->alt = event.alt; + data->hid_keycode = event.hid_keycode; + data->hid_modifier = event.hid_modifier; } else { data->key = 0; data->pressed = false; data->continue_reading = false; data->ctrl = false; data->alt = false; + data->hid_keycode = 0; + data->hid_modifier = 0; } return ERROR_NONE; diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index 8ebaaa74f..946c24842 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -11,7 +11,10 @@ hardware.spiRamSpeed=200M hardware.esptoolFlashFreq=80M hardware.bluetooth=true hardware.usbHostEnabled=true -hardware.tinyUsb=true +hardware.tinyUsbMsc=true +hardware.tinyUsbHid=true +hardware.tinyUsbMidi=true +hardware.tinyUsbCdc=true storage.userDataLocation=SD diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index d84f9e10d..25e9c467f 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -210,4 +211,24 @@ 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"; + }; + }; }; diff --git a/Devices/waveshare-esp32-s3-geek/device.properties b/Devices/waveshare-esp32-s3-geek/device.properties index 19b06a31b..db338a21b 100644 --- a/Devices/waveshare-esp32-s3-geek/device.properties +++ b/Devices/waveshare-esp32-s3-geek/device.properties @@ -10,7 +10,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=QUAD hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts index fb532bea5..b159666c5 100644 --- a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts +++ b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -103,4 +104,12 @@ pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/waveshare-s3-lcd-13/device.properties b/Devices/waveshare-s3-lcd-13/device.properties index ed6889941..5e603fc40 100644 --- a/Devices/waveshare-s3-lcd-13/device.properties +++ b/Devices/waveshare-s3-lcd-13/device.properties @@ -10,7 +10,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts index eadabedee..388d0c20f 100644 --- a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts +++ b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -90,4 +91,12 @@ frequency-khz = <20000>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/waveshare-s3-touch-lcd-128/device.properties b/Devices/waveshare-s3-touch-lcd-128/device.properties index a000dbbae..6badafe83 100644 --- a/Devices/waveshare-s3-touch-lcd-128/device.properties +++ b/Devices/waveshare-s3-touch-lcd-128/device.properties @@ -10,7 +10,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=QUAD hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts index c91134fdc..09e23375a 100644 --- a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts +++ b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -93,4 +94,12 @@ backlight = <&display_backlight>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/waveshare-s3-touch-lcd-147/device.properties b/Devices/waveshare-s3-touch-lcd-147/device.properties index 0cb988252..92f228724 100644 --- a/Devices/waveshare-s3-touch-lcd-147/device.properties +++ b/Devices/waveshare-s3-touch-lcd-147/device.properties @@ -9,7 +9,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts index 891dee234..737a9e478 100644 --- a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts +++ b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -104,4 +105,12 @@ compatible = "tactility,button-control"; pin-primary = <&gpio0 0 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/waveshare-s3-touch-lcd-43/device.properties b/Devices/waveshare-s3-touch-lcd-43/device.properties index cafbaffdf..63b432e04 100644 --- a/Devices/waveshare-s3-touch-lcd-43/device.properties +++ b/Devices/waveshare-s3-touch-lcd-43/device.properties @@ -9,7 +9,7 @@ hardware.flashSize=8MB hardware.spiRam=true hardware.spiRamMode=OCT hardware.spiRamSpeed=120M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=120M hardware.bluetooth=true diff --git a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts index 406b6364f..3bb7edb1b 100644 --- a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts +++ b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -126,4 +127,12 @@ pin-tx = <&gpio0 43 GPIO_FLAG_NONE>; pin-rx = <&gpio0 44 GPIO_FLAG_NONE>; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Devices/wireless-tag-wt32-sc01-plus/device.properties b/Devices/wireless-tag-wt32-sc01-plus/device.properties index 18f88ac7b..5d797a0cd 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/device.properties +++ b/Devices/wireless-tag-wt32-sc01-plus/device.properties @@ -8,7 +8,7 @@ hardware.flashSize=16MB hardware.spiRam=true hardware.spiRamMode=QUAD hardware.spiRamSpeed=80M -hardware.tinyUsb=true +hardware.tinyUsbMsc=true hardware.esptoolFlashFreq=80M hardware.bluetooth=true diff --git a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts index 00298196d..385683d00 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts +++ b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -105,4 +106,12 @@ frequency-khz = <20000>; }; }; + + usbdevice0 { + compatible = "espressif,esp32-usbdevice"; + + usbdevicemsc0 { + compatible = "espressif,esp32-usbdevice-msc"; + }; + }; }; diff --git a/Modules/lvgl-module/include/lvgl/devices/pointer.h b/Modules/lvgl-module/include/lvgl/devices/pointer.h index 17e77ae60..af5f1e01c 100644 --- a/Modules/lvgl-module/include/lvgl/devices/pointer.h +++ b/Modules/lvgl-module/include/lvgl/devices/pointer.h @@ -13,6 +13,16 @@ extern "C" { #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. * @@ -31,7 +41,8 @@ struct LvglPointerCalibration { /** * @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 - * 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). * @@ -44,19 +55,20 @@ struct LvglPointerCalibration { 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. * @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); /** - * @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 * 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 - * data isn't a struct LvglPointerCtx*. + * data isn't this module's own context type. * * @warning Caller must hold the LVGL lock. * @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); /** - * @brief Creates an lv_indev_t bound to the given POINTER_TYPE device and registers a read callback - * that polls the device through its PointerApi. + * @brief Creates up to max_touch_points lv_indev_t instances bound to the given POINTER_TYPE + * 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 * 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] 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_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 */ -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 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 an indev previously created with lvgl_pointer_add(). + * @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. */ void lvgl_pointer_remove(lv_indev_t* indev); diff --git a/Modules/lvgl-module/include/lvgl/widgets/toolbar.h b/Modules/lvgl-module/include/lvgl/widgets/toolbar.h index 043f43a09..503ec6b09 100644 --- a/Modules/lvgl-module/include/lvgl/widgets/toolbar.h +++ b/Modules/lvgl-module/include/lvgl/widgets/toolbar.h @@ -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); +/** + * 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. * @param[in] obj the toolbar instance diff --git a/Modules/lvgl-module/source/devices/devices.cpp b/Modules/lvgl-module/source/devices/devices.cpp index 196cc26c7..364dc8759 100644 --- a/Modules/lvgl-module/source/devices/devices.cpp +++ b/Modules/lvgl-module/source/devices/devices.cpp @@ -97,15 +97,21 @@ void lvgl_devices_attach() { continue; } - lv_indev_t* lvgl_pointer_device; - if (lvgl_pointer_add(kernel_pointer_device, lvgl_display, &lvgl_pointer_device) == ERROR_NONE) { - LOG_I(TAG, "Bound %s to LVGL", kernel_pointer_device->name); + // Each physical touch device gets up to LVGL_POINTER_MAX_SLOTS independent indevs (a + // "pool", see lvgl_pointer_add()) so LVGL can track that many simultaneous touch points - + // 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 - if (display_updates_slowly ) { - lv_indev_set_long_press_time(lvgl_pointer_device, 2000); + if (display_updates_slowly) { + for (uint8_t slot = 0; slot < LVGL_POINTER_MAX_SLOTS; slot++) { + lv_indev_set_long_press_time(lvgl_pointer_slots[slot], 2000); + } } } 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); } } diff --git a/Modules/lvgl-module/source/devices/pointer.cpp b/Modules/lvgl-module/source/devices/pointer.cpp index a8237bcd7..8963b621a 100644 --- a/Modules/lvgl-module/source/devices/pointer.cpp +++ b/Modules/lvgl-module/source/devices/pointer.cpp @@ -2,14 +2,13 @@ #include #include +#include #include -constexpr auto* TAG = "lvgl_pointer"; +#include +#include -struct LvglPointerCtx { - bool calibration_enabled; - struct LvglPointerCalibration calibration; -}; +constexpr auto* TAG = "lvgl_pointer"; // 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); @@ -22,6 +21,61 @@ static lv_indev_t* default_pointer_indev = NULL; // Mirrors Tactility/Source/settings/TouchCalibrationSettings.cpp's isValid(). 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) { return calibration->x_max > calibration->x_min && calibration->y_max > calibration->y_min && @@ -53,91 +107,193 @@ static void lvgl_pointer_calibration_apply( *y = (uint16_t)mapped_y; } -// Reads the touch controller and applies calibration entirely in the graphics driver's own -// native (LV_DISPLAY_ROTATION_0) coordinate space - native_x_max/native_y_max are just the panel's -// fixed pixel dimensions, not a rotation. This function has no notion of LVGL rotation at all: -// calibration corrects the raw sensor's fixed physical mapping, which never changes with on-screen -// orientation, so it doesn't belong anywhere near rotation math. -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) { - if (pointer_read_data(device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) { - return false; +// Reads all currently-touched points from the device into pool->raw_x/raw_y/raw_count, applying +// calibration in the graphics driver's own native (LV_DISPLAY_ROTATION_0) coordinate space - +// native_x_max/native_y_max are just the panel's fixed pixel dimensions, not a rotation. This +// function has no notion of LVGL rotation at all: calibration corrects the raw sensor's fixed +// physical mapping, which never changes with on-screen orientation, so it doesn't belong anywhere +// near rotation math. +static void lvgl_pointer_pool_refresh(struct LvglPointerPool* pool, int32_t native_x_max, int32_t native_y_max) { + pool->raw_count = 0; + + if (pointer_read_data(pool->device, LVGL_POINTER_READ_TIMEOUT) != ERROR_NONE) { + return; } uint8_t point_count = 0; - if (!pointer_get_touched_points(device, x, y, NULL, &point_count, 1) || point_count == 0) { - return false; + if (!pointer_get_touched_points(pool->device, pool->raw_x, pool->raw_y, NULL, &point_count, LVGL_POINTER_MAX_SLOTS) || point_count == 0) { + return; } + if (point_count > LVGL_POINTER_MAX_SLOTS) point_count = LVGL_POINTER_MAX_SLOTS; - if (ctx->calibration_enabled && native_x_max > 0 && native_y_max > 0) { - lvgl_pointer_calibration_apply(&ctx->calibration, native_x_max, native_y_max, x, y); + 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]); + } } - return true; + pool->raw_count = point_count; } -// The actual LVGL indev read callback: wraps lvgl_pointer_read_calibrated() and, only here, applies -// the rotation needed to place the (still native-space) point into the currently active LVGL -// logical space - unconditionally, since native-space coordinates always need this regardless of -// whether calibration is enabled. -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 LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; - lv_display_t* display = lv_indev_get_display(indev); +// Matches this round's raw points onto pool slots by nearest-neighbor to each slot's last known +// position, so a slot "follows" the same physical finger across rounds instead of jumping when +// the touch controller reports points in a different order (no touch-ID/tracking field exists +// 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] = {}; - // 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; + // 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; + } + } +} - uint16_t x = 0; - uint16_t y = 0; - if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) { +// 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) { + struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev); + if (pool == NULL) { data->state = LV_INDEV_STATE_RELEASED; return; } - data->point.x = x; - data->point.y = y; - data->state = LV_INDEV_STATE_PRESSED; + uint8_t slot = 0; + 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; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } } -error_t lvgl_pointer_add(struct Device* device, lv_display_t* display, lv_indev_t** out_indev) { - if (device == NULL || out_indev == NULL) { +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_indevs == NULL) { return ERROR_INVALID_ARGUMENT; } if (device_get_type(device) != &POINTER_TYPE) { 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(); - if (ctx == NULL) { + auto* pool = new(std::nothrow) LvglPointerPool(); + if (pool == NULL) { return ERROR_OUT_OF_MEMORY; } - struct LvglDeviceContext* wrapper = new(std::nothrow) LvglDeviceContext(ctx); - if (wrapper == NULL) { - delete ctx; - return ERROR_OUT_OF_MEMORY; - } - wrapper->device = device; + pool->device = device; + pool->slot_count = max_touch_points; - lv_indev_t* indev = lv_indev_create(); - if (indev == NULL) { - delete wrapper; - return ERROR_OUT_OF_MEMORY; + // 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) { + break; + } + wrapper->device = device; + + lv_indev_t* indev = lv_indev_create(); + if (indev == NULL) { + delete wrapper; + break; + } + + lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); + lv_indev_set_read_cb(indev, lvgl_pointer_read_cb); + lv_indev_set_driver_data(indev, wrapper); + if (display != NULL) { + lv_indev_set_display(indev, display); + } + + pool->slot_indev[created] = indev; + out_indevs[created] = indev; } - lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); - lv_indev_set_read_cb(indev, lvgl_pointer_read_cb); - lv_indev_set_driver_data(indev, wrapper); - if (display != NULL) { - lv_indev_set_display(indev, display); + 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 = indev; + default_pointer_indev = pool->slot_indev[0]; } - *out_indev = indev; return ERROR_NONE; } @@ -145,48 +301,78 @@ lv_indev_t* lvgl_pointer_get_default(void) { 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) { - if (indev == NULL) { + struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev); + if (pool == NULL) { 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) { - ctx->calibration_enabled = false; + pool->calibration_enabled = false; return ERROR_NONE; } if (!lvgl_pointer_calibration_is_valid(calibration)) { return ERROR_INVALID_ARGUMENT; } - ctx->calibration = *calibration; - ctx->calibration_enabled = true; + pool->calibration = *calibration; + pool->calibration_enabled = true; return ERROR_NONE; } 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; } - struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); - struct LvglPointerCtx* ctx = (struct LvglPointerCtx*)wrapper->context; - if (!ctx->calibration_enabled) { + struct LvglPointerPool* pool = lvgl_pointer_pool_from_indev(indev); + if (pool == NULL || !pool->calibration_enabled) { return false; } - *out_calibration = ctx->calibration; + *out_calibration = pool->calibration; 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) { if (indev == NULL) { return; } struct LvglDeviceContext* wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(indev); - if (default_pointer_indev == indev) { - default_pointer_indev = NULL; + 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; + } + struct LvglDeviceContext* slot_wrapper = (struct LvglDeviceContext*)lv_indev_get_driver_data(slot_indev); + slot_wrapper->context = NULL; + lv_indev_delete(slot_indev); + delete slot_wrapper; } - lv_indev_delete(indev); - delete wrapper; + delete pool; } diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index ba74f2f73..b5a0c6a2d 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -45,6 +45,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lvgl_pointer_get_calibration), DEFINE_MODULE_SYMBOL(lvgl_pointer_get_default), DEFINE_MODULE_SYMBOL(lvgl_pointer_add), + DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index), DEFINE_MODULE_SYMBOL(lvgl_pointer_remove), // lvgl_spinner 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_text_button_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_clear_actions), // 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_grow), 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_mark_layout_as_dirty), 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_y), 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_set_scrollbar_mode), 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_cursor_click_pos), 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 DEFINE_MODULE_SYMBOL(lv_palette_main), 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_set_group), DEFINE_MODULE_SYMBOL(lv_indev_wait_release), + DEFINE_MODULE_SYMBOL(lv_indev_enable), // lv_timer DEFINE_MODULE_SYMBOL(lv_timer_handler), 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_mutable), DEFINE_MODULE_SYMBOL(lv_tick_get), + DEFINE_MODULE_SYMBOL(lv_delay_ms), DEFINE_MODULE_SYMBOL(lv_tick_elaps), // lv_slider DEFINE_MODULE_SYMBOL(lv_slider_create), diff --git a/Modules/lvgl-module/source/widgets/toolbar.cpp b/Modules/lvgl-module/source/widgets/toolbar.cpp index 612f4f019..8c8730539 100644 --- a/Modules/lvgl-module/source/widgets/toolbar.cpp +++ b/Modules/lvgl-module/source/widgets/toolbar.cpp @@ -234,6 +234,29 @@ lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj) { 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(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) { auto* toolbar = reinterpret_cast(obj); check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached"); diff --git a/Platforms/platform-esp32/CMakeLists.txt b/Platforms/platform-esp32/CMakeLists.txt index 4543232e9..419e0801a 100644 --- a/Platforms/platform-esp32/CMakeLists.txt +++ b/Platforms/platform-esp32/CMakeLists.txt @@ -2,11 +2,20 @@ cmake_minimum_required(VERSION 3.20) 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( SRCS ${SOURCES} INCLUDE_DIRS "include/" PRIV_INCLUDE_DIRS "private/" 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}) diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-cdc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-cdc.yaml new file mode 100644 index 000000000..34d6af3e4 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-cdc.yaml @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-hid.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-hid.yaml new file mode 100644 index 000000000..fa12221ac --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-hid.yaml @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-midi.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-midi.yaml new file mode 100644 index 000000000..56190c487 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-midi.yaml @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-msc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-msc.yaml new file mode 100644 index 000000000..4f5594f06 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice-msc.yaml @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice.yaml new file mode 100644 index 000000000..1209c9356 --- /dev/null +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbdevice.yaml @@ -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. diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_usbdevice.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbdevice.h new file mode 100644 index 000000000..fb136051a --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbdevice.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#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 diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_usbdevice.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbdevice.h new file mode 100644 index 000000000..95eb0ef4e --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbdevice.h @@ -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 diff --git a/Platforms/platform-esp32/include/tactility/drivers/hid_report_descriptors.h b/Platforms/platform-esp32/include/tactility/drivers/hid_report_descriptors.h new file mode 100644 index 000000000..682998638 --- /dev/null +++ b/Platforms/platform-esp32/include/tactility/drivers/hid_report_descriptors.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#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 diff --git a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp index cb3905e75..15d7b8604 100644 --- a/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/bluetooth/esp32_ble_hid.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -58,77 +59,10 @@ static uint8_t hid_protocol_mode = 0x01; // 0x00=Boot, 0x01=Report // ============================================================================ // Per-profile HID Report Maps // ============================================================================ - -// Keyboard + Consumer (IDs 1 and 2) -static const uint8_t hid_rpt_map_kb_consumer[] = { - 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, -}; - -// 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, -}; +// +// Shared with esp32_usb_hid_device.cpp - see hid_report_descriptors.h for the byte tables +// (report ID scheme, per-field layout) - HID report descriptors are transport-independent +// so the same bytes apply verbatim whether carried over USB or BLE GATT. // ---- Per-profile Report Reference descriptor data ---- // 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; 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_gamepad_input_handle) report_len = 8; int rc = os_mbuf_append(ctxt->om, zeros, report_len); 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; size_t new_rpt_map_len = 0; 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::Mouse: svcs = gatt_svcs_mouse; new_rpt_map = hid_rpt_map_mouse; new_rpt_map_len = sizeof(hid_rpt_map_mouse); 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::Gamepad: svcs = gatt_svcs_gamepad; new_rpt_map = hid_rpt_map_gamepad; new_rpt_map_len = sizeof(hid_rpt_map_gamepad); 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_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_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_report_map_gamepad; new_rpt_map_len = hid_report_map_gamepad_len; break; default: svcs = gatt_svcs_none; break; } diff --git a/Platforms/platform-esp32/source/drivers/hid_report_descriptors.cpp b/Platforms/platform-esp32/source/drivers/hid_report_descriptors.cpp new file mode 100644 index 000000000..0fa08e7f0 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/hid_report_descriptors.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +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" diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usb_cdc_device.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_cdc_device.cpp new file mode 100644 index 000000000..f4ba8b109 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_cdc_device.cpp @@ -0,0 +1,186 @@ +#include +#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#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(len) < sizeof(buf) ? static_cast(len) : sizeof(buf) - 1; + tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, reinterpret_cast(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(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(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 diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_controller.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_controller.cpp new file mode 100644 index 000000000..d760f67a1 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_controller.cpp @@ -0,0 +1,414 @@ +#include +#if CONFIG_SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED) + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#if CONFIG_IDF_TARGET_ESP32P4 +#include +#include +#elif CONFIG_IDF_TARGET_ESP32S3 +#include +#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(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(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(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(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(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(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(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(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(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) diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_msc.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_msc.cpp new file mode 100644 index 000000000..f90b36965 --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_device_msc.cpp @@ -0,0 +1,250 @@ +#include +#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#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(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(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(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(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(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(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(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 diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usb_hid_device.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_hid_device.cpp new file mode 100644 index 000000000..bd5e5919a --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_hid_device.cpp @@ -0,0 +1,352 @@ +#include +#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#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(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 diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usb_midi_device.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_midi_device.cpp new file mode 100644 index 000000000..17e77665b --- /dev/null +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usb_midi_device.cpp @@ -0,0 +1,195 @@ +#include +#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#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(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 diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 02614d245..557be5474 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -43,6 +43,21 @@ extern Driver esp32_usbhost_hid_driver; extern Driver esp32_usbhost_midi_driver; extern Driver esp32_usbhost_msc_driver; #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() { /* 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_midi_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 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_driver) == ERROR_NONE); #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 check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE); check(driver_remove_destruct(&esp32_usbhost_midi_driver) == ERROR_NONE); diff --git a/Tactility/Source/hal/usb/UsbTusb.cpp b/Tactility/Source/hal/usb/UsbTusb.cpp index 73fc8f063..ed2fc796c 100644 --- a/Tactility/Source/hal/usb/UsbTusb.cpp +++ b/Tactility/Source/hal/usb/UsbTusb.cpp @@ -10,102 +10,43 @@ #include #include #include -#include -#include #include +#include +#include +#include #include -#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"; namespace tt::hal::usb { - extern sdmmc_card_t* getCard(); -} // Set when mass storage was started as part of the dedicated reboot-into-MSC boot flow. // Used to decide whether ejecting the volume should automatically reboot back to normal OS. static bool startedFromBootMode = false; -enum { - ITF_NUM_MSC = 0, - ITF_NUM_TOTAL -}; - -enum { - EDPT_CTRL_OUT = 0x00, - EDPT_CTRL_IN = 0x80, - - EDPT_MSC_OUT = 0x01, - EDPT_MSC_IN = 0x81, -}; - -static bool driverInstalled = false; - -static tusb_desc_device_t descriptor_config = { - .bLength = sizeof(descriptor_config), - .bDescriptorType = TUSB_DESC_DEVICE, - .bcdUSB = 0x0200, - .bDeviceClass = TUSB_CLASS_MISC, - .bDeviceSubClass = MISC_SUBCLASS_COMMON, - .bDeviceProtocol = MISC_PROTOCOL_IAD, - .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, - .idVendor = 0x303A, // TODO: Espressif VID. Do we need to change this? - .idProduct = 0x4002, - .bcdDevice = 0x100, - .iManufacturer = 0x01, - .iProduct = 0x02, - .iSerialNumber = 0x03, - .bNumConfigurations = 0x01 -}; +static sdmmc_card_t* getCard() { + sdmmc_card_t* card = nullptr; -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 -}; + device_for_each(&card, [](auto* device, void* context) { + auto* driver = device_get_driver(device); + 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(context); + *out = esp32_sdcard_get_card(device); + return *out == nullptr; + }); -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), + if (card == nullptr) { + LOG_W(TAG, "Couldn't find a mounted SD card"); + } - // 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 + return card; +} -static void storage_mount_changed_cb(tinyusb_msc_event_t* event) { - if (event->mount_changed_data.is_mounted) { +static void onMountChanged(bool mounted, void* /*context*/) { + if (mounted) { LOG_I(TAG, "MSC Mounted"); // Storage is only (re)mounted into our own filesystem after the host sends a SCSI // START STOP UNIT eject (see tud_msc_start_stop_cb() in tusb_msc_storage.c). Windows @@ -121,130 +62,73 @@ static void storage_mount_changed_cb(tinyusb_msc_event_t* event) { } } -static bool ensureDriverInstalled() { - 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; -} +} // namespace tt::hal::usb bool tusbIsSupported() { return true; } 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; } - startedFromBootMode = fromBootMode; auto* card = tt::hal::usb::getCard(); if (card == nullptr) { LOG_E(TAG, "SD card not mounted"); + device_put(device); return false; } - const tinyusb_msc_sdmmc_config_t config_sdmmc = { - .card = card, - .callback_mount_changed = storage_mount_changed_cb, - .callback_premount_changed = nullptr, - .mount_config = { - .format_if_mount_failed = false, - .max_files = 5, - .allocation_unit_size = 0, - .disk_status_check_enable = false, - .use_one_fat = false - } - }; + tt::hal::usb::startedFromBootMode = fromBootMode; - auto result = tinyusb_msc_storage_init_sdmmc(&config_sdmmc); - if (result != ESP_OK) { - LOG_E(TAG, "TinyUSB SDMMC init failed: %s", esp_err_to_name(result)); + auto result = usb_msc_device_start(device, USB_MSC_DEVICE_SOURCE_SDMMC, card, + tt::hal::usb::onMountChanged, nullptr); + if (result != ERROR_NONE) { + LOG_E(TAG, "TinyUSB SDMMC init failed: %s", error_to_string(result)); } else { LOG_I(TAG, "TinyUSB SDMMC init success"); } - return result == ESP_OK; + device_put(device); + return result == ERROR_NONE; } bool tusbStartMassStorageWithFlash(bool fromBootMode) { 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; } - startedFromBootMode = fromBootMode; wl_handle_t handle = tt::getDataPartitionWlHandle(); if (handle == WL_INVALID_HANDLE) { LOG_E(TAG, "WL not mounted for /data"); + device_put(device); return false; } - const tinyusb_msc_spiflash_config_t config_flash = { - .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 - } - }; + tt::hal::usb::startedFromBootMode = fromBootMode; - esp_err_t result = tinyusb_msc_storage_init_spiflash(&config_flash); - if (result != ESP_OK) { - LOG_E(TAG, "TinyUSB flash init failed: %s", esp_err_to_name(result)); + auto result = usb_msc_device_start(device, USB_MSC_DEVICE_SOURCE_FLASH, + &handle, tt::hal::usb::onMountChanged, nullptr); + if (result != ERROR_NONE) { + LOG_E(TAG, "TinyUSB flash init failed: %s", error_to_string(result)); } else { LOG_I(TAG, "TinyUSB flash init success"); } - return result == ESP_OK; + device_put(device); + return result == ERROR_NONE; } void tusbStop() { - // Actively signal a disconnect to the host before tearing down the peripheral, otherwise - // a subsequent esp_restart() resets the chip too fast for the host to notice the device - // went away, leaving it stuck showing the old MSC device until the cable is replugged. - tud_disconnect(); - vTaskDelay(pdMS_TO_TICKS(250)); - - tinyusb_msc_storage_deinit(); -#if CONFIG_IDF_TARGET_ESP32P4 - usb_wrap_ll_phy_select(&USB_WRAP, 1); -#endif + auto* device = usb_msc_device_get(); + if (device != nullptr) { + usb_msc_device_stop(device); + device_put(device); + } } bool tusbCanStartMassStorageWithFlash() { diff --git a/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h b/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h index 0068e19a3..ef0418791 100644 --- a/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h +++ b/TactilityKernel/include/tactility/drivers/bluetooth_hid_device.h @@ -26,7 +26,8 @@ enum BtHidDeviceMode { BT_HID_DEVICE_MODE_MOUSE, /** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */ 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, }; @@ -90,7 +91,7 @@ struct BtHidDeviceApi { 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] report pointer to the 8-byte gamepad report * @param[in] len number of bytes (up to 8) diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index f1390f22d..a11a28ba2 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -43,6 +43,20 @@ struct KeyboardKeyData { * separately. Drivers whose hardware cannot report Alt leave this false. */ 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; }; /** diff --git a/TactilityKernel/include/tactility/drivers/usb_cdc_device.h b/TactilityKernel/include/tactility/drivers/usb_cdc_device.h new file mode 100644 index 000000000..8836165ef --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_cdc_device.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include + +#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 diff --git a/TactilityKernel/include/tactility/drivers/usb_device_controller.h b/TactilityKernel/include/tactility/drivers/usb_device_controller.h new file mode 100644 index 000000000..e91c9b0b7 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_device_controller.h @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include + +#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 diff --git a/TactilityKernel/include/tactility/drivers/usb_hid_device.h b/TactilityKernel/include/tactility/drivers/usb_hid_device.h new file mode 100644 index 000000000..1f1a16f95 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_hid_device.h @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include + +#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 diff --git a/TactilityKernel/include/tactility/drivers/usb_midi_device.h b/TactilityKernel/include/tactility/drivers/usb_midi_device.h new file mode 100644 index 000000000..a94549629 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_midi_device.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include + +#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 diff --git a/TactilityKernel/include/tactility/drivers/usb_msc_device.h b/TactilityKernel/include/tactility/drivers/usb_msc_device.h new file mode 100644 index 000000000..d9805549b --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_msc_device.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include + +#include + +#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 diff --git a/TactilityKernel/source/drivers/keyboard.cpp b/TactilityKernel/source/drivers/keyboard.cpp index 8c14bcd28..b43c539ef 100644 --- a/TactilityKernel/source/drivers/keyboard.cpp +++ b/TactilityKernel/source/drivers/keyboard.cpp @@ -10,10 +10,12 @@ extern "C" { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { const auto* driver = device_get_driver(device); - // Default the modifier fields here rather than in each driver: only drivers whose hardware can - // report modifiers set them, and the rest would otherwise leave whatever the caller's stack held. + // Default the modifier/HID fields here rather than in each driver: only drivers whose hardware + // can report them set them, and the rest would otherwise leave whatever the caller's stack held. data->ctrl = false; data->alt = false; + data->hid_keycode = 0; + data->hid_modifier = 0; return KEYBOARD_DRIVER_API(driver)->read_key(device, data); } diff --git a/TactilityKernel/source/drivers/usb_cdc_device.cpp b/TactilityKernel/source/drivers/usb_cdc_device.cpp new file mode 100644 index 000000000..50ef71035 --- /dev/null +++ b/TactilityKernel/source/drivers/usb_cdc_device.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +#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" diff --git a/TactilityKernel/source/drivers/usb_device_controller.cpp b/TactilityKernel/source/drivers/usb_device_controller.cpp new file mode 100644 index 000000000..b79840db1 --- /dev/null +++ b/TactilityKernel/source/drivers/usb_device_controller.cpp @@ -0,0 +1,45 @@ +#include +#include +#include + +#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" diff --git a/TactilityKernel/source/drivers/usb_hid_device.cpp b/TactilityKernel/source/drivers/usb_hid_device.cpp new file mode 100644 index 000000000..e59d2ef0a --- /dev/null +++ b/TactilityKernel/source/drivers/usb_hid_device.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +#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" diff --git a/TactilityKernel/source/drivers/usb_midi_device.cpp b/TactilityKernel/source/drivers/usb_midi_device.cpp new file mode 100644 index 000000000..1027d7564 --- /dev/null +++ b/TactilityKernel/source/drivers/usb_midi_device.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +#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" diff --git a/TactilityKernel/source/drivers/usb_msc_device.cpp b/TactilityKernel/source/drivers/usb_msc_device.cpp new file mode 100644 index 000000000..2983f566e --- /dev/null +++ b/TactilityKernel/source/drivers/usb_msc_device.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +#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" diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 418b116f8..e440f6dc8 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -32,9 +32,13 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include #include #include @@ -416,6 +420,40 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { // drivers/usb_host_msc DEFINE_MODULE_SYMBOL(usb_msc_eject), 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 DEFINE_MODULE_SYMBOL(dispatcher_alloc), DEFINE_MODULE_SYMBOL(dispatcher_free), diff --git a/device.py b/device.py index 448908258..ebdd082fc 100644 --- a/device.py +++ b/device.py @@ -341,11 +341,34 @@ def write_touch_calibration_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") - if has_tiny_usb: + has_tiny_usb_msc = get_boolean_property_or_false(device_properties, "hardware.tinyUsbMsc") + 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("CONFIG_TINYUSB_MSC_ENABLED=y\n") - output_file.write("CONFIG_TINYUSB_MSC_MOUNT_PATH=\"/sdcard\"\n") + if has_tiny_usb_msc: + output_file.write("CONFIG_TINYUSB_MSC_ENABLED=y\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() if idf_target == "esp32p4": # P4 has two USB-DWC controllers (HS/UTMI and FS/FSLS). esp_tinyusb defaults to