一、技术背景与应用场景
工业现场中,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文件中添加模块依赖:
cpp
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)
cpp
#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; // 串口名(如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();
// 打开/关闭串口
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;
// 重连间隔(ms)
const int RECONNECT_INTERVAL = 3000;
private slots:
// 读取串口数据
void readSerialData();
// 处理串口错误
void handleSerialError(QSerialPort::SerialPortError error);
// 自动重连
void tryReconnect();
};
#endif // SERIALMANAGER_H
4.1.2 源文件(serialmanager.cpp)
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)
cpp
#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)
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)
cpp
#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)
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)
cpp
#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)
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)
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中设计主界面,核心组件布局如下:
xml
<!-- 关键组件示例 -->
<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实现的可视化界面直观展示设备运行数据,帮助运维人员快速掌握设备状态,降低故障排查成本。
该系统可广泛应用于智能制造、工业监控、设备运维等场景,通过简单的协议适配即可对接不同类型的工业设备,具备较高的工程实用价值。