--- 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")),
+ )