feat: support 17-byte ADC status from updated vendor protocol

Protocol update adds FFE4 Status format: 7×PD sensors (little-endian)
+ VBAT battery voltage (mV) + XOR checksum = 17 bytes.

- parseVendorStatus: handles 1-byte heartbeat, 17-byte ADC, 33-byte params
- handleValueChange: ADC type extracts PD array + battery % from VBAT
- Battery calculated as linear 3000mV(0%) to 4200mV(100%)
- PD data passed through status event for treatment pages
- Updated protocol doc with full byte table and old/new comparison
这个提交包含在:
Guoguo
2026-05-19 03:54:15 -07:00
父节点 0feb900a1d
当前提交 3fd7578163
修改 3 个文件,包含 85 行新增84 行删除
+43 -80
查看文件
@@ -1,28 +1,15 @@
# 协议简述补充解析
来源:仓库根目录 `协议简述.docx`,已归档到 `docs/protocols/协议简述.docx`
来源:仓库根目录 `协议简述(1).docx`(更新版),已归档到 `docs/protocols/`
本文只记录该补充文档的可读内容和与当前实现的差异判断。原 DOCX 内容较短,主要描述 `FFE1` 数据通信服务下的控制/状态特征,以及一组 33 字节控制参数。
## 原文抽取
### 服务/功能摘要
| UUID | 名称 | 说明 |
| --- | --- | --- |
| `FFE1` | 数据通信服务 | 控制指令 |
| `FFE4` | 设备状态及 ADC 值 | 数据上报 |
### 特征值摘要
## 服务/特征值摘要
| UUID | 名称 | 属性 | 说明 |
| --- | --- | --- | --- |
| `FFE1` | `Command` | `Write+Read` | 下发控制指令 |
| `FFE4` | `Status` | `Read + Notify` | 数据上报 |
| `FFE1` | Command | Write+Read | 下发 33 字节控制指令 |
| `FFE4` | Status | Read+Notify | 上报 17 字节 ADC/电池数据 |
> 注意:补充文档将 `FFE1` 同时写作“数据通信服务”和 `Command` 特征 UUID;这与当前代码中的 `FFE1` 服务、`FFE4` 命令特征、`FFE5` 状态特征不一致,需硬件侧确认最终 UUID 分配。
### Command 33 字节定义
## Command 参数(FFE1 Write,33 字节)
| 字节 | 含义 |
| --- | --- |
@@ -32,78 +19,54 @@
| Byte4 | IO1 暖黄灯光强 |
| Byte5 | IO1 电流增益高字节 |
| Byte6 | IO1 电流增益低字节 |
| Byte7 | IO2 红灯光强 |
| Byte8 | IO2 红外灯光强 |
| Byte9 | IO2 紫外灯光强 |
| Byte10 | IO2 暖黄灯光强 |
| Byte11 | IO2 电流增益高字节 |
| Byte12 | IO2 电流增益低字节 |
| Byte13 | IO3 红灯光强 |
| Byte14 | IO3 红外灯光强 |
| Byte15 | IO3 紫外灯光强 |
| Byte16 | IO3 暖黄灯光强 |
| Byte17 | IO3 电流增益高字节 |
| Byte18 | IO3 电流增益低字节 |
| Byte19 | IO4 红灯光强 |
| Byte20 | IO4 红外灯光强 |
| Byte21 | IO4 紫外灯光强 |
| Byte22 | IO4 暖黄灯光强 |
| Byte23 | IO4 电流增益高字节 |
| Byte24 | IO4 电流增益低字节 |
| Byte25 | IO5 红灯光强 |
| Byte26 | IO5 红外灯光强 |
| Byte27 | IO5 紫外灯光强 |
| Byte28 | IO5 暖黄灯光强 |
| Byte29 | IO5 电流增益高字节 |
| Byte30 | IO5 电流增益低字节 |
| Byte7-12 | IO2(同 IO1 结构) |
| Byte13-18 | IO3 |
| Byte19-24 | IO4 |
| Byte25-30 | IO5 |
| Byte31 | 保持时间高字节 |
| Byte32 | 保持时间低字节 |
| Byte33 | 异或校验 |
| Byte33 | 异或校验Byte1-Byte32 |
## 对当前实现的影响
每个 IO 为 6 字节:红灯(1) + 红外(1) + 紫外(1) + 暖黄(1) + 电流增益(2, 大端)。
当前小程序 BLE 实现位于 `miniprogram/services/ble/`。它采用的是帧协议:
## Status 上报(FFE4 Notify,17 字节)— 新增
```text
0xAA 0x55 | length | type | payload | XOR checksum
```
| 字节 | 含义 | 字节序 |
| --- | --- | --- |
| Byte1-2 | PD1 ADC 值 | 小端(低字节在前) |
| Byte3-4 | PD2 ADC 值 | 小端 |
| Byte5-6 | PD3 ADC 值 | 小端 |
| Byte7-8 | PD4 ADC 值 | 小端 |
| Byte9-10 | PD5 ADC 值 | 小端 |
| Byte11-12 | PD6 ADC 值 | 小端 |
| Byte13-14 | PD7 ADC 值 | 小端 |
| Byte15-16 | VBAT 电池电压 | 小端(单位 mV) |
| Byte17 | 异或校验(Byte1-Byte16 |
并且 `setParams()` 当前下发的 payload 是:
7 个 PD(光电传感器)通道 + 1 个电池电压通道。
```text
region_mask | wavelength | brightness | duration_ms(4B) | mode | seq
```
## 与旧版协议简述的差异
补充文档描述的 Command 更像“裸 33 字节参数块”,没有出现 `0xAA 0x55` 帧头、命令 type、seq、ACK 或 `region_mask`。当前因赶进度先以该 33 字节协议为准,旧帧协议暂作为历史兼容代码保留。
| 项目 | 旧版 | 新版(当前) |
| --- | --- | --- |
| FFE4 上报格式 | 仅说"设备状态及 ADC 值",无字节定义 | 完整定义:7×PD + VBAT + XOR,共 17 字节 |
| FFE1 Command | 33 字节(相同) | 33 字节(不变) |
| PD 通道数 | 未知 | 7 个通道 |
| 电池信息 | 未知 | VBAT,小端 mV |
| 字节序 | 未标注 | Status 明确小端序 |
主要差异如下:
## 对代码的影响
| 项目 | 当前代码/文档 | 补充文档 | 影响 |
| --- | --- | --- | --- |
| 命令写入格式 | 帧格式 `0xAA55 + length + type + payload + checksum` | 33 字节参数块 | 需要确认设备固件实际接收哪一种格式 |
| 数据通信 UUID | `FFE1` 服务,`FFE4` 命令,`FFE5` 状态 | `FFE1` 服务/Command,`FFE4` Status | UUID 表存在冲突,需要硬件确认 |
| 治疗参数模型 | 区域 mask + 单一波长 + 全局亮度 | IO1-IO5 各自四种灯光强度 + 电流增益 | 当前 UI/协议无法表达每个 IO 的独立参数 |
| 时间字段 | `duration_ms` 4 字节大端毫秒 | 保持时间 2 字节 | 时间单位、范围、字节序均需确认 |
| 状态上报 | 当前解析 14 字节状态 | 仅说明“设备状态及 ADC 值” | 需要补齐 Status 字节表,尤其 ADC 字段 |
| 校验 | 当前 XOR 覆盖帧头/长度/type/payload | Byte33 为异或校验 | 需确认 Byte33 是否 XOR Byte1-Byte32 |
### 已更新
## 需要补充确认的问题
- `protocol.js``parseVendorStatus()` 增加 17 字节解析:提取 7 个 PD 值 + VBAT,校验 XOR
- `connection.js``handleValueChange()` 处理 `type === 'adc'`:从 VBAT 计算电量百分比,PD 数据通过 `status` 事件传递
优先级从高到低:
### 仍待确认
1. `FFE1``FFE4``FFE5` 的最终含义:哪个是服务 UUID,哪个是 Command characteristic,哪个是 Status characteristic。
2. Command 写入时是否仍需要 `0xAA 0x55` 帧封装,还是直接写 33 字节参数块。
3. Byte33 XOR 校验范围:是否对 Byte1-Byte32 做 XOR,初始值是否为 `0x00`
4. Byte31-Byte32 保持时间的单位:秒、100ms、分钟,或其他单位;以及字节序是否为高字节在前。
5. IO1-IO5 与面部区域/灯板通道的对应关系。
6. 红灯、红外、紫外、暖黄光强的取值范围和含义:是否为 PWM、百分比、DAC 值,范围是 `0-100` 还是 `0-255`
7. 电流增益高/低字节的取值范围、字节序和单位。
8. Status/ADC 数据上报的完整字节布局:状态码、电量、温度、ADC 通道数、每通道字节序和单位。
9. 是否存在开始、停止、查询状态、绑定等命令字;如果没有,33 字节写入是否同时承担“设置参数并启动”的语义。
10. 是否需要 ACK/NACK;如果有,ACK 格式和错误码表是什么。
## 建议后续处理
- 当前已先接入 `buildVendorCommand33()`,由“区域 + 单一波长 + 全局亮度”生成 IO1-IO5 的默认 33 字节参数。
- 旧帧协议暂保留,后续如供应商补齐旧协议或要求兼容,可再做协议模式切换。
- 真机联调时优先抓取/打印实际写入的字节数组和 Status notify 原始数据,再补 Status/ADC 解析。
1. PD1-PD7 与面部区域的对应关系
2. PD 值的物理含义(光照强度?反射率?)和有效范围
3. VBAT 值的精度(是否直接 mV?是否需要系数换算?)
4. 电量百分比计算方式(当前假设 3000mV=0%,4200mV=100%,可能不准确)
5. 上报频率(当前实测是每秒一次,但可能固件更新后改变)
6. 是否有 start/stop 命令,还是全 0 的 33 字节表示停止
+24 -2
查看文件
@@ -85,11 +85,33 @@ function handleValueChange(res) {
if (protocol.PROTOCOL_MODE === 'vendor_33') {
var parsed = protocol.parseVendorStatus(res.value)
emit('vendor_raw', parsed)
if (parsed.is_heartbeat) {
if (parsed.type === 'heartbeat') {
emit('heartbeat', { value: parsed.heartbeat })
return
}
if (parsed.ios) {
if (parsed.type === 'adc') {
var vbatMv = parsed.vbat
var batteryPct = Math.min(100, Math.max(0, Math.round((vbatMv - 3000) / (4200 - 3000) * 100)))
emit('status', {
mode_state: protocol.MODE_STATE.ACTIVE,
region_mask: protocol.REGION.FULL_FACE,
wavelength: 0,
brightness: 0,
remaining_ms: 0,
error_code: 0,
battery: batteryPct,
temperature: 0,
bind_status: 1,
subscription: 1,
pd: parsed.pd,
vbat: vbatMv
})
return
}
if (parsed.type === 'params' && parsed.ios) {
var anyActive = false
for (var i = 0; i < parsed.ios.length; i++) {
if (parsed.ios[i].red || parsed.ios[i].infrared || parsed.ios[i].uv || parsed.ios[i].warm_yellow) {
+18 -2
查看文件
@@ -197,7 +197,22 @@ function buildVendorCommand33(options) {
function parseVendorStatus(buffer) {
var bytes = bufferToBytes(buffer)
if (bytes.length === 1) {
return { heartbeat: bytes[0], is_heartbeat: true, raw_hex: bytesToHex(bytes) }
return { type: 'heartbeat', heartbeat: bytes[0], is_heartbeat: true, raw_hex: bytesToHex(bytes) }
}
if (bytes.length === 17) {
var checksum = xorChecksum(bytes.slice(0, 16))
var pds = []
for (var p = 0; p < 7; p++) {
pds.push(bytes[p * 2] | (bytes[p * 2 + 1] << 8))
}
return {
type: 'adc',
is_heartbeat: false,
pd: pds,
vbat: bytes[14] | (bytes[15] << 8),
checksum_ok: checksum === bytes[16],
raw_hex: bytesToHex(bytes)
}
}
if (bytes.length === 33) {
var ios = []
@@ -210,6 +225,7 @@ function parseVendorStatus(buffer) {
})
}
return {
type: 'params',
is_heartbeat: false,
ios: ios,
hold_time: (bytes[30] << 8) | bytes[31],
@@ -217,7 +233,7 @@ function parseVendorStatus(buffer) {
raw_hex: bytesToHex(bytes)
}
}
return { is_heartbeat: false, raw_bytes: bytes, raw_hex: bytesToHex(bytes) }
return { type: 'unknown', is_heartbeat: false, raw_bytes: bytes, raw_hex: bytesToHex(bytes) }
}
// --- frame encoding/decoding ---