modbus/modbus.py
branchsvghmi
changeset 2982 1627d552f181
parent 2677 556935640ec0
child 2703 32ffdb32b14e
child 2716 ebb2595504f0
--- a/modbus/modbus.py	Thu Jun 18 10:42:08 2020 +0200
+++ b/modbus/modbus.py	Thu Jun 18 11:00:26 2020 +0200
@@ -30,6 +30,7 @@
 from modbus.mb_utils import *
 from ConfigTreeNode import ConfigTreeNode
 from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY
+import util.paths as paths
 
 base_folder = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0]
 base_folder = os.path.join(base_folder, "..")
@@ -83,6 +84,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 +117,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),
@@ -260,17 +284,20 @@
 #
 #
 
+# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique
+
 class _ModbusTCPclientPlug(object):
     XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
     <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">
             <xsd:simpleType>
                 <xsd:restriction base="xsd:unsignedLong">
-                    <xsd:minInclusive value="1"/>
+                    <xsd:minInclusive value="0"/>
                     <xsd:maxInclusive value="2147483647"/>
                 </xsd:restriction>
             </xsd:simpleType>
@@ -285,11 +312,33 @@
     # 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):
         return (1, 0, 0)
 
+    def GetConfigName(self):
+        """ Return the node's Configuration_Name """
+        return self.ModbusTCPclient.getConfiguration_Name()
+
     def CTNGenerate_C(self, buildpath, locations):
         """
         Generate C code
@@ -314,6 +363,8 @@
 #
 #
 
+# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique
+
 class _ModbusTCPserverPlug(object):
     # NOTE: the Port number is a 'string' and not an 'integer'!
     # This is because the underlying modbus library accepts strings
@@ -322,6 +373,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">
@@ -340,17 +392,41 @@
     # 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):
         return (1, 0, 0)
 
-    # Return a list with a single tuple conatining the (location, port number)
-    #     location: location of this node in the configuration tree
+    # Return a list with a single tuple conatining the (location, IP address, port number)
+    #     location   : location of this node in the configuration tree
     #     port number: IP port used by this Modbus/IP server
+    #     IP address : IP address of the network interface on which the server will be listening
+    #                  ("", "*", or "#ANY#" => listening on all interfaces!)
     def GetIPServerPortNumbers(self):
-        port = self.GetParamsAttributes()[0]["children"][1]["value"]
-        return [(self.GetCurrentLocation(), port)]
+        port = self.ModbusServerNode.getLocal_Port_Number()
+        addr = self.ModbusServerNode.getLocal_IP_Address()
+        return [(self.GetCurrentLocation(), addr, port)]
+
+    def GetConfigName(self):
+        """ Return the node's Configuration_Name """
+        return self.ModbusServerNode.getConfiguration_Name()
 
     def CTNGenerate_C(self, buildpath, locations):
         """
@@ -376,11 +452,14 @@
 #
 #
 
+# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique
+
 class _ModbusRTUclientPlug(object):
     XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
     <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"/>
@@ -388,7 +467,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>
@@ -403,6 +482,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:
@@ -421,6 +517,10 @@
     def GetNodeCount(self):
         return (0, 1, 0)
 
+    def GetConfigName(self):
+        """ Return the node's Configuration_Name """
+        return self.ModbusRTUclient.getConfiguration_Name()
+
     def CTNGenerate_C(self, buildpath, locations):
         """
         Generate C code
@@ -445,12 +545,14 @@
 #
 #
 
+# XXX TODO "Configuration_Name" should disapear in favor of CTN Name, which is already unique
 
 class _ModbusRTUslavePlug(object):
     XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
     <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"/>
@@ -471,6 +573,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:
@@ -489,6 +608,10 @@
     def GetNodeCount(self):
         return (0, 1, 0)
 
+    def GetConfigName(self):
+        """ Return the node's Configuration_Name """
+        return self.ModbusRTUslave.getConfiguration_Name()
+
     def CTNGenerate_C(self, buildpath, locations):
         """
         Generate C code
@@ -550,8 +673,7 @@
                 x1 + x2 for x1, x2 in zip(total_node_count, child.GetNodeCount()))
         return total_node_count
 
-    # Return a list with tuples of the (location, port numbers) used by all
-    # the Modbus/IP servers
+    # Return a list with tuples of the (location, port numbers) used by all the Modbus/IP servers
     def GetIPServerPortNumbers(self):
         IPServer_port_numbers = []
         for child in self.IECSortedChildren():
@@ -559,6 +681,13 @@
                 IPServer_port_numbers.extend(child.GetIPServerPortNumbers())
         return IPServer_port_numbers
 
+    # Return a list with tuples of the (location, configuration_name) used by all the Modbus nodes (tcp/rtu, clients/servers)
+    def GetConfigNames(self):
+        Node_Configuration_Names = []
+        for child in self.IECSortedChildren():
+            Node_Configuration_Names.extend([(child.GetCurrentLocation(), child.GetConfigName())])
+        return Node_Configuration_Names
+
     def CTNGenerate_C(self, buildpath, locations):
         # print "#############"
         # print self.__class__
@@ -573,40 +702,61 @@
 
         # Determine the number of (modbus library) nodes ALL instances of the modbus plugin will need
         #   total_node_count: (tcp nodes, rtu nodes, ascii nodes)
-        # Also get a list with tuples of (location, IP port numbers) used by all the Modbus/IP server nodes
+        #
+        # Also get a list with tuples of (location, IP address, port number) used by all the Modbus/IP server nodes
         #   This list is later used to search for duplicates in port numbers!
-        #   IPServer_port_numbers = [(location ,IPserver_port_number), ...]
-        #       location: tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x"
-        # IPserver_port_number: a number (i.e. port number used by the
-        # Modbus/IP server)
+        #   IPServer_port_numbers = [(location, IP address, port number), ...]
+        #       location            : tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x"
+        #       IPserver_port_number: a number (i.e. port number used by the Modbus/IP server)
+        #       IP address          : IP address of the network interface on which the server will be listening
+        #                             ("", "*", or "#ANY#" => listening on all interfaces!)
+        #
+        # Also get a list with tuples of (location, Configuration_Name) used by all the Modbus nodes
+        #   This list is later used to search for duplicates in Configuration Names!
+        #   Node_Configuration_Names = [(location, Configuration_Name), ...]
+        #       location          : tuple similar to (0, 3, 1) representing the location in the configuration tree "0.3.1.x"
+        #       Configuration_Name: the "Configuration_Name" string
         total_node_count = (0, 0, 0)
-        IPServer_port_numbers = []
+        IPServer_port_numbers    = []
+        Node_Configuration_Names = []
         for CTNInstance in self.GetCTRoot().IterChildren():
             if CTNInstance.CTNType == "modbus":
-                # ask each modbus plugin instance how many nodes it needs, and
-                # add them all up.
-                total_node_count = tuple(x1 + x2 for x1, x2 in zip(
-                    total_node_count, CTNInstance.GetNodeCount()))
-                IPServer_port_numbers.extend(
-                    CTNInstance.GetIPServerPortNumbers())
+                # ask each modbus plugin instance how many nodes it needs, and add them all up.
+                total_node_count = tuple(x1 + x2 for x1, x2 in zip(total_node_count, CTNInstance.GetNodeCount()))
+                IPServer_port_numbers.   extend(CTNInstance.GetIPServerPortNumbers())
+                Node_Configuration_Names.extend(CTNInstance.GetConfigNames        ())
+
+        # Search for use of duplicate Configuration_Names by Modbus nodes
+        # Configuration Names are used by the web server running on the PLC
+        # (more precisely, run by Beremiz_service.py) to identify and allow 
+        # changing the Modbus parameters after the program has been downloaded 
+        # to the PLC (but before it is started)
+        # With clashes in the configuration names, the Modbus nodes will not be
+        # distinguasheble on the web interface!
+        for i in range(0, len(Node_Configuration_Names) - 1):
+            for j in range(i + 1, len(Node_Configuration_Names)):
+                if Node_Configuration_Names[i][1] == Node_Configuration_Names[j][1]:
+                    error_message = _("Error: Modbus plugin nodes %{a1}.x and %{a2}.x use the same Configuration_Name \"{a3}\".\n").format(
+                                        a1=_lt_to_str(Node_Configuration_Names[i][0]),
+                                        a2=_lt_to_str(Node_Configuration_Names[j][0]),
+                                        a3=Node_Configuration_Names[j][1])
+                    self.FatalError(error_message)
 
         # Search for use of duplicate port numbers by Modbus/IP servers
-        # print IPServer_port_numbers
-        # ..but first define a lambda function to convert a tuple with the config tree location to a nice looking string
-        #   for e.g., convert the tuple (0, 3, 4) to "0.3.4"
-
-        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
+        # Note: We only consider duplicate port numbers if using the same network interface!
+        i = 0
+        for loc1, addr1, port1 in IPServer_port_numbers[:-1]:
+            i = i + 1
+            for loc2, addr2, port2 in IPServer_port_numbers[i:]:
+                if (port1 == port2) and (
+                          (addr1 == addr2)   # on the same network interface
+                       or (addr1 == "") or (addr1 == "*") or (addr1 == "#ANY#") # or one (or both) of the servers
+                       or (addr2 == "") or (addr2 == "*") or (addr2 == "#ANY#") # use all available network interfaces
+                   ):
+                    error_message = _("Error: Modbus plugin nodes %{a1}.x and %{a2}.x use same port number \"{a3}\" " + 
+                                      "on the same (or overlapping) network interfaces \"{a4}\" and \"{a5}\".\n").format(
+                                        a1=_lt_to_str(loc1), a2=_lt_to_str(loc2), a3=port1, a4=addr1, a5=addr2)
+                    self.FatalError(error_message)
 
         # Determine the current location in Beremiz's project configuration
         # tree
@@ -720,12 +870,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 +915,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
@@ -803,4 +993,18 @@
         # LDFLAGS.append(" -lws2_32 ")  # on windows we need to load winsock
         # library!
 
-        return [(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True
+        websettingfile = open(paths.AbsNeighbourFile(__file__, "web_settings.py"), 'r')
+        websettingcode = websettingfile.read()
+        websettingfile.close()
+
+        location_str = "_".join(map(str, self.GetCurrentLocation()))
+        websettingcode = websettingcode % locals()
+
+        runtimefile_path = os.path.join(buildpath, "runtime_modbus_websettings.py")
+        runtimefile = open(runtimefile_path, 'w')
+        runtimefile.write(websettingcode)
+        runtimefile.close()
+
+        return ([(Gen_MB_c_path, ' -I"' + ModbusPath + '"')], LDFLAGS, True,
+                ("runtime_modbus_websettings_%s.py" % location_str, open(runtimefile_path, "rb")),
+        )