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.
@@ -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.writeSingleCoil = async function(slaveId, coil, value) {
1162
- if (!node.isConnected) {
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
- node.pausePolling = true;
1169
- const pauseStartTime = Date.now();
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
- // 等待锁释放(最多等待1000ms,减少超时时间)
1172
- const maxWait = 1000;
1173
- const startWait = Date.now();
1174
- let waitCount = 0;
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
- if (node.modbusLock) {
1181
- // 强制释放锁(避免死锁)
1182
- node.warn(`写入线圈等待超时(${waitCount * 10}ms),强制释放锁: 从站${slaveId} 线圈${coil}`);
1183
- node.modbusLock = false;
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
- }, 100);
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.writeMultipleCoils = async function(slaveId, startCoil, values) {
1319
+ // 批量写入多个线圈(内部实现,不经过队列)
1320
+ node._writeMultipleCoilsInternal = async function(slaveId, startCoil, values) {
1241
1321
  if (!node.isConnected) {
1242
- node.warn('Modbus未连接');
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
- node.deviceStates[slaveId].coils[startCoil + i] = values[i];
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, startCoil + i, values[i]);
1359
+ node.publishMqttState(slaveId, coilIndex, newValue);
1279
1360
  node.emit('stateUpdate', {
1280
1361
  slave: slaveId,
1281
- coil: startCoil + i,
1282
- value: values[i]
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
- }, 100);
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
- // 写入成功后,广播状态变化事件(用于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'}`);
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: true},
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
- // 显示时直接使用用户输入的路数(1-32)
26
- const coilDisplay = this.targetCoilNumber || 1;
27
- return this.name || `开关${this.switchId}-按钮${this.buttonNumber} 继电器${this.targetSlaveAddress}-${coilDisplay}路`;
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">开关按钮</option>
123
- <option value="scene">场景按钮</option>
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>:灯光、插座等开关控制,有开/关状态<br>
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
- <div class="form-row">
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;">