人机交互接口(HMI)是自动化系统中不可或缺的一部分。传统的做法是提供一个HMI 显示屏,并且通过组态软件来配置显示屏的功能,通过modbus 或者以太网与PLC 连接。
现在,事情变得复杂了许多,用户不仅需要通过专用的HMI 面板操控设备,而且需要使用标准化的设备与设备打交道,比如PC,iPAD 或者智能手机。与此同时,显示屏的性能提升和价格下降,在生产现场和公司管理部门也会安装更多的HMI设备。并且希望显示的内容传统的嵌入式HMI屏更加丰富。
传统的HMI 屏幕的软件通常是使用windows, QT作为技术平台,使用C#或者C语言实现。嵌入式HMI屏提供的组态工具缺乏足够的灵活性,而且局限性比较大。支持开放性协议的HMI 屏并不多见,例如OPC UA ,MQTT 等协议。
有一个发展趋势是使用HTML5 技术来构建开放自动化系统中的HMI 管理系统。本文探讨其中的一些技术细节。
HMI 的运行环境
HMI 管理系统在下列几种平台上运行
- WebServer
基于NodeJS 的Web服务器
- Windows PC HMI应用程序
- 基于Electron 跨平台HMI
能够在Android ,Linux ,Windows 平台上运行的HMI
- 基于嵌入式QT 的HMI
- 基于Go,C++ 等语言实现的web服务器。
轻量级的web 可以使用Golang实现
系统架构
以web 服务器为例
web服务器使用NodeJS 实现,前端UI组件能够使用多种技术实现,为了简单起见,我们使用Jquery /Bootstrap 构建UI组件,也可以使用vue 来构建UI组件。
使用Web socket 协议的原因是因为Web socket 是一种双向协议,而Restful 是Client/Server协议。事件通信需要双向通信。另外,Web socket 能够实现加密。
响应式布局
传统 HMI 的页面设计大多数采用了固定定位的方式实现。但是这种设计方法无法适应多种显示屏。而且要开发一种基于图形化的方式来设计页面的布局并不是一种轻松的工作,需要组态的参数非常多。传统的HMI 通常基于了Windows图形系统实现。灵活性受到限制。而且HMI 页面比较丑陋。比较合适的方式是采用web 页面响应式布局。所谓响应式布局是页面能够根据屏幕的类型自动地调整页面布局。
bootstrap 支持响应式布局。下面是我们构建的一个HMI 的例子。
bootstrap 的栅格技术
Bootstrap包含了一个强大的移动优先的网格系统,它是基于一个12列的布局、有5种响应尺寸(对应不同的屏幕)
- xs:超小屏幕 手机 (<768px)
- sm:小屏幕 平板 (≥768px)
- md:中等屏幕 桌面显示器 (≥992px)
- lg:大屏幕 大桌面显示器 (≥1200px)
bootstrap 将屏幕分成容器,行和列。并且将列分成为12等分。
一个页面包含若干个容器
容器内包含了若干行。
行包含若干列。
与表格不同的是,行内的列并不一定是在一行中排列的。它们从左到右地排列,当超过屏幕时,会自动地转向下一行。因此,在大屏幕上三列的内容,到了中等屏幕可能变成2 列,到了智能手机中,可能便成为一列。这就是所谓的“响应式”设计。
bootstrap的许多技术细节可以参考bootstrap 网站的文档。
组件式HMI
在我们的设计中,希望HMI 的设计与开放自动化系统中的其它信息模型的构建方式相似,这样能够降低系统的学习难度。具体地讲,就是要与OPC UA 信息系统,IEC61499 功能块,工业4.0 AAS 等模型的构建方式一致。在我们的实验系统中,采取了基于组件的设计方法(Component Based Engineering)。因此,我们需要设计一种web 页面组件化的架构。
依据Bootstrap 的格栅方案,我们将HMI 按如下分层架构来构建:
- HMI 的基本组件称为面板(Panel)和UI对象组成。
- HMI 页面由若干面板组成
- 面板是可以嵌套的。
根据面板的嵌套关系对应不同的Bootstrap 类
层级 | Bootstrap 对象 |
第一层面板 | 容器container |
第二层面板 | 列 |
第三层面板 | 行 |
UI对象 | 列 |
从此可见,同样的面板组件,可能是行,也可能是列。它们交替变化。而UIObject 确定是列。 这是一种web 页面组件化的一种简单和有效的方法。可以使用Panel 和UIObject两种主要的组件就能够构建HMI面板。
功能组件与HMI 组件的接口
HMI 是一种信息组件,它们能够与功能组件交互,为了简单起见,功能组件将HMI组件看作为IO资源就可以。而IO服务组件包括:
- HMI_Configuration
- HMI_Write
- HMI_Read
- HMI_Input
HMI 服务器中有一个运行时,支持OPC UA ,接口和IEC61499 功能块应用和一些标准功能组件库。
HMI 模型的模板
HMI 模型设计完成后,可以导出HMI模型,它们能够使用XML,JSON 格式描述,下面是一个JSON 描述的文件:
page={"panels":[{ "id":"panel_1","style":"height:280px;background-color: yellowgreen;color:white","title":"参数","panels":[{ "id":"panel_2", "panels":[{"id":"panels_211","style":"height:60px;background-color: blueviolet;color:white","UIObject":{"id":"UI_11","Name":"Temperature","Type":"Text","Value":"32"} },{"id":"panels_212","style":"height:60px;background-color: blue;color:white","UIObject":{"id":"UI_11","Name":"Voltage","Type":"Text","Value":"43"} },{"id":"panels_213","style":"height:60px;background-color: orange;color:white","UIObject":{"id":"UI_11","Name":"Description","Type":"Text","Value":"64"} }]}]},{ "id":"panel_3","style":"height:280px;background-color: red;color:white","title":"Panel2","panels":[{ "id":"panel_4","UIObject":{"id":"UI_2","Name":"Description","Type":"Text","Value":"HTML2 description"}}]},{ "id":"panel_5","style":"height:280px;background-color: blue;","title":"Panel3","panels":[{"id":"panel_6","UIObject":{"id":"UI_3","Name":"Description","Type":"Text","Value":"HTML3 description"} }]},{ "id":"panel_7","style":"height:280px;background-color: green;","title":"Panel4","panels":[{"id":"panel_8", "UIObject":{"id":"UI_4","Name":"温度阀值","Type":"Input","Value":"HTML4 description"}},]},{ "id":"panel_9","style":"height:280px;background-color:gold;","title":"Panel5","panels":[{ "id":"panel_10","UIObject":{"id":"highchart_01","Name":"Description","Type":"guage","Value":"HTML5 description"}}]},{ "id":"panel_11","style":"height:280px;background-color: blueviolet;","title":"Panel6","panels":[{"id":"panel_12", "UIObject":{"id":"UI_6","Name":"Description","Type":"Text","Value":"HTML6 description"}},{"id":"panel_12", "UIObject":{"id":"UI_7","Name":"Description","Type":"Text","Value":"HTML7 description"}},{"id":"panel_12", "UIObject":{"id":"UI_8","Name":"Setting","Type":"Text","Value":"HTML8 description"}}]},]}
支持的页面
前端JavaScript 程序
function LoadUIObject(ParentNodeId,UIObject){if (UIObject.Type=="Text"){$("#"+ParentNodeId).append("<div "+"id="+UIObject.id+">"+UIObject.Value+"</div>");}else if (UIObject.Type=="guage"){$("#"+ParentNodeId).append("<div "+"id="+UIObject.id+" style='height:240px'></div>");guageInit(UIObject.id);} else if (UIObject.Type=="Input"){$("#"+ParentNodeId).append('<div class="input-group mb-3">');$(".input-group").append('<div style="margin:5px">'+UIObject.Name+'</div>');$(".input-group").append('<input type="text" class="form-control" id="name" placeholder="Recipient username" aria-label="Recipient username" aria-describedby="basic-addon2">') ; $("input").append('<input type="text" class="form-control" id="name" placeholder="Recipient username" aria-label="Recipient username" aria-describedby="basic-addon2">') ;$(".input-group").append('<div class="input-group-append">');$(".input-group-append").append('<button class="btn btn-info" type="button">确认</button>'); }}function Load(parentNodeId,panels,level){console.log("level:"+level);for (var panel in panels){if(level%2==0){ if (level==0)$("#"+parentNodeId).append("<div "+"id="+panels[panel].id+" class='col-xs-6 col-lg-4 col-md-6 ' style='"+panels[panel].style+"'></div>");else$("#"+parentNodeId).append("<div "+"id="+panels[panel].id+" class='col' style='"+panels[panel].style+"'></div>");}else { $("#"+parentNodeId).append("<div "+"id="+panels[panel].id+" class='row' style='"+panels[panel].style+"'></div>");}if (panels[panel].title)$("#"+panels[panel].id).append("<div class='row' >"+panels[panel].title+" </div>");if (panels[panel].panels!=null){Load(panels[panel].id,panels[panel].panels,level+1);}else if (panels[panel].UIObject!=null) {LoadUIObject(panels[panel].id,panels[panel].UIObject);} }}function guageInit(id){console.log("id="+id);$( '#' +id).highcharts({chart: {type: 'gauge',alignTicks: false,plotBackgroundColor: null,plotBackgroundImage: null,plotBorderWidth: 0,plotShadow: false},title: {text: 'Temperature'},pane: {startAngle: -150,endAngle: 150},yAxis: [{min: 0,max: 200,lineColor: '#339',tickColor: '#339',minorTickColor: '#339',offset: -25,lineWidth: 2,labels: {distance: -20,rotation: 'auto'},tickLength: 5,minorTickLength: 5,endOnTick: false}, {min: 0,max: 124,tickPosition: 'outside',lineColor: '#933',lineWidth: 2,minorTickPosition: 'outside',tickColor: '#933',minorTickColor: '#933',tickLength: 5,minorTickLength: 5,labels: {distance: 12,rotation: 'auto'},offset: -20,endOnTick: false}],series: [{name: 'Speed',data: [80],dataLabels: {formatter: function () {var kmh = this.y,mph = Math.round(kmh * 0.621);return '<span style="color:#339">' + kmh + ' km/h</span><br/>' +'<span style="color:#933">' + mph + ' mph</span>';},backgroundColor: {linearGradient: {x1: 0,y1: 0,x2: 0,y2: 1},stops: [[0, '#DDD'],[1, '#FFF']]}},tooltip: {valueSuffix: ' km/h'}}]}); }console.log("start");console.log(page.panels);Load("top",page.panels,0);
结束语
这是为系统方案做的预研工作,还有许多问题有待研究,例如智能化响应式页面设计。让布局更加符合自动化系统的要求。德国有一家公司Helio 的HMI 做的不错,可惜还没有上市。