Use python-opcua to implement modbus gateway (2)

 

         Let's continue to study how to use python-opcua to implement opcua/modbus gateway. The opcua development kit contains a large number of functions. By studying the implementation of the opcua/modbus gateway, you can learn how to use these functions. Due to the large number of functions, the functions may not be used in a reasonable way in the article, or there may be errors. I hope readers can point out and discuss.

information model

 Two models are constructed, one is motor and the other is modbus. The motor object has four property variables (Property):

  • state
  • electric current
  • Voltage
  • temperature
  • speed

Modbus objects have three types of objects, they are

  1. Coils
  2. inputRegisters
  3. holdingRegisters 

They contain some modbus variable addresses inside. The length of the variable is determined by the datatype of the corresponding opcua attribute, for example, Float is 32 bits, corresponding to two modbus registers.

 

 The OPCUA information model and modbus are connected through to_modbus reference. Its reverse name is to_Property

Description of the information model, compiled

        Use the method introduced in the previous blog post, use UA ModelCompiler's Model.xml to describe, compile it into a NodeSet2 document through UA ModelCompiler, and read it in by OPCUA Server. You can also use uaModeler to build and generate NodeSet2 documents.

I use UA Modelcompiler method

<?xml version="1.0" encoding="utf-8"?>
<ModelDesign xmlns:OpcUaModbus="http://www.maxim.org/Modbus/"
             xmlns:OpcUa="http://opcfoundation.org/UA/"
             xmlns:uax="http://opcfoundation.org/UA/2008/02/Types.xsd"
             xmlns:xsd="http://www.w3.org/2001/XMLSchema"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             TargetNamespace="http://www.maxim.org/Modbus/"
             TargetXmlNamespace="http://www.maxim.org/Modbus/"
             TargetVersion="1.00"
             TargetPublicationDate="2023-06-25T17:49:15"
             xmlns="http://opcfoundation.org/UA/ModelDesign.xsd">
    <Namespaces>
        <Namespace Name="OpcUaModbus"
                   Prefix="OpcUaModbus"
                   XmlPrefix="OpcUaModbus">http://www.maxim.org/Modbus/</Namespace>
        <Namespace Name="OpcUa"
                   Version="1.03"
                   PublicationDate="2013-12-02T00:00:00Z"
                   Prefix="Opc.Ua"
                   InternalPrefix="Opc.Ua.Server"
                   XmlNamespace="http://opcfoundation.org/UA/2008/02/Types.xsd"
                   XmlPrefix="OpcUa">http://opcfoundation.org/UA/</Namespace>
    </Namespaces>
    <ReferenceType SymbolicName="OpcUaModbus:To_Modbus"
                   BaseType="OpcUa:HierarchicalReferences">
        <Description>modbus EndPoint</Description>
        <InverseName>To_Property</InverseName>
    </ReferenceType>
    <Object SymbolicName="OpcUaModbus:Motor"
            TypeDefinition="OpcUa:BaseObjectType">
        <Children>
           <Property SymbolicName="OpcUaModbus:Status"
                      DataType="OpcUa:Boolean">
                <DefaultValue>
                    <uax:Boolean>true</uax:Boolean>
                </DefaultValue>
                <References>
                    <Reference IsInverse="false">
                        <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                        <TargetId>OpcUaModbus:Device_Coils_Coil1</TargetId>
                    </Reference>
                </References>
            </Property>
            <Property SymbolicName="OpcUaModbus:Current"
                      DataType="OpcUa:Float">
                <DefaultValue>
                    <uax:Float>10</uax:Float>
                </DefaultValue>
                <References>
                    <Reference IsInverse="false">
                        <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                        <TargetId>OpcUaModbus:Device_inputRegisters_inputRegister1</TargetId>
                    </Reference>
                </References>
            </Property>
            <Property SymbolicName="OpcUaModbus:Voltage"
                      DataType="OpcUa:Float">
                <DefaultValue>
                    <uax:Float>10</uax:Float>
                </DefaultValue>
                <References>
                    <Reference IsInverse="false">
                        <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                        <TargetId>OpcUaModbus:Device_inputRegisters_inputRegister2</TargetId>
                    </Reference>
                </References>
            </Property>
                  <Property SymbolicName="OpcUaModbus:Temperature"
                      DataType="OpcUa:Float">
                <DefaultValue>
                    <uax:Float>10</uax:Float>
                </DefaultValue>
                <References>
                    <Reference IsInverse="false">
                        <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                        <TargetId>OpcUaModbus:Device_holdingRegisters_holdingRegister1</TargetId>
                    </Reference>
                </References>
            </Property>
              
                  <Property SymbolicName="OpcUaModbus:Speed"
                      DataType="OpcUa:Int16">
                <DefaultValue>
                    <uax:Int16>10</uax:Int16>
                </DefaultValue>
                <References>
                    <Reference IsInverse="false">
                        <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                        <TargetId>OpcUaModbus:Device_holdingRegisters_holdingRegister2</TargetId>
                    </Reference>
                </References>
            </Property>
        </Children>
        <References>
            <Reference IsInverse="true">
                <ReferenceType>OpcUa:Organizes</ReferenceType>
                <TargetId>OpcUa:ObjectsFolder</TargetId>
            </Reference>
        </References>
    </Object>
    <Object SymbolicName="OpcUaModbus:Device"
            TypeDefinition="OpcUa:BaseObjectType">
        <Children>
            <Object SymbolicName="OpcUaModbus:Coils"
                    TypeDefinition="OpcUa:FolderType">
                <Children>
                    <Property SymbolicName="OpcUaModbus:Coil1"
                              DataType="OpcUa:UInt16">
                        <DefaultValue>
                            <uax:String>4000</uax:String>
                        </DefaultValue>
                    </Property>
                </Children>
            </Object>
            <Object SymbolicName="OpcUaModbus:holdingRegisters"
                    TypeDefinition="OpcUa:FolderType">
                <Children>
                    <Property SymbolicName="OpcUaModbus:holdingRegister1"
                              DataType="OpcUa:UInt16">
                        <DefaultValue>
                            <uax:String>3000</uax:String>
                        </DefaultValue>
                    </Property>
                    <Property SymbolicName="OpcUaModbus:holdingRegister2"
                              DataType="OpcUa:UInt16">
                        <DefaultValue>
                            <uax:String>3002</uax:String>
                        </DefaultValue>
                    </Property>
                </Children>
            </Object>
            <Object SymbolicName="OpcUaModbus:inputRegisters"
                    TypeDefinition="OpcUa:FolderType">
                <Children>
                    <Property SymbolicName="OpcUaModbus:inputRegister1"
                              DataType="OpcUa:UInt16">
                        <DefaultValue>
                            <uax:String>5000</uax:String>
                        </DefaultValue>
                        <References>
                            <Reference IsInverse="true">
                                <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                                <TargetId>OpcUaModbus:Motor_Current</TargetId>
                            </Reference>
                        </References>
                    </Property>
                    <Property SymbolicName="OpcUaModbus:inputRegister2"
                              DataType="OpcUa:UInt16">
                        <DefaultValue>
                            <uax:String>5002</uax:String>
                        </DefaultValue>
                        <References>
                            <Reference IsInverse="true">
                                <ReferenceType>OpcUaModbus:To_Modbus</ReferenceType>
                                <TargetId>OpcUaModbus:Motor_Voltage</TargetId>
                            </Reference>
                        </References>
                    </Property>
                </Children>
            </Object>
        </Children>
        <References>
            <Reference IsInverse="true">
                <ReferenceType>OpcUa:Organizes</ReferenceType>
                <TargetId>OpcUa:ObjectsFolder</TargetId>
            </Reference>
        </References>
    </Object>
</ModelDesign>

data gateway method

The structure of the experimental project is as follows:

         modbusTCP is a simple modbus device emulation program (such as a PLC) that generates dynamic data. OpcUa/modbus Gayeway accesses modbusTCP Server through modbusTCP protocol, and OpcUa Client or uaExperty accesses OpcUa /modbus Gateway through OpcUa.

Methods for Polling Data

There are two ways to poll data:

Read on demand (on Demand)

        When the client needs to read data, it sends a Read_Value() request through the Opcua protocol. In the gateway, convert to Read_inputRegisters or Read_holdingRegisters of modbusTCP. Write_Value is also a similar method, this method is a synchronous access method (sync access)

Polling method (Cycle polling)

Polling the data of modbusTCP Server according to a certain cycle. The location of the poller can be placed in two places

  1. Gateway side

     There is a timer in the Gateway to poll the data of the modbusTCP server and store it in the information model of OpcUa. The OPC UA Client accesses the data in the information model in the Gateway asynchronously.

  1. Client

 Polling on the client side of OpcUa. This is similar to on-demand access, which is a synchronous method.

In the experimental project, we adopt the polling method on the Gateway side.

 Essentials of the Python implementation

Read Holding Registers (Read_Holding_Registers)

def Read_Holding_Registers():
        global to_modbus_ref
        root=server.get_root_node()
        holdingRegisters=root.get_child(["0:Objects", "2:Device", "2:holdingRegisters"])
        Childrens=holdingRegisters.get_children()
        for children in Childrens:
            address=children.get_value()
            reg_l=ModbusInterface.read_input_registers(int(address),2)
            val=utils.word_list_to_long(reg_l)
            value=utils.decode_ieee(val[0],False)
            OpcUa_Property=children.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Inverse,0,True)
            OpcUa_Property[0].set_value(value)

 step1 Find the holding_register node,

holdingRegisters=root.get_child(["0:Objects", "2:Device", "2:holdingRegisters"])

step Find all holdingRegisters under the holding_registers directory. The values ​​of these registers are the register addresses. The data here is Float corresponding to two modbus registers.

holding_register1 3000

holding_register2 3002

Step 3 Read the values ​​of all holding registers

 address=children.get_value()
 reg_l=ModbusInterface.read_input_registers(int(address),2)

The values ​​read in Step 4 are two 16-bit ints, converted to Float

 val=utils.word_list_to_long(reg_l)
 value=utils.decode_ieee(val[0],False)

Step5 Find the corresponding Node through to_modbus_ref reference, and set the value

OpcUa_Property=children.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Inverse,0,True)
OpcUa_Property[0].set_value(value)

Change Data Notification (datachange_notification)

        When the Client writes the Property value, it needs to write the value to the modbusTCP Server. In Open62541, there are BeforeRead and AfterWrite functions. In Python-opcua, a subhandler (subHandler) is established to respond to data changes.

        The following program monitors Temperature, and when its value changes, the datachange_notification method is called. Here we have made some simplifications, without judging the situation of Coils.

   

class SubHandler(object):
   def datachange_notification(self, node, val, data):
        print("Python: New data change event", node, val)
        modbusEndpoint=node.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Forward,0,True)
        print(modbusEndpoint)
        Address=modbusEndpoint[0].get_value
        #parentNode=modbusEndpoint[0].get_parent()
        #parentNodeName=parentNode.get_browse_name().Name  
        b32_l=[utils.encode_ieee(val,False)]
        regs_value = utils.long_list_to_word(b32_l)
        ModbusInterface.write_multiple_registers(Address, regs_value)
        #print(parentNode.get_browse_name().Name)
        pass
........

    server.start()  
    handler = SubHandler()
    sub = server.create_subscription(100, handler)
    handle = sub.subscribe_data_change(get_Property_By_Name("2:Temperature"))

complete program

import sys
sys.path.insert(0, "..")
import time
from opcua import  ua,Server
from pyModbusTCP.client import ModbusClient # Modbus TCP Client
from pyModbusTCP import utils
class SubHandler(object):
   def datachange_notification(self, node, val, data):
        print("Python: New data change event", node, val)
        modbusEndpoint=node.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Forward,0,True)
        print(modbusEndpoint)
        Address=modbusEndpoint[0].get_value
        #parentNode=modbusEndpoint[0].get_parent()
        #parentNodeName=parentNode.get_browse_name().Name  
        b32_l=[utils.encode_ieee(val,False)]
        regs_value = utils.long_list_to_word(b32_l)
        ModbusInterface.write_multiple_registers(Address, regs_value)
        #print(parentNode.get_browse_name().Name)
        pass
def get_Property_By_Name(Name):
        root=server.get_root_node()
        Property=root.get_child(["0:Objects", "2:Motor",Name])
        print(Property.get_browse_name())
        return   Property 
    
def get_referenced_Type_By_Name(Name):
        root=server.get_root_node()
        ReferenceType=root.get_child(["0:Types", "0:ReferenceTypes", "0:References","0:HierarchicalReferences",Name])
        return   ReferenceType 
def get_Property_DataType(Property):
               DataTypeNodeId=Property.get_data_type()
               return server.get_node(DataTypeNodeId).get_browse_name().Name
               
def Read_Input_Registers():
        global to_modbus_ref
        root=server.get_root_node()
        inputRegisters=root.get_child(["0:Objects", "2:Device", "2:inputRegisters"])
        Childrens=inputRegisters.get_children()
        for children in Childrens:
            OpcUa_Property=children.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Inverse,0,True)

            DataType=get_Property_DataType(OpcUa_Property[0])
            print(DataType)
            address=children.get_value()
            #print(address)
            reg_l=ModbusInterface.read_input_registers(int(address),2)
            val=utils.word_list_to_long(reg_l)
            value=utils.decode_ieee(val[0],False)
            #print(to_modbus_ref)
            #print(children.get_browse_name())
           
            OpcUa_Property[0].set_value(value)
            #print(OpcUa_Property[0].get_browse_name())
def Read_Holding_Registers():
        global to_modbus_ref
        root=server.get_root_node()
        holdingRegisters=root.get_child(["0:Objects", "2:Device", "2:holdingRegisters"])
        Childrens=holdingRegisters.get_children()
        for children in Childrens:
            address=children.get_value()
            reg_l=ModbusInterface.read_input_registers(int(address),2)
            val=utils.word_list_to_long(reg_l)
            value=utils.decode_ieee(val[0],False)
            OpcUa_Property=children.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Inverse,0,True)
            OpcUa_Property[0].set_value(value)
def Read_Coils():
        global to_modbus_ref
        root=server.get_root_node()
        Coils=root.get_child(["0:Objects", "2:Device", "2:Coils"])
        Childrens=Coils.get_children()
        for children in Childrens:
            address=children.get_value()
            val=ModbusInterface.read_coils(int(address),1)       
            OpcUa_Property=children.get_referenced_nodes(to_modbus_ref,ua.BrowseDirection.Inverse,0,True)
            OpcUa_Property[0].set_value(val)
if __name__ == "__main__":

    # setup our server
    server = Server()
    server.set_endpoint("opc.tcp://127.0.0.1:48400/freeopcua/server/")
    server.import_xml("OpcUaModbus.NodeSet2.xml")
    to_modbus_ref=get_referenced_Type_By_Name("2:To_Modbus")
    #print(to_modbus_ref)
    # get Objects node, this is where we should put our nodes
    #objects = server.get_objects_node() 
    ModbusInterface = ModbusClient(host="localhost", port=502, unit_id=1, auto_open=True, auto_close=False) 
    CurrebtNode=get_Property_By_Name("2:Current")
    CurrebtNode.set_writable()
    VoltageNode=get_Property_By_Name("2:Voltage")
    VoltageNode.set_writable()
    VoltageNode=get_Property_By_Name("2:Temperature")
    VoltageNode.set_writable()
    # starting!
    server.start()  
    handler = SubHandler()
    sub = server.create_subscription(100, handler)
    handle = sub.subscribe_data_change(get_Property_By_Name("2:Temperature"))
    try:
        count = 0
        while True:
            time.sleep(1)
            Read_Input_Registers()
            #reg_l=ModbusInterface.read_input_registers(0,2)
            #val=utils.word_list_to_long(reg_l)
            #print(utils.decode_ieee(val[0],False)) 
    finally:
        #close connection, remove subcsriptions, etc
        server.stop()

The above code will be continuously improved.

Guess you like

Origin blog.csdn.net/yaojiawan/article/details/131529431