--- 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()
havewamp = False
+haveBNconf = False
+
if havetwisted:
if webport is not None:
@@ -483,8 +485,17 @@
except Exception:
LogMessageAndException(_("Nevow/Athena import failed :"))
webport = None
- 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:
+ try:
+ import runtime.BACnet_config as BNconf
+ haveBNconf = True
+ except Exception:
+ LogMessageAndException(_("BACnet configuration web interface - import failed :"))
+
try:
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)
if havewx:
@@ -521,6 +534,12 @@
except Exception:
LogMessageAndException(_("Nevow Web service failed. "))
+ if haveBNconf:
+ try:
+ BNconf.init(plcobj, NS, WorkingDir)
+ except Exception:
+ LogMessageAndException(_("BACnet web configuration failed startup. "))
+
if havewamp:
try:
WC.SetServer(pyroserver)
@@ -584,7 +603,6 @@
pyroserver.Quit()
pyro_thread.join()
-plcobj = runtime.GetPLCObjectSingleton()
plcobj.StopPLC()
plcobj.UnLoadPLC()
--- 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
-
-base_folder = os.path.split(
- os.path.dirname(os.path.realpath(__file__)))[0]
+from ConfigTreeNode import ConfigTreeNode
+
+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 @@
</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
@@ -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 @@
#include "timesync.h"
+
+
+
+
/* 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);
-
- pthread_mutex_lock(&init_done_lock);
- init_done = 1;
- pthread_cond_signal(&init_done_cond);
- pthread_mutex_unlock(&init_done_lock);
-
+
+ /* 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);
+ init_done = 1;
+ pthread_cond_signal(&init_done_cond);
+ pthread_mutex_unlock(&init_done_lock);
+
/* Set callbacks and configure network interface */
res = Init_Service_Handlers();
if (res < 0) exit(1);
@@ -661,3 +681,108 @@
return res;
}
+
+
+
+
+/**********************************************/
+/** 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
+ * etc...
+ * 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
+ * in bacnet.py
+ */
+#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 @@
+
typedef struct{
- const char *location;
- 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 = {
"%(locstr)s",
- "%(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
};
-
#endif /* SERVER_H_ */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/BACnet_config.py Thu May 28 11:15:22 2020 +0100
@@ -0,0 +1,486 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This file is part of Beremiz runtime.
+#
+# Copyright (C) 2020: Mario de Sousa
+#
+# See COPYING.Runtime file for copyrights details.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+import json
+import os
+import ctypes
+
+from formless import annotate, webform
+
+
+
+# reference to the PLCObject in runtime/PLCObject.py
+# PLCObject is a singleton, created in runtime/__init__.py
+_plcobj = None
+
+# reference to the Nevow web server (a.k.a as NS in Beremiz_service.py)
+# (Note that NS will reference the NevowServer.py _module_, and not an object/class)
+_NS = None
+
+
+# WorkingDir: the directory on which Beremiz_service.py is running, and where
+# all the files downloaded to the PLC get stored
+_WorkingDir = None
+
+
+# Will contain references to the C functions
+# (implemented in beremiz/bacnet/runtime/server.c)
+# used to get/set the BACnet specific configuration paramters
+GetParamFuncs = {}
+SetParamFuncs = {}
+
+
+# Upon PLC load, this Dictionary is initialised with the BACnet configuration
+# hardcoded in the C file
+# (i.e. the configuration inserted in Beremiz IDE when project was compiled)
+_DefaultConfiguration = None
+
+
+# Dictionary that contains the BACnet configuration currently being shown
+# on the web interface
+# This configuration will almost always be identical to the current
+# configuration in the PLC (i.e., the current state stored in the
+# C variables in the .so file).
+# The configuration viewed on the web will only be different to the current
+# configuration when the user edits the configuration, and when
+# the user asks to save the edited configuration but it contains an error.
+_WebviewConfiguration = None
+
+
+# Dictionary that stores the BACnet configuration currently stored in a file
+# Currently only used to decide whether or not to show the "Delete" button on the
+# web interface (only shown if _SavedConfiguration is not None)
+_SavedConfiguration = None
+
+
+# File to which the new BACnet configuration gets stored on the PLC
+# Note that the stored configuration is likely different to the
+# configuration hardcoded in C generated code (.so file), so
+# this file should be persistent across PLC reboots so we can
+# re-configure the PLC (change values of variables in .so file)
+# before it gets a chance to start running
+#
+#_BACnetConfFilename = None
+_BACnetConfFilename = "/tmp/BeremizBACnetConfig.json"
+
+
+
+
+
+
+BACnet_parameters = [
+ # param. name label ctype type annotate type
+ # (C code var name) (used on web interface) (C data type) (web data type)
+ # (annotate.String,
+ # annotate.Integer, ...)
+ ("network_interface" , _("Network Interface") , ctypes.c_char_p, annotate.String),
+ ("port_number" , _("UDP Port Number") , ctypes.c_char_p, annotate.String),
+ ("comm_control_passwd" , _("BACnet Communication Control Password") , ctypes.c_char_p, annotate.String),
+ ("device_id" , _("BACnet Device ID") , ctypes.c_int, annotate.Integer),
+ ("device_name" , _("BACnet Device Name") , ctypes.c_char_p, annotate.String),
+ ("device_location" , _("BACnet Device Location") , ctypes.c_char_p, annotate.String),
+ ("device_description" , _("BACnet Device Description") , ctypes.c_char_p, annotate.String),
+ ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String)
+ ]
+
+
+
+
+
+
+def _CheckPortnumber(port_number):
+ """ check validity of the port number """
+ try:
+ portnum = int(port_number)
+ if (portnum < 0) or (portnum > 65535):
+ raise Exception
+ except Exception:
+ return False
+
+ return True
+
+
+
+def _CheckDeviceID(device_id):
+ """
+ # check validity of the Device ID
+ # 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
+ """
+ try:
+ devid = int(device_id)
+ if (devid < 0) or (devid > 4194302):
+ raise Exception
+ except Exception:
+ return False
+
+ return True
+
+
+
+
+
+def _CheckConfiguration(BACnetConfig):
+ res = True
+ res = res and _CheckPortnumber(BACnetConfig["port_number"])
+ res = res and _CheckDeviceID (BACnetConfig["device_id"])
+ return res
+
+
+
+def _CheckWebConfiguration(BACnetConfig):
+ res = True
+
+ # check the port number
+ if not _CheckPortnumber(BACnetConfig["port_number"]):
+ raise annotate.ValidateError(
+ {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])},
+ _("BACnet configuration error:"))
+ res = False
+
+ if not _CheckDeviceID(BACnetConfig["device_id"]):
+ raise annotate.ValidateError(
+ {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])},
+ _("BACnet configuration error:"))
+ res = False
+
+ return res
+
+
+
+
+
+
+def _SetSavedConfiguration(BACnetConfig):
+ """ Stores in a file a dictionary containing the BACnet parameter configuration """
+ with open(os.path.realpath(_BACnetConfFilename), 'w') as f:
+ json.dump(BACnetConfig, f, sort_keys=True, indent=4)
+ global _SavedConfiguration
+ _SavedConfiguration = BACnetConfig
+
+
+def _DelSavedConfiguration():
+ """ Deletes the file cotaining the persistent BACnet configuration """
+ if os.path.exists(_BACnetConfFilename):
+ os.remove(_BACnetConfFilename)
+
+
+def _GetSavedConfiguration():
+ """
+ # Returns a dictionary containing the BACnet parameter configuration
+ # that was last saved to file. If no file exists, then return None
+ """
+ try:
+ #if os.path.isfile(_BACnetConfFilename):
+ saved_config = json.load(open(_BACnetConfFilename))
+ except Exception:
+ return None
+
+ if _CheckConfiguration(saved_config):
+ return saved_config
+ else:
+ return None
+
+
+def _GetPLCConfiguration():
+ """
+ # Returns a dictionary containing the current BACnet parameter configuration
+ # stored in the C variables in the loaded PLC (.so file)
+ """
+ current_config = {}
+ for par_name, x1, x2, x3 in BACnet_parameters:
+ value = GetParamFuncs[par_name]()
+ if value is not None:
+ current_config[par_name] = value
+
+ return current_config
+
+
+def _SetPLCConfiguration(BACnetConfig):
+ """
+ # Stores the BACnet parameter configuration into the
+ # the C variables in the loaded PLC (.so file)
+ """
+ for par_name in BACnetConfig:
+ value = BACnetConfig[par_name]
+ #_plcobj.LogMessage("BACnet web server extension::_SetPLCConfiguration() Setting "
+ # + par_name + " to " + str(value) )
+ if value is not None:
+ SetParamFuncs[par_name](value)
+ # update the configuration shown on the web interface
+ global _WebviewConfiguration
+ _WebviewConfiguration = _GetPLCConfiguration()
+
+
+
+def _GetWebviewConfigurationValue(ctx, argument):
+ """
+ # Callback function, called by the web interface (NevowServer.py)
+ # to fill in the default value of each parameter
+ """
+ try:
+ return _WebviewConfiguration[argument.name]
+ except Exception:
+ return ""
+
+
+# The configuration of the web form used to see/edit the BACnet parameters
+webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue))
+ for name, web_label, c_dtype, web_dtype in BACnet_parameters]
+
+
+
+def _updateWebInterface():
+ """
+ # Add/Remove buttons to/from the web interface depending on the current state
+ #
+ # - If there is a saved state => add a delete saved state button
+ """
+
+ # Add a "Delete Saved Configuration" button if there is a saved configuration!
+ if _SavedConfiguration is None:
+ _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved")
+ else:
+ _NS.ConfigurableSettings.addSettings(
+ "BACnetConfigDelSaved", # name
+ _("BACnet Configuration"), # description
+ [], # fields (empty, no parameters required!)
+ _("Delete Configuration Stored in Persistent Storage"), # button label
+ OnButtonDel) # callback
+
+
+
+def OnButtonSave(**kwargs):
+ """
+ # Function called when user clicks 'Save' button in web interface
+ # The function will configure the BACnet plugin in the PLC with the values
+ # specified in the web interface. However, values must be validated first!
+ """
+
+ #_plcobj.LogMessage("BACnet web server extension::OnButtonSave() Called")
+
+ newConfig = {}
+ for par_name, x1, x2, x3 in BACnet_parameters:
+ value = kwargs.get(par_name, None)
+ if value is not None:
+ newConfig[par_name] = value
+
+ global _WebviewConfiguration
+ _WebviewConfiguration = newConfig
+
+ # First check if configuration is OK.
+ if not _CheckWebConfiguration(newConfig):
+ return
+
+ # store to file the new configuration so that
+ # we can recoup the configuration the next time the PLC
+ # has a cold start (i.e. when Beremiz_service.py is retarted)
+ _SetSavedConfiguration(newConfig)
+
+ # Configure PLC with the current BACnet parameters
+ _SetPLCConfiguration(newConfig)
+
+ # File has just been created => Delete button must be shown on web interface!
+ _updateWebInterface()
+
+
+
+
+def OnButtonDel(**kwargs):
+ """
+ # Function called when user clicks 'Delete' button in web interface
+ # The function will delete the file containing the persistent
+ # BACnet configution
+ """
+
+ # Delete the file
+ _DelSavedConfiguration()
+ # Set the current configuration to the default (hardcoded in C)
+ _SetPLCConfiguration(_DefaultConfiguration)
+ # Reset global variable
+ global _SavedConfiguration
+ _SavedConfiguration = None
+ # File has just been deleted => Delete button on web interface no longer needed!
+ _updateWebInterface()
+
+
+
+def OnButtonShowCur(**kwargs):
+ """
+ # Function called when user clicks 'Show Current PLC Configuration' button in web interface
+ # The function will load the current PLC configuration into the web form
+ """
+
+ global _WebviewConfiguration
+ _WebviewConfiguration = _GetPLCConfiguration()
+ # File has just been deleted => Delete button on web interface no longer needed!
+ _updateWebInterface()
+
+
+
+
+def OnLoadPLC():
+ """
+ # Callback function, called (by PLCObject.py) when a new PLC program
+ # (i.e. XXX.so file) is transfered to the PLC runtime
+ # and oaded into memory
+ """
+
+ #_plcobj.LogMessage("BACnet web server extension::OnLoadPLC() Called...")
+
+ if _plcobj.PLClibraryHandle is None:
+ # PLC was loaded but we don't have access to the library of compiled code (.so lib)?
+ # Hmm... This shold never occur!!
+ return
+
+ # Get the location (in the Config. Node Tree of Beremiz IDE) the BACnet plugin
+ # occupies in the currently loaded PLC project (i.e., the .so file)
+ # If the "__bacnet_plugin_location" C variable is not present in the .so file,
+ # we conclude that the currently loaded PLC does not have the BACnet plugin
+ # included (situation (2b) described above init())
+ try:
+ location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location")
+ except Exception:
+ # Loaded PLC does not have the BACnet plugin => nothing to do
+ # (i.e. do _not_ configure and make available the BACnet web interface)
+ return
+
+ # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
+ for name, web_label, c_dtype, web_dtype in BACnet_parameters:
+ GetParamFuncName = "__bacnet_" + location.value + "_get_ConfigParam_" + name
+ SetParamFuncName = "__bacnet_" + location.value + "_set_ConfigParam_" + name
+
+ GetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, GetParamFuncName)
+ GetParamFuncs[name].restype = c_dtype
+ GetParamFuncs[name].argtypes = None
+
+ SetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, SetParamFuncName)
+ SetParamFuncs[name].restype = None
+ SetParamFuncs[name].argtypes = [c_dtype]
+
+ # Default configuration is the configuration done in Beremiz IDE
+ # whose parameters get hardcoded into C, and compiled into the .so file
+ # We read the default configuration from the .so file before the values
+ # get changed by the user using the web server, or by the call (further on)
+ # to _SetPLCConfiguration(SavedConfiguration)
+ global _DefaultConfiguration
+ _DefaultConfiguration = _GetPLCConfiguration()
+
+ # Show the current PLC configuration on the web interface
+ global _WebviewConfiguration
+ _WebviewConfiguration = _GetPLCConfiguration()
+
+ # Read from file the last used configuration, which is likely
+ # different to the hardcoded configuration.
+ # We Reset the current configuration (i.e., the config stored in the
+ # variables of .so file) to this saved configuration
+ # so the PLC will start off with this saved configuration instead
+ # of the hardcoded (in Beremiz C generated code) configuration values.
+ #
+ # Note that _SetPLCConfiguration() will also update
+ # _WebviewConfiguration , if necessary.
+ global _SavedConfiguration
+ _SavedConfiguration = _GetSavedConfiguration()
+ if _SavedConfiguration is not None:
+ if _CheckConfiguration(_SavedConfiguration):
+ _SetPLCConfiguration(_SavedConfiguration)
+
+ # Configure the web interface to include the BACnet config parameters
+ _NS.ConfigurableSettings.addSettings(
+ "BACnetConfigParm", # name
+ _("BACnet Configuration"), # description
+ webFormInterface, # fields
+ _("Save Configuration to Persistent Storage"), # button label
+ OnButtonSave) # callback
+
+ # Add a "View Current Configuration" button
+ _NS.ConfigurableSettings.addSettings(
+ "BACnetConfigViewCur", # name
+ _("BACnet Configuration"), # description
+ [], # fields (empty, no parameters required!)
+ _("Show Current PLC Configuration"), # button label
+ OnButtonShowCur) # callback
+
+ # Add the Delete button to the web interface, if required
+ _updateWebInterface()
+
+
+
+
+
+def OnUnLoadPLC():
+ """
+ # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
+ """
+
+ #_plcobj.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...")
+
+ # Delete the BACnet specific web interface extensions
+ # (Safe to ask to delete, even if it has not been added!)
+ _NS.ConfigurableSettings.delSettings("BACnetConfigParm")
+ _NS.ConfigurableSettings.delSettings("BACnetConfigViewCur")
+ _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved")
+ GetParamFuncs = {}
+ SetParamFuncs = {}
+ _WebviewConfiguration = None
+ _SavedConfiguration = None
+
+
+
+
+# The Beremiz_service.py service, along with the integrated web server it launches
+# (i.e. Nevow web server, in runtime/NevowServer.py), will go through several states
+# once started:
+# (1) Web server is started, but no PLC is loaded
+# (2) PLC is loaded (i.e. the PLC compiled code is loaded)
+# (a) The loaded PLC includes the BACnet plugin
+# (b) The loaded PLC does not have the BACnet plugin
+#
+# During (1) and (2a):
+# we configure the web server interface to not have the BACnet web configuration extension
+# During (2b)
+# we configure the web server interface to include the BACnet web configuration extension
+#
+# plcobj : reference to the PLCObject defined in PLCObject.py
+# NS : reference to the web server (i.e. the NevowServer.py module)
+# WorkingDir: the directory on which Beremiz_service.py is running, and where
+# all the files downloaded to the PLC get stored, including
+# the .so file with the compiled C generated code
+def init(plcobj, NS, WorkingDir):
+ #plcobj.LogMessage("BACnet web server extension::init(plcobj, NS, " + WorkingDir + ") Called")
+ global _WorkingDir
+ _WorkingDir = WorkingDir
+ global _plcobj
+ _plcobj = plcobj
+ global _NS
+ _NS = NS
+ global _BACnetConfFilename
+ if _BACnetConfFilename is None:
+ _BACnetConfFilename = os.path.join(WorkingDir, "BACnetConfig.json")
+
+ _plcobj.RegisterCallbackLoad ("BACnet_Settins_Extension", OnLoadPLC)
+ _plcobj.RegisterCallbackUnLoad("BACnet_Settins_Extension", OnUnLoadPLC)
+ OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state
--- 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 @@
self.TraceLock = Lock()
self.Traces = []
self.DebugToken = 0
+ # 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
self._init_blobs()
@@ -167,6 +170,22 @@
return self._loading_error, 0, 0, 0
return None
+ 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 @@
res = self._LoadPLC()
if res:
self.PythonRuntimeInit()
+ for name, callbackFunc in self.LoadCallbacks.items():
+ callbackFunc()
else:
self._FreePLC()
@@ -278,6 +299,8 @@
@RunInMain
def UnLoadPLC(self):
self.PythonRuntimeCleanup()
+ for name, callbackFunc in self.UnLoadCallbacks.items():
+ callbackFunc()
self._FreePLC()
def _InitPLCStubCalls(self):