文档版本: dev
OpenZilo SDK 架构与数据流¶
适用设备
本文档适用于 OpenZilo Q。
连接要求
电脑需要支持低功耗蓝牙(BLE)。SDK 使用 NUS 服务与戒指通信,支持协议 v4。
本文介绍当前 SDK 如何处理 BLE 数据和录音文件。设备操作说明见戒指使用指南,各功能的固件要求见功能与版本要求。
这里说明 openzilo 包怎样收发数据、下载录音并生成 WAV。函数签名和调用示例见 API 索引,命令和包体字段见 协议参考。具体功能要求见功能与版本要求。
代码结构¶
应用只从 import openzilo 使用 __all__ 中的名称。实现按职责放在下划线开头的内部子模块中:
| 模块 | 职责 |
|---|---|
_errors |
异常类型和设备错误码 |
_protocol |
v4 包头、CRC、命令号、二进制读写 |
_transport |
基于 bleak 的 NUS 扫描、连接和分片写入 |
_client |
OpenZiloClient:请求响应匹配和收包分发 |
_tasks |
截止时间和后台回调任务的 asyncio 工具 |
_device |
扫描连接入口、系统信息、日志、校时 |
_ota |
应用固件升级 |
_compatibility |
查询设备功能、检查接口版本与录音参数 |
_recording |
远程录音、状态查询与录音设置 |
_identity、_version |
SDK 版本与源码校验值 |
_audio |
已保存录音的查询、下载和自动接收 |
_live_audio |
实时录音订阅和接收 |
_codec |
Speex、OP2C 格式与 WAV 转换 |
_pcm |
可选 PyAV 有状态 Opus 解码与 PCM 分块,不依赖 BLE |
_sensor |
IMU 上报和动作事件 |
_cli |
python -m openzilo 命令行 |
依赖只从上往下:_errors、_protocol 不依赖 BLE,录音编解码 _codec 也不依赖连接,可以单独用于离线文件处理。
实时音频流¶
固件版本
此用法需要固件 V2.001.0002.0001 或更新版本。
open_live_audio_stream() 用异步上下文管理订阅和清理,用异步迭代输出一段按键录音的 START、音频帧、END。进入上下文表示订阅已确认,录音仍由用户按键触发。一个流只能由一个任务读取,退出上下文会停止未完成的接收并尝试退订。
BLE 通知 → _PacketInbox 有界队列 → 后台接收任务:会话、序号、偏移校验
→ Opus 事件
→ 可选有状态解码/重采样 → 20 ms PCM 音频
→ 有界事件队列 → 应用 async for
后台接收任务独占音频操作,禁止并发下载、其他音频接收或 OTA。原始通知队列由 OpenZiloClient.receive_queue_size 控制,默认 256;应用事件队列由 queue_size 控制,默认 64。两层队列都在溢出时报错,不能通过暂停应用读取让戒指停止发送,也不静默丢弃帧。
PCM 模式的可选 pcm extra 提供 PyAV。每段录音单独创建 Opus 解码器和重采样器,逐包顺序执行,保留跨包状态;解码调用在工作线程中执行。输出为 16 kHz、有符号 16 位小端 PCM,每帧每声道 320 个采样;单声道或双声道与录音设置一致,双声道按左右交错排列。重采样器存在短缓存,END 校验成功后排空缓存并核对样本总数;不会给每个包单独创建文件或调用 ffmpeg 子进程。
流不累计整段录音。receive_live_audio_file() 读取同一条 Opus 事件流,累计原始包并生成 OP2C,沿用相同的完整性校验。收到并检查结束事件(END)之前,应用还不能确认整段音频完整,错误后无法从设备 Flash 补回。
从 BLE 通知到 SDK 接口¶
戒指 Nordic UART Service
↓ BLE 通知 / 特征值写入
NusClient 扫描、连接、订阅通知、分片写入
↓
PacketStream 拼出完整 v4 包,检查包头与 CRC
↓
OpenZiloClient 按命令分发响应和主动上报
↓
get_system_info() / download_audio_file() / wait_sensor_data() ...
NusClient 使用 bleak,按设备地址扫描和连接,不依赖广播名称。写入 NUS RX 特征值时,SDK 把数据切成最多 20 字节的片段;这不会改变系统蓝牙栈的 MTU 协商。戒指从 NUS TX 发来的通知没有相同的长度保证。
PacketStream.feed() 接收任意长度的通知数据。一个协议包可以跨多次通知,多包也可以连续到达。它按包头中的 body_length 截取完整包,跳过 magic 之前的杂字节;包头、CRC 或长度不合法时抛出 ProtocolError。包体上限是 5120 字节。
OpenZiloClient 为每个命令维护接收队列。request() 按响应命令串行执行,先清掉目标响应队列中的旧包,再发送请求并等待响应;wait_for_command() 用于等待 0x0605 等主动上报。长期监听可注册 add_packet_handler(),自动校时就是这种用法。每命令队列默认最多 256 包,可通过 receive_queue_size 调整;溢出清理积压并报告 ProtocolError。纯回调监听使用 add_packet_handler(..., queue_packets=False),独占该命令且不保留队列副本。等待期间 BLE 断开时抛出 TransportError。
v4 包的边界¶
包头固定 11 字节:
magic:u8=0x3F | version:u16=4 | command:u16 | body_length:u32 | body_crc:u16 | body
包头和普通包体整数用大端序,CRC16 只覆盖 body,空包体的 CRC 为 0。BinaryReader 和 BinaryWriter 处理这些字段。录音文件中的 Speex 帧长另用 2 字节小端序;它属于文件内容,不属于 v4 包头。各命令的包体布局在协议参考中。
录音文件格式¶
固件版本
OP2C 单/双声道 Opus 录音从固件 V2.001.0002.0001 开始提供。解码已有 Speex 文件无需升级固件。
固件 V2.001.0002.0000 保存连续的 Speex 帧,固件 V2.001.0002.0001保存 OP2C 格式的单声道或双声道 Opus 文件。升级不会转换已有录音,同一设备上可能两种都有,所以 SDK 按文件内容识别编码,不看固件版本或扩展名。
Speex 文件每帧前有 2 字节小端长度,后面紧跟该长度的 payload:
[length:u16 little-endian][Speex payload:length bytes]
[length:u16 little-endian][Speex payload:length bytes]
...
例如 14 00 表示下一帧 payload 长 20 字节。解析时要逐帧读取长度,不能按 20 字节固定切分。编码模式为 Speex Wideband;一帧对应 320 个 16 kHz 单声道采样点,即 20 ms。SDK 对单个 Speex 包设有 540 字节上限,输出的 PCM 默认为 16 kHz、单声道、16 bit。
OP2C 文件先是 12 字节小端文件头,之后同样是长度前缀加 Opus 包:
magic:"OP2C" | version:u16=1 | channels:u16=1或2 | sample_rate:u16=16000 | frame_samples:u16=320
[length:u16 little-endian][Opus payload:length bytes]
...
每个 Opus 包是 20 ms 的 16 kHz 音频,声道数与文件头一致,长度 1~100 字节,一段录音最多 3000 包(60 秒)。文件没有尾部填充或结束标记。
把各个 0x0505.data 按 frame_offset 拼接后,完整录音从第一个长度字段开始,不带设备文件管理元数据。AudioFileInfo.data_size 是这段编码数据的字节数。SDK 保存的 .bin 保持原始格式,便于重新解码。
录音怎样传到电脑¶
| 入口 | 设备交互 | SDK 接口 |
|---|---|---|
| 普通提取 | 0x0503 → 0x0504 取得文件信息,再用 0x0506 → 0x0505 按偏移读取;结束时 0x0507 → 0x0508 |
download_audio_file(..., quick=False) |
| 快速提取 | 0x0509 → 0x0504,随后设备连续发送 0x0505 |
download_audio_file(..., quick=True),默认方式 |
V2.001.0002.0000 保存后主动发送 |
设备直接发送连续 0x0505,事先没有 0x0504 |
receive_auto_audio_file() |
V2.001.0002.0001 保存后通知 |
设备发送 0x050A,SDK 再按普通提取下载 |
receive_auto_audio_file() |
| 实时录音 | 0x050D → 0x050E 订阅,按键录音时设备发送 0x050F、连续 0x0510、0x0511 |
receive_live_audio_file() |
每个 0x0505 包都有文件索引和 frame_offset。SDK 按偏移拼接,跳过已收到的重叠字节。快速提取遇到缺口或超时,会用 0x0506 请求缺失位置;连续补传超过 3 次仍不完整时抛出 ProtocolError。结果按 AudioFileInfo.data_size 截断。设备单帧数据区最大 4096 字节,SDK 的包体上限为 5120 字节。
固件 V2.001.0002.0000 主动发送发生在录音保存后,不需要应用先发提取命令。receive_auto_audio_file() 从首帧取得文件索引,返回 (file_index, raw_audio)。如果帧丢失,它会先用 0x0503 → 0x0504 核对文件长度,再按偏移补传;无法补齐就报错,不返回残缺录音。
固件 V2.001.0002.0001 保存后的 0x050A 只带文件索引。receive_auto_audio_file() 收到后走普通提取,结束或出错时都会发送 0x0507 释放文件。
仅实时发送的录音不保存在设备中,丢失的数据无法补传。选择同时保存时,可在设备保存完成后下载副本。receive_live_audio_file() 校验会话、序号、偏移和结束包里的计数,任一不符都会报错,不返回不完整的录音。成功后它把收到的 Opus 包封装成 OP2C,和下载得到的文件走同一条解码路径。
使用主动接收时,BLE 连接要在设备发送前建立。receive_auto_audio_file()、download_audio_file() 和 read_audio_frame() 会读取同一连接的 0x0505 队列,并发调用会被 SDK 拒绝(ProtocolError)。如果错过主动发送,查询录音数量后再按索引下载。
从 Speex / Opus 到 WAV¶
Speex: .bin → 逐帧读取长度前缀 → Speex payload → 临时 Ogg Speex
→ ffmpeg 解码为 s16le PCM → 加 RIFF/WAV 头 → .wav
OP2C: .bin → 校验文件头和每包长度 → Opus payload → 临时 Ogg Opus
→ ffmpeg 解码为 16 kHz 单声道或双声道 s16le PCM → 加 RIFF/WAV 头 → .wav
OP2C 按文件头中的声道数解码,调用方传入的 pcm_config 不会覆盖文件头。缺少 ffmpeg 时 Opus 路径抛出 OpusDecoderUnavailable。
save_audio_bundle() 先写入原始 .bin,再尝试生成 WAV。没有 ffmpeg 或解码失败时,原始文件仍在,函数会抛出 SpeexDecoderUnavailable、OpusDecoderUnavailable 或 AudioDecodeError。
build_playable_audio() 遇到已有的 WAV 会直接返回;其他路径还能识别 Ogg Speex 和无长度前缀的裸 Speex。返回的 source_type 可为 wav、ogg-speex、packet-speex 或 raw-speex。旧录音走 packet-speex 路径,Opus 录音按声道数返回 opus-mono 或 opus-stereo。只有裸 Speex 回退才使用 quality 和 bits_size:默认 quality=3 对应估算帧长 20 字节,显式传入的 bits_size 会覆盖该估算。
旧数据若带 1026 字节外层分块,可传入 allow_framed_blocks=True。底层 parse_packetized_speex_stream() 默认允许这种分块;decode_speex_to_pcm()、build_playable_audio()、decode_audio_to_wav() 和 save_audio_bundle() 默认不启用,调用高层函数时需显式指定。
build_ogg_speex() 只为解码临时封装 Ogg。Speex 头使用版本标记 speex-1.2.1、Wideband mode、16 kHz、单声道、每帧 320 个采样点、每包一帧;vendor comment 为 openzilo-python。音频包的 granule position 每包增加 320。ffmpeg 输出 s16le PCM,build_wav_from_pcm() 再写入 44 字节的 PCM WAV 头。若解码器为一个 Speex 包输出多帧且长度可整除,normalize_decoded_speex_pcm() 只保留每包的第一帧。SDK 默认不单独保存中间 Ogg 文件。
| 文件或数据 | 内容 | 能否直接播放 |
|---|---|---|
设备 .bin |
带长度前缀的 Speex 帧,或 OP2C 文件 | 否 |
.spx / .ogg |
Ogg 容器中的 Speex | 支持 Speex 的播放器可以 |
s16le PCM |
无容器的 16 bit 采样 | 需要另给采样率和声道数 |
.wav |
PCM 加 RIFF/WAV 头 | 可以 |
改扩展名不会改变数据格式。设备 .bin 要先拆出 Speex 或 Opus 包并封装、解码,才能得到 WAV。
IMU 与动作事件¶
固件 V2.001.0002.0001:start_sensor_report() 启动采集和上报,stop_sensor_report() 或断线停止采集;重连后重新订阅。固件 V2.001.0002.0000:先进入手势模式,停止 BLE 上报后仍可继续本地检测。
SDK 支持两种 0x0605 成功包体:
- 单点:
error_code:u16 + sequence:u32 + timestamp_ms:u32 + 六轴:i16*6,共 22 字节。 - 批量:
error_code:u16 + sequence_start:u32 + frame_count:u16 + sample_size:u16 + samples,共10 + 16*N字节。
两种格式统一返回 SensorDataBatch;单点的 frame_count=1。下列事件格式在 V2.001.0002.0000 与 V2.001.0002.0001 中保留,采集条件和单击行为的变化见下文。
| 命令 | 事件 | 包体 |
|---|---|---|
0x0701 |
普通双击 | timestamp_ms:u32(开机后毫秒数) |
0x0703 |
按键双击 | u32 秒级设备时间;SDK 字段名为 timestamp_ms |
0x0704 |
按键单击 | u32 秒级设备时间;SDK 字段名为 timestamp_ms |
V2.001.0002.0001 的敲击检测使用订阅开启的 IMU 数据,单击 0x0704 不再切换模式。V2.001.0002.0000 在手势模式下检测敲击,单击可切换模式。LED 是本地提示,SDK 无法查询其状态。
重连后客户端应重新开启实时上报,本地采集的恢复行为以对应固件为准。
联调时先查什么¶
.bin无法播放:这是原始 Speex 帧流或 OP2C 文件。用save_audio_bundle()生成 WAV,并确认安装了ffmpeg。ffmpeg报格式错误:检查文件是否从首个 2 字节小端长度字段开始,下载时是否按frame_offset拼接,是否把协议包字段混进了数据区。- 收不到
0x0605:确认start_sensor_report()收到了成功的0x0602;V2.001.0002.0000先进入手势模式;V2.001.0002.0001无需切换。 - 收到连续
0x0505却没有0x0504:这是保存后的主动发送,用receive_auto_audio_file()接收。
固件或协议升级时,先比对实机行为、src/openzilo/ 和协议参考,再更新调用手册和本页。