#include "widget.h" #include #include #include // 添加网格布局头文件 #include #include #include #include #include #include // 添加日期时间头文件 #include #include #include #include #include #include #include #include #include // 根据 Qt 版本选择编码处理方式 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif Widget::Widget(QWidget *parent) : QWidget(parent) { // 初始化串口对象 serialPort = new QSerialPort(this); // 创建UI控件 openButton = new QPushButton(tr("打开串口"), this); closeButton = new QPushButton(tr("关闭串口"), this); sendButton = new QPushButton(tr("发送"), this); autoSendButton = new QPushButton(tr("自动查询数据"), this); autoIntervalEdit = new QLineEdit("5", this); // 初始化时间间隔输入框 autoIntervalEdit->setFixedWidth(50); // 设置固定宽度 autoIntervalEdit->setValidator(new QIntValidator(1, 3600, this)); // 1秒-1小时 autoSendTimer = new QTimer(this); isAutoSending = false; // 初始化状态标签和定时器(简洁样式) statusLabel = new QLabel(this); statusLabel->setAlignment(Qt::AlignCenter); statusLabel->setMinimumHeight(24); statusLabel->setStyleSheet("font-weight: bold; font-size: 12pt;"); statusLabel->setMargin(5); statusTimer = new QTimer(this); statusTimer->setSingleShot(true); connect(statusTimer, &QTimer::timeout, this, &Widget::clearStatus); // 连接定时器信号 connect(autoSendTimer, &QTimer::timeout, this, [this]() { if (serialPort->isOpen()) { // 发送固定数据: 30 03 00 00 00 09 81ED QByteArray autoData = QByteArray::fromHex("30030000000981ED"); serialPort->write(autoData); } }); portComboBox = new QComboBox(this); baudRateComboBox = new QComboBox(this); sendLineEdit = new QLineEdit(this); // 设置输入验证器:只允许16进制字符和空格 QRegExp hexRegExp("^[0-9A-Fa-f\\s]*$"); QValidator *validator = new QRegExpValidator(hexRegExp, this); sendLineEdit->setValidator(validator); receiveTextEdit = new QTextEdit(this); receiveTextEdit->setReadOnly(true); // 设置下拉框选项 QStringList baudRates = {"9600", "19200", "38400", "57600", "115200"}; baudRateComboBox->addItems(baudRates); baudRateComboBox->setCurrentText("9600"); // 默认波特率改为9600 // 获取可用串口并显示描述信息 QList ports = QSerialPortInfo::availablePorts(); for (const QSerialPortInfo &port : ports) { QString portInfo = port.portName() + " - " + port.description(); portComboBox->addItem(portInfo, port.portName()); } // 初始按钮状态 closeButton->setEnabled(false); autoSendButton->setEnabled(false); // 初始禁用自动发送按钮 // 连接信号槽 connect(openButton, &QPushButton::clicked, this, &Widget::openPort); connect(closeButton, &QPushButton::clicked, this, &Widget::closePort); connect(sendButton, &QPushButton::clicked, this, &Widget::sendData); connect(autoSendButton, &QPushButton::clicked, this, &Widget::autoSendData); connect(serialPort, &QSerialPort::readyRead, this, &Widget::handleReadyRead); // 创建布局 QVBoxLayout *mainLayout = new QVBoxLayout(this); QHBoxLayout *portLayout = new QHBoxLayout(); portLayout->addWidget(new QLabel("串口:", this)); portLayout->addWidget(portComboBox); portLayout->addWidget(new QLabel("波特率:", this)); portLayout->addWidget(baudRateComboBox); QHBoxLayout *buttonLayout = new QHBoxLayout(); buttonLayout->addWidget(openButton); buttonLayout->addWidget(closeButton); // 添加自动查询控制区域(右侧)- 优化布局 QVBoxLayout *autoLayout = new QVBoxLayout(); // 时间间隔控制行 QHBoxLayout *intervalLayout = new QHBoxLayout(); intervalLayout->addWidget(new QLabel("间隔(秒):", this)); intervalLayout->addWidget(autoIntervalEdit); autoLayout->addLayout(intervalLayout); autoLayout->addWidget(autoSendButton); // 初始状态:禁用输入框 autoIntervalEdit->setEnabled(false); buttonLayout->addLayout(autoLayout); mainLayout->addLayout(portLayout); mainLayout->addLayout(buttonLayout); // 添加接收框(调整到上方) mainLayout->addWidget(new QLabel("接收数据:", this)); mainLayout->addWidget(receiveTextEdit); // 添加气象数据显示区域(调整到下方) QGridLayout *dataLayout = new QGridLayout(); minWindDirectionLabel = new QLabel("最小风向: -- °/s", this); avgWindDirectionLabel = new QLabel("平均风向: -- °/s", this); maxWindDirectionLabel = new QLabel("最大风向: -- °/s", this); minWindSpeedLabel = new QLabel("最小风速: -- m/s", this); avgWindSpeedLabel = new QLabel("平均风速: -- m/s", this); maxWindSpeedLabel = new QLabel("最大风速: -- m/s", this); temperatureLabel = new QLabel("大气温度: -- °", this); humidityLabel = new QLabel("大气湿度: -- %", this); pressureLabel = new QLabel("大气压强: -- hPa", this); dataLayout->addWidget(minWindDirectionLabel, 0, 0); dataLayout->addWidget(avgWindDirectionLabel, 0, 1); dataLayout->addWidget(maxWindDirectionLabel, 0, 2); dataLayout->addWidget(minWindSpeedLabel, 1, 0); dataLayout->addWidget(avgWindSpeedLabel, 1, 1); dataLayout->addWidget(maxWindSpeedLabel, 1, 2); dataLayout->addWidget(temperatureLabel, 2, 0); dataLayout->addWidget(humidityLabel, 2, 1); dataLayout->addWidget(pressureLabel, 2, 2); mainLayout->addLayout(dataLayout); // 添加发送框和按钮 QHBoxLayout *sendLayout = new QHBoxLayout(); sendLayout->addWidget(new QLabel("发送数据(16进制):", this)); sendLayout->addWidget(sendLineEdit); sendLayout->addWidget(sendButton); mainLayout->addLayout(sendLayout); // 添加状态标签 mainLayout->addWidget(statusLabel); setLayout(mainLayout); } Widget::~Widget() { if (serialPort->isOpen()) { serialPort->close(); } delete serialPort; } // CRC16-MODBUS计算函数 quint16 Widget::calculateCRC(const QByteArray &data) { quint16 crc = 0xFFFF; // 初始值 for (int i = 0; i < data.size(); ++i) { crc ^= static_cast(data.at(i)); // 异或当前字节 for (int j = 0; j < 8; ++j) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; // 多项式 0x8005 反转后为 0xA001 } else { crc = crc >> 1; } } } // 交换高8位和低8位(转换为大端序) return (crc << 8) | (crc >> 8); } void Widget::openPort() { // 获取实际端口名(去除描述部分) QString portName = portComboBox->currentData().toString(); serialPort->setPortName(portName); // 更新按钮状态 openButton->setEnabled(false); closeButton->setEnabled(true); if (serialPort->open(QIODevice::ReadWrite)) { statusLabel->setText("串口已打开"); autoSendButton->setEnabled(true); // 启用自动发送按钮 autoIntervalEdit->setEnabled(true); // 启用时间输入框 } else { statusLabel->setText("无法打开串口"); // 恢复按钮状态 openButton->setEnabled(true); closeButton->setEnabled(false); autoSendButton->setEnabled(false); // 确保禁用自动发送按钮 } statusTimer->start(800); // 0.8秒后清除状态消息(进一步缩短显示时间) } void Widget::closePort() { if (serialPort->isOpen()) { serialPort->close(); statusLabel->setText("串口已关闭"); } // 更新按钮状态 openButton->setEnabled(true); closeButton->setEnabled(false); autoSendButton->setEnabled(false); // 禁用自动发送按钮 // 如果正在自动发送,停止定时器并重置状态 if (isAutoSending) { autoSendTimer->stop(); isAutoSending = false; autoSendButton->setText("自动查询数据"); // 注意:这里不设置输入框状态,由调用方处理 } autoIntervalEdit->setEnabled(false); // 关闭串口时禁用输入框 statusTimer->start(800); // 0.8秒后清除状态消息(进一步缩短显示时间) } void Widget::clearStatus() { statusLabel->clear(); } void Widget::sendData() { if (!serialPort->isOpen()) { QMessageBox::warning(this, "警告", "请先打开串口"); return; } QString text = sendLineEdit->text().trimmed().remove(' '); if (text.isEmpty()) return; // 处理奇数长度输入:在最后一位前补零 if (text.length() % 2 != 0) { text = text.left(text.length() - 1) + "0" + text.right(1); } // 将16进制字符串转换为字节数组 QByteArray data; for (int i = 0; i < text.length(); i += 2) { QString hex = text.mid(i, 2); bool ok; char byte = static_cast(hex.toInt(&ok, 16)); if (ok) { data.append(byte); } else { QMessageBox::warning(this, "错误", "无效的16进制数据: " + hex); return; } } // 发送数据 serialPort->write(data); serialPort->flush(); // 确保数据立即发送 } // 保存数据到CSV文件(使用UTF-8编码) void Widget::saveToCSV(const QVector& values) { QFile file(csvFileName); if (!file.exists()) { // 创建文件并写入带BOM的UTF-8表头 if (file.open(QIODevice::WriteOnly)) { // 直接写入UTF-8 BOM头 file.write("\xEF\xBB\xBF"); // 写入UTF-8编码的表头(转换为QString后调用toUtf8) file.write(QString("序号,时间,最小风向,平均风向,最大风向,最小风速,平均风速,最大风速,温度,湿度,压强\n").toUtf8()); file.close(); } } // 追加数据(使用UTF-8编码) if (file.open(QIODevice::Append)) { // 构建数据行(使用用户要求的时间格式:YYYY/MM/DD-HH/MM/SS) QString line = QString("%1,%2,").arg(dataCount++).arg(QDateTime::currentDateTime().toString("yyyy/MM/dd-HH/mm/ss")); // 添加9个数据值 for (int i = 0; i < values.size(); i++) { line += QString::number(values[i]); if (i < values.size() - 1) line += ","; } line += "\n"; // 直接写入UTF-8编码的数据 file.write(line.toUtf8()); file.close(); } } void Widget::handleReadyRead() { if (!serialPort->isOpen()) return; // 读取数据并追加到缓冲区 receiveBuffer += serialPort->readAll(); // 循环处理缓冲区中的数据帧 while (receiveBuffer.size() >= 5) { // 最小帧长度:帧头(2) + 长度(1) + 数据(0) + CRC(2) = 5字节 // 查找帧头 (0x30, 0x03) int startIndex = receiveBuffer.indexOf("\x30\x03"); if (startIndex == -1) { // 没有找到有效帧头,清空缓冲区 receiveBuffer.clear(); break; } // 移除帧头前的无效数据 if (startIndex > 0) { receiveBuffer = receiveBuffer.mid(startIndex); } // 检查是否包含完整帧 if (receiveBuffer.size() < 3) break; // 帧头(2) + 长度(1) quint8 length = static_cast(receiveBuffer.at(2)); // 数据长度 int frameSize = 3 + length + 2; // 完整帧大小 if (receiveBuffer.size() < frameSize) { // 数据不完整,等待更多数据 break; } // 提取完整帧 QByteArray frame = receiveBuffer.left(frameSize); receiveBuffer = receiveBuffer.mid(frameSize); // 移除已处理帧 // 获取当前时间 QString timestamp = QTime::currentTime().toString("[HH:mm:ss] "); // 转换为16进制字符串 QString hexData; for (char byte : frame) { hexData += QString("%1 ").arg(static_cast(byte), 2, 16, QLatin1Char('0')).toUpper(); } // 解析数据帧并校验 QString statusText = timestamp + hexData.trimmed(); QString statusColor = "black"; // 提取数据部分(包括帧头和长度) QByteArray frameData = frame.left(3 + length); // 计算CRC quint16 calculatedCRC = calculateCRC(frameData); // 提取接收到的CRC(设备发送顺序:高字节在前,低字节在后) quint8 firstByte = static_cast(frame.at(3 + length)); // 先接收的字节(高字节) quint8 secondByte = static_cast(frame.at(3 + length + 1)); // 后接收的字节(低字节) quint16 receivedCRC = (firstByte << 8) | secondByte; // 组合为16位CRC值 // 比较CRC if (calculatedCRC == receivedCRC) { statusColor = "green"; statusText += " ✓ 校验通过"; // 解析寄存器数据并更新显示标签 QVector dataValues; for (int i = 3; i < 3 + length; i += 2) { if (i + 1 < 3 + length) { int regIndex = (i - 3) / 2; quint16 value = static_cast(frame.at(i)) << 8 | static_cast(frame.at(i + 1)); dataValues.append(value); // 保存数据值 // 更新对应的标签 switch (regIndex) { case 0: minWindDirectionLabel->setText(QString("最小风向: %1 °/s").arg(value)); break; case 1: avgWindDirectionLabel->setText(QString("平均风向: %1 °/s").arg(value)); break; case 2: maxWindDirectionLabel->setText(QString("最大风向: %1 °/s").arg(value)); break; case 3: minWindSpeedLabel->setText(QString("最小风速: %1 m/s").arg(value)); break; case 4: avgWindSpeedLabel->setText(QString("平均风速: %1 m/s").arg(value)); break; case 5: maxWindSpeedLabel->setText(QString("最大风速: %1 m/s").arg(value)); break; case 6: temperatureLabel->setText(QString("大气温度: %1 °").arg(value)); break; case 7: humidityLabel->setText(QString("大气湿度: %1 %").arg(value)); break; case 8: pressureLabel->setText(QString("大气压强: %1 hPa").arg(value)); break; } } } // 保存数据到CSV saveToCSV(dataValues); } else { statusColor = "red"; statusText += QString(" ✗ 校验不通过 (计算值: %1, 接收值: %2)") .arg(calculatedCRC, 4, 16, QChar('0').toUpper()) .arg(receivedCRC, 4, 16, QChar('0').toUpper()); } // 添加带样式的文本 receiveTextEdit->append("" + statusText + ""); } } void Widget::autoSendData() { if (!serialPort->isOpen()) { QMessageBox::warning(this, "警告", "请先打开串口"); return; } if (isAutoSending) { // 停止自动发送 autoSendTimer->stop(); autoSendButton->setText("自动查询数据"); isAutoSending = false; autoIntervalEdit->setEnabled(true); // 停止后启用输入框 } else { // 立即发送一次查询指令 if (serialPort->isOpen()) { QByteArray autoData = QByteArray::fromHex("30030000000981ED"); serialPort->write(autoData); } // 获取时间间隔(秒),默认为5秒 int interval = 5; bool ok; int inputInterval = autoIntervalEdit->text().toInt(&ok); if (ok && inputInterval > 0) { interval = inputInterval * 1000; // 转换为毫秒 } else { autoIntervalEdit->setText("5"); // 重置为默认值 interval = 5000; } // 开始自动发送 autoSendTimer->start(interval); autoSendButton->setText("停止自动查询"); isAutoSending = true; autoIntervalEdit->setEnabled(false); // 开始后禁用输入框 } }