简介:这是一份围绕开源SCADA组态软件pvbrowser的技术资源。pvbrowser基于QT框架开发,主要用于工业自动化、能源管理、楼宇监控等场景的实时数据采集与远程控制,面向有一定基础的自动化工程师、嵌入式开发者以及对SCADA系统感兴趣的学习者,可帮助理解开源组态软件的核心架构与落地方法。\n\n资源包约27.65MB,具体内部文件清单暂未上传,但内容聚焦pvbrowser的全面介绍:从SCADA系统基础组件,到QT跨平台界面开发,再到OPC、MODBUS、TCP/IP等设备通信协议,以及脚本支持、历史数据记录和图形化组态功能。读者可以借此梳理SCADA系统的设计思路,掌握pvbrowser在Windows、Linux等多平台上的部署与使用要点。\n\n该资源目前已有2663人学习下载。对于想快速上手开源SCADA方案、研究QT工业界面开发或搭建监控实验环境的人来说,这是一份兼具概览与细节的技术资料。
1. 一个不用写前端代码就能出监控界面的 SCADA 框架
很多做工业监控的团队在选型时容易被商业组态软件卡住,pvbrowser 这套基于 Qt 的 SCADA 组态软件走的是完全开源的路子,它像一套三件套:pvdevelop 负责拖界面、pvserver 负责跑数据和逻辑、pvbrowser 负责把画面显示出来。最反直觉的一点是,画面文件不用编译进客户端,运行期由服务器动态下发,改一张监控图,用户端下次刷新就能看到,不需要停机升级。它适合要搭 HMI 看板、设备数据大屏,又不想被商业授权绑死的团队,也适合想研究组态软件内部机制的开发者,至少能让你看清“编辑器、服务器、浏览器”是怎么在一个项目里协同工作的。
2. 架构拆解:三件套怎么分工,数据走完一条完整链路
pvbrowser 的核心思路并不神秘,它就是老派组态软件里常见的“编译时画图、运行时看图”模式,但实现上彻底拆成了独立进程。拆开以后,单看任何一块都不复杂,合起来才是一个完整的 SCADA 系统。
2.1 三件套各自做什么
先认识这套项目里的三个成员。
pvdevelop 是组态编辑器,它长得很像 Qt Designer,左侧是控件树,中间是画布,右侧是属性面板。SCADA 场景里常用的圆形仪表、趋势曲线、报警闪烁灯、开关按钮都能直接拖上去。编辑完成的画面会生成一份基于 XML 的描述文件,这个文件最终要交给运行时去渲染,它本身不参与实际的数据采集。
pvserver 是真正的后台服务,负责连接 PLC、读取数据库、跑控制逻辑、维护标签表,并处理来自客户端的连接请求。所有“值从哪里来”“什么时候更新”“报警怎么判断”这类业务逻辑都写在这边,它相当于组态软件的发动机。
pvbrowser 是瘦客户端,只管把服务器下发的画面描述文件解析出来,渲染成窗口,再把实时标签值作为属性刷新到控件上。它自己不做数据采集,也不保存工程文件,因此客户端可以随时从服务器拉取最新的画面。
把三者互相之间的关系记成一句话就是:pvdevelop 负责生产画面,pvserver 负责生产数据,pvbrowser 负责把两者合并成用户看到的东西。
2.2 和商业组态软件的架构差异
商业 SCADA 软件为了易用,通常把编辑器和运行环境装到同一台机器上,工程文件也保存在本机,画面开发完以后直接编译成一个独立运行包发给现场,改动一点就要重新编译发布。
pvbrowser 走的路线不太一样,它更像是“服务器集中管理、客户端按需拉取”。画面描述文件被放到服务器端,客户端连接进来的时候,向服务器请求指定画面,服务器把 XML 文本直接通过网络发过去,客户端用 Qt 的加载机制在内存里动态生成控件树。
这种架构带来的直接好处有几点。第一,现场客户端永远不需要重装,服务器替换画面文件,客户端下一轮刷新自动生效。第二,多个客户端看到的是同一份画面,不会出现“现场改了一版、办公室还是旧版”的版本漂移。第三,瘦客户端对硬件要求低,一台老旧工控机也能当显示终端。
代价也很明显:服务器一旦挂了,所有客户端都白屏,所以生产环境必须给 pvserver 配守护进程,这一点后面会单独讲。
2.3 数据从采集到控件刷新的完整链路
为了说清楚这套软件的数据流,把链路拆成六步,理解之后再去读代码会顺很多。
第一步,pvserver 内部维护一张标签表,标签名对应一个点位,比如温度 TAG_TEMP_01。第二步,设备驱动线程按配置的周期轮询 PLC 或者数据库,把新值写进标签表。第三步,服务器检测到某个标签值发生变化,把变化通知放入待发送队列。第四步,pvbrowser 客户端和 pvserver 之间维持一条长连接,客户端告诉服务器“我当前打开的是哪个画面”。第五步,服务器于是将画面描述文本和对应标签的实时值打包推给客户端。第六步,客户端解析文本,创建控件实例,再把标签值映射到控件属性上。
这里最理想的实现模式是“标签只管数据、控件只管显示”,数据怎么采集对控件不可见,控件怎么画对数据源不可见。实际代码里虽然没做到彻底解耦,但它确实把标签名作为了连接点,这也是二次开发时最容易卡住的地方,改错一个名字就刷不出值。
为了让你对这份工程有一个直观印象,贴一个项目里常见的顶层目录布局,大致长这样,不同分支会略有出入:
pvserver/ src/ main.cpp pvserver.cpp tagengine.cpp sql/ create_tables.sql pvdevelop/ src/ mainwindow.cpp widgetfactory.cpp pvbrowser/ src/ main.cpp clientwidget.cpp examples/ demo/pvserver 目录集中放服务端逻辑,pvdevelop 目录放编辑器,pvbrowser 目录放客户端,examples 目录是现成的 Demo 工程。第一次读代码,建议从 pvserver/src/main.cpp 读起,把服务端主循环看懂,再看客户端clientwidget.cpp里的渲染入口,这个阅读顺序最省力。
3. 编译与首次启动:30 分钟把 Demo 画面跑在本机
很多开源 SCADA 项目让人劝退的第一个坎就是编译。pvbrowser 是老项目出身,代码里有不少历史包袱,但只要把工具链版本选对,编译过程其实相当常规,就是标准的 qmake 工程。
3.1 依赖工具链准备
先在 Linux 发行版上装依赖。以 Debian/Ubuntu 系为例,打开终端执行:
sudo apt install build-essential qtbase5-dev qt5-qmake qtchooser libqt5svg5-devbuild-essential 提供 g++ 和 make,qtbase5-dev 是 Qt5 核心开发包,qt5-qmake 提供构建工具,qtchooser 是用来切换 Qt 版本的小工具,libqt5svg5-dev 用于显示 SVG 格式的图元,监控画面里某些符号资源会用到。
需要注意,pvbrowser 的历史分支里有相当一部分是基于 Qt4 写的,直接用 Qt6 去编几乎必然报错。如果系统默认 qmake 指向的是 Qt6,可以用qtchooser -qt=5 qmake强制定到 Qt5。经验是在这个项目上 Qt5.15 是最省心的选择,Qt6 的 Q_OBJECT 宏和 moc 处理方式变动较大。
3.2 从源码到三个可执行文件
源码放到本地后,进入顶层目录,按顺序执行:
cd pvbrowser qmake make -j4第一行 qmake 会根据工程文件生成 Makefile,第二行 make -j4 用四个线程并行编译。如果机器内存有限,建议改成make -j2,否则并行度太高容易在链接阶段内存不足。
编译结束以后,在子目录里会生成三个可执行文件,确认它们存在即可:
find . -name pvserver -type f find . -name pvbrowser -type f find . -name pvdevelop -type f如果这三个文件都在,说明这一步已经过了。有些人编译过程中会遇到c++: fatal error: killed signal terminated这种错误,多半是内存不足导致编译进程被杀,把-j4调成-j1,或者临时扩大 Swap 都能解决。
3.3 启动 pvserver 并连接客户端
先准备一个存放数据库文件的目录,再启动服务,然后打开客户端:
mkdir -p ~/pvs/db pvserver -d ~/pvs/db/demo.db -p 5700 & pvbrowser localhost:5700-d参数指定数据库文件,pvbrowser 的标签历史数据默认走 SQLite,也可以按编译选项换成 MySQL 驱动。-p参数指定监听端口,这里用 5700,只是一个示例,现场用哪个端口自己约定就行。
pvbrowser localhost:5700是客户端连接命令,第一个参数是服务器地址,第二个是端口,中间用冒号隔开。如果能看到一个空白主窗口,说明客户端和服务端已经连通了。
3.4 用 pvdevelop 拖一个最小画面
进入 pvdevelop,新建工程,然后从控件面板拖一个圆形仪表放到画布上。在属性栏里找到“标签绑定”之类的一项,填上TAG_TEMP_01作为点位名,保存工程。
关键点在于理解这里保存的文件是什么。pvdevelop 保存的不是可执行程序,而是一份 XML 画面描述,里面记录了控件的类型、坐标、大小、绑定的标签名。这份文件最终会放到 pvserver 能访问的目录下,等待客户端来取。
把画面文件放到服务器端以后,在客户端里重新加载画面,如果一切正常,画面上会出现一个仪表盘,数值区域显示 0。到这一步,一个最小可运行的 Demo 环境就搭起来了,接下来才是真正有意思的部分,看代码是怎么把画面文本变成窗口控件、又是怎么实时刷数据的。
4. 核心代码拆解:一次画面加载背后发生了什么
读完架构再读代码,会清晰很多。这里挑三段最值得跟读的实现,用概念上贴近项目风格的代码片段来做讲解,能理解这段链路,二次开发就成功了一半。
4.1 pvserver 主入口:监听与连接处理
服务端的入口做的事情非常直白,初始化一个监听套接字,然后把新连接交派给处理函数。代码思路如下:
// pvserver 主入口,简化版实现 int main(int argc, char *argv[]) { QApplication app(argc, argv); PvServer server; server.setupDatabase(argv[1]); // 初始化标签表和历史库 quint16 port = 5700; server.listen(QHostAddress::Any, port); // 监听所有网卡的 5700 端口 QObject::connect(&server, &PvServer::newClient, &server, &PvServer::handleClient); return app.exec(); // 进入 Qt 事件循环 }把监听放到主线程这件事,在入门阶段是合理的,连接量不大,单线程够用。setupDatabase负责把标签定义表载入内存,这样后面客户端问起来的时候,不用反复查库。listen的第二个参数就是端口号,现场要改端口,改这一处即可。newClient信号在 accept 到一个新连接时触发,handleClient是核心分发函数,后续所有画面请求都从它进入。
4.2 画面文件分发:XML 文本直接下发给客户端
客户端连上以后,会发来一个画面对应的关键字。服务端收到请求,把自己目录下对应的 XML 画面描述读成文本,原封不动发给客户端,代码示意如下:
void PvServer::handleClient(ClientConnection *conn) { QString screenKey = conn->readRequest(); // 客户端要求的画面名 QByteArray xmlData = loadScreenFile(screenKey); // 从约定的画面目录读取 conn->write(xmlData); // 直接把 XML 文本推给客户端 }这段代码最值得品的是loadScreenFile和write的分离。服务端根本不关心这串 XML 里是什么控件,它只负责把文本文件读出来再送出去,渲染的事交给客户端。这也解释了为什么修改画面不需要重新编译,因为改的只是服务器目录下的文本文件。当然,这里隐藏着一个隐患,就是画面目录的读权限。如果 pvserver 进程用户对画面文件只有执行权限没有读权限,客户端拿到的是空文本,画面自然是白屏。
4.3 客户端渲染:从 XML 到控件实例的映射
pvbrowser 拿到 XML 以后,用 Qt 的加载器把它动态解析成控件树,再扫描出绑定标签,创建标签到控件属性的映射。示意代码如下:
void PvBrowser::loadScreen(const QByteArray &xmlData) { QUiLoader loader; // Qt 自带的 UI 动态加载器 QWidget *screen = loader.load(xmlData); // 解析 XML,生成控件对象树 QMap<QString, QWidget*> tagMap; for (QWidget *w : screen->findChildren<QWidget *>()) { QString tag = w->property("pvTag").toString(); if (!tag.isEmpty()) { tagMap[tag] = w; // 每个控件绑一个点位名 } } m_tagWidgetMap = tagMap; // 备用,后面刷新数据用 }QUiLoader是 Qt 里少有的“不写代码就能生成界面”的组件,pvbrowser 的核心渲染机制离不开它。pvTag是控件属性里自定义的一个键,pvdevelop 保存画面时,会把你在属性栏里填的标签名写进这个键里,运行时再由客户端读出来建映射。
到这里你会发现,整个系统最关键的约定就是标签名。数据采集那一端把值塞进标签表,渲染这一端从标签表取名字去匹配控件属性,任何一个环节改名字都会导致值刷不出来。
再看实时刷新的处理函数,基本是一个标准的事件驱动回调:
void PvBrowser::onTagValueChanged(const QString &tag, double value) { QWidget *w = m_tagWidgetMap.value(tag); if (w) { w->setProperty("displayValue", value); // 把新值写入控件属性 w->update(); // 触发重绘,界面立即变化 } }setProperty是 Qt 给控件动态塞数据的方式,触发的update()会让控件在下一帧重画自己。这种做法的好处是数据线程和绘制线程之间只传递数值,不传递控件指针,避免死锁。现场如果觉得刷新不平滑,通常不是这段代码的问题,而是服务器端数据采集周期设得太长。
5. 避坑与排查:把开源组态跑起来要过的几道坎
编译过不代表能跑好。这类跨平台、多进程、依赖第三方库的开源组态软件,最容易在环境差异和细节约定上让人翻车。下面五条是按踩坑频率筛出来的,每一条都按“现象 → 原因 → 解决”来记录。
5.1 Qt 版本不对,编译到一半就死
现象:make 跑到大约三分之一的位置,突然冒出一堆moc: undefined reference或者QImageReader: No such file之类的错误。
原因:这台机器上默认的 qmake 指向 Qt6,而 pvbrowser 的源码大量使用 Qt5 时代才有的 API 和头文件路径,尤其是 SVG 渲染和 UI 加载相关的模块,在 Qt6 里被拆掉了或者改了名。
解决:先跑qmake -v看当前版本。如果不是 Qt5,用qtchooser -qt=5 qmake强制指定 Qt5,再重新make clean后编译。装依赖的时候把qt5-qmake和qtchooser一起装上,就是为了这一步。
5.2 画面文件下发失败,白屏无提示
现象:pvbrowser 能连上服务器,窗口打开了,但中间一片空白,日志里没有任何报错。
原因:最常见的是文件名大小写对不上,或者 pvserver 用户对画面目录只有执行权限、没有读权限。服务器端把空文本当成合法文件发给客户端,客户端解析出来自然什么都没有。
解决:先去 pvserver 所在机器上手动读一下画面文件,确认内容非空。然后检查启动 pvserver 的用户对目录权限是不是至少有r-x,路径别带空格,也别用中文目录。最后核对客户端请求的画面名和服务端实际文件名是否完全一致,包括大小写。
5.3 标签值一动不动,控件事先绑定了也没反应
现象:画面出来了,仪表也显示出来了,但数值永远是 0,就算数据源那边已经变了也没反应。
原因:九成是标签名对不上。pvdevelop 里绑定的名字和 pvserver 标签表里注册的名字不是一个,pvserver 每次广播标签新值时,客户端在映射表里找不到对应的控件,自然就丢掉了。
解决:先把标签表的注册名导出来,确认数据源配置里的标签名和 UI 绑定名完全一致,一个字符都不能差。调试阶段建议先用 pvserver 自带的模拟数据源,整链路通了再接真设备,这样能把“数据源问题”和“渲染问题”隔离开。
5.4 端口看起来没通,但 ping 也能通
现象:客户端连服务器,一直转圈,超时后报连接失败。ping 服务器 IP 是通的,防火墙也关了,就是连不上。
原因:有些云主机和工控机用的是内网网段,但网卡策略里只放开了 80/443 和远程桌面端口,自定义端口 5700 被挡在外网防火墙,从内网测没有问题,从客户端所在网段测就被静默丢弃了。
解决:先用telnet 服务器IP 5700试一下端口通不通,不通就检查中间每一层防火墙。工控现场特别容易出现“双网卡但路由表没写好”的情况,服务器监听在了Any上好办,但客户端网段不能到达服务器网段,还是白搭。
5.5 服务器重启后,客户端没有任何重连
现象:现场断电重启,pvserver 拉起来了,但已经打开的 pvbrowser 还停在断线前的画面,数据一动不动,必须手动刷新页面才能恢复。
原因:旧版客户端没有自动重连机制,连接断开以后客户端线程就进入等待状态,不会主动重试握手。这个问题在多客户端现场尤其明显,每次重启服务器都要一台一台去点刷新。
解决:给 pvserver 配一个守护脚本,检测进程退出就立刻拉起。客户端侧把超时时间调短,这样网络闪断后至少能感知到失败。如果改代码方便,可以在客户端底层增加一个定时器循环,发现连接断开就重新走一次握手流程。
6. 二次开发:用私有协议接管标签数据源
做组态软件二次开发,最常见的一个需求是接私有协议设备。pvbrowser 自带的通用驱动未必认识你自己定义的寄存器说明文档,但它的代码结构给你留了自定义驱动的口子。
我的做法是在 pvserver 工程里增加一个新驱动文件,让驱动类继承项目里设备类,重写轮询方法,把读到的值直接写进标签表:
// 自定义驱动:每 2 秒读一次私有设备寄存器 class MyModbusLiteDriver : public PvDevice { Q_OBJECT public: MyModbusLiteDriver(const QString &deviceName, QObject *parent = nullptr) : PvDevice(deviceName, parent) {} protected: void poll() override { double raw = readPrivateRegister(0x10); // 按私有协议读原始值 writeTag("TAG_TEMP_01", raw * 0.1); // 缩放后写入公共标签表 emit tagChanged("TAG_TEMP_01", raw * 0.1); // 触发客户端刷新 } };这里真正需要改三个地方:readPrivateRegister里换成你自己的报文封装和解帧逻辑,writeTag里的标签名换成你工程里定义的点位名,poll的触发周期在构造参数里配好。验证时先别接真设备,写一个循环自增的假数据源跑一夜,确认无内存增长,再换真实设备,这样能把协议问题控制在最小范围内。
从那以后,我每次给这个组态项目接新设备,都会强制走一遍“模拟数据源验证渲染、真设备验证采集、双端联合验证刷新”这三步流程。这个习惯替我挡掉了不少现场调试的熬夜场面,希望帮到你。
本文还有配套的精品资源,点击获取