Skip to content

Commit ede5f0c

Browse files
Katonapetejohanson
andcommitted
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 <[email protected]>
1 parent c965e35 commit ede5f0c

File tree

9 files changed

+299
-21
lines changed

9 files changed

+299
-21
lines changed

app/include/zmk/events/battery_state_changed.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,12 @@ struct zmk_battery_state_changed {
1414
uint8_t state_of_charge;
1515
};
1616

17-
ZMK_EVENT_DECLARE(zmk_battery_state_changed);
17+
ZMK_EVENT_DECLARE(zmk_battery_state_changed);
18+
19+
struct zmk_peripheral_battery_state_changed {
20+
uint8_t source;
21+
// TODO: Other battery channels
22+
uint8_t state_of_charge;
23+
};
24+
25+
ZMK_EVENT_DECLARE(zmk_peripheral_battery_state_changed);

app/include/zmk/split/bluetooth/central.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *bi
1616
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators);
1717

1818
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
19+
20+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
21+
22+
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level);
23+
24+
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

app/src/display/widgets/battery_status.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ void battery_status_update_cb(struct battery_status_state state) {
6363
}
6464

6565
static struct battery_status_state battery_status_get_state(const zmk_event_t *eh) {
66+
const struct zmk_battery_state_changed *ev = as_zmk_battery_state_changed(eh);
6667
return (struct battery_status_state) {
67-
.level = bt_bas_get_battery_level(),
68+
.level = ev->state_of_charge,
6869
#if IS_ENABLED(CONFIG_USB_DEVICE_STACK)
6970
.usb_present = zmk_usb_is_powered(),
7071
#endif /* IS_ENABLED(CONFIG_USB_DEVICE_STACK) */

app/src/events/battery_state_changed.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@
77
#include <zephyr/kernel.h>
88
#include <zmk/events/battery_state_changed.h>
99

10-
ZMK_EVENT_IMPL(zmk_battery_state_changed);
10+
ZMK_EVENT_IMPL(zmk_battery_state_changed);
11+
12+
ZMK_EVENT_IMPL(zmk_peripheral_battery_state_changed);

app/src/split/bluetooth/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
88
endif()
99
if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
1010
target_sources(app PRIVATE central.c)
11+
endif()
12+
13+
if (CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY)
14+
target_sources(app PRIVATE central_bas_proxy.c)
1115
endif()

app/src/split/bluetooth/Kconfig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,36 @@ config ZMK_SPLIT_ROLE_CENTRAL
1616
select BT_GATT_AUTO_DISCOVER_CCC
1717
select BT_SCAN_WITH_IDENTITY
1818

19+
# Bump this value needed for concurrent GATT discovery of splits
20+
config BT_L2CAP_TX_BUF_COUNT
21+
default 5 if ZMK_SPLIT_ROLE_CENTRAL
22+
1923
if ZMK_SPLIT_ROLE_CENTRAL
2024

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

29+
menuconfig ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING
30+
bool "Fetch Peripheral Battery Level Info"
31+
help
32+
Adds internal support for fetching the battery levels from peripherals
33+
and generating events in the ZMK eventing system.
34+
35+
if ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING
36+
37+
config ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE
38+
int "Max number of battery level events to queue when received from peripherals"
39+
default ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
40+
41+
config ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY
42+
bool "Proxy Peripheral Battery Level Info"
43+
help
44+
Adds support for reporting the battery levels of connected split
45+
peripherals through an additional Battery Level service.
46+
47+
endif
48+
2549
config ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE
2650
int "Max number of key position state events to queue when received from peripherals"
2751
default 5

app/src/split/bluetooth/central.c

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
2727
#include <zmk/event_manager.h>
2828
#include <zmk/events/position_state_changed.h>
2929
#include <zmk/events/sensor_event.h>
30+
#include <zmk/events/battery_state_changed.h>
3031
#include <zmk/hid_indicators_types.h>
3132

3233
static int start_scanning(void);
@@ -47,6 +48,10 @@ struct peripheral_slot {
4748
struct bt_gatt_subscribe_params sensor_subscribe_params;
4849
struct bt_gatt_discover_params sub_discover_params;
4950
uint16_t run_behavior_handle;
51+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
52+
struct bt_gatt_subscribe_params batt_lvl_subscribe_params;
53+
struct bt_gatt_read_params batt_lvl_read_params;
54+
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
5055
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
5156
uint16_t update_hid_indicators;
5257
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
@@ -265,6 +270,110 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
265270
return BT_GATT_ITER_CONTINUE;
266271
}
267272

273+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
274+
275+
static uint8_t peripheral_battery_levels[ZMK_SPLIT_BLE_PERIPHERAL_COUNT] = {0};
276+
277+
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
278+
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
279+
return -EINVAL;
280+
}
281+
282+
if (peripherals[source].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
283+
return -ENOTCONN;
284+
}
285+
286+
*level = peripheral_battery_levels[source];
287+
return 0;
288+
}
289+
290+
K_MSGQ_DEFINE(peripheral_batt_lvl_msgq, sizeof(struct zmk_peripheral_battery_state_changed),
291+
CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE, 4);
292+
293+
void peripheral_batt_lvl_change_callback(struct k_work *work) {
294+
struct zmk_peripheral_battery_state_changed ev;
295+
while (k_msgq_get(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT) == 0) {
296+
LOG_DBG("Triggering peripheral battery level change %u", ev.state_of_charge);
297+
peripheral_battery_levels[ev.source] = ev.state_of_charge;
298+
ZMK_EVENT_RAISE(new_zmk_peripheral_battery_state_changed(ev));
299+
}
300+
}
301+
302+
K_WORK_DEFINE(peripheral_batt_lvl_work, peripheral_batt_lvl_change_callback);
303+
304+
static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
305+
struct bt_gatt_subscribe_params *params,
306+
const void *data, uint16_t length) {
307+
struct peripheral_slot *slot = peripheral_slot_for_conn(conn);
308+
309+
if (!slot) {
310+
LOG_ERR("No peripheral state found for connection");
311+
return BT_GATT_ITER_CONTINUE;
312+
}
313+
314+
if (!data) {
315+
LOG_DBG("[UNSUBSCRIBED]");
316+
params->value_handle = 0U;
317+
return BT_GATT_ITER_STOP;
318+
}
319+
320+
if (length == 0) {
321+
LOG_ERR("Zero length battery notification received");
322+
return BT_GATT_ITER_CONTINUE;
323+
}
324+
325+
LOG_DBG("[BATTERY LEVEL NOTIFICATION] data %p length %u", data, length);
326+
uint8_t battery_level = ((uint8_t *)data)[0];
327+
LOG_DBG("Battery level: %u", battery_level);
328+
struct zmk_peripheral_battery_state_changed ev = {
329+
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
330+
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
331+
k_work_submit(&peripheral_batt_lvl_work);
332+
333+
return BT_GATT_ITER_CONTINUE;
334+
}
335+
336+
static uint8_t split_central_battery_level_read_func(struct bt_conn *conn, uint8_t err,
337+
struct bt_gatt_read_params *params,
338+
const void *data, uint16_t length) {
339+
if (err > 0) {
340+
LOG_ERR("Error during reading peripheral battery level: %u", err);
341+
return BT_GATT_ITER_STOP;
342+
}
343+
344+
struct peripheral_slot *slot = peripheral_slot_for_conn(conn);
345+
346+
if (!slot) {
347+
LOG_ERR("No peripheral state found for connection");
348+
return BT_GATT_ITER_CONTINUE;
349+
}
350+
351+
if (!data) {
352+
LOG_DBG("[READ COMPLETED]");
353+
return BT_GATT_ITER_STOP;
354+
}
355+
356+
LOG_DBG("[BATTERY LEVEL READ] data %p length %u", data, length);
357+
358+
if (length == 0) {
359+
LOG_ERR("Zero length battery notification received");
360+
return BT_GATT_ITER_CONTINUE;
361+
}
362+
363+
uint8_t battery_level = ((uint8_t *)data)[0];
364+
365+
LOG_DBG("Battery level: %u", battery_level);
366+
367+
struct zmk_peripheral_battery_state_changed ev = {
368+
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
369+
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
370+
k_work_submit(&peripheral_batt_lvl_work);
371+
372+
return BT_GATT_ITER_CONTINUE;
373+
}
374+
375+
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
376+
268377
static int split_central_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) {
269378
int err = bt_gatt_subscribe(conn, params);
270379
switch (err) {
@@ -306,10 +415,6 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
306415

307416
if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID)) == 0) {
308417
LOG_DBG("Found position state characteristic");
309-
slot->discover_params.uuid = NULL;
310-
slot->discover_params.start_handle = attr->handle + 2;
311-
slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
312-
313418
slot->subscribe_params.disc_params = &slot->sub_discover_params;
314419
slot->subscribe_params.end_handle = slot->discover_params.end_handle;
315420
slot->subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
@@ -342,16 +447,37 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
342447
LOG_DBG("Found update HID indicators handle");
343448
slot->update_hid_indicators = bt_gatt_attr_value_handle(attr);
344449
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
450+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
451+
} else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid,
452+
BT_UUID_BAS_BATTERY_LEVEL)) {
453+
LOG_DBG("Found battery level characteristics");
454+
slot->batt_lvl_subscribe_params.disc_params = &slot->sub_discover_params;
455+
slot->batt_lvl_subscribe_params.end_handle = slot->discover_params.end_handle;
456+
slot->batt_lvl_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
457+
slot->batt_lvl_subscribe_params.notify = split_central_battery_level_notify_func;
458+
slot->batt_lvl_subscribe_params.value = BT_GATT_CCC_NOTIFY;
459+
split_central_subscribe(conn, &slot->batt_lvl_subscribe_params);
460+
461+
slot->batt_lvl_read_params.func = split_central_battery_level_read_func;
462+
slot->batt_lvl_read_params.handle_count = 1;
463+
slot->batt_lvl_read_params.single.handle = bt_gatt_attr_value_handle(attr);
464+
slot->batt_lvl_read_params.single.offset = 0;
465+
bt_gatt_read(conn, &slot->batt_lvl_read_params);
466+
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
345467
}
346468

347-
bool subscribed = (slot->run_behavior_handle && slot->subscribe_params.value_handle);
469+
bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle;
470+
348471
#if ZMK_KEYMAP_HAS_SENSORS
349472
subscribed = subscribed && slot->sensor_subscribe_params.value_handle;
350473
#endif /* ZMK_KEYMAP_HAS_SENSORS */
351474

352475
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
353476
subscribed = subscribed && slot->update_hid_indicators;
354477
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
478+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
479+
subscribed = subscribed && slot->batt_lvl_subscribe_params.value_handle;
480+
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
355481

356482
return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE;
357483
}
@@ -382,7 +508,6 @@ static uint8_t split_central_service_discovery_func(struct bt_conn *conn,
382508
LOG_DBG("Found split service");
383509
slot->discover_params.uuid = NULL;
384510
slot->discover_params.func = split_central_chrc_discovery_func;
385-
slot->discover_params.start_handle = attr->handle + 1;
386511
slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
387512

388513
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) {
605730

606731
LOG_DBG("Disconnected: %s (reason %d)", addr, reason);
607732

733+
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
734+
struct zmk_peripheral_battery_state_changed ev = {
735+
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
736+
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
737+
k_work_submit(&peripheral_batt_lvl_work);
738+
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
739+
608740
err = release_peripheral_slot_for_conn(conn);
609741

610742
if (err < 0) {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2020 The ZMK Contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
7+
#include <zephyr/device.h>
8+
#include <zephyr/init.h>
9+
#include <sys/types.h>
10+
#include <zephyr/kernel.h>
11+
#include <zephyr/drivers/sensor.h>
12+
#include <zephyr/bluetooth/gatt.h>
13+
14+
#include <zephyr/logging/log.h>
15+
16+
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
17+
18+
#include <zmk/event_manager.h>
19+
#include <zmk/battery.h>
20+
#include <zmk/events/battery_state_changed.h>
21+
#include <zmk/split/bluetooth/central.h>
22+
23+
static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) {
24+
ARG_UNUSED(attr);
25+
26+
bool notif_enabled = (value == BT_GATT_CCC_NOTIFY);
27+
28+
LOG_INF("BAS Notifications %s", notif_enabled ? "enabled" : "disabled");
29+
}
30+
31+
static ssize_t read_blvl(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
32+
uint16_t len, uint16_t offset) {
33+
const uint8_t source = (uint8_t)(uint32_t)attr->user_data;
34+
uint8_t level = 0;
35+
int rc = zmk_split_get_peripheral_battery_level(source, &level);
36+
37+
if (rc == -EINVAL) {
38+
LOG_ERR("Invalid peripheral index requested for battery level read: %d", source);
39+
return 0;
40+
}
41+
42+
return bt_gatt_attr_read(conn, attr, buf, len, offset, &level, sizeof(uint8_t));
43+
}
44+
45+
static const struct bt_gatt_cpf aux_level_cpf = {
46+
.format = 0x04, // uint8
47+
.exponent = 0x0,
48+
.unit = 0x27AD, // Percentage
49+
.name_space = 0x01, // Bluetooth SIG
50+
.description = 0x0108, // "auxiliary"
51+
};
52+
53+
#define PERIPH_CUD_(x) "Peripheral " #x
54+
#define PERIPH_CUD(x) PERIPH_CUD_(x)
55+
56+
// How many GATT attributes each battery level adds to our service
57+
#define PERIPH_BATT_LEVEL_ATTR_COUNT 5
58+
// The second generated attribute is the one used to send GATT notifications
59+
#define PERIPH_BATT_LEVEL_ATTR_NOTIFY_IDX 1
60+
61+
#define PERIPH_BATT_LEVEL_ATTRS(i, _) \
62+
BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, \
63+
BT_GATT_PERM_READ, read_blvl, NULL, i), \
64+
BT_GATT_CCC(blvl_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE), \
65+
BT_GATT_CPF(&aux_level_cpf), BT_GATT_CUD(PERIPH_CUD(i), BT_GATT_PERM_READ),
66+
67+
BT_GATT_SERVICE_DEFINE(bas_aux, BT_GATT_PRIMARY_SERVICE(BT_UUID_BAS),
68+
LISTIFY(CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS, PERIPH_BATT_LEVEL_ATTRS,
69+
()));
70+
71+
int peripheral_batt_lvl_listener(const zmk_event_t *eh) {
72+
const struct zmk_peripheral_battery_state_changed *ev =
73+
as_zmk_peripheral_battery_state_changed(eh);
74+
if (ev == NULL) {
75+
return ZMK_EV_EVENT_BUBBLE;
76+
};
77+
78+
if (ev->source >= CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS) {
79+
LOG_WRN("Got battery level event for an out of range peripheral index");
80+
return ZMK_EV_EVENT_BUBBLE;
81+
}
82+
83+
LOG_DBG("Peripheral battery level event: %u", ev->state_of_charge);
84+
85+
// Offset by the index of the source plus the specific offset to find the attribute to notify
86+
// on.
87+
int index = (PERIPH_BATT_LEVEL_ATTR_COUNT * ev->source) + PERIPH_BATT_LEVEL_ATTR_NOTIFY_IDX;
88+
89+
int rc = bt_gatt_notify(NULL, &bas_aux.attrs[index], &ev->state_of_charge, sizeof(uint8_t));
90+
if (rc < 0 && rc != -ENOTCONN) {
91+
LOG_WRN("Failed to notify hosts of peripheral battery level: %d", rc);
92+
}
93+
94+
return ZMK_EV_EVENT_BUBBLE;
95+
};
96+
97+
ZMK_LISTENER(peripheral_batt_lvl_listener, peripheral_batt_lvl_listener);
98+
ZMK_SUBSCRIPTION(peripheral_batt_lvl_listener, zmk_peripheral_battery_state_changed);

0 commit comments

Comments
 (0)