--- a/Beremiz_service.py Thu May 28 11:01:42 2020 +0100
+++ b/Beremiz_service.py Thu May 28 11:15:22 2020 +0100
@@ -475,6 +475,8 @@
installThreadExcepthook()
@@ -483,8 +485,17 @@
LogMessageAndException(_("Nevow/Athena import failed :"))
- NS.WorkingDir = WorkingDir
+ NS.WorkingDir = WorkingDir # bug? what happens if import fails? + # Try to add support for BACnet configuration via web server interface + # NOTE:BACnet web config only makes sense if web server is available + if webport is not None: + import runtime.BACnet_config as BNconf + LogMessageAndException(_("BACnet configuration web interface - import failed :")) import runtime.WampClient as WC # pylint: disable=ungrouped-imports
WC.WorkingDir = WorkingDir
@@ -506,6 +517,8 @@
runtime.CreatePLCObjectSingleton(
WorkingDir, argv, statuschange, evaluator, pyruntimevars)
+plcobj = runtime.GetPLCObjectSingleton() pyroserver = PyroServer(servicename, interface, port)
@@ -521,6 +534,12 @@
LogMessageAndException(_("Nevow Web service failed. "))
+ BNconf.init(plcobj, NS, WorkingDir) + LogMessageAndException(_("BACnet web configuration failed startup. ")) @@ -584,7 +603,6 @@
-plcobj = runtime.GetPLCObjectSingleton()
--- a/bacnet/bacnet.py Thu May 28 11:01:42 2020 +0100
+++ b/bacnet/bacnet.py Thu May 28 11:15:22 2020 +0100
@@ -35,11 +35,11 @@
from bacnet.BacnetSlaveEditor import *
from bacnet.BacnetSlaveEditor import ObjectProperties
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY
+from ConfigTreeNode import ConfigTreeNode -base_folder = os.path.split(
- os.path.dirname(os.path.realpath(__file__)))[0]
+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")
+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")
@@ -50,6 +50,10 @@
BACNET_VENDOR_NAME = "Beremiz.org"
BACNET_DEVICE_MODEL_NAME = "Beremiz PLC"
+# Max String Size of BACnet Paramaters +BACNET_PARAM_STRING_SIZE = 64 @@ -97,6 +101,14 @@
+ # 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. + # <xsd:attribute name="Override_Parameters_Saved_on_PLC" + # type="xsd:boolean" use="optional" default="true"/> # 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
@@ -552,8 +564,10 @@
generate_file_handle .write(generate_file_content)
generate_file_handle .close()
- # Generate the source files #
+ # Generate the C source code files def CTNGenerate_C(self, buildpath, locations):
# Determine the current location in Beremiz's project configuration
@@ -596,6 +610,11 @@
# 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()
@@ -607,6 +626,8 @@
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
@@ -726,4 +747,33 @@
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! return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True
+ #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle) --- a/bacnet/runtime/device.h Thu May 28 11:01:42 2020 +0100
+++ b/bacnet/runtime/device.h Thu May 28 11:15:22 2020 +0100
@@ -161,11 +161,12 @@
* Maximum sizes excluding nul terminator .
#define STRLEN_X(minlen, str) (((minlen)>sizeof(str))?(minlen):sizeof(str))
-#define MAX_DEV_NAME_LEN STRLEN_X(32, "%(BACnet_Device_Name)s") /* Device name */
-#define MAX_DEV_LOC_LEN STRLEN_X(64, BACNET_DEVICE_LOCATION) /* Device location */
-#define MAX_DEV_MOD_LEN STRLEN_X(32, BACNET_DEVICE_MODEL_NAME) /* Device model name */
-#define MAX_DEV_VER_LEN STRLEN_X(16, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */
-#define MAX_DEV_DESC_LEN STRLEN_X(64, BACNET_DEVICE_DESCRIPTION) /* Device description */
+#define MAX_DEV_NAME_LEN STRLEN_X(%(BACnet_Param_String_Size)d, "%(BACnet_Device_Name)s") /* Device name */ +#define MAX_DEV_LOC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_LOCATION) /* Device location */ +#define MAX_DEV_MOD_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_MODEL_NAME) /* Device model name */ +#define MAX_DEV_VER_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_APPSOFT_VER) /* Device application software version */ +#define MAX_DEV_DESC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_DESCRIPTION) /* Device description */ /** Structure to define the Object Properties common to all Objects. */
typedef struct commonBacObj_s {
--- a/bacnet/runtime/server.c Thu May 28 11:01:42 2020 +0100
+++ b/bacnet/runtime/server.c Thu May 28 11:15:22 2020 +0100
@@ -52,6 +52,10 @@
/* A utility function used by most (all?) implementations of BACnet Objects */
/* Adds to Prop_List all entries in Prop_List_XX that are not
* PROP_OBJECT_IDENTIFIER, PROP_OBJECT_NAME, PROP_OBJECT_TYPE, PROP_PROPERTY_LIST
@@ -454,17 +458,33 @@
bvlc_bdt_restore_local();
/* Initiliaze the bacnet server 'device' */
Device_Init(server_node->device_name);
+ /* Although the default values for the following properties are hardcoded into + * the respective variable definition+initialization in the C code, + * these values may be potentially changed after compilation but before + * code startup. This is done by the web interface(1), directly loading the .so file, + * and changing the values in the server_node_t variable. + * We must therefore honor those values when we start running the BACnet device server + * (1) Web interface is implemented in runtime/BACnet_config.py + * which works as an extension of the web server in runtime/NevowServer.py + * which in turn is initialised/run by the Beremiz_service.py deamon + Device_Set_Location (server_node->device_location, strlen(server_node->device_location)); + Device_Set_Description (server_node->device_description, strlen(server_node->device_description)); + Device_Set_Application_Software_Version(server_node->device_appsoftware_ver, strlen(server_node->device_appsoftware_ver)); + /* Set the password (max 31 chars) for Device Communication Control request. */ + /* Default in the BACnet stack is hardcoded as "filister" */ + /* (char *) cast is to remove the cast. The function is incorrectly declared/defined in the BACnet stack! */ + /* BACnet stack needs to change demo/handler/h_dcc.c and include/handlers.h */ + handler_dcc_password_set((char *)server_node->comm_control_passwd); pthread_mutex_lock(&init_done_lock);
pthread_cond_signal(&init_done_cond);
pthread_mutex_unlock(&init_done_lock);
- /* Set the password (max 31 chars) for Device Communication Control request. */
- /* Default in the BACnet stack is hardcoded as "filister" */
- /* (char *) cast is to remove the cast. The function is incorrectly declared/defined in the BACnet stack! */
- /* BACnet stack needs to change demo/handler/h_dcc.c and include/handlers.h */
- handler_dcc_password_set((char *)server_node->comm_control_passwd);
/* Set callbacks and configure network interface */
res = Init_Service_Handlers();
@@ -661,3 +681,108 @@
+/**********************************************/ +/** Functions for Beremiz web interface. **/ +/**********************************************/ + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * BACnet plugin also comes with an extension to the web server, through + * which the basic BACnet plugin configuration parameters may be changed + * (basically, only the parameters in server_node_t may be changed) + * The following functions are never called from other C code. They are + * called instead from the python code in runtime/BACnet_config.py, that + * implements the web server extension for configuring BACnet parameters. +/* The location (in the Config. Node Tree of Beremiz IDE) + * of the BACnet plugin. + * This variable is also used by the BACnet web config code to determine + * whether the current loaded PLC includes the BACnet plugin + * (so it should make the BACnet parameter web interface visible to the user). +const char * __bacnet_plugin_location = "%(locstr)s"; +/* NOTE: We could have the python code in runtime/BACnet_config.py + * directly access the server_node_t structure, however + * this would create a tight coupling between these two + * disjoint pieces of code. + * Any change to the server_node_t structure would require the + * python code to be changed accordingly. I have therefore opted + * to cretae get/set functions, one for each parameter. + * NOTE: since the BACnet plugin can only support/allow at most one instance + * of the BACnet plugin in Beremiz (2 or more are not allowed due to + * limitations of the underlying BACnet protocol stack being used), + * a single generic version of each of the following functions would work. + * However, simply for the sake of keeping things general, we create + * a diferent function for each plugin instance (which, as I said, + * will never occur for now). + * The functions being declared are therefoe named: + * __bacnet_0_get_ConfigParam_location + * __bacnet_0_get_ConfigParam_device_name + * where the 0 will be replaced by the location of the BACnet plugin + * in the Beremiz configuration tree (will change depending on where + * the user inserted the BACnet plugin into their project) +/* macro works for all data types */ +#define __bacnet_get_ConfigParam(param_type,param_name) \ +param_type __bacnet_%(locstr)s_get_ConfigParam_##param_name(void) { \ + return server_node.param_name; \ +/* macro only works for char * data types */ +/* Note that the storage space (max string size) reserved for each parameter + * (this storage space is reserved in device.h) + * is set to a minimum of + * %(BACnet_Param_String_Size)d + * which is set to the value of + * BACNET_PARAM_STRING_SIZE +#define __bacnet_set_ConfigParam(param_type,param_name) \ +void __bacnet_%(locstr)s_set_ConfigParam_##param_name(param_type val) { \ + strncpy(server_node.param_name, val, %(BACnet_Param_String_Size)d); \ + server_node.param_name[%(BACnet_Param_String_Size)d - 1] = '\0'; \ +#define __bacnet_ConfigParam_str(param_name) \ +__bacnet_get_ConfigParam(const char*,param_name) \ +__bacnet_set_ConfigParam(const char*,param_name) +__bacnet_ConfigParam_str(location) +__bacnet_ConfigParam_str(network_interface) +__bacnet_ConfigParam_str(port_number) +__bacnet_ConfigParam_str(device_name) +__bacnet_ConfigParam_str(device_location) +__bacnet_ConfigParam_str(device_description) +__bacnet_ConfigParam_str(device_appsoftware_ver) +__bacnet_ConfigParam_str(comm_control_passwd) +__bacnet_get_ConfigParam(uint32_t,device_id) +void __bacnet_%(locstr)s_set_ConfigParam_device_id(uint32_t val) { + server_node.device_id = val; --- a/bacnet/runtime/server.h Thu May 28 11:01:42 2020 +0100
+++ b/bacnet/runtime/server.h Thu May 28 11:15:22 2020 +0100
@@ -33,12 +33,19 @@
- const char *network_interface;
- const char *port_number;
- const char *device_name;
- const char *comm_control_passwd;
+ char location [%(BACnet_Param_String_Size)d]; + char network_interface [%(BACnet_Param_String_Size)d]; + char port_number [%(BACnet_Param_String_Size)d]; + char device_name [%(BACnet_Param_String_Size)d]; + char device_location [%(BACnet_Param_String_Size)d]; + char device_description [%(BACnet_Param_String_Size)d]; + char device_appsoftware_ver[%(BACnet_Param_String_Size)d]; + char comm_control_passwd [%(BACnet_Param_String_Size)d]; +// int override_local_config; // bool flag => +// // true : use these parameter values +// // false: use values stored on local file in PLC uint32_t device_id; // device ID is 22 bits long! uint16_t is not enough!
int init_state; // store how far along the server's initialization has progressed
pthread_t thread_id; // thread handling this server
@@ -49,13 +56,16 @@
/*initialization following all parameters given by user in application*/
static server_node_t server_node = {
- "%(network_interface)s", // interface (NULL => use default (eth0))
- "%(port_number)s", // Port number (NULL => use default)
- "%(BACnet_Device_Name)s", // BACnet server's device (object) name
- "%(BACnet_Comm_Control_Password)s",// BACnet server's device (object) name
- %(BACnet_Device_ID)s // BACnet server's device (object) ID
+ "%(network_interface)s", // interface (NULL => use default (eth0)) + "%(port_number)s", // Port number (NULL => use default) + "%(BACnet_Device_Name)s", // BACnet server's device (object) Name + "%(BACnet_Device_Location)s", // BACnet server's device (object) Location + "%(BACnet_Device_Description)s", // BACnet server's device (object) Description + "%(BACnet_Device_AppSoft_Version)s", // BACnet server's device (object) App. Software Ver. + "%(BACnet_Comm_Control_Password)s", // BACnet server's device (object) Password +// (Override_Parameters_Saved_on_PLC)d, // override locally saved parameters (bool flag) + %(BACnet_Device_ID)s // BACnet server's device (object) ID
--- a/runtime/NevowServer.py Thu May 28 11:01:42 2020 +0100
+++ b/runtime/NevowServer.py Thu May 28 11:15:22 2020 +0100
@@ -179,7 +179,13 @@
setattr(self, 'action_' + name, callback)
- self.bindingsNames.append(name)
+ if name not in self.bindingsNames: + self.bindingsNames.append(name) + def delSettings(self, name): + if name in self.bindingsNames: + self.bindingsNames.remove(name) ConfigurableSettings = ConfigurableBindings()
--- a/runtime/PLCObject.py Thu May 28 11:01:42 2020 +0100
+++ b/runtime/PLCObject.py Thu May 28 11:15:22 2020 +0100
@@ -99,6 +99,9 @@
+ # Callbacks used by web settings extensions (e.g.: BACnet_config.py, Modbus_config.py) + self.LoadCallbacks = {} # list of functions to call when PLC is loaded + self.UnLoadCallbacks = {} # list of functions to call when PLC is unloaded @@ -167,6 +170,22 @@
return self._loading_error, 0, 0, 0
+ def RegisterCallbackLoad(self, ExtensionName, ExtensionCallback): + Register function to be called when PLC is loaded + ExtensionName: a string with the name of the extension asking to register the callback + ExtensionCallback: the function to be called... + self.LoadCallbacks[ExtensionName] = ExtensionCallback + def RegisterCallbackUnLoad(self, ExtensionName, ExtensionCallback): + Register function to be called when PLC is unloaded + ExtensionName: a string with the name of the extension asking to register the callback + ExtensionCallback: the function to be called... + self.UnLoadCallbacks[ExtensionName] = ExtensionCallback def _GetMD5FileName(self):
return os.path.join(self.workingdir, "lasttransferedPLC.md5")
@@ -270,6 +289,8 @@
+ for name, callbackFunc in self.LoadCallbacks.items(): @@ -278,6 +299,8 @@
self.PythonRuntimeCleanup()
+ for name, callbackFunc in self.UnLoadCallbacks.items(): def _InitPLCStubCalls(self):