Qt C++ 串口通信+数据可视化:工业设备数据实时采集与界面显示
一、技术背景与应用场景
工业现场中,PLC、传感器、智能仪表等设备常通过串口(RS232/RS485)输出实时运行数据(如温度、压力、转速、电压等)。Qt作为跨平台的C++应用开发框架,兼具串口通信API与强大的界面/绘图能力,是开发工业数据采集与可视化系统的理想选择。本文将完整实现一套工业设备数据实时采集系统,涵盖串口参数配置、数据解析、实时绘图、数据存储与异常报警等核心功能,满足工业场景下的高可靠性与实时性要求。
二、系统整体设计
2.1 核心功能模块
系统分为5个核心模块,各模块解耦设计,便于维护与扩展:
- 串口通信模块:负责串口参数配置、数据收发、异常处理(如断连重连);
- 数据解析模块:对串口接收的二进制/ASCII数据进行解析,提取有效工业参数;
- 可视化模块:基于Qt Charts实现实时曲线绘制、数值仪表盘、数据表格展示;
- 数据存储模块:将采集数据存入本地文件(CSV),支持历史数据回溯;
- 报警模块:对超阈值数据进行界面提示与声音报警。
2.2 技术选型
- 开发框架:Qt 6.5(兼容Qt 5.x),Qt Creator 12.0;
- 串口通信:Qt SerialPort模块(跨平台串口操作);
- 数据可视化:Qt Charts模块(QLineSeries、QValueAxis、QChartView);
- 数据解析:自定义协议解析(适配工业常用的Modbus RTU/自定义ASCII协议);
- 开发语言:C++17(兼容C++11/14);
- 编译环境:MSVC 2019(Windows)/GCC(Linux)。
三、开发环境搭建
3.1 环境配置
- 安装Qt时需勾选“SerialPort”和“Charts”模块(Qt 6中Charts属于Add-ons);
- 在项目.pro文件中添加模块依赖:
QT += core gui serialport charts widgets CONFIG += c++17 SOURCES += main.cpp \ mainwindow.cpp \ serialmanager.cpp \ dataparser.cpp \ datavisualizer.cpp HEADERS += mainwindow.h \ serialmanager.h \ dataparser.h \ datavisualizer.h 四、核心模块实现
4.1 串口通信模块(SerialManager)
串口模块是数据采集的基础,需实现串口枚举、参数配置、异步收发、异常处理等功能,采用单例模式设计,避免多实例冲突。
4.1.1 头文件(serialmanager.h)
#ifndefSERIALMANAGER_H#defineSERIALMANAGER_H#include<QObject>#include<QSerialPort>#include<QSerialPortInfo>#include<QTimer>#include<QMutex>classSerialManager:publicQObject{ Q_OBJECT public:// 单例获取static SerialManager*getInstance();// 串口参数结构体structSerialParams{ QString portName;// 串口名(如COM3、/dev/ttyUSB0) qint32 baudRate =9600;// 波特率 QSerialPort::DataBits dataBits = QSerialPort::Data8;// 数据位 QSerialPort::Parity parity = QSerialPort::NoParity;// 校验位 QSerialPort::StopBits stopBits = QSerialPort::OneStop;// 停止位 QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;// 流控};// 枚举可用串口 QStringList getAvailablePorts();// 打开/关闭串口boolopenSerial(const SerialParams& params);voidcloseSerial();// 发送数据boolsendData(const QByteArray& data); signals:// 接收数据信号voiddataReceived(const QByteArray& data);// 串口状态信号voidserialStateChanged(bool isOpen);// 错误信号voiderrorOccurred(const QString& error);private:explicitSerialManager(QObject *parent =nullptr);~SerialManager() override;// 禁用拷贝SerialManager(const SerialManager&)=delete; SerialManager&operator=(const SerialManager&)=delete;// 串口对象 QSerialPort* m_serialPort;// 重连定时器(断连后自动重连) QTimer* m_reconnectTimer;// 串口参数缓存 SerialParams m_lastParams;// 线程安全锁 QMutex m_mutex;// 重连间隔(ms)constint RECONNECT_INTERVAL =3000;private slots:// 读取串口数据voidreadSerialData();// 处理串口错误voidhandleSerialError(QSerialPort::SerialPortError error);// 自动重连voidtryReconnect();};#endif// SERIALMANAGER_H4.1.2 源文件(serialmanager.cpp)
#include"serialmanager.h"#include<QMutexLocker> SerialManager*SerialManager::getInstance(){static SerialManager instance;return&instance;}SerialManager::SerialManager(QObject *parent):QObject(parent),m_serialPort(newQSerialPort(this)),m_reconnectTimer(newQTimer(this)){// 连接串口信号connect(m_serialPort,&QSerialPort::readyRead,this,&SerialManager::readSerialData);connect(m_serialPort,&QSerialPort::errorOccurred,this,&SerialManager::handleSerialError);// 重连定时器配置 m_reconnectTimer->setInterval(RECONNECT_INTERVAL); m_reconnectTimer->setSingleShot(true);connect(m_reconnectTimer,&QTimer::timeout,this,&SerialManager::tryReconnect);}SerialManager::~SerialManager(){closeSerial();} QStringList SerialManager::getAvailablePorts(){ QMutexLocker locker(&m_mutex); QStringList ports;// 枚举系统可用串口for(const QSerialPortInfo& info :QSerialPortInfo::availablePorts()){ ports.append(info.portName());}return ports;}boolSerialManager::openSerial(const SerialParams& params){ QMutexLocker locker(&m_mutex);// 先关闭已打开的串口if(m_serialPort->isOpen()){ m_serialPort->close();}// 配置串口参数 m_serialPort->setPortName(params.portName); m_serialPort->setBaudRate(params.baudRate); m_serialPort->setDataBits(params.dataBits); m_serialPort->setParity(params.parity); m_serialPort->setStopBits(params.stopBits); m_serialPort->setFlowControl(params.flowControl);// 打开串口(读写模式)bool isOpen = m_serialPort->open(QIODevice::ReadWrite);if(isOpen){ m_lastParams = params; m_reconnectTimer->stop();// 关闭重连定时器 emit serialStateChanged(true);qInfo()<<"串口打开成功:"<< params.portName;}else{ QString error ="串口打开失败:"+ m_serialPort->errorString(); emit errorOccurred(error);qWarning()<< error;}return isOpen;}voidSerialManager::closeSerial(){ QMutexLocker locker(&m_mutex);if(m_serialPort->isOpen()){ m_serialPort->close(); m_reconnectTimer->stop(); emit serialStateChanged(false);qInfo()<<"串口已关闭";}}boolSerialManager::sendData(const QByteArray& data){ QMutexLocker locker(&m_mutex);if(!m_serialPort->isOpen()){ emit errorOccurred("串口未打开,发送失败");returnfalse;} qint64 bytesWritten = m_serialPort->write(data);if(bytesWritten ==-1){ QString error ="数据发送失败:"+ m_serialPort->errorString(); emit errorOccurred(error);returnfalse;}returntrue;}voidSerialManager::readSerialData(){ QMutexLocker locker(&m_mutex);if(!m_serialPort->isOpen())return; QByteArray data = m_serialPort->readAll();if(!data.isEmpty()){ emit dataReceived(data);qDebug()<<"接收数据:"<< data.toHex()<<"(原始:"<< data <<")";}}voidSerialManager::handleSerialError(QSerialPort::SerialPortError error){if(error == QSerialPort::NoError)return; QString errorMsg ="串口错误:"+ m_serialPort->errorString(); emit errorOccurred(errorMsg);qCritical()<< errorMsg;// 断连后启动重连if(error != QSerialPort::PermissionError && error != QSerialPort::NotFoundError){closeSerial(); m_reconnectTimer->start();}}voidSerialManager::tryReconnect(){qInfo()<<"尝试重新连接串口:"<< m_lastParams.portName;openSerial(m_lastParams);}4.2 数据解析模块(DataParser)
工业设备串口输出的数据格式多样,本文以“温度(℃)+压力(MPa)”的自定义ASCII协议为例(格式:T:25.5,P:1.23\r\n),实现通用解析框架,可扩展支持Modbus RTU等二进制协议。
4.2.1 头文件(dataparser.h)
#ifndefDATAPARSER_H#defineDATAPARSER_H#include<QObject>#include<QByteArray>#include<QVariantMap>classDataParser:publicQObject{ Q_OBJECT public:explicitDataParser(QObject *parent =nullptr);// 解析模式枚举enumParseMode{ AsciiMode,// ASCII协议 ModbusRTUMode // Modbus RTU协议};Q_ENUM(ParseMode)// 设置解析模式voidsetParseMode(ParseMode mode);// 解析原始数据 QVariantMap parseData(const QByteArray& rawData); signals:// 解析完成信号(key:参数名,value:数值)voiddataParsed(const QVariantMap& data);// 解析错误信号voidparseError(const QString& error);private:// ASCII协议解析 QVariantMap parseAsciiData(const QByteArray& data);// Modbus RTU协议解析(示例框架) QVariantMap parseModbusRTUData(const QByteArray& data);// 缓存未解析的残数据(如半包数据) QByteArray m_buffer;// 当前解析模式 ParseMode m_parseMode = AsciiMode;// 数据帧结束符const QByteArray FRAME_END ="\r\n";};#endif// DATAPARSER_H4.2.2 源文件(dataparser.cpp)
#include"dataparser.h"#include<QRegularExpression>#include<QDebug>DataParser::DataParser(QObject *parent):QObject(parent){}voidDataParser::setParseMode(ParseMode mode){ m_parseMode = mode; m_buffer.clear();// 切换模式清空缓存} QVariantMap DataParser::parseData(const QByteArray& rawData){// 拼接缓存数据(处理半包) m_buffer.append(rawData); QVariantMap result;// 按帧结束符分割数据int endIndex = m_buffer.indexOf(FRAME_END);if(endIndex ==-1){// 无完整帧,返回空return result;}// 提取完整帧并清空对应缓存 QByteArray frame = m_buffer.left(endIndex); m_buffer = m_buffer.mid(endIndex + FRAME_END.length());// 根据模式解析switch(m_parseMode){case AsciiMode: result =parseAsciiData(frame);break;case ModbusRTUMode: result =parseModbusRTUData(frame);break;default: emit parseError("未知解析模式");break;}if(!result.isEmpty()){ emit dataParsed(result);}return result;} QVariantMap DataParser::parseAsciiData(const QByteArray& data){ QVariantMap parsedData;// 正则表达式匹配 T:数值,P:数值 QRegularExpression regex(R"(T:([\d\.]+),P:([\d\.]+))"); QRegularExpressionMatch match = regex.match(data);if(match.hasMatch()){// 提取温度和压力数值double temp = match.captured(1).toDouble();double pressure = match.captured(2).toDouble(); parsedData.insert("Temperature", temp); parsedData.insert("Pressure", pressure);qDebug()<<"解析结果:温度="<< temp <<"℃,压力="<< pressure <<"MPa";}else{ QString error ="ASCII数据解析失败:"+QString(data); emit parseError(error);qWarning()<< error;}return parsedData;} QVariantMap DataParser::parseModbusRTUData(const QByteArray& data){ QVariantMap parsedData;// 此处实现Modbus RTU解析逻辑(示例)// Modbus RTU帧结构:地址(1B)+功能码(1B)+数据(NB)+CRC(2B)if(data.length()<4){// 最小帧长度 emit parseError("Modbus RTU帧长度不足");return parsedData;}// 示例:解析功能码03(读保持寄存器)返回的温度数据(寄存器地址0x0000) quint8 devAddr =static_cast<quint8>(data.at(0)); quint8 funcCode =static_cast<quint8>(data.at(1));if(devAddr ==0x01&& funcCode ==0x03){// 温度数据为2字节,高字节+低字节,缩放系数0.1 quint16 tempRaw =(static_cast<quint8>(data.at(3))<<8)|static_cast<quint8>(data.at(4));double temp = tempRaw *0.1; parsedData.insert("Temperature", temp);}return parsedData;}4.3 数据可视化模块(DataVisualizer)
基于Qt Charts实现实时曲线绘制,支持动态更新数据、坐标轴自适应、多曲线叠加,同时实现数值仪表盘和数据表格展示。
4.3.1 头文件(datavisualizer.h)
#ifndefDATAVISUALIZER_H#defineDATAVISUALIZER_H#include<QObject>#include<QChart>#include<QLineSeries>#include<QValueAxis>#include<QChartView>#include<QTimer>#include<QTableWidget>#include<QLabel>classDataVisualizer:publicQObject{ Q_OBJECT public:explicitDataVisualizer(QObject *parent =nullptr);// 初始化图表voidinitChart(QChartView* chartView);// 初始化数据表格voidinitTable(QTableWidget* table);// 初始化数值仪表盘voidinitDashboard(QLabel* tempLabel, QLabel* pressureLabel);// 设置数据缓存长度(曲线显示点数)voidsetDataCacheSize(int size);public slots:// 更新可视化数据voidupdateData(const QVariantMap& data);// 清空图表数据voidclearChart();private:// 图表组件 QChart* m_chart; QLineSeries* m_tempSeries;// 温度曲线 QLineSeries* m_pressureSeries;// 压力曲线 QValueAxis* m_xAxis;// X轴(时间/点数) QValueAxis* m_yAxis;// Y轴(数值)// 数据缓存 QList<double> m_tempData; QList<double> m_pressureData;int m_dataCacheSize =100;// 默认显示100个点int m_currentX =0;// 当前X轴坐标// UI组件 QTableWidget* m_dataTable =nullptr; QLabel* m_tempLabel =nullptr; QLabel* m_pressureLabel =nullptr;// 数据更新锁 QMutex m_mutex;// 自适应Y轴范围voidadjustYAxisRange();// 添加数据到表格voidaddDataToTable(double temp,double pressure);};#endif// DATAVISUALIZER_H4.3.2 源文件(datavisualizer.cpp)
#include"datavisualizer.h"#include<QDateTime>#include<QMutexLocker>#include<QVXYModelMapper>#include<QFont>DataVisualizer::DataVisualizer(QObject *parent):QObject(parent),m_chart(newQChart()),m_tempSeries(newQLineSeries()),m_pressureSeries(newQLineSeries()),m_xAxis(newQValueAxis()),m_yAxis(newQValueAxis()){// 初始化曲线样式 m_tempSeries->setName("温度 (℃)"); m_tempSeries->setColor(Qt::red); m_pressureSeries->setName("压力 (MPa)"); m_pressureSeries->setColor(Qt::blue);// 初始化坐标轴 m_xAxis->setTitleText("采样点"); m_xAxis->setRange(0, m_dataCacheSize); m_yAxis->setTitleText("数值"); m_yAxis->setRange(0,10);// 初始范围,后续自适应// 配置图表 m_chart->addSeries(m_tempSeries); m_chart->addSeries(m_pressureSeries); m_chart->setTitle("工业设备实时数据曲线"); m_chart->setAxisX(m_xAxis, m_tempSeries); m_chart->setAxisX(m_xAxis, m_pressureSeries); m_chart->setAxisY(m_yAxis, m_tempSeries); m_chart->setAxisY(m_yAxis, m_pressureSeries); m_chart->legend()->setVisible(true); m_chart->legend()->setAlignment(Qt::AlignBottom);}voidDataVisualizer::initChart(QChartView* chartView){ chartView->setChart(m_chart); chartView->setRenderHint(QPainter::Antialiasing);// 抗锯齿}voidDataVisualizer::initTable(QTableWidget* table){ m_dataTable = table;// 配置表格列 table->setColumnCount(3); table->setHorizontalHeaderLabels({"时间","温度(℃)","压力(MPa)"}); table->horizontalHeader()->setStretchLastSection(true);// 设置列宽 table->setColumnWidth(0,150); table->setColumnWidth(1,100); table->setColumnWidth(2,100);}voidDataVisualizer::initDashboard(QLabel* tempLabel, QLabel* pressureLabel){ m_tempLabel = tempLabel; m_pressureLabel = pressureLabel;// 设置仪表盘字体样式 QFont font = tempLabel->font(); font.setPointSize(16); font.setBold(true); tempLabel->setFont(font); pressureLabel->setFont(font);}voidDataVisualizer::setDataCacheSize(int size){ QMutexLocker locker(&m_mutex); m_dataCacheSize = size; m_xAxis->setRange(0, size);// 裁剪现有数据if(m_tempData.size()> size){ m_tempData = m_tempData.mid(m_tempData.size()- size); m_pressureData = m_pressureData.mid(m_pressureData.size()- size); m_currentX = size;}}voidDataVisualizer::updateData(const QVariantMap& data){ QMutexLocker locker(&m_mutex);if(!data.contains("Temperature")||!data.contains("Pressure")){return;}// 提取数值double temp = data["Temperature"].toDouble();double pressure = data["Pressure"].toDouble();// 更新仪表盘if(m_tempLabel){ m_tempLabel->setText(QString::asprintf("%.1f ℃", temp));// 超阈值(如50℃)标红 m_tempLabel->setStyleSheet(temp >50?"color: red;":"color: black;");}if(m_pressureLabel){ m_pressureLabel->setText(QString::asprintf("%.2f MPa", pressure)); m_pressureLabel->setStyleSheet(pressure >2.0?"color: red;":"color: black;");}// 添加数据到缓存 m_tempData.append(temp); m_pressureData.append(pressure);// 超出缓存长度时移除最早数据if(m_tempData.size()> m_dataCacheSize){ m_tempData.removeFirst(); m_pressureData.removeFirst();}// 更新曲线数据 m_tempSeries->clear(); m_pressureSeries->clear();for(int i =0; i < m_tempData.size();++i){ m_tempSeries->append(i, m_tempData[i]); m_pressureSeries->append(i, m_pressureData[i]);}// 自适应Y轴范围adjustYAxisRange();// 添加数据到表格addDataToTable(temp, pressure);}voidDataVisualizer::clearChart(){ QMutexLocker locker(&m_mutex); m_tempData.clear(); m_pressureData.clear(); m_tempSeries->clear(); m_pressureSeries->clear(); m_currentX =0;if(m_dataTable){ m_dataTable->setRowCount(0);}}voidDataVisualizer::adjustYAxisRange(){// 计算Y轴最大/最小值(留10%余量)double maxTemp = m_tempData.isEmpty()?0:*std::max_element(m_tempData.begin(), m_tempData.end());double maxPressure = m_pressureData.isEmpty()?0:*std::max_element(m_pressureData.begin(), m_pressureData.end());double maxY =qMax(maxTemp, maxPressure)*1.1;double minTemp = m_tempData.isEmpty()?0:*std::min_element(m_tempData.begin(), m_tempData.end());double minPressure = m_pressureData.isEmpty()?0:*std::min_element(m_pressureData.begin(), m_pressureData.end());double minY =qMin(minTemp, minPressure)*0.9; minY =qMax(minY,0.0);// 确保最小值不小于0 m_yAxis->setRange(minY, maxY);}voidDataVisualizer::addDataToTable(double temp,double pressure){if(!m_dataTable)return;// 插入行到表格首行int row =0; m_dataTable->insertRow(row);// 设置数据:时间、温度、压力 QString timeStr =QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"); m_dataTable->setItem(row,0,newQTableWidgetItem(timeStr)); m_dataTable->setItem(row,1,newQTableWidgetItem(QString::asprintf("%.1f", temp))); m_dataTable->setItem(row,2,newQTableWidgetItem(QString::asprintf("%.2f", pressure)));// 限制表格行数(最多1000行)if(m_dataTable->rowCount()>1000){ m_dataTable->removeRow(m_dataTable->rowCount()-1);}}4.4 主界面整合(MainWindow)
将上述模块整合到主界面,实现串口配置、数据采集、可视化展示的一体化操作。
4.4.1 头文件(mainwindow.h)
#ifndefMAINWINDOW_H#defineMAINWINDOW_H#include<QMainWindow>#include<QSerialPort>#include<QVariantMap>#include<QSoundEffect> QT_BEGIN_NAMESPACE namespace Ui {classMainWindow;} QT_END_NAMESPACE classSerialManager;classDataParser;classDataVisualizer;classMainWindow:publicQMainWindow{ Q_OBJECT public:MainWindow(QWidget *parent =nullptr);~MainWindow() override;private slots:// 串口配置相关槽函数voidon_btnRefreshPorts_clicked();voidon_btnOpenSerial_clicked();voidon_btnSendData_clicked();// 串口状态更新voidupdateSerialState(bool isOpen);// 数据接收与解析voidhandleRawData(const QByteArray& data);voidhandleParsedData(const QVariantMap& data);// 错误处理voidhandleSerialError(const QString& error);voidhandleParseError(const QString& error);// 报警处理voidcheckAlarm(const QVariantMap& data);// 数据存储voidsaveDataToCsv(const QVariantMap& data);private: Ui::MainWindow *ui;// 核心模块实例 SerialManager* m_serialManager; DataParser* m_dataParser; DataVisualizer* m_dataVisualizer;// 报警音效 QSoundEffect* m_alarmSound;// CSV文件句柄 QFile* m_csvFile;// 初始化UIvoidinitUI();// 初始化信号连接voidinitConnections();// 初始化CSV文件boolinitCsvFile();};#endif// MAINWINDOW_H4.4.2 源文件(mainwindow.cpp)
#include"mainwindow.h"#include"ui_mainwindow.h"#include"serialmanager.h"#include"dataparser.h"#include"datavisualizer.h"#include<QFileDialog>#include<QDateTime>#include<QTextStream>#include<QMessageBox>#include<QDir>MainWindow::MainWindow(QWidget *parent):QMainWindow(parent),ui(new Ui::MainWindow),m_serialManager(SerialManager::getInstance()),m_dataParser(newDataParser(this)),m_dataVisualizer(newDataVisualizer(this)),m_alarmSound(newQSoundEffect(this)),m_csvFile(nullptr){ ui->setupUi(this);initUI();initConnections();initCsvFile();// 初始化可视化组件 m_dataVisualizer->initChart(ui->chartView); m_dataVisualizer->initTable(ui->tableWidget); m_dataVisualizer->initDashboard(ui->lblTemp, ui->lblPressure); m_dataVisualizer->setDataCacheSize(200);// 曲线显示200个点// 加载报警音效 m_alarmSound->setSource(QUrl::fromLocalFile(":/sounds/alarm.wav")); m_alarmSound->setVolume(1.0);}MainWindow::~MainWindow(){// 关闭CSV文件if(m_csvFile && m_csvFile->isOpen()){ m_csvFile->close();delete m_csvFile;}delete ui;}voidMainWindow::initUI(){setWindowTitle("工业设备数据采集与可视化系统");// 初始化串口参数下拉框 ui->cbxBaudRate->addItems({"9600","19200","38400","115200"}); ui->cbxBaudRate->setCurrentText("9600"); ui->cbxDataBits->addItems({"8","7","6","5"}); ui->cbxDataBits->setCurrentText("8"); ui->cbxParity->addItems({"无","奇校验","偶校验"}); ui->cbxStopBits->addItems({"1","1.5","2"});// 刷新串口列表on_btnRefreshPorts_clicked();// 初始状态:关闭串口相关按钮 ui->btnOpenSerial->setText("打开串口"); ui->btnSendData->setEnabled(false);}voidMainWindow::initConnections(){// 串口模块信号连接connect(m_serialManager,&SerialManager::dataReceived,this,&MainWindow::handleRawData);connect(m_serialManager,&SerialManager::serialStateChanged,this,&MainWindow::updateSerialState);connect(m_serialManager,&SerialManager::errorOccurred,this,&MainWindow::handleSerialError);// 解析模块信号连接connect(m_dataParser,&DataParser::dataParsed,this,&MainWindow::handleParsedData);connect(m_dataParser,&DataParser::dataParsed,this,&MainWindow::checkAlarm);connect(m_dataParser,&DataParser::dataParsed,this,&MainWindow::saveDataToCsv);connect(m_dataParser,&DataParser::parseError,this,&MainWindow::handleParseError);}boolMainWindow::initCsvFile(){// 创建数据存储目录 QDir dataDir("data");if(!dataDir.exists()){ dataDir.mkdir(".");}// 生成CSV文件名(按时间戳) QString fileName =QString("data/采集数据_%1.csv").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss")); m_csvFile =newQFile(fileName);if(!m_csvFile->open(QIODevice::WriteOnly | QIODevice::Text)){QMessageBox::warning(this,"错误","CSV文件创建失败:"+ m_csvFile->errorString());returnfalse;}// 写入CSV表头 QTextStream stream(m_csvFile); stream <<"时间,温度(℃),压力(MPa)\n";returntrue;}voidMainWindow::on_btnRefreshPorts_clicked(){ ui->cbxPortName->clear(); QStringList ports = m_serialManager->getAvailablePorts(); ui->cbxPortName->addItems(ports);}voidMainWindow::on_btnOpenSerial_clicked(){if(ui->cbxPortName->currentText().isEmpty()){QMessageBox::warning(this,"警告","请选择串口!");return;}if(m_serialManager->openSerial({ ui->cbxPortName->currentText(), ui->cbxBaudRate->currentText().toInt(),static_cast<QSerialPort::DataBits>(ui->cbxDataBits->currentText().toInt()),(ui->cbxParity->currentIndex()==0)? QSerialPort::NoParity :(ui->cbxParity->currentIndex()==1? QSerialPort::OddParity : QSerialPort::EvenParity),(ui->cbxStopBits->currentIndex()==0)? QSerialPort::OneStop :(ui->cbxStopBits->currentIndex()==1? QSerialPort::OneAndHalfStop : QSerialPort::TwoStop), QSerialPort::NoFlowControl })){ ui->btnOpenSerial->setText("关闭串口"); ui->btnSendData->setEnabled(true); ui->cbxPortName->setEnabled(false); ui->cbxBaudRate->setEnabled(false); ui->cbxDataBits->setEnabled(false); ui->cbxParity->setEnabled(false); ui->cbxStopBits->setEnabled(false);}}voidMainWindow::on_btnSendData_clicked(){// 发送测试指令(如读取数据指令) QByteArray cmd ="READ_DATA\r\n"; m_serialManager->sendData(cmd);}voidMainWindow::updateSerialState(bool isOpen){if(!isOpen){ ui->btnOpenSerial->setText("打开串口"); ui->btnSendData->setEnabled(false); ui->cbxPortName->setEnabled(true); ui->cbxBaudRate->setEnabled(true); ui->cbxDataBits->setEnabled(true); ui->cbxParity->setEnabled(true); ui->cbxStopBits->setEnabled(true); ui->statusbar->showMessage("串口已断开",3000);}else{ ui->statusbar->showMessage("串口已连接:"+ ui->cbxPortName->currentText(),3000);}}voidMainWindow::handleRawData(const QByteArray& data){// 显示原始数据 ui->txtRawData->appendPlainText(QString("[%1] 原始数据:%2").arg(QDateTime::currentDateTime().toString("hh:mm:ss.zzz")).arg(data.toHex(' ')));// 解析数据 m_dataParser->parseData(data);}voidMainWindow::handleParsedData(const QVariantMap& data){// 更新可视化数据 m_dataVisualizer->updateData(data);}voidMainWindow::handleSerialError(const QString& error){ ui->statusbar->showMessage(error,5000);QMessageBox::critical(this,"串口错误", error);}voidMainWindow::handleParseError(const QString& error){ ui->statusbar->showMessage(error,5000); ui->txtRawData->appendPlainText("[解析错误] "+ error);}voidMainWindow::checkAlarm(const QVariantMap& data){double temp = data["Temperature"].toDouble();double pressure = data["Pressure"].toDouble();// 超阈值报警if(temp >50|| pressure >2.0){ ui->lblAlarm->setText("⚠ 数据超阈值!"); ui->lblAlarm->setStyleSheet("color: red; font-weight: bold;"); m_alarmSound->play();// 5秒后清除报警提示QTimer::singleShot(5000,[this](){ ui->lblAlarm->setText(""); ui->lblAlarm->setStyleSheet("");});}}voidMainWindow::saveDataToCsv(const QVariantMap& data){if(!m_csvFile ||!m_csvFile->isOpen())return;double temp = data["Temperature"].toDouble();double pressure = data["Pressure"].toDouble(); QString timeStr =QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz"); QTextStream stream(m_csvFile); stream << timeStr <<","<<QString::asprintf("%.1f", temp)<<","<<QString::asprintf("%.2f", pressure)<<"\n"; stream.flush();// 立即写入文件}4.5 主函数(main.cpp)
#include"mainwindow.h"#include<QApplication>#include<QStyleFactory>intmain(int argc,char*argv[]){ QApplication a(argc, argv);// 设置系统样式,提升界面美观度 a.setStyle(QStyleFactory::create("Fusion")); MainWindow w; w.resize(1200,800);// 设置主窗口大小 w.show();return a.exec();}五、界面设计(UI文件关键部分)
在Qt Designer中设计主界面,核心组件布局如下:
<!-- 关键组件示例 --><widgetclass="QWidget"name="centralwidget"><layoutclass="QVBoxLayout"name="verticalLayout"><!-- 串口配置区域 --><widgetclass="QGroupBox"name="groupBox"><propertyname="title"><string>串口配置</string></property><layoutclass="QGridLayout"name="gridLayout"><itemrow="0"column="0"><widgetclass="QLabel"name="label"><propertyname="text"><string>串口:</string></property></widget></item><itemrow="0"column="1"><widgetclass="QComboBox"name="cbxPortName"/></item><itemrow="0"column="2"><widgetclass="QPushButton"name="btnRefreshPorts"><propertyname="text"><string>刷新</string></property></widget></item><itemrow="0"column="3"><widgetclass="QPushButton"name="btnOpenSerial"><propertyname="text"><string>打开串口</string></property></widget></item><!-- 波特率、数据位等参数配置 --><itemrow="1"column="0"><widgetclass="QLabel"name="label_2"><propertyname="text"><string>波特率:</string></property></widget></item><itemrow="1"column="1"><widgetclass="QComboBox"name="cbxBaudRate"/></item><itemrow="1"column="2"><widgetclass="QLabel"name="label_3"><propertyname="text"><string>数据位:</string></property></widget></item><itemrow="1"column="3"><widgetclass="QComboBox"name="cbxDataBits"/></item><!-- 校验位、停止位 --><itemrow="2"column="0"><widgetclass="QLabel"name="label_4"><propertyname="text"><string>校验位:</string></property></widget></item><itemrow="2"column="1"><widgetclass="QComboBox"name="cbxParity"/></item><itemrow="2"column="2"><widgetclass="QLabel"name="label_5"><propertyname="text"><string>停止位:</string></property></widget></item><itemrow="2"column="3"><widgetclass="QComboBox"name="cbxStopBits"/></item><itemrow="3"column="0"colspan="4"><widgetclass="QPushButton"name="btnSendData"><propertyname="text"><string>发送读取指令</string></property></widget></item></layout></widget><!-- 实时数据仪表盘 --><widgetclass="QGroupBox"name="groupBox_2"><propertyname="title"><string>实时数据</string></property><layoutclass="QHBoxLayout"name="horizontalLayout"><item><widgetclass="QLabel"name="label_6"><propertyname="text"><string>温度:</string></property></widget></item><item><widgetclass="QLabel"name="lblTemp"><propertyname="text"><string>0.0 ℃</string></property></widget></item><item><widgetclass="QLabel"name="label_7"><propertyname="text"><string>压力:</string></property></widget></item><item><widgetclass="QLabel"name="lblPressure"><propertyname="text"><string>0.00 MPa</string></property></widget></item><item><widgetclass="QLabel"name="lblAlarm"><propertyname="text"><string/></property></widget></item></layout></widget><!-- 数据可视化区域 --><widgetclass="QSplitter"name="splitter"><propertyname="orientation"><enum>Qt::Horizontal</enum></property><widgetclass="QGroupBox"name="groupBox_3"><propertyname="title"><string>实时曲线</string></property><layoutclass="QVBoxLayout"name="verticalLayout_2"><item><widgetclass="QChartView"name="chartView"/></item></layout></widget><widgetclass="QGroupBox"name="groupBox_4"><propertyname="title"><string>数据列表</string></property><layoutclass="QVBoxLayout"name="verticalLayout_3"><item><widgetclass="QTableWidget"name="tableWidget"/></item></layout></widget></widget><!-- 原始数据显示 --><widgetclass="QGroupBox"name="groupBox_5"><propertyname="title"><string>原始数据</string></property><layoutclass="QVBoxLayout"name="verticalLayout_4"><item><widgetclass="QPlainTextEdit"name="txtRawData"/></item></layout></widget></layout></widget><widgetclass="QStatusBar"name="statusbar"/>六、功能测试与优化
6.1 测试步骤
- 串口模拟:使用串口调试工具(如SSCOM)模拟工业设备,发送格式为
T:25.5,P:1.23\r\n的ASCII数据; - 功能验证:
- 串口枚举、参数配置、打开/关闭功能正常;
- 数据接收后能正确解析,仪表盘实时更新;
- 曲线随数据动态绘制,坐标轴自适应;
- 超阈值(温度>50℃/压力>2.0MPa)触发声音报警;
- 数据自动写入CSV文件,表格显示历史数据;
- 断连后自动重连,重连成功后恢复数据采集。
6.2 性能优化
- 数据更新频率:限制曲线更新频率(如50ms/次),避免UI卡顿;
- 内存管理:表格数据限制最大行数(1000行),曲线缓存长度可配置;
- 线程优化:将串口数据接收与解析放到子线程,避免阻塞UI线程(可通过Qt的moveToThread实现);
- 绘图优化:使用QChart的OpenGL加速(setRenderHint(QPainter::Antialiasing)),提升曲线绘制效率。
七、扩展功能建议
- Modbus协议支持:完善Modbus RTU/TCP解析,适配工业标准协议;
- 历史数据回放:读取CSV文件,还原历史曲线;
- 多设备采集:支持多串口同时采集,多曲线对比显示;
- 网络传输:增加TCP/UDP模块,将数据上传至服务器;
- 自定义报警:支持用户配置阈值、报警方式(声音/邮件/短信);
- 导出报表:支持将历史数据导出为Excel/PDF报表。
八、总结
本文基于Qt C++实现了一套完整的工业设备串口数据采集与可视化系统,涵盖串口通信、数据解析、实时绘图、数据存储、异常报警等核心功能。系统采用模块化设计,代码结构清晰,可扩展性强,适配多种工业串口协议,满足工业现场的高可靠性与实时性要求。通过Qt Charts实现的可视化界面直观展示设备运行数据,帮助运维人员快速掌握设备状态,降低故障排查成本。
该系统可广泛应用于智能制造、工业监控、设备运维等场景,通过简单的协议适配即可对接不同类型的工业设备,具备较高的工程实用价值。