zmk/app/src/split/central.c
Pete Johanson 6b44d33db2
feat(split): Runtime selection of split transport (#2886)
feat(split): Runtime selection of split transport

Allow building multiple split transports, and select an active
one based on the transport availability. Wired split availability
depends on additional `detect-gpios` which must be a GPIO pin
that goes active when a wired connection is present.

feat(split): Suspend/resume wired UART devices.

To better support runtime split support, suspend/resume the UART
as necessary to save power when not using the UART.

docs(split): Document adjusting nRF52 UART interrupt priorities

For wired split on nRF52, you may need to adjust the priority for UART
interrupts lower, to ensure the interrupts used for timing sensitive
BT operations can run when needed, so document this in our pinctrl docs.

refactor(split): Restore use of aync UART on nRF52.

With fixes for Zephyr UART driver, re-enable using async API on nRF52.

fix(split): Minor wired split fixes.

Various minor fixes for wired split to avoid spurious TX in half duplex,
etc.

fix: Unconditionally define HID payloads to avoid error.

Don't conditionally define HID indicator payload, to avoid compilation
errors.

docs(split): Expand on details of split transports.

Expand the split keyboard documentation with a more fleshed out section
on the available split trasnports, and what is and isn't supported by
each, including the runtime selection functionality.

---------

Co-authored-by: Nicolas Munnich <98408764+nmunnich@users.noreply.github.com>
2025-06-16 03:45:25 -04:00

227 lines
8.1 KiB
C

/*
* 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);
const 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 = {
.source = source,
.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 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);