diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 0b681ea9531..36643149830 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -32,7 +32,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) @@ -40,7 +39,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) + target_sources_ifdef(CONFIG_ZMK_MOUSE app PRIVATE src/mouse/input_listener.c) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) target_sources(app PRIVATE src/behaviors/behavior_hold_tap.c) @@ -59,6 +58,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON app PRIVATE src/behaviors/behavior_sensor_rotate_common.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MOUSE_KEY_PRESS app PRIVATE src/behaviors/behavior_mouse_key_press.c) + target_sources_ifdef(CONFIG_ZMK_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) @@ -96,9 +96,12 @@ target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_MOUSE_PS2 app PRIVATE src/behaviors/behavior_mouse_setting.c) + target_sources(app PRIVATE src/main.c) add_subdirectory(src/display/) add_subdirectory_ifdef(CONFIG_SETTINGS src/settings/) +add_subdirectory(src/mouse/) zephyr_cc_option(-Wfatal-errors) diff --git a/app/Kconfig b/app/Kconfig index 8f2fe837317..23c78ab1f8f 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -367,13 +367,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 c9754bf7d83..8f633b3df39 100644 --- a/app/Kconfig.behaviors +++ b/app/Kconfig.behaviors @@ -9,14 +9,18 @@ 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_SOFT_OFF bool 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 @@ -35,4 +39,4 @@ config ZMK_BEHAVIOR_SENSOR_ROTATE_VAR config ZMK_BEHAVIOR_MACRO bool default y - depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED \ No newline at end of file + depends on DT_HAS_ZMK_BEHAVIOR_MACRO_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_ONE_PARAM_ENABLED || DT_HAS_ZMK_BEHAVIOR_MACRO_TWO_PARAM_ENABLED diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index fde75271891..b5770fccbef 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,5 +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 975c24aaafb..66e327e849e 100644 --- a/app/dts/behaviors/mouse_key_press.dtsi +++ b/app/dts/behaviors/mouse_key_press.dtsi @@ -5,4 +5,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..09b93520c1f --- /dev/null +++ b/app/dts/behaviors/mouse_move.dtsi @@ -0,0 +1,25 @@ +/* + * 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>; + 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..b482efded67 --- /dev/null +++ b/app/dts/behaviors/mouse_scroll.dtsi @@ -0,0 +1,26 @@ + +/* + * 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>; + x-input-code = ; + y-input-code = ; + time-to-max-speed-ms = <300>; + acceleration-exponent = <0>; + }; + }; + + msc_input_listener: msc_input_listener { + compatible = "zmk,input-listener"; + device = <&msc>; + }; +}; diff --git a/app/dts/behaviors/mouse_setting.dtsi b/app/dts/behaviors/mouse_setting.dtsi new file mode 100644 index 00000000000..3abb14c4fb7 --- /dev/null +++ b/app/dts/behaviors/mouse_setting.dtsi @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + mms: behavior_mouse_setting { + compatible = "zmk,behavior-mouse-setting"; + #binding-cells = <1>; + }; + }; +}; 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..0c138e03929 --- /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: 16 + 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/behaviors/zmk,behavior-mouse-setting.yaml b/app/dts/bindings/behaviors/zmk,behavior-mouse-setting.yaml new file mode 100644 index 00000000000..2388cbef1da --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-mouse-setting.yaml @@ -0,0 +1,6 @@ +description: Mouse Setting + +compatible: "zmk,behavior-mouse-setting" + +include: one_param.yaml + diff --git a/app/dts/bindings/zmk,input-listener.yaml b/app/dts/bindings/zmk,input-listener.yaml new file mode 100644 index 00000000000..fba83e3b3a6 --- /dev/null +++ b/app/dts/bindings/zmk,input-listener.yaml @@ -0,0 +1,36 @@ +description: | + Listener to subscribe to input events and send HID updates after processing + +compatible: "zmk,input-listener" + +properties: + device: + type: phandle + required: true + xy-swap: + type: boolean + x-invert: + type: boolean + y-invert: + type: boolean + scale-multiplier: + type: int + default: 1 + scale-divisor: + type: int + default: 1 + layer-toggle: + type: int + default: -1 + required: false + description: The layer that should be toggled when the mouse is moved. + layer-toggle-delay-ms: + type: int + default: 250 + required: false + description: How many miliseconds of mouse activity are required before the layer is toggled on. + layer-toggle-timeout-ms: + type: int + default: 250 + required: false + description: How many miliseconds of mouse inactivity are required before the layer is toggled off again. diff --git a/app/include/dt-bindings/zmk/mouse.h b/app/include/dt-bindings/zmk/mouse.h index 582518aff7e..ea34e1243f1 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 10 +#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/dt-bindings/zmk/mouse_settings.h b/app/include/dt-bindings/zmk/mouse_settings.h new file mode 100644 index 00000000000..0d2502376df --- /dev/null +++ b/app/include/dt-bindings/zmk/mouse_settings.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +#define MS_LOG 0 +#define MS_RESET 1 + +#define MS_TP_SENSITIVITY_INCR 10 +#define MS_TP_SENSITIVITY_DECR 11 + +#define MS_TP_NEG_INERTIA_INCR 12 +#define MS_TP_NEG_INERTIA_DECR 13 + +#define MS_TP_VALUE6_INCR 14 +#define MS_TP_VALUE6_DECR 15 + +#define MS_TP_PTS_THRESHOLD_INCR 16 +#define MS_TP_PTS_THRESHOLD_DECR 17 diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index f2aff2bcc2d..0177416d10d 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -72,6 +72,6 @@ int zmk_endpoints_send_report(uint16_t usage_page); #if IS_ENABLED(CONFIG_ZMK_MOUSE) int zmk_endpoints_send_mouse_report(); -#endif // IS_ENABLE(CONFIG_ZMK_MOUSE) +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) void zmk_endpoints_clear_current(void); diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index d1d3b7d47db..f11f00c1ec7 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -10,9 +10,6 @@ #include #include -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -#include -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) #include #include @@ -23,8 +20,6 @@ #define ZMK_HID_KEYBOARD_NKRO_MAX_USAGE HID_USAGE_KEY_KEYPAD_EQUAL #endif -#define ZMK_HID_MOUSE_NUM_BUTTONS 0x05 - // See https://www.usb.org/sites/default/files/hid1_11.pdf section 6.2.2.4 Main Items #define ZMK_HID_MAIN_VAL_DATA (0x00 << 0) @@ -57,7 +52,6 @@ #define ZMK_HID_REPORT_ID_KEYBOARD 0x01 #define ZMK_HID_REPORT_ID_LEDS 0x01 #define ZMK_HID_REPORT_ID_CONSUMER 0x02 -#define ZMK_HID_REPORT_ID_MOUSE 0x03 static const uint8_t zmk_hid_report_desc[] = { HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), @@ -143,38 +137,6 @@ static const uint8_t zmk_hid_report_desc[] = { HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_ARRAY | ZMK_HID_MAIN_VAL_ABS), HID_END_COLLECTION, -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - HID_USAGE_PAGE(HID_USAGE_GD), - HID_USAGE(HID_USAGE_GD_MOUSE), - HID_COLLECTION(HID_COLLECTION_APPLICATION), - HID_REPORT_ID(ZMK_HID_REPORT_ID_MOUSE), - HID_USAGE(HID_USAGE_GD_POINTER), - HID_COLLECTION(HID_COLLECTION_PHYSICAL), - HID_USAGE_PAGE(HID_USAGE_BUTTON), - HID_USAGE_MIN8(0x1), - HID_USAGE_MAX8(ZMK_HID_MOUSE_NUM_BUTTONS), - HID_LOGICAL_MIN8(0x00), - HID_LOGICAL_MAX8(0x01), - HID_REPORT_SIZE(0x01), - HID_REPORT_COUNT(0x5), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - // Constant padding for the last 3 bits. - HID_REPORT_SIZE(0x03), - HID_REPORT_COUNT(0x01), - HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), - // Some OSes ignore pointer devices without X/Y data. - HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), - HID_USAGE(HID_USAGE_GD_X), - HID_USAGE(HID_USAGE_GD_Y), - HID_USAGE(HID_USAGE_GD_WHEEL), - HID_LOGICAL_MIN8(-0x7F), - HID_LOGICAL_MAX8(0x7F), - HID_REPORT_SIZE(0x08), - HID_REPORT_COUNT(0x03), - HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), - HID_END_COLLECTION, - HID_END_COLLECTION, -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) }; #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) @@ -237,21 +199,6 @@ struct zmk_hid_consumer_report { struct zmk_hid_consumer_report_body body; } __packed; -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -struct zmk_hid_mouse_report_body { - zmk_mouse_button_flags_t buttons; - int8_t d_x; - int8_t d_y; - int8_t d_wheel; -} __packed; - -struct zmk_hid_mouse_report { - uint8_t report_id; - struct zmk_hid_mouse_report_body body; -} __packed; - -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - zmk_mod_flags_t zmk_hid_get_explicit_mods(void); int zmk_hid_register_mod(zmk_mod_t modifier); int zmk_hid_unregister_mod(zmk_mod_t modifier); @@ -278,21 +225,9 @@ int zmk_hid_press(uint32_t usage); int zmk_hid_release(uint32_t usage); bool zmk_hid_is_pressed(uint32_t usage); -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -int zmk_hid_mouse_button_press(zmk_mouse_button_t button); -int zmk_hid_mouse_button_release(zmk_mouse_button_t button); -int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons); -int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons); -void zmk_hid_mouse_clear(void); -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void); struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(void); #if IS_ENABLED(CONFIG_ZMK_USB_BOOT) zmk_hid_boot_report_t *zmk_hid_get_boot_report(); #endif - -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(); -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/hog.h b/app/include/zmk/hog.h index eb6e653f772..4e1603037a5 100644 --- a/app/include/zmk/hog.h +++ b/app/include/zmk/hog.h @@ -11,7 +11,3 @@ int zmk_hog_send_keyboard_report(struct zmk_hid_keyboard_report_body *body); int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *body); - -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *body); -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/mouse/hid.h b/app/include/zmk/mouse/hid.h new file mode 100644 index 00000000000..01fcc9ad7f3 --- /dev/null +++ b/app/include/zmk/mouse/hid.h @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +#include +#include + +#include + +#include +#include + +#define ZMK_MOUSE_HID_NUM_BUTTONS 0x05 + +#define ZMK_MOUSE_HID_REPORT_ID_MOUSE 0x01 + +// Needed until Zephyr offers a 2 byte usage macro +#define HID_USAGE16(idx) \ + HID_ITEM(HID_ITEM_TAG_USAGE, HID_ITEM_TYPE_LOCAL, 2), (idx & 0xFF), (idx >> 8 & 0xFF) + +static const uint8_t zmk_mouse_hid_report_desc[] = { + HID_USAGE_PAGE(HID_USAGE_GD), + HID_USAGE(HID_USAGE_GD_MOUSE), + HID_COLLECTION(HID_COLLECTION_APPLICATION), + HID_REPORT_ID(ZMK_MOUSE_HID_REPORT_ID_MOUSE), + HID_USAGE(HID_USAGE_GD_POINTER), + HID_COLLECTION(HID_COLLECTION_PHYSICAL), + HID_USAGE_PAGE(HID_USAGE_BUTTON), + HID_USAGE_MIN8(0x1), + HID_USAGE_MAX8(ZMK_MOUSE_HID_NUM_BUTTONS), + HID_LOGICAL_MIN8(0x00), + HID_LOGICAL_MAX8(0x01), + HID_REPORT_SIZE(0x01), + HID_REPORT_COUNT(0x5), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + // Constant padding for the last 3 bits. + HID_REPORT_SIZE(0x03), + HID_REPORT_COUNT(0x01), + HID_INPUT(ZMK_HID_MAIN_VAL_CONST | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_ABS), + // Some OSes ignore pointer devices without X/Y data. + HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), + HID_USAGE(HID_USAGE_GD_X), + HID_USAGE(HID_USAGE_GD_Y), + HID_USAGE(HID_USAGE_GD_WHEEL), + HID_LOGICAL_MIN16(0xFF, -0x7F), + HID_LOGICAL_MAX16(0xFF, 0x7F), + HID_REPORT_SIZE(0x10), + HID_REPORT_COUNT(0x03), + HID_INPUT(ZMK_HID_MAIN_VAL_DATA | ZMK_HID_MAIN_VAL_VAR | ZMK_HID_MAIN_VAL_REL), + HID_USAGE_PAGE(HID_USAGE_CONSUMER), + HID_USAGE16(HID_USAGE_CONSUMER_AC_PAN), + 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, +}; + +struct zmk_hid_mouse_report_body { + zmk_mouse_button_flags_t buttons; + int16_t d_x; + int16_t d_y; + int16_t d_scroll_y; + int16_t d_scroll_x; +} __packed; + +struct zmk_hid_mouse_report { + uint8_t report_id; + struct zmk_hid_mouse_report_body body; +} __packed; + +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); + +struct zmk_hid_mouse_report *zmk_mouse_hid_get_mouse_report(); diff --git a/app/include/zmk/mouse/hog.h b/app/include/zmk/mouse/hog.h new file mode 100644 index 00000000000..48a214e554a --- /dev/null +++ b/app/include/zmk/mouse/hog.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_mouse_hog_send_mouse_report(struct zmk_hid_mouse_report_body *body); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/include/zmk/mouse.h b/app/include/zmk/mouse/types.h similarity index 80% rename from app/include/zmk/mouse.h rename to app/include/zmk/mouse/types.h index d873f15689a..c898f001098 100644 --- a/app/include/zmk/mouse.h +++ b/app/include/zmk/mouse/types.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/usb_hid.h b/app/include/zmk/mouse/usb_hid.h new file mode 100644 index 00000000000..12205f28e4c --- /dev/null +++ b/app/include/zmk/mouse/usb_hid.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_mouse_usb_hid_send_mouse_report(void); +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) diff --git a/app/module/drivers/CMakeLists.txt b/app/module/drivers/CMakeLists.txt index 5281c3dcb21..045b266e48f 100644 --- a/app/module/drivers/CMakeLists.txt +++ b/app/module/drivers/CMakeLists.txt @@ -5,3 +5,5 @@ add_subdirectory_ifdef(CONFIG_GPIO gpio) add_subdirectory_ifdef(CONFIG_KSCAN kscan) add_subdirectory_ifdef(CONFIG_SENSOR sensor) add_subdirectory_ifdef(CONFIG_DISPLAY display) +add_subdirectory_ifdef(CONFIG_PS2 ps2) +add_subdirectory_ifdef(CONFIG_INPUT input) diff --git a/app/module/drivers/Kconfig b/app/module/drivers/Kconfig index c57ed3347c9..26051da21b8 100644 --- a/app/module/drivers/Kconfig +++ b/app/module/drivers/Kconfig @@ -5,3 +5,5 @@ rsource "gpio/Kconfig" rsource "kscan/Kconfig" rsource "sensor/Kconfig" rsource "display/Kconfig" +rsource "ps2/Kconfig" +rsource "input/Kconfig" diff --git a/app/module/drivers/input/CMakeLists.txt b/app/module/drivers/input/CMakeLists.txt new file mode 100644 index 00000000000..c85704d650f --- /dev/null +++ b/app/module/drivers/input/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2022 The ZMK Contributors + # SPDX-License-Identifier: MIT + + zephyr_library_amend() + + zephyr_library_sources_ifdef(CONFIG_ZMK_INPUT_MOUSE_PS2 input_mouse_ps2.c) diff --git a/app/module/drivers/input/Kconfig b/app/module/drivers/input/Kconfig new file mode 100644 index 00000000000..790c4afa8e4 --- /dev/null +++ b/app/module/drivers/input/Kconfig @@ -0,0 +1,27 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +DT_COMPAT_ZMK_INPUT_PS2_MOUSE := zmk,input-mouse-ps2 + +config ZMK_INPUT_MOUSE_PS2 + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_INPUT_PS2_MOUSE)) + depends on (!ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL) + select ZMK_MOUSE + select PS2 + +config ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK + bool "Get notifications from the underluing PS2 protocol driver when the received data is invalid and when a resend is requested from the peripheral. This helps the mouse driver to re-align its command buffer and prevent unexpected clicks and jerky movements. With the ps2-uart driver this option is not really necessary, but is very useful for the ps2-gpio driver where mis-transmissions are frequent." + default n + +if ZMK_INPUT_MOUSE_PS2 + +config ZMK_INPUT_MOUSE_PS2_ENABLE_UROB_COMPAT + bool "Makes the driver compatible with urob's zmk fork. His fork has commits that change the default zmk function definitions. This option adopts the code to make it compatible with his changes. Only enable it if you are using his fork or it will fail to compile." + default n + +config ZMK_INPUT_MOUSE_PS2_ENABLE_ERROR_MITIGATION + bool "Tries to mitigate transmission errors. Only useful when using a PS2 driver that is prone to miscommunication like the GPIO bitbanging driver." + default n + +endif # ZMK_INPUT_MOUSE_PS2 diff --git a/app/module/drivers/input/input_mouse_ps2.c b/app/module/drivers/input/input_mouse_ps2.c new file mode 100644 index 00000000000..0c1960f50d0 --- /dev/null +++ b/app/module/drivers/input/input_mouse_ps2.c @@ -0,0 +1,1779 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT zmk_input_mouse_ps2 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +/* + * Settings + */ + +// Delay mouse init to give the mouse time to send the init sequence. +#define ZMK_MOUSE_PS2_INIT_THREAD_DELAY_MS 1000 + +// How often the driver try to initialize a mouse before we give up. +#define MOUSE_PS2_INIT_ATTEMPTS 10 + +// Mouse activity packets are at least three bytes. +// This defines how much time between bytes can pass before +// we give up on the packet and start fresh. +#define MOUSE_PS2_TIMEOUT_ACTIVITY_PACKET K_MSEC(500) + +/* + * PS/2 Defines + */ + +// According to the `IBM TrackPoint System Version 4.0 Engineering +// Specification`... +// "The POR shall be timed to occur 600 ms ± 20 % from the time power is +// applied to the TrackPoint controller." +#define MOUSE_PS2_POWER_ON_RESET_TIME K_MSEC(600) + +// Common PS/2 Mouse commands +#define MOUSE_PS2_CMD_GET_SECONDARY_ID "\xe1" +#define MOUSE_PS2_CMD_GET_SECONDARY_ID_RESP_LEN 2 + +#define MOUSE_PS2_CMD_GET_DEVICE_ID "\xf2" +#define MOUSE_PS2_CMD_GET_DEVICE_ID_RESP_LEN 1 + +#define MOUSE_PS2_CMD_SET_SAMPLING_RATE "\xf3" +#define MOUSE_PS2_CMD_SET_SAMPLING_RATE_RESP_LEN 0 +#define MOUSE_PS2_CMD_SET_SAMPLING_RATE_DEFAULT 100 + +#define MOUSE_PS2_CMD_ENABLE_REPORTING "\xf4" +#define MOUSE_PS2_CMD_ENABLE_REPORTING_RESP_LEN 0 + +#define MOUSE_PS2_CMD_DISABLE_REPORTING "\xf5" +#define MOUSE_PS2_CMD_DISABLE_REPORTING_RESP_LEN 0 + +#define MOUSE_PS2_CMD_RESEND "\xfe" +#define MOUSE_PS2_CMD_RESEND_RESP_LEN 0 + +#define MOUSE_PS2_CMD_RESET "\xff" +#define MOUSE_PS2_CMD_RESET_RESP_LEN 0 + +// Trackpoint Commands +// They can be found in the `IBM TrackPoint System Version 4.0 Engineering +// Specification` (YKT3Eext.pdf)... + +#define MOUSE_PS2_CMD_TP_GET_CONFIG_BYTE "\xe2\x80\x2c" +#define MOUSE_PS2_CMD_TP_GET_CONFIG_BYTE_RESP_LEN 1 + +#define MOUSE_PS2_CMD_TP_SET_CONFIG_BYTE "\xe2\x81\x2c" +#define MOUSE_PS2_CMD_TP_SET_CONFIG_BYTE_RESP_LEN 0 + +#define MOUSE_PS2_ST_TP_SENSITIVITY "tp_sensitivity" +#define MOUSE_PS2_CMD_TP_GET_SENSITIVITY "\xe2\x80\x4a" +#define MOUSE_PS2_CMD_TP_GET_SENSITIVITY_RESP_LEN 1 + +#define MOUSE_PS2_CMD_TP_SET_SENSITIVITY "\xe2\x81\x4a" +#define MOUSE_PS2_CMD_TP_SET_SENSITIVITY_RESP_LEN 0 +#define MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MIN 0 +#define MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MAX 255 +#define MOUSE_PS2_CMD_TP_SET_SENSITIVITY_DEFAULT 128 + +#define MOUSE_PS2_ST_TP_NEG_INERTIA "tp_neg_inertia" +#define MOUSE_PS2_CMD_TP_GET_NEG_INERTIA "\xe2\x80\x4d" +#define MOUSE_PS2_CMD_TP_GET_NEG_INERTIA_RESP_LEN 1 + +#define MOUSE_PS2_CMD_TP_SET_NEG_INERTIA "\xe2\x81\x4d" +#define MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_RESP_LEN 0 +#define MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MIN 0 +#define MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MAX 255 +#define MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_DEFAULT 0x06 + +#define MOUSE_PS2_ST_TP_VALUE6 "tp_value6" +#define MOUSE_PS2_CMD_TP_GET_VALUE6_UPPER_PLATEAU_SPEED "\xe2\x80\x60" +#define MOUSE_PS2_CMD_TP_GET_VALUE6_UPPER_PLATEAU_SPEED_RESP_LEN 1 + +#define MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED "\xe2\x81\x60" +#define MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_RESP_LEN 0 +#define MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MIN 0 +#define MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MAX 255 +#define MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_DEFAULT 0x61 + +#define MOUSE_PS2_ST_TP_PTS_THRESHOLD "tp_pts_threshold" +#define MOUSE_PS2_CMD_TP_GET_PTS_THRESHOLD "\xe2\x80\x5c" +#define MOUSE_PS2_CMD_TP_GET_PTS_THRESHOLD_RESP_LEN 1 + +#define MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD "\xe2\x81\x5c" +#define MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_RESP_LEN 0 +#define MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MIN 0 +#define MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MAX 255 +#define MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_DEFAULT 0x08 + +// Trackpoint Config Bits +#define MOUSE_PS2_TP_CONFIG_BIT_PRESS_TO_SELECT 0x00 +#define MOUSE_PS2_TP_CONFIG_BIT_RESERVED 0x01 +#define MOUSE_PS2_TP_CONFIG_BIT_BUTTON2 0x02 +#define MOUSE_PS2_TP_CONFIG_BIT_INVERT_X 0x03 +#define MOUSE_PS2_TP_CONFIG_BIT_INVERT_Y 0x04 +#define MOUSE_PS2_TP_CONFIG_BIT_INVERT_Z 0x05 +#define MOUSE_PS2_TP_CONFIG_BIT_SWAP_XY 0x06 +#define MOUSE_PS2_TP_CONFIG_BIT_FORCE_TRANSPARENT 0x07 + +// Responses +#define MOUSE_PS2_RESP_SELF_TEST_PASS 0xaa +#define MOUSE_PS2_RESP_SELF_TEST_FAIL 0xfc + +/* + * ZMK Defines + */ + +#define MOUSE_PS2_BUTTON_L_IDX 0 +#define MOUSE_PS2_BUTTON_R_IDX 1 +#define MOUSE_PS2_BUTTON_M_IDX 3 + +#define MOUSE_PS2_THREAD_STACK_SIZE 1024 +#define MOUSE_PS2_THREAD_PRIORITY 10 + +/* + * Global Variables + */ + +#define MOUSE_PS2_SETTINGS_SUBTREE "mouse_ps2" + +typedef enum { + MOUSE_PS2_PACKET_MODE_PS2_DEFAULT, + MOUSE_PS2_PACKET_MODE_SCROLL, +} zmk_mouse_ps2_packet_mode; + +struct zmk_mouse_ps2_config { + const struct device *ps2_device; + struct gpio_dt_spec rst_gpio; + + bool scroll_mode; + bool disable_clicking; + int sampling_rate; + + bool tp_press_to_select; + int tp_press_to_select_threshold; + int tp_sensitivity; + int tp_neg_inertia; + int tp_val6_upper_speed; + bool tp_x_invert; + bool tp_y_invert; + bool tp_xy_swap; +}; + +struct zmk_mouse_ps2_packet { + int16_t mov_x; + int16_t mov_y; + int8_t scroll; + bool overflow_x; + bool overflow_y; + bool button_l; + bool button_m; + bool button_r; +}; + +struct zmk_mouse_ps2_data { + const struct device *dev; + struct gpio_dt_spec rst_gpio; /* GPIO used for Power-On-Reset line */ + + K_THREAD_STACK_MEMBER(thread_stack, MOUSE_PS2_THREAD_STACK_SIZE); + struct k_thread thread; + + zmk_mouse_ps2_packet_mode packet_mode; + uint8_t packet_buffer[4]; + int packet_idx; + struct zmk_mouse_ps2_packet prev_packet; + struct k_work_delayable packet_buffer_timeout; + + bool button_l_is_held; + bool button_m_is_held; + bool button_r_is_held; + + bool activity_reporting_on; + bool is_trackpoint; + + uint8_t sampling_rate; + uint8_t tp_sensitivity; + uint8_t tp_neg_inertia; + uint8_t tp_value6; + uint8_t tp_pts_threshold; +}; + +static const struct zmk_mouse_ps2_config zmk_mouse_ps2_config = { + .ps2_device = DEVICE_DT_GET(DT_INST_PHANDLE(0, ps2_device)), + +#if DT_INST_NODE_HAS_PROP(0, rst_gpios) + .rst_gpio = GPIO_DT_SPEC_INST_GET(0, rst_gpios), +#else + .rst_gpio = + { + .port = NULL, + .pin = 0, + .dt_flags = 0, + }, +#endif + + .scroll_mode = DT_INST_PROP_OR(0, scroll_mode, false), + .disable_clicking = DT_INST_PROP_OR(0, disable_clicking, false), + .sampling_rate = DT_INST_PROP_OR(0, sampling_rate, MOUSE_PS2_CMD_SET_SAMPLING_RATE_DEFAULT), + .tp_press_to_select = DT_INST_PROP_OR(0, tp_press_to_select, false), + .tp_press_to_select_threshold = DT_INST_PROP_OR(0, tp_press_to_select_threshold, -1), + .tp_sensitivity = DT_INST_PROP_OR(0, tp_sensitivity, -1), + .tp_neg_inertia = DT_INST_PROP_OR(0, tp_neg_inertia, -1), + .tp_val6_upper_speed = DT_INST_PROP_OR(0, tp_val6_upper_speed, -1), + .tp_x_invert = DT_INST_PROP_OR(0, tp_x_invert, false), + .tp_y_invert = DT_INST_PROP_OR(0, tp_y_invert, false), + .tp_xy_swap = DT_INST_PROP_OR(0, tp_xy_swap, false), +}; + +static struct zmk_mouse_ps2_data zmk_mouse_ps2_data = { + .packet_mode = MOUSE_PS2_PACKET_MODE_PS2_DEFAULT, + .packet_idx = 0, + .prev_packet = + { + .button_l = false, + .button_r = false, + .button_m = false, + .overflow_x = 0, + .overflow_y = 0, + .mov_x = 0, + .mov_y = 0, + .scroll = 0, + }, + + .button_l_is_held = false, + .button_m_is_held = false, + .button_r_is_held = false, + + // Data reporting is disabled on init + .activity_reporting_on = false, + .is_trackpoint = false, + + // PS2 devices initialize with this rate + .sampling_rate = MOUSE_PS2_CMD_SET_SAMPLING_RATE_DEFAULT, + .tp_sensitivity = MOUSE_PS2_CMD_TP_SET_SENSITIVITY_DEFAULT, + .tp_neg_inertia = MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_DEFAULT, + .tp_value6 = MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_DEFAULT, + .tp_pts_threshold = MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_DEFAULT, +}; + +static int allowed_sampling_rates[] = { + 10, 20, 40, 60, 80, 100, 200, +}; + +/* + * Function Definitions + */ + +int zmk_mouse_ps2_settings_save(); + +/* + * Helpers + */ + +#define MOUSE_PS2_GET_BIT(data, bit_pos) ((data >> bit_pos) & 0x1) +#define MOUSE_PS2_SET_BIT(data, bit_val, bit_pos) (data |= (bit_val) << bit_pos) + +/* + * Mouse Activity Packet Reading + */ + +void zmk_mouse_ps2_activity_process_cmd(zmk_mouse_ps2_packet_mode packet_mode, uint8_t packet_state, + uint8_t packet_x, uint8_t packet_y, uint8_t packet_extra); +void zmk_mouse_ps2_activity_abort_cmd(); +void zmk_mouse_ps2_activity_move_mouse(int16_t mov_x, int16_t mov_y); +void zmk_mouse_ps2_activity_scroll(int8_t scroll_y); +void zmk_mouse_ps2_activity_click_buttons(bool button_l, bool button_m, bool button_r); +void zmk_mouse_ps2_activity_reset_packet_buffer(); +struct zmk_mouse_ps2_packet +zmk_mouse_ps2_activity_parse_packet_buffer(zmk_mouse_ps2_packet_mode packet_mode, + uint8_t packet_state, uint8_t packet_x, uint8_t packet_y, + uint8_t packet_extra); +void zmk_mouse_ps2_activity_toggle_layer(); + +// Called by the PS/2 driver whenver the mouse sends a byte and +// reporting is enabled through `zmk_mouse_ps2_activity_reporting_enable`. +void zmk_mouse_ps2_activity_callback(const struct device *ps2_device, uint8_t byte) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + k_work_cancel_delayable(&data->packet_buffer_timeout); + + // LOG_DBG("Received mouse movement data: 0x%x", byte); + + data->packet_buffer[data->packet_idx] = byte; + + if (data->packet_idx == 0) { + + // Bit 3 of the first command byte should always be 1 + // If it is not, then we are definitely out of alignment. + // So we ask the device to resend the entire 3-byte command + // again. + int alignment_bit = MOUSE_PS2_GET_BIT(byte, 3); + if (alignment_bit != 1) { + + zmk_mouse_ps2_activity_abort_cmd("Bit 3 of packet is 0 instead of 1"); + return; + } + } else if (data->packet_idx == 1) { + // Do nothing + } else if ((data->packet_mode == MOUSE_PS2_PACKET_MODE_PS2_DEFAULT && data->packet_idx == 2) || + (data->packet_mode == MOUSE_PS2_PACKET_MODE_SCROLL && data->packet_idx == 3)) { + + zmk_mouse_ps2_activity_process_cmd(data->packet_mode, data->packet_buffer[0], + data->packet_buffer[1], data->packet_buffer[2], + data->packet_buffer[3]); + zmk_mouse_ps2_activity_reset_packet_buffer(); + return; + } + + data->packet_idx += 1; + + k_work_schedule(&data->packet_buffer_timeout, MOUSE_PS2_TIMEOUT_ACTIVITY_PACKET); +} + +void zmk_mouse_ps2_activity_abort_cmd(char *reason) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + const struct device *ps2_device = config->ps2_device; + + LOG_ERR("PS/2 Mouse cmd buffer is out of aligment. Requesting resend: %s", reason); + + data->packet_idx = 0; + ps2_write(ps2_device, MOUSE_PS2_CMD_RESEND[0]); + + zmk_mouse_ps2_activity_reset_packet_buffer(); +} + +#if IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK) + +// Called if the PS/2 driver encounters a transmission error and asks the +// device to resend the packet. +// The device will resend all bytes of the packet. So we need to reset our +// buffer. +void zmk_mouse_ps2_activity_resend_callback(const struct device *ps2_device) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + LOG_WRN("Mouse movement cmd had transmission error on idx=%d", data->packet_idx); + + zmk_mouse_ps2_activity_reset_packet_buffer(); +} + +#endif /* IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK) */ + +// Called if no new byte arrives within +// MOUSE_PS2_TIMEOUT_ACTIVITY_PACKET +void zmk_mouse_ps2_activity_packet_timout(struct k_work *item) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + LOG_DBG("Mouse movement cmd timed out on idx=%d", data->packet_idx); + + // Reset the cmd buffer in case we are out of alignment. + // This way if the mouse ever gets out of alignment, the user + // can reset it by just not moving it for a second. + zmk_mouse_ps2_activity_reset_packet_buffer(); +} + +void zmk_mouse_ps2_activity_reset_packet_buffer() { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + data->packet_idx = 0; + memset(data->packet_buffer, 0x0, sizeof(data->packet_buffer)); +} + +void zmk_mouse_ps2_activity_process_cmd(zmk_mouse_ps2_packet_mode packet_mode, uint8_t packet_state, + uint8_t packet_x, uint8_t packet_y, uint8_t packet_extra) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + struct zmk_mouse_ps2_packet packet; + packet = zmk_mouse_ps2_activity_parse_packet_buffer(packet_mode, packet_state, packet_x, + packet_y, packet_extra); + + int x_delta = abs(data->prev_packet.mov_x - packet.mov_x); + int y_delta = abs(data->prev_packet.mov_y - packet.mov_y); + + LOG_DBG("Got mouse activity cmd " + "(mov_x=%d, mov_y=%d, o_x=%d, o_y=%d, scroll=%d, " + "b_l=%d, b_m=%d, b_r=%d) and (" + "x_delta=%d, y_delta=%d)", + packet.mov_x, packet.mov_y, packet.overflow_x, packet.overflow_y, packet.scroll, + packet.button_l, packet.button_m, packet.button_r, x_delta, y_delta); + +#if IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_ERROR_MITIGATION) + if (packet.overflow_x == 1 && packet.overflow_y == 1) { + LOG_WRN("Detected overflow in both x and y. " + "Probably mistransmission. Aborting..."); + + zmk_mouse_ps2_activity_abort_cmd("Overflow in both x and y"); + return; + } + + // If the mouse exceeds the allowed threshold of movement, it's probably + // a mistransmission or misalignment. + // But we only do this check if there was prior movement that wasn't + // reset in `zmk_mouse_ps2_activity_packet_timout`. + if ((packet.mov_x != 0 && packet.mov_y != 0) && (x_delta > 150 || y_delta > 150)) { + LOG_WRN("Detected malformed packet with " + "(mov_x=%d, mov_y=%d, o_x=%d, o_y=%d, scroll=%d, " + "b_l=%d, b_m=%d, b_r=%d) and (" + "x_delta=%d, y_delta=%d)", + packet.mov_x, packet.mov_y, packet.overflow_x, packet.overflow_y, packet.scroll, + packet.button_l, packet.button_m, packet.button_r, x_delta, y_delta); + zmk_mouse_ps2_activity_abort_cmd("Exceeds movement threshold."); + return; + } +#endif + + zmk_mouse_ps2_activity_move_mouse(packet.mov_x, packet.mov_y); + zmk_mouse_ps2_activity_click_buttons(packet.button_l, packet.button_m, packet.button_r); + + data->prev_packet = packet; +} + +struct zmk_mouse_ps2_packet +zmk_mouse_ps2_activity_parse_packet_buffer(zmk_mouse_ps2_packet_mode packet_mode, + uint8_t packet_state, uint8_t packet_x, uint8_t packet_y, + uint8_t packet_extra) { + struct zmk_mouse_ps2_packet packet; + + packet.button_l = MOUSE_PS2_GET_BIT(packet_state, 0); + packet.button_r = MOUSE_PS2_GET_BIT(packet_state, 1); + packet.button_m = MOUSE_PS2_GET_BIT(packet_state, 2); + packet.overflow_x = MOUSE_PS2_GET_BIT(packet_state, 6); + packet.overflow_y = MOUSE_PS2_GET_BIT(packet_state, 7); + packet.scroll = 0; + + // The coordinates are delivered as a signed 9bit integers. + // But a PS/2 packet is only 8 bits, so the most significant + // bit with the sign is stored inside the state packet. + // + // Since we are converting the uint8_t into a int16_t + // we must pad the unused most significant bits with + // the sign bit. + // + // Example: + // ↓ x sign bit + // - State: 0x18 ( 0001 1000) + // ↑ y sign bit + // - X: 0xfd ( 1111 1101) / decimal 253 + // - New X: (1111 1111 1111 1101) / decimal -3 + // + // - Y: 0x02 ( 0000 0010) / decimal 2 + // - New Y: (0000 0000 0000 0010) / decimal 2 + // + // The code below creates a signed int and is from... + // https://wiki.osdev.org/PS/2_Mouse + packet.mov_x = packet_x - ((packet_state << 4) & 0x100); + packet.mov_y = packet_y - ((packet_state << 3) & 0x100); + + // If packet mode scroll or scroll+5 buttons is used, + // then the first 4 bit of the extra byte are used for the + // scroll wheel. It is a signed number with the rango of + // -8 to +7. + if (packet_mode == MOUSE_PS2_PACKET_MODE_SCROLL) { + MOUSE_PS2_SET_BIT(packet.scroll, MOUSE_PS2_GET_BIT(packet_extra, 0), 0); + MOUSE_PS2_SET_BIT(packet.scroll, MOUSE_PS2_GET_BIT(packet_extra, 1), 1); + MOUSE_PS2_SET_BIT(packet.scroll, MOUSE_PS2_GET_BIT(packet_extra, 2), 2); + packet.scroll = packet_extra - ((packet.scroll << 3) & 0x100); + } + + return packet; +} + +/* + * Mouse Moving and Clicking + */ + +static bool zmk_mouse_ps2_is_non_zero_1d_movement(int16_t speed) { return speed != 0; } + +void zmk_mouse_ps2_activity_move_mouse(int16_t mov_x, int16_t mov_y) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + int ret = 0; + + bool have_x = zmk_mouse_ps2_is_non_zero_1d_movement(mov_x); + bool have_y = zmk_mouse_ps2_is_non_zero_1d_movement(mov_y); + + if (have_x) { + ret = input_report_rel(data->dev, INPUT_REL_X, mov_x, !have_y, K_NO_WAIT); + } + if (have_y) { + ret = input_report_rel(data->dev, INPUT_REL_Y, mov_y, true, K_NO_WAIT); + } +} + +void zmk_mouse_ps2_activity_click_buttons(bool button_l, bool button_m, bool button_r) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + + // TODO: Integrate this with the proper button mask instead + // of hardcoding the mouse button indeces. + // Check hid.c and zmk_hid_mouse_buttons_press() for more info. + + int buttons_pressed = 0; + int buttons_released = 0; + + // First we check which mouse button press states have changed + bool button_l_pressed = false; + bool button_l_released = false; + if (button_l == true && data->button_l_is_held == false) { + LOG_INF("Pressed button_l"); + + button_l_pressed = true; + buttons_pressed++; + } else if (button_l == false && data->button_l_is_held == true) { + LOG_INF("Releasing button_l"); + + button_l_released = true; + buttons_released++; + } + + bool button_m_released = false; + bool button_m_pressed = false; + if (button_m == true && data->button_m_is_held == false) { + LOG_INF("Pressing button_m"); + + button_m_pressed = true; + buttons_pressed++; + } else if (button_m == false && data->button_m_is_held == true) { + LOG_INF("Releasing button_m"); + + button_m_released = true; + buttons_released++; + } + + bool button_r_released = false; + bool button_r_pressed = false; + if (button_r == true && data->button_r_is_held == false) { + LOG_INF("Pressing button_r"); + + button_r_pressed = true; + buttons_pressed++; + } else if (button_r == false && data->button_r_is_held == true) { + LOG_INF("Releasing button_r"); + + button_r_released = true; + buttons_released++; + } + + // Then we check if this is likely a transmission error + if (buttons_pressed > 1 || buttons_released > 1) { + LOG_WRN("Ignoring button presses: Received %d button presses " + "and %d button releases in one packet. " + "Probably tranmission error.", + buttons_pressed, buttons_released); + + zmk_mouse_ps2_activity_abort_cmd("Multiple button presses"); + return; + } + + if (config->disable_clicking != true) { + // If it wasn't, we actually send the events. + if (buttons_pressed > 0 || buttons_released > 0) { + + int buttons_need_reporting = buttons_pressed + buttons_released; + + // Left button + if (button_l_pressed) { + + input_report_key(data->dev, INPUT_BTN_0, 1, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_l_is_held = true; + } else if (button_l_released) { + + input_report_key(data->dev, INPUT_BTN_0, 0, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_l_is_held = false; + } + + buttons_need_reporting--; + + // Right button + if (button_r_pressed) { + + input_report_key(data->dev, INPUT_BTN_1, 1, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_r_is_held = true; + } else if (button_r_released) { + + input_report_key(data->dev, INPUT_BTN_1, 0, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_r_is_held = false; + } + + buttons_need_reporting--; + + // Middle Button + if (button_m_pressed) { + + input_report_key(data->dev, INPUT_BTN_2, 1, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_m_is_held = true; + } else if (button_m_released) { + + input_report_key(data->dev, INPUT_BTN_2, 0, + buttons_need_reporting == 1 ? true : false, K_FOREVER); + data->button_m_is_held = false; + } + } + } +} + +/* + * PS/2 Command Sending Wrapper + */ +int zmk_mouse_ps2_activity_reporting_enable(); +int zmk_mouse_ps2_activity_reporting_disable(); + +struct zmk_mouse_ps2_send_cmd_resp { + int err; + char err_msg[80]; + uint8_t resp_buffer[8]; + int resp_len; +}; + +struct zmk_mouse_ps2_send_cmd_resp zmk_mouse_ps2_send_cmd(char *cmd, int cmd_len, uint8_t *arg, + int resp_len, bool pause_reporting) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + const struct device *ps2_device = config->ps2_device; + int err = 0; + bool prev_activity_reporting_on = data->activity_reporting_on; + + struct zmk_mouse_ps2_send_cmd_resp resp = { + .err = 0, + .err_msg = "", + .resp_len = 0, + }; + memset(resp.resp_buffer, 0x0, sizeof(resp.resp_buffer)); + + // Don't send the string termination NULL byte + int cmd_bytes = cmd_len - 1; + if (cmd_bytes < 1) { + err = 1; + snprintf(resp.err_msg, sizeof(resp.err_msg), + "Cannot send cmd with less than 1 byte length"); + + return resp; + } + + if (resp_len > sizeof(resp.resp_buffer)) { + err = 2; + snprintf(resp.err_msg, sizeof(resp.err_msg), + "Response can't be longer than the resp_buffer (%d)", sizeof(resp.err_msg)); + + return resp; + } + + if (pause_reporting == true && data->activity_reporting_on == true) { + LOG_DBG("Disabling mouse activity reporting..."); + + err = zmk_mouse_ps2_activity_reporting_disable(); + if (err) { + resp.err = err; + snprintf(resp.err_msg, sizeof(resp.err_msg), "Could not disable data reporting (%d)", + err); + } + } + + if (resp.err == 0) { + LOG_DBG("Sending cmd..."); + + for (int i = 0; i < cmd_bytes; i++) { + err = ps2_write(ps2_device, cmd[i]); + if (err) { + resp.err = err; + snprintf(resp.err_msg, sizeof(resp.err_msg), "Could not send cmd byte %d/%d (%d)", + i + 1, cmd_bytes, err); + break; + } + } + } + + if (resp.err == 0 && arg != NULL) { + LOG_DBG("Sending arg..."); + err = ps2_write(ps2_device, *arg); + if (err) { + resp.err = err; + snprintf(resp.err_msg, sizeof(resp.err_msg), "Could not send arg (%d)", err); + } + } + + if (resp.err == 0 && resp_len > 0) { + LOG_DBG("Reading response..."); + for (int i = 0; i < resp_len; i++) { + err = ps2_read(ps2_device, &resp.resp_buffer[i]); + if (err) { + resp.err = err; + snprintf(resp.err_msg, sizeof(resp.err_msg), + "Could not read response cmd byte %d/%d (%d)", i + 1, resp_len, err); + break; + } + } + } + + if (pause_reporting == true && prev_activity_reporting_on == true) { + LOG_DBG("Enabling mouse activity reporting..."); + + err = zmk_mouse_ps2_activity_reporting_enable(); + if (err) { + // Don' overwrite existing error + if (resp.err == 0) { + resp.err = err; + snprintf(resp.err_msg, sizeof(resp.err_msg), + "Could not re-enable data reporting (%d)", err); + } + } + } + + return resp; +} + +int zmk_mouse_ps2_activity_reporting_enable() { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + const struct device *ps2_device = config->ps2_device; + + if (data->activity_reporting_on == true) { + return 0; + } + + uint8_t cmd = MOUSE_PS2_CMD_ENABLE_REPORTING[0]; + int err = ps2_write(ps2_device, cmd); + if (err) { + LOG_ERR("Could not enable data reporting: %d", err); + return err; + } + + err = ps2_enable_callback(ps2_device); + if (err) { + LOG_ERR("Could not enable ps2 callback: %d", err); + return err; + } + + data->activity_reporting_on = true; + + return 0; +} + +int zmk_mouse_ps2_activity_reporting_disable() { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + const struct device *ps2_device = config->ps2_device; + + if (data->activity_reporting_on == false) { + return 0; + } + + uint8_t cmd = MOUSE_PS2_CMD_DISABLE_REPORTING[0]; + int err = ps2_write(ps2_device, cmd); + if (err) { + LOG_ERR("Could not disable data reporting: %d", err); + return err; + } + + err = ps2_disable_callback(ps2_device); + if (err) { + LOG_ERR("Could not disable ps2 callback: %d", err); + return err; + } + + data->activity_reporting_on = false; + + return 0; +} + +/* + * PS/2 Command Helpers + */ + +int zmk_mouse_ps2_array_get_elem_index(int elem, int *array, size_t array_size) { + int elem_index = -1; + for (int i = 0; i < array_size; i++) { + if (array[i] == elem) { + elem_index = i; + break; + } + } + + return elem_index; +} + +int zmk_mouse_ps2_array_get_next_elem(int elem, int *array, size_t array_size) { + int elem_index = zmk_mouse_ps2_array_get_elem_index(elem, array, array_size); + if (elem_index == -1) { + return -1; + } + + int next_index = elem_index + 1; + if (next_index >= array_size) { + return -1; + } + + return array[next_index]; +} + +int zmk_mouse_ps2_array_get_prev_elem(int elem, int *array, size_t array_size) { + int elem_index = zmk_mouse_ps2_array_get_elem_index(elem, array, array_size); + if (elem_index == -1) { + return -1; + } + + int prev_index = elem_index - 1; + if (prev_index < 0 || prev_index >= array_size) { + return -1; + } + + return array[prev_index]; +} + +/* + * PS/2 Commands + */ + +int zmk_mouse_ps2_reset(const struct device *ps2_device) { + struct zmk_mouse_ps2_send_cmd_resp resp = + zmk_mouse_ps2_send_cmd(MOUSE_PS2_CMD_RESET, sizeof(MOUSE_PS2_CMD_RESET), NULL, + MOUSE_PS2_CMD_RESET_RESP_LEN, false); + if (resp.err) { + LOG_ERR("Could not send reset cmd"); + } + + return resp.err; +} + +int zmk_mouse_ps2_get_secondary_id(uint8_t *resp_byte_1, uint8_t *resp_byte_2) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_GET_SECONDARY_ID, sizeof(MOUSE_PS2_CMD_GET_SECONDARY_ID), NULL, + MOUSE_PS2_CMD_GET_SECONDARY_ID_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not get secondary id"); + return resp.err; + } + + *resp_byte_1 = resp.resp_buffer[0]; + *resp_byte_2 = resp.resp_buffer[1]; + + return 0; +} + +int zmk_mouse_ps2_set_sampling_rate(uint8_t sampling_rate) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + int rate_idx = zmk_mouse_ps2_array_get_elem_index(sampling_rate, allowed_sampling_rates, + sizeof(allowed_sampling_rates)); + if (rate_idx == -1) { + LOG_ERR("Requested to set illegal sampling rate: %d", sampling_rate); + return -1; + } + + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_SET_SAMPLING_RATE, sizeof(MOUSE_PS2_CMD_SET_SAMPLING_RATE), &sampling_rate, + MOUSE_PS2_CMD_SET_SAMPLING_RATE_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set sample rate to %d", sampling_rate); + return resp.err; + } + + data->sampling_rate = sampling_rate; + + LOG_INF("Successfully set sampling rate to %d", sampling_rate); + + return resp.err; +} + +int zmk_mouse_ps2_get_device_id(uint8_t *device_id) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_GET_DEVICE_ID, sizeof(MOUSE_PS2_CMD_GET_DEVICE_ID), NULL, 1, true); + if (resp.err) { + LOG_ERR("Could not get device id"); + return resp.err; + } + + *device_id = resp.resp_buffer[0]; + + return 0; +} + +int zmk_mouse_ps2_set_packet_mode(zmk_mouse_ps2_packet_mode mode) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + if (mode == MOUSE_PS2_PACKET_MODE_PS2_DEFAULT) { + // Do nothing. Mouse devices enable this by + // default. + return 0; + } + + bool prev_activity_reporting_on = data->activity_reporting_on; + zmk_mouse_ps2_activity_reporting_disable(); + + // Setting a mouse mode is a bit like using a cheat code + // in a video game. + // You have to send a specific sequence of sampling rates. + if (mode == MOUSE_PS2_PACKET_MODE_SCROLL) { + + zmk_mouse_ps2_set_sampling_rate(200); + zmk_mouse_ps2_set_sampling_rate(100); + zmk_mouse_ps2_set_sampling_rate(80); + } + + // Scroll mouse + 5 buttons mode can be enabled with the + // following sequence, but since I don't have a mouse to + // test it, I am commenting it out for now. + // else if(mode == MOUSE_PS2_PACKET_MODE_SCROLL_5_BUTTONS) { + + // zmk_mouse_ps2_set_sampling_rate(200); + // zmk_mouse_ps2_set_sampling_rate(200); + // zmk_mouse_ps2_set_sampling_rate(80); + // } + + uint8_t device_id; + int err = zmk_mouse_ps2_get_device_id(&device_id); + if (err) { + LOG_ERR("Could not enable packet mode %d. Failed to get device id with " + "error %d", + mode, err); + } else { + if (device_id == 0x00) { + LOG_ERR("Could not enable packet mode %d. The device does not " + "support it", + mode); + + data->packet_mode = MOUSE_PS2_PACKET_MODE_PS2_DEFAULT; + err = 1; + } else if (device_id == 0x03 || device_id == 0x04) { + LOG_INF("Successfully activated packet mode %d. Mouse returned " + "device id: %d", + mode, device_id); + + data->packet_mode = MOUSE_PS2_PACKET_MODE_SCROLL; + err = 0; + } + // else if(device_id == 0x04) { + // LOG_INF( + // "Successfully activated packet mode %d. Mouse returned device " + // "id: %d", mode, device_id + // ); + + // data->packet_mode = MOUSE_PS2_PACKET_MODE_SCROLL_5_BUTTONS; + // err = 0; + // } + else { + LOG_ERR("Could not enable packet mode %d. Received an invalid " + "device id: %d", + mode, device_id); + + data->packet_mode = MOUSE_PS2_PACKET_MODE_PS2_DEFAULT; + err = 1; + } + } + + // Restore sampling rate to prev value + zmk_mouse_ps2_set_sampling_rate(data->sampling_rate); + + if (prev_activity_reporting_on == true) { + zmk_mouse_ps2_activity_reporting_enable(); + } + + return err; +} + +/* + * Trackpoint Commands + */ + +bool zmk_mouse_ps2_is_device_trackpoint() { + bool ret = false; + + uint8_t second_id_1, second_id_2; + int err = zmk_mouse_ps2_get_secondary_id(&second_id_1, &second_id_2); + if (err) { + // Not all devices implement this command. + ret = false; + } else { + if (second_id_1 == 0x1) { + ret = true; + } + } + + LOG_DBG("Connected device is a trackpoint: %d", ret); + + return ret; +} + +int zmk_mouse_ps2_tp_get_config_byte(uint8_t *config_byte) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_GET_CONFIG_BYTE, sizeof(MOUSE_PS2_CMD_TP_GET_CONFIG_BYTE), NULL, + MOUSE_PS2_CMD_TP_GET_CONFIG_BYTE_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not read trackpoint config byte"); + return resp.err; + } + + *config_byte = resp.resp_buffer[0]; + + return 0; +} + +int zmk_mouse_ps2_tp_set_config_option(int config_bit, bool enabled, char *descr) { + uint8_t config_byte; + int err = zmk_mouse_ps2_tp_get_config_byte(&config_byte); + if (err) { + return err; + } + + bool is_enabled = MOUSE_PS2_GET_BIT(config_byte, config_bit); + + if (is_enabled == enabled) { + LOG_DBG("Trackpoint %s was already %s... not doing anything.", descr, + is_enabled ? "enabled" : "disabled"); + return 0; + } + + LOG_DBG("Setting trackpoint %s: %s", descr, enabled ? "enabled" : "disabled"); + + MOUSE_PS2_SET_BIT(config_byte, enabled, config_bit); + + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_SET_CONFIG_BYTE, sizeof(MOUSE_PS2_CMD_TP_SET_CONFIG_BYTE), &config_byte, + MOUSE_PS2_CMD_TP_SET_CONFIG_BYTE_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set trackpoint %s to %s", descr, enabled ? "enabled" : "disabled"); + return resp.err; + } + + LOG_INF("Successfully set config option %s to %s", descr, enabled ? "enabled" : "disabled"); + + return 0; +} + +int zmk_mouse_ps2_tp_press_to_select_set(bool enabled) { + int err = zmk_mouse_ps2_tp_set_config_option(MOUSE_PS2_TP_CONFIG_BIT_PRESS_TO_SELECT, enabled, + "Press To Select"); + + return err; +} + +int zmk_mouse_ps2_tp_invert_x_set(bool enabled) { + int err = + zmk_mouse_ps2_tp_set_config_option(MOUSE_PS2_TP_CONFIG_BIT_INVERT_X, enabled, "Invert X"); + + return err; +} + +int zmk_mouse_ps2_tp_invert_y_set(bool enabled) { + int err = + zmk_mouse_ps2_tp_set_config_option(MOUSE_PS2_TP_CONFIG_BIT_INVERT_Y, enabled, "Invert Y"); + + return err; +} + +int zmk_mouse_ps2_tp_swap_xy_set(bool enabled) { + int err = + zmk_mouse_ps2_tp_set_config_option(MOUSE_PS2_TP_CONFIG_BIT_SWAP_XY, enabled, "Swap XY"); + + return err; +} + +int zmk_mouse_ps2_tp_sensitivity_get(uint8_t *sensitivity) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_GET_SENSITIVITY, sizeof(MOUSE_PS2_CMD_TP_GET_SENSITIVITY), NULL, + MOUSE_PS2_CMD_TP_GET_SENSITIVITY_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not get trackpoint sensitivity"); + return resp.err; + } + + // Convert uint8_t to float + // 0x80 (128) represents 1.0 + uint8_t sensitivity_int = resp.resp_buffer[0]; + *sensitivity = sensitivity_int; + + LOG_DBG("Trackpoint sensitivity is %d", sensitivity_int); + + return 0; +} + +int zmk_mouse_ps2_tp_sensitivity_set(int sensitivity) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + if (sensitivity < MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MIN || + sensitivity > MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MAX) { + LOG_ERR("Invalid sensitivity value %d. Min: %d; Max: %d", sensitivity, + MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MIN, MOUSE_PS2_CMD_TP_SET_SENSITIVITY_MAX); + return 1; + } + + uint8_t arg = sensitivity; + + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_SET_SENSITIVITY, sizeof(MOUSE_PS2_CMD_TP_SET_SENSITIVITY), &arg, + MOUSE_PS2_CMD_TP_SET_SENSITIVITY_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set sensitivity to %d", sensitivity); + return resp.err; + } + + data->tp_sensitivity = sensitivity; + + LOG_INF("Successfully set TP sensitivity to %d", sensitivity); + + return 0; +} + +int zmk_mouse_ps2_tp_sensitivity_change(int amount) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + int new_val = data->tp_sensitivity + amount; + + LOG_INF("Setting trackpoint sensitivity to %d", new_val); + int err = zmk_mouse_ps2_tp_sensitivity_set(new_val); + if (err == 0) { + + zmk_mouse_ps2_settings_save(); + } + + return err; +} + +int zmk_mouse_ps2_tp_negative_inertia_get(uint8_t *neg_inertia) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_GET_NEG_INERTIA, sizeof(MOUSE_PS2_CMD_TP_GET_NEG_INERTIA), NULL, + MOUSE_PS2_CMD_TP_GET_NEG_INERTIA_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not get trackpoint negative inertia"); + return resp.err; + } + + uint8_t neg_inertia_int = resp.resp_buffer[0]; + *neg_inertia = neg_inertia_int; + + LOG_DBG("Trackpoint negative inertia is %d", neg_inertia_int); + + return 0; +} + +int zmk_mouse_ps2_tp_neg_inertia_set(int neg_inertia) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + if (neg_inertia < MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MIN || + neg_inertia > MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MAX) { + LOG_ERR("Invalid negative inertia value %d. Min: %d; Max: %d", neg_inertia, + MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MIN, MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_MAX); + return 1; + } + + uint8_t arg = neg_inertia; + + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_SET_NEG_INERTIA, sizeof(MOUSE_PS2_CMD_TP_SET_NEG_INERTIA), &arg, + MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set negative inertia to %d", neg_inertia); + return resp.err; + } + + data->tp_neg_inertia = neg_inertia; + + LOG_INF("Successfully set TP negative inertia to %d", neg_inertia); + + return 0; +} + +int zmk_mouse_ps2_tp_neg_inertia_change(int amount) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + int new_val = data->tp_neg_inertia + amount; + + LOG_INF("Setting negative inertia to %d", new_val); + int err = zmk_mouse_ps2_tp_neg_inertia_set(new_val); + if (err == 0) { + + zmk_mouse_ps2_settings_save(); + } + + return err; +} + +int zmk_mouse_ps2_tp_value6_upper_plateau_speed_get(uint8_t *value6) { + struct zmk_mouse_ps2_send_cmd_resp resp = + zmk_mouse_ps2_send_cmd(MOUSE_PS2_CMD_TP_GET_VALUE6_UPPER_PLATEAU_SPEED, + sizeof(MOUSE_PS2_CMD_TP_GET_VALUE6_UPPER_PLATEAU_SPEED), NULL, + MOUSE_PS2_CMD_TP_GET_VALUE6_UPPER_PLATEAU_SPEED_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not get trackpoint value6 upper plateau speed"); + return resp.err; + } + + uint8_t value6_int = resp.resp_buffer[0]; + *value6 = value6_int; + + LOG_DBG("Trackpoint value6 upper plateau speed is %d", value6_int); + + return 0; +} + +int zmk_mouse_ps2_tp_value6_upper_plateau_speed_set(int value6) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + if (value6 < MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MIN || + value6 > MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MAX) { + LOG_ERR("Invalid value6 upper plateau speed value %d. Min: %d; Max: %d", value6, + MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MIN, + MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_MAX); + return 1; + } + + uint8_t arg = value6; + + struct zmk_mouse_ps2_send_cmd_resp resp = + zmk_mouse_ps2_send_cmd(MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED, + sizeof(MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED), &arg, + MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set value6 upper plateau speed to %d", value6); + return resp.err; + } + + data->tp_value6 = value6; + + LOG_INF("Successfully set TP value6 upper plateau speed to %d", value6); + + return 0; +} + +int zmk_mouse_ps2_tp_value6_upper_plateau_speed_change(int amount) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + int new_val = data->tp_value6 + amount; + + LOG_INF("Setting value6 upper plateau speed to %d", new_val); + int err = zmk_mouse_ps2_tp_value6_upper_plateau_speed_set(new_val); + if (err == 0) { + + zmk_mouse_ps2_settings_save(); + } + + return err; +} + +int zmk_mouse_ps2_tp_pts_threshold_get(uint8_t *pts_threshold) { + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_GET_PTS_THRESHOLD, sizeof(MOUSE_PS2_CMD_TP_GET_PTS_THRESHOLD), NULL, + MOUSE_PS2_CMD_TP_GET_PTS_THRESHOLD_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not get trackpoint press-to-select threshold"); + return resp.err; + } + + uint8_t pts_threshold_int = resp.resp_buffer[0]; + *pts_threshold = pts_threshold_int; + + LOG_DBG("Trackpoint press-to-select threshold is %d", pts_threshold_int); + + return 0; +} + +int zmk_mouse_ps2_tp_pts_threshold_set(int pts_threshold) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + if (pts_threshold < MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MIN || + pts_threshold > MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MAX) { + LOG_ERR("Invalid press-to-select threshold value %d. Min: %d; Max: %d", pts_threshold, + MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MIN, MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_MAX); + return 1; + } + + uint8_t arg = pts_threshold; + + struct zmk_mouse_ps2_send_cmd_resp resp = zmk_mouse_ps2_send_cmd( + MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD, sizeof(MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD), &arg, + MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_RESP_LEN, true); + if (resp.err) { + LOG_ERR("Could not set press-to-select threshold to %d", pts_threshold); + return resp.err; + } + + data->tp_pts_threshold = pts_threshold; + + LOG_INF("Successfully set TP press-to-select threshold to %d", pts_threshold); + + return 0; +} + +int zmk_mouse_ps2_tp_pts_threshold_change(int amount) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + int new_val = data->tp_pts_threshold + amount; + + LOG_INF("Setting press-to-select threshold to %d", new_val); + int err = zmk_mouse_ps2_tp_pts_threshold_set(new_val); + if (err == 0) { + + zmk_mouse_ps2_settings_save(); + } + + return err; +} + +/* + * State Saving + */ + +#if IS_ENABLED(CONFIG_SETTINGS) + +struct k_work_delayable zmk_mouse_ps2_save_work; + +int zmk_mouse_ps2_settings_save_setting(char *setting_name, const void *value, size_t val_len) { + char setting_path[40]; + snprintf(setting_path, sizeof(setting_path), "%s/%s", MOUSE_PS2_SETTINGS_SUBTREE, setting_name); + + LOG_DBG("Saving setting to `%s`", setting_path); + int err = settings_save_one(setting_path, value, val_len); + if (err) { + LOG_ERR("Could not save setting to `%s`: %d", setting_path, err); + } + + return err; +} + +int zmk_mouse_ps2_settings_reset_setting(char *setting_name) { + char setting_path[40]; + snprintf(setting_path, sizeof(setting_path), "%s/%s", MOUSE_PS2_SETTINGS_SUBTREE, setting_name); + + LOG_DBG("Reseting setting `%s`", setting_path); + int err = settings_delete(setting_path); + if (err) { + LOG_ERR("Could not reset setting `%s`", setting_path); + } + + return err; +} + +static void zmk_mouse_ps2_settings_save_work(struct k_work *work) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + LOG_DBG(""); + + zmk_mouse_ps2_settings_save_setting(MOUSE_PS2_ST_TP_SENSITIVITY, &data->tp_sensitivity, + sizeof(data->tp_sensitivity)); + zmk_mouse_ps2_settings_save_setting(MOUSE_PS2_ST_TP_NEG_INERTIA, &data->tp_neg_inertia, + sizeof(data->tp_neg_inertia)); + zmk_mouse_ps2_settings_save_setting(MOUSE_PS2_ST_TP_VALUE6, &data->tp_value6, + sizeof(data->tp_value6)); + zmk_mouse_ps2_settings_save_setting(MOUSE_PS2_ST_TP_PTS_THRESHOLD, &data->tp_pts_threshold, + sizeof(data->tp_pts_threshold)); +} +#endif + +int zmk_mouse_ps2_settings_save() { + LOG_DBG(""); + +#if IS_ENABLED(CONFIG_SETTINGS) + int ret = + k_work_reschedule(&zmk_mouse_ps2_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE)); + return MIN(ret, 0); +#else + return 0; +#endif +} + +int zmk_mouse_ps2_settings_reset() { + + LOG_INF("Deleting runtime settings..."); + zmk_mouse_ps2_settings_reset_setting(MOUSE_PS2_ST_TP_SENSITIVITY); + zmk_mouse_ps2_settings_reset_setting(MOUSE_PS2_ST_TP_NEG_INERTIA); + zmk_mouse_ps2_settings_reset_setting(MOUSE_PS2_ST_TP_VALUE6); + zmk_mouse_ps2_settings_reset_setting(MOUSE_PS2_ST_TP_PTS_THRESHOLD); + + LOG_INF("Restoring default settings to TP.."); + zmk_mouse_ps2_tp_sensitivity_set(MOUSE_PS2_CMD_TP_SET_SENSITIVITY_DEFAULT); + + zmk_mouse_ps2_tp_neg_inertia_set(MOUSE_PS2_CMD_TP_SET_NEG_INERTIA_DEFAULT); + + zmk_mouse_ps2_tp_value6_upper_plateau_speed_set( + MOUSE_PS2_CMD_TP_SET_VALUE6_UPPER_PLATEAU_SPEED_DEFAULT); + + zmk_mouse_ps2_tp_pts_threshold_set(MOUSE_PS2_CMD_TP_SET_PTS_THRESHOLD_DEFAULT); + + return 0; +} + +int zmk_mouse_ps2_settings_log() { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + + char settings_str[250]; + + snprintf(settings_str, sizeof(settings_str), " \n\ +&mouse_ps2_conf = { \n\ + tp-sensitivity = <%d>; \n\ + tp-neg-inertia = <%d>; \n\ + tp-val6-upper-speed = <%d>; \n\ + tp-tp-press-to-select-threshold = <%d>; \n\ +}", + data->tp_sensitivity, data->tp_neg_inertia, data->tp_value6, data->tp_pts_threshold); + + LOG_INF("Current settings... %s", settings_str); + + return 0; +} + +// This function is called when settings are loaded from flash by +// `settings_load_subtree`. +// It's called once for each PS/2 mouse setting that has been stored. +static int zmk_mouse_ps2_settings_restore(const char *name, size_t len, settings_read_cb read_cb, + void *cb_arg) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + + uint8_t setting_val; + + if (len != sizeof(setting_val)) { + LOG_ERR("Could not restore settings %s: Len mismatch", name); + + return -EINVAL; + } + + int rc = read_cb(cb_arg, &setting_val, sizeof(setting_val)); + if (rc <= 0) { + LOG_ERR("Could not restore setting %s: %d", name, rc); + return -EINVAL; + } + + if (data->is_trackpoint == false) { + LOG_INF("Mouse device is not a trackpoint. Not restoring setting %s.", name); + + return 0; + } + + LOG_INF("Restoring setting %s with value: %d", name, setting_val); + + if (strcmp(name, MOUSE_PS2_ST_TP_SENSITIVITY) == 0) { + + if (config->tp_sensitivity != -1) { + LOG_WRN("Not restoring runtime settings for %s with value %d, because deviceconfig " + "defines the setting with value %d", + name, setting_val, config->tp_sensitivity); + + return 0; + } + + return zmk_mouse_ps2_tp_sensitivity_set(setting_val); + } else if (strcmp(name, MOUSE_PS2_ST_TP_NEG_INERTIA) == 0) { + if (config->tp_neg_inertia != -1) { + LOG_WRN("Not restoring runtime settings for %s with value %d, because deviceconfig " + "defines the setting with value %d", + name, setting_val, config->tp_neg_inertia); + + return 0; + } + + return zmk_mouse_ps2_tp_neg_inertia_set(setting_val); + } else if (strcmp(name, MOUSE_PS2_ST_TP_VALUE6) == 0) { + if (config->tp_val6_upper_speed != -1) { + LOG_WRN("Not restoring runtime settings for %s with value %d, because deviceconfig " + "defines the setting with value %d", + name, setting_val, config->tp_val6_upper_speed); + + return 0; + } + + return zmk_mouse_ps2_tp_value6_upper_plateau_speed_set(setting_val); + } else if (strcmp(name, MOUSE_PS2_ST_TP_PTS_THRESHOLD) == 0) { + if (config->tp_press_to_select_threshold != -1) { + LOG_WRN("Not restoring runtime settings for %s with value %d, because deviceconfig " + "defines the setting with value %d", + name, setting_val, config->tp_press_to_select_threshold); + + return 0; + } + + return zmk_mouse_ps2_tp_pts_threshold_set(setting_val); + } + + return -EINVAL; +} + +struct settings_handler zmk_mouse_ps2_settings_conf = { + .name = MOUSE_PS2_SETTINGS_SUBTREE, + .h_set = zmk_mouse_ps2_settings_restore, +}; + +int zmk_mouse_ps2_settings_init() { +#if IS_ENABLED(CONFIG_SETTINGS) + LOG_DBG(""); + + settings_subsys_init(); + + int err = settings_register(&zmk_mouse_ps2_settings_conf); + if (err) { + LOG_ERR("Failed to register the PS/2 mouse settings handler (err %d)", err); + return err; + } + + k_work_init_delayable(&zmk_mouse_ps2_save_work, zmk_mouse_ps2_settings_save_work); + + // This will load the settings and then call + // `zmk_mouse_ps2_settings_restore`, which will set the settings + settings_load_subtree(MOUSE_PS2_SETTINGS_SUBTREE); +#endif + + return 0; +} + +/* + * Init + */ + +static void zmk_mouse_ps2_init_thread(int dev_ptr, int unused); +int zmk_mouse_ps2_init_power_on_reset(); +int zmk_mouse_ps2_init_wait_for_mouse(const struct device *dev); + +static int zmk_mouse_ps2_init(const struct device *dev) { + LOG_DBG("Inside zmk_mouse_ps2_init"); + + LOG_DBG("Creating mouse_ps2 init thread."); + k_thread_create(&zmk_mouse_ps2_data.thread, zmk_mouse_ps2_data.thread_stack, + MOUSE_PS2_THREAD_STACK_SIZE, (k_thread_entry_t)zmk_mouse_ps2_init_thread, + (struct device *)dev, 0, NULL, K_PRIO_COOP(MOUSE_PS2_THREAD_PRIORITY), 0, + K_MSEC(ZMK_MOUSE_PS2_INIT_THREAD_DELAY_MS)); + + return 0; +} + +static void zmk_mouse_ps2_init_thread(int dev_ptr, int unused) { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + int err; + + data->dev = INT_TO_POINTER(dev_ptr); + + const struct zmk_mouse_ps2_config *config = data->dev->config; + + zmk_mouse_ps2_init_power_on_reset(); + + LOG_INF("Waiting for mouse to connect..."); + err = zmk_mouse_ps2_init_wait_for_mouse(data->dev); + if (err) { + LOG_ERR("Could not init a mouse in %d attempts. Giving up. " + "Power cycle the mouse and reset zmk to try again.", + MOUSE_PS2_INIT_ATTEMPTS); + return; + } + + if (config->sampling_rate != MOUSE_PS2_CMD_SET_SAMPLING_RATE_DEFAULT) { + + LOG_INF("Setting sample rate to %d...", config->sampling_rate); + zmk_mouse_ps2_set_sampling_rate(config->sampling_rate); + if (err) { + LOG_ERR("Could not set sampling rate to %d: %d", config->sampling_rate, err); + return; + } + } + + if (zmk_mouse_ps2_is_device_trackpoint() == true) { + LOG_INF("Device is a trackpoint"); + data->is_trackpoint = true; + + if (config->tp_press_to_select) { + LOG_INF("Enabling TP press to select..."); + zmk_mouse_ps2_tp_press_to_select_set(true); + } + + if (config->tp_press_to_select_threshold != -1) { + LOG_INF("Setting TP press to select thereshold to %d...", + config->tp_press_to_select_threshold); + zmk_mouse_ps2_tp_pts_threshold_set(config->tp_press_to_select_threshold); + } + + if (config->tp_sensitivity != -1) { + LOG_INF("Setting TP sensitivity to %d...", config->tp_sensitivity); + zmk_mouse_ps2_tp_sensitivity_set(config->tp_sensitivity); + } + + if (config->tp_neg_inertia != -1) { + LOG_INF("Setting TP inertia to %d...", config->tp_neg_inertia); + zmk_mouse_ps2_tp_neg_inertia_set(config->tp_neg_inertia); + } + + if (config->tp_val6_upper_speed != -1) { + LOG_INF("Setting TP value 6 upper speed plateau to %d...", config->tp_val6_upper_speed); + zmk_mouse_ps2_tp_value6_upper_plateau_speed_set(config->tp_val6_upper_speed); + } + if (config->tp_x_invert) { + LOG_INF("Inverting trackpoint x axis."); + zmk_mouse_ps2_tp_invert_x_set(true); + } + + if (config->tp_y_invert) { + LOG_INF("Inverting trackpoint y axis."); + zmk_mouse_ps2_tp_invert_y_set(true); + } + + if (config->tp_xy_swap) { + LOG_INF("Swapping trackpoint x and y axis."); + zmk_mouse_ps2_tp_swap_xy_set(true); + } + } + + if (config->scroll_mode) { + LOG_INF("Enabling scroll mode."); + zmk_mouse_ps2_set_packet_mode(MOUSE_PS2_PACKET_MODE_SCROLL); + } + + zmk_mouse_ps2_settings_init(); + + // Configure read callback + LOG_DBG("Configuring ps2 callback..."); +#if IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK) + + err = ps2_config(config->ps2_device, &zmk_mouse_ps2_activity_callback, + &zmk_mouse_ps2_activity_resend_callback); + +#else + + err = ps2_config(config->ps2_device, &zmk_mouse_ps2_activity_callback); + +#endif /* IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK) */ + + if (err) { + LOG_ERR("Could not configure ps2 interface: %d", err); + return; + } + + LOG_INF("Enabling data reporting and ps2 callback..."); + err = zmk_mouse_ps2_activity_reporting_enable(); + if (err) { + LOG_ERR("Could not activate ps2 callback: %d", err); + } else { + LOG_DBG("Successfully activated ps2 callback"); + } + + k_work_init_delayable(&data->packet_buffer_timeout, zmk_mouse_ps2_activity_packet_timout); + + return; +} + +// Power-On-Reset for trackpoints (and possibly other devices). +// From the `IBM TrackPoint System Version 4.0 Engineering +// Specification`... +// "The TrackPoint logic shall execute a Power On Reset (POR) when power is +// applied to the device. The POR shall be timed to occur 600 ms ± 20 % from +// the time power is applied to the TrackPoint controller. Activity on the +// clock and data lines is ignored prior to the completion of the diagnostic +// sequence. (See RESET mode of operation.)" +int zmk_mouse_ps2_init_power_on_reset() { + struct zmk_mouse_ps2_data *data = &zmk_mouse_ps2_data; + const struct zmk_mouse_ps2_config *config = &zmk_mouse_ps2_config; + + // Check if the optional rst-gpios setting was set + if (config->rst_gpio.port == NULL) { + return 0; + } + + LOG_INF("Performing Power-On-Reset..."); + + if (data->rst_gpio.port == NULL) { + data->rst_gpio = config->rst_gpio; + + // Overwrite any user-provided flags from the devicetree + data->rst_gpio.dt_flags = 0; + } + + // Set reset pin low... + int err = gpio_pin_configure_dt(&data->rst_gpio, (GPIO_OUTPUT_HIGH)); + if (err) { + LOG_ERR("Failed Power-On-Reset: Failed to configure RST GPIO pin to " + "output low (err %d)", + err); + return err; + } + + // Wait 600ms + k_sleep(MOUSE_PS2_POWER_ON_RESET_TIME); + + // Set pin high + err = gpio_pin_set_dt(&data->rst_gpio, 0); + if (err) { + LOG_ERR("Failed Power-On-Reset: Failed to set RST GPIO pin to " + "low (err %d)", + err); + return err; + } + + LOG_DBG("Finished Power-On-Reset successfully..."); + + return 0; +} + +int zmk_mouse_ps2_init_wait_for_mouse(const struct device *dev) { + const struct zmk_mouse_ps2_config *config = dev->config; + int err; + + uint8_t read_val; + + for (int i = 0; i < MOUSE_PS2_INIT_ATTEMPTS; i++) { + + LOG_INF("Trying to initialize mouse device (attempt %d / %d)", i + 1, + MOUSE_PS2_INIT_ATTEMPTS); + + // PS/2 Devices do a self-test and send the result when they power up. + + err = ps2_read(config->ps2_device, &read_val); + if (err == 0) { + if (read_val != MOUSE_PS2_RESP_SELF_TEST_PASS) { + LOG_WRN("Got invalid PS/2 self-test result: 0x%x", read_val); + + LOG_INF("Trying to reset PS2 device..."); + zmk_mouse_ps2_reset(config->ps2_device); + + continue; + } + + LOG_INF("PS/2 Device passed self-test: 0x%x", read_val); + + // Read device id + LOG_INF("Reading PS/2 device id..."); + err = ps2_read(config->ps2_device, &read_val); + if (err) { + LOG_WRN("Could not read PS/2 device id: %d", err); + } else { + if (read_val == 0) { + LOG_INF("Connected PS/2 device is a mouse..."); + return 0; + } else { + LOG_WRN("PS/2 device is not a mouse: 0x%x", read_val); + return 1; + } + } + } else { + LOG_WRN("Could not read PS/2 device self-test result: %d. ", err); + } + + // But when a zmk device is reset, it doesn't cut the power to external + // devices. So the device acts as if it was never disconnected. + // So we try sending the reset command. + if (i % 2 == 0) { + LOG_INF("Trying to reset PS2 device..."); + zmk_mouse_ps2_reset(config->ps2_device); + continue; + } + + k_sleep(K_SECONDS(5)); + } + + return 1; +} + +// Depends on the UART and PS2 init priorities, which are 55 and 45 by default +#define ZMK_MOUSE_PS2_INIT_PRIORITY 90 + +DEVICE_DT_INST_DEFINE(0, &zmk_mouse_ps2_init, NULL, &zmk_mouse_ps2_data, &zmk_mouse_ps2_config, + POST_KERNEL, ZMK_MOUSE_PS2_INIT_PRIORITY, NULL); diff --git a/app/module/drivers/ps2/CMakeLists.txt b/app/module/drivers/ps2/CMakeLists.txt new file mode 100644 index 00000000000..cb2dcc2b2d3 --- /dev/null +++ b/app/module/drivers/ps2/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +zephyr_library_sources_ifdef(CONFIG_PS2_GPIO ps2_gpio.c) +zephyr_library_sources_ifdef(CONFIG_PS2_UART ps2_uart.c) diff --git a/app/module/drivers/ps2/Kconfig b/app/module/drivers/ps2/Kconfig new file mode 100644 index 00000000000..63a21b9e5b9 --- /dev/null +++ b/app/module/drivers/ps2/Kconfig @@ -0,0 +1,9 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +if PS2 + +rsource "Kconfig.gpio" +rsource "Kconfig.uart" + +endif # PS2 diff --git a/app/module/drivers/ps2/Kconfig.gpio b/app/module/drivers/ps2/Kconfig.gpio new file mode 100644 index 00000000000..bf76cfdb751 --- /dev/null +++ b/app/module/drivers/ps2/Kconfig.gpio @@ -0,0 +1,66 @@ +# Copyright (c) 2017 Linaro Ltd. +# SPDX-License-Identifier: Apache-2.0 + +DT_COMPAT_PS2_GPIO := gpio-ps2 + +config PS2_GPIO + bool "GPIO bit banging PS/2 support" + default $(dt_compat_enabled,$(DT_COMPAT_PS2_GPIO)) + help + Enable software driven (bit banging) PS/2 support using GPIO pins + +if PS2_GPIO + +config PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK + bool "Notify the PS2 device driver (mouse driver) when the received data is invalid and a resend is requested from the peripheral. This can help the higher level PS2 device driver handle transmission errors with less side-effects, but it requires a forked zephyr. With the ps2-uart driver this option is not really necessary, but is very useful for the ps2-gpio driver." + default n + select ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK + +# PS/2 events must be processed within 30-50us. Therefore we shift the +# BT_CTLR_LLL_PRIO from 0 to 1 and BT_CTLR_ULL_* priorities from 1 to 2. +# We then set the gpio priority to 0. +# +# The following also has to be added to the device tree to set GPIOTE to the +# highest priority: +# &gpiote { +# interrupts = < 0x6 0 >; +# }; +# +# On top of that all other interrupts have to be set to priority 3 to prevent +# them from delaying the BT interrupts. +# +# Overrides can be generated using the following script in infused-kim's zmk +# fork: +# app/scripts/gen_interrupt_priority_overrides.py +# +# Make sure to do it on a zephyr.dts where the interrupts have not been +# adjusted yet (i.e. a config that doesn't have the the following options +# enabled). +# +# If you are building using a toolchain you can find the zephyr.dts in the +# build directory, which by default is: `app/build/zephyr/zephyr.dts` +# +# If you are building using the github actions, copy the output of the build +# step `xxxx_right - nice_nano_v2 Devicetree file` into a file and run the +# script on it. +# +# This will increase the probability that the PS/2 interrupts are triggered in +# time. + +config BT_CTLR_ADVANCED_FEATURES + default y + +config BT_CTLR_LLL_PRIO + default 1 + +config BT_CTLR_ULL_HIGH_PRIO + default 2 + +config BT_CTLR_ULL_LOW_PRIO + default 2 + +config PS2_GPIO_INTERRUPT_LOG_ENABLED + bool "Enable extensive interrupt logging." + default n + +endif # PS2_GPIO diff --git a/app/module/drivers/ps2/Kconfig.uart b/app/module/drivers/ps2/Kconfig.uart new file mode 100644 index 00000000000..0981fd3cc3a --- /dev/null +++ b/app/module/drivers/ps2/Kconfig.uart @@ -0,0 +1,76 @@ +# Copyright (c) 2017 Linaro Ltd. +# SPDX-License-Identifier: Apache-2.0 + +DT_COMPAT_PS2_UART := uart-ps2 + +config PS2_UART + bool "GPIO bit banging PS/2 support through UART" + default $(dt_compat_enabled,$(DT_COMPAT_PS2_UART)) + select SERIAL + select UART_ASYNC_API + select UART_USE_RUNTIME_CONFIGURE + help + Enable software driven (bit banging) PS/2 support using UART on GPIO pins + +if PS2_UART + +config PS2_UART_WRITE_MODE_BLOCKING + bool "Don't use interrupts for writing data and instead block using timers." + default n + +config PS2_UART_ENABLE_PS2_RESEND_CALLBACK + bool "Notify the PS2 device driver (mouse driver) when the received data is invalid and a resend is requested from the peripheral. This can help the higher level PS2 device driver handle transmission errors with less side-effects, but it requires a forked zephyr. With the ps2-uart driver this option is not really necessary, but is very useful for the ps2-gpio driver." + default n + select ZMK_INPUT_MOUSE_PS2_ENABLE_PS2_RESEND_CALLBACK + +# PM_DEVICE is needed, because we use GPIO bitbanging for writing. And to +# release the data PIN that is used by the UART device, we use pinctrl to +# switch to the sleep state. But the sleep state is only included if PM_DEVICE +# is enabled. +config PM_DEVICE + default y + +# PS/2 events must be processed within 30-50us. Therefore we shift the +# BT_CTLR_LLL_PRIO from 0 to 1 and BT_CTLR_ULL_* priorities from 1 to 2. +# We then set the gpio priority to 0. +# +# The following also has to be added to the device tree to set GPIOTE to the +# highest priority: +# &gpiote { +# interrupts = < 0x6 0 >; +# }; +# +# On top of that all other interrupts have to be set to priority 3 to prevent +# them from delaying the BT interrupts. +# +# Overrides can be generated using the following script in infused-kim's zmk +# fork: +# app/scripts/gen_interrupt_priority_overrides.py +# +# Make sure to do it on a zephyr.dts where the interrupts have not been +# adjusted yet (i.e. a config that doesn't have the the following options +# enabled). +# +# If you are building using a toolchain you can find the zephyr.dts in the +# build directory, which by default is: `app/build/zephyr/zephyr.dts` +# +# If you are building using the github actions, copy the output of the build +# step `xxxx_right - nice_nano_v2 Devicetree file` into a file and run the +# script on it. +# +# This will increase the probability that the PS/2 interrupts are triggered in +# time. + +config BT_CTLR_ADVANCED_FEATURES + default y + +config BT_CTLR_LLL_PRIO + default 1 + +config BT_CTLR_ULL_HIGH_PRIO + default 2 + +config BT_CTLR_ULL_LOW_PRIO + default 2 + +endif # PS2_UART diff --git a/app/module/drivers/ps2/ps2_gpio.c b/app/module/drivers/ps2/ps2_gpio.c new file mode 100644 index 00000000000..a5d941f6176 --- /dev/null +++ b/app/module/drivers/ps2/ps2_gpio.c @@ -0,0 +1,1362 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT gpio_ps2 + +#include +#include +#include +#include +#include +#include + +#define LOG_LEVEL CONFIG_PS2_LOG_LEVEL +LOG_MODULE_REGISTER(ps2_gpio); + +/* + * Settings + */ + +#define PS2_GPIO_WRITE_MAX_RETRY 5 +#define PS2_GPIO_READ_MAX_RETRY 3 + +#define PS2_GPIO_DATA_QUEUE_SIZE 100 + +// Custom queue for background PS/2 processing work at low priority +// We purposefully want this to be a fairly low priority, because +// this queue is used while we wait to start a write. +// If the system is very busy with interrupts and other threads, then we +// want to wait until that is over so that our write interrupts don't get +// missed. +#define PS2_GPIO_WORK_QUEUE_PRIORITY 10 +#define PS2_GPIO_WORK_QUEUE_STACK_SIZE 1024 + +// Custom queue for calling the zephyr ps/2 callback. +// We don't want to hand it off to that API in an ISR since that callback +// could be using blocking functions. +// But we also don't want to hand it off at a low priority, since the PS/2 +// packets must be dealt with quickly. So we use a fairly high priority. +#define PS2_GPIO_WORK_QUEUE_CB_PRIORITY 2 +#define PS2_GPIO_WORK_QUEUE_CB_STACK_SIZE 1024 + +/* + * PS/2 Defines + */ + +#define PS2_GPIO_POS_START 0 +// 1-8 are the data bits +#define PS2_GPIO_POS_PARITY 9 +#define PS2_GPIO_POS_STOP 10 +#define PS2_GPIO_POS_ACK 11 // Write mode only + +#define PS2_GPIO_RESP_ACK 0xfa +#define PS2_GPIO_RESP_RESEND 0xfe +#define PS2_GPIO_RESP_FAILURE 0xfc + +/* + * PS/2 Timings + */ + +// PS2 uses a frequency between 10 kHz and 16.7 kHz. So clocks should arrive +// within 60-100us. +#define PS2_GPIO_TIMING_SCL_CYCLE_MIN 60 +#define PS2_GPIO_TIMING_SCL_CYCLE_MAX 100 + +// The minimum time needed to inhibit clock to start a write +// is 100us, but we triple it just in case. +#define PS2_GPIO_TIMING_SCL_INHIBITION_MIN 100 +#define PS2_GPIO_TIMING_SCL_INHIBITION (3 * PS2_GPIO_TIMING_SCL_INHIBITION_MIN) + +// When we start the inhibition timer for PS2_GPIO_TIMING_SCL_INHIBITION us, +// it doesn't mean it will be called after exactly that time. +// If there are higher priority threads, it will be delayed and might stay +// inhibitied much longer. So we account for that delay and add a maximum +// allowed delay. +#define PS2_GPIO_TIMING_SCL_INHIBITION_TIMER_DELAY_MAX 1000 + +// After inhibiting and releasing the clock, the device starts sending +// the clock. It's supposed to start immediately, but some devices +// need much longer if you are asking them to interrupt an +// ongoing read. +#define PS2_GPIO_TIMING_SCL_INHIBITION_RESP_MAX 10000 + +// Writes start with us inhibiting the line and then respond +// with 11 bits (start bit included in inhibition time). +// To be conservative we give it another 2 cycles to complete +#define PS2_GPIO_TIMING_WRITE_MAX_TIME \ + (PS2_GPIO_TIMING_SCL_INHIBITION + PS2_GPIO_TIMING_SCL_INHIBITION_TIMER_DELAY_MAX + \ + PS2_GPIO_TIMING_SCL_INHIBITION_RESP_MAX + 11 * PS2_GPIO_TIMING_SCL_CYCLE_MAX + \ + 2 * PS2_GPIO_TIMING_SCL_CYCLE_MAX) + +// Reads are 11bit and we give it another 2 cycles to start and stop +#define PS2_GPIO_TIMING_READ_MAX_TIME \ + (11 * PS2_GPIO_TIMING_SCL_CYCLE_MAX + 2 * PS2_GPIO_TIMING_SCL_CYCLE_MAX) + +/* + * Driver Defines + */ + +// Timeout for blocking read using the zephyr PS2 ps2_read() function +// This is not a matter of PS/2 timings, but a preference of how long we let +// the user wait until we give up on reading. +#define PS2_GPIO_TIMEOUT_READ K_SECONDS(2) + +// Timeout for write_byte_blocking() +#define PS2_GPIO_TIMEOUT_WRITE_BLOCKING K_USEC(PS2_GPIO_TIMING_WRITE_MAX_TIME) + +// Timeout for write_byte_await_response() +// PS/2 spec says that device must respond within 20msec, +// but real life devices take much longer. Especially if +// you interrupt existing transmissions. +#define PS2_GPIO_TIMEOUT_WRITE_AWAIT_RESPONSE K_MSEC(300) + +// Max time we allow the device to send the next clock signal during reads +// and writes. +// Even though PS/2 devices send the clock at most every 100us, it doesn't mean +// that the interrupts always get triggered within that time. So we allow a +// little extra time. +#define PS2_GPIO_TIMEOUT_READ_SCL K_USEC(PS2_GPIO_TIMING_SCL_CYCLE_MAX + 50) +#define PS2_GPIO_TIMEOUT_WRITE_SCL K_USEC(PS2_GPIO_TIMING_SCL_CYCLE_MAX + 50) + +// But after inhibiting the clock line, sometimes clocks take a little longer +// to start. So we allow a bit more time for the first write clock. +#define PS2_GPIO_TIMEOUT_WRITE_SCL_START K_USEC(PS2_GPIO_TIMING_SCL_INHIBITION_RESP_MAX) + +#define PS2_GPIO_WRITE_INHIBIT_SLC_DURATION K_USEC(PS2_GPIO_TIMING_SCL_INHIBITION) + +/* + * Global Variables + */ + +typedef enum { PS2_GPIO_MODE_READ, PS2_GPIO_MODE_WRITE } ps2_gpio_mode; + +// Used to keep track of blocking write status +typedef enum { + PS2_GPIO_WRITE_STATUS_INACTIVE, + PS2_GPIO_WRITE_STATUS_ACTIVE, + PS2_GPIO_WRITE_STATUS_SUCCESS, + PS2_GPIO_WRITE_STATUS_FAILURE, +} ps2_gpio_write_status; + +struct ps2_gpio_data_queue_item { + uint8_t byte; +}; + +struct ps2_gpio_config { + struct gpio_dt_spec scl_gpio; + struct gpio_dt_spec sda_gpio; +}; + +struct ps2_gpio_data { + const struct device *dev; + struct gpio_dt_spec scl_gpio; /* GPIO used for PS2 SCL line */ + struct gpio_dt_spec sda_gpio; /* GPIO used for PS2 SDA line */ + + // Interrupt callback + struct gpio_callback scl_cb_data; + + // PS2 driver interface callback + struct k_work callback_work; + uint8_t callback_byte; + ps2_callback_t callback_isr; + +#if IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) + ps2_resend_callback_t resend_callback_isr; +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) */ + + bool callback_enabled; + + // Queue for ps2_read() + struct k_msgq data_queue; + char data_queue_buffer[PS2_GPIO_DATA_QUEUE_SIZE * sizeof(struct ps2_gpio_data_queue_item)]; + + ps2_gpio_mode mode; + + uint8_t cur_read_byte; + int cur_read_pos; + int cur_read_try; + uint32_t last_read_cycle_cnt; + struct k_work_delayable read_scl_timout; + + ps2_gpio_write_status cur_write_status; + uint8_t cur_write_byte; + int cur_write_pos; + struct k_work_delayable write_inhibition_wait; + struct k_work_delayable write_scl_timout; + struct k_sem write_lock; + + bool write_awaits_resp; + uint8_t write_awaits_resp_byte; + struct k_sem write_awaits_resp_sem; + + struct k_work resend_cmd_work; +}; + +static const struct ps2_gpio_config ps2_gpio_config = { + .scl_gpio = GPIO_DT_SPEC_INST_GET(0, scl_gpios), + .sda_gpio = GPIO_DT_SPEC_INST_GET(0, sda_gpios), +}; + +static struct ps2_gpio_data ps2_gpio_data = { + .callback_byte = 0x0, + .callback_isr = NULL, + +#if IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) + .resend_callback_isr = NULL, +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) */ + + .callback_enabled = false, + .mode = PS2_GPIO_MODE_READ, + + .cur_read_byte = 0x0, + .cur_read_pos = 0, + .cur_read_try = 0, + + .cur_write_byte = 0x0, + .cur_write_pos = 0, + .cur_write_status = PS2_GPIO_WRITE_STATUS_INACTIVE, + + .write_awaits_resp = false, + .write_awaits_resp_byte = 0x0, +}; + +K_THREAD_STACK_DEFINE(ps2_gpio_work_queue_stack_area, PS2_GPIO_WORK_QUEUE_STACK_SIZE); +static struct k_work_q ps2_gpio_work_queue; + +K_THREAD_STACK_DEFINE(ps2_gpio_work_queue_cb_stack_area, PS2_GPIO_WORK_QUEUE_CB_STACK_SIZE); +static struct k_work_q ps2_gpio_work_queue_cb; + +/* + * Function Definitions + */ + +int ps2_gpio_write_byte(uint8_t byte); + +/* + * Helpers functions + */ + +#define PS2_GPIO_GET_BIT(data, bit_pos) ((data >> bit_pos) & 0x1) +#define PS2_GPIO_SET_BIT(data, bit_val, bit_pos) (data |= (bit_val) << bit_pos) + +int ps2_gpio_get_scl() { + const struct ps2_gpio_data *data = &ps2_gpio_data; + int rc = gpio_pin_get_dt(&data->scl_gpio); + + return rc; +} + +int ps2_gpio_get_sda() { + const struct ps2_gpio_data *data = &ps2_gpio_data; + int rc = gpio_pin_get_dt(&data->sda_gpio); + + return rc; +} + +void ps2_gpio_set_scl(int state) { + const struct ps2_gpio_data *data = &ps2_gpio_data; + + // LOG_INF("Setting scl to %d", state); + gpio_pin_set_dt(&data->scl_gpio, state); +} + +void ps2_gpio_set_sda(int state) { + const struct ps2_gpio_data *data = &ps2_gpio_data; + + // LOG_INF("Seting sda to %d", state); + gpio_pin_set_dt(&data->sda_gpio, state); +} + +int ps2_gpio_set_scl_callback_enabled(bool enabled) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + if (enabled) { + err = gpio_pin_interrupt_configure_dt(&data->scl_gpio, (GPIO_INT_EDGE_FALLING)); + if (err) { + LOG_ERR("failed to enable interrupt on " + "SCL GPIO pin (err %d)", + err); + return err; + } + } else { + err = gpio_pin_interrupt_configure_dt(&data->scl_gpio, (GPIO_INT_DISABLE)); + if (err) { + LOG_ERR("failed to disable interrupt on " + "SCL GPIO pin (err %d)", + err); + return err; + } + } + + return err; +} + +int ps2_gpio_configure_pin_scl(gpio_flags_t flags, char *descr) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + err = gpio_pin_configure_dt(&data->scl_gpio, flags); + if (err) { + LOG_ERR("failed to configure SCL GPIO pin to %s (err %d)", descr, err); + } + + return err; +} + +int ps2_gpio_configure_pin_scl_input() { return ps2_gpio_configure_pin_scl((GPIO_INPUT), "input"); } + +int ps2_gpio_configure_pin_scl_output() { + return ps2_gpio_configure_pin_scl((GPIO_OUTPUT_HIGH), "output"); +} + +int ps2_gpio_configure_pin_sda(gpio_flags_t flags, char *descr) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + err = gpio_pin_configure_dt(&data->sda_gpio, flags); + if (err) { + LOG_ERR("failed to configure SDA GPIO pin to %s (err %d)", descr, err); + } + + return err; +} + +int ps2_gpio_configure_pin_sda_input() { return ps2_gpio_configure_pin_sda((GPIO_INPUT), "input"); } + +int ps2_gpio_configure_pin_sda_output() { + return ps2_gpio_configure_pin_sda((GPIO_OUTPUT_HIGH), "output"); +} + +bool ps2_gpio_get_byte_parity(uint8_t byte) { + int byte_parity = __builtin_parity(byte); + + // gcc parity returns 1 if there is an odd number of bits in byte + // But the PS2 protocol sets the parity bit to 0 if there is an odd number + return !byte_parity; +} + +uint8_t ps2_gpio_data_queue_get_next(uint8_t *dst_byte, k_timeout_t timeout) { + struct ps2_gpio_data *data = &ps2_gpio_data; + struct ps2_gpio_data_queue_item queue_data; + int ret; + + ret = k_msgq_get(&data->data_queue, &queue_data, timeout); + if (ret != 0) { + LOG_WRN("Data queue timed out..."); + return -ETIMEDOUT; + } + + *dst_byte = queue_data.byte; + + return 0; +} + +void ps2_gpio_data_queue_empty() { + struct ps2_gpio_data *data = &ps2_gpio_data; + + k_msgq_purge(&data->data_queue); +} + +void ps2_gpio_data_queue_add(uint8_t byte) { + struct ps2_gpio_data *data = &ps2_gpio_data; + + int ret; + + struct ps2_gpio_data_queue_item queue_data; + queue_data.byte = byte; + + LOG_INF("Adding byte to data queue: 0x%x", byte); + + for (int i = 0; i < 2; i++) { + ret = k_msgq_put(&data->data_queue, &queue_data, K_NO_WAIT); + if (ret == 0) { + break; + } else { + LOG_WRN("Data queue full. Removing oldest item."); + + uint8_t tmp_byte; + ps2_gpio_data_queue_get_next(&tmp_byte, K_NO_WAIT); + } + } + + if (ret != 0) { + LOG_ERR("Failed to add byte 0x%x to the data queue.", byte); + } +} + +void ps2_gpio_send_cmd_resend_worker(struct k_work *item) { + +#if IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) + + struct ps2_gpio_data *data = &ps2_gpio_data; + + // Notify the PS/2 device driver that we are requesting a resend. + // PS/2 devices don't just resend the last byte that was sent, but the + // entire command packet, which can be multiple bytes. + if (data->resend_callback_isr != NULL && data->callback_enabled) { + + data->resend_callback_isr(data->dev); + } + +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) */ + + uint8_t cmd = 0xfe; + // LOG_DBG("Requesting resend of data with command: 0x%x", cmd); + ps2_gpio_write_byte(cmd); +} + +void ps2_gpio_send_cmd_resend() { + struct ps2_gpio_data *data = &ps2_gpio_data; + + if (k_is_in_isr()) { + + // It's important to submit this on the cb queue and not on the + // same queue as the inhibition delay. + // Otherwise the queue will be blocked by the semaphore and the + // inhibition delay worker will never be called. + k_work_submit_to_queue(&ps2_gpio_work_queue_cb, &data->resend_cmd_work); + } else { + ps2_gpio_send_cmd_resend_worker(NULL); + } +} + +/* + * Interrupt logging + * + * Zephyr logs don't process fast enough and slow down the interrupts enough + * to make the writes and reads fail. + * + * This simple logging process allows us to debug the interrupts in detail. + */ + +#define PS2_GPIO_INTERRUPT_LOG_SCL_TIMEOUT K_SECONDS(1) +#define PS2_GPIO_INTERRUPT_LOG_MAX_ITEMS 1000 + +#if IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) + +#define LOG_PS2_INT(...) ps2_gpio_interrupt_log_add(__VA_ARGS__) + +struct interrupt_log { + int64_t uptime_ticks; + char msg[50]; + int scl; + int sda; + ps2_gpio_mode mode; + int pos; +}; + +int interrupt_log_offset = 0; +int interrupt_log_idx = 0; +struct interrupt_log interrupt_log[PS2_GPIO_INTERRUPT_LOG_MAX_ITEMS]; + +struct k_work_delayable interrupt_log_scl_timout; +struct k_work interrupt_log_print_worker; + +void ps2_gpio_interrupt_log_add(char *msg, uint8_t *arg); +void ps2_gpio_interrupt_log_print(); +void ps2_gpio_interrupt_log_clear(); +void ps2_gpio_strncat_hex(char *dst, uint8_t val, size_t dst_size); +char *ps2_gpio_interrupt_log_get_mode_str(); +void ps2_gpio_interrupt_log_get_pos_str(int pos, char *pos_str, int pos_str_size); + +void ps2_gpio_interrupt_log_add(char *msg, uint8_t *arg) { + struct ps2_gpio_data *data = &ps2_gpio_data; + struct interrupt_log l; + + l.uptime_ticks = k_uptime_ticks(); + + // va_list arglist; + // va_start(arglist, format); + // vsnprintfcb(l.msg, sizeof(l.msg) - 1, format, arglist); + // va_end(arglist); + strncpy(l.msg, msg, sizeof(l.msg) - 1); + if (arg != NULL) { + ps2_gpio_strncat_hex(l.msg, *arg, sizeof(l.msg)); + } + + l.scl = ps2_gpio_get_scl(); + l.sda = ps2_gpio_get_sda(); + l.mode = data->mode; + if (data->mode == PS2_GPIO_MODE_READ) { + l.pos = data->cur_read_pos; + } else { + l.pos = data->cur_write_pos; + } + + if (interrupt_log_idx == (PS2_GPIO_INTERRUPT_LOG_MAX_ITEMS * 0.80)) { + ps2_gpio_interrupt_log_print(); + } else if (interrupt_log_idx >= PS2_GPIO_INTERRUPT_LOG_MAX_ITEMS) { + interrupt_log_offset++; + return; + } + + interrupt_log[interrupt_log_idx] = l; + interrupt_log_idx += 1; +} + +void ps2_gpio_interrupt_log_print() { + // ps2_gpio_interrupt_log_print_worker(NULL); + k_work_submit_to_queue(&ps2_gpio_work_queue_cb, &interrupt_log_print_worker); +} + +void ps2_gpio_interrupt_log_print_worker(struct k_work *item) { + + LOG_INF("===== Interrupt Log ====="); + for (int i = 0; i < interrupt_log_idx; i++) { + struct interrupt_log *l = &interrupt_log[i]; + char pos_str[50]; + + ps2_gpio_interrupt_log_get_pos_str(l->pos, pos_str, sizeof(pos_str)); + + LOG_INF("%d - %" PRIu64 ": %s " + "(mode=%s, pos=%s, scl=%d, sda=%d)", + interrupt_log_offset + i + 1, l->uptime_ticks, l->msg, + ps2_gpio_interrupt_log_get_mode_str(), pos_str, l->scl, l->sda); + k_sleep(K_MSEC(15)); + } + LOG_INF("======== End Log ========"); + + ps2_gpio_interrupt_log_clear(); +} + +void ps2_gpio_interrupt_log_clear() { + memset(&interrupt_log, 0x0, sizeof(interrupt_log)); + interrupt_log_offset += interrupt_log_idx; + interrupt_log_idx = 0; +} + +void ps2_gpio_interrupt_log_scl_timeout(struct k_work *item) { + // Called if there is no interrupt for + // PS2_GPIO_INTERRUPT_LOG_SCL_TIMEOUT ms + ps2_gpio_interrupt_log_print(); +} + +void ps2_gpio_strncat_hex(char *dst, uint8_t val, size_t dst_size) { + const char hex_chars[] = "0123456789abcdef"; + char val_hex[5]; + + val_hex[0] = '0'; + val_hex[1] = 'x'; + + val_hex[2] = hex_chars[(val >> (4 * (1 - 0))) & 0xf]; + val_hex[3] = hex_chars[(val >> (4 * (1 - 1))) & 0xf]; + + val_hex[4] = '\0'; + + strncat(dst, val_hex, dst_size - strlen(dst) - 1); +} + +char *ps2_gpio_interrupt_log_get_mode_str() { + struct ps2_gpio_data *data = &ps2_gpio_data; + + if (data->mode == PS2_GPIO_MODE_READ) { + return "r"; + } else if (data->mode == PS2_GPIO_MODE_WRITE) { + return "w"; + } else { + return "?"; + } +} + +void ps2_gpio_interrupt_log_get_pos_str(int pos, char *pos_str, int pos_str_size) { + char *pos_names[] = { + "start", "data_1", "data_2", "data_3", "data_4", "data_5", + "data_6", "data_7", "data_8", "parity", "stop", "ack", + }; + + if (pos >= (sizeof(pos_names) / sizeof(pos_names[0]))) { + snprintf(pos_str, pos_str_size - 1, "%d", pos); + } else { + strncpy(pos_str, pos_names[pos], pos_str_size - 1); + } +} + +#else + +#define LOG_PS2_INT(...) + +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) */ + +/* + * Reading PS/2 data + */ + +void ps2_gpio_read_interrupt_handler(); +void ps2_gpio_read_scl_timeout(struct k_work *item); +void ps2_gpio_read_abort(bool should_resend, char *reason); +void ps2_gpio_read_process_received_byte(uint8_t byte); +void ps2_gpio_read_finish(); + +// Reading doesn't need to be initiated. It happens automatically whenever +// the device sends data. +// Once a full byte has been received successfully it is processed in +// ps2_gpio_read_process_received_byte, which decides what should happen +// with it. +void ps2_gpio_read_interrupt_handler() { + struct ps2_gpio_data *data = &ps2_gpio_data; + + uint32_t cur_read_cycle_cnt = k_cycle_get_32(); + uint32_t last_read_cycle_cnt = data->last_read_cycle_cnt; + + data->last_read_cycle_cnt = cur_read_cycle_cnt; + + if (data->cur_read_pos > 0) { + uint32_t prev_cycle_delta_us = + k_cyc_to_us_floor32(cur_read_cycle_cnt - last_read_cycle_cnt); + + if (prev_cycle_delta_us > PS2_GPIO_TIMING_SCL_CYCLE_MAX) { + ps2_gpio_read_abort(true, "missed interrupt"); + } + } + + k_work_cancel_delayable(&data->read_scl_timout); + + LOG_PS2_INT("Read interrupt", NULL); + + int sda_val = ps2_gpio_get_sda(); + + if (data->cur_read_pos == PS2_GPIO_POS_START) { + // The first bit of every transmission should be 0. + // If it is not, it means we are out of sync with the device. + // So we abort the transmission and start from scratch. + if (sda_val != 0) { + LOG_PS2_INT("Ignoring read interrupt due to invalid start bit.", NULL); + + // We don't request a resend here, because sometimes after writes + // devices send some unintended interrupts. If this is a "real + // transmission" and we are out of sync, we will catch it with the + // parity and stop bits and then request a resend. + ps2_gpio_read_abort(false, "invalid start bit"); + return; + } + } else if (data->cur_read_pos > PS2_GPIO_POS_START && + data->cur_read_pos < PS2_GPIO_POS_PARITY) { // Data Bits + + // Current position, minus start bit + int bit_pos = data->cur_read_pos - 1; + PS2_GPIO_SET_BIT(data->cur_read_byte, sda_val, bit_pos); + } else if (data->cur_read_pos == PS2_GPIO_POS_PARITY) { + bool read_byte_parity = ps2_gpio_get_byte_parity(data->cur_read_byte); + + if (read_byte_parity != sda_val) { + LOG_PS2_INT("Requesting re-send due to invalid parity bit.", NULL); + + // If we got to the parity bit and it's incorrect then we + // are definitly in a transmission and out of sync. So we + // request a resend. + ps2_gpio_read_abort(true, "invalid parity bit"); + return; + } + } else if (data->cur_read_pos == PS2_GPIO_POS_STOP) { + if (sda_val != 1) { + LOG_PS2_INT("Requesting re-send due to invalid stop bit.", NULL); + + // If we got to the stop bit and it's incorrect then we + // are definitly in a transmission and out of sync. So we + // request a resend. + ps2_gpio_read_abort(true, "invalid stop bit"); + return; + } + + ps2_gpio_read_process_received_byte(data->cur_read_byte); + + return; + } else { + LOG_PS2_INT("Invalid read clock triggered", NULL); + + return; + } + + data->cur_read_pos += 1; + k_work_schedule(&data->read_scl_timout, PS2_GPIO_TIMEOUT_READ_SCL); +} + +void ps2_gpio_read_scl_timeout(struct k_work *item) { + // Once we are receiving a transmission we expect the device to + // to send a new clock/interrupt within 100us. + // If we don't receive the next interrupt within that timeframe, + // we abort the read. + struct k_work_delayable *d_work = k_work_delayable_from_work(item); + struct ps2_gpio_data *data = CONTAINER_OF(d_work, struct ps2_gpio_data, read_scl_timout); + + LOG_PS2_INT("Read SCL timeout", NULL); + + // We don't request a resend if the timeout happens in the early + // stage of the transmission. + // + // Because, sometimes after writes devices send some unintended + // interrupts and start the "real response" after one or two cycles. + // + // If we are really out of sync the parity and stop bits should catch + // it and request a re-transmission. + if (data->cur_read_pos <= 3) { + ps2_gpio_read_abort(false, "scl timeout"); + } else { + ps2_gpio_read_abort(true, "scl timeout"); + } +} + +void ps2_gpio_read_abort(bool should_resend, char *reason) { + struct ps2_gpio_data *data = &ps2_gpio_data; + + if (should_resend == true) { + LOG_ERR("Aborting read with resend request on pos=%d: %s", data->cur_read_pos, reason); + LOG_PS2_INT("Aborting read with resend request.", NULL); + } else { + LOG_PS2_INT("Aborting read without resend request.", NULL); + } + + ps2_gpio_read_finish(); + + k_work_cancel_delayable(&data->read_scl_timout); + + if (should_resend == true) { + if (data->cur_read_try < PS2_GPIO_READ_MAX_RETRY) { + + data->cur_read_try++; + ps2_gpio_send_cmd_resend(); + } else { + LOG_ERR("Failed to read value %d times. Stopping asking the device " + "to resend.", + data->cur_read_try); + + data->cur_read_try = 0; + } + } +} + +void ps2_gpio_read_process_received_byte(uint8_t byte) { + struct ps2_gpio_data *data = &ps2_gpio_data; + + LOG_DBG("Successfully received value: 0x%x", byte); + LOG_PS2_INT("Successfully received value: ", &byte); + + // Since read was successful we reset the read try + data->cur_read_try = 0; + ps2_gpio_read_finish(); + + // If write_byte_await_response() is waiting, we notify + // the blocked write process of whether it was a success or not. + if (data->write_awaits_resp) { + data->write_awaits_resp_byte = byte; + data->write_awaits_resp = false; + k_sem_give(&data->write_awaits_resp_sem); + + // Don't send ack and err responses to the callback and read + // data queue. + // If it's an ack, the write process will return success. + // If it's an error, the write process will return failure. + if (byte == PS2_GPIO_RESP_ACK || byte == PS2_GPIO_RESP_RESEND || + byte == PS2_GPIO_RESP_FAILURE) { + + return; + } + } + + // If no callback is set, we add the data to a fifo queue + // that can be read later with the read using `ps2_read` + if (data->callback_isr != NULL && data->callback_enabled) { + + // Call callback from a worker to make sure the callback + // doesn't block the interrupt. + // Will call ps2_gpio_read_callback_work_handler + data->callback_byte = byte; + k_work_submit_to_queue(&ps2_gpio_work_queue_cb, &data->callback_work); + } else { + ps2_gpio_data_queue_add(byte); + } +} + +void ps2_gpio_read_callback_work_handler(struct k_work *work) { + struct ps2_gpio_data *data = &ps2_gpio_data; + + data->callback_isr(data->dev, data->callback_byte); + data->callback_byte = 0x0; +} + +void ps2_gpio_read_finish() { + struct ps2_gpio_data *data = &ps2_gpio_data; + + data->cur_read_pos = PS2_GPIO_POS_START; + data->cur_read_byte = 0x0; + + k_work_cancel_delayable(&data->read_scl_timout); +} + +/* + * Writing PS2 data + */ + +int ps2_gpio_write_byte_await_response(uint8_t byte); +int ps2_gpio_write_byte_blocking(uint8_t byte); +int ps2_gpio_write_byte_start(uint8_t byte); +void ps2_gpio_write_inhibition_wait(struct k_work *item); +void ps2_gpio_write_interrupt_handler(); +void ps2_gpio_write_finish(bool successful, char *descr); +void ps2_gpio_write_scl_timeout(struct k_work *item); +bool ps2_gpio_get_byte_parity(uint8_t byte); + +// Returned when there was an error writing to the PS2 device, such +// as not getting a clock from the device or receiving an invalid +// ack bit. +#define PS2_GPIO_E_WRITE_TRANSMIT 1 + +// Returned when the semaphore times out. Theoretically this shouldn't be +// happening. But it can happen if the same thread is used for both the +// semaphore wait and the inhibition timeout. +#define PS2_GPIO_E_WRITE_SEM_TIMEOUT 2 + +// Returned when the write finished seemingly successful, but the +// device didn't send a response in time. +#define PS2_GPIO_E_WRITE_RESPONSE 3 + +// Returned when the write finished seemingly successful, but the +// device responded with 0xfe (request to resend) and we ran out of +// retry attempts. +#define PS2_GPIO_E_WRITE_RESEND 4 + +// Returned when the write finished seemingly successful, but the +// device responded with 0xfc (failure / cancel). +#define PS2_GPIO_E_WRITE_FAILURE 5 + +K_MUTEX_DEFINE(ps2_gpio_write_mutex); + +int ps2_gpio_write_byte(uint8_t byte) { + int err; + + LOG_DBG("\n"); + LOG_DBG("START WRITE: 0x%x", byte); + + k_mutex_lock(&ps2_gpio_write_mutex, K_FOREVER); + + for (int i = 0; i < PS2_GPIO_WRITE_MAX_RETRY; i++) { + if (i > 0) { + LOG_WRN("Attempting write re-try #%d of %d...", i + 1, PS2_GPIO_WRITE_MAX_RETRY); + } + + err = ps2_gpio_write_byte_await_response(byte); + + if (err == 0) { + if (i > 0) { + LOG_WRN("Successfully wrote 0x%x on try #%d of %d...", byte, i + 1, + PS2_GPIO_WRITE_MAX_RETRY); + } + break; + } else if (err == PS2_GPIO_E_WRITE_FAILURE) { + // Write failed and the device requested to stop trying + // to resend. + break; + } + } + + LOG_DBG("END WRITE: 0x%x\n", byte); + k_mutex_unlock(&ps2_gpio_write_mutex); + + return err; +} + +// Writes the byte and blocks execution until we read the +// response byte. +// Returns failure if the write fails or the response is 0xfe/0xfc (error) +// Returns success if the response is 0xfa (ack) or any value except of +// 0xfe. +// 0xfe, 0xfc and 0xfa are not passed on to the read data queue or callback. +int ps2_gpio_write_byte_await_response(uint8_t byte) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + err = ps2_gpio_write_byte_blocking(byte); + if (err) { + return err; + } + + data->write_awaits_resp = true; + + err = k_sem_take(&data->write_awaits_resp_sem, PS2_GPIO_TIMEOUT_WRITE_AWAIT_RESPONSE); + + uint8_t resp_byte = data->write_awaits_resp_byte; + data->write_awaits_resp_byte = 0x0; + data->write_awaits_resp = false; + + if (err) { + LOG_WRN("Write response didn't arrive in time for byte " + "0x%x. Considering send a failure.", + byte); + + return PS2_GPIO_E_WRITE_RESPONSE; + } + + LOG_DBG("Write for byte 0x%x received response: 0x%x", byte, resp_byte); + + // We fail the write since we got an error response + if (resp_byte == PS2_GPIO_RESP_RESEND) { + + return PS2_GPIO_E_WRITE_RESEND; + } else if (resp_byte == PS2_GPIO_RESP_FAILURE) { + + return PS2_GPIO_E_WRITE_FAILURE; + } + + // Most of the time when a write was successful the device + // responds with an 0xfa (ack), but for some commands it doesn't. + // So we consider all non-0xfe and 0xfc responses as successful. + return 0; +} + +int ps2_gpio_write_byte_blocking(uint8_t byte) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + // LOG_DBG("ps2_gpio_write_byte_blocking called with byte=0x%x", byte); + + err = ps2_gpio_write_byte_start(byte); + if (err) { + LOG_ERR("Could not initiate writing of byte."); + return PS2_GPIO_E_WRITE_TRANSMIT; + } + + // The async `write_byte_start` function takes the only available semaphor. + // This causes the `k_sem_take` call below to block until + // `ps2_gpio_write_finish` gives it back. + err = k_sem_take(&data->write_lock, PS2_GPIO_TIMEOUT_WRITE_BLOCKING); + if (err) { + + // This usually means the controller is busy with other interrupts, + // timed out processing the interrupts and even the scl timeout + // delayable wasn't called due to the delay. + // + // So we abort the write and try again. + LOG_ERR("Blocking write failed due to semaphore timeout for byte " + "0x%x: %d", + byte, err); + + ps2_gpio_write_finish(false, "semaphore timeout"); + return PS2_GPIO_E_WRITE_SEM_TIMEOUT; + } + + if (data->cur_write_status == PS2_GPIO_WRITE_STATUS_SUCCESS) { + // LOG_DBG("Blocking write finished successfully for byte 0x%x", byte); + err = 0; + } else { + LOG_ERR("Blocking write finished with failure for byte 0x%x status: %d", byte, + data->cur_write_status); + err = -data->cur_write_status; + } + + data->cur_write_status = PS2_GPIO_WRITE_STATUS_INACTIVE; + + return err; +} + +int ps2_gpio_write_byte_start(uint8_t byte) { + struct ps2_gpio_data *data = &ps2_gpio_data; + int err; + + LOG_DBG("ps2_gpio_write_byte_start called with byte=0x%x", byte); + + if (data->mode == PS2_GPIO_MODE_WRITE) { + LOG_ERR("Preventing write off byte 0x%x: " + "Another write in progress for 0x%x", + byte, data->cur_write_byte); + + return -EBUSY; + } + + // Take semaphore so that when `ps2_gpio_write_byte_blocking` attempts + // taking it, the process gets blocked. + // It is released in `ps2_gpio_write_finish`. + err = k_sem_take(&data->write_lock, K_NO_WAIT); + if (err != 0 && err != -EBUSY) { + LOG_ERR("ps2_gpio_write_byte_start could not take semaphore: %d", err); + + return err; + } + + // Change mode and set write_pos so that the read interrupt handler + // doesn't trigger when we bring the clock line low. + data->mode = PS2_GPIO_MODE_WRITE; + data->cur_write_pos = PS2_GPIO_POS_START; + data->cur_write_byte = byte; + + // Initiating a send aborts any in-progress reads, so we + // reset the current read byte + data->cur_write_status = PS2_GPIO_WRITE_STATUS_ACTIVE; + if (data->cur_read_pos != PS2_GPIO_POS_START || data->cur_read_byte != 0x0) { + LOG_WRN("Aborting in-progress read due to write of byte 0x%x", byte); + ps2_gpio_read_abort(false, "starting write"); + } + + // Configure data and clock lines for output + ps2_gpio_set_scl_callback_enabled(false); + ps2_gpio_configure_pin_scl_output(); + ps2_gpio_configure_pin_sda_output(); + + LOG_PS2_INT("Starting write of byte ", &byte); + + // Inhibit the line by setting clock low and data high + ps2_gpio_set_scl(0); + ps2_gpio_set_sda(1); + + LOG_PS2_INT("Inhibited clock line", NULL); + + // Keep the line inhibited for at least 100 microseconds + k_work_schedule_for_queue(&ps2_gpio_work_queue, &data->write_inhibition_wait, + PS2_GPIO_WRITE_INHIBIT_SLC_DURATION); + + // The code continues in ps2_gpio_write_inhibition_wait + return 0; +} + +void ps2_gpio_write_inhibition_wait(struct k_work *item) { + LOG_PS2_INT("Inhibition timer finished", NULL); + + struct k_work_delayable *d_work = k_work_delayable_from_work(item); + struct ps2_gpio_data *data = CONTAINER_OF(d_work, struct ps2_gpio_data, write_inhibition_wait); + + // Enable the scl interrupt again + ps2_gpio_set_scl_callback_enabled(true); + + // Set data to value of start bit + ps2_gpio_set_sda(0); + + LOG_PS2_INT("Set sda to start bit", NULL); + + // The start bit was sent by setting sda to low + // So the next scl interrupt will be for the first + // data bit. + data->cur_write_pos += 1; + + // Release the clock line and configure it as input + // This let's the device take control of the clock again + ps2_gpio_set_scl(1); + ps2_gpio_configure_pin_scl_input(); + + LOG_PS2_INT("Released clock", NULL); + + k_work_schedule_for_queue(&ps2_gpio_work_queue, &data->write_scl_timout, + PS2_GPIO_TIMEOUT_WRITE_SCL_START); + + // From here on the device takes over the control of the clock again + // Every time it is ready for the next bit to be trasmitted, it will... + // - Pull the clock line low + // - Which will trigger our `ps2_gpio_write_interrupt_handler` + // - Which will send the correct bit + // - After all bits are sent `ps2_gpio_write_finish` is called +} + +void ps2_gpio_write_interrupt_handler() { + // After initiating writing, the device takes over + // the clock and asks us for a new bit of data on + // each falling edge. + struct ps2_gpio_data *data = &ps2_gpio_data; + + if (data->cur_write_pos == PS2_GPIO_POS_START) { + // This should not be happening, because the PS2_GPIO_POS_START bit + // is sent in ps2_gpio_write_byte_start during inhibition + LOG_PS2_INT("Write interrupt", NULL); + return; + } + + k_work_cancel_delayable(&data->write_scl_timout); + + if (data->cur_write_pos > PS2_GPIO_POS_START && data->cur_write_pos < PS2_GPIO_POS_PARITY) { + // Set it to the data bit corresponding to the current + // write position (subtract start bit postion) + ps2_gpio_set_sda(PS2_GPIO_GET_BIT(data->cur_write_byte, (data->cur_write_pos - 1))); + } else if (data->cur_write_pos == PS2_GPIO_POS_PARITY) { + ps2_gpio_set_sda(ps2_gpio_get_byte_parity(data->cur_write_byte)); + } else if (data->cur_write_pos == PS2_GPIO_POS_STOP) { + // Send the stop bit (always 1) + ps2_gpio_set_sda(1); + + // Give control over data pin back to device after sending stop bit + // so that we can receive the ack bit from the device + ps2_gpio_configure_pin_sda_input(); + } else if (data->cur_write_pos == PS2_GPIO_POS_ACK) { + int ack_val = ps2_gpio_get_sda(); + + LOG_PS2_INT("Write interrupt", NULL); + + if (ack_val == 0) { + LOG_PS2_INT("Write was successful on ack: ", NULL); + ps2_gpio_write_finish(true, "valid ack bit"); + } else { + LOG_PS2_INT("Write failed on ack", NULL); + ps2_gpio_write_finish(false, "invalid ack bit"); + } + + return; + } else { + LOG_PS2_INT("Invalid write clock triggered", NULL); + + return; + } + + LOG_PS2_INT("Write interrupt", NULL); + + data->cur_write_pos += 1; + k_work_schedule_for_queue(&ps2_gpio_work_queue, &data->write_scl_timout, + PS2_GPIO_TIMEOUT_WRITE_SCL); +} + +void ps2_gpio_write_scl_timeout(struct k_work *item) { + // Once we start a transmission we expect the device to + // to send a new clock/interrupt within 100us. + // If we don't receive the next interrupt within that timeframe, + // we abort the writ). + + LOG_PS2_INT("Write SCL timeout", NULL); + ps2_gpio_write_finish(false, "scl timeout"); +} + +void ps2_gpio_write_finish(bool successful, char *descr) { + struct ps2_gpio_data *data = &ps2_gpio_data; + + k_work_cancel_delayable(&data->write_scl_timout); + + if (successful) { + LOG_DBG("Successfully wrote value 0x%x", data->cur_write_byte); + LOG_PS2_INT("Successfully wrote value ", &data->cur_write_byte); + data->cur_write_status = PS2_GPIO_WRITE_STATUS_SUCCESS; + } else { // Failure + LOG_ERR("Failed to write value 0x%x at pos=%d: %s", data->cur_write_byte, + data->cur_write_pos, descr); + LOG_PS2_INT("Failed to write value ", &data->cur_write_byte); + + data->cur_write_status = PS2_GPIO_WRITE_STATUS_FAILURE; + + // Make sure the scl callback is enabled + // It's possible that all threads are busy, + // write_inhibition_wait doesn't get called in time + // and the semaphore times out. + // In that case we want to make sure the interrupt + // callback is enabled again. + ps2_gpio_set_scl_callback_enabled(true); + } + + data->mode = PS2_GPIO_MODE_READ; + data->cur_read_pos = PS2_GPIO_POS_START; + data->cur_write_pos = PS2_GPIO_POS_START; + data->cur_write_byte = 0x0; + + // Give back control over data and clock line if we still hold on to it + ps2_gpio_configure_pin_sda_input(); + ps2_gpio_configure_pin_scl_input(); + + // Give the semaphore to allow write_byte_blocking to continue + k_sem_give(&data->write_lock); +} + +/* + * Interrupt Handler + */ + +void ps2_gpio_scl_interrupt_handler(const struct device *dev, struct gpio_callback *cb, + uint32_t pins) { + struct ps2_gpio_data *data = &ps2_gpio_data; + +#if IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) + k_work_cancel_delayable(&interrupt_log_scl_timout); +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) */ + + if (data->mode == PS2_GPIO_MODE_READ) { + ps2_gpio_read_interrupt_handler(); + } else { + ps2_gpio_write_interrupt_handler(); + } + +#if IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) + k_work_schedule_for_queue(&ps2_gpio_work_queue_cb, &interrupt_log_scl_timout, + PS2_GPIO_INTERRUPT_LOG_SCL_TIMEOUT); +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) */ +} + +/* + * Zephyr PS/2 driver interface + */ +static int ps2_gpio_enable_callback(const struct device *dev); + +#if IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) + +static int ps2_gpio_configure(const struct device *dev, ps2_callback_t callback_isr, + ps2_resend_callback_t resend_callback_isr) { + struct ps2_gpio_data *data = dev->data; + + if (!callback_isr && !resend_callback_isr) { + return -EINVAL; + } + + if (callback_isr) { + data->callback_isr = callback_isr; + ps2_gpio_enable_callback(dev); + } + + if (resend_callback_isr) { + data->resend_callback_isr = resend_callback_isr; + } + + return 0; +} + +#else + +static int ps2_gpio_configure(const struct device *dev, ps2_callback_t callback_isr) { + struct ps2_gpio_data *data = dev->data; + + if (!callback_isr) { + return -EINVAL; + } + + data->callback_isr = callback_isr; + ps2_gpio_enable_callback(dev); + + return 0; +} + +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_ENABLE_PS2_RESEND_CALLBACK) */ + +int ps2_gpio_read(const struct device *dev, uint8_t *value) { + // TODO: Add a way to not return old queue items + // Maybe only bytes that were received within past 10 seconds. + uint8_t queue_byte; + int err = ps2_gpio_data_queue_get_next(&queue_byte, PS2_GPIO_TIMEOUT_READ); + if (err) { // Timeout due to no data to read in data queue + // LOG_DBG("ps2_gpio_read: Fifo timed out..."); + + return -ETIMEDOUT; + } + + // LOG_DBG("ps2_gpio_read: Returning 0x%x", queue_byte); + *value = queue_byte; + + return 0; +} + +static int ps2_gpio_write(const struct device *dev, uint8_t value) { + return ps2_gpio_write_byte(value); +} + +static int ps2_gpio_disable_callback(const struct device *dev) { + struct ps2_gpio_data *data = dev->data; + + // Make sure there are no stale items in the data queue + // from before the callback was disabled. + ps2_gpio_data_queue_empty(); + + data->callback_enabled = false; + + // LOG_DBG("Disabled PS2 callback."); + + return 0; +} + +static int ps2_gpio_enable_callback(const struct device *dev) { + struct ps2_gpio_data *data = dev->data; + data->callback_enabled = true; + + // LOG_DBG("Enabled PS2 callback."); + + ps2_gpio_data_queue_empty(); + + return 0; +} + +static const struct ps2_driver_api ps2_gpio_driver_api = { + .config = ps2_gpio_configure, + .read = ps2_gpio_read, + .write = ps2_gpio_write, + .disable_callback = ps2_gpio_disable_callback, + .enable_callback = ps2_gpio_enable_callback, +}; + +/* + * PS/2 GPIO Driver Init + */ + +static int ps2_gpio_init_gpio(void) { + struct ps2_gpio_data *data = &ps2_gpio_data; + struct ps2_gpio_config *config = (struct ps2_gpio_config *)&ps2_gpio_config; + int err; + + // Make pin info accessible through the data struct + data->scl_gpio = config->scl_gpio; + data->sda_gpio = config->sda_gpio; + + // Overwrite any user-provided flags from the devicetree + data->scl_gpio.dt_flags = 0; + data->scl_gpio.dt_flags = 0; + + // Setup interrupt callback for clock line + gpio_init_callback(&data->scl_cb_data, ps2_gpio_scl_interrupt_handler, BIT(data->scl_gpio.pin)); + + err = gpio_add_callback(config->scl_gpio.port, &data->scl_cb_data); + if (err) { + LOG_ERR("failed to enable interrupt callback on " + "SCL GPIO pin (err %d)", + err); + } + + ps2_gpio_set_scl_callback_enabled(true); + ps2_gpio_configure_pin_scl_input(); + ps2_gpio_configure_pin_sda_input(); + + // Check if this stuff is needed + // TODO: Figure out why this is requiered. + ps2_gpio_set_sda(1); + ps2_gpio_set_scl(1); + + return err; +} + +static int ps2_gpio_init(const struct device *dev) { + + struct ps2_gpio_data *data = dev->data; + + // Set the ps2 device so we can retrieve it later for + // the ps2 callback + data->dev = dev; + + ps2_gpio_init_gpio(); + + // Init data queue for synchronous read operations + k_msgq_init(&data->data_queue, data->data_queue_buffer, sizeof(struct ps2_gpio_data_queue_item), + PS2_GPIO_DATA_QUEUE_SIZE); + + // Init semaphore for blocking writes + k_sem_init(&data->write_lock, 0, 1); + + // Init semaphore that waits for read after write + k_sem_init(&data->write_awaits_resp_sem, 0, 1); + + // Custom queue for background PS/2 processing work at high priority + k_work_queue_start(&ps2_gpio_work_queue, ps2_gpio_work_queue_stack_area, + K_THREAD_STACK_SIZEOF(ps2_gpio_work_queue_stack_area), + PS2_GPIO_WORK_QUEUE_PRIORITY, NULL); + + // Custom queue for calling the zephyr ps/2 callback at lower priority + k_work_queue_start(&ps2_gpio_work_queue_cb, ps2_gpio_work_queue_cb_stack_area, + K_THREAD_STACK_SIZEOF(ps2_gpio_work_queue_cb_stack_area), + PS2_GPIO_WORK_QUEUE_CB_PRIORITY, NULL); + + // Timeouts for clock pulses during read and write + k_work_init_delayable(&data->read_scl_timout, ps2_gpio_read_scl_timeout); + k_work_init_delayable(&data->write_scl_timout, ps2_gpio_write_scl_timeout); + k_work_init_delayable(&data->write_inhibition_wait, ps2_gpio_write_inhibition_wait); + +#if IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) + k_work_init_delayable(&interrupt_log_scl_timout, ps2_gpio_interrupt_log_scl_timeout); + k_work_init(&interrupt_log_print_worker, ps2_gpio_interrupt_log_print_worker); +#endif /* IS_ENABLED(CONFIG_PS2_GPIO_INTERRUPT_LOG_ENABLED) */ + + k_work_init(&data->callback_work, ps2_gpio_read_callback_work_handler); + k_work_init(&data->resend_cmd_work, ps2_gpio_send_cmd_resend_worker); + + return 0; +} + +DEVICE_DT_INST_DEFINE(0, &ps2_gpio_init, NULL, &ps2_gpio_data, &ps2_gpio_config, POST_KERNEL, + CONFIG_PS2_INIT_PRIORITY, &ps2_gpio_driver_api); diff --git a/app/module/drivers/ps2/ps2_uart.c b/app/module/drivers/ps2/ps2_uart.c new file mode 100644 index 00000000000..51a62f12f5f --- /dev/null +++ b/app/module/drivers/ps2/ps2_uart.c @@ -0,0 +1,1301 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT uart_ps2 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_LEVEL CONFIG_PS2_LOG_LEVEL +LOG_MODULE_REGISTER(ps2_uart); + +/* + * Pin Control + */ + +PINCTRL_DT_DEFINE(DT_INST_BUS(0)); + +/* + * Settings + */ + +#define PS2_UART_WRITE_MAX_RETRY 5 +#define PS2_UART_READ_MAX_RETRY 3 + +#define PS2_UART_DATA_QUEUE_SIZE 100 + +// Custom queue for background PS/2 processing work at low priority +// We purposefully want this to be a fairly low priority, because +// this queue is used while we wait to start a write. +// If the system is very busy with interrupts and other threads, then we +// want to wait until that is over so that our write interrupts don't get +// missed. +#define PS2_UART_WORK_QUEUE_PRIORITY 10 +#define PS2_UART_WORK_QUEUE_STACK_SIZE 1024 + +// Custom queue for calling the zephyr ps/2 callback. +// We don't want to hand it off to that API in an ISR since that callback +// could be using blocking functions. +// But we also don't want to hand it off at a low priority, since the PS/2 +// packets must be dealt with quickly. So we use a fairly high priority. +#define PS2_UART_WORK_QUEUE_CB_PRIORITY 2 +#define PS2_UART_WORK_QUEUE_CB_STACK_SIZE 1024 + +/* + * PS/2 Defines + */ + +#define PS2_UART_POS_START 0 +#define PS2_UART_POS_DATA_FIRST 1 +#define PS2_UART_POS_DATA_LAST 8 +#define PS2_UART_POS_PARITY 9 +#define PS2_UART_POS_STOP 10 +#define PS2_UART_POS_ACK 11 // Write mode only + +#define PS2_UART_RESP_ACK 0xfa +#define PS2_UART_RESP_RESEND 0xfe +#define PS2_UART_RESP_FAILURE 0xfc + +/* + * PS/2 Timings + */ + +#define PS2_UART_TIMING_SCL_CYCLE_LEN 69 + +// The minimum time needed to inhibit clock to start a write +// is 100us, but we triple it just in case. +#define PS2_UART_TIMING_SCL_INHIBITION_MIN 100 + +// Theoretically, only 100us is required, but practically, trackponts +// seem to respond to a total duration of 1,000 us the best. +// This is also the duration my USB to PS/2 adapter is using. +#define PS2_UART_TIMING_SCL_INHIBITION (5 * PS2_UART_TIMING_SCL_INHIBITION_MIN) + +// PS2 uses a frequency between 10 kHz and 16.7 kHz. So clocks should arrive +// within 60-100us. +#define PS2_UART_TIMING_SCL_CYCLE_MIN 60 +#define PS2_UART_TIMING_SCL_CYCLE_MAX 100 + +// After inhibiting and releasing the clock, the device starts sending +// the clock. It's supposed to start immediately, but some devices +// need much longer if you are asking them to interrupt an +// ongoing read. +#define PS2_UART_TIMING_SCL_INHIBITION_RESP_MAX 3000 +#define PS2_UART_TIMEOUT_WRITE_SCL_START K_USEC(PS2_UART_TIMING_SCL_INHIBITION_RESP_MAX) + +// Max time we allow the device to send the next clock signal during writes. +// Even though PS/2 devices send the clock at most every 100us, it doesn't mean +// that the interrupts always get triggered within that time. So we allow a +// little extra time. +#define PS2_UART_TIMEOUT_WRITE_SCL K_USEC(PS2_UART_TIMING_SCL_CYCLE_MAX + 50) + +// Writes start with us inhibiting the line and then respond +// with 11 bits (start bit included in inhibition time). +// To be conservative we give it another 2 cycles to complete +#define PS2_UART_TIMING_WRITE_MAX_TIME \ + (PS2_UART_TIMING_SCL_INHIBITION + PS2_UART_TIMING_SCL_INHIBITION_RESP_MAX + \ + 11 * PS2_UART_TIMING_SCL_CYCLE_MAX + 2 * PS2_UART_TIMING_SCL_CYCLE_MAX) + +// Reads are 11bit and we give it another 2 cycles to start and stop +#define PS2_UART_TIMING_READ_MAX_TIME \ + (11 * PS2_UART_TIMING_SCL_CYCLE_MAX + 2 * PS2_UART_TIMING_SCL_CYCLE_MAX) + +// Timeout for write_byte_await_response() +// PS/2 spec says that device must respond within 20msec, +// but real life devices take much longer. Especially if +// you interrupt existing transmissions. +#define PS2_UART_TIMEOUT_WRITE_AWAIT_RESPONSE K_MSEC(300) + +/* + * Driver Defines + */ + +// Timeout for blocking read using the zephyr PS2 ps2_read() function +// This is not a matter of PS/2 timings, but a preference of how long we let +// the user wait until we give up on reading. +#define PS2_UART_TIMEOUT_READ K_SECONDS(2) + +// Timeout for write_byte_blocking() +#define PS2_UART_TIMEOUT_WRITE_BLOCKING K_USEC(PS2_UART_TIMING_WRITE_MAX_TIME) + +/* + * Global Variables + */ + +// Used to keep track of blocking write status +typedef enum { + PS2_UART_WRITE_STATUS_INACTIVE, + PS2_UART_WRITE_STATUS_ACTIVE, + PS2_UART_WRITE_STATUS_SUCCESS, + PS2_UART_WRITE_STATUS_FAILURE, +} ps2_uart_write_status; + +struct ps2_uart_data_queue_item { + uint8_t byte; +}; + +struct ps2_uart_config { + const struct device *uart_dev; + struct gpio_dt_spec scl_gpio; + struct gpio_dt_spec sda_gpio; + const struct pinctrl_dev_config *pcfg; +}; + +struct ps2_uart_data { + const struct device *dev; + + // SCL GPIO callback for writing + struct gpio_callback scl_cb_data; + + // PS2 driver interface callback + struct k_work callback_work; + uint8_t callback_byte; + ps2_callback_t callback_isr; +#if IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) + ps2_resend_callback_t resend_callback_isr; +#endif /* IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) */ + + bool callback_enabled; + + // Queue for ps2_read() + struct k_msgq data_queue; + char data_queue_buffer[PS2_UART_DATA_QUEUE_SIZE * sizeof(struct ps2_uart_data_queue_item)]; + + // Write byte + ps2_uart_write_status cur_write_status; + uint8_t cur_write_byte; + int cur_write_pos; + bool write_awaits_resp; + uint8_t write_awaits_resp_byte; + struct k_sem write_awaits_resp_sem; + struct k_sem write_lock; + struct k_work_delayable write_scl_timout; + + struct k_work resend_cmd_work; +}; + +static const struct ps2_uart_config ps2_uart_config = { + .uart_dev = DEVICE_DT_GET(DT_INST_BUS(0)), + .scl_gpio = GPIO_DT_SPEC_INST_GET(0, scl_gpios), + .sda_gpio = GPIO_DT_SPEC_INST_GET(0, sda_gpios), + .pcfg = PINCTRL_DT_DEV_CONFIG_GET(DT_INST_BUS(0)), +}; + +static struct ps2_uart_data ps2_uart_data = { + .callback_byte = 0x0, + .callback_isr = NULL, + +#if IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) + .resend_callback_isr = NULL, +#endif /* IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) */ + + .callback_enabled = false, + + .cur_write_status = PS2_UART_WRITE_STATUS_INACTIVE, + .cur_write_byte = 0x0, + .cur_write_pos = 0, + .write_awaits_resp = false, + .write_awaits_resp_byte = 0x0, +}; + +K_THREAD_STACK_DEFINE(ps2_uart_work_queue_stack_area, PS2_UART_WORK_QUEUE_STACK_SIZE); +static struct k_work_q ps2_uart_work_queue; + +K_THREAD_STACK_DEFINE(ps2_uart_work_queue_cb_stack_area, PS2_UART_WORK_QUEUE_CB_STACK_SIZE); +static struct k_work_q ps2_uart_work_queue_cb; + +/* + * Function Definitions + */ + +int ps2_uart_write_byte(uint8_t byte); + +/* + * Helpers functions + */ + +#define PS2_UART_GET_BIT(data, bit_pos) ((data >> bit_pos) & 0x1) + +int ps2_uart_get_scl() { + const struct ps2_uart_config *config = &ps2_uart_config; + int rc = gpio_pin_get_dt(&config->scl_gpio); + + return rc; +} + +int ps2_uart_get_sda() { + const struct ps2_uart_config *config = &ps2_uart_config; + int rc = gpio_pin_get_dt(&config->sda_gpio); + + return rc; +} + +void ps2_uart_set_scl(int state) { + const struct ps2_uart_config *config = &ps2_uart_config; + + gpio_pin_set_dt(&config->scl_gpio, state); +} + +void ps2_uart_set_sda(int state) { + const struct ps2_uart_config *config = &ps2_uart_config; + + // LOG_INF("Seting sda to %d", state); + gpio_pin_set_dt(&config->sda_gpio, state); +} + +int ps2_uart_configure_pin_scl(gpio_flags_t flags, char *descr) { + struct ps2_uart_config *config = (struct ps2_uart_config *)&ps2_uart_config; + int err; + + err = gpio_pin_configure_dt(&config->scl_gpio, flags); + if (err) { + LOG_ERR("failed to configure SCL GPIO pin to %s (err %d)", descr, err); + } + + return err; +} + +int ps2_uart_configure_pin_scl_input() { return ps2_uart_configure_pin_scl((GPIO_INPUT), "input"); } + +int ps2_uart_configure_pin_scl_output() { + return ps2_uart_configure_pin_scl((GPIO_OUTPUT_HIGH), "output"); +} + +int ps2_uart_configure_pin_sda(gpio_flags_t flags, char *descr) { + struct ps2_uart_config *config = (struct ps2_uart_config *)&ps2_uart_config; + int err; + + err = gpio_pin_configure_dt(&config->sda_gpio, flags); + if (err) { + LOG_ERR("failed to configure SDA GPIO pin to %s (err %d)", descr, err); + } + + return err; +} + +int ps2_uart_configure_pin_sda_input() { return ps2_uart_configure_pin_sda((GPIO_INPUT), "input"); } + +int ps2_uart_configure_pin_sda_output() { + return ps2_uart_configure_pin_sda((GPIO_OUTPUT_HIGH), "output"); +} + +int ps2_uart_set_scl_callback_enabled(bool enabled) { + struct ps2_uart_config *config = (struct ps2_uart_config *)&ps2_uart_config; + int err; + + // LOG_INF("Setting ps2_uart_set_scl_callback_enabled: %d", enabled); + + if (enabled) { + err = gpio_pin_interrupt_configure_dt(&config->scl_gpio, (GPIO_INT_EDGE_FALLING)); + if (err) { + LOG_ERR("failed to enable interrupt on " + "SCL GPIO pin (err %d)", + err); + return err; + } + } else { + err = gpio_pin_interrupt_configure_dt(&config->scl_gpio, (GPIO_INT_DISABLE)); + if (err) { + LOG_ERR("failed to disable interrupt on " + "SCL GPIO pin (err %d)", + err); + return err; + } + } + + return err; +} + +static int ps2_uart_set_mode_read() { + const struct ps2_uart_config *config = &ps2_uart_config; + int err; + + // Set the SDA pin for the uart device + err = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT); + if (err < 0) { + LOG_ERR("Could not switch pinctrl state to DEFAULT: %d", err); + return err; + } + + // Make sure SCL interrupt is disabled + ps2_uart_set_scl_callback_enabled(false); + + // Enable UART interrupt + uart_irq_rx_enable(config->uart_dev); + + return err; +} + +static int ps2_uart_set_mode_write() { + const struct ps2_uart_config *config = &ps2_uart_config; + int err; + + // Set pincntrl with unused pins so that we can control the pins + // through GPIO + err = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_SLEEP); + if (err < 0) { + LOG_ERR("Could not switch pinctrl state to OFF: %d", err); + return err; + } + + // Disable UART interrupt + // Unintuitively, this has to be done AFTER applying the pincntrl state, + // otherwise GPIO won't be able to use the data pin + uart_irq_rx_disable(config->uart_dev); + + // Configure data and clock lines for output + ps2_uart_set_scl_callback_enabled(false); + ps2_uart_configure_pin_scl_output(); + ps2_uart_configure_pin_sda_output(); + + return err; +} + +void log_binary(uint8_t value) { + char binary_str[9]; + + for (int i = 7; i >= 0; --i) { + binary_str[7 - i] = PS2_UART_GET_BIT(value, i) ? '1' : '0'; + } + + binary_str[8] = '\0'; + + LOG_INF("Binary Value of 0x%x: %s", value, binary_str); +} + +bool ps2_uart_get_byte_parity(uint8_t byte) { + int byte_parity = __builtin_parity(byte); + + // gcc parity returns 1 if there is an odd number of bits in byte + // But the PS2 protocol sets the parity bit to 0 if there is an odd number + return !byte_parity; +} + +uint8_t ps2_uart_data_queue_get_next(uint8_t *dst_byte, k_timeout_t timeout) { + struct ps2_uart_data *data = &ps2_uart_data; + struct ps2_uart_data_queue_item queue_data; + int ret; + + ret = k_msgq_get(&data->data_queue, &queue_data, timeout); + if (ret != 0) { + LOG_WRN("Data queue timed out..."); + return -ETIMEDOUT; + } + + *dst_byte = queue_data.byte; + + return 0; +} + +void ps2_uart_data_queue_empty() { + struct ps2_uart_data *data = &ps2_uart_data; + + k_msgq_purge(&data->data_queue); +} + +void ps2_uart_data_queue_add(uint8_t byte) { + struct ps2_uart_data *data = &ps2_uart_data; + + int ret; + + struct ps2_uart_data_queue_item queue_data; + queue_data.byte = byte; + + LOG_INF("Adding byte to data queue: 0x%x", byte); + + for (int i = 0; i < 2; i++) { + ret = k_msgq_put(&data->data_queue, &queue_data, K_NO_WAIT); + if (ret == 0) { + break; + } else { + LOG_WRN("Data queue full. Removing oldest item."); + + uint8_t tmp_byte; + ps2_uart_data_queue_get_next(&tmp_byte, K_NO_WAIT); + } + } + + if (ret != 0) { + LOG_ERR("Failed to add byte 0x%x to the data queue.", byte); + } +} + +void ps2_uart_send_cmd_resend_worker(struct k_work *item) { + +#if IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) + + struct ps2_uart_data *data = &ps2_uart_data; + + // Notify the PS/2 device driver that we are requesting a resend. + // PS/2 devices don't just resend the last byte that was sent, but the + // entire command packet, which can be multiple bytes. + if (data->resend_callback_isr != NULL && data->callback_enabled) { + + data->resend_callback_isr(data->dev); + } + +#endif /* IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) */ + + uint8_t cmd = 0xfe; + // LOG_DBG("Requesting resend of data with command: 0x%x", cmd); + ps2_uart_write_byte(cmd); +} + +void ps2_uart_send_cmd_resend() { + struct ps2_uart_data *data = &ps2_uart_data; + + if (k_is_in_isr()) { + + // It's important to submit this on the cb queue and not on the + // same queue as the inhibition delay. + // Otherwise the queue will be blocked by the semaphore and the + // inhibition delay worker will never be called. + k_work_submit_to_queue(&ps2_uart_work_queue_cb, &data->resend_cmd_work); + } else { + ps2_uart_send_cmd_resend_worker(NULL); + } +} + +/* + * Reading PS2 data + */ +static void ps2_uart_interrupt_handler(const struct device *uart_dev, void *user_data); +void ps2_uart_read_interrupt_handler(); +static int ps2_uart_read_err_check(const struct device *dev); +void ps2_uart_read_process_received_byte(uint8_t byte); +const char *ps2_uart_read_get_error_str(int err); + +static void ps2_uart_interrupt_handler(const struct device *uart_dev, void *user_data) { + int err; + + err = uart_irq_update(uart_dev); + if (err != 1) { + LOG_ERR("uart_irq_update returned: %d", err); + return; + } + + while (uart_irq_rx_ready(uart_dev)) { + ps2_uart_read_interrupt_handler(uart_dev, user_data); + } +} + +void ps2_uart_read_interrupt_handler(const struct device *uart_dev, void *user_data) { + uint8_t byte; + + int byte_len = uart_fifo_read(uart_dev, &byte, 1); + if (byte_len < 1) { + LOG_ERR("UART read failed with error: %d", byte_len); + return; + } + + ps2_uart_read_process_received_byte(byte); +} + +static int ps2_uart_read_err_check(const struct device *dev) { + // TOOD: Make this function only work if nrf52 is used + int err = uart_err_check(dev); + + // In the config we enabled even parity, because nrf52 does + // not support odd parity. + // But PS/2 uses odd parity. This should generate a parity error on + // every reception. + // If the parity error doesn't happen, it means the transfer had an + // actual even parity, which we consider an error. + if ((err & NRF_UARTE_ERROR_PARITY_MASK) == 0) { + err = UART_ERROR_PARITY; + } else if (err & NRF_UARTE_ERROR_OVERRUN_MASK) { + err = UART_ERROR_OVERRUN; + } else if (err & NRF_UARTE_ERROR_FRAMING_MASK) { + err = UART_ERROR_FRAMING; + } else if (err & NRF_UARTE_ERROR_BREAK_MASK) { + err = UART_BREAK; + } else { // No errors + err = 0; + } + + return err; +} + +void ps2_uart_read_process_received_byte(uint8_t byte) { + struct ps2_uart_data *data = &ps2_uart_data; + const struct ps2_uart_config *config = &ps2_uart_config; + + int err; + + LOG_DBG("UART Received: 0x%x", byte); + + err = ps2_uart_read_err_check(config->uart_dev); + if (err != 0) { + const char *err_str = ps2_uart_read_get_error_str(err); + + // Framing errors + if (byte == 0xfa && err == UART_ERROR_FRAMING) { + // Ignore, because it is not a real error and happens frequently + } else { + LOG_WRN("UART RX detected error for byte 0x%x: %s (%d)", byte, err_str, err); + } + } + + LOG_DBG("Received byte: 0x%x", byte); + + // If write_byte_await_response() is waiting, we notify + // the blocked write process of whether it was a success or not. + if (data->write_awaits_resp) { + data->write_awaits_resp_byte = byte; + data->write_awaits_resp = false; + k_sem_give(&data->write_awaits_resp_sem); + + // Don't send ack and err responses to the callback and read + // data queue. + // If it's an ack, the write process will return success. + // If it's an error, the write process will return failure. + if (byte == PS2_UART_RESP_ACK || byte == PS2_UART_RESP_RESEND || + byte == PS2_UART_RESP_FAILURE) { + + return; + } + } + + // If no callback is set, we add the data to a fifo queue + // that can be read later with the read using `ps2_read` + if (data->callback_isr != NULL && data->callback_enabled) { + + // Call callback from a worker to make sure the callback + // doesn't block the interrupt. + // Will call ps2_uart_read_callback_work_handler + data->callback_byte = byte; + k_work_submit_to_queue(&ps2_uart_work_queue_cb, &data->callback_work); + } else { + ps2_uart_data_queue_add(byte); + } +} + +const char *ps2_uart_read_get_error_str(int err) { + switch (err) { + case UART_ERROR_OVERRUN: + return "Overrun error"; + case UART_ERROR_PARITY: + return "Parity error"; + case UART_ERROR_FRAMING: + return "Framing error"; + case UART_BREAK: + return "Break interrupt"; + case UART_ERROR_COLLISION: + return "Collision error"; + default: + return "Unknown error"; + } +} + +void ps2_uart_read_callback_work_handler(struct k_work *work) { + struct ps2_uart_data *data = &ps2_uart_data; + + data->callback_isr(data->dev, data->callback_byte); + data->callback_byte = 0x0; +} + +/* + * Writing PS2 data + */ + +int ps2_uart_write_byte_await_response(uint8_t byte); +int ps2_uart_write_byte_blocking(uint8_t byte); +int ps2_uart_write_byte_start(uint8_t byte); +void ps2_uart_write_scl_interrupt_handler(const struct device *dev, struct gpio_callback *cb, + uint32_t pins); +void ps2_uart_write_scl_timeout(struct k_work *item); +void ps2_uart_write_finish(bool successful, char *descr); + +// Returned when there was an error writing to the PS2 device, such +// as not getting a clock from the device or receiving an invalid +// ack bit. +#define PS2_UART_E_WRITE_TRANSMIT 1 + +// Returned when the semaphore times out. Theoretically this shouldn't be +// happening. But it can happen if the same thread is used for both the +// semaphore wait and the inhibition timeout. +#define PS2_UART_E_WRITE_SEM_TIMEOUT 2 + +// Returned when the write finished seemingly successful, but the +// device didn't send a response in time. +#define PS2_UART_E_WRITE_RESPONSE 3 + +// Returned when the write finished seemingly successful, but the +// device responded with 0xfe (request to resend) and we ran out of +// retry attempts. +#define PS2_UART_E_WRITE_RESEND 4 + +// Returned when the write finished seemingly successful, but the +// device responded with 0xfc (failure / cancel). +#define PS2_UART_E_WRITE_FAILURE 5 + +K_MUTEX_DEFINE(ps2_uart_write_mutex); + +int ps2_uart_write_byte_debug(uint8_t byte) { + int err; + + LOG_WRN("DEBUG WRITE STARTED for byte 0x%x", byte); + + LOG_WRN("Setting Write mode"); + err = ps2_uart_set_mode_write(); + if (err != 0) { + LOG_ERR("Could not configure driver for write mode: %d", err); + return err; + } + // k_sleep(K_MSEC(1000)); + // err = ps2_uart_set_mode_write(); + // if (err != 0) { + // LOG_ERR("Could not configure driver for write mode: %d", err); + // return err; + // } + LOG_WRN("Setting Write mode: Done"); + + // Inhibit the line by setting clock low and data high for 100us + LOG_INF("Setting low"); + ps2_uart_set_scl(0); + ps2_uart_set_sda(0); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting high"); + ps2_uart_set_scl(1); + ps2_uart_set_sda(1); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting low"); + ps2_uart_set_scl(0); + ps2_uart_set_sda(0); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting high"); + ps2_uart_set_scl(1); + ps2_uart_set_sda(1); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting low"); + ps2_uart_set_scl(0); + ps2_uart_set_sda(0); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting high"); + ps2_uart_set_scl(1); + ps2_uart_set_sda(1); + k_sleep(K_MSEC(100)); + + LOG_INF("Setting low"); + ps2_uart_set_scl(0); + ps2_uart_set_sda(0); + k_sleep(K_MSEC(100)); + + LOG_WRN("Enabling interrupt callback"); + ps2_uart_set_scl_callback_enabled(true); + + LOG_WRN("Setting SCL input"); + ps2_uart_configure_pin_scl_input(); + + k_sleep(K_MSEC(300)); + + LOG_WRN("Switching back to mode read"); + err = ps2_uart_set_mode_read(); + if (err != 0) { + LOG_ERR("Could not configure driver for write mode: %d", err); + return err; + } + + LOG_WRN("Finished Debug write"); + + return -1; +} + +int ps2_uart_write_byte(uint8_t byte) { + int err; + + LOG_DBG("\n"); + LOG_DBG("Writing: 0x%x", byte); + + k_mutex_lock(&ps2_uart_write_mutex, K_FOREVER); + + for (int i = 0; i < PS2_UART_WRITE_MAX_RETRY; i++) { + if (i > 0) { + LOG_WRN("Attempting write re-try #%d of %d...", i + 1, PS2_UART_WRITE_MAX_RETRY); + } + + err = ps2_uart_write_byte_await_response(byte); + + if (err == 0) { + if (i > 0) { + LOG_WRN("Successfully wrote 0x%x on try #%d of %d...", byte, i + 1, + PS2_UART_WRITE_MAX_RETRY); + } + break; + } else if (err == PS2_UART_E_WRITE_FAILURE) { + // Write failed and the device requested to stop trying + // to resend. + break; + } + } + + LOG_DBG("END WRITE: 0x%x\n", byte); + k_mutex_unlock(&ps2_uart_write_mutex); + + return err; +} + +// Writes the byte and blocks execution until we read the +// response byte. +// Returns failure if the write fails or the response is 0xfe/0xfc (error) +// Returns success if the response is 0xfa (ack) or any value except of +// 0xfe. +// 0xfe, 0xfc and 0xfa are not passed on to the read data queue or callback. +int ps2_uart_write_byte_await_response(uint8_t byte) { + struct ps2_uart_data *data = &ps2_uart_data; + int err; + + err = ps2_uart_write_byte_blocking(byte); + if (err) { + return err; + } + + data->write_awaits_resp = true; + + err = k_sem_take(&data->write_awaits_resp_sem, PS2_UART_TIMEOUT_WRITE_AWAIT_RESPONSE); + + uint8_t resp_byte = data->write_awaits_resp_byte; + data->write_awaits_resp_byte = 0x0; + data->write_awaits_resp = false; + + if (err) { + LOG_WRN("Write response didn't arrive in time for byte " + "0x%x. Considering send a failure.", + byte); + + return PS2_UART_E_WRITE_RESPONSE; + } + + if (resp_byte == PS2_UART_RESP_RESEND || resp_byte == PS2_UART_RESP_FAILURE) { + LOG_WRN("Write of 0x%x received error response: 0x%x", byte, resp_byte); + } else { + LOG_DBG("Write for byte 0x%x received response: 0x%x", byte, resp_byte); + } + + // We fail the write since we got an error response + if (resp_byte == PS2_UART_RESP_RESEND) { + + return PS2_UART_E_WRITE_RESEND; + } else if (resp_byte == PS2_UART_RESP_FAILURE) { + + return PS2_UART_E_WRITE_FAILURE; + } + + // Most of the time when a write was successful the device + // responds with an 0xfa (ack), but for some commands it doesn't. + // So we consider all non-0xfe and 0xfc responses as successful. + return 0; +} + +int ps2_uart_write_byte_blocking(uint8_t byte) { + struct ps2_uart_data *data = &ps2_uart_data; + int err; + + // LOG_DBG("ps2_uart_write_byte_blocking called with byte=0x%x", byte); + + err = ps2_uart_write_byte_start(byte); + if (err) { + LOG_ERR("Could not initiate writing of byte."); + return PS2_UART_E_WRITE_TRANSMIT; + } + + // The async `write_byte_start` function takes the only available semaphor. + // This causes the `k_sem_take` call below to block until + // `ps2_uart_write_finish` gives it back. + err = k_sem_take(&data->write_lock, PS2_UART_TIMEOUT_WRITE_BLOCKING); + if (err) { + + // This usually means the controller is busy with other interrupts, + // timed out processing the interrupts and even the scl timeout + // delayable wasn't called due to the delay. + // + // So we abort the write and try again. + LOG_ERR("Blocking write failed due to semaphore timeout for byte " + "0x%x: %d", + byte, err); + + return PS2_UART_E_WRITE_SEM_TIMEOUT; + } + + if (data->cur_write_status == PS2_UART_WRITE_STATUS_SUCCESS) { + // LOG_DBG("Blocking write finished successfully for byte 0x%x", byte); + err = 0; + } else { + LOG_ERR("Blocking write finished with failure for byte 0x%x status: %d", byte, + data->cur_write_status); + err = -data->cur_write_status; + } + + data->cur_write_status = PS2_UART_WRITE_STATUS_INACTIVE; + + return err; +} + +int ps2_uart_write_byte_start(uint8_t byte) { + struct ps2_uart_data *data = &ps2_uart_data; + int err; + + // Take semaphore so that when `ps2_uart_write_byte_blocking` attempts + // taking it, the process gets blocked. + err = k_sem_take(&data->write_lock, K_NO_WAIT); + if (err != 0 && err != -EBUSY) { + LOG_ERR("ps2_uart_write_byte_start could not take semaphore: %d", err); + + return err; + } + + err = ps2_uart_set_mode_write(); + if (err != 0) { + LOG_ERR("Could not configure driver for write mode: %d", err); + return err; + } + + // Set the write byte so it can be used in + // the downstream write function that is called + // from the SCL interrupt + data->cur_write_byte = byte; + data->cur_write_pos = PS2_UART_POS_START; + + // Inhibit the line by setting clock low and data high for 100us + ps2_uart_set_scl(0); + ps2_uart_set_sda(1); + k_busy_wait(PS2_UART_TIMING_SCL_INHIBITION); + + // Set data to value of start bit + ps2_uart_set_sda(0); + k_busy_wait(PS2_UART_TIMING_SCL_INHIBITION); + + // The start bit was sent by setting sda to low + // So the next scl interrupt will be for the first + // data bit. + data->cur_write_pos += 1; + + // Release the clock line and configure it as input + // This let's the device take control of the clock again + ps2_uart_set_scl(1); + ps2_uart_configure_pin_scl_input(); + + // We need to wait for the first SCL clock + // Execution continues once it arrives in + // `ps2_uart_write_scl_interrupt_handler` + ps2_uart_set_scl_callback_enabled(true); + + // And if the PS/2 device doesn't start the clock, we want to + // handle that error... + k_work_schedule_for_queue(&ps2_uart_work_queue, &data->write_scl_timout, + PS2_UART_TIMEOUT_WRITE_SCL_START); + + k_mutex_unlock(&ps2_uart_write_mutex); + + return 0; +} + +void ps2_uart_write_scl_timeout(struct k_work *item) { + // Once we start a transmission we expect the device to + // to send a new clock/interrupt within + // PS2_UART_TIMEOUT_WRITE_SCL_START us. + // If we don't receive the next interrupt within that timeframe, + // we abort the write. + + ps2_uart_write_finish(false, "scl timeout"); +} + +// The nrf52 is too slow to process all SCL interrupts, so we +// try to avoid them as much as possible. +// +// But, after we initiate the write transmission with SCL and SDA LOW, +// the PS/2 device doesn't always respond right away. It can take as +// much as 5,000us for it to start sending the clock for the +// transmission. +// +// Once it does start sending the clock the cycles are pretty +// consistently between 67 and 70us (at least on the trackpoints I +// tested). +// +// So, we use a GPIO interrupt to wait for the first clock cycle and +// then use delays to send the actual data at the same rate as the +// UART baud rate. +void ps2_uart_write_scl_interrupt_handler_blocking(const struct device *dev, + struct gpio_callback *cb, uint32_t pins) { + struct ps2_uart_data *data = &ps2_uart_data; + + LOG_INF("Inside ps2_uart_write_scl_interrupt_handler_blocking"); + + // Cancel the SCL timeout + k_work_cancel_delayable(&data->write_scl_timout); + + // Disable the SCL interrupt again. + // From here we will just use time delays. + ps2_uart_set_scl_callback_enabled(false); + + for (int i = PS2_UART_POS_DATA_FIRST; i <= PS2_UART_POS_STOP; i++) { + + if (i >= PS2_UART_POS_DATA_FIRST && i <= PS2_UART_POS_DATA_LAST) { + + int data_pos = i - PS2_UART_POS_DATA_FIRST; + bool data_bit = PS2_UART_GET_BIT(data->cur_write_byte, data_pos); + + ps2_uart_set_sda(data_bit); + } else if (i == PS2_UART_POS_PARITY) { + + bool byte_parity = ps2_uart_get_byte_parity(data->cur_write_byte); + + ps2_uart_set_sda(byte_parity); + } else if (i == PS2_UART_POS_STOP) { + + ps2_uart_set_sda(1); + + // Give control over data pin back to device after sending + // the stop bit so that we can receive the ack bit from the + // device + ps2_uart_configure_pin_sda_input(); + } else { + LOG_ERR("UART unknown TX bit number: %d", i); + } + + // Sleep for the cycle length + k_busy_wait(PS2_UART_TIMING_SCL_CYCLE_LEN); + } + + // Check Ack + int ack_val = ps2_uart_get_sda(); + + if (ack_val == 0) { + ps2_uart_write_finish(true, "successful ack"); + } else { + // TODO: Properly handle write ack errors + LOG_WRN("Ack bit was invalid for write of 0x%x", data->cur_write_byte); + ps2_uart_write_finish(true, "failed ack"); + } +} + +void ps2_uart_write_scl_interrupt_handler_async(const struct device *dev, struct gpio_callback *cb, + uint32_t pins) { + struct ps2_uart_data *data = &ps2_uart_data; + + k_work_cancel_delayable(&data->write_scl_timout); + + if (data->cur_write_pos == PS2_UART_POS_START) { + // This should not be happening, because the PS2_UART_POS_START bit + // is sent in ps2_uart_write_byte_start during inhibition + return; + } else if (data->cur_write_pos >= PS2_UART_POS_DATA_FIRST && + data->cur_write_pos <= PS2_UART_POS_DATA_LAST) { + + int data_pos = data->cur_write_pos - PS2_UART_POS_DATA_FIRST; + bool data_bit = PS2_UART_GET_BIT(data->cur_write_byte, data_pos); + + ps2_uart_set_sda(data_bit); + } else if (data->cur_write_pos == PS2_UART_POS_PARITY) { + + bool byte_parity = ps2_uart_get_byte_parity(data->cur_write_byte); + + ps2_uart_set_sda(byte_parity); + } else if (data->cur_write_pos == PS2_UART_POS_STOP) { + + ps2_uart_set_sda(1); + + // Give control over data pin back to device after sending + // the stop bit so that we can receive the ack bit from the + // device + ps2_uart_configure_pin_sda_input(); + } else if (data->cur_write_pos == PS2_UART_POS_ACK) { + + int ack_val = ps2_uart_get_sda(); + + if (ack_val == 0) { + ps2_uart_write_finish(true, "successful ack"); + } else { + // TODO: Properly handle write ack errors + LOG_WRN("Ack bit was invalid for write of 0x%x", data->cur_write_byte); + ps2_uart_write_finish(true, "failed ack"); + } + } else { + LOG_ERR("UART unknown TX bit number: %d", data->cur_write_pos); + } + + if (data->cur_write_pos < PS2_UART_POS_ACK) { + k_work_schedule_for_queue(&ps2_uart_work_queue, &data->write_scl_timout, + PS2_UART_TIMEOUT_WRITE_SCL); + } + + data->cur_write_pos += 1; +} + +void ps2_uart_write_finish(bool successful, char *descr) { + struct ps2_uart_data *data = &ps2_uart_data; + int err; + + k_work_cancel_delayable(&data->write_scl_timout); + + if (successful) { + LOG_DBG("Successfully wrote value 0x%x", data->cur_write_byte); + data->cur_write_status = PS2_UART_WRITE_STATUS_SUCCESS; + } else { // Failure + LOG_ERR("Failed to write value 0x%x: %s", data->cur_write_byte, descr); + + data->cur_write_status = PS2_UART_WRITE_STATUS_FAILURE; + } + + err = ps2_uart_set_mode_read(); + if (err != 0) { + LOG_ERR("Could not configure driver for read mode: %d", err); + return; + } + + LOG_DBG("END WRITE: 0x%x\n", data->cur_write_byte); + + data->cur_write_byte = 0x0; + + // Give the semaphore to allow write_byte_blocking to continue + k_sem_give(&data->write_lock); + + k_mutex_unlock(&ps2_uart_write_mutex); +} + +/* + * Zephyr PS/2 driver interface + */ +static int ps2_uart_enable_callback(const struct device *dev); + +#if IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) + +static int ps2_uart_configure(const struct device *dev, ps2_callback_t callback_isr, + ps2_resend_callback_t resend_callback_isr) { + struct ps2_uart_data *data = dev->data; + + if (!callback_isr && !resend_callback_isr) { + return -EINVAL; + } + + if (callback_isr) { + data->callback_isr = callback_isr; + ps2_uart_enable_callback(dev); + } + + if (resend_callback_isr) { + data->resend_callback_isr = resend_callback_isr; + } + + return 0; +} + +#else + +static int ps2_uart_configure(const struct device *dev, ps2_callback_t callback_isr) { + struct ps2_uart_data *data = dev->data; + + if (!callback_isr) { + return -EINVAL; + } + + data->callback_isr = callback_isr; + ps2_uart_enable_callback(dev); + + return 0; +} + +#endif /* IS_ENABLED(CONFIG_PS2_UART_ENABLE_PS2_RESEND_CALLBACK) */ + +int ps2_uart_read(const struct device *dev, uint8_t *value) { + uint8_t queue_byte; + int err = ps2_uart_data_queue_get_next(&queue_byte, PS2_UART_TIMEOUT_READ); + if (err) { // Timeout due to no data to read in data queue + // LOG_DBG("ps2_uart_read: Fifo timed out..."); + + return -ETIMEDOUT; + } + + // LOG_DBG("ps2_uart_read: Returning 0x%x", queue_byte); + *value = queue_byte; + + return 0; +} + +static int ps2_uart_write(const struct device *dev, uint8_t value) { + int ret = ps2_uart_write_byte(value); + + return ret; +} + +static int ps2_uart_disable_callback(const struct device *dev) { + struct ps2_uart_data *data = dev->data; + + // Make sure there are no stale items in the data queue + // from before the callback was disabled. + ps2_uart_data_queue_empty(); + + data->callback_enabled = false; + + // LOG_DBG("Disabled PS2 callback."); + + return 0; +} + +static int ps2_uart_enable_callback(const struct device *dev) { + struct ps2_uart_data *data = dev->data; + data->callback_enabled = true; + + // LOG_DBG("Enabled PS2 callback."); + + ps2_uart_data_queue_empty(); + + return 0; +} + +static const struct ps2_driver_api ps2_uart_driver_api = { + .config = ps2_uart_configure, + .read = ps2_uart_read, + .write = ps2_uart_write, + .disable_callback = ps2_uart_disable_callback, + .enable_callback = ps2_uart_enable_callback, +}; + +/* + * PS/2 UART Driver Init + */ +static int ps2_uart_init_uart(void); +static int ps2_uart_init_gpio(void); + +static int ps2_uart_init(const struct device *dev) { + int err; + struct ps2_uart_data *data = dev->data; + + // Set the ps2 device so we can retrieve it later for + // the ps2 callback + data->dev = dev; + + LOG_INF("Inside ps2_uart_init"); + + // Init data queue for synchronous read operations + k_msgq_init(&data->data_queue, data->data_queue_buffer, sizeof(struct ps2_uart_data_queue_item), + PS2_UART_DATA_QUEUE_SIZE); + + // Custom queue for background PS/2 processing work at high priority + k_work_queue_start(&ps2_uart_work_queue, ps2_uart_work_queue_stack_area, + K_THREAD_STACK_SIZEOF(ps2_uart_work_queue_stack_area), + PS2_UART_WORK_QUEUE_PRIORITY, NULL); + + // Custom queue for calling the zephyr ps/2 callback at lower priority + k_work_queue_start(&ps2_uart_work_queue_cb, ps2_uart_work_queue_cb_stack_area, + K_THREAD_STACK_SIZEOF(ps2_uart_work_queue_cb_stack_area), + PS2_UART_WORK_QUEUE_CB_PRIORITY, NULL); + + k_work_init(&data->callback_work, ps2_uart_read_callback_work_handler); + + k_work_init_delayable(&data->write_scl_timout, ps2_uart_write_scl_timeout); + + // Init semaphore for blocking writes + k_sem_init(&data->write_lock, 0, 1); + + // Init semaphore that waits for read after write + k_sem_init(&data->write_awaits_resp_sem, 0, 1); + + err = ps2_uart_init_uart(); + if (err != 0) { + LOG_ERR("Could not init UART: %d", err); + return err; + } + + err = ps2_uart_init_gpio(); + if (err != 0) { + LOG_ERR("Could not init GPIO: %d", err); + return err; + } + + err = ps2_uart_set_mode_read(); + if (err != 0) { + LOG_ERR("Could not initialize in UART mode read: %d", err); + return err; + } + + return 0; +} + +static int ps2_uart_init_uart(void) { + struct ps2_uart_data *data = &ps2_uart_data; + struct ps2_uart_config *config = (struct ps2_uart_config *)&ps2_uart_config; + int err; + + if (!device_is_ready(config->uart_dev)) { + LOG_ERR("UART device not ready"); + return -ENODEV; + } else { + LOG_INF("UART device is ready"); + } + + struct uart_config uart_cfg; + err = uart_config_get(config->uart_dev, &uart_cfg); + if (err != 0) { + LOG_ERR("Could not retrieve UART config..."); + return -ENODEV; + } + + uart_cfg.data_bits = UART_CFG_DATA_BITS_8; + uart_cfg.stop_bits = UART_CFG_STOP_BITS_1; + uart_cfg.flow_ctrl = UART_CFG_FLOW_CTRL_NONE; + + // PS/2 uses odd parity, but nrf52840 doesn't support + // odd parity. Despite that, setting none works. + uart_cfg.parity = UART_CFG_PARITY_EVEN; + + err = uart_configure(config->uart_dev, &uart_cfg); + if (err != 0) { + LOG_ERR("Could not configure UART device: %d", err); + return -EINVAL; + } + + uart_irq_callback_user_data_set(config->uart_dev, ps2_uart_interrupt_handler, + (void *)data->dev); + + uart_irq_rx_enable(config->uart_dev); + uart_irq_err_enable(config->uart_dev); + + return 0; +} + +static int ps2_uart_init_gpio(void) { + struct ps2_uart_data *data = &ps2_uart_data; + struct ps2_uart_config *config = (struct ps2_uart_config *)&ps2_uart_config; + int err; + + // Interrupt for clock line +#if IS_ENABLED(CONFIG_PS2_UART_WRITE_MODE_BLOCKING) + gpio_init_callback(&data->scl_cb_data, ps2_uart_write_scl_interrupt_handler_blocking, + BIT(config->scl_gpio.pin)); +#else + gpio_init_callback(&data->scl_cb_data, ps2_uart_write_scl_interrupt_handler_async, + BIT(config->scl_gpio.pin)); +#endif /* IS_ENABLED(CONFIG_PS2_UART_WRITE_MODE_BLOCKING) */ + + err = gpio_add_callback(config->scl_gpio.port, &data->scl_cb_data); + if (err) { + LOG_ERR("failed to enable interrupt callback on " + "SCL GPIO pin (err %d)", + err); + } + + LOG_INF("Disabling callback..."); + ps2_uart_set_scl_callback_enabled(false); + + return err; +} + +DEVICE_DT_INST_DEFINE(0, &ps2_uart_init, NULL, &ps2_uart_data, &ps2_uart_config, POST_KERNEL, 80, + &ps2_uart_driver_api); diff --git a/app/module/dts/bindings/input/zmk,input-mouse-ps2.yaml b/app/module/dts/bindings/input/zmk,input-mouse-ps2.yaml new file mode 100644 index 00000000000..4f07cd46c83 --- /dev/null +++ b/app/module/dts/bindings/input/zmk,input-mouse-ps2.yaml @@ -0,0 +1,72 @@ +description: PS2 mouse configuration + +compatible: "zmk,input-mouse-ps2" + +properties: + ps2-device: + type: phandle + required: true + description: | + The ps2 device the mouse should use. + + rst-gpios: + type: phandle-array + required: false + description: GPIO to which the RST pin of the device is connected and on which the Power-On-Reset will be performed. + + sampling-rate: + type: int + description: Adjusts the sensitivity setting on trackpoint devices. The allowed values are 10, 20, 40, 60, 80, 100, 200 and the default is 100. + enum: + - 10 + - 20 + - 40 + - 60 + - 80 + - 100 + - 200 + + scroll-mode: + type: boolean + description: Enables scroll wheel on mouse devices supporting the Intellimouse extension. + + disable-clicking: + type: boolean + description: Disables clicking. Useful when using a PS2 driver that is prone to erros in transmissions to reduce accidental clicks. + + tp-press-to-select: + type: boolean + description: Enables mouse clicking by pressing hard on the trackpoint. Not all trackpoints support this feature. + + tp-press-to-select-threshold: + type: int + description: Adjusts how sensitive the press-to-select feature is. The range is 0 - 255 and the default is 8. You can also adjust this setting at runtime without recompiling using the &mms behavior. If this setting is enabled, then the runtime behavior won't be saved into the runtime settings. + + tp-sensitivity: + type: int + description: Adjusts the sensitivity setting on trackpoint devices. The range is 0 - 255 and the default is 128. You can also adjust this setting at runtime without recompiling using the &mms behavior. If this setting is enabled, then the runtime behavior won't be saved into the runtime settings. + + tp-neg-inertia: + type: int + description: Adjusts the negative inertia setting on trackpoint devices. The range is 0 - 255 and the default is 6. You can also adjust this setting at runtime without recompiling using the &mms behavior. If this setting is enabled, then the runtime behavior won't be saved into the runtime settings. + + tp-val6-upper-speed: + type: int + description: Adjusts the negative value0 upper plateau speed setting on trackpoint devices. The range is 0 - 255 and the default is 97. You can also adjust this setting at runtime without recompiling using the &mms behavior. If this setting is enabled, then the runtime behavior won't be saved into the runtime settings. + + tp-x-invert: + type: boolean + description: Enables x axis inversion on the trackpoint. Only works with some trackpoint devices, but is properly considered by the trackpoint's acceleration features (which may or may not have an impact). Alternatively consider achieving the same effect using the `zmk,input-configs` feature. + + tp-y-invert: + type: boolean + description: Enables y axis inversion on the trackpoint. Only works with some trackpoint devices, but is properly considered by the trackpoint's acceleration features (which may or may not have an impact). Alternatively consider achieving the same effect using the `zmk,input-configs` feature. + + tp-xy-swap: + type: boolean + description: Swaps the x and y axis on the trackpoint. Only works with some trackpoint devices, but may properly considered by the trackpoint's acceleration features (which may or may not have an impact). Alternatively consider achieving the same effect using the `zmk,input-configs` feature. + + layer-toggle: + type: int + required: false + description: The layer that should be toggled when the mouse is moved. diff --git a/app/module/dts/bindings/ps2/gpio-ps2.yaml b/app/module/dts/bindings/ps2/gpio-ps2.yaml new file mode 100644 index 00000000000..f6181806dd4 --- /dev/null +++ b/app/module/dts/bindings/ps2/gpio-ps2.yaml @@ -0,0 +1,19 @@ +# Copyright (c) 2019, Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +description: GPIO PS2 + +compatible: "gpio-ps2" + +include: base.yaml + +properties: + scl-gpios: + type: phandle-array + required: true + description: GPIO to which the SCL pin of the I2C bus is connected. + sda-gpios: + type: phandle-array + required: true + description: GPIO to which the SDA pin of the I2C bus is connected. + diff --git a/app/module/dts/bindings/ps2/uart-ps2.yaml b/app/module/dts/bindings/ps2/uart-ps2.yaml new file mode 100644 index 00000000000..b976460502a --- /dev/null +++ b/app/module/dts/bindings/ps2/uart-ps2.yaml @@ -0,0 +1,18 @@ +# Copyright (c) 2019, Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +description: GPIO PS2 + +compatible: "uart-ps2" + +include: base.yaml + +properties: + scl-gpios: + type: phandle-array + required: true + description: GPIO to which the SCL pin is connected. + sda-gpios: + type: phandle-array + required: true + description: GPIO to which the SDA pin is connected. diff --git a/app/module/include/zmk/input_mouse_ps2.h b/app/module/include/zmk/input_mouse_ps2.h new file mode 100644 index 00000000000..36e1a85c7ba --- /dev/null +++ b/app/module/include/zmk/input_mouse_ps2.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +int zmk_mouse_ps2_settings_log(); +int zmk_mouse_ps2_settings_reset(); + +int zmk_mouse_ps2_tp_sensitivity_change(int amount); +int zmk_mouse_ps2_tp_neg_inertia_change(int amount); +int zmk_mouse_ps2_tp_value6_upper_plateau_speed_change(int amount); +int zmk_mouse_ps2_tp_pts_threshold_change(int amount); diff --git a/app/scripts/gen_interrupt_priority_overrides.py b/app/scripts/gen_interrupt_priority_overrides.py new file mode 100755 index 00000000000..6271e77a488 --- /dev/null +++ b/app/scripts/gen_interrupt_priority_overrides.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +""" +Script for generating interrupt adjustments for zephyr dts files. + +Requirements: + - pip install devicetree +""" + +import argparse +from devicetree import dtlib + + +def get_interrupt_nodes(dt): + interrupt_nodes = [] + for node in dt.node_iter(): + if 'interrupts' in node.props.keys(): + interrupt_nodes.append(node) + + return interrupt_nodes + +def get_node_label(node): + label = node.labels[0] + return label + +def gen_interrupt_adjustment_str(node, priority_incr): + label = get_node_label(node) + interrupt_val1, interrupt_priority = node.props['interrupts'].to_nums() + + if label == 'gpiote': + new_priority = 0 + else: + new_priority = interrupt_priority + priority_incr + + adj_str = f''' +&{label} {{ + interrupts = < {interrupt_val1} {new_priority} >; +}}; + ''' + return adj_str + + +def gen_interrupt_adjustment(dts_path, priority_incr): + dt = dtlib.DT(dts_path) + interrupt_nodes = get_interrupt_nodes(dt) + + adjustments = '' + for node in interrupt_nodes: + adj_str = gen_interrupt_adjustment_str(node, priority_incr) + if get_node_label(node) == 'gpiote': + adjustments = adj_str + adjustments + else: + adjustments += adj_str + + return adjustments + + +def main(): + parser = argparse.ArgumentParser(description='Generate interrupt adjustments for zephyr.dts files.') + parser.add_argument('dts_path', metavar='DTS_PATH', type=str, help='Path to the zephyr.dts file') + parser.add_argument('-p', '--priority-incr', type=int, default=2, help='By how much to increase the priority (default: 2)') + args = parser.parse_args() + + adj = gen_interrupt_adjustment(args.dts_path, args.priority_incr) + print(adj) + + +if __name__ == '__main__': + main() 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..eaf84a766af --- /dev/null +++ b/app/src/behaviors/behavior_input_two_axis.c @@ -0,0 +1,265 @@ +/* + * 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 + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct vector2d { + float x; + float y; +}; + +struct movement_state_1d { + float remainder; + int16_t speed; + uint64_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 ms_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; +} + +static float speed(const struct behavior_input_two_axis_config *config, float max_speed, + int64_t duration_ms) { + // Calculate the speed based on MouseKeysAccel + // See https://en.wikipedia.org/wiki/Mouse_keys + if (duration_ms == 0) { + return 0; + } + + if (duration_ms > config->time_to_max_speed_ms || config->time_to_max_speed_ms == 0 || + config->acceleration_exponent == 0) { + return max_speed; + } + float time_fraction = (float)duration_ms / config->time_to_max_speed_ms; + return max_speed * powf(time_fraction, config->acceleration_exponent); +} + +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, + struct movement_state_1d *state, int64_t now) { + float move = 0; + if (state->speed == 0) { + state->remainder = 0; + return move; + } + + int64_t move_duration = ms_since_start(state->start_time, now, config->delay_ms); + move = (move_duration > 0) + ? (speed(config, 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, &state->x, now), + .y = update_movement_1d(config, &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_get(); + + 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_get(); + } 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); + } +} + +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..8f8df2a72f3 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_MOUSE_HID_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/behaviors/behavior_mouse_setting.c b/app/src/behaviors/behavior_mouse_setting.c new file mode 100644 index 00000000000..63d86ffcd74 --- /dev/null +++ b/app/src/behaviors/behavior_mouse_setting.c @@ -0,0 +1,62 @@ +#define DT_DRV_COMPAT zmk_behavior_mouse_setting + +#include +#include +#include + +#include +#include + +#define INCREMENT_TP_SENSITIVITY 10 +#define INCREMENT_TP_NEG_INERTIA 1 +#define INCREMENT_TP_VALUE6 5 +#define INCREMENT_TP_PTS_THRESHOLD 1 + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + switch (binding->param1) { + + case MS_LOG: + return zmk_mouse_ps2_settings_log(); + case MS_RESET: + return zmk_mouse_ps2_settings_reset(); + case MS_TP_SENSITIVITY_INCR: + return zmk_mouse_ps2_tp_sensitivity_change(INCREMENT_TP_SENSITIVITY); + case MS_TP_SENSITIVITY_DECR: + return zmk_mouse_ps2_tp_sensitivity_change(-INCREMENT_TP_SENSITIVITY); + + case MS_TP_NEG_INERTIA_INCR: + return zmk_mouse_ps2_tp_neg_inertia_change(INCREMENT_TP_NEG_INERTIA); + case MS_TP_NEG_INERTIA_DECR: + return zmk_mouse_ps2_tp_neg_inertia_change(-INCREMENT_TP_NEG_INERTIA); + + case MS_TP_VALUE6_INCR: + return zmk_mouse_ps2_tp_value6_upper_plateau_speed_change(INCREMENT_TP_VALUE6); + case MS_TP_VALUE6_DECR: + return zmk_mouse_ps2_tp_value6_upper_plateau_speed_change(-INCREMENT_TP_VALUE6); + + case MS_TP_PTS_THRESHOLD_INCR: + return zmk_mouse_ps2_tp_pts_threshold_change(INCREMENT_TP_PTS_THRESHOLD); + case MS_TP_PTS_THRESHOLD_DECR: + return zmk_mouse_ps2_tp_pts_threshold_change(-INCREMENT_TP_PTS_THRESHOLD); + } + + return -ENOTSUP; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return ZMK_BEHAVIOR_OPAQUE; +} + +// Initialization Function +static int zmk_behavior_mouse_setting_init(const struct device *dev) { return 0; }; + +static const struct behavior_driver_api zmk_behavior_mouse_setting_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +BEHAVIOR_DT_INST_DEFINE(0, zmk_behavior_mouse_setting_init, NULL, NULL, NULL, POST_KERNEL, + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, + &zmk_behavior_mouse_setting_driver_api); diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 7c9d15a31fe..1e09eaf5642 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -15,6 +15,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -208,7 +211,7 @@ int zmk_endpoints_send_mouse_report() { switch (current_instance.transport) { case ZMK_TRANSPORT_USB: { #if IS_ENABLED(CONFIG_ZMK_USB) - int err = zmk_usb_hid_send_mouse_report(); + int err = zmk_mouse_usb_hid_send_mouse_report(); if (err) { LOG_ERR("FAILED TO SEND OVER USB: %d", err); } @@ -221,8 +224,8 @@ int zmk_endpoints_send_mouse_report() { case ZMK_TRANSPORT_BLE: { #if IS_ENABLED(CONFIG_ZMK_BLE) - struct zmk_hid_mouse_report *mouse_report = zmk_hid_get_mouse_report(); - int err = zmk_hog_send_mouse_report(&mouse_report->body); + struct zmk_hid_mouse_report *mouse_report = zmk_mouse_hid_get_mouse_report(); + int err = zmk_mouse_hog_send_mouse_report(&mouse_report->body); if (err) { LOG_ERR("FAILED TO SEND OVER HOG: %d", err); } diff --git a/app/src/hid.c b/app/src/hid.c index 8b0c23f37ed..ae2a71b16e4 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -25,13 +25,6 @@ static uint8_t keys_held = 0; #endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - -static struct zmk_hid_mouse_report mouse_report = {.report_id = ZMK_HID_REPORT_ID_MOUSE, - .body = {.buttons = 0}}; - -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - // Keep track of how often a modifier was pressed. // Only release the modifier if the count is 0. static int explicit_modifier_counts[8] = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -369,71 +362,6 @@ bool zmk_hid_is_pressed(uint32_t usage) { return false; } -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - -// Keep track of how often a button was pressed. -// Only release the button if the count is 0. -static int explicit_button_counts[5] = {0, 0, 0, 0, 0}; -static zmk_mod_flags_t explicit_buttons = 0; - -#define SET_MOUSE_BUTTONS(btns) \ - { \ - mouse_report.body.buttons = btns; \ - LOG_DBG("Mouse buttons set to 0x%02X", mouse_report.body.buttons); \ - } - -int zmk_hid_mouse_button_press(zmk_mouse_button_t button) { - if (button >= ZMK_HID_MOUSE_NUM_BUTTONS) { - return -EINVAL; - } - - explicit_button_counts[button]++; - LOG_DBG("Button %d count %d", button, explicit_button_counts[button]); - WRITE_BIT(explicit_buttons, button, true); - SET_MOUSE_BUTTONS(explicit_buttons); - return 0; -} - -int zmk_hid_mouse_button_release(zmk_mouse_button_t button) { - if (button >= ZMK_HID_MOUSE_NUM_BUTTONS) { - return -EINVAL; - } - - if (explicit_button_counts[button] <= 0) { - LOG_ERR("Tried to release button %d too often", button); - return -EINVAL; - } - explicit_button_counts[button]--; - LOG_DBG("Button %d count: %d", button, explicit_button_counts[button]); - if (explicit_button_counts[button] == 0) { - LOG_DBG("Button %d released", button); - WRITE_BIT(explicit_buttons, button, false); - } - SET_MOUSE_BUTTONS(explicit_buttons); - return 0; -} - -int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons) { - for (zmk_mouse_button_t i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { - if (buttons & BIT(i)) { - zmk_hid_mouse_button_press(i); - } - } - return 0; -} - -int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons) { - for (zmk_mouse_button_t i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) { - if (buttons & BIT(i)) { - zmk_hid_mouse_button_release(i); - } - } - return 0; -} -void zmk_hid_mouse_clear(void) { 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; } @@ -441,11 +369,3 @@ struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(void) { 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 f17f759c908..65679586e09 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -69,15 +69,6 @@ static struct hids_report consumer_input = { .type = HIDS_INPUT, }; -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - -static struct hids_report mouse_input = { - .id = ZMK_HID_REPORT_ID_MOUSE, - .type = HIDS_INPUT, -}; - -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - static bool host_requests_notification = false; static uint8_t ctrl_point; // static uint8_t proto_mode; @@ -143,15 +134,6 @@ static ssize_t read_hids_consumer_input_report(struct bt_conn *conn, sizeof(struct zmk_hid_consumer_report_body)); } -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -static ssize_t read_hids_mouse_input_report(struct bt_conn *conn, const struct bt_gatt_attr *attr, - void *buf, uint16_t len, uint16_t offset) { - struct zmk_hid_mouse_report_body *report_body = &zmk_hid_get_mouse_report()->body; - return bt_gatt_attr_read(conn, attr, buf, len, offset, report_body, - sizeof(struct zmk_hid_mouse_report_body)); -} -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - // static ssize_t write_proto_mode(struct bt_conn *conn, // const struct bt_gatt_attr *attr, // const void *buf, uint16_t len, uint16_t offset, @@ -200,14 +182,6 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, NULL, &consumer_input), -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, - BT_GATT_PERM_READ_ENCRYPT, read_hids_mouse_input_report, NULL, NULL), - BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), - BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, - NULL, &mouse_input), -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - #if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP, @@ -220,7 +194,7 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT, BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE, NULL, write_ctrl_point, &ctrl_point)); -struct bt_conn *destination_connection(void) { +static struct bt_conn *destination_connection(void) { struct bt_conn *conn; bt_addr_le_t *addr = zmk_ble_active_profile_addr(); LOG_DBG("Address pointer %p", addr); @@ -237,7 +211,7 @@ struct bt_conn *destination_connection(void) { K_THREAD_STACK_DEFINE(hog_q_stack, CONFIG_ZMK_BLE_THREAD_STACK_SIZE); -struct k_work_q hog_work_q; +static struct k_work_q hog_work_q; K_MSGQ_DEFINE(zmk_hog_keyboard_msgq, sizeof(struct zmk_hid_keyboard_report_body), CONFIG_ZMK_BLE_KEYBOARD_REPORT_QUEUE_SIZE, 4); @@ -343,61 +317,6 @@ int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { return 0; }; -#if IS_ENABLED(CONFIG_ZMK_MOUSE) - -K_MSGQ_DEFINE(zmk_hog_mouse_msgq, sizeof(struct zmk_hid_mouse_report_body), - CONFIG_ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE, 4); - -void send_mouse_report_callback(struct k_work *work) { - struct zmk_hid_mouse_report_body report; - while (k_msgq_get(&zmk_hog_mouse_msgq, &report, K_NO_WAIT) == 0) { - struct bt_conn *conn = destination_connection(); - if (conn == NULL) { - return; - } - - struct bt_gatt_notify_params notify_params = { - .attr = &hog_svc.attrs[13], - .data = &report, - .len = sizeof(report), - }; - - int err = bt_gatt_notify_cb(conn, ¬ify_params); - if (err == -EPERM) { - bt_conn_set_security(conn, BT_SECURITY_L2); - } else if (err) { - LOG_DBG("Error notifying %d", err); - } - - bt_conn_unref(conn); - } -}; - -K_WORK_DEFINE(hog_mouse_work, send_mouse_report_callback); - -int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *report) { - int err = k_msgq_put(&zmk_hog_mouse_msgq, report, K_MSEC(100)); - if (err) { - switch (err) { - case -EAGAIN: { - LOG_WRN("Consumer message queue full, popping first message and queueing again"); - struct zmk_hid_mouse_report_body discarded_report; - k_msgq_get(&zmk_hog_mouse_msgq, &discarded_report, K_NO_WAIT); - return zmk_hog_send_mouse_report(report); - } - default: - LOG_WRN("Failed to queue mouse report to send (%d)", err); - return err; - } - } - - k_work_submit_to_queue(&hog_work_q, &hog_mouse_work); - - return 0; -}; - -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - static int zmk_hog_init(void) { static const struct k_work_queue_config queue_config = {.name = "HID Over GATT Send Work"}; k_work_queue_start(&hog_work_q, hog_q_stack, K_THREAD_STACK_SIZEOF(hog_q_stack), diff --git a/app/src/mouse.c b/app/src/mouse.c 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..405c10866a2 --- /dev/null +++ b/app/src/mouse/CMakeLists.txt @@ -0,0 +1,6 @@ +if(CONFIG_ZMK_MOUSE) + target_sources(app PRIVATE hid.c) + + target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE usb_hid.c) + target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE hog.c) +endif() \ No newline at end of file diff --git a/app/src/mouse/Kconfig b/app/src/mouse/Kconfig new file mode 100644 index 00000000000..d2f9d525995 --- /dev/null +++ b/app/src/mouse/Kconfig @@ -0,0 +1,19 @@ +# Copyright (c) 2023 The ZMK Contributors +# SPDX-License-Identifier: MIT + +menuconfig ZMK_MOUSE + bool "Mouse Emulation" + select INPUT + select INPUT_THREAD_PRIORITY_OVERRIDE + select USB_COMPOSITE_DEVICE if ZMK_USB + +if ZMK_MOUSE + +if ZMK_USB + +config USB_HID_DEVICE_COUNT + default 2 + +endif + +endif # ZMK_MOUSE \ No newline at end of file diff --git a/app/src/mouse/hid.c b/app/src/mouse/hid.c new file mode 100644 index 00000000000..24faf779b34 --- /dev/null +++ b/app/src/mouse/hid.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +static struct zmk_hid_mouse_report mouse_report = { + .report_id = ZMK_MOUSE_HID_REPORT_ID_MOUSE, + .body = {.buttons = 0, .d_x = 0, .d_y = 0, .d_scroll_y = 0}}; + +// Keep track of how often a button was pressed. +// Only release the button if the count is 0. +static int explicit_button_counts[5] = {0, 0, 0, 0, 0}; +static zmk_mod_flags_t explicit_buttons = 0; + +#define SET_MOUSE_BUTTONS(btns) \ + { \ + mouse_report.body.buttons = btns; \ + LOG_DBG("Mouse buttons set to 0x%02X", mouse_report.body.buttons); \ + } + +int zmk_hid_mouse_button_press(zmk_mouse_button_t button) { + if (button >= ZMK_MOUSE_HID_NUM_BUTTONS) { + return -EINVAL; + } + + explicit_button_counts[button]++; + LOG_DBG("Button %d count %d", button, explicit_button_counts[button]); + WRITE_BIT(explicit_buttons, button, true); + SET_MOUSE_BUTTONS(explicit_buttons); + return 0; +} + +int zmk_hid_mouse_button_release(zmk_mouse_button_t button) { + if (button >= ZMK_MOUSE_HID_NUM_BUTTONS) { + return -EINVAL; + } + + if (explicit_button_counts[button] <= 0) { + LOG_ERR("Tried to release button %d too often", button); + return -EINVAL; + } + explicit_button_counts[button]--; + LOG_DBG("Button %d count: %d", button, explicit_button_counts[button]); + if (explicit_button_counts[button] == 0) { + LOG_DBG("Button %d released", button); + WRITE_BIT(explicit_buttons, button, false); + } + SET_MOUSE_BUTTONS(explicit_buttons); + return 0; +} + +int zmk_hid_mouse_buttons_press(zmk_mouse_button_flags_t buttons) { + for (zmk_mouse_button_t i = 0; i < ZMK_MOUSE_HID_NUM_BUTTONS; i++) { + if (buttons & BIT(i)) { + zmk_hid_mouse_button_press(i); + } + } + return 0; +} + +int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons) { + for (zmk_mouse_button_t i = 0; i < ZMK_MOUSE_HID_NUM_BUTTONS; i++) { + if (buttons & BIT(i)) { + zmk_hid_mouse_button_release(i); + } + } + return 0; +} + +void zmk_hid_mouse_movement_set(int16_t x, int16_t y) { + mouse_report.body.d_x = x; + mouse_report.body.d_y = y; + LOG_DBG("Mouse movement set to %d/%d", mouse_report.body.d_x, mouse_report.body.d_y); +} + +void zmk_hid_mouse_movement_update(int16_t x, int16_t y) { + mouse_report.body.d_x += x; + mouse_report.body.d_y += y; + LOG_DBG("Mouse movement updated to %d/%d", mouse_report.body.d_x, mouse_report.body.d_y); +} + +void zmk_hid_mouse_scroll_set(int8_t x, int8_t y) { + mouse_report.body.d_scroll_x = x; + mouse_report.body.d_scroll_y = y; + LOG_DBG("Mouse scroll set to %d/%d", mouse_report.body.d_scroll_x, + mouse_report.body.d_scroll_y); +} + +void zmk_hid_mouse_scroll_update(int8_t x, int8_t y) { + mouse_report.body.d_scroll_x += x; + mouse_report.body.d_scroll_y += y; + LOG_DBG("Mouse scroll updated to X: %d/%d", mouse_report.body.d_scroll_x, + mouse_report.body.d_scroll_y); +} + +void zmk_hid_mouse_clear(void) { + LOG_DBG("Mouse report cleared"); + memset(&mouse_report.body, 0, sizeof(mouse_report.body)); +} + +struct zmk_hid_mouse_report *zmk_mouse_hid_get_mouse_report(void) { + return &mouse_report; +} diff --git a/app/src/mouse/hog.c b/app/src/mouse/hog.c new file mode 100644 index 00000000000..5ae67a4ba96 --- /dev/null +++ b/app/src/mouse/hog.c @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include + +#include +#include +#include +#include + +enum { + HIDS_REMOTE_WAKE = BIT(0), + HIDS_NORMALLY_CONNECTABLE = BIT(1), +}; + +struct hids_info { + uint16_t version; /* version number of base USB HID Specification */ + uint8_t code; /* country HID Device hardware is localized for. */ + uint8_t flags; +} __packed; + +struct hids_report { + uint8_t id; /* report id */ + uint8_t type; /* report type */ +} __packed; + +static struct hids_info info = { + .version = 0x1101, + .code = 0x00, + .flags = HIDS_NORMALLY_CONNECTABLE | HIDS_REMOTE_WAKE, +}; + +enum { + HIDS_INPUT = 0x01, + HIDS_OUTPUT = 0x02, + HIDS_FEATURE = 0x03, +}; + +static struct hids_report mouse_input = { + .id = ZMK_MOUSE_HID_REPORT_ID_MOUSE, + .type = HIDS_INPUT, +}; + +static bool host_requests_notification = false; +static uint8_t ctrl_point; + +static ssize_t read_hids_info(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) { + return bt_gatt_attr_read(conn, attr, buf, len, offset, attr->user_data, + sizeof(struct hids_info)); +} + +static ssize_t read_hids_report_ref(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) { + return bt_gatt_attr_read(conn, attr, buf, len, offset, attr->user_data, + sizeof(struct hids_report)); +} + +static ssize_t read_hids_report_map(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) { + return bt_gatt_attr_read(conn, attr, buf, len, offset, zmk_mouse_hid_report_desc, + sizeof(zmk_mouse_hid_report_desc)); +} + +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_mouse_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 void input_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) { + LOG_DBG("Input CC changed for %d", attr->handle); + host_requests_notification = (value == BT_GATT_CCC_NOTIFY) ? 1 : 0; +} + +static ssize_t write_ctrl_point(struct bt_conn *conn, const struct bt_gatt_attr *attr, + const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { + uint8_t *value = attr->user_data; + + if (offset + len > sizeof(ctrl_point)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + memcpy(value + offset, buf, len); + + return len; +} + +/* HID Service Declaration */ +BT_GATT_SERVICE_DEFINE( + mouse_hog_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_HIDS), + + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT_MAP, BT_GATT_CHRC_READ, BT_GATT_PERM_READ_ENCRYPT, + read_hids_report_map, NULL, NULL), + + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ_ENCRYPT, read_hids_mouse_input_report, NULL, NULL), + BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), + BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, + NULL, &mouse_input), + + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_INFO, BT_GATT_CHRC_READ, BT_GATT_PERM_READ, read_hids_info, + NULL, &info), + + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT, BT_GATT_CHRC_WRITE_WITHOUT_RESP, + BT_GATT_PERM_WRITE, NULL, write_ctrl_point, &ctrl_point)); + +static struct bt_conn *destination_connection(void) { + struct bt_conn *conn; + bt_addr_le_t *addr = zmk_ble_active_profile_addr(); + + if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) { + LOG_WRN("Not sending, no active address for current profile"); + return NULL; + } else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) { + LOG_WRN("Not sending, not connected to active profile"); + return NULL; + } + + return conn; +} + +K_THREAD_STACK_DEFINE(mouse_hog_q_stack, CONFIG_ZMK_BLE_THREAD_STACK_SIZE); + +static struct k_work_q mouse_hog_work_q; + +K_MSGQ_DEFINE(zmk_hog_mouse_msgq, sizeof(struct zmk_hid_mouse_report_body), + CONFIG_ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE, 4); + +void send_mouse_report_callback(struct k_work *work) { + struct zmk_hid_mouse_report_body report; + while (k_msgq_get(&zmk_hog_mouse_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &mouse_hog_svc.attrs[3], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err == -EPERM) { + bt_conn_set_security(conn, BT_SECURITY_L2); + } else if (err) { + LOG_DBG("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +}; + +K_WORK_DEFINE(hog_mouse_work, send_mouse_report_callback); + +int zmk_mouse_hog_send_mouse_report(struct zmk_hid_mouse_report_body *report) { + int err = k_msgq_put(&zmk_hog_mouse_msgq, report, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Consumer message queue full, popping first message and queueing again"); + struct zmk_hid_mouse_report_body discarded_report; + k_msgq_get(&zmk_hog_mouse_msgq, &discarded_report, K_NO_WAIT); + return zmk_mouse_hog_send_mouse_report(report); + } + default: + LOG_WRN("Failed to queue mouse report to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&mouse_hog_work_q, &hog_mouse_work); + + return 0; +}; + +static int zmk_mouse_hog_init(void) { + static const struct k_work_queue_config queue_config = {.name = + "Mouse HID Over GATT Send Work"}; + k_work_queue_start(&mouse_hog_work_q, mouse_hog_q_stack, + K_THREAD_STACK_SIZEOF(mouse_hog_q_stack), CONFIG_ZMK_BLE_THREAD_PRIORITY, + &queue_config); + + return 0; +} + +SYS_INIT(zmk_mouse_hog_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); diff --git a/app/src/mouse/input_listener.c b/app/src/mouse/input_listener.c new file mode 100644 index 00000000000..2880b33f9bb --- /dev/null +++ b/app/src/mouse/input_listener.c @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_listener + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include + +#include +#include +#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_xy_data { + enum input_listener_xy_data_mode mode; + int16_t x; + int16_t y; +}; + +struct input_listener_data { + const struct device *dev; + + union { + struct { + struct input_listener_xy_data data; + struct input_listener_xy_data wheel_data; + + uint8_t button_set; + uint8_t button_clear; + } mouse; + }; + + bool layer_toggle_layer_enabled; + int64_t layer_toggle_last_mouse_package_time; + struct k_work_delayable layer_toggle_activation_delay; + struct k_work_delayable layer_toggle_deactivation_delay; +}; + +struct input_listener_config { + bool xy_swap; + bool x_invert; + bool y_invert; + uint16_t scale_multiplier; + uint16_t scale_divisor; + int layer_toggle; + int layer_toggle_delay_ms; + int layer_toggle_timeout_ms; +}; + +void zmk_input_listener_layer_toggle_input_rel_received(const struct input_listener_config *config, + struct input_listener_data *data); + +static char *get_input_code_name(struct input_event *evt) { + switch (evt->code) { + case INPUT_REL_X: + return "INPUT_REL_X"; + case INPUT_REL_Y: + return "INPUT_REL_Y"; + case INPUT_REL_WHEEL: + return "INPUT_REL_WHEEL"; + case INPUT_REL_HWHEEL: + return "INPUT_REL_HWHEEL"; + case INPUT_BTN_0: + return "INPUT_BTN_0"; + case INPUT_BTN_1: + return "INPUT_BTN_1"; + case INPUT_BTN_2: + return "INPUT_BTN_2"; + case INPUT_BTN_3: + return "INPUT_BTN_3"; + case INPUT_BTN_4: + return "INPUT_BTN_4"; + default: + return "UNKNOWN"; + } +} + +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 += evt->value; + break; + case INPUT_REL_Y: + data->mouse.data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.data.y += evt->value; + break; + case INPUT_REL_WHEEL: + data->mouse.wheel_data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.wheel_data.y += evt->value; + break; + case INPUT_REL_HWHEEL: + data->mouse.wheel_data.mode = INPUT_LISTENER_XY_DATA_MODE_REL; + data->mouse.wheel_data.x += 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 void swap_xy(struct input_event *evt) { + switch (evt->code) { + case INPUT_REL_X: + evt->code = INPUT_REL_Y; + break; + case INPUT_REL_Y: + evt->code = INPUT_REL_X; + 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 filter_with_input_config(const struct input_listener_config *cfg, + struct input_event *evt) { + if (!evt->dev) { + return; + } + + if (cfg->xy_swap) { + swap_xy(evt); + } + + if ((cfg->x_invert && is_x_data(evt)) || (cfg->y_invert && is_y_data(evt))) { + evt->value = -(evt->value); + } + + evt->value = (int16_t)((evt->value * cfg->scale_multiplier) / cfg->scale_divisor); +} + +static void clear_xy_data(struct input_listener_xy_data *data) { + data->x = data->y = 0; + data->mode = INPUT_LISTENER_XY_DATA_MODE_NONE; +} + +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, evt); + + LOG_DBG("Got input_handler event: %s with value 0x%x", get_input_code_name(evt), evt->value); + + zmk_input_listener_layer_toggle_input_rel_received(config, data); + + 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, data->mouse.wheel_data.y); + } + + if (data->mouse.data.mode == INPUT_LISTENER_XY_DATA_MODE_REL) { + zmk_hid_mouse_movement_set(data->mouse.data.x, data->mouse.data.y); + } + + if (data->mouse.button_set != 0) { + for (int i = 0; i < ZMK_MOUSE_HID_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_MOUSE_HID_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; + } +} + +void zmk_input_listener_layer_toggle_input_rel_received(const struct input_listener_config *config, + struct input_listener_data *data) { + if (config->layer_toggle == -1) { + return; + } + + data->layer_toggle_last_mouse_package_time = k_uptime_get(); + + if (data->layer_toggle_layer_enabled == false) { + k_work_schedule(&data->layer_toggle_activation_delay, + K_MSEC(config->layer_toggle_delay_ms)); + } else { + // Deactivate the layer if no further movement within + // layer_toggle_timeout_ms + k_work_reschedule(&data->layer_toggle_deactivation_delay, + K_MSEC(config->layer_toggle_timeout_ms)); + } +} + +void zmk_input_listener_layer_toggle_activate_layer(struct k_work *item) { + struct k_work_delayable *d_work = k_work_delayable_from_work(item); + + struct input_listener_data *data = + CONTAINER_OF(d_work, struct input_listener_data, layer_toggle_activation_delay); + const struct input_listener_config *config = data->dev->config; + + int64_t current_time = k_uptime_get(); + int64_t last_mv_within_ms = current_time - data->layer_toggle_last_mouse_package_time; + + if (last_mv_within_ms <= config->layer_toggle_timeout_ms * 0.1) { + LOG_INF("Activating layer %d due to mouse activity...", config->layer_toggle); + +#if IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_UROB_COMPAT) + + zmk_keymap_layer_activate(config->layer_toggle, false); + +#else + + zmk_keymap_layer_activate(config->layer_toggle); + +#endif /* IS_ENABLED(CONFIG_ZMK_INPUT_MOUSE_PS2_ENABLE_UROB_COMPAT) */ + + data->layer_toggle_layer_enabled = true; + } else { + LOG_INF("Not activating mouse layer %d, because last mouse activity was %lldms ago", + config->layer_toggle, last_mv_within_ms); + } +} + +void zmk_input_listener_layer_toggle_deactivate_layer(struct k_work *item) { + struct k_work_delayable *d_work = k_work_delayable_from_work(item); + + struct input_listener_data *data = + CONTAINER_OF(d_work, struct input_listener_data, layer_toggle_deactivation_delay); + const struct input_listener_config *config = data->dev->config; + + LOG_INF("Deactivating layer %d due to mouse activity...", config->layer_toggle); + + if (zmk_keymap_layer_active(config->layer_toggle)) { + zmk_keymap_layer_deactivate(config->layer_toggle); + } + + data->layer_toggle_layer_enabled = false; +} + +static int zmk_input_listener_layer_toggle_init(const struct input_listener_config *config, + struct input_listener_data *data) { + k_work_init_delayable(&data->layer_toggle_activation_delay, + zmk_input_listener_layer_toggle_activate_layer); + k_work_init_delayable(&data->layer_toggle_deactivation_delay, + zmk_input_listener_layer_toggle_deactivate_layer); + + return 0; +} + +#endif // VALID_LISTENER_COUNT > 0 + +#define IL_INST(n) \ + COND_CODE_1( \ + DT_NODE_HAS_STATUS(DT_INST_PHANDLE(n, device), okay), \ + ( \ + static const struct input_listener_config config_##n = \ + { \ + .xy_swap = DT_INST_PROP(n, xy_swap), \ + .x_invert = DT_INST_PROP(n, x_invert), \ + .y_invert = DT_INST_PROP(n, y_invert), \ + .scale_multiplier = DT_INST_PROP(n, scale_multiplier), \ + .scale_divisor = DT_INST_PROP(n, scale_divisor), \ + .layer_toggle = DT_INST_PROP(n, layer_toggle), \ + .layer_toggle_delay_ms = DT_INST_PROP(n, layer_toggle_delay_ms), \ + .layer_toggle_timeout_ms = DT_INST_PROP(n, layer_toggle_timeout_ms), \ + }; \ + static struct input_listener_data data_##n = \ + { \ + .dev = DEVICE_DT_INST_GET(n), \ + .layer_toggle_layer_enabled = false, \ + .layer_toggle_last_mouse_package_time = 0, \ + }; \ + 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); \ + \ + static int zmk_input_listener_init_##n(const struct device *dev) { \ + struct input_listener_data *data = dev->data; \ + const struct input_listener_config *config = dev->config; \ + \ + zmk_input_listener_layer_toggle_init(config, data); \ + \ + return 0; \ + } \ + \ + DEVICE_DT_INST_DEFINE(n, &zmk_input_listener_init_##n, NULL, &data_##n, &config_##n, \ + POST_KERNEL, CONFIG_APPLICATION_INIT_PRIORITY, NULL);), \ + ()) + +DT_INST_FOREACH_STATUS_OKAY(IL_INST) diff --git a/app/src/mouse/usb_hid.c b/app/src/mouse/usb_hid.c new file mode 100644 index 00000000000..1bfdd4608ce --- /dev/null +++ b/app/src/mouse/usb_hid.c @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static const struct device *hid_dev; + +static K_SEM_DEFINE(hid_sem, 1, 1); + +static void in_ready_cb(const struct device *dev) { k_sem_give(&hid_sem); } + +#define HID_GET_REPORT_TYPE_MASK 0xff00 +#define HID_GET_REPORT_ID_MASK 0x00ff + +#define HID_REPORT_TYPE_INPUT 0x100 +#define HID_REPORT_TYPE_OUTPUT 0x200 +#define HID_REPORT_TYPE_FEATURE 0x300 + +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) +static uint8_t hid_protocol = HID_PROTOCOL_REPORT; + +static void set_proto_cb(const struct device *dev, uint8_t protocol) { hid_protocol = protocol; } + +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + +static int get_report_cb(const struct device *dev, struct usb_setup_packet *setup, int32_t *len, + uint8_t **data) { + + /* + * 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 && + (setup->wValue & HID_GET_REPORT_TYPE_MASK) != HID_REPORT_TYPE_FEATURE) { + LOG_ERR("Get: Unsupported report type %d requested", + (setup->wValue & HID_GET_REPORT_TYPE_MASK) >> 8); + return -ENOTSUP; + } + + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { + case ZMK_MOUSE_HID_REPORT_ID_MOUSE: + struct zmk_hid_mouse_report *report = zmk_mouse_hid_get_mouse_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; + } + + return 0; +} + +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 && + (setup->wValue & HID_GET_REPORT_TYPE_MASK) != HID_REPORT_TYPE_FEATURE) { + LOG_ERR("Set: Unsupported report type %d requested", + (setup->wValue & HID_GET_REPORT_TYPE_MASK) >> 8); + return -ENOTSUP; + } + + switch (setup->wValue & HID_GET_REPORT_ID_MASK) { + default: + LOG_ERR("Invalid report ID %d requested", setup->wValue & HID_GET_REPORT_ID_MASK); + return -EINVAL; + } + + return 0; +} + +static const struct hid_ops ops = { +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + .protocol_change = set_proto_cb, +#endif + .int_in_ready = in_ready_cb, + .get_report = get_report_cb, + .set_report = set_report_cb, +}; + +static int zmk_mouse_usb_hid_send_report(const uint8_t *report, size_t len) { + switch (zmk_usb_get_status()) { + case USB_DC_SUSPEND: + return usb_wakeup_request(); + case USB_DC_ERROR: + case USB_DC_RESET: + case USB_DC_DISCONNECTED: + case USB_DC_UNKNOWN: + return -ENODEV; + default: + k_sem_take(&hid_sem, K_MSEC(30)); + LOG_HEXDUMP_DBG(report, len, "Mouse HID report"); + int err = hid_int_ep_write(hid_dev, report, len, NULL); + + if (err) { + LOG_ERR("Failed to write %d", err); + k_sem_give(&hid_sem); + } + + return err; + } +} + +#if IS_ENABLED(CONFIG_ZMK_MOUSE) +int zmk_mouse_usb_hid_send_mouse_report() { +#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) + if (hid_protocol == HID_PROTOCOL_BOOT) { + return -ENOTSUP; + } +#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ + + struct zmk_hid_mouse_report *report = zmk_mouse_hid_get_mouse_report(); + return zmk_mouse_usb_hid_send_report((uint8_t *)report, sizeof(*report)); +} +#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) + +static int zmk_mouse_usb_hid_init(void) { + hid_dev = device_get_binding("HID_1"); + if (hid_dev == NULL) { + LOG_ERR("Unable to locate HID device"); + return -EINVAL; + } + + usb_hid_register_device(hid_dev, zmk_mouse_hid_report_desc, sizeof(zmk_mouse_hid_report_desc), + &ops); + + // usb_hid_set_proto_code(hid_dev, HID_BOOT_IFACE_CODE_MOUSE); + + usb_hid_init(hid_dev); + + return 0; +} + +SYS_INIT(zmk_mouse_usb_hid_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/usb_hid.c b/app/src/usb_hid.c index cd3ef920391..383aeb81272 100644 --- a/app/src/usb_hid.c +++ b/app/src/usb_hid.c @@ -164,19 +164,6 @@ int zmk_usb_hid_send_consumer_report(void) { return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); } -#if IS_ENABLED(CONFIG_ZMK_MOUSE) -int zmk_usb_hid_send_mouse_report() { -#if IS_ENABLED(CONFIG_ZMK_USB_BOOT) - if (hid_protocol == HID_PROTOCOL_BOOT) { - return -ENOTSUP; - } -#endif /* IS_ENABLED(CONFIG_ZMK_USB_BOOT) */ - - struct zmk_hid_mouse_report *report = zmk_hid_get_mouse_report(); - return zmk_usb_hid_send_report((uint8_t *)report, sizeof(*report)); -} -#endif // IS_ENABLED(CONFIG_ZMK_MOUSE) - static int zmk_usb_hid_init(void) { hid_dev = device_get_binding("HID_0"); if (hid_dev == NULL) { 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/behaviors/index.mdx b/docs/docs/behaviors/index.mdx index 4a05f5653a0..b1121129176 100644 --- a/docs/docs/behaviors/index.mdx +++ b/docs/docs/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/behaviors/mouse-emulation.md b/docs/docs/behaviors/mouse-emulation.md index 7b80bae65d9..db14925265a 100644 --- a/docs/docs/behaviors/mouse-emulation.md +++ b/docs/docs/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 +``` diff --git a/docs/docs/intro.md b/docs/docs/intro.md index da01e8297e7..6ac1569563b 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](features/combos.md) | ✅ | | ✅ | | [Macros](behaviors/macros.md) | ✅ | ✅ | ✅ | -| Mouse Keys | 🚧 | ✅ | ✅ | +| Mouse Keys | ✅ | ✅ | ✅ | | Low Active Power Usage | ✅ | | | | Low Power Sleep States | ✅ | ✅ | | | [Low Power Mode (VCC Shutoff)](behaviors/power.md) | ✅ | ✅ | |