diff --git a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts index cd1002566..7467a9020 100644 --- a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts +++ b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts @@ -107,20 +107,5 @@ usbhost0 { compatible = "espressif,esp32-usbhost"; - - usbhosthid0 { - compatible = "espressif,esp32-usbhost-hid"; - //status = "disabled"; - }; - - usbhostmidi0 { - compatible = "espressif,esp32-usbhost-midi"; - //status = "disabled"; - }; - - usbhostmsc0 { - compatible = "espressif,esp32-usbhost-msc"; - //status = "disabled"; - }; }; }; diff --git a/Devices/m5stack-tab5/m5stack,tab5.dts b/Devices/m5stack-tab5/m5stack,tab5.dts index 470bbd632..07218a8b0 100644 --- a/Devices/m5stack-tab5/m5stack,tab5.dts +++ b/Devices/m5stack-tab5/m5stack,tab5.dts @@ -198,26 +198,6 @@ usbhost0 { compatible = "espressif,esp32-usbhost"; peripheral-map = <0>; - - usbhosthid0 { - compatible = "espressif,esp32-usbhost-hid"; - }; - - usbhostmidi0 { - compatible = "espressif,esp32-usbhost-midi"; - }; - - usbhostmsc0 { - compatible = "espressif,esp32-usbhost-msc"; - }; - - // USB headphones/headsets (UAC 1.0, 48 kHz / 16-bit / stereo PCM output). - // The manager installs the class client; a dynamic "usb_uac0" codec device - // appears per connected headset and the audio-stream aggregate re-binds - // output to it. - usbhostaudio0 { - compatible = "espressif,esp32-usbhost-uac"; - }; }; usbdevice0 { diff --git a/Devices/tulip-4r11/tulip-4r11.dts b/Devices/tulip-4r11/tulip-4r11.dts index c5acd59f6..8875b61f6 100644 --- a/Devices/tulip-4r11/tulip-4r11.dts +++ b/Devices/tulip-4r11/tulip-4r11.dts @@ -177,17 +177,5 @@ usbhost0 { compatible = "espressif,esp32-usbhost"; - - usbhosthid0 { - compatible = "espressif,esp32-usbhost-hid"; - }; - - usbhostmidi0 { - compatible = "espressif,esp32-usbhost-midi"; - }; - - usbhostmsc0 { - compatible = "espressif,esp32-usbhost-msc"; - }; }; }; diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 64d4a7740..15f7505a9 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -7,12 +7,11 @@ ## Higher Priority -- Update to LVGL 9.6.x (ESP-IDF and POSIX) -- NimBLE looses pairing key after reboot -- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp -- Add bold fonts for e-ink readability improvement +- Improve posix support: use user home path and system /tmp folder. implement uart/i2c drivers properly. - Improve Setup: Add keyboard/keypad/encoder navigation explanation. e.g. encoder long press on t-lora pager to get out of textarea +- NimBLE looses pairing key after reboot +- Pick different font to improve monochrome screen readability (or change the weight). - Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. - Improve SPI kernel driver (implement read, write, transactions) - TCA9534 keyboards should use interrupts @@ -42,7 +41,6 @@ - Consider moving certain drivers into separate modules: audio, bt, wifi, etc - Consider using https://github.com/Graphify-Labs/graphify - Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html -- Make USB host driver disabled by default, so it doesn't consume memory - TactilityTool: Make API compatibility table (and check for compatibility in the tool itself) - Refactor HttpServer into C code and move implementation to http-module - Use GPS time to set/update the current time diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml deleted file mode 100644 index 8b59c903e..000000000 --- a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-hid.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml deleted file mode 100644 index 878cfbe85..000000000 --- a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-midi.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml deleted file mode 100644 index b2afee470..000000000 --- a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-msc.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-uac.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-uac.yaml deleted file mode 100644 index e69fdaa76..000000000 --- a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost-uac.yaml +++ /dev/null @@ -1,9 +0,0 @@ -description: ESP32 USB Host UAC manager (installs the USB Audio Class client) - -compatible: "espressif,esp32-usbhost-uac" - -properties: - _unused: - type: int - default: 0 - description: Placeholder required by the binding schema; this driver has no device-tree configuration. diff --git a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml index 80f20a1ea..00d80ee70 100644 --- a/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml +++ b/Platforms/platform-esp32/bindings/espressif,esp32-usbhost.yaml @@ -13,3 +13,19 @@ properties: 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) + hid: + type: boolean + default: true + description: Detect HID devices (keyboard, mouse, consumer control) + midi: + type: boolean + default: true + description: Detect MIDI devices + msc: + type: boolean + default: true + description: Detect and mount mass storage devices + audio: + type: boolean + default: true + description: Detect USB Audio Class output devices (ESP32-S3 and ESP32-P4 only, ignored on other targets) diff --git a/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h index 6e82a111b..977a10a71 100644 --- a/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h +++ b/Platforms/platform-esp32/include/tactility/bindings/esp32_usbhost.h @@ -9,10 +9,6 @@ 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) -DEFINE_DEVICETREE(esp32_usbhost_uac, struct Esp32UsbHostChildConfig) #ifdef __cplusplus } diff --git a/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h index 0a9fdff75..8b2c3f877 100644 --- a/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h +++ b/Platforms/platform-esp32/include/tactility/drivers/esp32_usbhost.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include #include #ifdef __cplusplus @@ -9,10 +10,10 @@ extern "C" { 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; + bool hid; + bool midi; + bool msc; + bool audio; }; #ifdef __cplusplus diff --git a/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_backend.h b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_backend.h new file mode 100644 index 000000000..d8fb4e816 --- /dev/null +++ b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_backend.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +struct Device; + +/** + * Detects connections for one USB class while the USB host runs. + * Creates a child device of the host for each connection and destroys it on disconnect. + */ +struct Esp32UsbHostBackend { + /** + * @param[in] host the USB host device + * @param[out] out_context the backend state, passed to stop() + */ + error_t (*start)(struct Device* host, void** out_context); + /** + * Destroys the connected devices, then releases the backend. + * @retval ERROR_NONE the context was freed + * @return any other error when the context is still in use and was retained + */ + error_t (*stop)(void* context); +}; + +extern const struct Esp32UsbHostBackend esp32_usbhost_hid_backend; +extern const struct Esp32UsbHostBackend esp32_usbhost_midi_backend; +extern const struct Esp32UsbHostBackend esp32_usbhost_msc_backend; +extern const struct Esp32UsbHostBackend esp32_usbhost_uac_backend; diff --git a/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_task.h b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_task.h new file mode 100644 index 000000000..04082ffc5 --- /dev/null +++ b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_task.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include +#include +#include + +/** + * Creates a task with its stack in PSRAM when available, otherwise in internal RAM. + * The task must be deleted with vTaskDeleteWithCaps(). + * The task must never perform SPI flash operations, because these crash when the stack is in PSRAM. + */ +inline BaseType_t esp32_usbhost_task_create_psram(TaskFunction_t function, const char* name, uint32_t stack_size, void* arg, UBaseType_t priority, TaskHandle_t* handle) { +#if CONFIG_SPIRAM + if (xTaskCreateWithCaps(function, name, stack_size, arg, priority, handle, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT) == pdPASS) { + return pdPASS; + } +#endif + return xTaskCreateWithCaps(function, name, stack_size, arg, priority, handle, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); +} diff --git a/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h new file mode 100644 index 000000000..035eb3a57 --- /dev/null +++ b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +struct Device; +struct Driver; + +/** + * Runs a function on the USB host worker task and blocks until it returns. + * Use it for device_start() and device_stop() of dynamic devices, so device listeners never run on a USB task. + * The worker task is created on demand, so this can fail when memory is low. + * @param[in] host the USB host device + * @param[in] function the function to run + * @param[in] context passed to the function + * @retval ERROR_NONE when the function ran + * @retval ERROR_INVALID_STATE when the host is not started + * @retval ERROR_RESOURCE when the worker task could not be created + */ +error_t esp32_usbhost_run_on_worker(struct Device* host, void (*function)(void* context), void* context); + +/** + * Constructs, adds and starts a device on the worker task. + * The driver data is set before the device starts. + * @param[in] host the USB host device + * @param[in,out] device storage that must outlive a successful esp32_usbhost_device_destroy() + * @param[in] parent the parent of the new device + * @retval ERROR_NONE when the device started + */ +error_t esp32_usbhost_device_create(struct Device* host, struct Device* device, struct Device* parent, const char* name, struct Driver* driver, void* driver_data); + +/** + * Stops, removes and destructs a device on the worker task. + * Waits a while for references held by other tasks to be released. + * @param[in] host the USB host device + * @param[in,out] device a device created with esp32_usbhost_device_create() + * @retval ERROR_NONE when the device was destructed + * @retval ERROR_RESOURCE_BUSY when references are still held. The device storage is still in use and calling this again retries. + */ +error_t esp32_usbhost_device_destroy(struct Device* host, struct Device* device); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp index 5eee7e5d9..1f278c05a 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp @@ -4,9 +4,14 @@ #include #include #include +#include +#include +#include +#include #include #include +#include #include #include #include @@ -16,17 +21,40 @@ #define GET_CONFIG(device) ((const Esp32UsbHostConfig*)(device)->config) +#define USB_HOST_AUDIO_SUPPORTED (CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3) + 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; +constexpr auto USB_WORKER_TASK_STACK = 4096; +constexpr auto USB_WORKER_TASK_PRIORITY = 5; +constexpr auto USB_WORKER_QUEUE_SIZE = 4; +constexpr auto USB_WORKER_IDLE_TIMEOUT_MS = 1000; +constexpr auto USB_HOST_CLASS_COUNT = USB_HOST_CLASS_AUDIO + 1; +constexpr auto DEVICE_DESTRUCT_RETRIES = 100; -struct UsbHostContext { - TaskHandle_t lib_task = nullptr; - SemaphoreHandle_t lib_task_done = nullptr; +struct UsbHostWorkerJob { + // nullptr stops the worker + void (*function)(void* context); + void* context; + StaticSemaphore_t done_buffer; + SemaphoreHandle_t done; }; +struct UsbHostContext { + // Exists only while there is work, guarded by worker_mutex + TaskHandle_t worker_task = nullptr; + QueueHandle_t worker_queue = nullptr; + SemaphoreHandle_t worker_mutex = nullptr; + TaskHandle_t lib_task = nullptr; + SemaphoreHandle_t lib_task_done = nullptr; + // Backend contexts indexed by UsbClass, nullptr when not running + void* backends[USB_HOST_CLASS_COUNT] = {}; +}; + +// Stack may be in PSRAM: no flash access allowed from this task. static void usbLibTask(void* arg) { auto* ctx = static_cast(arg); LOG_I(TAG, "lib task started"); @@ -53,18 +81,237 @@ static void usbLibTask(void* arg) { LOG_I(TAG, "lib task stopping"); xSemaphoreGive(ctx->lib_task_done); + vTaskDeleteWithCaps(nullptr); +} + +// region Worker + +// Runs device lifecycle work for the class drivers, so device listeners never run on a USB task. +// The stack stays in internal RAM because listeners may access flash. +// Created on demand and exits when idle, so it uses no memory while nothing is plugged in or out. +static void usbWorkerTask(void* arg) { + auto* ctx = static_cast(arg); + while (true) { + UsbHostWorkerJob* job = nullptr; + if (xQueueReceive(ctx->worker_queue, &job, pdMS_TO_TICKS(USB_WORKER_IDLE_TIMEOUT_MS)) != pdTRUE) { + // Jobs are queued under worker_mutex, so none can arrive between this check and the exit + xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY); + const bool idle = uxQueueMessagesWaiting(ctx->worker_queue) == 0; + if (idle) { + ctx->worker_task = nullptr; + } + xSemaphoreGive(ctx->worker_mutex); + if (idle) { + break; + } + continue; + } + if (job->function == nullptr) { + xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY); + ctx->worker_task = nullptr; + xSemaphoreGive(ctx->worker_mutex); + xSemaphoreGive(job->done); + break; + } + job->function(job->context); + xSemaphoreGive(job->done); + } vTaskDelete(nullptr); } -extern "C" { +static void wait_for_job(UsbHostWorkerJob* job) { + xSemaphoreTake(job->done, portMAX_DELAY); + vSemaphoreDelete(job->done); +} -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; +static error_t create_worker_queue(UsbHostContext* ctx) { + ctx->worker_mutex = xSemaphoreCreateMutex(); + ctx->worker_queue = xQueueCreate(USB_WORKER_QUEUE_SIZE, sizeof(UsbHostWorkerJob*)); + if (ctx->worker_mutex == nullptr || ctx->worker_queue == nullptr) { + LOG_E(TAG, "failed to create worker queue"); + if (ctx->worker_mutex != nullptr) vSemaphoreDelete(ctx->worker_mutex); + if (ctx->worker_queue != nullptr) vQueueDelete(ctx->worker_queue); + ctx->worker_mutex = nullptr; + ctx->worker_queue = nullptr; + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +// No jobs may be submitted anymore +static void delete_worker_queue(UsbHostContext* ctx) { + UsbHostWorkerJob job = { .function = nullptr, .context = nullptr, .done_buffer = {}, .done = nullptr }; + job.done = xSemaphoreCreateBinaryStatic(&job.done_buffer); + UsbHostWorkerJob* job_pointer = &job; + + xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY); + const bool running = ctx->worker_task != nullptr; + if (running) { + xQueueSend(ctx->worker_queue, &job_pointer, portMAX_DELAY); + } + xSemaphoreGive(ctx->worker_mutex); + + if (running) { + wait_for_job(&job); + } else { + vSemaphoreDelete(job.done); + } + vQueueDelete(ctx->worker_queue); + vSemaphoreDelete(ctx->worker_mutex); + ctx->worker_queue = nullptr; + ctx->worker_mutex = nullptr; +} + +error_t esp32_usbhost_run_on_worker(struct Device* host, void (*function)(void* context), void* context) { + auto* ctx = static_cast(device_get_driver_data(host)); + if (ctx == nullptr || ctx->worker_queue == nullptr) { + return ERROR_INVALID_STATE; } + UsbHostWorkerJob job = { .function = function, .context = context, .done_buffer = {}, .done = nullptr }; + UsbHostWorkerJob* job_pointer = &job; + + xSemaphoreTake(ctx->worker_mutex, portMAX_DELAY); + if (ctx->worker_task != nullptr && xTaskGetCurrentTaskHandle() == ctx->worker_task) { + xSemaphoreGive(ctx->worker_mutex); + function(context); + return ERROR_NONE; + } + if (ctx->worker_task == nullptr + && xTaskCreate(usbWorkerTask, "usb_worker", USB_WORKER_TASK_STACK, ctx, USB_WORKER_TASK_PRIORITY, &ctx->worker_task) != pdPASS) { + ctx->worker_task = nullptr; + xSemaphoreGive(ctx->worker_mutex); + LOG_E(TAG, "failed to create worker task"); + return ERROR_RESOURCE; + } + job.done = xSemaphoreCreateBinaryStatic(&job.done_buffer); + xQueueSend(ctx->worker_queue, &job_pointer, portMAX_DELAY); + xSemaphoreGive(ctx->worker_mutex); + + wait_for_job(&job); + return ERROR_NONE; +} + +// endregion + +// region Devices + +struct DeviceCreateJob { + Device* device; + Device* parent; + const char* name; + Driver* driver; + void* driver_data; + error_t result; +}; + +static void device_create_on_worker(void* context) { + auto* job = static_cast(context); + Device* device = job->device; + *device = Device {}; + device->name = job->name; + + job->result = device_construct(device); + if (job->result != ERROR_NONE) { + LOG_E(TAG, "failed to construct %s", job->name); + return; + } + device_set_driver_data(device, job->driver_data); + device_set_parent(device, job->parent); + device_set_driver(device, job->driver); + job->result = device_add(device); + if (job->result != ERROR_NONE) { + LOG_E(TAG, "failed to add %s", job->name); + device_destruct(device); + return; + } + job->result = device_start(device); + if (job->result != ERROR_NONE) { + LOG_E(TAG, "failed to start %s", job->name); + device_remove(device); + device_destruct(device); + } +} + +struct DeviceDestroyJob { + Device* device; + error_t result; +}; + +static void device_destroy_on_worker(void* context) { + auto* job = static_cast(context); + Device* device = job->device; + + if (device_is_added(device)) { + error_t error = device_stop(device); + if (error == ERROR_NONE) { + error = device_remove(device); + } + if (error != ERROR_NONE) { + LOG_E(TAG, "failed to stop %s: %s", device->name, error_to_string(error)); + job->result = error; + return; + } + } + + // Short-lived device_get() references block destruction + error_t error = device_destruct(device); + for (int i = 0; error == ERROR_RESOURCE_BUSY && i < DEVICE_DESTRUCT_RETRIES; i++) { + vTaskDelay(pdMS_TO_TICKS(10)); + error = device_destruct(device); + } + if (error != ERROR_NONE) { + LOG_E(TAG, "failed to destruct %s: %s", device->name, error_to_string(error)); + } + job->result = error; +} + +error_t esp32_usbhost_device_create(Device* host, Device* device, Device* parent, const char* name, Driver* driver, void* driver_data) { + DeviceCreateJob job = { device, parent, name, driver, driver_data, ERROR_NONE }; + error_t error = esp32_usbhost_run_on_worker(host, device_create_on_worker, &job); + return error != ERROR_NONE ? error : job.result; +} + +error_t esp32_usbhost_device_destroy(Device* host, Device* device) { + DeviceDestroyJob job = { device, ERROR_NONE }; + error_t error = esp32_usbhost_run_on_worker(host, device_destroy_on_worker, &job); + return error != ERROR_NONE ? error : job.result; +} + +// endregion + +// region Backends + +static const Esp32UsbHostBackend* get_backend(UsbClass usb_class) { + switch (usb_class) { + case USB_HOST_CLASS_HID: return &esp32_usbhost_hid_backend; + case USB_HOST_CLASS_MIDI: return &esp32_usbhost_midi_backend; + case USB_HOST_CLASS_MSC: return &esp32_usbhost_msc_backend; + case USB_HOST_CLASS_AUDIO: +#if USB_HOST_AUDIO_SUPPORTED + return &esp32_usbhost_uac_backend; +#else + return nullptr; +#endif + } + return nullptr; +} + +static bool is_class_configured(const Esp32UsbHostConfig* config, UsbClass usb_class) { + switch (usb_class) { + case USB_HOST_CLASS_HID: return config->hid; + case USB_HOST_CLASS_MIDI: return config->midi; + case USB_HOST_CLASS_MSC: return config->msc; + case USB_HOST_CLASS_AUDIO: return config->audio; + } + return false; +} + +// endregion + +// region Lib + +static error_t install_lib(UsbHostContext* ctx, const Esp32UsbHostConfig* cfg) { usb_host_config_t host_cfg = { .skip_phy_setup = false, .root_port_unpowered = false, @@ -80,35 +327,27 @@ static error_t start_device(struct Device* device) { 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); + BaseType_t result = esp32_usbhost_task_create_psram(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; + ctx->lib_task_done = nullptr; 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(device_get_driver_data(device)); - if (!ctx) return ERROR_NONE; - +static error_t uninstall_lib(UsbHostContext* ctx) { xTaskNotifyGive(ctx->lib_task); bool exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_TIMEOUT_MS)) == pdTRUE); @@ -120,36 +359,182 @@ static error_t stop_device(struct Device* device) { 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); + vTaskDeleteWithCaps(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; + ctx->lib_task_done = nullptr; return ERROR_RESOURCE; } ctx->lib_task = nullptr; vSemaphoreDelete(ctx->lib_task_done); + ctx->lib_task_done = nullptr; 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; } +// endregion + +// region API + +// Serializes backend changes with host start and stop, which create and delete the context +static SemaphoreHandle_t get_backends_mutex() { + static StaticSemaphore_t buffer; + static SemaphoreHandle_t mutex = xSemaphoreCreateMutexStatic(&buffer); + return mutex; +} + +static bool is_class_supported_by(const Esp32UsbHostConfig* config, UsbClass usb_class) { + return is_class_configured(config, usb_class) && get_backend(usb_class) != nullptr; +} + +static error_t start_backend(Device* device, UsbHostContext* ctx, UsbClass usb_class) { + error_t error = get_backend(usb_class)->start(device, &ctx->backends[usb_class]); + if (error != ERROR_NONE) { + LOG_E(TAG, "failed to start class %d", usb_class); + ctx->backends[usb_class] = nullptr; + } + return error; +} + +static error_t stop_backend(UsbHostContext* ctx, UsbClass usb_class) { + // A retained backend still uses the lib and the worker + error_t error = get_backend(usb_class)->stop(ctx->backends[usb_class]); + if (error != ERROR_NONE) { + LOG_E(TAG, "failed to stop class %d: %s", usb_class, error_to_string(error)); + return error; + } + ctx->backends[usb_class] = nullptr; + return ERROR_NONE; +} + +static error_t is_class_supported(struct Device* device, enum UsbClass usb_class, bool* supported) { + if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) { + return ERROR_INVALID_ARGUMENT; + } + *supported = is_class_supported_by(GET_CONFIG(device), usb_class); + return ERROR_NONE; +} + +static error_t is_class_enabled(struct Device* device, enum UsbClass usb_class, bool* enabled) { + if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) { + return ERROR_INVALID_ARGUMENT; + } + xSemaphoreTake(get_backends_mutex(), portMAX_DELAY); + auto* ctx = static_cast(device_get_driver_data(device)); + if (ctx != nullptr) { + *enabled = ctx->backends[usb_class] != nullptr; + } + xSemaphoreGive(get_backends_mutex()); + return ctx != nullptr ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t set_class_enabled(struct Device* device, enum UsbClass usb_class, bool enabled) { + if (usb_class < 0 || usb_class >= USB_HOST_CLASS_COUNT) { + return ERROR_INVALID_ARGUMENT; + } + if (!is_class_supported_by(GET_CONFIG(device), usb_class)) { + return ERROR_NOT_SUPPORTED; + } + xSemaphoreTake(get_backends_mutex(), portMAX_DELAY); + error_t error = ERROR_NONE; + auto* ctx = static_cast(device_get_driver_data(device)); + if (ctx == nullptr) { + error = ERROR_INVALID_STATE; + } else if (enabled && ctx->backends[usb_class] == nullptr) { + error = start_backend(device, ctx, usb_class); + } else if (!enabled && ctx->backends[usb_class] != nullptr) { + error = stop_backend(ctx, usb_class); + } + xSemaphoreGive(get_backends_mutex()); + return error; +} + +// endregion + +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; + } + + xSemaphoreTake(get_backends_mutex(), portMAX_DELAY); + auto* ctx = new UsbHostContext(); + if (create_worker_queue(ctx) != ERROR_NONE) { + xSemaphoreGive(get_backends_mutex()); + delete ctx; + return ERROR_RESOURCE; + } + if (install_lib(ctx, cfg) != ERROR_NONE) { + xSemaphoreGive(get_backends_mutex()); + delete_worker_queue(ctx); + delete ctx; + return ERROR_RESOURCE; + } + device_set_driver_data(device, ctx); + + for (int i = 0; i < USB_HOST_CLASS_COUNT; i++) { + const auto usb_class = static_cast(i); + if (is_class_supported_by(cfg, usb_class)) { + start_backend(device, ctx, usb_class); + } + } + xSemaphoreGive(get_backends_mutex()); + + LOG_I(TAG, "started (peripheral_map=0x%02x)", cfg->peripheral_map); + return ERROR_NONE; +} + +static error_t stop_device(struct Device* device) { + xSemaphoreTake(get_backends_mutex(), portMAX_DELAY); + auto* ctx = static_cast(device_get_driver_data(device)); + if (!ctx) { + xSemaphoreGive(get_backends_mutex()); + return ERROR_NONE; + } + + for (int i = USB_HOST_CLASS_COUNT - 1; i >= 0; i--) { + if (ctx->backends[i] == nullptr) { + continue; + } + error_t error = stop_backend(ctx, static_cast(i)); + if (error != ERROR_NONE) { + xSemaphoreGive(get_backends_mutex()); + return error; + } + } + device_set_driver_data(device, nullptr); + xSemaphoreGive(get_backends_mutex()); + + error_t result = uninstall_lib(ctx); + delete_worker_queue(ctx); + delete ctx; + LOG_I(TAG, "stopped"); + return result; +} + +static const UsbHostApi usb_host_api = { + .is_class_supported = is_class_supported, + .is_class_enabled = is_class_enabled, + .set_class_enabled = set_class_enabled, +}; + 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, + .probe = nullptr, + .api = &usb_host_api, + .device_type = &USB_HOST_TYPE, .owner = nullptr, .internal = nullptr, }; diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index 2ee7eadf9..2087e2947 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include #include @@ -38,7 +41,6 @@ typedef struct { struct UsbHidContext { std::atomic mouse_connected{false}; - std::atomic device_connected{false}; QueueHandle_t hid_event_queue = nullptr; TaskHandle_t hid_proc_task = nullptr; SemaphoreHandle_t hid_proc_task_done = nullptr; @@ -64,16 +66,21 @@ struct UsbHidContext { QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {}; SemaphoreHandle_t sub_mutex = nullptr; - // The controller Device itself (set in start_device()), used as the parent for kb_device. - Device* controller_device = nullptr; - // Dynamic KEYBOARD_TYPE child device, constructed while a physical USB keyboard is connected. + Device* host = nullptr; + // USB_HOST_HID_TYPE device, exists while any HID interface is open + Device hid_device{}; + bool hid_device_active = false; + // KEYBOARD_TYPE child of hid_device, exists while a keyboard is open Device kb_device{}; bool kb_device_active = false; }; extern "C" { +extern Driver esp32_usbhost_hid_driver; +static bool usb_hid_device_create(UsbHidContext* ctx); +static error_t usb_hid_device_destroy_if_unused(UsbHidContext* ctx); static void usb_hid_keyboard_device_construct(UsbHidContext* ctx); -static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx); +static error_t usb_hid_keyboard_device_destruct(UsbHidContext* ctx); static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bool pressed, bool ctrl, bool alt, uint8_t hid_keycode, uint8_t hid_modifier); } @@ -266,10 +273,6 @@ static void hid_interface_callback(hid_host_device_handle_t handle, ctx->consumer_prev_count = 0; } hid_host_device_close(handle); - if (!ctx->kb_handle.load() && !ctx->mouse_connected - && ctx->consumer_handle.load() == nullptr) { - ctx->device_connected = false; - } if (params.proto == HID_PROTOCOL_KEYBOARD || params.proto == HID_PROTOCOL_MOUSE) { UsbHidEventType disc_type = (params.proto == HID_PROTOCOL_KEYBOARD) ? USB_HID_EVENT_KEYBOARD_DISCONNECTED @@ -277,6 +280,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle, UsbHidEvent evt = { .type = disc_type, .scroll = {} }; publish_event(ctx, &evt); } + usb_hid_device_destroy_if_unused(ctx); break; case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: @@ -299,6 +303,7 @@ static void hid_driver_callback(hid_host_device_handle_t handle, } } +// Stack may be in PSRAM: no flash access allowed from this task. static void hid_proc_task(void* arg) { auto* ctx = static_cast(arg); LOG_I(TAG, "HID proc task started"); @@ -344,11 +349,11 @@ static void hid_proc_task(void* arg) { } LOG_I(TAG, "HID Consumer Control connected (%d usages)", (int) map.usage_count); + usb_hid_device_create(ctx); // No boot-protocol switch: consumer devices only implement report protocol. ctx->consumer_map = map; ctx->consumer_prev_count = 0; ctx->consumer_handle.store(dev_evt.handle); - ctx->device_connected = true; hid_host_device_start(dev_evt.handle); continue; } @@ -363,6 +368,8 @@ static void hid_proc_task(void* arg) { continue; } hid_class_request_set_protocol(dev_evt.handle, HID_REPORT_PROTOCOL_BOOT); + // Created before the interface counts as connected, so a subscriber added by a device listener gets one CONNECTED event + usb_hid_device_create(ctx); if (params.proto == HID_PROTOCOL_KEYBOARD) { hid_class_request_set_idle(dev_evt.handle, 0, 0); vTaskDelay(pdMS_TO_TICKS(200)); @@ -375,7 +382,6 @@ static void hid_proc_task(void* arg) { } 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 @@ -389,12 +395,7 @@ static void hid_proc_task(void* arg) { 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(device_get_driver_data(device)); - return ctx && ctx->device_connected.load(); + vTaskDeleteWithCaps(nullptr); } static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) { @@ -449,7 +450,6 @@ static void api_hid_unsubscribe(struct Device* device, UsbHidQueueHandle event_q } static const UsbHidApi hid_api = { - .is_connected = api_hid_is_connected, .subscribe = api_hid_subscribe, .unsubscribe = api_hid_unsubscribe, }; @@ -515,51 +515,75 @@ Driver esp32_usbhost_hid_keyboard_driver = { }; static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) { - if (ctx->kb_device_active) { + if (ctx->kb_device_active || !ctx->hid_device_active) { return; } - - ctx->kb_device = Device { - .address = 0, - .name = "usb_keyboard0", - .config = nullptr, - .parent = nullptr, - .flags = 0, - .internal = nullptr, - }; - - if (device_construct(&ctx->kb_device) != ERROR_NONE) { - LOG_E(TAG, "failed to construct USB keyboard device"); - return; + if (esp32_usbhost_device_create(ctx->host, &ctx->kb_device, &ctx->hid_device, "usb_keyboard0", &esp32_usbhost_hid_keyboard_driver, nullptr) == ERROR_NONE) { + ctx->kb_device_active = true; } - device_set_parent(&ctx->kb_device, ctx->controller_device); - device_set_driver(&ctx->kb_device, &esp32_usbhost_hid_keyboard_driver); - if (device_add(&ctx->kb_device) != ERROR_NONE) { - LOG_E(TAG, "failed to add USB keyboard device"); - device_destruct(&ctx->kb_device); - return; - } - if (device_start(&ctx->kb_device) != ERROR_NONE) { - LOG_E(TAG, "failed to start USB keyboard device"); - device_remove(&ctx->kb_device); - device_destruct(&ctx->kb_device); - return; - } - - ctx->kb_device_active = true; } -static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx) { +static error_t usb_hid_keyboard_device_destruct(UsbHidContext* ctx) { if (!ctx->kb_device_active) { - return; + return ERROR_NONE; } - ctx->kb_device_active = false; - - device_stop(&ctx->kb_device); - device_remove(&ctx->kb_device); - device_destruct(&ctx->kb_device); + error_t error = esp32_usbhost_device_destroy(ctx->host, &ctx->kb_device); + if (error == ERROR_NONE) { + ctx->kb_device_active = false; + } + return error; } +// endregion + +// region USB_HOST_HID_TYPE device + +static bool usb_hid_device_create(UsbHidContext* ctx) { + if (ctx->hid_device_active) { + return true; + } + if (esp32_usbhost_device_create(ctx->host, &ctx->hid_device, ctx->host, "usb_hid0", &esp32_usbhost_hid_driver, ctx) != ERROR_NONE) { + return false; + } + ctx->hid_device_active = true; + return true; +} + +static error_t usb_hid_device_destroy_if_unused(UsbHidContext* ctx) { + if (!ctx->hid_device_active || ctx->kb_handle.load() || ctx->mouse_connected || ctx->consumer_handle.load()) { + return ERROR_NONE; + } + error_t error = usb_hid_keyboard_device_destruct(ctx); + if (error == ERROR_NONE) { + error = esp32_usbhost_device_destroy(ctx->host, &ctx->hid_device); + } + if (error == ERROR_NONE) { + ctx->hid_device_active = false; + } + return error; +} + +static error_t hid_device_start(Device* device) { + return device_get_driver_data(device) != nullptr ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t hid_device_stop(Device* device) { + auto* ctx = static_cast(device_get_driver_data(device)); + // Subscriptions last for the lifetime of the device + if (xSemaphoreTake(ctx->sub_mutex, portMAX_DELAY) == pdTRUE) { + for (auto& subscriber : ctx->subscribers) { + subscriber = nullptr; + } + xSemaphoreGive(ctx->sub_mutex); + } + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +// endregion + +// region Keyboard events + static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bool pressed, bool ctrl, bool alt, uint8_t hid_keycode, uint8_t hid_modifier) { if (!ctx->kb_device_active) { return; @@ -571,9 +595,9 @@ static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bo // endregion -static error_t start_device(struct Device* device) { +static error_t backend_start(Device* host, void** out_context) { auto* ctx = new UsbHidContext(); - ctx->controller_device = device; + ctx->host = host; ctx->sub_mutex = xSemaphoreCreateMutex(); if (!ctx->sub_mutex) { @@ -617,8 +641,8 @@ static error_t start_device(struct Device* device) { } 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); + BaseType_t result = esp32_usbhost_task_create_psram(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; @@ -630,42 +654,52 @@ static error_t start_device(struct Device* device) { return ERROR_RESOURCE; } - device_set_driver_data(device, ctx); + *out_context = ctx; LOG_I(TAG, "started"); return ERROR_NONE; } -static error_t stop_device(struct Device* device) { - auto* ctx = static_cast(device_get_driver_data(device)); - if (!ctx) return ERROR_NONE; +static error_t backend_stop(void* context) { + auto* ctx = static_cast(context); // hid_host_device_close() halts and flushes the pending IN transfer before returning, so no // hid_interface_callback() for this handle can race the queue delete below. - if (auto kb_handle = ctx->kb_handle.load()) { + if (auto kb_handle = ctx->kb_handle.exchange(nullptr)) { hid_host_device_close(kb_handle); } // hid_host_uninstall() below can fail with any interface still registered, and its callback // still references ctx, about to be deleted. - if (auto consumer_handle = ctx->consumer_handle.load()) { + if (auto consumer_handle = ctx->consumer_handle.exchange(nullptr)) { hid_host_device_close(consumer_handle); } - usb_hid_keyboard_device_destruct(ctx); - 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); + if (ctx->hid_proc_task != nullptr) { + 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"); + vTaskDeleteWithCaps(ctx->hid_proc_task); + } + ctx->hid_proc_task = nullptr; + vSemaphoreDelete(ctx->hid_proc_task_done); + ctx->hid_proc_task_done = nullptr; + } + + error_t error = usb_hid_keyboard_device_destruct(ctx); + if (error == ERROR_NONE && ctx->hid_device_active) { + error = esp32_usbhost_device_destroy(ctx->host, &ctx->hid_device); + if (error == ERROR_NONE) { + ctx->hid_device_active = false; + } + } + if (error != ERROR_NONE) { + return error; } - 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; @@ -673,9 +707,9 @@ static error_t stop_device(struct Device* device) { 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, + .compatible = (const char*[]) { nullptr }, + .start_device = hid_device_start, + .stop_device = hid_device_stop, .api = &hid_api, .device_type = &USB_HOST_HID_TYPE, .owner = nullptr, @@ -684,4 +718,9 @@ Driver esp32_usbhost_hid_driver = { } // extern "C" +const Esp32UsbHostBackend esp32_usbhost_hid_backend = { + .start = backend_start, + .stop = backend_stop, +}; + #endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp index ac9f57f5a..3ef03f812 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include @@ -28,7 +31,13 @@ constexpr auto MIDI_TRANSFER_BUF_SIZE = 512; constexpr uint8_t MIDI_INTF_CLASS = 0x01; constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03; +extern "C" Driver esp32_usbhost_midi_driver; + struct UsbMidiContext { + Device* host = nullptr; + // USB_HOST_MIDI_TYPE device, exists while a MIDI interface is claimed + Device midi_device = {}; + bool midi_device_active = false; usb_host_client_handle_t client_hdl = nullptr; usb_device_handle_t dev_hdl = nullptr; usb_transfer_t* transfer = nullptr; @@ -38,10 +47,6 @@ struct UsbMidiContext { std::atomic 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) { @@ -72,14 +77,6 @@ static bool find_midi_interface(const usb_config_desc_t* cfg, uint8_t* out_intf, } 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; @@ -89,13 +86,13 @@ static void dispatch_midi_packets(UsbMidiContext* ctx, const uint8_t* buf, int l .data1 = buf[i + 2], .data2 = buf[i + 3], }; - cb(&msg, arg); + usb_midi_event_emit(&ctx->midi_device, &msg); } } static void midi_transfer_cb(usb_transfer_t* transfer) { auto* ctx = static_cast(transfer->context); - if (transfer->status == USB_TRANSFER_STATUS_COMPLETED && transfer->actual_num_bytes > 0) { + if (transfer->status == USB_TRANSFER_STATUS_COMPLETED && transfer->actual_num_bytes > 0 && ctx->midi_device_active) { dispatch_midi_packets(ctx, transfer->data_buffer, transfer->actual_num_bytes); } if (ctx->running && ctx->connected && transfer->status != USB_TRANSFER_STATUS_NO_DEVICE) { @@ -106,11 +103,31 @@ static void midi_transfer_cb(usb_transfer_t* transfer) { } } +static void midi_device_create(UsbMidiContext* ctx) { + if (ctx->midi_device_active) { + return; + } + if (esp32_usbhost_device_create(ctx->host, &ctx->midi_device, ctx->host, "usb_midi0", &esp32_usbhost_midi_driver, ctx) == ERROR_NONE) { + ctx->midi_device_active = true; + } +} + +static error_t midi_device_destroy(UsbMidiContext* ctx) { + if (!ctx->midi_device_active) { + return ERROR_NONE; + } + error_t error = esp32_usbhost_device_destroy(ctx->host, &ctx->midi_device); + if (error == ERROR_NONE) { + ctx->midi_device_active = false; + } + return error; +} + static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) { auto* ctx = static_cast(arg); if (msg->event == USB_HOST_CLIENT_EVENT_NEW_DEV) { - if (ctx->dev_hdl != nullptr || ctx->connected.load()) { + if (ctx->dev_hdl != nullptr || ctx->connected.load() || ctx->midi_device_active) { LOG_W(TAG, "ignoring additional MIDI device while one is already active"); return; } @@ -148,11 +165,15 @@ static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) { ctx->transfer->context = ctx; ctx->transfer->timeout_ms = 0; + // Created before the first transfer so no messages are dropped + midi_device_create(ctx); + ctx->connected = true; 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"); + ctx->connected = false; + midi_device_destroy(ctx); usb_host_interface_release(ctx->client_hdl, dev_hdl, intf_num); usb_host_device_close(ctx->client_hdl, dev_hdl); ctx->dev_hdl = nullptr; @@ -162,6 +183,7 @@ static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) { if (ctx->dev_hdl && msg->dev_gone.dev_hdl == ctx->dev_hdl) { LOG_I(TAG, "MIDI device disconnected"); ctx->connected = false; + midi_device_destroy(ctx); 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; @@ -169,6 +191,7 @@ static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) { } } +// Stack may be in PSRAM: no flash access allowed from this task. static void midi_client_task(void* arg) { auto* ctx = static_cast(arg); LOG_I(TAG, "MIDI client task started"); @@ -183,35 +206,27 @@ static void midi_client_task(void* arg) { usb_host_device_close(ctx->client_hdl, ctx->dev_hdl); ctx->dev_hdl = nullptr; } + midi_device_destroy(ctx); LOG_I(TAG, "MIDI client task stopped"); xSemaphoreGive(ctx->task_done); - vTaskDelete(nullptr); + vTaskDeleteWithCaps(nullptr); } -static void api_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) { - auto* ctx = static_cast(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(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) { +static error_t midi_device_start(Device* device) { + return device_get_driver_data(device) != nullptr ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t midi_device_stop(Device* device) { + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +static error_t backend_start(Device* host, void** out_context) { auto* ctx = new UsbMidiContext(); + ctx->host = host; if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) { LOG_E(TAG, "failed to allocate MIDI transfer"); @@ -245,8 +260,8 @@ static error_t start_device(struct Device* device) { } ctx->running = true; - BaseType_t result = xTaskCreate(midi_client_task, "midi_client", MIDI_TASK_STACK, - ctx, MIDI_TASK_PRIORITY, &ctx->task_handle); + BaseType_t result = esp32_usbhost_task_create_psram(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; @@ -257,35 +272,42 @@ static error_t start_device(struct Device* device) { return ERROR_RESOURCE; } - device_set_driver_data(device, ctx); + *out_context = ctx; LOG_I(TAG, "started"); return ERROR_NONE; } -static error_t stop_device(struct Device* device) { - auto* ctx = static_cast(device_get_driver_data(device)); - if (!ctx) return ERROR_NONE; +static error_t backend_stop(void* context) { + auto* ctx = static_cast(context); - ctx->running = false; - usb_host_client_unblock(ctx->client_hdl); + if (ctx->task_handle != nullptr) { + 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 (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; } - 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; + vTaskDeleteWithCaps(ctx->task_handle); + vTaskDelay(pdMS_TO_TICKS(50)); } - vTaskDelete(ctx->task_handle); - vTaskDelay(pdMS_TO_TICKS(50)); + ctx->task_handle = nullptr; + vSemaphoreDelete(ctx->task_done); + ctx->task_done = nullptr; + } + + error_t error = midi_device_destroy(ctx); + if (error != ERROR_NONE) { + return error; } - ctx->task_handle = nullptr; - vSemaphoreDelete(ctx->task_done); usb_host_client_deregister(ctx->client_hdl); ctx->client_hdl = nullptr; @@ -293,7 +315,6 @@ static error_t stop_device(struct Device* device) { usb_host_transfer_free(ctx->transfer); ctx->transfer = nullptr; - device_set_driver_data(device, nullptr); delete ctx; LOG_I(TAG, "stopped"); return ERROR_NONE; @@ -301,10 +322,10 @@ static error_t stop_device(struct Device* device) { 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, + .compatible = (const char*[]) { nullptr }, + .start_device = midi_device_start, + .stop_device = midi_device_stop, + .api = nullptr, .device_type = &USB_HOST_MIDI_TYPE, .owner = nullptr, .internal = nullptr, @@ -312,4 +333,9 @@ Driver esp32_usbhost_midi_driver = { } // extern "C" +const Esp32UsbHostBackend esp32_usbhost_midi_backend = { + .start = backend_start, + .stop = backend_stop, +}; + #endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp index 976cb059f..159796da0 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_msc.cpp @@ -3,6 +3,9 @@ #include #include +#include +#include +#include #include #include #include @@ -44,13 +47,14 @@ using msc_event_id_t = decltype(msc_host_event_t{}.event); static constexpr msc_event_id_t kMscDeviceConnected = static_cast(0); static constexpr msc_event_id_t kMscDeviceDisconnected = static_cast(1); -enum class MscMsgId : uint8_t { Connected, Disconnected }; +enum class MscMsgId : uint8_t { Connected, Disconnected, Ejected }; typedef struct { MscMsgId id; union { uint8_t address; msc_host_device_handle_t handle; + int slot; }; } msc_msg_t; @@ -61,8 +65,17 @@ struct UsbMscContext { TaskHandle_t proc_task = nullptr; SemaphoreHandle_t proc_task_done = nullptr; std::atomic proc_running{false}; + Device* host = nullptr; + // USB_HOST_MSC_TYPE device per mounted drive, indexed by slot + Device devices[MAX_MSC_DEVICES] = {}; + bool device_active[MAX_MSC_DEVICES] = {}; }; +extern "C" Driver esp32_usbhost_msc_driver; + +static const char* const MSC_DEVICE_NAMES[] = { "usb_msc0", "usb_msc1" }; +static_assert(sizeof(MSC_DEVICE_NAMES) / sizeof(MSC_DEVICE_NAMES[0]) == MAX_MSC_DEVICES); + 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) { @@ -84,11 +97,29 @@ static const FileSystemApi usb_fs_api = { static int find_free_slot(UsbMscContext* ctx) { for (int i = 0; i < MAX_MSC_DEVICES; i++) { - if (ctx->devs[i] == nullptr) return i; + // An ejected drive's device can outlive its entry until msc_proc destroys it + if (ctx->devs[i] == nullptr && !ctx->device_active[i]) return i; } return -1; } +static void msc_device_create(UsbMscContext* ctx, int slot) { + if (esp32_usbhost_device_create(ctx->host, &ctx->devices[slot], ctx->host, MSC_DEVICE_NAMES[slot], &esp32_usbhost_msc_driver, ctx) == ERROR_NONE) { + ctx->device_active[slot] = true; + } +} + +static error_t msc_device_destroy(UsbMscContext* ctx, int slot) { + if (!ctx->device_active[slot]) { + return ERROR_NONE; + } + error_t error = esp32_usbhost_device_destroy(ctx->host, &ctx->devices[slot]); + if (error == ERROR_NONE) { + ctx->device_active[slot] = false; + } + return error; +} + 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; @@ -97,7 +128,9 @@ static int find_slot_by_handle(UsbMscContext* ctx, msc_host_device_handle_t hand } static void free_msc_device(UsbMscContext* ctx, int slot) { - if (slot < 0 || slot >= MAX_MSC_DEVICES || !ctx->devs[slot]) return; + if (slot < 0 || slot >= MAX_MSC_DEVICES) return; + msc_device_destroy(ctx, slot); + if (!ctx->devs[slot]) return; if (ctx->devs[slot]->fs_entry) { ctx->devs[slot]->mounted = false; file_system_remove(ctx->devs[slot]->fs_entry); @@ -138,6 +171,7 @@ static void msc_event_cb(const msc_host_event_t* event, void* arg) { } } +// Stack may be in PSRAM: no flash access allowed from this task. static void msc_proc_task(void* arg) { auto* ctx = static_cast(arg); LOG_I(TAG, "MSC proc task started"); @@ -215,6 +249,7 @@ static void msc_proc_task(void* arg) { continue; } ctx->devs[slot]->mounted = true; + msc_device_create(ctx, slot); } else if (msg.id == MscMsgId::Disconnected) { taskENTER_CRITICAL(&ctx->devs_lock); @@ -224,29 +259,29 @@ static void msc_proc_task(void* arg) { LOG_I(TAG, "USB drive disconnected, unmounting slot %d", slot); free_msc_device(ctx, slot); } + } else if (msg.id == MscMsgId::Ejected) { + msc_device_destroy(ctx, msg.slot); } } free_all_msc_devices(ctx); LOG_I(TAG, "MSC proc task stopped"); xSemaphoreGive(ctx->proc_task_done); - vTaskDelete(nullptr); + vTaskDeleteWithCaps(nullptr); } static bool api_eject(struct Device* device, const char* mount_path) { auto* ctx = static_cast(device_get_driver_data(device)); if (!ctx) return false; + const int slot = static_cast(device - ctx->devices); 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; - } + if (ctx->devs[slot] && strcmp(ctx->devs[slot]->mount_path, mount_path) == 0) { + found = slot; + entry = ctx->devs[slot]; + ctx->devs[slot] = nullptr; // claim atomically under the lock } taskEXIT_CRITICAL(&ctx->devs_lock); @@ -265,6 +300,13 @@ static bool api_eject(struct Device* device, const char* mount_path) { msc_host_uninstall_device(entry->device); } free(entry); + // The caller holds a reference to the device, so msc_proc destroys it later + msc_msg_t msg = {}; + msg.id = MscMsgId::Ejected; + msg.slot = found; + if (xQueueSend(ctx->event_queue, &msg, pdMS_TO_TICKS(100)) != pdTRUE) { + LOG_W(TAG, "event queue full, %s stays until the drive is removed", MSC_DEVICE_NAMES[found]); + } LOG_I(TAG, "USB drive ejected, safe to remove"); return true; } @@ -278,8 +320,18 @@ static const UsbMscApi msc_api = { extern "C" { -static error_t start_device(struct Device* device) { +static error_t msc_device_start(Device* device) { + return device_get_driver_data(device) != nullptr ? ERROR_NONE : ERROR_INVALID_STATE; +} + +static error_t msc_device_stop(Device* device) { + device_set_driver_data(device, nullptr); + return ERROR_NONE; +} + +static error_t backend_start(Device* host, void** out_context) { auto* ctx = new UsbMscContext(); + ctx->host = host; ctx->event_queue = xQueueCreate(MSC_EVENT_QUEUE_SIZE, sizeof(msc_msg_t)); if (!ctx->event_queue) { @@ -313,8 +365,8 @@ static error_t start_device(struct Device* device) { } 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); + BaseType_t result = esp32_usbhost_task_create_psram(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; @@ -325,37 +377,46 @@ static error_t start_device(struct Device* device) { return ERROR_RESOURCE; } - device_set_driver_data(device, ctx); + *out_context = ctx; LOG_I(TAG, "started"); return ERROR_NONE; } -static error_t stop_device(struct Device* device) { - auto* ctx = static_cast(device_get_driver_data(device)); - if (!ctx) return ERROR_NONE; +static error_t backend_stop(void* context) { + auto* ctx = static_cast(context); - ctx->proc_running = false; + if (ctx->proc_task != nullptr) { + 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); + 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"); + vTaskDeleteWithCaps(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); + ctx->proc_task_done = nullptr; } - 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); + + // Covers devices of ejected drives and devices whose destruction failed before + for (int i = 0; i < MAX_MSC_DEVICES; i++) { + error_t error = msc_device_destroy(ctx, i); + if (error != ERROR_NONE) { + return error; + } } - 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; @@ -363,9 +424,9 @@ static error_t stop_device(struct Device* device) { 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, + .compatible = (const char*[]) { nullptr }, + .start_device = msc_device_start, + .stop_device = msc_device_stop, .api = &msc_api, .device_type = &USB_HOST_MSC_TYPE, .owner = nullptr, @@ -374,4 +435,9 @@ Driver esp32_usbhost_msc_driver = { } // extern "C" +const Esp32UsbHostBackend esp32_usbhost_msc_backend = { + .start = backend_start, + .stop = backend_stop, +}; + #endif // CONFIG_SOC_USB_OTG_SUPPORTED diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp index e2909599b..3bc74b096 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // -// USB Audio Class 1.0 output codec, as a class client beneath the shared +// USB Audio Class 1.0 output codec, as a class backend of the shared // esp32_usbhost controller. On attach of a headset supporting 48 kHz/16-bit/ // stereo PCM output, registers a dynamic "usb_uac0" AUDIO_CODEC_TYPE device // (see esp32_usbhost_hid.cpp's "usb_keyboard0" for the pattern) so @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include #include #include @@ -68,8 +71,8 @@ struct UsbUacData { bool streaming = false; uint8_t volume = 100; bool muted = false; - // The manager Device, set in manager_start_device; parent of codec_device. - Device* manager_device = nullptr; + // The USB host device, parent of codec_device. + Device* host = nullptr; // Dynamic AUDIO_CODEC_TYPE child device, constructed while a UAC output is // connected (mirrors esp32_usbhost_hid.cpp's kb_device). Device codec_device {}; @@ -79,7 +82,7 @@ struct UsbUacData { #define GET_DATA(device) (static_cast(device_get_driver_data(device))) void codec_device_construct(UsbUacData* data); -void codec_device_destruct(UsbUacData* data); +error_t codec_device_destruct(UsbUacData* data); void log_heap(const char* stage) { LOG_I(TAG, "%s heap: internal_free=%u internal_min=%u external_free=%u", @@ -204,7 +207,9 @@ bool close_output_interface(UsbUacData* data) { // listeners (audio-stream re-binds output to the board codec) and waits // for in-flight writes, which are serialized against the handle close // below by data->mutex. - codec_device_destruct(data); + if (codec_device_destruct(data) != ERROR_NONE) { + return false; + } xSemaphoreTake(data->mutex, portMAX_DELAY); uac_host_device_handle_t handle = data->handle; @@ -234,6 +239,7 @@ bool close_output_interface(UsbUacData* data) { return true; } +// Stack may be in PSRAM: no flash access allowed from this task. void uac_client_task(void* arg) { auto* data = static_cast(arg); UacConnectEvent event = {}; @@ -260,13 +266,13 @@ void uac_client_task(void* arg) { data->task = nullptr; xSemaphoreGive(data->mutex); xSemaphoreGive(data->task_done); - vTaskDelete(nullptr); + vTaskDeleteWithCaps(nullptr); } // region Dynamic codec device (usb_uac0) driver error_t codec_start_device(Device* device) { - // driver_data is set by the manager before device_start(); the UAC + // driver_data is set by the backend before device_start(); the UAC // interface is already open. Streaming begins lazily via the codec API's // open(). return (GET_DATA(device) != nullptr) ? ERROR_NONE : ERROR_RESOURCE; @@ -279,7 +285,7 @@ error_t codec_stop_device(Device* device) { } // Best-effort streaming stop; the class handle itself is closed by the - // manager's task (close_output_interface). Bounded mutex wait so a stuck + // backend task (close_output_interface). Bounded mutex wait so a stuck // client task cannot stall device-tree teardown indefinitely. if (xSemaphoreTake(data->mutex, UAC_MUTEX_STOP_TIMEOUT) != pdTRUE) { LOG_W(TAG, "codec device stop: client task busy, leaving teardown to it"); @@ -545,46 +551,20 @@ void codec_device_construct(UsbUacData* data) { if (data->codec_device_active) { return; } - - data->codec_device = Device { - .address = 0, - .name = "usb_uac0", - .config = nullptr, - .parent = nullptr, - .internal = nullptr, - }; - - if (device_construct(&data->codec_device) != ERROR_NONE) { - LOG_E(TAG, "failed to construct USB UAC codec device"); - return; + if (esp32_usbhost_device_create(data->host, &data->codec_device, data->host, "usb_uac0", &usb_uac_codec_driver, data) == ERROR_NONE) { + data->codec_device_active = true; } - device_set_driver_data(&data->codec_device, data); - device_set_parent(&data->codec_device, data->manager_device); - device_set_driver(&data->codec_device, &usb_uac_codec_driver); - if (device_add(&data->codec_device) != ERROR_NONE) { - LOG_E(TAG, "failed to add USB UAC codec device"); - device_destruct(&data->codec_device); - return; - } - if (device_start(&data->codec_device) != ERROR_NONE) { - LOG_E(TAG, "failed to start USB UAC codec device"); - device_remove(&data->codec_device); - device_destruct(&data->codec_device); - return; - } - - data->codec_device_active = true; } -void codec_device_destruct(UsbUacData* data) { +error_t codec_device_destruct(UsbUacData* data) { if (!data->codec_device_active) { - return; + return ERROR_NONE; } - data->codec_device_active = false; - - device_stop(&data->codec_device); - device_remove(&data->codec_device); - device_destruct(&data->codec_device); + error_t error = esp32_usbhost_device_destroy(data->host, &data->codec_device); + if (error == ERROR_NONE) { + data->codec_device_active = false; + } + return error; } } // namespace @@ -592,7 +572,7 @@ void codec_device_destruct(UsbUacData* data) { extern "C" { // Driver for the dynamic "usb_uac0" codec device. Never matched against the -// devicetree; bound directly via device_set_driver() by the manager. +// devicetree; bound directly via device_set_driver() by the backend. Driver usb_uac_codec_driver = { .name = "usb_uac_codec", .compatible = (const char*[]) { nullptr }, @@ -604,14 +584,9 @@ Driver usb_uac_codec_driver = { .internal = nullptr, }; -static error_t manager_start_device(Device* device) { - if (device_get_parent(device) == nullptr || !device_is_ready(device_get_parent(device))) { - LOG_E(TAG, "USB host parent is not ready"); - return ERROR_RESOURCE; - } - +static error_t backend_start(Device* host, void** out_context) { auto* data = new UsbUacData(); - data->manager_device = device; + data->host = host; data->mutex = xSemaphoreCreateMutex(); data->task_done = xSemaphoreCreateBinary(); data->event_queue = xQueueCreate(UAC_EVENT_QUEUE_LENGTH, sizeof(UacConnectEvent)); @@ -637,7 +612,7 @@ static error_t manager_start_device(Device* device) { } data->running = true; - if (xTaskCreate(uac_client_task, "usb_uac_client", UAC_TASK_STACK, data, + if (esp32_usbhost_task_create_psram(uac_client_task, "usb_uac_client", UAC_TASK_STACK, data, UAC_TASK_PRIORITY, &data->task) != pdPASS) { LOG_E(TAG, "Failed to create UAC client task"); data->running = false; @@ -645,8 +620,8 @@ static error_t manager_start_device(Device* device) { goto cleanup; } - device_set_driver_data(device, data); - LOG_I(TAG, "UAC manager started"); + *out_context = data; + LOG_I(TAG, "UAC backend started"); return ERROR_NONE; cleanup: @@ -663,11 +638,8 @@ cleanup: return ERROR_RESOURCE; } -static error_t manager_stop_device(Device* device) { - auto* data = GET_DATA(device); - if (data == nullptr) { - return ERROR_NONE; - } +static error_t backend_stop(void* context) { + auto* data = static_cast(context); data->running = false; xSemaphoreTake(data->mutex, portMAX_DELAY); @@ -697,23 +669,16 @@ static error_t manager_stop_device(Device* device) { vQueueDelete(data->event_queue); vSemaphoreDelete(data->task_done); vSemaphoreDelete(data->mutex); - device_set_driver_data(device, nullptr); delete data; - LOG_I(TAG, "UAC manager stopped"); + LOG_I(TAG, "UAC backend stopped"); return ERROR_NONE; } -Driver esp32_usbhost_uac_driver = { - .name = "esp32_usbhost_uac", - .compatible = (const char*[]) { "espressif,esp32-usbhost-uac", nullptr }, - .start_device = manager_start_device, - .stop_device = manager_stop_device, - .api = nullptr, - .device_type = nullptr, - .owner = nullptr, - .internal = nullptr, -}; - } // extern "C" +const Esp32UsbHostBackend esp32_usbhost_uac_backend = { + .start = backend_start, + .stop = backend_stop, +}; + #endif diff --git a/Platforms/platform-esp32/source/module.cpp b/Platforms/platform-esp32/source/module.cpp index 0ff85fdf6..f63e7dd47 100644 --- a/Platforms/platform-esp32/source/module.cpp +++ b/Platforms/platform-esp32/source/module.cpp @@ -359,7 +359,6 @@ extern Driver esp32_usbhost_hid_keyboard_driver; extern Driver esp32_usbhost_midi_driver; extern Driver esp32_usbhost_msc_driver; #if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 -extern Driver esp32_usbhost_uac_driver; extern Driver usb_uac_codec_driver; #endif #endif @@ -412,7 +411,6 @@ static Driver* const platform_esp32_drivers[] = { &esp32_usbhost_midi_driver, &esp32_usbhost_msc_driver, #if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 - &esp32_usbhost_uac_driver, &usb_uac_codec_driver, #endif #endif diff --git a/Tactility/Include/Tactility/hal/usb/UsbHost.h b/Tactility/Include/Tactility/hal/usb/UsbHost.h new file mode 100644 index 000000000..61a5d5998 --- /dev/null +++ b/Tactility/Include/Tactility/hal/usb/UsbHost.h @@ -0,0 +1,17 @@ +#pragma once + +/** USB host on/off, persisted in the USB host settings. */ +namespace tt::hal::usbhost { + +/** Applies the saved setting to the USB host device. */ +void systemStart(); + +/** @return true when the hardware has a USB host device */ +bool isAvailable(); + +bool isEnabled(); + +/** Starts or stops the USB host and saves the choice. */ +bool setEnabled(bool enabled); + +} // namespace tt::hal::usbhost diff --git a/Tactility/Include/Tactility/settings/UsbHostSettings.h b/Tactility/Include/Tactility/settings/UsbHostSettings.h new file mode 100644 index 000000000..80c7418b0 --- /dev/null +++ b/Tactility/Include/Tactility/settings/UsbHostSettings.h @@ -0,0 +1,17 @@ +#pragma once + +namespace tt::settings::usbhost { + +struct UsbHostSettings { + bool enabled; +}; + +bool load(UsbHostSettings& settings); + +UsbHostSettings loadOrGetDefault(); + +UsbHostSettings getDefault(); + +bool save(const UsbHostSettings& settings); + +} // namespace tt::settings::usbhost diff --git a/Tactility/Source/app/boot/BootInit.cpp b/Tactility/Source/app/boot/BootInit.cpp index 520ab487a..0b01b0e97 100644 --- a/Tactility/Source/app/boot/BootInit.cpp +++ b/Tactility/Source/app/boot/BootInit.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -231,6 +232,9 @@ bool bootInit(TickType_t startTime) { LOG_I(TAG, "Prepare file systems"); prepareFileSystems(); + LOG_I(TAG, "Init USB host"); + hal::usbhost::systemStart(); + bool sd_card_missing = false; #ifdef CONFIG_TT_USER_DATA_LOCATION_SD std::string sd_path; diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index e0c4879db..58d13b97c 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -34,6 +34,7 @@ #include #include #include +#include namespace tt::app::files { @@ -585,14 +586,26 @@ void View::onEjectPressed() { std::string mount_path = state->getSelectedChildPath(); LOG_I(TAG, "Ejecting %s", mount_path.c_str()); - Device* msc_dev = nullptr; - if (device_get_first_active_by_type(&USB_HOST_MSC_TYPE, &msc_dev) != ERROR_NONE || !usb_msc_eject(msc_dev, mount_path.c_str())) { - LOG_W(TAG, "usb_msc_eject: %s not found", mount_path.c_str()); - alertdialog::start(appInstanceId, "Eject failed", "Could not eject \"" + file::getLastPathSegment(mount_path) + "\"."); + // One device per mounted drive. Ejecting happens outside the iteration, which holds the device ledger lock. + std::vector msc_devices; + device_for_each_of_type(&USB_HOST_MSC_TYPE, &msc_devices, [](Device* device, void* context) -> bool { + if (device_is_ready(device) && device_get(device) == ERROR_NONE) { + static_cast*>(context)->push_back(device); + } + return true; + }); + + bool ejected = false; + for (auto* msc_dev : msc_devices) { + if (!ejected) { + ejected = usb_msc_eject(msc_dev, mount_path.c_str()); + } + device_put(msc_dev); } - if (msc_dev) { - device_put(msc_dev); + if (!ejected) { + LOG_W(TAG, "usb_msc_eject: %s not found", mount_path.c_str()); + alertdialog::start(appInstanceId, "Eject failed", "Could not eject \"" + file::getLastPathSegment(mount_path) + "\"."); } onNavigate(); diff --git a/Tactility/Source/app/usbsettings/UsbSettings.cpp b/Tactility/Source/app/usbsettings/UsbSettings.cpp index bda8f4822..c79ec4830 100644 --- a/Tactility/Source/app/usbsettings/UsbSettings.cpp +++ b/Tactility/Source/app/usbsettings/UsbSettings.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -10,8 +11,12 @@ #include #include +#include +#include #include +#include + #define TAG "usb_settings" namespace tt::app::usbsettings { @@ -20,8 +25,20 @@ extern const ::AppManifest manifest; namespace { +enum class Request { None, Disable, Enable }; + struct Context { uint32_t appInstanceId; + TaskEventGroup* eventGroup = nullptr; + uint32_t requestBit = 0; + // Set by the switch and applied by appMain: stopping the host runs device listeners that take the LVGL lock + std::atomic hostRequest { Request::None }; + + // Read by appMain outside the LVGL lock, used by the widgets with the LVGL lock held + bool hostEnabled = false; + + // nullptr while the widgets don't exist + lv_obj_t* hostSwitch = nullptr; }; @@ -39,45 +56,93 @@ void onRebootMassStorageFlash(lv_event_t* event) { hal::usb::rebootIntoMassStorageFlash(); } +void onHostSwitch(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* sw = static_cast(lv_event_get_target(event)); + ctx->hostRequest = lv_obj_has_state(sw, LV_STATE_CHECKED) ? Request::Enable : Request::Disable; + task_event_group_signal(ctx->eventGroup, ctx->requestBit); +} + +void onHostSwitchDeleted(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->hostSwitch = nullptr; +} + +// Must be called with the LVGL lock held +void refresh(Context* ctx) { + if (ctx->hostSwitch != nullptr) { + lv_obj_set_state(ctx->hostSwitch, LV_STATE_CHECKED, ctx->hostEnabled); + } +} + +// A card with the label left and the control right +lv_obj_t* createRow(lv_obj_t* parent, const char* text) { + auto* row = lvgl_card_create(parent); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_scrollable(row, false); + + auto* label = lv_label_create(row); + lv_label_set_text(label, text); + lv_obj_set_flex_grow(label, 1); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + return row; +} + +void createRebootRow(lv_obj_t* parent, const char* text, lv_event_cb_t onReboot) { + auto* row = createRow(parent, text); + auto* button = lv_button_create(row); + auto* label = lv_label_create(button); + lv_label_set_text(label, "Reboot"); + lv_obj_add_event_cb(button, onReboot, LV_EVENT_SHORT_CLICKED, nullptr); +} + +void createUsbHostRow(lv_obj_t* parent, Context* ctx) { + auto* row = createRow(parent, "USB host"); + ctx->hostSwitch = lv_switch_create(row); + lv_obj_add_event_cb(ctx->hostSwitch, onHostSwitch, LV_EVENT_VALUE_CHANGED, ctx); + // Removed with the widgets, which happens when another window covers this one + lv_obj_add_event_cb(ctx->hostSwitch, onHostSwitchDeleted, LV_EVENT_DELETE, ctx); + refresh(ctx); +} + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + auto* toolbar = lvgl_toolbar_create(parent, "USB"); // The global toolbar nav callback only knows how to stop old-model apps. lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); - lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0); - // Create a wrapper container for buttons - auto* wrapper = lv_obj_create(parent); - lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_size(wrapper, lv_pct(100), LV_SIZE_CONTENT); - lv_obj_align(wrapper, LV_ALIGN_CENTER, 0, 0); - // Ensures showing selection state for keyboard/encoder devices - lv_obj_set_style_pad_all(wrapper, 6, LV_ALIGN_DEFAULT); - lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); + auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); - bool hasSd = hal::usb::canRebootIntoMassStorageSdmmc(); - bool hasFlash = hal::usb::canRebootIntoMassStorageFlash(); + const bool hasSd = hal::usb::canRebootIntoMassStorageSdmmc(); + const bool hasFlash = hal::usb::canRebootIntoMassStorageFlash(); + const bool hasHost = hal::usbhost::isAvailable(); + // The storage is only named when there is a choice if (hasSd) { - auto* button_sd = lv_button_create(wrapper); - auto* label_sd = lv_label_create(button_sd); - lv_label_set_text(label_sd, "Reboot as USB storage (SD)"); - lv_obj_add_event_cb(button_sd, onRebootMassStorageSdmmc, LV_EVENT_SHORT_CLICKED, nullptr); + createRebootRow(main_wrapper, hasFlash ? "USB drive mode (SD card)" : "USB drive mode", onRebootMassStorageSdmmc); } - if (hasFlash) { - auto* button_flash = lv_button_create(wrapper); - auto* label_flash = lv_label_create(button_flash); - lv_label_set_text(label_flash, "Reboot as USB storage (Flash)"); - lv_obj_add_event_cb(button_flash, onRebootMassStorageFlash, LV_EVENT_SHORT_CLICKED, nullptr); + createRebootRow(main_wrapper, hasSd ? "USB drive mode (internal)" : "USB drive mode", onRebootMassStorageFlash); } - if (!hasSd && !hasFlash) { + if (hasHost) { + createUsbHostRow(main_wrapper, ctx); + } + + if (!hasSd && !hasFlash && !hasHost) { bool supported = hal::usb::isSupported(); const char* message = supported ? "USB storage not available" : "USB driver not supported"; - auto* label = lv_label_create(wrapper); + auto* label = lv_label_create(main_wrapper); lv_label_set_text(label, message); } } @@ -89,16 +154,33 @@ int32_t appMain(int argc, char* argv[]) { TaskEventGroup event_group {}; task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + check(task_event_group_claim_bit(&event_group, &ctx.requestBit) == ERROR_NONE); AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + const bool hasHost = hal::usbhost::isAvailable(); + if (hasHost) { + ctx.hostEnabled = hal::usbhost::isEnabled(); + } + WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + const auto hostRequest = ctx.hostRequest.exchange(Request::None); + if (hostRequest != Request::None) { + hal::usbhost::setEnabled(hostRequest == Request::Enable); + const bool hostEnabled = hal::usbhost::isEnabled(); + lvgl_lock(); + ctx.hostEnabled = hostEnabled; + refresh(&ctx); + lvgl_unlock(); + } + AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { switch (event.type) { @@ -114,6 +196,7 @@ int32_t appMain(int argc, char* argv[]) { window_manager_remove(window); check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_release_bit(&event_group, ctx.requestBit); task_event_group_destruct(&event_group); return 0; diff --git a/Tactility/Source/hal/usb/UsbHost.cpp b/Tactility/Source/hal/usb/UsbHost.cpp new file mode 100644 index 000000000..0aaf6d87b --- /dev/null +++ b/Tactility/Source/hal/usb/UsbHost.cpp @@ -0,0 +1,69 @@ +#include +#include + +#include +#include +#include + +namespace tt::hal::usbhost { + +constexpr auto* TAG = "UsbHost"; + +// The USB host comes from the devicetree and is never destructed, so the pointer stays valid without a reference +static Device* findHost() { + Device* host = nullptr; + if (device_get_first_by_type(&USB_HOST_TYPE, &host) != ERROR_NONE) { + return nullptr; + } + device_put(host); + return host; +} + +// The hotplug poller restarts registered devices that are stopped, so a disabled host is unregistered from it +static bool apply(Device* host, bool enabled) { + if (enabled) { + if (!device_is_ready(host) && device_start(host) != ERROR_NONE) { + LOG_E(TAG, "Failed to start USB host"); + return false; + } + device_hotplug_register(host); + return true; + } + + device_hotplug_unregister(host); + if (device_is_ready(host) && device_stop(host) != ERROR_NONE) { + LOG_E(TAG, "Failed to stop USB host"); + device_hotplug_register(host); + return false; + } + return true; +} + +void systemStart() { + Device* host = findHost(); + if (host != nullptr) { + apply(host, settings::usbhost::loadOrGetDefault().enabled); + } +} + +bool isAvailable() { + return findHost() != nullptr; +} + +bool isEnabled() { + Device* host = findHost(); + return host != nullptr && device_is_ready(host); +} + +bool setEnabled(bool enabled) { + Device* host = findHost(); + if (host == nullptr) { + return false; + } + if (!settings::usbhost::save({ .enabled = enabled })) { + LOG_W(TAG, "Failed to save settings"); + } + return apply(host, enabled); +} + +} // namespace tt::hal::usbhost diff --git a/Tactility/Source/lvgl/UsbHidInput.cpp b/Tactility/Source/lvgl/UsbHidInput.cpp index 0c2a1fefc..88145c5dc 100644 --- a/Tactility/Source/lvgl/UsbHidInput.cpp +++ b/Tactility/Source/lvgl/UsbHidInput.cpp @@ -3,9 +3,11 @@ #ifdef ESP_PLATFORM #include +#include #include #include +#include #include #include #include @@ -51,7 +53,6 @@ struct UsbHidInputCtx { StackType_t* task_stack = nullptr; StaticTask_t* task_tcb = nullptr; std::atomic running{false}; - std::atomic subscribed{false}; lv_indev_t* mouse_indev = nullptr; lv_indev_t* kb_indev = nullptr; @@ -71,6 +72,67 @@ struct UsbHidInputCtx { static UsbHidInputCtx* s_ctx = nullptr; +// region HID device listener + +static Mutex& listenerMutex() { + static Mutex mutex; + return mutex; +} + +// Queue that receives the HID events, nullptr while the listener is inactive. +// device_listener_remove() doesn't wait for in-flight callbacks, so they check this under listenerMutex. +static QueueHandle_t& listenerQueue() { + static QueueHandle_t queue = nullptr; + return queue; +} + +static void onDeviceEvent(Device* device, DeviceEvent event, void* context) { + (void)context; + if (device_get_type(device) != &USB_HOST_HID_TYPE) { + return; + } + auto lock = listenerMutex().asScopedLock(); + lock.lock(); + QueueHandle_t queue = listenerQueue(); + if (queue == nullptr) { + return; + } + if (event == DEVICE_EVENT_STARTED) { + if (!usb_host_hid_subscribe(device, queue)) { + LOG_W(TAG, "failed to subscribe to %s", device->name); + } + } else if (event == DEVICE_EVENT_STOPPING) { + usb_host_hid_unsubscribe(device, queue); + } +} + +static void startHidListener(QueueHandle_t queue) { + auto lock = listenerMutex().asScopedLock(); + lock.lock(); + listenerQueue() = queue; + device_listener_add(onDeviceEvent, nullptr); + // A device that started before the listener was added + Device* hid_dev = nullptr; + if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &hid_dev) == ERROR_NONE) { + usb_host_hid_subscribe(hid_dev, queue); + device_put(hid_dev); + } +} + +static void stopHidListener(QueueHandle_t queue) { + auto lock = listenerMutex().asScopedLock(); + lock.lock(); + listenerQueue() = nullptr; + device_listener_remove(onDeviceEvent, nullptr); + Device* hid_dev = nullptr; + if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &hid_dev) == ERROR_NONE) { + usb_host_hid_unsubscribe(hid_dev, queue); + device_put(hid_dev); + } +} + +// endregion + // Routes Consumer Control usages (headset buttons, media keyboards) to the audio service. // Only usages demonstrated on real hardware are acted on; everything else is logged at // debug level and dropped. Runs on this file's HID pump task. @@ -230,13 +292,6 @@ static void usbHidInputTask(void* arg) { while (ctx->running) { UsbHidEvent hid_evt; if (xQueueReceive(ctx->hid_queue, &hid_evt, pdMS_TO_TICKS(100)) != pdTRUE) { - if (!ctx->subscribed) { - Device* hid_dev; - if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &hid_dev) == ERROR_NONE) { - ctx->subscribed = usb_host_hid_subscribe(hid_dev, ctx->hid_queue); - device_put(hid_dev); - } - } continue; } @@ -391,11 +446,7 @@ void startUsbHidInput() { } lvgl_unlock(); - Device* hid_dev = nullptr; - if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &hid_dev) == ERROR_NONE) { - ctx->subscribed = usb_host_hid_subscribe(hid_dev, ctx->hid_queue); - device_put(hid_dev); - } + startHidListener(ctx->hid_queue); ctx->running = true; @@ -417,13 +468,7 @@ void startUsbHidInput() { if (ctx->task == nullptr) { LOG_E(TAG, "failed to create task"); ctx->running = false; - if (ctx->subscribed) { - Device* cleanup_dev = nullptr; - if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &cleanup_dev) == ERROR_NONE) { - usb_host_hid_unsubscribe(cleanup_dev, ctx->hid_queue); - device_put(cleanup_dev); - } - } + stopHidListener(ctx->hid_queue); memory_free(ctx->task_stack); memory_free(ctx->task_tcb); if (ctx->mouse_cursor != nullptr) { @@ -483,13 +528,7 @@ void stopUsbHidInput() { memory_free(ctx->task_stack); memory_free(ctx->task_tcb); - if (ctx->subscribed) { - Device* hid_dev; - if (device_get_first_active_by_type(&USB_HOST_HID_TYPE, &hid_dev) == ERROR_NONE) { - usb_host_hid_unsubscribe(hid_dev, ctx->hid_queue); - device_put(hid_dev); - } - } + stopHidListener(ctx->hid_queue); vQueueDelete(ctx->hid_queue); vQueueDelete(ctx->key_queue); vSemaphoreDelete(ctx->task_done); diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 507f724f5..f562b2491 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -255,8 +255,8 @@ class StatusbarService final : public Service { Device* midi_dev = nullptr; device_get_first_active_by_type(&USB_HOST_MIDI_TYPE, &midi_dev); - bool connected = (hid_dev && usb_host_hid_is_connected(hid_dev)) || - (midi_dev && usb_midi_is_connected(midi_dev)); + // These devices only exist while connected + bool connected = hid_dev != nullptr || midi_dev != nullptr; if (hid_dev) { device_put(hid_dev); diff --git a/Tactility/Source/settings/UsbHostSettings.cpp b/Tactility/Source/settings/UsbHostSettings.cpp new file mode 100644 index 000000000..1a61868d8 --- /dev/null +++ b/Tactility/Source/settings/UsbHostSettings.cpp @@ -0,0 +1,73 @@ +#include + +#include +#include + +#include + +#include +#include + +namespace tt::settings::usbhost { + +static bool getSettingsFilePath(std::string& outPath) { + char root[128]; + if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) { + return false; + } + outPath = std::string(root) + "/settings/usbhost.properties"; + return true; +} + +constexpr auto* SETTINGS_KEY_ENABLED = "enabled"; + +static bool toBool(const std::map& map, const char* key, bool defaultValue) { + auto entry = map.find(key); + if (entry == map.end()) { + return defaultValue; + } + return (entry->second == "1" || entry->second == "true" || entry->second == "True"); +} + +bool load(UsbHostSettings& settings) { + std::string settings_path; + if (!getSettingsFilePath(settings_path) || !file::isFile(settings_path)) { + return false; + } + + std::map map; + if (!file::loadPropertiesFile(settings_path, map)) { + return false; + } + + settings.enabled = toBool(map, SETTINGS_KEY_ENABLED, true); + return true; +} + +UsbHostSettings getDefault() { + return UsbHostSettings { .enabled = true }; +} + +UsbHostSettings loadOrGetDefault() { + UsbHostSettings settings; + if (!load(settings)) { + settings = getDefault(); + } + return settings; +} + +bool save(const UsbHostSettings& settings) { + std::map map; + map[SETTINGS_KEY_ENABLED] = settings.enabled ? "1" : "0"; + + std::string settings_path; + if (!getSettingsFilePath(settings_path)) { + return false; + } + if (!file::findOrCreateParentDirectory(settings_path, 0755)) { + return false; + } + return file::savePropertiesFile(settings_path, map); +} + +} // namespace tt::settings::usbhost diff --git a/TactilityKernel/include/tactility/drivers/usb_host.h b/TactilityKernel/include/tactility/drivers/usb_host.h new file mode 100644 index 000000000..78a8678b3 --- /dev/null +++ b/TactilityKernel/include/tactility/drivers/usb_host.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct Device; +struct DeviceType; + +enum UsbClass { + USB_HOST_CLASS_HID, + USB_HOST_CLASS_MIDI, + USB_HOST_CLASS_MSC, + USB_HOST_CLASS_AUDIO, +}; + +struct UsbHostApi { + /** + * Works while the host is stopped. + * @param[in] device the USB host device + * @param[in] usb_class the class to check + * @param[out] supported non-null, set to true when the hardware and the device configuration support the class + * @retval ERROR_NONE on success + * @retval ERROR_INVALID_ARGUMENT @a usb_class is out of range + */ + error_t (*is_class_supported)(struct Device* device, enum UsbClass usb_class, bool* supported); + /** + * @param[in] device the USB host device + * @param[in] usb_class the class to check + * @param[out] enabled non-null, set to true when the class is enabled and its driver is running + * @retval ERROR_NONE on success + * @retval ERROR_INVALID_ARGUMENT @a usb_class is out of range + * @retval ERROR_INVALID_STATE the host is not started + */ + error_t (*is_class_enabled)(struct Device* device, enum UsbClass usb_class, bool* enabled); + /** + * Starts or stops the detection of a class. Stopping removes the devices connected for that class. + * Restarting the host enables all supported classes again. + * @param[in] device the USB host device + * @param[in] usb_class the class to change + * @param[in] enabled true to start detecting the class + * @retval ERROR_NONE on success, also when the class already was in the requested state + * @retval ERROR_INVALID_ARGUMENT @a usb_class is out of range + * @retval ERROR_NOT_SUPPORTED the class is not supported + * @retval ERROR_INVALID_STATE the host is not started + * @return any other error when the class failed to start or stop + */ + error_t (*set_class_enabled)(struct Device* device, enum UsbClass usb_class, bool enabled); +}; + +extern const struct DeviceType USB_HOST_TYPE; + +error_t usb_host_is_class_supported(struct Device* host, enum UsbClass usb_class, bool* supported); + +error_t usb_host_is_class_enabled(struct Device* host, enum UsbClass usb_class, bool* enabled); + +error_t usb_host_set_class_enabled(struct Device* host, enum UsbClass usb_class, bool enabled); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/usb_host_hid.h b/TactilityKernel/include/tactility/drivers/usb_host_hid.h index d199569e4..bf5e123bf 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_hid.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_hid.h @@ -68,19 +68,19 @@ typedef struct { } UsbHidEvent; struct UsbHidApi { - bool (*is_connected)(struct Device* device); bool (*subscribe)(struct Device* device, UsbHidQueueHandle event_queue); void (*unsubscribe)(struct Device* device, UsbHidQueueHandle event_queue); }; +/** + * A USB_HOST_HID_TYPE device exists while at least one HID interface (keyboard, mouse or consumer control) is connected. + */ extern const struct DeviceType USB_HOST_HID_TYPE; -/** Returns true if any HID device (keyboard or mouse) is currently connected. */ -bool usb_host_hid_is_connected(struct Device* device); - /** * Subscribe a FreeRTOS queue to receive UsbHidEvent items from the HID driver. * A new subscriber immediately receives a *_CONNECTED event for each device that is already connected. + * Subscriptions end when the device stops. Subscribe again on DEVICE_EVENT_STARTED of the next device. */ bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue); diff --git a/TactilityKernel/include/tactility/drivers/usb_host_midi.h b/TactilityKernel/include/tactility/drivers/usb_host_midi.h index eb22006c2..67eb95fb0 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_midi.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_midi.h @@ -4,6 +4,9 @@ #include #include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -25,28 +28,87 @@ typedef struct { uint8_t data2; /**< Second data byte */ } usb_midi_message_t; -typedef void (*usb_midi_message_cb_t)(const usb_midi_message_t* msg, void* user_data); - -struct UsbMidiApi { - void (*set_callback)(struct Device* device, usb_midi_message_cb_t callback, void* user_data); - bool (*is_connected)(struct Device* device); -}; - +/** + * A USB_HOST_MIDI_TYPE device exists while a USB MIDI interface is connected. + */ extern const struct DeviceType USB_HOST_MIDI_TYPE; -/** - * Register a callback for incoming MIDI messages. - * Replaces any previously registered callback. Pass NULL to disable. - * @param device non-null ready USB MIDI device. - */ -// TODO: Make an interface that takes/releases control -void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data); +struct UsbMidiEvent { + /** Stamped by usb_midi_event_emit(); any value passed in by the caller is ignored. */ + uint64_t timestamp; + usb_midi_message_t message; +}; /** - * Returns true if a MIDI device is currently connected and streaming. - * @param device non-null ready USB MIDI device. + * Number of events that can be queued per subscription before usb_midi_event_emit() starts + * returning ERROR_RESOURCE (dropping the newest event, preserving FIFO order of what's + * already queued). Sized for bursts such as chords and controller sweeps. */ -bool usb_midi_is_connected(struct Device* device); +#define USB_MIDI_EVENT_QUEUE_CAPACITY 32 + +/** + * Caller-owned subscription node, registered with usb_midi_event_subscribe() and drained with + * usb_midi_event_poll(). Events queue by value (FIFO). + * @warning Fields other than `bit` are for internal use only; do not read or write them + * directly. + */ +struct UsbMidiEventSubscription { + /** Set by usb_midi_event_subscribe(). Read-only for the caller: OR it into a + * task_event_group_wait() mask to block on this subscription. */ + uint32_t bit; + + struct { + /** The MIDI device this subscription receives events for; set by usb_midi_event_subscribe(). */ + struct Device* device; + /** Caller-owned, borrowed; set by usb_midi_event_subscribe(). */ + struct TaskEventGroup* event_group; + struct UsbMidiEvent queue[USB_MIDI_EVENT_QUEUE_CAPACITY]; + uint8_t head; + uint8_t count; + struct UsbMidiEventSubscription* next; + } internal; +}; + +/** + * Register a subscription for incoming MIDI messages of @a device. + * @warning Does not work in ISR context. + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and + * stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be + * destructed only after usb_midi_event_unsubscribe()). To block for an event, call + * task_event_group_wait()/task_event_group_wait_any() on this group, then drain with + * usb_midi_event_poll(). + * @param[in] device the USB MIDI device. Unsubscribe on its DEVICE_EVENT_STOPPING: the next connection can reuse the same device pointer. + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered + * @retval ERROR_INVALID_STATE @a sub is already registered + */ +error_t usb_midi_event_subscribe(struct UsbMidiEventSubscription* sub, struct TaskEventGroup* event_group, struct Device* device); + +/** + * Remove a previously registered subscription. + * @warning Does not work in ISR context. + * @retval ERROR_NONE on success + * @retval ERROR_NOT_FOUND if no matching subscription exists + */ +error_t usb_midi_event_unsubscribe(struct UsbMidiEventSubscription* sub); + +/** + * Non-blocking: pop the next event for @a sub if one is already queued. + * @retval ERROR_NONE @a out_event was filled + * @retval ERROR_TIMEOUT nothing queued right now + */ +error_t usb_midi_event_poll(struct UsbMidiEventSubscription* sub, struct UsbMidiEvent* out_event); + +/** + * Queue @a message for every subscription of @a device. Called by USB MIDI drivers. + * Never calls subscriber code. + * @warning Does not work in ISR context. + * @retval ERROR_NONE delivered to every matching subscription + * @retval ERROR_NOT_FOUND no subscription for @a device + * @retval ERROR_RESOURCE at least one subscription's queue was full and dropped the event + */ +error_t usb_midi_event_emit(struct Device* device, const usb_midi_message_t* message); #ifdef __cplusplus } diff --git a/TactilityKernel/include/tactility/drivers/usb_host_msc.h b/TactilityKernel/include/tactility/drivers/usb_host_msc.h index 5a78184dc..fdb97821b 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_msc.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_msc.h @@ -16,13 +16,16 @@ struct UsbMscApi { bool (*eject)(struct Device* device, const char* mount_path); }; +/** + * A USB_HOST_MSC_TYPE device exists for each mounted USB drive. + */ extern const struct DeviceType USB_HOST_MSC_TYPE; /** - * Safely eject a mounted USB drive. - * @param device non-null ready USB MSC device (from device_find_first_active_by_type). + * Safely eject a mounted USB drive. The device is removed shortly after. + * @param device non-null ready USB MSC device * @param mount_path Full mount path (e.g. "/usb0"). - * @return true if the drive was found and ejected. + * @return true if @a device is the drive mounted at @a mount_path and it was ejected. */ bool usb_msc_eject(struct Device* device, const char* mount_path); diff --git a/TactilityKernel/source/drivers/usb_host.cpp b/TactilityKernel/source/drivers/usb_host.cpp new file mode 100644 index 000000000..07b949ab7 --- /dev/null +++ b/TactilityKernel/source/drivers/usb_host.cpp @@ -0,0 +1,25 @@ +#include +#include +#include + +#define USB_HOST_API(driver) ((struct UsbHostApi*)(driver)->api) + +extern "C" { + +const struct DeviceType USB_HOST_TYPE = { + .name = "usb-host", +}; + +error_t usb_host_is_class_supported(struct Device* host, enum UsbClass usb_class, bool* supported) { + return USB_HOST_API(device_get_driver(host))->is_class_supported(host, usb_class, supported); +} + +error_t usb_host_set_class_enabled(struct Device* host, enum UsbClass usb_class, bool enabled) { + return USB_HOST_API(device_get_driver(host))->set_class_enabled(host, usb_class, enabled); +} + +error_t usb_host_is_class_enabled(struct Device* host, enum UsbClass usb_class, bool* enabled) { + return USB_HOST_API(device_get_driver(host))->is_class_enabled(host, usb_class, enabled); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/usb_host_hid.cpp b/TactilityKernel/source/drivers/usb_host_hid.cpp index 3c64bf5f5..9f8dcca14 100644 --- a/TactilityKernel/source/drivers/usb_host_hid.cpp +++ b/TactilityKernel/source/drivers/usb_host_hid.cpp @@ -10,11 +10,6 @@ const struct DeviceType USB_HOST_HID_TYPE = { .name = "usb-host-hid", }; -bool usb_host_hid_is_connected(struct Device* device) { - auto* api = USB_HID_API(device_get_driver(device)); - return api->is_connected(device); -} - bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) { auto* api = USB_HID_API(device_get_driver(device)); return api->subscribe(device, event_queue); diff --git a/TactilityKernel/source/drivers/usb_host_midi.cpp b/TactilityKernel/source/drivers/usb_host_midi.cpp index a128eb1dd..ac4827625 100644 --- a/TactilityKernel/source/drivers/usb_host_midi.cpp +++ b/TactilityKernel/source/drivers/usb_host_midi.cpp @@ -1,8 +1,16 @@ #include +#include #include -#include +#include -#define USB_MIDI_API(driver) ((struct UsbMidiApi*)(driver)->api) +struct UsbMidiEventMutex { + Mutex handle {}; + UsbMidiEventMutex() { mutex_construct(&handle); } + ~UsbMidiEventMutex() { mutex_destruct(&handle); } +}; + +static UsbMidiEventSubscription* usb_midi_event_subscriptions = nullptr; +static UsbMidiEventMutex usb_midi_event_subscriptions_mutex; extern "C" { @@ -10,14 +18,97 @@ const struct DeviceType USB_HOST_MIDI_TYPE = { .name = "usb-host-midi", }; -void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) { - auto* api = USB_MIDI_API(device_get_driver(device)); - api->set_callback(device, callback, user_data); +error_t usb_midi_event_subscribe(UsbMidiEventSubscription* sub, TaskEventGroup* event_group, Device* device) { + uint32_t bit; + error_t claim_result = task_event_group_claim_bit(event_group, &bit); + if (claim_result != ERROR_NONE) { + return claim_result; + } + + mutex_lock(&usb_midi_event_subscriptions_mutex.handle); + + // Avoid cyclic subscription list that would loop forever + for (UsbMidiEventSubscription* existing = usb_midi_event_subscriptions; existing != nullptr; existing = existing->internal.next) { + if (existing == sub) { + mutex_unlock(&usb_midi_event_subscriptions_mutex.handle); + task_event_group_release_bit(event_group, bit); + return ERROR_INVALID_STATE; + } + } + + sub->bit = bit; + sub->internal.device = device; + sub->internal.event_group = event_group; + sub->internal.head = 0; + sub->internal.count = 0; + sub->internal.next = usb_midi_event_subscriptions; + usb_midi_event_subscriptions = sub; + mutex_unlock(&usb_midi_event_subscriptions_mutex.handle); + + return ERROR_NONE; } -bool usb_midi_is_connected(struct Device* device) { - auto* api = USB_MIDI_API(device_get_driver(device)); - return api->is_connected(device); +error_t usb_midi_event_unsubscribe(UsbMidiEventSubscription* sub) { + error_t result = ERROR_NOT_FOUND; + + mutex_lock(&usb_midi_event_subscriptions_mutex.handle); + for (UsbMidiEventSubscription** link = &usb_midi_event_subscriptions; *link != nullptr; link = &(*link)->internal.next) { + if (*link == sub) { + *link = sub->internal.next; + result = ERROR_NONE; + break; + } + } + mutex_unlock(&usb_midi_event_subscriptions_mutex.handle); + + if (result == ERROR_NONE) { + task_event_group_release_bit(sub->internal.event_group, sub->bit); + } + + return result; +} + +error_t usb_midi_event_emit(Device* device, const usb_midi_message_t* message) { + UsbMidiEvent stamped_event {}; + stamped_event.timestamp = get_micros_since_boot(); + stamped_event.message = *message; + + error_t result = ERROR_NOT_FOUND; + + mutex_lock(&usb_midi_event_subscriptions_mutex.handle); + for (UsbMidiEventSubscription* sub = usb_midi_event_subscriptions; sub != nullptr; sub = sub->internal.next) { + if (sub->internal.device != device) { + continue; + } + + if (sub->internal.count >= USB_MIDI_EVENT_QUEUE_CAPACITY) { + result = ERROR_RESOURCE; + continue; + } + + uint8_t tail = (sub->internal.head + sub->internal.count) % USB_MIDI_EVENT_QUEUE_CAPACITY; + sub->internal.queue[tail] = stamped_event; + sub->internal.count++; + if (result != ERROR_RESOURCE) { + result = ERROR_NONE; + } + task_event_group_signal(sub->internal.event_group, sub->bit); + } + mutex_unlock(&usb_midi_event_subscriptions_mutex.handle); + + return result; +} + +error_t usb_midi_event_poll(UsbMidiEventSubscription* sub, UsbMidiEvent* out_event) { + mutex_lock(&usb_midi_event_subscriptions_mutex.handle); + bool has_event = sub->internal.count > 0; + if (has_event) { + *out_event = sub->internal.queue[sub->internal.head]; + sub->internal.head = (sub->internal.head + 1) % USB_MIDI_EVENT_QUEUE_CAPACITY; + sub->internal.count--; + } + mutex_unlock(&usb_midi_event_subscriptions_mutex.handle); + return has_event ? ERROR_NONE : ERROR_TIMEOUT; } } // extern "C" diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 7481a24f6..fe01c4add 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -449,14 +450,20 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(lora_tx_event_unsubscribe), DEFINE_MODULE_SYMBOL(lora_tx_event_poll), DEFINE_MODULE_SYMBOL(LORA_TYPE), + // drivers/usb_host + DEFINE_MODULE_SYMBOL(usb_host_is_class_supported), + DEFINE_MODULE_SYMBOL(usb_host_is_class_enabled), + DEFINE_MODULE_SYMBOL(usb_host_set_class_enabled), + DEFINE_MODULE_SYMBOL(USB_HOST_TYPE), // drivers/usb_host_hid - DEFINE_MODULE_SYMBOL(usb_host_hid_is_connected), DEFINE_MODULE_SYMBOL(usb_host_hid_subscribe), DEFINE_MODULE_SYMBOL(usb_host_hid_unsubscribe), DEFINE_MODULE_SYMBOL(USB_HOST_HID_TYPE), // drivers/usb_host_midi - DEFINE_MODULE_SYMBOL(usb_midi_set_callback), - DEFINE_MODULE_SYMBOL(usb_midi_is_connected), + DEFINE_MODULE_SYMBOL(usb_midi_event_subscribe), + DEFINE_MODULE_SYMBOL(usb_midi_event_unsubscribe), + DEFINE_MODULE_SYMBOL(usb_midi_event_poll), + DEFINE_MODULE_SYMBOL(usb_midi_event_emit), DEFINE_MODULE_SYMBOL(USB_HOST_MIDI_TYPE), // drivers/usb_host_msc DEFINE_MODULE_SYMBOL(usb_msc_eject), diff --git a/TactilityKernel/tests/source/usb_host_midi_event_test.cpp b/TactilityKernel/tests/source/usb_host_midi_event_test.cpp new file mode 100644 index 000000000..2b645ceb4 --- /dev/null +++ b/TactilityKernel/tests/source/usb_host_midi_event_test.cpp @@ -0,0 +1,97 @@ +#include "doctest.h" + +#include +#include +#include + +static usb_midi_message_t make_message(uint8_t data1) { + return usb_midi_message_t { .cable = 0, .status = 0x90, .data1 = data1, .data2 = 100 }; +} + +TEST_CASE("usb_midi_event_emit queues messages in FIFO order and signals the subscription bit") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + + auto first = make_message(60); + auto second = make_message(64); + CHECK_EQ(usb_midi_event_emit(&device, &first), ERROR_NONE); + CHECK_EQ(usb_midi_event_emit(&device, &second), ERROR_NONE); + + uint32_t flags = 0; + CHECK_EQ(task_event_group_wait(&group, sub.bit, false, &flags, 0), ERROR_NONE); + CHECK_EQ(flags, sub.bit); + + UsbMidiEvent event = {}; + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, 60); + CHECK_EQ(event.message.status, 0x90); + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, 64); + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_emit only delivers to subscriptions of the emitting device") { + Device device_a = {}; + Device device_b = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device_a), ERROR_NONE); + + auto message = make_message(60); + CHECK_EQ(usb_midi_event_emit(&device_b, &message), ERROR_NOT_FOUND); + + UsbMidiEvent event = {}; + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_emit drops the newest message when the queue is full") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + + for (int i = 0; i < USB_MIDI_EVENT_QUEUE_CAPACITY; i++) { + auto message = make_message(static_cast(i)); + CHECK_EQ(usb_midi_event_emit(&device, &message), ERROR_NONE); + } + auto overflow = make_message(127); + CHECK_EQ(usb_midi_event_emit(&device, &overflow), ERROR_RESOURCE); + + UsbMidiEvent event = {}; + for (int i = 0; i < USB_MIDI_EVENT_QUEUE_CAPACITY; i++) { + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, i); + } + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_subscribe rejects a subscription that is already registered") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + CHECK_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_INVALID_STATE); + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NOT_FOUND); + + task_event_group_destruct(&group); +}