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:
@@ -668,7 +668,8 @@ public static class AddressSpaceComposer
|
||||
rawFolders, driverInstances, devices, tagGroups, tags, unsTagReferences,
|
||||
unsAreas, unsLines, equipment, topology);
|
||||
var unsReferenceVariables = BuildUnsReferenceVariables(
|
||||
unsTagReferences, tags, unsAreas, unsLines, equipment, topology, driverInstances);
|
||||
unsTagReferences, tags, unsAreas, unsLines, equipment, topology,
|
||||
BuildArrayWriteCapability(driverInstances, devices));
|
||||
|
||||
return new AddressSpaceComposition(areas, lines, nodes, plans, alarms)
|
||||
{
|
||||
@@ -767,7 +768,7 @@ public static class AddressSpaceComposer
|
||||
// TagId → the set of referencing equipment UNS folder paths (Area/Line/Equipment), for the
|
||||
// native-alarm multi-notifier. Distinct + ordinal-sorted for determinism.
|
||||
var equipPathByTag = BuildReferencingEquipmentPathsByTag(unsTagReferences, unsAreas, unsLines, equipment);
|
||||
var driverTypeById = BuildDriverTypeById(driverInstances);
|
||||
var arrayWritableFor = BuildArrayWriteCapability(driverInstances, devices);
|
||||
|
||||
var rawTags = new List<RawTagPlan>();
|
||||
foreach (var t in tags ?? Array.Empty<Tag>())
|
||||
@@ -779,8 +780,7 @@ public static class AddressSpaceComposer
|
||||
: topology.DevicePaths.GetValueOrDefault(t.DeviceId);
|
||||
var driverInstanceId = topology.DriverIdByDevice.GetValueOrDefault(t.DeviceId, string.Empty);
|
||||
var intent = TagConfigIntent.Parse(t.TagConfig);
|
||||
var arrayWritable = intent.IsArray && DriverArrayWriteSupport.Supports(
|
||||
driverTypeById.GetValueOrDefault(driverInstanceId), t.DataType, t.TagConfig);
|
||||
var arrayWritable = intent.IsArray && arrayWritableFor(t);
|
||||
var alarm = intent.Alarm is { } a
|
||||
? new EquipmentTagAlarmInfo(a.AlarmType, a.Severity, a.HistorizeToAveva)
|
||||
: null;
|
||||
@@ -808,12 +808,30 @@ public static class AddressSpaceComposer
|
||||
return (containers, rawTags);
|
||||
}
|
||||
|
||||
/// <summary><c>DriverInstanceId → DriverType</c>, for the per-driver array-write capability (Gitea #610).
|
||||
/// First row wins on a duplicate id, matching the composer's other id maps.</summary>
|
||||
private static IReadOnlyDictionary<string, string> BuildDriverTypeById(IReadOnlyList<DriverInstance> driverInstances) =>
|
||||
driverInstances
|
||||
/// <summary>
|
||||
/// The per-driver array-write capability (Gitea #610) as a per-tag predicate over the owning driver's
|
||||
/// <c>DriverType</c>, the driver's <c>DriverConfig</c> and the tag's own device's <c>DeviceConfig</c>
|
||||
/// (either config can carry a write cap such as Modbus <c>maxRegistersPerWrite</c>; the table takes the
|
||||
/// smaller). First row wins on a duplicate id, matching the composer's other id maps. Built once per
|
||||
/// compose and shared by the raw and UNS passes, so a mirror is writable exactly when its raw node is.
|
||||
/// </summary>
|
||||
private static Func<Tag, bool> BuildArrayWriteCapability(
|
||||
IReadOnlyList<DriverInstance> driverInstances, IReadOnlyList<Device>? devices)
|
||||
{
|
||||
var driverById = driverInstances
|
||||
.GroupBy(d => d.DriverInstanceId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First().DriverType, StringComparer.Ordinal);
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
var deviceById = (devices ?? Array.Empty<Device>())
|
||||
.GroupBy(d => d.DeviceId, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
return tag =>
|
||||
{
|
||||
var device = deviceById.GetValueOrDefault(tag.DeviceId);
|
||||
var driver = device is null ? null : driverById.GetValueOrDefault(device.DriverInstanceId);
|
||||
return DriverArrayWriteSupport.Supports(
|
||||
driver?.DriverType, tag.DataType, tag.TagConfig, driver?.DriverConfig, device?.DeviceConfig);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit the UNS reference Variables: one per <see cref="UnsTagReference"/> whose backing raw tag +
|
||||
@@ -829,11 +847,10 @@ public static class AddressSpaceComposer
|
||||
IReadOnlyList<UnsLine> unsLines,
|
||||
IReadOnlyList<Equipment> equipment,
|
||||
RawTopology topology,
|
||||
IReadOnlyList<DriverInstance> driverInstances)
|
||||
Func<Tag, bool> arrayWritableFor)
|
||||
{
|
||||
var refs = unsTagReferences ?? Array.Empty<UnsTagReference>();
|
||||
if (refs.Count == 0) return Array.Empty<UnsReferenceVariable>();
|
||||
var driverTypeById = BuildDriverTypeById(driverInstances);
|
||||
|
||||
var tagsById = (tags ?? Array.Empty<Tag>())
|
||||
.GroupBy(t => t.TagId, StringComparer.Ordinal)
|
||||
@@ -866,9 +883,7 @@ public static class AddressSpaceComposer
|
||||
var intent = TagConfigIntent.Parse(tag.TagConfig);
|
||||
// Same derivation as BuildRawSubtree's ArrayWritable, off the same row, so a mirror is writable
|
||||
// exactly when its raw node is.
|
||||
var backingArrayWritable = intent.IsArray && DriverArrayWriteSupport.Supports(
|
||||
driverTypeById.GetValueOrDefault(topology.DriverIdByDevice.GetValueOrDefault(tag.DeviceId, string.Empty)),
|
||||
tag.DataType, tag.TagConfig);
|
||||
var backingArrayWritable = intent.IsArray && arrayWritableFor(tag);
|
||||
|
||||
vars.Add(new UnsReferenceVariable(
|
||||
UnsTagReferenceId: r.UnsTagReferenceId,
|
||||
|
||||
Reference in New Issue
Block a user