Documentation version: dev
OpenZilo BLE protocol reference¶
Device applicability
This document applies to OpenZilo Q.
Connection requirements
Your computer needs Bluetooth Low Energy (BLE). The SDK communicates with the ring through NUS and supports protocol v4.
1. Scope¶
This reference describes protocol v4 framing, commands and fields used by the SDK. Firmware may implement only a subset; query features to check. Button and LED behavior is covered by the user guide.
See the API index for calls and architecture for transport and audio handling.
2. BLE services¶
The protocol uses Nordic UART Service (NUS):
| Purpose | UUID | Direction |
|---|---|---|
| Service | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
Service identifier. |
| TX Characteristic | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E |
Ring notifies SDK. |
| RX Characteristic | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E |
SDK writes to ring. |
Connect by the scanned address (UUID on macOS), without depending on advertising names.
SDK RX writes use chunks of up to 20 bytes; the last may be shorter. This is a BLE write policy, not a packet boundary or a replacement for system MTU negotiation. TX notifications can use other sizes; reconstruct them with OpenZiloClient or PacketStream.
3. Protocol v4 packets¶
The fixed header is 11 bytes:
| Offset | Field | Type | Bytes | Description |
|---|---|---|---|---|
| 0 | magic_number |
u8 |
1 | Always 0x3F. |
| 1 | version |
u16 |
2 | Currently 4. |
| 3 | command |
u16 |
2 | Command number. |
| 5 | body_length |
u32 |
4 | Body size in bytes. |
| 9 | body_crc |
u16 |
2 | Body CRC16; 0 for an empty body. |
| 11 | body |
bytes |
variable | Command body. |
Header and body integers are network byte order (big-endian). Strings and raw bytes are not reordered. CRC covers only the body, using SDK crc16_compute() with initial value 0xFFFF.
A packet can span notifications and a notification can contain multiple packets. Reconstruct using the fixed header and body_length before parsing. OpenZiloClient and PacketStream do this already.
4. Commands¶
| Function | Request/event | Response | SDK API |
|---|---|---|---|
| System information | 0x0101 |
0x0102 |
get_system_info() |
| Feature discovery | 0x0105 |
0x0106 |
get_device_capabilities() |
| Command error | 0x01FF device response |
None | DeviceError |
| Log storage information | 0x0301 |
0x0302 |
get_log_storage() |
| Log data | 0x0303 |
0x0304 |
read_log_chunk() |
| Time synchronization | Device requests 0x0401 |
SDK replies 0x0402 |
enable_time_sync(), send_time_response() |
| Recording count | 0x0501 |
0x0502 |
get_audio_file_count() |
| Normal recording extraction | 0x0503 |
0x0504 |
get_audio_file_info() |
| Recording data | 0x0506 |
0x0505 |
read_audio_frame() |
| End normal extraction | 0x0507 |
0x0508 |
end_audio_extract() |
| Fast recording extraction | 0x0509 |
0x0504, consecutive 0x0505 |
download_audio_file(..., quick=True) |
V2.001.0002.0000 post-save push |
Consecutive device 0x0505 |
No initial 0x0504; metadata queried for recovery |
receive_auto_audio_file() |
| Recording saved | Device 0x050A |
None | wait_audio_record_result(), receive_auto_audio_file() |
| Live audio subscription | 0x050D |
0x050E |
set_live_audio_subscription() |
| Live audio data | Device 0x050F, 0x0510, 0x0511 |
None | receive_live_audio_file() |
| Delete all recordings | 0x050B |
0x050C |
clear_audio_files() |
| Start recording | 0x0512 |
0x0513 |
start_audio_recording() |
| Stop recording | 0x0514 |
0x0515 |
stop_audio_recording() |
| Recording status | 0x0516 |
0x0517 |
get_audio_recording_status() |
| Read recording settings | 0x0518 |
0x0519 |
get_audio_recording_config() |
| Write recording settings | 0x051A |
0x051B |
set_audio_recording_config() |
| Start IMU reports | 0x0601 |
0x0602 |
start_sensor_report() |
| Stop IMU reports | 0x0603 |
0x0604 |
stop_sensor_report() |
| IMU batches | Device 0x0605 |
None | wait_sensor_data() |
| Physical double tap | Device 0x0701 |
None | wait_sensor_double_tap_event() |
| Double button press | Device 0x0703 |
None | wait_sensor_key_double_press_event() |
| Single button press | Device 0x0704 |
None | wait_sensor_key_single_press_event() |
5. Message bodies¶
All u16, i16 and u32 below are big-endian. A nonzero response error_code makes subsequent fields invalid; a response may contain only that error. Some recording failure paths return no response, so timeout alone does not identify the cause.
5.1 System information¶
0x0101 Get system information¶
Empty body.
0x0102 System information response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
firmware_version_len |
u16 |
2 | Firmware version string length. |
firmware_version |
UTF-8 | variable | Firmware version string. |
system_time |
u32 |
4 | Unix timestamp in seconds. |
audio_storage_total |
u32 |
4 | Total recording storage in bytes. |
audio_storage_available |
u32 |
4 | Available recording storage in bytes. |
battery_percent |
u16 |
2 | Battery percentage. |
battery_charging |
u8 |
1 | 0 not charging, 1 charging. |
sn_len |
u16 |
2 | Serial number string length. |
sn |
UTF-8 | variable | Device serial number. |
cpuid_len |
u16 |
2 | CPUID string length. |
cpuid |
UTF-8 | variable | Device CPUID. |
model_len |
u16 |
2 | Model string length. |
model |
UTF-8 | variable | Device model. |
5.2 Logs¶
0x0301 Get log storage¶
Empty body.
0x0302 Log storage response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
page_size |
u32 |
4 | Log page size or the device's current paging unit. |
total_len |
u32 |
4 | Total log data length. |
0x0303 Read log data¶
| Field | Type | Bytes | Description |
|---|---|---|---|
index |
u32 |
4 | Log index. |
offset |
u32 |
4 | Starting offset. |
size |
u32 |
4 | Requested byte count. |
0x0304 Log data response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
data_len |
u32 |
4 | Actual data length. |
data |
bytes |
data_len |
Log bytes. |
5.3 Time synchronization¶
0x0401 Device time request¶
| Field | Type | Bytes | Description |
|---|---|---|---|
request_time |
u32 |
4 | Request construction timestamp T1. |
0x0402 SDK time response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
request_time |
u32 |
4 | Original request timestamp T1. |
response_time |
u32 |
4 | Request arrival timestamp T2. |
send_time |
u32 |
4 | Reply send timestamp T3. |
5.4 Recordings¶
0x0501 Get recording count¶
Empty body.
0x0502 Recording count response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
file_count |
u32 |
4 | Number of stored recordings. |
0x0503 Begin normal extraction¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Always 0. |
file_index |
u32 |
4 | Recording file index. |
0x0509 Begin fast extraction¶
Same body as 0x0503. The device first returns 0x0504, then consecutive 0x0505 packets until is_end=1.
0x0504 Recording file information¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
file_index |
u32 |
4 | Recording file index. |
record_time |
u32 |
4 | Recording timestamp in seconds. |
data_size |
u32 |
4 | Total encoded data bytes. |
0x0505 Recording data chunk¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
file_index |
u32 |
4 | Recording file index. |
frame_offset |
u32 |
4 | Byte offset within recording data. |
frame_size |
u32 |
4 | Chunk byte count, currently at most 4096. |
is_end |
u8 |
1 | 0 not final, 1 final chunk. |
data |
bytes |
frame_size |
Encoded recording bytes. |
data is a raw file fragment: OP2C on Opus firmware, or consecutive little-endian two-byte lengths followed by Speex payloads on legacy firmware. See recording formats. File-internal little-endian lengths do not change big-endian protocol fields.
After saving, legacy Speex firmware pushes this same 0x0505 format without preceding 0x0504. receive_auto_audio_file() selects the first frame's index, assembles by offset, and finishes at is_end=1. It returns (file_index, raw_audio) without AudioFileInfo.
Normal continuous push needs no extraction commands. For a gap, timeout or damaged frame, the SDK queries and verifies index and data_size using 0x0503 -> 0x0504, then requests the expected offset with 0x0506, up to three attempts. Results are bounded by data_size. Keep BLE connected during saving and push. If connection was too late, query the recording count and download by index.
0x0506 Request recording offset¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Always 0. |
file_index |
u32 |
4 | Recording file index. |
frame_offset |
u32 |
4 | Byte offset to read or recover. |
reserved |
u16 |
2 | Reserved padding required by current firmware length checks; always 0. |
The SDK uses frame_offset for the next normal-download chunk and to recover missing fast-download or push data.
0x0507 End normal extraction¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Always 0. |
file_index |
u32 |
4 | Recording file index. |
0x0508 Extraction-end response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
file_index |
u32 |
4 | Recording file index. |
0x050A Recording save result¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
file_index |
u32 |
4 | Newly saved recording index. |
Firmware V2.001.0002.0001 sends this after saving instead of pushing 0x0505. Retrieve using normal 0x0503 extraction and finish with 0x0507. Failed saves may return only error_code.
0x050B Delete all recordings¶
Empty body. Irreversibly deletes all recordings on the device.
0x050C Delete response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
0x050D Live audio subscription¶
Firmware version
This usage requires firmware V2.001.0002.0001 or later.
| Field | Type | Bytes | Description |
|---|---|---|---|
enabled |
u8 |
1 | 1 enable, 0 disable. |
Subscribing does not start recording; users still hold the button in recording mode. Live-only output uses BLE without flash saving; configurable firmware can also save a copy. Disconnect clears the subscription. Unsubscribe aborts live transmission.
0x050E Subscription response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code; notifications disabled or ATT MTU below 128 causes an error. |
enabled |
u8 |
1 | Current device subscription state. |
0x050F Live recording START¶
| Field | Type | Bytes | Description |
|---|---|---|---|
session_id |
u32 |
4 | Nonzero recording session ID. |
reserved |
u32 |
4 | Always 0. |
record_time |
u32 |
4 | Recording timestamp in seconds. |
reserved |
u32 |
4 | Always 0. |
sample_rate |
u16 |
2 | Always 16000. |
channels |
u8 |
1 | 1 or 2, matching recording settings. |
codec |
u8 |
1 | Always 3 (Opus). |
0x0510 Live recording DATA¶
| Field | Type | Bytes | Description |
|---|---|---|---|
session_id |
u32 |
4 | Same as 0x050F. |
sequence |
u32 |
4 | Packet sequence starting at 0. |
byte_offset |
u32 |
4 | Offset in transmitted Opus bytes, starting at 0. |
length |
u16 |
2 | Opus packet length, 1–100 bytes. |
data |
bytes |
length |
One 20 ms Opus packet, with the START channel count. |
byte_offset counts Opus data only, excluding OP2C length prefixes.
0x0511 Live recording END¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
session_id |
u32 |
4 | Same as 0x050F. |
reserved |
u32 |
4 | Always 0. |
produced_bytes |
u32 |
4 | Opus bytes produced by the device encoder. |
sent_bytes |
u32 |
4 | Opus bytes sent by the device. |
dropped_frames |
u32 |
4 | Frames dropped by the device. |
A recording is complete only when error_code=0, dropped_frames=0, and both byte counts equal the actual received data. Downloads during live recording may receive device-busy in 0x0504.
5.5 Live IMU¶
Firmware V2.001.0002.0000 requires gesture mode first. Firmware V2.001.0002.0001 removes mode switching and starts capture directly on 0x0601. Both retain the following command numbers.
0x0601 Start live reports¶
Empty body. Direct capture startup requires firmware V2.001.0002.0001 or later. Firmware V2.001.0002.0000 requires gesture mode first. Starting from stopped resets sequence_start; an active subscription does not reset it.
Use this sequence:
Firmware V2.001.0002.0001: no mode change required
-> Send 0x0601
-> Receive 0x0602 with error_code=0
-> Receive batch 0x0605
In recording mode on V2.001.0002.0000, 0x0601 returns device-busy. Firmware V2.001.0002.0001 has no such mode restriction. wait_sensor_data() only waits for data and does not send 0x0601.
0x0602 Start-report response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
sample_rate_hz |
u16 |
2 | Current sample rate; use this returned value. |
accel_range_g |
u16 |
2 | Accelerometer range. |
gyro_range_dps |
u16 |
2 | Gyroscope range. |
0x0603 Stop live reports¶
Empty body. Firmware V2.001.0002.0001 stops both BLE reporting and IMU capture. Firmware V2.001.0002.0000 only stops BLE reports and can retain local capture in gesture mode.
0x0604 Stop-report response¶
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
0x0605 Single-sample and batch IMU data¶
A successful single-sample body is error_code:u16 + sequence:u32 + timestamp_ms:u32 + accel_x/y/z:i16 + gyro_x/y/z:i16, exactly 22 big-endian bytes. The SDK returns a one-sample SensorDataBatch.
Batch format follows; its successful length is 10 + 16*N, distinct from single-sample format:
| Field | Type | Bytes | Description |
|---|---|---|---|
error_code |
u16 |
2 | Error code. |
sequence_start |
u32 |
4 | First sample sequence in this batch. |
frame_count |
u16 |
2 | Samples in this batch. |
sample_size |
u16 |
2 | Bytes per sample, currently 16. |
samples |
bytes |
frame_count * sample_size |
Consecutive samples. |
Each 16-byte sample has this layout:
| Field | Type | Bytes | Description |
|---|---|---|---|
timestamp_ms |
u32 |
4 | Device capture timestamp in milliseconds. |
accel_x |
i16 |
2 | Raw X-axis acceleration. |
accel_y |
i16 |
2 | Raw Y-axis acceleration. |
accel_z |
i16 |
2 | Raw Z-axis acceleration. |
gyro_x |
i16 |
2 | Raw X-axis gyroscope value. |
gyro_y |
i16 |
2 | Raw Y-axis gyroscope value. |
gyro_z |
i16 |
2 | Raw Z-axis gyroscope value. |
Successful body length must equal:
2 + 4 + 2 + 2 + frame_count * sample_size
Batches contain at most 32 samples; use frame_count for the actual size. The next sequence is sequence_start + frame_count. Firmware V2.001.0002.0001 stops capture on disconnect and requires another 0x0601 after reconnecting. Firmware V2.001.0002.0000 can resume local gesture-mode detection after reconnecting, but reporting still needs another subscription.
5.6 Motion and button events¶
Both V2.001.0002.0000 and V2.001.0002.0001 retain these event layouts. Changes to capture conditions and single-press behavior are described below.
0x0701 Physical double tap¶
| Field | Type | Bytes | Description |
|---|---|---|---|
timestamp_ms |
u32 |
4 | Event timestamp in milliseconds. |
Firmware V2.001.0002.0001 needs 0x0601 to start IMU capture before detecting taps. Firmware V2.001.0002.0000 detects taps in gesture mode without BLE sample reporting. Timestamps are milliseconds since boot, unlike the device-second timestamps of button events.
0x0703 Double button press¶
| Field | Type | Bytes | Description |
|---|---|---|---|
timestamp_ms |
u32 |
4 | Device timestamp in seconds; SDK retains the field name timestamp_ms. |
A recognized double press emits only 0x0703, without the corresponding single press or mode change.
0x0704 Single button press¶
| Field | Type | Bytes | Description |
|---|---|---|---|
timestamp_ms |
u32 |
4 | Device timestamp in seconds; SDK retains the field name timestamp_ms. |
Firmware V2.001.0002.0001 only emits 0x0704, without switching modes. Firmware V2.001.0002.0000 also attempts to switch recording/gesture modes. Confirmation waits for the double-press window; the body has no mode field.
After synchronization, button seconds represent device Unix time. Before synchronization, do not interpret them as calendar time. Do not compare directly with IMU/tap milliseconds since boot.
5.7 Remote recording and settings¶
Firmware version
This usage requires firmware V2.001.0002.0001 or later.
These commands implement recording-control interface v1. Start, status and configuration-read requests have empty bodies. Integers are big-endian. See recording control for application examples.
0x0512 / 0x0513 Start recording¶
Empty request. A successful response is error_code:u16, session_id:u32, six bytes. Nonzero session ID confirms acceptance, not completion; query 0x0516 for final status. After START timeout, query status instead of automatically retrying.
0x0514 / 0x0515 Stop recording¶
Request: session_id:u32, cancel:u8. cancel=0 stops according to output settings; cancel=1 discards uncommitted audio. Success: error_code:u16, session_id:u32. The ID must match; final status must still be awaited.
0x0516 / 0x0517 Query recording status¶
Empty request. Successful response is 14 bytes:
| Field | Type | Description |
|---|---|---|
error_code |
u16 |
Error code for the status query. |
session_id |
u32 |
Session ID; 0 when idle. |
state |
u8 |
0 idle, 1 queued, 2 capturing, 3 finalizing, 4 complete, 5 failed, 6 cancelled. |
saved |
u8 |
1 if a device file was saved, otherwise 0. |
recording_error |
u16 |
Recording error code, separate from the query error. |
file_index |
u32 |
Saved file index; 0 if not saved. |
The SDK uses saved to determine whether the index is valid. Without a saved file, returned file_index is None.
0x0518 / 0x0519 Read recording configuration¶
Empty request. Response begins with error_code:u16; on success the 14-byte settings below are followed by sample rate, frame duration and fixed codec parameters.
| Field | Type | Description |
|---|---|---|
version |
u8 |
Configuration structure version, currently 1. |
output |
u8 |
0 automatic, 1 flash, 2 live, 3 live plus flash. |
disconnect_policy |
u8 |
0 stop, 1 continue saving on device. |
channels |
u8 |
1 or 2. |
bitrate |
u32 |
Bitrate in bit/s. |
gain_left_q8, gain_right_q8 |
u16 each |
Left/right digital gain; 256 means 1x. |
max_duration_ms |
u16 |
Maximum duration in milliseconds. |
Successful responses are 34 bytes. Settings are followed by sample_rate:u16, frame_duration_ms:u16, then codec:u8, complexity:u8, vbr:u8, constrained_vbr:u8, dtx:u8, fec:u8, packet_loss_percent:u8, signal:u16, bit_depth:u8, max_packet_size:u16, application:u16. The device returns these fixed parameters; configuration writes cannot change them.
0x051A / 0x051B Write recording configuration¶
Request contains only the 14-byte settings above. Response matches configuration readback, showing accepted settings. Write only while idle. Continue-saving on disconnect requires flash or live-plus-flash output.
6. Error codes¶
| Value | Name | Description |
|---|---|---|
| 0 | SUCCESS |
Success. |
| 1 | UNKNOWN |
Unknown error. |
| 2 | DEVICE_BUSY |
Device busy. |
| 3 | FILE_NOT_EXIST |
File not found. |
| 4 | CMD_GROUP_NOT_EXIST |
Command group not found. |
| 5 | CMD_NOT_EXIST |
Command not found. |
| 6 | TIMEOUT |
Operation timed out. |
| 7 | PARAM_INVALID |
Invalid argument. |
| 8 | COMMUNICATION |
Communication error. |
| 9 | UNSUPPORTED_SCHEMA |
Unsupported message structure version. |
See the firmware update guide. The SDK provides update_firmware() and ota-update; calls are documented in the SDK firmware updates.
Capability discovery messages¶
The release tags for V2.001.0002.0000 and V2.001.0002.0001 do not implement the discovery commands below. This section documents the SDK parser for the discovery extension; it does not attribute that extension to the V2.001.0002.0001 release.
Protocol v4 and existing GET_INFO and recording-v1 layouts remain unchanged. All integers are big-endian.
| Command | Direction | Body |
|---|---|---|
0x0105 |
Host → device | Empty |
0x0106 |
Device → host | error:u16; success adds schema:u8=1 followed by TLVs |
0x01FF |
Device → host | original_command:u16, error:u16, exactly four bytes |
Errors: 2 busy, 5 unknown command, 7 invalid argument, 9 unsupported structure version. An unknown command returns 5, nonempty discovery request returns 7, and unknown version in the 14-byte recording settings returns 9. Existing command-specific errors remain valid; not all legacy failures use generic errors. Never reply to an error with another error.
TLVs contain tag:u16, length:u16, value:length bytes, within v4's 5120-byte limit; current firmware constructs responses in a buffer of at most 512 bytes. Duplicate tags, truncation, invalid UTF-8 and missing required device fields fail parsing. Unknown optional tags are skipped; an unknown tag with its top bit set requires an SDK upgrade.
| Tag | Content |
|---|---|
| 1 | Full firmware version, UTF-8. |
| 2 | Product model, UTF-8. |
| 3 | Firmware compilation target, UTF-8. |
| 4 | Eight-byte random boot_id for this boot. |
| 5 | Firmware build ID, UTF-8. |
| 6 | Supported protocol versions as consecutive u16 values; currently 4. |
0x0100 + feature_id |
Feature interface version major:u8, minor:u8. |
0x0204 |
Recording limits, defined below. |
Feature IDs: 1 system/1, 2 time_sync/1, 3 audio_files/1, 4 recording_control/1, 5 live_audio/1, 6 imu/2, 7 key_events/1, 8 double_tap/1, 9 ota/1, 10 gesture/1. The suffix is the supported major version. Current firmware does not advertise gesture. IMU/2 starts capture on subscription; an implementation requiring separate gesture mode must not advertise it.
Firmware advertises features only for fully registered command groups. Button/tap events require connected producers in the product build. Advertisement identifies implementation support, not successful hardware initialization, current connection readiness or an idle device.
Recording limits occupy exactly 26 bytes:
| Position | Type | Meaning |
|---|---|---|
| 1–2 | u16, u16 | Sample rate and frame duration in milliseconds. |
| 3–6 | u8 × 4 | Channel mask, output mask, continue-saving output mask, reserved=0. |
| 7–8 | u32 × 2 | Minimum and maximum bitrate, inclusive. |
| 9–10 | u16 × 2 | Minimum and maximum Q8 digital gain. |
| 11–13 | u16 × 3 | Minimum and maximum duration, and duration step in milliseconds. |
Masks use enum values as bit positions: channels 1/2 give 0x06; CONTINUE_SAVING with FLASH=1/BOTH=3 gives 0x0A. Recording interface v1 requires these limits. Read fixed Opus parameters through the existing configuration-read command.
Interface evolution¶
Use optional TLVs for additive fields so older clients can skip them. Incompatible fields or behavior need a higher major version and new command or structure. Mark client-required fields as required. Reject unsupported fields in writes. Detect recording formats from content, independently of current firmware.
Requests have no transaction IDs. Query state before retrying START. Release notes specify OTA compatibility; the SDK does not infer every starting-version combination or provide automatic rollback. Application and bootloader security versions are independent.