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
@@ -39,6 +39,10 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
private readonly Lock _subsLock = new();
private long _nextId;
/// <summary>Initializes a new instance of the <see cref="AbCipAlarmProjection"/> class.</summary>
/// <param name="driver">The AB CIP driver instance.</param>
/// <param name="pollInterval">The interval at which to poll for alarm state changes.</param>
/// <param name="logger">Optional logger instance.</param>
public AbCipAlarmProjection(AbCipDriver driver, TimeSpan pollInterval, ILogger? logger = null)
{
_driver = driver;
@@ -46,6 +50,10 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
_logger = logger ?? NullLogger.Instance;
}
/// <summary>Subscribes to alarm events for the specified source nodes.</summary>
/// <param name="sourceNodeIds">The node identifiers to monitor for alarm state changes.</param>
/// <param name="cancellationToken">A cancellation token to stop the operation.</param>
/// <returns>A subscription handle for managing the subscription.</returns>
public async Task<IAlarmSubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
{
@@ -61,6 +69,10 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
return handle;
}
/// <summary>Unsubscribes from alarm events using the provided subscription handle.</summary>
/// <param name="handle">The subscription handle obtained from <see cref="SubscribeAsync"/>.</param>
/// <param name="cancellationToken">A cancellation token to stop the operation.</param>
/// <returns>A task representing the asynchronous unsubscribe operation.</returns>
public async Task UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
{
if (handle is not AbCipAlarmSubscriptionHandle h) return;
@@ -74,6 +86,10 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
sub.Cts.Dispose();
}
/// <summary>Acknowledges one or more active alarms.</summary>
/// <param name="acknowledgements">The list of acknowledgement requests specifying which alarms to acknowledge.</param>
/// <param name="cancellationToken">A cancellation token to stop the operation.</param>
/// <returns>A task representing the asynchronous acknowledgement operation.</returns>
public async Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
{
@@ -91,6 +107,8 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
_ = await _driver.WriteAsync(requests, cancellationToken).ConfigureAwait(false);
}
/// <summary>Releases all resources associated with this alarm projection.</summary>
/// <returns>A task representing the asynchronous disposal operation.</returns>
public async ValueTask DisposeAsync()
{
List<Subscription> snap;
@@ -108,6 +126,8 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
/// in the subscription, diffs each against last-seen state, fires raise/clear events.
/// Extracted so tests can drive one tick without standing up the Task.Run loop.
/// </summary>
/// <param name="sub">The subscription to process.</param>
/// <param name="results">The data values read from the subscription source nodes.</param>
internal void Tick(Subscription sub, IReadOnlyList<DataValueSnapshot> results)
{
// results index layout: for each sourceNode, [InFaulted, Severity] in order.
@@ -176,6 +196,9 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
}
}
/// <summary>Maps a raw severity value to an <see cref="AlarmSeverity"/> enum value.</summary>
/// <param name="raw">The raw severity value from the alarm data.</param>
/// <returns>The corresponding alarm severity level.</returns>
internal static AlarmSeverity MapSeverity(int raw) => raw switch
{
<= 250 => AlarmSeverity.Low,
@@ -203,14 +226,28 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
internal sealed class Subscription
{
/// <summary>Initializes a new instance of the <see cref="Subscription"/> class.</summary>
/// <param name="handle">The subscription handle.</param>
/// <param name="sourceNodeIds">The source node identifiers to monitor.</param>
/// <param name="cts">The cancellation token source for stopping the subscription.</param>
public Subscription(AbCipAlarmSubscriptionHandle handle, IReadOnlyList<string> sourceNodeIds, CancellationTokenSource cts)
{
Handle = handle; SourceNodeIds = sourceNodeIds; Cts = cts;
}
/// <summary>Gets the subscription handle.</summary>
public AbCipAlarmSubscriptionHandle Handle { get; }
/// <summary>Gets the source node identifiers being monitored.</summary>
public IReadOnlyList<string> SourceNodeIds { get; }
/// <summary>Gets the cancellation token source for this subscription.</summary>
public CancellationTokenSource Cts { get; }
/// <summary>Gets or sets the polling loop task.</summary>
public Task Loop { get; set; } = Task.CompletedTask;
/// <summary>Gets the dictionary tracking the last known InFaulted state for each node.</summary>
public Dictionary<string, bool> LastInFaulted { get; } = new(StringComparer.Ordinal);
}
}
@@ -218,6 +255,7 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
/// <summary>Handle returned by <see cref="AbCipAlarmProjection.SubscribeAsync"/>.</summary>
public sealed record AbCipAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle
{
/// <summary>Gets a diagnostic identifier for this subscription.</summary>
public string DiagnosticId => $"abcip-alarm-sub-{Id}";
}
@@ -234,6 +272,8 @@ public static class AbCipAlarmDetector
/// (analog alarms with <c>HHLimit</c>/<c>HLimit</c>/<c>LLimit</c>/<c>LLLimit</c>)
/// ships as a follow-up.
/// </summary>
/// <param name="tag">The tag definition to check for ALMD signature.</param>
/// <returns>True if the tag has the ALMD alarm signature; false otherwise.</returns>
public static bool IsAlmd(AbCipTagDefinition tag)
{
if (tag.DataType != AbCipDataType.Structure || tag.Members is null) return false;
@@ -51,6 +51,8 @@ public static class AbCipDataTypeExtensions
/// <item>USInt / UInt widen into Int32; they can never overflow it.</item>
/// </list>
/// </summary>
/// <param name="t">The Logix data type to convert.</param>
/// <returns>The corresponding driver data type.</returns>
public static DriverDataType ToDriverDataType(this AbCipDataType t) => t switch
{
AbCipDataType.Bool => DriverDataType.Boolean,
@@ -40,13 +40,26 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
private AbCipAlarmProjection _alarmProjection;
private DriverHealth _health = new(DriverState.Unknown, null, null);
/// <summary>Occurs when a subscribed tag's value changes.</summary>
public event EventHandler<DataChangeEventArgs>? OnDataChange;
/// <summary>Occurs when a device's host connectivity status changes.</summary>
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
/// <summary>Occurs when an alarm event is raised.</summary>
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
/// <summary>Internal seam for the alarm projection to raise events through the driver.</summary>
/// <param name="args">The alarm event arguments.</param>
internal void InvokeAlarmEvent(AlarmEventArgs args) => OnAlarmEvent?.Invoke(this, args);
/// <summary>Initializes a new instance of the <see cref="AbCipDriver"/> class.</summary>
/// <param name="options">The driver configuration options.</param>
/// <param name="driverInstanceId">A unique identifier for this driver instance.</param>
/// <param name="tagFactory">Optional factory for creating tag runtimes; uses libplctag default if null.</param>
/// <param name="enumeratorFactory">Optional factory for enumerating tags; uses libplctag default if null.</param>
/// <param name="templateReaderFactory">Optional factory for reading UDT templates; uses libplctag default if null.</param>
/// <param name="logger">Optional logger; uses null logger if not provided.</param>
public AbCipDriver(AbCipDriverOptions options, string driverInstanceId,
IAbCipTagFactory? tagFactory = null,
IAbCipTagEnumeratorFactory? enumeratorFactory = null,
@@ -74,6 +87,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// additional network traffic. <c>null</c> on template-not-found / decode failure so
/// callers can fall back to declaration-driven UDT fan-out.
/// </summary>
/// <param name="deviceHostAddress">The host address of the device to read the template from.</param>
/// <param name="templateInstanceId">The instance ID of the UDT template to fetch.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The UDT shape if found and decoded successfully; null otherwise.</returns>
internal async Task<AbCipUdtShape?> FetchUdtShapeAsync(
string deviceHostAddress, uint templateInstanceId, CancellationToken cancellationToken)
{
@@ -113,7 +130,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// </summary>
internal AbCipTemplateCache TemplateCache => _templateCache;
/// <summary>Gets the unique identifier for this driver instance.</summary>
public string DriverInstanceId => _driverInstanceId;
/// <summary>Gets the driver type identifier.</summary>
public string DriverType => "AbCip";
/// <summary>
@@ -127,6 +147,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// unit tests — keep those options. The driver's address-space + runtime state is then
/// built from the effective <see cref="_options"/>.
/// </summary>
/// <param name="driverConfigJson">The driver configuration as JSON; empty or "{}" means no override.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous initialization.</returns>
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
_health = new DriverHealth(DriverState.Initializing, null, null);
@@ -221,6 +244,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return Task.CompletedTask;
}
/// <summary>Reinitialize the driver by shutting down and reinitializing with new configuration.</summary>
/// <param name="driverConfigJson">The new driver configuration as JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous reinitialization.</returns>
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
@@ -235,6 +262,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// dictionary (Driver.AbCip-008). Idempotent — safe to call twice (e.g. ShutdownAsync
/// from ReinitializeAsync followed by DisposeAsync).
/// </summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous shutdown.</returns>
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
await _alarmProjection.DisposeAsync().ConfigureAwait(false);
@@ -276,10 +305,19 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ISubscribable (polling overlay via shared engine) ----
/// <summary>Subscribe to value changes for the specified tag references.</summary>
/// <param name="fullReferences">The tag references to subscribe to.</param>
/// <param name="publishingInterval">The interval at which to publish changes.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A handle representing the subscription.</returns>
public Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval));
/// <summary>Unsubscribe from value changes using a subscription handle.</summary>
/// <param name="handle">The subscription handle to unsubscribe.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
{
_poll.Unsubscribe(handle);
@@ -297,6 +335,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// <c>false</c> (the default), returns a handle wrapping a no-op subscription so
/// capability negotiation still works; <see cref="OnAlarmEvent"/> never fires.
/// </summary>
/// <param name="sourceNodeIds">The node IDs of alarm sources to subscribe to.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A handle representing the alarm subscription.</returns>
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
{
@@ -308,11 +349,19 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken);
}
/// <summary>Unsubscribe from alarm events.</summary>
/// <param name="handle">The alarm subscription handle.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
_options.EnableAlarmProjection
? _alarmProjection.UnsubscribeAsync(handle, cancellationToken)
: Task.CompletedTask;
/// <summary>Acknowledge alarms.</summary>
/// <param name="acknowledgements">The alarm acknowledgements to process.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
_options.EnableAlarmProjection
@@ -321,6 +370,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IHostConnectivityProbe ----
/// <summary>Gets the connectivity status of all configured devices.</summary>
/// <returns>A read-only list of host connectivity statuses.</returns>
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
@@ -395,6 +446,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// first configured device's host address rather than throwing — the invoker handles the
/// mislookup at the capability level when the actual read returns BadNodeIdUnknown.
/// </summary>
/// <param name="fullReference">The full tag reference to resolve.</param>
/// <returns>The device host address for the tag.</returns>
public string ResolveHost(string fullReference)
{
if (_tagsByName.TryGetValue(fullReference, out var def))
@@ -411,6 +464,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// <c>BadCommunicationError</c>. The driver health surface is updated per-call so the
/// Admin UI sees a tight feedback loop between read failures + the driver's state.
/// </summary>
/// <param name="fullReferences">The tag references to read.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A read-only list of data value snapshots.</returns>
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
{
@@ -584,6 +640,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// Non-writable configurations surface as <c>BadNotWritable</c>; type-conversion failures
/// as <c>BadTypeMismatch</c>; transport errors as <c>BadCommunicationError</c>.
/// </summary>
/// <param name="writes">The write requests to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A read-only list of write results.</returns>
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
@@ -814,6 +873,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
}
/// <summary>Gets the current health status of the driver.</summary>
/// <returns>The driver health information.</returns>
public DriverHealth GetHealth() => _health;
/// <summary>
@@ -821,8 +882,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// GC. driver-specs.md §3 flags this: operators must watch whole-process RSS for the
/// full picture, and <see cref="ReinitializeAsync"/> is the Tier-B remediation.
/// </summary>
/// <returns>The memory footprint in bytes.</returns>
public long GetMemoryFootprint() => 0;
/// <summary>Flushes optional caches to free memory.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken)
{
_templateCache.Clear();
@@ -838,6 +903,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// controller-discovered tags under a <c>Discovered/</c> sub-folder. System / module /
/// routine / task tags are hidden via <see cref="AbCipSystemTagFilter"/>.
/// </summary>
/// <param name="builder">The address space builder to populate with discovered tags.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous discovery.</returns>
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
@@ -934,11 +1002,16 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
internal int DeviceCount => _devices.Count;
/// <summary>Looked-up device state for the given host address. Tests + later-PR capabilities hit this.</summary>
/// <param name="hostAddress">The host address of the device to look up.</param>
/// <returns>The device state if found; null otherwise.</returns>
internal DeviceState? GetDeviceState(string hostAddress) =>
_devices.TryGetValue(hostAddress, out var s) ? s : null;
/// <summary>Releases all resources used by the driver.</summary>
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
/// <summary>Asynchronously releases all resources used by the driver.</summary>
/// <returns>A value task representing the asynchronous disposal.</returns>
public async ValueTask DisposeAsync()
{
await ShutdownAsync(CancellationToken.None).ConfigureAwait(false);
@@ -956,14 +1029,22 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
AbCipDeviceOptions options,
AbCipPlcFamilyProfile profile)
{
/// <summary>Gets the parsed host address for this device.</summary>
public AbCipHostAddress ParsedAddress { get; } = parsedAddress;
/// <summary>Gets the configuration options for this device.</summary>
public AbCipDeviceOptions Options { get; } = options;
/// <summary>Gets the PLC family profile for this device.</summary>
public AbCipPlcFamilyProfile Profile { get; } = profile;
/// <summary>Gets the lock object used for probe synchronization.</summary>
public object ProbeLock { get; } = new();
/// <summary>Gets or sets the current host state of this device.</summary>
public HostState HostState { get; set; } = HostState.Unknown;
/// <summary>Gets or sets the UTC timestamp when the host state was last changed.</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>Gets or sets whether the probe has been initialized.</summary>
public bool ProbeInitialized { get; set; }
/// <summary>
@@ -996,6 +1077,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, SemaphoreSlim> _rmwLocks = new();
/// <summary>Gets or creates a semaphore for coordinating RMW (read-modify-write) operations on a parent tag.</summary>
/// <param name="parentTagName">The name of the parent tag.</param>
/// <returns>A semaphore for coordinating RMW operations.</returns>
public SemaphoreSlim GetRmwLock(string parentTagName) =>
_rmwLocks.GetOrAdd(parentTagName, _ => new SemaphoreSlim(1, 1));
@@ -1006,6 +1090,9 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// <see cref="AbCipDeviceOptions.ConnectionSize"/>) with the family profile defaults
/// so the wire layer sees one place that resolves both.
/// </summary>
/// <param name="tagName">The name of the tag to create parameters for.</param>
/// <param name="timeout">The timeout for tag operations.</param>
/// <returns>The computed tag creation parameters.</returns>
public AbCipTagCreateParams BuildCreateParams(string tagName, TimeSpan timeout) => new(
Gateway: ParsedAddress.Gateway,
Port: ParsedAddress.Port,
@@ -1016,6 +1103,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
AllowPacking: Options.AllowPacking ?? Profile.SupportsRequestPacking,
ConnectionSize: Options.ConnectionSize ?? Profile.DefaultConnectionSize);
/// <summary>Disposes all runtime tag handles and clears the caches.</summary>
public void DisposeHandles()
{
foreach (var r in Runtimes.Values) r.Dispose();
@@ -14,12 +14,22 @@ public static class AbCipDriverFactoryExtensions
{
public const string DriverTypeName = "AbCip";
/// <summary>
/// Registers the AB CIP driver factory with the driver 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 an instance of the AB CIP driver from configuration.
/// </summary>
/// <param name="driverInstanceId">The unique identifier for this driver instance.</param>
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
/// <returns>A configured <see cref="AbCipDriver"/> instance.</returns>
internal static AbCipDriver CreateInstance(string driverInstanceId, string driverConfigJson)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -34,6 +44,9 @@ public static class AbCipDriverFactoryExtensions
/// so a reinitialize with a changed config JSON (new device, new tag, changed timeout)
/// actually takes effect rather than being silently discarded.
/// </summary>
/// <param name="driverInstanceId">The unique identifier for this driver instance.</param>
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
/// <returns>Parsed driver options.</returns>
internal static AbCipDriverOptions ParseOptions(string driverInstanceId, string driverConfigJson)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -120,50 +133,161 @@ public static class AbCipDriverFactoryExtensions
internal sealed class AbCipDriverConfigDto
{
/// <summary>
/// Gets or sets the timeout in milliseconds for operations.
/// </summary>
public int? TimeoutMs { get; init; }
/// <summary>
/// Gets or sets whether controller browsing is enabled.
/// </summary>
public bool? EnableControllerBrowse { get; init; }
/// <summary>
/// Gets or sets whether alarm projection is enabled.
/// </summary>
public bool? EnableAlarmProjection { get; init; }
/// <summary>
/// Gets or sets whether declaration-only UDT grouping is enabled.
/// </summary>
public bool? EnableDeclarationOnlyUdtGrouping { get; init; }
/// <summary>
/// Gets or sets the alarm poll interval in milliseconds.
/// </summary>
public int? AlarmPollIntervalMs { get; init; }
/// <summary>
/// Gets or sets the list of devices to connect to.
/// </summary>
public List<AbCipDeviceDto>? Devices { get; init; }
/// <summary>
/// Gets or sets the list of tags to monitor.
/// </summary>
public List<AbCipTagDto>? Tags { get; init; }
/// <summary>
/// Gets or sets the probe configuration.
/// </summary>
public AbCipProbeDto? Probe { get; init; }
}
internal sealed class AbCipDeviceDto
{
/// <summary>
/// Gets or sets the host address of the device.
/// </summary>
public string? HostAddress { get; init; }
/// <summary>
/// Gets or sets the PLC family.
/// </summary>
public string? PlcFamily { get; init; }
/// <summary>
/// Gets or sets the device name.
/// </summary>
public string? DeviceName { get; init; }
/// <summary>
/// Gets or sets whether packing is allowed.
/// </summary>
public bool? AllowPacking { get; init; }
/// <summary>
/// Gets or sets the connection size.
/// </summary>
public int? ConnectionSize { get; init; }
}
internal sealed class AbCipTagDto
{
/// <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 tag path.
/// </summary>
public string? TagPath { get; init; }
/// <summary>
/// Gets or sets the data type.
/// </summary>
public string? DataType { get; init; }
/// <summary>
/// Gets or sets whether the tag is writable.
/// </summary>
public bool? Writable { get; init; }
/// <summary>
/// Gets or sets whether write is idempotent.
/// </summary>
public bool? WriteIdempotent { get; init; }
/// <summary>
/// Gets or sets the list of structure members.
/// </summary>
public List<AbCipMemberDto>? Members { get; init; }
/// <summary>
/// Gets or sets whether this is a safety tag.
/// </summary>
public bool? SafetyTag { get; init; }
}
internal sealed class AbCipMemberDto
{
/// <summary>
/// Gets or sets the member name.
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Gets or sets the data type.
/// </summary>
public string? DataType { get; init; }
/// <summary>
/// Gets or sets whether the member is writable.
/// </summary>
public bool? Writable { get; init; }
/// <summary>
/// Gets or sets whether write is idempotent.
/// </summary>
public bool? WriteIdempotent { get; init; }
}
internal sealed class AbCipProbeDto
{
/// <summary>
/// Gets or sets 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; }
/// <summary>
/// Gets or sets the probe tag path.
/// </summary>
public string? ProbeTagPath { get; init; }
}
}
@@ -163,8 +163,11 @@ public enum AbCipPlcFamily
/// </summary>
public sealed class AbCipProbeOptions
{
/// <summary>Gets a value indicating whether the probe is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Gets the interval at which the probe reads the probe tag.</summary>
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// <summary>Gets the timeout for each probe read operation.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
/// <summary>
@@ -25,15 +25,17 @@ public sealed record AbCipHostAddress(string Gateway, int Port, string CipPath)
/// <summary>Default EtherNet/IP TCP port — spec-reserved.</summary>
public const int DefaultEipPort = 44818;
/// <summary>Recompose the canonical <c>ab://...</c> form.</summary>
/// <inheritdoc />
public override string ToString() => Port == DefaultEipPort
? $"ab://{Gateway}/{CipPath}"
: $"ab://{Gateway}:{Port}/{CipPath}";
/// <summary>
/// Parse <paramref name="value"/>. Returns <c>null</c> on any malformed input — callers
/// Parses an ab:// host address string. Returns <c>null</c> on any malformed input — callers
/// should treat a null return as a config-validation failure rather than catching.
/// </summary>
/// <param name="value">The ab:// URL string to parse.</param>
/// <returns>A parsed host address, or null if the value is invalid.</returns>
public static AbCipHostAddress? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -47,6 +47,7 @@ public static class AbCipStatusMapper
public const uint BadTypeMismatch = 0x80730000u;
/// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary>
/// <param name="status">The CIP general-status byte value.</param>
public static uint MapCipGeneralStatus(byte status) => status switch
{
0x00 => Good,
@@ -70,6 +71,7 @@ public static class AbCipStatusMapper
/// <see cref="Status.Ok"/> is success; <see cref="Status.Pending"/> is an in-flight
/// operation; every other (negative) member is an error.
/// </summary>
/// <param name="status">The libplctag status code as an integer.</param>
public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
/// <summary>
@@ -77,6 +79,7 @@ public static class AbCipStatusMapper
/// the strongly-typed core of the mapper; the <c>int</c> overload exists only for the
/// <see cref="IAbCipTagRuntime.GetStatus"/> seam, which returns the boxed-as-int value.
/// </summary>
/// <param name="status">The libplctag Status enum value.</param>
public static uint MapLibplctagStatus(Status status) => status switch
{
Status.Ok => Good,
@@ -18,6 +18,7 @@ public static class AbCipSystemTagFilter
/// should hide from the default address space. Case-sensitive — Logix symbols are
/// always preserved case and the system-tag prefixes are uppercase by convention.
/// </summary>
/// <param name="tagName">The tag name to check.</param>
public static bool IsSystemTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName)) return true;
@@ -47,6 +47,8 @@ public sealed record AbCipTagPath(
/// doesn't support — the driver surfaces that as a config-validation error rather than
/// attempting a best-effort translation.
/// </summary>
/// <param name="value">The tag path string to parse, or null.</param>
/// <returns>The parsed AbCipTagPath, or null if the syntax is invalid.</returns>
public static AbCipTagPath? TryParse(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
@@ -21,10 +21,15 @@ public sealed class AbCipTemplateCache
/// <summary>
/// Retrieve a cached UDT shape, or <c>null</c> if not yet read.
/// </summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) =>
_shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null;
/// <summary>Store a freshly-decoded UDT shape.</summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <param name="shape">The UDT shape to cache.</param>
public void Put(string deviceHostAddress, uint templateInstanceId, AbCipUdtShape shape) =>
_shapes[(deviceHostAddress, templateInstanceId)] = shape;
@@ -29,6 +29,7 @@ public static class AbCipUdtMemberLayout
/// Try to compute member offsets for the supplied declared members. Returns <c>null</c>
/// if any member type is unsupported for declaration-only layout.
/// </summary>
/// <param name="members">The list of UDT member declarations.</param>
public static IReadOnlyDictionary<string, int>? TryBuild(
IReadOnlyList<AbCipStructureMember> members)
{
@@ -25,6 +25,9 @@ public static class AbCipUdtReadPlanner
/// formed — every reference goes to the per-tag fallback path so member decoding never
/// relies on declaration-order offsets that may not match the controller layout.
/// </summary>
/// <param name="requests">The list of tag references to read.</param>
/// <param name="tagsByName">Dictionary mapping tag names to their definitions.</param>
/// <param name="enableDeclarationOnlyGrouping">Whether to enable UDT member grouping based on declaration order.</param>
public static AbCipUdtReadPlan Build(
IReadOnlyList<string> requests,
IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName,
@@ -44,6 +44,8 @@ public static class CipSymbolObjectDecoder
/// the tail cause decoding to stop gracefully — the caller gets whatever it could parse
/// cleanly before the corruption.
/// </summary>
/// <param name="buffer">The raw byte buffer from the Symbol Object response.</param>
/// <returns>An enumerable sequence of discovered CIP tags.</returns>
public static IEnumerable<AbCipDiscoveredTag> Decode(byte[] buffer)
{
ArgumentNullException.ThrowIfNull(buffer);
@@ -91,6 +93,8 @@ public static class CipSymbolObjectDecoder
/// Split a <c>Program:MainProgram.StepIndex</c>-style name into its scope + local
/// parts. Names without the <c>Program:</c> prefix pass through unchanged.
/// </summary>
/// <param name="fullName">The full tag name possibly prefixed with a program scope.</param>
/// <returns>A tuple containing the program scope (or null) and the simple name.</returns>
internal static (string? programScope, string simpleName) SplitProgramScope(string fullName)
{
const string prefix = "Program:";
@@ -107,6 +111,8 @@ public static class CipSymbolObjectDecoder
/// caller treats those as <see cref="AbCipDataType.Structure"/> so the symbol is still
/// surfaced + downstream config can add a concrete type override.
/// </summary>
/// <param name="typeCode">The CIP type code to map.</param>
/// <returns>The corresponding AbCipDataType, or null if unrecognized.</returns>
internal static AbCipDataType? MapTypeCode(byte typeCode) => typeCode switch
{
0xC1 => AbCipDataType.Bool,
@@ -46,6 +46,8 @@ public static class CipTemplateObjectDecoder
/// Decode the raw Template Object blob. Returns <c>null</c> when the header indicates
/// zero members or the buffer is too short to hold the fixed header.
/// </summary>
/// <param name="buffer">The raw Template Object buffer to decode.</param>
/// <returns>An AbCipUdtShape describing the structure, or null if decoding fails.</returns>
public static AbCipUdtShape? Decode(byte[] buffer)
{
ArgumentNullException.ThrowIfNull(buffer);
@@ -105,6 +107,8 @@ public static class CipTemplateObjectDecoder
/// the null byte after each semicolon is optional padding per Rockwell's string
/// encoding convention. Stops at a trailing null / end of buffer.
/// </summary>
/// <param name="span">The byte span containing semicolon-terminated strings.</param>
/// <returns>List of parsed strings, one per name in the span.</returns>
internal static List<string> ParseSemicolonTerminatedStrings(ReadOnlySpan<byte> span)
{
var result = new List<string>();
@@ -13,6 +13,8 @@ public interface IAbCipTagEnumerator : IDisposable
/// Enumerate the controller's tags for one device. Callers iterate asynchronously so
/// large symbol tables don't require buffering the entire list.
/// </summary>
/// <param name="deviceParams">Parameters for creating device tags.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
CancellationToken cancellationToken);
@@ -21,6 +23,9 @@ public interface IAbCipTagEnumerator : IDisposable
/// <summary>Factory for per-driver enumerators.</summary>
public interface IAbCipTagEnumeratorFactory
{
/// <summary>
/// Creates a new tag enumerator instance.
/// </summary>
IAbCipTagEnumerator Create();
}
@@ -49,6 +54,11 @@ public sealed record AbCipDiscoveredTag(
/// </summary>
internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator
{
/// <summary>
/// Enumerates an empty sequence of tags.
/// </summary>
/// <param name="deviceParams">Parameters for creating device tags.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -57,11 +67,17 @@ internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator
yield break;
}
/// <summary>
/// Releases resources (no-op for this implementation).
/// </summary>
public void Dispose() { }
}
/// <summary>Factory for <see cref="EmptyAbCipTagEnumerator"/>.</summary>
internal sealed class EmptyAbCipTagEnumeratorFactory : IAbCipTagEnumeratorFactory
{
/// <summary>
/// Creates a new empty tag enumerator.
/// </summary>
public IAbCipTagEnumerator Create() => new EmptyAbCipTagEnumerator();
}
@@ -10,12 +10,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
public interface IAbCipTagRuntime : IDisposable
{
/// <summary>Create the underlying native tag (equivalent to libplctag's <c>plc_tag_create</c>).</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task InitializeAsync(CancellationToken cancellationToken);
/// <summary>Issue a read; on completion the local buffer holds the current PLC value.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task ReadAsync(CancellationToken cancellationToken);
/// <summary>Flush the local buffer to the PLC.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task WriteAsync(CancellationToken cancellationToken);
/// <summary>
@@ -29,6 +32,8 @@ public interface IAbCipTagRuntime : IDisposable
/// <paramref name="bitIndex"/> is non-null only for BOOL-within-DINT tags captured in
/// the <c>.N</c> syntax at parse time.
/// </summary>
/// <param name="type">CIP data type to decode.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param>
object? DecodeValue(AbCipDataType type, int? bitIndex);
/// <summary>
@@ -40,12 +45,18 @@ public interface IAbCipTagRuntime : IDisposable
/// offsets greater than zero against an unsupporting runtime should return <c>null</c>
/// so the planner can skip grouping.
/// </summary>
/// <param name="type">CIP data type to decode.</param>
/// <param name="offset">Byte offset in the buffer.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param>
object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex);
/// <summary>
/// Encode <paramref name="value"/> into the local buffer per the tag's type. Callers
/// pair this with <see cref="WriteAsync"/>.
/// </summary>
/// <param name="type">CIP data type to encode.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT insertion, or null.</param>
/// <param name="value">Value to encode.</param>
void EncodeValue(AbCipDataType type, int? bitIndex, object? value);
}
@@ -55,6 +66,8 @@ public interface IAbCipTagRuntime : IDisposable
/// </summary>
public interface IAbCipTagFactory
{
/// <summary>Creates a tag runtime handle from the specified creation parameters.</summary>
/// <param name="createParams">Parameters needed to create the tag runtime.</param>
IAbCipTagRuntime Create(AbCipTagCreateParams createParams);
}
@@ -13,6 +13,9 @@ public interface IAbCipTemplateReader : IDisposable
/// full blob the Read Template service produced — the managed <see cref="CipTemplateObjectDecoder"/>
/// parses it into an <see cref="AbCipUdtShape"/>.
/// </summary>
/// <param name="deviceParams">The device connection parameters.</param>
/// <param name="templateInstanceId">The template instance ID to read.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task<byte[]> ReadAsync(
AbCipTagCreateParams deviceParams,
uint templateInstanceId,
@@ -22,5 +25,6 @@ public interface IAbCipTemplateReader : IDisposable
/// <summary>Factory for <see cref="IAbCipTemplateReader"/>.</summary>
public interface IAbCipTemplateReaderFactory
{
/// <summary>Creates a new template reader instance.</summary>
IAbCipTemplateReader Create();
}
@@ -21,6 +21,10 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
{
private Tag? _tag;
/// <summary>Enumerates all tags in the controller symbol table.</summary>
/// <param name="deviceParams">Device connection parameters including gateway and path.</param>
/// <param name="cancellationToken">Cancellation token for the enumeration.</param>
/// <returns>An async enumerable of discovered tags.</returns>
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
[EnumeratorCancellation] CancellationToken cancellationToken)
@@ -45,6 +49,7 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
yield return tag;
}
/// <summary>Disposes the enumerator and releases the underlying libplctag tag.</summary>
public void Dispose() => _tag?.Dispose();
private static PlcType MapPlcType(string attribute) => attribute switch
@@ -59,5 +64,7 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
/// <summary>Factory for <see cref="LibplctagTagEnumerator"/>.</summary>
internal sealed class LibplctagTagEnumeratorFactory : IAbCipTagEnumeratorFactory
{
/// <summary>Creates a new libplctag-based tag enumerator.</summary>
/// <returns>A new tag enumerator instance.</returns>
public IAbCipTagEnumerator Create() => new LibplctagTagEnumerator();
}
@@ -13,6 +13,8 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
{
private readonly Tag _tag;
/// <summary>Initializes a new instance of the LibplctagTagRuntime class.</summary>
/// <param name="p">The tag creation parameters.</param>
public LibplctagTagRuntime(AbCipTagCreateParams p)
{
_tag = new Tag
@@ -34,14 +36,36 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
// negotiates with — Driver.AbCip-013.
}
/// <summary>Initializes the tag asynchronously.</summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous initialization.</returns>
public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken);
/// <summary>Reads the tag value asynchronously.</summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous read operation.</returns>
public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken);
/// <summary>Writes the tag value asynchronously.</summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous write operation.</returns>
public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken);
/// <summary>Gets the current status of the tag.</summary>
/// <returns>The tag status as an integer.</returns>
public int GetStatus() => (int)_tag.GetStatus();
/// <summary>Decodes the tag value with the specified data type.</summary>
/// <param name="type">The data type to decode.</param>
/// <param name="bitIndex">The bit index for bit-level access, if applicable.</param>
/// <returns>The decoded value.</returns>
public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex);
/// <summary>Decodes the tag value at the specified offset with the specified data type.</summary>
/// <param name="type">The data type to decode.</param>
/// <param name="offset">The byte offset within the tag buffer.</param>
/// <param name="bitIndex">The bit index for bit-level access, if applicable.</param>
/// <returns>The decoded value.</returns>
public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch
{
AbCipDataType.Bool => bitIndex is int bit
@@ -63,6 +87,10 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
_ => null,
};
/// <summary>Encodes the specified value to the tag with the specified data type.</summary>
/// <param name="type">The data type to encode.</param>
/// <param name="bitIndex">The bit index for bit-level access, if applicable.</param>
/// <param name="value">The value to encode.</param>
public void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
{
switch (type)
@@ -126,6 +154,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
}
}
/// <summary>Disposes the tag and releases its resources.</summary>
public void Dispose() => _tag.Dispose();
private static PlcType MapPlcType(string attribute) => attribute switch
@@ -145,8 +174,12 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
/// Default <see cref="IAbCipTagFactory"/> — creates a fresh <see cref="LibplctagTagRuntime"/>
/// per call. Stateless; safe to share across devices.
/// </summary>
/// <summary>Default implementation of IAbCipTagFactory that creates LibplctagTagRuntime instances.</summary>
internal sealed class LibplctagTagFactory : IAbCipTagFactory
{
/// <summary>Creates a new tag runtime with the specified creation parameters.</summary>
/// <param name="createParams">The parameters for creating the tag.</param>
/// <returns>A new IAbCipTagRuntime instance.</returns>
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) =>
new LibplctagTagRuntime(createParams);
}
@@ -25,6 +25,11 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
{
private Tag? _tag;
/// <summary>Reads a template object from the PLC asynchronously.</summary>
/// <param name="deviceParams">The device connection parameters.</param>
/// <param name="templateInstanceId">The template instance ID to read.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous read operation.</returns>
public async Task<byte[]> ReadAsync(
AbCipTagCreateParams deviceParams,
uint templateInstanceId,
@@ -45,6 +50,7 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
return _tag.GetBuffer();
}
/// <inheritdoc />
public void Dispose() => _tag?.Dispose();
private static PlcType MapPlcType(string attribute) => attribute switch
@@ -58,5 +64,7 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
internal sealed class LibplctagTemplateReaderFactory : IAbCipTemplateReaderFactory
{
/// <summary>Creates a new instance of the libplctag template reader.</summary>
/// <returns>A new instance of <see cref="LibplctagTemplateReader"/>.</returns>
public IAbCipTemplateReader Create() => new LibplctagTemplateReader();
}
@@ -20,6 +20,7 @@ public sealed record AbCipPlcFamilyProfile(
int MaxFragmentBytes)
{
/// <summary>Look up the profile for a configured family.</summary>
/// <param name="family">The PLC family to look up the profile for.</param>
public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch
{
AbCipPlcFamily.ControlLogix => ControlLogix,