From 83dbb58bbdcfa77a18e47f14516bb16e9948084e Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 16 Apr 2023 15:13:50 -0500 Subject: [PATCH 01/10] refactor: Move reset logic to a new function Moved the logic to select the type of reboot from behavior_reset.c to a new zmk_reset() function. This allows rebooting to bootloader from other code, and it gives us a starting point for future work to support other bootloaders aside from the Adafruit nrf52 bootloader. --- app/CMakeLists.txt | 1 + app/dts/behaviors/reset.dtsi | 2 +- app/include/dt-bindings/zmk/reset.h | 10 +++----- app/include/zmk/reset.h | 16 +++++++++++++ app/src/behaviors/behavior_reset.c | 6 ++++- app/src/reset.c | 37 +++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 app/include/zmk/reset.h create mode 100644 app/src/reset.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index cc38244a4..2eee25a3f 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -30,6 +30,7 @@ target_sources(app PRIVATE src/behavior.c) target_sources_ifdef(CONFIG_ZMK_KSCAN_SIDEBAND_BEHAVIORS app PRIVATE src/kscan_sideband_behaviors.c) target_sources(app PRIVATE src/matrix_transform.c) target_sources(app PRIVATE src/physical_layouts.c) +target_sources(app PRIVATE src/reset.c) target_sources(app PRIVATE src/sensors.c) target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c) target_sources(app PRIVATE src/event_manager.c) diff --git a/app/dts/behaviors/reset.dtsi b/app/dts/behaviors/reset.dtsi index 1b29f9d19..a6adadeac 100644 --- a/app/dts/behaviors/reset.dtsi +++ b/app/dts/behaviors/reset.dtsi @@ -18,7 +18,7 @@ // Behavior can be invoked on peripherals, so name must be <= 8 characters. bootloader: bootload { compatible = "zmk,behavior-reset"; - type = ; + type = ; bootloader; #binding-cells = <0>; display-name = "Bootloader"; diff --git a/app/include/dt-bindings/zmk/reset.h b/app/include/dt-bindings/zmk/reset.h index 2b3d8760d..63f3428e2 100644 --- a/app/include/dt-bindings/zmk/reset.h +++ b/app/include/dt-bindings/zmk/reset.h @@ -4,10 +4,6 @@ * SPDX-License-Identifier: MIT */ -#define RST_WARM 0x00 -#define RST_COLD 0x01 - -// AdaFruit nrf52 Bootloader Specific. See -// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/d6b28e66053eea467166f44875e3c7ec741cb471/src/main.c#L107 - -#define RST_UF2 0x57 \ No newline at end of file +#define ZMK_RESET_WARM 0 +#define ZMK_RESET_COLD 1 +#define ZMK_RESET_BOOTLOADER 2 diff --git a/app/include/zmk/reset.h b/app/include/zmk/reset.h new file mode 100644 index 000000000..d23d1d8e6 --- /dev/null +++ b/app/include/zmk/reset.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +/** + * Reboot the system. + * @param type A ZMK_RESET_* value indicating how to reboot. + */ +FUNC_NORETURN void zmk_reset(int type); diff --git a/app/src/behaviors/behavior_reset.c b/app/src/behaviors/behavior_reset.c index 67f33aaed..f9e4d50ad 100644 --- a/app/src/behaviors/behavior_reset.c +++ b/app/src/behaviors/behavior_reset.c @@ -7,12 +7,12 @@ #define DT_DRV_COMPAT zmk_behavior_reset #include -#include #include #include #include +#include #if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) @@ -36,6 +36,7 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); const struct behavior_reset_config *cfg = dev->config; +<<<<<<< HEAD #if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) int ret = bootmode_set(cfg->boot_mode); if (ret < 0) { @@ -50,6 +51,9 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, sys_reboot(cfg->type); #endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ +======= + zmk_reset(cfg->type); +>>>>>>> 1d73fc26 (refactor: Move reset logic to a new function) return ZMK_BEHAVIOR_OPAQUE; } diff --git a/app/src/reset.c b/app/src/reset.c new file mode 100644 index 000000000..0470f519d --- /dev/null +++ b/app/src/reset.c @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +// AdaFruit nrf52 Bootloader Specific. See +// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/d6b28e66053eea467166f44875e3c7ec741cb471/src/main.c#L107 +#define ADAFRUIT_MAGIC_UF2 0x57 + +FUNC_NORETURN void zmk_reset(int type) { + switch (type) { + case ZMK_RESET_WARM: + sys_reboot(SYS_REBOOT_WARM); + break; + + case ZMK_RESET_COLD: + sys_reboot(SYS_REBOOT_COLD); + break; + + case ZMK_RESET_BOOTLOADER: + // TODO: Add support for other types of bootloaders. + sys_reboot(ADAFRUIT_MAGIC_UF2); + break; + + default: + LOG_ERR("Unknown reset type %d", type); + break; + } +} From 60c8e0959aee90e2e5eab29c1be83ccb3b3f6bd1 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sat, 29 Apr 2023 23:24:11 -0500 Subject: [PATCH 02/10] refactor: Add a settings reset function For now, this just clears BLE bonds, but it can be updated in the future to handle clearing all settings. --- app/include/zmk/ble.h | 4 +++ app/include/zmk/reset.h | 8 ++++++ app/src/ble.c | 59 +++++++++++++++++++++++------------------ app/src/reset.c | 11 ++++++++ 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h index 92b2107d8..95f0bbfb1 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -30,6 +30,7 @@ int zmk_ble_active_profile_index(void); int zmk_ble_profile_index(const bt_addr_le_t *addr); bt_addr_le_t *zmk_ble_profile_address(uint8_t index); +<<<<<<< HEAD bt_addr_le_t *zmk_ble_active_profile_addr(void); struct bt_conn *zmk_ble_active_profile_conn(void); @@ -43,6 +44,9 @@ char *zmk_ble_active_profile_name(void); int zmk_ble_unpair_all(void); int zmk_ble_set_device_name(char *name); +======= +void zmk_ble_unpair_all(void); +>>>>>>> c072f755 (refactor: Add a settings reset function) #if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr); diff --git a/app/include/zmk/reset.h b/app/include/zmk/reset.h index d23d1d8e6..fad6db6a4 100644 --- a/app/include/zmk/reset.h +++ b/app/include/zmk/reset.h @@ -14,3 +14,11 @@ * @param type A ZMK_RESET_* value indicating how to reboot. */ FUNC_NORETURN void zmk_reset(int type); + +/** + * Clear all persistent settings. + * + * This should typically be followed by a call to zmk_reset() to ensure that + * all subsystems are properly reset. + */ +void zmk_reset_settings(void); diff --git a/app/src/ble.c b/app/src/ble.c index 51b172b9f..ea25d58eb 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -39,6 +39,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY) #include +#include "ble.h" #define PASSKEY_DIGITS 6 @@ -336,6 +337,37 @@ int zmk_ble_prof_disconnect(uint8_t index) { return result; } +void zmk_ble_unpair_all(void) { + LOG_WRN("Clearing all existing BLE bond information from the keyboard"); + + int err = bt_unpair(BT_ID_DEFAULT, NULL); + if (err) { + LOG_ERR("Failed to unpair default identity: %d", err); + } + + for (int i = 0; i < 8; i++) { + char setting_name[15]; + sprintf(setting_name, "ble/profiles/%d", i); + + int err = settings_delete(setting_name); + if (err) { + LOG_ERR("Failed to delete setting: %d", err); + } + } + + // Hardcoding a reasonable hardcoded value of peripheral addresses + // to clear so we properly clear a split central as well. + for (int i = 0; i < 8; i++) { + char setting_name[32]; + sprintf(setting_name, "ble/peripheral_addresses/%d", i); + + int err = settings_delete(setting_name); + if (err) { + LOG_ERR("Failed to delete setting: %d", err); + } + } +} + bt_addr_le_t *zmk_ble_active_profile_addr(void) { return &profiles[active_profile].peer; } struct bt_conn *zmk_ble_active_profile_conn(void) { @@ -692,32 +724,7 @@ static void zmk_ble_ready(int err) { static int zmk_ble_complete_startup(void) { #if IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) - LOG_WRN("Clearing all existing BLE bond information from the keyboard"); - - bt_unpair(BT_ID_DEFAULT, NULL); - - for (int i = 0; i < 8; i++) { - char setting_name[15]; - sprintf(setting_name, "ble/profiles/%d", i); - - int err = settings_delete(setting_name); - if (err) { - LOG_ERR("Failed to delete setting: %d", err); - } - } - - // Hardcoding a reasonable hardcoded value of peripheral addresses - // to clear so we properly clear a split central as well. - for (int i = 0; i < 8; i++) { - char setting_name[32]; - sprintf(setting_name, "ble/peripheral_addresses/%d", i); - - int err = settings_delete(setting_name); - if (err) { - LOG_ERR("Failed to delete setting: %d", err); - } - } - + zmk_ble_unpair_all(); #endif // IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) bt_conn_cb_register(&conn_callbacks); diff --git a/app/src/reset.c b/app/src/reset.c index 0470f519d..c0fff9871 100644 --- a/app/src/reset.c +++ b/app/src/reset.c @@ -6,7 +6,11 @@ #include #include +#include +#if IS_ENABLED(CONFIG_ZMK_BLE) +#include +#endif #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -35,3 +39,10 @@ FUNC_NORETURN void zmk_reset(int type) { break; } } + +void zmk_reset_settings(void) { +#if IS_ENABLED(CONFIG_ZMK_BLE) + zmk_ble_unpair_all(); +#endif + // TODO: clear settings for all subsystems. +} \ No newline at end of file From d6b3707dde14cd48f5f91eb95da88a4625a619a9 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 16 Apr 2023 18:00:49 -0500 Subject: [PATCH 03/10] feat: Add a way to jump to bootloader on startup This adds an optional feature to trigger an action if a specific key is held when the keyboard is powered on. It can be configured to jump to the bootloader and/or clear settings. This is inspired by QMK's "bootmagic lite" feature, and it is primarily intended as a way to recover a keyboard which doesn't have a physical reset button in case it is flashed with firmware that doesn't have a &bootloader key in its keymap. It can also be used to clear BLE bonds on the peripheral side of a split keyboard without needing to flash special firmware. --- app/CMakeLists.txt | 1 + app/Kconfig | 14 ++ app/dts/bindings/zmk,boot-magic-key.yaml | 23 +++ app/src/boot_magic_key.c | 80 +++++++++ docs/docs/config/system.md | 11 +- docs/docs/features/boot-magic-key.md | 209 +++++++++++++++++++++++ docs/sidebars.js | 98 ++++++++++- 7 files changed, 424 insertions(+), 12 deletions(-) create mode 100644 app/dts/bindings/zmk,boot-magic-key.yaml create mode 100644 app/src/boot_magic_key.c create mode 100644 docs/docs/features/boot-magic-key.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 2eee25a3f..848525d34 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -105,6 +105,7 @@ target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) +target_sources_ifdef(CONFIG_ZMK_BOOT_MAGIC_KEY app PRIVATE src/boot_magic_key.c) target_sources(app PRIVATE src/main.c) add_subdirectory(src/display/) diff --git a/app/Kconfig b/app/Kconfig index e1c4ada9f..11a2697a3 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -691,6 +691,20 @@ config ZMK_KEYMAP_SENSORS_DEFAULT_TRIGGERS_PER_ROTATION endif # ZMK_KEYMAP_SENSORS +choice CBPRINTF_IMPLEMENTATION + default CBPRINTF_NANO + +endchoice + +DT_COMPAT_ZMK_BOOT_MAGIC_KEY := zmk,boot-magic-key +config ZMK_BOOT_MAGIC_KEY + bool "Enable actions when keys are held at boot" + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_BOOT_MAGIC_KEY)) + +config ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS + int "Milliseconds to wait for a boot magic key at startup" + default 500 + module = ZMK module-str = zmk source "subsys/logging/Kconfig.template.log_config" diff --git a/app/dts/bindings/zmk,boot-magic-key.yaml b/app/dts/bindings/zmk,boot-magic-key.yaml new file mode 100644 index 000000000..3629c0e8a --- /dev/null +++ b/app/dts/bindings/zmk,boot-magic-key.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2023, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + Triggers one or more actions if a specific key is held while the keyboard boots. + This is typically used for recovering a keyboard in cases such as &bootloader + being missing from the keymap or a split peripheral which isn't connected to + the central, and therefore can't process th ekeymap. + +compatible: "zmk,boot-magic-key" + +properties: + key-position: + type: int + default: 0 + description: Zero-based index of the key which triggers the action(s). + # Boot magic actions: + jump-to-bootloader: + type: boolean + description: Reboots into the bootloader. + reset-settings: + type: boolean + description: Clears settings and reboots. diff --git a/app/src/boot_magic_key.c b/app/src/boot_magic_key.c new file mode 100644 index 000000000..bc29873af --- /dev/null +++ b/app/src/boot_magic_key.c @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_boot_magic_key + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct boot_key_config { + int key_position; + bool jump_to_bootloader; + bool reset_settings; +}; + +#define BOOT_KEY_CONFIG(n) \ + { \ + .key_position = DT_INST_PROP_OR(n, key_position, 0), \ + .jump_to_bootloader = DT_INST_PROP_OR(n, jump_to_bootloader, false), \ + .reset_settings = DT_INST_PROP_OR(n, reset_settings, false), \ + }, + +static const struct boot_key_config boot_keys[] = {DT_INST_FOREACH_STATUS_OKAY(BOOT_KEY_CONFIG)}; + +static int64_t timeout_uptime; + +static int timeout_init(const struct device *device) { + timeout_uptime = k_uptime_get() + CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS; + return 0; +} + +SYS_INIT(timeout_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + +static void trigger_boot_key(const struct boot_key_config *config) { + if (config->reset_settings) { + LOG_INF("Boot key: resetting settings"); + zmk_reset_settings(); + } + + if (config->jump_to_bootloader) { + LOG_INF("Boot key: jumping to bootloader"); + zmk_reset(ZMK_RESET_BOOTLOADER); + } else if (config->reset_settings) { + // If resetting settings but not jumping to bootloader, we need to reboot + // to ensure all subsystems are properly reset. + zmk_reset(ZMK_RESET_WARM); + } +} + +static int event_listener(const zmk_event_t *eh) { + if (likely(k_uptime_get() > timeout_uptime)) { + return ZMK_EV_EVENT_BUBBLE; + } + + const struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); + if (ev && ev->state) { + for (int i = 0; i < ARRAY_SIZE(boot_keys); i++) { + if (ev->position == boot_keys[i].key_position) { + trigger_boot_key(&boot_keys[i]); + } + } + } + + return ZMK_EV_EVENT_BUBBLE; +} + +ZMK_LISTENER(boot_magic_key, event_listener); +ZMK_SUBSCRIPTION(boot_magic_key, zmk_position_state_changed); diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index e00dfd7a0..631be86eb 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -13,11 +13,12 @@ Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/ ### General -| Config | Type | Description | Default | -| --------------------------- | ------ | -------------------------------------------- | ------- | -| `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | -| `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | -| `CONFIG_HEAP_MEM_POOL_SIZE` | int | Size of the heap memory pool | 8192 | +| Config | Type | Description | Default | +|----------------------------------------|--------|---------------------------------------------------------------------------------------|---------| +| `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | +| `CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS` | int | Milliseconds to watch for [boot magic keys](../features/boot-magic-key.md) at startup | 500 | +| `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | +| `CONFIG_HEAP_MEM_POOL_SIZE` | int | Size of the heap memory pool | 8192 | :::info diff --git a/docs/docs/features/boot-magic-key.md b/docs/docs/features/boot-magic-key.md new file mode 100644 index 000000000..d51c2b340 --- /dev/null +++ b/docs/docs/features/boot-magic-key.md @@ -0,0 +1,209 @@ +--- +title: Boot Magic Key +sidebar_label: Boot Magic Key +--- + +A boot magic key performs one or more actions if a specific key is held while powering on the keyboard. This is useful for recovering a keyboard which doesn't have a physical reset button. It also works on the peripheral side of a split keyboard, even when it isn't connected to the central side. + +## Magic Keys + +To define a boot magic key on a new board or shield, add a `zmk,boot-magic-key` node to your board's `.dts` file or shield's `.overlay` file and select which key will trigger it with the `key-position` property. + +You can also enable the feature for any keyboard by adding it to your `.keymap` file. + +```c +/ { + ... + bootloader_key: bootloader_key { + compatible = "zmk,boot-magic-key"; + key-position = <0>; + }; + ... +}; +``` + +:::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. + +::: + +If `key-position` is omitted, it will trigger for the key in position `0`. + +Next, you should add properties to determine what the magic key will do: + +### Jump to Bootloader + +If a boot magic key has a `jump-to-bootloader` property, it will reboot to the bootloader: + +```c +/ { + ... + bootloader_key: bootloader_key { + compatible = "zmk,boot-magic-key"; + ... + jump-to-bootloader; + }; + ... +}; +``` + +### Reset Settings + +If a boot magic key has a `reset-settings` property, it will reset persistent settings and then reboot: + +```c +/ { + ... + reset_settings_key: reset_settings_key { + compatible = "zmk,boot-magic-key"; + ... + reset-settings; + }; + ... +}; +``` + +:::info + +This clears all BLE bonds. You will need to re-pair the keyboard with any hosts after using this. + +::: + +:::caution + +Currently this action _only_ clears BLE bonds. It will be updated to reset all settings in the future. + +::: + +## Multiple Actions + +If you want a single boot magic key to perform multiple actions, simply add properties for each action to the same `zmk,boot-magic-key` node. The order of the properties does not matter. + +For example, to make a key that resets settings and then reboots to the bootloader, add both `reset-settings` and `jump-to-bootloader`: + +```c +/ { + ... + recovery_key: recovery_key { + compatible = "zmk,boot-magic-key"; + jump-to-bootloader; + reset-settings; + }; + ... +}; +``` + +:::info + +You may define multiple `zmk,boot-magic-key` nodes for different keys, but note that if you hold multiple keys at boot, they will be run in an arbitrary order. If one of them reboots the keyboard, the rest of the keys will not run. + +::: + +## Split Keyboards + +For split keyboards, you can define multiple boot magic keys and then only enable the correct key(s) for each side. For example, if key 0 is the top-left key on the left side and key 11 is the top-right key on the right side, you could use: + +**shield.dtsi** + +```c +/ { + ... + bootloader_key_left: bootloader_key_left { + compatible = "zmk,boot-magic-key"; + key-position = <0>; + jump-to-bootloader; + status = "disabled"; + }; + + bootloader_key_right: bootloader_key_right { + compatible = "zmk,boot-magic-key"; + key-position = <11>; + jump-to-bootloader; + status = "disabled"; + }; + ... +}; +``` + +**shield_left.overlay** + +```c +#include "shield.dtsi" + +&bootloader_key_left { + status = "okay"; +}; +``` + +**shield_right.overlay** + +```c +#include "shield.dtsi" + +&bootloader_key_right { + status = "okay"; +}; +``` + +## Key Positions and Alternate Layouts + +Key positions are affected by the [matrix transform](../config/kscan.md#matrix-transform), so if your keyboard has multiple transforms for alternate layouts, you may need to adjust positions according to the user's selected transform. There is no automatic way to do this, but one way to simplify things for users is to add a block of commented out code to the keymap which selects the transform and updates the key positions to match if uncommented. + +For example, consider a split keyboard which has 6 columns per side by default but supports a 5-column layout, and assume you want the top-left key on the left side and the top-right key on the right side to be boot magic keys. The top-left key will be position 0 regardless of layout, but the top-right key will be position 11 by default and position 9 in the 5-column layout. + +**shield.dtsi** + +```c +/ { + chosen { + zmk,matrix_transform = &default_transform; + }; + + bootloader_key_left: bootloader_key_left { + compatible = "zmk,boot-magic-key"; + key-position = <0>; + jump-to-bootloader; + status = "disabled"; + }; + + bootloader_key_right: bootloader_key_right { + compatible = "zmk,boot-magic-key"; + key-position = <11>; + jump-to-bootloader; + status = "disabled"; + }; + ... +}; +``` + +**shield.keymap** + +```c +// Uncomment this block if using the 5-column layout +// / { +// chosen { +// zmk,matrix_transform = &five_column_transform; +// }; +// bootloader_key_right { +// key-position = <9>; +// }; +// }; +``` + +## Startup Timeout + +By default, the keyboard processes boot magic keys for 500 ms. You can change this timeout with `CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS` if it isn't reliably triggering, for example if you have some board-specific initialization code which takes a while. + +To change the value for a new board or shield, set this option in your `Kconfig.defconfig` file: + +``` +config ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS + default 1000 +``` + +You can also set it from your keyboard's `.conf` file in a user config repo: + +```ini +CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS=1000 +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 0b81c9c0b..5fa647fdb 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -1,11 +1,95 @@ module.exports = { - docs: [ - { - type: "category", - label: "Getting Started", - link: { - type: "doc", - id: "intro", + docs: { + "Getting Started": [ + "intro", + "hardware", + "faq", + "user-setup", + "customization", + "troubleshooting", + ], + Features: [ + "features/keymaps", + "features/bluetooth", + "features/boot-magic-key", + "features/combos", + "features/conditional-layers", + "features/debouncing", + "features/displays", + "features/encoders", + "features/underglow", + "features/backlight", + "features/battery", + "features/beta-testing", + ], + Behaviors: [ + "behaviors/key-press", + "behaviors/layers", + "behaviors/misc", + "behaviors/hold-tap", + "behaviors/mod-tap", + "behaviors/mod-morph", + "behaviors/macros", + "behaviors/key-toggle", + "behaviors/sticky-key", + "behaviors/sticky-layer", + "behaviors/tap-dance", + "behaviors/caps-word", + "behaviors/key-repeat", + "behaviors/sensor-rotate", + "behaviors/reset", + "behaviors/bluetooth", + "behaviors/outputs", + "behaviors/underglow", + "behaviors/backlight", + "behaviors/power", + ], + Codes: [ + "codes/index", + "codes/keyboard-keypad", + "codes/modifiers", + "codes/editing", + "codes/media", + "codes/applications", + "codes/input-assist", + "codes/power", + "codes/keymap-upgrader", + ], + Configuration: [ + "config/index", + "config/backlight", + "config/battery", + "config/behaviors", + "config/bluetooth", + "config/combos", + "config/displays", + "config/encoders", + "config/keymap", + "config/kscan", + "config/power", + "config/underglow", + "config/system", + ], + Development: [ + "development/clean-room", + "development/pre-commit", + "development/documentation", + "development/setup", + "development/build-flash", + "development/boards-shields-keymaps", + "development/posix-board", + "development/tests", + "development/usb-logging", + "development/ide-integration", + { + type: "category", + label: "Guides", + collapsed: false, + items: [ + "development/new-shield", + "development/hardware-metadata-files", + "development/new-behavior", + ], }, collapsed: false, items: [ From ace0613a210d15133f1c1b3a2f12e5acd1762dae Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Fri, 20 Jun 2025 00:01:23 -0400 Subject: [PATCH 04/10] refactor: Fix merge conflict --- app/include/zmk/ble.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h index 95f0bbfb1..3e734aa82 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -30,7 +30,6 @@ int zmk_ble_active_profile_index(void); int zmk_ble_profile_index(const bt_addr_le_t *addr); bt_addr_le_t *zmk_ble_profile_address(uint8_t index); -<<<<<<< HEAD bt_addr_le_t *zmk_ble_active_profile_addr(void); struct bt_conn *zmk_ble_active_profile_conn(void); @@ -41,12 +40,9 @@ bool zmk_ble_active_profile_is_open(void); bool zmk_ble_active_profile_is_connected(void); char *zmk_ble_active_profile_name(void); -int zmk_ble_unpair_all(void); +void zmk_ble_unpair_all(void); int zmk_ble_set_device_name(char *name); -======= -void zmk_ble_unpair_all(void); ->>>>>>> c072f755 (refactor: Add a settings reset function) #if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr); From f97e49f99f2daa64b9988ee98d79fc6b13430a91 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Thu, 22 Jan 2026 04:07:25 -0800 Subject: [PATCH 05/10] fix(bootmagic): Get stuff working on new zephyr --- app/Kconfig | 5 -- app/include/zmk/reset.h | 5 +- app/src/behaviors/behavior_reset.c | 17 +----- app/src/boot_magic_key.c | 14 +++++ app/src/reset.c | 19 +++++- docs/docs/config/system.md | 2 +- docs/sidebars.js | 98 +++--------------------------- 7 files changed, 44 insertions(+), 116 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 11a2697a3..2b5852cf7 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -691,11 +691,6 @@ config ZMK_KEYMAP_SENSORS_DEFAULT_TRIGGERS_PER_ROTATION endif # ZMK_KEYMAP_SENSORS -choice CBPRINTF_IMPLEMENTATION - default CBPRINTF_NANO - -endchoice - DT_COMPAT_ZMK_BOOT_MAGIC_KEY := zmk,boot-magic-key config ZMK_BOOT_MAGIC_KEY bool "Enable actions when keys are held at boot" diff --git a/app/include/zmk/reset.h b/app/include/zmk/reset.h index fad6db6a4..11a6d214b 100644 --- a/app/include/zmk/reset.h +++ b/app/include/zmk/reset.h @@ -11,9 +11,10 @@ /** * Reboot the system. - * @param type A ZMK_RESET_* value indicating how to reboot. + * @param type If CONFIG_RETENTION_BOOT_MODE is set: A BOOT_MODE_TYPE_* value indicating which type + * of reboot. Otherwise, A ZMK_RESET_* value indicating how to reboot. */ -FUNC_NORETURN void zmk_reset(int type); +void zmk_reset(int type); /** * Clear all persistent settings. diff --git a/app/src/behaviors/behavior_reset.c b/app/src/behaviors/behavior_reset.c index f9e4d50ad..6acf91e0f 100644 --- a/app/src/behaviors/behavior_reset.c +++ b/app/src/behaviors/behavior_reset.c @@ -36,24 +36,11 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); const struct behavior_reset_config *cfg = dev->config; -<<<<<<< HEAD #if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) - int ret = bootmode_set(cfg->boot_mode); - if (ret < 0) { - LOG_ERR("Failed to set the bootloader mode (%d)", ret); - return ZMK_BEHAVIOR_OPAQUE; - } - - sys_reboot(SYS_REBOOT_WARM); + zmk_reset(cfg->boot_mode); #else - // See - // https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/d6b28e66053eea467166f44875e3c7ec741cb471/src/main.c#L107 - sys_reboot(cfg->type); -#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ - -======= zmk_reset(cfg->type); ->>>>>>> 1d73fc26 (refactor: Move reset logic to a new function) +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ return ZMK_BEHAVIOR_OPAQUE; } diff --git a/app/src/boot_magic_key.c b/app/src/boot_magic_key.c index bc29873af..a4b430f4d 100644 --- a/app/src/boot_magic_key.c +++ b/app/src/boot_magic_key.c @@ -17,6 +17,12 @@ #include #include +#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) + +#include + +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ + LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); struct boot_key_config { @@ -51,11 +57,19 @@ static void trigger_boot_key(const struct boot_key_config *config) { if (config->jump_to_bootloader) { LOG_INF("Boot key: jumping to bootloader"); +#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) + zmk_reset(BOOT_MODE_TYPE_BOOTLOADER); +#else zmk_reset(ZMK_RESET_BOOTLOADER); +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ } else if (config->reset_settings) { // If resetting settings but not jumping to bootloader, we need to reboot // to ensure all subsystems are properly reset. +#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) + zmk_reset(BOOT_MODE_TYPE_NORMAL); +#else zmk_reset(ZMK_RESET_WARM); +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ } } diff --git a/app/src/reset.c b/app/src/reset.c index c0fff9871..dbdb52066 100644 --- a/app/src/reset.c +++ b/app/src/reset.c @@ -13,13 +13,27 @@ #endif #include +#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) + +#include + +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ + LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); // AdaFruit nrf52 Bootloader Specific. See // https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/d6b28e66053eea467166f44875e3c7ec741cb471/src/main.c#L107 #define ADAFRUIT_MAGIC_UF2 0x57 -FUNC_NORETURN void zmk_reset(int type) { +void zmk_reset(int type) { +#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) + int ret = bootmode_set(type); + if (ret < 0) { + LOG_ERR("Failed to set the bootloader mode (%d)", ret); + } else { + sys_reboot(SYS_REBOOT_WARM); + } +#else switch (type) { case ZMK_RESET_WARM: sys_reboot(SYS_REBOOT_WARM); @@ -38,6 +52,7 @@ FUNC_NORETURN void zmk_reset(int type) { LOG_ERR("Unknown reset type %d", type); break; } +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ } void zmk_reset_settings(void) { @@ -45,4 +60,4 @@ void zmk_reset_settings(void) { zmk_ble_unpair_all(); #endif // TODO: clear settings for all subsystems. -} \ No newline at end of file +} diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index 631be86eb..cf55d06fb 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -14,7 +14,7 @@ Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/ ### General | Config | Type | Description | Default | -|----------------------------------------|--------|---------------------------------------------------------------------------------------|---------| +| -------------------------------------- | ------ | ------------------------------------------------------------------------------------- | ------- | | `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | | `CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS` | int | Milliseconds to watch for [boot magic keys](../features/boot-magic-key.md) at startup | 500 | | `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | diff --git a/docs/sidebars.js b/docs/sidebars.js index 5fa647fdb..0b81c9c0b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -1,95 +1,11 @@ module.exports = { - docs: { - "Getting Started": [ - "intro", - "hardware", - "faq", - "user-setup", - "customization", - "troubleshooting", - ], - Features: [ - "features/keymaps", - "features/bluetooth", - "features/boot-magic-key", - "features/combos", - "features/conditional-layers", - "features/debouncing", - "features/displays", - "features/encoders", - "features/underglow", - "features/backlight", - "features/battery", - "features/beta-testing", - ], - Behaviors: [ - "behaviors/key-press", - "behaviors/layers", - "behaviors/misc", - "behaviors/hold-tap", - "behaviors/mod-tap", - "behaviors/mod-morph", - "behaviors/macros", - "behaviors/key-toggle", - "behaviors/sticky-key", - "behaviors/sticky-layer", - "behaviors/tap-dance", - "behaviors/caps-word", - "behaviors/key-repeat", - "behaviors/sensor-rotate", - "behaviors/reset", - "behaviors/bluetooth", - "behaviors/outputs", - "behaviors/underglow", - "behaviors/backlight", - "behaviors/power", - ], - Codes: [ - "codes/index", - "codes/keyboard-keypad", - "codes/modifiers", - "codes/editing", - "codes/media", - "codes/applications", - "codes/input-assist", - "codes/power", - "codes/keymap-upgrader", - ], - Configuration: [ - "config/index", - "config/backlight", - "config/battery", - "config/behaviors", - "config/bluetooth", - "config/combos", - "config/displays", - "config/encoders", - "config/keymap", - "config/kscan", - "config/power", - "config/underglow", - "config/system", - ], - Development: [ - "development/clean-room", - "development/pre-commit", - "development/documentation", - "development/setup", - "development/build-flash", - "development/boards-shields-keymaps", - "development/posix-board", - "development/tests", - "development/usb-logging", - "development/ide-integration", - { - type: "category", - label: "Guides", - collapsed: false, - items: [ - "development/new-shield", - "development/hardware-metadata-files", - "development/new-behavior", - ], + docs: [ + { + type: "category", + label: "Getting Started", + link: { + type: "doc", + id: "intro", }, collapsed: false, items: [ From f88c8bb9e7fec29b262171db9ae7a6de1542c825 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Thu, 22 Jan 2026 22:15:39 -0800 Subject: [PATCH 06/10] style: address comments from previous PR --- app/Kconfig | 4 ++-- app/dts/bindings/zmk,boot-magic-key.yaml | 2 +- app/src/ble.c | 1 - app/src/boot_magic_key.c | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 2b5852cf7..2278d3f4d 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -691,10 +691,10 @@ config ZMK_KEYMAP_SENSORS_DEFAULT_TRIGGERS_PER_ROTATION endif # ZMK_KEYMAP_SENSORS -DT_COMPAT_ZMK_BOOT_MAGIC_KEY := zmk,boot-magic-key config ZMK_BOOT_MAGIC_KEY bool "Enable actions when keys are held at boot" - default $(dt_compat_enabled,$(DT_COMPAT_ZMK_BOOT_MAGIC_KEY)) + default y + depends on DT_HAS_ZMK_BOOT_MAGIC_KEY_ENABLED config ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS int "Milliseconds to wait for a boot magic key at startup" diff --git a/app/dts/bindings/zmk,boot-magic-key.yaml b/app/dts/bindings/zmk,boot-magic-key.yaml index 3629c0e8a..89a24c99a 100644 --- a/app/dts/bindings/zmk,boot-magic-key.yaml +++ b/app/dts/bindings/zmk,boot-magic-key.yaml @@ -5,7 +5,7 @@ description: | Triggers one or more actions if a specific key is held while the keyboard boots. This is typically used for recovering a keyboard in cases such as &bootloader being missing from the keymap or a split peripheral which isn't connected to - the central, and therefore can't process th ekeymap. + the central, and therefore can't process the keymap. compatible: "zmk,boot-magic-key" diff --git a/app/src/ble.c b/app/src/ble.c index ea25d58eb..b94717e3a 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -39,7 +39,6 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY) #include -#include "ble.h" #define PASSKEY_DIGITS 6 diff --git a/app/src/boot_magic_key.c b/app/src/boot_magic_key.c index a4b430f4d..d20843b48 100644 --- a/app/src/boot_magic_key.c +++ b/app/src/boot_magic_key.c @@ -26,7 +26,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); struct boot_key_config { - int key_position; + uint16_t key_position; bool jump_to_bootloader; bool reset_settings; }; From 8b1eb38bedbd455be07de70c9762dcd59efb4343 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Sun, 1 Feb 2026 16:10:38 -0600 Subject: [PATCH 07/10] feat: Change Boot Magic Key -> Combo --- app/CMakeLists.txt | 2 +- app/Kconfig | 8 +-- ...gic-key.yaml => zmk,boot-magic-combo.yaml} | 12 ++-- .../{boot_magic_key.c => boot_magic_combo.c} | 55 ++++++++++++++---- docs/docs/config/system.md | 2 +- ...{boot-magic-key.md => boot-magic-combo.md} | 58 +++++++++---------- 6 files changed, 85 insertions(+), 52 deletions(-) rename app/dts/bindings/{zmk,boot-magic-key.yaml => zmk,boot-magic-combo.yaml} (63%) rename app/src/{boot_magic_key.c => boot_magic_combo.c} (53%) rename docs/docs/features/{boot-magic-key.md => boot-magic-combo.md} (54%) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 848525d34..730c2bdf7 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -105,7 +105,7 @@ target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) -target_sources_ifdef(CONFIG_ZMK_BOOT_MAGIC_KEY app PRIVATE src/boot_magic_key.c) +target_sources_ifdef(CONFIG_ZMK_BOOT_MAGIC_COMBO app PRIVATE src/boot_magic_combo.c) target_sources(app PRIVATE src/main.c) add_subdirectory(src/display/) diff --git a/app/Kconfig b/app/Kconfig index 2278d3f4d..7fc29725a 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -691,13 +691,13 @@ config ZMK_KEYMAP_SENSORS_DEFAULT_TRIGGERS_PER_ROTATION endif # ZMK_KEYMAP_SENSORS -config ZMK_BOOT_MAGIC_KEY +config ZMK_BOOT_MAGIC_COMBO bool "Enable actions when keys are held at boot" default y - depends on DT_HAS_ZMK_BOOT_MAGIC_KEY_ENABLED + depends on DT_HAS_ZMK_BOOT_MAGIC_COMBO_ENABLED -config ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS - int "Milliseconds to wait for a boot magic key at startup" +config ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS + int "Milliseconds to wait for a boot magic combo at startup" default 500 module = ZMK diff --git a/app/dts/bindings/zmk,boot-magic-key.yaml b/app/dts/bindings/zmk,boot-magic-combo.yaml similarity index 63% rename from app/dts/bindings/zmk,boot-magic-key.yaml rename to app/dts/bindings/zmk,boot-magic-combo.yaml index 89a24c99a..434edafcb 100644 --- a/app/dts/bindings/zmk,boot-magic-key.yaml +++ b/app/dts/bindings/zmk,boot-magic-combo.yaml @@ -2,18 +2,18 @@ # SPDX-License-Identifier: MIT description: | - Triggers one or more actions if a specific key is held while the keyboard boots. + Triggers one or more actions if a combination of keys is held while the keyboard boots. This is typically used for recovering a keyboard in cases such as &bootloader being missing from the keymap or a split peripheral which isn't connected to the central, and therefore can't process the keymap. -compatible: "zmk,boot-magic-key" +compatible: "zmk,boot-magic-combo" properties: - key-position: - type: int - default: 0 - description: Zero-based index of the key which triggers the action(s). + combo-positions: + type: array + required: true + description: Zero-based indices of the keys which must be simultaneously pressed to trigger the action(s). # Boot magic actions: jump-to-bootloader: type: boolean diff --git a/app/src/boot_magic_key.c b/app/src/boot_magic_combo.c similarity index 53% rename from app/src/boot_magic_key.c rename to app/src/boot_magic_combo.c index d20843b48..81e925fcf 100644 --- a/app/src/boot_magic_key.c +++ b/app/src/boot_magic_combo.c @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT */ -#define DT_DRV_COMPAT zmk_boot_magic_key +#define DT_DRV_COMPAT zmk_boot_magic_combo #include #include @@ -25,25 +25,38 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define _COMBO_LEN(inst) uint8_t _CONCAT(_len_, inst)[DT_INST_PROP_LEN(inst, combo_positions)]; +#define MAX_BOOT_COMBO_LEN sizeof(union {DT_INST_FOREACH_STATUS_OKAY(_COMBO_LEN)}) + struct boot_key_config { - uint16_t key_position; + const uint16_t *combo_positions; + uint8_t combo_positions_len; bool jump_to_bootloader; bool reset_settings; }; +#define BOOT_KEY_COMBO_POSITIONS(n) \ + static const uint16_t boot_key_combo_positions_##n[] = DT_INST_PROP(n, combo_positions); + #define BOOT_KEY_CONFIG(n) \ + BOOT_KEY_COMBO_POSITIONS(n) \ { \ - .key_position = DT_INST_PROP_OR(n, key_position, 0), \ + .combo_positions = boot_key_combo_positions_##n, \ + .combo_positions_len = DT_INST_PROP_LEN(n, combo_positions), \ .jump_to_bootloader = DT_INST_PROP_OR(n, jump_to_bootloader, false), \ .reset_settings = DT_INST_PROP_OR(n, reset_settings, false), \ }, -static const struct boot_key_config boot_keys[] = {DT_INST_FOREACH_STATUS_OKAY(BOOT_KEY_CONFIG)}; +static struct boot_key_config boot_keys[] = {DT_INST_FOREACH_STATUS_OKAY(BOOT_KEY_CONFIG)}; + +static bool boot_key_states[ARRAY_SIZE(boot_keys)][MAX_BOOT_COMBO_LEN] = {}; static int64_t timeout_uptime; static int timeout_init(const struct device *device) { - timeout_uptime = k_uptime_get() + CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS; + timeout_uptime = k_uptime_get() + CONFIG_ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS; return 0; } @@ -79,10 +92,28 @@ static int event_listener(const zmk_event_t *eh) { } const struct zmk_position_state_changed *ev = as_zmk_position_state_changed(eh); - if (ev && ev->state) { - for (int i = 0; i < ARRAY_SIZE(boot_keys); i++) { - if (ev->position == boot_keys[i].key_position) { - trigger_boot_key(&boot_keys[i]); + if (!ev) { + return ZMK_EV_EVENT_BUBBLE; + } + + for (int i = 0; i < ARRAY_SIZE(boot_keys); i++) { + const struct boot_key_config *config = &boot_keys[i]; + for (int j = 0; j < config->combo_positions_len; j++) { + if (ev->position == config->combo_positions[j]) { + boot_key_states[i][j] = ev->state; + if (ev->state) { + bool all_keys_pressed = true; + for (int k = 0; k < config->combo_positions_len; k++) { + if (!boot_key_states[i][k]) { + all_keys_pressed = false; + break; + } + } + if (all_keys_pressed) { + trigger_boot_key(config); + } + } + break; } } } @@ -90,5 +121,7 @@ static int event_listener(const zmk_event_t *eh) { return ZMK_EV_EVENT_BUBBLE; } -ZMK_LISTENER(boot_magic_key, event_listener); -ZMK_SUBSCRIPTION(boot_magic_key, zmk_position_state_changed); +ZMK_LISTENER(boot_magic_combo, event_listener); +ZMK_SUBSCRIPTION(boot_magic_combo, zmk_position_state_changed); + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index cf55d06fb..0fb922b2e 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -16,7 +16,7 @@ Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/ | Config | Type | Description | Default | | -------------------------------------- | ------ | ------------------------------------------------------------------------------------- | ------- | | `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | -| `CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS` | int | Milliseconds to watch for [boot magic keys](../features/boot-magic-key.md) at startup | 500 | +| `CONFIG_ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS` | int | Milliseconds to watch for [boot magic combos](../features/boot-magic-combo.md) at startup | 500 | | `CONFIG_ZMK_WPM` | bool | Enable calculating words per minute | n | | `CONFIG_HEAP_MEM_POOL_SIZE` | int | Size of the heap memory pool | 8192 | diff --git a/docs/docs/features/boot-magic-key.md b/docs/docs/features/boot-magic-combo.md similarity index 54% rename from docs/docs/features/boot-magic-key.md rename to docs/docs/features/boot-magic-combo.md index d51c2b340..e8682cc30 100644 --- a/docs/docs/features/boot-magic-key.md +++ b/docs/docs/features/boot-magic-combo.md @@ -1,13 +1,13 @@ --- -title: Boot Magic Key -sidebar_label: Boot Magic Key +title: Boot Magic Combo +sidebar_label: Boot Magic Combo --- -A boot magic key performs one or more actions if a specific key is held while powering on the keyboard. This is useful for recovering a keyboard which doesn't have a physical reset button. It also works on the peripheral side of a split keyboard, even when it isn't connected to the central side. +A boot magic combo performs one or more actions if a combination of keys is held while powering on the keyboard. This is useful for recovering a keyboard which doesn't have a physical reset button. It also works on the peripheral side of a split keyboard, even when it isn't connected to the central side. ## Magic Keys -To define a boot magic key on a new board or shield, add a `zmk,boot-magic-key` node to your board's `.dts` file or shield's `.overlay` file and select which key will trigger it with the `key-position` property. +To define a boot magic combo on a new board or shield, add a `zmk,boot-magic-combo` node to your board's `.dts` file or shield's `.overlay` file and select which keys will trigger it with the `combo-positions` property. All keys in the combo must be held simultaneously to trigger the action. You can also enable the feature for any keyboard by adding it to your `.keymap` file. @@ -15,8 +15,8 @@ You can also enable the feature for any keyboard by adding it to your `.keymap` / { ... bootloader_key: bootloader_key { - compatible = "zmk,boot-magic-key"; - key-position = <0>; + compatible = "zmk,boot-magic-combo"; + combo-positions = <0 1>; }; ... }; @@ -26,21 +26,21 @@ You can also enable the feature for any keyboard by adding it to your `.keymap` 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. -::: +The `combo-positions` property accepts an array of key positions. All keys in the array must be held simultaneously during boot to trigger the action. -If `key-position` is omitted, it will trigger for the key in position `0`. +::: Next, you should add properties to determine what the magic key will do: ### Jump to Bootloader -If a boot magic key has a `jump-to-bootloader` property, it will reboot to the bootloader: +If a boot magic combo has a `jump-to-bootloader` property, it will reboot to the bootloader: ```c / { ... bootloader_key: bootloader_key { - compatible = "zmk,boot-magic-key"; + compatible = "zmk,boot-magic-combo"; ... jump-to-bootloader; }; @@ -50,13 +50,13 @@ If a boot magic key has a `jump-to-bootloader` property, it will reboot to the b ### Reset Settings -If a boot magic key has a `reset-settings` property, it will reset persistent settings and then reboot: +If a boot magic combo has a `reset-settings` property, it will reset persistent settings and then reboot: ```c / { ... reset_settings_key: reset_settings_key { - compatible = "zmk,boot-magic-key"; + compatible = "zmk,boot-magic-combo"; ... reset-settings; }; @@ -78,7 +78,7 @@ Currently this action _only_ clears BLE bonds. It will be updated to reset all s ## Multiple Actions -If you want a single boot magic key to perform multiple actions, simply add properties for each action to the same `zmk,boot-magic-key` node. The order of the properties does not matter. +If you want a single boot magic combo to perform multiple actions, simply add properties for each action to the same `zmk,boot-magic-combo` node. The order of the properties does not matter. For example, to make a key that resets settings and then reboots to the bootloader, add both `reset-settings` and `jump-to-bootloader`: @@ -86,7 +86,7 @@ For example, to make a key that resets settings and then reboots to the bootload / { ... recovery_key: recovery_key { - compatible = "zmk,boot-magic-key"; + compatible = "zmk,boot-magic-combo"; jump-to-bootloader; reset-settings; }; @@ -96,13 +96,13 @@ For example, to make a key that resets settings and then reboots to the bootload :::info -You may define multiple `zmk,boot-magic-key` nodes for different keys, but note that if you hold multiple keys at boot, they will be run in an arbitrary order. If one of them reboots the keyboard, the rest of the keys will not run. +You may define multiple `zmk,boot-magic-combo` nodes for different keys, but note that if you hold multiple keys at boot, they will be run in an arbitrary order. If one of them reboots the keyboard, the rest of the keys will not run. ::: ## Split Keyboards -For split keyboards, you can define multiple boot magic keys and then only enable the correct key(s) for each side. For example, if key 0 is the top-left key on the left side and key 11 is the top-right key on the right side, you could use: +For split keyboards, you can define multiple boot magic combos and then only enable the correct key(s) for each side. For example, if key 0 is the top-left key on the left side and key 11 is the top-right key on the right side, you could use: **shield.dtsi** @@ -110,15 +110,15 @@ For split keyboards, you can define multiple boot magic keys and then only enabl / { ... bootloader_key_left: bootloader_key_left { - compatible = "zmk,boot-magic-key"; - key-position = <0>; + compatible = "zmk,boot-magic-combo"; + combo-positions = <0>; jump-to-bootloader; status = "disabled"; }; bootloader_key_right: bootloader_key_right { - compatible = "zmk,boot-magic-key"; - key-position = <11>; + compatible = "zmk,boot-magic-combo"; + combo-positions = <11>; jump-to-bootloader; status = "disabled"; }; @@ -150,7 +150,7 @@ For split keyboards, you can define multiple boot magic keys and then only enabl Key positions are affected by the [matrix transform](../config/kscan.md#matrix-transform), so if your keyboard has multiple transforms for alternate layouts, you may need to adjust positions according to the user's selected transform. There is no automatic way to do this, but one way to simplify things for users is to add a block of commented out code to the keymap which selects the transform and updates the key positions to match if uncommented. -For example, consider a split keyboard which has 6 columns per side by default but supports a 5-column layout, and assume you want the top-left key on the left side and the top-right key on the right side to be boot magic keys. The top-left key will be position 0 regardless of layout, but the top-right key will be position 11 by default and position 9 in the 5-column layout. +For example, consider a split keyboard which has 6 columns per side by default but supports a 5-column layout, and assume you want the top-left key on the left side and the top-right key on the right side to be boot magic combos. The top-left key will be position 0 regardless of layout, but the top-right key will be position 11 by default and position 9 in the 5-column layout. **shield.dtsi** @@ -161,15 +161,15 @@ For example, consider a split keyboard which has 6 columns per side by default b }; bootloader_key_left: bootloader_key_left { - compatible = "zmk,boot-magic-key"; - key-position = <0>; + compatible = "zmk,boot-magic-combo"; + combo-positions = <0>; jump-to-bootloader; status = "disabled"; }; bootloader_key_right: bootloader_key_right { - compatible = "zmk,boot-magic-key"; - key-position = <11>; + compatible = "zmk,boot-magic-combo"; + combo-positions = <11>; jump-to-bootloader; status = "disabled"; }; @@ -186,24 +186,24 @@ For example, consider a split keyboard which has 6 columns per side by default b // zmk,matrix_transform = &five_column_transform; // }; // bootloader_key_right { -// key-position = <9>; +// combo-positions = <9>; // }; // }; ``` ## Startup Timeout -By default, the keyboard processes boot magic keys for 500 ms. You can change this timeout with `CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS` if it isn't reliably triggering, for example if you have some board-specific initialization code which takes a while. +By default, the keyboard processes boot magic combos for 500 ms. You can change this timeout with `CONFIG_ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS` if it isn't reliably triggering, for example if you have some board-specific initialization code which takes a while. To change the value for a new board or shield, set this option in your `Kconfig.defconfig` file: ``` -config ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS +config ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS default 1000 ``` You can also set it from your keyboard's `.conf` file in a user config repo: ```ini -CONFIG_ZMK_BOOT_MAGIC_KEY_TIMEOUT_MS=1000 +CONFIG_ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS=1000 ``` From f9eb39d56135834b9812f9df015e7a0b965574f7 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Sun, 1 Feb 2026 16:19:14 -0600 Subject: [PATCH 08/10] refactor: move boot_magic_combo to boot folder --- app/CMakeLists.txt | 1 - app/src/boot/CMakeLists.txt | 1 + app/src/{ => boot}/boot_magic_combo.c | 5 ++--- 3 files changed, 3 insertions(+), 4 deletions(-) rename app/src/{ => boot}/boot_magic_combo.c (95%) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 730c2bdf7..2eee25a3f 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -105,7 +105,6 @@ target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) -target_sources_ifdef(CONFIG_ZMK_BOOT_MAGIC_COMBO app PRIVATE src/boot_magic_combo.c) target_sources(app PRIVATE src/main.c) add_subdirectory(src/display/) diff --git a/app/src/boot/CMakeLists.txt b/app/src/boot/CMakeLists.txt index 264c1aa7d..d8c4e57ca 100644 --- a/app/src/boot/CMakeLists.txt +++ b/app/src/boot/CMakeLists.txt @@ -2,3 +2,4 @@ target_sources_ifdef(CONFIG_ZMK_BOOTMODE_TO_MAGIC_VALUE_MAPPER app PRIVATE bootmode_to_magic_mapper.c) target_sources_ifdef(CONFIG_ZMK_DBL_TAP_BOOTLOADER app PRIVATE dbl_tap_bootloader.c) target_sources_ifdef(CONFIG_ZMK_BOOT_STM32_ENFORCE_NBOOT_SEL app PRIVATE stm32_enforce_nboot_sel.c) +target_sources_ifdef(CONFIG_ZMK_BOOT_MAGIC_COMBO app PRIVATE boot_magic_combo.c) \ No newline at end of file diff --git a/app/src/boot_magic_combo.c b/app/src/boot/boot_magic_combo.c similarity index 95% rename from app/src/boot_magic_combo.c rename to app/src/boot/boot_magic_combo.c index 81e925fcf..46f8f0919 100644 --- a/app/src/boot_magic_combo.c +++ b/app/src/boot/boot_magic_combo.c @@ -41,9 +41,8 @@ struct boot_key_config { static const uint16_t boot_key_combo_positions_##n[] = DT_INST_PROP(n, combo_positions); #define BOOT_KEY_CONFIG(n) \ - BOOT_KEY_COMBO_POSITIONS(n) \ - { \ - .combo_positions = boot_key_combo_positions_##n, \ + BOOT_KEY_COMBO_POSITIONS(n){ \ + .combo_positions = boot_key_combo_positions_##n, \ .combo_positions_len = DT_INST_PROP_LEN(n, combo_positions), \ .jump_to_bootloader = DT_INST_PROP_OR(n, jump_to_bootloader, false), \ .reset_settings = DT_INST_PROP_OR(n, reset_settings, false), \ From 44de7ccd51bc22dc7f5471d4f3535d6a5ef83432 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Sun, 1 Feb 2026 16:20:52 -0600 Subject: [PATCH 09/10] style: Update copyright dates --- app/dts/bindings/zmk,boot-magic-combo.yaml | 2 +- app/src/boot/boot_magic_combo.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/dts/bindings/zmk,boot-magic-combo.yaml b/app/dts/bindings/zmk,boot-magic-combo.yaml index 434edafcb..23daccac6 100644 --- a/app/dts/bindings/zmk,boot-magic-combo.yaml +++ b/app/dts/bindings/zmk,boot-magic-combo.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2023, The ZMK Contributors +# Copyright (c) 2026, The ZMK Contributors # SPDX-License-Identifier: MIT description: | diff --git a/app/src/boot/boot_magic_combo.c b/app/src/boot/boot_magic_combo.c index 46f8f0919..730361e63 100644 --- a/app/src/boot/boot_magic_combo.c +++ b/app/src/boot/boot_magic_combo.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 The ZMK Contributors + * Copyright (c) 2026 The ZMK Contributors * * SPDX-License-Identifier: MIT */ From 7339326e2bedc31e3c0399409033c796c35a0b64 Mon Sep 17 00:00:00 2001 From: Ryan Adolf Date: Sun, 1 Feb 2026 16:24:24 -0600 Subject: [PATCH 10/10] style: Unify BLE unpair loop --- app/src/ble.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/ble.c b/app/src/ble.c index b94717e3a..1c55c9b1f 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -345,24 +345,19 @@ void zmk_ble_unpair_all(void) { } for (int i = 0; i < 8; i++) { - char setting_name[15]; + char setting_name[32]; sprintf(setting_name, "ble/profiles/%d", i); int err = settings_delete(setting_name); if (err) { - LOG_ERR("Failed to delete setting: %d", err); + LOG_ERR("Failed to delete profile setting: %d", err); } - } - // Hardcoding a reasonable hardcoded value of peripheral addresses - // to clear so we properly clear a split central as well. - for (int i = 0; i < 8; i++) { - char setting_name[32]; sprintf(setting_name, "ble/peripheral_addresses/%d", i); - int err = settings_delete(setting_name); + err = settings_delete(setting_name); if (err) { - LOG_ERR("Failed to delete setting: %d", err); + LOG_ERR("Failed to delete peripheral setting: %d", err); } } }