First commit

This commit is contained in:
95384 2025-07-23 15:05:20 +08:00
commit 622168b3a8
6 changed files with 650 additions and 0 deletions

73
.gitignore vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="QT_Micro_Weather_Station_zh_CN"></TS>

11
main.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}

458
widget.cpp Normal file
View File

@ -0,0 +1,458 @@
#include "widget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout> // 添加网格布局头文件
#include <QLabel>
#include <QSerialPortInfo>
#include <QMessageBox>
#include <QByteArray>
#include <QTime>
#include <QDateTime> // 添加日期时间头文件
#include <QTimer>
#include <QSerialPort>
#include <QPushButton>
#include <QRegExpValidator>
#include <QDebug>
#include <QRegularExpression>
#include <QFile>
#include <QTextStream>
#include <QDir>
// 根据 Qt 版本选择编码处理方式
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QTextCodec>
#else
#include <QStringConverter>
#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<QSerialPortInfo> 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<quint8>(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<char>(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<quint16>& 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<quint8>(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<unsigned char>(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<quint8>(frame.at(3 + length)); // 先接收的字节(高字节)
quint8 secondByte = static_cast<quint8>(frame.at(3 + length + 1)); // 后接收的字节(低字节)
quint16 receivedCRC = (firstByte << 8) | secondByte; // 组合为16位CRC值
// 比较CRC
if (calculatedCRC == receivedCRC) {
statusColor = "green";
statusText += " <span style='color:green;'>✓ 校验通过</span>";
// 解析寄存器数据并更新显示标签
QVector<quint16> dataValues;
for (int i = 3; i < 3 + length; i += 2) {
if (i + 1 < 3 + length) {
int regIndex = (i - 3) / 2;
quint16 value = static_cast<quint8>(frame.at(i)) << 8 |
static_cast<quint8>(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(" <span style='color:red;'>✗ 校验不通过 (计算值: %1, 接收值: %2)</span>")
.arg(calculatedCRC, 4, 16, QChar('0').toUpper())
.arg(receivedCRC, 4, 16, QChar('0').toUpper());
}
// 添加带样式的文本
receiveTextEdit->append("<span style='color:" + statusColor + ";'>" + statusText + "</span>");
}
}
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); // 开始后禁用输入框
}
}

73
widget.h Normal file
View File

@ -0,0 +1,73 @@
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QSerialPort>
#include <QTimer>
#include <QVector>
#include <QPushButton>
#include <QLineEdit>
#include <QComboBox>
#include <QTextEdit>
#include <QLabel>
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<quint16>& values);
};
#endif // WIDGET_H