USB device-mode support (#613)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user