forked from kofal.net/zmk
feat(mouse): Add mouse move and scroll support (#2477)
* feat(mouse): Add mouse move and scroll support
* Use Zephyr input subsystem for all pointers.
* Input processors for modifying events, e.g. scaling, swapping
codes, temporary (mouse) layers, etc.
* Mouse move/scroll behaviors.
* Infrastructure in place for physical pointer input devices.
* feat: Add input split support.
* docs: Add initial pointer docs.
---------
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
Co-authored-by: Alexander Krikun <krikun98@gmail.com>
Co-authored-by: Robert U <urob@users.noreply.github.com>
Co-authored-by: Shawn Meier <ftc@users.noreply.github.com>
Co-authored-by: Chris Andreae <chris@andreae.gen.nz>
Co-authored-by: Anant Thazhemadam <47104651+thazhemadam@users.noreply.github.com>
Co-authored-by: Erik Tollerud <erik.tollerud@gmail.com>
Co-authored-by: Nicolas Munnich <98408764+Nick-Munnich@users.noreply.github.com>
This commit is contained in:
304
app/src/behaviors/behavior_input_two_axis.c
Normal file
304
app/src/behaviors/behavior_input_two_axis.c
Normal file
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* Copyright (c) 2024 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#define DT_DRV_COMPAT zmk_behavior_input_two_axis
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <drivers/behavior.h>
|
||||
#include <zephyr/input/input.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/sys/util.h> // CLAMP
|
||||
|
||||
#include <zmk/behavior.h>
|
||||
#include <dt-bindings/zmk/pointing.h>
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
|
||||
#include <zmk/pointing/resolution_multipliers.h>
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
struct vector2d {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
struct movement_state_1d {
|
||||
float remainder;
|
||||
int16_t speed;
|
||||
int64_t start_time;
|
||||
};
|
||||
|
||||
struct movement_state_2d {
|
||||
struct movement_state_1d x;
|
||||
struct movement_state_1d y;
|
||||
};
|
||||
|
||||
struct behavior_input_two_axis_data {
|
||||
struct k_work_delayable tick_work;
|
||||
const struct device *dev;
|
||||
|
||||
struct movement_state_2d state;
|
||||
};
|
||||
|
||||
struct behavior_input_two_axis_config {
|
||||
int16_t x_code;
|
||||
int16_t y_code;
|
||||
uint16_t delay_ms;
|
||||
uint16_t time_to_max_speed_ms;
|
||||
uint8_t trigger_period_ms;
|
||||
// acceleration exponent 0: uniform speed
|
||||
// acceleration exponent 1: uniform acceleration
|
||||
// acceleration exponent 2: uniform jerk
|
||||
uint8_t acceleration_exponent;
|
||||
};
|
||||
|
||||
#if CONFIG_MINIMAL_LIBC
|
||||
static float powf(float base, float exponent) {
|
||||
// poor man's power implementation rounds the exponent down to the nearest integer.
|
||||
float power = 1.0f;
|
||||
for (; exponent >= 1.0f; exponent--) {
|
||||
power = power * base;
|
||||
}
|
||||
return power;
|
||||
}
|
||||
#else
|
||||
#include <math.h>
|
||||
#endif
|
||||
|
||||
static int64_t ticks_since_start(int64_t start, int64_t now, int64_t delay) {
|
||||
if (start == 0) {
|
||||
return 0;
|
||||
}
|
||||
int64_t move_duration = now - (start + delay);
|
||||
// start can be in the future if there's a delay
|
||||
if (move_duration < 0) {
|
||||
move_duration = 0;
|
||||
}
|
||||
return move_duration;
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
|
||||
|
||||
static uint8_t get_acceleration_exponent(const struct behavior_input_two_axis_config *config,
|
||||
uint16_t code) {
|
||||
switch (code) {
|
||||
case INPUT_REL_WHEEL:
|
||||
return (zmk_pointing_resolution_multipliers_get_current_profile().wheel > 0)
|
||||
? 0
|
||||
: config->acceleration_exponent;
|
||||
case INPUT_REL_HWHEEL:
|
||||
return (zmk_pointing_resolution_multipliers_get_current_profile().hor_wheel > 0)
|
||||
? 0
|
||||
: config->acceleration_exponent;
|
||||
default:
|
||||
return config->acceleration_exponent;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static inline uint8_t get_acceleration_exponent(const struct behavior_input_two_axis_config *config,
|
||||
uint16_t code) {
|
||||
return config->acceleration_exponent;
|
||||
}
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_POINTING_SMOOTH_SCROLLING)
|
||||
|
||||
static float speed(const struct behavior_input_two_axis_config *config, uint16_t code,
|
||||
float max_speed, int64_t duration_ticks) {
|
||||
uint8_t accel_exp = get_acceleration_exponent(config, code);
|
||||
|
||||
if ((1000 * duration_ticks / CONFIG_SYS_CLOCK_TICKS_PER_SEC) > config->time_to_max_speed_ms ||
|
||||
config->time_to_max_speed_ms == 0 || accel_exp == 0) {
|
||||
return max_speed;
|
||||
}
|
||||
|
||||
// Calculate the speed based on MouseKeysAccel
|
||||
// See https://en.wikipedia.org/wiki/Mouse_keys
|
||||
if (duration_ticks == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float time_fraction = (float)(1000 * duration_ticks / CONFIG_SYS_CLOCK_TICKS_PER_SEC) /
|
||||
config->time_to_max_speed_ms;
|
||||
return max_speed * powf(time_fraction, accel_exp);
|
||||
}
|
||||
|
||||
static void track_remainder(float *move, float *remainder) {
|
||||
float new_move = *move + *remainder;
|
||||
*remainder = new_move - (int)new_move;
|
||||
*move = (int)new_move;
|
||||
}
|
||||
|
||||
static float update_movement_1d(const struct behavior_input_two_axis_config *config, uint16_t code,
|
||||
struct movement_state_1d *state, int64_t now) {
|
||||
float move = 0;
|
||||
if (state->speed == 0) {
|
||||
state->remainder = 0;
|
||||
return move;
|
||||
}
|
||||
|
||||
int64_t move_duration = ticks_since_start(state->start_time, now, config->delay_ms);
|
||||
LOG_DBG("Calculated speed: %f", speed(config, code, state->speed, move_duration));
|
||||
move =
|
||||
(move_duration > 0)
|
||||
? (speed(config, code, state->speed, move_duration) * config->trigger_period_ms / 1000)
|
||||
: 0;
|
||||
|
||||
track_remainder(&(move), &(state->remainder));
|
||||
|
||||
return move;
|
||||
}
|
||||
static struct vector2d update_movement_2d(const struct behavior_input_two_axis_config *config,
|
||||
struct movement_state_2d *state, int64_t now) {
|
||||
struct vector2d move = {0};
|
||||
|
||||
move = (struct vector2d){
|
||||
.x = update_movement_1d(config, config->x_code, &state->x, now),
|
||||
.y = update_movement_1d(config, config->y_code, &state->y, now),
|
||||
};
|
||||
|
||||
return move;
|
||||
}
|
||||
|
||||
static bool is_non_zero_1d_movement(int16_t speed) { return speed != 0; }
|
||||
|
||||
static bool is_non_zero_2d_movement(struct movement_state_2d *state) {
|
||||
return is_non_zero_1d_movement(state->x.speed) || is_non_zero_1d_movement(state->y.speed);
|
||||
}
|
||||
|
||||
static bool should_be_working(struct behavior_input_two_axis_data *data) {
|
||||
return is_non_zero_2d_movement(&data->state);
|
||||
}
|
||||
|
||||
static void tick_work_cb(struct k_work *work) {
|
||||
struct k_work_delayable *d_work = k_work_delayable_from_work(work);
|
||||
struct behavior_input_two_axis_data *data =
|
||||
CONTAINER_OF(d_work, struct behavior_input_two_axis_data, tick_work);
|
||||
const struct device *dev = data->dev;
|
||||
const struct behavior_input_two_axis_config *cfg = dev->config;
|
||||
|
||||
uint64_t timestamp = k_uptime_ticks();
|
||||
|
||||
// LOG_INF("x start: %llu, y start: %llu, current timestamp: %llu", data->state.x.start_time,
|
||||
// data->state.y.start_time, timestamp);
|
||||
|
||||
struct vector2d move = update_movement_2d(cfg, &data->state, timestamp);
|
||||
|
||||
int ret = 0;
|
||||
bool have_x = is_non_zero_1d_movement(move.x);
|
||||
bool have_y = is_non_zero_1d_movement(move.y);
|
||||
if (have_x) {
|
||||
ret = input_report_rel(dev, cfg->x_code, (int16_t)CLAMP(move.x, INT16_MIN, INT16_MAX),
|
||||
!have_y, K_NO_WAIT);
|
||||
}
|
||||
if (have_y) {
|
||||
ret = input_report_rel(dev, cfg->y_code, (int16_t)CLAMP(move.y, INT16_MIN, INT16_MAX), true,
|
||||
K_NO_WAIT);
|
||||
}
|
||||
|
||||
if (should_be_working(data)) {
|
||||
k_work_schedule(&data->tick_work, K_MSEC(cfg->trigger_period_ms));
|
||||
}
|
||||
}
|
||||
|
||||
static void set_start_times_for_activity_1d(struct movement_state_1d *state) {
|
||||
if (state->speed != 0 && state->start_time == 0) {
|
||||
state->start_time = k_uptime_ticks();
|
||||
} else if (state->speed == 0) {
|
||||
state->start_time = 0;
|
||||
}
|
||||
}
|
||||
static void set_start_times_for_activity(struct movement_state_2d *state) {
|
||||
set_start_times_for_activity_1d(&state->x);
|
||||
set_start_times_for_activity_1d(&state->y);
|
||||
}
|
||||
|
||||
static void update_work_scheduling(const struct device *dev) {
|
||||
struct behavior_input_two_axis_data *data = dev->data;
|
||||
const struct behavior_input_two_axis_config *cfg = dev->config;
|
||||
|
||||
set_start_times_for_activity(&data->state);
|
||||
|
||||
if (should_be_working(data)) {
|
||||
k_work_schedule(&data->tick_work, K_MSEC(cfg->trigger_period_ms));
|
||||
} else {
|
||||
k_work_cancel_delayable(&data->tick_work);
|
||||
data->state.y.remainder = 0;
|
||||
data->state.x.remainder = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int behavior_input_two_axis_adjust_speed(const struct device *dev, int16_t dx, int16_t dy) {
|
||||
struct behavior_input_two_axis_data *data = dev->data;
|
||||
|
||||
LOG_DBG("Adjusting: %d %d", dx, dy);
|
||||
data->state.x.speed += dx;
|
||||
data->state.y.speed += dy;
|
||||
|
||||
LOG_DBG("After: %d %d", data->state.x.speed, data->state.y.speed);
|
||||
|
||||
update_work_scheduling(dev);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int behavior_input_two_axis_init(const struct device *dev) {
|
||||
struct behavior_input_two_axis_data *data = dev->data;
|
||||
|
||||
data->dev = dev;
|
||||
k_work_init_delayable(&data->tick_work, tick_work_cb);
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event) {
|
||||
|
||||
const struct device *behavior_dev = zmk_behavior_get_binding(binding->behavior_dev);
|
||||
|
||||
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
|
||||
|
||||
int16_t x = MOVE_X_DECODE(binding->param1);
|
||||
int16_t y = MOVE_Y_DECODE(binding->param1);
|
||||
|
||||
behavior_input_two_axis_adjust_speed(behavior_dev, x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event) {
|
||||
const struct device *behavior_dev = zmk_behavior_get_binding(binding->behavior_dev);
|
||||
|
||||
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
|
||||
|
||||
int16_t x = MOVE_X_DECODE(binding->param1);
|
||||
int16_t y = MOVE_Y_DECODE(binding->param1);
|
||||
|
||||
behavior_input_two_axis_adjust_speed(behavior_dev, -x, -y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct behavior_driver_api behavior_input_two_axis_driver_api = {
|
||||
.binding_pressed = on_keymap_binding_pressed, .binding_released = on_keymap_binding_released};
|
||||
|
||||
#define ITA_INST(n) \
|
||||
static struct behavior_input_two_axis_data behavior_input_two_axis_data_##n = {}; \
|
||||
static struct behavior_input_two_axis_config behavior_input_two_axis_config_##n = { \
|
||||
.x_code = DT_INST_PROP(n, x_input_code), \
|
||||
.y_code = DT_INST_PROP(n, y_input_code), \
|
||||
.trigger_period_ms = DT_INST_PROP(n, trigger_period_ms), \
|
||||
.delay_ms = DT_INST_PROP_OR(n, delay_ms, 0), \
|
||||
.time_to_max_speed_ms = DT_INST_PROP(n, time_to_max_speed_ms), \
|
||||
.acceleration_exponent = DT_INST_PROP_OR(n, acceleration_exponent, 1), \
|
||||
}; \
|
||||
BEHAVIOR_DT_INST_DEFINE( \
|
||||
n, behavior_input_two_axis_init, NULL, &behavior_input_two_axis_data_##n, \
|
||||
&behavior_input_two_axis_config_##n, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
|
||||
&behavior_input_two_axis_driver_api);
|
||||
|
||||
DT_INST_FOREACH_STATUS_OKAY(ITA_INST)
|
||||
@@ -11,8 +11,9 @@
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include <zmk/behavior.h>
|
||||
#include <zmk/event_manager.h>
|
||||
#include <zmk/events/mouse_button_state_changed.h>
|
||||
#include <zmk/hid.h>
|
||||
#include <zephyr/input/input.h>
|
||||
#include <zephyr/dt-bindings/input/input-event-codes.h>
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
@@ -20,19 +21,31 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
static int behavior_mouse_key_press_init(const struct device *dev) { return 0; };
|
||||
|
||||
static void process_key_state(const struct device *dev, int32_t val, bool pressed) {
|
||||
for (int i = 0; i < ZMK_HID_MOUSE_NUM_BUTTONS; i++) {
|
||||
if (val & BIT(i)) {
|
||||
WRITE_BIT(val, i, 0);
|
||||
input_report_key(dev, INPUT_BTN_0 + i, pressed ? 1 : 0, val == 0, K_FOREVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event) {
|
||||
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
|
||||
|
||||
return raise_zmk_mouse_button_state_changed_from_encoded(binding->param1, true,
|
||||
event.timestamp);
|
||||
process_key_state(zmk_behavior_get_binding(binding->behavior_dev), binding->param1, true);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event) {
|
||||
LOG_DBG("position %d keycode 0x%02X", event.position, binding->param1);
|
||||
return raise_zmk_mouse_button_state_changed_from_encoded(binding->param1, false,
|
||||
event.timestamp);
|
||||
|
||||
process_key_state(zmk_behavior_get_binding(binding->behavior_dev), binding->param1, false);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct behavior_driver_api behavior_mouse_key_press_driver_api = {
|
||||
|
||||
Reference in New Issue
Block a user