mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Implement async keyboard API
This commit is contained in:
parent
65ee223876
commit
996aea8dac
@ -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
|
||||
|
||||
@ -3,13 +3,49 @@
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
|
||||
#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",
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
168
TactilityKernel/tests/source/keyboard_test.cpp
Normal file
168
TactilityKernel/tests/source/keyboard_test.cpp
Normal file
@ -0,0 +1,168 @@
|
||||
#include "doctest.h"
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user