# 手机传感器协议 V2

> 本文档为四木调试助手app手机传感器功能与嵌入式设备之间的交互协议。

## 1. 总体帧结构

协议使用字符 `'B'`（ASCII 0x42）开头，使用 `'\n'`（0x0A）结尾，使用 modbus-RTU 的 CRC 做校验。整体格式如下：

1. 协议头(HEAD): 1 bytes，固定 `0x42`（字符 `'B'`）。
1. 类型(TYPE): 1 bytes，范围为 0~255：
   1. `< 128`：整个报文应使用 ASCII 字符（TYPE、LENGTH、CRC、PAYLOAD 全部为可打印 ASCII 字符），用于简单的可打印输出，如使用这直接通过串口发送控制。
   1. `>= 128`：整个报文使用字节流格式（bin 格式），一般用于程序间的自行交互。
1. 长度(LENGTH): 4 bytes，使用大端格式（高字节在前），为负载(PAYLOAD)的字节数：
   1. TYPE < 128：以 4 个 ASCII 十六进制字符表示，eg：长度 100 使用 ASCII 字符 `0064`。
      特殊值 `FFFF`：**表示长度值无效**(一般用于使用者手动输入命令时不需要计算长度)。接收方不按 LENGTH 切帧，改为从 PAYLOAD 起点（帧头后第 5 字节）直接向后搜索帧尾 `0x0A`（`'\n'`），定位后自行计算内容长度（帧尾前恒有 2 字符 CRC 占位，内容长度 = 帧尾位置 − PAYLOAD 起点 − 2）。
      该模式下 PAYLOAD 必须是纯文本且**不得包含字节 `0x0A`**（JSON 文本将换行转义为 `\\n` 两个字符，天然满足）。
   1. TYPE >= 128：4 个二进制字节，高位在前，eg：长度 100 使用字节 `00 00 00 64`。
      **最高字节的最高 2 位（bit31~30）为标记位（预留用途，本版暂不定义），不参与长度计算**；有效内容长度 = 该 32 位值按位与 `0x3FFFFFFF` 后的低 30 位。
1. 负载(PAYLOAD): LENGTH 个 bytes。
1. 校验(CRC): modbus-RTU CRC16；计算范围 = TYPE + LENGTH（含表示形式）+ PAYLOAD：
   1. TYPE >= 128：完整 CRC16，2 个原始字节，高字节在前。
      **特殊值 `FF FF`（0xFFFF）：表示不做 CRC 校验**，接收方跳过校验直接接受（可用于免校验的流式/调试通道）。
   1. TYPE < 128：**仅使用 CRC16 的低位字节**（1 字节，即 1 个十六进制数值），以 2 个 ASCII 十六进制字符写入协议流（占位与二进制帧的 2 字节一致）；接收方校验时仅比较 CRC16 的低字节。
      **长度字段为 `FFFF` 的变长帧中，CRC 字段为 `FF` 表示不做 CRC 校验**；其余值按常规计算并比较（范围仍 = TYPE + LENGTH 字面量 `FFFF` + PAYLOAD）。
1. 结尾(END): `'\n'`（0x0A）。

协议解析时先根据 HEAD、TYPE、LENGTH、END 判断是否为完整帧，然后校验 CRC。

- **LENGTH 上限**：ASCII 帧常规长度最大 0xFFFE（4 个 hex 字符，`FFFF` 已保留为变长模式标记）；变长模式（LEN=FFFF）实际内容长度同样建议不超过 65535 字节；二进制帧长度字段理论 0xFFFFFFFF，因最高 2 位为标记位实际最大 0x3FFFFFFF，单帧内容仍建议不超过 65535 字节。
- **缓冲建议**：接收方累积缓冲建议 ≥ 16KB（普通帧）；「模拟屏幕」像素数据可能跨多帧累积，接收缓冲建议 ≥ 256KB。
- **帧同步**：任何一帧 HEAD/TYPE/LENGTH/END/CRC 校验失败即视为坏帧，丢弃该帧并从其后的字节重新搜索下一个 `0x42` 重同步；数据流中混有其他类型帧时按各自长度切帧跳过，不破坏帧同步。变长 ASCII 帧（LEN=FFFF）无长度信息，缓冲中尚未出现 `0x0A` 前保持等待，出现后以该 `0x0A` 为帧尾继续解析。
- **编码**：ASCII 帧 PAYLOAD 为 UTF-8/ASCII 文本；示例帧中的十六进制字节按高位在前书写。

CRC 算法：Modbus-RTU CRC16（多项式 x¹⁶+x¹⁵+x²+1，初值 0xFFFF），按字节低位先处理、逐字节迭代后，发送/写入时高 8 位在前（详见附录 A）。

## 2. 类型分配总览

| TYPE | 名称 | 方向 | 格式 | 章节 |
|---|---|---|---|---|
| `0x61`（`'a'`） | 输入输出、模拟手柄 | 双向 | ASCII，JSON | 3 |
| `0x62`（`'b'`） | 图表 | 设备 → App | ASCII，JSON | 4 |
| `0x63`（`'c'`） | 运动传感器、更多传感器、GNSS | App → 设备 | ASCII，JSON | 5、8 |
| `0x64`（`'d'`） | 红外控制 | 设备 → App | ASCII，JSON | 9 |
| `0x65`（`'e'`） | 事件通知 | App → 设备 | ASCII，JSON | 10 |
| `0x66`（`'f'`） | 二维码 | 双向 | ASCII，JSON | 11 |
| `0x67`（`'g'`） | 手机通知 / 语音播报 | 设备 → App | ASCII，JSON | 12 |
| `0x81`（129） | 模拟屏幕 | 双向 | 二进制，子类型 | 6 |
| `0x82`（130）~ | 摄像头 / 麦克风 / 喇叭（建议，待确认） | 双向 | 二进制 | 7 |
| 其他 | 预留 | — | — | — |

## 3. 输入输出、模拟手柄

类型(TYPE)：`'a'`（0x61）
负载(PAYLOAD)：**压缩后的 JSON**（不带空格换行的 json），UTF-8。方向双向：App → 设备为控制（滑条/旋钮/按键/手柄），设备 → App 为状态上报（LED/显示/震动输入）。

### 3.1 控制（App → 设备）

左侧模拟量名称：`V1`~`V4`（垂直滑动条）、`H1`~`H4`（水平滑动条）；右侧按键名称：`B1`~`B10`；中间状态名称：`LED1`~`LED8`、`DS1`~`DS6`。

| 键 | 数量 | 值类型 | 说明 |
|---|---|---|---|
| `V1`~`V4` | 4 | 十进制浮点 | 垂直滑动条值；App 按本地配置 min/max 钳制后发送（默认 0~100） |
| `H1`~`H4` | 4 | 十进制浮点 | 水平滑动条值；语义同上 |
| `B1`~`B6` | 6 | 0 / 1 | 普通按钮：按下 `1`、释放 `0`；自锁按钮发送当前闭合状态（`1`=闭合，`0`=断开） |
| `B7`、`B9` | 2 | 0 / 1 | 二值切换器：`0`=关，`1`=开 |
| `B8`、`B10` | 2 | 0 / 1 / 2 | 三值切换器：`0`/`1`/`2` = 低/中/高 |

手柄名称：`A`、`B`、`X`、`Y`、`L1`、`R1`、`L2`、`R2`、`SELECT`、`START`（按下 `1`，释放 `0`）。
手柄左侧摇杆名称：`LDX`、`LDY`；右侧摇杆名称：`RDX`、`RDY`。摇杆值为 int8 有符号（-128~127），中心 0，前推为正；死区内的偏移钳制为 0。

eg：用户输入状态信息，未提及的控件表示不修改：

```json
{
    "LED1": 1,
    "DS3": 20.6
}
```

eg：滑条、按键输出：

```json
{
    "H1": 74,
    "B8": 2,
    "A": 1,
    "LDX": -30
}
```

- 滑动条/旋钮连续拖动时发送节流（≤ 50ms 一帧），松手补发一次最终值；按钮按下/抬起、档位切换即时发送，不节流。
- 手柄按键/摇杆状态变化（超死区）时立即发送一帧；无变化不发送（周期快照模式可选支持）。

### 3.2 状态上报（设备 → App）

| 键 | 数量 | 值类型 | 说明 |
|---|---|---|---|
| `LED1`~`LED8` | 8 | 0~7 | LED 指示：`0`=灭，`1`=红，`2`=橙，`3`=黄，`4`=绿，`5`=青，`6`=蓝，`7`=紫 |
| `DS1`~`DS6` | 6 | 十进制浮点 | 浮点数字显示值（小数位数由 App 本地配置，默认 6 位） |

eg：设备上报：

```json
{
    "LED1": 2,
    "DS3": -12.5
}
```

### 3.3 震动输入

设备 → App 方向（并入 `'a'` 帧）。App 收到后调用系统振动器执行震动；「震动反馈」功能关闭时忽略。

| 键 | 值类型 | 说明 |
|---|---|---|
| `VIB` | 0~255 | 震动强度；`0` 表示停止当前震动 |
| `DUR` | 0~255 | 震动持续时间，单位 100ms（如 `10` = 1 秒）；`0` 表示使用 App 默认时长（200ms） |

eg：

```json
{
    "VIB": 200,
    "DUR": 10
}
```

### 示例

通过串口直接发送此帧就可控制输入输出LED
```txt
BaFFFF{"LED2":6,"DS2":17.3}FF\n
```
## 4. 图表

类型(TYPE)：`'b'`（0x62）
方向：**设备 → App**
负载(PAYLOAD)：压缩后的 JSON（UTF-8）。

图表名称（JSON 键）：`A`~`D`（最多 4 个图表），值类型为**数值数组**（十进制浮点，逗号语法由 JSON 表达）。数组元素个数 = 本帧追加的波形条数（1~8），按位置对应：第 i 个值 → 波形 i；**超过 8 个忽略多余**。每收到一帧，各波形的数据点追加 1 个时间点。

- 一帧可含多个图表键（如同时给图表 A 与 C 追加数据）。
- 键非法（非 A~D）时忽略该键；某数值解析失败时丢弃该数值（保留其他合法值）；JSON 整体解析失败时整帧丢弃。

eg：图表 A 画三个波形，每个波形各追加 1 个点 20.4 / 16 / 55：

```json
{
    "A": [20.4, 16, 55]
}
```

eg：同时追加到图表 C（4 条波形）：

```json
{
    "C": [20.3, 20.5, 40.8, 25.6]
}
```

### 示例

```txt
BbFFFF{"C":[20.3, 20.5, 40.8, 25.6]}FF\n
```

## 5. 运动传感器、更多传感器

类型(TYPE)：`'c'`（0x63）
方向：**App → 设备**
负载(PAYLOAD)：压缩后的 JSON（UTF-8）。

传感器名称（JSON 键）：**尽量短**，使用小写缩写；运动传感器输出三轴数组 `[x, y, z]`，环境/水平传感器为标量或角度数组。一帧可含任意组合的键；**仅包含「输出使能」且硬件存在的传感器**（缺硬件的传感器可省略该键，设备端视为无效/不更新）。

| 键 | 内容 | 含义 | 单位 | 量程/说明 |
|---|---|---|---|---|
| `acc` | `[x, y, z]` | 加速度计三轴 | g | 原始 m/s² ÷ 9.80665 |
| `gyr` | `[x, y, z]` | 陀螺仪三轴 | dps（°/s） | 原始 rad/s × 57.29578 |
| `mag` | `[x, y, z]` | 磁力计三轴 | μT | — |
| `temp` | 标量 | 温度 | ℃ | 0.01 精度 |
| `humi` | 标量 | 湿度 | %RH | 0.01 精度 |
| `press` | 标量 | 气压 | hPa | 0.01 精度 |
| `prox` | 标量 | 距离 | cm | 0.1 精度 |
| `light` | 标量 | 光线 | lux | — |
| `level` | `[azimuth, pitch, roll]` | 水平（旋转向量换算） | ° | 方位 0~360（0 = 正北）；俯仰 ±180；横滚 ±90；设备平放时俯仰/横滚趋近 0 |
| `time` | 字符串 | 时间戳（「更多传感器 → 时间输出」项） | — | 格式 `YYYY-MM-DD hh:mm:ss`（24 小时制、设备本地时区）；值为 JSON 字符串（非数值） |

- 数值为十进制浮点，App 限位输出（**与页面显示一致，够用即止**）：`acc` 3 位、`gyr` 2 位、`mag` 1 位、`temp`/`humi`/`press` 2 位、`prox` 1 位、`light` 0 位、`level` 角度 1 位小数；裁剪尾随零、整数值不带小数位（如 `23.5`、`320`）。接收端按浮点解析（精度损失小于硬件量化分辨率，可忽略）。
- 各传感器按自身刷新频率独立节流；任一传感器节流到期时发送一帧，内含**全部输出使能传感器的最新值**（与旧版 0x23 帧节奏一致）。
- 接收端处理：JSON 整体解析失败整帧丢弃；未知键忽略（向前兼容）。
- **时间输出**（`time` 键）独立于运动/环境传感器：仅在「更多传感器 → 时间输出」使能时按所选频率（`1s`/`5s`/`10s`/`30s`/`1min`/`10min`/`1h`/`12h`）单独发送一帧 `{"time":"YYYY-MM-DD hh:mm:ss"}`；`time` 值为字符串，不参与上面的浮点限位规则。

eg（加速度 + 水平同时输出）：

```json
{
    "acc": [0.02, -0.01, 9.81],
    "level": [123.45, 0.2, -1.5]
}
```

eg（仅环境传感器输出）：

```json
{
    "temp": 23.45,
    "humi": 42.5,
    "press": 1013.25,
    "prox": 1.2,
    "light": 320
}
```

eg（时间输出，每选定频率发送一帧；变长帧 LEN=`FFFF`、CRC=`FF` 免校验）：

```txt
BcFFFF{"time":"2026-09-25 14:30:05"}FF\n
```

## 6. 模拟屏幕

类型(TYPE)：129（0x81，二进制）

负载(PAYLOAD) = 子类型(SUB_TYPE) 1 字节 + 子负载(SUB_PAYLOAD)。子负载根据 SUB_TYPE 解析：

| SUB_TYPE | 名称 | 方向 |
|---|---|---|
| 1 | 窗口设置 | 设备 → App |
| 2 | 指针返回 | App → 设备 |
| 3 | 屏幕数据（未压缩） | 设备 → App |
| 4 | 屏幕数据（压缩，LZSS） | 设备 → App |
| 5 | 纯色刷屏 | 设备 → App |
| 6~15 | 屏幕数据（压缩，其他算法，预留） | 设备 → App |

- 模拟屏幕的坐标原点为屏幕左上角，单位为**逻辑像素**（App 按比例缩放显示）。
- 屏幕内容按「窗口 + 刷屏方向 + 颜色格式」解析：窗口参数默认取 App 本地设置（默认 320×240、RGB565、从左到右横向刷），**收到 SUB 1 窗口设置帧后以帧内声明为准**（设备可随时重发）。

### 6.1 窗口设置

子类型(SUB_TYPE)：1
子负载(SUB_PAYLOAD)：14 字节，全部字段**高位在前**：

| 字段 | 长度 | 说明 |
|---|---|---|
| 宽度 | 2 | 窗口逻辑像素宽；`0` 表示沿用当前（仅更新区域） |
| 高度 | 2 | 窗口逻辑像素高；`0` 表示沿用当前 |
| 颜色格式 | 1 | `0`=RGB565、`1`=BGR565、`2`=RGB888；`0xFF` 表示沿用当前 |
| 刷屏方向 | 1 | `0`=从左到右横向刷、`1`=从右到左横向刷、`2`=从上到下纵向刷、`3`=从下到上纵向刷；`0xFF` 表示沿用当前 |
| 区域 X | 2 | 刷屏区域左上角 X；区域为 0×0 时表示整个窗口 |
| 区域 Y | 2 | 刷屏区域左上角 Y |
| 区域 W | 2 | 刷屏区域宽；`0` 表示区域为（X, Y）到窗口右下 |
| 区域 H | 2 | 刷屏区域高；`0` 表示区域为（X, Y）到窗口右下 |

- 窗口尺寸/颜色格式/方向变化时，App 重建像素缓冲并清屏（未收到数据时显示不透明黑）。
- 区域用于**局部刷新**（如仅更新某个子窗口）：收到后刷屏光标移到区域起点，后续像素数据只在区域内行进。
- 兼容旧版 0x10 用法：只更新区域时，宽度/高度填 `0`、颜色格式填 `0xFF`、刷屏方向填 `0xFF`。

### 6.2 指针返回

子类型(SUB_TYPE)：2
子负载(SUB_PAYLOAD)：触摸指针信息，支持多点触控，每触点固定 6 字节：

| 字段 | 长度 | 说明 |
|---|---|---|
| 状态 | 1 | `0x01`=按下（按下及移动过程均发送 0x01）；`0x02`=抬起 |
| X | 2 | 触摸点 X 坐标，**高位在前**，逻辑像素 |
| Y | 2 | 触摸点 Y 坐标，**高位在前**，逻辑像素 |
| ID | 1 | 触点 ID（0~255），多点触控区分各触点 |

- 多个触点按序排列（子负载长度 = 触点个数 × 6）。
- 仅触摸点落在窗口**内部**时发送；区域外触摸不发送。受 App「触摸反馈」开关控制。

### 6.3 屏幕数据未压缩

子类型(SUB_TYPE)：3
子负载(SUB_PAYLOAD)：**纯像素流**（无附加头）。

- 像素字节数由颜色格式决定：RGB565 / BGR565 每像素 2 字节（BGR565 小端字节序：低字节在前，位布局仍为 r5g6b5），RGB888 每像素 3 字节（顺序 R、G、B）。像素数量 = 子负载长度 ÷ 每像素字节数。
- 像素从**当前刷屏光标**开始按刷屏方向线性写入，规则（区域为 6.1 节声明，写满区域后回到区域起点循环覆盖）：
  - `0` 从左到右横向刷：每行从左到右，行自上而下；
  - `1` 从右到左横向刷：每行从右到左，行自上而下；
  - `2` 从上到下纵向刷：每列自上而下，列自左向右；
  - `3` 从下到上纵向刷：每列自下而上，列自左向右。
- 窗口设置帧到达后，光标回到区域起点（起点取该刷屏方向下的区域首角）。单帧建议 ≤ 4096 字节（均衡开销与实时性），上限 65535。

### 6.4 屏幕数据压缩

由于数据量过大，建议使用压缩传输。**不同压缩算法以不同子类型区分**（每个算法一个子类型）；子负载**全部为数据内容**——不含内部长度字段（压缩后长度由帧 LENGTH 给出）也不含算法标识（算法由子类型确定）；解压后像素字节数以解码到子负载结束时的累计输出为准。

子类型(SUB_TYPE)：4 = LZSS 压缩（本协议定义，见下）；6~15 预留其他压缩算法（新增算法时从 6 依次分配）。
子负载(SUB_PAYLOAD)：纯压缩数据流（LZSS 编码）。

压缩前先把像素按 6.3 节顺序展开为**像素字节流**（RGB565/BGR565 每像素 2 字节、RGB888 每像素 3 字节），再对像素字节流做 LZSS 压缩。

**LZSS 编码**（LZ77 改进：仅当滑动窗口内存在长度 ≥ 3 字节的重复时才编码为回溯匹配对，否则输出字面量）：

- **bit 流**：MSB 优先（字节内先写高位）；每个编码段以 1 bit 标志开头：`0` = 字面量，`1` = 回溯匹配对。
- **字面量段**：标志 `0` + 8 bit 原样字节。
- **匹配段**：标志 `1` + 距离 12 bit + 长度 8 bit：
  - 距离：当前编码位置向前回溯的字节数（1~4096），字段写入「距离 − 1」（0~4095，高在前）；编码/解码滑动窗口固定 **4096 字节**。
  - 长度：匹配字节数（3~258），字段写入「长度 − 3」（0~255）；不足 3 字节的重复按字面量输出。
- **结束**：解码从子负载首 bit 持续至子负载末尾，累计输出字节数即本次写入的像素字节数（像素数 = 字节数 ÷ 每像素字节数）；最后字节不足 8 bit 时高位补 `0`，解码端忽略。
- 解码端维护同尺寸滑动窗口（初始全 `0`），按标志逐段解码，按 6.3 节刷屏规则从当前光标写入像素，写满区域后回到区域起点循环。
- 编码端匹配搜索范围为窗口内 4096 字节（朴素搜索或哈希均可，输出一致）；优先取最长匹配，长度相同时取更近匹配。
- 若窗口未声明颜色格式（无法确定像素字节流与像素的划分），接收方丢弃该帧。

eg（RGB565 像素字节流前 4 字节为 0x12 0x34 0x56 0x78，其后 4 字节与回溯 4 字节处内容相同）：
编码段序列 = `0` `0x12` | `0` `0x34` | `0` `0x56` | `0` `0x78` | `1` 距离 4（12 bit 写入 3）| 长度 4（8 bit 写入 1）
bit 流（MSB 优先）= `0 00010010 | 0 00110100 | 0 01010110 | 0 01111000 | 1 000000000011 | 00000001`（共 4×9 + 21 = 57 bit）

### 6.5 纯色刷屏

子类型(SUB_TYPE)：5
方向：设备 → App
子负载(SUB_PAYLOAD)：**1 个像素的颜色字节**，字节顺序与 6.3 节像素流内单个像素完全一致（RGB565 / BGR565 为 2 字节、RGB888 为 3 字节），无附加头。

- 收到后把**整个窗口**所有像素填充为该颜色（等效于发送一整帧同色像素流，常用于清屏/背景切换），随后刷屏光标回到当前区域起点，后续像素流可继续写入。
- 子负载长度必须等于该颜色格式的每像素字节数，否则整帧丢弃；窗口未声明颜色格式时同样丢弃。

## 7. 摄像头、扬声器、麦克风

后续开发（负载草案见下，类型编号为**建议值，待确认**）。音视频数据量大且含二进制内容，按总则必须使用 **TYPE ≥ 128** 的二进制帧（原规划 0x30/0x31/0x40/0x41/0x50/0x51 与 ASCII/二进制判定规则冲突，作废）。

| TYPE | 名称 | 方向 | 内容格式 |
|---|---|---|---|
| `130`（0x82） | 摄像头图像数据 | App → 设备 | 格式(1)=`0x10` + 图像ID(1) + 分片序号(2,高在前) + 总字节数(4,高在前) + JPEG数据(n)，见 7.1 |
| `131`（0x83） | 摄像头控制 | 设备 → App | 控制码(1)，见 7.2 |
| `132`（0x84） | 麦克风音频数据 | App → 设备 | 编码(1)=`0x20` + PCM16 采样数据(n)，小端字节序，见 7.3 |
| `133`（0x85） | 麦克风音频格式 | App → 设备 | 编码(1)=`0x20` + 采样率(2,高在前) + 位深(1)=16 + 声道数(1)=1，见 7.3 |
| `134`（0x86） | 喇叭音频数据 | 设备 → App | 编码(1)=`0x20` + PCM16 采样数据(n)，小端字节序，见 7.4 |
| `135`（0x87） | 喇叭播放控制 | 设备 → App | 编码(1)=`0x20` + 采样率(2,高在前) + 位深(1)=16 + 声道数(1) + 音量(1,0~100)，见 7.4 |

### 7.1 摄像头图像（type 130，App → 设备）

单帧拍照 / 连续预览流：相机采集帧编码为 JPEG 后**分片**发送，同一张 JPEG 拆为多片，接收方按图像ID 重组。内容（固定头 8 字节）：

| 字段 | 长度 | 说明 |
|---|---|---|
| 格式 | 1 | 固定 `0x10`（JPEG） |
| 图像ID | 1 | 同一张图像的所有分片共用；每张新图像 +1（0~255 循环） |
| 分片序号 | 2 | **高位在前**，从 0 开始 |
| 总字节数 | 4 | **高位在前**，整张 JPEG 的字节数（每片均携带，据此判定完成） |
| 数据 | n | JPEG 字节流分片，n ≤ 分片大小（默认 1024） |

- 分片序号 = 0 为首片；按图像ID 建缓冲、按序号写入；累计字节 ≥ 总字节数判定整图完成。
- 图像ID 变化、分片序号异常跳变或总字节数不一致时丢弃旧缓冲重新开始。

### 7.2 摄像头控制（type 131，设备 → App）

| 字段 | 长度 | 说明 |
|---|---|---|
| 控制码 | 1 | `0x01`=拍照（立即采集 1 帧发送）；`0x02`=开始预览流；`0x03`=停止预览流 |

### 7.3 麦克风（type 132/133，App → 设备）

手机麦克风实时采集 **PCM16 音频流**（小端字节序），**采集开始时先发送一次格式帧**（type 133，内容：编码(1) = `0x20` + 采样率(2,高在前) + 位深(1) = 16 + 声道数(1) = 1；参数变化时重发），随后连续发送数据帧（type 132，编码(1) = `0x20` + 纯 PCM16，n ≤ 1024）。流式无序号/无重传，丢帧直接跳过。

### 7.4 喇叭（type 134/135，设备 → App）

手机播放设备下发的 PCM16 音频流：type 134 数据帧（编码(1) = `0x20` + 纯 PCM16，小端字节序，n ≤ 1024）到达即写入播放缓冲（溢出时丢弃最旧数据，实时性优先）；type 135 控制帧（编码(1) = `0x20` + 采样率 2 + 位深 1 + 声道数 1 + 音量 1（0~100））在播放流开始前或参数变化时发送，App 以最近一次收到的控制帧为准（本地设置为兜底默认）。

## 8. GNSS

后续开发。规划方向：手机 GPS 定位数据周期上报（App → 设备），使用 ASCII JSON 帧 type `'c'`（0x63，与运动 / 环境传感器共用，见第 5 章），负载如：

```json
{
    "lat": 31.2304,
    "lng": 121.4737,
    "alt": 12.3,
    "speed": 0.5,
    "sat": 11
}
```

## 9. 红外控制

类型(TYPE)：`'d'`（0x64）
方向：**设备 → App**
负载(PAYLOAD)：压缩后的 JSON（UTF-8）。

App「红外控制」页内置 16 个红外按钮（本地配置每个按钮的载波频率 / 位序 / 引导码 / 逻辑电平 / 数据，或 RAW 时长数组），设备可下发命令触发指定按钮用手机红外发射器发射。

| 键 | 值类型 | 说明 |
|---|---|---|
| `IR` | 整数 1~16 | 触发发射第 N 个已存储红外按钮（序号从 1 起）；越界或非法值忽略，无 `IR` 键不动作 |

eg：触发第 3 个红外按钮发射：

```json
{
    "IR": 3
}
```

### 示例

通过串口 / TCP 直接发送此帧即可触发（变长帧，LEN=`FFFF`、CRC=`FF` 免校验）：

```txt
BdFFFF{"IR":3}FF\n
```

- **前置条件**：① 启动转发引擎且输出通道含已启用的「手机传感器」；② 手机具备红外发射硬件；③ 在「红外控制 → 更多设置」中打开「输入控制」开关（关闭时 App 丢弃该帧）。
- JSON 整体解析失败整帧丢弃；未知键忽略（向前兼容）。
- 一帧只触发一个按钮；如需连发多个，设备端按序发送多帧。

## 10. 事件通知

类型(TYPE)：`'e'`（0x65）
方向：**App → 设备**
负载(PAYLOAD)：压缩后的 JSON（UTF-8）。

App 在输入/输出通道连接状态变化、转发引擎启停等事件发生时向设备上报通知帧。通知帧统一经**当前输入通道**发送（与传感器数据同一通道）。

| 键 | 值类型 | 说明 |
|---|---|---|
| `notify` | 字符串 | 事件来源名称：`input`=输入通道；`engine`=转发引擎（启停）；输出通道使用其在 App 中配置的**通道名称**（如 `output1` 或用户自定义名） |
| `status` | 整数 | 事件状态，见下表 |
| `errno` | 整数 | 失败原因码（仅连接类事件有意义，成功/无关时为 `0`），见下表 |

`status` 取值：

| 值 | 含义 |
|---|---|
| `-1` | 连接失败 / 错误 |
| `0` | None（空闲 / 已停止） |
| `1` | 连接成功（引擎事件时表示「已启动」） |
| `2` | 连接中 |
| `3` | 监听中（TCP server 等待接入） |
| `4` | 重连中 |

`errno` 取值：

| 值 | 含义 |
|---|---|
| `0` | 无错误 |
| `1` | 连接被拒绝（connection refused） |
| `2` | 连接超时（timeout） |
| `3` | 主机不可达（unreachable / no route） |
| `4` | 连接被对端断开（disconnected） |
| `5` | 其他错误 |

- 引擎启停：启动发送 `{"notify":"engine","status":1,"errno":0}`，停止发送 `{"notify":"engine","status":0,"errno":0}`。
- 通道连接成功发送 `status=1`、失败发送 `status=-1` 并附对应 `errno`。
- **去重**：仅在通道状态发生**变化**时发送一帧，状态未变化不重复发送。
- **使能开关**：输入配置页、输出列表页、首页分别提供独立的通知使能开关（输入通道通知 / 输出通道通知 / 引擎启停通知）；对应开关关闭时不发送该类通知。开关为页面级设置，切换即时生效并持久化。
- JSON 整体解析失败整帧丢弃；未知键忽略（向前兼容）。

### 示例

输入通道连接成功（变长帧，LEN=`FFFF`、CRC=`FF` 免校验）：

```txt
BeFFFF{"notify":"input","status":1,"errno":0}FF\n
```

输出通道 `output1` 连接失败（连接被拒绝）：

```txt
BeFFFF{"notify":"output1","status":-1,"errno":1}FF\n
```

## 11. 二维码

类型(TYPE)：`'f'`（0x66），**双向复用**；ASCII 变长帧（LEN=`FFFF`、CRC=`FF` 免校验，同第 10 章）。内容按 UTF-8 编码（可含中文 / 网址等非 ASCII 字符）。

手机传感器页提供两个二维码功能：**扫描**（标题栏「扫一扫」图标入口）与**显示**（功能列表「二维码」条目入口）。

### 11.1 扫描发送（App → 设备）

扫描页识别二维码 / 条码后，点击「发送」把内容经输入通道发往设备：

```txt
BfFFFF{"qr":"<扫描内容>"}FF\n
```

| 键 | 类型 | 说明 |
|---|---|---|
| `qr` | 字符串 | 扫描到的原始内容（文本 / 网址等） |

- 扫描页保留最近 **5 条**历史（去重、最新在前），可逐条复制或发送。
- 引擎未运行时点击发送不生效（内部丢弃）。

### 11.2 显示接收（设备 → App）

设备下发内容，显示页在 **2×2 共 4 个展示位**渲染二维码：

```txt
BfFFFF{"qr1":"内容1","qr2":"内容2","qr3":"内容3","qr4":"内容4"}FF\n
```

| 键 | 对应展示位 |
|---|---|
| `qr1` | 第 1 个（左上） |
| `qr2` | 第 2 个（右上） |
| `qr3` | 第 3 个（左下） |
| `qr4` | 第 4 个（右下） |

- 键为 `qr` + 序号（1~4）；可只下发部分键（仅更新对应展示位，其余保持不变）。
- 无数字后缀的 `qr` 键（扫描发送格式）在显示接收中忽略。
- 展示位内容也支持页面**手动编辑**（点击「编辑」输入文本 / 网址）；手动内容与协议下发内容共用同一组展示位，均持久化保存。
- **接收开关**：显示页提供「接收显示」开关，关闭时忽略设备下发的 `'f'` 帧，不更新展示位。
- 点击任一二维码**全屏放大**，底部展示其内容（可选中复制）。
- JSON 整体解析失败整帧丢弃；未知键忽略（向前兼容）。

### 示例

设备下发 4 个展示位：

```txt
BfFFFF{"qr1":"https://a.com","qr2":"https://b.com","qr3":"HELLO","qr4":"12345"}FF\n
```

扫描到网址后发送到设备：

```txt
BfFFFF{"qr":"https://example.com"}FF\n
```

## 12. 手机通知 / 语音播报

类型(TYPE)：`'g'`（0x67）
方向：**设备 → App**
负载(PAYLOAD)：压缩后的 JSON。ASCII 变长帧（LEN=`FFFF`、CRC=`FF` 免校验，同第 10/11 章）。JSON 内容**编码自动识别**：优先严格 UTF-8，失败（非法字节序列）回退 **GBK**，兼容嵌入式设备常用编码，设备无需声明编码；内容可含中文。

同一 `'g'` 帧通过 `sub` 子类型区分两种功能：**手机通知**（`sub`=0 或缺省）与**语音播报 TTS**（`sub`=1）。

| 键 | 值类型 | 必填 | 适用子类型 | 说明 |
|---|---|---|---|---|
| `sub` | 整数 | 否 | 通用 | 子类型：`0`=手机通知（缺省）、`1`=语音播报（TTS） |
| `title` | 字符串 | 否 | 通知 | 通知标题 |
| `text` | 字符串 | 否 | 通知 / TTS | 通知（`sub`=0）：通知内容；语音播报（`sub`=1）：待播报文字 |
| `id` | 整数 | 否 | 通知 | 通知 ID：相同 `id` 更新同一条通知；缺省时 App 自动分配（每帧独立通知） |
| `cancel` | 0/1 或布尔 | 否 | 通知 | 真（`1`/`true`）= 撤销指定 `id` 的通知（不弹出）；缺省 `0` |
| `level` | 整数 | 否 | 通知 | 重要性：`0`=静默、`1`=普通（默认）、`2`=重要（横幅）；声音/横幅由系统通知渠道重要性决定（Android 8+） |

**手机通知（`sub`=0 / 缺省）**：App 调用系统通知能力在手机通知栏弹出。受手机传感器页「手机通知」开关控制；Android 13+ 需用户授予通知权限（POST_NOTIFICATIONS），未授权时不弹出。
- `title` 与 `text` 均为空时忽略该帧。
- `cancel` 为真时仅撤销 `id` 对应通知（`id` 缺省按 `0` 处理）。
- 点击通知返回 App。

**语音播报 TTS（`sub`=1）**：App 调用系统语音引擎（TextToSpeech）朗读 `text`。受手机通知页「语音播报」开关控制（开启时初始化引擎、关闭时释放）。
- `text` 为空或缺失 = 停止当前播报并清空待播队列。
- 引擎初始化期间收到的文字入队，就绪后按序补播；多段文字按到达顺序排队朗读（QUEUE_ADD）。
- 语言优先中文，引擎缺失中文数据时退回系统默认语言；系统未安装 TTS 引擎时初始化失败（详情页显示「语音引擎不可用」），不影响通知等其他功能。

**通用**：JSON 整体解析失败整帧丢弃；未知键忽略（向前兼容）。通知开关与语音播报开关相互独立，均关闭时该帧被丢弃。

### 示例

弹出一条普通通知（`sub` 缺省即 0）：

```txt
BgFFFF{"title":"温度告警","text":"当前温度 45℃ 超过阈值","level":1}FF\n
```

更新 id=5 的通知（相同 id 覆盖，静默）：

```txt
BgFFFF{"title":"下载进度","text":"已完成 60%","id":5,"level":0}FF\n
```

撤销 id=5 的通知：

```txt
BgFFFF{"id":5,"cancel":1}FF\n
```

语音播报一段中文（`sub`=1）：

```txt
BgFFFF{"sub":1,"text":"温度过高，请注意散热"}FF\n
```

停止语音播报（`sub`=1，`text` 为空）：

```txt
BgFFFF{"sub":1,"text":""}FF\n
```

> 说明：变长帧 LEN 固定 `FFFF`、CRC 固定 `FF`（免校验），帧尾 `0x0A`（`\n`）定位结束；上例末尾 `\n` 即帧尾字节。GBK 编码的 JSON 帧结构相同，App 自动识别。

