# HG changeset patch # User Edouard Tisserant # Date 1591950623 -7200 # Node ID be233279d17986539300627d39e830f4911721b5 # Parent cca3e5d7d6f39f04209559d8fa09859a24cdbeaa BACnet and Modbus: Remove additional loading and unloading, use the one already in place for extensions. diff -r cca3e5d7d6f3 -r be233279d179 Beremiz_service.py --- 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() havewamp = False -haveBNconf = False -haveMBconf = False - if havetwisted: if webport is not None: try: import runtime.NevowServer as NS # pylint: disable=ungrouped-imports + NS.WorkingDir = WorkingDir except Exception: LogMessageAndException(_("Nevow/Athena import failed :")) webport = None - 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 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: - try: - import runtime.Modbus_config as MBconf - haveMBconf = True - except Exception: - LogMessageAndException(_("Modbus configuration web interface - import failed :")) - + try: 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) if havewx: @@ -561,18 +538,6 @@ except Exception: LogMessageAndException(_("Nevow Web service failed. ")) - if haveBNconf: - try: - BNconf.init(plcobj, NS, WorkingDir) - except Exception: - LogMessageAndException(_("BACnet web configuration failed startup. ")) - - if haveMBconf: - try: - MBconf.init(plcobj, NS, WorkingDir) - except Exception: - LogMessageAndException(_("Modbus web configuration failed startup. ")) - if havewamp: try: WC.SetServer(pyroserver) @@ -636,6 +601,7 @@ pyroserver.Quit() pyro_thread.join() +plcobj = runtime.GetPLCObjectSingleton() plcobj.StopPLC() plcobj.UnLoadPLC() diff -r cca3e5d7d6f3 -r be233279d179 bacnet/bacnet.py --- 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() + websettingfile.close() + + location_str = "_".join(map(str, self.GetCurrentLocation())) + websettingcode = websettingcode % locals() + + runtimefile_path = os.path.join(buildpath, "runtime_bacnet_websettings.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(websettingcode) + runtimefile.close() + + return ([(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, + ("runtime_bacnet_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), + ) #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle) diff -r cca3e5d7d6f3 -r be233279d179 bacnet/web_settings.py --- /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 @@ +#!/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 + +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 +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" + + + + +class BN_StrippedString(annotate.String): + def __init__(self, *args, **kwargs): + annotate.String.__init__(self, strip = True, *args, **kwargs) + + + +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, 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 """ + 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] + #PLCObject.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 + "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") + + 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 _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!! + 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(PLCObject.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(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 + _updateWebInterface() + + + + + +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") + GetParamFuncs = {} + SetParamFuncs = {} + _WebviewConfiguration = None + _SavedConfiguration = None + + + + diff -r cca3e5d7d6f3 -r be233279d179 modbus/modbus.py --- 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 # library! - return [(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True + websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r') + websettingcode = websettingfile.read() + websettingfile.close() + + location_str = "_".join(map(str, self.GetCurrentLocation())) + websettingcode = websettingcode % locals() + + runtimefile_path = os.path.join(buildpath, "runtime_modbus_websettings.py") + runtimefile = open(runtimefile_path, 'w') + runtimefile.write(websettingcode) + runtimefile.close() + + return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True, + ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")), + ) diff -r cca3e5d7d6f3 -r be233279d179 modbus/web_settings.py --- /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 @@ +#!/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 + + + + +############################################################################################## +# 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 # +############################################################################################## + + + +import json +import os +import ctypes +import string +import hashlib + +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! +# TODO FIXME WTF +_ModbusConfFiledir = "/tmp" + +# List of all Web Extension Setting nodes we are handling. +# One WebNode each for: +# - Modbus TCP client +# - Modbus TCP server +# - Modbus RTU client +# - Modbus RTU slave +# 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. +_WebNodeList = [] + + + + +class MB_StrippedString(annotate.String): + def __init__(self, *args, **kwargs): + annotate.String.__init__(self, strip = True, *args, **kwargs) + + +class MB_StopBits(annotate.Choice): + _choices = [0, 1, 2] + + def coerce(self, val, configurable): + return int(val) + 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): + return int(val) + 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 + _choices = [ 0, 1, 2 ] + _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 + we are configuring. + """ + # 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 + # integer. + # + #PLCObject.LogMessage("Modbus web server extension::coerce " + val ) + return int(val) + + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, + choices = self._choices, + stringify = self.choice_to_label, + *args, **kwargs) + + + +# 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 +# following lists... +General_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, ...) + ("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 +# on the web interface. +TCPclient_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, ...) + ("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.String, + # 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.String, + # 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 ) + ] + +RTUslave_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, ...) + ("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 = {} + 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): + os.remove(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"] + try: + #if os.path.isfile(filename): + save_info = json.load(open(filename)) + except Exception: + return None + + if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: + return None + if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: + return None + if "config" not in save_info: + return None + + saved_config = save_info["config"] + + #if _CheckConfiguration(saved_config): + # return saved_config + #else: + # return None + + return 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) + """ + current_config = {} + 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) + if value is not None: + current_config[par_name] = value + + return current_config + + + +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] + if value is not None: + 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 + as a parameter. + """ + try: + return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] + except Exception: + return "" + + + + +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) + else: + 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") + + newConfig = {} + 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) + if value is not 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): + # 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(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 + Modbus configution + """ + + WebNode_id = kwargs.get("WebNode_id", None) + + # Delete the file + _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) + """ + WebNode_entry = {} + + # 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. + global _WebNodeList + _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 + # 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 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 + and loaded into memory + """ + 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!! + return + + # 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()) + try: + 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 + except Exception: + # Loaded PLC does not have the Modbus plugin => nothing to do + # (i.e. do _not_ configure and make available the Modbus web interface) + return + + 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) + return + + # 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!) + global _WebNodeList + 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) + + # Dele all entries... + _WebNodeList = [] + diff -r cca3e5d7d6f3 -r be233279d179 runtime/BACnet_config.py --- 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 @@ -#!/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" - - - - -class BN_StrippedString(annotate.String): - def __init__(self, *args, **kwargs): - annotate.String.__init__(self, strip = True, *args, **kwargs) - - - -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, 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 """ - 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 - "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") - - 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 diff -r cca3e5d7d6f3 -r be233279d179 runtime/Modbus_config.py --- 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 @@ -#!/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 - - - - -############################################################################################## -# 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 # -############################################################################################## - - - -import json -import os -import ctypes -import string -import hashlib - -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 - -# 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. -# One WebNode each for: -# - Modbus TCP client -# - Modbus TCP server -# - Modbus RTU client -# - Modbus RTU slave -# 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. -_WebNodeList = [] - - - - -class MB_StrippedString(annotate.String): - def __init__(self, *args, **kwargs): - annotate.String.__init__(self, strip = True, *args, **kwargs) - - -class MB_StopBits(annotate.Choice): - _choices = [0, 1, 2] - - def coerce(self, val, configurable): - return int(val) - 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): - return int(val) - 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 - _choices = [ 0, 1, 2 ] - _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 - we are configuring. - """ - # 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 - # integer. - # - #_plcobj.LogMessage("Modbus web server extension::coerce " + val ) - return int(val) - - def __init__(self, *args, **kwargs): - annotate.Choice.__init__(self, - choices = self._choices, - stringify = self.choice_to_label, - *args, **kwargs) - - - -# 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 -# following lists... -General_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, ...) - ("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 -# on the web interface. -TCPclient_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, ...) - ("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.String, - # 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.String, - # 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 ) - ] - -RTUslave_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, ...) - ("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 = {} - 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): - os.remove(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"] - try: - #if os.path.isfile(filename): - save_info = json.load(open(filename)) - except Exception: - return None - - if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: - return None - if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: - return None - if "config" not in save_info: - return None - - saved_config = save_info["config"] - - #if _CheckConfiguration(saved_config): - # return saved_config - #else: - # return None - - return 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) - """ - current_config = {} - 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) - if value is not None: - current_config[par_name] = value - - return current_config - - - -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] - if value is not None: - 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 - as a parameter. - """ - try: - return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] - except Exception: - return "" - - - - -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) - else: - 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") - - newConfig = {} - 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) - if value is not 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): - # 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(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 - Modbus configution - """ - - WebNode_id = kwargs.get("WebNode_id", None) - - # Delete the file - _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) - """ - WebNode_entry = {} - - # 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. - global _WebNodeList - _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 - # 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 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 OnLoadPLC(): - """ - Callback function, called (by PLCObject.py) when a new PLC program - (i.e. XXX.so file) is transfered to the PLC runtime - and loaded into memory - """ - - #_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!! - return - - # 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()) - try: - 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 - except Exception: - # Loaded PLC does not have the Modbus plugin => nothing to do - # (i.e. do _not_ configure and make available the Modbus web interface) - return - - 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) - return - - # 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) - - - - - -def OnUnLoadPLC(): - """ - 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!) - global _WebNodeList - 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) - - # Dele all entries... - _WebNodeList = [] - - - -# 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 Modbus plugin -# (b) The loaded PLC does not have the Modbus plugin -# -# During (1) and (2a): -# we configure the web server interface to not have the Modbus web configuration extension -# During (2b) -# 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") - global _WorkingDir - _WorkingDir = WorkingDir - global _plcobj - _plcobj = plcobj - global _NS - _NS = NS - - _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 diff -r cca3e5d7d6f3 -r be233279d179 runtime/PLCObject.py --- 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 @@ 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() @@ -170,22 +167,6 @@ 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") @@ -289,8 +270,6 @@ res = self._LoadPLC() if res: self.PythonRuntimeInit() - for name, callbackFunc in self.LoadCallbacks.items(): - callbackFunc() else: self._FreePLC() @@ -299,8 +278,6 @@ @RunInMain def UnLoadPLC(self): self.PythonRuntimeCleanup() - for name, callbackFunc in self.UnLoadCallbacks.items(): - callbackFunc() self._FreePLC() def _InitPLCStubCalls(self):