USB device-mode support (#613)

This commit is contained in:
Shadowtrance
2026-08-13 05:29:35 +10:00
committed by GitHub
parent 37c507544b
commit cc8be3faef
94 changed files with 3466 additions and 399 deletions
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
@@ -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";
};
};
};
@@ -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
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
@@ -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";
};
};
};
@@ -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
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/gpio_backlight.h>
#include <bindings/rgb_display.h>
@@ -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";
};
};
};
@@ -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
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
@@ -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";
};
};
};
+1 -1
View File
@@ -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"
+9
View File
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h>
@@ -139,4 +140,12 @@
slot = <SDMMC_HOST_SLOT_1>;
bus-width = <4>;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
@@ -8,7 +8,7 @@ apps.autoStartAppId=ApWebServer
hardware.target=ESP32S3
hardware.flashSize=8MB
hardware.spiRam=false
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -6,6 +6,7 @@
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/gpio_hog.h>
#include <bindings/button_control.h>
@@ -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";
};
};
};
+1 -1
View File
@@ -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
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/sx1262.h>
#include <bindings/xl9555.h>
@@ -179,4 +180,12 @@
dio2-as-rf-switch;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -14,6 +14,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/gt911.h>
#include <bindings/st7789.h>
@@ -198,4 +199,12 @@
model = <GPS_MODEL_UBLOX10>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -11,7 +11,7 @@ hardware.flashMode=DIO
hardware.spiRam=true
hardware.spiRamMode=AUTO
hardware.spiRamSpeed=80M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=80M
hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/cst328.h>
#include <bindings/gdeq031t10.h>
@@ -135,4 +136,12 @@
frequency-khz = <20000>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
+9
View File
@@ -14,6 +14,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/gt911.h>
#include <bindings/st7789.h>
@@ -192,4 +193,12 @@
i2cPort = <I2C_NUM_1>;
i2cClockFrequency = <400000>;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/battery_sense.h>
#include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789_i8080.h>
#include <bindings/button_control.h>
@@ -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";
};
};
};
+1 -1
View File
@@ -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
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7735.h>
@@ -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";
};
};
};
+1 -1
View File
@@ -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
+9
View File
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <tactility/bindings/gpio_hog.h>
#include <bindings/button_control.h>
@@ -131,4 +132,12 @@
bus-width = <1>;
pullups;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.flashMode=DIO
hardware.spiRam=true
hardware.spiRamMode=AUTO
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=40M
hardware.bluetooth=true
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <gps_generic/bindings.h>
#include <bindings/st7796.h>
@@ -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";
};
};
};
@@ -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
@@ -11,6 +11,7 @@
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/bmi270.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
@@ -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";
};
};
};
+1 -1
View File
@@ -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
@@ -11,6 +11,7 @@
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
@@ -164,4 +165,12 @@
i2s = <&i2s0>;
channels = <1>;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h>
#include <bindings/aw88298.h>
#include <bindings/aw9523b.h>
@@ -205,4 +206,12 @@
backlight = <&display_backlight>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -10,7 +10,7 @@ hardware.spiRam=true
hardware.spiRamMode=OPI
hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.bluetooth=true
storage.userDataLocation=SD
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/battery_sense.h>
#include <bindings/bmi270.h>
#include <bindings/bm8563.h>
@@ -114,4 +115,12 @@
// correct orientation.
rotation = <EPD_ROT_INVERTED_PORTRAIT>;
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -8,7 +8,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -10,6 +10,7 @@
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h>
#include <bindings/aw88298.h>
#include <bindings/aw9523b.h>
@@ -238,4 +239,12 @@
frequency-khz = <20000>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
+1 -1
View File
@@ -9,7 +9,7 @@ hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.bluetooth=true
storage.userDataLocation=Internal
@@ -7,6 +7,7 @@
#include <tactility/bindings/esp32_i2s.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <bindings/bmi270.h>
#include <bindings/m5pm1.h>
#include <bindings/es8311.h>
@@ -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";
};
};
};
@@ -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<uint32_t>('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<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U));
uint8_t modifier = static_cast<uint8_t>(m.modifier | (aa_active ? 0x02U : 0U));
if (internal->ctrl_held) modifier |= 0x01U; // HID LeftCtrl
if (internal->alt_held) modifier |= 0x04U; // HID LeftAlt
const uint32_t lv_key = tab5_translate_key(m.keycode, modifier, internal->ctrl_held);
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;
+4 -1
View File
@@ -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
+21
View File
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_usbhost.h>
#include <tactility/bindings/esp32_wifi.h>
#include <tactility/bindings/gpio_hog.h>
@@ -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";
};
};
};
@@ -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
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_sdmmc.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/pwm_backlight.h>
@@ -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";
};
};
};
@@ -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
@@ -9,6 +9,7 @@
#include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/st7789.h>
@@ -90,4 +91,12 @@
frequency-khz = <20000>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
@@ -10,7 +10,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=QUAD
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -9,6 +9,7 @@
#include <bindings/qmi8658.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/cst816t.h>
#include <bindings/gc9a01.h>
@@ -93,4 +94,12 @@
backlight = <&display_backlight>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
@@ -9,7 +9,7 @@ hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=120M
hardware.tinyUsb=true
hardware.tinyUsbMsc=true
hardware.esptoolFlashFreq=120M
hardware.bluetooth=true
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/axs5106.h>
#include <bindings/button_control.h>
@@ -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";
};
};
};
@@ -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
@@ -8,6 +8,7 @@
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_uart.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/gpio_hog.h>
#include <bindings/ch422g.h>
#include <bindings/gt911.h>
@@ -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";
};
};
};
@@ -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
@@ -9,6 +9,7 @@
#include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_i8080.h>
#include <tactility/bindings/esp32_pwm_ledc.h>
#include <tactility/bindings/esp32_usbdevice.h>
#include <tactility/bindings/pwm_backlight.h>
#include <bindings/ft6x36.h>
#include <bindings/st7796_i8080.h>
@@ -105,4 +106,12 @@
frequency-khz = <20000>;
};
};
usbdevice0 {
compatible = "espressif,esp32-usbdevice";
usbdevicemsc0 {
compatible = "espressif,esp32-usbdevice-msc";
};
};
};
@@ -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 Removes an indev previously created with lvgl_pointer_add().
* @brief Returns which slot index (0-based) of its pool the given indev is, or -1 if indev wasn't
* created by lvgl_pointer_add() (or is NULL). Useful for indexing small per-finger app state
* arrays from an event callback's lv_event_get_indev() result.
* @warning Caller must hold the LVGL lock.
*/
int8_t lvgl_pointer_get_slot_index(lv_indev_t* indev);
/**
* @brief Removes every slot indev in the same pool as the given indev (i.e. all indevs returned
* together by one lvgl_pointer_add() call).
* @warning Caller must hold the LVGL lock.
*/
void lvgl_pointer_remove(lv_indev_t* indev);
@@ -68,6 +68,16 @@ lv_obj_t* lvgl_toolbar_add_text_button_action(lv_obj_t* obj, const char* text, l
*/
lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj);
/**
* 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
+12 -6
View File
@@ -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);
}
}
+261 -75
View File
@@ -2,14 +2,13 @@
#include <lvgl/devices/pointer.h>
#include <lvgl/devices/device_context.h>
#include <tactility/device.h>
#include <tactility/drivers/pointer.h>
constexpr auto* TAG = "lvgl_pointer";
#include <cstring>
#include <new>
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 (pool->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
for (uint8_t i = 0; i < point_count; i++) {
lvgl_pointer_calibration_apply(&pool->calibration, native_x_max, native_y_max, &pool->raw_x[i], &pool->raw_y[i]);
}
}
if (ctx->calibration_enabled && native_x_max > 0 && native_y_max > 0) {
lvgl_pointer_calibration_apply(&ctx->calibration, native_x_max, native_y_max, x, y);
}
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.
// 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] = {};
// First pass: let already-active slots keep following their nearest raw point, so a held
// finger doesn't get reshuffled onto a different slot just because another finger moved.
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (!pool->slot_active[s]) continue;
int32_t best_dist = -1;
int8_t best_raw = -1;
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
int32_t dx = (int32_t)pool->raw_x[r] - pool->slot_point[s].x;
int32_t dy = (int32_t)pool->raw_y[r] - pool->slot_point[s].y;
int32_t dist = dx * dx + dy * dy;
if (best_raw < 0 || dist < best_dist) {
best_dist = dist;
best_raw = (int8_t)r;
}
}
if (best_raw >= 0 && best_dist <= max_track_dist_sq) {
raw_claimed[best_raw] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[best_raw];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[best_raw];
} else {
pool->slot_active[s] = false;
slot_released_now[s] = true;
}
}
// Second pass: any unclaimed raw point is a new touch - hand it to the first inactive slot
// that wasn't just released this round.
for (uint8_t r = 0; r < pool->raw_count; r++) {
if (raw_claimed[r]) continue;
for (uint8_t s = 0; s < pool->slot_count; s++) {
if (pool->slot_active[s] || slot_released_now[s]) continue;
pool->slot_active[s] = true;
pool->slot_point[s].x = (lv_coord_t)pool->raw_x[r];
pool->slot_point[s].y = (lv_coord_t)pool->raw_y[r];
raw_claimed[r] = true;
break;
}
}
}
// The actual LVGL indev read callback, shared by every slot in the pool. Only the first slot to
// be read each round (round_pos wraps 0..slot_count-1) triggers the real bus read + reassignment;
// the rest just report whatever lvgl_pointer_pool_assign() decided for their slot. Which slot
// happens to run first varies (LVGL calls each indev's timer independently), but since all slots
// share one timer period they complete one full round every slot_count calls regardless of order.
static void lvgl_pointer_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
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);
// lv_display_get_original_*_resolution() is the native (LV_DISPLAY_ROTATION_0) size,
// unaffected by the display's current rotation - no rotation lookup needed to get it.
int32_t native_x_max = display != NULL ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
int32_t native_y_max = display != NULL ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
uint16_t x = 0;
uint16_t y = 0;
if (!lvgl_pointer_read_calibrated(wrapper->device, ctx, native_x_max, native_y_max, &x, &y)) {
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;
}
+17
View File
@@ -45,6 +45,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_calibration),
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_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),
@@ -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<Toolbar*>(obj);
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
lv_obj_set_style_pad_hor(wrapper, 4, LV_STATE_DEFAULT);
lv_obj_t* widget = lv_dropdown_create(wrapper);
lv_dropdown_set_options(widget, options);
lv_dropdown_set_selected_highlight(widget, false);
if (width > 0) {
lv_obj_set_width(widget, width);
}
if (text != nullptr) {
lv_dropdown_set_text(widget, text);
}
lv_obj_set_align(widget, LV_ALIGN_CENTER);
return widget;
}
lv_obj_t* lvgl_toolbar_add_spinner_action(lv_obj_t* obj) {
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
+9
View File
@@ -2,11 +2,20 @@ cmake_minimum_required(VERSION 3.20)
file(GLOB_RECURSE SOURCES "source/*.c**")
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})
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode CDC-ACM addon (composites a USB serial console interface into whichever primary class - HID or MIDI - is active, or stands alone with no primary; deliberately excluded from MSC, which always presents as plain mass storage with no console). Requires CONFIG_TINYUSB_CDC_ENABLED.
compatible: "espressif,esp32-usbdevice-cdc"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode HID class driver (present the board as a USB HID peripheral, e.g. a keyboard, to a host). Requires CONFIG_TINYUSB_HID_COUNT > 0.
compatible: "espressif,esp32-usbdevice-hid"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode MIDI class driver (present the board as a USB MIDI peripheral to a host). Requires CONFIG_TINYUSB_MIDI_COUNT > 0.
compatible: "espressif,esp32-usbdevice-midi"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB device-mode MSC class driver (present the board's SD card or internal flash as a USB mass storage device to a host). Requires CONFIG_TINYUSB_MSC_ENABLED.
compatible: "espressif,esp32-usbdevice-msc"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,13 @@
description: ESP32 USB device-mode (peripheral) controller. Owns the single TinyUSB device-mode slot shared by its child classes (HID, MSC, ...); only one child class may be active at a time.
compatible: "espressif,esp32-usbdevice"
properties:
_unused:
type: int
default: 0
description: |
USB device-mode (peripheral) controller: presents itself as a USB device to a host (HID keyboard, mass storage, ...).
Only one of usbdevicehid0/usbdevicemsc0/usbdevicemidi0 may be active at a time -
usbdevicecdc0 is not part of that exclusion - it's an addon composited into whichever of HID/MIDI is active (never MSC)
toggle its status to enable/disable the USB serial console independent of which primary is running.
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_usbdevice.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_usbdevice, struct Esp32UsbDeviceConfig)
DEFINE_DEVICETREE(esp32_usbdevice_hid, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_msc, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_midi, struct Esp32UsbDeviceChildConfig)
DEFINE_DEVICETREE(esp32_usbdevice_cdc, struct Esp32UsbDeviceChildConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
struct Esp32UsbDeviceConfig {
int _unused;
};
struct Esp32UsbDeviceChildConfig {
int _unused;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// ---- Shared HID report descriptor tables ----
//
// HID report descriptors are transport-independent (the same USB HID Usage Tables spec applies
// regardless of whether the bytes travel over USB or BLE GATT), so these tables are shared
// between esp32_usb_hid_device.cpp and esp32_ble_hid.cpp rather than each keeping its own copy.
// Report ID scheme matches BtHidDeviceMode/UsbHidDeviceMode 1:1 across both transports:
// Keyboard mode: report ID 1 = keyboard, report ID 2 = consumer control
// Mouse mode: report ID 1 = mouse
// Keyboard+Mouse mode: report ID 1 = keyboard, 2 = consumer, 3 = mouse
// Gamepad mode: report ID 1 = gamepad
/** Keyboard (report ID 1, 8 bytes: modifier, reserved, keycode[6]) + Consumer (report ID 2,
* 16-bit usage code, 2 bytes). Keyboard collection uses the standard boot-protocol byte
* layout, though the descriptor itself is Report protocol (report-ID-multiplexed with
* Consumer, which boot protocol can't express). */
extern const uint8_t hid_report_map_keyboard_consumer[];
extern const size_t hid_report_map_keyboard_consumer_len;
/** Mouse only (report ID 1, 4 bytes: buttons[3]+5 padding bits, X, Y, wheel). */
extern const uint8_t hid_report_map_mouse[];
extern const size_t hid_report_map_mouse_len;
/** Keyboard (report ID 1) + Consumer (report ID 2) + Mouse (report ID 3) combined. */
extern const uint8_t hid_report_map_keyboard_consumer_mouse[];
extern const size_t hid_report_map_keyboard_consumer_mouse_len;
/** Gamepad only (report ID 1, 8 bytes). Xbox-360-style layout: X/Y (1 byte each, left stick),
* Rx/Ry (1 byte each, right stick), Z (1 byte, triggers - L=positive, R=negative, centered=0),
* hat/dpad (1 byte, 1-8 = 8-direction clockwise from Up, 0 = centered/released), buttons[10]
* (2 bytes, 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Select 8=Start 9=L3 10=R3, 6 padding bits at the end). */
extern const uint8_t hid_report_map_gamepad[];
extern const size_t hid_report_map_gamepad_len;
#ifdef __cplusplus
}
#endif
@@ -11,6 +11,7 @@
#include <host/ble_hs_mbuf.h>
#include <services/gap/ble_svc_gap.h>
#include <services/gatt/ble_svc_gatt.h>
#include <tactility/drivers/hid_report_descriptors.h>
#include <cstring>
@@ -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;
}
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/hid_report_descriptors.h>
extern "C" {
// Keyboard (report ID 1) + Consumer (report ID 2)
const uint8_t hid_report_map_keyboard_consumer[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x06, // Usage: Keyboard
0xA1, 0x01, // Collection: Application
// --- Keyboard
0x85, 0x01, // Report ID: 1
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0xE0, // Usage Minimum: Left Control
0x29, 0xE7, // Usage Maximum: Right GUI
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x08, // Report Count: 8
0x81, 0x02, // Input: Data, Variable, Absolute (modifier byte)
0x75, 0x08, // Report Size: 8
0x95, 0x01, // Report Count: 1
0x81, 0x01, // Input: Constant, Array, Absolute (reserved byte)
0x05, 0x08, // Usage Page: LEDs
0x19, 0x01, // Usage Minimum: 0x01
0x29, 0x05, // Usage Maximum: 0x05
0x75, 0x01, // Report Size: 1
0x95, 0x05, // Report Count: 5
0x91, 0x02, // Output: Data, Variable, Absolute (LED state)
0x75, 0x03, // Report Size: 3
0x95, 0x01, // Report Count: 1
0x91, 0x01, // Output: Constant, Array, Absolute (LED padding)
0x15, 0x00, // Logical Minimum: 0
0x25, 0x73, // Logical Maximum: 115
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0x00, // Usage Minimum: 0x00
0x29, 0x73, // Usage Maximum: 0x73
0x75, 0x08, // Report Size: 8
0x95, 0x06, // Report Count: 6
0x81, 0x00, // Input: Data, Array, Absolute (keycode[6])
0xC0, // End Collection
// --- Consumer / Media Keys
0x05, 0x0C, // Usage Page: Consumer
0x09, 0x01, // Usage: Consumer Control
0xA1, 0x01, // Collection: Application
0x85, 0x02, // Report ID: 2
0x15, 0x00, // Logical Minimum: 0
0x26, 0xFF, 0x03, // Logical Maximum: 1023
0x19, 0x00, // Usage Minimum: 0x00
0x2A, 0xFF, 0x03, // Usage Maximum: 0x3FF
0x75, 0x10, // Report Size: 16
0x95, 0x01, // Report Count: 1
0x81, 0x00, // Input: Data, Array, Absolute
0xC0, // End Collection
};
const size_t hid_report_map_keyboard_consumer_len = sizeof(hid_report_map_keyboard_consumer);
// Mouse only (report ID 1)
const uint8_t hid_report_map_mouse[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x02, // Usage: Mouse
0xA1, 0x01, // Collection: Application
0x85, 0x01, // Report ID: 1
0x09, 0x01, // Usage: Pointer
0xA1, 0x00, // Collection: Physical
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x03, // Usage Maximum: 3
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x95, 0x03, // Report Count: 3
0x75, 0x01, // Report Size: 1
0x81, 0x02, // Input: Data, Variable, Absolute (3 buttons)
0x95, 0x01, // Report Count: 1
0x75, 0x05, // Report Size: 5
0x81, 0x01, // Input: Constant, Array, Absolute (padding to byte boundary)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x38, // Usage: Wheel
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x03, // Report Count: 3
0x81, 0x06, // Input: Data, Variable, Relative
0xC0, // End Collection (Physical)
0xC0, // End Collection (Application)
};
const size_t hid_report_map_mouse_len = sizeof(hid_report_map_mouse);
// Keyboard (report ID 1) + Consumer (report ID 2) + Mouse (report ID 3)
const uint8_t hid_report_map_keyboard_consumer_mouse[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x06, // Usage: Keyboard
0xA1, 0x01, // Collection: Application
// --- Keyboard
0x85, 0x01, // Report ID: 1
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0xE0, // Usage Minimum: Left Control
0x29, 0xE7, // Usage Maximum: Right GUI
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x08, // Report Count: 8
0x81, 0x02, // Input: Data, Variable, Absolute (modifier byte)
0x75, 0x08, // Report Size: 8
0x95, 0x01, // Report Count: 1
0x81, 0x01, // Input: Constant, Array, Absolute (reserved byte)
0x05, 0x08, // Usage Page: LEDs
0x19, 0x01, // Usage Minimum: 0x01
0x29, 0x05, // Usage Maximum: 0x05
0x75, 0x01, // Report Size: 1
0x95, 0x05, // Report Count: 5
0x91, 0x02, // Output: Data, Variable, Absolute (LED state)
0x75, 0x03, // Report Size: 3
0x95, 0x01, // Report Count: 1
0x91, 0x01, // Output: Constant, Array, Absolute (LED padding)
0x15, 0x00, // Logical Minimum: 0
0x25, 0x73, // Logical Maximum: 115
0x05, 0x07, // Usage Page: Keyboard/Keypad
0x19, 0x00, // Usage Minimum: 0x00
0x29, 0x73, // Usage Maximum: 0x73
0x75, 0x08, // Report Size: 8
0x95, 0x06, // Report Count: 6
0x81, 0x00, // Input: Data, Array, Absolute (keycode[6])
0xC0, // End Collection
// --- Consumer / Media Keys
0x05, 0x0C, // Usage Page: Consumer
0x09, 0x01, // Usage: Consumer Control
0xA1, 0x01, // Collection: Application
0x85, 0x02, // Report ID: 2
0x15, 0x00, // Logical Minimum: 0
0x26, 0xFF, 0x03, // Logical Maximum: 1023
0x19, 0x00, // Usage Minimum: 0x00
0x2A, 0xFF, 0x03, // Usage Maximum: 0x3FF
0x75, 0x10, // Report Size: 16
0x95, 0x01, // Report Count: 1
0x81, 0x00, // Input: Data, Array, Absolute
0xC0, // End Collection
// --- Mouse
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x02, // Usage: Mouse
0xA1, 0x01, // Collection: Application
0x85, 0x03, // Report ID: 3
0x09, 0x01, // Usage: Pointer
0xA1, 0x00, // Collection: Physical
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x03, // Usage Maximum: 3
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x95, 0x03, // Report Count: 3
0x75, 0x01, // Report Size: 1
0x81, 0x02, // Input: Data, Variable, Absolute (3 buttons)
0x95, 0x01, // Report Count: 1
0x75, 0x05, // Report Size: 5
0x81, 0x01, // Input: Constant, Array, Absolute (padding to byte boundary)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x38, // Usage: Wheel
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x03, // Report Count: 3
0x81, 0x06, // Input: Data, Variable, Relative
0xC0, // End Collection (Physical)
0xC0, // End Collection (Application)
};
const size_t hid_report_map_keyboard_consumer_mouse_len = sizeof(hid_report_map_keyboard_consumer_mouse);
// Gamepad only (report ID 1):
// left stick = X/Y, right stick = Rx/Ry, triggers share a single Z axis (LT = positive, RT = negative, centered = 0,),
// hat/dpad, 10 buttons (1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Back/Select 8=Start 9=L3 10=R3).
// DirectInput assigns axis usages to lX/lY/lZ/lRx/lRy by USAGE TAG, not by declaration order, so X/Y/Rx/Ry/Z in that order (not X/Y/Z/Rx/Ry) is correct.
const uint8_t hid_report_map_gamepad[] = {
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x05, // Usage: Game Pad
0xA1, 0x01, // Collection: Application
0x85, 0x01, // Report ID: 1
// --- 8-bit X, Y (left stick), Rx, Ry (right stick), Z (triggers, LT=+, RT=-) (min -127, max 127)
0x05, 0x01, // Usage Page: Generic Desktop
0x09, 0x30, // Usage: X
0x09, 0x31, // Usage: Y
0x09, 0x33, // Usage: Rx
0x09, 0x34, // Usage: Ry
0x09, 0x32, // Usage: Z
0x15, 0x81, // Logical Minimum: -127
0x25, 0x7F, // Logical Maximum: 127
0x75, 0x08, // Report Size: 8
0x95, 0x05, // Report Count: 5
0x81, 0x02, // Input: Data, Variable, Absolute
// --- 4-bit hat/dpad (0-7 = direction clockwise from Up, 8 = centered/released via Null State).
// Logical range must be 0-7 (not 1-8) and size must be a nibble (not a full byte) - both are required for DirectInput to recognize this as a POV.
0x09, 0x39, // Usage: Hat Switch
0x15, 0x00, // Logical Minimum: 0
0x25, 0x07, // Logical Maximum: 7
0x35, 0x00, // Physical Minimum: 0
0x46, 0x3B, 0x01, // Physical Maximum: 315
0x65, 0x14, // Unit: Eng Rot:Angular Pos (degrees)
0x75, 0x04, // Report Size: 4
0x95, 0x01, // Report Count: 1
0x81, 0x42, // Input: Data, Variable, Absolute, Null State (value 8 = centered/released, outside the declared 0-7 logical range)
0x65, 0x00, // Unit: None - resets the Unit global item so it doesn't leak onto the button/padding items below (Unit is global, not local, in HID).
0x75, 0x04, // Report Size: 4
0x95, 0x01, // Report Count: 1
0x81, 0x03, // Input: Constant, Variable, Absolute (pad hat nibble to a full byte)
// --- 10 one-bit buttons: 1=A 2=B 3=X 4=Y 5=LB 6=RB 7=Back/Select 8=Start 9=L3 10=R3
0x05, 0x09, // Usage Page: Button
0x19, 0x01, // Usage Minimum: 1
0x29, 0x0A, // Usage Maximum: 10
0x15, 0x00, // Logical Minimum: 0
0x25, 0x01, // Logical Maximum: 1
0x75, 0x01, // Report Size: 1
0x95, 0x0A, // Report Count: 10
0x81, 0x02, // Input: Data, Variable, Absolute
// --- 6-bit padding to byte-align the buttons field (10 bits -> 16 bits / 2 bytes)
0x75, 0x06, // Report Size: 6
0x95, 0x01, // Report Count: 1
0x81, 0x03, // Input: Constant, Variable, Absolute
0xC0, // End Collection
};
const size_t hid_report_map_gamepad_len = sizeof(hid_report_map_gamepad);
} // extern "C"
@@ -0,0 +1,186 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_cdc_acm.h>
#include <esp_log.h>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#define TAG "esp32_usb_cdc_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- CDC device state ----
// A single presence flag, flipped by start_device()/stop_device() - matches how the USB device
// controller checks "is this board's usbdevicecdc0 child enabled" (usb_cdc_device_is_present()),
// independent of whether the console is actually installed right now (build_contribution()/
// start_console()/stop_console() are called by the controller, not tied to this device's own
// start/stop, since CDC's console lifecycle follows whichever primary claim is active - HID or
// MIDI; MSC is deliberately excluded, see esp32_usb_device_controller.cpp's claim()).
struct UsbCdcDeviceCtx {
bool present = false;
};
// ---- CDC console (log mirroring over the CDC ACM interface) ----
static bool cdc_console_installed = false;
static vprintf_like_t previous_vprintf = nullptr;
// Mirrors every log line to the CDC ACM interface in addition to whatever the log vprintf
// hook already does (UART0, ...) - deliberately additive rather than esp_tinyusb's own
// esp_tusb_init_console(), which freopen()s stdout/stderr and would replace the existing
// console instead of adding a second one alongside it.
static int cdc_mirroring_vprintf(const char* fmt, va_list args) {
// args must be copied before the first vprintf-family call consumes it - passing an
// already-consumed va_list to vsnprintf below is undefined behavior.
va_list cdc_args;
va_copy(cdc_args, args);
int result = previous_vprintf ? previous_vprintf(fmt, args) : vprintf(fmt, args);
if (tud_cdc_connected()) {
char buf[256];
int len = vsnprintf(buf, sizeof(buf), fmt, cdc_args);
if (len > 0) {
size_t to_write = static_cast<size_t>(len) < sizeof(buf) ? static_cast<size_t>(len) : sizeof(buf) - 1;
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0, reinterpret_cast<const uint8_t*>(buf), to_write);
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);
}
}
va_end(cdc_args);
return result;
}
// ---- CDC descriptor contribution ----
static char cdc_interface_string[32] = "Tactility Console";
static uint8_t cdc_descriptor_bytes[TUD_CDC_DESC_LEN]; // one CDC instance per board
// ---- CDC device API ----
static bool cdc_device_is_present(struct Device* device) {
auto* ctx = static_cast<UsbCdcDeviceCtx*>(device_get_driver_data(device));
return ctx != nullptr && ctx->present;
}
static error_t cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string) {
(void)device;
// TUD_CDC_DESCRIPTOR consumes 2 interfaces internally (control at itfnum, data at itfnum+1 -
// it emits its own IAD covering both, see usbd.h) - same shape as MIDI's 2-interface layout.
// Also needs 2 IN endpoints (notif + data-in) and 1 OUT (data-out), mirroring the old
// HID-composited layout (HID_EP_NOTIF/HID_EP_CDC_IN/HID_EP_CDC_OUT), just dynamically
// assigned now instead of fixed literals.
struct UsbInterfaceAllocation alloc;
error_t err = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/2,
/*in_endpoint_count=*/2, /*out_endpoint_count=*/1, &alloc);
if (err != ERROR_NONE) {
return err;
}
const uint8_t ep_notif = alloc.first_in_endpoint;
const uint8_t ep_data_in = (uint8_t)(alloc.first_in_endpoint + 1);
const uint8_t ep_data_out = alloc.first_out_endpoint;
const uint8_t built[] = {
TUD_CDC_DESCRIPTOR(alloc.first_interface_number, interface_string_index,
ep_notif, 8, ep_data_out, ep_data_in, 64),
};
memcpy(cdc_descriptor_bytes, built, sizeof(built));
out_contribution->descriptor_bytes = cdc_descriptor_bytes;
out_contribution->descriptor_bytes_len = sizeof(cdc_descriptor_bytes);
out_contribution->interface_count = 2;
out_contribution->in_endpoint_count = 2;
out_contribution->out_endpoint_count = 1;
*out_interface_string = cdc_interface_string;
return ERROR_NONE;
}
static error_t cdc_device_start_console(struct Device* device) {
(void)device;
const tinyusb_config_cdcacm_t acm_cfg = {
.usb_dev = TINYUSB_USBDEV_0,
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = nullptr,
.callback_rx_wanted_char = nullptr,
.callback_line_state_changed = nullptr,
.callback_line_coding_changed = nullptr,
};
if (tusb_cdc_acm_init(&acm_cfg) != ESP_OK) {
LOG_E(TAG, "tusb_cdc_acm_init failed - CDC console unavailable");
return ERROR_RESOURCE;
}
previous_vprintf = esp_log_set_vprintf(cdc_mirroring_vprintf);
cdc_console_installed = true;
return ERROR_NONE;
}
static error_t cdc_device_stop_console(struct Device* device) {
(void)device;
if (!cdc_console_installed) {
return ERROR_NONE;
}
esp_log_set_vprintf(previous_vprintf ? previous_vprintf : vprintf);
previous_vprintf = nullptr;
tusb_cdc_acm_deinit(TINYUSB_CDC_ACM_0);
cdc_console_installed = false;
return ERROR_NONE;
}
extern const UsbCdcDeviceApi esp32_usb_cdc_device_api = {
.is_present = cdc_device_is_present,
.build_contribution = cdc_device_build_contribution,
.start_console = cdc_device_start_console,
.stop_console = cdc_device_stop_console,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (usbdevicecdc0). Presence is boolean -
// this driver doesn't participate in usb_device_controller_claim()/release() itself; the
// controller discovers this device once via device_for_each_child() in its own start_device()
// and calls is_present()/build_contribution()/start_console()/stop_console() directly.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbCdcDeviceCtx();
ctx->present = true;
device_set_driver_data(device, ctx);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbCdcDeviceCtx*>(device_get_driver_data(device));
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_cdc_device_driver = {
.name = "esp32_usb_cdc_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-cdc", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_cdc_device_api,
.device_type = &USB_CDC_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_CDC_ENABLED
@@ -0,0 +1,414 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT || CONFIG_TINYUSB_CDC_ENABLED)
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <tinyusb.h>
#include <tusb.h>
#include <cstring>
#if CONFIG_IDF_TARGET_ESP32P4
#include <hal/usb_wrap_ll.h>
#include <soc/usb_wrap_struct.h>
#elif CONFIG_IDF_TARGET_ESP32S3
#include <hal/usb_serial_jtag_ll.h>
#endif
#define TAG "esp32_usb_device_controller"
#define GET_CONFIG(device) ((const Esp32UsbDeviceConfig*)(device)->config)
// Composite descriptor scratch buffer, sized for worst case (any one primary + CDC). Primary
// contributions today max out at MIDI's ~54 bytes (TUD_CONFIG_DESC_LEN + 2*9 + 2*9 + ...); 96
// leaves headroom without tracking each primary's exact size here.
static constexpr size_t COMPOSITE_CONFIG_DESCRIPTOR_MAX = TUD_CONFIG_DESC_LEN + 96 + TUD_CDC_DESC_LEN;
// Worst case: primary's own table (lang/mfr/product/serial/interface = 5) + CDC's own interface string.
static constexpr size_t COMPOSITE_STRING_DESCRIPTOR_MAX = 6;
// ---- Controller state ----
// One TinyUSB device-mode slot, shared by every USB device class (each a separate child device
// under usbdevice0 in the devicetree - see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp /
// esp32_usb_midi_device.cpp). Only one primary class may be installed at a time - see
// UsbDeviceControllerApi::claim(). CDC (esp32_usb_cdc_device.cpp) is a separate, orthogonal
// devicetree-presence addon composited into whichever primary is active (HID or MIDI; MSC is
// deliberately excluded, see claim()'s cdc_enabled computation) - see claim() below.
struct UsbDeviceControllerCtx {
enum UsbDeviceClass active_class = USB_DEVICE_CLASS_NONE;
bool phy_routed = false;
bool cdc_console_started = false;
// Allocation state, live from begin_claim() through claim()'s call to tinyusb_driver_install()
// (or until the claim attempt is abandoned by a caller that never follows through with claim()).
bool allocation_open = false;
uint8_t next_interface = 0;
uint8_t next_in_endpoint = 1; // EP0 reserved
uint8_t next_out_endpoint = 1;
// Composite descriptor scratch, rebuilt on every claim() - can't be `static const` per-class
// anymore now that CDC's presence is a runtime devicetree fact, not compile-time. Two buffers
// since a primary (MSC) may need different bytes per speed (see UsbInterfaceContribution's
// hs_descriptor_bytes) - under !TUD_OPT_HIGH_SPEED only the fs buffer is ever used.
uint8_t composite_fs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
#if (TUD_OPT_HIGH_SPEED)
uint8_t composite_hs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
#endif
const char* composite_string_descriptor[COMPOSITE_STRING_DESCRIPTOR_MAX];
tusb_desc_device_t composite_device_descriptor;
#if (TUD_OPT_HIGH_SPEED)
tusb_desc_device_qualifier_t composite_device_qualifier;
#endif
tinyusb_config_t composite_tusb_cfg;
struct Device* cdc_child = nullptr; // resolved once in start_device(); nullptr if no usbdevicecdc0 node
};
static void route_phy_for_device_mode() {
#if CONFIG_IDF_TARGET_ESP32P4
// Tab5's USB-C is wired to ESP32-P4 FSLS PHY0 (GPIO24/25). ESP-IDF's default USB_WRAP
// route uses FSLS PHY1 (GPIO26/27), so switch it here before installing TinyUSB.
usb_wrap_ll_phy_select(&USB_WRAP, 0);
#endif
}
static void restore_default_phy_route() {
#if CONFIG_IDF_TARGET_ESP32P4
usb_wrap_ll_phy_select(&USB_WRAP, 1);
#elif CONFIG_IDF_TARGET_ESP32S3
// S2/S3 share one FSLS PHY between USB-OTG and USB-Serial-JTAG. esp_tinyusb's teardown
// leaves the PHY muxed to USB-OTG \afterward, so the native console never comes back
// without this.
//
// \afterward ESP-IDF's usb_del_phy() (called from tinyusb_driver_uninstall() above).
usb_serial_jtag_ll_phy_enable_external(false);
#endif
}
static bool find_cdc_child(struct Device* child, void* context) {
if (device_get_type(child) == &USB_CDC_DEVICE_TYPE) {
*static_cast<struct Device**>(context) = child;
return false; // stop iterating
}
return true;
}
// Devicetree children are constructed/added/started strictly after their parent
// (kernel_init.cpp's dts_devices[] loop starts each device in list order, parent first) - so the
// usbdevicecdc0 child does not exist yet when this controller's own start_device() runs.
// Discovering it lazily here (called from is_cdc_enabled(), well after all devicetree devices
// have finished starting) instead of once at start_device() time is the only way to actually see
// it. Cheap to re-scan every call: at most one child of this type per board.
static struct Device* find_cdc_child_lazy(struct Device* device, struct UsbDeviceControllerCtx* ctx) {
if (ctx->cdc_child == nullptr) {
device_for_each_child(device, &ctx->cdc_child, find_cdc_child);
}
return ctx->cdc_child;
}
// ---- Controller API ----
static error_t begin_claim(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
LOG_E(TAG, "begin_claim: slot busy (active_class=%d)", ctx->active_class);
return ERROR_RESOURCE_BUSY;
}
ctx->allocation_open = true;
ctx->next_interface = 0;
ctx->next_in_endpoint = 1;
ctx->next_out_endpoint = 1;
return ERROR_NONE;
}
static error_t allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (!ctx->allocation_open) {
LOG_E(TAG, "allocate_interfaces: called without a preceding begin_claim()");
return ERROR_INVALID_STATE;
}
out_allocation->first_interface_number = ctx->next_interface;
out_allocation->first_in_endpoint = in_endpoint_count > 0 ? (uint8_t)(0x80 | ctx->next_in_endpoint) : 0;
out_allocation->first_out_endpoint = out_endpoint_count > 0 ? ctx->next_out_endpoint : 0;
ctx->next_interface = (uint8_t)(ctx->next_interface + interface_count);
ctx->next_in_endpoint = (uint8_t)(ctx->next_in_endpoint + in_endpoint_count);
ctx->next_out_endpoint = (uint8_t)(ctx->next_out_endpoint + out_endpoint_count);
return ERROR_NONE;
}
static bool is_cdc_enabled(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
struct Device* cdc_child = find_cdc_child_lazy(device, ctx);
return cdc_child != nullptr && usb_cdc_device_is_present(cdc_child);
}
static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const struct UsbDeviceClaimConfig* config) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
LOG_E(TAG, "claim: slot busy (active_class=%d, requested=%d)", ctx->active_class, usb_class);
return ERROR_RESOURCE_BUSY;
}
if (config == nullptr || config->device_descriptor == nullptr || config->primary.descriptor_bytes == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
if (!ctx->allocation_open) {
LOG_E(TAG, "claim: no begin_claim()/allocate_interfaces() session in progress");
return ERROR_INVALID_STATE;
}
// MSC is deliberately excluded from CDC compositing - a MSC-presenting device is meant to
// look/behave like plain mass storage to the host's storage stack, and there's no real use
// case for a console interface while acting as a flash drive, unlike HID/MIDI which
// plausibly want a console alongside. (A Windows safe-eject hang was seen during development
// of this compositing code but traced to a stale Windows-side driver/device-cache state,
// cleared by a host reboot - not caused by CDC compositing or this exclusion. Kept the
// exclusion anyway since it matches MSC's actual intended behavior.)
const bool cdc_enabled = usb_class != USB_DEVICE_CLASS_MSC && is_cdc_enabled(device);
// ---- String table: primary's table, plus CDC's own interface string appended last.
size_t string_count = config->string_descriptor_count;
if (string_count > COMPOSITE_STRING_DESCRIPTOR_MAX) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: primary string table too large (%u > %u)", (unsigned)string_count, (unsigned)COMPOSITE_STRING_DESCRIPTOR_MAX);
return ERROR_INVALID_ARGUMENT;
}
memcpy(ctx->composite_string_descriptor, config->string_descriptor, string_count * sizeof(const char*));
struct UsbInterfaceContribution cdc_contribution = {};
if (cdc_enabled) {
const char* cdc_interface_string = nullptr;
if (string_count >= COMPOSITE_STRING_DESCRIPTOR_MAX) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: no room for CDC's interface string");
return ERROR_INVALID_ARGUMENT;
}
error_t err = usb_cdc_device_build_contribution(ctx->cdc_child, device, (uint8_t)string_count,
&cdc_contribution, &cdc_interface_string);
if (err != ERROR_NONE) {
ctx->allocation_open = false;
LOG_E(TAG, "claim: CDC build_contribution failed");
return err;
}
ctx->composite_string_descriptor[string_count] = cdc_interface_string;
string_count++;
}
// ---- Descriptor byte assembly: primary's bytes, then CDC's (if enabled), behind one config
// header. Built twice (fs/hs) under TUD_OPT_HIGH_SPEED if the primary supplied distinct
// high-speed bytes (see UsbInterfaceContribution::hs_descriptor_bytes) - CDC never varies by
// speed, so its bytes are reused verbatim in both buffers.
auto assemble = [&](uint8_t* dest, size_t dest_capacity, const uint8_t* primary_bytes, size_t primary_len) -> error_t {
const size_t total_len = TUD_CONFIG_DESC_LEN + primary_len + (cdc_enabled ? cdc_contribution.descriptor_bytes_len : 0);
if (total_len > dest_capacity) {
LOG_E(TAG, "claim: composite descriptor too large (%u > %u)", (unsigned)total_len, (unsigned)dest_capacity);
return ERROR_INVALID_ARGUMENT;
}
const uint8_t config_header[] = {
TUD_CONFIG_DESCRIPTOR(1, ctx->next_interface, 0, total_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
};
uint8_t* p = dest;
memcpy(p, config_header, sizeof(config_header));
p += sizeof(config_header);
memcpy(p, primary_bytes, primary_len);
p += primary_len;
if (cdc_enabled) {
memcpy(p, cdc_contribution.descriptor_bytes, cdc_contribution.descriptor_bytes_len);
}
return ERROR_NONE;
};
error_t assemble_err = assemble(ctx->composite_fs_config_descriptor, sizeof(ctx->composite_fs_config_descriptor),
config->primary.descriptor_bytes, config->primary.descriptor_bytes_len);
if (assemble_err != ERROR_NONE) {
ctx->allocation_open = false;
return assemble_err;
}
#if (TUD_OPT_HIGH_SPEED)
const uint8_t* hs_primary_bytes = config->primary.hs_descriptor_bytes != nullptr
? config->primary.hs_descriptor_bytes : config->primary.descriptor_bytes;
const size_t hs_primary_len = config->primary.hs_descriptor_bytes != nullptr
? config->primary.hs_descriptor_bytes_len : config->primary.descriptor_bytes_len;
assemble_err = assemble(ctx->composite_hs_config_descriptor, sizeof(ctx->composite_hs_config_descriptor),
hs_primary_bytes, hs_primary_len);
if (assemble_err != ERROR_NONE) {
ctx->allocation_open = false;
return assemble_err;
}
#endif
ctx->allocation_open = false;
// ---- Device descriptor: primary's metadata (idVendor/idProduct/strings), controller decides
// the class triad since CDC's presence (not the primary's identity) is what needs IAD in the
// general case - except MSC, which always got MISC/IAD unconditionally even standalone in
// the pre-refactor code (see esp32_usb_device_msc.cpp history). Kept that behavior verbatim
// for MSC (regardless of cdc_enabled, which is always false for MSC anyway - see the
// cdc_enabled computation above) simply to match what shipped before rather than introduce
// an unrelated behavior change while separating CDC out of HID's descriptor.
ctx->composite_device_descriptor = *static_cast<const tusb_desc_device_t*>(config->device_descriptor);
if (cdc_enabled || usb_class == USB_DEVICE_CLASS_MSC) {
ctx->composite_device_descriptor.bDeviceClass = TUSB_CLASS_MISC;
ctx->composite_device_descriptor.bDeviceSubClass = MISC_SUBCLASS_COMMON;
ctx->composite_device_descriptor.bDeviceProtocol = MISC_PROTOCOL_IAD;
} else {
ctx->composite_device_descriptor.bDeviceClass = TUSB_CLASS_UNSPECIFIED;
ctx->composite_device_descriptor.bDeviceSubClass = 0x00;
ctx->composite_device_descriptor.bDeviceProtocol = 0x00;
}
ctx->composite_tusb_cfg = {};
ctx->composite_tusb_cfg.device_descriptor = &ctx->composite_device_descriptor;
ctx->composite_tusb_cfg.string_descriptor = ctx->composite_string_descriptor;
ctx->composite_tusb_cfg.string_descriptor_count = string_count;
ctx->composite_tusb_cfg.external_phy = false;
#if (TUD_OPT_HIGH_SPEED)
ctx->composite_tusb_cfg.fs_configuration_descriptor = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.hs_configuration_descriptor = ctx->composite_hs_config_descriptor;
// The qualifier descriptor's class triad must mirror the device descriptor's (same IAD
// reasoning) - built here rather than supplied per-primary since its content is boilerplate
// (same bMaxPacketSize0/bNumConfigurations for every class) and the controller already knows
// the class triad.
ctx->composite_device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = ctx->composite_device_descriptor.bDeviceClass,
.bDeviceSubClass = ctx->composite_device_descriptor.bDeviceSubClass,
.bDeviceProtocol = ctx->composite_device_descriptor.bDeviceProtocol,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0x00,
};
ctx->composite_tusb_cfg.qualifier_descriptor = &ctx->composite_device_qualifier;
#else
ctx->composite_tusb_cfg.configuration_descriptor = ctx->composite_fs_config_descriptor;
#endif
ctx->composite_tusb_cfg.self_powered = false;
ctx->composite_tusb_cfg.vbus_monitor_io = 0;
route_phy_for_device_mode();
ctx->phy_routed = true;
if (tinyusb_driver_install(&ctx->composite_tusb_cfg) != ESP_OK) {
LOG_E(TAG, "claim: tinyusb_driver_install failed for class %d", usb_class);
if (ctx->phy_routed) {
restore_default_phy_route();
ctx->phy_routed = false;
}
return ERROR_RESOURCE;
}
ctx->active_class = usb_class;
if (cdc_enabled) {
if (usb_cdc_device_start_console(ctx->cdc_child) == ERROR_NONE) {
ctx->cdc_console_started = true;
} else {
LOG_E(TAG, "claim: CDC start_console failed - continuing without console");
}
}
return ERROR_NONE;
}
static error_t release(struct Device* device, enum UsbDeviceClass usb_class) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
if (ctx->active_class != usb_class) {
LOG_E(TAG, "release: class %d does not hold the slot (active=%d)", usb_class, ctx->active_class);
return ERROR_INVALID_STATE;
}
if (ctx->cdc_console_started) {
usb_cdc_device_stop_console(ctx->cdc_child);
ctx->cdc_console_started = false;
}
// Signal disconnect before tearing down so the host notices promptly.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(250));
tinyusb_driver_uninstall();
if (ctx->phy_routed) {
restore_default_phy_route();
ctx->phy_routed = false;
}
ctx->active_class = USB_DEVICE_CLASS_NONE;
return ERROR_NONE;
}
static enum UsbDeviceClass get_active_class(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
return ctx->active_class;
}
extern const UsbDeviceControllerApi usb_device_controller_api = {
.begin_claim = begin_claim,
.allocate_interfaces = allocate_interfaces,
.claim = claim,
.release = release,
.get_active_class = get_active_class,
.is_cdc_enabled = is_cdc_enabled,
};
// ---- Driver lifecycle ----
// This device is defined in each board's .dts (usbdevice0), same pattern as usbhost0 - its
// child classes (usbdevicehid0, usbdevicemsc0, usbdevicemidi0, usbdevicecdc0) are separate .dts
// nodes with their own drivers, wired to this device as their parent by the devicetree compiler.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbDeviceControllerCtx();
device_set_driver_data(device, ctx);
// cdc_child is NOT resolved here: usbdevicecdc0 (a child of this device in the devicetree)
// hasn't been constructed/started yet at this point - kernel_init.cpp starts devicetree
// devices in list order, parent before children. Resolved lazily instead, see
// find_cdc_child_lazy() / is_cdc_enabled().
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
// Safety cleanup: if a class still holds the slot (e.g. this device is stopped while HID/
// MSC/MIDI is still active), tear TinyUSB down and restore the PHY route before freeing ctx -
// otherwise the installed TinyUSB driver and mis-routed PHY would outlive the context that
// tracks them. Mirrors the same safety-release pattern each child driver's own stop_device
// uses (see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp / esp32_usb_midi_device.cpp).
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
release(device, ctx->active_class);
}
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_device_controller_driver = {
.name = "esp32_usb_device_controller",
.compatible = (const char*[]) { "espressif,esp32-usbdevice", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &usb_device_controller_api,
.device_type = &USB_DEVICE_CONTROLLER_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && (CONFIG_TINYUSB_HID_COUNT || CONFIG_TINYUSB_MSC_ENABLED || CONFIG_TINYUSB_MIDI_COUNT)
@@ -0,0 +1,250 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <wear_levelling.h>
#include <cstring>
#define TAG "esp32_usb_device_msc"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- MSC device descriptor set ----
// Mutable, matching the other primary classes' descriptors, even though MSC's own
// bDeviceClass/SubClass/Protocol below are never actually rewritten by the controller - MSC is
// excluded from CDC compositing (see esp32_usb_device_controller.cpp's claim(), cdc_enabled
// computation) and always keeps its own unconditional MISC/IAD triad regardless.
static tusb_desc_device_t msc_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4002,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char* msc_string_descriptor[] = {
(const char[]) { 0x09, 0x04 }, // 0: English (0x0409)
"Tactility", // 1: Manufacturer
"Tactility Device", // 2: Product
"42", // 3: Serial
"Tactility Mass Storage", // 4: MSC
};
// Interface/endpoint numbers here are the values usb_device_controller_allocate_interfaces()
// always returns for MSC in practice (MSC is always the first/only allocation for itself, per
// the controller's fixed primary-first ordering) - still requested via allocate_interfaces() at
// claim time rather than assumed, so this stays correct if that ordering ever changes.
static uint8_t msc_fs_configuration_descriptor[TUD_MSC_DESC_LEN];
#if (TUD_OPT_HIGH_SPEED)
static uint8_t msc_hs_configuration_descriptor[TUD_MSC_DESC_LEN];
#endif
// ---- MSC device state ----
struct UsbMscDeviceCtx {
UsbMscDeviceMountChangedCallback mount_changed_cb = nullptr;
void* mount_changed_context = nullptr;
bool storage_active = false;
};
// device pointer isn't threaded through the TinyUSB mount-changed callback, so this driver
// supports a single active MSC device instance at a time - matches the single TinyUSB
// device-mode slot the controller already enforces.
static struct Device* active_msc_device = nullptr;
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
if (active_msc_device == nullptr) return;
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(active_msc_device));
if (ctx == nullptr) return;
const bool mounted = event->mount_changed_data.is_mounted;
LOG_I(TAG, "%s", mounted ? "MSC mounted" : "MSC unmounted");
if (ctx->mount_changed_cb != nullptr) {
ctx->mount_changed_cb(mounted, ctx->mount_changed_context);
}
}
// ---- MSC device API ----
static error_t msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context) {
if (source_handle == nullptr) {
return ERROR_INVALID_ARGUMENT;
}
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/1,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/1, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
const uint8_t msc_fs_bytes[] = {
TUD_MSC_DESCRIPTOR(alloc.first_interface_number, 4, alloc.first_out_endpoint, alloc.first_in_endpoint, 64),
};
memcpy(msc_fs_configuration_descriptor, msc_fs_bytes, sizeof(msc_fs_bytes));
#if (TUD_OPT_HIGH_SPEED)
const uint8_t msc_hs_bytes[] = {
TUD_MSC_DESCRIPTOR(alloc.first_interface_number, 4, alloc.first_out_endpoint, alloc.first_in_endpoint, 512),
};
memcpy(msc_hs_configuration_descriptor, msc_hs_bytes, sizeof(msc_hs_bytes));
#endif
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &msc_device_descriptor;
claim_config.string_descriptor = msc_string_descriptor;
claim_config.string_descriptor_count = sizeof(msc_string_descriptor) / sizeof(msc_string_descriptor[0]);
claim_config.primary.descriptor_bytes = msc_fs_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(msc_fs_configuration_descriptor);
#if (TUD_OPT_HIGH_SPEED)
claim_config.primary.hs_descriptor_bytes = msc_hs_configuration_descriptor;
claim_config.primary.hs_descriptor_bytes_len = sizeof(msc_hs_configuration_descriptor);
#endif
claim_config.primary.interface_count = 1;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 1;
error_t claim_result = usb_device_controller_claim(controller, USB_DEVICE_CLASS_MSC, &claim_config);
if (claim_result != ERROR_NONE) {
return claim_result;
}
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
ctx->mount_changed_cb = mount_changed_cb;
ctx->mount_changed_context = context;
active_msc_device = device;
esp_err_t result;
if (source == USB_MSC_DEVICE_SOURCE_SDMMC) {
const tinyusb_msc_sdmmc_config_t config_sdmmc = {
.card = static_cast<sdmmc_card_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_sdmmc(&config_sdmmc);
} else {
const tinyusb_msc_spiflash_config_t config_flash = {
.wl_handle = *static_cast<wl_handle_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_spiflash(&config_flash);
}
if (result != ESP_OK) {
LOG_E(TAG, "storage init failed: %s", esp_err_to_name(result));
active_msc_device = nullptr;
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
return ERROR_RESOURCE;
}
ctx->storage_active = true;
return ERROR_NONE;
}
static error_t msc_device_stop(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
if (ctx == nullptr || !ctx->storage_active) {
return ERROR_NONE;
}
tinyusb_msc_storage_deinit();
auto* controller = device_get_parent(device);
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
ctx->storage_active = false;
ctx->mount_changed_cb = nullptr;
ctx->mount_changed_context = nullptr;
if (active_msc_device == device) {
active_msc_device = nullptr;
}
return ERROR_NONE;
}
static bool msc_device_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
return ctx != nullptr && ctx->storage_active && tud_mounted();
}
extern const UsbMscDeviceApi esp32_usb_msc_device_api = {
.start = msc_device_start,
.stop = msc_device_stop,
.is_connected = msc_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicemsc0).
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
auto* ctx = new UsbMscDeviceCtx();
device_set_driver_data(device, ctx);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(device));
if (ctx != nullptr && ctx->storage_active) {
msc_device_stop(device);
}
delete ctx;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver esp32_usb_msc_device_driver = {
.name = "esp32_usb_msc_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-msc", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_msc_device_api,
.device_type = &USB_MSC_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MSC_ENABLED
@@ -0,0 +1,352 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/hid_report_descriptors.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstring>
#define TAG "esp32_usb_hid_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- HID Report Maps ----
// Defined once in Platforms/platform-esp32/source/drivers/hid_report_descriptors.cpp and shared
// with esp32_ble_hid.cpp - HID report descriptors are transport-independent (same USB HID Usage
// Tables spec applies to both USB and BLE HID), so the same bytes work verbatim over either
// transport.
// Report IDs, matching the shared hid_report_map_* tables.
static constexpr uint8_t REPORT_ID_KEYBOARD = 1;
static constexpr uint8_t REPORT_ID_CONSUMER = 2;
static constexpr uint8_t REPORT_ID_MOUSE_SOLO = 1; // USB_HID_DEVICE_MODE_MOUSE
static constexpr uint8_t REPORT_ID_MOUSE_COMBO = 3; // USB_HID_DEVICE_MODE_KEYBOARD_MOUSE
static constexpr uint8_t REPORT_ID_GAMEPAD = 1;
// Active mode/report-map state - the report descriptor TinyUSB serves via
// tud_hid_descriptor_report_cb() must match whichever mode start() was last called with, since
// (unlike BLE's mutable GATT) USB's descriptor is fixed for the lifetime of one claim()
// session; switching modes means stop() + start() with the new mode, re-enumerating.
static enum UsbHidDeviceMode active_mode = USB_HID_DEVICE_MODE_KEYBOARD;
static const uint8_t* active_report_map() {
switch (active_mode) {
case USB_HID_DEVICE_MODE_MOUSE: return hid_report_map_mouse;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return hid_report_map_keyboard_consumer_mouse;
case USB_HID_DEVICE_MODE_GAMEPAD: return hid_report_map_gamepad;
default: return hid_report_map_keyboard_consumer; // KEYBOARD
}
}
static size_t active_report_map_len() {
switch (active_mode) {
case USB_HID_DEVICE_MODE_MOUSE: return hid_report_map_mouse_len;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return hid_report_map_keyboard_consumer_mouse_len;
case USB_HID_DEVICE_MODE_GAMEPAD: return hid_report_map_gamepad_len;
default: return hid_report_map_keyboard_consumer_len;
}
}
// Per-mode default product name and idProduct offset. Distinct idProduct per mode matters more
// than it might seem: most hosts (Windows in particular) cache device metadata - including
// which icon/appearance to show - keyed by VID/PID/serial, and a HID descriptor swap alone
// isn't always enough to make the host re-read it on a fast re-enumeration. Giving each mode
// its own PID makes the host treat a mode switch as connecting a genuinely different device,
// which reliably clears the stale-appearance problem (was previously showing "Keyboard" for
// every mode almost all the time).
static const char* default_name_for_mode(enum UsbHidDeviceMode mode) {
switch (mode) {
case USB_HID_DEVICE_MODE_MOUSE: return "Tactility Mouse";
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return "Tactility Keyboard+Mouse";
case USB_HID_DEVICE_MODE_GAMEPAD: return "Tactility Gamepad";
default: return "Tactility Keyboard"; // KEYBOARD
}
}
static uint16_t product_id_for_mode(enum UsbHidDeviceMode mode) {
switch (mode) {
case USB_HID_DEVICE_MODE_MOUSE: return 0x4006;
case USB_HID_DEVICE_MODE_KEYBOARD_MOUSE: return 0x4007;
case USB_HID_DEVICE_MODE_GAMEPAD: return 0x4008;
default: return 0x4004; // KEYBOARD (unchanged - existing PID)
}
}
// Set via usb_hid_device_set_name() before start(); empty means "use the mode's default name".
// Mirrors bluetooth_set_device_name()'s pattern of being set once before starting the profile.
static char custom_name[32] = {};
// HID's own interface/endpoint numbers come from allocate_interfaces() at claim time
// (hid_device_start()) - CDC, if the board's usbdevicecdc0 child is enabled,
// is composited in by the USB device controller itself after HID's own contribution,
// so HID no longer needs to know or care whether CDC exists.
// The report descriptor length varies by mode (KEYBOARD_MOUSE's combined map is the largest),
// so the config descriptor's total length is computed at claim time in hid_device_start().
// Built fresh in start() (see build_hid_device_descriptor()) so idProduct/iProduct can vary per
// mode - see default_name_for_mode()/product_id_for_mode() above for why.
// Mutable for the same reason, plus: the USB device controller patches bDeviceClass/SubClass/Protocol at
// claim() time depending on whether CDC is composited in.
static tusb_desc_device_t hid_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_UNSPECIFIED,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4004, // overwritten per-mode in start()
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char hid_langid_descriptor[] = { 0x09, 0x04 };
// index 2 (iProduct) is overwritten per-mode/per-custom-name in start(), see
// build_hid_device_descriptor().
static char hid_product_string[sizeof(custom_name)] = "Tactility Keyboard";
// index 4 is the HID interface string, read directly by some HID apps/testers (gamepad testers, joystick tools) instead of iProduct (index 2).
// Point it at the same buffer as iProduct so both stay in sync with whatever mode/custom name is active.
static const char* hid_string_descriptor[] = {
hid_langid_descriptor, "Tactility", hid_product_string, "123456", hid_product_string,
};
// Built fresh in hid_device_start(), sized for the active mode's report map. TinyUSB only reads
// this once during tinyusb_driver_install() (inside claim()), so it's safe to mutate between
// claim sessions (i.e. stop() + start() with a different mode) as long as it's finalized before
// claim() is called.
static uint8_t hid_configuration_descriptor[TUD_HID_DESC_LEN];
// ---- TinyUSB HID callbacks (required by the TinyUSB HID class driver) ----
extern "C" {
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
(void)instance;
return active_report_map();
}
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
uint8_t* buffer, uint16_t reqlen) {
(void)instance; (void)report_id; (void)report_type; (void)buffer; (void)reqlen;
return 0;
}
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
uint8_t const* buffer, uint16_t bufsize) {
(void)instance; (void)report_id; (void)report_type; (void)buffer; (void)bufsize;
// LED state (caps/num/scroll lock) - not currently surfaced to callers.
}
} // extern "C"
// ---- HID device API ----
// Fills in idProduct/iProduct for the given mode, honoring a set_name() override if one was
// set. Must run before every claim() - the name/PID need to match whatever mode is about to
// start, not whatever they were left as from a previous session.
static void build_hid_device_descriptor(enum UsbHidDeviceMode mode) {
hid_device_descriptor.idProduct = product_id_for_mode(mode);
const char* name = custom_name[0] ? custom_name : default_name_for_mode(mode);
strncpy(hid_product_string, name, sizeof(hid_product_string) - 1);
hid_product_string[sizeof(hid_product_string) - 1] = '\0';
}
static error_t hid_device_start(struct Device* device, enum UsbHidDeviceMode mode) {
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/1,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/0, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
// active_mode/hid_device_descriptor commit only once resources are reserved - reads of
// active_mode (tud_hid_descriptor_report_cb(), the mode gates in send_*) must keep matching
// whatever session is actually installed if begin_claim()/allocate_interfaces() above failed.
active_mode = mode;
build_hid_device_descriptor(mode);
const uint8_t hid_bytes[] = {
TUD_HID_DESCRIPTOR(alloc.first_interface_number, 4, HID_ITF_PROTOCOL_NONE,
active_report_map_len(), alloc.first_in_endpoint, 16, 10),
};
memcpy(hid_configuration_descriptor, hid_bytes, sizeof(hid_bytes));
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &hid_device_descriptor;
claim_config.string_descriptor = hid_string_descriptor;
claim_config.string_descriptor_count = sizeof(hid_string_descriptor) / sizeof(hid_string_descriptor[0]);
claim_config.primary.descriptor_bytes = hid_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(hid_configuration_descriptor);
claim_config.primary.interface_count = 1;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 0;
return usb_device_controller_claim(controller, USB_DEVICE_CLASS_HID_KEYBOARD, &claim_config);
}
static error_t hid_device_stop(struct Device* device) {
auto* controller = device_get_parent(device);
return usb_device_controller_release(controller, USB_DEVICE_CLASS_HID_KEYBOARD);
}
static error_t hid_device_set_name(struct Device* device, const char* name) {
(void)device;
if (name == nullptr) {
custom_name[0] = '\0'; // clear override, fall back to the mode's default name
return ERROR_NONE;
}
strncpy(custom_name, name, sizeof(custom_name) - 1);
custom_name[sizeof(custom_name) - 1] = '\0';
return ERROR_NONE;
}
// tud_hid_report() fails outright (no queueing) if the endpoint is still busy sending a
// previous report - a single fire-and-forget call is unsafe whenever a caller sends two reports
// back to back (e.g. press immediately followed by release: the second call can lose the race and drop the release, leaving
// the host thinking a key/button/consumer-control is still held down (which then either does
// nothing further or, for keys the host auto-repeats, keeps repeating). Retry with a short
// backoff instead of failing immediately.
static error_t hid_send_report(uint8_t report_id, const uint8_t* report, size_t len) {
if (!tud_mounted()) {
LOG_W(TAG, "hid_send_report(id=%d): tud_mounted() false - real disconnect, not just a busy endpoint", report_id);
return ERROR_INVALID_STATE;
}
for (int attempt = 0; attempt < 10; attempt++) {
if (tud_hid_ready() && tud_hid_report(report_id, report, static_cast<uint16_t>(len))) {
return ERROR_NONE;
}
vTaskDelay(pdMS_TO_TICKS(5));
}
LOG_W(TAG, "hid_send_report(id=%d): gave up after 10 retries (~50ms) - endpoint stayed busy/not-ready the whole time", report_id);
return ERROR_RESOURCE;
}
static error_t hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_KEYBOARD && active_mode != USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[8] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_KEYBOARD, buf, sizeof(buf));
}
static error_t hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_KEYBOARD && active_mode != USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[2] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_CONSUMER, buf, sizeof(buf));
}
static error_t hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
uint8_t report_id;
if (active_mode == USB_HID_DEVICE_MODE_MOUSE) {
report_id = REPORT_ID_MOUSE_SOLO;
} else if (active_mode == USB_HID_DEVICE_MODE_KEYBOARD_MOUSE) {
report_id = REPORT_ID_MOUSE_COMBO;
} else {
return ERROR_NOT_SUPPORTED;
}
uint8_t buf[4] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(report_id, buf, sizeof(buf));
}
static error_t hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
(void)device;
if (active_mode != USB_HID_DEVICE_MODE_GAMEPAD) {
return ERROR_NOT_SUPPORTED;
}
// 8 bytes: X,Y,Rx,Ry,Z (1 each - Xbox-360-style, Z shared by triggers), hat/dpad (1),
// buttons[2] (10 buttons + 6 padding bits). See hid_report_descriptors.cpp for the full
// layout/rationale.
uint8_t buf[8] = {};
memcpy(buf, report, len < sizeof(buf) ? len : sizeof(buf));
return hid_send_report(REPORT_ID_GAMEPAD, buf, sizeof(buf));
}
static bool hid_device_is_connected(struct Device* device) {
(void)device;
// tud_hid_ready() deliberately NOT included here: it reflects whether the endpoint is idle
// right now (false during every in-flight report send, which is normal and momentary, not
// a disconnect), not whether a host is actually connected. Including it made this function
// flap constantly under any traffic - explains the "Connected" <-> "waiting for host..."
// flicker apps were seeing on every keypress/click even though nothing was actually wrong
// (hid_send_report()'s own retry logic was succeeding the whole time; no warning ever
// logged because there was nothing to warn about). tud_mounted() alone is the correct
// "is a host connected" signal.
return tud_mounted();
}
extern const UsbHidDeviceApi esp32_usb_hid_device_api = {
.start = hid_device_start,
.stop = hid_device_stop,
.set_name = hid_device_set_name,
.send_keyboard = hid_device_send_keyboard,
.send_consumer = hid_device_send_consumer,
.send_mouse = hid_device_send_mouse,
.send_gamepad = hid_device_send_gamepad,
.is_connected = hid_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicehid0) - the devicetree
// compiler wires device_get_parent() to the controller automatically.
extern "C" {
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
// Safety cleanup: release the slot if it's still held (e.g. this device is stopped while
// HID is still active).
auto* controller = device_get_parent(device);
if (controller != nullptr && usb_device_controller_get_active_class(controller) == USB_DEVICE_CLASS_HID_KEYBOARD) {
usb_device_controller_release(controller, USB_DEVICE_CLASS_HID_KEYBOARD);
}
return ERROR_NONE;
}
Driver esp32_usb_hid_device_driver = {
.name = "esp32_usb_hid_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-hid", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_hid_device_api,
.device_type = &USB_HID_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_HID_COUNT
@@ -0,0 +1,195 @@
#include <sdkconfig.h>
#if CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbdevice.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb.h>
#include <cstring>
#define TAG "esp32_usb_midi_device"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// ---- MIDI device descriptor set ----
// TUD_MIDI_DESCRIPTOR consumes 2 interfaces internally (Audio Control + MIDI Streaming,
// itfnum and itfnum+1 - see TUD_MIDI_DESC_HEAD in usbd.h), unlike HID/MSC/CDC which use 1
// each - allocate_interfaces(2, 1, 1, ...) in midi_device_start() reflects that.
// Mutable: the USB device controller patches bDeviceClass/SubClass/Protocol at
// claim() time depending on whether CDC is composited in (see esp32_usb_device_controller.cpp).
static tusb_desc_device_t midi_device_descriptor = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // Espressif VID
.idProduct = 0x4005,
.bcdDevice = 0x0100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01,
};
static const char midi_langid_descriptor[] = { 0x09, 0x04 };
// index 2 (iProduct) is overwritten by set_name() before start(), see
// midi_device_set_name()/midi_device_start(). Index 4 is the jack name (see
// midi_configuration_descriptor below).
//
// The interface string index is literal 0 ("no string" - a real, well-defined USB concept: the
// host skips string lookup entirely, distinct from an empty string AT a valid index), not an
// index into this table at all. An earlier attempt used an empty "" entry at a real index
// instead of literal 0 - that crashed the device outright on Windows (Code 10,
// STATUS_DEVICE_DATA_ERROR): empty-string-at-a-valid-index and "no string" (index 0) are not
// the same thing, only the latter is safe. Both Windows' newer MIDI naming and MIDIBerry were
// confirmed (live test) to concatenate interface-string + jack-string into one displayed name,
// so leaving the interface string as "no string" (0) means only the jack string shows, instead
// of duplicating/prefixing the product name.
static char midi_product_string[32] = "Tactility MIDI Device";
static const char* midi_string_descriptor[] = {
midi_langid_descriptor, "Tactility", midi_product_string, "123456", midi_product_string,
};
static constexpr uint8_t MIDI_STRIDX_JACK = 4;
// TUD_MIDI_DESCRIPTOR (the usbd.h convenience macro) hardcodes its jack descriptors' string
// index to 0 (no string) - it isn't a parameter the macro exposes. Built by hand here with a
// real string index for the jack (instead of the macro's hardcoded 0), while keeping the
// interface string index at literal 0 (see comment above on midi_string_descriptor for why 0,
// not an empty table entry). Interface/endpoint numbers come from allocate_interfaces() at claim
// time rather than compile-time constants, since CDC may now be composited in after MIDI.
static uint8_t midi_configuration_descriptor[TUD_MIDI_DESC_LEN];
// ---- MIDI device API ----
static error_t midi_device_start(struct Device* device) {
auto* controller = device_get_parent(device);
error_t begin_result = usb_device_controller_begin_claim(controller);
if (begin_result != ERROR_NONE) {
return begin_result;
}
struct UsbInterfaceAllocation alloc;
error_t alloc_result = usb_device_controller_allocate_interfaces(controller, /*interface_count=*/2,
/*in_endpoint_count=*/1, /*out_endpoint_count=*/1, &alloc);
if (alloc_result != ERROR_NONE) {
return alloc_result;
}
const uint8_t itf_ac = alloc.first_interface_number;
const uint8_t midi_bytes[] = {
TUD_MIDI_DESC_HEAD(itf_ac, 0, 1),
TUD_MIDI_DESC_JACK_DESC(1, MIDI_STRIDX_JACK),
TUD_MIDI_DESC_EP(alloc.first_out_endpoint, 64, 1),
TUD_MIDI_JACKID_IN_EMB(1),
TUD_MIDI_DESC_EP(alloc.first_in_endpoint, 64, 1),
TUD_MIDI_JACKID_OUT_EMB(1),
};
memcpy(midi_configuration_descriptor, midi_bytes, sizeof(midi_bytes));
struct UsbDeviceClaimConfig claim_config = {};
claim_config.device_descriptor = &midi_device_descriptor;
claim_config.string_descriptor = midi_string_descriptor;
claim_config.string_descriptor_count = sizeof(midi_string_descriptor) / sizeof(midi_string_descriptor[0]);
claim_config.primary.descriptor_bytes = midi_configuration_descriptor;
claim_config.primary.descriptor_bytes_len = sizeof(midi_configuration_descriptor);
claim_config.primary.interface_count = 2;
claim_config.primary.in_endpoint_count = 1;
claim_config.primary.out_endpoint_count = 1;
return usb_device_controller_claim(controller, USB_DEVICE_CLASS_MIDI, &claim_config);
}
static error_t midi_device_stop(struct Device* device) {
auto* controller = device_get_parent(device);
return usb_device_controller_release(controller, USB_DEVICE_CLASS_MIDI);
}
static error_t midi_device_set_name(struct Device* device, const char* name) {
(void)device;
if (name == nullptr) {
strncpy(midi_product_string, "Tactility MIDI Device", sizeof(midi_product_string) - 1);
midi_product_string[sizeof(midi_product_string) - 1] = '\0';
return ERROR_NONE;
}
strncpy(midi_product_string, name, sizeof(midi_product_string) - 1);
midi_product_string[sizeof(midi_product_string) - 1] = '\0';
return ERROR_NONE;
}
static error_t midi_device_send(struct Device* device, const uint8_t* msg, size_t len) {
auto* controller = device_get_parent(device);
if (controller == nullptr || usb_device_controller_get_active_class(controller) != USB_DEVICE_CLASS_MIDI) {
return ERROR_INVALID_STATE;
}
if (!tud_mounted()) {
return ERROR_INVALID_STATE;
}
uint32_t written = tud_midi_stream_write(0, msg, static_cast<uint32_t>(len));
return written == len ? ERROR_NONE : ERROR_RESOURCE;
}
static bool midi_device_is_connected(struct Device* device) {
auto* controller = device_get_parent(device);
if (controller == nullptr || usb_device_controller_get_active_class(controller) != USB_DEVICE_CLASS_MIDI) {
return false;
}
return tud_mounted();
}
extern const UsbMidiDeviceApi esp32_usb_midi_device_api = {
.start = midi_device_start,
.stop = midi_device_stop,
.set_name = midi_device_set_name,
.send = midi_device_send,
.is_connected = midi_device_is_connected,
};
// ---- Driver lifecycle ----
// Defined in each board's .dts as a child of usbdevice0 (e.g. usbdevicemidi0) - the devicetree
// compiler wires device_get_parent() to the controller automatically.
extern "C" {
// tud_midi_rx_cb is intentionally not implemented: this driver is send-only, matching
// bluetooth_midi.h's API shape (no receive exposed at the Tactility layer either). Incoming
// bytes are simply left in TinyUSB's RX FIFO and eventually overwritten/dropped.
static error_t start_device(struct Device* device) {
(void)GET_CONFIG(device); // no configuration - placeholder only
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
// Safety cleanup: release the slot if it's still held (e.g. this device is stopped while
// MIDI is still active).
auto* controller = device_get_parent(device);
if (controller != nullptr && usb_device_controller_get_active_class(controller) == USB_DEVICE_CLASS_MIDI) {
usb_device_controller_release(controller, USB_DEVICE_CLASS_MIDI);
}
return ERROR_NONE;
}
Driver esp32_usb_midi_device_driver = {
.name = "esp32_usb_midi_device",
.compatible = (const char*[]) { "espressif,esp32-usbdevice-midi", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &esp32_usb_midi_device_api,
.device_type = &USB_MIDI_DEVICE_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED && CONFIG_TINYUSB_MIDI_COUNT
@@ -43,6 +43,21 @@ extern Driver esp32_usbhost_hid_driver;
extern Driver esp32_usbhost_midi_driver;
extern Driver esp32_usbhost_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);
+50 -166
View File
@@ -10,102 +10,43 @@
#include <esp_system.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <wear_levelling.h>
#include <tactility/device.h>
#include <tactility/drivers/esp32_sdcard.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/log.h>
#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
};
static sdmmc_card_t* getCard() {
sdmmc_card_t* card = nullptr;
enum {
EDPT_CTRL_OUT = 0x00,
EDPT_CTRL_IN = 0x80,
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<sdmmc_card_t**>(context);
*out = esp32_sdcard_get_card(device);
return *out == nullptr;
});
EDPT_MSC_OUT = 0x01,
EDPT_MSC_IN = 0x81,
};
if (card == nullptr) {
LOG_W(TAG, "Couldn't find a mounted SD card");
}
static bool driverInstalled = false;
return card;
}
static tusb_desc_device_t descriptor_config = {
.bLength = sizeof(descriptor_config),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = 0x303A, // TODO: Espressif VID. Do we need to change this?
.idProduct = 0x4002,
.bcdDevice = 0x100,
.iManufacturer = 0x01,
.iProduct = 0x02,
.iSerialNumber = 0x03,
.bNumConfigurations = 0x01
};
static char const* string_desc_arr[] = {
(const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409)
"Espressif", // 1: Manufacturer
"Tactility Device", // 2: Product
"42", // 3: Serials
"Tactility Mass Storage", // 4. MSC
};
static uint8_t const msc_fs_configuration_desc[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, EP Out & EP In address, EP size
TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64),
};
#if (TUD_OPT_HIGH_SPEED)
static const tusb_desc_device_qualifier_t device_qualifier = {
.bLength = sizeof(tusb_desc_device_qualifier_t),
.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,
.bcdUSB = 0x0200,
.bDeviceClass = TUSB_CLASS_MISC,
.bDeviceSubClass = MISC_SUBCLASS_COMMON,
.bDeviceProtocol = MISC_PROTOCOL_IAD,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.bNumConfigurations = 0x01,
.bReserved = 0
};
static uint8_t const msc_hs_configuration_desc[] = {
// Config number, interface count, string index, total length, attribute, power in mA
TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),
// Interface number, string index, EP Out & EP In address, EP size
TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 512),
};
#endif // TUD_OPT_HIGH_SPEED
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
if (event->mount_changed_data.is_mounted) {
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() {
@@ -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)
@@ -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;
};
/**
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
struct UsbInterfaceContribution;
// ---- USB device-mode CDC-ACM addon ----
/**
* USB CDC-ACM addon API (present a serial console over USB device mode). Unlike MSC/HID/MIDI,
* CDC is not a primary USB device class and does not go through
* usb_device_controller_claim()/release() - it's a devicetree-presence addon the USB device
* controller composites into whichever primary class is active (or stands alone with none
* active), toggled per-board via the usbdevicecdc0 devicetree child's status. This API is
* exposed by that child device and called by the USB device controller itself, not by primary
* class drivers.
*/
struct UsbCdcDeviceApi {
/**
* @param[in] device the CDC child device
* @return true if this board's usbdevicecdc0 child is present and enabled (status != "disabled")
*/
bool (*is_present)(struct Device* device);
/**
* Build this CDC instance's descriptor contribution, requesting interface/endpoint numbers
* from the controller via usb_device_controller_allocate_interfaces(). Called by the USB
* device controller itself during claim(), after the primary class's own contribution has
* already been allocated.
*
* `interface_string_index` is the string-descriptor index CDC's own interface string will
* land at once the controller appends it to the primary's string table (always
* primary_string_descriptor_count, since CDC's string is always appended last) - CDC bakes
* this index into its TUD_CDC_DESCRIPTOR bytes directly since it can't be patched after the
* fact. `out_interface_string` is CDC's own interface string text for the controller to
* append at that same index.
*
* @param[in] device the CDC child device
* @param[in] controller the USB device controller (to call allocate_interfaces() on)
* @param[in] interface_string_index string index this contribution's descriptor bytes must reference
* @param[out] out_contribution the built fragment
* @param[out] out_interface_string CDC's interface string text, to append at interface_string_index
* @retval ERROR_NONE on success
*/
error_t (*build_contribution)(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string);
/**
* Install the CDC ACM interface and the log-mirroring vprintf hook. Called by the USB device
* controller after claim() successfully installs the composite descriptor.
* @param[in] device the CDC child device
* @retval ERROR_NONE on success
*/
error_t (*start_console)(struct Device* device);
/**
* Uninstall the CDC ACM interface and restore the previous vprintf hook. Called by the USB
* device controller before release() uninstalls the TinyUSB driver.
* @param[in] device the CDC child device
* @retval ERROR_NONE on success
*/
error_t (*stop_console)(struct Device* device);
};
extern const struct DeviceType USB_CDC_DEVICE_TYPE;
/**
* Find the first started USB CDC device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_cdc_device_get(void);
bool usb_cdc_device_is_present(struct Device* device);
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string);
error_t usb_cdc_device_start_console(struct Device* device);
error_t usb_cdc_device_stop_console(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,192 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB device-mode (peripheral) controller ----
/**
* Which USB device-mode class currently owns the single TinyUSB device-mode slot.
* Only one class may be active at a time - see usb_device_controller_claim(). CDC is not part of
* this enum: it's an addon that composites into whichever primary class is active (or stands
* alone with none active) rather than a primary itself - see usb_cdc_device.h.
*/
enum UsbDeviceClass {
USB_DEVICE_CLASS_NONE,
USB_DEVICE_CLASS_MSC,
USB_DEVICE_CLASS_HID_KEYBOARD,
USB_DEVICE_CLASS_MIDI,
};
/**
* A block of raw USB interface-descriptor bytes contributed by one class (MSC/HID/MIDI/CDC) to
* the composite configuration descriptor the controller assembles at claim() time.
*
* Interface and endpoint numbers inside the descriptor bytes must already be renumbered by the
* contributor to the base values the controller handed back via usb_device_controller_allocate_interfaces()
* - the controller does not parse or patch the bytes, it only concatenates them behind the
* config-descriptor header it writes itself.
*
* `hs_descriptor_bytes`/`hs_descriptor_bytes_len` are optional (leave both NULL/0 if this
* contributor's bytes don't differ between full-speed and high-speed, e.g. HID/MIDI/CDC's
* endpoint sizes are already speed-independent for their traffic) - only used when
* TUD_OPT_HIGH_SPEED is set, mirroring the fs_/hs_configuration_descriptor split
* tinyusb_config_t itself has (see MSC, whose bulk endpoint max-packet-size legitimately differs:
* 64 bytes FS vs 512 bytes HS).
*/
struct UsbInterfaceContribution {
const uint8_t* descriptor_bytes; // one or more TUD_*_DESCRIPTOR blocks, back to back (full-speed)
size_t descriptor_bytes_len;
const uint8_t* hs_descriptor_bytes; // optional high-speed variant; NULL to reuse descriptor_bytes
size_t hs_descriptor_bytes_len; // must equal descriptor_bytes_len (same interface/endpoint layout, only sizes differ)
uint8_t interface_count; // interfaces consumed (MSC/HID/CDC=1, MIDI=2)
uint8_t in_endpoint_count; // IN endpoints consumed (excluding EP0)
uint8_t out_endpoint_count; // OUT endpoints consumed
};
/**
* Interface/endpoint numbers assigned to one contributor by the controller before it built its
* descriptor bytes. Endpoint numbers are direction-local (IN and OUT each number from 1), as is
* standard for USB - usb_device_controller_allocate_interfaces() hands out the next free *pair*
* per direction, not a single shared counter.
*/
struct UsbInterfaceAllocation {
uint8_t first_interface_number;
uint8_t first_in_endpoint; // e.g. 0x81, 0x82, ... or 0 if in_endpoint_count was 0
uint8_t first_out_endpoint; // e.g. 0x01, 0x02, ... or 0 if out_endpoint_count was 0
};
/**
* A fully-described primary class descriptor plus device/string descriptor metadata, submitted
* to claim(). CDC (if enabled) is appended by the controller itself - primary contributors never
* see or reference CDC's interface numbers.
*/
struct UsbDeviceClaimConfig {
// Actually a `tusb_desc_device_t*` (TinyUSB's device descriptor struct) - void* here so this
// kernel header doesn't need to depend on TinyUSB's own headers; the controller and every
// contributor already build against TinyUSB directly and cast accordingly. Mutable: the
// controller patches the class triad (bDeviceClass/SubClass/Protocol) in place at claim()
// time depending on whether CDC is composited in.
void* device_descriptor;
const char* const* string_descriptor;
size_t string_descriptor_count;
struct UsbInterfaceContribution primary; // MSC or HID or MIDI's contribution
};
/**
* Shared owner of the TinyUSB device-mode (peripheral) stack. Exactly one primary USB device
* class (mass storage, HID, MIDI) may be installed at a time - callers claim the slot before use
* and release it when done. CDC is a separate, orthogonal addon (see usb_cdc_device.h) that the
* controller composites into whichever primary is active, independent of the claim/release cycle.
* This exists so multiple independent drivers (MSC, HID, MIDI, CDC) can share the single
* underlying `tinyusb_driver_install()` call, composite descriptor assembly, and any
* board-specific PHY routing without needing to know about each other.
*/
struct UsbDeviceControllerApi {
/**
* Starts a new interface/endpoint allocation session, resetting the numbering counters
* allocate_interfaces() hands out. Call once, before building any descriptor bytes, as the
* first step of a claim() attempt (i.e. before the primary class's own allocate_interfaces()
* call) - claim() itself calls this again internally for CDC's allocation, so primary
* contributors only need to call it for their own single call.
*
* There is no explicit abort/cancel: a session abandoned after begin_claim() (e.g. a
* contributor's own allocate_interfaces() call fails and it returns early without calling
* claim()) is simply reset by the next begin_claim() call, which unconditionally reinitializes
* the allocation state regardless of whether the previous session ever finished.
*
* @param[in] device the USB device controller device
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*begin_claim)(struct Device* device);
/**
* Ask the controller for the next free interface number and endpoint pair, before building
* descriptor bytes. Call once per contributor (primary class, and CDC internally) per
* begin_claim() session, in the order the resulting descriptor should list interfaces:
* primary class first, then CDC (the controller enforces this order internally for CDC;
* primary contributors just call this once for themselves, after begin_claim()).
* @param[in] device the USB device controller device
* @param[in] interface_count number of interfaces this contributor needs
* @param[in] in_endpoint_count number of IN endpoints needed (0 if none)
* @param[in] out_endpoint_count number of OUT endpoints needed (0 if none)
* @param[out] out_allocation the assigned numbers
* @retval ERROR_INVALID_STATE if called without a preceding begin_claim()
* @retval ERROR_NONE on success
*/
error_t (*allocate_interfaces)(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation);
/**
* Claim the USB device-mode slot for the given primary class and install the composite
* descriptor (primary + CDC, if the board's usbdevicecdc0 child is enabled). Must be called
* after begin_claim() and the primary's own allocate_interfaces() call, using the same
* device-mode session (no other claim()/begin_claim() calls in between).
* @param[in] device the USB device controller device
* @param[in] usb_class the class to claim the slot for
* @param[in] config the primary class's descriptor contribution and metadata
* @retval ERROR_RESOURCE_BUSY if a different class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*claim)(struct Device* device, enum UsbDeviceClass usb_class,
const struct UsbDeviceClaimConfig* config);
/**
* Release the USB device-mode slot. Stops the CDC console (if it was composited in),
* disconnects from the host, uninstalls the TinyUSB driver, and restores any board-specific
* PHY routing. Only the current holder may release.
* @param[in] device the USB device controller device
* @param[in] usb_class the class releasing the slot; must match the current holder
* @retval ERROR_INVALID_STATE if usb_class does not hold the slot
* @retval ERROR_NONE on success
*/
error_t (*release)(struct Device* device, enum UsbDeviceClass usb_class);
/**
* @param[in] device the USB device controller device
* @return the class currently holding the slot, or USB_DEVICE_CLASS_NONE if free
*/
enum UsbDeviceClass (*get_active_class)(struct Device* device);
/**
* @param[in] device the USB device controller device
* @return true if a usbdevicecdc0 child device is present and enabled on this board
*/
bool (*is_cdc_enabled)(struct Device* device);
};
extern const struct DeviceType USB_DEVICE_CONTROLLER_TYPE;
/**
* Find the first started USB device controller and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_device_controller_get(void);
error_t usb_device_controller_begin_claim(struct Device* device);
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation);
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class,
const struct UsbDeviceClaimConfig* config);
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class);
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device);
bool usb_device_controller_is_cdc_enabled(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB HID device mode ----
/**
* Selects the HID report descriptor installed when this device operates as a USB HID
* peripheral. Mirrors BtHidDeviceMode's shape (bluetooth_hid_device.h) so callers that want to
* support both transports use the same mode taxonomy and send_* call shape.
*/
enum UsbHidDeviceMode {
/** Keyboard (report ID 1, boot-protocol-compatible 8 bytes) + Consumer (report ID 2, 2 bytes). */
USB_HID_DEVICE_MODE_KEYBOARD,
/** Mouse only (report ID 1, 4 bytes). */
USB_HID_DEVICE_MODE_MOUSE,
/** Keyboard + Consumer + Mouse (report IDs 1, 2, 3). */
USB_HID_DEVICE_MODE_KEYBOARD_MOUSE,
/** Gamepad (report ID 1, 8 bytes: 5-byte axes, 1-byte hat/dpad, 2-byte buttons[10] padded). */
USB_HID_DEVICE_MODE_GAMEPAD,
};
/**
* USB HID device profile API (present this device as a USB HID peripheral to a host).
* This API is exposed by a child device of the USB device controller.
*/
struct UsbHidDeviceApi {
/**
* Claim the USB device-mode slot and start advertising as a USB HID device with the given
* mode.
* @param[in] device the HID device child device
* @param[in] mode the HID device mode (keyboard, mouse, keyboard+mouse, gamepad)
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device, enum UsbHidDeviceMode mode);
/**
* Stop presenting as a USB HID device and release the USB device-mode slot.
* @param[in] device the HID device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* Override the USB product name string reported to the host (iProduct descriptor). Only
* takes effect on the next start() - the descriptor is fixed for the lifetime of a claimed
* session, matching bluetooth_set_device_name()'s pattern of being set before starting
* advertising. If never called, each mode falls back to its own default name (e.g.
* "Tactility Keyboard", "Tactility Mouse").
* @param[in] device the HID device child device
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
* this call returns)
* @return ERROR_NONE on success
*/
error_t (*set_name)(struct Device* device, const char* name);
/**
* Send a keyboard HID report (report ID 1: modifier, reserved, keycodes[6] - 8 bytes after
* the ID byte). Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 8-byte keyboard report (modifier, reserved, keycode[6])
* @param[in] len number of bytes (up to 8)
* @return ERROR_NONE on success
*/
error_t (*send_keyboard)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a consumer control HID report (report ID 2: 16-bit usage code, little-endian).
* Only valid in USB_HID_DEVICE_MODE_KEYBOARD or _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 2-byte consumer report
* @param[in] len number of bytes (up to 2)
* @return ERROR_NONE on success
*/
error_t (*send_consumer)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a mouse HID report (buttons, X, Y, wheel - 4 bytes). Report ID 1 in
* USB_HID_DEVICE_MODE_MOUSE, report ID 3 in _KEYBOARD_MOUSE.
* @param[in] device the HID device child device
* @param[in] report pointer to the 4-byte mouse report
* @param[in] len number of bytes (up to 4)
* @return ERROR_NONE on success
*/
error_t (*send_mouse)(struct Device* device, const uint8_t* report, size_t len);
/**
* Send a gamepad HID report (report ID 1: axes[5] (X,Y,Rx,Ry,Z), hat/dpad[1] (low nibble),
* buttons[2] (10 buttons + 6 padding bits) - 8 bytes). Only valid in
* USB_HID_DEVICE_MODE_GAMEPAD. See hid_report_map_gamepad in hid_report_descriptors.cpp for
* the full wire layout.
* @param[in] device the HID device child device
* @param[in] report pointer to the 8-byte gamepad report
* @param[in] len number of bytes (up to 8)
* @return ERROR_NONE on success
*/
error_t (*send_gamepad)(struct Device* device, const uint8_t* report, size_t len);
/**
* @param[in] device the HID device child device
* @return true when a USB host has mounted the device and the HID interface is ready
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_HID_DEVICE_TYPE;
/**
* Find the first started USB HID device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_hid_device_get(void);
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode);
error_t usb_hid_device_stop(struct Device* device);
error_t usb_hid_device_set_name(struct Device* device, const char* name);
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len);
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len);
bool usb_hid_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB MIDI device mode ----
/**
* USB MIDI device profile API (present this device as a USB MIDI peripheral to a host).
* This API is exposed by a child device of the USB device controller. Mirrors
* bluetooth_midi.h's shape (no mode enum - unlike HID, MIDI has exactly one device profile) so
* the two transports stay symmetric for callers that want to support both.
*/
struct UsbMidiDeviceApi {
/**
* Claim the USB device-mode slot and start advertising as a USB MIDI device.
* @param[in] device the MIDI device child device
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device);
/**
* Stop presenting as a USB MIDI device and release the USB device-mode slot.
* @param[in] device the MIDI device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* Override the USB product name string reported to the host (iProduct descriptor). Only
* takes effect on the next start() - matches bluetooth_set_device_name()'s pattern of being
* set before starting advertising. If never called, falls back to "Tactility MIDI Device".
* @param[in] device the MIDI device child device
* @param[in] name the product name (copied; safe to free/reuse the caller's buffer after
* this call returns)
* @return ERROR_NONE on success
*/
error_t (*set_name)(struct Device* device, const char* name);
/**
* Send raw MIDI message bytes over the USB MIDI connection.
* @param[in] device the MIDI device child device
* @param[in] msg the raw MIDI bytes
* @param[in] len the number of bytes
* @return ERROR_NONE on success
*/
error_t (*send)(struct Device* device, const uint8_t* msg, size_t len);
/**
* @param[in] device the MIDI device child device
* @return true when a USB host has mounted the device and the MIDI interface is ready
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_MIDI_DEVICE_TYPE;
/**
* Find the first started USB MIDI device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_midi_device_get(void);
error_t usb_midi_device_start(struct Device* device);
error_t usb_midi_device_stop(struct Device* device);
error_t usb_midi_device_set_name(struct Device* device, const char* name);
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len);
bool usb_midi_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Device;
struct DeviceType;
// ---- USB mass-storage device mode ----
/** Which backing storage is exposed to the USB host as a mass-storage volume. */
enum UsbMscDeviceSource {
USB_MSC_DEVICE_SOURCE_SDMMC,
USB_MSC_DEVICE_SOURCE_FLASH,
};
/** Fired when the exposed volume's mount state changes (host mounted/unmounted it). */
typedef void (*UsbMscDeviceMountChangedCallback)(bool mounted, void* context);
/**
* USB mass-storage device profile API (present the board's SD card or internal flash as a USB
* mass-storage device to a host).
*/
struct UsbMscDeviceApi {
/**
* Claim the USB device-mode slot and expose the given storage source as a USB mass-storage
* volume.
* @warning the caller must ensure the backing storage is unmounted/quiesced from local use
* before calling this - a source exposed to a USB host while still locally mounted
* risks filesystem corruption from two concurrent writers. Tactility's own flash-MSC
* path (see UsbTusb.cpp / UsbSettingsApp) enforces this via a dedicated reboot-into-
* MSC boot flow rather than unmounting live, so local access never overlaps with USB
* exposure; local availability is only restored by rebooting back to normal OS.
* @param[in] device the MSC device child device
* @param[in] source which backing storage to expose
* @param[in] source_handle the backing storage handle: a `sdmmc_card_t*` when source is
* USB_MSC_DEVICE_SOURCE_SDMMC, or a pointer to a `wl_handle_t` (i.e. `wl_handle_t*`)
* when source is USB_MSC_DEVICE_SOURCE_FLASH - passed by address, not cast through
* `void*` by value, since `wl_handle_t` is a plain integer type where 0 is a valid
* handle and would collide with the nullptr/"no handle" check otherwise. The caller
* resolves this - platform-esp32 has no business knowing which wear-levelling
* partition Tactility mounted as /data.
* @param[in] mount_changed_cb optional callback fired on host mount/unmount, nullable
* @param[in] context passed back to mount_changed_cb, nullable
* @retval ERROR_RESOURCE_BUSY if another USB device class already holds the slot
* @retval ERROR_INVALID_ARGUMENT if source_handle is invalid for the given source
* @retval ERROR_NONE on success
*/
error_t (*start)(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
/**
* Stop presenting as a USB mass-storage device and release the USB device-mode slot.
* @param[in] device the MSC device child device
* @return ERROR_NONE on success
*/
error_t (*stop)(struct Device* device);
/**
* @param[in] device the MSC device child device
* @return true when a USB host currently has the volume mounted
*/
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_MSC_DEVICE_TYPE;
/**
* Find the first started USB MSC device child device and take a reference on it.
* @return the device with an outstanding reference, or NULL if none is available - caller must
* call device_put() exactly once when done, same as device_get_first_active_by_type().
*/
struct Device* usb_msc_device_get(void);
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context);
error_t usb_msc_device_stop(struct Device* device);
bool usb_msc_device_is_connected(struct Device* device);
#ifdef __cplusplus
}
#endif
+4 -2
View File
@@ -10,10 +10,12 @@ extern "C" {
error_t keyboard_read_key(Device* device, KeyboardKeyData* data) {
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);
}
@@ -0,0 +1,38 @@
#include <tactility/drivers/usb_cdc_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_CDC_DEVICE_API(device) ((const struct UsbCdcDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_CDC_DEVICE_TYPE = {
.name = "usb-cdc-device",
};
struct Device* usb_cdc_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_CDC_DEVICE_TYPE, &found);
return found;
}
bool usb_cdc_device_is_present(struct Device* device) {
return USB_CDC_DEVICE_API(device)->is_present(device);
}
error_t usb_cdc_device_build_contribution(struct Device* device, struct Device* controller,
uint8_t interface_string_index,
struct UsbInterfaceContribution* out_contribution,
const char** out_interface_string) {
return USB_CDC_DEVICE_API(device)->build_contribution(device, controller, interface_string_index, out_contribution, out_interface_string);
}
error_t usb_cdc_device_start_console(struct Device* device) {
return USB_CDC_DEVICE_API(device)->start_console(device);
}
error_t usb_cdc_device_stop_console(struct Device* device) {
return USB_CDC_DEVICE_API(device)->stop_console(device);
}
} // extern "C"
@@ -0,0 +1,45 @@
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_DEVICE_CONTROLLER_API(device) ((const struct UsbDeviceControllerApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_DEVICE_CONTROLLER_TYPE = {
.name = "usb-device-controller",
};
struct Device* usb_device_controller_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_DEVICE_CONTROLLER_TYPE, &found);
return found;
}
error_t usb_device_controller_begin_claim(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->begin_claim(device);
}
error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t interface_count,
uint8_t in_endpoint_count, uint8_t out_endpoint_count,
struct UsbInterfaceAllocation* out_allocation) {
return USB_DEVICE_CONTROLLER_API(device)->allocate_interfaces(device, interface_count, in_endpoint_count, out_endpoint_count, out_allocation);
}
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class, const struct UsbDeviceClaimConfig* config) {
return USB_DEVICE_CONTROLLER_API(device)->claim(device, usb_class, config);
}
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class) {
return USB_DEVICE_CONTROLLER_API(device)->release(device, usb_class);
}
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->get_active_class(device);
}
bool usb_device_controller_is_cdc_enabled(struct Device* device) {
return USB_DEVICE_CONTROLLER_API(device)->is_cdc_enabled(device);
}
} // extern "C"
@@ -0,0 +1,51 @@
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_HID_DEVICE_API(device) ((const struct UsbHidDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_HID_DEVICE_TYPE = {
.name = "usb-hid-device",
};
struct Device* usb_hid_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_HID_DEVICE_TYPE, &found);
return found;
}
error_t usb_hid_device_start(struct Device* device, enum UsbHidDeviceMode mode) {
return USB_HID_DEVICE_API(device)->start(device, mode);
}
error_t usb_hid_device_stop(struct Device* device) {
return USB_HID_DEVICE_API(device)->stop(device);
}
error_t usb_hid_device_set_name(struct Device* device, const char* name) {
return USB_HID_DEVICE_API(device)->set_name(device, name);
}
error_t usb_hid_device_send_keyboard(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_keyboard(device, report, len);
}
error_t usb_hid_device_send_consumer(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_consumer(device, report, len);
}
error_t usb_hid_device_send_mouse(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_mouse(device, report, len);
}
error_t usb_hid_device_send_gamepad(struct Device* device, const uint8_t* report, size_t len) {
return USB_HID_DEVICE_API(device)->send_gamepad(device, report, len);
}
bool usb_hid_device_is_connected(struct Device* device) {
return USB_HID_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
@@ -0,0 +1,39 @@
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_MIDI_DEVICE_API(device) ((const struct UsbMidiDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_MIDI_DEVICE_TYPE = {
.name = "usb-midi-device",
};
struct Device* usb_midi_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_MIDI_DEVICE_TYPE, &found);
return found;
}
error_t usb_midi_device_start(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->start(device);
}
error_t usb_midi_device_stop(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->stop(device);
}
error_t usb_midi_device_set_name(struct Device* device, const char* name) {
return USB_MIDI_DEVICE_API(device)->set_name(device, name);
}
error_t usb_midi_device_send(struct Device* device, const uint8_t* msg, size_t len) {
return USB_MIDI_DEVICE_API(device)->send(device, msg, len);
}
bool usb_midi_device_is_connected(struct Device* device) {
return USB_MIDI_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
@@ -0,0 +1,32 @@
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#define USB_MSC_DEVICE_API(device) ((const struct UsbMscDeviceApi*)device_get_driver(device)->api)
extern "C" {
const struct DeviceType USB_MSC_DEVICE_TYPE = {
.name = "usb-msc-device",
};
struct Device* usb_msc_device_get() {
struct Device* found = nullptr;
device_get_first_active_by_type(&USB_MSC_DEVICE_TYPE, &found);
return found;
}
error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource source, void* source_handle,
UsbMscDeviceMountChangedCallback mount_changed_cb, void* context) {
return USB_MSC_DEVICE_API(device)->start(device, source, source_handle, mount_changed_cb, context);
}
error_t usb_msc_device_stop(struct Device* device) {
return USB_MSC_DEVICE_API(device)->stop(device);
}
bool usb_msc_device_is_connected(struct Device* device) {
return USB_MSC_DEVICE_API(device)->is_connected(device);
}
} // extern "C"
+38
View File
@@ -33,9 +33,13 @@
#include <tactility/drivers/spi_controller.h>
#include <tactility/drivers/trackball.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_hid_device.h>
#include <tactility/drivers/usb_host_hid.h>
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/drivers/usb_host_msc.h>
#include <tactility/drivers/usb_midi_device.h>
#include <tactility/drivers/usb_msc_device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/error.h>
#include <tactility/filesystem/file_mutex.h>
@@ -422,6 +426,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),
+27 -4
View File
@@ -341,11 +341,34 @@ def write_touch_calibration_variables(output_file, device_properties: dict):
def write_usb_variables(output_file, device_properties: dict):
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