author | Edouard Tisserant <edouard@beremiz.fr> |
Thu, 16 Jan 2025 14:52:49 +0100 (2 months ago) | |
changeset 4096 | d459b9179ec3 |
parent 4000 | 87f11a874477 |
child 4098 | 8c12c0df026a |
permissions | -rw-r--r-- |
#!/usr/bin/env python # -*- coding: utf-8 -*- # This file is part of Beremiz, a Integrated Development Environment for # programming IEC 61131-3 automates supporting plcopen standard and CanFestival. # # Copyright (C) 2007: Edouard TISSERANT and Laurent BESSARD # # See COPYING file for copyrights details. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Config Tree Node base class. - A Beremiz project is organized in a tree each node derivate from ConfigTreeNode - Project tree organization match filesystem organization of project directory. - Each node of the tree have its own xml configuration, whose grammar is defined for each node type, as XSD - ... TODO : document """ import os import traceback import types import shutil from operator import add from functools import reduce from lxml import etree from xmlclass import GenerateParserFromXSDstring from PLCControler import LOCATION_CONFNODE from editors.ConfTreeNodeEditor import ConfTreeNodeEditor from POULibrary import UserAddressedException _BaseParamsParser = GenerateParserFromXSDstring("""<?xml version="1.0" encoding="ISO-8859-1" ?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <xsd:element name="BaseParams"> <xsd:complexType> <xsd:attribute name="Name" type="xsd:string" use="optional" default="__unnamed__"/> <xsd:attribute name="IEC_Channel" type="xsd:integer" use="required"/> <xsd:attribute name="Enabled" type="xsd:boolean" use="optional" default="true"/> </xsd:complexType> </xsd:element> </xsd:schema>""") NameTypeSeparator = '@' XSDSchemaErrorMessage = _("{a1} XML file doesn't follow XSD schema at line {a2}:\n{a3}") class ConfigTreeNode(object): """ This class is the one that define confnodes. """ XSD = None CTNChildrenTypes = [] CTNMaxCount = None ConfNodeMethods = [] LibraryControler = None EditorType = ConfTreeNodeEditor IconPath = None def _AddParamsMembers(self): self.CTNParams = None if self.XSD: self.Parser = GenerateParserFromXSDstring(self.XSD) obj = self.Parser.CreateRoot() name = obj.getLocalTag() self.CTNParams = (name, obj) setattr(self, name, obj) def __init__(self): # Create BaseParam self.BaseParams = _BaseParamsParser.CreateRoot() self.MandatoryParams = ("BaseParams", self.BaseParams) self._AddParamsMembers() self.Children = {} self._View = None # copy ConfNodeMethods so that it can be later customized self.ConfNodeMethods = [dic.copy() for dic in self.ConfNodeMethods] def ConfNodeBaseXmlFilePath(self, CTNName=None): return os.path.join(self.CTNPath(CTNName), "baseconfnode.xml") def ConfNodeXmlFilePath(self, CTNName=None): return os.path.join(self.CTNPath(CTNName), "confnode.xml") def CTNPath(self, CTNName=None, project_path=None): if not CTNName: CTNName = self.CTNName() if not project_path: project_path = self.CTNParent.CTNPath() return os.path.join(project_path, CTNName + NameTypeSeparator + self.CTNType) def CTNName(self): return self.BaseParams.getName() def CTNEnabled(self): return self.BaseParams.getEnabled() def CTNFullName(self): parent = self.CTNParent.CTNFullName() if parent != "": return parent + "." + self.CTNName() return self.BaseParams.getName() def CTNSearch(self, criteria): # TODO match config's fields name and fields contents return reduce(add, [ CTNChild.CTNSearch(criteria) for CTNChild in self.IterChildren()], []) def GetIconName(self): return None def CTNTestModified(self): return self.ChangesToSave def CTNMarkModified(self): oldChangesToSave = self.ChangesToSave self.ChangesToSave = True if not oldChangesToSave: appframe = self.GetCTRoot().AppFrame if appframe is not None: appframe.RefreshTitle() appframe.RefreshPageTitles() appframe.RefreshFileMenu() def ProjectTestModified(self): """ recursively check modified status """ if self.CTNTestModified(): return True for CTNChild in self.IterChildren(): if CTNChild.ProjectTestModified(): return True return False def RemoteExec(self, script, **kwargs): return self.CTNParent.RemoteExec(script, **kwargs) def OnCTNSave(self, from_project_path=None): """Default, do nothing and return success""" return True def GetParamsAttributes(self, path=None): if path: parts = path.split(".", 1) if self.MandatoryParams and parts[0] == self.MandatoryParams[0]: return self.MandatoryParams[1].getElementInfos(parts[0], parts[1]) elif self.CTNParams and parts[0] == self.CTNParams[0]: return self.CTNParams[1].getElementInfos(parts[0], parts[1]) else: params = [] if self.CTNParams: params.append(self.CTNParams[1].getElementInfos(self.CTNParams[0])) return params def SetParamsAttribute(self, path, value): self.ChangesToSave = True # Filter IEC_Channel and Name, that have specific behavior if path == "BaseParams.IEC_Channel": old_leading = ".".join(map(str, self.GetCurrentLocation())) new_value = self.FindNewIEC_Channel(value) if new_value != value: new_leading = ".".join(map(str, self.CTNParent.GetCurrentLocation() + (new_value,))) self.GetCTRoot().UpdateProjectVariableLocation(old_leading, new_leading) return new_value, True elif path == "BaseParams.Name": res = self.FindNewName(value) self.CTNRequestSave() return res, True parts = path.split(".", 1) if self.MandatoryParams and parts[0] == self.MandatoryParams[0]: self.MandatoryParams[1].setElementValue(parts[1], value) value = self.MandatoryParams[1].getElementInfos(parts[0], parts[1])["value"] elif self.CTNParams and parts[0] == self.CTNParams[0]: self.CTNParams[1].setElementValue(parts[1], value) value = self.CTNParams[1].getElementInfos(parts[0], parts[1])["value"] return value, False def CTNMakeDir(self): os.mkdir(self.CTNPath()) def CTNRequestSave(self, from_project_path=None): if self.GetCTRoot().CheckProjectPathPerm(): # If confnode do not have corresponding directory ctnpath = self.CTNPath() if not os.path.isdir(ctnpath): # Create it os.mkdir(ctnpath) # generate XML for base XML parameters controller of the confnode if self.MandatoryParams: BaseXMLFile = open(self.ConfNodeBaseXmlFilePath(), 'w', encoding='utf-8') BaseXMLFile.write(etree.tostring( self.MandatoryParams[1], pretty_print=True, xml_declaration=True, encoding='utf-8').decode()) BaseXMLFile.close() # generate XML for XML parameters controller of the confnode if self.CTNParams: XMLFile = open(self.ConfNodeXmlFilePath(), 'w', encoding='utf-8') XMLFile.write(etree.tostring( self.CTNParams[1], pretty_print=True, xml_declaration=True, encoding='utf-8').decode()) XMLFile.close() # Call the confnode specific OnCTNSave method result = self.OnCTNSave(from_project_path) if not result: return _("Error while saving \"%s\"\n") % self.CTNPath() # mark confnode as saved self.ChangesToSave = False # go through all children and do the same for CTNChild in self.IterChildren(): CTNChildPath = None if from_project_path is not None: CTNChildPath = CTNChild.CTNPath(project_path=from_project_path) result = CTNChild.CTNRequestSave(CTNChildPath) if result: return result return None def CTNImport(self, src_CTNPath): shutil.copytree(src_CTNPath, self.CTNPath) return True def CTNGlobalInstances(self): """ @return: [(instance_name, instance_type),...] """ return [] def _GlobalInstances(self): instances = self.CTNGlobalInstances() for CTNChild in self.IECSortedChildren(): instances.extend(CTNChild._GlobalInstances()) return instances def CTNGenerate_C(self, buildpath, locations): """ Generate C code @param locations: List of complete variables locations \ [{"IEC_TYPE" : the IEC type (i.e. "INT", "STRING", ...) "NAME" : name of the variable (generally "__IW0_1_2" style) "DIR" : direction "Q","I" or "M" "SIZE" : size "X", "B", "W", "D", "L" "LOC" : tuple of interger for IEC location (0,1,2,...) }, ...] @return: [(C_file_name, CFLAGS),...] , LDFLAGS_TO_APPEND """ self.GetCTRoot().logger.write_warning(".".join(map(str, self.GetCurrentLocation())) + " -> Nothing to do\n") return [], "", False def _Generate_C(self, buildpath, locations): # Generate confnodes [(Cfiles, CFLAGS)], LDFLAGS, DoCalls, extra_files # extra_files = [(fname,fobject), ...] gen_result = self.CTNGenerate_C(buildpath, locations) CTNCFilesAndCFLAGS, CTNLDFLAGS, DoCalls = gen_result[:3] extra_files = gen_result[3:] # if some files have been generated put them in the list with their location if CTNCFilesAndCFLAGS: LocationCFilesAndCFLAGS = [(self.GetCurrentLocation(), CTNCFilesAndCFLAGS, DoCalls)] else: LocationCFilesAndCFLAGS = [] # confnode asks for some LDFLAGS LDFLAGS = [] if CTNLDFLAGS is not None: # LDFLAGS can be either string if isinstance(CTNLDFLAGS, str): LDFLAGS += [CTNLDFLAGS] # or list of strings elif isinstance(CTNLDFLAGS, list): LDFLAGS += CTNLDFLAGS # recurse through all children, and stack their results for CTNChild in self.IECSortedChildren(): new_location = CTNChild.GetCurrentLocation() # How deep are we in the tree ? depth = len(new_location) _LocationCFilesAndCFLAGS, _LDFLAGS, _extra_files = \ CTNChild._Generate_C( # keep the same path buildpath, # filter locations that start with current IEC location [loc for loc in locations if loc["LOC"][0:depth] == new_location]) # stack the result LocationCFilesAndCFLAGS += _LocationCFilesAndCFLAGS LDFLAGS += _LDFLAGS extra_files += _extra_files return LocationCFilesAndCFLAGS, LDFLAGS, extra_files def IterChildren(self): for _CTNType, Children in list(self.Children.items()): for CTNInstance in Children: yield CTNInstance def IECSortedChildren(self): # reorder children by IEC_channels ordered = [(chld.BaseParams.getIEC_Channel(), chld) for chld in self.IterChildren()] if ordered: ordered.sort() return list(zip(*ordered))[1] else: return [] def _GetChildBySomething(self, something, toks): for CTNInstance in self.IterChildren(): # if match component of the name if getattr(CTNInstance.BaseParams, something) == toks[0]: # if Name have other components if len(toks) >= 2: # Recurse in order to find the latest object return CTNInstance._GetChildBySomething(something, toks[1:]) # No sub name -> found return CTNInstance # Not found return None def GetChildByName(self, Name): if Name: toks = Name.split('.') return self._GetChildBySomething("Name", toks) else: return self def GetChildByIECLocation(self, Location): if Location: return self._GetChildBySomething("IEC_Channel", Location) else: return self def GetCurrentLocation(self): """ @return: Tupple containing confnode IEC location of current confnode : %I0.0.4.5 => (0,0,4,5) """ return self.CTNParent.GetCurrentLocation() + (self.BaseParams.getIEC_Channel(),) def GetCurrentName(self): """ @return: String "ParentParentName.ParentName.Name" """ return self.CTNParent._GetCurrentName() + self.BaseParams.getName() def _GetCurrentName(self): """ @return: String "ParentParentName.ParentName.Name." """ return self.CTNParent._GetCurrentName() + self.BaseParams.getName() + "." def GetCTRoot(self): return self.CTNParent.GetCTRoot() def GetFullIEC_Channel(self): return ".".join([str(i) for i in self.GetCurrentLocation()]) + ".x" def GetLocations(self): location = self.GetCurrentLocation() return [loc for loc in self.CTNParent.GetLocations() if loc["LOC"][0:len(location)] == location] def GetVariableLocationTree(self): ''' This function is meant to be overridden by confnodes. It should returns an list of dictionaries - IEC_type is an IEC type like BOOL/BYTE/SINT/... - location is a string of this variable's location, like "%IX0.0.0" ''' children = [] for child in self.IECSortedChildren(): children.append(child.GetVariableLocationTree()) return {"name": self.BaseParams.getName(), "type": LOCATION_CONFNODE, "location": self.GetFullIEC_Channel(), "children": children} def FindNewName(self, DesiredName): """ Changes Name to DesiredName if available, Name-N if not. @param DesiredName: The desired Name (string) """ # Build a list of used Name out of parent's Children AllNames = [] for CTNInstance in self.CTNParent.IterChildren(): if CTNInstance != self: AllNames.append(CTNInstance.BaseParams.getName()) # Find a free name, eventually appending digit res = DesiredName if DesiredName.endswith("_0"): BaseDesiredName = DesiredName[:-2] else: BaseDesiredName = DesiredName suffix = 1 while res in AllNames: res = "%s_%d" % (BaseDesiredName, suffix) suffix += 1 # Check previous confnode existance dontexist = self.BaseParams.getName() == "__unnamed__" if not dontexist: # Get old path oldpath = self.CTNPath() # Set the new name self.BaseParams.setName(res) # Rename confnode dir if exist if not dontexist: shutil.move(oldpath, self.CTNPath()) # warn user he has two left hands if DesiredName != res: msg = _("A child named \"{a1}\" already exists -> \"{a2}\"\n").format(a1=DesiredName, a2=res) self.GetCTRoot().logger.write_warning(msg) return res def GetAllChannels(self): AllChannels = [] for CTNInstance in self.CTNParent.IterChildren(): if CTNInstance != self: AllChannels.append(CTNInstance.BaseParams.getIEC_Channel()) AllChannels.sort() return AllChannels def FindNewIEC_Channel(self, DesiredChannel): """ Changes IEC Channel number to DesiredChannel if available, nearest available if not. @param DesiredChannel: The desired IEC channel (int) """ # Get Current IEC channel CurrentChannel = self.BaseParams.getIEC_Channel() # Do nothing if no change # if CurrentChannel == DesiredChannel: return CurrentChannel # Build a list of used Channels out of parent's Children AllChannels = self.GetAllChannels() # Now, try to guess the nearest available channel res = DesiredChannel while res in AllChannels: # While channel not free if res < CurrentChannel: # Want to go down ? res -= 1 # Test for n-1 if res < 0: self.GetCTRoot().logger.write_warning(_("Cannot find lower free IEC channel than %d\n") % CurrentChannel) return CurrentChannel # Can't go bellow 0, do nothing else: # Want to go up ? res += 1 # Test for n-1 # Finally set IEC Channel self.BaseParams.setIEC_Channel(res) return res def GetContextualMenuItems(self): return None def GetView(self, onlyopened=False): if not self._View and not onlyopened and self.EditorType is not None: app_frame = self.GetCTRoot().AppFrame self._View = self.EditorType(app_frame.TabsOpened, self, app_frame) return self._View def _OpenView(self, name=None, onlyopened=False): view = self.GetView(onlyopened) if view is not None: if name is None: name = self.CTNFullName() app_frame = self.GetCTRoot().AppFrame app_frame.EditProjectElement(view, name, onlyopened) return view def _CloseView(self, view): app_frame = self.GetCTRoot().AppFrame if app_frame is not None: app_frame.DeletePage(view) def OnCloseEditor(self, view): if self._View == view: self._View = None def OnCTNClose(self): if self._View is not None: self._CloseView(self._View) self._View = None return True def _doRemoveChild(self, CTNInstance): # Remove all children of child for SubCTNInstance in CTNInstance.IterChildren(): CTNInstance._doRemoveChild(SubCTNInstance) # Call the OnCloseMethod CTNInstance.OnCTNClose() # Delete confnode dir try: shutil.rmtree(CTNInstance.CTNPath()) except Exception: pass # Remove child of Children self.Children[CTNInstance.CTNType].remove(CTNInstance) if len(self.Children[CTNInstance.CTNType]) == 0: self.Children.pop(CTNInstance.CTNType) # Forget it... (View have to refresh) def CTNRemove(self): # Fetch the confnode # CTNInstance = self.GetChildByName(CTNName) # Ask to his parent to remove it self.CTNParent._doRemoveChild(self) def CTNAddChild(self, CTNName, CTNType, IEC_Channel=0): """ Create the confnodes that may be added as child to this node self @param CTNType: string desining the confnode class name (get name from CTNChildrenTypes) @param CTNName: string for the name of the confnode instance """ # reorganize self.CTNChildrenTypes tuples from (name, CTNClass, Help) # to ( name, (CTNClass, Help)), an make a dict transpose = list(zip(*self.CTNChildrenTypes)) CTNChildrenTypes = dict(list(zip(transpose[0], list(zip(transpose[1], transpose[2]))))) # Check that adding this confnode is allowed try: CTNClass, CTNHelp = CTNChildrenTypes[CTNType] except KeyError: raise Exception(_("Cannot create child {a1} of type {a2} "). format(a1=CTNName, a2=CTNType)) # if CTNClass is a class factory, call it. (prevent unneeded imports) if isinstance(CTNClass, types.FunctionType): CTNClass = CTNClass() # Eventualy Initialize child instance list for this class of confnode ChildrenWithSameClass = self.Children.setdefault(CTNType, list()) # Check count if getattr(CTNClass, "CTNMaxCount", None) and len(ChildrenWithSameClass) >= CTNClass.CTNMaxCount: msg = _("Max count ({a1}) reached for this confnode of type {a2} ").format( a1=CTNClass.CTNMaxCount, a2=CTNType) self.GetCTRoot().logger.write_warning(msg) return None # create the final class, derived of provided confnode and template class FinalCTNClass(CTNClass, ConfigTreeNode): """ ConfNode class is derivated into FinalCTNClass before being instanciated This way __init__ is overloaded to ensure ConfigTreeNode.__init__ is called before CTNClass.__init__, and to do the file related stuff. """ def __init__(self, parent): self.CTNParent = parent # Keep track of the confnode type name self.CTNType = CTNType # remind the help string, for more fancy display self.CTNHelp = CTNHelp # Call the base confnode template init - change XSD into class members ConfigTreeNode.__init__(self) # check name is unique NewCTNName = self.FindNewName(CTNName) # If dir have already be made, and file exist if os.path.isdir(self.CTNPath(NewCTNName)): # and os.path.isfile(self.ConfNodeXmlFilePath(CTNName)): # Load the confnode.xml file into parameters members self.LoadXMLParams(NewCTNName) # Basic check. Better to fail immediately. if self.BaseParams.getName() != NewCTNName: raise Exception( _("Project tree layout do not match confnode.xml {a1}!={a2} "). format(a1=NewCTNName, a2=self.BaseParams.getName())) # Now, self.CTNPath() should be OK # Check that IEC_Channel is not already in use. self.FindNewIEC_Channel(self.BaseParams.getIEC_Channel()) # Call the confnode real __init__ if getattr(CTNClass, "__init__", None): CTNClass.__init__(self) # Load and init all the children self.LoadChildren() # just loaded, nothing to saved self.ChangesToSave = False else: # If confnode do not have corresponding file/dirs - they will be created on Save self.CTNMakeDir() # Find an IEC number self.FindNewIEC_Channel(IEC_Channel) # Call the confnode real __init__ if getattr(CTNClass, "__init__", None): CTNClass.__init__(self) self.CTNRequestSave() # just created, must be saved self.ChangesToSave = True def _getBuildPath(self): return self.CTNParent._getBuildPath() # Create the object out of the resulting class newConfNodeOpj = FinalCTNClass(self) # Store it in CTNgedChils ChildrenWithSameClass.append(newConfNodeOpj) return newConfNodeOpj def ClearChildren(self): for child in self.IterChildren(): child.ClearChildren() self.Children = {} def LoadXMLParams(self, CTNName=None): methode_name = os.path.join(self.CTNPath(CTNName), "methods.py") if os.path.isfile(methode_name): exec(compile(open(methode_name, "rb").read(), methode_name, 'exec')) ConfNodeName = CTNName if CTNName is not None else self.CTNName() # Get the base xml tree if self.MandatoryParams: try: basexmlfile = open(self.ConfNodeBaseXmlFilePath(CTNName), 'r') self.BaseParams, error = _BaseParamsParser.LoadXMLString(basexmlfile.read()) if error is not None: (fname, lnum, src) = ((ConfNodeName + " BaseParams",) + error) self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src)) self.MandatoryParams = ("BaseParams", self.BaseParams) basexmlfile.close() except Exception as exc: msg = _("Couldn't load confnode base parameters {a1} :\n {a2}").format(a1=ConfNodeName, a2=str(exc)) self.GetCTRoot().logger.write_error(msg) self.GetCTRoot().logger.write_error(traceback.format_exc()) # Get the xml tree if self.CTNParams: try: xmlfile = open(self.ConfNodeXmlFilePath(CTNName), 'r') obj, error = self.Parser.LoadXMLString(xmlfile.read()) if error is not None: (fname, lnum, src) = ((ConfNodeName,) + error) self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src)) name = obj.getLocalTag() setattr(self, name, obj) self.CTNParams = (name, obj) xmlfile.close() except Exception as exc: msg = _("Couldn't load confnode parameters {a1} :\n {a2}").format(a1=ConfNodeName, a2=str(exc)) self.GetCTRoot().logger.write_error(msg) self.GetCTRoot().logger.write_error(traceback.format_exc()) def LoadChildren(self): # Iterate over all CTNName@CTNType in confnode directory, and try to open them for CTNDir in os.listdir(self.CTNPath()): if os.path.isdir(os.path.join(self.CTNPath(), CTNDir)) and \ CTNDir.count(NameTypeSeparator) == 1: pname, ptype = CTNDir.split(NameTypeSeparator) try: self.CTNAddChild(pname, ptype) except Exception as exc: msg = _("Could not add child \"{a1}\", type {a2} :\n{a3}\n").format(a1=pname, a2=ptype, a3=str(exc)) self.GetCTRoot().logger.write_error(msg) self.GetCTRoot().logger.write_error(traceback.format_exc()) def FatalError(self, message): """ Raise an exception that will trigger error message intended to the user, but without backtrace since it is not a software error """ raise UserAddressedException(message)