Documentation version: v0.6.2
OpenZilo SDK architecture and data flow¶
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.
This page explains BLE and recording processing in the current SDK. See the ring guide for device operations and feature requirements for firmware requirements.
It covers receiving data, downloading files and producing WAV. Function signatures and examples are in the API index; commands and fields are in the protocol reference.
Module organization¶
Applications import public names from openzilo.__all__. Implementation modules are private and underscore-prefixed:
| Module | Responsibility |
|---|---|
_errors |
Exceptions and device error codes. |
_protocol |
V4 headers, CRC, command numbers and binary I/O. |
_transport |
Bleak-based NUS discovery, connection and chunked writes. |
_client |
OpenZiloClient request matching and packet dispatch. |
_tasks |
Asyncio deadline and background callback helpers. |
_device |
Scanning, connections, system information, logs and time synchronization. |
_ota |
Application firmware updates. |
_compatibility |
Feature discovery, interface version and recording parameter checks. |
_recording |
Remote recording, status and configuration. |
_identity, _version |
SDK version and source checksum. |
_audio |
Stored recording queries, download and automatic reception. |
_live_audio |
Live recording subscription and reception. |
_codec |
Speex, OP2C and WAV conversion. |
_pcm |
Optional stateful PyAV Opus decoding and PCM framing, independent of BLE. |
_sensor |
IMU reporting and motion events. |
_cli |
python -m openzilo CLI. |
_errors and _protocol do not depend on BLE. _codec is also independent of connections and can process files offline.
Live audio streams¶
Firmware version
This usage requires firmware V2.001.0002.0001 or later.
open_live_audio_stream() manages subscription and cleanup with an async context, yielding START, audio frames and END for one recording. Context entry confirms subscription; users still trigger recording with the button. One task reads the stream. Context exit aborts unfinished reception and attempts unsubscribe.
BLE notifications -> bounded _PacketInbox -> background receiver: session, sequence and offset checks
-> Opus events
-> optional stateful decode/resample -> 20 ms PCM
-> bounded event queue -> application async for
The receiver exclusively owns audio operations, excluding downloads, other receivers and OTA. Client receive_queue_size controls the raw queue (default 256); stream queue_size controls output events (default 64). Both report overflow errors instead of dropping data silently. Pausing local consumption cannot stop the ring from transmitting.
The optional pcm extra supplies PyAV. Each recording creates a stateful decoder/resampler and processes packets sequentially in a worker thread. Output is 16 kHz signed 16-bit little-endian PCM, 320 samples per channel per frame. Channel count follows the recording, with stereo interleaved L/R. After valid END, the resampler drains and total samples are checked. No per-packet files or ffmpeg processes are created.
Streaming does not accumulate an entire recording. receive_live_audio_file() reads the same validated Opus events and assembles OP2C. Only a checked END establishes completeness; live-only data lost after an error cannot be recovered from flash.
From BLE notifications to SDK APIs¶
Ring Nordic UART Service
↓ BLE notifications / characteristic writes
NusClient scan, connect, subscribe, chunk writes
↓
PacketStream reconstruct v4 packets, check header and CRC
↓
OpenZiloClient dispatch responses and events by command
↓
get_system_info() / download_audio_file() / wait_sensor_data() ...
NusClient uses bleak and connects by address, without relying on advertising names. NUS RX writes contain at most 20 bytes per chunk; system MTU negotiation remains enabled. Ring TX notifications have no matching 20-byte guarantee.
PacketStream.feed() accepts arbitrary notification sizes, including partial or consecutive packets. It uses body_length to extract packets and skips bytes before the magic marker. Invalid headers, CRC or lengths raise ProtocolError. Bodies are limited to 5120 bytes.
OpenZiloClient maintains per-command queues. request() serializes calls expecting the same response, drains stale queued packets, sends and waits. wait_for_command() receives events such as 0x0605. Long-lived listeners use add_packet_handler(), as time synchronization does. Queue capacity defaults to 256; overflow clears backlog and raises ProtocolError. Callback-only handlers use queue_packets=False for exclusive delivery without queue copies. BLE disconnect during waits raises TransportError.
Protocol v4 packet boundaries¶
The fixed header is 11 bytes:
magic:u8=0x3F | version:u16=4 | command:u16 | body_length:u32 | body_crc:u16 | body
Header and body integers are big-endian. CRC16 covers body only and is 0 for an empty body. BinaryReader and BinaryWriter handle these fields. Speex frame lengths are separate two-byte little-endian file fields, not protocol headers. See the protocol reference.
Recording formats¶
Firmware version
OP2C mono/stereo Opus recordings are available starting with V2.001.0002.0001. Decoding existing Speex files does not require a firmware upgrade.
Firmware V2.001.0002.0000 stores consecutive Speex frames; V2.001.0002.0001 stores mono/stereo OP2C. Firmware upgrades do not convert existing files, so both can coexist. The SDK detects encoding from contents, not firmware versions or filename extensions.
Each Speex frame has a two-byte little-endian payload length:
[length:u16 little-endian][Speex payload:length bytes]
[length:u16 little-endian][Speex payload:length bytes]
...
For example, 14 00 means 20 payload bytes. Read each length rather than splitting at fixed 20-byte boundaries. Speex Wideband frames contain 320 mono samples at 16 kHz (20 ms). Individual packets are limited to 540 bytes; default decoded PCM is 16 kHz mono 16-bit.
OP2C starts with a 12-byte little-endian header followed by length-prefixed Opus packets:
magic:"OP2C" | version:u16=1 | channels:u16=1 or 2 | sample_rate:u16=16000 | frame_samples:u16=320
[length:u16 little-endian][Opus payload:length bytes]
...
Each Opus packet carries 20 ms at 16 kHz with the header's channels, in 1–100 bytes. A recording has at most 3000 packets (60 seconds), without trailing padding or terminators.
Join 0x0505.data by frame_offset. The file data excludes device file-management metadata; AudioFileInfo.data_size is its byte count. For Speex, data starts with the first length field. SDK .bin output preserves the original format for later decoding.
Recording transfers¶
| Entry point | Device interaction | SDK API |
|---|---|---|
| Normal extraction | 0x0503 → 0x0504 metadata, repeated 0x0506 → 0x0505 by offset, then 0x0507 → 0x0508 |
download_audio_file(..., quick=False) |
| Fast extraction | 0x0509 → 0x0504, then consecutive 0x0505 |
download_audio_file(..., quick=True), default |
V2.001.0002.0000 post-save push |
Consecutive 0x0505, without initial 0x0504 |
receive_auto_audio_file() |
V2.001.0002.0001 post-save notification |
Device sends 0x050A; SDK downloads normally |
receive_auto_audio_file() |
| Live recording | Subscribe 0x050D → 0x050E; button recording sends 0x050F, consecutive 0x0510, then 0x0511 |
receive_live_audio_file() |
Each 0x0505 has a file index and offset. Assembly skips duplicate overlap. Gaps or timeouts in fast transfer trigger 0x0506 recovery; more than three unsuccessful recovery attempts raise ProtocolError. Output is truncated to data_size. Device data chunks are at most 4096 bytes; SDK bodies are limited to 5120.
Legacy push happens after saving, with no initial extraction request. receive_auto_audio_file() selects the first frame's index and returns (file_index, raw_audio). For missing data it queries metadata, checks file length and recovers by offset, failing rather than returning incomplete audio.
The 0x050A save notification carries a file index. The SDK performs normal extraction and sends 0x0507 to release the file, including on errors.
Live-only audio is not stored and cannot be retransmitted. Simultaneous saving permits downloading a completed device copy. Live reception validates session, sequence, offsets and END counts, rejecting incomplete audio. It packages Opus as OP2C for the same decoding path used by downloaded files.
Connect before automatic transmission begins. receive_auto_audio_file(), download_audio_file() and read_audio_frame() share 0x0505; concurrent use is rejected with ProtocolError. If push was missed, query the count and download by index.
From Speex / Opus to WAV¶
Speex: .bin -> read frame lengths -> Speex payload -> temporary Ogg Speex
-> ffmpeg s16le PCM -> add RIFF/WAV header -> .wav
OP2C: .bin -> validate header and packet lengths -> Opus -> temporary Ogg Opus
-> ffmpeg 16 kHz mono/stereo s16le PCM -> add RIFF/WAV header -> .wav
OP2C decoding follows its header's channel count; a caller-supplied pcm_config cannot override it. Missing ffmpeg raises OpusDecoderUnavailable on the Opus path.
save_audio_bundle() writes raw .bin first, then attempts WAV. The raw file survives missing ffmpeg or decode failure, reported as SpeexDecoderUnavailable, OpusDecoderUnavailable or AudioDecodeError.
build_playable_audio() returns existing WAV unchanged and recognizes Ogg Speex or raw Speex without lengths. source_type may be wav, ogg-speex, packet-speex or raw-speex; OP2C returns opus-mono or opus-stereo. Only raw-Speex fallback uses quality and bits_size: quality 3 estimates 20-byte frames, while explicit bits_size overrides that estimate.
For legacy 1026-byte outer blocks, pass allow_framed_blocks=True. Low-level parse_packetized_speex_stream() allows them by default; higher-level decoding and saving APIs require explicit opt-in.
build_ogg_speex() constructs a temporary container with speex-1.2.1, Wideband, 16 kHz mono, 320 samples per frame, one frame per packet, and vendor openzilo-python. Granule position advances by 320 per packet. ffmpeg emits s16le; build_wav_from_pcm() adds a 44-byte header. If one packet decodes to multiple evenly sized frames, normalize_decoded_speex_pcm() retains only the first. Intermediate Ogg files are not saved separately by default.
| File/data | Content | Directly playable? |
|---|---|---|
Device .bin |
Length-prefixed Speex or OP2C | No |
.spx / .ogg |
Speex in Ogg | With a Speex-capable player |
s16le PCM |
Unwrapped 16-bit samples | Requires sample rate and channels |
.wav |
PCM with RIFF/WAV header | Yes |
Changing extensions does not convert formats. Extract, wrap and decode the Speex or Opus packets to produce WAV.
IMU and motion events¶
Firmware V2.001.0002.0001 starts capture and reports on start_sensor_report(), and stops capture on stop_sensor_report() or disconnect. Subscribe again after reconnecting. Firmware V2.001.0002.0000 needs gesture mode first and can retain local detection after BLE reports stop.
The SDK accepts two successful 0x0605 formats:
- Single sample:
error_code:u16 + sequence:u32 + timestamp_ms:u32 + six_axes:i16*6, 22 bytes. - Batch:
error_code:u16 + sequence_start:u32 + frame_count:u16 + sample_size:u16 + samples,10 + 16*Nbytes.
Both return SensorDataBatch, with frame_count=1 for single-sample input. The event layouts below are retained in V2.001.0002.0000 and V2.001.0002.0001.
| Command | Event | Body |
|---|---|---|
0x0701 |
Physical double tap | timestamp_ms:u32, milliseconds since boot |
0x0703 |
Double button press | u32 device seconds; SDK retains timestamp_ms |
0x0704 |
Single button press | u32 device seconds; SDK retains timestamp_ms |
Firmware V2.001.0002.0001 detects taps using IMU data enabled by subscription, and single presses (0x0704) no longer switch modes. Firmware V2.001.0002.0000 detects taps in gesture mode and uses single presses for mode switching. LEDs are local indicators without an SDK query.
Restart reports after reconnection. Local capture restoration depends on firmware.
Debugging checklist¶
- Unplayable
.bin: usesave_audio_bundle()for raw Speex/OP2C conversion and check ffmpeg installation. - ffmpeg format error: check file framing, offset-based assembly, and that no protocol fields were mixed into audio. Speex starts with its two-byte length; OP2C starts with its header.
- No
0x0605: confirm successful0x0602after starting reports. Enter gesture mode first only on firmware requiring it. - Consecutive
0x0505without0x0504: this is post-save push; usereceive_auto_audio_file().
When firmware or protocols change, compare device behavior, src/openzilo/ and the protocol reference, then update the SDK guide and this page.