Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/SubscribeCommand.cs
T
2026-04-26 01:45:12 -04:00

90 lines
3.8 KiB
C#

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.TwinCAT.Cli.Commands;
/// <summary>
/// Watch a TwinCAT symbol until Ctrl+C. Native ADS notifications by default (TwinCAT
/// pushes on its own cycle); pass <c>--poll-only</c> to fall through to PollGroupEngine.
/// </summary>
[Command("subscribe", Description = "Watch a TwinCAT symbol via ADS notification or poll, until Ctrl+C.")]
public sealed class SubscribeCommand : TwinCATCommandBase
{
[CommandOption("symbol", 's', Description = "Symbol path — same format as `read`.", IsRequired = true)]
public string SymbolPath { get; init; } = default!;
[CommandOption("type", 't', Description =
"Bool / SInt / USInt / Int / UInt / DInt / UDInt / LInt / ULInt / Real / LReal / " +
"String / WString / Time / Date / DateTime / TimeOfDay (default DInt).")]
public TwinCATDataType DataType { get; init; } = TwinCATDataType.DInt;
[CommandOption("interval-ms", 'i', Description =
"Cycle time ms — minimum interval between change checks the PLC runtime applies " +
"(default 1000). Different from --max-delay-ms; see help for that flag.")]
public int IntervalMs { get; init; } = 1000;
[CommandOption("max-delay-ms", Description =
"Per-tag MaxDelay in ms (PR 3.1, issue #313) — upper bound on how long the runtime " +
"can buffer / coalesce change events before dispatch. 0 (default) = fire ASAP, no " +
"coalescing. Larger values (e.g. 500) reduce event rate on bursty signals so the " +
"OPC UA queue downstream doesn't flood. Distinct from --interval-ms (the cycle).")]
public int MaxDelayMs { get; init; } = 0;
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
var ct = console.RegisterCancellationHandler();
var tagName = ReadCommand.SynthesiseTagName(SymbolPath, DataType);
var tag = new TwinCATTagDefinition(
Name: tagName,
DeviceHostAddress: Gateway,
SymbolPath: SymbolPath,
DataType: DataType,
Writable: false,
MaxDelayMs: MaxDelayMs > 0 ? MaxDelayMs : null);
var options = BuildOptions([tag]);
await using var driver = new TwinCATDriver(options, DriverInstanceId);
ISubscriptionHandle? handle = null;
try
{
await driver.InitializeAsync("{}", ct);
driver.OnDataChange += (_, e) =>
{
var line = $"[{DateTime.UtcNow:HH:mm:ss.fff}] " +
$"{e.FullReference} = {SnapshotFormatter.FormatValue(e.Snapshot.Value)} " +
$"({SnapshotFormatter.FormatStatus(e.Snapshot.StatusCode)})";
console.Output.WriteLine(line);
};
handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
var mode = PollOnly ? "polling" : "ADS notification";
var coalesce = MaxDelayMs > 0 ? $", max-delay {MaxDelayMs}ms" : "";
await console.Output.WriteLineAsync(
$"Subscribed to {SymbolPath} @ {IntervalMs}ms ({mode}{coalesce}). Ctrl+C to stop.");
try
{
await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan, ct);
}
catch (OperationCanceledException)
{
// Expected on Ctrl+C.
}
}
finally
{
if (handle is not null)
{
try { await driver.UnsubscribeAsync(handle, CancellationToken.None); }
catch { /* teardown best-effort */ }
}
await driver.ShutdownAsync(CancellationToken.None);
}
}
}