bacnet/bacnet.py
author Edouard Tisserant
Wed, 01 Jul 2020 10:36:20 +0200
changeset 2686 703ebf57508a
parent 2669 be233279d179
child 2703 32ffdb32b14e
permissions -rw-r--r--
Modbus and Bacnet websettings : Rename variables and functions to avoid name collisions.

Websettings for modbus and bacnet are now passed to runtime as python files loaded (execfile) at init of PLCObject with the same globals.
Because if this, same names used in previously different modules now colide.
#!/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.
# This files implements the bacnet plugin for Beremiz, adding BACnet server support.
#
# Copyright (c) 2017 Mario de Sousa (msousa@fe.up.pt)
#
# 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, see <http://www.gnu.org/licenses/>.
#
# This code is made available on the understanding that it will not be
# used in safety-critical situations without a full and competent review.

from __future__ import absolute_import

import os
from collections import Counter
from datetime import datetime
import pickle

import wx

from bacnet.BacnetSlaveEditor import *
from bacnet.BacnetSlaveEditor import ObjectProperties
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY
from ConfigTreeNode import ConfigTreeNode
import util.paths as paths

base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
base_folder = os.path.join(base_folder, "..")
BacnetPath  = os.path.join(base_folder, "BACnet")
BacnetLibraryPath = os.path.join(BacnetPath, "lib")
BacnetIncludePath = os.path.join(BacnetPath, "include")
BacnetIncludePortPath = os.path.join(BacnetPath, "ports")
BacnetIncludePortPath = os.path.join(BacnetIncludePortPath, "linux")

# Parameters to be monkey patched in beremiz customizations
BACNET_VENDOR_ID = 9999
BACNET_VENDOR_NAME = "Beremiz.org"
BACNET_DEVICE_MODEL_NAME = "Beremiz PLC"


# Max String Size of BACnet Paramaters
BACNET_PARAM_STRING_SIZE = 64

#
#
#
# S L A V E    D E V I C E              #
#
#
#

# NOTE: Objects of class _BacnetSlavePlug are never instantiated directly.
#       The objects are instead instantiated from class FinalCTNClass
#       FinalCTNClass inherits from: - ConfigTreeNode
#                                    - The tree node plug (in our case _BacnetSlavePlug)
# class _BacnetSlavePlug:


class RootClass(object):
    XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
    <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
      <xsd:element name="BACnetServerNode">
        <xsd:complexType>
          <xsd:attribute name="Network_Interface"      type="xsd:string"  use="optional" default="eth0"/>
          <xsd:attribute name="UDP_Port_Number"                           use="optional" default="47808">
            <xsd:simpleType>
                <xsd:restriction base="xsd:integer">
                    <xsd:minInclusive value="0"/>
                    <xsd:maxInclusive value="65535"/>
                </xsd:restriction>
            </xsd:simpleType>
          </xsd:attribute>
          <xsd:attribute name="BACnet_Communication_Control_Password"
                                                       type="xsd:string"  use="optional" default="Malba Tahan"/>
          <xsd:attribute name="BACnet_Device_ID"                          use="optional" default="0">
            <xsd:simpleType>
                <xsd:restriction base="xsd:integer">
                    <xsd:minInclusive value="0"/>
                    <xsd:maxInclusive value="4194302"/>
                </xsd:restriction>
            </xsd:simpleType>
          </xsd:attribute>
          <xsd:attribute name="BACnet_Device_Name"        type="xsd:string"  use="optional" default="Beremiz device 0"/>
          <xsd:attribute name="BACnet_Device_Location"    type="xsd:string"  use="optional" default=""/>
          <xsd:attribute name="BACnet_Device_Description" type="xsd:string"  use="optional" default="Beremiz device 0"/>
          <xsd:attribute name="BACnet_Device_Application_Software_Version" type="xsd:string"  use="optional" default="1.0"/>
        </xsd:complexType>
      </xsd:element>
    </xsd:schema>
    """
    # NOTE; Add the following code/declaration to the aboce XSD in order to activate the
    #          Override_Parameters_Saved_on_PLC   flag (currenty not in use as it requires further
    #          analysis how the user would interpret this user interface option.
    #        <--- snip --->
    #          <xsd:attribute name="Override_Parameters_Saved_on_PLC" 
    #                                                   type="xsd:boolean" use="optional" default="true"/>
    #        <--- snip --->
    #
    # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID)
    #       so the Device instance ID is limited from 0 to 22^2-1 = 4194303
    #       However, 4194303 is reserved for special use (similar to NULL pointer), so last
    #       valid ID becomes 4194302

    # The class/object that will render the graphical interface to edit the
    #    BacnetSlavePlug's configuration parameters. The object of class BacnetSlaveEditorPlug
    #    will be instantiated by the ConfigTreeNode class.
    #    This BacnetSlaveEditorPlug object can be accessed from _BacnetSlavePlug as
    #    'self._View'
    #    See the following note to understand how this is possible!
    #
    # NOTE: Objects of class _BacnetSlavePlug are never instantiated directly.
    #       The objects are instead instantiated from class FinalCTNClass
    #       FinalCTNClass inherits from: - ConfigTreeNode
    #                                    - The tree node plug (in our case _BacnetSlavePlug)
    #
    #       This means that objects of class _BacnetSlavePlug may safely access all the members
    #       of classes ConfigTreeNode as well as FinalCTNClass (since they are always instantiated
    #       as a FinalCTNClass)
    EditorType = BacnetSlaveEditorPlug

    # The following classes follow the model/viewer design pattern
    #
    # _BacnetSlavePlug       - contains the model (i.e. configuration parameters)
    # BacnetSlaveEditorPlug  - contains the viewer (and editor, so it includes the 'controller' part of the
    #                                    design pattern which in this case is not separated from the viewer)
    #
    # The _BacnetSlavePlug      object is 'permanent', i.e. it exists as long as the beremiz project is open
    # The BacnetSlaveEditorPlug object is 'transient', i.e. it exists only while the editor is visible/open
    #                                                         in the editing panel. It is destoryed whenever
    #                                                         the user closes the corresponding tab in the
    #                                                         editing panel, and a new object is created when
    #                                                         the editor is re-opened.
    #
    # _BacnetSlavePlug contains:  AV_ObjTable, ...
    #                             (these are the objects that actually store the config parameters or 'model'
    #                              and are therefore stored to a file)
    #
    # _BacnetSlavePlug contains:  AV_VarEditor, ...
    #                             (these are the objects that implement a grid table to edit/view the
    #                              corresponding mode parameters)
    #
    #  Logic:
    #    - The xx_VarEditor classes inherit from wx.grid.Grid
    #    - The xx_ObjTable  classes inherit from wx.grid.PyGridTableBase
    #  To be more precise, the inheritance tree is actually:
    #    xx_VarEditor -> ObjectGrid -> CustomGrid   -> wx.grid.Grid
    #    xx_ObjTable  -> ObjectTable -> CustomTable -> wx.grid.PyGridTableBase)
    #
    #  Note that wx.grid.Grid is prepared to work with wx.grid.PyGridTableBase as the container of
    #  data that is displayed and edited in the Grid.

    ConfNodeMethods = [
        {"bitmap": "ExportSlave",
         "name": _("Export slave"),
         "tooltip": _("Export BACnet slave to EDE file"),
         "method": "_ExportBacnetSlave"},
    ]

    def __init__(self):
        # Initialize the dictionary that stores the current configuration for the Analog/Digital/MultiValued Variables
        #   in this BACnet server.
        self.ObjTablesData = {}

        # Each list will contain an entry for each row in the xxxxVar grid!!
        #   Each entry/row will be a dictionary
        #     Each dictionary will contain all entries/data
        # for one row in the grid.

        self.ObjTablesData["AV_Obj"] = []
        self.ObjTablesData["AO_Obj"] = []
        self.ObjTablesData["AI_Obj"] = []
        self.ObjTablesData["BV_Obj"] = []
        self.ObjTablesData["BO_Obj"] = []
        self.ObjTablesData["BI_Obj"] = []
        self.ObjTablesData["MSV_Obj"] = []
        self.ObjTablesData["MSO_Obj"] = []
        self.ObjTablesData["MSI_Obj"] = []

        self.ObjTablesData["EDEfile_parm"] = {"next_EDE_file_version": 1}

        # EDE files inlcude extra parameters (ex. file version)
        # We would like to save the parameters the user configures
        # so they are available the next time the user opens the project.
        # Since this plugin is only storing the ObjTablesData[] dict
        # to file, we add that info to this dictionary too.
        # Yes, I know this is kind of a
        # hack.

        filepath = self.GetFileName()
        if os.path.isfile(filepath):
            self.LoadFromFile(filepath)

        self.ObjTables = {}
        self.ObjTables["AV_Obj"] = ObjectTable(
            self, self.ObjTablesData["AV_Obj"],  AVObject)
        self.ObjTables["AO_Obj"] = ObjectTable(
            self, self.ObjTablesData["AO_Obj"],  AOObject)
        self.ObjTables["AI_Obj"] = ObjectTable(
            self, self.ObjTablesData["AI_Obj"],  AIObject)
        self.ObjTables["BV_Obj"] = ObjectTable(
            self, self.ObjTablesData["BV_Obj"],  BVObject)
        self.ObjTables["BO_Obj"] = ObjectTable(
            self, self.ObjTablesData["BO_Obj"],  BOObject)
        self.ObjTables["BI_Obj"] = ObjectTable(
            self, self.ObjTablesData["BI_Obj"],  BIObject)
        self.ObjTables["MSV_Obj"] = ObjectTable(
            self, self.ObjTablesData["MSV_Obj"], MSVObject)
        self.ObjTables["MSO_Obj"] = ObjectTable(
            self, self.ObjTablesData["MSO_Obj"], MSOObject)
        self.ObjTables["MSI_Obj"] = ObjectTable(
            self, self.ObjTablesData["MSI_Obj"], MSIObject)

    #
    # Functions to be called by CTNClass #
    #
    # The following functions would be somewhat equvalent to virtual functions/methods in C++ classes
    # They will be called by the base class (CTNClass) from which this
    # _BacnetSlavePlug class derives.

    def GetCurrentNodeName(self):
        return self.CTNName()

    def GetFileName(self):
        return os.path.join(self.CTNPath(), 'bacnet_slave')

    def OnCTNSave(self, from_project_path=None):
        return self.SaveToFile(self.GetFileName())

    def CTNTestModified(self):
        # self.ChangesToSave: Check whether any of the parameters, defined in the XSD above, were changed.
        #                     This is handled by the ConfigTreeNode class
        #                     (Remember that no objects are ever instantiated from _BacnetSlavePlug.
        #                      Objects are instead created from FinalCTNClass, which derives from
        #                      _BacnetSlavePlug and ConfigTreeNode. This means that we can exceptionally
        # consider that all objects of type _BacnetSlavePlug will also be a
        # ConfigTreeNode).
        result = self.ChangesToSave \
            or self.ObjTables["AV_Obj"].ChangesToSave \
            or self.ObjTables["AO_Obj"].ChangesToSave \
            or self.ObjTables["AI_Obj"].ChangesToSave \
            or self.ObjTables["BV_Obj"].ChangesToSave \
            or self.ObjTables["BO_Obj"].ChangesToSave \
            or self.ObjTables["BI_Obj"].ChangesToSave \
            or self.ObjTables["MSV_Obj"].ChangesToSave \
            or self.ObjTables["MSO_Obj"].ChangesToSave \
            or self.ObjTables["MSI_Obj"].ChangesToSave
        return result

    # Currently not needed. Override _OpenView() in case we need to do some special stuff whenever the editor is opened!
    # def _OpenView(self, name=None, onlyopened=False):
        # print "_BacnetSlavePlug._OpenView() Called!!!"
        # ConfigTreeNode._OpenView(self, name, onlyopened)
        # print self._View
        # if self._View is not None:
        #     self._View.SetBusId(self.GetCurrentLocation())
        # return self._View

    def GetVariableLocationTree(self):
        current_location = self.GetCurrentLocation()
        # see comment in CTNGenerate_C regarding identical line of code!
        locstr = ".".join(map(str, current_location))

        # IDs used by BACnet to identify object types/class.
        #     OBJECT_ANALOG_INPUT       =  0,
        #     OBJECT_ANALOG_OUTPUT      =  1,
        #     OBJECT_ANALOG_VALUE       =  2,
        #     OBJECT_BINARY_INPUT       =  3,
        #     OBJECT_BINARY_OUTPUT      =  4,
        #     OBJECT_BINARY_VALUE       =  5,
        #     OBJECT_MULTI_STATE_INPUT  = 13,
        #     OBJECT_MULTI_STATE_OUTPUT = 14,
        #     OBJECT_MULTI_STATE_VALUE  = 19,
        #
        #  Since Binary Value, Analog Value, etc. objects may use the same
        # object ID (since they have distinct class IDs), we must also distinguish them in some way in
        # the %MX0.3.4 IEC 61131-3 syntax.
        #
        # For this reason we add the BACnet class identifier to the %MX0.5.3 location.
        # For example, for a BACnet plugin in location '0' of the Beremiz configuration tree,
        #  all      Binary Values will be mapped onto: %MX0.5.xxx    (xxx is object ID)
        #  all Multi State Values will be mapped onto: %MB0.19.xxx   (xxx is object ID)
        #  all      Analog Values will be mapped onto: %MD0.2.xxx    (xxx is object ID)
        #  etc..
        #
        #   Value objects will be mapped onto %M
        #   Input objects will be mapped onto %I
        #  Output objects will be mapped onto %Q

        BACnetEntries = []
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["AV_Obj"], 32, 'REAL', 'D', locstr + '.2', 'Analog Values'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["AO_Obj"], 32, 'REAL', 'D', locstr + '.1', 'Analog Outputs'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["AI_Obj"], 32, 'REAL', 'D', locstr + '.0', 'Analog Inputs'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["BV_Obj"],  1, 'BOOL', 'X', locstr + '.5', 'Binary Values'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["BO_Obj"],  1, 'BOOL', 'X', locstr + '.4', 'Binary Outputs'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["BI_Obj"],  1, 'BOOL', 'X', locstr + '.3', 'Binary Inputs'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["MSV_Obj"],  8, 'BYTE', 'B', locstr + '.19', 'Multi State Values'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["MSO_Obj"],  8, 'BYTE', 'B', locstr + '.14', 'Multi State Outputs'))
        BACnetEntries.append(self.GetSlaveLocationTree(
            self.ObjTablesData["MSI_Obj"],  8, 'BYTE', 'B', locstr + '.13', 'Multi State Inputs'))

        return {"name": self.BaseParams.getName(),
                "type": LOCATION_CONFNODE,
                "location": locstr + ".x",
                "children": BACnetEntries}

    #
    # Helper functions/methods #
    #
    # a helper function to GetVariableLocationTree()
    def GetSlaveLocationTree(self, ObjTablesData, size_in_bits, IECdatatype, location_size, location_str, name):
        BACnetObjectEntries = []
        for xx_ObjProp in ObjTablesData:
            BACnetObjectEntries.append({
                "name": str(xx_ObjProp["Object Identifier"]) + ': ' + xx_ObjProp["Object Name"],
                "type": LOCATION_VAR_MEMORY,  # LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT, or LOCATION_VAR_MEMORY
                "size": size_in_bits,  # 1 or 16
                "IEC_type": IECdatatype,  # 'BOOL', 'WORD', ...
                "var_name": "var_name",  # seems to be ignored??
                "location": location_size + location_str + "." + str(xx_ObjProp["Object Identifier"]),
                "description": "description",  # seems to be ignored?
                "children": []})

        return {"name": name,
                "type": LOCATION_CONFNODE,
                "location": location_str + ".x",
                "children": BACnetObjectEntries}

    # Returns a dictionary with:
    #      keys: names  of BACnet objects
    #     value: number of BACnet objects using this same name
    #            (values larger than 1 indicates an error as BACnet requires unique names)
    def GetObjectNamesCount(self):
        # The dictionary is built by first creating a list containing the names of _ALL_
        # BACnet objects currently configured by the user (using the GUI)
        ObjectNames = []
        ObjectNames.extend(
            self.ObjTables["AV_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["AO_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["AI_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["BV_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["BO_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["BI_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["MSV_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["MSO_Obj"].GetAllValuesByName("Object Name"))
        ObjectNames.extend(
            self.ObjTables["MSI_Obj"].GetAllValuesByName("Object Name"))
        # This list is then transformed into a collections.Counter class
        # Which is then transformed into a dictionary using dict()
        return dict(Counter(ObjectNames))

    # Check whether the current configuration contains BACnet objects configured
    # with the same identical object name  (returns True or False)
    def HasDuplicateObjectNames(self):
        ObjectNamesCount = self.GetObjectNamesCount()
        for ObjName in ObjectNamesCount:
            if ObjectNamesCount[ObjName] > 1:
                return True
        return False

    # Check whether any object ID is used more than once (not valid in BACnet)
    # (returns True or False)
    def HasDuplicateObjectIDs(self):
        res = self.ObjTables["AV_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["AO_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["AI_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["BV_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["BO_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["BI_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["MSV_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["MSO_Obj"].HasDuplicateObjectIDs()
        res = res or self.ObjTables["MSI_Obj"].HasDuplicateObjectIDs()
        return res

    #
    # Methods related to files (saving/loading/exporting) #
    #
    def SaveToFile(self, filepath):
        # Save node data in file
        # The configuration data declared in the XSD string will be saved by the ConfigTreeNode class,
        # so we only need to save the data that is stored in ObjTablesData objects
        # Note that we do not store the ObjTables objects. ObjTables is of a class that
        # contains more stuff we do not need to store. Actually it is a bad idea to store
        # this extra stuff (as we would make the files we generate dependent on the actual
        # version of the wx library we are using!!! Remember that ObjTables evetually
        # derives/inherits from wx.grid.PyGridTableBase). Another reason not to store the whole
        # object is because it is not pickable (i.e. pickle.dump() cannot handle it)!!
        try:
            fd = open(filepath,   "w")
            pickle.dump(self.ObjTablesData, fd)
            fd.close()
            # On successfull save, reset flags to indicate no more changes that
            # need saving
            self.ObjTables["AV_Obj"].ChangesToSave = False
            self.ObjTables["AO_Obj"].ChangesToSave = False
            self.ObjTables["AI_Obj"].ChangesToSave = False
            self.ObjTables["BV_Obj"].ChangesToSave = False
            self.ObjTables["BO_Obj"].ChangesToSave = False
            self.ObjTables["BI_Obj"].ChangesToSave = False
            self.ObjTables["MSV_Obj"].ChangesToSave = False
            self.ObjTables["MSO_Obj"].ChangesToSave = False
            self.ObjTables["MSI_Obj"].ChangesToSave = False
            return True
        except Exception:
            return _("Unable to save to file \"%s\"!") % filepath

    def LoadFromFile(self, filepath):
        # Load the data that is saved in SaveToFile()
        try:
            fd = open(filepath,   "r")
            self.ObjTablesData = pickle.load(fd)
            fd.close()
            return True
        except Exception:
            return _("Unable to load file \"%s\"!") % filepath

    def _ExportBacnetSlave(self):
        dialog = wx.FileDialog(self.GetCTRoot().AppFrame,
                               _("Choose a file"),
                               os.path.expanduser("~"),
                               "%s_EDE.csv" % self.CTNName(),
                               _("EDE files (*_EDE.csv)|*_EDE.csv|All files|*.*"),
                               wx.SAVE | wx.OVERWRITE_PROMPT)
        if dialog.ShowModal() == wx.ID_OK:
            result = self.GenerateEDEFile(dialog.GetPath())
            result = False
            if result:
                self.GetCTRoot().logger.write_error(
                    _("Error: Export slave failed\n"))
        dialog.Destroy()

    def GenerateEDEFile(self, filename):
        template_file_dir = os.path.join(
            os.path.split(__file__)[0], "ede_files")

        # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers()
        # It will be an XML parser object created by
        # GenerateParserFromXSDstring(self.XSD).CreateRoot()
        BACnet_Device_ID = self.BACnetServerNode.getBACnet_Device_ID()

        # The EDE file contains a header that includes general project data (name, author, ...)
        # Instead of asking the user for this data, we get it from the configuration
        # of the Beremiz project itself.
        # We ask the root Config Tree Node for the data...
        ProjProp = {}
        FileProp = {}

        # this should be an object of class ProjectController
        CTN_Root = self.GetCTRoot()

        # this should be an object capable of parsing
        # PLCopen XML files. The parser is created automatically
        # (i.e. using GenerateParserFromXSD() from xmlclass module)
        # using the PLCopen XSD file defining the format of the XML.
        # See the file plcopen/plcopen.py
        Project = CTN_Root.Project
        if Project is not None:
            # getcontentHeader() and getfileHeader() are functions that are conditionally defined in
            # plcopn/plcopen.py    We cannot rely on their existance
            if getattr(Project, "getcontentHeader", None) is not None:
                ProjProp = Project.getcontentHeader()
                # getcontentHeader() returns a dictionary. Available keys are:
                # "projectName", "projectVersion", "modificationDateTime",
                # "organization", "authorName", "language", "pageSize", "scaling"
            if getattr(Project, "getfileHeader", None) is not None:
                FileProp = Project.getfileHeader()
                # getfileHeader() returns a dictionary. Available keys are:
                # "companyName", "companyURL", "productName", "productVersion",
                # "productRelease", "creationDateTime", "contentDescription"

        ProjName = ""
        if "projectName" in ProjProp:
            ProjName = ProjProp["projectName"]
        ProjAuthor = ""
        if "companyName" in FileProp:
            ProjAuthor += "(" + FileProp["companyName"] + ")"
        if "authorName" in ProjProp:
            ProjAuthor = ProjProp["authorName"] + " " + ProjAuthor

        projdata_dict = {}
        projdata_dict["Project Name"] = ProjName
        projdata_dict["Project Author"] = ProjAuthor
        projdata_dict["Current Time"] = datetime.now().strftime(
            '%Y-%m-%d %H:%M:%S')
        projdata_dict["EDE file version"] = self.ObjTablesData[
            "EDEfile_parm"]["next_EDE_file_version"]

        # Next time we generate an EDE file, use another version!
        self.ObjTablesData["EDEfile_parm"]["next_EDE_file_version"] += 1

        AX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \
            ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;;;%(Settable)s;N;;;;%(Unit ID)s;"

        BX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \
            ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;0;0;1;%(Settable)s;N;;;;;"

        MSX_params_format = "%(Object Name)s;" + str(BACnet_Device_ID) + \
            ";%(Object Name)s;%(BACnetObjTypeID)s;%(Object Identifier)s;%(Description)s;1;1;%(Number of States)s;%(Settable)s;N;;;;;"

        Objects_List = []
        for ObjType, params_format in [("AV",  AX_params_format),
                                       ("AO",  AX_params_format),
                                       ("AI",  AX_params_format),
                                       ("BV",  BX_params_format),
                                       ("BO",  BX_params_format),
                                       ("BI",  BX_params_format),
                                       ("MSV", MSX_params_format),
                                       ("MSO", MSX_params_format),
                                       ("MSI", MSX_params_format)]:
            self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties()
            for ObjProp in self.ObjTablesData[ObjType + "_Obj"]:
                Objects_List.append(params_format % ObjProp)

        # Normalize filename
        for extension in ["_EDE.csv", "_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]:
            if filename.lower().endswith(extension.lower()):
                filename = filename[:-len(extension)]

        # EDE_header
        generate_file_name = filename + "_EDE.csv"
        template_file_name = os.path.join(
            template_file_dir, "template_EDE.csv")
        generate_file_content = open(template_file_name).read() % projdata_dict
        generate_file_handle = open(generate_file_name, 'w')
        generate_file_handle  .write(generate_file_content)
        generate_file_handle  .write("\n".join(Objects_List))
        generate_file_handle  .close()

        # templates of remaining files do not need changes. They are simply
        # copied unchanged!
        for extension in ["_ObjTypes.csv", "_StateTexts.csv", "_Units.csv"]:
            generate_file_name = filename + extension
            template_file_name = os.path.join(
                template_file_dir, "template" + extension)
            generate_file_content = open(template_file_name).read()
            generate_file_handle = open(generate_file_name, 'w')
            generate_file_handle  .write(generate_file_content)
            generate_file_handle  .close()



    #
    # Generate the C source code files
    #
    def CTNGenerate_C(self, buildpath, locations):
        # Determine the current location in Beremiz's project configuration
        # tree
        current_location = self.GetCurrentLocation()
        # The current location of this plugin in Beremiz's configuration tree, separated by underscores
        #  NOTE: Since BACnet plugin currently does not use sub-branches in the tree (in other words, this
        #        _BacnetSlavePlug class was actually renamed as the RootClass), the current_location_dots
        #        will actually be a single number (e.g.: 0 or 3 or 6, corresponding to the location
        #        in which the plugin was inserted in the Beremiz configuration tree on Beremiz's left panel).
        locstr = "_".join(map(str, current_location))

        # First check whether all the current parameters (inserted by user in
        # the GUI) are valid...
        if self.HasDuplicateObjectNames():
            self.GetCTRoot().logger.write_warning(
                _("Error: BACnet server '{a1}.x:{a2}' contains objects with duplicate object names.\n").
                format(a1=locstr, a2=self.CTNName()))
            raise Exception(False)
            # TODO: return an error code instead of raising an exception
            # (currently unsupported by Beremiz)

        if self.HasDuplicateObjectIDs():
            self.GetCTRoot().logger.write_warning(
                _("Error: BACnet server '{a1}.x: {a2}' contains objects with duplicate object identifiers.\n").
                format(a1=locstr, a2=self.CTNName()))
            raise Exception(False)
            # TODO: return an error code instead of raising an exception
            # (currently unsupported by Beremiz)

        # -------------------------------------------------------------------------------
        # Create and populate the loc_dict dictionary with all parameters needed to configure
        #  the generated source code (.c and .h files)
        # ----------------------------------------------------------------------

        # 1) Create the dictionary (loc_dict = {})
        loc_dict = {}
        loc_dict["locstr"] = locstr

        # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers()
        # It will be an XML parser object created by
        # GenerateParserFromXSDstring(self.XSD).CreateRoot()
        #
        # Note: Override_Parameters_Saved_on_PLC is converted to an integer by int()
        #       The above flag is not currently in use. It requires further thinking on how the 
        #       user will interpret and interact with this user interface...
        #loc_dict["Override_Parameters_Saved_on_PLC"] = int(self.BACnetServerNode.getOverride_Parameters_Saved_on_PLC())
        loc_dict["network_interface"] = self.BACnetServerNode.getNetwork_Interface()
        loc_dict["port_number"] = self.BACnetServerNode.getUDP_Port_Number()
        loc_dict["BACnet_Device_ID"] = self.BACnetServerNode.getBACnet_Device_ID()
        loc_dict["BACnet_Device_Name"] = self.BACnetServerNode.getBACnet_Device_Name()
        loc_dict["BACnet_Comm_Control_Password"] = self.BACnetServerNode.getBACnet_Communication_Control_Password()
        loc_dict["BACnet_Device_Location"] = self.BACnetServerNode.getBACnet_Device_Location()
        loc_dict["BACnet_Device_Description"] = self.BACnetServerNode.getBACnet_Device_Description()
        loc_dict["BACnet_Device_AppSoft_Version"] = self.BACnetServerNode.getBACnet_Device_Application_Software_Version()
        loc_dict["BACnet_Vendor_ID"] = BACNET_VENDOR_ID
        loc_dict["BACnet_Vendor_Name"] = BACNET_VENDOR_NAME
        loc_dict["BACnet_Model_Name"] = BACNET_DEVICE_MODEL_NAME
        loc_dict["BACnet_Param_String_Size"] = BACNET_PARAM_STRING_SIZE
        

        # 2) Add the data specific to each BACnet object type
        # For each BACnet object type, start off by creating some intermediate helpful lists
        #  a) parameters_list containing the strings that will
        #     be included in the C source code, and which will initialize the struct with the
        #     object (Analog Value, Binary Value, or Multi State Value) parameters
        #  b) locatedvar_list containing the strings that will
        #     declare the memory to store the located variables, as well as the
        #     pointers (required by matiec) that point to that memory.

        # format for delaring IEC 61131-3 variable (and pointer) onto which
        # BACnet object is mapped
        locvar_format = '%(Ctype)s ___%(loc)s_%(Object Identifier)s; ' + \
                        '%(Ctype)s *__%(loc)s_%(Object Identifier)s = &___%(loc)s_%(Object Identifier)s;'

        # format for initializing a ANALOG_VALUE_DESCR struct in C code
        #    also valid for ANALOG_INPUT and ANALOG_OUTPUT
        AX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \
            '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Unit ID)d}'
        # format for initializing a BINARY_VALUE_DESCR struct in C code
        #    also valid for BINARY_INPUT and BINARY_OUTPUT
        BX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \
            '%(Object Identifier)s, "%(Object Name)s", "%(Description)s"}'

        # format for initializing a MULTISTATE_VALUE_DESCR struct in C code
        #    also valid for MULTISTATE_INPUT and MULTISTATE_OUTPUT
        MSX_params_format = '{&___%(loc)s_%(Object Identifier)s, ' + \
            '%(Object Identifier)s, "%(Object Name)s", "%(Description)s", %(Number of States)s}'

        # see the comment in GetVariableLocationTree()
        AV_locstr = 'MD' + locstr + '_2'
        AO_locstr = 'QD' + locstr + '_1'
        AI_locstr = 'ID' + locstr + '_0'
        BV_locstr = 'MX' + locstr + '_5'
        BO_locstr = 'QX' + locstr + '_4'
        BI_locstr = 'IX' + locstr + '_3'
        MSV_locstr = 'MB' + locstr + '_19'
        MSO_locstr = 'QB' + locstr + '_14'
        MSI_locstr = 'IB' + locstr + '_13'

        for ObjType,  ObjLocStr,     params_format in [
                ("AV",  AV_locstr,  AX_params_format),
                ("AO",  AO_locstr,  AX_params_format),
                ("AI",  AI_locstr,  AX_params_format),
                ("BV",  BV_locstr,  BX_params_format),
                ("BO",  BO_locstr,  BX_params_format),
                ("BI",  BI_locstr,  BX_params_format),
                ("MSV", MSV_locstr, MSX_params_format),
                ("MSO", MSO_locstr, MSX_params_format),
                ("MSI", MSI_locstr, MSX_params_format)]:
            parameters_list = []
            locatedvar_list = []
            self.ObjTables[ObjType + "_Obj"].UpdateAllVirtualProperties()
            for ObjProp in self.ObjTablesData[ObjType + "_Obj"]:
                ObjProp["loc"] = ObjLocStr
                parameters_list.append(params_format % ObjProp)
                locatedvar_list.append(locvar_format % ObjProp)
            loc_dict[ObjType + "_count"] = len(parameters_list)
            loc_dict[ObjType + "_param"] = ",\n".join(parameters_list)
            loc_dict[ObjType + "_lvars"] = "\n".join(locatedvar_list)

        # ----------------------------------------------------------------------
        # Create the C source files that implement the BACnet server
        # ----------------------------------------------------------------------

        # Names of the .c files that will be generated, based on a template file with same name
        #   (names without '.c'  --> this will be added later)
        #   main server.c file is handled separately
        Generated_BACnet_c_mainfile = "server"
        Generated_BACnet_c_files = [
            "ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv", "device"]

        # Names of the .h files that will be generated, based on a template file with same name
        #   (names without '.h'  --> this will be added later)
        Generated_BACnet_h_files = [
            "server", "device", "config_bacnet_for_beremiz",
            "ai", "ao", "av", "bi", "bo", "bv", "msi", "mso", "msv"
        ]

        # Generate the files with the source code
        postfix = "_".join(map(str, current_location))
        template_file_dir = os.path.join(
            os.path.split(__file__)[0], "runtime")

        def generate_file(file_name, extension):
            generate_file_name = os.path.join(
                buildpath, "%s_%s.%s" % (file_name, postfix, extension))
            template_file_name = os.path.join(
                template_file_dir, "%s.%s" % (file_name, extension))
            generate_file_content = open(template_file_name).read() % loc_dict
            generate_file_handle = open(generate_file_name, 'w')
            generate_file_handle.write(generate_file_content)
            generate_file_handle.close()

        for file_name in Generated_BACnet_c_files:
            generate_file(file_name, "c")
        for file_name in Generated_BACnet_h_files:
            generate_file(file_name, "h")
        generate_file(Generated_BACnet_c_mainfile, "c")
        Generated_BACnet_c_mainfile_name = \
            os.path.join(buildpath, "%s_%s.%s" %
                         (Generated_BACnet_c_mainfile, postfix, "c"))

        # ----------------------------------------------------------------------
        # Finally, define the compilation and linking commands and flags
        # ----------------------------------------------------------------------

        LDFLAGS = []
        # when using dynamically linked library...
        # LDFLAGS.append(' -lbacnet')
        # LDFLAGS.append(' -L"'+BacnetLibraryPath+'"')
        # LDFLAGS.append(' "-Wl,-rpath,' + BacnetLibraryPath + '"')
        # when using static library:
        LDFLAGS.append(
            ' "' + os.path.join(BacnetLibraryPath, "libbacnet.a") + '"')

        CFLAGS = ' -I"' + BacnetIncludePath + '"'
        CFLAGS += ' -I"' + BacnetIncludePortPath + '"'

        # ----------------------------------------------------------------------
        # Create a file containing the default configuration paramters.
        # Beremiz will then transfer this file to the PLC, where the web server 
        # will read it to obtain the default configuration parameters.
        # ----------------------------------------------------------------------
        # NOTE: This is no loner needed! The web interface will read these 
        # parameters directly from the compiled C code (.so file)
        #
        ### extra_file_name   = os.path.join(buildpath, "%s_%s.%s" % ('bacnet_extrafile', postfix, 'txt'))
        ### extra_file_handle = open(extra_file_name, 'w')
        ### 
        ### proplist = ["network_interface", "port_number", "BACnet_Device_ID", "BACnet_Device_Name", 
        ###             "BACnet_Comm_Control_Password", "BACnet_Device_Location", 
        ###             "BACnet_Device_Description", "BACnet_Device_AppSoft_Version"]
        ### for propname in proplist:
        ###     extra_file_handle.write("%s:%s\n" % (propname, loc_dict[propname]))
        ### 
        ### extra_file_handle.close()
        ### extra_file_handle = open(extra_file_name, 'r')

        # Format of data to return:
        #   [(Cfiles, CFLAGS), ...], LDFLAGS, DoCalls, extra_files
        # LDFLAGS     = ['flag1', 'flag2', ...]
        # DoCalls     = true  or  false
        # extra_files = (fname,fobject), ...
        # fobject     = file object, already open'ed for read() !!
        #
        # extra_files -> files that will be downloaded to the PLC!

        websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r')
        websettingcode = websettingfile.read()
        websettingfile.close()

        location_str = "_".join(map(str, self.GetCurrentLocation()))
        websettingcode = websettingcode % locals()

        runtimefile_path = os.path.join(buildpath, "runtime_bacnet_websettings.py")
        runtimefile = open(runtimefile_path, 'w')
        runtimefile.write(websettingcode)
        runtimefile.close()

        return ([(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True,
                ("runtime_bacnet_websettings_%s.py" % location_str, open(runtimefile_path, "rb")),
        )
        #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle)