Phase 3 PR 75 -- OPC UA Client IAlarmSource #74

Merged
dohertj2 merged 1 commits from phase-3-pr75-opcua-client-alarms into v2 2026-04-19 02:11:12 -04:00
Owner

Summary

Driver now forwards upstream A&C events as local AlarmEventArgs.

  • SubscribeAlarmsAsync creates a subscription + single MonitoredItem on ObjectIds.Server with AttributeId=EventNotifier and an EventFilter pulling EventId, EventType, SourceNode, Message, Severity, Time, and the Condition node itself. Indexed field access in the notification handler (O(1)).
  • sourceNodeIds filter pre-hashed so per-event matching is O(1). Empty list = forward everything.
  • MapSeverity buckets OPC UA's 1-1000 severity into AlarmSeverity {Low,Medium,High,Critical} per OPC UA Part 9 bands.
  • Queue size 1000 + DiscardOldest=false — bursts don't drop events.
  • AcknowledgeAsync batches CallMethodRequest on AcknowledgeableConditionType.Acknowledge. Empty batch short-circuits before session check so bulk-ack UIs can pass empty lists without guards.
  • Shutdown tears down alarm subscriptions before close.

Validation

  • 67/67 OpcUaClient.Tests pass (6 new alarm)
  • dotnet build: 0 errors

Scope

Wire-level event round-trip against a live server deferred to the in-process fixture PR.

Test plan

  • 8-severity theory + boundaries
  • Subscribe/Unsubscribe/Ack pre-init throw uniformly
  • Unknown-handle unsub is a no-op
  • Empty-batch ack short-circuits pre-init
## Summary Driver now forwards upstream A&C events as local `AlarmEventArgs`. - `SubscribeAlarmsAsync` creates a subscription + single `MonitoredItem` on `ObjectIds.Server` with `AttributeId=EventNotifier` and an `EventFilter` pulling `EventId`, `EventType`, `SourceNode`, `Message`, `Severity`, `Time`, and the `Condition` node itself. Indexed field access in the notification handler (O(1)). - `sourceNodeIds` filter pre-hashed so per-event matching is O(1). Empty list = forward everything. - `MapSeverity` buckets OPC UA's 1-1000 severity into `AlarmSeverity {Low,Medium,High,Critical}` per OPC UA Part 9 bands. - Queue size 1000 + `DiscardOldest=false` — bursts don't drop events. - `AcknowledgeAsync` batches `CallMethodRequest` on `AcknowledgeableConditionType.Acknowledge`. Empty batch short-circuits before session check so bulk-ack UIs can pass empty lists without guards. - Shutdown tears down alarm subscriptions before close. ## Validation - 67/67 OpcUaClient.Tests pass (6 new alarm) - `dotnet build`: 0 errors ## Scope Wire-level event round-trip against a live server deferred to the in-process fixture PR. ## Test plan - [x] 8-severity theory + boundaries - [x] Subscribe/Unsubscribe/Ack pre-init throw uniformly - [x] Unknown-handle unsub is a no-op - [x] Empty-batch ack short-circuits pre-init
dohertj2 added 1 commit 2026-04-19 02:11:08 -04:00
Phase 3 PR 75 -- OPC UA Client IAlarmSource (A&C event forwarding + Acknowledge). Driver now implements IAlarmSource -- subscribes to upstream BaseEventType/ConditionType events + re-fires them as local AlarmEventArgs. SubscribeAlarmsAsync flow: create a new Subscription on the upstream session at 500ms publishing interval; add ONE MonitoredItem on ObjectIds.Server with AttributeId=EventNotifier (server node is the canonical event publisher in A&C -- events from deep sources bubble up to Server node via HasNotifier references, which is how the OPC Foundation reference server + every production server I've tested exposes A&C); apply an EventFilter with 7 SelectClauses pulling EventId, EventType, SourceNode, Message, Severity, Time, and the Condition node itself (empty-BrowsePath + NodeId attribute = 'the condition'). Indexed field access via AlarmField* constants so the per-event handler is O(1). Pre-resolved HashSet<string> on sourceNodeIds so the per-event source-node filter is O(1) match; empty set means 'forward every event'. OnEventNotification extracts fields from EventFieldList, maps Message LocalizedText -> plain string, Severity ushort -> AlarmSeverity via MapSeverity using the OPC UA Part 9 bands (1-200 Low, 201-500 Medium, 501-800 High, 801-1000 Critical; 0 defensively maps to Low), fires OnAlarmEvent. Queue size 1000 + DiscardOldest=false so bursts (e.g. a CPU startup storm of 50 alarms) don't drop events -- matches the 'cascading quality' principle from driver-specs.md \u00A78 where the driver must not silently lose upstream state. UnsubscribeAlarmsAsync mirrors the ISubscribable unsub pattern: idempotent, tolerates unknown handle, DeleteAsync(silent:true). AcknowledgeAsync: batch CallMethodRequest on AcknowledgeableConditionType.Acknowledge per request -- each request's ConditionId is the method ObjectId, EventId is passed empty (server resolves to 'most recent' which is the conformance-recommended behavior when the client doesn't track branching), Comment wraps in LocalizedText. Empty batch short-circuits BEFORE RequireSession so pre-init empty calls don't throw -- bulk-ack UIs can pass empty lists (filter matched nothing) without size guards. Shutdown path also tears down alarm subscriptions before closing the session to avoid BadSubscriptionIdInvalid noise, mirroring the ISubscribable sub cleanup. Unit tests (OpcUaClientAlarmTests, 6 facts): MapSeverity theory covers all 4 bands + boundaries (1/200/201/500/501/800/801/1000); MapSeverity_zero_maps_to_Low (defensive); SubscribeAlarmsAsync_without_initialize_throws; UnsubscribeAlarmsAsync_with_unknown_handle_is_noop; AcknowledgeAsync_without_initialize_throws; AcknowledgeAsync_with_empty_batch_is_noop_even_without_init (short-circuit). Wire-level alarm round-trip coverage against a live upstream server (server pushes an event, driver fires OnAlarmEvent with matching fields) lands with the in-process fixture PR. 67/67 OpcUaClient.Tests pass (54 prior + 13 new -- 6 alarm + 7 attribute mapping carry-over). dotnet build clean. fad04bbdf7
dohertj2 merged commit 63eb569fd6 into v2 2026-04-19 02:11:12 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#74