我们继续来研究如何使用python-opcua 实现opcua/modbus 网关。 opcua 开发包包含了大量的函数,通过研究opcua/modbus 网关的实现,可以了解这些函数的使用方法。由于函数过多,文章中函数的使用方式可能不尽合理,或者存在错误。希望读者指正和讨论。
信息模型
构建了两个模型,一个是motor ,另一个是modbus。motor 对象具有四个属性变量(Property):
- 状态
- 电流
- 电压
- 温度
- 速度
modbus 对象有三类对象,它们分别是
- Coils
- inputRegisters
- 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 方法
<?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 的数据。轮询程序的位置可以放置在两个地方
- Gateway端
Gateway中有一个定时器轮询modbusTCP server 的数据,存放到OpcUa 的信息模型中。OPC UA Client 异步的方式访问Gateway中的信息模型中的数据。
- Client端
在OpcUa 的Client 端轮询。这类似与按需存取,是一种同步方式。
在实验项目中,我们采取Gateway 端的轮询方法。
Python 实现的要点
读取Holding 寄存器(Read_Holding_Registers)
def Read_Holding_Registers():global to_modbus_refroot=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().Namedef Read_Input_Registers():global to_modbus_refroot=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_refroot=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_refroot=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 serverserver = 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 = 0while 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, etcserver.stop()
上述代码会持续改进。