/* * Copyright (c) 2025 The ZMK Contributors * * SPDX-License-Identifier: MIT */ #include #include #include #include #include #include #include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include #include #include #include #include #include #include #include #include #include #include "wired.h" #define DT_DRV_COMPAT zmk_wired_split #define IS_HALF_DUPLEX_MODE \ (DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) && DT_INST_PROP_OR(0, half_duplex, false)) #define RX_BUFFER_SIZE \ ((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \ CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS) #define TX_BUFFER_SIZE \ ((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \ CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS) #if IS_HALF_DUPLEX_MODE static K_SEM_DEFINE(tx_sem, 0, 1); #endif RING_BUF_DECLARE(rx_buf, RX_BUFFER_SIZE); RING_BUF_DECLARE(tx_buf, TX_BUFFER_SIZE); #if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) static const struct device *uart = DEVICE_DT_GET(DT_INST_PHANDLE(0, device)); #define HAS_DIR_GPIO (IS_HALF_DUPLEX_MODE && 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 \ "Need to create a node with compatible of 'zmk,wired-split` with a `device` property set to an enabled UART. See http://zmk.dev/docs/development/hardware-integration/new-shield#wired-split" #endif static void publish_events_work(struct k_work *work); K_WORK_DEFINE(publish_events, publish_events_work); #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) uint8_t async_rx_buf[2][RX_BUFFER_SIZE / 2]; static struct zmk_split_wired_async_state async_state = { .process_tx_work = &publish_events, .rx_bufs = {async_rx_buf[0], async_rx_buf[1]}, .rx_bufs_len = RX_BUFFER_SIZE / 2, .rx_size_process_trigger = MSG_EXTRA_SIZE + 1, .rx_buf = &rx_buf, .tx_buf = &tx_buf, #if HAS_DIR_GPIO .dir_gpio = &dir_gpio, #endif }; #endif #if IS_HALF_DUPLEX_MODE static int can_tx(void) { return k_sem_take(&tx_sem, K_NO_WAIT); } #else static inline int can_tx(void) { return 0; } #endif #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING) static void send_pending_tx_work_cb(struct k_work *work); static K_WORK_DEFINE(wired_central_tx_work, send_pending_tx_work_cb); static void read_timer_cb(struct k_timer *_timer) { zmk_split_wired_poll_in(&rx_buf, uart, &publish_events, NULL); // Check if we found any bytes, read some, or read all the bytes in the RX } static K_TIMER_DEFINE(wired_central_read_timer, read_timer_cb, NULL); #endif static void begin_tx(void) { #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) uart_irq_tx_enable(uart); #elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC) zmk_split_wired_async_tx(&async_state); #else k_work_submit(&wired_central_tx_work); #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: return 0; case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: return sizeof(cmd->data.invoke_behavior); case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT: return sizeof(cmd->data.set_physical_layout); case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: return sizeof(cmd->data.set_hid_indicators); case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: return sizeof(cmd->data.set_rgb_layers); default: return -ENOTSUP; } } static int split_central_wired_send_command(uint8_t source, struct zmk_split_transport_central_command cmd) { if (source != 0) { return -EINVAL; } ssize_t data_size = get_payload_data_size(&cmd); if (data_size < 0) { LOG_WRN("Failed to determine payload data size %d", data_size); return data_size; } // Data + type + source size_t payload_size = data_size + sizeof(source) + sizeof(enum zmk_split_transport_central_command_type); if (ring_buf_space_get(&tx_buf) < MSG_EXTRA_SIZE + payload_size) { LOG_WRN("No room to send command to the peripheral %d", source); return -ENOSPC; } struct command_envelope env = {.prefix = { .magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX, .payload_size = payload_size, }, .payload = { .source = source, .cmd = cmd, }}; struct msg_postfix postfix = {.crc = crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)}; ring_buf_put(&tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size); ring_buf_put(&tx_buf, (uint8_t *)&postfix, sizeof(postfix)); if (can_tx() >= 0) { begin_tx(); } return 0; } #if IS_HALF_DUPLEX_MODE void rx_done_cb(struct k_work *work); static K_WORK_DELAYABLE_DEFINE(rx_done_work, rx_done_cb); void rx_done_cb(struct k_work *work) { k_sem_give(&tx_sem); // Poll for the next event data! split_central_wired_send_command(0, (struct zmk_split_transport_central_command){ .type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS, }); k_work_reschedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT)); } #endif #if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT) static void serial_cb(const struct device *dev, void *user_data) { while (uart_irq_update(dev) && uart_irq_is_pending(dev)) { if (uart_irq_rx_ready(dev)) { zmk_split_wired_fifo_read(dev, &rx_buf, &publish_events, NULL); #if IS_HALF_DUPLEX_MODE k_work_reschedule(&rx_done_work, K_TICKS(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT)); #endif } if (uart_irq_tx_complete(dev)) { if (ring_buf_size_get(&tx_buf) == 0) { uart_irq_tx_disable(dev); #if HAS_DIR_GPIO gpio_pin_set_dt(&dir_gpio, 0); #endif } } if (uart_irq_tx_ready(dev)) { #if HAS_DIR_GPIO gpio_pin_set_dt(&dir_gpio, 1); #endif zmk_split_wired_fifo_fill(dev, &tx_buf); } } } #elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING) static void send_pending_tx_work_cb(struct k_work *work) { zmk_split_wired_poll_out(&tx_buf, uart); } #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); if (ret < 0) { if (ret == -ENOTSUP) { LOG_ERR("Interrupt-driven UART API support not enabled"); } else if (ret == -ENOSYS) { LOG_ERR("UART device does not support interrupt-driven API"); } else { LOG_ERR("Error setting UART callback: %d\n", ret); } 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); if (ret < 0) { LOG_ERR("Failed to set up async wired split UART (%d)", ret); return ret; } #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_*) #if IS_HALF_DUPLEX_MODE #if HAS_DIR_GPIO gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE); #endif #endif // IS_HALF_DUPLEX_MODE #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; } SYS_INIT(zmk_split_wired_central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); static int split_central_wired_get_available_source_ids(uint8_t *sources) { sources[0] = 0; 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, 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) { #if IS_HALF_DUPLEX_MODE k_work_reschedule(&rx_done_work, K_MSEC(CONFIG_ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT)); #endif // IS_HALF_DUPLEX_MODE while (ring_buf_size_get(&rx_buf) > MSG_EXTRA_SIZE) { struct event_envelope env; int item_err = zmk_split_wired_get_item(&rx_buf, (uint8_t *)&env, sizeof(struct event_envelope)); switch (item_err) { case 0: zmk_split_transport_central_peripheral_event_handler(&wired_central, env.payload.source, env.payload.event); break; case -EAGAIN: return; default: LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err); return; } } }