Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(behaviors): add non-overlap behavior #2391

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE src/behaviors/behavior_to_layer.c)
target_sources(app PRIVATE src/behaviors/behavior_transparent.c)
target_sources(app PRIVATE src/behaviors/behavior_none.c)
target_sources(app PRIVATE src/behaviors/behavior_non_overlap.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE app PRIVATE src/behaviors/behavior_sensor_rotate.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c)
target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON app PRIVATE src/behaviors/behavior_sensor_rotate_common.c)
Expand Down
1 change: 1 addition & 0 deletions app/dts/behaviors.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
#include <behaviors/macros.dtsi>
#include <behaviors/mouse_key_press.dtsi>
#include <behaviors/soft_off.dtsi>
#include <behaviors/non_overlap.dtsi>
18 changes: 18 additions & 0 deletions app/dts/behaviors/non_overlap.dtsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <dt-bindings/zmk/keys.h>

/ {
behaviors {
/omit-if-no-ref/ nkp: non_overlap_key_press {
compatible = "zmk,behavior-non-overlap";
#binding-cells = <1>;
bindings = <&kp>;
display-name = "Non-overlap";
};
};
};
17 changes: 17 additions & 0 deletions app/dts/bindings/behaviors/zmk,behavior-non-overlap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT

description: Non-overlap behavior

compatible: "zmk,behavior-non-overlap"

include: one_param.yaml

properties:
bindings:
type: phandles
required: true
keep-active-size:
type: int
no-active:
type: boolean
278 changes: 278 additions & 0 deletions app/src/behaviors/behavior_non_overlap.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_behavior_non_overlap

#include <zephyr/device.h>
#include <drivers/behavior.h>
#include <zephyr/logging/log.h>

#include <zmk/behavior.h>

LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);

#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)

struct active_non_overlap {
struct active_non_overlap *previous;
struct active_non_overlap *next;
bool is_pressed;
uint32_t position;
struct zmk_behavior_binding binding;
};

struct behavior_non_overlap_config {
const char *behavior_dev;
};

struct behavior_non_overlap_data {
struct active_non_overlap *head;
struct active_non_overlap *tail;
struct active_non_overlap *actives;
const size_t keep_active_size;
};

/*
* Non-overlap key presses are kept in a static array of `struct active_non_overlap`.
* The array size defaults to 10 if `keep-active-size` is not specified.
* The array size limits the number of key presses that non-overlap behavior can remember.
* Each instance of non-overlap behavior has its own array.
*
* Non-overlap behavior must preserve the order of key presses.
* Linked list is implemented to allow efficient queue operations.
* When the number of key presses reaches its limit,
* previous key presses will be deleted to accommodate new key presses in a FIFO manner.
*/

static inline struct active_non_overlap *find_empty_slot(struct behavior_non_overlap_data *data) {
const size_t keep_active_size = data->keep_active_size;
struct active_non_overlap *actives = data->actives;

for (int i = 0; i < keep_active_size; i++) {
struct active_non_overlap *active = &actives[i];
if (!active->is_pressed) {
return active;
}
}

return NULL;
}

static inline bool matches_params(const struct zmk_behavior_binding *this,
const struct zmk_behavior_binding *that) {
return this->param1 == that->param1 && this->param2 == that->param2;
}

static inline bool matches_active(struct zmk_behavior_binding *binding, uint32_t position,
const struct active_non_overlap *active) {
return position == active->position && matches_params(binding, &active->binding);
}

static inline struct active_non_overlap *find_active(struct zmk_behavior_binding *binding,
uint32_t position,
struct active_non_overlap *tail) {
for (struct active_non_overlap *active = tail; active != NULL; active = active->previous) {
if (matches_active(binding, position, active)) {
return active;
}
}

return NULL;
}

static inline void release_binding(struct active_non_overlap *active, int64_t timestamp) {
struct zmk_behavior_binding_event event = {
.position = active->position,
.timestamp = timestamp,
};

behavior_keymap_binding_released(&active->binding, event);
}

static inline void press_binding(struct active_non_overlap *active, int64_t timestamp) {
struct zmk_behavior_binding_event event = {
.position = active->position,
.timestamp = timestamp,
};

behavior_keymap_binding_pressed(&active->binding, event);
}

static int behavior_non_overlap_init(const struct device *dev) { return 0; };

static int on_non_overlap_binding_pressed(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position = %d, param1 = 0x%02X.", event.position, binding->param1);

const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev);
struct behavior_non_overlap_data *data = dev->data;
struct active_non_overlap *least_recent_active = data->head;
struct active_non_overlap *most_recent_active = data->tail;
struct active_non_overlap *new_active;

if (most_recent_active == NULL) {
LOG_DBG("First active.");
new_active = data->actives;

data->head = new_active;
} else {
LOG_DBG("New active. Release the previous active.");
release_binding(most_recent_active, event.timestamp);

new_active = find_empty_slot(data);
if (new_active == NULL) {
new_active = least_recent_active;
most_recent_active->next = least_recent_active;

data->head = least_recent_active->next;
data->head->previous = NULL;
}

most_recent_active->next = new_active;
}

if (new_active == NULL) {
LOG_ERR("New active is not available for some reason.");
return ZMK_BEHAVIOR_OPAQUE;
}

new_active->is_pressed = true;
new_active->position = event.position;
new_active->binding.param1 = binding->param1;
new_active->binding.param2 = binding->param2;
press_binding(new_active, event.timestamp);

if (new_active != most_recent_active) {
new_active->previous = most_recent_active;
}
new_active->next = NULL;

data->tail = new_active;

return ZMK_BEHAVIOR_OPAQUE;
}

static int on_non_overlap_binding_released(struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event) {
LOG_DBG("position = %d, param1 = 0x%02X.", event.position, binding->param1);

const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev);
struct behavior_non_overlap_data *data = dev->data;
struct active_non_overlap *least_recent_active = data->head;
struct active_non_overlap *most_recent_active = data->tail;
struct active_non_overlap *active = find_active(binding, event.position, most_recent_active);
const int64_t timestamp = event.timestamp;

if (active == NULL) {
LOG_DBG("No existing active. Nothing to do here.");
return ZMK_BEHAVIOR_OPAQUE;
}

active->is_pressed = false;

if (active == most_recent_active) {
LOG_DBG("This is the most recent active. Release it.");
release_binding(most_recent_active, timestamp);

most_recent_active = most_recent_active->previous;
if (most_recent_active != NULL) {
LOG_DBG("Previous active exists. Re-press it.");
press_binding(most_recent_active, timestamp);

most_recent_active->next = NULL;
}

data->tail = most_recent_active;
return ZMK_BEHAVIOR_OPAQUE;
}

active->next->previous = active->previous;

if (active == least_recent_active) {
data->head = least_recent_active->next;
} else {
active->previous->next = active->next;
}

LOG_DBG("Matched active deleted.");
return ZMK_BEHAVIOR_OPAQUE;
}

#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)

static int non_overlap_parameter_metadata(const struct device *non_overlap,
struct behavior_parameter_metadata *param_metadata) {
const struct behavior_non_overlap_config *cfg = non_overlap->config;
struct behavior_parameter_metadata child_metadata;

int err = behavior_get_parameter_metadata(zmk_behavior_get_binding(cfg->behavior_dev),
&child_metadata);
if (err < 0) {
LOG_WRN("Failed to get the non-overlap behavior parameter: %d", err);
return err;
}

for (int s = 0; s < child_metadata.sets_len; s++) {
const struct behavior_parameter_metadata_set *set = &child_metadata.sets[s];

if (set->param2_values_len > 0) {
return -ENOTSUP;
}
}

*param_metadata = child_metadata;

return 0;
}

#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)

static struct behavior_driver_api behavior_non_overlap_driver_api = {
.binding_pressed = on_non_overlap_binding_pressed,
.binding_released = on_non_overlap_binding_released,
#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
.get_parameter_metadata = non_overlap_parameter_metadata,
#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA)
};

#define _SET_NULL(i, inst) \
{ \
.previous = NULL, \
.next = NULL, \
.is_pressed = false, \
.position = 0, \
.binding = \
{ \
.behavior_dev = behavior_non_overlap_config_##inst.behavior_dev, \
.param1 = 0, \
.param2 = 0, \
}, \
}

#define _KEEP_ACTIVE_SIZE(inst) \
COND_CODE_1(DT_INST_PROP(inst, no_active), (1), (DT_INST_PROP_OR(inst, keep_active_size, 10)))

#define _SET_ACTIVES(inst) {LISTIFY(_KEEP_ACTIVE_SIZE(inst), _SET_NULL, (, ), inst)}

#define KP_INST(inst) \
static const struct behavior_non_overlap_config behavior_non_overlap_config_##inst = { \
.behavior_dev = DEVICE_DT_NAME(DT_INST_PHANDLE_BY_IDX(inst, bindings, 0)), \
}; \
static struct active_non_overlap actives_##inst[] = _SET_ACTIVES(inst); \
static struct behavior_non_overlap_data behavior_non_overlap_data_##inst = { \
.head = NULL, \
.tail = NULL, \
.actives = actives_##inst, \
.keep_active_size = _KEEP_ACTIVE_SIZE(inst), \
}; \
BEHAVIOR_DT_INST_DEFINE( \
inst, behavior_non_overlap_init, NULL, &behavior_non_overlap_data_##inst, \
&behavior_non_overlap_config_##inst, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
&behavior_non_overlap_driver_api);

DT_INST_FOREACH_STATUS_OKAY(KP_INST)

#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */
1 change: 1 addition & 0 deletions app/tests/non-overlap/basic/events.patterns
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
s/.*hid_listener_keycode_//p
6 changes: 6 additions & 0 deletions app/tests/non-overlap/basic/keycode_events.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
19 changes: 19 additions & 0 deletions app/tests/non-overlap/basic/native_posix_64.keymap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>
#include "../behavior_keymap.dtsi"

/* Basic non-overlap test. */

&kscan {
events = <
// Press &nkp A
ZMK_MOCK_PRESS(0,0,10)
// Tap &nkp D
ZMK_MOCK_PRESS(1,0,10)
ZMK_MOCK_RELEASE(1,0,10)
// &kp A is re-pressed after releasing &nkp D
// &kp A remains pressed until &nkp A is released
ZMK_MOCK_RELEASE(0,0,10)
>;
};
21 changes: 21 additions & 0 deletions app/tests/non-overlap/behavior_keymap.dtsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan_mock.h>

/ {
keymap {
compatible = "zmk,keymap";

default_layer {
bindings = <
&nkp A &kp W
&nkp D &mo 1>;
};

lower_layer {
bindings = <
&kp LEFT_SHIFT &kp LEFT_CONTROL
&kp Y &kp Z>;
};
};
};
1 change: 1 addition & 0 deletions app/tests/non-overlap/keep-active-size/events.patterns
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
s/.*hid_listener_keycode_//p
10 changes: 10 additions & 0 deletions app/tests/non-overlap/keep-active-size/keycode_events.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pressed: usage_page 0x07 keycode 0x1A implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x1A implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x07 implicit_mods 0x00 explicit_mods 0x00
pressed: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00
released: usage_page 0x07 keycode 0x16 implicit_mods 0x00 explicit_mods 0x00
Loading
Loading