forked from kofal.net/zmk
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:
@@ -189,10 +189,6 @@ config BT_CTLR_PHY_2M
|
||||
config BT_TINYCRYPT_ECC
|
||||
default y if BT_HCI && !BT_CTLR
|
||||
|
||||
config SYSTEM_WORKQUEUE_STACK_SIZE
|
||||
default 4096 if SOC_RP2040
|
||||
default 2048
|
||||
|
||||
config ZMK_BLE_THREAD_STACK_SIZE
|
||||
int "BLE notify thread stack size"
|
||||
default 768
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Copyright (c) 2024 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
config SYSTEM_WORKQUEUE_STACK_SIZE
|
||||
default 2048 if SOC_RP2040
|
||||
default 2048 if ZMK_BLE
|
||||
|
||||
# HID
|
||||
if ZMK_HID_REPORT_TYPE_HKRO
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ if BOARD_GLOVE80_LH
|
||||
config BOARD
|
||||
default "glove80 lh"
|
||||
|
||||
config ZMK_SPLIT_BLE_ROLE_CENTRAL
|
||||
config ZMK_SPLIT_ROLE_CENTRAL
|
||||
default y
|
||||
|
||||
endif # BOARD_GLOVE80_LH
|
||||
|
||||
@@ -11,11 +11,20 @@ left_encoder: &encoder {
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
&arduino_serial {
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
/ {
|
||||
chosen {
|
||||
zmk,physical-layout = &split_matrix_physical_layout;
|
||||
};
|
||||
|
||||
wired_split {
|
||||
compatible = "zmk,wired-split";
|
||||
device = <&arduino_serial>;
|
||||
};
|
||||
|
||||
split_matrix_transform: split_matrix_transform {
|
||||
compatible = "zmk,matrix-transform";
|
||||
rows = <4>;
|
||||
|
||||
21
app/dts/bindings/zmk,wired-split.yaml
Normal file
21
app/dts/bindings/zmk,wired-split.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2025 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
description: |
|
||||
Complete specification of wired split connection
|
||||
|
||||
compatible: "zmk,wired-split"
|
||||
|
||||
properties:
|
||||
device:
|
||||
type: phandle
|
||||
required: true
|
||||
description: The UART device for wired split communication
|
||||
|
||||
half-duplex:
|
||||
type: boolean
|
||||
description: "Experimental: Enable half-duplex protocol mode"
|
||||
|
||||
dir-gpios:
|
||||
type: phandle-array
|
||||
description: "Experimental: Set the communication direction. Used for RS-422 style comms."
|
||||
9
app/include/linker/zmk-split-transport-central.ld
Normal file
9
app/include/linker/zmk-split-transport-central.ld
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <zephyr/linker/linker-defs.h>
|
||||
|
||||
ITERABLE_SECTION_ROM(zmk_split_transport_central, 4)
|
||||
9
app/include/linker/zmk-split-transport-peripheral.ld
Normal file
9
app/include/linker/zmk-split-transport-peripheral.ld
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <zephyr/linker/linker-defs.h>
|
||||
|
||||
ITERABLE_SECTION_ROM(zmk_split_transport_peripheral, 4)
|
||||
@@ -10,8 +10,7 @@
|
||||
#include <zmk/ble/profile.h>
|
||||
|
||||
#define ZMK_BLE_IS_CENTRAL \
|
||||
(IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_BLE) && \
|
||||
IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL))
|
||||
(IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL))
|
||||
|
||||
#if ZMK_BLE_IS_CENTRAL
|
||||
#define ZMK_BLE_PROFILE_COUNT (CONFIG_BT_MAX_PAIRED - CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/bluetooth/addr.h>
|
||||
#include <zmk/behavior.h>
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
#include <zmk/hid_indicators_types.h>
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event, bool state);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators);
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
|
||||
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level);
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
@@ -37,11 +37,3 @@ struct zmk_split_input_event_payload {
|
||||
uint32_t value;
|
||||
uint8_t sync;
|
||||
} __packed;
|
||||
|
||||
int zmk_split_bt_position_pressed(uint8_t position);
|
||||
int zmk_split_bt_position_released(uint8_t position);
|
||||
int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
|
||||
const struct zmk_sensor_channel_data channel_data[],
|
||||
size_t channel_data_size);
|
||||
|
||||
int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync);
|
||||
|
||||
48
app/include/zmk/split/central.h
Normal file
48
app/include/zmk/split/central.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/bluetooth/addr.h>
|
||||
#include <zmk/behavior.h>
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
|
||||
|
||||
#include <zmk/ble.h>
|
||||
#define BLE_PERIPHERAL_COUNT ZMK_SPLIT_BLE_PERIPHERAL_COUNT
|
||||
|
||||
#else
|
||||
|
||||
#define BLE_PERIPHERAL_COUNT 0
|
||||
|
||||
#endif
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED)
|
||||
#define WIRED_PERIPHERAL_COUNT 1
|
||||
#else
|
||||
#define WIRED_PERIPHERAL_COUNT 0
|
||||
#endif
|
||||
|
||||
#define ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT MAX(BLE_PERIPHERAL_COUNT, WIRED_PERIPHERAL_COUNT)
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
#include <zmk/hid_indicators_types.h>
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
int zmk_split_central_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event, bool state);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators);
|
||||
|
||||
#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);
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
11
app/include/zmk/split/peripheral.h
Normal file
11
app/include/zmk/split/peripheral.h
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zmk/split/transport/types.h>
|
||||
|
||||
int zmk_split_peripheral_report_event(const struct zmk_split_transport_peripheral_event *event);
|
||||
33
app/include/zmk/split/transport/central.h
Normal file
33
app/include/zmk/split/transport/central.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/types.h>
|
||||
|
||||
#include <zmk/split/transport/types.h>
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
struct zmk_split_transport_central {
|
||||
const struct zmk_split_transport_central_api *api;
|
||||
};
|
||||
|
||||
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) = { \
|
||||
.api = _api, \
|
||||
};
|
||||
31
app/include/zmk/split/transport/peripheral.h
Normal file
31
app/include/zmk/split/transport/peripheral.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/types.h>
|
||||
|
||||
#include <zmk/split/transport/types.h>
|
||||
|
||||
typedef int (*zmk_split_central_report_event_callback_t)(
|
||||
const struct zmk_split_transport_peripheral_event *event);
|
||||
|
||||
struct zmk_split_transport_peripheral_api {
|
||||
zmk_split_central_report_event_callback_t report_event;
|
||||
};
|
||||
|
||||
struct zmk_split_transport_peripheral {
|
||||
const struct zmk_split_transport_peripheral_api *api;
|
||||
};
|
||||
|
||||
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) = { \
|
||||
.api = _api, \
|
||||
};
|
||||
76
app/include/zmk/split/transport/types.h
Normal file
76
app/include/zmk/split/transport/types.h
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zmk/hid_indicators_types.h>
|
||||
#include <zmk/sensors.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
|
||||
enum zmk_split_transport_peripheral_event_type {
|
||||
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
|
||||
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
|
||||
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT,
|
||||
ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
|
||||
};
|
||||
|
||||
struct zmk_split_transport_peripheral_event {
|
||||
enum zmk_split_transport_peripheral_event_type type;
|
||||
|
||||
union {
|
||||
struct {
|
||||
uint8_t position;
|
||||
uint8_t pressed;
|
||||
} key_position_event;
|
||||
|
||||
struct {
|
||||
struct zmk_sensor_channel_data channel_data;
|
||||
|
||||
uint8_t sensor_index;
|
||||
} sensor_event;
|
||||
|
||||
struct {
|
||||
uint8_t reg;
|
||||
uint8_t sync;
|
||||
uint8_t type;
|
||||
uint16_t code;
|
||||
int32_t value;
|
||||
} input_event;
|
||||
|
||||
struct {
|
||||
uint8_t level;
|
||||
} battery_event;
|
||||
} data;
|
||||
} __packed;
|
||||
|
||||
enum zmk_split_transport_central_command_type {
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS,
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT,
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS,
|
||||
} __packed;
|
||||
|
||||
struct zmk_split_transport_central_command {
|
||||
enum zmk_split_transport_central_command_type type;
|
||||
|
||||
union {
|
||||
struct {
|
||||
char behavior_dev[16];
|
||||
uint32_t param1, param2;
|
||||
uint32_t position;
|
||||
uint8_t event_source;
|
||||
uint8_t state;
|
||||
} invoke_behavior;
|
||||
|
||||
struct {
|
||||
uint8_t layout_idx;
|
||||
} set_physical_layout;
|
||||
|
||||
struct {
|
||||
zmk_hid_indicators_t indicators;
|
||||
} set_hid_indicators;
|
||||
} data;
|
||||
} __packed;
|
||||
@@ -17,9 +17,8 @@
|
||||
|
||||
#endif
|
||||
|
||||
#include <zmk/ble.h>
|
||||
#if ZMK_BLE_IS_CENTRAL
|
||||
#include <zmk/split/bluetooth/central.h>
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
#include <zmk/split/central.h>
|
||||
#endif
|
||||
|
||||
#include <drivers/behavior.h>
|
||||
@@ -95,19 +94,19 @@ int zmk_behavior_invoke_binding(const struct zmk_behavior_binding *src_binding,
|
||||
case BEHAVIOR_LOCALITY_CENTRAL:
|
||||
return invoke_locally(&binding, event, pressed);
|
||||
case BEHAVIOR_LOCALITY_EVENT_SOURCE:
|
||||
#if ZMK_BLE_IS_CENTRAL // source is a member of event because CONFIG_ZMK_SPLIT is enabled
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
if (event.source == ZMK_POSITION_STATE_CHANGE_SOURCE_LOCAL) {
|
||||
return invoke_locally(&binding, event, pressed);
|
||||
} else {
|
||||
return zmk_split_bt_invoke_behavior(event.source, &binding, event, pressed);
|
||||
return zmk_split_central_invoke_behavior(event.source, &binding, event, pressed);
|
||||
}
|
||||
#else
|
||||
return invoke_locally(&binding, event, pressed);
|
||||
#endif
|
||||
case BEHAVIOR_LOCALITY_GLOBAL:
|
||||
#if ZMK_BLE_IS_CENTRAL
|
||||
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
|
||||
zmk_split_bt_invoke_behavior(i, &binding, event, pressed);
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
for (int i = 0; i < ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT; i++) {
|
||||
zmk_split_central_invoke_behavior(i, &binding, event, pressed);
|
||||
}
|
||||
#endif
|
||||
return invoke_locally(&binding, event, pressed);
|
||||
|
||||
@@ -78,7 +78,7 @@ static struct bt_data zmk_ble_ad[] = {
|
||||
),
|
||||
};
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
|
||||
static bt_addr_le_t peripheral_addrs[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];
|
||||
|
||||
@@ -357,7 +357,7 @@ int zmk_ble_set_device_name(char *name) {
|
||||
return update_advertising();
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
|
||||
int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr) {
|
||||
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
|
||||
@@ -446,7 +446,7 @@ static int ble_profiles_handle_set(const char *name, size_t len, settings_read_c
|
||||
return err;
|
||||
}
|
||||
}
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) && IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
else if (settings_name_steq(name, "peripheral_addresses", &next) && next) {
|
||||
if (len != sizeof(bt_addr_le_t)) {
|
||||
return -EINVAL;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <zmk/hid_indicators.h>
|
||||
#include <zmk/events/hid_indicators_changed.h>
|
||||
#include <zmk/events/endpoint_changed.h>
|
||||
#include <zmk/split/bluetooth/central.h>
|
||||
#include <zmk/split/central.h>
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
@@ -32,8 +32,8 @@ static void raise_led_changed_event(struct k_work *_work) {
|
||||
|
||||
raise_zmk_hid_indicators_changed((struct zmk_hid_indicators_changed){.indicators = indicators});
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) && IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
|
||||
zmk_split_bt_update_hid_indicator(indicators);
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) && IS_ENABLED(CONFIG_ZMK_SPLIT)
|
||||
zmk_split_central_update_hid_indicator(indicators);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t
|
||||
|
||||
#else
|
||||
|
||||
#include <zmk/split/bluetooth/service.h>
|
||||
#include <zmk/split/peripheral.h>
|
||||
|
||||
#define ZIS_INST(n) \
|
||||
static const struct zmk_input_processor_entry processors_##n[] = \
|
||||
@@ -59,8 +59,16 @@ int zmk_input_split_report_peripheral_event(uint8_t reg, uint8_t type, uint16_t
|
||||
zmk_input_processor_handle_event(processors_##n[i].dev, evt, processors_##n[i].param1, \
|
||||
processors_##n[i].param2, NULL); \
|
||||
} \
|
||||
zmk_split_bt_report_input(DT_INST_REG_ADDR(n), evt->type, evt->code, evt->value, \
|
||||
evt->sync); \
|
||||
struct zmk_split_transport_peripheral_event ev = { \
|
||||
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT, \
|
||||
.data = {.input_event = { \
|
||||
.reg = DT_INST_REG_ADDR(n), \
|
||||
.type = evt->type, \
|
||||
.code = evt->code, \
|
||||
.value = evt->value, \
|
||||
.sync = evt->sync, \
|
||||
}}}; \
|
||||
zmk_split_peripheral_report_event(&ev); \
|
||||
} \
|
||||
INPUT_CALLBACK_DEFINE(DEVICE_DT_GET(DT_INST_PHANDLE(n, device)), split_input_handler_##n);
|
||||
|
||||
|
||||
@@ -3,4 +3,16 @@
|
||||
|
||||
if (CONFIG_ZMK_SPLIT_BLE)
|
||||
add_subdirectory(bluetooth)
|
||||
endif()
|
||||
|
||||
if (CONFIG_ZMK_SPLIT_WIRED)
|
||||
add_subdirectory(wired)
|
||||
endif()
|
||||
|
||||
if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
target_sources(app PRIVATE central.c)
|
||||
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-central.ld)
|
||||
else()
|
||||
target_sources(app PRIVATE peripheral.c)
|
||||
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-peripheral.ld)
|
||||
endif()
|
||||
@@ -9,16 +9,20 @@ if ZMK_SPLIT
|
||||
config ZMK_SPLIT_ROLE_CENTRAL
|
||||
bool "Split central device"
|
||||
|
||||
choice ZMK_SPLIT_TRANSPORT
|
||||
prompt "Split transport"
|
||||
|
||||
config ZMK_SPLIT_BLE
|
||||
bool "BLE"
|
||||
bool "BLE Split"
|
||||
default y
|
||||
depends on ZMK_BLE
|
||||
select BT_USER_PHY_UPDATE
|
||||
select BT_AUTO_PHY_UPDATE
|
||||
|
||||
endchoice
|
||||
config ZMK_SPLIT_WIRED
|
||||
bool "Wired Split"
|
||||
default y if !ZMK_SPLIT_BLE
|
||||
depends on DT_HAS_ZMK_WIRED_SPLIT_ENABLED
|
||||
select SERIAL
|
||||
select RING_BUFFER
|
||||
select CRC
|
||||
|
||||
config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
|
||||
bool "Peripheral HID Indicators"
|
||||
@@ -29,3 +33,4 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
|
||||
endif # ZMK_SPLIT
|
||||
|
||||
rsource "bluetooth/Kconfig"
|
||||
rsource "wired/Kconfig"
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
rsource "bluetooth/Kconfig.defaults"
|
||||
rsource "wired/Kconfig.defaults"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
target_sources(app PRIVATE split_listener.c)
|
||||
target_sources(app PRIVATE service.c)
|
||||
target_sources(app PRIVATE peripheral.c)
|
||||
endif()
|
||||
|
||||
@@ -23,6 +23,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
#include <zmk/ble.h>
|
||||
#include <zmk/behavior.h>
|
||||
#include <zmk/sensors.h>
|
||||
#include <zmk/split/transport/central.h>
|
||||
#include <zmk/split/bluetooth/uuid.h>
|
||||
#include <zmk/split/bluetooth/service.h>
|
||||
#include <zmk/event_manager.h>
|
||||
@@ -134,16 +135,15 @@ static bool is_scanning = false;
|
||||
|
||||
static const struct bt_uuid_128 split_service_uuid = BT_UUID_INIT_128(ZMK_SPLIT_BT_SERVICE_UUID);
|
||||
|
||||
K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_position_state_changed),
|
||||
struct peripheral_event_wrapper {
|
||||
uint8_t source;
|
||||
struct zmk_split_transport_peripheral_event event;
|
||||
};
|
||||
|
||||
K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct peripheral_event_wrapper),
|
||||
CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4);
|
||||
|
||||
void peripheral_event_work_callback(struct k_work *work) {
|
||||
struct zmk_position_state_changed ev;
|
||||
while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) {
|
||||
LOG_DBG("Trigger key position state change for %d", ev.position);
|
||||
raise_zmk_position_state_changed(ev);
|
||||
}
|
||||
}
|
||||
void peripheral_event_work_callback(struct k_work *work);
|
||||
|
||||
K_WORK_DEFINE(peripheral_event_work, peripheral_event_work_callback);
|
||||
|
||||
@@ -190,10 +190,13 @@ int release_peripheral_slot(int index) {
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (slot->position_state[i] & BIT(j)) {
|
||||
uint32_t position = (i * 8) + j;
|
||||
struct zmk_position_state_changed ev = {.source = index,
|
||||
.position = position,
|
||||
.state = false,
|
||||
.timestamp = k_uptime_get()};
|
||||
struct peripheral_event_wrapper ev = {
|
||||
.source = index,
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
|
||||
.data = {.key_position_event = {
|
||||
.position = position,
|
||||
.pressed = false,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
@@ -251,18 +254,6 @@ int confirm_peripheral_slot_conn(struct bt_conn *conn) {
|
||||
}
|
||||
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
K_MSGQ_DEFINE(peripheral_sensor_event_msgq, sizeof(struct zmk_sensor_event),
|
||||
CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4);
|
||||
|
||||
void peripheral_sensor_event_work_callback(struct k_work *work) {
|
||||
struct zmk_sensor_event ev;
|
||||
while (k_msgq_get(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT) == 0) {
|
||||
LOG_DBG("Trigger sensor change for %d", ev.sensor_index);
|
||||
raise_zmk_sensor_event(ev);
|
||||
}
|
||||
}
|
||||
|
||||
K_WORK_DEFINE(peripheral_sensor_event_work, peripheral_sensor_event_work_callback);
|
||||
|
||||
static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
|
||||
struct bt_gatt_subscribe_params *params,
|
||||
@@ -282,15 +273,20 @@ static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
|
||||
|
||||
struct sensor_event sensor_event;
|
||||
memcpy(&sensor_event, data, MIN(length, sizeof(sensor_event)));
|
||||
struct zmk_sensor_event ev = {
|
||||
.sensor_index = sensor_event.sensor_index,
|
||||
.channel_data_size = MIN(sensor_event.channel_data_size, ZMK_SENSOR_EVENT_MAX_CHANNELS),
|
||||
.timestamp = k_uptime_get()};
|
||||
if (sensor_event.channel_data_size != 1) {
|
||||
return BT_GATT_ITER_STOP;
|
||||
}
|
||||
|
||||
memcpy(ev.channel_data, sensor_event.channel_data,
|
||||
sizeof(struct zmk_sensor_channel_data) * sensor_event.channel_data_size);
|
||||
k_msgq_put(&peripheral_sensor_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_sensor_event_work);
|
||||
struct peripheral_event_wrapper event_wrapper = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
|
||||
.data = {.sensor_event = {
|
||||
.channel_data = sensor_event.channel_data[0],
|
||||
.sensor_index = sensor_event.sensor_index,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &event_wrapper, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
|
||||
return BT_GATT_ITER_CONTINUE;
|
||||
}
|
||||
@@ -298,27 +294,6 @@ static uint8_t split_central_sensor_notify_func(struct bt_conn *conn,
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
|
||||
|
||||
struct zmk_input_event_msg {
|
||||
uint8_t reg;
|
||||
struct zmk_split_input_event_payload payload;
|
||||
};
|
||||
|
||||
K_MSGQ_DEFINE(peripheral_input_event_msgq, sizeof(struct zmk_input_event_msg), 5, 4);
|
||||
// CONFIG_ZMK_SPLIT_BLE_CENTRAL_INPUT_QUEUE_SIZE, 4);
|
||||
|
||||
void peripheral_input_event_work_callback(struct k_work *work) {
|
||||
struct zmk_input_event_msg msg;
|
||||
while (k_msgq_get(&peripheral_input_event_msgq, &msg, K_NO_WAIT) == 0) {
|
||||
int ret = zmk_input_split_report_peripheral_event(
|
||||
msg.reg, msg.payload.type, msg.payload.code, msg.payload.value, msg.payload.sync);
|
||||
if (ret < 0) {
|
||||
LOG_WRN("Failed to report peripheral event %d", ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_WORK_DEFINE(input_event_work, peripheral_input_event_work_callback);
|
||||
|
||||
static uint8_t peripheral_input_event_notify_cb(struct bt_conn *conn,
|
||||
struct bt_gatt_subscribe_params *params,
|
||||
const void *data, uint16_t length) {
|
||||
@@ -335,18 +310,25 @@ static uint8_t peripheral_input_event_notify_cb(struct bt_conn *conn,
|
||||
return BT_GATT_ITER_STOP;
|
||||
}
|
||||
|
||||
struct zmk_input_event_msg msg;
|
||||
|
||||
memcpy(&msg.payload, data, MIN(length, sizeof(struct zmk_split_input_event_payload)));
|
||||
|
||||
LOG_DBG("Got an input event with type %d, code %d, value %d, sync %d", msg.payload.type,
|
||||
msg.payload.code, msg.payload.value, msg.payload.sync);
|
||||
struct zmk_split_input_event_payload payload;
|
||||
memcpy(&payload, data, MIN(length, sizeof(struct zmk_split_input_event_payload)));
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(peripheral_input_slots); i++) {
|
||||
if (&peripheral_input_slots[i].sub == params) {
|
||||
msg.reg = peripheral_input_slots[i].reg;
|
||||
k_msgq_put(&peripheral_input_event_msgq, &msg, K_NO_WAIT);
|
||||
k_work_submit(&input_event_work);
|
||||
struct peripheral_event_wrapper event_wrapper = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT,
|
||||
.data = {.input_event = {
|
||||
.reg = peripheral_input_slots[i].reg,
|
||||
.sync = payload.sync,
|
||||
.code = payload.code,
|
||||
.type = payload.type,
|
||||
.value = payload.value,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &event_wrapper, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,12 +366,13 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
|
||||
if (slot->changed_positions[i] & BIT(j)) {
|
||||
uint32_t position = (i * 8) + j;
|
||||
bool pressed = slot->position_state[i] & BIT(j);
|
||||
struct zmk_position_state_changed ev = {.source =
|
||||
peripheral_slot_index_for_conn(conn),
|
||||
.position = position,
|
||||
.state = pressed,
|
||||
.timestamp = k_uptime_get()};
|
||||
|
||||
struct peripheral_event_wrapper ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
|
||||
.data = {.key_position_event = {
|
||||
.position = position,
|
||||
.pressed = pressed,
|
||||
}}}};
|
||||
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
}
|
||||
@@ -401,35 +384,6 @@ static uint8_t split_central_notify_func(struct bt_conn *conn,
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
|
||||
static uint8_t peripheral_battery_levels[ZMK_SPLIT_BLE_PERIPHERAL_COUNT] = {0};
|
||||
|
||||
int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level) {
|
||||
if (source >= ARRAY_SIZE(peripheral_battery_levels)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (peripherals[source].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
|
||||
return -ENOTCONN;
|
||||
}
|
||||
|
||||
*level = peripheral_battery_levels[source];
|
||||
return 0;
|
||||
}
|
||||
|
||||
K_MSGQ_DEFINE(peripheral_batt_lvl_msgq, sizeof(struct zmk_peripheral_battery_state_changed),
|
||||
CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE, 4);
|
||||
|
||||
void peripheral_batt_lvl_change_callback(struct k_work *work) {
|
||||
struct zmk_peripheral_battery_state_changed ev;
|
||||
while (k_msgq_get(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT) == 0) {
|
||||
LOG_DBG("Triggering peripheral battery level change %u", ev.state_of_charge);
|
||||
peripheral_battery_levels[ev.source] = ev.state_of_charge;
|
||||
raise_zmk_peripheral_battery_state_changed(ev);
|
||||
}
|
||||
}
|
||||
|
||||
K_WORK_DEFINE(peripheral_batt_lvl_work, peripheral_batt_lvl_change_callback);
|
||||
|
||||
static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
|
||||
struct bt_gatt_subscribe_params *params,
|
||||
const void *data, uint16_t length) {
|
||||
@@ -454,10 +408,16 @@ static uint8_t split_central_battery_level_notify_func(struct bt_conn *conn,
|
||||
LOG_DBG("[BATTERY LEVEL NOTIFICATION] data %p length %u", data, length);
|
||||
uint8_t battery_level = ((uint8_t *)data)[0];
|
||||
LOG_DBG("Battery level: %u", battery_level);
|
||||
struct zmk_peripheral_battery_state_changed ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
|
||||
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_batt_lvl_work);
|
||||
|
||||
struct peripheral_event_wrapper ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
|
||||
.data = {.battery_event = {
|
||||
.level = battery_level,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
|
||||
return BT_GATT_ITER_CONTINUE;
|
||||
}
|
||||
@@ -493,10 +453,15 @@ static uint8_t split_central_battery_level_read_func(struct bt_conn *conn, uint8
|
||||
|
||||
LOG_DBG("Battery level: %u", battery_level);
|
||||
|
||||
struct zmk_peripheral_battery_state_changed ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = battery_level};
|
||||
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_batt_lvl_work);
|
||||
struct peripheral_event_wrapper ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
|
||||
.data = {.battery_event = {
|
||||
.level = battery_level,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
|
||||
return BT_GATT_ITER_CONTINUE;
|
||||
}
|
||||
@@ -977,10 +942,19 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
|
||||
LOG_DBG("Disconnected: %s (reason %d)", addr, reason);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
struct zmk_peripheral_battery_state_changed ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
|
||||
k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_batt_lvl_work);
|
||||
struct peripheral_event_wrapper ev = {
|
||||
.source = peripheral_slot_index_for_conn(conn),
|
||||
.event = {.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
|
||||
.data = {.battery_event = {
|
||||
.level = 0,
|
||||
}}}};
|
||||
|
||||
k_msgq_put(&peripheral_event_msgq, &ev, K_NO_WAIT);
|
||||
k_work_submit(&peripheral_event_work);
|
||||
// struct zmk_peripheral_battery_state_changed ev = {
|
||||
// .source = peripheral_slot_index_for_conn(conn), .state_of_charge = 0};
|
||||
// k_msgq_put(&peripheral_batt_lvl_msgq, &ev, K_NO_WAIT);
|
||||
// k_work_submit(&peripheral_batt_lvl_work);
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
|
||||
@@ -1027,17 +1001,16 @@ K_THREAD_STACK_DEFINE(split_central_split_run_q_stack,
|
||||
|
||||
struct k_work_q split_central_split_run_q;
|
||||
|
||||
struct zmk_split_run_behavior_payload_wrapper {
|
||||
struct central_cmd_wrapper {
|
||||
uint8_t source;
|
||||
struct zmk_split_run_behavior_payload payload;
|
||||
struct zmk_split_transport_central_command cmd;
|
||||
};
|
||||
|
||||
K_MSGQ_DEFINE(zmk_split_central_split_run_msgq,
|
||||
sizeof(struct zmk_split_run_behavior_payload_wrapper),
|
||||
K_MSGQ_DEFINE(zmk_split_central_split_run_msgq, sizeof(struct central_cmd_wrapper),
|
||||
CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE, 4);
|
||||
|
||||
void split_central_split_run_callback(struct k_work *work) {
|
||||
struct zmk_split_run_behavior_payload_wrapper payload_wrapper;
|
||||
struct central_cmd_wrapper payload_wrapper;
|
||||
|
||||
LOG_DBG("");
|
||||
|
||||
@@ -1046,34 +1019,85 @@ void split_central_split_run_callback(struct k_work *work) {
|
||||
LOG_ERR("Source not connected");
|
||||
continue;
|
||||
}
|
||||
if (!peripherals[payload_wrapper.source].run_behavior_handle) {
|
||||
LOG_ERR("Run behavior handle not found");
|
||||
continue;
|
||||
|
||||
switch (payload_wrapper.cmd.type) {
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
|
||||
if (!peripherals[payload_wrapper.source].run_behavior_handle) {
|
||||
LOG_ERR("Run behavior handle not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
struct zmk_split_run_behavior_payload payload = {
|
||||
.data = {
|
||||
.param1 = payload_wrapper.cmd.data.invoke_behavior.param1,
|
||||
.param2 = payload_wrapper.cmd.data.invoke_behavior.param2,
|
||||
.position = payload_wrapper.cmd.data.invoke_behavior.position,
|
||||
.source = payload_wrapper.cmd.data.invoke_behavior.event_source,
|
||||
.state = payload_wrapper.cmd.data.invoke_behavior.state ? 1 : 0,
|
||||
}};
|
||||
const size_t payload_dev_size = sizeof(payload.behavior_dev);
|
||||
if (strlcpy(payload.behavior_dev, payload_wrapper.cmd.data.invoke_behavior.behavior_dev,
|
||||
payload_dev_size) >= payload_dev_size) {
|
||||
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
|
||||
payload_wrapper.cmd.data.invoke_behavior.behavior_dev,
|
||||
payload.behavior_dev);
|
||||
}
|
||||
|
||||
int err = bt_gatt_write_without_response(
|
||||
peripherals[payload_wrapper.source].conn,
|
||||
peripherals[payload_wrapper.source].run_behavior_handle, &payload,
|
||||
sizeof(struct zmk_split_run_behavior_payload), true);
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to write the behavior characteristic (err %d)", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT:
|
||||
update_peripheral_selected_layout(
|
||||
&peripherals[payload_wrapper.source],
|
||||
payload_wrapper.cmd.data.set_physical_layout.layout_idx);
|
||||
break;
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS:
|
||||
LOG_WRN("do the indicators dance");
|
||||
if (peripherals[payload_wrapper.source].update_hid_indicators == 0) {
|
||||
// It appears that sometimes the peripheral is considered connected
|
||||
// before the GATT characteristics have been discovered. If this is
|
||||
// the case, the update_hid_indicators handle will not yet be set.
|
||||
LOG_WRN("NO HANDLE TO SET ON PERIPHERAL");
|
||||
break;
|
||||
}
|
||||
|
||||
int err = bt_gatt_write_without_response(
|
||||
peripherals[payload_wrapper.source].conn,
|
||||
peripherals[payload_wrapper.source].run_behavior_handle, &payload_wrapper.payload,
|
||||
sizeof(struct zmk_split_run_behavior_payload), true);
|
||||
int err = bt_gatt_write_without_response(
|
||||
peripherals[payload_wrapper.source].conn,
|
||||
peripherals[payload_wrapper.source].update_hid_indicators,
|
||||
&payload_wrapper.cmd.data.set_hid_indicators.indicators,
|
||||
sizeof(payload_wrapper.cmd.data.set_hid_indicators.indicators), true);
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to write the behavior characteristic (err %d)", err);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to write HID indicator characteristic (err %d)", err);
|
||||
}
|
||||
break;
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
default:
|
||||
LOG_WRN("Unsupported wrapped central command type %d", payload_wrapper.cmd.type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_WORK_DEFINE(split_central_split_run_work, split_central_split_run_callback);
|
||||
|
||||
static int
|
||||
split_bt_invoke_behavior_payload(struct zmk_split_run_behavior_payload_wrapper payload_wrapper) {
|
||||
static int split_bt_invoke_behavior_payload(struct central_cmd_wrapper payload_wrapper) {
|
||||
LOG_DBG("");
|
||||
|
||||
int err = k_msgq_put(&zmk_split_central_split_run_msgq, &payload_wrapper, K_MSEC(100));
|
||||
if (err) {
|
||||
switch (err) {
|
||||
case -EAGAIN: {
|
||||
LOG_WRN("Consumer message queue full, popping first message and queueing again");
|
||||
struct zmk_split_run_behavior_payload_wrapper discarded_report;
|
||||
LOG_WRN("Run command message queue full, popping first message and queueing again");
|
||||
struct central_cmd_wrapper discarded_report;
|
||||
k_msgq_get(&zmk_split_central_split_run_msgq, &discarded_report, K_NO_WAIT);
|
||||
return split_bt_invoke_behavior_payload(payload_wrapper);
|
||||
}
|
||||
@@ -1088,63 +1112,6 @@ split_bt_invoke_behavior_payload(struct zmk_split_run_behavior_payload_wrapper p
|
||||
return 0;
|
||||
};
|
||||
|
||||
int zmk_split_bt_invoke_behavior(uint8_t source, struct zmk_behavior_binding *binding,
|
||||
struct zmk_behavior_binding_event event, bool state) {
|
||||
struct zmk_split_run_behavior_payload payload = {.data = {
|
||||
.param1 = binding->param1,
|
||||
.param2 = binding->param2,
|
||||
.position = event.position,
|
||||
.source = event.source,
|
||||
.state = state ? 1 : 0,
|
||||
}};
|
||||
const size_t payload_dev_size = sizeof(payload.behavior_dev);
|
||||
if (strlcpy(payload.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, payload.behavior_dev);
|
||||
}
|
||||
|
||||
struct zmk_split_run_behavior_payload_wrapper wrapper = {.source = source, .payload = payload};
|
||||
return split_bt_invoke_behavior_payload(wrapper);
|
||||
}
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
static zmk_hid_indicators_t hid_indicators = 0;
|
||||
|
||||
static void split_central_update_indicators_callback(struct k_work *work) {
|
||||
zmk_hid_indicators_t indicators = hid_indicators;
|
||||
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
|
||||
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (peripherals[i].update_hid_indicators == 0) {
|
||||
// It appears that sometimes the peripheral is considered connected
|
||||
// before the GATT characteristics have been discovered. If this is
|
||||
// the case, the update_hid_indicators handle will not yet be set.
|
||||
continue;
|
||||
}
|
||||
|
||||
int err = bt_gatt_write_without_response(peripherals[i].conn,
|
||||
peripherals[i].update_hid_indicators, &indicators,
|
||||
sizeof(indicators), true);
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to write HID indicator characteristic (err %d)", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static K_WORK_DEFINE(split_central_update_indicators, split_central_update_indicators_callback);
|
||||
|
||||
int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators) {
|
||||
hid_indicators = indicators;
|
||||
return k_work_submit_to_queue(&split_central_split_run_q, &split_central_update_indicators);
|
||||
}
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
|
||||
|
||||
static int finish_init() {
|
||||
return IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) ? 0 : start_scanning();
|
||||
}
|
||||
@@ -1185,4 +1152,55 @@ static int zmk_split_bt_central_listener_cb(const zmk_event_t *eh) {
|
||||
}
|
||||
|
||||
ZMK_LISTENER(zmk_split_bt_central, zmk_split_bt_central_listener_cb);
|
||||
ZMK_SUBSCRIPTION(zmk_split_bt_central, zmk_physical_layout_selection_changed);
|
||||
ZMK_SUBSCRIPTION(zmk_split_bt_central, zmk_physical_layout_selection_changed);
|
||||
|
||||
static int split_central_bt_send_command(uint8_t source,
|
||||
struct zmk_split_transport_central_command cmd) {
|
||||
if (source >= ARRAY_SIZE(peripherals)) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
switch (cmd.type) {
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS:
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT:
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
|
||||
struct central_cmd_wrapper wrapper = {.source = source, .cmd = cmd};
|
||||
return split_bt_invoke_behavior_payload(wrapper);
|
||||
}
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS:
|
||||
return -ENOTSUP;
|
||||
default:
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int split_central_bt_get_available_source_ids(uint8_t *sources) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
|
||||
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sources[count++] = i;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(bt_central, ¢ral_api);
|
||||
|
||||
void peripheral_event_work_callback(struct k_work *work) {
|
||||
struct peripheral_event_wrapper ev;
|
||||
while (k_msgq_get(&peripheral_event_msgq, &ev, K_NO_WAIT) == 0) {
|
||||
LOG_DBG("Trigger key position state change for %d",
|
||||
ev.event.data.key_position_event.position);
|
||||
zmk_split_transport_central_peripheral_event_handler(&bt_central, ev.source, ev.event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
#include <zmk/event_manager.h>
|
||||
#include <zmk/battery.h>
|
||||
#include <zmk/events/battery_state_changed.h>
|
||||
#include <zmk/split/bluetooth/central.h>
|
||||
#include <zmk/split/central.h>
|
||||
|
||||
static void blvl_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) {
|
||||
ARG_UNUSED(attr);
|
||||
@@ -32,7 +32,7 @@ static ssize_t read_blvl(struct bt_conn *conn, const struct bt_gatt_attr *attr,
|
||||
uint16_t len, uint16_t offset) {
|
||||
const uint8_t source = (uint8_t)(uint32_t)attr->user_data;
|
||||
uint8_t level = 0;
|
||||
int rc = zmk_split_get_peripheral_battery_level(source, &level);
|
||||
int rc = zmk_split_central_get_peripheral_battery_level(source, &level);
|
||||
|
||||
if (rc == -EINVAL) {
|
||||
LOG_ERR("Invalid peripheral index requested for battery level read: %d", source);
|
||||
|
||||
@@ -18,9 +18,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
#include <zephyr/bluetooth/uuid.h>
|
||||
|
||||
#include <drivers/behavior.h>
|
||||
#include <zmk/stdlib.h>
|
||||
#include <zmk/behavior.h>
|
||||
#include <zmk/matrix.h>
|
||||
#include <zmk/physical_layouts.h>
|
||||
#include <zmk/split/transport/peripheral.h>
|
||||
#include <zmk/split/bluetooth/uuid.h>
|
||||
#include <zmk/split/bluetooth/service.h>
|
||||
|
||||
@@ -60,48 +62,7 @@ static ssize_t split_svc_pos_state(struct bt_conn *conn, const struct bt_gatt_at
|
||||
|
||||
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) {
|
||||
struct zmk_split_run_behavior_payload *payload = attrs->user_data;
|
||||
uint16_t end_addr = offset + len;
|
||||
|
||||
LOG_DBG("offset %d len %d", offset, len);
|
||||
|
||||
if (end_addr > sizeof(struct zmk_split_run_behavior_payload)) {
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
||||
}
|
||||
|
||||
memcpy(payload + offset, buf, len);
|
||||
|
||||
// We run if:
|
||||
// 1: We've gotten all the position/state/param data.
|
||||
// 2: We have a null terminated string for the behavior device label.
|
||||
const size_t behavior_dev_offset =
|
||||
offsetof(struct zmk_split_run_behavior_payload, behavior_dev);
|
||||
if ((end_addr > sizeof(struct zmk_split_run_behavior_data)) &&
|
||||
payload->behavior_dev[end_addr - behavior_dev_offset - 1] == '\0') {
|
||||
struct zmk_behavior_binding binding = {
|
||||
.param1 = payload->data.param1,
|
||||
.param2 = payload->data.param2,
|
||||
.behavior_dev = payload->behavior_dev,
|
||||
};
|
||||
LOG_DBG("%s with params %d %d: pressed? %d", binding.behavior_dev, binding.param1,
|
||||
binding.param2, payload->data.state);
|
||||
struct zmk_behavior_binding_event event = {.position = payload->data.position,
|
||||
.timestamp = k_uptime_get()};
|
||||
int err;
|
||||
if (payload->data.state > 0) {
|
||||
err = behavior_keymap_binding_pressed(&binding, event);
|
||||
} else {
|
||||
err = behavior_keymap_binding_released(&binding, event);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to invoke behavior %s: %d", binding.behavior_dev, err);
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
uint8_t flags);
|
||||
|
||||
static ssize_t split_svc_num_of_positions(struct bt_conn *conn, const struct bt_gatt_attr *attrs,
|
||||
void *buf, uint16_t len, uint16_t offset) {
|
||||
@@ -285,12 +246,12 @@ int send_position_state() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int zmk_split_bt_position_pressed(uint8_t position) {
|
||||
static int zmk_split_bt_position_pressed(uint8_t position) {
|
||||
WRITE_BIT(position_state[position / 8], position % 8, true);
|
||||
return send_position_state();
|
||||
}
|
||||
|
||||
int zmk_split_bt_position_released(uint8_t position) {
|
||||
static int zmk_split_bt_position_released(uint8_t position) {
|
||||
WRITE_BIT(position_state[position / 8], position % 8, false);
|
||||
return send_position_state();
|
||||
}
|
||||
@@ -332,9 +293,9 @@ int send_sensor_state(struct sensor_event ev) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
|
||||
const struct zmk_sensor_channel_data channel_data[],
|
||||
size_t channel_data_size) {
|
||||
static int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
|
||||
const struct zmk_sensor_channel_data channel_data[],
|
||||
size_t channel_data_size) {
|
||||
if (channel_data_size > ZMK_SENSOR_EVENT_MAX_CHANNELS) {
|
||||
return -EINVAL;
|
||||
}
|
||||
@@ -349,7 +310,8 @@ int zmk_split_bt_sensor_triggered(uint8_t sensor_index,
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
|
||||
|
||||
int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value, bool sync) {
|
||||
static int zmk_split_bt_report_input(uint8_t reg, uint8_t type, uint16_t code, int32_t value,
|
||||
bool sync) {
|
||||
|
||||
for (size_t i = 0; i < split_svc.attr_count; i++) {
|
||||
if (bt_uuid_cmp(split_svc.attrs[i].uuid,
|
||||
@@ -380,3 +342,98 @@ 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) {
|
||||
switch (ev->type) {
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT:
|
||||
if (ev->data.key_position_event.pressed) {
|
||||
zmk_split_bt_position_pressed(ev->data.key_position_event.position);
|
||||
} else {
|
||||
zmk_split_bt_position_released(ev->data.key_position_event.position);
|
||||
}
|
||||
break;
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT:
|
||||
zmk_split_bt_sensor_triggered(ev->data.sensor_event.sensor_index,
|
||||
&ev->data.sensor_event.channel_data, 1);
|
||||
|
||||
break;
|
||||
#endif
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT:
|
||||
return zmk_split_bt_report_input(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
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT:
|
||||
// The BLE transport uses standard BAS service for propagation, so just return success here.
|
||||
return 0;
|
||||
#endif
|
||||
default:
|
||||
LOG_WRN("Unhandled event type %d", ev->type);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
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) {
|
||||
struct zmk_split_run_behavior_payload *payload = attrs->user_data;
|
||||
uint16_t end_addr = offset + len;
|
||||
|
||||
LOG_DBG("offset %d len %d", offset, len);
|
||||
|
||||
if (end_addr > sizeof(struct zmk_split_run_behavior_payload)) {
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
|
||||
}
|
||||
|
||||
memcpy(payload + offset, buf, len);
|
||||
|
||||
// We run if:
|
||||
// 1: We've gotten all the position/state/param data.
|
||||
// 2: We have a null terminated string for the behavior device label.
|
||||
const size_t behavior_dev_offset =
|
||||
offsetof(struct zmk_split_run_behavior_payload, behavior_dev);
|
||||
if ((end_addr > sizeof(struct zmk_split_run_behavior_data)) &&
|
||||
payload->behavior_dev[end_addr - behavior_dev_offset - 1] == '\0') {
|
||||
|
||||
struct zmk_split_transport_central_command cmd = {
|
||||
.type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR,
|
||||
.data = {.invoke_behavior = {
|
||||
.param1 = payload->data.param1,
|
||||
.param2 = payload->data.param2,
|
||||
.position = payload->data.position,
|
||||
.state = payload->data.state,
|
||||
}}};
|
||||
|
||||
const size_t payload_dev_size = sizeof(cmd.data.invoke_behavior.behavior_dev);
|
||||
if (strlcpy(cmd.data.invoke_behavior.behavior_dev, payload->behavior_dev,
|
||||
payload_dev_size) >= payload_dev_size) {
|
||||
LOG_ERR("Truncated behavior label %s to %s before invoking peripheral behavior",
|
||||
payload->behavior_dev, cmd.data.invoke_behavior.behavior_dev);
|
||||
}
|
||||
|
||||
LOG_DBG("%s with params %d %d: pressed? %d", cmd.data.invoke_behavior.behavior_dev,
|
||||
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);
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to invoke behavior %s: %d", payload->behavior_dev, err);
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2020 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#include <zmk/split/bluetooth/service.h>
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
#include <zmk/event_manager.h>
|
||||
#include <zmk/events/position_state_changed.h>
|
||||
#include <zmk/events/sensor_event.h>
|
||||
#include <zmk/hid.h>
|
||||
#include <zmk/sensors.h>
|
||||
#include <zmk/endpoints.h>
|
||||
|
||||
int split_listener(const zmk_event_t *eh) {
|
||||
LOG_DBG("");
|
||||
const struct zmk_position_state_changed *pos_ev;
|
||||
if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) {
|
||||
if (pos_ev->state) {
|
||||
return zmk_split_bt_position_pressed(pos_ev->position);
|
||||
} else {
|
||||
return zmk_split_bt_position_released(pos_ev->position);
|
||||
}
|
||||
}
|
||||
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
const struct zmk_sensor_event *sensor_ev;
|
||||
if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) {
|
||||
return zmk_split_bt_sensor_triggered(sensor_ev->sensor_index, sensor_ev->channel_data,
|
||||
sensor_ev->channel_data_size);
|
||||
}
|
||||
#endif /* ZMK_KEYMAP_HAS_SENSORS */
|
||||
return ZMK_EV_EVENT_BUBBLE;
|
||||
}
|
||||
|
||||
ZMK_LISTENER(split_listener, split_listener);
|
||||
ZMK_SUBSCRIPTION(split_listener, zmk_position_state_changed);
|
||||
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
ZMK_SUBSCRIPTION(split_listener, zmk_sensor_event);
|
||||
#endif /* ZMK_KEYMAP_HAS_SENSORS */
|
||||
173
app/src/split/central.c
Normal file
173
app/src/split/central.c
Normal 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);
|
||||
137
app/src/split/peripheral.c
Normal file
137
app/src/split/peripheral.c
Normal file
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
#include <zmk/stdlib.h>
|
||||
#include <zmk/split/transport/peripheral.h>
|
||||
|
||||
#include <drivers/behavior.h>
|
||||
#include <zmk/behavior.h>
|
||||
|
||||
#include <zmk/event_manager.h>
|
||||
#include <zmk/events/position_state_changed.h>
|
||||
#include <zmk/events/sensor_event.h>
|
||||
#include <zmk/events/battery_state_changed.h>
|
||||
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
// TODO: Active transport selection
|
||||
|
||||
struct zmk_split_transport_peripheral *active_transport;
|
||||
|
||||
int zmk_split_transport_peripheral_command_handler(
|
||||
const struct zmk_split_transport_peripheral *transport,
|
||||
struct zmk_split_transport_central_command cmd) {
|
||||
LOG_DBG("");
|
||||
|
||||
switch (cmd.type) {
|
||||
case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: {
|
||||
struct zmk_behavior_binding binding = {
|
||||
.param1 = cmd.data.invoke_behavior.param1,
|
||||
.param2 = cmd.data.invoke_behavior.param2,
|
||||
.behavior_dev = cmd.data.invoke_behavior.behavior_dev,
|
||||
};
|
||||
LOG_DBG("%s with params %d %d: pressed? %d", binding.behavior_dev, binding.param1,
|
||||
binding.param2, cmd.data.invoke_behavior.state);
|
||||
struct zmk_behavior_binding_event event = {.position = cmd.data.invoke_behavior.position,
|
||||
.timestamp = k_uptime_get()};
|
||||
int err;
|
||||
if (cmd.data.invoke_behavior.state > 0) {
|
||||
err = behavior_keymap_binding_pressed(&binding, event);
|
||||
} else {
|
||||
err = behavior_keymap_binding_released(&binding, event);
|
||||
}
|
||||
|
||||
if (err) {
|
||||
LOG_ERR("Failed to invoke behavior %s: %d", binding.behavior_dev, err);
|
||||
}
|
||||
}
|
||||
default:
|
||||
LOG_WRN("Unhandled command type %d", cmd.type);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int zmk_split_peripheral_report_event(const struct zmk_split_transport_peripheral_event *event) {
|
||||
if (!active_transport || !active_transport->api || !active_transport->api->report_event) {
|
||||
LOG_WRN("No active transport that supports reporting events!");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
return active_transport->api->report_event(event);
|
||||
}
|
||||
|
||||
static int peripheral_init(void) {
|
||||
STRUCT_SECTION_GET(zmk_split_transport_peripheral, 0, &active_transport);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
SYS_INIT(peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||
|
||||
int split_peripheral_listener(const zmk_event_t *eh) {
|
||||
LOG_DBG("");
|
||||
const struct zmk_position_state_changed *pos_ev;
|
||||
if ((pos_ev = as_zmk_position_state_changed(eh)) != NULL) {
|
||||
struct zmk_split_transport_peripheral_event ev = {
|
||||
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT,
|
||||
.data = {.key_position_event = {
|
||||
.position = pos_ev->position,
|
||||
.pressed = pos_ev->state,
|
||||
}}};
|
||||
|
||||
zmk_split_peripheral_report_event(&ev);
|
||||
}
|
||||
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
const struct zmk_sensor_event *sensor_ev;
|
||||
if ((sensor_ev = as_zmk_sensor_event(eh)) != NULL) {
|
||||
if (sensor_ev->channel_data_size != 1) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
struct zmk_split_transport_peripheral_event ev = {
|
||||
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT,
|
||||
.data = {.sensor_event = {
|
||||
.channel_data = sensor_ev->channel_data[0],
|
||||
.sensor_index = sensor_ev->sensor_index,
|
||||
}}};
|
||||
|
||||
zmk_split_peripheral_report_event(&ev);
|
||||
}
|
||||
#endif /* ZMK_KEYMAP_HAS_SENSORS */
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
|
||||
const struct zmk_battery_state_changed *battery_ev;
|
||||
if ((battery_ev = as_zmk_battery_state_changed(eh)) != NULL) {
|
||||
struct zmk_split_transport_peripheral_event ev = {
|
||||
.type = ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT,
|
||||
.data = {.battery_event = {
|
||||
.level = battery_ev->state_of_charge,
|
||||
}}};
|
||||
|
||||
zmk_split_peripheral_report_event(&ev);
|
||||
}
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
|
||||
|
||||
return ZMK_EV_EVENT_BUBBLE;
|
||||
}
|
||||
|
||||
ZMK_LISTENER(split_peripheral, split_peripheral_listener);
|
||||
ZMK_SUBSCRIPTION(split_peripheral, zmk_position_state_changed);
|
||||
|
||||
#if ZMK_KEYMAP_HAS_SENSORS
|
||||
ZMK_SUBSCRIPTION(split_peripheral, zmk_sensor_event);
|
||||
#endif /* ZMK_KEYMAP_HAS_SENSORS */
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING)
|
||||
ZMK_SUBSCRIPTION(split_peripheral, zmk_battery_state_changed);
|
||||
#endif
|
||||
6
app/src/split/wired/CMakeLists.txt
Normal file
6
app/src/split/wired/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) 2025 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
target_sources(app PRIVATE wired.c)
|
||||
target_sources_ifdef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE central.c)
|
||||
target_sources_ifndef(CONFIG_ZMK_SPLIT_ROLE_CENTRAL app PRIVATE peripheral.c)
|
||||
52
app/src/split/wired/Kconfig
Normal file
52
app/src/split/wired/Kconfig
Normal file
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) 2025 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
if ZMK_SPLIT_WIRED
|
||||
|
||||
choice
|
||||
prompt "UART Mode"
|
||||
|
||||
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
|
||||
select UART_ASYNC_API
|
||||
|
||||
config ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT
|
||||
bool "Interrupt Mode"
|
||||
depends on SERIAL_SUPPORT_INTERRUPT
|
||||
select UART_INTERRUPT_DRIVEN
|
||||
|
||||
config ZMK_SPLIT_WIRED_UART_MODE_POLLING
|
||||
bool "Polling Mode"
|
||||
|
||||
endchoice
|
||||
|
||||
if ZMK_SPLIT_WIRED_UART_MODE_POLLING
|
||||
|
||||
config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD
|
||||
int "Ticks between RX polls"
|
||||
|
||||
endif
|
||||
|
||||
if ZMK_SPLIT_WIRED_UART_MODE_ASYNC
|
||||
|
||||
config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT
|
||||
int "RX Timeout (in microseconds) before reporting received data"
|
||||
|
||||
endif
|
||||
|
||||
config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS
|
||||
int "Number of central commands to buffer for TX/RX"
|
||||
|
||||
config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS
|
||||
int "Number of peripheral events to buffer for TX/RX"
|
||||
|
||||
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT
|
||||
int "RX timeout (in ms) when polling peripheral(s) and waiting for any response"
|
||||
|
||||
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
|
||||
34
app/src/split/wired/Kconfig.defaults
Normal file
34
app/src/split/wired/Kconfig.defaults
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2025 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
if ZMK_SPLIT_WIRED
|
||||
|
||||
config ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS
|
||||
default 4
|
||||
|
||||
config ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS
|
||||
default 16
|
||||
|
||||
|
||||
if ZMK_SPLIT_WIRED_UART_MODE_POLLING
|
||||
|
||||
config ZMK_SPLIT_WIRED_POLLING_RX_PERIOD
|
||||
default 10
|
||||
|
||||
endif
|
||||
|
||||
|
||||
if ZMK_SPLIT_WIRED_UART_MODE_ASYNC
|
||||
|
||||
config ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT
|
||||
default 20
|
||||
|
||||
endif
|
||||
|
||||
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_TIMEOUT
|
||||
default 15
|
||||
|
||||
config ZMK_SPLIT_WIRED_HALF_DUPLEX_RX_COMPLETE_TIMEOUT
|
||||
default 20
|
||||
|
||||
endif
|
||||
342
app/src/split/wired/central.c
Normal file
342
app/src/split/wired/central.c
Normal file
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <zephyr/types.h>
|
||||
#include <zephyr/init.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
|
||||
|
||||
#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[RX_BUFFER_SIZE / 2][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);
|
||||
|
||||
#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 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);
|
||||
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,
|
||||
});
|
||||
|
||||
begin_tx();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 int zmk_split_wired_central_init(void) {
|
||||
if (!device_is_ready(uart)) {
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
#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
|
||||
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
|
||||
|
||||
#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
|
||||
|
||||
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 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,
|
||||
};
|
||||
|
||||
ZMK_SPLIT_TRANSPORT_CENTRAL_REGISTER(wired_central, ¢ral_api);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
302
app/src/split/wired/peripheral.c
Normal file
302
app/src/split/wired/peripheral.c
Normal file
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include <zephyr/types.h>
|
||||
#include <zephyr/init.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/peripheral.h>
|
||||
#include <zmk/split/transport/types.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 TX_BUFFER_SIZE \
|
||||
((sizeof(struct event_envelope) + sizeof(struct msg_postfix)) * \
|
||||
CONFIG_ZMK_SPLIT_WIRED_EVENT_BUFFER_ITEMS)
|
||||
#define RX_BUFFER_SIZE \
|
||||
((sizeof(struct command_envelope) + sizeof(struct msg_postfix)) * \
|
||||
CONFIG_ZMK_SPLIT_WIRED_CMD_BUFFER_ITEMS)
|
||||
|
||||
RING_BUF_DECLARE(chosen_rx_buf, RX_BUFFER_SIZE);
|
||||
RING_BUF_DECLARE(chosen_tx_buf, TX_BUFFER_SIZE);
|
||||
|
||||
static const uint8_t peripheral_id = 0;
|
||||
|
||||
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));
|
||||
|
||||
#if HAS_DIR_GPIO
|
||||
|
||||
static const struct gpio_dt_spec dir_gpio = GPIO_DT_SPEC_INST_GET(0, dir_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_commands_work(struct k_work *work);
|
||||
|
||||
K_WORK_DEFINE(publish_commands, publish_commands_work);
|
||||
|
||||
static void process_tx_cb(void);
|
||||
K_MSGQ_DEFINE(cmd_msg_queue, sizeof(struct zmk_split_transport_central_command), 3, 4);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
|
||||
|
||||
uint8_t async_rx_buf[RX_BUFFER_SIZE / 2][2];
|
||||
|
||||
static struct zmk_split_wired_async_state async_state = {
|
||||
.rx_bufs = {async_rx_buf[0], async_rx_buf[1]},
|
||||
.rx_bufs_len = RX_BUFFER_SIZE / 2,
|
||||
.rx_size_process_trigger = sizeof(struct command_envelope),
|
||||
.process_tx_callback = process_tx_cb,
|
||||
.rx_buf = &chosen_rx_buf,
|
||||
.tx_buf = &chosen_tx_buf,
|
||||
#if HAS_DIR_GPIO
|
||||
.dir_gpio = &dir_gpio,
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
||||
#if HAS_DIR_GPIO
|
||||
|
||||
static void set_dir(uint8_t tx) { gpio_pin_set_dt(&dir_gpio, tx); }
|
||||
|
||||
#else
|
||||
|
||||
static inline void set_dir(uint8_t tx) {}
|
||||
|
||||
#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, &chosen_rx_buf, NULL, process_tx_cb);
|
||||
}
|
||||
|
||||
if (uart_irq_tx_complete(dev)) {
|
||||
if (ring_buf_size_get(&chosen_tx_buf) == 0) {
|
||||
uart_irq_tx_disable(dev);
|
||||
}
|
||||
|
||||
set_dir(0);
|
||||
}
|
||||
|
||||
if (uart_irq_tx_ready(dev)) {
|
||||
set_dir(1);
|
||||
zmk_split_wired_fifo_fill(dev, &chosen_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(&chosen_tx_buf, uart);
|
||||
}
|
||||
|
||||
static K_WORK_DEFINE(send_pending_tx, send_pending_tx_work_cb);
|
||||
|
||||
static void wired_peripheral_read_tick_cb(struct k_timer *timer) {
|
||||
zmk_split_wired_poll_in(&chosen_rx_buf, uart, NULL, process_tx_cb);
|
||||
}
|
||||
|
||||
static K_TIMER_DEFINE(wired_peripheral_read_timer, wired_peripheral_read_tick_cb, NULL);
|
||||
|
||||
#endif
|
||||
|
||||
static int zmk_split_wired_peripheral_init(void) {
|
||||
if (!device_is_ready(uart)) {
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
#if HAS_DIR_GPIO
|
||||
gpio_pin_configure_dt(&dir_gpio, GPIO_OUTPUT_INACTIVE);
|
||||
#endif
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
|
||||
/* configure interrupt and callback to receive data */
|
||||
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;
|
||||
}
|
||||
|
||||
#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)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
SYS_INIT(zmk_split_wired_peripheral_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||
|
||||
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);
|
||||
#elif IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
|
||||
k_work_submit(&send_pending_tx);
|
||||
#endif
|
||||
}
|
||||
|
||||
static ssize_t get_payload_data_size(const struct zmk_split_transport_peripheral_event *evt) {
|
||||
switch (evt->type) {
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_INPUT_EVENT:
|
||||
return sizeof(evt->data.input_event);
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_KEY_POSITION_EVENT:
|
||||
return sizeof(evt->data.key_position_event);
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_SENSOR_EVENT:
|
||||
return sizeof(evt->data.sensor_event);
|
||||
case ZMK_SPLIT_TRANSPORT_PERIPHERAL_EVENT_TYPE_BATTERY_EVENT:
|
||||
return sizeof(evt->data.battery_event);
|
||||
default:
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
split_peripheral_wired_report_event(const struct zmk_split_transport_peripheral_event *event) {
|
||||
ssize_t data_size = get_payload_data_size(event);
|
||||
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(peripheral_id) + sizeof(enum zmk_split_transport_peripheral_event_type);
|
||||
|
||||
if (ring_buf_space_get(&chosen_tx_buf) < MSG_EXTRA_SIZE + payload_size) {
|
||||
LOG_WRN("No room to send peripheral to the central (have %d but only space for %d)",
|
||||
MSG_EXTRA_SIZE + payload_size, ring_buf_space_get(&chosen_tx_buf));
|
||||
return -ENOSPC;
|
||||
}
|
||||
|
||||
struct event_envelope env = {.prefix =
|
||||
{
|
||||
.magic_prefix = ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX,
|
||||
.payload_size = payload_size,
|
||||
},
|
||||
.payload = {
|
||||
.source = peripheral_id,
|
||||
.event = *event,
|
||||
}};
|
||||
|
||||
struct msg_postfix postfix = {.crc =
|
||||
crc32_ieee((void *)&env, sizeof(env.prefix) + payload_size)};
|
||||
|
||||
LOG_HEXDUMP_DBG(&env, sizeof(env.prefix) + payload_size, "Payload");
|
||||
|
||||
size_t put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&env, sizeof(env.prefix) + payload_size);
|
||||
if (put != sizeof(env.prefix) + payload_size) {
|
||||
LOG_WRN("Failed to put the whole message (%d vs %d)", put,
|
||||
sizeof(env.prefix) + payload_size);
|
||||
}
|
||||
put = ring_buf_put(&chosen_tx_buf, (uint8_t *)&postfix, sizeof(postfix));
|
||||
if (put != sizeof(postfix)) {
|
||||
LOG_WRN("Failed to put the whole message (%d vs %d)", put, sizeof(postfix));
|
||||
}
|
||||
|
||||
#if !IS_HALF_DUPLEX_MODE
|
||||
begin_tx();
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct zmk_split_transport_peripheral_api peripheral_api = {
|
||||
.report_event = split_peripheral_wired_report_event,
|
||||
};
|
||||
|
||||
ZMK_SPLIT_TRANSPORT_PERIPHERAL_REGISTER(wired_peripheral, &peripheral_api);
|
||||
|
||||
static void process_tx_cb(void) {
|
||||
while (ring_buf_size_get(&chosen_rx_buf) > MSG_EXTRA_SIZE) {
|
||||
struct command_envelope env;
|
||||
int item_err = zmk_split_wired_get_item(&chosen_rx_buf, (uint8_t *)&env,
|
||||
sizeof(struct command_envelope));
|
||||
switch (item_err) {
|
||||
case 0:
|
||||
if (env.payload.cmd.type == ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_POLL_EVENTS) {
|
||||
begin_tx();
|
||||
} else {
|
||||
int ret = k_msgq_put(&cmd_msg_queue, &env.payload.cmd, K_NO_WAIT);
|
||||
if (ret < 0) {
|
||||
LOG_WRN("Failed to queue command for processing (%d)", ret);
|
||||
return;
|
||||
}
|
||||
|
||||
k_work_submit(&publish_commands);
|
||||
}
|
||||
break;
|
||||
case -EAGAIN:
|
||||
return;
|
||||
default:
|
||||
LOG_WRN("Issue fetching an item from the RX buffer: %d", item_err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
static void publish_commands_work(struct k_work *work) {
|
||||
struct zmk_split_transport_central_command cmd;
|
||||
|
||||
while (k_msgq_get(&cmd_msg_queue, &cmd, K_NO_WAIT) >= 0) {
|
||||
zmk_split_transport_peripheral_command_handler(&wired_peripheral, cmd);
|
||||
}
|
||||
}
|
||||
318
app/src/split/wired/wired.c
Normal file
318
app/src/split/wired/wired.c
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "wired.h"
|
||||
|
||||
#include <zephyr/sys/crc.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
|
||||
|
||||
void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart) {
|
||||
uint8_t *buf;
|
||||
uint32_t claim_len;
|
||||
while ((claim_len = ring_buf_get_claim(tx_buf, &buf, MIN(32, tx_buf->size))) > 0) {
|
||||
LOG_HEXDUMP_DBG(buf, claim_len, "TX Bytes");
|
||||
for (int i = 0; i < claim_len; i++) {
|
||||
uart_poll_out(uart, buf[i]);
|
||||
}
|
||||
|
||||
ring_buf_get_finish(tx_buf, claim_len);
|
||||
}
|
||||
}
|
||||
|
||||
int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart,
|
||||
struct k_work *process_data_work,
|
||||
zmk_split_wired_process_tx_callback_t process_data_cb) {
|
||||
uint8_t *buf;
|
||||
uint32_t read = 0;
|
||||
uint32_t claim_len = ring_buf_put_claim(rx_buf, &buf, ring_buf_space_get(rx_buf));
|
||||
if (claim_len < 1) {
|
||||
LOG_WRN("No room available for reading in from the serial port");
|
||||
return -ENOSPC;
|
||||
}
|
||||
|
||||
bool all_read = false;
|
||||
while (read < claim_len) {
|
||||
if (uart_poll_in(uart, buf + read) < 0) {
|
||||
all_read = true;
|
||||
break;
|
||||
}
|
||||
|
||||
read++;
|
||||
}
|
||||
|
||||
ring_buf_put_finish(rx_buf, read);
|
||||
|
||||
if (ring_buf_size_get(rx_buf) > 0) {
|
||||
if (process_data_work) {
|
||||
k_work_submit(process_data_work);
|
||||
} else if (process_data_cb) {
|
||||
process_data_cb();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Also indicate if no bytes read at all?
|
||||
return (all_read ? 1 : 0);
|
||||
}
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
|
||||
|
||||
void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf,
|
||||
struct k_work *process_work,
|
||||
zmk_split_wired_process_tx_callback_t process_cb) {
|
||||
// TODO: Add error checking on platforms that support it
|
||||
uint32_t last_read = 0, len = 0;
|
||||
do {
|
||||
uint8_t *buffer;
|
||||
len = ring_buf_put_claim(buf, &buffer, buf->size);
|
||||
if (len > 0) {
|
||||
last_read = uart_fifo_read(dev, buffer, len);
|
||||
|
||||
ring_buf_put_finish(buf, last_read);
|
||||
} else {
|
||||
LOG_ERR("Dropping incoming RPC byte, insufficient room in the RX buffer. Bump "
|
||||
"CONFIG_ZMK_STUDIO_RPC_RX_BUF_SIZE.");
|
||||
uint8_t dummy;
|
||||
last_read = uart_fifo_read(dev, &dummy, 1);
|
||||
}
|
||||
} while (last_read && last_read == len);
|
||||
|
||||
if (process_work) {
|
||||
k_work_submit(process_work);
|
||||
} else if (process_cb) {
|
||||
process_cb();
|
||||
}
|
||||
}
|
||||
|
||||
void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf) {
|
||||
uint32_t len;
|
||||
while ((len = ring_buf_size_get(tx_buf)) > 0) {
|
||||
uint8_t *buf;
|
||||
uint32_t claim_len = ring_buf_get_claim(tx_buf, &buf, tx_buf->size);
|
||||
|
||||
if (claim_len <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
int sent = uart_fifo_fill(dev, buf, claim_len);
|
||||
|
||||
ring_buf_get_finish(tx_buf, MAX(sent, 0));
|
||||
|
||||
if (sent <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if (ring_buf_size_get(tx_buf) == 0) {
|
||||
// uart_irq_tx_disable(dev);
|
||||
// }
|
||||
}
|
||||
|
||||
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
|
||||
|
||||
enum ASYNC_STATE_BITS {
|
||||
ASYNC_STATE_BIT_RXBUF0_USED = 0,
|
||||
ASYNC_STATE_BIT_RXBUF1_USED,
|
||||
};
|
||||
|
||||
void zmk_split_wired_async_tx(struct zmk_split_wired_async_state *state) {
|
||||
uint8_t *buf;
|
||||
uint32_t claim_len = ring_buf_get_claim(state->tx_buf, &buf, ring_buf_size_get(state->tx_buf));
|
||||
|
||||
if (claim_len <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->dir_gpio) {
|
||||
gpio_pin_set_dt(state->dir_gpio, 1);
|
||||
}
|
||||
|
||||
#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||
LOG_DBG("Sending %d", claim_len);
|
||||
#endif
|
||||
int err = uart_tx(state->uart, buf, claim_len, SYS_FOREVER_US);
|
||||
if (err < 0) {
|
||||
LOG_DBG("NO TX %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static void async_uart_cb(const struct device *dev, struct uart_event *ev, void *user_data) {
|
||||
struct zmk_split_wired_async_state *state = (struct zmk_split_wired_async_state *)user_data;
|
||||
|
||||
switch (ev->type) {
|
||||
case UART_TX_ABORTED:
|
||||
// This can only really occur for a TX timeout for a HW flow control UART setup. What to do
|
||||
// here in practice?
|
||||
LOG_WRN("TX Aborted");
|
||||
break;
|
||||
case UART_TX_DONE:
|
||||
LOG_DBG("TX Done %d", ev->data.tx.len);
|
||||
ring_buf_get_finish(state->tx_buf, ev->data.tx.len);
|
||||
if (ring_buf_size_get(state->tx_buf) > 0) {
|
||||
zmk_split_wired_async_tx(state);
|
||||
} else {
|
||||
if (state->dir_gpio) {
|
||||
gpio_pin_set_dt(state->dir_gpio, 0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UART_RX_RDY: {
|
||||
size_t received =
|
||||
ring_buf_put(state->rx_buf, &ev->data.rx.buf[ev->data.rx.offset], ev->data.rx.len);
|
||||
if (received < ev->data.rx.len) {
|
||||
LOG_ERR("RX overrun!");
|
||||
break;
|
||||
}
|
||||
|
||||
// LOG_DBG("RX %d and now buffer is %d", received, ring_buf_size_get(state->rx_buf));
|
||||
if (state->process_tx_callback) {
|
||||
state->process_tx_callback();
|
||||
} else if (state->process_tx_work) {
|
||||
k_work_submit(state->process_tx_work);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case UART_RX_BUF_RELEASED:
|
||||
if (ev->data.rx_buf.buf == state->rx_bufs[0]) {
|
||||
atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED);
|
||||
} else if (ev->data.rx_buf.buf == state->rx_bufs[1]) {
|
||||
atomic_clear_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED);
|
||||
}
|
||||
|
||||
break;
|
||||
case UART_RX_BUF_REQUEST:
|
||||
if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF0_USED)) {
|
||||
uart_rx_buf_rsp(state->uart, state->rx_bufs[0], state->rx_bufs_len);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!atomic_test_and_set_bit(&state->state, ASYNC_STATE_BIT_RXBUF1_USED)) {
|
||||
uart_rx_buf_rsp(state->uart, state->rx_bufs[1], state->rx_bufs_len);
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_WRN("No RX buffers available!");
|
||||
break;
|
||||
case UART_RX_STOPPED:
|
||||
// LOG_WRN("UART RX Stopped %d with state %ld", ev->data.rx_stop.reason, state->state);
|
||||
break;
|
||||
case UART_RX_DISABLED: {
|
||||
k_work_schedule(&state->restart_rx_work, K_MSEC(1));
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int zmk_split_wired_async_init(struct zmk_split_wired_async_state *state) {
|
||||
__ASSERT(state != NULL, "State is null");
|
||||
|
||||
k_work_init_delayable(&state->restart_rx_work, restart_rx_work_cb);
|
||||
|
||||
int ret = uart_callback_set(state->uart, async_uart_cb, state);
|
||||
if (ret < 0) {
|
||||
LOG_ERR("Failed to set up async callback on UART (%d)", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size) {
|
||||
while (ring_buf_size_get(rx_buf) > sizeof(struct msg_prefix) + sizeof(struct msg_postfix)) {
|
||||
struct msg_prefix prefix;
|
||||
|
||||
__ASSERT_EVAL(
|
||||
(void)ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)),
|
||||
uint32_t peek_read = ring_buf_peek(rx_buf, (uint8_t *)&prefix, sizeof(prefix)),
|
||||
peek_read == sizeof(prefix), "Somehow read less than we expect from the RX buffer");
|
||||
|
||||
if (memcmp(&prefix.magic_prefix, &ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX,
|
||||
sizeof(prefix.magic_prefix)) != 0) {
|
||||
uint8_t discarded_byte;
|
||||
ring_buf_get(rx_buf, &discarded_byte, 1);
|
||||
|
||||
LOG_WRN("Prefix mismatch, discarding byte %0x", discarded_byte);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t payload_to_read = sizeof(prefix) + prefix.payload_size;
|
||||
|
||||
if (payload_to_read > env_size) {
|
||||
LOG_WRN("Invalid message with payload %d bigger than expected max %d", payload_to_read,
|
||||
env_size);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (ring_buf_size_get(rx_buf) < payload_to_read + sizeof(struct msg_postfix)) {
|
||||
return -EAGAIN;
|
||||
}
|
||||
|
||||
// Now that prefix matches, read it out so we can read the rest of the payload.
|
||||
__ASSERT_EVAL((void)ring_buf_get(rx_buf, env, payload_to_read),
|
||||
uint32_t read = ring_buf_get(rx_buf, env, payload_to_read),
|
||||
read == payload_to_read,
|
||||
"Somehow read less than we expect from the RX buffer");
|
||||
|
||||
struct msg_postfix postfix;
|
||||
__ASSERT_EVAL((void)ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)),
|
||||
uint32_t read = ring_buf_get(rx_buf, (uint8_t *)&postfix, sizeof(postfix)),
|
||||
read == sizeof(postfix),
|
||||
"Somehow read less of the postfix than we expect from the RX buffer");
|
||||
|
||||
uint32_t crc = crc32_ieee(env, payload_to_read);
|
||||
if (crc != postfix.crc) {
|
||||
LOG_WRN("Data corruption in received peripheral event, ignoring %d vs %d", crc,
|
||||
postfix.crc);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -EAGAIN;
|
||||
}
|
||||
94
app/src/split/wired/wired.h
Normal file
94
app/src/split/wired/wired.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2025 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
#include <zephyr/device.h>
|
||||
|
||||
#include <zmk/split/transport/types.h>
|
||||
|
||||
#define ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX "ZmKw"
|
||||
|
||||
struct msg_prefix {
|
||||
uint8_t magic_prefix[sizeof(ZMK_SPLIT_WIRED_ENVELOPE_MAGIC_PREFIX) - 1];
|
||||
uint8_t payload_size;
|
||||
} __packed;
|
||||
|
||||
struct command_payload {
|
||||
uint8_t source;
|
||||
struct zmk_split_transport_central_command cmd;
|
||||
} __packed;
|
||||
|
||||
struct command_envelope {
|
||||
struct msg_prefix prefix;
|
||||
struct command_payload payload;
|
||||
} __packed;
|
||||
|
||||
struct event_payload {
|
||||
uint8_t source;
|
||||
struct zmk_split_transport_peripheral_event event;
|
||||
} __packed;
|
||||
|
||||
struct event_envelope {
|
||||
struct msg_prefix prefix;
|
||||
struct event_payload payload;
|
||||
} __packed;
|
||||
|
||||
struct msg_postfix {
|
||||
uint32_t crc;
|
||||
} __packed;
|
||||
|
||||
#define MSG_EXTRA_SIZE (sizeof(struct msg_prefix) + sizeof(struct msg_postfix))
|
||||
|
||||
typedef void (*zmk_split_wired_process_tx_callback_t)(void);
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING)
|
||||
|
||||
void zmk_split_wired_poll_out(struct ring_buf *tx_buf, const struct device *uart);
|
||||
|
||||
int zmk_split_wired_poll_in(struct ring_buf *rx_buf, const struct device *uart,
|
||||
struct k_work *process_data_work,
|
||||
zmk_split_wired_process_tx_callback_t process_data_cb);
|
||||
|
||||
#endif
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT)
|
||||
|
||||
void zmk_split_wired_fifo_read(const struct device *dev, struct ring_buf *buf,
|
||||
struct k_work *process_work,
|
||||
zmk_split_wired_process_tx_callback_t process_cb);
|
||||
void zmk_split_wired_fifo_fill(const struct device *dev, struct ring_buf *tx_buf);
|
||||
|
||||
#endif
|
||||
|
||||
#if IS_ENABLED(CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC)
|
||||
|
||||
struct zmk_split_wired_async_state {
|
||||
atomic_t state;
|
||||
|
||||
uint8_t *rx_bufs[2];
|
||||
size_t rx_bufs_len;
|
||||
size_t rx_size_process_trigger;
|
||||
|
||||
struct ring_buf *tx_buf;
|
||||
struct ring_buf *rx_buf;
|
||||
|
||||
zmk_split_wired_process_tx_callback_t process_tx_callback;
|
||||
|
||||
const struct device *uart;
|
||||
|
||||
struct k_work_delayable restart_rx_work;
|
||||
struct k_work *process_tx_work;
|
||||
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);
|
||||
|
||||
#endif
|
||||
|
||||
int zmk_split_wired_get_item(struct ring_buf *rx_buf, uint8_t *env, size_t env_size);
|
||||
@@ -13,10 +13,12 @@ peripheral 0 <dbg> zmk: split_svc_select_phys_layout_callback: Selecting physica
|
||||
peripheral 0 <dbg> zmk: kscan_mock_work_handler_0: ev 327680000 row 0 column 0 state 0
|
||||
peripheral 0 <dbg> zmk: kscan_mock_schedule_next_event_0: delaying next keypress: 5000
|
||||
peripheral 0 <dbg> zmk: zmk_physical_layouts_kscan_process_msgq: Row: 0, col: 0, position: 0, pressed: false
|
||||
peripheral 0 <dbg> zmk: split_listener:
|
||||
peripheral 0 <dbg> zmk: split_peripheral_listener:
|
||||
peripheral 0 <dbg> zmk: kscan_mock_work_handler_0: ev 2475163905 row 1 column 1 state 1
|
||||
peripheral 0 <dbg> zmk: kscan_mock_schedule_next_event_0: delaying next keypress: 5000
|
||||
peripheral 0 <dbg> zmk: zmk_physical_layouts_kscan_process_msgq: Row: 1, col: 1, position: 3, pressed: true
|
||||
peripheral 0 <dbg> zmk: split_listener:
|
||||
peripheral 0 <dbg> zmk: split_peripheral_listener:
|
||||
peripheral 0 <dbg> zmk: split_svc_run_behavior: offset 0 len 20
|
||||
peripheral 0 <dbg> zmk: split_svc_run_behavior: sysreset with params 0 0: pressed? 1
|
||||
peripheral 0 <dbg> zmk: zmk_split_transport_peripheral_command_handler:
|
||||
peripheral 0 <dbg> zmk: zmk_split_transport_peripheral_command_handler: sysreset with params 0 0: pressed? 1
|
||||
|
||||
@@ -30,7 +30,7 @@ On macOS the BLE battery reporting packets can cause the computer to wakeup from
|
||||
|
||||
### Peripheral Battery Monitoring
|
||||
|
||||
You can [configure ZMK to allow support for peripheral battery monitoring over BLE](system.md#split-keyboards) (e.g. when having a split keyboard with two independent and wirelessly connected sides).
|
||||
You can [configure ZMK to allow support for peripheral battery monitoring over BLE](split.md) (e.g. when having a split keyboard with two independent and wirelessly connected sides).
|
||||
If you want to report the battery levels of both sides of a split keyboard, you should have both `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY` and `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING` set to `y`.
|
||||
|
||||
:::note[Displaying both battery levels on your host]
|
||||
|
||||
90
docs/docs/config/split.md
Normal file
90
docs/docs/config/split.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Split Configuration
|
||||
sidebar_label: Split
|
||||
---
|
||||
|
||||
These are settings that control how split keyboards behave.
|
||||
|
||||
See [Configuration Overview](index.md) for instructions on how to change these settings.
|
||||
|
||||
## Kconfig
|
||||
|
||||
Following [split keyboard](../features/split-keyboards.md) settings are defined in [zmk/app/src/split/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/Kconfig).
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| -------------------------------------------- | ---- | ------------------------------------------------------------------------ | ------- |
|
||||
| `CONFIG_ZMK_SPLIT` | bool | Enable split keyboard support | n |
|
||||
| `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` | bool | `y` for central device, `n` for peripheral | n |
|
||||
| `CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS` | bool | Enable split keyboard support for passing indicator state to peripherals | n |
|
||||
|
||||
### Bluetooth Splits
|
||||
|
||||
Following bluetooth [split keyboard](../features/split-keyboards.md) settings are defined in [zmk/app/src/split/bluetooth/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/bluetooth/Kconfig).
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| ------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| `CONFIG_ZMK_SPLIT_BLE` | bool | Use BLE to communicate between split keyboard halves | y |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` | int | Number of peripherals that will connect to the central | 1 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING` | bool | Enable fetching split peripheral battery levels to the central side | n |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY` | bool | Enable central reporting of split battery levels to hosts | n |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE` | int | Max number of battery level events to queue when received from peripherals | `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue when received from peripherals | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_STACK_SIZE` | int | Stack size of the BLE split central write thread | 512 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE` | int | Max number of behavior run events to queue to send to the peripheral(s) | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE` | int | Stack size of the BLE split peripheral notify thread | 756 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY` | int | Priority of the BLE split peripheral notify thread | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue to send to the central | 10 |
|
||||
|
||||
### Wired Splits
|
||||
|
||||
Hardware UARTs have a few different modes/approaches to sending and receiving data, with different levels of complexity and performance. Not all hardware nor drivers support all modes, so ZMK has code to support different interaction modes with the UART as needed. The default mode should be properly selected based on the platform's report support, but you can choose to override the mode if needed.
|
||||
|
||||
- Polling Mode - The least efficient mode, this requires the MCU to constantly poll the UART to see if more data has been received, taking time away from other processing. This is basic mode supported by all UART drivers.
|
||||
- Interrupt Mode - This mode allows the MCU to do other processing until the UART raises an interrupt to signal new data has been received. On platforms where this is combined with a FIFO, there is even less superfluous processing, and high speeds can be achieved while allowing other processing to continue. Examples:
|
||||
- RP2040
|
||||
- nRF52
|
||||
- Async (DMA) Mode - Similar to interrupt mode, data reception can occur without involving the MCU. Additionally, larger volumes can be copied directly into accessible memory without the use of the MCU, allowing even further efficiency/rates without tying up the MCU. Only some drivers support this mode (and the current Zephyr 3.5 version of the nRF52 UART has some bugs that prevent its use). Examples:
|
||||
- SAM0 (e.g. SAMD21)
|
||||
- STM32 (e.g. stm32f072)
|
||||
|
||||
Following wired [split keyboard](../features/split-keyboards.md) settings are defined in [zmk/app/src/split/wired/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/wired/Kconfig).
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| -------------------------------------------- | ---- | ----------------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED` | bool | Use wired connection to communicate between split keyboard halves | y (if no BLE split and devicetree is set appropriately) |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED_UART_MODE_ASYNC` | bool | Async (DMA) mode | y if the driver supports it (excluding nRF52 with known bugs) |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED_UART_MODE_INTERRUPT` | bool | Interrupt mode | y if the hardware supports it |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED_UART_MODE_POLLING` | bool | Polling mode | y if neither other mode is supported |
|
||||
|
||||
#### Async (DMA) Mode
|
||||
|
||||
The following settings only apply when using wired split in async (DMA) mode:
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| ----------------------------------------- | ---- | ----------------------------------------------------------- | ------- |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED_ASYNC_RX_TIMEOUT` | int | RX Timeout (in microseconds) before reporting received data | 20 |
|
||||
|
||||
#### Polling Mode
|
||||
|
||||
The following settings only apply when using wired split in polling mode:
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| ------------------------------------------ | ---- | ---------------------------------------------------- | ------- |
|
||||
| `CONFIG_ZMK_SPLIT_WIRED_POLLING_RX_PERIOD` | int | Number of ticks between calls to poll for split data | 10 |
|
||||
|
||||
## Devicetree
|
||||
|
||||
### Wired Split
|
||||
|
||||
Wired splits require a properly configured UART to function. If writing a shield, you may be able to use the standard UART already provided by the board, e.g. `&pro_micro_serial`. See [predefined nodes](../development/hardware-integration/pinctrl.mdx#predefined-nodes) for details on the UART node labels provided by various interconnects. If you are creating your own board, or using custom pins for the UART, see the documentation on [pin control](../development/hardware-integration/pinctrl.mdx#additional-examples) to configure the pins for your UART.
|
||||
|
||||
Once you have a properly configured UART device, it needs to be assigned in a new node with a compatible value of `"zmk,wired-split"`. For example:
|
||||
|
||||
```dts
|
||||
/ {
|
||||
wired_split {
|
||||
compatible = "zmk,wired-split";
|
||||
device = <&pro_micro_serial>;
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -115,27 +115,6 @@ Note that `CONFIG_BT_MAX_CONN` and `CONFIG_BT_MAX_PAIRED` should be set to the s
|
||||
| `CONFIG_ZMK_USB_LOGGING` | bool | Enable USB CDC ACM logging for debugging | n |
|
||||
| `CONFIG_ZMK_LOG_LEVEL` | int | Log level for ZMK debug messages | 4 |
|
||||
|
||||
### Split keyboards
|
||||
|
||||
Following [split keyboard](../features/split-keyboards.md) settings are defined in [zmk/app/src/split/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/Kconfig) (generic) and [zmk/app/src/split/bluetooth/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/src/split/bluetooth/Kconfig) (bluetooth).
|
||||
|
||||
| Config | Type | Description | Default |
|
||||
| ------------------------------------------------------- | ---- | -------------------------------------------------------------------------- | ------------------------------------------ |
|
||||
| `CONFIG_ZMK_SPLIT` | bool | Enable split keyboard support | n |
|
||||
| `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` | bool | `y` for central device, `n` for peripheral | |
|
||||
| `CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS` | bool | Enable split keyboard support for passing indicator state to peripherals | n |
|
||||
| `CONFIG_ZMK_SPLIT_BLE` | bool | Use BLE to communicate between split keyboard halves | y |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` | int | Number of peripherals that will connect to the central | 1 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING` | bool | Enable fetching split peripheral battery levels to the central side | n |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY` | bool | Enable central reporting of split battery levels to hosts | n |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_QUEUE_SIZE` | int | Max number of battery level events to queue when received from peripherals | `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue when received from peripherals | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_STACK_SIZE` | int | Stack size of the BLE split central write thread | 512 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_CENTRAL_SPLIT_RUN_QUEUE_SIZE` | int | Max number of behavior run events to queue to send to the peripheral(s) | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE` | int | Stack size of the BLE split peripheral notify thread | 756 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_PRIORITY` | int | Priority of the BLE split peripheral notify thread | 5 |
|
||||
| `CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_POSITION_QUEUE_SIZE` | int | Max number of key state events to queue to send to the central | 10 |
|
||||
|
||||
## Snippets
|
||||
|
||||
:::danger
|
||||
|
||||
@@ -513,6 +513,30 @@ If all of your physical layouts use the same `kscan` node under the hood, you ca
|
||||
|
||||
:::
|
||||
|
||||
### Wired Split
|
||||
|
||||
<SplitTabs>
|
||||
<TabItem value="unibody">This is only required for wired split keyboards.</TabItem>
|
||||
<TabItem value="split">
|
||||
|
||||
If testing the experimental [wired split](../../features/split-keyboards.md) support, you should assign a [predefined](./pinctrl.mdx#predefined-nodes) or [pinctrl configured](./pinctrl.mdx) UART to the `device` property of a new node with `compatible` value of `"zmk,wired-split"`:
|
||||
|
||||
```dts
|
||||
/ {
|
||||
wired_split {
|
||||
compatible = "zmk,wired-split";
|
||||
device = <&pro_micro_serial>;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
See the [wired split](../../config/split.md#wired-split) configuration for more details.
|
||||
|
||||
For wireless split keyboards, this step should be skipped, especially since the UART pins on your controller might already be in use for other functionality.
|
||||
|
||||
</TabItem>
|
||||
</SplitTabs>
|
||||
|
||||
## Default Keymap
|
||||
|
||||
Each keyboard should provide a default keymap to be used when building the firmware, which can be overridden and customized by user configs.
|
||||
|
||||
@@ -55,7 +55,7 @@ Please see the [explainer on boards & shields](development/hardware-integration/
|
||||
|
||||
### Does ZMK support wired split?
|
||||
|
||||
Currently, ZMK only supports wireless split, but wired split is possible and we welcome contributions!
|
||||
Currently, ZMK only supports wireless [split keyboards](features/split-keyboards.md). Experimental wired split support for some specific hardware designs is available for advanced users to test.
|
||||
|
||||
### How is the latency?
|
||||
|
||||
|
||||
@@ -6,10 +6,17 @@ 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]
|
||||
Currently ZMK only supports split keyboards that communicate with each other wirelessly over BLE.
|
||||
As such, only controllers that support BLE can be used with ZMK split keyboards.
|
||||
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.
|
||||
|
||||
Supporting split communication over wired protocols is planned, allowing for ZMK split keyboards using non-wireless controllers.
|
||||
:::
|
||||
|
||||
## Central and Peripheral Roles
|
||||
@@ -32,7 +39,7 @@ You can refer to the [power profiler](/power-profiler) to see battery life estim
|
||||
|
||||
The [new shield guide](../development/hardware-integration/new-shield.mdx) details how to define a split keyboard shield with two parts, enabling the split feature and setting up the necessary roles for each part.
|
||||
|
||||
Also see the reference section on [split keyboards configuration](../config/system.md#split-keyboards) where the relevant symbols include `CONFIG_ZMK_SPLIT` that enables the feature, `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` which sets the central role and `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` that sets the number of peripherals.
|
||||
Also see the reference section on [split keyboards configuration](../config/split.md) where the relevant symbols include `CONFIG_ZMK_SPLIT` that enables the feature, `CONFIG_ZMK_SPLIT_ROLE_CENTRAL` which sets the central role and `CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS` that sets the number of peripherals.
|
||||
|
||||
### Latency Considerations
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ module.exports = {
|
||||
"config/layout",
|
||||
"config/kscan",
|
||||
"config/power",
|
||||
"config/split",
|
||||
"config/system",
|
||||
"config/studio",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user