diff --git a/communicationhistory.cpp b/communicationhistory.cpp index 6268949..e9103cd 100644 --- a/communicationhistory.cpp +++ b/communicationhistory.cpp @@ -1,54 +1,133 @@ +/******************************* + * Copyright (C) 2025-. + * + * File Name: communicationhistory.cpp + * Description: 通信历史记录模块头文件,提供通信数据的保存和读取功能 + * Others: + * Version: 1.0.0 + * Author: lipengpeng + * Date: 2025-7-25 + *******************************/ + #include "communicationhistory.h" +/** + * @brief 保存文本内容到指定文件 + * @param parent 父窗口指针,用于对话框的模态显示 + * @param edit 包含待保存数据的文本编辑框指针 + * @return bool 保存成功返回true,用户取消或失败返回false + * @note 功能流程: + * 1. 弹出标准文件保存对话框 + * 2. 以追加模式打开文件(保留历史记录) + * 3. 自动处理文件内容分隔(添加换行符) + * 4. 使用UTF-8编码保存文本 + * 5. 错误处理包含用户提示 + */ bool SaveDate(QWidget *parent, QTextEdit *edit) { + // 弹出文件保存对话框,默认保存到用户主目录的note.txt文件 QString fileName = QFileDialog::getSaveFileName( - parent, - "保存文本", - QDir::homePath() + "/note.txt", - "文本文件 (*.txt);;所有文件 (*)"); + parent, + "保存文本", // 对话框标题 + QDir::homePath() + "/note.txt", // 默认路径和文件名 + "文本文件 (*.txt);;所有文件 (*)"); // 文件过滤器 + // 检查用户是否取消操作 if (fileName.isEmpty()) + { return false; + } + // 以追加模式和文本模式打开文件 QFile file(fileName); if (!file.open(QIODevice::Append | QIODevice::Text)) { - QMessageBox::warning(parent, "错误", "无法打开文件"); + QMessageBox::warning(parent, + "错误", + QString("无法打开文件\n%1").arg(file.errorString())); return false; } + // 如果文件已有内容,先追加换行符保持内容分隔 if (file.size() > 0) - file.write("\n"); + { + if (file.write("\n") == -1) + { + // 检查换行符写入是否成功 + QMessageBox::warning(parent, "错误", "无法写入分隔符"); + file.close(); + return false; + } + } + // 设置UTF-8编码并写入文本内容 QTextStream out(&file); out.setCodec("UTF-8"); out << edit->toPlainText(); + + // 检查流状态确保写入成功 + if (out.status() != QTextStream::Ok) + { + QMessageBox::warning(parent, "错误", "写入文件时发生错误"); + file.close(); + return false; + } + file.close(); return true; } +/** + * @brief 从文件读取文本内容 + * @param parent 父窗口指针,用于对话框的模态显示 + * @param edit 用于显示读取内容的文本编辑框指针 + * @return bool 读取成功返回true,用户取消或失败返回false + * @note 功能流程: + * 1. 弹出标准文件打开对话框 + * 2. 以只读模式打开文件 + * 3. 使用UTF-8编码读取内容 + * 4. 错误处理包含用户提示 + */ bool ReadDate(QWidget *parent, QTextEdit *edit) { + // 弹出文件打开对话框,默认从用户主目录开始浏览 QString fileName = QFileDialog::getOpenFileName( - parent, - "打开文本", - QDir::homePath(), - "文本文件 (*.txt);;所有文件 (*)"); + parent, + "打开文本", // 对话框标题 + QDir::homePath(), // 默认路径 + "文本文件 (*.txt);;所有文件 (*)"); // 文件过滤器 + // 检查用户是否取消操作 if (fileName.isEmpty()) + { return false; + } + // 以只读模式和文本模式打开文件 QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::warning(parent, "错误", "无法读取文件"); + QMessageBox::warning(parent, + "错误", + QString("无法读取文件\n%1").arg(file.errorString())); return false; } + // 设置UTF-8编码并读取全部内容 QTextStream in(&file); in.setCodec("UTF-8"); - edit->setPlainText(in.readAll()); + QString content = in.readAll(); + + // 检查流状态确保读取成功 + if (in.status() != QTextStream::Ok) + { + QMessageBox::warning(parent, "错误", "读取文件时发生错误"); + file.close(); + return false; + } + + // 将内容设置到文本编辑框 + edit->setPlainText(content); file.close(); return true; } diff --git a/communicationhistory.h b/communicationhistory.h index c8b040c..ec6691b 100644 --- a/communicationhistory.h +++ b/communicationhistory.h @@ -1,3 +1,14 @@ +/******************************* + * Copyright (C) 2025-. + * + * File Name: communicationhistory.h + * Description: 通信历史记录模块头文件,提供通信数据的保存和读取功能 + * Others: + * Version: 1.0.0 + * Author: lipengpeng + * Date: 2025-7-25 + *******************************/ + #ifndef COMMUNICATIONHISTORY_H #define COMMUNICATIONHISTORY_H @@ -10,8 +21,30 @@ #include #include +/** + * @brief 保存通信数据到文件 + * @param parent 父窗口指针,用于对话框的模态显示 + * @param edit 包含待保存通信数据的文本编辑框指针 + * @return bool 保存成功返回true,失败返回false + * @note 此函数将: + * 1. 弹出文件保存对话框让用户选择保存路径 + * 2. 将文本编辑框内容写入选定文件 + * 3. 支持UTF-8编码格式保存 + * 4. 操作失败时会显示错误提示 + */ bool SaveDate(QWidget *parent, QTextEdit *edit); +/** + * @brief 从文件读取通信数据 + * @param parent 父窗口指针,用于对话框的模态显示 + * @param edit 用于显示读取内容的文本编辑框指针 + * @return bool 读取成功返回true,失败返回false + * @note 此函数将: + * 1. 弹出文件选择对话框让用户选择文件 + * 2. 读取选定文件内容到文本编辑框 + * 3. 支持UTF-8编码格式读取 + * 4. 操作失败时会显示错误提示 + */ bool ReadDate(QWidget *parent, QTextEdit *edit); #endif // COMMUNICATIONHISTORY_H diff --git a/crc.cpp b/crc.cpp deleted file mode 100644 index 00f9a72..0000000 --- a/crc.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include "crc.h" - -quint16 CalculateCrc(const QByteArray &data) -{ - quint16 crc = 0xFFFF; // MODBUS初始值 - const quint16 polynomial = 0xA001; // MODBUS多项式(0x8005的反转) - - for (int i = 0; i < data.size(); ++i) - { - crc ^= static_cast(data.at(i)); - - for (int bit = 0; bit < 8; ++bit) - { - if (crc & 0x0001) - { // 检查最低位 - crc = (crc >> 1) ^ polynomial; // 右移并异或 - } else - { - crc >>= 1; // 仅右移 - } - } - } - return crc; -} - -bool CrcCheck(const QByteArray &data) -{ - //首先对接收报文的长度进行检验 - if (data.size() < 3) - { - return false; - } - //对接收的报文进行CRC校验 - QByteArray payload = data.left(data.length() - 2); - - //分离接收值的crc校验位 - quint8 receivedCrcLow = static_cast(data.at(data.length() - 2)); - quint8 receivedCrcHigh = static_cast(data.at(data.length() - 1)); - - //计算返回的报文的crc - quint16 crc = CalculateCrc(payload); - quint8 calcCrcLow = crc & 0xFF; - quint8 calcCrcHigh = (crc >> 8) & 0xFF; - - //比较计算的crc值和接收到的crc值是否一致 - if(calcCrcLow == receivedCrcLow && calcCrcHigh == receivedCrcHigh) - { - return true; - } - else - { - return false; - } -} diff --git a/crc.h b/crc.h deleted file mode 100644 index d4c1d7b..0000000 --- a/crc.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef CRC_H -#define CRC_H - -#include -#include - -quint16 CalculateCrc(const QByteArray &data); -bool CrcCheck(const QByteArray &data); - -#endif // CRC_H diff --git a/modbus.pro b/modbus.pro index e6da06e..2dae3d8 100644 --- a/modbus.pro +++ b/modbus.pro @@ -15,12 +15,10 @@ TEMPLATE = app SOURCES += main.cpp\ widget.cpp \ - crc.cpp \ mymodbus.cpp \ communicationhistory.cpp HEADERS += widget.h \ - crc.h \ mymodbus.h \ communicationhistory.h diff --git a/modbus.pro.user b/modbus.pro.user index bce9a42..1101bb9 100644 --- a/modbus.pro.user +++ b/modbus.pro.user @@ -1,6 +1,6 @@ - + EnvironmentId diff --git a/mymodbus.cpp b/mymodbus.cpp index 07b1836..7395122 100644 --- a/mymodbus.cpp +++ b/mymodbus.cpp @@ -1,14 +1,37 @@ -#include "mymodbus.h" +/****************************************************************************** +* Copyright (C) 2025-. +* +* File Name: mymodbus.cpp +* Description: Modbus RTU协议封装类,支持线圈和寄存器的读写操作 +* Others: +* Version: 1.0 +* Author: lipengpeng +* Date: 2025-7-23 +* +******************************************************************************/ +#include "mymodbus.h" +/** + * @brief MyModbus类构造函数 + * @note 初始化默认Modbus参数:站地址=1,功能码=0x01(读线圈), + * 起始地址=256,数据长度=1 + */ MyModbus::MyModbus() { - this->stationAddress_ = 1; - this->functionCode_ = 0x01; - this->startAdress_ = 256; - this->length_ = 1; + this->stationAddress_ = 1; // 默认站地址 + this->functionCode_ = 0x01; // 默认功能码(读线圈) + this->startAdress_ = 256; // 默认起始地址 + this->length_ = 1; // 默认数据长度 } +/** + * @brief 设置Modbus通信参数 + * @param stationAddress 从站地址 + * @param functionCode 功能码 + * @param startAdress 起始地址 + * @param length 数据长度 + */ void MyModbus::Set(quint16 stationAddress, quint16 functionCode, quint16 startAdress, quint16 length) { this->stationAddress_ = stationAddress; @@ -18,92 +41,129 @@ void MyModbus::Set(quint16 stationAddress, quint16 functionCode, quint16 startAd } +/** + * @brief 生成读取线圈/寄存器的Modbus命令 + * @note 命令结构:[站地址][功能码][起始地址Hi][起始地址Lo][长度Hi][长度Lo][CRC Lo][CRC Hi] + */ void MyModbus::ReadCoilAndReg() { sendCommand_.clear(); + // 构建标准读命令帧 sendCommand_.append(stationAddress_%256); sendCommand_.append(functionCode_%256); sendCommand_.append(startAdress_/256); sendCommand_.append(startAdress_%256); sendCommand_.append(length_/256); sendCommand_.append(length_%256); + + // 计算并附加CRC校验 quint16 temp = CalculateCrc(sendCommand_); - sendCommand_.append(temp%256); - sendCommand_.append(temp/256); + sendCommand_.append(temp%256); // CRC低字节在前 + sendCommand_.append(temp/256); // CRC高字节在后 } +/** + * @brief 生成写线圈命令 + * @param coils 要写入的线圈状态数组 + * @note 命令结构:[站地址][0x0F][起始地址Hi][起始地址Lo][数量Hi][数量Lo][字节数][数据...][CRC] + */ void MyModbus::WriteCoil(QVector &coils) { quint16 coilCount = coils.size(); - int byteCount = (coilCount + 7) / 8; + int byteCount = (coilCount + 7) / 8; // 计算所需字节数(每字节8位) sendCommand_.clear(); sendCommand_.append(stationAddress_%256); - sendCommand_.append(0x0f); + sendCommand_.append(0x0F); // 写多个线圈功能码 sendCommand_.append(startAdress_/256); sendCommand_.append(startAdress_%256); - sendCommand_.append(length_/256); - sendCommand_.append(length_%256); - sendCommand_.append(byteCount); + sendCommand_.append(coilCount/256); // 线圈数量 + sendCommand_.append(coilCount%256); + sendCommand_.append(byteCount); // 后续字节数 + + // 打包线圈状态到字节 for (int i = 0; i < byteCount; ++i) { quint8 byte = 0; for (int j = 0; j < 8; ++j) { int bitIndex = i * 8 + j; + // 只处理有效范围内的线圈 if (bitIndex < coils.size() && coils[bitIndex]) - byte |= (1 << j); + { + byte |= (1 << j); // 设置对应位 + } } sendCommand_.append(static_cast(byte)); } - quint16 temp = CalculateCrc(sendCommand_); //计算crc - sendCommand_.append(temp%256); //加入计算的crc值 + // 计算并附加CRC校验 + quint16 temp = CalculateCrc(sendCommand_); + sendCommand_.append(temp%256); sendCommand_.append(temp/256); } +/** + * @brief 生成写寄存器命令 + * @param values 要写入的寄存器值数组 + * @note 命令结构:[站地址][0x10][起始地址Hi][起始地址Lo][数量Hi][数量Lo][字节数][数据Hi][数据Lo...][CRC] + */ void MyModbus::WriteRegister(QVector &values) { sendCommand_.clear(); sendCommand_.append(stationAddress_%256); - sendCommand_.append(0x10); + sendCommand_.append(0x10); // 写多个寄存器功能码 sendCommand_.append(startAdress_/256); sendCommand_.append(startAdress_%256); - sendCommand_.append(length_/256); - sendCommand_.append(length_%256); - sendCommand_.append(static_cast(values.size() * 2)); + sendCommand_.append(values.size()/256); // 寄存器数量 + sendCommand_.append(values.size()%256); + sendCommand_.append(static_cast(values.size() * 2)); // 后续字节数(每个寄存器2字节) + + // 打包寄存器值(高位在前) for (quint16 v : values) { - sendCommand_.append(static_cast((v >> 8) & 0xFF)); - sendCommand_.append(static_cast(v & 0xFF)); + sendCommand_.append(static_cast((v >> 8) & 0xFF)); // 高字节 + sendCommand_.append(static_cast(v & 0xFF)); // 低字节 } - quint16 temp = CalculateCrc(sendCommand_); //计算crc - sendCommand_.append(temp%256); //加入计算的crc值 + + // 计算并附加CRC校验 + quint16 temp = CalculateCrc(sendCommand_); + sendCommand_.append(temp%256); sendCommand_.append(temp/256); } +/** + * @brief 获取生成的Modbus命令 + * @return 完整的Modbus命令字节数组 + */ QByteArray MyModbus::SendCommand() { return sendCommand_; } +/** + * @brief 处理接收到的Modbus响应 + * @param revMessage 接收到的原始报文 + * @return 处理后的有效数据部分(移除地址/功能码/CRC) + * @note 执行站地址匹配和CRC校验 + */ QByteArray MyModbus::Receive(const QByteArray &revMessage) { receive_.clear(); - // 最小Modbus响应长度检查 + // 最小长度检查:站地址1 + 功能码1 + CRC2 if(revMessage.size() < 4) - { // 地址1 + 功能码1 + CRC2 - return receive_; + { + return receive_; // 返回空数组表示无效报文 } - // 检查站地址匹配 + // 站地址匹配检查 if(static_cast(revMessage[0]) != stationAddress_) { return receive_; } - // CRC校验 + // CRC校验检查 if (!CrcCheck(revMessage)) { return receive_; @@ -113,72 +173,148 @@ QByteArray MyModbus::Receive(const QByteArray &revMessage) return receive_; } +/** + * @brief 检查Modbus异常响应 + * @return 0-正常响应, >0-Modbus异常码, -1-响应报文错误 + */ int MyModbus::ErrorCheck() { - // 异常响应最小长度检查 + // 异常响应最小长度:地址1 + 异常功能码1 + 异常码1 + CRC2 if(receive_.size() < 5) { - // 地址1 + 异常功能码1 + 异常码1 + CRC2 - return -1; // 特殊错误码表示协议错误 + return -1; // 响应报文错误 } + + // 检查功能码高位(异常响应标志) if ((receive_.at(1) & 0x80) == 0x80) { - // MODBUS异常响应结构:地址 | 功能码+0x80 | 异常码 | CRC - quint8 exCode = receive_.at(2); - return exCode; - } - else - { - return 0; + return static_cast(receive_.at(2)); // 返回异常码 } + return 0; // 正常响应 } +/** + * @brief 解析读取线圈的响应数据 + * @return 线圈状态数组 + */ QVector MyModbus::AnalReadCoil() { QVector coil; + // 最小长度检查:地址1+功能码1+字节数1+数据1+CRC2 if(receive_.size() < 6) { - // 最小长度: 地址1 + 功能码1 + 长度1 + 数据1 + CRC2 return coil; } - quint8 byteCount = static_cast(receive_[2]); + quint8 byteCount = static_cast(receive_[2]); // 数据字节数 + // 解析每个数据字节 for (int byteIndex = 0; byteIndex < byteCount; byteIndex++) { quint8 byteValue = static_cast(receive_[3 + byteIndex]); - // 解析每个字节的8个位 + // 解析字节中的每个位(线圈状态) for (int bitIndex = 0; bitIndex < 8; bitIndex++) { int coilIndex = byteIndex * 8 + bitIndex; if (coilIndex < length_) { - bool state = byteValue & (1 << bitIndex); - coil.append(state); + // 只处理请求的长度 + coil.append(byteValue & (1 << bitIndex)); } } } return coil; } +/** + * @brief 解析读取寄存器的响应数据 + * @return 寄存器值数组 + */ QVector MyModbus::AnalReadReg() { QVector registers; + // 最小长度检查:地址1+功能码1+字节数1+数据2+CRC2 if(receive_.size() < 7) { - // 最小长度: 地址1 + 功能码1 + 长度1 + 数据2 + CRC2 return registers; } - int byteCount = receive_.at(2); - QByteArray data = receive_.mid(3, byteCount); + int byteCount = receive_.at(2); // 数据字节数 + QByteArray data = receive_.mid(3, byteCount); // 提取数据部分 + // 每两个字节组合为一个寄存器值(高位在前) for (int i = 0; i < data.size(); i += 2) { - quint16 value = (static_cast(data[i]) << 8) | static_cast(data[i+1]); - registers.append(value); + quint16 high = static_cast(data[i]); + quint16 low = static_cast(data[i+1]); + registers.append((high << 8) | low); } - return registers; } + +/** + * @brief 计算Modbus CRC16校验码 + * @param data 输入数据字节数组 + * @return CRC16校验值 + * @note 使用MODBUS标准多项式0xA001(0x8005的反转) + */ +quint16 MyModbus::CalculateCrc(const QByteArray &data) +{ + quint16 crc = 0xFFFF; // MODBUS初始值 + const quint16 polynomial = 0xA001; // MODBUS多项式(0x8005的反转) + + for (int i = 0; i < data.size(); ++i) + { + crc ^= static_cast(data.at(i)); // 异或当前字节 + + // 处理每个字节的8位 + for (int bit = 0; bit < 8; ++bit) + { + if (crc & 0x0001) + { + // 检查最低位 + crc = (crc >> 1) ^ polynomial; // 右移并且异或多项式 + } else + { + crc >>= 1; // 仅右移 + } + } + } + return crc; +} + +/** + * @brief 校验接收数据的CRC + * @param data 完整接收数据(包含末尾CRC) + * @return true-CRC校验通过, false-校验失败 + */ +bool MyModbus::CrcCheck(const QByteArray &data) +{ + //首先对接收报文的长度进行检验 + if (data.size() < 3) + { + return false; + } + //对接收的报文进行CRC校验 + QByteArray payload = data.left(data.length() - 2); + + //分离接收值的crc校验位 + quint8 receivedCrcLow = static_cast(data.at(data.length() - 2)); + quint8 receivedCrcHigh = static_cast(data.at(data.length() - 1)); + + //计算返回的报文的crc + quint16 crc = CalculateCrc(payload); + quint8 calcCrcLow = crc & 0xFF; + quint8 calcCrcHigh = (crc >> 8) & 0xFF; + + //比较计算的crc值和接收到的crc值是否一致 + if(calcCrcLow == receivedCrcLow && calcCrcHigh == receivedCrcHigh) + { + return true; + } + else + { + return false; + } +} diff --git a/mymodbus.h b/mymodbus.h index 2cf932f..5e38467 100644 --- a/mymodbus.h +++ b/mymodbus.h @@ -4,29 +4,124 @@ #include #include #include -#include "crc.h" +#include + +/****************************************************************************** +* Copyright (C) 2025-. +* +* File Name: mymodbus.h +* Description: Modbus RTU协议封装类,支持线圈和寄存器的读写操作 +* Others: +* Version: 1.0 +* Author: lipengpeng +* Date: 2025-7-23 +* +******************************************************************************/ + +/** + * @class MyModbus + * @brief Modbus RTU协议封装类 + * + * 提供Modbus RTU协议的核心功能实现,包括: + * 1. 读线圈/寄存器(功能码0x01/0x03) + * 2. 写多个线圈(功能码0x0F) + * 3. 写多个寄存器(功能码0x10) + * 4. CRC校验计算与验证 + * 5. 响应报文解析 + * + * @note 使用时需先通过Set()方法设置从站参数 + */ class MyModbus { private: - quint16 stationAddress_; - quint16 functionCode_; - quint16 startAdress_; - quint16 length_; - QByteArray sendCommand_; - QByteArray receive_; + quint16 stationAddress_; // 从站地址 + quint16 functionCode_; // 功能码 + quint16 startAdress_; // 起始地址 + quint16 length_; // 数据长度 + QByteArray sendCommand_; // 待发送的命令报文 + QByteArray receive_; // 接收到的响应报文 public: + /** + * @brief 默认构造函数 + */ MyModbus(); - void Set(quint16 stationAddress_,quint16 functionCode_,quint16 startAdress_,quint16 length_); + + /** + * @brief 设置Modbus通信参数 + * @param stationAddress 从站地址 + * @param functionCode 功能码 + * @param startAdress 起始地址 + * @param length 数据长度 + */ + void Set(quint16 stationAddress, quint16 functionCode, + quint16 startAdress, quint16 length); + + /** + * @brief 生成读线圈/寄存器命令 + * + * 根据当前参数生成读取命令(功能码01或03) + */ void ReadCoilAndReg(); + + /** + * @brief 生成写多个线圈命令 + * @param coils 要写入的线圈状态数组 + */ void WriteCoil(QVector &coils); + + /** + * @brief 生成写多个寄存器命令 + * @param values 要写入的寄存器值数组 + */ void WriteRegister(QVector &values); + + /** + * @brief 获取待发送的命令报文 + * @return 完整的Modbus命令报文 + */ QByteArray SendCommand(); + + /** + * @brief 处理接收到的响应报文 + * @param revMessage 接收到的原始报文 + * @return 通过CRC校验的有效报文 + */ QByteArray Receive(const QByteArray &revMessage); + + /** + * @brief 检查响应报文中的错误码 + * @return 0表示正常,非0表示异常码 + */ int ErrorCheck(); + + /** + * @brief 解析读线圈响应报文 + * @return 线圈状态数组 + */ QVector AnalReadCoil(); + + /** + * @brief 解析读寄存器响应报文 + * @return 寄存器值数组 + */ QVector AnalReadReg(); + + /** + * @brief 计算CRC校验值 + * @param data 待计算的数据 + * @return CRC校验值 + */ + quint16 CalculateCrc(const QByteArray &data); + + /** + * @brief 校验报文的CRC值 + * @param data 待校验的报文 + * @return true表示校验通过,false表示校验失败 + */ + bool CrcCheck(const QByteArray &data); + }; #endif // MYMODBUS_H diff --git a/widget.cpp b/widget.cpp index 659e758..d7c24bc 100644 --- a/widget.cpp +++ b/widget.cpp @@ -8,7 +8,6 @@ #include #include #include -#include "crc.h" #include "mymodbus.h" #include "communicationhistory.h" @@ -142,6 +141,11 @@ void Widget::on_pushWrite_clicked() quint16 stratAddress = ui->lineEdit_stratAddress->text().toInt(); quint16 length = ui->lineEdit_length->text().toInt(); + if (coils.size() != length) + { + QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配"); + return; + } modbus->Set(stationAddress,functionCode,stratAddress,length); modbus->WriteCoil(coils); @@ -184,6 +188,11 @@ void Widget::on_pushWrite_clicked() quint16 stratAddress = ui->lineEdit_stratAddress->text().toInt(); quint16 length = ui->lineEdit_length->text().toInt(); + if (values.size() != length) + { + QMessageBox::warning(this, "提示", "输入数据数与设置的长度不匹配"); + return; + } modbus->Set(stationAddress,functionCode,stratAddress,length); modbus->WriteRegister(values); //要发送的报文