diff --git a/CMakeLists.txt b/CMakeLists.txt index 26662f5..724d17f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,8 @@ add_subdirectory(drivers) target_sources(app PRIVATE src/main.c src/battery_module.c + src/keyboard_core_module.c src/mode_switch_module.c + src/events/keyboard_hid_report_event.c src/events/mode_switch_event.c ) diff --git a/inc/events/keyboard_hid_report_event.h b/inc/events/keyboard_hid_report_event.h new file mode 100644 index 0000000..ae2841b --- /dev/null +++ b/inc/events/keyboard_hid_report_event.h @@ -0,0 +1,28 @@ +#ifndef BLINKY_KEYBOARD_HID_REPORT_EVENT_H_ +#define BLINKY_KEYBOARD_HID_REPORT_EVENT_H_ + +#include +#include + +#include "keyboard_core.h" +#include "mode_switch_event.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct keyboard_hid_report_event { + struct app_event_header header; + enum mode_switch_mode mode; + enum keyboard_report_type report_type; + enum keyboard_protocol_mode protocol_mode; + struct event_dyndata dyndata; +}; + +APP_EVENT_TYPE_DYNDATA_DECLARE(keyboard_hid_report_event); + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_KEYBOARD_HID_REPORT_EVENT_H_ */ diff --git a/inc/keyboard_core.h b/inc/keyboard_core.h new file mode 100644 index 0000000..288df4b --- /dev/null +++ b/inc/keyboard_core.h @@ -0,0 +1,40 @@ +#ifndef BLINKY_KEYBOARD_CORE_H_ +#define BLINKY_KEYBOARD_CORE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define KEYBOARD_BOOT_REPORT_SIZE 8U +#define KEYBOARD_NKRO_USAGE_MAX 0xDFU +#define KEYBOARD_NKRO_BITMAP_BYTES ((KEYBOARD_NKRO_USAGE_MAX + 8U) / 8U) +#define KEYBOARD_NKRO_REPORT_SIZE (1U + KEYBOARD_NKRO_BITMAP_BYTES) +#define KEYBOARD_CONSUMER_REPORT_SIZE 2U + +enum keyboard_protocol_mode { + KEYBOARD_PROTOCOL_MODE_BOOT, + KEYBOARD_PROTOCOL_MODE_REPORT, +}; + +enum keyboard_report_type { + KEYBOARD_REPORT_TYPE_KEYS, + KEYBOARD_REPORT_TYPE_CONSUMER, +}; + +enum keyboard_consumer_control { + KEYBOARD_CONSUMER_CTRL_MUTE, + KEYBOARD_CONSUMER_CTRL_VOLUME_UP, + KEYBOARD_CONSUMER_CTRL_VOLUME_DOWN, + KEYBOARD_CONSUMER_CTRL_PLAY_PAUSE, + KEYBOARD_CONSUMER_CTRL_NEXT_TRACK, + KEYBOARD_CONSUMER_CTRL_PREV_TRACK, + KEYBOARD_CONSUMER_CTRL_COUNT, +}; + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_KEYBOARD_CORE_H_ */ diff --git a/src/events/keyboard_hid_report_event.c b/src/events/keyboard_hid_report_event.c new file mode 100644 index 0000000..698075a --- /dev/null +++ b/src/events/keyboard_hid_report_event.c @@ -0,0 +1,107 @@ +#include + +#include "keyboard_hid_report_event.h" + +#define KEYBOARD_HID_REPORT_EVENT_LOG_BUF_LEN 192 + +static const char *mode_name(enum mode_switch_mode mode) +{ + switch (mode) { + case MODE_SWITCH_USB: + return "USB"; + case MODE_SWITCH_24G: + return "2.4G"; + case MODE_SWITCH_BLE: + return "BLE"; + default: + return "?"; + } +} + +static const char *report_type_name(enum keyboard_report_type report_type) +{ + switch (report_type) { + case KEYBOARD_REPORT_TYPE_KEYS: + return "keys"; + case KEYBOARD_REPORT_TYPE_CONSUMER: + return "consumer"; + default: + return "?"; + } +} + +static const char *protocol_mode_name(enum keyboard_protocol_mode protocol_mode) +{ + switch (protocol_mode) { + case KEYBOARD_PROTOCOL_MODE_BOOT: + return "boot"; + case KEYBOARD_PROTOCOL_MODE_REPORT: + return "report"; + default: + return "?"; + } +} + +static void log_keyboard_hid_report_event(const struct app_event_header *aeh) +{ + const struct keyboard_hid_report_event *event = + cast_keyboard_hid_report_event(aeh); + char log_buf[KEYBOARD_HID_REPORT_EVENT_LOG_BUF_LEN]; + int pos; + + pos = snprintf(log_buf, sizeof(log_buf), "mode:%s type:%s protocol:%s len:%zu", + mode_name(event->mode), + report_type_name(event->report_type), + protocol_mode_name(event->protocol_mode), + event->dyndata.size); + if ((pos > 0) && (pos < sizeof(log_buf))) { + for (size_t i = 0; i < event->dyndata.size; i++) { + int tmp = snprintf(&log_buf[pos], sizeof(log_buf) - pos, + " %02x", event->dyndata.data[i]); + + if (tmp < 0) { + log_buf[sizeof(log_buf) - 2] = '~'; + pos = tmp; + break; + } + + pos += tmp; + if (pos >= sizeof(log_buf)) { + break; + } + } + } + + if (pos < 0) { + APP_EVENT_MANAGER_LOG(aeh, "log message preparation failure"); + return; + } + + APP_EVENT_MANAGER_LOG(aeh, "%s", log_buf); +} + +static void profile_keyboard_hid_report_event(struct log_event_buf *buf, + const struct app_event_header *aeh) +{ + const struct keyboard_hid_report_event *event = + cast_keyboard_hid_report_event(aeh); + + nrf_profiler_log_encode_uint8(buf, event->mode); + nrf_profiler_log_encode_uint8(buf, event->report_type); + nrf_profiler_log_encode_uint8(buf, event->protocol_mode); + nrf_profiler_log_encode_uint8(buf, (uint8_t)event->dyndata.size); +} + +APP_EVENT_INFO_DEFINE(keyboard_hid_report_event, + ENCODE(NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8), + ENCODE("mode", "report_type", "protocol_mode", "len"), + profile_keyboard_hid_report_event); + +APP_EVENT_TYPE_DEFINE(keyboard_hid_report_event, + log_keyboard_hid_report_event, + &keyboard_hid_report_event_info, + APP_EVENT_FLAGS_CREATE( + APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/keyboard_core_module.c b/src/keyboard_core_module.c new file mode 100644 index 0000000..2529c3c --- /dev/null +++ b/src/keyboard_core_module.c @@ -0,0 +1,487 @@ +#include +#include +#include + +#include + +#define MODULE keyboard_core_module +#include +#include +#include + +#include +#include +#include +#include + +#include "keyboard_core.h" +#include "keyboard_hid_report_event.h" +#include "mode_switch_event.h" + +LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); + +#define KEYBOARD_USAGE_FIRST_MODIFIER 0xE0U +#define KEYBOARD_USAGE_LAST_MODIFIER 0xE7U +#define KEYBOARD_USAGE_ERROR_ROLLOVER 0x01U +#define KEYBOARD_BOOT_RESERVED_BYTE 0x00U + +enum key_usage_type { + KEY_USAGE_TYPE_KEYBOARD, + KEY_USAGE_TYPE_CONSUMER, +}; + +struct keymap_entry { + uint16_t key_id; + uint8_t usage_type; + uint16_t usage_id; +}; + +struct keyboard_state { + uint8_t modifiers; + uint8_t keys_bitmap[KEYBOARD_NKRO_BITMAP_BYTES]; + uint32_t consumer_bits; +}; + +struct keyboard_reports_cache { + uint8_t boot_report[KEYBOARD_BOOT_REPORT_SIZE]; + uint8_t nkro_report[KEYBOARD_NKRO_REPORT_SIZE]; + uint8_t consumer_report[KEYBOARD_CONSUMER_REPORT_SIZE]; + bool boot_valid; + bool nkro_valid; + bool consumer_valid; +}; + +static const struct keymap_entry keymap[] = { + { KEY_ID(0, 1), KEY_USAGE_TYPE_KEYBOARD, 0x0053 }, /* num lock */ + { KEY_ID(0, 2), KEY_USAGE_TYPE_KEYBOARD, 0x005F }, /* keypad 7 */ + { KEY_ID(0, 3), KEY_USAGE_TYPE_KEYBOARD, 0x005C }, /* keypad 4 */ + { KEY_ID(0, 4), KEY_USAGE_TYPE_KEYBOARD, 0x0059 }, /* keypad 1 */ + { KEY_ID(0, 5), KEY_USAGE_TYPE_KEYBOARD, 0x0062 }, /* keypad 0 */ + { KEY_ID(1, 1), KEY_USAGE_TYPE_KEYBOARD, 0x0054 }, /* keypad / */ + { KEY_ID(1, 2), KEY_USAGE_TYPE_KEYBOARD, 0x0060 }, /* keypad 8 */ + { KEY_ID(1, 3), KEY_USAGE_TYPE_KEYBOARD, 0x005D }, /* keypad 5 */ + { KEY_ID(1, 4), KEY_USAGE_TYPE_KEYBOARD, 0x005A }, /* keypad 2 */ + { KEY_ID(1, 5), KEY_USAGE_TYPE_KEYBOARD, 0x0063 }, /* keypad . */ + { KEY_ID(2, 1), KEY_USAGE_TYPE_KEYBOARD, 0x0055 }, /* keypad * */ + { KEY_ID(2, 2), KEY_USAGE_TYPE_KEYBOARD, 0x0061 }, /* keypad 9 */ + { KEY_ID(2, 3), KEY_USAGE_TYPE_KEYBOARD, 0x005E }, /* keypad 6 */ + { KEY_ID(2, 4), KEY_USAGE_TYPE_KEYBOARD, 0x005B }, /* keypad 3 */ + { KEY_ID(2, 5), KEY_USAGE_TYPE_KEYBOARD, 0x0058 }, /* keypad enter */ + { KEY_ID(3, 0), KEY_USAGE_TYPE_CONSUMER, KEYBOARD_CONSUMER_CTRL_MUTE }, + { KEY_ID(3, 1), KEY_USAGE_TYPE_KEYBOARD, 0x0056 }, /* keypad - */ + { KEY_ID(3, 3), KEY_USAGE_TYPE_KEYBOARD, 0x0057 }, /* keypad + */ +}; + +static const uint16_t consumer_usage_map[KEYBOARD_CONSUMER_CTRL_COUNT] = { + [KEYBOARD_CONSUMER_CTRL_MUTE] = 0x00E2, + [KEYBOARD_CONSUMER_CTRL_VOLUME_UP] = 0x00E9, + [KEYBOARD_CONSUMER_CTRL_VOLUME_DOWN] = 0x00EA, + [KEYBOARD_CONSUMER_CTRL_PLAY_PAUSE] = 0x00CD, + [KEYBOARD_CONSUMER_CTRL_NEXT_TRACK] = 0x00B5, + [KEYBOARD_CONSUMER_CTRL_PREV_TRACK] = 0x00B6, +}; + +static struct keyboard_state keyboard_state; +static struct keyboard_reports_cache reports_cache; +static enum keyboard_protocol_mode protocol_mode = KEYBOARD_PROTOCOL_MODE_REPORT; +static enum mode_switch_mode current_mode; +static bool initialized; +static bool running; +static bool mode_valid; + +static const struct keymap_entry *keymap_get(uint16_t key_id) +{ + size_t left = 0; + size_t right = ARRAY_SIZE(keymap); + + while (left < right) { + size_t mid = left + ((right - left) / 2U); + + if (keymap[mid].key_id == key_id) { + return &keymap[mid]; + } + + if (keymap[mid].key_id < key_id) { + left = mid + 1U; + } else { + right = mid; + } + } + + return NULL; +} + +static bool usage_is_modifier(uint16_t usage_id) +{ + return IN_RANGE(usage_id, KEYBOARD_USAGE_FIRST_MODIFIER, + KEYBOARD_USAGE_LAST_MODIFIER); +} + +static bool keyboard_key_update(uint16_t usage_id, bool pressed) +{ + if (usage_is_modifier(usage_id)) { + uint8_t new_modifiers = keyboard_state.modifiers; + + WRITE_BIT(new_modifiers, usage_id - KEYBOARD_USAGE_FIRST_MODIFIER, pressed); + if (new_modifiers == keyboard_state.modifiers) { + return false; + } + + keyboard_state.modifiers = new_modifiers; + return true; + } + + if (usage_id > KEYBOARD_NKRO_USAGE_MAX) { + LOG_WRN("Unsupported keyboard usage 0x%04x", usage_id); + return false; + } + + uint8_t byte_idx = usage_id / 8U; + uint8_t bit_idx = usage_id % 8U; + bool was_pressed = (keyboard_state.keys_bitmap[byte_idx] & BIT(bit_idx)) != 0U; + + if (was_pressed == pressed) { + return false; + } + + WRITE_BIT(keyboard_state.keys_bitmap[byte_idx], bit_idx, pressed); + return true; +} + +static bool consumer_key_update(uint16_t consumer_id, bool pressed) +{ + if (consumer_id >= KEYBOARD_CONSUMER_CTRL_COUNT) { + LOG_WRN("Unsupported consumer id %u", consumer_id); + return false; + } + + bool was_pressed = (keyboard_state.consumer_bits & BIT(consumer_id)) != 0U; + + if (was_pressed == pressed) { + return false; + } + + WRITE_BIT(keyboard_state.consumer_bits, consumer_id, pressed); + return true; +} + +static void keyboard_state_clear(void) +{ + memset(&keyboard_state, 0, sizeof(keyboard_state)); +} + +static void reports_cache_invalidate(void) +{ + reports_cache.boot_valid = false; + reports_cache.nkro_valid = false; + reports_cache.consumer_valid = false; +} + +static void build_boot_report(uint8_t report[KEYBOARD_BOOT_REPORT_SIZE]) +{ + size_t key_count = 0; + + memset(report, 0, KEYBOARD_BOOT_REPORT_SIZE); + report[0] = keyboard_state.modifiers; + report[1] = KEYBOARD_BOOT_RESERVED_BYTE; + + for (uint16_t usage_id = 0; usage_id <= KEYBOARD_NKRO_USAGE_MAX; usage_id++) { + uint8_t byte_idx = usage_id / 8U; + uint8_t bit_idx = usage_id % 8U; + + if ((keyboard_state.keys_bitmap[byte_idx] & BIT(bit_idx)) == 0U) { + continue; + } + + if (key_count == (KEYBOARD_BOOT_REPORT_SIZE - 2U)) { + memset(&report[2], KEYBOARD_USAGE_ERROR_ROLLOVER, + KEYBOARD_BOOT_REPORT_SIZE - 2U); + return; + } + + report[2U + key_count] = (uint8_t)usage_id; + key_count++; + } +} + +static void build_nkro_report(uint8_t report[KEYBOARD_NKRO_REPORT_SIZE]) +{ + report[0] = keyboard_state.modifiers; + memcpy(&report[1], keyboard_state.keys_bitmap, KEYBOARD_NKRO_BITMAP_BYTES); +} + +static uint16_t active_consumer_usage_get(void) +{ + for (uint8_t consumer_id = 0; consumer_id < KEYBOARD_CONSUMER_CTRL_COUNT; consumer_id++) { + if ((keyboard_state.consumer_bits & BIT(consumer_id)) != 0U) { + return consumer_usage_map[consumer_id]; + } + } + + return 0U; +} + +static void build_consumer_report(uint8_t report[KEYBOARD_CONSUMER_REPORT_SIZE]) +{ + sys_put_le16(active_consumer_usage_get(), report); +} + +static void submit_keyboard_report_event(enum keyboard_report_type report_type, + const uint8_t *data, size_t size) +{ + struct keyboard_hid_report_event *event = + new_keyboard_hid_report_event(size); + + event->mode = current_mode; + event->report_type = report_type; + event->protocol_mode = protocol_mode; + memcpy(event->dyndata.data, data, size); + + APP_EVENT_SUBMIT(event); +} + +static void emit_keys_report(bool force) +{ + uint8_t report_buf[KEYBOARD_NKRO_REPORT_SIZE]; + uint8_t report_size; + uint8_t *cache_buf; + bool *cache_valid; + + if (!mode_valid) { + return; + } + + if (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT) { + build_boot_report(report_buf); + report_size = KEYBOARD_BOOT_REPORT_SIZE; + cache_buf = reports_cache.boot_report; + cache_valid = &reports_cache.boot_valid; + } else { + build_nkro_report(report_buf); + report_size = KEYBOARD_NKRO_REPORT_SIZE; + cache_buf = reports_cache.nkro_report; + cache_valid = &reports_cache.nkro_valid; + } + + if (!force && *cache_valid && (memcmp(cache_buf, report_buf, report_size) == 0)) { + return; + } + + memcpy(cache_buf, report_buf, report_size); + *cache_valid = true; + + submit_keyboard_report_event(KEYBOARD_REPORT_TYPE_KEYS, report_buf, report_size); +} + +static void emit_consumer_report(bool force) +{ + uint8_t report_buf[KEYBOARD_CONSUMER_REPORT_SIZE]; + + if (!mode_valid) { + return; + } + + build_consumer_report(report_buf); + if (!force && reports_cache.consumer_valid && + (memcmp(reports_cache.consumer_report, report_buf, + KEYBOARD_CONSUMER_REPORT_SIZE) == 0)) { + return; + } + + memcpy(reports_cache.consumer_report, report_buf, KEYBOARD_CONSUMER_REPORT_SIZE); + reports_cache.consumer_valid = true; + + submit_keyboard_report_event(KEYBOARD_REPORT_TYPE_CONSUMER, + report_buf, + KEYBOARD_CONSUMER_REPORT_SIZE); +} + +static void emit_all_reports(bool force) +{ + emit_keys_report(force); + emit_consumer_report(force); +} + +static void emit_release_reports(enum mode_switch_mode mode) +{ + struct keyboard_hid_report_event *event; + uint8_t keys_report[KEYBOARD_NKRO_REPORT_SIZE] = { 0 }; + uint8_t consumer_report[KEYBOARD_CONSUMER_REPORT_SIZE] = { 0 }; + size_t keys_report_size = + (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT) ? + KEYBOARD_BOOT_REPORT_SIZE : KEYBOARD_NKRO_REPORT_SIZE; + + event = new_keyboard_hid_report_event(keys_report_size); + event->mode = mode; + event->report_type = KEYBOARD_REPORT_TYPE_KEYS; + event->protocol_mode = protocol_mode; + memcpy(event->dyndata.data, keys_report, keys_report_size); + APP_EVENT_SUBMIT(event); + + event = new_keyboard_hid_report_event(KEYBOARD_CONSUMER_REPORT_SIZE); + event->mode = mode; + event->report_type = KEYBOARD_REPORT_TYPE_CONSUMER; + event->protocol_mode = protocol_mode; + memcpy(event->dyndata.data, consumer_report, KEYBOARD_CONSUMER_REPORT_SIZE); + APP_EVENT_SUBMIT(event); +} + +static int module_init(void) +{ + keyboard_state_clear(); + reports_cache_invalidate(); + mode_valid = false; + protocol_mode = KEYBOARD_PROTOCOL_MODE_REPORT; + + return 0; +} + +static int module_start(void) +{ + if (running) { + return 0; + } + + running = true; + return 0; +} + +static void module_pause(void) +{ + if (!running) { + return; + } + + if (mode_valid) { + emit_release_reports(current_mode); + } + + keyboard_state_clear(); + reports_cache_invalidate(); + mode_valid = false; + running = false; +} + +static bool handle_button_event(const struct button_event *event) +{ + const struct keymap_entry *entry; + bool changed; + + if (!running) { + return false; + } + + entry = keymap_get(event->key_id); + if (!entry) { + LOG_WRN("Unmapped key id 0x%04x", event->key_id); + return false; + } + + if (entry->usage_type == KEY_USAGE_TYPE_KEYBOARD) { + changed = keyboard_key_update(entry->usage_id, event->pressed); + if (changed) { + emit_keys_report(false); + } + } else { + changed = consumer_key_update(entry->usage_id, event->pressed); + if (changed) { + emit_consumer_report(false); + } + } + + return false; +} + +static bool handle_mode_switch_event(const struct mode_switch_event *event) +{ + bool mode_changed; + + if (!running) { + current_mode = event->mode; + return false; + } + + mode_changed = mode_valid && (current_mode != event->mode); + if (mode_changed) { + emit_release_reports(current_mode); + keyboard_state_clear(); + reports_cache_invalidate(); + } + + current_mode = event->mode; + mode_valid = true; + emit_all_reports(true); + + return false; +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_button_event(aeh)) { + return handle_button_event(cast_button_event(aeh)); + } + + if (is_mode_switch_event(aeh)) { + return handle_mode_switch_event(cast_mode_switch_event(aeh)); + } + + if (is_module_state_event(aeh)) { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) { + int err; + + if (!initialized) { + err = module_init(); + if (err) { + module_set_state(MODULE_STATE_ERROR); + return false; + } + + initialized = true; + } + + err = module_start(); + if (err) { + module_set_state(MODULE_STATE_ERROR); + } else { + module_set_state(MODULE_STATE_READY); + } + } + + return false; + } + + if (is_power_down_event(aeh)) { + if (initialized) { + module_pause(); + module_set_state(MODULE_STATE_STANDBY); + } + + return false; + } + + if (is_wake_up_event(aeh)) { + if (initialized) { + int err = module_start(); + + if (err) { + module_set_state(MODULE_STATE_ERROR); + } else { + module_set_state(MODULE_STATE_READY); + } + } + + return false; + } + + __ASSERT_NO_MSG(false); + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, button_event); +APP_EVENT_SUBSCRIBE(MODULE, mode_switch_event); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); +APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);