diff --git a/app/Kconfig b/app/Kconfig
index 1189c6547b0..9419615cd3c 100644
--- a/app/Kconfig
+++ b/app/Kconfig
@@ -185,6 +185,13 @@ config ZMK_BLE_PASSKEY_ENTRY
bool "Require passkey entry on the keyboard to complete pairing"
select RING_BUFFER
+config ZMK_BLE_PERSIST_PROFILE_ON_CHANGE
+ bool "Persist the active BLE profile on change"
+ default y
+ help
+ Enables persisting the active BLE profile on change (after ZMK_SETTINGS_SAVE_DEBOUNCE ms),
+ ensuring it is active on startup.
+
config BT_SMP_ALLOW_UNAUTH_OVERWRITE
imply ZMK_BLE_PASSKEY_ENTRY
diff --git a/app/include/dt-bindings/zmk/bt.h b/app/include/dt-bindings/zmk/bt.h
index aaad4dc5b8b..10a1d94131d 100644
--- a/app/include/dt-bindings/zmk/bt.h
+++ b/app/include/dt-bindings/zmk/bt.h
@@ -10,6 +10,7 @@
#define BT_SEL_CMD 3
#define BT_CLR_ALL_CMD 4
#define BT_DISC_CMD 5
+#define BT_SAVE_CMD 6
/*
Note: Some future commands will include additional parameters, so we
@@ -22,3 +23,4 @@ defines these aliases up front.
#define BT_SEL BT_SEL_CMD
#define BT_CLR_ALL BT_CLR_ALL_CMD 0
#define BT_DISC BT_DISC_CMD
+#define BT_SAVE BT_SAVE_CMD 0
diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h
index cc55a6ce142..56aba918ea7 100644
--- a/app/include/zmk/ble.h
+++ b/app/include/zmk/ble.h
@@ -26,6 +26,7 @@ int zmk_ble_prof_prev(void);
int zmk_ble_prof_select(uint8_t index);
void zmk_ble_clear_all_bonds(void);
int zmk_ble_prof_disconnect(uint8_t index);
+void zmk_ble_save_profile(bool immediate);
int zmk_ble_active_profile_index(void);
int zmk_ble_profile_index(const bt_addr_le_t *addr);
diff --git a/app/src/behaviors/behavior_bt.c b/app/src/behaviors/behavior_bt.c
index f439e49b1cf..dca83b3cbbb 100644
--- a/app/src/behaviors/behavior_bt.c
+++ b/app/src/behaviors/behavior_bt.c
@@ -105,6 +105,9 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
return 0;
case BT_DISC_CMD:
return zmk_ble_prof_disconnect(binding->param2);
+ case BT_SAVE_CMD:
+ zmk_ble_save_profile(true);
+ return 0;
default:
LOG_ERR("Unknown BT command: %d", binding->param1);
}
diff --git a/app/src/ble.c b/app/src/ble.c
index 776730fe5c3..a6a2b09ced1 100644
--- a/app/src/ble.c
+++ b/app/src/ble.c
@@ -251,16 +251,16 @@ int zmk_ble_profile_index(const bt_addr_le_t *addr) {
#if IS_ENABLED(CONFIG_SETTINGS)
static void ble_save_profile_work(struct k_work *work) {
settings_save_one("ble/active_profile", &active_profile, sizeof(active_profile));
+ LOG_DBG("Saved active profile %d.", active_profile);
}
static struct k_work_delayable ble_save_work;
#endif
-static int ble_save_profile(void) {
+void zmk_ble_save_profile(bool immediate) {
#if IS_ENABLED(CONFIG_SETTINGS)
- return k_work_reschedule(&ble_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE));
-#else
- return 0;
+ int delay = immediate ? 0 : CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE;
+ k_work_reschedule(&ble_save_work, K_MSEC(delay));
#endif
}
@@ -275,7 +275,9 @@ int zmk_ble_prof_select(uint8_t index) {
}
active_profile = index;
- ble_save_profile();
+#if IS_ENABLED(CONFIG_ZMK_BLE_PERSIST_PROFILE_ON_CHANGE)
+ zmk_ble_save_profile(false);
+#endif
update_advertising();
@@ -422,6 +424,7 @@ static int ble_profiles_handle_set(const char *name, size_t len, settings_read_c
LOG_ERR("Failed to handle active profile from settings (err %d)", err);
return err;
}
+ LOG_DBG("Loaded active profile %d", active_profile);
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
else if (settings_name_steq(name, "peripheral_addresses", &next) && next) {
diff --git a/docs/docs/config/bluetooth.md b/docs/docs/config/bluetooth.md
index 83fb9ec09d8..51a66c0173b 100644
--- a/docs/docs/config/bluetooth.md
+++ b/docs/docs/config/bluetooth.md
@@ -9,10 +9,11 @@ See [Configuration Overview](index.md) for instructions on how to change these s
## Kconfig
-| Option | Type | Description | Default |
-| -------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
-| `CONFIG_ZMK_BLE_EXPERIMENTAL_CONN` | bool | Enables a combination of settings that are planned to be default in future versions of ZMK to improve connection stability. Currently this only disables 2M PHY support. | n |
-| `CONFIG_ZMK_BLE_EXPERIMENTAL_SEC` | bool | Enables a combination of settings that are planned to be officially supported in the future. This includes enabling BT Secure Connection passkey entry, and allows overwrite of keys from previously paired hosts. | n |
-| `CONFIG_ZMK_BLE_EXPERIMENTAL_FEATURES` | bool | Aggregate config that enables both `CONFIG_ZMK_BLE_EXPERIMENTAL_CONN` and `CONFIG_ZMK_BLE_EXPERIMENTAL_SEC`. | n |
-| `CONFIG_ZMK_BLE_PASSKEY_ENTRY` | bool | Enable passkey entry during pairing for enhanced security. (Note: After enabling this, you will need to re-pair all previously paired hosts.) | n |
-| `CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION` | bool | Low level setting for GATT subscriptions. Set to `n` to work around an annoying Windows bug with battery notifications. | y |
+| Option | Type | Description | Default |
+| ------------------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
+| `CONFIG_ZMK_BLE_EXPERIMENTAL_CONN` | bool | Enables a combination of settings that are planned to be default in future versions of ZMK to improve connection stability. Currently this only disables 2M PHY support. | n |
+| `CONFIG_ZMK_BLE_EXPERIMENTAL_SEC` | bool | Enables a combination of settings that are planned to be officially supported in the future. This includes enabling BT Secure Connection passkey entry, and allows overwrite of keys from previously paired hosts. | n |
+| `CONFIG_ZMK_BLE_EXPERIMENTAL_FEATURES` | bool | Aggregate config that enables both `CONFIG_ZMK_BLE_EXPERIMENTAL_CONN` and `CONFIG_ZMK_BLE_EXPERIMENTAL_SEC`. | n |
+| `CONFIG_ZMK_BLE_PASSKEY_ENTRY` | bool | Enable passkey entry during pairing for enhanced security. (Note: After enabling this, you will need to re-pair all previously paired hosts.) | n |
+| `CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION` | bool | Low level setting for GATT subscriptions. Set to `n` to work around an annoying Windows bug with battery notifications. | y |
+| `CONFIG_ZMK_BLE_PERSIST_PROFILE_ON_CHANGE` | bool | Enables the new profile to be saved upon selection, ensuring it is active at startup. (Note: The profile is only saved after a delay of `CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE` milliseconds, to minimize potential wear on the flash memory.) | y |
diff --git a/docs/docs/keymaps/behaviors/bluetooth.md b/docs/docs/keymaps/behaviors/bluetooth.md
index 93d0842814a..f7c46ac43d3 100644
--- a/docs/docs/keymaps/behaviors/bluetooth.md
+++ b/docs/docs/keymaps/behaviors/bluetooth.md
@@ -38,18 +38,22 @@ This will allow you to reference the actions defined in this header such as `BT_
Here is a table describing the command for each define:
-| Define | Action |
-| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `BT_CLR` | Clear bond information between the keyboard and host for the selected profile. |
-| `BT_CLR_ALL` | Clear bond information between the keyboard and host for all profiles. |
-| `BT_NXT` | Switch to the next profile, cycling through to the first one when the end is reached. |
-| `BT_PRV` | Switch to the previous profile, cycling through to the last one when the beginning is reached. |
-| `BT_SEL` | Select the 0-indexed profile by number; must include a number as an argument in the keymap to work correctly, e.g. `BT_SEL 0`. |
-| `BT_DISC` | Disconnect from the 0-indexed profile by number, if it's currently connected and inactive; must include a number as an argument in the keymap to work correctly, e.g. `BT_DISC 0`. |
+| Define | Action |
+| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `BT_CLR` | Clear bond information between the keyboard and host for the selected profile. |
+| `BT_CLR_ALL` | Clear bond information between the keyboard and host for all profiles. |
+| `BT_NXT` | Switch to the next profile, cycling through to the first one when the end is reached. |
+| `BT_PRV` | Switch to the previous profile, cycling through to the last one when the beginning is reached. |
+| `BT_SEL` | Select the 0-indexed profile by number; must include a number as an argument in the keymap to work correctly, e.g. `BT_SEL 0`. |
+| `BT_DISC` | Disconnect from the 0-indexed profile by number, if it's currently connected and inactive; must include a number as an argument in the keymap to work correctly, e.g. `BT_DISC 0`. |
+| `BT_SAVE` | Saves the currently selected profile, ensuring it is active at startup.
Note: This is only useful if `ZMK_CONFIG_BLE_PERSIST_PROFILE_ON_CHANGE` is disabled. Per default the active profile will be persisted on change automatically. |
:::note[Selected profile persistence]
The profile that is selected by the `BT_SEL`/`BT_PRV`/`BT_NXT` actions will be saved to flash storage and hence persist across restarts and firmware flashes.
+
However it will only be saved after [`CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE`](../../config/system.md#general) milliseconds in order to reduce potential wear on the flash memory.
+
+[`CONFIG_ZMK_BLE_PERSIST_PROFILE_ON_CHANGE`](../../config/bluetooth.md) can be used to disable automatic saving of the selected profile to flash storage. In this case `BT_SAVE` can be used to manually persist the currently selected profile to change the active profile at startup.
:::
## Bluetooth Behavior