# Lumen Code v1 协议规范

| 项目 | 内容 |
|---|---|
| 协议 | Lumen Code，第 1 版 |
| 状态 | 公开评审草案 |
| 日期 | 2026-10-17 |
| 英文版（规范性文本） | [PROTOCOL.md](PROTOCOL.md)（采用 ASD-STE100 简化技术英语） |
| 测试向量 | [test/vectors/test-vectors.json](../test/vectors/test-vectors.json) |
| 参考实现 | JavaScript（`src/core/`）、Rust（`src/rust/`） |
| 许可证 | MIT（本文档及参考实现） |

> 本文与英文版逐节对应。两者如有出入，以英文版为准。

---

## 1 范围

Lumen Code 用"屏幕 → 相机"的方式传输文件。发送端连续显示一系列帧，每一帧是一个由彩色格子组成的网格；接收端用相机拍摄这些帧并解码。

协议不需要网络、不需要配对、也没有回传通道。接收端可以在任意时刻开始，可以丢帧。每一帧都携带新的数据，接收端不需要收到某个特定的帧。

本文规定：

- 帧格式（网格、定位图案、帧头、码字、颜色）；
- 数据包格式与文件描述符；
- 生成数据包流的喷泉码；
- 发送端流程。

本文还给出接收端的建议做法。接收端可以使用任意检测算法。

## 2 约定

### 2.1 要求用语

MUST（必须）、MUST NOT（禁止）、SHOULD（应当）、SHOULD NOT（不应）、MAY（可以）的含义遵循 RFC 2119。标注为"资料性"的章节和说明不包含要求。

### 2.2 数与运算

- 除非另有说明，所有整数都是无符号整数。
- `floor(a / b)` 是整数除法；`ceil(a / b)` 等于 `floor((a + b − 1) / b)`。
- `a mod b` 是余数，取值范围 `[0, b)`。
- `XOR`、`AND`、`>>`、`<<` 是位运算。
- "32 位运算"指每一步结果都对 2^32 取模。
- `bitlen(k)` 是 `k` 的二进制位数，例如 `bitlen(1) = 1`、`bitlen(4) = 3`。
- 素数是大于等于 2、且除 1 和自身外没有其他因数的整数。

### 2.3 字节序与位序

- 数据包和描述符中的多字节字段使用大端序。
- 由字节构成的位序列，从第一个字节的最高位开始。
- 由位序列构成的整数，第一个位是最高位。

### 2.4 术语

| 术语 | 含义 |
|---|---|
| 格子（cell） | 网格中的一个方块，是所有帧尺寸的单位 |
| 网格（grid） | 一帧中的 X × Y 个格子 |
| 帧（frame） | 发送端显示的一幅图像 |
| 墨色、纸色（ink、paper） | 深色和浅色两种参考颜色 |
| 数据格子 | 承载码字比特的格子 |
| 槽位（slot） | 数据格子在数据顺序（§4.6）中的位置 |
| 码字（codeword） | 一个 n 字节的 Reed–Solomon 码字 |
| 数据包（packet） | 码字中的消息部分（k = n − nsym 字节） |
| 符号（symbol） | 喷泉码的一个编码符号，T 字节 |
| 传输（transfer） | 承载同一个文件的所有帧，共用一个传输 ID |
| 源块（source block） | 载荷中由喷泉码独立编码的一部分 |

## 3 总览（资料性）

```
文件 ──(可选 DEFLATE)──► 载荷 ──切分──► 源块（每块 ≤ 16384 个 T 字节符号）
                                              │ 喷泉码（§9）
                                              ▼
       文件描述符 ──► 数据包：[传输 ID | M,SBN,ESI | T 字节 | CRC-16]
                                              │ Reed–Solomon（§7）
                                              ▼
                               码字 ──比特──► 数据格子（Hilbert 顺序，加扰）
                                              │ + 定位图案、角标、校准点、帧头
                                              ▼
                                   帧（彩色格子网格）──► 屏幕 ──► 相机
```

接收端按相反方向执行这些步骤。收到一个完整的数据包就能使用其中的数据。对每个源块，收到约 K + 2 个不同的数据包即可恢复该源块。

## 4 网格

### 4.1 尺寸与坐标

网格有 X 列、Y 行。X 和 Y 必须在 40 到 511 之间。

格子 `(x, y)` 位于第 x 列、第 y 行，`(0, 0)` 在左上角。格子编号为 `i = y · X + x`。

每个格子属于一种类型：固定、帧头、校准点或数据。§4.2 至 §4.5 按顺序分配类型，未分配的格子都是数据格子。各项分配互不重叠。

### 4.2 定位图案与角标

网格有三个定位图案（finder）和一个角标（corner pattern），每个都是 7 × 7 的固定格子。

| 图案 | 左上角格子 |
|---|---|
| 定位图案 TL | (0, 0) |
| 定位图案 TR | (X − 7, 0) |
| 定位图案 BL | (0, Y − 7) |
| 角标 BR | (X − 7, Y − 7) |

对格子 `(ox + i, oy + j)`（`i, j ∈ [0, 7)`），计算 `d = max(|i − 3|, |j − 3|)`：

- 定位图案中，`d = 3` 或 `d ≤ 1` 的格子是纸色，其余是墨色；
- 角标中，`d = 3` 或 `d = 0` 的格子是纸色，其余是墨色。

穿过定位图案中心的直线依次经过纸、墨、纸、墨、纸，比例为 1 : 1 : 3 : 1 : 1；穿过角标中心的比例为 1 : 2 : 1 : 2 : 1。

### 4.3 分隔带

每个图案的内侧有一条墨色分隔带。对 `i` 从 0 到 7（含两端），把下列格子设为固定墨色：

```
(7, i)  (i, 7)  (X−8, i)  (X−1−i, 7)  (7, Y−1−i)  (i, Y−8)  (X−8, Y−1−i)  (X−1−i, Y−8)
```

### 4.4 帧头副本

网格中有帧头（§6）的四个副本，每个副本 70 个帧头格子。令 `r = floor(b / 10)`、`c = b mod 10`，副本的第 `b` 位位于：

| 副本 | 位置 | 第 b 位所在格子 |
|---|---|---|
| 0（A） | TL 右侧 | (8 + c, r) |
| 1（B） | TL 下方 | (r, 8 + c) |
| 2（C） | TR 左侧 | (X − 18 + c, r) |
| 3（D） | BL 上方 | (r, Y − 18 + c) |

位值为 1 的帧头格子是纸色，位值为 0 的是墨色。

### 4.5 校准点

校准点（alignment marker）构成点阵。沿长度为 D（X 或 Y）的轴，点阵位置按下式计算：

```
span = D − 7
segs = max(1, floor((span + 9) / 18))
while segs > 1 and span < 16 · segs:
    segs = segs − 1
for i = 0 … segs:
    L[i] = 3 + floor((2 · i · span + segs) / (2 · segs))
```

点阵点为 `(LX[i], LY[j])`。点阵的四个角点正好是三个定位图案和角标的中心，这四个点不放校准点。

其余每个点阵点放一个 3 × 3 的校准点：中心格子为纸色，周围 8 个为墨色。

### 4.6 数据格子与数据顺序

其余格子都是数据格子，其排列次序称为数据顺序，计算方法：

1. 找到满足 `N ≥ max(X, Y)` 的最小 2 的幂 `N`；
2. 对 `d = 0 … N·N − 1`，计算 `(x, y) = hilbert(N, d)`（附录 A.4）；
3. 若 `x < X`、`y < Y` 且 `(x, y)` 是数据格子，则把它追加到数据顺序。

格子在数据顺序中的序号就是它的槽位，`D` 是数据格子总数。

> 资料性：Hilbert 曲线让每个码字集中在一块紧凑区域内。当帧被撕裂（相机读到一半旧帧、一半新帧）时，只有撕裂线经过的码字会丢失。

### 4.7 加扰

每个槽位 `s` 有一个加扰值（32 位运算）：

```
w[s] = mix32(0x9E3779B9 XOR ((s + 1) · 0x85EBCA6B)) AND 7
```

`mix32` 见附录 A.1。

## 5 颜色模式

颜色模式决定每格比特数（bpc）和调色板，颜色值为名义 sRGB。

| 模式 | 名称 | bpc | 调色板（索引 → 颜色） |
|---|---|---|---|
| 0 | C2 | 1 | 0 墨色，1 纸色 |
| 1 | C4 | 2 | 0 墨色，1 (0, 216, 255)，2 (255, 40, 150)，3 (255, 214, 0) |
| 2 | C8 | 3 | 0 墨色，1 (44, 80, 255)，2 (26, 222, 96)，3 (0, 220, 255)，4 (255, 52, 76)，5 (250, 52, 220)，6 (255, 214, 10)，7 纸色 |

- 墨色为 (10, 13, 20)，纸色为 (242, 244, 248)。
- 模式 3 保留。接收端必须拒绝模式为 3 的帧头。
- C8 中索引的三个比特对应 (R, G, B)，一个颜色通道判断错误只造成一个比特错误。
- 发送端应当使用名义颜色，可以为适配显示器做小幅调整。
- 接收端不得依赖精确的名义颜色，应当在每一帧中实测颜色（§11）。

> 资料性：推荐默认使用 C4。C8 每格多 50% 比特，但需要对焦良好、摩尔纹较少。

## 6 帧头

### 6.1 字段

帧头共 70 位，字段依次为：

| 位数 | 字段 | 取值 |
|---|---|---|
| 4 | version | 1 |
| 2 | mode | 0、1 或 2（§5） |
| 9 | X | 40 … 511 |
| 9 | Y | 40 … 511 |
| 8 | n | 码字长度，24 … 255 |
| 7 | nsym / 2 | 校验字节数的一半 |
| 8 | seq | 帧序号，0 … 255 |
| 7 | reserved | 0 |
| 16 | CRC | 前 54 位的 CRC-16/CCITT-FALSE |

把前 54 位作为位序列计算 CRC（附录 A.3），然后按高位在前追加 16 位 CRC。

### 6.2 掩码

每个副本 `c`（0 到 3）使用不同的掩码。写入副本 `c` 之前，把帧头第 `i` 位与下式做 XOR（32 位运算）：

```
m[c][i] = mix32(0x4C554D45 + i + 977 · c) AND 1
```

> 资料性：副本 B 是副本 A 的转置。如果接收端以交换坐标轴的方式读取镜像图像，会在期望副本 A 的位置读到副本 B。不同的掩码使这种情况下 CRC 校验失败，接收端随后会尝试正确的方向。

### 6.3 有效性

满足下列任一条件时，接收端必须拒绝该帧头：

- CRC 错误；
- version 不为 1；
- mode 为 3；
- X < 40 或 Y < 40；
- n < 24，或 nsym < 2，或 nsym ≥ n。

发送端必须把保留位置 0，接收端必须忽略保留位。

### 6.4 帧序号

发送端每显示一个新帧，必须把 `seq` 加 1（模 256）。初值不作规定，发送端应当使用随机初值。

## 7 码字

### 7.1 Reed–Solomon 码

每个码字 n 字节，其中消息 `k = n − nsym` 字节、校验 nsym 字节。nsym 必须为偶数。

- 域为 GF(2^8)，本原多项式 x^8 + x^4 + x^3 + x^2 + 1（0x11D），本原元 α = 2。
- 生成多项式 `g(x) = (x − α^0)(x − α^1) … (x − α^(nsym−1))`。
- 采用系统码：码字 = 消息 + `message(x) · x^nsym` 除以 `g(x)` 的余式。
- 码字第 `i` 字节是 `x^(n−1−i)` 的系数。

### 7.2 每帧码字数

```
cpc = ceil(8 · n / bpc)        每个码字占用的格子数
m   = floor(D / cpc)           每帧码字数
```

发送端选择的 n 必须使 `m ≥ 1`。

### 7.3 码字到格子

码字 `j`（0 ≤ j < m）占用槽位 `j · cpc` 到 `(j + 1) · cpc − 1`：

1. 由 n 个码字字节构成位序列（§2.3），末尾补零到 `cpc · bpc` 位；
2. 对 `t = 0 … cpc − 1`，把第 `t · bpc` 到 `t · bpc + bpc − 1` 位读作整数 `v`；
3. 槽位 `s = j · cpc + t` 的值为 `v XOR (w[s] AND (2^bpc − 1))`。

数据格子的值是调色板索引（§5），发送端显示该索引对应的颜色。

### 7.4 填充

槽位 `m · cpc` 到 `D − 1` 是填充，按下式计算（32 位运算）：

```
r = mix32(seq + 0x01234567)
for s = m · cpc … D − 1:
    r = mix32(r + s)
    value[s] = r AND (2^bpc − 1)
```

接收端必须忽略填充槽位。

## 8 数据包

### 8.1 格式

每个码字的消息部分就是一个 `k = n − nsym` 字节的数据包：

| 字节 | 字段 |
|---|---|
| 0–1 | 传输 ID（TID） |
| 2 | 第 7 位：M（元数据标志）；第 6–0 位：SBN（源块号） |
| 3–4 | ESI（编码符号 ID） |
| 5 … k − 3 | 载荷，`T = k − 7` 字节 |
| k − 2 … k − 1 | 字节 0 … k − 3 的 CRC-16/CCITT-FALSE |

- 发送端选择的 n、nsym 必须使 `T ≥ 16`。
- 同一传输中 T 不得改变。
- 接收端必须丢弃 CRC 错误的数据包。

> 资料性：带擦除的 Reed–Solomon 译码可能接受错误的码字，数据包 CRC 防止错误数据包破坏喷泉解码器。

### 8.2 数据包（M = 0）

M 为 0 时是数据包，载荷是源块 SBN 的第 ESI 个喷泉符号（§9）。

### 8.3 元数据包（M = 1）

M 为 1 时是元数据包，SBN 必须为 0，ESI 表示元数据索引：

- 索引 0 是文件描述符（§8.4）；
- 其他索引保留，接收端必须忽略。

### 8.4 文件描述符

文件描述符占满元数据包 0 的 T 个载荷字节：

| 字节 | 字段 |
|---|---|
| 0 | 描述符版本 = 1 |
| 1 | flags；第 0 位 = 载荷为 raw DEFLATE；其他位为 0 |
| 2–5 | 载荷长度 L |
| 6–9 | 文件大小 |
| 10–13 | 文件的 CRC-32（附录 A.3） |
| 14 | 文件名长度 N |
| 15 … 14 + N | 文件名，UTF-8 |
| 文件名之后 | TLV 记录（可选） |
| 其余 | 0 |

- N 不得超过 `min(255, T − 15)`。文件名过长时，发送端必须在 UTF-8 字符边界截断。
- TLV 记录由类型字节、长度字节和 `length` 个值字节组成，类型 0 表示列表结束。
- 第 1 版未定义任何 TLV 类型，接收端必须忽略未知类型的 TLV。
- 接收端必须忽略版本不为 1 的描述符。

## 9 载荷、源块与喷泉码

### 9.1 载荷

flags 第 0 位为 1 时，载荷是用 raw DEFLATE（RFC 1951）压缩后的文件，否则载荷就是文件本身。发送端可以压缩；接收端必须能解压。

### 9.2 源符号与源块

```
Kt = max(1, ceil(L / T))           源符号总数
Z  = ceil(Kt / 16384)              源块数，必须 ≤ 128
KS = floor(Kt / Z)
ZL = Kt − KS · Z
K[b]     = b < ZL ? KS + 1 : KS
start[b] = K[0] + … + K[b − 1]
```

全局第 `i` 个源符号是载荷的第 `i · T` 到 `i · T + T − 1` 字节，载荷末尾之后用 0 补齐。源块 `b` 包含源符号 `start[b]` 到 `start[b] + K[b] − 1`。

### 9.3 种子

每个源块有独立种子（32 位运算）：

```
seed[b] = mix32(TID · 65537 + 0x51ED + b · 0x9E3779B9)
```

### 9.4 参数

对于有 K 个源符号的源块：

```
X  = 满足 X · (X − 1) ≥ 2 · K 的最小整数 X ≥ 2
S  = max(3, ceil(K / 100) + X)；然后把 S 逐次加 1，直到 K + S 为素数
W  = K + S
P  = 12 + bitlen(K)
P1 = 大于 P 的最小素数
L  = W + P
```

### 9.5 中间符号

编码器生成 L 个 T 字节的中间符号 `C[0] … C[L − 1]`，`⊕` 表示逐字节 XOR：

1. 对 `i = 0 … K − 1`：`C[i]` = 该源块的第 `i` 个源符号；
2. 对 `h = 0 … P − 1`：`C[W + h]` = `PI(h)` 中所有 `i` 的 `C[i]` 的 ⊕；
3. 对 `s = 0 … S − 1`：`C[K + s]` = `LDPC(s)` 中所有 `i` 的 `C[i]` 的 ⊕，再 ⊕ `C[W + (s mod P)]` ⊕ `C[W + ((s + 1) mod P)]`。

第 2 步必须在第 3 步之前完成。

`PI(h)` 是源符号的一个伪随机半数子集：

```
rng = Rng(mix32(seed XOR ((h + 1) · 0x5BD1E995)))
for i = 0, 32, 64, … while i < K:
    bits = rng.next()
    for t = 0 … min(32, K − i) − 1:
        if (bits >> t) AND 1 = 1: 把 i + t 加入 PI(h)
```

`LDPC(s)` 由下列过程分配：

```
for i = 0 … K − 1:
    a = 1 + (floor(i / S) mod (S − 1))
    if (2 · a) mod S = 0: a = 1
    b = i mod S
    把 i 加入 LDPC(b)、LDPC((b + a) mod S)、LDPC((b + 2 · a) mod S)
```

### 9.6 编码符号

ESI 为 `x` 的编码符号等于列表 LT 中每个 `j` 的 `C[j]`、以及列表 PI 中每个 `p` 的 `C[W + p]` 的 ⊕。两个列表按下式计算：

```
rng = Rng(mix32(seed XOR ((x + 1) · 0x9E3779B1)))
v = rng.next() AND 0xFFFFF
d = 满足 v < DEG[d] 的最小 d ≥ 1
d = max(1, min(d, W − 2))
a = 1 + (rng.next() mod (W − 1))
b = rng.next() mod W
LT = [b]；再重复 d − 1 次：b = (b + a) mod W；把 b 追加到 LT
若 d < 4：d1 = 2 + (rng.next() AND 1)，否则 d1 = 2
a1 = 1 + (rng.next() mod (P1 − 1))
b1 = rng.next() mod P1
PI = []；重复 d1 次：
    while b1 ≥ P: b1 = (b1 + a1) mod P1
    把 b1 追加到 PI
    b1 = (b1 + a1) mod P1
```

`rng.next()` 的调用顺序必须与上面完全一致；计算 d1 的那次调用只在 `d < 4` 时发生。

`DEG` 表（下标 0 到 30）：

```
0, 5243, 529531, 704294, 791675, 844104, 879057, 904023, 922747, 937311, 948962,
958494, 966438, 973160, 978921, 983914, 988283, 992138, 995565, 998631, 1001391,
1003887, 1006157, 1008229, 1010129, 1011876, 1013490, 1014983, 1016370, 1017662, 1048576
```

> 资料性：该码为非系统码。接收端用任意约 K + 2 个不同符号即可解出源块。参考实现的解码器采用"剥离 + 失活（inactivation）"，再对失活部分做 GF(2) 高斯–约当消元。

## 10 发送端流程

### 10.1 开始传输

1. 生成载荷（§9.1），计算文件 CRC-32；
2. 选择 X、Y、颜色模式、n、nsym，确保 `m ≥ 1` 且 `T ≥ 16`；
3. 随机选择 16 位 TID；
4. 计算源块（§9.2）及每个源块的中间符号（§9.5）；
5. 生成文件描述符（§8.4）。

### 10.2 每一帧

1. 生成 m 个数据包；
2. 满足下列任一条件时，把描述符包（M = 1，SBN = 0，ESI = 0）放在第一个：
   - `Kt ≤ 2 · m`；
   - 帧计数是 4 的倍数；
3. 其余位置放数据包。使用从 0 开始的全局包计数 `g`：第 `g` 个包属于源块 `b = g mod Z`，`ESI = floor(g / Z) mod 65536`，每放一个数据包 `g` 加 1；
4. 附加数据包 CRC（§8.1），把每个数据包编码为码字（§7.1）；
5. 生成格子：固定格子、校准点、带当前 seq 的四个帧头副本（§6）、码字（§7.3）、填充（§7.4）；
6. 显示该帧，seq 与帧计数加 1。

第 2、3 步中的描述符规则和包顺序是建议做法。接收端必须接受任意的数据包顺序。

### 10.3 传输过程中的调整

发送端可以在帧之间改变 X、Y、颜色模式、n、nsym，但 T 必须保持不变，因为接收端以 TID 和 T 识别传输。要改变 T，必须用新的 TID 开始新的传输。

### 10.4 n 与 nsym 的选择（资料性）

参考发送端的做法：

```
m0 = max(1, ceil(D / ceil(8 · 255 / bpc)))
n  = 255; while n > 24 and m0 · ceil(8 · n / bpc) > D: n = n − 1
nsym = max(4, 2 · floor((n · pct + 100) / 200))
```

纠错等级低、中、高、很高分别对应 `pct` = 10、16、22、30，默认 16。

### 10.5 显示（资料性）

- 每个格子使用整数个设备像素，不少于 3 个。
- 网格四周保留至少 2 个格子的墨色静区。
- 图案和格子可以使用圆角，格子之间可以留缝；缝必须是墨色；格子颜色在每个方向上至少覆盖格子中心的一半。
- 帧率要与接收端的拍摄能力匹配，参考发送端默认每秒 12 帧。
- 根据屏幕物理尺寸选择格子大小。参考发送端每格约 0.75 mm，短边限制在 52 … 150 格，网格铺满显示区域，宽高比不超过 2.4。

## 11 接收端（资料性）

接收端可以采用任意算法。参考实现的流程：

1. 用游程扫描寻找定位图案（1:1:3:1:1）和角标（1:2:1:2:1）的候选；
2. 使用四个角中的任意三个，在每个定位图案处测量格子尺寸，读取帧头副本；CRC 不对时，尝试下一个副本、其他格子尺寸或镜像方向；
3. 从各角出发逐步寻找校准点：对所有已知点做最小二乘单应性拟合，并用邻近点的残差修正轻微镜头畸变；
4. 采样每个数据格子，用附近校准点的局部墨色、纸色归一化其颜色；
5. 以名义调色板为初值做 k-means 颜色分类，修正局部增益（摩尔纹、光照不均）后再分类一次；
6. 对每个码字做 RS 译码；失败时把最不可靠的字节作为擦除重试，并保留至少 nsym / 2 字节冗余；
7. 只保留 CRC 正确的数据包；
8. 网格已定位、但没有一份帧头副本正确时，可以沿用同一传输中上一个正确的帧头：一次传输中 X、Y、mode、n、nsym 不变，而解数据不需要 seq。尺寸检查、校准点和数据包 CRC 会发现用错的帧头。

建议所有接收端遵守：

- 以 (TID, T) 识别一个传输；
- 忽略重复数据包，键为 (TID, T, SBN, ESI)；
- 在收到描述符之前先保存数据包；
- 各源块独立解码；
- 在把文件交给用户之前，核对结果的大小与 CRC-32；
- 若某帧的所有码字都已解出，不再重复解码该帧，键为 (seq, X, Y, mode, n, nsym)。

## 12 可扩展性

- 帧头中的 version 字段允许定义新的帧格式；
- 帧头保留位允许在第 1 版中增加帧选项，接收端忽略它们；
- 保留的元数据索引和 TLV 类型允许增加文件信息；
- 描述符 flags 的第 1 到 7 位保留；
- 新的调色板需要新的 mode 值（mode 3 已保留）。

## 13 一致性

- 一致的发送端：在相同输入（文件、TID、seq 初值、X、Y、颜色模式、纠错等级）下，生成的帧与测试向量逐位一致。
- 一致的接收端：能接受一致的发送端可能生成的所有帧和数据包序列。
- [test/vectors/test-vectors.json](../test/vectors/test-vectors.json) 提供以下各项的值：CRC-32、CRC-16、mix32、Rng、Reed–Solomon、网格布局、帧头比特、码字选择、喷泉码参数、种子、符号列表、符号、源块划分、数据包、描述符，以及两个完整的帧。

---

## 附录 A 算法

### A.1 mix32

```
mix32(x):                                    32 位运算
    x = (x XOR (x >> 16)) · 0x7FEB352D
    x = (x XOR (x >> 15)) · 0x846CA68B
    return x XOR (x >> 16)
```

校验值：`mix32(1) = 1753845952`，`mix32(0x51ED) = 1153879395`。

### A.2 Rng

```
Rng(seed): state = seed
next():    state = state + 0x9E3779B9        32 位运算
           return mix32(state)
```

### A.3 校验和

| 名称 | 多项式 | 初值 | 反射 | 结果异或 | 校验值（"123456789"） |
|---|---|---|---|---|---|
| CRC-32 | 0x04C11DB7 | 0xFFFFFFFF | 是 | 0xFFFFFFFF | 0xCBF43926 |
| CRC-16/CCITT-FALSE | 0x1021 | 0xFFFF | 否 | 0x0000 | 0x29B1 |

帧头 CRC 逐位计算：

```
crc = 0xFFFF
对每一位 bit：
    top = ((crc >> 15) AND 1) XOR bit
    crc = (crc << 1) AND 0xFFFF
    if top = 1: crc = crc XOR 0x1021
```

### A.4 Hilbert 曲线

```
hilbert(N, d):                    N 为 2 的幂
    x = 0; y = 0; t = d; s = 1
    while s < N:
        rx = 1 AND (t >> 1)
        ry = 1 AND (t XOR rx)
        if ry = 0:
            if rx = 1: x = s − 1 − x; y = s − 1 − y
            交换 x 与 y
        x = x + s · rx
        y = y + s · ry
        t = t >> 2
        s = s · 2
    return (x, y)
```

## 附录 B 设计说明（资料性）

- **纯色格子**：每格一种颜色，解码简单、耐模糊；调色板在相机 RGB 空间中保持较大的颜色间距。
- **墨底浅色图案**：画面以深色为主，观看舒适，相机曝光稳定。
- **自适应网格**：发送端按屏幕物理尺寸选择网格。手机屏幕使用更少、更大的格子，使相机在其对焦距离上仍能分辨。
- **四角任取三角**：眩光、模糊或画面边缘可能遮挡一个角，接收端用其余三个角继续解码。
- **校准点阵**：每 16 到 18 格一个校准点，局部修正透视和镜头畸变。
- **紧凑码字**：Hilbert 顺序把每个码字放在一个区域内，卷帘快门撕裂和局部缺陷只影响部分码字。
- **喷泉码**：每个数据包都是新信息，接收端不需要特定帧，也不需要回传通道。
- **数据包 CRC**：拦截 RS 擦除译码可能接受的错误数据包。
- **源块**：每块最多 16384 个符号，限制小设备上解码器的内存与耗时。
