node-red-contrib-symi-modbus 2.9.6 → 2.9.8
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 +106 -1
- package/nodes/clowire-protocol.js +409 -0
- 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 +22 -6
- package/nodes/modbus-slave-switch.js +137 -2
- 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 +5 -4
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,48 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
888
952
|
|
|
889
953
|
## 版本信息
|
|
890
954
|
|
|
891
|
-
**当前版本**: v2.9.
|
|
955
|
+
**当前版本**: v2.9.8 (2026-01-06)
|
|
956
|
+
|
|
957
|
+
**v2.9.8 更新内容**:
|
|
958
|
+
- **新增品牌支持**:Clowire(克伦威尔)智能面板
|
|
959
|
+
- 支持Clowire 1-8键开关面板,与亖米面板使用方式完全一致
|
|
960
|
+
- 支持单击、双击、长按等按键事件(统一当作单击处理,触发一次状态切换)
|
|
961
|
+
- 支持红外感应器(有人/无人状态检测,复用按键背光灯配置buttonNumber=15)
|
|
962
|
+
- 支持LED指示灯双向同步(继电器状态→面板LED)
|
|
963
|
+
- 协议文档:`doc/Clowire-标准面板485协议-V1.3.md`
|
|
964
|
+
- **界面优化**:
|
|
965
|
+
- 面板品牌下拉框新增"Clowire(克伦威尔)"选项
|
|
966
|
+
- Clowire默认面板地址为2(符合出厂设置)
|
|
967
|
+
- 节点标签显示品牌名称(如"Clowire[2]-按钮1")
|
|
968
|
+
- 感应器复用按键背光灯配置(buttonNumber=15),无需单独配置
|
|
969
|
+
- **协议实现**:
|
|
970
|
+
- 新增`clowire-protocol.js`协议模块
|
|
971
|
+
- 支持MODBUS RTU通讯格式(9600 8N1)
|
|
972
|
+
- 支持CRC16校验和帧尾0xAA
|
|
973
|
+
- 支持批量LED控制(0x25命令码)
|
|
974
|
+
|
|
975
|
+
**v2.9.7 更新内容**:
|
|
976
|
+
- **新增节点**:继电器输出节点(relay-output)
|
|
977
|
+
- 支持**绑定从站开关按键**,实现按键→继电器→主站之间**完全无需连线**
|
|
978
|
+
- 共享RS-485连接配置,支持多个节点共用同一连接
|
|
979
|
+
- 按键触发配置:面板品牌、按钮类型、开关ID(0-255)、按钮编号(1-8)、门禁ID(可选)
|
|
980
|
+
- 目标配置:从站地址、继电器路数、动作(打开/关闭/跟随/翻转)、延时(0-60000ms)
|
|
981
|
+
- 支持延时执行,一个按键控制多路继电器实现级联开灯
|
|
982
|
+
- 配置持久化保存,断电断网恢复后正常工作
|
|
983
|
+
- 长期稳定运行,不卡顿、不死机、内存不溢出,无调试数据输出
|
|
984
|
+
- **界面优化**:所有SYMI MODBUS节点左侧名称改为中文显示
|
|
985
|
+
- **重要修复**:HassOS串口兼容性问题
|
|
986
|
+
- 修复"Cannot lock port"错误:在所有串口连接中添加`lock: false`参数
|
|
987
|
+
- 修复位置:`serial-port-config.js`和`modbus-master.js`的串口连接配置
|
|
988
|
+
- 适用场景:HassOS、Docker容器、以及其他不支持串口锁定的环境
|
|
989
|
+
- **功能改进**:串口配置界面优化
|
|
990
|
+
- Modbus服务器配置:串口路径改为输入框+搜索按钮,支持手动输入(搜索不到也能使用)
|
|
991
|
+
- 显示完整串口参数:数据位(7/8)、停止位(1/2)、校验位(无/偶/奇)
|
|
992
|
+
- 添加更多波特率选项:1200/2400/4800/9600/19200/38400/57600/115200
|
|
993
|
+
- **稳定性保障**:
|
|
994
|
+
- 串口配置持久化保存到Node-RED配置文件
|
|
995
|
+
- 断电断网恢复后自动重连,正常工作
|
|
996
|
+
- 无调试数据输出,适合生产环境长期运行
|
|
892
997
|
|
|
893
998
|
**v2.9.6 更新内容**:
|
|
894
999
|
- **重要修复**:485开关场景按钮的CRC校验兼容性问题
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clowire 标准面板485协议实现
|
|
3
|
+
* 协议版本:V1.3
|
|
4
|
+
* 厂家:宁波星宏智能技术有限公司
|
|
5
|
+
*
|
|
6
|
+
* 协议特点:
|
|
7
|
+
* - 基于 MODBUS RTU 通讯格式
|
|
8
|
+
* - 面板主动发送模式(按键按下时主动上报)
|
|
9
|
+
* - 支持单击、双击、三击、长按
|
|
10
|
+
* - 支持 LED 指示灯控制
|
|
11
|
+
* - 支持红外感应信号上报
|
|
12
|
+
*
|
|
13
|
+
* 通讯帧格式:
|
|
14
|
+
* [面板地址][命令码][寄存器地址高][寄存器地址低][数据高][数据低][CRC低][CRC高][帧尾0xAA]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
// 帧尾
|
|
19
|
+
FRAME_TAIL: 0xAA,
|
|
20
|
+
|
|
21
|
+
// 广播地址
|
|
22
|
+
BROADCAST_ADDR: 0xFF,
|
|
23
|
+
|
|
24
|
+
// 默认面板地址
|
|
25
|
+
DEFAULT_PANEL_ADDR: 0x02,
|
|
26
|
+
|
|
27
|
+
// 命令码
|
|
28
|
+
CMD_READ: 0x03, // 读取寄存器
|
|
29
|
+
CMD_WRITE: 0x06, // 写入单个寄存器
|
|
30
|
+
CMD_BUTTON_EVENT: 0x20, // 按键事件上报
|
|
31
|
+
CMD_BATCH_CONTROL: 0x25, // 批量控制
|
|
32
|
+
|
|
33
|
+
// 寄存器地址
|
|
34
|
+
REG_PANEL_ADDR: 0x1000, // 面板地址
|
|
35
|
+
REG_BUTTON_S1: 0x1011, // 按键S1状态
|
|
36
|
+
REG_BUTTON_S2: 0x1012, // 按键S2状态
|
|
37
|
+
REG_BUTTON_S3: 0x1013, // 按键S3状态
|
|
38
|
+
REG_BUTTON_S4: 0x1014, // 按键S4状态
|
|
39
|
+
REG_BUTTON_S5: 0x1015, // 按键S5状态
|
|
40
|
+
REG_BUTTON_S6: 0x1016, // 按键S6状态
|
|
41
|
+
REG_BUTTON_S7: 0x1017, // 按键S7状态
|
|
42
|
+
REG_BUTTON_S8: 0x1018, // 按键S8状态
|
|
43
|
+
REG_DOUBLE_CLICK_INTERVAL: 0x1019, // 双击时间间隔
|
|
44
|
+
REG_TRIPLE_CLICK_INTERVAL: 0x101A, // 三击时间间隔
|
|
45
|
+
REG_LONG_PRESS_TIME: 0x101B, // 长按时间
|
|
46
|
+
REG_BACKLIGHT: 0x1020, // 字符背光
|
|
47
|
+
REG_LED1: 0x1021, // LED1指示灯
|
|
48
|
+
REG_LED2: 0x1022, // LED2指示灯
|
|
49
|
+
REG_LED3: 0x1023, // LED3指示灯
|
|
50
|
+
REG_LED4: 0x1024, // LED4指示灯
|
|
51
|
+
REG_LED5: 0x1025, // LED5指示灯
|
|
52
|
+
REG_LED6: 0x1026, // LED6指示灯
|
|
53
|
+
REG_LED7: 0x1027, // LED7指示灯
|
|
54
|
+
REG_LED8: 0x1028, // LED8指示灯
|
|
55
|
+
REG_SENSOR: 0x1040, // 感应信号
|
|
56
|
+
REG_VERSION: 0x1041, // 版本查询
|
|
57
|
+
REG_LONG_PRESS: 0x2050, // 长按寄存器
|
|
58
|
+
|
|
59
|
+
// 按键状态值
|
|
60
|
+
BUTTON_SINGLE_CLICK: 0x0080, // 单击 (bit7=1)
|
|
61
|
+
BUTTON_DOUBLE_CLICK: 0x0040, // 双击 (bit6=1)
|
|
62
|
+
BUTTON_TRIPLE_CLICK: 0x0020, // 三击 (bit5=1)
|
|
63
|
+
BUTTON_LONG_PRESS: 0x0180, // 长按
|
|
64
|
+
BUTTON_LONG_RELEASE: 0x01FE, // 长按松开
|
|
65
|
+
|
|
66
|
+
// 感应信号值
|
|
67
|
+
SENSOR_DETECTED: 0x0080, // 有感应信号
|
|
68
|
+
SENSOR_NONE: 0x0000, // 无感应信号
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 计算 MODBUS CRC16 校验
|
|
72
|
+
* @param {Buffer} buffer - 数据缓冲区
|
|
73
|
+
* @param {number} len - 数据长度(不包含CRC和帧尾)
|
|
74
|
+
* @returns {number} CRC16校验值(低字节在前)
|
|
75
|
+
*/
|
|
76
|
+
calculateCRC16: function(buffer, len) {
|
|
77
|
+
let crc = 0xFFFF;
|
|
78
|
+
for (let i = 0; i < len; i++) {
|
|
79
|
+
crc ^= buffer[i];
|
|
80
|
+
for (let j = 0; j < 8; j++) {
|
|
81
|
+
if (crc & 0x0001) {
|
|
82
|
+
crc = (crc >> 1) ^ 0xA001;
|
|
83
|
+
} else {
|
|
84
|
+
crc >>= 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return crc;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 构建LED指示灯控制指令
|
|
93
|
+
* @param {number} panelAddr - 面板地址(1-127)
|
|
94
|
+
* @param {number} buttonNumber - 按钮编号(1-8)
|
|
95
|
+
* @param {boolean} state - LED状态(true=开,false=关)
|
|
96
|
+
* @returns {Buffer} 完整的指令帧
|
|
97
|
+
*/
|
|
98
|
+
buildLedControlCommand: function(panelAddr, buttonNumber, state) {
|
|
99
|
+
const buffer = Buffer.alloc(9);
|
|
100
|
+
let idx = 0;
|
|
101
|
+
|
|
102
|
+
// 面板地址
|
|
103
|
+
buffer[idx++] = panelAddr;
|
|
104
|
+
// 命令码:写入单个寄存器
|
|
105
|
+
buffer[idx++] = this.CMD_WRITE;
|
|
106
|
+
// 寄存器地址(LED1=0x1021, LED2=0x1022, ...)
|
|
107
|
+
const regAddr = 0x1020 + buttonNumber;
|
|
108
|
+
buffer[idx++] = (regAddr >> 8) & 0xFF; // 高字节
|
|
109
|
+
buffer[idx++] = regAddr & 0xFF; // 低字节
|
|
110
|
+
// 数据(00 01=开,00 00=关)
|
|
111
|
+
buffer[idx++] = 0x00;
|
|
112
|
+
buffer[idx++] = state ? 0x01 : 0x00;
|
|
113
|
+
|
|
114
|
+
// 计算CRC16
|
|
115
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
116
|
+
buffer[idx++] = crc & 0xFF; // CRC低字节
|
|
117
|
+
buffer[idx++] = (crc >> 8) & 0xFF; // CRC高字节
|
|
118
|
+
// 帧尾
|
|
119
|
+
buffer[idx++] = this.FRAME_TAIL;
|
|
120
|
+
|
|
121
|
+
return buffer;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 构建批量LED控制指令(使用0x25命令码)
|
|
126
|
+
* @param {number} panelAddr - 面板地址(1-127)
|
|
127
|
+
* @param {Array<boolean>} ledStates - LED状态数组(最多8个)
|
|
128
|
+
* @param {boolean} backlight - 字符背光状态
|
|
129
|
+
* @returns {Buffer} 完整的指令帧
|
|
130
|
+
*/
|
|
131
|
+
buildBatchLedCommand: function(panelAddr, ledStates, backlight = true) {
|
|
132
|
+
const buffer = Buffer.alloc(11);
|
|
133
|
+
let idx = 0;
|
|
134
|
+
|
|
135
|
+
// 广播地址
|
|
136
|
+
buffer[idx++] = this.BROADCAST_ADDR;
|
|
137
|
+
// 命令码:批量控制
|
|
138
|
+
buffer[idx++] = this.CMD_BATCH_CONTROL;
|
|
139
|
+
// 面板开始地址(高字节、低字节)
|
|
140
|
+
buffer[idx++] = 0x00;
|
|
141
|
+
buffer[idx++] = panelAddr;
|
|
142
|
+
// 连续面板数量(高字节、低字节)
|
|
143
|
+
buffer[idx++] = 0x00;
|
|
144
|
+
buffer[idx++] = 0x01;
|
|
145
|
+
|
|
146
|
+
// 面板数据(2字节)
|
|
147
|
+
// 高字节:B8=背光,B9-B12=继电器(预留)
|
|
148
|
+
// 低字节:B0-B5=LED1-LED6
|
|
149
|
+
let highByte = backlight ? 0x01 : 0x00; // B8=背光
|
|
150
|
+
let lowByte = 0x00;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < Math.min(ledStates.length, 6); i++) {
|
|
153
|
+
if (ledStates[i]) {
|
|
154
|
+
lowByte |= (1 << i);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
buffer[idx++] = highByte;
|
|
159
|
+
buffer[idx++] = lowByte;
|
|
160
|
+
|
|
161
|
+
// 计算CRC16
|
|
162
|
+
const crc = this.calculateCRC16(buffer, 8);
|
|
163
|
+
buffer[idx++] = crc & 0xFF; // CRC低字节
|
|
164
|
+
buffer[idx++] = (crc >> 8) & 0xFF; // CRC高字节
|
|
165
|
+
// 帧尾
|
|
166
|
+
buffer[idx++] = this.FRAME_TAIL;
|
|
167
|
+
|
|
168
|
+
return buffer;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 构建字符背光控制指令
|
|
173
|
+
* @param {number} panelAddr - 面板地址(1-127)
|
|
174
|
+
* @param {boolean} state - 背光状态(true=开,false=关)
|
|
175
|
+
* @returns {Buffer} 完整的指令帧
|
|
176
|
+
*/
|
|
177
|
+
buildBacklightCommand: function(panelAddr, state) {
|
|
178
|
+
const buffer = Buffer.alloc(9);
|
|
179
|
+
let idx = 0;
|
|
180
|
+
|
|
181
|
+
buffer[idx++] = panelAddr;
|
|
182
|
+
buffer[idx++] = this.CMD_WRITE;
|
|
183
|
+
buffer[idx++] = 0x10; // 寄存器地址高字节
|
|
184
|
+
buffer[idx++] = 0x20; // 寄存器地址低字节(0x1020)
|
|
185
|
+
buffer[idx++] = 0x00;
|
|
186
|
+
buffer[idx++] = state ? 0x01 : 0x00;
|
|
187
|
+
|
|
188
|
+
const crc = this.calculateCRC16(buffer, 6);
|
|
189
|
+
buffer[idx++] = crc & 0xFF;
|
|
190
|
+
buffer[idx++] = (crc >> 8) & 0xFF;
|
|
191
|
+
buffer[idx++] = this.FRAME_TAIL;
|
|
192
|
+
|
|
193
|
+
return buffer;
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 解析接收到的协议帧
|
|
198
|
+
* @param {Buffer} buffer - 接收到的数据
|
|
199
|
+
* @returns {Object|null} 解析结果或null
|
|
200
|
+
*/
|
|
201
|
+
parseFrame: function(buffer) {
|
|
202
|
+
if (!buffer || buffer.length < 9) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 查找帧尾位置
|
|
207
|
+
let endIndex = -1;
|
|
208
|
+
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
209
|
+
if (buffer[i] === this.FRAME_TAIL) {
|
|
210
|
+
endIndex = i;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (endIndex === -1 || endIndex < 8) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 提取帧数据(从开头到帧尾)
|
|
220
|
+
const frameBuffer = buffer.slice(0, endIndex + 1);
|
|
221
|
+
|
|
222
|
+
// 验证CRC16
|
|
223
|
+
const dataLen = frameBuffer.length - 3; // 不包含CRC(2字节)和帧尾(1字节)
|
|
224
|
+
const receivedCRC = frameBuffer[dataLen] | (frameBuffer[dataLen + 1] << 8);
|
|
225
|
+
const calculatedCRC = this.calculateCRC16(frameBuffer, dataLen);
|
|
226
|
+
|
|
227
|
+
// CRC校验(宽松模式:按键事件允许CRC不匹配)
|
|
228
|
+
const cmdCode = frameBuffer[1];
|
|
229
|
+
const isButtonEvent = (cmdCode === this.CMD_BUTTON_EVENT || cmdCode === this.CMD_READ);
|
|
230
|
+
|
|
231
|
+
if (receivedCRC !== calculatedCRC && !isButtonEvent) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 解析帧
|
|
236
|
+
const frame = {
|
|
237
|
+
panelAddr: frameBuffer[0],
|
|
238
|
+
cmdCode: frameBuffer[1],
|
|
239
|
+
regAddrHigh: frameBuffer[2],
|
|
240
|
+
regAddrLow: frameBuffer[3],
|
|
241
|
+
regAddr: (frameBuffer[2] << 8) | frameBuffer[3],
|
|
242
|
+
dataHigh: frameBuffer[4],
|
|
243
|
+
dataLow: frameBuffer[5],
|
|
244
|
+
data: (frameBuffer[4] << 8) | frameBuffer[5],
|
|
245
|
+
crc: receivedCRC,
|
|
246
|
+
frameLength: frameBuffer.length
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return frame;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 解析所有协议帧(处理粘包)
|
|
254
|
+
* @param {Buffer} buffer - 接收到的数据(可能包含多个帧)
|
|
255
|
+
* @returns {Array} 解析出的所有帧数组
|
|
256
|
+
*/
|
|
257
|
+
parseAllFrames: function(buffer) {
|
|
258
|
+
const frames = [];
|
|
259
|
+
if (!buffer || buffer.length < 9) {
|
|
260
|
+
return frames;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let offset = 0;
|
|
264
|
+
let maxIterations = 10;
|
|
265
|
+
|
|
266
|
+
while (offset < buffer.length && maxIterations > 0) {
|
|
267
|
+
maxIterations--;
|
|
268
|
+
|
|
269
|
+
// 查找帧尾
|
|
270
|
+
let endIndex = -1;
|
|
271
|
+
for (let i = offset; i < buffer.length; i++) {
|
|
272
|
+
if (buffer[i] === this.FRAME_TAIL) {
|
|
273
|
+
endIndex = i;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (endIndex === -1) {
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 检查帧长度(最小9字节)
|
|
283
|
+
const frameLen = endIndex - offset + 1;
|
|
284
|
+
if (frameLen < 9) {
|
|
285
|
+
offset = endIndex + 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 提取帧
|
|
290
|
+
const frameBuffer = buffer.slice(offset, endIndex + 1);
|
|
291
|
+
const frame = this.parseFrame(frameBuffer);
|
|
292
|
+
|
|
293
|
+
if (frame) {
|
|
294
|
+
frames.push(frame);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
offset = endIndex + 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return frames;
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 检测是否是按键事件
|
|
305
|
+
* @param {Object} frame - 解析后的帧
|
|
306
|
+
* @returns {Object|null} 按键事件信息或null
|
|
307
|
+
*/
|
|
308
|
+
detectButtonEvent: function(frame) {
|
|
309
|
+
if (!frame) return null;
|
|
310
|
+
|
|
311
|
+
// 检查命令码是否是按键事件上报(0x20)或读取(0x03)
|
|
312
|
+
if (frame.cmdCode !== this.CMD_BUTTON_EVENT && frame.cmdCode !== this.CMD_READ) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 检查寄存器地址是否在按键范围内(0x1011-0x1018)
|
|
317
|
+
if (frame.regAddr >= this.REG_BUTTON_S1 && frame.regAddr <= this.REG_BUTTON_S8) {
|
|
318
|
+
// 按键事件
|
|
319
|
+
const buttonNumber = frame.regAddr - this.REG_BUTTON_S1 + 1; // 1-8
|
|
320
|
+
const data = frame.data;
|
|
321
|
+
|
|
322
|
+
// 判断按键类型
|
|
323
|
+
let eventType = 'unknown';
|
|
324
|
+
let state = true; // 默认触发状态
|
|
325
|
+
|
|
326
|
+
if (data === this.BUTTON_SINGLE_CLICK || (data & 0x0080) !== 0) {
|
|
327
|
+
eventType = 'single_click';
|
|
328
|
+
} else if (data === this.BUTTON_DOUBLE_CLICK || (data & 0x0040) !== 0) {
|
|
329
|
+
eventType = 'double_click';
|
|
330
|
+
} else if (data === this.BUTTON_TRIPLE_CLICK || (data & 0x0020) !== 0) {
|
|
331
|
+
eventType = 'triple_click';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
type: 'button',
|
|
336
|
+
panelAddr: frame.panelAddr,
|
|
337
|
+
buttonNumber: buttonNumber,
|
|
338
|
+
eventType: eventType,
|
|
339
|
+
state: state,
|
|
340
|
+
data: data,
|
|
341
|
+
raw: frame
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 检查是否是长按事件(寄存器0x2050)
|
|
346
|
+
if (frame.regAddr === this.REG_LONG_PRESS) {
|
|
347
|
+
const buttonNumber = frame.dataHigh; // 按键编号在高字节
|
|
348
|
+
const pressState = frame.dataLow; // 按下状态在低字节
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
type: 'button',
|
|
352
|
+
panelAddr: frame.panelAddr,
|
|
353
|
+
buttonNumber: buttonNumber,
|
|
354
|
+
eventType: pressState === 0xFE ? 'long_press_release' : 'long_press',
|
|
355
|
+
state: pressState !== 0xFE,
|
|
356
|
+
data: frame.data,
|
|
357
|
+
raw: frame
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 检查是否是感应信号(寄存器0x1040)
|
|
362
|
+
if (frame.regAddr === this.REG_SENSOR) {
|
|
363
|
+
const hasPresence = (frame.data & 0x0080) !== 0;
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
type: 'sensor',
|
|
367
|
+
panelAddr: frame.panelAddr,
|
|
368
|
+
sensorType: 'presence',
|
|
369
|
+
state: hasPresence,
|
|
370
|
+
data: frame.data,
|
|
371
|
+
raw: frame
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 判断是否是Clowire协议帧
|
|
380
|
+
* @param {Buffer} buffer - 接收到的数据
|
|
381
|
+
* @returns {boolean} 是否是Clowire协议
|
|
382
|
+
*/
|
|
383
|
+
isClowireFrame: function(buffer) {
|
|
384
|
+
if (!buffer || buffer.length < 9) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 检查帧尾是否是0xAA
|
|
389
|
+
const lastByte = buffer[buffer.length - 1];
|
|
390
|
+
if (lastByte !== this.FRAME_TAIL) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 检查命令码是否是Clowire支持的命令
|
|
395
|
+
const cmdCode = buffer[1];
|
|
396
|
+
const validCmds = [this.CMD_READ, this.CMD_WRITE, this.CMD_BUTTON_EVENT, this.CMD_BATCH_CONTROL];
|
|
397
|
+
if (!validCmds.includes(cmdCode)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 检查寄存器地址是否在Clowire范围内(0x1000-0x2FFF)
|
|
402
|
+
const regAddr = (buffer[2] << 8) | buffer[3];
|
|
403
|
+
if (regAddr < 0x1000 || regAddr > 0x2FFF) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
};
|
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`);
|