diff --git a/app/include/zmk/events/battery_state_changed.h b/app/include/zmk/events/battery_state_changed.h index 5a8c625e079..157490d9849 100644 --- a/app/include/zmk/events/battery_state_changed.h +++ b/app/include/zmk/events/battery_state_changed.h @@ -14,4 +14,12 @@ struct zmk_battery_state_changed { uint8_t state_of_charge; }; -ZMK_EVENT_DECLARE(zmk_battery_state_changed); \ No newline at end of file +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); \ No newline at end of file diff --git a/app/src/display/widgets/battery_status.c b/app/src/display/widgets/battery_status.c index e35f890ac6c..feb054db736 100644 --- a/app/src/display/widgets/battery_status.c +++ b/app/src/display/widgets/battery_status.c @@ -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) */ diff --git a/app/src/events/battery_state_changed.c b/app/src/events/battery_state_changed.c index 508ee971d69..ffb4297cdda 100644 --- a/app/src/events/battery_state_changed.c +++ b/app/src/events/battery_state_changed.c @@ -7,4 +7,6 @@ #include #include -ZMK_EVENT_IMPL(zmk_battery_state_changed); \ No newline at end of file +ZMK_EVENT_IMPL(zmk_battery_state_changed); + +ZMK_EVENT_IMPL(zmk_peripheral_battery_state_changed); \ No newline at end of file diff --git a/app/src/split/bluetooth/CMakeLists.txt b/app/src/split/bluetooth/CMakeLists.txt index 241a9b8d8c0..6e0ad617284 100644 --- a/app/src/split/bluetooth/CMakeLists.txt +++ b/app/src/split/bluetooth/CMakeLists.txt @@ -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() \ No newline at end of file diff --git a/app/src/split/bluetooth/Kconfig b/app/src/split/bluetooth/Kconfig index 858e7308fef..4da50528343 100644 --- a/app/src/split/bluetooth/Kconfig +++ b/app/src/split/bluetooth/Kconfig @@ -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 diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 6f3b78ab883..9df004346ce 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -27,6 +27,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #include 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,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) { @@ -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); @@ -342,9 +420,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 +448,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 +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); diff --git a/app/src/split/bluetooth/central_bas_proxy.c b/app/src/split/bluetooth/central_bas_proxy.c new file mode 100644 index 00000000000..90fb6ebfbde --- /dev/null +++ b/app/src/split/bluetooth/central_bas_proxy.c @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +// 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); diff --git a/docs/docs/config/system.md b/docs/docs/config/system.md index 6e834c678b9..432dfd16184 100644 --- a/docs/docs/config/system.md +++ b/docs/docs/config/system.md @@ -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 |