using CliFx.Attributes; using CliFx.Infrastructure; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli.Commands; /// /// Force a controller-side @tags re-walk on a live AbCip driver instance. Issue #233 — /// online tag-DB refresh trigger. The CLI variant builds a transient driver against the /// supplied gateway, runs , and prints the freshly /// discovered tag names. In-server (Tier-A) operators wire this same call to an Admin UI /// button so a controller program-download is reflected in the address space without a /// driver restart. /// [Command("rebrowse", Description = "Re-walk the AB CIP controller symbol table (force @tags refresh) and print discovered tags.")] public sealed class RebrowseCommand : AbCipCommandBase { public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); var ct = console.RegisterCancellationHandler(); // EnableControllerBrowse must be true for the @tags walk to happen; the CLI baseline // (BuildOptions in AbCipCommandBase) leaves it off for one-shot probes, so we flip it // here without touching the base helper. var baseOpts = BuildOptions(tags: []); var options = new AbCipDriverOptions { Devices = baseOpts.Devices, Tags = baseOpts.Tags, Timeout = baseOpts.Timeout, Probe = baseOpts.Probe, EnableControllerBrowse = true, EnableAlarmProjection = false, }; await using var driver = new AbCipDriver(options, DriverInstanceId); try { await driver.InitializeAsync("{}", ct); var builder = new ConsoleAddressSpaceBuilder(); await driver.RebrowseAsync(builder, ct); await console.Output.WriteLineAsync($"Gateway: {Gateway}"); await console.Output.WriteLineAsync($"Family: {Family}"); await console.Output.WriteLineAsync($"Variables: {builder.VariableCount}"); await console.Output.WriteLineAsync(); foreach (var line in builder.Lines) await console.Output.WriteLineAsync(line); } finally { await driver.ShutdownAsync(CancellationToken.None); } } /// /// Minimal in-memory that flattens the tree to one /// line per variable for CLI display. Folder nesting is captured in the prefix so the /// operator can see the same shape the in-server builder would receive. /// private sealed class ConsoleAddressSpaceBuilder : IAddressSpaceBuilder { private readonly string _prefix; private readonly Counter _counter; public List Lines { get; } public int VariableCount => _counter.Count; public ConsoleAddressSpaceBuilder() : this("", new List(), new Counter()) { } private ConsoleAddressSpaceBuilder(string prefix, List sharedLines, Counter counter) { _prefix = prefix; Lines = sharedLines; _counter = counter; } public IAddressSpaceBuilder Folder(string browseName, string displayName) { var newPrefix = string.IsNullOrEmpty(_prefix) ? browseName : $"{_prefix}/{browseName}"; return new ConsoleAddressSpaceBuilder(newPrefix, Lines, _counter); } public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo info) { _counter.Count++; Lines.Add($" {_prefix}/{browseName} ({info.DriverDataType}, {info.SecurityClass})"); return new Handle(info.FullName); } public void AddProperty(string browseName, DriverDataType dataType, object? value) { } private sealed class Counter { public int Count; } 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) { } } } }