mirror of https://github.com/zmkfirmware/zmk.git
refactor(combos): Reduce RAM usage, simplify config (#2849)
* Reference combos by index, not 32-bit pointers, and store bitfields instead of arrays in several places, to bring down our flash/RAM usage. * Use bit field to track candidate combos, to avoid needing an explicit `ZMK_COMBO_MAX_COMBOS_PER_KEY` setting. * Determine the max keys per combo automatically from the devicetree, so we remove the ZMK_COMBO_MAX_KEYS_PER_COMBO Kconfig symbol.
This commit is contained in:
parent
d9576c5534
commit
c4ee8ab86b
12
app/Kconfig
12
app/Kconfig
|
|
@ -444,12 +444,16 @@ config ZMK_COMBO_MAX_PRESSED_COMBOS
|
|||
default 4
|
||||
|
||||
config ZMK_COMBO_MAX_COMBOS_PER_KEY
|
||||
int "Maximum number of combos per key"
|
||||
default 5
|
||||
int
|
||||
default 0
|
||||
help
|
||||
Deprecated: Storage for combos is now determined automatically
|
||||
|
||||
config ZMK_COMBO_MAX_KEYS_PER_COMBO
|
||||
int "Maximum number of keys per combo"
|
||||
default 4
|
||||
int
|
||||
default 0
|
||||
help
|
||||
Deprecated: This is now auto-calculated based on `key-positions` in devicetree
|
||||
|
||||
# Combo options
|
||||
endmenu
|
||||
|
|
|
|||
|
|
@ -25,4 +25,3 @@ child-binding:
|
|||
type: boolean
|
||||
layers:
|
||||
type: array
|
||||
default: [-1]
|
||||
|
|
|
|||
380
app/src/combo.c
380
app/src/combo.c
|
|
@ -9,6 +9,7 @@
|
|||
#include <zephyr/device.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/sys/dlist.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
#include <drivers/behavior.h>
|
||||
|
|
@ -26,53 +27,99 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
|||
|
||||
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
|
||||
|
||||
#if CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO > 0
|
||||
|
||||
#warning \
|
||||
"CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO is deprecated, and is auto-calculated from the devicetree now."
|
||||
|
||||
#endif
|
||||
|
||||
#if CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY > 0
|
||||
|
||||
#warning "CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY is deprecated, and is auto-calculated."
|
||||
|
||||
#endif
|
||||
|
||||
#define COMBOS_KEYS_BYTE_ARRAY(node_id) \
|
||||
uint8_t _CONCAT(combo_prop_, node_id)[DT_PROP_LEN(node_id, key_positions)];
|
||||
|
||||
#define MAX_COMBO_KEYS sizeof(union {DT_INST_FOREACH_CHILD(0, COMBOS_KEYS_BYTE_ARRAY)})
|
||||
|
||||
struct combo_cfg {
|
||||
int32_t key_positions[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO];
|
||||
int32_t key_position_len;
|
||||
struct zmk_behavior_binding behavior;
|
||||
int32_t key_positions[MAX_COMBO_KEYS];
|
||||
int16_t key_position_len;
|
||||
int16_t require_prior_idle_ms;
|
||||
int32_t timeout_ms;
|
||||
int32_t require_prior_idle_ms;
|
||||
uint32_t layer_mask;
|
||||
struct zmk_behavior_binding behavior;
|
||||
// if slow release is set, the combo releases when the last key is released.
|
||||
// otherwise, the combo releases when the first key is released.
|
||||
bool slow_release;
|
||||
// the virtual key position is a key position outside the range used by the keyboard.
|
||||
// it is necessary so hold-taps can uniquely identify a behavior.
|
||||
int32_t virtual_key_position;
|
||||
int32_t layers_len;
|
||||
int8_t layers[];
|
||||
};
|
||||
|
||||
struct active_combo {
|
||||
const struct combo_cfg *combo;
|
||||
uint16_t combo_idx;
|
||||
// key_positions_pressed is filled with key_positions when the combo is pressed.
|
||||
// The keys are removed from this array when they are released.
|
||||
// Once this array is empty, the behavior is released.
|
||||
uint32_t key_positions_pressed_count;
|
||||
struct zmk_position_state_changed_event
|
||||
key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO];
|
||||
uint16_t key_positions_pressed_count;
|
||||
struct zmk_position_state_changed_event key_positions_pressed[MAX_COMBO_KEYS];
|
||||
};
|
||||
|
||||
struct combo_candidate {
|
||||
const struct combo_cfg *combo;
|
||||
// the time after which this behavior should be removed from candidates.
|
||||
// by keeping track of when the candidate should be cleared there is no
|
||||
// possibility of accidental releases.
|
||||
int64_t timeout_at;
|
||||
};
|
||||
#define PROP_BIT_AT_IDX(n, prop, idx) BIT(DT_PROP_BY_IDX(n, prop, idx))
|
||||
|
||||
uint32_t pressed_keys_count = 0;
|
||||
#define NODE_PROP_BITMASK(n, prop) \
|
||||
COND_CODE_1(DT_NODE_HAS_PROP(n, prop), \
|
||||
(DT_FOREACH_PROP_ELEM_SEP(n, prop, PROP_BIT_AT_IDX, (|))), (0))
|
||||
|
||||
#define GET_KEY_POSITION_MASK_PORTION(idx, n) ((NODE_PROP_BITMASK(n, key_positions) >> idx) & 0xFF)
|
||||
|
||||
#define COMBO_INST(n, positions) \
|
||||
COND_CODE_1(IS_EQ(DT_PROP_LEN(n, key_positions), positions), \
|
||||
( \
|
||||
{ \
|
||||
.timeout_ms = DT_PROP(n, timeout_ms), \
|
||||
.require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \
|
||||
.key_positions = DT_PROP(n, key_positions), \
|
||||
.key_position_len = DT_PROP_LEN(n, key_positions), \
|
||||
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \
|
||||
.slow_release = DT_PROP(n, slow_release), \
|
||||
.layer_mask = NODE_PROP_BITMASK(n, layers), \
|
||||
}, ), \
|
||||
())
|
||||
|
||||
#define COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN(positions, _ignore) \
|
||||
DT_INST_FOREACH_CHILD_VARGS(0, COMBO_INST, positions)
|
||||
|
||||
// We do some magic here to generate the `combos` array by "key position length", looping
|
||||
// by key position length and on each iteration, only include entries where the `key-positions`
|
||||
// length matches.
|
||||
// Doing so allows our bitmasks to be "shorted key positions list first" when searching for matches.
|
||||
// `20` is chosen as a reasonable limit, since the theoretical maximum number of keys you might
|
||||
// reasonably press simultaneously with 10 fingers is 20 keys, two keys per finger.
|
||||
static const struct combo_cfg combos[] = {
|
||||
LISTIFY(20, COMBO_CONFIGS_WITH_MATCHING_POSITIONS_LEN, (), 0)};
|
||||
|
||||
#define COMBO_ONE(n) +1
|
||||
|
||||
#define COMBO_CHILDREN_COUNT (0 DT_INST_FOREACH_CHILD(0, COMBO_ONE))
|
||||
|
||||
// We need at least 4 bytes to avoid alignment issues
|
||||
#define BYTES_FOR_COMBOS_MASK DIV_ROUND_UP(COMBO_CHILDREN_COUNT, 32)
|
||||
|
||||
uint8_t pressed_keys_count = 0;
|
||||
// set of keys pressed
|
||||
struct zmk_position_state_changed_event pressed_keys[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO] = {};
|
||||
struct zmk_position_state_changed_event pressed_keys[MAX_COMBO_KEYS] = {};
|
||||
// the set of candidate combos based on the currently pressed_keys
|
||||
struct combo_candidate candidates[CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY];
|
||||
uint32_t candidates[BYTES_FOR_COMBOS_MASK];
|
||||
// the last candidate that was completely pressed
|
||||
const struct combo_cfg *fully_pressed_combo = NULL;
|
||||
int16_t fully_pressed_combo = INT16_MAX;
|
||||
// a lookup dict that maps a key position to all combos on that position
|
||||
const struct combo_cfg *combo_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY] = {NULL};
|
||||
uint32_t combo_lookup[ZMK_KEYMAP_LEN][BYTES_FOR_COMBOS_MASK] = {};
|
||||
// combos that have been activated and still have (some) keys pressed
|
||||
// this array is always contiguous from 0.
|
||||
struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {NULL};
|
||||
int active_combo_count = 0;
|
||||
struct active_combo active_combos[CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS] = {};
|
||||
uint8_t active_combo_count = 0;
|
||||
|
||||
struct k_work_delayable timeout_task;
|
||||
int64_t timeout_task_timeout_at;
|
||||
|
|
@ -90,52 +137,22 @@ static void store_last_tapped(int64_t timestamp) {
|
|||
|
||||
// Store the combo key pointer in the combos array, one pointer for each key position
|
||||
// The combos are sorted shortest-first, then by virtual-key-position.
|
||||
static int initialize_combo(const struct combo_cfg *new_combo) {
|
||||
for (int i = 0; i < new_combo->key_position_len; i++) {
|
||||
int32_t position = new_combo->key_positions[i];
|
||||
if (position >= ZMK_KEYMAP_LEN) {
|
||||
LOG_ERR("Unable to initialize combo, key position %d does not exist", position);
|
||||
return -EINVAL;
|
||||
}
|
||||
static int initialize_combo(size_t index) {
|
||||
const struct combo_cfg *new_combo = &combos[index];
|
||||
|
||||
const struct combo_cfg *insert_combo = new_combo;
|
||||
bool set = false;
|
||||
for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; j++) {
|
||||
const struct combo_cfg *combo_at_j = combo_lookup[position][j];
|
||||
if (combo_at_j == NULL) {
|
||||
combo_lookup[position][j] = insert_combo;
|
||||
set = true;
|
||||
break;
|
||||
}
|
||||
if (combo_at_j->key_position_len < insert_combo->key_position_len ||
|
||||
(combo_at_j->key_position_len == insert_combo->key_position_len &&
|
||||
combo_at_j->virtual_key_position < insert_combo->virtual_key_position)) {
|
||||
continue;
|
||||
}
|
||||
// put insert_combo in this spot, move all other combos up.
|
||||
combo_lookup[position][j] = insert_combo;
|
||||
insert_combo = combo_at_j;
|
||||
}
|
||||
if (!set) {
|
||||
LOG_ERR("Too many combos for key position %d, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY %d.",
|
||||
position, CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY);
|
||||
return -ENOMEM;
|
||||
}
|
||||
for (size_t kp = 0; kp < new_combo->key_position_len; kp++) {
|
||||
sys_bitfield_set_bit((mem_addr_t)&combo_lookup[new_combo->key_positions[kp]], index);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool combo_active_on_layer(const struct combo_cfg *combo, uint8_t layer) {
|
||||
if (combo->layers[0] == -1) {
|
||||
// -1 in the first layer position is global layer scope
|
||||
if (!combo->layer_mask) {
|
||||
return true;
|
||||
}
|
||||
for (int j = 0; j < combo->layers_len; j++) {
|
||||
if (combo->layers[j] == layer) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
return combo->layer_mask & BIT(layer);
|
||||
}
|
||||
|
||||
static bool is_quick_tap(const struct combo_cfg *combo, int64_t timestamp) {
|
||||
|
|
@ -145,66 +162,58 @@ static bool is_quick_tap(const struct combo_cfg *combo, int64_t timestamp) {
|
|||
static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) {
|
||||
int number_of_combo_candidates = 0;
|
||||
uint8_t highest_active_layer = zmk_keymap_highest_layer_active();
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
|
||||
const struct combo_cfg *combo = combo_lookup[position][i];
|
||||
if (combo == NULL) {
|
||||
return number_of_combo_candidates;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(combos); i++) {
|
||||
if (sys_bitfield_test_bit((mem_addr_t)&combo_lookup[position], i)) {
|
||||
const struct combo_cfg *combo = &combos[i];
|
||||
if (combo_active_on_layer(combo, highest_active_layer) &&
|
||||
!is_quick_tap(combo, timestamp)) {
|
||||
sys_bitfield_set_bit((mem_addr_t)&candidates, i);
|
||||
number_of_combo_candidates++;
|
||||
}
|
||||
// LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at);
|
||||
}
|
||||
if (combo_active_on_layer(combo, highest_active_layer) && !is_quick_tap(combo, timestamp)) {
|
||||
candidates[number_of_combo_candidates].combo = combo;
|
||||
candidates[number_of_combo_candidates].timeout_at = timestamp + combo->timeout_ms;
|
||||
number_of_combo_candidates++;
|
||||
}
|
||||
// LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at);
|
||||
}
|
||||
|
||||
return number_of_combo_candidates;
|
||||
}
|
||||
|
||||
static inline uint8_t zero_one_or_more_bits(uint32_t field) {
|
||||
if (field == 0) {
|
||||
return 0;
|
||||
}
|
||||
if ((field & (field - 1)) == 0) {
|
||||
return 1;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
static int filter_candidates(int32_t position) {
|
||||
// this code iterates over candidates and the lookup together to filter in O(n)
|
||||
// assuming they are both sorted on key_position_len, virtual_key_position
|
||||
int matches = 0, lookup_idx = 0, candidate_idx = 0;
|
||||
while (lookup_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY &&
|
||||
candidate_idx < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) {
|
||||
const struct combo_cfg *candidate = candidates[candidate_idx].combo;
|
||||
const struct combo_cfg *lookup = combo_lookup[position][lookup_idx];
|
||||
if (candidate == NULL || lookup == NULL) {
|
||||
break;
|
||||
}
|
||||
if (candidate->virtual_key_position == lookup->virtual_key_position) {
|
||||
candidates[matches] = candidates[candidate_idx];
|
||||
matches++;
|
||||
candidate_idx++;
|
||||
lookup_idx++;
|
||||
} else if (candidate->key_position_len > lookup->key_position_len) {
|
||||
lookup_idx++;
|
||||
} else if (candidate->key_position_len < lookup->key_position_len) {
|
||||
candidate_idx++;
|
||||
} else if (candidate->virtual_key_position > lookup->virtual_key_position) {
|
||||
lookup_idx++;
|
||||
} else if (candidate->virtual_key_position < lookup->virtual_key_position) {
|
||||
candidate_idx++;
|
||||
int matches = 0;
|
||||
for (int i = 0; i < BYTES_FOR_COMBOS_MASK; i++) {
|
||||
candidates[i] &= combo_lookup[position][i];
|
||||
if (matches < 2) {
|
||||
matches += zero_one_or_more_bits(candidates[i]);
|
||||
}
|
||||
}
|
||||
// clear unmatched candidates
|
||||
for (int i = matches; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
|
||||
candidates[i].combo = NULL;
|
||||
}
|
||||
// LOG_DBG("combo matches after filter %d", matches);
|
||||
|
||||
LOG_DBG("combo matches after filter %d", matches);
|
||||
return matches;
|
||||
}
|
||||
|
||||
static int64_t first_candidate_timeout() {
|
||||
if (pressed_keys_count == 0) {
|
||||
return LONG_MAX;
|
||||
}
|
||||
|
||||
int64_t first_timeout = LONG_MAX;
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
|
||||
if (candidates[i].combo == NULL) {
|
||||
break;
|
||||
}
|
||||
if (candidates[i].timeout_at < first_timeout) {
|
||||
first_timeout = candidates[i].timeout_at;
|
||||
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
|
||||
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
|
||||
first_timeout = MIN(first_timeout, combos[i].timeout_ms);
|
||||
}
|
||||
}
|
||||
return first_timeout;
|
||||
|
||||
return pressed_keys[0].data.timestamp + first_timeout;
|
||||
}
|
||||
|
||||
static inline bool candidate_is_completely_pressed(const struct combo_cfg *candidate) {
|
||||
|
|
@ -219,26 +228,17 @@ static inline bool candidate_is_completely_pressed(const struct combo_cfg *candi
|
|||
static int cleanup();
|
||||
|
||||
static int filter_timed_out_candidates(int64_t timestamp) {
|
||||
int remaining_candidates = 0;
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
|
||||
struct combo_candidate *candidate = &candidates[i];
|
||||
if (candidate->combo == NULL) {
|
||||
break;
|
||||
}
|
||||
if (candidate->timeout_at > timestamp) {
|
||||
bool need_to_bubble_up = remaining_candidates != i;
|
||||
if (need_to_bubble_up) {
|
||||
// bubble up => reorder candidates so they're contiguous
|
||||
candidates[remaining_candidates].combo = candidate->combo;
|
||||
candidates[remaining_candidates].timeout_at = candidate->timeout_at;
|
||||
// clear the previous location
|
||||
candidates[i].combo = NULL;
|
||||
candidates[i].timeout_at = 0;
|
||||
}
|
||||
__ASSERT(pressed_keys_count > 0, "Searching for a candidate timeout with no keys pressed");
|
||||
|
||||
remaining_candidates++;
|
||||
} else {
|
||||
candidate->combo = NULL;
|
||||
int remaining_candidates = 0;
|
||||
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
|
||||
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
|
||||
|
||||
if (pressed_keys[0].data.timestamp + combos[i].timeout_ms > timestamp) {
|
||||
remaining_candidates++;
|
||||
} else {
|
||||
sys_bitfield_clear_bit((mem_addr_t)&candidates, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,18 +249,8 @@ static int filter_timed_out_candidates(int64_t timestamp) {
|
|||
return remaining_candidates;
|
||||
}
|
||||
|
||||
static int clear_candidates() {
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) {
|
||||
if (candidates[i].combo == NULL) {
|
||||
return i;
|
||||
}
|
||||
candidates[i].combo = NULL;
|
||||
}
|
||||
return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY;
|
||||
}
|
||||
|
||||
static int capture_pressed_key(const struct zmk_position_state_changed *ev) {
|
||||
if (pressed_keys_count == CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY) {
|
||||
if (pressed_keys_count == MAX_COMBO_KEYS) {
|
||||
return ZMK_EV_EVENT_BUBBLE;
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +261,7 @@ static int capture_pressed_key(const struct zmk_position_state_changed *ev) {
|
|||
const struct zmk_listener zmk_listener_combo;
|
||||
|
||||
static int release_pressed_keys() {
|
||||
uint32_t count = pressed_keys_count;
|
||||
uint8_t count = pressed_keys_count;
|
||||
pressed_keys_count = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
struct zmk_position_state_changed_event *ev = &pressed_keys[i];
|
||||
|
|
@ -288,9 +278,10 @@ static int release_pressed_keys() {
|
|||
return count;
|
||||
}
|
||||
|
||||
static inline int press_combo_behavior(const struct combo_cfg *combo, int32_t timestamp) {
|
||||
static inline int press_combo_behavior(int combo_idx, const struct combo_cfg *combo,
|
||||
int32_t timestamp) {
|
||||
struct zmk_behavior_binding_event event = {
|
||||
.position = combo->virtual_key_position,
|
||||
.position = ZMK_VIRTUAL_KEY_POSITION_COMBO(combo_idx),
|
||||
.timestamp = timestamp,
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
|
||||
.source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL,
|
||||
|
|
@ -302,9 +293,10 @@ static inline int press_combo_behavior(const struct combo_cfg *combo, int32_t ti
|
|||
return zmk_behavior_invoke_binding(&combo->behavior, event, true);
|
||||
}
|
||||
|
||||
static inline int release_combo_behavior(const struct combo_cfg *combo, int32_t timestamp) {
|
||||
static inline int release_combo_behavior(int combo_idx, const struct combo_cfg *combo,
|
||||
int32_t timestamp) {
|
||||
struct zmk_behavior_binding_event event = {
|
||||
.position = combo->virtual_key_position,
|
||||
.position = ZMK_VIRTUAL_KEY_POSITION_COMBO(combo_idx),
|
||||
.timestamp = timestamp,
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
|
||||
.source = ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL,
|
||||
|
|
@ -316,7 +308,7 @@ static inline int release_combo_behavior(const struct combo_cfg *combo, int32_t
|
|||
|
||||
static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) {
|
||||
|
||||
int combo_length = MIN(pressed_keys_count, active_combo->combo->key_position_len);
|
||||
int combo_length = MIN(pressed_keys_count, combos[active_combo->combo_idx].key_position_len);
|
||||
for (int i = 0; i < combo_length; i++) {
|
||||
active_combo->key_positions_pressed[i] = pressed_keys[i];
|
||||
}
|
||||
|
|
@ -330,10 +322,10 @@ static void move_pressed_keys_to_active_combo(struct active_combo *active_combo)
|
|||
pressed_keys_count -= combo_length;
|
||||
}
|
||||
|
||||
static struct active_combo *store_active_combo(const struct combo_cfg *combo) {
|
||||
static struct active_combo *store_active_combo(int32_t combo_idx) {
|
||||
for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) {
|
||||
if (active_combos[i].combo == NULL) {
|
||||
active_combos[i].combo = combo;
|
||||
if (active_combos[i].combo_idx == UINT16_MAX) {
|
||||
active_combos[i].combo_idx = combo_idx;
|
||||
active_combo_count++;
|
||||
return &active_combos[i];
|
||||
}
|
||||
|
|
@ -344,15 +336,16 @@ static struct active_combo *store_active_combo(const struct combo_cfg *combo) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
static void activate_combo(const struct combo_cfg *combo) {
|
||||
struct active_combo *active_combo = store_active_combo(combo);
|
||||
static void activate_combo(int combo_idx) {
|
||||
struct active_combo *active_combo = store_active_combo(combo_idx);
|
||||
if (active_combo == NULL) {
|
||||
// unable to store combo
|
||||
release_pressed_keys();
|
||||
return;
|
||||
}
|
||||
move_pressed_keys_to_active_combo(active_combo);
|
||||
press_combo_behavior(combo, active_combo->key_positions_pressed[0].data.timestamp);
|
||||
press_combo_behavior(combo_idx, &combos[combo_idx],
|
||||
active_combo->key_positions_pressed[0].data.timestamp);
|
||||
}
|
||||
|
||||
static void deactivate_combo(int active_combo_index) {
|
||||
|
|
@ -361,8 +354,8 @@ static void deactivate_combo(int active_combo_index) {
|
|||
memcpy(&active_combos[active_combo_index], &active_combos[active_combo_count],
|
||||
sizeof(struct active_combo));
|
||||
}
|
||||
active_combos[active_combo_count].combo = NULL;
|
||||
active_combos[active_combo_count] = (struct active_combo){0};
|
||||
active_combos[active_combo_count].combo_idx = UINT16_MAX;
|
||||
}
|
||||
|
||||
/* returns true if a key was released. */
|
||||
|
|
@ -371,8 +364,8 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
|
|||
struct active_combo *active_combo = &active_combos[combo_idx];
|
||||
|
||||
bool key_released = false;
|
||||
bool all_keys_pressed =
|
||||
active_combo->key_positions_pressed_count == active_combo->combo->key_position_len;
|
||||
bool all_keys_pressed = active_combo->key_positions_pressed_count ==
|
||||
combos[active_combo->combo_idx].key_position_len;
|
||||
bool all_keys_released = true;
|
||||
for (int i = 0; i < active_combo->key_positions_pressed_count; i++) {
|
||||
if (key_released) {
|
||||
|
|
@ -387,9 +380,9 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
|
|||
|
||||
if (key_released) {
|
||||
active_combo->key_positions_pressed_count--;
|
||||
if ((active_combo->combo->slow_release && all_keys_released) ||
|
||||
(!active_combo->combo->slow_release && all_keys_pressed)) {
|
||||
release_combo_behavior(active_combo->combo, timestamp);
|
||||
const struct combo_cfg *c = &combos[active_combo->combo_idx];
|
||||
if ((c->slow_release && all_keys_released) || (!c->slow_release && all_keys_pressed)) {
|
||||
release_combo_behavior(active_combo->combo_idx, c, timestamp);
|
||||
}
|
||||
if (all_keys_released) {
|
||||
deactivate_combo(combo_idx);
|
||||
|
|
@ -402,10 +395,10 @@ static bool release_combo_key(int32_t position, int64_t timestamp) {
|
|||
|
||||
static int cleanup() {
|
||||
k_work_cancel_delayable(&timeout_task);
|
||||
clear_candidates();
|
||||
if (fully_pressed_combo != NULL) {
|
||||
memset(candidates, 0, BYTES_FOR_COMBOS_MASK);
|
||||
if (fully_pressed_combo != INT16_MAX) {
|
||||
activate_combo(fully_pressed_combo);
|
||||
fully_pressed_combo = NULL;
|
||||
fully_pressed_combo = INT16_MAX;
|
||||
}
|
||||
return release_pressed_keys();
|
||||
}
|
||||
|
|
@ -427,7 +420,7 @@ static void update_timeout_task() {
|
|||
|
||||
static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
|
||||
int num_candidates;
|
||||
if (candidates[0].combo == NULL) {
|
||||
if (!pressed_keys_count) {
|
||||
num_candidates = setup_candidates_for_first_keypress(data->position, data->timestamp);
|
||||
if (num_candidates == 0) {
|
||||
return ZMK_EV_EVENT_BUBBLE;
|
||||
|
|
@ -436,27 +429,31 @@ static int position_state_down(const zmk_event_t *ev, struct zmk_position_state_
|
|||
filter_timed_out_candidates(data->timestamp);
|
||||
num_candidates = filter_candidates(data->position);
|
||||
}
|
||||
update_timeout_task();
|
||||
|
||||
const struct combo_cfg *candidate_combo = candidates[0].combo;
|
||||
LOG_DBG("combo: capturing position event %d", data->position);
|
||||
int ret = capture_pressed_key(data);
|
||||
switch (num_candidates) {
|
||||
case 0:
|
||||
update_timeout_task();
|
||||
|
||||
if (num_candidates) {
|
||||
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
|
||||
if (sys_bitfield_test_bit((mem_addr_t)&candidates, i)) {
|
||||
const struct combo_cfg *candidate_combo = &combos[i];
|
||||
if (candidate_is_completely_pressed(candidate_combo)) {
|
||||
fully_pressed_combo = i;
|
||||
if (num_candidates == 1) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleanup();
|
||||
return ret;
|
||||
case 1:
|
||||
if (candidate_is_completely_pressed(candidate_combo)) {
|
||||
fully_pressed_combo = candidate_combo;
|
||||
cleanup();
|
||||
}
|
||||
return ret;
|
||||
default:
|
||||
if (candidate_is_completely_pressed(candidate_combo)) {
|
||||
fully_pressed_combo = candidate_combo;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
static int position_state_up(const zmk_event_t *ev, struct zmk_position_state_changed *data) {
|
||||
|
|
@ -481,8 +478,11 @@ static void combo_timeout_handler(struct k_work *item) {
|
|||
return;
|
||||
}
|
||||
if (filter_timed_out_candidates(timeout_task_timeout_at) == 0) {
|
||||
LOG_DBG("CLEANUP!");
|
||||
cleanup();
|
||||
}
|
||||
|
||||
LOG_DBG("ABOUT TO UPDATE IN TIMEOUT");
|
||||
update_timeout_task();
|
||||
}
|
||||
|
||||
|
|
@ -520,26 +520,16 @@ ZMK_LISTENER(combo, behavior_combo_listener);
|
|||
ZMK_SUBSCRIPTION(combo, zmk_position_state_changed);
|
||||
ZMK_SUBSCRIPTION(combo, zmk_keycode_state_changed);
|
||||
|
||||
#define COMBO_INST(n) \
|
||||
static const struct combo_cfg combo_config_##n = { \
|
||||
.timeout_ms = DT_PROP(n, timeout_ms), \
|
||||
.require_prior_idle_ms = DT_PROP(n, require_prior_idle_ms), \
|
||||
.key_positions = DT_PROP(n, key_positions), \
|
||||
.key_position_len = DT_PROP_LEN(n, key_positions), \
|
||||
.behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \
|
||||
.virtual_key_position = ZMK_VIRTUAL_KEY_POSITION_COMBO(__COUNTER__), \
|
||||
.slow_release = DT_PROP(n, slow_release), \
|
||||
.layers = DT_PROP(n, layers), \
|
||||
.layers_len = DT_PROP_LEN(n, layers), \
|
||||
};
|
||||
|
||||
#define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n);
|
||||
|
||||
DT_INST_FOREACH_CHILD(0, COMBO_INST)
|
||||
|
||||
static int combo_init(void) {
|
||||
for (size_t i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) {
|
||||
active_combos[i].combo_idx = UINT16_MAX;
|
||||
}
|
||||
|
||||
k_work_init_delayable(&timeout_task, combo_timeout_handler);
|
||||
DT_INST_FOREACH_CHILD(0, INITIALIZE_COMBO);
|
||||
LOG_WRN("Have %d combos!", ARRAY_SIZE(combos));
|
||||
for (int i = 0; i < ARRAY_SIZE(combos); i++) {
|
||||
initialize_combo(i);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,9 @@ See [Configuration Overview](index.md) for instructions on how to change these s
|
|||
|
||||
Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig)
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| ------------------------------------- | ---- | -------------------------------------------------------------- | ------- |
|
||||
| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 |
|
||||
| `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` | int | Maximum number of active combos that use the same key position | 5 |
|
||||
| `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` | int | Maximum number of keys to press to activate a combo | 4 |
|
||||
|
||||
If `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` is 5, you can have 5 separate combos that use position `0`, 5 combos that use position `1`, and so on.
|
||||
|
||||
If you want a combo that triggers when pressing 5 keys, you must set `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` to 5.
|
||||
| Config | Type | Description | Default |
|
||||
| ------------------------------------- | ---- | ------------------------------------------------------------ | ------- |
|
||||
| `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` | int | Maximum number of combos that can be active at the same time | 4 |
|
||||
|
||||
## Devicetree
|
||||
|
||||
|
|
@ -39,5 +33,3 @@ Each child node can have the following properties:
|
|||
| `require-prior-idle-ms` | int | If any non-modifier key is pressed within `require-prior-idle-ms` before a key in the combo, the key will not be considered for the combo | -1 (disabled) |
|
||||
| `slow-release` | bool | Releases the combo when all keys are released instead of when any key is released | false |
|
||||
| `layers` | array | A list of layers on which the combo may be triggered. `-1` allows all layers. | `<-1>` |
|
||||
|
||||
The `key-positions` array must not be longer than the `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` setting, which defaults to 4. If you want a combo that triggers when pressing 5 keys, then you must change the setting to 5.
|
||||
|
|
|
|||
Loading…
Reference in New Issue