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