# HG changeset patch # User Mario de Sousa # Date 1590998066 -3600 # Node ID 7575050a80c5427f748b0c510425ea0d013bf3bc # Parent db68cb0e6bdcdcb10272f5ef1f8a4b9377d81fa8 Add web extension: configure Modbus plugin parameters (currently only supports Modbus clients) diff -r db68cb0e6bdc -r 7575050a80c5 Beremiz_service.py --- a/Beremiz_service.py Thu May 28 11:15:22 2020 +0100 +++ b/Beremiz_service.py Mon Jun 01 08:54:26 2020 +0100 @@ -476,6 +476,7 @@ installThreadExcepthook() havewamp = False haveBNconf = False +haveMBconf = False if havetwisted: @@ -495,7 +496,16 @@ haveBNconf = True except Exception: LogMessageAndException(_("BACnet configuration web interface - import failed :")) - + + # Try to add support for Modbus configuration via web server interface + # NOTE:Modbus web config only makes sense if web server is available + if webport is not None: + try: + import runtime.Modbus_config as MBconf + haveMBconf = True + except Exception: + LogMessageAndException(_("Modbus configuration web interface - import failed :")) + try: import runtime.WampClient as WC # pylint: disable=ungrouped-imports WC.WorkingDir = WorkingDir @@ -540,6 +550,12 @@ except Exception: LogMessageAndException(_("BACnet web configuration failed startup. ")) + if haveMBconf: + try: + MBconf.init(plcobj, NS, WorkingDir) + except Exception: + LogMessageAndException(_("Modbus web configuration failed startup. ")) + if havewamp: try: WC.SetServer(pyroserver) diff -r db68cb0e6bdc -r 7575050a80c5 modbus/mb_runtime.c --- a/modbus/mb_runtime.c Thu May 28 11:15:22 2020 +0100 +++ b/modbus/mb_runtime.c Mon Jun 01 08:54:26 2020 +0100 @@ -493,14 +493,27 @@ int __init_%(locstr)s (int argc, char **argv){ int index; - for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) + for (index=0; index < NUMBER_OF_CLIENT_NODES;index++) { client_nodes[index].mb_nd = -1; - for (index=0; index < NUMBER_OF_SERVER_NODES;index++) + /* see comment in mb_runtime.h to understad why we need to initialize these entries */ + switch (client_nodes[index].node_address.naf) { + case naf_tcp: + client_nodes[index].node_address.addr.tcp.host = client_nodes[index].str1; + client_nodes[index].node_address.addr.tcp.service = client_nodes[index].str2; + break; + case naf_rtu: + client_nodes[index].node_address.addr.rtu.device = client_nodes[index].str1; + break; + } + } + + for (index=0; index < NUMBER_OF_SERVER_NODES;index++) { // mb_nd with negative numbers indicate how far it has been initialised (or not) // -2 --> no modbus node created; no thread created // -1 --> modbus node created!; no thread created // >=0 --> modbus node created!; thread created! server_nodes[index].mb_nd = -2; + } /* modbus library init */ /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus @@ -815,3 +828,113 @@ return res; } + + + + +/**********************************************/ +/** Functions for Beremiz web interface. **/ +/**********************************************/ + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * Modbus plugin also comes with an extension to the web server, through + * which the basic Modbus plugin configuration parameters may be changed + * + * These parameters are changed _after_ the code (.so file) is loaded into + * memmory. These changes may be applied before (or after) the code starts + * running (i.e. before or after __init_() ets called)! + * + * The following functions are never called from other C code. They are + * called instead from the python code in runtime/Modbus_config.py, that + * implements the web server extension for configuring Modbus parameters. + */ + + +/* The number of Cient nodes (i.e. the number of entries in the client_nodes array) + * The number of Server nodes (i.e. the numb. of entries in the server_nodes array) + * + * These variables are also used by the Modbus web config code to determine + * whether the current loaded PLC includes the Modbus plugin + * (so it should make the Modbus parameter web interface visible to the user). + */ +const int __modbus_plugin_client_node_count = NUMBER_OF_CLIENT_NODES; +const int __modbus_plugin_server_node_count = NUMBER_OF_SERVER_NODES; +const int __modbus_plugin_param_string_size = MODBUS_PARAM_STRING_SIZE; + + + +/* NOTE: We could have the python code in runtime/Modbus_config.py + * directly access the server_node_t and client_node_t structures, + * however this would create a tight coupling between these two + * disjoint pieces of code. + * Any change to the server_node_t or client_node_t structures would + * require the python code to be changed accordingly. I have therefore + * opted to create get/set functions, one for each parameter. + * + * We also convert the enumerated constants naf_ascii, etc... + * (from node_addr_family_t in modbus/mb_addr.h) + * into strings so as to decouple the python code that will be calling + * these functions from the Modbus library code definitions. + */ +const char *addr_type_str[] = { + [naf_ascii] = "ascii", + [naf_rtu ] = "rtu", + [naf_tcp ] = "tcp" +}; + + +#define __safe_strcnpy(str_dest, str_orig, max_size) { \ + strncpy(str_dest, str_orig, max_size); \ + str_dest[max_size - 1] = '\0'; \ +} + + +/* NOTE: The host, port and device parameters are strings that may be changed + * (by calling the following functions) after loading the compiled code + * (.so file) into memory, but before the code starts running + * (i.e. before __init_() gets called). + * This means that the host, port and device parameters may be changed + * _before_ they get mapped onto the str1 and str2 variables by __init_(), + * which is why the following functions must access the str1 and str2 + * parameters directly. + */ +const char * __modbus_get_ClientNode_config_name(int nodeid) {return client_nodes[nodeid].config_name; } +const char * __modbus_get_ClientNode_host (int nodeid) {return client_nodes[nodeid].str1; } +const char * __modbus_get_ClientNode_port (int nodeid) {return client_nodes[nodeid].str2; } +const char * __modbus_get_ClientNode_device (int nodeid) {return client_nodes[nodeid].str1; } +int __modbus_get_ClientNode_baud (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.baud; } +int __modbus_get_ClientNode_parity (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.parity; } +int __modbus_get_ClientNode_stop_bits (int nodeid) {return client_nodes[nodeid].node_address.addr.rtu.stop_bits;} +u64 __modbus_get_ClientNode_comm_period(int nodeid) {return client_nodes[nodeid].comm_period; } +const char * __modbus_get_ClientNode_addr_type (int nodeid) {return addr_type_str[client_nodes[nodeid].node_address.naf];} + +const char * __modbus_get_ServerNode_config_name(int nodeid) {return server_nodes[nodeid].config_name; } +const char * __modbus_get_ServerNode_host (int nodeid) {return server_nodes[nodeid].node_address.addr.tcp.host; } +const char * __modbus_get_ServerNode_port (int nodeid) {return server_nodes[nodeid].node_address.addr.tcp.service; } +const char * __modbus_get_ServerNode_device (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.device; } +int __modbus_get_ServerNode_baud (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.baud; } +int __modbus_get_ServerNode_parity (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.parity; } +int __modbus_get_ServerNode_stop_bits (int nodeid) {return server_nodes[nodeid].node_address.addr.rtu.stop_bits;} +const char * __modbus_get_ServerNode_addr_type (int nodeid) {return addr_type_str[server_nodes[nodeid].node_address.naf];} + + +void __modbus_set_ClientNode_host (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_port (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_device (int nodeid, const char * value) {__safe_strcnpy(client_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ClientNode_baud (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.baud = value;} +void __modbus_set_ClientNode_parity (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.parity = value;} +void __modbus_set_ClientNode_stop_bits (int nodeid, int value) {client_nodes[nodeid].node_address.addr.rtu.stop_bits = value;} +void __modbus_set_ClientNode_comm_period(int nodeid, u64 value) {client_nodes[nodeid].comm_period = value;} + + + diff -r db68cb0e6bdc -r 7575050a80c5 modbus/mb_runtime.h --- a/modbus/mb_runtime.h Thu May 28 11:15:22 2020 +0100 +++ b/modbus/mb_runtime.h Mon Jun 01 08:54:26 2020 +0100 @@ -30,6 +30,10 @@ #define DEF_REQ_SEND_RETRIES 0 + +#define MODBUS_PARAM_STRING_SIZE 64 + + // Used by the Modbus server node #define MEM_AREA_SIZE 65536 typedef struct{ @@ -39,8 +43,46 @@ u16 rw_words[MEM_AREA_SIZE]; } server_mem_t; + +/* + * Beremiz has a program to run on the PLC (Beremiz_service.py) + * to handle downloading of compiled programs, start/stop of PLC, etc. + * (see runtime/PLCObject.py for start/stop, loading, ...) + * + * This service also includes a web server to access PLC state (start/stop) + * and to change some basic confiuration parameters. + * (see runtime/NevowServer.py for the web server) + * + * The web server allows for extensions, where additional configuration + * parameters may be changed on the running/downloaded PLC. + * Modbus plugin also comes with an extension to the web server, through + * which the basic Modbus plugin configuration parameters may be changed + * + * This means that most values in the server_node_t and client_node_t + * may be changed after the co,piled code (.so file) is loaded into + * memory, and before the code starts executing. + * Since the we will also want to change the host and port (TCP) and the + * serial device (RTU) at this time, it is best if we allocate memory for + * these strings that may be overwritten by the web server (i.e., do not use + * const strings) in the server_node_t and client_node_t structures. + * + * The following structure members + * - node_addr_t.addr.tcp.host + * - node_addr_t.addr.tcp.service (i.e. the port) + * - node_addr_t.addr.rtu.device + * are all char *, and do not allocate memory for the strings. + * + * We therefore include two generic char arrays, str1 and str2, + * that will store the above strings, and the C code will initiliaze + * the node_addre_t.addr string pointers to these strings. + * i.e., either addr.rtu.device will point to str1, + * or + * addr.tcp.host and addr.tcp.service + * will point to str1 and str2 respectively + */ typedef struct{ const char *location; + const char *config_name; u8 slave_id; node_addr_t node_address; int mb_nd; // modbus library node used for this server @@ -53,6 +95,9 @@ // Used by the Modbus client node typedef struct{ const char *location; + const char *config_name; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; node_addr_t node_address; int mb_nd; // modbus library node used for this client int init_state; // store how far along the client's initialization has progressed diff -r db68cb0e6bdc -r 7575050a80c5 modbus/mb_utils.py --- a/modbus/mb_utils.py Thu May 28 11:15:22 2020 +0100 +++ b/modbus/mb_utils.py Mon Jun 01 08:54:26 2020 +0100 @@ -57,10 +57,10 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", %(slaveid)s, {naf_tcp, {.tcp = {%(host)s, "%(port)s", DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - host, port, slaveid = GetCTVals(child, range(3)) +{"%(locnodestr)s", "%(config_name)s", %(slaveid)s, {naf_tcp, {.tcp = {%(host)s, "%(port)s", DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, host, port, slaveid = GetCTVals(child, range(4)) if host == "#ANY#": host = 'INADDR_ANY' else: @@ -71,6 +71,7 @@ # return None node_dict = {"locnodestr": location, + "config_name": config_name, "host": host, "port": port, "slaveid": slaveid} @@ -120,12 +121,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", %(slaveid)s, {naf_rtu, {.rtu = {"%(device)s", %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - device, baud, parity, stopbits, slaveid = GetCTVals(child, range(5)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", %(slaveid)s, {naf_rtu, {.rtu = {"%(device)s", %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, device, baud, parity, stopbits, slaveid = GetCTVals(child, range(6)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "device": device, "baud": baud, "parity": modbus_serial_parity_dict[parity], @@ -140,12 +142,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", {naf_rtu, {.rtu = {"%(device)s", %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - device, baud, parity, stopbits, coms_period = GetCTVals(child, range(5)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", "%(device)s", "", {naf_rtu, {.rtu = {NULL, %(baud)s /*baud*/, %(parity)s /*parity*/, 8 /*data bits*/, %(stopbits)s, 0 /* ignore echo */}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, device, baud, parity, stopbits, coms_period = GetCTVals(child, range(6)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "device": device, "baud": baud, "parity": modbus_serial_parity_dict[parity], @@ -160,12 +163,13 @@ params: child - the correspondent subplugin in Beremiz """ node_init_template = '''/*node %(locnodestr)s*/ -{"%(locnodestr)s", {naf_tcp, {.tcp = {"%(host)s", "%(port)s", DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */, 0 /* prev_error */}''' - - location = ".".join(map(str, child.GetCurrentLocation())) - host, port, coms_period = GetCTVals(child, range(3)) - - node_dict = {"locnodestr": location, +{"%(locnodestr)s", "%(config_name)s", "%(host)s", "%(port)s", {naf_tcp, {.tcp = {NULL, NULL, DEF_CLOSE_ON_SILENCE}}}, -1 /* mb_nd */, 0 /* init_state */, %(coms_period)s /* communication period */, 0 /* prev_error */}''' + + location = ".".join(map(str, child.GetCurrentLocation())) + config_name, host, port, coms_period = GetCTVals(child, range(4)) + + node_dict = {"locnodestr": location, + "config_name": config_name, "host": host, "port": port, "coms_period": coms_period} @@ -184,7 +188,7 @@ req_init_template = '''/*request %(locreqstr)s*/ {"%(locreqstr)s", %(nodeid)s, %(slaveid)s, %(iotype)s, %(func_nr)s, %(address)s , %(count)s, -DEF_REQ_SEND_RETRIES, 0 /* error_code */, 0 /* prev_code */, {%(timeout_s)d, %(timeout_ns)d} /* timeout */, %(write_on_change)d /* write_on_change */, +DEF_REQ_SEND_RETRIES, 0 /* error_code */, 0 /* prev_code */, {%(timeout_s)d, %(timeout_ns)d} /* timeout */, %(write_on_change)d /* write_on_change */, {%(buffer)s}, {%(buffer)s}}''' timeout = int(GetCTVal(child, 4)) diff -r db68cb0e6bdc -r 7575050a80c5 modbus/modbus.py --- a/modbus/modbus.py Thu May 28 11:15:22 2020 +0100 +++ b/modbus/modbus.py Mon Jun 01 08:54:26 2020 +0100 @@ -288,6 +288,7 @@ + @@ -308,6 +309,24 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusTCPclient" + + def __init__(self): + # NOTE: + # The ModbusTCPclient attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusTCPclient.setConfiguration_Name("Modbus TCP Client " + loc_str) + # Return the number of (modbus library) nodes this specific TCP client will need # return type: (tcp nodes, rtu nodes, ascii nodes) def GetNodeCount(self): @@ -345,6 +364,7 @@ + @@ -363,6 +383,23 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusTCPserver" + def __init__(self): + # NOTE: + # The ModbusServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusServerNode.setConfiguration_Name("Modbus TCP Server " + loc_str) + # Return the number of (modbus library) nodes this specific TCP server will need # return type: (tcp nodes, rtu nodes, ascii nodes) def GetNodeCount(self): @@ -404,6 +441,7 @@ + @@ -426,6 +464,23 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusRTUclient" + def __init__(self): + # NOTE: + # The ModbusRTUclient attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusRTUclient.setConfiguration_Name("Modbus RTU Client " + loc_str) + def GetParamsAttributes(self, path=None): infos = ConfigTreeNode.GetParamsAttributes(self, path=path) for element in infos: @@ -474,6 +529,7 @@ + @@ -494,6 +550,23 @@ # TODO: Replace with CTNType !!! PlugType = "ModbusRTUslave" + def __init__(self): + # NOTE: + # The ModbusRTUslave attribute is added dynamically by ConfigTreeNode._AddParamsMembers() + # It will be an XML parser object created by + # GenerateParserFromXSDstring(self.XSD).CreateRoot() + + # Set the default value for the "Configuration_Name" parameter + # The default value will need to be different for each instance of the + # _ModbusTCPclientPlug class, so we cannot hardcode the default value in the XSD above + # This value will be used by the web interface + # (i.e. the extension to the web server used to configure the Modbus parameters). + # (The web server is run/activated/started by Beremiz_service.py) + # (The web server code is found in runtime/NevowServer.py) + # (The Modbus extension to the web server is found in runtime/Modbus_config.py) + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.ModbusRTUslave.setConfiguration_Name("Modbus RTU Slave " + loc_str) + def GetParamsAttributes(self, path=None): infos = ConfigTreeNode.GetParamsAttributes(self, path=path) for element in infos: diff -r db68cb0e6bdc -r 7575050a80c5 runtime/Modbus_config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Modbus_config.py Mon Jun 01 08:54:26 2020 +0100 @@ -0,0 +1,569 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of Beremiz runtime. +# +# Copyright (C) 2020: Mario de Sousa +# +# See COPYING.Runtime file for copyrights details. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. + +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. + +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + + + +############################################################################################## +# This file implements an extension to the web server embedded in the Beremiz_service.py # +# runtime manager (webserver is in runtime/NevowServer.py). # +# # +# The extension implemented in this file allows for runtime configuration # +# of Modbus plugin parameters # +############################################################################################## + + + +import json +import os +import ctypes +import string +import hashlib + +from formless import annotate, webform + + + +# reference to the PLCObject in runtime/PLCObject.py +# PLCObject is a singleton, created in runtime/__init__.py +_plcobj = None + +# reference to the Nevow web server (a.k.a as NS in Beremiz_service.py) +# (Note that NS will reference the NevowServer.py _module_, and not an object/class) +_NS = None + + +# WorkingDir: the directory on which Beremiz_service.py is running, and where +# all the files downloaded to the PLC get stored +_WorkingDir = None + +# Directory in which to store the persistent configurations +# Should be a directory that does not get wiped on reboot! +_ModbusConfFiledir = "/tmp" + +# Will contain references to the C functions +# (implemented in beremiz/modbus/mb_runtime.c) +# used to get/set the Modbus specific configuration paramters +GetParamFuncs = {} +SetParamFuncs = {} + + +# List of all TCP clients configured in the loaded PLC (i.e. the .so file loaded into memory) +# Each entry will be a dictionary. See _Add_TCP_Client() for the data structure details... +_TCPclient_list = [] + + + + +# Paramters 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) +General_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # (annotate.String, + # annotate.Integer, ...) + ("config_name" , _("") , ctypes.c_char_p, annotate.String), + ("addr_type" , _("") , ctypes.c_char_p, annotate.String) + ] + +TCPclient_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # (annotate.String, + # annotate.Integer, ...) + ("host" , _("Remote IP Address") , ctypes.c_char_p, annotate.String), + ("port" , _("Remote Port Number") , ctypes.c_char_p, annotate.String), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer) + ] + +RTUclient_parameters = [ + # param. name label ctype type annotate type + # (C code var name) (used on web interface) (C data type) (web data type) + # (annotate.String, + # annotate.Integer, ...) + ("device" , _("Serial Port") , ctypes.c_char_p, annotate.String), + ("baud" , _("Baud Rate") , ctypes.c_int, annotate.Integer), + ("parity" , _("Parity") , ctypes.c_int, annotate.Integer), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, annotate.Integer), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer) + ] + + +# Note: the dictionary key must be the same as the string returned by the +# __modbus_get_ClientNode_addr_type() +# __modbus_get_ServerNode_addr_type() +# functions implemented in C (see modbus/mb_runtime.c) +_client_parameters = {} +_client_parameters["tcp" ] = TCPclient_parameters +_client_parameters["rtu" ] = RTUclient_parameters +_client_parameters["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) + + +#def _CheckPortnumber(port_number): +# """ check validity of the port number """ +# try: +# portnum = int(port_number) +# if (portnum < 0) or (portnum > 65535): +# raise Exception +# except Exception: +# return False +# +# return True + + + + +#def _CheckConfiguration(BACnetConfig): +# res = True +# res = res and _CheckPortnumber(BACnetConfig["port_number"]) +# res = res and _CheckDeviceID (BACnetConfig["device_id"]) +# return res + + + +#def _CheckWebConfiguration(BACnetConfig): +# res = True +# +# # check the port number +# if not _CheckPortnumber(BACnetConfig["port_number"]): +# raise annotate.ValidateError( +# {"port_number": "Invalid port number: " + str(BACnetConfig["port_number"])}, +# _("Modbus configuration error:")) +# res = False +# +# if not _CheckDeviceID(BACnetConfig["device_id"]): +# raise annotate.ValidateError( +# {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])}, +# _("Modbus configuration error:")) +# res = False +# +# return res + + + + + + +def _SetSavedConfiguration(node_id, newConfig): + """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """ + + filename = _TCPclient_list[node_id]["filename"] + + with open(os.path.realpath(filename), 'w') as f: + json.dump(newConfig, f, sort_keys=True, indent=4) + + _TCPclient_list[node_id]["SavedConfiguration"] = newConfig + + + + +def _DelSavedConfiguration(node_id): + """ Deletes the file cotaining the persistent Modbus configuration """ + filename = _TCPclient_list[node_id]["filename"] + + if os.path.exists(filename): + os.remove(filename) + + + + +def _GetSavedConfiguration(node_id): + """ + Returns a dictionary containing the Modbus parameter configuration + that was last saved to file. If no file exists, then return None + """ + filename = _TCPclient_list[node_id]["filename"] + try: + #if os.path.isfile(filename): + saved_config = json.load(open(filename)) + except Exception: + return None + + #if _CheckConfiguration(saved_config): + # return saved_config + #else: + # return None + + return saved_config + + + +def _GetPLCConfiguration(node_id): + """ + Returns a dictionary containing the current Modbus parameter configuration + stored in the C variables in the loaded PLC (.so file) + """ + current_config = {} + addr_type = _TCPclient_list[node_id]["addr_type"] + + for par_name, x1, x2, x3 in _client_parameters[addr_type]: + value = GetParamFuncs[par_name](node_id) + if value is not None: + current_config[par_name] = value + + return current_config + + + +def _SetPLCConfiguration(node_id, newconfig): + """ + Stores the Modbus parameter configuration into the + the C variables in the loaded PLC (.so file) + """ + addr_type = _TCPclient_list[node_id]["addr_type"] + + for par_name in newconfig: + value = newconfig[par_name] + if value is not None: + SetParamFuncs[par_name](node_id, value) + + + + +def _GetWebviewConfigurationValue(ctx, node_id, argument): + """ + Callback function, called by the web interface (NevowServer.py) + to fill in the default value of each parameter of the web form + + Note that the real callback function is a dynamically created function that + will simply call this function to do the work. It will also pass the node_id + as a parameter. + """ + try: + return _TCPclient_list[node_id]["WebviewConfiguration"][argument.name] + except Exception: + return "" + + + + +def _updateWebInterface(node_id): + """ + Add/Remove buttons to/from the web interface depending on the current state + - If there is a saved state => add a delete saved state button + """ + + config_hash = _TCPclient_list[node_id]["config_hash"] + config_name = _TCPclient_list[node_id]["config_name"] + + # Add a "Delete Saved Configuration" button if there is a saved configuration! + if _TCPclient_list[node_id]["SavedConfiguration"] is None: + _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) + else: + def __OnButtonDel(**kwargs): + return OnButtonDel(node_id = node_id, **kwargs) + + _NS.ConfigurableSettings.addSettings( + "ModbusConfigDelSaved" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + [], # fields (empty, no parameters required!) + _("Delete Configuration Stored in Persistent Storage"), # button label + __OnButtonDel, # callback + "ModbusConfigParm" + config_hash) # Add after entry xxxx + + + +def OnButtonSave(**kwargs): + """ + Function called when user clicks 'Save' button in web interface + The function will configure the Modbus plugin in the PLC with the values + specified in the web interface. However, values must be validated first! + + Note that this function does not get called directly. The real callback + function is the dynamic __OnButtonSave() function, which will add the + "node_id" argument, and call this function to do the work. + """ + + #_plcobj.LogMessage("Modbus web server extension::OnButtonSave() Called") + + newConfig = {} + node_id = kwargs.get("node_id", None) + addr_type = _TCPclient_list[node_id]["addr_type"] + + for par_name, x1, x2, x3 in _client_parameters[addr_type]: + value = kwargs.get(par_name, None) + if value is not None: + newConfig[par_name] = value + + _TCPclient_list[node_id]["WebviewConfiguration"] = newConfig + + # First check if configuration is OK. + ## TODO... + #if not _CheckWebConfiguration(newConfig): + # return + + # store to file the new configuration so that + # we can recoup the configuration the next time the PLC + # has a cold start (i.e. when Beremiz_service.py is retarted) + _SetSavedConfiguration(node_id, newConfig) + + # Configure PLC with the current Modbus parameters + _SetPLCConfiguration(node_id, newConfig) + + # File has just been created => Delete button must be shown on web interface! + _updateWebInterface(node_id) + + + + +def OnButtonDel(**kwargs): + """ + Function called when user clicks 'Delete' button in web interface + The function will delete the file containing the persistent + Modbus configution + """ + + node_id = kwargs.get("node_id", None) + + # Delete the file + _DelSavedConfiguration(node_id) + + # Set the current configuration to the default (hardcoded in C) + new_config = _TCPclient_list[node_id]["DefaultConfiguration"] + _SetPLCConfiguration(node_id, new_config) + + #Update the webviewconfiguration + _TCPclient_list[node_id]["WebviewConfiguration"] = new_config + + # Reset SavedConfiguration + _TCPclient_list[node_id]["SavedConfiguration"] = None + + # File has just been deleted => Delete button on web interface no longer needed! + _updateWebInterface(node_id) + + + + +def OnButtonShowCur(**kwargs): + """ + Function called when user clicks 'Show Current PLC Configuration' button in web interface + The function will load the current PLC configuration into the web form + + Note that this function does not get called directly. The real callback + function is the dynamic __OnButtonShowCur() function, which will add the + "node_id" argument, and call this function to do the work. + """ + node_id = kwargs.get("node_id", None) + + _TCPclient_list[node_id]["WebviewConfiguration"] = _GetPLCConfiguration(node_id) + + + + +def _Load_TCP_Client(node_id): + TCPclient_entry = {} + + config_name = GetParamFuncs["config_name"](node_id) + # addr_type will be one of "tcp", "rtu" or "ascii" + addr_type = GetParamFuncs["addr_type" ](node_id) + # For some operations we cannot use the config name (e.g. filename to store config) + # because the user may be using characters that are invalid for that purpose ('/' for + # example), so we create a hash of the config_name, and use that instead. + config_hash = hashlib.md5(config_name).hexdigest() + + _plcobj.LogMessage("Modbus web server extension::_Load_TCP_Client("+str(node_id)+") config_name="+config_name) + + # Add the new entry to the global list + # Note: it is OK, and actually necessary, to do this _before_ seting all the parameters in TCPclient_entry + # TCPclient_entry will be stored as a reference, so we can insert parameters at will. + global _TCPclient_list + _TCPclient_list.append(TCPclient_entry) + + # store all node_id relevant data for future reference + TCPclient_entry["node_id" ] = node_id + TCPclient_entry["config_name" ] = config_name + TCPclient_entry["addr_type" ] = addr_type + TCPclient_entry["config_hash" ] = config_hash + TCPclient_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json") + + # Dictionary that contains the Modbus configuration currently being shown + # on the web interface + # This configuration will almost always be identical to the current + # configuration in the PLC (i.e., the current state stored in the + # C variables in the .so file). + # The configuration viewed on the web will only be different to the current + # configuration when the user edits the configuration, and when + # the user asks to save an edited configuration that contains an error. + TCPclient_entry["WebviewConfiguration"] = None + + # Upon PLC load, this Dictionary is initialised with the Modbus configuration + # hardcoded in the C file + # (i.e. the configuration inserted in Beremiz IDE when project was compiled) + TCPclient_entry["DefaultConfiguration"] = _GetPLCConfiguration(node_id) + TCPclient_entry["WebviewConfiguration"] = TCPclient_entry["DefaultConfiguration"] + + # Dictionary that stores the Modbus configuration currently stored in a file + # Currently only used to decide whether or not to show the "Delete" button on the + # web interface (only shown if _SavedConfiguration is not None) + SavedConfig = _GetSavedConfiguration(node_id) + TCPclient_entry["SavedConfiguration"] = SavedConfig + + if SavedConfig is not None: + _SetPLCConfiguration(node_id, SavedConfig) + TCPclient_entry["WebviewConfiguration"] = SavedConfig + + # Define the format for the web form used to show/change the current parameters + # We first declare a dynamic function to work as callback to obtain the default values for each parameter + def __GetWebviewConfigurationValue(ctx, argument): + return _GetWebviewConfigurationValue(ctx, node_id, argument) + + webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue)) + for name, web_label, c_dtype, web_dtype in _client_parameters[addr_type]] + + # Configure the web interface to include the Modbus config parameters + def __OnButtonSave(**kwargs): + OnButtonSave(node_id=node_id, **kwargs) + + _NS.ConfigurableSettings.addSettings( + "ModbusConfigParm" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + webFormInterface, # fields + _("Save Configuration to Persistent Storage"), # button label + __OnButtonSave) # callback + + # Add a "View Current Configuration" button + def __OnButtonShowCur(**kwargs): + OnButtonShowCur(node_id=node_id, **kwargs) + + _NS.ConfigurableSettings.addSettings( + "ModbusConfigViewCur" + config_hash, # name (internal, may not contain spaces, ...) + _("Modbus Configuration: ") + config_name, # description (user visible label) + [], # fields (empty, no parameters required!) + _("Show Current PLC Configuration"), # button label + __OnButtonShowCur) # callback + + # Add the Delete button to the web interface, if required + _updateWebInterface(node_id) + + + + +def OnLoadPLC(): + """ + Callback function, called (by PLCObject.py) when a new PLC program + (i.e. XXX.so file) is transfered to the PLC runtime + and loaded into memory + """ + + #_plcobj.LogMessage("Modbus web server extension::OnLoadPLC() Called...") + + if _plcobj.PLClibraryHandle is None: + # PLC was loaded but we don't have access to the library of compiled code (.so lib)? + # Hmm... This shold never occur!! + return + + # Get the number of Modbus Client and Servers (Modbus plugin) + # configured in the currently loaded PLC project (i.e., the .so file) + # If the "__modbus_plugin_client_node_count" + # or the "__modbus_plugin_server_node_count" C variables + # are not present in the .so file we conclude that the currently loaded + # PLC does not have the Modbus plugin included (situation (2b) described above init()) + try: + client_count = ctypes.c_int.in_dll(_plcobj.PLClibraryHandle, "__modbus_plugin_client_node_count").value + server_count = ctypes.c_int.in_dll(_plcobj.PLClibraryHandle, "__modbus_plugin_server_node_count").value + except Exception: + # Loaded PLC does not have the Modbus plugin => nothing to do + # (i.e. do _not_ configure and make available the Modbus web interface) + return + + if client_count < 0: client_count = 0 + if server_count < 0: server_count = 0 + + if (client_count == 0) and (server_count == 0): + # The Modbus plugin in the loaded PLC does not have any client and servers configured + # => nothing to do (i.e. do _not_ configure and make available the Modbus web interface) + return + + # Map the get/set functions (written in C code) we will be using to get/set the configuration parameters + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters: + GetParamFuncName = "__modbus_get_ClientNode_" + name + GetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, GetParamFuncName) + GetParamFuncs[name].restype = c_dtype + GetParamFuncs[name].argtypes = [ctypes.c_int] + + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters: + SetParamFuncName = "__modbus_set_ClientNode_" + name + SetParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, SetParamFuncName) + SetParamFuncs[name].restype = None + SetParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + + for node_id in range(client_count): + _Load_TCP_Client(node_id) + + + + + + +def OnUnLoadPLC(): + """ + # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory + """ + + #_plcobj.LogMessage("Modbus web server extension::OnUnLoadPLC() Called...") + + # Delete the Modbus specific web interface extensions + # (Safe to ask to delete, even if it has not been added!) + global _TCPclient_list + for TCPclient_entry in _TCPclient_list: + config_hash = TCPclient_entry["config_hash"] + _NS.ConfigurableSettings.delSettings("ModbusConfigParm" + config_hash) + _NS.ConfigurableSettings.delSettings("ModbusConfigViewCur" + config_hash) + _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) + + # Dele all entries... + _TCPclient_list = [] + + + +# The Beremiz_service.py service, along with the integrated web server it launches +# (i.e. Nevow web server, in runtime/NevowServer.py), will go through several states +# once started: +# (1) Web server is started, but no PLC is loaded +# (2) PLC is loaded (i.e. the PLC compiled code is loaded) +# (a) The loaded PLC includes the Modbus plugin +# (b) The loaded PLC does not have the Modbus plugin +# +# During (1) and (2a): +# we configure the web server interface to not have the Modbus web configuration extension +# During (2b) +# we configure the web server interface to include the Modbus web configuration extension +# +# PS: reference to the pyroserver (i.e., the server object of Beremiz_service.py) +# (NOTE: PS.plcobj is a reference to PLCObject.py) +# NS: reference to the web server (i.e. the NevowServer.py module) +# WorkingDir: the directory on which Beremiz_service.py is running, and where +# all the files downloaded to the PLC get stored, including +# the .so file with the compiled C generated code +def init(plcobj, NS, WorkingDir): + #PS.plcobj.LogMessage("Modbus web server extension::init(PS, NS, " + WorkingDir + ") Called") + global _WorkingDir + _WorkingDir = WorkingDir + global _plcobj + _plcobj = plcobj + global _NS + _NS = NS + + _plcobj.RegisterCallbackLoad ("Modbus_Settins_Extension", OnLoadPLC) + _plcobj.RegisterCallbackUnLoad("Modbus_Settins_Extension", OnUnLoadPLC) + OnUnLoadPLC() # init is called before the PLC gets loaded... so we make sure we have the correct state diff -r db68cb0e6bdc -r 7575050a80c5 runtime/NevowServer.py --- a/runtime/NevowServer.py Thu May 28 11:15:22 2020 +0100 +++ b/runtime/NevowServer.py Mon Jun 01 08:54:26 2020 +0100 @@ -165,7 +165,8 @@ setattr(self, 'bind_' + name, _bind) self.bindingsNames.append(name) - def addSettings(self, name, desc, fields, btnlabel, callback): + def addSettings(self, name, desc, fields, btnlabel, callback, + addAfterName = None): def _bind(ctx): return annotate.MethodBinding( 'action_' + name, @@ -179,8 +180,23 @@ setattr(self, 'action_' + name, callback) - if name not in self.bindingsNames: - self.bindingsNames.append(name) + if addAfterName not in self.bindingsNames: + # Just append new setting if not yet present + if name not in self.bindingsNames: + self.bindingsNames.append(name) + else: + # We need to insert new setting + # imediately _after_ addAfterName + + # First remove new setting if already present + # to make sure it goes into correct place + if name in self.bindingsNames: + self.bindingsNames.remove(name) + # Now add new setting in correct place + self.bindingsNames.insert( + self.bindingsNames.index(addAfterName)+1, + name) + def delSettings(self, name):