node-red-zelecproto 0.1.1 → 0.1.3
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 +68 -35
- package/698.js +132 -99
- package/package.json +1 -1
- package/AGENTS.md +0 -133
package/645.js
CHANGED
|
@@ -433,6 +433,20 @@ function decode645(_msg) {
|
|
|
433
433
|
return arr.includes(di)
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
function expandBits16(v) {
|
|
437
|
+
const bits = {};
|
|
438
|
+
for (let i = 0; i < 16; i++) {
|
|
439
|
+
bits[`bit${i}`] = (v >> i) & 0x1;
|
|
440
|
+
}
|
|
441
|
+
return bits;
|
|
442
|
+
}
|
|
443
|
+
function expandBits32(v) {
|
|
444
|
+
const bits = {};
|
|
445
|
+
for (let i = 0; i < 32; i++) {
|
|
446
|
+
bits[`bit${i}`] = (v >>> i) & 0x1;
|
|
447
|
+
}
|
|
448
|
+
return bits;
|
|
449
|
+
}
|
|
436
450
|
|
|
437
451
|
// —— 常用分支(保留你的原分支,增加越界保护)——
|
|
438
452
|
function buildDays(prefix) {
|
|
@@ -520,31 +534,22 @@ function decode645(_msg) {
|
|
|
520
534
|
rawValue: w1,
|
|
521
535
|
rawBlockHex: statusBlockHex,
|
|
522
536
|
binary: bin16(w1),
|
|
523
|
-
|
|
524
|
-
'停电抄表电池欠压': !!bit(w1, 3),
|
|
525
|
-
'时钟电池欠压': !!bit(w1, 2),
|
|
526
|
-
'有功功率方向反向': !!bit(w1, 4),
|
|
527
|
-
'无功功率方向反向': !!bit(w1, 5),
|
|
528
|
-
'控制回路错误': !!bit(w1, 8),
|
|
529
|
-
'ESAM错误': !!bit(w1, 9),
|
|
530
|
-
'内部程序错误': !!bit(w1, 12),
|
|
531
|
-
'存储器故障或损坏': !!bit(w1, 13),
|
|
532
|
-
'透支状态': !!bit(w1, 14),
|
|
533
|
-
'时钟故障': !!bit(w1, 15)
|
|
534
|
-
}
|
|
537
|
+
bits: expandBits16(w1)
|
|
535
538
|
};
|
|
536
539
|
|
|
537
540
|
// 状态字2/3只保留原始数值和binary,具体位义由上层业务或文档表解释。
|
|
538
541
|
const w2 = readU16LE();
|
|
539
542
|
const word2 = (w2 === null) ? null : {
|
|
540
543
|
rawValue: w2,
|
|
541
|
-
binary: bin16(w2)
|
|
544
|
+
binary: bin16(w2),
|
|
545
|
+
bits: expandBits16(w2)
|
|
542
546
|
};
|
|
543
547
|
|
|
544
548
|
const w3 = readU16LE();
|
|
545
549
|
const word3 = (w3 === null) ? null : {
|
|
546
550
|
rawValue: w3,
|
|
547
|
-
binary: bin16(w3)
|
|
551
|
+
binary: bin16(w3),
|
|
552
|
+
bits: expandBits16(w3)
|
|
548
553
|
};
|
|
549
554
|
|
|
550
555
|
// —— 密钥状态字(与你现有的 04000508 保持一致,32位)——
|
|
@@ -553,6 +558,7 @@ function decode645(_msg) {
|
|
|
553
558
|
rawValue: w8,
|
|
554
559
|
hexValue: w8.toString(16).toUpperCase().padStart(8, '0'),
|
|
555
560
|
binary: bin32(w8),
|
|
561
|
+
bits: expandBits32(w8),
|
|
556
562
|
keys: {
|
|
557
563
|
'主控密钥有效': !!bit(w8, 0),
|
|
558
564
|
'身份认证密钥有效': !!bit(w8, 1),
|
|
@@ -581,18 +587,7 @@ function decode645(_msg) {
|
|
|
581
587
|
|
|
582
588
|
value = {
|
|
583
589
|
rawValue: v, binary: bin,
|
|
584
|
-
|
|
585
|
-
'停电抄表电池欠压': !!(v & (1 << 3)), // bit3: 0=正常, 1=欠压
|
|
586
|
-
'时钟电池欠压': !!(v & (1 << 2)), // bit2: 0=正常, 1=欠压
|
|
587
|
-
'有功功率方向反向': !!(v & (1 << 4)), // bit4: 0=正向, 1=反向
|
|
588
|
-
'无功功率方向反向': !!(v & (1 << 5)), // bit5: 0=正向, 1=反向
|
|
589
|
-
'控制回路错误': !!(v & (1 << 8)), // bit8: 0=正常, 1=错误
|
|
590
|
-
'ESAM错误': !!(v & (1 << 9)), // bit9: 0=正常, 1=错误
|
|
591
|
-
'内部程序错误': !!(v & (1 << 12)), // bit12: 0=正常, 1=错误
|
|
592
|
-
'存储器故障或损坏': !!(v & (1 << 13)), // bit13: 0=正常, 1=故障
|
|
593
|
-
'透支状态': !!(v & (1 << 14)), // bit14: 0=正常, 1=透支
|
|
594
|
-
'时钟故障': !!(v & (1 << 15)) // bit15: 0=正常, 1=故障
|
|
595
|
-
}
|
|
590
|
+
bits: expandBits16(v)
|
|
596
591
|
};
|
|
597
592
|
} else if (di === '04000502' && arrPush.length >= 6) {
|
|
598
593
|
// 状态字2只返回原始数值,binary为bit15..bit0。
|
|
@@ -603,7 +598,8 @@ function decode645(_msg) {
|
|
|
603
598
|
|
|
604
599
|
value = {
|
|
605
600
|
rawValue: v,
|
|
606
|
-
binary: bin
|
|
601
|
+
binary: bin,
|
|
602
|
+
bits: expandBits16(v)
|
|
607
603
|
};
|
|
608
604
|
} else if (di === '04000503' && arrPush.length >= 6) {
|
|
609
605
|
// 状态字3只返回原始数值,binary为bit15..bit0。
|
|
@@ -614,7 +610,8 @@ function decode645(_msg) {
|
|
|
614
610
|
|
|
615
611
|
value = {
|
|
616
612
|
rawValue: v,
|
|
617
|
-
binary: bin
|
|
613
|
+
binary: bin,
|
|
614
|
+
bits: expandBits16(v)
|
|
618
615
|
};
|
|
619
616
|
}
|
|
620
617
|
// else if (['0000FF00', '0001FF00', '0002FF00'].includes(di) && arrPush.length > 4) {
|
|
@@ -629,6 +626,7 @@ function decode645(_msg) {
|
|
|
629
626
|
rawValue: v,
|
|
630
627
|
hexValue: v.toString(16).toUpperCase().padStart(8, '0'),
|
|
631
628
|
binary: v.toString(2).padStart(32, '0'),
|
|
629
|
+
bits: expandBits32(v),
|
|
632
630
|
keys: {
|
|
633
631
|
'主控密钥有效': !!(v & (1 << 0)), '身份认证密钥有效': !!(v & (1 << 1)), '密钥协商密钥有效': !!(v & (1 << 2)),
|
|
634
632
|
'密钥更新密钥有效': !!(v & (1 << 3)), '传输密钥有效': !!(v & (1 << 4)), '保护密钥有效': !!(v & (1 << 5)),
|
|
@@ -757,6 +755,9 @@ function decode645(_msg) {
|
|
|
757
755
|
unit: 'V',
|
|
758
756
|
description: `${di === '02800008' ? '时钟电池电压' : '停电抄表电池电压'}: ${v.toFixed(2)}V`
|
|
759
757
|
};
|
|
758
|
+
} else if (di === '0280000A' && arrPush.length >= 8) {
|
|
759
|
+
// 0280000A: 内部电池工作时间,4字节,单位分
|
|
760
|
+
value = bytesToIntBE(arrPush.slice(-4).reverse());
|
|
760
761
|
} else if (di === '0400040A' && arrPush.length >= 7) {
|
|
761
762
|
// 无功脉冲常数:DI(4) + N3(3字节BCD,小端)
|
|
762
763
|
const dataBytes = arrPush.slice(4, 7); // LE:低字节在前
|
|
@@ -770,13 +771,7 @@ function decode645(_msg) {
|
|
|
770
771
|
} else if (di === '03300D01' && arrPush.length >= 16) {
|
|
771
772
|
value = parseCoverOpenLast645(arrPush);
|
|
772
773
|
} else {
|
|
773
|
-
|
|
774
|
-
value = {
|
|
775
|
-
rawMinus33: bytesToHex(arrPush).replace(/\s+/g, ''), // 含DI(4B) + 数据
|
|
776
|
-
di,
|
|
777
|
-
data: bytesToHex(arrPush.slice(4)).replace(/\s+/g, ''), // 纯数据部分(不含DI)
|
|
778
|
-
note: '未识别DI,已返回去0x33的原始数据'
|
|
779
|
-
};
|
|
774
|
+
value = parseGeneric645Data(di, arrPush);
|
|
780
775
|
}
|
|
781
776
|
} catch (e) {
|
|
782
777
|
return Object.assign(result, { ok: false, reason: 'decode_exception', exec_addr, di, ctrl, len, raw: put, err: String(e) });
|
|
@@ -890,6 +885,44 @@ function bcdDigitsStrLE(bytes) {
|
|
|
890
885
|
return s;
|
|
891
886
|
}
|
|
892
887
|
|
|
888
|
+
function parseGeneric645Data(di, arrPush) {
|
|
889
|
+
const dataBytes = Array.from(arrPush.slice(4));
|
|
890
|
+
const dataHex = bytesToHex(dataBytes).replace(/\s+/g, '');
|
|
891
|
+
const result = {
|
|
892
|
+
rawMinus33: bytesToHex(arrPush).replace(/\s+/g, ''),
|
|
893
|
+
di,
|
|
894
|
+
data: dataHex,
|
|
895
|
+
length: dataBytes.length,
|
|
896
|
+
note: '未识别DI,已按通用格式解析候选值'
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
if (dataBytes.length === 0) return result;
|
|
900
|
+
|
|
901
|
+
result.uintLE = bytesToUInt(dataBytes, true);
|
|
902
|
+
result.uintBE = bytesToUInt(dataBytes, false);
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
result.bcdLE = bcdLEToInt(dataBytes);
|
|
906
|
+
result.bcdText = bcdDigitsStrLE(dataBytes);
|
|
907
|
+
} catch (_) {
|
|
908
|
+
result.bcdLE = null;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (dataBytes.every(b => b >= 0x20 && b <= 0x7E)) {
|
|
912
|
+
result.ascii = Buffer.from(dataBytes).toString('ascii');
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function bytesToUInt(bytes, littleEndian) {
|
|
919
|
+
if (!bytes || bytes.length === 0) return null;
|
|
920
|
+
const arr = littleEndian ? bytes.slice().reverse() : bytes;
|
|
921
|
+
const hex = Buffer.from(arr).toString('hex') || '0';
|
|
922
|
+
const value = BigInt(`0x${hex}`);
|
|
923
|
+
return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
|
|
924
|
+
}
|
|
925
|
+
|
|
893
926
|
|
|
894
927
|
// === 开盖明细专用工具 ===
|
|
895
928
|
|
package/698.js
CHANGED
|
@@ -140,6 +140,7 @@ const OAD_CATEGORIES = {
|
|
|
140
140
|
BATTERY_VOLTAGE: {
|
|
141
141
|
"CLOCK_BATTERY_VOLTAGE": { oad: "20110200", desc: "时钟电池电压", type: "double-long-unsigned", unit: "V", scale: -2 },
|
|
142
142
|
"POWER_DOWN_READING_BATTERY_VOLTAGE": { oad: "20120200", desc: "停电抄表电池电压", type: "double-long-unsigned", unit: "V", scale: -2 },
|
|
143
|
+
"CLOCK_BATTERY_WORK_TIME": { oad: "20130200", desc: "时钟电池工作时间", type: "double-long-unsigned", unit: "min", scale: 0 },
|
|
143
144
|
},
|
|
144
145
|
FREEZE_DATA: {
|
|
145
146
|
"DAILY_FREEZE": { oad: "50040200", desc: "日冻结", requestType: "record" },
|
|
@@ -870,15 +871,11 @@ function parseStatusWordFromAxdrBitString(buffer) {
|
|
|
870
871
|
const bytes = buffer.slice(start, end);
|
|
871
872
|
if (bytes.length < 2) return null;
|
|
872
873
|
const bits = [];
|
|
873
|
-
|
|
874
|
-
// A-XDR bit-string按每字节高位到低位展开;另保留低位到高位展示用于和现场口径对照。
|
|
874
|
+
// A-XDR bit-string按每字节高位到低位展开。
|
|
875
875
|
for (const byte of bytes.slice(0, 2)) {
|
|
876
876
|
for (let bit = 7; bit >= 0; bit--) {
|
|
877
877
|
bits.push((byte >> bit) & 0x01);
|
|
878
878
|
}
|
|
879
|
-
for (let bit = 0; bit <= 7; bit++) {
|
|
880
|
-
lowToHighBits.push((byte >> bit) & 0x01);
|
|
881
|
-
}
|
|
882
879
|
}
|
|
883
880
|
let statusWord = 0;
|
|
884
881
|
// 文档表F.x中的bit编号按展开后的bit-string下标映射,后续binary再按645一致的数值形式输出。
|
|
@@ -887,136 +884,129 @@ function parseStatusWordFromAxdrBitString(buffer) {
|
|
|
887
884
|
}
|
|
888
885
|
return {
|
|
889
886
|
statusWord,
|
|
890
|
-
|
|
891
|
-
|
|
887
|
+
binary: bits.slice(0, 16).join(''),
|
|
888
|
+
consumed: end
|
|
892
889
|
};
|
|
893
890
|
}
|
|
894
891
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const result = createStandardResult("电表状态", oad, dataBuffer);
|
|
903
|
-
try {
|
|
904
|
-
// 1. 读取原始状态字(小端)
|
|
905
|
-
const statusWordLe = parseMeterStatusOptimized(dataBuffer);
|
|
906
|
-
|
|
907
|
-
// 2. 转换为大端,用于大端逻辑判断
|
|
908
|
-
const statusWordBe = toBigEndian16(statusWordLe);
|
|
909
|
-
|
|
910
|
-
if (statusWordBe !== null) {
|
|
911
|
-
const valTmp = statusWordBe & 0xFFFF;
|
|
912
|
-
const bin = valTmp.toString(2).padStart(16, '0'); // 最高位bit15在最左
|
|
892
|
+
function expandBits16(v) {
|
|
893
|
+
const bits = {};
|
|
894
|
+
for (let i = 0; i < 16; i++) {
|
|
895
|
+
bits[`bit${i}`] = (v >> i) & 0x1;
|
|
896
|
+
}
|
|
897
|
+
return bits;
|
|
898
|
+
}
|
|
913
899
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
'bit9 ESAM错误': bin[6] === '1',
|
|
926
|
-
'bit8 控制回路错误': bin[7] === '1',
|
|
927
|
-
'bit7 保留': bin[8] === '1',
|
|
928
|
-
'bit6 保留': bin[9] === '1',
|
|
929
|
-
'bit5 无功功率方向反向': bin[10] === '1',
|
|
930
|
-
'bit4 有功功率方向反向': bin[11] === '1',
|
|
931
|
-
'bit3 停电抄表电池欠压': bin[12] === '1',
|
|
932
|
-
'bit2 时钟电池欠压': bin[13] === '1',
|
|
933
|
-
'bit1 需量积算方式': bin[14] === '1',
|
|
934
|
-
'bit0 保留': bin[15] === '1'
|
|
935
|
-
}
|
|
936
|
-
};
|
|
900
|
+
function format698StatusWord(statusWord, bitStringStatus = null, index = null) {
|
|
901
|
+
const val16 = statusWord & 0xFFFF;
|
|
902
|
+
return {
|
|
903
|
+
...(index == null ? {} : { index }),
|
|
904
|
+
rawValue: val16,
|
|
905
|
+
statusWordHex: val16.toString(16).padStart(4, '0').toUpperCase(),
|
|
906
|
+
// binary按原始bit-string显示,便于与645页面展示口径保持一致。
|
|
907
|
+
binary: bitStringStatus?.binary || val16.toString(2).padStart(16, '0'),
|
|
908
|
+
bits: expandBits16(val16)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
937
911
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
912
|
+
function parseStatusWordsFromAxdrArray(buffer) {
|
|
913
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 2 || buffer[0] !== 0x01) return [];
|
|
914
|
+
const L = readAxdrLength(buffer, 1);
|
|
915
|
+
const count = L.len;
|
|
916
|
+
let offset = 1 + L.size;
|
|
917
|
+
const words = [];
|
|
918
|
+
|
|
919
|
+
for (let i = 0; i < count && offset < buffer.length; i++) {
|
|
920
|
+
if (buffer[offset] === 0x04) {
|
|
921
|
+
const word = parseStatusWordFromAxdrBitString(buffer.slice(offset));
|
|
922
|
+
if (!word) break;
|
|
923
|
+
words.push(word);
|
|
924
|
+
offset += word.consumed;
|
|
944
925
|
} else {
|
|
945
|
-
|
|
926
|
+
const { consumed } = enhancedParseData(buffer.slice(offset), '2014', '02');
|
|
927
|
+
if (!consumed) break;
|
|
928
|
+
offset += consumed;
|
|
946
929
|
}
|
|
947
|
-
|
|
948
|
-
} catch (e) {
|
|
949
|
-
console.error('parseMeterStatus错误:', e.message);
|
|
950
|
-
setErrorResult(result, e.message);
|
|
951
930
|
}
|
|
952
931
|
|
|
953
|
-
return
|
|
932
|
+
return words;
|
|
954
933
|
}
|
|
955
934
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
* 解析电表运行状态字3(20140203)- 按操作类位义输出字段
|
|
959
|
-
*/
|
|
960
|
-
function parseMeterStatusWord3(dataBuffer) {
|
|
961
|
-
const oad = '20140203';
|
|
962
|
-
const result = createStandardResult("电表运行状态字3", oad, dataBuffer);
|
|
935
|
+
function parseMeterStatusWord698(dataBuffer, oad, dataType) {
|
|
936
|
+
const result = createStandardResult(dataType, oad, dataBuffer);
|
|
963
937
|
try {
|
|
964
938
|
const bitStringStatus = parseStatusWordFromAxdrBitString(dataBuffer);
|
|
965
939
|
const statusWord = bitStringStatus?.statusWord ?? parseMeterStatusOptimized(dataBuffer);
|
|
966
940
|
if (statusWord === null) throw new Error('无法解析状态字');
|
|
967
941
|
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
const bin = val16.toString(2).padStart(16, '0');
|
|
971
|
-
|
|
972
|
-
result.value = {
|
|
973
|
-
rawValue: val16,
|
|
974
|
-
binary: bin,
|
|
975
|
-
...(bitStringStatus?.binaryLowToHigh ? { binaryLowToHigh: bitStringStatus.binaryLowToHigh } : {}),
|
|
976
|
-
...(bitStringStatus?.binaryHighToLow ? { binaryHighToLow: bitStringStatus.binaryHighToLow } : {})
|
|
977
|
-
};
|
|
942
|
+
const value = format698StatusWord(statusWord, bitStringStatus);
|
|
943
|
+
result.value = value;
|
|
978
944
|
|
|
979
945
|
setSuccessResult(result, {
|
|
980
|
-
statusWord:
|
|
981
|
-
statusWordHex:
|
|
982
|
-
statusBits: decodeMeterStatusBits(
|
|
946
|
+
statusWord: value.rawValue,
|
|
947
|
+
statusWordHex: value.statusWordHex,
|
|
948
|
+
statusBits: decodeMeterStatusBits(value.rawValue)
|
|
983
949
|
}, { generic: { dataType: 'bit-string' } });
|
|
984
950
|
} catch (e) {
|
|
985
951
|
setErrorResult(result, e.message);
|
|
986
952
|
}
|
|
953
|
+
|
|
987
954
|
return result;
|
|
988
955
|
}
|
|
989
956
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
function parseMeterStatusWord2(dataBuffer) {
|
|
994
|
-
const oad = '20140202';
|
|
995
|
-
const result = createStandardResult("电表运行状态字2", oad, dataBuffer);
|
|
957
|
+
function parseMeterStatusArray698(dataBuffer) {
|
|
958
|
+
const oad = '20140200';
|
|
959
|
+
const result = createStandardResult("电表状态", oad, dataBuffer);
|
|
996
960
|
try {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
if (statusWord === null) throw new Error('无法解析状态字');
|
|
1000
|
-
|
|
1001
|
-
const val16 = statusWord & 0xFFFF;
|
|
1002
|
-
// binary按最终状态字数值输出(bit15..bit0),与645状态字binary保持一致。
|
|
1003
|
-
const bin = val16.toString(2).padStart(16, '0');
|
|
961
|
+
const parsedWords = parseStatusWordsFromAxdrArray(dataBuffer);
|
|
962
|
+
if (!parsedWords.length) throw new Error('无法解析状态字数组');
|
|
1004
963
|
|
|
964
|
+
const words = parsedWords.map((word, index) => format698StatusWord(word.statusWord, word, index + 1));
|
|
1005
965
|
result.value = {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
bits: decodeMeterStatusBits(val16)
|
|
966
|
+
word1: words[0] || null,
|
|
967
|
+
word2: words[1] || null,
|
|
968
|
+
word3: words[2] || null,
|
|
969
|
+
words
|
|
1011
970
|
};
|
|
1012
971
|
|
|
1013
|
-
setSuccessResult(result, {
|
|
972
|
+
setSuccessResult(result, {
|
|
973
|
+
statusWords: words,
|
|
974
|
+
statusWord: words[0]?.rawValue ?? null,
|
|
975
|
+
statusWordHex: words[0]?.statusWordHex ?? null,
|
|
976
|
+
statusBits: words[0] ? decodeMeterStatusBits(words[0].rawValue) : []
|
|
977
|
+
}, { generic: { dataType: 'array' } });
|
|
1014
978
|
} catch (e) {
|
|
1015
979
|
setErrorResult(result, e.message);
|
|
1016
980
|
}
|
|
981
|
+
|
|
1017
982
|
return result;
|
|
1018
983
|
}
|
|
1019
984
|
|
|
985
|
+
/**
|
|
986
|
+
* 解析电表运行状态字1或状态字数组
|
|
987
|
+
* @param {Buffer} dataBuffer - 数据缓冲区
|
|
988
|
+
* @returns {Object} 解析结果
|
|
989
|
+
*/
|
|
990
|
+
|
|
991
|
+
function parseMeterStatus(dataBuffer, oad = '20140201') {
|
|
992
|
+
if (oad === '20140200') return parseMeterStatusArray698(dataBuffer);
|
|
993
|
+
return parseMeterStatusWord698(dataBuffer, oad, "电表状态");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* 解析电表运行状态字3(20140203)
|
|
998
|
+
*/
|
|
999
|
+
function parseMeterStatusWord3(dataBuffer) {
|
|
1000
|
+
return parseMeterStatusWord698(dataBuffer, '20140203', "电表运行状态字3");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* 解析电表运行状态字2(20140202)
|
|
1005
|
+
*/
|
|
1006
|
+
function parseMeterStatusWord2(dataBuffer) {
|
|
1007
|
+
return parseMeterStatusWord698(dataBuffer, '20140202', "电表运行状态字2");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1020
1010
|
/**
|
|
1021
1011
|
* 优化的状态字解析策略
|
|
1022
1012
|
* @param {Buffer} dataBuffer - 数据缓冲区
|
|
@@ -1792,6 +1782,48 @@ function parseBatteryVoltage(dataBuffer, oad) {
|
|
|
1792
1782
|
return result;
|
|
1793
1783
|
}
|
|
1794
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* 解析时钟电池工作时间(分钟)
|
|
1787
|
+
* @param {Buffer} dataBuffer - 数据缓冲区
|
|
1788
|
+
* @param {string} oad - OAD标识
|
|
1789
|
+
* @returns {Object} 解析结果
|
|
1790
|
+
*/
|
|
1791
|
+
function parseClockBatteryWorkTime(dataBuffer, oad) {
|
|
1792
|
+
const oadInfo = getOADInfo(oad);
|
|
1793
|
+
const result = createStandardResult(oadInfo ? oadInfo.desc : "时钟电池工作时间", oad, dataBuffer);
|
|
1794
|
+
|
|
1795
|
+
try {
|
|
1796
|
+
const { result: genericResult } = enhancedParseData(dataBuffer, oad.slice(0, 4), oad.slice(4, 6));
|
|
1797
|
+
|
|
1798
|
+
if (genericResult.dataType !== '双长无符号整数' || typeof genericResult.parsedValue !== 'number') {
|
|
1799
|
+
throw new Error(`时钟电池工作时间数据格式不正确: ${genericResult.dataType}`);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
const rawMinutes = genericResult.parsedValue;
|
|
1803
|
+
result.value = rawMinutes;
|
|
1804
|
+
|
|
1805
|
+
setSuccessResult(result, [{
|
|
1806
|
+
type: oadInfo ? oadInfo.desc : "时钟电池工作时间",
|
|
1807
|
+
rawValue: rawMinutes,
|
|
1808
|
+
minutes: rawMinutes,
|
|
1809
|
+
workMinutes: rawMinutes,
|
|
1810
|
+
clock_battery_work_min: rawMinutes,
|
|
1811
|
+
unit: "min",
|
|
1812
|
+
description: `${oadInfo ? oadInfo.desc : "时钟电池工作时间"} ${rawMinutes}分钟`
|
|
1813
|
+
}], {
|
|
1814
|
+
unit: "min",
|
|
1815
|
+
scale: 0,
|
|
1816
|
+
count: 1,
|
|
1817
|
+
generic: genericResult
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
setErrorResult(result, e.message);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return result;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1795
1827
|
function parseEnergyData(dataBuffer, oad) {
|
|
1796
1828
|
const result = createStandardResult("电能数据", oad, dataBuffer);
|
|
1797
1829
|
try {
|
|
@@ -2288,6 +2320,7 @@ function oadParserRouter(payload, oad) {
|
|
|
2288
2320
|
if (oad === '40070205') return parseLCDDecimalDigits(payload); //抄读当前组合有功电能数据块
|
|
2289
2321
|
if (oad === '20110200') return parseBatteryVoltage(payload, oad); // 时钟电池电压
|
|
2290
2322
|
if (oad === '20120200') return parseBatteryVoltage(payload, oad); // 停电抄表电池电压
|
|
2323
|
+
if (oad === '20130200') return parseClockBatteryWorkTime(payload, oad); // 时钟电池工作时间
|
|
2291
2324
|
// 通用兜底
|
|
2292
2325
|
const oadInfo = getOADInfo(oad) || { desc: "通用数据" };
|
|
2293
2326
|
const genericStdResult = createStandardResult(oadInfo.desc, oad, payload);
|
package/package.json
CHANGED
package/AGENTS.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
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
|