node-red-zelecproto 0.0.7 → 0.0.9
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 +88 -30
- package/AGENTS.md +133 -0
- package/package.json +1 -1
- package/test.js +33 -6
package/645.js
CHANGED
|
@@ -28,6 +28,10 @@ function build645Frame(i, reverseAddr, csMode) {
|
|
|
28
28
|
return buildEncrypted645Frame(i, addrBytes, reverseAddr, csMode);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
if (oad === '04040300') {
|
|
32
|
+
return buildSwitchScreen645Frame(i, addrBytes, csMode);
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
|
|
32
36
|
// OAD 加密:倒序 + 每字节 +0x33
|
|
33
37
|
const cmdEx = encryptOAD(oad); // "xx xx xx xx"
|
|
@@ -50,6 +54,38 @@ function build645Frame(i, reverseAddr, csMode) {
|
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
function buildSwitchScreen645Frame(i, addrBytes, csMode) {
|
|
58
|
+
const payload = i.payload || {};
|
|
59
|
+
const screenRaw = payload.screenContent || payload.switchScreenContent || payload.switchScreen || payload.itemContent || payload.dataHex;
|
|
60
|
+
let screenHex = String(screenRaw || '').replace(/\s+/g, '').toUpperCase();
|
|
61
|
+
if (!screenHex) throw new Error('04040300 屏显切换需要提供 screenContent/itemContent');
|
|
62
|
+
if (screenHex.length === 8) screenHex += '00';
|
|
63
|
+
if (!/^[0-9A-F]{10}$/.test(screenHex)) throw new Error('04040300 切屏内容必须是 10 位 HEX,例如 0001000000');
|
|
64
|
+
|
|
65
|
+
const screenBody = screenHex.slice(0, 8);
|
|
66
|
+
const optionByte = screenHex.slice(8, 10);
|
|
67
|
+
const oadBytes = hexToBytes(encryptOAD('04040300').replace(/\s+/g, ''));
|
|
68
|
+
const dataBytes = [
|
|
69
|
+
...oadBytes,
|
|
70
|
+
...hexToBytes(reverseHexBytes(screenBody)).map(b => (b + 0x33) & 0xFF),
|
|
71
|
+
...hexToBytes(optionByte).map(b => (b + 0x33) & 0xFF)
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const C = 0x11;
|
|
75
|
+
const L = dataBytes.length;
|
|
76
|
+
const frameNoCS = [0x68, ...addrBytes, 0x68, C, L, ...dataBytes];
|
|
77
|
+
const cs = calc645CSForBytes(frameNoCS, csMode);
|
|
78
|
+
const csHex = cs.toString(16).toUpperCase().padStart(2, '0');
|
|
79
|
+
const fullHex = (`FE FE FE FE ${bytesToHex(frameNoCS)} ${csHex} 16`).replace(/\s+/g, '');
|
|
80
|
+
let _payload = i.payload || {};
|
|
81
|
+
return Object.assign(i, _payload, {
|
|
82
|
+
com_exec_addr: bytesToHex(addrBytes).replace(/\s+/g, ''),
|
|
83
|
+
cmdEx: bytesToHex(oadBytes).replace(/\s+/g, ''),
|
|
84
|
+
switchScreenContent: screenHex,
|
|
85
|
+
payload: fullHex
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
53
89
|
/**
|
|
54
90
|
* 读取地址
|
|
55
91
|
*/
|
|
@@ -220,7 +256,16 @@ function reverseHexBytes(hex) {
|
|
|
220
256
|
|
|
221
257
|
/******************** 解码:batchMsg(返回统一对象,含 exec_addr/di/value 等) ********************/
|
|
222
258
|
function decode645(_msg) {
|
|
223
|
-
|
|
259
|
+
// 创建一个全新的对象,不依赖于传入的 _msg 对象
|
|
260
|
+
const result = {};
|
|
261
|
+
|
|
262
|
+
// 复制原始消息的非 payload 属性
|
|
263
|
+
for (const key in _msg) {
|
|
264
|
+
if (_msg.hasOwnProperty(key) && key !== 'payload') {
|
|
265
|
+
result[key] = _msg[key];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
224
269
|
// ===== 工具 =====
|
|
225
270
|
function calc645Checksum(u8) {
|
|
226
271
|
let s = 0;
|
|
@@ -238,23 +283,23 @@ function decode645(_msg) {
|
|
|
238
283
|
// ===== 取入参并清洗 =====
|
|
239
284
|
// 允许传 {put:'...'} 或 {payload:'...'},优先 put
|
|
240
285
|
let put = ((_msg.payload && _msg.payload.put) || _msg.put || _msg.payload || '').toString();
|
|
241
|
-
if (!put) return { ok: false, reason: 'empty' };
|
|
286
|
+
if (!put) return Object.assign(result, { ok: false, reason: 'empty' });
|
|
242
287
|
|
|
243
288
|
// 去前导 FE…FE(最多4个),并去空格
|
|
244
289
|
put = f_stripLeadingFE(put.replace(/\s+/g, '')).toUpperCase();
|
|
245
290
|
|
|
246
291
|
const buf = hexStringToBuffer(put);
|
|
247
|
-
if (!buf || buf.length < 12) return { ok: false, reason: 'too_short', raw: put };
|
|
292
|
+
if (!buf || buf.length < 12) return Object.assign(result, { ok: false, reason: 'too_short', raw: put });
|
|
248
293
|
|
|
249
294
|
// ===== 基本结构:68 AA(6) 68 CTRL LEN DATA(n) CS 16 =====
|
|
250
295
|
// 容错:定位到第一个 0x68
|
|
251
296
|
let p0 = 0;
|
|
252
297
|
while (p0 < buf.length && buf[p0] !== 0x68) p0++;
|
|
253
|
-
if (p0 + 12 > buf.length) return { ok: false, reason: 'no_head68', raw: put };
|
|
298
|
+
if (p0 + 12 > buf.length) return Object.assign(result, { ok: false, reason: 'no_head68', raw: put });
|
|
254
299
|
|
|
255
300
|
// 第二个 0x68 在地址后
|
|
256
301
|
const p68_2 = p0 + 7;
|
|
257
|
-
if (buf[p0] !== 0x68 || buf[p68_2] !== 0x68) return { ok: false, reason: 'bad_68_pair', raw: put };
|
|
302
|
+
if (buf[p0] !== 0x68 || buf[p68_2] !== 0x68) return Object.assign(result, { ok: false, reason: 'bad_68_pair', raw: put });
|
|
258
303
|
|
|
259
304
|
// 读出地址区(A0..A5)
|
|
260
305
|
const addrBytes = buf.subarray(p0 + 1, p0 + 7);
|
|
@@ -269,8 +314,8 @@ function decode645(_msg) {
|
|
|
269
314
|
const pCS = pDataEnd;
|
|
270
315
|
const pEnd = pDataEnd + 1;
|
|
271
316
|
|
|
272
|
-
if (len < 0 || pEnd >= buf.length) return { ok: false, reason: 'len_oob', exec_addr, raw: put };
|
|
273
|
-
if (buf[pEnd] !== 0x16) return { ok: false, reason: 'no_end16', exec_addr, raw: put };
|
|
317
|
+
if (len < 0 || pEnd >= buf.length) return Object.assign(result, { ok: false, reason: 'len_oob', exec_addr, raw: put });
|
|
318
|
+
if (buf[pEnd] !== 0x16) return Object.assign(result, { ok: false, reason: 'no_end16', exec_addr, raw: put });
|
|
274
319
|
|
|
275
320
|
// —— CS 双模校验:std(第二个68后)与 full(第一个68起)任一通过即可 ——
|
|
276
321
|
const cs_frame = buf[pCS];
|
|
@@ -278,19 +323,19 @@ function decode645(_msg) {
|
|
|
278
323
|
const cs_full = calc645Checksum(buf.subarray(p0, pCS));
|
|
279
324
|
const cs_ok = (cs_std === cs_frame) || (cs_full === cs_frame);
|
|
280
325
|
if (!cs_ok) {
|
|
281
|
-
return {
|
|
326
|
+
return Object.assign(result, {
|
|
282
327
|
success: false, reason: 'cs_fail',
|
|
283
328
|
cs_frame: cs_frame.toString(16).toUpperCase().padStart(2, '0'),
|
|
284
329
|
cs_std: cs_std.toString(16).toUpperCase().padStart(2, '0'),
|
|
285
330
|
cs_full: cs_full.toString(16).toUpperCase().padStart(2, '0'),
|
|
286
331
|
exec_addr, raw: put
|
|
287
|
-
};
|
|
332
|
+
});
|
|
288
333
|
}
|
|
289
334
|
|
|
290
335
|
const dataRaw = Array.from(buf.subarray(pDataStart, pDataEnd));
|
|
291
336
|
// ===== 控制确认/错误帧 =====
|
|
292
337
|
if (len === 0 && ctrl === 0x9C) {
|
|
293
|
-
return {
|
|
338
|
+
return Object.assign(result, {
|
|
294
339
|
ok: true,
|
|
295
340
|
type: "control_ack",
|
|
296
341
|
exec_addr,
|
|
@@ -300,12 +345,12 @@ function decode645(_msg) {
|
|
|
300
345
|
description: "控制命令执行成功 (0x9C)",
|
|
301
346
|
success: cs_ok,
|
|
302
347
|
raw: put
|
|
303
|
-
};
|
|
348
|
+
});
|
|
304
349
|
}
|
|
305
350
|
|
|
306
351
|
if (ctrl === 0xDC) {
|
|
307
352
|
const detail = minus33(dataRaw);
|
|
308
|
-
return {
|
|
353
|
+
return Object.assign(result, {
|
|
309
354
|
ok: true,
|
|
310
355
|
type: "control_ack_ext",
|
|
311
356
|
exec_addr,
|
|
@@ -315,12 +360,12 @@ function decode645(_msg) {
|
|
|
315
360
|
statusBytes: bytesToHex(detail).replace(/\s+/g, ""),
|
|
316
361
|
success: true,
|
|
317
362
|
raw: put
|
|
318
|
-
};
|
|
363
|
+
});
|
|
319
364
|
}
|
|
320
365
|
|
|
321
366
|
if (ctrl === 0xDA) {
|
|
322
367
|
const detail = minus33(dataRaw);
|
|
323
|
-
return {
|
|
368
|
+
return Object.assign(result, {
|
|
324
369
|
ok: false,
|
|
325
370
|
type: "control_error",
|
|
326
371
|
exec_addr,
|
|
@@ -331,12 +376,12 @@ function decode645(_msg) {
|
|
|
331
376
|
detail: detail.length ? detail[0].toString(16).toUpperCase().padStart(2, "0") : null,
|
|
332
377
|
data: bytesToHex(detail).replace(/\s+/g, ""),
|
|
333
378
|
raw: put
|
|
334
|
-
};
|
|
379
|
+
});
|
|
335
380
|
}
|
|
336
381
|
|
|
337
382
|
if (ctrl === 0xD1) {
|
|
338
383
|
const errRaw = minus33(dataRaw);
|
|
339
|
-
return {
|
|
384
|
+
return Object.assign(result, {
|
|
340
385
|
ok: false,
|
|
341
386
|
type: "control_error",
|
|
342
387
|
exec_addr,
|
|
@@ -347,13 +392,13 @@ function decode645(_msg) {
|
|
|
347
392
|
detail: errRaw.length ? errRaw[0].toString(16).toUpperCase().padStart(2, "0") : null,
|
|
348
393
|
data: bytesToHex(errRaw).replace(/\s+/g, ""),
|
|
349
394
|
raw: put
|
|
350
|
-
};
|
|
395
|
+
});
|
|
351
396
|
}
|
|
352
397
|
// ===== 读地址响应:CTRL=0x93 且 LEN=0x06 =====
|
|
353
398
|
if (ctrl === 0x93 && len === 0x06) {
|
|
354
399
|
const addrMinus33 = minus33(dataRaw);
|
|
355
400
|
const addr_from_data = bcdAddr12FromLE(Uint8Array.from(addrMinus33));
|
|
356
|
-
return {
|
|
401
|
+
return Object.assign(result, {
|
|
357
402
|
ok: true,
|
|
358
403
|
type: 'address_response',
|
|
359
404
|
exec_addr, // 帧头解析出的地址
|
|
@@ -363,7 +408,7 @@ function decode645(_msg) {
|
|
|
363
408
|
addr_bytes_hex,
|
|
364
409
|
success: cs_ok,
|
|
365
410
|
raw: put
|
|
366
|
-
};
|
|
411
|
+
});
|
|
367
412
|
}
|
|
368
413
|
|
|
369
414
|
// ===== 普通数据响应:DATA-0x33,首4字节(倒序)为 DI =====
|
|
@@ -371,7 +416,7 @@ function decode645(_msg) {
|
|
|
371
416
|
let di = '';
|
|
372
417
|
if (arrPush.length >= 4) di = Buffer.from(arrPush.slice(0, 4).reverse()).toString('hex').toUpperCase();
|
|
373
418
|
|
|
374
|
-
// 动态判定是否为
|
|
419
|
+
// 动态判定是否为 "日冻结/结算日冻结" 的 DI(不再限定 30 天,可到 62 等任意 1B 日号)
|
|
375
420
|
function isDailyFreezeDI(di) {
|
|
376
421
|
// 645 常见日冻结:050601dd(正向)、050602dd(反向);dd 为 1B 日号(01~3E/3F/…,扩展都能兜住)
|
|
377
422
|
return /^05060[12][0-9A-F]{2}$/i.test(di || '');
|
|
@@ -383,7 +428,7 @@ function decode645(_msg) {
|
|
|
383
428
|
|
|
384
429
|
|
|
385
430
|
//判定为组合电量
|
|
386
|
-
function
|
|
431
|
+
function isTootalDI(di) {
|
|
387
432
|
let arr = ['0000FF00', '0001FF00', '0002FF00']
|
|
388
433
|
return arr.includes(di)
|
|
389
434
|
}
|
|
@@ -403,7 +448,7 @@ function decode645(_msg) {
|
|
|
403
448
|
|
|
404
449
|
let value = '';
|
|
405
450
|
try {
|
|
406
|
-
// // ——
|
|
451
|
+
// // —— 读数据"请求帧":CTRL=0x11 且 LEN=0x04,仅含 DI,无数值 ——
|
|
407
452
|
// if (ctrl === 0x11 && len === 0x04 && arrPush.length === 4) {
|
|
408
453
|
// return Object.assign(_msg, {
|
|
409
454
|
// ok: true,
|
|
@@ -663,6 +708,20 @@ function decode645(_msg) {
|
|
|
663
708
|
'广播认证密钥有效': !!(v & (1 << 6)), '主控密钥不可恢复': !!(v & (1 << 7))
|
|
664
709
|
}
|
|
665
710
|
};
|
|
711
|
+
} else if (di === '04040300' && arrPush.length >= 5) {
|
|
712
|
+
const body = arrPush.slice(4);
|
|
713
|
+
const option = body.length > 0 ? body[body.length - 1] : null;
|
|
714
|
+
const screenContent = body.length > 1
|
|
715
|
+
? Buffer.from(body.slice(0, -1).reverse()).toString('hex').toUpperCase()
|
|
716
|
+
: '';
|
|
717
|
+
value = {
|
|
718
|
+
type: 'switch_screen',
|
|
719
|
+
description: '屏显切换',
|
|
720
|
+
screenContent,
|
|
721
|
+
option: option == null ? null : option.toString(16).toUpperCase().padStart(2, '0'),
|
|
722
|
+
itemContent: screenContent + (option == null ? '' : option.toString(16).toUpperCase().padStart(2, '0')),
|
|
723
|
+
rawMinus33: bytesToHex(arrPush).replace(/\s+/g, '')
|
|
724
|
+
};
|
|
666
725
|
} else if (di === '04000102' && arrPush.length >= 3) {
|
|
667
726
|
value = Buffer.from(arrPush.slice(-3).reverse()).toString('hex').toUpperCase(); // "HHMMSS"
|
|
668
727
|
} else if (di === '04000101' && arrPush.length >= 8) {
|
|
@@ -673,19 +732,18 @@ function decode645(_msg) {
|
|
|
673
732
|
} else if (di === '03300D00' && arrPush.length >= 4) {
|
|
674
733
|
value = bytesToIntBE(arrPush.slice(4).reverse());
|
|
675
734
|
}
|
|
676
|
-
else if ((isDailyFreezeDI(di) || isSettlementFreezeDI(di) ||
|
|
735
|
+
else if ((isDailyFreezeDI(di) || isSettlementFreezeDI(di) || isTootalDI(di)) && arrPush.length >= 8) {
|
|
677
736
|
// else if ((arrDays.includes(di) || arrDaysFX.includes(di) || arrDaysZX.includes(di) || arrDaysJSR.includes(di)) && arrPush.length >= 8) {
|
|
678
737
|
// const energyNum = bytesToIntBE(arrPush.slice(4, 8).reverse());
|
|
679
738
|
// value = Math.round(energyNum / 100 * 100) / 100;
|
|
680
739
|
// DATA-0x33 后:DI(4) + 5×(4B 小端 BCD) = 24B LEN → 与 698 的日冻结 record 风格一致
|
|
681
|
-
// DATA-0x33 后: DI(4) + N×(4B 小端BCD);常见为 5 组(总/尖/峰/平/谷),也有只回 1
|
|
682
|
-
|
|
740
|
+
// DATA-0x33 后: DI(4) + N×(4B 小端BCD);常见为 5 组(总/尖/峰/平/谷),也有只回 1 组"总"的情况
|
|
683
741
|
const afterDI = arrPush.slice(4);
|
|
684
742
|
const labels = ['总', '尖', '峰', '平', '谷'];
|
|
685
743
|
|
|
686
744
|
|
|
687
745
|
// 0000FF00 / 0001FF00 / 0002FF00:乱码模式
|
|
688
|
-
const isTotalBlock =
|
|
746
|
+
const isTotalBlock = isTootalDI(di);
|
|
689
747
|
|
|
690
748
|
// 4B 小端 BCD → 十进制(2 小数位,单位 kWh)
|
|
691
749
|
function parseEnergyLE4(b4) {
|
|
@@ -724,11 +782,11 @@ function decode645(_msg) {
|
|
|
724
782
|
// 如设备回了超过 5 组(极少见)或多余字节,挂个 extra 方便排查,不影响主结果
|
|
725
783
|
const extraStart = parseCount * 4;
|
|
726
784
|
if (afterDI.length > extraStart) {
|
|
727
|
-
|
|
785
|
+
result.extraRaw = bytesToHex(afterDI.slice(extraStart)).replace(/\s+/g, '');
|
|
728
786
|
}
|
|
729
787
|
|
|
730
788
|
value = items.map(it => it.value); // 与你 698 解析保持一致:value 返回纯数值数组
|
|
731
|
-
|
|
789
|
+
result.payload = { data: items, value }; // 附带明细(含 rawBCD)
|
|
732
790
|
} else if (di === '04000402' && arrPush.length >= 8) {
|
|
733
791
|
value = Buffer.from(arrPush.slice(4).reverse()).toString('hex').toUpperCase();
|
|
734
792
|
} else if (arrPub.includes(di) && arrPush.length >= 8) {
|
|
@@ -794,10 +852,10 @@ function decode645(_msg) {
|
|
|
794
852
|
};
|
|
795
853
|
}
|
|
796
854
|
} catch (e) {
|
|
797
|
-
return { ok: false, reason: 'decode_exception', exec_addr, di, ctrl, len, raw: put, err: String(e) };
|
|
855
|
+
return Object.assign(result, { ok: false, reason: 'decode_exception', exec_addr, di, ctrl, len, raw: put, err: String(e) });
|
|
798
856
|
}
|
|
799
857
|
|
|
800
|
-
return Object.assign(
|
|
858
|
+
return Object.assign(result, {
|
|
801
859
|
ok: true,
|
|
802
860
|
type: 'data_response',
|
|
803
861
|
exec_addr,
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file contains guidelines and commands for agentic coding agents working in the node-red-zelecproto repository.
|
|
4
|
+
|
|
5
|
+
## Global Settings
|
|
6
|
+
- **Proxy**: Execute `proxy` command before running any network-related commands to enable proxy connection
|
|
7
|
+
|
|
8
|
+
## Build/Test/Lint Commands
|
|
9
|
+
|
|
10
|
+
### Current Commands
|
|
11
|
+
- `npm test` - Currently shows "Error: no test specified" (placeholder)
|
|
12
|
+
- No build, lint, or typecheck commands are currently configured
|
|
13
|
+
|
|
14
|
+
### Testing
|
|
15
|
+
- Use `node test.js` to run the manual test file
|
|
16
|
+
- Test file located at: `test.js`
|
|
17
|
+
- For single test execution, run `node -e "require('./test.js')"`
|
|
18
|
+
|
|
19
|
+
### Development
|
|
20
|
+
- No build process required (Node-RED nodes load directly)
|
|
21
|
+
- Manual testing via Node-RED flow editor recommended
|
|
22
|
+
|
|
23
|
+
## Code Style Guidelines
|
|
24
|
+
|
|
25
|
+
### File Structure and Naming
|
|
26
|
+
- Main node files: `zelecproto.js`, `zbatchproto.js`, `zeleble.js`
|
|
27
|
+
- Protocol implementations: `645.js`, `698.js`, `ble.js`
|
|
28
|
+
- HTML definitions: `zelecproto.html`, `zbatchproto.html`, `zeleble.html`
|
|
29
|
+
- Icons in: `icons/` directory (SVG format)
|
|
30
|
+
- Use kebab-case for file names
|
|
31
|
+
|
|
32
|
+
### Module Pattern
|
|
33
|
+
All Node-RED nodes must follow this pattern:
|
|
34
|
+
```javascript
|
|
35
|
+
module.exports = function (RED) {
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
// Require dependencies
|
|
39
|
+
var proto645 = require("./645");
|
|
40
|
+
|
|
41
|
+
function NodeName(n) {
|
|
42
|
+
RED.nodes.createNode(this, n);
|
|
43
|
+
var node = this;
|
|
44
|
+
|
|
45
|
+
this.on("input", function (msg, send, done) {
|
|
46
|
+
// Process message
|
|
47
|
+
send(msg);
|
|
48
|
+
done();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.on('close', () => {
|
|
52
|
+
// Cleanup if needed
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
RED.nodes.registerType("nodename", NodeName);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Import/Require Style
|
|
61
|
+
- Use `var` for requires (consistent with existing codebase)
|
|
62
|
+
- Local dependencies first: `var proto645 = require("./645");`
|
|
63
|
+
- Group requires at top of module function
|
|
64
|
+
- Use relative paths for local files
|
|
65
|
+
|
|
66
|
+
### Code Formatting
|
|
67
|
+
- Use strict mode: `"use strict";` at top of module function
|
|
68
|
+
- Indentation: 4 spaces (consistent with existing files)
|
|
69
|
+
- Semicolons required
|
|
70
|
+
- String quotes: single quotes preferred
|
|
71
|
+
- Object property access: dot notation for known properties, bracket notation for dynamic
|
|
72
|
+
|
|
73
|
+
### Naming Conventions
|
|
74
|
+
- Node functions: PascalCase (e.g., `zelecproto`, `zbatchproto`)
|
|
75
|
+
- Variables: camelCase (e.g., `msg`, `node`, `addrBytes`)
|
|
76
|
+
- Constants: UPPER_SNAKE_CASE (e.g., `START`, `END`, `OAD`)
|
|
77
|
+
- File names: kebab-case (e.g., `zelecproto.js`, `645.js`)
|
|
78
|
+
|
|
79
|
+
### Error Handling
|
|
80
|
+
- Use `throw new Error()` for validation errors
|
|
81
|
+
- Include descriptive error messages
|
|
82
|
+
- Validate input parameters before processing
|
|
83
|
+
- Example: `if (addrRaw.length !== 12) throw new Error('com_exec_addr 必须是 6 字节(12个HEX字符)');`
|
|
84
|
+
|
|
85
|
+
### Message Processing
|
|
86
|
+
- Always use the three-parameter input handler: `function (msg, send, done)`
|
|
87
|
+
- Call `send(msg)` before `done()`
|
|
88
|
+
- Clean up temporary properties: `delete msg._proto`
|
|
89
|
+
- Preserve original message structure when possible
|
|
90
|
+
|
|
91
|
+
### Protocol Implementation
|
|
92
|
+
- 645 Protocol: Focus on DL/T 645 frame building/parsing
|
|
93
|
+
- 698 Protocol: DL/T 698.45 encode/decode with CRC-16/X-25
|
|
94
|
+
- BLE Protocol: 国网多芯物联表蓝牙通信协议
|
|
95
|
+
- Each protocol file should export a function that takes `msg` and returns modified `msg`
|
|
96
|
+
|
|
97
|
+
### HTML Node Definitions
|
|
98
|
+
- Use data-template-name attribute matching node type
|
|
99
|
+
- Include name field in all nodes
|
|
100
|
+
- Set appropriate category, color, and icon
|
|
101
|
+
- Category: 'zutils' for all nodes
|
|
102
|
+
- Icons: reference SVG files in icons/ directory
|
|
103
|
+
|
|
104
|
+
### Comments and Documentation
|
|
105
|
+
- Use Chinese comments for protocol-specific explanations (consistent with existing code)
|
|
106
|
+
- Use JSDoc-style comments for functions
|
|
107
|
+
- Include protocol references and frame format descriptions
|
|
108
|
+
- Example: `// 国网多芯物联表《蓝牙通信及脉冲检定说明手册(0224)》协议`
|
|
109
|
+
|
|
110
|
+
### Buffer/Hex Handling
|
|
111
|
+
- Use `Buffer.from()` for creating buffers
|
|
112
|
+
- Hex strings should be uppercase without spaces for final output
|
|
113
|
+
- Use helper functions for hex/bytes conversion
|
|
114
|
+
- Maintain consistent hex formatting across protocols
|
|
115
|
+
|
|
116
|
+
### Node-RED Integration
|
|
117
|
+
- Register nodes in package.json under `node-red.nodes`
|
|
118
|
+
- Minimum Node.js version: >=15
|
|
119
|
+
- Minimum Node-RED version: >=1.3
|
|
120
|
+
- No external dependencies currently required
|
|
121
|
+
|
|
122
|
+
## Development Workflow
|
|
123
|
+
1. Modify protocol implementation files as needed
|
|
124
|
+
2. Test with `node test.js` or via Node-RED flow editor
|
|
125
|
+
3. Ensure all nodes follow the standard module pattern
|
|
126
|
+
4. Verify HTML definitions match JavaScript node registrations
|
|
127
|
+
5. Test message processing with sample data
|
|
128
|
+
|
|
129
|
+
## Protocol-Specific Notes
|
|
130
|
+
- 645: Handle address reversal and OAD encryption
|
|
131
|
+
- 698: Implement CRC-16/X-25 checksum validation
|
|
132
|
+
- BLE: Follow frame format with Start/End markers
|
|
133
|
+
- All protocols should support both encode and decode operations
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -7,9 +7,11 @@ let msg = {};
|
|
|
7
7
|
// msg.payload = 'FE FE FE FE 68 30 00 C3 05 77 59 63 00 15 19 11 59 0F 90 00 17 85 01 01 30 1B 07 00 01 01 01 02 02 00 02 02 06 00 00 00 00 00 00 00 01 00 04 B2 F3 8B 23 F2 7C 16'
|
|
8
8
|
// msg.payload = 'FE FE FE FE 68 33 00 C3 05 60 80 63 00 15 19 11 8B E3 90 00 1A 85 01 01 30 13 0A 00 01 01 01 02 02 00 02 02 1C 07 E3 07 17 0B 06 1B 00 00 00 01 00 04 2C 96 C6 41 EF 15 16'
|
|
9
9
|
// msg.payload = 'FE FE FE FE 68 2C 00 C3 05 16 01 00 00 00 00 11 54 84 90 00 13 85 01 01 30 13 0A 00 01 01 01 02 02 00 02 02 00 00 00 00 01 00 04 EE E6 E8 86 13 64 16'
|
|
10
|
-
msg.payload = '
|
|
11
|
-
msg.proto = 645
|
|
12
|
-
msg.mode = 'decode'
|
|
10
|
+
// msg.payload = '685134234200006893068467567533336F16'
|
|
11
|
+
// msg.proto = 645
|
|
12
|
+
// msg.mode = 'decode'
|
|
13
|
+
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
|
|
@@ -17,6 +19,31 @@ msg.mode = 'decode'
|
|
|
17
19
|
// console.log(proto698(msg));
|
|
18
20
|
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// msg.payload=[
|
|
27
|
+
// {
|
|
28
|
+
// barcode:'1',
|
|
29
|
+
// payload : '685134234200006893068467567533336F16',
|
|
30
|
+
// proto : 645
|
|
31
|
+
// }
|
|
32
|
+
// ]
|
|
33
|
+
|
|
34
|
+
// let nmsg = proto645(msg);
|
|
35
|
+
// console.log(nmsg);
|
|
36
|
+
// console.log(JSON.stringify(nmsg));
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// msg.payload=[
|
|
41
|
+
// {
|
|
42
|
+
// barcode:'1',
|
|
43
|
+
// payload : 'FE FE FE FE 68 52 00 C3 05 60 50 30 00 49 25 11 BA 8D 90 00 39 85 01 01 00 00 04 00 01 01 05 14 00 00 00 00 00 C2 91 74 14 00 00 00 00 00 10 CE 12 14 00 00 00 00 00 0A 24 C2 14 00 00 00 00 00 66 C8 DA 14 00 00 00 00 00 3B FC 54 00 00 01 00 04 8D 62 D9 49 53 D1 16',
|
|
44
|
+
// proto : 698
|
|
45
|
+
// }
|
|
46
|
+
// ]
|
|
47
|
+
|
|
48
|
+
// console.log(JSON.stringify(proto698(msg)));
|
|
49
|
+
// // console.log(proto698(msg));
|