Feature: Full-Duplex Wired Split (#2766)

refactor(split): Refactor split code for extension

Extract central/peripheral code to allow for plugging in alternate
transports, instead of tying all split logic to BT.

feat(split): Add full-duplex wired split support

* Depends on full-duplex hardware UART for communication.
* Supports all existing central commands/peripheral events, including
  sensors/inputs from peripherals.
* Only one wired split peripheral supported (for now)
* Relies on chosen `zmk,split-uart` referencing the UART device.

docs: Add wired split config docs.

Migrate split to its own dedicated config file, and add details
on wired split config.

Co-authored-by: Nicolas Munnich <98408764+Nick-Munnich@users.noreply.github.com>

fix: Properly override stack size on RP2040

Move the system work queue stack size override on RP2040 ouf of
a `ZMK_BLE` conditional so it is properly applied generally for that
SoC.

---------

Co-authored-by: Nicolas Munnich <98408764+Nick-Munnich@users.noreply.github.com>
This commit is contained in:
Pete Johanson 2025-03-18 00:48:32 -06:00 committed by GitHub
parent 5ba7e260f4
commit 147c340c6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2201 additions and 373 deletions

173
app/src/split/central.c Normal file
View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <errno.h>
#include <zmk/stdlib.h>
#include <zmk/split/transport/central.h>
#include <zmk/split/central.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/pointing/input_split.h>
#include <zephyr/logging/log.h>
#include <zmk/event_manager.h>
#include <zmk/events/battery_state_changed.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
// TODO: Active transport selection
struct zmk_split_transport_central *active_transport;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
static uint8_t peripheral_battery_levels[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT] = {0};
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
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) {
if (transport != active_transport) {
// Ignoring events from non-active transport
LOG_WRN("Ignoring peripheral event from non-active transport");
return -EINVAL;
}
switch (ev.type) {
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT: {
struct zmk_position_state_changed state_ev = {.source = source,
.position =
ev.data.key_position_event.position,
.state = ev.data.key_position_event.pressed,
.timestamp = k_uptime_get()};
return raise_zmk_position_state_changed(state_ev);
}
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT: {
return zmk_input_split_report_peripheral_event(
ev.data.input_event.reg, ev.data.input_event.type, ev.data.input_event.code,
ev.data.input_event.value, ev.data.input_event.sync);
}
#endif // IS_ENABLED(CONFIG_ZMK_POINTING)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT: {
struct zmk_peripheral_battery_state_changed battery_ev = {
.state_of_charge = ev.data.battery_event.level,
};
peripheral_battery_levels[source] = ev.data.battery_event.level;
return raise_zmk_peripheral_battery_state_changed(battery_ev);
}
#endif
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT: {
struct zmk_sensor_event sensor_ev = {.sensor_index = ev.data.sensor_event.sensor_index,
.channel_data_size = 1,
.timestamp = k_uptime_get()};
sensor_ev.channel_data[0] = ev.data.sensor_event.channel_data;
return raise_zmk_sensor_event(sensor_ev);
}
default:
LOG_WRN("GOT AN UNKNOWN EVENT TYPE %d", ev.type);
return -ENOTSUP;
}
}
int zmk_split_central_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
struct zmk_behavior_binding_event event, bool state) {
if (!active_transport || !active_transport->api || !active_transport->api->send_command) {
return -ENODEV;
}
struct zmk_split_transport_central_command command =
(struct zmk_split_transport_central_command){
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
.data =
{
.invoke_behavior =
{
.param1 = binding->param1,
.param2 = binding->param2,
.position = event.position,
.event_source = event.source,
.state = state ? 1 : 0,
},
},
};
const size_t payload_dev_size = sizeof(command.data.invoke_behavior.behavior_dev);
if (strlcpy(command.data.invoke_behavior.behavior_dev, binding->behavior_dev,
payload_dev_size) >= payload_dev_size) {
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
binding->behavior_dev, command.data.invoke_behavior.behavior_dev);
}
return active_transport->api->send_command(source, command);
};
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators) {
if (!active_transport || !active_transport->api ||
!active_transport->api->get_available_source_ids || !active_transport->api->send_command) {
return -ENODEV;
}
uint8_t source_ids[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT];
int ret = active_transport->api->get_available_source_ids(source_ids);
if (ret < 0) {
return ret;
}
struct zmk_split_transport_central_command command =
(struct zmk_split_transport_central_command){
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS,
.data =
{
.set_hid_indicators =
{
.indicators = indicators,
},
},
};
for (size_t i = 0; i < ret; i++) {
ret = active_transport->api->send_command(source_ids[i], command);
if (ret < 0) {
return ret;
}
}
return 0;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
return -EINVAL;
}
*level = peripheral_battery_levels[source];
return 0;
}
#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);
return 0;
}
SYS_INIT(central_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);