├── zephyr └── module.yml ├── dts └── bindings │ ├── zmk,virtual-input.yaml │ └── zmk,split-peripheral-input-relay.yaml ├── include └── zmk │ └── split │ └── input-relay │ ├── event.h │ └── uuid.h ├── CMakeLists.txt ├── Kconfig ├── src ├── virtual_input.c ├── input_relay_peripheral.c └── input_relay_central.c └── README.md /zephyr/module.yml: -------------------------------------------------------------------------------- 1 | build: 2 | cmake: . 3 | kconfig: Kconfig 4 | settings: 5 | dts_root: . 6 | -------------------------------------------------------------------------------- /dts/bindings/zmk,virtual-input.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | description: | 5 | Dummy device as an dummy input event emitter. 6 | 7 | compatible: "zmk,virtual-input" 8 | -------------------------------------------------------------------------------- /include/zmk/split/input-relay/event.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | struct zmk_split_bt_input_relay_event { 10 | uint8_t relay_channel; 11 | uint8_t sync; 12 | uint8_t type; 13 | uint16_t code; 14 | int32_t value; 15 | } __packed; 16 | -------------------------------------------------------------------------------- /dts/bindings/zmk,split-peripheral-input-relay.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 The ZMK Contributors 2 | # SPDX-License-Identifier: MIT 3 | 4 | description: | 5 | Allows defining input relay channels 6 | 7 | compatible: "zmk,split-peripheral-input-relay" 8 | 9 | properties: 10 | relay-channel: 11 | type: int 12 | required: true 13 | device: 14 | type: phandle 15 | required: true 16 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | zephyr_library() 2 | 3 | if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) 4 | zephyr_library_sources(src/input_relay_central.c) 5 | endif() 6 | 7 | if (CONFIG_ZMK_SPLIT AND (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)) 8 | zephyr_library_sources(src/input_relay_peripheral.c) 9 | endif() 10 | 11 | zephyr_library_sources_ifdef(CONFIG_ZMK_VIRTUAL_INPUT src/virtual_input.c) 12 | zephyr_include_directories(include) 13 | 14 | zephyr_include_directories(${APPLICATION_SOURCE_DIR}/include) 15 | -------------------------------------------------------------------------------- /Kconfig: -------------------------------------------------------------------------------- 1 | # Sensor data simulator 2 | # 3 | # Copyright (c) 2019 Nordic Semiconductor 4 | # 5 | # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause 6 | # 7 | 8 | DT_COMPAT_ZMK_SPLT_PERIPHERAL_INPUT_RELAY := zmk,split-peripheral-input-relay 9 | config ZMK_SPLT_PERIPHERAL_INPUT_RELAY 10 | bool 11 | default $(dt_compat_enabled,$(DT_COMPAT_ZMK_SPLT_PERIPHERAL_INPUT_RELAY)) 12 | 13 | DT_COMPAT_ZMK_VIRTUAL_INPUT := zmk,virtual-input 14 | config ZMK_VIRTUAL_INPUT 15 | bool 16 | default $(dt_compat_enabled,$(DT_COMPAT_ZMK_VIRTUAL_INPUT)) 17 | -------------------------------------------------------------------------------- /include/zmk/split/input-relay/uuid.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | /* 12 | Define an alternative BT Split Service UUID [last byte: 0x2b] 13 | Main BT Split Service UUID retained unchanged [last byte: 0x2a] 14 | */ 15 | #define ZMK_BT_SPLIT_IR_UUID(num) BT_UUID_128_ENCODE(num, 0x0096, 0x7107, 0xc967, 0xc5cfb1c2482b) 16 | 17 | #define ZMK_SPLIT_BT_IR_SERVICE_UUID ZMK_BT_SPLIT_IR_UUID(0x00000000) 18 | #define ZMK_SPLIT_BT_IR_CHAR_INPUT_STATE_UUID ZMK_BT_SPLIT_IR_UUID(0x00000001) 19 | -------------------------------------------------------------------------------- /src/virtual_input.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #define DT_DRV_COMPAT zmk_virtual_input 8 | 9 | #include 10 | #include 11 | 12 | #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) 13 | 14 | #define VIRT_DEFINE(n) \ 15 | DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, NULL, POST_KERNEL, \ 16 | CONFIG_SENSOR_INIT_PRIORITY, NULL); 17 | 18 | DT_INST_FOREACH_STATUS_OKAY(VIRT_DEFINE) 19 | 20 | #endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZMK Split Peripheral Input Relay 2 | 3 | This module add a input relay to input subsystem for ZMK. 4 | 5 | > [!IMPORTANT] 6 | > UPDATE (2024-12-12): ZMK [PR #2477](https://github.com/zmkfirmware/zmk/pull/2477) has been merged into main branch. This module is no longer needed. 7 | > The feature details is available on ZMK Hardware Integration for [pointing devices on split peripherals](https://zmk.dev/docs/development/hardware-integration/pointing#split). 8 | 9 | ## What it does 10 | 11 | This module sideload a new set of GATT Service and Characteratics into existing split bt paired connection. The new characteristics allow to transfer input event from peripherals to central with a relay-channel id. Then, input events would re-emitted from a `zmk,virtual-device` on central. The events from peripherals could be handled by `zmk,input-listener` on central side. 12 | 13 | In short, user can read more than one pointing device on central and peripherals sheild simultaneously. 14 | 15 | You might also want to checkout [this module](https://github.com/badjeff/zmk-split-peripheral-output-relay) that provides opposite directional proxying for output. 16 | 17 | ## Installation 18 | 19 | Include this project on your ZMK's west manifest in `config/west.yml`: 20 | 21 | ```yaml 22 | manifest: 23 | remotes: 24 | # ... 25 | - name: badjeff 26 | url-base: https://github.com/badjeff 27 | projects: 28 | # ... 29 | # START ##### 30 | - name: zmk-split-peripheral-input-relay 31 | remote: badjeff 32 | revision: main 33 | # END ####### 34 | # ... 35 | ``` 36 | 37 | Update `board.keymap`: 38 | ```keymap 39 | / { 40 | /* assign `input-listener` to all pointing devices */ 41 | /* &pd0 on central, &pd1 on peripheral */ 42 | 43 | trackball_central_listener { 44 | compatible = "zmk,input-listener"; 45 | device = <&pd0>; 46 | }; 47 | 48 | trackball_peripheral_listener { 49 | compatible = "zmk,input-listener"; 50 | device = <&pd1>; 51 | }; 52 | }/ 53 | ``` 54 | 55 | Update split peripheral devicetree file `board_right.overlay`: 56 | ```dts 57 | /* enable &pd0 on split peripheral. typical input device for zephyr input subsystem. */ 58 | /* NOTE 1: use the same device alias (&pd0) on central and peripheral(s) is alright. */ 59 | /* NOTE 2: input event shall be intecepted by module `zmk-split-peripheral-input-relay`. */ 60 | 61 | /* this is an alias of your actual wired input device sensor in your peripheral shield. */ 62 | /* e.g. SPI optical sensor for trackball */ 63 | &pd0 { 64 | status = "okay"; 65 | /* the rest of sensor config should be config here, e.g. gpios */ 66 | }; 67 | 68 | / { 69 | /* THIS make keymap binding happy only, nothing happen on peripheral side */ 70 | pd1: virtual_input { 71 | compatible = "zmk,virtual-input"; 72 | }; 73 | 74 | /* for peripheral side, define (input-device)-to-(relay-channel) mapping */ 75 | input_relay_config_102 { 76 | compatible = "zmk,split-peripheral-input-relay"; 77 | 78 | /* peripheral side input device, used to... */ 79 | /* - be intecepted on peripheral; */ 80 | /* - and then, be resurrected as `zmk,virtual-device` on central; */ 81 | device = <&pd0>; 82 | 83 | /* channel id, used to be be transfered along with all input events. */ 84 | /* NOTE 1: pick any 8bit integer. (1 - 255) */ 85 | /* NOTE 2: should matching relay-channel on central overlay */ 86 | relay-channel = <102>; 87 | }; 88 | }; 89 | ``` 90 | 91 | Update split central devicetree file `board_left.overlay`: 92 | ```dts 93 | /* enable &pd0 on central. typical input device for zephyr input subsystem. */ 94 | /* NOTE 1: use the same device alias (&pd0) on central and peripheral(s) is alright. */ 95 | 96 | /* this is an alias of your actual wired input device sensor in your central shield. */ 97 | /* e.g. SPI optical sensor for trackball */ 98 | &pd0 { 99 | status = "okay"; 100 | /* the rest of sensor config should be config here, e.g. gpios */ 101 | }; 102 | 103 | / { 104 | /* define virtual input, will be resurrected for emitting input event */ 105 | /* NOTE: set `device = <&pd1>` in `zmk,input-listener` */ 106 | pd1: virtual_input { 107 | compatible = "zmk,virtual-input"; 108 | }; 109 | 110 | /* for central side, define (relay-channel)-to-(virtual-input) mapping */ 111 | input_relay_config_102 { 112 | compatible = "zmk,split-peripheral-input-relay"; 113 | 114 | /* channel id, used to filter incoming input event from split peripheral */ 115 | /* NOTE: should matching relay-channel on peripheral overlay */ 116 | relay-channel = <102>; 117 | 118 | /* virtual input device on central, which used to emit input event as an agent device */ 119 | device = <&pd1>; 120 | }; 121 | }; 122 | 123 | ``` 124 | 125 | Enable the input config in your all `_{ left | right }.config`: 126 | ```conf 127 | CONFIG_INPUT=y 128 | /* plus, other input device drive config */ 129 | ``` 130 | 131 | -------------------------------------------------------------------------------- /src/input_relay_peripheral.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #define DT_DRV_COMPAT zmk_split_peripheral_input_relay 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #if CONFIG_INPUT 26 | 27 | struct zmk_split_bt_input_relay_event last_input_event; 28 | 29 | static ssize_t split_svc_input_state(struct bt_conn *conn, const struct bt_gatt_attr *attrs, 30 | void *buf, uint16_t len, uint16_t offset) { 31 | return bt_gatt_attr_read(conn, attrs, buf, len, offset, &last_input_event, 32 | sizeof(last_input_event)); 33 | } 34 | 35 | static void split_svc_input_state_ccc(const struct bt_gatt_attr *attr, uint16_t value) { 36 | LOG_DBG("value %d", value); 37 | } 38 | #endif /* CONFIG_INPUT */ 39 | 40 | BT_GATT_SERVICE_DEFINE( 41 | ir_split_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_IR_SERVICE_UUID)), 42 | #if CONFIG_INPUT 43 | BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_IR_CHAR_INPUT_STATE_UUID), 44 | BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, BT_GATT_PERM_READ_ENCRYPT, 45 | split_svc_input_state, NULL, &last_input_event), 46 | BT_GATT_CCC(split_svc_input_state_ccc, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT), 47 | #endif /* CONFIG_INPUT */ 48 | ); 49 | 50 | K_THREAD_STACK_DEFINE(service_ir_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE); 51 | 52 | struct k_work_q service_ir_work_q; 53 | 54 | #if CONFIG_INPUT 55 | K_MSGQ_DEFINE(input_state_msgq, sizeof(struct zmk_split_bt_input_relay_event), 56 | CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE, 4); 57 | 58 | void send_input_state_callback(struct k_work *work) { 59 | while (k_msgq_get(&input_state_msgq, &last_input_event, K_NO_WAIT) == 0) { 60 | int err = bt_gatt_notify(NULL, &ir_split_svc.attrs[1], &last_input_event, 61 | sizeof(last_input_event)); 62 | if (err) { 63 | LOG_WRN("Error notifying %d", err); 64 | } 65 | } 66 | }; 67 | 68 | K_WORK_DEFINE(service_input_notify_work, send_input_state_callback); 69 | 70 | int send_input_state(struct zmk_split_bt_input_relay_event ev) { 71 | int err = k_msgq_put(&input_state_msgq, &ev, K_MSEC(100)); 72 | if (err) { 73 | // retry... 74 | switch (err) { 75 | case -EAGAIN: { 76 | LOG_WRN("Input state message queue full, popping first message and queueing again"); 77 | struct zmk_split_bt_input_relay_event discarded_state; 78 | k_msgq_get(&input_state_msgq, &discarded_state, K_NO_WAIT); 79 | return send_input_state(ev); 80 | } 81 | default: 82 | LOG_WRN("Failed to queue input state to send (%d)", err); 83 | return err; 84 | } 85 | } 86 | 87 | k_work_submit_to_queue(&service_ir_work_q, &service_input_notify_work); 88 | return 0; 89 | } 90 | 91 | void zmk_split_bt_input_ev_triggered(uint8_t relay_channel, struct input_event *evt) { 92 | struct zmk_split_bt_input_relay_event ev = 93 | (struct zmk_split_bt_input_relay_event){ 94 | .relay_channel = relay_channel, 95 | .sync = evt->sync, .type = evt->type, 96 | .code = evt->code, .value = evt->value}; 97 | 98 | LOG_DBG("Send input: rc-%d t-%d c-%d v-%d s-%d", 99 | ev.relay_channel, ev.type, ev.code, ev.value, ev.sync?1:0); 100 | 101 | send_input_state(ev); 102 | } 103 | 104 | #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) 105 | 106 | #define INPUT_RELY_INST(n) \ 107 | static uint8_t relay_channel_##n = DT_PROP(DT_DRV_INST(n), relay_channel); \ 108 | void input_handler_##n(struct input_event *evt) { \ 109 | zmk_split_bt_input_ev_triggered(relay_channel_##n, evt); \ 110 | } \ 111 | INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), input_handler_##n); 112 | 113 | DT_INST_FOREACH_STATUS_OKAY(INPUT_RELY_INST) 114 | 115 | #endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ 116 | 117 | #endif /* CONFIG_INPUT */ 118 | 119 | static int service_init(void) { 120 | static const struct k_work_queue_config queue_config = { 121 | .name = "Split Peripheral Alternative Notification Queue"}; 122 | k_work_queue_start(&service_ir_work_q, service_ir_q_stack, 123 | K_THREAD_STACK_SIZEOF(service_ir_q_stack), 124 | CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY, &queue_config); 125 | 126 | return 0; 127 | } 128 | 129 | SYS_INIT(service_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); 130 | -------------------------------------------------------------------------------- /src/input_relay_central.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 The ZMK Contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | #define DT_DRV_COMPAT zmk_split_peripheral_input_relay 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | 22 | LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); 23 | 24 | #include 25 | 26 | #include 27 | #include 28 | 29 | enum ir_peripheral_slot_state { 30 | PERIPHERAL_SLOT_STATE_OPEN, 31 | PERIPHERAL_SLOT_STATE_CONNECTING, 32 | PERIPHERAL_SLOT_STATE_CONNECTED, 33 | }; 34 | 35 | struct ir_peripheral_slot { 36 | enum ir_peripheral_slot_state state; 37 | struct bt_conn *conn; 38 | struct bt_gatt_discover_params discover_params; 39 | struct bt_gatt_subscribe_params subscribe_params; 40 | struct bt_gatt_subscribe_params input_subscribe_params; 41 | struct bt_gatt_discover_params sub_discover_params; 42 | }; 43 | 44 | static struct ir_peripheral_slot peripherals[ZMK_SPLIT_BLE_PERIPHERAL_COUNT]; 45 | 46 | static const struct bt_uuid_128 split_ir_service_uuid = BT_UUID_INIT_128( 47 | ZMK_SPLIT_BT_IR_SERVICE_UUID); 48 | 49 | static int ir_peripheral_slot_index_for_conn(struct bt_conn *conn) { 50 | for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) { 51 | if (peripherals[i].conn == conn) { 52 | return i; 53 | } 54 | } 55 | return -EINVAL; 56 | } 57 | 58 | static struct ir_peripheral_slot *ir_peripheral_slot_for_conn(struct bt_conn *conn) { 59 | int idx = ir_peripheral_slot_index_for_conn(conn); 60 | if (idx < 0) { 61 | return NULL; 62 | } 63 | return &peripherals[idx]; 64 | } 65 | 66 | static int release_ir_peripheral_slot(int index) { 67 | if (index < 0 || index >= ZMK_SPLIT_BLE_PERIPHERAL_COUNT) { 68 | return -EINVAL; 69 | } 70 | 71 | struct ir_peripheral_slot *slot = &peripherals[index]; 72 | 73 | if (slot->state == PERIPHERAL_SLOT_STATE_OPEN) { 74 | return -EINVAL; 75 | } 76 | 77 | LOG_DBG("Releasing peripheral slot at %d", index); 78 | 79 | if (slot->conn != NULL) { 80 | slot->conn = NULL; 81 | } 82 | slot->state = PERIPHERAL_SLOT_STATE_OPEN; 83 | 84 | // Clean up previously discovered handles; 85 | slot->subscribe_params.value_handle = 0; 86 | 87 | return 0; 88 | } 89 | 90 | static int reserve_ir_peripheral_slot_for_conn(struct bt_conn *conn) { 91 | #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_PREF_WEAK_BOND) 92 | for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) { 93 | if (peripherals[i].state == PERIPHERAL_SLOT_STATE_OPEN) { 94 | // Be sure the slot is fully reinitialized. 95 | release_ir_peripheral_slot(i); 96 | peripherals[i].conn = conn; 97 | peripherals[i].state = PERIPHERAL_SLOT_STATE_CONNECTED; 98 | return i; 99 | } 100 | } 101 | #else 102 | int i = zmk_ble_put_peripheral_addr(bt_conn_get_dst(conn)); 103 | if (i >= 0) { 104 | if (peripherals[i].state == PERIPHERAL_SLOT_STATE_OPEN) { 105 | // Be sure the slot is fully reinitialized. 106 | release_ir_peripheral_slot(i); 107 | peripherals[i].conn = conn; 108 | peripherals[i].state = PERIPHERAL_SLOT_STATE_CONNECTED; 109 | return i; 110 | } 111 | } 112 | #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_PREF_WEAK_BOND) 113 | 114 | return -ENOMEM; 115 | } 116 | 117 | int release_ir_peripheral_slot_for_conn(struct bt_conn *conn) { 118 | int idx = ir_peripheral_slot_index_for_conn(conn); 119 | if (idx < 0) { 120 | return idx; 121 | } 122 | 123 | return release_ir_peripheral_slot(idx); 124 | } 125 | 126 | #if CONFIG_INPUT 127 | K_MSGQ_DEFINE(peripheral_input_relay_event_msgq, sizeof(struct input_event), 128 | CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4); 129 | 130 | void peripheral_input_relay_event_work_callback(struct k_work *work) { 131 | struct input_event ev; 132 | while (k_msgq_get(&peripheral_input_relay_event_msgq, &ev, K_NO_WAIT) == 0) { 133 | LOG_DBG("Trigger input change for %d/%d/%d/%d", 134 | ev.type, ev.code, ev.value, ev.sync?0:1); 135 | switch (ev.type) { 136 | case INPUT_EV_REL: 137 | input_report_rel(ev.dev, ev.code, ev.value, ev.sync, K_NO_WAIT); 138 | break; 139 | case INPUT_EV_ABS: 140 | input_report_abs(ev.dev, ev.code, ev.value, ev.sync, K_NO_WAIT); 141 | break; 142 | case INPUT_EV_KEY: 143 | input_report_key(ev.dev, ev.code, ev.value, ev.sync, K_NO_WAIT); 144 | break; 145 | } 146 | } 147 | } 148 | 149 | K_WORK_DEFINE(peripheral_input_event_work, peripheral_input_relay_event_work_callback); 150 | 151 | const struct device* virtual_input_device_get_for_relay_channel(uint8_t relay_channel); 152 | 153 | static uint8_t split_central_input_notify_func(struct bt_conn *conn, 154 | struct bt_gatt_subscribe_params *params, 155 | const void *data, uint16_t length) { 156 | if (!data) { 157 | LOG_DBG("[UNSUBSCRIBED]"); 158 | params->value_handle = 0U; 159 | return BT_GATT_ITER_STOP; 160 | } 161 | 162 | LOG_DBG("[INPUT NOTIFICATION] data %p length %u", data, length); 163 | 164 | struct zmk_split_bt_input_relay_event evt; 165 | memcpy(&evt, data, MIN(length, sizeof(struct zmk_split_bt_input_relay_event))); 166 | 167 | const struct device *dev = virtual_input_device_get_for_relay_channel(evt.relay_channel); 168 | if (dev == NULL) { 169 | LOG_DBG("Unable to retrieve virtual device for channel: %d", evt.relay_channel); 170 | return BT_GATT_ITER_CONTINUE; 171 | } 172 | 173 | struct input_event ev = { 174 | .dev = dev, 175 | .sync = evt.sync, .type = evt.type, 176 | .code = evt.code, .value = evt.value}; 177 | 178 | k_msgq_put(&peripheral_input_relay_event_msgq, &ev, K_NO_WAIT); 179 | k_work_submit(&peripheral_input_event_work); 180 | 181 | return BT_GATT_ITER_CONTINUE; 182 | } 183 | #endif /* CONFIG_INPUT */ 184 | 185 | static int split_central_subscribe(struct bt_conn *conn, struct bt_gatt_subscribe_params *params) { 186 | int err = bt_gatt_subscribe(conn, params); 187 | switch (err) { 188 | case -EALREADY: 189 | LOG_DBG("[ALREADY SUBSCRIBED]"); 190 | break; 191 | case 0: 192 | LOG_DBG("[SUBSCRIBED]"); 193 | break; 194 | default: 195 | LOG_ERR("Subscribe failed (err %d)", err); 196 | break; 197 | } 198 | 199 | return err; 200 | } 201 | 202 | static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, 203 | const struct bt_gatt_attr *attr, 204 | struct bt_gatt_discover_params *params) { 205 | if (!attr) { 206 | LOG_DBG("Discover complete"); 207 | return BT_GATT_ITER_STOP; 208 | } 209 | 210 | if (!attr->user_data) { 211 | LOG_ERR("Required user data not passed to discovery"); 212 | return BT_GATT_ITER_STOP; 213 | } 214 | 215 | struct ir_peripheral_slot *slot = ir_peripheral_slot_for_conn(conn); 216 | if (slot == NULL) { 217 | LOG_ERR("No peripheral slot found for connection"); 218 | return BT_GATT_ITER_STOP; 219 | } 220 | 221 | LOG_DBG("[ATTRIBUTE] handle %u", attr->handle); 222 | const struct bt_uuid *chrc_uuid = ((struct bt_gatt_chrc *)attr->user_data)->uuid; 223 | 224 | #if CONFIG_INPUT 225 | if (bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_IR_CHAR_INPUT_STATE_UUID)) == 0) { 226 | LOG_DBG("Found input state characteristic"); 227 | slot->discover_params.uuid = NULL; 228 | slot->discover_params.start_handle = attr->handle + 2; 229 | slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; 230 | 231 | slot->input_subscribe_params.disc_params = &slot->sub_discover_params; 232 | slot->input_subscribe_params.end_handle = slot->discover_params.end_handle; 233 | slot->input_subscribe_params.value_handle = bt_gatt_attr_value_handle(attr); 234 | slot->input_subscribe_params.notify = split_central_input_notify_func; 235 | slot->input_subscribe_params.value = BT_GATT_CCC_NOTIFY; 236 | split_central_subscribe(conn, &slot->input_subscribe_params); 237 | } 238 | #endif /* CONFIG_INPUT */ 239 | 240 | bool subscribed = true; 241 | 242 | #if CONFIG_INPUT 243 | subscribed = subscribed && slot->input_subscribe_params.value_handle; 244 | #endif /* CONFIG_INPUT */ 245 | 246 | return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE; 247 | } 248 | 249 | static uint8_t split_central_service_discovery_func(struct bt_conn *conn, 250 | const struct bt_gatt_attr *attr, 251 | struct bt_gatt_discover_params *params) { 252 | if (!attr) { 253 | LOG_DBG("Discover complete"); 254 | (void)memset(params, 0, sizeof(*params)); 255 | return BT_GATT_ITER_STOP; 256 | } 257 | 258 | LOG_DBG("[ATTRIBUTE] handle %u", attr->handle); 259 | 260 | struct ir_peripheral_slot *slot = ir_peripheral_slot_for_conn(conn); 261 | if (slot == NULL) { 262 | LOG_ERR("No peripheral state found for connection"); 263 | return BT_GATT_ITER_STOP; 264 | } 265 | 266 | if (bt_uuid_cmp(slot->discover_params.uuid, 267 | BT_UUID_DECLARE_128(ZMK_SPLIT_BT_IR_SERVICE_UUID)) != 0 268 | ) { 269 | LOG_DBG("Found other service"); 270 | return BT_GATT_ITER_CONTINUE; 271 | } 272 | 273 | LOG_DBG("Found split service"); 274 | slot->discover_params.uuid = NULL; 275 | slot->discover_params.func = split_central_chrc_discovery_func; 276 | slot->discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC; 277 | 278 | int err = bt_gatt_discover(conn, &slot->discover_params); 279 | if (err) { 280 | LOG_ERR("Failed to start discovering split service characteristics (err %d)", err); 281 | } 282 | return BT_GATT_ITER_STOP; 283 | } 284 | 285 | static void split_central_process_connection(struct bt_conn *conn) { 286 | int err; 287 | 288 | LOG_DBG("Current security for connection: %d", bt_conn_get_security(conn)); 289 | 290 | struct ir_peripheral_slot *slot = ir_peripheral_slot_for_conn(conn); 291 | if (slot == NULL) { 292 | LOG_ERR("No peripheral state found for connection"); 293 | return; 294 | } 295 | 296 | if (!slot->subscribe_params.value_handle) { 297 | slot->discover_params.uuid = &split_ir_service_uuid.uuid; 298 | slot->discover_params.func = split_central_service_discovery_func; 299 | slot->discover_params.start_handle = 0x0001; 300 | slot->discover_params.end_handle = 0xffff; 301 | slot->discover_params.type = BT_GATT_DISCOVER_PRIMARY; 302 | 303 | err = bt_gatt_discover(slot->conn, &slot->discover_params); 304 | if (err) { 305 | LOG_ERR("Discover failed(err %d)", err); 306 | return; 307 | } 308 | } 309 | 310 | struct bt_conn_info info; 311 | 312 | bt_conn_get_info(conn, &info); 313 | 314 | LOG_DBG("New connection params: Interval: %d, Latency: %d, PHY: %d", info.le.interval, 315 | info.le.latency, info.le.phy->rx_phy); 316 | } 317 | 318 | static void split_central_connected(struct bt_conn *conn, uint8_t conn_err) { 319 | char addr_str[BT_ADDR_LE_STR_LEN]; 320 | struct bt_conn_info info; 321 | 322 | bt_addr_le_to_str(bt_conn_get_dst(conn), addr_str, sizeof(addr_str)); 323 | 324 | bt_conn_get_info(conn, &info); 325 | 326 | if (info.role != BT_CONN_ROLE_CENTRAL) { 327 | LOG_DBG("SKIPPING FOR ROLE %d", info.role); 328 | return; 329 | } 330 | 331 | if (conn_err) { 332 | LOG_ERR("Failed to connect to %s (%u)", addr_str, conn_err); 333 | release_ir_peripheral_slot_for_conn(conn); 334 | return; 335 | } 336 | 337 | LOG_DBG("Connected: %s", addr_str); 338 | 339 | int slot_idx = reserve_ir_peripheral_slot_for_conn(conn); 340 | if (slot_idx < 0) { 341 | LOG_ERR("Unable to reserve peripheral slot for connection (err %d)", slot_idx); 342 | return; 343 | } 344 | 345 | split_central_process_connection(conn); 346 | } 347 | 348 | static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) { 349 | char addr_str[BT_ADDR_LE_STR_LEN]; 350 | int err; 351 | 352 | bt_addr_le_to_str(bt_conn_get_dst(conn), addr_str, sizeof(addr_str)); 353 | 354 | LOG_DBG("Disconnected: %s (reason %d)", addr_str, reason); 355 | 356 | err = release_ir_peripheral_slot_for_conn(conn); 357 | 358 | if (err < 0) { 359 | return; 360 | } 361 | } 362 | 363 | static struct bt_conn_cb conn_callbacks = { 364 | .connected = split_central_connected, 365 | .disconnected = split_central_disconnected, 366 | }; 367 | 368 | static int zmk_split_bt_central_init(void) { 369 | bt_conn_cb_register(&conn_callbacks); 370 | return 0; 371 | } 372 | 373 | SYS_INIT(zmk_split_bt_central_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); 374 | 375 | 376 | #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) 377 | 378 | struct split_peripheral_input_relay_config { 379 | uint8_t relay_channel; 380 | const struct device *device; 381 | }; 382 | 383 | #define INPUT_RELY_CFG_DEFINE(n) \ 384 | static const struct split_peripheral_input_relay_config config_##n = { \ 385 | .relay_channel = DT_PROP(DT_DRV_INST(n), relay_channel), \ 386 | .device = DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), \ 387 | }; 388 | 389 | DT_INST_FOREACH_STATUS_OKAY(INPUT_RELY_CFG_DEFINE) 390 | 391 | const struct device* virtual_input_device_get_for_relay_channel(uint8_t relay_channel) { 392 | #define IR_C_COND_CMP_RELAY_CHANNEL(n) \ 393 | if (relay_channel == config_##n.relay_channel) { \ 394 | return config_##n.device; \ 395 | } 396 | DT_INST_FOREACH_STATUS_OKAY(IR_C_COND_CMP_RELAY_CHANNEL) 397 | return NULL; 398 | } 399 | 400 | #else 401 | 402 | const struct device* virtual_input_device_get_for_relay_channel(uint8_t relay_channel) { 403 | return NULL; 404 | } 405 | 406 | #endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ 407 | --------------------------------------------------------------------------------