SVGHMI: Fixed typo on session manager unregister, leading to wrong count of sessions and then exceptions when creating more session than allowed in protocol options. Also added more safety check in protocol in case session would be missing.
#!/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
BacnetPath = paths.ThirdPartyPath("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_%s_bacnet_websettings.py" % location_str, open(runtimefile_path, "rb")),
)
#return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle)