node-red-contrib-symi-modbus 2.6.6 → 2.6.8
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 +276 -44
- package/nodes/homekit-bridge.html +251 -0
- package/nodes/homekit-bridge.js +328 -0
- package/nodes/lightweight-protocol.js +13 -1
- package/nodes/modbus-debug.html +11 -48
- package/nodes/modbus-debug.js +5 -62
- package/nodes/modbus-master.js +236 -80
- package/nodes/modbus-slave-switch.html +2 -1
- package/nodes/modbus-slave-switch.js +101 -27
- package/nodes/serial-port-config.js +97 -34
- package/package.json +6 -3
package/nodes/modbus-master.js
CHANGED
|
@@ -139,23 +139,56 @@ module.exports = function(RED) {
|
|
|
139
139
|
node.isClosing = false;
|
|
140
140
|
node.lastErrorLog = {}; // 记录每个从站的最后错误日志时间
|
|
141
141
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
142
|
-
node.errorLogInterval =
|
|
142
|
+
node.errorLogInterval = 5 * 60 * 1000; // 错误日志间隔:5分钟(降低到5分钟,更快发现问题)
|
|
143
|
+
node.consecutiveErrors = {}; // 记录每个从站的连续错误次数
|
|
143
144
|
node.modbusLock = false; // Modbus操作互斥锁(防止读写冲突)
|
|
144
145
|
node.lastWriteTime = {}; // 记录每个从站的最后写入时间
|
|
145
146
|
node.pausePolling = false; // 暂停轮询标志(从站上报时暂停)
|
|
146
147
|
node.pollingPausedCount = 0; // 暂停轮询计数器
|
|
147
148
|
node._discoveryPublished = false; // Discovery发布标志(避免重复)
|
|
149
|
+
|
|
150
|
+
// 定期清理机制(每小时清理一次,防止内存泄漏)
|
|
151
|
+
node.cleanupTimer = setInterval(() => {
|
|
152
|
+
// 清理过期的错误日志记录
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const cleanupThreshold = 24 * 60 * 60 * 1000; // 24小时
|
|
155
|
+
|
|
156
|
+
Object.keys(node.lastErrorLog).forEach(slaveId => {
|
|
157
|
+
if (now - node.lastErrorLog[slaveId] > cleanupThreshold) {
|
|
158
|
+
delete node.lastErrorLog[slaveId];
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
Object.keys(node.consecutiveErrors).forEach(slaveId => {
|
|
163
|
+
if (now - (node.deviceStates[slaveId]?.lastTimeoutTime || 0) > cleanupThreshold) {
|
|
164
|
+
delete node.consecutiveErrors[slaveId];
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
node.debug(`内存清理完成(错误日志记录数: ${Object.keys(node.lastErrorLog).length})`);
|
|
169
|
+
}, 60 * 60 * 1000); // 每小时执行一次
|
|
148
170
|
|
|
149
171
|
// 更新节点状态显示
|
|
150
172
|
node.updateNodeStatus = function() {
|
|
151
173
|
const modbusStatus = node.isConnected ? "Modbus-OK" : "Modbus-ERR";
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
174
|
+
|
|
175
|
+
// 如果MQTT未启用,显示"本地模式"
|
|
176
|
+
let mqttStatus;
|
|
177
|
+
if (!node.config.enableMqtt) {
|
|
178
|
+
mqttStatus = "本地模式";
|
|
179
|
+
} else {
|
|
180
|
+
mqttStatus = node.mqttConnected ? "MQTT-OK" : "MQTT-ERR";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 状态显示逻辑
|
|
184
|
+
if (node.isConnected && (node.mqttConnected || !node.config.enableMqtt)) {
|
|
185
|
+
// Modbus已连接 且 (MQTT已连接 或 MQTT未启用)
|
|
155
186
|
node.status({fill: "green", shape: "dot", text: `${modbusStatus} ${mqttStatus}`});
|
|
156
187
|
} else if (node.isConnected || node.mqttConnected) {
|
|
188
|
+
// Modbus已连接 或 MQTT已连接(部分连接)
|
|
157
189
|
node.status({fill: "yellow", shape: "ring", text: `${modbusStatus} ${mqttStatus}`});
|
|
158
190
|
} else {
|
|
191
|
+
// 全部未连接
|
|
159
192
|
node.status({fill: "red", shape: "ring", text: `${modbusStatus} ${mqttStatus}`});
|
|
160
193
|
}
|
|
161
194
|
};
|
|
@@ -169,8 +202,7 @@ module.exports = function(RED) {
|
|
|
169
202
|
error: null,
|
|
170
203
|
config: slave, // 保存该从站的配置
|
|
171
204
|
initialPublished: false, // 标记是否已发布初始状态
|
|
172
|
-
timeoutCount: 0, //
|
|
173
|
-
isIgnored: false, // 是否被临时忽略(超时次数过多时)
|
|
205
|
+
timeoutCount: 0, // 连续超时次数(仅用于统计)
|
|
174
206
|
lastTimeoutTime: 0 // 最后一次超时时间
|
|
175
207
|
};
|
|
176
208
|
});
|
|
@@ -244,10 +276,17 @@ module.exports = function(RED) {
|
|
|
244
276
|
node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
245
277
|
}
|
|
246
278
|
|
|
247
|
-
//
|
|
248
|
-
const timeout = node.config.connectionType === "serial" ?
|
|
279
|
+
// 设置超时时间(串口需要更长的超时时间,但不能太长以免影响轮询)
|
|
280
|
+
const timeout = node.config.connectionType === "serial" ? 3000 : 2000;
|
|
249
281
|
node.client.setTimeout(timeout);
|
|
250
282
|
node.log(`Modbus超时设置: ${timeout}ms`);
|
|
283
|
+
|
|
284
|
+
// 设置错误处理器,防止未捕获的错误导致进程崩溃
|
|
285
|
+
if (node.client._port) {
|
|
286
|
+
node.client._port.on('error', (err) => {
|
|
287
|
+
node.warn(`串口错误(已忽略): ${err.message}`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
251
290
|
|
|
252
291
|
node.isConnected = true;
|
|
253
292
|
node.reconnectAttempts = 0; // 重置重连计数
|
|
@@ -259,12 +298,19 @@ module.exports = function(RED) {
|
|
|
259
298
|
node.lastMqttErrorLog = 0;
|
|
260
299
|
node._discoveryPublished = false; // 重置Discovery发布标志
|
|
261
300
|
|
|
262
|
-
// 添加Symi
|
|
263
|
-
if (node.
|
|
301
|
+
// 添加Symi按键事件监听(串口和TCP模式都支持)
|
|
302
|
+
if (node.client._port) {
|
|
303
|
+
// 串口模式:直接监听串口数据
|
|
264
304
|
node.client._port.on('data', (data) => {
|
|
265
305
|
node.handleSymiButtonEvent(data);
|
|
266
306
|
});
|
|
267
307
|
node.log('已启用Symi按键事件监听(串口模式)');
|
|
308
|
+
} else if (node.client._client && node.config.connectionType === "tcp") {
|
|
309
|
+
// TCP模式:监听TCP socket数据
|
|
310
|
+
node.client._client.on('data', (data) => {
|
|
311
|
+
node.handleSymiButtonEvent(data);
|
|
312
|
+
});
|
|
313
|
+
node.log('已启用Symi按键事件监听(TCP模式)');
|
|
268
314
|
}
|
|
269
315
|
|
|
270
316
|
// 立即启动轮询(不等待MQTT连接)
|
|
@@ -372,7 +418,7 @@ module.exports = function(RED) {
|
|
|
372
418
|
// 连接MQTT(带智能重试和fallback)
|
|
373
419
|
node.connectMqtt = function() {
|
|
374
420
|
if (!node.config.enableMqtt) {
|
|
375
|
-
node.log('MQTT未启用 -
|
|
421
|
+
node.log('MQTT未启用 - 使用本地模式(内部事件通信)');
|
|
376
422
|
node.log('提示:如需Home Assistant集成,请在节点配置中启用MQTT');
|
|
377
423
|
return;
|
|
378
424
|
}
|
|
@@ -729,6 +775,13 @@ module.exports = function(RED) {
|
|
|
729
775
|
// 开始轮询(使用递归调用而非定时器,避免并发)
|
|
730
776
|
node.startPolling = function() {
|
|
731
777
|
if (node.isPolling) {
|
|
778
|
+
node.log('轮询已在运行中,跳过重复启动');
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 检查从站配置
|
|
783
|
+
if (!node.config.slaves || node.config.slaves.length === 0) {
|
|
784
|
+
node.error('未配置从站设备,无法启动轮询');
|
|
732
785
|
return;
|
|
733
786
|
}
|
|
734
787
|
|
|
@@ -737,22 +790,37 @@ module.exports = function(RED) {
|
|
|
737
790
|
node.lastMqttErrorLog = 0;
|
|
738
791
|
|
|
739
792
|
const slaveList = node.config.slaves.map(s => `从站${s.address}(线圈${s.coilStart}-${s.coilEnd},间隔${s.pollInterval}ms)`).join(', ');
|
|
740
|
-
node.log(
|
|
793
|
+
node.log(`========== 开始轮询 ${node.config.slaves.length} 个从站设备 ==========`);
|
|
794
|
+
node.log(`从站列表: ${slaveList}`);
|
|
741
795
|
node.log(`Modbus连接: ${node.isConnected ? '已连接' : '未连接'}, MQTT连接: ${node.mqttConnected ? '已连接' : '未连接'}`);
|
|
742
796
|
node.currentSlaveIndex = 0;
|
|
743
797
|
node.isPolling = true;
|
|
798
|
+
node.pollingLoopCount = 0; // 轮询循环计数器
|
|
744
799
|
|
|
745
800
|
// 使用递归调用实现轮询,确保不会并发
|
|
746
801
|
const pollLoop = async () => {
|
|
747
|
-
if (!node.isPolling
|
|
802
|
+
if (!node.isPolling) {
|
|
803
|
+
node.log('轮询已停止(isPolling=false)');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!node.isConnected) {
|
|
808
|
+
node.log('轮询已停止(Modbus未连接)');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (node.isClosing) {
|
|
813
|
+
node.log('轮询已停止(节点正在关闭)');
|
|
748
814
|
return;
|
|
749
815
|
}
|
|
750
816
|
|
|
751
817
|
// 获取当前从站配置(在轮询前获取,确保使用正确的间隔)
|
|
752
818
|
const currentSlave = node.config.slaves[node.currentSlaveIndex];
|
|
753
819
|
if (!currentSlave) {
|
|
754
|
-
node.
|
|
820
|
+
node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
|
|
755
821
|
node.currentSlaveIndex = 0;
|
|
822
|
+
// 继续轮询,不要中断
|
|
823
|
+
node.pollTimer = setTimeout(pollLoop, 200);
|
|
756
824
|
return;
|
|
757
825
|
}
|
|
758
826
|
|
|
@@ -761,14 +829,26 @@ module.exports = function(RED) {
|
|
|
761
829
|
try {
|
|
762
830
|
await node.pollNextSlave();
|
|
763
831
|
} catch (err) {
|
|
764
|
-
|
|
832
|
+
// 捕获所有异常,防止轮询中断(只记录到日志文件,不输出到调试窗口)
|
|
833
|
+
node.log(`轮询异常(已忽略,继续轮询): ${err.message}`);
|
|
765
834
|
}
|
|
766
835
|
|
|
767
|
-
//
|
|
836
|
+
// 移动到下一个从站
|
|
837
|
+
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
838
|
+
|
|
839
|
+
// 每完成一轮轮询,计数器加1
|
|
840
|
+
if (node.currentSlaveIndex === 0) {
|
|
841
|
+
node.pollingLoopCount++;
|
|
842
|
+
// 轮询计数器静默运行,不输出日志(避免日志过多)
|
|
843
|
+
// 轮询会永久运行,无需频繁确认
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 等待指定间隔后继续下一次轮询(确保轮询永不停止)
|
|
768
847
|
node.pollTimer = setTimeout(pollLoop, interval);
|
|
769
848
|
};
|
|
770
849
|
|
|
771
850
|
// 立即开始轮询
|
|
851
|
+
node.log('立即启动轮询循环...');
|
|
772
852
|
pollLoop();
|
|
773
853
|
};
|
|
774
854
|
|
|
@@ -797,20 +877,19 @@ module.exports = function(RED) {
|
|
|
797
877
|
// 获取当前从站配置
|
|
798
878
|
const slave = node.config.slaves[node.currentSlaveIndex];
|
|
799
879
|
if (!slave) {
|
|
800
|
-
node.
|
|
880
|
+
node.log(`从站索引${node.currentSlaveIndex}无效,重置为0`);
|
|
801
881
|
node.currentSlaveIndex = 0;
|
|
802
882
|
return;
|
|
803
883
|
}
|
|
804
884
|
|
|
805
885
|
const slaveId = slave.address;
|
|
886
|
+
|
|
887
|
+
// 轮询静默运行,不输出日志(避免日志过多)
|
|
806
888
|
const deviceState = node.deviceStates[slaveId];
|
|
807
889
|
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
890
|
+
// 不再跳过任何从站,即使连续超时也继续轮询
|
|
891
|
+
// 原则:轮询永不停止,即使从站不响应也不影响总线轮询机制
|
|
892
|
+
// 只记录错误日志,不跳过轮询
|
|
814
893
|
|
|
815
894
|
const coilCount = slave.coilEnd - slave.coilStart + 1;
|
|
816
895
|
|
|
@@ -818,15 +897,13 @@ module.exports = function(RED) {
|
|
|
818
897
|
const lastWrite = node.lastWriteTime[slaveId] || 0;
|
|
819
898
|
const timeSinceWrite = Date.now() - lastWrite;
|
|
820
899
|
if (timeSinceWrite < 100) {
|
|
821
|
-
//
|
|
822
|
-
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
900
|
+
// 跳过该从站(不移动索引,由pollLoop统一管理)
|
|
823
901
|
return;
|
|
824
902
|
}
|
|
825
903
|
|
|
826
904
|
// 检查锁状态(如果有写操作正在进行,跳过本次轮询)
|
|
827
905
|
if (node.modbusLock) {
|
|
828
|
-
//
|
|
829
|
-
node.currentSlaveIndex = (node.currentSlaveIndex + 1) % node.config.slaves.length;
|
|
906
|
+
// 跳过该从站(不移动索引,由pollLoop统一管理)
|
|
830
907
|
return;
|
|
831
908
|
}
|
|
832
909
|
|
|
@@ -862,6 +939,17 @@ module.exports = function(RED) {
|
|
|
862
939
|
coil: coilIndex,
|
|
863
940
|
value: newValue
|
|
864
941
|
});
|
|
942
|
+
|
|
943
|
+
// 广播状态变化事件(用于LED反馈)
|
|
944
|
+
// 只在状态真正改变时广播(不包括首次轮询)
|
|
945
|
+
if (!isFirstPoll && oldValue !== newValue) {
|
|
946
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
947
|
+
slave: slaveId,
|
|
948
|
+
coil: coilIndex,
|
|
949
|
+
value: newValue,
|
|
950
|
+
source: 'polling'
|
|
951
|
+
});
|
|
952
|
+
}
|
|
865
953
|
}
|
|
866
954
|
}
|
|
867
955
|
|
|
@@ -877,7 +965,6 @@ module.exports = function(RED) {
|
|
|
877
965
|
|
|
878
966
|
// 轮询成功,重置超时计数
|
|
879
967
|
node.deviceStates[slaveId].timeoutCount = 0;
|
|
880
|
-
node.deviceStates[slaveId].isIgnored = false;
|
|
881
968
|
|
|
882
969
|
// 输出消息
|
|
883
970
|
const output = {
|
|
@@ -904,6 +991,12 @@ module.exports = function(RED) {
|
|
|
904
991
|
// 释放锁
|
|
905
992
|
node.modbusLock = false;
|
|
906
993
|
|
|
994
|
+
// 确保deviceState存在(防止未定义错误)
|
|
995
|
+
if (!node.deviceStates[slaveId]) {
|
|
996
|
+
node.log(`从站${slaveId}状态未初始化,跳过错误记录`);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
907
1000
|
node.deviceStates[slaveId].error = err.message;
|
|
908
1001
|
node.deviceStates[slaveId].lastTimeoutTime = Date.now();
|
|
909
1002
|
|
|
@@ -920,26 +1013,18 @@ module.exports = function(RED) {
|
|
|
920
1013
|
);
|
|
921
1014
|
|
|
922
1015
|
if (isTimeout) {
|
|
923
|
-
//
|
|
1016
|
+
// 增加超时计数(仅用于统计,不影响轮询)
|
|
924
1017
|
node.deviceStates[slaveId].timeoutCount++;
|
|
925
1018
|
|
|
926
|
-
//
|
|
927
|
-
if (
|
|
928
|
-
node.deviceStates[slaveId].
|
|
929
|
-
|
|
930
|
-
node.warn(`从站${slaveId}连续超时${node.deviceStates[slaveId].timeoutCount}次,临时忽略该从站(重启或重新部署后恢复)`);
|
|
931
|
-
node.lastErrorLog[slaveId] = now;
|
|
932
|
-
}
|
|
933
|
-
} else {
|
|
934
|
-
if (shouldLog) {
|
|
935
|
-
node.warn(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [超时${node.deviceStates[slaveId].timeoutCount}/5]`);
|
|
936
|
-
node.lastErrorLog[slaveId] = now;
|
|
937
|
-
}
|
|
1019
|
+
// 只记录日志到文件,不输出到调试窗口(避免日志刷屏)
|
|
1020
|
+
if (shouldLog) {
|
|
1021
|
+
node.log(`从站${slaveId}轮询超时: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd}) [连续超时${node.deviceStates[slaveId].timeoutCount}次]`);
|
|
1022
|
+
node.lastErrorLog[slaveId] = now;
|
|
938
1023
|
}
|
|
939
1024
|
} else {
|
|
940
|
-
//
|
|
1025
|
+
// 非超时错误(如CRC错误、总线干扰),记录日志到文件,不输出到调试窗口
|
|
941
1026
|
if (shouldLog) {
|
|
942
|
-
node.
|
|
1027
|
+
node.log(`从站${slaveId}轮询失败: ${err.message} (线圈${slave.coilStart}-${slave.coilEnd})`);
|
|
943
1028
|
node.lastErrorLog[slaveId] = now;
|
|
944
1029
|
}
|
|
945
1030
|
}
|
|
@@ -992,28 +1077,32 @@ module.exports = function(RED) {
|
|
|
992
1077
|
}
|
|
993
1078
|
}
|
|
994
1079
|
|
|
995
|
-
//
|
|
996
|
-
|
|
1080
|
+
// 注意:不在这里移动索引,由pollLoop统一管理
|
|
1081
|
+
// 这样可以确保每个从站都按照正确的间隔轮询
|
|
997
1082
|
};
|
|
998
1083
|
|
|
999
1084
|
// 处理Symi按键事件(私有协议)
|
|
1085
|
+
// 静默处理:只处理本节点相关的数据,忽略总线上的其他数据
|
|
1000
1086
|
node.handleSymiButtonEvent = function(data) {
|
|
1001
1087
|
try {
|
|
1002
|
-
// 解析Symi
|
|
1088
|
+
// 解析Symi协议帧(带CRC校验)
|
|
1003
1089
|
const frame = protocol.parseFrame(data);
|
|
1004
1090
|
if (!frame) {
|
|
1005
|
-
|
|
1091
|
+
// 不是有效的Symi帧(CRC校验失败或格式错误),静默忽略
|
|
1092
|
+
return;
|
|
1006
1093
|
}
|
|
1007
1094
|
|
|
1008
1095
|
// 只处理SET类型(0x03)的按键事件,忽略REPORT类型(0x04)
|
|
1009
1096
|
// REPORT类型是面板对我们指令的确认,不是按键事件
|
|
1010
1097
|
if (frame.dataType !== 0x03) {
|
|
1011
|
-
|
|
1098
|
+
// 不是按键事件,静默忽略
|
|
1099
|
+
return;
|
|
1012
1100
|
}
|
|
1013
1101
|
|
|
1014
1102
|
// 检查是否是灯光设备
|
|
1015
1103
|
if (frame.deviceType !== 0x01) {
|
|
1016
|
-
|
|
1104
|
+
// 不是灯光设备,静默忽略
|
|
1105
|
+
return;
|
|
1017
1106
|
}
|
|
1018
1107
|
|
|
1019
1108
|
// 提取按键信息
|
|
@@ -1021,8 +1110,6 @@ module.exports = function(RED) {
|
|
|
1021
1110
|
const channel = frame.channel; // 通道号(1-8)
|
|
1022
1111
|
const state = frame.opInfo[0] === 0x01; // 状态(1=开,0=关)
|
|
1023
1112
|
|
|
1024
|
-
node.debug(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1025
|
-
|
|
1026
1113
|
// 查找对应的从站和线圈
|
|
1027
1114
|
// 假设:设备地址1对应从站10,设备地址2对应从站11,以此类推
|
|
1028
1115
|
// 通道号直接对应线圈号(1-8 → 0-7)
|
|
@@ -1032,16 +1119,18 @@ module.exports = function(RED) {
|
|
|
1032
1119
|
// 检查从站是否在配置中
|
|
1033
1120
|
const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
|
|
1034
1121
|
if (!slaveConfig) {
|
|
1035
|
-
|
|
1122
|
+
// 从站未配置,静默忽略(不是本节点的数据)
|
|
1036
1123
|
return;
|
|
1037
1124
|
}
|
|
1038
1125
|
|
|
1039
1126
|
// 检查线圈是否在范围内
|
|
1040
1127
|
if (coilNumber < slaveConfig.coilStart || coilNumber > slaveConfig.coilEnd) {
|
|
1041
|
-
|
|
1128
|
+
// 线圈不在范围内,静默忽略(不是本节点的数据)
|
|
1042
1129
|
return;
|
|
1043
1130
|
}
|
|
1044
1131
|
|
|
1132
|
+
// 到这里说明是本节点的数据,输出日志
|
|
1133
|
+
node.debug(`Symi按键事件: 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1045
1134
|
node.debug(`Symi按键映射: 设备${deviceAddr}通道${channel} → 从站${slaveId}线圈${coilNumber}`);
|
|
1046
1135
|
|
|
1047
1136
|
// 写入线圈(异步,不阻塞)
|
|
@@ -1055,11 +1144,16 @@ module.exports = function(RED) {
|
|
|
1055
1144
|
if (node.client._port && node.client._port.write) {
|
|
1056
1145
|
node.client._port.write(responseFrame);
|
|
1057
1146
|
node.log(`Symi应答已发送(REPORT): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1147
|
+
} else if (node.client._client && node.client._client.write) {
|
|
1148
|
+
// TCP模式
|
|
1149
|
+
node.client._client.write(responseFrame);
|
|
1150
|
+
node.log(`Symi应答已发送(REPORT/TCP): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1058
1151
|
}
|
|
1059
1152
|
|
|
1060
1153
|
} catch (err) {
|
|
1061
1154
|
// 静默处理错误,避免干扰Modbus通信
|
|
1062
|
-
//
|
|
1155
|
+
// 总线上可能有各种数据,不是所有数据都能解析成功
|
|
1156
|
+
// 只有真正的错误才输出日志
|
|
1063
1157
|
}
|
|
1064
1158
|
};
|
|
1065
1159
|
|
|
@@ -1092,17 +1186,21 @@ module.exports = function(RED) {
|
|
|
1092
1186
|
try {
|
|
1093
1187
|
// 设置锁
|
|
1094
1188
|
node.modbusLock = true;
|
|
1095
|
-
|
|
1189
|
+
|
|
1096
1190
|
node.client.setID(slaveId);
|
|
1191
|
+
|
|
1192
|
+
// 记录写入操作(帮助追踪总线数据来源)
|
|
1193
|
+
node.log(`写入线圈: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1194
|
+
|
|
1097
1195
|
await node.client.writeCoil(coil, value);
|
|
1098
|
-
|
|
1196
|
+
|
|
1099
1197
|
// 记录写入时间(用于暂停轮询)
|
|
1100
1198
|
node.lastWriteTime[slaveId] = Date.now();
|
|
1101
|
-
|
|
1199
|
+
|
|
1102
1200
|
// 更新本地状态
|
|
1103
1201
|
node.deviceStates[slaveId].coils[coil] = value;
|
|
1104
|
-
|
|
1105
|
-
node.
|
|
1202
|
+
|
|
1203
|
+
node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1106
1204
|
|
|
1107
1205
|
// 发布到MQTT和触发事件
|
|
1108
1206
|
node.publishMqttState(slaveId, coil, value);
|
|
@@ -1213,43 +1311,89 @@ module.exports = function(RED) {
|
|
|
1213
1311
|
}
|
|
1214
1312
|
};
|
|
1215
1313
|
|
|
1216
|
-
//
|
|
1314
|
+
// 监听内部事件(从站开关节点发送的写入命令)
|
|
1315
|
+
// 这是免连线通信的核心机制
|
|
1316
|
+
node.internalEventHandler = async function(data) {
|
|
1317
|
+
if (!data || typeof data !== 'object') {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const slave = parseInt(data.slave);
|
|
1322
|
+
const coil = parseInt(data.coil);
|
|
1323
|
+
const value = Boolean(data.value);
|
|
1324
|
+
|
|
1325
|
+
// 输出日志确认收到事件
|
|
1326
|
+
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1327
|
+
|
|
1328
|
+
try {
|
|
1329
|
+
// 执行写入操作
|
|
1330
|
+
await node.writeSingleCoil(slave, coil, value);
|
|
1331
|
+
|
|
1332
|
+
// 写入成功后,广播状态变化事件(用于LED反馈)
|
|
1333
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
1334
|
+
slave: slave,
|
|
1335
|
+
coil: coil,
|
|
1336
|
+
value: value,
|
|
1337
|
+
source: 'master'
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
node.log(`内部事件写入成功,已广播状态变化:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// 注册内部事件监听器
|
|
1347
|
+
RED.events.on('modbus:writeCoil', node.internalEventHandler);
|
|
1348
|
+
node.log('已注册内部事件监听器(免连线通信)');
|
|
1349
|
+
|
|
1350
|
+
// 处理输入消息(保留兼容性,支持连线模式)
|
|
1217
1351
|
node.on('input', function(msg) {
|
|
1218
1352
|
if (!msg.payload || typeof msg.payload !== 'object') {
|
|
1219
1353
|
return;
|
|
1220
1354
|
}
|
|
1221
|
-
|
|
1355
|
+
|
|
1222
1356
|
const cmd = msg.payload.cmd;
|
|
1223
|
-
|
|
1357
|
+
|
|
1224
1358
|
switch(cmd) {
|
|
1225
1359
|
case 'start':
|
|
1226
1360
|
node.startPolling();
|
|
1227
1361
|
break;
|
|
1228
|
-
|
|
1362
|
+
|
|
1229
1363
|
case 'stop':
|
|
1230
1364
|
node.stopPolling();
|
|
1231
1365
|
break;
|
|
1232
|
-
|
|
1366
|
+
|
|
1233
1367
|
case 'writeCoil':
|
|
1234
|
-
if (msg.payload.slave && msg.payload.coil !== undefined && msg.payload.value !== undefined) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
)
|
|
1368
|
+
if (msg.payload.slave !== undefined && msg.payload.coil !== undefined && msg.payload.value !== undefined) {
|
|
1369
|
+
const slave = parseInt(msg.payload.slave);
|
|
1370
|
+
const coil = parseInt(msg.payload.coil);
|
|
1371
|
+
const value = Boolean(msg.payload.value);
|
|
1372
|
+
|
|
1373
|
+
node.writeSingleCoil(slave, coil, value).catch(err => {
|
|
1374
|
+
node.error(`连线模式写入失败: ${err.message}`);
|
|
1375
|
+
});
|
|
1376
|
+
} else {
|
|
1377
|
+
node.warn(`writeCoil命令参数不完整: slave=${msg.payload.slave}, coil=${msg.payload.coil}, value=${msg.payload.value}`);
|
|
1240
1378
|
}
|
|
1241
1379
|
break;
|
|
1242
|
-
|
|
1380
|
+
|
|
1243
1381
|
case 'writeCoils':
|
|
1244
|
-
if (msg.payload.slave && msg.payload.startCoil !== undefined && Array.isArray(msg.payload.values)) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
);
|
|
1382
|
+
if (msg.payload.slave !== undefined && msg.payload.startCoil !== undefined && Array.isArray(msg.payload.values)) {
|
|
1383
|
+
const slave = parseInt(msg.payload.slave);
|
|
1384
|
+
const startCoil = parseInt(msg.payload.startCoil);
|
|
1385
|
+
const values = msg.payload.values.map(v => Boolean(v));
|
|
1386
|
+
|
|
1387
|
+
node.log(`接收到本地模式批量写入命令: 从站${slave} 起始线圈${startCoil} 共${values.length}个`);
|
|
1388
|
+
|
|
1389
|
+
node.writeMultipleCoils(slave, startCoil, values).catch(err => {
|
|
1390
|
+
node.error(`本地模式批量写入失败: ${err.message}`);
|
|
1391
|
+
});
|
|
1392
|
+
} else {
|
|
1393
|
+
node.warn(`writeCoils命令参数不完整`);
|
|
1250
1394
|
}
|
|
1251
1395
|
break;
|
|
1252
|
-
|
|
1396
|
+
|
|
1253
1397
|
default:
|
|
1254
1398
|
node.warn(`未知命令: ${cmd}`);
|
|
1255
1399
|
}
|
|
@@ -1274,13 +1418,25 @@ module.exports = function(RED) {
|
|
|
1274
1418
|
node.on('close', function(done) {
|
|
1275
1419
|
node.isClosing = true;
|
|
1276
1420
|
node.stopPolling();
|
|
1277
|
-
|
|
1421
|
+
|
|
1422
|
+
// 移除内部事件监听器
|
|
1423
|
+
if (node.internalEventHandler) {
|
|
1424
|
+
RED.events.removeListener('modbus:writeCoil', node.internalEventHandler);
|
|
1425
|
+
node.internalEventHandler = null;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1278
1428
|
// 清除重连定时器
|
|
1279
1429
|
if (node.reconnectTimer) {
|
|
1280
1430
|
clearTimeout(node.reconnectTimer);
|
|
1281
1431
|
node.reconnectTimer = null;
|
|
1282
1432
|
}
|
|
1283
|
-
|
|
1433
|
+
|
|
1434
|
+
// 清除内存清理定时器
|
|
1435
|
+
if (node.cleanupTimer) {
|
|
1436
|
+
clearInterval(node.cleanupTimer);
|
|
1437
|
+
node.cleanupTimer = null;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1284
1440
|
// 发布离线状态到MQTT(让HA知道设备离线)
|
|
1285
1441
|
if (node.config.enableMqtt && node.mqttClient && node.mqttClient.connected) {
|
|
1286
1442
|
node.publishOfflineStatus();
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
<span style="font-size: 14px; font-weight: 600; color: #333;">RS-485连接配置</span>
|
|
58
58
|
</label>
|
|
59
59
|
<div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #fff3cd 0%, #fffbe6 100%); border-left: 4px solid #ffc107; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
|
60
|
-
<strong>说明:</strong>多个从站开关节点可以共享同一个RS-485连接配置(支持TCP
|
|
60
|
+
<strong>说明:</strong>多个从站开关节点可以共享同一个RS-485连接配置(支持TCP网关或串口)<br>
|
|
61
|
+
<strong>批量更换:</strong>如需更换连接配置,请先创建新配置,然后在配置节点侧边栏中删除旧配置,系统会提示你选择替换配置
|
|
61
62
|
</div>
|
|
62
63
|
</div>
|
|
63
64
|
|