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@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@2658: # The annotate type entry is basically useless and is completely ignored.
msousa@2658: # We kee that entry so that this list can later be correctly merged with the
msousa@2658: # following lists...
msousa@2654: General_parameters = [
msousa@2654:     #    param. name       label                        ctype type         annotate type
msousa@2654:     # (C code var name)   (used on web interface)      (C data type)       (web data type)
msousa@2654:     #                                                                      (annotate.String,
msousa@2654:     #                                                                       annotate.Integer, ...)
msousa@2654:     ("config_name"      , _("")                      , ctypes.c_char_p,    annotate.String),
msousa@2654:     ("addr_type"        , _("")                      , ctypes.c_char_p,    annotate.String)
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@2654:     #    param. name       label                        ctype type         annotate type
msousa@2654:     # (C code var name)   (used on web interface)      (C data type)       (web data type)
msousa@2654:     #                                                                      (annotate.String,
msousa@2654:     #                                                                       annotate.Integer, ...)
msousa@2666:     ("host"             , _("Remote IP Address")     , ctypes.c_char_p,    MB_StrippedString),
msousa@2666:     ("port"             , _("Remote Port Number")    , ctypes.c_char_p,    MB_StrippedString),
msousa@2666:     ("comm_period"      , _("Invocation Rate (ms)")  , ctypes.c_ulonglong, annotate.Integer )
msousa@2654:     ]
msousa@2654: 
msousa@2654: RTUclient_parameters = [                                                   
msousa@2654:     #    param. name       label                        ctype type         annotate type
msousa@2654:     # (C code var name)   (used on web interface)      (C data type)       (web data type)
msousa@2654:     #                                                                      (annotate.String,
msousa@2654:     #                                                                       annotate.Integer, ...)
msousa@2666:     ("device"           , _("Serial Port")           , ctypes.c_char_p,    MB_StrippedString),
msousa@2657:     ("baud"             , _("Baud Rate")             , ctypes.c_int,       MB_Baud         ),
msousa@2657:     ("parity"           , _("Parity")                , ctypes.c_int,       MB_Parity       ),
msousa@2657:     ("stop_bits"        , _("Stop Bits")             , ctypes.c_int,       MB_StopBits     ),
msousa@2654:     ("comm_period"      , _("Invocation Rate (ms)")  , ctypes.c_ulonglong, annotate.Integer)
msousa@2654:     ]
msousa@2654: 
msousa@2655: TCPserver_parameters = [                                                   
msousa@2655:     #    param. name       label                        ctype type         annotate type
msousa@2655:     # (C code var name)   (used on web interface)      (C data type)       (web data type)
msousa@2655:     #                                                                      (annotate.String,
msousa@2655:     #                                                                       annotate.Integer, ...)
msousa@2666:     ("host"             , _("Local IP Address")      , ctypes.c_char_p,    MB_StrippedString),
msousa@2666:     ("port"             , _("Local Port Number")     , ctypes.c_char_p,    MB_StrippedString),
msousa@2666:     ("slave_id"         , _("Slave ID")              , ctypes.c_ubyte,     annotate.Integer )
msousa@2655:     ]
msousa@2655: 
msousa@2655: RTUslave_parameters = [                                                   
msousa@2655:     #    param. name       label                        ctype type         annotate type
msousa@2655:     # (C code var name)   (used on web interface)      (C data type)       (web data type)
msousa@2655:     #                                                                      (annotate.String,
msousa@2655:     #                                                                       annotate.Integer, ...)
msousa@2666:     ("device"           , _("Serial Port")           , ctypes.c_char_p,    MB_StrippedString),
msousa@2657:     ("baud"             , _("Baud Rate")             , ctypes.c_int,       MB_Baud         ),
msousa@2657:     ("parity"           , _("Parity")                , ctypes.c_int,       MB_Parity       ),
msousa@2657:     ("stop_bits"        , _("Stop Bits")             , ctypes.c_int,       MB_StopBits     ),
Edouard@2691:     ("slave_id"         , _("Slave ID")              , ctypes.c_ubyte,     annotate.Integer)
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: 
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@2655:     for par_name, x1, x2, x3 in WebParamList:
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@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@2655:     for par_name, x1, x2, x3 in WebParamList:
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@2658:     # Note that this is not currently required, as we use drop down choice menus
msousa@2658:     # for baud, parity and sop bits, so the values should always be correct!
msousa@2654:     #if not _CheckWebConfiguration(newConfig):
msousa@2654:     #    return
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@2654:     Function called when user clicks 'Delete' button in web interface
msousa@2654:     The function will delete the file containing the persistent
msousa@2654:     Modbus configution
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@2656: def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs):
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.
msousa@2654:     config_hash = hashlib.md5(config_name).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@2655:     WebNode_entry["GetParamFuncs"] = GetParamFuncs
msousa@2655:     WebNode_entry["SetParamFuncs"] = SetParamFuncs
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@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@2656:                     for name, web_label, c_dtype, web_dtype 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@2993: 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@2655:     SetClientParamFuncs = {}
msousa@2655:     GetServerParamFuncs = {}
msousa@2655:     SetServerParamFuncs = {}
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@2654:     for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters:
msousa@2655:         ParamFuncName                      = "__modbus_get_ClientNode_" + name        
Edouard@2669:         GetClientParamFuncs[name]          = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
msousa@2655:         GetClientParamFuncs[name].restype  = c_dtype
msousa@2655:         GetClientParamFuncs[name].argtypes = [ctypes.c_int]
msousa@2654:         
msousa@2654:     for name, web_label, c_dtype, web_dtype 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@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@2655:     for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters:
msousa@2655:         ParamFuncName                      = "__modbus_get_ServerNode_" + name        
Edouard@2669:         GetServerParamFuncs[name]          = getattr(PLCObject.PLClibraryHandle, ParamFuncName)
msousa@2655:         GetServerParamFuncs[name].restype  = c_dtype
msousa@2655:         GetServerParamFuncs[name].argtypes = [ctypes.c_int]
msousa@2655:         
msousa@2655:     for name, web_label, c_dtype, web_dtype 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@2654: 
msousa@2654:     for node_id in range(client_count):
msousa@2656:         _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs)
msousa@2655: 
msousa@2655:     for node_id in range(server_count):
msousa@2656:         _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs)
msousa@2654: 
msousa@2654: 
msousa@2654: 
msousa@2654: 
msousa@2654: 
Edouard@2993: 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: