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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user