diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index d853255c..2911681a 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -28,6 +28,10 @@ target_sources(app PRIVATE src/activity.c) target_sources(app PRIVATE src/kscan.c) target_sources(app PRIVATE src/matrix_transform.c) target_sources(app PRIVATE src/hid.c) +target_sources(app PRIVATE src/mouse/key_listener.c) +target_sources(app PRIVATE src/mouse/main.c) +target_sources(app PRIVATE src/mouse/tick_listener.c) +target_sources_ifdef(CONFIG_ZMK_TRACKBALL_PIM447 app PRIVATE src/mouse/trackball_pim447.c) target_sources(app PRIVATE src/sensors.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c) target_sources(app PRIVATE src/event_manager.c) @@ -39,6 +43,10 @@ target_sources(app PRIVATE src/events/keycode_state_changed.c) target_sources(app PRIVATE src/events/modifiers_state_changed.c) target_sources(app PRIVATE src/events/endpoint_selection_changed.c) target_sources(app PRIVATE src/events/sensor_event.c) +target_sources(app PRIVATE src/events/mouse_button_state_changed.c) +target_sources(app PRIVATE src/events/mouse_move_state_changed.c) +target_sources(app PRIVATE src/events/mouse_tick.c) +target_sources(app PRIVATE src/events/mouse_scroll_state_changed.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/events/wpm_state_changed.c) target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c) target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/battery_state_changed.c) @@ -60,6 +68,10 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_transparent.c) target_sources(app PRIVATE src/behaviors/behavior_none.c) target_sources(app PRIVATE src/behaviors/behavior_sensor_rotate_key_press.c) + target_sources(app PRIVATE src/behaviors/behavior_mouse_key_press.c) + target_sources(app PRIVATE src/behaviors/behavior_mouse_move.c) + target_sources(app PRIVATE src/behaviors/behavior_mouse_scroll.c) + target_sources(app PRIVATE src/behaviors/behavior_trackball_pim447.c) target_sources(app PRIVATE src/combo.c) target_sources(app PRIVATE src/conditional_layer.c) target_sources(app PRIVATE src/keymap.c) diff --git a/app/Kconfig b/app/Kconfig index 1c9f929d..4dd2f525 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -124,6 +124,10 @@ config ZMK_BLE_CONSUMER_REPORT_QUEUE_SIZE int "Max number of consumer HID reports to queue for sending over BLE" default 5 +config ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE + int "Max number of mouse HID reports to queue for sending over BLE" + default 20 + config ZMK_BLE_CLEAR_BONDS_ON_START bool "Configuration that clears all bond information from the keyboard on startup." default n @@ -360,6 +364,13 @@ endif #Display/LED Options endmenu +menu "Mouse Options" + +rsource "src/mouse/Kconfig" + +#Mouse Options +endmenu + menu "Power Management" config ZMK_IDLE_TIMEOUT diff --git a/app/drivers/sensor/CMakeLists.txt b/app/drivers/sensor/CMakeLists.txt index b549320f..d5e35e75 100644 --- a/app/drivers/sensor/CMakeLists.txt +++ b/app/drivers/sensor/CMakeLists.txt @@ -2,4 +2,5 @@ # SPDX-License-Identifier: MIT add_subdirectory_ifdef(CONFIG_ZMK_BATTERY battery) -add_subdirectory_ifdef(CONFIG_EC11 ec11) \ No newline at end of file +add_subdirectory_ifdef(CONFIG_EC11 ec11) +add_subdirectory_ifdef(CONFIG_ZMK_TRACKBALL_PIM447 trackball_pim447) diff --git a/app/drivers/sensor/Kconfig b/app/drivers/sensor/Kconfig index a828f6c6..fcf451a1 100644 --- a/app/drivers/sensor/Kconfig +++ b/app/drivers/sensor/Kconfig @@ -2,4 +2,5 @@ # SPDX-License-Identifier: MIT rsource "battery/Kconfig" -rsource "ec11/Kconfig" \ No newline at end of file +rsource "ec11/Kconfig" +rsource "trackball_pim447/Kconfig" diff --git a/app/drivers/sensor/trackball_pim447/CMakeLists.txt b/app/drivers/sensor/trackball_pim447/CMakeLists.txt new file mode 100644 index 00000000..03e20c65 --- /dev/null +++ b/app/drivers/sensor/trackball_pim447/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() +zephyr_library_sources(trackball_pim447.c) diff --git a/app/drivers/sensor/trackball_pim447/Kconfig b/app/drivers/sensor/trackball_pim447/Kconfig new file mode 100644 index 00000000..ac837b93 --- /dev/null +++ b/app/drivers/sensor/trackball_pim447/Kconfig @@ -0,0 +1,8 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +config ZMK_TRACKBALL_PIM447 + bool "ZMK PIM447 trackball" + depends on I2C + help + Enable I2C-based driver for Pimoroni PIM447 trackball. diff --git a/app/drivers/sensor/trackball_pim447/trackball_pim447.c b/app/drivers/sensor/trackball_pim447/trackball_pim447.c new file mode 100644 index 00000000..6efb1ef8 --- /dev/null +++ b/app/drivers/sensor/trackball_pim447/trackball_pim447.c @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT pimoroni_trackball_pim447 + +#include +#include + +#define LOG_LEVEL CONFIG_SENSOR_LOG_LEVEL +#include +LOG_MODULE_REGISTER(trackball_pim447); + +#define TRACKBALL_PIM447_REG_LEFT 0x04 +#define TRACKBALL_PIM447_REG_RIGHT 0x05 +#define TRACKBALL_PIM447_REG_UP 0x06 +#define TRACKBALL_PIM447_REG_DOWN 0x07 +#define TRACKBALL_PIM447_REG_SWITCH 0x08 + +#define TRACKBALL_PIM447_REG_MIN TRACKBALL_PIM447_REG_LEFT +#define TRACKBALL_PIM447_REG_MAX TRACKBALL_PIM447_REG_SWITCH + +struct trackball_pim447_data { + const struct device *i2c_dev; + int32_t dx; + int32_t dy; + int32_t dz; +}; + +static int trackball_pim447_read_reg(const struct device *dev, + uint8_t reg, uint8_t *value) +{ + struct trackball_pim447_data *data = dev->data; + + if (reg < TRACKBALL_PIM447_REG_MIN || reg > TRACKBALL_PIM447_REG_MAX) { + return -ENOTSUP; + } + + int status = i2c_reg_read_byte(data->i2c_dev, DT_INST_REG_ADDR(0), + reg, value); + if (status < 0) { + LOG_ERR("Sensor reg read byte failed"); + return status; + } + + return 0; +} + +static int trackball_pim447_read_axis(const struct device *dev, + uint8_t reg_negative, + uint8_t reg_positive, + int32_t *value) +{ + uint8_t value_negative; + uint8_t value_positive; + int status; + + status = trackball_pim447_read_reg(dev, reg_negative, &value_negative); + if (status < 0) { + return status; + } + + status = trackball_pim447_read_reg(dev, reg_positive, &value_positive); + if (status < 0) { + return status; + } + + *value = value_positive - value_negative; + + return 0; +} + +static int trackball_pim447_sample_fetch(const struct device *dev, + enum sensor_channel chan) +{ + struct trackball_pim447_data *data = dev->data; + int status; + + if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_POS_DX) { + status = trackball_pim447_read_axis(dev, + TRACKBALL_PIM447_REG_LEFT, + TRACKBALL_PIM447_REG_RIGHT, + &data->dx); + if (status < 0) { + return status; + } + } + + if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_POS_DY) { + status = trackball_pim447_read_axis(dev, + TRACKBALL_PIM447_REG_UP, + TRACKBALL_PIM447_REG_DOWN, + &data->dy); + if (status < 0) { + return status; + } + } + + if (chan == SENSOR_CHAN_ALL || chan == SENSOR_CHAN_POS_DZ) { + uint8_t value; + status = trackball_pim447_read_reg(dev, + TRACKBALL_PIM447_REG_SWITCH, + &value); + if (status < 0) { + return status; + } + + data->dz = value; + } + + return 0; +} + +static int trackball_pim447_channel_get(const struct device *dev, + enum sensor_channel chan, + struct sensor_value *val) +{ + struct trackball_pim447_data *data = dev->data; + + /* Not used. */ + val->val2 = 0; + + switch (chan) { + case SENSOR_CHAN_POS_DX: + val->val1 = data->dx; + break; + + case SENSOR_CHAN_POS_DY: + val->val1 = data->dy; + break; + + case SENSOR_CHAN_POS_DZ: + val->val1 = data->dz; + break; + + default: + return -ENOTSUP; + } + + return 0; +} + +static int trackball_pim447_init(const struct device *dev) +{ + struct trackball_pim447_data *data = dev->data; + + data->i2c_dev = device_get_binding(DT_INST_BUS_LABEL(0)); + if (data->i2c_dev == NULL) { + LOG_ERR("Failed to get I2C device"); + return -EINVAL; + } + + return 0; +} + +static struct trackball_pim447_data trackball_pim447_data; + +static const struct sensor_driver_api trackball_pim447_api = { + .sample_fetch = trackball_pim447_sample_fetch, + .channel_get = trackball_pim447_channel_get, +}; + +DEVICE_DT_INST_DEFINE(0, &trackball_pim447_init, device_pm_control_nop, + &trackball_pim447_data, NULL, POST_KERNEL, + CONFIG_SENSOR_INIT_PRIORITY, &trackball_pim447_api); diff --git a/app/drivers/zephyr/dts/bindings/sensor/pimoroni,trackball_pim447.yaml b/app/drivers/zephyr/dts/bindings/sensor/pimoroni,trackball_pim447.yaml new file mode 100644 index 00000000..2e1796e7 --- /dev/null +++ b/app/drivers/zephyr/dts/bindings/sensor/pimoroni,trackball_pim447.yaml @@ -0,0 +1,58 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Trackball PIM447 + +compatible: "pimoroni,trackball_pim447" + +properties: + label: + type: string + required: true + + reg: + type: int + required: true + + move-factor: + type: int + required: false + default: 1 + + invert-move-x: + type: boolean + required: false + + invert-move-y: + type: boolean + required: false + + button: + type: int + required: false + default: 0 + + swap-axes: + type: boolean + required: false + + scroll-divisor: + type: int + required: false + default: 2 + + invert-scroll-x: + type: boolean + required: false + + invert-scroll-y: + type: boolean + required: false + + mode: + type: int + required: false + default: 1 + enum: + - 1 + - 2 diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 3e797cc9..5746b640 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -17,3 +17,7 @@ #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 new file mode 100644 index 00000000..8b2aacb3 --- /dev/null +++ b/app/dts/behaviors/mouse_key_press.dtsi @@ -0,0 +1,9 @@ +/ { + behaviors { + /omit-if-no-ref/ mkp: behavior_mouse_key_press { + compatible = "zmk,behavior-mouse-key-press"; + label = "MOUSE_KEY_PRESS"; + #binding-cells = <1>; + }; + }; +}; diff --git a/app/dts/behaviors/mouse_move.dtsi b/app/dts/behaviors/mouse_move.dtsi new file mode 100644 index 00000000..d34329c8 --- /dev/null +++ b/app/dts/behaviors/mouse_move.dtsi @@ -0,0 +1,12 @@ +/ { + behaviors { + /omit-if-no-ref/ mmv: behavior_mouse_move { + compatible = "zmk,behavior-mouse-move"; + label = "MOUSE_MOVE"; + #binding-cells = <1>; + delay-ms = <0>; + time-to-max-speed-ms = <300>; + acceleration-exponent = <1>; + }; + }; +}; diff --git a/app/dts/behaviors/mouse_scroll.dtsi b/app/dts/behaviors/mouse_scroll.dtsi new file mode 100644 index 00000000..fb54886d --- /dev/null +++ b/app/dts/behaviors/mouse_scroll.dtsi @@ -0,0 +1,12 @@ +/ { + behaviors { + /omit-if-no-ref/ mwh: msc: behavior_mouse_scroll { + compatible = "zmk,behavior-mouse-scroll"; + label = "MOUSE_SCROLL"; + #binding-cells = <1>; + delay-ms = <0>; + time-to-max-speed-ms = <300>; + acceleration-exponent = <0>; + }; + }; +}; diff --git a/app/dts/behaviors/trackball_pim447.dtsi b/app/dts/behaviors/trackball_pim447.dtsi new file mode 100644 index 00000000..dc95c692 --- /dev/null +++ b/app/dts/behaviors/trackball_pim447.dtsi @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + /omit-if-no-ref/ pim447_move: behavior_trackball_pim447_move { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_MOVE"; + mode = ; + #binding-cells = <0>; + }; + + /omit-if-no-ref/ pim447_scroll: behavior_trackball_pim447_scroll { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_SCROLL"; + mode = ; + #binding-cells = <0>; + }; + + /omit-if-no-ref/ pim447_toggle: behavior_trackball_pim447_toggle { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_TOGGLE"; + mode = ; + #binding-cells = <0>; + }; + + /omit-if-no-ref/ pim447_move_scroll: behavior_trackball_pim447_move_scroll { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_MOVE_SCROLL"; + mode = ; + momentary; + #binding-cells = <0>; + }; + + /omit-if-no-ref/ pim447_scroll_move: behavior_trackball_pim447_scroll_move { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_SCROLL_MOVE"; + mode = ; + momentary; + #binding-cells = <0>; + }; + + /omit-if-no-ref/ pim447_toggle_toggle: behavior_trackball_pim447_toggle_toggle { + compatible = "zmk,behavior-trackball-pim447"; + label = "PIM447_TOGGLE_TOGGLE"; + mode = ; + momentary; + #binding-cells = <0>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml b/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml new file mode 100644 index 00000000..8540916b --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-mouse-key-press.yaml @@ -0,0 +1,5 @@ +description: Mouse key press/release behavior + +compatible: "zmk,behavior-mouse-key-press" + +include: one_param.yaml diff --git a/app/dts/bindings/behaviors/zmk,behavior-mouse-move.yaml b/app/dts/bindings/behaviors/zmk,behavior-mouse-move.yaml new file mode 100644 index 00000000..73ec34ec --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-mouse-move.yaml @@ -0,0 +1,13 @@ +description: Mouse move + +compatible: "zmk,behavior-mouse-move" + +include: one_param.yaml + +properties: + delay-ms: + type: int + time-to-max-speed-ms: + type: int + acceleration-exponent: + type: int diff --git a/app/dts/bindings/behaviors/zmk,behavior-mouse-scroll.yaml b/app/dts/bindings/behaviors/zmk,behavior-mouse-scroll.yaml new file mode 100644 index 00000000..5a932bc5 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-mouse-scroll.yaml @@ -0,0 +1,13 @@ +description: Mouse scroll + +compatible: "zmk,behavior-mouse-scroll" + +include: one_param.yaml + +properties: + delay-ms: + type: int + time-to-max-speed-ms: + type: int + acceleration-exponent: + type: int diff --git a/app/dts/bindings/behaviors/zmk,behavior-trackball_pim447.yaml b/app/dts/bindings/behaviors/zmk,behavior-trackball_pim447.yaml new file mode 100644 index 00000000..0c34117b --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-trackball_pim447.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Trackball PIM447 behavior + +compatible: "zmk,behavior-trackball-pim447" + +include: zero_param.yaml + +properties: + mode: + type: int + required: true + momentary: + type: boolean diff --git a/app/include/dt-bindings/zmk/hid_usage_pages.h b/app/include/dt-bindings/zmk/hid_usage_pages.h index f38f4fd3..e3946867 100644 --- a/app/include/dt-bindings/zmk/hid_usage_pages.h +++ b/app/include/dt-bindings/zmk/hid_usage_pages.h @@ -26,6 +26,7 @@ #define HID_USAGE_GDV (0x06) // Generic Device Controls #define HID_USAGE_KEY (0x07) // Keyboard/Keypad #define HID_USAGE_LED (0x08) // LED +#define HID_USAGE_BUTTON (0x09) // Button #define HID_USAGE_TELEPHONY (0x0B) // Telephony Device #define HID_USAGE_CONSUMER (0x0C) // Consumer #define HID_USAGE_DIGITIZERS (0x0D) // Digitizers diff --git a/app/include/dt-bindings/zmk/mouse.h b/app/include/dt-bindings/zmk/mouse.h new file mode 100644 index 00000000..cf0415c9 --- /dev/null +++ b/app/include/dt-bindings/zmk/mouse.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +/* Mouse press behavior */ +/* Left click */ +#define MB1 (0x01) +#define LCLK (MB1) + +/* Right click */ +#define MB2 (0x02) +#define RCLK (MB2) + +/* Middle click */ +#define MB3 (0x04) +#define MCLK (MB3) + +#define MB4 (0x08) + +#define MB5 (0x10) + +#define MB6 (0x20) + +#define MB7 (0x40) + +#define MB8 (0x80) + +/* Mouse move behavior */ +#define MOVE_VERT(vert) ((vert)&0xFFFF) +#define MOVE_VERT_DECODE(encoded) (int16_t)((encoded)&0x0000FFFF) +#define MOVE_HOR(hor) (((hor)&0xFFFF) << 16) +#define MOVE_HOR_DECODE(encoded) (int16_t)(((encoded)&0xFFFF0000) >> 16) + +#define MOVE(hor, vert) (MOVE_HOR(hor) + MOVE_VERT(vert)) + +#define MOVE_UP MOVE_VERT(-600) +#define MOVE_DOWN MOVE_VERT(600) +#define MOVE_LEFT MOVE_HOR(-600) +#define MOVE_RIGHT MOVE_HOR(600) + +/* Mouse scroll behavior */ +#define SCROLL_VERT(vert) ((vert)&0xFFFF) +#define SCROLL_VERT_DECODE(encoded) (int16_t)((encoded)&0x0000FFFF) +#define SCROLL_HOR(hor) (((hor)&0xFFFF) << 16) +#define SCROLL_HOR_DECODE(encoded) (int16_t)(((encoded)&0xFFFF0000) >> 16) + +#define SCROLL(hor, vert) (SCROLL_HOR(hor) + SCROLL_VERT(vert)) + +#define SCROLL_UP SCROLL_VERT(10) +#define SCROLL_DOWN SCROLL_VERT(-10) +#define SCROLL_LEFT SCROLL_HOR(-10) +#define SCROLL_RIGHT SCROLL_HOR(10) diff --git a/app/include/dt-bindings/zmk/trackball_pim447.h b/app/include/dt-bindings/zmk/trackball_pim447.h new file mode 100644 index 00000000..a5ebc86d --- /dev/null +++ b/app/include/dt-bindings/zmk/trackball_pim447.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +#define PIM447_NONE 0 +#define PIM447_MOVE 1 +#define PIM447_SCROLL 2 +#define PIM447_TOGGLE 3 diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index c8860533..450d7ea3 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -13,3 +13,4 @@ int zmk_endpoints_toggle(); enum zmk_endpoint zmk_endpoints_selected(); int zmk_endpoints_send_report(uint16_t usage_page); +int zmk_endpoints_send_mouse_report(); diff --git a/app/include/zmk/events/mouse_button_state_changed.h b/app/include/zmk/events/mouse_button_state_changed.h new file mode 100644 index 00000000..7ec4d208 --- /dev/null +++ b/app/include/zmk/events/mouse_button_state_changed.h @@ -0,0 +1,27 @@ + +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include + +struct zmk_mouse_button_state_changed { + zmk_mouse_button_t buttons; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_mouse_button_state_changed); + +static inline struct zmk_mouse_button_state_changed_event * +zmk_mouse_button_state_changed_from_encoded(uint32_t encoded, bool pressed, int64_t timestamp) { + return new_zmk_mouse_button_state_changed((struct zmk_mouse_button_state_changed){ + .buttons = HID_USAGE_ID(encoded), .state = pressed, .timestamp = timestamp}); +} diff --git a/app/include/zmk/events/mouse_move_state_changed.h b/app/include/zmk/events/mouse_move_state_changed.h new file mode 100644 index 00000000..8866f81d --- /dev/null +++ b/app/include/zmk/events/mouse_move_state_changed.h @@ -0,0 +1,33 @@ + +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_mouse_move_state_changed { + struct vector2d max_speed; + struct mouse_config config; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_mouse_move_state_changed); + +static inline struct zmk_mouse_move_state_changed_event * +zmk_mouse_move_state_changed_from_encoded(uint32_t encoded, struct mouse_config config, + bool pressed, int64_t timestamp) { + struct vector2d max_speed = (struct vector2d){ + .x = MOVE_HOR_DECODE(encoded), + .y = MOVE_VERT_DECODE(encoded), + }; + + return new_zmk_mouse_move_state_changed((struct zmk_mouse_move_state_changed){ + .max_speed = max_speed, .config = config, .state = pressed, .timestamp = timestamp}); +} diff --git a/app/include/zmk/events/mouse_scroll_state_changed.h b/app/include/zmk/events/mouse_scroll_state_changed.h new file mode 100644 index 00000000..fa60e8a7 --- /dev/null +++ b/app/include/zmk/events/mouse_scroll_state_changed.h @@ -0,0 +1,34 @@ + +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include + +struct zmk_mouse_scroll_state_changed { + struct vector2d max_speed; + struct mouse_config config; + bool state; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_mouse_scroll_state_changed); + +static inline struct zmk_mouse_scroll_state_changed_event * +zmk_mouse_scroll_state_changed_from_encoded(uint32_t encoded, struct mouse_config config, + bool pressed, int64_t timestamp) { + struct vector2d max_speed = (struct vector2d){ + .x = SCROLL_HOR_DECODE(encoded), + .y = SCROLL_VERT_DECODE(encoded), + }; + + return new_zmk_mouse_scroll_state_changed((struct zmk_mouse_scroll_state_changed){ + .max_speed = max_speed, .config = config, .state = pressed, .timestamp = timestamp}); +} diff --git a/app/include/zmk/events/mouse_tick.h b/app/include/zmk/events/mouse_tick.h new file mode 100644 index 00000000..c75b9b4f --- /dev/null +++ b/app/include/zmk/events/mouse_tick.h @@ -0,0 +1,39 @@ + +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include +#include + +struct zmk_mouse_tick { + struct vector2d max_move; + struct vector2d max_scroll; + struct mouse_config move_config; + struct mouse_config scroll_config; + int64_t *start_time; + int64_t timestamp; +}; + +ZMK_EVENT_DECLARE(zmk_mouse_tick); + +static inline struct zmk_mouse_tick_event *zmk_mouse_tick(struct vector2d max_move, + struct vector2d max_scroll, + struct mouse_config move_config, + struct mouse_config scroll_config, + int64_t *movement_start) { + return new_zmk_mouse_tick((struct zmk_mouse_tick){ + .max_move = max_move, + .max_scroll = max_scroll, + .move_config = move_config, + .scroll_config = scroll_config, + .start_time = movement_start, + .timestamp = k_uptime_get(), + }); +} diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index e23caff9..eb3ca658 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -10,11 +10,10 @@ #include #include +#include #include #include -#define ZMK_HID_KEYBOARD_NKRO_MAX_USAGE HID_USAGE_KEY_KEYPAD_EQUAL - #define COLLECTION_REPORT 0x03 static const uint8_t zmk_hid_report_desc[] = { @@ -185,6 +184,116 @@ static const uint8_t zmk_hid_report_desc[] = { 0x00, /* END COLLECTION */ HID_MI_COLLECTION_END, + + /* USAGE_PAGE (Generic Desktop) */ + HID_GI_USAGE_PAGE, + HID_USAGE_GD, + /* USAGE (Mouse) */ + HID_LI_USAGE, + HID_USAGE_GD_MOUSE, + /* COLLECTION (Application) */ + HID_MI_COLLECTION, + COLLECTION_APPLICATION, + /* REPORT ID (4) */ + HID_GI_REPORT_ID, + 0x04, + /* USAGE (Pointer) */ + HID_LI_USAGE, + HID_USAGE_GD_POINTER, + /* COLLECTION (Physical) */ + HID_MI_COLLECTION, + COLLECTION_PHYSICAL, + /* USAGE_PAGE (Button) */ + HID_GI_USAGE_PAGE, + HID_USAGE_BUTTON, + /* USAGE_MINIMUM (0x1) (button 1?) */ + HID_LI_USAGE_MIN(1), + 0x1, + /* USAGE_MAXIMUM (0x10) (button 5? Buttons up to 8 still work) */ + HID_LI_USAGE_MAX(1), + 0x10, + /* LOGICAL_MINIMUM (0) */ + HID_GI_LOGICAL_MIN(1), + 0x00, + /* LOGICAL_MAXIMUM (1) */ + HID_GI_LOGICAL_MAX(1), + 0x01, + /* REPORT_SIZE (1) */ + HID_GI_REPORT_SIZE, + 0x01, + /* REPORT_COUNT (16) */ + HID_GI_REPORT_COUNT, + 0x10, + /* INPUT (Data,Var,Abs) */ + HID_MI_INPUT, + 0x02, + /* USAGE_PAGE (Generic Desktop) */ + HID_GI_USAGE_PAGE, + HID_USAGE_GD, + /* LOGICAL_MINIMUM (-32767) */ + HID_GI_LOGICAL_MIN(2), + 0x01, + 0x80, + /* LOGICAL_MAXIMUM (32767) */ + HID_GI_LOGICAL_MAX(2), + 0xFF, + 0x7F, + /* REPORT_SIZE (16) */ + HID_GI_REPORT_SIZE, + 0x10, + /* REPORT_COUNT (2) */ + HID_GI_REPORT_COUNT, + 0x02, + /* USAGE (X) */ // Vertical scroll + HID_LI_USAGE, + HID_USAGE_GD_X, + /* USAGE (Y) */ + HID_LI_USAGE, + HID_USAGE_GD_Y, + /* Input (Data,Var,Rel) */ + HID_MI_INPUT, + 0x06, + /* LOGICAL_MINIMUM (-127) */ + HID_GI_LOGICAL_MIN(1), + 0x81, + /* LOGICAL_MAXIMUM (127) */ + HID_GI_LOGICAL_MAX(1), + 0x7F, + /* REPORT_SIZE (8) */ + HID_GI_REPORT_SIZE, + 0x08, + /* REPORT_COUNT (1) */ + HID_GI_REPORT_COUNT, + 0x01, + /* USAGE (Wheel) */ + HID_LI_USAGE, + HID_USAGE_GD_WHEEL, + /* Input (Data,Var,Rel) */ + HID_MI_INPUT, + 0x06, + /* USAGE_PAGE (Consumer) */ // Horizontal scroll + HID_GI_USAGE_PAGE, + HID_USAGE_CONSUMER, + /* USAGE (AC Pan) */ + 0x0A, + 0x38, + 0x02, + /* LOGICAL_MINIMUM (-127) */ + HID_GI_LOGICAL_MIN(1), + 0x81, + /* LOGICAL_MAXIMUM (127) */ + HID_GI_LOGICAL_MAX(1), + 0x7F, + /* REPORT_COUNT (1) */ + HID_GI_REPORT_COUNT, + 0x01, + /* Input (Data,Var,Rel) */ + HID_MI_INPUT, + 0x06, + /* END COLLECTION */ + HID_MI_COLLECTION_END, + /* END COLLECTION */ + HID_MI_COLLECTION_END, }; // struct zmk_hid_boot_report @@ -222,6 +331,19 @@ struct zmk_hid_consumer_report { struct zmk_hid_consumer_report_body body; } __packed; +struct zmk_hid_mouse_report_body { + zmk_mouse_button_flags_t buttons; + int16_t x; + int16_t y; + int8_t scroll_y; + int8_t scroll_x; +} __packed; + +struct zmk_hid_mouse_report { + uint8_t report_id; + struct zmk_hid_mouse_report_body body; +} __packed; + zmk_mod_flags_t zmk_hid_get_explicit_mods(); int zmk_hid_register_mod(zmk_mod_t modifier); int zmk_hid_unregister_mod(zmk_mod_t modifier); @@ -237,5 +359,16 @@ int zmk_hid_consumer_press(zmk_key_t key); int zmk_hid_consumer_release(zmk_key_t key); void zmk_hid_consumer_clear(); +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(); + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report(); struct zmk_hid_consumer_report *zmk_hid_get_consumer_report(); +struct zmk_hid_mouse_report *zmk_hid_get_mouse_report(); diff --git a/app/include/zmk/hog.h b/app/include/zmk/hog.h index 7523fb66..9debc3ff 100644 --- a/app/include/zmk/hog.h +++ b/app/include/zmk/hog.h @@ -13,3 +13,4 @@ int zmk_hog_init(); int zmk_hog_send_keyboard_report(struct zmk_hid_keyboard_report_body *body); int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *body); +int zmk_hog_send_mouse_report(struct zmk_hid_mouse_report_body *body); diff --git a/app/include/zmk/mouse.h b/app/include/zmk/mouse.h new file mode 100644 index 00000000..f8f85744 --- /dev/null +++ b/app/include/zmk/mouse.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +typedef uint16_t zmk_mouse_button_flags_t; +typedef uint16_t zmk_mouse_button_t; + +struct mouse_config { + int delay_ms; + int time_to_max_speed_ms; + // acceleration exponent 0: uniform speed + // acceleration exponent 1: uniform acceleration + // acceleration exponent 2: uniform jerk + int acceleration_exponent; +}; + +struct vector2d { + float x; + float y; +}; + +struct k_work_q *zmk_mouse_work_q(); +int zmk_mouse_init(); \ No newline at end of file diff --git a/app/include/zmk/trackball_pim447.h b/app/include/zmk/trackball_pim447.h new file mode 100644 index 00000000..08620821 --- /dev/null +++ b/app/include/zmk/trackball_pim447.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ +#pragma once + +#include + +extern void zmk_trackball_pim447_set_mode(int mode); diff --git a/app/src/behaviors/behavior_mouse_key_press.c b/app/src/behaviors/behavior_mouse_key_press.c new file mode 100644 index 00000000..e5f2709c --- /dev/null +++ b/app/src/behaviors/behavior_mouse_key_press.c @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_mouse_key_press + +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int behavior_mouse_key_press_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + + return ZMK_EVENT_RAISE( + zmk_mouse_button_state_changed_from_encoded(binding->param1, true, event.timestamp)); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + return ZMK_EVENT_RAISE( + zmk_mouse_button_state_changed_from_encoded(binding->param1, false, event.timestamp)); +} + +static const struct behavior_driver_api behavior_mouse_key_press_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define KP_INST(n) \ + DEVICE_DT_INST_DEFINE(n, behavior_mouse_key_press_init, device_pm_control_nop, NULL, NULL, \ + APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_mouse_key_press_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ \ No newline at end of file diff --git a/app/src/behaviors/behavior_mouse_move.c b/app/src/behaviors/behavior_mouse_move.c new file mode 100644 index 00000000..5977a039 --- /dev/null +++ b/app/src/behaviors/behavior_mouse_move.c @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_mouse_move + +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int behavior_mouse_move_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct mouse_config *config = dev->config; + return ZMK_EVENT_RAISE( + zmk_mouse_move_state_changed_from_encoded(binding->param1, *config, true, event.timestamp)); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct mouse_config *config = dev->config; + return ZMK_EVENT_RAISE(zmk_mouse_move_state_changed_from_encoded(binding->param1, *config, + false, event.timestamp)); +} + +static const struct behavior_driver_api behavior_mouse_move_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define KP_INST(n) \ + static struct mouse_config behavior_mouse_move_config_##n = { \ + .delay_ms = DT_INST_PROP(n, delay_ms), \ + .time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \ + .acceleration_exponent = DT_INST_PROP(n, acceleration_exponent), \ + }; \ + DEVICE_DT_INST_DEFINE(n, behavior_mouse_move_init, device_pm_control_nop, NULL, \ + &behavior_mouse_move_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_mouse_move_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/behaviors/behavior_mouse_scroll.c b/app/src/behaviors/behavior_mouse_scroll.c new file mode 100644 index 00000000..64162352 --- /dev/null +++ b/app/src/behaviors/behavior_mouse_scroll.c @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_mouse_scroll + +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static int behavior_mouse_scroll_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct mouse_config *config = dev->config; + return ZMK_EVENT_RAISE(zmk_mouse_scroll_state_changed_from_encoded(binding->param1, *config, + true, event.timestamp)); +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1); + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct mouse_config *config = dev->config; + return ZMK_EVENT_RAISE(zmk_mouse_scroll_state_changed_from_encoded(binding->param1, *config, + false, event.timestamp)); +} + +static const struct behavior_driver_api behavior_mouse_scroll_driver_api = { + .binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released}; + +#define KP_INST(n) \ + static struct mouse_config behavior_mouse_scroll_config_##n = { \ + .delay_ms = DT_INST_PROP(n, delay_ms), \ + .time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \ + .acceleration_exponent = DT_INST_PROP(n, acceleration_exponent), \ + }; \ + DEVICE_DT_INST_DEFINE(n, behavior_mouse_scroll_init, device_pm_control_nop, NULL, \ + &behavior_mouse_scroll_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_mouse_scroll_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/behaviors/behavior_trackball_pim447.c b/app/src/behaviors/behavior_trackball_pim447.c new file mode 100644 index 00000000..d2eec9bc --- /dev/null +++ b/app/src/behaviors/behavior_trackball_pim447.c @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_trackball_pim447 + +#include +#include +#include + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct config { + int mode; + bool momentary; +}; + +static int behavior_trackball_pim447_init(const struct device *dev) { return 0; }; + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *device = device_get_binding(binding->behavior_dev); + const struct config *config = device->config; + + zmk_trackball_pim447_set_mode(config->mode); + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *device = device_get_binding(binding->behavior_dev); + const struct config *config = device->config; + + if (config->momentary) { + zmk_trackball_pim447_set_mode(PIM447_TOGGLE); + } + + return ZMK_BEHAVIOR_OPAQUE; +} + +static const struct behavior_driver_api behavior_trackball_pim447_driver_api = { + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, +}; + +#define PIM447_INST(n) \ + static const struct config config_##n = { \ + .mode = DT_INST_PROP(n, mode), \ + .momentary = DT_INST_PROP(n, momentary) \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, behavior_trackball_pim447_init, device_pm_control_nop, \ + NULL, &config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_trackball_pim447_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(PIM447_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/endpoints.c b/app/src/endpoints.c index c15aad87..7aabc5d4 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -145,6 +145,40 @@ int zmk_endpoints_send_report(uint16_t usage_page) { } } +int zmk_endpoints_send_mouse_report() { + struct zmk_hid_mouse_report *mouse_report = zmk_hid_get_mouse_report(); + + switch (current_endpoint) { +#if IS_ENABLED(CONFIG_ZMK_USB) + case ZMK_ENDPOINT_USB: { + int err = zmk_usb_hid_send_report((uint8_t *)mouse_report, sizeof(*mouse_report)); + if (err) { + LOG_ERR("FAILED TO SEND OVER USB: %d", err); + } + return err; + } +#endif /* IS_ENABLED(CONFIG_ZMK_USB) */ + +#if IS_ENABLED(CONFIG_ZMK_BLE) + case ZMK_ENDPOINT_BLE: { +#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) + int err = zmk_hog_send_mouse_report_direct(&mouse_report->body); +#else + int err = zmk_hog_send_mouse_report(&mouse_report->body); +#endif + if (err) { + LOG_ERR("FAILED TO SEND OVER HOG: %d", err); + } + return err; + } +#endif /* IS_ENABLED(CONFIG_ZMK_BLE) */ + + default: + LOG_ERR("Unsupported endpoint %d", current_endpoint); + return -ENOTSUP; + } +} + #if IS_ENABLED(CONFIG_SETTINGS) static int endpoints_handle_set(const char *name, size_t len, settings_read_cb read_cb, @@ -229,6 +263,7 @@ static enum zmk_endpoint get_selected_endpoint() { static void disconnect_current_endpoint() { zmk_hid_keyboard_clear(); zmk_hid_consumer_clear(); + zmk_hid_mouse_clear(); zmk_endpoints_send_report(HID_USAGE_KEY); zmk_endpoints_send_report(HID_USAGE_CONSUMER); diff --git a/app/src/events/mouse_button_state_changed.c b/app/src/events/mouse_button_state_changed.c new file mode 100644 index 00000000..e1ede414 --- /dev/null +++ b/app/src/events/mouse_button_state_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_mouse_button_state_changed); diff --git a/app/src/events/mouse_move_state_changed.c b/app/src/events/mouse_move_state_changed.c new file mode 100644 index 00000000..faf89cb8 --- /dev/null +++ b/app/src/events/mouse_move_state_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_mouse_move_state_changed); diff --git a/app/src/events/mouse_scroll_state_changed.c b/app/src/events/mouse_scroll_state_changed.c new file mode 100644 index 00000000..4b4170fe --- /dev/null +++ b/app/src/events/mouse_scroll_state_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_mouse_scroll_state_changed); diff --git a/app/src/events/mouse_tick.c b/app/src/events/mouse_tick.c new file mode 100644 index 00000000..0930b9fb --- /dev/null +++ b/app/src/events/mouse_tick.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_mouse_tick); diff --git a/app/src/hid.c b/app/src/hid.c index d6c63e1d..43918d7c 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -15,6 +15,9 @@ static struct zmk_hid_keyboard_report keyboard_report = { static struct zmk_hid_consumer_report consumer_report = {.report_id = 2, .body = {.keys = {0}}}; +static struct zmk_hid_mouse_report mouse_report = { + .report_id = 4, .body = {.buttons = 0, .x = 0, .y = 0, .scroll_x = 0, .scroll_y = 0}}; + // Keep track of how often a modifier was pressed. // Only release the modifier if the count is 0. static int explicit_modifier_counts[8] = {0, 0, 0, 0, 0, 0, 0, 0}; @@ -178,6 +181,85 @@ int zmk_hid_consumer_release(zmk_key_t code) { void zmk_hid_consumer_clear() { memset(&consumer_report.body, 0, sizeof(consumer_report.body)); } +// Keep track of how often a button was pressed. +// Only release the button if the count is 0. +static int explicit_button_counts[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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) { + 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 (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_mod_t i = 0; i < 16; i++) { + if (buttons & (1 << i)) { + zmk_hid_mouse_button_press(i); + } + } + return 0; +} + +int zmk_hid_mouse_buttons_release(zmk_mouse_button_flags_t buttons) { + for (zmk_mod_t i = 0; i < 16; i++) { + if (buttons & (1 << i)) { + zmk_hid_mouse_button_release(i); + } + } + return 0; +} + +void zmk_hid_mouse_movement_set(int16_t x, int16_t y) { + mouse_report.body.x = x; + mouse_report.body.y = y; + LOG_DBG("Mouse movement set to 0x%02X 0x%02X ", mouse_report.body.x, mouse_report.body.y); +} + +void zmk_hid_mouse_movement_update(int16_t x, int16_t y) { + mouse_report.body.x += x; + mouse_report.body.y += y; + LOG_DBG("Mouse movement updated to 0x%02X 0x%02X ", mouse_report.body.x, mouse_report.body.y); +} + +void zmk_hid_mouse_scroll_set(int8_t x, int8_t y) { + mouse_report.body.scroll_x = x; + mouse_report.body.scroll_y = y; + LOG_DBG("Mouse scroll set to 0x%02X 0x%02X ", mouse_report.body.scroll_x, + mouse_report.body.scroll_y); +} + +void zmk_hid_mouse_scroll_update(int8_t x, int8_t y) { + mouse_report.body.scroll_x += x; + mouse_report.body.scroll_y += y; + LOG_DBG("Mouse scroll updated to 0x%02X 0x%02X ", mouse_report.body.scroll_x, + mouse_report.body.scroll_y); +} +void zmk_hid_mouse_clear() { memset(&mouse_report.body, 0, sizeof(mouse_report.body)); } + struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { return &keyboard_report; } @@ -185,3 +267,7 @@ struct zmk_hid_keyboard_report *zmk_hid_get_keyboard_report() { struct zmk_hid_consumer_report *zmk_hid_get_consumer_report() { return &consumer_report; } + +struct zmk_hid_mouse_report *zmk_hid_get_mouse_report() { + return &mouse_report; +} diff --git a/app/src/hid_listener.c b/app/src/hid_listener.c index c0a82c34..0beec5d7 100644 --- a/app/src/hid_listener.c +++ b/app/src/hid_listener.c @@ -11,7 +11,6 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include -#include #include #include #include @@ -92,13 +91,14 @@ static int hid_listener_keycode_released(const struct zmk_keycode_state_changed } int hid_listener(const zmk_event_t *eh) { - const struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); - if (ev) { - if (ev->state) { - hid_listener_keycode_pressed(ev); + const struct zmk_keycode_state_changed *kc_ev = as_zmk_keycode_state_changed(eh); + if (kc_ev) { + if (kc_ev->state) { + hid_listener_keycode_pressed(kc_ev); } else { - hid_listener_keycode_released(ev); + hid_listener_keycode_released(kc_ev); } + return 0; } return 0; } diff --git a/app/src/hog.c b/app/src/hog.c index e8aceca3..e607e8c4 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -56,6 +56,11 @@ static struct hids_report consumer_input = { .type = HIDS_INPUT, }; +static struct hids_report mouse_input = { + .id = 0x04, + .type = HIDS_INPUT, +}; + static bool host_requests_notification = false; static uint8_t ctrl_point; // static uint8_t proto_mode; @@ -93,6 +98,13 @@ static ssize_t read_hids_consumer_input_report(struct bt_conn *conn, sizeof(struct zmk_hid_consumer_report_body)); } +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)); +} + // static ssize_t write_proto_mode(struct bt_conn *conn, // const struct bt_gatt_attr *attr, // const void *buf, uint16_t len, uint16_t offset, @@ -139,6 +151,13 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CCC(input_ccc_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF, BT_GATT_PERM_READ_ENCRYPT, read_hids_report_ref, NULL, &consumer_input), + + BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ_ENCRYPT, read_hids_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_CTRL_POINT, BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE, NULL, write_ctrl_point, &ctrl_point)); @@ -261,6 +280,76 @@ int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { return 0; }; +K_MSGQ_DEFINE(zmk_hog_mouse_msgq, sizeof(struct zmk_hid_mouse_report_body), + CONFIG_ZMK_BLE_MOUSE_REPORT_QUEUE_SIZE, 4); + +void send_mouse_report_callback(struct k_work *work) { + struct zmk_hid_mouse_report_body report; + while (k_msgq_get(&zmk_hog_mouse_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &hog_svc.attrs[13], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err) { + LOG_DBG("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +}; + +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_NO_WAIT); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Mouse message queue full, dropping report"); + return err; + } + 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; +}; + +int zmk_hog_send_mouse_report_direct(struct zmk_hid_mouse_report_body *report) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return 1; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &hog_svc.attrs[13], + .data = report, + .len = sizeof(*report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err) { + LOG_DBG("Error notifying %d", err); + return err; + } + + bt_conn_unref(conn); + + return 0; +}; + int zmk_hog_init(const struct device *_arg) { k_work_q_start(&hog_work_q, hog_q_stack, K_THREAD_STACK_SIZEOF(hog_q_stack), CONFIG_ZMK_BLE_THREAD_PRIORITY); diff --git a/app/src/main.c b/app/src/main.c index ae604a7b..d3b3e578 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -17,6 +17,10 @@ LOG_MODULE_REGISTER(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include +#ifdef CONFIG_ZMK_MOUSE +#include +#endif /* CONFIG_ZMK_MOUSE */ + #define ZMK_KSCAN_DEV DT_LABEL(ZMK_MATRIX_NODE_ID) void main(void) { @@ -29,4 +33,8 @@ void main(void) { #ifdef CONFIG_ZMK_DISPLAY zmk_display_init(); #endif /* CONFIG_ZMK_DISPLAY */ + +#ifdef CONFIG_ZMK_MOUSE + zmk_mouse_init(); +#endif /* CONFIG_ZMK_MOUSE */ } diff --git a/app/src/mouse/Kconfig b/app/src/mouse/Kconfig new file mode 100644 index 00000000..1161b86b --- /dev/null +++ b/app/src/mouse/Kconfig @@ -0,0 +1,38 @@ +# Copyright (c) 2021 The ZMK Contributors +# SPDX-License-Identifier: MIT + +menuconfig ZMK_MOUSE + bool "Enable ZMK mouse emulation" + default n + +config ZMK_MOUSE_TICK_DURATION + int "Mouse tick duration in ms" + default 8 + +if ZMK_MOUSE + +choice ZMK_MOUSE_WORK_QUEUE + prompt "Work queue selection for mouse events" + default ZMK_MOUSE_WORK_QUEUE_DEDICATED + +config ZMK_MOUSE_WORK_QUEUE_SYSTEM + bool "Use default system work queue for mouse events" + +config ZMK_MOUSE_WORK_QUEUE_DEDICATED + bool "Use dedicated work queue for mouse events" + +endchoice + +if ZMK_MOUSE_WORK_QUEUE_DEDICATED + +config ZMK_MOUSE_DEDICATED_THREAD_STACK_SIZE + int "Stack size for dedicated mouse thread/queue" + default 2048 + +config ZMK_MOUSE_DEDICATED_THREAD_PRIORITY + int "Thread priority for dedicated mouse thread/queue" + default 3 + +endif # ZMK_MOUSE_WORK_QUEUE_DEDICATED + +endif diff --git a/app/src/mouse/key_listener.c b/app/src/mouse/key_listener.c new file mode 100644 index 00000000..713d0323 --- /dev/null +++ b/app/src/mouse/key_listener.c @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include +#include + +static struct vector2d move_speed = {0}; +static struct vector2d scroll_speed = {0}; +static struct mouse_config move_config = (struct mouse_config){0}; +static struct mouse_config scroll_config = (struct mouse_config){0}; +static int64_t start_time = 0; + +bool equals(const struct mouse_config *one, const struct mouse_config *other) { + return one->delay_ms == other->delay_ms && + one->time_to_max_speed_ms == other->time_to_max_speed_ms && + one->acceleration_exponent == other->acceleration_exponent; +} + +static void clear_mouse_state(struct k_work *work) { + move_speed = (struct vector2d){0}; + scroll_speed = (struct vector2d){0}; + start_time = 0; + zmk_hid_mouse_movement_set(0, 0); + zmk_hid_mouse_scroll_set(0, 0); + LOG_DBG("Clearing state"); +} + +K_WORK_DEFINE(mouse_clear, &clear_mouse_state); + +void mouse_clear_cb(struct k_timer *dummy) { + k_work_submit_to_queue(zmk_mouse_work_q(), &mouse_clear); +} + +static void mouse_tick_timer_handler(struct k_work *work) { + zmk_hid_mouse_movement_set(0, 0); + zmk_hid_mouse_scroll_set(0, 0); + LOG_DBG("Raising mouse tick event"); + ZMK_EVENT_RAISE( + zmk_mouse_tick(move_speed, scroll_speed, move_config, scroll_config, &start_time)); + zmk_endpoints_send_mouse_report(); +} + +K_WORK_DEFINE(mouse_tick, &mouse_tick_timer_handler); + +void mouse_timer_cb(struct k_timer *dummy) { + LOG_DBG("Submitting mouse work to queue"); + k_work_submit_to_queue(zmk_mouse_work_q(), &mouse_tick); +} + +K_TIMER_DEFINE(mouse_timer, mouse_timer_cb, mouse_clear_cb); + +static int mouse_timer_ref_count = 0; + +void mouse_timer_ref() { + if (mouse_timer_ref_count == 0) { + start_time = k_uptime_get(); + k_timer_start(&mouse_timer, K_NO_WAIT, K_MSEC(CONFIG_ZMK_MOUSE_TICK_DURATION)); + } + mouse_timer_ref_count += 1; +} + +void mouse_timer_unref() { + if (mouse_timer_ref_count > 0) { + mouse_timer_ref_count--; + } + if (mouse_timer_ref_count == 0) { + k_timer_stop(&mouse_timer); + } +} + +static void listener_mouse_move_pressed(const struct zmk_mouse_move_state_changed *ev) { + move_speed.x += ev->max_speed.x; + move_speed.y += ev->max_speed.y; + mouse_timer_ref(); +} + +static void listener_mouse_move_released(const struct zmk_mouse_move_state_changed *ev) { + move_speed.x -= ev->max_speed.x; + move_speed.y -= ev->max_speed.y; + mouse_timer_unref(); +} + +static void listener_mouse_scroll_pressed(const struct zmk_mouse_scroll_state_changed *ev) { + scroll_speed.x += ev->max_speed.x; + scroll_speed.y += ev->max_speed.y; + mouse_timer_ref(); +} + +static void listener_mouse_scroll_released(const struct zmk_mouse_scroll_state_changed *ev) { + scroll_speed.x -= ev->max_speed.x; + scroll_speed.y -= ev->max_speed.y; + mouse_timer_unref(); +} + +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_move_state_changed *mmv_ev = as_zmk_mouse_move_state_changed(eh); + if (mmv_ev) { + if (!equals(&move_config, &(mmv_ev->config))) + move_config = mmv_ev->config; + + if (mmv_ev->state) { + listener_mouse_move_pressed(mmv_ev); + } else { + listener_mouse_move_released(mmv_ev); + } + return 0; + } + const struct zmk_mouse_scroll_state_changed *msc_ev = as_zmk_mouse_scroll_state_changed(eh); + if (msc_ev) { + if (!equals(&scroll_config, &(msc_ev->config))) + scroll_config = msc_ev->config; + if (msc_ev->state) { + listener_mouse_scroll_pressed(msc_ev); + } else { + listener_mouse_scroll_released(msc_ev); + } + return 0; + } + 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); +ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_move_state_changed); +ZMK_SUBSCRIPTION(mouse_listener, zmk_mouse_scroll_state_changed); diff --git a/app/src/mouse/main.c b/app/src/mouse/main.c new file mode 100644 index 00000000..49208a76 --- /dev/null +++ b/app/src/mouse/main.c @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) +K_THREAD_STACK_DEFINE(mouse_work_stack_area, CONFIG_ZMK_MOUSE_DEDICATED_THREAD_STACK_SIZE); +static struct k_work_q mouse_work_q; +#endif + +struct k_work_q *zmk_mouse_work_q() { +#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) + return &mouse_work_q; +#else + return &k_sys_work_q; +#endif +} + +int zmk_mouse_init() { +#if IS_ENABLED(CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED) + k_work_q_start(&mouse_work_q, mouse_work_stack_area, + K_THREAD_STACK_SIZEOF(mouse_work_stack_area), + CONFIG_ZMK_MOUSE_DEDICATED_THREAD_PRIORITY); +#endif + return 0; +} \ No newline at end of file diff --git a/app/src/mouse/tick_listener.c b/app/src/mouse/tick_listener.c new file mode 100644 index 00000000..9c76bd5d --- /dev/null +++ b/app/src/mouse/tick_listener.c @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include +#include + +#include // CLAMP + +#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 + +struct vector2d move_remainder = {0}; +struct vector2d scroll_remainder = {0}; + +static int64_t ms_since_start(int64_t start, int64_t now, int64_t delay) { + 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 mouse_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 > 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 struct vector2d update_movement(struct vector2d *remainder, + const struct mouse_config *config, struct vector2d max_speed, + int64_t now, int64_t *start_time) { + struct vector2d move = {0}; + if (max_speed.x == 0 && max_speed.y == 0) { + *remainder = (struct vector2d){0}; + return move; + } + + int64_t move_duration = ms_since_start(*start_time, now, config->delay_ms); + move = (struct vector2d){ + .x = speed(config, max_speed.x, move_duration) * CONFIG_ZMK_MOUSE_TICK_DURATION / 1000, + .y = speed(config, max_speed.y, move_duration) * CONFIG_ZMK_MOUSE_TICK_DURATION / 1000, + }; + + track_remainder(&(move.x), &(remainder->x)); + track_remainder(&(move.y), &(remainder->y)); + + return move; +} + +static void mouse_tick_handler(const struct zmk_mouse_tick *tick) { + struct vector2d move = update_movement(&move_remainder, &(tick->move_config), tick->max_move, + tick->timestamp, tick->start_time); + zmk_hid_mouse_movement_update((int16_t)CLAMP(move.x, INT16_MIN, INT16_MAX), + (int16_t)CLAMP(move.y, INT16_MIN, INT16_MAX)); + struct vector2d scroll = update_movement(&scroll_remainder, &(tick->scroll_config), + tick->max_scroll, tick->timestamp, tick->start_time); + zmk_hid_mouse_scroll_update((int8_t)CLAMP(scroll.x, INT8_MIN, INT8_MAX), + (int8_t)CLAMP(scroll.y, INT8_MIN, INT8_MAX)); +} + +int zmk_mouse_tick_listener(const zmk_event_t *eh) { + const struct zmk_mouse_tick *tick = as_zmk_mouse_tick(eh); + if (tick) { + mouse_tick_handler(tick); + return 0; + } + return 0; +} + +ZMK_LISTENER(zmk_mouse_tick_listener, zmk_mouse_tick_listener); +ZMK_SUBSCRIPTION(zmk_mouse_tick_listener, zmk_mouse_tick); \ No newline at end of file diff --git a/app/src/mouse/trackball_pim447.c b/app/src/mouse/trackball_pim447.c new file mode 100644 index 00000000..36374be9 --- /dev/null +++ b/app/src/mouse/trackball_pim447.c @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2021 Cedric VINCENT + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(PIM447, CONFIG_SENSOR_LOG_LEVEL); + +#define MOVE_FACTOR DT_PROP(DT_INST(0, pimoroni_trackball_pim447), move_factor) +#define MOVE_X_INVERT DT_PROP(DT_INST(0, pimoroni_trackball_pim447), invert_move_x) +#define MOVE_Y_INVERT DT_PROP(DT_INST(0, pimoroni_trackball_pim447), invert_move_y) +#define MOVE_X_FACTOR (MOVE_FACTOR * (MOVE_X_INVERT ? -1 : 1)) +#define MOVE_Y_FACTOR (MOVE_FACTOR * (MOVE_Y_INVERT ? -1 : 1)) + +#define SCROLL_DIVISOR DT_PROP(DT_INST(0, pimoroni_trackball_pim447), scroll_divisor) +#define SCROLL_X_INVERT DT_PROP(DT_INST(0, pimoroni_trackball_pim447), invert_scroll_x) +#define SCROLL_Y_INVERT DT_PROP(DT_INST(0, pimoroni_trackball_pim447), invert_scroll_y) +#define SCROLL_X_DIVISOR (SCROLL_DIVISOR * (SCROLL_X_INVERT ? -1 : 1)) +#define SCROLL_Y_DIVISOR (SCROLL_DIVISOR * (SCROLL_Y_INVERT ? 1 : -1)) + +#define BUTTON DT_PROP(DT_INST(0, pimoroni_trackball_pim447), button) +#define SWAP_AXES DT_PROP(DT_INST(0, pimoroni_trackball_pim447), swap_axes) + +static int mode = DT_PROP(DT_INST(0, pimoroni_trackball_pim447), mode); + +void zmk_trackball_pim447_set_mode(int new_mode) +{ + switch (new_mode) { + case PIM447_MOVE: + case PIM447_SCROLL: + mode = new_mode; + break; + + case PIM447_TOGGLE: + mode = mode == PIM447_MOVE + ? PIM447_SCROLL + : PIM447_MOVE; + break; + + default: + break; + } +} + +/* + * It feels more natural and more confortable to convert the speed + * reported by the PIM447 trackball. + */ +static int16_t convert_speed(int32_t value) +{ + bool negative = (value < 0); + + if (negative) { + value = -value; + } + + switch (value) { + case 0: value = 0; break; + case 1: value = 1; break; + case 2: value = 4; break; + case 3: value = 8; break; + case 4: value = 18; break; + case 5: value = 32; break; + case 6: value = 50; break; + case 7: value = 72; break; + case 8: value = 98; break; + default: value = 127; break; + } + + if (negative) { + value = -value; + } + + return value; +} + +static void thread_code(void *p1, void *p2, void *p3) +{ + const struct device *dev; + int result; + + /* PIM447 trackball initialization. */ + + const char *label = DT_LABEL(DT_INST(0, pimoroni_trackball_pim447)); + dev = device_get_binding(label); + if (dev == NULL) { + LOG_ERR("Cannot get TRACKBALL_PIM447 device"); + return; + } + + /* Event loop. */ + + bool button_press_sent = false; + bool button_release_sent = false; + + while (true) { + struct sensor_value pos_dx, pos_dy, pos_dz; + bool send_report = false; + int clear = PIM447_NONE; + + result = sensor_sample_fetch(dev); + if (result < 0) { + LOG_ERR("Failed to fetch TRACKBALL_PIM447 sample"); + return; + } + + result = sensor_channel_get(dev, SENSOR_CHAN_POS_DX, &pos_dx); + if (result < 0) { + LOG_ERR("Failed to get TRACKBALL_PIM447 pos_dx channel value"); + return; + } + + result = sensor_channel_get(dev, SENSOR_CHAN_POS_DY, &pos_dy); + if (result < 0) { + LOG_ERR("Failed to get TRACKBALL_PIM447 pos_dy channel value"); + return; + } + + result = sensor_channel_get(dev, SENSOR_CHAN_POS_DZ, &pos_dz); + if (result < 0) { + LOG_ERR("Failed to get TRACKBALL_PIM447 pos_dz channel value"); + return; + } + + if (pos_dx.val1 != 0 || pos_dy.val1 != 0) { + if (SWAP_AXES) { + int32_t tmp = pos_dx.val1; + pos_dx.val1 = pos_dy.val1; + pos_dy.val1 = tmp; + } + + switch(mode) { + default: + case PIM447_MOVE: { + int dx = convert_speed(pos_dx.val1) * MOVE_X_FACTOR; + int dy = convert_speed(pos_dy.val1) * MOVE_Y_FACTOR; + zmk_hid_mouse_movement_set(dx, dy); + send_report = true; + clear = PIM447_MOVE; + break; + } + + case PIM447_SCROLL: { + int dx = pos_dx.val1 / SCROLL_X_DIVISOR; + int dy = pos_dy.val1 / SCROLL_Y_DIVISOR; + zmk_hid_mouse_scroll_set(dx, dy); + send_report = true; + clear = PIM447_SCROLL; + break; + } + } + } + + if (pos_dz.val1 == 0x80 && button_press_sent == false) { + zmk_hid_mouse_button_press(BUTTON); + button_press_sent = true; + button_release_sent = false; + send_report = true; + } else if (pos_dz.val1 == 0x01 && button_release_sent == false) { + zmk_hid_mouse_button_release(BUTTON); + button_press_sent = false; + button_release_sent = true; + send_report = true; + } + + if (send_report) { + zmk_endpoints_send_mouse_report(); + + switch (clear) { + case PIM447_MOVE: zmk_hid_mouse_movement_set(0, 0); break; + case PIM447_SCROLL: zmk_hid_mouse_scroll_set(0, 0); break; + default: break; + } + } + + k_sleep(K_MSEC(10)); + } +} + +#define STACK_SIZE 1024 + +static K_THREAD_STACK_DEFINE(thread_stack, STACK_SIZE); +static struct k_thread thread; + +int zmk_trackball_pim447_init() +{ + k_thread_create(&thread, thread_stack, STACK_SIZE, thread_code, + NULL, NULL, NULL, K_PRIO_PREEMPT(8), 0, K_NO_WAIT); + return 0; +} + +SYS_INIT(zmk_trackball_pim447_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/tests/mouse-keys/mmv/events.patterns b/app/tests/mouse-keys/mmv/events.patterns new file mode 100644 index 00000000..833100f6 --- /dev/null +++ b/app/tests/mouse-keys/mmv/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p \ No newline at end of file diff --git a/app/tests/mouse-keys/mmv/keycode_events.snapshot b/app/tests/mouse-keys/mmv/keycode_events.snapshot new file mode 100644 index 00000000..259501ba --- /dev/null +++ b/app/tests/mouse-keys/mmv/keycode_events.snapshot @@ -0,0 +1,2 @@ +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 diff --git a/app/tests/mouse-keys/mmv/native_posix.keymap b/app/tests/mouse-keys/mmv/native_posix.keymap new file mode 100644 index 00000000..ecf06601 --- /dev/null +++ b/app/tests/mouse-keys/mmv/native_posix.keymap @@ -0,0 +1,26 @@ +#include +#include +#include +#include + +/ { + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mmv MOVE_LEFT &none + &none &none + >; + }; + }; +}; + + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/docs/docs/behaviors/mouse-emulation.md b/docs/docs/behaviors/mouse-emulation.md new file mode 100644 index 00000000..efe095e7 --- /dev/null +++ b/docs/docs/behaviors/mouse-emulation.md @@ -0,0 +1,110 @@ +--- +title: Mouse Emulation Behaviors +sidebar_label: Mouse Emulation +--- + +## Summary + +Mouse emulation behaviors send mouse movements, button presses or scroll actions. + +Please view [`dt-bindings/zmk/mouse.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/mouse.h) for a comprehensive list of signals. + +## Configuration options + +This feature should be enabled via a config option: + +``` +CONFIG_ZMK_MOUSE=y +``` + +This option enables several others. + +### Dedicated thread processing + +`CONFIG_ZMK_MOUSE_WORK_QUEUE_DEDICATED` is enabled by default and separates the processing of mouse signals into a dedicated thread, significantly improving performance. + +### Tick rate configuration + +`CONFIG_ZMK_MOUSE_TICK_DURATION` sets the tick rate for mouse polling. It is set to 8 ms. by default. + +## Keycode Defines + +To make it easier to encode the HID keycode numeric values, most keymaps include +the [`dt-bindings/zmk/mouse.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/mouse.h) header +provided by ZMK near the top: + +``` +#include +``` + +Doing so allows using a set of defines such as `MOVE_UP`, `MOVE_DOWN`, `LCLK` and `SCROLL_UP` with these behaviors. + +## Mouse Button Press + +This behavior can press/release up to 16 mouse buttons. + +### Behavior Binding + +- Reference: `&mkp` +- Parameter: A `uint16` with each bit referring to a button. + +Example: + +``` +&mkp LCLK +``` + +## Mouse Movement + +This behavior is used to manipulate the cursor. + +### Behavior Binding + +- Reference: `&mmv` +- Parameter: A `uint32` with the first 16 bits relating to horizontal movement + and the last 16 - to vertical movement. + +Example: + +``` +&mmv MOVE_UP +``` + +## Mouse Scrolling + +This behaviour is used to scroll, both horizontally and vertically. + +### Behavior Binding + +- Reference: `&mwh` +- Parameter: A `uint16` with the first 8 bits relating to horizontal movement + and the last 8 - to vertical movement. + +Example: + +``` +&mwh SCROLL_UP +``` + +## Acceleration + +Both mouse movement and scrolling have independently configurable acceleration profiles with three parameters: delay before movement, time to max speed and the acceleration exponent. +The exponent is usually set to 0 for constant speed, 1 for uniform acceleration or 2 for uniform jerk. + +These profiles can be configured inside your keymap: + +``` +&mmv { + time-to-max-speed-ms = <500>; +}; + +&mwh { + acceleration-exponent=<1>; +}; + +/ { + keymap { + ... + }; +}; +``` diff --git a/docs/docs/behaviors/trackball-pim447.md b/docs/docs/behaviors/trackball-pim447.md new file mode 100644 index 00000000..af28eb23 --- /dev/null +++ b/docs/docs/behaviors/trackball-pim447.md @@ -0,0 +1,33 @@ +--- +title: Trackball PIM447 Behaviors +sidebar_label: Trackball PIM447 +--- + +The mode of the trackball PIM447 driver can be changed using the +following key behaviors. + +- `&pim447_move`: set trackball mode to "move" . +- `&pim447_scroll`: set trackball mode to "scroll" . +- `&pim447_toggle`: toggle trackball mode (from "scroll" to "move", or vice versa). +- `&pim447_move_scroll`: set trackball mode to "move" when pressed, then to "scroll" when released. +- `&pim447_scroll_move`: set trackball mode to "scroll" when pressed, then to "move" when released. +- `&pim447_toggle_toggle`: toggle trackball mode when pressed, then toggle again when released. + +Example: + +``` +#include + +/ { + keymap { + compatible = "zmk,keymap"; + + a_layer { + bindings = <&kp a + &kp b + &kp c + &pim447_toggle>; + }; + }; +}; +``` diff --git a/docs/docs/features/trackballs.md b/docs/docs/features/trackballs.md new file mode 100644 index 00000000..e489c090 --- /dev/null +++ b/docs/docs/features/trackballs.md @@ -0,0 +1,71 @@ +--- +title: Trackballs +sidebar_label: Trackballs +--- + +Only the [Pimoroni PIM447](https://shop.pimoroni.com/products/trackball-breakout) +trackball is supported in ZMK so far. + +## Enable Trackball Support + +To enable support for PIM447 trackball — and to make it acts like a +mouse — add the two following lines to the shield `.config` file: + +```ini +CONFIG_ZMK_MOUSE=y +CONFIG_ZMK_TRACKBALL_PIM447=y +``` + +Assuming the PIM447 trackball is connected to the `pro_micro_i2c` pins +and its bus address hasn't been changed, then the following lines must +be added to the shield `.overlay` file: + +```devicetree +&pro_micro_i2c { + status = "okay"; + + trackball_pim447@a { + compatible = "pimoroni,trackball_pim447"; + reg = <0xa>; + label = "TRACKBALL_PIM447"; + }; +}; +``` + +## Customize Trackball Driver Properties + +The PIM447 trackball driver can be configured according to a lot of +different properties that are listed in the shield `.overlay` sample +below: + +```devicetree +#include // for PIM447_MOVE and PIM447_SCROLL constants. + +&pro_micro_i2c { + status = "okay"; + + trackball_pim447@a { + compatible = "pimoroni,trackball_pim447"; + reg = <0xa>; + label = "TRACKBALL_PIM447"; + + move-factor = <2>; // Increase pointer velocity (default: 1) + invert-move-x; // Invert pointer X axis (left is right, and vice versa) + invert-move-y; // Invert pointer Y axis (up is down, and vice versa) + button = <1>; // Send right-click when pressing the ball (default: 0, ie. left-click) + swap-axes; // Swap X and Y axes (horizontal is vertical, and vice versa) + scroll-divisor = <1>; // Increase wheel velocity (default: 2) + invert-scroll-x; // Invert wheel X axis (left is right, and vice versa) + invert-scroll-y; // Invert wheel Y axis (up is down, and vice versa) + mode = ; // Act as mouse wheels at startup (default: PIM447_MOVE for a pointer) + }; +}; +``` + +## Change Trackball Driver Mode + +As seen at the end of the previous section, the PIM447 trackball +driver can act as a mouse pointer or as mouse wheels according to the +`mode` specified in the shield `.overlay` file. It can also be +changed dynamically using [dedicated key +behaviors](/docs/behaviors/trackball-pim447). diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 39b53245..ad89991e 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -14,36 +14,34 @@ ZMK is currently missing some features found in other popular firmware. This tab | Legend: | ✅ Supported | 🚧 Under Development | 💡 Planned | | :------ | :----------- | :------------------- | :--------- | -| **Feature** | ZMK | BlueMicro | QMK | -| ---------------------------------------------------------------------------------------------------------------------------------- | :-: | :-------: | :-: | -| Low Latency BLE Support | ✅ | ✅ | | -| Multi-Device BLE Support | ✅ | | | -| [USB Connectivity](behaviors/outputs.md) | ✅ | ✅ | ✅ | -| User Configuration Repositories | ✅ | | | -| Split Keyboard Support | ✅ | ✅ | ✅ | -| [Keymaps and Layers](behaviors/layers.md) | ✅ | ✅ | ✅ | -| [Hold-Tap](behaviors/hold-tap.md) (which includes [Mod-Tap](behaviors/mod-tap.md) and [Layer-Tap](behaviors/layers.md/#layer-tap)) | ✅ | ✅ | ✅ | -| [Tap-Dance](behaviors/tap-dance.md) | ✅ | ✅[^3] | ✅ | -| [Keyboard Codes](codes/index.mdx#keyboard) | ✅ | ✅ | ✅ | -| [Media](codes/index.mdx#media-controls) & [Consumer](codes/index.mdx#consumer-controls) Codes | ✅ | ✅ | ✅ | -| [Encoders](features/encoders.md)[^1] | ✅ | ✅ | ✅ | -| [Display Support](features/displays.md)[^2] | 🚧 | 🚧 | ✅ | -| [RGB Underglow](features/underglow.md) | ✅ | ✅ | ✅ | -| [Backlight](features/backlight.md) | ✅ | ✅ | ✅ | -| One Shot Keys | ✅ | ✅ | ✅ | -| [Combo Keys](features/combos.md) | ✅ | | ✅ | -| Macros | 🚧 | ✅ | ✅ | -| Mouse Keys | 🚧 | ✅ | ✅ | -| Low Active Power Usage | ✅ | | | -| Low Power Sleep States | ✅ | ✅ | | -| [Low Power Mode (VCC Shutoff)](behaviors/power.md) | ✅ | ✅ | | -| Battery Reporting | ✅ | ✅ | | -| Shell over BLE | 💡 | | | -| Realtime Keymap Updating | 💡 | | ✅ | -| AVR/8 Bit | | | ✅ | -| [Wide Range of ARM Chips Supported](https://docs.zephyrproject.org/latest/boards/index.html) | ✅ | | | +| **Feature** | ZMK | BlueMicro | QMK | +| ------------------------------------------------------------------------------------------------------------------------- | :-: | :-------: | :-: | +| Low Latency BLE Support | ✅ | ✅ | | +| Multi-Device BLE Support | ✅ | | | +| [USB Connectivity](behaviors/outputs) | ✅ | ✅ | ✅ | +| User Configuration Repositories | ✅ | | | +| Split Keyboard Support | ✅ | ✅ | ✅ | +| [Keymaps and Layers](behaviors/layers) | ✅ | ✅ | ✅ | +| [Hold-Tap](behaviors/hold-tap) (which includes [Mod-Tap](behaviors/mod-tap) and [Layer-Tap](behaviors/layers/#layer-tap)) | ✅ | ✅ | ✅ | +| [Keyboard Codes](codes/#keyboard) | ✅ | ✅ | ✅ | +| [Media](codes/#media-controls) & [Consumer](codes/#consumer-controls) Codes | ✅ | ✅ | ✅ | +| [Encoders](features/encoders)[^1] | ✅ | ✅ | ✅ | +| [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ | +| [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ | +| One Shot Keys | ✅ | ✅ | ✅ | +| [Combo Keys](features/combos) | ✅ | | ✅ | +| Macros | 🚧 | ✅ | ✅ | +| Mouse Keys | ✅ | ✅ | ✅ | +| Low Active Power Usage | ✅ | | | +| Low Power Sleep States | ✅ | ✅ | | +| [Low Power Mode (VCC Shutoff)](behaviors/power) | ✅ | ✅ | | +| Battery Reporting | ✅ | ✅ | | +| Shell over BLE | 💡 | | | +| Realtime Keymap Updating | 💡 | | ✅ | +| AVR/8 Bit | | | ✅ | +| [Wide Range of ARM Chips Supported](https://docs.zephyrproject.org/latest/boards/index.html) | ✅ | | | +| [Trackballs](features/trackballs) | ✅ | | ✅ | -[^3]: Tap-Dances are limited to single and double-tap on BlueMicro [^2]: Encoders are not currently supported on peripheral side splits. [^1]: OLEDs are currently proof of concept in ZMK. diff --git a/docs/sidebars.js b/docs/sidebars.js index 7db3e055..2fb74b8e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -16,6 +16,7 @@ module.exports = { "features/displays", "features/encoders", "features/underglow", + "features/trackballs", "features/backlight", "features/beta-testing", ], @@ -28,6 +29,7 @@ module.exports = { "behaviors/mod-morph", "behaviors/sticky-key", "behaviors/sticky-layer", + "behaviors/mouse-emulation", "behaviors/tap-dance", "behaviors/caps-word", "behaviors/key-repeat", @@ -37,6 +39,7 @@ module.exports = { "behaviors/underglow", "behaviors/backlight", "behaviors/power", + "behaviors/trackball-pim447", ], Codes: [ "codes/index",