No driver's Bad/Uncertain status sub-code reaches an OPC UA client (3-state OpcUaQuality collapse) #519

Open
opened 2026-07-27 13:52:12 -04:00 by dohertj2 · 0 comments
Owner

Found during the MTConnect live gate (PR #506, 2026-07-27) and confirmed by direct code inspection. Not MTConnect-specific and out of scope for that plan.

Symptom

MTConnect maps the Agent's UNAVAILABLE sentinel to BadNoCommunication (0x80310000) at MTConnectObservationIndex.cs:255. An OPC UA client reading that node gets 0x80000000 (generic StatusCodes.Bad).

Verified live: ns=2;s=mtc-1/vmc/fixture_asset_changed read 0x80000000 while carrying the Agent's exact source timestamp — which proves the publish landed and that only the status differs.

Cause — two hops

  1. DriverInstanceActor.QualityFromStatus (src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs:1032-1041) projects the driver's uint status onto the 3-state OpcUaQuality enum using only the top two severity bits:

    var severity = statusCode >> 30;
    return severity switch { 0 => Good, 1 => Uncertain, _ => Bad };
    

    Every Bad sub-code collapses to OpcUaQuality.Bad.

  2. OtOpcUaNodeManager.StatusFromQuality (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs:3318-3322) re-expands BadStatusCodes.Bad = 0x80000000.

Every message type between the two is already enum-typed (AttributeValuePublishedAttributeValueUpdateIOpcUaAddressSpaceSink.WriteValue), so nothing in between could have preserved the sub-code.

Not a null-value artifact — there is no null/Variant special-casing on the path. A Bad status carrying a non-null value collapses identically.

It is deliberate — but the consequence looks undocumented

OpcUaQuality's own doc comment (src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs:164) says: "Real SDK has finer-grained codes; the engine actors only need this 3-state classification."

So the design decision is intentional. What appears unwritten anywhere is the client-visible consequence: no driver's Bad/Uncertain sub-code ever reaches an OPC UA client.

That interacts with recent work. #497 corrected 16 wrong status constants across 6 drivers, and StatusCodeParityTests now guards them by reflection — both valuable for internal consistency — but a client still cannot distinguish BadNoCommunication from BadTypeMismatch from BadNotSupported. Diagnostic intent survives in logs only.

One sub-code does survive: BadWaitingForInitialData (0x80320000), because it is written directly at the node (OtOpcUaNodeManager.cs:1806 / :1901) rather than routed through the quality projection.

Options

  • (a) Accept and document — state in docs/ that driver status sub-codes are log-only, so driver authors don't assume clients can act on them. Cheap and honest.
  • (b) Widen the path — carry the uint through AttributeValuePublishedAttributeValueUpdateIOpcUaAddressSpaceSink.WriteValueWriteValue, touching the sink interface and its three implementations (Sdk, Deferred, Null). The real fix if clients need to act on sub-codes.

Worth an explicit decision either way, since right now the constants are maintained as if they mattered end-to-end.

Found during the MTConnect live gate (PR #506, 2026-07-27) and confirmed by direct code inspection. Not MTConnect-specific and out of scope for that plan. ## Symptom MTConnect maps the Agent's `UNAVAILABLE` sentinel to `BadNoCommunication` (`0x80310000`) at `MTConnectObservationIndex.cs:255`. An OPC UA client reading that node gets **`0x80000000`** (generic `StatusCodes.Bad`). Verified live: `ns=2;s=mtc-1/vmc/fixture_asset_changed` read `0x80000000` **while carrying the Agent's exact source timestamp** — which proves the publish landed and that only the status differs. ## Cause — two hops 1. `DriverInstanceActor.QualityFromStatus` (`src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs:1032-1041`) projects the driver's `uint` status onto the 3-state `OpcUaQuality` enum using only the top two severity bits: ```csharp var severity = statusCode >> 30; return severity switch { 0 => Good, 1 => Uncertain, _ => Bad }; ``` Every `Bad` sub-code collapses to `OpcUaQuality.Bad`. 2. `OtOpcUaNodeManager.StatusFromQuality` (`src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs:3318-3322`) re-expands `Bad` → `StatusCodes.Bad` = `0x80000000`. Every message type between the two is already enum-typed (`AttributeValuePublished` → `AttributeValueUpdate` → `IOpcUaAddressSpaceSink.WriteValue`), so nothing in between could have preserved the sub-code. **Not a null-value artifact** — there is no null/Variant special-casing on the path. A `Bad` status carrying a non-null value collapses identically. ## It is deliberate — but the consequence looks undocumented `OpcUaQuality`'s own doc comment (`src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs:164`) says: *"Real SDK has finer-grained codes; the engine actors only need this 3-state classification."* So the design decision is intentional. What appears unwritten anywhere is the client-visible consequence: **no driver's `Bad`/`Uncertain` sub-code ever reaches an OPC UA client.** That interacts with recent work. #497 corrected 16 wrong status constants across 6 drivers, and `StatusCodeParityTests` now guards them by reflection — both valuable for internal consistency — but a client still cannot distinguish `BadNoCommunication` from `BadTypeMismatch` from `BadNotSupported`. Diagnostic intent survives in logs only. **One sub-code does survive:** `BadWaitingForInitialData` (`0x80320000`), because it is written directly at the node (`OtOpcUaNodeManager.cs:1806` / `:1901`) rather than routed through the quality projection. ## Options - **(a) Accept and document** — state in `docs/` that driver status sub-codes are log-only, so driver authors don't assume clients can act on them. Cheap and honest. - **(b) Widen the path** — carry the `uint` through `AttributeValuePublished` → `AttributeValueUpdate` → `IOpcUaAddressSpaceSink.WriteValue` → `WriteValue`, touching the sink interface and its three implementations (`Sdk`, `Deferred`, `Null`). The real fix if clients need to act on sub-codes. Worth an explicit decision either way, since right now the constants are maintained as if they mattered end-to-end.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#519