diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index c41153cb17c..46b8b5c1b61 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/Kconfig b/app/Kconfig index 8c0675e0d68..a242e61a3bf 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -704,6 +704,15 @@ config ZMK_KEYMAP_SENSORS_DEFAULT_TRIGGERS_PER_ROTATION endif # ZMK_KEYMAP_SENSORS +config ZMK_BOOT_MAGIC_COMBO + bool "Enable actions when keys are held at boot" + default y + depends on DT_HAS_ZMK_BOOT_MAGIC_COMBO_ENABLED + +config ZMK_BOOT_MAGIC_COMBO_TIMEOUT_MS + int "Milliseconds to wait for a boot magic combo at startup" + default 500 + module = ZMK module-str = zmk source "subsys/logging/Kconfig.template.log_config" diff --git a/app/dts/behaviors/reset.dtsi b/app/dts/behaviors/reset.dtsi index 1b29f9d19aa..a6adadeac4e 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/dts/bindings/zmk,boot-magic-combo.yaml b/app/dts/bindings/zmk,boot-magic-combo.yaml new file mode 100644 index 00000000000..23daccac6eb --- /dev/null +++ b/app/dts/bindings/zmk,boot-magic-combo.yaml @@ -0,0 +1,23 @@ +# Copyright (c) 2026, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: | + 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-combo" + +properties: + 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 + description: Reboots into the bootloader. + reset-settings: + type: boolean + description: Clears settings and reboots. diff --git a/app/dts/bindings/zmk,physical-layout.yaml b/app/dts/bindings/zmk,physical-layout.yaml index 7ed4cdc3d2d..9d9d628a6ed 100644 --- a/app/dts/bindings/zmk,physical-layout.yaml +++ b/app/dts/bindings/zmk,physical-layout.yaml @@ -27,3 +27,6 @@ properties: keys: type: phandle-array description: Array of key physical attributes. + boot-magic-combos: + type: phandles + description: List of combos specific to this layout that enter bootloader or reset settings when held during boot. diff --git a/app/include/dt-bindings/zmk/reset.h b/app/include/dt-bindings/zmk/reset.h index 2b3d8760d5c..63f3428e239 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/ble.h b/app/include/zmk/ble.h index 92b2107d88b..3e734aa8296 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -40,7 +40,7 @@ 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); diff --git a/app/include/zmk/boot_magic.h b/app/include/zmk/boot_magic.h new file mode 100644 index 00000000000..55839c04116 --- /dev/null +++ b/app/include/zmk/boot_magic.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2026 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +struct zmk_boot_magic_combo_config { + const uint16_t *combo_positions; + uint8_t combo_positions_len; + bool jump_to_bootloader; + bool reset_settings; + bool *state; +}; + +#define ZMK_BOOT_MAGIC_COMBO_CONFIG_NAME(node_id) _CONCAT(zmk_boot_magic_combo_config_, node_id) + +#define ZMK_BOOT_MAGIC_COMBO_CONFIG_DECLARE(node_id) \ + extern const struct zmk_boot_magic_combo_config ZMK_BOOT_MAGIC_COMBO_CONFIG_NAME(node_id) diff --git a/app/include/zmk/physical_layouts.h b/app/include/zmk/physical_layouts.h index e28c194fec5..cb9e2432a8d 100644 --- a/app/include/zmk/physical_layouts.h +++ b/app/include/zmk/physical_layouts.h @@ -9,6 +9,7 @@ #include #include #include +#include struct zmk_physical_layout_selection_changed { uint8_t selection; @@ -39,6 +40,9 @@ struct zmk_physical_layout { const struct zmk_key_physical_attrs *keys; size_t keys_len; + + const struct zmk_boot_magic_combo_config *const *boot_magic_combos; + size_t boot_magic_combos_len; }; #define ZMK_PHYS_LAYOUTS_FOREACH(_ref) STRUCT_SECTION_FOREACH(zmk_physical_layout, _ref) @@ -62,4 +66,4 @@ int zmk_physical_layouts_get_position_map(uint8_t source, uint8_t dest, size_t m * @retval a negative errno value in the case of errors * @retval a positive length of the position map array that map is updated to point to. */ -int zmk_physical_layouts_get_selected_to_stock_position_map(uint32_t const **map); \ No newline at end of file +int zmk_physical_layouts_get_selected_to_stock_position_map(uint32_t const **map); diff --git a/app/include/zmk/reset.h b/app/include/zmk/reset.h new file mode 100644 index 00000000000..11a6d214bf4 --- /dev/null +++ b/app/include/zmk/reset.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +/** + * Reboot the system. + * @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. + */ +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/behaviors/behavior_reset.c b/app/src/behaviors/behavior_reset.c index 67f33aaedb2..6acf91e0f84 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) @@ -37,19 +37,10 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, const struct behavior_reset_config *cfg = dev->config; #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); + zmk_reset(cfg->type); #endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ - return ZMK_BEHAVIOR_OPAQUE; } diff --git a/app/src/ble.c b/app/src/ble.c index 51b172b9f5f..1c55c9b1f1d 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -336,6 +336,32 @@ 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[32]; + sprintf(setting_name, "ble/profiles/%d", i); + + int err = settings_delete(setting_name); + if (err) { + LOG_ERR("Failed to delete profile setting: %d", err); + } + + sprintf(setting_name, "ble/peripheral_addresses/%d", i); + + err = settings_delete(setting_name); + if (err) { + LOG_ERR("Failed to delete peripheral 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 +718,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/boot/CMakeLists.txt b/app/src/boot/CMakeLists.txt index 264c1aa7d25..d8c4e57caac 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/boot_magic_combo.c b/app/src/boot/boot_magic_combo.c new file mode 100644 index 00000000000..2ec88d5795d --- /dev/null +++ b/app/src/boot/boot_magic_combo.c @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_boot_magic_combo + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#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); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define BOOT_KEY_CONFIG(n) \ + static const uint16_t boot_key_combo_positions_##n[] = DT_INST_PROP(n, combo_positions); \ + static bool boot_key_state_##n[DT_INST_PROP_LEN(n, combo_positions)]; \ + const struct zmk_boot_magic_combo_config ZMK_BOOT_MAGIC_COMBO_CONFIG_NAME(DT_DRV_INST(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), \ + .state = boot_key_state_##n, \ + }; + +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_COMBO_TIMEOUT_MS; + return 0; +} + +SYS_INIT(timeout_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); + +static void trigger_boot_key(const struct zmk_boot_magic_combo_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"); +#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) */ + } +} + +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); + int selected = zmk_physical_layouts_get_selected(); + + if (!ev || selected < 0) { + return ZMK_EV_EVENT_BUBBLE; + } + + const struct zmk_physical_layout *const *layouts; + zmk_physical_layouts_get_list(&layouts); + const struct zmk_physical_layout *active = layouts[selected]; + + for (int i = 0; i < active->boot_magic_combos_len; i++) { + const struct zmk_boot_magic_combo_config *config = active->boot_magic_combos[i]; + for (int j = 0; j < config->combo_positions_len; j++) { + if (ev->position == config->combo_positions[j]) { + config->state[j] = ev->state; + if (ev->state) { + bool all_keys_pressed = true; + for (int k = 0; k < config->combo_positions_len; k++) { + if (!config->state[k]) { + all_keys_pressed = false; + break; + } + } + if (all_keys_pressed) { + trigger_boot_key(config); + } + } + break; + } + } + } + + return ZMK_EV_EVENT_BUBBLE; +} + +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/app/src/physical_layouts.c b/app/src/physical_layouts.c index 31a8ac05741..b84b935bed2 100644 --- a/app/src/physical_layouts.c +++ b/app/src/physical_layouts.c @@ -23,6 +23,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include ZMK_EVENT_IMPL(zmk_physical_layout_selection_changed); @@ -68,6 +69,12 @@ BUILD_ASSERT( DEVICE_DT_GET(COND_CODE_1(DT_INST_PROP_LEN(n, input), (DT_INST_PHANDLE(n, input)), \ (DT_CHOSEN(zmk_matrix_input)))) +#define ZMK_BOOT_MAGIC_COMBO_DECLARE(n, prop, idx) \ + ZMK_BOOT_MAGIC_COMBO_CONFIG_DECLARE(DT_PHANDLE_BY_IDX(n, prop, idx)); + +#define ZMK_BOOT_MAGIC_COMBO_REF(n, prop, idx) \ + &ZMK_BOOT_MAGIC_COMBO_CONFIG_NAME(DT_PHANDLE_BY_IDX(n, prop, idx)), + #define ZMK_LAYOUT_INST(n) \ BUILD_ASSERT(!IS_ENABLED(CONFIG_ZMK_STUDIO) || DT_INST_NODE_HAS_PROP(n, keys), \ "ZMK Studio requires physical layouts with key positions. See " \ @@ -75,12 +82,22 @@ BUILD_ASSERT( static const struct zmk_key_physical_attrs _CONCAT(_zmk_physical_layout_keys_, \ n)[DT_INST_PROP_LEN_OR(n, keys, 0)] = { \ LISTIFY(DT_INST_PROP_LEN_OR(n, keys, 0), ZKPA_INIT, (, ), n)}; \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, boot_magic_combos), \ + (DT_INST_FOREACH_PROP_ELEM(n, boot_magic_combos, ZMK_BOOT_MAGIC_COMBO_DECLARE)), \ + ()) \ + static const struct zmk_boot_magic_combo_config *const _CONCAT( \ + _zmk_physical_layout_boot_magic_combos_, n)[] = { \ + COND_CODE_1( \ + DT_INST_NODE_HAS_PROP(n, boot_magic_combos), \ + (DT_INST_FOREACH_PROP_ELEM(n, boot_magic_combos, ZMK_BOOT_MAGIC_COMBO_REF)), )}; \ ZMK_MATRIX_TRANSFORM_EXTERN(DT_INST_PHANDLE(n, transform)); \ static const struct zmk_physical_layout _CONCAT(_zmk_physical_layout_, DT_DRV_INST(n)) = { \ .display_name = DT_INST_PROP_OR(n, display_name, "Layout #" #n), \ .matrix_transform = ZMK_MATRIX_TRANSFORM_T_FOR_NODE(DT_INST_PHANDLE(n, transform)), \ .keys = _CONCAT(_zmk_physical_layout_keys_, n), \ .keys_len = DT_INST_PROP_LEN_OR(n, keys, 0), \ + .boot_magic_combos = _CONCAT(_zmk_physical_layout_boot_magic_combos_, n), \ + .boot_magic_combos_len = DT_INST_PROP_LEN_OR(n, boot_magic_combos, 0), \ COND_CODE_1(UTIL_AND(MATRIX_INPUT_SUPPORT, DT_INST_PROP_LEN(n, input)), \ (.input = INPUT_FOR_INST(n)), ()) \ COND_CODE_1(UTIL_OR(DT_HAS_CHOSEN(zmk_kscan), DT_INST_PROP_LEN(n, kscan)), \ @@ -154,6 +171,8 @@ ZMK_MATRIX_TRANSFORM_EXTERN(DT_CHOSEN(zmk_matrix_transform)); static const struct zmk_physical_layout _CONCAT(_zmk_physical_layout_, chosen) = { .display_name = "Default", .matrix_transform = ZMK_MATRIX_TRANSFORM_T_FOR_NODE(DT_CHOSEN(zmk_matrix_transform)), + .boot_magic_combos = NULL, + .boot_magic_combos_len = 0, COND_CODE_1(DT_HAS_CHOSEN(zmk_kscan), (.kscan = DEVICE_DT_GET(DT_CHOSEN(zmk_kscan)), ), ())}; static const struct zmk_physical_layout *const layouts[] = { @@ -172,6 +191,8 @@ ZMK_MATRIX_TRANSFORM_DEFAULT_EXTERN(); static const struct zmk_physical_layout _CONCAT(_zmk_physical_layout_, chosen) = { .display_name = "Default", .matrix_transform = &zmk_matrix_transform_default, + .boot_magic_combos = NULL, + .boot_magic_combos_len = 0, #if DT_HAS_CHOSEN(zmk_matrix_input) .input = DEVICE_DT_GET(DT_CHOSEN(zmk_matrix_input)), #elif DT_HAS_CHOSEN(zmk_kscan) diff --git a/app/src/reset.c b/app/src/reset.c new file mode 100644 index 00000000000..dbdb5206647 --- /dev/null +++ b/app/src/reset.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZMK_BLE) +#include +#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 + +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); + 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; + } +#endif /* IS_ENABLED(CONFIG_RETENTION_BOOT_MODE) */ +} + +void zmk_reset_settings(void) { +#if IS_ENABLED(CONFIG_ZMK_BLE) + zmk_ble_unpair_all(); +#endif + // TODO: clear settings for all subsystems. +} diff --git a/docs/docs/config/layout.md b/docs/docs/config/layout.md index 04fdf73bc41..1ce70812414 100644 --- a/docs/docs/config/layout.md +++ b/docs/docs/config/layout.md @@ -182,12 +182,14 @@ Applies to: `compatible = zmk,physical-layout` Definition file: [zmk/app/dts/bindings/zmk,physical-layout.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/zmk%2Cphysical-layout.yaml) -| Property | Type | Description | Default | -| -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | -| `display-name` | string | The name of this layout, for display purposes | | -| `transform` | phandle | The matrix transform to use along with this layout | | -| `kscan` | phandle | The kscan to use along with this layout. The `zmk,kscan` chosen will be used as a fallback if this property is omitted | | -| `keys` | phandle-array | Array of key physical attributes. | | +| Property | Type | Description | Default | +| ------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | +| `display-name` | string | The name of this layout, for display purposes | | +| `transform` | phandle | The matrix transform to use along with this layout | | +| `kscan` | phandle | The kscan to use along with this layout. The `zmk,kscan` chosen will be used as a fallback if this property is omitted | | +| `keys` | phandle-array | Array of key physical attributes. | | +| `bootmagic-combos` | phandles | List of combos specific to this layout that enter bootloader or reset settings when held during boot. | +| | Each element of the `keys` array has the shape `<&key_physical_attrs w h x y r rx ry>`, with the following properties: diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index 1cf70a82554..489040e4c95 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -13,12 +13,13 @@ Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/ ### General -| Config | Type | Description | Default | -| --------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `CONFIG_ZMK_BOARD_COMPAT` | bool | A special config for boards to enable. This helps check if users have accidentally used an upstream Zephyr board without ZMK additions applied | n | -| `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_BOARD_COMPAT` | bool | A special config for boards to enable. This helps check if users have accidentally used an upstream Zephyr board without ZMK additions applied | n | +| `CONFIG_ZMK_KEYBOARD_NAME` | string | The name of the keyboard (max 16 characters) | | +| `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 | :::info diff --git a/docs/docs/features/boot-magic-combo.md b/docs/docs/features/boot-magic-combo.md new file mode 100644 index 00000000000..7cce0bfcc6a --- /dev/null +++ b/docs/docs/features/boot-magic-combo.md @@ -0,0 +1,252 @@ +--- +title: Boot Magic Combo +sidebar_label: Boot Magic Combo +--- + +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 + +Magic keys are specific to a physical layout. If your board doesn't have one, follow the [physical layouts documentation](../development/hardware-integration/physical-layouts.md) to create one. + +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. + +Then configure `boot-magic-combos` in your physical layout to point to your newly defined boot magic combo. If you have multiple boot magic combos, you can pass in a list. + +```c +/ { + ... + bootloader_key: bootloader_key { + compatible = "zmk,boot-magic-combo"; + combo-positions = <0 1>; + }; + + &default_layout { + boot-magic-combos = <&bootloader_key>; + }; + ... +}; +``` + +:::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. + +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. + +::: + +Next, you should add properties to determine what the magic key will do: + +### Jump to 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-combo"; + ... + jump-to-bootloader; + }; + ... +}; +``` + +### Reset Settings + +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-combo"; + ... + 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 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`: + +```c +/ { + ... + recovery_key: recovery_key { + compatible = "zmk,boot-magic-combo"; + jump-to-bootloader; + reset-settings; + }; + ... +}; +``` + +:::info + +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 combos and then only choose the correct combos(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-combo"; + combo-positions = <0>; + jump-to-bootloader; + }; + + bootloader_key_right: bootloader_key_right { + compatible = "zmk,boot-magic-combo"; + combo-positions = <11>; + jump-to-bootloader; + }; + ... +}; +``` + +**shield_left.overlay** + +```c +#include "shield.dtsi" + +&default_layout { + boot-magic-combos = <&bootloader_key_left>; +}; +``` + +**shield_right.overlay** + +```c +#include "shield.dtsi" + +&default_layout { + boot-magic-combos = <&bootloader_key_right>; +}; +``` + +## Key Positions and Physical Layouts + +Key positions are affected by the [matrix transform](../config/kscan.md#matrix-transform). If you use different transforms for each physical layout, then you will need to supply a different set of boot magic combos for each physical 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** + +```c +/ { + chosen { + zmk,physical-layout = &default_layout; + }; + + default_layout: default_layout { + compatible = "zmk,physical-layout"; + display-name = "Default Layout"; + transform = <&default_transform>; + }; + + five_column_layout: five_column_layout { + compatible = "zmk,physical-layout"; + display-name = "5-Column Layout"; + transform = <&five_column_transform>; + }; + + bootloader_key_left: bootloader_key_left { + compatible = "zmk,boot-magic-combo"; + combo-positions = <0>; + jump-to-bootloader; + }; + + bootloader_key_right: bootloader_key_right { + compatible = "zmk,boot-magic-combo"; + combo-positions = <11>; + jump-to-bootloader; + }; + + bootloader_key_right_fivecol: bootloader_key_right_fivecol { + compatible = "zmk,boot-magic-combo"; + combo-positions = <9>; + jump-to-bootloader; + }; + ... +}; +``` + +**shield_left.overlay** + +```c +#include "shield.dtsi" + +&default_layout { + boot-magic-combos = <&bootloader_key_left>; +}; +&five_column_layout { + boot-magic-combos = <&bootloader_key_left>; +} +``` + +**shield_right.overlay** + +```c +#include "shield.dtsi" + +&default_layout { + boot-magic-combos = <&bootloader_key_right>; +}; +&five_column_layout { + boot-magic-combos = <&bootloader_key_right_fivecol>; +} +``` + +**shield.keymap** + +```c +// Uncomment this block if using the 5-column layout +// / { +// chosen { +// zmk,physical-layout = &five_column_layout; +// }; +// }; +``` + +## Startup Timeout + +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_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_COMBO_TIMEOUT_MS=1000 +```