node-red-contrib-symi-modbus 2.7.5 → 2.7.7
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/README.md +82 -11
- package/nodes/homekit-bridge.html +34 -20
- package/nodes/homekit-bridge.js +18 -0
- package/nodes/mesh-protocol.js +286 -0
- package/nodes/modbus-dashboard.html +61 -25
- package/nodes/modbus-dashboard.js +18 -0
- package/nodes/modbus-slave-switch.html +196 -12
- package/nodes/modbus-slave-switch.js +386 -29
- package/nodes/serial-port-config.js +66 -21
- package/package.json +1 -1
|
@@ -2,10 +2,256 @@ module.exports = function(RED) {
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const mqtt = require("mqtt");
|
|
4
4
|
const protocol = require("./lightweight-protocol");
|
|
5
|
+
const meshProtocol = require("./mesh-protocol")(RED);
|
|
6
|
+
const storage = require('node-persist');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
5
9
|
|
|
6
10
|
// 全局防抖缓存:防止多个节点重复处理同一个按键事件
|
|
7
11
|
const globalDebounceCache = new Map(); // key: "switchId-buttonNumber", value: timestamp
|
|
8
12
|
|
|
13
|
+
// 初始化Mesh设备持久化存储
|
|
14
|
+
const meshPersistDir = path.join(RED.settings.userDir || os.homedir() + '/.node-red', 'mesh-devices-persist');
|
|
15
|
+
let meshStorageInitialized = false;
|
|
16
|
+
|
|
17
|
+
async function initMeshStorage() {
|
|
18
|
+
if (meshStorageInitialized) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await storage.init({
|
|
23
|
+
dir: meshPersistDir,
|
|
24
|
+
stringify: JSON.stringify,
|
|
25
|
+
parse: JSON.parse,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
logging: false,
|
|
28
|
+
ttl: false,
|
|
29
|
+
expiredInterval: 2 * 60 * 1000,
|
|
30
|
+
forgiveParseErrors: true
|
|
31
|
+
});
|
|
32
|
+
meshStorageInitialized = true;
|
|
33
|
+
RED.log.info(`Mesh设备持久化存储初始化成功: ${meshPersistDir}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
RED.log.error(`Mesh设备持久化存储初始化失败: ${err.message}`);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Mesh设备发现API(使用共享连接)
|
|
41
|
+
RED.httpAdmin.post('/symi-mesh/discover', async function(req, res) {
|
|
42
|
+
try {
|
|
43
|
+
const serialPortConfigId = req.body.serialPortConfig;
|
|
44
|
+
RED.log.info(`[Mesh设备发现] 收到请求,连接配置ID: ${serialPortConfigId}`);
|
|
45
|
+
|
|
46
|
+
if (!serialPortConfigId) {
|
|
47
|
+
RED.log.warn('[Mesh设备发现] 缺少连接配置参数');
|
|
48
|
+
return res.status(400).json({error: '缺少连接配置'});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 获取配置节点
|
|
52
|
+
const serialPortConfig = RED.nodes.getNode(serialPortConfigId);
|
|
53
|
+
if (!serialPortConfig) {
|
|
54
|
+
RED.log.warn(`[Mesh设备发现] 连接配置节点不存在: ${serialPortConfigId}`);
|
|
55
|
+
return res.status(400).json({error: '连接配置不存在,请先创建RS-485连接配置'});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
RED.log.info(`[Mesh设备发现] 配置节点类型: ${serialPortConfig.type || '未知'}`);
|
|
59
|
+
RED.log.info(`[Mesh设备发现] 连接类型: ${serialPortConfig.connectionType || '串口'}`);
|
|
60
|
+
|
|
61
|
+
// 初始化持久化存储
|
|
62
|
+
await initMeshStorage();
|
|
63
|
+
|
|
64
|
+
// 发送设备列表请求
|
|
65
|
+
const requestFrame = meshProtocol.buildGetDeviceListFrame();
|
|
66
|
+
RED.log.info(`[Mesh设备发现] 发送设备列表请求: ${requestFrame.toString('hex').toUpperCase()}`);
|
|
67
|
+
|
|
68
|
+
// 设置响应超时
|
|
69
|
+
let timeoutHandle;
|
|
70
|
+
let isCompleted = false;
|
|
71
|
+
let dataHandler = null;
|
|
72
|
+
|
|
73
|
+
const completeRequest = async (devices, reason) => {
|
|
74
|
+
if (isCompleted) return;
|
|
75
|
+
isCompleted = true;
|
|
76
|
+
|
|
77
|
+
clearTimeout(timeoutHandle);
|
|
78
|
+
|
|
79
|
+
// 注销数据监听器
|
|
80
|
+
if (dataHandler) {
|
|
81
|
+
serialPortConfig.unregisterDataListener(dataHandler);
|
|
82
|
+
RED.log.info(`[Mesh设备发现] 已注销数据监听器`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (devices.length === 0) {
|
|
86
|
+
RED.log.warn(`[Mesh设备发现] 未发现设备 (${reason || '超时'})`);
|
|
87
|
+
} else {
|
|
88
|
+
RED.log.info(`[Mesh设备发现] 发现 ${devices.length} 个设备`);
|
|
89
|
+
|
|
90
|
+
// 持久化保存设备列表(MAC地址 → 短地址映射)
|
|
91
|
+
const deviceMap = {};
|
|
92
|
+
devices.forEach(device => {
|
|
93
|
+
deviceMap[device.mac] = {
|
|
94
|
+
shortAddr: device.shortAddr,
|
|
95
|
+
type: device.type,
|
|
96
|
+
buttons: device.buttons,
|
|
97
|
+
lastUpdate: Date.now()
|
|
98
|
+
};
|
|
99
|
+
RED.log.info(` - MAC: ${device.mac}, 短地址: ${device.shortAddr}, 类型: ${device.type}, 按键数: ${device.buttons}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await storage.setItem('meshDeviceMap', deviceMap);
|
|
103
|
+
RED.log.info(`[Mesh设备发现] 设备列表已保存到持久化存储`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
res.json(devices);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
timeoutHandle = setTimeout(() => {
|
|
110
|
+
RED.log.warn(`[Mesh设备发现] 5秒超时,未收到完整响应`);
|
|
111
|
+
completeRequest([], '超时');
|
|
112
|
+
}, 5000);
|
|
113
|
+
|
|
114
|
+
// 收集响应帧
|
|
115
|
+
const responseFrames = [];
|
|
116
|
+
let totalDevices = 0;
|
|
117
|
+
let receivedDevices = 0;
|
|
118
|
+
let buffer = Buffer.alloc(0);
|
|
119
|
+
|
|
120
|
+
dataHandler = (data) => {
|
|
121
|
+
// 拼接数据到缓冲区
|
|
122
|
+
buffer = Buffer.concat([buffer, data]);
|
|
123
|
+
RED.log.info(`[Mesh设备发现] 收到数据: ${data.toString('hex').toUpperCase()}, 缓冲区总长: ${buffer.length}`);
|
|
124
|
+
|
|
125
|
+
// 解析所有完整的帧
|
|
126
|
+
// 协议格式: 53 92 00 10 [总数] [索引] [MAC 6字节] [短地址 2字节] [vendor_id 2字节] [dev_type] [dev_sub_type] [online/status] [resv] [校验]
|
|
127
|
+
// 总长度: 1+1+1+1+16+1 = 21字节
|
|
128
|
+
while (buffer.length >= 21) {
|
|
129
|
+
// 查找帧头 0x53
|
|
130
|
+
const headerIndex = buffer.indexOf(0x53);
|
|
131
|
+
if (headerIndex === -1) {
|
|
132
|
+
buffer = Buffer.alloc(0);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (headerIndex > 0) {
|
|
137
|
+
buffer = buffer.slice(headerIndex);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (buffer.length < 21) break;
|
|
141
|
+
|
|
142
|
+
// 检查是否是设备列表响应 (0x53 0x92 0x00 0x10)
|
|
143
|
+
if (buffer[1] !== 0x92 || buffer[2] !== 0x00 || buffer[3] !== 0x10) {
|
|
144
|
+
buffer = buffer.slice(1);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 提取完整帧(21字节)
|
|
149
|
+
const frame = buffer.slice(0, 21);
|
|
150
|
+
buffer = buffer.slice(21);
|
|
151
|
+
|
|
152
|
+
// 解析帧数据
|
|
153
|
+
const deviceCount = frame[4]; // 设备总数
|
|
154
|
+
const index = frame[5]; // 索引 (00, 01, 02...)
|
|
155
|
+
const mac = Array.from(frame.slice(6, 12))
|
|
156
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
157
|
+
.join(':');
|
|
158
|
+
const shortAddr = frame[12] | (frame[13] << 8); // 小端序
|
|
159
|
+
const vendorId = frame[14] | (frame[15] << 8); // 小端序
|
|
160
|
+
const devType = frame[16];
|
|
161
|
+
const devSubType = frame[17]; // 按键数
|
|
162
|
+
const online = frame[18];
|
|
163
|
+
const resv = frame[19];
|
|
164
|
+
const checksum = frame[20];
|
|
165
|
+
|
|
166
|
+
RED.log.info(`[Mesh设备发现] 收到响应帧,索引: ${index}/${deviceCount-1}, 完整帧: ${frame.toString('hex').toUpperCase()}`);
|
|
167
|
+
|
|
168
|
+
// 第一帧:记录设备总数
|
|
169
|
+
if (totalDevices === 0) {
|
|
170
|
+
totalDevices = deviceCount;
|
|
171
|
+
RED.log.info(`[Mesh设备发现] 设备总数: ${totalDevices}`);
|
|
172
|
+
if (totalDevices === 0) {
|
|
173
|
+
completeRequest([], '网关下无设备');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 只保存开关类型的设备(devType=0x01或0x02)
|
|
179
|
+
if ((devType === 0x01 || devType === 0x02) && devSubType > 0) {
|
|
180
|
+
responseFrames.push({
|
|
181
|
+
shortAddr,
|
|
182
|
+
mac,
|
|
183
|
+
type: devType,
|
|
184
|
+
buttons: devSubType
|
|
185
|
+
});
|
|
186
|
+
RED.log.info(` - 索引${index}: MAC=${mac}, 短地址=0x${shortAddr.toString(16).toUpperCase().padStart(4, '0')}, 类型=0x${devType.toString(16).toUpperCase()}, 按键数=${devSubType}, 在线=${online}`);
|
|
187
|
+
} else {
|
|
188
|
+
RED.log.debug(` - 索引${index}: 跳过非开关设备,MAC=${mac}, 类型=0x${devType.toString(16).toUpperCase()}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
receivedDevices++;
|
|
192
|
+
RED.log.info(`[Mesh设备发现] 已收到 ${receivedDevices}/${totalDevices} 个设备数据帧`);
|
|
193
|
+
|
|
194
|
+
// 如果收到所有设备数据,返回结果
|
|
195
|
+
if (receivedDevices >= totalDevices) {
|
|
196
|
+
RED.log.info(`[Mesh设备发现] 所有设备数据接收完成,共${responseFrames.length}个开关设备`);
|
|
197
|
+
completeRequest(responseFrames, '完成');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// 使用共享连接(TCP和串口都一样)
|
|
204
|
+
RED.log.info(`[Mesh设备发现] 使用共享连接(${serialPortConfig.connectionType})`);
|
|
205
|
+
|
|
206
|
+
// 注册数据监听器
|
|
207
|
+
serialPortConfig.registerDataListener(dataHandler);
|
|
208
|
+
|
|
209
|
+
// 发送请求
|
|
210
|
+
try {
|
|
211
|
+
serialPortConfig.write(requestFrame, (err) => {
|
|
212
|
+
if (err) {
|
|
213
|
+
RED.log.error(`[Mesh设备发现] 发送请求失败: ${err.message}`);
|
|
214
|
+
serialPortConfig.unregisterDataListener(dataHandler);
|
|
215
|
+
completeRequest([], `发送失败: ${err.message}`);
|
|
216
|
+
} else {
|
|
217
|
+
RED.log.info(`[Mesh设备发现] 请求已发送,等待响应...`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
} catch (err) {
|
|
221
|
+
RED.log.error(`[Mesh设备发现] 发送请求异常: ${err.message}`);
|
|
222
|
+
serialPortConfig.unregisterDataListener(dataHandler);
|
|
223
|
+
return res.status(500).json({error: `发送请求失败: ${err.message}`});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
} catch (err) {
|
|
227
|
+
RED.log.error(`Mesh设备发现失败: ${err.message}`);
|
|
228
|
+
res.status(500).json({error: err.message});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// 获取已保存的Mesh设备列表API
|
|
233
|
+
RED.httpAdmin.get('/symi-mesh/devices', async function(req, res) {
|
|
234
|
+
try {
|
|
235
|
+
await initMeshStorage();
|
|
236
|
+
const deviceMap = await storage.getItem('meshDeviceMap') || {};
|
|
237
|
+
|
|
238
|
+
// 转换为数组格式
|
|
239
|
+
const devices = Object.keys(deviceMap).map(mac => {
|
|
240
|
+
return {
|
|
241
|
+
mac: mac,
|
|
242
|
+
shortAddr: deviceMap[mac].shortAddr,
|
|
243
|
+
type: deviceMap[mac].type,
|
|
244
|
+
buttons: deviceMap[mac].buttons
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
res.json(devices);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
RED.log.error(`获取Mesh设备列表失败: ${err.message}`);
|
|
251
|
+
res.status(500).json({error: err.message});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
9
255
|
// 串口列表API - 支持Windows、Linux、macOS所有串口设备
|
|
10
256
|
RED.httpAdmin.get('/modbus-slave-switch/serialports', async function(req, res) {
|
|
11
257
|
try {
|
|
@@ -87,9 +333,15 @@ module.exports = function(RED) {
|
|
|
87
333
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay",
|
|
88
334
|
// 开关面板配置
|
|
89
335
|
switchBrand: config.switchBrand || "symi", // 面板品牌(默认亖米)
|
|
90
|
-
buttonType: config.buttonType || "switch", // 按钮类型:switch=开关模式,scene
|
|
336
|
+
buttonType: config.buttonType || "switch", // 按钮类型:switch=开关模式,scene=场景模式,mesh=Mesh模式
|
|
91
337
|
switchId: parseInt(config.switchId) || 0, // 开关ID(0-255,物理面板地址)
|
|
92
338
|
buttonNumber: parseInt(config.buttonNumber) || 1, // 按钮编号(1-8)
|
|
339
|
+
// Mesh模式配置
|
|
340
|
+
meshMacAddress: config.meshMacAddress || "", // Mesh设备MAC地址
|
|
341
|
+
meshShortAddress: parseInt(config.meshShortAddress) || 0, // Mesh设备短地址
|
|
342
|
+
meshButtonNumber: parseInt(config.meshButtonNumber) || 1, // Mesh按键编号(1-6)
|
|
343
|
+
meshTotalButtons: parseInt(config.meshTotalButtons) || 1, // Mesh开关总路数(1-6)
|
|
344
|
+
// 目标继电器配置
|
|
93
345
|
targetSlaveAddress: parseInt(config.targetSlaveAddress) || 10, // 目标继电器从站地址
|
|
94
346
|
targetCoilNumber: modbusCoilNumber // 目标继电器线圈编号(0-31,从用户输入的1-32转换)
|
|
95
347
|
};
|
|
@@ -101,6 +353,28 @@ module.exports = function(RED) {
|
|
|
101
353
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
102
354
|
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
103
355
|
|
|
356
|
+
// Mesh模式状态缓存
|
|
357
|
+
node.meshCurrentStates = null; // Mesh设备当前状态(用于保持其他路不变)
|
|
358
|
+
|
|
359
|
+
// Mesh模式:从持久化存储更新短地址(如果MAC地址对应的短地址发生变化)
|
|
360
|
+
if (node.config.buttonType === 'mesh' && node.config.meshMacAddress) {
|
|
361
|
+
initMeshStorage().then(async () => {
|
|
362
|
+
try {
|
|
363
|
+
const deviceMap = await storage.getItem('meshDeviceMap') || {};
|
|
364
|
+
const savedDevice = deviceMap[node.config.meshMacAddress];
|
|
365
|
+
|
|
366
|
+
if (savedDevice && savedDevice.shortAddr !== node.config.meshShortAddress) {
|
|
367
|
+
node.log(`Mesh设备短地址已更新: ${node.config.meshMacAddress} 从 ${node.config.meshShortAddress} 更新为 ${savedDevice.shortAddr}`);
|
|
368
|
+
node.config.meshShortAddress = savedDevice.shortAddr;
|
|
369
|
+
}
|
|
370
|
+
} catch (err) {
|
|
371
|
+
node.warn(`更新Mesh设备短地址失败: ${err.message}`);
|
|
372
|
+
}
|
|
373
|
+
}).catch(err => {
|
|
374
|
+
node.warn(`初始化Mesh存储失败: ${err.message}`);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
104
378
|
// 命令队列(处理多个按键同时按下)- 带时间戳
|
|
105
379
|
node.commandQueue = [];
|
|
106
380
|
node.isProcessingCommand = false;
|
|
@@ -310,7 +584,13 @@ module.exports = function(RED) {
|
|
|
310
584
|
// 处理RS-485接收到的数据
|
|
311
585
|
node.handleRs485Data = function(data) {
|
|
312
586
|
try {
|
|
313
|
-
//
|
|
587
|
+
// 如果是Mesh模式,使用Mesh协议解析
|
|
588
|
+
if (node.config.buttonType === 'mesh') {
|
|
589
|
+
node.handleMeshData(data);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 解析轻量级协议帧(RS-485模式)
|
|
314
594
|
const frame = protocol.parseFrame(data);
|
|
315
595
|
if (!frame) {
|
|
316
596
|
return; // 静默忽略无效帧
|
|
@@ -379,7 +659,60 @@ module.exports = function(RED) {
|
|
|
379
659
|
node.error(`解析RS-485数据失败: ${err.message}`);
|
|
380
660
|
}
|
|
381
661
|
};
|
|
382
|
-
|
|
662
|
+
|
|
663
|
+
// 处理Mesh协议数据
|
|
664
|
+
node.handleMeshData = function(data) {
|
|
665
|
+
try {
|
|
666
|
+
// 解析Mesh状态事件
|
|
667
|
+
const event = meshProtocol.parseStatusEvent(data);
|
|
668
|
+
if (!event) {
|
|
669
|
+
return; // 静默忽略无效帧
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 检查是否是我们监听的Mesh设备
|
|
673
|
+
if (event.shortAddr !== node.config.meshShortAddress) {
|
|
674
|
+
return; // 不是我们的设备,忽略
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 检查消息类型
|
|
678
|
+
if (event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH &&
|
|
679
|
+
event.msgType !== meshProtocol.PROTOCOL.MSG_TYPE_SWITCH_6) {
|
|
680
|
+
return; // 不是开关状态,忽略
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 获取按钮状态
|
|
684
|
+
if (!event.states || event.states.length < node.config.meshButtonNumber) {
|
|
685
|
+
return; // 状态数据不完整
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const buttonState = event.states[node.config.meshButtonNumber - 1];
|
|
689
|
+
if (buttonState === null) {
|
|
690
|
+
return; // 状态未知,忽略
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 全局防抖:防止多个节点重复处理同一个按键
|
|
694
|
+
const debounceKey = `mesh-${node.config.meshShortAddress}-${node.config.meshButtonNumber}`;
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
|
|
697
|
+
|
|
698
|
+
// 全局防抖:200ms内只触发一次
|
|
699
|
+
if (now - lastTriggerTime < 200) {
|
|
700
|
+
return; // 静默忽略重复触发
|
|
701
|
+
}
|
|
702
|
+
globalDebounceCache.set(debounceKey, now);
|
|
703
|
+
|
|
704
|
+
// 更新当前状态缓存(用于后续控制时保持其他路不变)
|
|
705
|
+
node.meshCurrentStates = event.states;
|
|
706
|
+
|
|
707
|
+
// 发送命令到继电器
|
|
708
|
+
node.debug(`Mesh开关${buttonState ? 'ON' : 'OFF'}: MAC=${node.config.meshMacAddress} 按键${node.config.meshButtonNumber}`);
|
|
709
|
+
node.sendMqttCommand(buttonState);
|
|
710
|
+
|
|
711
|
+
} catch (err) {
|
|
712
|
+
node.error(`解析Mesh数据失败: ${err.message}`);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
383
716
|
// 发送命令到继电器(支持两种模式:MQTT模式和内部事件模式)
|
|
384
717
|
node.sendMqttCommand = function(state) {
|
|
385
718
|
// 模式1:MQTT模式(通过MQTT发送命令,由主站节点统一处理)
|
|
@@ -534,36 +867,54 @@ module.exports = function(RED) {
|
|
|
534
867
|
}
|
|
535
868
|
|
|
536
869
|
const state = item.state;
|
|
537
|
-
|
|
870
|
+
|
|
538
871
|
try {
|
|
539
|
-
// 使用初始化时计算的deviceAddr和channel
|
|
540
|
-
// 当物理按键按下时,会更新为实际的deviceAddr和channel
|
|
541
|
-
const deviceAddr = node.buttonDeviceAddr;
|
|
542
|
-
const channel = node.buttonChannel;
|
|
543
|
-
|
|
544
|
-
// 根据按钮类型选择协议类型
|
|
545
|
-
// 开关模式:使用SET协议(0x03),面板LED需要接收SET指令
|
|
546
|
-
// 场景模式:使用REPORT协议(0x04),面板LED需要接收REPORT指令
|
|
547
872
|
let command;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
873
|
+
|
|
874
|
+
if (node.config.buttonType === 'mesh') {
|
|
875
|
+
// Mesh模式:发送Mesh控制帧
|
|
876
|
+
command = meshProtocol.buildSwitchControlFrame(
|
|
877
|
+
node.config.meshShortAddress,
|
|
878
|
+
node.config.meshButtonNumber,
|
|
879
|
+
node.config.meshTotalButtons,
|
|
880
|
+
state,
|
|
881
|
+
node.meshCurrentStates || null
|
|
555
882
|
);
|
|
883
|
+
|
|
884
|
+
if (!command) {
|
|
885
|
+
node.error(`构建Mesh控制帧失败`);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
556
888
|
} else {
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
889
|
+
// RS-485模式:使用轻量级协议
|
|
890
|
+
// 使用初始化时计算的deviceAddr和channel
|
|
891
|
+
// 当物理按键按下时,会更新为实际的deviceAddr和channel
|
|
892
|
+
const deviceAddr = node.buttonDeviceAddr;
|
|
893
|
+
const channel = node.buttonChannel;
|
|
894
|
+
|
|
895
|
+
// 根据按钮类型选择协议类型
|
|
896
|
+
// 开关模式:使用SET协议(0x03),面板LED需要接收SET指令
|
|
897
|
+
// 场景模式:使用REPORT协议(0x04),面板LED需要接收REPORT指令
|
|
898
|
+
if (node.config.buttonType === 'scene') {
|
|
899
|
+
// 场景模式:使用REPORT协议
|
|
900
|
+
command = protocol.buildSingleLightReport(
|
|
901
|
+
node.config.switchId, // 本地地址(面板地址)
|
|
902
|
+
deviceAddr, // 设备地址(从按键事件中获取)
|
|
903
|
+
channel, // 通道(从按键事件中获取)
|
|
904
|
+
state
|
|
905
|
+
);
|
|
906
|
+
} else {
|
|
907
|
+
// 开关模式(默认):使用SET协议
|
|
908
|
+
command = protocol.buildSingleLightCommand(
|
|
909
|
+
node.config.switchId, // 本地地址(面板地址)
|
|
910
|
+
deviceAddr, // 设备地址(从按键事件中获取)
|
|
911
|
+
channel, // 通道(从按键事件中获取)
|
|
912
|
+
state
|
|
913
|
+
);
|
|
914
|
+
}
|
|
564
915
|
}
|
|
565
916
|
|
|
566
|
-
// 发送到RS-485
|
|
917
|
+
// 发送到RS-485总线或Mesh网关(使用共享连接配置)
|
|
567
918
|
if (node.serialPortConfig) {
|
|
568
919
|
node.serialPortConfig.write(command, (err) => {
|
|
569
920
|
if (err) {
|
|
@@ -575,11 +926,17 @@ module.exports = function(RED) {
|
|
|
575
926
|
|
|
576
927
|
// 输出调试日志,确认LED反馈已发送(包含协议帧十六进制)
|
|
577
928
|
const hexStr = command.toString('hex').toUpperCase().match(/.{1,2}/g).join(' ');
|
|
578
|
-
|
|
929
|
+
if (node.config.buttonType === 'mesh') {
|
|
930
|
+
node.log(`Mesh LED反馈已发送:MAC=${node.config.meshMacAddress} 按钮${node.config.meshButtonNumber} = ${state ? 'ON' : 'OFF'} [${hexStr}]`);
|
|
931
|
+
} else {
|
|
932
|
+
const deviceAddr = node.buttonDeviceAddr;
|
|
933
|
+
const channel = node.buttonChannel;
|
|
934
|
+
node.log(`LED反馈已发送:面板${node.config.switchId} 按钮${node.config.buttonNumber} 设备${deviceAddr} 通道${channel} = ${state ? 'ON' : 'OFF'} (${node.config.buttonType === 'scene' ? 'REPORT' : 'SET'}) [${hexStr}]`);
|
|
935
|
+
}
|
|
579
936
|
}
|
|
580
937
|
});
|
|
581
938
|
} else {
|
|
582
|
-
node.warn('
|
|
939
|
+
node.warn('连接未配置');
|
|
583
940
|
}
|
|
584
941
|
|
|
585
942
|
// 队列间隔20ms(优化速度,确保200个节点能快速反馈)
|
|
@@ -29,6 +29,10 @@ module.exports = function(RED) {
|
|
|
29
29
|
node.reconnectTimer = null; // 重连定时器
|
|
30
30
|
node.reconnectAttempts = 0; // 重连尝试次数
|
|
31
31
|
|
|
32
|
+
// 错误日志限流(避免断网时产生大量日志)
|
|
33
|
+
node.lastErrorLog = 0; // 上次错误日志时间
|
|
34
|
+
node.errorLogInterval = 60 * 1000; // 错误日志间隔:60秒
|
|
35
|
+
|
|
32
36
|
// 数据监听器列表(每个从站开关节点注册一个)
|
|
33
37
|
node.dataListeners = [];
|
|
34
38
|
|
|
@@ -81,15 +85,23 @@ module.exports = function(RED) {
|
|
|
81
85
|
}
|
|
82
86
|
});
|
|
83
87
|
|
|
84
|
-
//
|
|
88
|
+
// 监听错误(限流日志)
|
|
85
89
|
node.connection.on('error', (err) => {
|
|
86
|
-
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
92
|
+
node.error(`TCP连接错误: ${err.message}`);
|
|
93
|
+
node.lastErrorLog = now;
|
|
94
|
+
}
|
|
87
95
|
// 不在这里重连,在close事件中统一处理
|
|
88
96
|
});
|
|
89
97
|
|
|
90
98
|
// 监听关闭(统一的重连入口)
|
|
91
99
|
node.connection.on('close', (hadError) => {
|
|
92
|
-
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
102
|
+
node.log(`TCP连接已关闭${hadError ? '(有错误)' : ''}`);
|
|
103
|
+
node.lastErrorLog = now;
|
|
104
|
+
}
|
|
93
105
|
|
|
94
106
|
// 清理连接对象
|
|
95
107
|
if (node.connection) {
|
|
@@ -103,7 +115,11 @@ module.exports = function(RED) {
|
|
|
103
115
|
const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000); // 指数退避,最大60秒
|
|
104
116
|
node.reconnectAttempts++;
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
const nowLog = Date.now();
|
|
119
|
+
if (nowLog - node.lastErrorLog > node.errorLogInterval) {
|
|
120
|
+
node.log(`${delay/1000}秒后尝试重新连接TCP(第${node.reconnectAttempts}次)...`);
|
|
121
|
+
node.lastErrorLog = nowLog;
|
|
122
|
+
}
|
|
107
123
|
|
|
108
124
|
node.reconnectTimer = setTimeout(() => {
|
|
109
125
|
node.reconnectTimer = null;
|
|
@@ -164,15 +180,23 @@ module.exports = function(RED) {
|
|
|
164
180
|
node.isOpening = false;
|
|
165
181
|
|
|
166
182
|
if (err) {
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
185
|
+
node.error(`串口打开失败: ${err.message}`);
|
|
186
|
+
node.lastErrorLog = now;
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
// 打开失败时也要触发重连(如果有监听器在使用)
|
|
170
190
|
if (!node.isClosing && node.dataListeners.length > 0) {
|
|
171
191
|
const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000);
|
|
172
192
|
node.reconnectAttempts++;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
|
|
194
|
+
const nowLog = Date.now();
|
|
195
|
+
if (nowLog - node.lastErrorLog > node.errorLogInterval) {
|
|
196
|
+
node.log(`${delay/1000}秒后尝试重新打开串口(第${node.reconnectAttempts}次)...`);
|
|
197
|
+
node.lastErrorLog = nowLog;
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
node.reconnectTimer = setTimeout(() => {
|
|
177
201
|
node.reconnectTimer = null;
|
|
178
202
|
node.openSerialConnection();
|
|
@@ -191,25 +215,37 @@ module.exports = function(RED) {
|
|
|
191
215
|
try {
|
|
192
216
|
listener(data);
|
|
193
217
|
} catch (err) {
|
|
194
|
-
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
220
|
+
node.error(`数据监听器错误: ${err.message}`);
|
|
221
|
+
node.lastErrorLog = now;
|
|
222
|
+
}
|
|
195
223
|
}
|
|
196
224
|
});
|
|
197
225
|
});
|
|
198
226
|
|
|
199
|
-
//
|
|
227
|
+
// 监听错误(限流日志)
|
|
200
228
|
node.connection.on('error', (err) => {
|
|
201
|
-
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
231
|
+
node.error(`串口错误: ${err.message}`);
|
|
232
|
+
node.lastErrorLog = now;
|
|
233
|
+
}
|
|
202
234
|
// 不在这里重连,在close事件中统一处理
|
|
203
235
|
});
|
|
204
236
|
|
|
205
237
|
// 监听关闭(统一的重连入口)
|
|
206
238
|
node.connection.on('close', (err) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
if (now - node.lastErrorLog > node.errorLogInterval) {
|
|
241
|
+
if (err) {
|
|
242
|
+
node.log(`串口已关闭(错误: ${err.message})`);
|
|
243
|
+
} else {
|
|
244
|
+
node.log('串口已关闭');
|
|
245
|
+
}
|
|
246
|
+
node.lastErrorLog = now;
|
|
211
247
|
}
|
|
212
|
-
|
|
248
|
+
|
|
213
249
|
// 清理连接对象
|
|
214
250
|
if (node.connection) {
|
|
215
251
|
try {
|
|
@@ -219,14 +255,18 @@ module.exports = function(RED) {
|
|
|
219
255
|
}
|
|
220
256
|
node.connection = null;
|
|
221
257
|
}
|
|
222
|
-
|
|
258
|
+
|
|
223
259
|
// 自动重连(如果不是主动关闭且有监听器在使用)
|
|
224
260
|
if (!node.isClosing && node.dataListeners.length > 0) {
|
|
225
261
|
const delay = Math.min(5000 * Math.pow(2, node.reconnectAttempts), 60000); // 指数退避,最大60秒
|
|
226
262
|
node.reconnectAttempts++;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
|
|
264
|
+
const nowLog = Date.now();
|
|
265
|
+
if (nowLog - node.lastErrorLog > node.errorLogInterval) {
|
|
266
|
+
node.log(`检测到串口断开,${delay/1000}秒后尝试重新连接(第${node.reconnectAttempts}次)...`);
|
|
267
|
+
node.lastErrorLog = nowLog;
|
|
268
|
+
}
|
|
269
|
+
|
|
230
270
|
node.reconnectTimer = setTimeout(() => {
|
|
231
271
|
node.reconnectTimer = null;
|
|
232
272
|
node.openSerialConnection();
|
|
@@ -251,6 +291,11 @@ module.exports = function(RED) {
|
|
|
251
291
|
}
|
|
252
292
|
};
|
|
253
293
|
|
|
294
|
+
// 获取连接对象(用于Mesh设备发现等场景)
|
|
295
|
+
node.getConnection = function() {
|
|
296
|
+
return node.connection;
|
|
297
|
+
};
|
|
298
|
+
|
|
254
299
|
// 注册数据监听器
|
|
255
300
|
node.registerDataListener = function(listener) {
|
|
256
301
|
if (typeof listener !== 'function') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.7",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|