diff -r ac0e6de439b5 -r 65969628e920 modbus/web_settings.py --- a/modbus/web_settings.py Wed Mar 01 10:54:54 2023 +0100 +++ b/modbus/web_settings.py Fri Mar 10 09:13:29 2023 +0100 @@ -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)