diff -r a0932a52e53b -r 1627d552f181 modbus/modbus.py --- 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 @@ + @@ -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 = """ + - + @@ -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 @@ + @@ -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 = """ + @@ -388,7 +467,7 @@ - + @@ -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 = """ + @@ -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")), + )