diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 793f386dbe30..0891364b4377 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -31,12 +31,14 @@ target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/ext_power_generic.c) 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_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) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) 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(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) @@ -54,6 +56,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE app PRIVATE src/behaviors/behavior_sensor_rotate.c) 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(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 798cb7855d53..f864b18c6343 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -154,6 +154,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_MOUSE_REPORT_QUEUE_SIZE + int "Max number of mouse 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." default n @@ -313,6 +317,15 @@ endif #Display/LED Options endmenu +menu "Mouse Options" + +config ZMK_MOUSE + bool "Enable ZMK mouse emulation" + default n + +#Mouse Options +endmenu + menu "Power Management" config ZMK_BATTERY_REPORTING diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index 9e4a82b06e7e..11bc8c5900f1 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -6,6 +6,11 @@ config ZMK_BEHAVIOR_KEY_TOGGLE default y depends on DT_HAS_ZMK_BEHAVIOR_KEY_TOGGLE_ENABLED +config ZMK_BEHAVIOR_MOUSE_KEY_PRESS + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_MOUSE_KEY_PRESS_ENABLED + imply ZMK_MOUSE config ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON bool diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index b3502cbbc134..23f2fee28068 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -18,4 +18,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include diff --git a/app/dts/behaviors/mouse_key_press.dtsi b/app/dts/behaviors/mouse_key_press.dtsi new file mode 100644 index 000000000000..9cc16e81e755 --- /dev/null +++ b/app/dts/behaviors/mouse_key_press.dtsi @@ -0,0 +1,9 @@ +/ { + behaviors { + /omit-if-no-ref/ mkp: behavior_mouse_key_press { + compatible = "zmk,behavior-mouse-key-press"; + label = "MOUSE_KEY_PRESS"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml b/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml new file mode 100644 index 000000000000..8540916b72a7 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml @@ -0,0 +1,5 @@ +description: Mouse key press/release behavior + +compatible: "zmk,behavior-mouse-key-press" + +include: one_param.yaml diff --git a/app/include/dt-bindings/zmk/hid_usage_pages.h b/app/include/dt-bindings/zmk/hid_usage_pages.h index 2ccdba5540f6..7fa54fd88b90 100644 --- a/app/include/dt-bindings/zmk/hid_usage_pages.h +++ b/app/include/dt-bindings/zmk/hid_usage_pages.h @@ -26,6 +26,7 @@ #define HID_USAGE_GDV (0x06) // Generic Device Controls #define HID_USAGE_KEY (0x07) // Keyboard/Keypad #define HID_USAGE_LED (0x08) // LED +#define HID_USAGE_BUTTON (0x09) // Button #define HID_USAGE_TELEPHONY (0x0B) // Telephony Device #define HID_USAGE_CONSUMER (0x0C) // Consumer #define HID_USAGE_DIGITIZERS (0x0D) // Digitizers diff --git a/app/include/dt-bindings/zmk/mouse.h b/app/include/dt-bindings/zmk/mouse.h new file mode 100644 index 000000000000..dec99c13b65c --- /dev/null +++ b/app/include/dt-bindings/zmk/mouse.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +/* Mouse press behavior */ +/* Left click */ +#define MB1 (0x01) +#define LCLK (MB1) + +/* Right click */ +#define MB2 (0x02) +#define RCLK (MB2) + +/* Middle click */ +#define MB3 (0x04) +#define MCLK (MB3) + +#define MB4 (0x08) + +#define MB5 (0x10) diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index c5964ff8535c..70240183e366 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -69,3 +69,7 @@ int zmk_endpoints_toggle_transport(void); struct zmk_endpoint_instance zmk_endpoints_selected(void); int zmk_endpoints_send_report(uint16_t usage_page); + +#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/mouse_button_state_changed.h b/app/include/zmk/events/mouse_button_state_changed.h new file mode 100644 index 000000000000..9382789e56f6 --- /dev/null +++ b/app/include/zmk/events/mouse_button_state_changed.h @@ -0,0 +1,26 @@ + +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_mouse_button_state_changed { + zmk_mouse_button_t buttons; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_mouse_button_state_changed); + +static inline struct zmk_mouse_button_state_changed_event * +zmk_mouse_button_state_changed_from_encoded(uint32_t encoded, bool pressed, int64_t timestamp) { + return new_zmk_mouse_button_state_changed((struct zmk_mouse_button_state_changed){ + .buttons = ZMK_HID_USAGE_ID(encoded), .state = pressed, .timestamp = timestamp}); +} diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index afe9210199f2..5c5f6d715d3e 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -10,10 +10,15 @@ #include #include +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +#include +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + #include #include #define ZMK_HID_KEYBOARD_NKRO_MAX_USAGE HID_USAGE_KEY_KEYPAD_EQUAL +#define ZMK_HID_MOUSE_NUM_BUTTONS 0x05 // See https://www.usb.org/sites/default/files/hid1_11.pdf section 6.2.2.4 Main Items @@ -46,6 +51,7 @@ #define ZMK_HID_REPORT_ID_KEYBOARD 0x01 #define ZMK_HID_REPORT_ID_CONSUMER 0x02 +#define ZMK_HID_REPORT_ID_MOUSE 0x03 static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), @@ -114,6 +120,39 @@ static const uint8_t zmk_hid_report_desc[] = { HID_REPORT_COUNT(CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE), 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_MOUSE) + HID_USAGE_PAGE(HID_USAGE_GD), + HID_USAGE(HID_USAGE_GD_MOUSE), + HID_COLLECTION(HID_COLLECTION_APPLICATION), + HID_REPORT_ID(ZMK_HID_REPORT_ID_MOUSE), + HID_USAGE(HID_USAGE_GD_POINTER), + HID_COLLECTION(HID_COLLECTION_PHYSICAL), + HID_USAGE_PAGE(HID_USAGE_BUTTON), + HID_USAGE_MIN8(0x1), + HID_USAGE_MAX8(ZMK_HID_MOUSE_NUM_BUTTONS), + HID_LOGICAL_MIN8(0x00), + HID_LOGICAL_MAX8(0x01), + HID_REPORT_SIZE(0x01), + HID_REPORT_COUNT(0x5), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + // Constant padding for the last 3 bits. + HID_REPORT_SIZE(0x03), + HID_REPORT_COUNT(0x01), + HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + // Some OSes ignore pointer devices without X/Y data. + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), + HID_USAGE(HID_USAGE_GD_X), + HID_USAGE(HID_USAGE_GD_Y), + HID_USAGE(HID_USAGE_GD_WHEEL), + HID_LOGICAL_MIN8(-0x7F), + HID_LOGICAL_MAX8(0x7F), + HID_REPORT_SIZE(0x08), + HID_REPORT_COUNT(0x03), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), + HID_END_COLLECTION, + HID_END_COLLECTION, +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) }; #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) @@ -163,6 +202,21 @@ struct zmk_hid_consumer_report { struct zmk_hid_consumer_report_body body; } __packed; +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +struct zmk_hid_mouse_report_body { + zmk_mouse_button_flags_t buttons; + int8_t d_x; + int8_t d_y; + int8_t d_wheel; +} __packed; + +struct zmk_hid_mouse_report { + uint8_t report_id; + struct zmk_hid_mouse_report_body body; +} __packed; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + 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); @@ -189,9 +243,22 @@ 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_MOUSE) +int zmk_hid_mouse_button_press(zmk_mouse_button_t button); +int zmk_hid_mouse_button_release(zmk_mouse_button_t button); +int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons); +int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons); +void zmk_hid_mouse_clear(); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(); struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) zmk_hid_boot_report_t *zmk_hid_get_boot_report(); #endif + +#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 7523fb661ad0..b4e45d9186d6 100644 --- a/app/include/zmk/hog.h +++ b/app/include/zmk/hog.h @@ -13,3 +13,7 @@ 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); + +#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/mouse.h b/app/include/zmk/mouse.h new file mode 100644 index 000000000000..d873f15689a7 --- /dev/null +++ b/app/include/zmk/mouse.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +typedef uint8_t zmk_mouse_button_flags_t; +typedef uint16_t zmk_mouse_button_t; diff --git a/app/include/zmk/usb_hid.h b/app/include/zmk/usb_hid.h index 777f2b48a038..f90917785013 100644 --- a/app/include/zmk/usb_hid.h +++ b/app/include/zmk/usb_hid.h @@ -10,4 +10,7 @@ int zmk_usb_hid_send_keyboard_report(); int zmk_usb_hid_send_consumer_report(); +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_usb_hid_send_mouse_report(); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) void zmk_usb_hid_set_protocol(uint8_t protocol); diff --git a/app/src/behaviors/behavior_mouse_key_press.c b/app/src/behaviors/behavior_mouse_key_press.c new file mode 100644 index 000000000000..60a23799d13c --- /dev/null +++ b/app/src/behaviors/behavior_mouse_key_press.c @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_mouse_key_press + +#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 behavior_mouse_key_press_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + + return ZMK_EVENT_RAISE( + zmk_mouse_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("position %d keycode 0x%02X", event.position, binding->param1); + return ZMK_EVENT_RAISE( + zmk_mouse_button_state_changed_from_encoded(binding->param1, false, event.timestamp)); +} + +static const struct behavior_driver_api behavior_mouse_key_press_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define KP_INST(n) \ + DEVICE_DT_INST_DEFINE(n, behavior_mouse_key_press_init, device_pm_control_nop, NULL, NULL, \ + APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_mouse_key_press_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 10357d4e02d8..098e04e27769 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -191,6 +191,36 @@ int zmk_endpoints_send_report(uint16_t usage_page) { return -ENOTSUP; } +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_endpoints_send_mouse_report() { + switch (current_instance.transport) { +#if IS_ENABLED(CONFIG_ZMK_USB) + case ZMK_TRANSPORT_USB: { + int err = zmk_usb_hid_send_mouse_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_mouse_report *mouse_report = zmk_hid_get_mouse_report(); + int err = zmk_hog_send_mouse_report(&mouse_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_MOUSE) + #if IS_ENABLED(CONFIG_SETTINGS) static int endpoints_handle_set(const char *name, size_t len, settings_read_cb read_cb, @@ -295,6 +325,9 @@ static int zmk_endpoints_init(const struct device *_arg) { static void disconnect_current_endpoint() { zmk_hid_keyboard_clear(); zmk_hid_consumer_clear(); +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + zmk_hid_mouse_clear(); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) zmk_endpoints_send_report(HID_USAGE_KEY); zmk_endpoints_send_report(HID_USAGE_CONSUMER); diff --git a/app/src/events/mouse_button_state_changed.c b/app/src/events/mouse_button_state_changed.c new file mode 100644 index 000000000000..419a7ce956d4 --- /dev/null +++ b/app/src/events/mouse_button_state_changed.c @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +ZMK_EVENT_IMPL(zmk_mouse_button_state_changed); diff --git a/app/src/hid.c b/app/src/hid.c index 689a2361e13f..bc35f2ae2691 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -12,6 +12,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include static struct zmk_hid_keyboard_report keyboard_report = { + .report_id = ZMK_HID_REPORT_ID_KEYBOARD, .body = {.modifiers = 0, ._reserved = 0, .keys = {0}}}; static struct zmk_hid_consumer_report consumer_report = {.report_id = ZMK_HID_REPORT_ID_CONSUMER, @@ -24,6 +25,13 @@ static uint8_t keys_held = 0; #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +static struct zmk_hid_mouse_report mouse_report = {.report_id = ZMK_HID_REPORT_ID_MOUSE, + .body = {.buttons = 0}}; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + // 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}; @@ -357,6 +365,71 @@ bool zmk_hid_is_pressed(uint32_t usage) { return false; } +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +// Keep track of how often a button was pressed. +// Only release the button if the count is 0. +static int explicit_button_counts[5] = {0, 0, 0, 0, 0}; +static zmk_mod_flags_t explicit_buttons = 0; + +#define SET_MOUSE_BUTTONS(btns) \ + { \ + mouse_report.body.buttons = btns; \ + LOG_DBG("Mouse buttons set to 0x%02X", mouse_report.body.buttons); \ + } + +int zmk_hid_mouse_button_press(zmk_mouse_button_t button) { + if (button >= ZMK_HID_MOUSE_NUM_BUTTONS) { + return -EINVAL; + } + + explicit_button_counts[button]++; + LOG_DBG("Button %d count %d", button, explicit_button_counts[button]); + WRITE_BIT(explicit_buttons, button, true); + SET_MOUSE_BUTTONS(explicit_buttons); + return 0; +} + +int zmk_hid_mouse_button_release(zmk_mouse_button_t button) { + if (button >= ZMK_HID_MOUSE_NUM_BUTTONS) { + return -EINVAL; + } + + if (explicit_button_counts[button] <= 0) { + LOG_ERR("Tried to release button %d too often", button); + return -EINVAL; + } + explicit_button_counts[button]--; + LOG_DBG("Button %d count: %d", button, explicit_button_counts[button]); + if (explicit_button_counts[button] == 0) { + LOG_DBG("Button %d released", button); + WRITE_BIT(explicit_buttons, button, false); + } + SET_MOUSE_BUTTONS(explicit_buttons); + return 0; +} + +int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons) { + for (zmk_mouse_button_t i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { + if (buttons & (1 << i)) { + zmk_hid_mouse_button_press(i); + } + } + return 0; +} + +int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons) { + for (zmk_mouse_button_t i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { + if (buttons & (1 << i)) { + zmk_hid_mouse_button_release(i); + } + } + return 0; +} +void zmk_hid_mouse_clear() { memset(&mouse_report.body, 0, sizeof(mouse_report.body)); } + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { return &keyboard_report; } @@ -364,3 +437,11 @@ struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { struct zmk_hid_consumer_report *zmk_hid_get_consumer_report() { return &consumer_report; } + +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +struct zmk_hid_mouse_report *zmk_hid_get_mouse_report() { + return &mouse_report; +} + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/src/hog.c b/app/src/hog.c index 9ccfd9d2c0ee..89a903cb9671 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -56,6 +56,15 @@ static struct hids_report consumer_input = { .type = HIDS_INPUT, }; +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +static struct hids_report mouse_input = { + .id = ZMK_HID_REPORT_ID_MOUSE, + .type = HIDS_INPUT, +}; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + static bool host_requests_notification = false; static uint8_t ctrl_point; // static uint8_t proto_mode; @@ -93,6 +102,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_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) { + struct zmk_hid_mouse_report_body *report_body = &zmk_hid_get_mouse_report()->body; + return bt_gatt_attr_read(conn, attr, buf, len, offset, report_body, + sizeof(struct zmk_hid_mouse_report_body)); +} +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + // 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 +157,15 @@ 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), + +#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), + 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, &mouse_input), +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + 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 +288,59 @@ int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { return 0; }; +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +K_MSGQ_DEFINE(zmk_hog_mouse_msgq, sizeof(struct zmk_hid_mouse_report_body), + CONFIG_ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE, 4); + +void send_mouse_report_callback(struct k_work *work) { + struct zmk_hid_mouse_report_body report; + while (k_msgq_get(&zmk_hog_mouse_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) { + LOG_DBG("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +}; + +int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *report) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return 1; + } + + 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) { + LOG_DBG("Error notifying %d", err); + return err; + } + + bt_conn_unref(conn); + + return 0; +}; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + 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/app/src/mouse.c b/app/src/mouse.c new file mode 100644 index 000000000000..c1b9ac0261e2 --- /dev/null +++ b/app/src/mouse.c @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include +#include + +static void listener_mouse_button_pressed(const struct zmk_mouse_button_state_changed *ev) { + LOG_DBG("buttons: 0x%02X", ev->buttons); + zmk_hid_mouse_buttons_press(ev->buttons); + zmk_endpoints_send_mouse_report(); +} + +static void listener_mouse_button_released(const struct zmk_mouse_button_state_changed *ev) { + LOG_DBG("buttons: 0x%02X", ev->buttons); + zmk_hid_mouse_buttons_release(ev->buttons); + zmk_endpoints_send_mouse_report(); +} + +int mouse_listener(const zmk_event_t *eh) { + const struct zmk_mouse_button_state_changed *mbt_ev = as_zmk_mouse_button_state_changed(eh); + if (mbt_ev) { + if (mbt_ev->state) { + listener_mouse_button_pressed(mbt_ev); + } else { + listener_mouse_button_released(mbt_ev); + } + return 0; + } + return 0; +} + +ZMK_LISTENER(mouse_listener, mouse_listener); +ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_button_state_changed); diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index c72bb36c9ef7..ed44072949fb 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -129,6 +129,19 @@ int zmk_usb_hid_send_consumer_report() { return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); } +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_usb_hid_send_mouse_report() { +#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_mouse_report *report = zmk_hid_get_mouse_report(); + return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); +} +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + static int zmk_usb_hid_init(const struct device *_arg) { hid_dev = device_get_binding("HID_0"); if (hid_dev == NULL) { diff --git a/app/tests/mouse-keys/mkp/events.patterns b/app/tests/mouse-keys/mkp/events.patterns new file mode 100644 index 000000000000..2599345c2d72 --- /dev/null +++ b/app/tests/mouse-keys/mkp/events.patterns @@ -0,0 +1 @@ +s/.*zmk_hid_mouse_button_//p diff --git a/app/tests/mouse-keys/mkp/keycode_events.snapshot b/app/tests/mouse-keys/mkp/keycode_events.snapshot new file mode 100644 index 000000000000..ab58cc9575e5 --- /dev/null +++ b/app/tests/mouse-keys/mkp/keycode_events.snapshot @@ -0,0 +1,10 @@ +press: Button 0 count 1 +press: Mouse buttons set to 0x01 +press: Button 1 count 1 +press: Mouse buttons set to 0x03 +release: Button 1 count: 0 +release: Button 1 released +release: Mouse buttons set to 0x01 +release: Button 0 count: 0 +release: Button 0 released +release: Mouse buttons set to 0x00 diff --git a/app/tests/mouse-keys/mkp/native_posix.keymap b/app/tests/mouse-keys/mkp/native_posix.keymap new file mode 100644 index 000000000000..04316eb34bb3 --- /dev/null +++ b/app/tests/mouse-keys/mkp/native_posix.keymap @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label = "Default keymap"; + + default_layer { + bindings = < + &mkp LCLK &none + &none &mkp RCLK + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS (0,0,100) + ZMK_MOCK_PRESS (1,1,100) + ZMK_MOCK_RELEASE(1,1, 10) + ZMK_MOCK_RELEASE(0,0, 10) + >; +}; diff --git a/app/tests/mouse-keys/mkp/native_posix_64.keymap b/app/tests/mouse-keys/mkp/native_posix_64.keymap new file mode 100644 index 000000000000..04316eb34bb3 --- /dev/null +++ b/app/tests/mouse-keys/mkp/native_posix_64.keymap @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label = "Default keymap"; + + default_layer { + bindings = < + &mkp LCLK &none + &none &mkp RCLK + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS (0,0,100) + ZMK_MOCK_PRESS (1,1,100) + ZMK_MOCK_RELEASE(1,1, 10) + ZMK_MOCK_RELEASE(0,0, 10) + >; +}; diff --git a/docs/docs/behaviors/mouse-emulation.md b/docs/docs/behaviors/mouse-emulation.md new file mode 100644 index 000000000000..9b5845e1036f --- /dev/null +++ b/docs/docs/behaviors/mouse-emulation.md @@ -0,0 +1,64 @@ +--- +title: Mouse Emulation Behaviors +sidebar_label: Mouse Emulation +--- + +## Summary + +Mouse emulation behaviors send mouse events. Currently, only mouse button presses are supported, but movement +and scroll action support is planned for the future. + +## Configuration Option + +This feature can be enabled via a config option: + +``` +CONFIG_ZMK_MOUSE=y +``` + +If you use the mouse key press behavior in your keymap, the feature will automatically be enabled for you. + +## Mouse Button Defines + +To make it easier to encode the HID mouse button numeric values, include +the [`dt-bindings/zmk/mouse.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/mouse.h) header +provided by ZMK near the top: + +``` +#include +``` + +The following defines are available: + +| Define | Action | +| :------------ | :------------- | +| `MB1`, `LCLK` | Left click | +| `MB2`, `RCLK` | Right click | +| `MB3`, `MCLK` | Middle click | +| `MB4` | Mouse button 4 | +| `MB5` | Mouse button 5 | + +Mouse buttons 4 and 5 typically map to "back" and "forward" actions in most applications. + +## Mouse Button Press + +This behavior can press/release up to 5 mouse buttons. + +### Behavior Binding + +- Reference: `&mkp` +- Parameter: A `uint8` with bits 0 through 4 each referring to a button. + +### Examples + +The following will send a left click press when the binding is triggered: + +``` +&mkp LCLK +``` + +This example will send press of the fourth mouse button when the binding is triggered: + +``` +&mkp MB4 +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 43f17b4193f5..19b0ad7e68ca 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -36,6 +36,7 @@ module.exports = { "behaviors/caps-word", "behaviors/key-repeat", "behaviors/sensor-rotate", + "behaviors/mouse-emulation", "behaviors/reset", "behaviors/bluetooth", "behaviors/outputs",