node-red-contrib-symi-modbus 2.9.10 → 2.9.11
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 +10 -2
- package/nodes/modbus-master.js +83 -57
- package/nodes/modbus-slave-switch.js +159 -73
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -952,10 +952,18 @@ HomeKit网桥节点无需输入消息,自动同步主站配置和状态。
|
|
|
952
952
|
|
|
953
953
|
## 版本信息
|
|
954
954
|
|
|
955
|
-
**当前版本**: v2.9.
|
|
955
|
+
**当前版本**: v2.9.11 (2026-01-09)
|
|
956
|
+
|
|
957
|
+
**v2.9.11 更新内容**:
|
|
958
|
+
- **串口数据拼包优化 (解决工控机分包问题)**:
|
|
959
|
+
- 在 `modbus-master` 和 `modbus-slave-switch` 节点中引入了协议级拼包缓冲区 `serialBuffer` / `rs485Buffer`。
|
|
960
|
+
- 针对工业机硬件 UART FIFO 触发阈值(如 8 字节)导致的数据断裂,实现了自动拼包解析。
|
|
961
|
+
- 逻辑支持:自动寻找帧头、动态识别长度字段、跨包拼接、CRC/校验和验证。
|
|
962
|
+
- 兼容协议:亖米 (Symi) 私有协议、Clowire (克伦威尔) 485 协议、Mesh 协议。
|
|
963
|
+
- 确保在任何硬件环境下(尤其是工控机)都能 100% 稳定识别从站开关、传感器等 485 设备。
|
|
956
964
|
|
|
957
965
|
**v2.9.10 更新内容**:
|
|
958
|
-
-
|
|
966
|
+
- **日志系统优化 (解决日志占用问题)**:
|
|
959
967
|
- **彻底静默重连日志**:将 TCP/串口连接过程中的 `node.error` 降级为 `node.log` 或 `node.debug`,不再发送到 Node-RED 调试面板(Debug Tab),彻底解决重连期间日志刷屏问题。
|
|
960
968
|
- **智能过滤**:相同的连接错误在重试期间不再重复输出日志(每 10 分钟仅后台提醒一次)。
|
|
961
969
|
- **状态栏增强**:将具体错误信息(如“拒绝连接”、“串口不存在”)直接显示在节点状态文字中,无需查看日志即可掌握连接状况。
|
package/nodes/modbus-master.js
CHANGED
|
@@ -1161,73 +1161,99 @@ module.exports = function(RED) {
|
|
|
1161
1161
|
// 这样可以确保每个从站都按照正确的间隔轮询
|
|
1162
1162
|
};
|
|
1163
1163
|
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1164
|
+
// 初始化拼包缓冲区
|
|
1165
|
+
node.serialBuffer = Buffer.alloc(0);
|
|
1166
|
+
|
|
1167
|
+
// 处理Symi按键事件(私有协议)- 增加拼包支持
|
|
1166
1168
|
node.handleSymiButtonEvent = function(data) {
|
|
1167
1169
|
try {
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
if (!frame) {
|
|
1171
|
-
// 不是有效的Symi帧(CRC校验失败或格式错误),静默忽略
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1170
|
+
// 将新数据拼接到缓冲区
|
|
1171
|
+
node.serialBuffer = Buffer.concat([node.serialBuffer, data]);
|
|
1174
1172
|
|
|
1175
|
-
//
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
// 不是按键事件,静默忽略
|
|
1179
|
-
return;
|
|
1173
|
+
// 限制缓冲区大小,防止异常情况下内存溢出(最大1KB)
|
|
1174
|
+
if (node.serialBuffer.length > 1024) {
|
|
1175
|
+
node.serialBuffer = node.serialBuffer.slice(-1024);
|
|
1180
1176
|
}
|
|
1181
1177
|
|
|
1182
|
-
//
|
|
1183
|
-
|
|
1184
|
-
//
|
|
1185
|
-
|
|
1186
|
-
|
|
1178
|
+
// 循环处理缓冲区中的所有完整帧
|
|
1179
|
+
while (node.serialBuffer.length >= 15) {
|
|
1180
|
+
// 查找帧头 0x7E
|
|
1181
|
+
const startIndex = node.serialBuffer.indexOf(0x7E);
|
|
1182
|
+
if (startIndex === -1) {
|
|
1183
|
+
// 没找到帧头,清空缓冲区
|
|
1184
|
+
node.serialBuffer = Buffer.alloc(0);
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
1187
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
// 查找对应的从站和线圈
|
|
1194
|
-
// 假设:设备地址1对应从站10,设备地址2对应从站11,以此类推
|
|
1195
|
-
// 通道号直接对应线圈号(1-8 → 0-7)
|
|
1196
|
-
const slaveId = 10 + (deviceAddr - 1);
|
|
1197
|
-
const coilNumber = channel - 1;
|
|
1198
|
-
|
|
1199
|
-
// 检查从站是否在配置中
|
|
1200
|
-
const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
|
|
1201
|
-
if (!slaveConfig) {
|
|
1202
|
-
// 从站未配置,静默忽略(不是本节点的数据)
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1188
|
+
// 如果帧头不在开始位置,丢弃前面的垃圾数据
|
|
1189
|
+
if (startIndex > 0) {
|
|
1190
|
+
node.serialBuffer = node.serialBuffer.slice(startIndex);
|
|
1191
|
+
}
|
|
1205
1192
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1193
|
+
// 再次检查剩余长度是否足够解析长度字段(至少4字节)
|
|
1194
|
+
if (node.serialBuffer.length < 4) break;
|
|
1195
|
+
|
|
1196
|
+
// 获取协议声明的整帧长度(第4个字节)
|
|
1197
|
+
const frameLen = node.serialBuffer[3];
|
|
1198
|
+
|
|
1199
|
+
// 验证长度合理性 (Symi协议最小15字节,最大一般不超过64)
|
|
1200
|
+
if (frameLen < 15 || frameLen > 64) {
|
|
1201
|
+
// 长度非法,丢弃这个错误的帧头,继续找下一个
|
|
1202
|
+
node.serialBuffer = node.serialBuffer.slice(1);
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1211
1205
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1206
|
+
// 检查缓冲区数据是否已经达到完整帧长度
|
|
1207
|
+
if (node.serialBuffer.length < frameLen) {
|
|
1208
|
+
// 数据还没到齐,跳出循环等待下一波数据
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1215
1211
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1212
|
+
// 截取完整帧进行解析
|
|
1213
|
+
const completeFrame = node.serialBuffer.slice(0, frameLen);
|
|
1214
|
+
|
|
1215
|
+
// 移除缓冲区中已处理的部分
|
|
1216
|
+
node.serialBuffer = node.serialBuffer.slice(frameLen);
|
|
1217
|
+
|
|
1218
|
+
// 执行解析逻辑
|
|
1219
|
+
const frame = protocol.parseFrame(completeFrame);
|
|
1220
|
+
if (!frame) continue;
|
|
1221
|
+
|
|
1222
|
+
// 只处理SET类型(0x03)的按键事件
|
|
1223
|
+
if (frame.dataType !== 0x03) continue;
|
|
1224
|
+
if (frame.deviceType !== 0x01) continue;
|
|
1225
|
+
|
|
1226
|
+
const deviceAddr = frame.deviceAddr;
|
|
1227
|
+
const channel = frame.channel;
|
|
1228
|
+
const state = frame.opInfo[0] === 0x01;
|
|
1220
1229
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
node.
|
|
1230
|
-
node.
|
|
1230
|
+
const slaveId = 10 + (deviceAddr - 1);
|
|
1231
|
+
const coilNumber = channel - 1;
|
|
1232
|
+
|
|
1233
|
+
const slaveConfig = node.config.slaves.find(s => s.address === slaveId);
|
|
1234
|
+
if (!slaveConfig) continue;
|
|
1235
|
+
|
|
1236
|
+
if (coilNumber < slaveConfig.coilStart || coilNumber > slaveConfig.coilEnd) continue;
|
|
1237
|
+
|
|
1238
|
+
node.debug(`Symi按键事件(拼包成功): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1239
|
+
node.debug(`Symi按键映射: 设备${deviceAddr}通道${channel} → 从站${slaveId}线圈${coilNumber}`);
|
|
1240
|
+
|
|
1241
|
+
// 写入线圈(异步,不阻塞)
|
|
1242
|
+
node.writeSingleCoil(slaveId, coilNumber, state).catch(err => {
|
|
1243
|
+
node.log(`Symi按键控制失败: ${err.message}`);
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// 发送应答帧(REPORT类型0x04,反馈LED状态)
|
|
1247
|
+
// 协议规定:面板发送0x03(SET)按键事件,主机应该用0x04(REPORT)反馈LED状态
|
|
1248
|
+
const responseFrame = protocol.buildSingleLightReport(0x01, deviceAddr, channel, state);
|
|
1249
|
+
if (node.client._port && node.client._port.write) {
|
|
1250
|
+
node.client._port.write(responseFrame);
|
|
1251
|
+
node.log(`Symi应答已发送(REPORT): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1252
|
+
} else if (node.client._client && node.client._client.write) {
|
|
1253
|
+
// TCP模式
|
|
1254
|
+
node.client._client.write(responseFrame);
|
|
1255
|
+
node.log(`Symi应答已发送(REPORT/TCP): 设备${deviceAddr} 通道${channel} 状态=${state ? 'ON' : 'OFF'}`);
|
|
1256
|
+
}
|
|
1231
1257
|
}
|
|
1232
1258
|
|
|
1233
1259
|
} catch (err) {
|
|
@@ -399,6 +399,9 @@ module.exports = function(RED) {
|
|
|
399
399
|
node.lastMqttErrorLog = 0; // MQTT错误日志时间
|
|
400
400
|
node.errorLogInterval = 10 * 60 * 1000; // 错误日志间隔:10分钟
|
|
401
401
|
|
|
402
|
+
// RS-485 拼包缓冲区(解决工控机分包问题)
|
|
403
|
+
node.rs485Buffer = Buffer.alloc(0);
|
|
404
|
+
|
|
402
405
|
// Mesh模式状态缓存
|
|
403
406
|
node.meshCurrentStates = null; // Mesh设备当前状态(用于保持其他路不变)
|
|
404
407
|
|
|
@@ -791,102 +794,185 @@ module.exports = function(RED) {
|
|
|
791
794
|
};
|
|
792
795
|
|
|
793
796
|
// 处理RS-485接收到的数据(支持TCP粘包处理)
|
|
797
|
+
// 处理从串口接收到的RS-485原始数据
|
|
794
798
|
node.handleRs485Data = function(data) {
|
|
795
799
|
try {
|
|
796
|
-
//
|
|
797
|
-
|
|
798
|
-
node.handleMeshData(data);
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
800
|
+
// 将新数据拼接到缓冲区
|
|
801
|
+
node.rs485Buffer = Buffer.concat([node.rs485Buffer, data]);
|
|
801
802
|
|
|
802
|
-
//
|
|
803
|
-
if (node.
|
|
804
|
-
node.
|
|
805
|
-
return;
|
|
803
|
+
// 限制缓冲区大小,防止异常情况下内存溢出(最大1KB)
|
|
804
|
+
if (node.rs485Buffer.length > 1024) {
|
|
805
|
+
node.rs485Buffer = node.rs485Buffer.slice(-1024);
|
|
806
806
|
}
|
|
807
807
|
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
808
|
+
// 根据品牌/类型选择不同的拼包解析逻辑
|
|
809
|
+
if (node.config.buttonType === 'mesh') {
|
|
810
|
+
// Mesh 协议拼包逻辑
|
|
811
|
+
// 最小帧长约5字节: [53][op][sub][len]...[check]
|
|
812
|
+
while (node.rs485Buffer.length >= 5) {
|
|
813
|
+
const headerIndex = node.rs485Buffer.indexOf(0x53);
|
|
814
|
+
if (headerIndex === -1) {
|
|
815
|
+
node.rs485Buffer = Buffer.alloc(0);
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
if (headerIndex > 0) {
|
|
819
|
+
node.rs485Buffer = node.rs485Buffer.slice(headerIndex);
|
|
820
|
+
}
|
|
821
|
+
if (node.rs485Buffer.length < 4) break;
|
|
813
822
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
823
|
+
const dataLen = node.rs485Buffer[3];
|
|
824
|
+
const totalLen = 4 + dataLen + 1; // [53][op][sub][len] + [data...] + [check]
|
|
825
|
+
|
|
826
|
+
if (totalLen > 64) { // 非法长度
|
|
827
|
+
node.rs485Buffer = node.rs485Buffer.slice(1);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (node.rs485Buffer.length < totalLen) break; // 数据未到齐
|
|
821
832
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
833
|
+
const completeFrame = node.rs485Buffer.slice(0, totalLen);
|
|
834
|
+
node.rs485Buffer = node.rs485Buffer.slice(totalLen);
|
|
835
|
+
|
|
836
|
+
// 处理解析出来的完整Mesh帧
|
|
837
|
+
node.handleMeshData(completeFrame);
|
|
826
838
|
}
|
|
839
|
+
} else if (node.config.switchBrand === 'clowire') {
|
|
840
|
+
// Clowire 协议拼包逻辑 (克伦威尔)
|
|
841
|
+
// 特点:以 0xAA 结尾,长度通常为 9 或 11 字节
|
|
842
|
+
while (node.rs485Buffer.length >= 9) {
|
|
843
|
+
const endIndex = node.rs485Buffer.indexOf(0xAA);
|
|
844
|
+
if (endIndex === -1) {
|
|
845
|
+
// 如果缓冲区太长且没找到结尾,保留一部分可能的数据
|
|
846
|
+
if (node.rs485Buffer.length > 32) {
|
|
847
|
+
node.rs485Buffer = node.rs485Buffer.slice(-16);
|
|
848
|
+
}
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
827
851
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
852
|
+
// 尝试匹配可能的长度
|
|
853
|
+
let foundFrame = false;
|
|
854
|
+
const possibleLengths = [9, 11];
|
|
855
|
+
|
|
856
|
+
for (const frameLen of possibleLengths) {
|
|
857
|
+
const startIndex = endIndex - frameLen + 1;
|
|
858
|
+
if (startIndex >= 0) {
|
|
859
|
+
const completeFrame = node.rs485Buffer.slice(startIndex, endIndex + 1);
|
|
860
|
+
if (clowireProtocol.isClowireFrame(completeFrame)) {
|
|
861
|
+
node.rs485Buffer = node.rs485Buffer.slice(endIndex + 1);
|
|
862
|
+
node.handleClowireData(completeFrame);
|
|
863
|
+
foundFrame = true;
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (foundFrame) continue;
|
|
870
|
+
|
|
871
|
+
// 没找到有效帧,跳过当前的 0xAA
|
|
872
|
+
node.rs485Buffer = node.rs485Buffer.slice(endIndex + 1);
|
|
843
873
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
874
|
+
} else {
|
|
875
|
+
// 亖米 (Symi) 协议拼包逻辑
|
|
876
|
+
// 格式: 7E [addr] [type] [len] ... [tail:7D]
|
|
877
|
+
while (node.rs485Buffer.length >= 15) {
|
|
878
|
+
const headerIndex = node.rs485Buffer.indexOf(0x7E);
|
|
879
|
+
if (headerIndex === -1) {
|
|
880
|
+
node.rs485Buffer = Buffer.alloc(0);
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
if (headerIndex > 0) {
|
|
884
|
+
node.rs485Buffer = node.rs485Buffer.slice(headerIndex);
|
|
885
|
+
}
|
|
886
|
+
if (node.rs485Buffer.length < 4) break;
|
|
856
887
|
|
|
857
|
-
|
|
858
|
-
if (
|
|
859
|
-
|
|
888
|
+
const frameLen = node.rs485Buffer[3];
|
|
889
|
+
if (frameLen < 15 || frameLen > 64) {
|
|
890
|
+
node.rs485Buffer = node.rs485Buffer.slice(1);
|
|
891
|
+
continue;
|
|
860
892
|
}
|
|
861
|
-
globalDebounceCache.set(debounceKey, now);
|
|
862
893
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
894
|
+
if (node.rs485Buffer.length < frameLen) break;
|
|
895
|
+
|
|
896
|
+
const completeFrame = node.rs485Buffer.slice(0, frameLen);
|
|
897
|
+
node.rs485Buffer = node.rs485Buffer.slice(frameLen);
|
|
898
|
+
|
|
899
|
+
// 验证尾部
|
|
900
|
+
if (completeFrame[completeFrame.length - 1] !== 0x7D) {
|
|
901
|
+
// 尾部不匹配,说明不是有效帧
|
|
902
|
+
continue;
|
|
866
903
|
}
|
|
867
904
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
// 场景模式:立即发送LED反馈(修复:不等待状态变化事件)
|
|
875
|
-
// 因为currentState已经更新,后续的coilStateChanged事件会被认为"状态未变化"而跳过
|
|
876
|
-
node.sendCommandToPanel(node.currentState);
|
|
877
|
-
} else {
|
|
878
|
-
// 开关模式:根据状态发送ON/OFF
|
|
879
|
-
node.debug(`开关${buttonEvent.state ? 'ON' : 'OFF'}: 面板${node.config.switchId} 按键${node.config.buttonNumber}`);
|
|
880
|
-
node.sendMqttCommand(buttonEvent.state);
|
|
905
|
+
// 亖米协议:使用parseAllFrames处理粘包,解析所有帧
|
|
906
|
+
const frames = protocol.parseAllFrames(completeFrame);
|
|
907
|
+
if (frames && frames.length > 0) {
|
|
908
|
+
for (const frame of frames) {
|
|
909
|
+
node.processSymiFrame(frame);
|
|
910
|
+
}
|
|
881
911
|
}
|
|
882
912
|
}
|
|
883
|
-
// 不匹配的节点静默忽略,不输出任何日志
|
|
884
913
|
}
|
|
885
914
|
} catch (err) {
|
|
886
915
|
node.log(`解析RS-485数据失败: ${err.message}`);
|
|
887
916
|
}
|
|
888
917
|
};
|
|
889
918
|
|
|
919
|
+
// 提取原 handleRs485Data 中的亖米协议处理逻辑
|
|
920
|
+
node.processSymiFrame = function(frame) {
|
|
921
|
+
try {
|
|
922
|
+
// 忽略 REPORT (0x04) 类型的帧
|
|
923
|
+
if (frame.dataType === 0x04) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 检测是否是按键按下事件
|
|
928
|
+
const buttonEvent = protocol.detectButtonPress(frame);
|
|
929
|
+
if (!buttonEvent) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 计算实际按键编号
|
|
934
|
+
const actualButtonNumber = (buttonEvent.deviceAddr === 1) ? buttonEvent.channel : (buttonEvent.deviceAddr * 4 - 4 + buttonEvent.channel);
|
|
935
|
+
|
|
936
|
+
if (buttonEvent.channel === 0x0F && node.config.buttonNumber !== 15) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (buttonEvent.raw.localAddr === node.config.switchId && actualButtonNumber === node.config.buttonNumber) {
|
|
941
|
+
// 判断按钮类型
|
|
942
|
+
const isSceneMode = buttonEvent.isSceneMode ||
|
|
943
|
+
node.config.buttonType === 'scene' ||
|
|
944
|
+
buttonEvent.deviceType === 0x07;
|
|
945
|
+
|
|
946
|
+
// 全局防抖
|
|
947
|
+
const debounceKey = `${node.config.switchId}-${node.config.buttonNumber}-${node.config.targetSlaveAddress}`;
|
|
948
|
+
const now = Date.now();
|
|
949
|
+
const lastTriggerTime = globalDebounceCache.get(debounceKey) || 0;
|
|
950
|
+
|
|
951
|
+
if (now - lastTriggerTime < 200) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
globalDebounceCache.set(debounceKey, now);
|
|
955
|
+
|
|
956
|
+
// 设置触发源
|
|
957
|
+
if (node.serialPortConfig && typeof node.serialPortConfig.setTriggerSource === 'function') {
|
|
958
|
+
node.serialPortConfig.setTriggerSource(node.config.switchId);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (isSceneMode) {
|
|
962
|
+
node.debug(`场景触发: 面板${node.config.switchId} 按键${node.config.buttonNumber} (opInfo=0x${buttonEvent.opInfo.toString(16).toUpperCase()})`);
|
|
963
|
+
node.currentState = !node.currentState;
|
|
964
|
+
node.sendMqttCommand(node.currentState);
|
|
965
|
+
node.sendCommandToPanel(node.currentState);
|
|
966
|
+
} else {
|
|
967
|
+
node.debug(`开关${buttonEvent.state ? 'ON' : 'OFF'}: 面板${node.config.switchId} 按键${node.config.buttonNumber}`);
|
|
968
|
+
node.sendMqttCommand(buttonEvent.state);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch (err) {
|
|
972
|
+
node.log(`处理亖米协议帧失败: ${err.message}`);
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
890
976
|
// 处理Clowire协议数据
|
|
891
977
|
node.handleClowireData = function(data) {
|
|
892
978
|
try {
|
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.11",
|
|
4
4
|
"description": "Node-RED Modbus节点,支持TCP/串口通信、多主站独立运行、串口自动搜索、多设备轮询、可选MQTT集成(支持纯本地模式和MQTT模式)、Home Assistant自动发现、HomeKit网桥、可视化控制看板、自定义协议转换和物理开关面板双向同步(支持亖米/Clowire品牌),工控机长期稳定运行",
|
|
5
5
|
"main": "nodes/modbus-master.js",
|
|
6
6
|
"scripts": {
|