QT-Micro_Weather_Station/widget.cpp

459 lines
17 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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); // 开始后禁用输入框
}
}