Rynk Protocol Reference
Current protocol version: 0.1.
Every transport (USB vendor bulk, BLE GATT, BLE HID) carries the same frame — a 3-byte header plus a postcard-encoded payload:
On the wire the whole frame is COBS-encoded and terminated by a single 0x00 delimiter, so the byte stream is self-synchronizing.
- Requests use CMD
0x0000..=0x7FFF. The response echoes CMD and SEQ and wraps its payload in postcardResult<T, RynkError>(T = ()forSet*). - Topics use CMD
0x8000..=0xFFFF(server → host push, SEQ0, bare payload).
Which commands a firmware answers depends on the RMK Cargo features it was built with: a row with no Feature is present once rynk is on, and the rest need their feature (_ble, split, …) compiled in. A command the firmware wasn't built with answers UnknownCmd.
Transports
The same COBS-framed byte stream runs over every transport; only how a host finds and opens the link differs.
A dongle relays these frames untouched, so a host talks to the dongle's USB interface exactly as it would to the keyboard.
Sizing and bulk transfer
Each peer holds one frame in a buffer of rynk_buffer_size bytes (a [rmk] option, see RMK config). The largest payload a frame can carry is what remains after COBS overhead, the delimiter, and the 3-byte header; the firmware reports it as DeviceCapabilities.max_payload_size. Read the capabilities and size requests from them rather than assuming a fixed limit.
DeviceCapabilities also advertises bulk_transfer_supported and the paging strides max_bulk_keys (worst-case keys per GetKeymapBulk page) and max_bulk_items (worst-case entries per GetComboBulk/GetMorseBulk page). A bulk read names a start — for the keymap (layer, row, col), read forward through the flat row-major, layer-major keymap; for combos and morses a slot index — and returns as many consecutive entries as fit in one payload, or fewer at the end. A host pages by advancing its start by the stride; a short page ends the read. A bulk write carries a start plus a list of entries and is packed by encoded size up to max_payload_size. A reply that does not fit beside other pipelined requests answers Busy; retry once they complete.
GetLayout serves the compressed layout blob 244 bytes per call: the request is a byte offset and LayoutChunk carries total_len plus that page's bytes. Macros move in macro_chunk_size pieces (protocol_macro_chunk_size in [rmk]) addressed by byte offset.
Errors
A request's response is postcard Result<T, RynkError>; the Err side is one of these variants.
Lock
Commands that can flash firmware, wipe storage, or read the matrix sit behind a physical-presence unlock. BootloaderJump, StorageReset, GetMatrixState, and (with _ble) ClearBleProfile always need an unlocked session; every Set* command joins them when the firmware was built with [host] write_requires_unlock = true. A gated command on a locked session answers Locked and does nothing. GetLockStatus, UnlockPoll, and Lock are never gated.
The lock is per session and starts locked; Lock or the end of the session (unplug, BLE disconnect) relocks it. To unlock, a host polls UnlockPoll while the user holds the challenge keys that LockStatus.key_positions reports ([host].unlock_keys); the session is unlocked once locked clears. With no unlock_keys configured the challenge is empty and the gated commands can never be unlocked; a firmware built with [host] insecure = true starts unlocked and ignores Lock. See Rynk for the user-facing side.
Endpoints
Topics
Topics are best-effort pushes; the Get* endpoints above mirror their payloads so a host can recover a missed push.