USB host support (#527)

This commit is contained in:
Shadowtrance
2026-06-04 07:10:37 +10:00
committed by GitHub
parent a59fbf4ed5
commit 3dcc95f6f3
46 changed files with 2409 additions and 121 deletions
+1 -1
View File
@@ -9,4 +9,4 @@ idf_component_register(
REQUIRES TactilityKernel driver vfs fatfs
)
idf_component_optional_requires(PRIVATE bt)
idf_component_optional_requires(PRIVATE bt usb espressif__usb_host_hid espressif__usb_host_msc)
@@ -0,0 +1,9 @@
description: ESP32 USB Host HID class driver
compatible: "espressif,esp32-usbhost-hid"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB Host MIDI class driver
compatible: "espressif,esp32-usbhost-midi"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,9 @@
description: ESP32 USB Host MSC class driver (mass storage)
compatible: "espressif,esp32-usbhost-msc"
properties:
_unused:
type: int
default: 0
description: Placeholder required by the binding schema; this driver has no device-tree configuration.
@@ -0,0 +1,15 @@
description: ESP32 USB Host controller
compatible: "espressif,esp32-usbhost"
properties:
peripheral-map:
type: int
default: 0
description: |
Bitmask of USB OTG controllers to use. Only meaningful on ESP32-P4,
which has two independent USB DWC cores. On ESP32-S2/S3 this field
is ignored (single controller, always used).
Default 0 selects the default peripheral (P4 default: controller 0, HS/UTMI).
BIT(0) = controller 0 (HS, UTMI PHY - GPIO 49/50 aka USB2_OTG pins, e.g. USB-A on Tab5)
BIT(1) = controller 1 (FS, FSLS PHY - GPIO 24/25 aka USB1 pins, e.g. USB-C OTG on Tab5)
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <tactility/drivers/esp32_usbhost.h>
#ifdef __cplusplus
extern "C" {
#endif
DEFINE_DEVICETREE(esp32_usbhost, struct Esp32UsbHostConfig)
DEFINE_DEVICETREE(esp32_usbhost_hid, struct Esp32UsbHostChildConfig)
DEFINE_DEVICETREE(esp32_usbhost_midi, struct Esp32UsbHostChildConfig)
DEFINE_DEVICETREE(esp32_usbhost_msc, struct Esp32UsbHostChildConfig)
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct Esp32UsbHostConfig {
uint8_t peripheral_map; /**< BIT(0)=controller 0 (HS/UTMI, e.g. USB-A on Tab5), BIT(1)=controller 1 (FS/FSLS, e.g. USB-C OTG on Tab5) */
};
struct Esp32UsbHostChildConfig {
int _unused;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,161 @@
#include <sdkconfig.h>
#ifdef CONFIG_SOC_USB_OTG_SUPPORTED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost.h>
#include <tactility/log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <usb/usb_host.h>
#include <esp_intr_alloc.h>
#define TAG "esp32_usbhost"
#define GET_CONFIG(device) ((const Esp32UsbHostConfig*)(device)->config)
constexpr auto USB_LIB_TASK_STACK = 4096;
constexpr auto USB_LIB_TASK_PRIORITY = 10;
constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500;
constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000;
constexpr auto USB_HOST_STOP_RETRY_MS = 1000;
struct UsbHostContext {
TaskHandle_t lib_task = nullptr;
SemaphoreHandle_t lib_task_done = nullptr;
};
static void usbLibTask(void* arg) {
auto* ctx = static_cast<UsbHostContext*>(arg);
LOG_I(TAG, "lib task started");
while (true) {
uint32_t flags = 0;
esp_err_t err = usb_host_lib_handle_events(pdMS_TO_TICKS(USB_LIB_EVENT_TIMEOUT_MS), &flags);
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
LOG_W(TAG, "usb_host_lib_handle_events: %s", esp_err_to_name(err));
}
if (flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) {
LOG_I(TAG, "no more USB clients, freeing all devices");
usb_host_device_free_all();
}
if (flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) {
LOG_I(TAG, "all USB devices freed");
}
if (ulTaskNotifyTake(pdFALSE, 0) > 0) {
break;
}
}
LOG_I(TAG, "lib task stopping");
xSemaphoreGive(ctx->lib_task_done);
vTaskDelete(nullptr);
}
extern "C" {
static error_t start_device(struct Device* device) {
auto* cfg = GET_CONFIG(device);
if (!cfg) {
LOG_E(TAG, "device config is null");
return ERROR_INVALID_ARGUMENT;
}
usb_host_config_t host_cfg = {
.skip_phy_setup = false,
.root_port_unpowered = false,
.intr_flags = ESP_INTR_FLAG_LEVEL1,
.enum_filter_cb = nullptr,
.fifo_settings_custom = {},
#if CONFIG_IDF_TARGET_ESP32P4
.peripheral_map = cfg->peripheral_map,
#endif
};
esp_err_t ret = usb_host_install(&host_cfg);
if (ret != ESP_OK) {
LOG_E(TAG, "usb_host_install failed: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
auto* ctx = new UsbHostContext();
ctx->lib_task_done = xSemaphoreCreateBinary();
if (!ctx->lib_task_done) {
LOG_E(TAG, "failed to create lib_task_done semaphore");
delete ctx;
usb_host_uninstall();
return ERROR_RESOURCE;
}
BaseType_t result = xTaskCreate(usbLibTask, "usb_lib", USB_LIB_TASK_STACK,
ctx, USB_LIB_TASK_PRIORITY, &ctx->lib_task);
if (result != pdPASS) {
LOG_E(TAG, "failed to create usb_lib task");
vSemaphoreDelete(ctx->lib_task_done);
delete ctx;
usb_host_uninstall();
return ERROR_RESOURCE;
}
device_set_driver_data(device, ctx);
LOG_I(TAG, "started (peripheral_map=0x%02x)", cfg->peripheral_map);
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
xTaskNotifyGive(ctx->lib_task);
bool exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_TIMEOUT_MS)) == pdTRUE);
if (!exited) {
// Free all devices to unblock the NO_CLIENTS / ALL_FREE flags, then retry.
usb_host_device_free_all();
exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_RETRY_MS)) == pdTRUE);
}
if (!exited) {
LOG_E(TAG, "lib task stop timed out after %dms, force terminating — USB host restart required",
USB_HOST_STOP_TIMEOUT_MS + USB_HOST_STOP_RETRY_MS);
vTaskDelete(ctx->lib_task);
vTaskDelay(pdMS_TO_TICKS(50));
// Skip usb_host_uninstall — USB stack is in undefined state.
ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done);
device_set_driver_data(device, nullptr);
delete ctx;
return ERROR_RESOURCE;
}
ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done);
esp_err_t uninstall_err = usb_host_uninstall();
if (uninstall_err != ESP_OK) {
LOG_W(TAG, "usb_host_uninstall: %s", esp_err_to_name(uninstall_err));
}
device_set_driver_data(device, nullptr);
delete ctx;
LOG_I(TAG, "stopped");
return ERROR_NONE;
}
Driver esp32_usbhost_driver = {
.name = "esp32_usbhost",
.compatible = (const char*[]) { "espressif,esp32-usbhost", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = nullptr,
.device_type = nullptr,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED
@@ -0,0 +1,506 @@
#include <sdkconfig.h>
#ifdef CONFIG_SOC_USB_OTG_SUPPORTED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/usb_host_hid.h>
#include <tactility/log.h>
#include <atomic>
#include <cstring>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <usb/hid_host.h>
#include <usb/hid_usage_keyboard.h>
#include <usb/hid_usage_mouse.h>
#include <esp_timer.h>
#define TAG "esp32_usbhost_hid"
constexpr auto HID_EVENT_QUEUE_SIZE = 8;
constexpr auto HID_PROC_TASK_STACK = 4096;
constexpr auto HID_PROC_TASK_PRIORITY = 5;
constexpr auto HID_STOP_TIMEOUT_MS = 2000;
constexpr auto MAX_SUBSCRIBERS = 4;
typedef struct {
hid_host_device_handle_t handle;
hid_host_driver_event_t event;
void* arg;
} hid_dev_event_t;
struct UsbHidContext {
std::atomic<bool> mouse_connected{false};
std::atomic<bool> device_connected{false};
QueueHandle_t hid_event_queue = nullptr;
TaskHandle_t hid_proc_task = nullptr;
SemaphoreHandle_t hid_proc_task_done = nullptr;
std::atomic<bool> hid_proc_running{false};
uint8_t prev_keys[HID_KEYBOARD_KEY_MAX] = {};
uint32_t pressed_lv_keys[256] = {};
bool caps_lock_active = false;
bool num_lock_active = true;
bool scroll_lock_active = false;
bool prev_mouse_button2 = false;
std::atomic<hid_host_device_handle_t> kb_handle{nullptr};
std::atomic<bool> kb_led_pending{false};
QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {};
SemaphoreHandle_t sub_mutex = nullptr;
};
static const uint8_t keycode2ascii[57][2] = {
{0, 0}, {0, 0}, {0, 0}, {0, 0},
{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'},
{'f', 'F'}, {'g', 'G'}, {'h', 'H'}, {'i', 'I'}, {'j', 'J'},
{'k', 'K'}, {'l', 'L'}, {'m', 'M'}, {'n', 'N'}, {'o', 'O'},
{'p', 'P'}, {'q', 'Q'}, {'r', 'R'}, {'s', 'S'}, {'t', 'T'},
{'u', 'U'}, {'v', 'V'}, {'w', 'W'}, {'x', 'X'}, {'y', 'Y'},
{'z', 'Z'},
{'1', '!'}, {'2', '@'}, {'3', '#'}, {'4', '$'}, {'5', '%'},
{'6', '^'}, {'7', '&'}, {'8', '*'}, {'9', '('}, {'0', ')'},
{'\r', '\r'}, {0, 0}, {'\b', 0}, {'\t', '\t'}, {' ', ' '},
{'-', '_'}, {'=', '+'}, {'[', '{'}, {']', '}'},
{'\\', '|'}, {'\\', '|'}, {';', ':'}, {'\'', '"'},
{'`', '~'}, {',', '<'}, {'.', '>'}, {'/', '?'},
};
static uint32_t hid_keycode_to_key(uint8_t modifier, uint8_t key_code,
bool caps_lock, bool num_lock) {
bool shift = (modifier & HID_LEFT_SHIFT) || (modifier & HID_RIGHT_SHIFT);
bool ctrl = (modifier & HID_LEFT_CONTROL) || (modifier & HID_RIGHT_CONTROL);
bool alt = (modifier & HID_LEFT_ALT) || (modifier & HID_RIGHT_ALT);
switch (key_code) {
case HID_KEY_ENTER: return USB_HID_KEY_ENTER;
case HID_KEY_ESC: return USB_HID_KEY_ESC;
case HID_KEY_DEL: return USB_HID_KEY_BACKSPACE;
case HID_KEY_DELETE: return USB_HID_KEY_DEL;
case HID_KEY_TAB: return shift ? USB_HID_KEY_PREV : USB_HID_KEY_NEXT;
case HID_KEY_UP: return USB_HID_KEY_UP;
case HID_KEY_DOWN: return USB_HID_KEY_DOWN;
case HID_KEY_LEFT: return USB_HID_KEY_LEFT;
case HID_KEY_RIGHT: return USB_HID_KEY_RIGHT;
case HID_KEY_HOME: return USB_HID_KEY_HOME;
case HID_KEY_END: return USB_HID_KEY_END;
case HID_KEY_KEYPAD_ENTER: return USB_HID_KEY_ENTER;
case HID_KEY_KEYPAD_ADD: return '+';
case HID_KEY_KEYPAD_SUB: return '-';
case HID_KEY_KEYPAD_MUL: return '*';
case HID_KEY_KEYPAD_DIV: return '/';
case HID_KEY_KEYPAD_0: return num_lock ? (uint32_t)'0' : 0u;
case HID_KEY_KEYPAD_1: return num_lock ? (uint32_t)'1' : (uint32_t)USB_HID_KEY_END;
case HID_KEY_KEYPAD_2: return num_lock ? (uint32_t)'2' : (uint32_t)USB_HID_KEY_DOWN;
case HID_KEY_KEYPAD_3: return num_lock ? (uint32_t)'3' : 0u;
case HID_KEY_KEYPAD_4: return num_lock ? (uint32_t)'4' : (uint32_t)USB_HID_KEY_LEFT;
case HID_KEY_KEYPAD_5: return num_lock ? (uint32_t)'5' : 0u;
case HID_KEY_KEYPAD_6: return num_lock ? (uint32_t)'6' : (uint32_t)USB_HID_KEY_RIGHT;
case HID_KEY_KEYPAD_7: return num_lock ? (uint32_t)'7' : (uint32_t)USB_HID_KEY_HOME;
case HID_KEY_KEYPAD_8: return num_lock ? (uint32_t)'8' : (uint32_t)USB_HID_KEY_UP;
case HID_KEY_KEYPAD_9: return num_lock ? (uint32_t)'9' : 0u;
case HID_KEY_KEYPAD_DELETE: return num_lock ? (uint32_t)'.' : (uint32_t)USB_HID_KEY_DEL;
default: break;
}
if (ctrl || alt) return 0;
if (key_code < (sizeof(keycode2ascii) / sizeof(keycode2ascii[0]))) {
bool is_letter = (key_code >= 0x04 && key_code <= 0x1D);
bool effective_shift = is_letter ? (shift ^ caps_lock) : shift;
uint8_t ch = keycode2ascii[key_code][effective_shift ? 1 : 0];
if (ch != 0) return (uint32_t)ch;
}
return 0;
}
static void publish_event(UsbHidContext* ctx, const UsbHidEvent* evt) {
if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(10)) != pdTRUE) {
LOG_W(TAG, "publish_event: sub_mutex contended, event type=%d dropped", (int)evt->type);
return;
}
bool is_release = (evt->type == USB_HID_EVENT_KEY && !evt->key.pressed);
TickType_t send_timeout = is_release ? pdMS_TO_TICKS(10) : 0;
for (int i = 0; i < MAX_SUBSCRIBERS; i++) {
if (ctx->subscribers[i]) {
xQueueSend(ctx->subscribers[i], evt, send_timeout);
}
}
xSemaphoreGive(ctx->sub_mutex);
}
static void publish_scroll(UsbHidContext* ctx, int32_t delta) {
UsbHidEvent evt = { .type = USB_HID_EVENT_SCROLL, .scroll = { delta } };
publish_event(ctx, &evt);
}
static void hid_interface_callback(hid_host_device_handle_t handle,
const hid_host_interface_event_t event,
void* arg)
{
auto* ctx = static_cast<UsbHidContext*>(arg);
uint8_t data[64] = {};
size_t data_len = 0;
hid_host_dev_params_t params;
if (hid_host_device_get_params(handle, &params) != ESP_OK) return;
switch (event) {
case HID_HOST_INTERFACE_EVENT_INPUT_REPORT:
if (hid_host_device_get_raw_input_report_data(handle, data, sizeof(data), &data_len) != ESP_OK) break;
if (params.proto == HID_PROTOCOL_KEYBOARD) {
if (data_len < sizeof(hid_keyboard_input_report_boot_t)) break;
auto* kb = reinterpret_cast<const hid_keyboard_input_report_boot_t*>(data);
for (int i = 0; i < HID_KEYBOARD_KEY_MAX; i++) {
uint8_t prev_hid = ctx->prev_keys[i];
if (prev_hid > HID_KEY_ERROR_UNDEFINED) {
bool still_pressed = false;
for (int j = 0; j < HID_KEYBOARD_KEY_MAX; j++) {
if (kb->key[j] == prev_hid) { still_pressed = true; break; }
}
if (!still_pressed) {
uint32_t lv_key = ctx->pressed_lv_keys[prev_hid];
ctx->pressed_lv_keys[prev_hid] = 0;
if (lv_key) {
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false } };
publish_event(ctx, &evt);
}
}
}
uint8_t hid_code = kb->key[i];
if (hid_code > HID_KEY_ERROR_UNDEFINED) {
bool was_pressed = false;
for (int j = 0; j < HID_KEYBOARD_KEY_MAX; j++) {
if (ctx->prev_keys[j] == hid_code) { was_pressed = true; break; }
}
if (!was_pressed) {
if (hid_code == HID_KEY_CAPS_LOCK) {
ctx->caps_lock_active = !ctx->caps_lock_active;
ctx->kb_led_pending.store(true);
continue;
}
if (hid_code == HID_KEY_NUM_LOCK) {
ctx->num_lock_active = !ctx->num_lock_active;
ctx->kb_led_pending.store(true);
continue;
}
if (hid_code == HID_KEY_SCROLL_LOCK) {
ctx->scroll_lock_active = !ctx->scroll_lock_active;
ctx->kb_led_pending.store(true);
continue;
}
bool is_pgup = (hid_code == HID_KEY_PAGEUP) ||
(!ctx->num_lock_active && hid_code == HID_KEY_KEYPAD_9);
bool is_pgdn = (hid_code == HID_KEY_PAGEDOWN) ||
(!ctx->num_lock_active && hid_code == HID_KEY_KEYPAD_3);
if (is_pgup || is_pgdn) {
publish_scroll(ctx, is_pgup ? -8 : 8);
continue;
}
uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code,
ctx->caps_lock_active, ctx->num_lock_active);
if (lv_key) {
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, true } };
publish_event(ctx, &evt);
ctx->pressed_lv_keys[hid_code] = lv_key;
}
}
}
}
memcpy(ctx->prev_keys, kb->key, HID_KEYBOARD_KEY_MAX);
} else if (params.proto == HID_PROTOCOL_MOUSE) {
if (data_len < sizeof(hid_mouse_input_report_boot_t)) break;
auto* ms = reinterpret_cast<const hid_mouse_input_report_boot_t*>(data);
if (ms->x_displacement != 0 || ms->y_displacement != 0) {
UsbHidEvent evt = { .type = USB_HID_EVENT_MOUSE_MOVE,
.mouse_move = { (int32_t)ms->x_displacement, (int32_t)ms->y_displacement } };
publish_event(ctx, &evt);
}
{
bool b1 = ms->buttons.button1 != 0;
bool b2 = ms->buttons.button2 != 0;
UsbHidEvent evt = { .type = USB_HID_EVENT_MOUSE_BTN, .mouse_btn = { b1, b2 } };
publish_event(ctx, &evt);
if (b2 != ctx->prev_mouse_button2) {
UsbHidEvent key_evt = { .type = USB_HID_EVENT_KEY, .key = { USB_HID_KEY_ESC, b2 } };
publish_event(ctx, &key_evt);
ctx->prev_mouse_button2 = b2;
}
}
if (data_len > sizeof(hid_mouse_input_report_boot_t)) {
int8_t wheel = (int8_t)data[sizeof(hid_mouse_input_report_boot_t)];
if (wheel != 0) {
int32_t delta = (wheel < -8) ? -8 : (wheel > 8) ? 8 : (int32_t)wheel;
publish_scroll(ctx, delta);
}
}
}
break;
case HID_HOST_INTERFACE_EVENT_DISCONNECTED:
if (params.proto == HID_PROTOCOL_KEYBOARD) {
memset(ctx->prev_keys, 0, sizeof(ctx->prev_keys));
memset(ctx->pressed_lv_keys, 0, sizeof(ctx->pressed_lv_keys));
ctx->kb_handle.store(nullptr);
ctx->kb_led_pending.store(false);
} else if (params.proto == HID_PROTOCOL_MOUSE) {
ctx->mouse_connected = false;
}
hid_host_device_close(handle);
if (!ctx->kb_handle.load() && !ctx->mouse_connected) {
ctx->device_connected = false;
}
{
UsbHidEventType disc_type = (params.proto == HID_PROTOCOL_KEYBOARD)
? USB_HID_EVENT_KEYBOARD_DISCONNECTED
: USB_HID_EVENT_MOUSE_DISCONNECTED;
UsbHidEvent evt = { .type = disc_type };
publish_event(ctx, &evt);
}
break;
case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR:
LOG_W(TAG, "HID transfer error (proto=%d)", params.proto);
break;
default:
break;
}
}
static void hid_driver_callback(hid_host_device_handle_t handle,
const hid_host_driver_event_t event,
void* arg)
{
auto* ctx = static_cast<UsbHidContext*>(arg);
hid_dev_event_t evt = { handle, event, arg };
if (ctx->hid_event_queue) {
xQueueSend(ctx->hid_event_queue, &evt, 0);
}
}
static void hid_proc_task(void* arg) {
auto* ctx = static_cast<UsbHidContext*>(arg);
LOG_I(TAG, "HID proc task started");
while (ctx->hid_proc_running) {
hid_dev_event_t dev_evt;
if (xQueueReceive(ctx->hid_event_queue, &dev_evt, pdMS_TO_TICKS(100)) != pdTRUE) {
if (ctx->kb_led_pending.exchange(false) && ctx->kb_handle.load()) {
uint8_t leds = (ctx->num_lock_active ? 0x01 : 0)
| (ctx->caps_lock_active ? 0x02 : 0)
| (ctx->scroll_lock_active ? 0x04 : 0);
hid_class_request_set_report(ctx->kb_handle.load(), HID_REPORT_TYPE_OUTPUT, 0, &leds, 1);
}
continue;
}
if (dev_evt.event == HID_HOST_DRIVER_EVENT_CONNECTED) {
hid_host_dev_params_t params;
if (hid_host_device_get_params(dev_evt.handle, &params) != ESP_OK) continue;
if (params.proto != HID_PROTOCOL_KEYBOARD && params.proto != HID_PROTOCOL_MOUSE) {
LOG_D(TAG, "ignoring HID interface with unhandled proto=%d", params.proto);
continue;
}
LOG_I(TAG, "HID device connected (proto=%d)", params.proto);
const hid_host_device_config_t dev_cfg = {
.callback = hid_interface_callback,
.callback_arg = ctx,
};
if (hid_host_device_open(dev_evt.handle, &dev_cfg) != ESP_OK) {
LOG_W(TAG, "hid_host_device_open failed");
continue;
}
hid_class_request_set_protocol(dev_evt.handle, HID_REPORT_PROTOCOL_BOOT);
if (params.proto == HID_PROTOCOL_KEYBOARD) {
hid_class_request_set_idle(dev_evt.handle, 0, 0);
vTaskDelay(pdMS_TO_TICKS(200));
ctx->kb_handle.store(dev_evt.handle);
uint8_t leds = (ctx->num_lock_active ? 0x01 : 0)
| (ctx->caps_lock_active ? 0x02 : 0)
| (ctx->scroll_lock_active ? 0x04 : 0);
hid_class_request_set_report(dev_evt.handle, HID_REPORT_TYPE_OUTPUT, 0, &leds, 1);
} else if (params.proto == HID_PROTOCOL_MOUSE) {
ctx->mouse_connected = true;
}
ctx->device_connected = true;
{
UsbHidEventType conn_type = (params.proto == HID_PROTOCOL_KEYBOARD)
? USB_HID_EVENT_KEYBOARD_CONNECTED
: USB_HID_EVENT_MOUSE_CONNECTED;
UsbHidEvent evt = { .type = conn_type };
publish_event(ctx, &evt);
}
hid_host_device_start(dev_evt.handle);
}
}
LOG_I(TAG, "HID proc task stopped");
xSemaphoreGive(ctx->hid_proc_task_done);
vTaskDelete(nullptr);
}
static bool api_hid_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
return ctx && ctx->device_connected.load();
}
static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
if (!ctx || !event_queue) return false;
if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
bool added = false;
for (int i = 0; i < MAX_SUBSCRIBERS; i++) {
if (ctx->subscribers[i] == static_cast<QueueHandle_t>(event_queue)) {
added = true;
break;
}
if (!ctx->subscribers[i]) {
ctx->subscribers[i] = static_cast<QueueHandle_t>(event_queue);
added = true;
break;
}
}
xSemaphoreGive(ctx->sub_mutex);
if (!added) LOG_W(TAG, "subscriber list full");
return added;
}
static void api_hid_unsubscribe(struct Device* device, UsbHidQueueHandle event_queue) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
if (!ctx || !event_queue) return;
if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) {
LOG_W(TAG, "unsubscribe: mutex timeout, subscriber slot may remain stale");
return;
}
for (int i = 0; i < MAX_SUBSCRIBERS; i++) {
if (ctx->subscribers[i] == static_cast<QueueHandle_t>(event_queue)) {
ctx->subscribers[i] = nullptr;
break;
}
}
xSemaphoreGive(ctx->sub_mutex);
}
static const UsbHidApi hid_api = {
.is_connected = api_hid_is_connected,
.subscribe = api_hid_subscribe,
.unsubscribe = api_hid_unsubscribe,
};
extern "C" {
static error_t start_device(struct Device* device) {
auto* ctx = new UsbHidContext();
ctx->sub_mutex = xSemaphoreCreateMutex();
if (!ctx->sub_mutex) {
LOG_E(TAG, "failed to create subscriber mutex");
delete ctx;
return ERROR_RESOURCE;
}
ctx->hid_event_queue = xQueueCreate(HID_EVENT_QUEUE_SIZE, sizeof(hid_dev_event_t));
if (!ctx->hid_event_queue) {
LOG_E(TAG, "failed to create HID event queue");
vSemaphoreDelete(ctx->sub_mutex);
delete ctx;
return ERROR_RESOURCE;
}
ctx->hid_proc_task_done = xSemaphoreCreateBinary();
if (!ctx->hid_proc_task_done) {
LOG_E(TAG, "failed to create task done semaphore");
vQueueDelete(ctx->hid_event_queue);
vSemaphoreDelete(ctx->sub_mutex);
delete ctx;
return ERROR_RESOURCE;
}
const hid_host_driver_config_t hid_cfg = {
.create_background_task = true,
.task_priority = HID_PROC_TASK_PRIORITY,
.stack_size = HID_PROC_TASK_STACK,
.core_id = tskNO_AFFINITY,
.callback = hid_driver_callback,
.callback_arg = ctx,
};
if (hid_host_install(&hid_cfg) != ESP_OK) {
LOG_E(TAG, "hid_host_install failed");
vQueueDelete(ctx->hid_event_queue);
vSemaphoreDelete(ctx->hid_proc_task_done);
vSemaphoreDelete(ctx->sub_mutex);
delete ctx;
return ERROR_RESOURCE;
}
ctx->hid_proc_running = true;
BaseType_t result = xTaskCreate(hid_proc_task, "hid_proc", HID_PROC_TASK_STACK,
ctx, HID_PROC_TASK_PRIORITY, &ctx->hid_proc_task);
if (result != pdPASS) {
LOG_E(TAG, "failed to create hid_proc task");
ctx->hid_proc_running = false;
hid_host_uninstall();
vQueueDelete(ctx->hid_event_queue);
vSemaphoreDelete(ctx->hid_proc_task_done);
vSemaphoreDelete(ctx->sub_mutex);
delete ctx;
return ERROR_RESOURCE;
}
device_set_driver_data(device, ctx);
LOG_I(TAG, "started");
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
ctx->hid_proc_running = false;
if (xSemaphoreTake(ctx->hid_proc_task_done, pdMS_TO_TICKS(HID_STOP_TIMEOUT_MS)) != pdTRUE) {
LOG_W(TAG, "HID proc task stop timed out, force terminating");
vTaskDelete(ctx->hid_proc_task);
}
ctx->hid_proc_task = nullptr;
vSemaphoreDelete(ctx->hid_proc_task_done);
hid_host_uninstall();
if (ctx->hid_event_queue) { vQueueDelete(ctx->hid_event_queue); ctx->hid_event_queue = nullptr; }
if (ctx->sub_mutex) { vSemaphoreDelete(ctx->sub_mutex); ctx->sub_mutex = nullptr; }
device_set_driver_data(device, nullptr);
delete ctx;
LOG_I(TAG, "stopped");
return ERROR_NONE;
}
Driver esp32_usbhost_hid_driver = {
.name = "esp32_usbhost_hid",
.compatible = (const char*[]) { "espressif,esp32-usbhost-hid", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &hid_api,
.device_type = &USB_HOST_HID_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED
@@ -0,0 +1,314 @@
#include <sdkconfig.h>
#ifdef CONFIG_SOC_USB_OTG_SUPPORTED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/log.h>
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <freertos/portmacro.h>
#include <usb/usb_host.h>
#include <usb/usb_helpers.h>
#include <usb/usb_types_ch9.h>
#include <usb/usb_types_stack.h>
#define TAG "esp32_usbhost_midi"
constexpr auto MIDI_TASK_STACK = 4096;
constexpr auto MIDI_TASK_PRIORITY = 5;
constexpr auto MIDI_STOP_TIMEOUT_MS = 2000;
constexpr auto MIDI_TRANSFER_BUF_SIZE = 512;
constexpr uint8_t MIDI_INTF_CLASS = 0x01;
constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03;
struct UsbMidiContext {
usb_host_client_handle_t client_hdl = nullptr;
usb_device_handle_t dev_hdl = nullptr;
usb_transfer_t* transfer = nullptr;
uint8_t ep_addr = 0;
uint8_t intf_num = 0;
std::atomic<bool> connected{false};
std::atomic<bool> running{false};
TaskHandle_t task_handle = nullptr;
SemaphoreHandle_t task_done = nullptr;
portMUX_TYPE callback_lock = portMUX_INITIALIZER_UNLOCKED;
usb_midi_message_cb_t callback = nullptr;
void* callback_arg = nullptr;
};
static bool find_midi_interface(const usb_config_desc_t* cfg, uint8_t* out_intf, uint8_t* out_ep) {
int offset = 0;
const usb_standard_desc_t* cur = reinterpret_cast<const usb_standard_desc_t*>(cfg);
while ((cur = usb_parse_next_descriptor_of_type(
cur, cfg->wTotalLength, USB_W_VALUE_DT_INTERFACE, &offset)) != nullptr) {
const auto* intf = reinterpret_cast<const usb_intf_desc_t*>(cur);
if (intf->bInterfaceClass != MIDI_INTF_CLASS || intf->bInterfaceSubClass != MIDI_INTF_SUBCLASS) continue;
int ep_offset = offset;
const usb_standard_desc_t* ep_cur = cur;
for (int e = 0; e < intf->bNumEndpoints; e++) {
ep_cur = usb_parse_next_descriptor_of_type(ep_cur, cfg->wTotalLength, USB_W_VALUE_DT_ENDPOINT, &ep_offset);
if (!ep_cur) break;
const auto* ep = reinterpret_cast<const usb_ep_desc_t*>(ep_cur);
usb_transfer_type_t xtype = USB_EP_DESC_GET_XFERTYPE(ep);
bool is_in = USB_EP_DESC_GET_EP_DIR(ep) == 1;
if (is_in && (xtype == USB_TRANSFER_TYPE_BULK || xtype == USB_TRANSFER_TYPE_INTR)) {
*out_intf = intf->bInterfaceNumber;
*out_ep = ep->bEndpointAddress;
return true;
}
}
}
return false;
}
static void dispatch_midi_packets(UsbMidiContext* ctx, const uint8_t* buf, int len) {
usb_midi_message_cb_t cb;
void* arg;
taskENTER_CRITICAL(&ctx->callback_lock);
cb = ctx->callback;
arg = ctx->callback_arg;
taskEXIT_CRITICAL(&ctx->callback_lock);
if (!cb) return;
for (int i = 0; i + 3 < len; i += 4) {
uint8_t cin = buf[i] & 0x0F;
if (cin < 0x02) continue;
usb_midi_message_t msg = {
.cable = static_cast<uint8_t>(buf[i] >> 4),
.status = buf[i + 1],
.data1 = buf[i + 2],
.data2 = buf[i + 3],
};
cb(&msg, arg);
}
}
static void midi_transfer_cb(usb_transfer_t* transfer) {
auto* ctx = static_cast<UsbMidiContext*>(transfer->context);
if (transfer->status == USB_TRANSFER_STATUS_COMPLETED && transfer->actual_num_bytes > 0) {
dispatch_midi_packets(ctx, transfer->data_buffer, transfer->actual_num_bytes);
}
if (ctx->running && ctx->connected && transfer->status != USB_TRANSFER_STATUS_NO_DEVICE) {
transfer->num_bytes = MIDI_TRANSFER_BUF_SIZE;
if (usb_host_transfer_submit(transfer) != ESP_OK) {
LOG_E(TAG, "failed to resubmit MIDI transfer");
}
}
}
static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) {
auto* ctx = static_cast<UsbMidiContext*>(arg);
if (msg->event == USB_HOST_CLIENT_EVENT_NEW_DEV) {
if (ctx->dev_hdl != nullptr || ctx->connected.load()) {
LOG_W(TAG, "ignoring additional MIDI device while one is already active");
return;
}
uint8_t addr = msg->new_dev.address;
usb_device_handle_t dev_hdl = nullptr;
if (usb_host_device_open(ctx->client_hdl, addr, &dev_hdl) != ESP_OK) return;
const usb_config_desc_t* cfg = nullptr;
if (usb_host_get_active_config_descriptor(dev_hdl, &cfg) != ESP_OK) {
usb_host_device_close(ctx->client_hdl, dev_hdl);
return;
}
uint8_t intf_num = 0, ep_addr = 0;
if (!find_midi_interface(cfg, &intf_num, &ep_addr)) {
LOG_D(TAG, "USB device addr=%d: no MIDI Streaming interface found", addr);
usb_host_device_close(ctx->client_hdl, dev_hdl);
return;
}
if (usb_host_interface_claim(ctx->client_hdl, dev_hdl, intf_num, 0) != ESP_OK) {
LOG_E(TAG, "failed to claim MIDI interface %d", intf_num);
usb_host_device_close(ctx->client_hdl, dev_hdl);
return;
}
ctx->dev_hdl = dev_hdl;
ctx->intf_num = intf_num;
ctx->ep_addr = ep_addr;
ctx->transfer->device_handle = dev_hdl;
ctx->transfer->bEndpointAddress = ep_addr;
ctx->transfer->num_bytes = MIDI_TRANSFER_BUF_SIZE;
ctx->transfer->callback = midi_transfer_cb;
ctx->transfer->context = ctx;
ctx->transfer->timeout_ms = 0;
if (usb_host_transfer_submit(ctx->transfer) == ESP_OK) {
ctx->connected = true;
LOG_I(TAG, "MIDI device connected (intf=%d ep=0x%02x)", intf_num, ep_addr);
} else {
LOG_E(TAG, "failed to submit initial MIDI transfer");
usb_host_interface_release(ctx->client_hdl, dev_hdl, intf_num);
usb_host_device_close(ctx->client_hdl, dev_hdl);
ctx->dev_hdl = nullptr;
}
} else if (msg->event == USB_HOST_CLIENT_EVENT_DEV_GONE) {
if (ctx->dev_hdl && msg->dev_gone.dev_hdl == ctx->dev_hdl) {
LOG_I(TAG, "MIDI device disconnected");
ctx->connected = false;
usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num);
usb_host_device_close(ctx->client_hdl, ctx->dev_hdl);
ctx->dev_hdl = nullptr;
}
}
}
static void midi_client_task(void* arg) {
auto* ctx = static_cast<UsbMidiContext*>(arg);
LOG_I(TAG, "MIDI client task started");
while (ctx->running) {
usb_host_client_handle_events(ctx->client_hdl, pdMS_TO_TICKS(100));
}
if (ctx->dev_hdl) {
ctx->connected = false;
usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num);
usb_host_device_close(ctx->client_hdl, ctx->dev_hdl);
ctx->dev_hdl = nullptr;
}
LOG_I(TAG, "MIDI client task stopped");
xSemaphoreGive(ctx->task_done);
vTaskDelete(nullptr);
}
static void api_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device));
if (!ctx) return;
taskENTER_CRITICAL(&ctx->callback_lock);
ctx->callback = callback;
ctx->callback_arg = user_data;
taskEXIT_CRITICAL(&ctx->callback_lock);
}
static bool api_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device));
return ctx && ctx->connected.load();
}
static const UsbMidiApi midi_api = {
.set_callback = api_set_callback,
.is_connected = api_is_connected,
};
extern "C" {
static error_t start_device(struct Device* device) {
auto* ctx = new UsbMidiContext();
if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) {
LOG_E(TAG, "failed to allocate MIDI transfer");
delete ctx;
return ERROR_RESOURCE;
}
ctx->task_done = xSemaphoreCreateBinary();
if (!ctx->task_done) {
LOG_E(TAG, "failed to create task done semaphore");
usb_host_transfer_free(ctx->transfer);
delete ctx;
return ERROR_RESOURCE;
}
const usb_host_client_config_t client_cfg = {
.is_synchronous = false,
.max_num_event_msg = 5,
.async = {
.client_event_callback = client_event_cb,
.callback_arg = ctx,
},
};
if (usb_host_client_register(&client_cfg, &ctx->client_hdl) != ESP_OK) {
LOG_E(TAG, "failed to register USB host client");
vSemaphoreDelete(ctx->task_done);
usb_host_transfer_free(ctx->transfer);
delete ctx;
return ERROR_RESOURCE;
}
ctx->running = true;
BaseType_t result = xTaskCreate(midi_client_task, "midi_client", MIDI_TASK_STACK,
ctx, MIDI_TASK_PRIORITY, &ctx->task_handle);
if (result != pdPASS) {
LOG_E(TAG, "failed to create midi_client task");
ctx->running = false;
usb_host_client_deregister(ctx->client_hdl);
vSemaphoreDelete(ctx->task_done);
usb_host_transfer_free(ctx->transfer);
delete ctx;
return ERROR_RESOURCE;
}
device_set_driver_data(device, ctx);
LOG_I(TAG, "started");
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
ctx->running = false;
usb_host_client_unblock(ctx->client_hdl);
if (xSemaphoreTake(ctx->task_done, pdMS_TO_TICKS(MIDI_STOP_TIMEOUT_MS)) != pdTRUE) {
LOG_E(TAG, "MIDI client task stop timed out after %dms — a full USB host restart may be required", MIDI_STOP_TIMEOUT_MS);
if (ctx->dev_hdl) {
ctx->connected = false;
if (usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num) != ESP_OK) {
LOG_W(TAG, "failed to release MIDI interface during force-stop");
}
if (usb_host_device_close(ctx->client_hdl, ctx->dev_hdl) != ESP_OK) {
LOG_W(TAG, "failed to close MIDI device during force-stop");
}
ctx->dev_hdl = nullptr;
}
vTaskDelete(ctx->task_handle);
vTaskDelay(pdMS_TO_TICKS(50));
}
ctx->task_handle = nullptr;
vSemaphoreDelete(ctx->task_done);
usb_host_client_deregister(ctx->client_hdl);
ctx->client_hdl = nullptr;
usb_host_transfer_free(ctx->transfer);
ctx->transfer = nullptr;
device_set_driver_data(device, nullptr);
delete ctx;
LOG_I(TAG, "stopped");
return ERROR_NONE;
}
Driver esp32_usbhost_midi_driver = {
.name = "esp32_usbhost_midi",
.compatible = (const char*[]) { "espressif,esp32-usbhost-midi", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &midi_api,
.device_type = &USB_HOST_MIDI_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED
@@ -0,0 +1,377 @@
#include <sdkconfig.h>
#ifdef CONFIG_SOC_USB_OTG_SUPPORTED
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/usb_host_msc.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h>
#include <atomic>
#include <cstring>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <freertos/portmacro.h>
#include <usb/msc_host.h>
#include <usb/msc_host_vfs.h>
#include <esp_vfs_fat.h>
#define TAG "esp32_usbhost_msc"
#define USB_MSC_MOUNT_PATH_PREFIX "/usb"
constexpr auto MAX_MSC_DEVICES = 2;
constexpr auto MSC_EVENT_QUEUE_SIZE = 8;
constexpr auto MSC_PROC_TASK_STACK = 4096;
constexpr auto MSC_PROC_TASK_PRIORITY = 5;
constexpr auto MSC_STOP_TIMEOUT_MS = 2000;
typedef struct {
uint8_t usb_addr;
msc_host_device_handle_t device;
msc_host_vfs_handle_t vfs;
char mount_path[16];
struct FileSystem* fs_entry;
bool mounted;
} msc_dev_entry_t;
// The anonymous enum inside msc_host_event_t is C-only scoped; use a local alias in C++.
using msc_event_id_t = decltype(msc_host_event_t{}.event);
static constexpr msc_event_id_t kMscDeviceConnected = static_cast<msc_event_id_t>(0);
static constexpr msc_event_id_t kMscDeviceDisconnected = static_cast<msc_event_id_t>(1);
enum class MscMsgId : uint8_t { Connected, Disconnected };
typedef struct {
MscMsgId id;
union {
uint8_t address;
msc_host_device_handle_t handle;
};
} msc_msg_t;
struct UsbMscContext {
msc_dev_entry_t* devs[MAX_MSC_DEVICES] = {};
portMUX_TYPE devs_lock = portMUX_INITIALIZER_UNLOCKED;
QueueHandle_t event_queue = nullptr;
TaskHandle_t proc_task = nullptr;
SemaphoreHandle_t proc_task_done = nullptr;
std::atomic<bool> proc_running{false};
};
static error_t usb_fs_mount(void* /*data*/) { return ERROR_NONE; }
static error_t usb_fs_unmount(void* /*data*/) { return ERROR_NONE; }
static bool usb_fs_is_mounted(void* data) {
if (!data) return false;
return static_cast<msc_dev_entry_t*>(data)->mounted;
}
static error_t usb_fs_get_path(void* data, char* out_path, size_t out_path_size) {
if (!data) return ERROR_INVALID_ARGUMENT;
snprintf(out_path, out_path_size, "%s", static_cast<msc_dev_entry_t*>(data)->mount_path);
return ERROR_NONE;
}
static const FileSystemApi usb_fs_api = {
.mount = usb_fs_mount,
.unmount = usb_fs_unmount,
.is_mounted = usb_fs_is_mounted,
.get_path = usb_fs_get_path,
};
static int find_free_slot(UsbMscContext* ctx) {
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (ctx->devs[i] == nullptr) return i;
}
return -1;
}
static int find_slot_by_handle(UsbMscContext* ctx, msc_host_device_handle_t handle) {
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (ctx->devs[i] && ctx->devs[i]->device == handle) return i;
}
return -1;
}
static void free_msc_device(UsbMscContext* ctx, int slot) {
if (slot < 0 || slot >= MAX_MSC_DEVICES || !ctx->devs[slot]) return;
if (ctx->devs[slot]->fs_entry) {
ctx->devs[slot]->mounted = false;
file_system_remove(ctx->devs[slot]->fs_entry);
ctx->devs[slot]->fs_entry = nullptr;
}
if (ctx->devs[slot]->vfs) {
msc_host_vfs_unregister(ctx->devs[slot]->vfs);
}
if (ctx->devs[slot]->device) {
msc_host_uninstall_device(ctx->devs[slot]->device);
}
free(ctx->devs[slot]);
ctx->devs[slot] = nullptr;
}
static void free_all_msc_devices(UsbMscContext* ctx) {
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
free_msc_device(ctx, i);
}
}
static void msc_event_cb(const msc_host_event_t* event, void* arg) {
auto* ctx = static_cast<UsbMscContext*>(arg);
if (!ctx->event_queue) return;
msc_msg_t msg = {};
if (event->event == kMscDeviceConnected) {
msg.id = MscMsgId::Connected;
msg.address = event->device.address;
if (xQueueSend(ctx->event_queue, &msg, 0) != pdTRUE) {
LOG_W(TAG, "event queue full, dropped Connected event (addr=%d)", event->device.address);
}
} else if (event->event == kMscDeviceDisconnected) {
msg.id = MscMsgId::Disconnected;
msg.handle = event->device.handle;
if (xQueueSend(ctx->event_queue, &msg, 0) != pdTRUE) {
LOG_W(TAG, "event queue full, dropped Disconnected event");
}
}
}
static void msc_proc_task(void* arg) {
auto* ctx = static_cast<UsbMscContext*>(arg);
LOG_I(TAG, "MSC proc task started");
while (ctx->proc_running) {
msc_msg_t msg;
if (xQueueReceive(ctx->event_queue, &msg, pdMS_TO_TICKS(100)) != pdTRUE) continue;
if (msg.id == MscMsgId::Connected) {
LOG_I(TAG, "USB drive connected (addr=%d)", msg.address);
// Find a free slot under the lock, then allocate outside it.
taskENTER_CRITICAL(&ctx->devs_lock);
int slot = find_free_slot(ctx);
taskEXIT_CRITICAL(&ctx->devs_lock);
if (slot < 0) {
LOG_W(TAG, "no free slots for USB drive");
continue;
}
auto* entry = static_cast<msc_dev_entry_t*>(calloc(1, sizeof(msc_dev_entry_t)));
if (!entry) {
LOG_E(TAG, "failed to allocate MSC device entry");
continue;
}
// Re-check the slot is still free before committing.
taskENTER_CRITICAL(&ctx->devs_lock);
if (ctx->devs[slot] != nullptr) {
slot = find_free_slot(ctx); // another path claimed it; find a new one
if (slot >= 0) ctx->devs[slot] = entry;
} else {
ctx->devs[slot] = entry;
}
taskEXIT_CRITICAL(&ctx->devs_lock);
if (slot < 0) {
free(entry);
LOG_W(TAG, "no free slots for USB drive after allocation");
continue;
}
if (msc_host_install_device(msg.address, &ctx->devs[slot]->device) != ESP_OK) {
LOG_E(TAG, "msc_host_install_device failed");
taskENTER_CRITICAL(&ctx->devs_lock);
free(ctx->devs[slot]);
ctx->devs[slot] = nullptr;
taskEXIT_CRITICAL(&ctx->devs_lock);
continue;
}
ctx->devs[slot]->usb_addr = msg.address;
snprintf(ctx->devs[slot]->mount_path, sizeof(ctx->devs[slot]->mount_path),
USB_MSC_MOUNT_PATH_PREFIX "%d", slot);
const char* mount_path = ctx->devs[slot]->mount_path;
const esp_vfs_fat_mount_config_t mount_cfg = {
.format_if_mount_failed = false,
.max_files = 4,
.allocation_unit_size = 4096,
.disk_status_check_enable = false,
.use_one_fat = false,
};
esp_err_t vfs_err = msc_host_vfs_register(ctx->devs[slot]->device, mount_path, &mount_cfg, &ctx->devs[slot]->vfs);
if (vfs_err != ESP_OK) {
LOG_E(TAG, "msc_host_vfs_register failed for %s: %s", mount_path, esp_err_to_name(vfs_err));
msc_host_uninstall_device(ctx->devs[slot]->device);
taskENTER_CRITICAL(&ctx->devs_lock);
free(ctx->devs[slot]);
ctx->devs[slot] = nullptr;
taskEXIT_CRITICAL(&ctx->devs_lock);
continue;
}
LOG_I(TAG, "USB drive mounted at %s", mount_path);
ctx->devs[slot]->fs_entry = file_system_add(&usb_fs_api, ctx->devs[slot]);
if (!ctx->devs[slot]->fs_entry) {
LOG_E(TAG, "failed to register filesystem for %s", mount_path);
free_msc_device(ctx, slot);
continue;
}
ctx->devs[slot]->mounted = true;
} else if (msg.id == MscMsgId::Disconnected) {
taskENTER_CRITICAL(&ctx->devs_lock);
int slot = find_slot_by_handle(ctx, msg.handle);
taskEXIT_CRITICAL(&ctx->devs_lock);
if (slot >= 0) {
LOG_I(TAG, "USB drive disconnected, unmounting slot %d", slot);
free_msc_device(ctx, slot);
}
}
}
free_all_msc_devices(ctx);
LOG_I(TAG, "MSC proc task stopped");
xSemaphoreGive(ctx->proc_task_done);
vTaskDelete(nullptr);
}
static bool api_eject(struct Device* device, const char* mount_path) {
auto* ctx = static_cast<UsbMscContext*>(device_get_driver_data(device));
if (!ctx) return false;
taskENTER_CRITICAL(&ctx->devs_lock);
msc_dev_entry_t* entry = nullptr;
int found = -1;
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (ctx->devs[i] && strcmp(ctx->devs[i]->mount_path, mount_path) == 0) {
found = i;
entry = ctx->devs[i];
ctx->devs[i] = nullptr; // claim atomically under the lock
break;
}
}
taskEXIT_CRITICAL(&ctx->devs_lock);
if (found >= 0) {
LOG_I(TAG, "ejecting USB drive at %s (slot %d)", mount_path, found);
// Free outside the lock; slot is already claimed (nullptr) so msc_proc_task won't touch it.
if (entry->fs_entry) {
entry->mounted = false;
file_system_remove(entry->fs_entry);
entry->fs_entry = nullptr;
}
if (entry->vfs) {
msc_host_vfs_unregister(entry->vfs);
}
if (entry->device) {
msc_host_uninstall_device(entry->device);
}
free(entry);
LOG_I(TAG, "USB drive ejected, safe to remove");
return true;
}
LOG_W(TAG, "no drive mounted at %s", mount_path);
return false;
}
static const UsbMscApi msc_api = {
.eject = api_eject,
};
extern "C" {
static error_t start_device(struct Device* device) {
auto* ctx = new UsbMscContext();
ctx->event_queue = xQueueCreate(MSC_EVENT_QUEUE_SIZE, sizeof(msc_msg_t));
if (!ctx->event_queue) {
LOG_E(TAG, "failed to create MSC event queue");
delete ctx;
return ERROR_RESOURCE;
}
ctx->proc_task_done = xSemaphoreCreateBinary();
if (!ctx->proc_task_done) {
LOG_E(TAG, "failed to create task done semaphore");
vQueueDelete(ctx->event_queue);
delete ctx;
return ERROR_RESOURCE;
}
const msc_host_driver_config_t msc_cfg = {
.create_backround_task = true,
.task_priority = MSC_PROC_TASK_PRIORITY,
.stack_size = MSC_PROC_TASK_STACK,
.core_id = tskNO_AFFINITY,
.callback = msc_event_cb,
.callback_arg = ctx,
};
if (msc_host_install(&msc_cfg) != ESP_OK) {
LOG_E(TAG, "msc_host_install failed");
vQueueDelete(ctx->event_queue);
vSemaphoreDelete(ctx->proc_task_done);
delete ctx;
return ERROR_RESOURCE;
}
ctx->proc_running = true;
BaseType_t result = xTaskCreate(msc_proc_task, "msc_proc", MSC_PROC_TASK_STACK,
ctx, MSC_PROC_TASK_PRIORITY, &ctx->proc_task);
if (result != pdPASS) {
LOG_E(TAG, "failed to create msc_proc task");
ctx->proc_running = false;
msc_host_uninstall();
vQueueDelete(ctx->event_queue);
vSemaphoreDelete(ctx->proc_task_done);
delete ctx;
return ERROR_RESOURCE;
}
device_set_driver_data(device, ctx);
LOG_I(TAG, "started");
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbMscContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
ctx->proc_running = false;
bool exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(MSC_STOP_TIMEOUT_MS)) == pdTRUE);
if (!exited) {
// Retry once with a short extra wait before resorting to force-delete.
exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(500)) == pdTRUE);
}
if (!exited) {
LOG_W(TAG, "MSC proc task stop timed out, force terminating");
vTaskDelete(ctx->proc_task);
vTaskDelay(pdMS_TO_TICKS(50));
// Task was killed mid-cleanup — free devices ourselves as best-effort.
free_all_msc_devices(ctx);
}
ctx->proc_task = nullptr;
vSemaphoreDelete(ctx->proc_task_done);
msc_host_uninstall();
if (ctx->event_queue) { vQueueDelete(ctx->event_queue); ctx->event_queue = nullptr; }
device_set_driver_data(device, nullptr);
delete ctx;
LOG_I(TAG, "stopped");
return ERROR_NONE;
}
Driver esp32_usbhost_msc_driver = {
.name = "esp32_usbhost_msc",
.compatible = (const char*[]) { "espressif,esp32-usbhost-msc", nullptr },
.start_device = start_device,
.stop_device = stop_device,
.api = &msc_api,
.device_type = &USB_HOST_MSC_TYPE,
.owner = nullptr,
.internal = nullptr,
};
} // extern "C"
#endif // CONFIG_SOC_USB_OTG_SUPPORTED
@@ -25,6 +25,12 @@ extern Driver esp32_ble_serial_driver;
extern Driver esp32_ble_midi_driver;
extern Driver esp32_ble_hid_device_driver;
#endif
#if SOC_USB_OTG_SUPPORTED
extern Driver esp32_usbhost_driver;
extern Driver esp32_usbhost_hid_driver;
extern Driver esp32_usbhost_midi_driver;
extern Driver esp32_usbhost_msc_driver;
#endif
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
@@ -42,6 +48,12 @@ static error_t start() {
check(driver_construct_add(&esp32_ble_serial_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_ble_midi_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_ble_hid_device_driver) == ERROR_NONE);
#endif
#if SOC_USB_OTG_SUPPORTED
check(driver_construct_add(&esp32_usbhost_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_hid_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_midi_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE);
#endif
return ERROR_NONE;
}
@@ -49,6 +61,12 @@ static error_t start() {
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
#if SOC_USB_OTG_SUPPORTED
check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_midi_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_hid_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_driver) == ERROR_NONE);
#endif
#if defined(CONFIG_BT_NIMBLE_ENABLED)
check(driver_remove_destruct(&esp32_ble_hid_device_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_ble_midi_driver) == ERROR_NONE);