@@ -20,9 +20,18 @@ public sealed class SubscribeCommand : TwinCATCommandBase
|
||||
"String / WString / Time / Date / DateTime / TimeOfDay (default DInt).")]
|
||||
public TwinCATDataType DataType { get; init; } = TwinCATDataType.DInt;
|
||||
|
||||
[CommandOption("interval-ms", 'i', Description = "Publishing interval ms (default 1000).")]
|
||||
[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();
|
||||
@@ -34,7 +43,8 @@ public sealed class SubscribeCommand : TwinCATCommandBase
|
||||
DeviceHostAddress: Gateway,
|
||||
SymbolPath: SymbolPath,
|
||||
DataType: DataType,
|
||||
Writable: false);
|
||||
Writable: false,
|
||||
MaxDelayMs: MaxDelayMs > 0 ? MaxDelayMs : null);
|
||||
var options = BuildOptions([tag]);
|
||||
|
||||
await using var driver = new TwinCATDriver(options, DriverInstanceId);
|
||||
@@ -54,8 +64,9 @@ public sealed class SubscribeCommand : TwinCATCommandBase
|
||||
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}). Ctrl+C to stop.");
|
||||
$"Subscribed to {SymbolPath} @ {IntervalMs}ms ({mode}{coalesce}). Ctrl+C to stop.");
|
||||
try
|
||||
{
|
||||
await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan, ct);
|
||||
|
||||
@@ -434,6 +434,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
TwinCATDataType type,
|
||||
int? bitIndex,
|
||||
TimeSpan cycleTime,
|
||||
int maxDelayMs,
|
||||
Action<string, object?> onChange,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -442,8 +443,14 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
// fires when the value differs; OnCycle fires every cycle. OnChange is the right default
|
||||
// for OPC UA data-change semantics — the PLC already has the best view of "has this
|
||||
// changed" so we let it decide.
|
||||
//
|
||||
// PR 3.1 (#313) — maxDelay is now per-tag tunable. 0 = "fire ASAP, no coalescing"
|
||||
// (the pre-PR-3.1 default). Larger values let the runtime coalesce bursty changes
|
||||
// so the OPC UA queue downstream doesn't flood under high-frequency signals. Same
|
||||
// unit (100ns ticks) as cycleTicks; convert from the caller-supplied milliseconds.
|
||||
var cycleTicks = (uint)Math.Max(1, cycleTime.Ticks / TimeSpan.TicksPerMillisecond * 10_000);
|
||||
var settings = new NotificationSettings(AdsTransMode.OnChange, (int)cycleTicks, 0);
|
||||
var maxDelayTicks = Math.Max(0, maxDelayMs) * 10_000;
|
||||
var settings = new NotificationSettings(AdsTransMode.OnChange, (int)cycleTicks, maxDelayTicks);
|
||||
|
||||
// AddDeviceNotificationExAsync returns Task<ResultHandle>; AdsNotificationEx fires
|
||||
// with the handle as part of the event args so we use the handle as the correlation
|
||||
|
||||
@@ -96,6 +96,13 @@ public interface ITwinCATClient : IDisposable
|
||||
/// <param name="type">Declared type; drives the native layout + callback value boxing.</param>
|
||||
/// <param name="bitIndex">For BOOL-within-word tags — the bit to extract from the parent word.</param>
|
||||
/// <param name="cycleTime">Minimum interval between change notifications (native-floor depends on target).</param>
|
||||
/// <param name="maxDelayMs">
|
||||
/// Per-tag <c>MaxDelay</c> in milliseconds — the upper bound on how long the runtime can
|
||||
/// buffer / coalesce change events before dispatching them (PR 3.1 / issue #313).
|
||||
/// <c>0</c> means "fire ASAP, no coalescing" (the pre-PR-3.1 default behaviour); larger
|
||||
/// values let the runtime coalesce bursty high-frequency changes so the OPC UA queue
|
||||
/// downstream doesn't flood. Plumbs straight into <c>NotificationSettings(mode, cycleTime, maxDelay)</c>.
|
||||
/// </param>
|
||||
/// <param name="onChange">Invoked with <c>(symbolPath, boxedValue)</c> per notification.</param>
|
||||
/// <param name="cancellationToken">Cancels the initial registration; does not tear down an established notification.</param>
|
||||
Task<ITwinCATNotificationHandle> AddNotificationAsync(
|
||||
@@ -103,6 +110,7 @@ public interface ITwinCATClient : IDisposable
|
||||
TwinCATDataType type,
|
||||
int? bitIndex,
|
||||
TimeSpan cycleTime,
|
||||
int maxDelayMs,
|
||||
Action<string, object?> onChange,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
|
||||
@@ -472,8 +472,11 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
var symbolName = parsed?.ToAdsSymbolName() ?? def.SymbolPath;
|
||||
var bitIndex = parsed?.BitIndex;
|
||||
|
||||
// PR 3.1 (#313) — pass the per-tag MaxDelay (default 0 = current behaviour)
|
||||
// through to NotificationSettings so the PLC can coalesce bursty changes.
|
||||
var maxDelayMs = def.MaxDelayMs ?? 0;
|
||||
var reg = await client.AddNotificationAsync(
|
||||
symbolName, def.DataType, bitIndex, publishingInterval,
|
||||
symbolName, def.DataType, bitIndex, publishingInterval, maxDelayMs,
|
||||
(_, value) => OnDataChange?.Invoke(this,
|
||||
new DataChangeEventArgs(handle, reference, new DataValueSnapshot(
|
||||
value, TwinCATStatusMapper.Good, DateTime.UtcNow, DateTime.UtcNow))),
|
||||
|
||||
@@ -50,6 +50,17 @@ public sealed record TwinCATDeviceOptions(
|
||||
/// only whole-array support; multi-dim shapes flatten to the product on the wire and the
|
||||
/// OPC UA layer reflects the rank via its own <c>ArrayDimensions</c> metadata.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>MaxDelayMs</b> (PR 3.1, issue #313) is the optional per-tag <c>MaxDelay</c>
|
||||
/// that flows into the native ADS <c>NotificationSettings(mode, cycleTime, maxDelay)</c>
|
||||
/// ctor. <c>maxDelay</c> is the upper bound on how long the runtime can buffer /
|
||||
/// coalesce change events before dispatching them. <c>null</c> / <c>0</c> (default)
|
||||
/// means "fire ASAP, no coalescing" — same as the pre-PR-3.1 behaviour. Set to a larger
|
||||
/// value (e.g. <c>500</c> ms) for bursty high-frequency signals where the OPC UA queue
|
||||
/// would otherwise flood. Has no effect when <c>UseNativeNotifications=false</c> (the
|
||||
/// polled fallback uses <c>PollGroupEngine</c>'s own publishing interval). Ignored for
|
||||
/// whole-array tags — those bypass the native-notification path entirely.</para>
|
||||
/// </remarks>
|
||||
public sealed record TwinCATTagDefinition(
|
||||
string Name,
|
||||
string DeviceHostAddress,
|
||||
@@ -57,7 +68,8 @@ public sealed record TwinCATTagDefinition(
|
||||
TwinCATDataType DataType,
|
||||
bool Writable = true,
|
||||
bool WriteIdempotent = false,
|
||||
int[]? ArrayDimensions = null);
|
||||
int[]? ArrayDimensions = null,
|
||||
int? MaxDelayMs = null);
|
||||
|
||||
public sealed class TwinCATProbeOptions
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user