diff --git a/Documentation/ideas.md b/Documentation/ideas.md index fdd8aa643..15f7505a9 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -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 diff --git a/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h new file mode 100644 index 000000000..7840d4a8e --- /dev/null +++ b/Platforms/platform-esp32/private/tactility/drivers/esp32_usbhost_worker.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +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); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp index 0cfcc1295..79b5f4d62 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost.cpp @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -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(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(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"); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp index fb4c3c580..c0e5a593e 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_hid.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -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(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(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(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); diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp index 5a6ea4cf5..9c5ef8ed6 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_midi.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -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 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(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(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; diff --git a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp index 06e95f286..9cac4c3d1 100644 --- a/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp +++ b/Platforms/platform-esp32/source/drivers/usb/esp32_usbhost_uac.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -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(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(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(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; diff --git a/TactilityKernel/include/tactility/drivers/usb_host_midi.h b/TactilityKernel/include/tactility/drivers/usb_host_midi.h index eb22006c2..f9bffef28 100644 --- a/TactilityKernel/include/tactility/drivers/usb_host_midi.h +++ b/TactilityKernel/include/tactility/drivers/usb_host_midi.h @@ -4,6 +4,9 @@ #include #include +#include +#include + #ifdef __cplusplus extern "C" { #endif @@ -25,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. diff --git a/TactilityKernel/source/drivers/usb_host_midi.cpp b/TactilityKernel/source/drivers/usb_host_midi.cpp index a128eb1dd..0c1394043 100644 --- a/TactilityKernel/source/drivers/usb_host_midi.cpp +++ b/TactilityKernel/source/drivers/usb_host_midi.cpp @@ -1,23 +1,122 @@ #include +#include #include #include +#include #define USB_MIDI_API(driver) ((struct UsbMidiApi*)(driver)->api) +struct UsbMidiEventMutex { + Mutex handle {}; + UsbMidiEventMutex() { mutex_construct(&handle); } + ~UsbMidiEventMutex() { mutex_destruct(&handle); } +}; + +static UsbMidiEventSubscription* usb_midi_event_subscriptions = nullptr; +static UsbMidiEventMutex usb_midi_event_subscriptions_mutex; + extern "C" { 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" diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 1e5d7de64..c7255fc15 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.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 diff --git a/TactilityKernel/tests/source/usb_host_midi_event_test.cpp b/TactilityKernel/tests/source/usb_host_midi_event_test.cpp new file mode 100644 index 000000000..2b645ceb4 --- /dev/null +++ b/TactilityKernel/tests/source/usb_host_midi_event_test.cpp @@ -0,0 +1,97 @@ +#include "doctest.h" + +#include +#include +#include + +static usb_midi_message_t make_message(uint8_t data1) { + return usb_midi_message_t { .cable = 0, .status = 0x90, .data1 = data1, .data2 = 100 }; +} + +TEST_CASE("usb_midi_event_emit queues messages in FIFO order and signals the subscription bit") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + + auto first = make_message(60); + auto second = make_message(64); + CHECK_EQ(usb_midi_event_emit(&device, &first), ERROR_NONE); + CHECK_EQ(usb_midi_event_emit(&device, &second), ERROR_NONE); + + uint32_t flags = 0; + CHECK_EQ(task_event_group_wait(&group, sub.bit, false, &flags, 0), ERROR_NONE); + CHECK_EQ(flags, sub.bit); + + UsbMidiEvent event = {}; + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, 60); + CHECK_EQ(event.message.status, 0x90); + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, 64); + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_emit only delivers to subscriptions of the emitting device") { + Device device_a = {}; + Device device_b = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device_a), ERROR_NONE); + + auto message = make_message(60); + CHECK_EQ(usb_midi_event_emit(&device_b, &message), ERROR_NOT_FOUND); + + UsbMidiEvent event = {}; + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_emit drops the newest message when the queue is full") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + + for (int i = 0; i < USB_MIDI_EVENT_QUEUE_CAPACITY; i++) { + auto message = make_message(static_cast(i)); + CHECK_EQ(usb_midi_event_emit(&device, &message), ERROR_NONE); + } + auto overflow = make_message(127); + CHECK_EQ(usb_midi_event_emit(&device, &overflow), ERROR_RESOURCE); + + UsbMidiEvent event = {}; + for (int i = 0; i < USB_MIDI_EVENT_QUEUE_CAPACITY; i++) { + REQUIRE_EQ(usb_midi_event_poll(&sub, &event), ERROR_NONE); + CHECK_EQ(event.message.data1, i); + } + CHECK_EQ(usb_midi_event_poll(&sub, &event), ERROR_TIMEOUT); + + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + task_event_group_destruct(&group); +} + +TEST_CASE("usb_midi_event_subscribe rejects a subscription that is already registered") { + Device device = {}; + TaskEventGroup group; + task_event_group_construct(&group); + UsbMidiEventSubscription sub = {}; + + REQUIRE_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_NONE); + CHECK_EQ(usb_midi_event_subscribe(&sub, &group, &device), ERROR_INVALID_STATE); + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NONE); + CHECK_EQ(usb_midi_event_unsubscribe(&sub), ERROR_NOT_FOUND); + + task_event_group_destruct(&group); +}