我们继续来研究如何使用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_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()
上述代码会持续改进。