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.
@@ -51,6 +51,11 @@
51
51
  renderRelayNameConfig();
52
52
  });
53
53
 
54
+ // 添加刷新按钮
55
+ $("#btn-refresh-homekit").on("click", function() {
56
+ renderRelayNameConfig();
57
+ });
58
+
54
59
  // 初始渲染
55
60
  renderRelayNameConfig();
56
61
 
@@ -69,21 +74,30 @@
69
74
  var masterNodeId = $("#node-input-masterNode").val();
70
75
  var container = $("#relay-names-container");
71
76
  container.empty();
72
-
77
+
73
78
  if (!masterNodeId) {
74
79
  container.html('<div style="padding: 20px; text-align: center; color: #999;">请先选择主站节点</div>');
75
80
  return;
76
81
  }
77
-
78
- // 获取主站节点配置
79
- var masterNode = RED.nodes.node(masterNodeId);
80
- if (!masterNode || !masterNode.slaves || masterNode.slaves.length === 0) {
81
- container.html('<div style="padding: 20px; text-align: center; color: #999;">主站节点未配置从站</div>');
82
- return;
83
- }
84
-
85
- // 遍历所有从站和线圈
86
- masterNode.slaves.forEach(function(slave) {
82
+
83
+ // 显示加载中
84
+ container.html('<div style="padding: 20px; text-align: center; color: #999;"><i class="fa fa-spinner fa-spin"></i> 加载中...</div>');
85
+
86
+ // 通过HTTP API获取主站节点的最新配置
87
+ $.ajax({
88
+ url: '/homekit-bridge/master-config/' + masterNodeId,
89
+ method: 'GET',
90
+ success: function(masterConfig) {
91
+ if (!masterConfig || !masterConfig.slaves || masterConfig.slaves.length === 0) {
92
+ container.html('<div style="padding: 20px; text-align: center; color: #999;">主站节点未配置从站</div>');
93
+ return;
94
+ }
95
+
96
+ // 清空容器
97
+ container.empty();
98
+
99
+ // 遍历所有从站和线圈
100
+ masterConfig.slaves.forEach(function(slave) {
87
101
  var slaveSection = $('<div class="slave-section" style="margin-bottom: 20px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; background: #f9f9f9;">');
88
102
 
89
103
  slaveSection.html(`
@@ -115,14 +129,19 @@
115
129
  container.append(slaveSection);
116
130
  });
117
131
 
118
- // 绑定输入事件
119
- $(".relay-name-input").on("input", function() {
120
- var key = $(this).data("key");
121
- var value = $(this).val().trim();
122
- if (value) {
123
- node.relayNames[key] = value;
124
- } else {
125
- delete node.relayNames[key];
132
+ // 绑定输入事件
133
+ $(".relay-name-input").on("input", function() {
134
+ var key = $(this).data("key");
135
+ var value = $(this).val().trim();
136
+ if (value) {
137
+ node.relayNames[key] = value;
138
+ } else {
139
+ delete node.relayNames[key];
140
+ }
141
+ });
142
+ },
143
+ error: function(xhr, status, error) {
144
+ container.html('<div style="padding: 20px; text-align: center; color: #f44336;"><i class="fa fa-exclamation-triangle"></i> 加载失败: ' + error + '</div>');
126
145
  }
127
146
  });
128
147
  }
@@ -148,10 +167,13 @@
148
167
 
149
168
  <div class="form-row">
150
169
  <label for="node-input-masterNode"><i class="fa fa-microchip"></i> 主站节点</label>
151
- <select id="node-input-masterNode" style="width: 70%;">
170
+ <select id="node-input-masterNode" style="width: 55%;">
152
171
  <option value="">请选择主站节点</option>
153
172
  </select>
154
- <div style="font-size: 11px; color: #999; margin-top: 5px;">选择要桥接的Modbus主站节点</div>
173
+ <button type="button" id="btn-refresh-homekit" class="red-ui-button" style="margin-left: 5px;">
174
+ <i class="fa fa-refresh"></i> 刷新
175
+ </button>
176
+ <div style="font-size: 11px; color: #999; margin-top: 5px;">选择要桥接的Modbus主站节点,点击刷新按钮更新显示</div>
155
177
  </div>
156
178
 
157
179
  <hr style="margin: 20px 0; border: none; border-top: 1px solid #e0e0e0;">
@@ -186,7 +208,7 @@
186
208
  <label style="width: 100%; margin-bottom: 10px;">
187
209
  <i class="fa fa-list"></i> 继电器名称配置
188
210
  </label>
189
- <div id="relay-names-container" style="width: 100%; max-height: 400px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; background: white;">
211
+ <div id="relay-names-container" style="width: 100%; max-height: 600px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; background: white;">
190
212
  <div style="padding: 20px; text-align: center; color: #999;">请先选择主站节点</div>
191
213
  </div>
192
214
  <div style="font-size: 11px; color: #999; margin-top: 5px;">
@@ -324,5 +324,23 @@ module.exports = function(RED) {
324
324
  }
325
325
 
326
326
  RED.nodes.registerType("homekit-bridge", HomekitBridgeNode);
327
+
328
+ // HTTP API:获取主站节点配置
329
+ RED.httpAdmin.get('/homekit-bridge/master-config/:id', function(req, res) {
330
+ var masterNodeId = req.params.id;
331
+ var masterNode = RED.nodes.getNode(masterNodeId);
332
+
333
+ if (!masterNode) {
334
+ res.status(404).json({error: '主站节点不存在'});
335
+ return;
336
+ }
337
+
338
+ // 返回主站配置(包括最新的从站列表)
339
+ // 从站列表存储在 node.config.slaves 中
340
+ res.json({
341
+ slaves: (masterNode.config && masterNode.config.slaves) ? masterNode.config.slaves : [],
342
+ relayNames: masterNode.relayNames || {}
343
+ });
344
+ });
327
345
  };
328
346
 
@@ -0,0 +1,286 @@
1
+ // Symi蓝牙Mesh网关协议处理模块
2
+ // 协议版本: V1.3.1
3
+ // 支持TCP/IP和串口通信
4
+
5
+ module.exports = function(RED) {
6
+ 'use strict';
7
+
8
+ // 协议常量
9
+ const PROTOCOL = {
10
+ HEADER: 0x53,
11
+ // 操作码
12
+ OP_GET_DEVICE_LIST: 0x12,
13
+ OP_DEVICE_CONTROL: 0x30,
14
+ OP_QUERY_STATUS: 0x32,
15
+ OP_SCENE_CONTROL: 0x34,
16
+ OP_STATUS_EVENT: 0x80,
17
+ OP_DEVICE_LIST_RESPONSE: 0x92,
18
+ OP_CONTROL_RESPONSE: 0xB0,
19
+ OP_SCENE_RESPONSE: 0xB4,
20
+ // 消息类型
21
+ MSG_TYPE_SWITCH: 0x02, // 开关状态(1-4路)
22
+ MSG_TYPE_DIMMER: 0x03, // 调光状态
23
+ MSG_TYPE_RGB: 0x04, // 五色调光
24
+ MSG_TYPE_CURTAIN: 0x05, // 窗帘动作
25
+ MSG_TYPE_CURTAIN_POS: 0x06, // 窗帘位置
26
+ MSG_TYPE_THERMOSTAT: 0x07, // 温控器
27
+ MSG_TYPE_SWITCH_6: 0x45, // 6路开关状态
28
+ // 设备类型
29
+ DEVICE_TYPE_SWITCH: 0x01,
30
+ DEVICE_TYPE_DIMMER: 0x02,
31
+ DEVICE_TYPE_DUAL_COLOR: 0x04,
32
+ DEVICE_TYPE_CURTAIN: 0x05,
33
+ DEVICE_TYPE_CARD_POWER: 0x09,
34
+ DEVICE_TYPE_THERMOSTAT: 0x0A,
35
+ DEVICE_TYPE_PIR: 0x0C,
36
+ DEVICE_TYPE_RGB: 0x18,
37
+ DEVICE_TYPE_THERMOSTAT_3IN1: 0x94
38
+ };
39
+
40
+ // 计算异或校验和
41
+ function calculateChecksum(buffer) {
42
+ let checksum = 0;
43
+ for (let i = 0; i < buffer.length; i++) {
44
+ checksum ^= buffer[i];
45
+ }
46
+ return checksum;
47
+ }
48
+
49
+ // 构建获取设备列表请求帧
50
+ function buildGetDeviceListFrame() {
51
+ const buffer = Buffer.from([
52
+ PROTOCOL.HEADER, // 0x53
53
+ PROTOCOL.OP_GET_DEVICE_LIST, // 0x12
54
+ 0x00 // Length = 0
55
+ ]);
56
+ const checksum = calculateChecksum(buffer);
57
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
58
+ }
59
+
60
+ // 解析设备列表响应
61
+ function parseDeviceListResponse(frames) {
62
+ const devices = [];
63
+
64
+ for (let i = 0; i < frames.length; i++) {
65
+ const frame = frames[i];
66
+
67
+ // 验证帧头
68
+ if (frame[0] !== PROTOCOL.HEADER || frame[1] !== PROTOCOL.OP_DEVICE_LIST_RESPONSE) {
69
+ continue;
70
+ }
71
+
72
+ const sequence = frame[2];
73
+ const length = frame[3];
74
+
75
+ if (sequence === 0x00) {
76
+ // 第一帧:设备总数
77
+ const totalDevices = frame[4];
78
+ continue;
79
+ }
80
+
81
+ // 解析设备数据
82
+ let offset = 4;
83
+ while (offset + 10 <= frame.length - 1) {
84
+ const shortAddr = frame[offset] | (frame[offset + 1] << 8); // 小端序
85
+ const deviceType = frame[offset + 2];
86
+ const mac = Buffer.from([
87
+ frame[offset + 3],
88
+ frame[offset + 4],
89
+ frame[offset + 5],
90
+ frame[offset + 6],
91
+ frame[offset + 7],
92
+ frame[offset + 8]
93
+ ]);
94
+ const buttons = frame[offset + 9];
95
+
96
+ devices.push({
97
+ shortAddr: shortAddr,
98
+ type: deviceType,
99
+ mac: mac.toString('hex').toUpperCase().match(/.{2}/g).join(':'),
100
+ buttons: buttons
101
+ });
102
+
103
+ offset += 10;
104
+ }
105
+ }
106
+
107
+ return devices;
108
+ }
109
+
110
+ // 构建开关控制帧(带当前状态,保持其他路不变)
111
+ function buildSwitchControlFrame(shortAddr, buttonNumber, totalButtons, state, currentStates) {
112
+ let stateValue;
113
+
114
+ if (totalButtons === 1) {
115
+ // 单路开关
116
+ stateValue = state ? 0x02 : 0x01;
117
+ const buffer = Buffer.from([
118
+ PROTOCOL.HEADER,
119
+ PROTOCOL.OP_DEVICE_CONTROL,
120
+ 0x05, // Length
121
+ shortAddr & 0xFF,
122
+ (shortAddr >> 8) & 0xFF,
123
+ 0x00, // ACK = 0
124
+ 0x05, // 重传5次
125
+ PROTOCOL.MSG_TYPE_SWITCH,
126
+ stateValue
127
+ ]);
128
+ const checksum = calculateChecksum(buffer);
129
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
130
+ } else if (totalButtons >= 2 && totalButtons <= 4) {
131
+ // 2-4路开关:使用当前状态构建完整状态值
132
+ if (currentStates && currentStates.length === totalButtons) {
133
+ stateValue = buildMultiSwitchState(currentStates, buttonNumber, state);
134
+ } else {
135
+ // 如果没有当前状态,只设置目标按钮,其他位设为00(保持不变)
136
+ const bitPos = (buttonNumber - 1) * 2;
137
+ stateValue = (state ? 0x02 : 0x01) << bitPos;
138
+ }
139
+
140
+ const buffer = Buffer.from([
141
+ PROTOCOL.HEADER,
142
+ PROTOCOL.OP_DEVICE_CONTROL,
143
+ 0x05, // Length
144
+ shortAddr & 0xFF,
145
+ (shortAddr >> 8) & 0xFF,
146
+ 0x00, // ACK = 0
147
+ 0x05, // 重传5次
148
+ PROTOCOL.MSG_TYPE_SWITCH,
149
+ stateValue
150
+ ]);
151
+ const checksum = calculateChecksum(buffer);
152
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
153
+ } else if (totalButtons === 6) {
154
+ // 6路开关:2字节状态值(小端序)
155
+ let stateValue16;
156
+ if (currentStates && currentStates.length === totalButtons) {
157
+ stateValue16 = buildMultiSwitchState(currentStates, buttonNumber, state);
158
+ } else {
159
+ // 如果没有当前状态,只设置目标按钮,其他位设为00(保持不变)
160
+ const bitPos = (buttonNumber - 1) * 2;
161
+ stateValue16 = (state ? 0x02 : 0x01) << bitPos;
162
+ }
163
+
164
+ const buffer = Buffer.from([
165
+ PROTOCOL.HEADER,
166
+ PROTOCOL.OP_DEVICE_CONTROL,
167
+ 0x06, // Length
168
+ shortAddr & 0xFF,
169
+ (shortAddr >> 8) & 0xFF,
170
+ 0x00, // ACK = 0
171
+ 0x05, // 重传5次
172
+ PROTOCOL.MSG_TYPE_SWITCH,
173
+ stateValue16 & 0xFF,
174
+ (stateValue16 >> 8) & 0xFF
175
+ ]);
176
+ const checksum = calculateChecksum(buffer);
177
+ return Buffer.concat([buffer, Buffer.from([checksum])]);
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ // 解析状态事件帧
184
+ function parseStatusEvent(frame) {
185
+ // 验证帧头
186
+ if (frame[0] !== PROTOCOL.HEADER || frame[1] !== PROTOCOL.OP_STATUS_EVENT) {
187
+ return null;
188
+ }
189
+
190
+ const subOp = frame[2];
191
+ const length = frame[3];
192
+ const shortAddr = frame[4] | (frame[5] << 8); // 小端序
193
+ const msgType = frame[6];
194
+
195
+ const event = {
196
+ subOp: subOp,
197
+ shortAddr: shortAddr,
198
+ msgType: msgType
199
+ };
200
+
201
+ // 根据消息类型解析状态
202
+ if (msgType === PROTOCOL.MSG_TYPE_SWITCH) {
203
+ // 1-4路开关状态
204
+ const stateValue = frame[7];
205
+ event.states = parseMultiSwitchState(stateValue, 4);
206
+ } else if (msgType === PROTOCOL.MSG_TYPE_SWITCH_6) {
207
+ // 6路开关状态
208
+ const stateLow = frame[7];
209
+ const stateHigh = frame[8];
210
+ const stateValue = stateLow | (stateHigh << 8);
211
+ event.states = parseMultiSwitchState(stateValue, 6);
212
+ } else if (msgType === PROTOCOL.MSG_TYPE_DIMMER) {
213
+ // 调光灯状态
214
+ event.brightness = frame[7];
215
+ event.colorTemp = frame[8];
216
+ } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN) {
217
+ // 窗帘动作状态
218
+ event.action = frame[7]; // 0=停止, 1=打开中, 2=关闭中
219
+ } else if (msgType === PROTOCOL.MSG_TYPE_CURTAIN_POS) {
220
+ // 窗帘位置状态
221
+ event.position = frame[7]; // 0-100%
222
+ }
223
+
224
+ return event;
225
+ }
226
+
227
+ // 解析多路开关状态值
228
+ function parseMultiSwitchState(stateValue, totalButtons) {
229
+ const states = [];
230
+
231
+ for (let i = 0; i < totalButtons; i++) {
232
+ const bitPos = i * 2;
233
+ const bits = (stateValue >> bitPos) & 0x03;
234
+
235
+ if (bits === 0x01) {
236
+ states.push(false); // 关
237
+ } else if (bits === 0x02) {
238
+ states.push(true); // 开
239
+ } else {
240
+ states.push(null); // 保持不变或未知
241
+ }
242
+ }
243
+
244
+ return states;
245
+ }
246
+
247
+ // 构建多路开关状态值(用于控制时保持其他路不变)
248
+ function buildMultiSwitchState(currentStates, buttonNumber, newState) {
249
+ let stateValue = 0;
250
+
251
+ for (let i = 0; i < currentStates.length; i++) {
252
+ const bitPos = i * 2;
253
+ let bits;
254
+
255
+ if (i === buttonNumber - 1) {
256
+ // 要改变的按钮
257
+ bits = newState ? 0x02 : 0x01;
258
+ } else {
259
+ // 保持不变的按钮
260
+ if (currentStates[i] === true) {
261
+ bits = 0x02;
262
+ } else if (currentStates[i] === false) {
263
+ bits = 0x01;
264
+ } else {
265
+ bits = 0x00; // 保持不变
266
+ }
267
+ }
268
+
269
+ stateValue |= (bits << bitPos);
270
+ }
271
+
272
+ return stateValue;
273
+ }
274
+
275
+ return {
276
+ PROTOCOL,
277
+ calculateChecksum,
278
+ buildGetDeviceListFrame,
279
+ parseDeviceListResponse,
280
+ buildSwitchControlFrame,
281
+ parseStatusEvent,
282
+ parseMultiSwitchState,
283
+ buildMultiSwitchState
284
+ };
285
+ };
286
+