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
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
170
|
+
<select id="node-input-masterNode" style="width: 55%;">
|
|
152
171
|
<option value="">请选择主站节点</option>
|
|
153
172
|
</select>
|
|
154
|
-
<
|
|
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:
|
|
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;">
|
package/nodes/homekit-bridge.js
CHANGED
|
@@ -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
|
+
|