node-red-contrib-symi-mesh 1.2.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/LICENSE +22 -0
- package/README.md +592 -0
- package/examples/knx-sync-example.json +465 -0
- package/lib/device-manager.js +575 -0
- package/lib/mqtt-helper.js +659 -0
- package/lib/protocol.js +510 -0
- package/lib/serial-client.js +286 -0
- package/lib/tcp-client.js +262 -0
- package/nodes/symi-device.html +303 -0
- package/nodes/symi-device.js +344 -0
- package/nodes/symi-gateway.html +83 -0
- package/nodes/symi-gateway.js +450 -0
- package/nodes/symi-mqtt.html +94 -0
- package/nodes/symi-mqtt.js +1113 -0
- package/package.json +58 -0
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symi Gateway Protocol Implementation
|
|
3
|
+
* Converted from Python to JavaScript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PROTOCOL_HEADER = 0x53;
|
|
7
|
+
const MIN_FRAME_LENGTH = 4;
|
|
8
|
+
|
|
9
|
+
// Operation codes
|
|
10
|
+
const OP_READ_DEVICE_LIST = 0x12;
|
|
11
|
+
const OP_DEVICE_CONTROL = 0x30;
|
|
12
|
+
const OP_TRANSPARENT_CONTROL = 0x40;
|
|
13
|
+
const OP_EVENT_NODE_STATUS = 0x80;
|
|
14
|
+
const OP_EVENT_TRANSPARENT_MSG = 0xC0;
|
|
15
|
+
const OP_RESP_DEVICE_LIST = 0x92;
|
|
16
|
+
const OP_RESP_DEVICE_CONTROL = 0xB0;
|
|
17
|
+
const OP_SCENE_CONTROL = 0x31;
|
|
18
|
+
const OP_DEVICE_STATUS_QUERY = 0x32;
|
|
19
|
+
|
|
20
|
+
class ProtocolFrame {
|
|
21
|
+
constructor(header, opcode, length, payload, checksum, status = null) {
|
|
22
|
+
this.header = header;
|
|
23
|
+
this.opcode = opcode;
|
|
24
|
+
this.length = length;
|
|
25
|
+
this.payload = payload;
|
|
26
|
+
this.checksum = checksum;
|
|
27
|
+
this.status = status;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
isResponse() {
|
|
31
|
+
return this.opcode >= 0x81;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isEvent() {
|
|
35
|
+
return this.opcode === OP_EVENT_NODE_STATUS ||
|
|
36
|
+
this.opcode === OP_EVENT_TRANSPARENT_MSG ||
|
|
37
|
+
(this.opcode === 0x90 && [0x02, 0x03, 0x04, 0x05, 0x06].includes(this.status));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isDeviceStatusEvent() {
|
|
41
|
+
return this.opcode === OP_EVENT_NODE_STATUS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
isTransparentEvent() {
|
|
45
|
+
return this.opcode === OP_EVENT_TRANSPARENT_MSG;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class ProtocolHandler {
|
|
50
|
+
constructor() {
|
|
51
|
+
this.buffer = Buffer.alloc(0);
|
|
52
|
+
this.lastDataTime = 0;
|
|
53
|
+
this.bufferTimeout = 10000; // 10 seconds
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 构建开关状态值
|
|
58
|
+
* @param {number} channels - 开关路数 (1-6)
|
|
59
|
+
* @param {number} targetChannel - 目标路数 (1-6)
|
|
60
|
+
* @param {boolean} targetState - 目标状态 (true=开, false=关)
|
|
61
|
+
* @param {number|null} currentState - 当前状态值,null时使用默认全关状态
|
|
62
|
+
* @returns {number|Buffer} - 1-4路返回number,6路返回Buffer(2字节)
|
|
63
|
+
*/
|
|
64
|
+
buildSwitchState(channels, targetChannel, targetState, currentState = null) {
|
|
65
|
+
// 验证参数
|
|
66
|
+
if (channels < 1 || channels > 6) {
|
|
67
|
+
throw new Error(`Invalid channels count: ${channels}, must be 1-6`);
|
|
68
|
+
}
|
|
69
|
+
if (targetChannel < 1 || targetChannel > channels) {
|
|
70
|
+
throw new Error(`Invalid target channel: ${targetChannel}, must be 1-${channels}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 单路开关直接返回状态
|
|
74
|
+
if (channels === 1) {
|
|
75
|
+
return targetState ? 0x02 : 0x01;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 多路开关状态组合
|
|
79
|
+
let stateValue;
|
|
80
|
+
|
|
81
|
+
if (channels <= 4) {
|
|
82
|
+
// 1-4路开关使用1字节,默认全关状态
|
|
83
|
+
const defaultStates = {
|
|
84
|
+
2: 0x05, // 01 01 (2路全关)
|
|
85
|
+
3: 0x15, // 01 01 01 (3路全关)
|
|
86
|
+
4: 0x55 // 01 01 01 01 (4路全关)
|
|
87
|
+
};
|
|
88
|
+
stateValue = currentState !== null ? currentState : defaultStates[channels];
|
|
89
|
+
|
|
90
|
+
// 计算目标路的位位置 (每路2位)
|
|
91
|
+
const bitPos = (targetChannel - 1) * 2;
|
|
92
|
+
const mask = ~(0x03 << bitPos); // 清除目标位
|
|
93
|
+
const newBits = (targetState ? 0x02 : 0x01) << bitPos; // 新状态位
|
|
94
|
+
|
|
95
|
+
stateValue = (stateValue & mask) | newBits;
|
|
96
|
+
return stateValue;
|
|
97
|
+
} else if (channels === 6) {
|
|
98
|
+
// 6路开关,2字节状态,小端序
|
|
99
|
+
const defaultState = 0x5555; // 全关状态
|
|
100
|
+
stateValue = currentState !== null ? currentState : defaultState;
|
|
101
|
+
|
|
102
|
+
// 6路开关状态组合
|
|
103
|
+
const bitPos = (targetChannel - 1) * 2;
|
|
104
|
+
const mask = ~(0x03 << bitPos);
|
|
105
|
+
const newBits = (targetState ? 0x02 : 0x01) << bitPos;
|
|
106
|
+
|
|
107
|
+
stateValue = (stateValue & mask) | newBits;
|
|
108
|
+
|
|
109
|
+
// 返回小端序Buffer
|
|
110
|
+
return Buffer.from([stateValue & 0xFF, (stateValue >> 8) & 0xFF]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 解析开关状态值
|
|
116
|
+
* @param {number|Buffer} stateValue - 状态值
|
|
117
|
+
* @param {number} channels - 开关路数
|
|
118
|
+
* @returns {Array<boolean>} - 每路的开关状态数组
|
|
119
|
+
*/
|
|
120
|
+
parseSwitchState(stateValue, channels) {
|
|
121
|
+
let value;
|
|
122
|
+
if (Buffer.isBuffer(stateValue)) {
|
|
123
|
+
// 6路开关,小端序解析
|
|
124
|
+
value = stateValue[0] | (stateValue[1] << 8);
|
|
125
|
+
} else {
|
|
126
|
+
value = stateValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const states = [];
|
|
130
|
+
for (let i = 0; i < channels; i++) {
|
|
131
|
+
const bitPos = i * 2;
|
|
132
|
+
const bits = (value >> bitPos) & 0x03;
|
|
133
|
+
states.push(bits === 0x02); // 0x02=开, 0x01=关
|
|
134
|
+
}
|
|
135
|
+
return states;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
calculateChecksum(data) {
|
|
139
|
+
let checksum = 0;
|
|
140
|
+
for (let i = 0; i < data.length; i++) {
|
|
141
|
+
checksum ^= data[i];
|
|
142
|
+
}
|
|
143
|
+
return checksum & 0xFF;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
buildFrame(opcode, data = Buffer.alloc(0)) {
|
|
147
|
+
const header = PROTOCOL_HEADER;
|
|
148
|
+
const length = data.length;
|
|
149
|
+
|
|
150
|
+
// Build frame without checksum
|
|
151
|
+
const frameWithoutChecksum = Buffer.concat([
|
|
152
|
+
Buffer.from([header, opcode, length]),
|
|
153
|
+
data
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
// Calculate checksum
|
|
157
|
+
const checksum = this.calculateChecksum(frameWithoutChecksum);
|
|
158
|
+
|
|
159
|
+
// Build complete frame
|
|
160
|
+
return Buffer.concat([frameWithoutChecksum, Buffer.from([checksum])]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
parseFrame(data) {
|
|
164
|
+
if (data.length < MIN_FRAME_LENGTH) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const header = data[0];
|
|
169
|
+
if (header !== PROTOCOL_HEADER) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const opcode = data[1];
|
|
174
|
+
|
|
175
|
+
// 0x80事件和响应都使用响应格式:53 [opcode] [status/sub_opcode] [length] [data] [checksum]
|
|
176
|
+
if (opcode >= 0x80) {
|
|
177
|
+
if (data.length < 5) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const status = data[2];
|
|
182
|
+
const paramLength = data[3];
|
|
183
|
+
const expectedLength = 5 + paramLength;
|
|
184
|
+
|
|
185
|
+
if (data.length !== expectedLength) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payload = data.slice(4, 4 + paramLength);
|
|
190
|
+
const checksum = data[4 + paramLength];
|
|
191
|
+
|
|
192
|
+
// Verify checksum
|
|
193
|
+
const calculatedChecksum = this.calculateChecksum(data.slice(0, -1));
|
|
194
|
+
if (checksum !== calculatedChecksum) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return new ProtocolFrame(header, opcode, paramLength, payload, checksum, status);
|
|
199
|
+
} else { // Send format
|
|
200
|
+
const paramLength = data[2];
|
|
201
|
+
const expectedLength = 4 + paramLength;
|
|
202
|
+
|
|
203
|
+
if (data.length !== expectedLength) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const payload = data.slice(3, 3 + paramLength);
|
|
208
|
+
const checksum = data[3 + paramLength];
|
|
209
|
+
|
|
210
|
+
const calculatedChecksum = this.calculateChecksum(data.slice(0, -1));
|
|
211
|
+
if (checksum !== calculatedChecksum) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new ProtocolFrame(header, opcode, paramLength, payload, checksum);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
addData(data) {
|
|
220
|
+
const currentTime = Date.now();
|
|
221
|
+
|
|
222
|
+
// Check buffer timeout
|
|
223
|
+
if (this.buffer.length > 0 && (currentTime - this.lastDataTime) > this.bufferTimeout) {
|
|
224
|
+
this.buffer = Buffer.alloc(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
228
|
+
this.lastDataTime = currentTime;
|
|
229
|
+
const frames = [];
|
|
230
|
+
|
|
231
|
+
while (this.buffer.length >= MIN_FRAME_LENGTH) {
|
|
232
|
+
// Look for frame header
|
|
233
|
+
let headerIndex = -1;
|
|
234
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
235
|
+
if (this.buffer[i] === PROTOCOL_HEADER) {
|
|
236
|
+
headerIndex = i;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (headerIndex === -1) {
|
|
242
|
+
if (this.buffer.length > 2) {
|
|
243
|
+
this.buffer = this.buffer.slice(-2);
|
|
244
|
+
} else {
|
|
245
|
+
this.buffer = Buffer.alloc(0);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (headerIndex > 0) {
|
|
251
|
+
this.buffer = this.buffer.slice(headerIndex);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.buffer.length < 3) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const opcode = this.buffer[1];
|
|
259
|
+
let totalFrameLength;
|
|
260
|
+
|
|
261
|
+
// 0x80和0xC0事件使用响应格式(5字节头部)
|
|
262
|
+
// 0x81及以上的响应也使用响应格式(5字节头部)
|
|
263
|
+
if (opcode >= 0x80) {
|
|
264
|
+
if (this.buffer.length < 4) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
const paramLength = this.buffer[3];
|
|
268
|
+
totalFrameLength = 5 + paramLength;
|
|
269
|
+
} else {
|
|
270
|
+
// 发送格式(4字节头部)
|
|
271
|
+
const paramLength = this.buffer[2];
|
|
272
|
+
totalFrameLength = 4 + paramLength;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (totalFrameLength > 1024) {
|
|
276
|
+
this.buffer = this.buffer.slice(1);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.buffer.length < totalFrameLength) {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const frameData = this.buffer.slice(0, totalFrameLength);
|
|
285
|
+
const frame = this.parseFrame(frameData);
|
|
286
|
+
|
|
287
|
+
if (frame) {
|
|
288
|
+
frames.push(frame);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.buffer = this.buffer.slice(totalFrameLength);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return frames;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
clearBuffer() {
|
|
298
|
+
this.buffer = Buffer.alloc(0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
buildReadDeviceListFrame() {
|
|
302
|
+
return this.buildFrame(OP_READ_DEVICE_LIST);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
buildDeviceControlFrame(networkAddr, attrType, param = Buffer.alloc(0)) {
|
|
306
|
+
// 地址使用小端序(低字节在前)
|
|
307
|
+
const addrLow = networkAddr & 0xFF;
|
|
308
|
+
const addrHigh = (networkAddr >> 8) & 0xFF;
|
|
309
|
+
|
|
310
|
+
let data;
|
|
311
|
+
|
|
312
|
+
if (attrType === 0x02) { // Switch control
|
|
313
|
+
// 开关控制需要正确的状态组合
|
|
314
|
+
// param格式: [channels, targetChannel, targetState, currentState(可选)]
|
|
315
|
+
// 或者直接传入计算好的状态值: [stateValue] 或 [stateLow, stateHigh]
|
|
316
|
+
|
|
317
|
+
if (param.length >= 3 && param[0] <= 6 && param[1] <= 6 && (param[2] === 0 || param[2] === 1)) {
|
|
318
|
+
// 使用状态组合算法 - 检查前3个参数是否符合状态组合格式
|
|
319
|
+
const channels = param[0];
|
|
320
|
+
const targetChannel = param[1];
|
|
321
|
+
const targetState = param[2] === 1;
|
|
322
|
+
const currentState = param.length > 3 ? param[3] : null;
|
|
323
|
+
|
|
324
|
+
const stateValue = this.buildSwitchState(channels, targetChannel, targetState, currentState);
|
|
325
|
+
|
|
326
|
+
if (Buffer.isBuffer(stateValue)) {
|
|
327
|
+
// 6路开关,2字节状态
|
|
328
|
+
data = Buffer.from([addrLow, addrHigh, 0x02, stateValue[0], stateValue[1]]);
|
|
329
|
+
} else {
|
|
330
|
+
// 1-4路开关,1字节状态
|
|
331
|
+
data = Buffer.from([addrLow, addrHigh, 0x02, stateValue]);
|
|
332
|
+
}
|
|
333
|
+
} else if (param.length === 2) {
|
|
334
|
+
// 6路开关直接状态值 (2字节)
|
|
335
|
+
data = Buffer.from([addrLow, addrHigh, 0x02, param[0], param[1]]);
|
|
336
|
+
} else {
|
|
337
|
+
// 1-4路开关直接状态值 (1字节)
|
|
338
|
+
const stateValue = param.length > 0 ? param[0] : 0x01;
|
|
339
|
+
data = Buffer.from([addrLow, addrHigh, 0x02, stateValue]);
|
|
340
|
+
}
|
|
341
|
+
} else if (attrType === 0x03 || attrType === 0x04) { // Brightness/Color temp
|
|
342
|
+
// 亮度和色温控制,参数范围0-100
|
|
343
|
+
const value = param.length > 0 ? Math.max(0, Math.min(100, param[0])) : 0;
|
|
344
|
+
data = Buffer.from([addrLow, addrHigh, attrType, value]);
|
|
345
|
+
} else if (attrType === 0x05) { // Curtain action
|
|
346
|
+
// 窗帘动作控制: 1=打开, 2=关闭, 3=停止
|
|
347
|
+
const action = param.length > 0 ? Math.max(1, Math.min(3, param[0])) : 1;
|
|
348
|
+
data = Buffer.from([addrLow, addrHigh, 0x05, action]);
|
|
349
|
+
} else if (attrType === 0x06) { // Curtain position
|
|
350
|
+
// 窗帘位置控制: 0-100百分比, 0xFF=未知
|
|
351
|
+
const position = param.length > 0 ? Math.max(0, Math.min(100, param[0])) : 0;
|
|
352
|
+
data = Buffer.from([addrLow, addrHigh, 0x06, position]);
|
|
353
|
+
} else if (attrType === 0x4C) { // RGB control (五色调光)
|
|
354
|
+
// 根据HA集成验证:五色调光RGB控制使用0x4C,5字节数据 [R, G, B, WW, CW]
|
|
355
|
+
// RGB范围0-255,WW和CW范围0-255
|
|
356
|
+
if (param.length >= 5) {
|
|
357
|
+
const rgbData = Buffer.from([
|
|
358
|
+
Math.max(0, Math.min(255, param[0])), // R: 0-255
|
|
359
|
+
Math.max(0, Math.min(255, param[1])), // G: 0-255
|
|
360
|
+
Math.max(0, Math.min(255, param[2])), // B: 0-255
|
|
361
|
+
Math.max(0, Math.min(255, param[3])), // WW: 0-255
|
|
362
|
+
Math.max(0, Math.min(255, param[4])) // CW: 0-255
|
|
363
|
+
]);
|
|
364
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, 0x4C]), rgbData]);
|
|
365
|
+
} else if (param.length >= 3) {
|
|
366
|
+
// 只有RGB,没有WW和CW
|
|
367
|
+
const rgbData = Buffer.from([
|
|
368
|
+
Math.max(0, Math.min(255, param[0])),
|
|
369
|
+
Math.max(0, Math.min(255, param[1])),
|
|
370
|
+
Math.max(0, Math.min(255, param[2])),
|
|
371
|
+
0, // WW默认0
|
|
372
|
+
0 // CW默认0
|
|
373
|
+
]);
|
|
374
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, 0x4C]), rgbData]);
|
|
375
|
+
} else {
|
|
376
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, 0x4C]), Buffer.from([0, 0, 0, 0, 0])]);
|
|
377
|
+
}
|
|
378
|
+
} else if (attrType === 0x1B) { // Climate temp (TMPC_TEMP)
|
|
379
|
+
// 根据协议文档3.5.1.12:1Byte 设置温度(16-30°C)
|
|
380
|
+
// 注意:这是直接温度值,不是温度*100!
|
|
381
|
+
const temp = param.length > 0 ? Math.max(16, Math.min(30, param[0])) : 20;
|
|
382
|
+
data = Buffer.from([addrLow, addrHigh, attrType, temp]);
|
|
383
|
+
} else if (attrType === 0x1C) { // Climate fan
|
|
384
|
+
const fanSpeed = param.length > 0 ? Math.max(1, Math.min(4, param[0])) : 1;
|
|
385
|
+
data = Buffer.from([addrLow, addrHigh, attrType, fanSpeed]);
|
|
386
|
+
} else if (attrType === 0x1D) { // Climate mode
|
|
387
|
+
const mode = param.length > 0 ? Math.max(1, Math.min(4, param[0])) : 1;
|
|
388
|
+
data = Buffer.from([addrLow, addrHigh, attrType, mode]);
|
|
389
|
+
} else if (attrType === 0x94) { // 三合一设备控制
|
|
390
|
+
// param格式: [subType, command, value]
|
|
391
|
+
// subType: 1=空调, 2=新风, 3=地暖
|
|
392
|
+
// command: 控制类型(开关、温度、模式等)
|
|
393
|
+
if (param.length >= 3) {
|
|
394
|
+
const subType = param[0];
|
|
395
|
+
const command = param[1];
|
|
396
|
+
const value = param[2];
|
|
397
|
+
|
|
398
|
+
// 构建三合一控制数据包
|
|
399
|
+
// 这里需要根据实际设备协议调整
|
|
400
|
+
const controlData = Buffer.from([subType, command, value]);
|
|
401
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, attrType]), controlData]);
|
|
402
|
+
} else {
|
|
403
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, attrType]), param]);
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
data = Buffer.concat([Buffer.from([addrLow, addrHigh, attrType]), param]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return this.buildFrame(OP_DEVICE_CONTROL, data);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
buildSceneControlFrame(sceneId) {
|
|
413
|
+
return this.buildFrame(OP_SCENE_CONTROL, Buffer.from([sceneId]));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
buildDeviceStatusQueryFrame(networkAddr, msgType = 0x00) {
|
|
417
|
+
// 地址使用小端序(低字节在前)
|
|
418
|
+
const addrLow = networkAddr & 0xFF;
|
|
419
|
+
const addrHigh = (networkAddr >> 8) & 0xFF;
|
|
420
|
+
|
|
421
|
+
if (msgType === 0x00) {
|
|
422
|
+
return this.buildFrame(OP_DEVICE_STATUS_QUERY, Buffer.from([addrLow, addrHigh]));
|
|
423
|
+
} else {
|
|
424
|
+
return this.buildFrame(OP_DEVICE_STATUS_QUERY, Buffer.from([addrLow, addrHigh, msgType]));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 构建开关控制帧(便捷方法)
|
|
430
|
+
* @param {number} networkAddr - 网络地址
|
|
431
|
+
* @param {number} channels - 开关路数 (1-6)
|
|
432
|
+
* @param {number} targetChannel - 目标路数 (1-6)
|
|
433
|
+
* @param {boolean} targetState - 目标状态 (true=开, false=关)
|
|
434
|
+
* @param {number|null} currentState - 当前状态值,null时需要先查询
|
|
435
|
+
* @returns {Buffer} - 控制帧数据
|
|
436
|
+
*/
|
|
437
|
+
buildSwitchControlFrame(networkAddr, channels, targetChannel, targetState, currentState = null) {
|
|
438
|
+
const param = Buffer.from([channels, targetChannel, targetState ? 1 : 0]);
|
|
439
|
+
if (currentState !== null) {
|
|
440
|
+
const paramWithState = Buffer.concat([param, Buffer.from([currentState])]);
|
|
441
|
+
return this.buildDeviceControlFrame(networkAddr, 0x02, paramWithState);
|
|
442
|
+
} else {
|
|
443
|
+
return this.buildDeviceControlFrame(networkAddr, 0x02, param);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function parseStatusEvent(frame) {
|
|
449
|
+
if (!frame.isEvent() || frame.opcode !== OP_EVENT_NODE_STATUS) {
|
|
450
|
+
throw new Error('Not a status event frame');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (frame.payload.length < 3) {
|
|
454
|
+
throw new Error('Status event payload too short');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 0x80事件格式: 53 80 [status/sub_opcode] [length] [payload] [checksum]
|
|
458
|
+
// payload部分: [addr_low] [addr_high] [msg_type] [data...]
|
|
459
|
+
// 地址使用小端序(低字节在前)
|
|
460
|
+
//
|
|
461
|
+
// 例: 53 80 05 04 9A 01 02 15 5E
|
|
462
|
+
// opcode=80, status=05(sub_opcode), length=04
|
|
463
|
+
// payload=[9A 01 02 15](4字节)
|
|
464
|
+
// 9A 01=地址(小端序)=0x019A, 02=消息类型, 15=参数
|
|
465
|
+
const addrLow = frame.payload[0];
|
|
466
|
+
const addrHigh = frame.payload[1];
|
|
467
|
+
const networkAddress = addrLow | (addrHigh << 8);
|
|
468
|
+
const attrType = frame.payload[2];
|
|
469
|
+
const parameters = frame.payload.length > 3 ? frame.payload.slice(3) : Buffer.alloc(0);
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
networkAddress,
|
|
473
|
+
attrType,
|
|
474
|
+
parameters,
|
|
475
|
+
subOpcode: frame.status
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function parseTransparentEvent(frame) {
|
|
480
|
+
if (!frame.isEvent() || frame.opcode !== OP_EVENT_TRANSPARENT_MSG) {
|
|
481
|
+
throw new Error('Not a transparent event frame');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (frame.payload.length < 2) {
|
|
485
|
+
throw new Error('Transparent event payload too short');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const networkAddress = (frame.payload[0] << 8) | frame.payload[1];
|
|
489
|
+
const transparentData = frame.payload.length > 2 ? frame.payload.slice(2) : Buffer.alloc(0);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
networkAddress,
|
|
493
|
+
transparentData
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
ProtocolHandler,
|
|
499
|
+
ProtocolFrame,
|
|
500
|
+
parseStatusEvent,
|
|
501
|
+
parseTransparentEvent,
|
|
502
|
+
PROTOCOL_HEADER,
|
|
503
|
+
OP_READ_DEVICE_LIST,
|
|
504
|
+
OP_DEVICE_CONTROL,
|
|
505
|
+
OP_EVENT_NODE_STATUS,
|
|
506
|
+
OP_RESP_DEVICE_LIST,
|
|
507
|
+
OP_SCENE_CONTROL,
|
|
508
|
+
OP_DEVICE_STATUS_QUERY
|
|
509
|
+
};
|
|
510
|
+
|