docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -22,10 +22,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
/// </remarks>
|
||||
internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
{
|
||||
// Bounded so a slow downstream consumer cannot back the AMS router thread up — the
|
||||
// router thread enqueues and returns immediately (Driver.TwinCAT-008). 50k matches the
|
||||
// Galaxy EventPump default; ~500 notifications/connection is the ADS ceiling so this is
|
||||
// generous headroom against bursty change storms.
|
||||
private const int NotificationQueueCapacity = 50_000;
|
||||
|
||||
private readonly AdsClient _client = new();
|
||||
@@ -57,9 +53,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
|
||||
private readonly record struct PendingNotification(NotificationRegistration Registration, object? Value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the client is connected.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected => _client.IsConnected;
|
||||
|
||||
/// <summary>
|
||||
@@ -75,13 +69,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
return TwinCATStatusMapper.MapAdsError(adsError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connects to the specified ADS target asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="address">The AMS address to connect to.</param>
|
||||
/// <param name="timeout">The connection timeout.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task representing the asynchronous connection operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_client.IsConnected) return Task.CompletedTask;
|
||||
@@ -91,16 +79,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a value from the specified symbol path asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="symbolPath">The ADS symbol path to read from.</param>
|
||||
/// <param name="type">The TwinCAT data type.</param>
|
||||
/// <param name="bitIndex">Optional bit index for BOOL values within larger containers.</param>
|
||||
/// <param name="arrayCount">When non-null, read a 1-D array of this many <paramref name="type"/>
|
||||
/// elements; the boxed value is the element-typed CLR array (e.g. <c>int[]</c>). Phase 4c.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A tuple containing the value and OPC UA status code.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<(object? value, uint status)> ReadValueAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -170,17 +149,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
: symbolPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to the specified symbol path asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="symbolPath">The ADS symbol path to write to.</param>
|
||||
/// <param name="type">The TwinCAT data type.</param>
|
||||
/// <param name="bitIndex">Optional bit index for BOOL values. BOOL-within-word writes are handled
|
||||
/// upstream by <see cref="TwinCATDriver.WriteAsync"/> as a parent-word read-modify-write, so a
|
||||
/// bit index does not reach this method on the write path.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The OPC UA status code of the write operation.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<uint> WriteValueAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -203,11 +172,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Probes the connection to verify the ADS target is reachable.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>True if the probe succeeds; otherwise false.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -221,17 +186,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a notification (subscription) for changes to the specified symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbolPath">The ADS symbol path to monitor.</param>
|
||||
/// <param name="type">The TwinCAT data type.</param>
|
||||
/// <param name="bitIndex">Optional bit index for BOOL values.</param>
|
||||
/// <param name="cycleTime">The minimum cycle time between notifications.</param>
|
||||
/// <param name="maxDelayMs">The maximum delay before delivering a batched notification.</param>
|
||||
/// <param name="onChange">The callback to invoke when the value changes.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A handle to manage the notification subscription.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<ITwinCATNotificationHandle> AddNotificationAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -242,13 +197,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var clrType = MapToClrType(type);
|
||||
// NotificationSettings takes cycle + max-delay in milliseconds (Beckhoff InfoSys
|
||||
// tcadsnetref/7313319051 — "The unit is 1ms"). AdsTransMode.OnChange 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. maxDelayMs > 0 lets TwinCAT batch notifications up
|
||||
// to that delay before pushing them — exposed via TwinCATDriverOptions
|
||||
// (Driver.TwinCAT-014).
|
||||
var cycleMs = (int)Math.Max(1, cycleTime.TotalMilliseconds);
|
||||
var settings = new NotificationSettings(AdsTransMode.OnChange, cycleMs, Math.Max(0, maxDelayMs));
|
||||
|
||||
@@ -270,8 +218,8 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
/// <summary>
|
||||
/// Runs on the <see cref="AdsClient"/> AMS router thread. Does the cheap bit-extraction
|
||||
/// decode then enqueues — no driver logic, no consumer callbacks — so a slow consumer
|
||||
/// can never stall ADS notification delivery for the rest of the process
|
||||
/// (Driver.TwinCAT-008). Drops the notification (DropWrite) if the queue is saturated.
|
||||
/// can never stall ADS notification delivery for the rest of the process.
|
||||
/// Drops the notification (DropWrite) if the queue is saturated.
|
||||
/// </summary>
|
||||
private void OnAdsNotificationEx(object? sender, AdsNotificationExEventArgs args)
|
||||
{
|
||||
@@ -311,11 +259,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
catch { /* best-effort tear-down; target may already be gone */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Browses all available symbols on the connected ADS target asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An async enumerable of discovered symbols.</returns>
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -341,11 +285,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
foreach (var ds in TwinCATSymbolExpander.ExpandLeaves(
|
||||
loader.Symbols.Select(s => new AdsSymbolNode(s))))
|
||||
{
|
||||
// ThrowIfCancellationRequested — not yield break — so a cancelled browse propagates
|
||||
// as OperationCanceledException rather than a silent clean completion. DiscoverAsync
|
||||
// has an explicit catch(OperationCanceledException){ throw; } to surface this
|
||||
// distinctly from a genuine browse failure; a yield break would let a partial
|
||||
// symbol set appear as a fully successful discovery (Driver.TwinCAT-010).
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
yield return ds;
|
||||
}
|
||||
@@ -368,6 +307,11 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
private readonly IReadOnlyList<ITwinCATSymbolNode> _children;
|
||||
private readonly bool _readOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdsSymbolNode"/> class, computing its
|
||||
/// struct/mapped-type/children/read-only state eagerly from the wrapped <paramref name="symbol"/>.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The Beckhoff ADS symbol to adapt.</param>
|
||||
public AdsSymbolNode(ISymbol symbol)
|
||||
{
|
||||
_symbol = symbol;
|
||||
@@ -379,14 +323,19 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
_readOnly = !IsSymbolWritable(symbol);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string InstancePath => _symbol.InstancePath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsStruct => _isStruct;
|
||||
|
||||
/// <inheritdoc />
|
||||
public (TwinCATDataType? Type, int? ArrayLength) Mapped => _mapped;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ITwinCATSymbolNode> Children => _children;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ReadOnly => _readOnly;
|
||||
}
|
||||
|
||||
@@ -577,9 +526,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
||||
/// <summary>Default <see cref="ITwinCATClientFactory"/> — one <see cref="AdsTwinCATClient"/> per call.</summary>
|
||||
internal sealed class AdsTwinCATClientFactory : ITwinCATClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="AdsTwinCATClient"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="AdsTwinCATClient"/> instance.</returns>
|
||||
/// <inheritdoc />
|
||||
public ITwinCATClient Create() => new AdsTwinCATClient();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// <param name="address">The target AMS address.</param>
|
||||
/// <param name="timeout">The connection timeout.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the connection attempt.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>True when the AMS router + target both accept commands.</summary>
|
||||
@@ -28,7 +29,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// 1809 / 0x0711) on any read / write / notification — the signal that a PLC program
|
||||
/// re-download has invalidated every symbol + notification handle. The driver forwards
|
||||
/// this to <see cref="Core.Abstractions.IRediscoverable.OnRediscoveryNeeded"/> so Core
|
||||
/// rebuilds the address space subtree (docs/v2/driver-specs.md §6, Driver.TwinCAT-013).
|
||||
/// rebuilds the address space subtree (docs/v2/driver-specs.md §6).
|
||||
/// </summary>
|
||||
event EventHandler? OnSymbolVersionChanged;
|
||||
|
||||
@@ -44,6 +45,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// elements; the boxed value is the element-typed CLR array (<c>int[]</c> / <c>float[]</c> /
|
||||
/// <c>bool[]</c> / <c>string[]</c> / …). When null, read a scalar (Phase 4c).</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
|
||||
/// <returns>The boxed value read (or <c>null</c>) together with the mapped OPC UA status.</returns>
|
||||
Task<(object? value, uint status)> ReadValueAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -60,6 +62,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// <param name="bitIndex">Optional bit index for bit manipulation within a word.</param>
|
||||
/// <param name="value">The value to write.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the write operation.</param>
|
||||
/// <returns>The mapped OPC UA status for the write (0 = Good).</returns>
|
||||
Task<uint> WriteValueAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -72,6 +75,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// Used by <see cref="Core.Abstractions.IHostConnectivityProbe"/>'s probe loop.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the probe operation.</param>
|
||||
/// <returns><c>true</c> when the target is reachable; otherwise <c>false</c>.</returns>
|
||||
Task<bool> ProbeAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -86,9 +90,10 @@ public interface ITwinCATClient : IDisposable
|
||||
/// <param name="cycleTime">Minimum interval between change notifications (native-floor depends on target).</param>
|
||||
/// <param name="maxDelayMs">Maximum batching delay in milliseconds — TwinCAT may coalesce
|
||||
/// notifications up to this delay before pushing them. <c>0</c> = no batching, push
|
||||
/// immediately (Driver.TwinCAT-014).</param>
|
||||
/// immediately.</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>
|
||||
/// <returns>A handle whose disposal tears the registered notification down.</returns>
|
||||
Task<ITwinCATNotificationHandle> AddNotificationAsync(
|
||||
string symbolPath,
|
||||
TwinCATDataType type,
|
||||
@@ -109,6 +114,7 @@ public interface ITwinCATClient : IDisposable
|
||||
/// and never see struct-typed entries — expansion is performed here, not in the caller.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the enumeration operation.</param>
|
||||
/// <returns>An asynchronous stream of the target's atomic-leaf symbols.</returns>
|
||||
IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -138,5 +144,6 @@ public sealed record TwinCATDiscoveredSymbol(
|
||||
public interface ITwinCATClientFactory
|
||||
{
|
||||
/// <summary>Creates a new TwinCAT client instance.</summary>
|
||||
/// <returns>A new, not-yet-connected <see cref="ITwinCATClient"/>.</returns>
|
||||
ITwinCATClient Create();
|
||||
}
|
||||
|
||||
@@ -13,16 +13,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
||||
public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable, IDisposable, IAsyncDisposable
|
||||
{
|
||||
// Mutable so ReinitializeAsync can apply a new config generation (Driver.TwinCAT-001).
|
||||
// The constructor seeds it; InitializeAsync re-parses driverConfigJson over the top of it.
|
||||
// Mutable so ReinitializeAsync can apply a new config generation. The constructor seeds
|
||||
// it; InitializeAsync re-parses driverConfigJson over the top of it.
|
||||
private TwinCATDriverOptions _options;
|
||||
private readonly string _driverInstanceId;
|
||||
private readonly ITwinCATClientFactory _clientFactory;
|
||||
private readonly ILogger<TwinCATDriver> _logger;
|
||||
private readonly PollGroupEngine _poll;
|
||||
// ConcurrentDictionary so ShutdownAsync (Clear) and ReadAsync/WriteAsync/SubscribeAsync
|
||||
// (TryGetValue) don't race — plain Dictionary is not safe for concurrent read+write
|
||||
// (Driver.TwinCAT-009).
|
||||
// (TryGetValue) don't race — plain Dictionary is not safe for concurrent read+write.
|
||||
private readonly ConcurrentDictionary<string, DeviceState> _devices =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, TwinCATTagDefinition> _tagsByName =
|
||||
@@ -73,23 +72,20 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
|
||||
}
|
||||
|
||||
/// <summary>Gets the unique driver instance identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverInstanceId => _driverInstanceId;
|
||||
/// <summary>Gets the driver type name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DriverType => "TwinCAT";
|
||||
|
||||
/// <summary>Initializes the driver with configuration and establishes device connections.</summary>
|
||||
/// <param name="driverConfigJson">JSON configuration string for the driver.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
_health = new DriverHealth(DriverState.Initializing, null, null);
|
||||
try
|
||||
{
|
||||
// Apply the supplied config generation (Driver.TwinCAT-001). A blank or content-free
|
||||
// document keeps the constructor-seeded options — that path covers callers that have
|
||||
// already materialised options up front (the factory passes both, in agreement).
|
||||
// Apply the supplied config generation. A blank or content-free document keeps the
|
||||
// constructor-seeded options — that path covers callers that have already
|
||||
// materialised options up front (the factory passes both, in agreement).
|
||||
if (!string.IsNullOrWhiteSpace(driverConfigJson))
|
||||
{
|
||||
var parsed = TwinCATDriverFactoryExtensions.ParseOptions(driverConfigJson, _driverInstanceId);
|
||||
@@ -112,7 +108,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
{
|
||||
state.ProbeCts = new CancellationTokenSource();
|
||||
var ct = state.ProbeCts.Token;
|
||||
// Store the task so ShutdownAsync can await it (Driver.TwinCAT-009).
|
||||
// Store the task so ShutdownAsync can await it.
|
||||
state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct);
|
||||
}
|
||||
}
|
||||
@@ -126,19 +122,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Reinitializes the driver by shutting down and reinitializing with new configuration.</summary>
|
||||
/// <param name="driverConfigJson">JSON configuration string for the driver.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
||||
{
|
||||
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
|
||||
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Shuts down the driver and releases all device connections and subscriptions.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Native subs first — disposing the handles is cheap + lets the client close its
|
||||
@@ -150,8 +141,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
await _poll.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
// Cancel every probe loop and await its task before disposing the client + gate so the
|
||||
// loop can never touch a disposed object. (Driver.TwinCAT-009: ShutdownAsync previously
|
||||
// cancelled ProbeCts but did not await the task before calling DisposeClient.)
|
||||
// loop can never touch a disposed object.
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
try { state.ProbeCts?.Cancel(); } catch { }
|
||||
@@ -176,27 +166,20 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
||||
}
|
||||
|
||||
/// <summary>Gets the current driver health status.</summary>
|
||||
/// <returns>Driver health information.</returns>
|
||||
/// <inheritdoc />
|
||||
public DriverHealth GetHealth() => _health;
|
||||
|
||||
/// <summary>
|
||||
/// Estimated bytes attributable to this driver instance (Driver.TwinCAT-012).
|
||||
/// This driver holds no flushable symbol cache — <c>BrowseSymbolsAsync</c> streams and
|
||||
/// discards; the footprint reflects live allocations only:
|
||||
/// ~256 bytes per pre-declared tag (tag-definition record + dictionary overhead) and
|
||||
/// ~512 bytes per active native subscription.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// This driver holds no flushable symbol cache — BrowseSymbolsAsync streams and discards; the
|
||||
// footprint reflects live allocations only: ~256 bytes per pre-declared tag (tag-definition
|
||||
// record + dictionary overhead) and ~512 bytes per active native subscription.
|
||||
public long GetMemoryFootprint() =>
|
||||
(_tagsByName.Count * 256L) + (_nativeSubs.Count * 512L);
|
||||
|
||||
/// <summary>
|
||||
/// No flushable cache exists in this driver — the symbol table is streamed fresh on
|
||||
/// every <see cref="DiscoverAsync"/> call. This is a no-op but is deliberately present
|
||||
/// so Core's cache-budget enforcement sees a compliant Tier-A driver.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
// No flushable cache exists in this driver — the symbol table is streamed fresh on every
|
||||
// DiscoverAsync call. This is a no-op but is deliberately present so Core's cache-budget
|
||||
// enforcement sees a compliant Tier-A driver.
|
||||
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>Gets the count of configured devices.</summary>
|
||||
@@ -209,10 +192,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
// ---- IReadable ----
|
||||
|
||||
/// <summary>Reads values for the specified tag references from ADS devices.</summary>
|
||||
/// <param name="fullReferences">The full tag references to read.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Data value snapshots for each reference.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
|
||||
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -272,10 +252,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
// ---- IWritable ----
|
||||
|
||||
/// <summary>Writes values to the specified tags on ADS devices.</summary>
|
||||
/// <param name="writes">The write requests to execute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Write results for each request.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -377,18 +354,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
// ---- ITagDiscovery ----
|
||||
|
||||
/// <summary>
|
||||
/// Run-once: <see cref="DiscoverAsync"/> emits pre-declared tags and (when
|
||||
/// EnableControllerBrowse is set) fully awaits the controller symbol browse within the
|
||||
/// single call, streaming the complete node set in one pass — nothing fills in
|
||||
/// asynchronously after connect, so a single discovery pass is sufficient.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// Run-once: DiscoverAsync emits pre-declared tags and (when EnableControllerBrowse is set)
|
||||
// fully awaits the controller symbol browse within the single call, streaming the complete
|
||||
// node set in one pass — nothing fills in asynchronously after connect, so a single
|
||||
// discovery pass is sufficient.
|
||||
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
|
||||
|
||||
/// <summary>Discovers devices and tags from ADS configuration and optionally controller symbols.</summary>
|
||||
/// <param name="builder">Address space builder for adding discovered nodes.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
@@ -468,17 +441,11 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
private readonly ConcurrentDictionary<long, NativeSubscription> _nativeSubs = new();
|
||||
private long _nextNativeSubId;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe via native ADS notifications when <see cref="TwinCATDriverOptions.UseNativeNotifications"/>
|
||||
/// is <c>true</c>, otherwise fall through to the shared <see cref="PollGroupEngine"/>.
|
||||
/// Native path registers one <see cref="ITwinCATNotificationHandle"/> per tag against the
|
||||
/// target's PLC runtime — the PLC pushes changes on its own cycle so we skip the poll
|
||||
/// loop entirely. Unsub path disposes the handles.
|
||||
/// </summary>
|
||||
/// <param name="fullReferences">The full tag references to subscribe to.</param>
|
||||
/// <param name="publishingInterval">The publishing interval for updates.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Subscription handle for managing the subscription.</returns>
|
||||
/// <inheritdoc />
|
||||
// Subscribe via native ADS notifications when TwinCATDriverOptions.UseNativeNotifications is
|
||||
// true, otherwise fall through to the shared PollGroupEngine. Native path registers one
|
||||
// ITwinCATNotificationHandle per tag against the target's PLC runtime — the PLC pushes
|
||||
// changes on its own cycle so we skip the poll loop entirely. Unsub path disposes the handles.
|
||||
public async Task<ISubscriptionHandle> SubscribeAsync(
|
||||
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -529,10 +496,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>Unsubscribes from a native or poll-based subscription.</summary>
|
||||
/// <param name="handle">The subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Completion task.</returns>
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
if (handle is NativeSubscriptionHandle native && _nativeSubs.TryRemove(native.Id, out var sub))
|
||||
@@ -546,7 +510,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
private sealed record NativeSubscriptionHandle(long Id) : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for the subscription.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => $"twincat-native-sub-{Id}";
|
||||
}
|
||||
|
||||
@@ -556,8 +520,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
// ---- IHostConnectivityProbe ----
|
||||
|
||||
/// <summary>Gets the connectivity status for all configured devices.</summary>
|
||||
/// <returns>List of host connectivity statuses.</returns>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
|
||||
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
|
||||
|
||||
@@ -569,7 +532,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
try
|
||||
{
|
||||
// Probe-initiated connects honor TwinCATProbeOptions.Timeout — distinct from
|
||||
// the driver-wide _options.Timeout used by reads/writes (Driver.TwinCAT-014).
|
||||
// the driver-wide _options.Timeout used by reads/writes.
|
||||
var client = await EnsureConnectedAsync(state, ct, _options.Probe.Timeout)
|
||||
.ConfigureAwait(false);
|
||||
success = await client.ProbeAsync(ct).ConfigureAwait(false);
|
||||
@@ -614,26 +577,23 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// host is an <c>ads://…</c> URI), so it cleanly signals "unresolved" without colliding
|
||||
/// with a real host key. Used to be <see cref="DriverInstanceId"/>, which is a logical
|
||||
/// config-DB identifier — that collided with consumers who expected the resolver and the
|
||||
/// connectivity-status table to share keys (Driver.TwinCAT-006).
|
||||
/// connectivity-status table to share keys.
|
||||
/// </summary>
|
||||
public const string UnresolvedHostSentinel = "";
|
||||
|
||||
/// <summary>Resolves the device host address for the specified tag reference.</summary>
|
||||
/// <param name="fullReference">The full tag reference.</param>
|
||||
/// <returns>The host address or <see cref="UnresolvedHostSentinel"/> if not found.</returns>
|
||||
/// <inheritdoc />
|
||||
public string ResolveHost(string fullReference)
|
||||
{
|
||||
if (_tagsByName.TryGetValue(fullReference, out var def))
|
||||
return def.DeviceHostAddress;
|
||||
// First device's HostAddress when one exists; otherwise the unresolved sentinel —
|
||||
// intentionally NOT DriverInstanceId, which is a config-DB key, not a host address
|
||||
// (Driver.TwinCAT-006).
|
||||
// intentionally NOT DriverInstanceId, which is a config-DB key, not a host address.
|
||||
return _options.Devices.FirstOrDefault()?.HostAddress ?? UnresolvedHostSentinel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazily connect a device's client, serialized per device by
|
||||
/// <see cref="DeviceState.ConnectGate"/> (Driver.TwinCAT-007). Without the gate, a
|
||||
/// <see cref="DeviceState.ConnectGate"/>. Without the gate, a
|
||||
/// concurrent read / write / probe could each create + connect a separate client and
|
||||
/// leak all-but-one, or dispose a client another thread is mid-connect on. The S7 and
|
||||
/// AB-CIP drivers serialize device access the same way; single-connection-per-PLC is
|
||||
@@ -661,8 +621,8 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
var client = _clientFactory.Create();
|
||||
client.OnSymbolVersionChanged += HandleSymbolVersionChanged;
|
||||
// timeoutOverride lets the probe loop use TwinCATProbeOptions.Timeout for probe-
|
||||
// initiated connects rather than the driver-level _options.Timeout
|
||||
// (Driver.TwinCAT-014). Reads / writes pass null and get the driver default.
|
||||
// initiated connects rather than the driver-level _options.Timeout. Reads / writes
|
||||
// pass null and get the driver default.
|
||||
var effectiveTimeout = timeoutOverride ?? _options.Timeout;
|
||||
try
|
||||
{
|
||||
@@ -692,7 +652,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
|
||||
/// <summary>
|
||||
/// Routes a wire-detected ADS symbol-version-changed (DeviceSymbolVersionInvalid 1809 /
|
||||
/// 0x0711) to Core as an <see cref="IRediscoverable"/> invocation (Driver.TwinCAT-013).
|
||||
/// 0x0711) to Core as an <see cref="IRediscoverable"/> invocation.
|
||||
/// A PLC re-download invalidates every symbol + notification handle, so the address
|
||||
/// space must be rebuilt — this is the documented TwinCAT failure mode, not a transient
|
||||
/// connection error.
|
||||
@@ -705,8 +665,8 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// <summary>
|
||||
/// Synchronous teardown — no <c>await</c>, no captured sync context. The OPC UA stack
|
||||
/// thread can call <see cref="Dispose"/>; routing through <c>DisposeAsync().GetResult()</c>
|
||||
/// can deadlock on a single-threaded sync context (Driver.TwinCAT-015,
|
||||
/// docs/v2/driver-stability.md). The operations here are all genuinely synchronous —
|
||||
/// can deadlock on a single-threaded sync context (see docs/v2/driver-stability.md).
|
||||
/// The operations here are all genuinely synchronous —
|
||||
/// cancel tokens, wait on task handles with a hard timeout, dispose clients — so a
|
||||
/// synchronous path does the right thing without re-entering the scheduler.
|
||||
/// </summary>
|
||||
@@ -759,7 +719,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
public ITwinCATClient? Client { get; set; }
|
||||
|
||||
/// <summary>Serializes connect / reconnect so concurrent callers never race a client
|
||||
/// create-or-dispose for this device (Driver.TwinCAT-007).</summary>
|
||||
/// create-or-dispose for this device.</summary>
|
||||
public SemaphoreSlim ConnectGate { get; } = new(1, 1);
|
||||
|
||||
/// <summary>Gets the lock object for synchronizing host state transitions.</summary>
|
||||
@@ -771,7 +731,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
|
||||
/// <summary>Gets or sets the cancellation token source for the probe loop.</summary>
|
||||
public CancellationTokenSource? ProbeCts { get; set; }
|
||||
/// <summary>The running probe-loop task — awaited by <see cref="TwinCATDriver.ShutdownAsync"/>
|
||||
/// so the loop cannot touch a disposed client (Driver.TwinCAT-009).</summary>
|
||||
/// so the loop cannot touch a disposed client.</summary>
|
||||
public Task? ProbeTask { get; set; }
|
||||
|
||||
/// <summary>Disposes the active ADS client if any.</summary>
|
||||
|
||||
@@ -24,6 +24,7 @@ public static class TwinCATDriverFactoryExtensions
|
||||
/// <summary>Creates a TwinCAT driver instance from the provided configuration.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier.</param>
|
||||
/// <param name="driverConfigJson">The driver configuration as JSON.</param>
|
||||
/// <returns>The constructed <see cref="TwinCATDriver"/> instance.</returns>
|
||||
internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
@@ -35,10 +36,11 @@ public static class TwinCATDriverFactoryExtensions
|
||||
/// Parse a TwinCAT driver-config JSON document into a <see cref="TwinCATDriverOptions"/>.
|
||||
/// Shared by <see cref="CreateInstance"/> (constructor-time) and
|
||||
/// <see cref="TwinCATDriver.InitializeAsync"/> / <see cref="TwinCATDriver.ReinitializeAsync"/>
|
||||
/// so a config generation pushed via Reinitialize is actually applied (Driver.TwinCAT-001).
|
||||
/// so a config generation pushed via Reinitialize is actually applied.
|
||||
/// </summary>
|
||||
/// <param name="driverConfigJson">The JSON configuration string.</param>
|
||||
/// <param name="driverInstanceId">The driver instance identifier.</param>
|
||||
/// <returns>The parsed <see cref="TwinCATDriverOptions"/>.</returns>
|
||||
internal static TwinCATDriverOptions ParseOptions(string driverConfigJson, string driverInstanceId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
@@ -79,6 +81,7 @@ public static class TwinCATDriverFactoryExtensions
|
||||
/// </summary>
|
||||
/// <param name="driverConfigJson">The JSON configuration string.</param>
|
||||
/// <param name="driverInstanceId">The driver instance identifier.</param>
|
||||
/// <returns>The parsed <see cref="TwinCATDriverOptions"/>.</returns>
|
||||
public static TwinCATDriverOptions ParseOptionsForTests(string driverConfigJson, string driverInstanceId)
|
||||
=> ParseOptions(driverConfigJson, driverInstanceId);
|
||||
|
||||
@@ -86,13 +89,6 @@ public static class TwinCATDriverFactoryExtensions
|
||||
{
|
||||
var dataType = ParseEnum<TwinCATDataType>(t.DataType, t.Name, driverInstanceId, "DataType");
|
||||
|
||||
// Driver.TwinCAT-003: Structure-typed pre-declared tags are not supported. The driver's
|
||||
// atomic surface cannot read/write UDT blobs — MapToClrType falls through to typeof(int)
|
||||
// and ConvertForWrite throws NotSupportedException, producing garbage reads or late
|
||||
// runtime failures. BrowseSymbolsAsync expands a Structure/UDT/FB symbol into its atomic
|
||||
// member leaves (the container itself is never surfaced as a writable Structure node), so the
|
||||
// supported way to address members is discovery — not a pre-declared Structure tag. Reject here
|
||||
// with a clear error so operators get a configuration-time failure, not a silent wrong value.
|
||||
if (dataType == TwinCATDataType.Structure)
|
||||
throw new InvalidOperationException(
|
||||
$"TwinCAT tag '{t.Name ?? "<unnamed>"}' in '{driverInstanceId}' specifies " +
|
||||
@@ -110,9 +106,6 @@ public static class TwinCATDriverFactoryExtensions
|
||||
DataType: dataType,
|
||||
Writable: t.Writable ?? true,
|
||||
WriteIdempotent: t.WriteIdempotent ?? false,
|
||||
// Driver.TwinCAT-017: thread arrayLength through so JSON-authored pre-declared tags can
|
||||
// declare 1-D arrays (Phase 4c). Only positive values are honoured; null / non-positive
|
||||
// keeps the scalar default (ArrayLength: null on TwinCATTagDefinition).
|
||||
ArrayLength: t.ArrayLength is > 0 ? t.ArrayLength : null);
|
||||
}
|
||||
|
||||
@@ -192,7 +185,7 @@ public static class TwinCATDriverFactoryExtensions
|
||||
/// <summary>
|
||||
/// Optional 1-D array element count. When positive, the tag is a 1-D array of this
|
||||
/// many <see cref="DataType"/> elements — drives <c>IsArray</c>/<c>ArrayDim</c> at
|
||||
/// discovery and a native ADS array read at runtime (Phase 4c, Driver.TwinCAT-017).
|
||||
/// discovery and a native ADS array read at runtime (Phase 4c).
|
||||
/// <c>null</c> or non-positive = scalar (the default).
|
||||
/// </summary>
|
||||
public int? ArrayLength { get; init; }
|
||||
|
||||
@@ -30,11 +30,11 @@ public static class TwinCATStatusMapper
|
||||
/// Raised by the runtime after a PLC program re-download: every symbol handle and
|
||||
/// notification handle the driver holds is now stale. The driver treats this as an
|
||||
/// <see cref="Core.Abstractions.IRediscoverable"/> trigger, not a connection error
|
||||
/// (docs/v2/driver-specs.md §6, Driver.TwinCAT-013).
|
||||
/// (docs/v2/driver-specs.md §6).
|
||||
/// <para>
|
||||
/// Note: legacy Beckhoff InfoSys documentation sometimes cites this as "0x0702"; that
|
||||
/// is a transcription error — 0x0702 is <c>DeviceInvalidGroup</c> (1794). The SDK enum
|
||||
/// value 1809 (0x0711) is authoritative (Driver.TwinCAT-011).
|
||||
/// value 1809 (0x0711) is authoritative.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public const uint AdsSymbolVersionChanged = 1809u; // DeviceSymbolVersionInvalid = 0x0711
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class TwinCATSystemSymbolFilter
|
||||
{
|
||||
/// <summary>Gets a value indicating whether a symbol is a system or infrastructure symbol.</summary>
|
||||
/// <param name="instancePath">The symbol instance path to check.</param>
|
||||
/// <returns><see langword="true"/> if the symbol is a system/infrastructure symbol that should be filtered out; otherwise <see langword="false"/>.</returns>
|
||||
public static bool IsSystemSymbol(string instancePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(instancePath)) return true;
|
||||
|
||||
Reference in New Issue
Block a user