aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--app/CMakeLists.txt23
-rw-r--r--app/Kconfig2
-rw-r--r--app/include/linker/zmk-rpc-event-mappers.ld9
-rw-r--r--app/include/linker/zmk-rpc-subsystem-handlers.ld9
-rw-r--r--app/include/linker/zmk-rpc-subsystems.ld9
-rw-r--r--app/include/linker/zmk-rpc-transport.ld9
-rw-r--r--app/include/zmk/hid.h16
-rw-r--r--app/include/zmk/studio/core.h31
-rw-r--r--app/include/zmk/studio/rpc.h215
-rw-r--r--app/src/hid.c5
-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
-rw-r--r--app/west.yml8
-rw-r--r--docs/docs/development/studio-rpc-protocol.md139
-rw-r--r--docs/docusaurus.config.js2
-rw-r--r--docs/package-lock.json1059
-rw-r--r--docs/package.json1
-rw-r--r--docs/sidebars.js1
-rw-r--r--docs/src/docusaurus-tree-sitter-plugin/index.js1
28 files changed, 2840 insertions, 9 deletions
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 5e19713a11..afc0431b68 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -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)
diff --git a/app/Kconfig b/app/Kconfig
index a45f2dc23f..1189c6547b 100644
--- a/app/Kconfig
+++ b/app/Kconfig
@@ -258,6 +258,8 @@ rsource "src/split/Kconfig"
#Basic Keyboard Setup
endmenu
+rsource "src/studio/Kconfig"
+
menu "Display/LED Options"
rsource "src/display/Kconfig"
diff --git a/app/include/linker/zmk-rpc-event-mappers.ld b/app/include/linker/zmk-rpc-event-mappers.ld
new file mode 100644
index 0000000000..bc5a0eea1f
--- /dev/null
+++ b/app/include/linker/zmk-rpc-event-mappers.ld
@@ -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)
diff --git a/app/include/linker/zmk-rpc-subsystem-handlers.ld b/app/include/linker/zmk-rpc-subsystem-handlers.ld
new file mode 100644
index 0000000000..286af1e48b
--- /dev/null
+++ b/app/include/linker/zmk-rpc-subsystem-handlers.ld
@@ -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)
diff --git a/app/include/linker/zmk-rpc-subsystems.ld b/app/include/linker/zmk-rpc-subsystems.ld
new file mode 100644
index 0000000000..9373154f09
--- /dev/null
+++ b/app/include/linker/zmk-rpc-subsystems.ld
@@ -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)
diff --git a/app/include/linker/zmk-rpc-transport.ld b/app/include/linker/zmk-rpc-transport.ld
new file mode 100644
index 0000000000..d5178c3d55
--- /dev/null
+++ b/app/include/linker/zmk-rpc-transport.ld
@@ -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)
diff --git a/app/include/zmk/hid.h b/app/include/zmk/hid.h
index 41f559b518..766fb9c463 100644
--- a/app/include/zmk/hid.h
+++ b/app/include/zmk/hid.h
@@ -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
diff --git a/app/include/zmk/studio/core.h b/app/include/zmk/studio/core.h
new file mode 100644
index 0000000000..a8fdc8ff00
--- /dev/null
+++ b/app/include/zmk/studio/core.h
@@ -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(); \ No newline at end of file
diff --git a/app/include/zmk/studio/rpc.h b/app/include/zmk/studio/rpc.h
new file mode 100644
index 0000000000..07bd98e464
--- /dev/null
+++ b/app/include/zmk/studio/rpc.h
@@ -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, \
+ }
diff --git a/app/src/hid.c b/app/src/hid.c
index 582db6763d..24572ad325 100644
--- a/app/src/hid.c
+++ b/app/src/hid.c
@@ -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; \
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)
diff --git a/app/west.yml b/app/west.yml
index 1b50247786..ac4964f370 100644
--- a/app/west.yml
+++ b/app/west.yml
@@ -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
diff --git a/docs/docs/development/studio-rpc-protocol.md b/docs/docs/development/studio-rpc-protocol.md
new file mode 100644
index 0000000000..91225bbaf0
--- /dev/null
+++ b/docs/docs/development/studio-rpc-protocol.md
@@ -0,0 +1,139 @@
+---
+title: Studio RPC Protocol
+---
+
+:::warning[Alpha Feature
+]
+ZMK Studio is still in active development, and the below information is for development purposes only. For up to date information, join the [ZMK Discord](https://zmk.dev/community/discord/invite) server and discuss in `#studio-development`.
+
+:::
+
+## Overview
+
+The ZMK Studio UI communicates with ZMK devices using a custom RPC protocol developed to be robust and reliable, while remaining simple and easy to extend with future enhancements.
+
+The protocol consists of [protocol buffer](https://protobuf.dev/programming-guides/proto3/) messages which are encoded/decoded using message framing, and then transmitted using an underlying transport. Two transports are currently implemented: a BLE transport using a custom GATT service and a serial port transport, which usually is used with CDC ADM devices over USB.
+
+## Protobuf Messages
+
+The messages for ZMK Studio are defined in a dedicated [zmk-studio-messages](https://github.com/zmkfirmware/zmk-studio-messages) repository. Fundamentally, the [`Request`](https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/studio.proto#L11) message is used to send any requests from the Studio client to the ZMK device, and the [`Response`](https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/studio.proto#L21) messages are sent from the ZMK device to the Studio client.
+
+Responses can either be [`RequestResponses`](https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/studio.proto#L28) that are sent in response to an incoming `Request` or a [`Notification`](https://github.com/zmkfirmware/zmk-studio-messages/blob/main/proto/zmk/studio.proto#L38) which is sent at any point from the ZMK device to the Studio client to inform the client about state changes on the device, e.g. that the device is unlocked.
+
+## Message Framing
+
+Studio uses a simple framing protocol to easily identify the start and end of a given message, with basic escaping to allow for unrestricted content.
+
+The following special bytes are used for the framing protocol:
+
+- Start of Frame (SoF): `0xAB`
+- Escape Byte (Esc): `0xAC`
+- End of Frame (EoF): `0xAD`
+
+A message consists of a SoF byte, the payload, escaped as needed, followed by an EoF byte. Within the payload, any of the special encoding bytes will be escaped by being prefixed with an Esc byte.
+
+### Example Encoding (Simple)
+
+Here is an example encoding when the message content does not include any of the special bytes:
+
+```mermaid
+block-beta
+ columns 5
+
+ space:1
+ block:group1:3
+ columns 3
+ contentLabel["Content"]:1 space:2
+ OrigA["0x12"] OrigB["0x01"] OrigC["0xBB"]
+ end
+
+ space
+
+ down<[" "]>(down):5
+
+ block:groupSoF:1
+ columns 1
+ SoFLabel["SoF"]
+ SoF["0xAB"]
+ end
+
+ block:group2:3
+ columns 3
+ contentLabel2["Content"]:1 space:2
+ EncA["0x12"]
+ EncB["0x01"]
+ EncC["0xBB"]
+ end
+ block:groupEoF:1
+ columns 1
+ EoFLabel["EoF"]
+ 0xAD
+ end
+
+ class contentLabel boxLabel
+ class contentLabel2 boxLabel
+ class SoFLabel boxLabel
+ class EoFLabel boxLabel
+
+ classDef boxLabel stroke:transparent,fill:transparent
+```
+
+### Example Encoding (Escaping)
+
+When the message content includes any of the special bytes, those bytes are escaped whe framed
+
+```mermaid
+block-beta
+ columns 6
+
+ space:1
+ block:group1:4
+ columns 5
+ contentLabel["Content"]:1 space:4
+ OrigA["0x12"] space OrigB["0xAD"] space OrigC["0xAC"]
+ end
+
+ space:1
+
+ down<[" "]>(down):6
+
+ block:groupSoF:1
+ columns 1
+ SoFLabel["SoF"]
+ SoF["0xAB"]
+ end
+
+ block:group2:4
+ columns 5
+ contentLabel2["Content"]:1 space:4
+ EncA["0x12"]
+ EscB["0xAC"]
+ EncB["0xAD"]
+ EscC["0xAC"]
+ EncC["0xAC"]
+ end
+ block:groupEoF:1
+ columns 1
+ EoFLabel["EoF"]
+ 0xAD
+ end
+
+ class contentLabel boxLabel
+ class contentLabel2 boxLabel
+ class SoFLabel boxLabel
+ class EoFLabel boxLabel
+
+ classDef boxLabel stroke:transparent,fill:transparent
+```
+
+## Transports
+
+Two transports are available right now, over USB or Bluetooth connections.
+
+### USB (Serial)
+
+The USB transport is actually a basic serial/UART transport, that happens to use the CDC/ACM USB class for a serial connection. Framed messages are sent between Studio client and ZMK device using simple UART transmission.
+
+### Bluetooth (GATT)
+
+The bluetooth transport uses a custom GATT service to transmit/receive. The service has UUID `00000000-0196-6107-c967-c5cfb1c2482a` and has exactly one characteristic with UUID `00000001-0196-6107-c967-c5cfb1c2482a`. The characteristic accepts writes of framed client messages, and will use GATT Indications to send framed messages to the client.
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 4017b125db..ae26fd4328 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -12,6 +12,7 @@ module.exports = {
organizationName: "zmkfirmware", // Usually your GitHub org/user name.
projectName: "zmk", // Usually your repo name.
plugins: [
+ "@docusaurus/theme-mermaid",
path.resolve(__dirname, "src/docusaurus-tree-sitter-plugin"),
path.resolve(__dirname, "src/hardware-metadata-collection-plugin"),
path.resolve(__dirname, "src/hardware-metadata-static-plugin"),
@@ -164,6 +165,7 @@ module.exports = {
],
],
markdown: {
+ mermaid: true,
mdx1Compat: {
comments: false,
admonitions: false,
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 3d9e459c36..e4009e20a5 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@docusaurus/core": "^3.0.0",
"@docusaurus/preset-classic": "^3.0.0",
+ "@docusaurus/theme-mermaid": "^3.0.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
@@ -2156,6 +2157,11 @@
"url": "https://github.com/sponsors/philsturgeon"
}
},
+ "node_modules/@braintree/sanitize-url": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
+ "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -2667,6 +2673,27 @@
"react-dom": "^18.0.0"
}
},
+ "node_modules/@docusaurus/theme-mermaid": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.1.0.tgz",
+ "integrity": "sha512-63y08fvRWIe9satRV1e/Dps9he+sPjQ+kwl4ccQQEzkM2nxeAgWwk8WzpbVhm1Pf02N/11y0C6FcvFqn4dERHA==",
+ "dependencies": {
+ "@docusaurus/core": "3.1.0",
+ "@docusaurus/module-type-aliases": "3.1.0",
+ "@docusaurus/theme-common": "3.1.0",
+ "@docusaurus/types": "3.1.0",
+ "@docusaurus/utils-validation": "3.1.0",
+ "mermaid": "^10.4.0",
+ "tslib": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.1.0.tgz",
@@ -3905,6 +3932,24 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
+ "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
+ "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw=="
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
+ "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4065,7 +4110,6 @@
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
"integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==",
- "dev": true,
"dependencies": {
"@types/unist": "^2"
}
@@ -4073,8 +4117,7 @@
"node_modules/@types/mdast/node_modules/@types/unist": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
- "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==",
- "dev": true
+ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
},
"node_modules/@types/mdx": {
"version": "2.0.10",
@@ -5852,6 +5895,14 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "node_modules/cose-base": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
+ "dependencies": {
+ "layout-base": "^1.0.0"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -6157,6 +6208,25 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "node_modules/cytoscape": {
+ "version": "3.30.0",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.0.tgz",
+ "integrity": "sha512-l590mjTHT6/Cbxp13dGPC2Y7VXdgc+rUeF8AnF/JPzhjNevbDJfObnJgaSjlldOgBQZbue+X6IUZ7r5GAgvauQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/cytoscape-cose-bilkent": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
+ "dependencies": {
+ "cose-base": "^1.0.0"
+ },
+ "peerDependencies": {
+ "cytoscape": "^3.2.0"
+ }
+ },
"node_modules/d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
@@ -6167,6 +6237,444 @@
"type": "^1.0.1"
}
},
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-sankey": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
+ "dependencies": {
+ "d3-array": "1 - 2",
+ "d3-shape": "^1.2.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-array": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
+ "dependencies": {
+ "internmap": "^1.0.0"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/d3-path": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
+ },
+ "node_modules/d3-sankey/node_modules/d3-shape": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
+ "dependencies": {
+ "d3-path": "1"
+ }
+ },
+ "node_modules/d3-sankey/node_modules/internmap": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dagre-d3-es": {
+ "version": "7.0.10",
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
+ "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
+ "dependencies": {
+ "d3": "^7.8.2",
+ "lodash-es": "^4.17.21"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.11",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
+ "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
+ },
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
@@ -6324,6 +6832,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -6497,6 +7013,11 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz",
+ "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA=="
+ },
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
@@ -6561,6 +7082,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.630.tgz",
"integrity": "sha512-osHqhtjojpCsACVnuD11xO5g9xaCyw7Qqn/C2KParkMv42i8jrJJgx3g7mkHfpxwhy9MnOJr8+pKOdZ7qzgizg=="
},
+ "node_modules/elkjs": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
+ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -9017,6 +9543,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/interpret": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
@@ -9806,6 +10340,29 @@
"node": ">=4.0"
}
},
+ "node_modules/katex": {
+ "version": "0.16.10",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz",
+ "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -9814,6 +10371,11 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/khroma": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
+ },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -9853,6 +10415,11 @@
"shell-quote": "^1.8.1"
}
},
+ "node_modules/layout-base": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9941,6 +10508,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -12172,6 +12744,450 @@
"node": ">= 8"
}
},
+ "node_modules/mermaid": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz",
+ "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==",
+ "dependencies": {
+ "@braintree/sanitize-url": "^6.0.1",
+ "@types/d3-scale": "^4.0.3",
+ "@types/d3-scale-chromatic": "^3.0.0",
+ "cytoscape": "^3.28.1",
+ "cytoscape-cose-bilkent": "^4.1.0",
+ "d3": "^7.4.0",
+ "d3-sankey": "^0.12.3",
+ "dagre-d3-es": "7.0.10",
+ "dayjs": "^1.11.7",
+ "dompurify": "^3.0.5",
+ "elkjs": "^0.9.0",
+ "katex": "^0.16.9",
+ "khroma": "^2.0.0",
+ "lodash-es": "^4.17.21",
+ "mdast-util-from-markdown": "^1.3.0",
+ "non-layered-tidy-tree-layout": "^2.0.2",
+ "stylis": "^4.1.3",
+ "ts-dedent": "^2.2.0",
+ "uuid": "^9.0.0",
+ "web-worker": "^1.2.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/@types/unist": {
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
+ "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ },
+ "node_modules/mermaid/node_modules/mdast-util-from-markdown": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+ "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "mdast-util-to-string": "^3.1.0",
+ "micromark": "^3.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "uvu": "^0.5.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/mdast-util-to-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+ "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+ "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.1",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-core-commonmark": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+ "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-factory-destination": "^1.0.0",
+ "micromark-factory-label": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-factory-title": "^1.0.0",
+ "micromark-factory-whitespace": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-classify-character": "^1.0.0",
+ "micromark-util-html-tag-name": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-destination": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+ "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-label": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+ "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-title": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+ "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-factory-whitespace": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+ "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-chunked": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+ "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-classify-character": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+ "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-combine-extensions": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+ "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+ "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-decode-string": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+ "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+ "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/mermaid/node_modules/micromark-util-html-tag-name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+ "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+ "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-resolve-all": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+ "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+ "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/micromark-util-subtokenize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+ "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/mermaid/node_modules/unist-util-stringify-position": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+ "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mermaid/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -14369,7 +15385,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -14497,6 +15512,11 @@
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
},
+ "node_modules/non-layered-tidy-tree-layout": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
+ "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
+ },
"node_modules/nopt": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
@@ -17141,6 +18161,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
+ },
"node_modules/rtl-detect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz",
@@ -17185,11 +18210,15 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+ },
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
- "dev": true,
"dependencies": {
"mri": "^1.1.0"
},
@@ -18164,6 +19193,11 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -18514,6 +19548,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ts-dedent": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
+ "engines": {
+ "node": ">=6.10"
+ }
+ },
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
@@ -19236,7 +20278,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
"integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
- "dev": true,
"dependencies": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
@@ -19254,7 +20295,6 @@
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -19481,6 +20521,11 @@
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz",
"integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ=="
},
+ "node_modules/web-worker": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz",
+ "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA=="
+ },
"node_modules/webpack": {
"version": "5.89.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
diff --git a/docs/package.json b/docs/package.json
index 81dba75b92..78a0817fef 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -17,6 +17,7 @@
"dependencies": {
"@docusaurus/core": "^3.0.0",
"@docusaurus/preset-classic": "^3.0.0",
+ "@docusaurus/theme-mermaid": "^3.0.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 1c718e518c..d4c398b462 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -135,6 +135,7 @@ module.exports = {
"development/tests",
"development/usb-logging",
"development/ide-integration",
+ "development/studio-rpc-protocol",
{
type: "category",
label: "Guides",
diff --git a/docs/src/docusaurus-tree-sitter-plugin/index.js b/docs/src/docusaurus-tree-sitter-plugin/index.js
index a6952ce7c5..e7f5596b36 100644
--- a/docs/src/docusaurus-tree-sitter-plugin/index.js
+++ b/docs/src/docusaurus-tree-sitter-plugin/index.js
@@ -6,6 +6,7 @@
module.exports = function () {
return {
+ name: "tree-sitter",
configureWebpack(config, isServer) {
let rules = [];