using CliFx.Attributes; using CliFx.Infrastructure; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands; /// /// Walk the target's symbol table (ADS SymbolLoaderFactory, flat mode) and print every /// symbol the driver's atomic-type mapper recognizes. Same path DiscoverAsync takes /// when EnableControllerBrowse = true — structured UDTs / function-block instances /// won't appear because the driver filters to the supported primitive surface. /// /// /// Inherits from rather than /// so the --poll-only flag does NOT surface in /// browse --help: browse never subscribes, the flag would be a no-op, and the help /// text would mislead users (Driver.TwinCAT.Cli-004). /// [Command("browse", Description = "Enumerate controller symbols via the driver's DiscoverAsync walk.")] public sealed class BrowseCommand : TwinCATCommandBase { [CommandOption("prefix", Description = "Case-sensitive instance-path prefix to filter on (e.g. 'GVL_Fixture' or " + "'MAIN.'). Empty (default) prints everything.")] public string? Prefix { get; init; } [CommandOption("max", Description = "Maximum number of symbols to print. 0 = unbounded (default 500 for large " + "controllers — flat-mode symbol counts easily top 10k).")] public int Max { get; init; } = 500; public override async ValueTask ExecuteAsync(IConsole console) { Validate(); ConfigureLogging(); var ct = console.RegisterCancellationHandler(); // Browse-only — no declared tags. EnableControllerBrowse=true flips DiscoverAsync's // symbol-walk on so every recognized primitive surfaces through the builder. Native // ADS notifications are irrelevant here (DiscoverAsync never subscribes); leave the // default on so the options record matches the production wiring. var options = new TwinCATDriverOptions { Devices = [new TwinCATDeviceOptions(Gateway, $"cli-{AmsNetId}:{AmsPort}")], Tags = [], Timeout = Timeout, Probe = new TwinCATProbeOptions { Enabled = false }, UseNativeNotifications = true, EnableControllerBrowse = true, }; await using var driver = new TwinCATDriver(options, DriverInstanceId); var builder = new CollectingAddressSpaceBuilder(); try { await driver.InitializeAsync("{}", ct); await driver.DiscoverAsync(builder, ct); } finally { await driver.ShutdownAsync(CancellationToken.None); } var matched = FilterByPrefix(builder.Variables, Prefix); var printLimit = PrintLimit(matched.Count, Max); await console.Output.WriteLineAsync($"AMS: {AmsNetId}:{AmsPort}"); await console.Output.WriteLineAsync( $"Symbols: {matched.Count} matched ({builder.Variables.Count} total), showing {printLimit}"); await console.Output.WriteLineAsync(); foreach (var v in matched.Take(printLimit)) { await console.Output.WriteLineAsync($" [{AccessTag(v.Info)}] {v.Info.DriverDataType,-8} {v.BrowseName}"); } if (matched.Count > printLimit) await console.Output.WriteLineAsync( $" … {matched.Count - printLimit} more — raise --max or tighten --prefix"); } /// /// Case-sensitive prefix filter. A null/empty prefix keeps everything; otherwise we /// keep symbols whose browse name starts with under /// — TwinCAT identifiers are case-sensitive on /// the wire, so a relaxed match would be misleading. /// internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix( IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix) => source .Where(v => string.IsNullOrEmpty(prefix) || v.BrowseName.StartsWith(prefix, StringComparison.Ordinal)) .ToList(); /// /// Cap-to-max projection. <= 0 means unbounded, otherwise the /// min of and . /// internal static int PrintLimit(int matchedCount, int max) => max <= 0 ? matchedCount : Math.Min(max, matchedCount); /// /// Coarse RO/RW label used in the browse output. /// is the only classification that is unconditionally read-only; everything else can be /// written from at least one ACL tier, so the CLI labels it RW. The real per-tier /// authorization is enforced server-side. /// internal static string AccessTag(DriverAttributeInfo info) => info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW"; internal sealed class CollectingAddressSpaceBuilder : IAddressSpaceBuilder { public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = []; public IAddressSpaceBuilder Folder(string browseName, string displayName) => this; public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info) { Variables.Add((browseName, info)); return new Handle(info.FullName); } public void AddProperty(string name, DriverDataType type, object? value) { } private sealed class Handle(string fullRef) : IVariableHandle { public string FullReference => fullRef; public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink(); } private sealed class NullSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } } } }