Edouard@2745: #!/usr/bin/env python Edouard@2745: # -*- coding: utf-8 -*- Edouard@2745: Edouard@2745: # This file is part of Beremiz Edouard@2745: # Copyright (C) 2019: Edouard TISSERANT Edouard@2745: # Edouard@2745: # See COPYING file for copyrights details. Edouard@2745: Edouard@2745: from __future__ import absolute_import Edouard@2745: import os Edouard@2745: import shutil Edouard@2763: from itertools import izip, imap Edouard@2822: from pprint import pformat Edouard@2789: import hashlib Edouard@2816: import weakref Edouard@2823: import shlex edouard@3156: import time Edouard@2745: Edouard@2745: import wx Edouard@2816: import wx.dataview as dv 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@3114: from svghmi.i18n import EtreeToMessages, SaveCatalog, ReadTranslations, MatchTranslations, TranslationToEtree, open_pofile Edouard@2756: Edouard@2749: HMI_TYPES_DESC = { Edouard@2814: "HMI_NODE":{}, Edouard@2749: "HMI_STRING":{}, Edouard@2749: "HMI_INT":{}, edouard@3068: "HMI_BOOL":{}, edouard@3068: "HMI_REAL":{} Edouard@2749: } Edouard@2749: Edouard@2749: HMI_TYPES = HMI_TYPES_DESC.keys() Edouard@2745: Edouard@2753: Edouard@2753: ScriptDirectory = paths.AbsDir(__file__) Edouard@2753: Edouard@2757: class HMITreeNode(object): Edouard@2822: def __init__(self, path, name, nodetype, iectype = None, vartype = None, cpath = None, hmiclass = None): Edouard@2757: self.path = path Edouard@2757: self.name = name Edouard@2757: self.nodetype = nodetype Edouard@2814: self.hmiclass = hmiclass Edouard@2764: Edouard@2764: if iectype is not None: Edouard@2764: self.iectype = iectype Edouard@2764: self.vartype = vartype Edouard@2822: self.cpath = cpath Edouard@2822: edouard@2890: if nodetype in ["HMI_NODE"]: Edouard@2757: self.children = [] Edouard@2757: Edouard@2757: def pprint(self, indent = 0): Edouard@2757: res = ">"*indent + pformat(self.__dict__, indent = indent, depth = 1) + "\n" Edouard@2817: if hasattr(self, "children"): Edouard@2757: res += "\n".join([child.pprint(indent = indent + 1) Edouard@2757: for child in self.children]) Edouard@2757: res += "\n" Edouard@2817: Edouard@2757: return res Edouard@2757: Edouard@2757: def place_node(self, node): Edouard@2757: best_child = None Edouard@2757: known_best_match = 0 Edouard@2965: potential_siblings = {} Edouard@2817: for child in self.children: Edouard@2762: if child.path is not None: Edouard@2762: in_common = 0 Edouard@2762: for child_path_item, node_path_item in izip(child.path, node.path): Edouard@2762: if child_path_item == node_path_item: Edouard@2762: in_common +=1 Edouard@2762: else: Edouard@2762: break Edouard@2945: # Match can only be HMI_NODE, and the whole path of node Edouard@2945: # must match candidate node (except for name part) Edouard@2945: # since candidate would become child of that node Edouard@2945: if in_common > known_best_match and \ Edouard@2945: child.nodetype == "HMI_NODE" and \ Edouard@2945: in_common == len(child.path) - 1: Edouard@2762: known_best_match = in_common Edouard@2762: best_child = child Edouard@2965: else: Edouard@2965: potential_siblings[child.path[ Edouard@2965: -2 if child.nodetype == "HMI_NODE" else -1]] = child Edouard@2945: if best_child is not None: Edouard@2965: if node.nodetype == "HMI_NODE" and best_child.path[:-1] == node.path[:-1]: Edouard@2965: return "Duplicate_HMI_NODE", best_child Edouard@2965: return best_child.place_node(node) Edouard@2757: else: Edouard@2965: candidate_name = node.path[-2 if node.nodetype == "HMI_NODE" else -1] Edouard@2965: if candidate_name in potential_siblings: Edouard@2965: return "Non_Unique", potential_siblings[candidate_name] Edouard@2965: Edouard@2965: if node.nodetype == "HMI_NODE" and len(self.children) > 0: Edouard@2965: prev = self.children[-1] Edouard@2965: if prev.path[:-1] == node.path[:-1]: Edouard@2965: return "Late_HMI_NODE",prev Edouard@2965: Edouard@2757: self.children.append(node) Edouard@2965: return None Edouard@2817: Edouard@2788: def etree(self, add_hash=False): Edouard@2763: Edouard@2763: attribs = dict(name=self.name) Edouard@2763: if self.path is not None: Edouard@2788: attribs["path"] = ".".join(self.path) Edouard@2788: Edouard@2815: if self.hmiclass is not None: Edouard@2815: attribs["class"] = self.hmiclass Edouard@2815: Edouard@2788: if add_hash: Edouard@2788: attribs["hash"] = ",".join(map(str,self.hash())) Edouard@2763: Edouard@2763: res = etree.Element(self.nodetype, **attribs) Edouard@2763: Edouard@2817: if hasattr(self, "children"): Edouard@2763: for child_etree in imap(lambda c:c.etree(), self.children): Edouard@2763: res.append(child_etree) Edouard@2763: Edouard@2763: return res Edouard@2763: Edouard@2764: def traverse(self): Edouard@2764: yield self Edouard@2817: if hasattr(self, "children"): Edouard@2764: for c in self.children: Edouard@2764: for yoodl in c.traverse(): Edouard@2764: yield yoodl Edouard@2764: Edouard@2788: Edouard@2788: def hash(self): Edouard@2788: """ Produce a hash, any change in HMI tree structure change that hash """ Edouard@2788: s = hashlib.new('md5') Edouard@2788: self._hash(s) Edouard@2788: # limit size to HMI_HASH_SIZE as in svghmi.c Edouard@2817: return map(ord,s.digest())[:8] Edouard@2788: Edouard@2788: def _hash(self, s): Edouard@2788: s.update(str((self.name,self.nodetype))) Edouard@2817: if hasattr(self, "children"): Edouard@2788: for c in self.children: Edouard@2788: c._hash(s) 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@2763: hmi_tree_root = None Edouard@2757: Edouard@2817: on_hmitree_update = None Edouard@2816: edouard@2890: SPECIAL_NODES = [("HMI_ROOT", "HMI_NODE"), edouard@2890: ("heartbeat", "HMI_INT")] Edouard@2822: # ("current_page", "HMI_STRING")]) Edouard@2822: Edouard@2745: class SVGHMILibrary(POULibrary): 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@2822: global hmi_tree_root, on_hmitree_update Edouard@2749: 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@2890: 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@2890: 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@3051: if hmi_tree_root is None: Edouard@3051: self.FatalError("SVGHMI : Library is selected but not used. Please either deselect it in project config or add a SVGHMI node to project.") Edouard@2757: 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@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@2822: new_node = HMITreeNode(path, name, derived, v["type"], v["vartype"], v["C_path"], **kwargs) Edouard@2965: placement_result = 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@2965: for v in varlist: Edouard@2965: if v["vartype"] == "FB": Edouard@2965: last_FB = v Edouard@2965: 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@2817: if on_hmitree_update is not None: Edouard@2817: 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@2764: for node in 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@2822: "{&(" + 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@2768: str(buf_index) + ", 0, }"] 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@2764: "t" if node.vartype is "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@2789: "hmi_hash_ints": ",".join(map(str,hmi_tree_root.hash())) 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@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@2816: Edouard@2818: class HMITreeSelector(wx.TreeCtrl): Edouard@2818: def __init__(self, parent): Edouard@2818: global on_hmitree_update Edouard@2818: wx.TreeCtrl.__init__(self,parent,style=wx.TR_MULTIPLE)# | wx.TR_HIDE_ROOT) Edouard@2818: Edouard@2818: isz = (16,16) Edouard@2818: self.il = il = wx.ImageList(*isz) Edouard@2818: self.fldridx = il.AddIcon(wx.ArtProvider.GetIcon(wx.ART_FOLDER, wx.ART_OTHER, isz)) Edouard@2818: self.fldropenidx = il.AddIcon(wx.ArtProvider.GetIcon(wx.ART_FOLDER_OPEN, wx.ART_OTHER, isz)) Edouard@2818: self.fileidx = il.AddIcon(wx.ArtProvider.GetIcon(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz)) Edouard@2818: self.SetImageList(il) Edouard@2818: Edouard@2818: on_hmitree_update = self.SVGHMIEditorUpdater() Edouard@2818: self.MakeTree() Edouard@2818: Edouard@2818: def _recurseTree(self, current_hmitree_root, current_tc_root): Edouard@2818: for c in current_hmitree_root.children: Edouard@2818: if hasattr(c, "children"): Edouard@2818: display_name = ('{} (class={})'.format(c.name, c.hmiclass)) \ Edouard@2818: if c.hmiclass is not None else c.name Edouard@2818: tc_child = self.AppendItem(current_tc_root, display_name) Edouard@2818: self.SetPyData(tc_child, None) Edouard@2818: self.SetItemImage(tc_child, self.fldridx, wx.TreeItemIcon_Normal) Edouard@2818: self.SetItemImage(tc_child, self.fldropenidx, wx.TreeItemIcon_Expanded) Edouard@2818: Edouard@2818: self._recurseTree(c,tc_child) Edouard@2818: else: Edouard@2818: display_name = '{} {}'.format(c.nodetype[4:], c.name) Edouard@2818: tc_child = self.AppendItem(current_tc_root, display_name) Edouard@2818: self.SetPyData(tc_child, None) Edouard@2818: self.SetItemImage(tc_child, self.fileidx, wx.TreeItemIcon_Normal) Edouard@2818: self.SetItemImage(tc_child, self.fileidx, wx.TreeItemIcon_Expanded) Edouard@2818: Edouard@2818: def MakeTree(self): Edouard@2818: global hmi_tree_root Edouard@2818: Edouard@2818: self.Freeze() Edouard@2818: Edouard@2818: self.root = None Edouard@2818: self.DeleteAllItems() Edouard@2818: Edouard@2818: root_display_name = _("Please build to see HMI Tree") if hmi_tree_root is None else "HMI" Edouard@2818: self.root = self.AddRoot(root_display_name) Edouard@2818: self.SetPyData(self.root, None) Edouard@2818: self.SetItemImage(self.root, self.fldridx, wx.TreeItemIcon_Normal) Edouard@2818: self.SetItemImage(self.root, self.fldropenidx, wx.TreeItemIcon_Expanded) Edouard@2818: Edouard@2818: if hmi_tree_root is not None: Edouard@2818: self._recurseTree(hmi_tree_root, self.root) Edouard@2818: Edouard@2818: self.Thaw() Edouard@2816: Edouard@2816: def SVGHMIEditorUpdater(self): Edouard@2816: selfref = weakref.ref(self) Edouard@2816: def SVGHMIEditorUpdate(): Edouard@2816: o = selfref() Edouard@2816: if o is not None: Edouard@2816: wx.CallAfter(o.MakeTree) Edouard@2816: return SVGHMIEditorUpdate Edouard@2816: Edouard@2818: class HMITreeView(wx.SplitterWindow): Edouard@2818: Edouard@2818: def __init__(self, parent): Edouard@2818: wx.SplitterWindow.__init__(self, parent, Edouard@2818: style=wx.SUNKEN_BORDER | wx.SP_3D) Edouard@2818: Edouard@2818: self.SelectionTree = HMITreeSelector(self) Edouard@2818: #self.Staging = wx.Panel(self) Edouard@2818: #self.SplitHorizontally(self.SelectionTree, self.Staging, 200) Edouard@2818: self.Initialize(self.SelectionTree) Edouard@2818: Edouard@2818: Edouard@2818: class SVGHMIEditor(ConfTreeNodeEditor): Edouard@2818: CONFNODEEDITOR_TABS = [ Edouard@2818: (_("HMI Tree"), "CreateHMITreeView")] Edouard@2818: Edouard@2816: def CreateHMITreeView(self, parent): Edouard@2818: #self.HMITreeView = HMITreeView(self) Edouard@2818: return HMITreeSelector(parent) Edouard@2818: edouard@3165: def _ProgressArgs(args): edouard@3165: if len(args) == 2: edouard@3165: key, message = args edouard@3165: else: edouard@3165: key, = args edouard@3165: message = key edouard@3165: return str(key), str(message), time.time() Edouard@2816: Edouard@2745: class SVGHMI(object): Edouard@2771: XSD = """ Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2823: Edouard@2823: Edouard@2823: Edouard@2831: Edouard@2831: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: """ 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@3112: "bitmap": "EditSVG", # should be something different Edouard@2745: "name": _("Inkscape"), Edouard@2745: "tooltip": _("Edit HMI"), Edouard@2745: "method": "_StartInkscape" Edouard@2745: }, Edouard@3112: { Edouard@3112: "bitmap": "OpenPOT", # should be something different 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@3112: { Edouard@3112: "bitmap": "EditPO", # should be something different Edouard@3112: "name": _("Edit lang"), Edouard@3112: "tooltip": _("Edit existing message catalog (PO) for specific language"), Edouard@3112: "method": "_EditPO" Edouard@3112: }, Edouard@2788: Edouard@2771: # TODO : HMITree button Edouard@2771: # - can drag'n'drop variabes to Inkscape 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@3112: shutil.copyfile(self._getPOTpath(from_project_path), Edouard@3112: self._getPOTpath()) Edouard@3112: # XXX TODO copy .PO files Edouard@2750: return True Edouard@2745: Edouard@2753: def GetSVGGeometry(self): edouard@3165: t = time.time() Edouard@2756: # invoke inskscape -S, csv-parse output, produce elements Edouard@2756: InkscapeGeomColumns = ["Id", "x", "y", "w", "h"] Edouard@2756: Edouard@2756: inkpath = get_inkscape_path() Edouard@3052: 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@3032: '"' + 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( Edouard@2756: zip(InkscapeGeomColumns, line.strip().split(','))) Edouard@2756: Edouard@2756: res.append(etree.Element("bbox", **attrs)) Edouard@2756: edouard@3165: self.GetCTRoot().logger.write(" Start collecting SVG geometry (Inkscape)\n") edouard@3165: self.GetCTRoot().logger.write(" Finished collecting SVG geometry (Inkscape) in %.3fs\n"%(time.time()-t)) Edouard@2756: return res Edouard@2753: Edouard@2763: def GetHMITree(self): Edouard@2763: global hmi_tree_root edouard@3165: t = time.time() Edouard@2788: res = [hmi_tree_root.etree(add_hash=True)] edouard@3165: self.GetCTRoot().logger.write(" Start getting HMI tree\n") edouard@3165: self.GetCTRoot().logger.write(" Fnished getting HMI tree in %.3fs\n"%(time.time()-t)) Edouard@2763: return res Edouard@2763: edouard@3108: def GetTranslations(self, _context, msgs): edouard@3165: t = time.time() edouard@3113: messages = EtreeToMessages(msgs) edouard@3113: edouard@3140: if len(messages) == 0: 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@3165: self.GetCTRoot().logger.write(" Start getting Translations\n") edouard@3165: self.GetCTRoot().logger.write(" Finished getting Translations in %.3fs\n"%(time.time()-t)) edouard@3165: edouard@3165: return ret edouard@3108: edouard@3156: times = {} edouard@3165: edouard@3165: def ProgressStart(self, _context, *args): edouard@3165: k,m,t = _ProgressArgs(args) edouard@3165: self.times[k] = t edouard@3165: # self.GetCTRoot().logger.write(" Start %s: %.3f\n"%(m, t - self.transform_begin)) edouard@3165: self.GetCTRoot().logger.write(" Start %s\n"%m) edouard@3165: edouard@3165: def ProgressEnd(self, _context, *args): edouard@3165: k,m,t = _ProgressArgs(args) Edouard@3167: self.GetCTRoot().logger.write(" Finished %s in %.3f\n"%(m, t - self.times[k])) edouard@3165: self.times[k] = t edouard@3156: Edouard@2745: def CTNGenerate_C(self, buildpath, locations): Edouard@2745: Edouard@2771: location_str = "_".join(map(str, self.GetCurrentLocation())) Edouard@2771: view_name = self.BaseParams.getName() 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@2771: target_path = os.path.join(self._getBuildPath(), target_fname) Edouard@2812: target_file = open(target_path, 'wb') Edouard@2771: edouard@3156: self.GetCTRoot().logger.write("SVGHMI:\n") edouard@3156: Edouard@2745: if os.path.exists(svgfile): Edouard@2753: Edouard@2753: # TODO : move to __init__ Edouard@2753: transform = XSLTransform(os.path.join(ScriptDirectory, "gen_index_xhtml.xslt"), Edouard@2763: [("GetSVGGeometry", lambda *_ignored:self.GetSVGGeometry()), edouard@3108: ("GetHMITree", lambda *_ignored:self.GetHMITree()), edouard@3156: ("GetTranslations", self.GetTranslations), edouard@3156: ("ProgressStart", self.ProgressStart), edouard@3156: ("ProgressEnd", self.ProgressEnd)]) edouard@3156: edouard@3156: t = time.time() Edouard@2753: Edouard@2753: # load svg as a DOM with Etree Edouard@2753: svgdom = etree.parse(svgfile) Edouard@2753: edouard@3165: self.GetCTRoot().logger.write(" Source SVG parsing: %.3f\n"%(time.time()-t)) edouard@3156: Edouard@2753: # call xslt transform on Inkscape's SVG to generate XHTML Edouard@2834: try: edouard@3165: self.transform_begin = time.time() edouard@3165: result = transform.transform(svgdom) # , profile_run=True) edouard@3165: self.GetCTRoot().logger.write(" XSLT transform: %.3f\n"%(time.time()-self.transform_begin)) Edouard@2814: except XSLTApplyError as e: Edouard@2814: self.FatalError("SVGHMI " + view_name + ": " + e.message) Edouard@2834: finally: Edouard@2834: for entry in transform.get_error_log(): Edouard@2834: message = "SVGHMI: "+ entry.message + "\n" Edouard@2834: self.GetCTRoot().logger.write_warning(message) Edouard@2817: Edouard@2771: result.write(target_file, encoding="utf-8") Edouard@2764: # print(str(result)) Edouard@2764: # print(transform.xslt.error_log) edouard@3165: # print(etree.tostring(result.xslt_profile,pretty_print=True)) Edouard@2753: Edouard@2753: # TODO Edouard@2745: # - Errors on HMI semantics Edouard@2745: # - ... maybe something to have a global view of what is declared in SVG. Edouard@2753: Edouard@2745: else: Edouard@2817: # TODO : use default svg that expose the HMI tree as-is Edouard@2771: target_file.write(""" Edouard@2771: Edouard@2771: Edouard@2771:

No SVG file provided

Edouard@2771: Edouard@2771: Edouard@2771: """) Edouard@2771: Edouard@2771: target_file.close() Edouard@2771: 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@2828: repr(shlex.split(given_command.format(port="8008", name=view_name))) + Edouard@2834: ")") if given_command else "pass # 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@2831: # TODO : multiple watchdog (one for each svghmi instance) Edouard@2831: def svghmi_watchdog_trigger(): Edouard@2828: {svghmi_cmds[Watchdog]} edouard@2824: Edouard@2831: svghmi_watchdog = None Edouard@2831: Edouard@2993: def _runtime_{location}_svghmi_start(): Edouard@2831: global svghmi_watchdog Edouard@2831: svghmi_root.putChild( Edouard@2831: '{view_name}', Edouard@2831: NoCacheFile('{xhtml}', Edouard@2831: defaultType='application/xhtml+xml')) Edouard@2831: Edouard@2828: {svghmi_cmds[Start]} edouard@2824: Edouard@2831: svghmi_watchdog = Watchdog( Edouard@2831: {watchdog_initial}, Edouard@2831: {watchdog_interval}, Edouard@2831: svghmi_watchdog_trigger) Edouard@2831: Edouard@2993: def _runtime_{location}_svghmi_stop(): Edouard@2831: global svghmi_watchdog Edouard@2831: if svghmi_watchdog is not None: Edouard@2831: svghmi_watchdog.cancel() Edouard@2831: svghmi_watchdog = None Edouard@2831: edouard@2824: svghmi_root.delEntity('{view_name}') Edouard@2828: {svghmi_cmds[Stop]} edouard@2824: edouard@2824: """.format(location=location_str, edouard@2824: xhtml=target_fname, edouard@2824: view_name=view_name, 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@2831: )) 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@2745: dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a SVG file"), os.getcwd(), "", _("SVG files (*.svg)|*.svg|All files|*.*"), wx.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@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@2745: svgfile = None 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@3112: dialog = wx.FileDialog(self.GetCTRoot().AppFrame, _("Choose a PO file"), project_path, "", _("PO files (*.po)|*.po"), wx.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@2822: def CTNGlobalInstances(self): Edouard@2822: # view_name = self.BaseParams.getName() Edouard@2822: # return [ (view_name + "_" + name, iec_type, "") for name, iec_type in SPECIAL_NODES] Edouard@2822: # TODO : move to library level for multiple hmi Edouard@2822: return [(name, iec_type, "") for name, iec_type in SPECIAL_NODES] Edouard@2822: Edouard@3159: def GetIconName(self): Edouard@3159: return "SVGHMI"