node-red-contrib-symi-modbus 2.5.0 → 2.5.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/README.md +314 -494
- package/examples/basic-flow.json +198 -0
- package/nodes/lightweight-protocol.js +35 -1
- package/nodes/modbus-master.html +13 -185
- package/nodes/modbus-master.js +458 -153
- package/nodes/modbus-server-config.html +174 -0
- package/nodes/modbus-server-config.js +18 -0
- package/nodes/modbus-slave-switch.html +11 -153
- package/nodes/modbus-slave-switch.js +250 -219
- package/nodes/serial-port-config.html +191 -0
- package/nodes/serial-port-config.js +270 -0
- package/package.json +8 -5
package/nodes/modbus-master.js
CHANGED
|
@@ -2,6 +2,7 @@ module.exports = function(RED) {
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const ModbusRTU = require("modbus-serial");
|
|
4
4
|
const mqtt = require("mqtt");
|
|
5
|
+
const protocol = require("./lightweight-protocol");
|
|
5
6
|
|
|
6
7
|
// 串口列表API - 支持Windows、Linux、macOS所有串口设备
|
|
7
8
|
RED.httpAdmin.get('/modbus-master/serialports', async function(req, res) {
|
|
@@ -90,33 +91,41 @@ module.exports = function(RED) {
|
|
|
90
91
|
RED.nodes.createNode(this, config);
|
|
91
92
|
const node = this;
|
|
92
93
|
|
|
94
|
+
// 获取Modbus服务器配置节点
|
|
95
|
+
node.modbusServerConfig = RED.nodes.getNode(config.modbusServer);
|
|
96
|
+
|
|
93
97
|
// 获取MQTT服务器配置节点
|
|
94
98
|
node.mqttServerConfig = RED.nodes.getNode(config.mqttServer);
|
|
95
99
|
|
|
96
|
-
//
|
|
100
|
+
// 配置参数(从Modbus服务器配置节点读取)
|
|
97
101
|
node.config = {
|
|
98
|
-
connectionType:
|
|
99
|
-
tcpHost:
|
|
100
|
-
tcpPort: parseInt(
|
|
101
|
-
serialPort:
|
|
102
|
-
serialBaudRate: parseInt(
|
|
103
|
-
serialDataBits: parseInt(
|
|
104
|
-
serialStopBits: parseInt(
|
|
105
|
-
serialParity:
|
|
102
|
+
connectionType: node.modbusServerConfig ? (node.modbusServerConfig.connectionType || "tcp") : "tcp",
|
|
103
|
+
tcpHost: node.modbusServerConfig ? (node.modbusServerConfig.tcpHost || "127.0.0.1") : "127.0.0.1",
|
|
104
|
+
tcpPort: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.tcpPort) || 502) : 502,
|
|
105
|
+
serialPort: node.modbusServerConfig ? (node.modbusServerConfig.serialPort || "/dev/ttyUSB0") : "/dev/ttyUSB0",
|
|
106
|
+
serialBaudRate: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialBaudRate) || 9600) : 9600,
|
|
107
|
+
serialDataBits: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialDataBits) || 8) : 8,
|
|
108
|
+
serialStopBits: node.modbusServerConfig ? (parseInt(node.modbusServerConfig.serialStopBits) || 1) : 1,
|
|
109
|
+
serialParity: node.modbusServerConfig ? (node.modbusServerConfig.serialParity || "none") : "none",
|
|
106
110
|
slaves: config.slaves || [{
|
|
107
111
|
address: 10,
|
|
108
112
|
coilStart: 0,
|
|
109
113
|
coilEnd: 31,
|
|
110
114
|
pollInterval: 200
|
|
111
115
|
}],
|
|
112
|
-
enableMqtt: config.enableMqtt,
|
|
116
|
+
enableMqtt: config.enableMqtt !== false,
|
|
113
117
|
// 从config节点读取MQTT配置
|
|
114
|
-
mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "",
|
|
118
|
+
mqttBroker: node.mqttServerConfig ? node.mqttServerConfig.broker : "mqtt://localhost:1883",
|
|
115
119
|
mqttUsername: node.mqttServerConfig ? node.mqttServerConfig.username : "",
|
|
116
120
|
mqttPassword: node.mqttServerConfig ? (node.mqttServerConfig.credentials ? node.mqttServerConfig.credentials.password : "") : "",
|
|
117
121
|
mqttBaseTopic: node.mqttServerConfig ? node.mqttServerConfig.baseTopic : "modbus/relay"
|
|
118
122
|
};
|
|
119
123
|
|
|
124
|
+
// 验证Modbus服务器配置
|
|
125
|
+
if (!node.modbusServerConfig) {
|
|
126
|
+
node.warn('未配置Modbus服务器,将使用默认配置');
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
// Modbus客户端
|
|
121
130
|
node.client = new ModbusRTU();
|
|
122
131
|
node.isConnected = false;
|
|
@@ -132,6 +141,9 @@ module.exports = function(RED) {
|
|
|
132
141
|
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
133
142
|
node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
|
|
134
143
|
node.lastWriteTime = {}; // 记录每个从站的最后写入时间
|
|
144
|
+
node.pausePolling = false; // 暂停轮询标志(从站上报时暂停)
|
|
145
|
+
node.pollingPausedCount = 0; // 暂停轮询计数器
|
|
146
|
+
node._discoveryPublished = false; // Discovery发布标志(避免重复)
|
|
135
147
|
|
|
136
148
|
// 更新节点状态显示
|
|
137
149
|
node.updateNodeStatus = function() {
|
|
@@ -155,7 +167,10 @@ module.exports = function(RED) {
|
|
|
155
167
|
lastUpdate: null,
|
|
156
168
|
error: null,
|
|
157
169
|
config: slave, // 保存该从站的配置
|
|
158
|
-
initialPublished: false // 标记是否已发布初始状态
|
|
170
|
+
initialPublished: false, // 标记是否已发布初始状态
|
|
171
|
+
timeoutCount: 0, // 连续超时次数
|
|
172
|
+
isIgnored: false, // 是否被临时忽略(超时次数过多时)
|
|
173
|
+
lastTimeoutTime: 0 // 最后一次超时时间
|
|
159
174
|
};
|
|
160
175
|
});
|
|
161
176
|
|
|
@@ -163,32 +178,75 @@ module.exports = function(RED) {
|
|
|
163
178
|
node.connectModbus = async function() {
|
|
164
179
|
try {
|
|
165
180
|
if (node.config.connectionType === "tcp") {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
// 验证TCP主机地址
|
|
182
|
+
if (!node.config.tcpHost || node.config.tcpHost.trim() === '') {
|
|
183
|
+
throw new Error('TCP主机地址未配置,请在节点配置中填写Modbus服务器IP地址');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tcpMode = node.config.tcpMode || "telnet";
|
|
187
|
+
const modeNames = {
|
|
188
|
+
"telnet": "Telnet ASCII",
|
|
189
|
+
"rtu": "RTU over TCP",
|
|
190
|
+
"tcp": "Modbus TCP"
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
node.log(`正在连接TCP网关(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}...`);
|
|
194
|
+
|
|
195
|
+
if (tcpMode === "telnet") {
|
|
196
|
+
await node.client.connectTelnet(node.config.tcpHost, {
|
|
197
|
+
port: node.config.tcpPort
|
|
198
|
+
});
|
|
199
|
+
} else if (tcpMode === "rtu") {
|
|
200
|
+
await node.client.connectTcpRTUBuffered(node.config.tcpHost, {
|
|
201
|
+
port: node.config.tcpPort
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
await node.client.connectTCP(node.config.tcpHost, {
|
|
205
|
+
port: node.config.tcpPort
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
node.log(`TCP网关连接成功(${modeNames[tcpMode]}): ${node.config.tcpHost}:${node.config.tcpPort}`);
|
|
170
210
|
} else {
|
|
171
211
|
await node.client.connectRTUBuffered(node.config.serialPort, {
|
|
172
|
-
baudRate: node.config.serialBaudRate,
|
|
173
|
-
dataBits: node.config.serialDataBits,
|
|
174
|
-
stopBits: node.config.serialStopBits,
|
|
175
|
-
parity: node.config.serialParity
|
|
212
|
+
baudRate: node.config.serialBaudRate || 9600,
|
|
213
|
+
dataBits: node.config.serialDataBits || 8,
|
|
214
|
+
stopBits: node.config.serialStopBits || 1,
|
|
215
|
+
parity: node.config.serialParity || 'none'
|
|
176
216
|
});
|
|
177
|
-
node.log(
|
|
217
|
+
node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
178
218
|
}
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
|
|
220
|
+
// 设置超时时间(串口需要更长的超时时间)
|
|
221
|
+
const timeout = node.config.connectionType === "serial" ? 10000 : 5000;
|
|
222
|
+
node.client.setTimeout(timeout);
|
|
223
|
+
node.log(`Modbus超时设置: ${timeout}ms`);
|
|
181
224
|
node.isConnected = true;
|
|
182
225
|
node.log(`Modbus已连接: ${node.config.connectionType === "tcp" ? `${node.config.tcpHost}:${node.config.tcpPort}` : node.config.serialPort}`);
|
|
183
226
|
node.updateNodeStatus();
|
|
184
|
-
|
|
227
|
+
|
|
185
228
|
// 清除错误日志记录(重新部署或重连时允许再次显示错误)
|
|
186
229
|
node.lastErrorLog = {};
|
|
187
230
|
node.lastMqttErrorLog = 0;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
231
|
+
node._discoveryPublished = false; // 重置Discovery发布标志
|
|
232
|
+
|
|
233
|
+
// 添加Symi按键事件监听(串口模式)
|
|
234
|
+
if (node.config.connectionType === "serial" && node.client._port) {
|
|
235
|
+
node.client._port.on('data', (data) => {
|
|
236
|
+
node.handleSymiButtonEvent(data);
|
|
237
|
+
});
|
|
238
|
+
node.log('已启用Symi按键事件监听(串口模式)');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 立即启动轮询(不等待MQTT连接)
|
|
242
|
+
// 修复:确保轮询在Modbus连接成功后立即启动,不依赖MQTT状态
|
|
243
|
+
if (!node.pollTimer) {
|
|
244
|
+
node.log('Modbus连接成功,立即启动轮询...');
|
|
245
|
+
node.startPolling();
|
|
246
|
+
} else {
|
|
247
|
+
node.log('轮询已在运行中');
|
|
248
|
+
}
|
|
249
|
+
|
|
192
250
|
} catch (err) {
|
|
193
251
|
node.error(`Modbus连接失败: ${err.message}`);
|
|
194
252
|
node.isConnected = false;
|
|
@@ -281,6 +339,7 @@ module.exports = function(RED) {
|
|
|
281
339
|
// 连接MQTT(带智能重试和fallback)
|
|
282
340
|
node.connectMqtt = function() {
|
|
283
341
|
if (!node.config.enableMqtt) {
|
|
342
|
+
node.log('MQTT未启用,跳过MQTT连接');
|
|
284
343
|
return;
|
|
285
344
|
}
|
|
286
345
|
|
|
@@ -299,7 +358,7 @@ module.exports = function(RED) {
|
|
|
299
358
|
node.log(`正在连接MQTT broker: ${brokerCandidates[0]}`);
|
|
300
359
|
|
|
301
360
|
const options = {
|
|
302
|
-
clientId: `modbus_master_${Math.random().toString(16).
|
|
361
|
+
clientId: `modbus_master_${Math.random().toString(16).substring(2, 10)}`,
|
|
303
362
|
clean: false, // 持久化会话,断线重连后继续接收消息
|
|
304
363
|
reconnectPeriod: 0, // 禁用自动重连,我们手动管理
|
|
305
364
|
connectTimeout: 5000, // 5秒连接超时
|
|
@@ -309,9 +368,7 @@ module.exports = function(RED) {
|
|
|
309
368
|
if (node.config.mqttUsername) {
|
|
310
369
|
options.username = node.config.mqttUsername;
|
|
311
370
|
options.password = node.config.mqttPassword;
|
|
312
|
-
node.log(`MQTT认证: 用户名=${node.config.mqttUsername}
|
|
313
|
-
} else {
|
|
314
|
-
node.warn('MQTT未配置认证信息,如果broker需要认证将会连接失败');
|
|
371
|
+
node.log(`MQTT认证: 用户名=${node.config.mqttUsername}`);
|
|
315
372
|
}
|
|
316
373
|
|
|
317
374
|
// 尝试连接函数
|
|
@@ -332,18 +389,20 @@ module.exports = function(RED) {
|
|
|
332
389
|
node.mqttClient.on('connect', () => {
|
|
333
390
|
node.mqttConnected = true;
|
|
334
391
|
node.log(`MQTT已连接: ${brokerUrl}`);
|
|
335
|
-
|
|
392
|
+
|
|
336
393
|
// 成功连接后,更新配置的broker地址(下次优先使用成功的地址)
|
|
337
394
|
if (brokerUrl !== brokerCandidates[0]) {
|
|
338
395
|
node.log(`使用fallback地址成功: ${brokerUrl}(原配置: ${brokerCandidates[0]})`);
|
|
339
396
|
}
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
397
|
+
|
|
398
|
+
// 异步发送设备发现消息(避免阻塞事件循环)
|
|
399
|
+
setImmediate(() => {
|
|
400
|
+
node.publishDiscovery();
|
|
401
|
+
});
|
|
402
|
+
|
|
344
403
|
// 订阅命令主题
|
|
345
404
|
node.subscribeCommands();
|
|
346
|
-
|
|
405
|
+
|
|
347
406
|
// 更新状态显示
|
|
348
407
|
node.updateNodeStatus();
|
|
349
408
|
});
|
|
@@ -454,6 +513,12 @@ module.exports = function(RED) {
|
|
|
454
513
|
return;
|
|
455
514
|
}
|
|
456
515
|
|
|
516
|
+
// 检查是否已经发布过(避免重复)
|
|
517
|
+
if (node._discoveryPublished) {
|
|
518
|
+
node.log('Discovery已发布,跳过重复发布');
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
457
522
|
let discoveryCount = 0;
|
|
458
523
|
|
|
459
524
|
node.config.slaves.forEach((slave) => {
|
|
@@ -464,7 +529,7 @@ module.exports = function(RED) {
|
|
|
464
529
|
node.mqttClient.publish(availabilityTopic, 'online', { retain: true });
|
|
465
530
|
|
|
466
531
|
for (let coil = slave.coilStart; coil <= slave.coilEnd; coil++) {
|
|
467
|
-
// 使用稳定的唯一ID:从站地址_
|
|
532
|
+
// 使用稳定的唯一ID:从站地址_线圈编号(确保全局唯一)
|
|
468
533
|
const uniqueId = `modbus_relay_${slaveId}_${coil}`;
|
|
469
534
|
const objectId = `relay_${slaveId}_${coil}`;
|
|
470
535
|
const discoveryTopic = `homeassistant/switch/${uniqueId}/config`;
|
|
@@ -506,15 +571,11 @@ module.exports = function(RED) {
|
|
|
506
571
|
};
|
|
507
572
|
|
|
508
573
|
// 发布发现消息(retain=true 确保HA重启后仍能发现)
|
|
574
|
+
// 使用QoS=0避免阻塞,发现消息只需发送一次即可
|
|
509
575
|
node.mqttClient.publish(
|
|
510
576
|
discoveryTopic,
|
|
511
577
|
JSON.stringify(discoveryPayload),
|
|
512
|
-
{ retain: true, qos:
|
|
513
|
-
(err) => {
|
|
514
|
-
if (err) {
|
|
515
|
-
node.error(`发布发现消息失败 ${uniqueId}: ${err.message}`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
578
|
+
{ retain: true, qos: 0 }
|
|
518
579
|
);
|
|
519
580
|
|
|
520
581
|
discoveryCount++;
|
|
@@ -523,6 +584,9 @@ module.exports = function(RED) {
|
|
|
523
584
|
|
|
524
585
|
const slaveAddresses = node.config.slaves.map(s => s.address).join(', ');
|
|
525
586
|
node.log(`已发布 ${discoveryCount} 个MQTT发现消息(从站地址: ${slaveAddresses})`);
|
|
587
|
+
|
|
588
|
+
// 标记为已发布(避免重复)
|
|
589
|
+
node._discoveryPublished = true;
|
|
526
590
|
};
|
|
527
591
|
|
|
528
592
|
// 订阅命令主题
|
|
@@ -542,109 +606,179 @@ module.exports = function(RED) {
|
|
|
542
606
|
});
|
|
543
607
|
};
|
|
544
608
|
|
|
545
|
-
//
|
|
609
|
+
// MQTT命令队列(去重 + 防抖)
|
|
610
|
+
node.mqttCommandQueue = new Map(); // key: "slaveId_coil", value: {slaveId, coil, value, timestamp}
|
|
611
|
+
node.mqttCommandTimer = null;
|
|
612
|
+
node.mqttCommandDebounceMs = 50; // 防抖时间:50ms
|
|
613
|
+
|
|
614
|
+
// 处理MQTT命令队列
|
|
615
|
+
node.processMqttCommandQueue = async function() {
|
|
616
|
+
if (node.mqttCommandQueue.size === 0) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 获取所有待处理命令
|
|
621
|
+
const commands = Array.from(node.mqttCommandQueue.values());
|
|
622
|
+
node.mqttCommandQueue.clear();
|
|
623
|
+
|
|
624
|
+
// 按优先级排序:物理按键 > HA控制
|
|
625
|
+
// 这里简化处理,直接按时间戳排序(最新的优先)
|
|
626
|
+
commands.sort((a, b) => b.timestamp - a.timestamp);
|
|
627
|
+
|
|
628
|
+
// 批量执行命令
|
|
629
|
+
for (const cmd of commands) {
|
|
630
|
+
try {
|
|
631
|
+
await node.writeSingleCoil(cmd.slaveId, cmd.coil, cmd.value);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
node.error(`MQTT命令执行失败: 从站${cmd.slaveId} 线圈${cmd.coil} - ${err.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// 处理MQTT命令(异步 + 去重 + 防抖)
|
|
546
639
|
node.handleMqttCommand = async function(topic, message) {
|
|
547
640
|
const parts = topic.split('/');
|
|
548
641
|
if (parts.length < 4) {
|
|
549
642
|
return;
|
|
550
643
|
}
|
|
551
|
-
|
|
644
|
+
|
|
552
645
|
const slaveId = parseInt(parts[parts.length - 3]);
|
|
553
646
|
const coil = parseInt(parts[parts.length - 2]);
|
|
554
647
|
const command = message.toString();
|
|
555
|
-
|
|
648
|
+
|
|
556
649
|
const value = (command === 'ON' || command === 'true' || command === '1');
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
650
|
+
|
|
651
|
+
// 添加到队列(去重:相同从站+线圈的命令只保留最新的)
|
|
652
|
+
const key = `${slaveId}_${coil}`;
|
|
653
|
+
node.mqttCommandQueue.set(key, {
|
|
654
|
+
slaveId,
|
|
655
|
+
coil,
|
|
656
|
+
value,
|
|
657
|
+
timestamp: Date.now()
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// 防抖:延迟执行,合并短时间内的多个命令
|
|
661
|
+
if (node.mqttCommandTimer) {
|
|
662
|
+
clearTimeout(node.mqttCommandTimer);
|
|
565
663
|
}
|
|
664
|
+
|
|
665
|
+
node.mqttCommandTimer = setTimeout(() => {
|
|
666
|
+
node.mqttCommandTimer = null;
|
|
667
|
+
node.processMqttCommandQueue();
|
|
668
|
+
}, node.mqttCommandDebounceMs);
|
|
566
669
|
};
|
|
567
670
|
|
|
568
671
|
// 发布MQTT状态
|
|
569
672
|
node.publishMqttState = function(slaveId, coil, value) {
|
|
570
673
|
if (!node.mqttClient || !node.mqttClient.connected) {
|
|
571
|
-
|
|
674
|
+
// 不输出警告,避免日志过多
|
|
572
675
|
return;
|
|
573
676
|
}
|
|
574
|
-
|
|
677
|
+
|
|
575
678
|
const stateTopic = `${node.config.mqttBaseTopic}/${slaveId}/${coil}/state`;
|
|
576
679
|
const payload = value ? 'ON' : 'OFF';
|
|
577
|
-
|
|
578
|
-
// 使用QoS=
|
|
579
|
-
|
|
680
|
+
|
|
681
|
+
// 使用QoS=0提高性能,retain=true确保断线重连后可获取最新状态
|
|
682
|
+
// QoS=0不等待确认,避免阻塞轮询
|
|
683
|
+
node.mqttClient.publish(stateTopic, payload, { qos: 0, retain: true }, (err) => {
|
|
580
684
|
if (err) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
685
|
+
// 只在首次错误时输出警告
|
|
686
|
+
if (!node.lastMqttPublishError || Date.now() - node.lastMqttPublishError > 60000) {
|
|
687
|
+
node.warn(`发布状态失败: ${stateTopic} - ${err.message}`);
|
|
688
|
+
node.lastMqttPublishError = Date.now();
|
|
689
|
+
}
|
|
584
690
|
}
|
|
585
691
|
});
|
|
586
692
|
};
|
|
587
693
|
|
|
588
|
-
//
|
|
694
|
+
// 开始轮询(使用递归调用而非定时器,避免并发)
|
|
589
695
|
node.startPolling = function() {
|
|
590
|
-
if (node.
|
|
696
|
+
if (node.isPolling) {
|
|
591
697
|
return;
|
|
592
698
|
}
|
|
593
|
-
|
|
699
|
+
|
|
594
700
|
// 清除错误日志记录(重新开始轮询时允许显示错误)
|
|
595
701
|
node.lastErrorLog = {};
|
|
596
702
|
node.lastMqttErrorLog = 0;
|
|
597
|
-
|
|
598
|
-
const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd})`).join(', ');
|
|
703
|
+
|
|
704
|
+
const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd},间隔${s.pollInterval}ms)`).join(', ');
|
|
599
705
|
node.log(`开始轮询 ${node.config.slaves.length} 个从站设备: ${slaveList}`);
|
|
600
706
|
node.log(`Modbus连接: ${node.isConnected ? '已连接' : '未连接'}, MQTT连接: ${node.mqttConnected ? '已连接' : '未连接'}`);
|
|
601
707
|
node.currentSlaveIndex = 0;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
708
|
+
node.isPolling = true;
|
|
709
|
+
|
|
710
|
+
// 使用递归调用实现轮询,确保不会并发
|
|
711
|
+
const pollLoop = async () => {
|
|
712
|
+
if (!node.isPolling || !node.isConnected || node.isClosing) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 获取当前从站配置(在轮询前获取,确保使用正确的间隔)
|
|
717
|
+
const currentSlave = node.config.slaves[node.currentSlaveIndex];
|
|
718
|
+
if (!currentSlave) {
|
|
719
|
+
node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
|
|
720
|
+
node.currentSlaveIndex = 0;
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const interval = currentSlave.pollInterval || 200;
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
await node.pollNextSlave();
|
|
728
|
+
} catch (err) {
|
|
608
729
|
node.error(`轮询错误: ${err.message}`);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 等待指定间隔后继续下一次轮询
|
|
733
|
+
node.pollTimer = setTimeout(pollLoop, interval);
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// 立即开始轮询
|
|
737
|
+
pollLoop();
|
|
616
738
|
};
|
|
617
739
|
|
|
618
740
|
// 停止轮询
|
|
619
741
|
node.stopPolling = function() {
|
|
742
|
+
node.isPolling = false;
|
|
620
743
|
if (node.pollTimer) {
|
|
621
|
-
|
|
744
|
+
clearTimeout(node.pollTimer);
|
|
622
745
|
node.pollTimer = null;
|
|
623
746
|
node.log('停止轮询');
|
|
624
747
|
}
|
|
625
748
|
};
|
|
626
749
|
|
|
627
|
-
//
|
|
750
|
+
// 轮询下一个从站(串行执行,不会并发)
|
|
628
751
|
node.pollNextSlave = async function() {
|
|
629
752
|
if (!node.isConnected || node.isClosing) {
|
|
630
753
|
return;
|
|
631
754
|
}
|
|
632
|
-
|
|
633
|
-
//
|
|
634
|
-
if (node.
|
|
635
|
-
|
|
755
|
+
|
|
756
|
+
// 检查是否暂停轮询(从站上报时优先处理)
|
|
757
|
+
if (node.pausePolling) {
|
|
758
|
+
node.pollingPausedCount++;
|
|
759
|
+
return; // 暂停轮询,优先处理从站上报
|
|
636
760
|
}
|
|
637
|
-
|
|
761
|
+
|
|
638
762
|
// 获取当前从站配置
|
|
639
763
|
const slave = node.config.slaves[node.currentSlaveIndex];
|
|
640
764
|
if (!slave) {
|
|
765
|
+
node.warn(`从站索引${node.currentSlaveIndex}无效,重置为0`);
|
|
641
766
|
node.currentSlaveIndex = 0;
|
|
642
767
|
return;
|
|
643
768
|
}
|
|
644
|
-
|
|
769
|
+
|
|
645
770
|
const slaveId = slave.address;
|
|
771
|
+
const deviceState = node.deviceStates[slaveId];
|
|
772
|
+
|
|
773
|
+
// 检查该从站是否被临时忽略(连续超时过多)
|
|
774
|
+
if (deviceState.isIgnored) {
|
|
775
|
+
// 移动到下一个从站
|
|
776
|
+
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
646
780
|
const coilCount = slave.coilEnd - slave.coilStart + 1;
|
|
647
|
-
|
|
781
|
+
|
|
648
782
|
// 检查是否刚写入过(写入后100ms内不轮询该从站,避免读到旧值)
|
|
649
783
|
const lastWrite = node.lastWriteTime[slaveId] || 0;
|
|
650
784
|
const timeSinceWrite = Date.now() - lastWrite;
|
|
@@ -653,33 +787,40 @@ module.exports = function(RED) {
|
|
|
653
787
|
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
654
788
|
return;
|
|
655
789
|
}
|
|
656
|
-
|
|
790
|
+
|
|
791
|
+
// 检查锁状态(如果有写操作正在进行,跳过本次轮询)
|
|
792
|
+
if (node.modbusLock) {
|
|
793
|
+
// 移动到下一个从站
|
|
794
|
+
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
657
798
|
try {
|
|
658
|
-
//
|
|
799
|
+
// 设置锁(防止轮询期间有写操作)
|
|
659
800
|
node.modbusLock = true;
|
|
660
|
-
|
|
801
|
+
|
|
661
802
|
node.client.setID(slaveId);
|
|
662
|
-
|
|
803
|
+
|
|
663
804
|
// 读取线圈状态(功能码01)
|
|
664
805
|
const data = await node.client.readCoils(slave.coilStart, coilCount);
|
|
665
|
-
|
|
666
|
-
// 释放锁
|
|
667
|
-
node.modbusLock = false;
|
|
668
|
-
|
|
806
|
+
|
|
669
807
|
// 更新设备状态
|
|
670
808
|
const isFirstPoll = !node.deviceStates[slaveId].initialPublished;
|
|
671
809
|
let publishCount = 0;
|
|
672
|
-
|
|
810
|
+
|
|
673
811
|
for (let i = 0; i < coilCount; i++) {
|
|
674
812
|
const coilIndex = slave.coilStart + i;
|
|
675
813
|
const oldValue = node.deviceStates[slaveId].coils[coilIndex];
|
|
676
814
|
const newValue = data.data[i];
|
|
677
|
-
|
|
815
|
+
|
|
678
816
|
node.deviceStates[slaveId].coils[coilIndex] = newValue;
|
|
679
|
-
|
|
817
|
+
|
|
680
818
|
// 第一次轮询或状态改变时,发布到MQTT和触发事件
|
|
681
819
|
if (isFirstPoll || oldValue !== newValue) {
|
|
682
|
-
|
|
820
|
+
// 异步发布MQTT,不阻塞轮询
|
|
821
|
+
setImmediate(() => {
|
|
822
|
+
node.publishMqttState(slaveId, coilIndex, newValue);
|
|
823
|
+
});
|
|
683
824
|
publishCount++;
|
|
684
825
|
node.emit('stateUpdate', {
|
|
685
826
|
slave: slaveId,
|
|
@@ -688,17 +829,21 @@ module.exports = function(RED) {
|
|
|
688
829
|
});
|
|
689
830
|
}
|
|
690
831
|
}
|
|
691
|
-
|
|
832
|
+
|
|
692
833
|
if (isFirstPoll) {
|
|
693
|
-
node.log(`从站${slaveId}
|
|
834
|
+
node.log(`从站${slaveId}首次轮询成功,读取${coilCount}个线圈`);
|
|
694
835
|
} else if (publishCount > 0) {
|
|
695
|
-
node.log(`从站${slaveId}
|
|
836
|
+
node.log(`从站${slaveId}状态变化: ${publishCount}个线圈`);
|
|
696
837
|
}
|
|
697
|
-
|
|
838
|
+
|
|
698
839
|
node.deviceStates[slaveId].lastUpdate = Date.now();
|
|
699
840
|
node.deviceStates[slaveId].error = null;
|
|
700
|
-
node.deviceStates[slaveId].initialPublished = true;
|
|
701
|
-
|
|
841
|
+
node.deviceStates[slaveId].initialPublished = true;
|
|
842
|
+
|
|
843
|
+
// 轮询成功,重置超时计数
|
|
844
|
+
node.deviceStates[slaveId].timeoutCount = 0;
|
|
845
|
+
node.deviceStates[slaveId].isIgnored = false;
|
|
846
|
+
|
|
702
847
|
// 输出消息
|
|
703
848
|
const output = {
|
|
704
849
|
payload: {
|
|
@@ -710,46 +855,78 @@ module.exports = function(RED) {
|
|
|
710
855
|
timestamp: node.deviceStates[slaveId].lastUpdate
|
|
711
856
|
}
|
|
712
857
|
};
|
|
713
|
-
|
|
858
|
+
|
|
714
859
|
node.send(output);
|
|
715
|
-
|
|
860
|
+
|
|
716
861
|
// 更新状态显示
|
|
717
862
|
node.updateNodeStatus();
|
|
718
|
-
|
|
863
|
+
|
|
864
|
+
// 释放锁
|
|
865
|
+
node.modbusLock = false;
|
|
866
|
+
|
|
719
867
|
} catch (err) {
|
|
868
|
+
|
|
720
869
|
// 释放锁
|
|
721
870
|
node.modbusLock = false;
|
|
722
|
-
|
|
871
|
+
|
|
723
872
|
node.deviceStates[slaveId].error = err.message;
|
|
724
|
-
|
|
873
|
+
node.deviceStates[slaveId].lastTimeoutTime = Date.now();
|
|
874
|
+
|
|
725
875
|
// 日志限流:每个从站的错误日志最多每10分钟输出一次
|
|
726
876
|
const now = Date.now();
|
|
727
877
|
const lastLogTime = node.lastErrorLog[slaveId] || 0;
|
|
728
878
|
const shouldLog = (now - lastLogTime) > node.errorLogInterval;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
879
|
+
|
|
880
|
+
// 检查是否是超时错误
|
|
881
|
+
const isTimeout = err.message && (
|
|
882
|
+
err.message.includes('Timed out') ||
|
|
883
|
+
err.message.includes('ETIMEDOUT') ||
|
|
884
|
+
err.message.includes('timeout')
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
if (isTimeout) {
|
|
888
|
+
// 增加超时计数
|
|
889
|
+
node.deviceStates[slaveId].timeoutCount++;
|
|
890
|
+
|
|
891
|
+
// 连续超时5次后临时忽略该从站(避免拖慢其他从站)
|
|
892
|
+
if (node.deviceStates[slaveId].timeoutCount >= 5) {
|
|
893
|
+
node.deviceStates[slaveId].isIgnored = true;
|
|
894
|
+
if (shouldLog) {
|
|
895
|
+
node.warn(`从站${slaveId}连续超时${node.deviceStates[slaveId].timeoutCount}次,临时忽略该从站(重启或重新部署后恢复)`);
|
|
896
|
+
node.lastErrorLog[slaveId] = now;
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
if (shouldLog) {
|
|
900
|
+
node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [超时${node.deviceStates[slaveId].timeoutCount}/5]`);
|
|
901
|
+
node.lastErrorLog[slaveId] = now;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
// 非超时错误,记录日志
|
|
906
|
+
if (shouldLog) {
|
|
907
|
+
node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
|
|
908
|
+
node.lastErrorLog[slaveId] = now;
|
|
909
|
+
}
|
|
733
910
|
}
|
|
734
|
-
|
|
911
|
+
|
|
735
912
|
// 更新状态显示
|
|
736
913
|
node.updateNodeStatus();
|
|
737
|
-
|
|
914
|
+
|
|
738
915
|
// 检测是否是连接断开错误
|
|
739
|
-
if (err.message &&
|
|
740
|
-
(err.message.includes('ECONNRESET') ||
|
|
741
|
-
err.message.includes('ETIMEDOUT') ||
|
|
916
|
+
if (err.message &&
|
|
917
|
+
(err.message.includes('ECONNRESET') ||
|
|
918
|
+
err.message.includes('ETIMEDOUT') ||
|
|
742
919
|
err.message.includes('ENOTCONN') ||
|
|
743
920
|
err.message.includes('Port Not Open'))) {
|
|
744
|
-
|
|
921
|
+
|
|
745
922
|
// 连接断开,尝试重连
|
|
746
923
|
if (shouldLog) {
|
|
747
924
|
node.warn('检测到连接断开,尝试重连...');
|
|
748
925
|
}
|
|
749
|
-
|
|
926
|
+
|
|
750
927
|
node.isConnected = false;
|
|
751
928
|
node.stopPolling();
|
|
752
|
-
|
|
929
|
+
|
|
753
930
|
// 关闭当前连接
|
|
754
931
|
if (node.client && node.client.isOpen) {
|
|
755
932
|
try {
|
|
@@ -758,7 +935,7 @@ module.exports = function(RED) {
|
|
|
758
935
|
// 忽略关闭错误
|
|
759
936
|
}
|
|
760
937
|
}
|
|
761
|
-
|
|
938
|
+
|
|
762
939
|
// 尝试重连
|
|
763
940
|
if (!node.isClosing && !node.reconnectTimer) {
|
|
764
941
|
node.reconnectTimer = setTimeout(() => {
|
|
@@ -768,28 +945,102 @@ module.exports = function(RED) {
|
|
|
768
945
|
}
|
|
769
946
|
}
|
|
770
947
|
}
|
|
771
|
-
|
|
948
|
+
|
|
772
949
|
// 移动到下一个从站
|
|
773
950
|
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
774
951
|
};
|
|
775
952
|
|
|
776
|
-
//
|
|
953
|
+
// 处理Symi按键事件(私有协议)
|
|
954
|
+
node.handleSymiButtonEvent = function(data) {
|
|
955
|
+
try {
|
|
956
|
+
// 解析Symi协议帧
|
|
957
|
+
const frame = protocol.parseFrame(data);
|
|
958
|
+
if (!frame) {
|
|
959
|
+
return; // 不是有效的Symi帧,忽略
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// 只处理SET类型(0x03)的按键事件,忽略REPORT类型(0x04)
|
|
963
|
+
// REPORT类型是面板对我们指令的确认,不是按键事件
|
|
964
|
+
if (frame.dataType !== 0x03) {
|
|
965
|
+
return; // 不是按键事件
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// 检查是否是灯光设备
|
|
969
|
+
if (frame.deviceType !== 0x01) {
|
|
970
|
+
return; // 不是灯光设备
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// 提取按键信息
|
|
974
|
+
const deviceAddr = frame.deviceAddr; // 设备地址(1-255)
|
|
975
|
+
const channel = frame.channel; // 通道号(1-8)
|
|
976
|
+
const state = frame.opInfo[0] === 0x01; // 状态(1=开,0=关)
|
|
977
|
+
|
|
978
|
+
node.log(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
979
|
+
|
|
980
|
+
// 查找对应的从站和线圈
|
|
981
|
+
// 假设:设备地址1对应从站10,设备地址2对应从站11,以此类推
|
|
982
|
+
// 通道号直接对应线圈号(1-8 → 0-7)
|
|
983
|
+
const slaveId = 10 + (deviceAddr - 1);
|
|
984
|
+
const coilNumber = channel - 1;
|
|
985
|
+
|
|
986
|
+
// 检查从站是否在配置中
|
|
987
|
+
const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
|
|
988
|
+
if (!slaveConfig) {
|
|
989
|
+
node.warn(`Symi按键事件: 从站${slaveId}未配置,忽略`);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// 检查线圈是否在范围内
|
|
994
|
+
if (coilNumber < slaveConfig.coilStart || coilNumber > slaveConfig.coilEnd) {
|
|
995
|
+
node.warn(`Symi按键事件: 线圈${coilNumber}不在从站${slaveId}的范围内(${slaveConfig.coilStart}-${slaveConfig.coilEnd}),忽略`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
node.log(`Symi按键映射: 设备${deviceAddr}通道${channel} → 从站${slaveId}线圈${coilNumber}`);
|
|
1000
|
+
|
|
1001
|
+
// 写入线圈(异步,不阻塞)
|
|
1002
|
+
node.writeSingleCoil(slaveId, coilNumber, state).catch(err => {
|
|
1003
|
+
node.error(`Symi按键控制失败: ${err.message}`);
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// 发送应答帧(REPORT类型0x04,反馈LED状态)
|
|
1007
|
+
// 协议规定:面板发送0x03(SET)按键事件,主机应该用0x04(REPORT)反馈LED状态
|
|
1008
|
+
const responseFrame = protocol.buildSingleLightReport(0x01, deviceAddr, channel, state);
|
|
1009
|
+
if (node.client._port && node.client._port.write) {
|
|
1010
|
+
node.client._port.write(responseFrame);
|
|
1011
|
+
node.log(`Symi应答已发送(REPORT): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
// 静默处理错误,避免干扰Modbus通信
|
|
1016
|
+
// node.warn(`Symi按键事件处理错误: ${err.message}`);
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
// 写单个线圈(带互斥锁和轮询暂停)
|
|
777
1021
|
node.writeSingleCoil = async function(slaveId, coil, value) {
|
|
778
1022
|
if (!node.isConnected) {
|
|
779
1023
|
node.warn('Modbus未连接');
|
|
780
1024
|
return;
|
|
781
1025
|
}
|
|
782
|
-
|
|
783
|
-
//
|
|
784
|
-
|
|
1026
|
+
|
|
1027
|
+
// 暂停轮询(写操作优先)
|
|
1028
|
+
node.pausePolling = true;
|
|
1029
|
+
const pauseStartTime = Date.now();
|
|
1030
|
+
|
|
1031
|
+
// 等待锁释放(最多等待1000ms,减少超时时间)
|
|
1032
|
+
const maxWait = 1000;
|
|
785
1033
|
const startWait = Date.now();
|
|
1034
|
+
let waitCount = 0;
|
|
786
1035
|
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
787
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1036
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1037
|
+
waitCount++;
|
|
788
1038
|
}
|
|
789
|
-
|
|
1039
|
+
|
|
790
1040
|
if (node.modbusLock) {
|
|
791
|
-
|
|
792
|
-
|
|
1041
|
+
// 强制释放锁(避免死锁)
|
|
1042
|
+
node.warn(`写入线圈等待超时(${waitCount * 10}ms),强制释放锁: 从站${slaveId} 线圈${coil}`);
|
|
1043
|
+
node.modbusLock = false;
|
|
793
1044
|
}
|
|
794
1045
|
|
|
795
1046
|
try {
|
|
@@ -818,31 +1069,51 @@ module.exports = function(RED) {
|
|
|
818
1069
|
// 释放锁
|
|
819
1070
|
node.modbusLock = false;
|
|
820
1071
|
|
|
1072
|
+
// 延迟恢复轮询(给从站响应预留时间)
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
node.pausePolling = false;
|
|
1075
|
+
const pauseDuration = Date.now() - pauseStartTime;
|
|
1076
|
+
if (node.pollingPausedCount > 0) {
|
|
1077
|
+
node.log(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1078
|
+
node.pollingPausedCount = 0;
|
|
1079
|
+
}
|
|
1080
|
+
}, 100);
|
|
1081
|
+
|
|
821
1082
|
} catch (err) {
|
|
822
1083
|
// 释放锁
|
|
823
1084
|
node.modbusLock = false;
|
|
824
1085
|
|
|
1086
|
+
// 恢复轮询
|
|
1087
|
+
node.pausePolling = false;
|
|
1088
|
+
node.pollingPausedCount = 0;
|
|
1089
|
+
|
|
825
1090
|
node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
|
|
826
1091
|
throw err; // 抛出错误,让调用者知道失败
|
|
827
1092
|
}
|
|
828
1093
|
};
|
|
829
1094
|
|
|
830
|
-
//
|
|
1095
|
+
// 批量写入多个线圈(带互斥锁和轮询暂停)
|
|
831
1096
|
node.writeMultipleCoils = async function(slaveId, startCoil, values) {
|
|
832
1097
|
if (!node.isConnected) {
|
|
833
1098
|
node.warn('Modbus未连接');
|
|
834
1099
|
return;
|
|
835
1100
|
}
|
|
836
1101
|
|
|
837
|
-
//
|
|
838
|
-
|
|
1102
|
+
// 暂停轮询(从站上报优先处理)
|
|
1103
|
+
node.pausePolling = true;
|
|
1104
|
+
const pauseStartTime = Date.now();
|
|
1105
|
+
|
|
1106
|
+
// 等待锁释放(最多等待500ms)
|
|
1107
|
+
const maxWait = 500;
|
|
839
1108
|
const startWait = Date.now();
|
|
840
1109
|
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
841
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
1110
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
842
1111
|
}
|
|
843
1112
|
|
|
844
1113
|
if (node.modbusLock) {
|
|
845
|
-
node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (
|
|
1114
|
+
node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (等待锁释放超时)`);
|
|
1115
|
+
// 恢复轮询
|
|
1116
|
+
node.pausePolling = false;
|
|
846
1117
|
return;
|
|
847
1118
|
}
|
|
848
1119
|
|
|
@@ -873,10 +1144,24 @@ module.exports = function(RED) {
|
|
|
873
1144
|
// 释放锁
|
|
874
1145
|
node.modbusLock = false;
|
|
875
1146
|
|
|
1147
|
+
// 延迟恢复轮询(给从站响应预留时间)
|
|
1148
|
+
setTimeout(() => {
|
|
1149
|
+
node.pausePolling = false;
|
|
1150
|
+
const pauseDuration = Date.now() - pauseStartTime;
|
|
1151
|
+
if (node.pollingPausedCount > 0) {
|
|
1152
|
+
node.log(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1153
|
+
node.pollingPausedCount = 0;
|
|
1154
|
+
}
|
|
1155
|
+
}, 100);
|
|
1156
|
+
|
|
876
1157
|
} catch (err) {
|
|
877
1158
|
// 释放锁
|
|
878
1159
|
node.modbusLock = false;
|
|
879
1160
|
|
|
1161
|
+
// 恢复轮询
|
|
1162
|
+
node.pausePolling = false;
|
|
1163
|
+
node.pollingPausedCount = 0;
|
|
1164
|
+
|
|
880
1165
|
node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
|
|
881
1166
|
throw err; // 抛出错误,让调用者知道失败
|
|
882
1167
|
}
|
|
@@ -955,29 +1240,49 @@ module.exports = function(RED) {
|
|
|
955
1240
|
node.publishOfflineStatus();
|
|
956
1241
|
}
|
|
957
1242
|
|
|
1243
|
+
// 清理设备状态缓存(释放内存)
|
|
1244
|
+
node.deviceStates = {};
|
|
1245
|
+
node.lastErrorLog = {};
|
|
1246
|
+
node.lastWriteTime = {};
|
|
1247
|
+
node.lastMqttErrorLog = 0;
|
|
1248
|
+
|
|
958
1249
|
// 关闭Modbus连接
|
|
959
|
-
if (node.client
|
|
1250
|
+
if (node.client) {
|
|
960
1251
|
try {
|
|
961
|
-
node.client.
|
|
962
|
-
node.
|
|
963
|
-
|
|
1252
|
+
if (node.client.isOpen) {
|
|
1253
|
+
node.client.close(() => {
|
|
1254
|
+
node.log('Modbus连接已关闭');
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
964
1257
|
} catch (err) {
|
|
965
1258
|
node.warn(`关闭Modbus连接时出错: ${err.message}`);
|
|
966
1259
|
}
|
|
1260
|
+
node.client = null; // 释放引用
|
|
967
1261
|
}
|
|
968
1262
|
|
|
969
1263
|
// 关闭MQTT连接
|
|
970
|
-
if (node.mqttClient
|
|
1264
|
+
if (node.mqttClient) {
|
|
971
1265
|
try {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
node.mqttClient.
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1266
|
+
if (node.mqttClient.connected) {
|
|
1267
|
+
// 移除所有监听器(防止内存泄漏)
|
|
1268
|
+
node.mqttClient.removeAllListeners();
|
|
1269
|
+
|
|
1270
|
+
// 等待离线消息发送后再关闭
|
|
1271
|
+
setTimeout(() => {
|
|
1272
|
+
node.mqttClient.end(false, () => {
|
|
1273
|
+
node.log('MQTT连接已关闭');
|
|
1274
|
+
node.mqttClient = null; // 释放引用
|
|
1275
|
+
done();
|
|
1276
|
+
});
|
|
1277
|
+
}, 100);
|
|
1278
|
+
} else {
|
|
1279
|
+
node.mqttClient.removeAllListeners();
|
|
1280
|
+
node.mqttClient = null;
|
|
1281
|
+
done();
|
|
1282
|
+
}
|
|
979
1283
|
} catch (err) {
|
|
980
1284
|
node.warn(`关闭MQTT连接时出错: ${err.message}`);
|
|
1285
|
+
node.mqttClient = null;
|
|
981
1286
|
done();
|
|
982
1287
|
}
|
|
983
1288
|
} else {
|