Edouard@2745: #!/usr/bin/env python
Edouard@2745: # -*- coding: utf-8 -*-
Edouard@2745: 
Edouard@2745: # This file is part of Beremiz
edouard@3197: # Copyright (C) 2021: Edouard TISSERANT
Edouard@2745: #
Edouard@2745: # See COPYING file for copyrights details.
Edouard@2745: 
kinsamanka@3750: 
Edouard@2745: import os
edouard@3573: import sys
Edouard@2745: import shutil
Edouard@2789: import hashlib
Edouard@2823: import shlex
edouard@3156: import time
Edouard@2745: 
Edouard@2745: import wx
Edouard@2816: 
Edouard@2816: from lxml import etree
Edouard@2816: from lxml.etree import XSLTApplyError
Edouard@2745: 
Edouard@2745: import util.paths as paths
Edouard@2745: from POULibrary import POULibrary
Edouard@2756: from docutil import open_svg, get_inkscape_path
Edouard@2745: 
Edouard@2756: from util.ProcessLogger import ProcessLogger
Edouard@2764: from runtime.typemapping import DebugTypesSize
Edouard@2767: import targets
Edouard@2816: from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
Edouard@2816: from XSLTransform import XSLTransform
edouard@3201: from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations,\
edouard@3214:                         MatchTranslations, TranslationToEtree, open_pofile,\
edouard@3214:                         GetPoFiles
edouard@3197: from svghmi.hmi_tree import HMI_TYPES, HMITreeNode, SPECIAL_NODES 
edouard@3201: from svghmi.ui import SVGHMI_UI
edouard@3210: from svghmi.fonts import GetFontTypeAndFamilyName, GetCSSFontFaceFromFontFile
Edouard@2745: 
Edouard@2753: 
Edouard@2753: ScriptDirectory = paths.AbsDir(__file__)
Edouard@2753: 
Edouard@2788: 
Edouard@2763: # module scope for HMITree root
Edouard@2763: # so that CTN can use HMITree deduced in Library
Edouard@2817: # note: this only works because library's Generate_C is
Edouard@2763: #       systematicaly invoked before CTN's CTNGenerate_C
Edouard@2763: 
Edouard@3270: 
Edouard@2745: class SVGHMILibrary(POULibrary):
edouard@3880: 
edouard@3880:     hmi_tree_root = None
edouard@3880: 
edouard@3880:     maxConnectionsTotal = 0
edouard@3880: 
Edouard@2745:     def GetLibraryPath(self):
Edouard@2750:          return paths.AbsNeighbourFile(__file__, "pous.xml")
Edouard@2745: 
Edouard@2749:     def Generate_C(self, buildpath, varlist, IECCFLAGS):
edouard@3880: 
edouard@3880:         self.maxConnectionsTotal = 0
Edouard@3341: 
Edouard@3287:         already_found_watchdog = False
edouard@3290:         found_SVGHMI_instance = False
Edouard@3287:         for CTNChild in self.GetCTR().IterChildren():
Edouard@3287:             if isinstance(CTNChild, SVGHMI):
edouard@3290:                 found_SVGHMI_instance = True
Edouard@3287:                 # collect maximum connection total for all svghmi nodes
edouard@3880:                 self.maxConnectionsTotal += CTNChild.GetParamsAttributes("SVGHMI.MaxConnections")["value"]
Edouard@3287: 
Edouard@3287:                 # spot watchdog abuse
Edouard@3287:                 if CTNChild.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]:
Edouard@3287:                     if already_found_watchdog:
Edouard@3287:                         self.FatalError("SVGHMI: Only one watchdog enabled HMI allowed")
Edouard@3287:                     already_found_watchdog = True
Edouard@3287: 
edouard@3290:         if not found_SVGHMI_instance:
edouard@3290:             self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.")
edouard@3290: 
Edouard@3287: 
Edouard@2757:         """
Edouard@2757:         PLC Instance Tree:
Edouard@2757:           prog0
Edouard@2757:            +->v1 HMI_INT
Edouard@2757:            +->v2 HMI_INT
Edouard@2757:            +->fb0 (type mhoo)
Edouard@2814:            |   +->va HMI_NODE
Edouard@2757:            |   +->v3 HMI_INT
Edouard@2757:            |   +->v4 HMI_INT
Edouard@2757:            |
Edouard@2757:            +->fb1 (type mhoo)
Edouard@2814:            |   +->va HMI_NODE
Edouard@2757:            |   +->v3 HMI_INT
Edouard@2757:            |   +->v4 HMI_INT
Edouard@2757:            |
Edouard@2757:            +->fb2
Edouard@2757:                +->v5 HMI_IN
Edouard@2757: 
Edouard@2757:         HMI tree:
Edouard@2757:           hmi0
Edouard@2757:            +->v1
Edouard@2757:            +->v2
Edouard@2814:            +->fb0 class:va
Edouard@2757:            |   +-> v3
Edouard@2757:            |   +-> v4
Edouard@2757:            |
Edouard@2814:            +->fb1 class:va
Edouard@2757:            |   +-> v3
Edouard@2757:            |   +-> v4
Edouard@2757:            |
Edouard@2757:            +->v5
Edouard@2757: 
Edouard@2757:         """
Edouard@2757: 
Edouard@2749:         # Filter known HMI types
Edouard@2749:         hmi_types_instances = [v for v in varlist if v["derived"] in HMI_TYPES]
Edouard@2758: 
edouard@3880:         self.hmi_tree_root = None
edouard@2890: 
edouard@2890:         # take first HMI_NODE (placed as special node), make it root
edouard@2890:         for i,v in enumerate(hmi_types_instances):
edouard@2890:             path = v["IEC_path"].split(".")
edouard@2890:             derived = v["derived"]
Edouard@2965:             if derived == "HMI_NODE":
edouard@3880:                 self.hmi_tree_root = HMITreeNode(path, "", derived, v["type"], v["vartype"], v["C_path"])
edouard@2890:                 hmi_types_instances.pop(i)
edouard@2890:                 break
edouard@2890: 
Edouard@2757:         # deduce HMI tree from PLC HMI_* instances
Edouard@2757:         for v in hmi_types_instances:
Edouard@2822:             path = v["IEC_path"].split(".")
Edouard@2758:             # ignores variables starting with _TMP_
Edouard@2758:             if path[-1].startswith("_TMP_"):
Edouard@2758:                 continue
Edouard@3381:             vartype = v["vartype"]
Edouard@3381:             # ignores external variables
Edouard@3381:             if vartype == "EXT":
Edouard@3381:                 continue
Edouard@2814:             derived = v["derived"]
Edouard@2814:             kwargs={}
Edouard@2814:             if derived == "HMI_NODE":
Edouard@2822:                 # TODO : make problem if HMI_NODE used in CONFIG or RESOURCE
Edouard@2814:                 name = path[-2]
Edouard@2814:                 kwargs['hmiclass'] = path[-1]
Edouard@2814:             else:
Edouard@2814:                 name = path[-1]
Edouard@3381:             new_node = HMITreeNode(path, name, derived, v["type"], vartype, v["C_path"], **kwargs)
edouard@3880:             placement_result = self.hmi_tree_root.place_node(new_node)
Edouard@2965:             if placement_result is not None:
Edouard@2965:                 cause, problematic_node = placement_result
Edouard@2965:                 if cause == "Non_Unique":
Edouard@2965:                     message = _("HMI tree nodes paths are not unique.\nConflicting variable: {} {}").format(
Edouard@2965:                         ".".join(problematic_node.path),
Edouard@2965:                         ".".join(new_node.path))
Edouard@2965: 
Edouard@2965:                     last_FB = None 
Edouard@3381:                     for _v in varlist:
Edouard@3381:                         if _v["vartype"] == "FB":
Edouard@3381:                             last_FB = _v 
Edouard@3381:                         if _v["C_path"] == problematic_node:
Edouard@2965:                             break
Edouard@2965:                     if last_FB is not None:
Edouard@2965:                         failing_parent = last_FB["type"]
Edouard@2965:                         message += "\n"
Edouard@2965:                         message += _("Solution: Add HMI_NODE at beginning of {}").format(failing_parent)
Edouard@2965: 
Edouard@2965:                 elif cause in ["Late_HMI_NODE", "Duplicate_HMI_NODE"]:
Edouard@2965:                     cause, problematic_node = placement_result
Edouard@2965:                     message = _("There must be only one occurrence of HMI_NODE before any HMI_* variable in POU.\nConflicting variable: {} {}").format(
Edouard@2965:                         ".".join(problematic_node.path),
Edouard@2965:                         ".".join(new_node.path))
Edouard@2965: 
Edouard@2965:                 self.FatalError("SVGHMI : " + message)
Edouard@2757: 
edouard@3880:         self.on_hmitree_update()
Edouard@2816: 
Edouard@2764:         variable_decl_array = []
Edouard@2764:         extern_variables_declarations = []
Edouard@2765:         buf_index = 0
Edouard@2775:         item_count = 0
Edouard@2822:         found_heartbeat = False
Edouard@2822: 
Edouard@2822:         hearbeat_IEC_path = ['CONFIG', 'HEARTBEAT']
Edouard@2828: 
edouard@3880:         for node in self.hmi_tree_root.traverse():
Edouard@2828:             if not found_heartbeat and node.path == hearbeat_IEC_path:
Edouard@2822:                 hmi_tree_hearbeat_index = item_count
Edouard@2822:                 found_heartbeat = True
Edouard@2822:                 extern_variables_declarations += [
Edouard@2822:                     "#define heartbeat_index "+str(hmi_tree_hearbeat_index)
Edouard@2822:                 ]
Edouard@2866:             if hasattr(node, "iectype"):
Edouard@2764:                 sz = DebugTypesSize.get(node.iectype, 0)
Edouard@2764:                 variable_decl_array += [
Edouard@3399:                     "HMITREE_ITEM_INITIALIZER(" + node.cpath + ", " + node.iectype + {
Edouard@2764:                         "EXT": "_P_ENUM",
Edouard@2764:                         "IN":  "_P_ENUM",
Edouard@2764:                         "MEM": "_O_ENUM",
Edouard@2764:                         "OUT": "_O_ENUM",
Edouard@2764:                         "VAR": "_ENUM"
Edouard@2765:                     }[node.vartype] + ", " +
Edouard@3399:                     str(buf_index) + ")"]
Edouard@2765:                 buf_index += sz
Edouard@2775:                 item_count += 1
Edouard@2764:                 if len(node.path) == 1:
Edouard@2764:                     extern_variables_declarations += [
Edouard@2764:                         "extern __IEC_" + node.iectype + "_" +
edouard@3863:                         "t" if node.vartype == "VAR" else "p"
Edouard@2822:                         + node.cpath + ";"]
Edouard@2828: 
Edouard@2822:         assert(found_heartbeat)
Edouard@2764: 
Edouard@2764:         # TODO : filter only requiered external declarations
Edouard@2828:         for v in varlist:
Edouard@2828:             if v["C_path"].find('.') < 0:
Edouard@2764:                 extern_variables_declarations += [
Edouard@2764:                     "extern %(type)s %(C_path)s;" % v]
Edouard@2764: 
Edouard@2764:         # TODO check if programs need to be declared separately
Edouard@2764:         # "programs_declarations": "\n".join(["extern %(type)s %(C_path)s;" %
Edouard@2764:         #                                     p for p in self._ProgramList]),
Edouard@2764: 
Edouard@2771:         # C code to observe/access HMI tree variables
Edouard@2749:         svghmi_c_filepath = paths.AbsNeighbourFile(__file__, "svghmi.c")
Edouard@2749:         svghmi_c_file = open(svghmi_c_filepath, 'r')
Edouard@2749:         svghmi_c_code = svghmi_c_file.read()
Edouard@2749:         svghmi_c_file.close()
Edouard@2817:         svghmi_c_code = svghmi_c_code % {
Edouard@2764:             "variable_decl_array": ",\n".join(variable_decl_array),
Edouard@2764:             "extern_variables_declarations": "\n".join(extern_variables_declarations),
Edouard@2767:             "buffer_size": buf_index,
Edouard@2775:             "item_count": item_count,
Edouard@2768:             "var_access_code": targets.GetCode("var_access.c"),
Edouard@2788:             "PLC_ticktime": self.GetCTR().GetTicktime(),
edouard@3880:             "hmi_hash_ints": ",".join(map(str,self.hmi_tree_root.hash())),
edouard@3880:             "max_connections": self.maxConnectionsTotal
Edouard@2764:             }
Edouard@2749: 
Edouard@2749:         gen_svghmi_c_path = os.path.join(buildpath, "svghmi.c")
Edouard@2749:         gen_svghmi_c = open(gen_svghmi_c_path, 'w')
Edouard@2749:         gen_svghmi_c.write(svghmi_c_code)
Edouard@2749:         gen_svghmi_c.close()
Edouard@2749: 
Edouard@2771:         # Python based WebSocket HMITree Server
Edouard@2771:         svghmiserverfile = open(paths.AbsNeighbourFile(__file__, "svghmi_server.py"), 'r')
Edouard@2771:         svghmiservercode = svghmiserverfile.read()
Edouard@2771:         svghmiserverfile.close()
Edouard@2771: 
Edouard@2993:         runtimefile_path = os.path.join(buildpath, "runtime_00_svghmi.py")
Edouard@2771:         runtimefile = open(runtimefile_path, 'w')
Edouard@2771:         runtimefile.write(svghmiservercode)
Edouard@2771:         runtimefile.close()
Edouard@2771: 
edouard@3176:         # Backup HMI Tree in XML form so that it can be loaded without building
edouard@3176:         hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
edouard@3180:         hmitree_backup_file = open(hmitree_backup_path, 'wb')
edouard@3880:         hmitree_backup_file.write(etree.tostring(self.hmi_tree_root.etree()))
edouard@3176:         hmitree_backup_file.close()
edouard@3176: 
Edouard@2771:         return ((["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "",
Edouard@2993:                 ("runtime_00_svghmi.py", open(runtimefile_path, "rb")))
Edouard@2993:                 #         ^
Edouard@2993:                 # note the double zero after "runtime_", 
Edouard@2993:                 # to ensure placement before other CTN generated code in execution order
Edouard@2745: 
edouard@3267:     def GlobalInstances(self):
edouard@3267:         """ Adds HMI tree root and hearbeat to PLC Configuration's globals """
edouard@3267:         return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES]
edouard@3267: 
edouard@3267: 
edouard@3880:     registered_uis = []
edouard@3880:     def on_hmitree_update(self):
edouard@3880:         for uiref in self.registered_uis[:]:
edouard@3880:             obj = uiref()
edouard@3880:             if obj is None:
edouard@3880:                 self.registered_uis.remove(uiref)
edouard@3880:             else:
edouard@3880:                 obj.HMITreeUpdate(self.hmi_tree_root)
edouard@3880: 
edouard@3880: 
edouard@3880:     def Register_SVGHMI_UI_for_HMI_tree_updates(self, uiref):
edouard@3880:         self.registered_uis.append(uiref)
Edouard@2818: 
Edouard@2818: 
Edouard@2818: class SVGHMIEditor(ConfTreeNodeEditor):
Edouard@2818:     CONFNODEEDITOR_TABS = [
edouard@3201:         (_("HMI Tree"), "CreateSVGHMI_UI")]
edouard@3201: 
edouard@3499:     def __init__(self, parent, controler, window):
edouard@3499:         ConfTreeNodeEditor.__init__(self, parent, controler, window)
edouard@3499:         self.Controler = controler
edouard@3499: 
edouard@3201:     def CreateSVGHMI_UI(self, parent):
edouard@3880:         ctroot = self.Controler.GetCTRoot()
edouard@3880:         svghmilib = ctroot.Libraries["SVGHMI"]
edouard@3880: 
edouard@3880:         if svghmilib.hmi_tree_root is None:
edouard@3880:             buildpath = ctroot._getBuildPath()
edouard@3176:             hmitree_backup_path = os.path.join(buildpath, "hmitree.xml")
edouard@3176:             if os.path.exists(hmitree_backup_path):
edouard@3180:                 hmitree_backup_file = open(hmitree_backup_path, 'rb')
edouard@3880:                 svghmilib.hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
edouard@3880: 
edouard@3880:         ret = SVGHMI_UI(parent, self.Controler, svghmilib.Register_SVGHMI_UI_for_HMI_tree_updates)
edouard@3880: 
edouard@3880:         svghmilib.on_hmitree_update()
Edouard@3208: 
Edouard@3208:         return ret
Edouard@2818: 
edouard@3573: if sys.platform.startswith('win'):
edouard@3498:     default_cmds={
edouard@3498:         "launch":"cmd.exe /c 'start msedge {url}'",
edouard@3498:         "watchdog":"cmd.exe /k 'echo watchdog for {url} !'"}
kinsamanka@3750: elif "SNAP" in os.environ:
edouard@3573:     default_cmds={
edouard@3573:         "launch":"xdg-open {url}",
edouard@3573:         "watchdog":"echo Watchdog for {name} !"}
edouard@3348: else:
edouard@3498:     default_cmds={
edouard@3498:         "launch":"chromium {url}",
edouard@3498:         "watchdog":"echo Watchdog for {name} !"}
edouard@3348: 
Edouard@2745: class SVGHMI(object):
Edouard@2771:     XSD = """<?xml version="1.0" encoding="utf-8" ?>
Edouard@2745:     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
Edouard@2745:       <xsd:element name="SVGHMI">
Edouard@2745:         <xsd:complexType>
edouard@3498:           <xsd:attribute name="OnStart" type="xsd:string" use="optional" default="%(launch)s"/>
edouard@3348:           <xsd:attribute name="OnStop" type="xsd:string" use="optional" default=""/>
edouard@3498:           <xsd:attribute name="OnWatchdog" type="xsd:string" use="optional" default="%(watchdog)s"/>
edouard@3269:           <xsd:attribute name="EnableWatchdog" type="xsd:boolean" use="optional" default="false"/>
Edouard@3277:           <xsd:attribute name="WatchdogInitial" use="optional" default="30">
Edouard@3277:             <xsd:simpleType>
Edouard@3277:                 <xsd:restriction base="xsd:integer">
Edouard@3277:                     <xsd:minInclusive value="2"/>
Edouard@3277:                     <xsd:maxInclusive value="600"/>
Edouard@3277:                 </xsd:restriction>
Edouard@3277:             </xsd:simpleType>
Edouard@3277:           </xsd:attribute>
Edouard@3277:           <xsd:attribute name="WatchdogInterval" use="optional" default="5">
Edouard@3277:             <xsd:simpleType>
Edouard@3277:                 <xsd:restriction base="xsd:integer">
Edouard@3277:                     <xsd:minInclusive value="2"/>
Edouard@3277:                     <xsd:maxInclusive value="60"/>
Edouard@3277:                 </xsd:restriction>
Edouard@3277:             </xsd:simpleType>
Edouard@3277:           </xsd:attribute>
edouard@3269:           <xsd:attribute name="Port" type="xsd:integer" use="optional" default="8008"/>
edouard@3269:           <xsd:attribute name="Interface" type="xsd:string" use="optional" default="localhost"/>
Edouard@3270:           <xsd:attribute name="Path" type="xsd:string" use="optional" default="{name}"/>
Edouard@3277:           <xsd:attribute name="MaxConnections" use="optional" default="16">
Edouard@3277:             <xsd:simpleType>
Edouard@3277:                 <xsd:restriction base="xsd:integer">
Edouard@3277:                     <xsd:minInclusive value="1"/>
Edouard@3277:                     <xsd:maxInclusive value="1024"/>
Edouard@3277:                 </xsd:restriction>
Edouard@3277:             </xsd:simpleType>
Edouard@3277:           </xsd:attribute>
Edouard@2745:         </xsd:complexType>
Edouard@2745:       </xsd:element>
Edouard@2745:     </xsd:schema>
edouard@3498:     """%default_cmds
Edouard@2816: 
Edouard@2816:     EditorType = SVGHMIEditor
Edouard@2745: 
Edouard@2745:     ConfNodeMethods = [
Edouard@2745:         {
Edouard@2745:             "bitmap":    "ImportSVG",
Edouard@2745:             "name":    _("Import SVG"),
Edouard@2745:             "tooltip": _("Import SVG"),
Edouard@2745:             "method":   "_ImportSVG"
Edouard@2745:         },
Edouard@2745:         {
edouard@3210:             "bitmap":    "EditSVG",
Edouard@2745:             "name":    _("Inkscape"),
Edouard@2745:             "tooltip": _("Edit HMI"),
Edouard@2745:             "method":   "_StartInkscape"
Edouard@2745:         },
Edouard@3112:         {
edouard@3210:             "bitmap":    "OpenPOT",
Edouard@3112:             "name":    _("New lang"),
Edouard@3112:             "tooltip": _("Open non translated message catalog (POT) to start new language"),
Edouard@3112:             "method":   "_OpenPOT"
Edouard@3112:         },
Edouard@3112:         {
edouard@3210:             "bitmap":    "EditPO",
Edouard@3112:             "name":    _("Edit lang"),
Edouard@3112:             "tooltip": _("Edit existing message catalog (PO) for specific language"),
Edouard@3112:             "method":   "_EditPO"
Edouard@3112:         },
edouard@3210:         {
edouard@3210:             "bitmap":    "AddFont",
edouard@3210:             "name":    _("Add Font"),
Edouard@3211:             "tooltip": _("Add TTF, OTF or WOFF font to be embedded in HMI"),
edouard@3210:             "method":   "_AddFont"
edouard@3210:         },
edouard@3210:         {
edouard@3210:             "bitmap":    "DelFont",
edouard@3210:             "name":    _("Delete Font"),
edouard@3210:             "tooltip": _("Remove font previously added to HMI"),
edouard@3210:             "method":   "_DelFont"
edouard@3210:         },
Edouard@2745:     ]
Edouard@2745: 
Edouard@2745:     def _getSVGpath(self, project_path=None):
Edouard@2745:         if project_path is None:
Edouard@2745:             project_path = self.CTNPath()
Edouard@2781:         return os.path.join(project_path, "svghmi.svg")
Edouard@2745: 
edouard@3108:     def _getPOTpath(self, project_path=None):
edouard@3108:         if project_path is None:
edouard@3108:             project_path = self.CTNPath()
edouard@3108:         return os.path.join(project_path, "messages.pot")
Edouard@2745: 
Edouard@2745:     def OnCTNSave(self, from_project_path=None):
Edouard@2745:         if from_project_path is not None:
Edouard@2745:             shutil.copyfile(self._getSVGpath(from_project_path),
Edouard@2745:                             self._getSVGpath())
edouard@3498: 
edouard@3498:             potpath = self._getPOTpath(from_project_path)
edouard@3498:             if os.path.isfile(potpath):
edouard@3498:                 shutil.copyfile(potpath, self._getPOTpath())
edouard@3498:                 # copy .PO files
edouard@3498:                 for _name, pofile in GetPoFiles(from_project_path):
edouard@3498:                     shutil.copy(pofile, self.CTNPath())
Edouard@2750:         return True
Edouard@2745: 
Edouard@2753:     def GetSVGGeometry(self):
Edouard@3170:         self.ProgressStart("inkscape", "collecting SVG geometry (Inkscape)")
Edouard@2756:         # invoke inskscape -S, csv-parse output, produce elements
Edouard@2756:         InkscapeGeomColumns = ["Id", "x", "y", "w", "h"]
Edouard@2756: 
edouard@3900:         inkpath = get_inkscape_path().decode()
Edouard@3052:         if inkpath is None:
Edouard@3052:             self.FatalError("SVGHMI: inkscape is not installed.")
Edouard@3052: 
Edouard@2756:         svgpath = self._getSVGpath()
Edouard@3032:         status, result, _err_result = ProcessLogger(self.GetCTRoot().logger,
edouard@3817:                                                      [inkpath, '-S', svgpath],
Edouard@2756:                                                      no_stdout=True,
Edouard@2756:                                                      no_stderr=True).spin()
Edouard@3032:         if status != 0:
Edouard@3052:             self.FatalError("SVGHMI: inkscape couldn't extract geometry from given SVG.")
Edouard@3032: 
Edouard@2756:         res = []
Edouard@2756:         for line in result.split():
Edouard@2756:             strippedline = line.strip()
Edouard@2756:             attrs = dict(
kinsamanka@3750:                 list(zip(InkscapeGeomColumns, line.strip().split(','))))
Edouard@2756: 
Edouard@2756:             res.append(etree.Element("bbox", **attrs))
Edouard@2756: 
Edouard@3170:         self.ProgressEnd("inkscape")
Edouard@2756:         return res
Edouard@2753: 
Edouard@2763:     def GetHMITree(self):
edouard@3880:         ctroot = self.GetCTRoot()
edouard@3880:         svghmilib = ctroot.Libraries["SVGHMI"]
Edouard@3170:         self.ProgressStart("hmitree", "getting HMI tree")
edouard@3880:         res = [svghmilib.hmi_tree_root.etree(add_hash=True)]
Edouard@3170:         self.ProgressEnd("hmitree")
Edouard@2763:         return res
Edouard@2763: 
edouard@3108:     def GetTranslations(self, _context, msgs):
Edouard@3170:         self.ProgressStart("i18n", "getting Translations")
edouard@3113:         messages = EtreeToMessages(msgs)
edouard@3113: 
edouard@3140:         if len(messages) == 0:
Edouard@3170:             self.ProgressEnd("i18n")
edouard@3140:             return
edouard@3140: 
edouard@3113:         SaveCatalog(self._getPOTpath(), messages)
edouard@3113: 
edouard@3113:         translations = ReadTranslations(self.CTNPath())
edouard@3113:             
edouard@3113:         langs,translated_messages = MatchTranslations(translations, messages, 
edouard@3113:             errcallback=self.GetCTRoot().logger.write_warning)
edouard@3113: 
edouard@3165:         ret = TranslationToEtree(langs,translated_messages)
edouard@3165: 
Edouard@3170:         self.ProgressEnd("i18n")
edouard@3165: 
edouard@3165:         return ret
edouard@3108: 
edouard@3214:     def GetFontsFiles(self):
Edouard@3211:         project_path = self.CTNPath()
Edouard@3211:         fontdir = os.path.join(project_path, "fonts") 
edouard@3214:         if os.path.isdir(fontdir):
edouard@3214:             return [os.path.join(fontdir,f) for f in sorted(os.listdir(fontdir))]
edouard@3214:         return []
edouard@3214: 
edouard@3214:     def GetFonts(self, _context):
Edouard@3211:         css_parts = []
Edouard@3211: 
edouard@3214:         for fontfile in self.GetFontsFiles():
Edouard@3211:             if os.path.isfile(fontfile):
Edouard@3211:                 css_parts.append(GetCSSFontFaceFromFontFile(fontfile))
Edouard@3211: 
Edouard@3211:         return "".join(css_parts)
Edouard@3211: 
Edouard@3170:     times_msgs = {}
Edouard@3170:     indent = 1
Edouard@3170:     def ProgressStart(self, k, m):
Edouard@3170:         self.times_msgs[k] = (time.time(), m)
Edouard@3170:         self.GetCTRoot().logger.write("    "*self.indent + "Start %s...\n"%m)
Edouard@3170:         self.indent = self.indent + 1
Edouard@3170: 
Edouard@3170:     def ProgressEnd(self, k):
Edouard@3170:         t = time.time()
Edouard@3170:         oldt, m = self.times_msgs[k]
Edouard@3170:         self.indent = self.indent - 1
Edouard@3170:         self.GetCTRoot().logger.write("    "*self.indent + "... finished in %.3fs\n"%(t - oldt))
edouard@3156: 
Edouard@3286:     def get_SVGHMI_options(self):
edouard@3290:         name = self.BaseParams.getName()
Edouard@3286:         port = self.GetParamsAttributes("SVGHMI.Port")["value"]
Edouard@3286:         interface = self.GetParamsAttributes("SVGHMI.Interface")["value"]
edouard@3290:         path = self.GetParamsAttributes("SVGHMI.Path")["value"].format(name=name)
Edouard@3286:         if path and path[0]=='/':
Edouard@3286:             path = path[1:]
Edouard@3286:         enable_watchdog = self.GetParamsAttributes("SVGHMI.EnableWatchdog")["value"]
Edouard@3286:         url="http://"+interface+("" if port==80 else (":"+str(port))
Edouard@3286:             ) + (("/"+path) if path else ""
Edouard@3286:             ) + ("#watchdog" if enable_watchdog else "")
Edouard@3286: 
Edouard@3286:         return dict(
edouard@3290:             name=name,
Edouard@3286:             port=port,
Edouard@3286:             interface=interface,
Edouard@3286:             path=path,
Edouard@3286:             enable_watchdog=enable_watchdog,
Edouard@3286:             url=url)
Edouard@3286: 
Edouard@2745:     def CTNGenerate_C(self, buildpath, locations):
edouard@3880:         ctroot = self.GetCTRoot()
edouard@3880:         svghmilib = ctroot.Libraries["SVGHMI"]
edouard@3880:         hmi_tree_root = svghmilib.hmi_tree_root
edouard@3880:         
edouard@3290: 
edouard@3290:         if hmi_tree_root is None:
edouard@3290:             self.FatalError("SVGHMI : Library is not selected. Please select it in project config.")
Edouard@2745: 
Edouard@2771:         location_str = "_".join(map(str, self.GetCurrentLocation()))
Edouard@3286:         svghmi_options = self.get_SVGHMI_options()
Edouard@2771: 
Edouard@2745:         svgfile = self._getSVGpath()
Edouard@2771: 
Edouard@2771:         res = ([], "", False)
Edouard@2771: 
Edouard@2812:         target_fname = "svghmi_"+location_str+".xhtml"
Edouard@2771: 
edouard@3180:         build_path = self._getBuildPath()
edouard@3180:         target_path = os.path.join(build_path, target_fname)
Edouard@3375:         hash_path = os.path.join(build_path, "svghmi_"+location_str+".md5")
Edouard@2771: 
edouard@3880:         ctroot.logger.write("SVGHMI:\n")
edouard@3156: 
Edouard@2745:         if os.path.exists(svgfile):
Edouard@2753: 
edouard@3180:             hasher = hashlib.md5()
edouard@3180:             hmi_tree_root._hash(hasher)
Edouard@3218:             pofiles = GetPoFiles(self.CTNPath())
edouard@3214:             filestocheck = [svgfile] + \
Edouard@3218:                            (list(zip(*pofiles)[1]) if pofiles else []) + \
edouard@3214:                            self.GetFontsFiles()
edouard@3214: 
edouard@3214:             for filetocheck in filestocheck:
edouard@3214:                 with open(filetocheck, 'rb') as afile:
edouard@3214:                     while True:
edouard@3214:                         buf = afile.read(65536)
edouard@3214:                         if len(buf) > 0:
edouard@3214:                             hasher.update(buf)
edouard@3214:                         else:
edouard@3214:                             break
edouard@3180:             digest = hasher.hexdigest()
edouard@3180: 
edouard@3180:             if os.path.exists(hash_path):
edouard@3180:                 with open(hash_path, 'rb') as digest_file:
edouard@3180:                     last_digest = digest_file.read()
edouard@3180:             else:
edouard@3180:                 last_digest = None
edouard@3180:             
edouard@3180:             if digest != last_digest:
edouard@3180: 
edouard@3180:                 transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"),
edouard@3180:                               [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()),
edouard@3180:                                ("GetHMITree", lambda *_ignored:self.GetHMITree()),
edouard@3180:                                ("GetTranslations", self.GetTranslations),
Edouard@3211:                                ("GetFonts", self.GetFonts),
edouard@3180:                                ("ProgressStart", lambda _ign,k,m:self.ProgressStart(str(k),str(m))),
edouard@3180:                                ("ProgressEnd", lambda _ign,k:self.ProgressEnd(str(k)))])
edouard@3180: 
edouard@3180:                 self.ProgressStart("svg", "source SVG parsing")
edouard@3180: 
edouard@3180:                 # load svg as a DOM with Etree
edouard@3180:                 svgdom = etree.parse(svgfile)
edouard@3180: 
edouard@3180:                 self.ProgressEnd("svg")
edouard@3180: 
edouard@3180:                 # call xslt transform on Inkscape's SVG to generate XHTML
edouard@3180:                 try: 
edouard@3180:                     self.ProgressStart("xslt", "XSLT transform")
Edouard@3381:                     result = transform.transform(
Edouard@3381:                         svgdom, instance_name=location_str)  # , profile_run=True)
edouard@3180:                     self.ProgressEnd("xslt")
edouard@3180:                 except XSLTApplyError as e:
edouard@3290:                     self.FatalError("SVGHMI " + svghmi_options["name"] + ": " + e.message)
edouard@3180:                 finally:
edouard@3180:                     for entry in transform.get_error_log():
edouard@3180:                         message = "SVGHMI: "+ entry.message + "\n" 
edouard@3180:                         self.GetCTRoot().logger.write_warning(message)
edouard@3180: 
edouard@3180:                 target_file = open(target_path, 'wb')
edouard@3180:                 result.write(target_file, encoding="utf-8")
edouard@3180:                 target_file.close()
edouard@3180: 
edouard@3180:                 # print(str(result))
edouard@3180:                 # print(transform.xslt.error_log)
edouard@3180:                 # print(etree.tostring(result.xslt_profile,pretty_print=True))
edouard@3180: 
edouard@3817:                 with open(hash_path, 'w') as digest_file:
edouard@3180:                     digest_file.write(digest)
edouard@3180:             else:
edouard@3180:                 self.GetCTRoot().logger.write("    No changes - XSLT transformation skipped\n")
Edouard@2753: 
Edouard@2745:         else:
edouard@3180:             target_file = open(target_path, 'wb')
edouard@3875:             target_file.write(b"""<!DOCTYPE html>
Edouard@3274: <html xmlns="http://www.w3.org/1999/xhtml">
Edouard@3274: <head>
Edouard@3274: <title>SVGHMI</title>
Edouard@3274: </head>
Edouard@2771: <body>
Edouard@2771: <h1> No SVG file provided </h1>
Edouard@2771: </body>
Edouard@2771: </html>
Edouard@2771: """)
edouard@3180:             target_file.close()
Edouard@2771: 
Edouard@3372:             # In case no SVG is given, watchdog is useless
Edouard@3372:             svghmi_options["enable_watchdog"] = False
Edouard@3372: 
Edouard@2772:         res += ((target_fname, open(target_path, "rb")),)
Edouard@2828: 
Edouard@2823:         svghmi_cmds = {}
Edouard@2823:         for thing in ["Start", "Stop", "Watchdog"]:
Edouard@2823:              given_command = self.GetParamsAttributes("SVGHMI.On"+thing)["value"]
edouard@2824:              svghmi_cmds[thing] = (
edouard@2824:                 "Popen(" +
Edouard@3286:                 repr(shlex.split(given_command.format(**svghmi_options))) +
Edouard@3647:                 ")") if given_command else "None # no command given"
Edouard@2772: 
Edouard@2993:         runtimefile_path = os.path.join(buildpath, "runtime_%s_svghmi_.py" % location_str)
Edouard@2771:         runtimefile = open(runtimefile_path, 'w')
Edouard@2771:         runtimefile.write("""
Edouard@3647: #!/usr/bin/env python
Edouard@3647: # -*- coding: utf-8 -*-
Edouard@3647: 
Edouard@3647: # generated by beremiz/svghmi/svghmi.py
Edouard@3647: 
Edouard@3647: browser_proc = None
Edouard@3647: 
edouard@3269: def svghmi_{location}_watchdog_trigger():
Edouard@3647:     global browser_proc
Edouard@3681:     watchdog_proc = {svghmi_cmds[Watchdog]}
Edouard@3681:     waitpid_timeout(watchdog_proc, "SVGHMI watchdog triggered command")
Edouard@3662:     stop_proc = {svghmi_cmds[Stop]}
Edouard@3662:     waitpid_timeout(stop_proc, "SVGHMI stop command")
Edouard@3647:     waitpid_timeout(browser_proc, "SVGHMI browser process")
Edouard@3662:     browser_proc = {svghmi_cmds[Start]}
edouard@2824: 
Edouard@3270: max_svghmi_sessions = {maxConnections_total}
Edouard@2831: 
Edouard@2993: def _runtime_{location}_svghmi_start():
Edouard@3647:     global svghmi_watchdog, svghmi_servers, browser_proc
edouard@3269: 
edouard@3269:     srv = svghmi_servers.get("{interface}:{port}", None)
edouard@3269:     if srv is not None:
Edouard@3270:         svghmi_root, svghmi_listener, path_list = srv
edouard@3269:         if '{path}' in path_list:
edouard@3290:             raise Exception("SVGHMI {name}: path {path} already used on {interface}:{port}")
edouard@3269:     else:
edouard@3269:         svghmi_root = Resource()
Edouard@3270:         factory = HMIWebSocketServerFactory()
Edouard@3270:         factory.setProtocolOptions(maxConnections={maxConnections})
Edouard@3270: 
edouard@3817:         svghmi_root.putChild(b"ws", WebSocketResource(factory))
edouard@3269: 
edouard@3269:         svghmi_listener = reactor.listenTCP({port}, Site(svghmi_root), interface='{interface}')
edouard@3269:         path_list = []
edouard@3269:         svghmi_servers["{interface}:{port}"] = (svghmi_root, svghmi_listener, path_list)
edouard@3269: 
Edouard@2831:     svghmi_root.putChild(
edouard@3817:         b'{path}',
Edouard@2831:         NoCacheFile('{xhtml}',
edouard@3269:             defaultType='application/xhtml+xml'))
edouard@3269: 
edouard@3269:     path_list.append("{path}")
Edouard@2831: 
Edouard@3647:     browser_proc = {svghmi_cmds[Start]}
edouard@2824: 
edouard@3269:     if {enable_watchdog}:
edouard@3269:         if svghmi_watchdog is None:
edouard@3269:             svghmi_watchdog = Watchdog(
Edouard@3270:                 {watchdog_initial},
Edouard@3270:                 {watchdog_interval},
edouard@3269:                 svghmi_{location}_watchdog_trigger)
edouard@3269:         else:
edouard@3290:             raise Exception("SVGHMI {name}: only one watchdog allowed")
edouard@3269: 
Edouard@2831: 
Edouard@2993: def _runtime_{location}_svghmi_stop():
Edouard@3647:     global svghmi_watchdog, svghmi_servers, browser_proc
edouard@3269: 
Edouard@2831:     if svghmi_watchdog is not None:
Edouard@2831:         svghmi_watchdog.cancel()
Edouard@2831:         svghmi_watchdog = None
Edouard@2831: 
edouard@3269:     svghmi_root, svghmi_listener, path_list = svghmi_servers["{interface}:{port}"]
edouard@3878:     svghmi_root.delEntity(b'{path}')
edouard@3269: 
edouard@3269:     path_list.remove('{path}')
edouard@3269: 
edouard@3269:     if len(path_list)==0:
edouard@3878:         svghmi_root.delEntity(b"ws")
edouard@3269:         svghmi_listener.stopListening()
edouard@3269:         svghmi_servers.pop("{interface}:{port}")
edouard@3269: 
Edouard@3647:     stop_proc = {svghmi_cmds[Stop]}
Edouard@3647:     waitpid_timeout(stop_proc, "SVGHMI stop command")
Edouard@3647:     waitpid_timeout(browser_proc, "SVGHMI browser process")
Edouard@3647:     browser_proc = None
edouard@2824: 
edouard@2824:         """.format(location=location_str,
edouard@2824:                    xhtml=target_fname,
Edouard@2831:                    svghmi_cmds=svghmi_cmds,
Edouard@2831:                    watchdog_initial = self.GetParamsAttributes("SVGHMI.WatchdogInitial")["value"],
Edouard@2831:                    watchdog_interval = self.GetParamsAttributes("SVGHMI.WatchdogInterval")["value"],
Edouard@3270:                    maxConnections = self.GetParamsAttributes("SVGHMI.MaxConnections")["value"],
edouard@3880:                    maxConnections_total = svghmilib.maxConnectionsTotal,
Edouard@3286:                    **svghmi_options
Edouard@3286:         ))
Edouard@2771: 
Edouard@2771:         runtimefile.close()
Edouard@2771: 
Edouard@2993:         res += (("runtime_%s_svghmi.py" % location_str, open(runtimefile_path, "rb")),)
Edouard@2745: 
Edouard@2745:         return res
Edouard@2745: 
Edouard@2745:     def _ImportSVG(self):
edouard@3572:         dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a SVG file"), os.getcwd(), "",  _("SVG files (*.svg)|*.svg|All files|*.*"), wx.FD_OPEN)
Edouard@2745:         if dialog.ShowModal() == wx.ID_OK:
Edouard@2745:             svgpath = dialog.GetPath()
Edouard@2745:             if os.path.isfile(svgpath):
Edouard@2745:                 shutil.copy(svgpath, self._getSVGpath())
Edouard@2745:             else:
Edouard@2745:                 self.GetCTRoot().logger.write_error(_("No such SVG file: %s\n") % svgpath)
Edouard@2745:         dialog.Destroy()
Edouard@2745: 
Edouard@3286:     def getDefaultSVG(self):
Edouard@3286:         return os.path.join(ScriptDirectory, "default.svg")
Edouard@3286: 
Edouard@2745:     def _StartInkscape(self):
Edouard@2745:         svgfile = self._getSVGpath()
Edouard@2745:         open_inkscape = True
Edouard@2745:         if not self.GetCTRoot().CheckProjectPathPerm():
Edouard@2745:             dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
Edouard@2745:                                       _("You don't have write permissions.\nOpen Inkscape anyway ?"),
Edouard@2745:                                       _("Open Inkscape"),
Edouard@2745:                                       wx.YES_NO | wx.ICON_QUESTION)
Edouard@2745:             open_inkscape = dialog.ShowModal() == wx.ID_YES
Edouard@2745:             dialog.Destroy()
Edouard@2745:         if open_inkscape:
Edouard@2745:             if not os.path.isfile(svgfile):
Edouard@3274:                 # make a copy of default svg from source
Edouard@3286:                 default = self.getDefaultSVG()
Edouard@3274:                 shutil.copyfile(default, svgfile)
Edouard@2745:             open_svg(svgfile)
Edouard@2822: 
edouard@3108:     def _StartPOEdit(self, POFile):
edouard@3108:         open_poedit = True
edouard@3108:         if not self.GetCTRoot().CheckProjectPathPerm():
edouard@3108:             dialog = wx.MessageDialog(self.GetCTRoot().AppFrame,
edouard@3108:                                       _("You don't have write permissions.\nOpen POEdit anyway ?"),
edouard@3108:                                       _("Open POEdit"),
edouard@3108:                                       wx.YES_NO | wx.ICON_QUESTION)
edouard@3108:             open_poedit = dialog.ShowModal() == wx.ID_YES
edouard@3108:             dialog.Destroy()
edouard@3108:         if open_poedit:
Edouard@3112:             open_pofile(POFile)
Edouard@3112: 
Edouard@3112:     def _EditPO(self):
edouard@3108:         """ Select a specific translation and edit it with POEdit """
Edouard@3112:         project_path = self.CTNPath()
edouard@3572:         dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "",  _("PO files (*.po)|*.po"), wx.FD_OPEN)
Edouard@3112:         if dialog.ShowModal() == wx.ID_OK:
Edouard@3112:             POFile = dialog.GetPath()
Edouard@3112:             if os.path.isfile(POFile):
edouard@3113:                 if os.path.relpath(POFile, project_path) == os.path.basename(POFile):
Edouard@3112:                     self._StartPOEdit(POFile)
Edouard@3112:                 else:
Edouard@3112:                     self.GetCTRoot().logger.write_error(_("PO file misplaced: %s is not in %s\n") % (POFile,project_path))
Edouard@3112:             else:
Edouard@3158:                 self.GetCTRoot().logger.write_error(_("PO file does not exist: %s\n") % POFile)
Edouard@3112:         dialog.Destroy()
Edouard@3112: 
Edouard@3112:     def _OpenPOT(self):
edouard@3108:         """ Start POEdit with untouched empty catalog """
edouard@3108:         POFile = self._getPOTpath()
Edouard@3158:         if os.path.isfile(POFile):
Edouard@3158:             self._StartPOEdit(POFile)
Edouard@3158:         else:
Edouard@3158:             self.GetCTRoot().logger.write_error(_("POT file does not exist, add translatable text (label starting with '_') in Inkscape first\n"))
edouard@3108: 
edouard@3210:     def _AddFont(self):
Edouard@3211:         dialog = wx.FileDialog(
Edouard@3211:             self.GetCTRoot().AppFrame,
Edouard@3211:             _("Choose a font"),
Edouard@3211:             os.path.expanduser("~"),
Edouard@3211:             "",
edouard@3572:             _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.FD_OPEN)
Edouard@3211: 
Edouard@3211:         if dialog.ShowModal() == wx.ID_OK:
Edouard@3211:             fontfile = dialog.GetPath()
Edouard@3211:             if os.path.isfile(fontfile):
Edouard@3211:                 familyname, uniquename, formatname, mimetype = GetFontTypeAndFamilyName(fontfile)
Edouard@3211:             else:
Edouard@3211:                 self.GetCTRoot().logger.write_error(
Edouard@3211:                     _('Selected font %s is not a readable file\n')%fontfile)
Edouard@3211:                 return
Edouard@3211:             if familyname is None or uniquename is None or formatname is None or mimetype is None:
Edouard@3211:                 self.GetCTRoot().logger.write_error(
Edouard@3211:                     _('Selected font file %s is invalid or incompatible\n')%fontfile)
Edouard@3211:                 return
Edouard@3211: 
Edouard@3211:             project_path = self.CTNPath()
Edouard@3211: 
Edouard@3211:             fontfname = uniquename + "." + mimetype.split('/')[1]
Edouard@3211:             fontdir = os.path.join(project_path, "fonts") 
Edouard@3211:             newfontfile = os.path.join(fontdir, fontfname) 
Edouard@3211: 
Edouard@3211:             if not os.path.exists(fontdir):
Edouard@3211:                 os.mkdir(fontdir)
Edouard@3211: 
Edouard@3211:             shutil.copyfile(fontfile, newfontfile)
Edouard@3211: 
Edouard@3211:             self.GetCTRoot().logger.write(
Edouard@3211:                 _('Added font %s as %s\n')%(fontfile,newfontfile))
edouard@3210: 
edouard@3210:     def _DelFont(self):
Edouard@3211:         project_path = self.CTNPath()
Edouard@3211:         fontdir = os.path.join(project_path, "fonts") 
Edouard@3211:         dialog = wx.FileDialog(
Edouard@3211:             self.GetCTRoot().AppFrame,
Edouard@3211:             _("Choose a font to remove"),
Edouard@3211:             fontdir,
Edouard@3211:             "",
edouard@3572:             _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.FD_OPEN)
Edouard@3211:         if dialog.ShowModal() == wx.ID_OK:
Edouard@3211:             fontfile = dialog.GetPath()
Edouard@3211:             if os.path.isfile(fontfile):
Edouard@3211:                 if os.path.relpath(fontfile, fontdir) == os.path.basename(fontfile):
Edouard@3211:                     os.remove(fontfile) 
Edouard@3211:                     self.GetCTRoot().logger.write(
Edouard@3211:                         _('Removed font %s\n')%fontfile)
Edouard@3211:                 else:
Edouard@3211:                     self.GetCTRoot().logger.write_error(
Edouard@3211:                         _("Font to remove %s is not in %s\n") % (fontfile,fontdir))
Edouard@3211:             else:
Edouard@3211:                 self.GetCTRoot().logger.write_error(
Edouard@3211:                     _("Font file does not exist: %s\n") % fontfile)
edouard@3210:         
Edouard@3381:     def CTNGlobalInstances(self):
Edouard@3381:         location_str = "_".join(map(str, self.GetCurrentLocation()))
Edouard@3381:         return [("CURRENT_PAGE_"+location_str, "HMI_STRING", "")]
Edouard@3381: 
edouard@3267:     ## In case one day we support more than one heartbeat
edouard@3267:     #     view_name = self.BaseParams.getName()
edouard@3267:     #     return [(view_name + "_HEARTBEAT", "HMI_INT", "")]
Edouard@2822: 
Edouard@3159:     def GetIconName(self):
Edouard@3159:         return "SVGHMI"