msousa@2654: #!/usr/bin/env python msousa@2654: # -*- coding: utf-8 -*- msousa@2654: msousa@2654: # This file is part of Beremiz runtime. msousa@2654: # msousa@2654: # Copyright (C) 2020: Mario de Sousa msousa@2654: # msousa@2654: # See COPYING.Runtime file for copyrights details. msousa@2654: # msousa@2654: # This library is free software; you can redistribute it and/or msousa@2654: # modify it under the terms of the GNU Lesser General Public msousa@2654: # License as published by the Free Software Foundation; either msousa@2654: # version 2.1 of the License, or (at your option) any later version. msousa@2654: msousa@2654: # This library is distributed in the hope that it will be useful, msousa@2654: # but WITHOUT ANY WARRANTY; without even the implied warranty of msousa@2654: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU msousa@2654: # Lesser General Public License for more details. msousa@2654: msousa@2654: # You should have received a copy of the GNU Lesser General Public msousa@2654: # License along with this library; if not, write to the Free Software msousa@2654: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA msousa@2654: msousa@2654: msousa@2654: msousa@2654: msousa@2654: ############################################################################################## msousa@2654: # This file implements an extension to the web server embedded in the Beremiz_service.py # msousa@2654: # runtime manager (webserver is in runtime/NevowServer.py). # msousa@2654: # # msousa@2654: # The extension implemented in this file allows for runtime configuration # msousa@2654: # of Modbus plugin parameters # msousa@2654: ############################################################################################## msousa@2654: msousa@2654: msousa@2654: msousa@2654: import json msousa@2654: import os msousa@2654: import ctypes msousa@2654: import string msousa@2654: import hashlib msousa@2654: msousa@2654: from formless import annotate, webform msousa@2654: Edouard@2669: import runtime.NevowServer as NS msousa@2654: msousa@2654: # Directory in which to store the persistent configurations msousa@2654: # Should be a directory that does not get wiped on reboot! Edouard@2673: _ModbusConfFiledir = WorkingDir msousa@2654: msousa@2655: # List of all Web Extension Setting nodes we are handling. msousa@2655: # One WebNode each for: msousa@2655: # - Modbus TCP client msousa@2655: # - Modbus TCP server msousa@2655: # - Modbus RTU client msousa@2655: # - Modbus RTU slave msousa@2655: # configured in the loaded PLC (i.e. the .so file loaded into memory) msousa@2655: # Each entry will be a dictionary. See _AddWebNode() for the details msousa@2655: # of the data structure in each entry. msousa@2655: _WebNodeList = [] msousa@2655: msousa@2655: msousa@2655: msousa@2666: msousa@2666: class MB_StrippedString(annotate.String): msousa@2666: def __init__(self, *args, **kwargs): msousa@2666: annotate.String.__init__(self, strip = True, *args, **kwargs) msousa@2666: msousa@2666: msousa@2657: class MB_StopBits(annotate.Choice): msousa@2657: _choices = [0, 1, 2] msousa@2657: msousa@2657: def coerce(self, val, configurable): msousa@2657: return int(val) msousa@2657: def __init__(self, *args, **kwargs): msousa@2657: annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) msousa@2657: msousa@2657: msousa@2657: class MB_Baud(annotate.Choice): msousa@2657: _choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200] msousa@2657: msousa@2657: def coerce(self, val, configurable): msousa@2657: return int(val) msousa@2657: def __init__(self, *args, **kwargs): msousa@2657: annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) msousa@2657: msousa@2657: msousa@2657: class MB_Parity(annotate.Choice): msousa@2660: # For more info on what this class really does, have a look at the code in msousa@2660: # file twisted/nevow/annotate.py msousa@2660: # grab this code from $git clone https://github.com/twisted/nevow/ msousa@2660: # msousa@2657: # Warning: do _not_ name this variable choice[] without underscore, as that name is msousa@2657: # already used for another similar variable by the underlying class annotate.Choice msousa@2657: _choices = [ 0, 1, 2 ] msousa@2657: _label = ["none", "odd", "even"] msousa@2657: msousa@2657: def choice_to_label(self, key): Edouard@2669: #PLCObject.LogMessage("Modbus web server extension::choice_to_label() " + str(key)) msousa@2657: return self._label[key] msousa@2657: msousa@2657: def coerce(self, val, configurable): msousa@2657: """Coerce a value with the help of an object, which is the object msousa@2657: we are configuring. msousa@2657: """ msousa@2657: # Basically, make sure the value the user introduced is valid, and transform msousa@2657: # into something that is valid if necessary or mark it as an error msousa@2657: # (by raising an exception ??). msousa@2657: # msousa@2657: # We are simply using this functions to transform the input value (a string) msousa@2657: # into an integer. Note that although the available options are all msousa@2657: # integers (0, 1 or 2), even though what is shown on the user interface msousa@2657: # are actually strings, i.e. the labels), these parameters are for some msousa@2657: # reason being parsed as strings, so we need to map them back to an msousa@2657: # integer. msousa@2657: # Edouard@2669: #PLCObject.LogMessage("Modbus web server extension::coerce " + val ) msousa@2657: return int(val) msousa@2657: msousa@2657: def __init__(self, *args, **kwargs): msousa@2657: annotate.Choice.__init__(self, msousa@2657: choices = self._choices, msousa@2657: stringify = self.choice_to_label, msousa@2657: *args, **kwargs) msousa@2657: msousa@2657: msousa@2655: msousa@3743: # The parameter coerce functions referenced in the TCPclient_parameters etc. lists. msousa@3743: # These functions check wether the parameter has a legal value (i.e. they validate the parameter), msousa@3743: # and if not changes it to the closest legal value (i.e. coerces the parameter value to a legal value) msousa@3743: msousa@3743: def _coerce_comm_period(value): msousa@3743: # Period may not be negative value msousa@3743: if (value < 0): msousa@3743: value = 0 msousa@3743: return value msousa@3743: msousa@3743: def _coerce_req_delay(value): msousa@3743: # Request delay may not be negative value msousa@3743: # (i.e. delay between any two consecutive requests sent by a Beremiz RTU master or TCP client) msousa@3743: if (value < 0): msousa@3743: value = 0 msousa@3743: return value msousa@3743: msousa@3743: msousa@3743: msousa@2655: # Parameters we will need to get from the C code, but that will not be shown msousa@2654: # on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii) msousa@2658: # msousa@3743: # The annotate type entry is used to tell the web server the type of data entry widget to use. msousa@3743: # (see the file twisted/nevow/formless/annotate.py for list of options) msousa@3743: # Unfortunately an unsigned integer option does not exist. msousa@2654: General_parameters = [ msousa@3743: # param. name label ctype type annotate type Coerce msousa@3743: # (C code var name) (used on web interface) (C data type) (web data type) function msousa@2654: # (annotate.String, msousa@2654: # annotate.Integer, ...) msousa@3743: ("config_name" , _("") , ctypes.c_char_p, annotate.String, None), msousa@3743: ("addr_type" , _("") , ctypes.c_char_p, annotate.String, None) msousa@2654: ] msousa@2654: msousa@2661: # Parameters we will need to get from the C code, and that _will_ be shown msousa@2661: # on the web interface. msousa@2654: TCPclient_parameters = [ msousa@3743: # param. name label ctype type annotate type Coerce msousa@3743: # (C code var name) (used on web interface) (C data type) (web data type) function msousa@2654: # (annotate.String, msousa@2654: # annotate.Integer, ...) msousa@3743: ("host" , _("Remote IP Address") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("port" , _("Remote Port Number") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer , _coerce_comm_period), msousa@3743: ("req_delay" , _("Request Delay (ms)") , ctypes.c_ulonglong, annotate.Integer , _coerce_req_delay) msousa@2654: ] msousa@2654: msousa@2654: RTUclient_parameters = [ msousa@3743: # param. name label ctype type annotate type Coerce msousa@3743: # (C code var name) (used on web interface) (C data type) (web data type) function msousa@2654: # (annotate.String, msousa@2654: # annotate.Integer, ...) msousa@3743: ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud , None), msousa@3743: ("parity" , _("Parity") , ctypes.c_int, MB_Parity , None), msousa@3743: ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits , None), msousa@3743: ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer, _coerce_comm_period), msousa@3743: ("req_delay" , _("Request Delay (ms)") , ctypes.c_ulonglong, annotate.Integer, _coerce_req_delay) msousa@2654: ] msousa@2654: msousa@2655: TCPserver_parameters = [ msousa@3743: # param. name label ctype type annotate type Coerce msousa@3743: # (C code var name) (used on web interface) (C data type) (web data type) function msousa@2655: # (annotate.String, msousa@2655: # annotate.Integer, ...) msousa@3743: ("host" , _("Local IP Address") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("port" , _("Local Port Number") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer , None) msousa@2655: ] msousa@2655: msousa@2655: RTUslave_parameters = [ msousa@3743: # param. name label ctype type annotate type Coerce msousa@3743: # (C code var name) (used on web interface) (C data type) (web data type) function msousa@2655: # (annotate.String, msousa@2655: # annotate.Integer, ...) msousa@3743: ("device" , _("Serial Port") , ctypes.c_char_p, MB_StrippedString, None), msousa@3743: ("baud" , _("Baud Rate") , ctypes.c_int, MB_Baud , None), msousa@3743: ("parity" , _("Parity") , ctypes.c_int, MB_Parity , None), msousa@3743: ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits , None), msousa@3743: ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer, None) msousa@2655: ] msousa@2655: msousa@2655: msousa@2657: msousa@2657: msousa@2655: # Dictionary containing List of Web viewable parameters msousa@2654: # Note: the dictionary key must be the same as the string returned by the msousa@2654: # __modbus_get_ClientNode_addr_type() msousa@2654: # __modbus_get_ServerNode_addr_type() msousa@2654: # functions implemented in C (see modbus/mb_runtime.c) msousa@2655: _client_WebParamListDict = {} msousa@2655: _client_WebParamListDict["tcp" ] = TCPclient_parameters msousa@2655: _client_WebParamListDict["rtu" ] = RTUclient_parameters msousa@2655: _client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) msousa@2655: msousa@2655: _server_WebParamListDict = {} msousa@2655: _server_WebParamListDict["tcp" ] = TCPserver_parameters msousa@2655: _server_WebParamListDict["rtu" ] = RTUslave_parameters msousa@2655: _server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) msousa@2654: msousa@2656: WebParamListDictDict = {} msousa@2656: WebParamListDictDict['client'] = _client_WebParamListDict msousa@2656: WebParamListDictDict['server'] = _server_WebParamListDict msousa@2656: msousa@2654: msousa@2657: msousa@2657: Edouard@2686: def _SetModbusSavedConfiguration(WebNode_id, newConfig): msousa@2654: """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """ Edouard@2675: WebNode_entry = _WebNodeList[WebNode_id] Edouard@2675: Edouard@2675: if WebNode_entry["DefaultConfiguration"] == newConfig: Edouard@2675: Edouard@2686: _DelModbusSavedConfiguration(WebNode_id) Edouard@2686: WebNode_entry["ModbusSavedConfiguration"] = None Edouard@2675: Edouard@2675: else: Edouard@2675: Edouard@2675: # Add the addr_type and node_type to the data that will be saved to file Edouard@2675: # This allows us to confirm the saved data contains the correct addr_type Edouard@2675: # when loading from file Edouard@2675: save_info = {} Edouard@2690: save_info["addr_type"] = WebNode_entry["addr_type"] Edouard@2675: save_info["node_type"] = WebNode_entry["node_type"] Edouard@2675: save_info["config" ] = newConfig msousa@2654: Edouard@2675: filename = WebNode_entry["filename"] Edouard@2675: Edouard@2675: with open(os.path.realpath(filename), 'w') as f: Edouard@2675: json.dump(save_info, f, sort_keys=True, indent=4) Edouard@2675: Edouard@2686: WebNode_entry["ModbusSavedConfiguration"] = newConfig Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: def _DelModbusSavedConfiguration(WebNode_id): msousa@2654: """ Deletes the file cotaining the persistent Modbus configuration """ msousa@2655: filename = _WebNodeList[WebNode_id]["filename"] msousa@2654: msousa@2654: if os.path.exists(filename): msousa@2654: os.remove(filename) msousa@2654: msousa@2654: msousa@2654: msousa@2654: Edouard@2686: def _GetModbusSavedConfiguration(WebNode_id): msousa@2654: """ msousa@2654: Returns a dictionary containing the Modbus parameter configuration msousa@2656: that was last saved to file. If no file exists, or file contains msousa@2656: wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the msousa@2656: addr_type of the WebNode_id), then return None msousa@2654: """ msousa@2655: filename = _WebNodeList[WebNode_id]["filename"] msousa@2654: try: msousa@2654: #if os.path.isfile(filename): Edouard@2690: save_info = json.load(open(os.path.realpath(filename))) msousa@2654: except Exception: msousa@2654: return None msousa@2654: msousa@2656: if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: msousa@2656: return None msousa@2656: if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: msousa@2656: return None msousa@2656: if "config" not in save_info: msousa@2656: return None msousa@2656: msousa@2656: saved_config = save_info["config"] msousa@2656: msousa@2654: #if _CheckConfiguration(saved_config): msousa@2654: # return saved_config msousa@2654: #else: msousa@2654: # return None msousa@2654: msousa@2654: return saved_config msousa@2654: msousa@2654: msousa@2654: Edouard@2686: def _GetModbusPLCConfiguration(WebNode_id): msousa@2654: """ msousa@2654: Returns a dictionary containing the current Modbus parameter configuration msousa@2654: stored in the C variables in the loaded PLC (.so file) msousa@2654: """ msousa@2654: current_config = {} msousa@2655: C_node_id = _WebNodeList[WebNode_id]["C_node_id"] msousa@2655: WebParamList = _WebNodeList[WebNode_id]["WebParamList"] msousa@2655: GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"] msousa@2655: msousa@3743: for WebParamListEntry in WebParamList: msousa@3743: par_name = WebParamListEntry[0] msousa@2655: value = GetParamFuncs[par_name](C_node_id) msousa@2654: if value is not None: msousa@2654: current_config[par_name] = value msousa@2654: msousa@2654: return current_config msousa@2654: msousa@2654: msousa@2654: Edouard@2686: def _SetModbusPLCConfiguration(WebNode_id, newconfig): msousa@2654: """ msousa@2654: Stores the Modbus parameter configuration into the msousa@2654: the C variables in the loaded PLC (.so file) msousa@2654: """ msousa@2655: C_node_id = _WebNodeList[WebNode_id]["C_node_id"] msousa@2655: SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"] msousa@2655: msousa@2654: for par_name in newconfig: msousa@2654: value = newconfig[par_name] msousa@2654: if value is not None: msousa@2655: SetParamFuncs[par_name](C_node_id, value) msousa@2654: msousa@2654: msousa@2654: msousa@2654: Edouard@2686: def _GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument): msousa@2654: """ msousa@2654: Callback function, called by the web interface (NevowServer.py) msousa@2654: to fill in the default value of each parameter of the web form msousa@2654: msousa@2654: Note that the real callback function is a dynamically created function that msousa@2655: will simply call this function to do the work. It will also pass the WebNode_id msousa@2654: as a parameter. msousa@2655: """ msousa@2654: try: msousa@2655: return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] msousa@2654: except Exception: msousa@2654: return "" msousa@2654: msousa@2654: msousa@3743: def _CoerceConfigurationValues(WebNode_id, config): msousa@3743: """ msousa@3743: Coerces any incorrect value in the passed configuration msousa@3743: to the nearest correct value. msousa@3743: For example, negative times in delay paramater are changed to 0, etc... msousa@3743: """ msousa@3743: CoerceParamFuncs = _WebNodeList[WebNode_id]["CoerceParamFuncs"] msousa@3743: msousa@3743: for par_name in config: msousa@3743: CoerceFunction = CoerceParamFuncs[par_name] msousa@3743: if (config[par_name] is not None) and (CoerceFunction is not None): msousa@3743: config[par_name] = CoerceFunction(config[par_name]) msousa@3743: msousa@3743: msousa@2654: Edouard@2686: def OnModbusButtonSave(**kwargs): msousa@2654: """ msousa@2654: Function called when user clicks 'Save' button in web interface msousa@2654: The function will configure the Modbus plugin in the PLC with the values msousa@2654: specified in the web interface. However, values must be validated first! msousa@2654: msousa@2654: Note that this function does not get called directly. The real callback msousa@2654: function is the dynamic __OnButtonSave() function, which will add the msousa@2655: "WebNode_id" argument, and call this function to do the work. msousa@2654: """ msousa@2654: Edouard@2686: #PLCObject.LogMessage("Modbus web server extension::OnModbusButtonSave() Called") msousa@2654: msousa@2655: newConfig = {} msousa@2655: WebNode_id = kwargs.get("WebNode_id", None) msousa@2655: WebParamList = _WebNodeList[WebNode_id]["WebParamList"] msousa@2655: msousa@3743: for WebParamListEntry in WebParamList: msousa@3743: par_name = WebParamListEntry[0] msousa@2654: value = kwargs.get(par_name, None) msousa@2654: if value is not None: msousa@2654: newConfig[par_name] = value msousa@2654: msousa@2654: # First check if configuration is OK. msousa@3743: # Note that this is currently not really required for most of the parameters, msousa@3743: # as we use drop down choice menus for baud, parity and sop bits msousa@3743: # (so these values should always be correct!). msousa@3743: # The timing properties (period in ms, delays in ms, etc.) msousa@3743: # do however need to be validated as the web interface does not msousa@3743: # enforce any limits on the values (e.g., they must be positive values). msousa@3743: # NOTE: It would be confusing for the user if we simply refuse to make the msousa@3743: # requested changes without giving any feedback that the changes were not applied, msousa@3743: # and why they were refused (i.e. at the moment the web page does not have msousa@3743: # any pop-up windows or special messages for that). msousa@3743: # That is why instead of returning without making the changes in case of error, msousa@3743: # we instead opt to change the requested configuration to the closest valid values msousa@3743: # and apply that instead. The user gets feedback on the applied values as these get msousa@3743: # shown on the web interface. msousa@3743: # msousa@3743: # TODO: Check IP address, Port number, etc... msousa@3743: _CoerceConfigurationValues(WebNode_id, newConfig) msousa@2658: msousa@2654: # store to file the new configuration so that msousa@2654: # we can recoup the configuration the next time the PLC msousa@2654: # has a cold start (i.e. when Beremiz_service.py is retarted) Edouard@2686: _SetModbusSavedConfiguration(WebNode_id, newConfig) msousa@2654: msousa@2654: # Configure PLC with the current Modbus parameters Edouard@2686: _SetModbusPLCConfiguration(WebNode_id, newConfig) msousa@2654: msousa@2665: # Update the viewable configuration Edouard@2686: # The PLC may have coerced the values on calling _SetModbusPLCConfiguration() msousa@2665: # so we do not set it directly to newConfig Edouard@2686: _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetModbusPLCConfiguration(WebNode_id) Edouard@2686: Edouard@2686: Edouard@2686: Edouard@2686: def OnModbusButtonReset(**kwargs): msousa@2654: """ msousa@3743: Function called when user clicks 'Reset' button in web interface msousa@2654: The function will delete the file containing the persistent msousa@3743: Modbus configution. More specifically, reset to the configuration msousa@3743: hardcoded in the source code (<=> configuration specified msousa@3743: in Beremiz IDE). msousa@2654: """ msousa@2654: msousa@2655: WebNode_id = kwargs.get("WebNode_id", None) msousa@2654: msousa@2654: # Delete the file Edouard@2686: _DelModbusSavedConfiguration(WebNode_id) msousa@2654: msousa@2654: # Set the current configuration to the default (hardcoded in C) msousa@2655: new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"] Edouard@2686: _SetModbusPLCConfiguration(WebNode_id, new_config) msousa@2654: msousa@2654: #Update the webviewconfiguration msousa@2655: _WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config msousa@2654: Edouard@2686: # Reset ModbusSavedConfiguration Edouard@2686: _WebNodeList[WebNode_id]["ModbusSavedConfiguration"] = None msousa@2654: Edouard@2670: msousa@2655: msousa@2655: msousa@2655: msousa@3743: def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs, CoerceParamFuncs): msousa@2655: """ msousa@2655: Load from the compiled code (.so file, aloready loaded into memmory) msousa@2655: the configuration parameters of a specific Modbus plugin node. msousa@2655: This function works with both client and server nodes, depending on the msousa@2655: Get/SetParamFunc dictionaries passed to it (either the client or the server msousa@2655: node versions of the Get/Set functions) msousa@2655: """ msousa@2655: WebNode_entry = {} msousa@2655: msousa@2656: # Get the config_name from the C code... msousa@2655: config_name = GetParamFuncs["config_name"](C_node_id) msousa@2656: # Get the addr_type from the C code... msousa@2654: # addr_type will be one of "tcp", "rtu" or "ascii" msousa@2655: addr_type = GetParamFuncs["addr_type" ](C_node_id) msousa@2654: # For some operations we cannot use the config name (e.g. filename to store config) msousa@2654: # because the user may be using characters that are invalid for that purpose ('/' for msousa@2654: # example), so we create a hash of the config_name, and use that instead. edouard@3869: config_hash = hashlib.md5(config_name.encode()).hexdigest() msousa@2654: Edouard@2669: #PLCObject.LogMessage("Modbus web server extension::_AddWebNode("+str(C_node_id)+") config_name="+config_name) msousa@2654: msousa@2654: # Add the new entry to the global list msousa@2655: # Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in WebNode_entry msousa@2655: # WebNode_entry will be stored as a reference, so we can later insert parameters at will. msousa@2655: global _WebNodeList msousa@2655: _WebNodeList.append(WebNode_entry) msousa@2655: WebNode_id = len(_WebNodeList) - 1 msousa@2655: msousa@2655: # store all WebNode relevant data for future reference msousa@2655: # msousa@2655: # Note that "WebParamList" will reference one of: msousa@2655: # - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters msousa@2655: WebNode_entry["C_node_id" ] = C_node_id msousa@2655: WebNode_entry["config_name" ] = config_name msousa@2655: WebNode_entry["config_hash" ] = config_hash msousa@2655: WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json") msousa@2656: WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type] msousa@2656: WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function) msousa@2656: WebNode_entry["node_type" ] = node_type # 'client', 'server' msousa@3743: WebNode_entry["GetParamFuncs"] = GetParamFuncs msousa@3743: WebNode_entry["SetParamFuncs"] = SetParamFuncs msousa@3743: WebNode_entry["CoerceParamFuncs"] = CoerceParamFuncs msousa@2656: msousa@2654: msousa@2654: # Dictionary that contains the Modbus configuration currently being shown msousa@2654: # on the web interface msousa@2654: # This configuration will almost always be identical to the current msousa@2654: # configuration in the PLC (i.e., the current state stored in the msousa@2654: # C variables in the .so file). msousa@2654: # The configuration viewed on the web will only be different to the current msousa@2654: # configuration when the user edits the configuration, and when msousa@2654: # the user asks to save an edited configuration that contains an error. msousa@2655: WebNode_entry["WebviewConfiguration"] = None msousa@2654: msousa@2654: # Upon PLC load, this Dictionary is initialised with the Modbus configuration msousa@2654: # hardcoded in the C file msousa@2654: # (i.e. the configuration inserted in Beremiz IDE when project was compiled) Edouard@2686: WebNode_entry["DefaultConfiguration"] = _GetModbusPLCConfiguration(WebNode_id) msousa@2655: WebNode_entry["WebviewConfiguration"] = WebNode_entry["DefaultConfiguration"] msousa@2654: msousa@2654: # Dictionary that stores the Modbus configuration currently stored in a file msousa@2654: # Currently only used to decide whether or not to show the "Delete" button on the Edouard@2686: # web interface (only shown if "ModbusSavedConfiguration" is not None) Edouard@2686: SavedConfig = _GetModbusSavedConfiguration(WebNode_id) Edouard@2686: WebNode_entry["ModbusSavedConfiguration"] = SavedConfig msousa@2654: msousa@2654: if SavedConfig is not None: Edouard@2686: _SetModbusPLCConfiguration(WebNode_id, SavedConfig) msousa@2655: WebNode_entry["WebviewConfiguration"] = SavedConfig msousa@2654: msousa@2654: # Define the format for the web form used to show/change the current parameters msousa@2654: # We first declare a dynamic function to work as callback to obtain the default values for each parameter msousa@2657: # Note: We transform every parameter into a string msousa@2657: # This is not strictly required for parameters of type annotate.Integer that will correctly msousa@2657: # accept the default value as an Integer python object msousa@2657: # This is obviously also not required for parameters of type annotate.String, that are msousa@2657: # always handled as strings. msousa@2657: # However, the annotate.Choice parameters (and all parameters that derive from it, msousa@2657: # sucn as Parity, Baud, etc.) require the default value as a string msousa@2657: # even though we store it as an integer, which is the data type expected msousa@2657: # by the set_***() C functions in mb_runtime.c msousa@2654: def __GetWebviewConfigurationValue(ctx, argument): Edouard@2686: return str(_GetModbusWebviewConfigurationValue(ctx, WebNode_id, argument)) msousa@2654: msousa@2654: webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue)) msousa@3743: for name, web_label, c_dtype, web_dtype, coerce_function in WebNode_entry["WebParamList"]] msousa@2654: msousa@2654: # Configure the web interface to include the Modbus config parameters msousa@2654: def __OnButtonSave(**kwargs): Edouard@2686: OnModbusButtonSave(WebNode_id=WebNode_id, **kwargs) msousa@2654: Edouard@2672: WebSettings = NS.newExtensionSetting("Modbus #"+ str(WebNode_id), config_hash) Edouard@2670: Edouard@2670: WebSettings.addSettings( msousa@2654: "ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...) msousa@2654: _("Modbus Configuration: ") + config_name, # description (user visible label) msousa@2654: webFormInterface, # fields Edouard@2670: _("Apply"), # button label msousa@2654: __OnButtonSave) # callback msousa@2654: Edouard@2670: def __OnButtonReset(**kwargs): Edouard@2686: return OnModbusButtonReset(WebNode_id = WebNode_id, **kwargs) Edouard@2670: Edouard@2686: def getModbusConfigStatus(): Edouard@2670: if WebNode_entry["WebviewConfiguration"] == WebNode_entry["DefaultConfiguration"]: Edouard@2670: return "Unchanged" Edouard@2670: return "Modified" Edouard@2670: Edouard@2670: WebSettings.addSettings( Edouard@2670: "ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...) Edouard@2670: _("Modbus Configuration: ") + config_name, # description (user visible label) Edouard@2670: [ ("status", Edouard@2670: annotate.String(label=_("Current state"), Edouard@2670: immutable=True, Edouard@2686: default=lambda *k:getModbusConfigStatus())), Edouard@2670: ], # fields (empty, no parameters required!) Edouard@2670: _("Reset"), # button label Edouard@2670: __OnButtonReset) msousa@2655: msousa@2654: msousa@2654: msousa@2654: Edouard@2703: def _runtime_%(location_str)s_modbus_websettings_init(): msousa@2654: """ msousa@2654: Callback function, called (by PLCObject.py) when a new PLC program msousa@2654: (i.e. XXX.so file) is transfered to the PLC runtime msousa@2654: and loaded into memory msousa@2654: """ Edouard@2669: Edouard@2669: #PLCObject.LogMessage("Modbus web server extension::OnLoadPLC() Called...") Edouard@2669: Edouard@2669: if PLCObject.PLClibraryHandle is None: msousa@2654: # PLC was loaded but we don't have access to the library of compiled code (.so lib)? msousa@2654: # Hmm... This shold never occur!! msousa@2654: return msousa@2654: msousa@2654: # Get the number of Modbus Client and Servers (Modbus plugin) msousa@2654: # configured in the currently loaded PLC project (i.e., the .so file) msousa@2654: # If the "__modbus_plugin_client_node_count" msousa@2654: # or the "__modbus_plugin_server_node_count" C variables msousa@2654: # are not present in the .so file we conclude that the currently loaded msousa@2654: # PLC does not have the Modbus plugin included (situation (2b) described above init()) msousa@2654: try: Edouard@2676: # XXX TODO : stop reading from PLC .so file. This code is template code Edouard@2676: # that can use modbus extension build data, such as client node count. Edouard@2669: client_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_client_node_count").value Edouard@2669: server_count = ctypes.c_int.in_dll(PLCObject.PLClibraryHandle, "__modbus_plugin_server_node_count").value msousa@2654: except Exception: msousa@2654: # Loaded PLC does not have the Modbus plugin => nothing to do msousa@2654: # (i.e. do _not_ configure and make available the Modbus web interface) msousa@2654: return msousa@2654: msousa@2654: if client_count < 0: client_count = 0 msousa@2654: if server_count < 0: server_count = 0 msousa@2654: msousa@2654: if (client_count == 0) and (server_count == 0): msousa@2654: # The Modbus plugin in the loaded PLC does not have any client and servers configured msousa@2654: # => nothing to do (i.e. do _not_ configure and make available the Modbus web interface) msousa@2654: return msousa@2654: msousa@2654: # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters msousa@2655: # Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c) msousa@2655: GetClientParamFuncs = {} msousa@3743: GetServerParamFuncs = {} msousa@2655: SetClientParamFuncs = {} msousa@2655: SetServerParamFuncs = {} msousa@3743: CoerceClientParamFuncs = {} msousa@3743: CoerceServerParamFuncs = {} msousa@2655: Edouard@2676: # XXX TODO : stop reading from PLC .so file. This code is template code Edouard@2676: # that can use modbus extension build data msousa@3743: for name, web_label, c_dtype, web_dtype, coerce_function in TCPclient_parameters + RTUclient_parameters + General_parameters: msousa@2655: ParamFuncName = "__modbus_get_ClientNode_" + name edouard@3869: func = getattr(PLCObject.PLClibraryHandle, ParamFuncName) edouard@3869: func.restype = c_dtype edouard@3869: func.argtypes = [ctypes.c_int] edouard@3869: GetClientParamFuncs[name] = (lambda *a,**k: func(*a,**k).decode()) if c_dtype == ctypes.c_char_p else func msousa@2654: msousa@3743: for name, web_label, c_dtype, web_dtype, coerce_function in TCPclient_parameters + RTUclient_parameters: msousa@2655: ParamFuncName = "__modbus_set_ClientNode_" + name Edouard@2669: SetClientParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) msousa@2655: SetClientParamFuncs[name].restype = None msousa@2655: SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] msousa@3743: CoerceClientParamFuncs[name] = coerce_function msousa@2655: Edouard@2676: # XXX TODO : stop reading from PLC .so file. This code is template code Edouard@2676: # that can use modbus extension build data msousa@3743: for name, web_label, c_dtype, web_dtype, coerce_function in TCPserver_parameters + RTUslave_parameters + General_parameters: msousa@2655: ParamFuncName = "__modbus_get_ServerNode_" + name edouard@3869: func = getattr(PLCObject.PLClibraryHandle, ParamFuncName) edouard@3869: func.restype = c_dtype edouard@3869: func.argtypes = [ctypes.c_int] edouard@3869: GetServerParamFuncs[name] = (lambda *a,**k: func(*a,**k).decode()) if c_dtype == ctypes.c_char_p else func edouard@3869: msousa@3743: for name, web_label, c_dtype, web_dtype, coerce_function in TCPserver_parameters + RTUslave_parameters: msousa@2655: ParamFuncName = "__modbus_set_ServerNode_" + name Edouard@2669: SetServerParamFuncs[name] = getattr(PLCObject.PLClibraryHandle, ParamFuncName) msousa@2655: SetServerParamFuncs[name].restype = None msousa@2655: SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] msousa@3743: CoerceServerParamFuncs[name] = coerce_function msousa@2654: msousa@2654: for node_id in range(client_count): msousa@3743: _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs, CoerceClientParamFuncs) msousa@2655: msousa@2655: for node_id in range(server_count): msousa@3743: _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs, CoerceServerParamFuncs) msousa@2654: msousa@2654: msousa@2654: msousa@2654: msousa@2654: Edouard@2703: def _runtime_%(location_str)s_modbus_websettings_cleanup(): msousa@2654: """ msousa@2655: Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory msousa@2654: """ msousa@2654: Edouard@2669: #PLCObject.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...") msousa@2654: msousa@2654: # Delete the Modbus specific web interface extensions msousa@2654: # (Safe to ask to delete, even if it has not been added!) Edouard@2672: global _WebNodeList Edouard@2672: for index, WebNode_entry in enumerate(_WebNodeList): msousa@2655: config_hash = WebNode_entry["config_hash"] Edouard@2672: NS.removeExtensionSetting(config_hash) msousa@2654: msousa@2654: # Dele all entries... msousa@2655: _WebNodeList = [] msousa@2654: