aboutsummaryrefslogtreecommitdiffhomepage
path: root/app/src/studio
diff options
context:
space:
mode:
authorPeter Johanson <[email protected]>2024-02-19 08:48:20 +0000
committerPete Johanson <[email protected]>2024-08-15 11:45:18 -0600
commitfeda96eb40f66440143e2dcfa26b5fcac6f38f33 (patch)
treebeabac37cf220e3a64156a2503f9e61a23aa2d94 /app/src/studio
parentea64fcaf71279d9a718265653c98d7c80a51d252 (diff)
downloadzmk-feda96eb40f66440143e2dcfa26b5fcac6f38f33.tar.gz
zmk-feda96eb40f66440143e2dcfa26b5fcac6f38f33.zip
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 <[email protected]>
Diffstat (limited to 'app/src/studio')
-rw-r--r--app/src/studio/CMakeLists.txt15
-rw-r--r--app/src/studio/Kconfig95
-rw-r--r--app/src/studio/behavior_subsystem.c211
-rw-r--r--app/src/studio/core.c50
-rw-r--r--app/src/studio/core_subsystem.c80
-rw-r--r--app/src/studio/gatt_rpc_transport.c222
-rw-r--r--app/src/studio/msg_framing.c80
-rw-r--r--app/src/studio/msg_framing.h31
-rw-r--r--app/src/studio/rpc.c343
-rw-r--r--app/src/studio/uart_rpc_transport.c170
-rw-r--r--app/src/studio/uuid.h13
11 files changed, 1310 insertions, 0 deletions
diff --git a/app/src/studio/CMakeLists.txt b/app/src/studio/CMakeLists.txt
new file mode 100644
index 0000000000..e8f0d49d26
--- /dev/null
+++ b/app/src/studio/CMakeLists.txt
@@ -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) \ No newline at end of file
diff --git a/app/src/studio/Kconfig b/app/src/studio/Kconfig
new file mode 100644
index 0000000000..ebe680bb8c
--- /dev/null
+++ b/app/src/studio/Kconfig
@@ -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
diff --git a/app/src/studio/behavior_subsystem.c b/app/src/studio/behavior_subsystem.c
new file mode 100644
index 0000000000..b8d1ef1d67
--- /dev/null
+++ b/app/src/studio/behavior_subsystem.c
@@ -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);
diff --git a/app/src/studio/core.c b/app/src/studio/core.c
new file mode 100644
index 0000000000..fafe0248c9
--- /dev/null
+++ b/app/src/studio/core.c
@@ -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); } \ No newline at end of file
diff --git a/app/src/studio/core_subsystem.c b/app/src/studio/core_subsystem.c
new file mode 100644
index 0000000000..001aed9b9c
--- /dev/null
+++ b/app/src/studio/core_subsystem.c
@@ -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);
diff --git a/app/src/studio/gatt_rpc_transport.c b/app/src/studio/gatt_rpc_transport.c
new file mode 100644
index 0000000000..f0ab3152aa
--- /dev/null
+++ b/app/src/studio/gatt_rpc_transport.c
@@ -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);
diff --git a/app/src/studio/msg_framing.c b/app/src/studio/msg_framing.c
new file mode 100644
index 0000000000..a14289ee6f
--- /dev/null
+++ b/app/src/studio/msg_framing.c
@@ -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;
+ }
+} \ No newline at end of file
diff --git a/app/src/studio/msg_framing.h b/app/src/studio/msg_framing.h
new file mode 100644
index 0000000000..cbe4abdfb0
--- /dev/null
+++ b/app/src/studio/msg_framing.h
@@ -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);
diff --git a/app/src/studio/rpc.c b/app/src/studio/rpc.c
new file mode 100644
index 0000000000..3d5bfcd7a1
--- /dev/null
+++ b/app/src/studio/rpc.c
@@ -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);
diff --git a/app/src/studio/uart_rpc_transport.c b/app/src/studio/uart_rpc_transport.c
new file mode 100644
index 0000000000..d4a4de832a
--- /dev/null
+++ b/app/src/studio/uart_rpc_transport.c
@@ -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);
diff --git a/app/src/studio/uuid.h b/app/src/studio/uuid.h
new file mode 100644
index 0000000000..4b412ac8d2
--- /dev/null
+++ b/app/src/studio/uuid.h
@@ -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)