diff --git a/app/dts/bindings/zmk,wired-split.yaml b/app/dts/bindings/zmk,wired-split.yaml index 107e437b0..311d367ab 100644 --- a/app/dts/bindings/zmk,wired-split.yaml +++ b/app/dts/bindings/zmk,wired-split.yaml @@ -12,6 +12,12 @@ properties: required: true description: The UART device for wired split communication + detect-gpios: + type: phandle-array + description: | + If your split includes support for an extra GPIO to detect presence of the split cable, set + this to the GPIO pin used to detect the connection. + half-duplex: type: boolean description: "Experimental: Enable half-duplex protocol mode" diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h index 6f9e2ee93..8a3f40b1b 100644 --- a/app/include/zmk/hid.h +++ b/app/include/zmk/hid.h @@ -289,8 +289,6 @@ struct zmk_hid_keyboard_report { struct zmk_hid_keyboard_report_body body; } __packed; -#if IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) - struct zmk_hid_led_report_body { uint8_t leds; } __packed; @@ -300,8 +298,6 @@ struct zmk_hid_led_report { struct zmk_hid_led_report_body body; } __packed; -#endif // IS_ENABLED(CONFIG_ZMK_HID_INDICATORS) - struct zmk_hid_consumer_report_body { #if IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC) uint8_t keys[CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE]; diff --git a/app/include/zmk/split/transport/central.h b/app/include/zmk/split/transport/central.h index fcdc8be92..a6b819d17 100644 --- a/app/include/zmk/split/transport/central.h +++ b/app/include/zmk/split/transport/central.h @@ -10,13 +10,23 @@ #include +struct zmk_split_transport_central; + +typedef int (*zmk_split_transport_central_status_changed_cb_t)( + const struct zmk_split_transport_central *transport, struct zmk_split_transport_status status); + typedef int (*zmk_split_transport_central_send_command_t)( uint8_t source, struct zmk_split_transport_central_command cmd); typedef int (*zmk_split_transport_central_get_available_source_ids_t)(uint8_t *sources); +typedef int (*zmk_split_transport_central_set_status_callback_t)( + zmk_split_transport_central_status_changed_cb_t cb); struct zmk_split_transport_central_api { zmk_split_transport_central_send_command_t send_command; zmk_split_transport_central_get_available_source_ids_t get_available_source_ids; + zmk_split_transport_set_enabled_t set_enabled; + zmk_split_transport_get_status_t get_status; + zmk_split_transport_central_set_status_callback_t set_status_callback; }; struct zmk_split_transport_central { @@ -27,7 +37,8 @@ int zmk_split_transport_central_peripheral_event_handler( const struct zmk_split_transport_central *transport, uint8_t source, struct zmk_split_transport_peripheral_event ev); -#define ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(name, _api) \ - STRUCT_SECTION_ITERABLE(zmk_split_transport_central, name) = { \ +#define ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(name, _api, priority) \ + STRUCT_SECTION_ITERABLE_NAMED(zmk_split_transport_central, _CONCAT(priority, _##name), \ + name) = { \ .api = _api, \ }; diff --git a/app/include/zmk/split/transport/peripheral.h b/app/include/zmk/split/transport/peripheral.h index c865ccc4c..fdc3943d9 100644 --- a/app/include/zmk/split/transport/peripheral.h +++ b/app/include/zmk/split/transport/peripheral.h @@ -10,11 +10,22 @@ #include -typedef int (*zmk_split_central_report_event_callback_t)( +struct zmk_split_transport_peripheral; + +typedef int (*zmk_split_transport_peripheral_status_changed_cb_t)( + const struct zmk_split_transport_peripheral *transport, + struct zmk_split_transport_status status); + +typedef int (*zmk_split_transport_peripheral_report_event_callback_t)( const struct zmk_split_transport_peripheral_event *event); +typedef int (*zmk_split_transport_peripheral_set_status_callback_t)( + zmk_split_transport_peripheral_status_changed_cb_t cb); struct zmk_split_transport_peripheral_api { - zmk_split_central_report_event_callback_t report_event; + zmk_split_transport_peripheral_report_event_callback_t report_event; + zmk_split_transport_set_enabled_t set_enabled; + zmk_split_transport_get_status_t get_status; + zmk_split_transport_peripheral_set_status_callback_t set_status_callback; }; struct zmk_split_transport_peripheral { @@ -25,7 +36,8 @@ int zmk_split_transport_peripheral_command_handler( const struct zmk_split_transport_peripheral *transport, struct zmk_split_transport_central_command cmd); -#define ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(name, _api) \ - STRUCT_SECTION_ITERABLE(zmk_split_transport_peripheral, name) = { \ +#define ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(name, _api, priority) \ + STRUCT_SECTION_ITERABLE_NAMED(zmk_split_transport_peripheral, _CONCAT(priority, _##name), \ + name) = { \ .api = _api, \ }; diff --git a/app/include/zmk/split/transport/types.h b/app/include/zmk/split/transport/types.h index 14040aec9..1d6eb734c 100644 --- a/app/include/zmk/split/transport/types.h +++ b/app/include/zmk/split/transport/types.h @@ -10,6 +10,21 @@ #include #include +enum zmk_split_transport_connections_status { + ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED = 0, + ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_SOME_CONNECTED, + ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED, +}; + +struct zmk_split_transport_status { + bool available; + bool enabled; + enum zmk_split_transport_connections_status connections; +}; + +typedef struct zmk_split_transport_status (*zmk_split_transport_get_status_t)(void); +typedef int (*zmk_split_transport_set_enabled_t)(bool enabled); + enum zmk_split_transport_peripheral_event_type { ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT, ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT, diff --git a/app/src/split/bluetooth/Kconfig b/app/src/split/bluetooth/Kconfig index dec192247..5f4a782f7 100644 --- a/app/src/split/bluetooth/Kconfig +++ b/app/src/split/bluetooth/Kconfig @@ -5,6 +5,11 @@ if ZMK_SPLIT && ZMK_SPLIT_BLE menu "BLE Transport" +config ZMK_SPLIT_BLE_PRIORITY + int "BLE transport priority" + help + Lower number priorities transports are favored over higher numbers. + # Added for backwards compatibility. New shields / board should set `ZMK_SPLIT_ROLE_CENTRAL` only. config ZMK_SPLIT_BLE_ROLE_CENTRAL bool diff --git a/app/src/split/bluetooth/Kconfig.defaults b/app/src/split/bluetooth/Kconfig.defaults index bf6fa1c1c..85bd4d621 100644 --- a/app/src/split/bluetooth/Kconfig.defaults +++ b/app/src/split/bluetooth/Kconfig.defaults @@ -3,7 +3,12 @@ if ZMK_BLE -if ZMK_SPLIT_BLE && ZMK_SPLIT_ROLE_CENTRAL +if ZMK_SPLIT_BLE + +config ZMK_SPLIT_BLE_PRIORITY + default 1 + +if ZMK_SPLIT_ROLE_CENTRAL config ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS default 1 @@ -17,6 +22,8 @@ config BT_MAX_PAIRED #ZMK_SPLIT_BLE && ZMK_SPLIT_ROLE_CENTRAL endif +endif + if !ZMK_SPLIT_BLE config BT_MAX_CONN diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 8f8e76d99..685deb51c 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -129,6 +129,9 @@ void release_peripheral_input_subs(struct bt_conn *conn) { #endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) +static zmk_split_transport_central_status_changed_cb_t transport_status_cb; +static bool is_enabled; + static struct peripheral_slot peripherals[ZMK_SPLIT_BLE_PERIPHERAL_COUNT]; static bool is_scanning = false; @@ -253,6 +256,12 @@ int confirm_peripheral_slot_conn(struct bt_conn *conn) { return 0; } +static void notify_transport_status(void); + +static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); } + +static K_WORK_DEFINE(notify_status_work, notify_status_work_cb); + #if ZMK_KEYMAP_HAS_SENSORS static uint8_t split_central_sensor_notify_func(struct bt_conn *conn, @@ -874,6 +883,11 @@ static void split_central_device_found(const bt_addr_le_t *addr, int8_t rssi, ui } static int start_scanning(void) { + if (!is_enabled) { + LOG_DBG("Not scanning, we're disabled"); + return 0; + } + // No action is necessary if central is already scanning. if (is_scanning) { LOG_DBG("Scanning already running"); @@ -931,6 +945,7 @@ static void split_central_connected(struct bt_conn *conn, uint8_t conn_err) { confirm_peripheral_slot_conn(conn); split_central_process_connection(conn); + k_work_submit(¬ify_status_work); } static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) { @@ -964,9 +979,11 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) { err = release_peripheral_slot_for_conn(conn); if (err < 0) { - return; + LOG_WRN("Failed to release peripheral slot (%d)", err); } + k_work_submit(¬ify_status_work); + start_scanning(); } @@ -1112,9 +1129,9 @@ static int split_bt_invoke_behavior_payload(struct central_cmd_wrapper payload_w return 0; }; -static int finish_init() { - return IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) ? 0 : start_scanning(); -} +static int finish_init(); + +static bool settings_loaded = false; #if IS_ENABLED(CONFIG_SETTINGS) @@ -1189,12 +1206,84 @@ static int split_central_bt_get_available_source_ids(uint8_t *sources) { return count; } +static int split_central_bt_set_enabled(bool enabled) { + is_enabled = enabled; + if (enabled) { + return start_scanning(); + } else { + int err = stop_scanning(); + if (err < 0) { + LOG_WRN("Failed to stop scanning for peripherals (%d)", err); + } + + for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) { + if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) { + continue; + } + + err = bt_conn_disconnect(peripherals[i].conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err < 0) { + LOG_WRN("Failed to disconnect a peripheral (%d)", err); + } + } + + return 0; + } +} + +static int +split_central_bt_set_status_callback(zmk_split_transport_central_status_changed_cb_t cb) { + transport_status_cb = cb; + return 0; +} + +static struct zmk_split_transport_status split_central_bt_get_status() { + uint8_t _source_ids[ZMK_SPLIT_BLE_PERIPHERAL_COUNT]; + + int count = split_central_bt_get_available_source_ids(_source_ids); + + enum zmk_split_transport_connections_status conn_status; + + if (count == 0) { + conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED; + } else if (count == ZMK_SPLIT_BLE_PERIPHERAL_COUNT) { + conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED; + } else { + conn_status = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_SOME_CONNECTED; + } + + return (struct zmk_split_transport_status){ + .available = !IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) && settings_loaded, + .enabled = is_enabled, + .connections = conn_status, + }; +} + static const struct zmk_split_transport_central_api central_api = { .send_command = split_central_bt_send_command, .get_available_source_ids = split_central_bt_get_available_source_ids, + .set_enabled = split_central_bt_set_enabled, + .set_status_callback = split_central_bt_set_status_callback, + .get_status = split_central_bt_get_status, }; -ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(bt_central, ¢ral_api); +ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(bt_central, ¢ral_api, CONFIG_ZMK_SPLIT_BLE_PRIORITY); + +static void notify_transport_status(void) { + if (transport_status_cb) { + transport_status_cb(&bt_central, split_central_bt_get_status()); + } +} + +static int finish_init() { + settings_loaded = true; + + if (!transport_status_cb) { + return 0; + } + + return transport_status_cb(&bt_central, split_central_bt_get_status()); +} void peripheral_event_work_callback(struct k_work *work) { struct peripheral_event_wrapper ev; diff --git a/app/src/split/bluetooth/peripheral.c b/app/src/split/bluetooth/peripheral.c index 5a12e0fc4..e4e7ba3a3 100644 --- a/app/src/split/bluetooth/peripheral.c +++ b/app/src/split/bluetooth/peripheral.c @@ -20,6 +20,9 @@ #include #include +#include "peripheral.h" +#include "service.h" + #if IS_ENABLED(CONFIG_SETTINGS) #include @@ -70,6 +73,7 @@ static int start_advertising(bool low_duty) { }; static bool low_duty_advertising = false; +static bool enabled = false; static void advertising_cb(struct k_work *work) { const int err = start_advertising(low_duty_advertising); @@ -86,7 +90,7 @@ static void connected(struct bt_conn *conn, uint8_t err) { raise_zmk_split_peripheral_status_changed( (struct zmk_split_peripheral_status_changed){.connected = is_connected}); - if (err == BT_HCI_ERR_ADV_TIMEOUT) { + if (err == BT_HCI_ERR_ADV_TIMEOUT && enabled) { low_duty_advertising = true; k_work_submit(&advertising_work); } @@ -104,8 +108,10 @@ static void disconnected(struct bt_conn *conn, uint8_t reason) { raise_zmk_split_peripheral_status_changed( (struct zmk_split_peripheral_status_changed){.connected = is_connected}); - low_duty_advertising = false; - k_work_submit(&advertising_work); + if (enabled) { + low_duty_advertising = false; + k_work_submit(&advertising_work); + } } static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) { @@ -146,6 +152,85 @@ bool zmk_split_bt_peripheral_is_connected(void) { return is_connected; } bool zmk_split_bt_peripheral_is_bonded(void) { return is_bonded; } +static zmk_split_transport_peripheral_status_changed_cb_t transport_status_cb; + +static int +split_peripheral_bt_set_status_callback(zmk_split_transport_peripheral_status_changed_cb_t cb) { + transport_status_cb = cb; + return 0; +} + +static void find_first_conn(struct bt_conn *conn, void *data) { + struct bt_conn **cp = (struct bt_conn **)data; + + *cp = conn; +} + +static int split_peripheral_bt_set_enabled(bool en) { + int err; + + enabled = en; + if (en) { + k_work_submit(&advertising_work); + return 0; + } else { + struct bt_conn *conn = NULL; + bt_conn_foreach(BT_CONN_TYPE_LE, find_first_conn, &conn); + if (conn) { + err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err < 0) { + LOG_WRN("Failed to disconnect connection to central (%d)", err); + } + } + + err = bt_le_adv_stop(); + + if (err < 0) { + LOG_WRN("Failed to stop advertising (%d)", err); + } + + return 0; + } +} + +static void notify_transport_status(void); + +static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); } + +static K_WORK_DEFINE(notify_status_work, notify_status_work_cb); + +static bool settings_loaded = false; + +static struct zmk_split_transport_status split_peripheral_bt_get_status(void) { + return (struct zmk_split_transport_status){ + .available = !IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) && settings_loaded, + .enabled = enabled, + .connections = zmk_split_bt_peripheral_is_connected() + ? ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED + : ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED, + }; +} + +static const struct zmk_split_transport_peripheral_api peripheral_api = { + .report_event = zmk_split_transport_peripheral_bt_report_event, + .set_enabled = split_peripheral_bt_set_enabled, + .set_status_callback = split_peripheral_bt_set_status_callback, + .get_status = split_peripheral_bt_get_status, +}; + +ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(bt_peripheral, &peripheral_api, + CONFIG_ZMK_SPLIT_BLE_PRIORITY); + +struct zmk_split_transport_peripheral *zmk_split_transport_peripheral_bt(void) { + return &bt_peripheral; +} + +static void notify_transport_status(void) { + if (transport_status_cb) { + transport_status_cb(&bt_peripheral, split_peripheral_bt_get_status()); + } +} + static int zmk_peripheral_ble_complete_startup(void) { #if IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) LOG_WRN("Clearing all existing BLE bond information from the keyboard"); @@ -156,7 +241,9 @@ static int zmk_peripheral_ble_complete_startup(void) { bt_conn_auth_info_cb_register(&zmk_peripheral_ble_auth_info_cb); low_duty_advertising = false; - k_work_submit(&advertising_work); + + settings_loaded = true; + k_work_submit(¬ify_status_work); #endif return 0; diff --git a/app/src/split/bluetooth/peripheral.h b/app/src/split/bluetooth/peripheral.h new file mode 100644 index 000000000..fd6c22f48 --- /dev/null +++ b/app/src/split/bluetooth/peripheral.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_split_transport_peripheral *zmk_split_transport_peripheral_bt(void); \ No newline at end of file diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 9181e0bbc..5bbed1373 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -26,6 +26,8 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include +#include "peripheral.h" + #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) @@ -343,7 +345,8 @@ static int service_init(void) { SYS_INIT(service_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY); -static int zmk_peripheral_ble_report_event(const struct zmk_split_transport_peripheral_event *ev) { +int zmk_split_transport_peripheral_bt_report_event( + const struct zmk_split_transport_peripheral_event *ev) { switch (ev->type) { case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT: if (ev->data.key_position_event.pressed) { @@ -380,12 +383,6 @@ static int zmk_peripheral_ble_report_event(const struct zmk_split_transport_peri return 0; } -static const struct zmk_split_transport_peripheral_api peripheral_api = { - .report_event = zmk_peripheral_ble_report_event, -}; - -ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(bt_peripheral, &peripheral_api); - static ssize_t split_svc_run_behavior(struct bt_conn *conn, const struct bt_gatt_attr *attrs, const void *buf, uint16_t len, uint16_t offset, uint8_t flags) { @@ -428,7 +425,8 @@ static ssize_t split_svc_run_behavior(struct bt_conn *conn, const struct bt_gatt cmd.data.invoke_behavior.param1, cmd.data.invoke_behavior.param2, cmd.data.invoke_behavior.state); - int err = zmk_split_transport_peripheral_command_handler(&bt_peripheral, cmd); + int err = zmk_split_transport_peripheral_command_handler( + zmk_split_transport_peripheral_bt(), cmd); if (err) { LOG_ERR("Failed to invoke behavior %s: %d", payload->behavior_dev, err); diff --git a/app/src/split/bluetooth/service.h b/app/src/split/bluetooth/service.h new file mode 100644 index 000000000..20ff7513c --- /dev/null +++ b/app/src/split/bluetooth/service.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +int zmk_split_transport_peripheral_bt_report_event( + const struct zmk_split_transport_peripheral_event *ev); \ No newline at end of file diff --git a/app/src/split/central.c b/app/src/split/central.c index e53e7125a..e2b806437 100644 --- a/app/src/split/central.c +++ b/app/src/split/central.c @@ -21,9 +21,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); -// TODO: Active transport selection - -struct zmk_split_transport_central *active_transport; +const struct zmk_split_transport_central *active_transport; #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) @@ -165,10 +163,65 @@ int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *leve #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) -static int central_init(void) { - STRUCT_SECTION_GET(zmk_split_transport_central, 0, &active_transport); +static int select_first_available_transport(void) { + // Transports are sorted by priority, so find the first + // One that's available, and enable it. Any transport that + // Doesn't support `get_status` is assumed to be always + // available and fully connected. + STRUCT_SECTION_FOREACH(zmk_split_transport_central, t) { + if (!t->api->get_status || t->api->get_status().available) { + + if (active_transport == t) { + LOG_DBG("First available is already selected, moving on"); + return 0; + } + + if (active_transport && active_transport->api->set_enabled) { + int err = active_transport->api->set_enabled(false); + if (err < 0) { + LOG_WRN("Error disabling previously selected split transport (%d)", err); + } + } + + active_transport = t; + int err = 0; + if (active_transport->api->set_enabled) { + err = active_transport->api->set_enabled(true); + } + + return err; + } + } + + return -ENODEV; +} + +static int transport_status_changed_cb(const struct zmk_split_transport_central *central, + struct zmk_split_transport_status status) { + if (central == active_transport) { + LOG_DBG("Central at %p changed status: enabled %d, available %d, connections %d", central, + status.enabled, status.available, status.connections); + if (status.connections == ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED) { + return select_first_available_transport(); + } + } else { + // Just to be sure, in case a higher priority transport becomes available + select_first_available_transport(); + } return 0; } +static int central_init(void) { + STRUCT_SECTION_FOREACH(zmk_split_transport_central, t) { + if (!t->api->set_status_callback) { + continue; + } + + t->api->set_status_callback(transport_status_changed_cb); + } + + return select_first_available_transport(); +} + SYS_INIT(central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); diff --git a/app/src/split/peripheral.c b/app/src/split/peripheral.c index ff12c0e12..4157ffa22 100644 --- a/app/src/split/peripheral.c +++ b/app/src/split/peripheral.c @@ -22,9 +22,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); -// TODO: Active transport selection - -struct zmk_split_transport_peripheral *active_transport; +const struct zmk_split_transport_peripheral *active_transport; int zmk_split_transport_peripheral_command_handler( const struct zmk_split_transport_peripheral *transport, @@ -69,12 +67,67 @@ int zmk_split_peripheral_report_event(const struct zmk_split_transport_periphera return active_transport->api->report_event(event); } -static int peripheral_init(void) { - STRUCT_SECTION_GET(zmk_split_transport_peripheral, 0, &active_transport); +static int select_first_available_transport(void) { + // Transports are sorted by priority, so find the first + // One that's available, and enable it. Any transport that + // Doesn't support `get_status` is assumed to be always + // available and fully connected. + STRUCT_SECTION_FOREACH(zmk_split_transport_peripheral, t) { + if (!t->api->get_status || t->api->get_status().available) { + if (active_transport == t) { + LOG_DBG("First available is already selected, moving on"); + return 0; + } + + if (active_transport && active_transport->api->set_enabled) { + int err = active_transport->api->set_enabled(false); + if (err < 0) { + LOG_WRN("Error disabling previously selected split transport (%d)", err); + } + } + + active_transport = t; + int err = 0; + if (active_transport->api->set_enabled) { + err = active_transport->api->set_enabled(true); + } + + return err; + } + } + + return -ENODEV; +} + +static int transport_status_changed_cb(const struct zmk_split_transport_peripheral *p, + struct zmk_split_transport_status status) { + if (p == active_transport) { + LOG_DBG("Peripheral at %p changed status: enabled %d, available %d, connections %d", p, + status.enabled, status.available, status.connections); + if (status.connections == ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED) { + LOG_DBG("Find us a new active transport!"); + + return select_first_available_transport(); + } + } else { + select_first_available_transport(); + } return 0; } +static int peripheral_init(void) { + STRUCT_SECTION_FOREACH(zmk_split_transport_peripheral, t) { + if (!t->api->set_status_callback) { + continue; + } + + t->api->set_status_callback(transport_status_changed_cb); + } + + return select_first_available_transport(); +} + SYS_INIT(peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); int split_peripheral_listener(const zmk_event_t *eh) { diff --git a/app/src/split/wired/Kconfig b/app/src/split/wired/Kconfig index 2be4a8e69..465fac014 100644 --- a/app/src/split/wired/Kconfig +++ b/app/src/split/wired/Kconfig @@ -3,6 +3,11 @@ if ZMK_SPLIT_WIRED +config ZMK_SPLIT_WIRED_PRIORITY + int "Wired transport priority" + help + Lower number priorities transports are favored over higher numbers. + choice prompt "UART Mode" @@ -10,7 +15,7 @@ config ZMK_SPLIT_WIRED_UART_MODE_ASYNC bool "Async (DMA) Mode" # For now, don't use async/DMA on nRF52 due to RX bug (fixed # in newer Zephyr version?) - depends on SERIAL_SUPPORT_ASYNC && !SOC_FAMILY_NRF + depends on SERIAL_SUPPORT_ASYNC select UART_ASYNC_API config ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT @@ -49,4 +54,4 @@ config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT int "RX complete timeout (in ticks) when polling peripheral(s) after receiving some response data" -endif \ No newline at end of file +endif diff --git a/app/src/split/wired/Kconfig.defaults b/app/src/split/wired/Kconfig.defaults index dfb2deea2..8d810efe9 100644 --- a/app/src/split/wired/Kconfig.defaults +++ b/app/src/split/wired/Kconfig.defaults @@ -3,6 +3,9 @@ if ZMK_SPLIT_WIRED +config ZMK_SPLIT_WIRED_PRIORITY + default 0 + config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS default 4 diff --git a/app/src/split/wired/central.c b/app/src/split/wired/central.c index 32dc6be1c..1284d4c7c 100644 --- a/app/src/split/wired/central.c +++ b/app/src/split/wired/central.c @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -64,6 +66,14 @@ static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios); #endif +#define HAS_DETECT_GPIO DT_INST_NODE_HAS_PROP(0, detect_gpios) + +#if HAS_DETECT_GPIO + +static const struct gpio_dt_spec detect_gpio = GPIO_DT_SPEC_INST_GET(0, detect_gpios); + +#endif + #else #error \ @@ -121,6 +131,43 @@ static void begin_tx(void) { #endif } +static void begin_rx(void) { +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_get(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_RESUME); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) + uart_irq_rx_enable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) + zmk_split_wired_async_rx(&async_state); +#else + k_timer_start(&wired_central_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), + K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); +#endif +} + +#if HAS_DETECT_GPIO + +static void stop_rx(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) + uart_irq_rx_disable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) + zmk_split_wired_async_rx_cancel(&async_state); +#else + k_timer_stop(&wired_central_read_timer); +#endif + +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_put(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) +} + +#endif // HAS_DETECT_GPIO + static ssize_t get_payload_data_size(const struct zmk_split_transport_central_command *cmd) { switch (cmd->type) { case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS: @@ -195,8 +242,6 @@ void rx_done_cb(struct k_work *work) { .type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS, }); - begin_tx(); - k_work_reschedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); } @@ -249,11 +294,34 @@ static K_TIMER_DEFINE(wired_central_read_timer, read_timer_cb, NULL); #endif +#if HAS_DETECT_GPIO + +static void notify_transport_status(void); + +static struct gpio_callback detect_callback; + +static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); } + +static K_WORK_DEFINE(notify_status_work, notify_status_work_cb); + +static void detect_pin_irq_callback_handler(const struct device *port, struct gpio_callback *cb, + const gpio_port_pins_t pin) { + k_work_submit(¬ify_status_work); +} + +#endif + static int zmk_split_wired_central_init(void) { if (!device_is_ready(uart)) { return -ENODEV; } +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_put(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) + #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) int ret = uart_irq_callback_user_data_set(uart, serial_cb, NULL); @@ -285,18 +353,29 @@ static int zmk_split_wired_central_init(void) { #if IS_HALF_DUPLEX_MODE #if HAS_DIR_GPIO - LOG_DBG("CONFIGURING AS OUTPUT"); gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE); #endif - k_work_schedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); +#endif // IS_HALF_DUPLEX_MODE -#endif +#if HAS_DETECT_GPIO -#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING) - k_timer_start(&wired_central_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), - K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); -#endif + gpio_pin_configure_dt(&detect_gpio, GPIO_INPUT); + + gpio_init_callback(&detect_callback, detect_pin_irq_callback_handler, BIT(detect_gpio.pin)); + int err = gpio_add_callback(detect_gpio.port, &detect_callback); + if (err) { + LOG_ERR("Error adding the callback to the detect pin: %i", err); + return err; + } + + err = gpio_pin_interrupt_configure_dt(&detect_gpio, GPIO_INT_EDGE_BOTH); + if (err < 0) { + LOG_WRN("Failed to so configure interrupt for detection pin (%d)", err); + return err; + } + +#endif // HAS_DETECT_GPIO return 0; } @@ -309,12 +388,78 @@ static int split_central_wired_get_available_source_ids(uint8_t *sources) { return 1; } +static int split_central_wired_set_enabled(bool enabled) { + if (enabled) { + begin_rx(); +#if IS_HALF_DUPLEX_MODE + k_work_schedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); +#endif + return 0; +#if HAS_DETECT_GPIO + } else { +#if IS_HALF_DUPLEX_MODE + k_work_cancel_delayable(&rx_done_work); +#endif + stop_rx(); + return 0; +#endif + } + + return -ENOTSUP; +} + +#if HAS_DETECT_GPIO + +static zmk_split_transport_central_status_changed_cb_t transport_status_cb; + +static int +split_central_wired_set_status_callback(zmk_split_transport_central_status_changed_cb_t cb) { + transport_status_cb = cb; + return 0; +} + +static struct zmk_split_transport_status split_central_wired_get_status() { + int detected = gpio_pin_get_dt(&detect_gpio); + if (detected > 0) { + return (struct zmk_split_transport_status){ + .available = true, + .enabled = true, // Track this + .connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED, + + }; + } else { + return (struct zmk_split_transport_status){ + .available = false, + .enabled = true, // Track this + .connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED, + + }; + } +} + +#endif // HAS_DETECT_GPIO + static const struct zmk_split_transport_central_api central_api = { .send_command = split_central_wired_send_command, .get_available_source_ids = split_central_wired_get_available_source_ids, + .set_enabled = split_central_wired_set_enabled, +#if HAS_DETECT_GPIO + .set_status_callback = split_central_wired_set_status_callback, + .get_status = split_central_wired_get_status, +#endif // HAS_DETECT_GPIO }; -ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, ¢ral_api); +ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, ¢ral_api, CONFIG_ZMK_SPLIT_WIRED_PRIORITY); + +#if HAS_DETECT_GPIO + +static void notify_transport_status(void) { + if (transport_status_cb) { + transport_status_cb(&wired_central, split_central_wired_get_status()); + } +} + +#endif static void publish_events_work(struct k_work *work) { diff --git a/app/src/split/wired/peripheral.c b/app/src/split/wired/peripheral.c index aa2a110b0..562da72bf 100644 --- a/app/src/split/wired/peripheral.c +++ b/app/src/split/wired/peripheral.c @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -53,15 +55,24 @@ K_SEM_DEFINE(tx_sem, 0, 1); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) -#define HAS_DIR_GPIO (DT_INST_NODE_HAS_PROP(0, dir_gpios)) static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device)); +#define HAS_DIR_GPIO (DT_INST_NODE_HAS_PROP(0, dir_gpios)) + #if HAS_DIR_GPIO static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_gpios); #endif +#define HAS_DETECT_GPIO DT_INST_NODE_HAS_PROP(0, detect_gpios) + +#if HAS_DETECT_GPIO + +static const struct gpio_dt_spec detect_gpio = GPIO_DT_SPEC_INST_GET(0, detect_gpios); + +#endif + #else #error \ @@ -93,15 +104,43 @@ static struct zmk_split_wired_async_state async_state = { }; #endif -#if HAS_DIR_GPIO -static void set_dir(uint8_t tx) { gpio_pin_set_dt(&dir_gpio, tx); } +static void begin_rx(void) { +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_get(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_RESUME); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) + uart_irq_rx_enable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) + zmk_split_wired_async_rx(&async_state); #else - -static inline void set_dir(uint8_t tx) {} - + k_timer_start(&wired_central_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), + K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); #endif +} + +#if HAS_DETECT_GPIO + +static void stop_rx(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) + uart_irq_rx_disable(uart); +#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) + zmk_split_wired_async_rx_cancel(&async_state); +#else + k_timer_stop(&wired_central_read_timer); +#endif + +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_put(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) +} + +#endif // HAS_DETECT_GPIO #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) @@ -116,11 +155,15 @@ static void serial_cb(const struct device *dev, void *user_data) { uart_irq_tx_disable(dev); } - set_dir(0); +#if HAS_DIR_GPIO + gpio_pin_set_dt(&dir_gpio, 0); +#endif } if (uart_irq_tx_ready(dev)) { - set_dir(1); +#if HAS_DIR_GPIO + gpio_pin_set_dt(&dir_gpio, 1); +#endif zmk_split_wired_fifo_fill(dev, &chosen_tx_buf); } } @@ -142,11 +185,34 @@ static K_TIMER_DEFINE(wired_peripheral_read_timer, wired_peripheral_read_tick_cb #endif +#if HAS_DETECT_GPIO + +static void notify_transport_status(void); + +static struct gpio_callback detect_callback; + +static void notify_status_work_cb(struct k_work *_work) { notify_transport_status(); } + +static K_WORK_DEFINE(notify_status_work, notify_status_work_cb); + +static void detect_pin_irq_callback_handler(const struct device *port, struct gpio_callback *cb, + const gpio_port_pins_t pin) { + k_work_submit(¬ify_status_work); +} + +#endif + static int zmk_split_wired_peripheral_init(void) { if (!device_is_ready(uart)) { return -ENODEV; } +#if IS_ENABLED(CONFIG_PM_DEVICE_RUNTIME) + pm_device_runtime_put(uart); +#elif IS_ENABLED(CONFIG_PM_DEVICE) + pm_device_action_run(uart, PM_DEVICE_ACTION_SUSPEND); +#endif // IS_ENABLED(CONFIG_PM_DEVICE) + #if HAS_DIR_GPIO gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE); #endif @@ -166,7 +232,6 @@ static int zmk_split_wired_peripheral_init(void) { return ret; } - uart_irq_rx_enable(uart); #elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) async_state.uart = uart; int ret = zmk_split_wired_async_init(&async_state); @@ -174,11 +239,26 @@ static int zmk_split_wired_peripheral_init(void) { LOG_ERR("Failed to set up async wired split UART (%d)", ret); return ret; } +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) -#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING) - k_timer_start(&wired_peripheral_read_timer, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD), - K_TICKS(CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD)); -#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) +#if HAS_DETECT_GPIO + + gpio_pin_configure_dt(&detect_gpio, GPIO_INPUT); + + gpio_init_callback(&detect_callback, detect_pin_irq_callback_handler, BIT(detect_gpio.pin)); + int err = gpio_add_callback(detect_gpio.port, &detect_callback); + if (err) { + LOG_ERR("Error adding the callback to the detect pin: %i", err); + return err; + } + + err = gpio_pin_interrupt_configure_dt(&detect_gpio, GPIO_INT_EDGE_BOTH); + if (err < 0) { + LOG_WRN("Failed to so configure interrupt for detection pin (%d)", err); + return err; + } + +#endif // HAS_DETECT_GPIO return 0; } @@ -260,11 +340,81 @@ split_peripheral_wired_report_event(const struct zmk_split_transport_peripheral_ return 0; } +static bool is_enabled; + +static int split_peripheral_wired_set_enabled(bool enabled) { + if (is_enabled == enabled) { + return 0; + } + + is_enabled = enabled; + + if (enabled) { + begin_rx(); + return 0; +#if HAS_DETECT_GPIO + } else { + stop_rx(); + return 0; +#endif + } + + return -ENOTSUP; +} + +#if HAS_DETECT_GPIO + +static zmk_split_transport_peripheral_status_changed_cb_t transport_status_cb; + +static int +split_peripheral_wired_set_status_callback(zmk_split_transport_peripheral_status_changed_cb_t cb) { + transport_status_cb = cb; + return 0; +} + +static struct zmk_split_transport_status split_peripheral_wired_get_status() { + int detected = gpio_pin_get_dt(&detect_gpio); + if (detected > 0) { + return (struct zmk_split_transport_status){ + .available = true, + .enabled = true, // Track this + .connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_ALL_CONNECTED, + + }; + } else { + return (struct zmk_split_transport_status){ + .available = false, + .enabled = true, // Track this + .connections = ZMK_SPLIT_TRANSPORT_CONNECTIONS_STATUS_DISCONNECTED, + + }; + } +} + +#endif // HAS_DETECT_GPIO + static const struct zmk_split_transport_peripheral_api peripheral_api = { .report_event = split_peripheral_wired_report_event, + .set_enabled = split_peripheral_wired_set_enabled, +#if HAS_DETECT_GPIO + .set_status_callback = split_peripheral_wired_set_status_callback, + .get_status = split_peripheral_wired_get_status, +#endif // HAS_DETECT_GPIO }; -ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(wired_peripheral, &peripheral_api); +ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(wired_peripheral, &peripheral_api, + CONFIG_ZMK_SPLIT_WIRED_PRIORITY); + +#if HAS_DETECT_GPIO + +static void notify_transport_status(void) { + if (transport_status_cb) { + LOG_DBG("Invoking the status CB"); + transport_status_cb(&wired_peripheral, split_peripheral_wired_get_status()); + } +} + +#endif // HAS_DETECT_GPIO static void process_tx_cb(void) { while (ring_buf_size_get(&chosen_rx_buf) > MSG_EXTRA_SIZE) { diff --git a/app/src/split/wired/wired.c b/app/src/split/wired/wired.c index 159f429ee..0df4c7092 100644 --- a/app/src/split/wired/wired.c +++ b/app/src/split/wired/wired.c @@ -148,21 +148,30 @@ void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state) { } } -static void restart_rx_work_cb(struct k_work *work) { - struct k_work_delayable *dwork = k_work_delayable_from_work(work); - struct zmk_split_wired_async_state *state = - CONTAINER_OF(dwork, struct zmk_split_wired_async_state, restart_rx_work); +int zmk_split_wired_async_rx(struct zmk_split_wired_async_state *state) { atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED); - LOG_WRN("RESTART!"); - int ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len, CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT); if (ret < 0) { LOG_ERR("Failed to enable RX (%d)", ret); } + + return ret; +} + +int zmk_split_wired_async_rx_cancel(struct zmk_split_wired_async_state *state) { + return uart_rx_disable(state->uart); +} + +static void restart_rx_work_cb(struct k_work *work) { + struct k_work_delayable *dwork = k_work_delayable_from_work(work); + struct zmk_split_wired_async_state *state = + CONTAINER_OF(dwork, struct zmk_split_wired_async_state, restart_rx_work); + + zmk_split_wired_async_rx(state); } static void async_uart_cb(const struct device *dev, struct uart_event *ev, void *user_data) { @@ -247,14 +256,14 @@ int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state) { return ret; } - atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); + // atomic_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED); - ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len, - CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT); - if (ret < 0) { - LOG_ERR("Failed to enable RX (%d)", ret); - return ret; - } + // ret = uart_rx_enable(state->uart, state->rx_bufs[0], state->rx_bufs_len, + // CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT); + // if (ret < 0) { + // LOG_ERR("Failed to enable RX (%d)", ret); + // return ret; + // } return 0; } diff --git a/app/src/split/wired/wired.h b/app/src/split/wired/wired.h index ab39eb27f..d48f29b78 100644 --- a/app/src/split/wired/wired.h +++ b/app/src/split/wired/wired.h @@ -86,8 +86,10 @@ struct zmk_split_wired_async_state { const struct gpio_dt_spec *dir_gpio; }; -void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state); int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state); +void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state); +int zmk_split_wired_async_rx(struct zmk_split_wired_async_state *state); +int zmk_split_wired_async_rx_cancel(struct zmk_split_wired_async_state *state); #endif diff --git a/docs/docs/development/hardware-integration/pinctrl.mdx b/docs/docs/development/hardware-integration/pinctrl.mdx index 62ad9f185..7c61f50dc 100644 --- a/docs/docs/development/hardware-integration/pinctrl.mdx +++ b/docs/docs/development/hardware-integration/pinctrl.mdx @@ -358,6 +358,28 @@ In the pin control file: bias-pull-up; }; }; + + uart0_default: uart0_default { + group1 { + psels = , // EXT1 + ; // EXT2 + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + /* configure P0.1 as UART_TX and P0.2 as UART_RTS */ + psels = , ; + low-power-enable; + }; + group2 { + /* configure P0.3 as UART_RX and P0.4 as UART_CTS */ + psels = , ; + /* both P0.3 and P0.4 are configured with pull-up */ + bias-pull-up; + low-power-enable; + }; + }; }; ``` @@ -368,10 +390,35 @@ In the main file: &uart0 { pinctrl-0 = <&uart0_default>; - pinctrl-names = "default"; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; }; ``` +On designs using wired split on nRF52840, using asynchronous UART APIs with DMA will help ensure that the interrupts used to handle timing sensitive BT interactions can respond when needed. + +This can be accomplished by overwriding the `compatible` property to `"nordic,nrf-uarte"`, e.g.: + +```dts +&uart0 { + compatible = "nordic,nrf-uarte"; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; +``` + +In addition to this `compatible` override, a setting needs to be tweaked to ensure that the UART isn't selected for interrupt mode when async is prefered. + +The following should be added to the board's/shield's `Kconfig.defconfig`: + +```dts +config UART_0_INTERRUPT_DRIVEN + depends on !ZMK_SPLIT_WIRED_UART_MODE_ASYNC +``` + +for the correct `UART_#` prefix matching the numbered UART being used. + #### UART rp2040 In the pin control file: diff --git a/docs/docs/features/split-keyboards.md b/docs/docs/features/split-keyboards.md index e7b28e1d6..c10c305bd 100644 --- a/docs/docs/features/split-keyboards.md +++ b/docs/docs/features/split-keyboards.md @@ -5,20 +5,6 @@ sidebar_label: Split Keyboards ZMK supports setups where a keyboard is split into two or more physical parts (also called "sides" or "halves" when split in two), each with their own controller running ZMK. The parts communicate with each other to work as a single keyboard device. -:::note[Split communication protocols] -ZMK supports split keyboards that communicate with each other wirelessly over BLE. - -Full-duplex UART, wired split support is currently experimental, and is available for advanced/technical users to test. - -Future single-wire, half-duplex UART support, which is planned, will allow using wired ZMK with designs like Corne, Sweep, etc. that use only a single GPIO pin for bidirectional communication between split sides. -::: - -:::warning[Hot Plugging Cables] - -Many popular cables, in particular, TRRS/TRS cables, can cause irreparable damage to controllers if they are inserted or removed when power is already present on them. Whether or not you are using the wired split functionality or not, _never_ insert or remove such a cable when a controller is powered by USB _or_ battery. - -::: - ## Central and Peripheral Roles In split keyboards running ZMK, one part is assigned the "central" role which receives key position and sensor events from the other parts that are called "peripherals." @@ -46,6 +32,40 @@ Also see the reference section on [split keyboards configuration](../config/spli Since peripherals communicate through centrals, the key and sensor events originating from them will naturally have a larger latency, especially with a wireless split communication protocol. For the currently used BLE-based transport, split communication increases the average latency by 3.75ms with a worst case increase of 7.5ms. +## Split Transports + +ZMK supports two transports for connecting split parts: Bluetooth and full-duplex wired UART. Only one transport can be active at a time, so designs involving some portions connected via Bluetooth and others via full-duplex wired are _not_ supported. + +:::warning[Hot Plugging Cables] + +Many popular cables, in particular, TRRS/TRS cables, can cause irreparable damage to controllers if they are inserted or removed when power is already present on them. Whether or not you are using the wired split functionality or not, _never_ insert or remove such a cable when a controller is powered by USB _or_ battery. + +::: + +### Bluetooth + +[Bluetooth](./bluetooth.md) is the most well tested and flexible transport available in ZMK. Using Bluetooth, a central can connect to multiple peripherals, enabling the use of a [dongle](../development/hardware-integration/dongle.mdx) to improve battery life, or allowing for multi-part split keyboards. + +This transport will be enabled for designs that set `CONFIG_ZMK_SPLIT=y` and have `CONFIG_ZMK_BLE=y` set by a supported MCU/controller. + +### Full-Duplex Wired (UART) + +The full-duplex wired UART transport is a recent addition, and is intended for testing by early adopters. It allows for fully functional communication between one central and one peripheral. Unlike the Bluetooth transport, the central cannot currently have more than one peripheral connected via wired split. + +This transport will be enabled for designs that set `CONFIG_ZMK_SPLIT=y` and have a node with `compatible = "zmk,wired-split";` present in their devicetree configuration. + +:::note[Full Duplex vs Half Duplex] +Full-duplex UART requires the use of two wires connecting the halves. Future half-duplex (single-wire) UART support, which is planned, will allow using wired ZMK with designs such as the Corne, Sweep, etc. that use only a single GPIO pin for bidirectional communication between split sides. + +Until half-duplex support is completed, those particular designs will not work with the wired split transport, and can only be used with the Bluetooth transport. +::: + +### Runtime Switching + +ZMK features highly experimental support for switching between the two available transports. This requires specially designed hardware, and attempting to use this feature on a keyboard not explicitly designed for it (e.g. Corne, Sofle) _WILL_ cause permanent damage. + +Currently, there are no open source/reference designs that implement this functionality, and only experienced designers with extensive EE knowledge should attempt to implement a design with this functionality. + ## Building and Flashing Firmware ZMK split keyboards require building and flashing different firmware files for each split part.