This repository contains the message definitions used to interact with ZMK Studio enabled devices.
Note This is a fork of zmkfirmware/zmk-studio-messages. In addition to the upstream messages, it adds an unofficial custom subsystem protocol that lets ZMK modules expose their own RPC endpoints and companion UIs to ZMK Studio. See Custom subsystem protocol below.
The messages are defined as Protocol Buffers (proto3) under
proto/zmk/. Each file maps to a subsystem of the RPC surface exposed by a device:
| File | Package | Description |
|---|---|---|
studio.proto |
zmk.studio |
Top-level envelope that multiplexes all subsystems |
meta.proto |
zmk.meta |
Framing / error metadata |
core.proto |
zmk.core |
Device info, lock state, resets |
behaviors.proto |
zmk.behaviors |
Behavior (binding) metadata |
keymap.proto |
zmk.keymap |
Keymap read/write |
custom.proto |
zmk.custom |
Custom subsystem protocol (this fork) |
The *.options.in files carry nanopb options (max sizes, etc.)
that are expanded from Kconfig values at firmware build time.
Every request/response is wrapped in the top-level envelope defined in
studio.proto. A subsystem is selected via the subsystem oneof,
so each subsystem defines its own Request, Response, and Notification messages.
Upstream ZMK Studio only knows about the built-in subsystems (core, behaviors, keymap).
This fork adds a generic zmk.custom subsystem so that arbitrary ZMK modules can define and
expose their own protocol over the same ZMK Studio transport — without having to patch the core
message set for every feature.
The custom subsystem is attached to the top-level envelope using field number 100 (well outside
the range used by upstream subsystems, to avoid collisions when rebasing on upstream):
// studio.proto
message Request {
uint32 request_id = 1;
oneof subsystem {
zmk.core.Request core = 3;
zmk.behaviors.Request behaviors = 4;
zmk.keymap.Request keymap = 5;
zmk.custom.Request custom = 100; // added by this fork
}
}
// ...RequestResponse and Notification gain `zmk.custom` at field 100 as well.Defined in custom.proto:
-
ListCustomSubsystemRequest/ListCustomSubsystemResponse— discovery. A client asks the device which custom subsystems are available. The response is a list ofCustomSubsystemInfo. -
CustomSubsystemInfo— describes one custom subsystem:index— a device-specific numeric handle used to address the subsystem in later calls. It is not stable: it may change on every firmware compile and potentially across reboots, so clients must resolve it via discovery rather than hard-coding it.identifier— a stable, unique string identifier for the subsystem (this is what a client matches against).ui_url— zero or more URLs pointing to web UIs that know how to talk to this subsystem.
-
CallRequest/CallResponse— the actual RPC. Both carry asubsystem_index(matchingCustomSubsystemInfo.index) and an opaquepayload(bytes). The custom subsystem protocol is intentionally transport-only: the meaning ofpayloadis defined entirely by the target subsystem, not by this schema. This lets a module ship its own encoding (its own protobuf, CBOR, raw bytes, …) and evolve it independently. -
CustomNotification— device-initiated (unsolicited) message from a custom subsystem to the client, again addressed bysubsystem_indexwith an opaquepayload.
- Client sends
custom.ListCustomSubsystemRequest. - Device replies with
ListCustomSubsystemResponselisting eachCustomSubsystemInfo(index,identifier,ui_url). - Client matches the subsystem it cares about by
identifier, remembers its currentindex, and (optionally) opens one of theui_urls to drive it. - Client exchanges module-defined payloads via
CallRequest/CallResponseon thatindex. - The device may push
CustomNotifications at any time for thatindex.
custom.options.in bounds the wire sizes via Kconfig-expanded
nanopb options:
CustomSubsystemInfo.identifier→CONFIG_ZMK_STUDIO_RPC_CUSTOM_SUBSYSTEM_IDENTIFIER_MAX_LENCallRequest.payload→CONFIG_ZMK_STUDIO_RPC_CUSTOM_SUBSYSTEM_REQUEST_PAYLOAD_MAX_BYTES
- Document transport protocol used with these messages
- Release/versioning strategy