USB host refactored (#684)

- Create `USB_HOST_TYPE` base driver in TactilityKernel
- Refactor esp32_usbhost dts format, and create child devices dynamically
- Use PSRAM for task stacks where possible: saving 20 kB from just the task stacks, but roughly 30 kB when nothing is connected.
- Allow the user to disable USB host interface in the USB settings app, saving an additionaly 20 kB of IRAM when nothing was connected.
- Change listener-based APIs to subscribe and poll/await pattern. This avoids callstacks of apps and services leaking into the USB API

This results in big IRAM savings: 30 kB when USB host is on and nothing is connected, and 50 kB total when USB host is disabled.
This brings the total free IRAM after boot from roughly 170 kB to 200 kB (host enabled) or 220 kB (host disabled).
This commit is contained in:
Ken Van Hoeylandt 2026-10-10 13:33:32 +02:00 committed by GitHub
parent def65ef899
commit cb4ce5246d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1629 additions and 473 deletions

View File

@ -107,20 +107,5 @@
usbhost0 { usbhost0 {
compatible = "espressif,esp32-usbhost"; 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";
};
}; };
}; };

View File

@ -198,26 +198,6 @@
usbhost0 { usbhost0 {
compatible = "espressif,esp32-usbhost"; compatible = "espressif,esp32-usbhost";
peripheral-map = <0>; 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 { usbdevice0 {

View File

@ -177,17 +177,5 @@
usbhost0 { usbhost0 {
compatible = "espressif,esp32-usbhost"; compatible = "espressif,esp32-usbhost";
usbhosthid0 {
compatible = "espressif,esp32-usbhost-hid";
};
usbhostmidi0 {
compatible = "espressif,esp32-usbhost-midi";
};
usbhostmsc0 {
compatible = "espressif,esp32-usbhost-msc";
};
}; };
}; };

View File

@ -7,12 +7,11 @@
## Higher Priority ## Higher Priority
- Update to LVGL 9.6.x (ESP-IDF and POSIX) - Improve posix support: use user home path and system /tmp folder. implement uart/i2c drivers properly.
- 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 Setup: Add keyboard/keypad/encoder navigation explanation. - Improve Setup: Add keyboard/keypad/encoder navigation explanation.
e.g. encoder long press on t-lora pager to get out of textarea 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. - 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) - Improve SPI kernel driver (implement read, write, transactions)
- TCA9534 keyboards should use interrupts - TCA9534 keyboards should use interrupts
@ -42,7 +41,6 @@
- Consider moving certain drivers into separate modules: audio, bt, wifi, etc - Consider moving certain drivers into separate modules: audio, bt, wifi, etc
- Consider using https://github.com/Graphify-Labs/graphify - 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 - 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) - TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
- Refactor HttpServer into C code and move implementation to http-module - Refactor HttpServer into C code and move implementation to http-module
- Use GPS time to set/update the current time - Use GPS time to set/update the current time

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -13,3 +13,19 @@ properties:
Default 0 selects the default peripheral (P4 default: controller 0, HS/UTMI). 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(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) 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)

View File

@ -9,10 +9,6 @@ extern "C" {
#endif #endif
DEFINE_DEVICETREE(esp32_usbhost, struct Esp32UsbHostConfig) 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 #ifdef __cplusplus
} }

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
#pragma once #pragma once
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#ifdef __cplusplus #ifdef __cplusplus
@ -9,10 +10,10 @@ extern "C" {
struct Esp32UsbHostConfig { 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) */ 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) */
}; bool hid;
bool midi;
struct Esp32UsbHostChildConfig { bool msc;
int _unused; bool audio;
}; };
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -0,0 +1,28 @@
#pragma once
#include <tactility/error.h>
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;

View File

@ -0,0 +1,21 @@
#pragma once
#include <sdkconfig.h>
#include <freertos/FreeRTOS.h>
#include <freertos/idf_additions.h>
#include <esp_heap_caps.h>
/**
* 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);
}

View File

@ -0,0 +1,39 @@
#pragma once
#include <tactility/error.h>
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);

View File

@ -4,9 +4,14 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost.h> #include <tactility/drivers/esp32_usbhost.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/usb_host.h>
#include <tactility/log.h> #include <tactility/log.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <usb/usb_host.h> #include <usb/usb_host.h>
@ -16,17 +21,40 @@
#define GET_CONFIG(device) ((const Esp32UsbHostConfig*)(device)->config) #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_STACK = 4096;
constexpr auto USB_LIB_TASK_PRIORITY = 10; constexpr auto USB_LIB_TASK_PRIORITY = 10;
constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500; constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500;
constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000; constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000;
constexpr auto USB_HOST_STOP_RETRY_MS = 1000; 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 { struct UsbHostWorkerJob {
TaskHandle_t lib_task = nullptr; // nullptr stops the worker
SemaphoreHandle_t lib_task_done = nullptr; 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) { static void usbLibTask(void* arg) {
auto* ctx = static_cast<UsbHostContext*>(arg); auto* ctx = static_cast<UsbHostContext*>(arg);
LOG_I(TAG, "lib task started"); LOG_I(TAG, "lib task started");
@ -53,18 +81,237 @@ static void usbLibTask(void* arg) {
LOG_I(TAG, "lib task stopping"); LOG_I(TAG, "lib task stopping");
xSemaphoreGive(ctx->lib_task_done); 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<UsbHostContext*>(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); 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) { static error_t create_worker_queue(UsbHostContext* ctx) {
auto* cfg = GET_CONFIG(device); ctx->worker_mutex = xSemaphoreCreateMutex();
if (!cfg) { ctx->worker_queue = xQueueCreate(USB_WORKER_QUEUE_SIZE, sizeof(UsbHostWorkerJob*));
LOG_E(TAG, "device config is null"); if (ctx->worker_mutex == nullptr || ctx->worker_queue == nullptr) {
return ERROR_INVALID_ARGUMENT; 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<UsbHostContext*>(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<DeviceCreateJob*>(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<DeviceDestroyJob*>(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 = { usb_host_config_t host_cfg = {
.skip_phy_setup = false, .skip_phy_setup = false,
.root_port_unpowered = false, .root_port_unpowered = false,
@ -80,35 +327,27 @@ static error_t start_device(struct Device* device) {
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
auto* ctx = new UsbHostContext();
ctx->lib_task_done = xSemaphoreCreateBinary(); ctx->lib_task_done = xSemaphoreCreateBinary();
if (!ctx->lib_task_done) { if (!ctx->lib_task_done) {
LOG_E(TAG, "failed to create lib_task_done semaphore"); LOG_E(TAG, "failed to create lib_task_done semaphore");
delete ctx;
usb_host_uninstall(); usb_host_uninstall();
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
BaseType_t result = xTaskCreate(usbLibTask, "usb_lib", USB_LIB_TASK_STACK, BaseType_t result = esp32_usbhost_task_create_psram(usbLibTask, "usb_lib", USB_LIB_TASK_STACK,
ctx, USB_LIB_TASK_PRIORITY, &ctx->lib_task); ctx, USB_LIB_TASK_PRIORITY, &ctx->lib_task);
if (result != pdPASS) { if (result != pdPASS) {
LOG_E(TAG, "failed to create usb_lib task"); LOG_E(TAG, "failed to create usb_lib task");
vSemaphoreDelete(ctx->lib_task_done); vSemaphoreDelete(ctx->lib_task_done);
delete ctx; ctx->lib_task_done = nullptr;
usb_host_uninstall(); usb_host_uninstall();
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
device_set_driver_data(device, ctx);
LOG_I(TAG, "started (peripheral_map=0x%02x)", cfg->peripheral_map);
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop_device(struct Device* device) { static error_t uninstall_lib(UsbHostContext* ctx) {
auto* ctx = static_cast<UsbHostContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
xTaskNotifyGive(ctx->lib_task); xTaskNotifyGive(ctx->lib_task);
bool exited = (xSemaphoreTake(ctx->lib_task_done, pdMS_TO_TICKS(USB_HOST_STOP_TIMEOUT_MS)) == pdTRUE); 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) { if (!exited) {
LOG_E(TAG, "lib task stop timed out after %dms, force terminating — USB host restart required", 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); USB_HOST_STOP_TIMEOUT_MS + USB_HOST_STOP_RETRY_MS);
vTaskDelete(ctx->lib_task); vTaskDeleteWithCaps(ctx->lib_task);
vTaskDelay(pdMS_TO_TICKS(50)); vTaskDelay(pdMS_TO_TICKS(50));
// Skip usb_host_uninstall — USB stack is in undefined state. // Skip usb_host_uninstall — USB stack is in undefined state.
ctx->lib_task = nullptr; ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done); vSemaphoreDelete(ctx->lib_task_done);
device_set_driver_data(device, nullptr); ctx->lib_task_done = nullptr;
delete ctx;
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
ctx->lib_task = nullptr; ctx->lib_task = nullptr;
vSemaphoreDelete(ctx->lib_task_done); vSemaphoreDelete(ctx->lib_task_done);
ctx->lib_task_done = nullptr;
esp_err_t uninstall_err = usb_host_uninstall(); esp_err_t uninstall_err = usb_host_uninstall();
if (uninstall_err != ESP_OK) { if (uninstall_err != ESP_OK) {
LOG_W(TAG, "usb_host_uninstall: %s", esp_err_to_name(uninstall_err)); 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; 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<UsbHostContext*>(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<UsbHostContext*>(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<UsbClass>(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<UsbHostContext*>(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<UsbClass>(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 = { Driver esp32_usbhost_driver = {
.name = "esp32_usbhost", .name = "esp32_usbhost",
.compatible = (const char*[]) { "espressif,esp32-usbhost", nullptr }, .compatible = (const char*[]) { "espressif,esp32-usbhost", nullptr },
.start_device = start_device, .start_device = start_device,
.stop_device = stop_device, .stop_device = stop_device,
.api = nullptr, .probe = nullptr,
.device_type = nullptr, .api = &usb_host_api,
.device_type = &USB_HOST_TYPE,
.owner = nullptr, .owner = nullptr,
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -3,6 +3,9 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/hid_consumer.h> #include <tactility/drivers/hid_consumer.h>
#include <tactility/drivers/keyboard.h> #include <tactility/drivers/keyboard.h>
#include <tactility/drivers/usb_host_hid.h> #include <tactility/drivers/usb_host_hid.h>
@ -38,7 +41,6 @@ typedef struct {
struct UsbHidContext { struct UsbHidContext {
std::atomic<bool> mouse_connected{false}; std::atomic<bool> mouse_connected{false};
std::atomic<bool> device_connected{false};
QueueHandle_t hid_event_queue = nullptr; QueueHandle_t hid_event_queue = nullptr;
TaskHandle_t hid_proc_task = nullptr; TaskHandle_t hid_proc_task = nullptr;
SemaphoreHandle_t hid_proc_task_done = nullptr; SemaphoreHandle_t hid_proc_task_done = nullptr;
@ -64,16 +66,21 @@ struct UsbHidContext {
QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {}; QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {};
SemaphoreHandle_t sub_mutex = nullptr; SemaphoreHandle_t sub_mutex = nullptr;
// The controller Device itself (set in start_device()), used as the parent for kb_device. Device* host = nullptr;
Device* controller_device = nullptr; // USB_HOST_HID_TYPE device, exists while any HID interface is open
// Dynamic KEYBOARD_TYPE child device, constructed while a physical USB keyboard is connected. Device hid_device{};
bool hid_device_active = false;
// KEYBOARD_TYPE child of hid_device, exists while a keyboard is open
Device kb_device{}; Device kb_device{};
bool kb_device_active = false; bool kb_device_active = false;
}; };
extern "C" { 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_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); 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; ctx->consumer_prev_count = 0;
} }
hid_host_device_close(handle); 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) { if (params.proto == HID_PROTOCOL_KEYBOARD || params.proto == HID_PROTOCOL_MOUSE) {
UsbHidEventType disc_type = (params.proto == HID_PROTOCOL_KEYBOARD) UsbHidEventType disc_type = (params.proto == HID_PROTOCOL_KEYBOARD)
? USB_HID_EVENT_KEYBOARD_DISCONNECTED ? 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 = {} }; UsbHidEvent evt = { .type = disc_type, .scroll = {} };
publish_event(ctx, &evt); publish_event(ctx, &evt);
} }
usb_hid_device_destroy_if_unused(ctx);
break; break;
case HID_HOST_INTERFACE_EVENT_TRANSFER_ERROR: 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) { static void hid_proc_task(void* arg) {
auto* ctx = static_cast<UsbHidContext*>(arg); auto* ctx = static_cast<UsbHidContext*>(arg);
LOG_I(TAG, "HID proc task started"); 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); 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. // No boot-protocol switch: consumer devices only implement report protocol.
ctx->consumer_map = map; ctx->consumer_map = map;
ctx->consumer_prev_count = 0; ctx->consumer_prev_count = 0;
ctx->consumer_handle.store(dev_evt.handle); ctx->consumer_handle.store(dev_evt.handle);
ctx->device_connected = true;
hid_host_device_start(dev_evt.handle); hid_host_device_start(dev_evt.handle);
continue; continue;
} }
@ -363,6 +368,8 @@ static void hid_proc_task(void* arg) {
continue; continue;
} }
hid_class_request_set_protocol(dev_evt.handle, HID_REPORT_PROTOCOL_BOOT); 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) { if (params.proto == HID_PROTOCOL_KEYBOARD) {
hid_class_request_set_idle(dev_evt.handle, 0, 0); hid_class_request_set_idle(dev_evt.handle, 0, 0);
vTaskDelay(pdMS_TO_TICKS(200)); vTaskDelay(pdMS_TO_TICKS(200));
@ -375,7 +382,6 @@ static void hid_proc_task(void* arg) {
} else if (params.proto == HID_PROTOCOL_MOUSE) { } else if (params.proto == HID_PROTOCOL_MOUSE) {
ctx->mouse_connected = true; ctx->mouse_connected = true;
} }
ctx->device_connected = true;
{ {
UsbHidEventType conn_type = (params.proto == HID_PROTOCOL_KEYBOARD) UsbHidEventType conn_type = (params.proto == HID_PROTOCOL_KEYBOARD)
? USB_HID_EVENT_KEYBOARD_CONNECTED ? USB_HID_EVENT_KEYBOARD_CONNECTED
@ -389,12 +395,7 @@ static void hid_proc_task(void* arg) {
LOG_I(TAG, "HID proc task stopped"); LOG_I(TAG, "HID proc task stopped");
xSemaphoreGive(ctx->hid_proc_task_done); xSemaphoreGive(ctx->hid_proc_task_done);
vTaskDelete(nullptr); vTaskDeleteWithCaps(nullptr);
}
static bool api_hid_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
return ctx && ctx->device_connected.load();
} }
static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) { 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 = { static const UsbHidApi hid_api = {
.is_connected = api_hid_is_connected,
.subscribe = api_hid_subscribe, .subscribe = api_hid_subscribe,
.unsubscribe = api_hid_unsubscribe, .unsubscribe = api_hid_unsubscribe,
}; };
@ -515,51 +515,75 @@ Driver esp32_usbhost_hid_keyboard_driver = {
}; };
static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) { static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
if (ctx->kb_device_active) { if (ctx->kb_device_active || !ctx->hid_device_active) {
return; 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 = Device { ctx->kb_device_active = true;
.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;
} }
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) { if (!ctx->kb_device_active) {
return; return ERROR_NONE;
} }
ctx->kb_device_active = false; error_t error = esp32_usbhost_device_destroy(ctx->host, &ctx->kb_device);
if (error == ERROR_NONE) {
device_stop(&ctx->kb_device); ctx->kb_device_active = false;
device_remove(&ctx->kb_device); }
device_destruct(&ctx->kb_device); 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<UsbHidContext*>(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) { 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) { if (!ctx->kb_device_active) {
return; return;
@ -571,9 +595,9 @@ static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bo
// endregion // endregion
static error_t start_device(struct Device* device) { static error_t backend_start(Device* host, void** out_context) {
auto* ctx = new UsbHidContext(); auto* ctx = new UsbHidContext();
ctx->controller_device = device; ctx->host = host;
ctx->sub_mutex = xSemaphoreCreateMutex(); ctx->sub_mutex = xSemaphoreCreateMutex();
if (!ctx->sub_mutex) { if (!ctx->sub_mutex) {
@ -617,8 +641,8 @@ static error_t start_device(struct Device* device) {
} }
ctx->hid_proc_running = true; ctx->hid_proc_running = true;
BaseType_t result = xTaskCreate(hid_proc_task, "hid_proc", HID_PROC_TASK_STACK, 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); ctx, HID_PROC_TASK_PRIORITY, &ctx->hid_proc_task);
if (result != pdPASS) { if (result != pdPASS) {
LOG_E(TAG, "failed to create hid_proc task"); LOG_E(TAG, "failed to create hid_proc task");
ctx->hid_proc_running = false; ctx->hid_proc_running = false;
@ -630,42 +654,52 @@ static error_t start_device(struct Device* device) {
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
device_set_driver_data(device, ctx); *out_context = ctx;
LOG_I(TAG, "started"); LOG_I(TAG, "started");
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop_device(struct Device* device) { static error_t backend_stop(void* context) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device)); auto* ctx = static_cast<UsbHidContext*>(context);
if (!ctx) return ERROR_NONE;
// hid_host_device_close() halts and flushes the pending IN transfer before returning, so no // 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. // 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_device_close(kb_handle);
} }
// hid_host_uninstall() below can fail with any interface still registered, and its callback // hid_host_uninstall() below can fail with any interface still registered, and its callback
// still references ctx, about to be deleted. // 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); hid_host_device_close(consumer_handle);
} }
usb_hid_keyboard_device_destruct(ctx);
ctx->hid_proc_running = false; 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) { 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"); LOG_W(TAG, "HID proc task stop timed out, force terminating");
vTaskDelete(ctx->hid_proc_task); 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(); hid_host_uninstall();
if (ctx->hid_event_queue) { vQueueDelete(ctx->hid_event_queue); ctx->hid_event_queue = nullptr; } 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; } if (ctx->sub_mutex) { vSemaphoreDelete(ctx->sub_mutex); ctx->sub_mutex = nullptr; }
device_set_driver_data(device, nullptr);
delete ctx; delete ctx;
LOG_I(TAG, "stopped"); LOG_I(TAG, "stopped");
return ERROR_NONE; return ERROR_NONE;
@ -673,9 +707,9 @@ static error_t stop_device(struct Device* device) {
Driver esp32_usbhost_hid_driver = { Driver esp32_usbhost_hid_driver = {
.name = "esp32_usbhost_hid", .name = "esp32_usbhost_hid",
.compatible = (const char*[]) { "espressif,esp32-usbhost-hid", nullptr }, .compatible = (const char*[]) { nullptr },
.start_device = start_device, .start_device = hid_device_start,
.stop_device = stop_device, .stop_device = hid_device_stop,
.api = &hid_api, .api = &hid_api,
.device_type = &USB_HOST_HID_TYPE, .device_type = &USB_HOST_HID_TYPE,
.owner = nullptr, .owner = nullptr,
@ -684,4 +718,9 @@ Driver esp32_usbhost_hid_driver = {
} // extern "C" } // extern "C"
const Esp32UsbHostBackend esp32_usbhost_hid_backend = {
.start = backend_start,
.stop = backend_stop,
};
#endif // CONFIG_SOC_USB_OTG_SUPPORTED #endif // CONFIG_SOC_USB_OTG_SUPPORTED

View File

@ -3,6 +3,9 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/usb_host_midi.h> #include <tactility/drivers/usb_host_midi.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -28,7 +31,13 @@ constexpr auto MIDI_TRANSFER_BUF_SIZE = 512;
constexpr uint8_t MIDI_INTF_CLASS = 0x01; constexpr uint8_t MIDI_INTF_CLASS = 0x01;
constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03; constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03;
extern "C" Driver esp32_usbhost_midi_driver;
struct UsbMidiContext { 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_host_client_handle_t client_hdl = nullptr;
usb_device_handle_t dev_hdl = nullptr; usb_device_handle_t dev_hdl = nullptr;
usb_transfer_t* transfer = nullptr; usb_transfer_t* transfer = nullptr;
@ -38,10 +47,6 @@ struct UsbMidiContext {
std::atomic<bool> running{false}; std::atomic<bool> running{false};
TaskHandle_t task_handle = nullptr; TaskHandle_t task_handle = nullptr;
SemaphoreHandle_t task_done = 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) { 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) { 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) { for (int i = 0; i + 3 < len; i += 4) {
uint8_t cin = buf[i] & 0x0F; uint8_t cin = buf[i] & 0x0F;
if (cin < 0x02) continue; 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], .data1 = buf[i + 2],
.data2 = buf[i + 3], .data2 = buf[i + 3],
}; };
cb(&msg, arg); usb_midi_event_emit(&ctx->midi_device, &msg);
} }
} }
static void midi_transfer_cb(usb_transfer_t* transfer) { static void midi_transfer_cb(usb_transfer_t* transfer) {
auto* ctx = static_cast<UsbMidiContext*>(transfer->context); auto* ctx = static_cast<UsbMidiContext*>(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); dispatch_midi_packets(ctx, transfer->data_buffer, transfer->actual_num_bytes);
} }
if (ctx->running && ctx->connected && transfer->status != USB_TRANSFER_STATUS_NO_DEVICE) { 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) { static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) {
auto* ctx = static_cast<UsbMidiContext*>(arg); auto* ctx = static_cast<UsbMidiContext*>(arg);
if (msg->event == USB_HOST_CLIENT_EVENT_NEW_DEV) { 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"); LOG_W(TAG, "ignoring additional MIDI device while one is already active");
return; 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->context = ctx;
ctx->transfer->timeout_ms = 0; 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) { 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); LOG_I(TAG, "MIDI device connected (intf=%d ep=0x%02x)", intf_num, ep_addr);
} else { } else {
LOG_E(TAG, "failed to submit initial MIDI transfer"); 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_interface_release(ctx->client_hdl, dev_hdl, intf_num);
usb_host_device_close(ctx->client_hdl, dev_hdl); usb_host_device_close(ctx->client_hdl, dev_hdl);
ctx->dev_hdl = nullptr; 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) { if (ctx->dev_hdl && msg->dev_gone.dev_hdl == ctx->dev_hdl) {
LOG_I(TAG, "MIDI device disconnected"); LOG_I(TAG, "MIDI device disconnected");
ctx->connected = false; ctx->connected = false;
midi_device_destroy(ctx);
usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num); usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num);
usb_host_device_close(ctx->client_hdl, ctx->dev_hdl); usb_host_device_close(ctx->client_hdl, ctx->dev_hdl);
ctx->dev_hdl = nullptr; 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) { static void midi_client_task(void* arg) {
auto* ctx = static_cast<UsbMidiContext*>(arg); auto* ctx = static_cast<UsbMidiContext*>(arg);
LOG_I(TAG, "MIDI client task started"); 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); usb_host_device_close(ctx->client_hdl, ctx->dev_hdl);
ctx->dev_hdl = nullptr; ctx->dev_hdl = nullptr;
} }
midi_device_destroy(ctx);
LOG_I(TAG, "MIDI client task stopped"); LOG_I(TAG, "MIDI client task stopped");
xSemaphoreGive(ctx->task_done); 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<UsbMidiContext*>(device_get_driver_data(device));
if (!ctx) return;
taskENTER_CRITICAL(&ctx->callback_lock);
ctx->callback = callback;
ctx->callback_arg = user_data;
taskEXIT_CRITICAL(&ctx->callback_lock);
}
static bool api_is_connected(struct Device* device) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device));
return ctx && ctx->connected.load();
}
static const UsbMidiApi midi_api = {
.set_callback = api_set_callback,
.is_connected = api_is_connected,
};
extern "C" { 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(); auto* ctx = new UsbMidiContext();
ctx->host = host;
if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) { if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) {
LOG_E(TAG, "failed to allocate MIDI transfer"); LOG_E(TAG, "failed to allocate MIDI transfer");
@ -245,8 +260,8 @@ static error_t start_device(struct Device* device) {
} }
ctx->running = true; ctx->running = true;
BaseType_t result = xTaskCreate(midi_client_task, "midi_client", MIDI_TASK_STACK, BaseType_t result = esp32_usbhost_task_create_psram(midi_client_task, "midi_client", MIDI_TASK_STACK,
ctx, MIDI_TASK_PRIORITY, &ctx->task_handle); ctx, MIDI_TASK_PRIORITY, &ctx->task_handle);
if (result != pdPASS) { if (result != pdPASS) {
LOG_E(TAG, "failed to create midi_client task"); LOG_E(TAG, "failed to create midi_client task");
ctx->running = false; ctx->running = false;
@ -257,35 +272,42 @@ static error_t start_device(struct Device* device) {
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
device_set_driver_data(device, ctx); *out_context = ctx;
LOG_I(TAG, "started"); LOG_I(TAG, "started");
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop_device(struct Device* device) { static error_t backend_stop(void* context) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device)); auto* ctx = static_cast<UsbMidiContext*>(context);
if (!ctx) return ERROR_NONE;
ctx->running = false; if (ctx->task_handle != nullptr) {
usb_host_client_unblock(ctx->client_hdl); ctx->running = false;
usb_host_client_unblock(ctx->client_hdl);
if (xSemaphoreTake(ctx->task_done, pdMS_TO_TICKS(MIDI_STOP_TIMEOUT_MS)) != pdTRUE) { 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); 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) { if (ctx->dev_hdl) {
ctx->connected = false; ctx->connected = false;
if (usb_host_interface_release(ctx->client_hdl, ctx->dev_hdl, ctx->intf_num) != ESP_OK) { 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"); 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) { vTaskDeleteWithCaps(ctx->task_handle);
LOG_W(TAG, "failed to close MIDI device during force-stop"); vTaskDelay(pdMS_TO_TICKS(50));
}
ctx->dev_hdl = nullptr;
} }
vTaskDelete(ctx->task_handle); ctx->task_handle = nullptr;
vTaskDelay(pdMS_TO_TICKS(50)); 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); usb_host_client_deregister(ctx->client_hdl);
ctx->client_hdl = nullptr; ctx->client_hdl = nullptr;
@ -293,7 +315,6 @@ static error_t stop_device(struct Device* device) {
usb_host_transfer_free(ctx->transfer); usb_host_transfer_free(ctx->transfer);
ctx->transfer = nullptr; ctx->transfer = nullptr;
device_set_driver_data(device, nullptr);
delete ctx; delete ctx;
LOG_I(TAG, "stopped"); LOG_I(TAG, "stopped");
return ERROR_NONE; return ERROR_NONE;
@ -301,10 +322,10 @@ static error_t stop_device(struct Device* device) {
Driver esp32_usbhost_midi_driver = { Driver esp32_usbhost_midi_driver = {
.name = "esp32_usbhost_midi", .name = "esp32_usbhost_midi",
.compatible = (const char*[]) { "espressif,esp32-usbhost-midi", nullptr }, .compatible = (const char*[]) { nullptr },
.start_device = start_device, .start_device = midi_device_start,
.stop_device = stop_device, .stop_device = midi_device_stop,
.api = &midi_api, .api = nullptr,
.device_type = &USB_HOST_MIDI_TYPE, .device_type = &USB_HOST_MIDI_TYPE,
.owner = nullptr, .owner = nullptr,
.internal = nullptr, .internal = nullptr,
@ -312,4 +333,9 @@ Driver esp32_usbhost_midi_driver = {
} // extern "C" } // extern "C"
const Esp32UsbHostBackend esp32_usbhost_midi_backend = {
.start = backend_start,
.stop = backend_stop,
};
#endif // CONFIG_SOC_USB_OTG_SUPPORTED #endif // CONFIG_SOC_USB_OTG_SUPPORTED

View File

@ -3,6 +3,9 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/usb_host_msc.h> #include <tactility/drivers/usb_host_msc.h>
#include <tactility/filesystem/file_system.h> #include <tactility/filesystem/file_system.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -44,13 +47,14 @@ using msc_event_id_t = decltype(msc_host_event_t{}.event);
static constexpr msc_event_id_t kMscDeviceConnected = static_cast<msc_event_id_t>(0); static constexpr msc_event_id_t kMscDeviceConnected = static_cast<msc_event_id_t>(0);
static constexpr msc_event_id_t kMscDeviceDisconnected = static_cast<msc_event_id_t>(1); static constexpr msc_event_id_t kMscDeviceDisconnected = static_cast<msc_event_id_t>(1);
enum class MscMsgId : uint8_t { Connected, Disconnected }; enum class MscMsgId : uint8_t { Connected, Disconnected, Ejected };
typedef struct { typedef struct {
MscMsgId id; MscMsgId id;
union { union {
uint8_t address; uint8_t address;
msc_host_device_handle_t handle; msc_host_device_handle_t handle;
int slot;
}; };
} msc_msg_t; } msc_msg_t;
@ -61,8 +65,17 @@ struct UsbMscContext {
TaskHandle_t proc_task = nullptr; TaskHandle_t proc_task = nullptr;
SemaphoreHandle_t proc_task_done = nullptr; SemaphoreHandle_t proc_task_done = nullptr;
std::atomic<bool> proc_running{false}; std::atomic<bool> 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_mount(void* /*data*/) { return ERROR_NONE; }
static error_t usb_fs_unmount(void* /*data*/) { return ERROR_NONE; } static error_t usb_fs_unmount(void* /*data*/) { return ERROR_NONE; }
static bool usb_fs_is_mounted(void* data) { 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) { static int find_free_slot(UsbMscContext* ctx) {
for (int i = 0; i < MAX_MSC_DEVICES; i++) { 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; 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) { static int find_slot_by_handle(UsbMscContext* ctx, msc_host_device_handle_t handle) {
for (int i = 0; i < MAX_MSC_DEVICES; i++) { for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (ctx->devs[i] && ctx->devs[i]->device == handle) return 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) { 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) { if (ctx->devs[slot]->fs_entry) {
ctx->devs[slot]->mounted = false; ctx->devs[slot]->mounted = false;
file_system_remove(ctx->devs[slot]->fs_entry); 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) { static void msc_proc_task(void* arg) {
auto* ctx = static_cast<UsbMscContext*>(arg); auto* ctx = static_cast<UsbMscContext*>(arg);
LOG_I(TAG, "MSC proc task started"); LOG_I(TAG, "MSC proc task started");
@ -215,6 +249,7 @@ static void msc_proc_task(void* arg) {
continue; continue;
} }
ctx->devs[slot]->mounted = true; ctx->devs[slot]->mounted = true;
msc_device_create(ctx, slot);
} else if (msg.id == MscMsgId::Disconnected) { } else if (msg.id == MscMsgId::Disconnected) {
taskENTER_CRITICAL(&ctx->devs_lock); 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); LOG_I(TAG, "USB drive disconnected, unmounting slot %d", slot);
free_msc_device(ctx, slot); free_msc_device(ctx, slot);
} }
} else if (msg.id == MscMsgId::Ejected) {
msc_device_destroy(ctx, msg.slot);
} }
} }
free_all_msc_devices(ctx); free_all_msc_devices(ctx);
LOG_I(TAG, "MSC proc task stopped"); LOG_I(TAG, "MSC proc task stopped");
xSemaphoreGive(ctx->proc_task_done); xSemaphoreGive(ctx->proc_task_done);
vTaskDelete(nullptr); vTaskDeleteWithCaps(nullptr);
} }
static bool api_eject(struct Device* device, const char* mount_path) { static bool api_eject(struct Device* device, const char* mount_path) {
auto* ctx = static_cast<UsbMscContext*>(device_get_driver_data(device)); auto* ctx = static_cast<UsbMscContext*>(device_get_driver_data(device));
if (!ctx) return false; if (!ctx) return false;
const int slot = static_cast<int>(device - ctx->devices);
taskENTER_CRITICAL(&ctx->devs_lock); taskENTER_CRITICAL(&ctx->devs_lock);
msc_dev_entry_t* entry = nullptr; msc_dev_entry_t* entry = nullptr;
int found = -1; int found = -1;
for (int i = 0; i < MAX_MSC_DEVICES; i++) { if (ctx->devs[slot] && strcmp(ctx->devs[slot]->mount_path, mount_path) == 0) {
if (ctx->devs[i] && strcmp(ctx->devs[i]->mount_path, mount_path) == 0) { found = slot;
found = i; entry = ctx->devs[slot];
entry = ctx->devs[i]; ctx->devs[slot] = nullptr; // claim atomically under the lock
ctx->devs[i] = nullptr; // claim atomically under the lock
break;
}
} }
taskEXIT_CRITICAL(&ctx->devs_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); msc_host_uninstall_device(entry->device);
} }
free(entry); 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"); LOG_I(TAG, "USB drive ejected, safe to remove");
return true; return true;
} }
@ -278,8 +320,18 @@ static const UsbMscApi msc_api = {
extern "C" { 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(); auto* ctx = new UsbMscContext();
ctx->host = host;
ctx->event_queue = xQueueCreate(MSC_EVENT_QUEUE_SIZE, sizeof(msc_msg_t)); ctx->event_queue = xQueueCreate(MSC_EVENT_QUEUE_SIZE, sizeof(msc_msg_t));
if (!ctx->event_queue) { if (!ctx->event_queue) {
@ -313,8 +365,8 @@ static error_t start_device(struct Device* device) {
} }
ctx->proc_running = true; ctx->proc_running = true;
BaseType_t result = xTaskCreate(msc_proc_task, "msc_proc", MSC_PROC_TASK_STACK, 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); ctx, MSC_PROC_TASK_PRIORITY, &ctx->proc_task);
if (result != pdPASS) { if (result != pdPASS) {
LOG_E(TAG, "failed to create msc_proc task"); LOG_E(TAG, "failed to create msc_proc task");
ctx->proc_running = false; ctx->proc_running = false;
@ -325,37 +377,46 @@ static error_t start_device(struct Device* device) {
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
device_set_driver_data(device, ctx); *out_context = ctx;
LOG_I(TAG, "started"); LOG_I(TAG, "started");
return ERROR_NONE; return ERROR_NONE;
} }
static error_t stop_device(struct Device* device) { static error_t backend_stop(void* context) {
auto* ctx = static_cast<UsbMscContext*>(device_get_driver_data(device)); auto* ctx = static_cast<UsbMscContext*>(context);
if (!ctx) return ERROR_NONE;
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); bool exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(MSC_STOP_TIMEOUT_MS)) == pdTRUE);
if (!exited) { if (!exited) {
// Retry once with a short extra wait before resorting to force-delete. // Retry once with a short extra wait before resorting to force-delete.
exited = (xSemaphoreTake(ctx->proc_task_done, pdMS_TO_TICKS(500)) == pdTRUE); 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"); // Covers devices of ejected drives and devices whose destruction failed before
vTaskDelete(ctx->proc_task); for (int i = 0; i < MAX_MSC_DEVICES; i++) {
vTaskDelay(pdMS_TO_TICKS(50)); error_t error = msc_device_destroy(ctx, i);
// Task was killed mid-cleanup — free devices ourselves as best-effort. if (error != ERROR_NONE) {
free_all_msc_devices(ctx); return error;
}
} }
ctx->proc_task = nullptr;
vSemaphoreDelete(ctx->proc_task_done);
msc_host_uninstall(); msc_host_uninstall();
if (ctx->event_queue) { vQueueDelete(ctx->event_queue); ctx->event_queue = nullptr; } if (ctx->event_queue) { vQueueDelete(ctx->event_queue); ctx->event_queue = nullptr; }
device_set_driver_data(device, nullptr);
delete ctx; delete ctx;
LOG_I(TAG, "stopped"); LOG_I(TAG, "stopped");
return ERROR_NONE; return ERROR_NONE;
@ -363,9 +424,9 @@ static error_t stop_device(struct Device* device) {
Driver esp32_usbhost_msc_driver = { Driver esp32_usbhost_msc_driver = {
.name = "esp32_usbhost_msc", .name = "esp32_usbhost_msc",
.compatible = (const char*[]) { "espressif,esp32-usbhost-msc", nullptr }, .compatible = (const char*[]) { nullptr },
.start_device = start_device, .start_device = msc_device_start,
.stop_device = stop_device, .stop_device = msc_device_stop,
.api = &msc_api, .api = &msc_api,
.device_type = &USB_HOST_MSC_TYPE, .device_type = &USB_HOST_MSC_TYPE,
.owner = nullptr, .owner = nullptr,
@ -374,4 +435,9 @@ Driver esp32_usbhost_msc_driver = {
} // extern "C" } // extern "C"
const Esp32UsbHostBackend esp32_usbhost_msc_backend = {
.start = backend_start,
.stop = backend_stop,
};
#endif // CONFIG_SOC_USB_OTG_SUPPORTED #endif // CONFIG_SOC_USB_OTG_SUPPORTED

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: Apache-2.0 // 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/ // 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 // stereo PCM output, registers a dynamic "usb_uac0" AUDIO_CODEC_TYPE device
// (see esp32_usbhost_hid.cpp's "usb_keyboard0" for the pattern) so // (see esp32_usbhost_hid.cpp's "usb_keyboard0" for the pattern) so
@ -14,6 +14,9 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/driver.h>
#include <tactility/drivers/audio_codec.h> #include <tactility/drivers/audio_codec.h>
#include <tactility/drivers/esp32_usbhost_backend.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/error_esp32.h> #include <tactility/error_esp32.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -68,8 +71,8 @@ struct UsbUacData {
bool streaming = false; bool streaming = false;
uint8_t volume = 100; uint8_t volume = 100;
bool muted = false; bool muted = false;
// The manager Device, set in manager_start_device; parent of codec_device. // The USB host device, parent of codec_device.
Device* manager_device = nullptr; Device* host = nullptr;
// Dynamic AUDIO_CODEC_TYPE child device, constructed while a UAC output is // Dynamic AUDIO_CODEC_TYPE child device, constructed while a UAC output is
// connected (mirrors esp32_usbhost_hid.cpp's kb_device). // connected (mirrors esp32_usbhost_hid.cpp's kb_device).
Device codec_device {}; Device codec_device {};
@ -79,7 +82,7 @@ struct UsbUacData {
#define GET_DATA(device) (static_cast<UsbUacData*>(device_get_driver_data(device))) #define GET_DATA(device) (static_cast<UsbUacData*>(device_get_driver_data(device)))
void codec_device_construct(UsbUacData* data); 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) { void log_heap(const char* stage) {
LOG_I(TAG, "%s heap: internal_free=%u internal_min=%u external_free=%u", 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 // listeners (audio-stream re-binds output to the board codec) and waits
// for in-flight writes, which are serialized against the handle close // for in-flight writes, which are serialized against the handle close
// below by data->mutex. // below by data->mutex.
codec_device_destruct(data); if (codec_device_destruct(data) != ERROR_NONE) {
return false;
}
xSemaphoreTake(data->mutex, portMAX_DELAY); xSemaphoreTake(data->mutex, portMAX_DELAY);
uac_host_device_handle_t handle = data->handle; uac_host_device_handle_t handle = data->handle;
@ -234,6 +239,7 @@ bool close_output_interface(UsbUacData* data) {
return true; return true;
} }
// Stack may be in PSRAM: no flash access allowed from this task.
void uac_client_task(void* arg) { void uac_client_task(void* arg) {
auto* data = static_cast<UsbUacData*>(arg); auto* data = static_cast<UsbUacData*>(arg);
UacConnectEvent event = {}; UacConnectEvent event = {};
@ -260,13 +266,13 @@ void uac_client_task(void* arg) {
data->task = nullptr; data->task = nullptr;
xSemaphoreGive(data->mutex); xSemaphoreGive(data->mutex);
xSemaphoreGive(data->task_done); xSemaphoreGive(data->task_done);
vTaskDelete(nullptr); vTaskDeleteWithCaps(nullptr);
} }
// region Dynamic codec device (usb_uac0) driver // region Dynamic codec device (usb_uac0) driver
error_t codec_start_device(Device* device) { 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 // interface is already open. Streaming begins lazily via the codec API's
// open(). // open().
return (GET_DATA(device) != nullptr) ? ERROR_NONE : ERROR_RESOURCE; 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 // 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. // client task cannot stall device-tree teardown indefinitely.
if (xSemaphoreTake(data->mutex, UAC_MUTEX_STOP_TIMEOUT) != pdTRUE) { if (xSemaphoreTake(data->mutex, UAC_MUTEX_STOP_TIMEOUT) != pdTRUE) {
LOG_W(TAG, "codec device stop: client task busy, leaving teardown to it"); 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) { if (data->codec_device_active) {
return; 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 = Device { data->codec_device_active = true;
.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;
} }
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) { if (!data->codec_device_active) {
return; return ERROR_NONE;
} }
data->codec_device_active = false; error_t error = esp32_usbhost_device_destroy(data->host, &data->codec_device);
if (error == ERROR_NONE) {
device_stop(&data->codec_device); data->codec_device_active = false;
device_remove(&data->codec_device); }
device_destruct(&data->codec_device); return error;
} }
} // namespace } // namespace
@ -592,7 +572,7 @@ void codec_device_destruct(UsbUacData* data) {
extern "C" { extern "C" {
// Driver for the dynamic "usb_uac0" codec device. Never matched against the // 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 = { Driver usb_uac_codec_driver = {
.name = "usb_uac_codec", .name = "usb_uac_codec",
.compatible = (const char*[]) { nullptr }, .compatible = (const char*[]) { nullptr },
@ -604,14 +584,9 @@ Driver usb_uac_codec_driver = {
.internal = nullptr, .internal = nullptr,
}; };
static error_t manager_start_device(Device* device) { static error_t backend_start(Device* host, void** out_context) {
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;
}
auto* data = new UsbUacData(); auto* data = new UsbUacData();
data->manager_device = device; data->host = host;
data->mutex = xSemaphoreCreateMutex(); data->mutex = xSemaphoreCreateMutex();
data->task_done = xSemaphoreCreateBinary(); data->task_done = xSemaphoreCreateBinary();
data->event_queue = xQueueCreate(UAC_EVENT_QUEUE_LENGTH, sizeof(UacConnectEvent)); 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; 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) { UAC_TASK_PRIORITY, &data->task) != pdPASS) {
LOG_E(TAG, "Failed to create UAC client task"); LOG_E(TAG, "Failed to create UAC client task");
data->running = false; data->running = false;
@ -645,8 +620,8 @@ static error_t manager_start_device(Device* device) {
goto cleanup; goto cleanup;
} }
device_set_driver_data(device, data); *out_context = data;
LOG_I(TAG, "UAC manager started"); LOG_I(TAG, "UAC backend started");
return ERROR_NONE; return ERROR_NONE;
cleanup: cleanup:
@ -663,11 +638,8 @@ cleanup:
return ERROR_RESOURCE; return ERROR_RESOURCE;
} }
static error_t manager_stop_device(Device* device) { static error_t backend_stop(void* context) {
auto* data = GET_DATA(device); auto* data = static_cast<UsbUacData*>(context);
if (data == nullptr) {
return ERROR_NONE;
}
data->running = false; data->running = false;
xSemaphoreTake(data->mutex, portMAX_DELAY); xSemaphoreTake(data->mutex, portMAX_DELAY);
@ -697,23 +669,16 @@ static error_t manager_stop_device(Device* device) {
vQueueDelete(data->event_queue); vQueueDelete(data->event_queue);
vSemaphoreDelete(data->task_done); vSemaphoreDelete(data->task_done);
vSemaphoreDelete(data->mutex); vSemaphoreDelete(data->mutex);
device_set_driver_data(device, nullptr);
delete data; delete data;
LOG_I(TAG, "UAC manager stopped"); LOG_I(TAG, "UAC backend stopped");
return ERROR_NONE; 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" } // extern "C"
const Esp32UsbHostBackend esp32_usbhost_uac_backend = {
.start = backend_start,
.stop = backend_stop,
};
#endif #endif

View File

@ -359,7 +359,6 @@ extern Driver esp32_usbhost_hid_keyboard_driver;
extern Driver esp32_usbhost_midi_driver; extern Driver esp32_usbhost_midi_driver;
extern Driver esp32_usbhost_msc_driver; extern Driver esp32_usbhost_msc_driver;
#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 #if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3
extern Driver esp32_usbhost_uac_driver;
extern Driver usb_uac_codec_driver; extern Driver usb_uac_codec_driver;
#endif #endif
#endif #endif
@ -412,7 +411,6 @@ static Driver* const platform_esp32_drivers[] = {
&esp32_usbhost_midi_driver, &esp32_usbhost_midi_driver,
&esp32_usbhost_msc_driver, &esp32_usbhost_msc_driver,
#if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3 #if CONFIG_IDF_TARGET_ESP32P4 || CONFIG_IDF_TARGET_ESP32S3
&esp32_usbhost_uac_driver,
&usb_uac_codec_driver, &usb_uac_codec_driver,
#endif #endif
#endif #endif

View File

@ -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

View File

@ -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

View File

@ -21,6 +21,7 @@
#include <Tactility/TactilityPrivate.h> #include <Tactility/TactilityPrivate.h>
#include <Tactility/app/boot/BootScreen.h> #include <Tactility/app/boot/BootScreen.h>
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
#include <Tactility/hal/usb/UsbHost.h>
#include <Tactility/lvgl/Fonts.h> #include <Tactility/lvgl/Fonts.h>
#include <Tactility/lvgl/Lvgl.h> #include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/Theme.h> #include <Tactility/lvgl/Theme.h>
@ -231,6 +232,9 @@ bool bootInit(TickType_t startTime) {
LOG_I(TAG, "Prepare file systems"); LOG_I(TAG, "Prepare file systems");
prepareFileSystems(); prepareFileSystems();
LOG_I(TAG, "Init USB host");
hal::usbhost::systemStart();
bool sd_card_missing = false; bool sd_card_missing = false;
#ifdef CONFIG_TT_USER_DATA_LOCATION_SD #ifdef CONFIG_TT_USER_DATA_LOCATION_SD
std::string sd_path; std::string sd_path;

View File

@ -34,6 +34,7 @@
#include <fcntl.h> #include <fcntl.h>
#include <format> #include <format>
#include <unistd.h> #include <unistd.h>
#include <vector>
namespace tt::app::files { namespace tt::app::files {
@ -585,14 +586,26 @@ void View::onEjectPressed() {
std::string mount_path = state->getSelectedChildPath(); std::string mount_path = state->getSelectedChildPath();
LOG_I(TAG, "Ejecting %s", mount_path.c_str()); LOG_I(TAG, "Ejecting %s", mount_path.c_str());
Device* msc_dev = nullptr; // One device per mounted drive. Ejecting happens outside the iteration, which holds the device ledger lock.
if (device_get_first_active_by_type(&USB_HOST_MSC_TYPE, &msc_dev) != ERROR_NONE || !usb_msc_eject(msc_dev, mount_path.c_str())) { std::vector<Device*> msc_devices;
LOG_W(TAG, "usb_msc_eject: %s not found", mount_path.c_str()); device_for_each_of_type(&USB_HOST_MSC_TYPE, &msc_devices, [](Device* device, void* context) -> bool {
alertdialog::start(appInstanceId, "Eject failed", "Could not eject \"" + file::getLastPathSegment(mount_path) + "\"."); if (device_is_ready(device) && device_get(device) == ERROR_NONE) {
static_cast<std::vector<Device*>*>(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) { if (!ejected) {
device_put(msc_dev); 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(); onNavigate();

View File

@ -1,4 +1,5 @@
#include <Tactility/hal/usb/Usb.h> #include <Tactility/hal/usb/Usb.h>
#include <Tactility/hal/usb/UsbHost.h>
#include <app/event.h> #include <app/event.h>
#include <app/manager.h> #include <app/manager.h>
@ -10,8 +11,12 @@
#include <tactility/check.h> #include <tactility/check.h>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <atomic>
#define TAG "usb_settings" #define TAG "usb_settings"
namespace tt::app::usbsettings { namespace tt::app::usbsettings {
@ -20,8 +25,20 @@ extern const ::AppManifest manifest;
namespace { namespace {
enum class Request { None, Disable, Enable };
struct Context { struct Context {
uint32_t appInstanceId; 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<Request> 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(); hal::usb::rebootIntoMassStorageFlash();
} }
void onHostSwitch(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(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<Context*>(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) { void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData); auto* ctx = static_cast<Context*>(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"); auto* toolbar = lvgl_toolbar_create(parent, "USB");
// The global toolbar nav callback only knows how to stop old-model apps. // The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); 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* main_wrapper = lv_obj_create(parent);
auto* wrapper = lv_obj_create(parent); lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(main_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_width(main_wrapper, LV_PCT(100));
lv_obj_set_size(wrapper, lv_pct(100), LV_SIZE_CONTENT); lv_obj_set_flex_grow(main_wrapper, 1);
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);
bool hasSd = hal::usb::canRebootIntoMassStorageSdmmc(); const bool hasSd = hal::usb::canRebootIntoMassStorageSdmmc();
bool hasFlash = hal::usb::canRebootIntoMassStorageFlash(); const bool hasFlash = hal::usb::canRebootIntoMassStorageFlash();
const bool hasHost = hal::usbhost::isAvailable();
// The storage is only named when there is a choice
if (hasSd) { if (hasSd) {
auto* button_sd = lv_button_create(wrapper); createRebootRow(main_wrapper, hasFlash ? "USB drive mode (SD card)" : "USB drive mode", onRebootMassStorageSdmmc);
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);
} }
if (hasFlash) { if (hasFlash) {
auto* button_flash = lv_button_create(wrapper); createRebootRow(main_wrapper, hasSd ? "USB drive mode (internal)" : "USB drive mode", onRebootMassStorageFlash);
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);
} }
if (!hasSd && !hasFlash) { if (hasHost) {
createUsbHostRow(main_wrapper, ctx);
}
if (!hasSd && !hasFlash && !hasHost) {
bool supported = hal::usb::isSupported(); bool supported = hal::usb::isSupported();
const char* message = supported ? "USB storage not available" : "USB driver not supported"; 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); lv_label_set_text(label, message);
} }
} }
@ -89,16 +154,33 @@ int32_t appMain(int argc, char* argv[]) {
TaskEventGroup event_group {}; TaskEventGroup event_group {};
task_event_group_construct(&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 {}; AppEventSubscription sub {};
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); 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); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
bool shouldClose = false; bool shouldClose = false;
while (!shouldClose) { while (!shouldClose) {
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); 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 {}; AppEvent event {};
while (app_event_poll(&sub, &event) == ERROR_NONE) { while (app_event_poll(&sub, &event) == ERROR_NONE) {
switch (event.type) { switch (event.type) {
@ -114,6 +196,7 @@ int32_t appMain(int argc, char* argv[]) {
window_manager_remove(window); window_manager_remove(window);
check(app_event_unsubscribe(&sub) == ERROR_NONE); check(app_event_unsubscribe(&sub) == ERROR_NONE);
task_event_group_release_bit(&event_group, ctx.requestBit);
task_event_group_destruct(&event_group); task_event_group_destruct(&event_group);
return 0; return 0;

View File

@ -0,0 +1,69 @@
#include <Tactility/hal/usb/UsbHost.h>
#include <Tactility/settings/UsbHostSettings.h>
#include <tactility/device.h>
#include <tactility/drivers/usb_host.h>
#include <tactility/log.h>
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

View File

@ -3,9 +3,11 @@
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/audio/Audio.h> #include <Tactility/service/audio/Audio.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/drivers/hid_consumer.h> #include <tactility/drivers/hid_consumer.h>
#include <tactility/drivers/usb_host_hid.h> #include <tactility/drivers/usb_host_hid.h>
#include <tactility/log.h> #include <tactility/log.h>
@ -51,7 +53,6 @@ struct UsbHidInputCtx {
StackType_t* task_stack = nullptr; StackType_t* task_stack = nullptr;
StaticTask_t* task_tcb = nullptr; StaticTask_t* task_tcb = nullptr;
std::atomic<bool> running{false}; std::atomic<bool> running{false};
std::atomic<bool> subscribed{false};
lv_indev_t* mouse_indev = nullptr; lv_indev_t* mouse_indev = nullptr;
lv_indev_t* kb_indev = nullptr; lv_indev_t* kb_indev = nullptr;
@ -71,6 +72,67 @@ struct UsbHidInputCtx {
static UsbHidInputCtx* s_ctx = nullptr; 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. // 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 // 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. // debug level and dropped. Runs on this file's HID pump task.
@ -230,13 +292,6 @@ static void usbHidInputTask(void* arg) {
while (ctx->running) { while (ctx->running) {
UsbHidEvent hid_evt; UsbHidEvent hid_evt;
if (xQueueReceive(ctx->hid_queue, &hid_evt, pdMS_TO_TICKS(100)) != pdTRUE) { 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; continue;
} }
@ -391,11 +446,7 @@ void startUsbHidInput() {
} }
lvgl_unlock(); lvgl_unlock();
Device* hid_dev = nullptr; startHidListener(ctx->hid_queue);
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);
}
ctx->running = true; ctx->running = true;
@ -417,13 +468,7 @@ void startUsbHidInput() {
if (ctx->task == nullptr) { if (ctx->task == nullptr) {
LOG_E(TAG, "failed to create task"); LOG_E(TAG, "failed to create task");
ctx->running = false; ctx->running = false;
if (ctx->subscribed) { stopHidListener(ctx->hid_queue);
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);
}
}
memory_free(ctx->task_stack); memory_free(ctx->task_stack);
memory_free(ctx->task_tcb); memory_free(ctx->task_tcb);
if (ctx->mouse_cursor != nullptr) { if (ctx->mouse_cursor != nullptr) {
@ -483,13 +528,7 @@ void stopUsbHidInput() {
memory_free(ctx->task_stack); memory_free(ctx->task_stack);
memory_free(ctx->task_tcb); memory_free(ctx->task_tcb);
if (ctx->subscribed) { stopHidListener(ctx->hid_queue);
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);
}
}
vQueueDelete(ctx->hid_queue); vQueueDelete(ctx->hid_queue);
vQueueDelete(ctx->key_queue); vQueueDelete(ctx->key_queue);
vSemaphoreDelete(ctx->task_done); vSemaphoreDelete(ctx->task_done);

View File

@ -255,8 +255,8 @@ class StatusbarService final : public Service {
Device* midi_dev = nullptr; Device* midi_dev = nullptr;
device_get_first_active_by_type(&USB_HOST_MIDI_TYPE, &midi_dev); device_get_first_active_by_type(&USB_HOST_MIDI_TYPE, &midi_dev);
bool connected = (hid_dev && usb_host_hid_is_connected(hid_dev)) || // These devices only exist while connected
(midi_dev && usb_midi_is_connected(midi_dev)); bool connected = hid_dev != nullptr || midi_dev != nullptr;
if (hid_dev) { if (hid_dev) {
device_put(hid_dev); device_put(hid_dev);

View File

@ -0,0 +1,73 @@
#include <Tactility/settings/UsbHostSettings.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <tactility/paths.h>
#include <map>
#include <string>
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<std::string, std::string>& 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<std::string, std::string> 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<std::string, std::string> 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

View File

@ -0,0 +1,66 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <tactility/error.h>
#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

View File

@ -68,19 +68,19 @@ typedef struct {
} UsbHidEvent; } UsbHidEvent;
struct UsbHidApi { struct UsbHidApi {
bool (*is_connected)(struct Device* device);
bool (*subscribe)(struct Device* device, UsbHidQueueHandle event_queue); bool (*subscribe)(struct Device* device, UsbHidQueueHandle event_queue);
void (*unsubscribe)(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; 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. * 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. * 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); bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue);

View File

@ -4,6 +4,9 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/error.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -25,28 +28,87 @@ typedef struct {
uint8_t data2; /**< Second data byte */ uint8_t data2; /**< Second data byte */
} usb_midi_message_t; } usb_midi_message_t;
typedef void (*usb_midi_message_cb_t)(const usb_midi_message_t* msg, void* user_data); /**
* A USB_HOST_MIDI_TYPE device exists while a USB MIDI interface is connected.
struct UsbMidiApi { */
void (*set_callback)(struct Device* device, usb_midi_message_cb_t callback, void* user_data);
bool (*is_connected)(struct Device* device);
};
extern const struct DeviceType USB_HOST_MIDI_TYPE; extern const struct DeviceType USB_HOST_MIDI_TYPE;
/** struct UsbMidiEvent {
* Register a callback for incoming MIDI messages. /** Stamped by usb_midi_event_emit(); any value passed in by the caller is ignored. */
* Replaces any previously registered callback. Pass NULL to disable. uint64_t timestamp;
* @param device non-null ready USB MIDI device. usb_midi_message_t message;
*/ };
// 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);
/** /**
* Returns true if a MIDI device is currently connected and streaming. * Number of events that can be queued per subscription before usb_midi_event_emit() starts
* @param device non-null ready USB MIDI device. * 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 #ifdef __cplusplus
} }

View File

@ -16,13 +16,16 @@ struct UsbMscApi {
bool (*eject)(struct Device* device, const char* mount_path); 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; extern const struct DeviceType USB_HOST_MSC_TYPE;
/** /**
* Safely eject a mounted USB drive. * Safely eject a mounted USB drive. The device is removed shortly after.
* @param device non-null ready USB MSC device (from device_find_first_active_by_type). * @param device non-null ready USB MSC device
* @param mount_path Full mount path (e.g. "/usb0"). * @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); bool usb_msc_eject(struct Device* device, const char* mount_path);

View File

@ -0,0 +1,25 @@
#include <tactility/drivers/usb_host.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#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"

View File

@ -10,11 +10,6 @@ const struct DeviceType USB_HOST_HID_TYPE = {
.name = "usb-host-hid", .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) { bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue) {
auto* api = USB_HID_API(device_get_driver(device)); auto* api = USB_HID_API(device_get_driver(device));
return api->subscribe(device, event_queue); return api->subscribe(device, event_queue);

View File

@ -1,8 +1,16 @@
#include <tactility/drivers/usb_host_midi.h> #include <tactility/drivers/usb_host_midi.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/driver.h> #include <tactility/time.h>
#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" { extern "C" {
@ -10,14 +18,97 @@ const struct DeviceType USB_HOST_MIDI_TYPE = {
.name = "usb-host-midi", .name = "usb-host-midi",
}; };
void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) { error_t usb_midi_event_subscribe(UsbMidiEventSubscription* sub, TaskEventGroup* event_group, Device* device) {
auto* api = USB_MIDI_API(device_get_driver(device)); uint32_t bit;
api->set_callback(device, callback, user_data); 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) { error_t usb_midi_event_unsubscribe(UsbMidiEventSubscription* sub) {
auto* api = USB_MIDI_API(device_get_driver(device)); error_t result = ERROR_NOT_FOUND;
return api->is_connected(device);
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" } // extern "C"

View File

@ -39,6 +39,7 @@
#include <tactility/drivers/uart_controller.h> #include <tactility/drivers/uart_controller.h>
#include <tactility/drivers/usb_device_controller.h> #include <tactility/drivers/usb_device_controller.h>
#include <tactility/drivers/usb_hid_device.h> #include <tactility/drivers/usb_hid_device.h>
#include <tactility/drivers/usb_host.h>
#include <tactility/drivers/usb_host_hid.h> #include <tactility/drivers/usb_host_hid.h>
#include <tactility/drivers/usb_host_midi.h> #include <tactility/drivers/usb_host_midi.h>
#include <tactility/drivers/usb_host_msc.h> #include <tactility/drivers/usb_host_msc.h>
@ -449,14 +450,20 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(lora_tx_event_unsubscribe), DEFINE_MODULE_SYMBOL(lora_tx_event_unsubscribe),
DEFINE_MODULE_SYMBOL(lora_tx_event_poll), DEFINE_MODULE_SYMBOL(lora_tx_event_poll),
DEFINE_MODULE_SYMBOL(LORA_TYPE), 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 // 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_subscribe),
DEFINE_MODULE_SYMBOL(usb_host_hid_unsubscribe), DEFINE_MODULE_SYMBOL(usb_host_hid_unsubscribe),
DEFINE_MODULE_SYMBOL(USB_HOST_HID_TYPE), DEFINE_MODULE_SYMBOL(USB_HOST_HID_TYPE),
// drivers/usb_host_midi // drivers/usb_host_midi
DEFINE_MODULE_SYMBOL(usb_midi_set_callback), DEFINE_MODULE_SYMBOL(usb_midi_event_subscribe),
DEFINE_MODULE_SYMBOL(usb_midi_is_connected), 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), DEFINE_MODULE_SYMBOL(USB_HOST_MIDI_TYPE),
// drivers/usb_host_msc // drivers/usb_host_msc
DEFINE_MODULE_SYMBOL(usb_msc_eject), DEFINE_MODULE_SYMBOL(usb_msc_eject),

View File

@ -0,0 +1,97 @@
#include "doctest.h"
#include <tactility/concurrent/task_event_group.h>
#include <tactility/device.h>
#include <tactility/drivers/usb_host_midi.h>
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<uint8_t>(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);
}