Edouard@2020: #!/usr/bin/env python Edouard@2020: # -*- coding: utf-8 -*- Edouard@2020: Edouard@2020: # This file is part of Beremiz, a Integrated Development Environment for Edouard@2020: # programming IEC 61131-3 automates supporting plcopen standard. Edouard@2020: # This files implements the bacnet plugin for Beremiz, adding BACnet server support. Edouard@2020: # Edouard@2020: # Copyright (c) 2017 Mario de Sousa (msousa@fe.up.pt) Edouard@2020: # Edouard@2020: # This program is free software: you can redistribute it and/or modify Edouard@2020: # it under the terms of the GNU General Public License as published by Edouard@2020: # the Free Software Foundation, either version 2 of the License, or Edouard@2020: # (at your option) any later version. Edouard@2020: # Edouard@2020: # This program is distributed in the hope that it will be useful, Edouard@2020: # but WITHOUT ANY WARRANTY; without even the implied warranty of Edouard@2020: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Edouard@2020: # GNU General Public License for more details. Edouard@2020: # Edouard@2020: # You should have received a copy of the GNU General Public License Edouard@2020: # along with this program. If not, see . Edouard@2250: # Edouard@2020: # This code is made available on the understanding that it will not be Edouard@2020: # used in safety-critical situations without a full and competent review. Edouard@2020: Edouard@2250: from __future__ import absolute_import Edouard@2250: Edouard@2250: import os Edouard@2020: from collections import Counter Edouard@2250: from datetime import datetime Edouard@2250: import pickle Edouard@2250: Edouard@2250: import wx Edouard@2250: Edouard@2250: from bacnet.BacnetSlaveEditor import * Edouard@2250: from bacnet.BacnetSlaveEditor import ObjectProperties Edouard@2250: from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY msousa@2649: from ConfigTreeNode import ConfigTreeNode Edouard@2669: import util.paths as paths msousa@2649: Edouard@2736: BacnetPath = paths.ThirdPartyPath("BACnet") Edouard@2250: BacnetLibraryPath = os.path.join(BacnetPath, "lib") Edouard@2250: BacnetIncludePath = os.path.join(BacnetPath, "include") Edouard@2020: BacnetIncludePortPath = os.path.join(BacnetPath, "ports") Edouard@2020: BacnetIncludePortPath = os.path.join(BacnetIncludePortPath, "linux") Edouard@2020: Edouard@2020: # Parameters to be monkey patched in beremiz customizations Edouard@2250: BACNET_VENDOR_ID = 9999 Edouard@2020: BACNET_VENDOR_NAME = "Beremiz.org" Edouard@2020: BACNET_DEVICE_MODEL_NAME = "Beremiz PLC" Edouard@2020: msousa@2649: msousa@2649: # Max String Size of BACnet Paramaters msousa@2649: BACNET_PARAM_STRING_SIZE = 64 msousa@2649: Edouard@2250: # Edouard@2250: # Edouard@2250: # Edouard@2250: # S L A V E D E V I C E # Edouard@2250: # Edouard@2250: # Edouard@2250: # Edouard@2020: Edouard@2020: # NOTE: Objects of class _BacnetSlavePlug are never instantiated directly. Edouard@2020: # The objects are instead instantiated from class FinalCTNClass Edouard@2020: # FinalCTNClass inherits from: - ConfigTreeNode Edouard@2020: # - The tree node plug (in our case _BacnetSlavePlug) Edouard@2250: # class _BacnetSlavePlug: Edouard@2250: Edouard@2250: Edouard@2250: class RootClass(object): Edouard@2020: XSD = """ Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2250: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: Edouard@2020: """ msousa@2649: # NOTE; Add the following code/declaration to the aboce XSD in order to activate the msousa@2649: # Override_Parameters_Saved_on_PLC flag (currenty not in use as it requires further msousa@2649: # analysis how the user would interpret this user interface option. msousa@2649: # <--- snip ---> msousa@2649: # msousa@2649: # <--- snip ---> msousa@2649: # Edouard@2020: # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID) Edouard@2020: # so the Device instance ID is limited from 0 to 22^2-1 = 4194303 Edouard@2020: # However, 4194303 is reserved for special use (similar to NULL pointer), so last Edouard@2020: # valid ID becomes 4194302 Edouard@2250: Edouard@2020: # The class/object that will render the graphical interface to edit the Edouard@2020: # BacnetSlavePlug's configuration parameters. The object of class BacnetSlaveEditorPlug Edouard@2020: # will be instantiated by the ConfigTreeNode class. Edouard@2020: # This BacnetSlaveEditorPlug object can be accessed from _BacnetSlavePlug as Edouard@2020: # 'self._View' Edouard@2020: # See the following note to understand how this is possible! Edouard@2020: # Edouard@2020: # NOTE: Objects of class _BacnetSlavePlug are never instantiated directly. Edouard@2020: # The objects are instead instantiated from class FinalCTNClass Edouard@2020: # FinalCTNClass inherits from: - ConfigTreeNode Edouard@2020: # - The tree node plug (in our case _BacnetSlavePlug) Edouard@2020: # Edouard@2020: # This means that objects of class _BacnetSlavePlug may safely access all the members Edouard@2020: # of classes ConfigTreeNode as well as FinalCTNClass (since they are always instantiated Edouard@2020: # as a FinalCTNClass) Edouard@2020: EditorType = BacnetSlaveEditorPlug Edouard@2250: Edouard@2020: # The following classes follow the model/viewer design pattern Edouard@2020: # Edouard@2020: # _BacnetSlavePlug - contains the model (i.e. configuration parameters) Edouard@2250: # BacnetSlaveEditorPlug - contains the viewer (and editor, so it includes the 'controller' part of the Edouard@2020: # design pattern which in this case is not separated from the viewer) Edouard@2020: # Edouard@2250: # The _BacnetSlavePlug object is 'permanent', i.e. it exists as long as the beremiz project is open Edouard@2020: # The BacnetSlaveEditorPlug object is 'transient', i.e. it exists only while the editor is visible/open Edouard@2020: # in the editing panel. It is destoryed whenever Edouard@2250: # the user closes the corresponding tab in the Edouard@2020: # editing panel, and a new object is created when Edouard@2020: # the editor is re-opened. Edouard@2020: # Edouard@2020: # _BacnetSlavePlug contains: AV_ObjTable, ... Edouard@2020: # (these are the objects that actually store the config parameters or 'model' Edouard@2020: # and are therefore stored to a file) Edouard@2020: # Edouard@2020: # _BacnetSlavePlug contains: AV_VarEditor, ... Edouard@2250: # (these are the objects that implement a grid table to edit/view the Edouard@2020: # corresponding mode parameters) Edouard@2250: # Edouard@2020: # Logic: Edouard@2020: # - The xx_VarEditor classes inherit from wx.grid.Grid Edouard@2020: # - The xx_ObjTable classes inherit from wx.grid.PyGridTableBase Edouard@2020: # To be more precise, the inheritance tree is actually: Edouard@2020: # xx_VarEditor -> ObjectGrid -> CustomGrid -> wx.grid.Grid Edouard@2020: # xx_ObjTable -> ObjectTable -> CustomTable -> wx.grid.PyGridTableBase) Edouard@2020: # Edouard@2020: # Note that wx.grid.Grid is prepared to work with wx.grid.PyGridTableBase as the container of Edouard@2020: # data that is displayed and edited in the Grid. Edouard@2020: Edouard@2020: ConfNodeMethods = [ Edouard@2250: {"bitmap": "ExportSlave", Edouard@2250: "name": _("Export slave"), Edouard@2250: "tooltip": _("Export BACnet slave to EDE file"), Edouard@2250: "method": "_ExportBacnetSlave"}, Edouard@2020: ] Edouard@2250: Edouard@2020: def __init__(self): Edouard@2250: # Initialize the dictionary that stores the current configuration for the Analog/Digital/MultiValued Variables Edouard@2020: # in this BACnet server. Edouard@2020: self.ObjTablesData = {} Edouard@2250: Edouard@2250: # Each list will contain an entry for each row in the xxxxVar grid!! Edouard@2250: # Each entry/row will be a dictionary Edouard@2250: # Each dictionary will contain all entries/data Edouard@2250: # for one row in the grid. Edouard@2250: Edouard@2250: self.ObjTablesData["AV_Obj"] = [] Edouard@2250: self.ObjTablesData["AO_Obj"] = [] Edouard@2250: self.ObjTablesData["AI_Obj"] = [] Edouard@2250: self.ObjTablesData["BV_Obj"] = [] Edouard@2250: self.ObjTablesData["BO_Obj"] = [] Edouard@2250: self.ObjTablesData["BI_Obj"] = [] Edouard@2250: self.ObjTablesData["MSV_Obj"] = [] Edouard@2250: self.ObjTablesData["MSO_Obj"] = [] Edouard@2250: self.ObjTablesData["MSI_Obj"] = [] Edouard@2250: Edouard@2250: self.ObjTablesData["EDEfile_parm"] = {"next_EDE_file_version": 1} Edouard@2250: Edouard@2250: # EDE files inlcude extra parameters (ex. file version) Edouard@2250: # We would like to save the parameters the user configures Edouard@2250: # so they are available the next time the user opens the project. Edouard@2250: # Since this plugin is only storing the ObjTablesData[] dict Edouard@2250: # to file, we add that info to this dictionary too. Edouard@2250: # Yes, I know this is kind of a Edouard@2250: # hack. Edouard@2250: Edouard@2020: filepath = self.GetFileName() Edouard@2250: if os.path.isfile(filepath): Edouard@2020: self.LoadFromFile(filepath) Edouard@2020: Edouard@2020: self.ObjTables = {} Edouard@2250: self.ObjTables["AV_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["AV_Obj"], AVObject) Edouard@2250: self.ObjTables["AO_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["AO_Obj"], AOObject) Edouard@2250: self.ObjTables["AI_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["AI_Obj"], AIObject) Edouard@2250: self.ObjTables["BV_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["BV_Obj"], BVObject) Edouard@2250: self.ObjTables["BO_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["BO_Obj"], BOObject) Edouard@2250: self.ObjTables["BI_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["BI_Obj"], BIObject) Edouard@2250: self.ObjTables["MSV_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["MSV_Obj"], MSVObject) Edouard@2250: self.ObjTables["MSO_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["MSO_Obj"], MSOObject) Edouard@2250: self.ObjTables["MSI_Obj"] = ObjectTable( Edouard@2250: self, self.ObjTablesData["MSI_Obj"], MSIObject) Edouard@2250: Edouard@2250: # Edouard@2020: # Functions to be called by CTNClass # Edouard@2250: # Edouard@2020: # The following functions would be somewhat equvalent to virtual functions/methods in C++ classes Edouard@2250: # They will be called by the base class (CTNClass) from which this Edouard@2250: # _BacnetSlavePlug class derives. Edouard@2250: Edouard@2020: def GetCurrentNodeName(self): Edouard@2020: return self.CTNName() Edouard@2020: Edouard@2020: def GetFileName(self): Edouard@2020: return os.path.join(self.CTNPath(), 'bacnet_slave') Edouard@2250: Edouard@2020: def OnCTNSave(self, from_project_path=None): Edouard@2250: return self.SaveToFile(self.GetFileName()) Edouard@2020: Edouard@2020: def CTNTestModified(self): Edouard@2020: # self.ChangesToSave: Check whether any of the parameters, defined in the XSD above, were changed. Edouard@2020: # This is handled by the ConfigTreeNode class Edouard@2250: # (Remember that no objects are ever instantiated from _BacnetSlavePlug. Edouard@2020: # Objects are instead created from FinalCTNClass, which derives from Edouard@2020: # _BacnetSlavePlug and ConfigTreeNode. This means that we can exceptionally Edouard@2250: # consider that all objects of type _BacnetSlavePlug will also be a Edouard@2250: # ConfigTreeNode). Edouard@2250: result = self.ChangesToSave \ Edouard@2250: or self.ObjTables["AV_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["AO_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["AI_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["BV_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["BO_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["BI_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["MSV_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["MSO_Obj"].ChangesToSave \ Edouard@2250: or self.ObjTables["MSI_Obj"].ChangesToSave Edouard@2020: return result Edouard@2020: Edouard@2250: # Currently not needed. Override _OpenView() in case we need to do some special stuff whenever the editor is opened! Edouard@2250: # def _OpenView(self, name=None, onlyopened=False): Edouard@2250: # print "_BacnetSlavePlug._OpenView() Called!!!" Edouard@2250: # ConfigTreeNode._OpenView(self, name, onlyopened) Edouard@2250: # print self._View Edouard@2250: # if self._View is not None: Edouard@2250: # self._View.SetBusId(self.GetCurrentLocation()) Edouard@2250: # return self._View Edouard@2020: Edouard@2020: def GetVariableLocationTree(self): Edouard@2020: current_location = self.GetCurrentLocation() Edouard@2020: # see comment in CTNGenerate_C regarding identical line of code! Edouard@2250: locstr = ".".join(map(str, current_location)) Edouard@2250: Edouard@2020: # IDs used by BACnet to identify object types/class. Edouard@2020: # OBJECT_ANALOG_INPUT = 0, Edouard@2020: # OBJECT_ANALOG_OUTPUT = 1, Edouard@2020: # OBJECT_ANALOG_VALUE = 2, Edouard@2020: # OBJECT_BINARY_INPUT = 3, Edouard@2020: # OBJECT_BINARY_OUTPUT = 4, Edouard@2020: # OBJECT_BINARY_VALUE = 5, Edouard@2020: # OBJECT_MULTI_STATE_INPUT = 13, Edouard@2020: # OBJECT_MULTI_STATE_OUTPUT = 14, Edouard@2020: # OBJECT_MULTI_STATE_VALUE = 19, Edouard@2020: # Edouard@2020: # Since Binary Value, Analog Value, etc. objects may use the same Edouard@2020: # object ID (since they have distinct class IDs), we must also distinguish them in some way in Edouard@2020: # the %MX0.3.4 IEC 61131-3 syntax. Edouard@2020: # Edouard@2020: # For this reason we add the BACnet class identifier to the %MX0.5.3 location. Edouard@2020: # For example, for a BACnet plugin in location '0' of the Beremiz configuration tree, Edouard@2020: # all Binary Values will be mapped onto: %MX0.5.xxx (xxx is object ID) Edouard@2020: # all Multi State Values will be mapped onto: %MB0.19.xxx (xxx is object ID) Edouard@2020: # all Analog Values will be mapped onto: %MD0.2.xxx (xxx is object ID) Edouard@2020: # etc.. Edouard@2020: # Edouard@2020: # Value objects will be mapped onto %M Edouard@2020: # Input objects will be mapped onto %I Edouard@2020: # Output objects will be mapped onto %Q Edouard@2250: Edouard@2020: BACnetEntries = [] Edouard@2020: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["AV_Obj"], 32, 'REAL', 'D', locstr + '.2', 'Analog Values')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["AO_Obj"], 32, 'REAL', 'D', locstr + '.1', 'Analog Outputs')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["AI_Obj"], 32, 'REAL', 'D', locstr + '.0', 'Analog Inputs')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["BV_Obj"], 1, 'BOOL', 'X', locstr + '.5', 'Binary Values')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["BO_Obj"], 1, 'BOOL', 'X', locstr + '.4', 'Binary Outputs')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["BI_Obj"], 1, 'BOOL', 'X', locstr + '.3', 'Binary Inputs')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["MSV_Obj"], 8, 'BYTE', 'B', locstr + '.19', 'Multi State Values')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["MSO_Obj"], 8, 'BYTE', 'B', locstr + '.14', 'Multi State Outputs')) Edouard@2250: BACnetEntries.append(self.GetSlaveLocationTree( Edouard@2250: self.ObjTablesData["MSI_Obj"], 8, 'BYTE', 'B', locstr + '.13', 'Multi State Inputs')) Edouard@2250: Edouard@2250: return {"name": self.BaseParams.getName(), Edouard@2250: "type": LOCATION_CONFNODE, Edouard@2250: "location": locstr + ".x", Edouard@2250: "children": BACnetEntries} Edouard@2250: Edouard@2250: # Edouard@2020: # Helper functions/methods # Edouard@2250: # Edouard@2020: # a helper function to GetVariableLocationTree() Edouard@2020: def GetSlaveLocationTree(self, ObjTablesData, size_in_bits, IECdatatype, location_size, location_str, name): Edouard@2020: BACnetObjectEntries = [] Edouard@2250: for xx_ObjProp in ObjTablesData: Edouard@2020: BACnetObjectEntries.append({ Edouard@2020: "name": str(xx_ObjProp["Object Identifier"]) + ': ' + xx_ObjProp["Object Name"], Edouard@2250: "type": LOCATION_VAR_MEMORY, # LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, or LOCATION_VAR_MEMORY Edouard@2250: "size": size_in_bits, # 1 or 16 Edouard@2250: "IEC_type": IECdatatype, # 'BOOL', 'WORD', ... Edouard@2250: "var_name": "var_name", # seems to be ignored?? Edouard@2020: "location": location_size + location_str + "." + str(xx_ObjProp["Object Identifier"]), Edouard@2250: "description": "description", # seems to be ignored? Edouard@2020: "children": []}) Edouard@2250: Edouard@2250: return {"name": name, Edouard@2250: "type": LOCATION_CONFNODE, Edouard@2250: "location": location_str + ".x", Edouard@2250: "children": BACnetObjectEntries} Edouard@2250: Edouard@2020: # Returns a dictionary with: Edouard@2020: # keys: names of BACnet objects Edouard@2250: # value: number of BACnet objects using this same name Edouard@2020: # (values larger than 1 indicates an error as BACnet requires unique names) Edouard@2020: def GetObjectNamesCount(self): Edouard@2250: # The dictionary is built by first creating a list containing the names of _ALL_ Edouard@2020: # BACnet objects currently configured by the user (using the GUI) Edouard@2020: ObjectNames = [] Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["AV_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["AO_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["AI_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["BV_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["BO_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["BI_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["MSV_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["MSO_Obj"].GetAllValuesByName("Object Name")) Edouard@2250: ObjectNames.extend( Edouard@2250: self.ObjTables["MSI_Obj"].GetAllValuesByName("Object Name")) Edouard@2020: # This list is then transformed into a collections.Counter class Edouard@2020: # Which is then transformed into a dictionary using dict() Edouard@2020: return dict(Counter(ObjectNames)) Edouard@2250: Edouard@2020: # Check whether the current configuration contains BACnet objects configured Edouard@2020: # with the same identical object name (returns True or False) Edouard@2020: def HasDuplicateObjectNames(self): Edouard@2020: ObjectNamesCount = self.GetObjectNamesCount() Edouard@2020: for ObjName in ObjectNamesCount: Edouard@2020: if ObjectNamesCount[ObjName] > 1: Edouard@2020: return True Edouard@2020: return False Edouard@2020: Edouard@2020: # Check whether any object ID is used more than once (not valid in BACnet) Edouard@2020: # (returns True or False) Edouard@2020: def HasDuplicateObjectIDs(self): Edouard@2250: res = self.ObjTables["AV_Obj"].HasDuplicateObjectIDs() Edouard@2250: res = res or self.ObjTables["AO_Obj"].HasDuplicateObjectIDs() Edouard@2250: res = res or self.ObjTables["AI_Obj"].HasDuplicateObjectIDs() Edouard@2250: res = res or self.ObjTables["BV_Obj"].HasDuplicateObjectIDs() Edouard@2250: res = res or self.ObjTables["BO_Obj"].HasDuplicateObjectIDs() Edouard@2250: res = res or self.ObjTables["BI_Obj"].HasDuplicateObjectIDs() Edouard@2020: res = res or self.ObjTables["MSV_Obj"].HasDuplicateObjectIDs() Edouard@2020: res = res or self.ObjTables["MSO_Obj"].HasDuplicateObjectIDs() Edouard@2020: res = res or self.ObjTables["MSI_Obj"].HasDuplicateObjectIDs() Edouard@2020: return res Edouard@2020: Edouard@2250: # Edouard@2020: # Methods related to files (saving/loading/exporting) # Edouard@2250: # Edouard@2020: def SaveToFile(self, filepath): Edouard@2020: # Save node data in file Edouard@2020: # The configuration data declared in the XSD string will be saved by the ConfigTreeNode class, Edouard@2020: # so we only need to save the data that is stored in ObjTablesData objects Edouard@2250: # Note that we do not store the ObjTables objects. ObjTables is of a class that Edouard@2020: # contains more stuff we do not need to store. Actually it is a bad idea to store Edouard@2020: # this extra stuff (as we would make the files we generate dependent on the actual Edouard@2020: # version of the wx library we are using!!! Remember that ObjTables evetually Edouard@2020: # derives/inherits from wx.grid.PyGridTableBase). Another reason not to store the whole Edouard@2020: # object is because it is not pickable (i.e. pickle.dump() cannot handle it)!! Edouard@2020: try: Edouard@2020: fd = open(filepath, "w") Edouard@2020: pickle.dump(self.ObjTablesData, fd) Edouard@2020: fd.close() Edouard@2250: # On successfull save, reset flags to indicate no more changes that Edouard@2250: # need saving Edouard@2250: self.ObjTables["AV_Obj"].ChangesToSave = False Edouard@2250: self.ObjTables["AO_Obj"].ChangesToSave = False Edouard@2250: self.ObjTables["AI_Obj"].ChangesToSave = False Edouard@2250: self.ObjTables["BV_Obj"].ChangesToSave = False Edouard@2250: self.ObjTables["BO_Obj"].ChangesToSave = False Edouard@2250: self.ObjTables["BI_Obj"].ChangesToSave = False Edouard@2020: self.ObjTables["MSV_Obj"].ChangesToSave = False Edouard@2020: self.ObjTables["MSO_Obj"].ChangesToSave = False Edouard@2020: self.ObjTables["MSI_Obj"].ChangesToSave = False Edouard@2020: return True edouard@2309: except Exception: Edouard@2250: return _("Unable to save to file \"%s\"!") % filepath Edouard@2020: Edouard@2020: def LoadFromFile(self, filepath): Edouard@2020: # Load the data that is saved in SaveToFile() Edouard@2020: try: Edouard@2020: fd = open(filepath, "r") Edouard@2020: self.ObjTablesData = pickle.load(fd) Edouard@2020: fd.close() Edouard@2020: return True edouard@2309: except Exception: Edouard@2250: return _("Unable to load file \"%s\"!") % filepath Edouard@2020: Edouard@2020: def _ExportBacnetSlave(self): Edouard@2250: dialog = wx.FileDialog(self.GetCTRoot().AppFrame, Edouard@2250: _("Choose a file"), Edouard@2250: os.path.expanduser("~"), Edouard@2250: "%s_EDE.csv" % self.CTNName(), Edouard@2020: _("EDE files (*_EDE.csv)|*_EDE.csv|All files|*.*"), Edouard@2250: wx.SAVE | wx.OVERWRITE_PROMPT) Edouard@2020: if dialog.ShowModal() == wx.ID_OK: Edouard@2020: result = self.GenerateEDEFile(dialog.GetPath()) Edouard@2020: result = False Edouard@2020: if result: Edouard@2250: self.GetCTRoot().logger.write_error( Edouard@2250: _("Error: Export slave failed\n")) Edouard@2250: dialog.Destroy() Edouard@2020: Edouard@2020: def GenerateEDEFile(self, filename): Edouard@2250: template_file_dir = os.path.join( Edouard@2250: os.path.split(__file__)[0], "ede_files") Edouard@2250: Edouard@2250: # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() Edouard@2250: # It will be an XML parser object created by Edouard@2250: # GenerateParserFromXSDstring(self.XSD).CreateRoot() Edouard@2020: BACnet_Device_ID = self.BACnetServerNode.getBACnet_Device_ID() Edouard@2250: Edouard@2020: # The EDE file contains a header that includes general project data (name, author, ...) Edouard@2020: # Instead of asking the user for this data, we get it from the configuration Edouard@2020: # of the Beremiz project itself. Edouard@2020: # We ask the root Config Tree Node for the data... Edouard@2020: ProjProp = {} Edouard@2020: FileProp = {} Edouard@2250: Edouard@2250: # this should be an object of class ProjectController Edouard@2250: CTN_Root = self.GetCTRoot() Edouard@2250: Edouard@2250: # this should be an object capable of parsing Edouard@2250: # PLCopen XML files. The parser is created automatically Edouard@2250: # (i.e. using GenerateParserFromXSD() from xmlclass module) Edouard@2250: # using the PLCopen XSD file defining the format of the XML. Edouard@2250: # See the file plcopen/plcopen.py Edouard@2250: Project = CTN_Root.Project Edouard@2020: if Project is not None: Edouard@2250: # getcontentHeader() and getfileHeader() are functions that are conditionally defined in Edouard@2250: # plcopn/plcopen.py We cannot rely on their existance Edouard@2250: if getattr(Project, "getcontentHeader", None) is not None: Edouard@2250: ProjProp = Project.getcontentHeader() Edouard@2250: # getcontentHeader() returns a dictionary. Available keys are: Edouard@2250: # "projectName", "projectVersion", "modificationDateTime", Edouard@2250: # "organization", "authorName", "language", "pageSize", "scaling" Edouard@2250: if getattr(Project, "getfileHeader", None) is not None: Edouard@2250: FileProp = Project.getfileHeader() Edouard@2250: # getfileHeader() returns a dictionary. Available keys are: Edouard@2250: # "companyName", "companyURL", "productName", "productVersion", Edouard@2250: # "productRelease", "creationDateTime", "contentDescription" Edouard@2250: Edouard@2250: ProjName = "" Edouard@2020: if "projectName" in ProjProp: Edouard@2250: ProjName = ProjProp["projectName"] Edouard@2020: ProjAuthor = "" Edouard@2020: if "companyName" in FileProp: Edouard@2020: ProjAuthor += "(" + FileProp["companyName"] + ")" Edouard@2020: if "authorName" in ProjProp: Edouard@2250: ProjAuthor = ProjProp["authorName"] + " " + ProjAuthor Edouard@2250: Edouard@2020: projdata_dict = {} Edouard@2250: projdata_dict["Project Name"] = ProjName Edouard@2250: projdata_dict["Project Author"] = ProjAuthor Edouard@2250: projdata_dict["Current Time"] = datetime.now().strftime( Edouard@2250: '%Y-%m-%d %H:%M:%S') Edouard@2250: projdata_dict["EDE file version"] = self.ObjTablesData[ Edouard@2250: "EDEfile_parm"]["next_EDE_file_version"] Edouard@2020: Edouard@2020: # Next time we generate an EDE file, use another version! Edouard@2020: self.ObjTablesData["EDEfile_parm"]["next_EDE_file_version"] += 1 Edouard@2020: Edouard@2250: AX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \ Edouard@2250: ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;;;%(Settable)s;N;;;;%(Unit ID)s;" Edouard@2250: Edouard@2250: BX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \ Edouard@2250: ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;0;1;%(Settable)s;N;;;;;" Edouard@2250: Edouard@2250: MSX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \ Edouard@2250: ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;1;1;%(Number of States)s;%(Settable)s;N;;;;;" Edouard@2250: Edouard@2020: Objects_List = [] Edouard@2250: for ObjType, params_format in [("AV", AX_params_format), Edouard@2250: ("AO", AX_params_format), Edouard@2250: ("AI", AX_params_format), Edouard@2250: ("BV", BX_params_format), Edouard@2250: ("BO", BX_params_format), Edouard@2250: ("BI", BX_params_format), Edouard@2250: ("MSV", MSX_params_format), Edouard@2250: ("MSO", MSX_params_format), Edouard@2250: ("MSI", MSX_params_format)]: Edouard@2020: self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties() Edouard@2020: for ObjProp in self.ObjTablesData[ObjType + "_Obj"]: Edouard@2020: Objects_List.append(params_format % ObjProp) Edouard@2250: Edouard@2020: # Normalize filename Edouard@2020: for extension in ["_EDE.csv", "_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]: Edouard@2020: if filename.lower().endswith(extension.lower()): Edouard@2020: filename = filename[:-len(extension)] Edouard@2250: Edouard@2020: # EDE_header Edouard@2250: generate_file_name = filename + "_EDE.csv" Edouard@2250: template_file_name = os.path.join( Edouard@2250: template_file_dir, "template_EDE.csv") Edouard@2020: generate_file_content = open(template_file_name).read() % projdata_dict Edouard@2250: generate_file_handle = open(generate_file_name, 'w') Edouard@2020: generate_file_handle .write(generate_file_content) Edouard@2020: generate_file_handle .write("\n".join(Objects_List)) Edouard@2020: generate_file_handle .close() Edouard@2250: Edouard@2250: # templates of remaining files do not need changes. They are simply Edouard@2250: # copied unchanged! Edouard@2020: for extension in ["_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]: Edouard@2250: generate_file_name = filename + extension Edouard@2250: template_file_name = os.path.join( Edouard@2250: template_file_dir, "template" + extension) Edouard@2020: generate_file_content = open(template_file_name).read() Edouard@2250: generate_file_handle = open(generate_file_name, 'w') Edouard@2020: generate_file_handle .write(generate_file_content) Edouard@2020: generate_file_handle .close() Edouard@2020: msousa@2649: msousa@2649: msousa@2649: # msousa@2649: # Generate the C source code files Edouard@2250: # Edouard@2250: def CTNGenerate_C(self, buildpath, locations): Edouard@2250: # Determine the current location in Beremiz's project configuration Edouard@2250: # tree Edouard@2020: current_location = self.GetCurrentLocation() Edouard@2250: # The current location of this plugin in Beremiz's configuration tree, separated by underscores Edouard@2020: # NOTE: Since BACnet plugin currently does not use sub-branches in the tree (in other words, this Edouard@2020: # _BacnetSlavePlug class was actually renamed as the RootClass), the current_location_dots Edouard@2020: # will actually be a single number (e.g.: 0 or 3 or 6, corresponding to the location Edouard@2020: # in which the plugin was inserted in the Beremiz configuration tree on Beremiz's left panel). Edouard@2250: locstr = "_".join(map(str, current_location)) Edouard@2250: Edouard@2250: # First check whether all the current parameters (inserted by user in Edouard@2250: # the GUI) are valid... Edouard@2020: if self.HasDuplicateObjectNames(): Edouard@2250: self.GetCTRoot().logger.write_warning( andrej@2425: _("Error: BACnet server '{a1}.x:{a2}' contains objects with duplicate object names.\n"). andrej@2425: format(a1=locstr, a2=self.CTNName())) Edouard@2250: raise Exception(False) Edouard@2250: # TODO: return an error code instead of raising an exception Edouard@2250: # (currently unsupported by Beremiz) Edouard@2020: Edouard@2020: if self.HasDuplicateObjectIDs(): Edouard@2250: self.GetCTRoot().logger.write_warning( andrej@2425: _("Error: BACnet server '{a1}.x: {a2}' contains objects with duplicate object identifiers.\n"). andrej@2425: format(a1=locstr, a2=self.CTNName())) Edouard@2250: raise Exception(False) Edouard@2250: # TODO: return an error code instead of raising an exception Edouard@2250: # (currently unsupported by Beremiz) Edouard@2250: Edouard@2250: # ------------------------------------------------------------------------------- Edouard@2020: # Create and populate the loc_dict dictionary with all parameters needed to configure Edouard@2020: # the generated source code (.c and .h files) Edouard@2250: # ---------------------------------------------------------------------- Edouard@2250: Edouard@2020: # 1) Create the dictionary (loc_dict = {}) Edouard@2020: loc_dict = {} Edouard@2250: loc_dict["locstr"] = locstr Edouard@2250: Edouard@2250: # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() Edouard@2250: # It will be an XML parser object created by Edouard@2250: # GenerateParserFromXSDstring(self.XSD).CreateRoot() msousa@2649: # msousa@2649: # Note: Override_Parameters_Saved_on_PLC is converted to an integer by int() msousa@2649: # The above flag is not currently in use. It requires further thinking on how the msousa@2649: # user will interpret and interact with this user interface... msousa@2649: #loc_dict["Override_Parameters_Saved_on_PLC"] = int(self.BACnetServerNode.getOverride_Parameters_Saved_on_PLC()) Edouard@2250: loc_dict["network_interface"] = self.BACnetServerNode.getNetwork_Interface() Edouard@2250: loc_dict["port_number"] = self.BACnetServerNode.getUDP_Port_Number() Edouard@2250: loc_dict["BACnet_Device_ID"] = self.BACnetServerNode.getBACnet_Device_ID() Edouard@2250: loc_dict["BACnet_Device_Name"] = self.BACnetServerNode.getBACnet_Device_Name() Edouard@2020: loc_dict["BACnet_Comm_Control_Password"] = self.BACnetServerNode.getBACnet_Communication_Control_Password() Edouard@2250: loc_dict["BACnet_Device_Location"] = self.BACnetServerNode.getBACnet_Device_Location() Edouard@2250: loc_dict["BACnet_Device_Description"] = self.BACnetServerNode.getBACnet_Device_Description() Edouard@2250: loc_dict["BACnet_Device_AppSoft_Version"] = self.BACnetServerNode.getBACnet_Device_Application_Software_Version() Edouard@2020: loc_dict["BACnet_Vendor_ID"] = BACNET_VENDOR_ID Edouard@2020: loc_dict["BACnet_Vendor_Name"] = BACNET_VENDOR_NAME Edouard@2020: loc_dict["BACnet_Model_Name"] = BACNET_DEVICE_MODEL_NAME msousa@2649: loc_dict["BACnet_Param_String_Size"] = BACNET_PARAM_STRING_SIZE msousa@2649: Edouard@2020: Edouard@2020: # 2) Add the data specific to each BACnet object type Edouard@2020: # For each BACnet object type, start off by creating some intermediate helpful lists Edouard@2020: # a) parameters_list containing the strings that will Edouard@2250: # be included in the C source code, and which will initialize the struct with the Edouard@2020: # object (Analog Value, Binary Value, or Multi State Value) parameters Edouard@2020: # b) locatedvar_list containing the strings that will Edouard@2250: # declare the memory to store the located variables, as well as the Edouard@2020: # pointers (required by matiec) that point to that memory. Edouard@2020: Edouard@2250: # format for delaring IEC 61131-3 variable (and pointer) onto which Edouard@2250: # BACnet object is mapped Edouard@2020: locvar_format = '%(Ctype)s ___%(loc)s_%(Object Identifier)s; ' + \ Edouard@2020: '%(Ctype)s *__%(loc)s_%(Object Identifier)s = &___%(loc)s_%(Object Identifier)s;' Edouard@2020: Edouard@2020: # format for initializing a ANALOG_VALUE_DESCR struct in C code Edouard@2020: # also valid for ANALOG_INPUT and ANALOG_OUTPUT Edouard@2020: AX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ Edouard@2250: '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Unit ID)d}' Edouard@2020: # format for initializing a BINARY_VALUE_DESCR struct in C code Edouard@2020: # also valid for BINARY_INPUT and BINARY_OUTPUT Edouard@2020: BX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ Edouard@2250: '%(Object Identifier)s, "%(Object Name)s", "%(Description)s"}' Edouard@2020: Edouard@2020: # format for initializing a MULTISTATE_VALUE_DESCR struct in C code Edouard@2020: # also valid for MULTISTATE_INPUT and MULTISTATE_OUTPUT Edouard@2020: MSX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \ Edouard@2250: '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Number of States)s}' Edouard@2250: Edouard@2250: # see the comment in GetVariableLocationTree() Edouard@2250: AV_locstr = 'MD' + locstr + '_2' Edouard@2250: AO_locstr = 'QD' + locstr + '_1' Edouard@2250: AI_locstr = 'ID' + locstr + '_0' Edouard@2250: BV_locstr = 'MX' + locstr + '_5' Edouard@2250: BO_locstr = 'QX' + locstr + '_4' Edouard@2250: BI_locstr = 'IX' + locstr + '_3' Edouard@2250: MSV_locstr = 'MB' + locstr + '_19' Edouard@2250: MSO_locstr = 'QB' + locstr + '_14' Edouard@2250: MSI_locstr = 'IB' + locstr + '_13' Edouard@2250: Edouard@2250: for ObjType, ObjLocStr, params_format in [ Edouard@2250: ("AV", AV_locstr, AX_params_format), Edouard@2250: ("AO", AO_locstr, AX_params_format), Edouard@2250: ("AI", AI_locstr, AX_params_format), Edouard@2250: ("BV", BV_locstr, BX_params_format), Edouard@2250: ("BO", BO_locstr, BX_params_format), Edouard@2250: ("BI", BI_locstr, BX_params_format), Edouard@2250: ("MSV", MSV_locstr, MSX_params_format), Edouard@2250: ("MSO", MSO_locstr, MSX_params_format), Edouard@2250: ("MSI", MSI_locstr, MSX_params_format)]: Edouard@2020: parameters_list = [] Edouard@2020: locatedvar_list = [] Edouard@2020: self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties() Edouard@2020: for ObjProp in self.ObjTablesData[ObjType + "_Obj"]: Edouard@2250: ObjProp["loc"] = ObjLocStr Edouard@2020: parameters_list.append(params_format % ObjProp) Edouard@2020: locatedvar_list.append(locvar_format % ObjProp) Edouard@2250: loc_dict[ObjType + "_count"] = len(parameters_list) Edouard@2250: loc_dict[ObjType + "_param"] = ",\n".join(parameters_list) Edouard@2250: loc_dict[ObjType + "_lvars"] = "\n".join(locatedvar_list) Edouard@2250: Edouard@2250: # ---------------------------------------------------------------------- Edouard@2020: # Create the C source files that implement the BACnet server Edouard@2250: # ---------------------------------------------------------------------- Edouard@2250: Edouard@2020: # Names of the .c files that will be generated, based on a template file with same name Edouard@2020: # (names without '.c' --> this will be added later) Edouard@2020: # main server.c file is handled separately Edouard@2020: Generated_BACnet_c_mainfile = "server" Edouard@2250: Generated_BACnet_c_files = [ Edouard@2250: "ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv", "device"] Edouard@2020: Edouard@2020: # Names of the .h files that will be generated, based on a template file with same name Edouard@2020: # (names without '.h' --> this will be added later) Edouard@2250: Generated_BACnet_h_files = [ Edouard@2250: "server", "device", "config_bacnet_for_beremiz", Edouard@2250: "ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv" Edouard@2250: ] Edouard@2020: Edouard@2020: # Generate the files with the source code Edouard@2020: postfix = "_".join(map(str, current_location)) Edouard@2250: template_file_dir = os.path.join( Edouard@2250: os.path.split(__file__)[0], "runtime") Edouard@2250: Edouard@2250: def generate_file(file_name, extension): Edouard@2250: generate_file_name = os.path.join( Edouard@2250: buildpath, "%s_%s.%s" % (file_name, postfix, extension)) Edouard@2250: template_file_name = os.path.join( Edouard@2250: template_file_dir, "%s.%s" % (file_name, extension)) Edouard@2020: generate_file_content = open(template_file_name).read() % loc_dict Edouard@2250: generate_file_handle = open(generate_file_name, 'w') Edouard@2250: generate_file_handle.write(generate_file_content) Edouard@2250: generate_file_handle.close() Edouard@2020: Edouard@2020: for file_name in Generated_BACnet_c_files: Edouard@2020: generate_file(file_name, "c") Edouard@2020: for file_name in Generated_BACnet_h_files: Edouard@2020: generate_file(file_name, "h") Edouard@2020: generate_file(Generated_BACnet_c_mainfile, "c") Edouard@2020: Generated_BACnet_c_mainfile_name = \ Edouard@2250: os.path.join(buildpath, "%s_%s.%s" % Edouard@2250: (Generated_BACnet_c_mainfile, postfix, "c")) Edouard@2250: Edouard@2250: # ---------------------------------------------------------------------- Edouard@2020: # Finally, define the compilation and linking commands and flags Edouard@2250: # ---------------------------------------------------------------------- Edouard@2250: Edouard@2020: LDFLAGS = [] Edouard@2020: # when using dynamically linked library... Edouard@2250: # LDFLAGS.append(' -lbacnet') Edouard@2250: # LDFLAGS.append(' -L"'+BacnetLibraryPath+'"') Edouard@2250: # LDFLAGS.append(' "-Wl,-rpath,' + BacnetLibraryPath + '"') Edouard@2020: # when using static library: Edouard@2250: LDFLAGS.append( Edouard@2250: ' "' + os.path.join(BacnetLibraryPath, "libbacnet.a") + '"') Edouard@2250: Edouard@2250: CFLAGS = ' -I"' + BacnetIncludePath + '"' Edouard@2250: CFLAGS += ' -I"' + BacnetIncludePortPath + '"' Edouard@2250: msousa@2649: # ---------------------------------------------------------------------- msousa@2649: # Create a file containing the default configuration paramters. msousa@2649: # Beremiz will then transfer this file to the PLC, where the web server msousa@2649: # will read it to obtain the default configuration parameters. msousa@2649: # ---------------------------------------------------------------------- msousa@2649: # NOTE: This is no loner needed! The web interface will read these msousa@2649: # parameters directly from the compiled C code (.so file) msousa@2649: # msousa@2649: ### extra_file_name = os.path.join(buildpath, "%s_%s.%s" % ('bacnet_extrafile', postfix, 'txt')) msousa@2649: ### extra_file_handle = open(extra_file_name, 'w') msousa@2649: ### msousa@2649: ### proplist = ["network_interface", "port_number", "BACnet_Device_ID", "BACnet_Device_Name", msousa@2649: ### "BACnet_Comm_Control_Password", "BACnet_Device_Location", msousa@2649: ### "BACnet_Device_Description", "BACnet_Device_AppSoft_Version"] msousa@2649: ### for propname in proplist: msousa@2649: ### extra_file_handle.write("%s:%s\n" % (propname, loc_dict[propname])) msousa@2649: ### msousa@2649: ### extra_file_handle.close() msousa@2649: ### extra_file_handle = open(extra_file_name, 'r') msousa@2649: msousa@2649: # Format of data to return: msousa@2649: # [(Cfiles, CFLAGS), ...], LDFLAGS, DoCalls, extra_files msousa@2649: # LDFLAGS = ['flag1', 'flag2', ...] msousa@2649: # DoCalls = true or false msousa@2649: # extra_files = (fname,fobject), ... msousa@2649: # fobject = file object, already open'ed for read() !! msousa@2649: # msousa@2649: # extra_files -> files that will be downloaded to the PLC! Edouard@2669: Edouard@2669: websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') Edouard@2669: websettingcode = websettingfile.read() Edouard@2669: websettingfile.close() Edouard@2669: Edouard@2669: location_str = "_".join(map(str, self.GetCurrentLocation())) Edouard@2669: websettingcode = websettingcode % locals() Edouard@2669: Edouard@2669: runtimefile_path = os.path.join(buildpath, "runtime_bacnet_websettings.py") Edouard@2669: runtimefile = open(runtimefile_path, 'w') Edouard@2669: runtimefile.write(websettingcode) Edouard@2669: runtimefile.close() Edouard@2669: Edouard@2669: return ([(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, Edouard@2703: ("runtime_%s_bacnet_websettings.py" % location_str, open(runtimefile_path, "rb")), Edouard@2669: ) msousa@2649: #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle)