svghmi/svghmi.py
changeset 3302 c89fc366bebd
parent 3290 f0c97422b34a
child 3312 2c0511479b18
child 3341 dce1d5413310
equal deleted inserted replaced
2744:577118ebd179 3302:c89fc366bebd
       
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 # This file is part of Beremiz
       
     5 # Copyright (C) 2021: Edouard TISSERANT
       
     6 #
       
     7 # See COPYING file for copyrights details.
       
     8 
       
     9 from __future__ import absolute_import
       
    10 import os
       
    11 import shutil
       
    12 import hashlib
       
    13 import shlex
       
    14 import time
       
    15 
       
    16 import wx
       
    17 
       
    18 from lxml import etree
       
    19 from lxml.etree import XSLTApplyError
       
    20 
       
    21 import util.paths as paths
       
    22 from POULibrary import POULibrary
       
    23 from docutil import open_svg, get_inkscape_path
       
    24 
       
    25 from util.ProcessLogger import ProcessLogger
       
    26 from runtime.typemapping import DebugTypesSize
       
    27 import targets
       
    28 from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
       
    29 from XSLTransform import XSLTransform
       
    30 from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations,\
       
    31                         MatchTranslations, TranslationToEtree, open_pofile,\
       
    32                         GetPoFiles
       
    33 from svghmi.hmi_tree import HMI_TYPES, HMITreeNode, SPECIAL_NODES 
       
    34 from svghmi.ui import SVGHMI_UI
       
    35 from svghmi.fonts import GetFontTypeAndFamilyName, GetCSSFontFaceFromFontFile
       
    36 
       
    37 
       
    38 ScriptDirectory = paths.AbsDir(__file__)
       
    39 
       
    40 
       
    41 # module scope for HMITree root
       
    42 # so that CTN can use HMITree deduced in Library
       
    43 # note: this only works because library's Generate_C is
       
    44 #       systematicaly invoked before CTN's CTNGenerate_C
       
    45 
       
    46 hmi_tree_root = None
       
    47 
       
    48 on_hmitree_update = None
       
    49 
       
    50 maxConnectionsTotal = 0
       
    51 
       
    52 class SVGHMILibrary(POULibrary):
       
    53     def GetLibraryPath(self):
       
    54          return paths.AbsNeighbourFile(__file__, "pous.xml")
       
    55 
       
    56     def Generate_C(self, buildpath, varlist, IECCFLAGS):
       
    57         global hmi_tree_root, on_hmitree_update, maxConnectionsTotal
       
    58 
       
    59         already_found_watchdog = False
       
    60         found_SVGHMI_instance = False
       
    61         for CTNChild in self.GetCTR().IterChildren():
       
    62             if isinstance(CTNChild, SVGHMI):
       
    63                 found_SVGHMI_instance = True
       
    64                 # collect maximum connection total for all svghmi nodes
       
    65                 maxConnectionsTotal += CTNChild.GetParamsAttributes("SVGHMI.MaxConnections")["value"]
       
    66 
       
    67                 # spot watchdog abuse
       
    68                 if CTNChild.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]:
       
    69                     if already_found_watchdog:
       
    70                         self.FatalError("SVGHMI: Only one watchdog enabled HMI allowed")
       
    71                     already_found_watchdog = True
       
    72 
       
    73         if not found_SVGHMI_instance:
       
    74             self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.")
       
    75 
       
    76 
       
    77         """
       
    78         PLC Instance Tree:
       
    79           prog0
       
    80            +->v1 HMI_INT
       
    81            +->v2 HMI_INT
       
    82            +->fb0 (type mhoo)
       
    83            |   +->va HMI_NODE
       
    84            |   +->v3 HMI_INT
       
    85            |   +->v4 HMI_INT
       
    86            |
       
    87            +->fb1 (type mhoo)
       
    88            |   +->va HMI_NODE
       
    89            |   +->v3 HMI_INT
       
    90            |   +->v4 HMI_INT
       
    91            |
       
    92            +->fb2
       
    93                +->v5 HMI_IN
       
    94 
       
    95         HMI tree:
       
    96           hmi0
       
    97            +->v1
       
    98            +->v2
       
    99            +->fb0 class:va
       
   100            |   +-> v3
       
   101            |   +-> v4
       
   102            |
       
   103            +->fb1 class:va
       
   104            |   +-> v3
       
   105            |   +-> v4
       
   106            |
       
   107            +->v5
       
   108 
       
   109         """
       
   110 
       
   111         # Filter known HMI types
       
   112         hmi_types_instances = [v for v in varlist if v["derived"] in HMI_TYPES]
       
   113 
       
   114         hmi_tree_root = None
       
   115 
       
   116         # take first HMI_NODE (placed as special node), make it root
       
   117         for i,v in enumerate(hmi_types_instances):
       
   118             path = v["IEC_path"].split(".")
       
   119             derived = v["derived"]
       
   120             if derived == "HMI_NODE":
       
   121                 hmi_tree_root = HMITreeNode(path, "", derived, v["type"], v["vartype"], v["C_path"])
       
   122                 hmi_types_instances.pop(i)
       
   123                 break
       
   124 
       
   125         # deduce HMI tree from PLC HMI_* instances
       
   126         for v in hmi_types_instances:
       
   127             path = v["IEC_path"].split(".")
       
   128             # ignores variables starting with _TMP_
       
   129             if path[-1].startswith("_TMP_"):
       
   130                 continue
       
   131             derived = v["derived"]
       
   132             kwargs={}
       
   133             if derived == "HMI_NODE":
       
   134                 # TODO : make problem if HMI_NODE used in CONFIG or RESOURCE
       
   135                 name = path[-2]
       
   136                 kwargs['hmiclass'] = path[-1]
       
   137             else:
       
   138                 name = path[-1]
       
   139             new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs)
       
   140             placement_result = hmi_tree_root.place_node(new_node)
       
   141             if placement_result is not None:
       
   142                 cause, problematic_node = placement_result
       
   143                 if cause == "Non_Unique":
       
   144                     message = _("HMI tree nodes paths are not unique.\nConflicting variable: {} {}").format(
       
   145                         ".".join(problematic_node.path),
       
   146                         ".".join(new_node.path))
       
   147 
       
   148                     last_FB = None 
       
   149                     for v in varlist:
       
   150                         if v["vartype"] == "FB":
       
   151                             last_FB = v 
       
   152                         if v["C_path"] == problematic_node:
       
   153                             break
       
   154                     if last_FB is not None:
       
   155                         failing_parent = last_FB["type"]
       
   156                         message += "\n"
       
   157                         message += _("Solution: Add HMI_NODE at beginning of {}").format(failing_parent)
       
   158 
       
   159                 elif cause in ["Late_HMI_NODE", "Duplicate_HMI_NODE"]:
       
   160                     cause, problematic_node = placement_result
       
   161                     message = _("There must be only one occurrence of HMI_NODE before any HMI_* variable in POU.\nConflicting variable: {} {}").format(
       
   162                         ".".join(problematic_node.path),
       
   163                         ".".join(new_node.path))
       
   164 
       
   165                 self.FatalError("SVGHMI : " + message)
       
   166 
       
   167         if on_hmitree_update is not None:
       
   168             on_hmitree_update(hmi_tree_root)
       
   169 
       
   170         variable_decl_array = []
       
   171         extern_variables_declarations = []
       
   172         buf_index = 0
       
   173         item_count = 0
       
   174         found_heartbeat = False
       
   175 
       
   176         hearbeat_IEC_path = ['CONFIG', 'HEARTBEAT']
       
   177 
       
   178         for node in hmi_tree_root.traverse():
       
   179             if not found_heartbeat and node.path == hearbeat_IEC_path:
       
   180                 hmi_tree_hearbeat_index = item_count
       
   181                 found_heartbeat = True
       
   182                 extern_variables_declarations += [
       
   183                     "#define heartbeat_index "+str(hmi_tree_hearbeat_index)
       
   184                 ]
       
   185             if hasattr(node, "iectype"):
       
   186                 sz = DebugTypesSize.get(node.iectype, 0)
       
   187                 variable_decl_array += [
       
   188                     "{&(" + node.cpath + "), " + node.iectype + {
       
   189                         "EXT": "_P_ENUM",
       
   190                         "IN":  "_P_ENUM",
       
   191                         "MEM": "_O_ENUM",
       
   192                         "OUT": "_O_ENUM",
       
   193                         "VAR": "_ENUM"
       
   194                     }[node.vartype] + ", " +
       
   195                     str(buf_index) + ", 0, }"]
       
   196                 buf_index += sz
       
   197                 item_count += 1
       
   198                 if len(node.path) == 1:
       
   199                     extern_variables_declarations += [
       
   200                         "extern __IEC_" + node.iectype + "_" +
       
   201                         "t" if node.vartype is "VAR" else "p"
       
   202                         + node.cpath + ";"]
       
   203 
       
   204         assert(found_heartbeat)
       
   205 
       
   206         # TODO : filter only requiered external declarations
       
   207         for v in varlist:
       
   208             if v["C_path"].find('.') < 0:
       
   209                 extern_variables_declarations += [
       
   210                     "extern %(type)s %(C_path)s;" % v]
       
   211 
       
   212         # TODO check if programs need to be declared separately
       
   213         # "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;" %
       
   214         #                                     p for p in self._ProgramList]),
       
   215 
       
   216         # C code to observe/access HMI tree variables
       
   217         svghmi_c_filepath = paths.AbsNeighbourFile(__file__, "svghmi.c")
       
   218         svghmi_c_file = open(svghmi_c_filepath, 'r')
       
   219         svghmi_c_code = svghmi_c_file.read()
       
   220         svghmi_c_file.close()
       
   221         svghmi_c_code = svghmi_c_code % {
       
   222             "variable_decl_array": ",\n".join(variable_decl_array),
       
   223             "extern_variables_declarations": "\n".join(extern_variables_declarations),
       
   224             "buffer_size": buf_index,
       
   225             "item_count": item_count,
       
   226             "var_access_code": targets.GetCode("var_access.c"),
       
   227             "PLC_ticktime": self.GetCTR().GetTicktime(),
       
   228             "hmi_hash_ints": ",".join(map(str,hmi_tree_root.hash())),
       
   229             "max_connections": maxConnectionsTotal
       
   230             }
       
   231 
       
   232         gen_svghmi_c_path = os.path.join(buildpath, "svghmi.c")
       
   233         gen_svghmi_c = open(gen_svghmi_c_path, 'w')
       
   234         gen_svghmi_c.write(svghmi_c_code)
       
   235         gen_svghmi_c.close()
       
   236 
       
   237         # Python based WebSocket HMITree Server
       
   238         svghmiserverfile = open(paths.AbsNeighbourFile(__file__, "svghmi_server.py"), 'r')
       
   239         svghmiservercode = svghmiserverfile.read()
       
   240         svghmiserverfile.close()
       
   241 
       
   242         runtimefile_path = os.path.join(buildpath, "runtime_00_svghmi.py")
       
   243         runtimefile = open(runtimefile_path, 'w')
       
   244         runtimefile.write(svghmiservercode)
       
   245         runtimefile.close()
       
   246 
       
   247         # Backup HMI Tree in XML form so that it can be loaded without building
       
   248         hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
       
   249         hmitree_backup_file = open(hmitree_backup_path, 'wb')
       
   250         hmitree_backup_file.write(etree.tostring(hmi_tree_root.etree()))
       
   251         hmitree_backup_file.close()
       
   252 
       
   253         return ((["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "",
       
   254                 ("runtime_00_svghmi.py", open(runtimefile_path, "rb")))
       
   255                 #         ^
       
   256                 # note the double zero after "runtime_", 
       
   257                 # to ensure placement before other CTN generated code in execution order
       
   258 
       
   259     def GlobalInstances(self):
       
   260         """ Adds HMI tree root and hearbeat to PLC Configuration's globals """
       
   261         return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES]
       
   262 
       
   263 
       
   264 
       
   265 def Register_SVGHMI_UI_for_HMI_tree_updates(ref):
       
   266     global on_hmitree_update
       
   267     def HMITreeUpdate(_hmi_tree_root):
       
   268         obj = ref()
       
   269         if obj is not None:
       
   270             obj.HMITreeUpdate(_hmi_tree_root)
       
   271 
       
   272     on_hmitree_update = HMITreeUpdate
       
   273 
       
   274 
       
   275 class SVGHMIEditor(ConfTreeNodeEditor):
       
   276     CONFNODEEDITOR_TABS = [
       
   277         (_("HMI Tree"), "CreateSVGHMI_UI")]
       
   278 
       
   279     def CreateSVGHMI_UI(self, parent):
       
   280         global hmi_tree_root
       
   281 
       
   282         if hmi_tree_root is None:
       
   283             buildpath = self.Controler.GetCTRoot()._getBuildPath()
       
   284             hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
       
   285             if os.path.exists(hmitree_backup_path):
       
   286                 hmitree_backup_file = open(hmitree_backup_path, 'rb')
       
   287                 hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
       
   288 
       
   289         ret = SVGHMI_UI(parent, Register_SVGHMI_UI_for_HMI_tree_updates)
       
   290 
       
   291         on_hmitree_update(hmi_tree_root)
       
   292 
       
   293         return ret
       
   294 
       
   295 class SVGHMI(object):
       
   296     XSD = """<?xml version="1.0" encoding="utf-8" ?>
       
   297     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       
   298       <xsd:element name="SVGHMI">
       
   299         <xsd:complexType>
       
   300           <xsd:attribute name="OnStart" type="xsd:string" use="optional" default="chromium {url}"/>
       
   301           <xsd:attribute name="OnStop" type="xsd:string" use="optional" default="echo 'please close chromium window at {url}'"/>
       
   302           <xsd:attribute name="OnWatchdog" type="xsd:string" use="optional" default="echo 'Watchdog for {name} !'"/>
       
   303           <xsd:attribute name="EnableWatchdog" type="xsd:boolean" use="optional" default="false"/>
       
   304           <xsd:attribute name="WatchdogInitial" use="optional" default="30">
       
   305             <xsd:simpleType>
       
   306                 <xsd:restriction base="xsd:integer">
       
   307                     <xsd:minInclusive value="2"/>
       
   308                     <xsd:maxInclusive value="600"/>
       
   309                 </xsd:restriction>
       
   310             </xsd:simpleType>
       
   311           </xsd:attribute>
       
   312           <xsd:attribute name="WatchdogInterval" use="optional" default="5">
       
   313             <xsd:simpleType>
       
   314                 <xsd:restriction base="xsd:integer">
       
   315                     <xsd:minInclusive value="2"/>
       
   316                     <xsd:maxInclusive value="60"/>
       
   317                 </xsd:restriction>
       
   318             </xsd:simpleType>
       
   319           </xsd:attribute>
       
   320           <xsd:attribute name="Port" type="xsd:integer" use="optional" default="8008"/>
       
   321           <xsd:attribute name="Interface" type="xsd:string" use="optional" default="localhost"/>
       
   322           <xsd:attribute name="Path" type="xsd:string" use="optional" default="{name}"/>
       
   323           <xsd:attribute name="MaxConnections" use="optional" default="16">
       
   324             <xsd:simpleType>
       
   325                 <xsd:restriction base="xsd:integer">
       
   326                     <xsd:minInclusive value="1"/>
       
   327                     <xsd:maxInclusive value="1024"/>
       
   328                 </xsd:restriction>
       
   329             </xsd:simpleType>
       
   330           </xsd:attribute>
       
   331         </xsd:complexType>
       
   332       </xsd:element>
       
   333     </xsd:schema>
       
   334     """
       
   335 
       
   336     EditorType = SVGHMIEditor
       
   337 
       
   338     ConfNodeMethods = [
       
   339         {
       
   340             "bitmap":    "ImportSVG",
       
   341             "name":    _("Import SVG"),
       
   342             "tooltip": _("Import SVG"),
       
   343             "method":   "_ImportSVG"
       
   344         },
       
   345         {
       
   346             "bitmap":    "EditSVG",
       
   347             "name":    _("Inkscape"),
       
   348             "tooltip": _("Edit HMI"),
       
   349             "method":   "_StartInkscape"
       
   350         },
       
   351         {
       
   352             "bitmap":    "OpenPOT",
       
   353             "name":    _("New lang"),
       
   354             "tooltip": _("Open non translated message catalog (POT) to start new language"),
       
   355             "method":   "_OpenPOT"
       
   356         },
       
   357         {
       
   358             "bitmap":    "EditPO",
       
   359             "name":    _("Edit lang"),
       
   360             "tooltip": _("Edit existing message catalog (PO) for specific language"),
       
   361             "method":   "_EditPO"
       
   362         },
       
   363         {
       
   364             "bitmap":    "AddFont",
       
   365             "name":    _("Add Font"),
       
   366             "tooltip": _("Add TTF, OTF or WOFF font to be embedded in HMI"),
       
   367             "method":   "_AddFont"
       
   368         },
       
   369         {
       
   370             "bitmap":    "DelFont",
       
   371             "name":    _("Delete Font"),
       
   372             "tooltip": _("Remove font previously added to HMI"),
       
   373             "method":   "_DelFont"
       
   374         },
       
   375     ]
       
   376 
       
   377     def _getSVGpath(self, project_path=None):
       
   378         if project_path is None:
       
   379             project_path = self.CTNPath()
       
   380         return os.path.join(project_path, "svghmi.svg")
       
   381 
       
   382     def _getPOTpath(self, project_path=None):
       
   383         if project_path is None:
       
   384             project_path = self.CTNPath()
       
   385         return os.path.join(project_path, "messages.pot")
       
   386 
       
   387     def OnCTNSave(self, from_project_path=None):
       
   388         if from_project_path is not None:
       
   389             shutil.copyfile(self._getSVGpath(from_project_path),
       
   390                             self._getSVGpath())
       
   391             shutil.copyfile(self._getPOTpath(from_project_path),
       
   392                             self._getPOTpath())
       
   393             # XXX TODO copy .PO files
       
   394         return True
       
   395 
       
   396     def GetSVGGeometry(self):
       
   397         self.ProgressStart("inkscape", "collecting SVG geometry (Inkscape)")
       
   398         # invoke inskscape -S, csv-parse output, produce elements
       
   399         InkscapeGeomColumns = ["Id", "x", "y", "w", "h"]
       
   400 
       
   401         inkpath = get_inkscape_path()
       
   402 
       
   403         if inkpath is None:
       
   404             self.FatalError("SVGHMI: inkscape is not installed.")
       
   405 
       
   406         svgpath = self._getSVGpath()
       
   407         status, result, _err_result = ProcessLogger(self.GetCTRoot().logger,
       
   408                                                      '"' + inkpath + '" -S "' + svgpath + '"',
       
   409                                                      no_stdout=True,
       
   410                                                      no_stderr=True).spin()
       
   411         if status != 0:
       
   412             self.FatalError("SVGHMI: inkscape couldn't extract geometry from given SVG.")
       
   413 
       
   414         res = []
       
   415         for line in result.split():
       
   416             strippedline = line.strip()
       
   417             attrs = dict(
       
   418                 zip(InkscapeGeomColumns, line.strip().split(',')))
       
   419 
       
   420             res.append(etree.Element("bbox", **attrs))
       
   421 
       
   422         self.ProgressEnd("inkscape")
       
   423         return res
       
   424 
       
   425     def GetHMITree(self):
       
   426         global hmi_tree_root
       
   427         self.ProgressStart("hmitree", "getting HMI tree")
       
   428         res = [hmi_tree_root.etree(add_hash=True)]
       
   429         self.ProgressEnd("hmitree")
       
   430         return res
       
   431 
       
   432     def GetTranslations(self, _context, msgs):
       
   433         self.ProgressStart("i18n", "getting Translations")
       
   434         messages = EtreeToMessages(msgs)
       
   435 
       
   436         if len(messages) == 0:
       
   437             self.ProgressEnd("i18n")
       
   438             return
       
   439 
       
   440         SaveCatalog(self._getPOTpath(), messages)
       
   441 
       
   442         translations = ReadTranslations(self.CTNPath())
       
   443             
       
   444         langs,translated_messages = MatchTranslations(translations, messages, 
       
   445             errcallback=self.GetCTRoot().logger.write_warning)
       
   446 
       
   447         ret = TranslationToEtree(langs,translated_messages)
       
   448 
       
   449         self.ProgressEnd("i18n")
       
   450 
       
   451         return ret
       
   452 
       
   453     def GetFontsFiles(self):
       
   454         project_path = self.CTNPath()
       
   455         fontdir = os.path.join(project_path, "fonts") 
       
   456         if os.path.isdir(fontdir):
       
   457             return [os.path.join(fontdir,f) for f in sorted(os.listdir(fontdir))]
       
   458         return []
       
   459 
       
   460     def GetFonts(self, _context):
       
   461         css_parts = []
       
   462 
       
   463         for fontfile in self.GetFontsFiles():
       
   464             if os.path.isfile(fontfile):
       
   465                 css_parts.append(GetCSSFontFaceFromFontFile(fontfile))
       
   466 
       
   467         return "".join(css_parts)
       
   468 
       
   469     times_msgs = {}
       
   470     indent = 1
       
   471     def ProgressStart(self, k, m):
       
   472         self.times_msgs[k] = (time.time(), m)
       
   473         self.GetCTRoot().logger.write("    "*self.indent + "Start %s...\n"%m)
       
   474         self.indent = self.indent + 1
       
   475 
       
   476     def ProgressEnd(self, k):
       
   477         t = time.time()
       
   478         oldt, m = self.times_msgs[k]
       
   479         self.indent = self.indent - 1
       
   480         self.GetCTRoot().logger.write("    "*self.indent + "... finished in %.3fs\n"%(t - oldt))
       
   481 
       
   482     def get_SVGHMI_options(self):
       
   483         name = self.BaseParams.getName()
       
   484         port = self.GetParamsAttributes("SVGHMI.Port")["value"]
       
   485         interface = self.GetParamsAttributes("SVGHMI.Interface")["value"]
       
   486         path = self.GetParamsAttributes("SVGHMI.Path")["value"].format(name=name)
       
   487         if path and path[0]=='/':
       
   488             path = path[1:]
       
   489         enable_watchdog = self.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]
       
   490         url="http://"+interface+("" if port==80 else (":"+str(port))
       
   491             ) + (("/"+path) if path else ""
       
   492             ) + ("#watchdog" if enable_watchdog else "")
       
   493 
       
   494         return dict(
       
   495             name=name,
       
   496             port=port,
       
   497             interface=interface,
       
   498             path=path,
       
   499             enable_watchdog=enable_watchdog,
       
   500             url=url)
       
   501 
       
   502     def CTNGenerate_C(self, buildpath, locations):
       
   503         global hmi_tree_root
       
   504 
       
   505         if hmi_tree_root is None:
       
   506             self.FatalError("SVGHMI : Library is not selected. Please select it in project config.")
       
   507 
       
   508         location_str = "_".join(map(str, self.GetCurrentLocation()))
       
   509         svghmi_options = self.get_SVGHMI_options()
       
   510 
       
   511         svgfile = self._getSVGpath()
       
   512 
       
   513         res = ([], "", False)
       
   514 
       
   515         target_fname = "svghmi_"+location_str+".xhtml"
       
   516 
       
   517         build_path = self._getBuildPath()
       
   518         target_path = os.path.join(build_path, target_fname)
       
   519         hash_path = os.path.join(build_path, "svghmi.md5")
       
   520 
       
   521         self.GetCTRoot().logger.write("SVGHMI:\n")
       
   522 
       
   523         if os.path.exists(svgfile):
       
   524 
       
   525             hasher = hashlib.md5()
       
   526             hmi_tree_root._hash(hasher)
       
   527             pofiles = GetPoFiles(self.CTNPath())
       
   528             filestocheck = [svgfile] + \
       
   529                            (list(zip(*pofiles)[1]) if pofiles else []) + \
       
   530                            self.GetFontsFiles()
       
   531 
       
   532             for filetocheck in filestocheck:
       
   533                 with open(filetocheck, 'rb') as afile:
       
   534                     while True:
       
   535                         buf = afile.read(65536)
       
   536                         if len(buf) > 0:
       
   537                             hasher.update(buf)
       
   538                         else:
       
   539                             break
       
   540             digest = hasher.hexdigest()
       
   541 
       
   542             if os.path.exists(hash_path):
       
   543                 with open(hash_path, 'rb') as digest_file:
       
   544                     last_digest = digest_file.read()
       
   545             else:
       
   546                 last_digest = None
       
   547             
       
   548             if digest != last_digest:
       
   549 
       
   550                 transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
       
   551                               [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
       
   552                                ("GetHMITree", lambda *_ignored:self.GetHMITree()),
       
   553                                ("GetTranslations", self.GetTranslations),
       
   554                                ("GetFonts", self.GetFonts),
       
   555                                ("ProgressStart", lambda _ign,k,m:self.ProgressStart(str(k),str(m))),
       
   556                                ("ProgressEnd", lambda _ign,k:self.ProgressEnd(str(k)))])
       
   557 
       
   558                 self.ProgressStart("svg", "source SVG parsing")
       
   559 
       
   560                 # load svg as a DOM with Etree
       
   561                 svgdom = etree.parse(svgfile)
       
   562 
       
   563                 self.ProgressEnd("svg")
       
   564 
       
   565                 # call xslt transform on Inkscape's SVG to generate XHTML
       
   566                 try: 
       
   567                     self.ProgressStart("xslt", "XSLT transform")
       
   568                     result = transform.transform(svgdom)  # , profile_run=True)
       
   569                     self.ProgressEnd("xslt")
       
   570                 except XSLTApplyError as e:
       
   571                     self.FatalError("SVGHMI " + svghmi_options["name"] + ": " + e.message)
       
   572                 finally:
       
   573                     for entry in transform.get_error_log():
       
   574                         message = "SVGHMI: "+ entry.message + "\n" 
       
   575                         self.GetCTRoot().logger.write_warning(message)
       
   576 
       
   577                 target_file = open(target_path, 'wb')
       
   578                 result.write(target_file, encoding="utf-8")
       
   579                 target_file.close()
       
   580 
       
   581                 # print(str(result))
       
   582                 # print(transform.xslt.error_log)
       
   583                 # print(etree.tostring(result.xslt_profile,pretty_print=True))
       
   584 
       
   585                 with open(hash_path, 'wb') as digest_file:
       
   586                     digest_file.write(digest)
       
   587             else:
       
   588                 self.GetCTRoot().logger.write("    No changes - XSLT transformation skipped\n")
       
   589 
       
   590         else:
       
   591             target_file = open(target_path, 'wb')
       
   592             target_file.write("""<!DOCTYPE html>
       
   593 <html xmlns="http://www.w3.org/1999/xhtml">
       
   594 <head>
       
   595 <title>SVGHMI</title>
       
   596 </head>
       
   597 <body>
       
   598 <h1> No SVG file provided </h1>
       
   599 </body>
       
   600 </html>
       
   601 """)
       
   602             target_file.close()
       
   603 
       
   604         res += ((target_fname, open(target_path, "rb")),)
       
   605 
       
   606         svghmi_cmds = {}
       
   607         for thing in ["Start", "Stop", "Watchdog"]:
       
   608              given_command = self.GetParamsAttributes("SVGHMI.On"+thing)["value"]
       
   609              svghmi_cmds[thing] = (
       
   610                 "Popen(" +
       
   611                 repr(shlex.split(given_command.format(**svghmi_options))) +
       
   612                 ")") if given_command else "pass # no command given"
       
   613 
       
   614         runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str)
       
   615         runtimefile = open(runtimefile_path, 'w')
       
   616         runtimefile.write("""
       
   617 # TODO : multiple watchdog (one for each svghmi instance)
       
   618 def svghmi_{location}_watchdog_trigger():
       
   619     {svghmi_cmds[Watchdog]}
       
   620 
       
   621 max_svghmi_sessions = {maxConnections_total}
       
   622 
       
   623 def _runtime_{location}_svghmi_start():
       
   624     global svghmi_watchdog, svghmi_servers
       
   625 
       
   626     srv = svghmi_servers.get("{interface}:{port}", None)
       
   627     if srv is not None:
       
   628         svghmi_root, svghmi_listener, path_list = srv
       
   629         if '{path}' in path_list:
       
   630             raise Exception("SVGHMI {name}: path {path} already used on {interface}:{port}")
       
   631     else:
       
   632         svghmi_root = Resource()
       
   633         factory = HMIWebSocketServerFactory()
       
   634         factory.setProtocolOptions(maxConnections={maxConnections})
       
   635 
       
   636         svghmi_root.putChild("ws", WebSocketResource(factory))
       
   637 
       
   638         svghmi_listener = reactor.listenTCP({port}, Site(svghmi_root), interface='{interface}')
       
   639         path_list = []
       
   640         svghmi_servers["{interface}:{port}"] = (svghmi_root, svghmi_listener, path_list)
       
   641 
       
   642     svghmi_root.putChild(
       
   643         '{path}',
       
   644         NoCacheFile('{xhtml}',
       
   645             defaultType='application/xhtml+xml'))
       
   646 
       
   647     path_list.append("{path}")
       
   648 
       
   649     {svghmi_cmds[Start]}
       
   650 
       
   651     if {enable_watchdog}:
       
   652         if svghmi_watchdog is None:
       
   653             svghmi_watchdog = Watchdog(
       
   654                 {watchdog_initial},
       
   655                 {watchdog_interval},
       
   656                 svghmi_{location}_watchdog_trigger)
       
   657         else:
       
   658             raise Exception("SVGHMI {name}: only one watchdog allowed")
       
   659 
       
   660 
       
   661 def _runtime_{location}_svghmi_stop():
       
   662     global svghmi_watchdog, svghmi_servers
       
   663 
       
   664     if svghmi_watchdog is not None:
       
   665         svghmi_watchdog.cancel()
       
   666         svghmi_watchdog = None
       
   667 
       
   668     svghmi_root, svghmi_listener, path_list = svghmi_servers["{interface}:{port}"]
       
   669     svghmi_root.delEntity('{path}')
       
   670 
       
   671     path_list.remove('{path}')
       
   672 
       
   673     if len(path_list)==0:
       
   674         svghmi_root.delEntity("ws")
       
   675         svghmi_listener.stopListening()
       
   676         svghmi_servers.pop("{interface}:{port}")
       
   677 
       
   678     {svghmi_cmds[Stop]}
       
   679 
       
   680         """.format(location=location_str,
       
   681                    xhtml=target_fname,
       
   682                    svghmi_cmds=svghmi_cmds,
       
   683                    watchdog_initial = self.GetParamsAttributes("SVGHMI.WatchdogInitial")["value"],
       
   684                    watchdog_interval = self.GetParamsAttributes("SVGHMI.WatchdogInterval")["value"],
       
   685                    maxConnections = self.GetParamsAttributes("SVGHMI.MaxConnections")["value"],
       
   686                    maxConnections_total = maxConnectionsTotal,
       
   687                    **svghmi_options
       
   688         ))
       
   689 
       
   690         runtimefile.close()
       
   691 
       
   692         res += (("runtime_%s_svghmi.py" % location_str, open(runtimefile_path, "rb")),)
       
   693 
       
   694         return res
       
   695 
       
   696     def _ImportSVG(self):
       
   697         dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a SVG file"), os.getcwd(), "",  _("SVG files (*.svg)|*.svg|All files|*.*"), wx.OPEN)
       
   698         if dialog.ShowModal() == wx.ID_OK:
       
   699             svgpath = dialog.GetPath()
       
   700             if os.path.isfile(svgpath):
       
   701                 shutil.copy(svgpath, self._getSVGpath())
       
   702             else:
       
   703                 self.GetCTRoot().logger.write_error(_("No such SVG file: %s\n") % svgpath)
       
   704         dialog.Destroy()
       
   705 
       
   706     def getDefaultSVG(self):
       
   707         return os.path.join(ScriptDirectory, "default.svg")
       
   708 
       
   709     def _StartInkscape(self):
       
   710         svgfile = self._getSVGpath()
       
   711         open_inkscape = True
       
   712         if not self.GetCTRoot().CheckProjectPathPerm():
       
   713             dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
       
   714                                       _("You don't have write permissions.\nOpen Inkscape anyway ?"),
       
   715                                       _("Open Inkscape"),
       
   716                                       wx.YES_NO | wx.ICON_QUESTION)
       
   717             open_inkscape = dialog.ShowModal() == wx.ID_YES
       
   718             dialog.Destroy()
       
   719         if open_inkscape:
       
   720             if not os.path.isfile(svgfile):
       
   721                 # make a copy of default svg from source
       
   722                 default = self.getDefaultSVG()
       
   723                 shutil.copyfile(default, svgfile)
       
   724             open_svg(svgfile)
       
   725 
       
   726     def _StartPOEdit(self, POFile):
       
   727         open_poedit = True
       
   728         if not self.GetCTRoot().CheckProjectPathPerm():
       
   729             dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
       
   730                                       _("You don't have write permissions.\nOpen POEdit anyway ?"),
       
   731                                       _("Open POEdit"),
       
   732                                       wx.YES_NO | wx.ICON_QUESTION)
       
   733             open_poedit = dialog.ShowModal() == wx.ID_YES
       
   734             dialog.Destroy()
       
   735         if open_poedit:
       
   736             open_pofile(POFile)
       
   737 
       
   738     def _EditPO(self):
       
   739         """ Select a specific translation and edit it with POEdit """
       
   740         project_path = self.CTNPath()
       
   741         dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "",  _("PO files (*.po)|*.po"), wx.OPEN)
       
   742         if dialog.ShowModal() == wx.ID_OK:
       
   743             POFile = dialog.GetPath()
       
   744             if os.path.isfile(POFile):
       
   745                 if os.path.relpath(POFile, project_path) == os.path.basename(POFile):
       
   746                     self._StartPOEdit(POFile)
       
   747                 else:
       
   748                     self.GetCTRoot().logger.write_error(_("PO file misplaced: %s is not in %s\n") % (POFile,project_path))
       
   749             else:
       
   750                 self.GetCTRoot().logger.write_error(_("PO file does not exist: %s\n") % POFile)
       
   751         dialog.Destroy()
       
   752 
       
   753     def _OpenPOT(self):
       
   754         """ Start POEdit with untouched empty catalog """
       
   755         POFile = self._getPOTpath()
       
   756         if os.path.isfile(POFile):
       
   757             self._StartPOEdit(POFile)
       
   758         else:
       
   759             self.GetCTRoot().logger.write_error(_("POT file does not exist, add translatable text (label starting with '_') in Inkscape first\n"))
       
   760 
       
   761     def _AddFont(self):
       
   762         dialog = wx.FileDialog(
       
   763             self.GetCTRoot().AppFrame,
       
   764             _("Choose a font"),
       
   765             os.path.expanduser("~"),
       
   766             "",
       
   767             _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.OPEN)
       
   768 
       
   769         if dialog.ShowModal() == wx.ID_OK:
       
   770             fontfile = dialog.GetPath()
       
   771             if os.path.isfile(fontfile):
       
   772                 familyname, uniquename, formatname, mimetype = GetFontTypeAndFamilyName(fontfile)
       
   773             else:
       
   774                 self.GetCTRoot().logger.write_error(
       
   775                     _('Selected font %s is not a readable file\n')%fontfile)
       
   776                 return
       
   777             if familyname is None or uniquename is None or formatname is None or mimetype is None:
       
   778                 self.GetCTRoot().logger.write_error(
       
   779                     _('Selected font file %s is invalid or incompatible\n')%fontfile)
       
   780                 return
       
   781 
       
   782             project_path = self.CTNPath()
       
   783 
       
   784             fontfname = uniquename + "." + mimetype.split('/')[1]
       
   785             fontdir = os.path.join(project_path, "fonts") 
       
   786             newfontfile = os.path.join(fontdir, fontfname) 
       
   787 
       
   788             if not os.path.exists(fontdir):
       
   789                 os.mkdir(fontdir)
       
   790 
       
   791             shutil.copyfile(fontfile, newfontfile)
       
   792 
       
   793             self.GetCTRoot().logger.write(
       
   794                 _('Added font %s as %s\n')%(fontfile,newfontfile))
       
   795 
       
   796     def _DelFont(self):
       
   797         project_path = self.CTNPath()
       
   798         fontdir = os.path.join(project_path, "fonts") 
       
   799         dialog = wx.FileDialog(
       
   800             self.GetCTRoot().AppFrame,
       
   801             _("Choose a font to remove"),
       
   802             fontdir,
       
   803             "",
       
   804             _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.OPEN)
       
   805         if dialog.ShowModal() == wx.ID_OK:
       
   806             fontfile = dialog.GetPath()
       
   807             if os.path.isfile(fontfile):
       
   808                 if os.path.relpath(fontfile, fontdir) == os.path.basename(fontfile):
       
   809                     os.remove(fontfile) 
       
   810                     self.GetCTRoot().logger.write(
       
   811                         _('Removed font %s\n')%fontfile)
       
   812                 else:
       
   813                     self.GetCTRoot().logger.write_error(
       
   814                         _("Font to remove %s is not in %s\n") % (fontfile,fontdir))
       
   815             else:
       
   816                 self.GetCTRoot().logger.write_error(
       
   817                     _("Font file does not exist: %s\n") % fontfile)
       
   818         
       
   819     ## In case one day we support more than one heartbeat
       
   820     # def CTNGlobalInstances(self):
       
   821     #     view_name = self.BaseParams.getName()
       
   822     #     return [(view_name + "_HEARTBEAT", "HMI_INT", "")]
       
   823 
       
   824     def GetIconName(self):
       
   825         return "SVGHMI"