node-red-contrib-symi-mesh 1.3.1 → 1.6.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.
@@ -0,0 +1,776 @@
1
+ /**
2
+ * Symi RS485 Bridge Node - Mesh与RS485设备双向同步桥接
3
+ * 使用配置节点管理RS485连接(与Mesh网关相同的配置方式)
4
+ * 事件驱动架构,命令队列顺序处理
5
+ */
6
+
7
+ const { SerialPort } = require('serialport');
8
+
9
+ module.exports = function(RED) {
10
+
11
+ // 协议模板定义 - 按品牌/小区组织,不含房间名,只有设备类型标准模板
12
+ const PROTOCOLS = {
13
+ brands: {
14
+ 'huayuqianwan': {
15
+ name: '话语前湾',
16
+ devices: {
17
+ // ===== 开关类型 (A4B3协议头,FC=06写单寄存器) =====
18
+ // 按键: 0x1031-0x1036 (按键1-6)
19
+ // 指示灯: 0x1021-0x1026 (指示灯1-6)
20
+ 'switch_1': {
21
+ name: '一键开关',
22
+ type: 'switch',
23
+ channels: 1,
24
+ registers: {
25
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
26
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 }
27
+ }
28
+ },
29
+ 'switch_2': {
30
+ name: '二键开关',
31
+ type: 'switch',
32
+ channels: 2,
33
+ registers: {
34
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
35
+ switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
36
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
37
+ led2: { address: 0x1022, type: 'holding', on: 1, off: 0 }
38
+ }
39
+ },
40
+ 'switch_3': {
41
+ name: '三键开关',
42
+ type: 'switch',
43
+ channels: 3,
44
+ registers: {
45
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
46
+ switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
47
+ switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
48
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
49
+ led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
50
+ led3: { address: 0x1023, type: 'holding', on: 1, off: 0 }
51
+ }
52
+ },
53
+ 'switch_4': {
54
+ name: '四键开关',
55
+ type: 'switch',
56
+ channels: 4,
57
+ registers: {
58
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
59
+ switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
60
+ switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
61
+ switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 },
62
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
63
+ led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
64
+ led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
65
+ led4: { address: 0x1024, type: 'holding', on: 1, off: 0 }
66
+ }
67
+ },
68
+ 'switch_6': {
69
+ name: '六键开关',
70
+ type: 'switch',
71
+ channels: 6,
72
+ registers: {
73
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
74
+ switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
75
+ switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
76
+ switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 },
77
+ switch5: { address: 0x1035, type: 'holding', on: 1, off: 0 },
78
+ switch6: { address: 0x1036, type: 'holding', on: 1, off: 0 },
79
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
80
+ led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
81
+ led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
82
+ led4: { address: 0x1024, type: 'holding', on: 1, off: 0 },
83
+ led5: { address: 0x1025, type: 'holding', on: 1, off: 0 },
84
+ led6: { address: 0x1026, type: 'holding', on: 1, off: 0 }
85
+ }
86
+ },
87
+ 'switch_8': {
88
+ name: '八键开关',
89
+ type: 'switch',
90
+ channels: 8,
91
+ registers: {
92
+ switch1: { address: 0x1031, type: 'holding', on: 1, off: 0 },
93
+ switch2: { address: 0x1032, type: 'holding', on: 1, off: 0 },
94
+ switch3: { address: 0x1033, type: 'holding', on: 1, off: 0 },
95
+ switch4: { address: 0x1034, type: 'holding', on: 1, off: 0 },
96
+ switch5: { address: 0x1035, type: 'holding', on: 1, off: 0 },
97
+ switch6: { address: 0x1036, type: 'holding', on: 1, off: 0 },
98
+ switch7: { address: 0x1037, type: 'holding', on: 1, off: 0 },
99
+ switch8: { address: 0x1038, type: 'holding', on: 1, off: 0 },
100
+ led1: { address: 0x1021, type: 'holding', on: 1, off: 0 },
101
+ led2: { address: 0x1022, type: 'holding', on: 1, off: 0 },
102
+ led3: { address: 0x1023, type: 'holding', on: 1, off: 0 },
103
+ led4: { address: 0x1024, type: 'holding', on: 1, off: 0 },
104
+ led5: { address: 0x1025, type: 'holding', on: 1, off: 0 },
105
+ led6: { address: 0x1026, type: 'holding', on: 1, off: 0 },
106
+ led7: { address: 0x1027, type: 'holding', on: 1, off: 0 },
107
+ led8: { address: 0x1028, type: 'holding', on: 1, off: 0 }
108
+ }
109
+ },
110
+ // ===== 调光类型 =====
111
+ 'dimmer_single': {
112
+ name: '调光-单色',
113
+ type: 'light',
114
+ registers: {
115
+ switch: { address: 0x1031, type: 'holding', on: 1, off: 0 },
116
+ brightness: { address: 0x1032, type: 'holding', min: 0, max: 100 }
117
+ }
118
+ },
119
+ 'dimmer_cct': {
120
+ name: '调光-双色',
121
+ type: 'light',
122
+ registers: {
123
+ switch: { address: 0x1031, type: 'holding', on: 1, off: 0 },
124
+ brightness: { address: 0x1032, type: 'holding', min: 0, max: 100 },
125
+ colorTemp: { address: 0x1033, type: 'holding', min: 2700, max: 6500 }
126
+ }
127
+ },
128
+ 'dimmer_rgb': {
129
+ name: '调光-彩色',
130
+ type: 'light',
131
+ registers: {
132
+ switch: { address: 0x1031, type: 'holding', on: 1, off: 0 },
133
+ brightness: { address: 0x1032, type: 'holding', min: 0, max: 100 },
134
+ colorR: { address: 0x1033, type: 'holding', min: 0, max: 255 },
135
+ colorG: { address: 0x1034, type: 'holding', min: 0, max: 255 },
136
+ colorB: { address: 0x1035, type: 'holding', min: 0, max: 255 }
137
+ }
138
+ },
139
+ // ===== 空调 (A5B5协议头) =====
140
+ // 模式: 1=制热, 2=制冷, 4=送风, 8=除湿
141
+ // 风速: 1=低风, 2=中风, 3=高风
142
+ 'ac_living': {
143
+ name: '客厅空调',
144
+ type: 'climate',
145
+ registers: {
146
+ switch: { address: 0x0FA0, type: 'holding', on: 1, off: 0 },
147
+ mode: { address: 0x0FA1, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
148
+ targetTemp: { address: 0x0FA2, type: 'holding' },
149
+ fanSpeed: { address: 0x0FA3, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
150
+ }
151
+ },
152
+ 'ac_bedroom2_1': {
153
+ name: '次卧1空调',
154
+ type: 'climate',
155
+ registers: {
156
+ switch: { address: 0x0FA4, type: 'holding', on: 1, off: 0 },
157
+ mode: { address: 0x0FA5, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
158
+ targetTemp: { address: 0x0FA6, type: 'holding' },
159
+ fanSpeed: { address: 0x0FA7, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
160
+ }
161
+ },
162
+ 'ac_bedroom2_2': {
163
+ name: '次卧2空调',
164
+ type: 'climate',
165
+ registers: {
166
+ switch: { address: 0x0FA8, type: 'holding', on: 1, off: 0 },
167
+ mode: { address: 0x0FA9, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
168
+ targetTemp: { address: 0x0FAA, type: 'holding' },
169
+ fanSpeed: { address: 0x0FAB, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
170
+ }
171
+ },
172
+ 'ac_master': {
173
+ name: '主卧空调',
174
+ type: 'climate',
175
+ registers: {
176
+ switch: { address: 0x0FAC, type: 'holding', on: 1, off: 0 },
177
+ mode: { address: 0x0FAD, type: 'holding', map: { 1: 'heat', 2: 'cool', 4: 'fan', 8: 'dry' } },
178
+ targetTemp: { address: 0x0FAE, type: 'holding' },
179
+ fanSpeed: { address: 0x0FAF, type: 'holding', map: { 1: 'low', 2: 'medium', 3: 'high' } }
180
+ }
181
+ },
182
+ // ===== 地暖 (A3B3协议头) =====
183
+ 'floor_heating': {
184
+ name: '地暖',
185
+ type: 'climate',
186
+ registers: {
187
+ switch: { address: 0x0039, type: 'holding', on: 2, off: 0 },
188
+ targetTemp: { address: 0x0043, type: 'holding' }
189
+ }
190
+ },
191
+ // ===== 新风 (A3B3协议头) =====
192
+ 'fresh_air': {
193
+ name: '新风',
194
+ type: 'fan',
195
+ registers: {
196
+ switch: { address: 0x0039, type: 'holding', on: 1, off: 0 },
197
+ fanSpeed: { address: 0x004B, type: 'holding', map: { 0: 'low', 2: 'high' } }
198
+ }
199
+ },
200
+ // ===== 窗帘 (A6B6协议头) =====
201
+ 'curtain': {
202
+ name: '窗帘',
203
+ type: 'cover',
204
+ registers: {
205
+ position: { address: 0x0003, type: 'holding', open: 1, close: 2, stop: 3 }
206
+ }
207
+ },
208
+ // ===== 场景 =====
209
+ 'scene': {
210
+ name: '场景',
211
+ type: 'scene',
212
+ registers: {
213
+ trigger: { address: 0x0000, type: 'holding' }
214
+ }
215
+ }
216
+ }
217
+ },
218
+ // ===== 通用协议(标准Modbus) =====
219
+ 'generic': {
220
+ name: '通用Modbus',
221
+ devices: {
222
+ 'switch_1': { name: '开关-1键', type: 'switch', registers: { switch: { address: 0x0000, type: 'coil' } } },
223
+ 'switch_2': { name: '开关-2键', type: 'switch', registers: { switch1: { address: 0x0000, type: 'coil' }, switch2: { address: 0x0001, type: 'coil' } } },
224
+ 'switch_3': { name: '开关-3键', type: 'switch', registers: { switch1: { address: 0x0000, type: 'coil' }, switch2: { address: 0x0001, type: 'coil' }, switch3: { address: 0x0002, type: 'coil' } } },
225
+ 'switch_4': { name: '开关-4键', type: 'switch', registers: { switch1: { address: 0x0000, type: 'coil' }, switch2: { address: 0x0001, type: 'coil' }, switch3: { address: 0x0002, type: 'coil' }, switch4: { address: 0x0003, type: 'coil' } } },
226
+ 'switch_6': { name: '开关-6键', type: 'switch', registers: { switch1: { address: 0x0000, type: 'coil' }, switch2: { address: 0x0001, type: 'coil' }, switch3: { address: 0x0002, type: 'coil' }, switch4: { address: 0x0003, type: 'coil' }, switch5: { address: 0x0004, type: 'coil' }, switch6: { address: 0x0005, type: 'coil' } } },
227
+ 'switch_8': { name: '开关-8键', type: 'switch', registers: { switch1: { address: 0x0000, type: 'coil' }, switch2: { address: 0x0001, type: 'coil' }, switch3: { address: 0x0002, type: 'coil' }, switch4: { address: 0x0003, type: 'coil' }, switch5: { address: 0x0004, type: 'coil' }, switch6: { address: 0x0005, type: 'coil' }, switch7: { address: 0x0006, type: 'coil' }, switch8: { address: 0x0007, type: 'coil' } } },
228
+ 'dimmer_single': { name: '调光-单色', type: 'light', registers: { switch: { address: 0x0000, type: 'coil' }, brightness: { address: 0x0001, type: 'holding' } } },
229
+ 'dimmer_cct': { name: '调光-双色', type: 'light', registers: { switch: { address: 0x0000, type: 'coil' }, brightness: { address: 0x0001, type: 'holding' }, colorTemp: { address: 0x0002, type: 'holding' } } },
230
+ 'dimmer_rgb': { name: '调光-彩色', type: 'light', registers: { switch: { address: 0x0000, type: 'coil' }, brightness: { address: 0x0001, type: 'holding' }, colorR: { address: 0x0002, type: 'holding' }, colorG: { address: 0x0003, type: 'holding' }, colorB: { address: 0x0004, type: 'holding' } } },
231
+ 'curtain': { name: '窗帘', type: 'cover', registers: { position: { address: 0x0000, type: 'holding' } } },
232
+ 'ac': { name: '空调', type: 'climate', registers: { switch: { address: 0x0000, type: 'coil' }, targetTemp: { address: 0x0001, type: 'holding' }, mode: { address: 0x0002, type: 'holding' }, fanSpeed: { address: 0x0003, type: 'holding' } } },
233
+ 'floor_heating': { name: '地暖', type: 'climate', registers: { switch: { address: 0x0000, type: 'coil' }, targetTemp: { address: 0x0001, type: 'holding' } } },
234
+ 'fresh_air': { name: '新风', type: 'fan', registers: { switch: { address: 0x0000, type: 'coil' }, fanSpeed: { address: 0x0001, type: 'holding' } } },
235
+ 'scene': { name: '场景', type: 'scene', registers: { trigger: { address: 0x0000, type: 'holding' } } }
236
+ }
237
+ }
238
+ }
239
+ };
240
+
241
+ function SymiRS485BridgeNode(config) {
242
+ RED.nodes.createNode(this, config);
243
+ const node = this;
244
+
245
+ // 基本配置
246
+ node.name = config.name || '';
247
+ node.gateway = RED.nodes.getNode(config.gateway);
248
+ node.rs485Config = RED.nodes.getNode(config.rs485Config);
249
+
250
+ // 解析实体映射
251
+ try {
252
+ node.mappings = JSON.parse(config.mappings || '[]');
253
+ } catch (e) {
254
+ node.mappings = [];
255
+ }
256
+
257
+ if (!node.gateway) {
258
+ node.status({ fill: 'red', shape: 'ring', text: '未配置Mesh网关' });
259
+ return;
260
+ }
261
+
262
+ if (!node.rs485Config) {
263
+ node.status({ fill: 'red', shape: 'ring', text: '未配置RS485连接' });
264
+ return;
265
+ }
266
+
267
+ // 状态管理
268
+ node.commandQueue = [];
269
+ node.processing = false;
270
+ node.syncLock = false;
271
+ node.lastSyncTime = 0;
272
+ node.pendingVerify = false;
273
+
274
+ if (node.mappings.length === 0) {
275
+ node.status({ fill: 'grey', shape: 'ring', text: '请添加实体映射' });
276
+ } else {
277
+ node.status({ fill: 'yellow', shape: 'ring', text: '连接中...' });
278
+ }
279
+
280
+ // 注册到RS485配置节点
281
+ node.rs485Config.register(node);
282
+
283
+ // 监听RS485连接事件
284
+ node.rs485Config.on('connected', () => {
285
+ node.status({ fill: 'green', shape: 'dot', text: `已连接 ${node.mappings.length}个映射` });
286
+ });
287
+
288
+ node.rs485Config.on('disconnected', () => {
289
+ node.status({ fill: 'yellow', shape: 'ring', text: '已断开' });
290
+ });
291
+
292
+ node.rs485Config.on('error', (err) => {
293
+ node.status({ fill: 'red', shape: 'ring', text: '连接错误' });
294
+ });
295
+
296
+ // 监听RS485接收帧
297
+ node.rs485Config.on('frame', (frame) => {
298
+ node.parseModbusResponse(frame);
299
+ });
300
+
301
+ // 查找Mesh设备的映射配置
302
+ node.findMeshMapping = function(mac, channel) {
303
+ return node.mappings.find(m =>
304
+ m.meshMac === mac && (m.meshChannel === 0 || m.meshChannel === channel)
305
+ );
306
+ };
307
+
308
+ // Find mapping for RS485 device
309
+ node.findRS485Mapping = function(address) {
310
+ return node.mappings.find(m => m.address === address);
311
+ };
312
+
313
+ // 获取映射的寄存器配置
314
+ node.getRegistersForMapping = function(mapping) {
315
+ if (!mapping.brand || !mapping.device) return null;
316
+ const brand = PROTOCOLS.brands[mapping.brand];
317
+ if (!brand || !brand.devices[mapping.device]) return null;
318
+ return brand.devices[mapping.device].registers;
319
+ };
320
+
321
+ // Mesh设备状态变化处理(事件驱动)
322
+ const handleMeshStateChange = (eventData) => {
323
+ if (node.syncLock) return;
324
+
325
+ const mac = eventData.device.macAddress;
326
+ const state = eventData.state || {};
327
+ const channel = state.channel || 0;
328
+
329
+ // 查找匹配的映射
330
+ const mapping = node.findMeshMapping(mac, channel);
331
+ if (!mapping) return;
332
+
333
+ const registers = node.getRegistersForMapping(mapping);
334
+ if (!registers) return;
335
+
336
+ node.log(`[Mesh->RS485] ${eventData.device.name} 状态变化`);
337
+ node.queueCommand({
338
+ direction: 'mesh-to-modbus',
339
+ mapping: mapping,
340
+ registers: registers,
341
+ state: state,
342
+ timestamp: Date.now()
343
+ });
344
+ };
345
+
346
+ // RS485 device state change handler (event-driven)
347
+ const handleModbusStateChange = (data) => {
348
+ if (node.syncLock) return;
349
+
350
+ const mapping = node.findRS485Mapping(data.device.modbusAddress);
351
+ if (!mapping) return;
352
+
353
+ const registers = node.getRegistersForMapping(mapping);
354
+ if (!registers) return;
355
+
356
+ node.log(`[RS485->Mesh] 设备@${data.device.modbusAddress} 状态变化`);
357
+ node.queueCommand({
358
+ direction: 'modbus-to-mesh',
359
+ mapping: mapping,
360
+ registers: registers,
361
+ state: data.states,
362
+ timestamp: Date.now()
363
+ });
364
+ };
365
+
366
+ // 命令队列顺序处理
367
+ node.queueCommand = function(cmd) {
368
+ // 检查队列中是否有相似命令(防抖)
369
+ const existing = node.commandQueue.find(c =>
370
+ c.direction === cmd.direction &&
371
+ Date.now() - c.timestamp < 100
372
+ );
373
+ if (existing) {
374
+ // 合并状态
375
+ existing.state = { ...existing.state, ...cmd.state };
376
+ return;
377
+ }
378
+
379
+ node.commandQueue.push(cmd);
380
+ node.processQueue();
381
+ };
382
+
383
+ node.processQueue = async function() {
384
+ if (node.processing || node.commandQueue.length === 0) return;
385
+
386
+ node.processing = true;
387
+ node.syncLock = true;
388
+ let multiChange = node.commandQueue.length > 1;
389
+
390
+ while (node.commandQueue.length > 0) {
391
+ const cmd = node.commandQueue.shift();
392
+ try {
393
+ if (cmd.direction === 'mesh-to-modbus') {
394
+ await node.syncMeshToModbus(cmd);
395
+ } else if (cmd.direction === 'modbus-to-mesh') {
396
+ await node.syncModbusToMesh(cmd);
397
+ }
398
+ // 命令之间延迟50ms
399
+ await node.sleep(50);
400
+ } catch (err) {
401
+ node.error(`同步失败: ${err.message}`);
402
+ }
403
+ }
404
+
405
+ // 如果发生多次变化,安排验证
406
+ if (multiChange && !node.pendingVerify) {
407
+ node.pendingVerify = true;
408
+ setTimeout(() => {
409
+ node.verifySync();
410
+ node.pendingVerify = false;
411
+ }, 200);
412
+ }
413
+
414
+ node.syncLock = false;
415
+ node.processing = false;
416
+ node.lastSyncTime = Date.now();
417
+ };
418
+
419
+ // 多实体变化后验证同步状态
420
+ node.verifySync = async function() {
421
+ node.log('[Verify] Checking sync state after multi-change');
422
+ // 从双方请求当前状态以确保一致性
423
+ // 这是安全机制,不是轮询
424
+ };
425
+
426
+ // Mesh -> Modbus sync
427
+ node.syncMeshToModbus = async function(cmd) {
428
+ const { mapping, registers, state } = cmd;
429
+ const stateMapping = {
430
+ 'switch': 'switch', 'acSwitch': 'switch',
431
+ 'targetTemp': 'targetTemp', 'acTargetTemp': 'targetTemp',
432
+ 'acMode': 'mode', 'acFanSpeed': 'fanSpeed',
433
+ 'brightness': 'brightness'
434
+ };
435
+
436
+ for (const [meshKey, value] of Object.entries(state)) {
437
+ const regKey = stateMapping[meshKey];
438
+ // Only sync if RS485 device has this register (partial sync support)
439
+ if (regKey && registers[regKey]) {
440
+ try {
441
+ await node.writeModbusRegister(mapping.address, registers[regKey], value);
442
+ node.debug(`Mesh->RS485@${mapping.address}: ${meshKey}=${value}`);
443
+ } catch (err) {
444
+ node.error(`RS485写入失败: ${regKey}=${value}, ${err.message}`);
445
+ }
446
+ }
447
+ }
448
+
449
+ node.status({ fill: 'green', shape: 'dot', text: `同步 ${node.mappings.length}个映射` });
450
+ };
451
+
452
+ // Modbus -> Mesh sync
453
+ node.syncModbusToMesh = async function(cmd) {
454
+ const { mapping, registers, state } = cmd;
455
+
456
+ // 查找Mesh设备
457
+ const meshDevice = node.gateway.getDevice(mapping.meshMac);
458
+ if (!meshDevice) {
459
+ node.warn(`未找到Mesh设备: ${mapping.meshMac}`);
460
+ return;
461
+ }
462
+
463
+ const attrMapping = {
464
+ 'switch': { attrType: 0x02, param: (v) => Buffer.from([v ? 0x02 : 0x01]) },
465
+ 'targetTemp': { attrType: 0x1C, param: (v) => Buffer.from([Math.round(v)]) },
466
+ 'mode': {
467
+ attrType: 0x16,
468
+ param: (v) => {
469
+ const map = { 'cool': 0, 'heat': 1, 'fan': 2, 'dry': 3 };
470
+ return Buffer.from([map[v] !== undefined ? map[v] : 0]);
471
+ }
472
+ },
473
+ 'fanSpeed': {
474
+ attrType: 0x1D,
475
+ param: (v) => {
476
+ const map = { 'high': 1, 'medium': 2, 'low': 3, 'auto': 4 };
477
+ return Buffer.from([map[v] !== undefined ? map[v] : 4]);
478
+ }
479
+ },
480
+ 'brightness': { attrType: 0x03, param: (v) => Buffer.from([Math.round(v)]) }
481
+ };
482
+
483
+ for (const [key, value] of Object.entries(state)) {
484
+ // 仅在Mesh设备支持此属性时同步
485
+ const m = attrMapping[key];
486
+ if (m) {
487
+ try {
488
+ const param = typeof m.param === 'function' ? m.param(value) : m.param;
489
+ await node.gateway.sendControl(meshDevice.networkAddress, m.attrType, param);
490
+ node.debug(`RS485@${mapping.address}->Mesh: ${key}=${value}`);
491
+ } catch (err) {
492
+ node.error(`Mesh写入失败: ${key}=${value}, ${err.message}`);
493
+ }
494
+ }
495
+ }
496
+
497
+ node.status({ fill: 'blue', shape: 'dot', text: `同步 ${node.mappings.length}个映射` });
498
+ };
499
+
500
+ // Write Modbus register
501
+ node.writeModbusRegister = async function(modbusAddr, reg, value) {
502
+ if (!reg) return;
503
+
504
+ const address = typeof reg.address === 'number' ? reg.address : parseInt(reg.address, 16);
505
+
506
+ // 枚举值反向映射
507
+ let writeValue = value;
508
+ if (reg.map) {
509
+ const reverseMap = Object.entries(reg.map).find(([k, v]) => v === value);
510
+ if (reverseMap) {
511
+ writeValue = parseInt(reverseMap[0]);
512
+ }
513
+ }
514
+
515
+ // 通过网关透传从机发送Modbus帧
516
+ // 构建Modbus RTU帧:地址 + 功能码 + 寄存器地址 + 值 + CRC
517
+ const frame = node.buildModbusFrame(modbusAddr, reg.type === 'coil' ? 0x05 : 0x06, address, writeValue);
518
+ await node.sendRS485Frame(frame);
519
+ };
520
+
521
+ node.sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
522
+
523
+ // 构建Modbus RTU帧
524
+ node.buildModbusFrame = function(slaveAddr, functionCode, registerAddr, value) {
525
+ const buffer = Buffer.alloc(8);
526
+ buffer.writeUInt8(slaveAddr, 0);
527
+ buffer.writeUInt8(functionCode, 1);
528
+ buffer.writeUInt16BE(registerAddr, 2);
529
+ buffer.writeUInt16BE(value, 4);
530
+ // 计算CRC16
531
+ const crc = node.calculateCRC16(buffer.subarray(0, 6));
532
+ buffer.writeUInt16LE(crc, 6);
533
+ return buffer;
534
+ };
535
+
536
+ // CRC16计算
537
+ node.calculateCRC16 = function(buffer) {
538
+ let crc = 0xFFFF;
539
+ for (let i = 0; i < buffer.length; i++) {
540
+ crc ^= buffer[i];
541
+ for (let j = 0; j < 8; j++) {
542
+ if (crc & 0x0001) {
543
+ crc = (crc >> 1) ^ 0xA001;
544
+ } else {
545
+ crc >>= 1;
546
+ }
547
+ }
548
+ }
549
+ return crc;
550
+ };
551
+
552
+ // 发送RS485帧(通过配置节点)
553
+ node.sendRS485Frame = async function(frame) {
554
+ if (!node.rs485Config || !node.rs485Config.connected) {
555
+ node.warn('RS485未连接,无法发送数据');
556
+ return;
557
+ }
558
+ await node.rs485Config.send(frame);
559
+ node.debug(`发送RS485帧: ${frame.toString('hex')}`);
560
+ };
561
+
562
+ // 解析Modbus响应
563
+ node.parseModbusResponse = function(frame) {
564
+ const slaveAddr = frame[0];
565
+ const fc = frame[1];
566
+
567
+ // 查找对应的映射
568
+ const mapping = node.findRS485Mapping(slaveAddr);
569
+ if (!mapping) return;
570
+
571
+ const registers = node.getRegistersForMapping(mapping);
572
+ if (!registers) return;
573
+
574
+ // 根据功能码解析数据
575
+ let state = {};
576
+ if (fc === 0x03 || fc === 0x04) {
577
+ // 读寄存器响应
578
+ const byteCount = frame[2];
579
+ for (let i = 0; i < byteCount / 2; i++) {
580
+ const value = frame.readUInt16BE(3 + i * 2);
581
+ // 根据寄存器映射解析
582
+ for (const [key, reg] of Object.entries(registers)) {
583
+ if (reg.map) {
584
+ state[key] = reg.map[value] || value;
585
+ } else {
586
+ state[key] = value;
587
+ }
588
+ }
589
+ }
590
+ }
591
+
592
+ if (Object.keys(state).length > 0) {
593
+ node.log(`[RS485->Mesh] 设备@${slaveAddr} 状态: ${JSON.stringify(state)}`);
594
+ node.queueCommand({
595
+ direction: 'modbus-to-mesh',
596
+ mapping: mapping,
597
+ registers: registers,
598
+ state: state,
599
+ timestamp: Date.now()
600
+ });
601
+ }
602
+ };
603
+
604
+ // Initialize
605
+ const init = () => {
606
+ const validMappings = node.mappings.filter(m => m.meshMac && m.brand && m.device);
607
+ if (validMappings.length > 0) {
608
+ node.status({ fill: 'green', shape: 'dot', text: `同步 ${validMappings.length}个映射` });
609
+ node.log(`[RS485 Bridge] 已初始化 ${validMappings.length} 个实体映射`);
610
+ } else {
611
+ node.status({ fill: 'yellow', shape: 'ring', text: '等待配置...' });
612
+ }
613
+ };
614
+
615
+ // 事件监听 - Mesh网关共享,无冲突
616
+ node.gateway.on('device-list-complete', init);
617
+ node.gateway.on('device-state-changed', handleMeshStateChange);
618
+
619
+ if (node.gateway.deviceListComplete) {
620
+ init();
621
+ }
622
+
623
+ // 输出调试信息
624
+ node.outputDebug = function(direction, info) {
625
+ const msg = {
626
+ topic: 'rs485-bridge/' + direction,
627
+ payload: info,
628
+ timestamp: new Date().toISOString()
629
+ };
630
+ node.send(msg);
631
+ };
632
+
633
+ // 监听RS485帧事件并输出
634
+ if (node.rs485Config) {
635
+ node.rs485Config.on('frame', (frame) => {
636
+ const slaveAddr = frame[0];
637
+ const fc = frame[1];
638
+ const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
639
+ node.outputDebug('rx', {
640
+ direction: 'RX',
641
+ slaveAddr: slaveAddr,
642
+ funcCode: fc,
643
+ hex: hexData,
644
+ raw: frame
645
+ });
646
+ });
647
+
648
+ node.rs485Config.on('tx', (frame) => {
649
+ const hexData = frame.toString('hex').toUpperCase().match(/.{1,2}/g)?.join(' ') || '';
650
+ node.outputDebug('tx', {
651
+ direction: 'TX',
652
+ hex: hexData,
653
+ raw: frame
654
+ });
655
+ });
656
+ }
657
+
658
+ // 处理输入消息(手动控制)
659
+ node.on('input', function(msg) {
660
+ if (!msg.payload) return;
661
+
662
+ // 支持直接发送Modbus帧
663
+ if (msg.payload.modbusFrame || msg.payload.hex) {
664
+ let frame;
665
+ if (Buffer.isBuffer(msg.payload.modbusFrame)) {
666
+ frame = msg.payload.modbusFrame;
667
+ } else if (typeof msg.payload.hex === 'string') {
668
+ const hexStr = msg.payload.hex.replace(/\s/g, '');
669
+ if (/^[0-9A-Fa-f]+$/.test(hexStr)) {
670
+ frame = Buffer.from(hexStr, 'hex');
671
+ }
672
+ }
673
+ if (frame && node.rs485Config && node.rs485Config.connected) {
674
+ node.rs485Config.send(frame);
675
+ node.log(`手动发送Modbus帧: ${frame.toString('hex')}`);
676
+ }
677
+ return;
678
+ }
679
+
680
+ // 支持通过消息触发同步
681
+ if (msg.payload.sync === 'mesh-to-rs485' && msg.payload.mac) {
682
+ const mapping = node.findMeshMapping(msg.payload.mac, msg.payload.channel || 0);
683
+ if (mapping) {
684
+ const registers = node.getRegistersForMapping(mapping);
685
+ if (registers) {
686
+ node.queueCommand({
687
+ direction: 'mesh-to-modbus',
688
+ mapping: mapping,
689
+ registers: registers,
690
+ state: msg.payload.state || {},
691
+ timestamp: Date.now()
692
+ });
693
+ }
694
+ }
695
+ }
696
+ });
697
+
698
+ // 清理
699
+ node.on('close', (done) => {
700
+ node.gateway.removeListener('device-list-complete', init);
701
+ node.gateway.removeListener('device-state-changed', handleMeshStateChange);
702
+
703
+ // 注销RS485配置节点
704
+ if (node.rs485Config) {
705
+ node.rs485Config.deregister(node);
706
+ }
707
+ done();
708
+ });
709
+ }
710
+
711
+ RED.nodes.registerType('symi-rs485-bridge', SymiRS485BridgeNode);
712
+
713
+ // API: Get Mesh devices list with channel info
714
+ RED.httpAdmin.get('/symi-rs485-bridge/mesh-devices/:gatewayId', function(req, res) {
715
+ const gateway = RED.nodes.getNode(req.params.gatewayId);
716
+ if (gateway && gateway.deviceManager) {
717
+ // 按键数转中文
718
+ const channelNames = { 1: '一键', 2: '二键', 3: '三键', 4: '四键', 6: '六键', 8: '八键' };
719
+
720
+ const devices = gateway.deviceManager.getAllDevices().map(d => {
721
+ // 判断是否为开关设备(type=1是零火开关,type=2是普通开关)
722
+ const isSwitch = d.deviceType === 1 || d.deviceType === 2 || d.name?.includes('开关');
723
+
724
+ // 获取按键数
725
+ let channels = 1;
726
+ if (isSwitch) {
727
+ channels = d.channels || d.switchState?.length || 1;
728
+ }
729
+
730
+ // 生成显示名称 - 使用完整MAC地址(去除冒号)
731
+ const macClean = d.macAddress?.replace(/:/g, '') || '';
732
+ let displayName = d.name;
733
+ if (isSwitch && channels >= 1) {
734
+ // 开关设备:用按键数命名 + 完整MAC地址
735
+ const chName = channelNames[channels] || channels + '键';
736
+ displayName = chName + '开关_' + macClean;
737
+ } else {
738
+ // 非开关设备也显示完整MAC
739
+ displayName = (d.name || '设备') + '_' + macClean;
740
+ }
741
+
742
+ return {
743
+ mac: d.macAddress,
744
+ name: displayName,
745
+ originalName: d.name,
746
+ type: d.deviceType,
747
+ channels: channels
748
+ };
749
+ });
750
+ res.json(devices);
751
+ } else {
752
+ res.json([]);
753
+ }
754
+ });
755
+
756
+ // API: Get protocol definitions (brands and device types)
757
+ RED.httpAdmin.get('/symi-rs485-bridge/protocols', function(req, res) {
758
+ res.json(PROTOCOLS);
759
+ });
760
+
761
+ // API: Get available serial ports (与Mesh网关使用相同的方式)
762
+ RED.httpAdmin.get('/symi-rs485-bridge/serial-ports', async function(req, res) {
763
+ try {
764
+ const ports = await SerialPort.list();
765
+ const portList = ports.map(p => ({
766
+ path: p.path,
767
+ manufacturer: p.manufacturer || '',
768
+ vendorId: p.vendorId || '',
769
+ productId: p.productId || ''
770
+ }));
771
+ res.json(portList);
772
+ } catch (err) {
773
+ res.json([]);
774
+ }
775
+ });
776
+ };