ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt温湿度显示系统开发实战:串口通信、多线程与QChart实时曲线

Qt温湿度显示系统开发实战:串口通信、多线程与QChart实时曲线 我最早做这个Qt温湿度显示系统完全是帮实验室同事救急。对方买了一批USB转串口的温湿度传感器厂家给的上位机又老又难用曲线能看但没法导出数据。想着自己写一个也没多难结果从串口读到曲线刷新再到打包发给别人用前前后后踩了不少坑。这篇文章就把整个项目从需求拆解到最终落地的完整过程复盘一遍涉及QSerialPort串口通信、多线程数据采集、QChart实时绘图、Qt打包发布这些高频知识点适合正在学Qt或者准备做类似上位机项目的朋友参考按照这里面讲的思路来能少走很多弯路。1. 先从需求说起一个温湿度显示系统到底要做什么1.1 核心功能拆解很多人一上来就直接打开Qt Creator拖控件我建议先把需求想清楚。一个标准的温湿度显示系统核心功能其实是这几块实时采集温度、湿度数据把数据以数字和曲线形式展示出来超限时给出告警历史数据能保存和回看。听起来不复杂但每一块都有细节。比如实时采集你要决定数据是从串口来、网口来还是USB HID来。这次项目用的是串口传感器所以通信方式就定了QSerialPort。再比如数据显示一个QLabel谁都会写问题在于数据量上来以后曲线怎么刷新才能不卡界面这就是很多人说的“Qt曲线刷新能放在另一个线程里面吗”这个问题的来源。界面布局也需要提前想清楚温度湿度各一个实时数值显示区域一个QChartView放曲线底部是日志和告警信息区。我见过有人做了两三个界面来回切换反而麻烦。单窗口多区域是这类上位机最舒服的布局。1.2 技术选型为什么是Qt而不是Web前端现在做个可视化系统很多人第一反应是Vue、ECharts加个后端但一碰到串口通信就绕远了——浏览器没法直接读串口需要用中间服务转发或者依赖Web Serial这种新API兼容性和稳定性都有限。Qt的优势在于它是一个真正意义上的桌面原生框架。C直接操作串口、文件、系统底层资源性能开销低信号槽机制让异步数据传递非常顺手QChart、QCustomPlot这类绘图库成熟稳定。对温湿度采集这种需要长时间运行、低延迟响应、稳定连接传感器硬件的场景Qt是更省心的选择。另外Qt跨平台特性也值得一提。你在Windows上开发完代码几乎不用改就能在Linux下重新编译。很多工业现场的上位机都跑在Windows工控机上但也不排除有人部署在Ubuntu的嵌入式设备上用Qt意味着这些场景都能覆盖。1.3 架构设计界面线程与采集线程要分开这是整个项目最关键的一个设计决策。串口读取数据、解析数据帧这些操作绝对不能放在界面主线程里做。原因很简单一旦串口数据量大或者解析逻辑里出现阻塞比如同步等待设备响应界面刷新就会卡住用户拖窗口、点按钮都没反应体验非常差。我这次采用了典型的“采集线程 主界面线程”模型把QSerialPort对象moveToThread到一个QThread子线程中在子线程里接收串口数据、校验帧头帧尾、解析温度湿度值然后通过信号把解析好的数据发回主线程主线程只负责接收数据并刷新UI。这里有个容易踩的坑QSerialPort本身是异步的它发出readyRead信号后如果你在槽函数里做复杂的解析或耗时处理照样会阻塞当前线程。所以串口对象所在线程的优先级事件循环要保持轻量重活可以再拆出去或者用队列缓存数据。2. 环境搭建与基础工程准备2.1 Qt版本和编译器的选择说到Qt环境很多人一上来就卡壳。这次项目我用的版本是Qt 5.15.2装的是MSVC2019 64位套件。为什么不选Qt 6原因很实际Qt 5.15.2是目前兼容性极好的LTS版本网上的教程、现成的示例代码最多遇到问题搜索一下就有答案适合项目开发Qt 6虽然更新但有些第三方串口库、旧代码风格需要额外适配没必要在练手项目里折腾。编译器方面Windows下就是MSVC和MinGW二选一。我推荐MSVC因为打包出来的程序在目标机器上的运行时依赖更可控性能也略好。MinGW胜在开箱即用不用装Visual Studio但个别模块比如某些Qt插件可能遇到兼容问题。如果你用的是Qt 5.15.2记得MSVC2019对应的运行库要装全否则发布后对方机器上全是“找不到MSVCP140.dll”这类报错。2.2 新建工程的配置细节用Qt Creator新建Widgets Application后我习惯先把.pro文件里需要的模块声明齐了。温湿度显示系统至少需要QT core gui serialport charts注意默认模板里通常只有core和guiserialport和charts需要你手动加。不加的话include相关头文件时直接报错。很多人问“为什么我用了QChart却编译不过”99%就是.pro里少写了charts。创建主窗口类时我建议基类选QMainWindow而不是QWidget。QMainWindow自带菜单栏、状态栏、Docker布局体系后期加菜单比如“导出数据”“打开配置文件”会非常方便。项目结构上尽量把串口通信、数据解析、绘图分别独立成类比如ComThread、ModbusParser、ChartWidget这样分工明确出了问题也好排查。2.3 串口通信与Modbus协议接入准备温湿度传感器常用的通信协议有Modbus RTU和自由文本协议两大类。Modbus RTU在工业场景里非常普遍几乎每个PLC和数据采集模块都支持文本协议则常见于一些低成本温湿度传感器输出类似“T:25.6 H:58.3”这样的ASCII字符串。这次项目里我先做了自由文本协议的解析逻辑简单调试方便。后面专门预留了一个ModbusParser类用来对接Modbus RTU的03功能码读保持寄存器。如果你用的是Modbus设备需要在界面里让用户配置从站地址、寄存器起始地址、寄存器数量然后组帧发送收到响应后再按字节解析。这部分代码比较机械但一定要把超时处理和CRC校验写对否则通信质量会非常不稳定。3. 数据采集与多线程设计3.1 为什么采集必须放到单独的线程我见过很多新手写的上位机直接在mainwindow里new一个QSerialPort然后连接readyRead信号刷新UI。小数据量、低频率下这么写勉强能跑但一旦波特率提高、数据帧变长或者你引入类似Modbus的请求-应答机制主线程就很容易阻塞。界面卡顿只是表象更严重的是串口接收缓冲区溢出丢帧丢到怀疑人生。放到子线程后采集、拼包、解析这些琐碎工作在后台完成主线程只做轻量的刷新动作两者通过信号槽通信。Qt的跨线程信号槽默认走队列连接也就是发送和接收不在同一线程时信号会自动排队投递接收方在自己的事件循环里处理。这正好是一个天然的线程安全机制比你自己加锁传递数据简单可靠得多。3.2 串口数据读取与粘包半包处理串口数据是一个字节一个字节到缓冲区里的你没办法保证一次readyRead收到的就是一整帧。所以解析数据时必须有“黏包/半包”处理思路维护一个QByteArray缓存每次收到新数据就累加到缓存尾部然后循环检查缓存里是否有完整的一帧数据有就切出来解析没有就等下次。我用的文本协议帧格式是这样约定的[HEAD][温度值][湿度值][CRC][TAIL] AA 55 19 0C 25.6 58.3 ...简单文本版就是每次以“\r\n”作为结束符比如“T:25.6,H:58.3\r\n”。读取逻辑大致是void SerialWorker::onReadyRead() { buffer.append(serial-readAll()); while (buffer.contains(\n)) { int pos buffer.indexOf(\n); QByteArray line buffer.left(pos).trimmed(); buffer.remove(0, pos 1); parseLine(line); } }这就是典型的按行切帧简单可靠。如果你用Modbus RTU那帧判断要按字节间隔计算接收完一帧后判断后续3.5个字符时间内是否还有新数据没有则认为一帧结束。这块写起来更讲究一些但理解了上面的缓存思路Modbus协议也不难实现。3.3 信号槽跨线程传递数据子线程解析完数据后通过信号把温度、湿度值发出去。这里有一个重要原则跨线程传数据要用值传递不要用指针传递。class SerialWorker : public QObject { Q_OBJECT signals: void dataReceived(double temp, double humi); public slots: void onReadyRead(); };主线程MainWindow里连接这个信号然后在槽函数里更新界面。Qt的信号槽机制会保证这个信号安全地跨线程投递数据以值拷贝方式传递不会出现同一份数据被多线程同时访问的问题。我在实际项目里遇到过一种情况数据量特别大时信号投递频率太高主线程来不及处理积压的队列越来越大内存不断上涨。解决办法是降低信号发送频率——比如每秒最多发20次或者用定时器在子线程里主动读取最新值再发送。对于温湿度采集这种秒级变化的场景其实每秒发一次都完全够用。4. 界面布局与实时绘图实现4.1 用卡片式布局组织仪表盘信息布局是很多人不重视但实际很影响使用体验的部分。我的习惯是把主界面分成三个区域顶部或左侧放“当前值展示区”中间大片区域给“实时曲线图”底部放“告警日志与状态栏”。当前值展示区我用的是自绘的卡片式QFrame里面包含一个QLabel显示数值、一个QLabel显示单位、一个QLabel显示更新时间。为了提高可读性温度超过设定阈值时卡片边框变成红色并闪烁正常时是绿色这个用动态属性和QSS配合就能实现。QSS是Qt界面美化的核心手段几乎不需要什么额外控件。比如设置一个圆角卡片QFrame#CardGood { background-color: #ffffff; border-radius: 8px; border: 2px solid #2ecc71; } QFrame#CardAlarm { background-color: #fff0f0; border-radius: 8px; border: 2px solid #e74c3c; }在小项目里用QSS完全够了不建议为了好看引入Qt Quick或QML那会陡增复杂度维护成本也高。4.2 QChart实现温湿度双曲线Qt 5.7之后集成了QChart模块画实时曲线很方便。我的实现思路是使用QChartView作为容器两个dataset分别对应温度和湿度都使用QSplineSeries平滑曲线或QLineSeries折线X轴用QDateTimeAxis显示时间Y轴动态范围温度和湿度各自设置合理的range数据点通过append追加当数据点超过最大数量时把最前面的点删掉形成滚动的窗口效果。QChart *chart new QChart(); chart-legend()-setVisible(true); chart-setAnimationOptions(QChart::NoAnimation); // 性能优先动画反而卡 QSplineSeries *tempSeries new QSplineSeries(); tempSeries-setName(温度); tempSeries-setColor(QColor(#e74c3c)); QSplineSeries *humiSeries new QSplineSeries(); humiSeries-setName(湿度); humiSeries-setColor(QColor(#3498db)); QDateTimeAxis *axisX new QDateTimeAxis(); axisX-setFormat(HH:mm:ss); axisX-setTitleText(时间); QValueAxis *axisY new QValueAxis(); axisY-setLabelFormat(%.1f);这里有个让人纠结的问题QChart里的数据在绘图时是怎样的刷新节奏我是用一个QTimer500ms触发一次从QVector缓存里取出最新数据批量更新到Series。注意不要每收到一个数据点就刷新一次图表那样绘制开销太大实测CPU占用会明显飙升。4.3 绘图效率优化与滚动刷新策略曲线刷新的效率直接决定系统能不能长时间稳定运行。我推荐一个策略维护一个环形缓存容量设置为600个点比如10Hz采样下能存60秒数据每次新数据到来时先判断缓存是否已满满了就把最早的坐标点remove掉然后append新点。这样曲线始终是最近一分钟的数据滚动效果流畅内存占用也不会无限增长。还有一个优化细节QChart的removePoints相对比较耗性能数据很多时建议不要逐点删而是直接调用series-clear()后整体重新填充或者干脆使用replace()方法在合适位置更新。当然对温湿度这种低频采集逐点删除问题不大但如果你并行采集几百个通道这个优化就是必需的了。绘图本身一定在主线程做不要在子线程里new QChart或QChartView。跨线程操图形对象是非常典型的崩溃隐患轻则闪退重则死锁。正确的做法是子线程传数据信号主线程槽函数里更新图表。5. 温湿度系统高频问题排查实录5.1 串口打不开或收不到数据常见原因排第一的是串口号不对Windows下COM口经常因为插入USB顺序变化而变动建议界面上做一个刷新串口列表的按钮第二是波特率、数据位、停止位、校验位没和传感器模块保持一致这个没有技巧只能逐项核对传感器手册第三是串口被占用比如厂家上位机还开着调试工具串口被独占打不开。还有一个特别容易忽略的问题USB转串口线质量差或驱动版本不匹配时readyRead信号可能间隔很久才触发一次数据在缓冲区里攒了一堆才通知你。程序里别想当然地认为每次信号就来了一帧数据一定要用上面说的缓存按帧切分逻辑。5.2 界面卡顿与假死界面假死十有八九是主线程里做了耗时操作。检查下面两处一是有没有在某个信号槽里直接同步地调用类似usleep、sleep之类的阻塞操作二是有没有在UI线程里进行大文件读写或数据库操作比如点击“导出”按钮后直接同步写几万条数据到EXCEL界面必然冻住解决办法是导出也丢到线程池。另一个容易被忽视的坑在主线程中使用QMessageBox::information等模态对话框时如果触发的时机是在密集的数据刷新槽函数里对话框可能被多次重复弹出导致界面卡死。设置一个布尔量做互斥或者用全局标志位很管用。5.3 打包后提示缺少DLL或版本冲突Qt程序发布给别的机器用最常见的报错就是“无法启动此程序因为计算机中丢失Qt5Core.dll”之类。用windeployqt工具可以自动收集依赖库。命令行里执行cd /d D:\build\release C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe AppName.exe执行完后目录下会多出一堆Qt相关DLL和插件文件夹整个目录拷给目标机器就能跑。注意两点一是必须是release版本debug版本带调试信息发布过去运行效率差且依赖一大批debug库二是windeployqt要和你打包的Qt版本严格对应用Qt 5.15.2编的程序却用Qt 5.15.3的windeployqt处理轻则多拷几个用不到的库重则引入版本不一致导致启动崩溃这就是网上常见那句“cannot mix incompatible Qt library”报错的来源。5.4 曲线刷新卡顿与内存增长曲线卡顿先看是不是多个Series共用一个Axis时没设置好动态范围。湿度值在0~100之间温度值可能在-20~60之间同一个Y轴显示会非常不协调。我把温湿度分成左右两个Y轴左边温度、右边湿度数据各自落在合理区间看起来清晰多了。内存不断增长则要检查是不是每帧都new对象但没有释放。QChart的append会保留数据点的副本如果你缓存列表也在无上限地追加那就是双重增长。建议给缓存设置上限同时在内存里保留数据点数量比如温度series最多保留600个点到上限后每次append前先remove(0)可以保证长期运行内存稳定。问题现象可能原因排查思路串口打不开串口号错误/被占用刷新列表、关闭其他串口软件数据乱码波特率不匹配、编码格式错核对参数、统一UTF-8或ASCII界面卡死主线程耗时操作耗时逻辑移入线程曲线不刷新信号未连接/队列积压确认connect类型、减少发送频率发布后缺DLL未运行windeployqtrelease后执行对应版本deploy工具程序启动崩溃Qt版本混用保持编译/部署/运行环境版本一致6. 项目落地的扩展与优化方向6.1 数据存储与回放功能实时显示只能看当下温湿度监测最重要的是趋势分析。我建议把原始数据存到SQLite数据库里Qt的QSqlDatabase对SQLite支持得非常完善不需要额外安装服务一个文件搞掂。存储策略上不要每帧都写数据库那会白白消耗磁盘IO和增加程序复杂度。我采用定时落盘每秒采集1条数据数据先放到内存队列每30秒统一批量写入一次。即使突然断电最多丢失30秒的数据对温湿度监测场景完全可以接受。回放功能做起来也很顺手用户选择时间段程序从数据库查询出数据重新填充到Series里曲线就会像录像一样重新滚动起来。这块代码量不大但实用价值极高——很多用户就是需要早上看昨晚的温湿度变化而不是一直盯着屏幕。6.2 自定义仪表盘控件QChart画曲线很强大但如果要做类似指针式温湿度计那种仪表盘就需要自己写QWidget了。Qt里自定义控件其实不复杂核心是重写paintEvent用QPainter画圆弧、刻度线、指针再用QTimer驱动指针角度变化。我当时做了一个圆环仪表盘上半圆画刻度数值下方显示当前温度和湿度的数字中心用渐变背景模拟发光效果。这样界面比单纯数字直观很多用户扫一眼就知道温湿度是否正常。你也可以用网上开源的Qwt例子或者自定义仪表盘控件但自己写一遍paintEvent对理解Qt绘图体系很有帮助。6.3 国际化和模块化扩展程序做完了如果还想给海外客户用Qt的国际化机制很成熟代码里所有可见字符串都走tr()包裹然后使用lupdate工具提取.ts文件用Qt Linguist翻译后生成.qm文件运行时通过QTranslator加载即可切换语言。此外如果有更复杂的控制需求比如要控制多个温湿度采集模块、对接PLC可以在现有架构上增加多个采集器实例每个采集器对应一个SerialWorker线程统一注册到一个数据管理器中界面层完全不需要改动。这就是前面模块化设计的好处——串口通信、数据解析、界面显示彼此解耦每加一个需求就往对应模块里加功能不会牵一发而动全身。我在实际调这个项目时最大的体会是先把底层的串口通信和数据协议调通再用模拟数据生成器把界面和曲线跑起来最后才接真实传感器。这个顺序能帮你把硬件、软件的问题分开排查不至于混在一起的时候设备有问题你也怀疑自己的代码代码有bug你又去折腾硬件。Qt温湿度显示系统虽然看起来是个练手规模的项目但串口、多线程、绘图、数据库、打包发布这些点串起来之后它几乎覆盖了桌面端上位机开发的全链路认真做完一遍后面再做更复杂的设备管理、SCADA类系统思路都能顺下来。
返回列表