commit 622168b3a8f6af9c1fc300dacb2806729c35d096 Author: 95384 <664090429@qq.com> Date: Wed Jul 23 15:05:20 2025 +0800 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fab7372 --- /dev/null +++ b/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/QT_Micro_Weather_Station.pro b/QT_Micro_Weather_Station.pro new file mode 100644 index 0000000..3a6a6ae --- /dev/null +++ b/QT_Micro_Weather_Station.pro @@ -0,0 +1,32 @@ +QT += core gui widgets serialport + +# 添加包含路径配置 +INCLUDEPATH += $$[QT_INSTALL_HEADERS] + +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 \ + widget.cpp + +HEADERS += \ + widget.h + +TRANSLATIONS += \ + QT_Micro_Weather_Station_zh_CN.ts + +# 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/QT_Micro_Weather_Station_zh_CN.ts b/QT_Micro_Weather_Station_zh_CN.ts new file mode 100644 index 0000000..9e50197 --- /dev/null +++ b/QT_Micro_Weather_Station_zh_CN.ts @@ -0,0 +1,3 @@ + + + diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..b0a4ec2 --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include "widget.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + Widget w; + w.show(); + return a.exec(); +} diff --git a/widget.cpp b/widget.cpp new file mode 100644 index 0000000..08dc68a --- /dev/null +++ b/widget.cpp @@ -0,0 +1,458 @@ +#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); // 开始后禁用输入框 + } +} diff --git a/widget.h b/widget.h new file mode 100644 index 0000000..0808ac1 --- /dev/null +++ b/widget.h @@ -0,0 +1,73 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class Widget; } +QT_END_NAMESPACE + +class Widget : public QWidget +{ + Q_OBJECT + +public: + Widget(QWidget *parent = nullptr); + ~Widget(); + +private slots: + void openPort(); + void closePort(); + void sendData(); + void autoSendData(); + void handleReadyRead(); + void clearStatus(); + +private: + // UI 控件 + QPushButton *openButton; + QPushButton *closeButton; + QPushButton *sendButton; + QPushButton *autoSendButton; + QLineEdit *autoIntervalEdit; + QComboBox *portComboBox; + QComboBox *baudRateComboBox; + QLineEdit *sendLineEdit; + QTextEdit *receiveTextEdit; + QLabel *statusLabel; + QLabel *minWindDirectionLabel; + QLabel *avgWindDirectionLabel; + QLabel *maxWindDirectionLabel; + QLabel *minWindSpeedLabel; + QLabel *avgWindSpeedLabel; + QLabel *maxWindSpeedLabel; + QLabel *temperatureLabel; + QLabel *humidityLabel; + QLabel *pressureLabel; + + // 串口和定时器 + QSerialPort *serialPort; + QTimer *autoSendTimer; + QTimer *statusTimer; + + // 状态标志和缓冲区 + bool isAutoSending; + QByteArray receiveBuffer; + + // 数据记录 + QString csvFileName = "weather_data.csv"; + int dataCount = 1; + + // 辅助函数 + quint16 calculateCRC(const QByteArray &data); + void saveToCSV(const QVector& values); +}; +#endif // WIDGET_H