跳到主要内容 Qt C++ 串口通信与数据可视化:工业设备实时采集系统 | 极客日志
C++
Qt C++ 串口通信与数据可视化:工业设备实时采集系统 基于 Qt C++ 构建工业设备数据采集与可视化系统,集成串口通信、数据解析、实时绘图及异常报警功能。利用 Qt SerialPort 和 Charts 模块实现跨平台数据交互,支持 Modbus RTU 与自定义 ASCII 协议解析。系统采用模块化架构,包含断连重连、CSV 数据存储及阈值声音报警机制,适用于智能制造监控场景。
CoderByte 发布于 2026/3/15 更新于 2026/4/18 1 浏览一、技术背景与应用场景
工业现场中,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) #ifndef SERIALMANAGER_H
#define SERIALMANAGER_H
#include <QObject>
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QTimer>
#include <QMutex>
class SerialManager : public QObject {
Q_OBJECT
public :
static SerialManager* getInstance () ;
struct SerialParams {
QString portName;
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 () ;
bool openSerial (const SerialParams& params) ;
void closeSerial () ;
bool sendData (const QByteArray& data) ;
signals:
void dataReceived (const QByteArray& data) ;
void serialStateChanged (bool isOpen) ;
void errorOccurred (const QString& error) ;
private :
explicit SerialManager (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;
const int RECONNECT_INTERVAL = 3000 ;
private slots:
void readSerialData () ;
void handleSerialError (QSerialPort::SerialPortError error) ;
void tryReconnect () ;
};
#endif
4.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 (new QSerialPort (this )), m_reconnectTimer (new QTimer (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;
}
bool SerialManager::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;
}
void SerialManager::closeSerial () {
QMutexLocker locker (&m_mutex) ;
if (m_serialPort->isOpen ()) {
m_serialPort->close ();
m_reconnectTimer->stop ();
emit serialStateChanged (false ) ;
qInfo () << "串口已关闭" ;
}
}
bool SerialManager::sendData (const QByteArray& data) {
QMutexLocker locker (&m_mutex) ;
if (!m_serialPort->isOpen ()) {
emit errorOccurred ("串口未打开,发送失败" ) ;
return false ;
}
qint64 bytesWritten = m_serialPort->write (data);
if (bytesWritten == -1 ) {
QString error = "数据发送失败:" + m_serialPort->errorString ();
emit errorOccurred (error) ;
return false ;
}
return true ;
}
void SerialManager::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 << ")" ;
}
}
void SerialManager::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 ();
}
}
void SerialManager::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) #ifndef DATAPARSER_H
#define DATAPARSER_H
#include <QObject>
#include <QByteArray>
#include <QVariantMap>
class DataParser : public QObject {
Q_OBJECT
public :
explicit DataParser (QObject *parent = nullptr ) ;
enum ParseMode {
AsciiMode,
ModbusRTUMode
};
Q_ENUM (ParseMode)
void setParseMode (ParseMode mode) ;
QVariantMap parseData (const QByteArray& rawData) ;
signals:
void dataParsed (const QVariantMap& data) ;
void parseError (const QString& error) ;
private :
QVariantMap parseAsciiData (const QByteArray& data) ;
QVariantMap parseModbusRTUData (const QByteArray& data) ;
QByteArray m_buffer;
ParseMode m_parseMode = AsciiMode;
const QByteArray FRAME_END = "\r\n" ;
};
#endif
4.2.2 源文件(dataparser.cpp) #include "dataparser.h"
#include <QRegularExpression>
#include <QDebug>
DataParser::DataParser (QObject *parent) : QObject (parent) {}
void DataParser::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;
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;
if (data.length () < 4 ) {
emit parseError ("Modbus RTU 帧长度不足" ) ;
return parsedData;
}
quint8 devAddr = static_cast <quint8>(data.at (0 ));
quint8 funcCode = static_cast <quint8>(data.at (1 ));
if (devAddr == 0x01 && funcCode == 0x03 ) {
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) #ifndef DATAVISUALIZER_H
#define DATAVISUALIZER_H
#include <QObject>
#include <QChart>
#include <QLineSeries>
#include <QValueAxis>
#include <QChartView>
#include <QTimer>
#include <QTableWidget>
#include <QLabel>
class DataVisualizer : public QObject {
Q_OBJECT
public :
explicit DataVisualizer (QObject *parent = nullptr ) ;
void initChart (QChartView* chartView) ;
void initTable (QTableWidget* table) ;
void initDashboard (QLabel* tempLabel, QLabel* pressureLabel) ;
void setDataCacheSize (int size) ;
public slots:
void updateData (const QVariantMap& data) ;
void clearChart () ;
private :
QChart* m_chart;
QLineSeries* m_tempSeries;
QLineSeries* m_pressureSeries;
QValueAxis* m_xAxis;
QValueAxis* m_yAxis;
QList<double > m_tempData;
QList<double > m_pressureData;
int m_dataCacheSize = 100 ;
int m_currentX = 0 ;
QTableWidget* m_dataTable = nullptr ;
QLabel* m_tempLabel = nullptr ;
QLabel* m_pressureLabel = nullptr ;
QMutex m_mutex;
void adjustYAxisRange () ;
void addDataToTable (double temp, double pressure) ;
};
#endif
4.3.2 源文件(datavisualizer.cpp) #include "datavisualizer.h"
#include <QDateTime>
#include <QMutexLocker>
#include <QVXYModelMapper>
#include <QFont>
DataVisualizer::DataVisualizer (QObject *parent)
: QObject (parent), m_chart (new QChart ()), m_tempSeries (new QLineSeries ()),
m_pressureSeries (new QLineSeries ()), m_xAxis (new QValueAxis ()), m_yAxis (new QValueAxis ()) {
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);
}
void DataVisualizer::initChart (QChartView* chartView) {
chartView->setChart (m_chart);
chartView->setRenderHint (QPainter::Antialiasing);
}
void DataVisualizer::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 );
}
void DataVisualizer::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);
}
void DataVisualizer::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;
}
}
void DataVisualizer::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));
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]);
}
adjustYAxisRange ();
addDataToTable (temp, pressure);
}
void DataVisualizer::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 );
}
}
void DataVisualizer::adjustYAxisRange () {
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 );
m_yAxis->setRange (minY, maxY);
}
void DataVisualizer::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 , new QTableWidgetItem (timeStr));
m_dataTable->setItem (row, 1 , new QTableWidgetItem (QString::asprintf ("%.1f" , temp)));
m_dataTable->setItem (row, 2 , new QTableWidgetItem (QString::asprintf ("%.2f" , pressure)));
if (m_dataTable->rowCount () > 1000 ) {
m_dataTable->removeRow (m_dataTable->rowCount () - 1 );
}
}
4.4 主界面整合(MainWindow) 将上述模块整合到主界面,实现串口配置、数据采集、可视化展示的一体化操作。
4.4.1 头文件(mainwindow.h) #ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QVariantMap>
#include <QSoundEffect>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow ; }
QT_END_NAMESPACE
class SerialManager ;
class DataParser ;
class DataVisualizer ;
class MainWindow : public QMainWindow {
Q_OBJECT
public :
MainWindow (QWidget *parent = nullptr );
~MainWindow () override ;
private slots:
void on_btnRefreshPorts_clicked () ;
void on_btnOpenSerial_clicked () ;
void on_btnSendData_clicked () ;
void updateSerialState (bool isOpen) ;
void handleRawData (const QByteArray& data) ;
void handleParsedData (const QVariantMap& data) ;
void handleSerialError (const QString& error) ;
void handleParseError (const QString& error) ;
void checkAlarm (const QVariantMap& data) ;
void saveDataToCsv (const QVariantMap& data) ;
private :
Ui::MainWindow *ui;
SerialManager* m_serialManager;
DataParser* m_dataParser;
DataVisualizer* m_dataVisualizer;
QSoundEffect* m_alarmSound;
QFile* m_csvFile;
void initUI () ;
void initConnections () ;
bool initCsvFile () ;
};
#endif
4.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 (new DataParser (this )),
m_dataVisualizer (new DataVisualizer (this )), m_alarmSound (new QSoundEffect (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 );
m_alarmSound->setSource (QUrl::fromLocalFile (":/sounds/alarm.wav" ));
m_alarmSound->setVolume (1.0 );
}
MainWindow::~MainWindow () {
if (m_csvFile && m_csvFile->isOpen ()) {
m_csvFile->close ();
delete m_csvFile;
}
delete ui;
}
void MainWindow::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 );
}
void MainWindow::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);
}
bool MainWindow::initCsvFile () {
QDir dataDir ("data" ) ;
if (!dataDir.exists ()) {
dataDir.mkdir ("." );
}
QString fileName = QString ("data/采集数据_%1.csv" ).arg (QDateTime::currentDateTime ().toString ("yyyyMMdd_hhmmss" ));
m_csvFile = new QFile (fileName);
if (!m_csvFile->open (QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning (this , "错误" , "CSV 文件创建失败:" + m_csvFile->errorString ());
return false ;
}
QTextStream stream (m_csvFile) ;
stream << "时间,温度 (℃),压力 (MPa)\n" ;
return true ;
}
void MainWindow::on_btnRefreshPorts_clicked () {
ui->cbxPortName->clear ();
QStringList ports = m_serialManager->getAvailablePorts ();
ui->cbxPortName->addItems (ports);
}
void MainWindow::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 );
}
}
void MainWindow::on_btnSendData_clicked () {
QByteArray cmd = "READ_DATA\r\n" ;
m_serialManager->sendData (cmd);
}
void MainWindow::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 );
}
}
void MainWindow::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);
}
void MainWindow::handleParsedData (const QVariantMap& data) {
m_dataVisualizer->updateData (data);
}
void MainWindow::handleSerialError (const QString& error) {
ui->statusbar->showMessage (error, 5000 );
QMessageBox::critical (this , "串口错误" , error);
}
void MainWindow::handleParseError (const QString& error) {
ui->statusbar->showMessage (error, 5000 );
ui->txtRawData->appendPlainText ("[解析错误] " + error);
}
void MainWindow::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 ();
QTimer::singleShot (5000 , [this ]() {
ui->lblAlarm->setText ("" );
ui->lblAlarm->setStyleSheet ("" );
});
}
}
void MainWindow::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>
int main (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 中设计主界面,核心组件布局如下:
<widget class ="QWidget" name ="centralwidget" >
<layout class ="QVBoxLayout" name ="verticalLayout" >
<widget class ="QGroupBox" name ="groupBox" >
<property name ="title" > <string > 串口配置</string > </property >
<layout class ="QGridLayout" name ="gridLayout" >
<item row ="0" column ="0" >
<widget class ="QLabel" name ="label" >
<property name ="text" > <string > 串口:</string > </property >
</widget >
</item >
<item row ="0" column ="1" >
<widget class ="QComboBox" name ="cbxPortName" />
</item >
<item row ="0" column ="2" >
<widget class ="QPushButton" name ="btnRefreshPorts" >
<property name ="text" > <string > 刷新</string > </property >
</widget >
</item >
<item row ="0" column ="3" >
<widget class ="QPushButton" name ="btnOpenSerial" >
<property name ="text" > <string > 打开串口</string > </property >
</widget >
</item >
<item row ="1" column ="0" >
<widget class ="QLabel" name ="label_2" >
<property name ="text" > <string > 波特率:</string > </property >
</widget >
</item >
<item row ="1" column ="1" >
<widget class ="QComboBox" name ="cbxBaudRate" />
</item >
<item row ="1" column ="2" >
<widget class ="QLabel" name ="label_3" >
<property name ="text" > <string > 数据位:</string > </property >
</widget >
</item >
<item row ="1" column ="3" >
<widget class ="QComboBox" name ="cbxDataBits" />
</item >
<item row ="2" column ="0" >
<widget class ="QLabel" name ="label_4" >
<property name ="text" > <string > 校验位:</string > </property >
</widget >
</item >
<item row ="2" column ="1" >
<widget class ="QComboBox" name ="cbxParity" />
</item >
<item row ="2" column ="2" >
<widget class ="QLabel" name ="label_5" >
<property name ="text" > <string > 停止位:</string > </property >
</widget >
</item >
<item row ="2" column ="3" >
<widget class ="QComboBox" name ="cbxStopBits" />
</item >
<item row ="3" column ="0" colspan ="4" >
<widget class ="QPushButton" name ="btnSendData" >
<property name ="text" > <string > 发送读取指令</string > </property >
</widget >
</item >
</layout >
</widget >
<widget class ="QGroupBox" name ="groupBox_2" >
<property name ="title" > <string > 实时数据</string > </property >
<layout class ="QHBoxLayout" name ="horizontalLayout" >
<item >
<widget class ="QLabel" name ="label_6" >
<property name ="text" > <string > 温度:</string > </property >
</widget >
</item >
<item >
<widget class ="QLabel" name ="lblTemp" >
<property name ="text" > <string > 0.0 ℃</string > </property >
</widget >
</item >
<item >
<widget class ="QLabel" name ="label_7" >
<property name ="text" > <string > 压力:</string > </property >
</widget >
</item >
<item >
<widget class ="QLabel" name ="lblPressure" >
<property name ="text" > <string > 0.00 MPa</string > </property >
</widget >
</item >
<item >
<widget class ="QLabel" name ="lblAlarm" >
<property name ="text" > <string /> </property >
</widget >
</item >
</layout >
</widget >
<widget class ="QSplitter" name ="splitter" >
<property name ="orientation" > <enum > Qt::Horizontal</enum > </property >
<widget class ="QGroupBox" name ="groupBox_3" >
<property name ="title" > <string > 实时曲线</string > </property >
<layout class ="QVBoxLayout" name ="verticalLayout_2" >
<item >
<widget class ="QChartView" name ="chartView" />
</item >
</layout >
</widget >
<widget class ="QGroupBox" name ="groupBox_4" >
<property name ="title" > <string > 数据列表</string > </property >
<layout class ="QVBoxLayout" name ="verticalLayout_3" >
<item >
<widget class ="QTableWidget" name ="tableWidget" />
</item >
</layout >
</widget >
</widget >
<widget class ="QGroupBox" name ="groupBox_5" >
<property name ="title" > <string > 原始数据</string > </property >
<layout class ="QVBoxLayout" name ="verticalLayout_4" >
<item >
<widget class ="QPlainTextEdit" name ="txtRawData" />
</item >
</layout >
</widget >
</layout >
</widget >
<widget class ="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 实现的可视化界面直观展示设备运行数据,帮助运维人员快速掌握设备状态,降低故障排查成本。
该系统可广泛应用于智能制造、工业监控、设备运维等场景,通过简单的协议适配即可对接不同类型的工业设备,具备较高的工程实用价值。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online