node-red-contrib-symi-mesh 1.3.1 → 1.6.1
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 +219 -48
- package/examples/basic-example.json +151 -0
- package/lib/device-manager.js +109 -23
- package/lib/mqtt-helper.js +25 -4
- package/lib/protocol.js +22 -14
- package/lib/tcp-client.js +15 -13
- package/nodes/rs485-debug.html +238 -0
- package/nodes/rs485-debug.js +220 -0
- package/nodes/symi-485-bridge.html +376 -0
- package/nodes/symi-485-bridge.js +776 -0
- package/nodes/symi-485-config.html +125 -0
- package/nodes/symi-485-config.js +275 -0
- package/nodes/symi-cloud-sync.html +4 -4
- package/nodes/symi-cloud-sync.js +43 -10
- package/nodes/symi-device.html +1 -0
- package/nodes/symi-device.js +23 -14
- package/nodes/symi-gateway.js +121 -39
- package/nodes/symi-mqtt.js +233 -49
- package/package.json +7 -4
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('symi-rs485-bridge', {
|
|
3
|
+
category: 'Symi Mesh',
|
|
4
|
+
color: '#E9967A',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
gateway: { value: '', type: 'symi-gateway', required: true },
|
|
8
|
+
rs485Config: { value: '', type: 'symi-485-config', required: true },
|
|
9
|
+
mappings: { value: '[]' }
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
|
+
icon: 'bridge.png',
|
|
14
|
+
paletteLabel: 'RS485桥接',
|
|
15
|
+
label: function() {
|
|
16
|
+
if (this.name) return this.name;
|
|
17
|
+
try {
|
|
18
|
+
var m = JSON.parse(this.mappings || '[]');
|
|
19
|
+
if (m.length > 0) return 'RS485桥接 (' + m.length + '组)';
|
|
20
|
+
} catch(e) {}
|
|
21
|
+
return 'RS485桥接';
|
|
22
|
+
},
|
|
23
|
+
oneditprepare: function() {
|
|
24
|
+
var node = this;
|
|
25
|
+
var meshDevices = [];
|
|
26
|
+
var protocolData = {};
|
|
27
|
+
var mappings = [];
|
|
28
|
+
|
|
29
|
+
// 设置编辑面板更宽
|
|
30
|
+
var panel = $('#dialog-form').parent();
|
|
31
|
+
if (panel.length && panel.width() < 920) {
|
|
32
|
+
panel.css('width', '900px');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try { mappings = JSON.parse(node.mappings || '[]'); } catch(e) { mappings = []; }
|
|
36
|
+
|
|
37
|
+
// 加载Mesh设备 - 等待gateway选择框初始化完成
|
|
38
|
+
function loadMeshDevices(callback) {
|
|
39
|
+
var gatewayId = $('#node-input-gateway').val();
|
|
40
|
+
if (!gatewayId) {
|
|
41
|
+
meshDevices = [];
|
|
42
|
+
if (callback) callback();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
$.getJSON('/symi-rs485-bridge/mesh-devices/' + gatewayId)
|
|
46
|
+
.done(function(devices) {
|
|
47
|
+
meshDevices = devices || [];
|
|
48
|
+
console.log('[RS485 Bridge] 加载Mesh设备:', meshDevices.length);
|
|
49
|
+
if (callback) callback();
|
|
50
|
+
})
|
|
51
|
+
.fail(function() {
|
|
52
|
+
meshDevices = [];
|
|
53
|
+
if (callback) callback();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 加载协议模板
|
|
58
|
+
$.getJSON('/symi-rs485-bridge/protocols', function(data) {
|
|
59
|
+
protocolData = data || { brands: {} };
|
|
60
|
+
// 延迟加载设备,确保gateway选择框已初始化
|
|
61
|
+
setTimeout(function() {
|
|
62
|
+
loadMeshDevices(renderMappings);
|
|
63
|
+
}, 300);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
$('#node-input-gateway').on('change', function() {
|
|
67
|
+
setTimeout(function() {
|
|
68
|
+
loadMeshDevices(renderMappings);
|
|
69
|
+
}, 100);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 获取RS485设备按键数
|
|
73
|
+
function getDeviceChannels(brandId, deviceId) {
|
|
74
|
+
if (!brandId || !deviceId || !protocolData.brands) return 1;
|
|
75
|
+
var brand = protocolData.brands[brandId];
|
|
76
|
+
if (!brand || !brand.devices) return 1;
|
|
77
|
+
var device = brand.devices[deviceId];
|
|
78
|
+
if (!device) return 1;
|
|
79
|
+
// 直接使用channels属性
|
|
80
|
+
return device.channels || 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 构建Mesh设备选项(不包含按键,按键单独选择)
|
|
84
|
+
function getMeshOptions(selectedMac) {
|
|
85
|
+
var html = '<option value="">-- 选择 --</option>';
|
|
86
|
+
meshDevices.forEach(function(d) {
|
|
87
|
+
var selected = (d.mac === selectedMac) ? ' selected' : '';
|
|
88
|
+
html += '<option value="' + d.mac + '" data-channels="' + (d.channels || 1) + '"' + selected + '>' + d.name + '</option>';
|
|
89
|
+
});
|
|
90
|
+
return html;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 构建Mesh按键选项(仅当开关设备时显示)
|
|
94
|
+
function getMeshChannelOptions(mac, selectedChannel) {
|
|
95
|
+
var device = meshDevices.find(function(d) { return d.mac === mac; });
|
|
96
|
+
var channels = device ? (device.channels || 1) : 0;
|
|
97
|
+
if (channels <= 1) return '';
|
|
98
|
+
var html = '<select class="mesh-channel">';
|
|
99
|
+
for (var i = 1; i <= channels; i++) {
|
|
100
|
+
var sel = (i == selectedChannel) ? ' selected' : '';
|
|
101
|
+
html += '<option value="' + i + '"' + sel + '>第' + i + '路</option>';
|
|
102
|
+
}
|
|
103
|
+
html += '</select>';
|
|
104
|
+
return html;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 获取Mesh设备的按键数
|
|
108
|
+
function getMeshDeviceChannels(mac) {
|
|
109
|
+
var device = meshDevices.find(function(d) { return d.mac === mac; });
|
|
110
|
+
return device ? (device.channels || 1) : 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 构建品牌选项
|
|
114
|
+
function getBrandOptions(selected) {
|
|
115
|
+
var html = '<option value="">-- 品牌 --</option>';
|
|
116
|
+
if (protocolData.brands) {
|
|
117
|
+
Object.keys(protocolData.brands).forEach(function(brandId) {
|
|
118
|
+
var sel = (brandId === selected) ? ' selected' : '';
|
|
119
|
+
html += '<option value="' + brandId + '"' + sel + '>' + protocolData.brands[brandId].name + '</option>';
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return html;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 构建设备类型选项
|
|
126
|
+
function getDeviceOptions(brandId, selected) {
|
|
127
|
+
var html = '<option value="">-- 类型 --</option>';
|
|
128
|
+
if (brandId && protocolData.brands && protocolData.brands[brandId]) {
|
|
129
|
+
var devices = protocolData.brands[brandId].devices;
|
|
130
|
+
Object.keys(devices).forEach(function(deviceId) {
|
|
131
|
+
var sel = (deviceId === selected) ? ' selected' : '';
|
|
132
|
+
html += '<option value="' + deviceId + '"' + sel + '>' + devices[deviceId].name + '</option>';
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return html;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 构建RS485按键选项
|
|
139
|
+
function getRS485ChannelOptions(brandId, deviceId, selected) {
|
|
140
|
+
var channels = getDeviceChannels(brandId, deviceId);
|
|
141
|
+
if (channels <= 1) return '';
|
|
142
|
+
var html = '<select class="rs485-channel">';
|
|
143
|
+
for (var i = 1; i <= channels; i++) {
|
|
144
|
+
var sel = (i == selected) ? ' selected' : '';
|
|
145
|
+
html += '<option value="' + i + '"' + sel + '>第' + i + '路</option>';
|
|
146
|
+
}
|
|
147
|
+
html += '</select>';
|
|
148
|
+
return html;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 渲染映射列表
|
|
152
|
+
function renderMappings() {
|
|
153
|
+
var container = $('#mapping-list');
|
|
154
|
+
container.empty();
|
|
155
|
+
|
|
156
|
+
if (mappings.length === 0) {
|
|
157
|
+
container.append('<div class="mapping-empty">暂无映射,点击下方按钮添加</div>');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
mappings.forEach(function(m, idx) {
|
|
162
|
+
var row = $('<div class="mapping-row" data-idx="' + idx + '"></div>');
|
|
163
|
+
row.html(
|
|
164
|
+
'<div class="mesh-col">' +
|
|
165
|
+
' <select class="mesh-select">' + getMeshOptions(m.meshMac) + '</select>' +
|
|
166
|
+
' <span class="mesh-ch-wrap">' + getMeshChannelOptions(m.meshMac, m.meshChannel || 1) + '</span>' +
|
|
167
|
+
'</div>' +
|
|
168
|
+
'<div class="arrow-col"><i class="fa fa-arrows-h"></i></div>' +
|
|
169
|
+
'<div class="brand-col">' +
|
|
170
|
+
' <select class="brand-select">' + getBrandOptions(m.brand) + '</select>' +
|
|
171
|
+
'</div>' +
|
|
172
|
+
'<div class="device-col">' +
|
|
173
|
+
' <select class="device-select">' + getDeviceOptions(m.brand, m.device) + '</select>' +
|
|
174
|
+
' <span class="rs485-ch-wrap">' + getRS485ChannelOptions(m.brand, m.device, m.rs485Channel || 1) + '</span>' +
|
|
175
|
+
'</div>' +
|
|
176
|
+
'<div class="addr-col">' +
|
|
177
|
+
' <input type="number" class="addr-input" value="' + (m.address || 1) + '" min="1" max="255" title="Modbus地址">' +
|
|
178
|
+
'</div>' +
|
|
179
|
+
'<div class="del-col"><button type="button" class="red-ui-button red-ui-button-small btn-remove" title="删除"><i class="fa fa-times"></i></button></div>'
|
|
180
|
+
);
|
|
181
|
+
container.append(row);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
bindEvents();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 绑定事件
|
|
188
|
+
function bindEvents() {
|
|
189
|
+
var container = $('#mapping-list');
|
|
190
|
+
|
|
191
|
+
container.find('.brand-select').off('change').on('change', function() {
|
|
192
|
+
var row = $(this).closest('.mapping-row');
|
|
193
|
+
var idx = row.data('idx');
|
|
194
|
+
var brandId = $(this).val();
|
|
195
|
+
row.find('.device-select').html(getDeviceOptions(brandId, ''));
|
|
196
|
+
row.find('.rs485-ch-wrap').empty();
|
|
197
|
+
mappings[idx].brand = brandId;
|
|
198
|
+
mappings[idx].device = '';
|
|
199
|
+
mappings[idx].rs485Channel = 1;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
container.find('.device-select').off('change').on('change', function() {
|
|
203
|
+
var row = $(this).closest('.mapping-row');
|
|
204
|
+
var idx = row.data('idx');
|
|
205
|
+
var deviceId = $(this).val();
|
|
206
|
+
var brandId = row.find('.brand-select').val();
|
|
207
|
+
mappings[idx].device = deviceId;
|
|
208
|
+
// 更新RS485按键选择
|
|
209
|
+
row.find('.rs485-ch-wrap').html(getRS485ChannelOptions(brandId, deviceId, 1));
|
|
210
|
+
mappings[idx].rs485Channel = 1;
|
|
211
|
+
bindEvents(); // 重新绑定新添加的元素事件
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
container.find('.rs485-channel').off('change').on('change', function() {
|
|
215
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
216
|
+
mappings[idx].rs485Channel = parseInt($(this).val()) || 1;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
container.find('.mesh-select').off('change').on('change', function() {
|
|
220
|
+
var row = $(this).closest('.mapping-row');
|
|
221
|
+
var idx = row.data('idx');
|
|
222
|
+
var mac = $(this).val();
|
|
223
|
+
mappings[idx].meshMac = mac || '';
|
|
224
|
+
mappings[idx].meshChannel = 1;
|
|
225
|
+
// 更新Mesh按键选择器
|
|
226
|
+
row.find('.mesh-ch-wrap').html(getMeshChannelOptions(mac, 1));
|
|
227
|
+
bindEvents(); // 重新绑定新元素事件
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
container.find('.mesh-channel').off('change').on('change', function() {
|
|
231
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
232
|
+
mappings[idx].meshChannel = parseInt($(this).val()) || 1;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
container.find('.addr-input').off('change').on('change', function() {
|
|
236
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
237
|
+
mappings[idx].address = parseInt($(this).val()) || 1;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
container.find('.btn-remove').off('click').on('click', function() {
|
|
241
|
+
var idx = $(this).closest('.mapping-row').data('idx');
|
|
242
|
+
mappings.splice(idx, 1);
|
|
243
|
+
renderMappings();
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 添加新映射
|
|
248
|
+
$('#btn-add-mapping').on('click', function() {
|
|
249
|
+
mappings.push({ meshMac: '', meshChannel: 0, brand: '', device: '', address: 1, rs485Channel: 1 });
|
|
250
|
+
renderMappings();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 初始渲染
|
|
254
|
+
renderMappings();
|
|
255
|
+
},
|
|
256
|
+
oneditsave: function() {
|
|
257
|
+
var mappings = [];
|
|
258
|
+
$('#mapping-list .mapping-row').each(function() {
|
|
259
|
+
mappings.push({
|
|
260
|
+
meshMac: $(this).find('.mesh-select').val() || '',
|
|
261
|
+
meshChannel: parseInt($(this).find('.mesh-channel').val()) || 1,
|
|
262
|
+
brand: $(this).find('.brand-select').val() || '',
|
|
263
|
+
device: $(this).find('.device-select').val() || '',
|
|
264
|
+
address: parseInt($(this).find('.addr-input').val()) || 1,
|
|
265
|
+
rs485Channel: parseInt($(this).find('.rs485-channel').val()) || 1
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
this.mappings = JSON.stringify(mappings);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
</script>
|
|
272
|
+
|
|
273
|
+
<script type="text/html" data-template-name="symi-rs485-bridge">
|
|
274
|
+
<style>
|
|
275
|
+
.bridge-section { margin: 12px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; background: #fafafa; }
|
|
276
|
+
.bridge-section h4 { margin: 0 0 10px 0; padding-bottom: 6px; border-bottom: 1px solid #eee; color: #333; font-size: 13px; }
|
|
277
|
+
.bridge-section h4 i { margin-right: 6px; color: #666; }
|
|
278
|
+
|
|
279
|
+
#mapping-list { max-height: calc(100vh - 380px); min-height: 200px; overflow-y: auto; }
|
|
280
|
+
.mapping-empty { padding: 15px; text-align: center; color: #999; font-size: 12px; }
|
|
281
|
+
.mapping-row { display: flex; align-items: center; padding: 6px 8px; margin-bottom: 6px; background: #fff; border: 1px solid #e0e0e0; border-radius: 4px; gap: 6px; }
|
|
282
|
+
.mesh-col { flex: 0 0 25%; display: flex; gap: 4px; }
|
|
283
|
+
.mesh-col .mesh-select { flex: 1; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #e8f5e9; font-size: 12px; }
|
|
284
|
+
.mesh-col .mesh-channel { width: 58px; padding: 4px; border: 1px solid #81c784; border-radius: 3px; background: #c8e6c9; font-size: 11px; font-weight: bold; }
|
|
285
|
+
.arrow-col { flex: 0 0 20px; text-align: center; color: #999; }
|
|
286
|
+
.brand-col { flex: 0 0 14%; }
|
|
287
|
+
.brand-col select { width: 100%; padding: 4px; border: 1px solid #64b5f6; border-radius: 3px; background: #e3f2fd; font-size: 12px; }
|
|
288
|
+
.device-col { flex: 0 0 28%; display: flex; gap: 4px; }
|
|
289
|
+
.device-col select { padding: 4px; border: 1px solid #64b5f6; border-radius: 3px; background: #e3f2fd; font-size: 12px; }
|
|
290
|
+
.device-col .device-select { flex: 1; }
|
|
291
|
+
.device-col .rs485-channel { width: 58px; background: #bbdefb; font-size: 11px; font-weight: bold; }
|
|
292
|
+
.addr-col { flex: 0 0 50px; }
|
|
293
|
+
.addr-col input { width: 100%; padding: 4px; border: 1px solid #ccc; border-radius: 3px; text-align: center; font-size: 12px; }
|
|
294
|
+
.del-col { flex: 0 0 32px; text-align: center; }
|
|
295
|
+
.btn-remove { color: #d32f2f !important; padding: 2px 6px !important; }
|
|
296
|
+
.form-tips { font-size: 11px; }
|
|
297
|
+
.form-tips p { margin: 3px 0; }
|
|
298
|
+
</style>
|
|
299
|
+
|
|
300
|
+
<div class="form-row">
|
|
301
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
302
|
+
<input type="text" id="node-input-name" placeholder="如:客厅设备同步">
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div class="bridge-section">
|
|
306
|
+
<h4><i class="fa fa-cog"></i> 基本配置</h4>
|
|
307
|
+
<div class="form-row">
|
|
308
|
+
<label for="node-input-gateway"><i class="fa fa-server"></i> Mesh网关</label>
|
|
309
|
+
<input type="text" id="node-input-gateway">
|
|
310
|
+
</div>
|
|
311
|
+
<div class="form-row">
|
|
312
|
+
<label for="node-input-rs485Config"><i class="fa fa-plug"></i> RS485连接</label>
|
|
313
|
+
<input type="text" id="node-input-rs485Config">
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="bridge-section">
|
|
318
|
+
<h4><i class="fa fa-exchange"></i> 实体映射(Mesh ↔ RS485)</h4>
|
|
319
|
+
<div style="display:flex; padding:4px 8px; font-size:11px; color:#666; border-bottom:1px solid #eee; margin-bottom:6px; gap:6px;">
|
|
320
|
+
<span style="flex:0 0 24%">Mesh实体/按键</span>
|
|
321
|
+
<span style="flex:0 0 20px"></span>
|
|
322
|
+
<span style="flex:0 0 14%">品牌</span>
|
|
323
|
+
<span style="flex:0 0 28%">类型/按键</span>
|
|
324
|
+
<span style="flex:0 0 50px">地址</span>
|
|
325
|
+
</div>
|
|
326
|
+
<div id="mapping-list"></div>
|
|
327
|
+
<button type="button" id="btn-add-mapping" class="red-ui-button" style="margin-top:8px; width:100%">
|
|
328
|
+
<i class="fa fa-plus"></i> 添加映射
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div class="form-tips" style="margin-top: 10px;">
|
|
333
|
+
<p><b>提示:</b>开关设备需要分别选择Mesh和RS485的按键,可自由配置对应关系(如Mesh第3路 ↔ RS485第1路)</p>
|
|
334
|
+
</div>
|
|
335
|
+
</script>
|
|
336
|
+
|
|
337
|
+
<script type="text/html" data-help-name="symi-rs485-bridge">
|
|
338
|
+
<p>Mesh与RS485设备双向同步桥接节点</p>
|
|
339
|
+
|
|
340
|
+
<h3>功能特点</h3>
|
|
341
|
+
<ul>
|
|
342
|
+
<li>单节点管理多个实体映射</li>
|
|
343
|
+
<li>事件驱动同步(非轮询)</li>
|
|
344
|
+
<li>命令队列顺序处理</li>
|
|
345
|
+
<li>防循环保护机制</li>
|
|
346
|
+
<li>断电重启自动恢复</li>
|
|
347
|
+
<li>支持多个桥接节点共享同一Mesh网关</li>
|
|
348
|
+
<li>输出端口可连接debug节点查看通信数据</li>
|
|
349
|
+
</ul>
|
|
350
|
+
|
|
351
|
+
<h3>配置说明</h3>
|
|
352
|
+
<dl>
|
|
353
|
+
<dt>Mesh网关</dt><dd>选择Symi Mesh网关节点(可多个桥接节点共享)</dd>
|
|
354
|
+
<dt>RS485连接</dt><dd>选择RS485连接配置(不同TCP端口独立配置)</dd>
|
|
355
|
+
<dt>实体映射</dt><dd>配置Mesh与RS485实体的对应关系</dd>
|
|
356
|
+
</dl>
|
|
357
|
+
|
|
358
|
+
<h3>输入</h3>
|
|
359
|
+
<p>支持手动发送Modbus帧或触发同步:</p>
|
|
360
|
+
<pre>msg.payload = { hex: "01 06 10 31 00 01" } // 发送十六进制帧</pre>
|
|
361
|
+
<pre>msg.payload = { sync: "mesh-to-rs485", mac: "xxx", state: {...} }</pre>
|
|
362
|
+
|
|
363
|
+
<h3>输出</h3>
|
|
364
|
+
<p>输出RS485通信数据供调试:</p>
|
|
365
|
+
<dl>
|
|
366
|
+
<dt>topic</dt><dd>rs485-bridge/rx 或 rs485-bridge/tx</dd>
|
|
367
|
+
<dt>payload</dt><dd>包含方向、从机地址、功能码、十六进制数据</dd>
|
|
368
|
+
</dl>
|
|
369
|
+
|
|
370
|
+
<h3>同步规则</h3>
|
|
371
|
+
<ul>
|
|
372
|
+
<li>只同步双方都支持的功能点</li>
|
|
373
|
+
<li>开关协议:按键0x1031-0x1036,指示灯0x1021-0x1026</li>
|
|
374
|
+
<li>开关设备需指定具体按键</li>
|
|
375
|
+
</ul>
|
|
376
|
+
</script>
|