bug(dcl): native OPC UA alarm SourceReference is both a NodeId and a name prefix — NodeId-form bindings drop every transition #17
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
A native OPC UA alarm source's
SourceReferencehas 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'sSourceNameto 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 OtOpcUamaster(PR #474,7339a4af).The conflict
Subscribe path — the string is a NodeId:
TemplateNativeAlarmSource.SourceReferenceis 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:478resolves it to aNodeIdforMonitoredItem.StartNodeId.Route path — the same string is a name prefix:
RealOpcUaClient.cs:749-754derives identity from the event:sourceObjectRef = SourceName;sourceRef = "{SourceName}.{ConditionName}".DataConnectionActor.cs:1978-1979routes withtransition.SourceObjectReference.StartsWith(sourceRef, Ordinal) || transition.SourceReference.StartsWith(sourceRef, Ordinal).Against OtOpcUa v3 (post-#473) a condition on raw tag
pymodbus/plc/HR200now emits:ns=2;s=pymodbus/plc/HR200(the condition's own NodeId, == ConditionId)pymodbus/plc/HR200(the RawPath — deliberately the unique id, not the leaf)HR200(leaf)So:
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.pymodbus/plc/HR200→ routing matches, but the subscribe path resolves it asns=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
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("")).DataConnectionActorAlarmTestsconstructs transitions directly with already-matching strings, so it exercises the routing rule but never the NodeId-vs-SourceName seam.TemplateEngine.Testsuse NodeId-form values (SourceReference = "ns=2;s=Alarm") but only assert flattening/inheritance, never runtime routing.No test drives a NodeId-form
SourceReferenceend to end.Constraint: MxGateway shares this code
MxGatewayDataConnectionroutes through the sameDataConnectionActorrule, 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
NodeIdof the binding against the event'sSourceNode(now populated and equal to the ConditionId). Like-for-like, keepsSourceReferencea 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.ConditionIdexplicitly. Add aSimpleAttributeOperand(ConditionType, empty BrowsePath, Attributes.NodeId)select clause — ScadaBridge currently never selectsConditionId— and key on it. This is the Part 9 canonical identity and the value OtOpcUa's owndocs/AlarmTracking.md:47says ack/confirm/shelve key on. Strictly more correct than (1); slightly larger.SourceReferenceas 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 theEventTypefield, which #473 now populates — soAlarmConditionFiltershould work against v3 once bound. Worth a live check rather than an assumption.Acceptance
nsu=orns=)SourceReferencedelivers transitions to the bound instance, proven end to end rather than with hand-fed strings.References
RealOpcUaClient.cs:464(select clause),:478(subscribe),:749-754(identity),DataConnectionActor.cs:1978(routing).