Galaxy native alarm authored by the AdminUI picker (attributeRef tag.Attr) never activates: feed reports Galaxy!Area.tag.Attr #895

Closed
opened 2026-11-12 23:34:14 -05:00 by dohertj2 · 2 comments
Owner

Found during the #615 G6 live gate (windev test Galaxy DEV, mxaccessgw 447070a5, 2026-11-12).

Symptom. A Galaxy native alarm authored the way the AdminUI Galaxy picker authors it never goes active in OPC UA. The tag TestMachine_015.TestAlarm003 was authored with TagConfig = {"attributeRef":"TestMachine_015.TestAlarm003","alarm":{"alarmType":"OffNormalAlarm","severity":700}}. The alarm was raised in Galaxy and the gateway feed reported it ALARM_CONDITION_STATE_ACTIVE. The OPC UA condition ns=2;s=galaxy-windev/dev/TestArea/TestMachine_015/TestAlarm003 stayed Inactive, Acknowledged, including after a ConditionRefresh.

Cause. The gateway alarm feed names an alarm by its provider-qualified reference, alarmFullReference = "Galaxy!TestArea.TestMachine_015.TestAlarm003", with sourceObjectReference = "TestMachine_015.TestAlarm003". GalaxyDriver.OnAlarmFeedTransition reverse-mapped only AlarmFullReference through the authored attributeRef table (ToRawPath). The picker writes the plain tag.Attr form (RawBrowseCommitMapper: attributeRef = the discovered full name), so the lookup missed, the identity fallback produced a ConditionId that matches no condition, and every transition was dropped. The ack path had the mirror problem: it dialled ToAttributeRef(rawPath) = tag.Attr, which the gateway's AcknowledgeAlarm refuses as a non-provider reference.

The only form that worked was hand-authoring attributeRef = "Galaxy!<Area>.<tag>.<Attr>" (what the H6 gates did). That form gives up the value read for the same attribute.

The unit test Alarm_ConditionIdIsRawPath_AndAckRoundTripsToAttributeRef fed the bare attributeRef as AlarmFullReference, which the real gateway never sends, so CI never exercised the mismatch.

Fix (e113faf5 on master). ResolveAlarmRawPath matches the full reference first, then SourceObjectReference, then the full reference with its Provider!Area. prefix stripped. A fallback match records the full reference per RawPath. AcknowledgeAsync dials that learned reference, so the gateway accepts the ack and the #869 confirmation watch matches the feed. Three unit tests were added; the main one fails with the fix reverted.

Live re-check after the fix (same run): ConditionRefresh showed TestAlarm003 as Active, Unacknowledged, Severity 750. The OPC UA Acknowledge dialled Galaxy!TestArea.TestMachine_015.TestAlarm003; afterwards Galaxy .Acked was True and .AckMsg was "OtOpcUa #615 G6 gate picker-form (#895) [OPC UA: g6gate, key …]". The clear then arrived as Inactive, Acknowledged, Retain=False.

Found during the #615 G6 live gate (windev test Galaxy `DEV`, mxaccessgw `447070a5`, 2026-11-12). **Symptom.** A Galaxy native alarm authored the way the AdminUI Galaxy picker authors it never goes active in OPC UA. The tag `TestMachine_015.TestAlarm003` was authored with `TagConfig = {"attributeRef":"TestMachine_015.TestAlarm003","alarm":{"alarmType":"OffNormalAlarm","severity":700}}`. The alarm was raised in Galaxy and the gateway feed reported it `ALARM_CONDITION_STATE_ACTIVE`. The OPC UA condition `ns=2;s=galaxy-windev/dev/TestArea/TestMachine_015/TestAlarm003` stayed `Inactive, Acknowledged`, including after a ConditionRefresh. **Cause.** The gateway alarm feed names an alarm by its provider-qualified reference, `alarmFullReference = "Galaxy!TestArea.TestMachine_015.TestAlarm003"`, with `sourceObjectReference = "TestMachine_015.TestAlarm003"`. `GalaxyDriver.OnAlarmFeedTransition` reverse-mapped only `AlarmFullReference` through the authored attributeRef table (`ToRawPath`). The picker writes the plain `tag.Attr` form (`RawBrowseCommitMapper`: `attributeRef` = the discovered full name), so the lookup missed, the identity fallback produced a ConditionId that matches no condition, and every transition was dropped. The ack path had the mirror problem: it dialled `ToAttributeRef(rawPath)` = `tag.Attr`, which the gateway's `AcknowledgeAlarm` refuses as a non-provider reference. The only form that worked was hand-authoring `attributeRef = "Galaxy!<Area>.<tag>.<Attr>"` (what the H6 gates did). That form gives up the value read for the same attribute. The unit test `Alarm_ConditionIdIsRawPath_AndAckRoundTripsToAttributeRef` fed the bare attributeRef as `AlarmFullReference`, which the real gateway never sends, so CI never exercised the mismatch. **Fix** (`e113faf5` on `master`). `ResolveAlarmRawPath` matches the full reference first, then `SourceObjectReference`, then the full reference with its `Provider!Area.` prefix stripped. A fallback match records the full reference per RawPath. `AcknowledgeAsync` dials that learned reference, so the gateway accepts the ack and the #869 confirmation watch matches the feed. Three unit tests were added; the main one fails with the fix reverted. **Live re-check after the fix (same run):** ConditionRefresh showed `TestAlarm003` as `Active, Unacknowledged`, Severity 750. The OPC UA `Acknowledge` dialled `Galaxy!TestArea.TestMachine_015.TestAlarm003`; afterwards Galaxy `.Acked` was True and `.AckMsg` was `"OtOpcUa #615 G6 gate picker-form (#895) [OPC UA: g6gate, key …]"`. The clear then arrived as `Inactive, Acknowledged`, `Retain=False`.
dohertj2 added the type:bugpriority:p2 labels 2026-11-12 23:34:14 -05:00
Author
Owner

Fixed on master in e113faf5, pushed together with the G6 record d9b23812.

Live-verified on the windev test Galaxy:

  • The picker-form tag TestMachine_015.TestAlarm003 went Active, Unacknowledged.
  • The OPC UA ack dialled Galaxy!TestArea.TestMachine_015.TestAlarm003.
  • Galaxy then showed .Acked=True.

Evidence: docs/drivers/evidence/galaxy-g6-e2e-2026-11-12.md, Step 6. Left open for the owner to close after review.

Fixed on `master` in `e113faf5`, pushed together with the G6 record `d9b23812`. Live-verified on the windev test Galaxy: - The picker-form tag `TestMachine_015.TestAlarm003` went `Active, Unacknowledged`. - The OPC UA ack dialled `Galaxy!TestArea.TestMachine_015.TestAlarm003`. - Galaxy then showed `.Acked=True`. Evidence: `docs/drivers/evidence/galaxy-g6-e2e-2026-11-12.md`, Step 6. Left open for the owner to close after review.
Author
Owner

Independent review of e113faf5: the fix holds for the case it targets, and I've fixed three gaps in follow-up d5288388 (pushed to master).

What holds

  • Picker form (tag.Attr): resolves through SourceObjectReference, and the ack dials the learned Galaxy!Area.tag.Attr. I re-checked the mutation: with the call sites reverted to ToRawPath / ToAttributeRef, Alarm_PickerAuthoredAttributeRef_… fails.
  • Hand-authored Galaxy!Area.tag.Attr in the current area: matches directly, and the ack sends the authored reference.
  • If a picker-form object moves to another area, the source reference doesn't change, so it still resolves. The learned reference is overwritten on each transition, so the ack follows the new area.
  • The gateway also splits the area on the first . after ! (GatewayAlarmMonitor.TryParseAlarmReference). An area name has no dots, so StripProviderAndArea agrees with the gateway.
  • The initial-state snapshot and ConditionRefresh path is consistent: GatewayGalaxyAlarmFeed.DispatchSnapshotEntry raises snapshot entries through the same OnAlarmTransition → OnAlarmFeedTransition → ResolveAlarmRawPath path. The driver has no separate QueryActiveAlarms mapping.
  • Thread safety: the reverse tables are built in the constructor and only read after that. The learned map is a ConcurrentDictionary keyed by authored RawPath, so it is bounded by the number of tags. A redeploy that changes the tags builds a new driver instance, which starts with fresh tables.
  • An unmatched reference keeps the identity fallback and does not throw. It teaches nothing, because the server matches no condition for it.

Gaps found and fixed in d5288388

  1. Re-areaed hand-authored tag. A tag authored as Galaxy!Area1.tag.Attr dropped every transition once Galaxy moved the object, because the feed then names Galaxy!Area2… and no fallback looked at the authored provider form. Provider-form attributeRefs are now also indexed by their area-free tag.Attr, and the ack dials the new full reference.
  2. Empty-area references with the subtag provider. When the area is blank, the gateway composes Galaxy!tag.Attr (AlarmWatchListResolver.ComposeFullReference). Its subtag provider sends only the object (tag) as the source reference. Stripping the area then removed tag. and missed. The part after the ! is now tried too.
  3. Learning ran after the subscription gate. A transition seen with no handle registered taught nothing for the ack. Resolution now runs before the gate. Fan-out is still gated.
  4. Collision. When one alarm is authored in both forms, the result is deterministic: the provider-form tag wins while the area matches, any other area surfaces under the picker tag, and a warning is logged at build time.

Tests

  • 18 new cases in GalaxyDriverRawPathKeyingTests: re-area for both forms, subtag shapes (area / empty area / null source), H6-style refs (Galaxy!CVDAisle_1.TableAlarms_008.RuntimeAlert, dotted Tank1.Level.HiHi), unmatched and malformed refs (no throw, nothing learned), the collision, learning before the subscription is registered, and the #869 confirmation of an unconfirmed picker-form ack by the feed's Acknowledge transition.
  • The new tests fail where they should:
    • Against e113faf5's GalaxyDriver.cs: 4 fail (re-area hand form, two empty-area shapes, pre-subscription learning).
    • With the call sites reverted: 3 fail (the original test, the #869 confirmation and pre-subscription learning).
  • Galaxy.Tests: 605 passed, 12 skipped. Galaxy.Browser.Tests: 24 passed. Release build: 0 warnings.
  • docs/drivers/Galaxy.md has been updated.

Out of scope: #904. An ack of a picker-form alarm before this driver instance has seen any transition for it still dials the bare tag.Attr, which the gateway refuses. This can happen right after the host restores conditions from LocalDb and before the feed snapshot arrives. The refusal is visible and the host reverts the ack.

No live re-check was run. The change only widens the reverse-map fallbacks, and the live-verified picker path takes the same branch as before.

Closing.

**Independent review of `e113faf5`: the fix holds for the case it targets, and I've fixed three gaps in follow-up `d5288388` (pushed to `master`).** **What holds** - Picker form (`tag.Attr`): resolves through `SourceObjectReference`, and the ack dials the learned `Galaxy!Area.tag.Attr`. I re-checked the mutation: with the call sites reverted to `ToRawPath` / `ToAttributeRef`, `Alarm_PickerAuthoredAttributeRef_…` fails. - Hand-authored `Galaxy!Area.tag.Attr` in the current area: matches directly, and the ack sends the authored reference. - If a picker-form object moves to another area, the source reference doesn't change, so it still resolves. The learned reference is overwritten on each transition, so the ack follows the new area. - The gateway also splits the area on the first `.` after `!` (`GatewayAlarmMonitor.TryParseAlarmReference`). An area name has no dots, so `StripProviderAndArea` agrees with the gateway. - The initial-state snapshot and ConditionRefresh path is consistent: `GatewayGalaxyAlarmFeed.DispatchSnapshotEntry` raises snapshot entries through the same `OnAlarmTransition` → `OnAlarmFeedTransition` → `ResolveAlarmRawPath` path. The driver has no separate `QueryActiveAlarms` mapping. - Thread safety: the reverse tables are built in the constructor and only read after that. The learned map is a `ConcurrentDictionary` keyed by authored RawPath, so it is bounded by the number of tags. A redeploy that changes the tags builds a new driver instance, which starts with fresh tables. - An unmatched reference keeps the identity fallback and does not throw. It teaches nothing, because the server matches no condition for it. **Gaps found and fixed in `d5288388`** 1. **Re-areaed hand-authored tag.** A tag authored as `Galaxy!Area1.tag.Attr` dropped every transition once Galaxy moved the object, because the feed then names `Galaxy!Area2…` and no fallback looked at the authored provider form. Provider-form attributeRefs are now also indexed by their area-free `tag.Attr`, and the ack dials the new full reference. 2. **Empty-area references with the subtag provider.** When the area is blank, the gateway composes `Galaxy!tag.Attr` (`AlarmWatchListResolver.ComposeFullReference`). Its subtag provider sends only the object (`tag`) as the source reference. Stripping the area then removed `tag.` and missed. The part after the `!` is now tried too. 3. **Learning ran after the subscription gate.** A transition seen with no handle registered taught nothing for the ack. Resolution now runs before the gate. Fan-out is still gated. 4. **Collision.** When one alarm is authored in both forms, the result is deterministic: the provider-form tag wins while the area matches, any other area surfaces under the picker tag, and a warning is logged at build time. **Tests** - 18 new cases in `GalaxyDriverRawPathKeyingTests`: re-area for both forms, subtag shapes (area / empty area / null source), H6-style refs (`Galaxy!CVDAisle_1.TableAlarms_008.RuntimeAlert`, dotted `Tank1.Level.HiHi`), unmatched and malformed refs (no throw, nothing learned), the collision, learning before the subscription is registered, and the #869 confirmation of an unconfirmed picker-form ack by the feed's Acknowledge transition. - The new tests fail where they should: - Against `e113faf5`'s `GalaxyDriver.cs`: 4 fail (re-area hand form, two empty-area shapes, pre-subscription learning). - With the call sites reverted: 3 fail (the original test, the #869 confirmation and pre-subscription learning). - Galaxy.Tests: 605 passed, 12 skipped. Galaxy.Browser.Tests: 24 passed. Release build: 0 warnings. - `docs/drivers/Galaxy.md` has been updated. **Out of scope:** #904. An ack of a picker-form alarm before this driver instance has seen any transition for it still dials the bare `tag.Attr`, which the gateway refuses. This can happen right after the host restores conditions from LocalDb and before the feed snapshot arrives. The refusal is visible and the host reverts the ack. No live re-check was run. The change only widens the reverse-map fallbacks, and the live-verified picker path takes the same branch as before. Closing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#895