跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Qt C++ 串口通信与数据可视化:工业设备实时采集系统

基于 Qt C++ 构建工业设备数据采集与可视化系统,集成串口通信、数据解析、实时绘图及异常报警功能。利用 Qt SerialPort 和 Charts 模块实现跨平台数据交互,支持 Modbus RTU 与自定义 ASCII 协议解析。系统采用模块化架构,包含断连重连、CSV 数据存储及阈值声音报警机制,适用于智能制造监控场景。

CoderByte发布于 2026/3/15更新于 2026/9/1278 浏览

一、技术背景与应用场景

工业现场中,PLC、传感器、智能仪表等设备常通过串口(RS232/RS485)输出实时运行数据(如温度、压力、转速、电压等)。Qt 作为跨平台的 C++ 应用开发框架,兼具串口通信 API 与强大的界面/绘图能力,是开发工业数据采集与可视化系统的理想选择。本文将完整实现一套工业设备数据实时采集系统,涵盖串口参数配置、数据解析、实时绘图、数据存储与异常报警等核心功能,满足工业场景下的高可靠性与实时性要求。

二、系统整体设计

2.1 核心功能模块

系统分为 5 个核心模块,各模块解耦设计,便于维护与扩展:

  1. 串口通信模块:负责串口参数配置、数据收发、异常处理(如断连重连);
  2. 数据解析模块:对串口接收的二进制/ASCII 数据进行解析,提取有效工业参数;
  3. 可视化模块:基于 Qt Charts 实现实时曲线绘制、数值仪表盘、数据表格展示;
  4. 数据存储模块:将采集数据存入本地文件(CSV),支持历史数据回溯;
  5. 报警模块:对超阈值数据进行界面提示与声音报警。
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 环境配置
  1. 安装 Qt 时需勾选'SerialPort'和'Charts'模块(Qt 6 中 Charts 属于 Add-ons);
  2. 在项目.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>
# 


  :  QObject {
    Q_OBJECT
:
    
    ;

    
      {
        QString portName;       
        qint32 baudRate = ; 
        QSerialPort::DataBits dataBits = QSerialPort::Data8;     
        QSerialPort::Parity parity = QSerialPort::NoParity;      
        QSerialPort::StopBits stopBits = QSerialPort::OneStop;   
        QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl; 
    };

    
    ;

    
    ;
    ;

    
    ;

signals:
    
    ;
    
    ;
    
    ;

:
    ;
    ~() ;

    
    ( SerialManager&) = ;
    SerialManager& =( SerialManager&) = ;

    
    QSerialPort* m_serialPort;
    
    QTimer* m_reconnectTimer;
    
    SerialParams m_lastParams;
    
    QMutex m_mutex;
    
      RECONNECT_INTERVAL = ;

 slots:
    
    ;
    
    ;
    
    ;
};

include
<QTimer>
#include <QMutex>
class
SerialManager
public
public
// 单例获取
static SerialManager* getInstance()
// 串口参数结构体
struct
SerialParams
// 串口名(如 COM3、/dev/ttyUSB0)
9600
// 波特率
// 数据位
// 校验位
// 停止位
// 流控
// 枚举可用串口
QStringList getAvailablePorts()
// 打开/关闭串口
bool openSerial(const SerialParams& params)
void closeSerial()
// 发送数据
bool sendData(const QByteArray& data)
// 接收数据信号
void dataReceived(const QByteArray& data)
// 串口状态信号
void serialStateChanged(bool isOpen)
// 错误信号
void errorOccurred(const QString& error)
private
explicit SerialManager(QObject *parent = nullptr)
SerialManager
override
// 禁用拷贝
SerialManager
const
delete
operator
const
delete
// 串口对象
// 重连定时器(断连后自动重连)
// 串口参数缓存
// 线程安全锁
// 重连间隔(ms)
const
int
3000
private
// 读取串口数据
void readSerialData()
// 处理串口错误
void handleSerialError(QSerialPort::SerialPortError error)
// 自动重连
void tryReconnect()
#endif // SERIALMANAGER_H
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,    // ASCII 协议
        ModbusRTUMode // Modbus RTU 协议
    };
    Q_ENUM(ParseMode)

    // 设置解析模式
    void setParseMode(ParseMode mode);

    // 解析原始数据
    QVariantMap parseData(const QByteArray& rawData);

signals:
    // 解析完成信号(key:参数名,value:数值)
    void dataParsed(const QVariantMap& data);
    // 解析错误信号
    void parseError(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_H
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;
    // 正则表达式匹配 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)
#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; // 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 轴范围
    void adjustYAxisRange();
    // 添加数据到表格
    void addDataToTable(double temp, double pressure);
};
#endif // DATAVISUALIZER_H
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));
        // 超阈值(如 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);
}

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() {
    // 计算 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);
}

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)));

    // 限制表格行数(最多 1000 行)
    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;
    // CSV 文件句柄
    QFile* m_csvFile;

    // 初始化 UI
    void initUI();
    // 初始化信号连接
    void initConnections();
    // 初始化 CSV 文件
    bool initCsvFile();
};
#endif // MAINWINDOW_H
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); // 曲线显示 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;
}

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(".");
    }

    // 生成 CSV 文件名(按时间戳)
    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;
    }

    // 写入 CSV 表头
    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();

        // 5 秒后清除报警提示
        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 测试步骤
  1. 串口模拟:使用串口调试工具(如 SSCOM)模拟工业设备,发送格式为 T:25.5,P:1.23\r\n 的 ASCII 数据;
  2. 功能验证:
    • 串口枚举、参数配置、打开/关闭功能正常;
    • 数据接收后能正确解析,仪表盘实时更新;
    • 曲线随数据动态绘制,坐标轴自适应;
    • 超阈值(温度>50℃/压力>2.0MPa)触发声音报警;
    • 数据自动写入 CSV 文件,表格显示历史数据;
    • 断连后自动重连,重连成功后恢复数据采集。
6.2 性能优化
  1. 数据更新频率:限制曲线更新频率(如 50ms/次),避免 UI 卡顿;
  2. 内存管理:表格数据限制最大行数(1000 行),曲线缓存长度可配置;
  3. 线程优化:将串口数据接收与解析放到子线程,避免阻塞 UI 线程(可通过 Qt 的 moveToThread 实现);
  4. 绘图优化:使用 QChart 的 OpenGL 加速(setRenderHint(QPainter::Antialiasing)),提升曲线绘制效率。

七、扩展功能建议

  1. Modbus 协议支持:完善 Modbus RTU/TCP 解析,适配工业标准协议;
  2. 历史数据回放:读取 CSV 文件,还原历史曲线;
  3. 多设备采集:支持多串口同时采集,多曲线对比显示;
  4. 网络传输:增加 TCP/UDP 模块,将数据上传至服务器;
  5. 自定义报警:支持用户配置阈值、报警方式(声音/邮件/短信);
  6. 导出报表:支持将历史数据导出为 Excel/PDF 报表。

八、总结

本文基于 Qt C++ 实现了一套完整的工业设备串口数据采集与可视化系统,涵盖串口通信、数据解析、实时绘图、数据存储、异常报警等核心功能。系统采用模块化设计,代码结构清晰,可扩展性强,适配多种工业串口协议,满足工业现场的高可靠性与实时性要求。通过 Qt Charts 实现的可视化界面直观展示设备运行数据,帮助运维人员快速掌握设备状态,降低故障排查成本。

该系统可广泛应用于智能制造、工业监控、设备运维等场景,通过简单的协议适配即可对接不同类型的工业设备,具备较高的工程实用价值。

目录

  1. 一、技术背景与应用场景
  2. 二、系统整体设计
  3. 2.1 核心功能模块
  4. 2.2 技术选型
  5. 三、开发环境搭建
  6. 3.1 环境配置
  7. 四、核心模块实现
  8. 4.1 串口通信模块(SerialManager)
  9. 4.1.1 头文件(serialmanager.h)
  10. 4.1.2 源文件(serialmanager.cpp)
  11. 4.2 数据解析模块(DataParser)
  12. 4.2.1 头文件(dataparser.h)
  13. 4.2.2 源文件(dataparser.cpp)
  14. 4.3 数据可视化模块(DataVisualizer)
  15. 4.3.1 头文件(datavisualizer.h)
  16. 4.3.2 源文件(datavisualizer.cpp)
  17. 4.4 主界面整合(MainWindow)
  18. 4.4.1 头文件(mainwindow.h)
  19. 4.4.2 源文件(mainwindow.cpp)
  20. 4.5 主函数(main.cpp)
  21. 五、界面设计(UI 文件关键部分)
  22. 六、功能测试与优化
  23. 6.1 测试步骤
  24. 6.2 性能优化
  25. 七、扩展功能建议
  26. 八、总结

更多推荐文章

查看全部
  • Python 中使用 HTML 模板的完整指南
  • Spring Boot 开发入门:从零搭建第一个 Web 项目
  • 法奥机器人 ROS2 环境搭建
  • Spring Web 模块核心概念与 RESTful API 调用实践
  • JVS-APS 解析:算法驱动与低代码融合的智能排产实践
  • 初阶数据结构:二叉树与堆的实现
  • 6 年自研纯 C# UI 引擎 XchyUI 轻量跨平台架构解析
  • PX4 无人机结合 MID360 与 FAST-LIO 实现室内定位及定点
  • Kimi Code:Moonshot AI 推出的智能编程助手
  • ChatGPT 结构化 Prompt 的高级应用技巧与实践
  • Milvus 实战:Attu 可视化安装与 Python 整合指南
  • 前端 Canvas 基础绘制与动画交互实战
  • Unity 编辑器扩展开发指南
  • AMD 显卡 Vulkan 兼容性深度解析:5 步解决 llama.cpp 部署难题
  • 腾讯前端 AI 面试考点整理:基础、原理与系统设计
  • Spring Boot 自动配置原理与自定义 Starter 实战
  • OpenCode 本地 AI 模型配置指南
  • 利用 AI 工具提升前端 UI 设计质量的实践方法
  • 费曼学习法:以教为学的高效成长路径
  • 基于 Cogito-v1-preview-llama-3B 的汽车电子 ECU 诊断逻辑建模实践

相关免费在线工具

  • 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