Files
zmk/app/src/ble.c
Pete Johanson c06fa48ce5 feat!: Move to zephyr v4.1 (#3060)
refactor: Move to Zephyr v4.1.0

Move to Zephyr v4.1.0, with various build/compilation fixes needed for
basic use.

refactor(tests): Move to native_sim for tests.

feat(core): (Optionally) use Zephyr keyboard input devices

Add ability to assign a keyboard `input` device to a physical layout,
or use a chosen `zmk,matrix-input`.

fix(pointing): Refactor for changes to input API

Pass NULL user_data to input callbacks.

fix(tests): Fix BLE test to account for Zephyr changes

Handle additional read callback invocation once all matching
characteristic have been read.

fix(sensors): Initialize sensor data to 0 before fetching.

Be sure we don't get back any uninitialized data by initializing
the channel data to 0 before calling into the sensor API.

refactor(input): Adjust split input to input API changes.

Input callbacks now have a user_data parameter, adjust accordingly.

chore(bluetooth): Minor cleanup of split BT code after refactor

Small fixes and remove commented dead code left after the split
refactor.

refactor: Fix up BLE tests after Zephyr upgrade.

Minor changes to snapshots based on newer Zephyr version.

refactor(boards): Move to upstream xiao_ble board ID.

Move to official upstream board definition for the Seeed XIAO BLE.

refactor: Adjust metadata schema for HWMv2 board IDs w/ qualifiers

Adjust our ZMK metadata to allow for board IDs that include qualifiers
with slash delimeters.

refactor!(boards): Move nice!nano to HWMv2, and proper revisioning

Upgrade the nice!nano board to HWMv2, under the proper nicekeyboards
vendor directory, and with proper revisions. Includes a breaking change
to default the `2.0.0` version instead of the much older v1 (`1.0.0`).

fix: Disable Nordic dt-bindings header checks.

Disable the recently added Nordic dt-bindings header checks, which cause
issues for our HID related headers.

fix(studio): Correct `memset` usage.

Use the correct memset call to clear our RPC memory.

fix: Refactor for new Zephyr PM API

Adjustments to our PM code to match Zephyr PM APIs.

refactor(ble): Use correct BT opt for connectable.

Adjust for upstream Zephyr BT API changes for advertising options.

refactor(boards): Move MakerDiary M2 board to HWMv2.

Run the HWMv2 script to convert the MakerDiary M2 board.

fix(studio): Correct usage of thread analyzer API

Fix up the RPC code that invoke the thread analyzer API to account for
API changes.

chore: Remove nanopb module override.

Leverage nanopb version that's used by Zephyr.

feat(core): mapper for magic bootloader values.

To trigger bootloaders that use a magic value in RAM to trigger
bootloader mode, add a mapping retained memory driver that maps
write/read of boot mode values to a special magic value stored
in the actually backing RAM.

feat(behaviors): Add retention boot mode to reset.

Support new generic Zephyr retention boot mode API in the reset
behavior.

feat: Add double tap to enter bootloader functionality

Add ability to enter the bootloader if double tapping reset within the
specified window.

refactor(CI): Move to 4.1 container tags.

Move to the new 4.1 tagged container, to ensure updated SDK, Python
packages, etc.

refactor(boards): Move nRFMicro to HWMv2

Refactor nRFMicro to HWMv2, using proper SoC, revisions, and variants
(for flipped). Also move to devicetree setup of DCDC/HV DCDC.

refactor(boards): Move QMK Proton-C to HWMv2

Move Proton-C to HWMv2 for use with Zephyr 4.1.

chore(ci): Adjust core coverage for new board IDs.

Use correct board IDs, with qualifiers, for our core coverage testing.

refactor(boards): Move BDN9 to HWMv2

Move BDN9 to HWMv2, using the base `bdn9` ID, no longer including the
`_rev2` suffix in the ID.

refactor(boards): Move nice!60 to HWMv2

Migrate nice!60 to HWMv2.

refactor: Adjust how we're searching/loading keymap files

Use new post_boards_shields extension point for loading keymap files
from board/shield directories.

refactor(boards): Move planck rev6 to HWMv2.

Move Planck board definition to HWMv2, including versioning tweaks.

refactor(boards): Move OLKB Preonic to HWMv2

Move Preonic board definition to HWMv2 and remove `_rev3` variant
suffix in favor of board versioning with `3.0.0` as the default.

chore(deps): Pull in Zephyr optional group for nanopb.

Ensure we enable nanopb by adding +optional group filter.

fix(ci): Prevent slash characters in artifact names.

Move to HWMv2 means board IDs often include slashes, so replace those
with underscores when doing file uploads.

fix(usb): Adjust Kconfig settings for USB.

* Ensure USB isn't initialized automatically before we do, which can
  happen if USB CDC logging is used/enabled for a given board.
* Adjust USB HID to initialize the USB class/interface before we enable
  the USB device itself.

fix(display): Fix setting the small font for the mono theme.

Adjust for modified mono theme init function to pass the small font.

chore(ci): Fix changed board IDs for core coverage.

Adjust board IDs for our core coverage after move to HWMv2 and board
versioning consistently.

* planck_rev6 -> planck
* bdn9_rev2 -> bdn9

fix(underglow): Remove use of removed Kconfig WS2812 symbol

refactor(boards): Move PW CKP boards to HWMv2

Migrate the bt60, bt65, and bt75 to HWMv2.

refactor(boards): Move Puchi BLE to HWMv2

Migrate the Puchi BLE to HWMv2.

refactor(boards): Migrate Ferris rev02 to HWMv2.

Move Ferris rev02 to HMWv2, and remove the revision from the ID.

refactor(boards): Move Pillbug to HWMv2

Migrate the MechWild PillBug board to HWMv2.

refactor(boards): Migrate s40nc to HWMv2

Move the ShortyFortyNoCordy (s40nc) to HWMv2.

refactor(boards): Move bluemicro840 board to HWMv2.

Migrate bluemicro840 board to HWMv2, set up boot mode retention.

fix(boards): Retore bootloader support on XIAO BLE.

Set up necessary boot mode/retention to properly set GPREGRET to trigger
Adafruit bootloader to run on the XIAO BLE.

refactor(boards): Move Adv360 Pro to HWMv2.

Migrate Adv360 Pro left/right to HWMv2.

refactor(boards): Move Glove80 to HMWv2

Refactor the MoErgo Glove80 left/right to HWMv2.

refactor(boards): Move Mikoto to HMWv2.

Migrate Mikoto to HWMv2, with non-exact matching, tweaks to I2C
selection to imply it for the 7.2.0 revision for the fuel gauge.

refactor(boards): Move kbdfans Tofu65 2.0 to HMWv2

Move Tofu65 2.0 to HMWv2, with ID of just `tofu65`.

refactor(boards): Remove dz60rgb board

Remove dz60rgb, it's no longer readily available and we have other
current stm32 reference designs for testing.

refactor(boards): Move Corneish Zen to HMWv2

Move Corneish Zen to HMWv2, with IDs of
`corneish_zen_left`/`corneish_zen_right`.

refactor(boards): Migrate Corne-ish Zen status screen

* refactor(boards): Add boot mode to the nice!nano using common dtsi

* Add a new .dtsi for setting up nRF52 boot mode/retained memory
  settings
* Adjust XIAO BLE to use the new include file
* Add boot mode to to the nice!nano

refactor(boards): Add boot mode support to nice!60 board

Enable boot mode for nice!60 board.

refactor(boards): Adjust Zephyr board metadata file locations

Move the ZMK metadata files for upstream Zephyr boards to align with the
HWMv2 directory structure that uses the vendor ID for the parent
directory for a board directory.

fix: Don't enable ZMK Display by default for a few shields

By convention, avoid enabling ZMK Display by default on shields that may
be built with under-resourced controllers (e.g. nRF52833 based ones).

fix: Remove usage of renamed Kconfig from core coverage.

Avoid using WS2812_LED_STRIP, since that Kconfig was renamed/split into
SPI/GPIO/I2S symbols.

refactor(boards): Adjust XIAO RP2040 override names, bootloader support

Adjust the .conf/.overlay files to match the proper naming for the
XIAO rp2040 board. Also add the necessary Kconfig/DTS bits for
supporting bootloader using retained memory/boot mode retention.

fix(display): Adjust stack sizes for display usage.

Updated LVGL is bumping our stack size, so adjust the system work queue
and dedicated display queue stack sizes as needed to account for this.

feat(display): Add thread name to dedicated display queue.

When thread names are enabled, pass a name to the dedicated display
queue for better tracibility when using the thread analyzer.

docs(blog): Add Zephyr upgrade post

docs: Add bootloader integration page

Add a dedicated page to outline steps to set up bootloader integration
using the boot retention mechanism in newer Zephyr versions.

fix(display): port nice!view display code

* remove `lv_` prefix from old LVGL methods

doc: Update local setup docs to use `west packages pip`

Install Zephyr deps using the newer `west packages pip --install`.

Signed-off-by: Peter Johanson <peter@peterjohanson.com>

refactor(split): Adjust BT split code for newer Zephyr APIs.

refactor(boards): Adjust upstream RP2040 boards for boot mode retention

Add necessary DTS/Kconfig settings to upstream RP2040 boards so they can
use the ZMK bootloader functionality using the boot mode retention
infrastructure.

docs: Update Zephyr docs links to 4.1.0 version.

Update all links to the Zephyr docs to the 4.1.0 versions to match our
Zephyr version in use.

docs: Add a note about using CMake v3 for maximum compatibility.

Some optional modules, like libmetal, which is used on nRF5340,
specifically require CMake v3, so add a note in the native toolchain
setup about this.

feat(pointing): Handle INPUT_BTN_TOUCH codes for mouse buttons

Translate INPUT_BTN_TOUCH input codes into button 0 press/release for
HID layer.

chore(pointing): Clean up some warning messages.

Properly check return code from queue-ing messages, and fix up some type
warnings in our logging calls.

* Fix input event codes line numbers

fix(studio): Properly serialize GATT RPC indications.

fix(core): Set a system work queue stack size of 2048 by default

We use a fair amount of stack even without BLE or RP2040, so default to
2048 by default everywhere, and constrained platforms can lowes this if
they really need.

refactor(core): Move away from deprecated DIS Kconfig symbols

Use the correct Device Information Service Kconfig symbols for our model
number and manufacturer.

refactor: Move upstream Zephyr board overrides to extensions dirs

Newer Zephyr supports "board extensions" to formally do what we've added
in ourselves via some hacks, so move all our board overlay/config file
overrides for upstream Zephyr boards into that correct structure.

fix(boards): Add xiao_ble sd_partition label for nosd snippet compat

Upstream xiao_ble uses different naming convention for the partition
labels, so add an additional label for the SD range, so the existing
nrf52840-nosd snippet will still work with the board.

fix(core): Don't force CBPRINTF_NANO, for proper formatting.

The nano CBPRINTF implementation lacks some padded formatting needed to
ensure consistent formatting of BLE addresses, which we use to store
keys as strings in a few places, so use the complete CBPRINTF by default
now.

fix(boards): Remove some references to old nice_nano_v2 board ID.

The nice!nano board definition now properly uses versioning, so avoid
referring to it with old `nice_nano_v2` board ID.

fix(boards): Remove nano overlays for old nice_nano_v2 board ID.

With board versioning in place, we can remove the unused
`nice_nano_v2.overlay` files from shields.

---------

Signed-off-by: Peter Johanson <peter@peterjohanson.com>
Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
Co-authored-by: Nicolas Munnich <munnich@lipn.univ-paris13.fr>
Co-authored-by: snoyer <noyer.stephane@gmail.com>
2025-12-09 19:43:22 -05:00

837 lines
27 KiB
C

/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <errno.h>
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci_types.h>
#if IS_ENABLED(CONFIG_SETTINGS)
#include <zephyr/settings/settings.h>
#endif
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/ble.h>
#include <zmk/keys.h>
#include <zmk/split/bluetooth/uuid.h>
#include <zmk/event_manager.h>
#include <zmk/events/ble_active_profile_changed.h>
#if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY)
#include <zmk/events/keycode_state_changed.h>
#define PASSKEY_DIGITS 6
static struct bt_conn *auth_passkey_entry_conn;
RING_BUF_DECLARE(passkey_entries, PASSKEY_DIGITS);
#endif /* IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY) */
enum advertising_type {
ZMK_ADV_NONE,
ZMK_ADV_DIR,
ZMK_ADV_CONN,
} advertising_status;
#define CURR_ADV(adv) (adv << 4)
#define ZMK_ADV_CONN_NAME \
BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_NAME | BT_LE_ADV_OPT_FORCE_NAME_IN_AD, \
BT_GAP_ADV_FAST_INT_MIN_2, BT_GAP_ADV_FAST_INT_MAX_2, NULL)
static struct zmk_ble_profile profiles[ZMK_BLE_PROFILE_COUNT];
static uint8_t active_profile;
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
#define DEVICE_APPEARANCE \
(uint8_t) CONFIG_BT_DEVICE_APPEARANCE, (uint8_t)(CONFIG_BT_DEVICE_APPEARANCE >> 8)
BUILD_ASSERT(
DEVICE_NAME_LEN <= CONFIG_BT_DEVICE_NAME_MAX,
"ERROR: BLE device name is too long. Max length: " STRINGIFY(CONFIG_BT_DEVICE_NAME_MAX));
static struct bt_data zmk_ble_ad[] = {
BT_DATA_BYTES(BT_DATA_GAP_APPEARANCE, DEVICE_APPEARANCE),
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID16_SOME, 0x12, 0x18, /* HID Service */
0x0f, 0x18 /* Battery Service */
),
};
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
static bt_addr_le_t peripheral_addrs[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) */
static void raise_profile_changed_event(void) {
raise_zmk_ble_active_profile_changed((struct zmk_ble_active_profile_changed){
.index = active_profile, .profile = &profiles[active_profile]});
}
static void raise_profile_changed_event_callback(struct k_work *work) {
raise_profile_changed_event();
}
K_WORK_DEFINE(raise_profile_changed_event_work, raise_profile_changed_event_callback);
bool zmk_ble_active_profile_is_open(void) { return zmk_ble_profile_is_open(active_profile); }
bool zmk_ble_profile_is_open(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return false;
}
return !bt_addr_le_cmp(&profiles[index].peer, BT_ADDR_LE_ANY);
}
void set_profile_address(uint8_t index, const bt_addr_le_t *addr) {
char setting_name[17];
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
memcpy(&profiles[index].peer, addr, sizeof(bt_addr_le_t));
sprintf(setting_name, "ble/profiles/%d", index);
LOG_DBG("Setting profile addr for %s to %s", setting_name, addr_str);
#if IS_ENABLED(CONFIG_SETTINGS)
settings_save_one(setting_name, &profiles[index], sizeof(struct zmk_ble_profile));
#endif
k_work_submit(&raise_profile_changed_event_work);
}
bool zmk_ble_active_profile_is_connected(void) {
return zmk_ble_profile_is_connected(active_profile);
}
bool zmk_ble_profile_is_connected(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return false;
}
struct bt_conn *conn;
struct bt_conn_info info;
bt_addr_le_t *addr = &profiles[index].peer;
if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) {
return false;
} else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) {
return false;
}
bt_conn_get_info(conn, &info);
bt_conn_unref(conn);
return info.state == BT_CONN_STATE_CONNECTED;
}
#define CHECKED_ADV_STOP() \
err = bt_le_adv_stop(); \
advertising_status = ZMK_ADV_NONE; \
if (err) { \
LOG_ERR("Failed to stop advertising (err %d)", err); \
return err; \
}
#define CHECKED_DIR_ADV() \
addr = zmk_ble_active_profile_addr(); \
conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr); \
if (conn != NULL) { /* TODO: Check status of connection */ \
LOG_DBG("Skipping advertising, profile host is already connected"); \
bt_conn_unref(conn); \
return 0; \
} \
err = bt_le_adv_start(BT_LE_ADV_CONN_DIR_LOW_DUTY(addr), zmk_ble_ad, ARRAY_SIZE(zmk_ble_ad), \
NULL, 0); \
if (err) { \
LOG_ERR("Advertising failed to start (err %d)", err); \
return err; \
} \
advertising_status = ZMK_ADV_DIR;
#define CHECKED_OPEN_ADV() \
err = bt_le_adv_start(ZMK_ADV_CONN_NAME, zmk_ble_ad, ARRAY_SIZE(zmk_ble_ad), NULL, 0); \
if (err) { \
LOG_ERR("Advertising failed to start (err %d)", err); \
return err; \
} \
advertising_status = ZMK_ADV_CONN;
int update_advertising(void) {
int err = 0;
bt_addr_le_t *addr;
struct bt_conn *conn;
enum advertising_type desired_adv = ZMK_ADV_NONE;
if (zmk_ble_active_profile_is_open()) {
desired_adv = ZMK_ADV_CONN;
} else if (!zmk_ble_active_profile_is_connected()) {
desired_adv = ZMK_ADV_CONN;
// Need to fix directed advertising for privacy centrals. See
// https://github.com/zephyrproject-rtos/zephyr/pull/14984 char
// addr_str[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(zmk_ble_active_profile_addr(), addr_str,
// sizeof(addr_str));
// LOG_DBG("Directed advertising to %s", addr_str);
// desired_adv = ZMK_ADV_DIR;
}
LOG_DBG("advertising from %d to %d", advertising_status, desired_adv);
switch (desired_adv + CURR_ADV(advertising_status)) {
case ZMK_ADV_NONE + CURR_ADV(ZMK_ADV_DIR):
case ZMK_ADV_NONE + CURR_ADV(ZMK_ADV_CONN):
CHECKED_ADV_STOP();
break;
case ZMK_ADV_DIR + CURR_ADV(ZMK_ADV_DIR):
case ZMK_ADV_DIR + CURR_ADV(ZMK_ADV_CONN):
CHECKED_ADV_STOP();
CHECKED_DIR_ADV();
break;
case ZMK_ADV_DIR + CURR_ADV(ZMK_ADV_NONE):
CHECKED_DIR_ADV();
break;
case ZMK_ADV_CONN + CURR_ADV(ZMK_ADV_DIR):
CHECKED_ADV_STOP();
CHECKED_OPEN_ADV();
break;
case ZMK_ADV_CONN + CURR_ADV(ZMK_ADV_NONE):
CHECKED_OPEN_ADV();
break;
}
return 0;
};
static void update_advertising_callback(struct k_work *work) { update_advertising(); }
K_WORK_DEFINE(update_advertising_work, update_advertising_callback);
static void clear_profile_bond(uint8_t profile) {
if (bt_addr_le_cmp(&profiles[profile].peer, BT_ADDR_LE_ANY)) {
bt_unpair(BT_ID_DEFAULT, &profiles[profile].peer);
set_profile_address(profile, BT_ADDR_LE_ANY);
}
}
void zmk_ble_clear_bonds(void) {
LOG_DBG("zmk_ble_clear_bonds()");
clear_profile_bond(active_profile);
update_advertising();
};
void zmk_ble_clear_all_bonds(void) {
LOG_DBG("zmk_ble_clear_all_bonds()");
// Unpair all profiles
for (int i = 0; i < ZMK_BLE_PROFILE_COUNT; i++) {
clear_profile_bond(i);
}
// Automatically switch to profile 0
zmk_ble_prof_select(0);
update_advertising();
};
int zmk_ble_active_profile_index(void) { return active_profile; }
int zmk_ble_profile_index(const bt_addr_le_t *addr) {
for (int i = 0; i < ZMK_BLE_PROFILE_COUNT; i++) {
if (bt_addr_le_cmp(addr, &profiles[i].peer) == 0) {
return i;
}
}
return -ENODEV;
}
bt_addr_le_t *zmk_ble_profile_address(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return (bt_addr_le_t *)(BT_ADDR_LE_NONE);
}
return &profiles[index].peer;
}
#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));
}
static struct k_work_delayable ble_save_work;
#endif
static int ble_save_profile(void) {
#if IS_ENABLED(CONFIG_SETTINGS)
return k_work_reschedule(&ble_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE));
#else
return 0;
#endif
}
int zmk_ble_prof_select(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT) {
return -ERANGE;
}
LOG_DBG("profile %d", index);
if (active_profile == index) {
return 0;
}
active_profile = index;
ble_save_profile();
update_advertising();
raise_profile_changed_event();
return 0;
};
int zmk_ble_prof_next(void) {
LOG_DBG("");
return zmk_ble_prof_select((active_profile + 1) % ZMK_BLE_PROFILE_COUNT);
};
int zmk_ble_prof_prev(void) {
LOG_DBG("");
return zmk_ble_prof_select((active_profile + ZMK_BLE_PROFILE_COUNT - 1) %
ZMK_BLE_PROFILE_COUNT);
};
int zmk_ble_prof_disconnect(uint8_t index) {
if (index >= ZMK_BLE_PROFILE_COUNT)
return -ERANGE;
bt_addr_le_t *addr = &profiles[index].peer;
struct bt_conn *conn;
int result;
if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) {
return -ENODEV;
} else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) {
return -ENODEV;
}
result = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
LOG_DBG("Disconnected from profile %d: %d", index, result);
bt_conn_unref(conn);
return result;
}
bt_addr_le_t *zmk_ble_active_profile_addr(void) { return &profiles[active_profile].peer; }
struct bt_conn *zmk_ble_active_profile_conn(void) {
struct bt_conn *conn;
bt_addr_le_t *addr = zmk_ble_active_profile_addr();
if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) {
LOG_WRN("Not sending, no active address for current profile");
return NULL;
} else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) {
LOG_WRN("Not sending, not connected to active profile");
return NULL;
}
return conn;
}
char *zmk_ble_active_profile_name(void) { return profiles[active_profile].name; }
int zmk_ble_set_device_name(char *name) {
// Copy new name to advertising parameters
int err = bt_set_name(name);
LOG_DBG("New device name: %s", name);
if (err) {
LOG_ERR("Failed to set new device name (err %d)", err);
return err;
}
if (advertising_status == ZMK_ADV_CONN) {
// Stop current advertising so it can restart with new name
err = bt_le_adv_stop();
advertising_status = ZMK_ADV_NONE;
if (err) {
LOG_ERR("Failed to stop advertising (err %d)", err);
return err;
}
}
return update_advertising();
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr) {
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
// If the address is recognized and already stored in settings, return
// index and no additional action is necessary.
if (bt_addr_le_cmp(&peripheral_addrs[i], addr) == 0) {
LOG_DBG("Found existing peripheral address in slot %d", i);
return i;
} else {
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&peripheral_addrs[i], addr_str, sizeof(addr_str));
LOG_DBG("peripheral slot %d occupied by %s", i, addr_str);
}
// If the peripheral address slot is open, store new peripheral in the
// slot and return index. This compares against BT_ADDR_LE_ANY as that
// is the zero value.
if (bt_addr_le_cmp(&peripheral_addrs[i], BT_ADDR_LE_ANY) == 0) {
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
LOG_DBG("Storing peripheral %s in slot %d", addr_str, i);
bt_addr_le_copy(&peripheral_addrs[i], addr);
#if IS_ENABLED(CONFIG_SETTINGS)
char setting_name[32];
sprintf(setting_name, "ble/peripheral_addresses/%d", i);
settings_save_one(setting_name, addr, sizeof(bt_addr_le_t));
#endif // IS_ENABLED(CONFIG_SETTINGS)
return i;
}
}
// The peripheral does not match a known peripheral and there is no
// available slot.
return -ENOMEM;
}
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) */
#if IS_ENABLED(CONFIG_SETTINGS)
static int ble_profiles_handle_set(const char *name, size_t len, settings_read_cb read_cb,
void *cb_arg) {
const char *next;
LOG_DBG("Setting BLE value %s", name);
if (settings_name_steq(name, "profiles", &next) && next) {
char *endptr;
uint8_t idx = strtoul(next, &endptr, 10);
if (*endptr != '\0') {
LOG_WRN("Invalid profile index: %s", next);
return -EINVAL;
}
if (len != sizeof(struct zmk_ble_profile)) {
LOG_ERR("Invalid profile size (got %d expected %d)", len,
sizeof(struct zmk_ble_profile));
return -EINVAL;
}
if (idx >= ZMK_BLE_PROFILE_COUNT) {
LOG_WRN("Profile address for index %d is larger than max of %d", idx,
ZMK_BLE_PROFILE_COUNT);
return -EINVAL;
}
int err = read_cb(cb_arg, &profiles[idx], sizeof(struct zmk_ble_profile));
if (err <= 0) {
LOG_ERR("Failed to handle profile address from settings (err %d)", err);
return err;
}
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&profiles[idx].peer, addr_str, sizeof(addr_str));
LOG_DBG("Loaded %s address for profile %d", addr_str, idx);
} else if (settings_name_steq(name, "active_profile", &next) && !next) {
if (len != sizeof(active_profile)) {
return -EINVAL;
}
int err = read_cb(cb_arg, &active_profile, sizeof(active_profile));
if (err <= 0) {
LOG_ERR("Failed to handle active profile from settings (err %d)", err);
return err;
}
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
else if (settings_name_steq(name, "peripheral_addresses", &next) && next) {
if (len != sizeof(bt_addr_le_t)) {
return -EINVAL;
}
int i = atoi(next);
if (i < 0 || i >= ZMK_SPLIT_BLE_PERIPHERAL_COUNT) {
LOG_ERR("Failed to store peripheral address in memory");
} else {
int err = read_cb(cb_arg, &peripheral_addrs[i], sizeof(bt_addr_le_t));
if (err <= 0) {
LOG_ERR("Failed to handle peripheral address from settings (err %d)", err);
return err;
}
}
}
#endif
return 0;
};
static int zmk_ble_complete_startup(void);
static struct settings_handler profiles_handler = {
.name = "ble", .h_set = ble_profiles_handle_set, .h_commit = zmk_ble_complete_startup};
#endif /* IS_ENABLED(CONFIG_SETTINGS) */
static bool is_conn_active_profile(const struct bt_conn *conn) {
return bt_addr_le_cmp(bt_conn_get_dst(conn), &profiles[active_profile].peer) == 0;
}
static void connected(struct bt_conn *conn, uint8_t err) {
char addr[BT_ADDR_LE_STR_LEN];
struct bt_conn_info info;
LOG_DBG("Connected thread: %p", k_current_get());
bt_conn_get_info(conn, &info);
if (info.role != BT_CONN_ROLE_PERIPHERAL) {
LOG_DBG("SKIPPING FOR ROLE %d", info.role);
return;
}
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
advertising_status = ZMK_ADV_NONE;
if (err) {
LOG_WRN("Failed to connect to %s (%u)", addr, err);
update_advertising();
return;
}
LOG_DBG("Connected %s", addr);
update_advertising();
if (is_conn_active_profile(conn)) {
LOG_DBG("Active profile connected");
k_work_submit(&raise_profile_changed_event_work);
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason) {
char addr[BT_ADDR_LE_STR_LEN];
struct bt_conn_info info;
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("Disconnected from %s (reason 0x%02x)", addr, reason);
bt_conn_get_info(conn, &info);
if (info.role != BT_CONN_ROLE_PERIPHERAL) {
LOG_DBG("SKIPPING FOR ROLE %d", info.role);
return;
}
// We need to do this in a work callback, otherwise the advertising update will still see the
// connection for a profile as active, and not start advertising yet.
k_work_submit(&update_advertising_work);
if (is_conn_active_profile(conn)) {
LOG_DBG("Active profile disconnected");
k_work_submit(&raise_profile_changed_event_work);
}
}
static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (!err) {
LOG_DBG("Security changed: %s level %u", addr, level);
} else {
LOG_ERR("Security failed: %s level %u err %d", addr, level, err);
}
}
static void le_param_updated(struct bt_conn *conn, uint16_t interval, uint16_t latency,
uint16_t timeout) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("%s: interval %d latency %d timeout %d", addr, interval, latency, timeout);
}
static struct bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
.security_changed = security_changed,
.le_param_updated = le_param_updated,
};
/*
static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("Passkey for %s: %06u", addr, passkey);
}
*/
#if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY)
static void auth_passkey_entry(struct bt_conn *conn) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_DBG("Passkey entry requested for %s", addr);
ring_buf_reset(&passkey_entries);
auth_passkey_entry_conn = bt_conn_ref(conn);
}
#endif
static void auth_cancel(struct bt_conn *conn) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
#if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY)
if (auth_passkey_entry_conn) {
bt_conn_unref(auth_passkey_entry_conn);
auth_passkey_entry_conn = NULL;
}
ring_buf_reset(&passkey_entries);
#endif
LOG_DBG("Pairing cancelled: %s", addr);
}
static bool pairing_allowed_for_current_profile(struct bt_conn *conn) {
return zmk_ble_active_profile_is_open() ||
(IS_ENABLED(CONFIG_BT_SMP_ALLOW_UNAUTH_OVERWRITE) &&
bt_addr_le_cmp(zmk_ble_active_profile_addr(), bt_conn_get_dst(conn)) == 0);
}
static enum bt_security_err auth_pairing_accept(struct bt_conn *conn,
const struct bt_conn_pairing_feat *const feat) {
struct bt_conn_info info;
bt_conn_get_info(conn, &info);
LOG_DBG("role %d, open? %s", info.role, zmk_ble_active_profile_is_open() ? "yes" : "no");
if (info.role == BT_CONN_ROLE_PERIPHERAL && !pairing_allowed_for_current_profile(conn)) {
LOG_WRN("Rejecting pairing request to taken profile %d", active_profile);
return BT_SECURITY_ERR_PAIR_NOT_ALLOWED;
}
return BT_SECURITY_ERR_SUCCESS;
};
static void auth_pairing_complete(struct bt_conn *conn, bool bonded) {
struct bt_conn_info info;
char addr[BT_ADDR_LE_STR_LEN];
const bt_addr_le_t *dst = bt_conn_get_dst(conn);
bt_addr_le_to_str(dst, addr, sizeof(addr));
bt_conn_get_info(conn, &info);
if (info.role != BT_CONN_ROLE_PERIPHERAL) {
LOG_DBG("SKIPPING FOR ROLE %d", info.role);
return;
}
if (!pairing_allowed_for_current_profile(conn)) {
LOG_ERR("Pairing completed but current profile is not open: %s", addr);
bt_unpair(BT_ID_DEFAULT, dst);
return;
}
set_profile_address(active_profile, dst);
update_advertising();
};
static struct bt_conn_auth_cb zmk_ble_auth_cb_display = {
.pairing_accept = auth_pairing_accept,
// .passkey_display = auth_passkey_display,
#if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY)
.passkey_entry = auth_passkey_entry,
#endif
.cancel = auth_cancel,
};
static struct bt_conn_auth_info_cb zmk_ble_auth_info_cb_display = {
.pairing_complete = auth_pairing_complete,
};
static void zmk_ble_ready(int err) {
LOG_DBG("ready? %d", err);
if (err) {
LOG_ERR("Bluetooth init failed (err %d)", err);
return;
}
update_advertising();
}
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);
}
}
#endif // IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START)
bt_conn_cb_register(&conn_callbacks);
bt_conn_auth_cb_register(&zmk_ble_auth_cb_display);
bt_conn_auth_info_cb_register(&zmk_ble_auth_info_cb_display);
zmk_ble_ready(0);
return 0;
}
static int zmk_ble_init(void) {
int err = bt_enable(NULL);
if (err < 0 && err != -EALREADY) {
LOG_ERR("BLUETOOTH FAILED (%d)", err);
return err;
}
#if IS_ENABLED(CONFIG_SETTINGS)
settings_register(&profiles_handler);
k_work_init_delayable(&ble_save_work, ble_save_profile_work);
#else
zmk_ble_complete_startup();
#endif
return 0;
}
#if IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY)
static bool zmk_ble_numeric_usage_to_value(const zmk_key_t key, const zmk_key_t one,
const zmk_key_t zero, uint8_t *value) {
if (key < one || key > zero) {
return false;
}
*value = (key == zero) ? 0 : (key - one + 1);
return true;
}
static int zmk_ble_handle_key_user(struct zmk_keycode_state_changed *event) {
zmk_key_t key = event->keycode;
LOG_DBG("key %d", key);
if (!auth_passkey_entry_conn) {
LOG_DBG("No connection for passkey entry");
return ZMK_EV_EVENT_BUBBLE;
}
if (event->state) {
LOG_DBG("Key press, ignoring");
return ZMK_EV_EVENT_HANDLED;
}
if (key == HID_USAGE_KEY_KEYBOARD_ESCAPE) {
bt_conn_auth_cancel(auth_passkey_entry_conn);
return ZMK_EV_EVENT_HANDLED;
}
if (key == HID_USAGE_KEY_KEYBOARD_RETURN || key == HID_USAGE_KEY_KEYBOARD_RETURN_ENTER) {
uint8_t digits[PASSKEY_DIGITS];
uint32_t count = ring_buf_get(&passkey_entries, digits, PASSKEY_DIGITS);
uint32_t passkey = 0;
for (int i = 0; i < count; i++) {
passkey = (passkey * 10) + digits[i];
}
LOG_DBG("Final passkey: %d", passkey);
bt_conn_auth_passkey_entry(auth_passkey_entry_conn, passkey);
bt_conn_unref(auth_passkey_entry_conn);
auth_passkey_entry_conn = NULL;
return ZMK_EV_EVENT_HANDLED;
}
uint8_t val;
if (!(zmk_ble_numeric_usage_to_value(key, HID_USAGE_KEY_KEYBOARD_1_AND_EXCLAMATION,
HID_USAGE_KEY_KEYBOARD_0_AND_RIGHT_PARENTHESIS, &val) ||
zmk_ble_numeric_usage_to_value(key, HID_USAGE_KEY_KEYPAD_1_AND_END,
HID_USAGE_KEY_KEYPAD_0_AND_INSERT, &val))) {
LOG_DBG("Key not a number, ignoring");
return ZMK_EV_EVENT_HANDLED;
}
if (ring_buf_space_get(&passkey_entries) <= 0) {
uint8_t discard_val;
ring_buf_get(&passkey_entries, &discard_val, 1);
}
ring_buf_put(&passkey_entries, &val, 1);
LOG_DBG("value entered: %d, digits collected so far: %d", val,
ring_buf_size_get(&passkey_entries));
return ZMK_EV_EVENT_HANDLED;
}
static int zmk_ble_listener(const zmk_event_t *eh) {
struct zmk_keycode_state_changed *kc_state;
kc_state = as_zmk_keycode_state_changed(eh);
if (kc_state != NULL) {
return zmk_ble_handle_key_user(kc_state);
}
return 0;
}
ZMK_LISTENER(zmk_ble, zmk_ble_listener);
ZMK_SUBSCRIPTION(zmk_ble, zmk_keycode_state_changed);
#endif /* IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY) */
SYS_INIT(zmk_ble_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY);