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