Qt C++ 串口通信+数据可视化:工业设备数据实时采集与界面显示

一、技术背景与应用场景

工业现场中,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)
#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_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(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_H
4.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_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(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_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(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 测试步骤

  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实现的可视化界面直观展示设备运行数据,帮助运维人员快速掌握设备状态,降低故障排查成本。

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

Read more

不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

不止“996”!曝硅谷AI创业圈「极限工作制」:每天16小时、凌晨3点下班、周末也在写代码

编译 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) “如果你周日去旧金山的咖啡馆,会发现几乎每个人都在工作。” 这是 AI 创业公司 Mythril 联合创始人 Sanju Lokuhitige 最近最直观的感受。去年 11 月,他特地搬到旧金山,只为了更接近 AI 创业浪潮的中心。但很快,他也被卷入了这股浪潮带来的另一面——一种越来越极端的工作文化。 Lokuhitige 坦言,他现在几乎每天工作 12 小时,每周 7 天。除了每周少数几场刻意安排的社交活动(主要是为了和创业者们建立联系),其余时间几乎都在写代码、做产品。 “有时候我整整一天都在编程,”他说,“我基本没有什么工作与生活的平衡。”而这样的生活,在如今的 AI 创业圈里并不算罕见。 旧金山 AI 创业圈的真实日常 一位在旧金山一家 AI

By Ne0inhk
黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

黄仁勋公开发文:传统软件开发模式终结,参与AI不必非得拥有计算机博士学位

AI 究竟是什么?在 NVIDIA CEO 黄仁勋看来,它早已不只是聊天机器人或某个大模型,而是一种正在迅速成形的“新型基础设施”。 近日,黄仁勋在英伟达官网发布了一篇长文,提出一个颇具形象的比喻——AI 就像一块“五层蛋糕”。从最底层的能源,到芯片、基础设施、模型,再到最上层的应用,人工智能正在形成一整套完整的产业技术栈,并像电力和互联网一样,逐渐成为现代社会的底层能力。 这也是黄仁勋自 2016 年以来公开发表的第七篇长文。在这篇文章中,他从计算机发展史与第一性原理出发,试图解释 AI 技术栈为何会演化成如今的形态,以及为什么全球正在掀起一场规模空前的 AI 基础设施建设。 在他看来,过去几十年的软件大多是预先编写好的程序:人类设计好算法,计算机按指令执行,数据被结构化存储在数据库中,通过精确查询调用。而 AI 的出现打破了这一模式——计算机开始能够理解图像、文本和声音,并根据上下文实时生成答案、推理结果甚至新的内容。 正因为智能不再是预先写好的代码,而是实时生成的能力,支撑它运行的整个计算体系也必须被重新设计。

By Ne0inhk
猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

猛裁1.6万人后,网站再崩6小时、一周4次重大事故!官方“紧急复盘”:跟裁员无关,也不是AI写代码的锅

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 过去几年里,科技公司几乎都在同一件事上加速:让 AI 参与写代码。 从自动补全、自动生成函数,到直接修改系统配置,生成式 AI 已经逐渐走进真实生产环境。但最近发生在亚马逊的一连串事故,却给整个行业泼了一盆冷水——当 AI 开始真正参与生产环境开发时,事情可能远比想象复杂。 最近,多家媒体披露,本周二亚马逊内部紧急召开了一场工程“深度复盘(deep dive)”会议,专门讨论最近频繁出现的系统故障——其中,一个被反复提及的关键词是:AI 辅助代码。 一周 4 次严重事故,亚马逊内部紧急复盘 事情的起点,是最近一段时间亚马逊系统稳定性明显下降。 负责亚马逊网站技术架构的高级副总裁 Dave Treadwell 在一封内部邮件中坦言:“各位,正如大家可能已经知道的,最近网站及相关基础设施的可用性确实不太理想。” 为此,公司决定把原本每周例行举行的技术会议

By Ne0inhk
这回真的“装”到了!来OpenClaw全国纵深行,你只需要带一台电脑……

这回真的“装”到了!来OpenClaw全国纵深行,你只需要带一台电脑……

AI Agent 的风,已经从 GitHub 吹到了线下。 过去几个月,越来越多开发者开始讨论一个问题: 当 AI 不再只是聊天,而是可以执行任务,软件会变成什么样? 在这股浪潮中,一个开源项目迅速进入开发者视野——OpenClaw,在 GitHub 上获得大量关注,相关教程、实践案例不断出现。有人用它自动整理资料,有人用它管理开发流程,还有人尝试让它执行复杂的工作流。 很多开发者第一次意识到: AI 不只是工具,它可能成为“执行者”。 不过,在技术社区之外,大多数人对 Agent 的理解仍停留在概念层面。 * AI Agent 到底是什么? * 如何在自己的电脑上运行? * 普通开发者能否真正用起来? 带着这些问题,一场围绕 OpenClaw 的开发者城市行动正在展开。 ZEEKLOG 发起的OpenClaw 全国纵深行将走进 20 个城市,用最直接的方式回答一个问题——如果

By Ne0inhk