diff options
author | Peter Johanson <[email protected]> | 2024-02-19 08:48:20 +0000 |
---|---|---|
committer | Pete Johanson <[email protected]> | 2024-08-15 11:45:18 -0600 |
commit | feda96eb40f66440143e2dcfa26b5fcac6f38f33 (patch) | |
tree | beabac37cf220e3a64156a2503f9e61a23aa2d94 /app/src/studio | |
parent | ea64fcaf71279d9a718265653c98d7c80a51d252 (diff) | |
download | zmk-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.txt | 15 | ||||
-rw-r--r-- | app/src/studio/Kconfig | 95 | ||||
-rw-r--r-- | app/src/studio/behavior_subsystem.c | 211 | ||||
-rw-r--r-- | app/src/studio/core.c | 50 | ||||
-rw-r--r-- | app/src/studio/core_subsystem.c | 80 | ||||
-rw-r--r-- | app/src/studio/gatt_rpc_transport.c | 222 | ||||
-rw-r--r-- | app/src/studio/msg_framing.c | 80 | ||||
-rw-r--r-- | app/src/studio/msg_framing.h | 31 | ||||
-rw-r--r-- | app/src/studio/rpc.c | 343 | ||||
-rw-r--r-- | app/src/studio/uart_rpc_transport.c | 170 | ||||
-rw-r--r-- | app/src/studio/uuid.h | 13 |
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(¬ify_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(¬ify_size); + + if (msg_done || state->pending_notify > ns) { + k_work_submit(¬ify_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) |