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
+
+
+
+
+
+
+
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
+
+