- Driver.TwinCAT.Cli-001: TwinCATCommandBase.Validate rejects non-positive TimeoutMs / IntervalMs and AmsPort outside 1..65535; ExecuteAsync calls it first. - Driver.TwinCAT.Cli-002: SubscribeCommand serialises every WriteLine through a writeLock to remove the notification-callback vs banner interleave risk. - Driver.TwinCAT.Cli-003: SubscribeCommand.DescribeMechanism derives the banner label from the returned ISubscriptionHandle.DiagnosticId so it can't disagree with what the driver actually did. - Driver.TwinCAT.Cli-004: introduced TwinCATTagCommandBase carrying --poll-only + BuildOptions; BrowseCommand stays on the slimmer TwinCATCommandBase so --poll-only no longer surfaces in browse --help. - Driver.TwinCAT.Cli-005: ProbeCommand --type now carries the 't' short alias to match the other commands. - Driver.TwinCAT.Cli-006: 35 new tests covering Gateway / AmsAddress parse / BuildOptions / PollOnly / browse-helpers / probe-alias / mechanism derivation. - Driver.TwinCAT.Cli-007: replaced the empty-init <inheritdoc/> with an explicit summary warning future maintainers about the no-op init. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
6.0 KiB
C#
136 lines
6.0 KiB
C#
using CliFx.Attributes;
|
|
using CliFx.Infrastructure;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;
|
|
|
|
/// <summary>
|
|
/// Walk the target's symbol table (ADS <c>SymbolLoaderFactory</c>, flat mode) and print every
|
|
/// symbol the driver's atomic-type mapper recognizes. Same path <c>DiscoverAsync</c> takes
|
|
/// when <c>EnableControllerBrowse = true</c> — structured UDTs / function-block instances
|
|
/// won't appear because the driver filters to the supported primitive surface.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Inherits from <see cref="TwinCATCommandBase"/> rather than
|
|
/// <see cref="TwinCATTagCommandBase"/> so the <c>--poll-only</c> flag does NOT surface in
|
|
/// <c>browse --help</c>: browse never subscribes, the flag would be a no-op, and the help
|
|
/// text would mislead users (Driver.TwinCAT.Cli-004).
|
|
/// </remarks>
|
|
[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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Case-sensitive prefix filter. A null/empty prefix keeps everything; otherwise we
|
|
/// keep symbols whose browse name starts with <paramref name="prefix"/> under
|
|
/// <see cref="StringComparison.Ordinal"/> — TwinCAT identifiers are case-sensitive on
|
|
/// the wire, so a relaxed match would be misleading.
|
|
/// </summary>
|
|
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();
|
|
|
|
/// <summary>
|
|
/// Cap-to-max projection. <paramref name="max"/> <= 0 means unbounded, otherwise the
|
|
/// min of <paramref name="matchedCount"/> and <paramref name="max"/>.
|
|
/// </summary>
|
|
internal static int PrintLimit(int matchedCount, int max)
|
|
=> max <= 0 ? matchedCount : Math.Min(max, matchedCount);
|
|
|
|
/// <summary>
|
|
/// Coarse RO/RW label used in the browse output. <see cref="SecurityClassification.ViewOnly"/>
|
|
/// 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.
|
|
/// </summary>
|
|
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) { }
|
|
}
|
|
}
|
|
}
|