docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -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 #143–145 — 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,
|
||||
|
||||
Reference in New Issue
Block a user