node-red-contrib-symi-modbus 2.6.8 → 2.6.9
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 +212 -366
- package/examples/basic-flow.json +33 -21
- package/nodes/custom-protocol.html +276 -0
- package/nodes/custom-protocol.js +240 -0
- package/nodes/homekit-bridge.html +44 -22
- package/nodes/homekit-bridge.js +18 -0
- package/nodes/mesh-protocol.js +286 -0
- package/nodes/modbus-dashboard.html +444 -0
- package/nodes/modbus-dashboard.js +116 -0
- package/nodes/modbus-debug.js +10 -2
- package/nodes/modbus-master.js +175 -74
- package/nodes/modbus-slave-switch.html +196 -12
- package/nodes/modbus-slave-switch.js +479 -157
- package/nodes/serial-port-config.js +84 -21
- 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 = 50; // 写入队列处理间隔(50ms,确保总线稳定,避免数据丢失)
|
|
154
|
+
|
|
150
155
|
// 定期清理机制(每小时清理一次,防止内存泄漏)
|
|
151
156
|
node.cleanupTimer = setInterval(() => {
|
|
152
157
|
// 清理过期的错误日志记录
|
|
@@ -1157,33 +1162,78 @@ 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
|
+
// 记录队列开始处理时间
|
|
1174
|
+
const queueStartTime = Date.now();
|
|
1175
|
+
const queueLength = node.writeQueue.length;
|
|
1176
|
+
|
|
1177
|
+
while (node.writeQueue.length > 0) {
|
|
1178
|
+
const task = node.writeQueue.shift();
|
|
1179
|
+
|
|
1180
|
+
try {
|
|
1181
|
+
if (task.type === 'single') {
|
|
1182
|
+
await node._writeSingleCoilInternal(task.slaveId, task.coil, task.value);
|
|
1183
|
+
} else if (task.type === 'multiple') {
|
|
1184
|
+
await node._writeMultipleCoilsInternal(task.slaveId, task.startCoil, task.values);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// 写入成功,调用回调
|
|
1188
|
+
if (task.resolve) {
|
|
1189
|
+
task.resolve();
|
|
1190
|
+
}
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
// 写入失败,调用错误回调
|
|
1193
|
+
if (task.reject) {
|
|
1194
|
+
task.reject(err);
|
|
1195
|
+
}
|
|
1196
|
+
// 写入失败不中断队列,继续处理下一个任务
|
|
1197
|
+
node.warn(`队列任务失败,继续处理下一个任务: ${err.message}`);
|
|
1198
|
+
}
|
|
1170
1199
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1176
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1177
|
-
waitCount++;
|
|
1200
|
+
// 等待一段时间再处理下一个任务(20ms间隔,确保总线稳定)
|
|
1201
|
+
if (node.writeQueue.length > 0) {
|
|
1202
|
+
await new Promise(resolve => setTimeout(resolve, node.writeQueueInterval));
|
|
1203
|
+
}
|
|
1178
1204
|
}
|
|
1179
1205
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
node.
|
|
1206
|
+
// 队列处理完成,输出统计信息
|
|
1207
|
+
const queueDuration = Date.now() - queueStartTime;
|
|
1208
|
+
if (queueLength > 1) {
|
|
1209
|
+
node.debug(`写入队列处理完成:${queueLength}个任务,耗时${queueDuration}ms`);
|
|
1184
1210
|
}
|
|
1185
|
-
|
|
1211
|
+
|
|
1212
|
+
node.isProcessingWrite = false;
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
// 写单个线圈(内部实现,不经过队列)
|
|
1216
|
+
node._writeSingleCoilInternal = async function(slaveId, coil, value) {
|
|
1217
|
+
if (!node.isConnected) {
|
|
1218
|
+
throw new Error('Modbus未连接');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 暂停轮询(写操作优先)
|
|
1222
|
+
node.pausePolling = true;
|
|
1223
|
+
const pauseStartTime = Date.now();
|
|
1224
|
+
|
|
1186
1225
|
try {
|
|
1226
|
+
// 等待锁释放(最多等待100ms,因为有队列机制,不需要等太久)
|
|
1227
|
+
const maxWait = 100;
|
|
1228
|
+
const startWait = Date.now();
|
|
1229
|
+
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1230
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (node.modbusLock) {
|
|
1234
|
+
throw new Error(`写入线圈等待锁超时: 从站${slaveId} 线圈${coil}`);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1187
1237
|
// 设置锁
|
|
1188
1238
|
node.modbusLock = true;
|
|
1189
1239
|
|
|
@@ -1198,10 +1248,11 @@ module.exports = function(RED) {
|
|
|
1198
1248
|
node.lastWriteTime[slaveId] = Date.now();
|
|
1199
1249
|
|
|
1200
1250
|
// 更新本地状态
|
|
1251
|
+
const oldValue = node.deviceStates[slaveId].coils[coil];
|
|
1201
1252
|
node.deviceStates[slaveId].coils[coil] = value;
|
|
1202
1253
|
|
|
1203
1254
|
node.log(`写入成功: 从站${slaveId} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1204
|
-
|
|
1255
|
+
|
|
1205
1256
|
// 发布到MQTT和触发事件
|
|
1206
1257
|
node.publishMqttState(slaveId, coil, value);
|
|
1207
1258
|
node.emit('stateUpdate', {
|
|
@@ -1209,11 +1260,22 @@ module.exports = function(RED) {
|
|
|
1209
1260
|
coil: coil,
|
|
1210
1261
|
value: value
|
|
1211
1262
|
});
|
|
1212
|
-
|
|
1263
|
+
|
|
1264
|
+
// 只在状态真正改变时广播状态变化事件(用于LED反馈)
|
|
1265
|
+
// 避免重复广播导致LED反馈死循环
|
|
1266
|
+
if (oldValue !== value) {
|
|
1267
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
1268
|
+
slave: slaveId,
|
|
1269
|
+
coil: coil,
|
|
1270
|
+
value: value,
|
|
1271
|
+
source: 'write'
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1213
1275
|
// 释放锁
|
|
1214
1276
|
node.modbusLock = false;
|
|
1215
|
-
|
|
1216
|
-
//
|
|
1277
|
+
|
|
1278
|
+
// 延迟恢复轮询(给从站响应预留时间,减少到20ms)
|
|
1217
1279
|
setTimeout(() => {
|
|
1218
1280
|
node.pausePolling = false;
|
|
1219
1281
|
const pauseDuration = Date.now() - pauseStartTime;
|
|
@@ -1221,74 +1283,103 @@ module.exports = function(RED) {
|
|
|
1221
1283
|
node.debug(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1222
1284
|
node.pollingPausedCount = 0;
|
|
1223
1285
|
}
|
|
1224
|
-
},
|
|
1225
|
-
|
|
1286
|
+
}, 20);
|
|
1287
|
+
|
|
1226
1288
|
} catch (err) {
|
|
1227
1289
|
// 释放锁
|
|
1228
1290
|
node.modbusLock = false;
|
|
1229
|
-
|
|
1291
|
+
|
|
1230
1292
|
// 恢复轮询
|
|
1231
1293
|
node.pausePolling = false;
|
|
1232
1294
|
node.pollingPausedCount = 0;
|
|
1233
|
-
|
|
1295
|
+
|
|
1234
1296
|
node.error(`写入线圈失败: 从站${slaveId} 线圈${coil} - ${err.message}`);
|
|
1235
|
-
throw err;
|
|
1297
|
+
throw err;
|
|
1236
1298
|
}
|
|
1237
1299
|
};
|
|
1300
|
+
|
|
1301
|
+
// 写单个线圈(公共接口,通过队列执行)
|
|
1302
|
+
node.writeSingleCoil = function(slaveId, coil, value) {
|
|
1303
|
+
return new Promise((resolve, reject) => {
|
|
1304
|
+
// 添加到队列
|
|
1305
|
+
node.writeQueue.push({
|
|
1306
|
+
type: 'single',
|
|
1307
|
+
slaveId: slaveId,
|
|
1308
|
+
coil: coil,
|
|
1309
|
+
value: value,
|
|
1310
|
+
resolve: resolve,
|
|
1311
|
+
reject: reject
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// 触发队列处理
|
|
1315
|
+
node.processWriteQueue();
|
|
1316
|
+
});
|
|
1317
|
+
};
|
|
1238
1318
|
|
|
1239
|
-
//
|
|
1240
|
-
node.
|
|
1319
|
+
// 批量写入多个线圈(内部实现,不经过队列)
|
|
1320
|
+
node._writeMultipleCoilsInternal = async function(slaveId, startCoil, values) {
|
|
1241
1321
|
if (!node.isConnected) {
|
|
1242
|
-
|
|
1243
|
-
return;
|
|
1322
|
+
throw new Error('Modbus未连接');
|
|
1244
1323
|
}
|
|
1245
|
-
|
|
1324
|
+
|
|
1246
1325
|
// 暂停轮询(从站上报优先处理)
|
|
1247
1326
|
node.pausePolling = true;
|
|
1248
1327
|
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
|
-
|
|
1328
|
+
|
|
1264
1329
|
try {
|
|
1330
|
+
// 等待锁释放(最多等待100ms)
|
|
1331
|
+
const maxWait = 100;
|
|
1332
|
+
const startWait = Date.now();
|
|
1333
|
+
while (node.modbusLock && (Date.now() - startWait) < maxWait) {
|
|
1334
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (node.modbusLock) {
|
|
1338
|
+
throw new Error(`批量写入线圈等待锁超时: 从站${slaveId} 起始线圈${startCoil}`);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1265
1341
|
// 设置锁
|
|
1266
1342
|
node.modbusLock = true;
|
|
1267
|
-
|
|
1343
|
+
|
|
1268
1344
|
node.client.setID(slaveId);
|
|
1269
1345
|
await node.client.writeCoils(startCoil, values);
|
|
1270
|
-
|
|
1346
|
+
|
|
1271
1347
|
// 记录写入时间(用于暂停轮询)
|
|
1272
1348
|
node.lastWriteTime[slaveId] = Date.now();
|
|
1273
|
-
|
|
1349
|
+
|
|
1274
1350
|
// 更新本地状态
|
|
1275
1351
|
for (let i = 0; i < values.length; i++) {
|
|
1276
|
-
|
|
1352
|
+
const coilIndex = startCoil + i;
|
|
1353
|
+
const oldValue = node.deviceStates[slaveId].coils[coilIndex];
|
|
1354
|
+
const newValue = values[i];
|
|
1355
|
+
|
|
1356
|
+
node.deviceStates[slaveId].coils[coilIndex] = newValue;
|
|
1357
|
+
|
|
1277
1358
|
// 发布到MQTT和触发事件
|
|
1278
|
-
node.publishMqttState(slaveId,
|
|
1359
|
+
node.publishMqttState(slaveId, coilIndex, newValue);
|
|
1279
1360
|
node.emit('stateUpdate', {
|
|
1280
1361
|
slave: slaveId,
|
|
1281
|
-
coil:
|
|
1282
|
-
value:
|
|
1362
|
+
coil: coilIndex,
|
|
1363
|
+
value: newValue
|
|
1283
1364
|
});
|
|
1365
|
+
|
|
1366
|
+
// 只在状态真正改变时广播状态变化事件(用于LED反馈)
|
|
1367
|
+
if (oldValue !== newValue) {
|
|
1368
|
+
RED.events.emit('modbus:coilStateChanged', {
|
|
1369
|
+
slave: slaveId,
|
|
1370
|
+
coil: coilIndex,
|
|
1371
|
+
value: newValue,
|
|
1372
|
+
source: 'write'
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1284
1375
|
}
|
|
1285
|
-
|
|
1376
|
+
|
|
1286
1377
|
node.debug(`批量写入成功: 从站${slaveId} 起始线圈${startCoil} 共${values.length}个线圈`);
|
|
1287
|
-
|
|
1378
|
+
|
|
1288
1379
|
// 释放锁
|
|
1289
1380
|
node.modbusLock = false;
|
|
1290
|
-
|
|
1291
|
-
//
|
|
1381
|
+
|
|
1382
|
+
// 延迟恢复轮询(给从站响应预留时间,减少到20ms)
|
|
1292
1383
|
setTimeout(() => {
|
|
1293
1384
|
node.pausePolling = false;
|
|
1294
1385
|
const pauseDuration = Date.now() - pauseStartTime;
|
|
@@ -1296,20 +1387,38 @@ module.exports = function(RED) {
|
|
|
1296
1387
|
node.debug(`轮询暂停 ${pauseDuration}ms,跳过 ${node.pollingPausedCount} 次轮询`);
|
|
1297
1388
|
node.pollingPausedCount = 0;
|
|
1298
1389
|
}
|
|
1299
|
-
},
|
|
1300
|
-
|
|
1390
|
+
}, 20);
|
|
1391
|
+
|
|
1301
1392
|
} catch (err) {
|
|
1302
1393
|
// 释放锁
|
|
1303
1394
|
node.modbusLock = false;
|
|
1304
|
-
|
|
1395
|
+
|
|
1305
1396
|
// 恢复轮询
|
|
1306
1397
|
node.pausePolling = false;
|
|
1307
1398
|
node.pollingPausedCount = 0;
|
|
1308
|
-
|
|
1399
|
+
|
|
1309
1400
|
node.error(`批量写入线圈失败: 从站${slaveId} 起始线圈${startCoil} - ${err.message}`);
|
|
1310
|
-
throw err;
|
|
1401
|
+
throw err;
|
|
1311
1402
|
}
|
|
1312
1403
|
};
|
|
1404
|
+
|
|
1405
|
+
// 批量写入多个线圈(公共接口,通过队列执行)
|
|
1406
|
+
node.writeMultipleCoils = function(slaveId, startCoil, values) {
|
|
1407
|
+
return new Promise((resolve, reject) => {
|
|
1408
|
+
// 添加到队列
|
|
1409
|
+
node.writeQueue.push({
|
|
1410
|
+
type: 'multiple',
|
|
1411
|
+
slaveId: slaveId,
|
|
1412
|
+
startCoil: startCoil,
|
|
1413
|
+
values: values,
|
|
1414
|
+
resolve: resolve,
|
|
1415
|
+
reject: reject
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// 触发队列处理
|
|
1419
|
+
node.processWriteQueue();
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1313
1422
|
|
|
1314
1423
|
// 监听内部事件(从站开关节点发送的写入命令)
|
|
1315
1424
|
// 这是免连线通信的核心机制
|
|
@@ -1326,18 +1435,10 @@ module.exports = function(RED) {
|
|
|
1326
1435
|
node.log(`收到内部事件:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1327
1436
|
|
|
1328
1437
|
try {
|
|
1329
|
-
//
|
|
1438
|
+
// 执行写入操作(writeSingleCoil内部已经会广播状态变化事件)
|
|
1330
1439
|
await node.writeSingleCoil(slave, coil, value);
|
|
1331
1440
|
|
|
1332
|
-
|
|
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'}`);
|
|
1441
|
+
node.log(`内部事件写入成功:从站${slave} 线圈${coil} = ${value ? 'ON' : 'OFF'}`);
|
|
1341
1442
|
} catch (err) {
|
|
1342
1443
|
node.error(`内部事件写入失败: 从站${slave} 线圈${coil} - ${err.message}`);
|
|
1343
1444
|
}
|
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value: "从站开关"},
|
|
7
7
|
// RS-485连接配置(共享配置节点)
|
|
8
|
-
serialPortConfig: {value: "", type: "serial-port-config", required:
|
|
8
|
+
serialPortConfig: {value: "", type: "serial-port-config", required: false},
|
|
9
9
|
// MQTT配置(可选)
|
|
10
10
|
enableMqtt: {value: false}, // 默认不启用MQTT
|
|
11
11
|
mqttServer: {value: "", type: "mqtt-server-config", required: false},
|
|
12
12
|
// 开关面板配置
|
|
13
13
|
switchBrand: {value: "symi"}, // 品牌选择
|
|
14
|
-
buttonType: {value: "switch"}, // 按钮类型:switch=开关模式,scene
|
|
14
|
+
buttonType: {value: "switch"}, // 按钮类型:switch=开关模式,scene=场景模式,mesh=Mesh模式
|
|
15
15
|
switchId: {value: 0, validate: RED.validators.number()},
|
|
16
16
|
buttonNumber: {value: 1, validate: RED.validators.number()},
|
|
17
|
+
// Mesh模式配置
|
|
18
|
+
meshMacAddress: {value: ""}, // Mesh设备MAC地址
|
|
19
|
+
meshShortAddress: {value: 0}, // Mesh设备短地址
|
|
20
|
+
meshButtonNumber: {value: 1, validate: RED.validators.number()}, // Mesh按键编号(1-6)
|
|
21
|
+
meshTotalButtons: {value: 1, validate: RED.validators.number()}, // Mesh开关总路数(1-6)
|
|
17
22
|
// 映射到继电器
|
|
18
23
|
targetSlaveAddress: {value: 10, validate: RED.validators.number()},
|
|
19
24
|
targetCoilNumber: {value: 1, validate: RED.validators.number()} // 默认值改为1(显示为1路)
|
|
@@ -22,11 +27,21 @@
|
|
|
22
27
|
outputs: 1,
|
|
23
28
|
icon: "light.png",
|
|
24
29
|
label: function() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
if (this.buttonType === 'mesh') {
|
|
31
|
+
// Mesh模式显示
|
|
32
|
+
const mac = this.meshMacAddress || '未选择';
|
|
33
|
+
const btn = this.meshButtonNumber || 1;
|
|
34
|
+
const coilDisplay = this.targetCoilNumber || 1;
|
|
35
|
+
return this.name || `Mesh[${mac}]-按钮${btn} → 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
|
|
36
|
+
} else {
|
|
37
|
+
// 开关模式和场景模式显示
|
|
38
|
+
const coilDisplay = this.targetCoilNumber || 1;
|
|
39
|
+
return this.name || `开关${this.switchId}-按钮${this.buttonNumber} → 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
|
|
40
|
+
}
|
|
28
41
|
},
|
|
29
42
|
oneditprepare: function() {
|
|
43
|
+
const node = this;
|
|
44
|
+
|
|
30
45
|
// MQTT开关控制显示/隐藏
|
|
31
46
|
$("#node-input-enableMqtt").on("change", function() {
|
|
32
47
|
if ($(this).is(":checked")) {
|
|
@@ -35,9 +50,139 @@
|
|
|
35
50
|
$(".form-row-mqtt-slave").hide();
|
|
36
51
|
}
|
|
37
52
|
});
|
|
38
|
-
|
|
53
|
+
|
|
54
|
+
// 按钮类型切换控制显示/隐藏
|
|
55
|
+
$("#node-input-buttonType").on("change", function() {
|
|
56
|
+
const buttonType = $(this).val();
|
|
57
|
+
if (buttonType === 'mesh') {
|
|
58
|
+
// Mesh模式:显示Mesh配置,隐藏RS-485配置
|
|
59
|
+
$(".form-row-mesh").show();
|
|
60
|
+
$(".form-row-rs485").hide();
|
|
61
|
+
$("#btn-discover-mesh").show();
|
|
62
|
+
} else {
|
|
63
|
+
// 开关/场景模式:显示RS-485配置,隐藏Mesh配置
|
|
64
|
+
$(".form-row-mesh").hide();
|
|
65
|
+
$(".form-row-rs485").show();
|
|
66
|
+
$("#btn-discover-mesh").hide();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Mesh设备发现按钮
|
|
71
|
+
$("#btn-discover-mesh").on("click", function() {
|
|
72
|
+
const serialPortConfig = $("#node-input-serialPortConfig").val();
|
|
73
|
+
if (!serialPortConfig) {
|
|
74
|
+
RED.notify("请先选择RS-485连接配置", "warning");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 禁用按钮,显示加载状态
|
|
79
|
+
$(this).prop("disabled", true).html('<i class="fa fa-spinner fa-spin"></i> 正在扫描...');
|
|
80
|
+
|
|
81
|
+
// 发送设备发现请求
|
|
82
|
+
$.ajax({
|
|
83
|
+
url: "symi-mesh/discover",
|
|
84
|
+
type: "POST",
|
|
85
|
+
data: JSON.stringify({
|
|
86
|
+
serialPortConfig: serialPortConfig
|
|
87
|
+
}),
|
|
88
|
+
contentType: "application/json",
|
|
89
|
+
success: function(devices) {
|
|
90
|
+
// 更新设备列表
|
|
91
|
+
const select = $("#node-input-meshMacAddress");
|
|
92
|
+
select.empty();
|
|
93
|
+
select.append('<option value="">请选择Mesh开关</option>');
|
|
94
|
+
|
|
95
|
+
devices.forEach(function(device) {
|
|
96
|
+
if (device.type === 0x01 && device.buttons > 0) {
|
|
97
|
+
// 只显示开关类型设备
|
|
98
|
+
const mac = device.mac;
|
|
99
|
+
const label = `${mac} (${device.buttons}路开关)`;
|
|
100
|
+
select.append(`<option value="${mac}" data-short-addr="${device.shortAddr}" data-buttons="${device.buttons}">${label}</option>`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
RED.notify(`发现 ${devices.length} 个Mesh设备`, "success");
|
|
105
|
+
$("#btn-discover-mesh").prop("disabled", false).html('<i class="fa fa-search"></i> 扫描设备');
|
|
106
|
+
},
|
|
107
|
+
error: function(xhr, status, error) {
|
|
108
|
+
RED.notify("设备发现失败: " + error, "error");
|
|
109
|
+
$("#btn-discover-mesh").prop("disabled", false).html('<i class="fa fa-search"></i> 扫描设备');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Mesh设备选择变化时更新短地址和按钮数
|
|
115
|
+
$("#node-input-meshMacAddress").on("change", function() {
|
|
116
|
+
const selected = $(this).find("option:selected");
|
|
117
|
+
const shortAddr = selected.data("short-addr");
|
|
118
|
+
const buttons = selected.data("buttons");
|
|
119
|
+
|
|
120
|
+
if (shortAddr !== undefined) {
|
|
121
|
+
$("#node-input-meshShortAddress").val(shortAddr);
|
|
122
|
+
$("#node-input-meshTotalButtons").val(buttons);
|
|
123
|
+
|
|
124
|
+
// 更新按钮编号选项
|
|
125
|
+
const btnSelect = $("#node-input-meshButtonNumber");
|
|
126
|
+
btnSelect.empty();
|
|
127
|
+
for (let i = 1; i <= buttons; i++) {
|
|
128
|
+
btnSelect.append(`<option value="${i}">按钮 ${i}</option>`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 加载已保存的Mesh设备列表
|
|
134
|
+
function loadSavedMeshDevices() {
|
|
135
|
+
$.ajax({
|
|
136
|
+
url: "symi-mesh/devices",
|
|
137
|
+
type: "GET",
|
|
138
|
+
success: function(devices) {
|
|
139
|
+
if (devices && devices.length > 0) {
|
|
140
|
+
const select = $("#node-input-meshMacAddress");
|
|
141
|
+
select.empty();
|
|
142
|
+
select.append('<option value="">请选择Mesh开关</option>');
|
|
143
|
+
|
|
144
|
+
devices.forEach(function(device) {
|
|
145
|
+
if (device.type === 0x01 && device.buttons > 0) {
|
|
146
|
+
const mac = device.mac;
|
|
147
|
+
const label = `${mac} (${device.buttons}路开关)`;
|
|
148
|
+
const option = $(`<option value="${mac}" data-short-addr="${device.shortAddr}" data-buttons="${device.buttons}">${label}</option>`);
|
|
149
|
+
select.append(option);
|
|
150
|
+
|
|
151
|
+
// 如果是当前配置的设备,选中它
|
|
152
|
+
if (mac === node.meshMacAddress) {
|
|
153
|
+
option.prop("selected", true);
|
|
154
|
+
$("#node-input-meshShortAddress").val(device.shortAddr);
|
|
155
|
+
$("#node-input-meshTotalButtons").val(device.buttons);
|
|
156
|
+
|
|
157
|
+
// 更新按钮编号选项
|
|
158
|
+
const btnSelect = $("#node-input-meshButtonNumber");
|
|
159
|
+
btnSelect.empty();
|
|
160
|
+
for (let i = 1; i <= device.buttons; i++) {
|
|
161
|
+
const btnOption = $(`<option value="${i}">按钮 ${i}</option>`);
|
|
162
|
+
if (i === node.meshButtonNumber) {
|
|
163
|
+
btnOption.prop("selected", true);
|
|
164
|
+
}
|
|
165
|
+
btnSelect.append(btnOption);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
error: function(xhr, status, error) {
|
|
173
|
+
console.log("加载Mesh设备列表失败: " + error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
39
178
|
// 初始化显示状态
|
|
40
179
|
$("#node-input-enableMqtt").trigger("change");
|
|
180
|
+
$("#node-input-buttonType").trigger("change");
|
|
181
|
+
|
|
182
|
+
// 如果是Mesh模式,加载已保存的设备列表
|
|
183
|
+
if (node.buttonType === 'mesh') {
|
|
184
|
+
loadSavedMeshDevices();
|
|
185
|
+
}
|
|
41
186
|
}
|
|
42
187
|
});
|
|
43
188
|
</script>
|
|
@@ -119,16 +264,19 @@
|
|
|
119
264
|
<div class="form-row">
|
|
120
265
|
<label for="node-input-buttonType" style="width: 110px;"><i class="fa fa-cog"></i> 按钮类型</label>
|
|
121
266
|
<select id="node-input-buttonType" style="width: 200px;">
|
|
122
|
-
<option value="switch"
|
|
123
|
-
<option value="scene"
|
|
267
|
+
<option value="switch">开关按钮(RS-485)</option>
|
|
268
|
+
<option value="scene">场景按钮(RS-485)</option>
|
|
269
|
+
<option value="mesh">Mesh开关(蓝牙Mesh)</option>
|
|
124
270
|
</select>
|
|
125
271
|
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
126
|
-
<strong>开关按钮</strong
|
|
127
|
-
<strong>场景按钮</strong
|
|
272
|
+
<strong>开关按钮</strong>:RS-485开关,有开/关状态<br>
|
|
273
|
+
<strong>场景按钮</strong>:RS-485场景触发<br>
|
|
274
|
+
<strong>Mesh开关</strong>:蓝牙Mesh开关(1-6路)
|
|
128
275
|
</div>
|
|
129
276
|
</div>
|
|
130
277
|
|
|
131
|
-
|
|
278
|
+
<!-- RS-485模式配置 -->
|
|
279
|
+
<div class="form-row form-row-rs485">
|
|
132
280
|
<label for="node-input-switchId" style="width: 110px;"><i class="fa fa-id-card"></i> 开关ID</label>
|
|
133
281
|
<input type="number" id="node-input-switchId" placeholder="0" min="0" max="255" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
134
282
|
<span style="margin-left: 10px; color: #666; font-size: 12px;">物理面板地址:<strong>0-255</strong></span>
|
|
@@ -137,7 +285,7 @@
|
|
|
137
285
|
</div>
|
|
138
286
|
</div>
|
|
139
287
|
|
|
140
|
-
<div class="form-row">
|
|
288
|
+
<div class="form-row form-row-rs485">
|
|
141
289
|
<label for="node-input-buttonNumber" style="width: 110px;"><i class="fa fa-hand-pointer-o"></i> 按钮编号</label>
|
|
142
290
|
<select id="node-input-buttonNumber" style="width: 150px;">
|
|
143
291
|
<option value="1">按钮 1</option>
|
|
@@ -151,6 +299,42 @@
|
|
|
151
299
|
</select>
|
|
152
300
|
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">面板物理按键</span>
|
|
153
301
|
</div>
|
|
302
|
+
|
|
303
|
+
<!-- Mesh模式配置 -->
|
|
304
|
+
<div class="form-row form-row-mesh" style="display: none;">
|
|
305
|
+
<button type="button" id="btn-discover-mesh" class="red-ui-button" style="margin-left: 110px;">
|
|
306
|
+
<i class="fa fa-search"></i> 扫描设备
|
|
307
|
+
</button>
|
|
308
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 5px;">
|
|
309
|
+
点击扫描按钮自动发现Mesh网关中的所有开关设备
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div class="form-row form-row-mesh" style="display: none;">
|
|
314
|
+
<label for="node-input-meshMacAddress" style="width: 110px;"><i class="fa fa-bluetooth"></i> Mesh设备</label>
|
|
315
|
+
<select id="node-input-meshMacAddress" style="width: 300px;">
|
|
316
|
+
<option value="">请选择Mesh开关</option>
|
|
317
|
+
</select>
|
|
318
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
319
|
+
选择要控制的Mesh开关设备(MAC地址)
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="form-row form-row-mesh" style="display: none;">
|
|
324
|
+
<label for="node-input-meshButtonNumber" style="width: 110px;"><i class="fa fa-hand-pointer-o"></i> 按钮编号</label>
|
|
325
|
+
<select id="node-input-meshButtonNumber" style="width: 150px;">
|
|
326
|
+
<option value="1">按钮 1</option>
|
|
327
|
+
<option value="2">按钮 2</option>
|
|
328
|
+
<option value="3">按钮 3</option>
|
|
329
|
+
<option value="4">按钮 4</option>
|
|
330
|
+
<option value="5">按钮 5</option>
|
|
331
|
+
<option value="6">按钮 6</option>
|
|
332
|
+
</select>
|
|
333
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">Mesh开关按键(1-6路)</span>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<input type="hidden" id="node-input-meshShortAddress">
|
|
337
|
+
<input type="hidden" id="node-input-meshTotalButtons">
|
|
154
338
|
|
|
155
339
|
<!-- 映射到继电器配置 -->
|
|
156
340
|
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|