Skip to content

Protocol Specification

RadioKit uses a compact binary protocol which is transport-agnostic (works for BLE, Serial, WiFi, and Cloud). The protocol is designed for low-latency, reliable control with minimal overhead.

RadioKit runs five parallel protocols on the same physical transport, demuxed by start byte:

Start ByteProtocolPurpose
0x55Widget ProtocolUI config, variable sync, reliability
0xAAFilesystem ProtocolBulk file operations (LittleFS)
0xBBOTA ProtocolOver-the-air firmware updates
0xDDSettings ProtocolDevice config, auth, telemetry, features
0xEEPrint ProtocolTransport-agnostic console output

[START][LENGTH_LO][LENGTH_HI][CMD][PAYLOAD...][CRC_LO][CRC_HI]
0x55 total total
FieldSizeDescription
START1Always 0x55
LENGTH2Total packet length including all fields
CMD1Command byte
PAYLOAD0-NCommand-specific payload
CRC2CRC-16/CCITT-FALSE over CMD + PAYLOAD

Minimum packet size: 6 bytes (no payload).

ValueNameDirectionDescription
0x01GET_CONFApp -> ArduinoRequest configuration descriptor
0x02CONF_DATAArduino -> AppConfiguration descriptor response
0x05ACKBothAcknowledge reliable packet
0x06GET_VARSApp -> ArduinoRequest current variable state
0x07VAR_DATAArduino -> AppVariable state response
0x08VAR_UPDATEBothReliable push of a single widget state
0x09GET_METAApp -> ArduinoRequest widget metadata
0x0AMETA_DATAArduino -> AppMetadata response
0x0BMETA_UPDATEBothReliable push of widget metadata
0x0CSET_INPUTArduino -> AppSet state of an input widget from Arduino
0x1DGET_WIFI_INFOApp -> ArduinoRequest WiFi status
0x1EWIFI_INFO_DATAArduino -> AppWiFi status payload

Sent in response to GET_CONF. Contains widget configuration for the UI.

[ORIENT][WIDGET_COUNT][THEME_LEN][THEME...]
FieldTypeDescription
ORIENTuint8_t0x00 = Landscape, 0x01 = Portrait
WIDGET_COUNTuint8_tNumber of widget descriptors that follow
THEME_LENuint8_tLength of theme identifier string
THEMEchar[N]Theme name (e.g., "default", "dark", "retro")

Note: Device name, description, and firmware version are no longer in CONF_DATA. They are retrieved via the Settings protocol’s GET_DEVICE_INFO command (0x08).

Each widget is described by a fixed header followed by optional string data.

[TYPE][ID][X][Y][WIDTH][HEIGHT][ROT_LO][ROT_HI][STYLE][VARIANT][STR_MASK][STR_DATA...]
FieldTypeDescription
TYPEuint8_tWidget type ID (see table below)
IDuint8_tWidget index (0-based, sequential)
Xuint8_tCenter X on virtual canvas (0-200)
Yuint8_tCenter Y on virtual canvas (0-200)
WIDTHuint8_tPhysical width in virtual units (0 = auto-aspect)
HEIGHTuint8_tPhysical height in virtual units (0-200)
ROTATIONint16_tRotation in degrees (clockwise)
STYLEuint8_tWidget style (0=primary, 1=dim, 2=success, 3=warning, 4=danger)
VARIANTuint8_tBehavioral variation (e.g., spring mode, detent count)
STR_MASKuint8_tString bitmask (determines following string segments)
IDWidgetDescription
1PushButtonMomentary button
2ToggleButtonLatching button
3SliderLinear slider (-100 to +100)
4Joystick2-axis joystick
5LEDStatus indicator
6TextRead-only text display
7MultipleButton group / select group
8SlideSwitchiOS-style toggle switch
9KnobRotary knob (-100 to +100)
10TelemetryDisplay-only metrics

Bits indicate which optional strings are included. Each active bit adds a [LEN][STR] pair to the STR_DATA block.

  • Bit 0 (0x01): Label (primary display text)
  • Bit 1 (0x02): Icon (standard name string)
  • Bit 2 (0x04): OnText (for buttons)
  • Bit 3 (0x08): OffText (for buttons)
  • Bit 4 (0x10): Content (for Text widget initial value)
  • Bit 5 (0x20): Extra (widget-specific binary configuration)
  • Bit 6 (0x40): Label hidden (label is present but should be hidden)

Each string is encoded as: [LENGTH (1 byte)][UTF-8 DATA]


Sent in response to GET_VARS. Contains the current state of all widgets in ID order.

[DATA_W0][DATA_W1][DATA_W2]...

Each widget’s data is encoded according to its type:

TypeWidgetData BytesDescription
1PushButton1 byteCurrent state (0/1)
2ToggleButton1 byteCurrent state (0/1)
3Slider1 byteValue (Signed int8, -100 to +100)
4Joystick2 bytesX, Y values (Signed int8, -100 to +100)
5LED5 bytesState (1), R, G, B, Opacity
6Text32 bytesUTF-8 string (null-padded)
7Multiple1 byteSelected item(s) (Bitmask)
8SlideSwitch1 byteCurrent state (0/1)
9Knob1 byteValue (Signed int8, -100 to +100)
10Telemetry32 bytesUTF-8 string (null-padded)

Sent by the Arduino to programmatically set the state of an input widget in the app (e.g., resetting a slider or toggle).

[ID][VALUE...]
FieldTypeDescription
IDuint8_tWidget index
VALUEvariableType-specific value (see VAR_DATA table)

The app updates the widget UI immediately. Unlike VAR_UPDATE, this is not acknowledged.

Pushes a change for a single widget. Can be sent by either side:

  • App -> Arduino: When user changes an input.
  • Arduino -> App: When firmware programmatically changes a widget state
[ID][SEQ][DATA...]
FieldTypeDescription
IDuint8_tWidget index
SEQuint8_tRolling sequence number (0-255)
DATAvariableType-specific value (see VAR_DATA table)
  • The sender maintains a pending bitmask (32-bit) for unacknowledged updates
  • Retransmission timeout: 500 ms
  • Maximum retries: 5 attempts
  • After 5 failures, the packet is dropped (fail-soft)
  • The receiver sends ACK immediately upon receipt

Confirms receipt of a reliable packet (VAR_UPDATE or META_UPDATE).

[SEQ]
FieldTypeDescription
SEQuint8_tSequence number being acknowledged

Same format as VAR_DATA/VAR_UPDATE but for widget metadata (label, icon, etc.). Used when firmware changes widget appearance at runtime.

WiFi status response sent in response to GET_WIFI_INFO.

[IP0][IP1][IP2][IP3][MODE][SSID_LEN][SSID...][RSSI]
FieldTypeDescription
IP4 bytesIP address (4 octets)
MODEuint8_t0x00 = STA, 0x01 = AP
SSID_LENuint8_tLength of STA SSID
SSIDchar[N]Connected STA SSID
RSSIint8_tWiFi signal strength (dBm)

A dedicated protocol for device settings, authentication, telemetry, and configuration management. Uses a separate start byte (0xDD) and parallel state machine.

[START][SUB_CMD][LEN_LO][LEN_HI][PAYLOAD...]
0xDD uint8 uint8 uint8 0..1024 bytes

No CRC — transport reliability is assumed.

ValueNamePayloadDescription
0x01GET_TELEMETRY(empty)Request signal/battery status
0x02BLE_INFO(empty)Request BLE connection details
0x03GET_FEATURES(empty)Request feature bitmask
0x04GET_CHIP_INFO(empty)Request chip model, flash, PSRAM
0x05SET_CONF[MASK(2 LE)][DATA...]Update device config fields
0x06PWD_AUTH[PWD_LEN(1)][PWD(N)]Authenticate with password
0x07FACTORY_RESET(empty)Erase NVS and reboot
0x08GET_DEVICE_INFO(empty)Request name, description, UID, icon
0x09NVS_RAW_READ[KEY_LEN(1)][KEY(N)]Read raw NVS key
0x0ANVS_RAW_WRITE[KEY_LEN(1)][KEY(N)][VALUE(1)]Write raw NVS key
0x0BSET_WIFI[MASK(2 LE)][DATA...]Update WiFi credentials
0x0CGET_CLOUD_INFO(empty)Request cloud relay config
0x0DREBOOT(empty)Reboot device (NVS preserved)
0x0ESET_CLOUD_INFO[MASK(2 LE)][DATA...]Update cloud relay config

Response = subCmd | 0x80.

ValueNamePayloadDescription
0x81TELEMETRY_DATA[RSSI][LATENCY][TS_LO][TS_HI]Telemetry values
0x82BLE_INFO_DATA[INTERVAL(2 LE)][MTU(2 LE)][RSSI]BLE connection details
0x83FEATURES_DATA[BITMASK(1)]Feature capabilities
0x84CHIP_INFO_DATA[MODEL...][REV][CORES][FLASH(4)][PSRAM(4)][SDK...][MAC(6)]Chip info
0x85SET_CONF_ACK[STATUS(1)]Config update result
0x86PWD_AUTH_ACK[STATUS(1)]Auth result (0=OK, 1=user, 2=denied)
0x87FACTORY_RESET_ACK[STATUS(1)]Factory reset confirm
0x88DEVICE_INFO_DATA[PROTO_VER][NAME...][DESC...][UID...][ICON...]Device info
0x89NVS_RAW_READ_DATA[STATUS][LEN][VALUE...]NVS read result
0x8ANVS_RAW_WRITE_ACK[STATUS(1)]NVS write result
0x8BSET_WIFI_ACK[STATUS(1)]WiFi update result
0x8CCLOUD_INFO_DATA[URL_LEN][URL...][ACC_LEN][ACC...]Cloud config
0x8DREBOOT_ACK[STATUS(1)]Reboot confirm
0x8ESET_CLOUD_INFO_ACK[STATUS(1)]Cloud config result
BitFieldDescription
0NAMEDevice name string
1DESCDevice description string
2DEVICE_PWDDevice password (full access)
3USER_PWDUser password (widgets-only)
4ICONDevice icon name
7ERRORError flag (set in response on failure)
BitFeatureDescription
0OTAOver-the-air updates supported
1FILESYSTEMLittleFS mounted and ready
2HAS_DEVICE_PWDDevice password is set
3HAS_USER_PWDUser password is set
4WiFiWiFi transport active
5CLOUDCloud relay transport active
6BLEBLE transport compiled in
7PRINT_STREAM0xEE print stream supported
ValueNameDescription
0x00DEVICEAuthenticated as device (full access)
0x01USERAuthenticated as user (widgets-only)
0x02DENIEDPassword did not match
  • Unauthenticated: Only GET_CONF, PWD_AUTH, GET_FEATURES, GET_DEVICE_INFO are allowed
  • User-authenticated: Widget commands + most Settings commands, but cannot change device config, factory reset, or reboot
  • Device-authenticated: Full access to all commands
  • Serial bypass: Physical serial access implies full device-level authentication

Over-the-air firmware updates using ESP32’s Update library.

[START][SUB_CMD][LEN_LO][LEN_HI][PAYLOAD...]
0xBB uint8 uint8 uint8 0..N bytes
ValueNamePayloadDescription
0x01OTA_BEGIN[SIZE(4 LE)]Begin firmware update
0x02OTA_CHUNK[OFFSET(4 LE)][DATA...]Write firmware chunk
0x03OTA_END[CRC32(4 LE)]Finalize and verify
0x04OTA_ABORT(empty)Abort current update
0x05OTA_SET_ERASE_FLAG[MODE(1)]Set erase flag for next boot
ValueNamePayloadDescription
0x81OTA_ACK[STATUS(1)]Operation result
0x82OTA_PROGRESS[RECEIVED(4 LE)][TOTAL(4 LE)]Progress notification
ValueNameDescription
0x00OKSuccess
0x01NO_SPACENot enough flash space for firmware
0x02FLASHFlash write error
0x03SEQOffset mismatch (chunk out of order)
0x04INVALID_STATEOTA not started or already finished
0x05NOT_SUPPORTEDOTA not available on this platform
ValueDescription
0x00No erase
0x01Erase NVS only
0x02Erase FS only
0x03Erase both NVS and FS

The erase flag is written to NVS (which survives OTA updates since it’s a separate flash partition). On the next boot, begin() reads the flag and performs the requested erase before continuing.


Transport-agnostic console output. Messages sent via RadioKit.print() / RadioKit.println() are framed as 0xEE packets and delivered to the app’s console view.

[START][LEN_LO][LEN_HI][DATA...]
0xEE uint16 uint16 0..N bytes (UTF-8 text)
  • Unidirectional: Device -> App only
  • Buffered: Print data is buffered in a 1024-byte circular buffer and flushed on newlines or when the buffer is near-full
  • Flush on connect: Boot-time messages are preserved and sent when a client first connects

A separate, higher-bandwidth protocol for filesystem operations (browse, read, write, delete, mkdir, rename, info, ping, format). It uses a different start byte so it can coexist with the widget protocol (0x55) without interfering.

[START][SUB_CMD][LEN_LO][LEN_HI][PAYLOAD...]
0xAA uint8 uint8 uint8 0..16380 bytes
FieldSizeDescription
START1Always 0xAA
SUB_CMD1FS sub-command
LEN2 (LE)Total frame length (header(4) + payload)
PAYLOAD0..16380Sub-command-specific payload
CRCnoneNo checksum - rely on transport reliability

Maximum payload size: 16 380 bytes (16 384 - 4-byte header).

ValueNamePayloadDescription
0x01FS_LIST[u8 path_len][path...]List entries in a directory
0x02FS_READ[u8 path_len][path...][u32 offset][u16 max_bytes]Read up to 65 535 bytes from offset
0x03FS_WRITE[u8 path_len][path...][u32 offset][bytes...]Write a file chunk
0x04FS_DELETE[u8 path_len][path...][u8 recursive]Delete a file or directory
0x05FS_INFO(empty)Get FS usage / free space
0x06FS_MKDIR[u8 path_len][path...]Create a directory
0x07FS_RENAME[u8 old_len][old...][u8 new_len][new...]Rename / move
0x08FS_UPLOAD_BEGIN[u8 path_len][path...][u32 size]Begin a multi-frame write
0x09FS_UPLOAD_CHUNK[u8 path_len][path...][u32 offset][bytes...]Write a chunk of an upload
0x0AFS_UPLOAD_END[u8 path_len][path...][u32 size]Finalize a multi-frame write
0x0BFS_PING(empty)Capability probe
0x0CFS_FORMAT(empty)Re-format the default FS (destructive)
ValueNamePayloadDescription
0x81FS_LIST_DATA[u16 count] then per entry [u8 is_dir][u32 size][u8 name_len][name...]Directory listing result
0x82FS_READ_DATA[u32 total_size][u32 offset][bytes...]File chunk
0x83FS_WRITE_ACK[u8 err]Write ack
0x84FS_DELETE_ACK[u8 err]Delete ack
0x85FS_INFO_DATA[u32 total][u32 used][u16 block_size][u8 fs_type]FS usage / metadata
0x86FS_MKDIR_ACK[u8 err]Mkdir ack
0x87FS_RENAME_ACK[u8 err]Rename ack
0x88FS_UPLOAD_BEGIN_ACK[u8 err]Upload begin ack
0x89FS_UPLOAD_CHUNK_ACK[u8 err]Upload chunk ack
0x8AFS_UPLOAD_END_ACK[u8 err]Upload end ack
0x8BFS_PING_ACK[u8 status]Mount status
0x8CFS_FORMAT_ACK[u8 err]Format ack
ValueNameMeaning
0x00OKSuccess
0x01NOT_FOUNDFile or directory does not exist
0x02IORead/write error
0x03NO_FSLittleFS not compiled in or not mounted
0x04ACCESS_DENIEDPermission denied (requires device-level auth)
0x05INVALID_PATHMalformed path or argument
0x06OUT_OF_SPACEFilesystem full
0x07INVALID_STATEOperation not valid in current state
  • The FS protocol is optional. Sketches that don’t use it can simply not call RadioKit.beginFs().
  • The four state machines (0x55 widget, 0xAA FS, 0xBB OTA, 0xDD Settings) are fed from the same byte stream and demuxed by the start byte.
  • FS_PING is the recommended way to detect FS support on a freshly-connected device.
  • FS operations require device-level authentication (or serial bypass).

RoleUUID
Service0000FFE0-0000-1000-8000-00805F9B34FB
Characteristic0000FFE1-0000-1000-8000-00805F9B34FB
  • Properties: Write (no response), Notify, Indicate
  • MTU: 23 bytes (default), negotiated up to 517
  • Packet fragmentation: Handled by NimBLE for packets > MTU
  • Advertising name: Auto-prefixed with RK_ for app filtering
  • Connection interval: Reported via BLE_INFO settings command
  • Baud rate: 1000000 (recommended), any speed supported
  • Connection timeout: 3000 ms after last valid packet
  • Hardware flow control: Optional (RTS/CTS)
  • Serial bypass: Physical serial access implies full device-level authentication
  • Port: 5555
  • Modes: AP mode (open network RK_<name>) or STA mode (connects to configured network)
  • Credentials: Stored in NVS, configurable via SET_WIFI settings command
  • Build flag: -D RADIOKIT_ENABLE_WIFI
  • Connection: Outbound WSS connection to configured relay server
  • Authentication: Ed25519 challenge-response (public key as account identifier)
  • Requires: WiFi transport to be active first
  • Build flag: -D RADIOKIT_ENABLE_WIFI

  • VAR_UPDATE retry: 500 ms interval, 5 max retries
  • Connection timeout: 3000 ms (serial), transport-driven (BLE/WiFi)
  • BLE connection interval: Reported via BLE_INFO command
  • Settings frame timeout: No explicit timeout — transport-driven

  • CRC mismatch: Packet silently discarded
  • Invalid START byte: Stream resynchronized on next valid start byte
  • Unknown CMD: Packet ignored (future compatibility)
  • Buffer overflow: Packet truncated, connection reset
  • Reliable packet timeout: Retransmit up to 5 times, then drop
  • Auth rejection: Returns ACK with error code, command ignored

VersionChanges
v1.0Initial protocol (GET_CONF, CONF_DATA only)
v2.0Added VAR_DATA, SET_INPUT
v3.0Added reliability (ACK, VAR_UPDATE), META_DATA, TELEMETRY
v3.1Added bulk Filesystem protocol (0xAA)
v3.2Added FS_PING and FS_FORMAT sub-commands
v4.0NVS-backed config, Settings protocol (0xDD), OTA protocol (0xBB)
v4.2Removed PING/PONG (transport-driven health), added Print protocol (0xEE)
v5.0Current version — widget type IDs reordered, CONF_DATA simplified (name/desc moved to Settings), STYLE/VARIANT bytes in widget descriptor