feat(dcl): implement BrowseChildrenAsync on RealOpcUaClient

This commit is contained in:
Joseph Doherty
2026-05-28 11:59:03 -04:00
parent 1d2e2c1614
commit 6999aedc60
3 changed files with 143 additions and 3 deletions
@@ -327,10 +327,70 @@ public class RealOpcUaClient : IOpcUaClient
: configured;
/// <inheritdoc />
// Real implementation lands in Task 8 of the OPC UA tag browser plan.
public Task<Commons.Interfaces.Protocol.BrowseChildrenResult> BrowseChildrenAsync(
public async Task<Commons.Interfaces.Protocol.BrowseChildrenResult> BrowseChildrenAsync(
string? parentNodeId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
{
// Mirror the SubscribeAsync/ReadAsync wrap idiom: snapshot the session
// reference once, fail fast with a typed exception if the link is
// down, then call the SDK's async API directly (no Task.Run wrap —
// the OPC Foundation SDK already provides true async I/O).
var session = _session;
if (session is null || !session.Connected)
{
throw new Commons.Interfaces.Protocol.ConnectionNotConnectedException(
"OPC UA session is not connected.");
}
// ObjectsFolder = ns=0;i=85 — the OPC UA standard server root. Empty
// / null input means "browse the root"; anything else is parsed as
// an absolute NodeId expression.
var nodeToBrowse = string.IsNullOrEmpty(parentNodeId)
? ObjectIds.ObjectsFolder
: NodeId.Parse(parentNodeId);
// NodeClassMask intentionally excludes ReferenceType, View, Variable-
// Type, ObjectType, DataType. UI only needs Objects (navigable),
// Variables (selectable), Methods (display-only).
var nodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method);
var (_, continuationPoint, references) = await session.BrowseAsync(
null,
null,
nodeToBrowse,
1000u,
BrowseDirection.Forward,
ReferenceTypeIds.HierarchicalReferences,
true,
nodeClassMask,
cancellationToken).ConfigureAwait(false);
var refs = references ?? new ReferenceDescriptionCollection();
var children = new List<Commons.Interfaces.Protocol.BrowseNode>(refs.Count);
foreach (var r in refs)
{
children.Add(new Commons.Interfaces.Protocol.BrowseNode(
NodeId: r.NodeId.ToString(),
DisplayName: r.DisplayName?.Text ?? r.BrowseName?.Name ?? "(unnamed)",
NodeClass: MapNodeClass(r.NodeClass),
HasChildren: r.NodeClass == NodeClass.Object));
}
// A non-empty continuation point means the server had more refs than
// our requestedMaxReferencesPerNode cap. The UI surfaces a "more
// children, type the node id manually" hint rather than auto-paging;
// BrowseNext is not invoked here. Discarding the continuation point
// is acceptable because the server expires it on session close.
var truncated = continuationPoint != null && continuationPoint.Length > 0;
return new Commons.Interfaces.Protocol.BrowseChildrenResult(children, truncated);
}
private static Commons.Interfaces.Protocol.BrowseNodeClass MapNodeClass(NodeClass nc) => nc switch
{
NodeClass.Object => Commons.Interfaces.Protocol.BrowseNodeClass.Object,
NodeClass.Variable => Commons.Interfaces.Protocol.BrowseNodeClass.Variable,
NodeClass.Method => Commons.Interfaces.Protocol.BrowseNodeClass.Method,
_ => Commons.Interfaces.Protocol.BrowseNodeClass.Other
};
}
/// <summary>