node-red-zelecproto 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/645.js ADDED
@@ -0,0 +1,1055 @@
1
+ // 放在文件顶部,供全局复用
2
+ const __HEX__ = Array.from({ length: 256 }, (_, n) =>
3
+ (n < 16 ? '0' : '') + n.toString(16).toUpperCase()
4
+ );
5
+
6
+
7
+
8
+
9
+ /******************** 编码:build645Frame(含读地址命令) ********************/
10
+ function build645Frame(i, reverseAddr, csMode) {
11
+ const oad = (i.payload && i.payload.oad) ? i.payload.oad : i.oad;
12
+
13
+ // —— 特殊:读通信地址命令(无 DATA)——
14
+ if (oad === 'readAdress' || oad === 'readAddress') {
15
+ return buildReadAddressFrame(i, reverseAddr, csMode);
16
+ }
17
+
18
+ const addrRaw = (i.com_exec_addr || i.addr || 'AAAAAAAAAAAA').toUpperCase().replace(/\s+/g, '');
19
+ if (addrRaw.length !== 12) throw new Error('com_exec_addr 必须是 6 字节(12个HEX字符)');
20
+
21
+ // 地址字节
22
+ let addrBytes = hexToBytes(addrRaw);
23
+ if (reverseAddr) addrBytes = addrBytes.slice().reverse();
24
+
25
+
26
+ // 检查是否为加密报文
27
+ if (i.payload && i.payload.isEncrypted) {
28
+ return buildEncrypted645Frame(i, addrBytes, reverseAddr, csMode);
29
+ }
30
+
31
+
32
+ // OAD 加密:倒序 + 每字节 +0x33
33
+ const cmdEx = encryptOAD(oad); // "xx xx xx xx"
34
+ const dataBytes = hexToBytes(cmdEx.replace(/\s+/g, ''));
35
+
36
+ // 帧体:68 A0..A5 68 C L DATA CS 16
37
+ const C = 0x11; // 读命令
38
+ const L = 0x04; // 数据长度
39
+ const frameNoCS = [0x68, ...addrBytes, 0x68, C, L, ...dataBytes];
40
+ const cs = calc645CSForBytes(frameNoCS, csMode);
41
+ const csHex = cs.toString(16).toUpperCase().padStart(2, '0');
42
+
43
+ // 为兼容你原用法:最终 HEX 串不含空格,前导 FE×4 + 帧体 + CS + 16
44
+ const fullHex = (`FE FE FE FE ${bytesToHex(frameNoCS)} ${csHex} 16`).replace(/\s+/g, '');
45
+ let _payload = i.payload || {}
46
+ return Object.assign(i, _payload, {
47
+ com_exec_addr: bytesToHex(addrBytes).replace(/\s+/g, ''), // 按当前编码顺序(倒序与否)给出
48
+ cmdEx, // 加密后的 OAD(调试用)
49
+ payload: fullHex // 最终 645 帧(HEX 串,无空格)
50
+ });
51
+ }
52
+
53
+ /**
54
+ * 读取地址
55
+ */
56
+ function buildReadAddressFrame(i, reverseAddr, csMode) {
57
+ // 读地址命令:C=0x13, L=0x00, 无 DATA
58
+ // 下行目的地址通常用广播地址 'AA AA AA AA AA AA'
59
+ const addrRaw = 'AAAAAAAAAAAA';
60
+ let addrBytes = hexToBytes(addrRaw);
61
+ if (reverseAddr) addrBytes = addrBytes.slice().reverse();
62
+
63
+ const C = 0x13;
64
+ const L = 0x00;
65
+
66
+ const frameNoCS = [0x68, ...addrBytes, 0x68, C, L];
67
+ const cs = calc645CSForBytes(frameNoCS, csMode);
68
+ const csHex = cs.toString(16).toUpperCase().padStart(2, '0');
69
+
70
+ const fullHex = (`FE FE FE FE ${bytesToHex(frameNoCS)} ${csHex} 16`).replace(/\s+/g, '');
71
+ let _payload = i.payload || {}
72
+ return Object.assign(i, _payload, {
73
+ com_exec_addr: bytesToHex(addrBytes).replace(/\s+/g, ''),
74
+ payload: fullHex
75
+ });
76
+ }
77
+ /**
78
+ * 构建加密报文
79
+ * {
80
+ com_exec_addr: '398766010000',
81
+ cs_mode: 'full',
82
+ payload: {
83
+ oad: '070000FF',
84
+ des: '数据回抄',
85
+ code: '03',
86
+ tag: 'dataReadback',
87
+ isEncrypted: true,
88
+ operatorCode: '00000000',
89
+ cipherText: '8071B9EAE918ACB0', // 密文
90
+ randomNumber: '43E19E3BD45E172B', // 随机数
91
+ diversifier: '6CBA993433333333', // 分散因子
92
+ controlCode: 0x03 // 添加控制码设置
93
+ }
94
+ }
95
+ */
96
+ function buildEncrypted645Frame(i, addrBytes, reverseAddr, csMode) {
97
+ const payload = i.payload;
98
+
99
+ // 加密报文的数据域结构
100
+ let dataBytes = [];
101
+ const isControlFrame = !!(payload && payload.password);
102
+
103
+ if (isControlFrame) {
104
+ if (payload.password) {
105
+ dataBytes.push(...encodeControlSegment(payload.password, { pad: 4 }));
106
+ }
107
+ if (payload.operatorCode) {
108
+ dataBytes.push(...encodeControlSegment(payload.operatorCode, { pad: 4 }));
109
+ }
110
+ if (payload.cipherText) {
111
+ dataBytes.push(...encodeControlSegment(payload.cipherText, {
112
+ reverse: payload.cipherReverse !== false,
113
+ apply33: payload.cipherNeeds33 !== false
114
+ }));
115
+ }
116
+ if (payload.extraData) {
117
+ dataBytes.push(...encodeControlSegment(payload.extraData, {
118
+ reverse: payload.extraReverse !== false,
119
+ apply33: payload.extraNeeds33 !== false
120
+ }));
121
+ }
122
+ } else {
123
+ if (!payload.oad) throw new Error('加密报文需要提供数据标识 oad');
124
+ // 数据标识(OAD)
125
+ const oadBytes = hexToBytes(encryptOAD(payload.oad).replace(/\s+/g, ''));
126
+ dataBytes.push(...oadBytes);
127
+
128
+ // 操作者代码(通常为 4 字节)
129
+ if (payload.operatorCode) {
130
+ const opCodeBytes = hexToBytes(payload.operatorCode);
131
+ // 操作者代码也需要 +0x33 处理
132
+ const encryptedOpCode = opCodeBytes.map(b => (b + 0x33) & 0xFF);
133
+ dataBytes.push(...encryptedOpCode);
134
+ }
135
+
136
+ // 根据不同的加密类型处理数据
137
+ if (payload.authData) {
138
+ // 身份认证数据:密文 + 随机数 + 分散因子
139
+ const authDataBytes = hexToBytes(payload.authData.replace(/\s+/g, ''));
140
+ dataBytes.push(...authDataBytes);
141
+ } else if (payload.cipherText) {
142
+ // 密文数据
143
+
144
+ const cipherHex = payload.cipherText.replace(/\s+/g, '');
145
+ const reversedCipher = reverseHexBytes(cipherHex); // 反转字节序
146
+ const cipherBytes = hexToBytes(reversedCipher).map(b => (b + 0x33) & 0xFF);
147
+ dataBytes.push(...cipherBytes);
148
+ // const cipherBytes = hexToBytes(payload.cipherText.replace(/\s+/g, ''));
149
+ // dataBytes.push(...cipherBytes);
150
+ // 随机数
151
+ if (payload.randomNumber) {
152
+ const randHex = payload.randomNumber.replace(/\s+/g, '');
153
+ const reversedRand = reverseHexBytes(randHex);
154
+ const randBytes = hexToBytes(reversedRand).map(b => (b + 0x33) & 0xFF);
155
+ dataBytes.push(...randBytes);
156
+ }
157
+
158
+ // if (payload.randomNumber) {
159
+ // const randomBytes = hexToBytes(payload.randomNumber.replace(/\s+/g, ''));
160
+ // dataBytes.push(...randomBytes);
161
+ // }
162
+
163
+ // 分散因子
164
+ if (payload.diversifier) {
165
+ const divHex = payload.diversifier.replace(/\s+/g, '');
166
+ const reversedDiv = reverseHexBytes(divHex);
167
+ const divBytes = hexToBytes(reversedDiv).map(b => (b + 0x33) & 0xFF);
168
+ dataBytes.push(...divBytes);
169
+ }
170
+ // if (payload.diversifier) {
171
+ // const divBytes = hexToBytes(payload.diversifier.replace(/\s+/g, ''));
172
+ // dataBytes.push(...divBytes);
173
+ // }
174
+ }
175
+ }
176
+
177
+ // 帧体:68 A0..A5 68 C L DATA CS 16
178
+ const C = payload.controlCode || 0x11; // 控制码,默认为读数据
179
+ const L = dataBytes.length; // 数据长度
180
+ const frameNoCS = [0x68, ...addrBytes, 0x68, C, L, ...dataBytes];
181
+ const cs = calc645CSForBytes(frameNoCS, csMode);
182
+ const csHex = cs.toString(16).toUpperCase().padStart(2, '0');
183
+
184
+ // 为兼容你原用法:最终 HEX 串不含空格,前导 FE×4 + 帧体 + CS + 16
185
+ const fullHex = (`FE FE FE FE ${bytesToHex(frameNoCS)} ${csHex} 16`).replace(/\s+/g, '');
186
+ let _payload = i.payload || {}
187
+ return Object.assign(i, _payload, {
188
+ com_exec_addr: bytesToHex(addrBytes).replace(/\s+/g, ''), // 按当前编码顺序(倒序与否)给出
189
+ payload: fullHex // 最终 645 帧(HEX 串,无空格)
190
+ });
191
+ }
192
+ function encodeControlSegment(hexStr, { pad = undefined, reverse = true, apply33 = true } = {}) {
193
+ if (!hexStr) return [];
194
+ let clean = hexStr.replace(/\s+/g, '').toUpperCase();
195
+ if (pad) clean = clean.padStart(pad * 2, '0');
196
+ if (clean.length % 2 !== 0) throw new Error('控制段必须是偶数个HEX字符');
197
+ const bytes = [];
198
+ if (reverse) {
199
+ for (let i = clean.length; i > 0; i -= 2) {
200
+ bytes.push(parseInt(clean.slice(i - 2, i), 16));
201
+ }
202
+ } else {
203
+ for (let i = 0; i < clean.length; i += 2) {
204
+ bytes.push(parseInt(clean.slice(i, i + 2), 16));
205
+ }
206
+ }
207
+ if (!apply33) return bytes;
208
+ return bytes.map(b => (b + 0x33) & 0xFF);
209
+ }
210
+ // 辅助函数:反转 hex 字符串的字节序
211
+ function reverseHexBytes(hex) {
212
+ const clean = hex.toUpperCase();
213
+ if (clean.length % 2 !== 0) throw new Error('Invalid hex');
214
+ const bytes = [];
215
+ for (let i = clean.length - 2; i >= 0; i -= 2) {
216
+ bytes.push(clean[i], clean[i + 1]);
217
+ }
218
+ return bytes.join('');
219
+ }
220
+
221
+ /******************** 解码:batchMsg(返回统一对象,含 exec_addr/di/value 等) ********************/
222
+ function batchMsg(_msg) {
223
+ // ===== 工具 =====
224
+ function calc645Checksum(u8) {
225
+ let s = 0;
226
+ for (let i = 0; i < u8.length; i++) s = (s + u8[i]) & 0xFF;
227
+ return s;
228
+ }
229
+ function bcdAddr12FromLE(addr6) {
230
+ // 帧内地址:小端字节序(低位在前) → 显示需倒序
231
+ const rev = Array.from(addr6).reverse();
232
+ return Buffer.from(rev).toString('hex').toUpperCase(); // 12位
233
+ }
234
+ function minus33(arr) { return arr.map(v => (v - 0x33) & 0xFF); }
235
+ function bytesToIntBE(a) { return parseInt(Buffer.from(a).toString('hex')); }
236
+
237
+ // ===== 取入参并清洗 =====
238
+ // 允许传 {put:'...'} 或 {payload:'...'},优先 put
239
+ let put = ((_msg.payload && _msg.payload.put) || _msg.put || _msg.payload || '').toString();
240
+ if (!put) return { ok: false, reason: 'empty' };
241
+
242
+ // 去前导 FE…FE(最多4个),并去空格
243
+ put = f_stripLeadingFE(put.replace(/\s+/g, '')).toUpperCase();
244
+
245
+ const buf = hexStringToBuffer(put);
246
+ if (!buf || buf.length < 12) return { ok: false, reason: 'too_short', raw: put };
247
+
248
+ // ===== 基本结构:68 AA(6) 68 CTRL LEN DATA(n) CS 16 =====
249
+ // 容错:定位到第一个 0x68
250
+ let p0 = 0;
251
+ while (p0 < buf.length && buf[p0] !== 0x68) p0++;
252
+ if (p0 + 12 > buf.length) return { ok: false, reason: 'no_head68', raw: put };
253
+
254
+ // 第二个 0x68 在地址后
255
+ const p68_2 = p0 + 7;
256
+ if (buf[p0] !== 0x68 || buf[p68_2] !== 0x68) return { ok: false, reason: 'bad_68_pair', raw: put };
257
+
258
+ // 读出地址区(A0..A5)
259
+ const addrBytes = buf.subarray(p0 + 1, p0 + 7);
260
+ const exec_addr = bcdAddr12FromLE(addrBytes); // 解析后 12 位
261
+ const addr_bytes_hex = bytesToHex(addrBytes).replace(/\s+/g, ''); // 帧中原始顺序(小端)
262
+
263
+ // CTRL / LEN / DATA / CS / 16
264
+ const ctrl = buf[p68_2 + 1];
265
+ const len = buf[p68_2 + 2];
266
+ const pDataStart = p68_2 + 3;
267
+ const pDataEnd = pDataStart + len; // 不包含
268
+ const pCS = pDataEnd;
269
+ const pEnd = pDataEnd + 1;
270
+
271
+ if (len < 0 || pEnd >= buf.length) return { ok: false, reason: 'len_oob', exec_addr, raw: put };
272
+ if (buf[pEnd] !== 0x16) return { ok: false, reason: 'no_end16', exec_addr, raw: put };
273
+
274
+ // —— CS 双模校验:std(第二个68后)与 full(第一个68起)任一通过即可 ——
275
+ const cs_frame = buf[pCS];
276
+ const cs_std = calc645Checksum(buf.subarray(p68_2 + 1, pCS));
277
+ const cs_full = calc645Checksum(buf.subarray(p0, pCS));
278
+ const cs_ok = (cs_std === cs_frame) || (cs_full === cs_frame);
279
+ if (!cs_ok) {
280
+ return Object.assign(_msg, {
281
+ success: false, reason: 'cs_fail',
282
+ cs_frame: cs_frame.toString(16).toUpperCase().padStart(2, '0'),
283
+ cs_std: cs_std.toString(16).toUpperCase().padStart(2, '0'),
284
+ cs_full: cs_full.toString(16).toUpperCase().padStart(2, '0'),
285
+ exec_addr, raw: put
286
+ });
287
+ }
288
+
289
+ const dataRaw = Array.from(buf.subarray(pDataStart, pDataEnd));
290
+ // ===== 控制确认/错误帧 =====
291
+ if (len === 0 && ctrl === 0x9C) {
292
+ return Object.assign(_msg, {
293
+ ok: true,
294
+ type: "control_ack",
295
+ exec_addr,
296
+ addr_bytes_hex,
297
+ ctrl,
298
+ len,
299
+ description: "控制命令执行成功 (0x9C)",
300
+ success: cs_ok,
301
+ raw: put
302
+ });
303
+ }
304
+
305
+ if (ctrl === 0xDC) {
306
+ const detail = minus33(dataRaw);
307
+ return Object.assign(_msg, {
308
+ ok: true,
309
+ type: "control_ack_ext",
310
+ exec_addr,
311
+ addr_bytes_hex,
312
+ ctrl,
313
+ len,
314
+ statusBytes: bytesToHex(detail).replace(/\s+/g, ""),
315
+ success: true,
316
+ raw: put
317
+ });
318
+ }
319
+
320
+ if (ctrl === 0xDA) {
321
+ const detail = minus33(dataRaw);
322
+ return Object.assign(_msg, {
323
+ ok: false,
324
+ type: "control_error",
325
+ exec_addr,
326
+ addr_bytes_hex,
327
+ ctrl,
328
+ len,
329
+ reason: "unauthorized_or_password_error",
330
+ detail: detail.length ? detail[0].toString(16).toUpperCase().padStart(2, "0") : null,
331
+ data: bytesToHex(detail).replace(/\s+/g, ""),
332
+ raw: put
333
+ });
334
+ }
335
+
336
+ if (ctrl === 0xD1) {
337
+ const errRaw = minus33(dataRaw);
338
+ return Object.assign(_msg, {
339
+ ok: false,
340
+ type: "control_error",
341
+ exec_addr,
342
+ addr_bytes_hex,
343
+ ctrl,
344
+ len,
345
+ reason: "control_rejected",
346
+ detail: errRaw.length ? errRaw[0].toString(16).toUpperCase().padStart(2, "0") : null,
347
+ data: bytesToHex(errRaw).replace(/\s+/g, ""),
348
+ raw: put
349
+ });
350
+ }
351
+ // ===== 读地址响应:CTRL=0x93 且 LEN=0x06 =====
352
+ if (ctrl === 0x93 && len === 0x06) {
353
+ const addrMinus33 = minus33(dataRaw);
354
+ const addr_from_data = bcdAddr12FromLE(Uint8Array.from(addrMinus33));
355
+ return Object.assign(_msg, {
356
+ ok: true,
357
+ type: 'address_response',
358
+ exec_addr, // 帧头解析出的地址
359
+ addr_from_data, // DATA-0x33 解析出的地址
360
+ same: addr_from_data === exec_addr,
361
+ ctrl, len,
362
+ addr_bytes_hex,
363
+ success: cs_ok,
364
+ raw: put
365
+ });
366
+ }
367
+
368
+ // ===== 普通数据响应:DATA-0x33,首4字节(倒序)为 DI =====
369
+ const arrPush = minus33(dataRaw);
370
+ let di = '';
371
+ if (arrPush.length >= 4) di = Buffer.from(arrPush.slice(0, 4).reverse()).toString('hex').toUpperCase();
372
+
373
+ // 动态判定是否为 “日冻结/结算日冻结” 的 DI(不再限定 30 天,可到 62 等任意 1B 日号)
374
+ function isDailyFreezeDI(di) {
375
+ // 645 常见日冻结:050601dd(正向)、050602dd(反向);dd 为 1B 日号(01~3E/3F/…,扩展都能兜住)
376
+ return /^05060[12][0-9A-F]{2}$/i.test(di || '');
377
+ }
378
+ function isSettlementFreezeDI(di) {
379
+ // 结算日冻结(部分表厂):000100dd(正向)、000200dd(反向)
380
+ return /^(000100|000200)[0-9A-F]{2}$/i.test(di || '');
381
+ }
382
+
383
+
384
+ //判定为组合电量
385
+ function isToatalDI(di) {
386
+ let arr = ['0000FF00', '0001FF00', '0002FF00']
387
+ return arr.includes(di)
388
+ }
389
+
390
+
391
+ // —— 常用分支(保留你的原分支,增加越界保护)——
392
+ function buildDays(prefix) {
393
+ let a = [];
394
+ for (let i = 0; i < 30; i++) a.push(`${prefix}${(i + 1).toString(16).toUpperCase().padStart(2, '0')}`);
395
+ return a;
396
+ }
397
+ const arrDays = buildDays('050601'); // 日冻结正向
398
+ const arrDaysFX = buildDays('050602'); // 日冻结反向
399
+ const arrDaysZX = buildDays('000100'); // 结算正向总电能
400
+ const arrDaysJSR = buildDays('000200'); // 结算反向总电能
401
+ const arrPub = ['04000B01', '04000B02', '04000B03']; // A/B/C 相电流(瞬时)
402
+
403
+ let value = '';
404
+ try {
405
+ // // —— 读数据“请求帧”:CTRL=0x11 且 LEN=0x04,仅含 DI,无数值 ——
406
+ // if (ctrl === 0x11 && len === 0x04 && arrPush.length === 4) {
407
+ // return Object.assign(_msg, {
408
+ // ok: true,
409
+ // type: 'read_request',
410
+ // exec_addr,
411
+ // addr_bytes_hex,
412
+ // ctrl, len, di,
413
+ // success: cs_ok,
414
+ // raw: put,
415
+ // value: null
416
+ // });
417
+ // }
418
+ if (di === '03110000' && arrPush.length >= 3) {
419
+ value = bytesToIntBE(arrPush.slice(-3).reverse());
420
+ } else if (di == '04000401') {
421
+ //通信地址解析
422
+ value = Buffer.from(arrPush.slice(-6).reverse()).toString('hex').toUpperCase()
423
+ } else if (di === '03370000' && arrPush.length >= 7) {
424
+ // 电源异常事件总次数(3字节无符号数)
425
+ value = bytesToIntBE(arrPush.slice(-3).reverse());
426
+ } else if (di === '04000105' && arrPush.length >= 2) {
427
+ value = bytesToIntBE(arrPush.slice(-2).reverse());
428
+ } else if (di === '03300100' && arrPush.length >= 3) {
429
+ value = bytesToIntBE(arrPush.slice(-3).reverse());
430
+ } else if (di === '01013003' && arrPush.length >= 106) {
431
+ value = Buffer.from(arrPush.slice(-106).reverse()).toString('hex').toUpperCase();
432
+ } else if (['02020100', '02020200', '02020300'].includes(di) && arrPush.length >= 3) {
433
+ value = bytesToIntBE(arrPush.slice(-3).reverse());
434
+ } else if (['02010100', '02010200', '02010300'].includes(di) && arrPush.length >= 3) {
435
+ value = bytesToIntBE(arrPush.slice(-3).reverse());
436
+ } else if (di === '02030000' && arrPush.length >= 3) {
437
+ //读瞬时功率
438
+ // value = Math.round((bytesToIntBE(arrPush.slice(-3).reverse()) * 0.0001) *1e4)/1e4;
439
+ value = bytesToIntBE(arrPush.slice(-3).reverse())
440
+ } else if (di === '02060000' && arrPush.length >= 2) {
441
+ value = bytesToIntBE(arrPush.slice(-2).reverse());
442
+ } else if (di === '02030100' && arrPush.length >= 4) {
443
+ value = bytesToIntBE(arrPush.slice(-4).reverse());
444
+ }
445
+ else if (di === '040005FF' && arrPush.length >= 6) {
446
+ // 运行状态数据块:常见为 2+2+2+4 = 10字节(状态字1/2/3 + 密钥状态字)
447
+ // 也有表只回部分:例如只回 1/2/3(6字节),或 1/2/3/8 之外还带厂商私有扩展。
448
+ const data = arrPush.slice(4); // 去掉DI
449
+ const statusBlockHex = Buffer.from(data).toString("hex").toUpperCase();
450
+ let off = 0;
451
+ const left = () => data.length - off;
452
+ const readU16LE = () => {
453
+ if (left() < 2) return null;
454
+ const lo = data[off] & 0xFF, hi = data[off + 1] & 0xFF;
455
+ off += 2;
456
+ return (hi << 8) | lo;
457
+ };
458
+ const readU32LE = () => {
459
+ if (left() < 4) return null;
460
+ const b0 = data[off] & 0xFF,
461
+ b1 = data[off + 1] & 0xFF,
462
+ b2 = data[off + 2] & 0xFF,
463
+ b3 = data[off + 3] & 0xFF;
464
+ off += 4;
465
+ return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0; // 无符号
466
+ };
467
+ const bin16 = (v) => (v >>> 0).toString(2).padStart(16, '0');
468
+ const bin32 = (v) => (v >>> 0).toString(2).padStart(32, '0');
469
+ const bit = (v, n) => ((v >>> n) & 1);
470
+
471
+ // —— 状态字1(与你现有的 04000501 保持一致)——
472
+ const w1 = readU16LE();
473
+ const word1 = (w1 === null) ? null : {
474
+ rawValue: w1,
475
+ rawBlockHex: statusBlockHex,
476
+ binary: bin16(w1),
477
+ keys: {
478
+ '停电抄表电池欠压': !!bit(w1, 3),
479
+ '时钟电池欠压': !!bit(w1, 2),
480
+ '有功功率方向反向': !!bit(w1, 4),
481
+ '无功功率方向反向': !!bit(w1, 5),
482
+ '控制回路错误': !!bit(w1, 8),
483
+ 'ESAM错误': !!bit(w1, 9),
484
+ '内部程序错误': !!bit(w1, 12),
485
+ '存储器故障或损坏': !!bit(w1, 13),
486
+ '透支状态': !!bit(w1, 14),
487
+ '时钟故障': !!bit(w1, 15)
488
+ }
489
+ };
490
+
491
+ // —— 状态字2(与你现有的 04000502 保持一致)——
492
+ const w2 = readU16LE();
493
+ const dir = (b) => (b ? '反向' : '正向');
494
+ const word2 = (w2 === null) ? null : {
495
+ rawValue: w2,
496
+ binary: bin16(w2),
497
+ fields: {
498
+ 'A相有功功率方向': dir(bit(w2, 0)),
499
+ 'B相有功功率方向': dir(bit(w2, 1)),
500
+ 'C相有功功率方向': dir(bit(w2, 2)),
501
+ 'A相无功功率方向': dir(bit(w2, 4)),
502
+ 'B相无功功率方向': dir(bit(w2, 5)),
503
+ 'C相无功功率方向': dir(bit(w2, 6))
504
+ }
505
+ };
506
+
507
+ // —— 状态字3(与你现有的 04000503 保持一致)——
508
+ const w3 = readU16LE();
509
+ const supplyBits = (w3 === null) ? 0 : ((w3 >> 1) & 0b11);
510
+ const supplyMode = (
511
+ supplyBits === 0 ? '主电源' :
512
+ supplyBits === 1 ? '辅助电源' :
513
+ supplyBits === 2 ? '电池供电' : '保留'
514
+ );
515
+ const meterTypeBits = (w3 === null) ? 0 : ((w3 >> 8) & 0b11);
516
+ const meterType = (
517
+ meterTypeBits === 0 ? '非预付费表' :
518
+ meterTypeBits === 1 ? '电量型预付费表' :
519
+ meterTypeBits === 2 ? '电费型预付费表' : '保留'
520
+ );
521
+ const word3 = (w3 === null) ? null : {
522
+ rawValue: w3,
523
+ binary: bin16(w3),
524
+ fields: {
525
+ 当前运行时段套数: (bit(w3, 0) ? '第二套' : '第一套'),
526
+ 供电方式: supplyMode,
527
+ 编程允许状态: (bit(w3, 3) ? '有效' : '失效'),
528
+ 继电器状态: (bit(w3, 4) ? '断' : '通'),
529
+ 当前运行时区套数: (bit(w3, 5) ? '第二套' : '第一套'),
530
+ 继电器命令状态: (bit(w3, 6) ? '断' : '通'),
531
+ 预跳闸报警状态: (bit(w3, 7) ? '有' : '无'),
532
+ 电能表类型: meterType,
533
+ 当前运行分时费率套数: (bit(w3, 10) ? '第二套' : '第一套'),
534
+ 当前阶梯套数: (bit(w3, 11) ? '第二套' : '第一套'),
535
+ 保电状态: (bit(w3, 12) ? '保电' : '非保电')
536
+ }
537
+ };
538
+
539
+ // —— 密钥状态字(与你现有的 04000508 保持一致,32位)——
540
+ const w8 = readU32LE();
541
+ const word8 = (w8 === null) ? null : {
542
+ rawValue: w8,
543
+ hexValue: w8.toString(16).toUpperCase().padStart(8, '0'),
544
+ binary: bin32(w8),
545
+ keys: {
546
+ '主控密钥有效': !!bit(w8, 0),
547
+ '身份认证密钥有效': !!bit(w8, 1),
548
+ '密钥协商密钥有效': !!bit(w8, 2),
549
+ '密钥更新密钥有效': !!bit(w8, 3),
550
+ '传输密钥有效': !!bit(w8, 4),
551
+ '保护密钥有效': !!bit(w8, 5),
552
+ '广播认证密钥有效': !!bit(w8, 6),
553
+ '主控密钥不可恢复': !!bit(w8, 7)
554
+ }
555
+ };
556
+
557
+ const out = { word1, word2, word3, word8 };
558
+ if (left() > 0) {
559
+ out.extraRaw = bytesToHex(data.slice(off)).replace(/\s+/g, ''); // 厂商私有扩展原样保留
560
+ }
561
+ value = out;
562
+ }
563
+
564
+ else if (di === '04000501' && arrPush.length >= 6) {
565
+ // arrPush = DATA-0x33 后的数组
566
+ const lo = arrPush[4] & 0xFF; // 低字节
567
+ const hi = arrPush[5] & 0xFF; // 高字节
568
+ const v = (hi << 8) | lo; // 16 位状态字,bit0 为最低位
569
+ const bin = v.toString(2).padStart(16, '0');
570
+
571
+ value = {
572
+ rawValue: v, binary: bin,
573
+ keys: {
574
+ '停电抄表电池欠压': !!(v & (1 << 3)), // bit3: 0=正常, 1=欠压
575
+ '时钟电池欠压': !!(v & (1 << 2)), // bit2: 0=正常, 1=欠压
576
+ '有功功率方向反向': !!(v & (1 << 4)), // bit4: 0=正向, 1=反向
577
+ '无功功率方向反向': !!(v & (1 << 5)), // bit5: 0=正向, 1=反向
578
+ '控制回路错误': !!(v & (1 << 8)), // bit8: 0=正常, 1=错误
579
+ 'ESAM错误': !!(v & (1 << 9)), // bit9: 0=正常, 1=错误
580
+ '内部程序错误': !!(v & (1 << 12)), // bit12: 0=正常, 1=错误
581
+ '存储器故障或损坏': !!(v & (1 << 13)), // bit13: 0=正常, 1=故障
582
+ '透支状态': !!(v & (1 << 14)), // bit14: 0=正常, 1=透支
583
+ '时钟故障': !!(v & (1 << 15)) // bit15: 0=正常, 1=故障
584
+ }
585
+ };
586
+ } else if (di === '04000502' && arrPush.length >= 6) {
587
+ // 运行状态字2(方向类):A/B/C 相有功与无功功率方向(0=正向,1=反向)
588
+ const lo = arrPush[4] & 0xFF;
589
+ const hi = arrPush[5] & 0xFF;
590
+ const v = (hi << 8) | lo;
591
+ const bin = v.toString(2).padStart(16, '0');
592
+
593
+ const bit = (n) => ((v >> n) & 0x1);
594
+ const dir = (b) => (b ? '反向' : '正向');
595
+
596
+ value = {
597
+ rawValue: v,
598
+ binary: bin,
599
+ fields: {
600
+ 'A相有功功率方向': dir(bit(0)),
601
+ 'B相有功功率方向': dir(bit(1)),
602
+ 'C相有功功率方向': dir(bit(2)),
603
+ 'A相无功功率方向': dir(bit(4)),
604
+ 'B相无功功率方向': dir(bit(5)),
605
+ 'C相无功功率方向': dir(bit(6))
606
+ }
607
+ };
608
+ } else if (di === '04000503' && arrPush.length >= 6) {
609
+ // 运行状态字3(操作类)
610
+ const lo = arrPush[4] & 0xFF;
611
+ const hi = arrPush[5] & 0xFF;
612
+ const v = (hi << 8) | lo;
613
+ const bin = v.toString(2).padStart(16, '0');
614
+
615
+ const supplyBits = (v >> 1) & 0b11; // bit2-bit1
616
+ const supplyMode = (
617
+ supplyBits === 0 ? '主电源' :
618
+ supplyBits === 1 ? '辅助电源' :
619
+ supplyBits === 2 ? '电池供电' : '保留'
620
+ );
621
+
622
+ const meterTypeBits = (v >> 8) & 0b11; // bit9-bit8
623
+ const meterType = (
624
+ meterTypeBits === 0 ? '非预付费表' :
625
+ meterTypeBits === 1 ? '电量型预付费表' :
626
+ meterTypeBits === 2 ? '电费型预付费表' : '保留'
627
+ );
628
+
629
+ value = {
630
+ rawValue: v,
631
+ binary: bin,
632
+ fields: {
633
+ 当前运行时段套数: (v & 0x1) ? '第二套' : '第一套', // bit0
634
+ 供电方式: supplyMode, // bit2-bit1
635
+ 编程允许状态: (v & (1 << 3)) ? '有效' : '失效', // bit3
636
+ 继电器状态: (v & (1 << 4)) ? '断' : '通', // bit4(线路实际工作状态)
637
+ 当前运行时区套数: (v & (1 << 5)) ? '第二套' : '第一套', // bit5
638
+ 继电器命令状态: (v & (1 << 6)) ? '断' : '通', // bit6(远程拉闸命令)
639
+ 预跳闸报警状态: (v & (1 << 7)) ? '有' : '无', // bit7
640
+ 电能表类型: meterType, // bit9-bit8
641
+ 当前运行分时费率套数: (v & (1 << 10)) ? '第二套' : '第一套', // bit10
642
+ 当前阶梯套数: (v & (1 << 11)) ? '第二套' : '第一套', // bit11
643
+ 保电状态: (v & (1 << 12)) ? '保电' : '非保电' // bit12
644
+ }
645
+ };
646
+ }
647
+ // else if (['0000FF00', '0001FF00', '0002FF00'].includes(di) && arrPush.length > 4) {
648
+ // const data = arrPush.slice(4); const result = [];
649
+ // for (let i = 0; i < data.length; i += 4) { result.push(bytesToIntBE(data.slice(i, i + 4).reverse())); }
650
+ // value = result;
651
+ // }
652
+ else if (di === '04000508' && arrPush.length >= 4) {
653
+ const b4 = Buffer.from(arrPush.slice(-4).reverse());
654
+ const v = b4.readUInt32BE(0);
655
+ value = {
656
+ rawValue: v,
657
+ hexValue: v.toString(16).toUpperCase().padStart(8, '0'),
658
+ binary: v.toString(2).padStart(32, '0'),
659
+ keys: {
660
+ '主控密钥有效': !!(v & (1 << 0)), '身份认证密钥有效': !!(v & (1 << 1)), '密钥协商密钥有效': !!(v & (1 << 2)),
661
+ '密钥更新密钥有效': !!(v & (1 << 3)), '传输密钥有效': !!(v & (1 << 4)), '保护密钥有效': !!(v & (1 << 5)),
662
+ '广播认证密钥有效': !!(v & (1 << 6)), '主控密钥不可恢复': !!(v & (1 << 7))
663
+ }
664
+ };
665
+ } else if (di === '04000102' && arrPush.length >= 3) {
666
+ value = Buffer.from(arrPush.slice(-3).reverse()).toString('hex').toUpperCase(); // "HHMMSS"
667
+ } else if (di === '04000101' && arrPush.length >= 8) {
668
+ const s = '20' + Buffer.from(arrPush.slice(4).reverse()).toString('hex').toUpperCase();
669
+ value = s.slice(0, 8);
670
+ } else if (di === '03300101' && arrPush.length >= 10) {
671
+ value = '20' + Buffer.from(arrPush.slice(4, 10).reverse()).toString('hex').toUpperCase();
672
+ } else if (di === '03300D00' && arrPush.length >= 4) {
673
+ value = bytesToIntBE(arrPush.slice(4).reverse());
674
+ }
675
+ else if ((isDailyFreezeDI(di) || isSettlementFreezeDI(di) || isToatalDI(di)) && arrPush.length >= 8) {
676
+ // else if ((arrDays.includes(di) || arrDaysFX.includes(di) || arrDaysZX.includes(di) || arrDaysJSR.includes(di)) && arrPush.length >= 8) {
677
+ // const energyNum = bytesToIntBE(arrPush.slice(4, 8).reverse());
678
+ // value = Math.round(energyNum / 100 * 100) / 100;
679
+ // DATA-0x33 后:DI(4) + 5×(4B 小端 BCD) = 24B LEN → 与 698 的日冻结 record 风格一致
680
+ // DATA-0x33 后: DI(4) + N×(4B 小端BCD);常见为 5 组(总/尖/峰/平/谷),也有只回 1 组“总”的情况
681
+ const afterDI = arrPush.slice(4);
682
+ const labels = ['总', '尖', '峰', '平', '谷'];
683
+
684
+
685
+ // 0000FF00 / 0001FF00 / 0002FF00:乱码模式
686
+ const isTotalBlock = isToatalDI(di);
687
+
688
+ // 4B 小端 BCD → 十进制(2 小数位,单位 kWh)
689
+ function parseEnergyLE4(b4) {
690
+
691
+ // const val = bcdLEToInt(b4); // 例如 "00 63 24 02"(LE BCD)→ 解析为整数,再 /100
692
+ // return Math.round((val / 100) * 100) / 100;
693
+ //--fixed
694
+ if (isTotalBlock) {
695
+ // 乱码模式:4 字节先按大端转 hex 字符串,再插入小数点
696
+ const beBytes = Array.from(b4).reverse(); // 小端 => 大端
697
+ let hexStr = bytesToHex(beBytes).replace(/\s+/g, '').toLowerCase(); // 8 位 hex
698
+ hexStr = hexStr.padStart(8, '0'); // 防守一下
699
+ return hexStr.slice(0, 6) + '.' + hexStr.slice(6); // "142942.59"
700
+ } else {
701
+ // 原来日冻结/结算日逻辑:小端 BCD → 十进制,保留 2 位小数
702
+ const val = bcdLEToInt(b4);
703
+ return Math.round((val / 100) * 100) / 100;
704
+ }
705
+ }
706
+
707
+ const totalGroups = Math.floor(afterDI.length / 4); // 能解析的 4B 组数
708
+ const parseCount = Math.min(5, Math.max(1, totalGroups)); // 至少解析 1 组,最多 5 组
709
+ const items = [];
710
+
711
+ for (let i = 0; i < parseCount; i++) {
712
+ const seg = afterDI.slice(i * 4, i * 4 + 4);
713
+ items.push({
714
+ label: labels[i] || `项${i + 1}`,
715
+ rawBCD: bytesToHex(seg),
716
+ value: parseEnergyLE4(seg),
717
+ unit: 'kWh',
718
+ scale: -2
719
+ });
720
+ }
721
+
722
+ // 如设备回了超过 5 组(极少见)或多余字节,挂个 extra 方便排查,不影响主结果
723
+ const extraStart = parseCount * 4;
724
+ if (afterDI.length > extraStart) {
725
+ _msg.extraRaw = bytesToHex(afterDI.slice(extraStart)).replace(/\s+/g, '');
726
+ }
727
+
728
+ value = items.map(it => it.value); // 与你 698 解析保持一致:value 返回纯数值数组
729
+ _msg.payload = { data: items, value }; // 附带明细(含 rawBCD)
730
+ } else if (di === '04000402' && arrPush.length >= 8) {
731
+ value = Buffer.from(arrPush.slice(4).reverse()).toString('hex').toUpperCase();
732
+ } else if (arrPub.includes(di) && arrPush.length >= 8) {
733
+ const v = bytesToIntBE(arrPush.slice(4, 8).reverse());
734
+ value = Math.round(v / 100 * 100) / 100;
735
+ } else if (di === '070000FF' && arrPush.length >= 16) {
736
+ // 身份认证(数据标识 070000FF):随机数2(4B) + ESAM序列号(8B) + 可能的附加字段
737
+ const dataBytes = arrPush.slice(4);
738
+ const asBigEndianHex = (segment) => {
739
+ if (!segment || segment.length === 0) return '';
740
+ return Buffer.from(segment.slice().reverse()).toString('hex').toUpperCase();
741
+ };
742
+ const random2Bytes = dataBytes.slice(0, 4);
743
+ const esamBytes = dataBytes.slice(4, 12);
744
+ const extraBytes = dataBytes.slice(12);
745
+ value = {
746
+ type: 'identity_auth',
747
+ random2: asBigEndianHex(random2Bytes), // 与 C# 相同:按大端显示
748
+ esam: asBigEndianHex(esamBytes),
749
+ extraData: extraBytes.length ? Buffer.from(extraBytes).toString('hex').toUpperCase() : null,
750
+ rawMinus33: bytesToHex(arrPush).replace(/\s+/g, '')
751
+ };
752
+ } else if (di === '04000409' && arrPush.length >= 7) {
753
+ // 有功脉冲常数:DI(4) + N3(3字节BCD,小端)
754
+ const dataBytes = arrPush.slice(4, 7); // LE:低字节在前
755
+ const val = bcdLEToInt(dataBytes);
756
+ value = {
757
+ rawValue: val,
758
+ unit: 'imp/kWh',
759
+ description: `有功脉冲常数: ${val} imp/kWh`,
760
+ bcdData: bcdDigitsStrLE(dataBytes) // 例如 "000400" → "400"
761
+ };
762
+ } else if ((di === '02800008' || di === '02800009') && arrPush.length >= 6) {
763
+ // 02800008: 时钟电池电压,02800009: 停电抄表电池电压
764
+ // C# 解析为2字节,保留2位小数 → V
765
+ const n = bytesToIntBE(arrPush.slice(-2).reverse());
766
+ const v = n / 100.0;
767
+ value = {
768
+ rawValue: n,
769
+ voltage: v,
770
+ unit: 'V',
771
+ description: `${di === '02800008' ? '时钟电池电压' : '停电抄表电池电压'}: ${v.toFixed(2)}V`
772
+ };
773
+ } else if (di === '0400040A' && arrPush.length >= 7) {
774
+ // 无功脉冲常数:DI(4) + N3(3字节BCD,小端)
775
+ const dataBytes = arrPush.slice(4, 7); // LE:低字节在前
776
+ const val = bcdLEToInt(dataBytes);
777
+ value = {
778
+ rawValue: val,
779
+ unit: 'imp/kvarh',
780
+ description: `无功脉冲常数: ${val} imp/kvarh`,
781
+ bcdData: bcdDigitsStrLE(dataBytes)
782
+ };
783
+ } else if (di === '03300D01' && arrPush.length >= 16) {
784
+ value = parseCoverOpenLast645(arrPush);
785
+ } else {
786
+ // 未匹配的DI,返回去偏移(−0x33)后的原始数据,便于排查/厂商私有解析
787
+ value = {
788
+ rawMinus33: bytesToHex(arrPush).replace(/\s+/g, ''), // 含DI(4B) + 数据
789
+ di,
790
+ data: bytesToHex(arrPush.slice(4)).replace(/\s+/g, ''), // 纯数据部分(不含DI)
791
+ note: '未识别DI,已返回去0x33的原始数据'
792
+ };
793
+ }
794
+ } catch (e) {
795
+ return Object.assign(_msg, { ok: false, reason: 'decode_exception', exec_addr, di, ctrl, len, raw: put, err: String(e) });
796
+ }
797
+
798
+ return Object.assign(_msg, {
799
+ ok: true,
800
+ type: 'data_response',
801
+ exec_addr,
802
+ addr_bytes_hex,
803
+ ctrl, len, di, value,
804
+ success: cs_ok,
805
+ raw: put
806
+ });
807
+ }
808
+
809
+ /******************** 通用工具 ********************/
810
+ function calc645CSForBytes(frameNoCS, csMode) {
811
+ // frameNoCS = [68, A0..A5, 68, C, L, ...DATA]
812
+ if (csMode === 'full') {
813
+ // 从第一个 0x68 起(含)一直累加到 CS 前
814
+ return calc645Checksum(frameNoCS);
815
+ }
816
+ // 'std':从第二个 0x68 后开始(C+L+DATA)
817
+ let second68 = -1, seen = 0;
818
+ for (let i = 0; i < frameNoCS.length; i++) {
819
+ if (frameNoCS[i] === 0x68) { seen++; if (seen === 2) { second68 = i; break; } }
820
+ }
821
+ const part = frameNoCS.slice(second68 + 1);
822
+ return calc645Checksum(part);
823
+
824
+ function calc645Checksum(arr) {
825
+ let sum = 0;
826
+ for (let i = 0; i < arr.length; i++) sum = (sum + arr[i]) & 0xFF;
827
+ return sum & 0xFF;
828
+ }
829
+ }
830
+
831
+ function encryptOAD(oadStr) {
832
+ const clean = oadStr.replace(/\s+/g, '').toUpperCase();
833
+ if (clean.length !== 8) throw new Error("数据标识必须是4字节(8个十六进制字符)");
834
+ let bytes = [];
835
+ for (let i = 0; i < 8; i += 2) bytes.push(parseInt(clean.slice(i, i + 2), 16));
836
+ const entagd = bytes.reverse().map(b => (b + 0x33) & 0xFF);
837
+ return entagd.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
838
+ }
839
+
840
+ function decryptOAD(entagdStr) {
841
+ const clean = entagdStr.replace(/\s+/g, '').toUpperCase();
842
+ if (clean.length !== 8) throw new Error("加密标识必须是4字节(8个十六进制字符)");
843
+ let bytes = [];
844
+ for (let i = 0; i < 8; i += 2) bytes.push(parseInt(clean.slice(i, i + 2), 16));
845
+ const detagd = bytes.map(b => (b - 0x33 + 256) & 0xFF).reverse();
846
+ return detagd.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
847
+ }
848
+
849
+ function hexToBytes(hexStr) {
850
+ const clean = hexStr.replace(/\s+/g, '').toUpperCase();
851
+ if (!clean || clean.length % 2 !== 0) throw new Error('HEX 长度必须为偶数');
852
+ const out = [];
853
+ for (let i = 0; i < clean.length; i += 2) out.push(parseInt(clean.slice(i, i + 2), 16));
854
+ return out;
855
+ }
856
+ // function bytesToHex(bytes) {
857
+ // return bytes.map(b => b.toString(16).toUpperCase().padStart(2, '0')).join(' ');
858
+ // }
859
+
860
+ function bytesToHex(bytes) {
861
+ const a = Array.isArray(bytes) ? bytes : Array.from(bytes || []);
862
+ let out = '';
863
+ for (let i = 0; i < a.length; i++) {
864
+ const v = (a[i] | 0) & 0xFF; // 强制为 0..255
865
+ out += __HEX__[v]; // 查表确保两位HEX
866
+ if (i !== a.length - 1) out += ' ';
867
+ }
868
+ return out;
869
+ }
870
+ function hexStringToBuffer(hexStr) {
871
+ const clean = hexStr.replace(/\s+/g, '');
872
+ if (!clean || clean.length % 2 !== 0) return null;
873
+ return Buffer.from(clean, 'hex');
874
+ }
875
+ // 只剔除前导 FE…FE(最多4个)
876
+ function f_stripLeadingFE(hex) {
877
+ const s = hex.replace(/\s+/g, '').toUpperCase();
878
+ return s.replace(/^(FE){1,4}/, '');
879
+ }
880
+
881
+ // 【新增】BCD(小端) → 整数:bytes[0] 是最低两位(个/十),bytes[1] 是百/千 ...
882
+ function bcdLEToInt(bytes) {
883
+ let m = 1, val = 0;
884
+ for (let i = 0; i < bytes.length; i++) {
885
+ const b = bytes[i] & 0xFF;
886
+ const lo = b & 0x0F; // 个
887
+ const hi = (b >> 4) & 0x0F; // 十
888
+ if (lo > 9 || hi > 9) throw new Error('BCD 半字节越界');
889
+ val += lo * m; m *= 10;
890
+ val += hi * m; m *= 10;
891
+ }
892
+ return val;
893
+ }
894
+ // 【新增】把 BCD(小端) 转成"正常阅读顺序"的数字字符串(去前导 0)
895
+ function bcdDigitsStrLE(bytes) {
896
+ let s = '';
897
+ for (let i = bytes.length - 1; i >= 0; i--) {
898
+ const b = bytes[i] & 0xFF;
899
+ s += ((b >> 4) & 0x0F).toString();
900
+ s += (b & 0x0F).toString();
901
+ }
902
+ s = s.replace(/^0+/, '') || '0';
903
+ return s;
904
+ }
905
+
906
+
907
+ // === 开盖明细专用工具 ===
908
+
909
+ // 单字节 BCD → 十进制(0x25 -> 25)
910
+ function bcdByteToDec(b) {
911
+ const hi = (b >> 4) & 0x0F, lo = b & 0x0F;
912
+ if (hi > 9 || lo > 9) return NaN;
913
+ return hi * 10 + lo;
914
+ }
915
+
916
+ // 解析 6B/7B BCD 时间(顺序:秒 分 时 日 月 年 [周]),返回 {formatted,...}
917
+ function parseTimeBCD6or7(bytes) {
918
+ if (!(bytes && (bytes.length === 6 || bytes.length === 7))) return null;
919
+ const ss = bcdByteToDec(bytes[0]);
920
+ const mm = bcdByteToDec(bytes[1]);
921
+ const hh = bcdByteToDec(bytes[2]);
922
+ const DD = bcdByteToDec(bytes[3]);
923
+ const MM = bcdByteToDec(bytes[4]);
924
+ const YY = bcdByteToDec(bytes[5]);
925
+ if ([ss, mm, hh, DD, MM, YY].some(v => isNaN(v))) return null;
926
+ if (ss > 59 || mm > 59 || hh > 23 || DD < 1 || DD > 31 || MM < 1 || MM > 12) return null;
927
+ const YYYY = 2000 + YY;
928
+ const pad = n => String(n).padStart(2, '0');
929
+ return {
930
+ year: YYYY, month: pad(MM), day: pad(DD),
931
+ hour: pad(hh), minute: pad(mm), second: pad(ss),
932
+ formatted: `${YYYY}-${pad(MM)}-${pad(DD)} ${pad(hh)}:${pad(mm)}:${pad(ss)}`
933
+ };
934
+ }
935
+
936
+ // 解析“上一次开盖明细”(DI=03300D01):arrPush = DATA-0x33 后的数组
937
+ function parseCoverOpenLast645(arrPush) {
938
+ // 结构:DI(4) + 发生时刻(6B) + 结束时刻(6B) + 能量(4项,小端 BCD,长度可为4B/5B/6B)
939
+ const data = arrPush.slice(4); // 去掉 DI
940
+ if (data.length < 12) {
941
+ return { type: 'cover_open_record', ok: false, reason: 'data_too_short', rawData: bytesToHex(data).replace(/\s+/g, '') };
942
+ }
943
+
944
+ // 1) 时间(优先按 6B+6B)
945
+ const startTimeBytes = data.slice(0, 6);
946
+ const endTimeBytes = data.slice(6, 12);
947
+ const start = parseTimeBCD6or7(startTimeBytes);
948
+ const end = parseTimeBCD6or7(endTimeBytes);
949
+
950
+ // 2) 能量段起始偏移
951
+ let off = 12;
952
+
953
+ // 3) 能量长度自适应检测(4×4B / 4×5B / 4×6B)
954
+ function isAllBCDNibbles(bs) {
955
+ for (let i = 0; i < bs.length; i++) {
956
+ const b = bs[i] & 0xFF, hi = (b >> 4) & 0x0F, lo = b & 0x0F;
957
+ if (hi > 9 || lo > 9) return false;
958
+ }
959
+ return true;
960
+ }
961
+ function parseEnergiesFlexible(bytes) {
962
+ const labels = ['开盖前正向有功总', '开盖前反向有功总', '开盖后正向有功总', '开盖后反向有功总'];
963
+
964
+ // 尝试 4B×4(scale=2)
965
+ if (bytes.length >= 16 && isAllBCDNibbles(bytes.slice(0, 16))) {
966
+ const out = [];
967
+ for (let i = 0; i < 4; i++) {
968
+ const seg = bytes.slice(i * 4, i * 4 + 4);
969
+ const val = bcdLEToInt(seg);
970
+ out.push({ label: labels[i], rawValue: val, kwh: val / 100, unit: 'kWh', bcdData: bcdDigitsStrLE(seg), bytes: bytesToHex(seg) });
971
+ }
972
+ return { list: out, used: 16, scale: 2 };
973
+ }
974
+ // 尝试 5B×4(scale=3)
975
+ if (bytes.length >= 20 && isAllBCDNibbles(bytes.slice(0, 20))) {
976
+ const out = [];
977
+ for (let i = 0; i < 4; i++) {
978
+ const seg = bytes.slice(i * 5, i * 5 + 5);
979
+ const val = bcdLEToInt(seg);
980
+ out.push({ label: labels[i], rawValue: val, kwh: val / 1000, unit: 'kWh', bcdData: bcdDigitsStrLE(seg), bytes: bytesToHex(seg) });
981
+ }
982
+ return { list: out, used: 20, scale: 3 };
983
+ }
984
+ // 尝试 6B×4(scale=4)
985
+ if (bytes.length >= 24 && isAllBCDNibbles(bytes.slice(0, 24))) {
986
+ const out = [];
987
+ for (let i = 0; i < 4; i++) {
988
+ const seg = bytes.slice(i * 6, i * 6 + 6);
989
+ const val = bcdLEToInt(seg);
990
+ out.push({ label: labels[i], rawValue: val, kwh: val / 10000, unit: 'kWh', bcdData: bcdDigitsStrLE(seg), bytes: bytesToHex(seg) });
991
+ }
992
+ return { list: out, used: 24, scale: 4 };
993
+ }
994
+ return { list: [], used: 0, scale: null };
995
+ }
996
+
997
+ const energiesBytes = data.slice(off);
998
+ const parsed = parseEnergiesFlexible(energiesBytes);
999
+
1000
+ return {
1001
+ type: 'cover_open_record',
1002
+ ok: true,
1003
+ di: '03300D01',
1004
+ startTime: start ? start.formatted : null,
1005
+ endTime: end ? end.formatted : null,
1006
+ startTimeDetail: start,
1007
+ endTimeDetail: end,
1008
+ energies: parsed.list, // 按顺序:前正、前反、后正、后反
1009
+ scale: parsed.scale, // 小数位(2/3/4)
1010
+ rawData: bytesToHex(data).replace(/\s+/g, ''),
1011
+ note: '按 6B时间 + 4×小端 BCD 能量 自适应解析'
1012
+ };
1013
+ }
1014
+
1015
+
1016
+
1017
+
1018
+
1019
+ function batchMsg(msg) {
1020
+ /******************** 入口:根据 msg.mode / msg.action 选择 encode / decode ********************/
1021
+ const MODE = String(msg.mode || msg.action || 'decode').toLowerCase();
1022
+
1023
+ // 可选:地址是否在“编码”时倒序(有些 645 实装要求);解码固定按帧内小端规则处理
1024
+ const ADDR_REVERSE = !!msg.addr_reverse;
1025
+
1026
+ // 可选:编码时 CS 模式:'std'(从第二个68后 C+L+DATA) | 'full'(从第一个68起至 CS 前)
1027
+ // 解码侧会双模自动通过,无需设置
1028
+ const CS_MODE = (msg.cs_mode === 'full' ? 'full' : 'std');
1029
+
1030
+ if (MODE === 'encode') {
1031
+ const isArr = Array.isArray(msg.payload);
1032
+ const arrCmds = isArr ? msg.payload : [msg.payload];
1033
+ const frames = arrCmds
1034
+ .filter(i => i && (i.payload?.oad || i.oad))
1035
+ .map(i => build645Frame(i, ADDR_REVERSE, (i.cs_mode || CS_MODE)));
1036
+
1037
+ msg.payload = isArr ? frames : frames[0];
1038
+ return msg;
1039
+ }
1040
+
1041
+ /******************** decode:解码 645 响应(统一返回对象,含 exec_addr 等) ********************/
1042
+ const dataIn = msg.payload;
1043
+
1044
+
1045
+ if (Array.isArray(dataIn)) {
1046
+ msg.payload = dataIn.map(_msg => batchMsg(_msg));
1047
+ } else {
1048
+ msg.payload = batchMsg(msg);
1049
+ }
1050
+ return msg;
1051
+ }
1052
+
1053
+
1054
+ module.exports = batchMsg;
1055
+ module.exports.batchMsg645 = batchMsg;