fix(address-space): exact element-type check for array writes + array-capability shape edges (#610 review R4)

MAJOR-1: the SDK skips TypeInfo.IsInstanceOfDataType on any node with an
OnWriteValue handler (every writable equipment node), and the node manager's
fail-fast treated numeric<->numeric as compatible, so a UInt32[] to a Galaxy
Int32 array matched MxValueEncoder's int[] arm by CLR covariance and was
written bit-reinterpreted with a Good result. EvaluateEquipmentWriteStructure
now matches an ARRAY write's element type exactly (Byte[]/ByteString the only
equivalence; Variant[]/object[] refused) -> BadTypeMismatch, nothing
dispatched. MxValueEncoder's int[]/long[] arms are exact-type guarded too, and
the Galaxy parity test's false "the SDK refuses" premise is corrected (the
UInt32/UInt64 exclusion is now the encoder's own answer). Scalar writes keep
numeric widening by decision: consumers write CLR-typed values (Double to a
Float node) and every driver's scalar encoder range-checks.

MINOR: Modbus row honours maxRegistersPerWrite (smallest of DriverConfig /
the tag's DeviceConfig, default 123) and FC15's 1968-coil ceiling, and
requires region + numeric address; Modbus/S7/Galaxy require isArray + integer
arrayLength >= 1 (Galaxy: an unbounded node would let a short write resize the
attribute); an array value to a scalar node is refused inline. Docs: Uns.md
array-write rules + upgrade note (ReadWrite array tags become client-writable
on first boot), Modbus.md, Galaxy.md.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 08:07:36 -04:00
parent b7a48e2b93
commit dabd8eb955
14 changed files with 532 additions and 85 deletions
@@ -7,10 +7,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
/// <summary>
/// Gitea #610 — pins <see cref="DriverArrayWriteSupport"/>'s Galaxy row against the REAL write encoder
/// (<see cref="MxValueEncoder"/>). The OPC UA server hands the driver the CLR array type of the node's
/// <c>DataType</c> (the SDK refuses a mistyped array before the write handler runs), so for every
/// <c>Tag.DataType</c> the server can materialise, the node is advertised writable exactly when the encoder
/// accepts that array type.
/// (<see cref="MxValueEncoder"/>). The driver receives the CLR array type of the node's <c>DataType</c> and no
/// other: NOT because the SDK checks it (it skips its own type check on a node with an <c>OnWriteValue</c>
/// handler, which every writable equipment node has) but because <c>OtOpcUaNodeManager.EvaluateEquipmentWriteStructure</c>
/// refuses an array whose element type does not match the node's DataType exactly (#610 review MAJOR-1). So
/// for every <c>Tag.DataType</c> the server can materialise, the node is advertised writable exactly when the
/// encoder accepts that array type.
/// </summary>
[Trait("Category", "Unit")]
public sealed class GalaxyArrayWriteSupportParityTests
@@ -50,14 +52,18 @@ public sealed class GalaxyArrayWriteSupportParityTests
encoderAccepts = false;
}
// UInt32 / UInt64 are a deliberate exclusion: CLR array covariance lets uint[] / ulong[] match the encoder's
// int[] / long[] arms, so it "accepts" them by REINTERPRETING the bits (uint.MaxValue → -1) — unlike the
// scalar arms, which range-check. A Galaxy attribute is never unsigned (DataTypeMap), so the table refuses
// rather than advertise a write that silently corrupts large values.
var expected = encoderAccepts && dataType is not ("UInt32" or "UInt64");
if (dataType is "UInt32" or "UInt64") encoderAccepts.ShouldBeTrue("the covariance hazard this exclusion documents");
DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dataType, "{\"FullName\":\"Obj.Attr\"}")
.ShouldBe(expected, dataType);
// UInt32 / UInt64 used to be a hand-written exclusion here, because CLR array covariance let uint[] /
// ulong[] match the encoder's int[] / long[] arms and be encoded by reinterpreting their bits. The encoder
// now refuses them itself, so the table and the encoder agree with no special case.
DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dataType, "{\"FullName\":\"Obj.Attr\",\"isArray\":true,\"arrayLength\":2}")
.ShouldBe(encoderAccepts, dataType);
}
/// <summary>Positive control: the theory above sees both answers, so it is not vacuously all-refused.</summary>
[Fact]
public void The_matrix_contains_both_answers() =>
ClrArrays.Keys
.Select(dt => DriverArrayWriteSupport.Supports(DriverTypeNames.Galaxy, dt, "{\"isArray\":true,\"arrayLength\":2}"))
.ToHashSet()
.ShouldBe(new[] { true, false }, ignoreOrder: true);
}