node-red-contrib-symi-modbus 2.9.6 → 2.9.7
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 +88 -1
- package/nodes/custom-protocol.html +1 -0
- package/nodes/homekit-bridge.html +1 -0
- package/nodes/modbus-dashboard.html +1 -0
- package/nodes/modbus-debug.html +1 -0
- package/nodes/modbus-master.html +1 -0
- package/nodes/modbus-master.js +2 -1
- package/nodes/modbus-server-config.html +41 -48
- package/nodes/modbus-slave-switch.html +1 -0
- package/nodes/relay-output.html +342 -0
- package/nodes/relay-output.js +233 -0
- package/nodes/serial-port-config.html +4 -1
- package/nodes/serial-port-config.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -816,6 +816,70 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
816
816
|
- 测试功能需先选择串口配置
|
|
817
817
|
- 无需连线到debug节点,直接通过串口配置节点发送数据
|
|
818
818
|
|
|
819
|
+
### 继电器输出节点(relay-output)
|
|
820
|
+
|
|
821
|
+
用于控制Modbus继电器,支持**绑定从站开关触发源**或接收外部输入,**无需连线到主站**。
|
|
822
|
+
|
|
823
|
+
**核心特性**:
|
|
824
|
+
- 从站开关、继电器输出、主站之间**完全无需连线**
|
|
825
|
+
- 共享RS-485连接配置,支持多个节点共用
|
|
826
|
+
- 支持绑定从站开关作为触发源(内部事件通信)
|
|
827
|
+
- 支持外部设备触发(如海康门禁)
|
|
828
|
+
- 支持延时执行(0-60000毫秒),实现级联开灯效果
|
|
829
|
+
- 配置持久化保存,断电断网恢复后正常工作
|
|
830
|
+
|
|
831
|
+
**配置参数**:
|
|
832
|
+
|
|
833
|
+
*RS-485连接配置*:
|
|
834
|
+
- **连接配置**:选择已配置的RS-485连接(支持TCP网关或串口),多个节点共享
|
|
835
|
+
|
|
836
|
+
*按键触发配置*:
|
|
837
|
+
- **面板品牌**:开关面板品牌,目前支持亖米(Symi)
|
|
838
|
+
- **按钮类型**:开关按钮/场景按钮/输入端触发(外部设备)
|
|
839
|
+
- **开关ID**:物理面板地址(0-255)
|
|
840
|
+
- **按钮编号**:按键编号(1-8,15=背光灯)
|
|
841
|
+
- **门禁ID**:可选,0=不过滤,>0=只响应指定门禁编号
|
|
842
|
+
|
|
843
|
+
*目标继电器配置*:
|
|
844
|
+
- **从站地址**:要控制的Modbus从站地址(1-247)
|
|
845
|
+
- **继电器路数**:要控制的继电器通道(1-32)
|
|
846
|
+
|
|
847
|
+
*动作配置*:
|
|
848
|
+
- **动作**:打开/关闭/跟随输入/翻转状态
|
|
849
|
+
- **延时**:执行延时(0-60000毫秒),用于实现级联开灯效果
|
|
850
|
+
- **忽略释放信号**:只响应按下信号
|
|
851
|
+
|
|
852
|
+
**使用示例**:
|
|
853
|
+
|
|
854
|
+
**示例1:按键触发继电器(无需任何连线)**
|
|
855
|
+
```
|
|
856
|
+
配置:开关ID=1,按钮编号=3,从站地址=10,继电器路数=1,动作=打开
|
|
857
|
+
效果:当开关1的按钮3被按下时,自动控制从站10的1路继电器打开
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**示例2:一个按键控制多路继电器(级联开灯)**
|
|
861
|
+
```
|
|
862
|
+
继电器输出1: 开关ID=1, 按钮=3, 继电器=1路, 延时=0ms, 动作=打开
|
|
863
|
+
继电器输出2: 开关ID=1, 按钮=3, 继电器=2路, 延时=500ms, 动作=打开
|
|
864
|
+
继电器输出3: 开关ID=1, 按钮=3, 继电器=3路, 延时=1000ms, 动作=打开
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**示例3:海康门禁触发(使用输入端)**
|
|
868
|
+
```
|
|
869
|
+
海康门禁事件 → 继电器输出(按钮类型=输入端触发, 从站地址=10, 继电器=1路, 门禁ID=4)
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
**工作原理**:
|
|
873
|
+
- 节点监听 `modbus:buttonPressed` 事件(按键触发)
|
|
874
|
+
- 通过 `modbus:writeCoil` 事件发送控制命令到主站
|
|
875
|
+
- 实现按键 → 继电器输出 → 主站之间的无连线通信
|
|
876
|
+
|
|
877
|
+
**稳定性保障**:
|
|
878
|
+
- 长期运行不卡顿、不死机、内存不溢出
|
|
879
|
+
- 无调试数据输出,具备生产条件长期运行
|
|
880
|
+
- 断电断网恢复后正常工作
|
|
881
|
+
- 内置防抖和状态检测机制,不会出现设备反复开关进入死循环
|
|
882
|
+
|
|
819
883
|
## 输出消息格式
|
|
820
884
|
|
|
821
885
|
### 主站节点
|
|
@@ -888,7 +952,30 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
888
952
|
|
|
889
953
|
## 版本信息
|
|
890
954
|
|
|
891
|
-
**当前版本**: v2.9.
|
|
955
|
+
**当前版本**: v2.9.7 (2025-12-24)
|
|
956
|
+
|
|
957
|
+
**v2.9.7 更新内容**:
|
|
958
|
+
- **新增节点**:继电器输出节点(relay-output)
|
|
959
|
+
- 支持**绑定从站开关按键**,实现按键→继电器→主站之间**完全无需连线**
|
|
960
|
+
- 共享RS-485连接配置,支持多个节点共用同一连接
|
|
961
|
+
- 按键触发配置:面板品牌、按钮类型、开关ID(0-255)、按钮编号(1-8)、门禁ID(可选)
|
|
962
|
+
- 目标配置:从站地址、继电器路数、动作(打开/关闭/跟随/翻转)、延时(0-60000ms)
|
|
963
|
+
- 支持延时执行,一个按键控制多路继电器实现级联开灯
|
|
964
|
+
- 配置持久化保存,断电断网恢复后正常工作
|
|
965
|
+
- 长期稳定运行,不卡顿、不死机、内存不溢出,无调试数据输出
|
|
966
|
+
- **界面优化**:所有SYMI MODBUS节点左侧名称改为中文显示
|
|
967
|
+
- **重要修复**:HassOS串口兼容性问题
|
|
968
|
+
- 修复"Cannot lock port"错误:在所有串口连接中添加`lock: false`参数
|
|
969
|
+
- 修复位置:`serial-port-config.js`和`modbus-master.js`的串口连接配置
|
|
970
|
+
- 适用场景:HassOS、Docker容器、以及其他不支持串口锁定的环境
|
|
971
|
+
- **功能改进**:串口配置界面优化
|
|
972
|
+
- Modbus服务器配置:串口路径改为输入框+搜索按钮,支持手动输入(搜索不到也能使用)
|
|
973
|
+
- 显示完整串口参数:数据位(7/8)、停止位(1/2)、校验位(无/偶/奇)
|
|
974
|
+
- 添加更多波特率选项:1200/2400/4800/9600/19200/38400/57600/115200
|
|
975
|
+
- **稳定性保障**:
|
|
976
|
+
- 串口配置持久化保存到Node-RED配置文件
|
|
977
|
+
- 断电断网恢复后自动重连,正常工作
|
|
978
|
+
- 无调试数据输出,适合生产环境长期运行
|
|
892
979
|
|
|
893
980
|
**v2.9.6 更新内容**:
|
|
894
981
|
- **重要修复**:485开关场景按钮的CRC校验兼容性问题
|
package/nodes/modbus-debug.html
CHANGED
package/nodes/modbus-master.html
CHANGED
package/nodes/modbus-master.js
CHANGED
|
@@ -275,7 +275,8 @@ module.exports = function(RED) {
|
|
|
275
275
|
baudRate: node.config.serialBaudRate || 9600,
|
|
276
276
|
dataBits: node.config.serialDataBits || 8,
|
|
277
277
|
stopBits: node.config.serialStopBits || 1,
|
|
278
|
-
parity: node.config.serialParity || 'none'
|
|
278
|
+
parity: node.config.serialParity || 'none',
|
|
279
|
+
lock: false // 禁用串口锁定,解决HassOS中"Cannot lock port"错误
|
|
279
280
|
});
|
|
280
281
|
|
|
281
282
|
node.log(`串口Modbus连接成功: ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
@@ -37,57 +37,46 @@
|
|
|
37
37
|
|
|
38
38
|
$("#node-config-input-connectionType").trigger("change");
|
|
39
39
|
|
|
40
|
-
//
|
|
40
|
+
// 串口搜索按钮
|
|
41
41
|
$("#node-config-refresh-serial").on("click", function() {
|
|
42
42
|
var btn = $(this);
|
|
43
|
-
btn.prop("disabled", true).html('<i class="fa fa-spinner fa-spin"></i>
|
|
44
|
-
|
|
45
|
-
$.getJSON('modbus-master/serialports', function(data) {
|
|
46
|
-
var select = $("#node-config-input-serialPort");
|
|
47
|
-
var currentVal = select.val();
|
|
48
|
-
|
|
49
|
-
// 清空并重新填充
|
|
50
|
-
select.empty();
|
|
43
|
+
btn.prop("disabled", true).html('<i class="fa fa-spinner fa-spin"></i>');
|
|
51
44
|
|
|
45
|
+
$.getJSON('serial-ports', function(data) {
|
|
52
46
|
if (data && data.length > 0) {
|
|
47
|
+
var select = $('<select id="serial-port-select" style="width:100%; margin-top:10px;"></select>');
|
|
53
48
|
data.forEach(function(port) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.text(port.comName + ' - ' + port.manufacturer));
|
|
59
|
-
} else {
|
|
60
|
-
var label = port.comName;
|
|
61
|
-
if (port.manufacturer && port.manufacturer !== '未知设备') {
|
|
62
|
-
label += ' (' + port.manufacturer + ')';
|
|
63
|
-
}
|
|
64
|
-
select.append($('<option></option>')
|
|
65
|
-
.attr('value', port.comName)
|
|
66
|
-
.text(label));
|
|
49
|
+
var path = port.path || port.comName;
|
|
50
|
+
var label = path;
|
|
51
|
+
if (port.manufacturer) {
|
|
52
|
+
label += ' (' + port.manufacturer + ')';
|
|
67
53
|
}
|
|
54
|
+
select.append('<option value="' + path + '">' + label + '</option>');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 显示选择框
|
|
58
|
+
$('#serial-port-list').html(select);
|
|
59
|
+
|
|
60
|
+
// 监听选择
|
|
61
|
+
select.on('change', function() {
|
|
62
|
+
$('#node-config-input-serialPort').val($(this).val());
|
|
68
63
|
});
|
|
69
64
|
|
|
70
|
-
//
|
|
71
|
-
if (
|
|
72
|
-
|
|
65
|
+
// 自动填充第一个
|
|
66
|
+
if (data.length > 0) {
|
|
67
|
+
var firstPath = data[0].path || data[0].comName;
|
|
68
|
+
$('#node-config-input-serialPort').val(firstPath);
|
|
73
69
|
}
|
|
74
70
|
} else {
|
|
75
|
-
|
|
76
|
-
.attr('value', '/dev/ttyUSB0')
|
|
77
|
-
.text('未检测到串口,请手动输入'));
|
|
71
|
+
$('#serial-port-list').html('<div style="color:#999; margin-top:10px;">未找到可用串口,请手动输入</div>');
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
btn.prop("disabled", false).html('<i class="fa fa-
|
|
74
|
+
btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
|
|
81
75
|
}).fail(function() {
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
$('#serial-port-list').html('<div style="color:#f00; margin-top:10px;">搜索失败</div>');
|
|
77
|
+
btn.prop("disabled", false).html('<i class="fa fa-search"></i> 搜索');
|
|
84
78
|
});
|
|
85
79
|
});
|
|
86
|
-
|
|
87
|
-
// 页面加载时自动刷新一次
|
|
88
|
-
setTimeout(function() {
|
|
89
|
-
$("#node-config-refresh-serial").trigger("click");
|
|
90
|
-
}, 100);
|
|
91
80
|
}
|
|
92
81
|
});
|
|
93
82
|
</script>
|
|
@@ -127,18 +116,22 @@
|
|
|
127
116
|
|
|
128
117
|
<div class="form-row form-row-serial">
|
|
129
118
|
<label for="node-config-input-serialPort"><i class="fa fa-terminal"></i> 串口</label>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<button type="button" id="node-config-refresh-serial" class="red-ui-button" style="margin-left: 10px;">
|
|
134
|
-
<i class="fa fa-refresh"></i> 刷新串口列表
|
|
119
|
+
<input type="text" id="node-config-input-serialPort" placeholder="/dev/ttyUSB0" style="width: calc(50% - 90px);">
|
|
120
|
+
<button type="button" id="node-config-refresh-serial" class="red-ui-button" style="margin-left: 5px; width: 80px;">
|
|
121
|
+
<i class="fa fa-search"></i> 搜索
|
|
135
122
|
</button>
|
|
136
123
|
</div>
|
|
124
|
+
<div class="form-row form-row-serial">
|
|
125
|
+
<div id="serial-port-list" style="margin-left: 110px;"></div>
|
|
126
|
+
</div>
|
|
137
127
|
|
|
138
128
|
<div class="form-row form-row-serial">
|
|
139
129
|
<label for="node-config-input-serialBaudRate"><i class="fa fa-tachometer"></i> 波特率</label>
|
|
140
130
|
<select id="node-config-input-serialBaudRate" style="width: 150px;">
|
|
141
|
-
<option value="
|
|
131
|
+
<option value="1200">1200</option>
|
|
132
|
+
<option value="2400">2400</option>
|
|
133
|
+
<option value="4800">4800</option>
|
|
134
|
+
<option value="9600" selected>9600</option>
|
|
142
135
|
<option value="19200">19200</option>
|
|
143
136
|
<option value="38400">38400</option>
|
|
144
137
|
<option value="57600">57600</option>
|
|
@@ -146,7 +139,7 @@
|
|
|
146
139
|
</select>
|
|
147
140
|
</div>
|
|
148
141
|
|
|
149
|
-
<div class="form-row form-row-serial"
|
|
142
|
+
<div class="form-row form-row-serial">
|
|
150
143
|
<label for="node-config-input-serialDataBits"><i class="fa fa-database"></i> 数据位</label>
|
|
151
144
|
<select id="node-config-input-serialDataBits" style="width: 100px;">
|
|
152
145
|
<option value="7">7</option>
|
|
@@ -154,7 +147,7 @@
|
|
|
154
147
|
</select>
|
|
155
148
|
</div>
|
|
156
149
|
|
|
157
|
-
<div class="form-row form-row-serial"
|
|
150
|
+
<div class="form-row form-row-serial">
|
|
158
151
|
<label for="node-config-input-serialStopBits"><i class="fa fa-stop"></i> 停止位</label>
|
|
159
152
|
<select id="node-config-input-serialStopBits" style="width: 100px;">
|
|
160
153
|
<option value="1" selected>1</option>
|
|
@@ -162,12 +155,12 @@
|
|
|
162
155
|
</select>
|
|
163
156
|
</div>
|
|
164
157
|
|
|
165
|
-
<div class="form-row form-row-serial"
|
|
158
|
+
<div class="form-row form-row-serial">
|
|
166
159
|
<label for="node-config-input-serialParity"><i class="fa fa-check"></i> 校验位</label>
|
|
167
160
|
<select id="node-config-input-serialParity" style="width: 100px;">
|
|
168
|
-
<option value="none" selected
|
|
169
|
-
<option value="even"
|
|
170
|
-
<option value="odd"
|
|
161
|
+
<option value="none" selected>无 (N)</option>
|
|
162
|
+
<option value="even">偶校验 (E)</option>
|
|
163
|
+
<option value="odd">奇校验 (O)</option>
|
|
171
164
|
</select>
|
|
172
165
|
</div>
|
|
173
166
|
</script>
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('relay-output', {
|
|
3
|
+
category: 'SYMI MODBUS',
|
|
4
|
+
color: '#FFA500',
|
|
5
|
+
paletteLabel: "继电器输出",
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: "" },
|
|
8
|
+
// RS-485连接配置(共享配置节点)
|
|
9
|
+
serialPortConfig: { value: "", type: "serial-port-config", required: false },
|
|
10
|
+
// 物理开关面板配置(触发源)
|
|
11
|
+
switchBrand: { value: "symi" },
|
|
12
|
+
buttonType: { value: "switch" },
|
|
13
|
+
switchId: { value: 0, validate: RED.validators.number() },
|
|
14
|
+
buttonNumber: { value: 1, validate: RED.validators.number() },
|
|
15
|
+
// 目标继电器配置
|
|
16
|
+
slaveAddress: { value: 10, validate: RED.validators.number() },
|
|
17
|
+
coilNumber: { value: 1, validate: RED.validators.number() },
|
|
18
|
+
// 动作配置
|
|
19
|
+
action: { value: "on" },
|
|
20
|
+
delayMs: { value: 0, validate: RED.validators.number() },
|
|
21
|
+
ignoreRelease: { value: true },
|
|
22
|
+
// 门禁ID过滤(可选)
|
|
23
|
+
filterDeviceId: { value: 0, validate: RED.validators.number() }
|
|
24
|
+
},
|
|
25
|
+
inputs: 1,
|
|
26
|
+
outputs: 1,
|
|
27
|
+
icon: "font-awesome/fa-lightbulb-o",
|
|
28
|
+
label: function() {
|
|
29
|
+
if (this.name) return this.name;
|
|
30
|
+
const actionMap = { 'on': '开', 'off': '关', 'follow': '跟随', 'toggle': '翻转' };
|
|
31
|
+
const btnLabel = this.buttonNumber == 15 ? '背光灯' : `按钮${this.buttonNumber}`;
|
|
32
|
+
if (this.buttonType === 'input') {
|
|
33
|
+
return `输入端 → 继电器${this.slaveAddress}-${this.coilNumber}路 ${actionMap[this.action] || '开'}`;
|
|
34
|
+
}
|
|
35
|
+
return `开关${this.switchId}-${btnLabel} → 继电器${this.slaveAddress}-${this.coilNumber}路 ${actionMap[this.action] || '开'}`;
|
|
36
|
+
},
|
|
37
|
+
oneditprepare: function() {
|
|
38
|
+
const node = this;
|
|
39
|
+
|
|
40
|
+
// 延时提示
|
|
41
|
+
$("#node-input-delayMs").on("change", function() {
|
|
42
|
+
var val = parseInt($(this).val()) || 0;
|
|
43
|
+
if (val > 0) {
|
|
44
|
+
$("#delay-hint").text(`(${val}毫秒 = ${(val/1000).toFixed(1)}秒)`);
|
|
45
|
+
} else {
|
|
46
|
+
$("#delay-hint").text("(立即执行)");
|
|
47
|
+
}
|
|
48
|
+
}).trigger("change");
|
|
49
|
+
|
|
50
|
+
// 按钮类型切换控制显示/隐藏
|
|
51
|
+
$("#node-input-buttonType").on("change", function() {
|
|
52
|
+
const buttonType = $(this).val();
|
|
53
|
+
if (buttonType === 'input') {
|
|
54
|
+
$(".form-row-switch").hide();
|
|
55
|
+
} else {
|
|
56
|
+
$(".form-row-switch").show();
|
|
57
|
+
}
|
|
58
|
+
}).trigger("change");
|
|
59
|
+
|
|
60
|
+
// 按钮编号切换时显示/隐藏背光灯提示
|
|
61
|
+
$("#node-input-buttonNumber").on("change", function() {
|
|
62
|
+
const buttonNumber = $(this).val();
|
|
63
|
+
if (buttonNumber === '15') {
|
|
64
|
+
$("#backlight-hint").show();
|
|
65
|
+
} else {
|
|
66
|
+
$("#backlight-hint").hide();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 配置摘要更新
|
|
71
|
+
function updateSummary() {
|
|
72
|
+
var switchId = parseInt($("#node-input-switchId").val()) || 0;
|
|
73
|
+
var btnNum = parseInt($("#node-input-buttonNumber").val()) || 1;
|
|
74
|
+
var slave = parseInt($("#node-input-slaveAddress").val()) || 10;
|
|
75
|
+
var coil = parseInt($("#node-input-coilNumber").val()) || 1;
|
|
76
|
+
var action = $("#node-input-action").val() || 'on';
|
|
77
|
+
var delay = parseInt($("#node-input-delayMs").val()) || 0;
|
|
78
|
+
var buttonType = $("#node-input-buttonType").val();
|
|
79
|
+
|
|
80
|
+
const actionMap = { 'on': '打开', 'off': '关闭', 'follow': '跟随', 'toggle': '翻转' };
|
|
81
|
+
const btnLabel = btnNum == 15 ? '背光灯' : `按钮${btnNum}`;
|
|
82
|
+
|
|
83
|
+
var triggerText = buttonType === 'input'
|
|
84
|
+
? '输入端触发'
|
|
85
|
+
: `开关${switchId}-${btnLabel}`;
|
|
86
|
+
var delayText = delay > 0 ? `, 延时${delay}ms` : '';
|
|
87
|
+
|
|
88
|
+
$("#config-summary").html(`<strong>${triggerText}</strong> → 继电器${slave}-${coil}路 <strong>${actionMap[action]}</strong>${delayText}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$("#node-input-switchId, #node-input-buttonNumber, #node-input-slaveAddress, #node-input-coilNumber, #node-input-action, #node-input-delayMs, #node-input-buttonType").on("change", updateSummary);
|
|
92
|
+
updateSummary();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<script type="text/html" data-template-name="relay-output">
|
|
98
|
+
<!-- 基本配置 -->
|
|
99
|
+
<div class="form-row">
|
|
100
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> 名称</label>
|
|
101
|
+
<input type="text" id="node-input-name" placeholder="例如:客厅灯">
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- RS-485连接配置 -->
|
|
105
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
108
|
+
<i class="fa fa-plug" style="color: #ff9800;"></i>
|
|
109
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">RS-485连接配置</span>
|
|
110
|
+
</label>
|
|
111
|
+
<div style="font-size: 11px; color: #555; padding: 10px 12px; background: linear-gradient(135deg, #fff3cd 0%, #fffbe6 100%); border-left: 4px solid #ffc107; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
|
112
|
+
<strong>说明:</strong>多个继电器输出节点可以共享同一个RS-485连接配置(支持TCP网关或串口)<br>
|
|
113
|
+
<strong>提示:</strong>此配置用于监听开关按键事件,无需连线到主站节点
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="form-row">
|
|
118
|
+
<label for="node-input-serialPortConfig" style="width: 110px;"><i class="fa fa-server"></i> 连接配置</label>
|
|
119
|
+
<input type="text" id="node-input-serialPortConfig" placeholder="选择或添加RS-485连接配置" style="width: calc(70% - 110px);">
|
|
120
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
121
|
+
选择已配置的RS-485连接(支持多个节点共享)
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- 按键触发配置 -->
|
|
126
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
127
|
+
<div class="form-row">
|
|
128
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
129
|
+
<i class="fa fa-hand-pointer-o" style="color: #3f51b5;"></i>
|
|
130
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">按键触发配置</span>
|
|
131
|
+
</label>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="form-row">
|
|
135
|
+
<label for="node-input-switchBrand" style="width: 110px;"><i class="fa fa-trademark"></i> 面板品牌</label>
|
|
136
|
+
<select id="node-input-switchBrand" style="width: 200px;">
|
|
137
|
+
<option value="symi">亖米(Symi)</option>
|
|
138
|
+
<option value="other1" disabled style="color: #999;">其他品牌1(待开发)</option>
|
|
139
|
+
<option value="other2" disabled style="color: #999;">其他品牌2(待开发)</option>
|
|
140
|
+
</select>
|
|
141
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">支持1-8键开关</span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="form-row">
|
|
145
|
+
<label for="node-input-buttonType" style="width: 110px;"><i class="fa fa-cog"></i> 按钮类型</label>
|
|
146
|
+
<select id="node-input-buttonType" style="width: 200px;">
|
|
147
|
+
<option value="switch">开关按钮(RS-485)</option>
|
|
148
|
+
<option value="scene">场景按钮(RS-485)</option>
|
|
149
|
+
<option value="input">输入端触发(外部设备)</option>
|
|
150
|
+
</select>
|
|
151
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
152
|
+
<strong>开关按钮</strong>:RS-485开关,有开/关状态<br>
|
|
153
|
+
<strong>场景按钮</strong>:RS-485场景触发<br>
|
|
154
|
+
<strong>输入端触发</strong>:海康门禁等外部设备
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="form-row form-row-switch">
|
|
159
|
+
<label for="node-input-switchId" style="width: 110px;"><i class="fa fa-id-card"></i> 开关ID</label>
|
|
160
|
+
<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;">
|
|
161
|
+
<span style="margin-left: 10px; color: #666; font-size: 12px;">物理面板地址:<strong>0-255</strong></span>
|
|
162
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
163
|
+
RS-485总线上的设备地址标识
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="form-row form-row-switch">
|
|
168
|
+
<label for="node-input-buttonNumber" style="width: 110px;"><i class="fa fa-hand-pointer-o"></i> 按钮编号</label>
|
|
169
|
+
<select id="node-input-buttonNumber" style="width: 150px;">
|
|
170
|
+
<option value="1">按钮 1</option>
|
|
171
|
+
<option value="2">按钮 2</option>
|
|
172
|
+
<option value="3">按钮 3</option>
|
|
173
|
+
<option value="4">按钮 4</option>
|
|
174
|
+
<option value="5">按钮 5</option>
|
|
175
|
+
<option value="6">按钮 6</option>
|
|
176
|
+
<option value="7">按钮 7</option>
|
|
177
|
+
<option value="8">按钮 8</option>
|
|
178
|
+
<option value="15">按键背光灯</option>
|
|
179
|
+
</select>
|
|
180
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666; font-style: italic;">面板物理按键</span>
|
|
181
|
+
<div id="backlight-hint" style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px; display: none;">
|
|
182
|
+
<i class="fa fa-info-circle" style="color: #2196f3;"></i> 按键背光灯用于红外感应触发,通道0x0F
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div class="form-row">
|
|
187
|
+
<label for="node-input-filterDeviceId" style="width: 110px;"><i class="fa fa-id-badge"></i> 门禁ID</label>
|
|
188
|
+
<input type="number" id="node-input-filterDeviceId" min="0" max="99" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
189
|
+
<span style="margin-left: 10px; font-size: 11px; color: #666;">可选,0=不过滤,>0=只响应指定门禁</span>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- 目标继电器配置 -->
|
|
193
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
194
|
+
<div class="form-row">
|
|
195
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
196
|
+
<i class="fa fa-arrow-right" style="color: #4caf50;"></i>
|
|
197
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">目标继电器配置</span>
|
|
198
|
+
</label>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="form-row">
|
|
202
|
+
<label for="node-input-slaveAddress" style="width: 110px;"><i class="fa fa-map-marker"></i> 从站地址</label>
|
|
203
|
+
<input type="number" id="node-input-slaveAddress" placeholder="10" min="1" max="247" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
204
|
+
<span style="margin-left: 10px; color: #666; font-size: 12px;">Modbus继电器:<strong>10-19</strong></span>
|
|
205
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
206
|
+
主站节点中配置的从站设备地址
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="form-row">
|
|
211
|
+
<label for="node-input-coilNumber" style="width: 110px;"><i class="fa fa-plug"></i> 继电器路数</label>
|
|
212
|
+
<input type="number" id="node-input-coilNumber" placeholder="1" min="1" max="32" style="width: 90px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
213
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
214
|
+
继电器1-32路,对应线圈0-31,只需填写正确的继电器通道即可
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<!-- 动作配置 -->
|
|
219
|
+
<hr style="margin: 15px 0; border: 0; border-top: 2px solid #e0e0e0;">
|
|
220
|
+
<div class="form-row">
|
|
221
|
+
<label style="width: 100%; margin-bottom: 8px;">
|
|
222
|
+
<i class="fa fa-bolt" style="color: #ff5722;"></i>
|
|
223
|
+
<span style="font-size: 14px; font-weight: 600; color: #333;">动作配置</span>
|
|
224
|
+
</label>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="form-row">
|
|
228
|
+
<label for="node-input-action" style="width: 110px;"><i class="fa fa-play"></i> 动作</label>
|
|
229
|
+
<select id="node-input-action" style="width: 150px;">
|
|
230
|
+
<option value="on">打开</option>
|
|
231
|
+
<option value="off">关闭</option>
|
|
232
|
+
<option value="follow">跟随输入</option>
|
|
233
|
+
<option value="toggle">翻转状态</option>
|
|
234
|
+
</select>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="form-row">
|
|
238
|
+
<label for="node-input-delayMs" style="width: 110px;"><i class="fa fa-clock-o"></i> 延时</label>
|
|
239
|
+
<input type="number" id="node-input-delayMs" min="0" max="60000" style="width: 120px; padding: 5px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px;">
|
|
240
|
+
<span style="margin-left: 5px;">ms</span>
|
|
241
|
+
<span id="delay-hint" style="margin-left: 10px; color: #f57c00; font-size: 11px;"></span>
|
|
242
|
+
<div style="font-size: 11px; color: #888; margin-left: 110px; margin-top: 3px;">
|
|
243
|
+
执行延时(0-60000毫秒),用于实现级联开灯效果
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div class="form-row">
|
|
248
|
+
<input type="checkbox" id="node-input-ignoreRelease" style="width: auto; margin-left: 110px;">
|
|
249
|
+
<label for="node-input-ignoreRelease" style="width: auto;">忽略释放信号(只响应按下)</label>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- 配置摘要 -->
|
|
253
|
+
<div class="form-row" style="margin-top: 20px; padding: 14px; background: linear-gradient(135deg, #e8f5e9 0%, #f1f8f4 100%); border-left: 4px solid #4caf50; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.08);">
|
|
254
|
+
<div style="font-size: 12px; color: #333; line-height: 1.8;">
|
|
255
|
+
<div style="font-weight: 600; color: #2e7d32; margin-bottom: 10px; font-size: 13px;">
|
|
256
|
+
配置摘要
|
|
257
|
+
</div>
|
|
258
|
+
<div id="config-summary" style="background: white; padding: 8px; border-radius: 4px; border: 1px solid #c8e6c9; font-size: 12px; color: #555;">
|
|
259
|
+
</div>
|
|
260
|
+
<div style="margin-top: 10px; font-size: 11px; color: #666;">
|
|
261
|
+
<strong>工作原理:</strong>监听按键事件,通过内部事件发送控制命令到主站,<strong>无需连线</strong>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<script type="text/html" data-help-name="relay-output">
|
|
268
|
+
<p>继电器输出节点,绑定从站开关按键触发继电器动作,或接收外部输入(如海康门禁)。</p>
|
|
269
|
+
<p><b>无需连线到主站</b>,按键触发后自动通过内部事件控制继电器。</p>
|
|
270
|
+
|
|
271
|
+
<h3>RS-485连接配置</h3>
|
|
272
|
+
<dl class="message-properties">
|
|
273
|
+
<dt>连接配置 <span class="property-type">config</span></dt>
|
|
274
|
+
<dd>选择已配置的RS-485连接(支持TCP网关或串口),用于监听开关按键事件</dd>
|
|
275
|
+
</dl>
|
|
276
|
+
|
|
277
|
+
<h3>按键触发配置</h3>
|
|
278
|
+
<dl class="message-properties">
|
|
279
|
+
<dt>面板品牌 <span class="property-type">string</span></dt>
|
|
280
|
+
<dd>开关面板品牌,目前支持亖米(Symi)</dd>
|
|
281
|
+
|
|
282
|
+
<dt>按钮类型 <span class="property-type">string</span></dt>
|
|
283
|
+
<dd>开关按钮/场景按钮/输入端触发</dd>
|
|
284
|
+
|
|
285
|
+
<dt>开关ID <span class="property-type">number</span></dt>
|
|
286
|
+
<dd>物理面板地址(0-255)</dd>
|
|
287
|
+
|
|
288
|
+
<dt>按钮编号 <span class="property-type">number</span></dt>
|
|
289
|
+
<dd>按键编号(1-8,15=背光灯)</dd>
|
|
290
|
+
|
|
291
|
+
<dt>门禁ID <span class="property-type">number</span></dt>
|
|
292
|
+
<dd>可选,0=不过滤,>0=只响应指定门禁编号(用于多门禁场景)</dd>
|
|
293
|
+
</dl>
|
|
294
|
+
|
|
295
|
+
<h3>目标继电器配置</h3>
|
|
296
|
+
<dl class="message-properties">
|
|
297
|
+
<dt>从站地址 <span class="property-type">number</span></dt>
|
|
298
|
+
<dd>要控制的Modbus从站地址(1-247)</dd>
|
|
299
|
+
|
|
300
|
+
<dt>继电器路数 <span class="property-type">number</span></dt>
|
|
301
|
+
<dd>要控制的继电器通道(1-32)</dd>
|
|
302
|
+
</dl>
|
|
303
|
+
|
|
304
|
+
<h3>动作配置</h3>
|
|
305
|
+
<dl class="message-properties">
|
|
306
|
+
<dt>动作 <span class="property-type">string</span></dt>
|
|
307
|
+
<dd>打开/关闭/跟随输入/翻转状态</dd>
|
|
308
|
+
|
|
309
|
+
<dt>延时 <span class="property-type">number</span></dt>
|
|
310
|
+
<dd>执行延时(0-60000毫秒),用于实现级联开灯效果</dd>
|
|
311
|
+
|
|
312
|
+
<dt>忽略释放信号 <span class="property-type">boolean</span></dt>
|
|
313
|
+
<dd>勾选后,只响应按下信号,忽略释放信号</dd>
|
|
314
|
+
</dl>
|
|
315
|
+
|
|
316
|
+
<h3>使用场景</h3>
|
|
317
|
+
<p><b>场景1:按键触发继电器(无需连线)</b></p>
|
|
318
|
+
<p>配置开关ID=1,按钮编号=3,当开关1的按钮3被按下时,自动控制目标继电器。</p>
|
|
319
|
+
|
|
320
|
+
<p><b>场景2:一个按键控制多路继电器(级联开灯)</b></p>
|
|
321
|
+
<p>多个继电器输出节点绑定同一个按键(开关ID+按钮编号),配置不同的延时和动作,实现级联开灯效果。</p>
|
|
322
|
+
<pre>开关ID=1 按钮=3 → 继电器1(1路 延时0ms 打开)
|
|
323
|
+
→ 继电器2(2路 延时500ms 打开)
|
|
324
|
+
→ 继电器3(3路 延时1000ms 打开)</pre>
|
|
325
|
+
|
|
326
|
+
<p><b>场景3:海康门禁触发(使用输入端)</b></p>
|
|
327
|
+
<pre>海康门禁事件 → 继电器输出(按钮类型=输入端触发 目标从站10 1路)</pre>
|
|
328
|
+
|
|
329
|
+
<h3>工作原理</h3>
|
|
330
|
+
<p>节点监听 <code>modbus:buttonPressed</code> 事件(按键触发),
|
|
331
|
+
通过 <code>modbus:writeCoil</code> 事件发送控制命令到主站。</p>
|
|
332
|
+
|
|
333
|
+
<h3>特点</h3>
|
|
334
|
+
<ul>
|
|
335
|
+
<li>✅ 完全解耦:无需连线到主站节点</li>
|
|
336
|
+
<li>✅ 配置持久化:配置自动保存到Node-RED</li>
|
|
337
|
+
<li>✅ 稳定可靠:长期运行不卡顿、不死机、内存不溢出</li>
|
|
338
|
+
<li>✅ 断电恢复:断电断网后自动恢复工作</li>
|
|
339
|
+
<li>✅ 防死循环:内置防抖和状态检测机制</li>
|
|
340
|
+
<li>✅ 生产就绪:无调试数据输出</li>
|
|
341
|
+
</ul>
|
|
342
|
+
</script>
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 继电器输出节点
|
|
3
|
+
* 接收触发信号,通过内部事件直接发送命令到Modbus主站
|
|
4
|
+
* 支持绑定从站开关触发源,实现无需连线的联动
|
|
5
|
+
* 无需连线到主站,简化流程配置
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = function(RED) {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
function RelayOutputNode(config) {
|
|
12
|
+
RED.nodes.createNode(this, config);
|
|
13
|
+
const node = this;
|
|
14
|
+
|
|
15
|
+
// 配置参数 - RS-485连接配置
|
|
16
|
+
node.serialPortConfig = config.serialPortConfig;
|
|
17
|
+
|
|
18
|
+
// 配置参数 - 按键触发(绑定从站开关)
|
|
19
|
+
node.switchBrand = config.switchBrand || 'symi';
|
|
20
|
+
node.buttonType = config.buttonType || 'switch'; // switch, scene, input
|
|
21
|
+
node.switchId = parseInt(config.switchId) || 0;
|
|
22
|
+
node.buttonNumber = parseInt(config.buttonNumber) || 1; // 按键编号(1-8, 15=背光灯)
|
|
23
|
+
node.filterDeviceId = parseInt(config.filterDeviceId) || 0; // 0=不过滤,>0=只响应指定门禁ID
|
|
24
|
+
|
|
25
|
+
// 配置参数 - 目标继电器
|
|
26
|
+
node.name = config.name || '';
|
|
27
|
+
node.slaveAddress = parseInt(config.slaveAddress) || 10;
|
|
28
|
+
node.coilNumber = parseInt(config.coilNumber) || 1;
|
|
29
|
+
|
|
30
|
+
// 配置参数 - 动作
|
|
31
|
+
node.action = config.action || 'on'; // on, off, follow, toggle
|
|
32
|
+
node.delayMs = parseInt(config.delayMs) || 0;
|
|
33
|
+
node.ignoreRelease = config.ignoreRelease !== false; // 默认忽略释放信号
|
|
34
|
+
|
|
35
|
+
// 节点状态
|
|
36
|
+
node.isClosing = false;
|
|
37
|
+
node.pendingTimer = null; // 延时定时器
|
|
38
|
+
node.stateChangeListener = null; // 状态变化监听器
|
|
39
|
+
|
|
40
|
+
// 初始化后延迟启动(防止重启时自动动作)
|
|
41
|
+
node.initialized = false;
|
|
42
|
+
node.initTimer = setTimeout(() => {
|
|
43
|
+
node.initialized = true;
|
|
44
|
+
const btnLabel = node.buttonNumber == 15 ? '背光灯' : `按键${node.buttonNumber}`;
|
|
45
|
+
const triggerInfo = node.buttonType === 'input'
|
|
46
|
+
? '← 输入端'
|
|
47
|
+
: `← 开关${node.switchId}-${btnLabel}`;
|
|
48
|
+
node.status({ fill: 'blue', shape: 'ring', text: `就绪 ${triggerInfo} → 从站${node.slaveAddress}-${node.coilNumber}路` });
|
|
49
|
+
}, 2000);
|
|
50
|
+
|
|
51
|
+
// 更新状态显示
|
|
52
|
+
node.updateStatus = function(text, color) {
|
|
53
|
+
node.status({ fill: color || 'green', shape: 'dot', text: text });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 监听从站开关按键事件(用于绑定触发源,非输入端模式)
|
|
57
|
+
if (node.buttonType !== 'input') {
|
|
58
|
+
node.stateChangeListener = function(data) {
|
|
59
|
+
// 未初始化时忽略
|
|
60
|
+
if (!node.initialized || node.isClosing) return;
|
|
61
|
+
|
|
62
|
+
// 检查是否匹配触发源
|
|
63
|
+
const msgSwitchId = data.switchId;
|
|
64
|
+
const msgButtonNumber = data.button || data.buttonNumber;
|
|
65
|
+
|
|
66
|
+
// 开关ID必须匹配
|
|
67
|
+
if (parseInt(msgSwitchId) !== node.switchId) return;
|
|
68
|
+
|
|
69
|
+
// 按键位必须匹配
|
|
70
|
+
if (parseInt(msgButtonNumber) !== node.buttonNumber) return;
|
|
71
|
+
|
|
72
|
+
// 门禁ID匹配(0=不过滤)
|
|
73
|
+
if (node.filterDeviceId > 0) {
|
|
74
|
+
const msgDeviceId = data.deviceId || 0;
|
|
75
|
+
if (msgDeviceId > 0 && parseInt(msgDeviceId) !== node.filterDeviceId) return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 获取触发值
|
|
79
|
+
const triggerValue = data.value !== undefined ? data.value : data.payload;
|
|
80
|
+
|
|
81
|
+
// 忽略释放信号
|
|
82
|
+
if (node.ignoreRelease && !triggerValue) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 执行控制
|
|
87
|
+
const btnLabel = msgButtonNumber == 15 ? '背光灯' : `按键${msgButtonNumber}`;
|
|
88
|
+
node.executeControl(triggerValue, `开关${msgSwitchId}-${btnLabel}`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
RED.events.on('modbus:buttonPressed', node.stateChangeListener);
|
|
92
|
+
const btnLabel = node.buttonNumber == 15 ? '背光灯' : `按键${node.buttonNumber}`;
|
|
93
|
+
node.log(`绑定触发源: 开关${node.switchId} ${btnLabel}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 发送写入命令到主站(通过内部事件)
|
|
97
|
+
node.sendWriteCommand = function(value, source) {
|
|
98
|
+
const coilIndex = node.coilNumber - 1; // 用户输入1-32,内部使用0-31
|
|
99
|
+
|
|
100
|
+
RED.events.emit('modbus:writeCoil', {
|
|
101
|
+
slave: node.slaveAddress,
|
|
102
|
+
coil: coilIndex,
|
|
103
|
+
value: value,
|
|
104
|
+
source: 'relay-output',
|
|
105
|
+
nodeId: node.id
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const actionText = value ? '打开' : '关闭';
|
|
109
|
+
const sourceText = source ? ` (${source})` : '';
|
|
110
|
+
node.updateStatus(`${actionText} → 从站${node.slaveAddress}线圈${node.coilNumber}${sourceText}`, value ? 'green' : 'grey');
|
|
111
|
+
node.log(`继电器输出: 从站${node.slaveAddress} 线圈${node.coilNumber} ${actionText}${sourceText}`);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// 执行控制逻辑
|
|
115
|
+
node.executeControl = function(triggerValue, source) {
|
|
116
|
+
// 确定目标值
|
|
117
|
+
let targetValue;
|
|
118
|
+
switch (node.action) {
|
|
119
|
+
case 'on':
|
|
120
|
+
targetValue = true;
|
|
121
|
+
break;
|
|
122
|
+
case 'off':
|
|
123
|
+
targetValue = false;
|
|
124
|
+
break;
|
|
125
|
+
case 'follow':
|
|
126
|
+
targetValue = triggerValue;
|
|
127
|
+
break;
|
|
128
|
+
case 'toggle':
|
|
129
|
+
targetValue = !triggerValue;
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
targetValue = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 清除之前的延时定时器
|
|
136
|
+
if (node.pendingTimer) {
|
|
137
|
+
clearTimeout(node.pendingTimer);
|
|
138
|
+
node.pendingTimer = null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 延时执行
|
|
142
|
+
if (node.delayMs > 0) {
|
|
143
|
+
node.status({ fill: 'yellow', shape: 'ring', text: `延时${node.delayMs}ms...` });
|
|
144
|
+
node.pendingTimer = setTimeout(() => {
|
|
145
|
+
node.pendingTimer = null;
|
|
146
|
+
if (!node.isClosing) {
|
|
147
|
+
node.sendWriteCommand(targetValue, source);
|
|
148
|
+
}
|
|
149
|
+
}, node.delayMs);
|
|
150
|
+
} else {
|
|
151
|
+
node.sendWriteCommand(targetValue, source);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// 处理输入消息(用于海康门禁等外部设备触发)
|
|
156
|
+
node.on('input', function(msg, send, done) {
|
|
157
|
+
// 未初始化时忽略(防止重启时自动动作)
|
|
158
|
+
if (!node.initialized) {
|
|
159
|
+
done();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 获取触发值
|
|
164
|
+
let triggerValue = msg.payload;
|
|
165
|
+
if (typeof triggerValue === 'object' && triggerValue !== null) {
|
|
166
|
+
triggerValue = triggerValue.value !== undefined ? triggerValue.value : triggerValue.state;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 转换为布尔值
|
|
170
|
+
const isTriggerOn = triggerValue === true || triggerValue === 'true' ||
|
|
171
|
+
triggerValue === 1 || triggerValue === '1' ||
|
|
172
|
+
triggerValue === 'ON' || triggerValue === 'on';
|
|
173
|
+
|
|
174
|
+
// 忽略释放信号(如果配置了)
|
|
175
|
+
if (node.ignoreRelease && !isTriggerOn) {
|
|
176
|
+
node.status({ fill: 'grey', shape: 'ring', text: '忽略释放信号' });
|
|
177
|
+
done();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 门禁ID匹配(输入端模式)
|
|
182
|
+
if (node.filterDeviceId > 0) {
|
|
183
|
+
const msgDeviceId = msg.deviceId || 0;
|
|
184
|
+
if (msgDeviceId > 0 && parseInt(msgDeviceId) !== node.filterDeviceId) {
|
|
185
|
+
done();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 执行控制
|
|
191
|
+
node.executeControl(isTriggerOn, '输入端');
|
|
192
|
+
|
|
193
|
+
// 传递消息到输出(可选,用于连接其他节点如debug)
|
|
194
|
+
if (send) {
|
|
195
|
+
msg.relayOutput = {
|
|
196
|
+
slave: node.slaveAddress,
|
|
197
|
+
coil: node.coilNumber,
|
|
198
|
+
delay: node.delayMs
|
|
199
|
+
};
|
|
200
|
+
send(msg);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
done();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 节点关闭时清理
|
|
207
|
+
node.on('close', function(done) {
|
|
208
|
+
node.isClosing = true;
|
|
209
|
+
|
|
210
|
+
// 清除初始化定时器
|
|
211
|
+
if (node.initTimer) {
|
|
212
|
+
clearTimeout(node.initTimer);
|
|
213
|
+
node.initTimer = null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 清除延时定时器
|
|
217
|
+
if (node.pendingTimer) {
|
|
218
|
+
clearTimeout(node.pendingTimer);
|
|
219
|
+
node.pendingTimer = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 移除按键事件监听器
|
|
223
|
+
if (node.stateChangeListener) {
|
|
224
|
+
RED.events.removeListener('modbus:buttonPressed', node.stateChangeListener);
|
|
225
|
+
node.stateChangeListener = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
done();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
RED.nodes.registerType("relay-output", RelayOutputNode);
|
|
233
|
+
};
|
|
@@ -126,7 +126,10 @@
|
|
|
126
126
|
<div class="form-row config-serial-row">
|
|
127
127
|
<label for="node-config-input-baudRate"><i class="fa fa-tachometer"></i> 波特率</label>
|
|
128
128
|
<select id="node-config-input-baudRate" style="width:70%">
|
|
129
|
-
<option value="
|
|
129
|
+
<option value="1200">1200</option>
|
|
130
|
+
<option value="2400">2400</option>
|
|
131
|
+
<option value="4800">4800</option>
|
|
132
|
+
<option value="9600" selected>9600</option>
|
|
130
133
|
<option value="19200">19200</option>
|
|
131
134
|
<option value="38400">38400</option>
|
|
132
135
|
<option value="57600">57600</option>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.7",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步,工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"author": {
|
|
24
24
|
"name": "symi-daguo",
|
|
25
|
-
"email": "
|
|
25
|
+
"email": "303316404@qq.com"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"engines": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"modbus-debug": "nodes/modbus-debug.js",
|
|
41
41
|
"homekit-bridge": "nodes/homekit-bridge.js",
|
|
42
42
|
"modbus-dashboard": "nodes/modbus-dashboard.js",
|
|
43
|
-
"custom-protocol": "nodes/custom-protocol.js"
|
|
43
|
+
"custom-protocol": "nodes/custom-protocol.js",
|
|
44
|
+
"relay-output": "nodes/relay-output.js"
|
|
44
45
|
}
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|