From a94192c65d21de02cc292fa31768230d03c0d88e Mon Sep 17 00:00:00 2001 From: Nick Conway Date: Fri, 2 Jun 2023 20:53:59 -0400 Subject: [PATCH] Dynamic macros --- app/CMakeLists.txt | 1 + app/Kconfig | 11 + .../behaviors/zmk,behavior-dynamic-macro.yaml | 16 ++ app/include/dt-bindings/zmk/dynamic-macros.h | 8 + app/src/behaviors/behavior_dynamic_macro.c | 206 ++++++++++++++++++ .../dynamic-macros/basic/events.patterns | 3 + .../basic/keycode_events.snapshot | 28 +++ .../basic/native_posix_64.keymap | 27 +++ app/tests/dynamic-macros/behavior_keymap.dtsi | 60 +++++ .../dynamic-macros/call-macro/events.patterns | 3 + .../call-macro/keycode_events.snapshot | 40 ++++ .../call-macro/native_posix_64.keymap | 27 +++ .../dynamic-macros/interrupt/events.patterns | 3 + .../interrupt/keycode_events.snapshot | 28 +++ .../interrupt/native_posix_64.keymap | 27 +++ .../recorded-time/behavior_keymap.dtsi | 59 +++++ .../recorded-time/events.patterns | 3 + .../recorded-time/keycode_events.snapshot | 28 +++ .../recorded-time/native_posix_64.keymap | 27 +++ docs/docs/behaviors/dynamic-macros.md | 72 ++++++ docs/docs/intro.md | 1 + docs/sidebars.js | 1 + 22 files changed, 679 insertions(+) create mode 100644 app/dts/bindings/behaviors/zmk,behavior-dynamic-macro.yaml create mode 100644 app/include/dt-bindings/zmk/dynamic-macros.h create mode 100644 app/src/behaviors/behavior_dynamic_macro.c create mode 100644 app/tests/dynamic-macros/basic/events.patterns create mode 100644 app/tests/dynamic-macros/basic/keycode_events.snapshot create mode 100644 app/tests/dynamic-macros/basic/native_posix_64.keymap create mode 100644 app/tests/dynamic-macros/behavior_keymap.dtsi create mode 100644 app/tests/dynamic-macros/call-macro/events.patterns create mode 100644 app/tests/dynamic-macros/call-macro/keycode_events.snapshot create mode 100644 app/tests/dynamic-macros/call-macro/native_posix_64.keymap create mode 100644 app/tests/dynamic-macros/interrupt/events.patterns create mode 100644 app/tests/dynamic-macros/interrupt/keycode_events.snapshot create mode 100644 app/tests/dynamic-macros/interrupt/native_posix_64.keymap create mode 100644 app/tests/dynamic-macros/recorded-time/behavior_keymap.dtsi create mode 100644 app/tests/dynamic-macros/recorded-time/events.patterns create mode 100644 app/tests/dynamic-macros/recorded-time/keycode_events.snapshot create mode 100644 app/tests/dynamic-macros/recorded-time/native_posix_64.keymap create mode 100644 docs/docs/behaviors/dynamic-macros.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index fd4b7ab5..86c28c43 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -52,6 +52,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_caps_word.c) target_sources(app PRIVATE src/behaviors/behavior_key_repeat.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_MACRO app PRIVATE src/behaviors/behavior_macro.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_DYNAMIC_MACRO app PRIVATE src/behaviors/behavior_dynamic_macro.c) target_sources(app PRIVATE src/behaviors/behavior_momentary_layer.c) target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c) target_sources(app PRIVATE src/behaviors/behavior_outputs.c) diff --git a/app/Kconfig b/app/Kconfig index 1189c654..d01da2b7 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -478,6 +478,11 @@ config ZMK_BEHAVIORS_QUEUE_SIZE int "Maximum number of behaviors to allow queueing from a macro or other complex behavior" default 64 +config ZMK_DYNAMIC_MACRO_MAX_ACTIONS + int "Maximum number of keystrokes to be recorded in a dynamic macro" + default 64 + +DT_COMPAT_ZMK_BEHAVIOR_KEY_TOGGLE := zmk,behavior-key-toggle rsource "Kconfig.behaviors" config ZMK_MACRO_DEFAULT_WAIT_MS @@ -715,6 +720,12 @@ choice CBPRINTF_IMPLEMENTATION endchoice +DT_COMPAT_ZMK_BEHAVIOR_DYNAMIC_MACRO := zmk,behavior-dynamic-macro + +config ZMK_BEHAVIOR_DYNAMIC_MACRO + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_BEHAVIOR_DYNAMIC_MACRO)) + module = ZMK module-str = zmk source "subsys/logging/Kconfig.template.log_config" diff --git a/app/dts/bindings/behaviors/zmk,behavior-dynamic-macro.yaml b/app/dts/bindings/behaviors/zmk,behavior-dynamic-macro.yaml new file mode 100644 index 00000000..48db2f2c --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-dynamic-macro.yaml @@ -0,0 +1,16 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Dynamic Macro Behavior + +compatible: "zmk,behavior-dynamic-macro" + +include: one_param.yaml + +properties: + wait-ms: + type: int + default: -1 + description: The default time to wait (in milliseconds) before triggering the next behavior in the macro bindings list. + no-output: + type: boolean diff --git a/app/include/dt-bindings/zmk/dynamic-macros.h b/app/include/dt-bindings/zmk/dynamic-macros.h new file mode 100644 index 00000000..29f15489 --- /dev/null +++ b/app/include/dt-bindings/zmk/dynamic-macros.h @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define PLAY 0 +#define RECORD 1 diff --git a/app/src/behaviors/behavior_dynamic_macro.c b/app/src/behaviors/behavior_dynamic_macro.c new file mode 100644 index 00000000..5a04b09c --- /dev/null +++ b/app/src/behaviors/behavior_dynamic_macro.c @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define DT_DRV_COMPAT zmk_behavior_dynamic_macro + +#define HID_KEY_USAGE_PAGE 0x70000 + +int8_t total_recorded_actions = 0; + +struct behavior_dynamic_macro_bind { + uint32_t wait_ms; + bool pressed; + struct zmk_behavior_binding binding; +}; + +struct behavior_dynamic_macro_state { + bool recording; + uint32_t lastEventTime; + uint32_t count; + struct behavior_dynamic_macro_bind bindings[CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS]; +}; + +struct behavior_dynamic_macro_config { + uint32_t wait_ms; + bool no_output; +}; + +#define ZMK_BHV_RECORDING_MACRO_MAX 10 + +struct recording_macro { + uint32_t count; + uint32_t position; + bool recording; + const struct behavior_dynamic_macro_config *config; + struct behavior_dynamic_macro_state *state; +}; + +struct recording_macro recording_macros[ZMK_BHV_RECORDING_MACRO_MAX] = {}; + +static struct recording_macro *find_recording_macro(uint32_t position) { + for (int i = 0; i < ZMK_BHV_RECORDING_MACRO_MAX; i++) { + if (recording_macros[i].position == position && recording_macros[i].recording) { + return &recording_macros[i]; + } + } + return NULL; +} + +static int new_recording_macro(uint32_t position, + const struct behavior_dynamic_macro_config *config, + struct behavior_dynamic_macro_state *state, + struct recording_macro **macro) { + for (int i = 0; i < ZMK_BHV_RECORDING_MACRO_MAX; i++) { + struct recording_macro *const ref_macro = &recording_macros[i]; + if (!ref_macro->recording) { + ref_macro->recording = true; + ref_macro->count = 0; + ref_macro->position = position; + ref_macro->config = config; + ref_macro->state = state; + *macro = ref_macro; + return 0; + } + } + return -ENOMEM; +} + +static void queue_dynamic_macro(uint32_t position, uint32_t time, + struct behavior_dynamic_macro_state *state) { + LOG_DBG("Iterating dynamic macro bindings - count: %d", state->count); + for (int i = 0; i < state->count; i++) { + uint32_t wait_ms = time; + if (time == -1) { + wait_ms = state->bindings[i].wait_ms; + } + zmk_behavior_queue_add(position, state->bindings[i].binding, state->bindings[i].pressed, + wait_ms); + } +} + +static int on_dynamic_macro_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_dynamic_macro_config *cfg = dev->config; + struct behavior_dynamic_macro_state *state = dev->data; + + if (binding->param1 == PLAY) { + if (state->recording) { + LOG_ERR("Macro is currently recording, can't play"); + } else { + LOG_DBG("Playing Dynamic Macro"); + queue_dynamic_macro(event.position, cfg->wait_ms, state); + } + } else if (binding->param1 == RECORD) { + state->recording = !state->recording; + LOG_DBG("Recording Status: %d", state->recording); + if (state->recording) { + struct recording_macro *macro; + macro = find_recording_macro(event.position); + if (new_recording_macro(event.position, cfg, state, ¯o) == -ENOMEM) { + LOG_ERR("Unable to record new macro. Insufficient space in recording_macros[]"); + return ZMK_BEHAVIOR_OPAQUE; + } + LOG_DBG("Recording new macro: %d", event.position); + if (macro) { + total_recorded_actions -= macro->count; + } + macro->count = 0; + } else { + struct recording_macro *macro; + macro = find_recording_macro(event.position); + macro->recording = false; + macro->state->count = macro->count; + } + } + + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_dynamic_macro_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return ZMK_BEHAVIOR_OPAQUE; +} + +static int behavior_dynamic_macro_init(const struct device *dev) { return 0; }; + +static const struct behavior_driver_api behavior_dynamic_macro_driver_api = { + .binding_pressed = on_dynamic_macro_binding_pressed, + .binding_released = on_dynamic_macro_binding_released, +}; + +static int dynamic_macro_keycode_state_changed_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_dynamic_macro, dynamic_macro_keycode_state_changed_listener); +ZMK_SUBSCRIPTION(behavior_dynamic_macro, zmk_keycode_state_changed); + +static int dynamic_macro_keycode_state_changed_listener(const zmk_event_t *eh) { + struct zmk_keycode_state_changed *ev = as_zmk_keycode_state_changed(eh); + if (ev == NULL) { + return ZMK_EV_EVENT_BUBBLE; + } + + for (int i = 0; i < ZMK_BHV_RECORDING_MACRO_MAX; i++) { + struct recording_macro *macro = &recording_macros[i]; + if (macro->recording && total_recorded_actions < CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS) { + uint32_t eventTime = k_uptime_get(); + uint32_t elapsedTime = eventTime - macro->state->lastEventTime; + macro->state->lastEventTime = eventTime; + if (ev->state) { + macro->state->bindings[macro->count].pressed = true; + } else { + macro->state->bindings[macro->count].pressed = false; + } + macro->state->bindings[macro->count].binding.behavior_dev = "KEY_PRESS"; + macro->state->bindings[macro->count].binding.param1 = HID_KEY_USAGE_PAGE + ev->keycode; + macro->state->bindings[macro->count].binding.param2 = 0; + + if (macro->count > 0) { + macro->state->bindings[macro->count - 1].wait_ms = elapsedTime; + } + + macro->count++; + total_recorded_actions++; + + if (macro->config->no_output) { + return ZMK_EV_EVENT_HANDLED; + } + return ZMK_EV_EVENT_BUBBLE; + } else if (total_recorded_actions >= CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS) { + LOG_ERR( + "Action not recorded, not enough space, CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS %d", + CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS); + if (macro->config->no_output) { + return ZMK_EV_EVENT_HANDLED; + } + return ZMK_EV_EVENT_BUBBLE; + } + } + return ZMK_EV_EVENT_BUBBLE; +} + +#define DYNAMIC_MACRO_INST(n) \ + static struct behavior_dynamic_macro_state behavior_dynamic_macro_state_##n = { \ + .recording = false, .count = 0}; \ + static struct behavior_dynamic_macro_config behavior_dynamic_macro_config_##n = { \ + .wait_ms = DT_INST_PROP_OR(n, wait_ms, -1), .no_output = DT_INST_PROP(n, no_output)}; \ + DEVICE_DT_INST_DEFINE(n, behavior_dynamic_macro_init, NULL, &behavior_dynamic_macro_state_##n, \ + &behavior_dynamic_macro_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &behavior_dynamic_macro_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(DYNAMIC_MACRO_INST) diff --git a/app/tests/dynamic-macros/basic/events.patterns b/app/tests/dynamic-macros/basic/events.patterns new file mode 100644 index 00000000..e399dfe6 --- /dev/null +++ b/app/tests/dynamic-macros/basic/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode/kp/p +s/.*behavior_queue_process_next/queue_process_next/p +s/.*macro_binding/mac/p diff --git a/app/tests/dynamic-macros/basic/keycode_events.snapshot b/app/tests/dynamic-macros/basic/keycode_events.snapshot new file mode 100644 index 00000000..2be6ee25 --- /dev/null +++ b/app/tests/dynamic-macros/basic/keycode_events.snapshot @@ -0,0 +1,28 @@ +mac_pressed: Recording Status: 1 +mac_pressed: Recording new macro: 0 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +mac_pressed: Recording Status: 0 +mac_pressed: Playing Dynamic Macro +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms diff --git a/app/tests/dynamic-macros/basic/native_posix_64.keymap b/app/tests/dynamic-macros/basic/native_posix_64.keymap new file mode 100644 index 00000000..43ed36d0 --- /dev/null +++ b/app/tests/dynamic-macros/basic/native_posix_64.keymap @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,100) + >; +}; diff --git a/app/tests/dynamic-macros/behavior_keymap.dtsi b/app/tests/dynamic-macros/behavior_keymap.dtsi new file mode 100644 index 00000000..3d163aea --- /dev/null +++ b/app/tests/dynamic-macros/behavior_keymap.dtsi @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/ { + macros { + dm1: dm1 { + compatible = "zmk,behavior-dynamic-macro"; + label = "dynamic-macro-1"; + wait-ms = <10>; + #binding-cells = <1>; + }; + dm2: dm2 { + compatible = "zmk,behavior-dynamic-macro"; + label = "dynamic-macro-2"; + wait-ms = <10>; + #binding-cells = <1>; + }; + rm1: rm1 { + compatible = "zmk,behavior-macro"; + label = "macro-1"; + wait-ms = <10>; + tap-ms = <10>; + #binding-cells = <0>; + bindings = <&kp A &kp A &kp A>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &dm1 RECORD &dm1 PLAY + &kp A &tog 1>; + }; + + extra_layer { + bindings = < + &kp N1 &rm1 + &tog 2 &trans>; + + }; + + extra_layer_2 { + bindings = < + &dm2 RECORD &dm2 PLAY + &trans &kp N1>; + }; + + }; +}; diff --git a/app/tests/dynamic-macros/call-macro/events.patterns b/app/tests/dynamic-macros/call-macro/events.patterns new file mode 100644 index 00000000..e399dfe6 --- /dev/null +++ b/app/tests/dynamic-macros/call-macro/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode/kp/p +s/.*behavior_queue_process_next/queue_process_next/p +s/.*macro_binding/mac/p diff --git a/app/tests/dynamic-macros/call-macro/keycode_events.snapshot b/app/tests/dynamic-macros/call-macro/keycode_events.snapshot new file mode 100644 index 00000000..fb128a85 --- /dev/null +++ b/app/tests/dynamic-macros/call-macro/keycode_events.snapshot @@ -0,0 +1,40 @@ +mac_pressed: Recording Status: 1 +mac_pressed: Recording new macro: 0 +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +mac_pressed: Recording Status: 0 +mac_pressed: Playing Dynamic Macro +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms diff --git a/app/tests/dynamic-macros/call-macro/native_posix_64.keymap b/app/tests/dynamic-macros/call-macro/native_posix_64.keymap new file mode 100644 index 00000000..2ddb6fad --- /dev/null +++ b/app/tests/dynamic-macros/call-macro/native_posix_64.keymap @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,100) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,100) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,100) + >; +}; diff --git a/app/tests/dynamic-macros/interrupt/events.patterns b/app/tests/dynamic-macros/interrupt/events.patterns new file mode 100644 index 00000000..e399dfe6 --- /dev/null +++ b/app/tests/dynamic-macros/interrupt/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode/kp/p +s/.*behavior_queue_process_next/queue_process_next/p +s/.*macro_binding/mac/p diff --git a/app/tests/dynamic-macros/interrupt/keycode_events.snapshot b/app/tests/dynamic-macros/interrupt/keycode_events.snapshot new file mode 100644 index 00000000..2be6ee25 --- /dev/null +++ b/app/tests/dynamic-macros/interrupt/keycode_events.snapshot @@ -0,0 +1,28 @@ +mac_pressed: Recording Status: 1 +mac_pressed: Recording new macro: 0 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +mac_pressed: Recording Status: 0 +mac_pressed: Playing Dynamic Macro +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 10ms diff --git a/app/tests/dynamic-macros/interrupt/native_posix_64.keymap b/app/tests/dynamic-macros/interrupt/native_posix_64.keymap new file mode 100644 index 00000000..303e60ce --- /dev/null +++ b/app/tests/dynamic-macros/interrupt/native_posix_64.keymap @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,100) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,100) + >; +}; diff --git a/app/tests/dynamic-macros/recorded-time/behavior_keymap.dtsi b/app/tests/dynamic-macros/recorded-time/behavior_keymap.dtsi new file mode 100644 index 00000000..bdd6bbde --- /dev/null +++ b/app/tests/dynamic-macros/recorded-time/behavior_keymap.dtsi @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +/ { + macros { + dm1: dm1 { + compatible = "zmk,behavior-dynamic-macro"; + label = "dynamic-macro-1"; + #binding-cells = <1>; + }; + dm2: dm2 { + compatible = "zmk,behavior-dynamic-macro"; + label = "dynamic-macro-2"; + wait-ms = <10>; + #binding-cells = <1>; + }; + rm1: rm1 { + compatible = "zmk,behavior-macro"; + label = "macro-1"; + wait-ms = <10>; + tap-ms = <10>; + #binding-cells = <0>; + bindings = <&kp A &kp A &kp A>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &dm1 RECORD &dm1 PLAY + &kp A &tog 1>; + }; + + extra_layer { + bindings = < + &kp N1 &rm1 + &tog 2 &trans>; + + }; + + extra_layer_2 { + bindings = < + &dm2 RECORD &dm2 PLAY + &trans &kp N1>; + }; + + }; +}; diff --git a/app/tests/dynamic-macros/recorded-time/events.patterns b/app/tests/dynamic-macros/recorded-time/events.patterns new file mode 100644 index 00000000..e399dfe6 --- /dev/null +++ b/app/tests/dynamic-macros/recorded-time/events.patterns @@ -0,0 +1,3 @@ +s/.*hid_listener_keycode/kp/p +s/.*behavior_queue_process_next/queue_process_next/p +s/.*macro_binding/mac/p diff --git a/app/tests/dynamic-macros/recorded-time/keycode_events.snapshot b/app/tests/dynamic-macros/recorded-time/keycode_events.snapshot new file mode 100644 index 00000000..e8d34635 --- /dev/null +++ b/app/tests/dynamic-macros/recorded-time/keycode_events.snapshot @@ -0,0 +1,28 @@ +mac_pressed: Recording Status: 1 +mac_pressed: Recording new macro: 0 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +mac_pressed: Recording Status: 0 +mac_pressed: Playing Dynamic Macro +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 11ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 1001ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 11ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 1001ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 11ms +queue_process_next: Invoking KEY_PRESS: 0x70004 0x00 +kp_released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +queue_process_next: Processing next queued behavior in 0ms diff --git a/app/tests/dynamic-macros/recorded-time/native_posix_64.keymap b/app/tests/dynamic-macros/recorded-time/native_posix_64.keymap new file mode 100644 index 00000000..2c527b66 --- /dev/null +++ b/app/tests/dynamic-macros/recorded-time/native_posix_64.keymap @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include "behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,1000) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,1000) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,1000) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,100) + ZMK_MOCK_PRESS(0,1,10000) + ZMK_MOCK_RELEASE(0,1,100) + >; +}; diff --git a/docs/docs/behaviors/dynamic-macros.md b/docs/docs/behaviors/dynamic-macros.md new file mode 100644 index 00000000..2bde5cea --- /dev/null +++ b/docs/docs/behaviors/dynamic-macros.md @@ -0,0 +1,72 @@ +--- +title: Dynamic Macro Behavior +sidebar_label: Dynamic Macros +--- + +## Summary + +The dynamic macro behavior allows creating macros and replaying them by recording key presses. While recording a macro, you can also play another macro. + +:::note +Dynamic macros are cleared on reboot. +::: + +:::warning +Dynamic macros are memory intensive, and may cause the firmware to crash. It is recommended to use only 1 dynamic macro in your keymap and re-record it if it needs changed. The maximum amount of actions to be recorded can be set with `CONFIG_ZMK_DYNAMIC_MACRO_MAX_ACTIONS` (default 64). +::: + +## Dynamic Macro Action Defines + +Dynamic macro action defines are provided through the [`dt-bindings/zmk/dynamic-macros.h`](https://github.com/zmkfirmware/zmk/blob/main/app/include/dt-bindings/zmk/dynamic-macros.h) header, +which is added at the top of the keymap file: + +``` +#include +``` + +This will allow you to reference the actions defined in this header such as `PLAY`. + +Here is a table describing the action for each define: + +| Define | Action | +| -------- | --------------------------- | +| `PLAY` | Play back a recorded macro | +| `RECORD` | Toggle recording of a macro | + +## Macro Definition + +A dynamic macro definition looks like: + +``` +/ { + macros { + dyn-macro: dyn-macro { + label = "ZM_dynamic-macro"; + compatible = "zmk,behavior-dynamic-macro"; + #binding-cells = <1>; + }; + }; +}; +``` + +The macro can then be bound in your keymap by referencing it by the label `dyn-macro` followed by PLAY or RECORD, e.g.: + +``` + / { + keymap { + &dyn-macro PLAY &dyn-macro RECORD + ... + }; +}; +``` + +## Configuration + +### Wait Time + +The wait time setting controls how long of a delay is introduced between behaviors. By default, a macro will play back at the speed it +was recorded, but it can be overwritten by assigning a value to the `wait-ms` property of the macro, e.g. `wait-ms = <20>;`. + +### No Output + +By default, keystrokes will still be sent to the host while a dynamic macro is recording. Setting `no-output` will change this and will not send keystrokes to the host while recording. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index e11eda71..09469bcb 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -33,6 +33,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | One Shot Keys | ✅ | ✅ | ✅ | | [Combo Keys](features/combos.md) | ✅ | | ✅ | | [Macros](behaviors/macros.md) | ✅ | ✅ | ✅ | +| [Dynamic Macros](behaviors/dynamic-macros.md) | ✅ | | ✅ | | Mouse Keys | 🚧 | ✅ | ✅ | | Low Active Power Usage | ✅ | | | | Low Power Sleep States | ✅ | ✅ | | diff --git a/docs/sidebars.js b/docs/sidebars.js index d4c398b4..66d1817a 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -61,6 +61,7 @@ module.exports = { "behaviors/hold-tap", "behaviors/mod-tap", "behaviors/mod-morph", + "behaviors/dynamic-macros", "behaviors/macros", "behaviors/key-toggle", "behaviors/sticky-key",