BACnet and Modbus: Remove additional loading and unloading, use the one already in place for extensions.
authorEdouard Tisserant
Fri, 12 Jun 2020 10:30:23 +0200
changeset 2669 be233279d179
parent 2668 cca3e5d7d6f3
child 2670 fd348d79a1f3
BACnet and Modbus: Remove additional loading and unloading, use the one already in place for extensions.
Beremiz_service.py
bacnet/bacnet.py
bacnet/web_settings.py
modbus/modbus.py
modbus/web_settings.py
runtime/BACnet_config.py
runtime/Modbus_config.py
runtime/PLCObject.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()
 
--- 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)
--- /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
+
+
+
+
--- 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")),
+        )
--- /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 = []
+
--- 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
--- 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
--- 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):