merge
authorMario de Sousa <msousa@fe.up.pt>
Thu, 28 May 2020 11:16:59 +0100
changeset 2650 449c9539887a
parent 2644 769fefae7c81 (current diff)
parent 2649 db68cb0e6bdc (diff)
child 2659 09bac1f52b1e
merge
Beremiz_service.py
--- a/Beremiz_service.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/Beremiz_service.py	Thu May 28 11:16:59 2020 +0100
@@ -492,6 +492,8 @@
 
 installThreadExcepthook()
 havewamp = False
+haveBNconf = False
+
 
 if havetwisted:
     if webport is not None:
@@ -500,8 +502,17 @@
         except Exception:
             LogMessageAndException(_("Nevow/Athena import failed :"))
             webport = None
-        NS.WorkingDir = WorkingDir
-
+        NS.WorkingDir = WorkingDir  # bug? what happens if import fails?
+
+    # Try to add support for BACnet configuration via web server interface
+    # NOTE:BACnet web config only makes sense if web server is available
+    if webport is not None:
+        try:
+            import runtime.BACnet_config as BNconf
+            haveBNconf = True
+        except Exception:
+            LogMessageAndException(_("BACnet configuration web interface - import failed :"))
+        
     try:
         import runtime.WampClient as WC  # pylint: disable=ungrouped-imports
         WC.WorkingDir = WorkingDir
@@ -523,6 +534,8 @@
 runtime.CreatePLCObjectSingleton(
     WorkingDir, argv, statuschange, evaluator, pyruntimevars)
 
+plcobj = runtime.GetPLCObjectSingleton()
+
 pyroserver = PyroServer(servicename, interface, port)
 
 if havewx:
@@ -538,6 +551,12 @@
         except Exception:
             LogMessageAndException(_("Nevow Web service failed. "))
 
+    if haveBNconf:
+        try:
+            BNconf.init(plcobj, NS, WorkingDir)
+        except Exception:
+            LogMessageAndException(_("BACnet web configuration failed startup. "))
+
     if havewamp:
         try:
             WC.SetServer(pyroserver)
@@ -601,7 +620,6 @@
 pyroserver.Quit()
 pyro_thread.join()
 
-plcobj = runtime.GetPLCObjectSingleton()
 plcobj.StopPLC()
 plcobj.UnLoadPLC()
 
--- a/bacnet/bacnet.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/bacnet/bacnet.py	Thu May 28 11:16:59 2020 +0100
@@ -35,11 +35,11 @@
 from bacnet.BacnetSlaveEditor import *
 from bacnet.BacnetSlaveEditor import ObjectProperties
 from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY
-
-base_folder = os.path.split(
-    os.path.dirname(os.path.realpath(__file__)))[0]
+from ConfigTreeNode import ConfigTreeNode
+
+base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
 base_folder = os.path.join(base_folder, "..")
-BacnetPath = os.path.join(base_folder, "BACnet")
+BacnetPath  = os.path.join(base_folder, "BACnet")
 BacnetLibraryPath = os.path.join(BacnetPath, "lib")
 BacnetIncludePath = os.path.join(BacnetPath, "include")
 BacnetIncludePortPath = os.path.join(BacnetPath, "ports")
@@ -50,6 +50,10 @@
 BACNET_VENDOR_NAME = "Beremiz.org"
 BACNET_DEVICE_MODEL_NAME = "Beremiz PLC"
 
+
+# Max String Size of BACnet Paramaters
+BACNET_PARAM_STRING_SIZE = 64
+
 #
 #
 #
@@ -97,6 +101,14 @@
       </xsd:element>
     </xsd:schema>
     """
+    # NOTE; Add the following code/declaration to the aboce XSD in order to activate the
+    #          Override_Parameters_Saved_on_PLC   flag (currenty not in use as it requires further
+    #          analysis how the user would interpret this user interface option.
+    #        <--- snip --->
+    #          <xsd:attribute name="Override_Parameters_Saved_on_PLC" 
+    #                                                   type="xsd:boolean" use="optional" default="true"/>
+    #        <--- snip --->
+    #
     # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID)
     #       so the Device instance ID is limited from 0 to 22^2-1 = 4194303
     #       However, 4194303 is reserved for special use (similar to NULL pointer), so last
@@ -552,8 +564,10 @@
             generate_file_handle  .write(generate_file_content)
             generate_file_handle  .close()
 
-    #
-    # Generate the source files #
+
+
+    #
+    # Generate the C source code files
     #
     def CTNGenerate_C(self, buildpath, locations):
         # Determine the current location in Beremiz's project configuration
@@ -596,6 +610,11 @@
         # The BACnetServerNode attribute is added dynamically by ConfigTreeNode._AddParamsMembers()
         # It will be an XML parser object created by
         # GenerateParserFromXSDstring(self.XSD).CreateRoot()
+        #
+        # Note: Override_Parameters_Saved_on_PLC is converted to an integer by int()
+        #       The above flag is not currently in use. It requires further thinking on how the 
+        #       user will interpret and interact with this user interface...
+        #loc_dict["Override_Parameters_Saved_on_PLC"] = int(self.BACnetServerNode.getOverride_Parameters_Saved_on_PLC())
         loc_dict["network_interface"] = self.BACnetServerNode.getNetwork_Interface()
         loc_dict["port_number"] = self.BACnetServerNode.getUDP_Port_Number()
         loc_dict["BACnet_Device_ID"] = self.BACnetServerNode.getBACnet_Device_ID()
@@ -607,6 +626,8 @@
         loc_dict["BACnet_Vendor_ID"] = BACNET_VENDOR_ID
         loc_dict["BACnet_Vendor_Name"] = BACNET_VENDOR_NAME
         loc_dict["BACnet_Model_Name"] = BACNET_DEVICE_MODEL_NAME
+        loc_dict["BACnet_Param_String_Size"] = BACNET_PARAM_STRING_SIZE
+        
 
         # 2) Add the data specific to each BACnet object type
         # For each BACnet object type, start off by creating some intermediate helpful lists
@@ -726,4 +747,33 @@
         CFLAGS = ' -I"' + BacnetIncludePath + '"'
         CFLAGS += ' -I"' + BacnetIncludePortPath + '"'
 
+        # ----------------------------------------------------------------------
+        # Create a file containing the default configuration paramters.
+        # Beremiz will then transfer this file to the PLC, where the web server 
+        # will read it to obtain the default configuration parameters.
+        # ----------------------------------------------------------------------
+        # NOTE: This is no loner needed! The web interface will read these 
+        # parameters directly from the compiled C code (.so file)
+        #
+        ### extra_file_name   = os.path.join(buildpath, "%s_%s.%s" % ('bacnet_extrafile', postfix, 'txt'))
+        ### extra_file_handle = open(extra_file_name, 'w')
+        ### 
+        ### proplist = ["network_interface", "port_number", "BACnet_Device_ID", "BACnet_Device_Name", 
+        ###             "BACnet_Comm_Control_Password", "BACnet_Device_Location", 
+        ###             "BACnet_Device_Description", "BACnet_Device_AppSoft_Version"]
+        ### for propname in proplist:
+        ###     extra_file_handle.write("%s:%s\n" % (propname, loc_dict[propname]))
+        ### 
+        ### extra_file_handle.close()
+        ### extra_file_handle = open(extra_file_name, 'r')
+
+        # Format of data to return:
+        #   [(Cfiles, CFLAGS), ...], LDFLAGS, DoCalls, extra_files
+        # LDFLAGS     = ['flag1', 'flag2', ...]
+        # DoCalls     = true  or  false
+        # extra_files = (fname,fobject), ...
+        # fobject     = file object, already open'ed for read() !!
+        #
+        # extra_files -> files that will be downloaded to the PLC!
         return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True
+        #return [(Generated_BACnet_c_mainfile_name, CFLAGS)], LDFLAGS, True, ('extrafile1.txt', extra_file_handle)
--- a/bacnet/runtime/device.h	Wed Dec 18 13:31:22 2019 +0100
+++ b/bacnet/runtime/device.h	Thu May 28 11:16:59 2020 +0100
@@ -161,11 +161,12 @@
  * Maximum sizes excluding nul terminator .
  */
 #define STRLEN_X(minlen, str)  (((minlen)>sizeof(str))?(minlen):sizeof(str))
-#define MAX_DEV_NAME_LEN STRLEN_X(32, "%(BACnet_Device_Name)s")     /* Device name         */
-#define MAX_DEV_LOC_LEN  STRLEN_X(64, BACNET_DEVICE_LOCATION)       /* Device location     */
-#define MAX_DEV_MOD_LEN  STRLEN_X(32, BACNET_DEVICE_MODEL_NAME)     /* Device model name   */
-#define MAX_DEV_VER_LEN  STRLEN_X(16, BACNET_DEVICE_APPSOFT_VER)    /* Device application software version */
-#define MAX_DEV_DESC_LEN STRLEN_X(64, BACNET_DEVICE_DESCRIPTION)    /* Device description  */
+#define MAX_DEV_NAME_LEN STRLEN_X(%(BACnet_Param_String_Size)d, "%(BACnet_Device_Name)s")     /* Device name         */
+#define MAX_DEV_LOC_LEN  STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_LOCATION)       /* Device location     */
+#define MAX_DEV_MOD_LEN  STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_MODEL_NAME)     /* Device model name   */
+#define MAX_DEV_VER_LEN  STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_APPSOFT_VER)    /* Device application software version */
+#define MAX_DEV_DESC_LEN STRLEN_X(%(BACnet_Param_String_Size)d, BACNET_DEVICE_DESCRIPTION)    /* Device description  */
+
 
 /** Structure to define the Object Properties common to all Objects. */
 typedef struct commonBacObj_s {
--- a/bacnet/runtime/server.c	Wed Dec 18 13:31:22 2019 +0100
+++ b/bacnet/runtime/server.c	Thu May 28 11:16:59 2020 +0100
@@ -52,6 +52,10 @@
 #include "timesync.h"
 
 
+
+
+
+
 /* A utility function used by most (all?) implementations of BACnet Objects */
 /* Adds to Prop_List all entries in Prop_List_XX that are not
  * PROP_OBJECT_IDENTIFIER, PROP_OBJECT_NAME, PROP_OBJECT_TYPE, PROP_PROPERTY_LIST
@@ -454,17 +458,33 @@
     bvlc_bdt_restore_local();
     /* Initiliaze the bacnet server 'device' */    
     Device_Init(server_node->device_name);
-
-    pthread_mutex_lock(&init_done_lock);
-    init_done = 1;
-    pthread_cond_signal(&init_done_cond);
-    pthread_mutex_unlock(&init_done_lock);
-
+    
+    /* Although the default values for the following properties are hardcoded into
+     * the respective variable definition+initialization in the C code,
+     * these values may be potentially changed after compilation but before 
+     * code startup. This is done by the web interface(1), directly loading the .so file, 
+     * and changing the values in the server_node_t variable.
+     * We must therefore honor those values when we start running the BACnet device server
+     * 
+     * (1) Web interface is implemented in runtime/BACnet_config.py
+     *     which works as an extension of the web server in runtime/NevowServer.py
+     *     which in turn is initialised/run by the Beremiz_service.py deamon
+     */
+    Device_Set_Location                    (server_node->device_location,        strlen(server_node->device_location));
+    Device_Set_Description                 (server_node->device_description,     strlen(server_node->device_description));
+    Device_Set_Application_Software_Version(server_node->device_appsoftware_ver, strlen(server_node->device_appsoftware_ver));
     /* Set the password (max 31 chars) for Device Communication Control request. */
     /* Default in the BACnet stack is hardcoded as "filister" */
     /* (char *) cast is to remove the cast. The function is incorrectly declared/defined in the BACnet stack! */
     /* BACnet stack needs to change demo/handler/h_dcc.c and include/handlers.h                               */
     handler_dcc_password_set((char *)server_node->comm_control_passwd);
+
+    
+    pthread_mutex_lock(&init_done_lock);
+    init_done = 1;
+    pthread_cond_signal(&init_done_cond);
+    pthread_mutex_unlock(&init_done_lock);
+
     /* Set callbacks and configure network interface */
     res = Init_Service_Handlers();
     if (res < 0) exit(1);
@@ -661,3 +681,108 @@
 	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.
+ * BACnet plugin also comes with an extension to the web server, through
+ * which the basic BACnet plugin configuration parameters may be changed
+ * (basically, only the parameters in server_node_t may be changed)
+ * 
+ * The following functions are never called from other C code. They are 
+ * called instead from the python code in runtime/BACnet_config.py, that
+ * implements the web server extension for configuring BACnet parameters.
+ */
+
+
+/* The location (in the Config. Node Tree of Beremiz IDE)
+ * of the BACnet plugin.
+ * 
+ * This variable is also used by the BACnet web config code to determine 
+ * whether the current loaded PLC includes the BACnet plugin
+ * (so it should make the BACnet parameter web interface visible to the user).
+ */
+const char * __bacnet_plugin_location = "%(locstr)s";
+
+
+/* NOTE: We could have the python code in runtime/BACnet_config.py
+ *       directly access the server_node_t structure, however
+ *       this would create a tight coupling between these two
+ *       disjoint pieces of code.
+ *       Any change to the server_node_t structure would require the 
+ *       python code to be changed accordingly. I have therefore opted
+ *       to cretae get/set functions, one for each parameter.
+ *       
+ * NOTE: since the BACnet plugin can only support/allow at most one instance 
+ *       of the BACnet plugin in Beremiz (2 or more are not allowed due to 
+ *       limitations of the underlying BACnet protocol stack being used),
+ *       a single generic version of each of the following functions would work.
+ *       However, simply for the sake of keeping things general, we create
+ *       a diferent function for each plugin instance (which, as I said,
+ *       will never occur for now).
+ * 
+ *      The functions being declared are therefoe named:
+ *          __bacnet_0_get_ConfigParam_location
+ *          __bacnet_0_get_ConfigParam_device_name
+ *        etc...
+ *      where the 0 will be replaced by the location of the BACnet plugin
+ *      in the Beremiz configuration tree (will change depending on where
+ *      the user inserted the BACnet plugin into their project) 
+ */
+
+/* macro works for all data types */
+#define __bacnet_get_ConfigParam(param_type,param_name)                 \
+param_type __bacnet_%(locstr)s_get_ConfigParam_##param_name(void) {     \
+    return server_node.param_name;                                      \
+}
+
+/* macro only works for char * data types */
+/* Note that the storage space (max string size) reserved for each parameter
+ * (this storage space is reserved in device.h)
+ * is set to a minimum of 
+ *     %(BACnet_Param_String_Size)d 
+ * which is set to the value of
+ *     BACNET_PARAM_STRING_SIZE
+ * in bacnet.py
+ */
+#define __bacnet_set_ConfigParam(param_type,param_name)                 \
+void __bacnet_%(locstr)s_set_ConfigParam_##param_name(param_type val) { \
+    strncpy(server_node.param_name, val, %(BACnet_Param_String_Size)d); \
+    server_node.param_name[%(BACnet_Param_String_Size)d - 1] = '\0';    \
+}
+
+
+#define __bacnet_ConfigParam_str(param_name)        \
+__bacnet_get_ConfigParam(const char*,param_name)    \
+__bacnet_set_ConfigParam(const char*,param_name)     
+
+
+__bacnet_ConfigParam_str(location)
+__bacnet_ConfigParam_str(network_interface)
+__bacnet_ConfigParam_str(port_number)
+__bacnet_ConfigParam_str(device_name)
+__bacnet_ConfigParam_str(device_location)
+__bacnet_ConfigParam_str(device_description)
+__bacnet_ConfigParam_str(device_appsoftware_ver)
+__bacnet_ConfigParam_str(comm_control_passwd)
+
+__bacnet_get_ConfigParam(uint32_t,device_id)
+void __bacnet_%(locstr)s_set_ConfigParam_device_id(uint32_t val) {
+    server_node.device_id = val;
+}
+
--- a/bacnet/runtime/server.h	Wed Dec 18 13:31:22 2019 +0100
+++ b/bacnet/runtime/server.h	Thu May 28 11:16:59 2020 +0100
@@ -33,12 +33,19 @@
 
 
 
+
 typedef struct{
-	    const char *location;
-	    const char *network_interface;
-	    const char *port_number;
-	    const char *device_name;
-	    const char *comm_control_passwd;
+	    char location              [%(BACnet_Param_String_Size)d];
+	    char network_interface     [%(BACnet_Param_String_Size)d];
+	    char port_number           [%(BACnet_Param_String_Size)d];
+	    char device_name           [%(BACnet_Param_String_Size)d];
+	    char device_location       [%(BACnet_Param_String_Size)d];        
+	    char device_description    [%(BACnet_Param_String_Size)d];
+	    char device_appsoftware_ver[%(BACnet_Param_String_Size)d];        
+	    char comm_control_passwd   [%(BACnet_Param_String_Size)d];
+// 	    int         override_local_config;  // bool flag => 
+// 	                                        //   true : use these parameter values
+// 	                                        //   false: use values stored on local file in PLC
 	    uint32_t	device_id; // device ID is 22 bits long! uint16_t is not enough!
 	    int		init_state; // store how far along the server's initialization has progressed
 	    pthread_t	thread_id;  // thread handling this server
@@ -49,13 +56,16 @@
 /*initialization following all parameters given by user in application*/
 static server_node_t server_node = {
   "%(locstr)s",
-  "%(network_interface)s",           // interface    (NULL => use default (eth0))
-  "%(port_number)s",                 // Port number  (NULL => use default)
-  "%(BACnet_Device_Name)s",          // BACnet server's device (object) name
-  "%(BACnet_Comm_Control_Password)s",// BACnet server's device (object) name
-   %(BACnet_Device_ID)s              // BACnet server's device (object) ID
+  "%(network_interface)s",              // interface    (NULL => use default (eth0))
+  "%(port_number)s",                    // Port number  (NULL => use default)
+  "%(BACnet_Device_Name)s",             // BACnet server's device (object) Name
+  "%(BACnet_Device_Location)s",         // BACnet server's device (object) Location
+  "%(BACnet_Device_Description)s",      // BACnet server's device (object) Description
+  "%(BACnet_Device_AppSoft_Version)s",  // BACnet server's device (object) App. Software Ver.
+  "%(BACnet_Comm_Control_Password)s",   // BACnet server's device (object) Password
+//  (Override_Parameters_Saved_on_PLC)d, // override locally saved parameters (bool flag)
+  %(BACnet_Device_ID)s                  // BACnet server's device (object) ID
 };
 
 
-
 #endif /* SERVER_H_ */
--- a/modbus/mb_runtime.c	Wed Dec 18 13:31:22 2019 +0100
+++ b/modbus/mb_runtime.c	Thu May 28 11:16:59 2020 +0100
@@ -25,6 +25,8 @@
 
 #include <stdio.h>
 #include <string.h>  /* required for memcpy() */
+#include <time.h>
+#include <signal.h>
 #include "mb_slave_and_master.h"
 #include "MB_%(locstr)s.h"
 
@@ -299,10 +301,42 @@
 	// Enable thread cancelation. Enabled is default, but set it anyway to be safe.
 	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
 	
-	// get the current time
-	clock_gettime(CLOCK_MONOTONIC, &next_cycle);
-
-	// loop the communication with the client
+	// configure the timer for periodic activation
+    {
+      struct itimerspec timerspec;
+      timerspec.it_interval.tv_sec  = period_sec;
+      timerspec.it_interval.tv_nsec = period_nsec;
+      timerspec.it_value            = timerspec.it_interval;
+      
+      if (timer_settime(client_nodes[client_node_id].timer_id, 0 /* flags */, &timerspec, NULL) < 0)
+        fprintf(stderr, "Modbus plugin: Error configuring periodic activation timer for Modbus client %%s.\n", client_nodes[client_node_id].location);          
+    }
+
+    /* loop the communication with the client
+     * 
+         * When the client thread has difficulty communicating with remote client and/or server (network issues, for example),
+         * then the communications get delayed and we will fall behind in the period. 
+         * 
+         * This is OK. Note that if the condition variable were to be signaled multiple times while the client thread is inside the same
+         * Modbus transaction, then all those signals would be ignored.
+         * However, and since we keep the mutex locked during the communication cycle, it is not possible to signal the condition variable
+         * during that time (it is only possible while the thread is blocked during the call to pthread_cond_wait().
+         * 
+         * This means that when network issues eventually get resolved, we will NOT have a bunch of delayed activations to handle
+         * in quick succession (which would goble up CPU time). 
+         * 
+         * Notice that the above property is valid whether the communication cycle is run with the mutex locked, or unlocked.
+         * Since it makes it easier to implement the correct semantics for the other activation methods if the communication cycle
+         * is run with the mutex locked, then that is what we do.
+         * 
+     * Note that during all the communication cycle we will keep locked the mutex 
+     * (i.e. the mutex used together with the condition variable that will activate a new communication cycle)
+     * 
+     * Note that we never get to explicitly unlock this mutex. It will only be unlocked by the pthread_cond_wait()
+     * call at the end of the cycle.
+     */
+    pthread_mutex_lock(&(client_nodes[client_node_id].mutex));
+
 	while (1) {
 		/*
 		struct timespec cur_time;
@@ -311,9 +345,22 @@
 		*/
 		int req;
 		for (req=0; req < NUMBER_OF_CLIENT_REQTS; req ++){
-			/*just do the requests belonging to the client */
+			/* just do the requests belonging to the client */
 			if (client_requests[req].client_node_id != client_node_id)
 				continue;
+            
+            /* only do the request if:
+             *   - this request was explictly asked to be executed by the client program
+             *  OR
+             *   - the client thread was activated periodically
+             *     (in which case we execute all the requests belonging to the client node)
+             */
+            if ((client_requests[req].flag_exec_req == 0) && (client_nodes[client_requests[req].client_node_id].periodic_act == 0))
+                continue;
+            
+            //fprintf(stderr, "Modbus plugin: RUNNING<###> of Modbus request %%d  (periodic = %%d  flag_exec_req = %%d)\n", 
+            //        req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req );
+            
 			int res_tmp = __execute_mb_request(req);
 			switch (res_tmp) {
 			  case PORT_FAILURE: {
@@ -357,36 +404,40 @@
 				break;
 			  }
 			}
-		}
-		// Determine absolute time instant for starting the next cycle
-		struct timespec prev_cycle, now;
-		prev_cycle = next_cycle;
-		timespec_add(next_cycle, period_sec, period_nsec);
-		/* NOTE A:
-		 * When we have difficulty communicating with remote client and/or server, then the communications get delayed and we will
-		 * fall behind in the period. This means that when communication is re-established we may end up running this loop continuously
-		 * for some time until we catch up.
-		 * This is undesirable, so we detect it by making sure the next_cycle will start in the future.
-		 * When this happens we will switch from a purely periodic task _activation_ sequence, to a fixed task suspension interval.
-		 * 
-		 * NOTE B:
-		 * It probably does not make sense to check for overflow of timer - so we don't do it for now!
-		 * Even in 32 bit systems this will take at least 68 years since the computer booted
-		 * (remember, we are using CLOCK_MONOTONIC, which should start counting from 0
-		 * every time the system boots). On 64 bit systems, it will take over 
-		 * 10^11 years to overflow.
-		 */
-		clock_gettime(CLOCK_MONOTONIC, &now);
-		if (  ((now.tv_sec > next_cycle.tv_sec) || ((now.tv_sec == next_cycle.tv_sec) && (now.tv_nsec > next_cycle.tv_nsec)))
-		   /* We are falling behind. See NOTE A above */
-		   || (next_cycle.tv_sec < prev_cycle.tv_sec)
-		   /* Timer overflow. See NOTE B above */
-		   ) {
-			next_cycle = now;
-			timespec_add(next_cycle, period_sec, period_nsec);
-		}
-
-		clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_cycle, NULL);
+        
+            /* We have just finished excuting a client transcation request.
+             * If the current cycle was activated by user request we reset the flag used to ask to run it
+             */
+            if (0 != client_requests[req].flag_exec_req) {
+                client_requests[req].flag_exec_req     = 0;
+                client_requests[req].flag_exec_started = 0;   
+            }
+            
+            //fprintf(stderr, "Modbus plugin: RUNNING<---> of Modbus request %%d  (periodic = %%d  flag_exec_req = %%d)\n", 
+            //        req, client_nodes[client_requests[req].client_node_id].periodic_act, client_requests[req].flag_exec_req );
+        }
+
+        // Wait for signal (from timer or explicit request from user program) before starting the next cycle
+        {
+            // No need to lock the mutex. Is is already locked just before the while(1) loop.
+            // Read the comment there to understand why.
+            // pthread_mutex_lock(&(client_nodes[client_node_id].mutex));
+            
+            /* the client thread has just finished a cycle, so all the flags used to signal an activation
+             * and specify the activation source (periodic, user request, ...)
+             * get reset here, before waiting for a new activation.
+             */
+            client_nodes[client_node_id].periodic_act = 0;            
+            client_nodes[client_node_id].execute_req  = 0;
+            
+            while (client_nodes[client_node_id].execute_req == 0)
+                pthread_cond_wait(&(client_nodes[client_node_id].condv),
+                                &(client_nodes[client_node_id].mutex)); 
+                            
+            // We run the communication cycle with the mutex locked.
+            // Read the comment just above the while(1) to understand why.
+            // pthread_mutex_unlock(&(client_nodes[client_node_id].mutex));
+        }
 	}
 
 	// humour the compiler.
@@ -394,6 +445,50 @@
 }
 
 
+
+/* Function to activate a client node's thread */
+/* returns -1 if it could not send the signal */
+static int __signal_client_thread(int client_node_id) {
+    /* We TRY to signal the client thread.
+     * We do this because this function can be called at the end of the PLC scan cycle
+     * and we don't want it to block at that time.
+     */
+    if (pthread_mutex_trylock(&(client_nodes[client_node_id].mutex)) != 0)
+        return -1;
+    client_nodes[client_node_id].execute_req = 1; // tell the thread to execute
+    pthread_cond_signal (&(client_nodes[client_node_id].condv));
+    pthread_mutex_unlock(&(client_nodes[client_node_id].mutex));
+    return 0;
+}
+
+
+
+/* Function that will be called whenever a client node's periodic timer expires. */
+/* The client node's thread will be waiting on a condition variable, so this function simply signals that 
+ * condition variable.
+ * 
+ * The same callback function is called by the timers of all client nodes. The id of the client node
+ * in question will be passed as a parameter to the call back function.
+ */
+void __client_node_timer_callback_function(union sigval sigev_value) {
+    /* signal the client node's condition variable on which the client node's thread should be waiting... */
+    /* Since the communication cycle is run with the mutex locked, we use trylock() instead of lock() */
+    //pthread_mutex_lock  (&(client_nodes[sigev_value.sival_int].mutex));
+    if (pthread_mutex_trylock (&(client_nodes[sigev_value.sival_int].mutex)) != 0)
+        /* we never get to signal the thread for activation. But that is OK.
+         * If it still in the communication cycle (during which the mutex is kept locked)
+         * then that means that the communication cycle is falling behing in the periodic 
+         * communication cycle, and we therefore need to skip a period.
+         */
+        return;
+    client_nodes[sigev_value.sival_int].execute_req  = 1; // tell the thread to execute
+    client_nodes[sigev_value.sival_int].periodic_act = 1; // tell the thread the activation was done by periodic timer   
+    pthread_cond_signal (&(client_nodes[sigev_value.sival_int].condv));
+    pthread_mutex_unlock(&(client_nodes[sigev_value.sival_int].mutex));
+}
+
+
+
 int __cleanup_%(locstr)s ();
 int __init_%(locstr)s (int argc, char **argv){
 	int index;
@@ -421,9 +516,14 @@
 		return -1;
 	}
 	
-	/* init the mutex for each client request */
+	/* init each client request */
 	/* Must be done _before_ launching the client threads!! */
 	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
+        /* make sure flags connected to user program MB transaction start request are all reset */
+        client_requests[index].flag_exec_req     = 0;
+        client_requests[index].flag_exec_started = 0;        
+        /* init the mutex for each client request */
+        /* Must be done _before_ launching the client threads!! */
 		if (pthread_mutex_init(&(client_requests[index].coms_buf_mutex), NULL)) {
 			fprintf(stderr, "Modbus plugin: Error initializing request for modbus client node %%s\n", client_nodes[client_requests[index].client_node_id].location);
 			goto error_exit;
@@ -443,6 +543,39 @@
 		}
 		client_nodes[index].init_state = 1; // we have created the node 
 		
+		/* initialize the mutex variable that will be used by the thread handling the client node */
+        if (pthread_mutex_init(&(client_nodes[index].mutex), NULL) < 0) {
+			fprintf(stderr, "Modbus plugin: Error creating mutex for modbus client node %%s\n", client_nodes[index].location);
+			goto error_exit;                
+        }
+		client_nodes[index].init_state = 2; // we have created the mutex
+		
+		/* initialize the condition variable that will be used by the thread handling the client node */
+        if (pthread_cond_init(&(client_nodes[index].condv), NULL) < 0) {
+			fprintf(stderr, "Modbus plugin: Error creating condition variable for modbus client node %%s\n", client_nodes[index].location);
+			goto error_exit;                
+        }
+        client_nodes[index].execute_req = 0; //variable associated with condition variable
+		client_nodes[index].init_state = 3; // we have created the condition variable
+		
+		/* initialize the timer that will be used to periodically activate the client node */
+        {
+            // start off by reseting the flag that will be set whenever the timer expires
+            client_nodes[index].periodic_act = 0;
+
+            struct sigevent evp;
+            evp.sigev_notify            = SIGEV_THREAD; /* Notification method - call a function in a new thread context */
+            evp.sigev_value.sival_int   = index;        /* Data passed to function upon notification - used to indentify which client node to activate */
+            evp.sigev_notify_function   = __client_node_timer_callback_function; /* function to call upon timer expiration */
+            evp.sigev_notify_attributes = NULL;         /* attributes for new thread in which sigev_notify_function will be called/executed */
+            
+            if (timer_create(CLOCK_MONOTONIC, &evp, &(client_nodes[index].timer_id)) < 0) {
+                fprintf(stderr, "Modbus plugin: Error creating timer for modbus client node %%s\n", client_nodes[index].location);
+                goto error_exit;                
+            }
+        }
+        client_nodes[index].init_state = 4; // we have created the timer
+
 		/* launch a thread to handle this client node */
 		{
 			int res = 0;
@@ -450,11 +583,11 @@
 			res |= pthread_attr_init(&attr);
 			res |= pthread_create(&(client_nodes[index].thread_id), &attr, &__mb_client_thread, (void *)((char *)NULL + index));
 			if (res !=  0) {
-				fprintf(stderr, "Modbus plugin: Error starting modbus client thread for node %%s\n", client_nodes[index].location);
+				fprintf(stderr, "Modbus plugin: Error starting thread for modbus client node %%s\n", client_nodes[index].location);
 				goto error_exit;
 			}
 		}
-		client_nodes[index].init_state = 2; // we have created the node and a thread
+		client_nodes[index].init_state = 5; // we have created the thread
 	}
 
 	/* init each local server */
@@ -499,9 +632,26 @@
 	int index;
 
 	for (index=0; index < NUMBER_OF_CLIENT_REQTS; index ++){
-		/*just do the output requests */
+		/* synchronize the PLC and MB buffers only for the output requests */
 		if (client_requests[index].req_type == req_output){
+            
+            // lock the mutex brefore copying the data
 			if(pthread_mutex_trylock(&(client_requests[index].coms_buf_mutex)) == 0){
+                
+                // Check if user configured this MB request to be activated whenever the data to be written changes
+                if (client_requests[index].write_on_change) {
+                    // Let's check if the data did change...
+                    // compare the data in plcv_buffer to coms_buffer
+                    int res;
+                    res = memcmp((void *)client_requests[index].coms_buffer /* buf 1 */,
+                                 (void *)client_requests[index].plcv_buffer /* buf 2*/,
+                                 REQ_BUF_SIZE * sizeof(u16) /* size in bytes */);
+                    
+                    // if data changed, activate execution request 
+                    if (0 != res)
+                        client_requests[index].flag_exec_req = 1;
+                }
+                
                 // copy from plcv_buffer to coms_buffer
                 memcpy((void *)client_requests[index].coms_buffer /* destination */,
                        (void *)client_requests[index].plcv_buffer /* source */,
@@ -509,7 +659,33 @@
                 pthread_mutex_unlock(&(client_requests[index].coms_buf_mutex));
             }
 		}
-	}
+        /* if the user program set the execution request flag, then activate the thread
+         *  that handles this Modbus client transaction so it gets a chance to be executed
+         *  (but don't activate the thread if it has already been activated!)
+         * 
+         * NOTE that we do this, for both the IN and OUT mapped location, under this
+         *  __publish_() function. The scan cycle of the PLC works as follows:
+         *   - call __retrieve_()
+         *   - execute user programs
+         *   - call __publish_()
+         *   - insert <delay> until time to start next periodic/cyclic scan cycle
+         * 
+         *  In an attempt to be able to run the MB transactions during the <delay>
+         *  interval in which not much is going on, we handle the user program
+         *  requests to execute a specific MB transaction in this __publish_()
+         *  function.
+         */
+        if ((client_requests[index].flag_exec_req != 0) && (0 == client_requests[index].flag_exec_started)) {
+            int client_node_id = client_requests[index].client_node_id;
+            if (__signal_client_thread(client_node_id) >= 0) {
+                /* - upon success, set flag_exec_started
+                 * - both flags (flag_exec_req and flag_exec_started) will be reset
+                 *   once the transaction has completed.
+                 */
+                client_requests[index].flag_exec_started = 1;    
+            }
+        }                    
+    }
 }
 
 
@@ -544,12 +720,39 @@
 	/* kill thread and close connections of each modbus client node */
 	for (index=0; index < NUMBER_OF_CLIENT_NODES; index++) {
 		close = 0;
-		if (client_nodes[index].init_state >= 2) {
+		if (client_nodes[index].init_state >= 5) {
 			// thread was launched, so we try to cancel it!
 			close  = pthread_cancel(client_nodes[index].thread_id);
 			close |= pthread_join  (client_nodes[index].thread_id, NULL);
 			if (close < 0)
-				fprintf(stderr, "Modbus plugin: Error closing thread for modbus client %%s\n", client_nodes[index].location);
+				fprintf(stderr, "Modbus plugin: Error closing thread for modbus client node %%s\n", client_nodes[index].location);
+		}
+		res |= close;
+
+		close = 0;
+		if (client_nodes[index].init_state >= 4) {
+			// timer was created, so we try to destroy it!
+			close  = timer_delete(client_nodes[index].timer_id);
+			if (close < 0)
+				fprintf(stderr, "Modbus plugin: Error destroying timer for modbus client node %%s\n", client_nodes[index].location);
+		}
+		res |= close;
+
+		close = 0;
+		if (client_nodes[index].init_state >= 3) {
+			// condition variable was created, so we try to destroy it!
+			close  = pthread_cond_destroy(&(client_nodes[index].condv));
+			if (close < 0)
+				fprintf(stderr, "Modbus plugin: Error destroying condition variable for modbus client node %%s\n", client_nodes[index].location);
+		}
+		res |= close;
+
+		close = 0;
+		if (client_nodes[index].init_state >= 2) {
+			// mutex was created, so we try to destroy it!
+			close  = pthread_mutex_destroy(&(client_nodes[index].mutex));
+			if (close < 0)
+				fprintf(stderr, "Modbus plugin: Error destroying mutex for modbus client node %%s\n", client_nodes[index].location);
 		}
 		res |= close;
 
--- a/modbus/mb_runtime.h	Wed Dec 18 13:31:22 2019 +0100
+++ b/modbus/mb_runtime.h	Thu May 28 11:16:59 2020 +0100
@@ -54,11 +54,32 @@
 typedef struct{
 	    const char *location;
 	    node_addr_t	node_address;
-	    int		mb_nd;
+	    int		mb_nd;      // modbus library node used for this client
 	    int		init_state; // store how far along the client's initialization has progressed
-	    u64		comm_period;
+	    u64		comm_period;// period to use when periodically sending requests to remote server
 	    int		prev_error; // error code of the last printed error message (0 when no error) 
-	    pthread_t	thread_id;  // thread handling all communication with this client
+	    pthread_t   thread_id;  // thread handling all communication for this client node
+	    timer_t      timer_id;  // timer used to periodically activate this client node's thread
+	    pthread_mutex_t mutex;  // mutex to be used with the following condition variable
+        pthread_cond_t  condv;  // used to signal the client thread when to start new modbus transactions
+        int       execute_req;  /* used, in association with condition variable,  
+                                 *   to signal when to send the modbus request to the server
+                                 * Note that we cannot simply rely on the condition variable to signal
+                                 *   when to activate the client thread, as the call to 
+                                 *   pthread_cond_wait() may return without having been signaled!
+                                 *   From the manual:
+                                 *      Spurious  wakeups  from  the
+                                 *      pthread_cond_timedwait() or pthread_cond_wait()  functions  may  occur.
+                                 *      Since  the  return from pthread_cond_timedwait() or pthread_cond_wait()
+                                 *      does not imply anything about the value of this predicate,  the  predi-
+                                 *      cate should be re-evaluated upon such return.
+                                 */
+        int      periodic_act;  /* (boolen) flag will be set when the client node's thread was activated 
+                                 * (by signaling the above condition variable) by the periodic timer.
+                                 * Note that this same thread may also be activated (condition variable is signaled)
+                                 * by other sources, such as when the user program requests that a specific 
+                                 * client MB transation be executed (flag_exec_req in client_request_t)
+                                 */
 	} client_node_t;
 
 
@@ -82,11 +103,37 @@
 	    u8		error_code; // modbus error code (if any) of current request
 	    int		prev_error; // error code of the last printed error message (0 when no error) 
 	    struct timespec resp_timeout;
+	    u8		write_on_change; // boolean flag. If true => execute MB request when data to send changes
 	      // buffer used to store located PLC variables
 	    u16		plcv_buffer[REQ_BUF_SIZE];
 	      // buffer used to store data coming from / going to server
 	    u16		coms_buffer[REQ_BUF_SIZE]; 
 	    pthread_mutex_t coms_buf_mutex; // mutex to access coms_buffer[]
+          /* boolean flag that will be mapped onto a (BOOL) located variable 
+           * (u16 because IEC 61131-3 BOOL are mapped onto u16 in C code! )
+           *    -> allow PLC program to request when to start the MB transaction
+           *    -> will be reset once the MB transaction has completed
+           */
+        u16     flag_exec_req;  
+          /* flag that works in conjunction with flag_exec_req
+           * (does not really need to be u16 as it is not mapped onto a located variable. )
+           *    -> used by internal logic to indicate that the client thread 
+           *       that will be executing the MB transaction
+           *       requested by flag exec_req has already been activated.
+           *    -> will be reset once the MB transaction has completed
+           */
+        u16     flag_exec_started;  
+          /* flag that will be mapped onto a (WORD) located variable 
+           * (u16 because the flag is a word! )
+           *    -> MSByte will store the result of the last executed MB transaction
+           *         1 -> error accessing IP network, or serial interface
+           *         2 -> reply received from server was an invalid frame
+           *         3 -> server did not reply before timeout expired
+           *         4 -> server returned a valid error frame
+           *    -> if the MSByte is 4, the LSByte will store the MB error code returned by the server
+           *    -> will be reset (set to 0) once this MB transaction has completed sucesfully
+           */
+        u16     flag_exec_status;  
 	} client_request_t;
 
 
--- a/modbus/mb_utils.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/modbus/mb_utils.py	Thu May 28 11:16:59 2020 +0100
@@ -184,7 +184,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 */,
+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))
@@ -198,6 +198,7 @@
         "slaveid": GetCTVal(child, 1),
         "address": GetCTVal(child, 3),
         "count": GetCTVal(child, 2),
+        "write_on_change": GetCTVal(child, 5),
         "timeout": timeout,
         "timeout_s": timeout_s,
         "timeout_ns": timeout_ns,
@@ -222,5 +223,11 @@
         self.GetCTRoot().logger.write_error(
             "Modbus plugin: Invalid number of channels in TCP client request node %(locreqstr)s (start_address + nr_channels must be less than 65536)\nModbus plugin: Aborting C code generation for this node\n" % request_dict)
         return None
+    if (request_dict["write_on_change"] and (request_dict["iotype"] == 'req_input')):
+        self.GetCTRoot().logger.write_error(
+            "Modbus plugin: (warning) MB client request node %(locreqstr)s has option 'write_on_change' enabled.\nModbus plugin: This option will be ignored by the Modbus read function.\n" % request_dict)
+        # NOTE: this is only a warning (we don't wish to abort code generation) so following line must be left commented out!
+        # return None
+    
 
     return req_init_template % request_dict
--- a/modbus/modbus.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/modbus/modbus.py	Thu May 28 11:16:59 2020 +0100
@@ -83,6 +83,7 @@
                 </xsd:restriction>
             </xsd:simpleType>
           </xsd:attribute>
+          <xsd:attribute name="Write_on_change" type="xsd:boolean" use="optional" default="false"/>
         </xsd:complexType>
       </xsd:element>
     </xsd:schema>
@@ -115,7 +116,29 @@
         datatacc = modbus_function_dict[function][6]
         # 'Coil', 'Holding Register', 'Input Discrete' or 'Input Register'
         dataname = modbus_function_dict[function][7]
+        # start off with a boolean entry
+        # This is a flag used to allow the user program to control when to 
+        # execute the Modbus request.
+        # NOTE: If the Modbus request has a 'current_location' of
+        #          %QX1.2.3
+        #       then the execution control flag will be
+        #          %QX1.2.3.0.0
+        #       and all the Modbus registers/coils will be
+        #          %QX1.2.3.0
+        #          %QX1.2.3.1
+        #          %QX1.2.3.2
+        #            ..
+        #          %QX1.2.3.n
         entries = []
+        entries.append({
+            "name": "Exec. request flag",
+            "type": LOCATION_VAR_MEMORY,
+            "size": 1,           # BOOL flag
+            "IEC_type": "BOOL",  # BOOL flag
+            "var_name": "var_name",
+            "location": "X" + ".".join([str(i) for i in current_location]) + ".0.0",
+            "description": "MB request execution control flag",
+            "children": []})        
         for offset in range(address, address + count):
             entries.append({
                 "name": dataname + " " + str(offset),
@@ -270,7 +293,7 @@
           <xsd:attribute name="Invocation_Rate_in_ms" use="optional" default="100">
             <xsd:simpleType>
                 <xsd:restriction base="xsd:unsignedLong">
-                    <xsd:minInclusive value="1"/>
+                    <xsd:minInclusive value="0"/>
                     <xsd:maxInclusive value="2147483647"/>
                 </xsd:restriction>
             </xsd:simpleType>
@@ -388,7 +411,7 @@
           <xsd:attribute name="Invocation_Rate_in_ms" use="optional" default="100">
             <xsd:simpleType>
                 <xsd:restriction base="xsd:integer">
-                    <xsd:minInclusive value="1"/>
+                    <xsd:minInclusive value="0"/>
                     <xsd:maxInclusive value="2147483647"/>
                 </xsd:restriction>
             </xsd:simpleType>
@@ -598,15 +621,13 @@
         for i in range(0, len(IPServer_port_numbers) - 1):
             for j in range(i + 1, len(IPServer_port_numbers)):
                 if IPServer_port_numbers[i][1] == IPServer_port_numbers[j][1]:
-                    self.GetCTRoot().logger.write_warning(
-                        _("Error: Modbus/IP Servers %{a1}.x and %{a2}.x use the same port number {a3}.\n").
-                        format(
-                            a1=_lt_to_str(IPServer_port_numbers[i][0]),
-                            a2=_lt_to_str(IPServer_port_numbers[j][0]),
-                            a3=IPServer_port_numbers[j][1]))
-                    raise Exception
-                    # TODO: return an error code instead of raising an
-                    # exception
+                    error_message = _("Error: Modbus/IP Servers %{a1}.x and %{a2}.x use the same port number {a3}.\n").format(
+                                        a1=_lt_to_str(IPServer_port_numbers[i][0]),
+                                        a2=_lt_to_str(IPServer_port_numbers[j][0]),
+                                        a3=IPServer_port_numbers[j][1])
+                    self.FatalError(error_message)
+                    #self.GetCTRoot().logger.write_warning(error_message)
+                    #raise Exception
 
         # Determine the current location in Beremiz's project configuration
         # tree
@@ -720,12 +741,32 @@
                     for iecvar in subchild.GetLocations():
                         # absloute address - start address
                         relative_addr = iecvar["LOC"][3] - int(GetCTVal(subchild, 3))
-                        # test if relative address in request specified range
-                        if relative_addr in xrange(int(GetCTVal(subchild, 2))):
+                        # test if the located variable 
+                        #    (a) has relative address in request specified range
+                        #  AND is NOT
+                        #    (b) is a control flag added by this modbus plugin
+                        #        to control its execution at runtime.
+                        #        Currently, we only add the "Execution Control Flag"
+                        #        to each client request (one flag per request)
+                        #        to control when to execute the request (if not executed periodically)
+                        #        While all Modbus registers/coils are mapped onto a location
+                        #        with 4 numbers (e.g. %QX0.1.2.55), this control flag is mapped
+                        #        onto a location with 4 numbers (e.g. %QX0.1.2.0.0), where the last
+                        #        two numbers are always '0.0', and the first two identify the request.
+                        #        In the following if, we check for this condition by checking
+                        #        if their are at least 4 or more number in the location's address.
+                        if (        relative_addr in xrange(int(GetCTVal(subchild, 2)))  # condition (a) explained above
+                            and len(iecvar["LOC"]) < 5):                                  # condition (b) explained above
                             if str(iecvar["NAME"]) not in loc_vars_list:
                                 loc_vars.append(
                                     "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].plcv_buffer[%d];" % (client_requestid, relative_addr))
                                 loc_vars_list.append(str(iecvar["NAME"]))
+                        # Now add the located variable in case it is a flag (condition (b) above
+                        if  len(iecvar["LOC"]) >= 5:       # condition (b) explained above
+                            if str(iecvar["NAME"]) not in loc_vars_list:
+                                loc_vars.append(
+                                    "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid))
+                                loc_vars_list.append(str(iecvar["NAME"]))
                     client_requestid += 1
                 tcpclient_node_count += 1
                 client_nodeid += 1
@@ -745,12 +786,32 @@
                     for iecvar in subchild.GetLocations():
                         # absloute address - start address
                         relative_addr = iecvar["LOC"][3] - int(GetCTVal(subchild, 3))
-                        # test if relative address in request specified range
-                        if relative_addr in xrange(int(GetCTVal(subchild, 2))):
+                        # test if the located variable 
+                        #    (a) has relative address in request specified range
+                        #  AND is NOT
+                        #    (b) is a control flag added by this modbus plugin
+                        #        to control its execution at runtime.
+                        #        Currently, we only add the "Execution Control Flag"
+                        #        to each client request (one flag per request)
+                        #        to control when to execute the request (if not executed periodically)
+                        #        While all Modbus registers/coils are mapped onto a location
+                        #        with 4 numbers (e.g. %QX0.1.2.55), this control flag is mapped
+                        #        onto a location with 4 numbers (e.g. %QX0.1.2.0.0), where the last
+                        #        two numbers are always '0.0', and the first two identify the request.
+                        #        In the following if, we check for this condition by checking
+                        #        if their are at least 4 or more number in the location's address.
+                        if (        relative_addr in xrange(int(GetCTVal(subchild, 2)))  # condition (a) explained above
+                            and len(iecvar["LOC"]) < 5):                                  # condition (b) explained above
                             if str(iecvar["NAME"]) not in loc_vars_list:
                                 loc_vars.append(
                                     "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].plcv_buffer[%d];" % (client_requestid, relative_addr))
                                 loc_vars_list.append(str(iecvar["NAME"]))
+                        # Now add the located variable in case it is a flag (condition (b) above
+                        if  len(iecvar["LOC"]) >= 5:       # condition (b) explained above
+                            if str(iecvar["NAME"]) not in loc_vars_list:
+                                loc_vars.append(
+                                    "u16 *" + str(iecvar["NAME"]) + " = &client_requests[%d].flag_exec_req;" % (client_requestid))
+                                loc_vars_list.append(str(iecvar["NAME"]))
                     client_requestid += 1
                 rtuclient_node_count += 1
                 client_nodeid += 1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/runtime/BACnet_config.py	Thu May 28 11:16:59 2020 +0100
@@ -0,0 +1,486 @@
+#!/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
+
+
+import json
+import os
+import ctypes
+
+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
+
+
+# Will contain references to the C functions 
+# (implemented in beremiz/bacnet/runtime/server.c)
+# used to get/set the BACnet specific configuration paramters
+GetParamFuncs = {}
+SetParamFuncs = {}
+
+
+# Upon PLC load, this Dictionary is initialised with the BACnet configuration
+# hardcoded in the C file
+# (i.e. the configuration inserted in Beremiz IDE when project was compiled)
+_DefaultConfiguration = None
+
+
+# Dictionary that contains the BACnet 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 the edited configuration but it contains an error.
+_WebviewConfiguration = None
+
+
+# Dictionary that stores the BACnet 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)
+_SavedConfiguration = None
+
+
+# File to which the new BACnet configuration gets stored on the PLC
+# Note that the stored configuration is likely different to the
+# configuration hardcoded in C generated code (.so file), so
+# this file should be persistent across PLC reboots so we can
+# re-configure the PLC (change values of variables in .so file)
+# before it gets a chance to start running
+#
+#_BACnetConfFilename = None
+_BACnetConfFilename = "/tmp/BeremizBACnetConfig.json"
+
+
+
+
+
+
+BACnet_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, ...)
+    ("network_interface"      , _("Network Interface")                         , ctypes.c_char_p, annotate.String),
+    ("port_number"            , _("UDP Port Number")                           , ctypes.c_char_p, annotate.String),
+    ("comm_control_passwd"    , _("BACnet Communication Control Password")     , ctypes.c_char_p, annotate.String),
+    ("device_id"              , _("BACnet Device ID")                          , ctypes.c_int,    annotate.Integer),
+    ("device_name"            , _("BACnet Device Name")                        , ctypes.c_char_p, annotate.String),
+    ("device_location"        , _("BACnet Device Location")                    , ctypes.c_char_p, annotate.String),
+    ("device_description"     , _("BACnet Device Description")                 , ctypes.c_char_p, annotate.String),
+    ("device_appsoftware_ver" , _("BACnet Device Application Software Version"), ctypes.c_char_p, annotate.String)
+    ]
+
+
+
+
+
+
+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 _CheckDeviceID(device_id):
+    """ 
+    # check validity of the Device ID 
+    # NOTE: BACnet device (object) IDs are 22 bits long (not counting the 10 bits for the type ID)
+    #       so the Device instance ID is limited from 0 to 22^2-1 = 4194303
+    #       However, 4194303 is reserved for special use (similar to NULL pointer), so last
+    #       valid ID becomes 4194302
+    """
+    try:
+        devid = int(device_id)
+        if (devid < 0) or (devid > 4194302):
+            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"])},
+            _("BACnet configuration error:"))
+        res = False
+    
+    if not _CheckDeviceID(BACnetConfig["device_id"]):
+        raise annotate.ValidateError(
+            {"device_id": "Invalid device ID: " + str(BACnetConfig["device_id"])},
+            _("BACnet configuration error:"))
+        res = False
+        
+    return res
+
+
+
+
+
+
+def _SetSavedConfiguration(BACnetConfig):
+    """ Stores in a file a dictionary containing the BACnet parameter configuration """
+    with open(os.path.realpath(_BACnetConfFilename), 'w') as f:
+        json.dump(BACnetConfig, f, sort_keys=True, indent=4)
+    global _SavedConfiguration
+    _SavedConfiguration = BACnetConfig
+
+
+def _DelSavedConfiguration():
+    """ Deletes the file cotaining the persistent BACnet configuration """
+    if os.path.exists(_BACnetConfFilename):
+        os.remove(_BACnetConfFilename)
+
+
+def _GetSavedConfiguration():
+    """
+    # Returns a dictionary containing the BACnet parameter configuration
+    # that was last saved to file. If no file exists, then return None
+    """
+    try:
+        #if os.path.isfile(_BACnetConfFilename):
+        saved_config = json.load(open(_BACnetConfFilename))
+    except Exception:    
+        return None
+
+    if _CheckConfiguration(saved_config):
+        return saved_config
+    else:
+        return None
+
+
+def _GetPLCConfiguration():
+    """
+    # Returns a dictionary containing the current BACnet parameter configuration
+    # stored in the C variables in the loaded PLC (.so file)
+    """
+    current_config = {}
+    for par_name, x1, x2, x3 in BACnet_parameters:
+        value = GetParamFuncs[par_name]()
+        if value is not None:
+            current_config[par_name] = value
+    
+    return current_config
+
+
+def _SetPLCConfiguration(BACnetConfig):
+    """
+    # Stores the BACnet parameter configuration into the
+    # the C variables in the loaded PLC (.so file)
+    """
+    for par_name in BACnetConfig:
+        value = BACnetConfig[par_name]
+        #_plcobj.LogMessage("BACnet web server extension::_SetPLCConfiguration()  Setting "
+        #                       + par_name + " to " + str(value) )
+        if value is not None:
+            SetParamFuncs[par_name](value)
+    # update the configuration shown on the web interface
+    global _WebviewConfiguration 
+    _WebviewConfiguration = _GetPLCConfiguration()
+
+
+
+def _GetWebviewConfigurationValue(ctx, argument):
+    """
+    # Callback function, called by the web interface (NevowServer.py)
+    # to fill in the default value of each parameter
+    """
+    try:
+        return _WebviewConfiguration[argument.name]
+    except Exception:
+        return ""
+
+
+# The configuration of the web form used to see/edit the BACnet parameters
+webFormInterface = [(name, web_dtype (label=web_label, default=_GetWebviewConfigurationValue)) 
+                    for name, web_label, c_dtype, web_dtype in BACnet_parameters]
+
+
+
+def _updateWebInterface():
+    """
+    # 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
+    """
+
+    # Add a "Delete Saved Configuration" button if there is a saved configuration!
+    if _SavedConfiguration is None:
+        _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved")
+    else:
+        _NS.ConfigurableSettings.addSettings(
+            "BACnetConfigDelSaved",                   # name
+            _("BACnet Configuration"),                # description
+            [],                                       # fields  (empty, no parameters required!)
+            _("Delete Configuration Stored in Persistent Storage"), # button label
+            OnButtonDel)                              # callback    
+
+
+
+def OnButtonSave(**kwargs):
+    """
+    # Function called when user clicks 'Save' button in web interface
+    # The function will configure the BACnet plugin in the PLC with the values
+    # specified in the web interface. However, values must be validated first!
+    """
+
+    #_plcobj.LogMessage("BACnet web server extension::OnButtonSave()  Called")
+    
+    newConfig = {}
+    for par_name, x1, x2, x3 in BACnet_parameters:
+        value = kwargs.get(par_name, None)
+        if value is not None:
+            newConfig[par_name] = value
+
+    global _WebviewConfiguration
+    _WebviewConfiguration = newConfig
+    
+    # First check if configuration is OK.
+    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(newConfig)
+
+    # Configure PLC with the current BACnet parameters
+    _SetPLCConfiguration(newConfig)
+
+    # File has just been created => Delete button must be shown on web interface!
+    _updateWebInterface()
+
+
+
+
+def OnButtonDel(**kwargs):
+    """
+    # Function called when user clicks 'Delete' button in web interface
+    # The function will delete the file containing the persistent
+    # BACnet configution
+    """
+
+    # Delete the file
+    _DelSavedConfiguration()
+    # Set the current configuration to the default (hardcoded in C)
+    _SetPLCConfiguration(_DefaultConfiguration)
+    # Reset global variable
+    global _SavedConfiguration
+    _SavedConfiguration = None
+    # File has just been deleted => Delete button on web interface no longer needed!
+    _updateWebInterface()
+
+
+
+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
+    """
+    
+    global _WebviewConfiguration
+    _WebviewConfiguration = _GetPLCConfiguration()
+    # File has just been deleted => Delete button on web interface no longer needed!
+    _updateWebInterface()
+
+
+
+
+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 oaded into memory
+    """
+
+    #_plcobj.LogMessage("BACnet 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 location (in the Config. Node Tree of Beremiz IDE) the BACnet plugin
+    # occupies in the currently loaded PLC project (i.e., the .so file)
+    # If the "__bacnet_plugin_location" C variable is not present in the .so file,
+    # we conclude that the currently loaded PLC does not have the BACnet plugin
+    # included (situation (2b) described above init())
+    try:
+        location = ctypes.c_char_p.in_dll(_plcobj.PLClibraryHandle, "__bacnet_plugin_location")
+    except Exception:
+        # Loaded PLC does not have the BACnet plugin => nothing to do
+        #   (i.e. do _not_ configure and make available the BACnet 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 BACnet_parameters:
+        GetParamFuncName = "__bacnet_" + location.value + "_get_ConfigParam_" + name
+        SetParamFuncName = "__bacnet_" + location.value + "_set_ConfigParam_" + name
+        
+        GetParamFuncs[name]          = getattr(_plcobj.PLClibraryHandle, GetParamFuncName)
+        GetParamFuncs[name].restype  = c_dtype
+        GetParamFuncs[name].argtypes = None
+        
+        SetParamFuncs[name]          = getattr(_plcobj.PLClibraryHandle, SetParamFuncName)
+        SetParamFuncs[name].restype  = None
+        SetParamFuncs[name].argtypes = [c_dtype]
+
+    # Default configuration is the configuration done in Beremiz IDE
+    # whose parameters get hardcoded into C, and compiled into the .so file
+    # We read the default configuration from the .so file before the values
+    # get changed by the user using the web server, or by the call (further on)
+    # to _SetPLCConfiguration(SavedConfiguration)
+    global _DefaultConfiguration 
+    _DefaultConfiguration = _GetPLCConfiguration()
+    
+    # Show the current PLC configuration on the web interface        
+    global _WebviewConfiguration
+    _WebviewConfiguration = _GetPLCConfiguration()
+ 
+    # Read from file the last used configuration, which is likely
+    # different to the hardcoded configuration.
+    # We Reset the current configuration (i.e., the config stored in the 
+    # variables of .so file) to this saved configuration
+    # so the PLC will start off with this saved configuration instead
+    # of the hardcoded (in Beremiz C generated code) configuration values.
+    #
+    # Note that _SetPLCConfiguration() will also update 
+    # _WebviewConfiguration , if necessary.
+    global _SavedConfiguration
+    _SavedConfiguration  = _GetSavedConfiguration()
+    if _SavedConfiguration is not None:
+        if _CheckConfiguration(_SavedConfiguration):
+            _SetPLCConfiguration(_SavedConfiguration)
+            
+    # Configure the web interface to include the BACnet config parameters
+    _NS.ConfigurableSettings.addSettings(
+        "BACnetConfigParm",                # name
+        _("BACnet Configuration"),         # description
+        webFormInterface,                  # fields
+        _("Save Configuration to Persistent Storage"),  # button label
+        OnButtonSave)                      # callback    
+    
+    # Add a "View Current Configuration" button 
+    _NS.ConfigurableSettings.addSettings(
+        "BACnetConfigViewCur",                    # name
+        _("BACnet Configuration"),                # description
+        [],                                       # fields  (empty, no parameters required!)
+        _("Show Current PLC Configuration"),      # button label
+        OnButtonShowCur)                          # callback    
+
+    # Add the Delete button to the web interface, if required
+    _updateWebInterface()
+
+
+
+
+
+def OnUnLoadPLC():
+    """
+    # Callback function, called (by PLCObject.py) when a PLC program is unloaded from memory
+    """
+
+    #_plcobj.LogMessage("BACnet web server extension::OnUnLoadPLC() Called...")
+    
+    # Delete the BACnet specific web interface extensions
+    # (Safe to ask to delete, even if it has not been added!)
+    _NS.ConfigurableSettings.delSettings("BACnetConfigParm")
+    _NS.ConfigurableSettings.delSettings("BACnetConfigViewCur")  
+    _NS.ConfigurableSettings.delSettings("BACnetConfigDelSaved")  
+    GetParamFuncs = {}
+    SetParamFuncs = {}
+    _WebviewConfiguration = None
+    _SavedConfiguration   = None
+
+
+
+
+# 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 BACnet plugin
+#         (b) The loaded PLC does not have the BACnet plugin
+#
+# During (1) and (2a):
+#     we configure the web server interface to not have the BACnet web configuration extension
+# During (2b) 
+#     we configure the web server interface to include the BACnet web configuration extension
+#
+# plcobj    : reference to the PLCObject defined in 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):
+    #plcobj.LogMessage("BACnet web server extension::init(plcobj, NS, " + WorkingDir + ") Called")
+    global _WorkingDir
+    _WorkingDir = WorkingDir
+    global _plcobj
+    _plcobj = plcobj
+    global _NS
+    _NS = NS
+    global _BACnetConfFilename
+    if _BACnetConfFilename is None:
+        _BACnetConfFilename = os.path.join(WorkingDir, "BACnetConfig.json")
+    
+    _plcobj.RegisterCallbackLoad  ("BACnet_Settins_Extension", OnLoadPLC)
+    _plcobj.RegisterCallbackUnLoad("BACnet_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	Wed Dec 18 13:31:22 2019 +0100
+++ b/runtime/NevowServer.py	Thu May 28 11:16:59 2020 +0100
@@ -179,7 +179,13 @@
 
         setattr(self, 'action_' + name, callback)
 
-        self.bindingsNames.append(name)
+        if name not in self.bindingsNames:
+            self.bindingsNames.append(name)
+
+
+    def delSettings(self, name):
+        if name in self.bindingsNames:
+            self.bindingsNames.remove(name)
 
 
 ConfigurableSettings = ConfigurableBindings()
--- a/runtime/PLCObject.py	Wed Dec 18 13:31:22 2019 +0100
+++ b/runtime/PLCObject.py	Thu May 28 11:16:59 2020 +0100
@@ -99,6 +99,9 @@
         self.TraceLock = Lock()
         self.Traces = []
         self.DebugToken = 0
+        # Callbacks used by web settings extensions (e.g.: BACnet_config.py, Modbus_config.py)
+        self.LoadCallbacks   = {} # list of functions to call when PLC is   loaded
+        self.UnLoadCallbacks = {} # list of functions to call when PLC is unloaded
 
         self._init_blobs()
 
@@ -167,6 +170,22 @@
             return self._loading_error, 0, 0, 0
         return None
 
+    def RegisterCallbackLoad(self, ExtensionName, ExtensionCallback):
+        """
+        Register function to be called when PLC is loaded
+        ExtensionName: a string with the name of the extension asking to register the callback 
+        ExtensionCallback: the function to be called...
+        """
+        self.LoadCallbacks[ExtensionName] = ExtensionCallback
+
+    def RegisterCallbackUnLoad(self, ExtensionName, ExtensionCallback):
+        """
+        Register function to be called when PLC is unloaded
+        ExtensionName: a string with the name of the extension asking to register the callback 
+        ExtensionCallback: the function to be called...
+        """
+        self.UnLoadCallbacks[ExtensionName] = ExtensionCallback
+
     def _GetMD5FileName(self):
         return os.path.join(self.workingdir, "lasttransferedPLC.md5")
 
@@ -270,6 +289,8 @@
         res = self._LoadPLC()
         if res:
             self.PythonRuntimeInit()
+            for name, callbackFunc in self.LoadCallbacks.items():
+                callbackFunc()
         else:
             self._FreePLC()
 
@@ -278,6 +299,8 @@
     @RunInMain
     def UnLoadPLC(self):
         self.PythonRuntimeCleanup()
+        for name, callbackFunc in self.UnLoadCallbacks.items():
+            callbackFunc()        
         self._FreePLC()
 
     def _InitPLCStubCalls(self):