소스 검색

docs:故障录波设计文档阶段性定稿提交(编码前)

Jerry Liang 2 주 전
부모
커밋
1560bd2ba0

BIN
docs/fault-record/2026-10-14_05_46_20.tar.bz2


+ 25 - 0
docs/fault-record/CloudLog_2026-10-14_104644.csv

@@ -0,0 +1,25 @@
+"cmdType","cmdName","gatewayId","gatewayName","gatewayType","gatewayStatus","activeStatus","logData","logTime","time"
+610,故障录波文件上传结果应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":610,""crc"":""3070A9DC"",""dataArea"":{""result"":1},""equipNo"":""AFCI_TEST_0713"",""len"":12,""snno"":6061,""timeStamp"":1791971193,""type"":0}",2026-10-14 05:46:33,2026-10-14 09:46:33
+609,故障录波文件上传结果,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":609,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971193,""snno"":29,""len"":53,""crc"":""07C1B7FA"",""dataArea"":{""result"":0,""fileName"":""2026-10-14_05_46_20.tar.bz2""}}",2026-10-14 05:46:33,2026-10-14 09:46:33
+380,故障录波信息上传应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":380,""crc"":""FF9D0192"",""dataArea"":{""result"":0,""cmd"":2},""equipNo"":""AFCI_TEST_0713"",""len"":20,""snno"":6052,""timeStamp"":1791971182,""type"":0}",2026-10-14 05:46:22,2026-10-14 09:46:22
+607,故障录波召唤下行数据,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":607,""crc"":""87BD67DE"",""dataArea"":{""faultRecordID"":5226,""fileName"":""2026-10-14_05_46_20"",""url"":""http://test.franklinwh.com/hes-upload/v1/file/upload"",""logTime"":""2026-10-14""},""equipNo"":""AFCI_TEST_0713"",""len"":139,""snno"":6049,""timeStamp"":1791971181,""type"":0}",2026-10-14 05:46:21,2026-10-14 09:46:21
+608,故障录波召唤应答上行数据,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":608,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971181,""snno"":28,""len"":59,""crc"":""A4970F2B"",""dataArea"":{""result"":0,""filename"":""2026-10-14_05_46_20"",""result"":5226}}",2026-10-14 05:46:21,2026-10-14 09:46:22
+379,故障录波信息上传,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":379,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971181,""snno"":27,""len"":187,""crc"":""E1808BA3"",""dataArea"":{""result"":""ok"",""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""indexInfo"":[{""index"":4,""resultOne"":""ok""},{""index"":5,""resultOne"":""ok""}]}]}],""filename"":""2026-10-14_05_46_20"",""cmd"":2}}",2026-10-14 05:46:21,2026-10-14 09:46:21
+378,故障录波信息应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":378,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971138,""snno"":25,""len"":20,""crc"":""05E9AF33"",""dataArea"":{""cmd"":2,""result"":0}}",2026-10-14 05:45:38,2026-10-14 09:45:38
+377,故障录波实时数据获取,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":377,""crc"":""FB01C55A"",""dataArea"":{""data"":{""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""index"":[7]}]}]},""cmd"":2},""equipNo"":""AFCI_TEST_0713"",""len"":89,""snno"":6014,""timeStamp"":1791971138,""type"":0}",2026-10-14 05:45:38,2026-10-14 09:45:38
+380,故障录波信息上传应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":380,""crc"":""CDAB6310"",""dataArea"":{""result"":0,""cmd"":0},""equipNo"":""AFCI_TEST_0713"",""len"":20,""snno"":6011,""timeStamp"":1791971136,""type"":0}",2026-10-14 05:45:36,2026-10-14 09:45:37
+379,故障录波信息上传,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":379,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971128,""snno"":20,""len"":557,""crc"":""918AD93E"",""dataArea"":{""result"":""ok"",""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""hexList"":[{""code"":3,""hexData"":""0F0107E801010000220000000110020207EA070E0235230000000110010307EA080C0203050000000110010407EA080D1528280000000110010507EA080E0318100000000110010607EA0810033A050000000110010707EA08120303080000000110010807E90B18010B2E0000000110010907E90B1A152C060000000110010A07E705041601030000000110020B07E705041602210000000110020C07E705041603280000000110020D07E7050416042E0000000110020E07E70504161C0E0000000110020F07EA061D0F0723000000011001"",""resultOne"":""ok""}]}]}],""cmd"":0}}",2026-10-14 05:45:28,2026-10-14 09:45:29
+378,故障录波信息应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":378,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791971125,""snno"":18,""len"":20,""crc"":""65B51A78"",""dataArea"":{""cmd"":0,""result"":0}}",2026-10-14 05:45:25,2026-10-14 09:45:26
+377,故障录波实时数据获取,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":377,""crc"":""5169219D"",""dataArea"":{""data"":{""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""codeList"":[3]}]}]},""cmd"":0},""equipNo"":""AFCI_TEST_0713"",""len"":92,""snno"":6008,""timeStamp"":1791971125,""type"":0}",2026-10-14 05:45:25,2026-10-14 09:45:25
+610,故障录波文件上传结果应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":610,""crc"":""3070A9DC"",""dataArea"":{""result"":1},""equipNo"":""AFCI_TEST_0713"",""len"":12,""snno"":5731,""timeStamp"":1791970664,""type"":0}",2026-10-14 05:37:44,2026-10-14 09:37:47
+609,故障录波文件上传结果,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":609,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970664,""snno"":31,""len"":53,""crc"":""C206AC38"",""dataArea"":{""result"":0,""fileName"":""2026-10-14_05_37_32.tar.bz2""}}",2026-10-14 05:37:44,2026-10-14 09:37:44
+380,故障录波信息上传应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":380,""crc"":""FF9D0192"",""dataArea"":{""result"":0,""cmd"":2},""equipNo"":""AFCI_TEST_0713"",""len"":20,""snno"":5726,""timeStamp"":1791970655,""type"":0}",2026-10-14 05:37:35,2026-10-14 09:37:36
+608,故障录波召唤应答上行数据,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":608,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970654,""snno"":30,""len"":59,""crc"":""A14FF691"",""dataArea"":{""result"":0,""filename"":""2026-10-14_05_37_32"",""result"":5224}}",2026-10-14 05:37:34,2026-10-14 09:37:34
+607,故障录波召唤下行数据,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":607,""crc"":""4AEDD288"",""dataArea"":{""faultRecordID"":5224,""fileName"":""2026-10-14_05_37_32"",""url"":""http://test.franklinwh.com/hes-upload/v1/file/upload"",""logTime"":""2026-10-14""},""equipNo"":""AFCI_TEST_0713"",""len"":139,""snno"":5722,""timeStamp"":1791970653,""type"":0}",2026-10-14 05:37:33,2026-10-14 09:37:34
+379,故障录波信息上传,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":379,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970652,""snno"":29,""len"":187,""crc"":""24479061"",""dataArea"":{""result"":""ok"",""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""indexInfo"":[{""index"":4,""resultOne"":""ok""},{""index"":5,""resultOne"":""ok""}]}]}],""filename"":""2026-10-14_05_37_32"",""cmd"":2}}",2026-10-14 05:37:32,2026-10-14 09:37:34
+378,故障录波信息应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":378,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970610,""snno"":25,""len"":20,""crc"":""05E9AF33"",""dataArea"":{""cmd"":2,""result"":0}}",2026-10-14 05:36:50,2026-10-14 09:36:51
+377,故障录波实时数据获取,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":377,""crc"":""FB01C55A"",""dataArea"":{""data"":{""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""index"":[7]}]}]},""cmd"":2},""equipNo"":""AFCI_TEST_0713"",""len"":89,""snno"":5701,""timeStamp"":1791970610,""type"":0}",2026-10-14 05:36:50,2026-10-14 09:36:51
+380,故障录波信息上传应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":380,""crc"":""CDAB6310"",""dataArea"":{""result"":0,""cmd"":0},""equipNo"":""AFCI_TEST_0713"",""len"":20,""snno"":5699,""timeStamp"":1791970608,""type"":0}",2026-10-14 05:36:48,2026-10-14 09:36:48
+379,故障录波信息上传,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":379,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970585,""snno"":17,""len"":557,""crc"":""918AD93E"",""dataArea"":{""result"":""ok"",""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""hexList"":[{""code"":3,""hexData"":""0F0107E801010000220000000110020207EA070E0235230000000110010307EA080C0203050000000110010407EA080D1528280000000110010507EA080E0318100000000110010607EA0810033A050000000110010707EA08120303080000000110010807E90B18010B2E0000000110010907E90B1A152C060000000110010A07E705041601030000000110020B07E705041602210000000110020C07E705041603280000000110020D07E7050416042E0000000110020E07E70504161C0E0000000110020F07EA061D0F0723000000011001"",""resultOne"":""ok""}]}]}],""cmd"":0}}",2026-10-14 05:36:25,2026-10-14 09:36:26
+378,故障录波信息应答,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":378,""equipNo"":""AFCI_TEST_0713"",""type"":0,""timeStamp"":1791970583,""snno"":15,""len"":20,""crc"":""65B51A78"",""dataArea"":{""cmd"":0,""result"":0}}",2026-10-14 05:36:23,2026-10-14 09:36:26
+377,故障录波实时数据获取,AFCI_TEST_0713,-,-,1,1,"{""cmdType"":377,""crc"":""5169219D"",""dataArea"":{""data"":{""list"":[{""type"":""aGate"",""info"":[{""sn"":""AFCI_TEST_0713"",""codeList"":[3]}]}]},""cmd"":0},""equipNo"":""AFCI_TEST_0713"",""len"":92,""snno"":5682,""timeStamp"":1791970582,""type"":0}",2026-10-14 05:36:22,2026-10-14 09:36:26

+ 928 - 0
docs/fault-record/FaultFilter_MQTT协议文档.md

@@ -0,0 +1,928 @@
+# 故障滤波(FaultFilter)MQTT 接口协议
+
+本协议用于云端 / 上位机与故障滤波设备之间通过 MQTT 进行的数据采集、记录导出与参数读写交互。本文档面向对接方开发者,仅描述通信主题、消息流程与字段定义。
+
+---
+
+## 1. 通信主题(Topic)
+
+| 主题                        | 方向                     | QoS | 说明                       |
+| --------------------------- | ------------------------ | --- | -------------------------- |
+| `aws/faultrecord`         | 对端 → 设备(下行命令) | 2   | 下发采集 / 记录 / 参数命令 |
+| `faultfilter/faultrecord` | 设备 → 对端(上行回复) | 2   | 对下行命令的处理结果回复   |
+
+交互模型为「请求 / 应答」:对端在 `aws/faultrecord` 发布一条命令,设备处理后在 `faultfilter/faultrecord` 发布一条对应的回复。请求与回复通过 `cmd` 字段关联,回复的 `cmd` 与请求保持一致。
+
+> 消息体均为 UTF-8 编码的 JSON 字符串。字段顺序不固定,接收方应按字段名解析。
+
+---
+
+## 2. 消息总体结构
+
+所有下行命令与上行回复的顶层结构一致:
+
+```json
+{
+  "cmd": <命令码>,
+  "data": { ... }
+}
+```
+
+| 字段     | 类型 | 必填 | 说明                                      |
+| -------- | ---- | ---- | ----------------------------------------- |
+| `cmd`  | 整数 | 是   | 命令码,标识本次交互的业务类型,见第 3 节 |
+| `data` | 对象 | 是   | 命令数据体,内容随 `cmd` 不同而不同     |
+
+---
+
+## 3. 命令码(cmd)
+
+| 命令码 | 名称         | 方向        | 说明                               |
+| ------ | ------------ | ----------- | ---------------------------------- |
+| 0      | 获取实时数据 | 请求 / 回复 | 读取设备实时数据                   |
+| 1      | 设置参数     | 请求 / 回复 | 写入设备参数                       |
+| 2      | 获取记录数据 | 请求 / 回复 | 导出故障录波 / 波形记录            |
+| 3      | 查询参数     | 请求 / 回复 | 读取设备参数                       |
+| 4      | 退出         | 请求        | 通知设备结束当前处理               |
+| 90     | 测试成功     | 请求        | 连通性测试,设备回复 `ok`        |
+| 99     | 测试失败     | 请求        | 连通性测试,设备回复 `test fail` |
+
+> 处理约束:
+>
+> - 命令码按 8 位无符号整数处理,有效范围 0–255。
+> - 设备同一时刻只处理一条业务命令。收到命令后的判定顺序为:先校验 `cmd` 字段 → 再判忙碌 → 再校验 `data` 字段。
+> - 当设备处于忙碌状态时收到非「退出(4)」命令,直接返回错误 `busy`(此时不中断正在进行的处理);其余在校验阶段失败的命令,回复错误后会结束当前处理流程。
+
+---
+
+## 4. 设备分组与寻址
+
+命令数据体中的设备通过「类型 + 序列号」寻址,统一组织为 `list → info` 两级结构。
+
+### 4.1 设备类型(type)
+
+| type      | 含义            | 寻址方式                             |
+| --------- | --------------- | ------------------------------------ |
+| `pe`    | PE 功率单元     | 通过 `sn` 序列号定位(最多 15 台) |
+| `aGate` | 网关 / 从控单元 | 系统内唯一,固定寻址                 |
+
+### 4.2 分组结构
+
+```json
+"data": {
+  "list": [
+    {
+      "type": "pe",
+      "info": [
+        { "sn": "<设备序列号>", ... }
+      ]
+    },
+    {
+      "type": "aGate",
+      "info": [
+        { "sn": "<设备序列号>", ... }
+      ]
+    }
+  ]
+}
+```
+
+| 字段            | 类型   | 层级 | 说明                                                |
+| --------------- | ------ | ---- | --------------------------------------------------- |
+| `list`        | 数组   | data | 设备分组列表,按 `type` 分组                      |
+| `list[].type` | 字符串 | list | 设备类型:`pe` 或 `aGate`                       |
+| `list[].info` | 数组   | list | 该类型下的具体设备项                                |
+| `info[].sn`   | 字符串 | info | 设备序列号。`pe` 必填;`aGate` 可填但不用于寻址 |
+
+> 若 `pe` 的 `sn` 在设备侧无法匹配,整条命令返回错误 `pe sn not find`(见第 6 节)。
+
+### 4.3 回复中的整体结果(result)
+
+每条回复的 `data.result` 表示本次命令的整体处理结果:
+
+| 取值      | 含义                     |
+| --------- | ------------------------ |
+| `ok`    | 全部子项处理成功         |
+| `error` | 存在至少一个子项处理失败 |
+
+子项级别的结果由各自的 `resultOne` 字段描述。
+
+---
+
+## 5. 各命令详细定义
+
+### 5.1 获取实时数据(cmd = 0)
+
+#### 请求
+
+```json
+{
+  "cmd": 0,
+  "data": {
+    "list": [
+      {
+        "type": "aGate",
+        "info": [
+          {
+            "sn": "10060010A01X26310190",
+            "codeList": [3]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 示例取自 2026-09-29 实报:查询 aGate 的故障录波总目录。
+
+| 字段                | 类型     | 说明                                   |
+| ------------------- | -------- | -------------------------------------- |
+| `info[].codeList` | 整数数组 | 需要读取的数据项命令码集合,取值见下表 |
+
+`codeList` / `code` 取值定义:
+
+| code | 含义                     |
+| ---- | ------------------------ |
+| 1    | 故障总体信息             |
+| 2    | 设备故障录波功能基本信息 |
+| 3    | 故障录波总目录           |
+
+> 数组中的多个 `code` 由设备按子设备串行逐个读取(每个 `code` 对应一次独立的子设备读取事务),全部完成后在回复的 `hexList` 中汇总返回。
+> 说明:`hexData` 回传的是子设备原始字节的十六进制字符串,各 code 的字节结构见 5.1.1。
+>
+> **cmd=0 不能用于查询 code=4(单一故障信息)和 code=5(单一故障波形数据)。** 这两类数据属于某条具体记录,须通过获取记录数据(cmd=2,见 5.2)按记录序号导出:先用 cmd=0 + code=3 查询总目录,再以目录项的位置索引作为 cmd=2 的 `index`。
+
+#### 5.1.1 各 code 返回数据(hexData)的字节格式
+
+`hexData` 回传的是子设备返回的原始字节的十六进制字符串。`code=1/2/3` 的字节结构如下(多字节字段按紧凑排列、无填充),已由子设备方资料确认。
+
+**时间字段结构(固定 7 字节)**:
+
+| 字节 | 字段 | 类型                       | 说明                             |
+| ---- | ---- | -------------------------- | -------------------------------- |
+| 1~2  | year | Uint16(大端,高字节在前) | 年,如 `07 E6` = 0x07E6 = 2022 |
+| 3    | mon  | Uint8                      | 月                               |
+| 4    | day  | Uint8                      | 日                               |
+| 5    | hour | Uint8                      | 时                               |
+| 6    | min  | Uint8                      | 分                               |
+| 7    | sec  | Uint8                      | 秒                               |
+
+> 示例:`07 E6 01 01 01 01 01` 表示 2022-01-01 1:1:1。
+
+**code=1 故障总体信息**:
+
+| 顺序 | 字段                   | 字节数 | 含义                                    |
+| ---- | ---------------------- | ------ | --------------------------------------- |
+| 1    | 最大可存储故障记录总数 | 1      | —                                      |
+| 2    | 当前故障记录总数       | 1      | —                                      |
+| 3    | 最早故障发生时间       | 7      | 时间字段结构见本节上方                  |
+| 4    | 最新故障发生时间       | 7      | 时间字段结构见本节上方                  |
+| 5    | 快照通道总数           | 1      | 记为 a                                  |
+| 6    | 已使用快照通道个数     | 1      | —                                      |
+| 7    | 各快照通道含义         | 2 × a | 每通道 2 字节含义编号(见第 10 节 F14) |
+| 8    | 采样通道总数           | 1      | 记为 b                                  |
+| 9    | 已使用采样通道个数     | 1      | —                                      |
+| 10   | 各采样通道含义         | 2 × b | 每通道 2 字节含义编号(见第 10 节 F14) |
+| 11   | 最大可配快照通道总数   | 1      | 记为 c                                  |
+| 12   | 已配置可配快照通道个数 | 1      | —                                      |
+| 13   | 各可配快照通道         | 6 × c | 每项 = 类型(2B) + 地址(4B)              |
+| 14   | 最大可配采样通道总数   | 1      | 记为 d                                  |
+| 15   | 已配置可配采样通道个数 | 1      | —                                      |
+| 16   | 各可配采样通道         | 6 × d | 每项 = 类型(2B) + 地址(4B)              |
+
+> 关于 code=1 中的编号含义:
+>
+> - 「各快照通道含义」/「各采样通道含义」中每个 2 字节值为通道含义参数代码,用于标识该通道对应的物理量(故障字 / 控制字 / 状态量,如温度、SOC、母线电流等),具体编号与物理量的对应关系由产品的参数代码表定义(见第 10 节 F14)。
+> - 「各可配快照通道」/「各可配采样通道」中每项的类型(2B) 与 地址(4B) 用于标识可配通道所记录的变量,取值含义同属产品定义(见第 10 节 F14)。
+>
+> 与 code=4 的关系(可相互参考):code=1 从「快照通道」起的后半部分,与 code=4(见 5.2.2)的通道部分结构基本一致,均按「快照通道组 → 采样通道组 → 可配快照通道组 → 可配采样通道组」组织,每组均为「总数 + 已用/已配数 + 逐通道数据」。两者的通道含义编号(F14)与可配通道类型/地址共用同一套定义。**唯一区别**:code=1 的快照通道逐项**只有含义(2B)**;code=4 的快照通道逐项是**含义(2B) + 数值(2B)**(code=4 需记录故障发生时刻的瞬时值,code=1 仅描述通道配置)。
+
+**code=2 设备故障录波功能基本信息**(固定 16 字节):
+
+| 偏移 | 字段                   | 字节数 | 含义 |
+| ---- | ---------------------- | ------ | ---- |
+| 0    | CAN 通讯协议版本号     | 1      | —   |
+| 1    | 故障数据存储格式版本号 | 1      | —   |
+| 2    | 参数列表版本号         | 4      | —   |
+| 6    | 最大支持采样频率       | 2      | —   |
+| 8    | 采样频率               | 2      | —   |
+| 10   | 采样点总数             | 2      | —   |
+| 12   | 故障前采样点个数       | 2      | —   |
+| 14   | 故障后采样点个数       | 2      | —   |
+
+**code=3 故障录波总目录**:
+
+| 顺序 | 字段                 | 字节数  | 含义                   |
+| ---- | -------------------- | ------- | ---------------------- |
+| 1    | 总目录中故障记录条数 | 1       | 记为 N                 |
+| 2    | 记录项列表           | N × 14 | N 条记录,每条结构见下 |
+
+单条记录(固定 **14 字节**):
+
+| 顺序 | 字段           | 字节数 | 含义                             |
+| ---- | -------------- | ------ | -------------------------------- |
+| 1    | 位置索引       | 1      | —                               |
+| 2    | 故障发生时间   | 7      | 时间字段结构见本节上方,年份大端 |
+| 3    | 参数列表版本号 | 4      | —                               |
+| 4    | 事件触发源     | 2      | 代码表见第 10 节 F15             |
+
+> 单条长度 = 1 + 7 + 4 + 2 = **14 字节**(时间按 7 字节)。
+
+**code=4 单一故障信息 / code=5 单一故障波形数据**:
+不能通过 cmd=0 查询,由 cmd=2 按记录导出并保存为文件。文件内容的字节结构见 5.2.2(`failuredata`,code=4)与 5.2.3(`waveformdata`,code=5),解析导出文件时使用。
+
+#### 回复
+
+```json
+{
+  "cmd": 0,
+  "data": {
+    "result": "ok",
+    "list": [
+      {
+        "type": "aGate",
+        "info": [
+          {
+            "sn": "10060010A01X26310190",
+            "hexList": [
+              {
+                "code": 3,
+                "hexData": "<故障录波总目录原始数据>",
+                "resultOne": "ok"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 示例取自 2026-09-29 实报(对上述请求的回复)。该次 `hexData` 原文未存档,此处以占位符表示,结构见 5.1.1 code=3。2026-10-14 实报的 `hexData` 原文见 `CloudLog_2026-10-14_104644.csv`,已按 5.1.1 验证。
+
+| 字段                    | 类型   | 说明                                       |
+| ----------------------- | ------ | ------------------------------------------ |
+| `info[].hexList`      | 数组   | 本设备各数据项的读取结果                   |
+| `hexList[].code`      | 整数   | 数据项命令码,对应请求 `codeList` 中的值 |
+| `hexList[].hexData`   | 字符串 | 读取到的原始数据,十六进制字符串形式       |
+| `hexList[].resultOne` | 字符串 | 该数据项的处理结果                         |
+
+---
+
+### 5.2 获取记录数据(cmd = 2)
+
+用于导出故障录波 / 波形记录文件。
+
+#### 请求
+
+```json
+{
+  "cmd": 2,
+  "data": {
+    "list": [
+      {
+        "type": "aGate",
+        "info": [
+          {
+            "sn": "10060010A01X26310190",
+            "index": [9]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 示例取自 2026-09-29 实报:导出目录中位置索引为 9 的记录。
+
+| 字段             | 类型     | 说明                   |
+| ---------------- | -------- | ---------------------- |
+| `info[].index` | 整数数组 | 需要导出的记录索引集合 |
+
+#### 回复
+
+```json
+{
+  "cmd": 2,
+  "data": {
+    "result": "ok",
+    "list": [
+      {
+        "type": "aGate",
+        "info": [
+          {
+            "sn": "10060010A01X26310190",
+            "indexInfo": [
+              {
+                "index": 4,
+                "resultOne": "ok"
+              },
+              {
+                "index": 5,
+                "resultOne": "ok"
+              }
+            ]
+          }
+        ]
+      }
+    ],
+    "filename": "2026-09-29_14_18_29"
+  }
+}
+```
+
+> 示例取自 2026-09-29 实报(对上述请求的回复):记录序号 9 展开为 `4`(故障数据)与 `5`(波形数据)两项。
+
+| 字段                      | 类型   | 说明                                                                                                               |
+| ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ |
+| `info[].indexInfo`      | 数组   | 本设备各记录项的处理结果                                                                                           |
+| `indexInfo[].index`     | 整数   | 记录数据类型码(非请求的记录序号):`4`=故障数据,`5`=波形数据。请求的每个记录序号在回复中会展开为这两类各一项 |
+| `indexInfo[].resultOne` | 字符串 | 该记录项的处理结果                                                                                                 |
+| `data.filename`         | 字符串 | 本次导出记录对应的时间戳目录名,格式 `YYYY-MM-DD_HH_MM_SS`                                                       |
+
+> 实现细节:
+>
+> - 对请求中的每个记录序号,设备向子设备分别请求「故障数据」和「波形数据」两类,因此回复的 `indexInfo` 中每个记录序号对应两项,`index` 字段分别为 `4`(故障数据)与 `5`(波形数据)。回复项中**不直接回传请求的记录序号数值**。
+
+#### 5.2.1 记录文件保存格式(code=4 / code=5 的落盘内容)
+
+获取记录(cmd=2)读取的是 `code=4`(单一故障信息 / 故障数据)与 `code=5`(单一故障波形数据),这两类数据设备不解析其内部结构,而是直接保存为文件。保存规则如下:
+
+**保存根目录**:`/home/root/ExportFile/faultFilterRecode/`
+
+**完整文件路径**:
+
+```
+/home/root/ExportFile/faultFilterRecode/<filename>/<sn>/index_<记录序号>/<类型文件名>
+```
+
+| 路径段               | 来源                          | 说明                                                                       |
+| -------------------- | ----------------------------- | -------------------------------------------------------------------------- |
+| `<filename>`       | 本次导出批次时间戳            | 与回复 `data.filename` 相同,格式 `YYYY-MM-DD_HH_MM_SS`                |
+| `<sn>`             | 设备序列号                    | 该记录所属子设备的 SN                                                      |
+| `index_<记录序号>` | 请求 `index[]` 中的记录序号 | 记录序号在此目录名中体现(**不**在 `indexInfo[].index` 字段中)    |
+| `<类型文件名>`     | 数据类型                      | `failuredata`(故障数据,code=4)或 `waveformdata`(波形数据,code=5) |
+
+示例:按上述实报(记录序号 9,SN `10060010A01X26310190`,批次时间 `2026-09-29_14_18_29`),按本节规则应生成两个文件(路径为按规则推导,未在设备上核对):
+
+```
+/home/root/ExportFile/faultFilterRecode/2026-09-29_14_18_29/10060010A01X26310190/index_9/failuredata
+/home/root/ExportFile/faultFilterRecode/2026-09-29_14_18_29/10060010A01X26310190/index_9/waveformdata
+```
+
+**文件内容**:子设备返回的原始字节,经「每字节转 2 位大写十六进制字符」转换后的 **ASCII 文本字符串**(例如字节 `0x0A 0x1B` 存为文本 `0A1B`)。文件以覆盖方式写入。
+
+> 补充说明:
+>
+> - 文件为纯十六进制文本,不含文件头。
+> - 每次获取记录(cmd=2)开始前,设备会先清空保存根目录下的**全部**历史记录文件,再保存本次结果。
+> - 仅当该记录项处理成功时才落盘;失败项不生成文件,仅在 `indexInfo` 中以 `resultOne` 反映错误。
+
+#### 5.2.2 `failuredata` 文件内容格式(code=4 单一故障信息)
+
+文件内为十六进制文本,还原成字节后按下表顺序解析(紧凑排列,多字节字段除另有说明外按示例中的大端顺序)。
+
+| 字节数 | 字段                                                 | 数据类型 | 备注                                                                                                                                                                                                                           |
+| ------ | ---------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 2      | 单一故障信息数据长度                                 | Uint16   | 后续数据的长度                                                                                                                                                                                                                 |
+| 16     | 故障录波功能基本信息                                 | —       | 结构同 code=2「基本信息」,共 16 字节(见 5.1.1)                                                                                                                                                                              |
+| 1      | 位置索引号                                           | Uint8    | 从 1 开始,最大不超过「最大可存储故障记录总数」                                                                                                                                                                                |
+| 7      | 故障发生时间                                         | Uint8[7] | 例:`07 E6 01 01 01 01 01` 表示 2022-01-01 1:1:1                                                                                                                                                                             |
+| 2      | 事件触发源                                           | Uint16   | 采用事件触发源代码表示,触发源在应用层配置                                                                                                                                                                                     |
+| 64     | 故障发生时 DCDC 软件版本号                           | char[64] | aGate 中表示从 DSP 软件版本号。例:`PE_DCDC:V10R03B32D00` 或 `EMS_SL:V10R03B32D00`                                                                                                                                         |
+| 64     | 故障发生时 INV 软件版本号                            | char[64] | aGate 中留空。例:`PE_INV:V10R03B32D07`                                                                                                                                                                                      |
+| 64     | 故障发生时 FPGA 软件版本号                           | char[64] | aGate 中留空。例:`PE_FPGA:V255R03B32D00`                                                                                                                                                                                    |
+| 1      | 快照通道总数                                         | Uint8    | 设计最大 32 通道;无论是否使用都上传该参数及后续快照参数                                                                                                                                                                       |
+| 1      | 已使用的快照通道个数                                 | Uint8    | ≤ 快照通道总数                                                                                                                                                                                                                |
+| 2      | 第 1 个快照通道含义                                  | Uint16   | 通道含义参数代码,标识该通道对应的物理量(故障字 / 控制字 / 状态量,如温度、SOC 等);「采样通道」与「快照通道」共用同一套参数代码(见第 10 节 F14)                                                                           |
+| 2      | 第 1 个快照通道数值                                  | Uint16   | 取参数代码对应变量的数值,如母线电流                                                                                                                                                                                           |
+| …     | …(第 2 … n 个快照通道的含义 + 数值,各 2+2 字节) | Uint16   | n = 快照通道总数;每通道按「含义(2B) + 数值(2B)」重复                                                                                                                                                                          |
+| 2      | 第 n 个快照通道含义                                  | Uint16   | 同上                                                                                                                                                                                                                           |
+| 2      | 第 n 个快照通道数值                                  | Uint16   | 同上                                                                                                                                                                                                                           |
+| 1      | 采样通道总数                                         | Uint8    | —                                                                                                                                                                                                                             |
+| 1      | 已使用的采样通道个数                                 | Uint8    | ≤ 采样通道总数                                                                                                                                                                                                                |
+| 2      | 第 1 个采样通道含义                                  | Uint16   | 含义同快照通道含义                                                                                                                                                                                                             |
+| …     | …(第 2 … m 个采样通道含义,各 2 字节)            | Uint16   | m = 采样通道总数;每通道仅「含义(2B)」                                                                                                                                                                                         |
+| 2      | 第 m 个采样通道含义                                  | Uint16   | 同上                                                                                                                                                                                                                           |
+| 1      | 最大可配快照通道总数                                 | Uint8    | —                                                                                                                                                                                                                             |
+| 1      | 已配置的可配快照通道个数                             | Uint8    | ≤ 最大可配快照通道总数                                                                                                                                                                                                        |
+| 2      | 第 1 个可配快照通道所记录变量的类型                  | Uint16   | 采用约定类型代码(同 5.4.1 的 id=6 功能);「可配采样通道」与「可配快照通道」共用同一套类型代码:`0h`=Empty;`1h`=Uint16;`2h`=int16;`3h`=float32;`4h`=Uint32_HWord;`5h`=Uint32_LWord;`6h`=Uint32;其他=无效 |
+| 4      | 当前第 1 个可配采样通道所记录变量的地址              | Uint32   | 与程序版本相关。例:`00 31 11 94` 表示地址 0x311194                                                                                                                                                                          |
+| …     | …(第 2 … x 个:类型(2B) + 地址(4B))              | —       | x = 最大可配快照通道总数;每项按「类型(2B) + 地址(4B)」重复                                                                                                                                                                    |
+| 2      | 第 x 个可配快照通道所记录变量的类型                  | Uint16   | 同上                                                                                                                                                                                                                           |
+| 4      | 当前第 x 个可配采样通道所记录变量的地址              | Uint32   | 同上                                                                                                                                                                                                                           |
+| 1      | 最大可配采样通道总数                                 | Uint8    | —                                                                                                                                                                                                                             |
+| 1      | 已配置的可配采样通道个数                             | Uint8    | ≤ 最大可配采样通道总数                                                                                                                                                                                                        |
+| 2      | 第 1 个可配采样通道所记录变量的类型                  | Uint16   | 类型代码同可配快照通道                                                                                                                                                                                                         |
+| 4      | 当前第 1 个可配采样通道所记录变量的地址              | Uint32   | 与程序版本相关,同可配快照通道                                                                                                                                                                                                 |
+| …     | …(第 2 … y 个:类型(2B) + 地址(4B))              | —       | y = 最大可配采样通道总数;每项按「类型(2B) + 地址(4B)」重复                                                                                                                                                                    |
+| 2      | 第 y 个可配采样通道所记录变量的类型                  | Uint16   | 同上                                                                                                                                                                                                                           |
+| 4      | 当前第 y 个可配采样通道所记录变量的地址              | Uint32   | 同上                                                                                                                                                                                                                           |
+
+> 注:上表「16 字节 故障录波功能基本信息」与 code=2 的基本信息结构一致(见 5.1.1,共 16 字节),两处相符。
+> 通道含义参数代码(快照 / 采样通道含义编号)的取值定义见第 10 节 F14;事件触发源代码表见 10.4(第 10 节 F15)。
+>
+> 与 code=1 的关系(可相互参考):本表从「快照通道总数」起的后半部分,与 code=1(见 5.1.1)的通道部分结构基本一致(快照 / 采样 / 可配快照 / 可配采样四组,每组「总数 + 已用/已配数 + 逐通道数据」),通道「含义」编号(F14)与可配通道 type/addr 共用同一套定义。**唯一区别**:此处 code=4 的快照通道逐项为**含义(2B) + 数值(2B)**(含故障时刻瞬时值),而 code=1 的快照通道逐项**只有含义(2B)**。
+
+#### 5.2.3 `waveformdata` 文件内容格式(code=5 单一故障波形数据)
+
+文件内为十六进制文本,还原成字节后为连续的 Uint16 采样点序列。排列方式为**按采样点外层、波形通道内层**:先排所有波形在第 1 个采样点的值,再排所有波形在第 2 个采样点的值,依此类推。
+
+| 字节数 | 字段                                  | 数据类型 | 备注                     |
+| ------ | ------------------------------------- | -------- | ------------------------ |
+| 2      | 第 1 个波形 第 1 个点的数值           | Uint16   | —                       |
+| 2      | 第 2 个波形 第 1 个点的数值           | Uint16   | —                       |
+| …     | …(第 1 个采样点,第 1 … n 个波形) | Uint16   | n = 波形通道数           |
+| 2      | 第 n 个波形 第 1 个点的数值           | Uint16   | —                       |
+| 2      | 第 1 个波形 第 2 个点的数值           | Uint16   | 第 2 个采样点开始        |
+| …     | …(第 2 个采样点,第 1 … n 个波形) | Uint16   | —                       |
+| 2      | 第 n 个波形 第 2 个点的数值           | Uint16   | —                       |
+| …     | …(依次到第 m 个采样点)             | Uint16   | m = 每个波形的采样点总数 |
+| 2      | 第 n 个波形 第 m 个点的数值           | Uint16   | 最后一个点               |
+
+> 说明:
+>
+> - 存储顺序为「采样点优先」:第 k 个采样点处所有 n 个波形的值连续排列,再进入第 k+1 个采样点。
+> - 波形通道数 n、采样点总数 m 等由 code=1 / code=2 中的通道配置与采样点参数决定(如 code=2 的「采样点总数 / 故障前采样点 / 故障后采样点」)。
+> - 每个波形各采样点代表哪个物理量,对应 code=4 中的「采样通道含义」编号。
+
+---
+
+### 5.3 查询参数(cmd = 3)
+
+> 与 cmd=1 相同,`id` 是**命令控制符(功能码)**(定义见 5.4.1)。查询时只提供功能码、不带输入参数;返回的 `idData` 为该功能码对应的当前配置 / 状态,其数据结构可参照 5.4.1 中该功能码的参数结构。
+
+#### 请求
+
+```json
+{
+  "cmd": 3,
+  "data": {
+    "list": [
+      {
+        "type": "pe",
+        "info": [
+          {
+            "sn": "10050015A00X25070634",
+            "idList": [5]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 暂无 cmd=3 实报,本例为按协议构造的示意:查询 PE 当前采样频率。
+
+| 字段              | 类型     | 说明                                             |
+| ----------------- | -------- | ------------------------------------------------ |
+| `info[].idList` | 整数数组 | 需要查询的命令控制符(功能码)集合,取值见 5.4.1 |
+
+#### 回复
+
+```json
+{
+  "cmd": 3,
+  "data": {
+    "result": "ok",
+    "list": [
+      {
+        "type": "pe",
+        "info": [
+          {
+            "sn": "10050015A00X25070634",
+            "idInfo": [
+              {
+                "id": 5,
+                "idData": "1194",
+                "resultOne": "ok"
+              }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 构造示意:`idData = "1194"` 表示 4500 Hz,数值为假设值,非实测。
+
+| 字段                   | 类型   | 说明                                                                  |
+| ---------------------- | ------ | --------------------------------------------------------------------- |
+| `info[].idInfo`      | 数组   | 本设备各功能项的查询结果                                              |
+| `idInfo[].id`        | 整数   | 命令控制符(功能码),对应请求 `idList` 中的值                      |
+| `idInfo[].idData`    | 字符串 | 读取到的该功能码对应配置 / 状态值,十六进制字符串形式;结构参照 5.4.1 |
+| `idInfo[].resultOne` | 字符串 | 该功能项的处理结果                                                    |
+
+---
+
+### 5.4 设置参数(cmd = 1)
+
+> `id` 并非「某个数据参数的编号」,而是**命令控制符(功能码)**:每个 `id` 代表一种对子设备的操作,`setVal` 是该操作的「函数输入参数」,其内容结构随 `id` 不同而不同。功能码及其输入参数结构见 5.4.1。
+
+#### 请求
+
+```json
+{
+  "cmd": 1,
+  "data": {
+    "list": [
+      {
+        "type": "pe",
+        "info": [
+          {
+            "sn": "10050015A00X25070634",
+            "paraList": [
+              { "id": 6, "setVal": "010100015984" },
+              { "id": 6, "setVal": "020100015981" },
+              { "id": 6, "setVal": "030100015983" },
+              { "id": 6, "setVal": "04030003FD34" }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 示例取自已验证代码 `ChargeChannel`(`service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go`):一次配置 4 个可配采样通道,同一功能码 `id=6` 重复出现,由 `setVal` 首字节的通道索引区分。按 5.4.1 解析:
+>
+> | setVal             | 索引号 | 数据类型          | 数据            |
+> | ------------------ | ------ | ----------------- | --------------- |
+> | `01 01 00015984` | 1      | 默认方式,Uint16  | 地址 0x00015984 |
+> | `02 01 00015981` | 2      | 默认方式,Uint16  | 地址 0x00015981 |
+> | `03 01 00015983` | 3      | 默认方式,Uint16  | 地址 0x00015983 |
+> | `04 03 0003FD34` | 4      | 默认方式,float32 | 地址 0x0003FD34 |
+
+| 字段                  | 类型   | 说明                                                         |
+| --------------------- | ------ | ------------------------------------------------------------ |
+| `info[].paraList`   | 数组   | 需要执行的功能项列表                                         |
+| `paraList[].id`     | 整数   | 命令控制符(功能码),取值见 5.4.1                           |
+| `paraList[].setVal` | 字符串 | 该功能码的输入参数(结构见 5.4.1);无输入参数的功能码可为空 |
+
+#### 回复
+
+```json
+{
+  "cmd": 1,
+  "data": {
+    "result": "ok",
+    "list": [
+      {
+        "type": "pe",
+        "info": [
+          {
+            "sn": "10050015A00X25070634",
+            "setResultList": [
+              { "id": 6, "resultOne": "ok" },
+              { "id": 6, "resultOne": "ok" },
+              { "id": 6, "resultOne": "ok" },
+              { "id": 6, "resultOne": "ok" }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+> 暂无 cmd=1 回复实报,本例为按协议构造的示意。重复功能码的结果是否严格按请求顺序逐项返回,待实测确认。
+
+| 字段                          | 类型   | 说明                                             |
+| ----------------------------- | ------ | ------------------------------------------------ |
+| `info[].setResultList`      | 数组   | 本设备各功能项的执行结果                         |
+| `setResultList[].id`        | 整数   | 命令控制符(功能码),对应请求 `paraList[].id` |
+| `setResultList[].resultOne` | 字符串 | 该功能项的执行结果                               |
+
+#### 5.4.1 命令控制符(id / 功能码)定义
+
+`id` 为命令控制符(功能码),指明对设备执行的操作。设置(cmd=1)时 `setVal` 为该功能码的输入参数;查询(cmd=3)时无输入参数,查询返回的数据结构可参照对应功能码的参数结构。
+
+| id | 功能                 | 设置时的输入参数(setVal)结构                                                                                                                        |
+| -- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 1  | 清除设备故障波形     | 无输入参数                                                                                                                                            |
+| 2  | 采样通道配置功能     | 「最大可配置的采样通道总数」(1 字节) + 「第 1 个采样通道含义」(2 字节, 大端) + … + 「第 n 个采样通道含义」(2 字节, 大端)。通道含义编号见第 10 节 F14 |
+| 3  | 手动触发故障录波     | 无输入参数                                                                                                                                            |
+| 4  | 设备对时             | 7 字节,按「年(2 字节) 月 日 时 分 秒」顺序。例:`07 E6 01 01 01 01 01` 表示 2022-01-01 1:1:1(年为大端)                                           |
+| 5  | 设置故障录波采样频率 | 2 字节,单位 Hz。例:`11 94` 表示 4500 Hz(大端)                                                                                                   |
+| 6  | 配置可配采样通道功能 | 「可配采样通道的索引号」(1 字节) + 「数据类型」(1 字节) + 「数据」(4 字节),见下方说明                                                                |
+| 7  | 故障波形功能状态控制 | 「控制字节」(1 字节):`0h`=关闭故障录波;`1h`=使能故障录波;其他=无效                                                                             |
+
+**id=6「配置可配采样通道功能」的参数结构**:
+
+- 索引号(1 字节):被配置的可配采样通道索引。
+- 数据类型(1 字节),拆为高半字节 ByteH 与低半字节 ByteL:
+  - ByteH = 设置方式:`0h`=默认方式(需携带采样通道地址类型与地址信息);`1h`=代码方式(需携带采样通道参数代码);其他=无效。
+  - ByteL = 采样通道地址类型(仅默认方式 ByteH=0h 时有效):`0h`=Empty;`1h`=Uint16;`2h`=int16;`3h`=float32;`4h`=Uint32_HWord;`5h`=Uint32_LWord;`6h`=Uint32;其他=无效。
+- 数据(4 字节),按设置方式解析:
+  - ByteH=0h(默认方式):4 字节表示要设置的地址。例:`00 31 11 94` 表示地址 0x311194。
+  - ByteH=1h(代码方式):前 2 字节表示采样通道代码(参数代码见 F14),后 2 字节无效。例:`12 34 xx xx` 表示采样通道代码 0x1234。
+
+**`setVal` 的编码方式**:`setVal` 为**十六进制字符串**,每 2 个字符表示 1 个字节。例如 `setVal = "010100015984"` 表示字节序列 `0x01 0x01 0x00 0x01 0x59 0x84`。各功能码输入参数的字节内容按上表 / 上文结构填写后,整体以十六进制字符串形式放入 `setVal`。
+
+> 对时(id=4)示例:7 字节 `07 E6 01 01 01 01 01` → `setVal = "07E60101010101"`(14 个十六进制字符)。采样频率(id=5)设 4500Hz 示例:2 字节 `11 94` → `setVal = "1194"`。
+
+> 备注(设备端实现):`setVal` 的十六进制字符串由设备端负责转换为字节后发往子设备。此转换属设备端实现细节,待后续联调测试确认,不影响本协议对接。
+
+> 查询(cmd=3):请求 `idList` 中填功能码,无输入参数;返回的 `idData` 为对应功能码的当前配置 / 状态,其数据结构**参照上表中该功能码的参数结构**(例如查 id=2 返回采样通道配置、查 id=5 返回当前采样频率等)。
+
+---
+
+### 5.5 退出(cmd = 4)
+
+通知设备结束当前处理流程。
+
+```json
+{
+  "cmd": 4,
+  "data": {}
+}
+```
+
+该命令不产生业务数据回复。
+
+---
+
+### 5.6 连通性测试(cmd = 90 / 99)
+
+用于链路连通性验证。
+
+```json
+{
+  "cmd": 90,
+  "data": {}
+}
+```
+
+设备回复(错误回复结构,见第 6 节):
+
+- `cmd = 90` → `data.result = "ok"`
+- `cmd = 99` → `data.result = "test fail"`
+
+---
+
+## 6. 错误回复
+
+当命令无法正常进入业务处理(如格式错误、字段缺失、设备忙、序列号无效等)时,设备返回简化的错误回复:
+
+```json
+{
+  "cmd": <命令码>,
+  "data": {
+    "result": "<错误说明>"
+  }
+}
+```
+
+| `data.result`     | 触发原因                     |
+| ------------------- | ---------------------------- |
+| `json unpack err` | 消息体不是合法 JSON          |
+| `cmd unpack err`  | 缺少 `cmd` 字段            |
+| `data unpack err` | 缺少 `data` 字段           |
+| `busy`            | 设备正忙,收到非「退出」命令 |
+| `pe sn not find`  | `pe` 设备序列号无法匹配    |
+| `cmd no define`   | 不支持的命令码               |
+| `ok`              | 测试命令(cmd=90)的成功回复 |
+| `test fail`       | 测试命令(cmd=99)的回复     |
+
+> 当 `cmd` 字段无法解析时,错误回复中的 `cmd` 取值为 0。
+
+---
+
+## 7. 字段速查
+
+| 字段                   | 出现位置            | 类型     | 含义                                                       |
+| ---------------------- | ------------------- | -------- | ---------------------------------------------------------- |
+| `cmd`                | 顶层                | 整数     | 命令码                                                     |
+| `data`               | 顶层                | 对象     | 数据体                                                     |
+| `data.result`        | 回复                | 字符串   | 整体结果:`ok` / `error`(或错误说明)                 |
+| `data.filename`      | 获取记录回复        | 字符串   | 导出文件标识                                               |
+| `list`               | data                | 数组     | 设备分组列表                                               |
+| `type`               | list                | 字符串   | 设备类型:`pe` / `aGate`                               |
+| `info`               | list                | 数组     | 设备项列表                                                 |
+| `sn`                 | info                | 字符串   | 设备序列号                                                 |
+| `codeList`           | info(请求,cmd=0) | 整数数组 | 数据项命令码列表                                           |
+| `index`              | info(请求,cmd=2) | 整数数组 | 记录索引列表                                               |
+| `idList`             | info(请求,cmd=3) | 整数数组 | 命令控制符(功能码)列表                                   |
+| `paraList`           | info(请求,cmd=1) | 数组     | 功能项列表                                                 |
+| `paraList[].id`      | paraList            | 整数     | 命令控制符(功能码)                                       |
+| `paraList[].setVal`  | paraList            | 字符串   | 功能码的输入参数(十六进制字符串)                         |
+| `hexList`            | info(回复,cmd=0) | 数组     | 实时数据结果列表                                           |
+| `hexList[].code`     | hexList             | 整数     | 数据项命令码                                               |
+| `hexList[].hexData`  | hexList             | 字符串   | 数据值(十六进制字符串)                                   |
+| `indexInfo`          | info(回复,cmd=2) | 数组     | 记录结果列表                                               |
+| `indexInfo[].index`  | indexInfo           | 整数     | 记录数据类型码:4=故障数据,5=波形数据(非请求的记录序号) |
+| `idInfo`             | info(回复,cmd=3) | 数组     | 功能码查询结果列表                                         |
+| `idInfo[].id`        | idInfo              | 整数     | 命令控制符(功能码)                                       |
+| `idInfo[].idData`    | idInfo              | 字符串   | 功能码对应的配置 / 状态值(十六进制字符串)                |
+| `setResultList`      | info(回复,cmd=1) | 数组     | 功能项执行结果列表                                         |
+| `setResultList[].id` | setResultList       | 整数     | 命令控制符(功能码)                                       |
+| `resultOne`          | 各回复子项          | 字符串   | 子项处理结果                                               |
+
+---
+
+## 8. 编号字段速览(code / index / id)
+
+本协议中有三类编号字段,分别用于不同命令。下面汇总其含义与取值出处。
+
+### 8.1 `code`(获取实时数据,cmd=0)
+
+- 表示要读取的实时数据类别,取值 1~3,定义见 5.1。code=4/5 不能通过 cmd=0 查询,须用 cmd=2 按记录导出。
+- 读取结果以十六进制字符串放入回复的 `hexData`;各 code 的 `hexData` 字节结构见 5.1.1。
+
+### 8.2 `index`(获取记录,cmd=2)
+
+- 表示要导出的记录序号,取值来源于 code=3「故障录波总目录」中各记录的位置索引(见 5.1.1)。
+- 每条记录包含故障数据(failuredata)与波形数据(waveformdata)两类,分别保存为文件(见 5.2.1)。
+
+### 8.3 `id`(设置 / 查询,cmd=1 / cmd=3)
+
+- 为**命令控制符(功能码)**,表示对设备执行的一种操作,而非某个数据参数的编号。
+- 功能码取值、设置时的输入参数 `setVal` 结构、查询返回的数据结构,均见 5.4.1。
+
+---
+
+## 9. 子项处理结果(resultOne)取值
+
+读取 / 设置的每个子项(`hexList[]`、`indexInfo[]`、`idInfo[]`、`setResultList[]`)都带一个 `resultOne` 字符串,表示该子项的处理结果。取值如下:
+
+| `resultOne` 取值             | 含义                                                                             |
+| ------------------------------ | -------------------------------------------------------------------------------- |
+| `ok`                         | 该子项处理成功;对应的 `hexData` / `idData` 有效                             |
+| `cancomerr:suberrcode=<hex>` | 与子设备通信时,子设备返回了错误码;`<hex>` 为子设备返回的错误代码(十六进制) |
+| `cancomerr:time out`         | 与子设备通信超时                                                                 |
+| `cancomerr:index=<hex>`      | 其他通信类错误;`<hex>` 为设备内部的错误判定码(十六进制)                     |
+| `emserr:can p null`          | 未取到目标子设备(子设备不可达 / 不存在)                                        |
+| `emserr:can data null`       | 子设备返回数据为空                                                               |
+| `emserr:set cmd err`         | 下发命令失败                                                                     |
+| `emserr:other`               | 其他错误                                                                         |
+
+> 当 `resultOne` 不为 `ok` 时,对应子项的 `hexData` / `idData` 可能为空或无效。
+> 其中 `cancomerr:suberrcode=<hex>` 里的 `<hex>` 为子设备错误码(F12,代码表见 10.6);`cancomerr:index=<hex>` 里的 `<hex>` 为设备内部判定码(F13,数值表见 10.5)。
+
+---
+
+## 10. 编号 / 代码字段汇总与消歧
+
+本节汇总协议中各类编号 / 代码字段的含义与取值出处,并对同名字段做消歧。各字段的取值表或来源均已在本文档内给出或指明。
+
+> 为避免混淆:`code`、`id`、`index`、`resultOne` 等名字在「请求 / 回复」「不同命令」里重复出现,但含义不同。下表用**完整 JSON 路径**(从消息根对象 `{}` 开始,`data.list[].info[]` 为设备项层级)加上**命令号与方向**唯一标识每个字段,请以路径为准,不要只看字段末名。
+
+### 10.1 编号字段清单(按完整 JSON 路径定位)
+
+**状态总览**:
+
+| 字段            | 含义与取值出处                                                          |
+| --------------- | ----------------------------------------------------------------------- |
+| F1、F2          | 实时数据类别 `code`,取值 1~3,见 5.1                                 |
+| F3              | 各 code 的 `hexData` 字节结构,见 5.1.1                               |
+| F4              | 记录序号,取值即 code=3 目录中各记录的 `index`(见 5.1.1)            |
+| F5、F6、F8、F10 | 命令控制符(功能码),见 5.4.1                                          |
+| F7              | 查询返回值 `idData`,结构参照 5.4.1 对应功能码                        |
+| F9              | 设置输入参数 `setVal`,十六进制字符串,结构见 5.4.1                   |
+| F11             | 记录文件保存格式与字节结构,见 5.2.1~5.2.3                              |
+| F12             | 下级设备错误码表,见 10.6                                               |
+| F13             | 设备内部通信错误判定码,见 10.5                                         |
+| F14             | 通道含义参数代码(Uint16 编号 → 物理量),对照表为产品级《参数代码表》 |
+| F15             | 事件触发源代码表,见 10.4                                               |
+
+> 下表为完整清单,列出每个字段的完整 JSON 路径、含义与取值出处。
+
+| 编号 | 完整 JSON 路径                                                                                  | 命令 / 方向           | 类型             | 含义                                                                                                                                               | 取值出处                               |
+| ---- | ----------------------------------------------------------------------------------------------- | --------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
+| F1   | `data.list[].info[].codeList[]`                                                               | cmd=0 请求            | 整数数组         | 实时数据类别编号:1-故障总体信息;2-设备故障录波功能基本信息;3-故障录波总目录(4/5 不能用于 cmd=0)                                               | 5.1                                    |
+| F2   | `data.list[].info[].hexList[].code`                                                           | cmd=0 回复            | 整数             | 回显请求 F1 的编号                                                                                                                                 | 5.1                                    |
+| F3   | `data.list[].info[].hexList[].hexData`                                                        | cmd=0 回复            | 字符串(hex)      | 各 code 返回数据的原始字节(十六进制字符串)                                                                                                       | 字节结构:5.1.1;内含编号见 F14 / F15  |
+| F4   | `data.list[].info[].index[]`                                                                  | cmd=2 请求            | 整数数组         | 记录序号,取值即 code=3 目录中各记录的位置索引 `index`(从 1 开始)。对端应先用 cmd=0 + code=3 查目录,再以目录中列出的 `index` 作为本字段取值 | 5.1.1(code=3)                        |
+| F5   | `data.list[].info[].idList[]`                                                                 | cmd=3 请求            | 整数数组         | 命令控制符(功能码)                                                                                                                               | 5.4.1                                  |
+| F6   | `data.list[].info[].idInfo[].id`                                                              | cmd=3 回复            | 整数             | 回显请求 F5 的功能码                                                                                                                               | 5.4.1                                  |
+| F7   | `data.list[].info[].idInfo[].idData`                                                          | cmd=3 回复            | 字符串(hex)      | 查询返回的功能码对应配置 / 状态字节(如 id=2 返回采样通道配置、id=5 返回采样频率)                                                                 | 5.4.1(对应功能码结构)                |
+| F8   | `data.list[].info[].paraList[].id`                                                            | cmd=1 请求            | 整数             | 命令控制符(功能码)                                                                                                                               | 5.4.1                                  |
+| F9   | `data.list[].info[].paraList[].setVal`                                                        | cmd=1 请求            | 字符串(hex)      | 功能码输入参数,十六进制字符串(每 2 字符 = 1 字节,如 `"010100015984"` = `01 01 00 01 59 84`)                                                | 5.4.1                                  |
+| F10  | `data.list[].info[].setResultList[].id`                                                       | cmd=1 回复            | 整数             | 回显请求 F8 的功能码                                                                                                                               | 5.4.1                                  |
+| F11  | 设备侧导出的记录文件(`failuredata`/`waveformdata`)内容                                    | cmd=2(非 MQTT 字段) | 文本文件         | 导出记录的落盘文件                                                                                                                                 | 保存规则 5.2.1;字节结构 5.2.2 / 5.2.3 |
+| F12  | `resultOne` = `cancomerr:suberrcode=<hex>` 中的 `<hex>`                                   | 回复子项              | 整数(hex)        | 子设备返回的错误码                                                                                                                                 | 10.6                                   |
+| F13  | `resultOne` = `cancomerr:index=<hex>` 中的 `<hex>`                                        | 回复子项              | 整数(hex)        | 设备内部的通信错误判定码                                                                                                                           | 10.5                                   |
+| F14  | 通道含义编号(参数代码),见 code=1 的 `hexData`(5.1.1)、code=4 的 `failuredata`(5.2.2) | cmd=0 / cmd=2         | 整数编号(Uint16) | 快照 / 采样通道的含义编号(快照与采样共用同一套参数代码),标识通道对应的物理量;code=1 的可配通道「类型 / 地址」标识可配通道所记录的变量          | 产品级《参数代码表》(编号 → 物理量) |
+| F15  | 事件触发源代码,见 code=3(5.1.1)、code=4(5.2.2)的「事件触发源」字段                         | cmd=0                 | 整数(Uint16)     | 事件触发源                                                                                                                                         | 10.4(表外代码为未定义)               |
+
+### 10.2 同名字段消歧(重点)
+
+以下几组字段末名相同但不是同一个东西,按路径区分:
+
+| 末名                        | 不同含义的字段                                                                                                                   | 区别                                                                                                                                                               |
+| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `index`                   | ① `data.list[].info[].index[]`(cmd=2 **请求**,F4)② `data.list[].info[].indexInfo[].index`(cmd=2 **回复**) | ① 是请求的**记录序号**,取值来源于 code=3 目录(见 5.1.1)。② 是**记录数据类型码**(固定 4=故障数据 / 5=波形数据,见 5.2)。两者含义不同,均已明确。 |
+| `code`                    | `data.list[].info[].hexList[].code`(cmd=0 回复,F2)                                                                          | 仅此一处;它回显请求 `codeList`(F1)的元素。                                                                                                                    |
+| `id`                      | `idList[]`(F5) / `idInfo[].id`(F6) / `paraList[].id`(F8) / `setResultList[].id`(F10)                                     | 四处均为**同一套命令控制符(功能码)**,分布在不同命令 / 方向;取值见 5.4.1。                                                                                |
+| `result` vs `resultOne` | `data.result`(整条命令结果)`...resultOne`(单个子项结果)                                                                  | `data.result` 取值 `ok`/`error`(见 4.3);`resultOne` 取值见第 9 节;其中内嵌的 `<hex>` 代码表见 10.5(F13)/ 10.6(F12)。                             |
+
+> 注意 F13:`cancomerr:index=<hex>` 里的 `index` 与记录序号、记录类型码都**无关**,它是设备内部的通信错误判定码,与子设备无关。其数值表见 10.5。
+
+### 10.3 各编号 / 代码表的取值出处汇总
+
+本协议所涉编号 / 代码字段的取值表或来源均已在本文档内给出或指明:
+
+| 字段                                                                 | 取值出处                                   |
+| -------------------------------------------------------------------- | ------------------------------------------ |
+| 实时数据类别 `code`(cmd=0)                                       | 5.1                                        |
+| `hexData` 字节结构(code=1/2/3)                                   | 5.1.1                                      |
+| 记录文件字节结构(code=4 `failuredata` / code=5 `waveformdata`) | 5.2.2 / 5.2.3                              |
+| 记录序号 `index`(cmd=2)                                          | 来源于 code=3 目录(5.1.1)                |
+| 命令控制符 `id` 及输入参数 `setVal`、查询返回结构(cmd=1/3)     | 5.4.1                                      |
+| 通道含义参数代码(F14)                                              | 产品级《参数代码表》(编号 → 物理量对照) |
+| 事件触发源代码(F15)                                                | 10.4                                       |
+| 设备内部通信错误判定码(F13)                                        | 10.5                                       |
+| 子设备错误码(F12)                                                  | 10.6                                       |
+
+### 10.4 事件触发源代码表(F15)
+
+以下为「事件触发源」字段(`triggerSource`,Uint16)的取值定义。该字段出现在 code=3(故障录波总目录,见 5.1.1)与 code=4(单一故障信息,见 5.2.2)中。
+
+| 代码       | 含义                     |
+| ---------- | ------------------------ |
+| `0xFF0`  | 手动触发 _Fix            |
+| `0xFF1`  | 模拟故障触发 _Auto       |
+| `0x1000` | SL_DSP 预留触发源 16_1   |
+| `0x1001` | 并网发生状态切换故障     |
+| `0x1002` | 离网发生 aPower 离运问题 |
+| `0x1003` | 油机发生状态切换故障     |
+| `0x5000` | PE_INV 预留触发源 16_1   |
+| `0x5001` | 运行态下逆变故障         |
+| `0xD000` | MPPT 预留触发源 16_1     |
+| `0xD001` | 运行态下 MPPT 故障       |
+| `0xD800` | APBOX2.0 预留触发源 16_1 |
+| `0xD801` | 运行态下 APBOX2.0 故障   |
+| `0xE000` | DIF 预留触发源 16_1      |
+| `0xE001` | 并网发生状态切换故障     |
+| `0xE002` | 离网发生 aPower 离运问题 |
+
+> 上表以外的代码为未定义,待后续补充。
+
+### 10.5 内部通信错误判定码(F13)
+
+`resultOne` 为 `cancomerr:index=<hex>` 时,`<hex>` 是**设备内部**在与子设备通信过程中判定的错误类型码(以十六进制输出)。此表为设备自身定义,与子设备无关。
+
+说明:这些码对应与子设备通信在不同阶段(数据读取、记录传输、参数读写)的异常,供排查参考。
+
+| `<hex>` | 含义                                                |
+| --------- | --------------------------------------------------- |
+| `3`     | 下发命令与子设备回复的命令不一致                    |
+| `4`     | 数据读取阶段:子设备回复错误                        |
+| `5`     | 数据读取阶段:回复的数据类别不符                    |
+| `6`     | 记录传输阶段:起始报文接收错误                      |
+| `7`     | 记录传输阶段:后续数据未接收完成                    |
+| `8`     | 记录传输阶段:数据块不连续                          |
+| `9`     | 记录传输阶段:数据块内序号不连续                    |
+| `a`     | 记录传输阶段:收到多余的后续数据                    |
+| `b`     | 记录传输阶段:首包数据过短                          |
+| `c`     | 记录传输结束阶段:后续数据未接收完成                |
+| `d`     | 数据读取阶段:接收超时                              |
+| `e`     | 记录传输阶段:接收超时                              |
+| `f`     | 记录传输结束阶段:接收超时                          |
+| `10`    | 其他命令:接收超时                                  |
+| `11`    | 记录传输阶段:后续数据接收超时                      |
+| `12`    | 参数读写阶段:读 / 写类型不一致(如请求读却回复写) |
+| `13`    | 参数读写阶段:回复的功能码不一致                    |
+| `14`    | 参数读写阶段:回复数据过短                          |
+| `15`    | 参数读写阶段:接收超时                              |
+| `16`    | 参数读写阶段:后续数据未接收完成                    |
+
+> 说明:
+>
+> - 另有「子设备回复错误」与「通用超时」两种情况,分别以 `cancomerr:suberrcode=<hex>`(F12)与 `cancomerr:time out` 形式返回,**不会**出现在 `cancomerr:index=` 中。故本表取值集中在 `0x3`~`0x16`。
+> - 这些是「设备侧判定」的通信错误;而 F12(`suberrcode`)是「子设备回复」的错误码,二者来源不同,不要混淆。
+
+### 10.6 子设备错误码表(F12 / suberrcode)
+
+`resultOne` 为 `cancomerr:suberrcode=<hex>` 时,`<hex>` 是**子设备**返回的错误码(否定响应码),以十六进制输出。取值定义如下。
+
+| 代码     | 名称                                     | 说明                                       |
+| -------- | ---------------------------------------- | ------------------------------------------ |
+| `0x10` | GeneralReject                            | 诊断服务被服务器(ECU)拒绝                  |
+| `0x11` | serviceNotSupported                      | ECU 不支持该诊断请求                       |
+| `0x12` | subFunctionNotSupported                  | ECU 不支持该诊断请求子功能                 |
+| `0x13` | incorrectMessageLengthOrInvalidFormat    | 请求报文长度或格式错误                     |
+| `0x14` | ResponseTooLong                          | 诊断响应数据长度超出服务器所支持的最大长度 |
+| `0x22` | conditionsNotCorrect                     | 条件错误                                   |
+| `0x23` | commTimeoutError                         | 自定义,通信超时                           |
+| `0x31` | requestOutOfRange                        | 请求超过范围                               |
+| `0x33` | securityAccessDenied                     | 安全访问拒绝该请求                         |
+| `0x35` | invalidKey                               | 反馈密钥错误                               |
+| `0x36` | exceedNumberOfAttempts                   | 安全访问尝试次数超限                       |
+| `0x37` | requiredTimeDelayNotExpired              | 安全访问延时未到期                         |
+| `0x70` | UploadDownloadNotAccepted                | 上传或下载数据失败                         |
+| `0x71` | TransferDataSuspended                    | 由于故障导致数据传输操作被中断             |
+| `0x72` | GeneralProgrammingFailure                | 擦除或写入 Flash 出现错误                  |
+| `0x73` | WrongBlockSequenceCounter                | 数据包序列编号错误                         |
+| `0x78` | requestCorrectlyReceived-ResponsePending | ECU 正忙,无法及时响应该请求               |
+| `0x7F` | serviceNotSupportedInActiveSession       | ECU 不支持当前诊断模式下进行该 SID 请求    |
+
+> 说明:此表为子设备(PE / aGate)定义的错误码,由子设备在负响应中返回、经设备原样透传到 `suberrcode`。请勿与 F13(10.5,设备内部判定码)混淆:F12 是「子设备回复的错误」,F13 是「本设备判定的错误」。

+ 35 - 0
docs/fault-record/README.md

@@ -0,0 +1,35 @@
+# 故障录波文档导航
+
+本目录整理通过 MQTT 直接获取和设置子设备故障录波数据的需求与设计,覆盖生产服和测试服。当前状态:需求和整体设计已确认,待编码;双环境 MQTT 客户端已完成,其余未实现(见 [implementation-status.md](implementation-status.md))。文档中的待实测内容不代表设备已支持。
+
+建议阅读顺序:requirements → technical-design → ui-design;查协议细节再看 protocol-specification 和 FaultFilter 文档。
+
+## 文档职责
+
+| 文档 | 内容 | 何时读取 |
+| --- | --- | --- |
+| [requirements.md](requirements.md) | 四项功能目标、预期行为、待确认的产品口径 | 确定要做什么、讨论验收范围时 |
+| [technical-design.md](technical-design.md) | 分层架构、MQTT 客户端、异步操作与恢复、存储、取消、双环境 | 讨论如何实现、评审后端设计时 |
+| [ui-design.md](ui-design.md) | 菜单结构、各功能页面布局和交互 | 实现或评审前端页面时 |
+| [FaultFilter_MQTT协议文档.md](FaultFilter_MQTT协议文档.md) | 设备侧故障录波服务的业务协议:命令码、字段、`hexData` 字节结构、功能码、错误码 | 业务层字段的权威依据;与其他文档冲突时以它为准 |
+| [protocol-specification.md](protocol-specification.md) | `377~380` 逐字段结构、示意报文、关联规则和编号补充清单 | 核对协议结构、逐项补充缺失信息时 |
+| [protocol-observations.md](protocol-observations.md) | `377~380` 报文、真实样本时间线、旧协议描述和存疑项 | 判断协议字段、核对新抓包时 |
+| [implementation-status.md](implementation-status.md) | 当前仓库已实现的能力、代码入口和差距 | 开始修改代码或核对当前行为时 |
+| [CloudLog_2026-10-14_104644.csv](CloudLog_2026-10-14_104644.csv) | 2026-10-14 测试服原始通讯日志,含 `607~610` 上传过程 | 核对样本原文时 |
+| [2026-10-14_05_46_20.tar.bz2](2026-10-14_05_46_20.tar.bz2) | 上述日志对应的录波文件(实为 gzip tar),记录序号 7 | 开发和测试文件解析时 |
+
+## 事实与决策的边界
+
+- **真实报文**用于确认样本覆盖的消息字段和顺序;单份日志不证明未覆盖场景的行为。
+- **已验证代码**用于确认现有参数设置请求结构;当前代码行为见实现状态文档。
+- **旧协议描述**保留为待验证线索。与真实报文冲突时,以真实报文为准。
+- **用户后续确认**记录在协议详细描述中;与旧样本不同的服务端约定单独标注,不改写历史样本。
+- **功能需求**定义目标;**技术设计**只记录当前可支持的方案与必须先验证的条件。两者不因现有代码或单份日志而自动改写。
+
+## 与 DataDownload 的关系
+
+[DataDownload](../data-download/README.md) 已有基于 FWH API 的故障录波召唤、下载和归档流程。本目录讨论新增的 MQTT 直连路径。MQTT 路径的文件同样由平台上传到 hes-upload,`607.faultRecordID` 是平台召唤记录 ID,可与 DataDownload 的记录互查。用户决定:本功能完整实现并测试通过后,再考虑开放给 DataDownload;在此之前 DataDownload 继续使用原 FWH API 方案。本阶段不把现有 DataDownload 行为视为 MQTT 协议事实。
+
+## 维护方式
+
+新增抓包先更新 `protocol-observations.md` 并标明证据;字段和协议规则确认后更新 `protocol-specification.md`;产品范围变化更新 `requirements.md`;确认设计决策后更新 `technical-design.md`;页面调整更新 `ui-design.md`;代码落地后再更新 `implementation-status.md`。避免把同一项存疑写成多处互相冲突的结论。

+ 47 - 0
docs/fault-record/implementation-status.md

@@ -0,0 +1,47 @@
+# 故障录波当前实现状态
+
+> 核对日期:2026-10-16。本文只描述当前仓库事实,不代表设计目标已经落地。目标见 [requirements.md](requirements.md),设计见 [technical-design.md](technical-design.md) 和 [ui-design.md](ui-design.md)。
+
+## 1. 能力现状
+
+| 能力 | 当前状态 | 代码依据 |
+| --- | --- | --- |
+| 生产服/测试服 MQTT | **已完成**(2026-10-16):按环境各建一个客户端实例;单个环境初始化失败只记日志、不影响服务启动;原复制包 `fwh_mqtt_test` 已删除 | `service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go` 的 `Env`、`ParseEnv`、`InitFwhMqttClient(env, cfg)`;`service/main.go`;单元测试 `fwh_mqtt_client_test.go`。未用真实设备验证 |
+| MQTT 收发方式 | 只有一问一答(`sendMessageWait`)和只发送(`sendMessageOnce`);没有持续监听;空闲 5 分钟断开 | `fwh_mqtt_client.go` |
+| 参数设置 | 已有 MQTT 入口,等待 `378` 后返回;接口支持可选 `env`;没有观察 `379` 逐项结果;前端无环境选择、无确认弹窗 | `fwh_mqtt_fault_record.go`、`/faultRecord/paramSetting`、`src/views/fault-record/write/index.vue` |
+| 设备信息(`cmd=0`) | 未实现 | — |
+| 录波记录获取(`cmd=2` + `607~610` + 下载) | 未实现;`607~610` 只有日志类型常量 | `service/domain/fwhProMqttDomain.go` |
+| 参数读取(`cmd=3`) | 未实现 | `src/views/fault-record/read/index.vue` 为空页面 |
+| 取消(`cmd=4`) | 未实现 | — |
+| 协议编解码、文件解析、CSV | 未实现;`service/function/FaultRecordFile` 为空目录 | — |
+| 异步任务与恢复 | 未实现;可复用 asynq 任务系统 | `service/task`,参考 `task/queue/dataDownloadQueue` |
+| 操作记录与文件存储 | 未实现;可复用 MongoDB、MinIO | `service/repositories/mongo`、`service/repositories/minio`、`service/utils/minio` |
+| 站点设备列表 | 生产服、测试服的站点信息接口均已存在,未用于本功能 | `fwh_pro.QuerySiteInfoList`、`fwh_test.QuerySiteInfoList`(`fhpSn` 为 aPower SN 列表) |
+| 前端菜单 | 现为“故障录波配置 → 读取配置/写入配置”,未按新结构调整 | `src/router/record.ts` |
+
+## 2. 现有代码入口
+
+| 位置 | 当前作用 |
+| --- | --- |
+| `service/utils/mqttClient/mqttClient.go` | paho 封装:连接、订阅(断线重连后自动恢复)、发布 |
+| `service/repositories/mqtt/fwh_mqtt/fwh_mqtt_client.go` | 按环境的 FWH MQTT 客户端;发送经 worker 串行处理 |
+| `service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go` | `377/378` 常量、参数设置结构、`FaultRecordParamSetting`、调试用 `ChargeChannel` |
+| `service/repositories/mqtt/fwh_mqtt/fwh_mqtt_debug.go` | `EnableXlog`,固定使用生产服 |
+| `service/repositories/mqtt/fwh_mqtt/test/main.go` | 测试服手动调试脚本 |
+| `service/services/faultRecordServices.go`、`controllers/faultRecordControllers.go`、`routes/faultRecordRoutes.go` | 参数设置 HTTP 路径 `/faultRecord/paramSetting` |
+| `src/views/fault-record/write/index.vue`、`read/index.vue` | 旧参数设置页面、读取页占位 |
+| `service/repositories/api/fwh_pro/fwh_faultRecord.go` | 现有 FWH API 故障录波流程(DataDownload 使用,不改动) |
+
+## 3. 与设计的差距
+
+按 [technical-design.md](technical-design.md) 的分层,尚需实现:
+
+1. **L2 协议编解码与解析器**:`377` 构造;`378/379/607~610` 解析(含 `608` 重复键);解压;`hexData`、`failuredata`、`waveformdata` 解析;CSV 生成。
+2. **L1 客户端改造**:增加 `Subscribe`(多订阅者、引用计数、订阅期间不空闲断开)和通用信封构造;把业务代码移出客户端包。
+3. **下载**:按环境域名、无 token 下载 hes-upload 文件。
+4. **存储**:Mongo `fault_record_operations`、MinIO `fault-record/<env>/<gatewaySn>/<fileName>`。
+5. **L3 任务执行器**:`faultRecordQueue`、网关锁、状态机、取消、重启恢复。
+6. **L4/L5 接口**:创建操作、查询状态与历史、取消、导出、设备列表。
+7. **前端**:新菜单结构和四个功能页、录波详情、参数设置新页面。
+
+依赖实测才能最终确认的部分见 [technical-design.md](technical-design.md#9-待实测前提)。

+ 217 - 0
docs/fault-record/protocol-observations.md

@@ -0,0 +1,217 @@
+# 故障录波 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/<filename>/<sn>/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` 键重复) |
+| 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/<equipNo>/<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/<equipNo>/<fileName>` 规律下载。用户确认下载**不需要 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 文本,无换行,与文档一致。`<sn>` 为 `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《参数代码表》,目前只能展示原始值。
+
+## 待验证事项
+
+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)。

+ 474 - 0
docs/fault-record/protocol-specification.md

@@ -0,0 +1,474 @@
+# 故障录波 MQTT 协议详细描述
+
+> 状态:待补全的协议描述,2026-10-12;2026-10-14 按 [FaultFilter_MQTT协议文档](FaultFilter_MQTT协议文档.md) 更正业务层字段;2026-10-16 补充 `607~610` 文件上传与下载地址(第 8A 节)。本文按现有信息逐字段描述 `377~380`,供补充和确认使用;不是已经完成设备验证的最终规范。
+>
+> 业务层(`dataArea` 内的 `cmd/data`、设备回复的 `result/list`)以 FaultFilter 协议文档为准;外层(`cmdType`、`snno`、`crc`、topic 等)不在该文档范围内,仍按本文记录。原始证据与样本时间线见 [protocol-observations.md](protocol-observations.md)。功能目标见 [requirements.md](requirements.md),架构讨论见 [technical-design.md](technical-design.md)。本文中的字段类型不等于完整的必填性、值域或失败行为约束。
+
+## 1. 信息来源与阅读规则
+
+| 标记             | 来源与适用范围                                                                                                                        |
+| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| 样本记录         | 观察文档整理的 CSV 通讯记录,仅覆盖`cmd=0/2`、`aGate` 和成功结果;2026-09-29 附件未入库,2026-10-14 附件为 `CloudLog_2026-10-14_104644.csv`(含 `607~610`) |
+| 已验证代码       | 用户此前确认可信的现有参数设置请求结构;代码入口为`service/repositories/mqtt/fwh_mqtt/fwh_mqtt_fault_record.go`                      |
+| FaultFilter 文档 | [FaultFilter_MQTT协议文档](FaultFilter_MQTT协议文档.md):设备侧故障录波服务的 `{cmd, data}` 业务协议;业务字段以其为准,经 `377~380` 外层转发后的表现仍需实测 |
+| 现有实现         | 仓库当前行为,不能单独证明设备协议要求                                                                                                |
+| 用户确认         | 本轮讨论确认的协议行为或服务端约定                                                                                                    |
+| 旧描述           | 先前协议说明;与 FaultFilter 文档冲突时以后者为准,其余未被样本或用户确认覆盖的部分仍待验证                                         |
+| 待补充           | 缺少资料、语义不清或证据存在冲突                                                                                                      |
+
+本文 JSON 均为**结构示意**,不是完整原始抓包。占位字符串没有协议值域含义,`hexData` 占位内容不能发送给设备;不为示意报文编造 `crc/len`。
+
+**业务层映射**:FaultFilter 文档的请求为 `{cmd, data:{list}}`,与 `377.dataArea` 一致;其回复为 `{cmd, data:{result, list, filename}}`,而样本 `379.dataArea` 中 `result/list/filename` 直接位于 `dataArea` 顶层,没有 `data` 包裹。推断网关转发时展开了 `data`,`379` 承载的即 FaultFilter 回复;该映射在 `cmd=1/3` 和错误场景下尚未实测。FaultFilter 文档中的 topic(`aws/faultrecord` / `faultfilter/faultrecord`)是该服务自身的收发主题,与云端 `fwh/s2c|c2s/mng/{SN}` 的关系待确认,不能互相替代。
+
+## 2. 协议角色、方向与传输
+
+存在两套方向不同的请求应答:
+
+| 请求    | 请求方 | 应答    | 应答方 | 用途                             |
+| ------- | ------ | ------- | ------ | -------------------------------- |
+| `377` | 本工具 | `378` | 设备   | 发起故障录波操作并获得指令应答   |
+| `379` | 设备   | `380` | FWH 平台 | 上传数据或处理结果并获得上传应答;本工具只监听 |
+| `607` | FWH 平台 | `608` | 设备 | 平台通知设备上传录波文件;本工具只监听(见 8A) |
+| `609` | 设备   | `610` | FWH 平台 | 设备报告文件上传结果;本工具只监听 |
+
+`378` 与 `379` 不是同一种响应。现有样本在 `377/378` 后出现 `379/380`,但尚未确认每次请求是否必然产生上传、上传条数以及严格顺序。两套协议分别关联;`379/380` 的关联不能反向证明某次 `379` 属于某次 `377`。FaultFilter 文档规定请求与回复通过 `cmd` 关联、设备同一时刻只处理一条业务命令(见第 4.1 节),可作为 `377→379` 业务关联的依据,但外层是否保证一请求一上传仍待实测。
+
+| 消息    | MQTT topic                  | 来源/状态                                 |
+| ------- | --------------------------- | ------------------------------------------ |
+| `377` | `fwh/s2c/mng/{gatewaySN}` | 现有实现;所有设备和失败场景是否适用待确认 |
+| `378` | `fwh/c2s/mng/{gatewaySN}` | 现有实现的等待 topic;原 CSV 未包含 topic  |
+| `379` | 待补充                      | 不能假定与`378` 相同                     |
+| `380` | 待补充                      | 不能假定与`377` 相同;样本中由 FWH 平台发出 |
+| `607`/`610` | 待补充(推测 s2c 方向) | FWH 平台发给设备,本工具只监听 |
+| `608`/`609` | 待补充(推测 c2s 方向) | 设备发给 FWH 平台 |
+
+测试服与生产服使用不同的 broker、证书和平台域名,同一操作的 MQTT 与 HTTP 下载必须属于同一环境,见 [technical-design.md](technical-design.md#3-测试服与生产服)。
+
+现有通用客户端订阅 QoS 为 `1`,故障录波发布使用 QoS `0`、`retained=false`。这些是实现配置,设备要求的 QoS、会话和离线投递规则待补充。FaultFilter 文档中的 QoS `2` 针对其自身 topic,不直接适用于云端外层。
+
+## 3. 公共报文外层
+
+四种消息的样本记录均包含以下外层字段。`dataArea` 是 JSON 对象,不是 JSON 字符串。
+
+| 字段          | JSON 类型 | 含义/现有行为                                                   | 来源与待补充内容                                              |
+| ------------- | --------- | ---------------------------------------------------------------- | ------------------------------------------------------------- |
+| `cmdType`   | 整数      | 消息类型,分别为`377/378/379/380`                              | 样本记录、已验证代码                                          |
+| `equipNo`   | 字符串    | 现有`377` 填目标网关 SN                                        | 样本中均为同一网关;真正子设备场景及全部应答取值待确认        |
+| `type`      | 整数      | 外层设备类型;现有发送填`0`,代码注释为网关 IBG                | 样本为`0`;其他值待补充。不同于业务层字符串 `list[].type` |
+| `timeStamp` | 整数      | 现有发送使用 Unix 秒`time.Now().Unix()`                        | 接收端单位、生成时点和校时规则待确认                          |
+| `snno`      | 整数      | 消息序号;关联规则见第 4 节                                      | 取值范围、回绕及设备重发是否保留序号待补充                    |
+| `len`       | 整数      | 现有发送为序列化后`dataArea` 的字节长度                        | 接收规则与失败校验行为待确认                                  |
+| `crc`       | 字符串    | 现有发送为`dataArea` 序列化字节的 CRC32 IEEE,8 位大写十六进制 | 接收规则与校验失败行为待确认                                  |
+| `dataArea`  | 对象      | 命令业务内容,详见第 5~8 节                                     | 业务字段见 FaultFilter 文档;外层失败报文的形态待补充         |
+
+现有发送计算方式:先以 Go `json.Marshal(dataArea)` 得到字节 `B`,再取 `len(B)` 和 `CRC32_IEEE(B)`。发送外层中的 `dataArea` 仍为对象。协议尚未明确 JSON 字段顺序、空白、Unicode 转义和数值格式对接收端 CRC 校验的影响,不能直接规定“反序列化后重新序列化”就是正确的校验方式。FaultFilter 文档说明业务 JSON 字段顺序不固定、接收方应按字段名解析。
+
+外层字段完整必填规则、最大报文长度、未知字段处理及空值规则均待补充。
+
+## 4. 序号、关联和当前服务端约定
+
+### 4.1 `377/378`
+
+- **用户确认**:设备端没有严格按请求 `snno` 应答,请求方和应答方各自维护序号,不能用它关联 `377/378`。
+- **用户确认**:服务端发送 `377` 可使用固定 `snno=123` 或随机数;现有代码使用 `123`。
+- **用户确认**:当前采用串行发送并等待对应类型应答的方向;串行范围及释放时点尚待细化。
+- **FaultFilter 文档**:设备同一时刻只处理一条业务命令。处理期间收到非「退出(`cmd=4`)」命令时直接回复错误 `busy`,不中断当前处理;`cmd=4` 用于结束当前处理。判定顺序为先校验 `cmd`、再判忙、再校验 `data`。该 `busy` 回复经外层体现为 `378` 还是 `379` 待实测。
+- **服务端暂定**:响应等待超时为 **15 秒**。在当前讨论中针对 `377/378` 梳理;计时起点尚待确认,不是设备承诺的最长响应时间,也不作为 `379` 的超时。
+- **待设计**:建议核对 topic、`equipNo`、`cmdType=378`、`dataArea.cmd`,而非只匹配 `cmdType`;此过滤方案尚未落地。
+- **待补充**:迟到或重复 `378`、其他发送方请求产生的 `378` 如何处理;相同网关和 `cmd` 的消息没有唯一关联凭据。
+
+### 4.2 `379/380`
+
+- **用户确认**:`380` 由服务端实现,可以使用 `snno` 关联 `379/380`。
+- **待细化**:是否通过 `380.snno=379.snno` 实现关联,还是另有规则;尚未确认,本文不将回显写为既定要求。
+- **证据冲突**:旧样本记录的 `379/380` 序号不同(`14→7536`、`26→7559`)。保留其历史事实,不将它解释为新服务端必须继续采用的行为;新关联方式需补充设备接受条件和验证结果。
+- **待补充**:应答 topic、应答期限、重发规则、应答成功的语义及重复上传处理。
+
+## 5. `377`:服务端操作请求
+
+### 5.1 公共业务结构
+
+| 字段路径(相对`dataArea`) | 类型     | 含义                                                                                                    | 来源/约束                                                         |
+| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
+| `cmd`                      | 整数     | `0` 获取实时数据;`1` 设置参数;`2` 获取记录;`3` 查询参数;`4` 退出;`90/99` 连通性测试 | FaultFilter 文档;样本覆盖`0/2`,已验证代码覆盖 `1`;`3/4/90/99` 经外层转发待实测 |
+| `data`                     | 对象     | 请求内容                                                                                                | `cmd=4/90/99` 为空对象`{}`                                       |
+| `data.list`                | 对象数组 | 按设备类型分组                                                                                          | 最小/最大数量待补充                                               |
+| `data.list[].type`         | 字符串   | 目标设备类型:`pe` 或 `aGate`                                                                       | 字典见第 9 节                                                      |
+| `data.list[].info`         | 对象数组 | 该类型下的目标设备和操作项                                                                              | 批量限制待补充                                                     |
+| `data.list[].info[].sn`    | 字符串   | 目标设备 SN;`pe` 必填并用于寻址(最多 15 台),`aGate` 可填但不用于寻址                              | FaultFilter 文档;`pe` 的 SN 无法匹配时整条命令回复 `pe sn not find` |
+
+### 5.2 `cmd=0`:获取实时数据
+
+附加字段 `data.list[].info[].codeList` 为整数数组,每个元素为数据项编号。设备对每个 `code` 串行读取子设备,汇总后在回复的 `hexList` 中返回。
+
+| `code` | 含义                     | `hexData` 结构(FaultFilter 文档)       |
+| ------ | ------------------------ | ---------------------------------------- |
+| `1`  | 故障总体信息             | 5.1.1,变长                              |
+| `2`  | 设备故障录波功能基本信息 | 5.1.1,固定 16 字节                      |
+| `3`  | 故障录波总目录           | 5.1.1,1 字节条数 + N × 14 字节目录项    |
+
+`cmd=0` 只支持 `code=1~3`(用户确认,2026-10-14)。单一故障信息(`4`)和单一故障波形数据(`5`)属于具体记录,必须用 `cmd=2` 按记录序号导出(见 5.4)。样本请求 `[3]` 即查询录波总目录,目录项中的位置索引 `index` 是 `cmd=2` 的请求取值。重复值及空数组的行为未定义。
+
+`dataArea` 结构示意:
+
+```json
+{
+  "cmd": 0,
+  "data": {
+    "list": [
+      { "type": "aGate", "info": [{ "sn": "<目标设备SN>", "codeList": [3] }] }
+    ]
+  }
+}
+```
+
+### 5.3 `cmd=1`:设置参数
+
+| 附加字段路径(相对`dataArea`)         | 类型     | 含义                                                                                     |
+| ---------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
+| `data.list[].info[].paraList`          | 对象数组 | 待执行的功能项列表                                                                       |
+| `data.list[].info[].paraList[].id`     | 整数     | **命令控制符(功能码)**,不是数据参数编号;取值 `1~7`,见下表                       |
+| `data.list[].info[].paraList[].setVal` | 字符串   | 该功能码的输入参数,十六进制字符串(每 2 字符 1 字节);无输入参数的功能码可为空字符串 |
+
+| `id` | 功能                     | `setVal` 字节结构                                                                                 |
+| ---- | ------------------------ | ------------------------------------------------------------------------------------------------- |
+| `1`  | 清除设备故障波形         | 无                                                                                                |
+| `2`  | 采样通道配置             | 通道数(1B) + n × 通道含义(2B,大端)                                                               |
+| `3`  | 手动触发故障录波         | 无                                                                                                |
+| `4`  | 设备对时                 | 年(2B,大端) 月 日 时 分 秒,共 7 字节                                                            |
+| `5`  | 设置采样频率             | 2 字节,Hz,大端                                                                                  |
+| `6`  | 配置可配采样通道         | 索引号(1B) + 数据类型(1B,高半字节=设置方式,低半字节=地址类型) + 数据(4B)                        |
+| `7`  | 故障录波功能状态控制     | 1 字节:`0`=关闭,`1`=使能                                                                        |
+
+完整定义见 FaultFilter 文档 5.4.1。
+
+`dataArea` 结构示意,值取自现有辅助函数,不代表通用默认配置:
+
+```json
+{
+  "cmd": 1,
+  "data": {
+    "list": [
+      {
+        "type": "pe",
+        "info": [
+          {
+            "sn": "<目标子设备SN>",
+            "paraList": [
+              { "id": 6, "setVal": "010100015984" },
+              { "id": 6, "setVal": "020100015981" }
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+两项 `id=6` 均为「配置可配采样通道」,由 `setVal` 首字节的通道索引区分:`01 01 00015984` 表示通道 1、默认方式、Uint16、地址 `0x00015984`;`02 01 00015981` 表示通道 2、地址 `0x00015981`。因此 `id` 不能作为条目唯一键;回复 `setResultList` 是否按请求顺序逐项对应待实测。前端目前只校验十六进制字符,未按功能码校验字节长度。HTTP DTO 使用 `val`,MQTT 使用 `setVal`,两者不可混淆。
+
+### 5.4 `cmd=2`:获取记录
+
+附加字段 `data.list[].info[].index` 为整数数组,字段名为单数 `index`,值为数组。每个元素是**记录序号**,即 `cmd=0, code=3` 目录中各记录的位置索引(从 1 开始,不超过 `code=1` 的「最大可存储故障记录总数」)。旧描述的范围 `1~16` 未在 FaultFilter 文档中出现,以目录实际返回为准。样本请求 `[9]`。
+
+设备对每个记录序号分别读取「故障数据」(`code=4`) 与「波形数据」(`code=5`),并保存为设备本地文件(见 7.4)。每次 `cmd=2` 开始前,设备会删除保存根目录下的**全部**历史记录文件。
+
+```json
+{
+  "cmd": 2,
+  "data": {
+    "list": [
+      { "type": "aGate", "info": [{ "sn": "<目标设备SN>", "index": [9] }] }
+    ]
+  }
+}
+```
+
+### 5.5 `cmd=3`:查询参数
+
+FaultFilter 文档定义,无样本,经 `377` 转发待实测。附加字段 `data.list[].info[].idList` 为整数数组,元素为第 5.3 节的功能码,不带输入参数。
+
+```json
+{
+  "cmd": 3,
+  "data": {
+    "list": [
+      { "type": "pe", "info": [{ "sn": "<目标子设备SN>", "idList": [2, 5, 7] }] }
+    ]
+  }
+}
+```
+
+### 5.6 `cmd=4`/`cmd=90`/`cmd=99`
+
+- `cmd=4` 退出:`data` 为 `{}`,结束设备当前处理,不产生业务数据回复。
+- `cmd=90`/`cmd=99` 连通性测试:`data` 为 `{}`,设备分别回复 `result="ok"`/`"test fail"`。
+
+以上均来自 FaultFilter 文档,经外层转发的表现待实测。
+
+## 6. `378`:设备操作应答
+
+| 字段路径(相对`dataArea`) | 类型 | 含义/状态                                                           |
+| ---------------------------- | ---- | -------------------------------------------------------------------- |
+| `cmd`                      | 整数 | 样本为`0/2`;现有代码按 `0/1/2` 定义。是否始终等于请求命令待确认 |
+| `result`                   | 整数 | 样本成功为`0`;旧描述失败为 `-1`;完整错误值域待补充             |
+
+```json
+{ "cmd": 0, "result": 0 }
+```
+
+`378` 属于外层应答,FaultFilter 文档未定义(其回复 `result` 为字符串,对应 `379`)。样本与现有代码没有显示这里包含逐设备、逐参数或文件字节。`result=0` 表示收到、接受、排队还是执行完成,尚未确认;不能仅凭它断言数据已取得、文件可下载或参数已生效。
+
+**待补充的应答行为**:一条请求对应一条还是多条应答;格式错误、设备忙和不支持命令时 `378` 是否仍为 `0`;重复及迟到应答规则;字段缺失规则;收到应答后能否立即发送下一条请求(FaultFilter 层忙时会回复 `busy`)。
+
+## 7. `379`:设备数据/处理结果上传请求
+
+### 7.1 公共业务字段
+
+| 字段路径(相对`dataArea`) | 类型     | 含义/状态                                                                       |
+| ---------------------------- | -------- | -------------------------------------------------------------------------------- |
+| `cmd`                      | 整数     | 回显请求命令码;样本为`0/2`                                                    |
+| `result`                   | 字符串   | 整体结果:`"ok"` 全部子项成功,`"error"` 至少一个子项失败;错误回复时为错误说明 |
+| `list`                     | 对象数组 | 上传设备列表,位于`dataArea.list`,不是 `dataArea.data.list`                 |
+| `list[].type`              | 字符串   | 设备类型:`pe` 或 `aGate`                                                    |
+| `list[].info`              | 对象数组 | 各设备结果                                                                       |
+| `list[].info[].sn`         | 字符串   | 设备 SN;样本类型为字符串,不是数组                                              |
+
+**错误回复**:命令无法进入业务处理时,FaultFilter 回复只含 `result`,没有 `list`:
+
+| `result`          | 原因                                    |
+| ----------------- | --------------------------------------- |
+| `json unpack err` | 消息体不是合法 JSON                     |
+| `cmd unpack err`  | 缺少 `cmd`(此时回复 `cmd` 为 `0`) |
+| `data unpack err` | 缺少 `data`                           |
+| `busy`            | 设备正忙,收到非退出命令                |
+| `pe sn not find`  | `pe` 的 SN 无法匹配                   |
+| `cmd no define`   | 不支持的命令码                          |
+
+**子项结果 `resultOne`**(所有 `hexList[]`/`indexInfo[]`/`idInfo[]`/`setResultList[]` 子项通用):
+
+| 取值                          | 含义                                                              |
+| ----------------------------- | ----------------------------------------------------------------- |
+| `ok`                          | 成功,对应数据有效                                                |
+| `cancomerr:suberrcode=<hex>`  | 子设备负响应错误码(NRC),见 FaultFilter 文档 10.6               |
+| `cancomerr:time out`          | 与子设备通信超时                                                  |
+| `cancomerr:index=<hex>`       | 设备内部通信错误判定码,见 FaultFilter 文档 10.5                  |
+| `emserr:can p null`           | 未取到目标子设备                                                  |
+| `emserr:can data null`        | 子设备返回数据为空                                                |
+| `emserr:set cmd err`          | 下发命令失败                                                      |
+| `emserr:other`                | 其他错误                                                          |
+
+`resultOne` 不为 `ok` 时,对应 `hexData`/`idData` 可能为空或无效。错误回复经外层后是否仍以 `379` 上传待实测。
+
+### 7.2 `cmd=0`:实时数据上传
+
+| 附加字段路径                          | 类型     | 含义                                                                  |
+| ------------------------------------- | -------- | --------------------------------------------------------------------- |
+| `list[].info[].hexList`             | 对象数组 | 各数据项结果                                                          |
+| `list[].info[].hexList[].code`      | 整数     | 回显请求的`code`;样本`3` 为故障录波总目录                          |
+| `list[].info[].hexList[].resultOne` | 字符串   | 子项结果,见 7.1                                                      |
+| `list[].info[].hexList[].hexData`   | 字符串   | 子设备原始字节的十六进制字符串;结构按 `code` 见第 5.2 节             |
+
+```json
+{
+  "cmd": 0,
+  "result": "ok",
+  "list": [
+    {
+      "type": "aGate",
+      "info": [{ "sn": "<目标设备SN>", "hexList": [{ "code": 3, "resultOne": "ok", "hexData": "<原始十六进制数据>" }] }]
+    }
+  ]
+}
+```
+
+`code=3` 目录项固定 14 字节:位置索引(1) + 发生时间(7,年份大端) + 参数列表版本(4) + 事件触发源(2)。2026-10-14 样本的 `hexData` 共 211 字节 = 1 + 15 × 14,15 条记录均按此结构解析出合理的时间与触发源(`0x1001`/`0x1002`),结构已验证;解析结果见 [protocol-observations.md](protocol-observations.md#2026-10-14-样本)。
+
+### 7.3 `cmd=1`:参数设置结果上传
+
+无样本,结构来自 FaultFilter 文档。旧描述的 `resultList[{id, result}]` 有误,正确字段为 `setResultList`:
+
+| 附加字段路径                                | 类型     | 含义                                  |
+| ------------------------------------------- | -------- | ------------------------------------- |
+| `list[].info[].setResultList`             | 对象数组 | 各功能项执行结果                      |
+| `list[].info[].setResultList[].id`        | 整数     | 回显请求 `paraList[].id`(功能码)  |
+| `list[].info[].setResultList[].resultOne` | 字符串   | 子项结果,见 7.1;为字符串,不是数组  |
+
+```json
+{
+  "cmd": 1,
+  "result": "ok",
+  "list": [
+    {
+      "type": "pe",
+      "info": [{ "sn": "<目标子设备SN>", "setResultList": [{ "id": 6, "resultOne": "ok" }, { "id": 6, "resultOne": "ok" }] }]
+    }
+  ]
+}
+```
+
+重复功能码(如两项 `id=6`)的结果是否严格按请求顺序返回,待实测。
+
+### 7.4 `cmd=2`:记录处理结果上传
+
+| 附加字段路径                            | 类型     | 含义                                                                                     |
+| --------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
+| `filename`                            | 字符串   | 本次导出批次的时间戳目录名,格式`YYYY-MM-DD_HH_MM_SS`;样本为 `2026-09-29_14_18_29`  |
+| `list[].info[].indexInfo`             | 对象数组 | 各记录项结果                                                                             |
+| `list[].info[].indexInfo[].index`     | 整数     | **记录数据类型码**:`4`=故障数据,`5`=波形数据;**不是**请求的记录序号            |
+| `list[].info[].indexInfo[].resultOne` | 字符串   | 子项结果,见 7.1;不是数组                                                               |
+
+```json
+{
+  "cmd": 2,
+  "result": "ok",
+  "filename": "2026-09-29_14_18_29",
+  "list": [
+    {
+      "type": "aGate",
+      "info": [{ "sn": "<目标设备SN>", "indexInfo": [{ "index": 4, "resultOne": "ok" }, { "index": 5, "resultOne": "ok" }] }]
+    }
+  ]
+}
+```
+
+请求的每个记录序号在回复中展开为 `4`、`5` 两项,回复不回传记录序号本身。因此样本请求 `[9]` 后上传 `4/5` 是正常对应,并非索引转换。请求多个记录序号时,回复中会出现多组 `4/5`,无法仅凭回复区分属于哪个记录序号,只能依赖请求顺序(待实测)。
+
+上传不含文件字节或下载地址。文件保存在设备本地:
+
+```
+/home/root/ExportFile/faultFilterRecode/<filename>/<sn>/index_<记录序号>/failuredata
+/home/root/ExportFile/faultFilterRecode/<filename>/<sn>/index_<记录序号>/waveformdata
+```
+
+文件内容为原始字节的大写十六进制 ASCII 文本,无文件头;仅成功项落盘。内部结构见 FaultFilter 文档 5.2.2/5.2.3。文件由 FWH 平台通过 `607~610` 触发上传到 hes-upload,见第 8A 节。
+
+### 7.5 `cmd=3`:参数查询结果上传
+
+无样本,结构来自 FaultFilter 文档。
+
+| 附加字段路径                         | 类型     | 含义                                                         |
+| ------------------------------------ | -------- | ------------------------------------------------------------ |
+| `list[].info[].idInfo`             | 对象数组 | 各功能项查询结果                                             |
+| `list[].info[].idInfo[].id`        | 整数     | 回显请求的功能码                                             |
+| `list[].info[].idInfo[].idData`    | 字符串   | 当前配置/状态的十六进制字符串,结构参照第 5.3 节对应功能码 |
+| `list[].info[].idInfo[].resultOne` | 字符串   | 子项结果,见 7.1                                             |
+
+## 8. `380`:服务端上传应答
+
+| 字段路径(相对`dataArea`) | 类型 | 含义/状态                                       |
+| ---------------------------- | ---- | ------------------------------------------------ |
+| `cmd`                      | 整数 | 样本为`0/2`;是否必须与 `379.cmd` 一致待明确 |
+| `result`                   | 整数 | 样本为`0`;旧描述失败为 `-1`                 |
+
+```json
+{ "cmd": 0, "result": 0 }
+```
+
+`380` 属于外层,FaultFilter 文档未定义。关联方向已由用户确认可使用 `snno`,具体赋值规则见第 4.2 节。`result=0` 是接收成功、校验通过还是保存成功尚待确认。业务项失败时能否仍回复上传接收成功,以及失败应答是否触发设备重发,也待补充。
+
+**设计决策(2026-10-16)**:样本中的 `380` 由 FWH 平台回复,本工具不回复 `380`,只监听,避免设备收到重复应答。`cmd=0/1/3` 的 `380` 是否同样由平台回复待实测。
+
+## 8A. `607~610`:录波文件上传
+
+来源:2026-10-14 样本记录(两轮 `cmd=2`,结构一致)。这一组消息由 FWH 平台在收到 `379(cmd=2)` 后自动发起,本工具只监听(用户确认,2026-10-16)。
+
+| `cmdType` | 日志名称 | 方向 | `dataArea` 样本 |
+| --------- | -------- | ---- | --------------- |
+| `607` | 故障录波召唤下行数据 | 平台 → 设备 | `{"faultRecordID":5226,"fileName":"2026-10-14_05_46_20","url":"http://test.franklinwh.com/hes-upload/v1/file/upload","logTime":"2026-10-14"}` |
+| `608` | 故障录波召唤应答上行数据 | 设备 → 平台 | `{"result":0,"filename":"2026-10-14_05_46_20","result":5226}` |
+| `609` | 故障录波文件上传结果 | 设备 → 平台 | `{"result":0,"fileName":"2026-10-14_05_46_20.tar.bz2"}` |
+| `610` | 故障录波文件上传结果应答 | 平台 → 设备 | `{"result":1}` |
+
+字段说明:
+
+| 字段 | 消息 | 类型 | 含义/状态 |
+| ---- | ---- | ---- | ---------- |
+| `faultRecordID` | `607` | 整数 | FWH 平台的召唤记录 ID;可与 DataDownload 使用的平台记录互查 |
+| `fileName` | `607` | 字符串 | 等于 `379.filename`,无扩展名 |
+| `url` | `607` | 字符串 | 设备 HTTP 上传地址;样本为测试服 `http://` 地址,不用于推导下载地址 |
+| `logTime` | `607` | 字符串 | 日期 `YYYY-MM-DD`,用途待确认 |
+| `filename` | `608` | 字符串 | 回显 `607.fileName`;注意键名小写 |
+| `result` | `608` | 整数 | **同一对象内出现两次**:第一次 `0`,第二次回显 `faultRecordID`。Go `encoding/json` 只保留最后一个值;如需第一次的值,必须按原始字节逐键读取 |
+| `result` | `609` | 整数 | 样本 `0`,推测为上传成功;失败值待补充 |
+| `fileName` | `609` | 字符串 | 上传文件名 = `379.filename + ".tar.bz2"` |
+| `result` | `610` | 整数 | 样本 `1`;与 `378/380` 的 `0=成功` 约定不同,不能统一按 `0` 判定成功 |
+
+时序(样本):`379` 与 `607/608` 在同一秒内,`380` 晚约 1 秒,`609/610` 在 `607` 后约 11~12 秒。
+
+**下载地址**(用户确认,2026-10-16):
+
+```text
+https://<环境域名>/hes-upload/v1/file/download/logger/<equipNo>/<609.fileName>
+```
+
+测试服 `<环境域名>` 为 `test.franklinwh.com`,生产服为 `FWH_ENERGY` 配置的域名。下载**不需要 token**(用户确认,2026-10-16)。文件保留期限待确认。
+
+**压缩包**(2026-10-14 样本验证):扩展名为 `.tar.bz2`,实际是 **gzip** 压缩的 tar,解压须按文件头(`1f 8b` 为 gzip,`42 5a 68` 为 bzip2)判断。内部结构为:
+
+```text
+<filename>/<sn>/index_<记录序号>/failuredata
+<filename>/<sn>/index_<记录序号>/waveformdata
+```
+
+**文件解析规则**(以 FaultFilter 文档 5.2.2/5.2.3 为基础,按样本补充,详见 [protocol-observations.md](protocol-observations.md#2026-10-14-录波文件解析)):
+
+| 项目 | 规则 | 依据 |
+| --- | --- | --- |
+| `failuredata` 长度字段 | 等于整个文件字节数(含填充),不能当有效数据长度用;按结构逐项解析 | 样本 512 字节,结构在第 308 字节结束 |
+| `failuredata` 末尾 | `0xFF` 填充,解析到可配采样通道组结束即停止 | 样本 |
+| 版本字符串 | 取第一个 NUL 之前的内容。aGate 的 INV/FPGA 两段为填充,填充方式不限定,**不解析**,只跳过 64 字节 | 用户确认(2026-10-16);样本填充为 `01 02 … 40` |
+| 波形通道数 n | = `failuredata` 的“采样通道总数”,已包含可配采样通道 | 样本 16 通道,文件长度恰好整除 |
+| 波形点数 m | = 基本信息“采样点总数”;故障时刻在第“故障前采样点数”点 | 样本 1536 点,第 1024 点故障字跳变 |
+| 波形排列 | 采样点优先(同一点的 n 个通道连续) | 样本验证 |
+| 采样值 | 每点每通道 2 字节、大端;按 **int16** 解释(文档写 Uint16) | 样本正弦通道按有符号解释才连续 |
+| float32 可配通道 | 在波形中同样只占 2 字节,换算方式待确认 | 样本 |
+
+## 9. 设备类型与数据字典
+
+| 位置/来源                       | 已出现的值                             | 说明                                                                       |
+| -------------------------------- | -------------------------------------- | -------------------------------------------------------------------------- |
+| FaultFilter 文档                 | `pe`、`aGate`                      | 业务层仅定义这两种;`pe` 按 SN 寻址(最多 15 台),`aGate` 系统内唯一 |
+| `377/379.list[].type` 样本     | `aGate`                              | `cmd=0/2`                                                                |
+| `377.list[].type` 已验证代码   | `pe`、`aGate`                      | 参数设置                                                                   |
+| `377.list[].type` 旧描述       | `pe`、`agate`、`mppt`、`apbox` | FaultFilter 文档未定义`agate`(小写)、`mppt`、`apbox`,不应使用  |
+| `379.list[].type` 旧描述       | `aGate`、`aPower`                  | FaultFilter 文档未定义`aPower`,回复类型与请求一致为 `pe`/`aGate`  |
+| HTTP/页面类型                   | `pe`、`agate`                      | 现有设置代码将 HTTP`agate` 转成 MQTT `aGate`;MQTT 层必须使用 `aGate` |
+
+已由 FaultFilter 文档补齐的字典:`code` 含义及 `hexData` 结构(5.2)、功能码及 `setVal` 结构(5.3)、`indexInfo[].index` 类型码(7.4)、`resultOne` 及错误字符串(7.1)、事件触发源与子设备错误码(见该文档第 10 节)。通道含义参数代码(F14)见产品级《参数代码表》,未随该文档提供。
+
+仍缺失:外层 `378/380.result` 值域、支持的设备型号与固件版本。
+
+## 10. 待补充清单
+
+以下编号用于逐项讨论;“待补充”不是已经确定的限制。优先补齐 `A` 类,再继续分析上传协议。
+
+| 编号 | 待补充信息                 | 当前已知/缺口                                                                     | 可补充的材料                             |
+| ---- | -------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- |
+| A01  | `378.result` 的精确定义  | 只知道样本为`0`,成功阶段未知                                                    | 协议说明、设备处理流程或对照实测         |
+| A02  | `378` 应答条数和批量语义 | 未知是否一请求一应答;批量子项结果已知在`379` 中按 `resultOne` 表达            | 多子设备/多参数请求及完整应答           |
+| A03  | `378` 字段与错误值域     | 业务层错误字符串已知(7.1),其在外层的体现方式未知                              | 非法输入、忙、不支持等场景的完整报文     |
+| A04  | 连续请求的设备行为         | FaultFilter 层串行处理、忙时回复`busy`;`busy` 经外层如何体现未知              | 处理中发送下一条请求的结果               |
+| A05  | 迟到、重复和重发应答       | 无唯一关联标识,15 秒不保证设备停止回复                                            | 延迟/断线场景的完整时间线和最大时限说明 |
+| A06  | topic 与发送方隔离         | 当前实现有`mng/{SN}`;FaultFilter 自身 topic 与云端 topic 的关系未知             | 双向 topic、其他平台发送时的消息可见性   |
+| A07  | 外层字段和 CRC 规则        | 发送代码有算法,接收及错误规则未知                                                 | 原始 JSON 字节、校验规范、失败样本       |
+| A08  | `377` 输入约束           | `pe` 最多 15 台;其余批量上限未知                                                | 数量限制和输入校验说明                   |
+| A09  | 15 秒超时的计时口径        | 服务端暂定值,起点与超时后策略未确定                                               | 服务端决策;设备延迟证据供参考           |
+| B01  | `cmd=3`/`cmd=4` 外层支持  | FaultFilter 文档定义了 `cmd=3`(查询参数)和 `cmd=4`(退出),旧描述、现有代码和实报只有 `0/1/2`。用户判断可能支持(2026-10-16),待实测。不支持时参数读取、复读和设备侧取消不可用 | `cmd=3`、`cmd=4` 实测报文               |
+| B02  | ~~`code/hexData` 解释~~ | 已明确(5.2);`code=3` 已用 2026-10-14 样本验证(7.2),`code=1/2` 待样本       | `code=1/2` 原始十六进制样本              |
+| B03  | ~~参数 ID/值/重复 ID~~  | `id` 为功能码,`setVal` 结构及重复 `id=6` 的区分方式已明确(5.3)            | 重复功能码结果顺序的实测                 |
+| B04  | ~~文件传输链路~~           | 已明确:平台经 `607~610` 触发设备 HTTP 上传 `tar.bz2`,按规律下载(8A);平台是否对本工具直连发出的 `377` 同样自动上传待实测 | 本工具直连 `cmd=2` 的完整时间线         |
+| B05  | `379` 产生条件和批次     | 是否依赖成功`378`、是否主动/拆分上传未知                                        | 操作到全部上传的时间线                   |
+| B06  | ~~`379(cmd=1)` 结构~~    | 已明确为`setResultList[{id, resultOne}]`(7.3)                                  | 成功、部分失败实测报文                   |
+| B07  | `379/380` topic 与关联   | 服务端可用`snno`,具体规则和旧样本不一致                                         | 明确赋值规则及设备接受新应答的实测       |
+| B08  | 上传应答语义和可靠性       | 成功判据、应答时限、重发和断线恢复未知;本工具不回复 `380`,此项影响降低           | 设备上传处理规范及异常时间线             |
+| B09  | `607~610` topic 与订阅权限 | 本工具需监听平台下发的 `607/610`,s2c 方向能否订阅未知                            | 两个环境下订阅 s2c 的实测               |
+| B10  | `608/609/610` 值域       | `608.result` 键重复;`609` 失败值未知;`610.result=1` 含义未知                    | 上传失败样本、平台说明                   |
+| B11  | ~~下载认证与压缩包结构~~   | 已明确:不需要 token;实为 gzip tar;结构见 8A。保留期限和生产服是否一致待确认    | 生产服下载一个真实文件                   |
+| B12  | float32 可配通道的 16 位表示 | 样本第 13、14 通道类型为 float32,波形中每点仅 2 字节,值在 -248~249;换算方式未知。**待用户统一找设备方确认** | 设备方说明                               |
+| B13  | ~~`failuredata` 中 aGate 的 INV/FPGA 版本字段~~ | 已明确(用户确认,2026-10-16):aGate 中这两段是填充,填充方式不限定,解析时跳过 | —                                        |
+| B14  | 采样值有无符号             | 文档写 Uint16,样本正弦通道按 int16 解释才连续;暂按 int16 处理               | 设备方确认                               |
+| B15  | F14 物理量与换算系数       | **决策(2026-10-16)**:解析器先只显示原始值和通道含义代码,是否按《参数代码表》换算后续再定 | 《参数代码表》                           |
+
+补充时建议记录:编号、确认结论、适用设备/固件、证据来源、原始请求/响应和 topic。先保留历史样本,再更新字段或规则,避免将实现选择写成设备协议事实。

+ 96 - 0
docs/fault-record/requirements.md

@@ -0,0 +1,96 @@
+# 故障录波功能需求
+
+> 状态:需求梳理,2026-09-30;2026-10-16 补充双环境需求、文件获取方式和 FR-06 操作方式。本文记录目标能力与待确认的产品口径,不将协议描述中的未验证字段写为已确认需求。协议证据见 [protocol-observations.md](protocol-observations.md),当前实现见 [implementation-status.md](implementation-status.md)。
+
+## 1. 目标与范围
+
+目标是在 `repositories/mqtt` 对接设备故障录波协议,使以下操作可以直接通过 MQTT 完成,减少对 FWH API 的依赖:
+
+1. 查询子设备故障录波实时数据;
+2. 查询和导出子设备故障录波记录;
+3. 查询子设备故障录波参数;
+4. 设置子设备故障录波参数。
+
+本工具负责向设备发送请求并监听设备上传;对 `379` 的应答和录波文件上传由 FWH 平台自动完成,本工具不重复应答。录波文件的触发和状态跟踪走 MQTT,文件本身仍从 FWH 平台的 hes-upload 下载。
+
+以上能力需同时支持**测试服**和**生产服**(见 FR-05)。这里的“子设备”至少需要明确所在网关、设备类型和设备 SN。现有样本只有 aGate,且网关 SN 与 `info.sn` 相同;真正子设备的寻址方式待实测。
+
+本需求描述独立的 MQTT 能力。现有 [DataDownload](../data-download/requirements.md) 的故障录波任务、日期固化和文件归档规则继续由其文档定义;是否接入新 MQTT 能力需单独设计。
+
+## 2. 功能需求
+
+### FR-01 查询实时数据
+
+- 用户能指定目标网关和子设备,发起故障录波实时数据查询。
+- 系统应呈现设备实际返回的数据、对应设备和单项结果;请求被应答与数据已返回应是可区分的状态。
+- 若设备没有返回目标数据、返回失败或超时,系统应给出可定位的结果,不把空数据解释为查询成功。
+
+**已确定**:开放 `code=1~3`,按字段显示原始值,可展开原始 `hexData`;页面见 [ui-design.md](ui-design.md#4-设备信息)。`code` 集合(`1~3`)及含义已由 [FaultFilter_MQTT协议文档](FaultFilter_MQTT协议文档.md) 定义;单条记录的故障数据和波形数据不属于实时数据查询,归入 FR-02。
+
+### FR-02 查询与导出录波记录
+
+- 用户能查询目标子设备可获取的录波记录,并选择记录发起获取。
+- 系统应区分“请求已被设备应答”“设备报告文件处理结果”和“原始文件已可读取”三个阶段。
+- 导出应交付实际录波文件;仅有文件名或逐索引成功标志不满足导出需求。
+- 同一次请求包含多个记录时,应能看到每个记录的结果,避免把部分成功显示为全部成功。
+
+- 状态应覆盖“设备已应答”“记录已就绪(`379`)”“平台开始上传(`607`)”“上传完成(`609`)”“已下载”,以及每个阶段的失败或超时。
+- 获取到的压缩包应解析出故障数据和波形数据供查看:基本信息、快照通道、多通道波形(原始值),见 [ui-design.md](ui-design.md#31-录波详情抽屉或独立标签页)。
+- 导出:用户可任选原始压缩包或 CSV。
+
+**已确定**:每个设备每次获取 1 条记录;导出原始压缩包或 CSV;导出不设权限;暂不清理。按 FaultFilter 文档,记录列表来自 `cmd=0, code=3` 目录,记录序号即目录项索引;每条记录生成 `failuredata` 与 `waveformdata` 两个文件,设备每次获取记录前会清空历史文件。文件由平台上传后,按 `https://<环境域名>/hes-upload/v1/file/download/logger/<equipNo>/<文件名>.tar.bz2` 下载。功能统一称为“故障录波”。
+
+### FR-03 查询录波参数
+
+- 用户能读取目标子设备的当前故障录波参数,并看到参数 ID、当前值和读取结果。
+- 读取结果应来自设备本次返回的数据;本地上次填写的设置值不能冒充设备当前值。
+
+**已确定**:开放功能码 2、4、5、6、7(1、3 为动作,无可读状态);显示原始十六进制,协议已定义结构的同时显示解读。按 FaultFilter 文档,查询指令为 `cmd=3`,`idList` 可一次填多个功能码(`1~7`),返回 `idInfo[].idData` 十六进制,结构参照对应功能码的设置参数;经外层转发的实际报文待实测。
+
+### FR-04 设置录波参数
+
+- 用户能为指定子设备提交一个或多个参数 ID 与设置值。
+- 系统应分别呈现指令应答与逐参数执行结果;仅收到接收应答时,不应声称每项参数已经生效。
+- 失败或未确认的参数应能被识别,避免整批请求被误报为全部成功。
+
+**已确定**:提交前弹窗确认,危险操作醒目标注;以 `379` 逐项 `resultOne` 判定结果,可选设置后自动复读。**仍待实测**:`378` 与 `379(cmd=1)` 的实际报文。按 FaultFilter 文档,参数 ID 实为功能码,各功能码的 `setVal` 字节结构已定义;逐项结果为 `setResultList[].resultOne`,整体 `result` 为 `ok`/`error`。
+
+### FR-05 支持测试服和生产服
+
+- 用户发起任一操作时能选择测试服或生产服。
+- 同一操作的 MQTT 收发、文件下载和登录凭据都使用所选环境,结果中标明环境。
+- 一个环境不可用(未配置、连接失败)时,不影响另一个环境的操作。
+
+### FR-06 操作方式与记录(用户确认,2026-10-16)
+
+- **页面**:主菜单“故障录波”,子菜单分生产服和测试服,再按功能分页面(录波记录、设备信息、参数读取、参数设置)。页面设计见 [ui-design.md](ui-design.md),用户根据实现效果再提意见。
+- **异步**:所有操作异步执行,前端查询进度;服务重启后能自动恢复未完成的操作。
+- **单条获取**:每个设备每次只获取 1 条录波记录。
+- **设备忙**:本工具内对同一网关串行;设备因其他来源(如 FWH 平台网页)回复 `busy` 时,明确提示用户,不自动重试。
+- **取消**:进行中的操作可取消,必要时向设备发送 `cmd=4`(退出)结束当前处理。
+- **设备列表**:pe 子设备 SN 即 aPower SN,从站点信息中获取;生产服和测试服调用各自的接口。
+- **时间**:目录和录波中的时间暂按设备时间原样显示,测试有问题再改。
+- **记录**:操作过程记录在数据库中,原始文件存 MinIO;操作历史所有人共享,并记录操作人。暂不实现数据清理。
+- **权限**:功能全部开放;参数设置提交前必须弹窗确认,危险操作醒目标注。
+- **导出**:用户可任选导出原始压缩包或 CSV。
+- **与 DataDownload**:本功能完整实现并测试通过后,再考虑开放给 DataDownload;DataDownload 暂时继续使用原 FWH API 方案。
+- **数据展示**:录波数据只显示原始值和通道含义代码,不做物理量换算。
+
+## 3. 共同需求
+
+- 每次操作应保留目标网关、子设备类型/SN、所请求的 `code`/`index`/参数 ID,以及设备返回的原始结果,便于排查请求与上传不一致的问题。
+- 请求超时、设备拒绝、上传失败、单项失败、文件缺失和报文无法关联应分别呈现,不合并成笼统的“成功/失败”。
+- 设备类型的可选值及大小写应以真实协议验证;当前样本不能证明所有类型都能使用这四项能力。
+- 并发操作不能互相误认上传结果:本工具内同一网关串行,关联规则见 [technical-design.md](technical-design.md#44-关联规则)。
+
+## 4. 验收前需确定的事项
+
+| 功能 | 必须取得的证据或决策 |
+| --- | --- |
+| 实时数据 | `code=3` 已验证;`code=1/2` 实报;可见输出形式 |
+| 记录查询 | 查询条件(目录来源与 `index` 映射已明确) |
+| 文件导出 | 平台对本工具直连发出的 `cmd=2` 同样自动上传(下载认证、压缩包结构已确认) |
+| 双环境 | 测试服与生产服各完成一次完整的 `cmd=2` 获取与下载 |
+| 参数读取 | `cmd=3` 实报样本 |
+| 参数设置 | `cmd=1` 完整成功及失败样本、最终成功条件 |
+| 通用收发 | `379/380` topic、消息关联、超时与重发行为 |

+ 225 - 0
docs/fault-record/technical-design.md

@@ -0,0 +1,225 @@
+# 故障录波 MQTT 整体设计
+
+> 状态:设计草案,2026-09-30;2026-10-14 按 [FaultFilter_MQTT协议文档](FaultFilter_MQTT协议文档.md) 更新业务协议;2026-10-16 补充文件上传链路、双环境、分层架构、异步任务、存储和取消等用户已确认的决策。功能目标见 [requirements.md](requirements.md),页面设计见 [ui-design.md](ui-design.md),报文依据见 [protocol-observations.md](protocol-observations.md) 和 [protocol-specification.md](protocol-specification.md)。
+
+## 1. 设计目标与原则
+
+建立直接经 MQTT 与设备交互的故障录波能力,覆盖设备信息(实时数据)、录波记录获取、参数读取和参数设置,同时支持**生产服**和**测试服**。
+
+- **只发起、不应答**:本工具只发送 `377`(以及取消用的 `cmd=4`),其余消息全部监听。`380`、`607`、`610` 由 FWH 平台发出,平台在收到 `379(cmd=2)` 后自动发起文件上传。
+- **分层清晰**:每层只负责自己的事,只依赖下层。MQTT 客户端只提供底层收发,业务判断全部在上层。
+- **复用现有模块**:asynq 任务系统、MongoDB、MinIO、Redis、`fwh_pro`/`fwh_test` 站点信息接口、uPlot。
+- **只显示原始值**:录波数据不做物理量换算(B15)。
+- **DataDownload 暂不接入**:本功能完整实现并测试通过后,再考虑开放给 DataDownload;DataDownload 继续使用原 FWH API 方案。
+
+## 2. 分层架构
+
+```text
+L5 Route / Controller        HTTP 入参校验、出参转换
+L4 Service                   对外用例:创建操作、查询状态与结果、取消、导出、查询设备列表
+L3 Task Executor (asynq)     操作执行器:按状态机推进、持久化每一步、重启恢复
+L2 Protocol / Parser         纯函数,不访问网络、数据库和文件系统
+     ├─ protocol:构造 377 dataArea,解析 378/379/607~610(含 608 重复键)
+     └─ parser:解压、hexData(code=1/2/3)、failuredata、waveformdata、CSV 生成
+L1 FWH MQTT Client           按环境实例化;FWH 报文外层(len/crc/信封)+ Request/Publish/Subscribe
+L0 utils/mqttClient          paho 封装
+
+存储:MongoDB(操作记录) · MinIO(原始文件) · Redis(网关锁、asynq)
+外部:FWH 平台 HTTP(站点信息、hes-upload 下载)
+```
+
+| 层 | 建议位置 | 只负责 | 不负责 |
+| --- | --- | --- | --- |
+| L0 | `utils/mqttClient` | 连接、订阅、发布、重连 | FWH 报文格式 |
+| L1 | `repositories/mqtt/fwh_mqtt` | 按环境的连接实例;FWH 信封编解码;三种收发方法;订阅期间保持连接 | 任何 cmdType/cmd 的业务含义 |
+| L2 | `function/FaultRecordFile`(已有空目录,按现有 `function/SnapFile` 的模式放解析器);协议编解码放同级 `function/FaultRecordProtocol` | 字节和 JSON 的结构化转换 | IO |
+| L3 | `task/queue/faultRecordQueue` | 一次操作的完整执行和恢复 | HTTP 入参、页面格式 |
+| L4 | `services/faultRecordServices.go` | 用例编排,调用 Repository 和入队 | 协议细节 |
+| L5 | `controllers/faultRecordControllers.go`、`routes/faultRecordRoutes.go` | HTTP | 业务 |
+| 存储 | `repositories/mongo/faultRecordRepo.go`、`repositories/minio/faultRecordRepo.go` | 数据读写 | 业务 |
+| 下载 | `repositories/api/fwh_upload`(或在 `fwh_pro`/`fwh_test` 中各加一个无 token 下载函数) | 按环境域名下载 | 解压、解析 |
+
+L2 是纯函数,用仓库中的样本文件([CloudLog_2026-10-14_104644.csv](CloudLog_2026-10-14_104644.csv)、[2026-10-14_05_46_20.tar.bz2](2026-10-14_05_46_20.tar.bz2))做单元测试。以后 DataDownload 接入时,复用 L1 和 L2。
+
+## 3. L1 FWH MQTT 客户端
+
+### 3.1 对外方法
+
+| 方法 | 用途 | 行为 |
+| --- | --- | --- |
+| `Request(ctx, req) (*Msg, error)` | 一问一答(保留原方式) | 订阅应答 topic → 发送 → 等到第一条满足 `Match` 的消息或超时 → 取消订阅 |
+| `Publish(ctx, topic, msg) error` | 只请求 | 发送后立即返回 |
+| `Subscribe(topic, handler) (cancel func(), error)` | 持续监听 | 同一 topic 可有多个订阅者,各自收到每条消息;`cancel` 只移除自己 |
+
+- `handler` 收到 `{Topic, Envelope, Raw []byte, ReceivedAt}`。`Envelope` 为解析后的外层,`Raw` 为原始字节,供上层处理 `608` 的重复 `result` 键。
+- `Request` 的匹配条件由调用方传入(如 `cmdType=378 && dataArea.cmd=1`),客户端不内置任何业务过滤。
+- 信封构造(`len`、`crc`、`timeStamp`、`snno`)属于 L1 的通用能力,以 `NewEnvelope(cmdType, equipNo, dataArea)` 提供。
+- 现有 `EnableXlog`、`FaultRecordParamSetting`、`ChargeChannel` 中的业务内容移到上层,L1 只保留通用部分。
+
+### 3.2 连接与订阅管理
+
+- 内部维护 `topic → 订阅者集合`。第一个订阅者加入时向 broker 订阅;最后一个退出时向 broker 取消订阅。
+- **有活动订阅时不做空闲断开**;没有订阅且空闲 5 分钟后才断开(保持现有省连接的行为)。
+- 断线自动重连后恢复全部 broker 订阅(L0 已有 `ResumeSubscribe`)。L1 需要记录断线区间,供上层判断“这段时间可能漏了消息”。
+- 发布仍经过一个 worker 串行发送;订阅回调不阻塞 paho 线程,按订阅者各自的缓冲 channel 分发,满了就丢弃并记日志。
+
+## 4. 操作模型
+
+### 4.1 操作类型
+
+| 操作 | `cmd` | 输入 | 结果 |
+| --- | --- | --- | --- |
+| `device_info` | 0 | `codeList ⊆ {1,2,3}` | `hexList` 原文 + 解析结果 |
+| `record_fetch` | 2 | **一个**设备、**一条**记录序号 | 原始文件(MinIO)+ 解析结果 |
+| `param_read` | 3 | `idList` | `idInfo` 原文 + 解析结果 |
+| `param_write` | 1 | `paraList` | `setResultList` |
+
+每次操作只针对一个网关下的一个设备(`aGate` 或某个 `pe`)。
+
+`cmd=3`(查询参数)和 `cmd=4`(退出)只在 FaultFilter 文档中定义,旧描述、现有代码和实报只有 `0/1/2`。用户判断外层可能支持,先按支持实现,实测后确认(B01)。如不支持:参数读取页、参数设置的“读取当前值”和“自动复读”下线,取消只停止本工具的等待。`record_fetch` 每次只取一条记录,避免多组 `4/5` 无法区分。
+
+### 4.2 异步执行
+
+```text
+前端 ──POST 创建操作──▶ Service ──写 Mongo(pending) + 入队 asynq──▶ 返回 operationId
+前端 ──GET 轮询 operationId──▶ Service ──读 Mongo──▶ 状态、阶段时间、结果
+asynq worker ──▶ Task Executor ──▶ 执行并逐步写回 Mongo
+```
+
+- 队列:新建 `faultRecordQueue`,在 `task/queue/taskQueue.go` 注册,沿用 `dataDownloadQueue` 的写法(`asynq.TaskID` 保证幂等,`Retention` 保留记录)。
+- **关闭 asynq 自动重试**(`MaxRetry(0)`),重复执行由执行器根据 Mongo 状态自行判断,避免重复发送 `377(cmd=2)` 清空设备文件。
+- 任务超时:`record_fetch` 10 分钟,其他 3 分钟(暂定,实测后调整)。
+
+### 4.3 执行器流程(以 `record_fetch` 为例)
+
+1. 取 Redis 网关锁 `fault-record:lock:<env>:<gatewaySN>`(带 TTL 并续租)。取不到就结束操作,状态为 `gateway_locked`(本工具内已有操作在进行)。
+2. 订阅 `fwh/c2s/mng/<sn>` 和 `fwh/s2c/mng/<sn>`。
+3. 用 `Publish` 发 `377`,写 `sent`。
+4. 收到报文就交给 L2 解析,推进状态,并把原始报文追加到记录:
+
+```text
+pending → sent → accepted(378) → ready(379) → uploading(607) → uploaded(609) → downloaded → done
+```
+
+5. 收到 `609.result=0` 后,按环境域名下载,存入 MinIO,解析,写 `done`。
+6. 释放锁,取消订阅。
+
+终止状态分开记录:`busy`(设备回复 busy,明确提示,不重试)、`rejected`(378 失败)、`item_failed`(379 中 `resultOne` 不为 ok)、`timeout_<阶段>`、`upload_failed`(609 失败)、`download_failed`、`parse_failed`、`cancelled`、`interrupted`。
+
+其他操作按同一框架执行,只到 `ready(379)` 为止,再解析结果写 `done`。
+
+### 4.4 关联规则
+
+- 377 → 378/379:环境 + `equipNo` + `cmdType` + `dataArea.cmd`。网关锁保证本工具内同一网关同一时间只有一个操作。
+- 379 → 607/608/609:`equipNo` + 文件名(`609.fileName` = `379.filename + ".tar.bz2"`)。
+- 发送的 `377` 也会出现在 s2c topic 上,按 `cmdType` 过滤掉。
+
+### 4.5 取消(`cmd=4`)
+
+`cmd=4` 是 FaultFilter 协议的“退出”命令:通知设备结束当前处理,`data` 为 `{}`,没有业务回复。它是设备忙时**唯一**会被接受的命令。
+
+| 取消时所处阶段 | 处理 |
+| --- | --- |
+| `pending`(还没发 377) | 直接取消,不发任何消息 |
+| `sent`/`accepted`(设备正在处理我们的命令) | 发 `377(cmd=4)`,标记 `cancelled`,释放锁 |
+| `ready` 及之后(设备已处理完,平台在上传) | 不发 `cmd=4`(设备已无处理中的命令,且无法终止平台上传),只停止等待,标记 `cancelled` |
+| 状态为 `busy` 等终止态 | 不可取消 |
+
+限制:
+
+- `cmd=4` 作用于设备,不区分发起方。只有在本操作的命令确实被设备接受(没有收到 `busy`)时才发送,避免中断别人发起的处理。
+- 设备是否真的停止、外层是否有回复,待实测;页面显示“已发送取消”,不承诺设备一定停止。
+
+### 4.6 重启恢复
+
+asynq 会把服务重启时未完成的任务重新投递。执行器先读 Mongo 状态,从断点继续:
+
+| 重启时状态 | 恢复方式 |
+| --- | --- |
+| `pending` | 正常执行 |
+| `sent`/`accepted` | 不能确定设备是否已回复,且重发会清空设备文件,标记 `interrupted`,由用户手动重试 |
+| `ready`/`uploading`(已知文件名) | 直接按下载地址尝试下载;成功则继续解析,失败则重新订阅等待 `609`,直到超时 |
+| `uploaded`/`downloaded` | 继续下载/解析 |
+
+服务启动时扫描 Mongo 中未终止、但 asynq 中已不存在的操作(例如 Redis 数据丢失),按上表处理。
+
+## 5. 设备与站点信息
+
+- 用户输入网关 SN,Service 按环境调用 `fwh_pro.QuerySiteInfoList` 或 `fwh_test.QuerySiteInfoList` 获取站点信息。
+- 可选设备为:`aGate`(网关 SN)+ `fhpSn` 中逗号分隔的每个 aPower SN。**aPower SN 就是 `pe` SN**(用户确认,2026-10-16)。
+- 生产服和测试服是两个不同的接口(`fwh_pro` 与 `fwh_test`),必须按操作所属环境调用,不能用生产服站点信息去操作测试服设备,反之亦然。
+- 只调用 Repository 层接口,不复用 `services.GetSiteInfoData`:后者会把结果写入站点信息表,测试服数据不能写入。
+
+## 6. 测试服与生产服
+
+每次操作绑定一个环境,MQTT、站点信息和文件下载都取自同一环境,不能混用。
+
+| 项目 | 生产服 | 测试服 |
+| --- | --- | --- |
+| MQTT broker/证书 | `FWH_MQTT_BROKER`、`FWH_MQTT_CA/CERT/KEY` | `FWH_TEST_MQTT_BROKER`、`FWH_TEST_MQTT_CA/CERT/KEY` |
+| FWH 平台域名 | `FWH_ENERGY`(默认 `energy.franklinwh.com`) | `FWH_TEST_ENERGY`(如 `test.franklinwh.com`) |
+| 站点信息 | `fwh_pro` | `fwh_test` |
+| 下载凭据 | 不需要 | 不需要 |
+
+文件下载地址(用户确认,2026-10-16):
+
+```text
+https://<环境域名>/hes-upload/v1/file/download/logger/<equipNo>/<609.fileName>
+```
+
+- `<equipNo>` 取 `609` 外层的 `equipNo`;`pe` 场景下是否仍为网关 SN 待实测。
+- 域名取自环境配置,不从 `607.url` 推导(`607.url` 是上传地址,样本为 `http://`)。
+- 文件扩展名是 `.tar.bz2`,实际是 gzip tar;解压按文件头判断,同时兼容 gzip 和 bzip2。
+
+## 7. 存储
+
+### 7.1 MongoDB:操作记录
+
+选择 MongoDB:每次操作的报文条数和结果结构不固定,适合文档存储;与 DataDownload 的任务记录方式一致,已有连接与 Repository 写法。不使用 PostgreSQL:波形约 49KB,查看和导出时现场解析即可,无需结构化建表。
+
+集合 `fault_record_operations`(字段草案):
+
+| 字段 | 说明 |
+| --- | --- |
+| `_id` | operationId |
+| `env` | `prod`/`test` |
+| `gatewaySn`、`deviceType`、`deviceSn` | 目标 |
+| `kind`、`cmd`、`request` | 操作类型与请求参数 |
+| `status`、`stage`、`stageTimes` | 当前状态和各阶段时间 |
+| `operator` | 操作人(取自登录用户),历史共享可见 |
+| `messages[]` | 原始报文:方向、topic、cmdType、接收时间、原文 |
+| `result` | 解析后的结果(目录、设备信息、参数等) |
+| `fileName`、`faultRecordId`、`minioObject` | `record_fetch` 专用 |
+| `error` | 失败原因 |
+| `createdAt`、`updatedAt` | |
+
+索引:`env + gatewaySn + createdAt`、`status`、`createdAt`。暂不实现数据清理。
+
+### 7.2 MinIO:原始文件
+
+- 对象键:`fault-record/<env>/<gatewaySn>/<fileName>`,保留下载到的原始字节和原始文件名。
+- 对象键不经 HTTP 暴露,由 Service 按 operationId 读取。
+
+### 7.3 导出
+
+| 格式 | 内容 |
+| --- | --- |
+| 原始压缩包 | 直接从 MinIO 读出返回 |
+| CSV(zip) | `waveform.csv`:每行一个采样点,列为点序号、相对故障时刻毫秒、各通道原始值,表头为通道序号和含义代码;`summary.csv`:基本信息、版本、触发源、快照通道、采样通道、可配通道 |
+
+CSV 现场生成,不缓存。
+
+## 8. 时间口径
+
+目录、`failuredata`、对时功能中的时间均按**设备时间**原样显示,不做时区换算(暂定,实测有问题再改)。操作记录中的时间(创建、阶段时间)用服务器时间存储,前端按本地时间显示。
+
+## 9. 待实测前提
+
+1. 本工具直接发出 `377(cmd=2)` 后,平台是否同样自动发 `607` 并上传。这是“只监听”方案成立的前提。
+2. 两个环境下能否订阅 s2c 方向(`607/380/610`);不能时只能依赖 `609` 推进状态。
+3. `cmd=0/1/3` 的 `380` 是否也由平台回复。
+4. `pe` 场景:外层 `equipNo` 与 `info[].sn` 的关系、下载路径中的 `<equipNo>`。
+5. `cmd=1/3` 经外层的实际报文和失败样本;`busy` 经外层如何体现。
+6. `cmd=4` 的外层表现和实际效果。
+
+协议遗留问题见 [protocol-specification.md](protocol-specification.md#10-待补充清单) B12、B14。

+ 184 - 0
docs/fault-record/ui-design.md

@@ -0,0 +1,184 @@
+# 故障录波前端页面设计
+
+> 状态:初稿,2026-10-16。用户将根据实现效果再提意见。功能目标见 [requirements.md](requirements.md),后端设计见 [technical-design.md](technical-design.md)。组件库沿用 naive-ui,波形图沿用 uPlot。
+
+## 1. 菜单结构
+
+```text
+故障录波                     (替换现有“故障录波配置”)
+├── 生产服
+│   ├── 录波记录
+│   ├── 设备信息
+│   ├── 参数读取
+│   └── 参数设置
+└── 测试服
+    └── (同上)
+```
+
+- 路由形如 `fault-record/prod/record`、`fault-record/test/param-write`。两套环境使用同一组页面组件,环境从路由参数获取。
+- 现有 `fault-record/read`、`fault-record/write` 页面下线,参数设置按第 6 节重新设计。
+- 每个页面顶部固定显示环境标签:**生产服**用醒目的警示色,测试服用中性色,避免在错误的环境操作。
+
+## 2. 公共组件
+
+### 2.1 设备选择条
+
+```text
+┌──────────────────────────────────────────────────────────────────┐
+│ [生产服]  网关 SN [________________] [查询]   设备 [aGate ▼]        │
+│           站点:xxx  在线  aPower ×3                               │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+- 输入网关 SN 后查询站点信息,设备下拉为 `aGate` + 各 aPower SN(`pe`)。
+- 显示站点在线状态,离线时给出提示但不禁止操作。
+- 最近使用的网关 SN 存在本地,方便再次选择。
+
+### 2.2 操作进度
+
+```text
+已发送 ─── 设备已应答 ─── 数据就绪 ─── 平台上传中 ─── 上传完成 ─── 已下载 ─── 完成
+  ✓            ✓            ●
+                                                         [取消]
+```
+
+- 每个阶段显示时间;失败时在对应阶段显示原因(设备忙、超时、单项失败等)。
+- “设备忙”单独提示:“设备正在处理其他命令(可能来自 FWH 平台),请稍后再试”。
+- 可取消的阶段显示“取消”按钮,规则见 [technical-design.md](technical-design.md#45-取消cmd4)。
+- 可展开“原始报文”,按时间列出收发的全部报文 JSON,便于排查。
+
+### 2.3 操作历史
+
+各功能页底部都有历史表,按当前环境和网关筛选,所有人共享:
+
+| 时间 | 操作人 | 网关 | 设备 | 操作 | 参数摘要 | 状态 | 操作 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+
+点击一行可回看当时的进度、原始报文和结果。
+
+## 3. 录波记录
+
+```text
+┌ 设备选择条 ───────────────────────────────────────────────────────┐
+├ 故障录波总目录                                   [刷新目录] ────────┤
+│ 序号 │ 发生时间(设备时间)│ 触发源                       │ 参数版本 │ 操作      │
+│  7   │ 2026-08-18 03:03:08 │ 0x1001 并网发生状态切换故障 │ 00000001 │ [获取]    │
+│  …                                                                 │
+├ 当前操作进度(见 2.2) ─────────────────────────────────────────────┤
+├ 操作历史(见 2.3),已完成的行有 [查看] [导出 ▼] ──────────────────────┤
+└──────────────────────────────────────────────────────────────────┘
+```
+
+- 目录按设备返回的顺序显示,不按序号推断时间先后;允许按时间列排序。
+- 触发源显示代码和名称(名称来自协议文档 F15 表,表外代码只显示代码)。
+- 每次只获取一条记录;获取进行中时,其他“获取”按钮禁用。
+- 导出下拉:原始压缩包、CSV。
+
+### 3.1 录波详情(抽屉或独立标签页)
+
+```text
+┌ 基本信息 ───────────────────────────────────────────────────────┐
+│ 记录序号 7  发生时间 2026-08-18 03:03:08  触发源 0x1001           │
+│ 采样频率 4500 Hz  采样点 1536(故障前 1024 / 故障后 512)          │
+│ 存储格式 v1  CAN 协议 v1  参数版本 00000001  DCDC EMS_SL:V10R80B21D00 │
+├ 波形 ────────────────────────────────────────────────────────────┤
+│ 通道:☑0 0x0010 ☑1 0x0011 … ☑12 可配 Uint16@0x14990 …   [全选][反选] │
+│ ┌──────────────────────────────────────────────────────────────┐ │
+│ │  uPlot 多通道折线,竖线标出故障时刻(第 1024 点 / 0 ms)        │ │
+│ └──────────────────────────────────────────────────────────────┘ │
+│ X 轴:[采样点 | 相对故障时刻 ms]   Y 轴:原始值(int16)             │
+├ 快照通道 ──────────────────────────────────────────────────────────┤
+│ 序号 │ 含义代码 │ 原始值 │                                          │
+├ 可配通道 ──────────────────────────────────────────────────────────┤
+│ 序号 │ 类型 │ 地址 │                                                │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+- 通道数值量级差异大,提供“分组显示”:每个勾选通道一个小图,共享 X 轴和游标,避免小信号被大信号压平。
+- 游标悬停显示所有勾选通道在该点的原始值。
+
+## 4. 设备信息
+
+- 勾选 `code`:故障总体信息(1)、录波功能基本信息(2)、录波总目录(3),点击“读取”。
+- 每个 code 一张卡片,按协议字段列出原始值;可展开原始 `hexData`。
+
+## 5. 参数读取
+
+```text
+┌ 设备选择条 ───────────────────────────────────────────────────────┐
+├ 功能码:☑2 采样通道配置 ☑4 设备时间 ☑5 采样频率 ☑6 可配采样通道 ☑7 录波开关  [读取] ┤
+├ 结果 ─────────────────────────────────────────────────────────────┤
+│ 功能码 │ 名称         │ 原始十六进制 │ 解读           │ 结果      │
+│ 5      │ 采样频率     │ 1194         │ 4500 Hz        │ ok        │
+│ 7      │ 录波开关     │ 01           │ 使能           │ ok        │
+│ 2      │ 采样通道配置 │ …            │ 16 个通道(展开)│ ok        │
+└──────────────────────────────────────────────────────────────────┘
+```
+
+- 功能码 1、3 是动作,没有可读的状态,不在读取列表中。
+- “解读”只针对协议已定义结构的功能码(2、4、5、6、7),解不出来时只显示原始值。
+- 结果可一键“带入参数设置”。
+
+## 6. 参数设置
+
+相对现有页面的改进:
+
+| 现有页面 | 新设计 |
+| --- | --- |
+| 手填功能码数字和十六进制值 | 按功能码提供结构化表单,自动生成 `setVal`,并实时显示十六进制预览 |
+| 不知道设备当前值 | 可先“读取当前值”预填表单,修改项高亮 |
+| 无提交前确认 | 统一确认弹窗,危险操作醒目标注 |
+| 只显示成功/失败 | 显示进度和逐项 `resultOne`,可选设置后自动复读 |
+| 一次可填多个子设备 | 一次操作只针对一个设备(与后端操作模型一致) |
+
+```text
+┌ 设备选择条 ─────────────────────────────── [读取当前值] ──────────┐
+├ 功能项 ─────────────────────────────────┬ 待提交 ─────────────────┤
+│ ▸ 采样频率 (5)     [ 4500 ] Hz           │ 5  采样频率   1194   [×] │
+│ ▸ 录波开关 (7)     (●使能 ○关闭)         │ 7  录波开关   01     [×] │
+│ ▸ 设备对时 (4)     [日期时间] [取当前时间] │ 6  可配通道1  010100014990│
+│ ▸ 采样通道配置 (2) 通道列表编辑器          │                          │
+│ ▸ 可配采样通道 (6) 行编辑器:              │ ☐ 设置成功后自动复读      │
+│     索引 │ 方式 │ 地址类型 │ 地址/代码    │                          │
+│ ▸ 危险操作                               │        [提交设置]         │
+│     [清除故障波形 (1)] [手动触发录波 (3)]  │                          │
+├ 高级:原始模式(直接输入功能码 + 十六进制,保留旧页面的能力) ────────┤
+├ 进度与逐项结果 ───────────────────────────────────────────────────────┤
+├ 操作历史 ────────────────────────────────────────────────────────────┤
+└──────────────────────────────────────────────────────────────────┘
+```
+
+各功能码的表单:
+
+| id | 表单 | 校验 |
+| --- | --- | --- |
+| 2 | 通道含义代码列表(每项 4 位十六进制),可增删、拖动排序;通道数自动计算 | 每项 2 字节 |
+| 4 | 日期时间选择(按设备时间)+“取当前时间” | 年 0~65535 |
+| 5 | 数字输入,单位 Hz | 1~65535 |
+| 6 | 每行:索引号、设置方式(默认/代码)、地址类型(默认方式时)、地址或代码(十六进制);可多行,每行生成一项 `id=6` | 按方式校验长度 |
+| 7 | 单选:使能/关闭 | — |
+| 1、3 | 按钮加入待提交列表 | 无参数 |
+
+### 6.1 提交确认弹窗
+
+```text
+┌ 确认设置 ────────────────────────────────────┐
+│ 环境:[生产服]   网关:xxx   设备:pe 10050015…│
+│                                              │
+│ 5  采样频率      4500 Hz  (1194)              │
+│ 7  录波开关      关闭     (00)   ⚠ 将停止录波   │
+│ 1  清除故障波形           ⚠ 不可恢复           │
+│                                              │
+│ ☐ 我已确认以上操作                            │
+│                        [取消]  [确认提交]      │
+└──────────────────────────────────────────────┘
+```
+
+- 所有设置都弹窗确认;包含 id=1(清除波形)、id=3(手动触发)、id=4(对时)、id=7 关闭录波时,对应行标红并加说明,且必须勾选“我已确认”才能提交。
+- 生产服环境的确认按钮使用警示色。
+
+### 6.2 结果
+
+- 进度同 2.2,到 `379` 为止。
+- 逐项结果表:功能码、名称、设置值、`resultOne`(非 ok 时显示错误码含义,来自协议文档 10.5/10.6)。
+- 勾选“自动复读”时,成功后对涉及的功能码自动发起一次参数读取,并与设置值比对。