From 4b5758888420d6ed7c8e58fdd82aa52f95324925 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 16 Apr 2023 15:13:50 -0500 Subject: [PATCH 1/3] 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 | 7 ++---- app/src/reset.c | 37 +++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 13 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 a647e883..8dbc2535 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -24,6 +24,7 @@ target_sources(app PRIVATE src/stdlib.c) target_sources(app PRIVATE src/activity.c) target_sources(app PRIVATE src/kscan.c) target_sources(app PRIVATE src/matrix_transform.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 2e775269..bc95f05f 100644 --- a/app/dts/behaviors/reset.dtsi +++ b/app/dts/behaviors/reset.dtsi @@ -17,7 +17,7 @@ bootloader: behavior_reset_dfu { compatible = "zmk,behavior-reset"; label = "BOOTLOAD"; - type = ; + type = ; #binding-cells = <0>; }; }; diff --git a/app/include/dt-bindings/zmk/reset.h b/app/include/dt-bindings/zmk/reset.h index 2b3d8760..63f3428e 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 00000000..d23d1d8e --- /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 0b983c84..71c9ce56 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 LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -28,10 +28,7 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, const struct device *dev = device_get_binding(binding->behavior_dev); const struct behavior_reset_config *cfg = dev->config; - // TODO: Correct magic code for going into DFU? - // See - // https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/d6b28e66053eea467166f44875e3c7ec741cb471/src/main.c#L107 - sys_reboot(cfg->type); + zmk_reset(cfg->type); return ZMK_BEHAVIOR_OPAQUE; } diff --git a/app/src/reset.c b/app/src/reset.c new file mode 100644 index 00000000..0470f519 --- /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 c072f75563ab855588a14d5de043d7ef7973924d Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sat, 29 Apr 2023 23:24:11 -0500 Subject: [PATCH 2/3] 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 | 2 +- app/include/zmk/reset.h | 8 ++++++++ app/src/ble.c | 34 +++++++++++++++++++++------------- app/src/reset.c | 11 +++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h index 4380a33a..5f51a092 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -31,7 +31,7 @@ bool zmk_ble_active_profile_is_open(); bool zmk_ble_active_profile_is_connected(); char *zmk_ble_active_profile_name(); -int zmk_ble_unpair_all(); +void zmk_ble_unpair_all(void); #if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) void zmk_ble_set_peripheral_addr(bt_addr_le_t *addr); diff --git a/app/include/zmk/reset.h b/app/include/zmk/reset.h index d23d1d8e..fad6db6a 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 a7037d0c..5a1d81c5 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -38,6 +38,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 @@ -278,6 +279,25 @@ bt_addr_le_t *zmk_ble_active_profile_addr() { return &profiles[active_profile].p char *zmk_ble_active_profile_name() { return profiles[active_profile].name; } +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 < ZMK_BLE_PROFILE_COUNT; i++) { + char setting_name[15]; + sprintf(setting_name, "ble/profiles/%d", i); + + err = settings_delete(setting_name); + if (err) { + LOG_ERR("Failed to delete setting: %d", err); + } + } +} + #if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) void zmk_ble_set_peripheral_addr(bt_addr_le_t *addr) { @@ -575,19 +595,7 @@ static int zmk_ble_init(const struct device *_arg) { #endif #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 < ZMK_BLE_PROFILE_COUNT; i++) { - char setting_name[15]; - sprintf(setting_name, "ble/profiles/%d", i); - - err = settings_delete(setting_name); - if (err) { - LOG_ERR("Failed to delete setting: %d", err); - } - } + zmk_ble_unpair_all(); #endif bt_conn_cb_register(&conn_callbacks); diff --git a/app/src/reset.c b/app/src/reset.c index 0470f519..c0fff987 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 7178cba204b5558fbd4eeacd3225acefdd4d5422 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 16 Apr 2023 18:00:49 -0500 Subject: [PATCH 3/3] 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 | 9 + app/dts/bindings/zmk,boot-magic-key.yaml | 23 +++ app/src/boot_magic_key.c | 80 +++++++++ docs/docs/config/system.md | 15 +- docs/docs/features/boot-magic-key.md | 209 +++++++++++++++++++++++ docs/sidebars.js | 1 + 7 files changed, 331 insertions(+), 7 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 8dbc2535..4429502e 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -88,6 +88,7 @@ target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c) 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_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 d1b6682f..1bde2cdd 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -523,6 +523,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" 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 00000000..3629c0e8 --- /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 00000000..bc29873a --- /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 a5347b96..78ca5147 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -13,13 +13,14 @@ 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_ZMK_BATTERY_REPORT_INTERVAL` | int | Battery level report interval in seconds | 60 | +| 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 | +| `CONFIG_ZMK_BATTERY_REPORT_INTERVAL` | int | Battery level report interval in seconds | 60 | ### HID diff --git a/docs/docs/features/boot-magic-key.md b/docs/docs/features/boot-magic-key.md new file mode 100644 index 00000000..d51c2b34 --- /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 43f17b41..a4c2c24e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -11,6 +11,7 @@ module.exports = { Features: [ "features/keymaps", "features/bluetooth", + "features/boot-magic-key", "features/combos", "features/conditional-layers", "features/debouncing",