├── Kconfig ├── .clang-format ├── zephyr └── module.yml ├── CMakeLists.txt ├── .gitignore ├── compile_tests ├── config │ ├── boards │ │ ├── arm │ │ │ └── waveshare_rp2040_zero │ │ │ │ ├── board.cmake │ │ │ │ ├── Kconfig.board │ │ │ │ ├── waveshare_rp2040_zero.zmk.yml │ │ │ │ ├── Kconfig.defconfig │ │ │ │ ├── waveshare_rp2040_zero.yaml │ │ │ │ ├── waveshare_rp2040_zero_defconfig │ │ │ │ ├── waveshare_rp2040_zero_pinctrl.dtsi │ │ │ │ └── waveshare_rp2040_zero.dts │ │ └── shields │ │ │ ├── ble │ │ │ ├── ble01.keymap │ │ │ ├── ble02.keymap │ │ │ ├── ble02_left.overlay │ │ │ ├── ble01_left.overlay │ │ │ ├── Kconfig.shield │ │ │ ├── Kconfig.defconfig │ │ │ ├── ble01.dtsi │ │ │ ├── ble02.dtsi │ │ │ ├── ble01_right.overlay │ │ │ ├── ble02_right.overlay │ │ │ └── ble-layouts.dtsi │ │ │ └── usbonly │ │ │ ├── usbonly01.keymap │ │ │ ├── usbonly02.keymap │ │ │ ├── usbonly01_left.overlay │ │ │ ├── Kconfig.shield │ │ │ ├── usbonly02_left.overlay │ │ │ ├── Kconfig.defconfig │ │ │ ├── usbonly01.dtsi │ │ │ ├── usbonly02.dtsi │ │ │ ├── usbonly01_right.overlay │ │ │ ├── usbonly02_right.overlay │ │ │ └── usbonly-layouts.dtsi │ ├── usbonly01.conf │ ├── usbonly02.conf │ ├── ble01.conf │ ├── ble02.conf │ └── west.yml ├── build.yaml └── README.md ├── dts ├── behaviors │ └── input_processor_gestures.dtsi └── bindings │ └── zmk,input-processor-gestures.yaml ├── src ├── Kconfig ├── CMakeLists.txt ├── tap_detection.h ├── inertial_cursor.h ├── circular_scroll.h ├── touch_detection.h ├── input_processor_gestures.h ├── tap_detection.c ├── touch_detection.c ├── inertial_cursor.c ├── circular_scroll.c └── input_processor_gestures.c └── README.md /Kconfig: -------------------------------------------------------------------------------- 1 | rsource "src/Kconfig" 2 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | IndentWidth: 4 3 | ColumnLimit: 100 4 | SortIncludes: false 5 | -------------------------------------------------------------------------------- /zephyr/module.yml: -------------------------------------------------------------------------------- 1 | build: 2 | cmake: . 3 | kconfig: Kconfig 4 | settings: 5 | dts_root: . 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | add_subdirectory(src) 3 | zephyr_include_directories(${APPLICATION_SOURCE_DIR}/include) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .west 2 | build 3 | zephyr 4 | zmk 5 | modules 6 | cirque-input-module 7 | zmk-input-gestures 8 | zmk-input-processors 9 | -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/board.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | include(${ZEPHYR_BASE}/boards/common/uf2.board.cmake) 5 | -------------------------------------------------------------------------------- /dts/behaviors/input_processor_gestures.dtsi: -------------------------------------------------------------------------------- 1 | 2 | / { 3 | zip_gestures: zip_gestures { 4 | status = "okay"; 5 | #input-processor-cells = <0>; 6 | compatible = "zmk,input-processor-gestures"; 7 | }; 8 | }; -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_WAVESHARE_RP2040_ZERO 5 | bool "Waveshare RP2040 Zero Board" 6 | depends on SOC_RP2040 7 | -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/waveshare_rp2040_zero.zmk.yml: -------------------------------------------------------------------------------- 1 | file_format: "1" 2 | id: waveshare_rp2040_zero 3 | name: RP2040-Zero 4 | type: board 5 | arch: arm 6 | outputs: 7 | - usb 8 | url: https://www.waveshare.com/rp2040-zero.htm 9 | exposes: [waveshare_rp2040_zero] 10 | -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_WAVESHARE_RP2040_ZERO 5 | 6 | config BOARD 7 | default "waveshare_rp2040_zero" 8 | 9 | config RP2_FLASH_W25Q080 10 | default y 11 | 12 | endif # BOARD_WAVESHARE_RP2040_ZERO 13 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble01.keymap: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | / { 8 | keymap { 9 | compatible = "zmk,keymap"; 10 | LOL { 11 | display-name = "lol"; 12 | bindings = < &bootloader >; 13 | }; 14 | }; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble02.keymap: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | / { 8 | keymap { 9 | compatible = "zmk,keymap"; 10 | LOL { 11 | display-name = "lol"; 12 | bindings = < &bootloader >; 13 | }; 14 | }; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly01.keymap: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | / { 8 | keymap { 9 | compatible = "zmk,keymap"; 10 | LOL { 11 | display-name = "lol"; 12 | bindings = < &bootloader >; 13 | }; 14 | }; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly02.keymap: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | / { 8 | keymap { 9 | compatible = "zmk,keymap"; 10 | LOL { 11 | display-name = "lol"; 12 | bindings = < &bootloader >; 13 | }; 14 | }; 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble02_left.overlay: -------------------------------------------------------------------------------- 1 | #include "ble02.dtsi" 2 | 3 | &kscan0 { 4 | col-gpios 5 | = <&gpio0 2 GPIO_ACTIVE_HIGH> 6 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 7 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 8 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 9 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 10 | , <&gpio0 7 GPIO_ACTIVE_HIGH> 11 | ; 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly01_left.overlay: -------------------------------------------------------------------------------- 1 | #include "usbonly01.dtsi" 2 | 3 | &kscan0 { 4 | col-gpios 5 | = <&gpio0 2 GPIO_ACTIVE_HIGH> 6 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 7 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 8 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 9 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 10 | , <&gpio0 7 GPIO_ACTIVE_HIGH> 11 | ; 12 | }; 13 | -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/waveshare_rp2040_zero.yaml: -------------------------------------------------------------------------------- 1 | identifier: waveshare_rp2040_zero 2 | name: RP2040-Zero 3 | type: mcu 4 | arch: arm 5 | flash: 2048 6 | ram: 264 7 | toolchain: 8 | - zephyr 9 | - gnuarmemb 10 | - xtools 11 | supported: 12 | - uart 13 | - gpio 14 | - adc 15 | - i2c 16 | - spi 17 | - hwinfo 18 | - watchdog 19 | - flash 20 | - dma 21 | - counter 22 | -------------------------------------------------------------------------------- /compile_tests/config/usbonly01.conf: -------------------------------------------------------------------------------- 1 | 2 | CONFIG_ZMK_LOG_LEVEL_DBG=n 3 | 4 | CONFIG_LOG_PROCESS_THREAD_STARTUP_DELAY_MS=2000 5 | CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY=20 6 | CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO=20 7 | 8 | CONFIG_ZMK_USB_BOOT=y 9 | CONFIG_USB_DEVICE_REMOTE_WAKEUP=y 10 | CONFIG_ZMK_SPLIT_WIRED=y 11 | CONFIG_ZMK_SPLIT_BLE=n 12 | CONFIG_I2C=y 13 | 14 | CONFIG_MAIN_STACK_SIZE=4096 15 | CONFIG_INPUT_THREAD_STACK_SIZE=4096 -------------------------------------------------------------------------------- /compile_tests/config/usbonly02.conf: -------------------------------------------------------------------------------- 1 | 2 | CONFIG_ZMK_LOG_LEVEL_DBG=n 3 | 4 | CONFIG_LOG_PROCESS_THREAD_STARTUP_DELAY_MS=2000 5 | CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY=20 6 | CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO=20 7 | 8 | CONFIG_ZMK_USB_BOOT=y 9 | CONFIG_USB_DEVICE_REMOTE_WAKEUP=y 10 | CONFIG_ZMK_SPLIT_WIRED=y 11 | CONFIG_ZMK_SPLIT_BLE=n 12 | CONFIG_I2C=y 13 | 14 | CONFIG_MAIN_STACK_SIZE=4096 15 | CONFIG_INPUT_THREAD_STACK_SIZE=4096 -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble01_left.overlay: -------------------------------------------------------------------------------- 1 | #include "ble01.dtsi" 2 | 3 | &kscan0 { 4 | col-gpios 5 | = <&gpio0 2 GPIO_ACTIVE_HIGH> 6 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 7 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 8 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 9 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 10 | , <&gpio0 7 GPIO_ACTIVE_HIGH> 11 | ; 12 | }; 13 | 14 | 15 | &glidepoint_listener { 16 | status = "disabled"; 17 | }; -------------------------------------------------------------------------------- /compile_tests/config/ble01.conf: -------------------------------------------------------------------------------- 1 | 2 | CONFIG_ZMK_LOG_LEVEL_DBG=n 3 | 4 | CONFIG_LOG_PROCESS_THREAD_STARTUP_DELAY_MS=2000 5 | CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY=20 6 | CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO=20 7 | 8 | CONFIG_BT=y 9 | CONFIG_ZMK_BLE=y 10 | 11 | CONFIG_ZMK_USB=n 12 | CONFIG_ZMK_USB_BOOT=n 13 | CONFIG_USB_DEVICE_REMOTE_WAKEUP=y 14 | CONFIG_ZMK_SPLIT_WIRED=n 15 | CONFIG_ZMK_SPLIT_BLE=y 16 | CONFIG_I2C=n 17 | CONFIG_SPI=y 18 | 19 | CONFIG_MAIN_STACK_SIZE=4096 20 | CONFIG_INPUT_THREAD_STACK_SIZE=4096 -------------------------------------------------------------------------------- /compile_tests/config/ble02.conf: -------------------------------------------------------------------------------- 1 | 2 | CONFIG_ZMK_LOG_LEVEL_DBG=n 3 | 4 | CONFIG_LOG_PROCESS_THREAD_STARTUP_DELAY_MS=2000 5 | CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY=20 6 | CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO=20 7 | 8 | CONFIG_BT=y 9 | CONFIG_ZMK_BLE=y 10 | 11 | CONFIG_ZMK_USB=n 12 | CONFIG_ZMK_USB_BOOT=n 13 | CONFIG_USB_DEVICE_REMOTE_WAKEUP=y 14 | CONFIG_ZMK_SPLIT_WIRED=n 15 | CONFIG_ZMK_SPLIT_BLE=y 16 | CONFIG_I2C=n 17 | CONFIG_SPI=y 18 | 19 | CONFIG_MAIN_STACK_SIZE=4096 20 | CONFIG_INPUT_THREAD_STACK_SIZE=4096 -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/Kconfig.shield: -------------------------------------------------------------------------------- 1 | config SHIELD_BLE01_LEFT 2 | def_bool $(shields_list_contains,ble01_left) 3 | 4 | # No whitespace after the comma or in your part name! 5 | config SHIELD_BLE01_RIGHT 6 | def_bool $(shields_list_contains,ble01_right) 7 | 8 | config SHIELD_BLE02_LEFT 9 | def_bool $(shields_list_contains,ble02_left) 10 | 11 | # No whitespace after the comma or in your part name! 12 | config SHIELD_BLE02_RIGHT 13 | def_bool $(shields_list_contains,ble02_right) 14 | 15 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/Kconfig.shield: -------------------------------------------------------------------------------- 1 | config SHIELD_USBONLY01_LEFT 2 | def_bool $(shields_list_contains,usbonly01_left) 3 | 4 | # No whitespace after the comma or in your part name! 5 | config SHIELD_USBONLY01_RIGHT 6 | def_bool $(shields_list_contains,usbonly01_right) 7 | 8 | config SHIELD_USBONLY02_LEFT 9 | def_bool $(shields_list_contains,usbonly02_left) 10 | 11 | # No whitespace after the comma or in your part name! 12 | config SHIELD_USBONLY02_RIGHT 13 | def_bool $(shields_list_contains,usbonly02_right) 14 | 15 | -------------------------------------------------------------------------------- /src/Kconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | DT_COMPAT_ZMK_INPUT_PROCESSOR_GESTURES := zmk,input-processor-gestures 5 | 6 | config ZMK_INPUT_PROCESSOR_GESTURES 7 | bool 8 | default $(dt_compat_enabled,$(DT_COMPAT_ZMK_INPUT_PROCESSOR_GESTURES)) 9 | depends on ZMK_POINTING 10 | depends on (!ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL) 11 | 12 | config INPUT_GESTURES_INIT_PRIORITY 13 | int "Touchpad gestures initialization priority" 14 | default INPUT_INIT_PRIORITY 15 | depends on ZMK_INPUT_PROCESSOR_GESTURES 16 | 17 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | if (ZMK_INPUT_PROCESSOR_GESTURES AND NOT CONFIG_ZMK_POINTING) 5 | message( FATAL_ERROR "You don't have ZMK_POINTING configured, but zmk_input_gestures depends on it." ) 6 | endif() 7 | 8 | 9 | if (CONFIG_ZMK_INPUT_PROCESSOR_GESTURES) 10 | zephyr_library() 11 | 12 | zephyr_library_sources(input_processor_gestures.c) 13 | zephyr_library_sources(tap_detection.c) 14 | zephyr_library_sources(touch_detection.c) 15 | zephyr_library_sources(circular_scroll.c) 16 | zephyr_library_sources(inertial_cursor.c) 17 | endif() -------------------------------------------------------------------------------- /src/tap_detection.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "input_processor_gestures.h" 10 | 11 | struct tap_detection_data { 12 | bool is_waiting_for_tap; 13 | struct k_work_delayable tap_timeout_work; 14 | gesture_data *all; 15 | }; 16 | 17 | struct tap_detection_config { 18 | const bool enabled; 19 | const bool prevent_movement_during_tap; 20 | const uint8_t tap_timout_ms; 21 | }; 22 | 23 | handle_init_t tap_detection_init; 24 | handle_touch_t tap_detection_handle_start; 25 | handle_touch_t tap_detection_handle_touch; 26 | 27 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly02_left.overlay: -------------------------------------------------------------------------------- 1 | #include "usbonly02.dtsi" 2 | 3 | &kscan0 { 4 | col-gpios 5 | = <&gpio0 2 GPIO_ACTIVE_HIGH> 6 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 7 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 8 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 9 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 10 | , <&gpio0 7 GPIO_ACTIVE_HIGH> 11 | ; 12 | }; 13 | 14 | 15 | &i2c0_default { 16 | group1 { 17 | pinmux = , ; 18 | input-enable; 19 | input-schmitt-enable; 20 | }; 21 | status = "okay"; 22 | }; 23 | 24 | &glidepoint_listener { 25 | status = "okay"; 26 | }; -------------------------------------------------------------------------------- /src/inertial_cursor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "input_processor_gestures.h" 4 | 5 | struct inertial_cursor_data { 6 | struct k_work_delayable inertial_work; 7 | uint16_t previous_x, previous_y; 8 | double delta_x, delta_y; 9 | uint32_t delta_time; 10 | double velocity_decay; 11 | gesture_data *all; 12 | }; 13 | 14 | struct inertial_cursor_config { 15 | const bool enabled; 16 | const uint16_t velocity_threshold; 17 | const uint8_t decay_percent; 18 | }; 19 | 20 | handle_init_t inertial_cursor_init; 21 | handle_touch_t inertial_cursor_handle_touch_start; 22 | handle_touch_t inertial_cursor_handle_touch; 23 | handle_touch_end_t inertial_cursor_handle_end; 24 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | if SHIELD_USBONLY01_RIGHT || SHIELD_USBONLY02_LEFT 2 | 3 | config ZMK_SPLIT_ROLE_CENTRAL 4 | default y 5 | 6 | endif 7 | 8 | if SHIELD_USBONLY01_LEFT || SHIELD_USBONLY01_RIGHT 9 | config ZMK_SPLIT 10 | default y 11 | 12 | config ZMK_KEYBOARD_NAME 13 | default "UsbOnly01" 14 | 15 | config ZMK_POINTING 16 | default y 17 | 18 | 19 | config I2C 20 | default y 21 | 22 | endif 23 | 24 | if SHIELD_USBONLY02_LEFT || SHIELD_USBONLY02_RIGHT 25 | config ZMK_SPLIT 26 | default y 27 | 28 | config ZMK_KEYBOARD_NAME 29 | default "UsbOnly02" 30 | 31 | config ZMK_POINTING 32 | default y 33 | 34 | 35 | config I2C 36 | default y 37 | 38 | endif 39 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | if SHIELD_BLE01_RIGHT || SHIELD_BLE02_LEFT 2 | 3 | config ZMK_SPLIT_ROLE_CENTRAL 4 | default y 5 | 6 | endif 7 | 8 | if SHIELD_BLE01_LEFT || SHIELD_BLE01_RIGHT 9 | config ZMK_SPLIT 10 | default y 11 | 12 | config ZMK_KEYBOARD_NAME 13 | default "ble01" 14 | 15 | config ZMK_POINTING 16 | default y 17 | 18 | 19 | config I2C 20 | default n 21 | 22 | config SPI 23 | default y 24 | 25 | endif 26 | 27 | if SHIELD_BLE02_LEFT || SHIELD_BLE02_RIGHT 28 | config ZMK_SPLIT 29 | default y 30 | 31 | config ZMK_KEYBOARD_NAME 32 | default "ble02" 33 | 34 | config ZMK_POINTING 35 | default y 36 | 37 | config I2C 38 | default n 39 | 40 | config SPI 41 | default y 42 | 43 | endif 44 | -------------------------------------------------------------------------------- /src/circular_scroll.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "input_processor_gestures.h" 10 | 11 | struct circular_scroll_data { 12 | bool is_tracking; 13 | uint16_t previous_angle; 14 | gesture_data *all; 15 | 16 | uint16_t half_width, half_height; 17 | uint32_t inner_radius_squared, outer_radius_squared; 18 | }; 19 | 20 | struct circular_scroll_config { 21 | const bool enabled; 22 | const uint16_t width, height; 23 | const uint8_t circular_scroll_rim_percent; 24 | }; 25 | 26 | extern volatile bool scroll_mode_active; 27 | 28 | handle_init_t circular_scroll_init; 29 | handle_touch_t circular_scroll_handle_start; 30 | handle_touch_t circular_scroll_handle_touch; 31 | handle_touch_end_t circular_scroll_handle_end; 32 | 33 | -------------------------------------------------------------------------------- /src/touch_detection.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "input_processor_gestures.h" 10 | 11 | struct touch_detection_data { 12 | bool touching; 13 | struct k_work_delayable touch_end_timeout_work; 14 | uint32_t last_touch_timestamp; 15 | uint16_t x, y, previous_x, previous_y; 16 | bool absolute; 17 | bool complete; 18 | struct input_event *previous_event; 19 | gesture_data *all; 20 | }; 21 | 22 | struct touch_detection_config { 23 | const uint8_t wait_for_new_position_ms; 24 | }; 25 | 26 | handle_init_t touch_detection_init; 27 | int touch_detection_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, 28 | uint32_t param2, struct zmk_input_processor_state *state); -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/waveshare_rp2040_zero_defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | CONFIG_SOC_SERIES_RP2XXX=y 5 | CONFIG_SOC_RP2040=y 6 | CONFIG_BOARD_WAVESHARE_RP2040_ZERO=y 7 | 8 | CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=125000000 9 | CONFIG_RESET=y 10 | 11 | # Enable GPIO 12 | CONFIG_GPIO=y 13 | 14 | # Code partition needed to target the correct flash range 15 | CONFIG_USE_DT_CODE_PARTITION=y 16 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 17 | CONFIG_NVS=y 18 | CONFIG_FLASH=y 19 | CONFIG_FLASH_PAGE_LAYOUT=y 20 | CONFIG_FLASH_MAP=y 21 | 22 | # Output UF2 by default, native bootloader supports it. 23 | CONFIG_BUILD_OUTPUT_UF2=y 24 | 25 | # enable uart driver 26 | CONFIG_SERIAL=y 27 | 28 | 29 | # enable console 30 | CONFIG_CONSOLE=y 31 | CONFIG_UART_CONSOLE=y 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /compile_tests/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - board: waveshare_rp2040_zero 4 | shield: usbonly01_left 5 | snippet: zmk-usb-logging studio-rpc-usb-uart 6 | - board: waveshare_rp2040_zero 7 | shield: usbonly01_right 8 | snippet: zmk-usb-logging studio-rpc-usb-uart 9 | - board: waveshare_rp2040_zero 10 | shield: usbonly02_left 11 | snippet: zmk-usb-logging studio-rpc-usb-uart 12 | - board: waveshare_rp2040_zero 13 | shield: usbonly02_right 14 | snippet: zmk-usb-logging studio-rpc-usb-uart 15 | 16 | - board: nice_nano_v2 17 | shield: ble01_left 18 | snippet: zmk-usb-logging studio-rpc-usb-uart 19 | - board: nice_nano_v2 20 | shield: ble01_right 21 | snippet: zmk-usb-logging studio-rpc-usb-uart 22 | - board: nice_nano_v2 23 | shield: ble02_left 24 | snippet: zmk-usb-logging studio-rpc-usb-uart 25 | - board: nice_nano_v2 26 | shield: ble02_right 27 | snippet: zmk-usb-logging studio-rpc-usb-uart 28 | -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/waveshare_rp2040_zero_pinctrl.dtsi: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2024 The ZMK Contributors 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | #include 7 | 8 | &pinctrl { 9 | uart0_default: uart0_default { 10 | group1 { 11 | pinmux = ; 12 | }; 13 | group2 { 14 | pinmux = ; 15 | input-enable; 16 | }; 17 | }; 18 | 19 | i2c0_default: i2c0_default { 20 | group1 { 21 | pinmux = , ; 22 | input-enable; 23 | input-schmitt-enable; 24 | }; 25 | }; 26 | 27 | spi0_default: spi0_default { 28 | group1 { 29 | pinmux = , , ; 30 | }; 31 | group2 { 32 | pinmux = ; 33 | input-enable; 34 | }; 35 | }; 36 | 37 | adc_default: adc_default { 38 | group1 { 39 | pinmux = , , , ; 40 | input-enable; 41 | }; 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /compile_tests/README.md: -------------------------------------------------------------------------------- 1 | # compile tests 2 | 3 | These shields provide several different kinds of ways to include the cirque on a keyboard. 4 | I'm using them to check that compilation still works as expected. 5 | 6 | # Shields 7 | 8 | - **UsbOnly01**: 9 | - wired 10 | - i2c cirque 11 | - on central (right) 12 | - usb only - ble disabled 13 | --> Left (peripheral) **should NOT** include the input stuff 14 | --> Right (central) **should** include the input stuff 15 | 16 | - **UsbOnly02**: 17 | - wired 18 | - i2c cirque 19 | - on peripheral (right) 20 | - usb only - ble disabled 21 | --> Left (central) **should NOT** include the input stuff 22 | --> Right (peripheral) **should** include the input stuff 23 | 24 | - **Ble01**: 25 | - wireless 26 | - spi cirque 27 | - on central (right) 28 | - right is central 29 | - ble only - usb disabled 30 | --> Left (peripheral) **should NOT** include the input stuff 31 | --> Right (central) **should** include the input stuff 32 | - **Ble02**: 33 | - wireless 34 | - spi cirque 35 | - on peripheral (right) 36 | - left is central 37 | - ble only - usb disabled 38 | --> Left (central) **should NOT** include the input stuff 39 | --> Right (peripheral) **should** include the input stuff 40 | -------------------------------------------------------------------------------- /compile_tests/config/west.yml: -------------------------------------------------------------------------------- 1 | manifest: 2 | remotes: 3 | - name: zmkfirmware 4 | url-base: https://github.com/zmkfirmware 5 | - name: halfdane 6 | url-base: https://github.com/halfdane/ 7 | - name: johanson 8 | url-base: https://github.com/petejohanson 9 | projects: 10 | - name: zmk 11 | remote: johanson 12 | revision: split/wired-split-first-pass 13 | import: app/west.yml 14 | - name: cirque-input-module 15 | remote: halfdane 16 | revision: absolute_mode 17 | - name: zmk-input-processors 18 | remote: halfdane 19 | revision: main 20 | - name: zmk-input-gestures 21 | remote: halfdane 22 | revision: main 23 | - name: zephyr 24 | remote: johanson 25 | revision: v3.5.0+zmk-fixes+rp2040-uart-fifo 26 | clone-depth: 1 27 | import: 28 | name-blocklist: 29 | - ci-tools 30 | - hal_altera 31 | - hal_cypress 32 | - hal_infineon 33 | - hal_microchip 34 | - hal_nxp 35 | - hal_openisa 36 | - hal_silabs 37 | - hal_xtensa 38 | - hal_st 39 | - hal_ti 40 | - loramac-node 41 | - mcuboot 42 | - mcumgr 43 | - net-tools 44 | - openthread 45 | - edtt 46 | - trusted-firmware-m 47 | self: 48 | path: config 49 | 50 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly01.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | #include "usbonly-layouts.dtsi" 3 | 4 | &usbonly_layout { 5 | transform = <&default_transform>; 6 | }; 7 | 8 | &uart0 { 9 | status = "okay"; 10 | }; 11 | 12 | / { 13 | chosen { 14 | zmk,kscan = &kscan0; 15 | zmk,physical-layout = &usbonly_layout; 16 | }; 17 | 18 | split_config { 19 | compatible = "zmk,wired-split"; 20 | device = <&uart0>; 21 | }; 22 | 23 | default_transform: keymap_transform_0 { 24 | compatible = "zmk,matrix-transform"; 25 | columns = <12>; 26 | rows = <3>; 27 | // | SW1 | SW2 | SW3 | SW4 | SW5 | | SW1 | SW2 | SW3 | SW4 | SW5 | 28 | // | SW7 | SW8 | SW9 | SW10 | SW11 | | SW7 | SW8 | SW9 | SW10 | SW11 | 29 | // | SW13 | SW14 | SW15 | SW16 | SW17 | | SW13 | SW14 | SW15 | SW16 | SW17 | 30 | // | SW18 | SW12 | | SW12 | SW18 | 31 | map = < 32 | RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,7) RC(0,8) RC(0,9) RC(0,10) RC(0,11) 33 | RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,7) RC(1,8) RC(1,9) RC(1,10) RC(1,11) 34 | RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,7) RC(2,8) RC(2,9) RC(2,10) RC(2,11) 35 | RC(2,5) RC(1,5) RC(1,6) RC(2,6) 36 | >; 37 | }; 38 | 39 | kscan0: kscan { 40 | compatible = "zmk,kscan-gpio-matrix"; 41 | wakeup-source; 42 | 43 | diode-direction = "col2row"; 44 | row-gpios 45 | = <&gpio0 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 46 | , <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 47 | , <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 48 | ; 49 | 50 | }; 51 | 52 | }; 53 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble01.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ble-layouts.dtsi" 3 | 4 | &ble_layout { 5 | transform = <&default_transform>; 6 | }; 7 | 8 | / { 9 | glidepoint_listener: glidepoint_listener { 10 | compatible = "zmk,input-listener"; 11 | status = "disabled"; 12 | }; 13 | }; 14 | 15 | &uart0 { 16 | status = "okay"; 17 | }; 18 | 19 | / { 20 | chosen { 21 | zmk,kscan = &kscan0; 22 | zmk,physical-layout = &ble_layout; 23 | }; 24 | 25 | default_transform: keymap_transform_0 { 26 | compatible = "zmk,matrix-transform"; 27 | columns = <12>; 28 | rows = <3>; 29 | // | SW1 | SW2 | SW3 | SW4 | SW5 | | SW1 | SW2 | SW3 | SW4 | SW5 | 30 | // | SW7 | SW8 | SW9 | SW10 | SW11 | | SW7 | SW8 | SW9 | SW10 | SW11 | 31 | // | SW13 | SW14 | SW15 | SW16 | SW17 | | SW13 | SW14 | SW15 | SW16 | SW17 | 32 | // | SW18 | SW12 | | SW12 | SW18 | 33 | map = < 34 | RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,7) RC(0,8) RC(0,9) RC(0,10) RC(0,11) 35 | RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,7) RC(1,8) RC(1,9) RC(1,10) RC(1,11) 36 | RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,7) RC(2,8) RC(2,9) RC(2,10) RC(2,11) 37 | RC(2,5) RC(1,5) RC(1,6) RC(2,6) 38 | >; 39 | }; 40 | 41 | kscan0: kscan { 42 | compatible = "zmk,kscan-gpio-matrix"; 43 | wakeup-source; 44 | 45 | diode-direction = "col2row"; 46 | row-gpios 47 | = <&gpio0 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 48 | , <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 49 | , <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 50 | ; 51 | 52 | }; 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /src/input_processor_gestures.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | struct gesture_event_t { 12 | uint32_t last_touch_timestamp, previous_touch_timestamp, delta_time; 13 | uint16_t x, y, previous_x, previous_y; 14 | int delta_x, delta_y; 15 | bool absolute; 16 | struct input_event *raw_event_1; 17 | struct input_event *raw_event_2; 18 | }; 19 | 20 | typedef struct gesture_data gesture_data; 21 | typedef struct gesture_config_t gesture_config; 22 | 23 | typedef int (handle_init_t)(const struct device *dev); 24 | typedef int (handle_touch_t)(const struct device *dev, struct gesture_event_t *event); 25 | typedef int (handle_touch_end_t)(const struct device *dev); 26 | 27 | #include "touch_detection.h" 28 | #include "tap_detection.h" 29 | #include "circular_scroll.h" 30 | #include "inertial_cursor.h" 31 | 32 | struct gesture_data { 33 | const struct device *dev; 34 | 35 | // I would prefer these to be pointers, but then dereferencing 36 | // the embedded k_work_delayable in there doesn't work: 37 | // &gesture_data->touch_detection.touch_end_timeout_work crashes 38 | // the firmware :/ 39 | struct touch_detection_data touch_detection; 40 | struct tap_detection_data tap_detection; 41 | struct circular_scroll_data circular_scroll; 42 | struct inertial_cursor_data inertial_cursor; 43 | }; 44 | 45 | struct gesture_config { 46 | handle_touch_t *handle_touch_start; 47 | handle_touch_t *handle_touch_continue; 48 | handle_touch_end_t *handle_touch_end; 49 | 50 | struct touch_detection_config touch_detection; 51 | struct tap_detection_config tap_detection; 52 | struct circular_scroll_config circular_scroll; 53 | struct inertial_cursor_config inertial_cursor; 54 | }; -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble02.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ble-layouts.dtsi" 3 | 4 | &ble_layout { 5 | transform = <&default_transform>; 6 | }; 7 | 8 | &uart0 { 9 | status = "okay"; 10 | }; 11 | 12 | / { 13 | chosen { 14 | zmk,kscan = &kscan0; 15 | zmk,physical-layout = &ble_layout; 16 | }; 17 | 18 | default_transform: keymap_transform_0 { 19 | compatible = "zmk,matrix-transform"; 20 | columns = <12>; 21 | rows = <3>; 22 | // | SW1 | SW2 | SW3 | SW4 | SW5 | | SW1 | SW2 | SW3 | SW4 | SW5 | 23 | // | SW7 | SW8 | SW9 | SW10 | SW11 | | SW7 | SW8 | SW9 | SW10 | SW11 | 24 | // | SW13 | SW14 | SW15 | SW16 | SW17 | | SW13 | SW14 | SW15 | SW16 | SW17 | 25 | // | SW18 | SW12 | | SW12 | SW18 | 26 | map = < 27 | RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,7) RC(0,8) RC(0,9) RC(0,10) RC(0,11) 28 | RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,7) RC(1,8) RC(1,9) RC(1,10) RC(1,11) 29 | RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,7) RC(2,8) RC(2,9) RC(2,10) RC(2,11) 30 | RC(2,5) RC(1,5) RC(1,6) RC(2,6) 31 | >; 32 | }; 33 | 34 | kscan0: kscan { 35 | compatible = "zmk,kscan-gpio-matrix"; 36 | wakeup-source; 37 | 38 | diode-direction = "col2row"; 39 | row-gpios 40 | = <&gpio0 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 41 | , <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 42 | , <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 43 | ; 44 | 45 | }; 46 | 47 | }; 48 | 49 | / { 50 | split_inputs { 51 | #address-cells = <1>; 52 | #size-cells = <0>; 53 | 54 | glidepoint_split: glidepoint_split@0 { 55 | compatible = "zmk,input-split"; 56 | reg = <0>; 57 | }; 58 | }; 59 | 60 | glidepoint_listener: glidepoint_listener { 61 | compatible = "zmk,input-listener"; 62 | status = "disabled"; 63 | device = <&glidepoint_split>; 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly02.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | #include "usbonly-layouts.dtsi" 3 | 4 | &usbonly_layout { 5 | transform = <&default_transform>; 6 | }; 7 | 8 | / { 9 | split_inputs { 10 | #address-cells = <1>; 11 | #size-cells = <0>; 12 | 13 | glidepoint_split: glidepoint_split@0 { 14 | compatible = "zmk,input-split"; 15 | reg = <0>; 16 | }; 17 | }; 18 | 19 | glidepoint_listener: glidepoint_listener { 20 | compatible = "zmk,input-listener"; 21 | status = "disabled"; 22 | device = <&glidepoint_split>; 23 | }; 24 | }; 25 | 26 | &uart0 { 27 | status = "okay"; 28 | }; 29 | 30 | / { 31 | chosen { 32 | zmk,kscan = &kscan0; 33 | zmk,physical-layout = &usbonly_layout; 34 | }; 35 | 36 | split_config { 37 | compatible = "zmk,wired-split"; 38 | device = <&uart0>; 39 | }; 40 | 41 | default_transform: keymap_transform_0 { 42 | compatible = "zmk,matrix-transform"; 43 | columns = <12>; 44 | rows = <3>; 45 | // | SW1 | SW2 | SW3 | SW4 | SW5 | | SW1 | SW2 | SW3 | SW4 | SW5 | 46 | // | SW7 | SW8 | SW9 | SW10 | SW11 | | SW7 | SW8 | SW9 | SW10 | SW11 | 47 | // | SW13 | SW14 | SW15 | SW16 | SW17 | | SW13 | SW14 | SW15 | SW16 | SW17 | 48 | // | SW18 | SW12 | | SW12 | SW18 | 49 | map = < 50 | RC(0,0) RC(0,1) RC(0,2) RC(0,3) RC(0,4) RC(0,7) RC(0,8) RC(0,9) RC(0,10) RC(0,11) 51 | RC(1,0) RC(1,1) RC(1,2) RC(1,3) RC(1,4) RC(1,7) RC(1,8) RC(1,9) RC(1,10) RC(1,11) 52 | RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,7) RC(2,8) RC(2,9) RC(2,10) RC(2,11) 53 | RC(2,5) RC(1,5) RC(1,6) RC(2,6) 54 | >; 55 | }; 56 | 57 | kscan0: kscan { 58 | compatible = "zmk,kscan-gpio-matrix"; 59 | wakeup-source; 60 | 61 | diode-direction = "col2row"; 62 | row-gpios 63 | = <&gpio0 8 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 64 | , <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 65 | , <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN)> 66 | ; 67 | 68 | }; 69 | 70 | }; 71 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble01_right.overlay: -------------------------------------------------------------------------------- 1 | #include "ble01.dtsi" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | &default_transform { 8 | col-offset = <6>; 9 | }; 10 | 11 | &kscan0 { 12 | col-gpios 13 | = <&gpio0 7 GPIO_ACTIVE_HIGH> 14 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 15 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 16 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 17 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 18 | , <&gpio0 2 GPIO_ACTIVE_HIGH> 19 | ; 20 | }; 21 | 22 | 23 | &zip_gestures { 24 | tap-detection; 25 | prevent_movement_during_tap; 26 | 27 | circular-scroll; 28 | circular-scroll-rim-percent=<15>; 29 | }; 30 | 31 | &pro_micro_spi { 32 | status = "okay"; 33 | cs-gpios = <&pro_micro 19 GPIO_ACTIVE_LOW>; 34 | 35 | glidepoint: glidepoint@0 { 36 | compatible = "cirque,pinnacle"; 37 | reg = <0>; 38 | spi-max-frequency = <1000000>; 39 | status = "okay"; 40 | dr-gpios = <&pro_micro 5 (GPIO_ACTIVE_HIGH)>; 41 | 42 | sensitivity = "4x"; 43 | // sleep; 44 | no-taps; 45 | absolute-mode; 46 | 47 | absolute-mode-clamp-min-x=<271>; 48 | absolute-mode-clamp-max-x=<1713>; 49 | absolute-mode-clamp-min-y=<199>; 50 | absolute-mode-clamp-max-y=<1388>; 51 | }; 52 | }; 53 | 54 | / { 55 | glidepoint_listener: glidepoint_listener { 56 | compatible = "zmk,input-listener"; 57 | status = "okay"; 58 | device = <&glidepoint>; 59 | input-processors = < 60 | &zip_gestures 61 | &zip_absolute_to_relative 62 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 63 | &zip_temp_layer 3 100 64 | >; 65 | scroller { 66 | layers = <5>; 67 | input-processors = < 68 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_X_INVERT) 69 | &zip_xy_scaler 1 20 70 | &zip_xy_to_scroll_mapper 71 | >; 72 | }; 73 | }; 74 | }; 75 | 76 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly01_right.overlay: -------------------------------------------------------------------------------- 1 | #include "usbonly01.dtsi" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | &default_transform { 9 | col-offset = <6>; 10 | }; 11 | 12 | &kscan0 { 13 | col-gpios 14 | = <&gpio0 7 GPIO_ACTIVE_HIGH> 15 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 16 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 17 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 18 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 19 | , <&gpio0 2 GPIO_ACTIVE_HIGH> 20 | ; 21 | }; 22 | 23 | 24 | &i2c0_default { 25 | group1 { 26 | pinmux = , ; 27 | input-enable; 28 | input-schmitt-enable; 29 | }; 30 | status = "okay"; 31 | }; 32 | 33 | &i2c0 { 34 | status = "okay"; 35 | 36 | glidepoint: glidepoint@2a { 37 | compatible = "cirque,pinnacle"; 38 | reg = <0x2a>; 39 | status = "okay"; 40 | dr-gpios = <&gpio0 29 (GPIO_ACTIVE_HIGH)>; 41 | 42 | sensitivity = "2x"; 43 | // sleep; 44 | no-taps; 45 | absolute-mode; 46 | 47 | absolute-mode-clamp-min-x=<271>; 48 | absolute-mode-clamp-max-x=<1713>; 49 | absolute-mode-clamp-min-y=<199>; 50 | absolute-mode-clamp-max-y=<1388>; 51 | }; 52 | 53 | }; 54 | 55 | &zip_gestures { 56 | tap-detection; 57 | prevent_movement_during_tap; 58 | 59 | circular-scroll; 60 | circular-scroll-rim-percent=<15>; 61 | }; 62 | 63 | / { 64 | glidepoint_listener: glidepoint_listener { 65 | compatible = "zmk,input-listener"; 66 | status = "okay"; 67 | device = <&glidepoint>; 68 | input-processors = < 69 | &zip_gestures 70 | &zip_absolute_to_relative 71 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 72 | &zip_temp_layer 3 100 73 | >; 74 | scroller { 75 | layers = <5>; 76 | input-processors = < 77 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_X_INVERT) 78 | &zip_xy_scaler 1 20 79 | &zip_xy_to_scroll_mapper 80 | >; 81 | }; 82 | }; 83 | }; -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly02_right.overlay: -------------------------------------------------------------------------------- 1 | #include "usbonly02.dtsi" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | &default_transform { 10 | col-offset = <6>; 11 | }; 12 | 13 | &kscan0 { 14 | col-gpios 15 | = <&gpio0 7 GPIO_ACTIVE_HIGH> 16 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 17 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 18 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 19 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 20 | , <&gpio0 2 GPIO_ACTIVE_HIGH> 21 | ; 22 | }; 23 | 24 | 25 | &i2c0_default { 26 | group1 { 27 | pinmux = , ; 28 | input-enable; 29 | input-schmitt-enable; 30 | }; 31 | status = "okay"; 32 | }; 33 | 34 | &i2c0 { 35 | status = "okay"; 36 | 37 | glidepoint: glidepoint@2a { 38 | compatible = "cirque,pinnacle"; 39 | reg = <0x2a>; 40 | status = "okay"; 41 | dr-gpios = <&gpio0 29 (GPIO_ACTIVE_HIGH)>; 42 | 43 | sensitivity = "1x"; 44 | //sleep; 45 | no-taps; 46 | absolute-mode; 47 | 48 | absolute-mode-clamp-min-x=<271>; 49 | absolute-mode-clamp-max-x=<1713>; 50 | absolute-mode-clamp-min-y=<199>; 51 | absolute-mode-clamp-max-y=<1388>; 52 | 53 | }; 54 | }; 55 | 56 | &zip_gestures { 57 | tap-detection; 58 | prevent_movement_during_tap; 59 | 60 | circular-scroll; 61 | circular-scroll-rim-percent=<15>; 62 | }; 63 | 64 | 65 | / { 66 | split_inputs { 67 | #address-cells = <1>; 68 | #size-cells = <0>; 69 | 70 | glidepoint_split: glidepoint_split@0 { 71 | compatible = "zmk,input-split"; 72 | device = <&glidepoint>; 73 | reg = <0>; 74 | }; 75 | }; 76 | 77 | glidepoint_listener: glidepoint_listener { 78 | compatible = "zmk,input-listener"; 79 | status = "okay"; 80 | device = <&glidepoint_split>; 81 | 82 | input-processors = < 83 | &zip_gestures 84 | &zip_absolute_to_relative 85 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 86 | &zip_temp_layer 3 100 87 | >; 88 | scroller { 89 | layers = <5>; 90 | input-processors = < 91 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_X_INVERT) 92 | &zip_xy_scaler 1 20 93 | &zip_xy_to_scroll_mapper 94 | >; 95 | }; 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble02_right.overlay: -------------------------------------------------------------------------------- 1 | #include "ble02.dtsi" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | &default_transform { 9 | col-offset = <6>; 10 | }; 11 | 12 | &kscan0 { 13 | col-gpios 14 | = <&gpio0 7 GPIO_ACTIVE_HIGH> 15 | , <&gpio0 6 GPIO_ACTIVE_HIGH> 16 | , <&gpio0 5 GPIO_ACTIVE_HIGH> 17 | , <&gpio0 4 GPIO_ACTIVE_HIGH> 18 | , <&gpio0 3 GPIO_ACTIVE_HIGH> 19 | , <&gpio0 2 GPIO_ACTIVE_HIGH> 20 | ; 21 | }; 22 | 23 | &pro_micro_spi { 24 | status = "okay"; 25 | cs-gpios = <&pro_micro 19 GPIO_ACTIVE_LOW>; 26 | 27 | glidepoint: glidepoint@0 { 28 | compatible = "cirque,pinnacle"; 29 | reg = <0>; 30 | spi-max-frequency = <1000000>; 31 | status = "okay"; 32 | dr-gpios = <&pro_micro 5 (GPIO_ACTIVE_HIGH)>; 33 | 34 | sensitivity = "4x"; 35 | sleep; 36 | no-taps; 37 | absolute-mode; 38 | 39 | absolute-mode-clamp-min-x=<271>; 40 | absolute-mode-clamp-max-x=<1713>; 41 | absolute-mode-clamp-min-y=<199>; 42 | absolute-mode-clamp-max-y=<1388>; 43 | 44 | }; 45 | }; 46 | 47 | 48 | &glidepoint_split { 49 | device = <&glidepoint>; 50 | }; 51 | 52 | / { 53 | split_inputs { 54 | #address-cells = <1>; 55 | #size-cells = <0>; 56 | 57 | glidepoint_split: glidepoint_split@0 { 58 | compatible = "zmk,input-split"; 59 | reg = <0>; 60 | }; 61 | }; 62 | 63 | glidepoint_listener: glidepoint_listener { 64 | compatible = "zmk,input-listener"; 65 | status = "okay"; 66 | device = <&glidepoint_split>; 67 | }; 68 | }; 69 | 70 | &zip_gestures { 71 | tap-detection; 72 | prevent_movement_during_tap; 73 | 74 | circular-scroll; 75 | circular-scroll-rim-percent=<15>; 76 | }; 77 | 78 | / { 79 | glidepoint_listener { 80 | compatible = "zmk,input-listener"; 81 | device = <&glidepoint_split>; 82 | 83 | input-processors = < 84 | &zip_gestures 85 | &zip_absolute_to_relative 86 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 87 | &zip_temp_layer 3 100 88 | >; 89 | scroller { 90 | layers = <5>; 91 | input-processors = < 92 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_X_INVERT) 93 | &zip_xy_scaler 1 20 94 | &zip_xy_to_scroll_mapper 95 | >; 96 | }; 97 | }; 98 | }; -------------------------------------------------------------------------------- /src/tap_detection.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "input_processor_gestures.h" 12 | 13 | LOG_MODULE_DECLARE(gestures, CONFIG_ZMK_LOG_LEVEL); 14 | 15 | int tap_detection_handle_start(const struct device *dev, struct gesture_event_t *event) { 16 | struct gesture_config *config = (struct gesture_config *)dev->config; 17 | struct gesture_data *data = (struct gesture_data *)dev->data; 18 | 19 | if (! config->tap_detection.enabled) { 20 | return -1; 21 | } 22 | 23 | k_work_reschedule(&data->tap_detection.tap_timeout_work, K_MSEC(config->tap_detection.tap_timout_ms)); 24 | data->tap_detection.is_waiting_for_tap = true; 25 | 26 | if (config->tap_detection.prevent_movement_during_tap) { 27 | event->raw_event_1->code = 0; 28 | event->raw_event_1->type = 0; 29 | event->raw_event_1->value = 0; 30 | 31 | event->raw_event_2->code = 0; 32 | event->raw_event_2->type = 0; 33 | event->raw_event_2->value = 0; 34 | } 35 | 36 | return 0; 37 | } 38 | 39 | int tap_detection_handle_touch(const struct device *dev, struct gesture_event_t *event) { 40 | struct gesture_config *config = (struct gesture_config *)dev->config; 41 | struct gesture_data *data = (struct gesture_data *)dev->data; 42 | 43 | if (! config->tap_detection.enabled) { 44 | return -1; 45 | } 46 | 47 | if (data->tap_detection.is_waiting_for_tap && config->tap_detection.prevent_movement_during_tap) { 48 | event->raw_event_1->code = 0; 49 | event->raw_event_1->type = 0; 50 | event->raw_event_1->value = 0; 51 | 52 | event->raw_event_2->code = 0; 53 | event->raw_event_2->type = 0; 54 | event->raw_event_2->value = 0; 55 | } 56 | 57 | return 0; 58 | } 59 | 60 | 61 | /* Work Queue Callback */ 62 | static void tap_timeout_callback(struct k_work *work) { 63 | struct k_work_delayable *d_work = k_work_delayable_from_work(work); 64 | struct tap_detection_data *data = CONTAINER_OF(d_work, struct tap_detection_data, tap_timeout_work); 65 | data->is_waiting_for_tap = false; 66 | if (!data->all->touch_detection.touching) { 67 | LOG_DBG("tap detected - sending button presses"); 68 | zmk_hid_mouse_button_press(0); 69 | zmk_endpoints_send_mouse_report(); 70 | zmk_hid_mouse_button_release(0); 71 | zmk_endpoints_send_mouse_report(); 72 | } else { 73 | LOG_DBG("time expired but touch is ongoing - it's not a tap"); 74 | } 75 | } 76 | 77 | int tap_detection_init(const struct device *dev) { 78 | struct gesture_config *config = (struct gesture_config *)dev->config; 79 | struct gesture_data *data = (struct gesture_data *)dev->data; 80 | 81 | LOG_DBG("tap_detection: %s, timeout in ms: %d, prevent_movement_during_tap: %s", 82 | config->tap_detection.enabled ? "yes" : "no", 83 | config->tap_detection.tap_timout_ms, 84 | config->tap_detection.prevent_movement_during_tap ? "yes" : "no"); 85 | 86 | if (!config->tap_detection.enabled) { 87 | return -1; 88 | } 89 | k_work_init_delayable(&data->tap_detection.tap_timeout_work, tap_timeout_callback); 90 | 91 | return 0; 92 | } 93 | -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/ble/ble-layouts.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | / { 4 | ble_layout: physical_layout_0 { 5 | compatible = "zmk,physical-layout"; 6 | display-name = "ble Layout"; 7 | 8 | keys // w h x y rot rx ry 9 | = <&key_physical_attrs 100 100 0 38 (-800) 500 260> 10 | , <&key_physical_attrs 100 100 100 12 0 0 0> 11 | , <&key_physical_attrs 100 100 200 0 0 0 0> 12 | , <&key_physical_attrs 100 100 300 12 0 0 0> 13 | , <&key_physical_attrs 100 100 400 25 0 0 0> 14 | , <&key_physical_attrs 100 100 800 25 0 0 0> 15 | , <&key_physical_attrs 100 100 900 12 0 0 0> 16 | , <&key_physical_attrs 100 100 1000 0 0 0 0> 17 | , <&key_physical_attrs 100 100 1100 12 0 0 0> 18 | , <&key_physical_attrs 100 100 1200 38 800 800 260> 19 | , <&key_physical_attrs 100 100 0 138 (-800) 500 260> 20 | , <&key_physical_attrs 100 100 100 112 0 0 0> 21 | , <&key_physical_attrs 100 100 200 100 0 0 0> 22 | , <&key_physical_attrs 100 100 300 112 0 0 0> 23 | , <&key_physical_attrs 100 100 400 125 0 0 0> 24 | , <&key_physical_attrs 100 100 800 125 0 0 0> 25 | , <&key_physical_attrs 100 100 900 112 0 0 0> 26 | , <&key_physical_attrs 100 100 1000 100 0 0 0> 27 | , <&key_physical_attrs 100 100 1100 112 0 0 0> 28 | , <&key_physical_attrs 100 100 1200 138 800 800 260> 29 | , <&key_physical_attrs 100 100 0 238 (-800) 500 260> 30 | , <&key_physical_attrs 100 100 100 212 0 0 0> 31 | , <&key_physical_attrs 100 100 200 200 0 0 0> 32 | , <&key_physical_attrs 100 100 300 212 0 0 0> 33 | , <&key_physical_attrs 100 100 400 225 0 0 0> 34 | , <&key_physical_attrs 100 100 800 225 0 0 0> 35 | , <&key_physical_attrs 100 100 900 212 0 0 0> 36 | , <&key_physical_attrs 100 100 1000 200 0 0 0> 37 | , <&key_physical_attrs 100 100 1100 212 0 0 0> 38 | , <&key_physical_attrs 100 100 1200 238 800 800 260> 39 | , <&key_physical_attrs 100 100 380 320 800 0 0> 40 | , <&key_physical_attrs 100 100 480 335 800 0 0> 41 | , <&key_physical_attrs 100 100 720 350 (-800) 0 0> 42 | , <&key_physical_attrs 100 100 820 335 (-800) 0 0> 43 | ; 44 | }; 45 | }; 46 | 47 | / { 48 | layouts_ble_position_map: layouts_ble_position_map { 49 | compatible = "zmk,physical-layout-position-map"; 50 | 51 | complete; 52 | }; 53 | }; 54 | 55 | &layouts_ble_position_map { 56 | ble_posmap: five { 57 | physical-layout = <&ble_layout>; 58 | positions 59 | = <36 0 1 2 3 4 5 6 7 8 9 37> 60 | , <38 10 11 12 13 14 15 16 17 18 19 39> 61 | , <40 20 21 22 23 24 25 26 27 28 29 41> 62 | , < 30 31 32 33 34 35 >; 63 | }; 64 | }; -------------------------------------------------------------------------------- /compile_tests/config/boards/shields/usbonly/usbonly-layouts.dtsi: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | / { 4 | usbonly_layout: physical_layout_0 { 5 | compatible = "zmk,physical-layout"; 6 | display-name = "UsbOnly Layout"; 7 | 8 | keys // w h x y rot rx ry 9 | = <&key_physical_attrs 100 100 0 38 (-800) 500 260> 10 | , <&key_physical_attrs 100 100 100 12 0 0 0> 11 | , <&key_physical_attrs 100 100 200 0 0 0 0> 12 | , <&key_physical_attrs 100 100 300 12 0 0 0> 13 | , <&key_physical_attrs 100 100 400 25 0 0 0> 14 | , <&key_physical_attrs 100 100 800 25 0 0 0> 15 | , <&key_physical_attrs 100 100 900 12 0 0 0> 16 | , <&key_physical_attrs 100 100 1000 0 0 0 0> 17 | , <&key_physical_attrs 100 100 1100 12 0 0 0> 18 | , <&key_physical_attrs 100 100 1200 38 800 800 260> 19 | , <&key_physical_attrs 100 100 0 138 (-800) 500 260> 20 | , <&key_physical_attrs 100 100 100 112 0 0 0> 21 | , <&key_physical_attrs 100 100 200 100 0 0 0> 22 | , <&key_physical_attrs 100 100 300 112 0 0 0> 23 | , <&key_physical_attrs 100 100 400 125 0 0 0> 24 | , <&key_physical_attrs 100 100 800 125 0 0 0> 25 | , <&key_physical_attrs 100 100 900 112 0 0 0> 26 | , <&key_physical_attrs 100 100 1000 100 0 0 0> 27 | , <&key_physical_attrs 100 100 1100 112 0 0 0> 28 | , <&key_physical_attrs 100 100 1200 138 800 800 260> 29 | , <&key_physical_attrs 100 100 0 238 (-800) 500 260> 30 | , <&key_physical_attrs 100 100 100 212 0 0 0> 31 | , <&key_physical_attrs 100 100 200 200 0 0 0> 32 | , <&key_physical_attrs 100 100 300 212 0 0 0> 33 | , <&key_physical_attrs 100 100 400 225 0 0 0> 34 | , <&key_physical_attrs 100 100 800 225 0 0 0> 35 | , <&key_physical_attrs 100 100 900 212 0 0 0> 36 | , <&key_physical_attrs 100 100 1000 200 0 0 0> 37 | , <&key_physical_attrs 100 100 1100 212 0 0 0> 38 | , <&key_physical_attrs 100 100 1200 238 800 800 260> 39 | , <&key_physical_attrs 100 100 380 320 800 0 0> 40 | , <&key_physical_attrs 100 100 480 335 800 0 0> 41 | , <&key_physical_attrs 100 100 720 350 (-800) 0 0> 42 | , <&key_physical_attrs 100 100 820 335 (-800) 0 0> 43 | ; 44 | }; 45 | }; 46 | 47 | / { 48 | layouts_usbonly_position_map: layouts_usbonly_position_map { 49 | compatible = "zmk,physical-layout-position-map"; 50 | 51 | complete; 52 | }; 53 | }; 54 | 55 | &layouts_usbonly_position_map { 56 | usbonly_posmap: five { 57 | physical-layout = <&usbonly_layout>; 58 | positions 59 | = <36 0 1 2 3 4 5 6 7 8 9 37> 60 | , <38 10 11 12 13 14 15 16 17 18 19 39> 61 | , <40 20 21 22 23 24 25 26 27 28 29 41> 62 | , < 30 31 32 33 34 35 >; 63 | }; 64 | }; -------------------------------------------------------------------------------- /compile_tests/config/boards/arm/waveshare_rp2040_zero/waveshare_rp2040_zero.dts: -------------------------------------------------------------------------------- 1 | /dts-v1/; 2 | 3 | #include 4 | #include "waveshare_rp2040_zero_pinctrl.dtsi" 5 | #include 6 | #include 7 | 8 | 9 | / { 10 | model = "Waveshare RP2040 Zero Board"; 11 | compatible = "waveshare,rp2040-zero", "raspberrypi,rp2040"; 12 | 13 | 14 | chosen { 15 | zephyr,code-partition = &code_partition; 16 | zephyr,sram = &sram0; 17 | zephyr,flash = &flash0; 18 | zephyr,flash-controller = &ssi; 19 | }; 20 | 21 | leds { 22 | compatible = "gpio-leds"; 23 | led: led_0 { 24 | gpios = <&gpio0 16 GPIO_ACTIVE_LOW>; 25 | label = "LED"; 26 | }; 27 | }; 28 | 29 | /* These aliases are provided for compatibility with samples */ 30 | aliases { 31 | led0 = &led; 32 | }; 33 | 34 | xtal_clk: xtal-clk { 35 | compatible = "fixed-clock"; 36 | clock-frequency = <12000000>; 37 | #clock-cells = <0>; 38 | }; 39 | 40 | aliases { 41 | watchdog0 = &wdt0; 42 | }; 43 | 44 | 45 | zero_header: connector { 46 | compatible = "waveshare,waveshare_rp2040_zero"; 47 | #gpio-cells = <2>; 48 | gpio-map-mask = <0xffffffff 0xffffffc0>; 49 | gpio-map-pass-thru = <0 0x3f>; 50 | gpio-map = <0 0 &gpio0 0 0>, /* GP0 */ 51 | <1 0 &gpio0 1 0>, /* GP1 */ 52 | <2 0 &gpio0 2 0>, /* GP2 */ 53 | <3 0 &gpio0 3 0>, /* GP3 */ 54 | <4 0 &gpio0 4 0>, /* GP4 */ 55 | <5 0 &gpio0 5 0>, /* GP5 */ 56 | <6 0 &gpio0 6 0>, /* GP6 */ 57 | <7 0 &gpio0 7 0>, /* GP7 */ 58 | <8 0 &gpio0 8 0>, /* GP8 */ 59 | <9 0 &gpio0 9 0>, /* GP9 */ 60 | <10 0 &gpio0 10 0>, /* GP10 */ 61 | <11 0 &gpio0 11 0>, /* GP11 */ 62 | <12 0 &gpio0 12 0>, /* GP12 */ 63 | <13 0 &gpio0 13 0>, /* GP13 */ 64 | <14 0 &gpio0 14 0>, /* GP14 */ 65 | <15 0 &gpio0 15 0>, /* GP15 */ 66 | <16 0 &gpio0 16 0>, /* GP16 */ 67 | <17 0 &gpio0 17 0>, /* GP17 */ 68 | <18 0 &gpio0 18 0>, /* GP18 */ 69 | <19 0 &gpio0 19 0>, /* GP19 */ 70 | <20 0 &gpio0 20 0>, /* GP20 */ 71 | <21 0 &gpio0 21 0>, /* GP21 */ 72 | <22 0 &gpio0 22 0>, /* GP22 */ 73 | <23 0 &gpio0 23 0>, /* GP23 */ 74 | <24 0 &gpio0 24 0>, /* GP24 */ 75 | <25 0 &gpio0 25 0>, /* GP25 */ 76 | <26 0 &gpio0 26 0>, /* GP26 */ 77 | <27 0 &gpio0 27 0>, /* GP27 */ 78 | <28 0 &gpio0 28 0>, /* GP28 */ 79 | <29 0 &gpio0 29 0>; /* GP29 */ 80 | }; 81 | 82 | }; 83 | 84 | &flash0 { 85 | /* 2MB of flash minus the 0x100 used for 86 | * the second stage bootloader 87 | */ 88 | reg = <0x10000000 DT_SIZE_M(2)>; 89 | 90 | partitions { 91 | compatible = "fixed-partitions"; 92 | #address-cells = <1>; 93 | #size-cells = <1>; 94 | 95 | /* 96 | * Start at the beginning of usable flash, 8MB minus the 97 | * second stage space and the 16 KiB reserved for settings 98 | */ 99 | code_partition: partition@100 { 100 | label = "code"; 101 | reg = <0x100 (DT_SIZE_M(2) - DT_SIZE_K(16))>; 102 | read-only; 103 | }; 104 | 105 | /* 106 | * The final 16 KiB is reserved for the application. 107 | * Storage partition may be used by FCB or LittleFS. 108 | */ 109 | storage_partition: partition@7fbe00 { 110 | label = "storage"; 111 | reg = <0x007fbe00 DT_SIZE_K(16)>; 112 | }; 113 | }; 114 | }; 115 | 116 | &uart0 { 117 | current-speed = <115200>; 118 | pinctrl-0 = <&uart0_default>; 119 | pinctrl-names = "default"; 120 | }; 121 | 122 | &spi0 { 123 | pinctrl-0 = <&spi0_default>; 124 | pinctrl-names = "default"; 125 | clock-frequency = ; 126 | }; 127 | 128 | &adc { 129 | status = "okay"; 130 | pinctrl-0 = <&adc_default>; 131 | pinctrl-names = "default"; 132 | }; 133 | 134 | &i2c0 { 135 | pinctrl-0 = <&i2c0_default>; 136 | pinctrl-names = "default"; 137 | clock-frequency = ; 138 | }; 139 | 140 | &gpio0 { 141 | status = "okay"; 142 | }; 143 | 144 | &timer { 145 | status = "okay"; 146 | }; 147 | 148 | &wdt0 { 149 | status = "okay"; 150 | }; 151 | 152 | zephyr_udc0: &usbd { 153 | status = "okay"; 154 | }; 155 | -------------------------------------------------------------------------------- /src/touch_detection.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #include 8 | #include 9 | #include "input_processor_gestures.h" 10 | 11 | LOG_MODULE_DECLARE(gestures, CONFIG_ZMK_LOG_LEVEL); 12 | 13 | 14 | int touch_detection_handle_event(const struct device *dev, struct input_event *event, uint32_t param1, 15 | uint32_t param2, struct zmk_input_processor_state *state) { 16 | struct gesture_config *config = (struct gesture_config *)dev->config; 17 | struct gesture_data *data = (struct gesture_data *)dev->data; 18 | k_work_reschedule(&data->touch_detection.touch_end_timeout_work, K_MSEC(config->touch_detection.wait_for_new_position_ms)); 19 | 20 | if (event->type != INPUT_EV_ABS && event->type == INPUT_EV_REL) { 21 | return ZMK_INPUT_PROC_CONTINUE; 22 | } 23 | 24 | data->touch_detection.complete = !data->touch_detection.complete; 25 | 26 | if (data->touch_detection.complete && data->touch_detection.absolute != (event->type == INPUT_EV_ABS)) { 27 | LOG_ERR("Surprising change of absolute/relative type. It's now [%s] but it's supposed to be [%s]. Don't know how to handle that, so ignoring this", 28 | event->type == INPUT_EV_ABS ? "absolute" : "relative", 29 | data->touch_detection.absolute ? "absolute" : "relative" 30 | ); 31 | return ZMK_INPUT_PROC_CONTINUE; 32 | } else { 33 | data->touch_detection.absolute = (event->type == INPUT_EV_ABS); 34 | } 35 | 36 | 37 | if (event->code == INPUT_ABS_X || event->code == INPUT_REL_X) { 38 | data->touch_detection.x = event->value; 39 | } else if (event->code == INPUT_ABS_Y || event->code == INPUT_REL_Y) { 40 | data->touch_detection.y = event->value; 41 | } 42 | 43 | if (! data->touch_detection.complete) { 44 | // is this true? 45 | // because sometimes we might not want to forward half the events! 46 | // for example when waiting during a tap or while a scroll is happening 47 | return ZMK_INPUT_PROC_CONTINUE; 48 | } 49 | 50 | uint32_t now = k_uptime_get(); 51 | 52 | struct gesture_event_t gesture_event = { 53 | .last_touch_timestamp = now, 54 | .previous_touch_timestamp = data->touch_detection.last_touch_timestamp, 55 | .x = data->touch_detection.x, 56 | .y = data->touch_detection.y, 57 | .previous_x = data->touch_detection.previous_x, 58 | .previous_y = data->touch_detection.previous_y, 59 | .delta_x = data->touch_detection.x - data->touch_detection.previous_x, 60 | .delta_y = data->touch_detection.y - data->touch_detection.previous_y, 61 | .delta_time = now - data->touch_detection.last_touch_timestamp, 62 | .absolute = data->touch_detection.absolute, 63 | .raw_event_1 = data->touch_detection.previous_event, 64 | .raw_event_2 = event 65 | }; 66 | 67 | data->touch_detection.last_touch_timestamp = now; 68 | 69 | if (!data->touch_detection.touching){ 70 | data->touch_detection.touching = true; 71 | config->handle_touch_start(dev, &gesture_event); 72 | } else { 73 | config->handle_touch_continue(dev, &gesture_event); 74 | } 75 | 76 | data->touch_detection.previous_x = data->touch_detection.x; 77 | data->touch_detection.previous_y = data->touch_detection.y; 78 | 79 | return ZMK_INPUT_PROC_CONTINUE; 80 | } 81 | 82 | void touch_end_timeout_callback(struct k_work *work) { 83 | struct k_work_delayable *d_work = k_work_delayable_from_work(work); 84 | struct touch_detection_data *data = CONTAINER_OF(d_work, struct touch_detection_data, touch_end_timeout_work); 85 | const struct device *dev = data->all->dev; 86 | struct gesture_config *config = (struct gesture_config *)dev->config; 87 | 88 | data->touching = false; 89 | data->complete = true; 90 | config->handle_touch_end(dev); 91 | } 92 | 93 | int touch_detection_init(const struct device *dev) { 94 | struct gesture_data *data = (struct gesture_data *)dev->data; 95 | data->touch_detection.last_touch_timestamp = k_uptime_get(); 96 | data->touch_detection.complete = true; 97 | k_work_init_delayable(&data->touch_detection.touch_end_timeout_work, touch_end_timeout_callback); 98 | return 0; 99 | } 100 | -------------------------------------------------------------------------------- /src/inertial_cursor.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "input_processor_gestures.h" 9 | #include "inertial_cursor.h" 10 | 11 | LOG_MODULE_DECLARE(gestures, CONFIG_ZMK_LOG_LEVEL); 12 | 13 | #define ANIMATE_MSEC 50 14 | 15 | static void inertial_cursor_work_handler(struct k_work *work) { 16 | struct k_work_delayable *d_work = k_work_delayable_from_work(work); 17 | struct inertial_cursor_data *data = CONTAINER_OF(d_work, struct inertial_cursor_data, inertial_work); 18 | 19 | LOG_DBG("data->delta_x: %d, data->delta_y: %d", 20 | (int) data->delta_x, 21 | (int) data->delta_y); 22 | 23 | data->delta_x *= data->velocity_decay; 24 | data->delta_y *= data->velocity_decay; 25 | 26 | if (abs((int) data->delta_x) > 0 || abs((int) data->delta_y) > 0) { 27 | zmk_hid_mouse_movement_update((int) data->delta_y, (int) -data->delta_x); 28 | zmk_endpoints_send_mouse_report(); 29 | k_work_reschedule(&data->inertial_work, K_MSEC(data->delta_time)); 30 | } 31 | } 32 | 33 | int inertial_cursor_handle_touch(const struct device *dev, struct gesture_event_t *event) { 34 | struct gesture_data *data = (struct gesture_data *)dev->data; 35 | struct gesture_config *config = (struct gesture_config *)dev->config; 36 | 37 | if (!config->inertial_cursor.enabled) { 38 | return -1; 39 | } 40 | 41 | if (event->delta_x != 0) { 42 | data->inertial_cursor.delta_x = event->delta_x; 43 | } 44 | 45 | if (event->delta_x != 0) { 46 | data->inertial_cursor.delta_y = event->delta_y ; 47 | } 48 | 49 | if (event->delta_time != 0) { 50 | data->inertial_cursor.delta_time = event->delta_time; 51 | } 52 | 53 | return 0; 54 | } 55 | 56 | 57 | int inertial_cursor_handle_touch_start(const struct device *dev, struct gesture_event_t *event) { 58 | struct gesture_data *data = (struct gesture_data *)dev->data; 59 | struct gesture_config *config = (struct gesture_config *)dev->config; 60 | 61 | if (!config->inertial_cursor.enabled) { 62 | return -1; 63 | } 64 | 65 | k_work_cancel_delayable(&data->inertial_cursor.inertial_work); 66 | 67 | data->inertial_cursor.delta_x = 0; 68 | data->inertial_cursor.delta_y = 0; 69 | data->inertial_cursor.delta_time = 0; 70 | 71 | inertial_cursor_handle_touch(dev, event); 72 | 73 | return 0; 74 | } 75 | 76 | int inertial_cursor_handle_end(const struct device *dev) { 77 | struct gesture_data *data = (struct gesture_data *)dev->data; 78 | struct gesture_config *config = (struct gesture_config *)dev->config; 79 | 80 | if (!config->inertial_cursor.enabled) { 81 | return -1; 82 | } 83 | 84 | double velocity = sqrt( 85 | data->inertial_cursor.delta_x * data->inertial_cursor.delta_x + 86 | data->inertial_cursor.delta_y * data->inertial_cursor.delta_y 87 | ) / data->inertial_cursor.delta_time; 88 | 89 | 90 | LOG_DBG("velocity: %d, velocity_threshold: %d, too slow: %s", 91 | (int)velocity, 92 | (int) config->inertial_cursor.velocity_threshold, 93 | velocity <= config->inertial_cursor.velocity_threshold?"yes":"no"); 94 | 95 | if (velocity <= config->inertial_cursor.velocity_threshold) { 96 | return -1; 97 | } 98 | 99 | data->inertial_cursor.delta_x *= data->inertial_cursor.velocity_decay; 100 | data->inertial_cursor.delta_y *= data->inertial_cursor.velocity_decay; 101 | 102 | zmk_hid_mouse_movement_set(0, 0); 103 | zmk_endpoints_send_mouse_report(); 104 | 105 | k_work_reschedule(&data->inertial_cursor.inertial_work, K_MSEC(data->inertial_cursor.delta_time)); 106 | 107 | return 0; 108 | } 109 | 110 | int inertial_cursor_init(const struct device *dev) { 111 | struct gesture_data *data = (struct gesture_data *)dev->data; 112 | struct gesture_config *config = (struct gesture_config *)dev->config; 113 | 114 | LOG_DBG("inertial_cursor: %s, velocity_threshold: %d, decay_percent: %d", 115 | config->inertial_cursor.enabled ? "yes" : "no", 116 | config->inertial_cursor.velocity_threshold, 117 | config->inertial_cursor.decay_percent); 118 | 119 | 120 | if (!config->inertial_cursor.enabled) { 121 | return -1; 122 | } 123 | 124 | data->inertial_cursor.velocity_decay = (100.0 - config->inertial_cursor.decay_percent) / 100.0; 125 | LOG_ERR("velocity_decay *1000: %d", (int) (data->inertial_cursor.velocity_decay * 1000.0)); 126 | 127 | k_work_init_delayable(&data->inertial_cursor.inertial_work, inertial_cursor_work_handler); 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /dts/bindings/zmk,input-processor-gestures.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | description: Several gestures for touchpads 5 | 6 | compatible: "zmk,input-processor-gestures" 7 | 8 | include: ip_zero_param.yaml 9 | 10 | properties: 11 | tap-detection: 12 | type: boolean 13 | description: | 14 | A touch that doesn't last longer than tap-timout-ms is interpreted as a tap (click). 15 | tap-timout-ms: 16 | type: int 17 | default: 120 18 | description: | 19 | A lower value requires a quicker tap motion, but reduces accidental taps for short movements. 20 | The default value is a good compromise that works for me. 21 | prevent_movement_during_tap: 22 | type: boolean 23 | description: | 24 | While determining if the beginning of a touch is a tap, ignore all other movements. 25 | This prevents accidental movement aways from the tap-target, but it makes the 26 | beginning of regular touches more sluggish. 27 | 28 | inertial-cursor: 29 | type: boolean 30 | description: | 31 | If the cursor moves faster than inertial-cursor-velocity-threshold-ms when the touch ends, the cursor 32 | keeps moving in the same direction, gradually getting slower by inertial-cursor-decay-percent. 33 | inertial-cursor-velocity-threshold: 34 | type: int 35 | default: 2 36 | description: | 37 | A lower value makes it easier to activate the inertial, but increases the accidental activation. 38 | The default value is a good compromise that works for me. 39 | inertial-cursor-decay-percent: 40 | type: int 41 | default: 30 42 | description: | 43 | A lower value will make the cursor move longer after the touch ends - kinda like changing the friction that 44 | slows down the cursor. 45 | The default value mimics a relatively high friction. 46 | 47 | circular-scroll: 48 | type: boolean 49 | description: | 50 | If a touch begins on the outer circular-scroll-rim-percent percentage of the touchpad, angular 51 | movement of the touch is interpreted as scrolling down (clockwise) or up (counter-clockwise). 52 | circular-scroll-rim-percent: 53 | type: int 54 | default: 10 55 | description: | 56 | Width of the outer ring of the touchpad that activates circular scroll when the touch starts there. 57 | A lower value reduces accicental activation during normal usage, but requires better targeting 58 | to activate. 59 | The default value is a good compromise that works for me. 60 | circular-scroll-width: 61 | type: int 62 | default: 1024 63 | description: | 64 | Width of the touchpad. 65 | circular-scroll-height: 66 | type: int 67 | default: 1024 68 | description: | 69 | Height of the touchpad. 70 | 71 | right-side-vertical-scroll: 72 | type: boolean 73 | description: | 74 | If a touch begins on the right right-side-vertical-scroll-percent percentage of the touchpad, vertical 75 | movement of the touch is interpreted as vertical scrolling. 76 | right-side-vertical-scroll-percent: 77 | type: int 78 | default: 10 79 | description: | 80 | Width of the right part of the touchpad that activates vertical scroll when the touch starts there. 81 | A lower value reduces accicental activation during normal usage, but requires better targeting 82 | to activate. 83 | The default value is a good compromise that works for me. 84 | 85 | top-side-horizontal-scroll: 86 | type: boolean 87 | description: | 88 | If a touch begins on the top top-side-horizontal-scroll-percent percentage of the touchpad, horizontal 89 | movement of the touch is interpreted as horizontal scrolling 90 | top-side-horizontal-scroll-percent: 91 | type: int 92 | default: 10 93 | description: | 94 | Height of the upper part of the touchpad that activates horizontal scroll when the touch starts there. 95 | A lower value reduces accicental activation during normal usage, but requires better targeting 96 | to activate. 97 | The default value is a good compromise that works for me. 98 | 99 | wait-for-new-position-ms: 100 | type: int 101 | default: 30 102 | description: | 103 | The time it takes the touchpad to generate a new position in normal usage. 104 | Lower values result in better detection when a touch ends, but might trigger accidental 105 | taps or other gesture if a new position gets reported after the wait-for-new-position-ms 106 | while the current touch is ongoing. 107 | Since Cirque Glidepoint touchpads report around every 10 ms, and some overhead to account 108 | for intense usage, the default value allows reliable tap detection while being quick enough that 109 | I can't notice it. 110 | -------------------------------------------------------------------------------- /src/circular_scroll.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "input_processor_gestures.h" 6 | #include "circular_scroll.h" 7 | 8 | #define PI 3.14159265358979323846f 9 | 10 | LOG_MODULE_DECLARE(gestures, CONFIG_ZMK_LOG_LEVEL); 11 | 12 | static bool is_touch_on_perimeter(struct gesture_event_t *event, struct gesture_config *config, struct gesture_data *data) { 13 | uint32_t squared_distance = (event->x - data->circular_scroll.half_width) * (event->x - data->circular_scroll.half_width) + 14 | (event->y - data->circular_scroll.half_width) * (event->y - data->circular_scroll.half_width); 15 | return (squared_distance >= data->circular_scroll.inner_radius_squared && 16 | squared_distance <= data->circular_scroll.outer_radius_squared); 17 | } 18 | 19 | static uint16_t calculate_angle(struct gesture_event_t *event, struct gesture_config *config, struct gesture_data *data) { 20 | float angleRadians = atan2f(event->x - data->circular_scroll.half_width, event->y - data->circular_scroll.half_height); 21 | float angleDegrees = angleRadians * (180.0f / PI); 22 | if (angleDegrees < 0) { 23 | angleDegrees += 360.0f; 24 | } 25 | return (uint16_t)angleDegrees; 26 | } 27 | 28 | static float normalizeAngleDifference(uint16_t angle1, uint16_t angle2) { 29 | float difference = (float)angle2 - (float)angle1; 30 | while (difference > 180.0f) { 31 | difference -= 360.0f; 32 | } 33 | while (difference < -180.0f) { 34 | difference += 360.0f; 35 | } 36 | return difference; 37 | } 38 | 39 | int circular_scroll_handle_start(const struct device *dev, struct gesture_event_t *event) { 40 | struct gesture_data *data = (struct gesture_data *)dev->data; 41 | struct gesture_config *config = (struct gesture_config *)dev->config; 42 | if (!config->circular_scroll.enabled || !event->absolute) { 43 | return -1; 44 | } 45 | 46 | if (is_touch_on_perimeter(event, config, data)) { 47 | data->circular_scroll.is_tracking = true; 48 | data->circular_scroll.previous_angle = calculate_angle(event, config, data); 49 | LOG_DBG("starting circular scrolling with angle %d!", data->circular_scroll.previous_angle); 50 | } 51 | 52 | return 0; 53 | } 54 | 55 | int circular_scroll_handle_touch(const struct device *dev, struct gesture_event_t *event) { 56 | struct gesture_config *config = (struct gesture_config *)dev->config; 57 | struct gesture_data *data = (struct gesture_data *)dev->data; 58 | 59 | if (!config->circular_scroll.enabled || 60 | !data->circular_scroll.is_tracking) { 61 | return -1; 62 | } 63 | 64 | if (event->absolute) { 65 | uint16_t current_angle = calculate_angle(event, config, data); 66 | event->raw_event_1->code = 0; 67 | event->raw_event_1->type = 0; 68 | event->raw_event_1->value = 0; 69 | 70 | event->raw_event_2->code = INPUT_REL_WHEEL; 71 | event->raw_event_2->type = INPUT_EV_REL; 72 | event->raw_event_2->value = normalizeAngleDifference(current_angle, data->circular_scroll.previous_angle); 73 | 74 | data->circular_scroll.previous_angle = current_angle; 75 | } 76 | 77 | return 0; 78 | } 79 | 80 | int circular_scroll_handle_end(const struct device *dev) { 81 | struct gesture_data *data = (struct gesture_data *)dev->data; 82 | 83 | if (data->circular_scroll.is_tracking) { 84 | data->circular_scroll.is_tracking = false; 85 | } 86 | 87 | return 0; 88 | } 89 | 90 | int circular_scroll_init(const struct device *dev) { 91 | struct gesture_config *config = (struct gesture_config *)dev->config; 92 | struct gesture_data *data = (struct gesture_data *)dev->data; 93 | LOG_DBG("circular_scroll: %s, rim_percent: %d, width: %d, height: %d", 94 | config->circular_scroll.enabled ? "yes" : "no", 95 | config->circular_scroll.circular_scroll_rim_percent, 96 | config->circular_scroll.width, 97 | config->circular_scroll.height); 98 | 99 | if (!config->circular_scroll.enabled) { 100 | return -1; 101 | } 102 | 103 | // Предварительный расчёт для оптимизации обработки событий касания 104 | data->circular_scroll.half_width = config->circular_scroll.width / 2; 105 | data->circular_scroll.half_height = config->circular_scroll.height / 2; 106 | 107 | uint16_t threshold = (config->circular_scroll.width + config->circular_scroll.height) * (config->circular_scroll.circular_scroll_rim_percent / 2); 108 | threshold /= 100; 109 | uint16_t inner_radius = data->circular_scroll.half_width - threshold; 110 | data->circular_scroll.inner_radius_squared = inner_radius * inner_radius; 111 | data->circular_scroll.outer_radius_squared = data->circular_scroll.half_width * data->circular_scroll.half_width; 112 | 113 | return 0; 114 | } 115 | -------------------------------------------------------------------------------- /src/input_processor_gestures.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 The ZMK Contributors 3 | * 4 | * SPDX_License_Identifier: MIT 5 | */ 6 | 7 | #define DT_DRV_COMPAT zmk_input_processor_gestures 8 | 9 | #include 10 | #include 11 | #include 12 | #include "input_processor_gestures.h" 13 | 14 | #include "touch_detection.h" 15 | #include "tap_detection.h" 16 | #include "circular_scroll.h" 17 | #include "inertial_cursor.h" 18 | 19 | 20 | LOG_MODULE_REGISTER(gestures, CONFIG_ZMK_LOG_LEVEL); 21 | 22 | static void handle_init(const struct device *dev) { 23 | touch_detection_init(dev); 24 | tap_detection_init(dev); 25 | circular_scroll_init(dev); 26 | inertial_cursor_init(dev); 27 | } 28 | 29 | static int handle_touch_start(const struct device *dev, struct gesture_event_t *event) { 30 | LOG_DBG("handle_touch_start"); 31 | circular_scroll_handle_start(dev, event); 32 | tap_detection_handle_start(dev, event); 33 | inertial_cursor_handle_touch_start(dev, event); 34 | return 0; 35 | } 36 | 37 | static int handle_touch(const struct device *dev, struct gesture_event_t *event) { 38 | LOG_DBG("handle_touch_ongoing"); 39 | circular_scroll_handle_touch(dev, event); 40 | tap_detection_handle_touch(dev, event); 41 | inertial_cursor_handle_touch(dev, event); 42 | return 0; 43 | } 44 | 45 | static int handle_touch_end(const struct device *dev) { 46 | LOG_DBG("handle_touch_end"); 47 | circular_scroll_handle_end(dev); 48 | inertial_cursor_handle_end(dev); 49 | return 0; 50 | } 51 | 52 | static int gestures_init(const struct device *dev) { 53 | struct gesture_data *data = (struct gesture_data *)dev->data; 54 | 55 | data->dev = dev; 56 | data->touch_detection.all = data; 57 | data->tap_detection.all = data; 58 | data->circular_scroll.all = data; 59 | data->inertial_cursor.all = data; 60 | 61 | handle_init(dev); 62 | return 0; 63 | } 64 | 65 | 66 | // use touch_detection.c#touch_detection_handle_event as api 67 | static const struct zmk_input_processor_driver_api gestures_driver_api = { 68 | .handle_event = touch_detection_handle_event, 69 | }; 70 | 71 | 72 | #define GESTURES_INST(n) \ 73 | static struct gesture_data gesture_data_##n = { \ 74 | }; \ 75 | static const struct tap_detection_config tap_detection_config_##n = { \ 76 | .enabled = DT_INST_PROP(n, tap_detection), \ 77 | .tap_timout_ms = DT_INST_PROP(n, tap_timout_ms), \ 78 | .prevent_movement_during_tap = DT_INST_PROP(n, prevent_movement_during_tap), \ 79 | }; \ 80 | static const struct touch_detection_config touch_detection_config_##n = { \ 81 | .wait_for_new_position_ms = DT_INST_PROP(n, wait_for_new_position_ms), \ 82 | }; \ 83 | static const struct circular_scroll_config circular_scroll_config_##n = { \ 84 | .enabled = DT_INST_PROP(n, circular_scroll), \ 85 | .circular_scroll_rim_percent = DT_INST_PROP(n, circular_scroll_rim_percent), \ 86 | .width = DT_INST_PROP(n, circular_scroll_width), \ 87 | .height = DT_INST_PROP(n, circular_scroll_height), \ 88 | }; \ 89 | static const struct inertial_cursor_config inertial_cursor_config_##n = { \ 90 | .enabled = DT_INST_PROP(n, inertial_cursor), \ 91 | .velocity_threshold = DT_INST_PROP(n, inertial_cursor_velocity_threshold), \ 92 | .decay_percent = DT_INST_PROP(n, inertial_cursor_decay_percent), \ 93 | }; \ 94 | static const struct gesture_config gesture_config_##n = { \ 95 | .handle_touch_start = &handle_touch_start, \ 96 | .handle_touch_continue = &handle_touch, \ 97 | .handle_touch_end = &handle_touch_end, \ 98 | .tap_detection = tap_detection_config_##n, \ 99 | .touch_detection = touch_detection_config_##n, \ 100 | .circular_scroll = circular_scroll_config_##n, \ 101 | .inertial_cursor = inertial_cursor_config_##n, \ 102 | }; \ 103 | DEVICE_DT_INST_DEFINE(n, gestures_init, (struct pm_device *)DEVICE_DT_GET(DT_NODELABEL(glidepoint)), &gesture_data_##n, \ 104 | &gesture_config_##n, POST_KERNEL, CONFIG_INPUT_GESTURES_INIT_PRIORITY, \ 105 | &gestures_driver_api); 106 | 107 | DT_INST_FOREACH_STATUS_OKAY(GESTURES_INST) 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ZMK INPUT GESTURES 3 | 4 | This repository contains a collection of gestures touchpads like the cirque glidepoint. 5 | 6 | Some of these gestures require absolute positioning data, which isn't natively supported by ZMK, 7 | but a modified driver for the cirque touchpad is available. 8 | 9 | Although most of this code seems to work fine, please note that 10 | it's very new and there absolutely can be bugs that might crash 11 | your keyboard, so that you need to physically press the reset button 12 | to flash a previously known-to-work uf2 file. That won't be a problem for most people, 13 | but not all boards have easy access to these buttons. 14 | 15 | To be clear: right now, this is not for the faint of heart. 16 | 17 | **Before you start, you should make sure that you have a working 18 | input device by following this: https://zmk.dev/docs/features/pointing** 19 | 20 | ## Table of Contents 21 | 22 | * [Relative and Absolute Mode](#relative-and-absolute-mode) 23 | * [Supported gestures](#supported-gestures) 24 | * [Planned gestures](#planned-gestures) 25 | * [Installation & Usage](#installation--usage) 26 | * [Adjust west.yml](#adjust-westyml) 27 | * [Import the dependencies](#import-the-dependencies) 28 | * [Activate absolute mode for cirque glidepoint](#activate-absolute-mode-for-cirque-glidepoint) 29 | * [Translate absolute to relative positions](#translate-absolute-to-relative-positions) 30 | * [Configure some gestures and add them](#configure-some-gestures-and-add-them) 31 | * [Increase the Stack Size](#increase-the-stack-size) 32 | * [Gestures](#gestures) 33 | * [Tap Detection (Absolute and Relative Mode)](#tap-detection-absolute-and-relative-mode) 34 | * [Inertial Cursor (Absolute and Relative Mode)](#inertial-cursor-absolute-and-relative-mode) 35 | * [Circular Scroll (Absolute Mode only!)](#circular-scroll-absolute-mode-only) 36 | * [Right-Side Vertical Scroll (Absolute Mode only! Not implemented.)](#right-side-vertical-scroll-absolute-mode-only-not-implemented) 37 | * [Top-Side Horizontal Scroll (Absolute Mode only! Not implemented.)](#top-side-horizontal-scroll-absolute-mode-only-not-implemented) 38 | * [Wait for New Position](#wait-for-new-position) 39 | * [Configuration options for absolute mode in cirque glidepad driver](#configuration-options-for-absolute-mode-in-cirque-glidepad-driver) 40 | * [Absolute Mode](#absolute-mode) 41 | 42 | 43 | ## Relative and Absolute Mode 44 | 45 | In **relative mode**, the input device reports the change that happened 46 | relative to the previous state. This is the normal behavior for a 47 | mouse: moving the mouse up will report only that the mouse has 48 | moved an amount of pixels up. 49 | 50 | In **absolute mode** however, the input device reports the position 51 | of a touch on a touchpad: the top left might be x=1024/y=998 or so. 52 | 53 | In order to detect whether or not a touch has happened on the outer perimeter of a touchpad, it's necessary to know the absolute position of the touch, so some of the gestures are 54 | only doing anything if you're using absolute mode. 55 | 56 | As of now, ZMK doesn't support absolute mode natively, so to use the gestures that depend on it, a bit more work is necessary. But not much: the pieces are all there, you just need to put them in the right places. 57 | 58 | ## Supported gestures 59 | 60 | - Tap (all modes): translate quick touches to a mouse click 61 | - Circular Scroll (absolute mode only): translate angular movement to scroll events 62 | - Inertial Cursor (all modes): keep the cursor moving after the touch ends 63 | 64 | ## Planned gestures 65 | 66 | - Right side vertical scroll (absolute mode only): translate vertical movement to vertical scroll events 67 | - Top side horizontal scroll (absolute mode only): translate horizontal movement to horizontal scroll events 68 | 69 | ## Installation & Usage 70 | 71 | To use these gestures, there are several steps necessary: 72 | - adjust the `west.yml` to make new dependencies available 73 | - import the dependencies into your configuration files 74 | - use the dependencies as input listeners 75 | - possibly increase the stack size 76 | 77 | We'll go through these steps one by one now. 78 | 79 | ### Adjust west.yml 80 | 81 | Depending on whether or not you want to use gestures that rely on absolute mode, 82 | you need to add one or more of these: 83 | 84 | - `zmk-input-gestures`: this repo that contains the actual gestures 85 | - `cirque-input-module`: an adjusted version of the normal driver that includes support of absolute mode for the cirque glidepoint. This won't hurt in case you switch to relative mode. **If you use something else than the cirque glidepoint, you're on your own to activate absolute mode.** 86 | - `zmk-input-gestures`: contains an input processor to translate absolute positions to relative positions. This is necessary because ZMK won't interpret absolute positions at all. This won't hurt in case you switch to relative mode. 87 | 88 | Add the necessary repositories to your `west.yml`: 89 | 90 | ```yaml 91 | manifest: 92 | remotes: 93 | - name: zmkfirmware 94 | url-base: https://github.com/zmkfirmware 95 | # Add this: 96 | - name: halfdane 97 | url-base: https://github.com/halfdane/ 98 | projects: 99 | - name: zmk 100 | remote: zmkfirmware 101 | revision: main 102 | import: app/west.yml 103 | # Add this: 104 | - name: zmk-input-gestures 105 | remote: halfdane 106 | revision: main 107 | # Add these if you want to use absolute mode: 108 | - name: zmk-input-processors 109 | remote: halfdane 110 | revision: main 111 | - name: cirque-input-module 112 | remote: halfdane 113 | revision: absolute_mode 114 | 115 | 116 | ``` 117 | 118 | > [!WARNING] 119 | > Run `west update` if you're building on your local machine (not github actions). 120 | 121 | ### Import the dependencies 122 | Since you're already using the `cirque-input-module`, you don't need to import that again. 123 | 124 | Add the necessary includes to your `dtsi`-file: 125 | 126 | ```C 127 | // to use gestures this is always necessary: 128 | #include 129 | 130 | // to translate absolute positions to relative positions. If you don't 131 | // want to use gestures for absolute mode, and you don't want to activate it, 132 | // you can ignore this line 133 | #include 134 | ``` 135 | 136 | At this point, your firmware should compile just fine and not behave any differently. 137 | Please build and flash to make sure that's true! 138 | 139 | ### Activate absolute mode for cirque glidepoint 140 | 141 | If you don't want to use absolute mode, you can skip this. 142 | 143 | Adjust the configuration of your cirque glidepoint to activate absolute mode if you wish to use gestures that depend on it. If you don't want any of these gestures, you can skip this step. 144 | 145 | This should be somewhere in your dtsi-files. 146 | If you don't have such a configuration yet, please make sure your cirque glidepoint works 147 | as expected *without* gestures an absolute mode as described here: https://zmk.dev/docs/features/pointing 148 | 149 | ```devicetree 150 | 151 | glidepoint: ... { 152 | compatible = "cirque,pinnacle"; 153 | 154 | ... 155 | 156 | // if you used no-taps before, that's not necessary for absolute mode 157 | // because cirque doesn't detect taps in absolute mode anyways. 158 | // You can comment the setting or just remove it completely. 159 | // no-taps; 160 | 161 | // Add this to activate absolute mode. 162 | // Unless you translate absolute to relative positions as described below, 163 | // your mouse cursor won't move anymore 164 | absolute-mode; 165 | }; 166 | 167 | ``` 168 | 169 | Your firmware should compile fine, but if you activated absolute mode, the mouse cursor shouldn't move anymore. Please build and flash to make sure that's true! 170 | 171 | ### Translate absolute to relative positions 172 | If you don't want to use absolute mode, you can skip this. 173 | 174 | Since your mouse cursor doesn't move anymore, it's time to change that with the input processor `zip_absolute_to_relative`. 175 | You probably already have a place where you've added input processors as described here: https://zmk.dev/docs/keymaps/input-processors. 176 | 177 | Add the processor to the list of used input processors: 178 | 179 | ```devicetree 180 | input-processors = < 181 | // Add this. It should come before the inbuilt input processsors that 182 | // rely on relative mode: 183 | &zip_absolute_to_relative 184 | 185 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 186 | &zip_temp_layer 3 100 187 | >; 188 | ``` 189 | 190 | Your firmware should compile fine, and the mouse cursor should move again. Please build and flash to make sure that's true! 191 | 192 | ### Configure some gestures and add them 193 | 194 | Add the configuration for the gesture input processor before the input listener that you adjusted in the step before. It should be `compatible = "zmk,input-listener";` 195 | 196 | This configuration should be at the top level of your devicetree, not within any brackets or other definitions: 197 | 198 | ```devicetree 199 | &zip_gestures { 200 | // Activate gestures and change their configuration as you see fit. 201 | // for details, see the section below that describes all gestures and 202 | // their configuration options. 203 | 204 | // This will create a mouseclick if your touch is short enough. 205 | // if you're running in relative mode and don't use no-taps, this might 206 | // result in duplicate taps 207 | tap-detection; 208 | }; 209 | ``` 210 | 211 | Add the gesture input processor to the list of input-processors: 212 | 213 | ```devicetree 214 | input-processors = < 215 | // the gesture input processor should come before 216 | // zip_absolute_to_relative 217 | &zip_gestures 218 | 219 | // You have this line only if you're using absolute mode 220 | &zip_absolute_to_relative 221 | 222 | &zip_xy_transform (INPUT_TRANSFORM_XY_SWAP | INPUT_TRANSFORM_Y_INVERT) 223 | &zip_temp_layer 3 100 224 | >; 225 | ``` 226 | 227 | ### Increase the Stack Size 228 | 229 | You may encounter instabilities when using calculation intense gestures. 230 | I found that increasing stack sizes helps. 231 | 232 | Add this to your `.conf` file: 233 | 234 | ``` 235 | CONFIG_MAIN_STACK_SIZE=4096 236 | CONFIG_INPUT_THREAD_STACK_SIZE=4096 237 | ``` 238 | 239 | ## Gestures 240 | 241 | Activate and configure the gestures by adding the corresponding lines to the predefined `&zip_gesture` container like so: 242 | 243 | ```plaintext 244 | &zip_gestures { 245 | tap-detection; 246 | tap-timout-ms=<120>; 247 | }; 248 | ``` 249 | 250 | The given numbers are default values that seem to work well for me. If you don't add a value, the default is used. 251 | Default value for booleans is `false`. 252 | 253 | 254 | ### Tap Detection (Absolute and Relative Mode) 255 | 256 | **Description:** 257 | A touch that doesn't last longer than `tap-timout-ms` is interpreted as a tap (click). 258 | 259 | **Configuration Options:** 260 | - `tap-detection;`: Activates the tap detection feature. 261 | - `tap-timout-ms=<120>;`: Sets the timeout in milliseconds for detecting a tap. A lower value requires a quicker tap motion but reduces accidental taps for short movements. 262 | - `prevent_movement_during_tap;`: While determining if the beginning of a touch is a tap, ignore all other movements. This prevents accidental movement away from the tap-target but makes the beginning of regular touches more sluggish. 263 | 264 | ### Inertial Cursor (Absolute and Relative Mode) 265 | 266 | **Description:** 267 | If the cursor moves faster than `inertial-cursor-velocity-threshold-ms` when the touch ends, the cursor keeps moving in the same direction, gradually slowing down by `inertial-cursor-decay-percent`. 268 | 269 | **Configuration Options:** 270 | - `inertial-cursor;`: Activates the inertial cursor feature. 271 | - `inertial-cursor-velocity-threshold-ms=<20>;`: Sets the velocity threshold in milliseconds for activating inertial movement. A lower value makes it easier to activate but increases accidental activation. 272 | - `inertial-cursor-decay-percent=<98>;`: Sets the decay percentage for the cursor's inertial movement. A lower value makes the cursor move longer after the touch ends, mimicking lower friction. 273 | 274 | ### Circular Scroll (Absolute Mode only!) 275 | 276 | **Description:** 277 | If a touch begins on the outer `circular-scroll-rim-percent` percentage of the touchpad, angular movement of the touch is interpreted as scrolling down (clockwise) or up (counter-clockwise). 278 | Make sure you have absolute mode activated, otherwise this won't do anything 279 | 280 | **Configuration Options:** 281 | - `circular-scroll;`: Activates the circular scroll feature. 282 | - `circular-scroll-rim-percent=<10>;`: Sets the percentage width of the outer ring of the touchpad that activates circular scroll. A lower value reduces accidental activation during normal usage but requires better targeting to activate. 283 | - `circular-scroll-width=<1024>;`: Sets the width of the touchpad. If your device driver supports scaling to a target interval you should make sure to use the same values. See the [section about the cirque-driver](#configuration-options-for-absolute-mode-in-cirque-glidepad-driver) below, if you want to change this, but you probably shouldn't. 284 | - `circular-scroll-height=<1024>;`: Sets the height of the touchpad. If your device driver supports scaling to a target interval you should make sure to use the same values. See the [section about the cirque-driver](#configuration-options-for-absolute-mode-in-cirque-glidepad-driver) below, if you want to change this, but you probably shouldn't. 285 | 286 | ### Right-Side Vertical Scroll (Absolute Mode only! Not implemented.) 287 | 288 | THIS ISN'T IMPLEMENTED YET 289 | 290 | > **Description:** 291 | > If a touch begins on the right `right-side-vertical-scroll-percent` percentage of the touchpad, vertical movement of the touch is interpreted as vertical scrolling. 292 | > 293 | > **Configuration Options:** 294 | > - `right-side-vertical-scroll;`: Activates the right-side vertical scroll feature. 295 | > - `right-side-vertical-scroll-percent=<10>;`: Sets the percentage width of the right part of the touchpad that activates vertical scroll. A lower value reduces accidental activation during normal usage but requires better targeting to activate. 296 | 297 | ### Top-Side Horizontal Scroll (Absolute Mode only! Not implemented.) 298 | 299 | THIS ISN'T IMPLEMENTED YET! 300 | 301 | > **Description:** 302 | > If a touch begins on the top `top-side-horizontal-scroll-percent` percentage of the touchpad, horizontal movement of the touch is interpreted as horizontal scrolling. 303 | > 304 | > **Configuration Options:** 305 | > - `top-side-horizontal-scroll;`: Activates the top-side horizontal scroll feature. 306 | > - `top-side-horizontal-scroll-percent=<10>;`: Sets the percentage height of the upper part of the touchpad that activates horizontal scroll. A lower value reduces accidental activation during normal usage but requires better targeting to activate. 307 | 308 | ### Wait for New Position 309 | 310 | **Description:** 311 | The time it takes the touchpad to generate a new position in normal usage. Lower values result in better detection when a touch ends but might trigger accidental taps or other gestures if a new position gets reported after the wait time while the current touch is ongoing. 312 | 313 | **Configuration Options:** 314 | - `wait-for-new-position-ms=<30>`: Sets the time in milliseconds to wait for a new position. The default value allows reliable tap detection while being quick enough to go unnoticed. 315 | 316 | 317 | ## Configuration options for absolute mode in cirque glidepad driver 318 | 319 | ### Absolute Mode 320 | 321 | **Description:** 322 | Report the position of a touch on the touchpad as absolute positions instead of just the change relative to the previous touch. 323 | 324 | **Configuration Options:** 325 | - `absolute-mode;`: Activates the absolute mode 326 | - `absolute-mode-scale-to-width=<1024>;`: Scale reported X-positions so they are in the interval [0-1024]. 327 | - `absolute-mode-scale-to-height=<1024>;`: Scale reported X-positions so they are in the interval [0-1024]. 328 | --------------------------------------------------------------------------------