node-red-contrib-symi-modbus 2.6.8 → 2.7.2
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 +164 -57
- package/nodes/custom-protocol.html +276 -0
- package/nodes/custom-protocol.js +201 -0
- package/nodes/modbus-dashboard.html +396 -0
- package/nodes/modbus-dashboard.js +98 -0
- package/nodes/modbus-master.js +130 -60
- package/package.json +5 -3
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit
|
|
3
|
+
"version": "2.7.2",
|
|
4
|
+
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"mqtt-server-config": "nodes/mqtt-server-config.js",
|
|
39
39
|
"serial-port-config": "nodes/serial-port-config.js",
|
|
40
40
|
"modbus-debug": "nodes/modbus-debug.js",
|
|
41
|
-
"homekit-bridge": "nodes/homekit-bridge.js"
|
|
41
|
+
"homekit-bridge": "nodes/homekit-bridge.js",
|
|
42
|
+
"modbus-dashboard": "nodes/modbus-dashboard.js",
|
|
43
|
+
"custom-protocol": "nodes/custom-protocol.js"
|
|
42
44
|
}
|
|
43
45
|
},
|
|
44
46
|
"dependencies": {
|