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:
Edouard@2745: from __future__ import absolute_import
Edouard@2745: import os
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@2763: hmi_tree_root = None
Edouard@2757:
Edouard@2817: on_hmitree_update = None
Edouard@2816:
Edouard@3270: maxConnectionsTotal = 0
Edouard@3270:
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@3270: global hmi_tree_root, on_hmitree_update, maxConnectionsTotal
Edouard@2749:
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@3287: 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@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@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@3201: on_hmitree_update(hmi_tree_root)
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@3270: "hmi_hash_ints": ",".join(map(str,hmi_tree_root.hash())),
Edouard@3270: "max_connections": 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@3176: hmitree_backup_file.write(etree.tostring(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@2816:
edouard@3201: def Register_SVGHMI_UI_for_HMI_tree_updates(ref):
edouard@3201: global on_hmitree_update
edouard@3201: def HMITreeUpdate(_hmi_tree_root):
edouard@3201: obj = ref()
edouard@3201: if obj is not None:
edouard@3201: obj.HMITreeUpdate(_hmi_tree_root)
edouard@3201:
edouard@3201: on_hmitree_update = HMITreeUpdate
Edouard@2818:
Edouard@2818:
Edouard@2818: class SVGHMIEditor(ConfTreeNodeEditor):
Edouard@2818: CONFNODEEDITOR_TABS = [
edouard@3201: (_("HMI Tree"), "CreateSVGHMI_UI")]
edouard@3201:
edouard@3201: def CreateSVGHMI_UI(self, parent):
edouard@3176: global hmi_tree_root
edouard@3176:
edouard@3176: if hmi_tree_root is None:
edouard@3176: buildpath = self.Controler.GetCTRoot()._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@3176: hmi_tree_root = HMITreeNode.from_etree(etree.parse(hmitree_backup_file).getroot())
edouard@3176:
Edouard@3208: ret = SVGHMI_UI(parent, Register_SVGHMI_UI_for_HMI_tree_updates)
Edouard@3208:
Edouard@3208: on_hmitree_update(hmi_tree_root)
Edouard@3208:
Edouard@3208: return ret
Edouard@2818:
Edouard@2745: class SVGHMI(object):
Edouard@2771: XSD = """
Edouard@2745:
Edouard@2745:
Edouard@2745:
edouard@3269:
edouard@3269:
Edouard@3289:
edouard@3269:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
edouard@3269:
edouard@3269:
Edouard@3270:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
Edouard@3277:
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@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@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@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@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@3170: self.ProgressEnd("inkscape")
Edouard@2756: return res
Edouard@2753:
Edouard@2763: def GetHMITree(self):
Edouard@2763: global hmi_tree_root
Edouard@3170: self.ProgressStart("hmitree", "getting HMI tree")
Edouard@2788: res = [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@3290: global hmi_tree_root
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@3180: hash_path = os.path.join(build_path, "svghmi.md5")
Edouard@2771:
edouard@3156: self.GetCTRoot().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@3180: result = transform.transform(svgdom) # , 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@3180: with open(hash_path, 'wb') 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@2771: target_file.write("""
Edouard@3274:
Edouard@3274:
Edouard@3274: SVGHMI
Edouard@3274:
Edouard@2771:
Edouard@2771: No SVG file provided
Edouard@2771:
Edouard@2771:
Edouard@2771: """)
edouard@3180: 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@3286: repr(shlex.split(given_command.format(**svghmi_options))) +
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@3269: def svghmi_{location}_watchdog_trigger():
Edouard@2828: {svghmi_cmds[Watchdog]}
edouard@2824:
Edouard@3270: max_svghmi_sessions = {maxConnections_total}
Edouard@2831:
Edouard@2993: def _runtime_{location}_svghmi_start():
edouard@3269: global svghmi_watchdog, svghmi_servers
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@3270: svghmi_root.putChild("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@3269: '{path}',
Edouard@2831: NoCacheFile('{xhtml}',
edouard@3269: defaultType='application/xhtml+xml'))
edouard@3269:
edouard@3269: path_list.append("{path}")
Edouard@2831:
Edouard@2828: {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@3269: global svghmi_watchdog, svghmi_servers
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@3269: svghmi_root.delEntity('{path}')
edouard@3269:
edouard@3269: path_list.remove('{path}')
edouard@3269:
edouard@3269: if len(path_list)==0:
edouard@3269: svghmi_root.delEntity("ws")
edouard@3269: svghmi_listener.stopListening()
edouard@3269: svghmi_servers.pop("{interface}:{port}")
edouard@3269:
Edouard@2828: {svghmi_cmds[Stop]}
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@3286: maxConnections_total = 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@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@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@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@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@3211: _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.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@3211: _("Font files (*.ttf;*.otf;*.woff;*.woff2)|*.ttf;*.otf;*.woff;*.woff2"), wx.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@3267: ## In case one day we support more than one heartbeat
edouard@3267: # def CTNGlobalInstances(self):
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"