zmk/app/src/split/wired/central.c

490 lines
14 KiB
C

/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/types.h>
#include <zephyr/init.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/crc.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zmk/stdlib.h>
#include <zmk/behavior.h>
#include <zmk/sensors.h>
#include <zmk/split/transport/central.h>
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/pointing/input_split.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/physical_layouts.h>
#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(&notify_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, &central_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;
}
}
}