☰
ThingsBoard集成工业组态软件实战:嵌入式组态画面完整方案
2026/10/14 15:38:37 网站建设 项目流程

把ThingsBoard和工业组态软件集成到同一个页面,这事听起来像个“浏览器里多开两个标签页”就能解决的小需求,但真正做起来会发现里面全是细节。

我之前在一个设备监控项目里就撞上了这件事:现场操作员要在ThingsBoard上看设备遥测和告警,又要切到工控机上用组态软件看工艺流程,两个系统各管一摊,数据还经常对不上。领导给的需求很直接:“能不能在一个屏幕上把两样都看了?”于是我花了将近两周时间,专门折腾如何在ThingsBoard中集成工业组态软件,期间踩了不少坑,也沉淀了一套相对成熟的方案。

这篇文章会把我的完整思路、技术选型、实操步骤和踩过的坑一次写清楚。适合刚接触ThingsBoard、想把第三方Web组态画面嵌入Dashboard的人,也适合正在做工业数据平台建设、需要打通SCADA和IoT平台的朋友。文章里不会涉及某个特定商业产品的绑定操作,原理和方案是通用的,你可以直接套用在自己的项目里。

1. 先把集成这事想明白:组态为什么要“住进”IoT平台

1.1 两类系统的天然分工

ThingsBoard本质上是一个物联网平台,它擅长的是设备接入、数据存储、规则引擎、告警管理和多租户权限控制。你在ThingsBoard上做得最多的事是“把设备数据接进来,然后通过Dashboard图表展示”。

工业组态软件就不一样了,它的核心是工艺流程的可视化:水箱、阀门、管道、电机、温度变送器,这些现场设备的图形化表达是它的看家本领。组态画面里会有液位动画、阀门颜色变化、泵的启停状态闪烁,这些都是ThingsBoard自带图表很难替代的。

这两类系统能力高度互补,但现实情况是它们往往部署在两台不同的机器上:一台是跑在工控机里的传统组态软件,一台是跑在服务器上的ThingsBoard。现场操作员在ThingsBoard上看到告警之后,还得跑到工控机前看组态画面确认故障位置,效率很低。

1.2 真实项目里最常见的五种集成诉求

不是所有项目都需要把组态软件搬进ThingsBoard,但从我接触到的实际需求来看,以下五类诉求出现频率最高:

需求场景业务痛点集成思路
统一监控门户操作员要在多个系统间来回切换,容易漏看告警把组态画面嵌入ThingsBoard Dashboard,做成“单屏监视”
远程工艺查看专家在外地,看不到现场工艺流程界面通过ThingsBoard的用户体系授权,远程浏览器打开组态画面
告警联动组态画面里的设备状态与平台告警相互独立让组态画面读取ThingsBoard遥测,告警状态直接在画面里高亮
历史回放工艺画面只显示当前状态,无法回溯历史组态画面按时间轴读取历史遥测数据并回放动画
移动端查看领导在外面想用手机看现场把Web组态画面嵌入移动端页面,后端复用ThingsBoard接口

这五类需求里,第一类和第二类是最常见的,也是本文要重点解决的场景。其它三类可以看作集成完成之后的进阶玩法,后面会简单聊到扩展思路。

1.3 三条可选的集成路线

基于我实际对比过的方案,集成工业组态软件到ThingsBoard通常有三条路线:

  • 重集成:用iframe把整个组态系统门户嵌入ThingsBoard,画面、菜单、配置页面全都带进来。优点是省事,缺点是体验割裂,ThingsBoard的权限体系很难渗透到组态系统里。

  • 轻集成:只在ThingsBoard的Dashboard上通过自定义Widget嵌入单个组态画面,组态系统的管理功能仍然独立。这是我推荐的做法,集成度适中,两边边界清晰,出了问题也好定位。

  • 数据级集成:放弃界面嵌入,只做数据打通。组态软件直接读取ThingsBoard的遥测数据接口,或者ThingsBoard通过规则链调用组态软件的业务接口。这种方式最灵活,但开发量也是最大的。

做选型的时候有个很关键的原则:集成是为了让使用者少切换系统,不是为了让系统变成一个巨大的怪物。所以我一般建议先做“轻集成”,跑通之后再按需加深。

2. 技术原理与集成路线:先摸清ThingsBoard的“接口地形”

2.1 自定义Widget是唯一的入口吗

ThingsBoard的界面基于Angular框架,但好消息是它提供了一套“自定义Widget”机制,允许使用者用纯HTML、CSS和JavaScript写自己的组件,不需要深入Angular内部。

Widget在ThingsBoard里的角色很像一个个积木块。平台自带了很多图表类Widget:折线图、柱状图、表格、实时数值等等。但这些Widget都是基于“数据键”做渲染的,画不出工业现场那种带管道、阀门、泵的工艺画面。

要集成组态软件,核心思路就是写一个“高级Widget”,在这个Widget里放一个iframe或者web component,把组态画面加载进来。ThingsBoard会把这个Widget当成普通的Dashboard组件来管理,缩放、全屏、多设备绑定这些平台能力都能复用。

2.2 组态Web化:不是所有组态软件都能被直接嵌入

这个前提很重要。传统工业组态软件大多是C/S架构,跑在Windows工控机上,主程序是独立的exe,虽然视觉效果专业,但它们没法被浏览器iframe直接加载。

我在项目里遇到的情况是现场已经在用一款老牌C/S组态软件,按理说要从头换成Web组态,成本很高。后来发现那款组态软件自带Web发布功能,可以把画面导出成网页格式,再由IIS发布出来。这样就能得到一个可访问的URL,嵌入ThingsBoard才有了基础。

如果你的组态软件完全没有Web化能力,那集成思路就得换一个方向:通过网络采集服务,把组态画面截图或者视频流推送给ThingsBoard,但这只是“看得到画面”,没法交互,体验差很多。

所以选型阶段一定要确认一个前提:组态软件必须能输出浏览器可访问的Web页面。现在市面上所有主流的Web组态软件,或者传统组态软件的Web发布功能,都能满足这个条件。

2.3 数据通道设计:REST轮询还是WebSocket推送

组态画面与ThingsBoard的数据交互,是整个集成方案里最需要动脑筋的部分。组态画面里的液位、阀门状态、电机启停,本质上是ThingsBoard上设备的遥测(telemetry)数据。

我梳理过四种常见的数据通道:

数据通道实时性开发成本适用场景
组态软件直接连现场设备高低组态与ThingsBoard数据各自独立,不推荐
组态页面轮询ThingsBoard REST API中低简单画面、低频数据、刚起步的集成项目
组态页面通过WebSocket订阅ThingsBoard推送高中实时性要求高、需要即时告警的画面
自建网关做数据聚合再分发最高高上千点位、多系统、大规模分布式项目

我的建议是从REST轮询开始,先把集成链路跑通,确认真实性无法满足需求时再升级为WebSocket或者自建网关。不要一上来就追求最高端方案,工业项目里稳定可靠比技术炫技更值钱。

2.4 令牌与权限体系:谁有资格看到组态画面

ThingsBoard所有API都要求携带JWT令牌,Dashboard通过当前登录用户的身份获取数据。这意味着组态画面如果要从ThingsBoard拉数据,也必须带上合法的令牌。

这里有几个层级可以处理:

  • 最简单的方式:组态URL后面拼上设备ID和长期令牌,画面自己从ThingsBoard接口拉数据。缺点是令牌一旦泄露,别人也能拿到设备数据。

  • 相对安全的方式:ThingsBoard自定义Widget在运行时可以拿到当前用户的上下文信息,动态生成一段短期令牌传给iframe里的组态画面,过期后由Widget负责刷新。

  • 更安全的方式:组态画面不走ThingsBoard接口,而是通过一个自建的网关服务读取数据,网关负责鉴权和令牌管理。网关方案适合对安全要求较高、组态画面对外公开的场景。

权限体系的设计直接决定整个系统的安全边界,这里先立一个原则:组态画面里的数据权限不能超过当前登录用户在ThingsBoard里的数据权限。

3. 手把手集成实操:以一个水处理车间的组态画面为例

3.1 环境准备:ThingsBoard实例和一组模拟设备

我在本地环境用的是Docker方式部署的ThingsBoard社区版,版本为3.x。先创建一个名为“水处理车间”的设备,记录下设备令牌。接着用Python脚本模拟一个水箱设备的上报数据,协议使用MQTT:

import paho.mqtt.client as mqtt import time import json import random broker = "localhost" port = 1883 device_token = "WATER_TREATMENT_TOKEN" client = mqtt.Client() client.username_pw_set(device_token) client.connect(broker, port, 60) client.loop_start() while True: telemetry = { "level": round(random.uniform(0.2, 0.9), 2), "valveOpen": random.choice([True, False]), "pumpRunning": random.choice([True, False]) } client.publish("v1/devices/me/telemetry", json.dumps(telemetry)) time.sleep(3)

运行脚本之后,在ThingsBoard的设备详情页能看到最新遥测数据持续更新,说明数据链路是通的。这里有一个细节:脚本发布的主题是ThingsBoard规定的MQTT遥测主题,用户名位置放的是设备令牌,密码可以省略,这是ThingsBoard MQTT接入的标准方式。

3.2 准备好一个可被浏览器访问的Web组态页面

因为重点是讲“集成”而不是“组态开发”,我这里用一个最小可用的SVG模拟画面来演示。如果你项目里已经有成熟的Web组态软件,直接把发布后的URL替换进来就行。

<svg width="300" height="300" id="tank"> <rect x="60" y="80" width="120" height="150" fill="none" stroke="black"/> <rect id="water" x="60" y="200" width="120" height="30" fill="#63b3ed"/> <text x="130" y="250" id="levelText">0%</text> <circle id="valve" cx="180" cy="230" r="15" fill="gray"/> </svg> <script> async function loadData() { const res = await fetch("/api/telemetry?deviceId=" + getDeviceId(), { headers: {"Authorization": "Bearer " + getToken()} }); const data = await res.json(); const level = parseFloat(data.level) * 100; document.getElementById("water").setAttribute("height", level * 1.5); document.getElementById("water").setAttribute("y", 230 - level * 1.5); document.getElementById("levelText").textContent = level.toFixed(1) + "%"; document.getElementById("valve").setAttribute("fill", data.valveOpen ? "#38a169" : "#e53e3e"); } setInterval(loadData, 3000); loadData(); </script>

这段代码的思路是:组态页面每隔3秒请求一次后端接口,然后通过修改SVG图元属性来刷新液位高度和阀门颜色。实际项目里,Web组态软件都有数据绑定机制,把变量绑定到图元属性上,效果和这段逻辑是一样的。

部署时我用Nginx托管这个页面,保证浏览器能通过http://scada-web/scada/screen1.html访问。注意这一步不要跳过,后面嵌入iframe时需要这个实际可访问的URL。

3.3 编写ThingsBoard自定义Widget,把组态画面嵌进Dashboard

这是整个集成方案最核心的步骤。在ThingsBoard的Dashboard编辑界面,添加一个Widget,选择“高级Widget”类型,然后用HTML和JavaScript定义Widget内容。

Widget的HTML部分:

<div id="scada-container" style="width:100%;height:100%;"> <iframe id="scadaFrame" style="width:100%;height:100%;border:none;"></iframe> </div>

Widget的JavaScript部分:

self.onInit = function() { var scadaUrl = self.ctx.settings.scadaUrl; var entityId = self.ctx.entityId; var token = self.ctx.securityTokens ? self.ctx.securityTokens.JWT_TOKEN : ""; var frame = document.getElementById("scadaFrame"); frame.src = scadaUrl + "?deviceId=" + encodeURIComponent(entityId) + "&token=" + encodeURIComponent(token); };

这里几个关键点:

  • self.ctx.settings.scadaUrl是Widget配置项里填写的组态页面地址,这样同一个Widget模板可以复用到不同画面的嵌入场景。

  • self.ctx.entityId是当前Dashboard绑定的设备ID,组态页面拿到它之后才能按设备拉取对应的遥测数据。

  • self.ctx.securityTokens在不同版本的ThingsBoard里属性名略有差异,如果拿不到就打印一下self.ctx上下文,看看当前环境里实际的令牌字段是什么。这是新手最容易卡住的地方。

Widget需要配置一个数据源,我把“水处理车间”设备绑定到这个Widget上,这样仪表盘切换设备时,组态画面也会跟着切换对应设备的画面状态。

3.4 Dashboard权限与多角色视角的验证

Widget开发完成后,我把Dashboard分别授权给了系统管理员、租户管理员和一个新建的客户账号,逐个验证:

  • 系统管理员:能看到所有设备和组态画面,拥有编辑权。

  • 租户管理员:能看到自己租户下的设备和组态画面,不能看到其他租户的数据。

  • 客户账号:在“客户Dashboard”里能看到被分配的设备和组态画面,只能操作授权范围内的数据。

验证的目的是确认iframe里的组态画面不会绕过ThingsBoard的权限体系。如果你的组态画面里能显示当前用户无权访问的设备数据,说明令牌或者数据接口的鉴权逻辑有问题,需要立刻修复。

4. 让画面“动”起来:双向数据通信的实现细节

4.1 遥测数据的正向注入

组态画面里的液位、温度、阀门状态,数据源头是设备上报给ThingsBoard的遥测。When组态页面拿到deviceId和token之后,通过ThingsBoard的REST接口读取遥测:

GET /api/plugins/telemetry/DEVICE/{deviceId}/values/timeseries?keys=level,valveOpen,pumpRunning

请求头里带上Authorization: Bearer {token},返回结果是一个JSON对象,每个key对应一个数组,数组里包含时间戳和值:

{ "level": [{"ts": 1700000000000, "value": "0.652"}], "valveOpen": [{"ts": 1700000000000, "value": "true"}], "pumpRunning": [{"ts": 1700000000000, "value": "false"}] }

组态页面解析这段JSON,把数值映射到对应的图元上,就实现了画面动态刷新。这里我习惯用3秒的轮询周期,工业工艺画面的液位变化一般不会快过这个频率,三秒刷新人眼看起来也足够流畅。如果数据变化极快,再考虑缩短到1秒,但不要低于1秒,否则会对ThingsBoard产生不必要的请求压力。

4.2 控制指令的反向下发

组态画面不仅是“看”,还要能“操作”。比如画面上放一个“开阀”按钮,点下去之后要通过ThingsBoard把指令下发到设备。这里不建议组态页面直接连设备,建议统一走ThingsBoard的RPC能力:

POST /api/rpc/twoway/{deviceId}

请求体:

{ "method": "setValve", "params": {"open": true} }

设备端通过MQTT订阅RPC请求来处理这条指令:

import paho.mqtt.client as mqtt import json def on_message(client, userdata, msg): if "v1/devices/me/rpc/request" in msg.topic: request = json.loads(msg.payload) request_id = request.get("id") method = request.get("method") params = request.get("params", {}) if method == "setValve": # 执行阀门控制逻辑 actual_open = params.get("open", False) response = {"success": True, "open": actual_open} client.publish("v1/devices/me/rpc/response/" + str(request_id), json.dumps(response)) client = mqtt.Client() client.username_pw_set("WATER_TREATMENT_TOKEN") client.connect("localhost", 1883, 60) client.subscribe("v1/devices/me/rpc/request/+") client.on_message = on_message client.loop_forever()

之所以要走RPC而不是让组态页面直接发设备指令,是为了让所有控制操作都经过ThingsBoard的规则链,记录操作历史,方便日后追溯审计。工业场景下“谁在什么时候操作了什么设备”,这个审计需求几乎是刚需。

4.3 数据频率与实时性的平衡

集成过程中很容易犯一个错误:把所有数据点都用同一个刷新频率去拉。液位变化慢,3秒刷新合理;但水泵电流可能变化很快,3秒刷新就会错过峰值;而有些仪表数据一天变化不了几次,频繁拉取纯属浪费。

实际项目中我一般把数据分成三类:

数据类型刷新策略示例
实时变化量(高频)1-2秒轮询或WebSocket推送电流、压力、流量
缓慢变化量(中频)3-5秒轮询液位、温度、阀门开度
状态量事件触发时刷新泵启停、报警状态

分类之后,组态页面针对不同数据键设置不同的刷新间隔,既保证关键数据的实时性,又避免无谓的接口压力。这个优化在点位超过几百个时效果非常明显。

5. 踩坑实录:跨域、令牌、断线与性能问题

5.1 iframe嵌入的跨域与安全策略冲突

第一个大坑是iframe加载空白。组态页面部署在独立的Nginx上,ThingsBoard在另一个域名,浏览器会因为跨域限制或者安全策略拒绝加载。

常见原因有两个:一是组态页面所在服务返回了X-Frame-Options: DENY或者SAMEORIGIN,导致其他域名的页面无法iframe加载它;二是服务端CSP(内容安全策略)里没有允许ThingsBoard域名的frame-ancestors。

排查方式很简单:在ThingsBoard页面按F12打开开发者工具,看Console和Network里的报错信息,浏览器会明确告诉你是不是被X-Frame-Options拦截了。

如果是自己的组态服务,就在Nginx配置里调整:

server { listen 80; server_name scada-web; add_header X-Frame-Options "ALLOW-FROM https://tb.example.com" always; add_header Content-Security-Policy "frame-ancestors https://tb.example.com" always; }

注意ALLOW-FROM在一些现代浏览器里已不再支持,CSP的frame-ancestors是更通用的做法。如果组态服务不是自己的,就得找组态厂商确认如何解除iframe限制。

5.2 JWT令牌过期问题与静默刷新方案

ThingsBoard的JWT令牌有时效性,默认一般是一小时。我在大屏上挂了一个组态Dashboard,运行一个多小时后,画面突然开始报401错误,数据全停了。

原因是iframe里的组态页面拿着Widget初始化时传入的令牌在请求数据,令牌过期之后没有刷新机制。解决思路有两个方向:

第一个方向是Widget内部定期刷新iframe地址。Widget本身在ThingsBoard页面里,它的上下文会跟随用户登录状态保持有效。我在Widget里加了一个定时器,每50分钟重新读取一次securityTokens,然后更新iframe的src,让组态页面拿到新令牌重新加载。

第二个方向是不把令牌直接传给组态前端,而是在自建网关里做令牌缓存和转发。组态页面请求网关,网关用自己的长时凭证去访问ThingsBoard接口,这样组态页面不需要感知令牌的过期时间。这个方案更安全,但需要多维护一个服务。

如果你的项目要求组态页面长期挂在电视大屏上,强烈建议优先考虑网关方案。大屏没有人工干预,令牌一过期画面就全白,运维会很痛苦。

5.3 大屏场景下的断线重连与页面卡死

大屏场景还有一类问题是“画面看起来活着,实际上数据已经不动了”。最常见的诱因是浏览器标签页进入后台后,JavaScript定时器被浏览器节流,间隔被拉长到几分钟甚至暂停执行,组态数据刷新就停了。

另外一个诱因是WebSocket连接断开后没有自动重连,数据通道断了但页面没有报错。

我的处理经验是在组态页面里加一套“心跳探活”机制:组态页面每隔10秒通过postMessage向父页面报告“我还活着”;父页面的Widget监听消息,如果连续三次没有收到心跳,就直接调用location.reload()强制刷新iframe。这套机制虽然粗暴,但在大屏这种无人值守场景里非常实用。

同时监听浏览器的visibilitychange事件,当页面从后台切回前台时,立刻触发一次全量数据刷新,避免用户重新看向大屏时看到的还是几分钟前的旧数据。

5.4 上千点位时的性能优化思路

组态画面点位超过一千个之后,性能和并发压力会成为新的瓶颈。每个点位每3秒一次REST请求,ThingsBoard要处理几百个并发请求,轻则响应变慢,重则拖垮同租户下的其他设备。

我踩过这个坑之后做了几件事:

第一,在ThingsBoard侧用规则链把数据做聚合,组态画面只读取“实时快照”接口,而不是逐点拉历史数据。

第二,在组态服务与ThingsBoard之间加一层缓存。我用ZeroMQ做了一个轻量级网关,ThingsBoard的消息通过订阅发送到网关,网关把最新点位缓存在内存里,组态画面读网关而不是直接打ThingsBoard接口。

第三,高频数据边缘化。高频变化的数据点(比如振动这类毫秒级数据)不在组态画面里展示,如果需要看,单独做趋势曲线,低频拉取历史数据。

做完这三步优化之后,就算点位扩展到几千个,组态画面依然能保持流畅。

6. 集成完成之后,这套方案还能往哪走

如果你已经按上面的方法跑通了第一个组态画面,那接下来有几件事是我个人觉得值得继续投入精力的方向。

第一,把组态Widget扩展成一套模板。实际上不同车间的组态画面结构完全一样,只是绑定的设备不同。我可以做一个“通用组态容器Widget”,把组态URL和数据点映射做成配置项,以后接新设备的时候只需要复制Widget、改配置,不用再改任何代码。

第二,把告警信息叠加到组态画面上。让ThingsBoard的告警severity直接映射成画面的颜色闪烁效果,设备告警的同时,工艺流程画面里对应图元会闪烁,操作员一眼就能看清故障点在哪里。这个联动算是性价比很高的增强功能。

第三,把组态画面做成移动端可访问。ThingsBoard自带响应式布局,Widget在手机上可以自动全屏显示,组态画面如果在设计时就兼顾小屏适配,就能直接支持移动端远程查看。这对于工厂管理者来说很实用。

最后从代维角度说一句:集成之后要特别关注ThingsBoard版本升级带来的Widget API变化。我在一次升级后遇到过self.ctx.securityTokens这个字段被移除的情况,幸好当时组态页面用了网关方案,令牌问题没有影响线上运行,但要是在升级前没有做好兼容测试,画面就会全部停摆。所以给组态集成单独写一份运维手册,标注清楚Widget依赖了哪些上下文属性、令牌刷新机制、以及升级前需要做的验证清单,这事值得花半天时间认真做。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询