前阵子一个老同事打电话问我:你们那套“中国移动NB-IoT QT采集终端”,到底是怎么个做法?我缓了一下才意识到,这个项目名其实已经把关键技术路线全说清楚了——NB-IoT决定它跟外界通信的通道,QT决定了终端侧软件用什么框架来写,采集终端才是它真正的身份。
这套东西说到底,是放在工业现场边缘侧的一台采集“小管家”:下面接着各种传感器和仪表,上面通过NB-IoT模组连接运营商网络,背地里还要干好本地数据解析、波形展示、缓存补传这些细活。如果你正好在做物联网采集设备、NB-IoT接入、Qt上位机或者嵌入式验证工具,这篇内容应该能帮你少踩不少坑。我把从方案选型、串口采集、FFT绘图到NB-IoT入网上报、再到跨平台打包排障的完整过程,按可以复现的方式写一遍。
1. 项目定位与整体设计思路
1.1 先拆清楚“采集终端”到底是个什么角色
项目一开始最容易犯的错,就是把“采集终端”当成一个App或者一个后台服务来做。实际上它在整个物联网系统里处于最前线:和传感器打交道、把分散的数据收拢、做初步处理,然后再把结果交给网络。可以说它就是整个数字化系统的“触手”,既要能听懂底层设备的Modbus/串口协议,又要能用NB-IoT把自己的心跳和数据递出去。
我这边实际负责的是终端侧Qt程序,跑在工控机/嵌入式主板上。它一边接RS485总线上挂的温湿度、压力、振动传感器,一边接NB-IoT模组(移远BC26那类),把采集数据封装后走运营商网络送到物联网平台。所以这个项目的产品形态不是云平台,更不是纯手机App,而是那个天天“蹲在现场”的本地终端。
1.2 为什么选了NB-IoT而不是4G/WiFi
这是方案评审阶段被问得最多的问题。道理也简单:现场在厂区外围和部分地下室,WiFi信号不稳定,普通4G又费电、资费也下不来。NB-IoT在城市基础设施、表计、管线监测这些领域覆盖优势非常明显,一个基站能扛几万个终端,穿墙能力强,终端侧功耗也能压到极低,用电池都能撑很久。我们这项目里虽然没有非常强的电池约束,但覆盖和成本同样是硬指标,所以主通道确定用NB-IoT。
NB-IoT也不是万能。它的下行速率只有几十kbps的级别,不适合把原始波形直接传上去。所以我们针对带宽做了“本地处理、远程上传特征值/摘要”的策略:终端里完成FFT频域转换,把谱线极值、频带能量这些压缩后的结果传上去,原始时域信号只留本地。这个取舍贯穿了整个QT采集终端的设计,后面所有模块的切分其实都围绕它展开。
1.3 Qt在这个项目里解决了什么问题
很多人在项目初期会纠结:Windows上用MFC,Linux上用GTK,或者干脆做个Web页面。实际把需求摊开之后,Qt的优势非常明显:跨平台,我这套代码在Windows上开发调试,最终要部署到麒麟Linux的工控机上,不需要重写一套界面。C++的性能用来做波形处理也够用,加上QSerialPort、QNetworkAccessManager、QSQLite这些现成模块,基本不用四处凑第三方库。
界面展示和图表是我选Qt的另一个原因。采集终端需要看到实时时域波形和频域谱线,QCustomPlot这个纯C++绘图控件在Qt里整合起来非常顺手,缩放、游标、动态刷新都能自己控制。相比Qt官方的QChart,QCustomPlot在嵌入式场景下更轻,定制也更透。最后定的是Qt 5.15.2 LTS,长期维护版本,生态和第三方库兼容性都更稳。
1.4 整体链路与模块划分
整个系统的数据流可以这样理解(不是严格架构图,方便大家对齐):
传感器/仪表 → RS485串口 → Qt采集服务 → 数据缓存 → FFT/特征提取 → 界面显示;同时,特征数据与摘要 → NB-IoT模组(AT指令) → 中国移动NB-IoT网络 → 物联网平台。
终端侧Qt软件划分成四块:采集通信层负责串口收发和Modbus解析;数据处理层负责FFT和特征值计算;存储层用SQLite管本地缓存和补传;界面层负责实时波形、参数配置和调试日志。网络上报层再单独拆出来,管NB-IoT模组AT指令和UDP数据通道。分层做的好处是现场某个模块出问题不会一崩全崩,改动和排障都快。
2. 核心模块逐个拆解与实现要点
2.1 串口采集与Modbus协议解析
采集层用的就是Qt自带的QSerialPort。配置串口参数时,不要只看波特率,校验位、数据位、停止位任何一个不匹配都会导致一片乱码。常见设备默认9600/8/N/1,但真实项目里很多电表、传感器是2400甚至1200波特率,必须先跟设备厂家确认,否则调一整天都读不到有效数据。
代码骨架:
m_serial = new QSerialPort(this); m_serial->setPortName(config.portName); m_serial->setBaudRate(config.baudRate); m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); m_serial->setStopBits(QSerialPort::OneStop); m_serial->setFlowControl(QSerialPort::NoFlowControl); m_serial->open(QIODevice::ReadWrite); connect(m_serial, &QSerialPort::readyRead, this, &DataAcquirer::onSerialReadyRead);关键点:不要在槽函数里做阻塞等待和重逻辑,串口数据是“来了就触发”,一帧Modbus报文可能在几次readyRead中才凑齐。我习惯在程序里维护一个接收缓冲区,每收到一段就尝试解析帧头、长度、CRC,合法帧才交给上层。CRC16/Modbus校验必须自己写好,否则现场总线上偶尔的电噪声会导致误帧,而误帧比丢帧更危险。
另外,QSerialPort有一个容易踩的坑:它在哪个线程里创建和读写,就必须一直在那个线程里访问,不要在UI线程直接去写一个工作线程的串口对象。我后来把整个串口读写和Modbus状态机放到一个QThread里面,界面只通过signal/slot拿最终数据,稳定很多。
2.2 时域转频域:用kissfft计算,用QCustomPlot显示
振动监测、谐波分析、电机异响检测这类场景,用户不只想看原始波形,更想看频域结果。这里就需要把时域波形做FFT转换。FFT库我选了kissfft,理由很简单:代码量小、纯C实现、依赖为零,放进Qt工程里一个文件夹就能编过。FFTW功能当然更全面,但体积和依赖不太适合工控终端快速部署。
两个库的对比可以这样看:
| FFT库 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| FFTW | 计算极快、功能全面 | 体积大、依赖重、授权复杂 | 仿真分析、服务器端运算 |
| kissfft | 轻量、零依赖、编译简单 | 功能相对基础 | 嵌入式终端、Qt本地实时FFT |
FFT的准确性取决于两点:采样率和帧长度。采样率必须大于目标最高频率的两倍,这一点在采集端配置传感器时就定死;帧长度选1024或2048,既能分辨出低频特征,又不会让计算量失控。具体做FFT的代码参考:
const int N = 2048; kiss_fft_cfg cfg = kiss_fft_alloc(N, 0, nullptr, nullptr); kiss_fft_cpx *in = new kiss_fft_cpx[N]; kiss_fft_cpx *out = new kiss_fft_cpx[N]; for (int i = 0; i < N; ++i) { in[i].r = waveBuffer[i]; in[i].i = 0.0f; } kiss_fft(cfg, in, out); for (int i = 0; i < N / 2; ++i) { double mag = sqrt(out[i].r * out[i].r + out[i].i * out[i].i); freqMagnitude[i] = mag / (N / 2); } kiss_fft_free(cfg);得到频域幅度谱后,就是QCustomPlot的活了。我在界面上放两个plot控件,一个画时域波形,一个画频域谱线,QCustomPlot的addGraph、setData、replot三件套很直接。需要注意:高频次循环replot在低配工控机上会吃满CPU,解决办法是把刷新频率限制在10-20Hz,并调用setNoAntialiasing关掉抗锯齿;曲线点数如果超过几千就做下采样,否则一次setData的时间都会超过几十毫秒。
2.3 NB-IoT模组接入:AT指令入网全流程
NB-IoT模组(BC26/BC35-G这类)本质是个“会联网的串口设备”,通过UART发送AT指令来控制。项目一上来最容易懵的就是指令流程,我直接给一份我们现场验证过的简化流程:
- 上电后先发 AT,模组回 OK,确认串口通。
- 设置射频功能:AT+CFUN=1。
- 终端入网注册:AT+COPS=0 或自动,随后循环查询 AT+CEREG?,直到返回 +CEREG: 0,1 表示已注册。如果长时间 0,2/0,3,说明信号差或SIM卡未在NB-IoT网络开户。
- 配置PDP上下文:AT+CGDCONT=1,"IP","cmnbiot"。部分区域会用到 cmiot,以SIM卡和当地核心网配置为准。
- 建数据通道,UDP场景:AT+NSOCR="UDP",53,1,返回 socket id。
- 发送数据:AT+NSOST=0,"<平台IP>",<端口>,<长度>,<十六进制数据>。
对应的关键AT指令示例:
AT OK AT+CFUN=1 OK AT+CEREG? +CEREG: 0,1 OK AT+CGDCONT=1,"IP","cmnbiot" OK AT+NSOCR="UDP",53,1 0 OK AT+NSOST=0,"120.0.0.1",5683,10,0102030405060708090A OK一定注意,不要在查询注册状态那里写死超时。NB-IoT模组在信号弱或刚上电时,注册可能用十秒甚至更久;PDN激活后,也要检查AT+CGPADDR=1拿到IP地址,否则后面数据根本没有发送通道。
数据上报的小技巧:由于NB-IoT带宽和网络特性,建议把多条采集结果先本地聚合,到上报周期统一发一个小批次,而不是来一条发一条。模组频繁进出PSM休眠会带来额外的唤醒时间和功耗,合并发包在现场能明显提高成功率。
2.4 本地缓存与断网补传:别让数据丢在现场
终端一旦断网或信号不稳,采集数据不能直接丢弃,否则整个系统的采集价值就没了。我这里用QSQLite做本地存储,里面一张pending表,字段大致是id、采集时间、设备地址、上报数据、上报状态、重试次数。正常上报成功后把对应记录标记掉,失败则留在待重试队列;一个后台定时器每30秒扫一次表,把超时未上报的数据重新组包上传。
补传逻辑里有一个容易出问题的点:不能因为队首那条一直失败,就把后面所有数据卡死。我给每条记录都带上重试次数上限,超过上限就标记“异常”,再扫下一批。这样后台发送线程永远只处理可重试的记录,整体吞吐不会被单条坏数据堵住。
3. 从工程搭建到打包部署的实操记录
这个环节看着不起眼,却能直接耗掉两三天,值得单独拿出来讲。
3.1 Qt版本、编译器、pro文件的坑
我们工程最终定的是Qt 5.15.2。下载安装方便起见,用国内镜像源会快很多。安装包下载时组件一定要跟工程类型匹配:MinGW 32位工程,就得用MinGW对应的Qt组件,MSVC工程则需要Qt的msvc2019组件外加Visual Studio工具链。编译器不匹配的情况,Qt Creator打开工程就会报一堆moc和头文件错误。
热搜里那条“dependent '........\allinstall\qt\5.15.2\msvc2019\include\qtw...”其实是典型的pro文件头文件路径写成了相对路径,指向了某台机器上的allinstall共享目录,换一台机器就崩。我的习惯是所有第三方库目录用变量定义,或者直接用$$PWD相对工程文件路径。大工程还可以把公共配置抽成pri文件,比如qcustomplot.pri、kissfft.pri,这样在多模块项目里不会到处复制路径。
顺便说一句,开发IDE之争在2024年其实没那么玄:Qt Creator对CMake/qmake项目和调试器集成都够省心;VS Code更适合远程开发或者已经有大量存量工程的团队,但新手在配环境上容易多花时间。我的个人选择是终端调试用Qt Creator,日常代码浏览用VS Code加Qt插件。
3.2 关键代码实现:从串口帧到界面刷新再到上报
整套流程是:串口解析完数据后,把原始波形放进缓冲区,数据处理线程取出一个时域帧算FFT,再通过信号通知主线程刷新曲线。上报线程独立跑,拿特征数据封JSON走网络。
刷新逻辑示意:
void MainWindow::onWaveReady(const QVector<double> &time, const QVector<double> &wave, const QVector<double> &freqAxis, const QVector<double> &mag) { m_timePlot->graph(0)->setData(time, wave); m_freqPlot->graph(0)->setData(freqAxis, mag); m_timePlot->rescaleAxes(); m_freqPlot->rescaleAxes(); m_timePlot->replot(QCustomPlot::rpQueuedReplot); m_freqPlot->replot(QCustomPlot::rpQueuedReplot); }注意replot这里用rpQueuedReplot,可以在高频UI刷新时合并重绘调用,避免卡顿;如果数据本身有间断,记得调用setData时把超出范围的坏点过滤掉,否则坐标会自动缩到惊人的范围,图表看起来就像一条竖线。
3.3 跨平台打包与麒麟Linux部署
Windows打包是老生常谈。打开Qt自带的命令行环境,进入exe所在目录执行:
windeployqt 你的程序名.exewindeployqt会把Qt需要的DLL和platforms目录自动拷贝过来。但要注意,如果你的程序动态用到了QCustomPlot、kissfft等第三方开库的DLL,windeployqt不会帮你带,还得自己手工拷贝。发布后如果双击没反应,先到命令行运行exe看报错,这是最快定位方式。
部署到麒麟Linux(x86架构)相对麻烦一点。一是必须在同类系统或兼容环境上编译,二进制包要和系统的GLIBC等基础库匹配;二是Qt的运行依赖里面,libxcb、libGL、fontconfig这些一个都不能缺。现场没有网络、只能离线装Qt时,找一个对应架构的Qt离线安装包最省事;装好后可以用qmake -query查安装路径,避免Linux下找不到Qt的尴尬。
发布目录里一定要有platforms子目录,里面放libqxcb.so,否则Qt会在麒麟系统上直接报“No Qt platform plugin could be initialized”然后退出。这个问题我在第一个现场版本就踩过,后来把platforms目录和依赖库按固定目录结构拷好,同时在入口程序旁边加qt.conf指定Plugins路径,基本就稳了。
3.4 界面自动化与命令行调试方式
多轮现场调测下来,我额外在程序里加了两个能力:用QCommandLineParser解析启动参数,支持--config=xxx指定配置文件、--debug打印AT指令日志;界面右上角放一个独立的“调试台”窗口,把串口收发和AT指令raw日志实时滚出来。这个“调试台”在远程支持时帮了大忙,用户把日志发过来,现场问题基本能一眼定位。
界面上那些重复的点击验证,也别纯靠手点。Qt自带的QTest可以模拟鼠标点击、键盘输入,我在回归验证时就用QTest::mouseClick去触发保存配置、切换页面,省下不少人力。如果你的界面逻辑比较复杂,强烈建议在工程里加一个自动化测试目录,哪怕只是跑冒烟用例,也能在改版后快速发现严重回归。
4. 常见问题与排查技巧实录
最后把现场真实遇到的高频问题列出来,按现象、原因、处理方式给一份速查。
4.1 启动即崩溃与Qt平台插件错误
启动崩溃最常见的是假死,或者弹窗“No Qt platform plugin could be initialized”。这个报错的本意是Qt找不到对应平台的插件dll/so。Windows上多半是platforms文件夹里没有qwindows.dll,或插件目录不在可执行程序旁边;Linux上大概率是platforms里缺少libqxcb.so,或者xcb依赖的libGL缺失。
排查办法很固定:先在命令行直接运行程序,看有没有“error while loading shared libraries”;然后把环境变量QT_DEBUG_PLUGINS=1开开,Qt会输出找插件的过程,一目了然。固定发布包建议用qt.conf把Plugins路径明确到安装目录,不要依赖环境变量。
4.2 HTTP POST请求报错与返回异常
Qt里用QNetworkAccessManager发POST,有时服务器会回“Request method 'POST' not supported”。第一反应不要怀疑Qt,先确认服务器端接口是否真的接受POST;很多时候是URL写错、网关只开了GET,或者代码里setTransferTimeout设太短,请求还没发出去就被中断。也遇到过用sendCustomRequest时HTTP Verb设置成小写导致服务器不识别,后来老老实实改用manager->post(...)。
POST发送JSON时,Content-Type必须设置成application/json,否则服务端解析不到body。返回读取不要放在信号槽里跨线程直接操作UI,建议用Lambda捕获reply,在lambda里先处理完数据,再把结果用signal抛回主线程。
4.3 串口打不开和Modbus无响应的本质原因
现场“串口打不开”一半以上是权限或占用问题。Linux下要检查用户是否在dialout组,Windows下则是COM口号写错或驱动没装;还有一种情况是程序上一轮崩溃没释放串口,重启后设备被系统占用,杀掉残留进程就好。
Modbus请求发出去没有响应,按顺序查:设备地址对不对、功能码有没有写错、CRC有没有算错、从站波特率和数据格式是否一致。我在调试时会直接把发送和接收的hex日志打到调试台,逐字节对照协议文档,比瞎猜有效得多。NB-IoT上报没有回应的排查也类似,先确认APN、PDN激活,再看网络注册状态,最后看平台侧有没有到包。
4.4 绘图卡顿与界面假死
绘图卡顿通常不是QCustomPlot本身的问题,而是刷新频率和数据量控制得不好。曲线数据点到几万的时候,一次setData都会很慢。解决方法是限帧(上限20帧)、限点(显示窗口只保留最新N点),或者做下采样。另外replot一定要在主线程调用,数据线程拿到结果后通过signal通知UI,千万别在子线程直接replot,否则Qt会在某些平台上直接崩溃或者界面花掉。
还有一个很隐蔽的坑:QCustomPlot析构时如果开启的Graph很多,且定时器里还在调用replot,可能造成野指针崩溃。退出界面前先停定时器,断开所有关联,再delete对象,顺序不能反。
4.5 高频问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 启动报No Qt platform plugin | platforms目录缺失/插件依赖缺失 | 检查qwindows.dll或libqxcb.so,用qt.conf固定插件路径 |
| 编译时报dependent头文件路径错误 | pro文件使用了他人机器绝对路径 | 改用$$PWD或环境变量,抽pri统一管理 |
| POST报method not supported | 服务器接口不支持POST/网关配置 | 核对URL与接口文档,必要时临时用GET验证 |
| 串口打不开 | 权限不足、COM占用 | Linux加dialout组,Windows释放COM资源 |
| NB-IoT上报无回应 | APN错误、PDN未激活、模组仍在PSM | 查AT+CGPADDR,重新触发CFUN=0/1 |
| 绘图卡顿/崩溃 | 点太多、刷新太快、跨线程replot | 限点限帧,主线程刷新,退出先停定时器 |
最后说点个人体会。这套“中国移动NB-IoT QT采集终端”做下来,技术栈其实不复杂,真正烧时间的全是现场问题:一张SIM卡没开NB-IoT权限、一个APN大小写不对、一个platforms目录漏拷贝,都会让人排查到怀疑人生。我的经验是:项目一开始就做一个“可以说话的终端”,把串口原始日志、AT指令回显、网络注册状态全部显示在界面上,现场问题就变成可见问题;同时,先用最简单的AT指令把NB-IoT链路验证通,再往上叠Qt功能和图表,不然你永远分不清是网络问题还是代码问题。希望这些踩坑记录能帮正在做同类终端的你省几个通宵。