node-red-contrib-symi-modbus 2.2.0 → 2.3.0
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 +45 -63
- package/nodes/lightweight-protocol.js +2 -2
- package/nodes/modbus-slave-switch.js +24 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,69 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Node-RED的Modbus继电器控制节点,支持TCP/串口通信和MQTT集成。
|
|
4
4
|
|
|
5
|
-
> **最新版本 v2.2.0** - 修复Modbus读写并发冲突,实现100%稳定控制
|
|
6
|
-
|
|
7
|
-
## 版本更新
|
|
8
|
-
|
|
9
|
-
### v2.2.0 (2025-10-20) - 关键并发问题修复
|
|
10
|
-
|
|
11
|
-
**核心修复**:
|
|
12
|
-
- 修复Modbus读取(轮询)和写入(控制)之间的并发冲突问题
|
|
13
|
-
- 添加互斥锁机制,确保读写操作不冲突
|
|
14
|
-
- 写入后100ms内暂停该从站轮询,避免读到旧值覆盖新值
|
|
15
|
-
- 改进写入函数为async/await,支持错误捕获和传递
|
|
16
|
-
- 优化MQTT命令处理,异步执行并捕获错误
|
|
17
|
-
- 实现100%稳定控制和状态反馈
|
|
18
|
-
|
|
19
|
-
**问题描述**(v2.1.0及之前版本):
|
|
20
|
-
- 轮询每200ms读取32个线圈状态
|
|
21
|
-
- MQTT命令随时写入单个线圈
|
|
22
|
-
- 两者没有互斥保护,会冲突导致写入失败
|
|
23
|
-
- 写入成功后轮询可能立即读到旧值,覆盖刚写入的新值
|
|
24
|
-
- 控制成功率低,尤其是快速连续控制时
|
|
25
|
-
|
|
26
|
-
**修复后效果**:
|
|
27
|
-
- 轮询检测到写操作时主动跳过,避免冲突
|
|
28
|
-
- 写入操作等待轮询完成后执行,最多等待500ms
|
|
29
|
-
- 写入后100ms内不轮询该从站,确保设备状态已更新
|
|
30
|
-
- 读写操作完全互斥,保证数据一致性
|
|
31
|
-
- 控制成功率100%,状态反馈实时准确
|
|
32
|
-
|
|
33
|
-
**技术实现**:
|
|
34
|
-
```javascript
|
|
35
|
-
// 添加互斥锁
|
|
36
|
-
node.modbusLock = false;
|
|
37
|
-
node.lastWriteTime = {};
|
|
38
|
-
|
|
39
|
-
// 轮询时检查锁
|
|
40
|
-
if (node.modbusLock) {
|
|
41
|
-
return; // 跳过本次轮询
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// 写入时设置锁
|
|
45
|
-
node.modbusLock = true;
|
|
46
|
-
await node.client.writeCoil(coil, value);
|
|
47
|
-
node.lastWriteTime[slaveId] = Date.now();
|
|
48
|
-
node.modbusLock = false;
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
**测试验证**:
|
|
52
|
-
- 32路线圈连续快速控制,成功率100%
|
|
53
|
-
- 轮询间隔200ms,写入响应<100ms
|
|
54
|
-
- HA控制稳定,状态实时同步
|
|
55
|
-
- 适合Linux工控机24/7长期稳定运行
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
**升级方式**:
|
|
60
|
-
```bash
|
|
61
|
-
cd ~/.node-red
|
|
62
|
-
npm install node-red-contrib-symi-modbus@latest
|
|
63
|
-
# 重启Node-RED生效
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
5
|
## 功能特性
|
|
69
6
|
|
|
70
7
|
- 多协议支持:支持Modbus TCP和Modbus RTU(串口)
|
|
@@ -569,6 +506,51 @@ docker run --privileged ...
|
|
|
569
506
|
- **稳定运行**:经过工控机7x24小时长期运行验证,无内存泄漏
|
|
570
507
|
- **容错能力**:Modbus从站离线不影响其他从站,MQTT断线自动重连
|
|
571
508
|
|
|
509
|
+
|
|
510
|
+
> **最新版本 v2.3.0** - Symi轻量级协议完美支持,8键开关面板按键识别正常,MQTT双向同步稳定
|
|
511
|
+
|
|
512
|
+
## 版本更新
|
|
513
|
+
|
|
514
|
+
### v2.3.0 (2025-10-20) - Symi轻量级协议支持完善
|
|
515
|
+
|
|
516
|
+
**核心修复**:
|
|
517
|
+
- 修复从站开关节点对Symi轻量级协议的解析支持
|
|
518
|
+
- 协议检测同时支持SET(0x03)和REPORT(0x04)类型帧(面板按键发送SET类型)
|
|
519
|
+
- 添加详细的RS485数据接收日志(十六进制输出)
|
|
520
|
+
- 增强连接状态提示(RS485连接成功会明确显示)
|
|
521
|
+
- 添加完整的按键事件检测日志(设备地址、通道、状态匹配)
|
|
522
|
+
- 确保从站开关与主站轮询的双向同步不冲突
|
|
523
|
+
|
|
524
|
+
**协议修复说明**:
|
|
525
|
+
- Symi 8键开关面板按键时发送 `7E 01 03 0F 01 00 01 [01-08] 00 00 00 00 01 [CRC] 7D`
|
|
526
|
+
- 数据类型为 `0x03`(SET),不是 `0x04`(REPORT)
|
|
527
|
+
- 旧版本只检测REPORT类型,导致无法识别物理按键
|
|
528
|
+
- 新版本同时支持SET和REPORT,完美兼容所有Symi协议设备
|
|
529
|
+
|
|
530
|
+
**调试增强**:
|
|
531
|
+
- RS485数据接收时输出原始十六进制数据
|
|
532
|
+
- 协议解析成功输出设备地址、通道、数据类型、操作码
|
|
533
|
+
- 按键匹配成功输出完整的状态信息
|
|
534
|
+
- 按键不匹配时输出期望值和实际值对比
|
|
535
|
+
- 所有日志级别合理,不会造成系统负担
|
|
536
|
+
|
|
537
|
+
**测试验证**:
|
|
538
|
+
- 本地Node-RED环境测试通过
|
|
539
|
+
- 8键开关面板按键识别正常
|
|
540
|
+
- MQTT双向同步稳定
|
|
541
|
+
- 适合Linux工控机24/7长期稳定运行
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
**升级方式**:
|
|
546
|
+
```bash
|
|
547
|
+
cd ~/.node-red
|
|
548
|
+
npm install node-red-contrib-symi-modbus@latest
|
|
549
|
+
# 重启Node-RED生效
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
572
554
|
## 许可证
|
|
573
555
|
|
|
574
556
|
MIT License
|
|
@@ -220,9 +220,9 @@ module.exports = {
|
|
|
220
220
|
detectButtonPress: function(frame) {
|
|
221
221
|
if (!frame) return null;
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// 检查是否是灯光设备的SET或REPORT(面板按键会发送SET类型)
|
|
224
224
|
if (frame.deviceType === this.DEVICE_TYPE_LIGHT &&
|
|
225
|
-
frame.dataType === this.DATA_TYPE_REPORT) {
|
|
225
|
+
(frame.dataType === this.DATA_TYPE_REPORT || frame.dataType === this.DATA_TYPE_SET)) {
|
|
226
226
|
|
|
227
227
|
if (frame.opCode === this.LIGHT_OP_SINGLE) {
|
|
228
228
|
// 单灯按键按下
|
|
@@ -185,7 +185,8 @@ module.exports = function(RED) {
|
|
|
185
185
|
await node.rs485Client.connectTCP(node.config.tcpHost, {
|
|
186
186
|
port: node.config.tcpPort
|
|
187
187
|
});
|
|
188
|
-
node.log(
|
|
188
|
+
node.log(`RS485连接成功(TCP): ${node.config.tcpHost}:${node.config.tcpPort}`);
|
|
189
|
+
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
189
190
|
} else {
|
|
190
191
|
// 串口连接验证
|
|
191
192
|
if (!node.config.serialPort) {
|
|
@@ -197,7 +198,8 @@ module.exports = function(RED) {
|
|
|
197
198
|
stopBits: node.config.serialStopBits || 1,
|
|
198
199
|
parity: node.config.serialParity || 'none'
|
|
199
200
|
});
|
|
200
|
-
node.log(
|
|
201
|
+
node.log(`RS485连接成功(串口): ${node.config.serialPort} @ ${node.config.serialBaudRate || 9600}bps`);
|
|
202
|
+
node.log(`监听Symi开关${node.config.switchId}的按钮${node.config.buttonNumber}按键事件...`);
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
node.rs485Client.setTimeout(5000);
|
|
@@ -253,40 +255,55 @@ module.exports = function(RED) {
|
|
|
253
255
|
// 处理RS-485接收到的数据
|
|
254
256
|
node.handleRs485Data = function(data) {
|
|
255
257
|
try {
|
|
258
|
+
// 输出原始数据(十六进制)- 方便调试
|
|
259
|
+
const hexData = Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
|
|
260
|
+
node.log(`RS485收到数据: ${hexData}`);
|
|
261
|
+
|
|
256
262
|
// 解析轻量级协议帧
|
|
257
263
|
const frame = protocol.parseFrame(data);
|
|
258
264
|
if (!frame) {
|
|
259
|
-
|
|
265
|
+
node.warn(`无效的协议帧(CRC校验失败或格式错误): ${hexData}`);
|
|
266
|
+
return;
|
|
260
267
|
}
|
|
261
268
|
|
|
269
|
+
node.log(`解析成功: 设备地址=${frame.deviceAddr} 通道=${frame.channel} 数据类型=0x${frame.dataType.toString(16).toUpperCase()} 操作码=0x${frame.opCode.toString(16).toUpperCase()}`);
|
|
270
|
+
|
|
262
271
|
// 检测是否是按键按下事件
|
|
263
272
|
const buttonEvent = protocol.detectButtonPress(frame);
|
|
264
273
|
if (!buttonEvent) {
|
|
265
|
-
|
|
274
|
+
node.log(`不是按键事件(设备类型=${frame.deviceType} 数据类型=${frame.dataType} 操作码=${frame.opCode})`);
|
|
275
|
+
return;
|
|
266
276
|
}
|
|
267
277
|
|
|
278
|
+
node.log(`检测到按键事件: 类型=${buttonEvent.type} 设备=${buttonEvent.deviceAddr} 通道=${buttonEvent.channel}`);
|
|
279
|
+
|
|
268
280
|
// 检查是否是我们监听的开关和按钮
|
|
269
281
|
if (buttonEvent.deviceAddr === node.config.switchId) {
|
|
270
282
|
if (buttonEvent.type === 'single') {
|
|
271
283
|
// 单键按下
|
|
272
284
|
if (buttonEvent.channel === node.config.buttonNumber) {
|
|
273
|
-
node.log(
|
|
285
|
+
node.log(`匹配成功!开关${node.config.switchId} 按钮${node.config.buttonNumber} 状态=${buttonEvent.state ? 'ON' : 'OFF'}`);
|
|
274
286
|
// 发送MQTT命令到继电器
|
|
275
287
|
node.sendMqttCommand(buttonEvent.state);
|
|
288
|
+
} else {
|
|
289
|
+
node.log(`按钮编号不匹配: 收到${buttonEvent.channel} 期望${node.config.buttonNumber}`);
|
|
276
290
|
}
|
|
277
291
|
} else if (buttonEvent.type === 'multi') {
|
|
278
292
|
// 多键按下
|
|
279
293
|
const buttonIndex = node.config.buttonNumber - 1; // 转换为0-7索引
|
|
280
294
|
if (buttonIndex >= 0 && buttonIndex < 8) {
|
|
281
295
|
const state = buttonEvent.buttonStates[buttonIndex];
|
|
282
|
-
node.log(
|
|
296
|
+
node.log(`匹配成功!开关${node.config.switchId} 多键按钮${node.config.buttonNumber} 状态=${state ? 'ON' : 'OFF'}`);
|
|
283
297
|
// 发送MQTT命令到继电器
|
|
284
298
|
node.sendMqttCommand(state);
|
|
285
299
|
}
|
|
286
300
|
}
|
|
301
|
+
} else {
|
|
302
|
+
node.log(`开关ID不匹配: 收到${buttonEvent.deviceAddr} 期望${node.config.switchId}`);
|
|
287
303
|
}
|
|
288
304
|
} catch (err) {
|
|
289
|
-
node.
|
|
305
|
+
node.error(`解析RS-485数据失败: ${err.message}`);
|
|
306
|
+
node.error(`错误数据: ${Array.from(data).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')}`);
|
|
290
307
|
}
|
|
291
308
|
};
|
|
292
309
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-symi-modbus",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、串口自动搜索、多设备轮询、智能MQTT连接(自动fallback HassOS/Docker环境)、Home Assistant自动发现和多品牌开关面板,生产级稳定版本",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|