bug(dcl): native OPC UA alarm SourceReference is both a NodeId and a name prefix — NodeId-form bindings drop every transition #17

Closed
opened 2026-07-17 02:58:42 -04:00 by dohertj2 · 0 comments
Owner

Summary

A native OPC UA alarm source's SourceReference has to be two incompatible things at once: it is parsed as a NodeId to create the monitored item, and matched as a plain-name prefix against the event's SourceName to route the transition to an instance. No single string satisfies both, so a NodeId-form binding subscribes correctly and then silently drops every transition.

Split out of #14. Now unblocked — the server-side half (lmxopcua#473, conditions emitted null SourceNode/SourceName/EventType) is fixed and merged in OtOpcUa master (PR #474, 7339a4af).

The conflict

Subscribe path — the string is a NodeId:

  • TemplateNativeAlarmSource.SourceReference is documented as the "OPC UA SourceNode/notifier nodeId" (src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs:22).
  • DataConnectionActor.cs:1871SubscribeAlarmsAsync(sourceRef, …)RealOpcUaClient.cs:478 resolves it to a NodeId for MonitoredItem.StartNodeId.
  • The Central UI picker emits NodeIds into this field.

Route path — the same string is a name prefix:

  • RealOpcUaClient.cs:749-754 derives identity from the event: sourceObjectRef = SourceName; sourceRef = "{SourceName}.{ConditionName}".
  • DataConnectionActor.cs:1978-1979 routes with
    transition.SourceObjectReference.StartsWith(sourceRef, Ordinal) || transition.SourceReference.StartsWith(sourceRef, Ordinal).

Against OtOpcUa v3 (post-#473) a condition on raw tag pymodbus/plc/HR200 now emits:

Field Value
SourceNode ns=2;s=pymodbus/plc/HR200 (the condition's own NodeId, == ConditionId)
SourceName pymodbus/plc/HR200 (the RawPath — deliberately the unique id, not the leaf)
ConditionName HR200 (leaf)
EventType the TypeDefinitionId

So:

  • Bind nsu=https://zb.com/otopcua/raw;s=pymodbus/plc/HR200 → monitored item is created correctly; routing evaluates "pymodbus/plc/HR200".StartsWith("nsu=https://…;s=pymodbus/plc/HR200")false → every transition dropped, silently.
  • Bind pymodbus/plc/HR200 → routing matches, but the subscribe path resolves it as ns=0;s=pymodbus/plc/HR200 (no namespace prefix ⇒ namespace 0) → a node that does not exist → the monitored item never delivers.

Either way, no alarm reaches the instance.

Why it was never caught

  • The only live smoke test passes sourceReference: string.Empty (tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs:64), which subscribes at the Server object and makes the prefix match trivially true ("".StartsWith("")).
  • DataConnectionActorAlarmTests constructs transitions directly with already-matching strings, so it exercises the routing rule but never the NodeId-vs-SourceName seam.
  • TemplateEngine.Tests use NodeId-form values (SourceReference = "ns=2;s=Alarm") but only assert flattening/inheritance, never runtime routing.

No test drives a NodeId-form SourceReference end to end.

Constraint: MxGateway shares this code

MxGatewayDataConnection routes through the same DataConnectionActor rule, and for MxGateway the source reference genuinely is the object name (MxGatewayAlarmMapper.cs:97-99), so the prefix match is correct there today. Any fix must keep the MxGateway path working — the two protocols currently share one routing rule with two different identifier spaces, which is the underlying design problem.

Fix options

  1. (recommended) Route on identity, not name. Compare the resolved NodeId of the binding against the event's SourceNode (now populated and equal to the ConditionId). Like-for-like, keeps SourceReference a NodeId as documented and as the picker emits, and needs no entity/schema change. Requires the transition to carry the source NodeId alongside the current name fields.
  2. Key on ConditionId explicitly. Add a SimpleAttributeOperand(ConditionType, empty BrowsePath, Attributes.NodeId) select clause — ScadaBridge currently never selects ConditionId — and key on it. This is the Part 9 canonical identity and the value OtOpcUa's own docs/AlarmTracking.md:47 says ack/confirm/shelve key on. Strictly more correct than (1); slightly larger.
  3. Redefine SourceReference as a name. Cheapest, but contradicts the field's documented meaning, breaks the browse picker (which emits NodeIds), and gives up per-source server-side filtering.

(1) and (2) compose: resolve the binding to a NodeId, select ConditionId, and match them.

Note the condition-type filter is a separate concern: ResolveAlarmTypeName (RealOpcUaClient.cs:580) reads the EventType field, which #473 now populates — so AlarmConditionFilter should work against v3 once bound. Worth a live check rather than an assumption.

Acceptance

  • A NodeId-form (nsu= or ns=) SourceReference delivers transitions to the bound instance, proven end to end rather than with hand-fed strings.
  • The MxGateway name-based path keeps working.
  • A regression test that fails against today's code.

References

  • Split out of #14 (OtOpcUa v3.0 cutover); server-side half fixed in lmxopcua#473 / PR #474.
  • RealOpcUaClient.cs:464 (select clause), :478 (subscribe), :749-754 (identity), DataConnectionActor.cs:1978 (routing).
## Summary A native OPC UA alarm source's `SourceReference` has to be two incompatible things at once: it is **parsed as a NodeId** to create the monitored item, and **matched as a plain-name prefix** against the event's `SourceName` to route the transition to an instance. No single string satisfies both, so a NodeId-form binding subscribes correctly and then silently drops every transition. Split out of #14. **Now unblocked** — the server-side half (lmxopcua#473, conditions emitted null `SourceNode`/`SourceName`/`EventType`) is fixed and merged in OtOpcUa `master` (PR #474, `7339a4af`). ## The conflict Subscribe path — the string is a NodeId: - `TemplateNativeAlarmSource.SourceReference` is documented as the "OPC UA SourceNode/notifier nodeId" (`src/ZB.MOM.WW.ScadaBridge.Commons/Entities/Templates/TemplateNativeAlarmSource.cs:22`). - `DataConnectionActor.cs:1871` → `SubscribeAlarmsAsync(sourceRef, …)` → `RealOpcUaClient.cs:478` resolves it to a `NodeId` for `MonitoredItem.StartNodeId`. - The Central UI picker emits NodeIds into this field. Route path — the same string is a name prefix: - `RealOpcUaClient.cs:749-754` derives identity from the event: `sourceObjectRef = SourceName`; `sourceRef = "{SourceName}.{ConditionName}"`. - `DataConnectionActor.cs:1978-1979` routes with `transition.SourceObjectReference.StartsWith(sourceRef, Ordinal) || transition.SourceReference.StartsWith(sourceRef, Ordinal)`. Against OtOpcUa v3 (post-#473) a condition on raw tag `pymodbus/plc/HR200` now emits: | Field | Value | |---|---| | SourceNode | `ns=2;s=pymodbus/plc/HR200` (the condition's own NodeId, == ConditionId) | | SourceName | `pymodbus/plc/HR200` (the RawPath — deliberately the unique id, not the leaf) | | ConditionName | `HR200` (leaf) | | EventType | the TypeDefinitionId | So: - Bind `nsu=https://zb.com/otopcua/raw;s=pymodbus/plc/HR200` → monitored item is created correctly; routing evaluates `"pymodbus/plc/HR200".StartsWith("nsu=https://…;s=pymodbus/plc/HR200")` → **false** → every transition dropped, silently. - Bind `pymodbus/plc/HR200` → routing matches, but the subscribe path resolves it as `ns=0;s=pymodbus/plc/HR200` (no namespace prefix ⇒ namespace 0) → a node that does not exist → the monitored item never delivers. Either way, no alarm reaches the instance. ## Why it was never caught - The only live smoke test passes `sourceReference: string.Empty` (`tests/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests/OpcUaAlarmLiveSmokeTests.cs:64`), which subscribes at the Server object and makes the prefix match trivially true (`"".StartsWith("")`). - `DataConnectionActorAlarmTests` constructs transitions directly with already-matching strings, so it exercises the routing rule but never the NodeId-vs-SourceName seam. - `TemplateEngine.Tests` use NodeId-form values (`SourceReference = "ns=2;s=Alarm"`) but only assert flattening/inheritance, never runtime routing. No test drives a NodeId-form `SourceReference` end to end. ## Constraint: MxGateway shares this code `MxGatewayDataConnection` routes through the same `DataConnectionActor` rule, and for MxGateway the source reference genuinely **is** the object name (`MxGatewayAlarmMapper.cs:97-99`), so the prefix match is correct there today. Any fix must keep the MxGateway path working — the two protocols currently share one routing rule with two different identifier spaces, which is the underlying design problem. ## Fix options 1. **(recommended) Route on identity, not name.** Compare the *resolved* `NodeId` of the binding against the event's `SourceNode` (now populated and equal to the ConditionId). Like-for-like, keeps `SourceReference` a NodeId as documented and as the picker emits, and needs no entity/schema change. Requires the transition to carry the source NodeId alongside the current name fields. 2. **Key on `ConditionId` explicitly.** Add a `SimpleAttributeOperand(ConditionType, empty BrowsePath, Attributes.NodeId)` select clause — ScadaBridge currently **never selects `ConditionId`** — and key on it. This is the Part 9 canonical identity and the value OtOpcUa's own `docs/AlarmTracking.md:47` says ack/confirm/shelve key on. Strictly more correct than (1); slightly larger. 3. **Redefine `SourceReference` as a name.** Cheapest, but contradicts the field's documented meaning, breaks the browse picker (which emits NodeIds), and gives up per-source server-side filtering. (1) and (2) compose: resolve the binding to a NodeId, select `ConditionId`, and match them. Note the condition-type filter is a separate concern: `ResolveAlarmTypeName` (`RealOpcUaClient.cs:580`) reads the `EventType` **field**, which #473 now populates — so `AlarmConditionFilter` should work against v3 once bound. Worth a live check rather than an assumption. ## Acceptance - A NodeId-form (`nsu=` or `ns=`) `SourceReference` delivers transitions to the bound instance, proven end to end rather than with hand-fed strings. - The MxGateway name-based path keeps working. - A regression test that fails against today's code. ## References - Split out of #14 (OtOpcUa v3.0 cutover); server-side half fixed in lmxopcua#473 / PR #474. - `RealOpcUaClient.cs:464` (select clause), `:478` (subscribe), `:749-754` (identity), `DataConnectionActor.cs:1978` (routing).
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/ScadaBridge#17