蓝牙模块挂在主机的 UART 上:手机连上蓝牙放歌,模块把曲名、艺术家、进度等状态以文本行推给主机,主机再转发到控制面板显示。没有正式协议文档,厂商给的"说明"和实际行为对不上。这篇记录怎么用外置串口把协议抓明白,以及五个疑点的修复——其中三个修复后来直接变成了协议设计。

一、方法论:外置串口当旁路探头

飞线把 USB 串口适配器的 RX 并联到蓝牙模块的 TX 脚——只听不打扰:主机和模块之间的对话原样进行,我在旁边抄底稿。然后让手机做一套标准动作:连接、播放、暂停、切歌、断开,把每个动作触发的原始行录下来。

比"在主机代码里加日志"强在三点:

  1. 不改任何代码——加日志本身可能改变接收时序,掩盖问题;
  2. 主机侧解析还没写好的阶段也能先积累语料,协议认识和代码实现解耦;
  3. 出争议时它是权威仲裁——“芯片到底发了没有"一锤定音。

后来查串口 RX 失聪问题(另篇)时,这个探头又立了一功:芯片在发、主机收不到,硬件和软件的责任当场切开。

二、建立词汇表

抓了两天语料,整理出 21 个词条的词汇表。每个词条三要素:样本、触发动作、语义推断。例如:

词条 样本(节选) 语义
TITLE 0,歌名 曲名(带流索引前缀)
ARTIST / ALBUM 0,歌手 艺术家/专辑
PLAYPOS 毫秒数值 播放进度(单位是毫秒!)
DURATION 毫秒数值 总时长
MEDIAINFO START … END 块 编码/采样率/比特率复合块
CONI / DISI 事件行 连接/断开事件
NUMBER 号码串 来电号码

词汇表是后续一切的根:面板显示映射、主机解析、测试用例都从它抄。

三、五个疑点与修复

1. “0,” 前缀剥离。 曲名行恒以 0, 开头——那是模块的流索引前缀,不是歌名的一部分。解析层剥掉,显示层永远不该见到它。

2. MEDIAINFO 块合并(这个成了设计)。 模块把编码/采样率/比特率拆成多行、用 START/END 包起来发。如果逐行透传,面板会闪中间态:先只看到编码、再蹦出采样率、最后比特率——一行"44.1kHz FLAC"拆成三次 UI 抖动。改成攒齐 END 才推最终形态,一次成型。协议推送的最小单位应该是"一条完整信息”,不是"一行"。

3. ms→s 转换放协议层。 PLAYPOS/DURATION 推的是毫秒,显示要 mm:ss。转换放在主机协议层而不是面板显示层——显示层不该知道单位的历史包袱,以后加第二种显示终端时不用再转一遍。

4. 0x06 参数变化事件(这个也成了设计)。 采样率/编码的变化以前靠面板轮询发现,慢半拍还费查询流量。协议增加 0x06(AUDIO_PARAMS_CHANGED)事件:主机一收到就主动把最新参数推给面板——变轮询为推送。事件驱动这招在私有协议里性价比极高:一个事件号换来一整类轮询的删除。

5. CONI/DISI 正名。 最初按字面猜"连接/断开",实测事件序列发现语义对不上。推翻重录——词汇表里每个词条都必须有"触发动作 → 观测样本"的成对证据,字面猜测一律标注"未证实",拿到证据才能转正。

四、协议权威:只有一份真相

面板和主机是两套代码各自实现同一份私有协议,最怕"两份都像真的"文档各说各话。这次立下规矩:枚举权威在下位机的协议头文件(一条词条一个常量,注释里带样本),面板要改映射、加词条,先查它再动手。文档会过期,代码里的枚举不会。

五、小结

  • 旁路探头(只听不打扰)是私有协议逆向的第一工具;
  • 词汇表三要素:样本、触发动作、语义——缺一不可;
  • 推送的最小单位是完整信息块,不是一行;
  • 能用事件推送解决的,不要留给轮询;
  • 私有协议必须有单一权威定义点,且它在代码里。