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 Byte Protocol Purpose 0x55Widget Protocol UI config, variable sync, reliability 0xAAFilesystem Protocol Bulk file operations (LittleFS) 0xBBOTA Protocol Over-the-air firmware updates 0xDDSettings Protocol Device config, auth, telemetry, features 0xEEPrint Protocol Transport-agnostic console output
[START][LENGTH_LO][LENGTH_HI][CMD][PAYLOAD...][CRC_LO][CRC_HI]
Field Size Description START1 Always 0x55 LENGTH2 Total packet length including all fields CMD1 Command byte PAYLOAD0-N Command-specific payload CRC2 CRC-16/CCITT-FALSE over CMD + PAYLOAD
Minimum packet size: 6 bytes (no payload).
Value Name Direction Description 0x01GET_CONFApp -> Arduino Request configuration descriptor 0x02CONF_DATAArduino -> App Configuration descriptor response 0x05ACKBoth Acknowledge reliable packet 0x06GET_VARSApp -> Arduino Request current variable state 0x07VAR_DATAArduino -> App Variable state response 0x08VAR_UPDATEBoth Reliable push of a single widget state 0x09GET_METAApp -> Arduino Request widget metadata 0x0AMETA_DATAArduino -> App Metadata response 0x0BMETA_UPDATEBoth Reliable push of widget metadata 0x0CSET_INPUTArduino -> App Set state of an input widget from Arduino 0x1DGET_WIFI_INFOApp -> Arduino Request WiFi status 0x1EWIFI_INFO_DATAArduino -> App WiFi status payload
Sent in response to GET_CONF. Contains widget configuration for the UI.
[ORIENT][WIDGET_COUNT][THEME_LEN][THEME...]
Field Type Description ORIENTuint8_t0x00 = Landscape, 0x01 = PortraitWIDGET_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...]
Field Type Description 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)
ID Widget Description 1 PushButton Momentary button 2 ToggleButton Latching button 3 Slider Linear slider (-100 to +100) 4 Joystick 2-axis joystick 5 LED Status indicator 6 Text Read-only text display 7 Multiple Button group / select group 8 SlideSwitch iOS-style toggle switch 9 Knob Rotary knob (-100 to +100) 10 Telemetry Display-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:
Type Widget Data Bytes Description 1 PushButton 1 byte Current state (0/1) 2 ToggleButton 1 byte Current state (0/1) 3 Slider 1 byte Value (Signed int8, -100 to +100) 4 Joystick 2 bytes X, Y values (Signed int8, -100 to +100) 5 LED 5 bytes State (1), R, G, B, Opacity 6 Text 32 bytes UTF-8 string (null-padded) 7 Multiple 1 byte Selected item(s) (Bitmask) 8 SlideSwitch 1 byte Current state (0/1) 9 Knob 1 byte Value (Signed int8, -100 to +100) 10 Telemetry 32 bytes UTF-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).
Field Type Description IDuint8_tWidget index VALUEvariable Type-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
Field Type Description IDuint8_tWidget index SEQuint8_tRolling sequence number (0-255) DATAvariable Type-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).
Field Type Description 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]
Field Type Description IP4 bytes IP address (4 octets) MODEuint8_t0x00 = STA, 0x01 = APSSID_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.
Value Name Payload Description 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.
Value Name Payload Description 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
Bit Field Description 0 NAME Device name string 1 DESC Device description string 2 DEVICE_PWD Device password (full access) 3 USER_PWD User password (widgets-only) 4 ICON Device icon name 7 ERROR Error flag (set in response on failure)
Bit Feature Description 0 OTA Over-the-air updates supported 1 FILESYSTEM LittleFS mounted and ready 2 HAS_DEVICE_PWD Device password is set 3 HAS_USER_PWD User password is set 4 WiFi WiFi transport active 5 CLOUD Cloud relay transport active 6 BLE BLE transport compiled in 7 PRINT_STREAM 0xEE print stream supported
Value Name Description 0x00DEVICE Authenticated as device (full access) 0x01USER Authenticated as user (widgets-only) 0x02DENIED Password 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
Value Name Payload Description 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
Value Name Payload Description 0x81OTA_ACK[STATUS(1)]Operation result 0x82OTA_PROGRESS[RECEIVED(4 LE)][TOTAL(4 LE)]Progress notification
Value Name Description 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
Value Description 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
Field Size Description START1 Always 0xAA SUB_CMD1 FS sub-command LEN2 (LE) Total frame length (header(4) + payload)PAYLOAD0..16380 Sub-command-specific payload CRC none No checksum - rely on transport reliability
Maximum payload size: 16 380 bytes (16 384 - 4-byte header).
Value Name Payload Description 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)
Value Name Payload Description 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
Value Name Meaning 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).
Role UUID Service 0000FFE0-0000-1000-8000-00805F9B34FBCharacteristic 0000FFE1-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
Version Changes v1.0 Initial protocol (GET_CONF, CONF_DATA only) v2.0 Added VAR_DATA, SET_INPUT v3.0 Added reliability (ACK, VAR_UPDATE), META_DATA, TELEMETRY v3.1 Added bulk Filesystem protocol (0xAA) v3.2 Added FS_PING and FS_FORMAT sub-commands v4.0 NVS-backed config, Settings protocol (0xDD), OTA protocol (0xBB) v4.2 Removed PING/PONG (transport-driven health), added Print protocol (0xEE) v5.0 Current version — widget type IDs reordered, CONF_DATA simplified (name/desc moved to Settings), STYLE/VARIANT bytes in widget descriptor