Calling method with complex objects from Python OPC UA server

Hello

After a longer absence from here, I’m back with a question :slightly_smiling_face:

We are trying to call an OPC UA method with a B&R PLC as client and a python OPC UA server using the asyncua library.

If we call a method with only basic arguments, it works as expected and I can call the method from the PLC. However, if we use custom struct types for the method arguments, we get the error 0x80340000 Bad_NodeIdUnknown on the UA_MethodGetHandle function block.

For a small test, we created the same method on the PLC OPC UA server, and there it works as expected. We tested already various things on Python side, but nothing works. The python method can be called without issues with UaExpert as client.

Did someone already use a Python OPC UA server and read values / call methods with custom structs using a B&R PLC client?
If so, are we missing something?

Python code
"""

#### First Setup:
- Open PowerShell here and then execute each line as separate command:

python -m venv ./.venv
./.venv/Scripts/activate
pip install asyncio
pip install asyncua


#### Run Script:
- Open PowerShell here and then execute each line as separate command:

./.venv/Scripts/activate
python opcua_server_test.py

"""

import asyncio

from asyncua import Server, ua, Node
from asyncua.ua.uatypes import String, Int32, UInt32, QualifiedName
from asyncua.common.structures104 import new_struct, new_struct_field
from asyncua.common.structures import Struct as ssssss
from asyncua.common.manage_nodes import create_encoding
from asyncua.common.type_dictionary_builder import DataTypeDictionaryBuilder


async def create_and_start_server():
    server = await _setup_server()
    namespace_idx = await server.register_namespace("http://test.app.com/api/v2")
    server_base_obj = server.get_objects_node()
    method_base_obj = await server_base_obj.add_object(
        nodeid=f"ns={namespace_idx};s=TestObj",
        bname="TestObj",
    )
    await _add_method_basic(server, method_base_obj, namespace_idx)
    await _add_method_struct(server, method_base_obj, namespace_idx)
    await _run_server(server)


async def _setup_server():
    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://127.0.0.1:4840")
    server.set_security_policy([ua.SecurityPolicyType.NoSecurity])
    # Set max_browse_continuation_points as otherwise it is UInt16 with value NULL which makes connection from B&R client fail -> see https://github.com/FreeOpcUa/python-opcua/issues/1136
    max_browse_continuation_points = server.get_node(ua.NodeId(2735, 0))
    await max_browse_continuation_points.set_value(ua.Variant(0, ua.VariantType.UInt16))  # 0 means infinite
    return server


def _method_callback_basic(*args):
    print(args)
    return [
        ua.Variant(42, ua.VariantType.UInt16),
        ua.Variant(55, ua.VariantType.UInt16),
    ]


async def _add_method_basic(server: Server, parent_object: Node, namespace_idx: int):
    in_arg_1 = ua.Argument()
    in_arg_1.Name = "InArg1"
    in_arg_1.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    in_arg_1.ValueRank = -1
    in_arg_1.ArrayDimensions = []
    in_arg_1.Description = ua.LocalizedText("input argument 1")

    in_arg_2 = ua.Argument()
    in_arg_2.Name = "InArg2"
    in_arg_2.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    in_arg_2.ValueRank = -1
    in_arg_2.ArrayDimensions = []
    in_arg_2.Description = ua.LocalizedText("input argument 2")

    out_arg_1 = ua.Argument()
    out_arg_1.Name = "OutArg1"
    out_arg_1.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    out_arg_1.ValueRank = -1
    out_arg_1.ArrayDimensions = []
    out_arg_1.Description = ua.LocalizedText("output argument 1")

    out_arg_2 = ua.Argument()
    out_arg_2.Name = "OutArg2"
    out_arg_2.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    out_arg_2.ValueRank = -1
    out_arg_2.ArrayDimensions = []
    out_arg_2.Description = ua.LocalizedText("output argument 2")

    await parent_object.add_method(
        f"ns={namespace_idx};s=TestMethodBasic",
        "TestMethodBasic",
        _method_callback_basic,
        [in_arg_1, in_arg_2],
        [out_arg_1, out_arg_2],
    )


def _method_callback_struct(*args):
    print(args)
    return [
        ua.Variant(42, ua.VariantType.UInt16),
        ua.Variant(55, ua.VariantType.UInt16),
    ]


async def _add_method_struct(server: Server, parent_object: Node, namespace_idx: int):
    type_node, encoding_nodes = await new_struct(
        server=server,
        idx=namespace_idx,
        name="CoffeeRecipeTypeee",
        fields=[
            new_struct_field("Milk", ua.VariantType.UInt32),
            new_struct_field("Sugar", ua.VariantType.UInt32),
        ],
        is_union=False,
    )

    for n in encoding_nodes:
        await n.add_reference(target=ua.NodeId(76, 0), reftype=ua.ObjectIds.HasTypeDefinition, forward=True, bidirectional=False)

    # dict_builder = DataTypeDictionaryBuilder(server, namespace_idx, "http://test.app.com/api/v2", "MyDictionary")
    # await dict_builder.init()
    # basic_struct_name = "CoffeeRecipeType"
    # basic_struct = await dict_builder.create_data_type(basic_struct_name)
    # basic_struct.add_field("Milk", ua.VariantType.UInt32)
    # basic_struct.add_field("Sugar", ua.VariantType.UInt32)
    # await dict_builder.set_dict_byte_string()

    custom_objs = await server.load_data_type_definitions()
    # await server.load_type_definitions()

    await parent_object.add_variable(
        f"ns={namespace_idx};s=TestValueStruct",
        "TestValueStruct",
        ua.Variant(ua.CoffeeRecipeTypeee(), ua.VariantType.ExtensionObject),
    )

    in_arg_1 = ua.Argument()
    in_arg_1.Name = "InArg1"
    in_arg_1.DataType = type_node.nodeid  # type_node.nodeid / basic_struct.data_type
    in_arg_1.ValueRank = -1
    in_arg_1.ArrayDimensions = []
    in_arg_1.Description = ua.LocalizedText("input argument 1")

    in_arg_2 = ua.Argument()
    in_arg_2.Name = "InArg2"
    in_arg_2.DataType = type_node.nodeid
    in_arg_2.ValueRank = -1
    in_arg_2.ArrayDimensions = []
    in_arg_2.Description = ua.LocalizedText("input argument 2")

    out_arg_1 = ua.Argument()
    out_arg_1.Name = "OutArg1"
    out_arg_1.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    out_arg_1.ValueRank = -1
    out_arg_1.ArrayDimensions = []
    out_arg_1.Description = ua.LocalizedText("output argument 1")

    out_arg_2 = ua.Argument()
    out_arg_2.Name = "OutArg2"
    out_arg_2.DataType = ua.NodeId(ua.ObjectIds.UInt32)
    out_arg_2.ValueRank = -1
    out_arg_2.ArrayDimensions = []
    out_arg_2.Description = ua.LocalizedText("output argument 2")

    await parent_object.add_method(
        f"ns={namespace_idx};s=TestMethodStruct",
        "TestMethodStruct",
        _method_callback_struct,
        [in_arg_1, in_arg_2],
        [out_arg_1, out_arg_2],
    )


async def _run_server(server: Server):
    async with server:
        print(f"OPC UA server is now running at {server.endpoint.geturl()}")
        print("Press Ctrl + C to cancel")
        while True:
            await asyncio.sleep(1)


async def main():
    await create_and_start_server()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Server stopped by Ctrl + C")

PLC Client code
PROGRAM _INIT
	Connect.ServerEndpointUrl := 'opc.tcp://127.0.0.1:4840';
	Connect.Timeout := T#5s;
	Connect.SessionConnectInfo.MonitorConnection := T#5s;
	Connect.SessionConnectInfo.SessionTimeout := T#5s;
	
	Disconnect.Timeout := T#5s;
	
	MethodGetHandle.ObjectNodeID.NamespaceIndex := 2;
	MethodGetHandle.ObjectNodeID.IdentifierType := UAIdentifierType_String;
	MethodGetHandle.ObjectNodeID.Identifier := 'TestObj';
	MethodGetHandle.MethodNodeID.NamespaceIndex := 2;
	MethodGetHandle.MethodNodeID.IdentifierType := UAIdentifierType_String;
	MethodGetHandle.MethodNodeID.Identifier := 'TestMethodStruct'; // TestMethodBasic TestMethodStruct
	MethodGetHandle.Timeout := T#30s;
	
	MethodCall.Timeout := T#30s;
	
	MethodInArgsMapping[0].Name := 'InArg1';
	MethodInArgsMapping[0].Value := '::Client:MethodArgs.Struct.InArg1'; // Basic Struct
	MethodInArgsMapping[1].Name := 'InArg2';
	MethodInArgsMapping[1].Value := '::Client:MethodArgs.Struct.InArg2';
	
	MethodOutArgsMapping[0].Name := 'OutArg1';
	MethodOutArgsMapping[0].Value := '::Client:MethodArgs.Struct.OutArg1';
	MethodOutArgsMapping[1].Name := 'OutArg2';
	MethodOutArgsMapping[1].Value := '::Client:MethodArgs.Struct.OutArg2';
	
	MethodReleaseHandle.Timeout := T#30s;
	
	ReadStructValueNodeIDs[0].NamespaceIndex := 2;
	ReadStructValueNodeIDs[0].IdentifierType := UAIdentifierType_String;
	ReadStructValueNodeIDs[0].Identifier := 'TestValueStruct';
	ReadStructValueVariables[0] := '::Client:StructValue';
	ReadStructValue.Timeout := T#10s;
END_PROGRAM

PROGRAM _CYCLIC
	Connect();
	IF Connect.Done THEN
		ConnectionHandle := Connect.ConnectionHdl;
	END_IF;
	
	Disconnect.ConnectionHdl := ConnectionHandle;
	Disconnect();
	IF Disconnect.Done THEN
		ConnectionHandle := 0;
	END_IF;
	
	MethodGetHandle.ConnectionHdl := ConnectionHandle;
	MethodGetHandle();
	IF MethodGetHandle.Done THEN
		MethodHandle := MethodGetHandle.MethodHdl;
	END_IF;
	
	MethodCall.ConnectionHdl := ConnectionHandle;
	MethodCall.MethodHdl := MethodHandle;
	MethodCall(
		InputArguments := MethodInArgsMapping,
		OutputArguments := MethodOutArgsMapping
	);
	
	MethodReleaseHandle.ConnectionHdl := ConnectionHandle;
	MethodReleaseHandle.MethodHdl := MethodHandle;
	MethodReleaseHandle();
	IF MethodReleaseHandle.Done THEN
		MethodHandle := 0;
	END_IF;
	
	ReadStructValue.ConnectionHdl := ConnectionHandle;
	ReadStructValue.NodeIDs := ADR(ReadStructValueNodeIDs);
	ReadStructValue.NodeIDCount := SIZEOF(ReadStructValueNodeIDs) / SIZEOF(ReadStructValueNodeIDs[0]);
	ReadStructValue.NodeErrorIDs := ADR(ReadStructValueNodeErrors);
	ReadStructValue.Variables := ADR(ReadStructValueVariables);
	ReadStructValue();
END_PROGRAM

PROGRAM _EXIT
END_PROGRAM
PLC Client Variables
VAR
	Connect : UA_Connect;
	ConnectionHandle : DWORD;
	Disconnect : UA_Disconnect;
	MethodGetHandle : UA_MethodGetHandle;
	MethodHandle : DWORD;
	MethodCall : UA_MethodCall;
	MethodInArgsMapping : ARRAY[0..MAX_INDEX_ARGUMENTS] OF UAMethodArgument;
	MethodOutArgsMapping : ARRAY[0..MAX_INDEX_ARGUMENTS] OF UAMethodArgument;
	MethodArgs : MethodArgsType;
	MethodReleaseHandle : UA_MethodReleaseHandle;
	ReadStructValue : UaClt_ReadBulk;
	ReadStructValueNodeIDs : ARRAY[0..0] OF UANodeID;
	ReadStructValueVariables : ARRAY[0..0] OF STRING[MAX_LENGTH_VARIABLE];
	ReadStructValueNodeErrors : ARRAY[0..0] OF DWORD;
	StructValue : CoffeeRecipeType;
END_VAR
PLC Global Types
TYPE
	CoffeeRecipeType : 	STRUCT 
		Milk : UDINT;
		Sugar : UDINT;
	END_STRUCT;
	MethodArgsType : 	STRUCT 
		Basic : MethodArgsBasicType;
		Struct : MethodArgsStructType;
	END_STRUCT;
	MethodArgsStructType : 	STRUCT 
		InArg1 : CoffeeRecipeType;
		InArg2 : CoffeeRecipeType;
		OutArg1 : UDINT;
		OutArg2 : UDINT;
	END_STRUCT;
	MethodArgsBasicType : 	STRUCT 
		InArg1 : UDINT;
		InArg2 : UDINT;
		OutArg1 : UDINT;
		OutArg2 : UDINT;
	END_STRUCT;
END_TYPE

Attached is also the complete PLC project, including the python script.
UaClient.zip (100.8 KB)

First of all, I don’t have a direct answer to your question, but I found something that looks suspicious to me
I use Softing OPC UA Client, and the Type of InArg1 and InArg2 is represented as Byte:


I would expect a CoffeeRecipeType (this type is known by the OPC-UA Server, as you can finde it in Types/DataTypes/BaseDataType/Structure…)

Cheers
Christoph

Hi @patrick.tanner,

Sorry to answer your question with a question: Are you able to use UA Expert to call methods to your Python server? This might help you determine whether it is on the server or the client side. There are also server diagnostics available with UA Expert.

Without actually running and testing your code my first thought is that there’s either a identifier mismatch, either with the name of the object or with the datatype. I won’t pretend to understand your Python code, but it looks like your node datatype is a 32 bit integer, but in the PLC code it is string.

Sorry if this is not completely helpful. Most of my experience in this area is just from one B&R PLC to another B&R PLC.

Also, the method and data types look good on UaExpert from my point of view. It looks almost same as a method from a B&R PLC server which works. Only the namespace index und URI is different.

This is also what I’m comfortable with :upside_down_face:

1 Like

So, it seems that the B&R client is not the only one which cannot handle this method. Did you try to call the method with your Softing Client?

Sadly, from my point of view it looks very good in UaExpert. It even looks almost same as the B&R method which works.

Python Server

Method:
Method

Input Args:

CoffeRecipeType:

Encoding:
Encoding

B&R PLC test server

Method:
Method

Input Args:

CoffeRecipeType:

Encoding:
Encoding

But UaExpert is usually very robust and can handle a lot. Maybe there’s some small thing missing on the server which is important for the B&R client.

Yes. Our HMI team is using the Softing OPC-UA SDK and I remember that few years ago, we also had some troubles. With UaExpert, everything worked well but if we tried Softing Client, we could figure out a problem. I can’t remember what exactly it was but definitly NOT related to methodes. Since then, we use only Softing Client in the PLC team.

However… I can call the methode, but not exactly as expected.

TestMethodBasic

TestMethodStruct

I can call the TestMethodStruct, but get a warning in the MessageLog and the InArg must be in range of a Byte…

According to this, i am pretty sure that with the configuration of the methode is something missing, but have no clue how to fix it.

Cheers
Christoph

1 Like

I also asked for B&R internal help from our OPC UA experts. Analyzing the communication in Wireshark led to following conclusion:

The B&R Client sends a BrowseRequest to the Python Server and specifically requests the type definition.

But the Python server does not return a proper type definition in the response. Therefore, the B&R client cannot find the required data type node and returns the error 0x80340000 Bad_NodeIdUnknown.

UaExpert seems to resolve the data types from the browse tree and can handle the method. But the B&R client does not do any additional browsing except for the requested objects and can therefore not handle the method.

There seem to be a couple of open issues on the Python OPC UA library around the types topic, but there seems to be no specific plan to fix this.
Browsing for HasTypeDefinition references is broken · Issue #1529 · FreeOpcUa/opcua-asyncio (github.com)
RuntimeError: Unknown datatype for field: StructureField · Issue #1567 · FreeOpcUa/opcua-asyncio (github.com)

For our current project we do not have time to analyze the issue further, or even fix it on Python side. We will probably switch over to sending JSON strings over OPC UA as a workaround, at least for the moment.

1 Like