Add web extension: configure Modbus plugin parameters (currently only supports Modbus clients)
authorMario de Sousa <msousa@fe.up.pt>
Mon, 01 Jun 2020 08:54:26 +0100
changeset 2654 7575050a80c5
parent 2649 db68cb0e6bdc
child 2655 d2b2ee04bfa1
Add web extension: configure Modbus plugin parameters (currently only supports Modbus clients)
Beremiz_service.py
modbus/mb_runtime.c
modbus/mb_runtime.h
modbus/mb_utils.py
modbus/modbus.py
runtime/Modbus_config.py
runtime/NevowServer.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)
--- 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;}
+                                                                                                                        
+
+
--- 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
--- 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))
--- 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 @@
     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:element name="ModbusTCPclient">
         <xsd:complexType>
+          <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
           <xsd:attribute name="Remote_IP_Address" type="xsd:string" use="optional" default="localhost"/>
           <xsd:attribute name="Remote_Port_Number" type="xsd:string" use="optional" default="502"/>
           <xsd:attribute name="Invocation_Rate_in_ms" use="optional" default="100">
@@ -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 @@
     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:element name="ModbusServerNode">
         <xsd:complexType>
+          <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
           <xsd:attribute name="Local_IP_Address" type="xsd:string" use="optional"  default="#ANY#"/>
           <xsd:attribute name="Local_Port_Number" type="xsd:string" use="optional" default="502"/>
           <xsd:attribute name="SlaveID" use="optional" default="0">
@@ -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 @@
     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:element name="ModbusRTUclient">
         <xsd:complexType>
+          <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
           <xsd:attribute name="Serial_Port" type="xsd:string"  use="optional" default="/dev/ttyS0"/>
           <xsd:attribute name="Baud_Rate"   type="xsd:string"  use="optional" default="9600"/>
           <xsd:attribute name="Parity"      type="xsd:string"  use="optional" default="even"/>
@@ -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 @@
     <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
       <xsd:element name="ModbusRTUslave">
         <xsd:complexType>
+          <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
           <xsd:attribute name="Serial_Port" type="xsd:string"  use="optional" default="/dev/ttyS0"/>
           <xsd:attribute name="Baud_Rate"   type="xsd:string"  use="optional" default="9600"/>
           <xsd:attribute name="Parity"      type="xsd:string"  use="optional" default="even"/>
@@ -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:
--- /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
--- 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):