# 录音笔 BLE 通讯协议 CB08 录音笔 ↔ 微信小程序|协议实现与真机验证版 | **文档项** | **内容** | | --- | --- | | 版本 | V1.0(2026-07-10) | | 适用项目 | E:\\codex\\record | | 通信角色 | 小程序为 Central / 主机;录音笔为 Peripheral / 从机 | | 依据 | 厂商《录音笔与APP通讯协议(20260313)》及 CB08 真机抓包、下载验证 | | 验证范围 | 连接、电量、文件列表、WAV 下载、帧 CRC、字节序及文件输出结构 | **协议结论摘要** 帧头 LEN 与 CRC 均按小端;文件列表 count/time/size 按大端;CRC 使用 CRC-16/XMODEM。文件下载命令 2-2 的完整请求帧为 36B,CB08 要求一次 GATT 写入,拆成 20+16 会稳定返回“文件不存在”。 # 1\. 适用范围与约定 本协议用于微信小程序通过 BLE 控制 CB08 录音笔、接收实时音频、读取设备文件列表,并将设备内录音导入为 WAV 或 OPUS 文件。 多字节数值若本协议明确标注 LE,则按小端解析;文件列表结构中的整数按真机抓包结果使用 BE。除特别说明外,所有命令号均为十进制。 | **术语** | **说明** | | --- | --- | | App / 小程序 | BLE Central,主动扫描、连接并发送命令 | | Dev / 录音笔 | BLE Peripheral,提供 AE20 服务并通过 Notify 回传数据 | | LE | Little Endian,小端;低字节在前 | | BE | Big Endian,大端;高字节在前 | | GATT 单写 | 一次调用 writeBLECharacteristicValue 写入一个完整 characteristic value | # 2\. BLE 传输层 | **对象** | **UUID** | **属性** | **方向 / 用途** | | --- | --- | --- | --- | | Service | 0xAE20 | Primary Service | 录音笔业务服务 | | Characteristic | 0xAE21 | WRITE_WITHOUT_RESPONSE | App → Dev,发送协议帧 | | Characteristic | 0xAE22 | NOTIFY | Dev → App,控制应答、音频、列表和文件数据 | | Characteristic | 0xAE23 | NOTIFY | Dev → App,机身按键及录音状态消息 | 连接成功后必须先订阅 AE22;AE23 用于按键消息,建议同时订阅。AE22 与 AE23 应使用独立的流式帧缓存,避免两个通知特征的字节交织破坏半帧。 安卓可调用 setBLEMTU 主动协商;iOS 的 MTU 由系统固定协商。常规命令可按 MTU-3 选择载荷,但文件导入请求 2-2 按第 7 节的真机约束处理。 # 3\. 通用帧格式 | **偏移** | **长度** | **字段** | **字节序** | **取值 / 说明** | | --- | --- | --- | --- | --- | | 0 | 1B | MAGIC | \- | 固定 0x5A | | 1 | 1B | SEQ | \- | 包序号,0~255 循环递增 | | 2 | 2B | CRC | LE | CRC-16/XMODEM,计算范围为 LEN(2B)+DATA | | 4 | 2B | LEN | LE | DATA 的真实字节数 | | 6 | LEN | DATA | 见命令 | \[TYPE:1B\]\[CMD:1B\]\[PARAMS...\];ACK 可仅含 TYPE | ## 3.1 CRC-16/XMODEM 参数 | **参数** | **值** | | --- | --- | | Polynomial | 0x1021 | | Initial value | 0x0000 | | RefIn / RefOut | false / false | | XorOut | 0x0000 | | 标准检验向量 | ASCII "123456789" → 0x31C3 | CRC 输入必须包含帧头中的两个 LEN 原始字节,再拼接 DATA;不包含 MAGIC、SEQ 和 CRC 字段本身。 ## 3.2 DATA 类型 | **TYPE** | **名称** | **内容** | | --- | --- | --- | | 0 | 控制命令 | 时间、电量、容量、固件、授权码 | | 1 | 实时音频 / 转写 | 开始、音频数据、停止、暂停/继续、设备状态 | | 2 | 文件操作 | 列表、导入、数据、结束、删除、终止 | | 3 | 按键 / 录音控制 | 机身按键、App 控制、状态、增益 | 厂商原文第四节曾将 TYPE=3 描述为 ACK,第七节又将其定义为按键命令。实现以第七节命令表为准;若 DATA 只有一个 TYPE 字节,则按 ACK 处理。 # 4\. 控制命令(TYPE=0) | **CMD** | **方向** | **名称** | **参数 / 应答** | | --- | --- | --- | --- | | 0 | App→Dev | 同步时间 | year:2B LE + month/day/hour/minute/second,各1B,共7B | | 1 | App→Dev | 获取容量 | 无 | | 2 | Dev→App | 容量应答 | remain:4B LE + total:4B LE;厂商原文单位标8KB,当前实现按1KB显示 | | 3 | App→Dev | 获取电量 | 无 | | 4 | Dev→App | 电量应答 | 1B:0~100;110 表示充电中 | | 10 | App→Dev | 获取固件版本 | 无 | | 11 | Dev→App | 固件版本应答 | 6B ASCII,例如 V1.0.0 | | 12 | App→Dev | 获取授权码 | 无 | | 13 | Dev→App | 授权码应答 | 授权码字节串 | # 5\. 实时音频命令(TYPE=1) | **CMD** | **方向** | **名称** | **参数 / 应答** | | --- | --- | --- | --- | | 0 | App→Dev | 开始实时转写 | 无 | | 0 | Dev→App | 本次录音文件名 | 设备在开始推流前发送文件名 | | 1 | Dev→App | 实时音频数据 | 压缩音频码流字节;当前机型为 OPUS 系码流 | | 2 | App→Dev | 结束实时转写 | 无 | | 3 | App→Dev | 暂停 / 继续 | 1B:0=继续,1=暂停 | | 4 | Dev→App | 设备端状态 | 1B:0=继续,1=暂停,2=停止 | 实时音频数据可以原样保存作为备份;真正的语音转文字需要后端完成 OPUS 解码/转码并调用流式 ASR。云服务永久密钥不得写入小程序。 # 6\. 文件命令(TYPE=2) | **CMD** | **方向** | **名称** | **参数 / 应答** | | --- | --- | --- | --- | | 0 | App→Dev | 获取文件列表 | 无 | | 1 | Dev→App | 文件列表数据 | count:4B BE + N×28B 条目 | | 2 | App→Dev | 请求导入文件 | offset:4B LE + filename:24B | | 3 | Dev→App | 开始导入 | 实际导入文件名 | | 4 | Dev→App | 文件数据 | 音频文件字节分片 | | 5 | Dev→App | 导入结束 | 1B 状态码:0完成、1不存在、2 offset过大、3其他停止 | | 7 | App→Dev | 终止导入 | 无 | | 8 | App→Dev | 删除单个文件 | 与文件列表相同的28B条目 | | 9 | App→Dev | 删除全部文件 | 无 | | 10 | Dev→App | 删除全部应答 | 1B:0成功、1失败;旧固件可能不发送 | | 11 | Dev→App | 终止导入应答 | 无 | | 12 | App→Dev | 分段导入 | start:4B LE + end:4B LE + filename | | 13 | Dev→App | 删除单个应答 | 1B:0成功、1失败;旧固件可能不发送 | | 18 | Dev→App | 列表发送完毕 | 1B:0 表示完成 | # 7\. 文件列表与下载流程 ## 7.1 文件列表结构(CMD 2-1) | **字段** | **长度** | **字节序** | **说明** | | --- | --- | --- | --- | | count | 4B | BE | 本帧包含的文件条目数,不是全部文件总数 | | time | 4B | BE | 录音时长,单位秒;个别固件可能使用绝对时间戳 | | size | 4B | BE | 设备内压缩文件大小,单位 Byte | | name | 20B | UTF-8/ASCII | 固定长度、NUL填充;长文件名会截断扩展名 | 设备可用多帧 CMD=1 发送列表。App 应累积每一帧的 N 条记录,收到 CMD=18 后交付完整列表;为兼容不发送 CMD=18 的旧固件,可在最后一帧后空闲约1.2秒进行 best-effort 收尾。 典型文件名 note20260710-162938.opus 长24B,而列表字段只有20B,因此列表实际返回 note20260710-162938.。下载时必须重建完整扩展名。 ## 7.2 普通下载请求(CMD 2-2) | **参数** | **长度** | **字节序** | **示例** | | --- | --- | --- | --- | | offset | 4B | LE | 首次下载为 00 00 00 00 | | filename | 24B | ASCII/UTF-8 | note20260710-162938.wav + 00 | **强制约束|2-2 必须整帧单写** 完整协议帧为 36B。CB08 真机 A/B 已确认:一次 GATT write 写入36B可成功;若在应用层拆为20B+16B,设备会把文件名解析错误并返回 CMD=5 / code=1(文件不存在)。不要让通用分包器拆分2-2帧。 **步骤 1|读取真实 MTU** 连接后可调用 getBLEMTU 获取 ATT_MTU,常规命令的单次载荷为 MTU-3。2-2 仍按上述强制约束整帧发送。 **步骤 2|重建目标文件名** 优先使用 base.wav,让录音笔转码输出标准 WAV;失败时可尝试 base.opus 和列表原始截断名。filename 字段固定24B,不足补0。 **步骤 3|发送完整请求** 构造 TYPE=2、CMD=2、offset=0 和24B文件名,连同6B通用帧头一次写入 AE21。 **步骤 4|接收文件数据** 收到 CMD=3 后建立会话;持续拼接 CMD=4 的 body 字节;每次有效数据可刷新空闲超时。 **步骤 5|处理结束码** 收到 CMD=5:code=0 时写盘;code=1 时换候选名;code=2 时重置 offset;code=3 或传输中断时停止并提示。 ## 7.3 成功请求帧示例 2026-07-10 本机蓝牙成功下载 note20260710-162938.wav 时的真实 TX 帧: 5a 03 9e 20 1e 00 02 02 00 00 00 00 6e 6f 74 65 32 30 32 36 30 37 31 30 2d 31 36 32 39 33 38 2e 77 61 76 00 | **片段** | **解析** | | --- | --- | | 5a | MAGIC | | 03 | SEQ=3 | | 9e 20 | CRC=0x209E(LE存储) | | 1e 00 | LEN=30 | | 02 02 | TYPE=2,CMD=2 | | 00 00 00 00 | offset=0 | | 其余24B | filename=note20260710-162938.wav,末尾NUL | ## 7.4 WAV 输出验证 | **验证项** | **真机结果** | | --- | --- | | 设备 / 地址 | CB08 / D1:A1:C7:00:02:F2 | | 文件列表 | 24条 | | 请求文件 | note20260710-162938.wav | | IMPORT_END | code=0 | | 接收字节数 | 38,444B | | RIFF声明长度 | 38,444B,与实际长度一致 | | 音频参数 | PCM、16kHz、16bit、单声道、1.2秒 | WAV 文件头应满足 bytes\[0:4\]=RIFF、bytes\[8:12\]=WAVE。列表 size 是设备内压缩体积,不等于转码后的 WAV 体积;WAV 进度可按时长×32000B/s+44估算。 # 8\. 按键与录音控制(TYPE=3) | **CMD** | **名称** | **参数 / 应答** | | --- | --- | --- | | 1 / 2 | 开始录音 / 开始结果 | 结果1B:1成功、2失败 | | 3 / 4 | 保存录音 / 保存结果 | 结果1B:1成功、2失败 | | 5 / 6 | 暂停录音 / 暂停结果 | 结果1B:1成功、2失败 | | 7 / 8 | 继续录音 / 继续结果 | 结果1B:1成功、2失败 | | 19 / 20 | 获取 / 应答录音状态 | 1=录音中、2=未录音、3=暂停 | | 21 / 22 | 获取 / 应答录音时间 | duration:2B LE + currentSize:4B LE | | 23 / 24 | 获取 / 应答当前文件名 | 文件名字节串 | | 25 / 26 | 获取 / 应答增益 | 1=低、2=中、3=高 | | 27 / 28 | 设置增益 / 设置结果 | 设置值1~3;结果0成功、1失败 | CMD 1/3/5/7 同时可能由机身按键触发并经 AE23 上报;实现应按来源特征和当前会话状态判定是主动应答还是设备事件。 # 9\. 解析、超时与兼容策略 | **场景** | **建议处理** | | --- | --- | | BLE Notify 分片 | 按 LEN 跨通知重组;一个通知可能含半帧,也可能含多帧 | | AE22 / AE23 并发 | 使用独立 FrameParser 缓冲,解析后统一分发 | | CRC错误 | 已锁定端序后丢弃损坏帧并记录原始hex | | 端序未知 | 连接后用电量查询探测 LEN/CRC 组合,CRC命中即锁定 | | 列表无 CMD=18 | 收到数据后空闲约1.2秒,返回已累积列表 | | 文件下载无数据 | 空闲约12秒判定超时;若 received=0 可换候选名 | | 传输中断且已有数据 | 不要自动换文件名;提示失败或使用 offset 续传 | | 主动取消 | 发送 CMD=7,清理定时器和 Promise,忽略取消类弹窗 | | 同名文件 | 本地缓存路径加入 time/size/序号,避免覆盖 | # 10\. 小程序实现映射 | **文件** | **职责** | | --- | --- | | utils/crc16.js | CRC-16/XMODEM | | utils/protocol.js | 命令常量、帧构造、流式解析、字段解码 | | utils/ble.js | 扫描、连接、MTU、Notify订阅、AE21写入;对2-2强制单写 | | utils/recorder.js | 高层请求/应答、文件列表组装、下载会话与事件 | | pages/scan | 发现并连接录音笔 | | pages/files | 列表、下载、播放、导出、删除 | | pages/transcribe | 实时音频、波形、计时、暂停/继续与ASR文本 | | test/integration/ble_e2e.py | 本机蓝牙端到端验证脚本 | # 11\. 联调验收清单 | **检查项** | **通过标准** | | --- | --- | | 扫描 | 发现 CB08,广播服务包含 AE20 | | 连接 | 发现 AE21/AE22;AE22 Notify 开启成功 | | 电量 | 0-3 请求后收到 0-4,应答值合理 | | 列表 | 2-0 后累积 2-1,条目数量与原 App 一致 | | 下载请求 | 2-2 完整36B一次写入,设备返回2-3而非直接2-5/code1 | | 文件数据 | 持续收到2-4,最终2-5/code0 | | WAV | RIFF/WAVE头正确,声明长度等于实际长度,可播放 | | 断开 | 下载Promise结束、定时器释放、UI回到未连接状态 | # 附录 A. 关键状态码 | **位置** | **值** | **含义** | | --- | --- | --- | | 电量 0-4 | 110 | 充电中;界面可显示100%或“充电中” | | 实时状态 1-4 | 0 / 1 / 2 | 继续 / 暂停 / 停止 | | 导入结束 2-5 | 0 / 1 / 2 / 3 | 完成 / 文件不存在 / offset过大 / 其他停止 | | 录音状态 3-20 | 1 / 2 / 3 | 录音中 / 未录音 / 暂停 | | 增益 3-26 | 1 / 2 / 3 | 低 / 中 / 高 | **安全说明** 文件删除命令具有破坏性。端到端验证默认只执行读取和下载,不执行 CMD=8/9。发布前应在专用测试录音上验证删除,并确认大端28B条目与目标固件一致。