# This file is part of Beremiz
# Copyright (C) 2019: Edouard TISSERANT
# See COPYING file for copyrights details.
from __future__ import absolute_import
from itertools import izip, imap
from pprint import pformat
from lxml.etree import XSLTApplyError
from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
import util.paths as paths
from POULibrary import POULibrary
from docutil import open_svg, get_inkscape_path
from util.ProcessLogger import ProcessLogger
from runtime.typemapping import DebugTypesSize
from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
from XSLTransform import XSLTransform
from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations, MatchTranslations, TranslationToEtree, open_pofile
HMI_TYPES = HMI_TYPES_DESC.keys()
ScriptDirectory = paths.AbsDir(__file__)
class HMITreeNode(object):
def __init__(self, path, name, nodetype, iectype = None, vartype = None, cpath = None, hmiclass = None):
if nodetype in ["HMI_NODE"]:
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])
def place_node(self, node):
for child in self.children:
if child.path is not None:
for child_path_item, node_path_item in izip(child.path, node.path):
if child_path_item == node_path_item:
# Match can only be HMI_NODE, and the whole path of node
# must match candidate node (except for name part)
# since candidate would become child of that node
if in_common > known_best_match and \
child.nodetype == "HMI_NODE" and \
in_common == len(child.path) - 1:
known_best_match = in_common
potential_siblings[child.path[
-2 if child.nodetype == "HMI_NODE" else -1]] = child
if best_child is not None:
if node.nodetype == "HMI_NODE" and best_child.path[:-1] == node.path[:-1]:
return "Duplicate_HMI_NODE", best_child
return best_child.place_node(node)
candidate_name = node.path[-2 if node.nodetype == "HMI_NODE" else -1]
if candidate_name in potential_siblings:
return "Non_Unique", potential_siblings[candidate_name]
if node.nodetype == "HMI_NODE" and len(self.children) > 0:
if prev.path[:-1] == node.path[:-1]:
return "Late_HMI_NODE",prev
self.children.append(node)
def etree(self, add_hash=False):
attribs = dict(name=self.name)
if self.path is not None:
attribs["path"] = ".".join(self.path)
if self.hmiclass is not None:
attribs["class"] = self.hmiclass
attribs["hash"] = ",".join(map(str,self.hash()))
res = etree.Element(self.nodetype, **attribs)
if hasattr(self, "children"):
for child_etree in imap(lambda c:c.etree(), self.children):
def from_etree(cls, enode):
alternative constructor, restoring HMI Tree from XML backup
note: all C-related information is gone,
this restore is only for tree display and widget picking
attributes = enode.attrib
name = attributes["name"]
path = attributes["path"].split('.') if "path" in attributes else None
hmiclass = attributes.get("class", None)
# hash is computed on demand
node = cls(path, name, nodetype, hmiclass=hmiclass)
for child in enode.iterchildren():
node.children.append(cls.from_etree(child))
if hasattr(self, "children"):
for yoodl in c.traverse():
""" Produce a hash, any change in HMI tree structure change that hash """
# limit size to HMI_HASH_SIZE as in svghmi.c
return map(ord,s.digest())[:8]
s.update(str((self.name,self.nodetype)))
if hasattr(self, "children"):
# 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
SPECIAL_NODES = [("HMI_ROOT", "HMI_NODE"),
("heartbeat", "HMI_INT")]
# ("current_page", "HMI_STRING")])
class SVGHMILibrary(POULibrary):
def GetLibraryPath(self):
return paths.AbsNeighbourFile(__file__, "pous.xml")
def Generate_C(self, buildpath, varlist, IECCFLAGS):
global hmi_tree_root, on_hmitree_update
hmi_types_instances = [v for v in varlist if v["derived"] in HMI_TYPES]
# take first HMI_NODE (placed as special node), make it root
for i,v in enumerate(hmi_types_instances):
path = v["IEC_path"].split(".")
if derived == "HMI_NODE":
hmi_tree_root = HMITreeNode(path, "", derived, v["type"], v["vartype"], v["C_path"])
hmi_types_instances.pop(i)
if hmi_tree_root is None:
self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.")
# deduce HMI tree from PLC HMI_* instances
for v in hmi_types_instances:
path = v["IEC_path"].split(".")
# ignores variables starting with _TMP_
if path[-1].startswith("_TMP_"):
if derived == "HMI_NODE":
# TODO : make problem if HMI_NODE used in CONFIG or RESOURCE
kwargs['hmiclass'] = path[-1]
new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs)
placement_result = hmi_tree_root.place_node(new_node)
if placement_result is not None:
cause, problematic_node = placement_result
if cause == "Non_Unique":
message = _("HMI tree nodes paths are not unique.\nConflicting variable: {} {}").format(
".".join(problematic_node.path),
if v["C_path"] == problematic_node:
failing_parent = last_FB["type"]
message += _("Solution: Add HMI_NODE at beginning of {}").format(failing_parent)
elif cause in ["Late_HMI_NODE", "Duplicate_HMI_NODE"]:
cause, problematic_node = placement_result
message = _("There must be only one occurrence of HMI_NODE before any HMI_* variable in POU.\nConflicting variable: {} {}").format(
".".join(problematic_node.path),
self.FatalError("SVGHMI : " + message)
if on_hmitree_update is not None:
extern_variables_declarations = []
hearbeat_IEC_path = ['CONFIG', 'HEARTBEAT']
for node in hmi_tree_root.traverse():
if not found_heartbeat and node.path == hearbeat_IEC_path:
hmi_tree_hearbeat_index = item_count
extern_variables_declarations += [
"#define heartbeat_index "+str(hmi_tree_hearbeat_index)
if hasattr(node, "iectype"):
sz = DebugTypesSize.get(node.iectype, 0)
"{&(" + node.cpath + "), " + node.iectype + {
str(buf_index) + ", 0, }"]
extern_variables_declarations += [
"extern __IEC_" + node.iectype + "_" +
"t" if node.vartype is "VAR" else "p"
# TODO : filter only requiered external declarations
if v["C_path"].find('.') < 0:
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_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(),
"hmi_hash_ints": ",".join(map(str,hmi_tree_root.hash()))
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)
# Python based WebSocket HMITree Server
svghmiserverfile = open(paths.AbsNeighbourFile(__file__, "svghmi_server.py"), 'r')
svghmiservercode = svghmiserverfile.read()
runtimefile_path = os.path.join(buildpath, "runtime_00_svghmi.py")
runtimefile = open(runtimefile_path, 'w')
runtimefile.write(svghmiservercode)
# Backup HMI Tree in XML form so that it can be loaded without building
hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
hmitree_backup_file = open(hmitree_backup_path, 'wb')
hmitree_backup_file.write(etree.tostring(hmi_tree_root.etree()))
hmitree_backup_file.close()
return ((["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "",
("runtime_00_svghmi.py", open(runtimefile_path, "rb")))
# note the double zero after "runtime_",
# to ensure placement before other CTN generated code in execution order
def SVGHMIEditorUpdater(ref):
def SVGHMIEditorUpdate():
return SVGHMIEditorUpdate
class HMITreeSelector(wx.TreeCtrl):
def __init__(self, parent):
wx.TreeCtrl.__init__(self, parent, style=(
on_hmitree_update = SVGHMIEditorUpdater(weakref.ref(self))
def _recurseTree(self, current_hmitree_root, current_tc_root):
for c in current_hmitree_root.children:
if hasattr(c, "children"):
display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
if c.hmiclass is not None else c.name
tc_child = self.AppendItem(current_tc_root, display_name)
self.SetPyData(tc_child, None) # TODO
self._recurseTree(c,tc_child)
display_name = '{} {}'.format(c.nodetype[4:], c.name)
tc_child = self.AppendItem(current_tc_root, display_name)
self.SetPyData(tc_child, None) # TODO
root_display_name = _("Please build to see HMI Tree") \
if hmi_tree_root is None else "HMI"
self.root = self.AddRoot(root_display_name)
self.SetPyData(self.root, None)
if hmi_tree_root is not None:
self._recurseTree(hmi_tree_root, self.root)
class WidgetPicker(wx.TreeCtrl):
def __init__(self, parent, initialdir=None):
wx.TreeCtrl.__init__(self, parent, style=(
self.MakeTree(initialdir)
def _recurseTree(self, current_dir, current_tc_root, dirlist):
recurse through subdirectories, but creates tree nodes
only when (sub)directory conbtains .svg file
for f in sorted(os.listdir(current_dir)):
p = os.path.join(current_dir,f)
r = self._recurseTree(p, current_tc_root, dirlist + [f])
current_tc_root = res.pop()
elif os.path.splitext(f)[1].upper() == ".SVG":
current_tc_root = self.AppendItem(current_tc_root, d)
res.append(current_tc_root)
self.SetPyData(current_tc_root, None)
tc_child = self.AppendItem(current_tc_root, f)
self.SetPyData(tc_child, p)
def MakeTree(self, lib_dir = None):
root_display_name = _("Please select widget library directory") \
if lib_dir is None else os.path.basename(lib_dir)
self.root = self.AddRoot(root_display_name)
self.SetPyData(self.root, None)
self._recurseTree(lib_dir, self.root, [])
_conf_key = "SVGHMIWidgetLib"
class WidgetLibBrowser(wx.Panel):
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
wx.Panel.__init__(self, parent, id, pos, size)
self.Config = wx.ConfigBase.Get()
self.libdir = self.RecallLibDir()
sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0)
self.libbutton = wx.Button(self, -1, _("Select SVG widget library"))
self.widgetpicker = WidgetPicker(self, self.libdir)
self.preview = wx.Panel(self, size=(-1, _preview_height + 10)) #, style=wx.SIMPLE_BORDER)
#self.preview.SetBackgroundColour(wx.WHITE)
sizer.AddWindow(self.libbutton, flag=wx.GROW)
sizer.AddWindow(self.widgetpicker, flag=wx.GROW)
sizer.AddWindow(self.preview, flag=wx.GROW)
self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
self.msg = _("Drag selected Widget from here to Inkscape")
conf = self.Config.Read(_conf_key)
return DecodeFileSystemPath(conf)
def RememberLibDir(self, path):
self.Config.Write(_conf_key,
EncodeFileSystemPath(path))
# Init preview panel paint device context
dc = wx.PaintDC(self.preview)
sz = self.preview.GetClientSize()
dc.DrawBitmap(self.bmp, (sz.width - w)/2, 5)
dc.SetFont(self.GetFont())
dc.DrawText(self.msg, 25,25)
def OnSelectLibDir(self, event):
defaultpath = self.RecallLibDir()
defaultpath = os.path.expanduser("~")
dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
if dialog.ShowModal() == wx.ID_OK:
self.libdir = dialog.GetPath()
self.RememberLibDir(self.libdir)
self.widgetpicker.MakeTree(self.libdir)
def OnPaint(self, event):
Called when Preview panel needs to be redrawn
@param event: wx.PaintEvent
def GenThumbnail(self, svgpath, thumbpath):
inkpath = get_inkscape_path()
self.msg = _("Inkscape is not installed.")
# TODO: spawn a thread, to decouple thumbnail gen
status, result, _err_result = ProcessLogger(
'"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
'" -D -h ' + str(_preview_height)).spin()
self.msg = _("Inkscape couldn't generate thumbnail.")
def OnWidgetSelection(self, event):
Called when tree item is selected
@param event: wx.TreeEvent
item_pydata = self.widgetpicker.GetPyData(event.GetItem())
if item_pydata is not None:
dname = os.path.dirname(svgpath)
fname = os.path.basename(svgpath)
hasher = hashlib.new('md5')
with open(svgpath, 'rb') as afile:
digest = hasher.hexdigest()
thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
thumbdir = os.path.join(dname, ".svghmithumbs")
thumbpath = os.path.join(thumbdir, thumbfname)
have_thumb = os.path.exists(thumbpath)
if not os.path.exists(thumbdir):
self.msg = _("Widget library must be writable")
have_thumb = self.GenThumbnail(svgpath, thumbpath)
self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
self.selected_SVG = svgpath if have_thumb else None
def OnHMITreeNodeSelection(self, hmitree_node):
self.hmitree_node = hmitree_node
def ValidateWidget(self):
if self.selected_SVG is not None:
if self.hmitree_node is not None:
# - check SVG is valid for selected HMI tree item
class HMITreeView(wx.SplitterWindow):
def __init__(self, parent):
wx.SplitterWindow.__init__(self, parent,
style=wx.SUNKEN_BORDER | wx.SP_3D)
self.SelectionTree = HMITreeSelector(self)
self.Staging = WidgetLibBrowser(self)
self.SplitVertically(self.SelectionTree, self.Staging, 300)
class SVGHMIEditor(ConfTreeNodeEditor):
(_("HMI Tree"), "CreateHMITreeView")]
def CreateHMITreeView(self, parent):
if hmi_tree_root is None:
buildpath = self.Controler.GetCTRoot()._getBuildPath()
hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
if os.path.exists(hmitree_backup_path):
hmitree_backup_file = open(hmitree_backup_path, 'rb')
hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
#self.HMITreeView = HMITreeView(self)
#return HMITreeSelector(parent)
return HMITreeView(parent)
XSD = """<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="SVGHMI">
<xsd:attribute name="OnStart" type="xsd:string" use="optional"/>
<xsd:attribute name="OnStop" type="xsd:string" use="optional"/>
<xsd:attribute name="OnWatchdog" type="xsd:string" use="optional"/>
<xsd:attribute name="WatchdogInitial" type="xsd:integer" use="optional"/>
<xsd:attribute name="WatchdogInterval" type="xsd:integer" use="optional"/>
EditorType = SVGHMIEditor
"tooltip": _("Import SVG"),
"bitmap": "EditSVG", # should be something different
"tooltip": _("Edit HMI"),
"method": "_StartInkscape"
"bitmap": "OpenPOT", # should be something different
"tooltip": _("Open non translated message catalog (POT) to start new language"),
"bitmap": "EditPO", # should be something different
"tooltip": _("Edit existing message catalog (PO) for specific language"),
# - can drag'n'drop variabes to Inkscape
def _getSVGpath(self, project_path=None):
project_path = self.CTNPath()
return os.path.join(project_path, "svghmi.svg")
def _getPOTpath(self, project_path=None):
project_path = self.CTNPath()
return os.path.join(project_path, "messages.pot")
def OnCTNSave(self, from_project_path=None):
if from_project_path is not None:
shutil.copyfile(self._getSVGpath(from_project_path),
shutil.copyfile(self._getPOTpath(from_project_path),
# XXX TODO copy .PO files
def GetSVGGeometry(self):
self.ProgressStart("inkscape", "collecting SVG geometry (Inkscape)")
# invoke inskscape -S, csv-parse output, produce elements
InkscapeGeomColumns = ["Id", "x", "y", "w", "h"]
inkpath = get_inkscape_path()
self.FatalError("SVGHMI: inkscape is not installed.")
svgpath = self._getSVGpath()
status, result, _err_result = ProcessLogger(self.GetCTRoot().logger,
'"' + inkpath + '" -S "' + svgpath + '"',
self.FatalError("SVGHMI: inkscape couldn't extract geometry from given SVG.")
for line in result.split():
strippedline = line.strip()
zip(InkscapeGeomColumns, line.strip().split(',')))
res.append(etree.Element("bbox", **attrs))
self.ProgressEnd("inkscape")
self.ProgressStart("hmitree", "getting HMI tree")
res = [hmi_tree_root.etree(add_hash=True)]
self.ProgressEnd("hmitree")
def GetTranslations(self, _context, msgs):
self.ProgressStart("i18n", "getting Translations")
messages = EtreeToMessages(msgs)
SaveCatalog(self._getPOTpath(), messages)
translations = ReadTranslations(self.CTNPath())
langs,translated_messages = MatchTranslations(translations, messages,
errcallback=self.GetCTRoot().logger.write_warning)
ret = TranslationToEtree(langs,translated_messages)
def ProgressStart(self, k, m):
self.times_msgs[k] = (time.time(), m)
self.GetCTRoot().logger.write(" "*self.indent + "Start %s...\n"%m)
self.indent = self.indent + 1
def ProgressEnd(self, k):
oldt, m = self.times_msgs[k]
self.indent = self.indent - 1
self.GetCTRoot().logger.write(" "*self.indent + "... finished in %.3fs\n"%(t - oldt))
def CTNGenerate_C(self, buildpath, locations):
location_str = "_".join(map(str, self.GetCurrentLocation()))
view_name = self.BaseParams.getName()
svgfile = self._getSVGpath()
target_fname = "svghmi_"+location_str+".xhtml"
build_path = self._getBuildPath()
target_path = os.path.join(build_path, target_fname)
hash_path = os.path.join(build_path, "svghmi.md5")
self.GetCTRoot().logger.write("SVGHMI:\n")
if os.path.exists(svgfile):
hmi_tree_root._hash(hasher)
with open(svgfile, 'rb') as afile:
digest = hasher.hexdigest()
if os.path.exists(hash_path):
with open(hash_path, 'rb') as digest_file:
last_digest = digest_file.read()
if digest != last_digest:
transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
[("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
("GetHMITree", lambda *_ignored:self.GetHMITree()),
("GetTranslations", self.GetTranslations),
("ProgressStart", lambda _ign,k,m:self.ProgressStart(str(k),str(m))),
("ProgressEnd", lambda _ign,k:self.ProgressEnd(str(k)))])
self.ProgressStart("svg", "source SVG parsing")
# load svg as a DOM with Etree
svgdom = etree.parse(svgfile)
# call xslt transform on Inkscape's SVG to generate XHTML
self.ProgressStart("xslt", "XSLT transform")
result = transform.transform(svgdom) # , profile_run=True)
except XSLTApplyError as e:
self.FatalError("SVGHMI " + view_name + ": " + e.message)
for entry in transform.get_error_log():
message = "SVGHMI: "+ entry.message + "\n"
self.GetCTRoot().logger.write_warning(message)
target_file = open(target_path, 'wb')
result.write(target_file, encoding="utf-8")
# print(transform.xslt.error_log)
# print(etree.tostring(result.xslt_profile,pretty_print=True))
with open(hash_path, 'wb') as digest_file:
digest_file.write(digest)
self.GetCTRoot().logger.write(" No changes - XSLT transformation skipped\n")
target_file = open(target_path, 'wb')
target_file.write("""<!DOCTYPE html>
<h1> No SVG file provided </h1>
res += ((target_fname, open(target_path, "rb")),)
for thing in ["Start", "Stop", "Watchdog"]:
given_command = self.GetParamsAttributes("SVGHMI.On"+thing)["value"]
repr(shlex.split(given_command.format(port="8008", name=view_name))) +
")") if given_command else "pass # no command given"
runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str)
runtimefile = open(runtimefile_path, 'w')
# TODO : multiple watchdog (one for each svghmi instance)
def svghmi_watchdog_trigger():
def _runtime_{location}_svghmi_start():
defaultType='application/xhtml+xml'))
svghmi_watchdog = Watchdog(
def _runtime_{location}_svghmi_stop():
if svghmi_watchdog is not None:
svghmi_root.delEntity('{view_name}')
""".format(location=location_str,
watchdog_initial = self.GetParamsAttributes("SVGHMI.WatchdogInitial")["value"],
watchdog_interval = self.GetParamsAttributes("SVGHMI.WatchdogInterval")["value"],
res += (("runtime_%s_svghmi.py" % location_str, open(runtimefile_path, "rb")),)
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())
self.GetCTRoot().logger.write_error(_("No such SVG file: %s\n") % svgpath)
def _StartInkscape(self):
svgfile = self._getSVGpath()
if not self.GetCTRoot().CheckProjectPathPerm():
dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
_("You don't have write permissions.\nOpen Inkscape anyway ?"),
wx.YES_NO | wx.ICON_QUESTION)
open_inkscape = dialog.ShowModal() == wx.ID_YES
if not os.path.isfile(svgfile):
def _StartPOEdit(self, POFile):
if not self.GetCTRoot().CheckProjectPathPerm():
dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
_("You don't have write permissions.\nOpen POEdit anyway ?"),
wx.YES_NO | wx.ICON_QUESTION)
open_poedit = dialog.ShowModal() == wx.ID_YES
""" Select a specific translation and edit it with POEdit """
project_path = self.CTNPath()
dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "", _("PO files (*.po)|*.po"), wx.OPEN)
if dialog.ShowModal() == wx.ID_OK:
POFile = dialog.GetPath()
if os.path.isfile(POFile):
if os.path.relpath(POFile, project_path) == os.path.basename(POFile):
self._StartPOEdit(POFile)
self.GetCTRoot().logger.write_error(_("PO file misplaced: %s is not in %s\n") % (POFile,project_path))
self.GetCTRoot().logger.write_error(_("PO file does not exist: %s\n") % POFile)
""" Start POEdit with untouched empty catalog """
POFile = self._getPOTpath()
if os.path.isfile(POFile):
self._StartPOEdit(POFile)
self.GetCTRoot().logger.write_error(_("POT file does not exist, add translatable text (label starting with '_') in Inkscape first\n"))
def CTNGlobalInstances(self):
# view_name = self.BaseParams.getName()
# return [ (view_name + "_" + name, iec_type, "") for name, iec_type in SPECIAL_NODES]
# TODO : move to library level for multiple hmi
return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES]