From e90e11d37717eca5e73f2b147926007773f719f5 Mon Sep 17 00:00:00 2001 From: Cem Aksoylar Date: Wed, 15 Nov 2023 21:11:41 -0800 Subject: [PATCH] feat(mouse): Add mouse move and scroll support * Use Zephyr input subsystem for all pointers. * Input processors for modifying events, e.g. scaling, swapping codes, temporary (mouse) layers, etc. * Mouse move/scroll behaviors. * Infrastructure in place for physical pointer input devices. Co-authored-by: Alexander Krikun Co-authored-by: Robert U Co-authored-by: Shawn Meier --- app/CMakeLists.txt | 5 +- app/Kconfig | 8 +- app/Kconfig.behaviors | 8 +- app/dts/behaviors.dtsi | 8 +- app/dts/behaviors/mouse_key_press.dtsi | 5 + app/dts/behaviors/mouse_keys.dtsi | 9 + app/dts/behaviors/mouse_move.dtsi | 26 ++ app/dts/behaviors/mouse_scroll.dtsi | 27 ++ .../zmk,behavior-input-two-axis.yaml | 25 ++ .../bindings/input_processors/ip_common.yaml | 6 + .../input_processors/ip_one_param.yaml | 13 + .../input_processors/ip_two_param.yaml | 14 + .../input_processors/ip_zero_param.yaml | 10 + .../zmk,input-processor-code-mapper.yaml | 16 + .../zmk,input-processor-scaler.yaml | 16 + .../zmk,input-processor-temp-layer.yaml | 8 + .../zmk,input-processor-transform.yaml | 18 + app/dts/bindings/zmk,input-listener.yaml | 23 ++ app/include/drivers/input_processor.h | 48 +++ app/include/dt-bindings/input.h | 12 + app/include/dt-bindings/zmk/input_transform.h | 5 + app/include/dt-bindings/zmk/mouse.h | 28 +- app/include/zmk/hid.h | 92 ++++- app/include/zmk/input.h | 14 + app/include/zmk/mouse.h | 2 +- .../zmk/mouse/resolution_multipliers.h | 25 ++ app/src/behaviors/behavior_input_two_axis.c | 304 ++++++++++++++ app/src/behaviors/behavior_mouse_key_press.c | 25 +- app/src/hid.c | 51 ++- app/src/hog.c | 62 ++- app/src/main.c | 4 + app/src/mouse.c | 43 -- app/src/mouse/CMakeLists.txt | 7 + app/src/mouse/Kconfig | 37 ++ app/src/mouse/input_listener.c | 390 ++++++++++++++++++ app/src/mouse/input_processor_code_mapper.c | 58 +++ app/src/mouse/input_processor_scaler.c | 70 ++++ app/src/mouse/input_processor_temp_layer.c | 60 +++ app/src/mouse/input_processor_transform.c | 92 +++++ app/src/mouse/resolution_multipliers.c | 49 +++ app/src/usb_hid.c | 130 ++++-- app/tests/mouse-keys/mkp/native_posix_64.conf | 6 + .../move_diagonal_scaling/events.patterns | 1 + .../keycode_events.snapshot | 18 + .../native_posix_64.conf | 6 + .../native_posix_64.keymap | 33 ++ .../move_diagonal_xy_invert/events.patterns | 1 + .../keycode_events.snapshot | 18 + .../native_posix_64.conf | 6 + .../native_posix_64.keymap | 33 ++ .../move_diagonal_xy_swap/events.patterns | 1 + .../keycode_events.snapshot | 18 + .../native_posix_64.conf | 6 + .../native_posix_64.keymap | 32 ++ .../mouse-move/move_diagonal/events.patterns | 1 + .../move_diagonal/keycode_events.snapshot | 18 + .../move_diagonal/native_posix_64.conf | 6 + .../move_diagonal/native_posix_64.keymap | 29 ++ .../mouse-move/move_x/events.patterns | 1 + .../mouse-move/move_x/keycode_events.snapshot | 24 ++ .../mouse-move/move_x/native_posix_64.conf | 6 + .../mouse-move/move_x/native_posix_64.keymap | 29 ++ .../mouse-move/move_y/events.patterns | 1 + .../mouse-move/move_y/keycode_events.snapshot | 24 ++ .../mouse-move/move_y/native_posix_64.conf | 6 + .../mouse-move/move_y/native_posix_64.keymap | 29 ++ docs/docs/intro.md | 2 +- docs/docs/keymaps/behaviors/index.mdx | 2 + .../docs/keymaps/behaviors/mouse-emulation.md | 71 +++- 69 files changed, 2157 insertions(+), 124 deletions(-) create mode 100644 app/dts/behaviors/mouse_keys.dtsi create mode 100644 app/dts/behaviors/mouse_move.dtsi create mode 100644 app/dts/behaviors/mouse_scroll.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-input-two-axis.yaml create mode 100644 app/dts/bindings/input_processors/ip_common.yaml create mode 100644 app/dts/bindings/input_processors/ip_one_param.yaml create mode 100644 app/dts/bindings/input_processors/ip_two_param.yaml create mode 100644 app/dts/bindings/input_processors/ip_zero_param.yaml create mode 100644 app/dts/bindings/input_processors/zmk,input-processor-code-mapper.yaml create mode 100644 app/dts/bindings/input_processors/zmk,input-processor-scaler.yaml create mode 100644 app/dts/bindings/input_processors/zmk,input-processor-temp-layer.yaml create mode 100644 app/dts/bindings/input_processors/zmk,input-processor-transform.yaml create mode 100644 app/dts/bindings/zmk,input-listener.yaml create mode 100644 app/include/drivers/input_processor.h create mode 100644 app/include/dt-bindings/input.h create mode 100644 app/include/dt-bindings/zmk/input_transform.h create mode 100644 app/include/zmk/input.h create mode 100644 app/include/zmk/mouse/resolution_multipliers.h create mode 100644 app/src/behaviors/behavior_input_two_axis.c delete mode 100644 app/src/mouse.c create mode 100644 app/src/mouse/CMakeLists.txt create mode 100644 app/src/mouse/Kconfig create mode 100644 app/src/mouse/input_listener.c create mode 100644 app/src/mouse/input_processor_code_mapper.c create mode 100644 app/src/mouse/input_processor_scaler.c create mode 100644 app/src/mouse/input_processor_temp_layer.c create mode 100644 app/src/mouse/input_processor_transform.c create mode 100644 app/src/mouse/resolution_multipliers.c create mode 100644 app/tests/mouse-keys/mkp/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.keymap create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.keymap create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.keymap create mode 100644 app/tests/mouse-keys/mouse-move/move_diagonal/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/move_diagonal/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.keymap create mode 100644 app/tests/mouse-keys/mouse-move/move_x/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/move_x/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/move_x/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/move_x/native_posix_64.keymap create mode 100644 app/tests/mouse-keys/mouse-move/move_y/events.patterns create mode 100644 app/tests/mouse-keys/mouse-move/move_y/keycode_events.snapshot create mode 100644 app/tests/mouse-keys/mouse-move/move_y/native_posix_64.conf create mode 100644 app/tests/mouse-keys/mouse-move/move_y/native_posix_64.keymap diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index fd4b7ab5519..7ca7814d9e2 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -16,6 +16,7 @@ if(CONFIG_ZMK_BEHAVIOR_LOCAL_IDS) endif() zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/behavior.h) +zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/input_processor.h) zephyr_syscall_header(${APPLICATION_SOURCE_DIR}/include/drivers/ext_power.h) # Add your source file to the "app" target. This must come after @@ -36,7 +37,6 @@ target_sources_ifdef(CONFIG_ZMK_GPIO_KEY_WAKEUP_TRIGGER app PRIVATE src/gpio_key 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) @@ -44,7 +44,7 @@ target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SOFT_OFF app PRIVATE src/behaviors/behavior_soft_off.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) + add_subdirectory_ifdef(CONFIG_ZMK_MOUSE src/mouse/) 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_ifdef(CONFIG_ZMK_BEHAVIOR_HOLD_TAP app PRIVATE src/behaviors/behavior_hold_tap.c) @@ -64,6 +64,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) 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_BEHAVIOR_STUDIO_UNLOCK app PRIVATE src/behaviors/behavior_studio_unlock.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_INPUT_TWO_AXIS app PRIVATE src/behaviors/behavior_input_two_axis.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 108fcbe7096..f35e860c94d 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -395,13 +395,7 @@ endif #Display/LED Options endmenu -menu "Mouse Options" - -config ZMK_MOUSE - bool "Enable ZMK mouse emulation" - -#Mouse Options -endmenu +rsource "src/mouse/Kconfig" menu "Power Management" diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors index 69419a2f17e..601eb2ee64b 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -71,8 +71,7 @@ config ZMK_BEHAVIOR_KEY_TOGGLE config ZMK_BEHAVIOR_MOUSE_KEY_PRESS bool default y - depends on DT_HAS_ZMK_BEHAVIOR_MOUSE_KEY_PRESS_ENABLED - imply ZMK_MOUSE + depends on DT_HAS_ZMK_BEHAVIOR_MOUSE_KEY_PRESS_ENABLED && ZMK_MOUSE config ZMK_BEHAVIOR_STICKY_KEY bool @@ -94,6 +93,11 @@ config ZMK_BEHAVIOR_SOFT_OFF default y depends on DT_HAS_ZMK_BEHAVIOR_SOFT_OFF_ENABLED && ZMK_PM_SOFT_OFF +config ZMK_BEHAVIOR_INPUT_TWO_AXIS + bool + default y + depends on DT_HAS_ZMK_BEHAVIOR_INPUT_TWO_AXIS_ENABLED && ZMK_MOUSE + config ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON bool diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index fcb4a63d450..653b085d5c5 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -1,3 +1,9 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + #include #include #include @@ -19,6 +25,6 @@ #include #include #include -#include #include #include +#include diff --git a/app/dts/behaviors/mouse_key_press.dtsi b/app/dts/behaviors/mouse_key_press.dtsi index 4fbc2eb4cf7..fcb7666493a 100644 --- a/app/dts/behaviors/mouse_key_press.dtsi +++ b/app/dts/behaviors/mouse_key_press.dtsi @@ -16,4 +16,9 @@ #binding-cells = <1>; }; }; + + mkp_input_listener: mkp_input_listener { + compatible = "zmk,input-listener"; + device = <&mkp>; + }; }; diff --git a/app/dts/behaviors/mouse_keys.dtsi b/app/dts/behaviors/mouse_keys.dtsi new file mode 100644 index 00000000000..f9a99fede0c --- /dev/null +++ b/app/dts/behaviors/mouse_keys.dtsi @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "mouse_key_press.dtsi" +#include "mouse_move.dtsi" +#include "mouse_scroll.dtsi" \ No newline at end of file diff --git a/app/dts/behaviors/mouse_move.dtsi b/app/dts/behaviors/mouse_move.dtsi new file mode 100644 index 00000000000..99d1ca2c982 --- /dev/null +++ b/app/dts/behaviors/mouse_move.dtsi @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + /omit-if-no-ref/ mmv: mouse_move { + compatible = "zmk,behavior-input-two-axis"; + #binding-cells = <1>; + trigger-period-ms = <3>; + x-input-code = ; + y-input-code = ; + time-to-max-speed-ms = <300>; + acceleration-exponent = <1>; + }; + }; + + mmv_input_listener: mmv_input_listener { + compatible = "zmk,input-listener"; + device = <&mmv>; + }; +}; diff --git a/app/dts/behaviors/mouse_scroll.dtsi b/app/dts/behaviors/mouse_scroll.dtsi new file mode 100644 index 00000000000..e0373bef37c --- /dev/null +++ b/app/dts/behaviors/mouse_scroll.dtsi @@ -0,0 +1,27 @@ + +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + /omit-if-no-ref/ msc: mouse_scroll { + compatible = "zmk,behavior-input-two-axis"; + #binding-cells = <1>; + trigger-period-ms = <3>; + x-input-code = ; + y-input-code = ; + time-to-max-speed-ms = <300>; + acceleration-exponent = <1>; + }; + }; + + msc_input_listener: msc_input_listener { + compatible = "zmk,input-listener"; + device = <&msc>; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-input-two-axis.yaml b/app/dts/bindings/behaviors/zmk,behavior-input-two-axis.yaml new file mode 100644 index 00000000000..ab668ea93b0 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-input-two-axis.yaml @@ -0,0 +1,25 @@ +description: Two axis input behavior + +compatible: "zmk,behavior-input-two-axis" + +include: one_param.yaml + +properties: + x-input-code: + type: int + required: true + y-input-code: + type: int + required: true + trigger-period-ms: + type: int + default: 5 + description: The time (in ms) between generated inputs when an input has non-zero speed. + delay-ms: + type: int + time-to-max-speed-ms: + type: int + required: true + acceleration-exponent: + type: int + default: 1 diff --git a/app/dts/bindings/input_processors/ip_common.yaml b/app/dts/bindings/input_processors/ip_common.yaml new file mode 100644 index 00000000000..9add07acab9 --- /dev/null +++ b/app/dts/bindings/input_processors/ip_common.yaml @@ -0,0 +1,6 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +properties: + track-remainders: + type: boolean diff --git a/app/dts/bindings/input_processors/ip_one_param.yaml b/app/dts/bindings/input_processors/ip_one_param.yaml new file mode 100644 index 00000000000..6ef83b146e9 --- /dev/null +++ b/app/dts/bindings/input_processors/ip_one_param.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +include: ip_common.yaml + +properties: + "#input-processor-cells": + type: int + required: true + const: 1 + +input-processor-cells: + - param1 diff --git a/app/dts/bindings/input_processors/ip_two_param.yaml b/app/dts/bindings/input_processors/ip_two_param.yaml new file mode 100644 index 00000000000..f9538eb1040 --- /dev/null +++ b/app/dts/bindings/input_processors/ip_two_param.yaml @@ -0,0 +1,14 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +include: ip_common.yaml + +properties: + "#input-processor-cells": + type: int + required: true + const: 2 + +input-processor-cells: + - param1 + - param2 diff --git a/app/dts/bindings/input_processors/ip_zero_param.yaml b/app/dts/bindings/input_processors/ip_zero_param.yaml new file mode 100644 index 00000000000..c22f6ec8100 --- /dev/null +++ b/app/dts/bindings/input_processors/ip_zero_param.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2024 The ZMK Contributors +# SPDX-License-Identifier: MIT + +include: ip_common.yaml + +properties: + "#input-processor-cells": + type: int + required: true + const: 0 diff --git a/app/dts/bindings/input_processors/zmk,input-processor-code-mapper.yaml b/app/dts/bindings/input_processors/zmk,input-processor-code-mapper.yaml new file mode 100644 index 00000000000..6cad85a8460 --- /dev/null +++ b/app/dts/bindings/input_processors/zmk,input-processor-code-mapper.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Input Processor for remapping certain input codes to other codes + +compatible: "zmk,input-processor-code-mapper" + +include: ip_zero_param.yaml + +properties: + type: + type: int + required: true + map: + type: array + required: true diff --git a/app/dts/bindings/input_processors/zmk,input-processor-scaler.yaml b/app/dts/bindings/input_processors/zmk,input-processor-scaler.yaml new file mode 100644 index 00000000000..da7ca13f6bc --- /dev/null +++ b/app/dts/bindings/input_processors/zmk,input-processor-scaler.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Input Processor for scaling values + +compatible: "zmk,input-processor-scaler" + +include: ip_two_param.yaml + +properties: + type: + type: int + required: true + codes: + type: array + required: true diff --git a/app/dts/bindings/input_processors/zmk,input-processor-temp-layer.yaml b/app/dts/bindings/input_processors/zmk,input-processor-temp-layer.yaml new file mode 100644 index 00000000000..dedad47616d --- /dev/null +++ b/app/dts/bindings/input_processors/zmk,input-processor-temp-layer.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Input Processor for temporarily enabling a layer after input events + +compatible: "zmk,input-processor-temp-layer" + +include: ip_two_param.yaml diff --git a/app/dts/bindings/input_processors/zmk,input-processor-transform.yaml b/app/dts/bindings/input_processors/zmk,input-processor-transform.yaml new file mode 100644 index 00000000000..24fbd28815b --- /dev/null +++ b/app/dts/bindings/input_processors/zmk,input-processor-transform.yaml @@ -0,0 +1,18 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Input Processor for transforming values in various ways + +compatible: "zmk,input-processor-transform" + +include: ip_one_param.yaml + +properties: + type: + type: int + x-codes: + type: array + required: true + y-codes: + type: array + required: true diff --git a/app/dts/bindings/zmk,input-listener.yaml b/app/dts/bindings/zmk,input-listener.yaml new file mode 100644 index 00000000000..0267b9503a9 --- /dev/null +++ b/app/dts/bindings/zmk,input-listener.yaml @@ -0,0 +1,23 @@ +description: | + Listener to subscribe to input events and send HID updates after processing + +compatible: "zmk,input-listener" + +properties: + device: + type: phandle + required: true + input-processors: + type: phandle-array + +child-binding: + description: "Listener overrides for certain layers" + + properties: + layers: + type: array + required: true + inherit: + type: boolean + input-processors: + type: phandle-array diff --git a/app/include/drivers/input_processor.h b/app/include/drivers/input_processor.h new file mode 100644 index 00000000000..8f3ff7025dc --- /dev/null +++ b/app/include/drivers/input_processor.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +struct zmk_input_processor_state { + float *remainder; +}; + +// TODO: Need the ability to store remainders? Some data passed in? +typedef int (*zmk_input_processor_handle_event_callback_t)(const struct device *dev, + struct input_event *event, + uint32_t param1, uint32_t param2, + struct zmk_input_processor_state *state); + +__subsystem struct zmk_input_processor_driver_api { + zmk_input_processor_handle_event_callback_t handle_event; +}; + +__syscall int zmk_input_processor_handle_event(const struct device *dev, struct input_event *event, + uint32_t param1, uint32_t param2, + struct zmk_input_processor_state *state); + +static inline int z_impl_zmk_input_processor_handle_event(const struct device *dev, + struct input_event *event, + uint32_t param1, uint32_t param2, + struct zmk_input_processor_state *state) { + const struct zmk_input_processor_driver_api *api = + (const struct zmk_input_processor_driver_api *)dev->api; + + if (api->handle_event == NULL) { + return -ENOTSUP; + } + + return api->handle_event(dev, event, param1, param2, state); +} + +#include diff --git a/app/include/dt-bindings/input.h b/app/include/dt-bindings/input.h new file mode 100644 index 00000000000..2437b50461f --- /dev/null +++ b/app/include/dt-bindings/input.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#define ZMK_INPUT_EXPLICIT_CODE(type, code) ((type << 16) & code) + +#define ZMK_INPUT_EXPLICIT_CODE_TYPE(val) ((val >> 16) & 0xFF) +#define ZMK_INPUT_EXPLICIT_CODE_CODE(val) (val & 0xFFFF) \ No newline at end of file diff --git a/app/include/dt-bindings/zmk/input_transform.h b/app/include/dt-bindings/zmk/input_transform.h new file mode 100644 index 00000000000..5e883c1359c --- /dev/null +++ b/app/include/dt-bindings/zmk/input_transform.h @@ -0,0 +1,5 @@ +#include + +#define INPUT_TRANSFORM_XY_SWAP BIT(0) +#define INPUT_TRANSFORM_X_INVERT BIT(1) +#define INPUT_TRANSFORM_Y_INVERT BIT(2) \ No newline at end of file diff --git a/app/include/dt-bindings/zmk/mouse.h b/app/include/dt-bindings/zmk/mouse.h index 582518aff7e..99a35a508c9 100644 --- a/app/include/dt-bindings/zmk/mouse.h +++ b/app/include/dt-bindings/zmk/mouse.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 The ZMK Contributors + * Copyright (c) 2023 The ZMK Contributors * * SPDX-License-Identifier: MIT */ @@ -22,3 +22,29 @@ #define MB4 BIT(3) #define MB5 BIT(4) + +#ifndef ZMK_MOUSE_DEFAULT_MOVE_VAL +#define ZMK_MOUSE_DEFAULT_MOVE_VAL 600 +#endif + +#ifndef ZMK_MOUSE_DEFAULT_SCRL_VAL +#define ZMK_MOUSE_DEFAULT_SCRL_VAL 300 +#endif + +/* Mouse move behavior */ +#define MOVE_Y(vert) ((vert) & 0xFFFF) +#define MOVE_Y_DECODE(encoded) (int16_t)((encoded) & 0x0000FFFF) +#define MOVE_X(hor) (((hor) & 0xFFFF) << 16) +#define MOVE_X_DECODE(encoded) (int16_t)(((encoded) & 0xFFFF0000) >> 16) + +#define MOVE(hor, vert) (MOVE_X(hor) + MOVE_Y(vert)) + +#define MOVE_UP MOVE_Y(-ZMK_MOUSE_DEFAULT_MOVE_VAL) +#define MOVE_DOWN MOVE_Y(ZMK_MOUSE_DEFAULT_MOVE_VAL) +#define MOVE_LEFT MOVE_X(-ZMK_MOUSE_DEFAULT_MOVE_VAL) +#define MOVE_RIGHT MOVE_X(ZMK_MOUSE_DEFAULT_MOVE_VAL) + +#define SCRL_UP MOVE_Y(ZMK_MOUSE_DEFAULT_SCRL_VAL) +#define SCRL_DOWN MOVE_Y(-ZMK_MOUSE_DEFAULT_SCRL_VAL) +#define SCRL_LEFT MOVE_X(-ZMK_MOUSE_DEFAULT_SCRL_VAL) +#define SCRL_RIGHT MOVE_X(ZMK_MOUSE_DEFAULT_SCRL_VAL) diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 766fb9c4632..f2293cc1217 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -77,6 +77,30 @@ #define ZMK_HID_REPORT_ID_CONSUMER 0x02 #define ZMK_HID_REPORT_ID_MOUSE 0x03 +#ifndef HID_ITEM_TAG_PUSH +#define HID_ITEM_TAG_PUSH 0xA +#endif + +#ifndef HID_ITEM_TAG_POP +#define HID_ITEM_TAG_POP 0xB +#endif + +#define HID_PUSH HID_ITEM(HID_ITEM_TAG_PUSH, HID_ITEM_TYPE_GLOBAL, 0) + +#define HID_POP HID_ITEM(HID_ITEM_TAG_POP, HID_ITEM_TYPE_GLOBAL, 0) + +#ifndef HID_PHYSICAL_MIN8 +#define HID_PHYSICAL_MIN8(a) HID_ITEM(HID_ITEM_TAG_PHYSICAL_MIN, HID_ITEM_TYPE_GLOBAL, 1), a +#endif + +#ifndef HID_PHYSICAL_MAX8 +#define HID_PHYSICAL_MAX8(a) HID_ITEM(HID_ITEM_TAG_PHYSICAL_MAX, HID_ITEM_TYPE_GLOBAL, 1), a +#endif + +#define HID_USAGE16(a, b) HID_ITEM(HID_ITEM_TAG_USAGE, HID_ITEM_TYPE_LOCAL, 2), a, b + +#define HID_USAGE16_SINGLE(a) HID_USAGE16((a & 0xFF), ((a >> 8) & 0xFF)) + static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_KEYBOARD), @@ -184,14 +208,50 @@ static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), HID_USAGE(HID_USAGE_GD_X), HID_USAGE(HID_USAGE_GD_Y), + HID_LOGICAL_MIN16(0xFF, -0x7F), + HID_LOGICAL_MAX16(0xFF, 0x7F), + HID_REPORT_SIZE(0x10), + HID_REPORT_COUNT(0x02), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), + HID_COLLECTION(HID_COLLECTION_LOGICAL), +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), + HID_LOGICAL_MIN8(0x00), + HID_LOGICAL_MAX8(0x0F), + HID_PHYSICAL_MIN8(0x01), + HID_PHYSICAL_MAX8(0x10), + HID_REPORT_SIZE(0x04), + HID_REPORT_COUNT(0x01), + HID_PUSH, + HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) HID_USAGE(HID_USAGE_GD_WHEEL), - HID_LOGICAL_MIN8(-0x7F), - HID_LOGICAL_MAX8(0x7F), + HID_LOGICAL_MIN16(0xFF, -0x7F), + HID_LOGICAL_MAX16(0xFF, 0x7F), + HID_PHYSICAL_MIN8(0x00), + HID_PHYSICAL_MAX8(0x00), + HID_REPORT_SIZE(0x10), + HID_REPORT_COUNT(0x01), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), + HID_END_COLLECTION, + HID_COLLECTION(HID_COLLECTION_LOGICAL), +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + HID_USAGE(HID_USAGE_GD_RESOLUTION_MULTIPLIER), + HID_POP, + HID_FEATURE(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + HID_USAGE_PAGE(HID_USAGE_CONSUMER), + HID_USAGE16_SINGLE(HID_USAGE_CONSUMER_AC_PAN), + HID_LOGICAL_MIN16(0xFF, -0x7F), + HID_LOGICAL_MAX16(0xFF, 0x7F), + HID_PHYSICAL_MIN8(0x00), + HID_PHYSICAL_MAX8(0x00), HID_REPORT_SIZE(0x08), - HID_REPORT_COUNT(0x03), + HID_REPORT_COUNT(0x01), HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), HID_END_COLLECTION, HID_END_COLLECTION, + HID_END_COLLECTION, #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) }; @@ -258,9 +318,10 @@ struct zmk_hid_consumer_report { #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; + int16_t d_x; + int16_t d_y; + int16_t d_scroll_y; + int16_t d_scroll_x; } __packed; struct zmk_hid_mouse_report { @@ -268,6 +329,20 @@ struct zmk_hid_mouse_report { struct zmk_hid_mouse_report_body body; } __packed; +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +struct zmk_hid_mouse_resolution_feature_report_body { + uint8_t wheel_res : 4; + uint8_t hwheel_res : 4; +} __packed; + +struct zmk_hid_mouse_resolution_feature_report { + uint8_t report_id; + struct zmk_hid_mouse_resolution_feature_report_body body; +} __packed; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) zmk_mod_flags_t zmk_hid_get_explicit_mods(void); @@ -301,7 +376,12 @@ 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_movement_set(int16_t x, int16_t y); +void zmk_hid_mouse_scroll_set(int8_t x, int8_t y); +void zmk_hid_mouse_movement_update(int16_t x, int16_t y); +void zmk_hid_mouse_scroll_update(int8_t x, int8_t y); void zmk_hid_mouse_clear(void); + #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void); diff --git a/app/include/zmk/input.h b/app/include/zmk/input.h new file mode 100644 index 00000000000..b1cbae6f99d --- /dev/null +++ b/app/include/zmk/input.h @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +struct zmk_input_explicit_code { + uint8_t type; + uint16_t code; +}; diff --git a/app/include/zmk/mouse.h b/app/include/zmk/mouse.h index d873f15689a..c898f001098 100644 --- a/app/include/zmk/mouse.h +++ b/app/include/zmk/mouse.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 The ZMK Contributors + * Copyright (c) 2023 The ZMK Contributors * * SPDX-License-Identifier: MIT */ diff --git a/app/include/zmk/mouse/resolution_multipliers.h b/app/include/zmk/mouse/resolution_multipliers.h new file mode 100644 index 00000000000..da423e33884 --- /dev/null +++ b/app/include/zmk/mouse/resolution_multipliers.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_mouse_resolution_multipliers { + uint8_t wheel; + uint8_t hor_wheel; +}; + +struct zmk_mouse_resolution_multipliers zmk_mouse_resolution_multipliers_get_current_profile(void); +struct zmk_mouse_resolution_multipliers +zmk_mouse_resolution_multipliers_get_profile(struct zmk_endpoint_instance endpoint); +void zmk_mouse_resolution_multipliers_set_profile( + struct zmk_mouse_resolution_multipliers multipliers, struct zmk_endpoint_instance endpoint); + +void zmk_mouse_resolution_multipliers_process_report( + struct zmk_hid_mouse_resolution_feature_report_body *report, + struct zmk_endpoint_instance endpoint); diff --git a/app/src/behaviors/behavior_input_two_axis.c b/app/src/behaviors/behavior_input_two_axis.c new file mode 100644 index 00000000000..20f5d52a571 --- /dev/null +++ b/app/src/behaviors/behavior_input_two_axis.c @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_input_two_axis + +#include +#include +#include +#include +#include // CLAMP + +#include +#include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) +#include +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct vector2d { + float x; + float y; +}; + +struct movement_state_1d { + float remainder; + int16_t speed; + int64_t start_time; +}; + +struct movement_state_2d { + struct movement_state_1d x; + struct movement_state_1d y; +}; + +struct behavior_input_two_axis_data { + struct k_work_delayable tick_work; + const struct device *dev; + + struct movement_state_2d state; +}; + +struct behavior_input_two_axis_config { + int16_t x_code; + int16_t y_code; + uint16_t delay_ms; + uint16_t time_to_max_speed_ms; + uint8_t trigger_period_ms; + // acceleration exponent 0: uniform speed + // acceleration exponent 1: uniform acceleration + // acceleration exponent 2: uniform jerk + uint8_t acceleration_exponent; +}; + +#if CONFIG_MINIMAL_LIBC +static float powf(float base, float exponent) { + // poor man's power implementation rounds the exponent down to the nearest integer. + float power = 1.0f; + for (; exponent >= 1.0f; exponent--) { + power = power * base; + } + return power; +} +#else +#include +#endif + +static int64_t ticks_since_start(int64_t start, int64_t now, int64_t delay) { + if (start == 0) { + return 0; + } + int64_t move_duration = now - (start + delay); + // start can be in the future if there's a delay + if (move_duration < 0) { + move_duration = 0; + } + return move_duration; +} + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +static uint8_t get_acceleration_exponent(const struct behavior_input_two_axis_config *config, + uint16_t code) { + switch (code) { + case INPUT_REL_WHEEL: + return (zmk_mouse_resolution_multipliers_get_current_profile().wheel > 0) + ? 0 + : config->acceleration_exponent; + case INPUT_REL_HWHEEL: + return (zmk_mouse_resolution_multipliers_get_current_profile().hor_wheel > 0) + ? 0 + : config->acceleration_exponent; + default: + return config->acceleration_exponent; + } +} + +#else + +static inline uint8_t get_acceleration_exponent(const struct behavior_input_two_axis_config *config, + uint16_t code) { + return config->acceleration_exponent; +} + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +static float speed(const struct behavior_input_two_axis_config *config, uint16_t code, + float max_speed, int64_t duration_ticks) { + uint8_t accel_exp = get_acceleration_exponent(config, code); + + if ((duration_ticks * CONFIG_SYS_CLOCK_TICKS_PER_SEC / 1000) > config->time_to_max_speed_ms || + config->time_to_max_speed_ms == 0 || accel_exp == 0) { + return max_speed; + } + + // Calculate the speed based on MouseKeysAccel + // See https://en.wikipedia.org/wiki/Mouse_keys + if (duration_ticks == 0) { + return 0; + } + + float time_fraction = (float)duration_ticks * CONFIG_SYS_CLOCK_TICKS_PER_SEC / + config->time_to_max_speed_ms / 1000; + return max_speed * powf(time_fraction, accel_exp); +} + +static void track_remainder(float *move, float *remainder) { + float new_move = *move + *remainder; + *remainder = new_move - (int)new_move; + *move = (int)new_move; +} + +static float update_movement_1d(const struct behavior_input_two_axis_config *config, uint16_t code, + struct movement_state_1d *state, int64_t now) { + float move = 0; + if (state->speed == 0) { + state->remainder = 0; + return move; + } + + int64_t move_duration = ticks_since_start(state->start_time, now, config->delay_ms); + LOG_DBG("Calculated speed: %f", speed(config, code, state->speed, move_duration)); + move = + (move_duration > 0) + ? (speed(config, code, state->speed, move_duration) * config->trigger_period_ms / 1000) + : 0; + + track_remainder(&(move), &(state->remainder)); + + return move; +} +static struct vector2d update_movement_2d(const struct behavior_input_two_axis_config *config, + struct movement_state_2d *state, int64_t now) { + struct vector2d move = {0}; + + move = (struct vector2d){ + .x = update_movement_1d(config, config->x_code, &state->x, now), + .y = update_movement_1d(config, config->y_code, &state->y, now), + }; + + return move; +} + +static bool is_non_zero_1d_movement(int16_t speed) { return speed != 0; } + +static bool is_non_zero_2d_movement(struct movement_state_2d *state) { + return is_non_zero_1d_movement(state->x.speed) || is_non_zero_1d_movement(state->y.speed); +} + +static bool should_be_working(struct behavior_input_two_axis_data *data) { + return is_non_zero_2d_movement(&data->state); +} + +static void tick_work_cb(struct k_work *work) { + struct k_work_delayable *d_work = k_work_delayable_from_work(work); + struct behavior_input_two_axis_data *data = + CONTAINER_OF(d_work, struct behavior_input_two_axis_data, tick_work); + const struct device *dev = data->dev; + const struct behavior_input_two_axis_config *cfg = dev->config; + + uint64_t timestamp = k_uptime_ticks(); + + // LOG_INF("x start: %llu, y start: %llu, current timestamp: %llu", data->state.x.start_time, + // data->state.y.start_time, timestamp); + + struct vector2d move = update_movement_2d(cfg, &data->state, timestamp); + + int ret = 0; + bool have_x = is_non_zero_1d_movement(move.x); + bool have_y = is_non_zero_1d_movement(move.y); + if (have_x) { + ret = input_report_rel(dev, cfg->x_code, (int16_t)CLAMP(move.x, INT16_MIN, INT16_MAX), + !have_y, K_NO_WAIT); + } + if (have_y) { + ret = input_report_rel(dev, cfg->y_code, (int16_t)CLAMP(move.y, INT16_MIN, INT16_MAX), true, + K_NO_WAIT); + } + + if (should_be_working(data)) { + k_work_schedule(&data->tick_work, K_MSEC(cfg->trigger_period_ms)); + } +} + +static void set_start_times_for_activity_1d(struct movement_state_1d *state) { + if (state->speed != 0 && state->start_time == 0) { + state->start_time = k_uptime_ticks(); + } else if (state->speed == 0) { + state->start_time = 0; + } +} +static void set_start_times_for_activity(struct movement_state_2d *state) { + set_start_times_for_activity_1d(&state->x); + set_start_times_for_activity_1d(&state->y); +} + +static void update_work_scheduling(const struct device *dev) { + struct behavior_input_two_axis_data *data = dev->data; + const struct behavior_input_two_axis_config *cfg = dev->config; + + set_start_times_for_activity(&data->state); + + if (should_be_working(data)) { + k_work_schedule(&data->tick_work, K_MSEC(cfg->trigger_period_ms)); + } else { + k_work_cancel_delayable(&data->tick_work); + data->state.y.remainder = 0; + data->state.x.remainder = 0; + } +} + +int behavior_input_two_axis_adjust_speed(const struct device *dev, int16_t dx, int16_t dy) { + struct behavior_input_two_axis_data *data = dev->data; + + LOG_DBG("Adjusting: %d %d", dx, dy); + data->state.x.speed += dx; + data->state.y.speed += dy; + + LOG_DBG("After: %d %d", data->state.x.speed, data->state.y.speed); + + update_work_scheduling(dev); + + return 0; +} + +static int behavior_input_two_axis_init(const struct device *dev) { + struct behavior_input_two_axis_data *data = dev->data; + + data->dev = dev; + k_work_init_delayable(&data->tick_work, tick_work_cb); + + return 0; +}; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + + const struct device *behavior_dev = zmk_behavior_get_binding(binding->behavior_dev); + + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + + int16_t x = MOVE_X_DECODE(binding->param1); + int16_t y = MOVE_Y_DECODE(binding->param1); + + behavior_input_two_axis_adjust_speed(behavior_dev, x, y); + return 0; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *behavior_dev = zmk_behavior_get_binding(binding->behavior_dev); + + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + + int16_t x = MOVE_X_DECODE(binding->param1); + int16_t y = MOVE_Y_DECODE(binding->param1); + + behavior_input_two_axis_adjust_speed(behavior_dev, -x, -y); + return 0; +} + +static const struct behavior_driver_api behavior_input_two_axis_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define ITA_INST(n) \ + static struct behavior_input_two_axis_data behavior_input_two_axis_data_##n = {}; \ + static struct behavior_input_two_axis_config behavior_input_two_axis_config_##n = { \ + .x_code = DT_INST_PROP(n, x_input_code), \ + .y_code = DT_INST_PROP(n, y_input_code), \ + .trigger_period_ms = DT_INST_PROP(n, trigger_period_ms), \ + .delay_ms = DT_INST_PROP_OR(n, delay_ms, 0), \ + .time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \ + .acceleration_exponent = DT_INST_PROP_OR(n, acceleration_exponent, 1), \ + }; \ + BEHAVIOR_DT_INST_DEFINE( \ + n, behavior_input_two_axis_init, NULL, &behavior_input_two_axis_data_##n, \ + &behavior_input_two_axis_config_##n, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_input_two_axis_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(ITA_INST) diff --git a/app/src/behaviors/behavior_mouse_key_press.c b/app/src/behaviors/behavior_mouse_key_press.c index 9064a1aa5c8..66b54fce05b 100644 --- a/app/src/behaviors/behavior_mouse_key_press.c +++ b/app/src/behaviors/behavior_mouse_key_press.c @@ -11,8 +11,9 @@ #include #include -#include -#include +#include +#include +#include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -20,19 +21,31 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); static int behavior_mouse_key_press_init(const struct device *dev) { return 0; }; +static void process_key_state(const struct device *dev, int32_t val, bool pressed) { + for (int i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { + if (val & BIT(i)) { + WRITE_BIT(val, i, 0); + input_report_key(dev, INPUT_BTN_0 + i, pressed ? 1 : 0, val == 0, K_FOREVER); + } + } +} + 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 raise_zmk_mouse_button_state_changed_from_encoded(binding->param1, true, - event.timestamp); + process_key_state(zmk_behavior_get_binding(binding->behavior_dev), binding->param1, true); + + return 0; } 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 raise_zmk_mouse_button_state_changed_from_encoded(binding->param1, false, - event.timestamp); + + process_key_state(zmk_behavior_get_binding(binding->behavior_dev), binding->param1, false); + + return 0; } static const struct behavior_driver_api behavior_mouse_key_press_driver_api = { diff --git a/app/src/hid.c b/app/src/hid.c index 24572ad325b..480a5c01d5b 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -27,8 +27,9 @@ static uint8_t keys_held = 0; #if IS_ENABLED(CONFIG_ZMK_MOUSE) -static struct zmk_hid_mouse_report mouse_report = {.report_id = ZMK_HID_REPORT_ID_MOUSE, - .body = {.buttons = 0}}; +static struct zmk_hid_mouse_report mouse_report = { + .report_id = ZMK_HID_REPORT_ID_MOUSE, + .body = {.buttons = 0, .d_x = 0, .d_y = 0, .d_scroll_y = 0}}; #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) @@ -431,22 +432,50 @@ int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons) { } return 0; } -void zmk_hid_mouse_clear(void) { memset(&mouse_report.body, 0, sizeof(mouse_report.body)); } -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) +void zmk_hid_mouse_movement_set(int16_t hwheel, int16_t wheel) { + mouse_report.body.d_x = hwheel; + mouse_report.body.d_y = wheel; + LOG_DBG("Mouse movement set to %d/%d", mouse_report.body.d_x, mouse_report.body.d_y); +} -struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void) { - return &keyboard_report; +void zmk_hid_mouse_movement_update(int16_t hwheel, int16_t wheel) { + mouse_report.body.d_x += hwheel; + mouse_report.body.d_y += wheel; + LOG_DBG("Mouse movement updated to %d/%d", mouse_report.body.d_x, mouse_report.body.d_y); } -struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void) { - return &consumer_report; +void zmk_hid_mouse_scroll_set(int8_t hwheel, int8_t wheel) { + mouse_report.body.d_scroll_x = hwheel; + mouse_report.body.d_scroll_y = wheel; + // mouse_report.body.d_scroll_x = (hwheel / (16 - mouse_res_feature_report.body.hwheel_res)); + // mouse_report.body.d_scroll_y = (wheel / (16 - mouse_res_feature_report.body.wheel_res)); + LOG_DBG("Mouse scroll set to %d/%d", mouse_report.body.d_scroll_x, + mouse_report.body.d_scroll_y); } -#if IS_ENABLED(CONFIG_ZMK_MOUSE) +void zmk_hid_mouse_scroll_update(int8_t hwheel, int8_t wheel) { + mouse_report.body.d_scroll_x += hwheel; + mouse_report.body.d_scroll_y += wheel; + // mouse_report.body.d_scroll_x += (hwheel / (16 - mouse_res_feature_report.body.hwheel_res)); + // mouse_report.body.d_scroll_y += (wheel / (16 - mouse_res_feature_report.body.wheel_res)); + LOG_DBG("Mouse scroll updated to X: %d/%d", mouse_report.body.d_scroll_x, + mouse_report.body.d_scroll_y); +} -struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(void) { - return &mouse_report; +void zmk_hid_mouse_clear(void) { + LOG_DBG("Mouse report cleared"); + 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(void) { return &keyboard_report; } + +struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void) { return &consumer_report; } + +#if IS_ENABLED(CONFIG_ZMK_MOUSE) + +struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(void) { return &mouse_report; } + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/src/hog.c b/app/src/hog.c index 82fafc29cd7..060fc26717a 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -18,6 +18,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) +#include +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) #if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) @@ -76,6 +79,15 @@ static struct hids_report mouse_input = { .type = HIDS_INPUT, }; +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +static struct hids_report mouse_feature = { + .id = ZMK_HID_REPORT_ID_MOUSE, + .type = HIDS_FEATURE, +}; + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) static bool host_requests_notification = false; @@ -144,12 +156,51 @@ static ssize_t read_hids_consumer_input_report(struct bt_conn *conn, } #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)); } + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +static ssize_t read_hids_mouse_feature_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)); +} + +static ssize_t write_hids_mouse_feature_report(struct bt_conn *conn, + const struct bt_gatt_attr *attr, const void *buf, + uint16_t len, uint16_t offset, uint8_t flags) { + if (offset != 0) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + if (len != sizeof(struct zmk_hid_mouse_resolution_feature_report_body)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + struct zmk_hid_mouse_resolution_feature_report_body *report = + (struct zmk_hid_mouse_resolution_feature_report_body *)buf; + int profile = zmk_ble_profile_index(bt_conn_get_dst(conn)); + if (profile < 0) { + return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); + } + + struct zmk_endpoint_instance endpoint = {.transport = ZMK_TRANSPORT_BLE, + .ble = { + .profile_index = profile, + }}; + zmk_mouse_resolution_multipliers_process_report(report, endpoint); + + return len; +} + +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) // static ssize_t write_proto_mode(struct bt_conn *conn, @@ -206,6 +257,16 @@ 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, &mouse_input), + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, + BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT, + read_hids_mouse_feature_report, write_hids_mouse_feature_report, NULL), + BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, + NULL, &mouse_feature), +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) #if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) @@ -380,7 +441,6 @@ int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *report) { return 0; }; - #endif // IS_ENABLED(CONFIG_ZMK_MOUSE) static int zmk_hog_init(void) { diff --git a/app/src/main.c b/app/src/main.c index 60df1a45d83..fcf1d11531f 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -14,6 +14,10 @@ LOG_MODULE_REGISTER(zmk, CONFIG_ZMK_LOG_LEVEL); #include +#ifdef CONFIG_ZMK_MOUSE +#include +#endif /* CONFIG_ZMK_MOUSE */ + int main(void) { LOG_INF("Welcome to ZMK!\n"); diff --git a/app/src/mouse.c b/app/src/mouse.c deleted file mode 100644 index c1b9ac0261e..00000000000 --- a/app/src/mouse.c +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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/mouse/CMakeLists.txt b/app/src/mouse/CMakeLists.txt new file mode 100644 index 00000000000..709d4968978 --- /dev/null +++ b/app/src/mouse/CMakeLists.txt @@ -0,0 +1,7 @@ + +target_sources(app PRIVATE input_listener.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_TRANSFORM app PRIVATE input_processor_transform.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_SCALER app PRIVATE input_processor_scaler.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_TEMP_LAYER app PRIVATE input_processor_temp_layer.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_CODE_MAPPER app PRIVATE input_processor_code_mapper.c) +target_sources_ifdef(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING app PRIVATE resolution_multipliers.c) \ No newline at end of file diff --git a/app/src/mouse/Kconfig b/app/src/mouse/Kconfig new file mode 100644 index 00000000000..7d90d3f1f72 --- /dev/null +++ b/app/src/mouse/Kconfig @@ -0,0 +1,37 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config ZMK_MOUSE + bool "Mouse Emulation" + select INPUT + select INPUT_THREAD_PRIORITY_OVERRIDE + +if ZMK_MOUSE + +config ZMK_MOUSE_SMOOTH_SCROLLING + bool "Smooth Scrolling" + help + Enable smooth scrolling, with hosts that support HID Resolution Multipliers + +config ZMK_INPUT_PROCESSOR_TRANSFORM + bool "Transform Input Processor" + default y + depends on DT_HAS_ZMK_INPUT_PROCESSOR_TRANSFORM_ENABLED + +config ZMK_INPUT_PROCESSOR_SCALER + bool "Scaling Input Processor" + default y + depends on DT_HAS_ZMK_INPUT_PROCESSOR_SCALER_ENABLED + +config ZMK_INPUT_PROCESSOR_TEMP_LAYER + bool "Temporary Layer Input Processor" + default y + depends on DT_HAS_ZMK_INPUT_PROCESSOR_TEMP_LAYER_ENABLED + +config ZMK_INPUT_PROCESSOR_CODE_MAPPER + bool "Code Mapper Input Processor" + default y + depends on DT_HAS_ZMK_INPUT_PROCESSOR_CODE_MAPPER_ENABLED + + +endif diff --git a/app/src/mouse/input_listener.c b/app/src/mouse/input_listener.c new file mode 100644 index 00000000000..4617eebfd45 --- /dev/null +++ b/app/src/mouse/input_listener.c @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_listener + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) +#include +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +#include +#include + +#define ONE_IF_DEV_OK(n) \ + COND_CODE_1(DT_NODE_HAS_STATUS(DT_INST_PHANDLE(n, device), okay), (1 +), (0 +)) + +#define VALID_LISTENER_COUNT (DT_INST_FOREACH_STATUS_OKAY(ONE_IF_DEV_OK) 0) + +#if VALID_LISTENER_COUNT > 0 + +enum input_listener_xy_data_mode { + INPUT_LISTENER_XY_DATA_MODE_NONE, + INPUT_LISTENER_XY_DATA_MODE_REL, + INPUT_LISTENER_XY_DATA_MODE_ABS, +}; + +struct input_listener_axis_data { + int16_t value; +}; + +struct input_listener_xy_data { + enum input_listener_xy_data_mode mode; + struct input_listener_axis_data x; + struct input_listener_axis_data y; +}; + +struct input_processor_entry { + const struct device *dev; + uint32_t param1; + uint32_t param2; + bool track_remainders; +}; + +struct input_listener_config_entry { + size_t processors_len; + const struct input_processor_entry *processors; +}; + +struct input_listener_layer_override { + uint32_t layer_mask; + bool inherit; + struct input_listener_config_entry config; +}; + +struct input_processor_remainder_data { + float x, y, wheel, h_wheel; +}; + +struct input_listener_processor_data { + size_t remainders_len; + struct input_processor_remainder_data *remainders; +}; + +struct input_listener_config { + struct input_listener_config_entry base; + size_t layer_overrides_len; + struct input_listener_layer_override layer_overrides[]; +}; + +struct input_listener_data { + union { + struct { + struct input_listener_xy_data data; + struct input_listener_xy_data wheel_data; + + uint8_t button_set; + uint8_t button_clear; + } mouse; + }; + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + float wheel_remainder; + float h_wheel_remainder; +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + + struct input_listener_processor_data base_processor_data; + struct input_listener_processor_data layer_override_data[]; +}; + +static void handle_rel_code(struct input_listener_data *data, struct input_event *evt) { + switch (evt->code) { + case INPUT_REL_X: + data->mouse.data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.data.x.value += evt->value; + break; + case INPUT_REL_Y: + data->mouse.data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.data.y.value += evt->value; + break; + case INPUT_REL_WHEEL: + data->mouse.wheel_data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.wheel_data.y.value += evt->value; + break; + case INPUT_REL_HWHEEL: + data->mouse.wheel_data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.wheel_data.x.value += evt->value; + break; + default: + break; + } +} + +static void handle_abs_code(const struct input_listener_config *config, + struct input_listener_data *data, struct input_event *evt) {} + +static void handle_key_code(const struct input_listener_config *config, + struct input_listener_data *data, struct input_event *evt) { + int8_t btn; + + switch (evt->code) { + case INPUT_BTN_0: + case INPUT_BTN_1: + case INPUT_BTN_2: + case INPUT_BTN_3: + case INPUT_BTN_4: + btn = evt->code - INPUT_BTN_0; + if (evt->value > 0) { + WRITE_BIT(data->mouse.button_set, btn, 1); + } else { + WRITE_BIT(data->mouse.button_clear, btn, 1); + } + break; + default: + break; + } +} + +static inline bool is_x_data(const struct input_event *evt) { + return evt->type == INPUT_EV_REL && evt->code == INPUT_REL_X; +} + +static inline bool is_y_data(const struct input_event *evt) { + return evt->type == INPUT_EV_REL && evt->code == INPUT_REL_Y; +} + +static void apply_config(const struct input_listener_config_entry *cfg, + struct input_listener_processor_data *processor_data, + struct input_listener_data *data, struct input_event *evt) { + size_t remainder_index = 0; + for (size_t p = 0; p < cfg->processors_len; p++) { + const struct input_processor_entry *proc_e = &cfg->processors[p]; + struct input_processor_remainder_data *remainders = NULL; + if (proc_e->track_remainders) { + remainders = &processor_data->remainders[remainder_index++]; + } + + float *remainder = NULL; + if (remainders) { + if (evt->type == INPUT_EV_REL) { + switch (evt->code) { + case INPUT_REL_X: + remainder = &remainders->x; + break; + case INPUT_REL_Y: + remainder = &remainders->y; + break; + case INPUT_REL_WHEEL: + remainder = &remainders->wheel; + break; + case INPUT_REL_HWHEEL: + remainder = &remainders->h_wheel; + break; + } + } + } + + struct zmk_input_processor_state state = {.remainder = remainder}; + + zmk_input_processor_handle_event(proc_e->dev, evt, proc_e->param1, proc_e->param2, &state); + } +} +static void filter_with_input_config(const struct input_listener_config *cfg, + struct input_listener_data *data, struct input_event *evt) { + if (!evt->dev) { + return; + } + + for (size_t oi = 0; oi < cfg->layer_overrides_len; oi++) { + const struct input_listener_layer_override *override = &cfg->layer_overrides[oi]; + struct input_listener_processor_data *override_data = &data->layer_override_data[oi]; + uint32_t mask = override->layer_mask; + uint8_t layer = 0; + while (mask != 0) { + if (mask & BIT(0) && zmk_keymap_layer_active(layer)) { + apply_config(&override->config, override_data, data, evt); + if (!override->inherit) { + return; + } + } + + layer++; + mask = mask >> 1; + } + } + + apply_config(&cfg->base, &data->base_processor_data, data, evt); +} + +static void clear_xy_data(struct input_listener_xy_data *data) { + data->x.value = data->y.value = 0; + data->mode = INPUT_LISTENER_XY_DATA_MODE_NONE; +} + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) +static void apply_resolution_scaling(struct input_listener_data *data, struct input_event *evt) { + float *remainder; + uint8_t div; + + switch (evt->code) { + case INPUT_REL_WHEEL: + remainder = &data->wheel_remainder; + div = (16 - zmk_mouse_resolution_multipliers_get_current_profile().wheel); + break; + case INPUT_REL_HWHEEL: + remainder = &data->h_wheel_remainder; + div = (16 - zmk_mouse_resolution_multipliers_get_current_profile().hor_wheel); + break; + default: + return; + } + + float value = (((float)evt->value) / div) + *remainder; + evt->value = (int16_t)value; + *remainder = value - evt->value; +} +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + +static void input_handler(const struct input_listener_config *config, + struct input_listener_data *data, struct input_event *evt) { + // First, filter to update the event data as needed. + filter_with_input_config(config, data, evt); + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + apply_resolution_scaling(data, evt); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + + switch (evt->type) { + case INPUT_EV_REL: + handle_rel_code(data, evt); + break; + case INPUT_EV_ABS: + handle_abs_code(config, data, evt); + break; + case INPUT_EV_KEY: + handle_key_code(config, data, evt); + break; + } + + if (evt->sync) { + if (data->mouse.wheel_data.mode == INPUT_LISTENER_XY_DATA_MODE_REL) { + zmk_hid_mouse_scroll_set(data->mouse.wheel_data.x.value, + data->mouse.wheel_data.y.value); + } + + if (data->mouse.data.mode == INPUT_LISTENER_XY_DATA_MODE_REL) { + zmk_hid_mouse_movement_set(data->mouse.data.x.value, data->mouse.data.y.value); + } + + if (data->mouse.button_set != 0) { + for (int i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { + if ((data->mouse.button_set & BIT(i)) != 0) { + zmk_hid_mouse_button_press(i); + } + } + } + + if (data->mouse.button_clear != 0) { + for (int i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { + if ((data->mouse.button_clear & BIT(i)) != 0) { + zmk_hid_mouse_button_release(i); + } + } + } + + zmk_endpoints_send_mouse_report(); + zmk_hid_mouse_scroll_set(0, 0); + zmk_hid_mouse_movement_set(0, 0); + + clear_xy_data(&data->mouse.data); + clear_xy_data(&data->mouse.wheel_data); + + data->mouse.button_set = data->mouse.button_clear = 0; + } +} + +#endif // VALID_LISTENER_COUNT > 0 + +#define ZMK_EXTRACT_INPUT_PROC(idx, n) \ + { \ + .dev = DEVICE_DT_GET(DT_PHANDLE_BY_IDX(n, input_processors, idx)), \ + .param1 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(n, input_processors, idx, param1), (0), \ + (DT_PHA_BY_IDX(n, input_processors, idx, param1))), \ + .param2 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(n, input_processors, idx, param2), (0), \ + (DT_PHA_BY_IDX(n, input_processors, idx, param2))), \ + .track_remainders = \ + COND_CODE_1(DT_PROP(DT_PHANDLE_BY_IDX(n, input_processors, idx), track_remainders), \ + (true), (false)), \ + } + +#define ONE_FOR_TRACKED(n, elem, idx) \ + +DT_PROP(DT_PHANDLE_BY_IDX(n, input_processors, idx), track_remainders) +#define PROCESSOR_REM_TRACKERS(n) (0 DT_FOREACH_PROP_ELEM(n, input_processors, ONE_FOR_TRACKED)) + +#define SCOPED_PROCESSOR(scope, n, id) \ + COND_CODE_1(DT_NODE_HAS_PROP(n, input_processors), \ + (static struct input_processor_remainder_data _CONCAT( \ + input_processor_remainders_##id, scope)[PROCESSOR_REM_TRACKERS(n)] = {};), \ + ()) \ + static const struct input_processor_entry _CONCAT( \ + processor_##id, scope)[DT_PROP_LEN_OR(n, input_processors, 0)] = \ + COND_CODE_1( \ + DT_NODE_HAS_PROP(n, input_processors), \ + ({LISTIFY(DT_PROP_LEN(n, input_processors), ZMK_EXTRACT_INPUT_PROC, (, ), n)}), ({})); + +#define IL_EXTRACT_CONFIG(n, id, scope) \ + { \ + .processors_len = DT_PROP_LEN_OR(n, input_processors, 0), \ + .processors = _CONCAT(processor_##id, scope), \ + } + +#define IL_EXTRACT_DATA(n, id, scope) \ + {COND_CODE_1(DT_NODE_HAS_PROP(n, input_processors), \ + (.remainders_len = PROCESSOR_REM_TRACKERS(n), \ + .remainders = _CONCAT(input_processor_remainders_##id, scope), ), \ + ())} + +#define IL_ONE(...) +1 + +#define CHILD_CONFIG(node, parent) SCOPED_PROCESSOR(node, node, parent) + +#define OVERRIDE_LAYER_BIT(node, prop, idx) BIT(DT_PROP_BY_IDX(node, prop, idx)) + +#define IL_OVERRIDE(node, parent) \ + { \ + .layer_mask = DT_FOREACH_PROP_ELEM_SEP(node, layers, OVERRIDE_LAYER_BIT, (|)), \ + .inherit = DT_PROP_OR(node, inherit, false), \ + .config = IL_EXTRACT_CONFIG(node, parent, node), \ + } + +#define IL_OVERRIDE_DATA(node, parent) IL_EXTRACT_DATA(node, parent, node) + +#define IL_INST(n) \ + COND_CODE_1( \ + DT_NODE_HAS_STATUS(DT_INST_PHANDLE(n, device), okay), \ + (SCOPED_PROCESSOR(base, DT_DRV_INST(n), n); \ + DT_INST_FOREACH_CHILD_VARGS(n, CHILD_CONFIG, \ + n) static const struct input_listener_config config_##n = \ + { \ + .base = IL_EXTRACT_CONFIG(DT_DRV_INST(n), n, base), \ + .layer_overrides_len = (0 DT_INST_FOREACH_CHILD(n, IL_ONE)), \ + .layer_overrides = {DT_INST_FOREACH_CHILD_SEP_VARGS(n, IL_OVERRIDE, (, ), n)}, \ + }; \ + static struct input_listener_data data_##n = \ + { \ + .base_processor_data = IL_EXTRACT_DATA(DT_DRV_INST(n), n, base), \ + .layer_override_data = {DT_INST_FOREACH_CHILD_SEP_VARGS(n, IL_OVERRIDE_DATA, \ + (, ), n)}, \ + }; \ + void input_handler_##n(struct input_event *evt) { \ + input_handler(&config_##n, &data_##n, evt); \ + } INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), input_handler_##n);), \ + ()) + +DT_INST_FOREACH_STATUS_OKAY(IL_INST) diff --git a/app/src/mouse/input_processor_code_mapper.c b/app/src/mouse/input_processor_code_mapper.c new file mode 100644 index 00000000000..20b5744ea96 --- /dev/null +++ b/app/src/mouse/input_processor_code_mapper.c @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_processor_code_mapper + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct cm_config { + uint8_t type; + size_t mapping_size; + uint16_t mapping[]; +}; + +static int cm_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, + uint32_t param2, struct zmk_input_processor_state *state) { + const struct cm_config *cfg = dev->config; + + if (event->type != cfg->type) { + return 0; + } + + for (int i = 0; i < cfg->mapping_size / 2; i++) { + if (cfg->mapping[i * 2] == event->code) { + uint16_t orig = event->code; + event->code = cfg->mapping[(i * 2) + 1]; + LOG_DBG("Remapped %d to %d", orig, event->code); + break; + } + } + + return 0; +} + +static struct zmk_input_processor_driver_api cm_driver_api = { + .handle_event = cm_handle_event, +}; + +#define TL_INST(n) \ + static const struct cm_config cm_config_##n = { \ + .type = DT_INST_PROP_OR(n, type, INPUT_EV_REL), \ + .mapping_size = DT_INST_PROP_LEN(n, map), \ + .mapping = DT_INST_PROP(n, map), \ + }; \ + BUILD_ASSERT(DT_INST_PROP_LEN(n, map) % 2 == 0, \ + "Must have an even number of mapping entries"); \ + DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, &cm_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &cm_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(TL_INST) \ No newline at end of file diff --git a/app/src/mouse/input_processor_scaler.c b/app/src/mouse/input_processor_scaler.c new file mode 100644 index 00000000000..cf8145fd1de --- /dev/null +++ b/app/src/mouse/input_processor_scaler.c @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_processor_scaler + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct scaler_config { + uint8_t type; + size_t codes_len; + uint16_t codes[]; +}; + +static int scale_val(struct input_event *event, uint32_t mul, uint32_t div, + struct zmk_input_processor_state *state) { + float value = ((((float)event->value) * mul) / div); + if (state && state->remainder) { + value += *state->remainder; + } + + event->value = (int16_t)value; + LOG_DBG("SCALED VALUE %f, cast to int %d", value, event->value); + + if (state && state->remainder) { + *state->remainder = value - event->value; + } + + return 0; +} + +static int scaler_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, + uint32_t param2, struct zmk_input_processor_state *state) { + const struct scaler_config *cfg = dev->config; + + if (event->type != cfg->type) { + return 0; + } + + for (int i = 0; i < cfg->codes_len; i++) { + if (cfg->codes[i] == event->code) { + return scale_val(event, param1, param2, state); + } + } + + return 0; +} + +static struct zmk_input_processor_driver_api scaler_driver_api = { + .handle_event = scaler_handle_event, +}; + +#define SCALER_INST(n) \ + static const struct scaler_config scaler_config_##n = { \ + .type = DT_INST_PROP_OR(n, type, INPUT_EV_REL), \ + .codes_len = DT_INST_PROP_LEN(n, codes), \ + .codes = DT_INST_PROP(n, codes), \ + }; \ + DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, &scaler_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &scaler_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(SCALER_INST) \ No newline at end of file diff --git a/app/src/mouse/input_processor_temp_layer.c b/app/src/mouse/input_processor_temp_layer.c new file mode 100644 index 00000000000..6a8ad99bb67 --- /dev/null +++ b/app/src/mouse/input_processor_temp_layer.c @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_processor_temp_layer + +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +struct k_work_delayable layer_disable_works[ZMK_KEYMAP_LAYERS_LEN]; + +static void layer_disable_cb(struct k_work *work) { + struct k_work_delayable *d_work = k_work_delayable_from_work(work); + int layer_index = ARRAY_INDEX(layer_disable_works, d_work); + if (zmk_keymap_layer_active(layer_index)) { + zmk_keymap_layer_deactivate(layer_index); + } +} + +static int tl_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, + uint32_t param2, struct zmk_input_processor_state *state) { + if (param1 >= ZMK_KEYMAP_LAYERS_LEN) { + LOG_ERR("Passed an invalid layer id %d", param1); + return -EINVAL; + } + + if (!zmk_keymap_layer_active(param1)) { + zmk_keymap_layer_activate(param1); + } + + k_work_reschedule(&layer_disable_works[param1], K_MSEC(param2)); + return 0; +} + +static struct zmk_input_processor_driver_api tl_driver_api = { + .handle_event = tl_handle_event, +}; + +static int tl_init(const struct device *dev) { + for (int i = 0; i < ZMK_KEYMAP_LAYERS_LEN; i++) { + k_work_init_delayable(&layer_disable_works[i], layer_disable_cb); + } + + return 0; +} + +#define TL_INST(n) \ + DEVICE_DT_INST_DEFINE(n, &tl_init, NULL, NULL, NULL, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &tl_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(TL_INST) \ No newline at end of file diff --git a/app/src/mouse/input_processor_transform.c b/app/src/mouse/input_processor_transform.c new file mode 100644 index 00000000000..b22e76e04cb --- /dev/null +++ b/app/src/mouse/input_processor_transform.c @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_processor_transform + +#include +#include +#include + +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +struct ipt_config { + size_t x_codes_size; + size_t y_codes_size; + uint8_t type; + + const uint16_t *x_codes; + const uint16_t *y_codes; +}; + +static int code_idx(uint16_t code, const uint16_t *list, size_t len) { + for (int i = 0; i < len; i++) { + if (list[0] == code) { + return i; + } + } + + return -ENODEV; +} + +static int ipt_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, + uint32_t param2, struct zmk_input_processor_state *state) { + const struct ipt_config *cfg = dev->config; + + if (event->type != cfg->type) { + return 0; + } + + if (param1 & INPUT_TRANSFORM_XY_SWAP) { + int idx = code_idx(event->code, cfg->x_codes, cfg->x_codes_size); + if (idx >= 0) { + event->code = cfg->y_codes[idx]; + } else { + idx = code_idx(event->code, cfg->y_codes, cfg->y_codes_size); + + if (idx >= 0) { + event->code = cfg->x_codes[idx]; + } + } + } + + if ((param1 & INPUT_TRANSFORM_X_INVERT && + code_idx(event->code, cfg->x_codes, cfg->x_codes_size) >= 0) || + (param1 & INPUT_TRANSFORM_Y_INVERT && + code_idx(event->code, cfg->y_codes, cfg->y_codes_size) >= 0)) { + event->value = -event->value; + } + + return 0; +} + +static struct zmk_input_processor_driver_api ipt_driver_api = { + .handle_event = ipt_handle_event, +}; + +static int ipt_init(const struct device *dev) { return 0; } + +#define IPT_INST(n) \ + static const uint16_t ipt_x_codes_##n[] = DT_INST_PROP(n, x_codes); \ + static const uint16_t ipt_y_codes_##n[] = DT_INST_PROP(n, y_codes); \ + BUILD_ASSERT(ARRAY_SIZE(ipt_x_codes_##n) == ARRAY_SIZE(ipt_x_codes_##n), \ + "X and Y codes need to be the same size"); \ + static const struct ipt_config ipt_config_##n = { \ + .type = DT_INST_PROP_OR(n, type, INPUT_EV_REL), \ + .x_codes_size = DT_INST_PROP_LEN(n, x_codes), \ + .y_codes_size = DT_INST_PROP_LEN(n, y_codes), \ + .x_codes = ipt_x_codes_##n, \ + .y_codes = ipt_y_codes_##n, \ + }; \ + DEVICE_DT_INST_DEFINE(n, &ipt_init, NULL, NULL, &ipt_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &ipt_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(IPT_INST) \ No newline at end of file diff --git a/app/src/mouse/resolution_multipliers.c b/app/src/mouse/resolution_multipliers.c new file mode 100644 index 00000000000..fc205dc7677 --- /dev/null +++ b/app/src/mouse/resolution_multipliers.c @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static struct zmk_mouse_resolution_multipliers multipliers[ZMK_ENDPOINT_COUNT]; + +struct zmk_mouse_resolution_multipliers zmk_mouse_resolution_multipliers_get_current_profile(void) { + return zmk_mouse_resolution_multipliers_get_profile(zmk_endpoints_selected()); +} + +struct zmk_mouse_resolution_multipliers +zmk_mouse_resolution_multipliers_get_profile(struct zmk_endpoint_instance endpoint) { + const int profile = zmk_endpoint_instance_to_index(endpoint); + return multipliers[profile]; +} + +void zmk_mouse_resolution_multipliers_set_profile(struct zmk_mouse_resolution_multipliers m, + struct zmk_endpoint_instance endpoint) { + int profile = zmk_endpoint_instance_to_index(endpoint); + + // This write is not happening on the main thread. To prevent potential data races, every + // operation involving hid_indicators must be atomic. Currently, each function either reads + // or writes only one entry at a time, so it is safe to do these operations without a lock. + multipliers[profile] = m; +} + +void zmk_mouse_resolution_multipliers_process_report( + struct zmk_hid_mouse_resolution_feature_report_body *report, + struct zmk_endpoint_instance endpoint) { + struct zmk_mouse_resolution_multipliers vals = { + .wheel = report->wheel_res, + .hor_wheel = report->hwheel_res, + }; + zmk_mouse_resolution_multipliers_set_profile(vals, endpoint); + + LOG_DBG("Update resolution multipliers: endpoint=%d, wheel=%d, hor_wheel=%d", + endpoint.transport, vals.wheel, vals.hor_wheel); +} diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index 9db10952c95..6273962379e 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -13,9 +13,15 @@ #include #include #include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) +#include +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + #if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) + #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -56,31 +62,56 @@ static uint8_t *get_keyboard_report(size_t *len) { static int get_report_cb(const struct device *dev, struct usb_setup_packet *setup, int32_t *len, uint8_t **data) { + switch (setup->wValue & HID_GET_REPORT_TYPE_MASK) { + case HID_REPORT_TYPE_FEATURE: + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + case ZMK_HID_REPORT_ID_MOUSE: + static struct zmk_hid_mouse_resolution_feature_report res_feature_report; - /* - * 7.2.1 of the HID v1.11 spec is unclear about handling requests for reports that do not exist - * For requested reports that aren't input reports, return -ENOTSUP like the Zephyr subsys does - */ - if ((setup->wValue & HID_GET_REPORT_TYPE_MASK) != HID_REPORT_TYPE_INPUT) { - LOG_ERR("Unsupported report type %d requested", (setup->wValue & HID_GET_REPORT_TYPE_MASK) - << 8); - return -ENOTSUP; - } + struct zmk_endpoint_instance endpoint = { + .transport = ZMK_TRANSPORT_USB, + }; - switch (setup->wValue & HID_GET_REPORT_ID_MASK) { - case ZMK_HID_REPORT_ID_KEYBOARD: { - *data = get_keyboard_report(len); + *len = sizeof(struct zmk_hid_mouse_resolution_feature_report); + struct zmk_mouse_resolution_multipliers mult = + zmk_mouse_resolution_multipliers_get_profile(endpoint); + + res_feature_report.body.wheel_res = mult.wheel; + res_feature_report.body.hwheel_res = mult.hor_wheel; + *data = (uint8_t *)&res_feature_report; + break; +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + default: + return -ENOTSUP; + } break; - } - case ZMK_HID_REPORT_ID_CONSUMER: { - struct zmk_hid_consumer_report *report = zmk_hid_get_consumer_report(); - *data = (uint8_t *)report; - *len = sizeof(*report); + case HID_REPORT_TYPE_INPUT: + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { + case ZMK_HID_REPORT_ID_KEYBOARD: { + *data = get_keyboard_report(len); + break; + } + case ZMK_HID_REPORT_ID_CONSUMER: { + struct zmk_hid_consumer_report *report = zmk_hid_get_consumer_report(); + *data = (uint8_t *)report; + *len = sizeof(*report); + break; + } + default: + LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); + return -EINVAL; + } break; - } default: - LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); - return -EINVAL; + /* + * 7.2.1 of the HID v1.11 spec is unclear about handling requests for reports that do not + * exist For requested reports that aren't input reports, return -ENOTSUP like the Zephyr + * subsys does + */ + LOG_ERR("Unsupported report type %d requested", (setup->wValue & HID_GET_REPORT_TYPE_MASK) + << 8); + return -ENOTSUP; } return 0; @@ -88,30 +119,55 @@ static int get_report_cb(const struct device *dev, struct usb_setup_packet *setu static int set_report_cb(const struct device *dev, struct usb_setup_packet *setup, int32_t *len, uint8_t **data) { - if ((setup->wValue & HID_GET_REPORT_TYPE_MASK) != HID_REPORT_TYPE_OUTPUT) { - LOG_ERR("Unsupported report type %d requested", - (setup->wValue & HID_GET_REPORT_TYPE_MASK) >> 8); - return -ENOTSUP; - } - - switch (setup->wValue & HID_GET_REPORT_ID_MASK) { -#if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) - case ZMK_HID_REPORT_ID_LEDS: - if (*len != sizeof(struct zmk_hid_led_report)) { - LOG_ERR("LED set report is malformed: length=%d", *len); - return -EINVAL; - } else { - struct zmk_hid_led_report *report = (struct zmk_hid_led_report *)*data; + switch (setup->wValue & HID_GET_REPORT_TYPE_MASK) { + case HID_REPORT_TYPE_FEATURE: + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { +#if IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + case ZMK_HID_REPORT_ID_MOUSE: + if (*len != sizeof(struct zmk_hid_mouse_resolution_feature_report)) { + return -EINVAL; + } + + struct zmk_hid_mouse_resolution_feature_report *report = + (struct zmk_hid_mouse_resolution_feature_report *)*data; struct zmk_endpoint_instance endpoint = { .transport = ZMK_TRANSPORT_USB, }; - zmk_hid_indicators_process_report(&report->body, endpoint); + + zmk_mouse_resolution_multipliers_process_report(&report->body, endpoint); + + break; +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING) + default: + return -ENOTSUP; } break; + + case HID_REPORT_TYPE_OUTPUT: + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { +#if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) + case ZMK_HID_REPORT_ID_LEDS: + if (*len != sizeof(struct zmk_hid_led_report)) { + LOG_ERR("LED set report is malformed: length=%d", *len); + return -EINVAL; + } else { + struct zmk_hid_led_report *report = (struct zmk_hid_led_report *)*data; + struct zmk_endpoint_instance endpoint = { + .transport = ZMK_TRANSPORT_USB, + }; + zmk_hid_indicators_process_report(&report->body, endpoint); + } + break; #endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) + default: + LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); + return -EINVAL; + } + break; default: - LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); - return -EINVAL; + LOG_ERR("Unsupported report type %d requested", + (setup->wValue & HID_GET_REPORT_TYPE_MASK) >> 8); + return -ENOTSUP; } return 0; diff --git a/app/tests/mouse-keys/mkp/native_posix_64.conf b/app/tests/mouse-keys/mkp/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mkp/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/events.patterns b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/keycode_events.snapshot new file mode 100644 index 00000000000..15d31600960 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/keycode_events.snapshot @@ -0,0 +1,18 @@ +movement_set: Mouse movement set to -1/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -5/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -5/-5 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/-5 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.keymap new file mode 100644 index 00000000000..df8cda8cfdf --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_scaling/native_posix_64.keymap @@ -0,0 +1,33 @@ +#include +#include +#include +#include + +&mmv_input_listener { + scale-multiplier = <5>; + scale-divisor = <3>; +}; + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &mmv MOVE_UP + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/events.patterns b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/keycode_events.snapshot new file mode 100644 index 00000000000..33bb267b073 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/keycode_events.snapshot @@ -0,0 +1,18 @@ +movement_set: Mouse movement set to 1/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 2/2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 2/2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 3/2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 3/3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.keymap new file mode 100644 index 00000000000..9b07e1b9808 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_invert/native_posix_64.keymap @@ -0,0 +1,33 @@ +#include +#include +#include +#include + +&mmv_input_listener { + x-invert; + y-invert; +}; + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &mmv MOVE_UP + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/events.patterns b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/keycode_events.snapshot new file mode 100644 index 00000000000..40daa64f0f6 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/keycode_events.snapshot @@ -0,0 +1,18 @@ +movement_set: Mouse movement set to 0/-1 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.keymap new file mode 100644 index 00000000000..719bca98f14 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/input-configs/move_diagonal_xy_swap/native_posix_64.keymap @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +&mmv_input_listener { + xy-swap; +}; + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &mmv MOVE_UP + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_diagonal/events.patterns b/app/tests/mouse-keys/mouse-move/move_diagonal/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_diagonal/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_diagonal/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/move_diagonal/keycode_events.snapshot new file mode 100644 index 00000000000..6b9fa770b11 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_diagonal/keycode_events.snapshot @@ -0,0 +1,18 @@ +movement_set: Mouse movement set to -1/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.keymap new file mode 100644 index 00000000000..7e4d7af2a1d --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_diagonal/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &mmv MOVE_UP + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_x/events.patterns b/app/tests/mouse-keys/mouse-move/move_x/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_x/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_x/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/move_x/keycode_events.snapshot new file mode 100644 index 00000000000..678f71c9ac2 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_x/keycode_events.snapshot @@ -0,0 +1,24 @@ +movement_set: Mouse movement set to -1/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -2/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to -3/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 1/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 2/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 2/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 3/0 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.keymap new file mode 100644 index 00000000000..89d50e2b839 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_x/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &mmv MOVE_RIGHT + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_y/events.patterns b/app/tests/mouse-keys/mouse-move/move_y/events.patterns new file mode 100644 index 00000000000..812126fb828 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_y/events.patterns @@ -0,0 +1 @@ +s/.*hid_mouse_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mouse-move/move_y/keycode_events.snapshot b/app/tests/mouse-keys/mouse-move/move_y/keycode_events.snapshot new file mode 100644 index 00000000000..d20154d5507 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_y/keycode_events.snapshot @@ -0,0 +1,24 @@ +movement_set: Mouse movement set to 0/-1 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/-2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/-3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/1 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/2 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 +movement_set: Mouse movement set to 0/3 +scroll_set: Mouse scroll set to 0/0 +movement_set: Mouse movement set to 0/0 diff --git a/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.conf b/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.conf new file mode 100644 index 00000000000..65ed54bb204 --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.conf @@ -0,0 +1,6 @@ +CONFIG_GPIO=n +CONFIG_ZMK_BLE=n +CONFIG_LOG=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_ZMK_LOG_LEVEL_DBG=y +CONFIG_ZMK_MOUSE=y diff --git a/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.keymap b/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.keymap new file mode 100644 index 00000000000..5b02246b05f --- /dev/null +++ b/app/tests/mouse-keys/mouse-move/move_y/native_posix_64.keymap @@ -0,0 +1,29 @@ +#include +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_UP &mmv MOVE_DOWN + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/docs/docs/intro.md b/docs/docs/intro.md index d8d992e7663..d5bf527ca5d 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -33,7 +33,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | One Shot Keys | ✅ | ✅ | ✅ | | [Combo Keys](keymaps/combos.md) | ✅ | | ✅ | | [Macros](keymaps/behaviors/macros.md) | ✅ | ✅ | ✅ | -| Mouse Keys | 🚧 | ✅ | ✅ | +| Mouse Keys | ✅ | ✅ | ✅ | | Low Active Power Usage | ✅ | | | | Low Power Sleep States | ✅ | ✅ | | | [Low Power Mode (VCC Shutoff)](keymaps/behaviors/power.md) | ✅ | ✅ | | diff --git a/docs/docs/keymaps/behaviors/index.mdx b/docs/docs/keymaps/behaviors/index.mdx index c9d0fbe49c7..c876beb111e 100644 --- a/docs/docs/keymaps/behaviors/index.mdx +++ b/docs/docs/keymaps/behaviors/index.mdx @@ -43,6 +43,8 @@ Below is a summary of pre-defined behavior bindings and user-definable behaviors | Binding | Behavior | Description | | ------- | ----------------------------------------------------------- | ------------------------------- | | `&mkp` | [Mouse Button Press](mouse-emulation.md#mouse-button-press) | Emulates pressing mouse buttons | +| `&mmv` | [Mouse Button Press](mouse-emulation.md#mouse-move) | Emulates mouse movement | +| `&msc` | [Mouse Button Press](mouse-emulation.md#mouse-scroll) | Emulates mouse scrolling | ## Reset Behaviors diff --git a/docs/docs/keymaps/behaviors/mouse-emulation.md b/docs/docs/keymaps/behaviors/mouse-emulation.md index 26bef8ccce8..029666ca72b 100644 --- a/docs/docs/keymaps/behaviors/mouse-emulation.md +++ b/docs/docs/keymaps/behaviors/mouse-emulation.md @@ -5,8 +5,7 @@ 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. +Mouse emulation behaviors send mouse events, including mouse button presses, cursor movement and scrolling. :::warning[Refreshing the HID descriptor] @@ -17,14 +16,12 @@ The mouse functionality will not work over BLE until that is done. ## Configuration Option -This feature can be enabled or disabled explicitly via a config option: +To use any of the behaviors documented here, the ZMK mouse feature must be enabled explicitly 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 @@ -69,3 +66,67 @@ This example will send press of the fourth mouse button when the binding is trig ``` &mkp MB4 ``` + +## Mouse Move + +This behavior sends mouse X/Y movement events to the connected host. + +### Behavior Binding + +- Reference: `&mmv` +- Parameter: A `uint32` with 16-bits each used for vertical and horizontal velocity. + +The following defines can be passed for the parameter: + +| Define | Action | +| :----------- | :--------- | +| `MOVE_UP` | Move up | +| `MOVE_DOWN` | Move down | +| `MOVE_LEFT` | Move left | +| `MOVE_RIGHT` | Move right | + +### Examples + +The following will send a scroll down event to the host when pressed/held: + +``` +&mmv MOVE_DOWN +``` + +The following will send a scroll left event to the host when pressed/held: + +``` +&mmv MOVE_LEFT +``` + +## Mouse Scroll + +This behavior sends vertical and horizontal scroll events to the connected host. + +### Behavior Binding + +- Reference: `&msc` +- Parameter: A `uint32` with 16-bits each used for vertical and horizontal velocity. + +The following defines can be passed for the parameter: + +| Define | Action | +| :----------- | :--------- | +| `MOVE_UP` | Move up | +| `MOVE_DOWN` | Move down | +| `MOVE_LEFT` | Move left | +| `MOVE_RIGHT` | Move right | + +### Examples + +The following will send a scroll down event to the host when pressed/held: + +``` +&msc MOVE_DOWN +``` + +The following will send a scroll left event to the host when pressed/held: + +``` +&msc MOVE_LEFT +```