Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(battery): Split battery reporting over BLE GATT #1243

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.... We already have the concept of position events having a "source" (see app/include/zmk/events/position_state_changed.h). If we're now going to have yet another event that is either from local or peripheral device, we might want to pull that out in a more generalized place, and simply supplement the existin battery state changed event w/ a source field as well. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I need some time to understand to how that's implemented/used.

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 @@ -104,15 +104,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 |