diff --git a/examples/complex-type-client-server/client.py b/examples/complex-type-client-server/client.py new file mode 100644 index 000000000..adbda538d --- /dev/null +++ b/examples/complex-type-client-server/client.py @@ -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) diff --git a/examples/complex-type-client-server/common.py b/examples/complex-type-client-server/common.py new file mode 100644 index 000000000..6f2739f46 --- /dev/null +++ b/examples/complex-type-client-server/common.py @@ -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) diff --git a/examples/complex-type-client-server/nodeset.xml b/examples/complex-type-client-server/nodeset.xml new file mode 100644 index 000000000..419ed54af --- /dev/null +++ b/examples/complex-type-client-server/nodeset.xml @@ -0,0 +1,151 @@ + + + + i=35 + i=40 + i=45 + i=46 + i=47 + i=296 + + + + KeyValue + KeyValue + + i=22 + + + + ErrorKeyValue + ErrorKeyValue + + i=22 + + + + HellowerType + HellowerType + + i=58 + + + + HellowerType + HellowerType + + i=85 + i=20004 + i=20006 + + + + SayComplexHello + SayComplexHello + + i=20005 + i=20007 + i=20008 + + + + InputArguments + InputArguments + + + + + i=296 + + + + + + + + + + i=20001 + + -1 + complex_variable + + + + + + i=296 + + + + + + + + + + i=20001 + + -1 + complex_variable_list + + + + + + + i=20006 + i=68 + + + + OutputArguments + OutputArguments + + + + + i=296 + + + + + + + + + + i=20002 + + -1 + complex_error + + + + + + i=296 + + + + + + + + + + i=20002 + + -1 + complex_error_list + + + + + + + i=20006 + i=68 + + + diff --git a/examples/complex-type-client-server/server.py b/examples/complex-type-client-server/server.py new file mode 100644 index 000000000..535258051 --- /dev/null +++ b/examples/complex-type-client-server/server.py @@ -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() diff --git a/opcua/common/methods.py b/opcua/common/methods.py index dd9545249..8a29a695b 100644 --- a/opcua/common/methods.py +++ b/opcua/common/methods.py @@ -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) return to_variant(result) return wrapper @@ -70,5 +71,3 @@ def to_variant(*args): for arg in args: uaargs.append(ua.Variant(arg)) return uaargs - - diff --git a/opcua/common/ua_utils.py b/opcua/common/ua_utils.py index 10dad752b..36a02ff52 100644 --- a/opcua/common/ua_utils.py +++ b/opcua/common/ua_utils.py @@ -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): @@ -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 diff --git a/opcua/ua/uatypes.py b/opcua/ua/uatypes.py index b4aff17bd..dd80bf913 100644 --- a/opcua/ua/uatypes.py +++ b/opcua/ua/uatypes.py @@ -1083,5 +1083,3 @@ def get_default_value(vtype): return Variant() else: raise RuntimeError("function take a uatype as argument, got:", vtype) - - diff --git a/tests/tests_common.py b/tests/tests_common.py index 2d224170e..3bd85da7d 100644 --- a/tests/tests_common.py +++ b/tests/tests_common.py @@ -1,17 +1,19 @@ # encoding: utf-8 -from concurrent.futures import Future, TimeoutError -import time from datetime import datetime from datetime import timedelta import math from opcua import ua -from opcua import Node from opcua import uamethod from opcua import instantiate from opcua import copy_node +from opcua.common.ua_utils import register_extension_object + +from opcua.ua import ua_binary as uabin from opcua.common import ua_utils +import io + def add_server_methods(srv): @uamethod @@ -548,7 +550,7 @@ def test_path_string(self): self.assertEqual(["1:titif", "3:opath"], path) path = self.opc.get_node("i=13387").get_path_as_string() # FIXME this is wrong in our server! BaseObjectType is missing an inverse reference to its parent! seems xml definition is wrong - self.assertEqual(['0:BaseObjectType', '0:FolderType', '0:FileDirectoryType', '0:CreateDirectory'], path) + self.assertEqual(['0:BaseObjectType', '0:FolderType', '0:FileDirectoryType', '0:CreateDirectory'], path) def test_path(self): of = self.opc.nodes.objects.add_folder(1, "titif") @@ -560,7 +562,7 @@ def test_path(self): target = self.opc.get_node("i=13387") path = target.get_path() # FIXME this is wrong in our server! BaseObjectType is missing an inverse reference to its parent! seems xml definition is wrong - self.assertEqual([self.opc.nodes.base_object_type, self.opc.nodes.folder_type, self.opc.get_node(ua.ObjectIds.FileDirectoryType), target], path) + self.assertEqual([self.opc.nodes.base_object_type, self.opc.nodes.folder_type, self.opc.get_node(ua.ObjectIds.FileDirectoryType), target], path) def test_get_endpoints(self): endpoints = self.opc.get_endpoints() @@ -699,6 +701,87 @@ def test_base_data_type(self): self.assertEqual(ua_utils.get_base_data_type(d), self.opc.get_node(ua.ObjectIds.Structure)) self.assertEqual(ua_utils.data_type_to_variant_type(d), ua.VariantType.ExtensionObject) + def test_ua_method_unique(self): + def unique_response(parent): + return 42 + + decorated_unique_response = uamethod(unique_response) + response = decorated_unique_response(ua.NodeId("ServerMethod", 2)) + + self.assertEqual(len(response), 1) + self.assertEqual(type(response[0]), ua.Variant) + + def test_ua_method_multiple(self): + def unique_response(parent): + return 42, 49 + + decorated_unique_response = uamethod(unique_response) + response = decorated_unique_response(ua.NodeId("ServerMethod", 2)) + + self.assertEqual(len(response), 2) + self.assertEqual(type(response[0]), ua.Variant) + + def test_ua_method_complex(self): + register_extension_object(MyCustomClass) + + def unique_response(parent): + return MyCustomClass("Key", "Value") + + decorated_unique_response = uamethod(unique_response) + response = decorated_unique_response(ua.NodeId("ServerMethod", 2)) + + self.assertEqual(len(response), 1) + self.assertEqual(type(response[0]), ua.Variant) + + def test_ua_method_complex_multiple(self): + register_extension_object(MyCustomClass) + + def unique_response(parent): + return [MyCustomClass("Key", "Value"), + MyCustomClass("Hello", "World")] + + decorated_unique_response = uamethod(unique_response) + response = decorated_unique_response(ua.NodeId("ServerMethod", 2)) + + self.assertEqual(len(response), 2) + self.assertEqual(type(response[0]), ua.Variant) + + # TODO: Fix the test + def test_decode_custom_object(self): + register_extension_object(MyCustomClass) + + def unique_response(parent): + return MyCustomClass("Key", "Value") + + decorated_unique_response = uamethod(unique_response) + response = decorated_unique_response(ua.NodeId("ServerMethod", 2)) + + self.assertEqual(len(response), 1) + self.assertEqual(type(response[0]), ua.Variant) + + key_value = uabin.unpack_uatype_array(response[0].VariantType, io.BytesIO(response[0].to_binary())) + self.assertEqual(key_value.key, "Key") + self.assertEqual(key_value.value, "Value") + + +class MyCustomClass(object): + DEFAULT_BINARY = 20001 + def __init__(self, key, value, namespace_id=0): + self.key = key + self.value = value + self.NamespaceIndex = namespace_id + def to_binary(self): + 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): + namespace_index = uabin.Primitives.UInt16.unpack(data) + key = uabin.Primitives.String.unpack(data) + value = uabin.Primitives.String.unpack(data) + return MyCustomClass(key, value, namespace_index)