From 11467448b99c0f4c7352b6a83898beb89d42f911 Mon Sep 17 00:00:00 2001 From: Peter Johanson Date: Tue, 15 Oct 2024 13:53:00 -0600 Subject: [PATCH] feat: Add input split support. --- app/CMakeLists.txt | 2 +- app/dts/bindings/zmk,input-split.yaml | 15 ++ app/include/zmk/mouse/input_split.h | 10 + app/include/zmk/split/bluetooth/service.h | 9 + app/include/zmk/split/bluetooth/uuid.h | 1 + app/src/mouse/CMakeLists.txt | 5 +- app/src/mouse/Kconfig | 35 ++- app/src/mouse/input_split.c | 69 +++++ app/src/split/bluetooth/central.c | 311 ++++++++++++++++++---- app/src/split/bluetooth/service.c | 70 ++++- 10 files changed, 464 insertions(+), 63 deletions(-) create mode 100644 app/dts/bindings/zmk,input-split.yaml create mode 100644 app/include/zmk/mouse/input_split.h create mode 100644 app/src/mouse/input_split.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 7ca7814d9e2..49b42420f00 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -42,9 +42,9 @@ target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/events/usb_conn_sta target_sources(app PRIVATE src/behaviors/behavior_reset.c) target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/behaviors/behavior_ext_power.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SOFT_OFF app PRIVATE src/behaviors/behavior_soft_off.c) +add_subdirectory_ifdef(CONFIG_ZMK_MOUSE src/mouse/) if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/hid.c) - add_subdirectory_ifdef(CONFIG_ZMK_MOUSE src/mouse/) target_sources(app PRIVATE src/behaviors/behavior_key_press.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_TOGGLE app PRIVATE src/behaviors/behavior_key_toggle.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_HOLD_TAP app PRIVATE src/behaviors/behavior_hold_tap.c) diff --git a/app/dts/bindings/zmk,input-split.yaml b/app/dts/bindings/zmk,input-split.yaml new file mode 100644 index 00000000000..bc2ca26f804 --- /dev/null +++ b/app/dts/bindings/zmk,input-split.yaml @@ -0,0 +1,15 @@ +include: [base.yaml] + +compatible: "zmk,input-split" + +description: Device to wire up an input device for split use. + +properties: + reg: + required: true + + device: + type: phandle + + input-processors: + type: phandle-array diff --git a/app/include/zmk/mouse/input_split.h b/app/include/zmk/mouse/input_split.h new file mode 100644 index 00000000000..812035477e2 --- /dev/null +++ b/app/include/zmk/mouse/input_split.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t code, int32_t value, + bool sync); \ No newline at end of file diff --git a/app/include/zmk/split/bluetooth/service.h b/app/include/zmk/split/bluetooth/service.h index 1c9e75226ad..90e5dd7ee8e 100644 --- a/app/include/zmk/split/bluetooth/service.h +++ b/app/include/zmk/split/bluetooth/service.h @@ -31,8 +31,17 @@ struct zmk_split_run_behavior_payload { char behavior_dev[ZMK_SPLIT_RUN_BEHAVIOR_DEV_LEN]; } __packed; +struct zmk_split_input_event_payload { + uint8_t type; + uint16_t code; + uint32_t value; + uint8_t sync; +} __packed; + int zmk_split_bt_position_pressed(uint8_t position); int zmk_split_bt_position_released(uint8_t position); int zmk_split_bt_sensor_triggered(uint8_t sensor_index, const struct zmk_sensor_channel_data channel_data[], size_t channel_data_size); + +int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync); diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index 4a653c73b83..c9a63efa702 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -19,3 +19,4 @@ #define ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID ZMK_BT_SPLIT_UUID(0x00000003) #define ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID ZMK_BT_SPLIT_UUID(0x00000004) #define ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID ZMK_BT_SPLIT_UUID(0x00000005) +#define ZMK_SPLIT_BT_INPUT_EVENT_UUID ZMK_BT_SPLIT_UUID(0x00000006) diff --git a/app/src/mouse/CMakeLists.txt b/app/src/mouse/CMakeLists.txt index ff7039c0ec2..fdee57c8540 100644 --- a/app/src/mouse/CMakeLists.txt +++ b/app/src/mouse/CMakeLists.txt @@ -1,9 +1,10 @@ # Copyright (c) 2024 The ZMK Contributors # SPDX-License-Identifier: MIT -target_sources(app PRIVATE input_listener.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_LISTENER app PRIVATE input_listener.c) target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_TRANSFORM app PRIVATE input_processor_transform.c) target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_SCALER app PRIVATE input_processor_scaler.c) target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_TEMP_LAYER app PRIVATE input_processor_temp_layer.c) target_sources_ifdef(CONFIG_ZMK_INPUT_PROCESSOR_CODE_MAPPER app PRIVATE input_processor_code_mapper.c) -target_sources_ifdef(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING app PRIVATE resolution_multipliers.c) \ No newline at end of file +target_sources_ifdef(CONFIG_ZMK_MOUSE_SMOOTH_SCROLLING app PRIVATE resolution_multipliers.c) +target_sources_ifdef(CONFIG_ZMK_INPUT_SPLIT app PRIVATE input_split.c) \ No newline at end of file diff --git a/app/src/mouse/Kconfig b/app/src/mouse/Kconfig index 547707b99f9..29695628f1d 100644 --- a/app/src/mouse/Kconfig +++ b/app/src/mouse/Kconfig @@ -12,11 +12,29 @@ if ZMK_MOUSE config INPUT_GPIO_KEYS default n +config INPUT_THREAD_STACK_SIZE + default 1024 if ZMK_SPLIT && !ZMK_SPLIT_ROLE_CENTRAL + +if !ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL + config ZMK_MOUSE_SMOOTH_SCROLLING bool "Smooth Scrolling" help Enable smooth scrolling, with hosts that support HID Resolution Multipliers +config ZMK_INPUT_LISTENER + bool "Input listener for processing input events in the system" + default y + depends on DT_HAS_ZMK_INPUT_LISTENER_ENABLED + + +config ZMK_INPUT_PROCESSOR_TEMP_LAYER + bool "Temporary Layer Input Processor" + default y + depends on DT_HAS_ZMK_INPUT_PROCESSOR_TEMP_LAYER_ENABLED + +endif + config ZMK_INPUT_PROCESSOR_TRANSFORM bool "Transform Input Processor" default y @@ -27,15 +45,22 @@ config ZMK_INPUT_PROCESSOR_SCALER default y depends on DT_HAS_ZMK_INPUT_PROCESSOR_SCALER_ENABLED -config ZMK_INPUT_PROCESSOR_TEMP_LAYER - bool "Temporary Layer Input Processor" - default y - depends on DT_HAS_ZMK_INPUT_PROCESSOR_TEMP_LAYER_ENABLED - config ZMK_INPUT_PROCESSOR_CODE_MAPPER bool "Code Mapper Input Processor" default y depends on DT_HAS_ZMK_INPUT_PROCESSOR_CODE_MAPPER_ENABLED +config ZMK_INPUT_SPLIT + bool "Split input support" + default y + depends on DT_HAS_ZMK_INPUT_SPLIT_ENABLED && ZMK_SPLIT + +if ZMK_INPUT_SPLIT + +config ZMK_INPUT_SPLIT_INIT_PRIORITY + int "Input Split initialization priority" + default INPUT_INIT_PRIORITY + +endif # ZMK_INPUT_SPLIT endif diff --git a/app/src/mouse/input_split.c b/app/src/mouse/input_split.c new file mode 100644 index 00000000000..55a84ea4624 --- /dev/null +++ b/app/src/mouse/input_split.c @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_input_split + +#include +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + +struct zis_entry { + uint8_t reg; + const struct device *dev; +}; + +#define ZIS_ENTRY(n) {.reg = DT_INST_REG_ADDR(n), .dev = DEVICE_DT_GET(DT_DRV_INST(n))}, + +static const struct zis_entry proxy_inputs[] = {DT_INST_FOREACH_STATUS_OKAY(ZIS_ENTRY)}; + +int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t code, int32_t value, + bool sync) { + LOG_DBG("Got peripheral event for %d!", reg); + for (size_t i = 0; i < ARRAY_SIZE(proxy_inputs); i++) { + if (reg == proxy_inputs[i].reg) { + return input_report(proxy_inputs[i].dev, type, code, value, sync, K_NO_WAIT); + } + } + + return -ENODEV; +} + +#define ZIS_INST(n) \ + DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, NULL, POST_KERNEL, \ + CONFIG_ZMK_INPUT_SPLIT_INIT_PRIORITY, NULL); + +#else + +#include + +#define ZIS_INST(n) \ + static const struct zmk_input_processor_entry processors_##n[] = \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, input_processors), \ + ({LISTIFY(DT_INST_PROP_LEN(n, input_processors), \ + ZMK_INPUT_PROCESSOR_ENTRY_AT_IDX, (, ), DT_DRV_INST(n))}), \ + ({})); \ + BUILD_ASSERT(DT_INST_NODE_HAS_PROP(n, device), \ + "Peripheral input splits need an `input` property set"); \ + void split_input_handler_##n(struct input_event *evt) { \ + for (size_t i = 0; i < ARRAY_SIZE(processors_##n); i++) { \ + zmk_input_processor_handle_event(processors_##n[i].dev, evt, processors_##n[i].param1, \ + processors_##n[i].param2, NULL); \ + } \ + zmk_split_bt_report_input(DT_INST_REG_ADDR(n), evt->type, evt->code, evt->value, \ + evt->sync); \ + } \ + INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), split_input_handler_##n); + +#endif + +DT_INST_FOREACH_STATUS_OKAY(ZIS_INST) \ No newline at end of file diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 21ff611f568..d2120fd4a5c 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -29,6 +29,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #include #include @@ -62,6 +63,71 @@ struct peripheral_slot { uint8_t changed_positions[POSITION_STATE_DATA_LEN]; }; +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + +static const struct bt_uuid *gatt_ccc_uuid = BT_UUID_GATT_CCC; +static const struct bt_uuid *gatt_cpf_uuid = BT_UUID_GATT_CPF; + +struct peripheral_input_slot { + struct bt_conn *conn; + struct bt_gatt_subscribe_params sub; + uint8_t reg; +}; + +#define COUNT_INPUT_SPLIT(n) +1 + +static struct peripheral_input_slot + peripheral_input_slots[(0 DT_FOREACH_STATUS_OKAY(zmk_input_split, COUNT_INPUT_SPLIT))]; + +static bool input_slot_is_open(size_t i) { + return i < ARRAY_SIZE(peripheral_input_slots) && peripheral_input_slots[i].conn == NULL; +} + +static bool input_slot_is_pending(size_t i) { + return i < ARRAY_SIZE(peripheral_input_slots) && peripheral_input_slots[i].conn != NULL && + (!peripheral_input_slots[i].sub.value_handle || + !peripheral_input_slots[i].sub.ccc_handle || !peripheral_input_slots[i].reg); +} + +static int reserve_next_open_input_slot(struct peripheral_input_slot **slot, struct bt_conn *conn) { + for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) { + if (input_slot_is_open(i)) { + peripheral_input_slots[i].conn = conn; + + // Clear out any previously set values + peripheral_input_slots[i].sub.value_handle = 0; + peripheral_input_slots[i].sub.ccc_handle = 0; + peripheral_input_slots[i].reg = 0; + *slot = &peripheral_input_slots[i]; + return i; + } + } + + return -ENOMEM; +} + +static int find_pending_input_slot(struct peripheral_input_slot **slot, struct bt_conn *conn) { + for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) { + if (peripheral_input_slots[i].conn == conn && input_slot_is_pending(i)) { + *slot = &peripheral_input_slots[i]; + return i; + } + } + + return -ENODEV; +} + +void release_peripheral_input_subs(struct bt_conn *conn) { + for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) { + if (peripheral_input_slots[i].conn == conn) { + peripheral_input_slots[i].conn = NULL; + // memset(&peripheral_input_slots[i], 0, sizeof(struct peripheral_input_slot)); + } + } +} + +#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + static struct peripheral_slot peripherals[ZMK_SPLIT_BLE_PERIPHERAL_COUNT]; static bool is_scanning = false; @@ -230,6 +296,65 @@ static uint8_t split_central_sensor_notify_func(struct bt_conn *conn, } #endif /* ZMK_KEYMAP_HAS_SENSORS */ +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + +struct zmk_input_event_msg { + uint8_t reg; + struct zmk_split_input_event_payload payload; +}; + +K_MSGQ_DEFINE(peripheral_input_event_msgq, sizeof(struct zmk_input_event_msg), 5, 4); +// CONFIG_ZMK_SPLIT_BLE_CENTRAL_INPUT_QUEUE_SIZE, 4); + +void peripheral_input_event_work_callback(struct k_work *work) { + struct zmk_input_event_msg msg; + while (k_msgq_get(&peripheral_input_event_msgq, &msg, K_NO_WAIT) == 0) { + int ret = zmk_input_split_report_peripheral_event( + msg.reg, msg.payload.type, msg.payload.code, msg.payload.value, msg.payload.sync); + if (ret < 0) { + LOG_WRN("Failed to report peripheral event %d", ret); + } + } +} + +K_WORK_DEFINE(input_event_work, peripheral_input_event_work_callback); + +static uint8_t peripheral_input_event_notify_cb(struct bt_conn *conn, + struct bt_gatt_subscribe_params *params, + const void *data, uint16_t length) { + if (!data) { + LOG_DBG("[UNSUBSCRIBED]"); + params->value_handle = 0U; + return BT_GATT_ITER_STOP; + } + + LOG_DBG("[INPUT EVENT] data %p length %u", data, length); + + if (length != sizeof(struct zmk_split_input_event_payload)) { + LOG_WRN("Ignoring input event notify with incorrect data length (%d)", length); + return BT_GATT_ITER_STOP; + } + + struct zmk_input_event_msg msg; + + memcpy(&msg.payload, data, MIN(length, sizeof(struct zmk_split_input_event_payload))); + + LOG_DBG("Got an input event with type %d, code %d, value %d, sync %d", msg.payload.type, + msg.payload.code, msg.payload.value, msg.payload.sync); + + for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) { + if (&peripheral_input_slots[i].sub == params) { + msg.reg = peripheral_input_slots[i].reg; + k_msgq_put(&peripheral_input_event_msgq, &msg, K_NO_WAIT); + k_work_submit(&input_event_work); + } + } + + return BT_GATT_ITER_CONTINUE; +} + +#endif + static uint8_t split_central_notify_func(struct bt_conn *conn, struct bt_gatt_subscribe_params *params, const void *data, uint16_t length) { @@ -379,6 +504,7 @@ static uint8_t split_central_battery_level_read_func(struct bt_conn *conn, uint8 #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) { + atomic_set(params->flags, BT_GATT_SUBSCRIBE_FLAG_NO_RESUB); int err = bt_gatt_subscribe(conn, params); switch (err) { case -EALREADY: @@ -455,64 +581,133 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, } LOG_DBG("[ATTRIBUTE] handle %u", attr->handle); - const struct bt_uuid *chrc_uuid = ((struct bt_gatt_chrc *)attr->user_data)->uuid; - - 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->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); - slot->subscribe_params.notify = split_central_notify_func; - slot->subscribe_params.value = BT_GATT_CCC_NOTIFY; - split_central_subscribe(conn, &slot->subscribe_params); + switch (params->type) { + case BT_GATT_DISCOVER_CHARACTERISTIC: + const struct bt_uuid *chrc_uuid = ((struct bt_gatt_chrc *)attr->user_data)->uuid; + + 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->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); + slot->subscribe_params.notify = split_central_notify_func; + slot->subscribe_params.value = BT_GATT_CCC_NOTIFY; + split_central_subscribe(conn, &slot->subscribe_params); #if ZMK_KEYMAP_HAS_SENSORS - } else if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID)) == - 0) { - slot->discover_params.uuid = NULL; - slot->discover_params.start_handle = attr->handle + 2; - slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; - - slot->sensor_subscribe_params.disc_params = &slot->sub_discover_params; - slot->sensor_subscribe_params.end_handle = slot->discover_params.end_handle; - slot->sensor_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr); - slot->sensor_subscribe_params.notify = split_central_sensor_notify_func; - slot->sensor_subscribe_params.value = BT_GATT_CCC_NOTIFY; - split_central_subscribe(conn, &slot->sensor_subscribe_params); + } else if (bt_uuid_cmp(chrc_uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID)) == 0) { + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 2; + slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + + slot->sensor_subscribe_params.disc_params = &slot->sub_discover_params; + slot->sensor_subscribe_params.end_handle = slot->discover_params.end_handle; + slot->sensor_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr); + slot->sensor_subscribe_params.notify = split_central_sensor_notify_func; + slot->sensor_subscribe_params.value = BT_GATT_CCC_NOTIFY; + split_central_subscribe(conn, &slot->sensor_subscribe_params); #endif /* ZMK_KEYMAP_HAS_SENSORS */ - } else if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID)) == - 0) { - LOG_DBG("Found run behavior handle"); - slot->discover_params.uuid = NULL; - slot->discover_params.start_handle = attr->handle + 2; - slot->run_behavior_handle = bt_gatt_attr_value_handle(attr); - } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, - BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID))) { - LOG_DBG("Found select physical layout handle"); - slot->selected_physical_layout_handle = bt_gatt_attr_value_handle(attr); - k_work_submit(&update_peripherals_selected_layouts_work); +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + } else if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_INPUT_EVENT_UUID)) == + 0) { + LOG_DBG("Found an input characteristic"); + struct peripheral_input_slot *input_slot; + int ret = reserve_next_open_input_slot(&input_slot, conn); + if (ret < 0) { + LOG_WRN("No available slot for peripheral input subscriptions (%d)", ret); + + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 1; + slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + } else { + LOG_DBG("Reserved a slot for the input subscription"); + input_slot->sub.value_handle = bt_gatt_attr_value_handle(attr); + + slot->discover_params.uuid = gatt_ccc_uuid; + slot->discover_params.start_handle = attr->handle; + slot->discover_params.type = BT_GATT_DISCOVER_STD_CHAR_DESC; + } +#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + } else if (bt_uuid_cmp(chrc_uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID)) == 0) { + LOG_DBG("Found run behavior handle"); + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 2; + slot->run_behavior_handle = bt_gatt_attr_value_handle(attr); + } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID))) { + LOG_DBG("Found select physical layout handle"); + slot->selected_physical_layout_handle = bt_gatt_attr_value_handle(attr); + k_work_submit(&update_peripherals_selected_layouts_work); #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) - } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, - BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID))) { - LOG_DBG("Found update HID indicators handle"); - slot->update_hid_indicators = bt_gatt_attr_value_handle(attr); + } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID))) { + 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); + } 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) */ + } + break; + case BT_GATT_DISCOVER_STD_CHAR_DESC: +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + if (bt_uuid_cmp(slot->discover_params.uuid, BT_UUID_GATT_CCC) == 0) { + LOG_DBG("Found input CCC descriptor"); + struct peripheral_input_slot *input_slot; + int ret = find_pending_input_slot(&input_slot, conn); + if (ret < 0) { + LOG_DBG("No pending input slot (%d)", ret); + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 1; + slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + } else { + LOG_DBG("Found pending input slot"); + input_slot->sub.ccc_handle = attr->handle; + + slot->discover_params.uuid = gatt_cpf_uuid; + slot->discover_params.start_handle = attr->handle + 1; + slot->discover_params.type = BT_GATT_DISCOVER_STD_CHAR_DESC; + } + } else if (bt_uuid_cmp(slot->discover_params.uuid, BT_UUID_GATT_CPF) == 0) { + LOG_DBG("Found input CPF descriptor"); + struct bt_gatt_cpf *cpf = attr->user_data; + struct peripheral_input_slot *input_slot; + int ret = find_pending_input_slot(&input_slot, conn); + if (ret < 0) { + LOG_DBG("No pending input slot (%d)", ret); + } else { + LOG_DBG("Found pending input slot"); + input_slot->reg = cpf->description; + input_slot->sub.notify = peripheral_input_event_notify_cb; + input_slot->sub.value = BT_GATT_CCC_NOTIFY; + int err = split_central_subscribe(conn, &input_slot->sub); + if (err < 0) { + LOG_WRN("Failed to subscribe to input notifications %d", err); + } + } + + slot->discover_params.uuid = NULL; + slot->discover_params.start_handle = attr->handle + 1; + slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; + } +#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + break; } bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle && @@ -528,6 +723,14 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, #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) */ +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) { + if (input_slot_is_open(i) || input_slot_is_pending(i)) { + subscribed = false; + break; + } + } +#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE; } @@ -779,6 +982,10 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) { k_work_submit(&peripheral_batt_lvl_work); #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + release_peripheral_input_subs(conn); +#endif + err = release_peripheral_slot_for_conn(conn); if (err < 0) { diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 9529d51613c..def883b0c08 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -175,6 +176,45 @@ static ssize_t split_svc_get_selected_phys_layout(struct bt_conn *conn, return bt_gatt_attr_read(conn, attrs, buf, len, offset, &selected, sizeof(selected)); } +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + +static void split_input_events_ccc(const struct bt_gatt_attr *attr, uint16_t value) { + LOG_DBG("value %d", value); +} + +// Duplicated from Zephyr, since it is internal there +struct gatt_cpf { + uint8_t format; + int8_t exponent; + uint16_t unit; + uint8_t name_space; + uint16_t description; +} __packed; + +ssize_t bt_gatt_attr_read_input_split_cpf(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) { + uint16_t reg = (uint16_t)(uint32_t)attr->user_data; + struct gatt_cpf value; + + value.format = 0x1B; // Struct + value.exponent = 0; + value.unit = sys_cpu_to_le16(0x2700); // Unitless + value.name_space = 0x01; // Bluetooth SIG + value.description = sys_cpu_to_le16(reg); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, &value, sizeof(value)); +} + +#define INPUT_SPLIT_CHARS(node_id) \ + BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_INPUT_EVENT_UUID), \ + BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ_ENCRYPT, NULL, NULL, NULL), \ + BT_GATT_CCC(split_input_events_ccc, \ + BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), \ + BT_GATT_DESCRIPTOR(BT_UUID_GATT_CPF, BT_GATT_PERM_READ, bt_gatt_attr_read_input_split_cpf, \ + NULL, (void *)DT_REG_ADDR(node_id)), + +#endif + BT_GATT_SERVICE_DEFINE( split_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SERVICE_UUID)), BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID), @@ -192,10 +232,11 @@ BT_GATT_SERVICE_DEFINE( split_svc_sensor_state, NULL, &last_sensor_event), BT_GATT_CCC(split_svc_sensor_state_ccc, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), #endif /* ZMK_KEYMAP_HAS_SENSORS */ + DT_FOREACH_STATUS_OKAY(zmk_input_split, INPUT_SPLIT_CHARS) #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) - BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID), - BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL, - split_svc_update_indicators, NULL), + BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID), + BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL, + split_svc_update_indicators, NULL), #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID), BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ, @@ -306,6 +347,29 @@ int zmk_split_bt_sensor_triggered(uint8_t sensor_index, } #endif /* ZMK_KEYMAP_HAS_SENSORS */ +#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + +int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync) { + + for (size_t i = 0; i < split_svc.attr_count; i++) { + if (bt_uuid_cmp(split_svc.attrs[i].uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_INPUT_EVENT_UUID)) == 0 && + (uint8_t)(uint32_t)split_svc.attrs[i + 2].user_data == reg) { + struct zmk_split_input_event_payload payload = { + .type = type, + .code = code, + .value = value, + .sync = sync ? 1 : 0, + }; + + return bt_gatt_notify(NULL, &split_svc.attrs[i], &payload, sizeof(payload)); + } + } + return -ENODEV; +} + +#endif /* IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) */ + static int service_init(void) { static const struct k_work_queue_config queue_config = { .name = "Split Peripheral Notification Queue"};