# HG changeset patch # User Mario de Sousa # Date 1591429421 -3600 # Node ID 09bac1f52b1ea4040f46b3df6ece5f7e80347892 # Parent 449c9539887a9df270ee962816646778be4230a0# Parent fdca999c0c1ab55ccd0cd7bb9efdfb879e317824 merge diff -r 449c9539887a -r 09bac1f52b1e Beremiz_service.py --- a/Beremiz_service.py Thu May 28 11:16:59 2020 +0100 +++ b/Beremiz_service.py Sat Jun 06 08:43:41 2020 +0100 @@ -493,6 +493,7 @@ installThreadExcepthook() havewamp = False haveBNconf = False +haveMBconf = False if havetwisted: @@ -512,7 +513,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 @@ -557,6 +567,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 449c9539887a -r 09bac1f52b1e modbus/mb_runtime.c --- a/modbus/mb_runtime.c Thu May 28 11:16:59 2020 +0100 +++ b/modbus/mb_runtime.c Sat Jun 06 08:43:41 2020 +0100 @@ -493,14 +493,37 @@ 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; + /* see comment in mb_runtime.h to understad why we need to initialize these entries */ + switch (server_nodes[index].node_address.naf) { + case naf_tcp: + server_nodes[index].node_address.addr.tcp.host = server_nodes[index].str1; + server_nodes[index].node_address.addr.tcp.service = server_nodes[index].str2; + break; + case naf_rtu: + server_nodes[index].node_address.addr.rtu.device = server_nodes[index].str1; + break; + } + } /* modbus library init */ /* Note that TOTAL_xxxNODE_COUNT are the nodes required by _ALL_ the instances of the modbus @@ -815,3 +838,121 @@ 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].str1; } +const char * __modbus_get_ServerNode_port (int nodeid) {return server_nodes[nodeid].str2; } +const char * __modbus_get_ServerNode_device (int nodeid) {return server_nodes[nodeid].str1; } +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;} +u8 __modbus_get_ServerNode_slave_id (int nodeid) {return server_nodes[nodeid].slave_id; } +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;} + + +void __modbus_set_ServerNode_host (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_port (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str2, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_device (int nodeid, const char * value) {__safe_strcnpy(server_nodes[nodeid].str1, value, MODBUS_PARAM_STRING_SIZE);} +void __modbus_set_ServerNode_baud (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.baud = value;} +void __modbus_set_ServerNode_parity (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.parity = value;} +void __modbus_set_ServerNode_stop_bits (int nodeid, int value) {server_nodes[nodeid].node_address.addr.rtu.stop_bits = value;} +void __modbus_set_ServerNode_slave_id (int nodeid, u8 value) {server_nodes[nodeid].slave_id = value;} + diff -r 449c9539887a -r 09bac1f52b1e modbus/mb_runtime.h --- a/modbus/mb_runtime.h Thu May 28 11:16:59 2020 +0100 +++ b/modbus/mb_runtime.h Sat Jun 06 08:43:41 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,48 @@ 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; + char str1[MODBUS_PARAM_STRING_SIZE]; + char str2[MODBUS_PARAM_STRING_SIZE]; u8 slave_id; node_addr_t node_address; int mb_nd; // modbus library node used for this server @@ -53,6 +97,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 449c9539887a -r 09bac1f52b1e modbus/mb_utils.py --- a/modbus/mb_utils.py Thu May 28 11:16:59 2020 +0100 +++ b/modbus/mb_utils.py Sat Jun 06 08:43:41 2020 +0100 @@ -57,20 +57,19 @@ 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", "%(host)s", "%(port)s", %(slaveid)s, {naf_tcp, {.tcp = {NULL, NULL, 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: - host = '"' + host + '"' + host = '' # slaveid = GetCTVal(child, 2) # if int(slaveid) not in xrange(256): # self.GetCTRoot().logger.write_error("Error: Wrong slave ID in %s server node\nModbus Plugin C code returns empty\n"%location) # return None node_dict = {"locnodestr": location, + "config_name": config_name, "host": host, "port": port, "slaveid": slaveid} @@ -120,12 +119,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", "%(device)s", "",%(slaveid)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 */}''' + + 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 +140,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 +161,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 +186,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 449c9539887a -r 09bac1f52b1e modbus/modbus.py --- a/modbus/modbus.py Thu May 28 11:16:59 2020 +0100 +++ b/modbus/modbus.py Sat Jun 06 08:43:41 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 449c9539887a -r 09bac1f52b1e runtime/Modbus_config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/runtime/Modbus_config.py Sat Jun 06 08:43:41 2020 +0100 @@ -0,0 +1,690 @@ +#!/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" + +# List of all Web Extension Setting nodes we are handling. +# One WebNode each for: +# - Modbus TCP client +# - Modbus TCP server +# - Modbus RTU client +# - Modbus RTU slave +# configured in the loaded PLC (i.e. the .so file loaded into memory) +# Each entry will be a dictionary. See _AddWebNode() for the details +# of the data structure in each entry. +_WebNodeList = [] + + + +class MB_StopBits(annotate.Choice): + _choices = [0, 1, 2] + + def coerce(self, val, configurable): + return int(val) + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) + + +class MB_Baud(annotate.Choice): + _choices = [110, 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200] + + def coerce(self, val, configurable): + return int(val) + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, choices = self._choices, *args, **kwargs) + + +class MB_Parity(annotate.Choice): + # Warning: do _not_ name this variable choice[] without underscore, as that name is + # already used for another similar variable by the underlying class annotate.Choice + _choices = [ 0, 1, 2 ] + _label = ["none", "odd", "even"] + + def choice_to_label(self, key): + #_plcobj.LogMessage("Modbus web server extension::choice_to_label() " + str(key)) + return self._label[key] + + def coerce(self, val, configurable): + """Coerce a value with the help of an object, which is the object + we are configuring. + """ + # Basically, make sure the value the user introduced is valid, and transform + # into something that is valid if necessary or mark it as an error + # (by raising an exception ??). + # + # We are simply using this functions to transform the input value (a string) + # into an integer. Note that although the available options are all + # integers (0, 1 or 2), even though what is shown on the user interface + # are actually strings, i.e. the labels), these parameters are for some + # reason being parsed as strings, so we need to map them back to an + # integer. + # + #_plcobj.LogMessage("Modbus web server extension::coerce " + val ) + return int(val) + + def __init__(self, *args, **kwargs): + annotate.Choice.__init__(self, + choices = self._choices, + stringify = self.choice_to_label, + *args, **kwargs) + + + +# Parameters we will need to get from the C code, but that will not be shown +# on the web interface. Common to all modbus entry types (client/server, tcp/rtu/ascii) +# +# The annotate type entry is basically useless and is completely ignored. +# We kee that entry so that this list can later be correctly merged with the +# following lists... +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, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("comm_period" , _("Invocation Rate (ms)") , ctypes.c_ulonglong, annotate.Integer) + ] + +TCPserver_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" , _("Local IP Address") , ctypes.c_char_p, annotate.String), + ("port" , _("Local Port Number") , ctypes.c_char_p, annotate.String), + ("slave_id" , _("Slave ID") , ctypes.c_ubyte, annotate.Integer) + ] + +RTUslave_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, MB_Baud ), + ("parity" , _("Parity") , ctypes.c_int, MB_Parity ), + ("stop_bits" , _("Stop Bits") , ctypes.c_int, MB_StopBits ), + ("slave_id" , _("Slave ID") , ctypes.c_ulonglong, annotate.Integer) + ] + + + + +# Dictionary containing List of Web viewable parameters +# 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_WebParamListDict = {} +_client_WebParamListDict["tcp" ] = TCPclient_parameters +_client_WebParamListDict["rtu" ] = RTUclient_parameters +_client_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) + +_server_WebParamListDict = {} +_server_WebParamListDict["tcp" ] = TCPserver_parameters +_server_WebParamListDict["rtu" ] = RTUslave_parameters +_server_WebParamListDict["ascii"] = [] # (Note: ascii not yet implemented in Beremiz modbus plugin) + +WebParamListDictDict = {} +WebParamListDictDict['client'] = _client_WebParamListDict +WebParamListDictDict['server'] = _server_WebParamListDict + + + + + + +def _SetSavedConfiguration(WebNode_id, newConfig): + """ Stores a dictionary in a persistant file containing the Modbus parameter configuration """ + + # Add the addr_type and node_type to the data that will be saved to file + # This allows us to confirm the saved data contains the correct addr_type + # when loading from file + save_info = {} + save_info["addr_type"] = _WebNodeList[WebNode_id]["addr_type"] + save_info["node_type"] = _WebNodeList[WebNode_id]["node_type"] + save_info["config" ] = newConfig + + filename = _WebNodeList[WebNode_id]["filename"] + + with open(os.path.realpath(filename), 'w') as f: + json.dump(save_info, f, sort_keys=True, indent=4) + + _WebNodeList[WebNode_id]["SavedConfiguration"] = newConfig + + + + +def _DelSavedConfiguration(WebNode_id): + """ Deletes the file cotaining the persistent Modbus configuration """ + filename = _WebNodeList[WebNode_id]["filename"] + + if os.path.exists(filename): + os.remove(filename) + + + + +def _GetSavedConfiguration(WebNode_id): + """ + Returns a dictionary containing the Modbus parameter configuration + that was last saved to file. If no file exists, or file contains + wrong addr_type (i.e. 'tcp', 'rtu' or 'ascii' -> does not match the + addr_type of the WebNode_id), then return None + """ + filename = _WebNodeList[WebNode_id]["filename"] + try: + #if os.path.isfile(filename): + save_info = json.load(open(filename)) + except Exception: + return None + + if save_info["addr_type"] != _WebNodeList[WebNode_id]["addr_type"]: + return None + if save_info["node_type"] != _WebNodeList[WebNode_id]["node_type"]: + return None + if "config" not in save_info: + return None + + saved_config = save_info["config"] + + #if _CheckConfiguration(saved_config): + # return saved_config + #else: + # return None + + return saved_config + + + +def _GetPLCConfiguration(WebNode_id): + """ + Returns a dictionary containing the current Modbus parameter configuration + stored in the C variables in the loaded PLC (.so file) + """ + current_config = {} + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + GetParamFuncs = _WebNodeList[WebNode_id]["GetParamFuncs"] + + for par_name, x1, x2, x3 in WebParamList: + value = GetParamFuncs[par_name](C_node_id) + if value is not None: + current_config[par_name] = value + + return current_config + + + +def _SetPLCConfiguration(WebNode_id, newconfig): + """ + Stores the Modbus parameter configuration into the + the C variables in the loaded PLC (.so file) + """ + C_node_id = _WebNodeList[WebNode_id]["C_node_id"] + SetParamFuncs = _WebNodeList[WebNode_id]["SetParamFuncs"] + + for par_name in newconfig: + value = newconfig[par_name] + if value is not None: + SetParamFuncs[par_name](C_node_id, value) + + + + +def _GetWebviewConfigurationValue(ctx, WebNode_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 WebNode_id + as a parameter. + """ + try: + return _WebNodeList[WebNode_id]["WebviewConfiguration"][argument.name] + except Exception: + return "" + + + + +def _updateWebInterface(WebNode_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 = _WebNodeList[WebNode_id]["config_hash"] + config_name = _WebNodeList[WebNode_id]["config_name"] + + # Add a "Delete Saved Configuration" button if there is a saved configuration! + if _WebNodeList[WebNode_id]["SavedConfiguration"] is None: + _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) + else: + def __OnButtonDel(**kwargs): + return OnButtonDel(WebNode_id = WebNode_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 + "WebNode_id" argument, and call this function to do the work. + """ + + #_plcobj.LogMessage("Modbus web server extension::OnButtonSave() Called") + + newConfig = {} + WebNode_id = kwargs.get("WebNode_id", None) + WebParamList = _WebNodeList[WebNode_id]["WebParamList"] + + for par_name, x1, x2, x3 in WebParamList: + value = kwargs.get(par_name, None) + if value is not None: + newConfig[par_name] = value + + _WebNodeList[WebNode_id]["WebviewConfiguration"] = newConfig + + # First check if configuration is OK. + # Note that this is not currently required, as we use drop down choice menus + # for baud, parity and sop bits, so the values should always be correct! + #if not _CheckWebConfiguration(newConfig): + # return + + # 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(WebNode_id, newConfig) + + # Configure PLC with the current Modbus parameters + _SetPLCConfiguration(WebNode_id, newConfig) + + # File has just been created => Delete button must be shown on web interface! + _updateWebInterface(WebNode_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 + """ + + WebNode_id = kwargs.get("WebNode_id", None) + + # Delete the file + _DelSavedConfiguration(WebNode_id) + + # Set the current configuration to the default (hardcoded in C) + new_config = _WebNodeList[WebNode_id]["DefaultConfiguration"] + _SetPLCConfiguration(WebNode_id, new_config) + + #Update the webviewconfiguration + _WebNodeList[WebNode_id]["WebviewConfiguration"] = new_config + + # Reset SavedConfiguration + _WebNodeList[WebNode_id]["SavedConfiguration"] = None + + # File has just been deleted => Delete button on web interface no longer needed! + _updateWebInterface(WebNode_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 + "WebNode_id" argument, and call this function to do the work. + """ + WebNode_id = kwargs.get("WebNode_id", None) + + _WebNodeList[WebNode_id]["WebviewConfiguration"] = _GetPLCConfiguration(WebNode_id) + + + + +def _AddWebNode(C_node_id, node_type, GetParamFuncs, SetParamFuncs): + """ + Load from the compiled code (.so file, aloready loaded into memmory) + the configuration parameters of a specific Modbus plugin node. + This function works with both client and server nodes, depending on the + Get/SetParamFunc dictionaries passed to it (either the client or the server + node versions of the Get/Set functions) + """ + WebNode_entry = {} + + # Get the config_name from the C code... + config_name = GetParamFuncs["config_name"](C_node_id) + # Get the addr_type from the C code... + # addr_type will be one of "tcp", "rtu" or "ascii" + addr_type = GetParamFuncs["addr_type" ](C_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::_AddWebNode("+str(C_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 WebNode_entry + # WebNode_entry will be stored as a reference, so we can later insert parameters at will. + global _WebNodeList + _WebNodeList.append(WebNode_entry) + WebNode_id = len(_WebNodeList) - 1 + + # store all WebNode relevant data for future reference + # + # Note that "WebParamList" will reference one of: + # - TCPclient_parameters, TCPserver_parameters, RTUclient_parameters, RTUslave_parameters + WebNode_entry["C_node_id" ] = C_node_id + WebNode_entry["config_name" ] = config_name + WebNode_entry["config_hash" ] = config_hash + WebNode_entry["filename" ] = os.path.join(_ModbusConfFiledir, "Modbus_config_" + config_hash + ".json") + WebNode_entry["GetParamFuncs"] = GetParamFuncs + WebNode_entry["SetParamFuncs"] = SetParamFuncs + WebNode_entry["WebParamList" ] = WebParamListDictDict[node_type][addr_type] + WebNode_entry["addr_type" ] = addr_type # 'tcp', 'rtu', or 'ascii' (as returned by C function) + WebNode_entry["node_type" ] = node_type # 'client', 'server' + + + # 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. + WebNode_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) + WebNode_entry["DefaultConfiguration"] = _GetPLCConfiguration(WebNode_id) + WebNode_entry["WebviewConfiguration"] = WebNode_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(WebNode_id) + WebNode_entry["SavedConfiguration"] = SavedConfig + + if SavedConfig is not None: + _SetPLCConfiguration(WebNode_id, SavedConfig) + WebNode_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 + # Note: We transform every parameter into a string + # This is not strictly required for parameters of type annotate.Integer that will correctly + # accept the default value as an Integer python object + # This is obviously also not required for parameters of type annotate.String, that are + # always handled as strings. + # However, the annotate.Choice parameters (and all parameters that derive from it, + # sucn as Parity, Baud, etc.) require the default value as a string + # even though we store it as an integer, which is the data type expected + # by the set_***() C functions in mb_runtime.c + def __GetWebviewConfigurationValue(ctx, argument): + return str(_GetWebviewConfigurationValue(ctx, WebNode_id, argument)) + + webFormInterface = [(name, web_dtype (label=web_label, default=__GetWebviewConfigurationValue)) + for name, web_label, c_dtype, web_dtype in WebNode_entry["WebParamList"]] + + # Configure the web interface to include the Modbus config parameters + def __OnButtonSave(**kwargs): + OnButtonSave(WebNode_id=WebNode_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(WebNode_id=WebNode_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(WebNode_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 + # Will contain references to the C functions (implemented in beremiz/modbus/mb_runtime.c) + GetClientParamFuncs = {} + SetClientParamFuncs = {} + GetServerParamFuncs = {} + SetServerParamFuncs = {} + + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters + General_parameters: + ParamFuncName = "__modbus_get_ClientNode_" + name + GetClientParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName) + GetClientParamFuncs[name].restype = c_dtype + GetClientParamFuncs[name].argtypes = [ctypes.c_int] + + for name, web_label, c_dtype, web_dtype in TCPclient_parameters + RTUclient_parameters: + ParamFuncName = "__modbus_set_ClientNode_" + name + SetClientParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName) + SetClientParamFuncs[name].restype = None + SetClientParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters + General_parameters: + ParamFuncName = "__modbus_get_ServerNode_" + name + GetServerParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName) + GetServerParamFuncs[name].restype = c_dtype + GetServerParamFuncs[name].argtypes = [ctypes.c_int] + + for name, web_label, c_dtype, web_dtype in TCPserver_parameters + RTUslave_parameters: + ParamFuncName = "__modbus_set_ServerNode_" + name + SetServerParamFuncs[name] = getattr(_plcobj.PLClibraryHandle, ParamFuncName) + SetServerParamFuncs[name].restype = None + SetServerParamFuncs[name].argtypes = [ctypes.c_int, c_dtype] + + for node_id in range(client_count): + _AddWebNode(node_id, "client" ,GetClientParamFuncs, SetClientParamFuncs) + + for node_id in range(server_count): + _AddWebNode(node_id, "server", GetServerParamFuncs, SetServerParamFuncs) + + + + + +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 _WebNodeList + for WebNode_entry in _WebNodeList: + config_hash = WebNode_entry["config_hash"] + _NS.ConfigurableSettings.delSettings("ModbusConfigParm" + config_hash) + _NS.ConfigurableSettings.delSettings("ModbusConfigViewCur" + config_hash) + _NS.ConfigurableSettings.delSettings("ModbusConfigDelSaved" + config_hash) + + # Dele all entries... + _WebNodeList = [] + + + +# 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 449c9539887a -r 09bac1f52b1e runtime/NevowServer.py --- a/runtime/NevowServer.py Thu May 28 11:16:59 2020 +0100 +++ b/runtime/NevowServer.py Sat Jun 06 08:43:41 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):