--- a/PLCGenerator.py Tue Sep 24 11:55:59 2019 +0200
+++ b/PLCGenerator.py Tue Sep 24 11:58:31 2019 +0200
@@ -935,7 +935,7 @@
if invar.getformalParameter() == "EN":
if len(invar.getconnectionPointIn().getconnections()) > 0:
if blk.getinstanceName() is None:
- var_name = "%s%d_ENO" % (blk.gettypeName(), blk.getlocalId())
+ var_name = "_TMP_%s%d_ENO" % (blk.gettypeName(), blk.getlocalId())
else:
var_name = "%s.ENO" % blk.getinstanceName()
return var_name
@@ -1160,7 +1160,7 @@
if variable.getformalParameter() == "":
variable_name = "%s%d" % (type, block.getlocalId())
else:
- variable_name = "%s%d_%s" % (type, block.getlocalId(), parameter)
+ variable_name = "_TMP_%s%d_%s" % (type, block.getlocalId(), parameter)
if self.Interface[-1][0] != "VAR" or self.Interface[-1][1] is not None or self.Interface[-1][2]:
self.Interface.append(("VAR", None, False, []))
if variable.connectionPointOut in self.ConnectionTypes:
@@ -1253,7 +1253,7 @@
if output_parameter == "":
output_name = "%s%d" % (type, block.getlocalId())
else:
- output_name = "%s%d_%s" % (type, block.getlocalId(), output_parameter)
+ output_name = "_TMP_%s%d_%s" % (type, block.getlocalId(), output_parameter)
output_value = [(output_name, output_info)]
return self.ExtractModifier(output_variable, output_value, output_info)
if block_infos["type"] == "functionBlock":
--- a/ProjectController.py Tue Sep 24 11:55:59 2019 +0200
+++ b/ProjectController.py Tue Sep 24 11:58:31 2019 +0200
@@ -1053,7 +1053,8 @@
"FB": "extern %(type)s %(C_path)s;"
}[v["vartype"]] % v
for v in self._VariablesList if v["C_path"].find('.') < 0]),
- "variable_decl_array": ",\n".join(variable_decl_array)
+ "variable_decl_array": ",\n".join(variable_decl_array),
+ "var_access_code": targets.GetCode("var_access.c")
}
return debug_code
@@ -1163,6 +1164,19 @@
def _Generate_runtime(self):
buildpath = self._getBuildPath()
+ # CTN code gen is expected AFTER Libraries code gen,
+ # at least SVGHMI relies on it.
+
+ # Generate C code and compilation params from liraries
+ try:
+ LibCFilesAndCFLAGS, LibLDFLAGS, LibExtraFiles = self.GetLibrariesCCode(
+ buildpath)
+ except Exception:
+ self.logger.write_error(
+ _("Runtime library extensions C code generation failed !\n"))
+ self.logger.write_error(traceback.format_exc())
+ return False
+
# Generate C code and compilation params from confnode hierarchy
try:
CTNLocationCFilesAndCFLAGS, CTNLDFLAGS, CTNExtraFiles = self._Generate_C(
@@ -1174,16 +1188,6 @@
self.logger.write_error(traceback.format_exc())
return False
- # Generate C code and compilation params from liraries
- try:
- LibCFilesAndCFLAGS, LibLDFLAGS, LibExtraFiles = self.GetLibrariesCCode(
- buildpath)
- except Exception:
- self.logger.write_error(
- _("Runtime library extensions C code generation failed !\n"))
- self.logger.write_error(traceback.format_exc())
- return False
-
self.LocationCFilesAndCFLAGS = LibCFilesAndCFLAGS + \
CTNLocationCFilesAndCFLAGS
self.LDFLAGS = CTNLDFLAGS + LibLDFLAGS
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/XSLTransform.py Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# This file is part of Beremiz.
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+from lxml import etree
+
+class XSLTransform(object):
+ """ a class to handle XSLT queries on project and libs """
+ def __init__(self, xsltpath, xsltext):
+
+ # parse and compile. "beremiz" arbitrary namespace for extensions
+ self.xslt = etree.XSLT(
+ etree.parse(
+ xsltpath,
+ etree.XMLParser()),
+ extensions={("beremiz", name): call for name, call in xsltext})
+
+ def transform(self, root, **kwargs):
+ res = self.xslt(root, **{k: etree.XSLT.strparam(v) for k, v in kwargs.iteritems()})
+ # print(self.xslt.error_log)
+ return res
+
+
--- a/docutil/docsvg.py Tue Sep 24 11:55:59 2019 +0200
+++ b/docutil/docsvg.py Tue Sep 24 11:58:31 2019 +0200
@@ -31,15 +31,19 @@
def get_inkscape_path():
""" Return the Inkscape path """
- from six.moves import winreg
- try:
- svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
- 'Software\\Classes\\svgfile\\shell\\Inkscape\\command')
- except OSError:
- svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
- 'Software\\Classes\\inkscape.svg\\shell\\open\\command')
- svgexepath = svgexepath.replace('"%1"', '')
- return svgexepath.replace('"', '')
+ if wx.Platform == '__WXMSW__':
+ from six.moves import winreg
+ try:
+ svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
+ 'Software\\Classes\\svgfile\\shell\\Inkscape\\command')
+ except OSError:
+ svgexepath = winreg.QueryValue(winreg.HKEY_LOCAL_MACHINE,
+ 'Software\\Classes\\inkscape.svg\\shell\\open\\command')
+ svgexepath = svgexepath.replace('"%1"', '').strip()
+ return svgexepath.replace('"', '')
+ else:
+ # TODO: search path
+ return os.path.join("/usr/bin", "inkscape")
def open_win_svg(svgexepath, svgfile):
@@ -65,7 +69,7 @@
wx.MessageBox("Inkscape is not found or installed !")
return None
else:
- svgexepath = os.path.join("/usr/bin", "inkscape")
+ svgexepath=get_inkscape_path()
if os.path.isfile(svgexepath):
open_lin_svg(svgexepath, svgfile)
else:
--- a/features.py Tue Sep 24 11:55:59 2019 +0200
+++ b/features.py Tue Sep 24 11:58:31 2019 +0200
@@ -12,7 +12,8 @@
('Native', 'NativeLib.NativeLibrary', True),
('Python', 'py_ext.PythonLibrary', True),
('Etherlab', 'etherlab.EthercatMaster.EtherlabLibrary', False),
- ('SVGUI', 'svgui.SVGUILibrary', False)]
+ ('SVGUI', 'svgui.SVGUILibrary', False),
+ ('SVGHMI', 'svghmi.SVGHMILibrary', False)]
catalog = [
('canfestival', _('CANopen support'), _('Map located variables over CANopen'), 'canfestival.canfestival.RootClass'),
@@ -22,6 +23,7 @@
('c_ext', _('C extension'), _('Add C code accessing located variables synchronously'), 'c_ext.CFile'),
('py_ext', _('Python file'), _('Add Python code executed asynchronously'), 'py_ext.PythonFile'),
('wxglade_hmi', _('WxGlade GUI'), _('Add a simple WxGlade based GUI.'), 'wxglade_hmi.WxGladeHMI'),
- ('svgui', _('SVGUI'), _('Experimental web based HMI'), 'svgui.SVGUI')]
+ ('svgui', _('SVGUI'), _('Experimental web based HMI'), 'svgui.SVGUI'),
+ ('svghmi', _('SVGHMI'), _('SVG based HMI'), 'svghmi.SVGHMI')]
file_editors = []
--- a/plcopen/Makefile Tue Sep 24 11:55:59 2019 +0200
+++ b/plcopen/Makefile Tue Sep 24 11:58:31 2019 +0200
@@ -1,13 +1,23 @@
#! gmake
-yml := ../../yml2
+# Makefile to generate XSLT stylesheets from ysl2 files in the same directory
+
+# This uses YML2.
+# hg clone https://pep.foundation/dev/repos/yml2/
+
+# It should be just fine if yml2 is cloned just asside beremiz
+# otherwise, point yml2path to yml2 source directory
+# make yml2path=path/to/yml/dir
+
+yml2path ?= $(abspath ../../yml2)
+
ysl2files := $(wildcard *.ysl2)
xsltfiles := $(patsubst %.ysl2, %.xslt, $(ysl2files))
all:$(xsltfiles)
-%.xslt: %.ysl2 yslt_noindent.yml2
- $(yml)/yml2c -I $(yml) $< -o $@.tmp
+%.xslt: %.ysl2 ../yslt_noindent.yml2
+ $(yml2path)/yml2c -I $(yml2path):../ $< -o $@.tmp
xmlstarlet fo $@.tmp > $@
rm $@.tmp
--- a/plcopen/XSLTModelQuery.py Tue Sep 24 11:55:59 2019 +0200
+++ b/plcopen/XSLTModelQuery.py Tue Sep 24 11:58:31 2019 +0200
@@ -8,13 +8,14 @@
from lxml import etree
import util.paths as paths
from plcopen.structures import StdBlckLibs
+from XSLTransform import XSLTransform
ScriptDirectory = paths.AbsDir(__file__)
-
-class XSLTModelQuery(object):
+class XSLTModelQuery(XSLTransform):
""" a class to handle XSLT queries on project and libs """
def __init__(self, controller, xsltpath, ext=None):
+
# arbitrary set debug to false, updated later
self.debug = False
@@ -31,19 +32,12 @@
if ext is not None:
xsltext.extend(ext)
- # parse and compile. "beremiz" arbitrary namespace for extensions
- self.xslt = etree.XSLT(
- etree.parse(
- os.path.join(ScriptDirectory, xsltpath),
- etree.XMLParser()),
- extensions={("beremiz", name): call for name, call in xsltext})
-
+ XSLTransform.__init__(self,
+ os.path.join(ScriptDirectory, xsltpath),
+ xsltext)
def _process_xslt(self, root, debug, **kwargs):
self.debug = debug
- res = self.xslt(root, **{k: etree.XSLT.strparam(v) for k, v in kwargs.iteritems()})
- # print(self.xslt.error_log)
- return res
-
+ return self.transform(root, **kwargs)
# -------------------------------------------------------------------------------
# Helpers functions for translating list of arguments
--- a/plcopen/yslt_noindent.yml2 Tue Sep 24 11:55:59 2019 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-include yslt.yml2
-
-in xsl {
- decl istylesheet (
- *output="xml",
- version="1.0",
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform",
- xmlns:exsl='http://exslt.org/common',
- extension-element-prefixes='exsl'
- ) alias stylesheet {
- output *output;
- content;
- };
-
- decl template(match) {
- content;
- };
-
- decl function(name) alias template {
- content;
- };
-
- decl call(name) alias call-template {
- content;
- };
-
- decl apply(select) alias apply-templates {
- content;
- };
-}
-
-
--- a/runtime/PLCObject.py Tue Sep 24 11:55:59 2019 +0200
+++ b/runtime/PLCObject.py Tue Sep 24 11:58:31 2019 +0200
@@ -319,12 +319,15 @@
return False
- def PythonRuntimeCall(self, methodname, use_evaluator=True):
+ def PythonRuntimeCall(self, methodname, use_evaluator=True, reverse_order=False):
"""
Calls init, start, stop or cleanup method provided by
runtime python files, loaded when new PLC uploaded
"""
- for method in self.python_runtime_vars.get("_runtime_%s" % methodname, []):
+ methods = self.python_runtime_vars.get("_runtime_%s" % methodname, [])
+ if reverse_order:
+ methods = reversed(methods)
+ for method in methods:
if use_evaluator:
_res, exp = self.evaluator(method)
else:
@@ -395,7 +398,7 @@
if self.python_runtime_vars is not None:
self.PythonThreadCommand("Finish")
self.PythonThread.join()
- self.PythonRuntimeCall("cleanup", use_evaluator=False)
+ self.PythonRuntimeCall("cleanup", use_evaluator=False, reverse_order=True)
self.python_runtime_vars = None
@@ -438,7 +441,7 @@
if cmd == "Activate":
self.PythonRuntimeCall("start")
self.PythonThreadLoop()
- self.PythonRuntimeCall("stop")
+ self.PythonRuntimeCall("stop", reverse_order=True)
else: # "Finish"
break
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/Makefile Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,25 @@
+#! gmake
+
+# Makefile to generate XSLT stylesheets from ysl2 files in the same directory
+
+# This uses YML2.
+# hg clone https://pep.foundation/dev/repos/yml2/
+
+# It should be just fine if yml2 is cloned just asside beremiz
+# otherwise, point yml2path to yml2 source directory
+# make yml2path=path/to/yml/dir
+
+yml2path ?= $(abspath ../../yml2)
+
+ysl2files := $(wildcard *.ysl2)
+xsltfiles := $(patsubst %.ysl2, %.xslt, $(ysl2files))
+
+all:$(xsltfiles)
+
+%.xslt: %.ysl2 ../yslt_noindent.yml2
+ $(yml2path)/yml2c -I $(yml2path):../ $< -o $@.tmp
+ xmlstarlet fo $@.tmp > $@
+ rm $@.tmp
+
+clean:
+ rm -f $(xsltfiles)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/README Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,1 @@
+SVG HMI
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/__init__.py Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2019: Edouard TISSERANT
+#
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+from svghmi.svghmi import *
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/default.svg Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1280"
+ height="720"
+ viewBox="0 0 1280 720"
+ version="1.1"
+ id="hmi0"
+ sodipodi:docname="default.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata4542">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:document-units="px"
+ inkscape:current-layer="hmi0"
+ showgrid="false"
+ units="px"
+ inkscape:zoom="0.7"
+ inkscape:cx="576.80864"
+ inkscape:cy="330.28432"
+ inkscape:window-width="1600"
+ inkscape:window-height="886"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <rect
+ style="color:#000000"
+ id="page0"
+ width="1280"
+ height="720"
+ x="0"
+ y="0">
+ <desc
+ id="desc_page0">This is description for page 0
+
+all lines in the form "name: value"
+are used as js object definition initializer
+
+role: "page"
+name: "Home"
+
+after triple opening braces is global JavaScript code
+
+{{{
+/* JS style Comment */
+alert("Hello World");
+}}}
+
+after triple closing braces is back to description
+</desc>
+ </rect>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="436.32812"
+ y="418.24219"
+ id="text5151"
+ inkscape:label="count"><desc
+ id="desc5153">path: "count"
+format: "%4.4d"</desc><tspan
+ sodipodi:role="line"
+ id="tspan5149"
+ x="436.32812"
+ y="418.24219"
+ style="stroke-width:1px">8888</tspan></text>
+</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/gen_index_xhtml.xslt Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<xsl:stylesheet xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns="beremiz" xmlns:cc="http://creativecommons.org/ns#" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/1999/xhtml" xmlns:str="http://exslt.org/strings" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:exsl="http://exslt.org/common" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" exclude-result-prefixes="ns" extension-element-prefixes="ns" version="1.0">
+ <xsl:output method="xml" cdata-section-elements="script"/>
+ <xsl:variable name="geometry" select="ns:GetSVGGeometry()"/>
+ <xsl:variable name="hmitree" select="ns:GetHMITree()"/>
+ <xsl:template match="@* | node()">
+ <xsl:copy>
+ <xsl:apply-templates select="@* | node()"/>
+ </xsl:copy>
+ </xsl:template>
+ <xsl:variable name="mark">
+ <xsl:text>=HMI=
+</xsl:text>
+ </xsl:variable>
+ <xsl:template match="/">
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head/>
+ <body style="margin:0;">
+ <xsl:copy>
+ <xsl:comment>
+ <xsl:apply-templates mode="testgeo" select="$geometry"/>
+ </xsl:comment>
+ <xsl:comment>
+ <xsl:apply-templates mode="testtree" select="$hmitree"/>
+ </xsl:comment>
+ <xsl:apply-templates select="@* | node()"/>
+ </xsl:copy>
+ <script>
+ <xsl:text>function evaluate_js_from_descriptions() {
+</xsl:text>
+ <xsl:text> var Page;
+</xsl:text>
+ <xsl:text> var Input;
+</xsl:text>
+ <xsl:text> var Display;
+</xsl:text>
+ <xsl:text> var res = [];
+</xsl:text>
+ <xsl:variable name="midmark">
+ <xsl:text>
+</xsl:text>
+ <xsl:value-of select="$mark"/>
+ </xsl:variable>
+ <xsl:apply-templates mode="code_from_descs" select="//*[contains(child::svg:desc, $midmark) or starts-with(child::svg:desc, $mark)]"/>
+ <xsl:text> return res;
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ <xsl:text>(function(){
+</xsl:text>
+ <xsl:text> var relative_URI = window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws');
+</xsl:text>
+ <xsl:text> var ws = new WebSocket(relative_URI);
+</xsl:text>
+ <xsl:text> ws.onmessage = function (evt) {
+</xsl:text>
+ <xsl:text> var received_msg = evt.data;
+</xsl:text>
+ <xsl:text> alert("Message is received..."+received_msg);
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text> ws.onopen = function (evt) {
+</xsl:text>
+ <xsl:text> ws.send("test");
+</xsl:text>
+ <xsl:text> };
+</xsl:text>
+ <xsl:text>})();
+</xsl:text>
+ </script>
+ </body>
+ </html>
+ </xsl:template>
+ <xsl:template mode="code_from_descs" match="*">
+ <xsl:text>{
+</xsl:text>
+ <xsl:text> var path, role, name, priv;
+</xsl:text>
+ <xsl:text> var id = "</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>";
+</xsl:text>
+ <xsl:if test="«@inkscape:label»">
+ <xsl:text>name = "</xsl:text>
+ <xsl:value-of select="@inkscape:label"/>
+ <xsl:text>";
+</xsl:text>
+ </xsl:if>
+ <xsl:text>/* -------------- */
+</xsl:text>
+ <xsl:value-of select="substring-after(svg:desc, $mark)"/>
+ <xsl:text>
+</xsl:text>
+ <xsl:text> /* -------------- */
+</xsl:text>
+ <xsl:text> res.push({
+</xsl:text>
+ <xsl:text> path:path,
+</xsl:text>
+ <xsl:text> role:role,
+</xsl:text>
+ <xsl:text> name:name,
+</xsl:text>
+ <xsl:text> priv:priv
+</xsl:text>
+ <xsl:text> })
+</xsl:text>
+ <xsl:text>}
+</xsl:text>
+ </xsl:template>
+ <xsl:template mode="testgeo" match="bbox">
+ <xsl:text>ID: </xsl:text>
+ <xsl:value-of select="@Id"/>
+ <xsl:text> x: </xsl:text>
+ <xsl:value-of select="@x"/>
+ <xsl:text> y: </xsl:text>
+ <xsl:value-of select="@y"/>
+ <xsl:text> w: </xsl:text>
+ <xsl:value-of select="@w"/>
+ <xsl:text> h: </xsl:text>
+ <xsl:value-of select="@h"/>
+ <xsl:text>
+</xsl:text>
+ </xsl:template>
+ <xsl:template mode="testtree" match="*">
+ <xsl:param name="indent" select="''"/>
+ <xsl:value-of select="$indent"/>
+ <xsl:text> </xsl:text>
+ <xsl:value-of select="local-name()"/>
+ <xsl:text> </xsl:text>
+ <xsl:value-of select="@name"/>
+ <xsl:text> </xsl:text>
+ <xsl:value-of select="@type"/>
+ <xsl:text> </xsl:text>
+ <xsl:value-of select="@path"/>
+ <xsl:text>
+</xsl:text>
+ <xsl:apply-templates mode="testtree" select="*">
+ <xsl:with-param name="indent">
+ <xsl:value-of select="concat($indent,'>')"/>
+ </xsl:with-param>
+ </xsl:apply-templates>
+ </xsl:template>
+</xsl:stylesheet>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/gen_index_xhtml.ysl2 Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,122 @@
+include yslt_noindent.yml2
+
+// overrides yslt's output function to set CDATA
+decl output(method, cdata-section-elements="script");
+istylesheet
+ /* From Inkscape */
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns="http://www.w3.org/1999/xhtml"
+
+ /* Our namespace to invoke python code */
+ xmlns:ns="beremiz"
+ extension-element-prefixes="ns"
+ exclude-result-prefixes="ns" {
+
+ /* This retrieves geometry obtained through "inkscape -S"
+ * already parsed by python and presented as a list of
+ * <bbox x="0" y="0" w="42" h="42">
+ */
+ variable "geometry", "ns:GetSVGGeometry()";
+ variable "hmitree", "ns:GetHMITree()";
+
+ /* Identity template :
+ * - copy every attributes
+ * - copy every sub-elements
+ */
+ template "@* | node()" {
+ /* use real xsl:copy instead copy-of alias from yslt.yml2 */
+ xsl:copy apply "@* | node()";
+ }
+
+ variable "mark" > =HMI=\n
+
+ /* copy root node and add geometry as comment for a test */
+ template "/"
+ html xmlns="http://www.w3.org/1999/xhtml" {
+ head;
+ body style="margin:0;" {
+ xsl:copy {
+ comment {
+ apply "$geometry", mode="testgeo";
+ }
+ comment {
+ apply "$hmitree", mode="testtree";
+ }
+ apply "@* | node()";
+ }
+ script{
+ ||
+ function evaluate_js_from_descriptions() {
+ var Page;
+ var Input;
+ var Display;
+ var res = [];
+ ||
+ variable "midmark" > \n«$mark»
+ apply """//*[contains(child::svg:desc, $midmark) or \
+ starts-with(child::svg:desc, $mark)]""",2
+ mode="code_from_descs";
+ ||
+ return res;
+ }
+ ||
+
+ /*TODO add :
+ - pages content
+ + with ref to elt ?
+ - widgets parameters
+ */
+
+ include text svghmi.js
+ }
+ }
+ }
+
+ template "*", mode="code_from_descs" {
+ ||
+ {
+ var path, role, name, priv;
+ var id = "«@id»";
+ ||
+
+ /* if label is used, use it as default name */
+ if "«@inkscape:label»"
+ |> name = "«@inkscape:label»";
+
+ | /* -------------- */
+
+ // this breaks indent, but fixing indent could break string literals
+ value "substring-after(svg:desc, $mark)";
+ // nobody reads generated code anyhow...
+
+ ||
+
+ /* -------------- */
+ res.push({
+ path:path,
+ role:role,
+ name:name,
+ priv:priv
+ })
+ }
+ ||
+ }
+
+
+ template "bbox", mode="testgeo"{
+ | ID: «@Id» x: «@x» y: «@y» w: «@w» h: «@h»
+ }
+
+ template "*", mode="testtree"{
+ param "indent", "''";
+ | «$indent» «local-name()» «@name» «@type» «@path»
+ apply "*", mode="testtree" {
+ with "indent" value "concat($indent,'>')"
+ };
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/pous.xml Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,55 @@
+<?xml version='1.0' encoding='utf-8'?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xsi:schemaLocation="http://www.plcopen.org/xml/tc6_0201">
+ <fileHeader companyName="Beremiz" productName="Beremiz" productVersion="0.0" creationDateTime="2008-12-14T16:53:26"/>
+ <contentHeader name="Beremiz non-standard POUs library" modificationDateTime="2019-08-06T14:08:26">
+ <coordinateInfo>
+ <fbd>
+ <scaling x="0" y="0"/>
+ </fbd>
+ <ld>
+ <scaling x="0" y="0"/>
+ </ld>
+ <sfc>
+ <scaling x="0" y="0"/>
+ </sfc>
+ </coordinateInfo>
+ </contentHeader>
+ <types>
+ <dataTypes>
+ <dataType name="HMI_INT">
+ <baseType>
+ <INT/>
+ </baseType>
+ </dataType>
+ <dataType name="HMI_REAL">
+ <baseType>
+ <REAL/>
+ </baseType>
+ </dataType>
+ <dataType name="HMI_STRING">
+ <baseType>
+ <string/>
+ </baseType>
+ </dataType>
+ <dataType name="HMI_BOOL">
+ <baseType>
+ <BOOL/>
+ </baseType>
+ </dataType>
+ <dataType name="HMI_CLASS">
+ <baseType>
+ <BOOL/>
+ </baseType>
+ </dataType>
+ <dataType name="HMI_LABEL">
+ <baseType>
+ <BOOL/>
+ </baseType>
+ </dataType>
+ </dataTypes>
+ <pous/>
+ </types>
+ <instances>
+ <configurations/>
+ </instances>
+</project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/svghmi.c Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,213 @@
+#include <pthread.h>
+#include <errno.h>
+#include "iec_types_all.h"
+#include "POUS.h"
+#include "config.h"
+#include "beremiz.h"
+
+#define DEFAULT_REFRESH_PERIOD_MS 100
+#define HMI_BUFFER_SIZE %(buffer_size)d
+#define HMI_ITEM_COUNT %(item_count)d
+
+/* PLC reads from that buffer */
+static char rbuf[HMI_BUFFER_SIZE];
+
+/* PLC writes to that buffer */
+static char wbuf[HMI_BUFFER_SIZE];
+
+%(extern_variables_declarations)s
+
+#define ticktime_ns %(PLC_ticktime)d
+uint16_t ticktime_ms = (ticktime_ns>1000000)?
+ ticktime_ns/1000000:
+ 1;
+
+typedef enum {
+ buf_free = 0,
+ buf_set,
+ buf_tosend
+} buf_state_t;
+
+int global_write_dirty = 0;
+
+typedef struct {
+ void *ptr;
+ __IEC_types_enum type;
+ uint32_t buf_index;
+
+ /* publish/write/send */
+ long wlock;
+ /* zero means not subscribed */
+ uint16_t refresh_period_ms;
+ uint16_t age_ms;
+
+ buf_state_t wstate;
+
+ /* retrieve/read/recv */
+ long rlock;
+ buf_state_t rstate;
+
+} hmi_tree_item_t;
+
+static hmi_tree_item_t hmi_tree_item[] = {
+%(variable_decl_array)s
+};
+
+static char sendbuf[HMI_BUFFER_SIZE];
+
+typedef void(*hmi_tree_iterator)(hmi_tree_item_t*);
+void traverse_hmi_tree(hmi_tree_iterator fp)
+{
+ unsigned int i;
+ for(i = 0; i < sizeof(hmi_tree_item)/sizeof(hmi_tree_item_t); i++){
+ hmi_tree_item_t *dsc = &hmi_tree_item[i];
+ if(dsc->type != UNKNOWN_ENUM)
+ (*fp)(dsc);
+ }
+}
+
+#define __Unpack_desc_type hmi_tree_item_t
+
+%(var_access_code)s
+
+void write_iterator(hmi_tree_item_t *dsc)
+{
+ void *dest_p = &wbuf[dsc->buf_index];
+ void *real_value_p = NULL;
+ char flags = 0;
+
+ void *visible_value_p = UnpackVar(dsc, &real_value_p, &flags);
+
+ /* Try take lock */
+ long was_locked = AtomicCompareExchange(&dsc->wlock, 0, 1);
+
+ if(was_locked) {
+ /* was locked. give up*/
+ return;
+ }
+
+ if(dsc->wstate == buf_set){
+ /* if being subscribed */
+ if(dsc->refresh_period_ms){
+ if(dsc->age_ms + ticktime_ms < dsc->refresh_period_ms){
+ dsc->age_ms += ticktime_ms;
+ }else{
+ dsc->wstate = buf_tosend;
+ }
+ }
+ }
+
+ /* if new value differs from previous one */
+ if(memcmp(dest_p, visible_value_p, __get_type_enum_size(dsc->type)) != 0){
+ /* copy and flag as set */
+ memcpy(dest_p, visible_value_p, __get_type_enum_size(dsc->type));
+ if(dsc->wstate == buf_free) {
+ dsc->wstate = buf_set;
+ dsc->age_ms = 0;
+ }
+ global_write_dirty = 1;
+ }
+
+ /* unlock - use AtomicComparExchange to have memory barrier */
+ AtomicCompareExchange(&dsc->wlock, 1, 0);
+}
+
+struct timespec sending_now;
+struct timespec next_sending;
+void send_iterator(hmi_tree_item_t *dsc)
+{
+ while(AtomicCompareExchange(&dsc->wlock, 0, 1)) sched_yield();
+
+ // check for variable being modified
+ if(dsc->wstate == buf_tosend){
+ // send
+
+ // TODO pack data in buffer
+
+ dsc->wstate = buf_free;
+ }
+
+ AtomicCompareExchange(&dsc->wlock, 1, 0);
+}
+
+void read_iterator(hmi_tree_item_t *dsc)
+{
+ void *src_p = &rbuf[dsc->buf_index];
+ void *real_value_p = NULL;
+ char flags = 0;
+
+ void *visible_value_p = UnpackVar(dsc, &real_value_p, &flags);
+
+
+ memcpy(visible_value_p, src_p, __get_type_enum_size(dsc->type));
+}
+
+static pthread_cond_t svghmi_send_WakeCond = PTHREAD_COND_INITIALIZER;
+static pthread_mutex_t svghmi_send_WakeCondLock = PTHREAD_MUTEX_INITIALIZER;
+
+static int continue_collect;
+
+int __init_svghmi()
+{
+ bzero(rbuf,sizeof(rbuf));
+ bzero(wbuf,sizeof(wbuf));
+ continue_collect = 1;
+
+ return 0;
+}
+
+void __cleanup_svghmi()
+{
+ pthread_mutex_lock(&svghmi_send_WakeCondLock);
+ continue_collect = 0;
+ pthread_cond_signal(&svghmi_send_WakeCond);
+ pthread_mutex_unlock(&svghmi_send_WakeCondLock);
+}
+
+void __retrieve_svghmi()
+{
+ traverse_hmi_tree(read_iterator);
+}
+
+void __publish_svghmi()
+{
+ global_write_dirty = 0;
+ traverse_hmi_tree(write_iterator);
+ if(global_write_dirty) {
+ pthread_cond_signal(&svghmi_send_WakeCond);
+ }
+}
+
+/* PYTHON CALLS */
+int svghmi_send_collect(uint32_t *size, char **ptr){
+
+ int do_collect;
+ pthread_mutex_lock(&svghmi_send_WakeCondLock);
+ do_collect = continue_collect;
+ if(do_collect){
+ pthread_cond_wait(&svghmi_send_WakeCond, &svghmi_send_WakeCondLock);
+ do_collect = continue_collect;
+ }
+ pthread_mutex_unlock(&svghmi_send_WakeCondLock);
+
+
+ if(do_collect) {
+ traverse_hmi_tree(send_iterator);
+ /* TODO set ptr and size to something */
+ return 0;
+ }
+ else
+ {
+ return EINTR;
+ }
+}
+
+int svghmi_recv_dispatch(uint32_t size, char *ptr){
+ printf("%%*s",size,ptr);
+ /* TODO something with ptr and size
+ - subscribe
+ or
+ - spread values
+ */
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/svghmi.js Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,22 @@
+// svghmi.js
+
+(function(){
+ // Open WebSocket to relative "/ws" address
+ var ws = new WebSocket(window.location.href.replace(/^http(s?:\/\/[^\/]*)\/.*$/, 'ws$1/ws'));
+
+ // Register message reception handler
+ ws.onmessage = function (evt) {
+ // TODO : dispatch and cache hmi tree updates
+
+ var received_msg = evt.data;
+ alert("Message is received..."+received_msg);
+ };
+
+ // Once connection established
+ ws.onopen = function (evt) {
+ // TODO : enable the HMI (was previously offline, or just starts)
+ // show main page
+
+ ws.send("test");
+ };
+})();
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/svghmi.py Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,401 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2019: Edouard TISSERANT
+#
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+import os
+import shutil
+from itertools import izip, imap
+from pprint import pprint, pformat
+
+import wx
+
+import util.paths as paths
+from POULibrary import POULibrary
+from docutil import open_svg, get_inkscape_path
+from lxml import etree
+
+from util.ProcessLogger import ProcessLogger
+from runtime.typemapping import DebugTypesSize
+import targets
+
+HMI_TYPES_DESC = {
+ "HMI_CLASS":{},
+ "HMI_LABEL":{},
+ "HMI_STRING":{},
+ "HMI_INT":{},
+ "HMI_REAL":{}
+}
+
+HMI_TYPES = HMI_TYPES_DESC.keys()
+
+from XSLTransform import XSLTransform
+
+ScriptDirectory = paths.AbsDir(__file__)
+
+class HMITreeNode(object):
+ def __init__(self, path, name, nodetype, iectype = None, vartype = None):
+ self.path = path
+ self.name = name
+ self.nodetype = nodetype
+
+ if iectype is not None:
+ self.iectype = iectype
+ self.vartype = vartype
+ if nodetype in ["HMI_LABEL", "HMI_ROOT"]:
+ self.children = []
+
+ def pprint(self, indent = 0):
+ res = ">"*indent + pformat(self.__dict__, indent = indent, depth = 1) + "\n"
+ if hasattr(self, "children"):
+ res += "\n".join([child.pprint(indent = indent + 1)
+ for child in self.children])
+ res += "\n"
+
+ return res
+
+ def place_node(self, node):
+ best_child = None
+ known_best_match = 0
+ for child in self.children :
+ if child.path is not None:
+ in_common = 0
+ for child_path_item, node_path_item in izip(child.path, node.path):
+ if child_path_item == node_path_item:
+ in_common +=1
+ else:
+ break
+ if in_common > known_best_match:
+ known_best_match = in_common
+ best_child = child
+ if best_child is not None and best_child.nodetype == "HMI_LABEL":
+ best_child.place_node(node)
+ else:
+ self.children.append(node)
+
+ def etree(self):
+
+ attribs = dict(name=self.name)
+ if self.path is not None:
+ attribs["path"]=".".join(self.path)
+
+ res = etree.Element(self.nodetype, **attribs)
+
+ if hasattr(self, "children"):
+ for child_etree in imap(lambda c:c.etree(), self.children):
+ res.append(child_etree)
+
+ return res
+
+ def traverse(self):
+ yield self
+ if hasattr(self, "children"):
+ for c in self.children:
+ for yoodl in c.traverse():
+ yield yoodl
+
+# module scope for HMITree root
+# so that CTN can use HMITree deduced in Library
+# note: this only works because library's Generate_C is
+# systematicaly invoked before CTN's CTNGenerate_C
+
+hmi_tree_root = None
+
+class SVGHMILibrary(POULibrary):
+ def GetLibraryPath(self):
+ return paths.AbsNeighbourFile(__file__, "pous.xml")
+
+ def Generate_C(self, buildpath, varlist, IECCFLAGS):
+ global hmi_tree_root
+
+ """
+ PLC Instance Tree:
+ prog0
+ +->v1 HMI_INT
+ +->v2 HMI_INT
+ +->fb0 (type mhoo)
+ | +->va HMI_LABEL
+ | +->v3 HMI_INT
+ | +->v4 HMI_INT
+ |
+ +->fb1 (type mhoo)
+ | +->va HMI_LABEL
+ | +->v3 HMI_INT
+ | +->v4 HMI_INT
+ |
+ +->fb2
+ +->v5 HMI_IN
+
+ HMI tree:
+ hmi0
+ +->v1
+ +->v2
+ +->fb0_va
+ | +-> v3
+ | +-> v4
+ |
+ +->fb1_va
+ | +-> v3
+ | +-> v4
+ |
+ +->v5
+
+ """
+
+ # Filter known HMI types
+ hmi_types_instances = [v for v in varlist if v["derived"] in HMI_TYPES]
+
+ hmi_tree_root = HMITreeNode(None, "/", "HMI_ROOT")
+
+ # add special nodes
+ map(lambda (n,t): hmi_tree_root.children.append(HMITreeNode(None,n,t)), [
+ ("plc_status", "HMI_PLC_STATUS"),
+ ("current_page", "HMI_CURRENT_PAGE")])
+
+ # deduce HMI tree from PLC HMI_* instances
+ for v in hmi_types_instances:
+ path = v["C_path"].split(".")
+ # ignores variables starting with _TMP_
+ if path[-1].startswith("_TMP_"):
+ continue
+ new_node = HMITreeNode(path, path[-1], v["derived"], v["type"], v["vartype"])
+ hmi_tree_root.place_node(new_node)
+
+ variable_decl_array = []
+ extern_variables_declarations = []
+ buf_index = 0
+ item_count = 0
+ for node in hmi_tree_root.traverse():
+ if hasattr(node, "iectype"):
+ sz = DebugTypesSize.get(node.iectype, 0)
+ variable_decl_array += [
+ "{&(" + ".".join(node.path) + "), " + node.iectype + {
+ "EXT": "_P_ENUM",
+ "IN": "_P_ENUM",
+ "MEM": "_O_ENUM",
+ "OUT": "_O_ENUM",
+ "VAR": "_ENUM"
+ }[node.vartype] + ", " +
+ str(buf_index) + ", 0, }"]
+ buf_index += sz
+ item_count += 1
+ if len(node.path) == 1:
+ extern_variables_declarations += [
+ "extern __IEC_" + node.iectype + "_" +
+ "t" if node.vartype is "VAR" else "p"
+ + ".".join(node.path) + ";"]
+
+ # TODO : filter only requiered external declarations
+ for v in varlist :
+ if v["C_path"].find('.') < 0 and v["vartype"] == "FB" :
+ extern_variables_declarations += [
+ "extern %(type)s %(C_path)s;" % v]
+
+ # TODO check if programs need to be declared separately
+ # "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;" %
+ # p for p in self._ProgramList]),
+
+ # C code to observe/access HMI tree variables
+ svghmi_c_filepath = paths.AbsNeighbourFile(__file__, "svghmi.c")
+ svghmi_c_file = open(svghmi_c_filepath, 'r')
+ svghmi_c_code = svghmi_c_file.read()
+ svghmi_c_file.close()
+ svghmi_c_code = svghmi_c_code % {
+ "variable_decl_array": ",\n".join(variable_decl_array),
+ "extern_variables_declarations": "\n".join(extern_variables_declarations),
+ "buffer_size": buf_index,
+ "item_count": item_count,
+ "var_access_code": targets.GetCode("var_access.c"),
+ "PLC_ticktime": self.GetCTR().GetTicktime()
+ }
+
+ gen_svghmi_c_path = os.path.join(buildpath, "svghmi.c")
+ gen_svghmi_c = open(gen_svghmi_c_path, 'w')
+ gen_svghmi_c.write(svghmi_c_code)
+ gen_svghmi_c.close()
+
+ # Python based WebSocket HMITree Server
+ svghmiserverfile = open(paths.AbsNeighbourFile(__file__, "svghmi_server.py"), 'r')
+ svghmiservercode = svghmiserverfile.read()
+ svghmiserverfile.close()
+
+ runtimefile_path = os.path.join(buildpath, "runtime_svghmi.py")
+ runtimefile = open(runtimefile_path, 'w')
+ runtimefile.write(svghmiservercode)
+ runtimefile.close()
+
+ return ((["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "",
+ ("runtime_svghmi0.py", open(runtimefile_path, "rb")))
+
+class SVGHMI(object):
+ XSD = """<?xml version="1.0" encoding="utf-8" ?>
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <xsd:element name="SVGHMI">
+ <xsd:complexType>
+ <xsd:attribute name="enableHTTP" type="xsd:boolean" use="optional" default="false"/>
+ <xsd:attribute name="bindAddress" type="xsd:string" use="optional" default="localhost"/>
+ <xsd:attribute name="port" type="xsd:string" use="optional" default="8080"/>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ """
+
+ ConfNodeMethods = [
+ {
+ "bitmap": "ImportSVG",
+ "name": _("Import SVG"),
+ "tooltip": _("Import SVG"),
+ "method": "_ImportSVG"
+ },
+ {
+ "bitmap": "ImportSVG", # should be something different
+ "name": _("Inkscape"),
+ "tooltip": _("Edit HMI"),
+ "method": "_StartInkscape"
+ },
+
+ # TODO : HMITree button
+ # - can drag'n'drop variabes to Inkscape
+
+ ]
+
+ def _getSVGpath(self, project_path=None):
+ if project_path is None:
+ project_path = self.CTNPath()
+ return os.path.join(project_path, "svghmi.svg")
+
+
+ def OnCTNSave(self, from_project_path=None):
+ if from_project_path is not None:
+ shutil.copyfile(self._getSVGpath(from_project_path),
+ self._getSVGpath())
+ return True
+
+ def GetSVGGeometry(self):
+ # invoke inskscape -S, csv-parse output, produce elements
+ InkscapeGeomColumns = ["Id", "x", "y", "w", "h"]
+
+ inkpath = get_inkscape_path()
+ svgpath = self._getSVGpath()
+ _status, result, _err_result = ProcessLogger(None,
+ inkpath + " -S " + svgpath,
+ no_stdout=True,
+ no_stderr=True).spin()
+ res = []
+ for line in result.split():
+ strippedline = line.strip()
+ attrs = dict(
+ zip(InkscapeGeomColumns, line.strip().split(',')))
+
+ res.append(etree.Element("bbox", **attrs))
+
+ return res
+
+ def GetHMITree(self):
+ global hmi_tree_root
+ res = [hmi_tree_root.etree()]
+ return res
+
+ def CTNGenerate_C(self, buildpath, locations):
+ """
+ Return C code generated by iec2c compiler
+ when _generate_softPLC have been called
+ @param locations: ignored
+ @return: [(C_file_name, CFLAGS),...] , LDFLAGS_TO_APPEND
+ """
+
+ location_str = "_".join(map(str, self.GetCurrentLocation()))
+ view_name = self.BaseParams.getName()
+
+ svgfile = self._getSVGpath()
+
+ res = ([], "", False)
+
+ target_fname = "sghmi_"+location_str+".xhtml"
+
+ target_path = os.path.join(self._getBuildPath(), target_fname)
+ target_file = open(target_path, 'w')
+
+ if os.path.exists(svgfile):
+
+ # TODO : move to __init__
+ transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
+ [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
+ ("GetHMITree", lambda *_ignored:self.GetHMITree())])
+
+
+ # load svg as a DOM with Etree
+ svgdom = etree.parse(svgfile)
+
+ # call xslt transform on Inkscape's SVG to generate XHTML
+ result = transform.transform(svgdom)
+
+ result.write(target_file, encoding="utf-8")
+ # print(str(result))
+ # print(transform.xslt.error_log)
+
+ # TODO
+ # - Errors on HMI semantics
+ # - ... maybe something to have a global view of what is declared in SVG.
+
+ else:
+ # TODO : use default svg that expose the HMI tree as-is
+ target_file.write("""<!DOCTYPE html>
+<html>
+<body>
+<h1> No SVG file provided </h1>
+</body>
+</html>
+""")
+
+ target_file.close()
+
+ res += ((target_fname, open(target_path, "rb")),)
+
+ runtimefile_path = os.path.join(buildpath, "runtime_svghmi1_%s.py" % location_str)
+ runtimefile = open(runtimefile_path, 'w')
+ runtimefile.write("""
+def _runtime_svghmi1_%(location)s_start():
+ svghmi_root.putChild('%(view_name)s',File('%(xhtml)s'))
+
+def _runtime_svghmi1_%(location)s_stop():
+ svghmi_root.delEntity('%(view_name)s')
+
+ """ % {"location": location_str,
+ "xhtml": target_fname,
+ "view_name": view_name})
+
+ runtimefile.close()
+
+ res += (("runtime_svghmi1_%s.py" % location_str, open(runtimefile_path, "rb")),)
+
+ return res
+
+ def _ImportSVG(self):
+ dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a SVG file"), os.getcwd(), "", _("SVG files (*.svg)|*.svg|All files|*.*"), wx.OPEN)
+ if dialog.ShowModal() == wx.ID_OK:
+ svgpath = dialog.GetPath()
+ if os.path.isfile(svgpath):
+ shutil.copy(svgpath, self._getSVGpath())
+ else:
+ self.GetCTRoot().logger.write_error(_("No such SVG file: %s\n") % svgpath)
+ dialog.Destroy()
+
+ def _StartInkscape(self):
+ svgfile = self._getSVGpath()
+ open_inkscape = True
+ if not self.GetCTRoot().CheckProjectPathPerm():
+ dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
+ _("You don't have write permissions.\nOpen Inkscape anyway ?"),
+ _("Open Inkscape"),
+ wx.YES_NO | wx.ICON_QUESTION)
+ open_inkscape = dialog.ShowModal() == wx.ID_YES
+ dialog.Destroy()
+ if open_inkscape:
+ if not os.path.isfile(svgfile):
+ svgfile = None
+ open_svg(svgfile)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/svghmi/svghmi_server.py Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz
+# Copyright (C) 2019: Edouard TISSERANT
+# See COPYING file for copyrights details.
+
+from __future__ import absolute_import
+
+from twisted.web.server import Site
+from twisted.web.resource import Resource
+from twisted.internet import reactor
+from twisted.web.static import File
+
+from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol
+from autobahn.twisted.resource import WebSocketResource
+
+# TODO multiclient :
+# session list lock
+# svghmi_sessions = []
+
+svghmi_session = None
+
+svghmi_send_collect = PLCBinary.svghmi_send_collect
+svghmi_send_collect.restype = ctypes.c_int # error or 0
+svghmi_send_collect.argtypes = [
+ ctypes.POINTER(ctypes.c_uint32), # size
+ ctypes.POINTER(ctypes.c_char_p)] # data ptr
+# TODO multiclient : switch to arrays
+
+svghmi_recv_dispatch = PLCBinary.svghmi_recv_dispatch
+svghmi_recv_dispatch.restype = ctypes.c_int # error or 0
+svghmi_recv_dispatch.argtypes = [
+ ctypes.c_uint32, # size
+ ctypes.c_char_p] # data ptr
+# TODO multiclient : switch to arrays
+
+class HMISession(object):
+ def __init__(self, protocol_instance):
+ global svghmi_session
+
+ # TODO: kill existing session for robustness
+ assert(svghmi_session is None)
+
+ svghmi_session = self
+ self.protocol_instance = protocol_instance
+
+ # TODO multiclient :
+ # svghmi_sessions.append(self)
+ # get a unique bit index amont other svghmi_sessions,
+ # so that we can match flags passed by C->python callback
+
+ def __del__(self):
+ global svghmi_session
+ assert(svghmi_session)
+ svghmi_session = None
+
+ # TODO multiclient :
+ # svghmi_sessions.remove(self)
+
+ def onMessage(self, msg):
+ # pass message to the C side recieve_message()
+ c_string = ctypes.c_char_p(msg)
+ c_string_pointer = ctypes.c_void_p(ctypes.addressof(c_string))
+ svghmi_recv_dispatch(len(msg), msg)
+
+ # TODO multiclient : pass client index as well
+
+
+ def sendMessage(self, msg):
+ self.sendMessage(msg, True)
+
+class HMIProtocol(WebSocketServerProtocol):
+
+ def __init__(self, *args, **kwargs):
+ self._hmi_session = None
+ WebSocketServerProtocol.__init__(self, *args, **kwargs)
+
+ def onOpen(self):
+ self._hmi_session = HMISession(self)
+ print "open"
+
+ def onClose(self, wasClean, code, reason):
+ del self._hmi_session
+ self._hmi_session = None
+ print "close"
+
+ def onMessage(self, msg, isBinary):
+ self._hmi_session.onMessage(msg)
+ print msg
+ #self.sendMessage(msg, binary)
+
+class HMIWebSocketServerFactory(WebSocketServerFactory):
+ protocol = HMIProtocol
+
+svghmi_root = None
+svghmi_listener = None
+svghmi_send_thread = None
+
+def SendThreadProc():
+ global svghmi_session
+ size = ctypes.c_uint32()
+ ptr = ctypes.c_char_p()
+ res = 0
+ while svghmi_send_collect(ctypes.byref(size), ctypes.byref(ptr)) == 0 and \
+ svghmi_session is not None and \
+ svghmi_session.sendMessage(ctypes.string_at(ptr,size)) == 0:
+ pass
+
+ # TODO multiclient : dispatch to sessions
+
+
+
+# Called by PLCObject at start
+def _runtime_svghmi0_start():
+ global svghmi_listener, svghmi_root, svghmi_send_thread
+
+ svghmi_root = Resource()
+ svghmi_root.putChild("ws", WebSocketResource(HMIWebSocketServerFactory()))
+
+ svghmi_listener = reactor.listenTCP(8008, Site(svghmi_root))
+
+ # start a thread that call the C part of SVGHMI
+ svghmi_send_thread = Thread(target=SendThreadProc, name="SVGHMI Send")
+ svghmi_send_thread.start()
+
+
+# Called by PLCObject at stop
+def _runtime_svghmi0_stop():
+ global svghmi_listener, svghmi_root, svghmi_send_thread
+ svghmi_root.delEntity("ws")
+ svghmi_root = None
+ svghmi_listener.stopListening()
+ svghmi_listener = None
+ # plc cleanup calls svghmi_(locstring)_cleanup and unlocks send thread
+ svghmi_send_thread.join()
+ svghmi_send_thread = None
+
--- a/targets/Linux/plc_Linux_main_retain.c Tue Sep 24 11:55:59 2019 +0200
+++ b/targets/Linux/plc_Linux_main_retain.c Tue Sep 24 11:58:31 2019 +0200
@@ -105,20 +105,20 @@
/* Compare current hash with hash from file byte by byte. */
int CheckFilehash(void)
{
- int k;
+ int k,ret;
int offset = sizeof(retain_info.retain_size);
rewind(retain_buffer);
fseek(retain_buffer, offset , SEEK_SET);
uint32_t size;
- fread(&size, sizeof(size), 1, retain_buffer);
+ ret = fread(&size, sizeof(size), 1, retain_buffer);
if (size != retain_info.hash_size)
return 0;
for(k = 0; k < retain_info.hash_size; k++){
uint8_t file_digit;
- fread(&file_digit, sizeof(char), 1, retain_buffer);
+ ret = fread(&file_digit, sizeof(char), 1, retain_buffer);
if (file_digit != *(retain_info.hash+k))
return 0;
}
@@ -317,8 +317,9 @@
void Remind(unsigned int offset, unsigned int count, void *p)
{
+ int ret;
/* Remind variable from file. */
fseek(retain_buffer, retain_info.header_offset+offset, SEEK_SET);
- fread((void *)p, count, 1, retain_buffer);
+ ret = fread((void *)p, count, 1, retain_buffer);
}
#endif // !HAVE_RETAIN
--- a/targets/plc_debug.c Tue Sep 24 11:55:59 2019 +0200
+++ b/targets/plc_debug.c Tue Sep 24 11:58:31 2019 +0200
@@ -71,37 +71,9 @@
}
}
-#define __Unpack_case_t(TYPENAME) \
- case TYPENAME##_ENUM :\
- *flags = ((__IEC_##TYPENAME##_t *)varp)->flags;\
- forced_value_p = *real_value_p = &((__IEC_##TYPENAME##_t *)varp)->value;\
- break;
-
-#define __Unpack_case_p(TYPENAME)\
- case TYPENAME##_O_ENUM :\
- *flags = __IEC_OUTPUT_FLAG;\
- case TYPENAME##_P_ENUM :\
- *flags |= ((__IEC_##TYPENAME##_p *)varp)->flags;\
- *real_value_p = ((__IEC_##TYPENAME##_p *)varp)->value;\
- forced_value_p = &((__IEC_##TYPENAME##_p *)varp)->fvalue;\
- break;
-
-void* UnpackVar(dbgvardsc_t *dsc, void **real_value_p, char *flags)
-{
- void *varp = dsc->ptr;
- void *forced_value_p = NULL;
- *flags = 0;
- /* find data to copy*/
- switch(dsc->type){
- __ANY(__Unpack_case_t)
- __ANY(__Unpack_case_p)
- default:
- break;
- }
- if (*flags & __IEC_FORCE_FLAG)
- return forced_value_p;
- return *real_value_p;
-}
+#define __Unpack_desc_type dbgvardsc_t
+
+%(var_access_code)s
void Remind(unsigned int offset, unsigned int count, void * p);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/targets/var_access.c Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,33 @@
+
+#define __Unpack_case_t(TYPENAME) \
+ case TYPENAME##_ENUM :\
+ *flags = ((__IEC_##TYPENAME##_t *)varp)->flags;\
+ forced_value_p = *real_value_p = &((__IEC_##TYPENAME##_t *)varp)->value;\
+ break;
+
+#define __Unpack_case_p(TYPENAME)\
+ case TYPENAME##_O_ENUM :\
+ *flags = __IEC_OUTPUT_FLAG;\
+ case TYPENAME##_P_ENUM :\
+ *flags |= ((__IEC_##TYPENAME##_p *)varp)->flags;\
+ *real_value_p = ((__IEC_##TYPENAME##_p *)varp)->value;\
+ forced_value_p = &((__IEC_##TYPENAME##_p *)varp)->fvalue;\
+ break;
+
+static void* UnpackVar(__Unpack_desc_type *dsc, void **real_value_p, char *flags)
+{
+ void *varp = dsc->ptr;
+ void *forced_value_p = NULL;
+ *flags = 0;
+ /* find data to copy*/
+ switch(dsc->type){
+ __ANY(__Unpack_case_t)
+ __ANY(__Unpack_case_p)
+ default:
+ break;
+ }
+ if (*flags & __IEC_FORCE_FLAG)
+ return forced_value_p;
+ return *real_value_p;
+}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/svghmi/beremiz.xml Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="LOCAL://">
+ <TargetType/>
+ <Libraries Enable_SVGHMI_Library="true"/>
+</BeremizRoot>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/svghmi/plc.xml Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,241 @@
+<?xml version='1.0' encoding='utf-8'?>
+<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
+ <fileHeader companyName="Unknown" productName="Unnamed" productVersion="1" creationDateTime="2019-08-06T14:23:42"/>
+ <contentHeader name="Unnamed" modificationDateTime="2019-09-11T11:54:14">
+ <coordinateInfo>
+ <fbd>
+ <scaling x="5" y="5"/>
+ </fbd>
+ <ld>
+ <scaling x="0" y="0"/>
+ </ld>
+ <sfc>
+ <scaling x="0" y="0"/>
+ </sfc>
+ </coordinateInfo>
+ </contentHeader>
+ <types>
+ <dataTypes/>
+ <pous>
+ <pou name="MainStuff" pouType="program">
+ <interface>
+ <localVars>
+ <variable name="TargetPressure">
+ <type>
+ <derived name="HMI_INT"/>
+ </type>
+ </variable>
+ <variable name="Pump0">
+ <type>
+ <derived name="PumpControl"/>
+ </type>
+ </variable>
+ </localVars>
+ </interface>
+ <body>
+ <FBD>
+ <block localId="2" typeName="ADD" executionOrderId="0" height="60" width="63">
+ <position x="255" y="175"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="1">
+ <position x="255" y="205"/>
+ <position x="202" y="205"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="3">
+ <position x="255" y="225"/>
+ <position x="235" y="225"/>
+ <position x="235" y="235"/>
+ <position x="190" y="235"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="63" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inOutVariable localId="1" executionOrderId="0" height="26" width="122" negatedOut="false" negatedIn="false">
+ <position x="120" y="190"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="2" formalParameter="OUT">
+ <position x="120" y="205"/>
+ <position x="100" y="205"/>
+ <position x="100" y="140"/>
+ <position x="328" y="140"/>
+ <position x="328" y="205"/>
+ <position x="318" y="205"/>
+ </connection>
+ </connectionPointIn>
+ <connectionPointOut>
+ <relPosition x="122" y="15"/>
+ </connectionPointOut>
+ <expression>TargetPressure</expression>
+ </inOutVariable>
+ <inVariable localId="3" executionOrderId="0" height="25" width="60" negated="false">
+ <position x="130" y="225"/>
+ <connectionPointOut>
+ <relPosition x="60" y="10"/>
+ </connectionPointOut>
+ <expression>1</expression>
+ </inVariable>
+ <block localId="4" typeName="PumpControl" instanceName="Pump0" executionOrderId="0" height="40" width="127">
+ <position x="595" y="50"/>
+ <inputVariables>
+ <variable formalParameter="TargetPressure">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="5">
+ <position x="595" y="80"/>
+ <position x="570" y="80"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables/>
+ </block>
+ <inVariable localId="5" executionOrderId="0" height="30" width="125" negated="false">
+ <position x="445" y="65"/>
+ <connectionPointOut>
+ <relPosition x="125" y="15"/>
+ </connectionPointOut>
+ <expression>TargetPressure</expression>
+ </inVariable>
+ </FBD>
+ </body>
+ </pou>
+ <pou name="PumpControl" pouType="functionBlock">
+ <interface>
+ <localVars>
+ <variable name="Pump">
+ <type>
+ <derived name="HMI_LABEL"/>
+ </type>
+ </variable>
+ <variable name="Pressure">
+ <type>
+ <derived name="HMI_INT"/>
+ </type>
+ </variable>
+ </localVars>
+ <inputVars>
+ <variable name="TargetPressure">
+ <type>
+ <INT/>
+ </type>
+ </variable>
+ </inputVars>
+ <localVars>
+ <variable name="AddOut">
+ <type>
+ <derived name="HMI_INT"/>
+ </type>
+ </variable>
+ </localVars>
+ </interface>
+ <body>
+ <FBD>
+ <inOutVariable localId="1" executionOrderId="0" height="30" width="75" negatedOut="false" negatedIn="false">
+ <position x="285" y="105"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="2" formalParameter="OUT">
+ <position x="285" y="120"/>
+ <position x="275" y="120"/>
+ <position x="275" y="95"/>
+ <position x="550" y="95"/>
+ <position x="550" y="135"/>
+ <position x="540" y="135"/>
+ </connection>
+ </connectionPointIn>
+ <connectionPointOut>
+ <relPosition x="75" y="15"/>
+ </connectionPointOut>
+ <expression>Pressure</expression>
+ </inOutVariable>
+ <block localId="2" typeName="ADD" executionOrderId="0" height="60" width="65">
+ <position x="475" y="105"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="1">
+ <position x="475" y="135"/>
+ <position x="417" y="135"/>
+ <position x="417" y="120"/>
+ <position x="360" y="120"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="3">
+ <position x="475" y="155"/>
+ <position x="432" y="155"/>
+ <position x="432" y="150"/>
+ <position x="410" y="150"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="65" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inVariable localId="3" executionOrderId="0" height="25" width="30" negated="false">
+ <position x="380" y="140"/>
+ <connectionPointOut>
+ <relPosition x="30" y="10"/>
+ </connectionPointOut>
+ <expression>23</expression>
+ </inVariable>
+ <outVariable localId="4" executionOrderId="0" height="30" width="60" negated="false">
+ <position x="640" y="135"/>
+ <connectionPointIn>
+ <relPosition x="0" y="15"/>
+ <connection refLocalId="2" formalParameter="OUT">
+ <position x="640" y="150"/>
+ <position x="590" y="150"/>
+ <position x="590" y="135"/>
+ <position x="540" y="135"/>
+ </connection>
+ </connectionPointIn>
+ <expression>AddOut</expression>
+ </outVariable>
+ </FBD>
+ </body>
+ </pou>
+ </pous>
+ </types>
+ <instances>
+ <configurations>
+ <configuration name="config">
+ <resource name="resource1">
+ <task name="task0" priority="0" interval="T#20ms">
+ <pouInstance name="instance0" typeName="MainStuff"/>
+ </task>
+ </resource>
+ </configuration>
+ </configurations>
+ </instances>
+</project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/svghmi/svghmi_0@svghmi/baseconfnode.xml Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="0" Name="svghmi_0"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/svghmi/svghmi_0@svghmi/confnode.xml Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SVGHMI xmlns:xsd="http://www.w3.org/2001/XMLSchema"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/svghmi/svghmi_0@svghmi/svghmi.svg Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="1280"
+ height="720"
+ viewBox="0 0 1280 720"
+ version="1.1"
+ id="hmi0"
+ sodipodi:docname="gui.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata4542">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:document-units="px"
+ inkscape:current-layer="hmi0"
+ showgrid="false"
+ units="px"
+ inkscape:zoom="0.7"
+ inkscape:cx="586.09435"
+ inkscape:cy="381.71289"
+ inkscape:window-width="1600"
+ inkscape:window-height="886"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1" />
+ <rect
+ style="color:#000000"
+ id="page0"
+ width="1280"
+ height="720"
+ x="0"
+ y="0">
+ <desc
+ id="desc_page0">=HMI=
+role = Page;
+name = "Home";
+</desc>
+ </rect>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ x="136.32812"
+ y="418.24219"
+ id="text5151"
+ inkscape:label="count"><desc
+ id="desc5153">=HMI=
+role = Input;
+path = "/PRESSURETARGET";
+/* Don't forget to press the "Set" button ! */</desc><tspan
+ sodipodi:role="line"
+ id="tspan5149"
+ x="136.32812"
+ y="418.24219"
+ style="stroke-width:1px">8888</tspan></text>
+ <text
+ inkscape:label="count"
+ id="text823"
+ y="418.24219"
+ x="756.32812"
+ style="font-style:normal;font-weight:normal;font-size:160px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ xml:space="preserve"><desc
+ id="desc819">=HMI=
+role = Display;
+path = "/PUMP/PRESSURE";
+/* Don't forget to press the "Set" button ! */</desc><tspan
+ style="stroke-width:1px"
+ y="418.24219"
+ x="756.32812"
+ id="tspan821"
+ sodipodi:role="line">8888</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
+ x="242.85715"
+ y="197.14285"
+ id="text827"><tspan
+ sodipodi:role="line"
+ id="tspan825"
+ x="242.85715"
+ y="197.14285">SetPoint</tspan></text>
+ <text
+ id="text831"
+ y="197.14285"
+ x="882.85718"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
+ xml:space="preserve"><tspan
+ y="197.14285"
+ x="882.85718"
+ id="tspan829"
+ sodipodi:role="line">Actual</tspan></text>
+</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/yslt_noindent.yml2 Tue Sep 24 11:58:31 2019 +0200
@@ -0,0 +1,40 @@
+include yslt.yml2
+!!
+def indent(level):
+ return ""
+!!
+
+in xsl {
+ decl istylesheet (
+ *output="xml",
+ version="1.0",
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform",
+ xmlns:exsl='http://exslt.org/common',
+ xmlns:regexp="http://exslt.org/regular-expressions",
+ xmlns:str="http://exslt.org/strings",
+ extension-element-prefixes='exsl regexp str'
+ ) alias stylesheet {
+ output *output;
+ content;
+ };
+
+ decl indent() alias -;
+
+ decl template(match) {
+ content;
+ };
+
+ decl function(name) alias template {
+ content;
+ };
+
+ decl call(name) alias call-template {
+ content;
+ };
+
+ decl apply(select) alias apply-templates {
+ content;
+ };
+}
+
+