feat(address-space): server-side array writes gated per driver capability (#610)
Array nodes were forced read-only for every driver. A new single authority, Core.Abstractions DriverArrayWriteSupport, says which (driver, element type) pairs encode a whole-array write: Modbus (Coils / numeric HoldingRegisters), S7 (Byte/Int16/UInt16/Int32/UInt32/Float32), OpcUaClient (pass-through) and Galaxy (Boolean/Int32/Int64/Float/Double/String/DateTime). Everything else stays read-only, so the server never advertises a write the driver refuses. - Composer derives RawTagPlan.ArrayWritable + UnsReferenceVariable .BackingArrayWritable at compose time (both seams, byte-parity); the applier's one writability rule is Writable && (!IsArray || ArrayWritable) for materialise and surgical paths, raw and UNS alike. - ArrayWritable is on the classifier's surgical raw-tag whitelist and mirrored on the UNS record (UnsReferenceBackingParityTests updated). - Node manager refuses a wrong-length / non-array value on an array node (BadTypeMismatch) and any IndexRange write (BadWriteNotSupported) inline, before dispatch; WriteOperate/ACL gates and the revert are unchanged. - Per-driver parity tests pin the table against the real Modbus write path, S7 EncodeArrayBlock and Galaxy MxValueEncoder (UInt32/UInt64 excluded: array covariance reinterprets their bits). - Over-the-wire proof in OpcUaServer.IntegrationTests (ArrayWriteWireTests). - ValueRank > 1 documented as out of scope by decision (docs/Uns.md). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+4
-2
@@ -49,7 +49,7 @@ public sealed class DeploymentArtifactRawUnsParityTests
|
||||
var profile = new Tag
|
||||
{
|
||||
TagId = "tag-profile", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Profile", DataType = "Float",
|
||||
AccessLevel = TagAccessLevel.Read, TagConfig = "{\"isArray\":true,\"arrayLength\":16}",
|
||||
AccessLevel = TagAccessLevel.Read, TagConfig = "{\"region\":\"HoldingRegisters\",\"address\":100,\"dataType\":\"Float32\",\"isArray\":true,\"arrayLength\":16}",
|
||||
};
|
||||
var tempHi = new Tag
|
||||
{
|
||||
@@ -82,7 +82,7 @@ public sealed class DeploymentArtifactRawUnsParityTests
|
||||
{
|
||||
new { TagId = "tag-speed", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Speed", DataType = "Float", AccessLevel = 1, TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"WW.Speed\"}" },
|
||||
new { TagId = "tag-run", DeviceId = "dev-1", TagGroupId = (string?)null, Name = "Run", DataType = "Boolean", AccessLevel = 0, TagConfig = "{}" },
|
||||
new { TagId = "tag-profile", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Profile", DataType = "Float", AccessLevel = 0, TagConfig = "{\"isArray\":true,\"arrayLength\":16}" },
|
||||
new { TagId = "tag-profile", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Profile", DataType = "Float", AccessLevel = 0, TagConfig = "{\"region\":\"HoldingRegisters\",\"address\":100,\"dataType\":\"Float32\",\"isArray\":true,\"arrayLength\":16}" },
|
||||
new { TagId = "tag-temphi", DeviceId = "dev-1", TagGroupId = (string?)null, Name = "TempHi", DataType = "Boolean", AccessLevel = 1, TagConfig = "{\"alarm\":{\"alarmType\":\"OffNormalAlarm\",\"severity\":700}}" },
|
||||
},
|
||||
UnsAreas = new[] { new { UnsAreaId = "a1", Name = "filling", ClusterId = "c1" } },
|
||||
@@ -106,6 +106,7 @@ public sealed class DeploymentArtifactRawUnsParityTests
|
||||
var composedArray = composed.RawTags.Single(t => t.Name == "Profile");
|
||||
composedArray.IsArray.ShouldBeTrue();
|
||||
composedArray.ArrayLength.ShouldBe(16u);
|
||||
composedArray.ArrayWritable.ShouldBeTrue(); // #610: a Modbus holding-register Float32 array is writable
|
||||
var composedAlarm = composed.RawTags.Single(t => t.Name == "TempHi");
|
||||
composedAlarm.Alarm.ShouldNotBeNull();
|
||||
composedAlarm.Alarm!.AlarmType.ShouldBe("OffNormalAlarm");
|
||||
@@ -123,6 +124,7 @@ public sealed class DeploymentArtifactRawUnsParityTests
|
||||
var composedProfileRef = composed.UnsReferenceVariables.Single(v => v.UnsTagReferenceId == "ref-2");
|
||||
composedProfileRef.BackingIsArray.ShouldBeTrue();
|
||||
composedProfileRef.BackingArrayLength.ShouldBe(16u);
|
||||
composedProfileRef.BackingArrayWritable.ShouldBeTrue();
|
||||
composedProfileRef.BackingIsHistorized.ShouldBeFalse();
|
||||
|
||||
// Byte-parity: the three dual-namespace node sets are record-equal between the two seams.
|
||||
|
||||
Reference in New Issue
Block a user