From bf9234c602bdf61016f16aa6c2ba179b8ab1623f Mon Sep 17 00:00:00 2001 From: dnaq Date: Fri, 24 Sep 2021 17:53:17 +0200 Subject: [PATCH] feat(stenography): Support the Plover HID Protocol This commit adds tentative support for the Plover HID Protocol, a simple HID-based protocol that works over both USB and BLE. The main value-add of this feature is that it allows ZMK to be used for interfacing with Plover from the Open Steno Project without Plover having to intercept all keyboard keypresses. Usually hobbyist steno machines use legacy serial port protocols for this, but since we can't use those over BLE we might as well develop a protocol that works both for USB and BLE and at the same time remove some of the pain points of those old protocols. --- app/Kconfig | 13 ++++ app/include/dt-bindings/zmk/hid_usage_pages.h | 3 +- app/include/dt-bindings/zmk/keys.h | 65 +++++++++++++++++ app/include/zmk/hid.h | 55 ++++++++++++++ app/include/zmk/hog.h | 1 + app/src/endpoints.c | 37 ++++++++++ app/src/hid.c | 22 ++++++ app/src/hid_listener.c | 4 +- app/src/hog.c | 71 +++++++++++++++++++ docs/docs/features/stenography.md | 46 ++++++++++++ docs/docs/intro.md | 1 + 11 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 docs/docs/features/stenography.md diff --git a/app/Kconfig b/app/Kconfig index 9902046d..7106538b 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -139,6 +139,10 @@ config ZMK_BLE_CONSUMER_REPORT_QUEUE_SIZE int "Max number of consumer HID reports to queue for sending over BLE" default 5 +config ZMK_BLE_PLOVER_HID_REPORT_QUEUE_SIZE + int "Max number of plover HID reports to queue for sending over BLE" + default 5 + config ZMK_BLE_CLEAR_BONDS_ON_START bool "Configuration that clears all bond information from the keyboard on startup." default n @@ -180,6 +184,14 @@ endmenu rsource "src/split/Kconfig" +menu "Stenography Support" + +config ZMK_PLOVER_HID + bool "Support for the Plover HID Stenography Protocol" + +# Stenography Support +endmenu + #Basic Keyboard Setup endmenu @@ -387,6 +399,7 @@ menu "KSCAN Settings" config ZMK_KSCAN_EVENT_QUEUE_SIZE int "Size of the event queue for KSCAN events to buffer events" default 4 + default 64 if ZMK_PLOVER_HID #KSCAN Settings endmenu diff --git a/app/include/dt-bindings/zmk/hid_usage_pages.h b/app/include/dt-bindings/zmk/hid_usage_pages.h index 2ccdba55..d0f45f0a 100644 --- a/app/include/dt-bindings/zmk/hid_usage_pages.h +++ b/app/include/dt-bindings/zmk/hid_usage_pages.h @@ -48,4 +48,5 @@ #define HID_USAGE_CAMERA (0x90) // Camera Control #define HID_USAGE_ARCADE (0x91) // Arcade #define HID_USAGE_GAMING (0x92) // Gaming Device -#define HID_USAGE_FIDO (0xF1D0) // FIDO Alliance \ No newline at end of file +#define HID_USAGE_FIDO (0xF1D0) // FIDO Alliance +#define HID_USAGE_VENDOR_PLOVER (0xFF50)// Plover HID vendor-defined usage page diff --git a/app/include/dt-bindings/zmk/keys.h b/app/include/dt-bindings/zmk/keys.h index 3e67c402..7a2d65fe 100644 --- a/app/include/dt-bindings/zmk/keys.h +++ b/app/include/dt-bindings/zmk/keys.h @@ -1439,3 +1439,68 @@ #define C_KEYBOARD_INPUT_ASSIST_CANCEL \ (ZMK_HID_USAGE(HID_USAGE_CONSUMER, HID_USAGE_CONSUMER_KEYBOARD_INPUT_ASSIST_CANCEL)) #define C_KBIA_CANCEL (C_KEYBOARD_INPUT_ASSIST_CANCEL) + +#define PLV_SL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 0)) +#define PLV_TL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 1)) +#define PLV_KL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 2)) +#define PLV_PL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 3)) +#define PLV_WL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 4)) +#define PLV_HL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 5)) +#define PLV_RL (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 6)) +#define PLV_A (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 7)) +#define PLV_O (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 8)) +#define PLV_ST (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 9)) +#define PLV_E (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 10)) +#define PLV_U (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 11)) +#define PLV_FR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 12)) +#define PLV_RR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 13)) +#define PLV_PR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 14)) +#define PLV_BR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 15)) +#define PLV_LR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 16)) +#define PLV_GR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 17)) +#define PLV_TR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 18)) +#define PLV_SR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 19)) +#define PLV_DR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 20)) +#define PLV_ZR (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 21)) +#define PLV_NM (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 22)) +#define PLV_X1 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 23)) +#define PLV_X2 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 24)) +#define PLV_X3 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 25)) +#define PLV_X4 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 26)) +#define PLV_X5 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 27)) +#define PLV_X6 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 28)) +#define PLV_X7 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 29)) +#define PLV_X8 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 30)) +#define PLV_X9 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 31)) +#define PLV_X10 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 32)) +#define PLV_X11 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 33)) +#define PLV_X12 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 34)) +#define PLV_X13 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 35)) +#define PLV_X14 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 36)) +#define PLV_X15 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 37)) +#define PLV_X16 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 38)) +#define PLV_X17 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 39)) +#define PLV_X18 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 40)) +#define PLV_X19 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 41)) +#define PLV_X20 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 42)) +#define PLV_X21 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 43)) +#define PLV_X22 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 44)) +#define PLV_X23 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 45)) +#define PLV_X24 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 46)) +#define PLV_X25 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 47)) +#define PLV_X26 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 48)) +#define PLV_X27 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 49)) +#define PLV_X28 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 50)) +#define PLV_X29 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 51)) +#define PLV_X30 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 52)) +#define PLV_X31 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 53)) +#define PLV_X32 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 54)) +#define PLV_X33 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 55)) +#define PLV_X34 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 56)) +#define PLV_X35 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 57)) +#define PLV_X36 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 58)) +#define PLV_X37 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 59)) +#define PLV_X38 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 60)) +#define PLV_X39 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 61)) +#define PLV_X40 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 62)) +#define PLV_X41 (ZMK_HID_USAGE(HID_USAGE_VENDOR_PLOVER, 63)) diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 01104d1c..e336410c 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -17,6 +17,18 @@ #define COLLECTION_REPORT 0x03 +#define ZMK_HID_KEYBOARD_NKRO_SIZE 6 + +#define ZMK_HID_CONSUMER_NKRO_SIZE 6 + +#define ZMK_HID_PLOVER_SIZE 8 + +// As a workaround for limitations in how some operating systems expose hid +// descriptors to user level code the Plover HID protocol hard codes a report +// id of 0x50 so that the plover side can distinguish between Plover HID +// reports and other reports from the device. +#define PLOVER_HID_REPORT_ID 0x50 + static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_KEYBOARD), @@ -89,6 +101,34 @@ static const uint8_t zmk_hid_report_desc[] = { /* INPUT (Data,Ary,Abs) */ HID_INPUT(0x00), HID_END_COLLECTION, + +#if IS_ENABLED(CONFIG_ZMK_PLOVER_HID) + /* USAGE (\xffPLV) */ + HID_ITEM(HID_ITEM_TAG_USAGE_PAGE, HID_ITEM_TYPE_GLOBAL, 2), + 0x50, 0xff, + HID_ITEM(HID_ITEM_TAG_USAGE, HID_ITEM_TYPE_LOCAL, 2), + 0x56, 0x4c, + HID_COLLECTION(HID_COLLECTION_APPLICATION), + /* REPORT ID (80) */ + HID_REPORT_ID(PLOVER_HID_REPORT_ID), + /* LOGICAL MINIMUM (0) */ + HID_LOGICAL_MIN8(0x00), + /* LOGICAL MAXIMUM (1) */ + HID_LOGICAL_MAX8(0x01), + /* REPORT SIZE (1) */ + HID_REPORT_SIZE(0x01), + /* REPORT COUNT (64) */ + HID_REPORT_COUNT(0x40), + /* USAGE PAGE (Ordinal) */ + HID_USAGE_PAGE(0x0A), + /* USAGE MINIMUM (0) */ + HID_USAGE_MIN8(0x00), + /* USAGE MAXIMUM (63) */ + HID_USAGE_MAX8(63), + /* INPUT (Cnst,Var,Abs) */ + HID_INPUT(0x02), + HID_END_COLLECTION +#endif /* IS_ENABLED(CONFIG_ZMK_PLOVER_HID) */ }; // struct zmk_hid_boot_report @@ -126,6 +166,15 @@ struct zmk_hid_consumer_report { struct zmk_hid_consumer_report_body body; } __packed; +struct zmk_hid_plover_report_body { + uint8_t buttons[ZMK_HID_PLOVER_SIZE]; +} __packed; + +struct zmk_hid_plover_report { + uint8_t report_id; + struct zmk_hid_plover_report_body body; +} __packed; + zmk_mod_flags_t zmk_hid_get_explicit_mods(); int zmk_hid_register_mod(zmk_mod_t modifier); int zmk_hid_unregister_mod(zmk_mod_t modifier); @@ -152,5 +201,11 @@ int zmk_hid_press(uint32_t usage); int zmk_hid_release(uint32_t usage); bool zmk_hid_is_pressed(uint32_t usage); +int zmk_hid_plover_press(zmk_key_t key); +int zmk_hid_plover_release(zmk_key_t key); +void zmk_hid_plover_clear(); + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(); struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(); + +struct zmk_hid_plover_report *zmk_hid_get_plover_report(); diff --git a/app/include/zmk/hog.h b/app/include/zmk/hog.h index 7523fb66..481b7975 100644 --- a/app/include/zmk/hog.h +++ b/app/include/zmk/hog.h @@ -13,3 +13,4 @@ int zmk_hog_init(); int zmk_hog_send_keyboard_report(struct zmk_hid_keyboard_report_body *body); int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *body); +int zmk_hog_send_plover_report(struct zmk_hid_plover_report_body *body); diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 33760010..9ec540f2 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -130,6 +130,35 @@ static int send_consumer_report() { } } +#if IS_ENABLED(CONFIG_ZMK_PLOVER_HID) +static int send_plover_report() { + struct zmk_hid_plover_report *plover_report = zmk_hid_get_plover_report(); + switch (current_endpoint) { +#if IS_ENABLED(CONFIG_ZMK_USB) + case ZMK_ENDPOINT_USB: { + int err = zmk_usb_hid_send_report((uint8_t *)plover_report, sizeof(*plover_report)); + if (err) { + LOG_ERR("FAILED TO SEND OVER USB: %d", err); + } + return err; + } +#endif /* IS_ENABLED(CONFIG_ZMK_USB) */ +#if IS_ENABLED(CONFIG_ZMK_BLE) + case ZMK_ENDPOINT_BLE: { + int err = zmk_hog_send_plover_report(&plover_report->body); + if (err) { + LOG_ERR("FAILED TO SEND OVER HOG: %d", err); + } + return err; + } +#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ + default: + LOG_ERR("Unsupported endpoint %d", current_endpoint); + return -ENOTSUP; + } +} +#endif /* IS_ENABLED(CONFIG_ZMK_PLOVER_HID) */ + int zmk_endpoints_send_report(uint16_t usage_page) { LOG_DBG("usage page 0x%02X", usage_page); @@ -138,6 +167,12 @@ int zmk_endpoints_send_report(uint16_t usage_page) { return send_keyboard_report(); case HID_USAGE_CONSUMER: return send_consumer_report(); + // FIXME: we should not and the usage page here, we should make sure + // the whole page gets here +#if IS_ENABLED(CONFIG_ZMK_PLOVER_HID) + case (HID_USAGE_VENDOR_PLOVER & 0xFF): + return send_plover_report(); +#endif /* IS_ENABLED(CONFIG_ZMK_PLOVER_HID) */ default: LOG_ERR("Unsupported usage page %d", usage_page); return -ENOTSUP; @@ -228,9 +263,11 @@ static enum zmk_endpoint get_selected_endpoint() { static void disconnect_current_endpoint() { zmk_hid_keyboard_clear(); zmk_hid_consumer_clear(); + zmk_hid_plover_clear(); zmk_endpoints_send_report(HID_USAGE_KEY); zmk_endpoints_send_report(HID_USAGE_CONSUMER); + zmk_endpoints_send_report(HID_USAGE_VENDOR_PLOVER); } static void update_current_endpoint() { diff --git a/app/src/hid.c b/app/src/hid.c index b66a910d..c932840d 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -16,6 +16,8 @@ static struct zmk_hid_keyboard_report keyboard_report = { static struct zmk_hid_consumer_report consumer_report = {.report_id = 2, .body = {.keys = {0}}}; +static struct zmk_hid_plover_report plover_report = {.report_id = PLOVER_HID_REPORT_ID, .body = {.buttons = {0}}}; + // Keep track of how often a modifier was pressed. // Only release the modifier if the count is 0. static int explicit_modifier_counts[8] = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -240,6 +242,8 @@ int zmk_hid_press(uint32_t usage) { return zmk_hid_keyboard_press(ZMK_HID_USAGE_ID(usage)); case HID_USAGE_CONSUMER: return zmk_hid_consumer_press(ZMK_HID_USAGE_ID(usage)); + case HID_USAGE_VENDOR_PLOVER & 0xFF: + return zmk_hid_plover_press(ZMK_HID_USAGE_ID(usage)); } return -EINVAL; } @@ -250,6 +254,8 @@ int zmk_hid_release(uint32_t usage) { return zmk_hid_keyboard_release(ZMK_HID_USAGE_ID(usage)); case HID_USAGE_CONSUMER: return zmk_hid_consumer_release(ZMK_HID_USAGE_ID(usage)); + case HID_USAGE_VENDOR_PLOVER & 0xFF: + return zmk_hid_plover_release(ZMK_HID_USAGE_ID(usage)); } return -EINVAL; } @@ -264,6 +270,18 @@ bool zmk_hid_is_pressed(uint32_t usage) { return false; } +int zmk_hid_plover_press(zmk_key_t code) { + plover_report.body.buttons[code / 8] |= ( 1 << (7 - (code % 8))); + return 0; +}; + +int zmk_hid_plover_release(zmk_key_t code) { + plover_report.body.buttons[code / 8] &= ~( 1 << (7 - (code % 8))); + return 0; +}; + +void zmk_hid_plover_clear() { memset(&plover_report.body, 0, sizeof(plover_report.body)); } + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { return &keyboard_report; } @@ -271,3 +289,7 @@ struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { struct zmk_hid_consumer_report *zmk_hid_get_consumer_report() { return &consumer_report; } + +struct zmk_hid_plover_report *zmk_hid_get_plover_report() { + return &plover_report; +} diff --git a/app/src/hid_listener.c b/app/src/hid_listener.c index e233b0b8..c96b5b07 100644 --- a/app/src/hid_listener.c +++ b/app/src/hid_listener.c @@ -28,7 +28,7 @@ static int hid_listener_keycode_pressed(const struct zmk_keycode_state_changed * } explicit_mods_changed = zmk_hid_register_mods(ev->explicit_modifiers); implicit_mods_changed = zmk_hid_implicit_modifiers_press(ev->implicit_modifiers); - if (ev->usage_page != HID_USAGE_KEY && + if (ev->usage_page != HID_USAGE_KEY && ev->usage_page != (HID_USAGE_VENDOR_PLOVER & 0xFF) && (explicit_mods_changed > 0 || implicit_mods_changed > 0)) { err = zmk_endpoints_send_report(HID_USAGE_KEY); if (err < 0) { @@ -59,7 +59,7 @@ static int hid_listener_keycode_released(const struct zmk_keycode_state_changed // active and only releasing modifiers at that time. implicit_mods_changed = zmk_hid_implicit_modifiers_release(); ; - if (ev->usage_page != HID_USAGE_KEY && + if (ev->usage_page != HID_USAGE_KEY && ev->usage_page != (HID_USAGE_VENDOR_PLOVER & 0xFF) && (explicit_mods_changed > 0 || implicit_mods_changed > 0)) { err = zmk_endpoints_send_report(HID_USAGE_KEY); if (err < 0) { diff --git a/app/src/hog.c b/app/src/hog.c index 3dd3e874..f0e088be 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -56,6 +56,11 @@ static struct hids_report consumer_input = { .type = HIDS_INPUT, }; +static struct hids_report plover_input = { + .id = 0x50, + .type = HIDS_INPUT, +}; + static bool host_requests_notification = false; static uint8_t ctrl_point; // static uint8_t proto_mode; @@ -93,6 +98,14 @@ static ssize_t read_hids_consumer_input_report(struct bt_conn *conn, sizeof(struct zmk_hid_consumer_report_body)); } +static ssize_t read_hids_plover_input_report(struct bt_conn *conn, + const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) { + struct zmk_hid_plover_report_body *report_body = &zmk_hid_get_plover_report()->body; + return bt_gatt_attr_read(conn, attr, buf, len, offset, report_body, + sizeof(struct zmk_hid_plover_report_body)); +} + // static ssize_t write_proto_mode(struct bt_conn *conn, // const struct bt_gatt_attr *attr, // const void *buf, uint16_t len, uint16_t offset, @@ -139,6 +152,11 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, NULL, &consumer_input), + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ_ENCRYPT, read_hids_plover_input_report, NULL, NULL), + BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), + BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, + NULL, &plover_input), BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT, BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE, NULL, write_ctrl_point, &ctrl_point)); @@ -261,6 +279,59 @@ int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { return 0; }; +K_MSGQ_DEFINE(zmk_hog_plover_msgq, sizeof(struct zmk_hid_plover_report_body), + CONFIG_ZMK_BLE_PLOVER_HID_REPORT_QUEUE_SIZE, 4); + +void send_plover_report_callback(struct k_work *work) { + struct zmk_hid_plover_report_body report; + + while (k_msgq_get(&zmk_hog_plover_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + // FIXME: Try to understand this offset calculation, because I just brute-forced it + // until it worked. + // 13 seems to be working, but might be wrong + // or 14 + .attr = &hog_svc.attrs[13], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err) { + LOG_DBG("Error notifying %d", err); + } + bt_conn_unref(conn); + } +} + +K_WORK_DEFINE(hog_plover_work, send_plover_report_callback); + +int zmk_hog_send_plover_report(struct zmk_hid_plover_report_body *report) { + int err = k_msgq_put(&zmk_hog_plover_msgq, report, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Plover message queue full, popping first message and queueing again"); + struct zmk_hid_plover_report_body discarded_report; + k_msgq_get(&zmk_hog_plover_msgq, &discarded_report, K_NO_WAIT); + return zmk_hog_send_plover_report(report); + } + default: + LOG_WRN("Failed to queue plover report to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&hog_work_q, &hog_plover_work); + + return 0; +} + int zmk_hog_init(const struct device *_arg) { static const struct k_work_queue_config queue_config = {.name = "HID Over GATT Send Work"}; k_work_queue_start(&hog_work_q, hog_q_stack, K_THREAD_STACK_SIZEOF(hog_q_stack), diff --git a/docs/docs/features/stenography.md b/docs/docs/features/stenography.md new file mode 100644 index 00000000..d92addc7 --- /dev/null +++ b/docs/docs/features/stenography.md @@ -0,0 +1,46 @@ +--- +title: Stenography +--- + +## Summary + +Stenography is a way of writing used by Court Reporters and live captioners to +write at over 200 words per minute, using a chorded keyboard. + +The [Open Steno Project](https://www.openstenoproject.org) has developed an +open-source application named Plover to translate steno strokes. + +Plover supports any NKRO keyboard, but to not have to enable/disable Plover all +the time when wanting to use a normal keyboard usually serial stenography +protocols are used for many hobbyist steno machines. However we can't use those +protocols over BLE, so instead ZMK comes with an implementation of the [Plover +HID](https://github.com/dnaq/plover-machine-hid) protocol that can be used over both +USB and BLE and doesn't require any driver support from the operating system. + +### Configuration + +First enable the Plover HID protocol in your keymaps `.conf` file by adding the line: + +``` +CONFIG_ZMK_PLOVER_HID=y +``` + +Then add normal keypress behaviors to your keymap, but with usage codes that start with `PLV_`, .e.g. + +``` +plover_layer { + bindings = < + &kp &PLV_S1 &kp PLV_TL &kp PLV_PL &kp PLV_HL &kp PLV_ST1 &PLV_ST3 &kp PLV_LR &kp PLV_TR &kp PLV_DR + &kp &PLV_S2 &kp PLV_KL &kp PLV_WL &kp PLV_RL &kp PLV_ST2 &PLV_ST4 &kp PLV_GR &kp PLV_SR &kp PLV_ZR + >; +}; +``` + +To support alternate stenography systems the Plover HID protocol also supports the keys `PLV_X1` to `PLV_X26`. + +After reflashing your keyboard with a stenography enabled keymap you might need to restart your computer or restart the bluetooth +service so that it updates the HID descriptors of your computer. + +Then install the +[plover-machine-hid](https://github.com/dnaq/plover-machine-hid) plugin to +plover according to its installation instructions. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 142dcafc..0a66afa3 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -33,6 +33,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | One Shot Keys | ✅ | ✅ | ✅ | | [Combo Keys](features/combos.md) | ✅ | | ✅ | | [Macros](behaviors/macros.md) | ✅ | ✅ | ✅ | +| [Stenography](features/stenography.md) | ✅ | | ✅ | | Mouse Keys | 🚧 | ✅ | ✅ | | Low Active Power Usage | ✅ | | | | Low Power Sleep States | ✅ | ✅ | |