# 故障录波 MQTT 协议观察 > 整理日期:2026-09-29;最近修订:2026-09-30。本文只记录报文证据、协议描述和待验证项。功能目标见 [requirements.md](requirements.md),整体设计见 [technical-design.md](technical-design.md)。协议描述可能过时;发生冲突时以真实报文为准。 > 2026-10-12 补充:逐字段结构和本轮用户确认见 [协议详细描述](protocol-specification.md)。用户已明确 `377/378` 不能依靠 `snno` 关联;服务端实现的 `379/380` 可使用 `snno` 关联,具体赋值规则仍待补充。下文保留原样本事实和当时的观察,不将新约定改写为旧抓包行为。 > 2026-10-14 补充:业务层字段已由 [FaultFilter_MQTT协议文档](FaultFilter_MQTT协议文档.md) 定义。下文中与之冲突的旧描述已更正并标注;实报样本事实不变,且与该文档一致。 > 2026-10-16 补充:新增 [2026-10-14 样本](#2026-10-14-样本),包含文件上传消息 `607~610` 和可解析的 `hexData`。用户确认下载地址规律,并决定本工具只监听平台发起的上传。 ## 证据与范围 | 标记 | 含义 | | --- | --- | | 实报 | 用户提供的 `CloudLog_2026-09-29_063029.csv` 中的 8 条真实通讯记录(附件未纳入仓库);以及 [CloudLog_2026-10-14_104644.csv](CloudLog_2026-10-14_104644.csv) 中的 24 条记录 | | 已验证代码 | 用户确认 `service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go` 已实现的部分协议结构可信 | | FaultFilter 文档 | 设备侧故障录波服务的业务协议文档;定义 `dataArea` 内的业务字段,不覆盖 `377~380` 外层 | | 描述 | 用户提供的旧协议说明;与 FaultFilter 文档冲突的部分已作废,其余未被实报或已验证代码覆盖的细节仍待验证 | 本工具只发送 `377`,其余消息(`378`、`379`、`380`、`607~610`)全部监听;`380`、`607`、`610` 由 FWH 平台发出(设计决策,2026-10-16,见 [technical-design.md](technical-design.md))。2026-09-29 日志只覆盖 `cmd=0` 和 `cmd=2`、设备类型 `aGate`、成功结果。`cmd=1`、`cmd=3`、`pe` 设备类型及失败结果没有实报样本。 ## 报文外层与消息方向 实报中的四类消息均有 `cmdType`、`equipNo`、`type`、`timeStamp`、`snno`、`len`、`crc`、`dataArea` 字段。样本的 `equipNo` 均为 `10060010A01X26310190`,`type=0`。现有发送代码将 `len` 设为序列化后 `dataArea` 的字节长度,并计算其 CRC32 IEEE 大写十六进制值;接收报文是否始终采用同一计算方式,尚未单独验证。 | `cmdType` | 方向 | 作用 | `dataArea` | | --- | --- | --- | --- | | `377` | 服务端 → 设备 | 故障录波操作指令 | `cmd`、`data` | | `378` | 设备 → 服务端 | 对 `377` 的应答 | `cmd`、`result` | | `379` | 设备 → 服务端 | 故障录波信息上传请求 | `cmd`、`result`、按命令出现的 `list` / `filename` | | `380` | 服务端 → 设备 | 对 `379` 的应答 | `cmd`、`result` | `378` 是对指令的应答,业务数据或文件处理结果出现在后续 `379`。不能只凭 `378.result=0` 判定录波数据已经取得。CSV 没有 MQTT topic;当前客户端代码发送到 `fwh/s2c/mng/{sn}`,等待 `fwh/c2s/mng/{sn}` 的应答,但 `379/380` 的实际 topic 仍需确认。 `379.dataArea` 的 `result/list/filename` 与 FaultFilter 文档回复中 `data` 对象的内容一致,只是没有 `data` 包裹,推断 `379` 承载的就是 FaultFilter 回复。 ## `377`:服务端请求 `dataArea` 的形状为 `{ "cmd": <整数>, "data": { "list": [...] } }`。`list` 和每项的 `info` 均为数组。以下字段按每个 `list[]` / `info[]` 项列出。 | `cmd` | 作用 | `list[].type` | `list[].info[]` | 证据 | | --- | --- | --- | --- | --- | | `0` | 获取实时数据;`code=3` 即故障录波总目录 | 实报为 `aGate` | `sn` 字符串;`codeList` 整数数组(`1~3`;`4/5` 只能经 `cmd=2` 获取) | `aGate`、`codeList=[3]` 为实报;`code` 字典来自 FaultFilter 文档 | | `1` | 设置参数 | 已验证代码发送 `pe` 或 `aGate` | `sn` 字符串;`paraList` 为 `{id: 功能码, setVal: 十六进制字符串}` 数组 | 已验证代码;本次无实报 | | `2` | 获取记录 | 实报为 `aGate` | `sn` 字符串;`index` 整数数组,元素为目录中的记录序号 | `aGate`、`index=[9]` 为实报;旧描述的范围 `1~16` 未获 FaultFilter 文档支持,以目录为准 | | `3` | 查询参数 | — | `sn` 字符串;`idList` 功能码数组 | 仅 FaultFilter 文档,无实报 | | `4` / `90` / `99` | 退出/连通性测试 | — | `data` 为 `{}` | 仅 FaultFilter 文档,无实报 | 旧描述将部分 `type` 或 `sn` 写为 `string[]`,而真实报文及已验证代码对应的是**单个字符串字段**。旧描述列出的 `agate`、`mppt`、`apbox` 不在 FaultFilter 文档中;业务层设备类型只有 `pe` 和 `aGate`。 ## `378`:设备对 `377` 的应答 `dataArea` 包含 `cmd` 和整数 `result`。描述称 `result=0` 为成功,`result=-1` 为失败。实报覆盖 `cmd=0` 和 `cmd=2` 的 `result=0`;失败值及其后续行为待验证。FaultFilter 文档不定义 `378`。 ## `379`:设备上传 `dataArea.result` 为总结果字符串:`"ok"` 全部子项成功,`"error"` 存在子项失败;命令无法进入处理时为错误说明(如 `busy`、`pe sn not find`)。实报中的 `cmd=0` 和 `cmd=2` 均为 `"ok"`。`dataArea.list` 是数组,每项包含 `type` 和 `info[]`。 | `cmd` | `dataArea` 中的业务字段 | 证据 | | --- | --- | --- | | `0` | `list[].type`;`info[].sn`;`info[].hexList[]`,每项含 `code` 整数、`resultOne` 字符串、`hexData` 十六进制字符串 | 实报覆盖:`type="aGate"`、`code=3`、`resultOne="ok"`,且有 `hexData`。`hexData` 为子设备原始字节,`code=3` 的结构为 1 字节条数 + N × 14 字节目录项(FaultFilter 文档 5.1.1) | | `1` | `list[].type`、`info[].sn`、`info[].setResultList[{id, resultOne}]` | FaultFilter 文档。旧描述的 `resultList[{id, result}]`(`result` 为 `string[]`)有误;`resultOne` 为字符串 | | `2` | `filename` 字符串;`list[].type`;`info[].sn`;`info[].indexInfo[]`,每项含 `index` 整数及 `resultOne` 字符串 | 实报覆盖:`filename="2026-09-29_14_18_29"`,`type="aGate"`,`index` 为 `4` 和 `5`,`resultOne` 均为 `"ok"`。`index` 是数据类型码(`4`=故障数据,`5`=波形数据),不是记录序号 | | `3` | `list[].type`、`info[].sn`、`info[].idInfo[{id, idData, resultOne}]` | 仅 FaultFilter 文档 | 旧描述称 `379` 的设备类型可为 `aPower`,FaultFilter 文档未定义该值,实报仅见 `aGate`。旧描述将 `cmd=2` 的 `sn`、`resultOne` 写为数组,实报中二者都是字符串。`cmd=2` 的 `379` 只显示文件名和逐项结果,**没有录波文件字节或下载地址**:文件保存在设备本地 `/home/root/ExportFile/faultFilterRecode///index_<记录序号>/`,随后由平台经 `607~610` 触发上传,见 [2026-10-14 样本](#2026-10-14-样本)。 ## `380`:服务端对 `379` 的应答 `dataArea` 包含 `cmd` 和整数 `result`。描述称 `result=0` 成功、`result=-1` 失败。实报覆盖 `cmd=0` 和 `cmd=2` 的 `result=0`;`cmd=1` 及失败值待验证。FaultFilter 文档不定义 `380`。 ## 实报时间线 CSV 倒序展示;下表按 `logTime` 正序排列。`logTime` 比同一行的 `time` 晚 8 小时,与本地时间和 UTC 的差值一致。 | 本地时间 | 消息 | 关键字段 | | --- | --- | --- | | 14:17:35 | `377` | `cmd=0`,`aGate`,`codeList=[3]`,`snno=7529` | | 14:17:35 | `378` | `cmd=0`,`result=0`,`snno=13` | | 14:17:38 | `379` | `cmd=0`,`result="ok"`,`hexList` 中 `code=3`、`resultOne="ok"`、`hexData`,`snno=14` | | 14:17:46 | `380` | `cmd=0`,`result=0`,`snno=7536` | | 14:17:47 | `377` | `cmd=2`,`aGate`,`index=[9]`,`snno=7539` | | 14:17:47 | `378` | `cmd=2`,`result=0`,`snno=22` | | 14:18:31 | `379` | `cmd=2`,`result="ok"`,`filename="2026-09-29_14_18_29"`,`indexInfo` 为 `4/ok`、`5/ok`,`snno=26` | | 14:18:33 | `380` | `cmd=2`,`result=0`,`snno=7559` | 请求 `cmd=0` 后约 3 秒出现 `379`;请求 `cmd=2` 后约 44 秒出现 `379`。这些是单次样本的观察值,不是超时规则。请求、应答和上传的 `snno` 不相同;尚不能用它直接关联消息。 该样本没有记录 `607~610`;当时是否发生上传无法从该日志判断。 ## 2026-10-14 样本 来源:[CloudLog_2026-10-14_104644.csv](CloudLog_2026-10-14_104644.csv),测试服,网关 `AFCI_TEST_0713`,`aGate`,外层 `equipNo` 与 `info[].sn` 相同。包含两轮完全相同的操作(05:36 和 05:45),每轮依次为 `cmd=0, codeList=[3]` 和 `cmd=2, index=[7]`。日志未标明 `377` 的发送方。 第二轮时间线(`logTime`,UTC): | 时间 | 消息 | 关键字段 | | --- | --- | --- | | 05:45:25 | `377` | `cmd=0`,`codeList=[3]`,`snno=6008` | | 05:45:25 | `378` | `cmd=0`,`result=0` | | 05:45:28 | `379` | `cmd=0`,`result="ok"`,`hexList` 中 `code=3`、`resultOne="ok"`,`hexData` 见下 | | 05:45:36 | `380` | `cmd=0`,`result=0` | | 05:45:38 | `377` | `cmd=2`,`index=[7]` | | 05:45:38 | `378` | `cmd=2`,`result=0` | | 05:46:21 | `379` | `cmd=2`,`result="ok"`,`filename="2026-10-14_05_46_20"`,`indexInfo` 为 `4/ok`、`5/ok` | | 05:46:21 | `607` | `faultRecordID=5226`,`fileName="2026-10-14_05_46_20"`,`url="http://test.franklinwh.com/hes-upload/v1/file/upload"`,`logTime="2026-10-14"` | | 05:46:21 | `608` | `{"result":0,"filename":"2026-10-14_05_46_20","result":5226}`(第二个 `result` 实为误写的 `id`,用户确认,2026-10-16) | | 05:46:22 | `380` | `cmd=2`,`result=0` | | 05:46:33 | `609` | `result=0`,`fileName="2026-10-14_05_46_20.tar.bz2"` | | 05:46:33 | `610` | `result=1` | 第一轮(05:36~05:37)顺序与字段结构相同,`faultRecordID=5224`、`filename="2026-10-14_05_37_32"`,`607→609` 约 11 秒。 观察: - `607` 在 `379(cmd=2)` 同一秒发出,早于 `380`,带平台召唤记录 ID,推断平台收到 `379` 后自动发起上传。用户确认本工具只监听这一过程(2026-10-16)。 - 下载地址由用户确认为 `https://test.franklinwh.com/hes-upload/v1/file/download/logger/AFCI_TEST_0713/2026-10-14_05_46_20.tar.bz2`,即 `logger//<609.fileName>`;生产服替换域名。 - `610.result=1`,而 `378/380` 成功为 `0`,不同消息的成功值不一致。 - 两轮 `cmd=0` 的 `hexData` 完全相同。 `code=3` 目录解析(211 字节 = 1 + 15 × 14,结构与 FaultFilter 文档 5.1.1 一致): | 序号 | 发生时间 | 参数列表版本 | 触发源 | | --- | --- | --- | --- | | 1 | 2024-01-01 00:00:34 | `00000001` | `0x1002` | | 2 | 2026-07-14 02:53:35 | `00000001` | `0x1001` | | 3 | 2026-08-12 02:03:05 | `00000001` | `0x1001` | | 4 | 2026-08-13 21:40:40 | `00000001` | `0x1001` | | 5 | 2026-08-14 03:24:16 | `00000001` | `0x1001` | | 6 | 2026-08-16 03:58:05 | `00000001` | `0x1001` | | 7 | 2026-08-18 03:03:08 | `00000001` | `0x1001` | | 8 | 2025-11-24 01:11:46 | `00000001` | `0x1001` | | 9 | 2025-11-26 21:44:06 | `00000001` | `0x1001` | | 10 | 2023-05-04 22:01:03 | `00000001` | `0x1002` | | 11 | 2023-05-04 22:02:33 | `00000001` | `0x1002` | | 12 | 2023-05-04 22:03:40 | `00000001` | `0x1002` | | 13 | 2023-05-04 22:04:46 | `00000001` | `0x1002` | | 14 | 2023-05-04 22:28:14 | `00000001` | `0x1002` | | 15 | 2026-06-29 15:07:35 | `00000001` | `0x1001` | 目录不按时间排序,像是循环覆盖存储;第 1 条时间疑似设备未校时。 ## 2026-10-14 录波文件解析 来源:[2026-10-14_05_46_20.tar.bz2](2026-10-14_05_46_20.tar.bz2),即上表 `609.fileName` 对应的文件,按 `logger//` 规律下载。用户确认下载**不需要 token**(2026-10-16)。 ### 压缩包 - **扩展名是 `.tar.bz2`,实际是 gzip 压缩的 tar**(文件头 `1f 8b`),用 bzip2 解压会失败。解压时应按文件头判断格式,不能信任扩展名。 - 目录结构与 FaultFilter 文档 5.2.1 一致,但没有 `faultFilterRecode` 根目录: ```text 2026-10-14_05_46_20/AFCI_TEST_0713/index_7/failuredata 1024 字符(512 字节) 2026-10-14_05_46_20/AFCI_TEST_0713/index_7/waveformdata 98304 字符(49152 字节) ``` - 文件内容为大写十六进制 ASCII 文本,无换行,与文档一致。`` 为 `info[].sn`(本例与网关 SN 相同)。 ### `failuredata`(按 FaultFilter 5.2.2 解析) | 字段 | 值 | 说明 | | --- | --- | --- | | 单一故障信息数据长度 | `0x0200` = 512 | 等于整个文件的字节数(含本字段和末尾填充),**不是**有效数据长度 | | 基本信息 | CAN 协议版本 1;存储格式版本 1;参数列表版本 `00000001`;最大采样频率 4500;采样频率 4500;采样点总数 1536;故障前 1024;故障后 512 | 与 code=2 结构一致 | | 位置索引 | 7 | 与请求 `index=[7]` 一致 | | 故障发生时间 | 2026-08-18 03:03:08 | 与目录第 7 条一致 | | 事件触发源 | `0x1001` 并网发生状态切换故障 | 与目录一致 | | DCDC 版本 | `EMS_SL:V10R80B21D00`,NUL 填充 | aGate 中为从 DSP 版本 | | INV/FPGA 版本 | 字节 `01 02 03 … 40` | aGate 中为填充,填充方式不限定(用户确认,2026-10-16);解析时跳过 | | 快照通道 | 总数 6,已用 6:`0x10=2049`、`0x11=0`、`0x12=0`、`0x13=4096`、`0x05=1`、`0x04=0` | | | 采样通道 | 总数 16,已用 16:`0x10 0x11 0x12 0x13 0x3016 0x3017 0x3013 0x3014 0x3010 0x3011 0x3022 0x3023 0x01 0x03 0x03 0x01` | 最后 4 项与可配采样通道的类型码相同,见下 | | 可配快照通道 | 总数 0 | | | 可配采样通道 | 总数 4,已配 4:`Uint16@0x00014990`、`float32@0x00001510`、`float32@0x00001512`、`Uint16@0x0001476E` | | | 结构结束位置 | 第 308 字节 | 剩余 204 字节全部为 `0xFF` 填充 | ### `waveformdata`(按 FaultFilter 5.2.3 解析) - 49152 字节 = 16 通道 × 1536 点 × 2 字节,**恰好整除**。通道数 n = `failuredata` 的“采样通道总数”(16),点数 m = 基本信息的“采样点总数”(1536)。 - “采样点优先”排列得到确认:按此排列,相邻点平均变化约 18;若按通道优先排列,平均变化约 2388(无意义)。 - 采样通道列表的最后 4 项对应 4 个可配采样通道,即**可配采样通道已包含在 16 个采样通道中**,不另外追加。其“含义”位置填的是类型码(`0x01`/`0x03`),不是 F14 参数代码。 - 每个通道每点固定 2 字节,包括类型为 float32 的可配通道。第 13、14 通道(float32)的值范围为 -248~249,看起来像 16 位整数,float32 如何压成 16 位待确认。 - 数值应按**有符号 int16** 解释:第 4~7 通道(`0x3016/0x3017/0x3013/0x3014`)是幅值约 ±2200 的正弦波,按 Uint16 解释会在过零处跳变。文档写的是 Uint16。 - 采样频率得到验证:第 4、5 通道过零周期约 90 点,在 4500 Hz 下即 **50 Hz**。 - 故障时刻对齐得到验证:第 0 通道(`0x10`)在**第 1024 点**从 `0x0000` 变为 `0x0801`,正好等于故障前采样点数。快照 `0x10=2049`(`0x801`)等于故障后的值,快照 `0x13=4096` 等于第 3 通道故障前的值 `0x1000`。 | 通道 | 含义代码 | 特征 | | --- | --- | --- | | 0~3 | `0x10~0x13` | 状态/故障字,只有少数几个离散值 | | 4~7 | `0x3016 0x3017 0x3013 0x3014` | 50 Hz 正弦,±2200 左右 | | 8~11 | `0x3010 0x3011 0x3022 0x3023` | 小幅值噪声(-36~31) | | 12 | 可配 Uint16 `0x14990` | 3 个离散值(`0x3020`、`0x3900`、`0x1900`) | | 13~14 | 可配 float32 `0x1510`/`0x1512` | -248~249 | | 15 | 可配 Uint16 `0x1476E` | 4 或 5 | 各通道的物理量和换算系数需要 F14《参数代码表》,目前只能展示原始值。 ## 2026-10-19 测试服实测:607~610 的 topic 测试服 EamonAux 的 `record_fetch`(05:20 UTC):`mng/{SN}` 上收到 378、379、380,但 5 分钟内没有 607~610,最终靠超时后按规则直接下载完成,`faultRecordId` 为空。用户确认 607~610 走 debug topic:607、610 为 `fwh/s2c/debug/{SN}`,608、609 为 `fwh/c2s/debug/{SN}`。已改为录波获取时同时订阅这两个 topic。 ## 待验证事项 1. ~~`cmd=2` 请求 `index=[9]`,随后上传的 `indexInfo` 却是索引 `4`、`5`。~~ 已由 FaultFilter 文档解释:每个记录序号在回复中展开为类型码 `4`(故障数据)和 `5`(波形数据),两者对应同一请求。 2. ~~`cmd=0` 的 `hexData` 代表什么。~~ 已由 FaultFilter 文档定义,并已用 2026-10-14 样本验证 `code=3` 结构。`code=1/2` 仍无样本。 3. ~~`cmd=2` 的录波文件从设备传到服务端的通道。~~ 已由 2026-10-14 样本确认为 `607~610` 触发的 HTTP 上传,下载地址规律已确认。下载不需要 token,压缩包结构已确认(见 [2026-10-14 录波文件解析](#2026-10-14-录波文件解析))。仍待确认:平台对本工具直连发送的 `377` 是否同样自动上传。 7. 录波文件解析中与 FaultFilter 文档不一致或文档未说明的地方:压缩格式实为 gzip;`failuredata` 长度字段含义;采样值有无符号;float32 可配通道如何存成 16 位。(aGate 的 INV/FPGA 版本字段已确认为填充,解析时跳过。)见 [2026-10-14 录波文件解析](#2026-10-14-录波文件解析)。 4. `379` 上传和 `380` 应答的实际 topic、应答时限、重发机制及消息关联规则。 5. `cmd=1`/`cmd=3` 的 `379` 实报,以及各命令的失败报文在外层的表现。结构本身已由 FaultFilter 文档定义(`setResultList`/`idInfo`)。 6. `pe` 设备的实际寻址(网关 SN 与子设备 SN 不同时的报文);批量记录序号时多组 `4/5` 的顺序。 ## 下一轮实测需要记录的内容 | 优先级 | 实测场景 | 必须保留的证据 | 要解决的问题 | | --- | --- | --- | --- | | 1 | `pe` 子设备、单个 `code` 的完整 `cmd=0` 查询 | 操作时间、网关 SN、子设备类型/SN、双向 topic、`377~380` 原始报文和设备界面中的对应记录 | 按 FaultFilter 文档解析 `hexData` 验证;网关与子设备 SN 关系 | | 1 | 本工具直连发送 `377(cmd=2)`,测试服和生产服各一次 | 全部双向报文(含 `607~610`)、topic、按规律下载的 `tar.bz2` | 平台是否自动上传;s2c 能否订阅;下载认证与压缩包结构 | | 1 | `cmd=3` 读取一项已知功能码(如 `id=5` 采样频率) | 请求/返回报文、功能码和当前值 | `cmd=3` 经外层转发的实际表现 | | 2 | `cmd=1` 设置一项参数并用 `cmd=3` 复读确认 | `377~380` 全链路、设置前后值、单项结果 | `378` 与 `379` 各自的成功语义,`setResultList` 实际结构 | | 2 | 设备忙、SN 不存在、参数设置失败及超时 | 完整错误报文、重发次数与间隔 | `busy` 等错误字符串在外层的表现、`378` 失败值、超时和重试规则 | 新增样本时,先保留原始报文、topic、时间和触发操作,再将“待验证”条目逐项升级为已确认事实。当前代码入口和实现边界见 [implementation-status.md](implementation-status.md)。