feat(studio): Initial RPC infrastructure and subsystems.

* UART and BLE/GATT transports for a protobuf encoded RPC
  request/response protocol.
* Custom framing protocol is used to frame a give message.
* Requests/responses are divided into major "subsystems" which
  handle requests and create response messages.
* Notification support, including mapping local events to RPC
  notifications by a given subsystem.
* Meta responses for "no response" and "unlock needed".
* Initial basic lock state support in a new core section, and allow specifying
  if a given RPC callback requires unlocked state or not.
* Add behavior subsystem with full metadata support and examples of
  using callback to serialize a repeated field without extra stack space needed.

Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
This commit is contained in:
Peter Johanson
2024-02-19 08:48:20 +00:00
committed by Pete Johanson
parent ea64fcaf71
commit feda96eb40
28 changed files with 2840 additions and 9 deletions

View File

@@ -105,4 +105,27 @@ target_sources(app PRIVATE src/main.c)
add_subdirectory(src/display/)
add_subdirectory_ifdef(CONFIG_SETTINGS src/settings/)
if (CONFIG_ZMK_STUDIO_RPC)
# For some reason this is failing if run from a different sub-file.
list(APPEND CMAKE_MODULE_PATH ${ZEPHYR_BASE}/modules/nanopb)
include(nanopb)
# Turn off the default nanopb behavior
set(NANOPB_GENERATE_CPP_STANDALONE OFF)
nanopb_generate_cpp(proto_srcs proto_hdrs RELPATH ${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}
${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/studio.proto
${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/meta.proto
${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/core.proto
${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/behaviors.proto
${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR}/proto/zmk/keymap.proto
)
target_include_directories(app PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_sources(app PRIVATE ${proto_srcs} ${proto_hdrs})
add_subdirectory(src/studio)
endif()
zephyr_cc_option(-Wfatal-errors)

View File

@@ -258,6 +258,8 @@ rsource "src/split/Kconfig"
#Basic Keyboard Setup
endmenu
rsource "src/studio/Kconfig"
menu "Display/LED Options"
rsource "src/display/Kconfig"

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_rpc_event_mapper, 4)

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_rpc_subsystem_handler, 4)

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_RAM(zmk_rpc_subsystem, 4)

View File

@@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_rpc_transport, 4)

View File

@@ -25,6 +25,22 @@
#define ZMK_HID_KEYBOARD_NKRO_MAX_USAGE HID_USAGE_KEY_KEYPAD_EQUAL
#endif
#if IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC)
#define ZMK_HID_CONSUMER_MAX_USAGE 0xFF
#elif IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_FULL)
#define ZMK_HID_CONSUMER_MAX_USAGE 0xFFF
#else
#error "Unknown consumer report usages configuration"
#endif
#if IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_NKRO)
#define ZMK_HID_KEYBOARD_MAX_USAGE ZMK_HID_KEYBOARD_NKRO_MAX_USAGE
#elif IS_ENABLED(CONFIG_ZMK_HID_REPORT_TYPE_HKRO)
#define ZMK_HID_KEYBOARD_MAX_USAGE 0xFF
#else
#error "Unknown keyboard report usages configuration"
#endif
#define ZMK_HID_MOUSE_NUM_BUTTONS 0x05
// See https://www.usb.org/sites/default/files/hid1_11.pdf section 6.2.2.4 Main Items

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zmk/event_manager.h>
enum zmk_studio_core_lock_state {
ZMK_STUDIO_CORE_LOCK_STATE_LOCKED = 0,
ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED = 1,
};
struct zmk_studio_core_lock_state_changed {
enum zmk_studio_core_lock_state state;
};
struct zmk_studio_core_unlock_requested {};
ZMK_EVENT_DECLARE(zmk_studio_core_lock_state_changed);
enum zmk_studio_core_lock_state zmk_studio_core_get_lock_state(void);
void zmk_studio_core_unlock();
void zmk_studio_core_lock();
void zmk_studio_core_initiate_unlock();
void zmk_studio_core_complete_unlock();
void zmk_studio_core_reschedule_lock_timeout();

View File

@@ -0,0 +1,215 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/sys/iterable_sections.h>
#include <zephyr/sys/ring_buffer.h>
#include <proto/zmk/studio.pb.h>
#include <zmk/endpoints_types.h>
#include <zmk/event_manager.h>
#include <zmk/studio/core.h>
enum zmk_studio_rpc_handler_security {
ZMK_STUDIO_RPC_HANDLER_SECURED,
ZMK_STUDIO_RPC_HANDLER_UNSECURED,
};
struct zmk_studio_rpc_notification {
zmk_studio_Notification notification;
};
ZMK_EVENT_DECLARE(zmk_studio_rpc_notification);
struct zmk_rpc_subsystem;
typedef zmk_studio_Response(subsystem_func)(const struct zmk_rpc_subsystem *subsys,
const zmk_studio_Request *req);
typedef zmk_studio_Response(rpc_func)(const zmk_studio_Request *neq);
/**
* @brief An RPC subsystem is a cohesive collection of related RPCs. A specific RPC is identified by
* the pair or subsystem and request identifiers. This struct is the high level entity to
* aggregate all the possible handler functions for the request in the given subsystem.
*/
struct zmk_rpc_subsystem {
subsystem_func *func;
uint16_t handlers_start_index;
uint16_t handlers_end_index;
uint8_t subsystem_choice;
};
/**
* @brief An entry for a specific handler function in a given subsystem, including metadata
* indicating if the particular handler requires the device be unlock in order to be invoked.
*/
struct zmk_rpc_subsystem_handler {
rpc_func *func;
uint8_t subsystem_choice;
uint8_t request_choice;
enum zmk_studio_rpc_handler_security security;
};
/**
* @brief Generate a "meta" subsystem response indicating an "empty" response to an RPC request.
*/
#define ZMK_RPC_NO_RESPONSE() ZMK_RPC_RESPONSE(meta, no_response, true)
/**
* @brief Generate a "meta" subsystem response with one of a few possible simple error responses.
* @see https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/meta.proto#L5
*/
#define ZMK_RPC_SIMPLE_ERR(type) \
ZMK_RPC_RESPONSE(meta, simple_error, zmk_meta_ErrorConditions_##type)
/**
* @brief Register an RPC subsystem to aggregate handlers for request to that subsystem.
* @param prefix The identifier for the subsystem, e.g. `core`, `keymap`, etc.
* @see https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/studio.proto#L15
*/
#define ZMK_RPC_SUBSYSTEM(prefix) \
zmk_studio_Response subsystem_func_##prefix(const struct zmk_rpc_subsystem *subsys, \
const zmk_studio_Request *req) { \
uint8_t which_req = req->subsystem.prefix.which_request_type; \
return zmk_rpc_subsystem_delegate_to_subs(subsys, req, which_req); \
} \
STRUCT_SECTION_ITERABLE(zmk_rpc_subsystem, prefix##_subsystem) = { \
.func = subsystem_func_##prefix, \
.subsystem_choice = zmk_studio_Request_##prefix##_tag, \
};
/**
* @brief Register an RPC subsystem handler handler a specific request within the subsystem.
* @param prefix The identifier for the subsystem, e.g. `core`, `keymap`, etc.
* @param request_id The identifier for the request ID, e.g. `save_changes`.
* @param _secured Whether the handler requires the device be unlocked to allow invocation.
*
* @note A function with a name matching the request_id must be in-scope and will be used as the
* the callback handler. The function must have a signature of
* zmk_studio_Response (*func)(const zmk_studio_Request*)
*/
#define ZMK_RPC_SUBSYSTEM_HANDLER(prefix, request_id, _security) \
STRUCT_SECTION_ITERABLE(zmk_rpc_subsystem_handler, \
prefix##_subsystem_handler_##request_id) = { \
.func = request_id, \
.subsystem_choice = zmk_studio_Request_##prefix##_tag, \
.request_choice = zmk_##prefix##_Request_##request_id##_tag, \
.security = _security, \
};
/**
* @brief Create a zmk_studio_Notification struct for the given subsystem and type, including
initialization of the inner fields.
* @param subsys The identifier for the subsystem, e.g. `core`, `keymap`, etc.
* @param _type The identifier for the notification type in that subsystem, e.g.
`unsaved_changes_status_changed`.
*
* @see example
https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/keymap.proto#L41C14-L41C44
*/
#define ZMK_RPC_NOTIFICATION(subsys, _type, ...) \
((zmk_studio_Notification){ \
.which_subsystem = zmk_studio_Notification_##subsys##_tag, \
.subsystem = \
{ \
.subsys = \
{ \
.which_notification_type = zmk_##subsys##_Notification_##_type##_tag, \
.notification_type = {._type = __VA_ARGS__}, \
}, \
}, \
})
/**
* @brief Create a zmk_studio_Response struct for the given subsystem and type, including
initialization of the inner fields.
* @param subsys The identifier for the subsystem, e.g. `core`, `keymap`, etc.
* @param _type The identifier for the response type in that subsystem, e.g. `get_keymap`.
*
* @see example
https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/keymap.proto#L24
*/
#define ZMK_RPC_RESPONSE(subsys, _type, ...) \
((zmk_studio_Response){ \
.which_type = zmk_studio_Response_request_response_tag, \
.type = \
{ \
.request_response = \
{ \
.which_subsystem = zmk_studio_RequestResponse_##subsys##_tag, \
.subsystem = \
{ \
.subsys = \
{ \
.which_response_type = \
zmk_##subsys##_Response_##_type##_tag, \
.response_type = {._type = __VA_ARGS__}, \
}, \
}, \
}, \
}, \
})
typedef int(zmk_rpc_event_mapper_cb)(const zmk_event_t *ev, zmk_studio_Notification *n);
struct zmk_rpc_event_mapper {
zmk_rpc_event_mapper_cb *func;
};
/**
* @brief A single ZMK event listener is registered that will listen for events and map them to
* RPC notifications to be sent to the connected client. This macro adds additional
* subscriptions to that one single registered listener.
* @param _t The ZMK event type.
*/
#define ZMK_RPC_EVENT_MAPPER_ADD_LISTENER(_t) ZMK_SUBSCRIPTION(studio_rpc, _t)
/**
* @brief Register a mapping function that can selectively map a given internal ZMK event type into
* a possible zmk_studio_Notification type.
* @param name A unique identifier for the mapper. Often a subsystem identifier like `core` is used.
* @param _func The `zmk_rpc_event_mapper_cb` function used to map the internal event type.
*/
#define ZMK_RPC_EVENT_MAPPER(name, _func, ...) \
FOR_EACH_NONEMPTY_TERM(ZMK_RPC_EVENT_MAPPER_ADD_LISTENER, (;), __VA_ARGS__) \
STRUCT_SECTION_ITERABLE(zmk_rpc_event_mapper, name) = { \
.func = _func, \
};
typedef int (*zmk_rpc_rx_start_stop_func)(void);
typedef void (*zmk_rpc_tx_buffer_notify_func)(struct ring_buf *buf, size_t added, bool message_done,
void *user_data);
typedef void *(*zmk_rpc_tx_user_data_func)(void);
struct zmk_rpc_transport {
enum zmk_transport transport;
zmk_rpc_tx_user_data_func tx_user_data;
zmk_rpc_tx_buffer_notify_func tx_notify;
zmk_rpc_rx_start_stop_func rx_start;
zmk_rpc_rx_start_stop_func rx_stop;
};
zmk_studio_Response zmk_rpc_subsystem_delegate_to_subs(const struct zmk_rpc_subsystem *subsys,
const zmk_studio_Request *req,
uint8_t which_req);
struct ring_buf *zmk_rpc_get_tx_buf(void);
struct ring_buf *zmk_rpc_get_rx_buf(void);
void zmk_rpc_rx_notify(void);
#define ZMK_RPC_TRANSPORT(name, _transport, _rx_start, _rx_stop, _tx_user_data, _tx_notify) \
STRUCT_SECTION_ITERABLE(zmk_rpc_transport, name) = { \
.transport = _transport, \
.rx_start = _rx_start, \
.rx_stop = _rx_stop, \
.tx_user_data = _tx_user_data, \
.tx_notify = _tx_notify, \
}

View File

@@ -249,8 +249,9 @@ static inline int check_keyboard_usage(zmk_key_t usage) {
#endif
#define TOGGLE_CONSUMER(match, val) \
COND_CODE_1(IS_ENABLED(CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC), \
(if (val > 0xFF) { return -ENOTSUP; }), ()) \
if (val > ZMK_HID_CONSUMER_MAX_USAGE) { \
return -ENOTSUP; \
} \
for (int idx = 0; idx < CONFIG_ZMK_HID_CONSUMER_REPORT_SIZE; idx++) { \
if (consumer_report.body.keys[idx] != match) { \
continue; \

View File

@@ -0,0 +1,15 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT
zephyr_linker_sources(DATA_SECTIONS ../../include/linker/zmk-rpc-subsystems.ld)
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-rpc-subsystem-handlers.ld)
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-rpc-event-mappers.ld)
zephyr_linker_sources(SECTIONS ../../include/linker/zmk-rpc-transport.ld)
target_sources(app PRIVATE msg_framing.c)
target_sources(app PRIVATE rpc.c)
target_sources(app PRIVATE core.c)
target_sources(app PRIVATE behavior_subsystem.c)
target_sources(app PRIVATE core_subsystem.c)
target_sources_ifdef(CONFIG_ZMK_STUDIO_TRANSPORT_UART app PRIVATE uart_rpc_transport.c)
target_sources_ifdef(CONFIG_ZMK_STUDIO_TRANSPORT_BLE app PRIVATE gatt_rpc_transport.c)

95
app/src/studio/Kconfig Normal file
View File

@@ -0,0 +1,95 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT
menuconfig ZMK_STUDIO
bool "Studio Support"
select ZMK_STUDIO_RPC if !ZMK_SPLIT || ZMK_SPLIT_ROLE_CENTRAL
select PM_DEVICE # Needed for physical layout switching
help
Add firmware support for realtime keymap updates (ZMK Studio)
if ZMK_STUDIO
module = ZMK_STUDIO
module-str = zmk_studio
source "subsys/logging/Kconfig.template.log_config"
menuconfig ZMK_STUDIO_LOCKING
bool "Lock Support"
if ZMK_STUDIO_LOCKING
config ZMK_STUDIO_LOCK_IDLE_TIMEOUT_SEC
int "Idle Timeout"
default 600
config ZMK_STUDIO_LOCK_ON_DISCONNECT
bool "Lock On Disconnect"
default y
endif
menuconfig ZMK_STUDIO_RPC
bool "Remote Procedure Calls (RPC)"
select NANOPB
# These two save stack size
imply NANOPB_NO_ERRMSG
imply NANOPB_WITHOUT_64BIT
imply ZMK_STUDIO_LOCKING if !ARCH_POSIX
select CBPRINTF_LIBC_SUBSTS if ARCH_POSIX
select SETTINGS
select ZMK_BEHAVIOR_METADATA
select ZMK_BEHAVIOR_LOCAL_IDS
select RING_BUFFER
help
Add firmware support for studio RPC protocol
if ZMK_STUDIO_RPC
menu "Transports"
config ZMK_STUDIO_TRANSPORT_UART
bool "Serial"
select SERIAL
select RING_BUFFER
default y if ZMK_USB || ARCH_POSIX
config ZMK_STUDIO_TRANSPORT_UART_RX_STACK_SIZE
int "RX Stack Size"
depends on !UART_INTERRUPT_DRIVEN
default 512
config ZMK_STUDIO_TRANSPORT_BLE
bool "BLE (GATT)"
select RING_BUFFER
select BT_USER_DATA_LEN_UPDATE
depends on ZMK_BLE
default y
config BT_CONN_TX_MAX
default 64 if ZMK_STUDIO_TRANSPORT_BLE
config ZMK_STUDIO_TRANSPORT_BLE_PREF_LATENCY
int "BLE Transport preferred latency"
default 10
help
When the studio UI is connected, a lower latency can be requested in order
to make the interactions between keyboard and studio faster.
endmenu
config ZMK_STUDIO_RPC_THREAD_STACK_SIZE
int "RPC Thread Stack Size"
default 4096
config ZMK_STUDIO_RPC_RX_BUF_SIZE
int "RX Buffer Size"
default 30
config ZMK_STUDIO_RPC_TX_BUF_SIZE
int "TX Buffer Size"
default 64
endif
endif

View File

@@ -0,0 +1,211 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <pb_encode.h>
#include <zmk/studio/rpc.h>
#include <drivers/behavior.h>
#include <zmk/hid.h>
ZMK_RPC_SUBSYSTEM(behaviors)
#define BEHAVIOR_RESPONSE(type, ...) ZMK_RPC_RESPONSE(behaviors, type, __VA_ARGS__)
static bool encode_behavior_summaries(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
STRUCT_SECTION_FOREACH(zmk_behavior_local_id_map, beh) {
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
if (!pb_encode_varint(stream, beh->local_id)) {
LOG_ERR("Failed to encode behavior ID");
return false;
}
}
return true;
}
zmk_studio_Response list_all_behaviors(const zmk_studio_Request *req) {
zmk_behaviors_ListAllBehaviorsResponse beh_resp =
zmk_behaviors_ListAllBehaviorsResponse_init_zero;
beh_resp.behaviors.funcs.encode = encode_behavior_summaries;
return BEHAVIOR_RESPONSE(list_all_behaviors, beh_resp);
}
struct encode_metadata_sets_state {
const struct behavior_parameter_metadata_set *sets;
size_t sets_len;
size_t i;
};
static bool encode_value_description_name(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
struct behavior_parameter_value_metadata *state =
(struct behavior_parameter_value_metadata *)*arg;
if (!state->display_name) {
return true;
}
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
return pb_encode_string(stream, state->display_name, strlen(state->display_name));
}
static bool encode_value_description(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
struct encode_metadata_sets_state *state = (struct encode_metadata_sets_state *)*arg;
const struct behavior_parameter_metadata_set *set = &state->sets[state->i];
bool is_param1 = field->tag == zmk_behaviors_BehaviorBindingParametersSet_param1_tag;
size_t values_len = is_param1 ? set->param1_values_len : set->param2_values_len;
const struct behavior_parameter_value_metadata *values =
is_param1 ? set->param1_values : set->param2_values;
for (int val_i = 0; val_i < values_len; val_i++) {
const struct behavior_parameter_value_metadata *val = &values[val_i];
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
zmk_behaviors_BehaviorParameterValueDescription desc =
zmk_behaviors_BehaviorParameterValueDescription_init_zero;
desc.name.funcs.encode = encode_value_description_name;
desc.name.arg = val;
switch (val->type) {
case BEHAVIOR_PARAMETER_VALUE_TYPE_VALUE:
desc.which_value_type = zmk_behaviors_BehaviorParameterValueDescription_constant_tag;
desc.value_type.constant = val->value;
break;
case BEHAVIOR_PARAMETER_VALUE_TYPE_RANGE:
desc.which_value_type = zmk_behaviors_BehaviorParameterValueDescription_range_tag;
desc.value_type.range.min = val->range.min;
desc.value_type.range.max = val->range.max;
break;
case BEHAVIOR_PARAMETER_VALUE_TYPE_NIL:
desc.which_value_type = zmk_behaviors_BehaviorParameterValueDescription_nil_tag;
break;
case BEHAVIOR_PARAMETER_VALUE_TYPE_HID_USAGE:
desc.which_value_type = zmk_behaviors_BehaviorParameterValueDescription_hid_usage_tag;
desc.value_type.hid_usage.consumer_max = ZMK_HID_CONSUMER_MAX_USAGE;
desc.value_type.hid_usage.keyboard_max = ZMK_HID_KEYBOARD_MAX_USAGE;
break;
case BEHAVIOR_PARAMETER_VALUE_TYPE_LAYER_ID:
desc.which_value_type = zmk_behaviors_BehaviorParameterValueDescription_layer_id_tag;
break;
default:
LOG_ERR("Unknown value description type %d", val->type);
return false;
}
if (!pb_encode_submessage(stream, &zmk_behaviors_BehaviorParameterValueDescription_msg,
&desc)) {
LOG_WRN("Failed to encode submessage for set %d, value %d!", state->i, val_i);
return false;
}
}
return true;
}
static bool encode_metadata_sets(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) {
struct encode_metadata_sets_state *state = (struct encode_metadata_sets_state *)*arg;
bool ret = true;
LOG_DBG("Encoding the %d metadata sets with %p", state->sets_len, state->sets);
for (int i = 0; i < state->sets_len; i++) {
LOG_DBG("Encoding set %d", i);
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
state->i = i;
zmk_behaviors_BehaviorBindingParametersSet msg =
zmk_behaviors_BehaviorBindingParametersSet_init_zero;
msg.param1.funcs.encode = encode_value_description;
msg.param1.arg = state;
msg.param2.funcs.encode = encode_value_description;
msg.param2.arg = state;
ret = pb_encode_submessage(stream, &zmk_behaviors_BehaviorBindingParametersSet_msg, &msg);
if (!ret) {
LOG_WRN("Failed to encode submessage for set %d", i);
break;
}
}
return ret;
}
static bool encode_behavior_name(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) {
struct zmk_behavior_ref *zbm = (struct zmk_behavior_ref *)*arg;
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
return pb_encode_string(stream, zbm->metadata.display_name, strlen(zbm->metadata.display_name));
}
static struct encode_metadata_sets_state state = {};
zmk_studio_Response get_behavior_details(const zmk_studio_Request *req) {
uint32_t behavior_id = req->subsystem.behaviors.request_type.get_behavior_details.behavior_id;
const char *behavior_name = zmk_behavior_find_behavior_name_from_local_id(behavior_id);
if (!behavior_name) {
LOG_WRN("No behavior found for ID %d", behavior_id);
return ZMK_RPC_SIMPLE_ERR(GENERIC);
}
const struct device *device = behavior_get_binding(behavior_name);
struct zmk_behavior_ref *zbm = NULL;
STRUCT_SECTION_FOREACH(zmk_behavior_ref, item) {
if (item->device == device) {
zbm = item;
break;
}
}
__ASSERT(zbm != NULL, "Can't find a device without also having metadata");
struct behavior_parameter_metadata desc = {0};
int ret = behavior_get_parameter_metadata(device, &desc);
if (ret < 0) {
LOG_DBG("Failed to fetch the metadata for %s! %d", zbm->metadata.display_name, ret);
} else {
LOG_DBG("Got metadata with %d sets", desc.sets_len);
}
zmk_behaviors_GetBehaviorDetailsResponse resp =
zmk_behaviors_GetBehaviorDetailsResponse_init_zero;
resp.id = behavior_id;
resp.display_name.funcs.encode = encode_behavior_name;
resp.display_name.arg = zbm;
state.sets = desc.sets;
state.sets_len = desc.sets_len;
resp.metadata.funcs.encode = encode_metadata_sets;
resp.metadata.arg = &state;
return BEHAVIOR_RESPONSE(get_behavior_details, resp);
}
ZMK_RPC_SUBSYSTEM_HANDLER(behaviors, list_all_behaviors, ZMK_STUDIO_RPC_HANDLER_UNSECURED);
ZMK_RPC_SUBSYSTEM_HANDLER(behaviors, get_behavior_details, ZMK_STUDIO_RPC_HANDLER_SECURED);

50
app/src/studio/core.c Normal file
View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zmk/studio/core.h>
ZMK_EVENT_IMPL(zmk_studio_core_lock_state_changed);
static enum zmk_studio_core_lock_state state = IS_ENABLED(CONFIG_ZMK_STUDIO_LOCKING)
? ZMK_STUDIO_CORE_LOCK_STATE_LOCKED
: ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED;
enum zmk_studio_core_lock_state zmk_studio_core_get_lock_state(void) { return state; }
static void set_state(enum zmk_studio_core_lock_state new_state) {
if (state == new_state) {
return;
}
state = new_state;
raise_zmk_studio_core_lock_state_changed(
(struct zmk_studio_core_lock_state_changed){.state = state});
}
#if CONFIG_ZMK_STUDIO_LOCK_IDLE_TIMEOUT_SEC > 0
static void core_idle_lock_timeout_cb(struct k_work *work) { zmk_studio_core_lock(); }
K_WORK_DELAYABLE_DEFINE(core_idle_lock_timeout, core_idle_lock_timeout_cb);
void zmk_studio_core_reschedule_lock_timeout() {
k_work_reschedule(&core_idle_lock_timeout, K_SECONDS(CONFIG_ZMK_STUDIO_LOCK_IDLE_TIMEOUT_SEC));
}
#else
void zmk_studio_core_reschedule_lock_timeout() {}
#endif
void zmk_studio_core_unlock() {
set_state(ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED);
zmk_studio_core_reschedule_lock_timeout();
}
void zmk_studio_core_lock() { set_state(ZMK_STUDIO_CORE_LOCK_STATE_LOCKED); }

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <pb_encode.h>
#include <zmk/studio/core.h>
#include <zmk/studio/rpc.h>
ZMK_RPC_SUBSYSTEM(core)
#define CORE_RESPONSE(type, ...) ZMK_RPC_RESPONSE(core, type, __VA_ARGS__)
static bool encode_device_info_name(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
return pb_encode_string(stream, CONFIG_ZMK_KEYBOARD_NAME, strlen(CONFIG_ZMK_KEYBOARD_NAME));
}
#if IS_ENABLED(CONFIG_HWINFO)
static bool encode_device_info_serial_number(pb_ostream_t *stream, const pb_field_t *field,
void *const *arg) {
uint8_t id_buffer[32];
const ssize_t id_size = hwinfo_get_device_id(id_buffer, ARRAY_SIZE(id_buffer));
if (id_size <= 0) {
return true;
}
if (!pb_encode_tag_for_field(stream, field)) {
return false;
}
return pb_encode_string(stream, id_buffer, id_size);
}
#endif // IS_ENABLED(CONFIG_HWINFO)
zmk_studio_Response get_device_info(const zmk_studio_Request *req) {
zmk_core_GetDeviceInfoResponse resp = zmk_core_GetDeviceInfoResponse_init_zero;
resp.name.funcs.encode = encode_device_info_name;
#if IS_ENABLED(CONFIG_HWINFO)
resp.serial_number.funcs.encode = encode_device_info_serial_number;
#endif // IS_ENABLED(CONFIG_HWINFO)
return CORE_RESPONSE(get_device_info, resp);
}
zmk_studio_Response get_lock_state(const zmk_studio_Request *req) {
zmk_core_LockState resp = zmk_studio_core_get_lock_state();
return CORE_RESPONSE(get_lock_state, resp);
}
ZMK_RPC_SUBSYSTEM_HANDLER(core, get_device_info, ZMK_STUDIO_RPC_HANDLER_UNSECURED);
ZMK_RPC_SUBSYSTEM_HANDLER(core, get_lock_state, ZMK_STUDIO_RPC_HANDLER_UNSECURED);
static int core_event_mapper(const zmk_event_t *eh, zmk_studio_Notification *n) {
struct zmk_studio_core_lock_state_changed *lock_ev = as_zmk_studio_core_lock_state_changed(eh);
if (!lock_ev) {
return -ENOTSUP;
}
LOG_DBG("Mapped a lock state event properly");
*n = ZMK_RPC_NOTIFICATION(core, lock_state_changed, lock_ev->state);
return 0;
}
ZMK_RPC_EVENT_MAPPER(core, core_event_mapper, zmk_studio_core_lock_state_changed);

View File

@@ -0,0 +1,222 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <sys/types.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/sys/ring_buffer.h>
#include <zmk/ble.h>
#include <zmk/event_manager.h>
#include <zmk/events/ble_active_profile_changed.h>
#include <zmk/studio/rpc.h>
#include "uuid.h"
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk_studio, CONFIG_ZMK_STUDIO_LOG_LEVEL);
static bool handling_rx = false;
static atomic_t notify_size;
static void rpc_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) {
ARG_UNUSED(attr);
bool notif_enabled = (value == BT_GATT_CCC_INDICATE);
LOG_INF("RPC Notifications %s", notif_enabled ? "enabled" : "disabled");
#if CONFIG_ZMK_STUDIO_TRANSPORT_BLE_PREF_LATENCY < CONFIG_BT_PERIPHERAL_PREF_LATENCY
struct bt_conn *conn = zmk_ble_active_profile_conn();
if (conn) {
uint8_t latency = notif_enabled ? CONFIG_ZMK_STUDIO_TRANSPORT_BLE_PREF_LATENCY
: CONFIG_BT_PERIPHERAL_PREF_LATENCY;
int ret = bt_conn_le_param_update(
conn,
BT_LE_CONN_PARAM(CONFIG_BT_PERIPHERAL_PREF_MIN_INT, CONFIG_BT_PERIPHERAL_PREF_MAX_INT,
latency, CONFIG_BT_PERIPHERAL_PREF_TIMEOUT));
if (ret < 0) {
LOG_WRN("Failed to request lower latency while studio is active (%d)", ret);
}
bt_conn_unref(conn);
}
#endif
}
static ssize_t read_rpc_resp(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf,
uint16_t len, uint16_t offset) {
LOG_DBG("Read response for length %d at offset %d", len, offset);
return 0;
}
static ssize_t write_rpc_req(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf,
uint16_t len, uint16_t offset, uint8_t flags) {
if (!handling_rx) {
return len;
}
uint32_t copied = 0;
struct ring_buf *rpc_buf = zmk_rpc_get_rx_buf();
while (copied < len) {
uint8_t *buffer;
uint32_t claim_len = ring_buf_put_claim(rpc_buf, &buffer, len - copied);
if (claim_len > 0) {
memcpy(buffer, ((uint8_t *)buf) + copied, claim_len);
copied += claim_len;
}
ring_buf_put_finish(rpc_buf, claim_len);
}
zmk_rpc_rx_notify();
return len;
}
BT_GATT_SERVICE_DEFINE(
rpc_interface, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(ZMK_STUDIO_BT_SERVICE_UUID)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_STUDIO_BT_RPC_CHRC_UUID),
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ | BT_GATT_CHRC_INDICATE,
BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT, read_rpc_resp,
write_rpc_req, NULL),
BT_GATT_CCC(rpc_ccc_cfg_changed, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT));
static uint16_t get_notify_size_for_conn(struct bt_conn *conn) {
uint16_t notify_size = 23; // Default MTU size unless negotiated higher
struct bt_conn_info conn_info;
if (conn && bt_conn_get_info(conn, &conn_info) >= 0) {
notify_size = conn_info.le.data_len->tx_max_len;
}
return notify_size;
}
static void refresh_notify_size(void) {
struct bt_conn *conn = zmk_ble_active_profile_conn();
uint16_t ns = get_notify_size_for_conn(conn);
if (conn) {
bt_conn_unref(conn);
}
atomic_set(&notify_size, ns);
}
static int gatt_start_rx() {
refresh_notify_size();
handling_rx = true;
return 0;
}
static int gatt_stop_rx(void) {
handling_rx = false;
return 0;
}
static struct bt_gatt_indicate_params rpc_indicate_params = {
.attr = &rpc_interface.attrs[1],
};
static void notif_rpc_tx_cb(struct k_work *work) {
struct bt_conn *conn = zmk_ble_active_profile_conn();
struct ring_buf *tx_buf = zmk_rpc_get_tx_buf();
if (!conn) {
LOG_WRN("No active connection for queued data, dropping");
ring_buf_reset(tx_buf);
return;
}
uint16_t notify_size = get_notify_size_for_conn(conn);
uint8_t notify_bytes[notify_size];
while (ring_buf_size_get(tx_buf) > 0) {
uint16_t added = 0;
while (added < notify_size && ring_buf_size_get(tx_buf) > 0) {
uint8_t *buf;
int len = ring_buf_get_claim(tx_buf, &buf, notify_size - added);
memcpy(notify_bytes + added, buf, len);
added += len;
ring_buf_get_finish(tx_buf, len);
}
rpc_indicate_params.data = notify_bytes;
rpc_indicate_params.len = added;
int notify_attempts = 5;
do {
int err = bt_gatt_indicate(conn, &rpc_indicate_params);
if (err >= 0) {
break;
}
LOG_WRN("Failed to notify the response %d", err);
k_sleep(K_MSEC(200));
} while (notify_attempts-- > 0);
}
bt_conn_unref(conn);
}
static K_WORK_DEFINE(notify_tx_work, notif_rpc_tx_cb);
struct gatt_write_state {
size_t pending_notify;
};
static void gatt_tx_notify(struct ring_buf *tx_buf, size_t added, bool msg_done, void *user_data) {
struct gatt_write_state *state = (struct gatt_write_state *)user_data;
state->pending_notify += added;
atomic_t ns = atomic_get(&notify_size);
if (msg_done || state->pending_notify > ns) {
k_work_submit(&notify_tx_work);
state->pending_notify = 0;
}
}
static struct gatt_write_state tx_state = {};
static void *gatt_tx_user_data(void) {
memset(&tx_state, sizeof(tx_state), 0);
return &tx_state;
}
ZMK_RPC_TRANSPORT(gatt, ZMK_TRANSPORT_BLE, gatt_start_rx, gatt_stop_rx, gatt_tx_user_data,
gatt_tx_notify);
static int gatt_rpc_listener(const zmk_event_t *eh) {
refresh_notify_size();
#if IS_ENABLED(CONFIG_ZMK_STUDIO_LOCK_ON_DISCONNECT)
struct bt_conn *conn = zmk_ble_active_profile_conn();
if (!conn) {
zmk_studio_core_lock();
} else {
bt_conn_unref(conn);
}
#endif
return 0;
}
ZMK_LISTENER(gatt_rpc_listener, gatt_rpc_listener);
ZMK_SUBSCRIPTION(gatt_rpc_listener, zmk_ble_active_profile_changed);

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include "msg_framing.h"
static bool process_byte_err_state(enum studio_framing_state *rpc_framing_state, uint8_t c) {
switch (c) {
case FRAMING_EOF:
*rpc_framing_state = FRAMING_STATE_IDLE;
return false;
case FRAMING_SOF:
*rpc_framing_state = FRAMING_STATE_AWAITING_DATA;
return false;
default:
LOG_WRN("Discarding unexpected data 0x%02x", c);
return false;
}
return false;
}
static bool process_byte_idle_state(enum studio_framing_state *rpc_framing_state, uint8_t c) {
switch (c) {
case FRAMING_SOF:
*rpc_framing_state = FRAMING_STATE_AWAITING_DATA;
return false;
default:
LOG_WRN("Expected SOF, got 0x%02x", c);
return false;
}
return false;
}
static bool process_byte_awaiting_data_state(enum studio_framing_state *rpc_framing_state,
uint8_t c) {
switch (c) {
case FRAMING_SOF:
LOG_WRN("Unescaped SOF mid-data");
*rpc_framing_state = FRAMING_STATE_ERR;
return false;
case FRAMING_ESC:
*rpc_framing_state = FRAMING_STATE_ESCAPED;
return false;
case FRAMING_EOF:
*rpc_framing_state = FRAMING_STATE_EOF;
return false;
default:
return true;
}
return false;
}
static bool process_byte_escaped_state(enum studio_framing_state *rpc_framing_state, uint8_t c) {
*rpc_framing_state = FRAMING_STATE_AWAITING_DATA;
return true;
}
bool studio_framing_process_byte(enum studio_framing_state *rpc_framing_state, uint8_t c) {
switch (*rpc_framing_state) {
case FRAMING_STATE_ERR:
return process_byte_err_state(rpc_framing_state, c);
case FRAMING_STATE_IDLE:
case FRAMING_STATE_EOF:
return process_byte_idle_state(rpc_framing_state, c);
case FRAMING_STATE_AWAITING_DATA:
return process_byte_awaiting_data_state(rpc_framing_state, c);
case FRAMING_STATE_ESCAPED:
return process_byte_escaped_state(rpc_framing_state, c);
default:
LOG_ERR("Unsupported framing state: %d", *rpc_framing_state);
return false;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/kernel.h>
enum studio_framing_state {
FRAMING_STATE_IDLE,
FRAMING_STATE_AWAITING_DATA,
FRAMING_STATE_ESCAPED,
FRAMING_STATE_ERR,
FRAMING_STATE_EOF,
};
#define FRAMING_SOF 0xAB
#define FRAMING_ESC 0xAC
#define FRAMING_EOF 0xAD
/**
* @brief Process an incoming byte from a frame. Will possibly update the framing state depending on
* what data is received.
* @retval true if data is a non-framing byte, and is real data to be handled by the upper level
* logic.
* @retval false if data is a framing byte, and should be ignored. Also indicates the framing state
* has been updated.
*/
bool studio_framing_process_byte(enum studio_framing_state *frame_state, uint8_t data);

343
app/src/studio/rpc.c Normal file
View File

@@ -0,0 +1,343 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include "msg_framing.h"
#include <pb_encode.h>
#include <pb_decode.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zmk_studio, CONFIG_ZMK_STUDIO_LOG_LEVEL);
#include <zmk/endpoints.h>
#include <zmk/event_manager.h>
#include <zmk/events/endpoint_changed.h>
#include <zmk/studio/core.h>
#include <zmk/studio/rpc.h>
ZMK_EVENT_IMPL(zmk_studio_rpc_notification);
static struct zmk_rpc_subsystem *find_subsystem_for_choice(uint8_t choice) {
STRUCT_SECTION_FOREACH(zmk_rpc_subsystem, sub) {
if (sub->subsystem_choice == choice) {
return sub;
}
}
return NULL;
}
zmk_studio_Response zmk_rpc_subsystem_delegate_to_subs(const struct zmk_rpc_subsystem *subsys,
const zmk_studio_Request *req,
uint8_t which_req) {
LOG_DBG("Got subsystem func for %d", subsys->subsystem_choice);
for (int i = subsys->handlers_start_index; i <= subsys->handlers_end_index; i++) {
struct zmk_rpc_subsystem_handler *sub_handler;
STRUCT_SECTION_GET(zmk_rpc_subsystem_handler, i, &sub_handler);
if (sub_handler->request_choice == which_req) {
if (sub_handler->security == ZMK_STUDIO_RPC_HANDLER_SECURED &&
zmk_studio_core_get_lock_state() != ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED) {
return ZMK_RPC_RESPONSE(meta, simple_error,
zmk_meta_ErrorConditions_UNLOCK_REQUIRED);
}
return sub_handler->func(req);
}
}
LOG_ERR("No handler func found for %d", which_req);
return ZMK_RPC_RESPONSE(meta, simple_error, zmk_meta_ErrorConditions_RPC_NOT_FOUND);
}
static zmk_studio_Response handle_request(const zmk_studio_Request *req) {
zmk_studio_core_reschedule_lock_timeout();
struct zmk_rpc_subsystem *sub = find_subsystem_for_choice(req->which_subsystem);
if (!sub) {
LOG_WRN("No subsystem found for choice %d", req->which_subsystem);
return ZMK_RPC_RESPONSE(meta, simple_error, zmk_meta_ErrorConditions_RPC_NOT_FOUND);
}
zmk_studio_Response resp = sub->func(sub, req);
resp.type.request_response.request_id = req->request_id;
return resp;
}
RING_BUF_DECLARE(rpc_rx_buf, CONFIG_ZMK_STUDIO_RPC_RX_BUF_SIZE);
static K_SEM_DEFINE(rpc_rx_sem, 0, 1);
static enum studio_framing_state rpc_framing_state;
static K_MUTEX_DEFINE(rpc_transport_mutex);
static struct zmk_rpc_transport *selected_transport;
struct ring_buf *zmk_rpc_get_rx_buf(void) {
return &rpc_rx_buf;
}
void zmk_rpc_rx_notify(void) { k_sem_give(&rpc_rx_sem); }
static bool rpc_read_cb(pb_istream_t *stream, uint8_t *buf, size_t count) {
uint32_t write_offset = 0;
do {
uint8_t *buffer;
uint32_t len = ring_buf_get_claim(&rpc_rx_buf, &buffer, count);
if (len > 0) {
for (int i = 0; i < len; i++) {
if (studio_framing_process_byte(&rpc_framing_state, buffer[i])) {
buf[write_offset++] = buffer[i];
}
}
} else {
k_sem_take(&rpc_rx_sem, K_FOREVER);
}
ring_buf_get_finish(&rpc_rx_buf, len);
} while (write_offset < count && rpc_framing_state != FRAMING_STATE_EOF);
if (rpc_framing_state == FRAMING_STATE_EOF) {
stream->bytes_left = 0;
return false;
} else {
return true;
}
}
static pb_istream_t pb_istream_for_rx_ring_buf() {
pb_istream_t stream = {&rpc_read_cb, NULL, SIZE_MAX};
return stream;
}
RING_BUF_DECLARE(rpc_tx_buf, CONFIG_ZMK_STUDIO_RPC_TX_BUF_SIZE);
struct ring_buf *zmk_rpc_get_tx_buf(void) {
return &rpc_tx_buf;
}
static bool rpc_tx_buffer_write(pb_ostream_t *stream, const uint8_t *buf, size_t count) {
void *user_data = stream->state;
size_t written = 0;
bool escape_byte_already_written = false;
do {
uint32_t write_idx = 0;
uint8_t *write_buf;
uint32_t claim_len = ring_buf_put_claim(&rpc_tx_buf, &write_buf, count - written);
if (claim_len == 0) {
continue;
}
int escapes_written = 0;
for (int i = 0; i < claim_len && write_idx < claim_len; i++) {
uint8_t b = buf[written + i];
switch (b) {
case FRAMING_EOF:
case FRAMING_ESC:
case FRAMING_SOF:
// Care to be taken. We may need to write the escape byte,
// but that's the last available spot for this claim, so we track
// if the escape has already been written in the previous iteration
// of our loop.
if (!escape_byte_already_written) {
escapes_written++;
write_buf[write_idx++] = FRAMING_ESC;
escape_byte_already_written = true;
if (write_idx >= claim_len) {
LOG_WRN("Skipping on, no room to write escape and real byte");
continue;
}
}
default:
write_buf[write_idx++] = b;
escape_byte_already_written = false;
break;
}
}
ring_buf_put_finish(&rpc_tx_buf, write_idx);
written += (write_idx - escapes_written);
selected_transport->tx_notify(&rpc_tx_buf, write_idx, false, user_data);
} while (written < count);
return true;
}
static pb_ostream_t pb_ostream_for_tx_buf(void *user_data) {
pb_ostream_t stream = {&rpc_tx_buffer_write, (void *)user_data, SIZE_MAX, 0};
return stream;
}
static int send_response(const zmk_studio_Response *resp) {
k_mutex_lock(&rpc_transport_mutex, K_FOREVER);
if (!selected_transport) {
goto exit;
}
void *user_data = selected_transport->tx_user_data ? selected_transport->tx_user_data() : NULL;
pb_ostream_t stream = pb_ostream_for_tx_buf(user_data);
uint8_t framing_byte = FRAMING_SOF;
ring_buf_put(&rpc_tx_buf, &framing_byte, 1);
selected_transport->tx_notify(&rpc_tx_buf, 1, false, user_data);
/* Now we are ready to encode the message! */
bool status = pb_encode(&stream, &zmk_studio_Response_msg, resp);
if (!status) {
#if !IS_ENABLED(CONFIG_NANOPB_NO_ERRMSG)
LOG_ERR("Failed to encode the message %s", stream.errmsg);
#endif // !IS_ENABLED(CONFIG_NANOPB_NO_ERRMSG)
return -EINVAL;
}
framing_byte = FRAMING_EOF;
ring_buf_put(&rpc_tx_buf, &framing_byte, 1);
selected_transport->tx_notify(&rpc_tx_buf, 1, true, user_data);
exit:
k_mutex_unlock(&rpc_transport_mutex);
return 0;
}
static void rpc_main(void) {
for (;;) {
pb_istream_t stream = pb_istream_for_rx_ring_buf();
zmk_studio_Request req = zmk_studio_Request_init_zero;
bool status = pb_decode(&stream, &zmk_studio_Request_msg, &req);
rpc_framing_state = FRAMING_STATE_IDLE;
if (status) {
zmk_studio_Response resp = handle_request(&req);
int err = send_response(&resp);
if (err < 0) {
LOG_ERR("Failed to send the RPC response %d", err);
}
} else {
LOG_DBG("Decode failed");
}
}
}
K_THREAD_DEFINE(studio_rpc_thread, CONFIG_ZMK_STUDIO_RPC_THREAD_STACK_SIZE, rpc_main, NULL, NULL,
NULL, K_LOWEST_APPLICATION_THREAD_PRIO, 0, 0);
static void refresh_selected_transport(void) {
enum zmk_transport transport = zmk_endpoints_selected().transport;
k_mutex_lock(&rpc_transport_mutex, K_FOREVER);
if (selected_transport && selected_transport->transport == transport) {
return;
}
if (selected_transport) {
if (selected_transport->rx_stop) {
selected_transport->rx_stop();
}
selected_transport = NULL;
#if IS_ENABLED(CONFIG_ZMK_STUDIO_LOCK_ON_DISCONNECT)
zmk_studio_core_lock();
#endif
}
STRUCT_SECTION_FOREACH(zmk_rpc_transport, t) {
if (t->transport == transport) {
selected_transport = t;
if (selected_transport->rx_start) {
selected_transport->rx_start();
}
break;
}
}
if (!selected_transport) {
LOG_WRN("Failed to select a transport!");
}
k_mutex_unlock(&rpc_transport_mutex);
}
static int zmk_rpc_init(void) {
int prev_choice = -1;
struct zmk_rpc_subsystem *prev_sub = NULL;
int i = 0;
STRUCT_SECTION_FOREACH(zmk_rpc_subsystem_handler, handler) {
struct zmk_rpc_subsystem *sub = find_subsystem_for_choice(handler->subsystem_choice);
__ASSERT(sub != NULL, "RPC Handler for unknown subsystem choice %d",
handler->subsystem_choice);
if (prev_choice < 0) {
sub->handlers_start_index = i;
} else if ((prev_choice != handler->subsystem_choice) && prev_sub) {
prev_sub->handlers_end_index = i - 1;
sub->handlers_start_index = i;
}
prev_choice = handler->subsystem_choice;
prev_sub = sub;
i++;
}
if (prev_sub) {
prev_sub->handlers_end_index = i - 1;
}
refresh_selected_transport();
return 0;
}
SYS_INIT(zmk_rpc_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
static int studio_rpc_listener_cb(const zmk_event_t *eh) {
struct zmk_endpoint_changed *ep_changed = as_zmk_endpoint_changed(eh);
if (ep_changed) {
refresh_selected_transport();
return ZMK_EV_EVENT_BUBBLE;
}
struct zmk_studio_rpc_notification *rpc_notify = as_zmk_studio_rpc_notification(eh);
if (rpc_notify) {
zmk_studio_Response resp = zmk_studio_Response_init_zero;
resp.which_type = zmk_studio_Response_notification_tag;
resp.type.notification = rpc_notify->notification;
send_response(&resp);
return ZMK_EV_EVENT_BUBBLE;
}
zmk_studio_Notification n = zmk_studio_Notification_init_zero;
STRUCT_SECTION_FOREACH(zmk_rpc_event_mapper, mapper) {
int ret = mapper->func(eh, &n);
if (ret >= 0) {
raise_zmk_studio_rpc_notification(
(struct zmk_studio_rpc_notification){.notification = n});
break;
}
}
return ZMK_EV_EVENT_BUBBLE;
}
ZMK_LISTENER(studio_rpc, studio_rpc_listener_cb);
ZMK_SUBSCRIPTION(studio_rpc, zmk_endpoint_changed);
ZMK_SUBSCRIPTION(studio_rpc, zmk_studio_rpc_notification);

View File

@@ -0,0 +1,170 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>
#include <zmk/studio/rpc.h>
LOG_MODULE_DECLARE(zmk_studio, CONFIG_ZMK_STUDIO_LOG_LEVEL);
/* change this to any other UART peripheral if desired */
#define UART_DEVICE_NODE DT_CHOSEN(zmk_studio_rpc_uart)
static const struct device *const uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE);
static void tx_notify(struct ring_buf *tx_ring_buf, size_t written, bool msg_done,
void *user_data) {
if (msg_done || (ring_buf_size_get(tx_ring_buf) > (ring_buf_capacity_get(tx_ring_buf) / 2))) {
#if IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
uart_irq_tx_enable(uart_dev);
#else
struct ring_buf *tx_buf = zmk_rpc_get_tx_buf();
uint8_t *buf;
uint32_t claim_len;
while ((claim_len = ring_buf_get_claim(tx_buf, &buf, tx_buf->size)) > 0) {
for (int i = 0; i < claim_len; i++) {
uart_poll_out(uart_dev, buf[i]);
}
ring_buf_get_finish(tx_buf, claim_len);
}
#endif
}
}
#if !IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
static void uart_rx_main(void) {
for (;;) {
uint8_t *buf;
struct ring_buf *ring_buf = zmk_rpc_get_rx_buf();
uint32_t claim_len = ring_buf_put_claim(ring_buf, &buf, 1);
if (claim_len < 1) {
LOG_WRN("NO CLAIM ABLE TO BE HAD");
k_sleep(K_MSEC(1));
continue;
}
if (uart_poll_in(uart_dev, buf) < 0) {
ring_buf_put_finish(ring_buf, 0);
k_sleep(K_MSEC(1));
} else {
ring_buf_put_finish(ring_buf, 1);
zmk_rpc_rx_notify();
}
}
}
K_THREAD_DEFINE(uart_transport_read_thread, CONFIG_ZMK_STUDIO_TRANSPORT_UART_RX_STACK_SIZE,
uart_rx_main, NULL, NULL, NULL, K_LOWEST_APPLICATION_THREAD_PRIO, 0, 0);
#endif
static int start_rx() {
#if IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
uart_irq_rx_enable(uart_dev);
#else
k_thread_resume(uart_transport_read_thread);
#endif
return 0;
}
static int stop_rx(void) {
#if IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
uart_irq_rx_disable(uart_dev);
#else
k_thread_suspend(uart_transport_read_thread);
#endif
return 0;
}
ZMK_RPC_TRANSPORT(uart, ZMK_TRANSPORT_USB, start_rx, stop_rx, NULL, tx_notify);
#if IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
/*
* Read characters from UART until line end is detected. Afterwards push the
* data to the message queue.
*/
static void serial_cb(const struct device *dev, void *user_data) {
if (!uart_irq_update(uart_dev)) {
return;
}
if (uart_irq_rx_ready(uart_dev)) {
/* read until FIFO empty */
uint32_t last_read = 0, len = 0;
struct ring_buf *buf = zmk_rpc_get_rx_buf();
do {
uint8_t *buffer;
len = ring_buf_put_claim(buf, &buffer, buf->size);
if (len > 0) {
last_read = uart_fifo_read(uart_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(uart_dev, &dummy, 1);
}
} while (last_read && last_read == len);
zmk_rpc_rx_notify();
}
if (uart_irq_tx_ready(uart_dev)) {
struct ring_buf *tx_buf = zmk_rpc_get_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) {
continue;
}
int sent = uart_fifo_fill(uart_dev, buf, claim_len);
ring_buf_get_finish(tx_buf, MAX(sent, 0));
}
}
}
#endif
static int uart_rpc_interface_init(void) {
if (!device_is_ready(uart_dev)) {
LOG_ERR("UART device not found!");
return -ENODEV;
}
#if IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
/* configure interrupt and callback to receive data */
int ret = uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL);
if (ret < 0) {
if (ret == -ENOTSUP) {
printk("Interrupt-driven UART API support not enabled\n");
} else if (ret == -ENOSYS) {
printk("UART device does not support interrupt-driven API\n");
} else {
printk("Error setting UART callback: %d\n", ret);
}
return ret;
}
#endif // IS_ENABLED(CONFIG_UART_INTERRUPT_DRIVEN)
return 0;
}
SYS_INIT(uart_rpc_interface_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);

13
app/src/studio/uuid.h Normal file
View File

@@ -0,0 +1,13 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/bluetooth/uuid.h>
#define ZMK_BT_STUDIO_UUID(num) BT_UUID_128_ENCODE(num, 0x0196, 0x6107, 0xc967, 0xc5cfb1c2482a)
#define ZMK_STUDIO_BT_SERVICE_UUID ZMK_BT_STUDIO_UUID(0x00000000)
#define ZMK_STUDIO_BT_RPC_CHRC_UUID ZMK_BT_STUDIO_UUID(0x00000001)

View File

@@ -29,5 +29,13 @@ manifest:
- openthread
- edtt
- trusted-firmware-m
- name: nanopb
revision: 65cbefb4695bc7af1cb733ced99618afb3586b20
path: modules/lib/nanopb
remote: zephyrproject-rtos
- name: zmk-studio-messages
revision: 42446798e357e8021c5202a01ea250a34a776e85
path: modules/msgs/zmk-studio-messages
remote: zmkfirmware
self:
west-commands: scripts/west-commands.yml