docs(comments): strip internal task/milestone/bundle bookkeeping from code comments

Remove project bookkeeping citations from shipped code comments across the
solution: hyphenated task IDs (WP-14, StoreAndForward-025), milestone/task/
issue refs (M3, Task 4, Audit Log #23, #21), Bundle X task-bundle labels,
and C/D/K/S/T phase labels.

Comment text only — no code logic, string/log literals, or XML-doc structure
changed. Genuine descriptions are preserved (only the citation is stripped),
and technical lookalikes are retained (UTF-8, SHA-256, T00:00:00, M365,
UTC-5, pre-C4/pre-C5 schema versions). Flagged by the new CommentChecker
TaskReferenceInComment / TrackingReferenceInComment checks plus targeted
grep passes; full solution builds clean, append-only guard tests pass.
This commit is contained in:
Joseph Doherty
2026-07-07 11:03:26 -04:00
parent 67005ca4c0
commit 9cff87fe85
435 changed files with 2338 additions and 2547 deletions
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
/// <summary>
/// WP-6: Connection actor using Akka.NET Become/Stash pattern for lifecycle state machine.
/// Connection actor using Akka.NET Become/Stash pattern for lifecycle state machine.
///
/// States:
/// - Connecting: stash subscribe/write requests; attempts connection
@@ -19,12 +19,12 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
/// - Reconnecting: push bad quality for all subscribed tags, stash new requests,
/// fixed-interval reconnect
///
/// WP-9: Auto-reconnect with bad quality on disconnect.
/// WP-10: Transparent re-subscribe after reconnection.
/// WP-11: Write-back support (synchronous failure to caller, no S&amp;F).
/// WP-12: Tag path resolution with retry.
/// WP-13: Health reporting (connection status + tag resolution counts).
/// WP-14: Subscription lifecycle (register on create, cleanup on stop).
/// Auto-reconnect with bad quality on disconnect.
/// Transparent re-subscribe after reconnection.
/// Write-back support (synchronous failure to caller, no S&amp;F).
/// Tag path resolution with retry.
/// Health reporting (connection status + tag resolution counts).
/// Subscription lifecycle (register on create, cleanup on stop).
/// </summary>
public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
@@ -55,7 +55,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private readonly Dictionary<string, string> _subscriptionIds = new();
/// <summary>
/// DataConnectionLayer-008: reverse index of how many instances subscribe to each
/// Reverse index of how many instances subscribe to each
/// tag path. Lets <see cref="HandleUnsubscribe"/> decide whether any other instance
/// still needs a tag in O(1) instead of scanning every instance's tag set.
/// </summary>
@@ -67,14 +67,14 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private readonly HashSet<string> _unresolvedTags = new();
/// <summary>
/// DataConnectionLayer-010: tags whose retry SubscribeAsync is currently in flight.
/// Tags whose retry SubscribeAsync is currently in flight.
/// They are excluded from the next retry tick so a slow attempt is not duplicated
/// (which would leak monitored items / subscription ids).
/// </summary>
private readonly HashSet<string> _resolutionInFlight = new();
/// <summary>
/// DataConnectionLayer-018: tags whose initial SubscribeAsync (issued from
/// Tags whose initial SubscribeAsync (issued from
/// <see cref="HandleSubscribe"/>) is currently in flight. Two parallel
/// <c>SubscribeTagsRequest</c> messages for different instances sharing a tag
/// path would otherwise both observe "not subscribed" against
@@ -94,7 +94,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// </summary>
private readonly Dictionary<string, IActorRef> _subscribers = new();
// ── Native alarm subscriptions (Task-10) ──
// ── Native alarm subscriptions ──
// The connection opens one alarm feed per source reference; transitions are
// routed to subscribers (NativeAlarmActors) by source-object reference.
/// <summary>sourceReference → set of subscriber actor refs (NativeAlarmActors), for routing + ref-count.</summary>
@@ -107,7 +107,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// </summary>
private readonly Dictionary<string, string?> _alarmSourceFilter = new();
/// <summary>
/// sourceReference → parsed condition-type predicate (M2.4 / #8). The authoritative
/// sourceReference → parsed condition-type predicate. The authoritative
/// client-side gate in <see cref="HandleAlarmTransitionReceived"/>; applies uniformly
/// across OPC UA and the gateway-wide MxGateway feed.
/// </summary>
@@ -138,7 +138,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private DateTimeOffset _lastConnectedAt;
/// <summary>
/// DataConnectionLayer-011: monotonically increasing tag that identifies the
/// Monotonically increasing tag that identifies the
/// current adapter instance. Subscription callbacks capture the generation in
/// effect when they were created; a <see cref="TagValueReceived"/> whose
/// generation no longer matches comes from a disposed adapter and is dropped so
@@ -318,7 +318,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
break;
case SubscribeCompleted sc:
// In Connected state, a connection-level subscribe failure must drive
// the reconnection state machine (DataConnectionLayer-004).
// the reconnection state machine.
if (HandleSubscribeCompleted(sc))
{
_log.Warning("[{0}] Connection-level subscribe failure — entering Reconnecting", _connectionName);
@@ -426,7 +426,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_connectionDetails = newConfig;
_adapter.Disconnected += OnAdapterDisconnected;
// DataConnectionLayer-011: new adapter — bump the generation so callbacks
// New adapter — bump the generation so callbacks
// from the disposed adapter are recognised as stale and dropped.
_adapterGeneration++;
@@ -452,10 +452,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
Become(Reconnecting);
// WP-9: Push bad quality for all subscribed tags on disconnect
// Push bad quality for all subscribed tags on disconnect
PushBadQualityForAllTags();
// Task-10: notify native alarm subscribers the source feed is unavailable
// Notify native alarm subscribers the source feed is unavailable
// (mark mirrored alarms uncertain; the reconnect snapshot reconciles them).
PushAlarmSourceUnavailable();
@@ -555,7 +555,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
else
{
// DataConnectionLayer-015: the INITIAL connect must participate in the
// The INITIAL connect must participate in the
// failover counter exactly like a reconnect. Without this a primary that is
// unreachable when the actor first starts (fresh deployment, site restart, or
// a primary simply down) is retried forever and the configured backup is
@@ -582,9 +582,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
$"Connection restored on {_activeEndpoint} endpoint", null);
}
// WP-10: Transparent re-subscribe — re-establish all active subscriptions
// Transparent re-subscribe — re-establish all active subscriptions
ReSubscribeAll();
// Task-10: re-establish native alarm feeds (source replays a snapshot).
// Re-establish native alarm feeds (source replays a snapshot).
ReSubscribeAllAlarms();
BecomeConnected();
@@ -601,8 +601,8 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// Shared connect-failure handling for both the initial connect (Connecting state)
/// and reconnect (Reconnecting state). Assumes <see cref="_consecutiveFailures"/> has
/// already been incremented for the current failure. Switches to the other endpoint
/// once the retry count is exhausted and a backup is configured
/// (DataConnectionLayer-015 brought the initial connect onto this path).
/// once the retry count is exhausted and a backup is configured; the initial
/// connect follows this same path.
/// </summary>
private void CountFailureAndMaybeFailover(string? error)
{
@@ -630,7 +630,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// Wire disconnect handler on new adapter
_adapter.Disconnected += OnAdapterDisconnected;
// DataConnectionLayer-011: new adapter — bump the generation so callbacks
// New adapter — bump the generation so callbacks
// from the disposed adapter are recognised as stale and dropped.
_adapterGeneration++;
@@ -661,7 +661,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
BecomeReconnecting();
}
// ── Subscription Management (WP-14) ──
// ── Subscription Management ──
private void HandleSubscribe(SubscribeTagsRequest request)
{
@@ -675,11 +675,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
var self = Self;
var sender = Sender;
// DataConnectionLayer-011: capture the current adapter generation so callbacks
// Capture the current adapter generation so callbacks
// from this adapter can be distinguished from a later (post-failover) adapter.
var generation = _adapterGeneration;
// DataConnectionLayer-018: partition tags on the actor thread into "this
// Partition tags on the actor thread into "this
// request will issue _adapter.SubscribeAsync" vs. "already subscribed (by us
// or by another in-flight SubscribeTagsRequest)". A tag that is already in
// _subscriptionIds OR currently in _subscribesInFlight is treated as
@@ -727,7 +727,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
catch (Exception ex)
{
// DataConnectionLayer-004: distinguish a connection-level fault
// Distinguish a connection-level fault
// (adapter not connected / transport down) from a genuine
// node-not-found. Connection-level faults must drive the
// reconnection state machine, not be retried as unresolved tags.
@@ -740,7 +740,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// Initial read — capture current values for resolved tags so the Instance
// Actor doesn't stay Uncertain until the next data-change notification.
// DataConnectionLayer-026: these are NOT delivered here. Emitting a
// These are NOT delivered here. Emitting a
// TagValueReceived now (inside the background subscribe task) races ahead of
// the SubscribeCompleted that registers this instance's tags in
// _subscriptionsByInstance, so HandleTagValueReceived's fan-out finds no
@@ -749,7 +749,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// MES field that never changes) the dropped seed is the only value it will
// ever produce — leaving the attribute Uncertain forever. So the seeds ride
// back on SubscribeCompleted and are delivered after registration.
// DataConnectionLayer-027: SeedTagsAsync retries the still-empty reads so a
// SeedTagsAsync retries the still-empty reads so a
// seed that races the just-created advise (returns VT_EMPTY) is not silently
// dropped, and logs any tag that never yields a value.
var seedValues = await SeedTagsAsync(_adapter, tagsToSeed);
@@ -759,7 +759,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
/// <summary>
/// DataConnectionLayer-027: reads the current value of each tag so the Instance Actor
/// Reads the current value of each tag so the Instance Actor
/// has an initial value, retrying the still-empty subset a bounded number of times. A
/// STATIC tag (one that emits no further OnDataChange after the advise) depends
/// entirely on this seed; on a cold/fresh advise the read can race the just-created
@@ -785,12 +785,12 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
foreach (var tagPath in pending.ToList())
{
// DataConnectionLayer-027: bound each per-tag read with SeedReadTimeout so a
// Bound each per-tag read with SeedReadTimeout so a
// hung device read cannot delay SubscribeCompleted/the ack indefinitely.
// On timeout the catch block treats it identically to any other failed seed
// read — the tag stays in pending, is retried up to SeedReadMaxAttempts, and
// left Uncertain if still empty after the budget. Same CancellationTokenSource
// mechanism used by HandleWrite for WriteTimeout (DataConnectionLayer-005).
// mechanism used by HandleWrite for WriteTimeout.
using var cts = new CancellationTokenSource(_options.SeedReadTimeout);
try
{
@@ -830,7 +830,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// Applies the result of an asynchronous subscribe on the actor thread. ALL mutation
/// of subscription state and counters happens here — never on the background task —
/// so the actor model's single-threaded state guarantee holds.
/// Returns <c>true</c> if any tag failed at connection level (DataConnectionLayer-004),
/// Returns <c>true</c> if any tag failed at connection level,
/// signalling the caller (only the Connected state) to enter Reconnecting.
/// </summary>
private bool HandleSubscribeCompleted(SubscribeCompleted msg)
@@ -838,7 +838,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
var instanceName = msg.Request.InstanceUniqueName;
if (!_subscriptionsByInstance.TryGetValue(instanceName, out var instanceTags))
{
// DataConnectionLayer-021: the instance was unsubscribed while the
// The instance was unsubscribed while the
// subscribe I/O was in flight. Re-creating the per-instance entry and
// applying counter/handle mutations here would permanently leak state
// — _subscriptionsByInstance[instanceName] resurrected with no
@@ -858,7 +858,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
foreach (var result in msg.Results)
{
// DCL-018: clear in-flight markers we placed in HandleSubscribe.
// Clear in-flight markers we placed in HandleSubscribe.
if (!result.AlreadySubscribed)
_subscribesInFlight.Remove(result.TagPath);
@@ -879,7 +879,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
return false;
}
// DataConnectionLayer-004: if any tag failed because the adapter is not
// If any tag failed because the adapter is not
// connected (a connection-level fault), the subscribe needs the reconnection
// state machine, not the tag-resolution retry. Drive a disconnect and let the
// request be re-stashed/retried after reconnect via ReSubscribeAll.
@@ -887,7 +887,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
foreach (var result in msg.Results)
{
// DataConnectionLayer-018: a result with AlreadySubscribed: false means
// A result with AlreadySubscribed: false means
// this request was responsible for the SubscribeAsync call — the tag
// was added to _subscribesInFlight in HandleSubscribe. Clear it now so
// a later SubscribeTagsRequest for the same tag isn't forever treated
@@ -896,7 +896,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
if (!result.AlreadySubscribed)
_subscribesInFlight.Remove(result.TagPath);
// DataConnectionLayer-008: only a tag newly added to THIS instance's set
// Only a tag newly added to THIS instance's set
// increments the reference count, so the count stays an accurate "number
// of distinct instances subscribed to this tag".
if (instanceTags.Add(result.TagPath))
@@ -912,7 +912,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
_subscriptionIds[result.TagPath] = result.SubscriptionId!;
// DataConnectionLayer-020: distinguish fresh subscribe from
// Distinguish fresh subscribe from
// unresolved → resolved promotion. If an earlier instance's
// subscribe for this tag had failed at the resolution layer
// (the tag was already added to _unresolvedTags AND already
@@ -943,9 +943,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
else
{
// WP-12: genuine tag resolution failure — mark unresolved so the
// periodic retry timer picks it up. DataConnectionLayer-020:
// only increment _totalSubscribed when the tag is genuinely
// Genuine tag resolution failure — mark unresolved so the
// periodic retry timer picks it up. Only increment _totalSubscribed
// when the tag is genuinely
// newly-tracked. A second instance failing to resolve a tag the
// first instance already added to _unresolvedTags is the same
// logical tag, counted once — bumping _totalSubscribed again
@@ -958,7 +958,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_log.Debug("[{0}] Tag resolution failed for {1}: {2}",
_connectionName, result.TagPath, result.Error);
// DataConnectionLayer-004 / design doc Tag Path Resolution step 2:
// Design doc Tag Path Resolution step 2:
// mark the attribute quality `bad` so the Instance Actor sees a
// signal rather than staying Uncertain indefinitely.
if (_subscribers.TryGetValue(instanceName, out var subscriber))
@@ -969,7 +969,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
}
// DataConnectionLayer-026: now that every tag is registered in
// Now that every tag is registered in
// _subscriptionsByInstance, deliver the values captured by the initial read.
// Re-entering via Self reuses HandleTagValueReceived's generation guard, fan-out
// and quality accounting — and crucially runs AFTER registration, so the value
@@ -985,7 +985,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
// Start the tag-resolution retry timer if any tags are unresolved.
// DataConnectionLayer-022: StartPeriodicTimer with an existing key CANCELS
// StartPeriodicTimer with an existing key CANCELS
// and replaces the prior timer, so a fan-out of SubscribeTagsRequests
// arriving faster than TagResolutionRetryInterval would keep resetting
// the timer and starve the retry indefinitely. Gating on IsTimerActive
@@ -1000,7 +1000,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_options.TagResolutionRetryInterval);
}
// DataConnectionLayer-016: the response must match the actor's own assessment.
// The response must match the actor's own assessment.
// When a connection-level failure is driving the actor into Reconnecting, the
// tags were never subscribed at the adapter — replying Success: true would tell
// the Instance Actor the subscribe succeeded when it did not. Genuine
@@ -1020,7 +1020,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
/// <summary>
/// DataConnectionLayer-004: classifies a subscribe exception as a connection-level
/// Classifies a subscribe exception as a connection-level
/// fault (adapter not connected / transport down) versus a genuine tag-resolution
/// failure (the node does not exist on the device). Connection-level faults must
/// drive the reconnection state machine; resolution failures are retried on the
@@ -1043,10 +1043,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
if (!_subscriptionsByInstance.TryGetValue(request.InstanceUniqueName, out var tags))
return;
// WP-14: Cleanup on Instance Actor stop
// Cleanup on Instance Actor stop
foreach (var tagPath in tags)
{
// DataConnectionLayer-008: drop this instance's reference; the tag is only
// Drop this instance's reference; the tag is only
// released at the adapter when no other instance still subscribes to it.
// The reference count makes this O(1) instead of an O(instances) scan.
var remaining = _tagSubscriberCount.GetValueOrDefault(tagPath) - 1;
@@ -1070,7 +1070,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_totalSubscribed--;
_resolvedTags--;
// DataConnectionLayer-006: drop the tag's tracked quality so it is no
// Drop the tag's tracked quality so it is no
// longer counted by PushBadQualityForAllTags (which sets _tagsBadQuality
// from _lastTagQuality.Count). Leaving it here drifts the quality
// counters above _totalSubscribed across disconnect cycles.
@@ -1099,27 +1099,27 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_subscriptionsByInstance.Remove(request.InstanceUniqueName);
_subscribers.Remove(request.InstanceUniqueName);
// DataConnectionLayer-006: keep the reported quality counters in sync after the
// Keep the reported quality counters in sync after the
// unsubscribed tags' buckets were decremented above.
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
_healthCollector.UpdateTagResolution(_connectionName, _totalSubscribed, _resolvedTags);
}
// ── Write Support (WP-11) ──
// ── Write Support ──
private void HandleWrite(WriteTagRequest request)
{
_log.Debug("[{0}] Writing to tag {1}", _connectionName, request.TagPath);
var sender = Sender;
// DataConnectionLayer-005: bound the write with WriteTimeout. A hung device
// Bound the write with WriteTimeout. A hung device
// write (TCP black-hole) would otherwise never complete, so PipeTo never
// fires and the calling script gets no DCL-level error. The CancellationToken
// is passed to the adapter; on timeout we translate cancellation into a
// failed WriteTagResponse so the failure is returned synchronously (WP-11).
// failed WriteTagResponse so the failure is returned synchronously.
var cts = new CancellationTokenSource(_options.WriteTimeout);
// WP-11: Write through DCL to device, failure returned synchronously
// Write through DCL to device, failure returned synchronously
_adapter.WriteAsync(request.TagPath, request.Value, cts.Token).ContinueWith(t =>
{
cts.Dispose();
@@ -1145,7 +1145,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// <see cref="WriteTagBatchRequest.Values"/> to the device in ONE adapter
/// <c>WriteBatchAsync</c> round-trip, then (if present) writes the trigger flag with
/// a single <c>WriteAsync</c>. Both legs share one <see cref="DataConnectionOptions.WriteTimeout"/>
/// budget (DataConnectionLayer-005). Any failed value in the batch, a failed trigger,
/// budget. Any failed value in the batch, a failed trigger,
/// the timeout, or an adapter exception is translated into a failed
/// <see cref="WriteTagBatchResponse"/> returned synchronously to the caller — never a
/// dropped reply. NOTE: this deliberately composes the batch + trigger primitives and
@@ -1491,7 +1491,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}).PipeTo(sender);
}
// ── Tag Resolution Retry (WP-12) ──
// ── Tag Resolution Retry ──
private void HandleRetryTagResolution()
{
@@ -1503,7 +1503,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
var self = Self;
// DataConnectionLayer-010: only dispatch retries for tags that do not already
// Only dispatch retries for tags that do not already
// have an attempt in flight. A slow SubscribeAsync overlapping the next tick
// would otherwise produce duplicate concurrent subscribes for the same tag.
var toResolve = _unresolvedTags.Where(t => !_resolutionInFlight.Contains(t)).ToList();
@@ -1533,7 +1533,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
}
// ── Bad Quality Push (WP-9) ──
// ── Bad Quality Push ──
private void PushBadQualityForAllTags()
{
@@ -1555,7 +1555,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
}
// ── Re-subscribe (WP-10) ──
// ── Re-subscribe ──
private void ReSubscribeAll()
{
@@ -1572,7 +1572,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_subscriptionIds.Clear();
_unresolvedTags.Clear();
_resolutionInFlight.Clear();
// DataConnectionLayer-018: symmetric with _resolutionInFlight — any pending
// Symmetric with _resolutionInFlight — any pending
// initial-subscribe completions from the previous adapter generation will
// post SubscribeCompleted to the actor, but ReSubscribeAll has just emptied
// the in-flight tracking; the stale completion simply has nothing to
@@ -1580,7 +1580,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_subscribesInFlight.Clear();
_resolvedTags = 0;
// DataConnectionLayer-006: reset the quality tracking too. Otherwise tags
// Reset the quality tracking too. Otherwise tags
// resolved for the first time after reconnect (never in _lastTagQuality) only
// increment their bucket and the totals drift above _totalSubscribed. They are
// repopulated from fresh TagValueReceived messages once subscriptions activate.
@@ -1604,10 +1604,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}).PipeTo(self);
}
// DataConnectionLayer-027: re-advising alone does NOT restore a STATIC tag's value.
// Re-advising alone does NOT restore a STATIC tag's value.
// PushBadQualityForAllTags flipped every tag Bad on disconnect, and a tag that fires
// no further OnDataChange would stay Bad/Uncertain across the reconnect even though
// the source reads Good — the same seed gap as the initial subscribe (DCL-026/027),
// the source reads Good — the same seed gap as the initial subscribe,
// which this path previously did not cover. Mirror HandleSubscribe and re-seed: read
// each re-advised tag's current value and deliver it through the normal
// generation-guarded path. Capture the adapter + generation up front so a later
@@ -1616,7 +1616,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// generation no longer matches. Delivery fans out via _subscriptionsByInstance
// (preserved across reconnect), so it does not depend on _subscriptionIds being
// repopulated for the new adapter generation. Seeds delivered after registration is
// already established, so the DCL-026 ordering hazard does not apply here.
// already established, so the ordering hazard does not apply here.
var reseedAdapter = _adapter;
Task.Run(async () =>
{
@@ -1635,7 +1635,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
});
}
// ── Health Reporting (WP-13) ──
// ── Health Reporting ──
private void ReplyWithHealthReport()
{
@@ -1651,7 +1651,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private void HandleTagResolutionSucceeded(TagResolutionSucceeded msg)
{
// DataConnectionLayer-010: the retry attempt for this tag has completed.
// The retry attempt for this tag has completed.
_resolutionInFlight.Remove(msg.TagPath);
if (_unresolvedTags.Remove(msg.TagPath))
@@ -1673,11 +1673,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_log.Debug("[{0}] Tag resolution still failing for {1}: {2}",
_connectionName, msg.TagPath, msg.Error);
// DataConnectionLayer-010: the retry attempt for this tag has completed —
// The retry attempt for this tag has completed —
// it is eligible for the next retry tick again.
_resolutionInFlight.Remove(msg.TagPath);
// Track as unresolved so periodic retry picks it up. DCL-022: gate on
// Track as unresolved so periodic retry picks it up. Gate on
// IsTimerActive so a stream of TagResolutionFailed events doesn't keep
// cancelling and re-starting the timer faster than its own interval.
if (_unresolvedTags.Add(msg.TagPath) && !Timers.IsTimerActive("tag-resolution-retry"))
@@ -1692,7 +1692,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private void HandleTagValueReceived(TagValueReceived msg)
{
// DataConnectionLayer-011: drop values delivered by a disposed adapter. After a
// Drop values delivered by a disposed adapter. After a
// failover the old adapter's OPC UA SDK threads may still fire callbacks; those
// carry a stale generation and must not be forwarded to Instance Actors.
if (msg.AdapterGeneration != _adapterGeneration)
@@ -1737,7 +1737,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_healthCollector.UpdateTagQuality(_connectionName, _tagsGoodQuality, _tagsBadQuality, _tagsUncertainQuality);
}
// ── Native alarm subscriptions (Task-10) ──
// ── Native alarm subscriptions ──
private void HandleSubscribeAlarms(SubscribeAlarmsRequest request)
{
@@ -1762,7 +1762,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
subs.Add(subscriber);
_alarmSourceFilter[request.SourceReference] = request.ConditionFilter;
// Parse the type-name filter once; this is the authoritative client-side
// gate consulted on every routed transition (M2.4 / #8).
// gate consulted on every routed transition.
_alarmSourceFilterPredicate[request.SourceReference] = AlarmConditionFilter.Parse(request.ConditionFilter);
// If the adapter feed for this source is already (being) established, the
@@ -1796,11 +1796,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
_alarmSubscribesInFlight.Remove(msg.SourceReference);
// DataConnectionLayer-023: the last (or only) subscriber may have been
// The last (or only) subscriber may have been
// unsubscribed while this alarm subscribe was in flight. HandleUnsubscribeAlarms
// emptied/removed _alarmSourceSubscribers for the source but could not tear down
// the adapter feed because the subscription id was not stored yet. Mirror the
// DCL-021 tag-path guard: if no subscriber remains, release the just-created
// tag-path guard: if no subscriber remains, release the just-created
// adapter feed instead of storing an orphaned subscription id that would stream
// transitions to nobody for the lifetime of the adapter.
if (!_alarmSourceSubscribers.ContainsKey(msg.SourceReference))
@@ -1822,8 +1822,8 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
if (msg.Success && msg.SubscriptionId != null)
{
// DataConnectionLayer-029: a concurrent unsubscribe clears the in-flight
// marker (DCL-023), so a fresh subscribe for the same source can issue a
// A concurrent unsubscribe clears the in-flight
// marker, so a fresh subscribe for the same source can issue a
// SECOND adapter feed before this completion fires — yielding two completions
// for one source. Mirror the tag-path re-check (see HandleTagSubscribeCompleted,
// the `_subscriptionIds.ContainsKey` guard): if a feed is already stored, THIS
@@ -1860,7 +1860,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private void HandleAlarmTransitionReceived(AlarmTransitionReceived msg)
{
// DataConnectionLayer-011: drop transitions from a disposed adapter after failover.
// Drop transitions from a disposed adapter after failover.
if (msg.AdapterGeneration != _adapterGeneration)
return;
@@ -1895,7 +1895,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
if (!match)
continue;
// M2.4 (#8): authoritative client-side condition-type gate. Applied
// Authoritative client-side condition-type gate. Applied
// per matched source because two sources may share a prefix yet carry
// different filters. Empty filter = allow all (historical behaviour);
// framing sentinels (SnapshotComplete) are never dropped.
@@ -1924,7 +1924,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_alarmSourceSubscribers.Remove(request.SourceReference);
_alarmSourceFilter.Remove(request.SourceReference);
_alarmSourceFilterPredicate.Remove(request.SourceReference);
// DataConnectionLayer-023: clear the in-flight marker so that if an adapter
// Clear the in-flight marker so that if an adapter
// subscribe is still in flight for this source, the late AlarmSubscribeCompleted
// is recognized as orphaned (its guard checks _alarmSourceSubscribers, now empty)
// and the just-created feed is released rather than stored and leaked.
@@ -1990,7 +1990,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
IReadOnlyList<SeededValue> SeedValues);
/// <summary>An initial-read value captured during subscribe, delivered after the
/// instance's tags are registered for fan-out (DataConnectionLayer-026).</summary>
/// instance's tags are registered for fan-out.</summary>
internal record SeededValue(string TagPath, TagValue Value);
internal record AlarmTransitionReceived(NativeAlarmTransition Transition, int AdapterGeneration);
internal record AlarmSubscribeCompleted(
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Actors;
/// <summary>
/// WP-34: Protocol extensibility — manages DataConnectionActor instances.
/// Protocol extensibility — manages DataConnectionActor instances.
/// Routes messages to the correct connection actor based on connection name.
/// Adding a new protocol = implement IDataConnection + register with IDataConnectionFactory.
/// </summary>
@@ -24,7 +24,7 @@ public class DataConnectionManagerActor : ReceiveActor
private readonly DataConnectionOptions _options;
private readonly ISiteHealthCollector _healthCollector;
private readonly ISiteEventLogger? _siteEventLogger;
// T17: deployment-wide OPC UA application identity / cert-store paths — the same
// Deployment-wide OPC UA application identity / cert-store paths — the same
// global options the DataConnectionFactory feeds to RealOpcUaClient when creating OPC
// UA connections. Needed by the verify-endpoint probe (VerifyEndpointCommand), which
// builds an ApplicationConfiguration directly rather than through a connection actor.
@@ -79,7 +79,7 @@ public class DataConnectionManagerActor : ReceiveActor
return;
}
// WP-34: Factory creates the correct adapter based on protocol type
// Factory creates the correct adapter based on protocol type
var adapter = _factory.Create(command.ProtocolType, command.PrimaryConnectionDetails);
var props = Props.Create(() => new DataConnectionActor(
@@ -261,7 +261,7 @@ public class DataConnectionManagerActor : ReceiveActor
}
/// <summary>
/// T17: Handles a <see cref="VerifyEndpointCommand"/> from the Central UI's "Verify"
/// Handles a <see cref="VerifyEndpointCommand"/> from the Central UI's "Verify"
/// action — probes the endpoint config WITHOUT persisting it (connect → capture an
/// untrusted cert → disconnect) and pipes a structured <see cref="VerifyEndpointResult"/>
/// back to the sender. Verify does NOT require an existing connection (the config may be
@@ -17,7 +17,7 @@ public record OpcUaConnectionOptions(
int SamplingIntervalMs = 1000,
int QueueSize = 10,
string SecurityMode = "None",
// DataConnectionLayer-012: secure-by-default — untrusted server certificates are
// Secure-by-default — untrusted server certificates are
// rejected unless an operator explicitly opts in per connection. Accepting any
// certificate defeats the Sign / SignAndEncrypt modes against a man-in-the-middle.
bool AutoAcceptUntrustedCerts = false,
@@ -38,7 +38,7 @@ public record OpcUaUserIdentityOptions(
string CertificatePassword);
/// <summary>
/// WP-7: Abstraction over OPC UA client library for testability.
/// Abstraction over OPC UA client library for testability.
/// The real implementation would wrap an OPC UA SDK (e.g., OPC Foundation .NET Standard Library).
/// </summary>
public interface IOpcUaClient : IAsyncDisposable
@@ -150,7 +150,7 @@ public interface IOpcUaClient : IAsyncDisposable
CancellationToken cancellationToken = default);
/// <summary>
/// M7-B4 (T15): walks the server's address space breadth-first from the
/// Walks the server's address space breadth-first from the
/// root (ObjectsFolder), bounded by <paramref name="maxDepth"/> and
/// <paramref name="maxResults"/>, returning the nodes whose DisplayName or
/// root-relative path contains <paramref name="query"/> (case-insensitive
@@ -173,7 +173,7 @@ public interface IOpcUaClient : IAsyncDisposable
}
/// <summary>
/// M7-B4 (T15): shared bounded breadth-first address-space search, expressed
/// Shared bounded breadth-first address-space search, expressed
/// purely over an injected browse delegate so the stub and real OPC UA clients
/// run the IDENTICAL walk (matching, continuation-paging, depth bound, result
/// cap, node-visit ceiling, cancellation). The delegate is each client's own
@@ -351,7 +351,7 @@ internal class StubOpcUaClient : IOpcUaClient
string? parentNodeId, string? continuationToken = null, CancellationToken cancellationToken = default)
{
// Canned address-space tree so DCL browse/paging flows can be exercised
// without a live OPC UA server (T15):
// without a live OPC UA server:
// (root)
// ├─ Folder1 (Object, HasChildren=true)
// │ ├─ Tag1 (Variable) ← page 1
@@ -167,7 +167,7 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
// condition filter, so conditionFilter is intentionally NOT forwarded
// here: the DataConnectionActor applies it as the authoritative
// client-side gate per source reference AND per condition type
// (M2.4 / #8 — AlarmConditionFilter), uniform with the OPC UA path.
// (AlarmConditionFilter), uniform with the OPC UA path.
_ = Task.Run(() => client.RunAlarmStreamAsync(null, t => callback(t), token), token);
}
}
@@ -277,7 +277,7 @@ public class MxGatewayDataConnection : IDataConnection, IBrowsableDataConnection
public async ValueTask DisposeAsync()
{
_eventLoopCts?.Cancel();
// DataConnectionLayer-025: the DataConnectionActor disposes adapters
// The DataConnectionActor disposes adapters
// fire-and-forget on failover/stop without necessarily calling
// DisconnectAsync first, so tear down the alarm stream here too — otherwise
// the long-running RunAlarmStreamAsync task and its CTS leak on every
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
/// protocol-neutral <see cref="AlarmConditionState"/> / transition shape. Kept
/// free of any OPC UA SDK types so it is unit-testable without a live server;
/// the SDK field extraction lives in <c>RealOpcUaClient</c> and is exercised by
/// the live smoke test (Task 28).
/// the live smoke test.
/// </summary>
public static class OpcUaAlarmMapper
{
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
/// <summary>
/// WP-7: OPC UA adapter implementing IDataConnection.
/// OPC UA adapter implementing IDataConnection.
/// Maps IDataConnection methods to OPC UA concepts via IOpcUaClient abstraction.
///
/// OPC UA mapping:
@@ -25,13 +25,13 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
private string _endpointUrl = string.Empty;
private ConnectionHealth _status = ConnectionHealth.Disconnected;
// DataConnectionLayer-019: the previous _subscriptionHandles Dictionary was
// The previous _subscriptionHandles Dictionary was
// dead state — written by SubscribeAsync, removed by UnsubscribeAsync, but
// never read anywhere. Plain Dictionary writes from thread-pool continuations
// after await are racy (concurrent resize is undefined: InvalidOperationException,
// bucket corruption, or silently lost entries). Bookkeeping for subscriptions
// lives at two genuine layers: RealOpcUaClient._monitoredItems/_callbacks
// (already ConcurrentDictionary per DCL-003) at the device adapter, and
// (already ConcurrentDictionary) at the device adapter, and
// DataConnectionActor._subscriptionIds at the actor — both authoritative.
// The adapter has no need for a third copy; the field is removed rather than
// converted to ConcurrentDictionary because there is no reader.
@@ -49,7 +49,7 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
_logger = logger;
}
// DataConnectionLayer-013: an int flag toggled with Interlocked.Exchange so the
// An int flag toggled with Interlocked.Exchange so the
// "only the first caller fires Disconnected" guard in RaiseDisconnected is genuinely
// atomic. A plain volatile bool gives visibility but not atomicity — two threads
// (e.g. the keep-alive thread and a ReadAsync failure path) could both observe it
@@ -161,7 +161,7 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
{
EnsureConnected();
// DataConnectionLayer-019: subscriptionId is returned directly to the
// SubscriptionId is returned directly to the
// caller (DataConnectionActor stores it in _subscriptionIds). No local
// bookkeeping is kept here — see the field-deletion note above.
return await _client!.CreateSubscriptionAsync(
@@ -230,7 +230,7 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
/// <inheritdoc />
public async Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(IEnumerable<string> tagPaths, CancellationToken cancellationToken = default)
{
// DataConnectionLayer-007: a single failing tag must not abort the whole batch.
// A single failing tag must not abort the whole batch.
// ReadAsync re-throws non-cancellation exceptions; catch them per tag and record
// a failed ReadResult so the caller receives a complete result map for every
// requested tag (the ReadResult shape already carries per-tag Success/error).
@@ -269,12 +269,12 @@ public class OpcUaDataConnection : IDataConnection, IBrowsableDataConnection, IA
/// <inheritdoc />
public async Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(IDictionary<string, object?> values, CancellationToken cancellationToken = default)
{
// DataConnectionLayer-017: a mid-batch fault must not abort the whole batch.
// A mid-batch fault must not abort the whole batch.
// WriteAsync calls EnsureConnected(), which throws InvalidOperationException when
// the connection drops partway through; catch per-tag exceptions and record a
// failed WriteResult so the caller (including WriteBatchAndWaitAsync) receives a
// complete result map. OperationCanceledException is still propagated so a
// cancelled batch aborts as a whole — mirrors the DCL-007 fix for ReadBatchAsync.
// cancelled batch aborts as a whole — mirrors the ReadBatchAsync fix.
var results = new Dictionary<string, WriteResult>();
foreach (var (tagPath, value) in values)
{
@@ -21,7 +21,7 @@ public class RealOpcUaClient : IOpcUaClient
private ISession? _session;
private Subscription? _subscription;
// DataConnectionLayer-003: these maps are read from the OPC Foundation SDK's
// These maps are read from the OPC Foundation SDK's
// internal publish threads (the MonitoredItem.Notification handler reads
// _callbacks) concurrently with subscribe/disconnect mutations that run on
// thread-pool threads. Plain Dictionary access during a concurrent resize or
@@ -29,13 +29,13 @@ public class RealOpcUaClient : IOpcUaClient
private readonly ConcurrentDictionary<string, MonitoredItem> _monitoredItems = new();
private readonly ConcurrentDictionary<string, Action<string, object?, DateTime, uint>> _callbacks = new();
// Task-11: native alarm (A&C) event subscriptions, keyed by handle.
// Native alarm (A&C) event subscriptions, keyed by handle.
private readonly ConcurrentDictionary<string, MonitoredItem> _alarmItems = new();
// Per-handle "currently inside a ConditionRefresh replay" flag → Snapshot kind.
private readonly ConcurrentDictionary<string, bool> _alarmInRefresh = new();
// Per-handle last (active, acked) by source reference, to derive transition kind.
private readonly ConcurrentDictionary<string, Dictionary<string, (bool Active, bool Acked)>> _alarmLastState = new();
// DataConnectionLayer-013: int flag toggled with Interlocked.Exchange so the
// Int flag toggled with Interlocked.Exchange so the
// once-only ConnectionLost guard in OnSessionKeepAlive is atomic, not just visible.
// 0 = not fired, 1 = fired.
private int _connectionLostFired;
@@ -93,7 +93,7 @@ public class RealOpcUaClient : IOpcUaClient
await appConfig.ValidateAsync(ApplicationType.Client);
if (opts.AutoAcceptUntrustedCerts)
{
// DataConnectionLayer-012: this accepts ANY server certificate, defeating
// This accepts ANY server certificate, defeating
// certificate trust enforcement. Surface a prominent warning so an operator
// who has opted in is aware of the man-in-the-middle exposure on the link.
_logger.LogWarning(
@@ -158,7 +158,7 @@ public class RealOpcUaClient : IOpcUaClient
}
/// <summary>
/// T17: Probes an OPC UA endpoint configuration WITHOUT persisting it or creating a
/// Probes an OPC UA endpoint configuration WITHOUT persisting it or creating a
/// long-lived connection — connect, capture the server certificate if it is untrusted,
/// then disconnect. The probe is secure-by-default and READ-ONLY: it forces
/// <c>AutoAcceptUntrustedCertificates = false</c> and a validation hook that captures an
@@ -196,7 +196,7 @@ public class RealOpcUaClient : IOpcUaClient
_ => MessageSecurityMode.None
};
// T17: secure-by-default — force AutoAccept=false so an untrusted server cert is
// Secure-by-default — force AutoAccept=false so an untrusted server cert is
// captured and rejected rather than silently accepted (defeating the whole probe).
var appConfig = new ApplicationConfiguration
{
@@ -216,7 +216,7 @@ public class RealOpcUaClient : IOpcUaClient
TransportQuotas = new TransportQuotas { OperationTimeout = config.OperationTimeoutMs }
};
// T17: capture the untrusted server cert, then REJECT it (e.Accept = false). The
// Capture the untrusted server cert, then REJECT it (e.Accept = false). The
// validator runs on the SDK's connect thread; copying the cert is the only state we
// keep. Never accept — this probe must not trust anything.
appConfig.CertificateValidator.CertificateValidation += (_, e) =>
@@ -285,7 +285,7 @@ public class RealOpcUaClient : IOpcUaClient
}
finally
{
// T17: ALWAYS dispose the probe session — never leave a connection open.
// ALWAYS dispose the probe session — never leave a connection open.
if (session != null)
{
try { await session.CloseAsync(CancellationToken.None); }
@@ -298,7 +298,7 @@ public class RealOpcUaClient : IOpcUaClient
}
/// <summary>
/// T17: Pure mapping of a probe outcome — an optional exception plus an optionally
/// Pure mapping of a probe outcome — an optional exception plus an optionally
/// captured untrusted server certificate — to a <see cref="VerifyEndpointResult"/>.
/// Factored out so the classification is unit-testable WITHOUT a live OPC UA server.
/// Precedence: a captured certificate ALWAYS yields
@@ -455,8 +455,8 @@ public class RealOpcUaClient : IOpcUaClient
}
}
// ── Native alarm (Alarms & Conditions) subscription (Task-11) ──
// Behavioral correctness verified against a live A&C server in Task 28; only
// ── Native alarm (Alarms & Conditions) subscription ──
// Behavioral correctness verified against a live A&C server; only
// the OpcUaAlarmMapper value→state logic is unit-tested.
// Fixed select-clause order; parsed by index in HandleAlarmEvent.
@@ -485,7 +485,7 @@ public class RealOpcUaClient : IOpcUaClient
SamplingInterval = 0,
QueueSize = 1000,
// Server-side WhereClause is a bandwidth optimisation only — the
// authoritative condition-type gate lives in DataConnectionActor (M2.4 / #8).
// authoritative condition-type gate lives in DataConnectionActor.
Filter = BuildAlarmEventFilter(AlarmConditionFilter.Parse(conditionFilter))
};
@@ -519,7 +519,7 @@ public class RealOpcUaClient : IOpcUaClient
/// <summary>
/// Maps the standard OPC UA Alarms &amp; Conditions type names (case-insensitive)
/// to their well-known <see cref="ObjectTypeIds"/> NodeIds, for building the
/// optional server-side WhereClause (M2.4 / #8). Only standard types appear
/// optional server-side WhereClause. Only standard types appear
/// here; vendor/custom type names cannot be mapped without browsing the server
/// type tree, so they are handled by the client-side gate alone.
/// <para>
@@ -554,7 +554,7 @@ public class RealOpcUaClient : IOpcUaClient
/// <summary>
/// Inverse of <see cref="KnownConditionTypeIds"/> (NodeId → friendly name), derived
/// from it so the two cannot drift (M2.4 / #8). Used by <see cref="ResolveAlarmTypeName"/>
/// from it so the two cannot drift. Used by <see cref="ResolveAlarmTypeName"/>
/// to translate the event-type NodeId an OPC UA server sends back into the friendly
/// type name the conditionFilter gate and server-side WhereClause both key off.
/// </summary>
@@ -563,7 +563,7 @@ public class RealOpcUaClient : IOpcUaClient
/// <summary>
/// Resolves an event-type <see cref="NodeId"/> to the friendly condition-type name the
/// <c>conditionFilter</c> gate (and the server-side WhereClause) use (M2.4 / #8).
/// <c>conditionFilter</c> gate (and the server-side WhereClause) use.
///
/// <para>
/// Standard A&amp;C types are returned as their friendly name (e.g. <c>i=9341</c> →
@@ -591,7 +591,7 @@ public class RealOpcUaClient : IOpcUaClient
/// AlarmConditionType / AcknowledgeableConditionType state sub-variables we mirror,
/// and — when <paramref name="conditionFilter"/> is non-empty and every requested
/// type maps to a standard A&amp;C type — a server-side <see cref="ContentFilter"/>
/// WhereClause (OfType, OR'd) as a bandwidth optimisation (M2.4 / #8).
/// WhereClause (OfType, OR'd) as a bandwidth optimisation.
///
/// <para>
/// Conservative by design: if <em>any</em> requested type name cannot be mapped to
@@ -798,7 +798,7 @@ public class RealOpcUaClient : IOpcUaClient
SourceReference: sourceRef,
SourceObjectReference: sourceObjectRef,
// Resolve the event-type NodeId (e.g. "i=9341") to the friendly type name
// the conditionFilter gate keys off (M2.4 / #8); NodeId-string for custom types.
// the conditionFilter gate keys off; NodeId-string for custom types.
AlarmTypeName: ResolveAlarmTypeName(eventType),
Kind: kind,
Condition: OpcUaAlarmMapper.BuildCondition(active, acked, confirmed, shelve, suppressed, severity),
@@ -931,7 +931,7 @@ public class RealOpcUaClient : IOpcUaClient
// huge flat folder cannot return an unbounded set. 500 leaves headroom for
// the downstream frame-size budget (DataConnectionActor.CapBrowseChildren)
// even with long string NodeIds; a non-empty continuation point surfaces as
// a ContinuationToken so the caller can page via BrowseNext (T15).
// a ContinuationToken so the caller can page via BrowseNext.
private const uint BrowseMaxReferencesPerNode = 500u;
// NodeClassMask intentionally excludes ReferenceType, View, Variable-
@@ -1050,7 +1050,7 @@ public class RealOpcUaClient : IOpcUaClient
/// <summary>
/// Shared "build children + B1 type enrichment + continuation token" logic
/// used by both the initial browse and the BrowseNext paths so the
/// enrichment is never duplicated (T15). A non-empty
/// enrichment is never duplicated. A non-empty
/// <paramref name="continuationPoint"/> is surfaced as a Base64
/// <c>ContinuationToken</c> with <c>Truncated=true</c>; otherwise the result
/// is exhausted (<c>ContinuationToken=null, Truncated=false</c>).
@@ -1078,13 +1078,13 @@ public class RealOpcUaClient : IOpcUaClient
// Variable child; on ANY failure we leave the three fields null and
// return the children exactly as built above. Non-Variable nodes are
// never read and keep null type info. Runs identically on the browse
// and browse-next pages (T15 shares this path).
// and browse-next pages.
children = await EnrichVariableTypeInfoAsync(session, refs, children, cancellationToken)
.ConfigureAwait(false);
// A non-empty continuation point means the server has more refs than
// were returned on this page. Surface it as an opaque Base64 token the
// caller passes back to fetch the next page via BrowseNext (T15).
// caller passes back to fetch the next page via BrowseNext.
var hasMore = continuationPoint != null && continuationPoint.Length > 0;
var token = hasMore ? Convert.ToBase64String(continuationPoint!) : null;
return new Commons.Interfaces.Protocol.BrowseChildrenResult(children, hasMore, token);
@@ -1098,7 +1098,7 @@ public class RealOpcUaClient : IOpcUaClient
_ => Commons.Interfaces.Protocol.BrowseNodeClass.Other
};
// T16 type-info: best-effort enrichment of Variable rows with DataType,
// Type-info: best-effort enrichment of Variable rows with DataType,
// ValueRank, and Writable. Reads all three attributes for every Variable
// child in ONE ReadAsync round-trip; on any failure (or zero variables)
// returns the input children unchanged. Caller has already built the
@@ -1210,7 +1210,7 @@ public class RealOpcUaClient : IOpcUaClient
}
/// <summary>
/// T16 type-info: maps well-known OPC UA built-in <c>DataType</c> NodeIds
/// Type-info: maps well-known OPC UA built-in <c>DataType</c> NodeIds
/// (namespace 0 numeric ids in <see cref="DataTypeIds"/>) to friendly,
/// CLR-flavoured names for display in the node picker. Vendor / structured
/// DataTypes (anything not in the built-in table) fall back to the NodeId
@@ -1277,8 +1277,8 @@ public class RealOpcUaClientFactory : IOpcUaClientFactory
{
private readonly OpcUaGlobalOptions _globalOptions;
// DataConnectionLayer-014: a real logger must be threaded through to every
// RealOpcUaClient this factory builds, otherwise the DCL-012 auto-accept-certificate
// A real logger must be threaded through to every
// RealOpcUaClient this factory builds, otherwise the auto-accept-certificate
// warning emitted in RealOpcUaClient.ConnectAsync sinks into NullLogger and is never
// seen in production. The factory is constructed by DataConnectionFactory, which has
// an ILoggerFactory available.
@@ -4,7 +4,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
/// <summary>
/// Parsed native-alarm condition filter (M2.4 / #8).
/// Parsed native-alarm condition filter.
///
/// <para>
/// A source's <c>conditionFilter</c> is a comma-separated, case-insensitive list
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
/// <summary>
/// WP-34: Default factory that resolves protocol type strings to IDataConnection adapters.
/// Default factory that resolves protocol type strings to IDataConnection adapters.
/// Protocol extensibility: register new adapters via the constructor or AddAdapter method.
/// </summary>
public class DataConnectionFactory : IDataConnectionFactory
@@ -32,8 +32,8 @@ public class DataConnectionFactory : IDataConnectionFactory
var globalOptions = opcUaGlobalOptions.Value;
// Register built-in protocols.
// DataConnectionLayer-014: pass the ILoggerFactory into RealOpcUaClientFactory so
// the RealOpcUaClient it builds gets a real logger — without it the DCL-012
// Pass the ILoggerFactory into RealOpcUaClientFactory so
// the RealOpcUaClient it builds gets a real logger — without it the
// auto-accept-certificate security warning is silently discarded by NullLogger.
RegisterAdapter("OpcUa", details => new OpcUaDataConnection(
new RealOpcUaClientFactory(globalOptions, _loggerFactory),
@@ -15,7 +15,7 @@ public class DataConnectionOptions
public TimeSpan WriteTimeout { get; set; } = TimeSpan.FromSeconds(30);
/// <summary>
/// DataConnectionLayer-027: per-tag timeout applied to each <see cref="IDataConnection.ReadAsync"/>
/// Per-tag timeout applied to each <see cref="IDataConnection.ReadAsync"/>
/// call on the initial-subscribe seed path (and reconnect re-seed). A hung device read would
/// otherwise delay <c>SubscribeCompleted</c> and its acknowledgement indefinitely. On timeout
/// the tag is treated the same as any other failed seed read — it stays in the pending set and
@@ -27,12 +27,12 @@ public class DataConnectionOptions
/// <summary>
/// Minimum time a connection must stay up before it is considered stable.
/// If a connection drops before this threshold, it counts as an unstable
/// disconnect toward the failover retry count (DataConnectionLayer-009).
/// disconnect toward the failover retry count.
/// </summary>
public TimeSpan StableConnectionThreshold { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// DataConnectionLayer-027: total number of attempts the seed read makes per tag
/// Total number of attempts the seed read makes per tag
/// before giving up. The initial subscribe seed (and the reconnect re-seed) reads
/// each just-advised tag to capture its current value; on a cold/fresh advise the
/// read can race the subscription and return an empty/failed result. A STATIC tag
@@ -44,7 +44,7 @@ public class DataConnectionOptions
public int SeedReadMaxAttempts { get; set; } = 3;
/// <summary>
/// DataConnectionLayer-027: delay between seed-read attempts for the tags that have
/// Delay between seed-read attempts for the tags that have
/// not yet returned a usable value. Applied once per round across the whole pending
/// subset, so the total added latency is bounded to
/// <c>(SeedReadMaxAttempts - 1) × SeedReadRetryDelay</c> regardless of tag count.
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
namespace ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
/// <summary>
/// WP-34: Factory for creating IDataConnection adapters based on protocol type.
/// Factory for creating IDataConnection adapters based on protocol type.
/// Adding a new protocol = implement IDataConnection + register with the factory.
/// </summary>
public interface IDataConnectionFactory
@@ -21,7 +21,7 @@ public static class ServiceCollectionExtensions
services.AddOptions<MxGatewayGlobalOptions>()
.BindConfiguration("MxGateway");
// WP-34: Register the factory for protocol extensibility
// Register the factory for protocol extensibility
services.AddSingleton<IDataConnectionFactory, DataConnectionFactory>();
return services;