Commit Graph

4 Commits

Author SHA1 Message Date
Joseph Doherty 01bcca992c feat(alarms): thread an additive AckTime through the native-alarm mirror
MES alarm-status API §6.4 (docs/plans/2026-06-30-mes-alarm-status-api.md,
Phase 1 task 1). MES needs a real AckDT for a triggered alarm, and the mirror
carried acked-vs-unacked but never WHEN. AckTime now rides the whole path:
DCL transition -> AlarmStateChanged -> gRPC AlarmStateUpdate -> site SQLite.

Stamping rule, identical on both protocols: non-null ONLY while the condition
is active AND acknowledged. That single predicate yields all three required
behaviours -- null while unacked, cleared on re-raise (a re-raise arrives
unacknowledged), and no phantom ack on a return-to-normal. The last one is
load-bearing for MxGateway, which maps INACTIVE to Acknowledged = true; without
the active check every clear would claim an ack the system never observed.

Provenance is honest, never fabricated:
  - OPC UA A&C supplies a TRUE ack instant, so we now select it:
    AcknowledgeableConditionType/AckedState/TransitionTime at SelectClause
    index 18, APPENDED so the positional reads at 0-17 keep their meaning.
    Servers that omit the field fall back to the event's own Time.
  - MxAccess Gateway supplies none, so the ack transition's own timestamp is
    used -- accurate to when the system SAW the ack. An ACTIVE_ACKED
    re-subscribe snapshot restores one from LastTransitionTimestamp rather
    than dropping it.
The decision lives in pure mappers (Opc/Mx AlarmMapper.DeriveAckTime), so it is
unit-tested with no live server or gateway.

Additive-only throughout: init-only property on AlarmStateChanged, trailing
optional positional on NativeAlarmTransition (all 14-arg call sites untouched),
proto field 24 (never reusing a number) regenerated via docker/regen-proto.sh
sitestream with the csproj diff verified empty.

Persistence rides native_alarm_state's existing metadata_json blob rather than
a new column -- deliberately. That table is RegisterReplicated in
SiteLocalDbSetup and LocalDb builds its CDC triggers from the column list at
registration time, so an additive JSON property changes no schema, no triggers
and no replication contract; metadata_json is exactly the extension point UA4
introduced for this. Rows written before the field deserialize it as null.

Tests: 4 OPC UA + 6 MxGateway mapper cases, 3 NativeAlarmActor (emit,
failover rehydrate, pre-AckTime row), 1 proto round-trip incl. the null case,
4 Commons additive/back-compat. The OPC UA SelectClause count lock-in moves
18 -> 19 with an index-18 assertion -- intended, the clause is appended, which
is precisely what that guard exists to make visible.
2026-08-01 13:12:04 -04:00
Joseph Doherty 9b78e6071d fix(dcl): identify MxGateway native alarms by object-relative reference
Surface native (Galaxy/MxGateway) alarms by their object-relative reference
(e.g. "Z28061.HeartbeatTimeoutAlarm") instead of the gateway's full provider
reference ("Galaxy!<area>.<object>.<alarm>"). The area is already preserved in
Category and the object reference is globally unique within the galaxy, so the
full provider prefix added only noise to the alarm identity operators see.

MxGatewayAlarmMapper.MapTransition/MapSnapshot now set SourceReference from
SourceObjectReference, falling back to AlarmFullReference only when the gateway
omits the object reference. +2 mapper tests; full DCL suite green (158).
2026-06-16 19:46:44 -04:00
Joseph Doherty 722b8663c1 feat(dcl): populate obtainable NativeAlarmTransition fields from OPC UA and MxGateway (#27, M2.13)
OPC UA (RealOpcUaClient):
- Append 5 new SelectClauses at indices 13–17 (never renumber 0–12):
  - 13: AlarmConditionType/ActiveState/TransitionTime → OriginalRaiseTime
  - 14–17: LimitAlarmType HighHighLimit/HighLimit/LowLimit/LowLowLimit → LimitValue
- New OpcUaAlarmMapper.PickLimitValue helper: first non-null in HiHi→Hi→Lo→LoLo
  priority order, InvariantCulture-formatted; empty string for non-limit alarm types.
- HandleAlarmEvent reads new indices with fields.Count > N guards; hard minimum (6)
  unchanged so base ConditionType events still process without the limit fields.
- Document unavailable-by-protocol fields (Category, Description, OperatorUser,
  CurrentValue) inline in BuildAlarmEventFilter and HandleAlarmEvent.

MxGateway (MxGatewayAlarmMapper):
- MapTransition: CurrentValue and LimitValue now populated via MxValueToString
  (uses MxValueExtensions.ToClrValue + InvariantCulture) from OnAlarmTransitionEvent
  proto fields current_value/limit_value.
- MapSnapshot: same — populated from ActiveAlarmSnapshot.current_value/limit_value.
- MxValueToString helper (internal): null-safe MxValue → string conversion.

Tests (17 new, 40 total pass):
- OpcUaAlarmMapperTests: PickLimitValue priority, InvariantCulture, all-null case.
- MxGatewayAlarmMapperTests: CurrentValue/LimitValue populate from double/string
  MxValue; absent fields yield empty strings.
- RealOpcUaClientAlarmFilterTests: index alignment assertions (count=18, per-index
  TypeDefinitionId+BrowsePath), regression guard on existing indices 0–12.
2026-06-16 06:37:19 -04:00
Joseph Doherty c7411700dc feat(dcl): MxGateway StreamAlarms adapter (snapshot + live transitions, reconnecting)
Adds IAlarmSubscribableConnection to MxGatewayDataConnection (shared session-less
feed, ref-counted), IMxGatewayClient.RunAlarmStreamAsync over the package
StreamAlarmsAsync with internal reconnect, and MxGatewayAlarmMapper
(AlarmFeedMessage/OnAlarmTransitionEvent -> NativeAlarmTransition). Behavior
verified against a live gateway in Task 28; mapper unit-tested.
2026-05-29 16:49:25 -04:00