Skip to content

Commit

Permalink
feat(studio): Initial RPC infrastructure and subsystems.
Browse files Browse the repository at this point in the history
* UART and BLE/GATT transports for a protobuf encoded RPC
  request/response protocol.
* Custom framing protocol is used to frame a give message.
* Requests/responses are divided into major "subsystems" which
  handle requests and create response messages.
* Notification support, including mapping local events to RPC
  notifications by a given subsystem.
* Meta responses for "no response" and "unlock needed".
* Initial basic lock state support in a new core section, and allow specifying
  if a given RPC callback requires unlocked state or not.
* Add behavior subsystem with full metadata support and examples of
  using callback to serialize a repeated field without extra stack space needed.
  • Loading branch information
petejohanson committed Jul 5, 2024
1 parent 8c6bda2 commit a8052d6
Show file tree
Hide file tree
Showing 27 changed files with 3,744 additions and 1,021 deletions.
24 changes: 24 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,28 @@ target_sources(app PRIVATE src/main.c)
add_subdirectory(src/display/)
add_subdirectory_ifdef(CONFIG_SETTINGS src/settings/)

if (CONFIG_ZMK_STUDIO)
# 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)

message(${ZEPHYR_ZMK_STUDIO_MESSAGES_MODULE_DIR})
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)
2 changes: 2 additions & 0 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ rsource "src/split/Kconfig"
#Basic Keyboard Setup
endmenu

rsource "src/studio/Kconfig"

menu "Display/LED Options"

rsource "src/display/Kconfig"
Expand Down
9 changes: 9 additions & 0 deletions app/include/linker/zmk-rpc-event-mappers.ld
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions app/include/linker/zmk-rpc-subsystem-handlers.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) 2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/linker/linker-defs.h>

ITERABLE_SECTION_ROM(zmk_rpc_subsystem_handler, 4)
9 changes: 9 additions & 0 deletions app/include/linker/zmk-rpc-subsystems.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (c) 2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/linker/linker-defs.h>

ITERABLE_SECTION_RAM(zmk_rpc_subsystem, 4)
9 changes: 9 additions & 0 deletions app/include/linker/zmk-rpc-transport.ld
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 28 additions & 0 deletions app/include/zmk/studio/core.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

#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_UNLOCKING = 1,
ZMK_STUDIO_CORE_LOCK_STATE_UNLOCKED = 2,
};

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);
ZMK_EVENT_DECLARE(zmk_studio_core_unlock_requested);

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();
153 changes: 153 additions & 0 deletions app/include/zmk/studio/rpc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@

#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>

struct zmk_studio_rpc_notification {
zmk_Notification notification;
};

ZMK_EVENT_DECLARE(zmk_studio_rpc_notification);

struct zmk_rpc_subsystem;

typedef zmk_Response(subsystem_func)(const struct zmk_rpc_subsystem *subsys,
const zmk_Request *req);

typedef zmk_Response(rpc_func)(const zmk_Request *neq);

struct zmk_rpc_subsystem {
subsystem_func *func;
uint16_t handlers_start_index;
uint16_t handlers_end_index;
uint8_t subsystem_choice;
};

struct zmk_rpc_subsystem_handler {
rpc_func *func;
uint8_t subsystem_choice;
uint8_t request_choice;
bool secured;
};

#define ZMK_RPC_NO_RESPONSE() ZMK_RPC_RESPONSE(meta, no_response, true)
#define ZMK_RPC_SIMPLE_ERR(type) \
ZMK_RPC_RESPONSE(meta, simple_error, zmk_meta_ErrorConditions_##type)

#define ZMK_RPC_SUBSYSTEM(prefix) \
zmk_Response subsystem_func_##prefix(const struct zmk_rpc_subsystem *subsys, \
const zmk_Request *req) { \
uint8_t which_req = req->subsystem.prefix.which_request_type; \
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->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); \
} \
STRUCT_SECTION_ITERABLE(zmk_rpc_subsystem, prefix##_subsystem) = { \
.func = subsystem_func_##prefix, \
.subsystem_choice = zmk_Request_##prefix##_tag, \
};

#define ZMK_RPC_SUBSYSTEM_HANDLER(prefix, request_id, _secured) \
STRUCT_SECTION_ITERABLE(zmk_rpc_subsystem_handler, \
prefix##_subsystem_handler_##request_id) = { \
.func = request_id, \
.subsystem_choice = zmk_Request_##prefix##_tag, \
.request_choice = zmk_##prefix##_Request_##request_id##_tag, \
.secured = _secured, \
};

#define ZMK_RPC_NOTIFICATION(subsys, _type, ...) \
((zmk_Notification){ \
.which_subsystem = zmk_Notification_##subsys##_tag, \
.subsystem = \
{ \
.subsys = \
{ \
.which_notification_type = zmk_##subsys##_Notification_##_type##_tag, \
.notification_type = {._type = __VA_ARGS__}, \
}, \
}, \
})

#define ZMK_RPC_RESPONSE(subsys, _type, ...) \
((zmk_Response){ \
.which_type = zmk_Response_request_response_tag, \
.type = \
{ \
.request_response = \
{ \
.which_subsystem = zmk_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_Notification *n);

struct zmk_rpc_event_mapper {
zmk_rpc_event_mapper_cb *func;
};

#define ZMK_RPC_EVENT_MAPPER_ADD_LISTENER(_t) ZMK_SUBSCRIPTION(studio_rpc, _t)

#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;
};

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, \
}
15 changes: 15 additions & 0 deletions app/src/studio/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
86 changes: 86 additions & 0 deletions app/src/studio/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Copyright (c) 2024 The ZMK Contributors
# SPDX-License-Identifier: MIT

menuconfig ZMK_STUDIO
bool "Studio Support"
select NANOPB
# These two save stack size
imply NANOPB_NO_ERRMSG
imply NANOPB_WITHOUT_64BIT
select ZMK_BEHAVIOR_METADATA
select ZMK_BEHAVIOR_LOCAL_IDS
select RING_BUFFER
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"
default y

if ZMK_STUDIO_LOCKING

config ZMK_STUDIO_LOCK_IDLE_TIMEOUT_SEC
int "Idle Timeout"
default 500

config ZMK_STUDIO_LOCK_ON_DISCONNECT
bool "Lock On Disconnect"
default y

endif

menu "Remote Procedure Calls (RPC)"

menu "Transports"

config ZMK_STUDIO_TRANSPORT_UART
bool "Serial"
select SERIAL
select RING_BUFFER
default y if ZMK_USB

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 "RX Buffer Size"
default 64

endmenu

endif
Loading

0 comments on commit a8052d6

Please sign in to comment.