Skip to content

Commit

Permalink
feat: Add a way to jump to bootloader on startup
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joelspadin authored and caksoylar committed Dec 6, 2023
1 parent 2ebd870 commit 3f4d9ff
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 6 deletions.
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,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(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/)
Expand Down
9 changes: 9 additions & 0 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,15 @@ choice CBPRINTF_IMPLEMENTATION

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"
Expand Down
23 changes: 23 additions & 0 deletions app/dts/bindings/zmk,boot-magic-key.yaml
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions app/src/boot_magic_key.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#define DT_DRV_COMPAT zmk_boot_magic_key

#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>

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);
13 changes: 7 additions & 6 deletions docs/docs/config/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ 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_SETTINGS_SAVE_DEBOUNCE` | int | Milliseconds to wait after a setting change before writing it to flash memory | 60000 |
| `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_SETTINGS_SAVE_DEBOUNCE` | int | Milliseconds to wait after a setting change before writing it to flash memory | 60000 |
| `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 |

### HID

Expand Down
209 changes: 209 additions & 0 deletions docs/docs/features/boot-magic-key.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
Features: [
"features/keymaps",
"features/bluetooth",
"features/boot-magic-key",
"features/combos",
"features/conditional-layers",
"features/debouncing",
Expand Down

0 comments on commit 3f4d9ff

Please sign in to comment.