-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
(for v0.4) Add a way to jump to bootloader and/or reset settings on startup #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
83dbb58
60c8e09
d6b3707
ace0613
f97e49f
f88c8bb
8b1eb38
f9eb39d
44de7cc
7339326
645baaa
478fac8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
| 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) |
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /* | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| * 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) */ | ||
There was a problem hiding this comment.
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_unlockfrom their keymap when making changes.On that node, this should probably just be one properly, that's an enum, e.g.:
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, like this much better.
There was a problem hiding this comment.
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 :)