"merge main zmk updates"
This commit is contained in:
Jack Hartstein 2021-01-19 23:51:51 -08:00
commit 1f674534d6
84 changed files with 1812 additions and 91 deletions

5
CODEOWNERS Normal file
View file

@ -0,0 +1,5 @@
* @zmkfirmware/core
/app/boards @zmkfirmware/boards-shields
/docs @zmkfirmware/docs

View file

@ -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_none.c)
target_sources(app PRIVATE src/behaviors/behavior_sensor_rotate_key_press.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_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) target_sources(app PRIVATE src/keymap.c)
endif() endif()
target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c)

View file

@ -41,7 +41,7 @@ config USB_NUMOF_EP_WRITE_RETRIES
#ZMK_USB #ZMK_USB
endif endif
config ZMK_BLE menuconfig ZMK_BLE
bool "BLE (HID over GATT)" bool "BLE (HID over GATT)"
select BT select BT
select BT_SMP select BT_SMP
@ -58,6 +58,22 @@ if ZMK_BLE
config SYSTEM_WORKQUEUE_STACK_SIZE config SYSTEM_WORKQUEUE_STACK_SIZE
default 2048 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 config ZMK_BLE_CLEAR_BONDS_ON_START
bool "Configuration that clears all bond information from the keyboard on startup." bool "Configuration that clears all bond information from the keyboard on startup."
default n default n
@ -86,7 +102,7 @@ config ZMK_SPLIT
if ZMK_SPLIT if ZMK_SPLIT
config ZMK_SPLIT_BLE menuconfig ZMK_SPLIT_BLE
bool "Split keyboard support via BLE transport" bool "Split keyboard support via BLE transport"
depends on ZMK_BLE depends on ZMK_BLE
default y default y
@ -94,13 +110,33 @@ config ZMK_SPLIT_BLE
if ZMK_SPLIT_BLE if ZMK_SPLIT_BLE
config ZMK_SPLIT_BLE_ROLE_CENTRAL menuconfig ZMK_SPLIT_BLE_ROLE_CENTRAL
bool "Central" bool "Central"
select BT_CENTRAL select BT_CENTRAL
select BT_GATT_CLIENT 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 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 config ZMK_USB
default n default n
@ -251,6 +287,23 @@ config ZMK_EXT_POWER
#Power Management #Power Management
endmenu 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 "Advanced"
menu "Initialization Priorities" menu "Initialization Priorities"

View file

@ -66,12 +66,8 @@
&uart0 { &uart0 {
compatible = "nordic,nrf-uarte"; compatible = "nordic,nrf-uarte";
status = "okay"; tx-pin = <6>;
current-speed = <115200>; rx-pin = <8>;
tx-pin = <19>;
rx-pin = <21>;
rts-pin = <23>;
cts-pin = <25>;
}; };
&usbd { &usbd {

View file

@ -65,12 +65,8 @@
&uart0 { &uart0 {
compatible = "nordic,nrf-uarte"; compatible = "nordic,nrf-uarte";
status = "okay"; tx-pin = <6>;
current-speed = <115200>; rx-pin = <8>;
tx-pin = <19>;
rx-pin = <21>;
rts-pin = <23>;
cts-pin = <25>;
}; };
&usbd { &usbd {

View file

@ -51,16 +51,6 @@
status = "okay"; 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 { &usbd {
compatible = "nordic,nrf-usbd"; compatible = "nordic,nrf-usbd";
status = "okay"; status = "okay";

View file

@ -53,12 +53,8 @@
&uart0 { &uart0 {
compatible = "nordic,nrf-uarte"; compatible = "nordic,nrf-uarte";
status = "okay"; tx-pin = <6>;
current-speed = <115200>; rx-pin = <8>;
tx-pin = <19>;
rx-pin = <21>;
rts-pin = <23>;
cts-pin = <25>;
}; };
&usbd { &usbd {

View file

@ -53,12 +53,8 @@
&uart0 { &uart0 {
compatible = "nordic,nrf-uarte"; compatible = "nordic,nrf-uarte";
status = "okay"; tx-pin = <6>;
current-speed = <115200>; rx-pin = <8>;
tx-pin = <19>;
rx-pin = <21>;
rts-pin = <23>;
cts-pin = <25>;
}; };
&usbd { &usbd {

View file

@ -65,12 +65,8 @@
&uart0 { &uart0 {
compatible = "nordic,nrf-uarte"; compatible = "nordic,nrf-uarte";
status = "okay"; tx-pin = <6>;
current-speed = <115200>; rx-pin = <8>;
tx-pin = <19>;
rx-pin = <21>;
rts-pin = <23>;
cts-pin = <25>;
}; };
&usbd { &usbd {

View file

@ -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>; page-offset = <0>;
display-offset = <0>; display-offset = <0>;
multiplex-ratio = <31>; multiplex-ratio = <31>;
segment-remap;
com-invdir;
com-sequential; com-sequential;
prechargep = <0x22>; prechargep = <0x22>;
}; };

View file

@ -10,6 +10,11 @@
col-offset = <5>; col-offset = <5>;
}; };
&oled {
segment-remap;
com-invdir;
};
&kscan0 { &kscan0 {
col-gpios col-gpios
= <&pro_micro_d 15 GPIO_ACTIVE_HIGH> = <&pro_micro_d 15 GPIO_ACTIVE_HIGH>

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
properties: properties:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
properties: properties:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
properties: properties:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Peter Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: Bluetooth Behavior description: Bluetooth Behavior

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: Key press/release behavior description: Key press/release behavior

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: Momentary layer on press/release behavior description: Momentary layer on press/release behavior

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: None Binding Behavior description: None Binding Behavior

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: Keyboard Reset Behavior description: Keyboard Reset Behavior

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Pete Johanson # Copyright (c) 2020 The ZMK Contributors
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
description: Transparent Binding Behavior description: Transparent Binding Behavior

View file

@ -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

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 Pete Johanson * Copyright (c) 2020 The ZMK Contributors
* *
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */

View file

@ -18,4 +18,4 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint);
int zmk_endpoints_toggle(); int zmk_endpoints_toggle();
enum zmk_endpoint zmk_endpoints_selected(); enum zmk_endpoint zmk_endpoints_selected();
int zmk_endpoints_send_report(uint8_t usage_page); int zmk_endpoints_send_report(uint16_t usage_page);

View file

@ -14,7 +14,7 @@
struct keycode_state_changed { struct keycode_state_changed {
struct zmk_event_header header; struct zmk_event_header header;
uint8_t usage_page; uint16_t usage_page;
uint32_t keycode; uint32_t keycode;
uint8_t implicit_modifiers; uint8_t implicit_modifiers;
bool state; bool state;

View file

@ -376,7 +376,10 @@ static void connected(struct bt_conn *conn, uint8_t err) {
LOG_DBG("Connected %s", log_strdup(addr)); 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 #if IS_SPLIT_PERIPHERAL
bt_conn_le_phy_update(conn, BT_CONN_LE_PHY_PARAM_2M); 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 = { static struct bt_conn_cb conn_callbacks = {
.connected = connected, .connected = connected,
.disconnected = disconnected, .disconnected = disconnected,
.security_changed = security_changed, .security_changed = security_changed,
.le_param_updated = le_param_updated,
}; };
/* /*

466
app/src/combo.c Normal file
View file

@ -0,0 +1,466 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_combos
#include <device.h>
#include <drivers/behavior.h>
#include <logging/log.h>
#include <sys/dlist.h>
#include <kernel.h>
#include <zmk/behavior.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/hid.h>
#include <zmk/matrix.h>
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 <zmk/keymap.h>
#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

View file

@ -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); LOG_DBG("usage page 0x%02X", usage_page);
switch (usage_page) { switch (usage_page) {

View file

@ -55,7 +55,9 @@ int zmk_hid_unregister_mod(zmk_mod_t modifier) {
continue; \ continue; \
} \ } \
keyboard_report.body.keys[idx] = val; \ keyboard_report.body.keys[idx] = val; \
if (val) { \
break; \ break; \
} \
} }
#define TOGGLE_CONSUMER(match, val) \ #define TOGGLE_CONSUMER(match, val) \
@ -64,7 +66,9 @@ int zmk_hid_unregister_mod(zmk_mod_t modifier) {
continue; \ continue; \
} \ } \
consumer_report.body.keys[idx] = val; \ consumer_report.body.keys[idx] = val; \
if (val) { \
break; \ break; \
} \
} }
int zmk_hid_implicit_modifiers_press(zmk_mod_flags_t implicit_modifiers) { int zmk_hid_implicit_modifiers_press(zmk_mod_flags_t implicit_modifiers) {

View file

@ -16,7 +16,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <dt-bindings/zmk/hid_usage_pages.h> #include <dt-bindings/zmk/hid_usage_pages.h>
#include <zmk/endpoints.h> #include <zmk/endpoints.h>
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) { zmk_mod_flags_t implicit_modifiers) {
int err; int err;
LOG_DBG("usage_page 0x%02X keycode 0x%02X mods 0x%02X", usage_page, keycode, 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); 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) { zmk_mod_flags_t implicit_modifiers) {
int err; int err;
LOG_DBG("usage_page 0x%02X keycode 0x%02X mods 0x%02X", usage_page, keycode, LOG_DBG("usage_page 0x%02X keycode 0x%02X mods 0x%02X", usage_page, keycode,

View file

@ -5,6 +5,7 @@
*/ */
#include <settings/settings.h> #include <settings/settings.h>
#include <init.h>
#include <logging/log.h> #include <logging/log.h>
@ -156,28 +157,115 @@ struct bt_conn *destination_connection() {
return conn; return conn;
} }
int zmk_hog_send_keyboard_report(struct zmk_hid_keyboard_report_body *report) { 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(); struct bt_conn *conn = destination_connection();
if (conn == NULL) { if (conn == NULL) {
return -ENOTCONN; return;
} }
LOG_DBG("Sending to NULL? %s", conn == NULL ? "yes" : "no"); struct bt_gatt_notify_params notify_params = {
.attr = &hog_svc.attrs[5],
.data = &report,
.len = sizeof(report),
};
int err = bt_gatt_notify_cb(conn, &notify_params);
if (err) {
LOG_ERR("Error notifying %d", err);
}
int err = bt_gatt_notify(conn, &hog_svc.attrs[5], report,
sizeof(struct zmk_hid_keyboard_report_body));
bt_conn_unref(conn); 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) {
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; return err;
}
}
k_work_submit_to_queue(&hog_work_q, &hog_keyboard_work);
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, &notify_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) { int zmk_hog_send_consumer_report(struct zmk_hid_consumer_report_body *report) {
struct bt_conn *conn = destination_connection(); int err = k_msgq_put(&zmk_hog_consumer_msgq, report, K_MSEC(100));
if (conn == NULL) { if (err) {
return -ENOTCONN; 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, k_work_submit_to_queue(&hog_work_q, &hog_consumer_work);
sizeof(struct zmk_hid_consumer_report_body));
bt_conn_unref(conn); return 0;
return err;
}; };
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);

View file

@ -33,6 +33,30 @@ 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_discover_params discover_params;
static struct bt_gatt_subscribe_params subscribe_params; static struct bt_gatt_subscribe_params subscribe_params;
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, static uint8_t split_central_notify_func(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params, const void *data, struct bt_gatt_subscribe_params *params, const void *data,
uint16_t length) { uint16_t length) {
@ -58,13 +82,11 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
if (changed_positions[i] & BIT(j)) { if (changed_positions[i] & BIT(j)) {
uint32_t position = (i * 8) + j; uint32_t position = (i * 8) + j;
bool pressed = position_state[i] & BIT(j); bool pressed = position_state[i] & BIT(j);
struct position_state_changed *pos_ev = new_position_state_changed(); struct zmk_split_peripheral_event ev = {
pos_ev->position = position; .position = position, .state = pressed, .timestamp = k_uptime_get()};
pos_ev->state = pressed;
pos_ev->timestamp = k_uptime_get();
LOG_DBG("Trigger key position state change for %d", position); k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
ZMK_EVENT_RAISE(pos_ev); k_work_submit(&peripheral_event_work);
} }
} }
} }

View file

@ -6,6 +6,7 @@
#include <zephyr/types.h> #include <zephyr/types.h>
#include <sys/util.h> #include <sys/util.h>
#include <init.h>
#include <logging/log.h> #include <logging/log.h>
@ -18,8 +19,10 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/split/bluetooth/uuid.h> #include <zmk/split/bluetooth/uuid.h>
#include <zmk/split/bluetooth/service.h> #include <zmk/split/bluetooth/service.h>
#define POS_STATE_LEN 16
static uint8_t num_of_positions = ZMK_KEYMAP_LEN; 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, 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) { 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, BT_GATT_DESCRIPTOR(BT_UUID_NUM_OF_DIGITALS, BT_GATT_PERM_READ, split_svc_num_of_positions, NULL,
&num_of_positions), ); &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) { int zmk_split_bt_position_pressed(uint8_t position) {
WRITE_BIT(position_state[position / 8], position % 8, true); 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) { int zmk_split_bt_position_released(uint8_t position) {
WRITE_BIT(position_state[position / 8], position % 8, false); WRITE_BIT(position_state[position / 8], position % 8, false);
return bt_gatt_notify(NULL, &split_svc.attrs[1], &position_state, sizeof(position_state)); 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);

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,47 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
&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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,42 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
&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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -0,0 +1,2 @@
pressed: usage_page 0x07 keycode 0xe0 mods 0x00
pressed: usage_page 0x07 keycode 0xe4 mods 0x00

View file

@ -0,0 +1,45 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
&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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,40 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,117 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/*
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 = <TIMEOUT>;
key-positions = <0 1 2>;
bindings = <&kp X>;
};
combo_two {
timeout-ms = <TIMEOUT>;
key-positions = <0 2>;
bindings = <&kp Y>;
};
combo_three {
timeout-ms = <TIMEOUT>;
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,65 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/*
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,52 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/*
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo//p

View file

@ -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

View file

@ -0,0 +1,53 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/*
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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,84 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,51 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,35 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -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

View file

@ -0,0 +1,45 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -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

View file

@ -0,0 +1,46 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1,2 @@
s/.*hid_listener_keycode_//p
s/.*combo/combo/p

View file

@ -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

View file

@ -0,0 +1,46 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,38 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -0,0 +1 @@
s/.*hid_listener_keycode_//p

View file

@ -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

View file

@ -0,0 +1,38 @@
#include <dt-bindings/zmk/keys.h>
#include <behaviors.dtsi>
#include <dt-bindings/zmk/kscan-mock.h>
/ {
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)
>;
};

View file

@ -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`, `&lt` etc.
### Advanced configuration
There are three global combo parameters which are set through KConfig. You can set them in the `<boardname>.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.

View file

@ -26,7 +26,7 @@ ZMK is currently missing some features found in other popular firmware. This tab
| [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ | | [Display Support](features/displays)[^2] | 🚧 | 🚧 | ✅ |
| [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ | | [RGB Underglow](features/underglow) | ✅ | ✅ | ✅ |
| One Shot Keys | ✅ | ✅ | ✅ | | One Shot Keys | ✅ | ✅ | ✅ |
| [Combo Keys](https://github.com/zmkfirmware/zmk/pull/504) | 🚧 | | ✅ | | [Combo Keys](behaviors/combos) | ✅ | | ✅ |
| Macros | 🚧 | ✅ | ✅ | | Macros | 🚧 | ✅ | ✅ |
| Mouse Keys | 💡 | ✅ | ✅ | | Mouse Keys | 💡 | ✅ | ✅ |
| Low Active Power Usage | ✅ | | | | Low Active Power Usage | ✅ | | |

View file

@ -20,6 +20,7 @@ module.exports = {
"behaviors/misc", "behaviors/misc",
"behaviors/hold-tap", "behaviors/hold-tap",
"behaviors/mod-tap", "behaviors/mod-tap",
"behaviors/combos",
"behaviors/reset", "behaviors/reset",
"behaviors/bluetooth", "behaviors/bluetooth",
"behaviors/outputs", "behaviors/outputs",

View file

@ -91,7 +91,7 @@ echo ""
echo "Keyboard Shield Selection:" echo "Keyboard Shield Selection:"
prompt="Pick an keyboard:" 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 " PS3="$prompt "
# TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos. # TODO: Add support for "Other" and linking to docs on adding custom shields in user config repos.