Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion app/dts/behaviors/reset.dtsi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// Behavior can be invoked on peripherals, so name must be <= 8 characters.
bootloader: bootload {
compatible = "zmk,behavior-reset";
type = <RST_UF2>;
type = <ZMK_RESET_BOOTLOADER>;
bootloader;
#binding-cells = <0>;
display-name = "Bootloader";
Expand Down
23 changes: 23 additions & 0 deletions app/dts/bindings/zmk,boot-magic-combo.yaml
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +18 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice, now that we have ZMK Studio, if we could also add an option here for doing a "restore stock keymap", just in case someone mistakenly removes their &studio_unlock from their keymap when making changes.

On that node, this should probably just be one properly, that's an enum, e.g.:

Suggested change
jump-to-bootloader:
type: boolean
description: Reboots into the bootloader.
reset-settings:
type: boolean
description: Clears settings and reboots.
action:
type: string
required: true
enum:
- "jump-to-bootloader"
- "reset-settings"
- "restore-studio-stock-keymap"

since it only makes sense, IMHO, for a given boot magic key to perform one of the possible boot magic actions, and allows us to grow the enum list later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on the ZMK Studio "restore default keymap" idea - worth logging on https://github.com/zmkfirmware/zmk-studio/issues ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be even nicer if, instead of restoring the default keymap, we had an option to just unlock studio.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be even nicer if, instead of restoring the default keymap, we had an option to just unlock studio.

Yeah, like this much better.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost sounding like the action should just be a pointer to some ZMK behavior :)

3 changes: 3 additions & 0 deletions app/dts/bindings/zmk,physical-layout.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 3 additions & 7 deletions app/include/dt-bindings/zmk/reset.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
#define ZMK_RESET_WARM 0
#define ZMK_RESET_COLD 1
#define ZMK_RESET_BOOTLOADER 2
2 changes: 1 addition & 1 deletion app/include/zmk/ble.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
24 changes: 24 additions & 0 deletions app/include/zmk/boot_magic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <zephyr/types.h>
#include <stdbool.h>
#include <zephyr/sys/util.h>

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)
6 changes: 5 additions & 1 deletion app/include/zmk/physical_layouts.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <zephyr/kernel.h>
#include <zmk/matrix_transform.h>
#include <zmk/event_manager.h>
#include <zmk/boot_magic.h>

struct zmk_physical_layout_selection_changed {
uint8_t selection;
Expand Down Expand Up @@ -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)
Expand All @@ -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);
int zmk_physical_layouts_get_selected_to_stock_position_map(uint32_t const **map);
25 changes: 25 additions & 0 deletions app/include/zmk/reset.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <zephyr/toolchain.h>
#include <dt-bindings/zmk/reset.h>

/**
* 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);
15 changes: 3 additions & 12 deletions app/src/behaviors/behavior_reset.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
#define DT_DRV_COMPAT zmk_behavior_reset

#include <zephyr/device.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/logging/log.h>

#include <drivers/behavior.h>

#include <zmk/behavior.h>
#include <zmk/reset.h>

#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE)

Expand All @@ -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;
}

Expand Down
53 changes: 27 additions & 26 deletions app/src/ble.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions app/src/boot/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
122 changes: 122 additions & 0 deletions app/src/boot/boot_magic_combo.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copy link
Copy Markdown
Contributor

@petejohanson petejohanson Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to say this code should live in the fairly recently created app/src/boot/ directory at this point.

* Copyright (c) 2026 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_boot_magic_combo

#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/toolchain.h>
#include <zephyr/logging/log.h>

#include <zmk/reset.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/boot_magic.h>
#include <zmk/physical_layouts.h>

#if IS_ENABLED(CONFIG_RETENTION_BOOT_MODE)

#include <zephyr/retention/bootmode.h>

#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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I need a refresher here, or @joelspadin can weigh in... But do we really have a use case where you want to reset the settings and then jump to the bootloader? Having a hard time imaging a real need for that specific use case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure I probably came up with a reason for that when I originally wrote this, but I can't think of one now.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QMK does a reset then jump, but it's always bothered me that there's no way to do just a jump to bootloader. With QMK I've had some troubles with my saved changes causing conflicts when I make major changes to the hardware like changing the number of keys in the firmware, and a settings reset was necessary. I haven't stress tested ZMK as much, but if this isn't an issue here (Studio does seem very well architected), then I agree just one makes sense.

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) */
Loading