From 4cfe7885a09917ec267577ed49b9c807144c6cb1 Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Mon, 4 Jan 2021 13:44:57 -0500 Subject: [PATCH 01/12] chore: Initial simple CODEOWNERS. --- CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..d9bf0d44 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +* @zmkfirmware/core + +/app/boards @zmkfirmware/boards-shields + +/docs @zmkfirmware/docs \ No newline at end of file From 90c2c6672fbf7fb575b61dd9f5a482e1ffc5dbcd Mon Sep 17 00:00:00 2001 From: innovaker <66737976+innovaker@users.noreply.github.com> Date: Fri, 8 Jan 2021 16:02:24 +0000 Subject: [PATCH 02/12] fix(setup.sh): rename BF0-9000 to BFO-9000 Replaces BF0 (zero) with BFO. Refs: dcd665999a5ee04372559d56fc22b9a2d0784bf2 PR: #595 --- docs/static/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/static/setup.sh b/docs/static/setup.sh index 8ac39435..34f0f054 100644 --- a/docs/static/setup.sh +++ b/docs/static/setup.sh @@ -91,7 +91,7 @@ echo "" echo "Keyboard Shield Selection:" prompt="Pick an keyboard:" -options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BF0-9000" "Helix") +options=("Kyria" "Lily58" "Corne" "Splitreus62" "Sofle" "Iris" "Reviung41" "RoMac" "RoMac+" "makerdiary M60" "Microdox" "TG4X" "QAZ" "NIBBLE" "Jorne" "Jian" "CRBN" "Tidbit" "Eek!" "BFO-9000" "Helix") PS3="$prompt " # TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. From feb0d5b90cbbb1a1026bf356afd788c860824ccf Mon Sep 17 00:00:00 2001 From: Okke Formsma Date: Fri, 11 Dec 2020 23:24:44 +0100 Subject: [PATCH 03/12] feat(combos): initial implementation closes #45 --- app/CMakeLists.txt | 1 + app/Kconfig | 17 + app/dts/bindings/zmk,combos.yaml | 22 + app/src/combo.c | 466 ++++++++++++++++++ .../combos-and-holdtaps-0/events.patterns | 2 + .../keycode_events.snapshot | 4 + .../combos-and-holdtaps-0/native_posix.keymap | 47 ++ .../combos-and-holdtaps-1/events.patterns | 2 + .../keycode_events.snapshot | 4 + .../combos-and-holdtaps-1/native_posix.keymap | 42 ++ .../combos-and-holdtaps-2/events.patterns | 2 + .../keycode_events.snapshot | 2 + .../combos-and-holdtaps-2/native_posix.keymap | 45 ++ .../combo/multiple-timeouts/events.patterns | 1 + .../multiple-timeouts/keycode_events.snapshot | 4 + .../multiple-timeouts/native_posix.keymap | 40 ++ .../overlapping-combos-0/events.patterns | 2 + .../keycode_events.snapshot | 20 + .../overlapping-combos-0/native_posix.keymap | 117 +++++ .../overlapping-combos-1/events.patterns | 2 + .../keycode_events.snapshot | 8 + .../overlapping-combos-1/native_posix.keymap | 65 +++ .../overlapping-combos-2/events.patterns | 2 + .../keycode_events.snapshot | 4 + .../overlapping-combos-2/native_posix.keymap | 52 ++ .../overlapping-combos-3/events.patterns | 2 + .../keycode_events.snapshot | 4 + .../overlapping-combos-3/native_posix.keymap | 53 ++ .../events.patterns | 1 + .../keycode_events.snapshot | 16 + .../native_posix.keymap | 84 ++++ app/tests/combo/press-release/events.patterns | 1 + .../press-release/keycode_events.snapshot | 8 + .../combo/press-release/native_posix.keymap | 51 ++ app/tests/combo/press-timeout/events.patterns | 1 + .../press-timeout/keycode_events.snapshot | 4 + .../combo/press-timeout/native_posix.keymap | 35 ++ .../events.patterns | 2 + .../keycode_events.snapshot | 4 + .../native_posix.keymap | 45 ++ .../events.patterns | 2 + .../keycode_events.snapshot | 4 + .../native_posix.keymap | 46 ++ .../events.patterns | 2 + .../keycode_events.snapshot | 4 + .../native_posix.keymap | 46 ++ .../slowrelease-disabled/events.patterns | 1 + .../keycode_events.snapshot | 4 + .../slowrelease-disabled/native_posix.keymap | 38 ++ .../combo/slowrelease-enabled/events.patterns | 1 + .../keycode_events.snapshot | 4 + .../slowrelease-enabled/native_posix.keymap | 38 ++ docs/docs/behaviors/combos.md | 52 ++ docs/docs/intro.md | 2 +- docs/sidebars.js | 1 + 55 files changed, 1528 insertions(+), 1 deletion(-) create mode 100644 app/dts/bindings/zmk,combos.yaml create mode 100644 app/src/combo.c create mode 100644 app/tests/combo/combos-and-holdtaps-0/events.patterns create mode 100644 app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot create mode 100644 app/tests/combo/combos-and-holdtaps-0/native_posix.keymap create mode 100644 app/tests/combo/combos-and-holdtaps-1/events.patterns create mode 100644 app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot create mode 100644 app/tests/combo/combos-and-holdtaps-1/native_posix.keymap create mode 100644 app/tests/combo/combos-and-holdtaps-2/events.patterns create mode 100644 app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot create mode 100644 app/tests/combo/combos-and-holdtaps-2/native_posix.keymap create mode 100644 app/tests/combo/multiple-timeouts/events.patterns create mode 100644 app/tests/combo/multiple-timeouts/keycode_events.snapshot create mode 100644 app/tests/combo/multiple-timeouts/native_posix.keymap create mode 100644 app/tests/combo/overlapping-combos-0/events.patterns create mode 100644 app/tests/combo/overlapping-combos-0/keycode_events.snapshot create mode 100644 app/tests/combo/overlapping-combos-0/native_posix.keymap create mode 100644 app/tests/combo/overlapping-combos-1/events.patterns create mode 100644 app/tests/combo/overlapping-combos-1/keycode_events.snapshot create mode 100644 app/tests/combo/overlapping-combos-1/native_posix.keymap create mode 100644 app/tests/combo/overlapping-combos-2/events.patterns create mode 100644 app/tests/combo/overlapping-combos-2/keycode_events.snapshot create mode 100644 app/tests/combo/overlapping-combos-2/native_posix.keymap create mode 100644 app/tests/combo/overlapping-combos-3/events.patterns create mode 100644 app/tests/combo/overlapping-combos-3/keycode_events.snapshot create mode 100644 app/tests/combo/overlapping-combos-3/native_posix.keymap create mode 100644 app/tests/combo/partially-overlapping-combos/events.patterns create mode 100644 app/tests/combo/partially-overlapping-combos/keycode_events.snapshot create mode 100644 app/tests/combo/partially-overlapping-combos/native_posix.keymap create mode 100644 app/tests/combo/press-release/events.patterns create mode 100644 app/tests/combo/press-release/keycode_events.snapshot create mode 100644 app/tests/combo/press-release/native_posix.keymap create mode 100644 app/tests/combo/press-timeout/events.patterns create mode 100644 app/tests/combo/press-timeout/keycode_events.snapshot create mode 100644 app/tests/combo/press-timeout/native_posix.keymap create mode 100644 app/tests/combo/press1-press2-release1-release2/events.patterns create mode 100644 app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot create mode 100644 app/tests/combo/press1-press2-release1-release2/native_posix.keymap create mode 100644 app/tests/combo/press1-press2-release2-release1/events.patterns create mode 100644 app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot create mode 100644 app/tests/combo/press1-press2-release2-release1/native_posix.keymap create mode 100644 app/tests/combo/press1-release1-press2-release2/events.patterns create mode 100644 app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot create mode 100644 app/tests/combo/press1-release1-press2-release2/native_posix.keymap create mode 100644 app/tests/combo/slowrelease-disabled/events.patterns create mode 100644 app/tests/combo/slowrelease-disabled/keycode_events.snapshot create mode 100644 app/tests/combo/slowrelease-disabled/native_posix.keymap create mode 100644 app/tests/combo/slowrelease-enabled/events.patterns create mode 100644 app/tests/combo/slowrelease-enabled/keycode_events.snapshot create mode 100644 app/tests/combo/slowrelease-enabled/native_posix.keymap create mode 100644 docs/docs/behaviors/combos.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5fb3827c..b217a1a1 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -52,6 +52,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_none.c) target_sources(app PRIVATE src/behaviors/behavior_sensor_rotate_key_press.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) + target_sources(app PRIVATE src/combo.c) target_sources(app PRIVATE src/keymap.c) endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) diff --git a/app/Kconfig b/app/Kconfig index f5d92a88..0aa291d6 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -251,6 +251,23 @@ config ZMK_EXT_POWER #Power Management endmenu +menu "Combo options" + +config ZMK_COMBO_MAX_PRESSED_COMBOS + int "Maximum number of currently pressed combos" + default 4 + +config ZMK_COMBO_MAX_COMBOS_PER_KEY + int "Maximum number of combos per key" + default 5 + +config ZMK_COMBO_MAX_KEYS_PER_COMBO + int "Maximum number of keys per combo" + default 4 + +#Display/LED Options +endmenu + menu "Advanced" menu "Initialization Priorities" diff --git a/app/dts/bindings/zmk,combos.yaml b/app/dts/bindings/zmk,combos.yaml new file mode 100644 index 00000000..75eaa3e1 --- /dev/null +++ b/app/dts/bindings/zmk,combos.yaml @@ -0,0 +1,22 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Combos container + +compatible: "zmk,combos" + +child-binding: + description: "A combo" + + properties: + bindings: + type: phandle-array + required: true + key-positions: + type: array + required: true + timeout-ms: + type: int + default: 50 + slow-release: + type: boolean diff --git a/app/src/combo.c b/app/src/combo.c new file mode 100644 index 00000000..49638703 --- /dev/null +++ b/app/src/combo.c @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_combos + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +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 timeout_ms; + // 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; +}; + +struct active_combo { + struct combo_cfg *combo; + // 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. + struct position_state_changed *key_positions_pressed[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO]; +}; + +struct combo_candidate { + 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; +}; + +// set of keys pressed +struct position_state_changed *pressed_keys[CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO] = {NULL}; +// the set of candidate combos based on the currently pressed_keys +struct combo_candidate candidates[CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY]; +// the last candidate that was completely pressed +struct combo_cfg *fully_pressed_combo = NULL; +// a lookup dict that maps a key position to all combos on that position +struct combo_cfg *combo_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY] = {NULL}; +// 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 k_delayed_work timeout_task; +int64_t timeout_task_timeout_at; + +// 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(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; + } + + struct combo_cfg *insert_combo = new_combo; + bool set = false; + for (int j = 0; j < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; j++) { + 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; + } + } + return 0; +} + +static int setup_candidates_for_first_keypress(int32_t position, int64_t timestamp) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + struct combo_cfg *combo = combo_lookup[position][i]; + if (combo == NULL) { + return i; + } + candidates[i].combo = combo; + candidates[i].timeout_at = timestamp + combo->timeout_ms; + // LOG_DBG("combo timeout %d %d %d", position, i, candidates[i].timeout_at); + } + return CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; +} + +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, virtal_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) { + struct combo_cfg *candidate = candidates[candidate_idx].combo; + 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++; + } + } + // 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); + return matches; +} + +static int64_t first_candidate_timeout() { + 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; + } + } + return first_timeout; +} + +static inline bool candidate_is_completely_pressed(struct combo_cfg *candidate) { + // this code assumes set(pressed_keys) <= set(candidate->key_positions) + // this invariant is enforced by filter_candidates + // the only thing we need to do is check if len(pressed_keys) == len(combo->key_positions) + return pressed_keys[candidate->key_position_len - 1] != NULL; +} + +static void cleanup(); + +static int filter_timed_out_candidates(int64_t timestamp) { + int num_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) { + // reorder candidates so they're contiguous + candidates[num_candidates].combo = candidate->combo; + candidates[num_candidates].timeout_at = candidates->timeout_at; + num_candidates++; + } else { + candidate->combo = NULL; + } + } + return num_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(struct position_state_changed *ev) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (pressed_keys[i] != NULL) { + continue; + } + pressed_keys[i] = ev; + return ZMK_EV_EVENT_CAPTURED; + } + return 0; +} + +const struct zmk_listener zmk_listener_combo; + +static void release_pressed_keys() { + // release the first key that was pressed + if (pressed_keys[0] == NULL) { + return; + } + ZMK_EVENT_RELEASE(pressed_keys[0]) + pressed_keys[0] = NULL; + + // reprocess events (see tests/combo/fully-overlapping-combos-3 for why this is needed) + for (int i = 1; i < CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY; i++) { + if (pressed_keys[i] == NULL) { + return; + } + struct position_state_changed *captured_event = pressed_keys[i]; + pressed_keys[i] = NULL; + ZMK_EVENT_RAISE(captured_event); + } +} + +static inline int press_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = combo->virtual_key_position, + .timestamp = timestamp, + }; + + return behavior_keymap_binding_pressed(&combo->behavior, event); +} + +static inline int release_combo_behavior(struct combo_cfg *combo, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = combo->virtual_key_position, + .timestamp = timestamp, + }; + + return behavior_keymap_binding_released(&combo->behavior, event); +} + +static void move_pressed_keys_to_active_combo(struct active_combo *active_combo) { + int combo_length = active_combo->combo->key_position_len; + for (int i = 0; i < combo_length; i++) { + active_combo->key_positions_pressed[i] = pressed_keys[i]; + pressed_keys[i] = NULL; + } + // move any other pressed keys up + for (int i = 0; i + combo_length < CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO; i++) { + if (pressed_keys[i + combo_length] == NULL) { + return; + } + pressed_keys[i] = pressed_keys[i + combo_length]; + pressed_keys[i + combo_length] = NULL; + } +} + +static struct active_combo *store_active_combo(struct combo_cfg *combo) { + for (int i = 0; i < CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS; i++) { + if (active_combos[i].combo == NULL) { + active_combos[i].combo = combo; + active_combo_count++; + return &active_combos[i]; + } + } + LOG_ERR("Unable to store combo; already %d active. Increase " + "CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS", + CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS); + return NULL; +} + +static void activate_combo(struct combo_cfg *combo) { + struct active_combo *active_combo = store_active_combo(combo); + 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]->timestamp); +} + +static void deactivate_combo(int active_combo_index) { + active_combo_count--; + if (active_combo_index != active_combo_count) { + 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}; +} + +/* returns true if a key was released. */ +static bool release_combo_key(int32_t position, int64_t timestamp) { + for (int combo_idx = 0; combo_idx < active_combo_count; combo_idx++) { + struct active_combo *active_combo = &active_combos[combo_idx]; + + bool key_released = false; + bool all_keys_pressed = true; + bool all_keys_released = true; + for (int i = 0; i < active_combo->combo->key_position_len; i++) { + if (active_combo->key_positions_pressed[i] == NULL) { + all_keys_pressed = false; + } else if (active_combo->key_positions_pressed[i]->position != position) { + all_keys_released = false; + } else { // not null and position matches + k_free(active_combo->key_positions_pressed[i]); + active_combo->key_positions_pressed[i] = NULL; + key_released = true; + } + } + + if (key_released) { + if ((active_combo->combo->slow_release && all_keys_released) || + (!active_combo->combo->slow_release && all_keys_pressed)) { + release_combo_behavior(active_combo->combo, timestamp); + } + if (all_keys_released) { + deactivate_combo(combo_idx); + } + return true; + } + } + return false; +} + +static void cleanup() { + k_delayed_work_cancel(&timeout_task); + clear_candidates(); + if (fully_pressed_combo != NULL) { + activate_combo(fully_pressed_combo); + fully_pressed_combo = NULL; + } + release_pressed_keys(); +} + +static void update_timeout_task() { + int64_t first_timeout = first_candidate_timeout(); + if (timeout_task_timeout_at == first_timeout) { + return; + } + if (first_timeout == LLONG_MAX) { + timeout_task_timeout_at = 0; + k_delayed_work_cancel(&timeout_task); + return; + } + if (k_delayed_work_submit(&timeout_task, K_MSEC(first_timeout - k_uptime_get())) == 0) { + timeout_task_timeout_at = first_timeout; + } +} + +static int position_state_down(struct position_state_changed *ev) { + int num_candidates; + if (candidates[0].combo == NULL) { + num_candidates = setup_candidates_for_first_keypress(ev->position, ev->timestamp); + if (num_candidates == 0) { + return 0; + } + } else { + filter_timed_out_candidates(ev->timestamp); + num_candidates = filter_candidates(ev->position); + } + update_timeout_task(); + + struct combo_cfg *candidate_combo = candidates[0].combo; + int ret = capture_pressed_key(ev); + switch (num_candidates) { + case 0: + 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; + } +} + +static int position_state_up(struct position_state_changed *ev) { + cleanup(); + if (release_combo_key(ev->position, ev->timestamp)) { + return ZMK_EV_EVENT_HANDLED; + } else { + return 0; + } +} + +static void combo_timeout_handler(struct k_work *item) { + if (timeout_task_timeout_at == 0 || k_uptime_get() < timeout_task_timeout_at) { + // timer was cancelled or rescheduled. + return; + } + if (filter_timed_out_candidates(timeout_task_timeout_at) < 2) { + cleanup(); + } + update_timeout_task(); +} + +static int position_state_changed_listener(const struct zmk_event_header *eh) { + if (!is_position_state_changed(eh)) { + return 0; + } + + struct position_state_changed *ev = cast_position_state_changed(eh); + if (ev->state) { // keydown + return position_state_down(ev); + } else { // keyup + return position_state_up(ev); + } +} + +ZMK_LISTENER(combo, position_state_changed_listener); +ZMK_SUBSCRIPTION(combo, position_state_changed); + +// todo: remove this once #506 is merged and #include +#define KEY_BINDING_TO_STRUCT(idx, drv_inst) \ + { \ + .behavior_dev = DT_LABEL(DT_PHANDLE_BY_IDX(drv_inst, bindings, idx)), \ + .param1 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param1), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param1))), \ + .param2 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param2), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param2))), \ + } + +#define COMBO_INST(n) \ + static struct combo_cfg combo_config_##n = { \ + .timeout_ms = DT_PROP(n, timeout_ms), \ + .key_positions = DT_PROP(n, key_positions), \ + .key_position_len = DT_PROP_LEN(n, key_positions), \ + .behavior = KEY_BINDING_TO_STRUCT(0, n), \ + .virtual_key_position = ZMK_KEYMAP_LEN + __COUNTER__, \ + .slow_release = DT_PROP(n, slow_release), \ + }; + +#define INITIALIZE_COMBO(n) initialize_combo(&combo_config_##n); + +DT_INST_FOREACH_CHILD(0, COMBO_INST) + +static int combo_init() { + k_delayed_work_init(&timeout_task, combo_timeout_handler); + DT_INST_FOREACH_CHILD(0, INITIALIZE_COMBO); + return 0; +} + +SYS_INIT(combo_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +#endif \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-0/events.patterns b/app/tests/combo/combos-and-holdtaps-0/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot new file mode 100644 index 00000000..ad86b269 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0xe0 mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0xe0 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap new file mode 100644 index 00000000..d35c7277 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-0/native_posix.keymap @@ -0,0 +1,47 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* +This test fails if the order of event handlers for hold-taps +and combos is wrong. Hold-taps need to process key position events +first so the decision to hold or tap can be made. +*/ +/ { + combos { + compatible = "zmk,combos"; + + combo_two { + timeout-ms = <100>; + key-positions = <1 2>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-1/events.patterns b/app/tests/combo/combos-and-holdtaps-1/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot new file mode 100644 index 00000000..dc4dbb49 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap new file mode 100644 index 00000000..a99c15d9 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-1/native_posix.keymap @@ -0,0 +1,42 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* this test checks if hold-taps can be part of a combo */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-2/events.patterns b/app/tests/combo/combos-and-holdtaps-2/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot b/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot new file mode 100644 index 00000000..a6508804 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/keycode_events.snapshot @@ -0,0 +1,2 @@ +pressed: usage_page 0x07 keycode 0xe0 mods 0x00 +pressed: usage_page 0x07 keycode 0xe4 mods 0x00 diff --git a/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap b/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap new file mode 100644 index 00000000..f8dbe450 --- /dev/null +++ b/app/tests/combo/combos-and-holdtaps-2/native_posix.keymap @@ -0,0 +1,45 @@ +#include +#include +#include + +&mt { + flavor = "hold-preferred"; +}; + +/* This test verifies that hold-tap keys can observe + * events which were released from combos. + */ +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <100>; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + combo_two { + timeout-ms = <100>; + key-positions = <1 3>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &mt LEFT_CONTROL A &mt RIGHT_CONTROL B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,0) + ZMK_MOCK_PRESS(0,1,300) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/multiple-timeouts/events.patterns b/app/tests/combo/multiple-timeouts/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/multiple-timeouts/keycode_events.snapshot b/app/tests/combo/multiple-timeouts/keycode_events.snapshot new file mode 100644 index 00000000..c5bdd6e0 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x05 mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x05 mods 0x00 diff --git a/app/tests/combo/multiple-timeouts/native_posix.keymap b/app/tests/combo/multiple-timeouts/native_posix.keymap new file mode 100644 index 00000000..91bf5235 --- /dev/null +++ b/app/tests/combo/multiple-timeouts/native_posix.keymap @@ -0,0 +1,40 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + combo_two { + timeout-ms = <120>; + key-positions = <0 1 2>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-0/events.patterns b/app/tests/combo/overlapping-combos-0/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-0/keycode_events.snapshot b/app/tests/combo/overlapping-combos-0/keycode_events.snapshot new file mode 100644 index 00000000..ec63b77f --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/keycode_events.snapshot @@ -0,0 +1,20 @@ +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-0/native_posix.keymap b/app/tests/combo/overlapping-combos-0/native_posix.keymap new file mode 100644 index 00000000..e3cbf437 --- /dev/null +++ b/app/tests/combo/overlapping-combos-0/native_posix.keymap @@ -0,0 +1,117 @@ +#include +#include +#include + +/* + combo 0 timeout inf + combo 01 timeout inf + combo 0123 timeout inf + press 012 in any combination, release any of those keys + expected: combo 012 on key-release + */ + +/* it is useful to set timeout to a large value when attaching a debugger. */ +#define TIMEOUT (60*60*1000) + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = ; + key-positions = <0 1 2>; + bindings = <&kp X>; + }; + + combo_two { + timeout-ms = ; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = ; + key-positions = <1>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; +&kscan { + events = < + /* all permutations of combo one press, combo triggered by release */ + /* while debugging these, you may want to set the release_timer to a high number */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + /* all permutations of combo two press and release, combo triggered by release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-1/events.patterns b/app/tests/combo/overlapping-combos-1/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-1/keycode_events.snapshot b/app/tests/combo/overlapping-combos-1/keycode_events.snapshot new file mode 100644 index 00000000..daf72478 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-1/native_posix.keymap b/app/tests/combo/overlapping-combos-1/native_posix.keymap new file mode 100644 index 00000000..c228c475 --- /dev/null +++ b/app/tests/combo/overlapping-combos-1/native_posix.keymap @@ -0,0 +1,65 @@ +#include +#include +#include + +/* + combo 01 timeout 50 + combo 012 timeout 100 + AB is pressed within 50ms, C is never pressed. + expected outcome: AB after 100ms +*/ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = <100>; + key-positions = <0 1 2>; + bindings = <&kp X>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + /* all permutations of combo two press and release, combo triggered by timeout */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,100) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-2/events.patterns b/app/tests/combo/overlapping-combos-2/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-2/keycode_events.snapshot b/app/tests/combo/overlapping-combos-2/keycode_events.snapshot new file mode 100644 index 00000000..dc4dbb49 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/overlapping-combos-2/native_posix.keymap b/app/tests/combo/overlapping-combos-2/native_posix.keymap new file mode 100644 index 00000000..3d364213 --- /dev/null +++ b/app/tests/combo/overlapping-combos-2/native_posix.keymap @@ -0,0 +1,52 @@ +#include +#include +#include + +/* + combo 01 timeout 100 + combo 0123 timeout 100 + press 012, wait until timeout runs out + expected: combo 01 after 100ms, immediately followed by key 2. + */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <0 1>; + bindings = <&kp Y>; + }; + + combo_four { + timeout-ms = <100>; + key-positions = <0 1 2 3>; + bindings = <&kp W>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,100) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-3/events.patterns b/app/tests/combo/overlapping-combos-3/events.patterns new file mode 100644 index 00000000..b90d7863 --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo//p \ No newline at end of file diff --git a/app/tests/combo/overlapping-combos-3/keycode_events.snapshot b/app/tests/combo/overlapping-combos-3/keycode_events.snapshot new file mode 100644 index 00000000..e0cb655e --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/overlapping-combos-3/native_posix.keymap b/app/tests/combo/overlapping-combos-3/native_posix.keymap new file mode 100644 index 00000000..0622dcd0 --- /dev/null +++ b/app/tests/combo/overlapping-combos-3/native_posix.keymap @@ -0,0 +1,53 @@ +#include +#include +#include + +/* + combo 12 timeout 100 + combo 0123 timeout 100 + press 012, release 2 + expected: key pos 0 followed by combo 12 + */ +/ { + combos { + compatible = "zmk,combos"; + combo_two { + timeout-ms = <100>; + key-positions = <1 2>; + bindings = <&kp Y>; + }; + + + combo_four { + timeout-ms = <100>; + key-positions = <0 1 2 3>; + bindings = <&kp W>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* if you're debugging these, remember that the timer can be triggered between + events while stepping through code. */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,100) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,100) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/partially-overlapping-combos/events.patterns b/app/tests/combo/partially-overlapping-combos/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot b/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot new file mode 100644 index 00000000..adaa64bc --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/keycode_events.snapshot @@ -0,0 +1,16 @@ +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1b mods 0x00 +released: usage_page 0x07 keycode 0x1b mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 +pressed: usage_page 0x07 keycode 0x1c mods 0x00 +released: usage_page 0x07 keycode 0x1c mods 0x00 diff --git a/app/tests/combo/partially-overlapping-combos/native_posix.keymap b/app/tests/combo/partially-overlapping-combos/native_posix.keymap new file mode 100644 index 00000000..4e68105f --- /dev/null +++ b/app/tests/combo/partially-overlapping-combos/native_posix.keymap @@ -0,0 +1,84 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp X>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <0 2>; + bindings = <&kp Y>; + }; + + combo_three { + timeout-ms = <30>; + key-positions = <3>; + bindings = <&kp Z>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp C &none + >; + }; + }; +}; + +&kscan { + events = < + /* all permutations of combo one press and release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + /* all permutations of combo two press and release */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,2,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; diff --git a/app/tests/combo/press-release/events.patterns b/app/tests/combo/press-release/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/press-release/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/press-release/keycode_events.snapshot b/app/tests/combo/press-release/keycode_events.snapshot new file mode 100644 index 00000000..01718e71 --- /dev/null +++ b/app/tests/combo/press-release/keycode_events.snapshot @@ -0,0 +1,8 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/press-release/native_posix.keymap b/app/tests/combo/press-release/native_posix.keymap new file mode 100644 index 00000000..0f45792d --- /dev/null +++ b/app/tests/combo/press-release/native_posix.keymap @@ -0,0 +1,51 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + /* all different combinations of press and release order */ + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press-timeout/events.patterns b/app/tests/combo/press-timeout/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/press-timeout/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/press-timeout/keycode_events.snapshot b/app/tests/combo/press-timeout/keycode_events.snapshot new file mode 100644 index 00000000..c5bdd6e0 --- /dev/null +++ b/app/tests/combo/press-timeout/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x04 mods 0x00 +pressed: usage_page 0x07 keycode 0x05 mods 0x00 +released: usage_page 0x07 keycode 0x04 mods 0x00 +released: usage_page 0x07 keycode 0x05 mods 0x00 diff --git a/app/tests/combo/press-timeout/native_posix.keymap b/app/tests/combo/press-timeout/native_posix.keymap new file mode 100644 index 00000000..ff0b7493 --- /dev/null +++ b/app/tests/combo/press-timeout/native_posix.keymap @@ -0,0 +1,35 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &none &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release1-release2/events.patterns b/app/tests/combo/press1-press2-release1-release2/events.patterns new file mode 100644 index 00000000..5f3e4cf7 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot b/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot new file mode 100644 index 00000000..cfa02de2 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/press1-press2-release1-release2/native_posix.keymap b/app/tests/combo/press1-press2-release1-release2/native_posix.keymap new file mode 100644 index 00000000..2518bbc9 --- /dev/null +++ b/app/tests/combo/press1-press2-release1-release2/native_posix.keymap @@ -0,0 +1,45 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-press2-release2-release1/events.patterns b/app/tests/combo/press1-press2-release2-release1/events.patterns new file mode 100644 index 00000000..b54b66b6 --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p diff --git a/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot b/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot new file mode 100644 index 00000000..b55f09ba --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 diff --git a/app/tests/combo/press1-press2-release2-release1/native_posix.keymap b/app/tests/combo/press1-press2-release2-release1/native_posix.keymap new file mode 100644 index 00000000..4895636e --- /dev/null +++ b/app/tests/combo/press1-press2-release2-release1/native_posix.keymap @@ -0,0 +1,46 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/press1-release1-press2-release2/events.patterns b/app/tests/combo/press1-release1-press2-release2/events.patterns new file mode 100644 index 00000000..5f3e4cf7 --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*combo/combo/p \ No newline at end of file diff --git a/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot b/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot new file mode 100644 index 00000000..c41dee8c --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/press1-release1-press2-release2/native_posix.keymap b/app/tests/combo/press1-release1-press2-release2/native_posix.keymap new file mode 100644 index 00000000..0c4a698c --- /dev/null +++ b/app/tests/combo/press1-release1-press2-release2/native_posix.keymap @@ -0,0 +1,46 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + }; + + combo_two { + timeout-ms = <30>; + key-positions = <2 3>; + bindings = <&kp D>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp Z &kp Y + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,0,10) + ZMK_MOCK_RELEASE(1,1,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/slowrelease-disabled/events.patterns b/app/tests/combo/slowrelease-disabled/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/slowrelease-disabled/keycode_events.snapshot b/app/tests/combo/slowrelease-disabled/keycode_events.snapshot new file mode 100644 index 00000000..c41dee8c --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/slowrelease-disabled/native_posix.keymap b/app/tests/combo/slowrelease-disabled/native_posix.keymap new file mode 100644 index 00000000..3bacb886 --- /dev/null +++ b/app/tests/combo/slowrelease-disabled/native_posix.keymap @@ -0,0 +1,38 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + /* no slow-release! */ + }; + }; + + keymap { + compatible = "zmk,keymap"; + label = "Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp D &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) /* this should release the combo */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; \ No newline at end of file diff --git a/app/tests/combo/slowrelease-enabled/events.patterns b/app/tests/combo/slowrelease-enabled/events.patterns new file mode 100644 index 00000000..b1342af4 --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/events.patterns @@ -0,0 +1 @@ +s/.*hid_listener_keycode_//p diff --git a/app/tests/combo/slowrelease-enabled/keycode_events.snapshot b/app/tests/combo/slowrelease-enabled/keycode_events.snapshot new file mode 100644 index 00000000..cfa02de2 --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/keycode_events.snapshot @@ -0,0 +1,4 @@ +pressed: usage_page 0x07 keycode 0x06 mods 0x00 +pressed: usage_page 0x07 keycode 0x07 mods 0x00 +released: usage_page 0x07 keycode 0x06 mods 0x00 +released: usage_page 0x07 keycode 0x07 mods 0x00 diff --git a/app/tests/combo/slowrelease-enabled/native_posix.keymap b/app/tests/combo/slowrelease-enabled/native_posix.keymap new file mode 100644 index 00000000..8ac8316b --- /dev/null +++ b/app/tests/combo/slowrelease-enabled/native_posix.keymap @@ -0,0 +1,38 @@ +#include +#include +#include + +/ { + combos { + compatible = "zmk,combos"; + combo_one { + timeout-ms = <30>; + key-positions = <0 1>; + bindings = <&kp C>; + slow-release; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp A &kp B + &kp D &none + >; + }; + }; +}; + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) /* this should not release the combo yet */ + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(1,0,10) + >; +}; \ No newline at end of file diff --git a/docs/docs/behaviors/combos.md b/docs/docs/behaviors/combos.md new file mode 100644 index 00000000..e9b01761 --- /dev/null +++ b/docs/docs/behaviors/combos.md @@ -0,0 +1,52 @@ +--- +title: Combo Behavior +sidebar_label: Combos +--- + +## Summary + +Combo keys are a way to combine multiple keypresses to output a different key. For example, you can hit the Q and W keys on your keyboard to output escape. + +### Configuration + +Combos are specified like this: + +``` +/ { + combos { + compatible = "zmk,combos"; + combo_esc { + timeout-ms = <50>; + key-positions = <0 1>; + bindings = <&kp ESC>; + }; + }; +}; +``` + +- The name of the combo doesn't really matter, but convention is to start the node name with `combo_`. +- The `compatible` property should always be `"zmk,combos"` for combos. +- `timeout-ms` is the number of milliseconds that all keys of the combo must be pressed. +- `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. +- `bindings` is the behavior that is activated when the behavior is pressed. +- (advanced) you can specify `slow-release` if you want the combo binding to be released when all key-positions are released. The default is to release the combo as soon as any of the keys in the combo is released. + +:::info + +Key positions are numbered like the keys in your keymap, starting at 0. So, if the first key in your keymap is `Q`, this key is in position `0`. The next key (possibly `W`) will have position 1, etcetera. + +::: + +### Advanced usage + +- Partially overlapping combos like `0 1` and `0 2` are supported. +- Fully overlapping combos like `0 1` and `0 1 2` are supported. +- You are not limited to `&kp` bindings. You can use all ZMK behaviors there, like `&mo`, `&bt`, `&mt`, `<` etc. + +### Advanced configuration + +There are three global combo parameters which are set through KConfig. You can set them in the `.conf` file in the same directory as your keymap file. + +- `CONFIG_ZMK_COMBO_MAX_PRESSED_COMBOS` is the number of combos that can be active at the same time. Default 4. +- `CONFIG_ZMK_COMBO_MAX_COMBOS_PER_KEY` is the maximum number of combos that can be active on a key position. Defaults to 5. (So you can have 5 separate combos that use position `3` for example) +- `CONFIG_ZMK_COMBO_MAX_KEYS_PER_COMBO` is the maximum number of keys that need to be pressed to activate a combo. Default 4. If you want a combo that triggers when pressing 5 keys, you'd set this to 5 for example. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 57670ea0..2215291c 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -26,7 +26,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ | | [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ | | One Shot Keys | ✅ | ✅ | ✅ | -| [Combo Keys](https://github.com/zmkfirmware/zmk/pull/504) | 🚧 | | ✅ | +| [Combo Keys](behaviors/combos) | ✅ | | ✅ | | Macros | 🚧 | ✅ | ✅ | | Mouse Keys | 💡 | ✅ | ✅ | | Low Active Power Usage | ✅ | | | diff --git a/docs/sidebars.js b/docs/sidebars.js index 8fc1dc54..d095a47e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -20,6 +20,7 @@ module.exports = { "behaviors/misc", "behaviors/hold-tap", "behaviors/mod-tap", + "behaviors/combos", "behaviors/reset", "behaviors/bluetooth", "behaviors/outputs", From 1d18900a994b6a80e1f74d43eb0497c35e9e2f78 Mon Sep 17 00:00:00 2001 From: innovaker <66737976+innovaker@users.noreply.github.com> Date: Thu, 17 Dec 2020 19:36:19 +0000 Subject: [PATCH 04/12] refactor(core): define usage page as uint16_t Aligns with the HID specification. Usage page values were sometimes declared as uint8_t and sometimes uint16_t. This commit aligns all instances with the HID specification for consistency. PR: #521 --- app/include/zmk/endpoints.h | 2 +- app/include/zmk/events/keycode_state_changed.h | 2 +- app/src/endpoints.c | 2 +- app/src/hid_listener.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index 64280385..0d2bbce0 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -18,4 +18,4 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint); int zmk_endpoints_toggle(); enum zmk_endpoint zmk_endpoints_selected(); -int zmk_endpoints_send_report(uint8_t usage_page); +int zmk_endpoints_send_report(uint16_t usage_page); diff --git a/app/include/zmk/events/keycode_state_changed.h b/app/include/zmk/events/keycode_state_changed.h index d175605b..85b792b7 100644 --- a/app/include/zmk/events/keycode_state_changed.h +++ b/app/include/zmk/events/keycode_state_changed.h @@ -14,7 +14,7 @@ struct keycode_state_changed { struct zmk_event_header header; - uint8_t usage_page; + uint16_t usage_page; uint32_t keycode; uint8_t implicit_modifiers; bool state; diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 9f32f197..feff7996 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -130,7 +130,7 @@ static int send_consumer_report() { } } -int zmk_endpoints_send_report(uint8_t usage_page) { +int zmk_endpoints_send_report(uint16_t usage_page) { LOG_DBG("usage page 0x%02X", usage_page); switch (usage_page) { diff --git a/app/src/hid_listener.c b/app/src/hid_listener.c index 80e9054a..6537ccab 100644 --- a/app/src/hid_listener.c +++ b/app/src/hid_listener.c @@ -16,7 +16,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include -static int hid_listener_keycode_pressed(uint8_t usage_page, uint32_t keycode, +static int hid_listener_keycode_pressed(uint16_t usage_page, uint32_t keycode, zmk_mod_flags_t implicit_modifiers) { int err; LOG_DBG("usage_page 0x%02X keycode 0x%02X mods 0x%02X", usage_page, keycode, @@ -41,7 +41,7 @@ static int hid_listener_keycode_pressed(uint8_t usage_page, uint32_t keycode, return zmk_endpoints_send_report(usage_page); } -static int hid_listener_keycode_released(uint8_t usage_page, uint32_t keycode, +static int hid_listener_keycode_released(uint16_t usage_page, uint32_t keycode, zmk_mod_flags_t implicit_modifiers) { int err; LOG_DBG("usage_page 0x%02X keycode 0x%02X mods 0x%02X", usage_page, keycode, From 969e5568afa10a15a4d0735bf161abc96afb75ca Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Wed, 13 Jan 2021 23:44:17 -0500 Subject: [PATCH 05/12] fix(boards): Define but don't enable uart0. * Set pro-micro TX/RX pins, but don't enable the UART by default. --- app/boards/arm/bluemicro840/bluemicro840_v1.dts | 8 ++------ app/boards/arm/nice_nano/nice_nano.dts | 8 ++------ app/boards/arm/nrf52840_m2/nrf52840_m2.dts | 10 ---------- app/boards/arm/nrfmicro/nrfmicro_11.dts | 8 ++------ app/boards/arm/nrfmicro/nrfmicro_11_flipped.dts | 8 ++------ app/boards/arm/nrfmicro/nrfmicro_13.dts | 8 ++------ 6 files changed, 10 insertions(+), 40 deletions(-) diff --git a/app/boards/arm/bluemicro840/bluemicro840_v1.dts b/app/boards/arm/bluemicro840/bluemicro840_v1.dts index 18dff1af..31551f4f 100644 --- a/app/boards/arm/bluemicro840/bluemicro840_v1.dts +++ b/app/boards/arm/bluemicro840/bluemicro840_v1.dts @@ -66,12 +66,8 @@ &uart0 { compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; + tx-pin = <6>; + rx-pin = <8>; }; &usbd { diff --git a/app/boards/arm/nice_nano/nice_nano.dts b/app/boards/arm/nice_nano/nice_nano.dts index 01efb289..5efde4b2 100644 --- a/app/boards/arm/nice_nano/nice_nano.dts +++ b/app/boards/arm/nice_nano/nice_nano.dts @@ -65,12 +65,8 @@ &uart0 { compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; + tx-pin = <6>; + rx-pin = <8>; }; &usbd { diff --git a/app/boards/arm/nrf52840_m2/nrf52840_m2.dts b/app/boards/arm/nrf52840_m2/nrf52840_m2.dts index e090842c..6b613a1c 100644 --- a/app/boards/arm/nrf52840_m2/nrf52840_m2.dts +++ b/app/boards/arm/nrf52840_m2/nrf52840_m2.dts @@ -51,16 +51,6 @@ status = "okay"; }; -&uart0 { - compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; -}; - &usbd { compatible = "nordic,nrf-usbd"; status = "okay"; diff --git a/app/boards/arm/nrfmicro/nrfmicro_11.dts b/app/boards/arm/nrfmicro/nrfmicro_11.dts index 164b66d2..0dd8e8eb 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_11.dts +++ b/app/boards/arm/nrfmicro/nrfmicro_11.dts @@ -53,12 +53,8 @@ &uart0 { compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; + tx-pin = <6>; + rx-pin = <8>; }; &usbd { diff --git a/app/boards/arm/nrfmicro/nrfmicro_11_flipped.dts b/app/boards/arm/nrfmicro/nrfmicro_11_flipped.dts index fb558382..deea41fc 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_11_flipped.dts +++ b/app/boards/arm/nrfmicro/nrfmicro_11_flipped.dts @@ -53,12 +53,8 @@ &uart0 { compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; + tx-pin = <6>; + rx-pin = <8>; }; &usbd { diff --git a/app/boards/arm/nrfmicro/nrfmicro_13.dts b/app/boards/arm/nrfmicro/nrfmicro_13.dts index 1a05c7a1..bb40f3d0 100644 --- a/app/boards/arm/nrfmicro/nrfmicro_13.dts +++ b/app/boards/arm/nrfmicro/nrfmicro_13.dts @@ -65,12 +65,8 @@ &uart0 { compatible = "nordic,nrf-uarte"; - status = "okay"; - current-speed = <115200>; - tx-pin = <19>; - rx-pin = <21>; - rts-pin = <23>; - cts-pin = <25>; + tx-pin = <6>; + rx-pin = <8>; }; &usbd { From 4aa78a6f8d2742e1407f78c50a0cf93e194447a4 Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Thu, 3 Dec 2020 00:12:21 -0500 Subject: [PATCH 06/12] fix(split): Use queue/work for peripheral events. * Avoid corruption by using work to process peripheral key position events on the main work thread, like local kscan events are. * Fixes #221 --- app/Kconfig | 10 +++++++- app/src/split/bluetooth/central.c | 41 +++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 0aa291d6..65abf7a5 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -94,11 +94,19 @@ config ZMK_SPLIT_BLE if ZMK_SPLIT_BLE -config ZMK_SPLIT_BLE_ROLE_CENTRAL +menuconfig ZMK_SPLIT_BLE_ROLE_CENTRAL bool "Central" select BT_CENTRAL select BT_GATT_CLIENT +if ZMK_SPLIT_BLE_ROLE_CENTRAL + +config ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE + int "Max number of key position state events to queue when received from peripherals" + default 5 + +endif + if !ZMK_SPLIT_BLE_ROLE_CENTRAL config ZMK_USB diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 5ad83ad2..9cce7870 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -33,9 +33,32 @@ static struct bt_uuid_128 uuid = BT_UUID_INIT_128(ZMK_SPLIT_BT_SERVICE_UUID); static struct bt_gatt_discover_params discover_params; static struct bt_gatt_subscribe_params subscribe_params; -static uint8_t split_central_notify_func(struct bt_conn *conn, - struct bt_gatt_subscribe_params *params, const void *data, - uint16_t length) { +struct zmk_split_peripheral_event { + uint32_t position; + uint32_t state; + int32_t timestamp; +}; + +K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_split_peripheral_event), + CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4); + +void peripheral_event_work_callback(struct k_work *work) { + struct zmk_split_peripheral_event ev; + while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) { + struct position_state_changed *pos_ev = new_position_state_changed(); + pos_ev->position = ev.position; + pos_ev->state = ev.state; + pos_ev->timestamp = ev.timestamp; + + LOG_DBG("Trigger key position state change for %d", ev.position); + ZMK_EVENT_RAISE(pos_ev); + } +} + +K_WORK_DEFINE(peripheral_event_work, peripheral_event_work_callback); + +static uint8_t split_central_notify_func(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) { static uint8_t position_state[POSITION_STATE_DATA_LEN]; uint8_t changed_positions[POSITION_STATE_DATA_LEN]; @@ -58,13 +81,11 @@ static uint8_t split_central_notify_func(struct bt_conn *conn, if (changed_positions[i] & BIT(j)) { uint32_t position = (i * 8) + j; bool pressed = position_state[i] & BIT(j); - struct position_state_changed *pos_ev = new_position_state_changed(); - pos_ev->position = position; - pos_ev->state = pressed; - pos_ev->timestamp = k_uptime_get(); + struct zmk_split_peripheral_event ev = { + .position = position, .state = pressed, .timestamp = k_uptime_get()}; - LOG_DBG("Trigger key position state change for %d", position); - ZMK_EVENT_RAISE(pos_ev); + k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT); + k_work_submit(&peripheral_event_work); } } } @@ -321,4 +342,4 @@ int zmk_split_bt_central_init(const struct device *_arg) { return start_scan(); } -SYS_INIT(zmk_split_bt_central_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); \ No newline at end of file +SYS_INIT(zmk_split_bt_central_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); From a5c39dfa76eeebd09568ce959cd3dd088498ad3f Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Thu, 3 Dec 2020 16:04:48 -0500 Subject: [PATCH 07/12] fix(ble): Perform GATT notifies from dedicated queue. * Zephyr BT stack frees TX buffers from system workqueue, and to avoid blocking waiting to allocate, perform notify from a dedicated queue. --- app/Kconfig | 18 ++++- app/src/hog.c | 118 ++++++++++++++++++++++++++---- app/src/split/bluetooth/central.c | 5 +- 3 files changed, 123 insertions(+), 18 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 65abf7a5..7602b9bd 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -41,7 +41,7 @@ config USB_NUMOF_EP_WRITE_RETRIES #ZMK_USB endif -config ZMK_BLE +menuconfig ZMK_BLE bool "BLE (HID over GATT)" select BT select BT_SMP @@ -58,6 +58,22 @@ if ZMK_BLE config SYSTEM_WORKQUEUE_STACK_SIZE default 2048 +config ZMK_BLE_THREAD_STACK_SIZE + int "BLE notify thread stack size" + default 512 + +config ZMK_BLE_THREAD_PRIORITY + int "BLE notify thread priority" + default 5 + +config ZMK_BLE_KEYBOARD_REPORT_QUEUE_SIZE + int "Max number of keyboard HID reports to queue for sending over BLE" + default 20 + +config ZMK_BLE_CONSUMER_REPORT_QUEUE_SIZE + int "Max number of consumer HID reports to queue for sending over BLE" + default 5 + config ZMK_BLE_CLEAR_BONDS_ON_START bool "Configuration that clears all bond information from the keyboard on startup." default n diff --git a/app/src/hog.c b/app/src/hog.c index 8d10b8f7..07343097 100644 --- a/app/src/hog.c +++ b/app/src/hog.c @@ -5,6 +5,7 @@ */ #include +#include #include @@ -156,28 +157,115 @@ struct bt_conn *destination_connection() { return conn; } +K_THREAD_STACK_DEFINE(hog_q_stack, CONFIG_ZMK_BLE_THREAD_STACK_SIZE); + +struct k_work_q hog_work_q; + +K_MSGQ_DEFINE(zmk_hog_keyboard_msgq, sizeof(struct zmk_hid_keyboard_report_body), + CONFIG_ZMK_BLE_KEYBOARD_REPORT_QUEUE_SIZE, 4); + +void send_keyboard_report_callback(struct k_work *work) { + struct zmk_hid_keyboard_report_body report; + + while (k_msgq_get(&zmk_hog_keyboard_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &hog_svc.attrs[5], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err) { + LOG_ERR("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +} + +K_WORK_DEFINE(hog_keyboard_work, send_keyboard_report_callback); + int zmk_hog_send_keyboard_report(struct zmk_hid_keyboard_report_body *report) { - struct bt_conn *conn = destination_connection(); - if (conn == NULL) { - return -ENOTCONN; + int err = k_msgq_put(&zmk_hog_keyboard_msgq, report, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Keyboard message queue full, popping first message and queueing again"); + struct zmk_hid_keyboard_report_body discarded_report; + k_msgq_get(&zmk_hog_keyboard_msgq, &discarded_report, K_NO_WAIT); + return zmk_hog_send_keyboard_report(report); + } + default: + LOG_WRN("Failed to queue keyboard report to send (%d)", err); + return err; + } } - LOG_DBG("Sending to NULL? %s", conn == NULL ? "yes" : "no"); + k_work_submit_to_queue(&hog_work_q, &hog_keyboard_work); - int err = bt_gatt_notify(conn, &hog_svc.attrs[5], report, - sizeof(struct zmk_hid_keyboard_report_body)); - bt_conn_unref(conn); - return err; + return 0; }; +K_MSGQ_DEFINE(zmk_hog_consumer_msgq, sizeof(struct zmk_hid_consumer_report_body), + CONFIG_ZMK_BLE_CONSUMER_REPORT_QUEUE_SIZE, 4); + +void send_consumer_report_callback(struct k_work *work) { + struct zmk_hid_consumer_report_body report; + + while (k_msgq_get(&zmk_hog_consumer_msgq, &report, K_NO_WAIT) == 0) { + struct bt_conn *conn = destination_connection(); + if (conn == NULL) { + return; + } + + struct bt_gatt_notify_params notify_params = { + .attr = &hog_svc.attrs[10], + .data = &report, + .len = sizeof(report), + }; + + int err = bt_gatt_notify_cb(conn, ¬ify_params); + if (err) { + LOG_DBG("Error notifying %d", err); + } + + bt_conn_unref(conn); + } +}; + +K_WORK_DEFINE(hog_consumer_work, send_consumer_report_callback); + int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) { - struct bt_conn *conn = destination_connection(); - if (conn == NULL) { - return -ENOTCONN; + int err = k_msgq_put(&zmk_hog_consumer_msgq, report, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Consumer message queue full, popping first message and queueing again"); + struct zmk_hid_consumer_report_body discarded_report; + k_msgq_get(&zmk_hog_consumer_msgq, &discarded_report, K_NO_WAIT); + return zmk_hog_send_consumer_report(report); + } + default: + LOG_WRN("Failed to queue consumer report to send (%d)", err); + return err; + } } - int err = bt_gatt_notify(conn, &hog_svc.attrs[10], report, - sizeof(struct zmk_hid_consumer_report_body)); - bt_conn_unref(conn); - return err; + k_work_submit_to_queue(&hog_work_q, &hog_consumer_work); + + return 0; }; + +int zmk_hog_init(const struct device *_arg) { + k_work_q_start(&hog_work_q, hog_q_stack, K_THREAD_STACK_SIZEOF(hog_q_stack), + CONFIG_ZMK_BLE_THREAD_PRIORITY); + + return 0; +} + +SYS_INIT(zmk_hog_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 9cce7870..e9dfbac5 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -57,8 +57,9 @@ void peripheral_event_work_callback(struct k_work *work) { K_WORK_DEFINE(peripheral_event_work, peripheral_event_work_callback); -static uint8_t split_central_notify_func(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, - const void *data, uint16_t length) { +static uint8_t split_central_notify_func(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, const void *data, + uint16_t length) { static uint8_t position_state[POSITION_STATE_DATA_LEN]; uint8_t changed_positions[POSITION_STATE_DATA_LEN]; From a0c32bb47e3827f08096247e3bfecb82a8ed8e7d Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Sat, 2 Jan 2021 00:05:41 -0500 Subject: [PATCH 08/12] fix(bluetooth): improve LE param update logging --- app/src/ble.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/ble.c b/app/src/ble.c index b32696ff..3a115cf4 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -376,7 +376,10 @@ static void connected(struct bt_conn *conn, uint8_t err) { LOG_DBG("Connected %s", log_strdup(addr)); - bt_conn_le_param_update(conn, BT_LE_CONN_PARAM(0x0006, 0x000c, 30, 400)); + err = bt_conn_le_param_update(conn, BT_LE_CONN_PARAM(0x0006, 0x000c, 30, 400)); + if (err) { + LOG_WRN("Failed to update LE parameters (err %d)", err); + } #if IS_SPLIT_PERIPHERAL bt_conn_le_phy_update(conn, BT_CONN_LE_PHY_PARAM_2M); @@ -423,10 +426,20 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_ } } +static void le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency, + uint16_t timeout) { + char addr[BT_ADDR_LE_STR_LEN]; + + bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); + + LOG_DBG("%s: interval %d latency %d timeout %d", addr, interval, latency, timeout); +} + static struct bt_conn_cb conn_callbacks = { .connected = connected, .disconnected = disconnected, .security_changed = security_changed, + .le_param_updated = le_param_updated, }; /* From c11759bc795239c38c36ecd78d6408662aa77f54 Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Wed, 6 Jan 2021 00:15:48 -0500 Subject: [PATCH 09/12] fix(hid): Clear all matching usages, not just first. * If various events get dropped, we can end up with duplicate codes in our report, so tweak to ensure we look for all matches and clear them when we have a keycode released. --- app/src/hid.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/hid.c b/app/src/hid.c index 37378b45..b9ddfc52 100644 --- a/app/src/hid.c +++ b/app/src/hid.c @@ -55,7 +55,9 @@ int zmk_hid_unregister_mod(zmk_mod_t modifier) { continue; \ } \ keyboard_report.body.keys[idx] = val; \ - break; \ + if (val) { \ + break; \ + } \ } #define TOGGLE_CONSUMER(match, val) \ @@ -64,7 +66,9 @@ int zmk_hid_unregister_mod(zmk_mod_t modifier) { continue; \ } \ consumer_report.body.keys[idx] = val; \ - break; \ + if (val) { \ + break; \ + } \ } int zmk_hid_implicit_modifiers_press(zmk_mod_flags_t implicit_modifiers) { From 65e476df3e4584aa744763b52a86930c67e2e288 Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Wed, 6 Jan 2021 09:32:08 -0500 Subject: [PATCH 10/12] fix(splits): Send pos notify from dedicated thread * Avoid deadlocks by using a deadicated workqueue for sending position state notifications from peripherals. --- app/Kconfig | 14 ++++++- app/src/split/bluetooth/service.c | 61 +++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 7602b9bd..df00f1db 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -102,7 +102,7 @@ config ZMK_SPLIT if ZMK_SPLIT -config ZMK_SPLIT_BLE +menuconfig ZMK_SPLIT_BLE bool "Split keyboard support via BLE transport" depends on ZMK_BLE default y @@ -125,6 +125,18 @@ endif if !ZMK_SPLIT_BLE_ROLE_CENTRAL +config ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE + int "BLE split peripheral notify thread stack size" + default 512 + +config ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY + int "BLE split peripheral notify thread priority" + default 5 + +config ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE + int "Max number of key position state events to queue to send to the central" + default 10 + config ZMK_USB default n diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 48390849..fbac6446 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -6,6 +6,7 @@ #include #include +#include #include @@ -18,8 +19,10 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include +#define POS_STATE_LEN 16 + static uint8_t num_of_positions = ZMK_KEYMAP_LEN; -static uint8_t position_state[16]; +static uint8_t position_state[POS_STATE_LEN]; static ssize_t split_svc_pos_state(struct bt_conn *conn, const struct bt_gatt_attr *attrs, void *buf, uint16_t len, uint16_t offset) { @@ -45,12 +48,62 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_DESCRIPTOR(BT_UUID_NUM_OF_DIGITALS, BT_GATT_PERM_READ, split_svc_num_of_positions, NULL, &num_of_positions), ); +K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE); + +struct k_work_q service_work_q; + +K_MSGQ_DEFINE(position_state_msgq, sizeof(char[POS_STATE_LEN]), + CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE, 4); + +void send_position_state_callback(struct k_work *work) { + uint8_t state[POS_STATE_LEN]; + + while (k_msgq_get(&position_state_msgq, &state, K_NO_WAIT) == 0) { + int err = bt_gatt_notify(NULL, &split_svc.attrs[1], &state, sizeof(state)); + if (err) { + LOG_DBG("Error notifying %d", err); + } + } +}; + +K_WORK_DEFINE(service_position_notify_work, send_position_state_callback); + +int send_position_state() { + int err = k_msgq_put(&position_state_msgq, position_state, K_MSEC(100)); + if (err) { + switch (err) { + case -EAGAIN: { + LOG_WRN("Position state message queue full, popping first message and queueing again"); + uint8_t discarded_state[POS_STATE_LEN]; + k_msgq_get(&position_state_msgq, &discarded_state, K_NO_WAIT); + return send_position_state(); + } + default: + LOG_WRN("Failed to queue position state to send (%d)", err); + return err; + } + } + + k_work_submit_to_queue(&service_work_q, &service_position_notify_work); + + return 0; +} + int zmk_split_bt_position_pressed(uint8_t position) { WRITE_BIT(position_state[position / 8], position % 8, true); - return bt_gatt_notify(NULL, &split_svc.attrs[1], &position_state, sizeof(position_state)); + return send_position_state(); } int zmk_split_bt_position_released(uint8_t position) { WRITE_BIT(position_state[position / 8], position % 8, false); - return bt_gatt_notify(NULL, &split_svc.attrs[1], &position_state, sizeof(position_state)); -} \ No newline at end of file + return send_position_state(); +} + +int service_init(const struct device *_arg) { + k_work_q_start(&service_work_q, service_q_stack, K_THREAD_STACK_SIZEOF(service_q_stack), + CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY); + + return 0; +} + +SYS_INIT(service_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); From 5978990e6dce086f57b3861977f33bee7798c7b8 Mon Sep 17 00:00:00 2001 From: innovaker <66737976+innovaker@users.noreply.github.com> Date: Sat, 2 Jan 2021 00:05:18 +0000 Subject: [PATCH 11/12] chore(core): replace `Pete Johanson` with `The ZMK Contributors` Does not include boards and shields. --- app/dts/bindings/behaviors/one_param.yaml | 2 +- app/dts/bindings/behaviors/two_param.yaml | 2 +- app/dts/bindings/behaviors/zero_param.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-bluetooth.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-key-press.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-momentary-layer.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-none.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-reset.yaml | 2 +- app/dts/bindings/behaviors/zmk,behavior-transparent.yaml | 2 +- app/dts/common/arduino_uno_pro_micro_map.dtsi | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/dts/bindings/behaviors/one_param.yaml b/app/dts/bindings/behaviors/one_param.yaml index 8eabba1b..faa01a0d 100644 --- a/app/dts/bindings/behaviors/one_param.yaml +++ b/app/dts/bindings/behaviors/one_param.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT properties: diff --git a/app/dts/bindings/behaviors/two_param.yaml b/app/dts/bindings/behaviors/two_param.yaml index c508c401..d4cdfaa0 100644 --- a/app/dts/bindings/behaviors/two_param.yaml +++ b/app/dts/bindings/behaviors/two_param.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT properties: diff --git a/app/dts/bindings/behaviors/zero_param.yaml b/app/dts/bindings/behaviors/zero_param.yaml index 6defbb20..075270d6 100644 --- a/app/dts/bindings/behaviors/zero_param.yaml +++ b/app/dts/bindings/behaviors/zero_param.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT properties: diff --git a/app/dts/bindings/behaviors/zmk,behavior-bluetooth.yaml b/app/dts/bindings/behaviors/zmk,behavior-bluetooth.yaml index 9ce56734..b357fbf6 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-bluetooth.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-bluetooth.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Peter Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Bluetooth Behavior diff --git a/app/dts/bindings/behaviors/zmk,behavior-key-press.yaml b/app/dts/bindings/behaviors/zmk,behavior-key-press.yaml index 37f1886f..5a40a8db 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-key-press.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-key-press.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Key press/release behavior diff --git a/app/dts/bindings/behaviors/zmk,behavior-momentary-layer.yaml b/app/dts/bindings/behaviors/zmk,behavior-momentary-layer.yaml index b8f26af6..5423e29c 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-momentary-layer.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-momentary-layer.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Momentary layer on press/release behavior diff --git a/app/dts/bindings/behaviors/zmk,behavior-none.yaml b/app/dts/bindings/behaviors/zmk,behavior-none.yaml index 4d1ad406..42d22587 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-none.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-none.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: None Binding Behavior diff --git a/app/dts/bindings/behaviors/zmk,behavior-reset.yaml b/app/dts/bindings/behaviors/zmk,behavior-reset.yaml index 6133411e..9eb7aa6a 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-reset.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-reset.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Keyboard Reset Behavior diff --git a/app/dts/bindings/behaviors/zmk,behavior-transparent.yaml b/app/dts/bindings/behaviors/zmk,behavior-transparent.yaml index 19c25ffa..97295fcc 100644 --- a/app/dts/bindings/behaviors/zmk,behavior-transparent.yaml +++ b/app/dts/bindings/behaviors/zmk,behavior-transparent.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Pete Johanson +# Copyright (c) 2020 The ZMK Contributors # SPDX-License-Identifier: MIT description: Transparent Binding Behavior diff --git a/app/dts/common/arduino_uno_pro_micro_map.dtsi b/app/dts/common/arduino_uno_pro_micro_map.dtsi index fe59a866..3f3d64f0 100644 --- a/app/dts/common/arduino_uno_pro_micro_map.dtsi +++ b/app/dts/common/arduino_uno_pro_micro_map.dtsi @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Pete Johanson + * Copyright (c) 2020 The ZMK Contributors * * SPDX-License-Identifier: MIT */ From 95acbd8859b7fa29877f9494be94a3b302c170a1 Mon Sep 17 00:00:00 2001 From: Pete Johanson Date: Tue, 19 Jan 2021 14:42:55 -0500 Subject: [PATCH 12/12] fix(shields): Unflip left Microdox OLED. --- app/boards/shields/microdox/microdox.dtsi | 2 -- app/boards/shields/microdox/microdox_right.overlay | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/boards/shields/microdox/microdox.dtsi b/app/boards/shields/microdox/microdox.dtsi index ee2114d7..a692ce4d 100644 --- a/app/boards/shields/microdox/microdox.dtsi +++ b/app/boards/shields/microdox/microdox.dtsi @@ -58,8 +58,6 @@ RC(2,0) RC(2,1) RC(2,2) RC(2,3) RC(2,4) RC(2,5) RC(2,6) RC(2,7) RC(2,8) RC(2,9) page-offset = <0>; display-offset = <0>; multiplex-ratio = <31>; - segment-remap; - com-invdir; com-sequential; prechargep = <0x22>; }; diff --git a/app/boards/shields/microdox/microdox_right.overlay b/app/boards/shields/microdox/microdox_right.overlay index c5622b25..0801c7e1 100644 --- a/app/boards/shields/microdox/microdox_right.overlay +++ b/app/boards/shields/microdox/microdox_right.overlay @@ -10,6 +10,11 @@ col-offset = <5>; }; +&oled { + segment-remap; + com-invdir; +}; + &kscan0 { col-gpios = <&pro_micro_d 15 GPIO_ACTIVE_HIGH>