node-red-zelecproto 0.0.3 → 0.0.4
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/ble.js +483 -0
- package/icons/ble.svg +1 -0
- package/package.json +3 -2
- package/zeleble.html +26 -0
- package/zeleble.js +21 -0
package/ble.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 国网多芯物联表《蓝牙通信及脉冲检定说明手册(0224)》协议
|
|
3
|
+
* 帧格式:Start(7E7E7E5A) + L(1B) + OAD(1B) + DATA(N) + CS(1B) + End(7EA5)
|
|
4
|
+
* 其中 L = 6 + N(即 OAD+DATA+CS 共字节数 + 固定偏移;见示例长度规律)
|
|
5
|
+
* CS = 从起始 7E 开始到 DATA 最后一字节为止的逐字节求和低 8 位
|
|
6
|
+
* 参考:起止符、示例帧、命令/应答与数据域、波特率枚举、脉冲类型与模式等。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const START = Buffer.from([0x7E, 0x7E, 0x7E, 0x5A]);
|
|
10
|
+
const END = Buffer.from([0x7E, 0xA5]);
|
|
11
|
+
|
|
12
|
+
// —— 命令码(请求)/ 应答命令码(高位加 0x80) ——
|
|
13
|
+
// 0x00 复位转换器;0x01 连接表;0x02 待测表进入检定/切换项;0x03 转换器进入/退出检定
|
|
14
|
+
// 0x04 设置485波特率;0x05 读管理单元版本;0x06 读蓝牙模块版本;0x07 检定预处理;0x08 查询预处理状态
|
|
15
|
+
const OAD = {
|
|
16
|
+
RESET: 0x00,
|
|
17
|
+
CONNECT_METER: 0x01,
|
|
18
|
+
METER_TEST: 0x02,
|
|
19
|
+
CONV_TEST: 0x03,
|
|
20
|
+
SET_BAUD: 0x04,
|
|
21
|
+
VER_MCU: 0x05,
|
|
22
|
+
VER_BLE: 0x06,
|
|
23
|
+
PREPARE: 0x07,
|
|
24
|
+
PREPARE_Q: 0x08,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 结果码(各应答常见约定:00成功/01失败或超时/02参数非法/03授权非法)
|
|
28
|
+
const RESULT = {
|
|
29
|
+
OK: 0x00, FAIL_OR_TIMEOUT: 0x01, BAD_PARAM: 0x02, AUTH_FAIL: 0x03
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 脉冲类型(检定模式数据域里使用)
|
|
33
|
+
const PULSE = {
|
|
34
|
+
SEC: 0x00, DEMAND: 0x01, TARIFF: 0x02, HARMO_P: 0x03, HARMO_R: 0x04, REACTIVE: 0x05, ACTIVE: 0x06, EXIT: 0xFF
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 检定通信模式:普通(0x00) / 脉冲跟随(0x01)
|
|
38
|
+
const MODE = { NORMAL: 0x00, FOLLOW: 0x01 };
|
|
39
|
+
|
|
40
|
+
// 485 波特率枚举
|
|
41
|
+
const BAUD = { "2400": 0x00, "4800": 0x01, "9600": 0x02, "19200": 0x03, "38400": 0x04, "57600": 0x05 };
|
|
42
|
+
|
|
43
|
+
// —— 工具函数 ——
|
|
44
|
+
|
|
45
|
+
// 计算 1 字节 CS(从第一个 0x7E 起,累加至 DATA 末尾;不含 CS 与帧尾)
|
|
46
|
+
function calcCS(bufNoEndNoCS) {
|
|
47
|
+
let sum = 0;
|
|
48
|
+
for (const b of bufNoEndNoCS) sum = (sum + b) & 0xFF;
|
|
49
|
+
return sum;
|
|
50
|
+
}
|
|
51
|
+
// —— 新增:从 barCode 派生 6位显示地址(示例策略:请按你C#真实逻辑替换)——
|
|
52
|
+
function deriveAddrAscii6FromBarCode(barCode) {
|
|
53
|
+
// 占位策略:优先使用 payload.addrAscii6;若无,就“示例”回退为 "654321"
|
|
54
|
+
// 你可改成:取条码中的某段、或外部映射查询等
|
|
55
|
+
return "654321";
|
|
56
|
+
}
|
|
57
|
+
// BCD 地址(字符串如 '112233445566')→ 低字节在前的 6 字节(例:66 55 44 33 22 11)
|
|
58
|
+
function addrStrTo6LE(addr12) {
|
|
59
|
+
if (!/^[0-9A-Fa-f]{12}$/.test(addr12)) throw new Error("通信地址应为12位HEX/BCD字符串");
|
|
60
|
+
const be = Buffer.from(addr12, 'hex'); // BE: 11 22 33 44 55 66
|
|
61
|
+
return Buffer.from([...be].reverse()); // LE: 66 55 44 33 22 11
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 构帧:START + L + OAD + DATA + CS + END
|
|
65
|
+
function buildFrame(oad, dataBuf = Buffer.alloc(0)) {
|
|
66
|
+
const len = 6 + dataBuf.length; // L=6+N(示例规律)
|
|
67
|
+
const head = Buffer.concat([START, Buffer.from([len, oad]), dataBuf]);
|
|
68
|
+
const cs = Buffer.from([calcCS(head)]);
|
|
69
|
+
return Buffer.concat([head, cs, END]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 解析一帧;返回 {oad,isResp,len,data,cs,ok, result?}
|
|
73
|
+
function parseFrame(buf) {
|
|
74
|
+
// 基本边界检查
|
|
75
|
+
if (buf.length < 4 + 1 + 1 + 1 + 2) throw new Error("帧过短");
|
|
76
|
+
if (!buf.slice(0, 4).equals(START)) throw new Error("起始符错误");
|
|
77
|
+
if (!buf.slice(-2).equals(END)) throw new Error("结束符错误");
|
|
78
|
+
const len = buf[4];
|
|
79
|
+
const oad = buf[5];
|
|
80
|
+
const dataEndExclusive = 4 /*start*/ + 1 /*L*/ + 1 /*OAD*/ + (len - 6); // DATA 末尾位置(不含)
|
|
81
|
+
const data = buf.slice(6, 6 + (len - 6)); // N = len-6
|
|
82
|
+
const cs = buf[6 + (len - 6)]; // CS 紧随 DATA
|
|
83
|
+
const partForCS = buf.slice(0, 6 + (len - 6)); // 计算 CS 的区间(含 START/L/OAD/DATA,不含CS/END)
|
|
84
|
+
const ok = (calcCS(partForCS) === cs);
|
|
85
|
+
|
|
86
|
+
const isResp = (oad & 0x80) === 0x80;
|
|
87
|
+
const oadReq = isResp ? (oad & 0x7F) : oad;
|
|
88
|
+
|
|
89
|
+
const obj = { len, oad, isResp, oadReq, data, cs, ok };
|
|
90
|
+
|
|
91
|
+
// 应答帧通用 result 提取(大多数应答第1字节是结果码)
|
|
92
|
+
if (isResp && data.length >= 1) obj.result = data[0];
|
|
93
|
+
|
|
94
|
+
return obj;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// 解析 C# 风格的 itemContent: "01|06|01|00|01|01|09D0"
|
|
99
|
+
// 规则:前6段为单字节HEX或十进制字符串 -> 1B;第7段为16bit数值的HEX字符串 -> 小端两字节
|
|
100
|
+
/**
|
|
101
|
+
* 通用 itemContent 解析器(无需判断段数)
|
|
102
|
+
*
|
|
103
|
+
* 语法(对每一段):
|
|
104
|
+
* - 纯HEX/DEC: "01"、"9"、"09D0"、"255"、"0x1A"
|
|
105
|
+
* - 可选长度: "09D0:2" 表示用 2 字节编码该值
|
|
106
|
+
* - 可选端序: "@le" / "@be"(默认规则见下)
|
|
107
|
+
*
|
|
108
|
+
* 默认规则:
|
|
109
|
+
* - HEX 长度 ≤2 → 1 字节
|
|
110
|
+
* - HEX 长度 ==4 → 2 字节,小端(LE)← 为兼容 0x02 的 09D0→D0 09
|
|
111
|
+
* - HEX 长度 >4 且为偶数 → 直接按字节对切分拼接(BE),若加 @le 则按字节对反转
|
|
112
|
+
* - 纯十进制 → 1 字节(或用 :N 指定长度,长度>1 时默认 LE,可用 @be 改为 BE)
|
|
113
|
+
*/
|
|
114
|
+
function buildDataFromItemContent(itemContent) {
|
|
115
|
+
if (!itemContent) throw new Error("itemContent 为空");
|
|
116
|
+
const parts = String(itemContent)
|
|
117
|
+
.split('|')
|
|
118
|
+
.map(s => s.trim())
|
|
119
|
+
.filter(s => s.length > 0);
|
|
120
|
+
|
|
121
|
+
const out = [];
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < parts.length; i++) {
|
|
124
|
+
let seg = parts[i];
|
|
125
|
+
|
|
126
|
+
// 提取端序与长度标注(可选)
|
|
127
|
+
let endian = null; // null=按默认规则, 'le' | 'be'
|
|
128
|
+
let sizeBytes = null; // null=按默认规则, 否则固定字节数
|
|
129
|
+
|
|
130
|
+
// 端序标注:@le / @be
|
|
131
|
+
const endianMatch = seg.match(/@(le|be)$/i);
|
|
132
|
+
if (endianMatch) {
|
|
133
|
+
endian = endianMatch[1].toLowerCase();
|
|
134
|
+
seg = seg.slice(0, seg.length - endianMatch[0].length);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 固定长度标注::N
|
|
138
|
+
const lenMatch = seg.match(/:(\d+)$/);
|
|
139
|
+
if (lenMatch) {
|
|
140
|
+
sizeBytes = parseInt(lenMatch[1], 10);
|
|
141
|
+
seg = seg.slice(0, seg.length - lenMatch[0].length);
|
|
142
|
+
if (!(sizeBytes > 0)) throw new Error(`itemContent 段${i + 1} 长度无效: ${lenMatch[0]}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 支持 0x 前缀;识别十进制
|
|
146
|
+
let isDec = /^[0-9]+$/.test(seg);
|
|
147
|
+
let hex = seg
|
|
148
|
+
.replace(/^0x/i, '')
|
|
149
|
+
.toUpperCase()
|
|
150
|
+
.replace(/_/g, ''); // 容忍下划线分隔
|
|
151
|
+
|
|
152
|
+
if (!/^[0-9A-F]+$/.test(hex)) {
|
|
153
|
+
if (isDec) {
|
|
154
|
+
// 十进制:转数值
|
|
155
|
+
const val = parseInt(seg, 10);
|
|
156
|
+
if (Number.isNaN(val) || val < 0) throw new Error(`itemContent 段${i + 1} 非法十进制: ${seg}`);
|
|
157
|
+
if (sizeBytes == null) {
|
|
158
|
+
// 默认 1 字节
|
|
159
|
+
if (val > 0xFF) throw new Error(`itemContent 段${i + 1} 超过1字节范围,请用 :N 指定长度`);
|
|
160
|
+
out.push(val & 0xFF);
|
|
161
|
+
} else {
|
|
162
|
+
// 按长度编码,默认 LE,可用 @be 指定大端
|
|
163
|
+
let v = val >>> 0;
|
|
164
|
+
const tmp = [];
|
|
165
|
+
for (let k = 0; k < sizeBytes; k++) {
|
|
166
|
+
tmp.push(v & 0xFF);
|
|
167
|
+
v >>>= 8;
|
|
168
|
+
}
|
|
169
|
+
if (endian === 'be') tmp.reverse();
|
|
170
|
+
out.push(...tmp);
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`itemContent 段${i + 1} 非法:${parts[i]}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// HEX:若位数为奇数则左补0
|
|
178
|
+
if (hex.length % 2 === 1) hex = '0' + hex;
|
|
179
|
+
|
|
180
|
+
if (sizeBytes != null) {
|
|
181
|
+
// 强制长度
|
|
182
|
+
const buf = [];
|
|
183
|
+
const val = BigInt('0x' + hex);
|
|
184
|
+
// 按长度与端序编码
|
|
185
|
+
if (endian === 'be') {
|
|
186
|
+
// BE: 高位在前
|
|
187
|
+
for (let k = sizeBytes - 1; k >= 0; k--) {
|
|
188
|
+
const byte = Number((val >> BigInt(8 * k)) & 0xFFn);
|
|
189
|
+
buf.push(byte);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// 默认 LE
|
|
193
|
+
for (let k = 0; k < sizeBytes; k++) {
|
|
194
|
+
const byte = Number((val >> BigInt(8 * k)) & 0xFFn);
|
|
195
|
+
buf.push(byte);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out.push(...buf);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 未指定长度:按默认规则
|
|
203
|
+
if (hex.length <= 2) {
|
|
204
|
+
// 1B
|
|
205
|
+
out.push(parseInt(hex, 16) & 0xFF);
|
|
206
|
+
} else if (hex.length === 4) {
|
|
207
|
+
// 2B,默认 LE(与 0x02 的 09D0 → D0 09 对齐)
|
|
208
|
+
const v = parseInt(hex, 16);
|
|
209
|
+
if (endian === 'be') {
|
|
210
|
+
out.push((v >> 8) & 0xFF, v & 0xFF);
|
|
211
|
+
} else {
|
|
212
|
+
out.push(v & 0xFF, (v >> 8) & 0xFF);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// 多字节偶数位 HEX:默认按 BE 逐字节拼接;若 @le 则按字节对反转
|
|
216
|
+
const bytes = hex.match(/../g).map(h => parseInt(h, 16));
|
|
217
|
+
if (endian === 'le') bytes.reverse();
|
|
218
|
+
out.push(...bytes);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return Buffer.from(out);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 依据 meterNo 计算最后2字节: (2492 + meterNo*20) -> 16bit 小端
|
|
226
|
+
function tail2FromMeterNo(meterNo) {
|
|
227
|
+
const n = (2492 + (meterNo | 0) * 20) & 0xFFFF;
|
|
228
|
+
return Buffer.from([n & 0xFF, (n >>> 8) & 0xFF]); // LE: 低->高
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// —— 编码器 ——
|
|
232
|
+
// 传入 msg.payload 形如:
|
|
233
|
+
// { action:'encode', oad:'connect', addr:'112233445566' }
|
|
234
|
+
// { action:'encode', oad:'conv_test', ch:1, pulse: 'SEC', power:4, mode:'FOLLOW' }
|
|
235
|
+
// { action:'encode', oad:'set_baud', baud: '38400' } 等
|
|
236
|
+
function encode(payload) {
|
|
237
|
+
// const oad = (payload.action || payload.oad || '').toLowerCase();
|
|
238
|
+
const which = (payload.oad || '').toLowerCase();
|
|
239
|
+
|
|
240
|
+
switch (which) {
|
|
241
|
+
case 'reset':
|
|
242
|
+
// 0x00 复位转换器,请求无数据;应答 0x80 + 结果码
|
|
243
|
+
return buildFrame(OAD.RESET);
|
|
244
|
+
|
|
245
|
+
// 旧:6字节LE
|
|
246
|
+
// 新:优先 barCode/addrAscii6 的 12字节格式;否则回退旧法
|
|
247
|
+
case 'connect': {
|
|
248
|
+
const ascii6 = payload.addrAscii6;
|
|
249
|
+
const barCode = payload.barCode;
|
|
250
|
+
|
|
251
|
+
if (ascii6 && barCode) {
|
|
252
|
+
if (!/^[0-9A-Za-z]{6}$/.test(ascii6)) throw new Error("connect: addrAscii6 需为6位可显示字符");
|
|
253
|
+
const head6 = addrStrTo6LE(barCode.substring(barCode.length - 13, barCode.length - 13 + 12));
|
|
254
|
+
const tail6 = Buffer.from(ascii6, 'ascii'); // 6字节 ASCII,如 "654321"
|
|
255
|
+
return buildFrame(OAD.CONNECT_METER, Buffer.concat([head6, tail6]));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 兼容旧入参:addr=12位HEX(BCD),走 6字节LE
|
|
259
|
+
if (payload.addr) {
|
|
260
|
+
const addr6 = addrStrTo6LE(payload.addr);
|
|
261
|
+
return buildFrame(OAD.CONNECT_METER, addr6);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw new Error("connect: 请提供 barCode/addrAscii6 或 addr(12HEX)");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
case 'meter_test': {
|
|
268
|
+
// 优先 itemContent(与 C# 完全等价)
|
|
269
|
+
if (payload.itemContent) {
|
|
270
|
+
const data = buildDataFromItemContent(payload.itemContent);
|
|
271
|
+
return buildFrame(OAD.METER_TEST, data);
|
|
272
|
+
}
|
|
273
|
+
// 次选:meterNo 按公式生成末尾2字节;其余 6 字节来自参数(默认给出与 C# 示例相同的字段值)
|
|
274
|
+
if (payload.meterNo != null) {
|
|
275
|
+
const slot = (payload.slot ?? 0x01) & 0xFF; // 01
|
|
276
|
+
const pulse = (typeof payload.pulse === 'string')
|
|
277
|
+
? ({ SEC: 0, DEMAND: 1, TARIFF: 2, HARMO_P: 3, HARMO_R: 4, REACTIVE: 5, ACTIVE: 6, EXIT: 0xFF }[payload.pulse] ?? 0x06)
|
|
278
|
+
: (payload.pulse ?? 0x06) & 0xFF; // 06 (ACTIVE)
|
|
279
|
+
const power = (payload.power ?? 0x01) & 0xFF; // 01
|
|
280
|
+
const mode = (typeof payload.mode === 'string')
|
|
281
|
+
? ({ NORMAL: 0, FOLLOW: 1 }[payload.mode.toUpperCase()] ?? 0)
|
|
282
|
+
: (payload.mode ?? 0x00) & 0xFF; // 00
|
|
283
|
+
const rfu1 = (payload.rfu1 ?? 0x01) & 0xFF; // 01
|
|
284
|
+
const rfu2 = (payload.rfu2 ?? 0x01) & 0xFF; // 01
|
|
285
|
+
const head6 = Buffer.from([slot, pulse, power, mode, rfu1, rfu2]);
|
|
286
|
+
const tail2 = tail2FromMeterNo(payload.meterNo);
|
|
287
|
+
return buildFrame(OAD.METER_TEST, Buffer.concat([head6, tail2]));
|
|
288
|
+
}
|
|
289
|
+
// 兜底:保持你之前的4字节实现以兼容老设备
|
|
290
|
+
const PULSE = { SEC: 0, DEMAND: 1, TARIFF: 2, HARMO_P: 3, HARMO_R: 4, REACTIVE: 5, ACTIVE: 6, EXIT: 0xFF };
|
|
291
|
+
const MODE = { NORMAL: 0, FOLLOW: 1 };
|
|
292
|
+
const slot = payload.slot ?? 1;
|
|
293
|
+
const pulse = typeof payload.pulse === 'string' ? PULSE[payload.pulse] : payload.pulse;
|
|
294
|
+
const power = payload.power ?? 0;
|
|
295
|
+
const mode = typeof payload.mode === 'string' ? MODE[payload.mode.toUpperCase()] : payload.mode;
|
|
296
|
+
if (pulse == null || mode == null) throw new Error("meter_test: 需给出 pulse 与 mode");
|
|
297
|
+
const data = Buffer.from([slot & 0xFF, pulse & 0xFF, power & 0xFF, mode & 0xFF]);
|
|
298
|
+
return buildFrame(OAD.METER_TEST, data);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case 'conv_test': {
|
|
302
|
+
if (payload.itemContent) {
|
|
303
|
+
const data = buildDataFromItemContent(payload.itemContent);
|
|
304
|
+
return buildFrame(OAD.CONV_TEST, data);
|
|
305
|
+
}
|
|
306
|
+
if (payload.meterNo != null) {
|
|
307
|
+
const slot = (payload.slot ?? 0x01) & 0xFF;
|
|
308
|
+
const pulse = (typeof payload.pulse === 'string')
|
|
309
|
+
? ({ SEC: 0, DEMAND: 1, TARIFF: 2, HARMO_P: 3, HARMO_R: 4, REACTIVE: 5, ACTIVE: 6, EXIT: 0xFF }[payload.pulse] ?? 0x06)
|
|
310
|
+
: (payload.pulse ?? 0x06) & 0xFF;
|
|
311
|
+
const power = (payload.power ?? 0x01) & 0xFF;
|
|
312
|
+
const mode = (typeof payload.mode === 'string')
|
|
313
|
+
? ({ NORMAL: 0, FOLLOW: 1 }[payload.mode.toUpperCase()] ?? 0)
|
|
314
|
+
: (payload.mode ?? 0x00) & 0xFF;
|
|
315
|
+
const rfu1 = (payload.rfu1 ?? 0x01) & 0xFF;
|
|
316
|
+
const rfu2 = (payload.rfu2 ?? 0x01) & 0xFF;
|
|
317
|
+
const head6 = Buffer.from([slot, pulse, power, mode, rfu1, rfu2]);
|
|
318
|
+
const tail2 = tail2FromMeterNo(payload.meterNo);
|
|
319
|
+
return buildFrame(OAD.CONV_TEST, Buffer.concat([head6, tail2]));
|
|
320
|
+
}
|
|
321
|
+
const PULSE = { SEC: 0, DEMAND: 1, TARIFF: 2, HARMO_P: 3, HARMO_R: 4, REACTIVE: 5, ACTIVE: 6, EXIT: 0xFF };
|
|
322
|
+
const MODE = { NORMAL: 0, FOLLOW: 1 };
|
|
323
|
+
const slot = payload.slot ?? 1;
|
|
324
|
+
const pulse = typeof payload.pulse === 'string' ? PULSE[payload.pulse] : payload.pulse;
|
|
325
|
+
const power = payload.power ?? 0;
|
|
326
|
+
const mode = typeof payload.mode === 'string' ? MODE[payload.mode.toUpperCase()] : payload.mode;
|
|
327
|
+
if (pulse == null || mode == null) throw new Error("conv_test: 需给出 pulse 与 mode");
|
|
328
|
+
const data = Buffer.from([slot & 0xFF, pulse & 0xFF, power & 0xFF, mode & 0xFF]);
|
|
329
|
+
return buildFrame(OAD.CONV_TEST, data);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'set_baud': {
|
|
333
|
+
// 0x04 设置RS485波特率:DATA=1B(枚举 00..05),上电/初始化默认9600
|
|
334
|
+
const key = String(payload.baud || '').trim();
|
|
335
|
+
const code = BAUD[key];
|
|
336
|
+
if (code == null) throw new Error("set_baud: 波特率仅支持 2400/4800/9600/19200/38400/57600");
|
|
337
|
+
return buildFrame(OAD.SET_BAUD, Buffer.from([code]));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
case 'ver_mcu':
|
|
341
|
+
// 0x05 读管理单元固件版本(应答 4字节:HW(2)+SW(2),低字节在前)
|
|
342
|
+
return buildFrame(OAD.VER_MCU);
|
|
343
|
+
|
|
344
|
+
case 'ver_ble':
|
|
345
|
+
// 0x06 读蓝牙模块固件版本(应答同上)
|
|
346
|
+
return buildFrame(OAD.VER_BLE);
|
|
347
|
+
|
|
348
|
+
case 'prepare':
|
|
349
|
+
// 0x07 蓝牙检定预处理(说明:需随后轮询 0x08 获取最终状态)
|
|
350
|
+
return buildFrame(OAD.PREPARE);
|
|
351
|
+
|
|
352
|
+
case 'prepare_q':
|
|
353
|
+
// 0x08 查询预处理状态(应答:00成功/01失败/02处理中)
|
|
354
|
+
return buildFrame(OAD.PREPARE_Q);
|
|
355
|
+
|
|
356
|
+
default:
|
|
357
|
+
throw new Error("未知 oad:" + which);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// —— 解码器 ——
|
|
362
|
+
// 输入:Buffer 或 HEX 字符串
|
|
363
|
+
function decode(input) {
|
|
364
|
+
const buf = Buffer.isBuffer(input) ? input : Buffer.from(String(input).replace(/\s+/g, ''), 'hex');
|
|
365
|
+
const frame = parseFrame(buf);
|
|
366
|
+
|
|
367
|
+
const info = {
|
|
368
|
+
ok: frame.ok,
|
|
369
|
+
len: frame.len,
|
|
370
|
+
oadHex: '0x' + frame.oad.toString(16).padStart(2, '0'),
|
|
371
|
+
isResp: frame.isResp,
|
|
372
|
+
oadReq: frame.oadReq,
|
|
373
|
+
dataHex: frame.data.toString('hex').toUpperCase(),
|
|
374
|
+
csHex: '0x' + frame.cs.toString(16).padStart(2, '0'),
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// 针对常见应答附加解析
|
|
378
|
+
if (frame.isResp) {
|
|
379
|
+
// 统一结果码
|
|
380
|
+
info.result = frame.result;
|
|
381
|
+
|
|
382
|
+
switch (frame.oadReq) {
|
|
383
|
+
case OAD.RESET:
|
|
384
|
+
case OAD.CONNECT_METER:
|
|
385
|
+
case OAD.CONV_TEST:
|
|
386
|
+
case OAD.SET_BAUD:
|
|
387
|
+
case OAD.PREPARE:
|
|
388
|
+
// 这些应答通常只有 1B 结果
|
|
389
|
+
break;
|
|
390
|
+
|
|
391
|
+
case OAD.VER_MCU:
|
|
392
|
+
case OAD.VER_BLE:
|
|
393
|
+
if (frame.data.length >= 4) {
|
|
394
|
+
const hw = frame.data.readUInt16LE(0); // Vx.y → x=高字节?文档示例如 V1.0 => 0x0100
|
|
395
|
+
const sw = frame.data.readUInt16LE(2);
|
|
396
|
+
info.hwVer = `V${(hw >> 8)}.${(hw & 0xFF)}`;
|
|
397
|
+
info.swVer = `V${(sw >> 8)}.${(sw & 0xFF)}`;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
case OAD.PREPARE_Q:
|
|
402
|
+
// 00成功/01失败/02处理中
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
// 请求帧的常见数据解析
|
|
407
|
+
switch (frame.oadReq) {
|
|
408
|
+
case OAD.CONNECT_METER:
|
|
409
|
+
if (frame.data.length === 12 &&
|
|
410
|
+
frame.data[0] === 0x99 && frame.data[1] === 0x04 && frame.data[2] === 0x00 &&
|
|
411
|
+
frame.data[3] === 0x00 && frame.data[4] === 0x50 && frame.data[5] === 0x01) {
|
|
412
|
+
// 新版 12 字节格式
|
|
413
|
+
const ascii6 = frame.data.slice(6, 12).toString('ascii');
|
|
414
|
+
info.addrAscii6 = ascii6; // 如 "654321"
|
|
415
|
+
info.connectFmt = "12B(BLE)";
|
|
416
|
+
} else if (frame.data.length === 6) {
|
|
417
|
+
// 旧版 6 字节LE
|
|
418
|
+
const le = Buffer.from(frame.data);
|
|
419
|
+
const be = Buffer.from([...le].reverse());
|
|
420
|
+
info.addr = be.toString('hex').toUpperCase();
|
|
421
|
+
info.connectFmt = "6B(LE)";
|
|
422
|
+
}
|
|
423
|
+
break;
|
|
424
|
+
case OAD.METER_TEST:
|
|
425
|
+
case OAD.CONV_TEST:
|
|
426
|
+
if (frame.data.length === 4) {
|
|
427
|
+
info.slot = frame.data[0];
|
|
428
|
+
info.pulse = frame.data[1];
|
|
429
|
+
info.power = frame.data[2];
|
|
430
|
+
info.mode = frame.data[3];
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
case OAD.SET_BAUD:
|
|
434
|
+
info.baudCode = frame.data[0];
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return info;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// —— 主入口 ——
|
|
443
|
+
// 入参规范:
|
|
444
|
+
// 1) 编码:msg.payload = { action:'encode', oad:'connect', addr:'112233445566' }
|
|
445
|
+
// 返回 Buffer(也可改为 HEX 字符串)
|
|
446
|
+
// 2) 解码:msg.payload = <Buffer|HEX字符串>
|
|
447
|
+
// 主执行逻辑
|
|
448
|
+
function batchMsgBle(msg) {
|
|
449
|
+
try {
|
|
450
|
+
const pdata = msg.payload;
|
|
451
|
+
if (msg.mode == 'encode') {
|
|
452
|
+
|
|
453
|
+
if (Array.isArray(pdata)) {
|
|
454
|
+
let out = pdata.map((pd) => {
|
|
455
|
+
pd.payload = encode(pd)
|
|
456
|
+
return pd
|
|
457
|
+
})
|
|
458
|
+
msg.payload = out;
|
|
459
|
+
} else {
|
|
460
|
+
pdata.payload = encode(pdata);
|
|
461
|
+
msg.payload = pdata;
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
if (Array.isArray(pdata)) {
|
|
465
|
+
let out = pdata.map((pd) => {
|
|
466
|
+
return decode(pd)
|
|
467
|
+
})
|
|
468
|
+
msg.payload = out;
|
|
469
|
+
} else {
|
|
470
|
+
const out = decode(pdata);
|
|
471
|
+
msg.payload = out;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return msg;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
// console.log(err.message, msg);
|
|
477
|
+
msg.error = err.message;
|
|
478
|
+
return msg;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
module.exports = batchMsgBle;
|
|
483
|
+
module.exports.batchMsgBle = batchMsgBle;
|
package/icons/ble.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768563840650" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7668" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="30"><path d="M53.312 512a458.688 458.688 0 1 1 917.376 0A458.688 458.688 0 0 1 53.312 512zM584.96 301.824a356.16 356.16 0 0 0-39.808-26.688c-12.096-6.336-32-13.888-52.736-3.008-20.48 10.816-25.856 31.232-27.776 44.672-1.92 13.184-1.92 30.208-1.92 48.448v77.184l-57.92-49.6a32 32 0 0 0-41.6 48.64L445.44 512 363.2 582.4a32 32 0 1 0 41.6 48.64l57.92-49.6v77.184c0 18.24 0 35.328 1.92 48.512 1.92 13.44 7.232 33.856 27.776 44.608 20.736 10.88 40.64 3.328 52.736-2.944a356.48 356.48 0 0 0 39.808-26.688l39.424-28.8c10.624-7.744 21.312-15.552 29.056-23.104 8.64-8.576 18.56-21.568 18.56-40.064 0-18.56-9.92-31.552-18.56-40.064-7.68-7.552-18.432-15.36-29.056-23.168L548.992 512l75.392-54.976c10.624-7.744 21.312-15.552 29.056-23.168 8.64-8.512 18.56-21.504 18.56-40 0-18.56-9.92-31.552-18.56-40.064-7.68-7.616-18.432-15.36-29.056-23.168l-39.424-28.8zM526.72 367.36v64.768c0 7.36 0 11.008 2.368 12.16 2.304 1.28 5.248-0.896 11.2-5.248l44.8-32.704 8.32-6.08c3.968-2.944 5.952-4.416 5.952-6.528 0-2.176-1.984-3.648-5.952-6.528l-8.32-6.144-36.096-26.304a3344.064 3344.064 0 0 0-9.344-6.784c-5.44-3.968-8.192-5.952-10.496-4.8-2.368 1.152-2.368 4.544-2.368 11.328v12.864z m0 289.088V591.744c0-7.36 0-11.008 2.368-12.16 2.304-1.216 5.248 0.96 11.2 5.248l44.8 32.704 8.32 6.144c3.968 2.88 5.952 4.352 5.952 6.528 0 2.112-1.984 3.584-5.952 6.528l-8.32 6.08-36.096 26.368-9.344 6.784c-5.44 3.968-8.192 5.952-10.496 4.736-2.368-1.152-2.368-4.48-2.368-11.328v-12.8z" fill="#333333" p-id="7669"></path></svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-zelecproto",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "node-red zelecproto node",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"version": ">=1.3",
|
|
32
32
|
"nodes": {
|
|
33
33
|
"zelecproto": "zelecproto.js",
|
|
34
|
-
"zbatchproto": "zbatchproto.js"
|
|
34
|
+
"zbatchproto": "zbatchproto.js",
|
|
35
|
+
"zeleble": "zeleble.js"
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {}
|
package/zeleble.html
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="zeleble">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-inputoutput-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
|
|
4
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
|
|
5
|
+
</div>
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<script type="text/javascript">
|
|
9
|
+
RED.nodes.registerType('zeleble',{
|
|
10
|
+
category: 'zutils',
|
|
11
|
+
defaults: {
|
|
12
|
+
name: {name:""}
|
|
13
|
+
},
|
|
14
|
+
color:"BurlyWood",
|
|
15
|
+
inputs:1,
|
|
16
|
+
outputs:1,
|
|
17
|
+
icon: "ble.svg",
|
|
18
|
+
align: "left",
|
|
19
|
+
label: function() {
|
|
20
|
+
return this.name || "zeleble";
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
package/zeleble.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
"use strict";
|
|
3
|
+
var protoBle = require("./ble");
|
|
4
|
+
|
|
5
|
+
function zelecproto(n) {
|
|
6
|
+
RED.nodes.createNode(this, n);
|
|
7
|
+
var node = this;
|
|
8
|
+
|
|
9
|
+
this.on("input", function (msg, send, done) {
|
|
10
|
+
msg = protoBle(msg);
|
|
11
|
+
send(msg);
|
|
12
|
+
done();
|
|
13
|
+
});
|
|
14
|
+
this.on('close', () => {
|
|
15
|
+
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
RED.nodes.registerType("zeleble", zeleble);
|
|
20
|
+
|
|
21
|
+
}
|