diff --git a/TactilityKernel/include/tactility/drivers/keyboard.h b/TactilityKernel/include/tactility/drivers/keyboard.h index 067596ea3..a5c65507a 100644 --- a/TactilityKernel/include/tactility/drivers/keyboard.h +++ b/TactilityKernel/include/tactility/drivers/keyboard.h @@ -77,11 +77,11 @@ struct KeyboardKeyData { bool alt; /** * @brief Standard USB HID keyboard usage code for this key (USB HID Usage Tables, page 0x07), - * or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers - * need). Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so - * consumers that want to mirror physical key presses as real HID reports (e.g. USB HID output) - * don't have to reverse-engineer one out of `key`'s codepoint encoding - which is lossy for - * keys with no Unicode/LVGL representation at all, e.g. F1-F12. + * or 0 if this driver doesn't compute one (most don't - `key` is the only field most consumers need). + * Populated by drivers whose hardware layout maps cleanly onto HID usage codes, so consumers that + * want to mirror physical key presses as real HID reports (e.g. USB HID output) don't have to + * reverse-engineer one out of `key`'s codepoint encoding. + * Encoding is lossy for keys with no Unicode/LVGL representation at all, e.g. F1-F12. */ uint8_t hid_keycode; /** @@ -91,12 +91,35 @@ struct KeyboardKeyData { uint8_t hid_modifier; }; +/** + * @brief Number of key events that can be queued per subscription in the default + * subscribe/unsubscribe/poll implementation before older subscribers' fanned-out events start + * being dropped for that subscriber (silently, preserving FIFO order of what's already queued). + */ +#define KEYBOARD_EVENT_QUEUE_CAPACITY 4 + +/** + * @brief Caller-owned subscription node for keyboard_subscribe()/keyboard_unsubscribe()/ + * keyboard_poll(). + * @warning `internal` is private to keyboard.cpp; do not read or write it directly. + */ +struct KeyboardEventSubscription { + struct { + struct Device* device; + struct KeyboardKeyData queue[KEYBOARD_EVENT_QUEUE_CAPACITY]; + uint8_t head; + uint8_t count; + struct KeyboardEventSubscription* next; + } internal; +}; + /** * @brief API for keyboard drivers. */ struct KeyboardApi { /** * @brief Reads the next pending key event, if any. + * May be NULL if the driver instead pushes events itself via keyboard_emit_key() (e.g. from a dedicated interrupt-handling task). * @param[in] device the keyboard device * @param[out] data the key event data * @retval ERROR_NONE when the operation was successful @@ -104,7 +127,7 @@ struct KeyboardApi { error_t (*read_key)(struct Device* device, struct KeyboardKeyData* data); /** - * @brief Returns the baclight if the keyboard has one. + * @brief Returns the backlight if the keyboard has one. * @warning Returns a referenced device. Must call device_put() afterwards. * @param[in] device the keyboard device * @param[out] backlight_device the output backlight device @@ -114,10 +137,10 @@ struct KeyboardApi { error_t (*get_backlight)(struct Device* device, struct Device** backlight_device); /** - * @brief Optional: reports whether the keyboard is physically present right now. Only - * meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose - * kernel device is constructed and started once at boot regardless of physical attachment - - * leave NULL for a keyboard that's always physically present whenever its device is active + * @brief Optional: reports whether the keyboard is physically present right now. + * Only meaningful for hot-pluggable/detachable keyboards (e.g. a removable accessory) whose + * kernel device is constructed and started once at boot regardless of physical attachment. + * Leave NULL for a keyboard that's always physically present whenever its device is active * (the common case; callers must treat NULL the same as "always present"). * @param[in] device the keyboard device * @return true if physically attached/present @@ -127,9 +150,22 @@ struct KeyboardApi { /** * @brief Reads the next pending key event using the specified keyboard device. + * @retval ERROR_NOT_SUPPORTED the driver has no KeyboardApi::read_key (it pushes events via + * keyboard_emit_key() instead) */ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data); +/** + * @brief Push a key event from @a device to every current subscriber (see keyboard_subscribe()). + * For a driver whose hardware delivers key events asynchronously (e.g. from a dedicated + * interrupt-handling task) rather than on demand - KeyboardApi::read_key may be left NULL when + * the driver calls this instead. + * @warning Not ISR-safe (takes a mutex); call from a task, not directly from interrupt context. + * @param[in] device the keyboard device the event originated from + * @param[in] data the key event + */ +void keyboard_emit_key(struct Device* device, struct KeyboardKeyData data); + /** * @brief Returns the backlight if the keyboard has one. * @warning Returns a referenced device. Must call device_put() afterwards. @@ -141,13 +177,34 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data); error_t keyboard_get_backlight(struct Device* device, struct Device** backlight_device); /** - * @brief Whether the keyboard device is physically present right now. True when the driver - * doesn't implement KeyboardApi::is_present (i.e. it's always physically present whenever its - * device is active) - see that field's doc comment. + * @brief Whether the keyboard device is physically present right now. + * Returns true when the driver doesn't implement KeyboardApi::is_present * @param[in] device the keyboard device */ bool keyboard_is_present(struct Device* device); +/** + * @brief Register @a sub for async key events from @a device, derived from `read_key`. + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and stationary) + * until unsubscribed + * @retval ERROR_NONE on success + */ +error_t keyboard_subscribe(struct Device* device, struct KeyboardEventSubscription* sub); + +/** + * @brief Remove a subscription previously registered with keyboard_subscribe(). + * @retval ERROR_NONE on success + */ +error_t keyboard_unsubscribe(struct Device* device, struct KeyboardEventSubscription* sub); + +/** + * @brief Non-blocking: pop the next event for @a sub if one is already queued. + * @warning Never blocks. + * @retval ERROR_NONE @a out_data was filled + * @retval ERROR_TIMEOUT nothing queued right now + */ +error_t keyboard_poll(struct Device* device, struct KeyboardEventSubscription* sub, struct KeyboardKeyData* out_data); + extern const struct DeviceType KEYBOARD_TYPE; #ifdef __cplusplus diff --git a/TactilityKernel/source/drivers/keyboard.cpp b/TactilityKernel/source/drivers/keyboard.cpp index 9938b6a96..256d6e403 100644 --- a/TactilityKernel/source/drivers/keyboard.cpp +++ b/TactilityKernel/source/drivers/keyboard.cpp @@ -3,13 +3,49 @@ #include #include +#include + #define KEYBOARD_DRIVER_API(driver) ((struct KeyboardApi*)driver->api) +/** + * Intrusive singly-linked list of subscriptions, filtered by `device` on fan-out. Guarded by a + * single coarse-grained mutex - same rationale as app_event.cpp's subscriptions_mutex: fanning + * out here is just a struct copy, never caller code. + */ +static KeyboardEventSubscription* subscriptions = nullptr; + +struct KeyboardEventMutex { + Mutex handle {}; + KeyboardEventMutex() { mutex_construct(&handle); } + ~KeyboardEventMutex() { mutex_destruct(&handle); } +}; + +static KeyboardEventMutex subscriptions_mutex; + +// Pushes `data` onto every subscription for `device` except `exclude` (pass nullptr to include +// all of them). Caller must hold subscriptions_mutex. +static void fan_out_to_subscribers(Device* device, const KeyboardKeyData& data, const KeyboardEventSubscription* exclude) { + for (KeyboardEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->internal.next) { + if (sub == exclude || sub->internal.device != device) { + continue; + } + if (sub->internal.count < KEYBOARD_EVENT_QUEUE_CAPACITY) { + uint8_t tail = (sub->internal.head + sub->internal.count) % KEYBOARD_EVENT_QUEUE_CAPACITY; + sub->internal.queue[tail] = data; + sub->internal.count++; + } + } +} + extern "C" { error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { const auto* driver = device_get_driver(device); + if (KEYBOARD_DRIVER_API(driver)->read_key == nullptr) { + return ERROR_NOT_SUPPORTED; + } + // Default the modifier/HID fields here rather than in each driver: only drivers whose hardware // can report them set them, and the rest would otherwise leave whatever the caller's stack held. data->ctrl = false; @@ -20,6 +56,12 @@ error_t keyboard_read_key(Device* device, KeyboardKeyData* data) { return KEYBOARD_DRIVER_API(driver)->read_key(device, data); } +void keyboard_emit_key(Device* device, KeyboardKeyData data) { + mutex_lock(&subscriptions_mutex.handle); + fan_out_to_subscribers(device, data, nullptr); + mutex_unlock(&subscriptions_mutex.handle); +} + error_t keyboard_get_backlight(Device* device, Device** backlight_device) { const auto* driver = device_get_driver(device); @@ -40,8 +82,69 @@ bool keyboard_is_present(Device* device) { return KEYBOARD_DRIVER_API(driver)->is_present(device); } +error_t keyboard_subscribe(Device* device, KeyboardEventSubscription* sub) { + mutex_lock(&subscriptions_mutex.handle); + + // Avoid cyclic subscription list that would loop forever + if (subscriptions == sub) { + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_INVALID_STATE; + } + + sub->internal.device = device; + sub->internal.head = 0; + sub->internal.count = 0; + sub->internal.next = subscriptions; + subscriptions = sub; + + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_NONE; +} + +error_t keyboard_unsubscribe(Device*, KeyboardEventSubscription* sub) { + error_t result = ERROR_NOT_FOUND; + + mutex_lock(&subscriptions_mutex.handle); + for (KeyboardEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) { + if (*link == sub) { + *link = sub->internal.next; + result = ERROR_NONE; + break; + } + } + mutex_unlock(&subscriptions_mutex.handle); + + return result; +} + +error_t keyboard_poll(Device* device, KeyboardEventSubscription* sub, KeyboardKeyData* out_data) { + mutex_lock(&subscriptions_mutex.handle); + + if (sub->internal.count > 0) { + *out_data = sub->internal.queue[sub->internal.head]; + sub->internal.head = (sub->internal.head + 1) % KEYBOARD_EVENT_QUEUE_CAPACITY; + sub->internal.count--; + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_NONE; + } + + // No queued event of our own - try a synchronous read (if the driver has one; a driver that + // only pushes via keyboard_emit_key() has no read_key, and there's simply nothing new yet). + KeyboardKeyData data; + if (keyboard_read_key(device, &data) != ERROR_NONE || data.key == 0) { + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_TIMEOUT; + } + + fan_out_to_subscribers(device, data, sub); + + *out_data = data; + mutex_unlock(&subscriptions_mutex.handle); + return ERROR_NONE; +} + const DeviceType KEYBOARD_TYPE { - .name = "keyboard" + .name = "keyboard", }; } diff --git a/TactilityKernel/tests/source/keyboard_test.cpp b/TactilityKernel/tests/source/keyboard_test.cpp new file mode 100644 index 000000000..f1e00f055 --- /dev/null +++ b/TactilityKernel/tests/source/keyboard_test.cpp @@ -0,0 +1,168 @@ +#include "doctest.h" +#include +#include +#include +#include + +static Module keyboard_test_module = { + .name = "keyboard_test_module", + .start = nullptr, + .stop = nullptr +}; + +static int read_key_call_count = 0; + +// First call reports a key press, every call after that reports nothing pending (key == 0), +// matching the convention every real driver uses (see e.g. sdl_keyboard_read_key). +static error_t fake_read_key(Device*, KeyboardKeyData* data) { + read_key_call_count++; + if (read_key_call_count == 1) { + data->key = 'a'; + data->pressed = true; + data->continue_reading = false; + } else { + data->key = 0; + data->pressed = false; + data->continue_reading = false; + } + return ERROR_NONE; +} + +static const KeyboardApi fake_keyboard_api = { + .read_key = fake_read_key, + .get_backlight = nullptr, + .is_present = nullptr, +}; + +static Driver fake_keyboard_driver = { + .name = "fake_keyboard_driver", + .compatible = (const char*[]) { "keyboard_test,fake", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &fake_keyboard_api, + .device_type = &KEYBOARD_TYPE, + .owner = &keyboard_test_module, + .internal = nullptr, +}; + +// No read_key: this driver only ever pushes events via keyboard_emit_key(). +static const KeyboardApi fake_emit_only_keyboard_api = { + .read_key = nullptr, + .get_backlight = nullptr, + .is_present = nullptr, +}; + +static Driver fake_emit_only_keyboard_driver = { + .name = "fake_emit_only_keyboard_driver", + .compatible = (const char*[]) { "keyboard_test,fake_emit_only", nullptr }, + .start_device = nullptr, + .stop_device = nullptr, + .api = &fake_emit_only_keyboard_api, + .device_type = &KEYBOARD_TYPE, + .owner = &keyboard_test_module, + .internal = nullptr, +}; + +TEST_CASE("keyboard_poll's default implementation fans out a read_key result to other subscribers") { + read_key_call_count = 0; + + static Device fake_device { + .name = "fake_keyboard_device", + .config = nullptr, + .parent = nullptr, + }; + + CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); + CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); + CHECK_EQ(device_start(&fake_device), ERROR_NONE); + + KeyboardEventSubscription sub_a {}; + KeyboardEventSubscription sub_b {}; + CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE); + CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE); + + KeyboardKeyData out {}; + CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE); + CHECK_EQ(out.key, 'a'); + CHECK_EQ(read_key_call_count, 1); + + // sub_b receives the same event without a second hardware read - it was fanned out by sub_a's poll. + CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_NONE); + CHECK_EQ(out.key, 'a'); + CHECK_EQ(read_key_call_count, 1); + + // sub_b's queue is now empty; polling again falls through to a real (empty) hardware read. + CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT); + CHECK_EQ(read_key_call_count, 2); + + CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE); + CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE); + CHECK_EQ(device_stop(&fake_device), ERROR_NONE); + CHECK_EQ(device_remove(&fake_device), ERROR_NONE); + CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); + CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); +} + +TEST_CASE("keyboard_emit_key delivers to subscribers of a driver with no read_key") { + static Device fake_device { + .name = "fake_emit_only_keyboard_device", + .config = nullptr, + .parent = nullptr, + }; + + CHECK_EQ(driver_construct_add(&fake_emit_only_keyboard_driver), ERROR_NONE); + CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake_emit_only"), ERROR_NONE); + CHECK_EQ(device_start(&fake_device), ERROR_NONE); + + KeyboardEventSubscription sub {}; + CHECK_EQ(keyboard_subscribe(&fake_device, &sub), ERROR_NONE); + + // Nothing pushed yet, and there's no read_key to fall back on. + KeyboardKeyData out {}; + CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_TIMEOUT); + + KeyboardKeyData emitted { .key = 'z', .pressed = true, .continue_reading = false }; + keyboard_emit_key(&fake_device, emitted); + + CHECK_EQ(keyboard_poll(&fake_device, &sub, &out), ERROR_NONE); + CHECK_EQ(out.key, 'z'); + + CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub), ERROR_NONE); + CHECK_EQ(device_stop(&fake_device), ERROR_NONE); + CHECK_EQ(device_remove(&fake_device), ERROR_NONE); + CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); + CHECK_EQ(driver_remove_destruct(&fake_emit_only_keyboard_driver), ERROR_NONE); +} + +TEST_CASE("keyboard_unsubscribe stops a subscription from receiving further fanned-out events") { + read_key_call_count = 0; + + static Device fake_device { + .name = "fake_keyboard_device_2", + .config = nullptr, + .parent = nullptr, + }; + + CHECK_EQ(driver_construct_add(&fake_keyboard_driver), ERROR_NONE); + CHECK_EQ(device_construct_add(&fake_device, "keyboard_test,fake"), ERROR_NONE); + CHECK_EQ(device_start(&fake_device), ERROR_NONE); + + KeyboardEventSubscription sub_a {}; + KeyboardEventSubscription sub_b {}; + CHECK_EQ(keyboard_subscribe(&fake_device, &sub_a), ERROR_NONE); + CHECK_EQ(keyboard_subscribe(&fake_device, &sub_b), ERROR_NONE); + CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_b), ERROR_NONE); + + KeyboardKeyData out {}; + CHECK_EQ(keyboard_poll(&fake_device, &sub_a, &out), ERROR_NONE); + CHECK_EQ(out.key, 'a'); + + // sub_b was unsubscribed before the poll, so nothing was fanned out to it. + CHECK_EQ(keyboard_poll(&fake_device, &sub_b, &out), ERROR_TIMEOUT); + + CHECK_EQ(keyboard_unsubscribe(&fake_device, &sub_a), ERROR_NONE); + CHECK_EQ(device_stop(&fake_device), ERROR_NONE); + CHECK_EQ(device_remove(&fake_device), ERROR_NONE); + CHECK_EQ(device_destruct(&fake_device), ERROR_NONE); + CHECK_EQ(driver_remove_destruct(&fake_keyboard_driver), ERROR_NONE); +}