using System; using System.Collections.Generic; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Worker.MxAccess; /// /// In-process dispatcher that owns the lifetime of an /// + /// pair, and wires the consumer's AlarmTransitionEmitted stream /// onto the sink's EnqueueTransition path so transitions land on /// the worker's as proto /// messages ready for IPC dispatch. /// /// /// /// The dispatcher carries the consumer→sink→queue pipeline. The /// worker's IPC layer issues SubscribeAlarmsCommand / /// AcknowledgeAlarmCommand / QueryActiveAlarmsCommand /// through , which owns one /// dispatcher per session. /// /// /// Threading: owns no internal /// timer — the worker's STA drives polling via /// StaRuntime.InvokeAsync(() => PollOnce()), so the /// consumer's AlarmTransitionEmitted event fires on the STA. /// The dispatcher is purely a pass-through, so it inherits that /// thread. Fan-out into EnqueueTransition uses the /// thread-safe . /// /// public sealed class AlarmDispatcher : IDisposable { private readonly IMxAccessAlarmConsumer consumer; private readonly MxAccessAlarmEventSink sink; private readonly string sessionId; private readonly EventHandler handler; private bool disposed; /// Initializes a new alarm dispatcher for the given consumer, sink, and session ID. /// The alarm consumer. /// The alarm event sink. /// The session identifier. public AlarmDispatcher( IMxAccessAlarmConsumer consumer, MxAccessAlarmEventSink sink, string sessionId) { this.consumer = consumer ?? throw new ArgumentNullException(nameof(consumer)); this.sink = sink ?? throw new ArgumentNullException(nameof(sink)); this.sessionId = sessionId ?? string.Empty; // Sink.Attach is the seam that propagates the session id onto the // proto SessionId field of every emitted MxEvent. Pass the consumer // as the "associated COM object" — sink ignores the object reference // for the alarm path, but the existing IMxAccessEventSink contract // requires a non-null first arg. this.sink.Attach(this.consumer, this.sessionId); this.handler = OnTransition; consumer.AlarmTransitionEmitted += handler; } /// /// Begin polling the configured AVEVA alarm provider for /// transitions. The supplied subscription expression follows the /// canonical \\<machine>\Galaxy!<area> format. /// /// The subscription expression (e.g., \\HOST\Galaxy!Area). public void Subscribe(string subscription) { if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); consumer.Subscribe(subscription); } /// /// Forward an AcknowledgeAlarm request to the underlying /// consumer's AlarmAckByGUID. Returns the AVEVA-native /// status code (0 = success). /// /// The alarm GUID. /// The acknowledgment comment. /// The operator name. /// The operator node. /// The operator domain. /// The operator full name. /// The AVEVA-native status code. public int Acknowledge( Guid alarmGuid, string ackComment, string ackOperatorName, string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) { if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); return consumer.AcknowledgeByGuid( alarmGuid, ackComment, ackOperatorName, ackOperatorNode, ackOperatorDomain, ackOperatorFullName); } /// /// Acknowledge an alarm by its (name, provider, group) tuple. /// Routes to the consumer's AcknowledgeByName path which /// maps to wwAlarmConsumerClass.AlarmAckByName. /// /// The alarm name. /// The provider name. /// The group name. /// The acknowledgment comment. /// The operator name. /// The operator node. /// The operator domain. /// The operator full name. /// The AVEVA-native status code. public int AcknowledgeByName( string alarmName, string providerName, string groupName, string ackComment, string ackOperatorName, string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) { if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); return consumer.AcknowledgeByName( alarmName, providerName, groupName, ackComment, ackOperatorName, ackOperatorNode, ackOperatorDomain, ackOperatorFullName); } /// /// Drives a single synchronous poll of the underlying consumer. /// Must be called on the STA thread that owns the wnwrap COM object. /// No-op if the dispatcher has been disposed. /// public void PollOnce() { if (disposed) return; consumer.PollOnce(); } /// /// Snapshot the currently-active alarm set as /// protos for the /// QueryActiveAlarms RPC's ConditionRefresh stream. /// public IReadOnlyList SnapshotActiveAlarms() { if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher)); IReadOnlyList records = consumer.SnapshotActiveAlarms(); if (records.Count == 0) return Array.Empty(); List snapshots = new List(records.Count); foreach (MxAlarmSnapshotRecord record in records) { snapshots.Add(MapToSnapshot(record)); } return snapshots; } private void OnTransition(object? sender, MxAlarmTransitionEvent transition) { if (disposed) return; if (transition is null) return; MxAlarmSnapshotRecord record = transition.Record; AlarmTransitionKind kind = AlarmRecordTransitionMapper.MapTransition( transition.PreviousState, record.State); if (kind == AlarmTransitionKind.Unspecified) return; string fullReference = AlarmRecordTransitionMapper.ComposeFullReference( record.ProviderName, record.Group, record.TagName); sink.EnqueueTransition( alarmFullReference: fullReference, sourceObjectReference: record.TagName, alarmTypeName: record.Type, transitionKind: kind, severity: record.Priority, originalRaiseTimestampUtc: null, transitionTimestampUtc: record.TransitionTimestampUtc, operatorUser: record.OperatorName, operatorComment: record.AlarmComment, category: record.Group, description: string.Empty, degraded: record.Degraded); } private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record) { ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot { AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference( record.ProviderName, record.Group, record.TagName), SourceObjectReference = record.TagName, AlarmTypeName = record.Type, CurrentState = MapConditionState(record.State), Severity = record.Priority, OperatorUser = record.OperatorName, OperatorComment = record.AlarmComment, Category = record.Group, Description = string.Empty, Degraded = record.Degraded, SourceProvider = record.Degraded ? AlarmProviderMode.Subtag : AlarmProviderMode.Alarmmgr, }; if (record.TransitionTimestampUtc != DateTime.MinValue) { snapshot.LastTransitionTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime( DateTime.SpecifyKind(record.TransitionTimestampUtc, DateTimeKind.Utc)); } return snapshot; } private static AlarmConditionState MapConditionState(MxAlarmStateKind state) { // The proto's AlarmConditionState only distinguishes Active / // ActiveAcked / Inactive — both Rtn states collapse to Inactive // (the ack-vs-unack distinction on a cleared alarm is not exposed // through OPC UA's Part 9 condition state model anyway). return state switch { MxAlarmStateKind.UnackAlm => AlarmConditionState.Active, MxAlarmStateKind.AckAlm => AlarmConditionState.ActiveAcked, MxAlarmStateKind.UnackRtn => AlarmConditionState.Inactive, MxAlarmStateKind.AckRtn => AlarmConditionState.Inactive, _ => AlarmConditionState.Unspecified, }; } /// Gets the session ID. public string SessionId => sessionId; /// public void Dispose() { if (disposed) return; disposed = true; try { consumer.AlarmTransitionEmitted -= handler; } catch { /* swallow */ } try { sink.Detach(); } catch { /* swallow */ } try { consumer.Dispose(); } catch { /* swallow */ } } }