基于SVG的HMI组件
人机界面是自动化领域不可或缺重要组成部分。人机界面系统的设计看上去并没有太大的技术门槛,但是设计一个HMI系统的工作量是巨大的,如果你没有足够的耐心和精力是难以完成一个通用HMI系统的。构建UI控件库就是一个似乎永远完不成的事情,用户永远觉得不够用。
另一方面用户使用HMI的组态工具构建HMI应用也是一个十分繁琐又容易出错的地方,你需要将大量的数据点与HMI上的UI对应起来。
人们期望自动生成HMI界面, 本文讨论如何解决这些问题。
基于模型的设计
自动控制领域正走向开放性系统,并且采用基于模型的设计理念,OPC UA ,MTP,工业4.0 AAS等技术都是基于信息模型的工业标准。
当系统采用了信息模块之后,现场传送的数据已经不是简单的整型数和浮点数,而是一个结构化的信息模型。
如果 HMI 系统构建与信息模型相对应的UI 模型,那么数据模型的数据就能够直接呈现到UI模型上,不再需要组态工具去建立对应关系。这将减少组态时许多的工作量。
转变UI组件的开发方式
传统的HMI 中的UI组件都是由HMI软件开发商实现的,即便开放了用户开发专用UI组件的能力,通常也是在HMI 组态工具中实现的。在这种相对封闭的UI组件构建方法带来了HMI系统的开发难度。
由HMI软件开发者开发UI组件往往很难与应用完美的结合,与具体应用相结合时难免看上去牵强附会。
笔者看来,应该将UI组件库集中方式,转向用户自主开发。具体地讲,将UI组件的开发交给底层设备的开发者,在设备的信息模型中包含了UI 组件。当设备与HMI 软件连接起来后,将UI组件上传到HMI系统中去。例如,一台空气压缩机的信息模型中包含了它的UI组件。
设备开发者是最熟悉其设备的图形化方式,我们将这种由设备制造商提高UI 组件的方式称为“民主化开发”方法。
在流程控制行业的NAMUR 的MTP 模型中,就采取了这样一种方法,号称能够根据P&ID 图(Piping and instrumentation diagram)自动生成HMI的画面。
UI 组件的实现技术
HMI 系统实现的方式主要有如下几种:
- 基于Web 的HTML5技术
- 基于Window 的.NET/C# 技术
- 基于Android的技术
尽管.NET/C# 技术仅限于Windows 平台实现(目前.NET 的用户界面不支持Linux),C#仍然是开发工业控制上位机软件效率最高的设计工具。相比之下,HTML5 Web技术采用了开放性技术(HTML,CSS和javascript),带来了兼容性和灵活性,但是Web 技术过于碎片化,开发效率并不高。
UI组件的组成
所谓UI组件,就是将HMI的页面分解成多个小型的模块。
基于MVC 模型,现代界面采取了页面视图与控制逻辑的分离。
在.NET/C# 技术中,也已经从传统的Form 转向页面与逻辑分离的WPF技术。wpf 窗口包括了描述界面的XAML和后端的代码C#。后端实现XAML 的响应。
在QT技术中,大致也是如此。
UI 组件技术
- 单一文件
由于UI 组件是从底层信息模型中上传到HMI 系统的,为了避免多个文件的上传,最好是单一文件。 比如在Web技术中,我们可以采用VUE 组件。VUE 组件是将HTML,CSS 和JavaScript 合并在一个vue文件中。也可以采用SVG矢量图形
- 与平台无关
显然WPF 是依赖Windows 平台的。
- 简单设计工具
SVG
矢量图形SVG 也是一种单一UI 组件的方法。SVG的全称是可缩放矢量图形(Scalable Vector Graphics,SVG)基于 XML 标记语言。但是SVG不仅仅描述矢量图形,还能够包含JavaScript 程序。 SVG是一种理想的HMI UI组件工具
SVG 是W3C 的开放型标准。与具体的架构无关。有许多工具能够实现SVG 的绘制。添加Javascript代码只需要文本编辑器就能够完成。
所以SVG是一种理想的HMI UI组件工具。
XAML/C#
C# 的XAML 能够描述精美的图形界面,而且支持3D 模型的显示。WPF 的UI 组件是用户组件(UserControl )。每个UserControl 包含了一个XAML和一个CS文件。构建用户组件库的方法是:
- 新建用户控件库项目,在该库中可以包含多个用户控件。
- 编译成为DLL 文件。
- 在其它应用程序中导入该DLL 或者动态导入DLL。
在一些传统的HMI系统中,仍然使用XAML 开发用户界面,例如艾默生公司的MOVICON SCADA 软件就是采用XAML 。
XAML/C# 的缺点是依赖Windows 平台技术·,要使用微软的VS开发UI控件。另外,组件以DLL 形式呈现,不可阅读和修改。动态载入HMI系统也比较麻烦。
在笔者看来SVG是比较好的UI组件技术。
基于SVG 的UI组件
下面我们以svidget 开源架构为例介绍基于SVG 的UI组件技术。
注:也许有人会疑惑,为什么SVG在能够包含javascript 代码呢?这是由于SVG 是使用XML 描述的矢量化图形。在浏览器中是将SVG的XML 直接嵌入HTML 文本中处理的,所以SVG中能够嵌入HTML5 的内容。
项目Github:https://github.com/joeax/svidget
svidget 主要构建基于SVG 技术的UI组件,它被成为Widget(小部件),其特点是
可交互
您可以通过页面的参数、操作和事件从页面操作小部件。 也就是说,您可以设置参数值、调用操作以及处理小部件中的事件。
使用方便
可以使用<object>标签在网页中嵌入SVG Widget。并且使用<param> 标签将数据传递到小组件。
下面是一个简单的例子
假设我想创建一个圆环仪表来显示操作的进度。看起来像这样的东西:
这是它的原始静态SVG代码:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svidget="http://www.svidget.org/svidget"width="200" height="200" viewBox="0 0 200 200" style="background:transparent"><title>Donut Gauge</title><desc>Visualizes a donut-style gauge with text value in the middle.</desc><style>#textLabel { font-size: 16px; font-family: Helvetica; alignment-baseline: text-top; }#spanContent { font-size: 28px; font-weight: bold; margin: 0px; }</style><defs><mask id="donutMask"><rect x="0" y="0" width="100%" height="100%" fill="white" /><circle r="48" cx="100" cy="100" fill="black" /></mask></defs><g><circle id="backCircle" r="98" cx="100" cy="100" fill="#ddd" mask="url(#donutMask)" /><path id="foreArc" fill="none" stroke="#7f7fdf" stroke-width="50" d="M 100,27 A 73,73 0 1,1 27,100" /><text id="textLabel" x="100" y="110" text-anchor="middle" fill="#3f3f3f"><tspan id="spanContent">75</tspan> %</text></g></svg>
我们可以通过将其转换为 SVG 小部件来使其有用。 要对此进行小部件化,我们首先需要考虑的是它的属性是什么。在 svidget 中,我们将这些称为参数。 对于我们的甜甜圈仪表盘
- 数据 - 这是主要数据。让它成为 0 到 1 之间的值,表示百分比。
- 颜色 - 圆圈上前景的颜色。
- 背景颜色 - 圆圈上背景的颜色。
- 文本颜色 - 中间文本的颜色。
- showText - 是否显示文本的布尔标志。
- 宽度 - 甜甜圈的宽度。可以是介于 1 和 98 之间的值。
Svidget 使用声明性语法向小部件添加参数。由于SVG是结构化XML,我们可以用svidget命名空间来扩展它。
<svidget:params>
<svidget:param name="data" shortname="d" type="number" description="The percentage to fill. Value should be between 0 and 1." />
<svidget:param name="color" shortname="color" type="string" subtype="color" binding="#foreArc@stroke" description="The circle foreground color." />
<svidget:param name="backColor" shortname="bcolor" type="string" subtype="color" binding="#backCircle@fill" description="The circle background color." />
<svidget:param name="textColor" shortname="tcolor" type="string" subtype="color" binding="#textLabel@fill" description="The text color." />
<svidget:param name="showText" shortname="st" type="bool" description="Whether to show the text in the middle." />
<svidget:param name="width" shortname="w" type="number" description="The width of the donut portion of the circle." />
</svidget:params>
完整的代码
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svidget="http://www.svidget.org/svidget"width="200" height="200" viewBox="0 0 200 200" style="background:transparent"><title>Donut Gauge</title><desc>Visualizes a donut-style gauge with text value in the middle.</desc><style><![CDATA[ #textLabel { font-size: 16px; font-family: Helvetica; alignment-baseline: text-top; } #spanContent { font-size: 28px; font-weight: bold; margin: 0px; } ]]></style><svidget:params><svidget:param name="data" shortname="d" type="number" description="The percentage to fill. Value should be between 0 and 1." /><svidget:param name="color" shortname="color" type="string" subtype="color" binding="#foreArc@stroke" description="The circle foreground color." /><svidget:param name="backColor" shortname="bcolor" type="string" subtype="color" binding="#backCircle@fill" description="The circle background color." /><svidget:param name="textColor" shortname="tcolor" type="string" subtype="color" binding="#textLabel@fill" description="The text color." /><svidget:param name="showText" shortname="st" type="bool" description="Whether to show the text in the middle." /><svidget:param name="width" shortname="w" type="number" description="The width of the donut portion of the circle." /></svidget:params><defs><mask id="donutMask"><rect x="0" y="0" width="100%" height="100%" fill="white" /><circle r="48" cx="100" cy="100" fill="black" /></mask></defs><g><circle id="backCircle" r="98" cx="100" cy="100" fill="#ddd" mask="url(#donutMask)" /><path id="foreArc" fill="none" stroke="#7f7fdf" stroke-width="50" d="M 100,27 A 73,73 0 1,1 27,100" /><text id="textLabel" x="100" y="110" text-anchor="middle" fill="#3f3f3f" visibility="visible"><tspan id="spanContent">75</tspan> %</text></g><script type="application/javascript" xlink:href="../scripts/svidget.js"></script><script type="application/javascript"><![CDATA[var _data = 0;var _width = 50;var FULL_RADIUS = 100;var DONUT_RADIUS = FULL_RADIUS - 2; // 98function init() {//setArcPath(0.75);//debugger;var widget = svidget.current();widget.param("data").on("set", onParamDataSet);widget.param("width").on("set", onParamWidthSet);widget.param("showText").on("set", onParamShowTextSet);setParamData(widget.param("data").value());setParamWidth(widget.param("width").value());setParamWidth(widget.param("showText").value());}function onParamDataSet(e) {//alert('onParamDataSet');var val = parseFloat(e.value.value); // { value: val }setParamData(val);}function onParamWidthSet(e) {var w = parseInt(e.value.value); // { value: val }setParamWidth(w);}function onParamShowTextSet(e) {var show = e.value.value;setParamShowText(show);}// range: 0 to 1function setParamData(val) {val = rangify(val, 0, 1);// set donut arcsetArcPath(val);// set textsetContentLabel(val);// set global data_data = val;}// range: 1 to 98function setParamWidth(w) {w = rangify(w, 1, DONUT_RADIUS);// set donut mask - i.e. empty hole in middle aka the donut holevar rad = DONUT_RADIUS - w;setMaskRadius(rad);setArcWidth(w);// set global width_width = w;// re-set arc pathsetArcPath(_data);}function setParamShowText(show) {//debugger;var textLabel = document.getElementById("textLabel");var v = !show ? "hidden" : "visible";textLabel.setAttribute("visibility", v);}function setMaskRadius(rad) {var maskCir = document.querySelector("#donutMask > circle");maskCir.setAttribute("r", rad);}function setContentLabel(pct) {var val = parseInt(pct * 100);var spanContent = document.getElementById("spanContent");spanContent.textContent = val + "";}function setArcPath(pct) {var path = generateArcPath(pct);var foreArc = document.getElementById("foreArc");foreArc.setAttribute("d", path);}function setArcWidth(w) {var foreArc = document.getElementById("foreArc");foreArc.setAttribute("stroke-width", w);}// pct == 0 to 1function generateArcPath(pct) {var baseY = 2;var halfWidth = _width / 2.0;var startY = baseY + halfWidth;var arcRadius = FULL_RADIUS - startY;var largeArc = pct > 0.5 ? 1 : 0; // if greater than 50 we need to use large arc in pathvar pctRadians = (Math.PI * 2) * pct;var endX = round3((Math.sin(pctRadians) * arcRadius) + FULL_RADIUS);var endY = round3((-Math.cos(pctRadians) * arcRadius) + FULL_RADIUS);if (endX == 100 && pct > 0) endX = 99.999; // this corrects issue with path if start and end are same it won't draw an arc//debugger;// M 100,27 A 73,73 0 1,1 27,100var path = "M 100," + startY + " "; // move to startpath += "A " + arcRadius + "," + arcRadius + " 0 " + largeArc + ",1 " + endX + "," + endY;return path;}function rangify(val, min, max) {if (isNaN(val) || val < min) val = min;if (val > max) val = max;return val;}function round3(val) {return parseInt(val * 1000) / 1000.0;}window.addEventListener('load', init, false);]]></script>
</svg>
在网页上嵌入小部件的主要方法是object> 标记。 框架使用 role=“svidget” 查找所有 <object> 标签,并实例化小部件。
<object id="myDonutGauge" role="svidget" data="widgets/donut.svg" type="image/svg+xml" width="200" height="200">
<param name="data" value="0.55" />
<param name="color" value="#da3333" />
<param name="backColor" value="#ffac33" />
<param name="textColor" value="#da3333" />
<param name="showText" value="true" />
<param name="width" value="40" />
</object>
您还可以使用 svidget.load() 以编程方式加载小部件:
svidget.load("#widgetContainer", "widgets/donut.svg", {
data: 0.55,
color: "#da3333",
backColor: "#ffac33",
textColor: "#da3333",
showText: true,
width: 40,
});
在Github 中包含了一个demo 网页
很酷吧?
HMI组件实现的细节
在具体地实现中,还有如下细节要考虑:
- UI组件只是提供了组件,网页布局仍然需要组态软件来完成。尽管简单了许多。
- 网页中包含一个JavaScript 的运行时(runtime),它是与信息模型无关的。他只完成信息的交换。运行时将信息模型的NodeID 传递给UI 组件。UI组件订阅信息对象的特性(Property)并且支持事件和方法调用。
结束语
本文讨论的一种灵活的HMI组件方法,具体细节可以交流。