diff --git a/Modbus/ModbusMatser.pro b/Modbus/ModbusMatser.pro new file mode 100644 index 0000000..fdc83c4 --- /dev/null +++ b/Modbus/ModbusMatser.pro @@ -0,0 +1,38 @@ +QT += core gui +QT += serialport +QT += core +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++11 + +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + modbus_master.cpp \ + serial_communication.cpp \ + timeout_handler.cpp + +HEADERS += \ + mainwindow.h \ + modbus_master.h \ + serial_communication.h \ + timeout_handler.h + +FORMS += \ + mainwindow.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/Modbus/ModbusMatser.pro.user b/Modbus/ModbusMatser.pro.user new file mode 100644 index 0000000..a61f7c8 --- /dev/null +++ b/Modbus/ModbusMatser.pro.user @@ -0,0 +1,319 @@ + + + + + + EnvironmentId + {54b998bd-0b43-4b57-8d79-23e6237f0a57} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + -fno-delayed-template-parsing + + true + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.14.2 MinGW 64-bit + Desktop Qt 5.14.2 MinGW 64-bit + qt.qt5.5142.win64_mingw73_kit + 0 + 0 + 0 + + D:/QT/QTProject/build-ModbusMatser-Desktop_Qt_5_14_2_MinGW_64_bit-Debug + + + true + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + + + D:/QT/QTProject/build-ModbusMatser-Desktop_Qt_5_14_2_MinGW_64_bit-Release + + + true + QtProjectManager.QMakeBuildStep + false + + false + false + true + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + + + D:/QT/QTProject/build-ModbusMatser-Desktop_Qt_5_14_2_MinGW_64_bit-Profile + + + true + QtProjectManager.QMakeBuildStep + true + + false + true + true + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + Qt4ProjectManager.Qt4RunConfiguration:D:/QT/QTProject/ModbusMatser/ModbusMatser.pro + D:/QT/QTProject/ModbusMatser/ModbusMatser.pro + + false + + false + true + true + false + false + true + + D:/QT/QTProject/build-ModbusMatser-Desktop_Qt_5_14_2_MinGW_64_bit-Debug + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/Modbus/include/mainwindow.h b/Modbus/include/mainwindow.h new file mode 100644 index 0000000..c000ae2 --- /dev/null +++ b/Modbus/include/mainwindow.h @@ -0,0 +1,103 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: mainwindow.h +* Description: Modbus主站的人机交互界面头文件,包含其成员变量与成员函数声明 +* Others: +* Version: v1.0 +* Author: weikai XINJE +* Date: 2025-7-30 +***********************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include "serial_communication.h" +#include "modbus_master.h" + +namespace Ui +{ +class MainWindow; +} + +const int DEFAULT_TIMEOUT = 1000; //默认超时时间 +const int DEFAULT_MAXRETRISE = 3; //默认最大重发次数 + +/******************************************************************** +* Iterates over the contents of a MainWindow. +*人机交互界面: +*通过SerialCommunicator实例serialComm实现串口通信 +*通过ModbusRTUMaster实例modbusMaster实现Modbus请求帧生成与响应帧解析 +*通过QTimer实例serialPortTimer实现串口下拉框定时刷新 +***********************************************************************/ +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + + +private slots: + // UI按钮槽函数 + void onConnectButtonClicked();//连接按钮 + void onDisconnectButtonClicked();//断开连接按钮 + void onReadButtonClicked();//读取按钮 + void onWriteButtonClicked();//写入按钮 + void onClearLogButtonClicked();//清空日志框 + void onExportHistoryButtonClicked();//导出通信历史 + + // 串口通信信号处理槽 + /*********************************************************************** + * @brief : 串口接收数据到达信号的处理槽函数 + * @param : data 串口接收到的数据字节流 + ***********************************************************************/ + void onSerialDataReceived(const QString &data); + + /*********************************************************************** + * @brief : 串口接收状态变化信号的处理槽函数 + * @param : 串口status状态变化的描述字符串 + ***********************************************************************/ + void onSerialStatusChanged(const QString &status); + + /*********************************************************************** + * @brief : 串口接收错误发生信号的处理槽函数 + * @param : error 串口错误的描述字符串 + ***********************************************************************/ + void onSerialErrorOccurred(const QString &error); + + //断开连接信号处理槽 + void onConnectionDisconnected(); + + // 定时刷新槽函数 + void onRefreshSerialPorts(); + +private: + //界面初始化 + void init(); + + /*********************************************************************** + * @brief : 日志输出函数 + * @param : message 日志字符串 + ***********************************************************************/ + void logToBox(const QString &message); + + /*********************************************************************** + * @brief : 刷新串口下拉框 + ***********************************************************************/ + void refreshSerialPorts(); + + Ui::MainWindow *ui_; + SerialCommunicator *serialComm_; // 串口通信实例 + ModbusRTUMaster *modbusMaster_;//Modbus协议实例 + QTimer *serialPortTimer_;//用于定时刷新可用串口下拉栏的定时器 +}; + +#endif // MAINWINDOW_H + + + diff --git a/Modbus/include/mainwindow.ui b/Modbus/include/mainwindow.ui new file mode 100644 index 0000000..88a7c1d --- /dev/null +++ b/Modbus/include/mainwindow.ui @@ -0,0 +1,716 @@ + + + MainWindow + + + + 0 + 0 + 815 + 606 + + + + MainWindow + + + + + + 10 + 240 + 381 + 311 + + + + + 楷体 + 12 + + + + + + + + + 10 + 120 + 91 + 31 + + + + 起始地址: + + + + + + 190 + 110 + 91 + 41 + + + + + 楷体 + 12 + + + + 读取数量: + + + + + + 10 + 70 + 91 + 20 + + + + + 楷体 + 12 + + + + 从站地址: + + + + + + 200 + 60 + 81 + 31 + + + + + 楷体 + 12 + + + + 功能码: + + + + + + 270 + 60 + 101 + 31 + + + + + 楷体 + 11 + + + + + 01读线圈 + + + + + 03读寄存器 + + + + + 0F写线圈 + + + + + 10写寄存器 + + + + + + + 0 + 260 + 381 + 41 + + + + + + + 260 + 180 + 93 + 41 + + + + 写入 + + + + + + 90 + 180 + 93 + 41 + + + + 读取 + + + + + + 280 + 120 + 90 + 31 + + + + + 黑体 + 12 + + + + 1000 + + + + + + 100 + 120 + 91 + 31 + + + + 20000 + + + + + + 100 + 60 + 91 + 31 + + + + + 黑体 + 12 + + + + 1000 + + + + + + 0 + 0 + 101 + 31 + + + + Modbus配置 + + + + + + 0 + 230 + 91 + 31 + + + + 写入数据 + + + + + + + 400 + 310 + 401 + 241 + + + + + 楷体 + 12 + + + + + + + + + 0 + 30 + 391 + 201 + + + + + 微软雅黑 Light + 10 + + + + + + + 300 + 0 + 93 + 31 + + + + 清空 + + + + + + 0 + 0 + 81 + 21 + + + + 日志框 + + + + + + + 10 + 10 + 381 + 231 + + + + + 楷体 + 12 + + + + 通信配置 + + + + + 120 + 70 + 70 + 30 + + + + + + + 280 + 70 + 70 + 30 + + + + + 黑体 + 10 + + + + + + + 280 + 110 + 70 + 30 + + + + + 黑体 + 10 + + + + + + + 40 + 120 + 85 + 23 + + + + + 楷体 + 12 + + + + 波特率: + + + + + + 120 + 110 + 70 + 30 + + + + + 黑体 + 10 + + + + + 4800 + + + + + 9600 + + + + + 19200 + + + + + 38400 + + + + + 115200 + + + + + + + 260 + 190 + 93 + 31 + + + + 断开连接 + + + + + + 110 + 190 + 93 + 31 + + + + 连接 + + + + + + 120 + 30 + 231 + 31 + + + + + 楷体 + 10 + + + + + + + 30 + 30 + 85 + 31 + + + + + 楷体 + 12 + + + + 通信接口: + + + + + + 210 + 70 + 68 + 23 + + + + + 楷体 + 12 + + + + 数据位: + + + + + + 30 + 80 + 85 + 23 + + + + + 楷体 + 12 + + + + 奇偶校验: + + + + + + 210 + 110 + 68 + 23 + + + + + 楷体 + 12 + + + + 停止位: + + + + + + 120 + 150 + 71 + 21 + + + + + 黑体 + 9 + + + + 5000 + + + + + + 280 + 150 + 71 + 21 + + + + + 黑体 + 9 + + + + 10 + + + + + + 0 + 150 + 121 + 23 + + + + + 楷体 + 12 + + + + 超时时间(ms): + + + + + + 190 + 150 + 101 + 23 + + + + + 楷体 + 12 + + + + 重发次数: + + + + + + + 400 + 0 + 401 + 301 + + + + + + + + + 0 + 40 + 391 + 261 + + + + + 10 + + + + + + + 0 + 0 + 81 + 31 + + + + + 楷体 + 12 + + + + 通信历史 + + + + + + 300 + 10 + 93 + 31 + + + + 导出历史 + + + + groupBox + groupBox_3 + groupBox_4 + groupBox_2 + + + + + 0 + 0 + 815 + 26 + + + + + 主站界面 + + + + + + + + + diff --git a/Modbus/include/modbus_master.h b/Modbus/include/modbus_master.h new file mode 100644 index 0000000..0b40449 --- /dev/null +++ b/Modbus/include/modbus_master.h @@ -0,0 +1,178 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: modbus_master.h +* Description: Modbus主站的ModbusRTUMaster头文件,主要负责四种请求帧的生成:(01)(03)(0F)(10)、响应帧的解析。 +* Others: +* Version: v1.0 +* Author: weikai XINJE +* Date: 2025-7-30 +***********************************************************************/ + + + +#ifndef MODBUS_RTU_MASTER_H +#define MODBUS_RTU_MASTER_H + +#include +#include +#include +#include + +const int TYPE_REGISTERS = 1; +const int TYPE_COILS = 2; + +/********************************************************************** +* Iterates over the contents of a ModbusRTUMaster. +*ModbusRTUMaster: +*提供Modbus RTU协议的主站功能,支持生成各种Modbus RTU请求帧, +*以及解析从站返回的响应帧。所有协议相关参数均作为类成员变量,可通过setter方法进行设置。 +***********************************************************************/ +class ModbusRTUMaster : public QObject +{ + Q_OBJECT +public: + //构造函数 + explicit ModbusRTUMaster(QObject *parent = nullptr); + + //功能码枚举 - 定义Modbus协议支持的功能码 + enum FunctionCode + { + READ_COILS = 0x01, // 读线圈状态 + READ_HOLDING_REGISTERS = 0x03, // 读保持寄存器 + WRITE_MULTIPLE_COILS = 0x0F, // 写多个线圈 + WRITE_MULTIPLE_REGISTERS = 0x10 // 写多个保持寄存器 + }; + + //错误码枚举 - 定义Modbus协议可能返回的错误码 + enum ErrorCode + { + NO_ERROR = 0x00, // 无错误 + ILLEGAL_FUNCTION = 0x01, // 非法功能 - 从站不支持该功能码 + ILLEGAL_DATA_ADDRESS = 0x02, // 非法数据地址 - 从站不支持该地址 + ILLEGAL_DATA_VALUE = 0x03, // 非法数据值 - 数据值超出范围 + SERVER_DEVICE_FAILURE = 0x04,// 从站设备故障 + ACKNOWLEDGE = 0x05, // 确认 - 请求已接收但未处理 + SERVER_DEVICE_BUSY = 0x06, // 从站设备忙 - 无法处理请求 + MEMORY_PARITY_ERROR = 0x08 // 内存奇偶性错误 + }; + + + //设置从站地址 + void setSlaveAddr(quint8 slaveAddr) { slaveAddr_ = slaveAddr; } + + //设置功能码 + void setFuncCode(FunctionCode funcCode) { funcCode_ = funcCode; } + + //设置起始地址 + void setStartAddr(quint16 startAddr) { startAddr_ = startAddr; } + + //设置读取数量 + void setReadCount(quint16 readCount) { readCount_ = readCount; } + + //设置线圈数据 + void setCoils(const QVector& coils) { coils_ = coils; } + + //设置寄存器数据 + void setRegisters(const QVector& registers) { registers_ = registers; } + + //提取错误码对应描述 + QString getErrorDescription(int errorCode); + + //获取起始地址 + quint16 getStartAddr() const; + + //生成读线圈请求帧 (功能码01) + QByteArray createReadCoilsFrame(); + + //生成读保持寄存器请求帧 (功能码03) + QByteArray createReadHoldingRegistersFrame(); + + //生成写多个线圈请求帧 (功能码0F) + QByteArray createWriteMultipleCoilsFrame(); + + //生成写多个保持寄存器请求帧 (功能码10) + QByteArray createWriteMultipleRegistersFrame(); + + /*********************************************************************** + *@brief: 处理接收到的数据流并解析完整帧 + *@param: data 接收到的原始数据 + *@param: slaveAddr 输出参数,从响应中解析出的从站地址 + *@param: funcCode 输出参数,从响应中解析出的功能码 + *@param: parsedData 输出参数,解析后的数据 + *@param: errorCode 输出参数,错误码(NoError表示成功) + *@param: extractedFrame 输出参数,提取到的完整帧 + *@return:是否成功解析 + *@note: 提取到帧后会进行解析 + ***********************************************************************/ + bool processReceivedData(const QByteArray& data, quint8& slaveAddr, quint8& funcCode, + QVector& parsedData, quint8& errorCode,QByteArray& extractedFrame,bool& isComFrame); +public: + /*********************************************************************** + *@brief: 创建读请求帧 + *@param: frame 输出参数 请求帧 + *@param: type 请求类型 + ***********************************************************************/ + void buildReadFrame(QByteArray& frame,FunctionCode funcCode); + + /*********************************************************************** + *@brief: 创建部分写请求帧 + *@param: frame 输出参数 请求帧 + *@param: type 请求类型 + ***********************************************************************/ + void buildWriteFrame(QByteArray& frame,FunctionCode funcCode); + + /*********************************************************************** + *@brief: 解析读线圈响应数据 + *@param: responseData 响应中的数据部分 + *@param: coils 输出参数(bool类型的数组),解析出的线圈状态 + *@return:是否成功解析 + ***********************************************************************/ + bool parseReadCoilsResponse(const QByteArray& responseData, QVector& coils); + + /*********************************************************************** + *@brief: 解析读保持寄存器响应数据 + *@param: responseData 响应中的数据部分 + *@param: registers 输出参数(quint16类型的数组),解析出的寄存器值 + *@return:是否成功解析 + ***********************************************************************/ + bool parseReadHoldingRegistersResponse(const QByteArray& responseData, QVector& registers); + + /*********************************************************************** + *@brief: 解析单帧响应 + *@param: response 从站返回的响应数据 + *@param: slaveAddr 输出参数,从响应中解析出的从站地址 + *@param: funcCode 输出参数,从响应中解析出的功能码 + *@param: data 输出参数,解析后的数据 + *@param: errorCode 输出参数,错误码(NoError表示成功) + *@return:是否成功解析 + ***********************************************************************/ + bool parseResponse(const QByteArray& response, quint8& slaveAddr, quint8& funcCode, + QVector& data, quint8& errorCode); + + /*********************************************************************** + *@brief: 从缓冲区提取完整帧 + *@param: frame 输出参数,提取的完整帧 + *@return:是否成功提取到一帧 + ***********************************************************************/ + bool extractFrame(QByteArray& frame,bool& isComFrame); + + //计算CRC16校验值 + quint16 calculateCRC(const QByteArray& data); + + //验证帧的CRC16校验 + bool verifyCRC(const QByteArray& frame); + + //Modbus RTU协议参数 + quint8 slaveAddr_; // 从站地址 + FunctionCode funcCode_; // 功能码 + quint16 startAddr_; // 起始地址 + quint16 readCount_; // 读取数量 + QVector coils_; // 线圈数据(写入) + QVector registers_; // 寄存器数据(写入) + //其他 + QHash errMessage; //错误码描述映射表 + QByteArray buffer_; // 数据缓冲区 +}; + +#endif // MODBUS_RTU_MASTER_H diff --git a/Modbus/include/serial_communication.h b/Modbus/include/serial_communication.h new file mode 100644 index 0000000..772e9d8 --- /dev/null +++ b/Modbus/include/serial_communication.h @@ -0,0 +1,117 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: serial_communication.h +* Description: Modbus主站的串口通信头文件 +* Others: +* Version: v1.0 +* Author: weikai XINJE +* Date: 2025-7-30 +***********************************************************************/ + +#ifndef SERIALCOMMUNICATOR_H +#define SERIALCOMMUNICATOR_H + +#include +#include +#include +#include +#include +#include"timeout_handler.h" + +/********************************************************************** +* Iterates over the contents of a SerialCommunicator. +*SerialCommunicator +*提供串口通信功能,连接,断开连接,发送数据,接收数据处理等 +*调用TimeoutHandler实例进行超时处理 +***********************************************************************/ +class SerialCommunicator : public QObject +{ + Q_OBJECT +public: + // 构造函数 + explicit SerialCommunicator(QObject *parent = nullptr); + ~SerialCommunicator(); + + // 参数设置接口 + void setPortName(const QString &portName); + void setBaudRate(int baudRate); + void setDataBits(QSerialPort::DataBits dataBits); + void setStopBits(QSerialPort::StopBits stopBits); + void setParity(QSerialPort::Parity parity); + + //设置是否可以开始心跳 + void setIsCanHeart(bool flag) {isCanHeartbeat_ = flag;} + void setHeartbeatDFrame(const QByteArray& frame){ heartbeatFrame_ = frame; } + + // 初始化函数(设置默认参数) + void init(); + + // 连接/断开接口 + bool connectDevice(); + void disconnectDevice(); + + // 设置超时参数 + bool setTimeoutSettings(int timeoutMs, int maxRetries); + //获取 + void getTimeoutSettings(int& timeoutMs,int& maxRetries); + // 发送数据接口 + bool sendData(const QByteArray &data); + + // 状态查询接口 + bool isConnected() const; + //获取可用端口列表 + QStringList getAvailablePorts(); + +signals: + // 数据接收信号(发送处理后的十六进制字符串) + void dataReceived(const QString &hexData); + // 状态通知信号 + void statusChanged(const QString &status); + // 错误通知信号 + void errorOccurred(const QString &errorMsg); + // 连接断开信号 + void connectionDisconnected(); + +private: + // 内部数据接收处理 + void onReadyRead(); + // 处理超时信号 + void onTimeoutOccurred(int currentRetry); + // 处理最大重试次数达到 + void onMaxRetriesReached(); + //处理串口错误 + void onSerialError(QSerialPort::SerialPortError error); + + //发送心跳槽函数 + void onSendHeartbeat(); + //心跳超时槽函数 + void onHeartbeatTimeout(); + //启动心跳机制 + void startHeartbeat(); + //停止心跳机制 + void stopHeartbeat(); + + QSerialPort *serialPort_;// 串口对象 + // 串口参数 + QString portName_;//串口名 + int baudRate_;//波特率 + QSerialPort::DataBits dataBits_;//数据位 + QSerialPort::StopBits stopBits_;//停止位 + QSerialPort::Parity parity_;//奇偶校验 + + bool connected_;// 连接状态 + + TimeoutHandler timeoutHandler_; // 超时处理器 + QByteArray pendingData_; // 等待响应的数据(用于重发) + + //断线重连 + QTimer* heartbeatTimer_;//定期发送心跳帧 + QTimer* heartbeatTimeoutTimer_;//心跳响应计时器 + int heartbeatInterval_;//心跳间隔 + int heartbeatTimeout_;//心跳响应时间 + QByteArray heartbeatFrame_;//心跳帧 + bool isCanHeartbeat_;//是否可以开始心跳 +}; + +#endif // SERIALCOMMUNICATOR_H diff --git a/Modbus/include/timeout_handler.h b/Modbus/include/timeout_handler.h new file mode 100644 index 0000000..fb45514 --- /dev/null +++ b/Modbus/include/timeout_handler.h @@ -0,0 +1,73 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: timeout_handler.h +* Description: Modbus主站的超时处理头文件 +* Others: +* Version: v1.0 +* Author: weikai XINJE +* Date: 2025-7-30 +***********************************************************************/ + +#ifndef TIMEOUT_HANDLER_H +#define TIMEOUT_HANDLER_H +#include +#include + +/********************************************************************** +* Iterates over the contents of a TimeoutHandler. +*TimeoutHandler +*提供超时处理功能,若超过时间未收到响应,则触发超时重发机制 +*被SerialCommunicator调用使用 +***********************************************************************/ +class TimeoutHandler : public QObject +{ + Q_OBJECT +public: + explicit TimeoutHandler(QObject *parent = nullptr); + + // 获取当前重试次数 + int getCurrentRetryCount() const + { + return currentRetryCount_; + } + + // 设置超时时间(毫秒) + void setTimeoutInterval(int msec); + + // 获取当前超时时间(毫秒) + int getTimeoutInterval() const; + + // 设置最大重试次数 + void setRetryCount(int count); + + // 获取当前设置的重试次数 + int getRetryCount() const; + + // 启动超时计时(若已在运行则重启) + void start(); + + // 停止超时计时并重置重试计数 + void stop(); + + // 判断是否正在计时中 + bool isRunning() const; + +signals: + // 超时信号(参数为当前重试次数) + void timeoutOccurred(int retryCount); + + // 达到最大重试次数信号 + void maxRetriesReached(); + +private: + // 处理定时器超时 + void onTimerTimeout(); + + QTimer *timer_; // 定时器 + int timeoutInterval_; // 超时时间(毫秒) + int maxRetryCount_; // 最大重试次数 + int currentRetryCount_; // 当前重试次数 +}; + +#endif // TIMEOUT_HANDLER_H diff --git a/Modbus/src/main.cpp b/Modbus/src/main.cpp new file mode 100644 index 0000000..fd3e533 --- /dev/null +++ b/Modbus/src/main.cpp @@ -0,0 +1,11 @@ +#include "mainwindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + return a.exec(); +} diff --git a/Modbus/src/mainwindow.cpp b/Modbus/src/mainwindow.cpp new file mode 100644 index 0000000..e7f957b --- /dev/null +++ b/Modbus/src/mainwindow.cpp @@ -0,0 +1,613 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: // mainwindow.cpp +* Description: // ModbusRTU主站上位机人机交互界面源文件 +* //调用SerialCommunicator实现串口通信,调用ModbusRTUMaster实现帧的生成与解析。 +* Others: // +* Version: // v1.0 +* Author: // weikai,XINJE +* Date: // 2025-7-30 +***********************************************************************/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + :QMainWindow(parent), + ui_(new Ui::MainWindow), + serialComm_(new SerialCommunicator(this)), + modbusMaster_(new ModbusRTUMaster(this)), + serialPortTimer_(new QTimer(this)) +{ + ui_->setupUi(this); + statusBar()->showMessage("就绪"); + + // 添加UI控件显式关联 + connect(ui_->connectButton, &QPushButton::clicked,this, &MainWindow::onConnectButtonClicked);//连接按钮关联槽函数 + connect(ui_->disconnectButton, &QPushButton::clicked,this, &MainWindow::onDisconnectButtonClicked);//断开连接按钮关联槽函数 + connect(ui_->readButton, &QPushButton::clicked,this, &MainWindow::onReadButtonClicked);//读取按钮关联槽函数 + connect(ui_->writeButton, &QPushButton::clicked,this, &MainWindow::onWriteButtonClicked);//写入按钮关联槽函数 + connect(ui_->clearLogButton, &QPushButton::clicked,this, &MainWindow::onClearLogButtonClicked);//清空日志按钮关联槽函数 + connect(ui_->exportHistoryButton, &QPushButton::clicked,this, &MainWindow::onExportHistoryButtonClicked);//导出通信历史按钮 + + + // 连接串口通信信号 + connect(serialComm_, &SerialCommunicator::dataReceived,this, &MainWindow::onSerialDataReceived);//串口接收到数据关联槽函数 + connect(serialComm_, &SerialCommunicator::statusChanged,this, &MainWindow::onSerialStatusChanged);//状态变化信号关联槽函数 + connect(serialComm_, &SerialCommunicator::errorOccurred,this, &MainWindow::onSerialErrorOccurred);//错误发生信号关联槽函数 + connect(serialComm_, &SerialCommunicator::connectionDisconnected,this, &MainWindow::onConnectionDisconnected);//连接断开信号关联槽函数 + + //设置定时器,每2秒刷新一次串口列表 + connect(serialPortTimer_, &QTimer::timeout, this, &MainWindow::onRefreshSerialPorts);//定时信号关联刷新串口槽函数 + serialPortTimer_->start(2000); // 每2000毫秒(2秒)检查一次 + + init();//初始化 + + refreshSerialPorts();//刷新串口列表 + + logToBox("程序启动,就绪");// 启动日志 +} + +/*********************************************************************** +* 析构函数 +***********************************************************************/ +MainWindow::~MainWindow() +{ + delete ui_; +} + +/*********************************************************************** +* 初始化各个组件,如数据位,停止位等 +***********************************************************************/ +void MainWindow::init() +{ + // 初始化日志框组件 + ui_->logTextEdit->setReadOnly(true);//设置只读 + ui_->logTextEdit->setLineWrapMode(QTextEdit::WidgetWidth);//设置自动换行 + ui_->logTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);//设置垂直滚动条常显 + + // 初始化数据位 + ui_->dataComboBox->addItem("7", QSerialPort::Data7); + ui_->dataComboBox->addItem("8", QSerialPort::Data8); + ui_->dataComboBox->setCurrentIndex(1); + + //初始化停止位 + ui_->stopComboBox->addItem("1", QSerialPort::OneStop); + ui_->stopComboBox->addItem("1.5", QSerialPort::OneAndHalfStop); + ui_->stopComboBox->addItem("2", QSerialPort::TwoStop); + ui_->stopComboBox->setCurrentIndex(0); + + //初始化校验位下拉框 + ui_->verifyComboBox->addItem("无", QSerialPort::NoParity); + ui_->verifyComboBox->addItem("奇校验", QSerialPort::OddParity); + ui_->verifyComboBox->addItem("偶校验", QSerialPort::EvenParity); + ui_->verifyComboBox->setCurrentIndex(0); + + //初始化波特率 + ui_->baudComboBox->setCurrentIndex(1); + + //创建心跳帧 + modbusMaster_->setSlaveAddr(1); + modbusMaster_->setFuncCode(ModbusRTUMaster::READ_COILS); + modbusMaster_->setStartAddr(256); + modbusMaster_->setReadCount(1); + QByteArray frame = modbusMaster_ ->createReadCoilsFrame(); + //qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳帧frame:"<< frame; + serialComm_->setHeartbeatDFrame(frame); +} + +/*********************************************************************** +* 导出历史记录按钮,点击触发,打开文件保存对话框,打开文件并写入。 +***********************************************************************/ +void MainWindow::onExportHistoryButtonClicked() +{ + // 默认文件名:当前日期时间(格式为yyyyMMdd_HHmmss)加上"_history.txt"组成 + QString defaultFileName = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss") + "_history.txt"; + + // 打开文件保存对话框,让用户选择保存路径和文件名 + QString fileName = QFileDialog::getSaveFileName( + this, + "导出历史记录", + defaultFileName, + "文本文件 (*.txt);" + ); + + // 判断用户是否取消了文件选择 + if (fileName.isEmpty()) + { + //将导出取消输出至状态栏与日志框 + statusBar()->showMessage("导出取消", 2000); + logToBox("导出历史记录取消"); + return; + } + + // 根据用户选择的文件名创建QFile对象,用于文件操作 + QFile file(fileName); + + //文件打开失败 + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + statusBar()->showMessage("无法打开文件进行写入", 2000); // 将具体错误信息记录到日志框 + logToBox("错误: 无法打开文件 " + fileName); + return; + } + + // 创建文本流对象并关联 + QTextStream out(&file); + + // 遍历历史记录列表中的所有项,将内容写入文件 + for (int i = 0; i < ui_->historyListWidget->count(); ++i) + { + out << ui_->historyListWidget->item(i)->text() << "\n\n"; + } + + file.close(); + + statusBar()->showMessage("历史记录已导出到 " + fileName, 3000); + logToBox("历史记录已导出到 " + fileName); +} + +/*********************************************************************** +* 连接按钮,点击触发。获取界面通信配置参数,与对应串口建立连接 +***********************************************************************/ +void MainWindow::onConnectButtonClicked() +{ + // 获取UI参数:串口名、波特率、数据位、停止位、校验选项 + QString currentText = ui_->serialPortCombox->currentText(); + QString portName = currentText.split("(").first(); + + int baudRate = ui_->baudComboBox->currentText().toUInt(); + QSerialPort::DataBits dataBits = static_cast(ui_->dataComboBox->currentData().toUInt()); + QSerialPort::StopBits stopBits = static_cast(ui_->stopComboBox->currentData().toUInt()); + QSerialPort::Parity parity = static_cast(ui_->verifyComboBox->currentData().toUInt()); + + // 向serialComm设置串口参数 + serialComm_->setPortName(portName); + serialComm_->setBaudRate(baudRate); + serialComm_->setDataBits(dataBits); + serialComm_->setStopBits(stopBits); + serialComm_->setParity(parity); + + //设置超时时间与最大重发次数 + int timeout=DEFAULT_TIMEOUT,maxRetries=DEFAULT_MAXRETRISE; + if(!ui_->timeoutEdit->text().isEmpty()) + { + bool ok; + timeout = ui_->timeoutEdit->text().toUInt(&ok); + if(!ok) + { + logToBox("超时时间输入有误,请检查!"); + } + } + if(!ui_->reissueEdit->text().isEmpty()) + { + bool ok; + maxRetries = ui_->reissueEdit->text().toUInt(&ok); + if(!ok) + { + logToBox("最大重发次数输入有误,请检查!"); + } + } + + if(!serialComm_->setTimeoutSettings(timeout,maxRetries)) + { + QMessageBox::warning( + this, + "超时参数错误!", + "请重新输入,超时时间500~5000ms,最大重发次数1~5次", + QMessageBox::Ok + ); + return; + } + // 尝试连接 + if (serialComm_->connectDevice())//连接成功 + { + ui_->connectButton->setEnabled(false); + ui_->disconnectButton->setEnabled(true); + + ui_->timeoutEdit->setText(QString::number(timeout)); + ui_->reissueEdit->setText(QString::number(maxRetries)); + } + else + { + logToBox(QString(portName + "串口连接失败\n")); + } +} + +/*********************************************************************** +* 断开按钮,点击触发。更新连接与断开连接按钮状态,并调用serialComm断开连接函数 +***********************************************************************/ +void MainWindow::onDisconnectButtonClicked() +{ + serialComm_->disconnectDevice(); + ui_->connectButton->setEnabled(true); + ui_->disconnectButton->setEnabled(false); +} + +/*********************************************************************** +* 清空日志按钮,点击触发。清空日志框 +***********************************************************************/ +void MainWindow::onClearLogButtonClicked() +{ + ui_->logTextEdit->clear(); + logToBox("日志已清空"); +} + +/*********************************************************************** +* 读取按钮,点击触发。触发后读取界面参数,根据功能码选择请求帧类型,创建帧并发送 +***********************************************************************/ +void MainWindow::onReadButtonClicked() +{ + if (!serialComm_->isConnected()) + { + statusBar()->showMessage("请先连接设备", 2000); + return; + } + + // 从UI获取参数 + int slaveAddress = ui_->stationAddrSpinBox->value(); + QString funcText = ui_->functionComboBox->currentText(); + int startAddress = ui_->startAddrSpinBox->value(); + int readNum = ui_->numberSpinBox->value(); + + // 设置Modbus参数 + modbusMaster_->setSlaveAddr(slaveAddress); + modbusMaster_->setStartAddr(startAddress); + + QByteArray frame; + // 根据功能码生成请求帧 + if ("01读线圈" == funcText) + { + modbusMaster_->setFuncCode(ModbusRTUMaster::READ_COILS); + modbusMaster_->setReadCount(readNum); + frame = modbusMaster_->createReadCoilsFrame(); + } + else if ("03读寄存器" == funcText) + { + modbusMaster_->setFuncCode(ModbusRTUMaster::READ_HOLDING_REGISTERS); + modbusMaster_->setReadCount(readNum); + frame = modbusMaster_->createReadHoldingRegistersFrame(); + } + else + { + statusBar()->showMessage("请选择读取功能码", 2000); + return; + } + + // 发送帧 + if (!frame.isEmpty()) + { + serialComm_->sendData(frame); + logToBox("发送读取请求完成!"); + QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + ui_->historyListWidget->addItem("[" + timeStamp + "]" + "发送: " + frame.toHex(' ').toUpper()); + } + else + { + statusBar()->showMessage("生成请求帧失败", 2000); + } + + //读取请求发送成功弹窗 + QMessageBox::information( + this, + "读取请求", + "读取请求发送完成!", + QMessageBox::Ok + ); +} + +/*********************************************************************** +* 写入按钮,点击触发。触发后读取界面参数,根据功能码选择请求帧类型,创建帧并发送 +***********************************************************************/ +void MainWindow::onWriteButtonClicked() +{ + if (!serialComm_->isConnected()) + { + statusBar()->showMessage("请先连接设备", 2000); + return; + } + + // 从UI获取参数 + int slaveAddress = ui_->stationAddrSpinBox->value(); + QString funcText = ui_->functionComboBox->currentText(); + int startAddress = ui_->startAddrSpinBox->value(); + QString dataText = ui_->writeTextEdit->toPlainText().trimmed(); + + if (dataText.isEmpty()) + { + statusBar()->showMessage("请输入写入数据", 2000); + return; + } + + // 设置Modbus参数 + modbusMaster_->setSlaveAddr(slaveAddress); + modbusMaster_->setStartAddr(startAddress); + + QByteArray frame; + // 根据功能码生成请求帧 + if ("0F写线圈" == funcText) + { + // 解析线圈数据(二进制字符串) + QVector coils; + for (QChar c : dataText) + { + if ('0' == c) + { + coils.append(false); + } + else if ('1' == c) + { + coils.append(true); + } + else + { + statusBar()->showMessage("写线圈需输入二进制数据(仅0/1),请重新输入", 2000); + return; + } + } + modbusMaster_->setFuncCode(ModbusRTUMaster::WRITE_MULTIPLE_COILS); + modbusMaster_->setCoils(coils); + frame = modbusMaster_->createWriteMultipleCoilsFrame(); + } + else if ("10写寄存器" == funcText) + { + // 解析寄存器数据(逗号分隔的整数) + QStringList dataList = dataText.split(','); + QVector registers; + for (const QString &d : dataList) + { + bool flag; + quint16 data = d.toUInt(&flag);; + if (!flag) + { + statusBar()->showMessage("写线圈需输入整数,请重新输入)", 2000); + return; + } + registers.append(data); + } + modbusMaster_->setFuncCode(ModbusRTUMaster::WRITE_MULTIPLE_REGISTERS); + modbusMaster_->setRegisters(registers); + frame = modbusMaster_->createWriteMultipleRegistersFrame(); + } + else + { + statusBar()->showMessage("请选择写入功能码", 2000); + return; + } + + // 发送帧 + if (!frame.isEmpty()) + { + serialComm_->sendData(frame); + // 将帧转换为带空格分隔的十六进制大写字符串 + QString hexStr = frame.toHex(' ').toUpper(); + + logToBox("发送请求帧"); + QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + ui_->historyListWidget->addItem("[" + timeStamp + "]" +"发送: " + hexStr); + + QMessageBox::information( + this, + "写入请求", + "写入请求发送完成!", + QMessageBox::Ok + ); + } + else + { + statusBar()->showMessage("生成请求帧失败", 2000); + } +} + +/*********************************************************************** +* 收到串口刷新定时器信号的槽函数,调用刷新串口函数 +***********************************************************************/ +void MainWindow::onRefreshSerialPorts() +{ + refreshSerialPorts(); +} + +/*********************************************************************** +* 收到连接已断开信号的槽函数,更新按钮状态并弹出警告弹窗告知用户。 +***********************************************************************/ +void MainWindow::onConnectionDisconnected() +{ + // 更新UI按钮状态 + ui_->connectButton->setEnabled(true); + ui_->disconnectButton->setEnabled(false); + + logToBox("连接已断开"); + + QMessageBox::warning( + this, + "连接断开", + "连接已断开!", + QMessageBox::Ok + ); +} + +/*********************************************************************** +* 收到串口状态变化信号的槽函数,并将状态变化输出至底部状态栏与日志框。 +***********************************************************************/ +void MainWindow::onSerialStatusChanged(const QString &status) +{ + statusBar()->showMessage(status, 3000); + logToBox(status); +} + +/*********************************************************************** +* 收到串口错误信号的槽函数,并将错误输出至底部状态栏与日志框。 +***********************************************************************/ +void MainWindow::onSerialErrorOccurred(const QString &error) +{ + statusBar()->showMessage(error, 3000); + logToBox("错误: " + error); +} + +/*********************************************************************** +* 收到数据信号触发 +* 处理串口接收数据的逻辑,提取完整帧后解析并展示,data为串口收到的原始数据。 +***********************************************************************/ +void MainWindow::onSerialDataReceived(const QString &data) +{ + + QByteArray response = QByteArray::fromHex(data.toUtf8()); + quint8 slaveAddr, funcCode, errorCode; + QVector parsedData;//解析后的数据 + QByteArray extractedFrame; // 提取的完整帧 + bool isComFrame = true;//是否解析到完整帧 + + // 将数据传递到下层处理解析 + if (modbusMaster_->processReceivedData(response, slaveAddr, funcCode, parsedData, errorCode, extractedFrame,isComFrame)) + { + serialComm_->setIsCanHeart(true); + //解析成功,获取到帧的各部分,根据格式显示在日志框 + QString hexData = extractedFrame.toHex(' ').toUpper().trimmed(); + QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + QString display = QString("[%1]接收原始数据: %2\n").arg(timeStamp).arg(hexData); + display += QString("解析结果:\n从站地址: " + QString::number(slaveAddr) + "\n功能码: 0x" + QString::number(funcCode,16).rightJustified(2,'0')); + + //对解析成功后的数据进行打印 + if (ModbusRTUMaster::NO_ERROR == errorCode) + { + int startAddr = modbusMaster_->getStartAddr(); + switch (funcCode) + { + case ModbusRTUMaster::READ_COILS: + { + display += " 类型: 读线圈 (01)\n数据: "; + QString datas; + for (int i = 0; i < parsedData.size(); ++i) + { + if(0 == i%10) + { + datas += "\n"; + } + datas += QString("线圈%1=%2 ").arg(i + startAddr).arg(parsedData[i] ? "ON" : "OFF"); + } + display += datas; + break; + } + case ModbusRTUMaster::READ_HOLDING_REGISTERS: + { + display += " 类型: 读保持寄存器 (03)\n数据: "; + QString datas; + for (int i = 0; i < parsedData.size(); ++i) + { + if(0 == i%10) + { + datas += "\n"; + } + datas += QString("寄存器%1=%2 ").arg(i + startAddr).arg(parsedData[i]); + } + display += datas; + break; + } + case ModbusRTUMaster::WRITE_MULTIPLE_COILS: + { + display += QString(" 类型: 写多个线圈 (0F)\n起始地址: %1\n写入线圈数量: %2") + .arg(parsedData[0]) + .arg(parsedData[1]); + break; + } + case ModbusRTUMaster::WRITE_MULTIPLE_REGISTERS: + { + display += QString(" 类型: 写多个寄存器 (10)\n起始地址: %1\n写入寄存器数量: %2") + .arg(parsedData[0]) + .arg(parsedData[1]); + break; + } + default: + display += "\n类型: 不支持的功能码"; + break; + } + logToBox("接收并解析响应成功"); + } + else + { + display += QString(" 类型: 错误响应\n错误码: 0x%1 错误描述: %2") + .arg(errorCode, 2, 16, QChar('0')) + .arg(modbusMaster_->getErrorDescription(errorCode)); + logToBox(QString("接收错误响应: 错误码0x" + QString::number(errorCode,16).rightJustified(2,'0'))); + } + ui_->historyListWidget->addItem(display); // 添加解析结果 + ui_->historyListWidget->addItem("****************************************************************************************************"); + } + else + { + if(isComFrame) + { + QString hexData = response.toHex(' ').toUpper().trimmed(); + QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + QString display = QString("[%1]\n接收原始数据:%2\n").arg(timeStamp).arg(hexData); + display += "解析失败"; + ui_->historyListWidget->addItem(display); // 添加未解析的帧信息 + } + } +} + +/*********************************************************************** +* 刷新串口列表 +***********************************************************************/ +void MainWindow::refreshSerialPorts() +{ + // 获取当前已有的端口 + QStringList oldPorts; + for (int i = 0; i < ui_->serialPortCombox->count(); ++i) + { + QString port = ui_->serialPortCombox->itemText(i); + if (port != "未检测到串口") + { + oldPorts << port; + } + } + + // 获取新端口列表 + QStringList newPorts = serialComm_->getAvailablePorts(); + + // 处理新增端口 + for (const QString& port : newPorts) + { + if (!oldPorts.contains(port)) + { + ui_->serialPortCombox->addItem(port); + logToBox("新增串口: " + port); + } + } + + // 处理移除端口 + for (const QString& port : oldPorts) + { + if (!newPorts.contains(port)) + { + int index = ui_->serialPortCombox->findText(port); + if (index != -1) + { + ui_->serialPortCombox->removeItem(index); + logToBox("移除串口: " + port); + } + } + } +} + +/*********************************************************************** +* 日志输出函数。将message输出到日志框 +***********************************************************************/ +void MainWindow::logToBox(const QString &message) +{ + QString timeStamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + QString logEntry = QString("[%1] %2\n").arg(timeStamp).arg(message); + ui_->logTextEdit->insertPlainText(logEntry); + + // 自动滚动到底部 + QTextCursor cursor = ui_->logTextEdit->textCursor(); + cursor.movePosition(QTextCursor::End); + ui_->logTextEdit->setTextCursor(cursor); +} diff --git a/Modbus/src/modbus_master.cpp b/Modbus/src/modbus_master.cpp new file mode 100644 index 0000000..f4b1c3b --- /dev/null +++ b/Modbus/src/modbus_master.cpp @@ -0,0 +1,547 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: // modbus_master.cpp +* Description: // ModbusRTU协议源文件,负责请求帧的生成,响应帧的解析 +* Others: // +* Version: // v1.0 +* Author: // weikai,XINJE +* Date: // 2025-7-30 +***********************************************************************/ +#include +#include "modbus_master.h" + + +ModbusRTUMaster::ModbusRTUMaster(QObject *parent) + : QObject(parent), + slaveAddr_(0), + funcCode_(READ_COILS), + startAddr_(0), + readCount_(0) +{ + errMessage.insert(NO_ERROR,"无错误"); + errMessage.insert(ILLEGAL_FUNCTION,"非法功能-从站不支持该功能码"); + errMessage.insert(ILLEGAL_DATA_ADDRESS,"非法数据地址-从站不支持该地址"); + errMessage.insert(ILLEGAL_DATA_VALUE,"非法数据值-数据值超出范围"); + errMessage.insert(SERVER_DEVICE_FAILURE,"从站设备故障"); + errMessage.insert(ACKNOWLEDGE,"确认-请求已接收但未处理"); + errMessage.insert(SERVER_DEVICE_BUSY,"从站设备忙"); + errMessage.insert(MEMORY_PARITY_ERROR,"内存奇偶校验错误"); +} + +/************************************************************************************* +* 获取起始地址 +**************************************************************************************/ +quint16 ModbusRTUMaster::getStartAddr() const +{ + return startAddr_; +} + +/************************************************************************************* +*获取错误码描述 +**************************************************************************************/ +QString ModbusRTUMaster::getErrorDescription(int errorCode) +{ + QString resMessage; + if(errMessage.contains(errorCode)) + { + resMessage = errMessage[errorCode]; + } + else + { + resMessage = "未知错误"; + } + return resMessage; +} + +/************************************************************************************* +* 生成读请求帧 (功能码03) +*帧格式: [从站地址(1字节)] [功能码0x03(1字节)] [起始地址(2字节)] [读取数量(2字节)][CRC(2字节)] +**************************************************************************************/ +void ModbusRTUMaster::buildReadFrame(QByteArray& frame,FunctionCode funcCode) +{ + // 从站地址 + frame.append(slaveAddr_); + + // 功能码 + frame.append(funcCode); + + // 起始地址 + frame.append((startAddr_ >> 8) & 0xFF); + frame.append(startAddr_ & 0xFF); + + //读取数量 + frame.append((readCount_ >> 8) & 0xFF); + frame.append(readCount_ & 0xFF); + + // 计算并添加CRC校验 (2字节) + quint16 crc = calculateCRC(frame); + frame.append(crc & 0xFF); // CRC低字节在前 + frame.append((crc >> 8) & 0xFF); // CRC高字节在后 + +} + +/*********************************************************************** +* 生成部分写请求帧 (功能码03) +*帧格式: [从站地址(1字节)] [功能码0x03(1字节)] [起始地址(2字节)] +***********************************************************************/ +void ModbusRTUMaster::buildWriteFrame(QByteArray& frame,FunctionCode funcCode) +{ + + // 从站地址 + frame.append(slaveAddr_); + + // 功能码 + frame.append(funcCode); + + // 起始地址 + frame.append((startAddr_ >> 8) & 0xFF); + frame.append(startAddr_ & 0xFF); +} + +/*********************************************************************** +* 生成读线圈请求帧 (功能码01) +* 帧格式: [从站地址(1字节)] [功能码0x01(1字节)] [起始地址(2字节)] +* [线圈数量(2字节)] [CRC校验(2字节)] +***********************************************************************/ +QByteArray ModbusRTUMaster::createReadCoilsFrame() +{ + QByteArray frame; + buildReadFrame(frame,READ_COILS); + return frame; +} + +/*********************************************************************** +* 生成读保持寄存器请求帧 (功能码03) +*帧格式: [从站地址(1字节)] [功能码0x03(1字节)] [起始地址(2字节)] +* [寄存器数量(2字节)] [CRC校验(2字节)] +***********************************************************************/ +QByteArray ModbusRTUMaster::createReadHoldingRegistersFrame() +{ + QByteArray frame; + buildReadFrame(frame,READ_HOLDING_REGISTERS); + return frame; +} + +/*********************************************************************** +* 生成写多个线圈请求帧 (功能码0F) +*帧格式: [从站地址(1字节)] [功能码0x0F(1字节)] [起始地址(2字节)] +* [线圈数量(2字节)] [字节数(1字节)] [线圈数据(n字节)] [CRC校验(2字节)] +***********************************************************************/ +QByteArray ModbusRTUMaster::createWriteMultipleCoilsFrame() +{ + // 检查线圈数据是否为空,为空则返回空 + if (coils_.isEmpty()) + { + return QByteArray(); + } + + QByteArray frame; + buildWriteFrame(frame,WRITE_MULTIPLE_COILS); + + //线圈数量 + quint16 coilCount = coils_.size(); + frame.append((coilCount >> 8) & 0xFF); + frame.append(coilCount & 0xFF); + + //字节数 + quint8 byteCount = (coilCount + 7) / 8; + frame.append(byteCount); + + //线圈数据 + QByteArray coilData; + for (int currByte = 0; currByte < byteCount; ++currByte) + { + //当前字节 + quint8 byte = 0; + for (int currBit = 0; currBit < 8; ++currBit) + { + //当前比特位 + int index = currByte * 8 + currBit; + if (index < coilCount && coils_[index]) + { + //当前比特位按位或 + int mask = 1 << currBit; + byte |= mask; + } + } + coilData.append(byte); + } + frame.append(coilData); + + // 计算并添加CRC校验 (2字节) + quint16 crc = calculateCRC(frame); + frame.append(crc & 0xFF); + frame.append((crc >> 8) & 0xFF); + return frame; +} + +/*********************************************************************** +*生成写多个保持寄存器请求帧 (功能码10) +*帧格式: [从站地址(1字节)] [功能码0x10(1字节)] [起始地址(2字节)] +* [寄存器数量(2字节)] [字节数(1字节)] [寄存器数据(2n字节)] [CRC校验(2字节)] +***********************************************************************/ +QByteArray ModbusRTUMaster::createWriteMultipleRegistersFrame() +{ + // 检查寄存器数据是否为空,为空则返回空 + if (registers_.isEmpty()) + { + return QByteArray(); + } + + QByteArray frame; + buildWriteFrame(frame,WRITE_MULTIPLE_REGISTERS); + + //寄存器数量 + quint16 regCount = registers_.size(); + frame.append((regCount >> 8) & 0xFF); + frame.append(regCount & 0xFF); + + // 字节数 + quint8 byteCount = regCount * 2; + frame.append(byteCount); + + // 寄存器数据 + for (quint16 reg : registers_) + { + frame.append((reg >> 8) & 0xFF); + frame.append(reg & 0xFF); + } + + // 计算并添加CRC校验 + quint16 crc = calculateCRC(frame); + frame.append(crc & 0xFF); + frame.append((crc >> 8) & 0xFF); + return frame; +} + +/*********************************************************************** +*处理接收到的数据流 +***********************************************************************/ +bool ModbusRTUMaster::processReceivedData(const QByteArray& data, quint8& slaveAddr, quint8& funcCode, + QVector& parsedData, quint8& errorCode, QByteArray& extractedFrame,bool& isComFrame) +{ + buffer_.append(data); + //qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "内容:" << "接收数据追加到缓冲区,当前缓冲区大小:" << buffer_.size(); + + QByteArray frame; + bool frameExtracted = false; + + // 对完整帧做处理 + if (extractFrame(frame,isComFrame)) + { + //qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << " 完整帧: " << frame; + extractedFrame = frame; // 保存当前提取的完整帧 + if (parseResponse(frame, slaveAddr, funcCode, parsedData, errorCode)) + { + frameExtracted = true; + //qDebug() <<"文件:"<<__FILE__ << " 行数:" << __LINE__ << ": 帧解析成功,功能码:" << QString::number(funcCode, 16); + } + else + { + qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "内容:" << "帧解析失败"; + } + } + + return frameExtracted; // 返回是否提取并解析成功 +} +/******************************************************************** + * 从缓冲区中提取一个完整的Modbus RTU帧 + ********************************************************************/ +bool ModbusRTUMaster::extractFrame(QByteArray& frame,bool& isComFrame) +{ + // 检查缓冲区是否至少包含最小帧长度(异常帧5字节) + if (buffer_.size() < 5) + { + isComFrame = false; + return false; + } + + // 遍历缓冲区,尝试提取有效帧 + for (int i = 0; i <= buffer_.size() - 5; ++i) + { + // 确保有足够的字节读取功能码 + if (i + 1 >= buffer_.size()) + { + break; + } + + // 获取功能码 + quint8 funcCode = static_cast(buffer_[i + 1]); + int expectedLen = 0; // 预期帧长度 + + // 判断是否为异常响应帧 + if (funcCode & 0x80) + { + expectedLen = 5; //异常响应帧5字节 + } + else + { + // 根据功能码计算预期帧长度 + switch (funcCode) + { + case READ_COILS: + case READ_HOLDING_REGISTERS: + { + // 确保有足够的字节读取byteCount + if (i + 3 > buffer_.size()) + { + return false; // 数据不足 + } + int byteCount = static_cast(buffer_[i + 2]); + expectedLen = 3 + byteCount + 2; // 地址(1) + 功能码(1) + 字节数(1) + 数据 + CRC(2) + break; + } + case WRITE_MULTIPLE_COILS: + case WRITE_MULTIPLE_REGISTERS: + { + expectedLen = 8; // 固定长度:地址(1) + 功能码(1) + 地址(2) + 数量(2) + CRC(2) + break; + } + default: + { + // 未知功能码,移除无效字节并继续 + buffer_.remove(0, i + 1); + i = -1; // 重置索引 + continue; + } + } + } + + // 检查缓冲区是否包含完整帧 + if (i + expectedLen > buffer_.size()) + { + isComFrame = false; + return false; // 帧不完整 + } + + // 提取可能有效的帧并验证CRC + frame = buffer_.mid(i, expectedLen); + if (verifyCRC(frame)) + { + //qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "内容:" << "提取到的完整帧:" << frame.toHex(); + buffer_.remove(0, i + expectedLen); // 移除已处理的帧 + return true; // 提取到完整帧 + } + else + { + // CRC校验失败,移除无效字节 + buffer_.remove(0, i + 1); + i = -1; // 重置索引 + } + } + return false; +} + +/******************************************************************** + * 解析提取到的完整帧 + ********************************************************************/ +bool ModbusRTUMaster::parseResponse(const QByteArray& response, quint8& slaveAddr, quint8& funcCode, + QVector& data, quint8& errorCode) +{ + // 响应数据至少需要包含:地址(1字节) + 功能码(1字节) + 数据(至少1字节) + CRC(2字节),共5字节 + if (response.size() < 5) + { + return false; + } + + // 验证响应数据的CRC校验是否正确 + if (!verifyCRC(response)) + { + return false; + } + + // 提取从机地址 + slaveAddr = static_cast(response[0]); + // 提取功能码 + funcCode = static_cast(response[1]); + + // 判断是否为错误响应 + if (funcCode & 0x80) + { + // 错误响应格式:地址(1) + 错误功能码(1) + 错误码(1) + CRC(2),共5字节 + // 提取错误码 + errorCode = static_cast(response[2]); + return true; + } + + // 正常响应 + errorCode = NO_ERROR; + + // 提取数据部分(去除地址、功能码和CRC) + QByteArray responseData = response.mid(2, response.size() - 4); + + // 根据功能码解析对应的数据 + switch (funcCode) + { + case READ_COILS: + { + QVector coils; + // 解析读线圈响应数据 + if (!parseReadCoilsResponse(responseData, coils)) + { + return false; + } + // 将布尔线圈状态转换为16位整数 + data.clear(); + for (bool coil : coils) + { + int bit = 0; + if(coil) { bit=1; } + data.append(bit); + } + break; + } + case READ_HOLDING_REGISTERS: + { + // 解析读保持寄存器响应数据 + if (!parseReadHoldingRegistersResponse(responseData, data)) + { + return false; + } + break; + } + case WRITE_MULTIPLE_COILS: // 写多个线圈功能码 + case WRITE_MULTIPLE_REGISTERS: // 写多个寄存器功能码 + { + // 写多个线圈/寄存器的响应数据固定为4字节:起始地址(2字节) + 数量(2字节) + if (responseData.size() != 4) + { + return false; + } + // 解析起始地址 + quint16 startAddr = (static_cast(responseData[0]) << 8) | static_cast(responseData[1]); + // 解析写入数量 + quint16 count = (static_cast(responseData[2]) << 8) | static_cast(responseData[3]); + // 存入起始地址和数量 + data.clear(); + data.append(startAddr); + data.append(count); + break; + } + default: // 不支持的功能码 + return false; + } + + return true; +} + +/******************************************************************** + * 解析读线圈响应,[字节数][数据] + ********************************************************************/ +bool ModbusRTUMaster::parseReadCoilsResponse(const QByteArray& responseData, QVector& coils) +{ + // 数据为空,解析失败 + if (responseData.isEmpty()) + { + return false; + } + + quint8 byteCount = static_cast(responseData[0]); + // 验证数据长度是否与字节计数匹配(字节计数 + 自身1字节) + if (responseData.size() != byteCount + 1) + { + return false; + } + + coils.clear(); + int parseCoils = readCount_;//需要解析的线圈数量 + // 遍历每个字节 + for (int currByte = 0; currByte < byteCount && parseCoils > 0; ++currByte) + { + // 获取当前字节的数值 + quint8 byte = static_cast(responseData[currByte + 1]); + // 计算当前字节需要解析的位数(不超过8位,且不超过剩余需要读取的线圈数) + int parseBits = qMin(8, parseCoils); + // 遍历每个位 + for (int currBit = 0; currBit < parseBits; ++currBit) + { + //查看该位是0 or 1并添加 + int mask = 1 << currBit; + bool bitIsSet = ((byte & mask) != 0); + coils.append(bitIsSet); + } + // 更新剩余需要读取的线圈数 + parseCoils -= parseBits; + } + return true; +} + +/******************************************************************** + * 解析读寄存器响应,[字节数][数据] + ********************************************************************/ +bool ModbusRTUMaster::parseReadHoldingRegistersResponse(const QByteArray& responseData, QVector& registers) +{ + if (responseData.isEmpty()) + { + qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "内容:" << "响应数据为空"; + return false; + } + + //字节数 + quint8 byteCount = static_cast(responseData[0]); + if (byteCount != readCount_ * 2 || responseData.size() != byteCount + 1) + { + return false; + } + + //解析寄存器数据 + registers.clear(); + int regCount = byteCount / 2; + for (int i = 0; i < regCount; ++i) + { + if(2 + i * 2 < responseData.size()) + { + //1寄存器数据 2字节 + quint16 reg = static_cast(responseData[1 + i * 2]) << 8 | static_cast(responseData[2 + i * 2]); + registers.append(reg); + } + } + + return true; +} + +/******************************************************************** + * 计算CRC校验值 + ********************************************************************/ +quint16 ModbusRTUMaster::calculateCRC(const QByteArray &data) +{ + quint16 crc = 0xFFFF; + for (int i = 0; i < data.size(); ++i) + { + crc ^= (quint8)data[i]; + for (int j = 0; j < 8; ++j) + { + bool carry = crc & 0x0001; + crc >>= 1; + if (carry) + { + crc ^= 0xA001; + } + } + } + return crc; +} + +/******************************************************************** + * 验证响应帧校验码 + ********************************************************************/ +bool ModbusRTUMaster::verifyCRC(const QByteArray& frame) +{ + // 帧至少需要包含2字节CRC + if (frame.size() < 2) + { + return false; + } + + // 提取帧中的CRC值(小端) + quint16 receivedCRC = (static_cast(frame[frame.size() - 1]) << 8) | + static_cast(frame[frame.size() - 2]); + + // 计算数据部分的CRC + QByteArray data = frame.left(frame.size() - 2); + quint16 calculatedCRC = calculateCRC(data); + + // 比较接收的CRC和计算的CRC + return (receivedCRC == calculatedCRC); +} diff --git a/Modbus/src/serial_communication.cpp b/Modbus/src/serial_communication.cpp new file mode 100644 index 0000000..2cf24ee --- /dev/null +++ b/Modbus/src/serial_communication.cpp @@ -0,0 +1,371 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: // serial_communication.cpp +* Description: // 主站串口通信模块源文件,负责串口的连接管理,数据收发管理 +* //调用超时处理TimeoutHandler实现超时重传 +* Others: // +* Version: // v1.0 +* Author: // weikai,XINJE +* Date: // 2025-7-30 +***********************************************************************/ + +#include "serial_communication.h" +#include + +SerialCommunicator::SerialCommunicator(QObject *parent) + : QObject(parent), + serialPort_(new QSerialPort(this)), + connected_(false), + timeoutHandler_(this), + heartbeatTimer_(new QTimer(this)), + heartbeatTimeoutTimer_(new QTimer(this)) +{ + // 连接串口接收信号到内部处理槽 + connect(serialPort_, &QSerialPort::readyRead, this, &SerialCommunicator::onReadyRead);//串口收到数据 + //连接串口错误信号 + connect(serialPort_, &QSerialPort::errorOccurred, this, &SerialCommunicator::onSerialError);//串口错误 + + // 连接超时处理器的信号 + connect(&timeoutHandler_, &TimeoutHandler::timeoutOccurred,this, &SerialCommunicator::onTimeoutOccurred);//定时到达,触发超时重传 + connect(&timeoutHandler_, &TimeoutHandler::maxRetriesReached,this, &SerialCommunicator::onMaxRetriesReached);//到达最大重发次数 + + //连接心跳 + connect(heartbeatTimer_, &QTimer::timeout ,this, &SerialCommunicator::onSendHeartbeat);//心跳触发 + connect(heartbeatTimeoutTimer_, &QTimer::timeout, this, &SerialCommunicator::onHeartbeatTimeout);//心跳超时 + //初始化成员变量 + init(); +} + +SerialCommunicator::~SerialCommunicator() +{ + if (connected_) + { + serialPort_->close(); + } +} + +// 参数设置实现 +void SerialCommunicator::setPortName(const QString &portName) +{ + portName_ = portName; +} + +void SerialCommunicator::setBaudRate(int baudRate) +{ + baudRate_ = baudRate; +} + +void SerialCommunicator::setDataBits(QSerialPort::DataBits dataBits) +{ + dataBits_ = dataBits; +} + +void SerialCommunicator::setStopBits(QSerialPort::StopBits stopBits) +{ + stopBits_ = stopBits; +} + +void SerialCommunicator::setParity(QSerialPort::Parity parity) +{ + parity_ = parity; +} + +/******************************************************************** + * 初始化 + ********************************************************************/ +void SerialCommunicator::init() +{ + //串口参数 + portName_ = ""; + baudRate_ = 9600; + dataBits_ = QSerialPort::Data8; + stopBits_ = QSerialPort::OneStop; + parity_ = QSerialPort::NoParity; + connected_ = false; + + //心跳参数 + heartbeatInterval_ = 3000; + heartbeatTimeout_ = 1000; + isCanHeartbeat_ = true; +} + +/******************************************************************** + * 连接设备 + ********************************************************************/ +bool SerialCommunicator::connectDevice() +{ + if (connected_) + { + emit statusChanged("已处于连接状态"); + return true; + } + + // 检查端口名是否有效 + if (portName_.isEmpty()) + { + emit errorOccurred("未设置端口名"); + return false; + } + + // 配置串口参数 + serialPort_->setPortName(portName_); + serialPort_->setBaudRate(baudRate_); + serialPort_->setDataBits(dataBits_); + serialPort_->setStopBits(stopBits_); + serialPort_->setParity(parity_); + serialPort_->setFlowControl(QSerialPort::NoFlowControl); // 默认无流控 + + // 尝试打开串口 + if (serialPort_->open(QIODevice::ReadWrite)) + { + connected_ = true; + emit statusChanged("连接成功: " + portName_); + qDebug()<<__FILE__<<__LINE__<<"心跳机制启动"; + //启动心跳机制 + startHeartbeat(); + return true; + } + else + { + emit errorOccurred("连接失败: " + serialPort_->errorString()); + return false; + } +} + +/******************************************************************** +* 断开连接 +********************************************************************/ +void SerialCommunicator::disconnectDevice() +{ + if (!connected_) return; + + serialPort_->close(); + connected_ = false; + //停止心跳机制 + stopHeartbeat(); + emit connectionDisconnected(); +} + +/******************************************************************** +* 启动心跳机制 +********************************************************************/ +void SerialCommunicator::startHeartbeat() +{ + if(connected_) + { + //启动心跳 + heartbeatTimer_->start(heartbeatInterval_); + //qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳机制启动" << heartbeatInterval_ << "ms"; + } +} + +/******************************************************************** +* 停止心跳机制 +********************************************************************/ +void SerialCommunicator::stopHeartbeat() +{ + heartbeatTimer_->stop(); + heartbeatTimeoutTimer_->stop(); + //qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳机制终止" << heartbeatInterval_ << "ms"; +} + +/******************************************************************** +* 心跳触发,发送心跳帧 +********************************************************************/ +void SerialCommunicator::onSendHeartbeat() +{ + if(false == isCanHeartbeat_) + return; + + if(!connected_) + { + qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "未连接,停止发送心跳帧"; + return; + } + + qint64 success = serialPort_->write(heartbeatFrame_); + //qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳帧: " <start(heartbeatTimeout_); + qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳帧发送成功"; + } + +} + +/******************************************************************** +* 心跳超时触发,断开连接 +********************************************************************/ +void SerialCommunicator::onHeartbeatTimeout() +{ + //qDebug() << "文件:" << __FILE__ << "行:" << __LINE__ << "心跳超时" << heartbeatTimeout_; + disconnectDevice(); +} + +/******************************************************************** +* 设置超时参数 +********************************************************************/ +bool SerialCommunicator::setTimeoutSettings(int timeoutMs, int maxRetries) +{ + if(timeoutMs < 500 || timeoutMs > 5000 || maxRetries < 1 || maxRetries >5) + { + return false; + } + timeoutHandler_.setTimeoutInterval(timeoutMs); + timeoutHandler_.setRetryCount(maxRetries); + return true; +} + +/******************************************************************** +* 获取超时参数 +********************************************************************/ +void SerialCommunicator::getTimeoutSettings(int &timeoutMs,int &maxRetries) +{ + timeoutMs=timeoutHandler_.getTimeoutInterval(); + maxRetries=timeoutHandler_.getRetryCount(); +} + +/******************************************************************** +* 发送数据,启动计时 +********************************************************************/ +bool SerialCommunicator::sendData(const QByteArray &data) +{ + if (!connected_) + { + emit errorOccurred("发送失败: 未连接设备"); + return false; + } + if (data.isEmpty()) + { + emit errorOccurred("发送失败: 数据为空"); + return false; + } + isCanHeartbeat_ = false; + // 停止并重置超时处理器 + timeoutHandler_.stop(); + + // 保存数据用于可能的重发 + pendingData_ = data; + + // 发送数据 + qint64 bytesWritten = serialPort_->write(data); + + if (-1 == bytesWritten) + { + emit errorOccurred("发送失败: " + serialPort_->errorString()); + return false; + } + else + { + // 启动超时计时 + timeoutHandler_.start(); + return true; + } +} + +/******************************************************************** +* 检查是否连接 +********************************************************************/ +bool SerialCommunicator::isConnected() const +{ + return connected_; +} + +/******************************************************************** +* 获取可用串口 +********************************************************************/ +QStringList SerialCommunicator::getAvailablePorts() +{ + QStringList portList; + for (const QSerialPortInfo &info : QSerialPortInfo::availablePorts()) + { + QString portDescription = info.portName() + " (" + info.description() + ")"; + portList << portDescription; + } + return portList; +} + +/******************************************************************** +* 串口接收到数据,停止计时,并发送信号 +********************************************************************/ +void SerialCommunicator::onReadyRead() +{ + //qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << " 收到响应"; + if (!connected_) return; + + QByteArray data = serialPort_->readAll(); + if (data.isEmpty()) + { + emit statusChanged("接收为空数据"); + return; + } + + //如果心跳在计时,停止 + if(heartbeatTimeoutTimer_->isActive()) + { + heartbeatTimeoutTimer_->stop(); + } + // 停止超时计时器(收到响应) + if (timeoutHandler_.isRunning()) + { + timeoutHandler_.stop(); + } + + //处于心跳阶段,数据直接丢弃 + if(true == isCanHeartbeat_) + { + data.clear(); + } + // 转换为带空格的十六进制字符串 + QString hexData = data.toHex(' ').toUpper(); + // 发射数据接收信号 + emit dataReceived(hexData.trimmed()); +} + +// 处理超时信号 +void SerialCommunicator::onTimeoutOccurred(int currentRetry) +{ + if (!connected_) + { + qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "设备未连接,取消重试"; + return; + } + + // 重发数据 + qint64 bytesWritten = serialPort_->write(pendingData_); + + if (-1 == bytesWritten) + { + emit errorOccurred("重发失败: " + serialPort_->errorString()); + timeoutHandler_.stop(); // 停止重试 + } + else + { + emit statusChanged(QString("超时重发 (%1/%2)").arg(currentRetry) + .arg(timeoutHandler_.getRetryCount())); + } +} + +// 处理最大重试次数达到 +void SerialCommunicator::onMaxRetriesReached() +{ + qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "达到最大重试次数"; + emit statusChanged("通信超时,请检查连接后再试!"); + disconnectDevice(); +} + +// 处理串口错误(如设备拔出) +void SerialCommunicator::onSerialError(QSerialPort::SerialPortError error) +{ + if (QSerialPort::ResourceError == error && connected_) + { + qDebug() <<"文件:"<<__FILE__ << "行数:" << __LINE__ << "串口错误: 设备可能已被拔出或不可用"; + disconnectDevice(); // 调用断开连接,触发 connectionDisconnected 信号 + } +} diff --git a/Modbus/src/timeout_handler.cpp b/Modbus/src/timeout_handler.cpp new file mode 100644 index 0000000..cf51975 --- /dev/null +++ b/Modbus/src/timeout_handler.cpp @@ -0,0 +1,118 @@ +/*********************************************************************** +* Copyright (C) 2025-, XINJE Co., Ltd. +* +* File Name: // serial_communication.cpp +* Description: // 主站超时处理模块源文件 +* Others: // +* Version: // v1.0 +* Author: // weikai,XINJE +* Date: // 2025-7-30 +***********************************************************************/ + +#include "timeout_handler.h" +#include "mainwindow.h" +#include + +TimeoutHandler::TimeoutHandler(QObject *parent) + : QObject(parent), + timer_(new QTimer(this)), + timeoutInterval_(DEFAULT_TIMEOUT), + maxRetryCount_(DEFAULT_MAXRETRISE), + currentRetryCount_(0) +{ + // 配置定时器为单次触发 + timer_->setSingleShot(true); + // 连接定时器超时信号到内部处理函数 + connect(timer_, &QTimer::timeout, this, &TimeoutHandler::onTimerTimeout); +} + +/******************************************************************** +* 设置超时时间 +********************************************************************/ +void TimeoutHandler::setTimeoutInterval(int msec) +{ + timeoutInterval_ = msec; +} + +/******************************************************************** +* 获取超时时间 +********************************************************************/ +int TimeoutHandler::getTimeoutInterval() const +{ + return timeoutInterval_; +} + +/******************************************************************** +* 设置最大重发次数 +********************************************************************/ +void TimeoutHandler::setRetryCount(int count) +{ + maxRetryCount_ = count; +} + +/******************************************************************** +* 获取设置的最大重发次数 +********************************************************************/ +int TimeoutHandler::getRetryCount() const +{ + return maxRetryCount_; +} + +/******************************************************************** +* 启动超时计时 +********************************************************************/ +void TimeoutHandler::start() +{ + // 若已在运行,先停止 + if (timer_->isActive()) + { + timer_->stop(); + } + // 启动定时器 + timer_->start(timeoutInterval_); +} + +/******************************************************************** +* 停止超时计时,重置重发次数 +********************************************************************/ +void TimeoutHandler::stop() +{ + if (timer_->isActive()) + { + timer_->stop(); + } + // 重置重试计数 + currentRetryCount_ = 0; +} + +/******************************************************************** +* 判断是否在计时中 +********************************************************************/ +bool TimeoutHandler::isRunning() const +{ + return timer_->isActive(); +} + +/******************************************************************** +* 超时信号触发的槽函数,发送超时发生给串口通信模块,启动重发 +********************************************************************/ +void TimeoutHandler::onTimerTimeout() +{ + // 增加重发计数 + currentRetryCount_++; + + // 检查是否达到最大重发次数 + if (currentRetryCount_ <= maxRetryCount_) + { + // 发送超时信号,显示从 1 开始的计数 + emit timeoutOccurred(currentRetryCount_); + // 继续下一次重发 + timer_->start(timeoutInterval_); + } + else + { + // 达到最大重发次数,发送信号并重置计数 + emit maxRetriesReached(); + currentRetryCount_ = 0; + } +} diff --git a/ModbusMatser.pro b/ModbusMatser.pro new file mode 100644 index 0000000..fdc83c4 --- /dev/null +++ b/ModbusMatser.pro @@ -0,0 +1,38 @@ +QT += core gui +QT += serialport +QT += core +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++11 + +# The following define makes your compiler emit warnings if you use +# any Qt feature that has been marked deprecated (the exact warnings +# depend on your compiler). Please consult the documentation of the +# deprecated API in order to know how to port your code away from it. +DEFINES += QT_DEPRECATED_WARNINGS + +# You can also make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +# You can also select to disable deprecated APIs only up to a certain version of Qt. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp \ + mainwindow.cpp \ + modbus_master.cpp \ + serial_communication.cpp \ + timeout_handler.cpp + +HEADERS += \ + mainwindow.h \ + modbus_master.h \ + serial_communication.h \ + timeout_handler.h + +FORMS += \ + mainwindow.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/UnitTest/tst_test_modbus.cpp b/UnitTest/tst_test_modbus.cpp new file mode 100644 index 0000000..dd164af --- /dev/null +++ b/UnitTest/tst_test_modbus.cpp @@ -0,0 +1,165 @@ +#include +#include "../ModbusMatser/modbus_master.h" + +// 单元测试类,用于测试 ModbusRTUMaster 类的解析函数 +class Test_Modbus : public QObject +{ + Q_OBJECT + +public: + Test_Modbus(); + ~Test_Modbus(); + +private slots: + // 测试用例声明 + void test_parseResponse(); // 测试 parseResponse 函数 + void test_parseReadCoilsResponse(); // 测试 parseReadCoilsResponse 函数 + void test_parseReadHoldingRegistersResponse(); // 测试 parseReadHoldingRegistersResponse 函数 +}; + +// 构造函数 +Test_Modbus::Test_Modbus() +{ +} + +// 析构函数 +Test_Modbus::~Test_Modbus() +{ +} + +// 测试 parseResponse 函数 +void Test_Modbus::test_parseResponse() +{ + ModbusRTUMaster modbus; + quint8 slaveAddr, funcCode, errorCode; + QVector parsedData; + + // 测试用例 1: 正常读保持寄存器响应(功能码03,2个寄存器) + // 帧格式: [从站地址:01] [功能码:03] [字节数:04] [数据:0001 0003] [CRC] + QByteArray response = QByteArray::fromHex("01030400010003EBF2"); + modbus.setRegCount(2); // 设置期望的寄存器数量 + QVERIFY(modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); + QCOMPARE(slaveAddr, static_cast(0x01)); // 验证从站地址 + QCOMPARE(funcCode, static_cast(0x03)); // 验证功能码 + QCOMPARE(errorCode, ModbusRTUMaster::NO_ERROR); // 验证无错误 + QCOMPARE(parsedData.size(), 2); // 验证数据长度 + QCOMPARE(parsedData[0], static_cast(0x0001)); // 验证寄存器1 + QCOMPARE(parsedData[1], static_cast(0x0003)); // 验证寄存器3 + + // 测试用例 2: 正常读线圈响应(功能码01,8个线圈) + // 帧格式: [从站地址:01] [功能码:01] [字节数:01] [数据:FF] [CRC] + modbus.setCoilCount(8); // 设置期望的线圈数量 + response = QByteArray::fromHex("010101FF11C8"); + QVERIFY(modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); + QCOMPARE(slaveAddr, static_cast(0x01)); // 验证从站地址 + QCOMPARE(funcCode, static_cast(0x01)); // 验证功能码 + QCOMPARE(errorCode, ModbusRTUMaster::NO_ERROR); // 验证无错误 + QCOMPARE(parsedData.size(), 8); // 验证数据长度 + for (int i = 0; i < 8; ++i) + { + QCOMPARE(parsedData[i], static_cast(1)); // 验证所有线圈为1 + } + + // 测试用例 3: 错误响应(功能码83,非法功能) + // 帧格式: [从站地址:01] [功能码:83] [错误码:01] [CRC] + parsedData.clear(); + response = QByteArray::fromHex("01830180F0"); + QVERIFY(modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); + QCOMPARE(slaveAddr, static_cast(0x01)); // 验证从站地址 + QCOMPARE(funcCode, static_cast(0x83)); // 验证功能码 + QCOMPARE(errorCode, static_cast(0x01)); // 验证错误码 + QCOMPARE(parsedData.size(), 0); // 验证无数据 + + // 测试用例 4: 数据帧过短(无效帧) + response = QByteArray::fromHex("0103"); // 只有2字节,少于最小长度 + QVERIFY(!modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); + + // 测试用例 5: CRC错误 + // 帧格式: [从站地址:01] [功能码:03] [字节数:04] [数据:0001 0004] [错误CRC] + response = QByteArray::fromHex("01030400010004AB30"); + QVERIFY(!modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); + + // 测试用例 6: 不支持的功能码 + // 帧格式: [从站地址:01] [功能码:05] [数据] [CRC] + response = QByteArray::fromHex("01050400010002D6E9"); + QVERIFY(!modbus.parseResponse(response, slaveAddr, funcCode, parsedData, errorCode)); +} + +// 测试 parseReadCoilsResponse 函数 +void Test_Modbus::test_parseReadCoilsResponse() +{ + ModbusRTUMaster modbus; + QVector coils; + + // 测试用例 1: 正常读线圈响应(8个线圈全1) + // 数据格式: [字节数:01] [数据:FF] + modbus.setCoilCount(8); + QByteArray responseData = QByteArray::fromHex("01FF"); + QVERIFY(modbus.parseReadCoilsResponse(responseData, coils)); + QCOMPARE(coils.size(), 8); // 验证线圈数量 + for (int i = 0; i < 8; ++i) + { + QCOMPARE(coils[i], true); // 验证每个线圈状态为1 + } + + // 测试用例 2: 正常读线圈响应(5个线圈,前5位为1) + // 数据格式: [字节数:01] [数据:1F] + modbus.setCoilCount(5); + responseData = QByteArray::fromHex("011F"); + QVERIFY(modbus.parseReadCoilsResponse(responseData, coils)); + QCOMPARE(coils.size(), 5); // 验证线圈数量 + for (int i = 0; i < 5; ++i) + { + QCOMPARE(coils[i], true); // 验证每个线圈状态为1 + } + + // 测试用例 3: 字节数不匹配 + // 数据格式: [字节数:02] [数据:03](数据长度与字节数声明不符) + responseData = QByteArray::fromHex("0203"); + QVERIFY(!modbus.parseReadCoilsResponse(responseData, coils)); + + // 测试用例 4: 空数据 + responseData = QByteArray(); + QVERIFY(!modbus.parseReadCoilsResponse(responseData, coils)); +} + +// 测试 parseReadHoldingRegistersResponse 函数 +void Test_Modbus::test_parseReadHoldingRegistersResponse() +{ + ModbusRTUMaster modbus; + QVector registers; + + // 测试用例 1: 正常读保持寄存器响应(2个寄存器) + // 数据格式: [字节数:04] [数据:0001 0002] + modbus.setRegCount(2); + QByteArray responseData = QByteArray::fromHex("0400010002"); + QVERIFY(modbus.parseReadHoldingRegistersResponse(responseData, registers)); + QCOMPARE(registers.size(), 2); // 验证寄存器数量 + QCOMPARE(registers[0], static_cast(0x0001)); // 验证寄存器1 + QCOMPARE(registers[1], static_cast(0x0002)); // 验证寄存器2 + + // 测试用例 2: 读取10个寄存器 + // 数据格式: [字节数:14] [数据:0001 0002 ... 000A] + modbus.setRegCount(10); + responseData = QByteArray::fromHex("14000100020003000400050006000700080009000A"); + QVERIFY(modbus.parseReadHoldingRegistersResponse(responseData, registers)); + QCOMPARE(registers.size(), 10); // 验证寄存器数量 + for (int i = 0; i < 10; ++i) + { + QCOMPARE(registers[i], static_cast(i + 1)); // 验证每个寄存器值 + } + + // 测试用例 3: 字节数不匹配 + // 数据格式: [字节数:02] [数据:0001](数据长度与字节数声明不符) + responseData = QByteArray::fromHex("020001"); + QVERIFY(!modbus.parseReadHoldingRegistersResponse(responseData, registers)); + + // 测试用例 4: 空数据 + responseData = QByteArray(); + QVERIFY(!modbus.parseReadHoldingRegistersResponse(responseData, registers)); +} + + +QTEST_APPLESS_MAIN(Test_Modbus) + +#include "tst_test_modbus.moc" diff --git a/UnitTest/untitled.pro b/UnitTest/untitled.pro new file mode 100644 index 0000000..c0a697d --- /dev/null +++ b/UnitTest/untitled.pro @@ -0,0 +1,18 @@ +QT += testlib widgets serialport +QT += testlib +QT -= gui + + + +TEMPLATE = app + +SOURCES += tst_test_modbus.cpp \ + ../ModbusMatser/modbus_master.cpp + +HEADERS += \ + ../ModbusMatser/modbus_master.h + +TARGET = modbus_master +CONFIG += console +CONFIG -= app_bundle + diff --git a/UnitTest/untitled.pro.user b/UnitTest/untitled.pro.user new file mode 100644 index 0000000..5a93414 --- /dev/null +++ b/UnitTest/untitled.pro.user @@ -0,0 +1,319 @@ + + + + + + EnvironmentId + {54b998bd-0b43-4b57-8d79-23e6237f0a57} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + -fno-delayed-template-parsing + + true + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.14.2 MinGW 64-bit + Desktop Qt 5.14.2 MinGW 64-bit + qt.qt5.5142.win64_mingw73_kit + 0 + 0 + 0 + + D:/QT/QTProject/build-untitled-Desktop_Qt_5_14_2_MinGW_64_bit-Debug + + + true + QtProjectManager.QMakeBuildStep + true + + false + false + false + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + + + D:/QT/QTProject/build-untitled-Desktop_Qt_5_14_2_MinGW_64_bit-Release + + + true + QtProjectManager.QMakeBuildStep + false + + false + false + true + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + + + D:/QT/QTProject/build-untitled-Desktop_Qt_5_14_2_MinGW_64_bit-Profile + + + true + QtProjectManager.QMakeBuildStep + true + + false + true + true + + + true + Qt4ProjectManager.MakeStep + + false + + + false + + 2 + Build + Build + ProjectExplorer.BuildSteps.Build + + + + true + Qt4ProjectManager.MakeStep + + true + clean + + false + + 1 + Clean + Clean + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + + 3 + + + 0 + Deploy + Deploy + ProjectExplorer.BuildSteps.Deploy + + 1 + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + Qt4ProjectManager.Qt4RunConfiguration:D:/QT/QTProject/untitled/untitled.pro + D:/QT/QTProject/untitled/untitled.pro + + false + + false + true + true + false + false + true + + D:/QT/QTProject/build-untitled-Desktop_Qt_5_14_2_MinGW_64_bit-Debug + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + +