Coerce scalar OPC UA writes to the node DataType — String Variants are rejected with BadTypeMismatch #58

Closed
opened 2026-11-06 15:24:40 -05:00 by dohertj2 · 1 comment
Owner

Register row 120 (deferred-work register).

InstanceActor.HandleSetDataAttribute / HandleWriteAttributeBatch decode only List values before the DCL write, so codec-encoded scalar strings ("True", "42.5") reach RealOpcUaClient.WriteValuesAsync, which wraps them in new Variant(value) — a String variant to a Boolean/Double node → 0x80740000. Found live 2026-09-10 against opc-plc during the #36 rig gate. MxGateway path unaffected in practice.

Revisit trigger (from register): Before any OPC UA-bound template relies on script writes to non-String nodes.

Current state: Not fixed; no OPC UA-bound template relies on script writes to non-String nodes yet.
Preferred fix (b): adapter-local coercion in RealOpcUaClient to the node DataType (it already resolves DataTypeIds in EnrichVariableTypeInfoAsync), leaving the MxGateway wire untouched; pin with a Boolean-scalar test. Related: #36.

Source: docs/plans/2026-07-08-deferred-work-register.md row 120

Register row 120 (deferred-work register). `InstanceActor.HandleSetDataAttribute` / `HandleWriteAttributeBatch` decode only List values before the DCL write, so codec-encoded scalar strings (`"True"`, `"42.5"`) reach `RealOpcUaClient.WriteValuesAsync`, which wraps them in `new Variant(value)` — a String variant to a Boolean/Double node → `0x80740000`. Found live 2026-09-10 against opc-plc during the #36 rig gate. MxGateway path unaffected in practice. **Revisit trigger (from register):** Before any OPC UA-bound template relies on script writes to non-String nodes. **Current state:** Not fixed; no OPC UA-bound template relies on script writes to non-String nodes yet. Preferred fix (b): adapter-local coercion in `RealOpcUaClient` to the node DataType (it already resolves `DataTypeIds` in `EnrichVariableTypeInfoAsync`), leaving the MxGateway wire untouched; pin with a Boolean-scalar test. Related: #36. Source: `docs/plans/2026-07-08-deferred-work-register.md` row 120
dohertj2 added the type:bugpriority:p2migrated labels 2026-11-06 15:24:40 -05:00
Author
Owner

Fixed in a5c3de63 (pushed to main).

Design: adapter-local coercion in RealOpcUaClient (option b; MxGateway wire untouched). WriteValuesAsync reads the DataType attribute of each written node once per session (batched, ConcurrentDictionary<NodeId,BuiltInType>, cleared on connect/disconnect; derived types and enumerations resolved via TypeInfo.GetBuiltInTypeAsync over the session TypeTree), then converts non-null scalars via the new OpcUaWriteCoercion table: Boolean (true/false/1/0), SByte..UInt64, Float, Double, String, DateTime (ISO 8601, offset-less = UTC), Guid — all culture-invariant. Overflow, fractional-to-integer and unparseable strings are refused client-side with BadTypeMismatch + a message naming the value and target type (never rounded). Arrays, null, unmapped types and nodes whose DataType read fails pass through unchanged. WriteValueAsync now throws with the outcome status code instead of always BadNodeIdUnknown. Component-DataConnectionLayer.md updated.

Evidence:

  • OpcUaWriteCoercionTests (unit): 67 cases incl. overflow per integer width, invalid strings, 42,5/1,000 refused, de-DE current culture ignored.
  • OpcUaWriteCoercionLiveTests (RequiresOpcUa) against infra opc-plc: String \"1234.5\"/\"True\"/\"17\" written to Motor.Speed (Double) / Motor.Running (Boolean) / Motor.FaultCode (UInt32) read back as typed double/bool/uint (two value rounds); \"-1\"→UInt32 and \"abc\"→Double refused with BadTypeMismatch and node values unchanged. Positive control: with coercion disabled the same test fails with 2155085824 (0x80740000) for all three writes.
  • ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests: 483 passed / 0 failed / 0 skipped (opc-plc up).
  • dotnet build ZB.MOM.WW.ScadaBridge.slnx: 0 errors with NU1903 demoted — the NU1903 (Microsoft.DotNet.ILCompiler 10.0.5 advisory on DelmiaNotifier) is pre-existing on origin/main and unrelated.

Not deployed.

Fixed in `a5c3de63` (pushed to `main`). **Design:** adapter-local coercion in `RealOpcUaClient` (option b; MxGateway wire untouched). `WriteValuesAsync` reads the `DataType` attribute of each written node once per session (batched, `ConcurrentDictionary<NodeId,BuiltInType>`, cleared on connect/disconnect; derived types and enumerations resolved via `TypeInfo.GetBuiltInTypeAsync` over the session TypeTree), then converts non-null scalars via the new `OpcUaWriteCoercion` table: Boolean (`true`/`false`/`1`/`0`), SByte..UInt64, Float, Double, String, DateTime (ISO 8601, offset-less = UTC), Guid — all culture-invariant. Overflow, fractional-to-integer and unparseable strings are refused client-side with `BadTypeMismatch` + a message naming the value and target type (never rounded). Arrays, null, unmapped types and nodes whose DataType read fails pass through unchanged. `WriteValueAsync` now throws with the outcome status code instead of always `BadNodeIdUnknown`. `Component-DataConnectionLayer.md` updated. **Evidence:** - `OpcUaWriteCoercionTests` (unit): 67 cases incl. overflow per integer width, invalid strings, `42,5`/`1,000` refused, de-DE current culture ignored. - `OpcUaWriteCoercionLiveTests` (RequiresOpcUa) against infra opc-plc: String `\"1234.5\"`/`\"True\"`/`\"17\"` written to Motor.Speed (Double) / Motor.Running (Boolean) / Motor.FaultCode (UInt32) read back as typed double/bool/uint (two value rounds); `\"-1\"`→UInt32 and `\"abc\"`→Double refused with BadTypeMismatch and node values unchanged. Positive control: with coercion disabled the same test fails with 2155085824 (0x80740000) for all three writes. - `ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Tests`: 483 passed / 0 failed / 0 skipped (opc-plc up). - `dotnet build ZB.MOM.WW.ScadaBridge.slnx`: 0 errors with `NU1903` demoted — the NU1903 (Microsoft.DotNet.ILCompiler 10.0.5 advisory on DelmiaNotifier) is pre-existing on origin/main and unrelated. Not deployed.
Sign in to join this conversation.