12.3. 数据包格式

在摄像头与主机之间跨越线缆的每一个字节都是某个数据包的一部分。一个数据包以一个 10 字节的报头开始,接着是一段可变长度的载荷,最后以一个 4 字节的尾部 CRC 结束。线缆上不会出现其他字节——一旦主机看到那 2 字节的同步字,接下来的字节就是按这个确切顺序排列的报头。

一个协议数据包的横向布局图,显示了 10 字节的报头 (同步字、序列号、通道 ID、标志、操作码、载荷长度、报头 CRC), 后面跟着可变长度的载荷以及一个 4 字节的载荷 CRC。

12.3.1. 报头

十个字节,无填充紧密打包。各字段:

  • sync —— 小端字节序的 16 位字 0xD5AA。线缆上的字节 0 是 0xAA,字节 1 是 0xD5。扫描字节的接收方可以通过搜索字节对 AA D5 来找到一个数据包的起点;在它之前的任何内容都被视为垃圾。这个取值的选择是有意为之的:0xAA0xD5 很少出现在可打印文本中,而且这一字节对也不太可能在载荷中间偶然出现。

  • seq —— 一个字节。一个计数器,对某一方向上发送的每一个数据包递增一。接收方检查下一个数据包的序列号是否为预期的那个;如果不是,可靠性层就请求重传。

  • chan —— 一个字节。本数据包所属的通道 ID。通道 0..31 可供使用;内置的 stdinstdoutstream 以及(可选的)profile 通道占用摄像头保留的固定 ID。

  • flags —— 一个字节。一个位字段,告诉接收方如何解释该数据包:

    • 位 0 ACK —— 本数据包是对前一个数据包的确认。

    • 位 1 NAK —— 本数据包拒绝前一个数据包。

    • 位 2 RTX —— 本数据包是一次重传。

    • 位 3 ACK_REQ —— 发送方希望本数据包得到确认。

    • 位 4 FRAGMENT —— 在一条较大的消息中,本数据包之后还有更多分片。

    • 位 5 EVENT —— 本数据包携带的是一个通道事件而非数据。

    • 位 6 和位 7 为保留位。

  • opcode —— 一个字节。命令或响应代码。协议库按用途保留操作码范围:

    • 0x00..0x0F —— 协议命令(SYNC、GET_CAPS、SET_CAPS、STATS、VERSION)。

    • 0x10..0x1F —— 系统命令(RESET、BOOT、INFO、EVENT、MEMORY)。

    • 0x20..0x2F —— 通道命令(LIST、POLL、LOCK、UNLOCK、SHAPE、SIZE、READ、WRITE、IOCTL、EVENT)。

  • len —— 两个字节,小端字节序。报头之后跟随的载荷字节数。长度为零是合法的——许多确认和小型命令不携带载荷。

  • crc —— 两个字节。对前面八个报头字节计算的 CRC-16。收到报头 CRC 错误的接收方会丢弃整个数据包,甚至都不看一眼载荷。

12.3.2. 载荷

零个或多个字节,被分帧层视为不透明数据。载荷里的内容取决于操作码:对于一个 CHANNEL_READ 回复,它是实际的通道数据;对于一个 GET_CAPS 回复,它是一个固定的小型结构;对于一次通道写入,它是主机发送的任何内容。

最大载荷大小取决于摄像头的协议缓冲区大小(参阅 protocol.init() 中的逐板对照表)。超过上限的消息会被拆分成多个分片,除最后一个外都设置 FRAGMENT 标志。

12.3.3. 尾部 CRC

四个字节,对载荷计算的 CRC-32。它能捕获报头 CRC 看不到的损坏,尤其是在较长的载荷上,否则帧中间的单比特错误会蒙混过关。

把完整性校验拆分到两个 CRC 上是有意为之的。报头 CRC 保护分帧字段本身——尤其是载荷长度。如果没有单独的报头 CRC,长度字节中的一次单比特翻转就会导致接收方为载荷读取错误数量的字节,从而与字节流彻底失同步;有了它,损坏的报头会被直接拒绝,接收方则重新扫描下一个同步字。载荷 CRC 随后作为一个独立的关注点来保护消息体,这样数据中的一次比特翻转会被报告为载荷损坏,而不会被误判为分帧错误。

这个格式小到足以逐字节地走读一遍,而且每一个数据包都具有相同布局——同步字、然后报头、然后载荷、然后 CRC——这一事实意味着一个手写的解析器只需一屏代码就能容下。这就是为什么用 C、Python 或 Rust 写一个微型主机实现是个周末项目;而协议库则是两端各自维护的 Python 版本。