feat(abcip): 1-D array read via libplctag + IsArray discovery

This commit is contained in:
Joseph Doherty
2026-06-16 21:55:20 -04:00
parent a82c22c645
commit f4d5a5ee9c
8 changed files with 408 additions and 15 deletions
@@ -135,6 +135,10 @@ 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>
public sealed record AbCipTagDefinition(
string Name,
string DeviceHostAddress,
@@ -143,7 +147,8 @@ public sealed record AbCipTagDefinition(
bool Writable = true,
bool WriteIdempotent = false,
IReadOnlyList<AbCipStructureMember>? Members = null,
bool SafetyTag = false);
bool SafetyTag = false,
int ElementCount = 1);
/// <summary>
/// One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
@@ -155,7 +160,8 @@ public sealed record AbCipStructureMember(
string Name,
AbCipDataType DataType,
bool Writable = true,
bool WriteIdempotent = false);
bool WriteIdempotent = false,
int ElementCount = 1);
/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
public enum AbCipPlcFamily
@@ -31,9 +31,13 @@ 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);
def = new AbCipTagDefinition(
Name: reference, DeviceHostAddress: deviceHostAddress, TagPath: tagPath,
DataType: dataType, Writable: true);
DataType: dataType, Writable: true, ElementCount: elementCount);
return true;
}
catch (JsonException) { return false; }
@@ -41,6 +45,23 @@ public static class AbCipEquipmentTagParser
catch (InvalidOperationException) { return false; }
}
/// <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.
/// </summary>
private static int ReadArrayElementCount(JsonElement o)
{
var isArray = o.TryGetProperty("isArray", out var a) && a.ValueKind == JsonValueKind.True;
if (!isArray) return 1;
if (o.TryGetProperty("arrayLength", out var len)
&& len.ValueKind == JsonValueKind.Number
&& len.TryGetInt32(out var n)
&& n > 1)
return n;
return 1;
}
private static TEnum ReadEnum<TEnum>(JsonElement o, string name, TEnum fallback) where TEnum : struct, Enum
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String
&& Enum.TryParse<TEnum>(e.GetString(), ignoreCase: true, out var v) ? v : fallback;