Skip to content

Commit

Permalink
feat(ble): Support perhipheral battery levels.
Browse files Browse the repository at this point in the history
* Add ability to fetch and report peripheral battery levels
  on split centrals.
* Add additional support for adding a new Battery Level
  service to split centrals that exposes fetched peripheral
  battery levels to connected hosts.

Co-authored-by: Peter Johanson <[email protected]>
  • Loading branch information
Katona and petejohanson committed Nov 29, 2023
1 parent 69f7bfb commit 05f7cd1
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 21 deletions.
10 changes: 9 additions & 1 deletion app/include/zmk/events/battery_state_changed.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,12 @@ struct zmk_battery_state_changed {
uint8_t state_of_charge;
};

ZMK_EVENT_DECLARE(zmk_battery_state_changed);
ZMK_EVENT_DECLARE(zmk_battery_state_changed);

struct zmk_peripheral_battery_state_changed {
uint8_t source;
// TODO: Other battery channels
uint8_t state_of_charge;
};

ZMK_EVENT_DECLARE(zmk_peripheral_battery_state_changed);
3 changes: 2 additions & 1 deletion app/src/display/widgets/battery_status.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ void battery_status_update_cb(struct battery_status_state state) {
}

static struct battery_status_state battery_status_get_state(const zmk_event_t *eh) {
const struct zmk_battery_state_changed *ev = as_zmk_battery_state_changed(eh);
return (struct battery_status_state) {
.level = bt_bas_get_battery_level(),
.level = ev->state_of_charge,
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
.usb_present = zmk_usb_is_powered(),
#endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */
Expand Down
4 changes: 3 additions & 1 deletion app/src/events/battery_state_changed.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
#include <zephyr/kernel.h>
#include <zmk/events/battery_state_changed.h>

ZMK_EVENT_IMPL(zmk_battery_state_changed);
ZMK_EVENT_IMPL(zmk_battery_state_changed);

ZMK_EVENT_IMPL(zmk_peripheral_battery_state_changed);
4 changes: 4 additions & 0 deletions app/src/split/bluetooth/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
endif()
if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE central.c)
endif()

if (CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY)
target_sources(app PRIVATE central_bas_proxy.c)
endif()
24 changes: 24 additions & 0 deletions app/src/split/bluetooth/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,36 @@ config ZMK_SPLIT_ROLE_CENTRAL
select BT_GATT_AUTO_DISCOVER_CCC
select BT_SCAN_WITH_IDENTITY

# Bump this value needed for concurrent GATT discovery of splits
config BT_L2CAP_TX_BUF_COUNT
default 5 if ZMK_SPLIT_ROLE_CENTRAL

if ZMK_SPLIT_ROLE_CENTRAL

config ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
int "Number of peripherals that will connect to the central."
default 1

menuconfig ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING
bool "Fetch Peripheral Battery Level Info"
help
Adds internal support for fetching the battery levels from peripherals
and generating events in the ZMK eventing system.

if ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING

config ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE
int "Max number of battery level events to queue when received from peripherals"
default ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS

config ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY
bool "Proxy Peripheral Battery Level Info"
help
Adds support for reporting the battery levels of connected split
peripherals through an additional Battery Level service.

endif

config ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE
int "Max number of key position state events to queue when received from peripherals"
default 5
Expand Down
110 changes: 104 additions & 6 deletions app/src/split/bluetooth/central.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/events/battery_state_changed.h>
#include <zmk/hid_indicators_types.h>

static int start_scanning(void);
Expand All @@ -47,6 +48,10 @@ struct peripheral_slot {
struct bt_gatt_subscribe_params sensor_subscribe_params;
struct bt_gatt_discover_params sub_discover_params;
uint16_t run_behavior_handle;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
struct bt_gatt_subscribe_params batt_lvl_subscribe_params;
struct bt_gatt_read_params batt_lvl_read_params;
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
uint16_t update_hid_indicators;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
Expand Down Expand Up @@ -265,6 +270,83 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
return BT_GATT_ITER_CONTINUE;
}

#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

K_MSGQ_DEFINE(peripheral_batt_lvl_msgq, sizeof(struct zmk_peripheral_battery_state_changed),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE, 4);

void peripheral_batt_lvl_change_callback(struct k_work *work) {
struct zmk_peripheral_battery_state_changed ev;
while (k_msgq_get(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT) == 0) {
LOG_DBG("Triggering peripheral battery level change %u", ev.state_of_charge);
ZMK_EVENT_RAISE(new_zmk_peripheral_battery_state_changed(ev));
}
}

K_WORK_DEFINE(peripheral_batt_lvl_work, peripheral_batt_lvl_change_callback);

static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
struct bt_gatt_subscribe_params *params,
const void *data, uint16_t length) {
struct peripheral_slot *slot = peripheral_slot_for_conn(conn);

if (slot == NULL) {
LOG_ERR("No peripheral state found for connection");
return BT_GATT_ITER_CONTINUE;
}

if (!data) {
LOG_DBG("[UNSUBSCRIBED]");
params->value_handle = 0U;
return BT_GATT_ITER_STOP;
}

LOG_DBG("[BATTERY LEVEL NOTIFICATION] data %p length %u", data, length);
uint8_t battery_level = ((uint8_t *)data)[0];
LOG_DBG("Battery level: %u", battery_level);
struct zmk_peripheral_battery_state_changed ev = {
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);

return BT_GATT_ITER_CONTINUE;
}

static uint8_t split_central_battery_level_read_func(struct bt_conn *conn, uint8_t err,
struct bt_gatt_read_params *params,
const void *data, uint16_t length) {
if (err > 0) {
LOG_ERR("Error during reading peripheral battery level: %u", err);
return BT_GATT_ITER_STOP;
}

struct peripheral_slot *slot = peripheral_slot_for_conn(conn);

if (slot == NULL) {
LOG_ERR("No peripheral state found for connection");
return BT_GATT_ITER_CONTINUE;
}

if (!data) {
LOG_DBG("[READ COMPLETED]");
return BT_GATT_ITER_STOP;
}

LOG_DBG("[BATTERY LEVEL READ] data %p length %u", data, length);

uint8_t battery_level = ((uint8_t *)data)[0];

LOG_DBG("Battery level: %u", battery_level);

struct zmk_peripheral_battery_state_changed ev = {.state_of_charge = battery_level};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);

return BT_GATT_ITER_CONTINUE;
}

#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */

static int split_central_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) {
int err = bt_gatt_subscribe(conn, params);
switch (err) {
Expand Down Expand Up @@ -306,10 +388,6 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,

if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID)) == 0) {
LOG_DBG("Found position state characteristic");
slot->discover_params.uuid = NULL;
slot->discover_params.start_handle = attr->handle + 2;
slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;

slot->subscribe_params.disc_params = &slot->sub_discover_params;
slot->subscribe_params.end_handle = slot->discover_params.end_handle;
slot->subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
Expand Down Expand Up @@ -342,16 +420,37 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
LOG_DBG("Found update HID indicators handle");
slot->update_hid_indicators = bt_gatt_attr_value_handle(attr);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
} else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid,
BT_UUID_BAS_BATTERY_LEVEL)) {
LOG_DBG("Found battery level characteristics");
slot->batt_lvl_subscribe_params.disc_params = &slot->sub_discover_params;
slot->batt_lvl_subscribe_params.end_handle = slot->discover_params.end_handle;
slot->batt_lvl_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
slot->batt_lvl_subscribe_params.notify = split_central_battery_level_notify_func;
slot->batt_lvl_subscribe_params.value = BT_GATT_CCC_NOTIFY;
split_central_subscribe(conn, &slot->batt_lvl_subscribe_params);

slot->batt_lvl_read_params.func = split_central_battery_level_read_func;
slot->batt_lvl_read_params.handle_count = 1;
slot->batt_lvl_read_params.single.handle = bt_gatt_attr_value_handle(attr);
slot->batt_lvl_read_params.single.offset = 0;
bt_gatt_read(conn, &slot->batt_lvl_read_params);
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
}

bool subscribed = (slot->run_behavior_handle && slot->subscribe_params.value_handle);
bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle;

#if ZMK_KEYMAP_HAS_SENSORS
subscribed = subscribed && slot->sensor_subscribe_params.value_handle;
#endif /* ZMK_KEYMAP_HAS_SENSORS */

#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
subscribed = subscribed && slot->update_hid_indicators;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
subscribed = subscribed && slot->batt_lvl_subscribe_params.value_handle;
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */

return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE;
}
Expand Down Expand Up @@ -382,7 +481,6 @@ static uint8_t split_central_service_discovery_func(struct bt_conn *conn,
LOG_DBG("Found split service");
slot->discover_params.uuid = NULL;
slot->discover_params.func = split_central_chrc_discovery_func;
slot->discover_params.start_handle = attr->handle + 1;
slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;

int err = bt_gatt_discover(conn, &slot->discover_params);
Expand Down
90 changes: 90 additions & 0 deletions app/src/split/bluetooth/central_bas_proxy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/device.h>
#include <zephyr/init.h>
#include <sys/types.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/bluetooth/gatt.h>

#include <zephyr/logging/log.h>

LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);

#include <zmk/event_manager.h>
#include <zmk/battery.h>
#include <zmk/events/battery_state_changed.h>

// Initialize the charge level to a special value indicating no sampling has been made yet.
static uint8_t last_state_of_peripheral_charge[CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS] = {0};

static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) {
ARG_UNUSED(attr);

bool notif_enabled = (value == BT_GATT_CCC_NOTIFY);

LOG_INF("BAS Notifications %s", notif_enabled ? "enabled" : "disabled");
}

static ssize_t read_blvl(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset) {
const char *lvl8 = attr->user_data;
return bt_gatt_attr_read(conn, attr, buf, len, offset, lvl8, sizeof(uint8_t));
}

static const struct bt_gatt_cpf aux_level_cpf = {
.format = 0x04, // uint8
.exponent = 0x0,
.unit = 0x27AD, // Percentage
.name_space = 0x01, // Bluetooth SIG
.description = 0x0108, // "auxiliary"
};

#define PERIPH_CUD_(x) "Peripheral " #x
#define PERIPH_CUD(x) PERIPH_CUD_(x)

#define PERIPH_BATT_LEVEL_ATTRS(i, _) \
BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \
BT_GATT_PERM_READ, read_blvl, NULL, \
&last_state_of_peripheral_charge[i]), \
BT_GATT_CCC(blvl_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), \
BT_GATT_CPF(&aux_level_cpf), BT_GATT_CUD(PERIPH_CUD(i), BT_GATT_PERM_READ),

BT_GATT_SERVICE_DEFINE(bas_aux, BT_GATT_PRIMARY_SERVICE(BT_UUID_BAS),
LISTIFY(CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS, PERIPH_BATT_LEVEL_ATTRS,
()));

int peripheral_batt_lvl_listener(const zmk_event_t *eh) {
const struct zmk_peripheral_battery_state_changed *ev =
as_zmk_peripheral_battery_state_changed(eh);
if (ev == NULL) {
return ZMK_EV_EVENT_BUBBLE;
};

if (ev->source >= CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS) {
LOG_WRN("Got battery level event for an out of range peripheral index");
return ZMK_EV_EVENT_BUBBLE;
}

LOG_DBG("Peripheral battery level event: %u", ev->state_of_charge);
last_state_of_peripheral_charge[ev->source] = ev->state_of_charge;

// 1 offsets for the service attribute, then offset 5 for each battery because that's how
// many attributes are added per battery
uint8_t index = 1 + (5 * ev->source);

int rc = bt_gatt_notify(NULL, &bas_aux.attrs[index], &last_state_of_peripheral_charge,
sizeof(last_state_of_peripheral_charge));
if (rc < 0 && rc != -ENOTCONN) {
LOG_WRN("Failed to notify hosts of peripheral battery level: %d", rc);
}

return ZMK_EV_EVENT_BUBBLE;
};

ZMK_LISTENER(peripheral_batt_lvl_listener, peripheral_batt_lvl_listener);
ZMK_SUBSCRIPTION(peripheral_batt_lvl_listener, zmk_peripheral_battery_state_changed);
27 changes: 15 additions & 12 deletions docs/docs/config/system.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,18 @@ Note that `CONFIG_BT_MAX_CONN` and `CONFIG_BT_MAX_PAIRED` should be set to the s

Following split keyboard settings are defined in [zmk/app/src/split/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/Kconfig) (generic) and [zmk/app/src/split/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/bluetooth/Kconfig) (bluetooth).

| Config | Type | Description | Default |
| ----------------------------------------------------- | ---- | ------------------------------------------------------------------------ | ------- |
| `CONFIG_ZMK_SPLIT` | bool | Enable split keyboard support | n |
| `CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS` | bool | Enable split keyboard support for passing indicator state to peripherals | n |
| `CONFIG_ZMK_SPLIT_BLE` | bool | Use BLE to communicate between split keyboard halves | y |
| `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` | bool | `y` for central device, `n` for peripheral | |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue when received from peripherals | 5 |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_STACK_SIZE` | int | Stack size of the BLE split central write thread | 512 |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE` | int | Max number of behavior run events to queue to send to the peripheral(s) | 5 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE` | int | Stack size of the BLE split peripheral notify thread | 650 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY` | int | Priority of the BLE split peripheral notify thread | 5 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue to send to the central | 10 |
| Config | Type | Description | Default |
| ------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | ------------------------------------------ |
| `CONFIG_ZMK_SPLIT` | bool | Enable split keyboard support | n |
| `CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS` | bool | Enable split keyboard support for passing indicator state to peripherals | n |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING` | bool | Enable fetching split peripheral battery levels to the central side | n |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY` | bool | Enable central reporting of split battery levels to hosts | n |
| `CONFIG_ZMK_SPLIT_BLE` | bool | Use BLE to communicate between split keyboard halves | y |
| `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` | bool | `y` for central device, `n` for peripheral | |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE` | int | Max number of battery level events to queue when received from peripherals | `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue when received from peripherals | 5 |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_STACK_SIZE` | int | Stack size of the BLE split central write thread | 512 |
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE` | int | Max number of behavior run events to queue to send to the peripheral(s) | 5 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE` | int | Stack size of the BLE split peripheral notify thread | 650 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY` | int | Priority of the BLE split peripheral notify thread | 5 |
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue to send to the central | 10 |

0 comments on commit 05f7cd1

Please sign in to comment.