Hello
After a longer absence from here, I’m back with a question
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)