docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes
project bookkeeping IDs (task/tracking refs) from shipped code comments,
so the docs read cleanly and CommentChecker is quiet except for known
false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc).
Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
Joseph Doherty
2026-07-07 12:38:39 -04:00
parent 384dbd7d36
commit 9cad9ed0fc
375 changed files with 1899 additions and 2493 deletions
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Task #177 — projects AB Logix ALMD alarm instructions onto the OPC UA alarm surface by
/// Projects AB Logix ALMD alarm instructions onto the OPC UA alarm surface by
/// polling the ALMD UDT's <c>InFaulted</c> / <c>Acked</c> / <c>Severity</c> members at a
/// configurable interval + translating state transitions into <c>OnAlarmEvent</c>
/// callbacks on the owning <see cref="AbCipDriver"/>. Feature-flagged off by default via
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// the operator acked" shape maps cleanly onto the driver-agnostic
/// <see cref="IAlarmSource"/> contract without concessions.</para>
///
/// <para>Polling reuses <see cref="AbCipDriver.ReadAsync"/>, so ALMD reads get the #194
/// <para>Polling reuses <see cref="AbCipDriver.ReadAsync"/>, so ALMD reads get the
/// whole-UDT optimization for free when the ALMD is declared with its standard members.
/// One poll loop per subscription call; the loop batches every
/// member read across the full source-node set into a single ReadAsync per tick.</para>
@@ -185,7 +185,7 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable
// ToInt(null) returns 0 and MapSeverity buckets it as Low. This is acceptable because
// InFaulted and Severity are members of the same ALMD UDT read in one batch, so a Good
// InFaulted almost always implies a Good Severity. The "unknown severity → Low" fallback
// is intentional and matches the behaviour documented on Driver.AbCip-017.
// is intentional and matches the documented behaviour.
var severity = ToInt(severityDv.Value);
var wasFaulted = sub.LastInFaulted.GetValueOrDefault(nodeId, false);
@@ -302,7 +302,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>
/// <inheritdoc />
public string DiagnosticId => $"abcip-alarm-sub-{Id}";
}
@@ -12,7 +12,7 @@ public static class AbCipDataTypeExtensions
/// <list type="bullet">
/// <item>LInt (signed 64-bit) → Int64; ULInt (unsigned 64-bit) → UInt64.</item>
/// <item>UDInt (unsigned 32-bit) → UInt32 so values above Int32.MaxValue are not
/// wrapped to negative (Driver.AbCip-004).</item>
/// wrapped to negative.</item>
/// <item>USInt / UInt widen into Int32; they can never overflow it.</item>
/// </list>
/// </summary>
@@ -11,12 +11,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// capabilities via the libplctag.NET wrapper.
/// </summary>
/// <remarks>
/// <para>Wire layer is libplctag 1.6.x (plan decision #11). Per-device host addresses use
/// <para>Wire layer is libplctag 1.6.x. Per-device host addresses use
/// the <c>ab://gateway[:port]/cip-path</c> canonical form parsed via
/// <see cref="AbCipHostAddress.TryParse"/>; those strings become the <c>hostName</c> key
/// for Polly bulkhead + circuit-breaker isolation per plan decision #144.</para>
/// for Polly bulkhead + circuit-breaker isolation.</para>
///
/// <para>Tier A per plan decisions #143145 — in-process, shares server lifetime, no
/// <para>Tier A — in-process, shares server lifetime, no
/// sidecar. <see cref="ReinitializeAsync"/> is the Tier-B escape hatch for recovering
/// from native-heap growth that the CLR allocator can't see; it tears down the
/// libplctag.NET <c>Tag</c> instances held in <c>DeviceState.Runtimes</c> and reconnects
@@ -214,26 +214,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// </summary>
internal AbCipTemplateCache TemplateCache => _templateCache;
/// <summary>Gets the unique identifier for this driver instance.</summary>
/// <inheritdoc />
public string DriverInstanceId => _driverInstanceId;
/// <summary>Gets the driver type identifier.</summary>
/// <inheritdoc />
public string DriverType => "AbCip";
/// <summary>
/// Initialize the driver from its <c>DriverConfig</c> JSON. When
/// <paramref name="driverConfigJson"/> carries a real configuration (any device or tag),
/// it is parsed via <see cref="AbCipDriverFactoryExtensions.ParseOptions"/> and the
/// parsed options REPLACE the construction-time options — this is what makes
/// <see cref="ReinitializeAsync"/> pick up a changed config (new device, new tag,
/// changed timeout). A blank or empty-object JSON (<c>"{}"</c>) is treated as "no
/// override" so callers that constructed the driver with explicit options — chiefly
/// 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>
/// <inheritdoc />
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
_health = new DriverHealth(DriverState.Initializing, null, null);
@@ -261,7 +248,6 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
{
// Duplicate-key check: a collision means two configured tags have the same name.
// Fail fast at init time with a diagnostic rather than silently clobbering.
// (Driver.AbCip-005)
if (_tagsByName.TryGetValue(tag.Name, out var existingTag))
throw new InvalidOperationException(
$"AbCip tag name collision: '{tag.Name}' is declared more than once. " +
@@ -280,7 +266,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
DataType: member.DataType,
Writable: member.Writable,
WriteIdempotent: member.WriteIdempotent,
// Driver.AbCip-016 — carry the member's array shape into the fanned-out
// Carry the member's array shape into the fanned-out
// runtime definition. Discovery already emits an array node for an array
// member (member.IsArray || member.ElementCount > 1); without these the
// runtime def defaulted to scalar and the read returned a single element
@@ -308,13 +294,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
state.ProbeCts = new CancellationTokenSource();
var ct = state.ProbeCts.Token;
// Keep the loop Task so ShutdownAsync can await its clean exit before
// disposing the CTS / handles the loop is still using (Driver.AbCip-008).
// disposing the CTS / handles the loop is still using.
state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct);
}
}
else if (_options.Probe.Enabled && _devices.Count > 0)
{
// Driver.AbCip-011: probe is Enabled but no ProbeTagPath is configured. Without a
// Probe is Enabled but no ProbeTagPath is configured. Without a
// tag path the loop has nothing to read, so HostState would stay Unknown forever
// and GetHostStatuses() would report every device as Unknown with no warning.
// Log a warning so the misconfiguration is visible in the rolling Serilog file.
@@ -335,26 +321,14 @@ 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>
/// <inheritdoc />
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Tear the driver down: stop the alarm projection + poll engine, then for each device
/// cancel its probe loop, <em>await the loop's clean exit</em>, and only then dispose
/// the probe CTS + runtime handles. Awaiting the probe Task before disposing closes the
/// race where a still-running loop touches a disposed CTS or a cleared runtime
/// 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>
/// <inheritdoc />
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
await _alarmProjection.DisposeAsync().ConfigureAwait(false);
@@ -397,39 +371,21 @@ 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>
/// <inheritdoc />
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>
/// <inheritdoc />
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
{
_poll.Unsubscribe(handle);
return Task.CompletedTask;
}
// ---- IAlarmSource (ALMD projection, #177) ----
// ---- IAlarmSource (ALMD projection) ----
/// <summary>
/// Subscribe to ALMD alarm transitions on <paramref name="sourceNodeIds"/>. Each id
/// names a declared ALMD UDT tag; the projection polls the tag's <c>InFaulted</c> +
/// <c>Severity</c> members at <see cref="AbCipDriverOptions.AlarmPollInterval"/> and
/// fires <see cref="OnAlarmEvent"/> on 0→1 (raise) + 1→0 (clear) transitions.
/// Feature-gated — when <see cref="AbCipDriverOptions.EnableAlarmProjection"/> is
/// <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>
/// <inheritdoc />
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
{
@@ -441,19 +397,13 @@ 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>
/// <inheritdoc />
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>
/// <inheritdoc />
public Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
_options.EnableAlarmProjection
@@ -462,8 +412,7 @@ 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>
/// <inheritdoc />
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
@@ -530,16 +479,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IPerCallHostResolver ----
/// <summary>
/// Resolve the device host address for a given tag full-reference. Per plan decision #144
/// the Phase 6.1 resilience pipeline keys its bulkhead + breaker on
/// <c>(DriverInstanceId, hostName)</c> so multi-PLC drivers get per-device isolation —
/// one dead PLC trips only its own breaker. Unknown references fall back to the
/// 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>
/// <inheritdoc />
public string ResolveHost(string fullReference)
{
if (_tagsByName.TryGetValue(fullReference, out var def))
@@ -549,16 +489,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IReadable ----
/// <summary>
/// Read each <c>fullReference</c> in order. Unknown tags surface as
/// <c>BadNodeIdUnknown</c>; libplctag-layer failures map through
/// <see cref="AbCipStatusMapper.MapLibplctagStatus(int)"/>; any other exception becomes
/// <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>
/// <inheritdoc />
public async Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
{
@@ -566,14 +497,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var now = DateTime.UtcNow;
var results = new DataValueSnapshot[fullReferences.Count];
// Task #194 — plan the batch: members of the same parent UDT get collapsed into one
// Plan the batch: members of the same parent UDT get collapsed into one
// whole-UDT read + in-memory member decode; every other reference falls back to the
// per-tag read path. Planner is a pure function over the
// current tag map; BOOL/String/Structure members stay on the fallback path because
// declaration-only offsets can't place them under Logix alignment rules. Whole-UDT
// grouping is itself gated behind EnableDeclarationOnlyUdtGrouping — Studio 5000 may
// reorder UDT members vs declaration order, so the fast path is opt-in only (see
// Driver.AbCip-003 / AbCipUdtMemberLayout remarks).
// reorder UDT members vs declaration order, so the fast path is opt-in only.
var plan = AbCipUdtReadPlanner.Build(
fullReferences, _tagsByName, _options.EnableDeclarationOnlyUdtGrouping);
@@ -594,7 +524,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNodeIdUnknown, null, now);
return;
}
// Driver.AbCip-005: a Structure tag whose Members are declared is a container —
// A Structure tag whose Members are declared is a container —
// its bare name is readable via the whole-UDT grouping path (ReadGroupAsync), not the
// per-tag path. Reading it here returns BadNotSupported rather than Good/null so the
// caller knows to address individual member paths (e.g. "Motor.Speed").
@@ -617,7 +547,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var status = runtime.GetStatus();
if (status != 0)
{
// Evict the stale handle so the next call re-creates it (Driver.AbCip-010).
// Evict the stale handle so the next call re-creates it.
// A non-zero status can mean the controller dropped the connection or the tag
// handle became permanently invalid (e.g. after a PLC download). Evicting
// mirrors the probe loop's recreate-on-failure behaviour.
@@ -662,7 +592,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
/// <summary>
/// Task #194 — perform one whole-UDT read on the parent tag, then decode each
/// Perform one whole-UDT read on the parent tag, then decode each
/// grouped member from the runtime's buffer at its computed byte offset. A per-group
/// failure (parent read raised, non-zero libplctag status, or missing device) stamps
/// the mapped fault across every grouped member only — sibling groups + the
@@ -687,7 +617,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var status = runtime.GetStatus();
if (status != 0)
{
EvictRuntime(device, parent.Name); // Driver.AbCip-010
EvictRuntime(device, parent.Name);
var mapped = AbCipStatusMapper.MapLibplctagStatus(status);
StampGroupStatus(group, results, now, mapped);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
@@ -712,7 +642,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
catch (Exception ex)
{
EvictRuntime(device, parent.Name); // Driver.AbCip-010
EvictRuntime(device, parent.Name);
StampGroupStatus(group, results, now, AbCipStatusMapper.BadCommunicationError);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
_logger.LogWarning(ex,
@@ -730,16 +660,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IWritable ----
/// <summary>
/// Write each request in order. Writes are NOT auto-retried by the driver — per plan
/// decisions #44, #45, #143 the caller opts in via <see cref="AbCipTagDefinition.WriteIdempotent"/>
/// and the resilience pipeline (layered above the driver) decides whether to replay.
/// 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>
/// <inheritdoc />
public async Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
@@ -770,7 +691,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
{
var parsedPath = AbCipTagPath.TryParse(def.TagPath);
// BOOL-within-DINT writes — per task #181, RMW against a parallel parent-DINT
// BOOL-within-DINT writes — RMW against a parallel parent-DINT
// runtime. Dispatching here keeps the normal EncodeValue path clean; the
// per-parent lock prevents two concurrent bit writes to the same DINT from
// losing one another's update.
@@ -791,7 +712,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
var status = runtime.GetStatus();
if (status != 0)
{
EvictRuntime(device, def.Name); // Driver.AbCip-010
EvictRuntime(device, def.Name);
results[i] = new WriteResult(AbCipStatusMapper.MapLibplctagStatus(status));
_logger.LogWarning(
"AbCip write returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " +
@@ -845,7 +766,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
catch (Exception ex)
{
// Transport / wire error — evict so the next write creates a fresh handle.
EvictRuntime(device, def.Name); // Driver.AbCip-010
EvictRuntime(device, def.Name);
results[i] = new WriteResult(AbCipStatusMapper.BadCommunicationError);
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
_logger.LogWarning(ex,
@@ -861,7 +782,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// Read-modify-write one bit within a DINT parent. Creates / reuses a parallel
/// parent-DINT runtime (distinct from the bit-selector handle) + serialises concurrent
/// writers against the same parent via a per-parent <see cref="SemaphoreSlim"/>.
/// Matches the Modbus BitInRegister + FOCAS PMC Bit pattern shipped in pass 1 of task #181.
/// Matches the Modbus BitInRegister + FOCAS PMC Bit pattern.
/// </summary>
private async Task<uint> WriteBitInDIntAsync(
DeviceState device, AbCipTagPath bitPath, int bit, object? value, CancellationToken ct)
@@ -965,7 +886,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// it so the next read/write call re-creates and re-initializes a fresh handle.
/// Called from <see cref="ReadSingleAsync"/>, <see cref="ReadGroupAsync"/>, and
/// <see cref="WriteAsync"/> after a non-zero libplctag status or transport exception —
/// mirroring the probe loop's recreate-on-failure behaviour (Driver.AbCip-010).
/// mirroring the probe loop's recreate-on-failure behaviour.
/// </summary>
private static void EvictRuntime(DeviceState device, string tagName)
{
@@ -975,21 +896,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
}
/// <summary>Gets the current health status of the driver.</summary>
/// <returns>The driver health information.</returns>
/// <inheritdoc />
public DriverHealth GetHealth() => _health;
/// <summary>
/// CLR-visible allocation footprint only — libplctag's native heap is invisible to the
/// 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>
/// <inheritdoc />
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>
/// <inheritdoc />
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken)
{
_templateCache.Clear();
@@ -998,24 +911,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ITagDiscovery ----
/// <summary>
/// Run-once: <see cref="DiscoverAsync"/> emits pre-declared tags and (when
/// EnableControllerBrowse is set) fully awaits the @tags symbol-table walk + UDT-shape
/// resolution within the single call, streaming the complete node set in one pass —
/// nothing fills in asynchronously after connect, so a single discovery pass is sufficient.
/// </summary>
/// <inheritdoc />
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
/// <summary>
/// Stream the driver's tag set into the builder. Pre-declared tags from
/// <see cref="AbCipDriverOptions.Tags"/> emit first; optionally, the
/// <see cref="IAbCipTagEnumerator"/> walks each device's symbol table and adds
/// 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>
/// <inheritdoc />
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
@@ -1065,7 +964,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
}
// Controller-discovered tags — opt-in via EnableControllerBrowse. The real @tags
// walker (LibplctagTagEnumerator) is the factory default since task #178 shipped,
// walker (LibplctagTagEnumerator) is the factory default,
// so leaving the flag off keeps the strict-config path for deployments where only
// declared tags should appear.
if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state))
@@ -1283,7 +1182,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// live libplctag.NET <see cref="IAbCipTagRuntime"/> instances keyed by tag name.
/// Native tag lifetime is owned by the <c>Tag.Dispose()</c> inside each
/// <see cref="LibplctagTagRuntime"/>; libplctag.NET's own finalizer covers GC-collected
/// instances so no separate SafeHandle wrapper is needed here (Driver.AbCip-006).
/// instances so no separate SafeHandle wrapper is needed here.
/// </summary>
internal sealed class DeviceState(
AbCipHostAddress parsedAddress,
@@ -1313,7 +1212,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// <see cref="AbCipDriver.ShutdownAsync"/> can await the loop's clean exit after
/// cancelling <see cref="ProbeCts"/> and BEFORE disposing the CTS or the runtime
/// handles — otherwise the still-running loop can touch a disposed CTS or a cleared
/// runtime dictionary (Driver.AbCip-008).
/// runtime dictionary.
/// </summary>
public Task? ProbeTask { get; set; }
@@ -1345,7 +1244,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
_rmwLocks.GetOrAdd(parentTagName, _ => new SemaphoreSlim(1, 1));
/// <summary>
/// Driver.AbCip-013 — compute the effective <see cref="AbCipTagCreateParams"/> for a
/// Compute the effective <see cref="AbCipTagCreateParams"/> for a
/// tag on this device. Combines the per-device options
/// (<see cref="AbCipDeviceOptions.AllowPacking"/>,
/// <see cref="AbCipDeviceOptions.ConnectionSize"/>) with the family profile defaults
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Static factory registration helper for <see cref="AbCipDriver"/>. Server's Program.cs
/// calls <see cref="Register"/> once at startup; the bootstrapper (task #248) then
/// calls <see cref="Register"/> once at startup; the bootstrapper then
/// materialises AB CIP DriverInstance rows from the central config DB into live driver
/// instances. Mirrors <c>GalaxyProxyDriverFactoryExtensions</c>.
/// </summary>
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// Parsed <c>ab://gateway[:port]/cip-path</c> host-address string used by the AbCip driver
/// as the <c>hostName</c> key across <see cref="Core.Abstractions.IHostConnectivityProbe"/>,
/// <see cref="Core.Abstractions.IPerCallHostResolver"/>, and the Polly bulkhead key
/// <c>(DriverInstanceId, hostName)</c> per v2 plan decision #144.
/// <c>(DriverInstanceId, hostName)</c>.
/// </summary>
/// <remarks>
/// <para>Format matches what libplctag's <c>gateway=...</c> + <c>path=...</c> attributes
@@ -48,6 +48,7 @@ public static class AbCipStatusMapper
/// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary>
/// <param name="status">The CIP general-status byte value.</param>
/// <returns>The mapped OPC UA StatusCode value.</returns>
public static uint MapCipGeneralStatus(byte status) => status switch
{
0x00 => Good,
@@ -72,6 +73,7 @@ public static class AbCipStatusMapper
/// operation; every other (negative) member is an error.
/// </summary>
/// <param name="status">The libplctag status code as an integer.</param>
/// <returns>The mapped OPC UA StatusCode value.</returns>
public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
/// <summary>
@@ -80,6 +82,7 @@ public static class AbCipStatusMapper
/// <see cref="IAbCipTagRuntime.GetStatus"/> seam, which returns the boxed-as-int value.
/// </summary>
/// <param name="status">The libplctag Status enum value.</param>
/// <returns>The mapped OPC UA StatusCode value.</returns>
public static uint MapLibplctagStatus(Status status) => status switch
{
Status.Ok => Good,
@@ -6,10 +6,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// pointers, and <c>__DEFVAL_*</c> stubs that are noise for the OPC UA address space.
/// </summary>
/// <remarks>
/// Lifted from the filter conventions documented across Rockwell Knowledgebase article
/// IC-12345 and the Logix 5000 Controllers General Instructions Reference. The list is
/// conservative — when in doubt, a tag is surfaced rather than hidden so operators can
/// see it and the config flow can explicitly hide it via UnsArea ACL.
/// Lifted from the filter conventions documented in the Logix 5000 Controllers General
/// Instructions Reference. The list is conservative — when in doubt, a tag is surfaced
/// rather than hidden so operators can see it and the config flow can explicitly hide it
/// via UnsArea ACL.
/// </remarks>
public static class AbCipSystemTagFilter
{
@@ -19,6 +19,7 @@ public static class AbCipSystemTagFilter
/// always preserved case and the system-tag prefixes are uppercase by convention.
/// </summary>
/// <param name="tagName">The tag name to check.</param>
/// <returns><c>true</c> when the tag name matches a known system-tag pattern; otherwise <c>false</c>.</returns>
public static bool IsSystemTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName)) return true;
@@ -24,6 +24,7 @@ public sealed record AbCipTagPath(
int? BitIndex)
{
/// <summary>Rebuild the canonical Logix tag string.</summary>
/// <returns>The canonical Logix tag string.</returns>
public string ToLibplctagName()
{
var buf = new System.Text.StringBuilder();
@@ -23,6 +23,7 @@ public sealed class AbCipTemplateCache
/// </summary>
/// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param>
/// <returns>The cached UDT shape, or <c>null</c> if not yet read.</returns>
public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) =>
_shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null;
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Computes byte offsets for declared UDT members under Logix natural-alignment rules so
/// a single whole-UDT read (task #194) can decode each member from one buffer without
/// a single whole-UDT read can decode each member from one buffer without
/// re-reading per member. Declaration-driven — the caller supplies
/// <see cref="AbCipStructureMember"/> rows; this helper produces the offset each member
/// sits at in the parent tag's read buffer.
@@ -30,6 +30,7 @@ public static class AbCipUdtMemberLayout
/// if any member type is unsupported for declaration-only layout.
/// </summary>
/// <param name="members">The list of UDT member declarations.</param>
/// <returns>A dictionary of member offsets keyed by name, or <c>null</c> if any member type is unsupported.</returns>
public static IReadOnlyDictionary<string, int>? TryBuild(
IReadOnlyList<AbCipStructureMember> members)
{
@@ -44,7 +45,7 @@ public static class AbCipUdtMemberLayout
if (!TryGetSizeAlign(member.DataType, out var size, out var align))
return null;
// Driver.AbCip-016 — an array member can't be placed by declaration-only layout: the
// An array member can't be placed by declaration-only layout: the
// whole-UDT grouped read decodes one scalar per member at its offset and can't return
// an array, and advancing the cursor by the scalar size (not size * count) would
// mis-place every member after it. Opt the whole group out so array members fall back
@@ -1,7 +1,7 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// <summary>
/// Task #194 — groups a ReadAsync batch of full-references into whole-UDT reads where
/// Groups a ReadAsync batch of full-references into whole-UDT reads where
/// possible. A group is emitted for every parent UDT tag whose declared
/// <see cref="AbCipStructureMember"/>s produced a valid offset map AND at least two of
/// its members appear in the batch; every other reference stays in the per-tag fallback
@@ -28,6 +28,7 @@ public static class AbCipUdtReadPlanner
/// <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>
/// <returns>The resulting plan, containing whole-UDT read groups and per-tag fallback reads.</returns>
public static AbCipUdtReadPlan Build(
IReadOnlyList<string> requests,
IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName,
@@ -15,6 +15,7 @@ public interface IAbCipTagEnumerator : IDisposable
/// </summary>
/// <param name="deviceParams">Parameters for creating device tags.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An asynchronous sequence of the tags discovered for the device.</returns>
IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
CancellationToken cancellationToken);
@@ -26,6 +27,7 @@ public interface IAbCipTagEnumeratorFactory
/// <summary>
/// Creates a new tag enumerator instance.
/// </summary>
/// <returns>A new <see cref="IAbCipTagEnumerator"/> instance.</returns>
IAbCipTagEnumerator Create();
}
@@ -66,11 +68,7 @@ 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>
/// <inheritdoc />
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -88,8 +86,6 @@ internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator
/// <summary>Factory for <see cref="EmptyAbCipTagEnumerator"/>.</summary>
internal sealed class EmptyAbCipTagEnumeratorFactory : IAbCipTagEnumeratorFactory
{
/// <summary>
/// Creates a new empty tag enumerator.
/// </summary>
/// <inheritdoc />
public IAbCipTagEnumerator Create() => new EmptyAbCipTagEnumerator();
}
@@ -11,20 +11,24 @@ 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>
/// <returns>A task that represents the asynchronous operation.</returns>
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>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ReadAsync(CancellationToken cancellationToken);
/// <summary>Flush the local buffer to the PLC.</summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WriteAsync(CancellationToken cancellationToken);
/// <summary>
/// Raw libplctag status code — mapped to an OPC UA StatusCode via
/// <see cref="AbCipStatusMapper.MapLibplctagStatus(int)"/>. Zero on success, negative on error.
/// </summary>
/// <returns>The raw libplctag status code (zero on success, negative on error).</returns>
int GetStatus();
/// <summary>
@@ -34,11 +38,12 @@ public interface IAbCipTagRuntime : IDisposable
/// </summary>
/// <param name="type">CIP data type to decode.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param>
/// <returns>The decoded value, or <c>null</c> if the type is unsupported.</returns>
object? DecodeValue(AbCipDataType type, int? bitIndex);
/// <summary>
/// Decode a value at an arbitrary byte offset in the local buffer. Task #194 —
/// whole-UDT reads perform one <see cref="ReadAsync"/> on the parent UDT tag then
/// Decode a value at an arbitrary byte offset in the local buffer. Whole-UDT reads
/// perform one <see cref="ReadAsync"/> on the parent UDT tag then
/// call this per declared member with its computed offset, avoiding one libplctag
/// round-trip per member. Implementations that do not support offset-aware decoding
/// may fall back to <see cref="DecodeValue"/> when <paramref name="offset"/> is zero;
@@ -48,6 +53,7 @@ public interface IAbCipTagRuntime : IDisposable
/// <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>
/// <returns>The decoded value, or <c>null</c> if the type is unsupported.</returns>
object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex);
/// <summary>
@@ -61,6 +67,7 @@ public interface IAbCipTagRuntime : IDisposable
/// </summary>
/// <param name="type">CIP element data type to decode.</param>
/// <param name="count">Number of array elements to decode.</param>
/// <returns>A boxed element-typed CLR array, or <c>null</c> if the type is unsupported.</returns>
object? DecodeArray(AbCipDataType type, int count);
/// <summary>
@@ -81,6 +88,7 @@ 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>
/// <returns>The created tag runtime handle.</returns>
IAbCipTagRuntime Create(AbCipTagCreateParams createParams);
}
@@ -93,11 +101,11 @@ public interface IAbCipTagFactory
/// <param name="Timeout">libplctag operation timeout (applies to Initialize / Read / Write).</param>
/// <param name="AllowPacking">CIP request-packing flag — combines the per-device override (if
/// any) with the family profile's <c>SupportsRequestPacking</c>. Forwarded to the libplctag
/// <c>Tag.AllowPacking</c> property (Driver.AbCip-013).</param>
/// <c>Tag.AllowPacking</c> property.</param>
/// <param name="ConnectionSize">Forward Open ConnectionSize — combines the per-device override
/// (if any) with the family profile's <c>DefaultConnectionSize</c>. libplctag 1.5.2 has no
/// direct <c>ConnectionSize</c> property; the value is plumbed for forward-compat with future
/// wrappers / a custom tag-attribute path (Driver.AbCip-013).</param>
/// wrappers / a custom tag-attribute path.</param>
/// <param name="ElementCount">Phase 4c — libplctag <c>elem_count</c>. Forwarded to the
/// libplctag <c>Tag.ElementCount</c> property so a 1-D array tag pulls all elements in one
/// CIP transaction. Defaults to 1 (scalar); the driver sets it from the tag definition's
@@ -16,6 +16,7 @@ public interface IAbCipTemplateReader : IDisposable
/// <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>
/// <returns>The raw Template Object blob bytes.</returns>
Task<byte[]> ReadAsync(
AbCipTagCreateParams deviceParams,
uint templateInstanceId,
@@ -26,5 +27,6 @@ public interface IAbCipTemplateReader : IDisposable
public interface IAbCipTemplateReaderFactory
{
/// <summary>Creates a new template reader instance.</summary>
/// <returns>A new <see cref="IAbCipTemplateReader"/> instance.</returns>
IAbCipTemplateReader Create();
}
@@ -21,10 +21,7 @@ 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>
/// <inheritdoc />
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams,
[EnumeratorCancellation] CancellationToken cancellationToken)
@@ -67,7 +64,6 @@ 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>
/// <inheritdoc />
public IAbCipTagEnumerator Create() => new LibplctagTagEnumerator();
}
@@ -25,50 +25,29 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
Protocol = Protocol.ab_eip,
Name = p.TagName,
Timeout = p.Timeout,
// Driver.AbCip-013 — honour the per-device or family-default AllowPacking knob so
// operators can disable CIP request-packing for older firmware or a single device.
AllowPacking = p.AllowPacking,
// Phase 4c — libplctag elem_count. For a 1-D array tag the driver passes the element
// count so libplctag pulls every element in one CIP read; scalar tags pass 1.
ElementCount = p.ElementCount > 1 ? p.ElementCount : 1,
};
// ConnectionSize is captured on AbCipTagCreateParams for forward-compat (driver-specs.md
// exposes it as a per-device option) but libplctag.NET 1.5.2 has no direct Tag property
// for it. Until the wrapper exposes one (or we ship a custom tag-attribute path), the
// family profile DefaultConnectionSize is what the underlying CIP Forward Open
// 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>
/// <inheritdoc />
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>
/// <inheritdoc />
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>
/// <inheritdoc />
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>
/// <inheritdoc />
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>
/// <inheritdoc />
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>
/// <inheritdoc />
public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch
{
AbCipDataType.Bool => bitIndex is int bit
@@ -79,9 +58,9 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
AbCipDataType.Int => (int)_tag.GetInt16(offset),
AbCipDataType.UInt => (int)_tag.GetUInt16(offset),
AbCipDataType.DInt => _tag.GetInt32(offset),
AbCipDataType.UDInt => _tag.GetUInt32(offset), // UInt32 to match ToDriverDataType (Driver.AbCip-004)
AbCipDataType.LInt => _tag.GetInt64(offset), // Int64 to match ToDriverDataType (Driver.AbCip-004)
AbCipDataType.ULInt => _tag.GetUInt64(offset), // UInt64 to match ToDriverDataType (Driver.AbCip-004)
AbCipDataType.UDInt => _tag.GetUInt32(offset),
AbCipDataType.LInt => _tag.GetInt64(offset),
AbCipDataType.ULInt => _tag.GetUInt64(offset),
AbCipDataType.Real => _tag.GetFloat32(offset),
AbCipDataType.LReal => _tag.GetFloat64(offset),
AbCipDataType.String => _tag.GetString(offset),
@@ -90,18 +69,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
_ => null,
};
/// <summary>
/// Phase 4c — decode <paramref name="count"/> array elements from the post-read buffer
/// into an element-typed CLR array. libplctag has already pulled all elements (the tag
/// was created with <c>elem_count = count</c>); we slice the local buffer per element at
/// byte stride <see cref="Tag.ElementSize"/> using the same per-offset decoders the
/// scalar / UDT-member paths use. The boxed result is a strongly-typed array
/// (<c>int[]</c>, <c>float[]</c>, <c>bool[]</c>, <c>string[]</c>, …) so the OPC UA layer
/// materialises a 1-D array variant.
/// </summary>
/// <param name="type">The element data type to decode.</param>
/// <param name="count">The number of elements to decode.</param>
/// <returns>A boxed element-typed CLR array, or <c>null</c> for an unsupported element type.</returns>
/// <inheritdoc />
public object? DecodeArray(AbCipDataType type, int count)
{
if (count < 1) count = 1;
@@ -163,10 +131,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
_ => 4,
};
/// <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>
/// <inheritdoc />
public void EncodeValue(AbCipDataType type, int? bitIndex, object? value)
{
switch (type)
@@ -253,9 +218,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime
/// <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>
/// <inheritdoc />
public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) =>
new LibplctagTagRuntime(createParams);
}
@@ -8,28 +8,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
/// internally via a normal read call, + returns the raw byte buffer so
/// <see cref="CipTemplateObjectDecoder"/> can decode it.
/// </summary>
/// <remarks>
/// Driver.AbCip-012 — by design each <c>FetchUdtShapeAsync</c> call creates one fresh
/// <see cref="Tag"/>, pays one CIP connection handshake, reads, and disposes. Per-type
/// connect cost is accepted because (a) template reads are a low-frequency discovery path
/// (one-shot per UDT type, then the decoded shape is cached in
/// <see cref="AbCipTemplateCache"/>), (b) libplctag pools its underlying CIP connections per
/// gateway+path so the underlying TCP/EIP session is reused even when individual
/// <see cref="Tag"/> instances are torn down, and (c) pooling at the wrapper layer here would
/// buy a single Forward Open per device per discovery run — small relative to the rest of a
/// bulk-tag-walk discovery. If telemetry ever shows discovery latency dominated by
/// template-read connects, revisit by holding one <c>@udt</c>-capable <see cref="Tag"/> per
/// device for the duration of a discovery run.
/// </remarks>
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>
/// <inheritdoc />
public async Task<byte[]> ReadAsync(
AbCipTagCreateParams deviceParams,
uint templateInstanceId,
@@ -50,7 +33,7 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
return _tag.GetBuffer();
}
/// <inheritdoc />
/// <summary>Disposes the template reader and releases the underlying libplctag tag.</summary>
public void Dispose() => _tag?.Dispose();
private static PlcType MapPlcType(string attribute) => attribute switch
@@ -64,7 +47,6 @@ 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>
/// <inheritdoc />
public IAbCipTemplateReader Create() => new LibplctagTemplateReader();
}
@@ -21,6 +21,7 @@ public sealed record AbCipPlcFamilyProfile(
{
/// <summary>Look up the profile for a configured family.</summary>
/// <param name="family">The PLC family to look up the profile for.</param>
/// <returns>The matching profile, or <see cref="ControlLogix"/> if the family is unrecognized.</returns>
public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch
{
AbCipPlcFamily.ControlLogix => ControlLogix,