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@2757: from pprint import pprint, pformat Edouard@2745: Edouard@2745: import wx 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@2753: from lxml import etree Edouard@2745: Edouard@2756: from util.ProcessLogger import ProcessLogger Edouard@2764: from runtime.typemapping import DebugTypesSize Edouard@2767: import targets Edouard@2756: Edouard@2749: HMI_TYPES_DESC = { Edouard@2749: "HMI_CLASS":{}, Edouard@2749: "HMI_LABEL":{}, Edouard@2749: "HMI_STRING":{}, Edouard@2749: "HMI_INT":{}, Edouard@2749: "HMI_REAL":{} Edouard@2749: } Edouard@2749: Edouard@2749: HMI_TYPES = HMI_TYPES_DESC.keys() Edouard@2745: Edouard@2753: from XSLTransform import XSLTransform Edouard@2753: Edouard@2753: ScriptDirectory = paths.AbsDir(__file__) Edouard@2753: Edouard@2757: class HMITreeNode(object): Edouard@2764: def __init__(self, path, name, nodetype, iectype = None, vartype = None): Edouard@2757: self.path = path Edouard@2757: self.name = name Edouard@2757: self.nodetype = nodetype Edouard@2764: Edouard@2764: if iectype is not None: Edouard@2764: self.iectype = iectype Edouard@2764: self.vartype = vartype Edouard@2757: if nodetype in ["HMI_LABEL", "HMI_ROOT"]: 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@2757: 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@2757: 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@2762: 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@2762: if in_common > known_best_match: Edouard@2762: known_best_match = in_common Edouard@2762: best_child = child Edouard@2757: if best_child is not None and best_child.nodetype == "HMI_LABEL": Edouard@2757: best_child.place_node(node) Edouard@2757: else: Edouard@2757: self.children.append(node) Edouard@2757: Edouard@2763: def etree(self): Edouard@2763: Edouard@2763: attribs = dict(name=self.name) Edouard@2763: if self.path is not None: Edouard@2763: attribs["path"]=".".join(self.path) Edouard@2763: Edouard@2763: res = etree.Element(self.nodetype, **attribs) Edouard@2763: Edouard@2763: 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@2764: 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@2763: # module scope for HMITree root Edouard@2763: # so that CTN can use HMITree deduced in Library Edouard@2763: # 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@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@2763: global hmi_tree_root 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@2757: | +->va HMI_LABEL Edouard@2757: | +->v3 HMI_INT Edouard@2757: | +->v4 HMI_INT Edouard@2757: | Edouard@2757: +->fb1 (type mhoo) Edouard@2757: | +->va HMI_LABEL 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@2757: +->fb0_va Edouard@2757: | +-> v3 Edouard@2757: | +-> v4 Edouard@2757: | Edouard@2757: +->fb1_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@2758: hmi_tree_root = HMITreeNode(None, "/", "HMI_ROOT") Edouard@2757: Edouard@2762: # add special nodes Edouard@2762: map(lambda (n,t): hmi_tree_root.children.append(HMITreeNode(None,n,t)), [ Edouard@2762: ("plc_status", "HMI_PLC_STATUS"), Edouard@2762: ("current_page", "HMI_CURRENT_PAGE")]) Edouard@2757: Edouard@2757: # deduce HMI tree from PLC HMI_* instances Edouard@2757: for v in hmi_types_instances: Edouard@2764: path = v["C_path"].split(".") Edouard@2758: # ignores variables starting with _TMP_ Edouard@2758: if path[-1].startswith("_TMP_"): Edouard@2758: continue Edouard@2764: new_node = HMITreeNode(path, path[-1], v["derived"], v["type"], v["vartype"]) Edouard@2757: hmi_tree_root.place_node(new_node) Edouard@2757: Edouard@2757: print(hmi_tree_root.pprint()) Edouard@2749: Edouard@2764: variable_decl_array = [] Edouard@2764: extern_variables_declarations = [] Edouard@2765: buf_index = 0 Edouard@2764: for node in hmi_tree_root.traverse(): Edouard@2764: if hasattr(node, "iectype"): Edouard@2764: sz = DebugTypesSize.get(node.iectype, 0) Edouard@2764: variable_decl_array += [ Edouard@2764: "{&(" + ".".join(node.path) + "), " + 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@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@2764: + ".".join(node.path) + ";"] Edouard@2764: Edouard@2764: # TODO : filter only requiered external declarations Edouard@2764: for v in varlist : Edouard@2764: if v["C_path"].find('.') < 0 and v["vartype"] == "FB" : 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@2749: # TODO generate 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@2764: 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@2768: "var_access_code": targets.GetCode("var_access.c"), Edouard@2768: "PLC_ticktime": self.GetCTRoot().GetTicktime() 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@2749: return (["svghmi"], [(gen_svghmi_c_path, IECCFLAGS)], True), "" Edouard@2745: Edouard@2745: class SVGHMI(object): Edouard@2745: XSD = """ Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: Edouard@2745: """ 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@2745: "bitmap": "ImportSVG", # should be something different Edouard@2745: "name": _("Inkscape"), Edouard@2745: "tooltip": _("Edit HMI"), Edouard@2745: "method": "_StartInkscape" Edouard@2745: }, 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@2745: # define name for SVG file containing gui layout Edouard@2745: return os.path.join(project_path, "gui.svg") Edouard@2745: 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@2750: return True Edouard@2745: Edouard@2753: def GetSVGGeometry(self): Edouard@2756: # invoke inskscape -S, csv-parse output, produce elements Edouard@2756: InkscapeGeomColumns = ["Id", "x", "y", "w", "h"] Edouard@2756: Edouard@2756: # TODO : move following line to __init__ Edouard@2756: inkpath = get_inkscape_path() Edouard@2756: svgpath = self._getSVGpath() Edouard@2756: _status, result, _err_result = ProcessLogger(None, Edouard@2756: inkpath + " -S " + svgpath, Edouard@2756: no_stdout=True, Edouard@2756: no_stderr=True).spin() 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@2756: return res Edouard@2753: Edouard@2763: def GetHMITree(self): Edouard@2763: global hmi_tree_root Edouard@2763: res = [hmi_tree_root.etree()] Edouard@2763: return res Edouard@2763: Edouard@2745: def CTNGenerate_C(self, buildpath, locations): Edouard@2745: """ Edouard@2745: Return C code generated by iec2c compiler Edouard@2745: when _generate_softPLC have been called Edouard@2745: @param locations: ignored Edouard@2745: @return: [(C_file_name, CFLAGS),...] , LDFLAGS_TO_APPEND Edouard@2745: """ Edouard@2745: Edouard@2745: svgfile = self._getSVGpath() 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@2763: ("GetHMITree", lambda *_ignored:self.GetHMITree())]) Edouard@2753: Edouard@2753: Edouard@2753: # load svg as a DOM with Etree Edouard@2753: svgdom = etree.parse(svgfile) Edouard@2753: Edouard@2753: # call xslt transform on Inkscape's SVG to generate XHTML Edouard@2753: result = transform.transform(svgdom) Edouard@2753: Edouard@2764: # print(str(result)) Edouard@2764: # print(transform.xslt.error_log) 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@2745: # TODO : use default svg that expose the HMI tree as-is Edouard@2745: pass Edouard@2745: Edouard@2745: Edouard@2745: res = ([], "", False) Edouard@2745: Edouard@2745: targetpath = os.path.join(self._getBuildPath(), "target.xhtml") Edouard@2745: targetfile = open(targetpath, 'w') Edouard@2745: Edouard@2745: # TODO : DOM to string Edouard@2745: targetfile.write("TODO") Edouard@2745: targetfile.close() Edouard@2745: res += (("target.js", open(targetpath, "rb")),) Edouard@2745: Edouard@2745: # TODO add C code to expose HMI tree variables to shared memory Edouard@2745: # TODO generate a description of shared memory (xml or CSV) Edouard@2745: # that can be loaded by svghmi QTWeb* app or svghmi server Edouard@2745: 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)