svghmi/svghmi.py
branchsvghmi
changeset 3207 de6b878c324d
parent 3201 6dadc1690284
child 3208 b5330d76e225
equal deleted inserted replaced
3206:4fd7bd10e606 3207:de6b878c324d
     1 #!/usr/bin/env python
     1 #!/usr/bin/env python
     2 # -*- coding: utf-8 -*-
     2 # -*- coding: utf-8 -*-
     3 
     3 
     4 # This file is part of Beremiz
     4 # This file is part of Beremiz
     5 # Copyright (C) 2019: Edouard TISSERANT
     5 # Copyright (C) 2021: Edouard TISSERANT
     6 #
     6 #
     7 # See COPYING file for copyrights details.
     7 # See COPYING file for copyrights details.
     8 
     8 
     9 from __future__ import absolute_import
     9 from __future__ import absolute_import
    10 import os
    10 import os
    11 import shutil
    11 import shutil
    12 from itertools import izip, imap
       
    13 from pprint import pformat
       
    14 import hashlib
    12 import hashlib
    15 import weakref
       
    16 import shlex
    13 import shlex
    17 import time
    14 import time
    18 
    15 
    19 import wx
    16 import wx
    20 
    17 
    21 from lxml import etree
    18 from lxml import etree
    22 from lxml.etree import XSLTApplyError
    19 from lxml.etree import XSLTApplyError
    23 
    20 
    24 from IDEFrame import EncodeFileSystemPath, DecodeFileSystemPath
       
    25 import util.paths as paths
    21 import util.paths as paths
    26 from POULibrary import POULibrary
    22 from POULibrary import POULibrary
    27 from docutil import open_svg, get_inkscape_path
    23 from docutil import open_svg, get_inkscape_path
    28 
    24 
    29 from util.ProcessLogger import ProcessLogger
    25 from util.ProcessLogger import ProcessLogger
    30 from runtime.typemapping import DebugTypesSize
    26 from runtime.typemapping import DebugTypesSize
    31 import targets
    27 import targets
    32 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
    28 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
    33 from XSLTransform import XSLTransform
    29 from XSLTransform import XSLTransform
    34 from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations, MatchTranslations, TranslationToEtree, open_pofile
    30 from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations,\
    35 
    31                         MatchTranslations, TranslationToEtree, open_pofile
    36 HMI_TYPES_DESC = {
    32 from svghmi.hmi_tree import HMI_TYPES, HMITreeNode, SPECIAL_NODES 
    37     "HMI_NODE":{},
    33 from svghmi.ui import SVGHMI_UI
    38     "HMI_STRING":{},
       
    39     "HMI_INT":{},
       
    40     "HMI_BOOL":{},
       
    41     "HMI_REAL":{}
       
    42 }
       
    43 
       
    44 HMI_TYPES = HMI_TYPES_DESC.keys()
       
    45 
    34 
    46 
    35 
    47 ScriptDirectory = paths.AbsDir(__file__)
    36 ScriptDirectory = paths.AbsDir(__file__)
    48 
    37 
    49 class HMITreeNode(object):
       
    50     def __init__(self, path, name, nodetype, iectype = None, vartype = None, cpath = None, hmiclass = None):
       
    51         self.path = path
       
    52         self.name = name
       
    53         self.nodetype = nodetype
       
    54         self.hmiclass = hmiclass
       
    55 
       
    56         if iectype is not None:
       
    57             self.iectype = iectype
       
    58             self.vartype = vartype
       
    59             self.cpath = cpath
       
    60 
       
    61         if nodetype in ["HMI_NODE"]:
       
    62             self.children = []
       
    63 
       
    64     def pprint(self, indent = 0):
       
    65         res = ">"*indent + pformat(self.__dict__, indent = indent, depth = 1) + "\n"
       
    66         if hasattr(self, "children"):
       
    67             res += "\n".join([child.pprint(indent = indent + 1)
       
    68                               for child in self.children])
       
    69             res += "\n"
       
    70 
       
    71         return res
       
    72 
       
    73     def place_node(self, node):
       
    74         best_child = None
       
    75         known_best_match = 0
       
    76         potential_siblings = {}
       
    77         for child in self.children:
       
    78             if child.path is not None:
       
    79                 in_common = 0
       
    80                 for child_path_item, node_path_item in izip(child.path, node.path):
       
    81                     if child_path_item == node_path_item:
       
    82                         in_common +=1
       
    83                     else:
       
    84                         break
       
    85                 # Match can only be HMI_NODE, and the whole path of node
       
    86                 # must match candidate node (except for name part)
       
    87                 # since candidate would become child of that node
       
    88                 if in_common > known_best_match and \
       
    89                    child.nodetype == "HMI_NODE" and \
       
    90                    in_common == len(child.path) - 1:
       
    91                     known_best_match = in_common
       
    92                     best_child = child
       
    93                 else:
       
    94                     potential_siblings[child.path[
       
    95                         -2 if child.nodetype == "HMI_NODE" else -1]] = child
       
    96         if best_child is not None:
       
    97             if node.nodetype == "HMI_NODE" and best_child.path[:-1] == node.path[:-1]:
       
    98                 return "Duplicate_HMI_NODE", best_child
       
    99             return best_child.place_node(node)
       
   100         else:
       
   101             candidate_name = node.path[-2 if node.nodetype == "HMI_NODE" else -1]
       
   102             if candidate_name in potential_siblings:
       
   103                 return "Non_Unique", potential_siblings[candidate_name]
       
   104 
       
   105             if node.nodetype == "HMI_NODE" and len(self.children) > 0:
       
   106                 prev = self.children[-1]
       
   107                 if prev.path[:-1] == node.path[:-1]:
       
   108                     return "Late_HMI_NODE",prev
       
   109 
       
   110             self.children.append(node)
       
   111             return None
       
   112 
       
   113     def etree(self, add_hash=False):
       
   114 
       
   115         attribs = dict(name=self.name)
       
   116         if self.path is not None:
       
   117             attribs["path"] = ".".join(self.path)
       
   118 
       
   119         if self.hmiclass is not None:
       
   120             attribs["class"] = self.hmiclass
       
   121 
       
   122         if add_hash:
       
   123             attribs["hash"] = ",".join(map(str,self.hash()))
       
   124 
       
   125         res = etree.Element(self.nodetype, **attribs)
       
   126 
       
   127         if hasattr(self, "children"):
       
   128             for child_etree in imap(lambda c:c.etree(), self.children):
       
   129                 res.append(child_etree)
       
   130 
       
   131         return res
       
   132 
       
   133     @classmethod
       
   134     def from_etree(cls, enode):
       
   135         """
       
   136         alternative constructor, restoring HMI Tree from XML backup
       
   137         note: all C-related information is gone, 
       
   138               this restore is only for tree display and widget picking
       
   139         """
       
   140         nodetype = enode.tag
       
   141         attributes = enode.attrib
       
   142         name = attributes["name"]
       
   143         path = attributes["path"].split('.') if "path" in attributes else None 
       
   144         hmiclass = attributes.get("class", None)
       
   145         # hash is computed on demand
       
   146         node = cls(path, name, nodetype, hmiclass=hmiclass)
       
   147         for child in enode.iterchildren():
       
   148             node.children.append(cls.from_etree(child))
       
   149         return node
       
   150 
       
   151     def traverse(self):
       
   152         yield self
       
   153         if hasattr(self, "children"):
       
   154             for c in self.children:
       
   155                 for yoodl in c.traverse():
       
   156                     yield yoodl
       
   157 
       
   158 
       
   159     def hash(self):
       
   160         """ Produce a hash, any change in HMI tree structure change that hash """
       
   161         s = hashlib.new('md5')
       
   162         self._hash(s)
       
   163         # limit size to HMI_HASH_SIZE as in svghmi.c
       
   164         return map(ord,s.digest())[:8]
       
   165 
       
   166     def _hash(self, s):
       
   167         s.update(str((self.name,self.nodetype)))
       
   168         if hasattr(self, "children"):
       
   169             for c in self.children:
       
   170                 c._hash(s)
       
   171 
    38 
   172 # module scope for HMITree root
    39 # module scope for HMITree root
   173 # so that CTN can use HMITree deduced in Library
    40 # so that CTN can use HMITree deduced in Library
   174 # note: this only works because library's Generate_C is
    41 # note: this only works because library's Generate_C is
   175 #       systematicaly invoked before CTN's CTNGenerate_C
    42 #       systematicaly invoked before CTN's CTNGenerate_C
   176 
    43 
   177 hmi_tree_root = None
    44 hmi_tree_root = None
   178 
    45 
   179 on_hmitree_update = None
    46 on_hmitree_update = None
   180 
       
   181 SPECIAL_NODES = [("HMI_ROOT", "HMI_NODE"),
       
   182                  ("heartbeat", "HMI_INT")]
       
   183                  # ("current_page", "HMI_STRING")])
       
   184 
    47 
   185 class SVGHMILibrary(POULibrary):
    48 class SVGHMILibrary(POULibrary):
   186     def GetLibraryPath(self):
    49     def GetLibraryPath(self):
   187          return paths.AbsNeighbourFile(__file__, "pous.xml")
    50          return paths.AbsNeighbourFile(__file__, "pous.xml")
   188 
    51 
   281                         ".".join(new_node.path))
   144                         ".".join(new_node.path))
   282 
   145 
   283                 self.FatalError("SVGHMI : " + message)
   146                 self.FatalError("SVGHMI : " + message)
   284 
   147 
   285         if on_hmitree_update is not None:
   148         if on_hmitree_update is not None:
   286             on_hmitree_update()
   149             on_hmitree_update(hmi_tree_root)
   287 
   150 
   288         variable_decl_array = []
   151         variable_decl_array = []
   289         extern_variables_declarations = []
   152         extern_variables_declarations = []
   290         buf_index = 0
   153         buf_index = 0
   291         item_count = 0
   154         item_count = 0
   372                 #         ^
   235                 #         ^
   373                 # note the double zero after "runtime_", 
   236                 # note the double zero after "runtime_", 
   374                 # to ensure placement before other CTN generated code in execution order
   237                 # to ensure placement before other CTN generated code in execution order
   375 
   238 
   376 
   239 
   377 def SVGHMIEditorUpdater(ref):
   240 def Register_SVGHMI_UI_for_HMI_tree_updates(ref):
   378     def SVGHMIEditorUpdate():
   241     global on_hmitree_update
   379         o = ref()
   242     def HMITreeUpdate(_hmi_tree_root):
   380         if o is not None:
   243         obj = ref()
   381             wx.CallAfter(o.MakeTree)
   244         if obj is not None:
   382     return SVGHMIEditorUpdate
   245             obj.HMITreeUpdate(_hmi_tree_root)
   383 
   246 
   384 class HMITreeSelector(wx.TreeCtrl):
   247     on_hmitree_update = HMITreeUpdate
   385     def __init__(self, parent):
       
   386         global on_hmitree_update
       
   387         wx.TreeCtrl.__init__(self, parent, style=(
       
   388             wx.TR_MULTIPLE |
       
   389             wx.TR_HAS_BUTTONS |
       
   390             wx.SUNKEN_BORDER |
       
   391             wx.TR_LINES_AT_ROOT))
       
   392 
       
   393         on_hmitree_update = SVGHMIEditorUpdater(weakref.ref(self))
       
   394         self.MakeTree()
       
   395 
       
   396     def _recurseTree(self, current_hmitree_root, current_tc_root):
       
   397         for c in current_hmitree_root.children:
       
   398             if hasattr(c, "children"):
       
   399                 display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \
       
   400                                if c.hmiclass is not None else c.name
       
   401                 tc_child = self.AppendItem(current_tc_root, display_name)
       
   402                 self.SetPyData(tc_child, None) # TODO
       
   403 
       
   404                 self._recurseTree(c,tc_child)
       
   405             else:
       
   406                 display_name = '{} {}'.format(c.nodetype[4:], c.name)
       
   407                 tc_child = self.AppendItem(current_tc_root, display_name)
       
   408                 self.SetPyData(tc_child, None) # TODO
       
   409 
       
   410     def MakeTree(self):
       
   411         global hmi_tree_root
       
   412 
       
   413         self.Freeze()
       
   414 
       
   415         self.root = None
       
   416         self.DeleteAllItems()
       
   417 
       
   418         root_display_name = _("Please build to see HMI Tree") \
       
   419             if hmi_tree_root is None else "HMI"
       
   420         self.root = self.AddRoot(root_display_name)
       
   421         self.SetPyData(self.root, None)
       
   422 
       
   423         if hmi_tree_root is not None:
       
   424             self._recurseTree(hmi_tree_root, self.root)
       
   425             self.Expand(self.root)
       
   426 
       
   427         self.Thaw()
       
   428 
       
   429 class WidgetPicker(wx.TreeCtrl):
       
   430     def __init__(self, parent, initialdir=None):
       
   431         wx.TreeCtrl.__init__(self, parent, style=(
       
   432             wx.TR_MULTIPLE |
       
   433             wx.TR_HAS_BUTTONS |
       
   434             wx.SUNKEN_BORDER |
       
   435             wx.TR_LINES_AT_ROOT))
       
   436 
       
   437         self.MakeTree(initialdir)
       
   438 
       
   439     def _recurseTree(self, current_dir, current_tc_root, dirlist):
       
   440         """
       
   441         recurse through subdirectories, but creates tree nodes 
       
   442         only when (sub)directory conbtains .svg file
       
   443         """
       
   444         res = []
       
   445         for f in sorted(os.listdir(current_dir)):
       
   446             p = os.path.join(current_dir,f)
       
   447             if os.path.isdir(p):
       
   448 
       
   449                 r = self._recurseTree(p, current_tc_root, dirlist + [f])
       
   450                 if len(r) > 0 :
       
   451                     res = r
       
   452                     dirlist = []
       
   453                     current_tc_root = res.pop()
       
   454 
       
   455             elif os.path.splitext(f)[1].upper() == ".SVG":
       
   456                 if len(dirlist) > 0 :
       
   457                     res = []
       
   458                     for d in dirlist:
       
   459                         current_tc_root = self.AppendItem(current_tc_root, d)
       
   460                         res.append(current_tc_root)
       
   461                         self.SetPyData(current_tc_root, None)
       
   462                     dirlist = []
       
   463                     res.pop()
       
   464                 tc_child = self.AppendItem(current_tc_root, f)
       
   465                 self.SetPyData(tc_child, p)
       
   466         return res
       
   467 
       
   468     def MakeTree(self, lib_dir = None):
       
   469         global hmi_tree_root
       
   470 
       
   471         self.Freeze()
       
   472 
       
   473         self.root = None
       
   474         self.DeleteAllItems()
       
   475 
       
   476         root_display_name = _("Please select widget library directory") \
       
   477             if lib_dir is None else os.path.basename(lib_dir)
       
   478         self.root = self.AddRoot(root_display_name)
       
   479         self.SetPyData(self.root, None)
       
   480 
       
   481         if lib_dir is not None:
       
   482             self._recurseTree(lib_dir, self.root, [])
       
   483             self.Expand(self.root)
       
   484 
       
   485         self.Thaw()
       
   486 
       
   487 _conf_key = "SVGHMIWidgetLib"
       
   488 _preview_height = 200
       
   489 class WidgetLibBrowser(wx.Panel):
       
   490     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
       
   491                  size=wx.DefaultSize):
       
   492 
       
   493         wx.Panel.__init__(self, parent, id, pos, size)     
       
   494 
       
   495         self.bmp = None
       
   496         self.msg = None
       
   497         self.hmitree_node = None
       
   498         self.selected_SVG = None
       
   499 
       
   500         self.Config = wx.ConfigBase.Get()
       
   501         self.libdir = self.RecallLibDir()
       
   502 
       
   503         sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=3, vgap=0)
       
   504         sizer.AddGrowableCol(0)
       
   505         sizer.AddGrowableRow(1)
       
   506         self.libbutton = wx.Button(self, -1, _("Select SVG widget library"))
       
   507         self.widgetpicker = WidgetPicker(self, self.libdir)
       
   508         self.preview = wx.Panel(self, size=(-1, _preview_height + 10))  #, style=wx.SIMPLE_BORDER)
       
   509         #self.preview.SetBackgroundColour(wx.WHITE)
       
   510         sizer.AddWindow(self.libbutton, flag=wx.GROW)
       
   511         sizer.AddWindow(self.widgetpicker, flag=wx.GROW)
       
   512         sizer.AddWindow(self.preview, flag=wx.GROW)
       
   513         sizer.Layout()
       
   514         self.SetAutoLayout(True)
       
   515         self.SetSizer(sizer)
       
   516         sizer.Fit(self)
       
   517         self.Bind(wx.EVT_BUTTON, self.OnSelectLibDir, self.libbutton)
       
   518         self.preview.Bind(wx.EVT_PAINT, self.OnPaint)
       
   519 
       
   520         self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnWidgetSelection, self.widgetpicker)
       
   521 
       
   522         self.msg = _("Drag selected Widget from here to Inkscape")
       
   523 
       
   524     def RecallLibDir(self):
       
   525         conf = self.Config.Read(_conf_key)
       
   526         if len(conf) == 0:
       
   527             return None
       
   528         else:
       
   529             return DecodeFileSystemPath(conf)
       
   530 
       
   531     def RememberLibDir(self, path):
       
   532         self.Config.Write(_conf_key,
       
   533                           EncodeFileSystemPath(path))
       
   534         self.Config.Flush()
       
   535 
       
   536     def DrawPreview(self):
       
   537         """
       
   538         Refresh preview panel 
       
   539         """
       
   540         # Init preview panel paint device context
       
   541         dc = wx.PaintDC(self.preview)
       
   542         dc.Clear()
       
   543 
       
   544         if self.bmp:
       
   545             # Get Preview panel size
       
   546             sz = self.preview.GetClientSize()
       
   547             w = self.bmp.GetWidth()
       
   548             dc.DrawBitmap(self.bmp, (sz.width - w)/2, 5)
       
   549 
       
   550         if self.msg:
       
   551             dc.SetFont(self.GetFont())
       
   552             dc.DrawText(self.msg, 25,25)
       
   553 
       
   554 
       
   555     def OnSelectLibDir(self, event):
       
   556         defaultpath = self.RecallLibDir()
       
   557         if defaultpath == None:
       
   558             defaultpath = os.path.expanduser("~")
       
   559 
       
   560         dialog = wx.DirDialog(self, _("Choose a widget library"), defaultpath,
       
   561                               style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
       
   562 
       
   563         if dialog.ShowModal() == wx.ID_OK:
       
   564             self.libdir = dialog.GetPath()
       
   565             self.RememberLibDir(self.libdir)
       
   566             self.widgetpicker.MakeTree(self.libdir)
       
   567 
       
   568         dialog.Destroy()
       
   569 
       
   570     def OnPaint(self, event):
       
   571         """
       
   572         Called when Preview panel needs to be redrawn
       
   573         @param event: wx.PaintEvent
       
   574         """
       
   575         self.DrawPreview()
       
   576         event.Skip()
       
   577 
       
   578     def GenThumbnail(self, svgpath, thumbpath):
       
   579         inkpath = get_inkscape_path()
       
   580         if inkpath is None:
       
   581             self.msg = _("Inkscape is not installed.")
       
   582             return False
       
   583         # TODO: spawn a thread, to decouple thumbnail gen
       
   584         status, result, _err_result = ProcessLogger(
       
   585             None,
       
   586             '"' + inkpath + '" "' + svgpath + '" -e "' + thumbpath +
       
   587             '" -D -h ' + str(_preview_height)).spin()
       
   588         if status != 0:
       
   589             self.msg = _("Inkscape couldn't generate thumbnail.")
       
   590             return False
       
   591         return True
       
   592 
       
   593     def OnWidgetSelection(self, event):
       
   594         """
       
   595         Called when tree item is selected
       
   596         @param event: wx.TreeEvent
       
   597         """
       
   598         item_pydata = self.widgetpicker.GetPyData(event.GetItem())
       
   599         if item_pydata is not None:
       
   600             svgpath = item_pydata
       
   601             dname = os.path.dirname(svgpath)
       
   602             fname = os.path.basename(svgpath)
       
   603             hasher = hashlib.new('md5')
       
   604             with open(svgpath, 'rb') as afile:
       
   605                 while True:
       
   606                     buf = afile.read(65536)
       
   607                     if len(buf) > 0:
       
   608                         hasher.update(buf)
       
   609                     else:
       
   610                         break
       
   611             digest = hasher.hexdigest()
       
   612             thumbfname = os.path.splitext(fname)[0]+"_"+digest+".png"
       
   613             thumbdir = os.path.join(dname, ".svghmithumbs") 
       
   614             thumbpath = os.path.join(thumbdir, thumbfname) 
       
   615 
       
   616             self.msg = None
       
   617             have_thumb = os.path.exists(thumbpath)
       
   618 
       
   619             if not have_thumb:
       
   620                 try:
       
   621                     if not os.path.exists(thumbdir):
       
   622                         os.mkdir(thumbdir)
       
   623                 except IOError:
       
   624                     self.msg = _("Widget library must be writable")
       
   625                 else:
       
   626                     have_thumb = self.GenThumbnail(svgpath, thumbpath)
       
   627 
       
   628             self.bmp = wx.Bitmap(thumbpath) if have_thumb else None
       
   629 
       
   630             self.selected_SVG = svgpath if have_thumb else None
       
   631             self.ValidateWidget()
       
   632 
       
   633             self.Refresh()
       
   634         event.Skip()
       
   635 
       
   636     def OnHMITreeNodeSelection(self, hmitree_node):
       
   637         self.hmitree_node = hmitree_node
       
   638         self.ValidateWidget()
       
   639         self.Refresh()
       
   640 
       
   641     def ValidateWidget(self):
       
   642         if self.selected_SVG is not None:
       
   643             if self.hmitree_node is not None:
       
   644                 pass
       
   645         # XXX TODO: 
       
   646         #      - check SVG is valid for selected HMI tree item
       
   647         #      - prepare for D'n'D
       
   648 
       
   649 
       
   650 class HMITreeView(wx.SplitterWindow):
       
   651 
       
   652     def __init__(self, parent):
       
   653         wx.SplitterWindow.__init__(self, parent,
       
   654                                    style=wx.SUNKEN_BORDER | wx.SP_3D)
       
   655 
       
   656         self.SelectionTree = HMITreeSelector(self)
       
   657         self.Staging = WidgetLibBrowser(self)
       
   658         self.SplitVertically(self.SelectionTree, self.Staging, 300)
       
   659 
   248 
   660 
   249 
   661 class SVGHMIEditor(ConfTreeNodeEditor):
   250 class SVGHMIEditor(ConfTreeNodeEditor):
   662     CONFNODEEDITOR_TABS = [
   251     CONFNODEEDITOR_TABS = [
   663         (_("HMI Tree"), "CreateHMITreeView")]
   252         (_("HMI Tree"), "CreateSVGHMI_UI")]
   664 
   253 
   665     def CreateHMITreeView(self, parent):
   254     def CreateSVGHMI_UI(self, parent):
   666         global hmi_tree_root
   255         global hmi_tree_root
   667 
   256 
   668         if hmi_tree_root is None:
   257         if hmi_tree_root is None:
   669             buildpath = self.Controler.GetCTRoot()._getBuildPath()
   258             buildpath = self.Controler.GetCTRoot()._getBuildPath()
   670             hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
   259             hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
   671             if os.path.exists(hmitree_backup_path):
   260             if os.path.exists(hmitree_backup_path):
   672                 hmitree_backup_file = open(hmitree_backup_path, 'rb')
   261                 hmitree_backup_file = open(hmitree_backup_path, 'rb')
   673                 hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
   262                 hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
   674 
   263 
   675 
   264         return SVGHMI_UI(parent, Register_SVGHMI_UI_for_HMI_tree_updates)
   676         #self.HMITreeView = HMITreeView(self)
       
   677         #return HMITreeSelector(parent)
       
   678         return HMITreeView(parent)
       
   679 
   265 
   680 class SVGHMI(object):
   266 class SVGHMI(object):
   681     XSD = """<?xml version="1.0" encoding="utf-8" ?>
   267     XSD = """<?xml version="1.0" encoding="utf-8" ?>
   682     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   268     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   683       <xsd:element name="SVGHMI">
   269       <xsd:element name="SVGHMI">