mirror of
https://github.com/zmkfirmware/zmk.git
synced 2026-03-19 20:45:18 -05:00
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
|
||||
|
||||
Reference in New Issue
Block a user