# HG changeset patch # User Edouard Tisserant # Date 1591965572 -7200 # Node ID fd348d79a1f3e547b8b0fa5684d32f6326978d60 # Parent be233279d17986539300627d39e830f4911721b5 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. diff -r be233279d179 -r fd348d79a1f3 bacnet/web_settings.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 diff -r be233279d179 -r fd348d79a1f3 modbus/web_settings.py --- 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 = [] diff -r be233279d179 -r fd348d79a1f3 runtime/NevowServer.py --- 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):