使用python-opcua 实现modbus网关(2)

 

         我们继续来研究如何使用python-opcua 实现opcua/modbus 网关。 opcua 开发包包含了大量的函数,通过研究opcua/modbus 网关的实现,可以了解这些函数的使用方法。由于函数过多,文章中函数的使用方式可能不尽合理,或者存在错误。希望读者指正和讨论。

信息模型

 构建了两个模型,一个是motor ,另一个是modbus。motor 对象具有四个属性变量(Property):

  • 状态
  • 电流
  • 电压
  • 温度
  • 速度

modbus 对象有三类对象,它们分别是

  1. Coils
  2. inputRegisters
  3. holdingRegisters 

在它们的内部包含了一些modbus的变量地址。 而变量的长度是由对应的opcua 属性的datatype 确定的,例如 Float 是32位,对应modbus 两个register。

 

 OPCUA 信息模型与modbus 通过to_modbus 引用建立联系。它的反向名称是to_Property

信息模型的描述,编译

        使用前面博文介绍的方法,使用UA ModelCompiler 的Model.xml来描述,通过UA ModelCompiler 编译成NodeSet2 文档,由OPCUA Server 读入。你也可以使用uaModeler 来构建和生成NodeSet2 文档。

我使用UA Modelcompiler 方法

扫描二维码关注公众号,回复: 15995915 查看本文章
<?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>

数据网关方式

实验项目的结构如下:

         modbusTCP 是一个简单的modbus设备仿真程序(比如·PLC),产生动态数据。 OpcUa/modbus Gayeway 通过modbusTCP 协议访问 modbusTCP Server,OpcUa Client或者uaExperty 通过OpcUa 访问OpcUa /modbus Gateway.

轮询数据的方法

轮询数据的方式分为两种:

按需读取(on Demand)

        当client  需要读取数据时,通过Opcua 协议发送 Read_Value()请求。在网关中,转换为modbusTCP 的Read_inputRegisters或者Read_holdingRegisters。Write_Value 也是类似的方式,这种方式是同步访问方式(sync access)

轮询方式(Cycle polling)

按照一定的周期轮询modbusTCP Server 的数据。轮询程序的位置可以放置在两个地方

  1. Gateway端

     Gateway中有一个定时器轮询modbusTCP server 的数据,存放到OpcUa 的信息模型中。OPC UA Client 异步的方式访问Gateway中的信息模型中的数据。

  1. Client端

 在OpcUa 的Client 端轮询。这类似与按需存取,是一种同步方式。

在实验项目中,我们采取Gateway 端的轮询方法。

 Python 实现的要点

读取Holding 寄存器(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  找到holding_register 节点,

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

step 找出holding_registers 目录下的所有holdingRegister 这些寄存器的值是该寄存器地址。这里数据为Float 对应两个modbus register。

holding_register1 3000

holding_register2 3002

Step 3 读取所有holding register的值

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

Step 4读出来的值是两个16位int,转换位Float

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

Step5 通过to_modbus_ref 引用找到对应的Node ,并且设置值

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

改变数据通知(datachange_notification)

        当Client 写入Property 值时,需要将该值写入modbusTCP Server 。在Open62541 中,有BeforeRead和AfterWrite 函数,在Python-opcua 中,是通过建立一个子处理(subHandler) 来响应数据的改变。

        下面这一段程序监控 Temperature,当其值改变时,会调用 datachange_notification的方法。这里我们做了一些简化,没有判断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"))

完整的程序

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()

上述代码会持续改进。

猜你喜欢

转载自blog.csdn.net/yaojiawan/article/details/131529431