BACnet and Modbus : Simpler configuration management. NevowServer.py now allows each extension to create and delete multiple configuration forms in the setting page, deprecating delSettings and addAfter.
authorEdouard Tisserant
Fri, 12 Jun 2020 14:39:32 +0200
changeset 2670 fd348d79a1f3
parent 2669 be233279d179
child 2671 30493ff3a23a
BACnet and Modbus : Simpler configuration management. NevowServer.py now allows each extension to create and delete multiple configuration forms in the setting page, deprecating delSettings and addAfter.
bacnet/web_settings.py
modbus/web_settings.py
runtime/NevowServer.py
--- a/bacnet/web_settings.py	Fri Jun 12 10:30:23 2020 +0200
+++ b/bacnet/web_settings.py	Fri Jun 12 14:39:32 2020 +0200
@@ -243,27 +243,6 @@
                     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
@@ -279,8 +258,6 @@
         if value is not None:
             newConfig[par_name] = value
 
-    global _WebviewConfiguration
-    _WebviewConfiguration = newConfig
     
     # First check if configuration is OK.
     if not _CheckWebConfiguration(newConfig):
@@ -294,13 +271,9 @@
     # 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):
+
+
+def OnButtonReset(**kwargs):
     """
     # Function called when user clicks 'Delete' button in web interface
     # The function will delete the file containing the persistent
@@ -314,25 +287,10 @@
     # 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()
-
-
-
-
+
+
+
+# location_str is replaced by extension's value in CTNGenerateC call
 def _runtime_bacnet_websettings_%(location_str)s_init():
     """
     # Callback function, called (by PLCObject.py) when a new PLC program
@@ -346,23 +304,12 @@
         # 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
+        # location_str is replaced by extension's value in CTNGenerateC call
+        GetParamFuncName = "__bacnet_%(location_str)s_get_ConfigParam_" + name
+        SetParamFuncName = "__bacnet_%(location_str)s_set_ConfigParam_" + name
         
         GetParamFuncs[name]          = getattr(PLCObject.PLClibraryHandle, GetParamFuncName)
         GetParamFuncs[name].restype  = c_dtype
@@ -399,29 +346,37 @@
         if _CheckConfiguration(_SavedConfiguration):
             _SetPLCConfiguration(_SavedConfiguration)
             
+    WebSettings = NS.newExtensionSetting("BACnet")
+
     # Configure the web interface to include the BACnet config parameters
-    NS.ConfigurableSettings.addSettings(
+    WebSettings.addSettings(
         "BACnetConfigParm",                # name
         _("BACnet Configuration"),         # description
         webFormInterface,                  # fields
-        _("Save Configuration to Persistent Storage"),  # button label
+        _("Apply"),  # button label
         OnButtonSave)                      # callback    
-    
-    # Add a "View Current Configuration" button 
-    NS.ConfigurableSettings.addSettings(
-        "BACnetConfigViewCur",                    # name
+
+    # Add the Delete button to the web interface
+    WebSettings.addSettings(
+        "BACnetConfigDelSaved",                   # 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()
-
-
-
-
-
+        [ ("status",
+           annotate.String(label=_("Current state"),
+                           immutable=True,
+                           default=lambda *k:getConfigStatus())),
+        ],                                       # fields  (empty, no parameters required!)
+        _("Reset"), # button label
+        OnButtonReset) 
+
+
+
+def getConfigStatus():
+    if _WebviewConfiguration == _DefaultConfiguration :
+        return "Unchanged"
+    return "Modified"
+
+
+# location_str is replaced by extension's value in CTNGenerateC call
 def _runtime_bacnet_websettings_%(location_str)s_cleanup():
     """
     # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
@@ -429,11 +384,8 @@
 
     #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")  
+    NS.removeExtensionSetting("BACnet")
+    
     GetParamFuncs = {}
     SetParamFuncs = {}
     _WebviewConfiguration = None
--- a/modbus/web_settings.py	Fri Jun 12 10:30:23 2020 +0200
+++ b/modbus/web_settings.py	Fri Jun 12 14:39:32 2020 +0200
@@ -328,33 +328,6 @@
 
 
 
-
-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
@@ -396,13 +369,9 @@
     # 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):
+
+
+def OnButtonReset(**kwargs):
     """
     Function called when user clicks 'Delete' button in web interface
     The function will delete the file containing the persistent
@@ -424,25 +393,7 @@
     # 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)
-    
+
 
 
 
@@ -537,26 +488,33 @@
     def __OnButtonSave(**kwargs):
         OnButtonSave(WebNode_id=WebNode_id, **kwargs)
 
-    NS.ConfigurableSettings.addSettings(
+    WebSettings = NS.newExtensionSetting("Modbus "+config_hash)
+
+    WebSettings.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
+        _("Apply"), # 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 __OnButtonReset(**kwargs):
+        return OnButtonReset(WebNode_id = WebNode_id, **kwargs)
+            
+    def getConfigStatus():
+        if WebNode_entry["WebviewConfiguration"] == WebNode_entry["DefaultConfiguration"]:
+            return "Unchanged"
+        return "Modified"
+
+    WebSettings.addSettings(
+        "ModbusConfigDelSaved"      + config_hash,  # name (internal, may not contain spaces, ...)
+        _("Modbus Configuration: ") + config_name,  # description (user visible label)
+        [ ("status",
+           annotate.String(label=_("Current state"),
+                           immutable=True,
+                           default=lambda *k:getConfigStatus())),
+        ],                                       # fields  (empty, no parameters required!)
+        _("Reset"), # button label
+        __OnButtonReset)
 
 
 
@@ -567,7 +525,6 @@
     (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...")
 
@@ -651,9 +608,7 @@
     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)  
+        NS.removeExtensionSetting("Modbus "+config_hash)
         
     # Dele all entries...
     _WebNodeList = []
--- a/runtime/NevowServer.py	Fri Jun 12 10:30:23 2020 +0200
+++ b/runtime/NevowServer.py	Fri Jun 12 14:39:32 2020 +0200
@@ -26,6 +26,7 @@
 from __future__ import absolute_import
 from __future__ import print_function
 import os
+import collections
 import platform as platform_module
 from zope.interface import implements
 from nevow import appserver, inevow, tags, loaders, athena, url, rend
@@ -165,8 +166,7 @@
         setattr(self, 'bind_' + name, _bind)
         self.bindingsNames.append(name)
 
-    def addSettings(self, name, desc, fields, btnlabel, callback, 
-                    addAfterName = None):
+    def addSettings(self, name, desc, fields, btnlabel, callback):
         def _bind(ctx):
             return annotate.MethodBinding(
                 'action_' + name,
@@ -180,32 +180,20 @@
 
         setattr(self, 'action_' + name, callback)
 
-        if addAfterName not in self.bindingsNames:
-            # Just append new setting if not yet present
-            if name not in self.bindingsNames:
-                self.bindingsNames.append(name)
-        else:
-            # We need to insert new setting 
-            # imediately _after_ addAfterName
+        self.bindingsNames.append(name)
             
-            # First remove new setting if already present
-            # to make sure it goes into correct place
-            if name in self.bindingsNames:
-                self.bindingsNames.remove(name)
-            # Now add new setting in correct place
-            self.bindingsNames.insert(
-                self.bindingsNames.index(addAfterName)+1,
-                name)
-            
-
-
-    def delSettings(self, name):
-        if name in self.bindingsNames:
-            self.bindingsNames.remove(name)
-
 
 ConfigurableSettings = ConfigurableBindings()
 
+def newExtensionSetting(ext_name):
+    global extensions_settings_od
+    settings = ConfigurableBindings()
+    extensions_settings_od[ext_name] = settings
+    return settings
+
+def removeExtensionSetting(ext_name):
+    global extensions_settings_od
+    extensions_settings_od.pop(ext_name)
 
 class ISettings(annotate.TypedInterface):
     platform = annotate.String(label=_("Platform"),
@@ -233,6 +221,7 @@
 customSettingsURLs = {
 }
 
+extensions_settings_od = collections.OrderedDict()
 
 class SettingsPage(rend.Page):
     # We deserve a slash
@@ -243,6 +232,25 @@
     child_webinterface_css = File(paths.AbsNeighbourFile(__file__, 'webinterface.css'), 'text/css')
 
     implements(ISettings)
+   
+    def __getattr__(self, name):
+        global extensions_settings_od
+        if name.startswith('configurable_'):
+            ext_name = name[13:]
+            def configurable_something(ctx):
+                return extensions_settings_od[ext_name]
+            return configurable_something
+        raise AttributeError
+    
+    def extensions_settings(self, context, data):
+        """ Project extensions settings
+        Extensions added to Configuration Tree in IDE have their setting rendered here
+        """
+        global extensions_settings_od
+        res = []
+        for ext_name in extensions_settings_od:
+            res += [tags.h2[ext_name], webform.renderForms(ext_name)] 
+        return res
 
     docFactory = loaders.stan([tags.html[
         tags.head[
@@ -260,12 +268,16 @@
             webform.renderForms('staticSettings'),
             tags.h1["Extensions settings:"],
             webform.renderForms('dynamicSettings'),
+            extensions_settings
         ]]])
 
     def configurable_staticSettings(self, ctx):
         return configurable.TypedInterfaceConfigurable(self)
 
     def configurable_dynamicSettings(self, ctx):
+        """ Runtime Extensions settings
+        Extensions loaded through Beremiz_service -e or optional runtime features render setting forms here
+        """
         return ConfigurableSettings
 
     def sendLogMessage(self, level, message, **kwargs):