fix(dcl): coerce scalar OPC UA writes to the node DataType (#58)

Codec-encoded scalars ("True", "42.5") reached RealOpcUaClient.WriteValuesAsync
as strings and were sent as String Variants, so a Boolean/Double node answered
BadTypeMismatch (0x80740000).

RealOpcUaClient now reads each written node's DataType attribute once per
session (batched, cached, cleared on connect/disconnect; derived types and
enumerations resolved via the supertype chain) and converts non-null scalar
values with the new OpcUaWriteCoercion table: Boolean, SByte..UInt64, Float,
Double, String, DateTime, Guid, all culture-invariant. Overflow, fractional
values to integer types and unparseable strings are refused client-side with
BadTypeMismatch and a message naming the value and target type - never a
rounded value. Arrays, null, unmapped types and nodes whose DataType read
fails pass through unchanged. WriteValueAsync now surfaces the outcome's own
status code instead of always BadNodeIdUnknown. MxGateway path untouched.

Tests: OpcUaWriteCoercionTests (coercion table incl. overflow, invalid and
culture-sensitive strings) and OpcUaWriteCoercionLiveTests (RequiresOpcUa:
String writes to Double/Boolean/UInt32 opc-plc nodes read back typed; reverting
the coercion reproduces 0x80740000). Component-DataConnectionLayer.md updated.
This commit is contained in:
Joseph Doherty
2026-11-06 16:08:33 -05:00
parent 0e89966da6
commit a5c3de63e9
6 changed files with 619 additions and 11 deletions
+3 -3
View File
@@ -19,9 +19,9 @@ set -euo pipefail
# a data-sourced SCALAR write leaves the InstanceActor as its codec-encoded STRING ("True",
# "42.5") and RealOpcUaClient wraps it in a plain Variant, so opc-plc answers BadTypeMismatch
# (0x80740000) for every non-String node — observed live 2026-09-10 (deferred-work register
# row 120). Only List values are decoded before the write. The two String nodes above are
# the rig's only writable String scalars, so the flags are the strings "True"/"False";
# the reset-echo ordering this gate proves is identical either way.
# row 120). Only List values are decoded before the write. Gitea #58 since made RealOpcUaClient
# coerce scalar writes to the node DataType, so the Boolean nodes would now work too; the seed
# keeps the String nodes because the reset-echo ordering this gate proves is identical either way.
# - Instance handshake-demo-1, bound + deployed
#
# Not idempotent (names must be unique). To re-seed: