using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Opc.Ua; using Opc.Ua.Client; namespace OpcUaCli.Commands; [Command("browse", Description = "Browse the OPC UA address space")] public class BrowseCommand : ICommand { /// /// Gets the OPC UA endpoint URL to connect to before browsing. /// [CommandOption("url", 'u', Description = "OPC UA server endpoint URL", IsRequired = true)] public string Url { get; init; } = default!; [CommandOption("username", 'U', Description = "Username for authentication")] public string? Username { get; init; } [CommandOption("password", 'P', Description = "Password for authentication")] public string? Password { get; init; } [CommandOption("security", 'S', Description = "Transport security: none, sign, encrypt (default: none)")] public string Security { get; init; } = "none"; [CommandOption("failover-urls", 'F', Description = "Comma-separated failover endpoint URLs for redundancy")] public string? FailoverUrls { get; init; } /// /// Gets the optional node identifier to browse from; defaults to the OPC UA Objects folder. /// [CommandOption("node", 'n', Description = "Node ID to browse (default: Objects folder)")] public string? NodeId { get; init; } /// /// Gets the maximum browse depth when recursive traversal is enabled. /// [CommandOption("depth", 'd', Description = "Maximum browse depth")] public int Depth { get; init; } = 1; /// /// Gets a value indicating whether browse recursion should continue into child objects. /// [CommandOption("recursive", 'r', Description = "Browse recursively (uses --depth as max depth)")] public bool Recursive { get; init; } /// /// Connects to the OPC UA endpoint and writes the browse tree to the console. /// /// The console used to emit browse output. public async ValueTask ExecuteAsync(IConsole console) { var urls = FailoverUrlParser.Parse(Url, FailoverUrls); using var failover = new OpcUaFailoverHelper(urls, Username, Password, Security); using var session = await failover.ConnectAsync(); var startNode = string.IsNullOrEmpty(NodeId) ? ObjectIds.ObjectsFolder : new NodeId(NodeId); var maxDepth = Recursive ? Depth : 1; await BrowseNodeAsync(session, console, startNode, maxDepth, 0); } private static async Task BrowseNodeAsync( ISession session, IConsole console, NodeId nodeId, int maxDepth, int currentDepth) { var indent = new string(' ', currentDepth * 2); var (_, continuationPoint, references) = await session.BrowseAsync( null, null, nodeId, 0u, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences, true, (uint)NodeClass.Object | (uint)NodeClass.Variable | (uint)NodeClass.Method); if (references == null) return; // Handle continuation points for large result sets while (references.Count > 0) { foreach (var reference in references) { var nodeClass = reference.NodeClass; var marker = nodeClass switch { NodeClass.Object => "[Object]", NodeClass.Variable => "[Variable]", NodeClass.Method => "[Method]", _ => $"[{nodeClass}]" }; await console.Output.WriteLineAsync( $"{indent}{marker} {reference.DisplayName} (NodeId: {reference.NodeId})"); if (currentDepth + 1 < maxDepth && nodeClass == NodeClass.Object) { var childNodeId = ExpandedNodeId.ToNodeId( reference.NodeId, session.NamespaceUris); await BrowseNodeAsync(session, console, childNodeId, maxDepth, currentDepth + 1); } } // Follow continuation point if present if (continuationPoint != null && continuationPoint.Length > 0) { var (_, nextCp, nextRefs) = await session.BrowseNextAsync( null, false, continuationPoint); continuationPoint = nextCp; references = nextRefs; } else { break; } } } }