feat(ble): Support perhipheral battery levels.

* 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 <peter@peterjohanson.com>
This commit is contained in:
Gabor Hornyak 2022-03-29 21:20:52 +00:00 committed by Pete Johanson
parent d35311af97
commit 0e2f94b73b
9 changed files with 299 additions and 21 deletions

View file

@ -15,3 +15,11 @@ struct 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);

View file

@ -16,3 +16,9 @@ int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *bi
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

View file

@ -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) */

View file

@ -8,3 +8,5 @@
#include <zmk/events/battery_state_changed.h>
ZMK_EVENT_IMPL(zmk_battery_state_changed);
ZMK_EVENT_IMPL(zmk_peripheral_battery_state_changed);

View file

@ -9,3 +9,7 @@ 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()

View file

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

View file

@ -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);
@ -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)
@ -265,6 +270,110 @@ 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)
static uint8_t peripheral_battery_levels[ZMK_SPLIT_BLE_PERIPHERAL_COUNT] = {0};
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
return -EINVAL;
}
if (peripherals[source].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
return -ENOTCONN;
}
*level = peripheral_battery_levels[source];
return 0;
}
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);
peripheral_battery_levels[ev.source] = 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) {
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;
}
if (length == 0) {
LOG_ERR("Zero length battery notification received");
return BT_GATT_ITER_CONTINUE;
}
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) {
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);
if (length == 0) {
LOG_ERR("Zero length battery notification received");
return BT_GATT_ITER_CONTINUE;
}
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;
}
#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) {
@ -306,10 +415,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);
@ -342,9 +447,27 @@ 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 */
@ -352,6 +475,9 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
#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;
}
@ -382,7 +508,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);
@ -605,6 +730,13 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
LOG_DBG("Disconnected: %s (reason %d)", addr, reason);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
struct zmk_peripheral_battery_state_changed ev = {
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
k_work_submit(&peripheral_batt_lvl_work);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
err = release_peripheral_slot_for_conn(conn);
if (err < 0) {

View file

@ -0,0 +1,98 @@
/*
* 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>
#include <zmk/split/bluetooth/central.h>
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 uint8_t source = (uint8_t)(uint32_t)attr->user_data;
uint8_t level = 0;
int rc = zmk_split_get_peripheral_battery_level(source, &level);
if (rc == -EINVAL) {
LOG_ERR("Invalid peripheral index requested for battery level read: %d", source);
return 0;
}
return bt_gatt_attr_read(conn, attr, buf, len, offset, &level, 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)
// How many GATT attributes each battery level adds to our service
#define PERIPH_BATT_LEVEL_ATTR_COUNT 5
// The second generated attribute is the one used to send GATT notifications
#define PERIPH_BATT_LEVEL_ATTR_NOTIFY_IDX 1
#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, 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);
// Offset by the index of the source plus the specific offset to find the attribute to notify
// on.
int index = (PERIPH_BATT_LEVEL_ATTR_COUNT * ev->source) + PERIPH_BATT_LEVEL_ATTR_NOTIFY_IDX;
int rc = bt_gatt_notify(NULL, &bas_aux.attrs[index], &ev->state_of_charge, sizeof(uint8_t));
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);

View file

@ -105,11 +105,14 @@ 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_ROLE_CENTRAL` | bool | `y` for central device, `n` for peripheral | |
| `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_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_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 |