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 Jan 3, 2024
1 parent d35311a commit 0e2f94b
Show file tree
Hide file tree
Showing 9 changed files with 299 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);
6 changes: 6 additions & 0 deletions app/include/zmk/split/bluetooth/central.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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
144 changes: 138 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,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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -342,16 +447,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 +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);
Expand Down Expand Up @@ -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) {
Expand Down
98 changes: 98 additions & 0 deletions app/src/split/bluetooth/central_bas_proxy.c
Original file line number Diff line number Diff line change
@@ -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);
Loading

0 comments on commit 0e2f94b

Please sign in to comment.