USB host refactored to use async await/poll mechanisms

+ more task stacks with SPIRAM
This commit is contained in:
Ken Van Hoeylandt 2026-10-10 01:29:11 +02:00
parent 321ced744f
commit db66cb5f3b
10 changed files with 440 additions and 51 deletions

View File

@ -7,6 +7,7 @@
## Higher Priority
- Improve posix support: use user home path and system /tmp folder. implement uart/i2c drivers properly.
- Improve Setup: Add keyboard/keypad/encoder navigation explanation.
e.g. encoder long press on t-lora pager to get out of textarea
- NimBLE looses pairing key after reboot

View File

@ -0,0 +1,16 @@
#pragma once
#include <tactility/error.h>
struct Device;
/**
* 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.
* @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 worker is not running
*/
error_t esp32_usbhost_run_on_worker(struct Device* host, void (*function)(void* context), void* context);

View File

@ -5,10 +5,12 @@
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost.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 <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <freertos/semphr.h>
#include <usb/usb_host.h>
@ -34,6 +36,9 @@ constexpr auto USB_LIB_TASK_PRIORITY = 10;
constexpr auto USB_LIB_EVENT_TIMEOUT_MS = 500;
constexpr auto USB_HOST_STOP_TIMEOUT_MS = 3000;
constexpr auto USB_HOST_STOP_RETRY_MS = 1000;
constexpr auto USB_WORKER_TASK_STACK = 4096;
constexpr auto USB_WORKER_TASK_PRIORITY = 5;
constexpr auto USB_WORKER_QUEUE_SIZE = 4;
constexpr auto USB_HOST_CLASS_COUNT = USB_HOST_CLASS_AUDIO + 1;
constexpr auto CHILD_DESTRUCT_RETRIES = 100;
@ -42,7 +47,17 @@ struct UsbHostChild {
bool active = false;
};
struct UsbHostWorkerJob {
// nullptr stops the worker
void (*function)(void* context);
void* context;
StaticSemaphore_t done_buffer;
SemaphoreHandle_t done;
};
struct UsbHostContext {
TaskHandle_t worker_task = nullptr;
QueueHandle_t worker_queue = nullptr;
TaskHandle_t lib_task = nullptr;
SemaphoreHandle_t lib_task_done = nullptr;
// Indexed by UsbClass
@ -79,6 +94,73 @@ static void usbLibTask(void* arg) {
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.
static void usbWorkerTask(void* arg) {
auto* ctx = static_cast<UsbHostContext*>(arg);
while (true) {
UsbHostWorkerJob* job = nullptr;
if (xQueueReceive(ctx->worker_queue, &job, portMAX_DELAY) != pdTRUE) {
continue;
}
if (job->function == nullptr) {
xSemaphoreGive(job->done);
break;
}
job->function(job->context);
xSemaphoreGive(job->done);
}
vTaskDelete(nullptr);
}
static void submit_job_and_wait(UsbHostContext* ctx, UsbHostWorkerJob* job) {
job->done = xSemaphoreCreateBinaryStatic(&job->done_buffer);
xQueueSend(ctx->worker_queue, &job, portMAX_DELAY);
xSemaphoreTake(job->done, portMAX_DELAY);
vSemaphoreDelete(job->done);
}
static error_t start_worker(UsbHostContext* ctx) {
ctx->worker_queue = xQueueCreate(USB_WORKER_QUEUE_SIZE, sizeof(UsbHostWorkerJob*));
if (ctx->worker_queue == nullptr) {
LOG_E(TAG, "failed to create worker queue");
return ERROR_RESOURCE;
}
if (xTaskCreate(usbWorkerTask, "usb_worker", USB_WORKER_TASK_STACK, ctx, USB_WORKER_TASK_PRIORITY, &ctx->worker_task) != pdPASS) {
LOG_E(TAG, "failed to create worker task");
vQueueDelete(ctx->worker_queue);
ctx->worker_queue = nullptr;
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static void stop_worker(UsbHostContext* ctx) {
UsbHostWorkerJob job = { .function = nullptr, .context = nullptr, .done_buffer = {}, .done = nullptr };
submit_job_and_wait(ctx, &job);
ctx->worker_task = nullptr;
vQueueDelete(ctx->worker_queue);
ctx->worker_queue = 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;
}
if (xTaskGetCurrentTaskHandle() == ctx->worker_task) {
function(context);
return ERROR_NONE;
}
UsbHostWorkerJob job = { .function = function, .context = context, .done_buffer = {}, .done = nullptr };
submit_job_and_wait(ctx, &job);
return ERROR_NONE;
}
// endregion
// region Children
struct UsbHostChildSpec {
@ -260,7 +342,12 @@ static error_t start_device(struct Device* device) {
}
auto* ctx = new UsbHostContext();
if (start_worker(ctx) != ERROR_NONE) {
delete ctx;
return ERROR_RESOURCE;
}
if (install_lib(ctx, cfg) != ERROR_NONE) {
stop_worker(ctx);
delete ctx;
return ERROR_RESOURCE;
}
@ -286,6 +373,7 @@ static error_t stop_device(struct Device* device) {
}
error_t result = uninstall_lib(ctx);
stop_worker(ctx);
device_set_driver_data(device, nullptr);
delete ctx;
LOG_I(TAG, "stopped");

View File

@ -3,6 +3,8 @@
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/drivers/hid_consumer.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/usb_host_hid.h>
@ -299,6 +301,7 @@ static void hid_driver_callback(hid_host_device_handle_t handle,
}
}
// Stack may be in PSRAM: no flash access allowed from this task.
static void hid_proc_task(void* arg) {
auto* ctx = static_cast<UsbHidContext*>(arg);
LOG_I(TAG, "HID proc task started");
@ -389,7 +392,7 @@ static void hid_proc_task(void* arg) {
LOG_I(TAG, "HID proc task stopped");
xSemaphoreGive(ctx->hid_proc_task_done);
vTaskDelete(nullptr);
vTaskDeleteWithCaps(nullptr);
}
static bool api_hid_is_connected(struct Device* device) {
@ -514,7 +517,8 @@ Driver esp32_usbhost_hid_keyboard_driver = {
.internal = nullptr,
};
static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
static void usb_hid_keyboard_device_construct_on_worker(void* context) {
auto* ctx = static_cast<UsbHidContext*>(context);
if (ctx->kb_device_active) {
return;
}
@ -549,7 +553,8 @@ static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
ctx->kb_device_active = true;
}
static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx) {
static void usb_hid_keyboard_device_destruct_on_worker(void* context) {
auto* ctx = static_cast<UsbHidContext*>(context);
if (!ctx->kb_device_active) {
return;
}
@ -560,6 +565,18 @@ static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx) {
device_destruct(&ctx->kb_device);
}
static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
if (esp32_usbhost_run_on_worker(device_get_parent(ctx->controller_device), usb_hid_keyboard_device_construct_on_worker, ctx) != ERROR_NONE) {
LOG_E(TAG, "failed to construct USB keyboard device: worker unavailable");
}
}
static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx) {
if (esp32_usbhost_run_on_worker(device_get_parent(ctx->controller_device), usb_hid_keyboard_device_destruct_on_worker, ctx) != ERROR_NONE) {
LOG_E(TAG, "failed to destruct USB keyboard device: worker unavailable");
}
}
static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bool pressed, bool ctrl, bool alt, uint8_t hid_keycode, uint8_t hid_modifier) {
if (!ctx->kb_device_active) {
return;
@ -617,8 +634,8 @@ static error_t start_device(struct Device* device) {
}
ctx->hid_proc_running = true;
BaseType_t result = xTaskCreate(hid_proc_task, "hid_proc", HID_PROC_TASK_STACK,
ctx, HID_PROC_TASK_PRIORITY, &ctx->hid_proc_task);
BaseType_t result = esp32_usbhost_task_create_psram(hid_proc_task, "hid_proc", HID_PROC_TASK_STACK,
ctx, HID_PROC_TASK_PRIORITY, &ctx->hid_proc_task);
if (result != pdPASS) {
LOG_E(TAG, "failed to create hid_proc task");
ctx->hid_proc_running = false;
@ -655,7 +672,7 @@ static error_t stop_device(struct Device* device) {
if (xSemaphoreTake(ctx->hid_proc_task_done, pdMS_TO_TICKS(HID_STOP_TIMEOUT_MS)) != pdTRUE) {
LOG_W(TAG, "HID proc task stop timed out, force terminating");
vTaskDelete(ctx->hid_proc_task);
vTaskDeleteWithCaps(ctx->hid_proc_task);
}
ctx->hid_proc_task = nullptr;
vSemaphoreDelete(ctx->hid_proc_task_done);

View File

@ -3,6 +3,7 @@
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/log.h>
@ -29,6 +30,7 @@ constexpr uint8_t MIDI_INTF_CLASS = 0x01;
constexpr uint8_t MIDI_INTF_SUBCLASS = 0x03;
struct UsbMidiContext {
Device* device = nullptr;
usb_host_client_handle_t client_hdl = nullptr;
usb_device_handle_t dev_hdl = nullptr;
usb_transfer_t* transfer = nullptr;
@ -38,10 +40,6 @@ struct UsbMidiContext {
std::atomic<bool> running{false};
TaskHandle_t task_handle = nullptr;
SemaphoreHandle_t task_done = nullptr;
portMUX_TYPE callback_lock = portMUX_INITIALIZER_UNLOCKED;
usb_midi_message_cb_t callback = nullptr;
void* callback_arg = nullptr;
};
static bool find_midi_interface(const usb_config_desc_t* cfg, uint8_t* out_intf, uint8_t* out_ep) {
@ -72,14 +70,6 @@ static bool find_midi_interface(const usb_config_desc_t* cfg, uint8_t* out_intf,
}
static void dispatch_midi_packets(UsbMidiContext* ctx, const uint8_t* buf, int len) {
usb_midi_message_cb_t cb;
void* arg;
taskENTER_CRITICAL(&ctx->callback_lock);
cb = ctx->callback;
arg = ctx->callback_arg;
taskEXIT_CRITICAL(&ctx->callback_lock);
if (!cb) return;
for (int i = 0; i + 3 < len; i += 4) {
uint8_t cin = buf[i] & 0x0F;
if (cin < 0x02) continue;
@ -89,7 +79,7 @@ static void dispatch_midi_packets(UsbMidiContext* ctx, const uint8_t* buf, int l
.data1 = buf[i + 2],
.data2 = buf[i + 3],
};
cb(&msg, arg);
usb_midi_event_emit(ctx->device, &msg);
}
}
@ -169,6 +159,7 @@ static void client_event_cb(const usb_host_client_event_msg_t* msg, void* arg) {
}
}
// Stack may be in PSRAM: no flash access allowed from this task.
static void midi_client_task(void* arg) {
auto* ctx = static_cast<UsbMidiContext*>(arg);
LOG_I(TAG, "MIDI client task started");
@ -186,16 +177,7 @@ static void midi_client_task(void* arg) {
LOG_I(TAG, "MIDI client task stopped");
xSemaphoreGive(ctx->task_done);
vTaskDelete(nullptr);
}
static void api_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) {
auto* ctx = static_cast<UsbMidiContext*>(device_get_driver_data(device));
if (!ctx) return;
taskENTER_CRITICAL(&ctx->callback_lock);
ctx->callback = callback;
ctx->callback_arg = user_data;
taskEXIT_CRITICAL(&ctx->callback_lock);
vTaskDeleteWithCaps(nullptr);
}
static bool api_is_connected(struct Device* device) {
@ -204,7 +186,6 @@ static bool api_is_connected(struct Device* device) {
}
static const UsbMidiApi midi_api = {
.set_callback = api_set_callback,
.is_connected = api_is_connected,
};
@ -212,6 +193,7 @@ extern "C" {
static error_t start_device(struct Device* device) {
auto* ctx = new UsbMidiContext();
ctx->device = device;
if (usb_host_transfer_alloc(MIDI_TRANSFER_BUF_SIZE, 0, &ctx->transfer) != ESP_OK) {
LOG_E(TAG, "failed to allocate MIDI transfer");
@ -245,8 +227,8 @@ static error_t start_device(struct Device* device) {
}
ctx->running = true;
BaseType_t result = xTaskCreate(midi_client_task, "midi_client", MIDI_TASK_STACK,
ctx, MIDI_TASK_PRIORITY, &ctx->task_handle);
BaseType_t result = esp32_usbhost_task_create_psram(midi_client_task, "midi_client", MIDI_TASK_STACK,
ctx, MIDI_TASK_PRIORITY, &ctx->task_handle);
if (result != pdPASS) {
LOG_E(TAG, "failed to create midi_client task");
ctx->running = false;
@ -281,7 +263,7 @@ static error_t stop_device(struct Device* device) {
}
ctx->dev_hdl = nullptr;
}
vTaskDelete(ctx->task_handle);
vTaskDeleteWithCaps(ctx->task_handle);
vTaskDelay(pdMS_TO_TICKS(50));
}
ctx->task_handle = nullptr;

View File

@ -14,6 +14,8 @@
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/audio_codec.h>
#include <tactility/drivers/esp32_usbhost_task.h>
#include <tactility/drivers/esp32_usbhost_worker.h>
#include <tactility/error_esp32.h>
#include <tactility/log.h>
@ -234,6 +236,7 @@ bool close_output_interface(UsbUacData* data) {
return true;
}
// Stack may be in PSRAM: no flash access allowed from this task.
void uac_client_task(void* arg) {
auto* data = static_cast<UsbUacData*>(arg);
UacConnectEvent event = {};
@ -260,7 +263,7 @@ void uac_client_task(void* arg) {
data->task = nullptr;
xSemaphoreGive(data->mutex);
xSemaphoreGive(data->task_done);
vTaskDelete(nullptr);
vTaskDeleteWithCaps(nullptr);
}
// region Dynamic codec device (usb_uac0) driver
@ -541,7 +544,8 @@ const AudioCodecApi codec_api = {
// endregion
void codec_device_construct(UsbUacData* data) {
void codec_device_construct_on_worker(void* context) {
auto* data = static_cast<UsbUacData*>(context);
if (data->codec_device_active) {
return;
}
@ -576,7 +580,8 @@ void codec_device_construct(UsbUacData* data) {
data->codec_device_active = true;
}
void codec_device_destruct(UsbUacData* data) {
void codec_device_destruct_on_worker(void* context) {
auto* data = static_cast<UsbUacData*>(context);
if (!data->codec_device_active) {
return;
}
@ -587,6 +592,18 @@ void codec_device_destruct(UsbUacData* data) {
device_destruct(&data->codec_device);
}
void codec_device_construct(UsbUacData* data) {
if (esp32_usbhost_run_on_worker(device_get_parent(data->manager_device), codec_device_construct_on_worker, data) != ERROR_NONE) {
LOG_E(TAG, "failed to construct USB UAC codec device: worker unavailable");
}
}
void codec_device_destruct(UsbUacData* data) {
if (esp32_usbhost_run_on_worker(device_get_parent(data->manager_device), codec_device_destruct_on_worker, data) != ERROR_NONE) {
LOG_E(TAG, "failed to destruct USB UAC codec device: worker unavailable");
}
}
} // namespace
extern "C" {
@ -632,7 +649,7 @@ static error_t manager_start_device(Device* device) {
}
data->running = true;
if (xTaskCreate(uac_client_task, "usb_uac_client", UAC_TASK_STACK, data,
if (esp32_usbhost_task_create_psram(uac_client_task, "usb_uac_client", UAC_TASK_STACK, data,
UAC_TASK_PRIORITY, &data->task) != pdPASS) {
LOG_E(TAG, "Failed to create UAC client task");
data->running = false;

View File

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

View File

@ -1,23 +1,122 @@
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/concurrent/mutex.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" {
const struct DeviceType USB_HOST_MIDI_TYPE = {
.name = "usb-host-midi",
};
void usb_midi_set_callback(struct Device* device, usb_midi_message_cb_t callback, void* user_data) {
auto* api = USB_MIDI_API(device_get_driver(device));
api->set_callback(device, callback, user_data);
}
bool usb_midi_is_connected(struct Device* device) {
auto* api = USB_MIDI_API(device_get_driver(device));
return api->is_connected(device);
}
error_t usb_midi_event_subscribe(UsbMidiEventSubscription* sub, TaskEventGroup* event_group, Device* device) {
uint32_t bit;
error_t claim_result = task_event_group_claim_bit(event_group, &bit);
if (claim_result != ERROR_NONE) {
return claim_result;
}
mutex_lock(&usb_midi_event_subscriptions_mutex.handle);
// Avoid cyclic subscription list that would loop forever
for (UsbMidiEventSubscription* existing = usb_midi_event_subscriptions; existing != nullptr; existing = existing->internal.next) {
if (existing == sub) {
mutex_unlock(&usb_midi_event_subscriptions_mutex.handle);
task_event_group_release_bit(event_group, bit);
return ERROR_INVALID_STATE;
}
}
sub->bit = bit;
sub->internal.device = device;
sub->internal.event_group = event_group;
sub->internal.head = 0;
sub->internal.count = 0;
sub->internal.next = usb_midi_event_subscriptions;
usb_midi_event_subscriptions = sub;
mutex_unlock(&usb_midi_event_subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t usb_midi_event_unsubscribe(UsbMidiEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&usb_midi_event_subscriptions_mutex.handle);
for (UsbMidiEventSubscription** link = &usb_midi_event_subscriptions; *link != nullptr; link = &(*link)->internal.next) {
if (*link == sub) {
*link = sub->internal.next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&usb_midi_event_subscriptions_mutex.handle);
if (result == ERROR_NONE) {
task_event_group_release_bit(sub->internal.event_group, sub->bit);
}
return result;
}
error_t usb_midi_event_emit(Device* device, const usb_midi_message_t* message) {
UsbMidiEvent stamped_event {};
stamped_event.timestamp = get_micros_since_boot();
stamped_event.message = *message;
error_t result = ERROR_NOT_FOUND;
mutex_lock(&usb_midi_event_subscriptions_mutex.handle);
for (UsbMidiEventSubscription* sub = usb_midi_event_subscriptions; sub != nullptr; sub = sub->internal.next) {
if (sub->internal.device != device) {
continue;
}
if (sub->internal.count >= USB_MIDI_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->internal.head + sub->internal.count) % USB_MIDI_EVENT_QUEUE_CAPACITY;
sub->internal.queue[tail] = stamped_event;
sub->internal.count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
task_event_group_signal(sub->internal.event_group, sub->bit);
}
mutex_unlock(&usb_midi_event_subscriptions_mutex.handle);
return result;
}
error_t usb_midi_event_poll(UsbMidiEventSubscription* sub, UsbMidiEvent* out_event) {
mutex_lock(&usb_midi_event_subscriptions_mutex.handle);
bool has_event = sub->internal.count > 0;
if (has_event) {
*out_event = sub->internal.queue[sub->internal.head];
sub->internal.head = (sub->internal.head + 1) % USB_MIDI_EVENT_QUEUE_CAPACITY;
sub->internal.count--;
}
mutex_unlock(&usb_midi_event_subscriptions_mutex.handle);
return has_event ? ERROR_NONE : ERROR_TIMEOUT;
}
} // extern "C"

View File

@ -459,7 +459,10 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(usb_host_hid_unsubscribe),
DEFINE_MODULE_SYMBOL(USB_HOST_HID_TYPE),
// drivers/usb_host_midi
DEFINE_MODULE_SYMBOL(usb_midi_set_callback),
DEFINE_MODULE_SYMBOL(usb_midi_event_subscribe),
DEFINE_MODULE_SYMBOL(usb_midi_event_unsubscribe),
DEFINE_MODULE_SYMBOL(usb_midi_event_poll),
DEFINE_MODULE_SYMBOL(usb_midi_event_emit),
DEFINE_MODULE_SYMBOL(usb_midi_is_connected),
DEFINE_MODULE_SYMBOL(USB_HOST_MIDI_TYPE),
// drivers/usb_host_msc

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);
}