fix(abcip): explicit IsArray flag so 1-element arrays read as arrays (review I-1)
This commit is contained in:
@@ -135,10 +135,14 @@ public sealed record AbCipDeviceOptions(
|
||||
/// GuardLogix controller; non-safety writes violate the safety-partition isolation and are
|
||||
/// rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the
|
||||
/// write attempt failing at runtime.</param>
|
||||
/// <param name="ElementCount">Phase 4c — number of array elements for a 1-D array tag. Defaults
|
||||
/// to 1 (scalar). When greater than 1 the tag discovers as an OPC UA array node
|
||||
/// (<c>IsArray</c> + <c>ArrayDim</c>) and reads via libplctag's <c>elem_count</c> into an
|
||||
/// element-typed CLR array. Ignored for <see cref="AbCipDataType.Structure"/>.</param>
|
||||
/// <param name="ElementCount">Phase 4c — number of array elements for a 1-D array tag. Reads via
|
||||
/// libplctag's <c>elem_count</c> into an element-typed CLR array when <paramref name="IsArray"/>
|
||||
/// is set; <c>1</c> for a scalar. Ignored for <see cref="AbCipDataType.Structure"/>.</param>
|
||||
/// <param name="IsArray">Review I-1 — the EXPLICIT array signal. <c>true</c> ⟺ the source TagConfig
|
||||
/// had <c>isArray:true</c> (with <c>arrayLength >= 1</c>); the tag discovers as an OPC UA
|
||||
/// array node (<c>IsArray</c> + <c>ArrayDim</c>) and reads as a typed CLR array — even when
|
||||
/// <paramref name="ElementCount"/> is 1 (a valid 1-element array). <c>ElementCount</c> alone
|
||||
/// cannot carry this because a scalar and a 1-element array both have a count of 1.</param>
|
||||
public sealed record AbCipTagDefinition(
|
||||
string Name,
|
||||
string DeviceHostAddress,
|
||||
@@ -148,7 +152,8 @@ public sealed record AbCipTagDefinition(
|
||||
bool WriteIdempotent = false,
|
||||
IReadOnlyList<AbCipStructureMember>? Members = null,
|
||||
bool SafetyTag = false,
|
||||
int ElementCount = 1);
|
||||
int ElementCount = 1,
|
||||
bool IsArray = false);
|
||||
|
||||
/// <summary>
|
||||
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
|
||||
@@ -156,12 +161,20 @@ public sealed record AbCipTagDefinition(
|
||||
/// <see cref="AbCipTagDefinition"/>. Declaration-driven — the real CIP Template Object reader
|
||||
/// (class 0x6C) that would auto-discover member layouts lands as a follow-up PR.
|
||||
/// </summary>
|
||||
/// <param name="Name">The member identifier on the PLC.</param>
|
||||
/// <param name="DataType">The atomic Logix type of the member.</param>
|
||||
/// <param name="Writable">Whether the member is writable.</param>
|
||||
/// <param name="WriteIdempotent">Whether writes to the member are idempotent.</param>
|
||||
/// <param name="ElementCount">Number of array elements for a 1-D array member; <c>1</c> for scalar.</param>
|
||||
/// <param name="IsArray">Review I-1 — the EXPLICIT array signal for a member: <c>true</c> ⟺ the
|
||||
/// member is a 1-D array (even of length 1). Discovers as an OPC UA array node when set.</param>
|
||||
public sealed record AbCipStructureMember(
|
||||
string Name,
|
||||
AbCipDataType DataType,
|
||||
bool Writable = true,
|
||||
bool WriteIdempotent = false,
|
||||
int ElementCount = 1);
|
||||
int ElementCount = 1,
|
||||
bool IsArray = false);
|
||||
|
||||
/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
|
||||
public enum AbCipPlcFamily
|
||||
|
||||
@@ -31,13 +31,15 @@ public static class AbCipEquipmentTagParser
|
||||
|
||||
var deviceHostAddress = ReadString(root, "deviceHostAddress");
|
||||
var dataType = ReadEnum(root, "dataType", AbCipDataType.DInt);
|
||||
// Phase 4c — an isArray equipment tag carries arrayLength; thread it into the def's
|
||||
// ElementCount so the read pulls the whole array via libplctag elem_count. When
|
||||
// isArray is absent/false (or arrayLength is missing/<=1) the tag stays scalar.
|
||||
var elementCount = ReadArrayElementCount(root);
|
||||
// Review I-1 — an equipment tag is an ARRAY ⟺ isArray:true AND arrayLength >= 1. A
|
||||
// 1-element array (isArray:true, arrayLength:1) is a VALID 1-element array — the
|
||||
// foundation materialises a [1] OPC UA array node — so it must read as an array, not a
|
||||
// scalar. ElementCount can't carry the signal (a scalar and a 1-element array both
|
||||
// have a count of 1), so the explicit IsArray flag does.
|
||||
var (isArray, elementCount) = ReadArrayShape(root);
|
||||
def = new AbCipTagDefinition(
|
||||
Name: reference, DeviceHostAddress: deviceHostAddress, TagPath: tagPath,
|
||||
DataType: dataType, Writable: true, ElementCount: elementCount);
|
||||
DataType: dataType, Writable: true, ElementCount: elementCount, IsArray: isArray);
|
||||
return true;
|
||||
}
|
||||
catch (JsonException) { return false; }
|
||||
@@ -46,20 +48,22 @@ public static class AbCipEquipmentTagParser
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the 1-D array element count from an <c>isArray</c> / <c>arrayLength</c> pair.
|
||||
/// Returns 1 (scalar) unless <c>isArray</c> is truthy AND <c>arrayLength</c> is a number
|
||||
/// greater than 1; matches the sink's "isArray + arrayLength" carrier.
|
||||
/// Resolve the 1-D array shape from an <c>isArray</c> / <c>arrayLength</c> pair (the foundation
|
||||
/// contract carrier). The tag is an ARRAY ⟺ <c>isArray</c> is truthy AND <c>arrayLength</c>
|
||||
/// is a number <c>>= 1</c> (a 1-element array is valid). Returns
|
||||
/// <c>(IsArray: false, ElementCount: 1)</c> for a scalar.
|
||||
/// </summary>
|
||||
private static int ReadArrayElementCount(JsonElement o)
|
||||
private static (bool IsArray, int ElementCount) ReadArrayShape(JsonElement o)
|
||||
{
|
||||
var isArray = o.TryGetProperty("isArray", out var a) && a.ValueKind == JsonValueKind.True;
|
||||
if (!isArray) return 1;
|
||||
if (!isArray) return (false, 1);
|
||||
if (o.TryGetProperty("arrayLength", out var len)
|
||||
&& len.ValueKind == JsonValueKind.Number
|
||||
&& len.TryGetInt32(out var n)
|
||||
&& n > 1)
|
||||
return n;
|
||||
return 1;
|
||||
&& n >= 1)
|
||||
return (true, n);
|
||||
// isArray:true but arrayLength missing/invalid — treat as a 1-element array (count 1).
|
||||
return (true, 1);
|
||||
}
|
||||
|
||||
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
|
||||
|
||||
Reference in New Issue
Block a user