node-red-contrib-symi-modbus 2.6.8 → 2.7.0
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 +47 -3
- package/nodes/modbus-master.js +130 -60
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成,
|
|
|
18
18
|
- Telnet ASCII(推荐用于TCP转RS485网关)
|
|
19
19
|
- **Symi开关集成**:自动识别并处理Symi私有协议按键事件,实现开关面板与继电器的双向同步
|
|
20
20
|
- **HomeKit网桥**:一键桥接到Apple HomeKit,支持Siri语音控制,自动同步主站配置,名称可自定义
|
|
21
|
+
- **智能写入队列**:所有写入操作串行执行,支持HomeKit群控160个继电器同时动作,流畅无卡顿
|
|
21
22
|
- **多设备轮询**:支持最多10台Modbus从站设备,每台32路继电器
|
|
22
23
|
- **智能轮询机制**:从站上报时自动暂停轮询,优先处理数据,避免冲突
|
|
23
24
|
- **稳定可靠**:完整的内存管理、错误处理、断线重连,适合7x24小时长期运行
|
|
@@ -183,6 +184,35 @@ node-red-restart
|
|
|
183
184
|
- 支持Siri语音控制:"嘿Siri,打开客厅灯"
|
|
184
185
|
- 支持HomeKit自动化和场景
|
|
185
186
|
- 配置会自动保存,重启后无需重新配对
|
|
187
|
+
- **群控性能**:支持同时控制多个继电器(如创建编组),智能队列机制确保流畅无卡顿
|
|
188
|
+
|
|
189
|
+
**群控说明**(客户友好模式):
|
|
190
|
+
|
|
191
|
+
本节点专为智能家居群控场景优化,支持以下高性能操作:
|
|
192
|
+
|
|
193
|
+
1. **HomeKit编组群控**:
|
|
194
|
+
- 在HomeKit中创建房间或编组,可同时控制多个继电器
|
|
195
|
+
- 例如:创建"客厅"编组,包含10个灯光开关,一键全开/全关
|
|
196
|
+
- 智能队列机制确保所有继电器按序快速执行,无超时警告
|
|
197
|
+
- 10个继电器同时动作仅需约200ms(20ms间隔×10)
|
|
198
|
+
|
|
199
|
+
2. **场景联动**:
|
|
200
|
+
- 支持HomeKit场景(如"回家模式"、"离家模式")
|
|
201
|
+
- 场景可包含多个继电器动作,自动串行执行
|
|
202
|
+
- 前16个线圈(继电器)可同时动作,后16个线圈(场景)按需触发
|
|
203
|
+
|
|
204
|
+
3. **性能保证**:
|
|
205
|
+
- 最大支持10台继电器(每台32路),共320个线圈
|
|
206
|
+
- 前160个线圈(10台×16路)可用于继电器控制,支持群控
|
|
207
|
+
- 后160个线圈(10台×16路)可用于场景触发,一般不会全部同时动作
|
|
208
|
+
- 智能写入队列确保所有操作串行执行,避免总线冲突
|
|
209
|
+
- 长期稳定运行,反复控制不会造成内存增加、卡顿或死机
|
|
210
|
+
|
|
211
|
+
4. **技术细节**:
|
|
212
|
+
- 写入队列间隔:20ms(确保总线稳定)
|
|
213
|
+
- 轮询恢复时间:20ms(快速响应)
|
|
214
|
+
- 锁等待超时:100ms(快速检测异常)
|
|
215
|
+
- 队列自动处理,无需手动干预
|
|
186
216
|
|
|
187
217
|
## 核心特性说明
|
|
188
218
|
|
|
@@ -710,7 +740,7 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
710
740
|
|
|
711
741
|
## 项目信息
|
|
712
742
|
|
|
713
|
-
**版本**: v2.
|
|
743
|
+
**版本**: v2.7.0
|
|
714
744
|
|
|
715
745
|
**核心功能**:
|
|
716
746
|
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
@@ -718,6 +748,7 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
718
748
|
- 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
|
|
719
749
|
- 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线)
|
|
720
750
|
- 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制)
|
|
751
|
+
- 🔥 **智能写入队列**(所有写入操作串行执行,避免锁竞争,支持HomeKit群控)
|
|
721
752
|
- MQTT集成(可选启用,Home Assistant自动发现)
|
|
722
753
|
- 物理开关面板双向同步(支持开关模式和场景模式)
|
|
723
754
|
- 长期稳定运行(内存管理、智能重连、异步处理)
|
|
@@ -726,7 +757,19 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
726
757
|
- Node.js: >=14.0.0
|
|
727
758
|
- Node-RED: >=2.0.0
|
|
728
759
|
|
|
729
|
-
**最新更新(v2.
|
|
760
|
+
**最新更新(v2.7.0)**:
|
|
761
|
+
- **🔥 智能写入队列机制**:
|
|
762
|
+
- 所有写入操作(HomeKit、MQTT、内部事件)统一进入队列串行执行
|
|
763
|
+
- 彻底解决HomeKit群控时的锁竞争问题,不再出现"写入线圈等待超时"警告
|
|
764
|
+
- 写入间隔优化到20ms,确保快速响应(10个继电器同时动作仅需200ms)
|
|
765
|
+
- 队列自动处理,无需手动干预,确保总线稳定性
|
|
766
|
+
- 支持最多160个继电器(10台从站×16路)同时群控,流畅无卡顿
|
|
767
|
+
- **性能优化**:
|
|
768
|
+
- 轮询恢复时间从100ms优化到20ms,提升响应速度5倍
|
|
769
|
+
- 锁等待超时从1000ms降低到100ms,快速检测异常
|
|
770
|
+
- 内存占用更低,CPU占用更少,适合长期运行
|
|
771
|
+
|
|
772
|
+
**v2.6.8更新**:
|
|
730
773
|
- **🔥 新增HomeKit网桥节点**:
|
|
731
774
|
- 一键桥接Modbus继电器到Apple HomeKit
|
|
732
775
|
- 自动同步主站配置的所有从站和继电器
|
|
@@ -1063,7 +1106,7 @@ msg.payload = 1; // 或 0
|
|
|
1063
1106
|
|
|
1064
1107
|
## 项目信息
|
|
1065
1108
|
|
|
1066
|
-
**版本**: v2.
|
|
1109
|
+
**版本**: v2.7.0
|
|
1067
1110
|
|
|
1068
1111
|
**核心功能**:
|
|
1069
1112
|
- 支持多种Modbus协议(Telnet ASCII、RTU over TCP、Modbus TCP、Modbus RTU串口)
|
|
@@ -1073,6 +1116,7 @@ msg.payload = 1; // 或 0
|
|
|
1073
1116
|
- 🔥 **双模式支持**(本地模式和MQTT模式可选切换,断网也能稳定运行)
|
|
1074
1117
|
- 🔥 **免连线通信**(主站和从站通过内部事件通信,无需连线,支持本地模式和MQTT模式)
|
|
1075
1118
|
- 🔥 **HomeKit网桥**(一键桥接到Apple HomeKit,支持Siri语音控制,名称可自定义)
|
|
1119
|
+
- 🔥 **智能写入队列**(所有写入操作串行执行,避免锁竞争,支持HomeKit群控)
|
|
1076
1120
|
- MQTT集成(可选启用,Home Assistant自动发现,实体唯一性保证,QoS=0高性能发布)
|
|
1077
1121
|
- 物理开关面板双向同步(亖米协议支持,LED反馈同步,支持开关模式和场景模式)
|
|
1078
1122
|
- 共享连接架构(多个从站开关节点共享同一个串口/TCP连接,支持500+节点)
|
package/nodes/modbus-master.js
CHANGED
|
@@ -147,6 +147,11 @@ module.exports = function(RED) {
|
|
|
147
147
|
node.pollingPausedCount = 0; // 暂停轮询计数器
|
|
148
148
|
node._discoveryPublished = false; // Discovery发布标志(避免重复)
|
|
149
149
|
|
|
150
|
+
// 写入队列机制(确保所有写入操作串行执行,避免锁竞争)
|
|
151
|
+
node.writeQueue = []; // 写入队列
|
|
152
|
+
node.isProcessingWrite = false; // 是否正在处理写入队列
|
|
153
|
+
node.writeQueueInterval = 20; // 写入队列处理间隔(20ms,确保快速响应)
|
|
154
|
+
|
|
150
155
|
// 定期清理机制(每小时清理一次,防止内存泄漏)
|
|
151
156
|
node.cleanupTimer = setInterval(() => {
|
|
152
157
|
// 清理过期的错误日志记录
|
|
@@ -1157,33 +1162,66 @@ module.exports = function(RED) {
|
|
|
1157
1162
|
}
|
|
1158
1163
|
};
|
|
1159
1164
|
|
|
1160
|
-
//
|
|
1161
|
-
node.
|
|
1162
|
-
if (
|
|
1163
|
-
node.warn('Modbus未连接');
|
|
1165
|
+
// 写入队列处理函数(串行执行所有写入操作)
|
|
1166
|
+
node.processWriteQueue = async function() {
|
|
1167
|
+
if (node.isProcessingWrite || node.writeQueue.length === 0) {
|
|
1164
1168
|
return;
|
|
1165
1169
|
}
|
|
1166
1170
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1171
|
+
node.isProcessingWrite = true;
|
|
1172
|
+
|
|
1173
|
+
while (node.writeQueue.length > 0) {
|
|
1174
|
+
const task = node.writeQueue.shift();
|
|
1170
1175
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1176
|
+
try {
|
|
1177
|
+
if (task.type === 'single') {
|
|
1178
|
+
await node._writeSingleCoilInternal(task.slaveId, task.coil, task.value);
|
|
1179
|
+
} else if (task.type === 'multiple') {
|
|
1180
|
+
await node._writeMultipleCoilsInternal(task.slaveId, task.startCoil, task.values);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// 写入成功,调用回调
|
|
1184
|
+
if (task.resolve) {
|
|
1185
|
+
task.resolve();
|
|
1186
|
+
}
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
// 写入失败,调用错误回调
|
|
1189
|
+
if (task.reject) {
|
|
1190
|
+
task.reject(err);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// 等待一段时间再处理下一个任务(20ms间隔,确保总线稳定)
|
|
1195
|
+
if (node.writeQueue.length > 0) {
|
|
1196
|
+
await new Promise(resolve => setTimeout(resolve, node.writeQueueInterval));
|
|
1197
|
+
}
|
|
1178
1198
|
}
|
|
1179
1199
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1200
|
+
node.isProcessingWrite = false;
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// 写单个线圈(内部实现,不经过队列)
|
|
1204
|
+
node._writeSingleCoilInternal = async function(slaveId, coil, value) {
|
|
1205
|
+
if (!node.isConnected) {
|
|
1206
|
+
throw new Error('Modbus未连接');
|
|
1184
1207
|
}
|
|
1185
|
-
|
|
1208
|
+
|
|
1209
|
+
// 暂停轮询(写操作优先)
|
|
1210
|
+
node.pausePolling = true;
|
|
1211
|
+
const pauseStartTime = Date.now();
|
|
1212
|
+
|
|
1186
1213
|
try {
|
|
1214
|
+
// 等待锁释放(最多等待100ms,因为有队列机制,不需要等太久)
|
|
1215
|
+
const maxWait = 100;
|
|
1216
|
+
const startWait = Date.now();
|
|
1217
|
+
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1218
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (node.modbusLock) {
|
|
1222
|
+
throw new Error(`写入线圈等待锁超时: 从站${slaveId} 线圈${coil}`);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1187
1225
|
// 设置锁
|
|
1188
1226
|
node.modbusLock = true;
|
|
1189
1227
|
|
|
@@ -1201,7 +1239,7 @@ module.exports = function(RED) {
|
|
|
1201
1239
|
node.deviceStates[slaveId].coils[coil] = value;
|
|
1202
1240
|
|
|
1203
1241
|
node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1204
|
-
|
|
1242
|
+
|
|
1205
1243
|
// 发布到MQTT和触发事件
|
|
1206
1244
|
node.publishMqttState(slaveId, coil, value);
|
|
1207
1245
|
node.emit('stateUpdate', {
|
|
@@ -1209,11 +1247,11 @@ module.exports = function(RED) {
|
|
|
1209
1247
|
coil: coil,
|
|
1210
1248
|
value: value
|
|
1211
1249
|
});
|
|
1212
|
-
|
|
1250
|
+
|
|
1213
1251
|
// 释放锁
|
|
1214
1252
|
node.modbusLock = false;
|
|
1215
|
-
|
|
1216
|
-
//
|
|
1253
|
+
|
|
1254
|
+
// 延迟恢复轮询(给从站响应预留时间,减少到20ms)
|
|
1217
1255
|
setTimeout(() => {
|
|
1218
1256
|
node.pausePolling = false;
|
|
1219
1257
|
const pauseDuration = Date.now() - pauseStartTime;
|
|
@@ -1221,56 +1259,70 @@ module.exports = function(RED) {
|
|
|
1221
1259
|
node.debug(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1222
1260
|
node.pollingPausedCount = 0;
|
|
1223
1261
|
}
|
|
1224
|
-
},
|
|
1225
|
-
|
|
1262
|
+
}, 20);
|
|
1263
|
+
|
|
1226
1264
|
} catch (err) {
|
|
1227
1265
|
// 释放锁
|
|
1228
1266
|
node.modbusLock = false;
|
|
1229
|
-
|
|
1267
|
+
|
|
1230
1268
|
// 恢复轮询
|
|
1231
1269
|
node.pausePolling = false;
|
|
1232
1270
|
node.pollingPausedCount = 0;
|
|
1233
|
-
|
|
1271
|
+
|
|
1234
1272
|
node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
|
|
1235
|
-
throw err;
|
|
1273
|
+
throw err;
|
|
1236
1274
|
}
|
|
1237
1275
|
};
|
|
1276
|
+
|
|
1277
|
+
// 写单个线圈(公共接口,通过队列执行)
|
|
1278
|
+
node.writeSingleCoil = function(slaveId, coil, value) {
|
|
1279
|
+
return new Promise((resolve, reject) => {
|
|
1280
|
+
// 添加到队列
|
|
1281
|
+
node.writeQueue.push({
|
|
1282
|
+
type: 'single',
|
|
1283
|
+
slaveId: slaveId,
|
|
1284
|
+
coil: coil,
|
|
1285
|
+
value: value,
|
|
1286
|
+
resolve: resolve,
|
|
1287
|
+
reject: reject
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
// 触发队列处理
|
|
1291
|
+
node.processWriteQueue();
|
|
1292
|
+
});
|
|
1293
|
+
};
|
|
1238
1294
|
|
|
1239
|
-
//
|
|
1240
|
-
node.
|
|
1295
|
+
// 批量写入多个线圈(内部实现,不经过队列)
|
|
1296
|
+
node._writeMultipleCoilsInternal = async function(slaveId, startCoil, values) {
|
|
1241
1297
|
if (!node.isConnected) {
|
|
1242
|
-
|
|
1243
|
-
return;
|
|
1298
|
+
throw new Error('Modbus未连接');
|
|
1244
1299
|
}
|
|
1245
|
-
|
|
1300
|
+
|
|
1246
1301
|
// 暂停轮询(从站上报优先处理)
|
|
1247
1302
|
node.pausePolling = true;
|
|
1248
1303
|
const pauseStartTime = Date.now();
|
|
1249
|
-
|
|
1250
|
-
// 等待锁释放(最多等待500ms)
|
|
1251
|
-
const maxWait = 500;
|
|
1252
|
-
const startWait = Date.now();
|
|
1253
|
-
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1254
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
if (node.modbusLock) {
|
|
1258
|
-
node.error(`批量写入线圈超时: 从站${slaveId} 起始线圈${startCoil} (等待锁释放超时)`);
|
|
1259
|
-
// 恢复轮询
|
|
1260
|
-
node.pausePolling = false;
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1304
|
+
|
|
1264
1305
|
try {
|
|
1306
|
+
// 等待锁释放(最多等待100ms)
|
|
1307
|
+
const maxWait = 100;
|
|
1308
|
+
const startWait = Date.now();
|
|
1309
|
+
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1310
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (node.modbusLock) {
|
|
1314
|
+
throw new Error(`批量写入线圈等待锁超时: 从站${slaveId} 起始线圈${startCoil}`);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1265
1317
|
// 设置锁
|
|
1266
1318
|
node.modbusLock = true;
|
|
1267
|
-
|
|
1319
|
+
|
|
1268
1320
|
node.client.setID(slaveId);
|
|
1269
1321
|
await node.client.writeCoils(startCoil, values);
|
|
1270
|
-
|
|
1322
|
+
|
|
1271
1323
|
// 记录写入时间(用于暂停轮询)
|
|
1272
1324
|
node.lastWriteTime[slaveId] = Date.now();
|
|
1273
|
-
|
|
1325
|
+
|
|
1274
1326
|
// 更新本地状态
|
|
1275
1327
|
for (let i = 0; i < values.length; i++) {
|
|
1276
1328
|
node.deviceStates[slaveId].coils[startCoil + i] = values[i];
|
|
@@ -1282,13 +1334,13 @@ module.exports = function(RED) {
|
|
|
1282
1334
|
value: values[i]
|
|
1283
1335
|
});
|
|
1284
1336
|
}
|
|
1285
|
-
|
|
1337
|
+
|
|
1286
1338
|
node.debug(`批量写入成功: 从站${slaveId} 起始线圈${startCoil} 共${values.length}个线圈`);
|
|
1287
|
-
|
|
1339
|
+
|
|
1288
1340
|
// 释放锁
|
|
1289
1341
|
node.modbusLock = false;
|
|
1290
|
-
|
|
1291
|
-
//
|
|
1342
|
+
|
|
1343
|
+
// 延迟恢复轮询(给从站响应预留时间,减少到20ms)
|
|
1292
1344
|
setTimeout(() => {
|
|
1293
1345
|
node.pausePolling = false;
|
|
1294
1346
|
const pauseDuration = Date.now() - pauseStartTime;
|
|
@@ -1296,20 +1348,38 @@ module.exports = function(RED) {
|
|
|
1296
1348
|
node.debug(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1297
1349
|
node.pollingPausedCount = 0;
|
|
1298
1350
|
}
|
|
1299
|
-
},
|
|
1300
|
-
|
|
1351
|
+
}, 20);
|
|
1352
|
+
|
|
1301
1353
|
} catch (err) {
|
|
1302
1354
|
// 释放锁
|
|
1303
1355
|
node.modbusLock = false;
|
|
1304
|
-
|
|
1356
|
+
|
|
1305
1357
|
// 恢复轮询
|
|
1306
1358
|
node.pausePolling = false;
|
|
1307
1359
|
node.pollingPausedCount = 0;
|
|
1308
|
-
|
|
1360
|
+
|
|
1309
1361
|
node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
|
|
1310
|
-
throw err;
|
|
1362
|
+
throw err;
|
|
1311
1363
|
}
|
|
1312
1364
|
};
|
|
1365
|
+
|
|
1366
|
+
// 批量写入多个线圈(公共接口,通过队列执行)
|
|
1367
|
+
node.writeMultipleCoils = function(slaveId, startCoil, values) {
|
|
1368
|
+
return new Promise((resolve, reject) => {
|
|
1369
|
+
// 添加到队列
|
|
1370
|
+
node.writeQueue.push({
|
|
1371
|
+
type: 'multiple',
|
|
1372
|
+
slaveId: slaveId,
|
|
1373
|
+
startCoil: startCoil,
|
|
1374
|
+
values: values,
|
|
1375
|
+
resolve: resolve,
|
|
1376
|
+
reject: reject
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
// 触发队列处理
|
|
1380
|
+
node.processWriteQueue();
|
|
1381
|
+
});
|
|
1382
|
+
};
|
|
1313
1383
|
|
|
1314
1384
|
// 监听内部事件(从站开关节点发送的写入命令)
|
|
1315
1385
|
// 这是免连线通信的核心机制
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|