docs: backfill XML documentation across 756 files
v2-ci / build (push) Failing after 1m43s
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>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
This commit is contained in:
Joseph Doherty
2026-05-28 08:10:17 -04:00
parent f9fc7dd2e1
commit 64e3fbe035
756 changed files with 9876 additions and 96 deletions
@@ -46,6 +46,9 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
private readonly Task _dispatchLoop;
private int _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="AdsTwinCATClient"/> class.
/// </summary>
public AdsTwinCATClient()
{
_client.AdsNotificationEx += OnAdsNotificationEx;
@@ -54,8 +57,14 @@ 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>
public bool IsConnected => _client.IsConnected;
/// <summary>
/// Occurs when the symbol version changes on the connected ADS target.
/// </summary>
public event EventHandler? OnSymbolVersionChanged;
/// <summary>Raise <see cref="OnSymbolVersionChanged"/> when <paramref name="adsError"/> is <c>DeviceSymbolVersionInvalid</c> (1809 / 0x0711).</summary>
@@ -66,6 +75,13 @@ 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>
public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken)
{
if (_client.IsConnected) return Task.CompletedTask;
@@ -75,6 +91,14 @@ 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="cancellationToken">The cancellation token.</param>
/// <returns>A tuple containing the value and OPC UA status code.</returns>
public async Task<(object? value, uint status)> ReadValueAsync(
string symbolPath,
TwinCATDataType type,
@@ -123,6 +147,15 @@ 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 (not supported for writes).</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>
public async Task<uint> WriteValueAsync(
string symbolPath,
TwinCATDataType type,
@@ -149,6 +182,11 @@ 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>
public async Task<bool> ProbeAsync(CancellationToken cancellationToken)
{
try
@@ -162,6 +200,17 @@ 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>
public async Task<ITwinCATNotificationHandle> AddNotificationAsync(
string symbolPath,
TwinCATDataType type,
@@ -228,6 +277,12 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
}
}
/// <summary>
/// Deletes a notification subscription by handle.
/// </summary>
/// <param name="handle">The notification handle to delete.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous delete operation.</returns>
internal async Task DeleteNotificationAsync(uint handle, CancellationToken cancellationToken)
{
_notifications.TryRemove(handle, out _);
@@ -235,6 +290,11 @@ 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>
public async IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -299,6 +359,9 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
return true;
}
/// <summary>
/// Disposes the client and releases all resources.
/// </summary>
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
@@ -323,11 +386,29 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
AdsTwinCATClient owner,
uint handle) : ITwinCATNotificationHandle
{
/// <summary>
/// Gets the symbol path being monitored.
/// </summary>
public string SymbolPath { get; } = symbolPath;
/// <summary>
/// Gets the TwinCAT data type of the symbol.
/// </summary>
public TwinCATDataType Type { get; } = type;
/// <summary>
/// Gets the optional bit index for BOOL values.
/// </summary>
public int? BitIndex { get; } = bitIndex;
/// <summary>
/// Gets the callback to invoke when the value changes.
/// </summary>
public Action<string, object?> OnChange { get; } = onChange;
/// <summary>
/// Disposes the notification subscription.
/// </summary>
public void Dispose()
{
// Fire-and-forget AMS call — caller has already committed to the tear-down.
@@ -388,5 +469,9 @@ 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>
public ITwinCATClient Create() => new AdsTwinCATClient();
}
@@ -14,6 +14,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
public interface ITwinCATClient : IDisposable
{
/// <summary>Establish the AMS connection. Idempotent — subsequent calls are no-ops when already connected.</summary>
/// <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>
Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken);
/// <summary>True when the AMS router + target both accept commands.</summary>
@@ -34,6 +37,10 @@ public interface ITwinCATClient : IDisposable
/// <paramref name="type"/>, or <c>null</c> when the read produced no data; the
/// <c>status</c> tuple member carries the mapped OPC UA status (0 = Good).
/// </summary>
/// <param name="symbolPath">The ADS symbol path.</param>
/// <param name="type">The target data type.</param>
/// <param name="bitIndex">Optional bit index for bit extraction within a word.</param>
/// <param name="cancellationToken">Cancellation token for the read operation.</param>
Task<(object? value, uint status)> ReadValueAsync(
string symbolPath,
TwinCATDataType type,
@@ -44,6 +51,11 @@ public interface ITwinCATClient : IDisposable
/// Write a symbolic value. Returns the mapped OPC UA status for the operation
/// (0 = Good, non-zero = error mapped via <see cref="TwinCATStatusMapper"/>).
/// </summary>
/// <param name="symbolPath">The ADS symbol path.</param>
/// <param name="type">The data type.</param>
/// <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>
Task<uint> WriteValueAsync(
string symbolPath,
TwinCATDataType type,
@@ -55,6 +67,7 @@ public interface ITwinCATClient : IDisposable
/// Cheap health probe — returns <c>true</c> when the target's AMS state is reachable.
/// Used by <see cref="Core.Abstractions.IHostConnectivityProbe"/>'s probe loop.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the probe operation.</param>
Task<bool> ProbeAsync(CancellationToken cancellationToken);
/// <summary>
@@ -88,6 +101,7 @@ public interface ITwinCATClient : IDisposable
/// UDT / function-block typed symbols surface with <c>DataType = null</c> so callers can
/// decide whether to drill in via their own walker.
/// </summary>
/// <param name="cancellationToken">Cancellation token for the enumeration operation.</param>
IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(CancellationToken cancellationToken);
}
@@ -110,5 +124,6 @@ public sealed record TwinCATDiscoveredSymbol(
/// <summary>Factory for <see cref="ITwinCATClient"/>s. One client per device.</summary>
public interface ITwinCATClientFactory
{
/// <summary>Creates a new TwinCAT client instance.</summary>
ITwinCATClient Create();
}
@@ -22,10 +22,14 @@ public sealed record TwinCATAmsAddress(string NetId, int Port)
/// <summary>Default AMS port — TC3 PLC runtime 1.</summary>
public const int DefaultPlcPort = 851;
/// <inheritdoc />
public override string ToString() => Port == DefaultPlcPort
? $"ads://{NetId}"
: $"ads://{NetId}:{Port}";
/// <summary>Parses a TwinCAT AMS address from a string.</summary>
/// <param name="value">The address string to parse.</param>
/// <returns>The parsed AMS address, or null if parsing fails.</returns>
public static TwinCATAmsAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -35,8 +35,12 @@ public enum TwinCATDataType
Structure,
}
/// <summary>Extension methods for TwinCATDataType.</summary>
public static class TwinCATDataTypeExtensions
{
/// <summary>Maps a TwinCAT data type to the equivalent driver data type.</summary>
/// <param name="t">The TwinCAT data type to convert.</param>
/// <returns>The corresponding driver data type.</returns>
public static DriverDataType ToDriverDataType(this TwinCATDataType t) => t switch
{
TwinCATDataType.Bool => DriverDataType.Boolean,
@@ -29,10 +29,18 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
new(StringComparer.OrdinalIgnoreCase);
private DriverHealth _health = new(DriverState.Unknown, null, null);
/// <summary>Occurs when a subscribed tag value changes.</summary>
public event EventHandler<DataChangeEventArgs>? OnDataChange;
/// <summary>Occurs when a device host connectivity status changes.</summary>
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
/// <summary>Occurs when the Galaxy object hierarchy or TwinCAT symbol table is rediscovered.</summary>
public event EventHandler<RediscoveryEventArgs>? OnRediscoveryNeeded;
/// <summary>Initializes a new instance of the <see cref="TwinCATDriver"/> class.</summary>
/// <param name="options">Driver configuration options.</param>
/// <param name="driverInstanceId">Unique driver instance identifier.</param>
/// <param name="clientFactory">Optional ADS client factory; defaults to <see cref="AdsTwinCATClientFactory"/>.</param>
/// <param name="logger">Optional logger; defaults to <see cref="NullLogger{TwinCATDriver}"/>.</param>
public TwinCATDriver(TwinCATDriverOptions options, string driverInstanceId,
ITwinCATClientFactory? clientFactory = null,
ILogger<TwinCATDriver>? logger = null)
@@ -48,9 +56,15 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
}
/// <summary>Gets the unique driver instance identifier.</summary>
public string DriverInstanceId => _driverInstanceId;
/// <summary>Gets the driver type name.</summary>
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>
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
_health = new DriverHealth(DriverState.Initializing, null, null);
@@ -95,12 +109,19 @@ 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>
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>
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
// Native subs first — disposing the handles is cheap + lets the client close its
@@ -133,6 +154,8 @@ 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>
public DriverHealth GetHealth() => _health;
/// <summary>
@@ -150,14 +173,24 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
/// 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>
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>Gets the count of configured devices.</summary>
internal int DeviceCount => _devices.Count;
/// <summary>Gets the device state for the specified host address.</summary>
/// <param name="hostAddress">The ADS host address.</param>
/// <returns>Device state or null if not found.</returns>
internal DeviceState? GetDeviceState(string hostAddress) =>
_devices.TryGetValue(hostAddress, out var s) ? s : null;
// ---- 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>
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
{
@@ -212,6 +245,10 @@ 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>
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
@@ -272,6 +309,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
// ---- ITagDiscovery ----
/// <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>
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
@@ -352,6 +393,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
/// 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>
public async Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken)
{
@@ -402,6 +447,10 @@ 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>
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
{
if (handle is NativeSubscriptionHandle native && _nativeSubs.TryRemove(native.Id, out var sub))
@@ -415,6 +464,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>
public string DiagnosticId => $"twincat-native-sub-{Id}";
}
@@ -424,6 +474,8 @@ 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>
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
@@ -484,6 +536,9 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
/// </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>
public string ResolveHost(string fullReference)
{
if (_tagsByName.TryGetValue(fullReference, out var def))
@@ -573,6 +628,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
/// 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>
/// <summary>Synchronously disposes driver resources without awaiting async operations.</summary>
public void Dispose()
{
// Dispose native subscriptions first — handle disposal is sync.
@@ -602,32 +658,43 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
}
/// <summary>Asynchronously disposes driver resources.</summary>
/// <returns>Completion task.</returns>
public async ValueTask DisposeAsync() => await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);
internal sealed class DeviceState(TwinCATAmsAddress parsedAddress, TwinCATDeviceOptions options)
{
/// <summary>Gets the parsed AMS address for the device.</summary>
public TwinCATAmsAddress ParsedAddress { get; } = parsedAddress;
/// <summary>Gets the device configuration options.</summary>
public TwinCATDeviceOptions Options { get; } = options;
/// <summary>Gets or sets the active ADS client for this device.</summary>
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>
public SemaphoreSlim ConnectGate { get; } = new(1, 1);
/// <summary>Gets the lock object for synchronizing host state transitions.</summary>
public object ProbeLock { get; } = new();
/// <summary>Gets or sets the current host connectivity state.</summary>
public HostState HostState { get; set; } = HostState.Unknown;
/// <summary>Gets or sets the UTC timestamp of the last host state change.</summary>
public DateTime HostStateChangedUtc { get; set; } = DateTime.UtcNow;
/// <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>
public Task? ProbeTask { get; set; }
/// <summary>Disposes the active ADS client if any.</summary>
public void DisposeClient()
{
Client?.Dispose();
Client = null;
}
/// <summary>Disposes the connection gate semaphore.</summary>
public void DisposeGate() => ConnectGate.Dispose();
}
}
@@ -13,12 +13,17 @@ public static class TwinCATDriverFactoryExtensions
{
public const string DriverTypeName = "TwinCAT";
/// <summary>Registers the TwinCAT driver factory with the provided registry.</summary>
/// <param name="registry">The driver factory registry to register with.</param>
public static void Register(DriverFactoryRegistry registry)
{
ArgumentNullException.ThrowIfNull(registry);
registry.Register(DriverTypeName, CreateInstance);
}
/// <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>
internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -32,6 +37,8 @@ public static class TwinCATDriverFactoryExtensions
/// <see cref="TwinCATDriver.InitializeAsync"/> / <see cref="TwinCATDriver.ReinitializeAsync"/>
/// so a config generation pushed via Reinitialize is actually applied (Driver.TwinCAT-001).
/// </summary>
/// <param name="driverConfigJson">The JSON configuration string.</param>
/// <param name="driverInstanceId">The driver instance identifier.</param>
internal static TwinCATDriverOptions ParseOptions(string driverConfigJson, string driverInstanceId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
@@ -70,6 +77,8 @@ public static class TwinCATDriverFactoryExtensions
/// fields like <c>NotificationMaxDelayMs</c> and <c>Structure</c>-tag rejection are
/// honored end-to-end.
/// </summary>
/// <param name="driverConfigJson">The JSON configuration string.</param>
/// <param name="driverInstanceId">The driver instance identifier.</param>
public static TwinCATDriverOptions ParseOptionsForTests(string driverConfigJson, string driverInstanceId)
=> ParseOptions(driverConfigJson, driverInstanceId);
@@ -124,35 +133,67 @@ public static class TwinCATDriverFactoryExtensions
internal sealed class TwinCATDriverConfigDto
{
/// <summary>Gets or sets the timeout in milliseconds.</summary>
public int? TimeoutMs { get; init; }
/// <summary>Gets or sets a value indicating whether to use native notifications.</summary>
public bool? UseNativeNotifications { get; init; }
/// <summary>Gets or sets a value indicating whether to enable controller browsing.</summary>
public bool? EnableControllerBrowse { get; init; }
/// <summary>Gets or sets the maximum notification delay in milliseconds.</summary>
public int? NotificationMaxDelayMs { get; init; }
/// <summary>Gets or sets the list of configured devices.</summary>
public List<TwinCATDeviceDto>? Devices { get; init; }
/// <summary>Gets or sets the list of configured tags.</summary>
public List<TwinCATTagDto>? Tags { get; init; }
/// <summary>Gets or sets the probe configuration.</summary>
public TwinCATProbeDto? Probe { get; init; }
}
internal sealed class TwinCATDeviceDto
{
/// <summary>Gets or sets the host address.</summary>
public string? HostAddress { get; init; }
/// <summary>Gets or sets the device name.</summary>
public string? DeviceName { get; init; }
}
internal sealed class TwinCATTagDto
{
/// <summary>Gets or sets the tag name.</summary>
public string? Name { get; init; }
/// <summary>Gets or sets the device host address.</summary>
public string? DeviceHostAddress { get; init; }
/// <summary>Gets or sets the symbol path.</summary>
public string? SymbolPath { get; init; }
/// <summary>Gets or sets the data type.</summary>
public string? DataType { get; init; }
/// <summary>Gets or sets a value indicating whether the tag is writable.</summary>
public bool? Writable { get; init; }
/// <summary>Gets or sets a value indicating whether writes are idempotent.</summary>
public bool? WriteIdempotent { get; init; }
}
internal sealed class TwinCATProbeDto
{
/// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
public bool? Enabled { get; init; }
/// <summary>Gets or sets the probe interval in milliseconds.</summary>
public int? IntervalMs { get; init; }
/// <summary>Gets or sets the probe timeout in milliseconds.</summary>
public int? TimeoutMs { get; init; }
}
}
@@ -7,9 +7,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
/// </summary>
public sealed class TwinCATDriverOptions
{
/// <summary>Gets the list of TwinCAT devices to connect to.</summary>
public IReadOnlyList<TwinCATDeviceOptions> Devices { get; init; } = [];
/// <summary>Gets the list of TwinCAT tag definitions.</summary>
public IReadOnlyList<TwinCATTagDefinition> Tags { get; init; } = [];
/// <summary>Gets the probe options for TwinCAT connection probing.</summary>
public TwinCATProbeOptions Probe { get; init; } = new();
/// <summary>Gets the default communication timeout.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// <summary>
@@ -64,9 +68,13 @@ public sealed record TwinCATTagDefinition(
bool Writable = true,
bool WriteIdempotent = false);
/// <summary>Probe options for TwinCAT connection monitoring.</summary>
public sealed class TwinCATProbeOptions
{
/// <summary>Gets a value indicating whether probing is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Gets the probe interval.</summary>
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// <summary>Gets the probe timeout.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
}
@@ -40,6 +40,8 @@ public static class TwinCATStatusMapper
public const uint AdsSymbolVersionChanged = 1809u; // DeviceSymbolVersionInvalid = 0x0711
/// <summary>True when <paramref name="adsError"/> is the symbol-version-changed code.</summary>
/// <param name="adsError">The ADS error code to check.</param>
/// <returns>True if the error code indicates symbol version changed.</returns>
public static bool IsSymbolVersionChanged(uint adsError) => adsError == AdsSymbolVersionChanged;
/// <summary>
@@ -47,6 +49,8 @@ public static class TwinCATStatusMapper
/// StatusCode. 0 = success. Device-layer codes (0x07000x073F) cover the operations a
/// driver actually encounters during normal runtime.
/// </summary>
/// <param name="adsError">The ADS error code to map.</param>
/// <returns>The corresponding OPC UA status code.</returns>
public static uint MapAdsError(uint adsError) => adsError switch
{
0 => Good,
@@ -16,6 +16,8 @@ public sealed record TwinCATSymbolPath(
IReadOnlyList<TwinCATSymbolSegment> Segments,
int? BitIndex)
{
/// <summary>Converts this path to an ADS symbol name.</summary>
/// <returns>The ADS symbol name string.</returns>
public string ToAdsSymbolName()
{
var buf = new System.Text.StringBuilder();
@@ -31,6 +33,9 @@ public sealed record TwinCATSymbolPath(
return buf.ToString();
}
/// <summary>Attempts to parse a TwinCAT symbol path from the given string.</summary>
/// <param name="value">The path string to parse.</param>
/// <returns>A TwinCATSymbolPath if parsing succeeds; otherwise null.</returns>
public static TwinCATSymbolPath? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -8,7 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
/// </summary>
public static class TwinCATSystemSymbolFilter
{
/// <summary><c>true</c> when the symbol path matches a known system / infrastructure prefix.</summary>
/// <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>
public static bool IsSystemSymbol(string instancePath)
{
if (string.IsNullOrWhiteSpace(instancePath)) return true;