From d3065d732579f157a6d1cbc19f5dc6b0df4740be Mon Sep 17 00:00:00 2001 From: apewo Date: Sun, 12 May 2024 00:53:35 +0200 Subject: [PATCH] programmable buttons --- app/CMakeLists.txt | 3 + app/Kconfig | 13 +++ app/dts/behaviors.dtsi | 1 + app/dts/behaviors/programmable_buttons.dtsi | 14 ++++ .../zmk,behavior-programmable-buttons.yaml | 8 ++ app/include/zmk/endpoints.h | 5 ++ .../programmable_button_state_changed.h | 24 ++++++ app/include/zmk/hid.h | 37 +++++++++ app/include/zmk/hog.h | 4 + app/include/zmk/usb_hid.h | 3 + .../behaviors/behavior_programmable_buttons.c | 52 ++++++++++++ app/src/endpoints.c | 34 ++++++++ .../programmable_button_state_changed.c | 9 +++ app/src/hid.c | 32 ++++++++ app/src/hog.c | 80 +++++++++++++++++++ app/src/programmable_buttons.c | 42 ++++++++++ app/src/usb_hid.c | 13 +++ docs/docs/behaviors/programmable-buttons.md | 42 ++++++++++ 18 files changed, 416 insertions(+) create mode 100644 app/dts/behaviors/programmable_buttons.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-programmable-buttons.yaml create mode 100644 app/include/zmk/events/programmable_button_state_changed.h create mode 100644 app/src/behaviors/behavior_programmable_buttons.c create mode 100644 app/src/events/programmable_button_state_changed.c create mode 100644 app/src/programmable_buttons.c create mode 100644 docs/docs/behaviors/programmable-buttons.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0b681ea9..b4639979 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources(app PRIVATE src/events/activity_state_changed.c) target_sources(app PRIVATE src/events/position_state_changed.c) target_sources(app PRIVATE src/events/sensor_event.c) target_sources(app PRIVATE src/events/mouse_button_state_changed.c) +target_sources_ifdef(CONFIG_ZMK_PROGRAMMABLE_BUTTONS app PRIVATE src/events/programmable_button_state_changed.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/events/wpm_state_changed.c) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/events/usb_conn_state_changed.c) target_sources(app PRIVATE src/behaviors/behavior_reset.c) @@ -41,6 +42,7 @@ target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SOFT_OFF app PRIVATE src/behaviors/beha if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/hid.c) target_sources_ifdef(CONFIG_ZMK_MOUSE app PRIVATE src/mouse.c) + target_sources_ifdef(CONFIG_ZMK_PROGRAMMABLE_BUTTONS app PRIVATE src/programmable_buttons.c) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) target_sources(app PRIVATE src/behaviors/behavior_hold_tap.c) @@ -59,6 +61,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON app PRIVATE src/behaviors/behavior_sensor_rotate_common.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MOUSE_KEY_PRESS app PRIVATE src/behaviors/behavior_mouse_key_press.c) + target_sources_ifdef(CONFIG_ZMK_PROGRAMMABLE_BUTTONS app PRIVATE src/behaviors/behavior_programmable_buttons.c) target_sources(app PRIVATE src/combo.c) target_sources(app PRIVATE src/behaviors/behavior_tap_dance.c) target_sources(app PRIVATE src/behavior_queue.c) diff --git a/app/Kconfig b/app/Kconfig index 8f2fe837..f905fd01 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -213,6 +213,10 @@ config ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE int "Max number of mouse HID reports to queue for sending over BLE" default 20 +config ZMK_BLE_PROGRAMMABLE_BUTTONS_REPORT_QUEUE_SIZE + int "Max number of programmable buttons HID reports to queue for sending over BLE" + default 20 + config ZMK_BLE_CLEAR_BONDS_ON_START bool "Configuration that clears all bond information from the keyboard on startup." @@ -367,6 +371,15 @@ endif #Display/LED Options endmenu +menu "Programmable Buttons" + +config ZMK_PROGRAMMABLE_BUTTONS + bool "Enable programmable buttons" + default n + +#Mouse Options +endmenu + menu "Mouse Options" config ZMK_MOUSE diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index fde75271..9c55b518 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -21,3 +21,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/programmable_buttons.dtsi b/app/dts/behaviors/programmable_buttons.dtsi new file mode 100644 index 00000000..b701f15b --- /dev/null +++ b/app/dts/behaviors/programmable_buttons.dtsi @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + /omit-if-no-ref/ pb: programmable_buttons { + compatible = "zmk,behavior-programmable-buttons"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-programmable-buttons.yaml b/app/dts/bindings/behaviors/zmk,behavior-programmable-buttons.yaml new file mode 100644 index 00000000..8bfed366 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-programmable-buttons.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2020 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Programmable buttons press/release behavior + +compatible: "zmk,behavior-programmable-buttons" + +include: one_param.yaml diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index f2aff2bc..eff9d8b6 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -70,6 +70,11 @@ struct zmk_endpoint_instance zmk_endpoints_selected(void); int zmk_endpoints_send_report(uint16_t usage_page); + +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_endpoints_send_programmable_buttons_report(); +#endif // IS_ENABLE(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_endpoints_send_mouse_report(); #endif // IS_ENABLE(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/events/programmable_button_state_changed.h b/app/include/zmk/events/programmable_button_state_changed.h new file mode 100644 index 00000000..0ede0bac --- /dev/null +++ b/app/include/zmk/events/programmable_button_state_changed.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_programmable_button_state_changed { + uint8_t index; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_programmable_button_state_changed); + +static inline int raise_zmk_programmable_button_state_changed_from_encoded(uint8_t index, bool pressed, + int64_t timestamp) { + return raise_zmk_programmable_button_state_changed((struct zmk_programmable_button_state_changed){ + .index = index, .state = pressed, .timestamp = timestamp}); +} diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 41f559b5..d2470747 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -60,6 +60,7 @@ #define ZMK_HID_REPORT_ID_LEDS 0x01 #define ZMK_HID_REPORT_ID_CONSUMER 0x02 #define ZMK_HID_REPORT_ID_MOUSE 0x03 +#define ZMK_HID_REPORT_ID_PROGRAMMABLE_BUTTONS 0x04 static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), @@ -145,6 +146,25 @@ static const uint8_t zmk_hid_report_desc[] = { HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), HID_END_COLLECTION, +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + HID_USAGE_PAGE(HID_USAGE_CONSUMER), + HID_USAGE(HID_USAGE_CONSUMER_CONSUMER_CONTROL), + HID_COLLECTION(HID_COLLECTION_APPLICATION), + HID_REPORT_ID(ZMK_HID_REPORT_ID_PROGRAMMABLE_BUTTONS), + HID_USAGE(HID_USAGE_CONSUMER_PROGRAMMABLE_BUTTONS), + HID_COLLECTION(HID_COLLECTION_NAMED_ARRAY), + HID_USAGE_PAGE(HID_USAGE_BUTTON), + HID_USAGE_MIN8(0x1), + HID_USAGE_MAX8(0x20), + HID_LOGICAL_MIN8(0x00), + HID_LOGICAL_MAX8(0x01), + HID_REPORT_SIZE(0x01), + HID_REPORT_COUNT(0x20), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + HID_END_COLLECTION, + HID_END_COLLECTION, +#endif //IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) HID_USAGE_PAGE(HID_USAGE_GD), HID_USAGE(HID_USAGE_GD_MOUSE), @@ -239,6 +259,13 @@ struct zmk_hid_consumer_report { struct zmk_hid_consumer_report_body body; } __packed; +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +struct zmk_hid_programmable_buttons_report { + uint8_t report_id; + uint32_t body; +} __packed; +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) struct zmk_hid_mouse_report_body { zmk_mouse_button_flags_t buttons; @@ -280,6 +307,12 @@ int zmk_hid_press(uint32_t usage); int zmk_hid_release(uint32_t usage); bool zmk_hid_is_pressed(uint32_t usage); +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_hid_programmable_button_press(uint8_t index); +int zmk_hid_programmable_button_release(uint8_t index); +void zmk_hid_programmable_buttons_clear(void); +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_hid_mouse_button_press(zmk_mouse_button_t button); int zmk_hid_mouse_button_release(zmk_mouse_button_t button); @@ -295,6 +328,10 @@ struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void); zmk_hid_boot_report_t *zmk_hid_get_boot_report(); #endif +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +struct zmk_hid_programmable_buttons_report *zmk_hid_get_programmable_buttons_report(void); +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(); #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/hog.h b/app/include/zmk/hog.h index eb6e653f..ce34fd7d 100644 --- a/app/include/zmk/hog.h +++ b/app/include/zmk/hog.h @@ -12,6 +12,10 @@ 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); +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_hog_send_programmable_buttons_report(uint32_t body); +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *body); #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/usb_hid.h b/app/include/zmk/usb_hid.h index c0cbc08a..405131c1 100644 --- a/app/include/zmk/usb_hid.h +++ b/app/include/zmk/usb_hid.h @@ -10,6 +10,9 @@ int zmk_usb_hid_send_keyboard_report(void); int zmk_usb_hid_send_consumer_report(void); +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_usb_hid_send_programmable_buttons_report(void); +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_usb_hid_send_mouse_report(void); #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/src/behaviors/behavior_programmable_buttons.c b/app/src/behaviors/behavior_programmable_buttons.c new file mode 100644 index 00000000..3b2658e1 --- /dev/null +++ b/app/src/behaviors/behavior_programmable_buttons.c @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_programmable_buttons + +// Dependencies +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("programmable button : position %d keycode 0x%02X", event.position, binding->param1); + + return raise_zmk_programmable_button_state_changed_from_encoded(binding->param1, true, + event.timestamp); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("programmable button : position %d keycode 0x%02X", event.position, binding->param1); + return raise_zmk_programmable_button_state_changed_from_encoded(binding->param1, false, + event.timestamp); +} + +// Initialization Function +static int behavior_programmable_button_init(const struct device *dev) { + return 0; +}; + +// API Structure +static const struct behavior_driver_api behavior_programmable_button_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define KP_INST(n) \ + BEHAVIOR_DT_INST_DEFINE(n, behavior_programmable_button_init, NULL, NULL, NULL, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_programmable_button_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 7c9d15a3..1ba206b6 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -203,6 +203,37 @@ int zmk_endpoints_send_report(uint16_t usage_page) { return -ENOTSUP; } + +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_endpoints_send_programmable_buttons_report() { + switch (current_instance.transport) { +#if IS_ENABLED(CONFIG_ZMK_USB) + case ZMK_TRANSPORT_USB: { + int err = zmk_usb_hid_send_programmable_buttons_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_TRANSPORT_BLE: { + struct zmk_hid_programmable_buttons_report *programmable_buttons_report = zmk_hid_get_programmable_buttons_report(); + int err = zmk_hog_send_programmable_buttons_report(programmable_buttons_report->body); + if (err) { + LOG_ERR("FAILED TO SEND OVER HOG: %d", err); + } + return err; + } +#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ + } + + LOG_ERR("Unsupported endpoint transport %d", current_instance.transport); + return -ENOTSUP; +} +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_endpoints_send_mouse_report() { switch (current_instance.transport) { @@ -343,6 +374,9 @@ static int zmk_endpoints_init(void) { void zmk_endpoints_clear_current(void) { zmk_hid_keyboard_clear(); zmk_hid_consumer_clear(); +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + zmk_hid_programmable_buttons_clear(); +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) #if IS_ENABLED(CONFIG_ZMK_MOUSE) zmk_hid_mouse_clear(); #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/src/events/programmable_button_state_changed.c b/app/src/events/programmable_button_state_changed.c new file mode 100644 index 00000000..fa66b809 --- /dev/null +++ b/app/src/events/programmable_button_state_changed.c @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +ZMK_EVENT_IMPL(zmk_programmable_button_state_changed); diff --git a/app/src/hid.c b/app/src/hid.c index 582db676..ae1a2972 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -18,6 +18,11 @@ static struct zmk_hid_keyboard_report keyboard_report = { static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER, .body = {.keys = {0}}}; +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +static struct zmk_hid_programmable_buttons_report programmable_buttons_report = {.report_id = ZMK_HID_REPORT_ID_PROGRAMMABLE_BUTTONS, + .body = 0}; +#endif /* IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) */ + #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) static zmk_hid_boot_report_t boot_report = {.modifiers = 0, ._reserved = 0, .keys = {0}}; @@ -369,6 +374,27 @@ bool zmk_hid_is_pressed(uint32_t usage) { return false; } + +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + +int zmk_hid_programmable_button_press(uint8_t index){ + if (index < 1 || index > 32) { + return -EINVAL; + } + WRITE_BIT(programmable_buttons_report.body, index - 1, true); + return 0; +} +int zmk_hid_programmable_button_release(uint8_t index){ + if (index < 1 || index > 32) { + return -EINVAL; + } + WRITE_BIT(programmable_buttons_report.body, index -1, false); + return 0; +} + +void zmk_hid_programmable_buttons_clear(void) { programmable_buttons_report.body = 0; } +#endif /* IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) */ + #if IS_ENABLED(CONFIG_ZMK_MOUSE) // Keep track of how often a button was pressed. @@ -442,6 +468,12 @@ struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void) { return &consumer_report; } +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +struct zmk_hid_programmable_buttons_report *zmk_hid_get_programmable_buttons_report(void) { + return &programmable_buttons_report; +} +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(void) { diff --git a/app/src/hog.c b/app/src/hog.c index f17f759c..dd3d1e53 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -69,6 +69,15 @@ static struct hids_report consumer_input = { .type = HIDS_INPUT, }; +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + +static struct hids_report programmable_buttons_input = { + .id = ZMK_HID_REPORT_ID_PROGRAMMABLE_BUTTONS, + .type = HIDS_INPUT, +}; + +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) static struct hids_report mouse_input = { @@ -143,6 +152,15 @@ static ssize_t read_hids_consumer_input_report(struct bt_conn *conn, sizeof(struct zmk_hid_consumer_report_body)); } +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +static ssize_t read_hids_programmable_buttons_input_report(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) { + uint32_t report_body = zmk_hid_get_programmable_buttons_report()->body; + return bt_gatt_attr_read(conn, attr, buf, len, offset, &report_body, + sizeof(uint32_t)); +} +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) static ssize_t read_hids_mouse_input_report(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { @@ -200,6 +218,14 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, NULL, &consumer_input), +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ_ENCRYPT, read_hids_programmable_buttons_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, &programmable_buttons_input), +#endif // IS_ENABLED(CONFIG_ZMK_PROGAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ_ENCRYPT, read_hids_mouse_input_report, NULL, NULL), @@ -343,6 +369,60 @@ int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { return 0; }; +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + +K_MSGQ_DEFINE(zmk_hog_programmable_buttons_msgq, sizeof(uint32_t), + CONFIG_ZMK_BLE_PROGRAMMABLE_BUTTONS_REPORT_QUEUE_SIZE, 4); + +void send_programmable_buttons_report_callback(struct k_work *work) { + uint32_t report; + while (k_msgq_get(&zmk_hog_programmable_buttons_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &hog_svc.attrs[13], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err == -EPERM) { + bt_conn_set_security(conn, BT_SECURITY_L2); + } else if (err) { + LOG_DBG("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +}; + +K_WORK_DEFINE(hog_programmable_buttons_work, send_programmable_buttons_report_callback); + +int zmk_hog_send_programmable_buttons_report(uint32_t report) { + int err = k_msgq_put(&zmk_hog_programmable_buttons_msgq, &report, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Consumer message queue full, popping first message and queueing again"); + uint32_t discarded_report; + k_msgq_get(&zmk_hog_programmable_buttons_msgq, &discarded_report, K_NO_WAIT); + return zmk_hog_send_programmable_buttons_report(report); + } + default: + LOG_WRN("Failed to queue mouse report to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&hog_work_q, &hog_programmable_buttons_work); + + return 0; +}; +#endif //IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) K_MSGQ_DEFINE(zmk_hog_mouse_msgq, sizeof(struct zmk_hid_mouse_report_body), diff --git a/app/src/programmable_buttons.c b/app/src/programmable_buttons.c new file mode 100644 index 00000000..b21dd072 --- /dev/null +++ b/app/src/programmable_buttons.c @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +static void listener_programmable_button_pressed(const struct zmk_programmable_button_state_changed *ev) { + LOG_DBG("programmable button event press: 0x%02X", ev->index); + zmk_hid_programmable_button_press(ev->index); + zmk_endpoints_send_programmable_buttons_report(); +} + +static void listener_programmable_button_released(const struct zmk_programmable_button_state_changed *ev) { + LOG_DBG("programmable button event released: 0x%02X", ev->index); + zmk_hid_programmable_button_release(ev->index); + zmk_endpoints_send_programmable_buttons_report(); +} + +int programmable_buttons_listener(const zmk_event_t *eh) { + const struct zmk_programmable_button_state_changed *pb_ev = as_zmk_programmable_button_state_changed(eh); + if (pb_ev) { + if (pb_ev->state) { + listener_programmable_button_pressed(pb_ev); + } else { + listener_programmable_button_released(pb_ev); + } + return 0; + } + return 0; +} + +ZMK_LISTENER(programmable_buttons_listener, programmable_buttons_listener); +ZMK_SUBSCRIPTION(programmable_buttons_listener, zmk_programmable_button_state_changed); diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index cd3ef920..aa31f6fe 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -164,6 +164,19 @@ int zmk_usb_hid_send_consumer_report(void) { return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); } +#if IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) +int zmk_usb_hid_send_programmable_buttons_report(void) { +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + if (hid_protocol == HID_PROTOCOL_BOOT) { + return -ENOTSUP; + } +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + + struct zmk_hid_programmable_buttons_report *report = zmk_hid_get_programmable_buttons_report(); + return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); +} +#endif // IS_ENABLED(CONFIG_ZMK_PROGRAMMABLE_BUTTONS) + #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_usb_hid_send_mouse_report() { #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) diff --git a/docs/docs/behaviors/programmable-buttons.md b/docs/docs/behaviors/programmable-buttons.md new file mode 100644 index 00000000..94903d80 --- /dev/null +++ b/docs/docs/behaviors/programmable-buttons.md @@ -0,0 +1,42 @@ +--- +title: Programmable Buttons Behaviors +sidebar_label: Programmable Buttons +--- + +## Summary + +TODO: Programmable Buttons can be used to configure shortcuts that will not overlap with other softwares inputs. As per hid specification they are typically used to +control software applications or GUI objects. + +:::warning[OS Compatibility] + +Only Linux is known to have programmable buttons compatibility + +::: + +## Configuration Option + +This feature can be enabled or disabled explicitly via a config option: + +``` +CONFIG_ZMK_PROGRAMMABLE_BUTTONS=y +``` + +## Programmable Button Press + +This behavior can press/release up to 32 programmable buttons. + +### Behavior Binding + +- Reference: `&pb` +- Parameter: A `uint8` from 1 up to 32 + + +### Examples + +The following will send the programmable button 1 + +``` +&pb 1 +``` +