BACnet and Modbus: Remove additional loading and unloading, use the one already in place for extensions.
--- a/Beremiz_service.py Sun Jun 07 23:47:32 2020 +0100
+++ b/Beremiz_service.py Fri Jun 12 10:30:23 2020 +0200
@@ -492,37 +492,16 @@
installThreadExcepthook()
import runtime.NevowServer as NS # pylint: disable=ungrouped-imports
+ NS.WorkingDir = WorkingDir LogMessageAndException(_("Nevow/Athena import failed :"))
- 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 :"))
- # Try to add support for Modbus configuration via web server interface
- # NOTE:Modbus web config only makes sense if web server is available
- if webport is not None:
- import runtime.Modbus_config as MBconf
- LogMessageAndException(_("Modbus configuration web interface - import failed :"))
import runtime.WampClient as WC # pylint: disable=ungrouped-imports
WC.WorkingDir = WorkingDir
@@ -544,8 +523,6 @@
runtime.CreatePLCObjectSingleton(
WorkingDir, argv, statuschange, evaluator, pyruntimevars)
-plcobj = runtime.GetPLCObjectSingleton()
pyroserver = PyroServer(servicename, interface, port)
@@ -561,18 +538,6 @@
LogMessageAndException(_("Nevow Web service failed. "))
- BNconf.init(plcobj, NS, WorkingDir)
- LogMessageAndException(_("BACnet web configuration failed startup. "))
- MBconf.init(plcobj, NS, WorkingDir)
- LogMessageAndException(_("Modbus web configuration failed startup. "))
@@ -636,6 +601,7 @@
+plcobj = runtime.GetPLCObjectSingleton() --- a/bacnet/bacnet.py Sun Jun 07 23:47:32 2020 +0100
+++ b/bacnet/bacnet.py Fri Jun 12 10:30:23 2020 +0200
@@ -36,6 +36,7 @@
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, "..")
@@ -775,5 +776,20 @@
# 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
+ websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + 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) + 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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bacnet/web_settings.py Fri Jun 12 10:30:23 2020 +0200
@@ -0,0 +1,444 @@
+# 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 +from formless import annotate, webform +import runtime.NevowServer as NS +# Will contain references to the C functions +# (implemented in beremiz/bacnet/runtime/server.c) +# used to get/set the BACnet specific configuration paramters +# 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 +# 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" +class BN_StrippedString(annotate.String): + def __init__(self, *args, **kwargs): + annotate.String.__init__(self, strip = True, *args, **kwargs) + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("network_interface" , _("Network Interface") , ctypes.c_char_p, BN_StrippedString), + ("port_number" , _("UDP Port Number") , ctypes.c_char_p, BN_StrippedString), + ("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 """ + portnum = int(port_number) + if (portnum < 0) or (portnum > 65535): +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 + if (devid < 0) or (devid > 4194302): +def _CheckConfiguration(BACnetConfig): + res = res and _CheckPortnumber(BACnetConfig["port_number"]) + res = res and _CheckDeviceID (BACnetConfig["device_id"]) +def _CheckWebConfiguration(BACnetConfig): + # 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:")) + if not _CheckDeviceID(BACnetConfig["device_id"]): + raise annotate.ValidateError( + {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])}, + _("BACnet configuration error:")) +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 + #if os.path.isfile(_BACnetConfFilename): + saved_config = json.load(open(_BACnetConfFilename)) + if _CheckConfiguration(saved_config): +def _GetPLCConfiguration(): + # Returns a dictionary containing the current BACnet parameter configuration + # stored in the C variables in the loaded PLC (.so file) + for par_name, x1, x2, x3 in BACnet_parameters: + value = GetParamFuncs[par_name]() + current_config[par_name] = value +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] + #PLCObject.LogMessage("BACnet web server extension::_SetPLCConfiguration() Setting " + # + par_name + " to " + str(value) ) + 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 + return _WebviewConfiguration[argument.name] +# 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") + NS.ConfigurableSettings.addSettings( + "BACnetConfigDelSaved", # name + _("BACnet Configuration"), # description + [], # fields (empty, no parameters required!) + _("Delete Configuration Stored in Persistent Storage"), # button label + OnButtonDel, # callback + "BACnetConfigParm") # Add after entry xxxx +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! + #PLCObject.LogMessage("BACnet web server extension::OnButtonSave() Called") + for par_name, x1, x2, x3 in BACnet_parameters: + value = kwargs.get(par_name, None) + newConfig[par_name] = value + global _WebviewConfiguration + _WebviewConfiguration = newConfig + # First check if configuration is OK. + if not _CheckWebConfiguration(newConfig): + # 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! +def OnButtonDel(**kwargs): + # Function called when user clicks 'Delete' button in web interface + # The function will delete the file containing the persistent + _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! +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! +def _runtime_bacnet_websettings_%(location_str)s_init(): + # 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 + #PLCObject.LogMessage("BACnet web server extension::OnLoadPLC() Called...") + if PLCObject.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!! + # 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()) + location = ctypes.c_char_p.in_dll(PLCObject.PLClibraryHandle, "__bacnet_plugin_location") + # Loaded PLC does not have the BACnet plugin => nothing to do + # (i.e. do _not_ configure and make available the BACnet web interface) + # 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(PLCObject.PLClibraryHandle, GetParamFuncName) + GetParamFuncs[name].restype = c_dtype + GetParamFuncs[name].argtypes = None + SetParamFuncs[name] = getattr(PLCObject.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 +def _runtime_bacnet_websettings_%(location_str)s_cleanup(): + # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory + #PLCObject.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") + _WebviewConfiguration = None + _SavedConfiguration = None --- a/modbus/modbus.py Sun Jun 07 23:47:32 2020 +0100
+++ b/modbus/modbus.py Fri Jun 12 10:30:23 2020 +0200
@@ -30,6 +30,7 @@
from modbus.mb_utils import *
from ConfigTreeNode import ConfigTreeNode
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY
+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, "..")
@@ -985,4 +986,18 @@
# LDFLAGS.append(" -lws2_32 ") # on windows we need to load winsock
- return [(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True
+ websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + location_str = "_".join(map(str, self.GetCurrentLocation())) + websettingcode = websettingcode % locals() + runtimefile_path = os.path.join(buildpath, "runtime_modbus_websettings.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(websettingcode) + return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True, + ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modbus/web_settings.py Fri Jun 12 10:30:23 2020 +0200
@@ -0,0 +1,660 @@
+# 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 +############################################################################################## +# This file implements an extension to the web server embedded in the Beremiz_service.py # +# runtime manager (webserver is in runtime/NevowServer.py). # +# The extension implemented in this file allows for runtime configuration # +# of Modbus plugin parameters # +############################################################################################## +from formless import annotate, webform +import runtime.NevowServer as NS +# Directory in which to store the persistent configurations +# Should be a directory that does not get wiped on reboot! +_ModbusConfFiledir = "/tmp" +# List of all Web Extension Setting nodes we are handling. +# configured in the loaded PLC (i.e. the .so file loaded into memory) +# Each entry will be a dictionary. See _AddWebNode() for the details +# of the data structure in each entry. +class MB_StrippedString(annotate.String): + def __init__(self, *args, **kwargs): + annotate.String.__init__(self, strip = True, *args, **kwargs) +class MB_StopBits(annotate.Choice): + def coerce(self, val, configurable): + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) +class MB_Baud(annotate.Choice): + _choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200] + def coerce(self, val, configurable): + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) +class MB_Parity(annotate.Choice): + # For more info on what this class really does, have a look at the code in + # file twisted/nevow/annotate.py + # grab this code from $git clone https://github.com/twisted/nevow/ + # Warning: do _not_ name this variable choice[] without underscore, as that name is + # already used for another similar variable by the underlying class annotate.Choice + _label = ["none", "odd", "even"] + def choice_to_label(self, key): + #PLCObject.LogMessage("Modbus web server extension::choice_to_label() " + str(key)) + return self._label[key] + def coerce(self, val, configurable): + """Coerce a value with the help of an object, which is the object + # Basically, make sure the value the user introduced is valid, and transform + # into something that is valid if necessary or mark it as an error + # (by raising an exception ??). + # We are simply using this functions to transform the input value (a string) + # into an integer. Note that although the available options are all + # integers (0, 1 or 2), even though what is shown on the user interface + # are actually strings, i.e. the labels), these parameters are for some + # reason being parsed as strings, so we need to map them back to an + #PLCObject.LogMessage("Modbus web server extension::coerce " + val ) + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, + choices = self._choices, + stringify = self.choice_to_label, +# Parameters we will need to get from the C code, but that will not be shown +# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii) +# The annotate type entry is basically useless and is completely ignored. +# We kee that entry so that this list can later be correctly merged with the + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("config_name" , _("") , ctypes.c_char_p, annotate.String), + ("addr_type" , _("") , ctypes.c_char_p, annotate.String) +# Parameters we will need to get from the C code, and that _will_ be shown +TCPclient_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString), + ("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer ) +RTUclient_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString), + ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer) +TCPserver_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString), + ("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString), + ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer ) + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # annotate.Integer, ...) + ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString), + ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("slave_id" , _("Slave ID") , ctypes.c_ulonglong, annotate.Integer) +# Dictionary containing List of Web viewable parameters +# Note: the dictionary key must be the same as the string returned by the +# __modbus_get_ClientNode_addr_type() +# __modbus_get_ServerNode_addr_type() +# functions implemented in C (see modbus/mb_runtime.c) +_client_WebParamListDict = {} +_client_WebParamListDict["tcp" ] = TCPclient_parameters +_client_WebParamListDict["rtu" ] = RTUclient_parameters +_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) +_server_WebParamListDict = {} +_server_WebParamListDict["tcp" ] = TCPserver_parameters +_server_WebParamListDict["rtu" ] = RTUslave_parameters +_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) +WebParamListDictDict = {} +WebParamListDictDict['client'] = _client_WebParamListDict +WebParamListDictDict['server'] = _server_WebParamListDict +def _SetSavedConfiguration(WebNode_id, newConfig): + """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """ + # Add the addr_type and node_type to the data that will be saved to file + # This allows us to confirm the saved data contains the correct addr_type + # when loading from file + save_info["addr_type"] = _WebNodeList[WebNode_id]["addr_type"] + save_info["node_type"] = _WebNodeList[WebNode_id]["node_type"] + save_info["config" ] = newConfig + filename = _WebNodeList[WebNode_id]["filename"] + with open(os.path.realpath(filename), 'w') as f: + json.dump(save_info, f, sort_keys=True, indent=4) + _WebNodeList[WebNode_id]["SavedConfiguration"] = newConfig +def _DelSavedConfiguration(WebNode_id): + """ Deletes the file cotaining the persistent Modbus configuration """ + filename = _WebNodeList[WebNode_id]["filename"] + if os.path.exists(filename): +def _GetSavedConfiguration(WebNode_id): + Returns a dictionary containing the Modbus parameter configuration + that was last saved to file. If no file exists, or file contains + wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the + addr_type of the WebNode_id), then return None + filename = _WebNodeList[WebNode_id]["filename"] + #if os.path.isfile(filename): + save_info = json.load(open(filename)) + if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: + if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: + if "config" not in save_info: + saved_config = save_info["config"] + #if _CheckConfiguration(saved_config): +def _GetPLCConfiguration(WebNode_id): + Returns a dictionary containing the current Modbus parameter configuration + stored in the C variables in the loaded PLC (.so file) + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"] + for par_name, x1, x2, x3 in WebParamList: + value = GetParamFuncs[par_name](C_node_id) + current_config[par_name] = value +def _SetPLCConfiguration(WebNode_id, newconfig): + Stores the Modbus parameter configuration into the + the C variables in the loaded PLC (.so file) + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"] + for par_name in newconfig: + value = newconfig[par_name] + SetParamFuncs[par_name](C_node_id, value) +def _GetWebviewConfigurationValue(ctx, WebNode_id, argument): + Callback function, called by the web interface (NevowServer.py) + to fill in the default value of each parameter of the web form + Note that the real callback function is a dynamically created function that + will simply call this function to do the work. It will also pass the WebNode_id + return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] +def _updateWebInterface(WebNode_id): + 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 + config_hash = _WebNodeList[WebNode_id]["config_hash"] + config_name = _WebNodeList[WebNode_id]["config_name"] + # Add a "Delete Saved Configuration" button if there is a saved configuration! + if _WebNodeList[WebNode_id]["SavedConfiguration"] is None: + NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) + def __OnButtonDel(**kwargs): + return OnButtonDel(WebNode_id = WebNode_id, **kwargs) + NS.ConfigurableSettings.addSettings( + "ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + [], # fields (empty, no parameters required!) + _("Delete Configuration Stored in Persistent Storage"), # button label + __OnButtonDel, # callback + "ModbusConfigParm" + config_hash) # Add after entry xxxx +def OnButtonSave(**kwargs): + Function called when user clicks 'Save' button in web interface + The function will configure the Modbus plugin in the PLC with the values + specified in the web interface. However, values must be validated first! + Note that this function does not get called directly. The real callback + function is the dynamic __OnButtonSave() function, which will add the + "WebNode_id" argument, and call this function to do the work. + #PLCObject.LogMessage("Modbus web server extension::OnButtonSave() Called") + WebNode_id = kwargs.get("WebNode_id", None) + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + for par_name, x1, x2, x3 in WebParamList: + value = kwargs.get(par_name, None) + newConfig[par_name] = value + # First check if configuration is OK. + # Note that this is not currently required, as we use drop down choice menus + # for baud, parity and sop bits, so the values should always be correct! + #if not _CheckWebConfiguration(newConfig): + # 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(WebNode_id, newConfig) + # Configure PLC with the current Modbus parameters + _SetPLCConfiguration(WebNode_id, newConfig) + # Update the viewable configuration + # The PLC may have coerced the values on calling _SetPLCConfiguration() + # so we do not set it directly to newConfig + _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id) + # File has just been created => Delete button must be shown on web interface! + _updateWebInterface(WebNode_id) +def OnButtonDel(**kwargs): + Function called when user clicks 'Delete' button in web interface + The function will delete the file containing the persistent + WebNode_id = kwargs.get("WebNode_id", None) + _DelSavedConfiguration(WebNode_id) + # Set the current configuration to the default (hardcoded in C) + new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"] + _SetPLCConfiguration(WebNode_id, new_config) + #Update the webviewconfiguration + _WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config + # Reset SavedConfiguration + _WebNodeList[WebNode_id]["SavedConfiguration"] = None + # File has just been deleted => Delete button on web interface no longer needed! + _updateWebInterface(WebNode_id) +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 + Note that this function does not get called directly. The real callback + function is the dynamic __OnButtonShowCur() function, which will add the + "WebNode_id" argument, and call this function to do the work. + WebNode_id = kwargs.get("WebNode_id", None) + _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id) +def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs): + Load from the compiled code (.so file, aloready loaded into memmory) + the configuration parameters of a specific Modbus plugin node. + This function works with both client and server nodes, depending on the + Get/SetParamFunc dictionaries passed to it (either the client or the server + node versions of the Get/Set functions) + # Get the config_name from the C code... + config_name = GetParamFuncs["config_name"](C_node_id) + # Get the addr_type from the C code... + # addr_type will be one of "tcp", "rtu" or "ascii" + addr_type = GetParamFuncs["addr_type" ](C_node_id) + # For some operations we cannot use the config name (e.g. filename to store config) + # because the user may be using characters that are invalid for that purpose ('/' for + # example), so we create a hash of the config_name, and use that instead. + config_hash = hashlib.md5(config_name).hexdigest() + #PLCObject.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name) + # Add the new entry to the global list + # Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry + # WebNode_entry will be stored as a reference, so we can later insert parameters at will. + _WebNodeList.append(WebNode_entry) + WebNode_id = len(_WebNodeList) - 1 + # store all WebNode relevant data for future reference + # Note that "WebParamList" will reference one of: + # - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters + WebNode_entry["C_node_id" ] = C_node_id + WebNode_entry["config_name" ] = config_name + WebNode_entry["config_hash" ] = config_hash + WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json") + WebNode_entry["GetParamFuncs"] = GetParamFuncs + WebNode_entry["SetParamFuncs"] = SetParamFuncs + WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type] + WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function) + WebNode_entry["node_type" ] = node_type # 'client', 'server' + # Dictionary that contains the Modbus configuration currently being shown + # 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 an edited configuration that contains an error. + WebNode_entry["WebviewConfiguration"] = None + # Upon PLC load, this Dictionary is initialised with the Modbus configuration + # hardcoded in the C file + # (i.e. the configuration inserted in Beremiz IDE when project was compiled) + WebNode_entry["DefaultConfiguration"] = _GetPLCConfiguration(WebNode_id) + WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"] + # Dictionary that stores the Modbus 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) + SavedConfig = _GetSavedConfiguration(WebNode_id) + WebNode_entry["SavedConfiguration"] = SavedConfig + if SavedConfig is not None: + _SetPLCConfiguration(WebNode_id, SavedConfig) + WebNode_entry["WebviewConfiguration"] = SavedConfig + # Define the format for the web form used to show/change the current parameters + # We first declare a dynamic function to work as callback to obtain the default values for each parameter + # Note: We transform every parameter into a string + # This is not strictly required for parameters of type annotate.Integer that will correctly + # accept the default value as an Integer python object + # This is obviously also not required for parameters of type annotate.String, that are + # always handled as strings. + # However, the annotate.Choice parameters (and all parameters that derive from it, + # sucn as Parity, Baud, etc.) require the default value as a string + # even though we store it as an integer, which is the data type expected + # by the set_***() C functions in mb_runtime.c + def __GetWebviewConfigurationValue(ctx, argument): + return str(_GetWebviewConfigurationValue(ctx, WebNode_id, argument)) + webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue)) + for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]] + # Configure the web interface to include the Modbus config parameters + def __OnButtonSave(**kwargs): + OnButtonSave(WebNode_id=WebNode_id, **kwargs) + NS.ConfigurableSettings.addSettings( + "ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + webFormInterface, # fields + _("Save Configuration to Persistent Storage"), # button label + __OnButtonSave) # callback + # Add a "View Current Configuration" button + def __OnButtonShowCur(**kwargs): + OnButtonShowCur(WebNode_id=WebNode_id, **kwargs) + NS.ConfigurableSettings.addSettings( + "ModbusConfigViewCur" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + [], # fields (empty, no parameters required!) + _("Show Current PLC Configuration"), # button label + __OnButtonShowCur) # callback + # Add the Delete button to the web interface, if required + _updateWebInterface(WebNode_id) +def _runtime_modbus_websettings_%(location_str)s_init(): + Callback function, called (by PLCObject.py) when a new PLC program + (i.e. XXX.so file) is transfered to the PLC runtime + print("_runtime_modbus_websettings_init") + #PLCObject.LogMessage("Modbus web server extension::OnLoadPLC() Called...") + if PLCObject.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!! + # Get the number of Modbus Client and Servers (Modbus plugin) + # configured in the currently loaded PLC project (i.e., the .so file) + # If the "__modbus_plugin_client_node_count" + # or the "__modbus_plugin_server_node_count" C variables + # are not present in the .so file we conclude that the currently loaded + # PLC does not have the Modbus plugin included (situation (2b) described above init()) + client_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_client_node_count").value + server_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_server_node_count").value + # Loaded PLC does not have the Modbus plugin => nothing to do + # (i.e. do _not_ configure and make available the Modbus web interface) + if client_count < 0: client_count = 0 + if server_count < 0: server_count = 0 + if (client_count == 0) and (server_count == 0): + # The Modbus plugin in the loaded PLC does not have any client and servers configured + # => nothing to do (i.e. do _not_ configure and make available the Modbus web interface) + # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters + # Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c) + GetClientParamFuncs = {} + SetClientParamFuncs = {} + GetServerParamFuncs = {} + SetServerParamFuncs = {} + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters: + ParamFuncName = "__modbus_get_ClientNode_" + name + GetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + GetClientParamFuncs[name].restype = c_dtype + GetClientParamFuncs[name].argtypes = [ctypes.c_int] + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters: + ParamFuncName = "__modbus_set_ClientNode_" + name + SetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + SetClientParamFuncs[name].restype = None + SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters: + ParamFuncName = "__modbus_get_ServerNode_" + name + GetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + GetServerParamFuncs[name].restype = c_dtype + GetServerParamFuncs[name].argtypes = [ctypes.c_int] + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters: + ParamFuncName = "__modbus_set_ServerNode_" + name + SetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) + SetServerParamFuncs[name].restype = None + SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + for node_id in range(client_count): + _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs) + for node_id in range(server_count): + _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs) +def _runtime_modbus_websettings_%(location_str)s_cleanup(): + Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory + #PLCObject.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...") + # Delete the Modbus specific web interface extensions + # (Safe to ask to delete, even if it has not been added!) + for WebNode_entry in _WebNodeList: + config_hash = WebNode_entry["config_hash"] + NS.ConfigurableSettings.delSettings("ModbusConfigParm" + config_hash) + NS.ConfigurableSettings.delSettings("ModbusConfigViewCur" + config_hash) + NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) --- a/runtime/BACnet_config.py Sun Jun 07 23:47:32 2020 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,490 +0,0 @@
-# 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
-from formless import annotate, webform
-# reference to the PLCObject in runtime/PLCObject.py
-# PLCObject is a singleton, created in runtime/__init__.py
-# 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)
-# WorkingDir: the directory on which Beremiz_service.py is running, and where
-# all the files downloaded to the PLC get stored
-# Will contain references to the C functions
-# (implemented in beremiz/bacnet/runtime/server.c)
-# used to get/set the BACnet specific configuration paramters
-# 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
-# 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"
-class BN_StrippedString(annotate.String):
- def __init__(self, *args, **kwargs):
- annotate.String.__init__(self, strip = True, *args, **kwargs)
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("network_interface" , _("Network Interface") , ctypes.c_char_p, BN_StrippedString),
- ("port_number" , _("UDP Port Number") , ctypes.c_char_p, BN_StrippedString),
- ("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 """
- portnum = int(port_number)
- if (portnum < 0) or (portnum > 65535):
-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
- if (devid < 0) or (devid > 4194302):
-def _CheckConfiguration(BACnetConfig):
- res = res and _CheckPortnumber(BACnetConfig["port_number"])
- res = res and _CheckDeviceID (BACnetConfig["device_id"])
-def _CheckWebConfiguration(BACnetConfig):
- # 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:"))
- if not _CheckDeviceID(BACnetConfig["device_id"]):
- raise annotate.ValidateError(
- {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])},
- _("BACnet configuration error:"))
-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
- #if os.path.isfile(_BACnetConfFilename):
- saved_config = json.load(open(_BACnetConfFilename))
- if _CheckConfiguration(saved_config):
-def _GetPLCConfiguration():
- # Returns a dictionary containing the current BACnet parameter configuration
- # stored in the C variables in the loaded PLC (.so file)
- for par_name, x1, x2, x3 in BACnet_parameters:
- value = GetParamFuncs[par_name]()
- current_config[par_name] = value
-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) )
- 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
- return _WebviewConfiguration[argument.name]
-# 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")
- _NS.ConfigurableSettings.addSettings(
- "BACnetConfigDelSaved", # name
- _("BACnet Configuration"), # description
- [], # fields (empty, no parameters required!)
- _("Delete Configuration Stored in Persistent Storage"), # button label
- OnButtonDel, # callback
- "BACnetConfigParm") # Add after entry xxxx
-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")
- for par_name, x1, x2, x3 in BACnet_parameters:
- value = kwargs.get(par_name, None)
- newConfig[par_name] = value
- global _WebviewConfiguration
- _WebviewConfiguration = newConfig
- # First check if configuration is OK.
- if not _CheckWebConfiguration(newConfig):
- # 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!
-def OnButtonDel(**kwargs):
- # Function called when user clicks 'Delete' button in web interface
- # The function will delete the file containing the persistent
- _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!
-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!
- # 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!!
- # 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())
- location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location")
- # Loaded PLC does not have the BACnet plugin => nothing to do
- # (i.e. do _not_ configure and make available the BACnet web interface)
- # 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
- # 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")
- _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
-# (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
-# we configure the web server interface to not have the BACnet web configuration extension
-# 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")
- _WorkingDir = WorkingDir
- 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/Modbus_config.py Sun Jun 07 23:47:32 2020 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,705 +0,0 @@
-# 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
-##############################################################################################
-# This file implements an extension to the web server embedded in the Beremiz_service.py #
-# runtime manager (webserver is in runtime/NevowServer.py). #
-# The extension implemented in this file allows for runtime configuration #
-# of Modbus plugin parameters #
-##############################################################################################
-from formless import annotate, webform
-# reference to the PLCObject in runtime/PLCObject.py
-# PLCObject is a singleton, created in runtime/__init__.py
-# 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)
-# WorkingDir: the directory on which Beremiz_service.py is running, and where
-# all the files downloaded to the PLC get stored
-# Directory in which to store the persistent configurations
-# Should be a directory that does not get wiped on reboot!
-_ModbusConfFiledir = "/tmp"
-# List of all Web Extension Setting nodes we are handling.
-# configured in the loaded PLC (i.e. the .so file loaded into memory)
-# Each entry will be a dictionary. See _AddWebNode() for the details
-# of the data structure in each entry.
-class MB_StrippedString(annotate.String):
- def __init__(self, *args, **kwargs):
- annotate.String.__init__(self, strip = True, *args, **kwargs)
-class MB_StopBits(annotate.Choice):
- def coerce(self, val, configurable):
- def __init__(self, *args, **kwargs):
- annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
-class MB_Baud(annotate.Choice):
- _choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]
- def coerce(self, val, configurable):
- def __init__(self, *args, **kwargs):
- annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs)
-class MB_Parity(annotate.Choice):
- # For more info on what this class really does, have a look at the code in
- # file twisted/nevow/annotate.py
- # grab this code from $git clone https://github.com/twisted/nevow/
- # Warning: do _not_ name this variable choice[] without underscore, as that name is
- # already used for another similar variable by the underlying class annotate.Choice
- _label = ["none", "odd", "even"]
- def choice_to_label(self, key):
- #_plcobj.LogMessage("Modbus web server extension::choice_to_label() " + str(key))
- return self._label[key]
- def coerce(self, val, configurable):
- """Coerce a value with the help of an object, which is the object
- # Basically, make sure the value the user introduced is valid, and transform
- # into something that is valid if necessary or mark it as an error
- # (by raising an exception ??).
- # We are simply using this functions to transform the input value (a string)
- # into an integer. Note that although the available options are all
- # integers (0, 1 or 2), even though what is shown on the user interface
- # are actually strings, i.e. the labels), these parameters are for some
- # reason being parsed as strings, so we need to map them back to an
- #_plcobj.LogMessage("Modbus web server extension::coerce " + val )
- def __init__(self, *args, **kwargs):
- annotate.Choice.__init__(self,
- choices = self._choices,
- stringify = self.choice_to_label,
-# Parameters we will need to get from the C code, but that will not be shown
-# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii)
-# The annotate type entry is basically useless and is completely ignored.
-# We kee that entry so that this list can later be correctly merged with the
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("config_name" , _("") , ctypes.c_char_p, annotate.String),
- ("addr_type" , _("") , ctypes.c_char_p, annotate.String)
-# Parameters we will need to get from the C code, and that _will_ be shown
-TCPclient_parameters = [
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString),
- ("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString),
- ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer )
-RTUclient_parameters = [
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
- ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
- ("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
- ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
- ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer)
-TCPserver_parameters = [
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString),
- ("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString),
- ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer )
- # param. name label ctype type annotate type
- # (C code var name) (used on web interface) (C data type) (web data type)
- # annotate.Integer, ...)
- ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString),
- ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud ),
- ("parity" , _("Parity") , ctypes.c_int, MB_Parity ),
- ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ),
- ("slave_id" , _("Slave ID") , ctypes.c_ulonglong, annotate.Integer)
-# Dictionary containing List of Web viewable parameters
-# Note: the dictionary key must be the same as the string returned by the
-# __modbus_get_ClientNode_addr_type()
-# __modbus_get_ServerNode_addr_type()
-# functions implemented in C (see modbus/mb_runtime.c)
-_client_WebParamListDict = {}
-_client_WebParamListDict["tcp" ] = TCPclient_parameters
-_client_WebParamListDict["rtu" ] = RTUclient_parameters
-_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
-_server_WebParamListDict = {}
-_server_WebParamListDict["tcp" ] = TCPserver_parameters
-_server_WebParamListDict["rtu" ] = RTUslave_parameters
-_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin)
-WebParamListDictDict = {}
-WebParamListDictDict['client'] = _client_WebParamListDict
-WebParamListDictDict['server'] = _server_WebParamListDict
-def _SetSavedConfiguration(WebNode_id, newConfig):
- """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """
- # Add the addr_type and node_type to the data that will be saved to file
- # This allows us to confirm the saved data contains the correct addr_type
- # when loading from file
- save_info["addr_type"] = _WebNodeList[WebNode_id]["addr_type"]
- save_info["node_type"] = _WebNodeList[WebNode_id]["node_type"]
- save_info["config" ] = newConfig
- filename = _WebNodeList[WebNode_id]["filename"]
- with open(os.path.realpath(filename), 'w') as f:
- json.dump(save_info, f, sort_keys=True, indent=4)
- _WebNodeList[WebNode_id]["SavedConfiguration"] = newConfig
-def _DelSavedConfiguration(WebNode_id):
- """ Deletes the file cotaining the persistent Modbus configuration """
- filename = _WebNodeList[WebNode_id]["filename"]
- if os.path.exists(filename):
-def _GetSavedConfiguration(WebNode_id):
- Returns a dictionary containing the Modbus parameter configuration
- that was last saved to file. If no file exists, or file contains
- wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the
- addr_type of the WebNode_id), then return None
- filename = _WebNodeList[WebNode_id]["filename"]
- #if os.path.isfile(filename):
- save_info = json.load(open(filename))
- if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]:
- if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]:
- if "config" not in save_info:
- saved_config = save_info["config"]
- #if _CheckConfiguration(saved_config):
-def _GetPLCConfiguration(WebNode_id):
- Returns a dictionary containing the current Modbus parameter configuration
- stored in the C variables in the loaded PLC (.so file)
- C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
- WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
- GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"]
- for par_name, x1, x2, x3 in WebParamList:
- value = GetParamFuncs[par_name](C_node_id)
- current_config[par_name] = value
-def _SetPLCConfiguration(WebNode_id, newconfig):
- Stores the Modbus parameter configuration into the
- the C variables in the loaded PLC (.so file)
- C_node_id = _WebNodeList[WebNode_id]["C_node_id"]
- SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"]
- for par_name in newconfig:
- value = newconfig[par_name]
- SetParamFuncs[par_name](C_node_id, value)
-def _GetWebviewConfigurationValue(ctx, WebNode_id, argument):
- Callback function, called by the web interface (NevowServer.py)
- to fill in the default value of each parameter of the web form
- Note that the real callback function is a dynamically created function that
- will simply call this function to do the work. It will also pass the WebNode_id
- return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name]
-def _updateWebInterface(WebNode_id):
- 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
- config_hash = _WebNodeList[WebNode_id]["config_hash"]
- config_name = _WebNodeList[WebNode_id]["config_name"]
- # Add a "Delete Saved Configuration" button if there is a saved configuration!
- if _WebNodeList[WebNode_id]["SavedConfiguration"] is None:
- _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash)
- def __OnButtonDel(**kwargs):
- return OnButtonDel(WebNode_id = WebNode_id, **kwargs)
- _NS.ConfigurableSettings.addSettings(
- "ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...)
- _("Modbus Configuration: ") + config_name, # description (user visible label)
- [], # fields (empty, no parameters required!)
- _("Delete Configuration Stored in Persistent Storage"), # button label
- __OnButtonDel, # callback
- "ModbusConfigParm" + config_hash) # Add after entry xxxx
-def OnButtonSave(**kwargs):
- Function called when user clicks 'Save' button in web interface
- The function will configure the Modbus plugin in the PLC with the values
- specified in the web interface. However, values must be validated first!
- Note that this function does not get called directly. The real callback
- function is the dynamic __OnButtonSave() function, which will add the
- "WebNode_id" argument, and call this function to do the work.
- #_plcobj.LogMessage("Modbus web server extension::OnButtonSave() Called")
- WebNode_id = kwargs.get("WebNode_id", None)
- WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
- for par_name, x1, x2, x3 in WebParamList:
- value = kwargs.get(par_name, None)
- newConfig[par_name] = value
- # First check if configuration is OK.
- # Note that this is not currently required, as we use drop down choice menus
- # for baud, parity and sop bits, so the values should always be correct!
- #if not _CheckWebConfiguration(newConfig):
- # 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(WebNode_id, newConfig)
- # Configure PLC with the current Modbus parameters
- _SetPLCConfiguration(WebNode_id, newConfig)
- # Update the viewable configuration
- # The PLC may have coerced the values on calling _SetPLCConfiguration()
- # so we do not set it directly to newConfig
- _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id)
- # File has just been created => Delete button must be shown on web interface!
- _updateWebInterface(WebNode_id)
-def OnButtonDel(**kwargs):
- Function called when user clicks 'Delete' button in web interface
- The function will delete the file containing the persistent
- WebNode_id = kwargs.get("WebNode_id", None)
- _DelSavedConfiguration(WebNode_id)
- # Set the current configuration to the default (hardcoded in C)
- new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"]
- _SetPLCConfiguration(WebNode_id, new_config)
- #Update the webviewconfiguration
- _WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config
- # Reset SavedConfiguration
- _WebNodeList[WebNode_id]["SavedConfiguration"] = None
- # File has just been deleted => Delete button on web interface no longer needed!
- _updateWebInterface(WebNode_id)
-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
- Note that this function does not get called directly. The real callback
- function is the dynamic __OnButtonShowCur() function, which will add the
- "WebNode_id" argument, and call this function to do the work.
- WebNode_id = kwargs.get("WebNode_id", None)
- _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id)
-def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs):
- Load from the compiled code (.so file, aloready loaded into memmory)
- the configuration parameters of a specific Modbus plugin node.
- This function works with both client and server nodes, depending on the
- Get/SetParamFunc dictionaries passed to it (either the client or the server
- node versions of the Get/Set functions)
- # Get the config_name from the C code...
- config_name = GetParamFuncs["config_name"](C_node_id)
- # Get the addr_type from the C code...
- # addr_type will be one of "tcp", "rtu" or "ascii"
- addr_type = GetParamFuncs["addr_type" ](C_node_id)
- # For some operations we cannot use the config name (e.g. filename to store config)
- # because the user may be using characters that are invalid for that purpose ('/' for
- # example), so we create a hash of the config_name, and use that instead.
- config_hash = hashlib.md5(config_name).hexdigest()
- #_plcobj.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name)
- # Add the new entry to the global list
- # Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry
- # WebNode_entry will be stored as a reference, so we can later insert parameters at will.
- _WebNodeList.append(WebNode_entry)
- WebNode_id = len(_WebNodeList) - 1
- # store all WebNode relevant data for future reference
- # Note that "WebParamList" will reference one of:
- # - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters
- WebNode_entry["C_node_id" ] = C_node_id
- WebNode_entry["config_name" ] = config_name
- WebNode_entry["config_hash" ] = config_hash
- WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json")
- WebNode_entry["GetParamFuncs"] = GetParamFuncs
- WebNode_entry["SetParamFuncs"] = SetParamFuncs
- WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type]
- WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function)
- WebNode_entry["node_type" ] = node_type # 'client', 'server'
- # Dictionary that contains the Modbus configuration currently being shown
- # 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 an edited configuration that contains an error.
- WebNode_entry["WebviewConfiguration"] = None
- # Upon PLC load, this Dictionary is initialised with the Modbus configuration
- # hardcoded in the C file
- # (i.e. the configuration inserted in Beremiz IDE when project was compiled)
- WebNode_entry["DefaultConfiguration"] = _GetPLCConfiguration(WebNode_id)
- WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"]
- # Dictionary that stores the Modbus 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)
- SavedConfig = _GetSavedConfiguration(WebNode_id)
- WebNode_entry["SavedConfiguration"] = SavedConfig
- if SavedConfig is not None:
- _SetPLCConfiguration(WebNode_id, SavedConfig)
- WebNode_entry["WebviewConfiguration"] = SavedConfig
- # Define the format for the web form used to show/change the current parameters
- # We first declare a dynamic function to work as callback to obtain the default values for each parameter
- # Note: We transform every parameter into a string
- # This is not strictly required for parameters of type annotate.Integer that will correctly
- # accept the default value as an Integer python object
- # This is obviously also not required for parameters of type annotate.String, that are
- # always handled as strings.
- # However, the annotate.Choice parameters (and all parameters that derive from it,
- # sucn as Parity, Baud, etc.) require the default value as a string
- # even though we store it as an integer, which is the data type expected
- # by the set_***() C functions in mb_runtime.c
- def __GetWebviewConfigurationValue(ctx, argument):
- return str(_GetWebviewConfigurationValue(ctx, WebNode_id, argument))
- webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue))
- for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]]
- # Configure the web interface to include the Modbus config parameters
- def __OnButtonSave(**kwargs):
- OnButtonSave(WebNode_id=WebNode_id, **kwargs)
- _NS.ConfigurableSettings.addSettings(
- "ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...)
- _("Modbus Configuration: ") + config_name, # description (user visible label)
- webFormInterface, # fields
- _("Save Configuration to Persistent Storage"), # button label
- __OnButtonSave) # callback
- # Add a "View Current Configuration" button
- def __OnButtonShowCur(**kwargs):
- OnButtonShowCur(WebNode_id=WebNode_id, **kwargs)
- _NS.ConfigurableSettings.addSettings(
- "ModbusConfigViewCur" + config_hash, # name (internal, may not contain spaces, ...)
- _("Modbus Configuration: ") + config_name, # description (user visible label)
- [], # fields (empty, no parameters required!)
- _("Show Current PLC Configuration"), # button label
- __OnButtonShowCur) # callback
- # Add the Delete button to the web interface, if required
- _updateWebInterface(WebNode_id)
- Callback function, called (by PLCObject.py) when a new PLC program
- (i.e. XXX.so file) is transfered to the PLC runtime
- #_plcobj.LogMessage("Modbus 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!!
- # Get the number of Modbus Client and Servers (Modbus plugin)
- # configured in the currently loaded PLC project (i.e., the .so file)
- # If the "__modbus_plugin_client_node_count"
- # or the "__modbus_plugin_server_node_count" C variables
- # are not present in the .so file we conclude that the currently loaded
- # PLC does not have the Modbus plugin included (situation (2b) described above init())
- client_count = ctypes.c_int.in_dll(_plcobj.PLClibraryHandle, "__modbus_plugin_client_node_count").value
- server_count = ctypes.c_int.in_dll(_plcobj.PLClibraryHandle, "__modbus_plugin_server_node_count").value
- # Loaded PLC does not have the Modbus plugin => nothing to do
- # (i.e. do _not_ configure and make available the Modbus web interface)
- if client_count < 0: client_count = 0
- if server_count < 0: server_count = 0
- if (client_count == 0) and (server_count == 0):
- # The Modbus plugin in the loaded PLC does not have any client and servers configured
- # => nothing to do (i.e. do _not_ configure and make available the Modbus web interface)
- # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters
- # Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c)
- GetClientParamFuncs = {}
- SetClientParamFuncs = {}
- GetServerParamFuncs = {}
- SetServerParamFuncs = {}
- for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters:
- ParamFuncName = "__modbus_get_ClientNode_" + name
- GetClientParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName)
- GetClientParamFuncs[name].restype = c_dtype
- GetClientParamFuncs[name].argtypes = [ctypes.c_int]
- for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters:
- ParamFuncName = "__modbus_set_ClientNode_" + name
- SetClientParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName)
- SetClientParamFuncs[name].restype = None
- SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
- for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters:
- ParamFuncName = "__modbus_get_ServerNode_" + name
- GetServerParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName)
- GetServerParamFuncs[name].restype = c_dtype
- GetServerParamFuncs[name].argtypes = [ctypes.c_int]
- for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters:
- ParamFuncName = "__modbus_set_ServerNode_" + name
- SetServerParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName)
- SetServerParamFuncs[name].restype = None
- SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype]
- for node_id in range(client_count):
- _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs)
- for node_id in range(server_count):
- _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs)
- Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
- #_plcobj.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...")
- # Delete the Modbus specific web interface extensions
- # (Safe to ask to delete, even if it has not been added!)
- for WebNode_entry in _WebNodeList:
- config_hash = WebNode_entry["config_hash"]
- _NS.ConfigurableSettings.delSettings("ModbusConfigParm" + config_hash)
- _NS.ConfigurableSettings.delSettings("ModbusConfigViewCur" + config_hash)
- _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash)
-# 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
-# (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 Modbus plugin
-# (b) The loaded PLC does not have the Modbus plugin
-# we configure the web server interface to not have the Modbus web configuration extension
-# we configure the web server interface to include the Modbus web configuration extension
-# PS: reference to the pyroserver (i.e., the server object of Beremiz_service.py)
-# (NOTE: PS.plcobj is a reference to 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):
- #PS.plcobj.LogMessage("Modbus web server extension::init(PS, NS, " + WorkingDir + ") Called")
- _WorkingDir = WorkingDir
- _plcobj.RegisterCallbackLoad ("Modbus_Settins_Extension", OnLoadPLC)
- _plcobj.RegisterCallbackUnLoad("Modbus_Settins_Extension", OnUnLoadPLC)
- OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state
--- a/runtime/PLCObject.py Sun Jun 07 23:47:32 2020 +0100
+++ b/runtime/PLCObject.py Fri Jun 12 10:30:23 2020 +0200
@@ -99,9 +99,6 @@
- # 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
@@ -170,22 +167,6 @@
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")
@@ -289,8 +270,6 @@
- for name, callbackFunc in self.LoadCallbacks.items():
@@ -299,8 +278,6 @@
self.PythonRuntimeCleanup()
- for name, callbackFunc in self.UnLoadCallbacks.items():
def _InitPLCStubCalls(self):