mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
USB host refactored to use async await/poll mechanisms
+ more task stacks with SPIRAM
This commit is contained in:
parent
321ced744f
commit
db66cb5f3b
@ -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
|
||||
|
||||
@ -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);
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
97
TactilityKernel/tests/source/usb_host_midi_event_test.cpp
Normal file
97
TactilityKernel/tests/source/usb_host_midi_event_test.cpp
Normal 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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user