in src/Azure.IIoT.OpcUa.Publisher/src/Services/NodeServices.cs [603:807]
public async Task<MethodCallResponseModel> MethodCallAsync(T endpoint,
MethodCallRequestModel request, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(request);
if (string.IsNullOrEmpty(request.ObjectId) &&
(request.ObjectBrowsePath == null || request.ObjectBrowsePath.Count == 0))
{
throw new ArgumentException("Object id missing or bad browse path", nameof(request));
}
using var trace = _activitySource.StartActivity("MethodCall");
return await _client.ExecuteAsync(endpoint, async context =>
{
//
// A method call request can specify the targets in several ways:
//
// * Specify methodId and optionally objectId node ids with null browse paths.
// * Specify an objectBrowsePath to a real object node from the node specified
// with objectId. If objectId is null, the root node is used.
// * Specify a methodBrowsePath from the above object node to the actual
// method node to call on the object. MethodId remains null.
// * Like previously, but specify methodId and method browse path from it to a
// real method node.
//
var objectId = await context.Session.ResolveNodeIdAsync(request.Header, request.ObjectId,
request.ObjectBrowsePath, nameof(request.ObjectBrowsePath), _timeProvider,
context.Ct).ConfigureAwait(false);
if (NodeId.IsNull(objectId))
{
throw new ArgumentException("Object id missing", nameof(request));
}
var methodId = request.MethodId.ToNodeId(context.Session.MessageContext);
if (request.MethodBrowsePath?.Count > 0)
{
if (NodeId.IsNull(methodId))
{
// Browse from object id to method if possible
methodId = objectId ??
throw new ArgumentException("Method id and object id missing",
nameof(request));
}
methodId = await context.Session.ResolveBrowsePathToNodeAsync(request.Header,
methodId, [.. request.MethodBrowsePath], nameof(request.MethodBrowsePath),
_timeProvider, context.Ct).ConfigureAwait(false);
}
else if (NodeId.IsNull(methodId))
{
// Method is null and cannot browse to method from object
throw new ArgumentException("Method id missing", nameof(request));
}
var browseDescriptions = new BrowseDescriptionCollection {
new BrowseDescription {
BrowseDirection = Opc.Ua.BrowseDirection.Forward,
IncludeSubtypes = true,
NodeClassMask = 0,
NodeId = methodId,
ReferenceTypeId = ReferenceTypeIds.HasProperty,
ResultMask = (uint)BrowseResultMask.All
}
};
// Get default input arguments and types
var browse = await context.Session.Services.BrowseAsync(
request.Header.ToRequestHeader(_timeProvider), null, 0, browseDescriptions,
context.Ct).ConfigureAwait(false);
var browseresults = browse.Validate(browse.Results, r => r.StatusCode,
browse.DiagnosticInfos, browseDescriptions);
List<(TypeInfo, object)>? inputs = null, outputs = null;
if (browseresults.ErrorInfo == null)
{
foreach (var nodeReference in browseresults[0].Result.References)
{
List<(TypeInfo, object)>? temp = null;
if (nodeReference.BrowseName == BrowseNames.InputArguments)
{
temp = inputs = [];
}
else if (nodeReference.BrowseName == BrowseNames.OutputArguments)
{
temp = outputs = [];
}
else
{
continue;
}
var node = nodeReference.NodeId.ToNodeId(context.Session.MessageContext.NamespaceUris);
var (value, _) = await context.Session.ReadValueAsync(
request.Header.ToRequestHeader(_timeProvider),
node, context.Ct).ConfigureAwait(false);
// value is also null if the type is not a variable node
if (value?.Value is ExtensionObject[] argumentsList)
{
foreach (var argument in argumentsList.Select(a => (Argument)a.Body))
{
var builtInType = TypeInfo.GetBuiltInType(argument.DataType);
temp.Add((new TypeInfo(builtInType,
argument.ValueRank), argument.Value));
}
if (inputs != null && outputs != null)
{
break;
}
}
}
}
if ((request.Arguments?.Count ?? 0) > (inputs?.Count ?? 0))
{
// Too many arguments provided
if (browseresults.ErrorInfo != null)
{
return new MethodCallResponseModel
{
Results = Array.Empty<MethodCallArgumentModel>(),
ErrorInfo = browseresults.ErrorInfo
};
}
throw new ArgumentException("Arguments missing", nameof(request));
}
// Set default input arguments from meta data
var requests = new CallMethodRequestCollection {
new CallMethodRequest {
ObjectId = objectId,
MethodId = methodId,
InputArguments = inputs == null ? [] :
new VariantCollection(inputs
.Select(arg => arg.Item1.CreateVariant(arg.Item2)))
}
};
// Update with input arguments provided in request payload
if ((request.Arguments?.Count ?? 0) != 0)
{
Debug.Assert(request.Arguments != null);
Debug.Assert(inputs != null);
for (var i = 0; i < request.Arguments.Count; i++)
{
var arg = request.Arguments[i];
if (arg == null)
{
continue;
}
var builtinType = inputs[i].Item1.BuiltInType;
if (!string.IsNullOrEmpty(arg.DataType))
{
builtinType = await TypeInfo.GetBuiltInTypeAsync(
arg.DataType.ToNodeId(context.Session.MessageContext),
context.Session.TypeTree, context.Ct).ConfigureAwait(false);
}
requests[0].InputArguments[i] = context.Session.Codec.Decode(
arg.Value ?? VariantValue.Null, builtinType);
}
}
// Call method
var response = await context.Session.Services.CallAsync(
request.Header.ToRequestHeader(_timeProvider), requests,
context.Ct).ConfigureAwait(false);
var results = response.Validate(response.Results, r => r.StatusCode,
response.DiagnosticInfos, requests);
if (results.ErrorInfo != null)
{
return new MethodCallResponseModel
{
Results = Array.Empty<MethodCallArgumentModel>(),
ErrorInfo = results.ErrorInfo
};
}
// Create output argument list
var arguments = new List<MethodCallArgumentModel>();
var args = results[0].Result.OutputArguments?.Count ?? 0;
for (var i = 0; i < args; i++)
{
var arg = results[0].Result.OutputArguments[i];
if (arg == Variant.Null &&
(outputs?.Count ?? 0) > i && outputs![i].Item2 != null)
{
// return default value
arg = new Variant(outputs[i].Item2);
}
var value = context.Session.Codec.Encode(arg, out var type);
if (type == BuiltInType.Null && (outputs?.Count ?? 0) > i)
{
// return default type from type info
type = outputs![i].Item1.BuiltInType;
}
var dataType = type == BuiltInType.Null ?
null : type.ToString();
arguments.Add(new MethodCallArgumentModel
{
Value = value,
DataType = dataType
});
}
return new MethodCallResponseModel
{
Results = arguments,
ErrorInfo = results[0].ErrorInfo
};
}, request.Header, ct).ConfigureAwait(false);
}