# 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
from functools import reduce
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: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"/>
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.
EditorType = ConfTreeNodeEditor
def _AddParamsMembers(self):
self.Parser = GenerateParserFromXSDstring(self.XSD)
obj = self.Parser.CreateRoot()
self.CTNParams = (name, obj)
self.BaseParams = _BaseParamsParser.CreateRoot()
self.MandatoryParams = ("BaseParams", self.BaseParams)
# 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):
project_path = self.CTNParent.CTNPath()
return os.path.join(project_path,
CTNName + NameTypeSeparator + self.CTNType)
return self.BaseParams.getName()
return self.BaseParams.getEnabled()
parent = self.CTNParent.CTNFullName()
return parent + "." + self.CTNName()
return self.BaseParams.getName()
def CTNSearch(self, criteria):
# TODO match config's fields name and fields contents
CTNChild.CTNSearch(criteria)
for CTNChild in self.IterChildren()], [])
def CTNTestModified(self):
return self.ChangesToSave
def CTNMarkModified(self):
oldChangesToSave = self.ChangesToSave
self.ChangesToSave = True
appframe = self.GetCTRoot().AppFrame
appframe.RefreshPageTitles()
appframe.RefreshFileMenu()
def ProjectTestModified(self):
recursively check modified status
if self.CTNTestModified():
for CTNChild in self.IterChildren():
if CTNChild.ProjectTestModified():
def RemoteExec(self, script, **kwargs):
return self.CTNParent.RemoteExec(script, **kwargs)
def OnCTNSave(self, from_project_path=None):
"""Default, do nothing and return success"""
def GetParamsAttributes(self, path=None):
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])
params.append(self.CTNParams[1].getElementInfos(self.CTNParams[0]))
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)
new_leading = ".".join(map(str, self.CTNParent.GetCurrentLocation() + (new_value,)))
self.GetCTRoot().UpdateProjectVariableLocation(old_leading, new_leading)
elif path == "BaseParams.Name":
res = self.FindNewName(value)
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"]
def CTNRequestSave(self, from_project_path=None):
if self.GetCTRoot().CheckProjectPathPerm():
# If confnode do not have corresponding directory
if not os.path.isdir(ctnpath):
# generate XML for base XML parameters controller of the confnode
BaseXMLFile = open(self.ConfNodeBaseXmlFilePath(), 'w', encoding='utf-8')
BaseXMLFile.write(etree.tostring(
encoding='utf-8').decode())
# generate XML for XML parameters controller of the confnode
XMLFile = open(self.ConfNodeXmlFilePath(), 'w', encoding='utf-8')
XMLFile.write(etree.tostring(
encoding='utf-8').decode())
# Call the confnode specific OnCTNSave method
result = self.OnCTNSave(from_project_path)
return _("Error while saving \"%s\"\n") % self.CTNPath()
self.ChangesToSave = False
# go through all children and do the same
for CTNChild in self.IterChildren():
if from_project_path is not None:
CTNChildPath = CTNChild.CTNPath(project_path=from_project_path)
result = CTNChild.CTNRequestSave(CTNChildPath)
def CTNImport(self, src_CTNPath):
shutil.copytree(src_CTNPath, self.CTNPath)
def CTNGlobalInstances(self):
@return: [(instance_name, instance_type),...]
def _GlobalInstances(self):
instances = self.CTNGlobalInstances()
for CTNChild in self.IECSortedChildren():
instances.extend(CTNChild._GlobalInstances())
def CTNGenerate_C(self, buildpath, locations):
@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")
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
LocationCFilesAndCFLAGS = [(self.GetCurrentLocation(), CTNCFilesAndCFLAGS, DoCalls)]
LocationCFilesAndCFLAGS = []
# confnode asks for some LDFLAGS
if CTNLDFLAGS is not None:
# LDFLAGS can be either string
if isinstance(CTNLDFLAGS, str):
elif isinstance(CTNLDFLAGS, list):
# 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 = \
# filter locations that start with current IEC location
[loc for loc in locations if loc["LOC"][0:depth] == new_location])
LocationCFilesAndCFLAGS += _LocationCFilesAndCFLAGS
extra_files += _extra_files
return LocationCFilesAndCFLAGS, LDFLAGS, extra_files
for _CTNType, Children in list(self.Children.items()):
for CTNInstance in Children:
def IECSortedChildren(self):
# reorder children by IEC_channels
ordered = [(chld.BaseParams.getIEC_Channel(), chld) for chld in self.IterChildren()]
return list(zip(*ordered))[1]
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
# Recurse in order to find the latest object
return CTNInstance._GetChildBySomething(something, toks[1:])
def GetChildByName(self, Name):
return self._GetChildBySomething("Name", toks)
def GetChildByIECLocation(self, Location):
return self._GetChildBySomething("IEC_Channel", Location)
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() + "."
return self.CTNParent.GetCTRoot()
def GetFullIEC_Channel(self):
return ".".join([str(i) for i in self.GetCurrentLocation()]) + ".x"
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"
for child in self.IECSortedChildren():
children.append(child.GetVariableLocationTree())
return {"name": self.BaseParams.getName(),
"type": LOCATION_CONFNODE,
"location": self.GetFullIEC_Channel(),
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
for CTNInstance in self.CTNParent.IterChildren():
AllNames.append(CTNInstance.BaseParams.getName())
# Find a free name, eventually appending digit
if DesiredName.endswith("_0"):
BaseDesiredName = DesiredName[:-2]
BaseDesiredName = DesiredName
res = "%s_%d" % (BaseDesiredName, suffix)
# Check previous confnode existance
dontexist = self.BaseParams.getName() == "__unnamed__"
self.BaseParams.setName(res)
# Rename confnode dir if exist
shutil.move(oldpath, self.CTNPath())
# warn user he has two left hands
msg = _("A child named \"{a1}\" already exists -> \"{a2}\"\n").format(a1=DesiredName, a2=res)
self.GetCTRoot().logger.write_warning(msg)
def GetAllChannels(self):
for CTNInstance in self.CTNParent.IterChildren():
AllChannels.append(CTNInstance.BaseParams.getIEC_Channel())
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
while res in AllChannels: # While channel not free
if res < CurrentChannel: # Want to go down ?
self.GetCTRoot().logger.write_warning(_("Cannot find lower free IEC channel than %d\n") % CurrentChannel)
return CurrentChannel # Can't go bellow 0, do nothing
# Finally set IEC Channel
self.BaseParams.setIEC_Channel(res)
def GetContextualMenuItems(self):
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)
def _OpenView(self, name=None, onlyopened=False):
view = self.GetView(onlyopened)
name = self.CTNFullName()
app_frame = self.GetCTRoot().AppFrame
app_frame.EditProjectElement(view, name, onlyopened)
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 is not None:
self._CloseView(self._View)
def _doRemoveChild(self, CTNInstance):
# Remove all children of child
for SubCTNInstance in CTNInstance.IterChildren():
CTNInstance._doRemoveChild(SubCTNInstance)
shutil.rmtree(CTNInstance.CTNPath())
# 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)
# 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
CTNClass, CTNHelp = CTNChildrenTypes[CTNType]
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):
# Eventualy Initialize child instance list for this class of confnode
ChildrenWithSameClass = self.Children.setdefault(CTNType, list())
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)
# 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):
# Keep track of the confnode type name
# remind the help string, for more fancy display
# Call the base confnode template init - change XSD into class members
ConfigTreeNode.__init__(self)
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:
_("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):
# Load and init all the children
# just loaded, nothing to saved
self.ChangesToSave = False
# If confnode do not have corresponding file/dirs - they will be created on Save
self.FindNewIEC_Channel(IEC_Channel)
# Call the confnode real __init__
if getattr(CTNClass, "__init__", None):
# just created, must be saved
self.ChangesToSave = True
return self.CTNParent._getBuildPath()
# Create the object out of the resulting class
newConfNodeOpj = FinalCTNClass(self)
# Store it in CTNgedChils
ChildrenWithSameClass.append(newConfNodeOpj)
for child in self.IterChildren():
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()
basexmlfile = open(self.ConfNodeBaseXmlFilePath(CTNName), 'r')
self.BaseParams, error = _BaseParamsParser.LoadXMLString(basexmlfile.read())
(fname, lnum, src) = ((ConfNodeName + " BaseParams",) + error)
self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src))
self.MandatoryParams = ("BaseParams", self.BaseParams)
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())
xmlfile = open(self.ConfNodeXmlFilePath(CTNName), 'r')
obj, error = self.Parser.LoadXMLString(xmlfile.read())
(fname, lnum, src) = ((ConfNodeName,) + error)
self.GetCTRoot().logger.write_warning(XSDSchemaErrorMessage.format(a1=fname, a2=lnum, a3=src))
self.CTNParams = (name, obj)
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())
# 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)
self.CTNAddChild(pname, ptype)
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)