Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom type methods with examples #368

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
44 changes: 44 additions & 0 deletions examples/complex-type-client-server/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from opcua import Client
from common import KeyValuePair


class HelloClient(object):
def __init__(self, endpoint):
self._client = Client(endpoint)

# We cannot set them properly as we are still not connected to the server
self._root = None
self._objects = None
self._hellower = None

def __enter__(self):
# __enter__ and __exit__ are called when getting the object with the with keyword. See context manager
# documentation for more information
self._client.connect()

# As soon as we are connected to the server, we set the variables
self._root = self._client.get_root_node()
self._objects = self._client.get_objects_node()
self._hellower = self._objects.get_child("0:Hellower")
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self._client.disconnect()

def say_hello(self, complex_variable, complex_variable_list):
"""Adapt the method call so it is used like a normal python method"""
return self._hellower.call_method(
"0:SayComplexHello",
complex_variable,
complex_variable_list
)


if __name__ == '__main__':
with HelloClient("opc.tcp://localhost:40840/freeopcua/server/") as hello_client:
complex_error, complex_error_list = hello_client.say_hello(
KeyValuePair("foo", "bar"),
[KeyValuePair("toto", "tata"), KeyValuePair("Hello", "World")],
)

print(complex_error, complex_error_list)
75 changes: 75 additions & 0 deletions examples/complex-type-client-server/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from opcua.common.methods import to_variant
from opcua.ua import ua_binary as uabin
from opcua.ua.uatypes import Variant
from opcua.common.ua_utils import register_extension_object


class KeyValuePair(object):
# The DEFAULT_BINARY is the NodeId of the custom type
DEFAULT_BINARY = 20001

def __init__(self, key, value, namespace_id=0):
self.key = key
self.value = value
self.NamespaceIndex = namespace_id

def __repr__(self):
return "KeyValuePair(key={}, value={})".format(self.key, self.value)

def to_binary(self):
# We need to define to_binary. It will be used when serializing the object
packet = []
packet.append(uabin.Primitives.UInt16.pack(self.NamespaceIndex))
packet.append(uabin.Primitives.String.pack(self.key))
packet.append(uabin.Primitives.String.pack(self.value))
return b''.join(packet)

@staticmethod
def from_binary(data):
# This is how we deserialize the object
namespace_index = uabin.Primitives.UInt16.unpack(data)
key = uabin.Primitives.String.unpack(data)
value = uabin.Primitives.String.unpack(data)
return KeyValuePair(key, value, namespace_index)


class ErrorKeyValue(object):
DEFAULT_BINARY = 20002

def __init__(self, code, description, extensions, namespace_id=0):
self.code = code
self.description = description
self.extensions = extensions
self.NamespaceIndex = namespace_id

def __repr__(self):
return "ErrorKeyValue(code='{}', description='{}', extensions={})".format(
self.code, self.description, self.extensions)

def to_binary(self):
packet = []
packet.append(uabin.Primitives.UInt16.pack(self.NamespaceIndex))
packet.append(uabin.Primitives.String.pack(self.code))
packet.append(uabin.Primitives.String.pack(self.description))

# When we want to serialize a list, we need to transform the objects to a Variant and then manually call
# to_binary() to serialize them
for i in to_variant(self.extensions):
packet.append(i.to_binary())

return b''.join(packet)

@staticmethod
def from_binary(data):
namespace_index = uabin.Primitives.UInt16.unpack(data)
code = uabin.Primitives.String.unpack(data)
description = uabin.Primitives.String.unpack(data)

# When descerialising, you'll get a Variant object back. This is how get the object's value back
extensions = Variant.from_binary(data)
extensions = [ext for ext in extensions.Value]
return ErrorKeyValue(code, description, extensions, namespace_index)

# For each custom type defined, we need to register it so the script know how to serialize / deserialise them
register_extension_object(KeyValuePair)
register_extension_object(ErrorKeyValue)
151 changes: 151 additions & 0 deletions examples/complex-type-client-server/nodeset.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?xml version='1.0' encoding='utf-8'?>
<UANodeSet xmlns="http://opcfoundation.org/UA/2011/03/UANodeSet.xsd" 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">
<Aliases>
<Alias Alias="Organizes">i=35</Alias>
<Alias Alias="HasTypeDefinition">i=40</Alias>
<Alias Alias="HasSubtype">i=45</Alias>
<Alias Alias="HasProperty">i=46</Alias>
<Alias Alias="HasComponent">i=47</Alias>
<Alias Alias="Argument">i=296</Alias>
</Aliases>
<NamespaceUris />
<UADataType BrowseName="0:KeyValue" NodeId="i=20001">
<DisplayName>KeyValue</DisplayName>
<Description>KeyValue</Description>
<References>
<Reference IsForward="false" ReferenceType="HasSubtype">i=22</Reference>
</References>
</UADataType>
<UADataType BrowseName="0:ErrorKeyValue" NodeId="i=20002">
<DisplayName>ErrorKeyValue</DisplayName>
<Description>ErrorKeyValue</Description>
<References>
<Reference IsForward="false" ReferenceType="HasSubtype">i=22</Reference>
</References>
</UADataType>
<UAObjectType BrowseName="0:HellowerType" NodeId="i=20004">
<DisplayName>HellowerType</DisplayName>
<Description>HellowerType</Description>
<References>
<Reference IsForward="false" ReferenceType="HasSubtype">i=58</Reference>
</References>
</UAObjectType>
<UAObject BrowseName="0:Hellower" NodeId="i=20005" ParentNodeId="i=85">
<DisplayName>HellowerType</DisplayName>
<Description>HellowerType</Description>
<References>
<Reference IsForward="false" ReferenceType="Organizes">i=85</Reference>
<Reference ReferenceType="HasTypeDefinition">i=20004</Reference>
<Reference ReferenceType="HasComponent">i=20006</Reference>
</References>
</UAObject>
<UAMethod BrowseName="0:SayComplexHello" NodeId="i=20006" ParentNodeId="i=20005">
<DisplayName>SayComplexHello</DisplayName>
<Description>SayComplexHello</Description>
<References>
<Reference IsForward="false" ReferenceType="HasComponent">i=20005</Reference>
<Reference ReferenceType="HasProperty">i=20007</Reference>
<Reference ReferenceType="HasProperty">i=20008</Reference>
</References>
</UAMethod>
<UAVariable BrowseName="0:InputArguments" DataType="Argument" NodeId="i=20007" ParentNodeId="i=20006" ValueRank="0">
<DisplayName>InputArguments</DisplayName>
<Description>InputArguments</Description>
<Value>
<uax:ListOfExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=296</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Description>
<uax:Locale />
<uax:Text />
</uax:Description>
<uax:ArrayDimensions />
<uax:DataType>
<uax:Identifier>i=20001</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:Name>complex_variable</uax:Name>
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=296</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Description>
<uax:Locale />
<uax:Text />
</uax:Description>
<uax:ArrayDimensions />
<uax:DataType>
<uax:Identifier>i=20001</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:Name>complex_variable_list</uax:Name>
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
<References>
<Reference IsForward="false" ReferenceType="HasProperty">i=20006</Reference>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
<UAVariable BrowseName="0:OutputArguments" DataType="Argument" NodeId="i=20008" ParentNodeId="i=20006" ValueRank="0">
<DisplayName>OutputArguments</DisplayName>
<Description>OutputArguments</Description>
<Value>
<uax:ListOfExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=296</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Description>
<uax:Locale />
<uax:Text />
</uax:Description>
<uax:ArrayDimensions />
<uax:DataType>
<uax:Identifier>i=20002</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:Name>complex_error</uax:Name>
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
<uax:ExtensionObject>
<uax:TypeId>
<uax:Identifier>i=296</uax:Identifier>
</uax:TypeId>
<uax:Body>
<uax:Argument>
<uax:Description>
<uax:Locale />
<uax:Text />
</uax:Description>
<uax:ArrayDimensions />
<uax:DataType>
<uax:Identifier>i=20002</uax:Identifier>
</uax:DataType>
<uax:ValueRank>-1</uax:ValueRank>
<uax:Name>complex_error_list</uax:Name>
</uax:Argument>
</uax:Body>
</uax:ExtensionObject>
</uax:ListOfExtensionObject>
</Value>
<References>
<Reference IsForward="false" ReferenceType="HasProperty">i=20006</Reference>
<Reference ReferenceType="HasTypeDefinition">i=68</Reference>
</References>
</UAVariable>
</UANodeSet>
58 changes: 58 additions & 0 deletions examples/complex-type-client-server/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import os
from common import KeyValuePair, ErrorKeyValue
from opcua import uamethod, Server
try:
from IPython import embed
except ImportError:
import code

def embed():
vars = globals()
vars.update(locals())
shell = code.InteractiveConsole(vars)
shell.interact()


@uamethod
def say_complex_hello(parent, complex_variable, complex_variable_list):
# The uamethod decorator will take care of converting the data for us. We only work with python objects inside it
# For it to work, you need to register your DataType like in common.py
print("say_complex_hello called: {}, {}".format(complex_variable, complex_variable_list))
complex_error = ErrorKeyValue("0", "foo", [KeyValuePair("key", "value"), KeyValuePair("hello", "world")])
complex_error_list = ErrorKeyValue("1", "bar", [KeyValuePair("key", "value")])

return complex_error, complex_error_list


class HellowerServer(object):
def __init__(self, endpoint, name, model_filepath):
self.server = Server()

self.server.import_xml(model_filepath)

# Those need to be done after importing the xml file or it will be overwritten
self.server.set_endpoint(endpoint)
self.server.set_server_name(name)

objects = self.server.get_objects_node()
hellower = objects.get_child("0:Hellower")

say_hello_node = hellower.get_child("0:SayComplexHello")

self.server.link_method(say_hello_node, say_complex_hello)

def __enter__(self):
self.server.start()
return self.server

def __exit__(self, exc_type, exc_val, exc_tb):
self.server.stop()


if __name__ == '__main__':
script_dir = os.path.dirname(__file__)
with HellowerServer(
"opc.tcp://0.0.0.0:40840/freeopcua/server/",
"FreeOpcUa Example Server",
os.path.join(script_dir, "nodeset.xml")) as server:
embed()
5 changes: 2 additions & 3 deletions opcua/common/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def wrapper(parent, *args):
parent = args[0]
args = args[1:]
result = func(self, parent, *[arg.Value for arg in args])

if isinstance(result, (list, tuple)):
return to_variant(*result)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good!

return to_variant(result)
return wrapper

Expand All @@ -70,5 +71,3 @@ def to_variant(*args):
for arg in args:
uaargs.append(ua.Variant(arg))
return uaargs


8 changes: 8 additions & 0 deletions opcua/common/ua_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from opcua import ua
from opcua.ua.uaerrors import UaError
from opcua.ua import ObjectIds, ObjectIdNames
from opcua.ua.uaprotocol_auto import ExtensionClasses


def val_to_string(val):
Expand Down Expand Up @@ -256,3 +258,9 @@ def get_nodes_of_namespace(server, namespaces=[]):
nodes = [server.get_node(nodeid) for nodeid in server.iserver.aspace.keys()
if nodeid.NamespaceIndex != 0 and nodeid.NamespaceIndex in namespace_indexes]
return nodes


def register_extension_object(object_factory):
setattr(ObjectIds, "{}_Encoding_DefaultBinary".format(object_factory.__name__), object_factory.DEFAULT_BINARY)
ObjectIdNames[object_factory.DEFAULT_BINARY] = object_factory.__name__
ExtensionClasses[object_factory.DEFAULT_BINARY] = object_factory
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just saw that it won't work if your NamespaceIndex is not 0. I'll see what I can do to change that.

2 changes: 0 additions & 2 deletions opcua/ua/uatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,5 +1083,3 @@ def get_default_value(vtype):
return Variant()
else:
raise RuntimeError("function take a uatype as argument, got:", vtype)


Loading