Modbus web configuration: add check for valid delay and period parameters
authorMario de Sousa <msousa@fe.up.pt>
Tue, 07 Mar 2023 09:00:33 +0000
changeset 3743 5450dd9e9370
parent 3733 d1acf20e8e7c
child 3744 65969628e920
Modbus web configuration: add check for valid delay and period parameters
modbus/web_settings.py
--- a/modbus/web_settings.py	Sun Feb 19 08:37:27 2023 +0000
+++ b/modbus/web_settings.py	Tue Mar 07 09:00:33 2023 +0000
@@ -125,67 +125,86 @@
 
 
 
+# The parameter coerce functions referenced in the TCPclient_parameters etc. lists.
+# These functions check wether the parameter has a legal value (i.e. they validate the parameter),
+# and if not changes it to the closest legal value (i.e. coerces the parameter value to a legal value)
+
+def _coerce_comm_period(value):
+    # Period may not be negative value
+    if (value < 0):
+        value = 0
+    return value
+
+def _coerce_req_delay(value):
+    # Request delay may not be negative value
+    # (i.e. delay between any two consecutive requests sent by a Beremiz RTU master or TCP client)
+    if (value < 0):
+        value = 0
+    return value
+
+
+
 # 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...
+# The annotate type entry is used to tell the web server the type of data entry widget to use.
+# (see the file twisted/nevow/formless/annotate.py for list of options)
+# Unfortunately an unsigned integer option does not exist.
 General_parameters = [
-    #    param. name       label                        ctype type         annotate type
-    # (C code var name)   (used on web interface)      (C data type)       (web data type)
+    #    param. name       label                        ctype type         annotate type            Coerce
+    # (C code var name)   (used on web interface)      (C data type)       (web data type)          function
     #                                                                      (annotate.String,
     #                                                                       annotate.Integer, ...)
-    ("config_name"      , _("")                      , ctypes.c_char_p,    annotate.String),
-    ("addr_type"        , _("")                      , ctypes.c_char_p,    annotate.String)
+    ("config_name"      , _("")                      , ctypes.c_char_p,    annotate.String,         None),
+    ("addr_type"        , _("")                      , ctypes.c_char_p,    annotate.String,         None)
     ]                                                                      
                                                                            
 # 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)
+    #    param. name       label                        ctype type         annotate type            Coerce
+    # (C code var name)   (used on web interface)      (C data type)       (web data type)          function
     #                                                                      (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 ),
-    ("req_delay"        , _("Request Delay (ms)")    , ctypes.c_ulonglong, annotate.Integer )
+    ("host"             , _("Remote IP Address")     , ctypes.c_char_p,    MB_StrippedString,         None),
+    ("port"             , _("Remote Port Number")    , ctypes.c_char_p,    MB_StrippedString,         None),
+    ("comm_period"      , _("Invocation Rate (ms)")  , ctypes.c_ulonglong, annotate.Integer ,         _coerce_comm_period),
+    ("req_delay"        , _("Request Delay (ms)")    , ctypes.c_ulonglong, annotate.Integer ,         _coerce_req_delay)
     ]
 
 RTUclient_parameters = [                                                   
-    #    param. name       label                        ctype type         annotate type
-    # (C code var name)   (used on web interface)      (C data type)       (web data type)
+    #    param. name       label                        ctype type         annotate type            Coerce
+    # (C code var name)   (used on web interface)      (C data type)       (web data type)          function
     #                                                                      (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),
-    ("req_delay"        , _("Request Delay (ms)")    , ctypes.c_ulonglong, annotate.Integer)
+    ("device"           , _("Serial Port")           , ctypes.c_char_p,    MB_StrippedString,        None),
+    ("baud"             , _("Baud Rate")             , ctypes.c_int,       MB_Baud         ,         None),
+    ("parity"           , _("Parity")                , ctypes.c_int,       MB_Parity       ,         None),
+    ("stop_bits"        , _("Stop Bits")             , ctypes.c_int,       MB_StopBits     ,         None),
+    ("comm_period"      , _("Invocation Rate (ms)")  , ctypes.c_ulonglong, annotate.Integer,         _coerce_comm_period),
+    ("req_delay"        , _("Request Delay (ms)")    , ctypes.c_ulonglong, annotate.Integer,         _coerce_req_delay)
     ]
 
 TCPserver_parameters = [                                                   
-    #    param. name       label                        ctype type         annotate type
-    # (C code var name)   (used on web interface)      (C data type)       (web data type)
+    #    param. name       label                        ctype type         annotate type            Coerce
+    # (C code var name)   (used on web interface)      (C data type)       (web data type)          function
     #                                                                      (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 )
+    ("host"             , _("Local IP Address")      , ctypes.c_char_p,    MB_StrippedString,         None),
+    ("port"             , _("Local Port Number")     , ctypes.c_char_p,    MB_StrippedString,         None),
+    ("slave_id"         , _("Slave ID")              , ctypes.c_ubyte,     annotate.Integer ,         None)
     ]
 
 RTUslave_parameters = [                                                   
-    #    param. name       label                        ctype type         annotate type
-    # (C code var name)   (used on web interface)      (C data type)       (web data type)
+    #    param. name       label                        ctype type         annotate type            Coerce
+    # (C code var name)   (used on web interface)      (C data type)       (web data type)          function
     #                                                                      (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_ubyte,     annotate.Integer)
+    ("device"           , _("Serial Port")           , ctypes.c_char_p,    MB_StrippedString,        None),
+    ("baud"             , _("Baud Rate")             , ctypes.c_int,       MB_Baud         ,         None),
+    ("parity"           , _("Parity")                , ctypes.c_int,       MB_Parity       ,         None),
+    ("stop_bits"        , _("Stop Bits")             , ctypes.c_int,       MB_StopBits     ,         None),
+    ("slave_id"         , _("Slave ID")              , ctypes.c_ubyte,     annotate.Integer,         None)
     ]
 
 
@@ -213,8 +232,6 @@
 
 
 
-
-
 def _SetModbusSavedConfiguration(WebNode_id, newConfig):
     """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """
     WebNode_entry = _WebNodeList[WebNode_id]
@@ -296,7 +313,8 @@
     WebParamList   = _WebNodeList[WebNode_id]["WebParamList"]
     GetParamFuncs  = _WebNodeList[WebNode_id]["GetParamFuncs"]
 
-    for par_name, x1, x2, x3 in WebParamList:
+    for WebParamListEntry in WebParamList:
+        par_name = WebParamListEntry[0]
         value = GetParamFuncs[par_name](C_node_id)
         if value is not None:
             current_config[par_name] = value
@@ -336,6 +354,20 @@
         return ""
 
 
+def _CoerceConfigurationValues(WebNode_id, config):
+    """
+    Coerces any incorrect value in the passed configuration
+    to the nearest correct value.
+    For example, negative times in delay paramater are changed to 0, etc...
+    """
+    CoerceParamFuncs  = _WebNodeList[WebNode_id]["CoerceParamFuncs"]
+
+    for par_name in config:
+        CoerceFunction = CoerceParamFuncs[par_name]
+        if (config[par_name] is not None) and (CoerceFunction is not None):
+            config[par_name] = CoerceFunction(config[par_name])
+
+
 
 def OnModbusButtonSave(**kwargs):
     """
@@ -354,16 +386,30 @@
     WebNode_id   =  kwargs.get("WebNode_id", None)
     WebParamList = _WebNodeList[WebNode_id]["WebParamList"]
     
-    for par_name, x1, x2, x3 in WebParamList:
+    for WebParamListEntry in WebParamList:
+        par_name = WebParamListEntry[0]
         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
+    # Note that this is currently not really required for most of the parameters,
+    # as we use drop down choice menus for baud, parity and sop bits
+    # (so these values should always be correct!).
+    # The timing properties (period in ms, delays in ms, etc.)
+    # do however need to be validated as the web interface does not
+    # enforce any limits on the values (e.g., they must be positive values).
+    # NOTE: It would be confusing for the user if we simply refuse to make the
+    # requested changes without giving any feedback that the changes were not applied,
+    # and why they were refused (i.e. at the moment the web page does not have
+    # any pop-up windows or special messages for that).
+    # That is why instead of returning without making the changes in case of error,
+    # we instead opt to change the requested configuration to the closest valid values
+    # and apply that instead. The user gets feedback on the applied values as these get
+    # shown on the web interface.
+    #
+    # TODO: Check IP address, Port number, etc...
+    _CoerceConfigurationValues(WebNode_id, newConfig)
     
     # store to file the new configuration so that 
     # we can recoup the configuration the next time the PLC
@@ -382,9 +428,11 @@
 
 def OnModbusButtonReset(**kwargs):
     """
-    Function called when user clicks 'Delete' button in web interface
+    Function called when user clicks 'Reset' button in web interface
     The function will delete the file containing the persistent
-    Modbus configution
+    Modbus configution. More specifically, reset to the configuration
+    hardcoded in the source code (<=> configuration specified
+    in Beremiz IDE).
     """
 
     WebNode_id = kwargs.get("WebNode_id", None)
@@ -406,7 +454,7 @@
 
 
 
-def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs):
+def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs, CoerceParamFuncs):
     """
     Load from the compiled code (.so file, aloready loaded into memmory)
     the configuration parameters of a specific Modbus plugin node.
@@ -443,11 +491,12 @@
     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'
+    WebNode_entry["GetParamFuncs"] = GetParamFuncs
+    WebNode_entry["SetParamFuncs"] = SetParamFuncs
+    WebNode_entry["CoerceParamFuncs"] = CoerceParamFuncs
         
     
     # Dictionary that contains the Modbus configuration currently being shown
@@ -491,7 +540,7 @@
         return str(_GetModbusWebviewConfigurationValue(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"]]
+                    for name, web_label, c_dtype, web_dtype, coerce_function in WebNode_entry["WebParamList"]]
 
     # Configure the web interface to include the Modbus config parameters
     def __OnButtonSave(**kwargs):
@@ -569,43 +618,47 @@
     # 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 = {}
+    GetServerParamFuncs = {}
     SetClientParamFuncs = {}
-    GetServerParamFuncs = {}
     SetServerParamFuncs = {}
+    CoerceClientParamFuncs = {}
+    CoerceServerParamFuncs = {}
 
     # XXX TODO : stop reading from PLC .so file. This code is template code
     #            that can use modbus extension build data
-    for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters:
+    for name, web_label, c_dtype, web_dtype, coerce_function 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:
+    for name, web_label, c_dtype, web_dtype, coerce_function 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]
+        CoerceClientParamFuncs[name]       = coerce_function
 
     # XXX TODO : stop reading from PLC .so file. This code is template code
     #            that can use modbus extension build data
-    for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters:
+    for name, web_label, c_dtype, web_dtype, coerce_function 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:
+    for name, web_label, c_dtype, web_dtype, coerce_function 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]
+        CoerceServerParamFuncs[name]       = coerce_function
 
     for node_id in range(client_count):
-        _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs)
+        _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs, CoerceClientParamFuncs)
 
     for node_id in range(server_count):
-        _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs)
+        _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs, CoerceServerParamFuncs)