feat(s7): 1-D array block read + decode loop + IsArray discovery

This commit is contained in:
Joseph Doherty
2026-06-16 21:54:50 -04:00
parent 8d3dc32148
commit a82c22c645
4 changed files with 468 additions and 4 deletions
@@ -104,13 +104,22 @@ public sealed class S7ProbeOptions
/// value can be written again without side-effects. Unsafe: M (merker) bits or Q (output)
/// coils that drive edge-triggered routines in the PLC program.
/// </param>
/// <param name="ArrayCount">
/// Element count when the tag is a 1-D array; <c>null</c> (or <c>&lt;= 1</c>) for a scalar.
/// For an equipment tag this is threaded from the <c>TagConfig</c> JSON's <c>arrayLength</c>
/// (honoured only when <c>isArray</c> is true) by <see cref="S7EquipmentTagParser"/>. When
/// set, the driver issues a single contiguous block read of
/// <c>ArrayCount × element-bytes</c> from the tag's start address and decodes each element
/// into an element-typed CLR array (<c>short[]</c> / <c>int[]</c> / <c>float[]</c> / etc.).
/// </param>
public sealed record S7TagDefinition(
string Name,
string Address,
S7DataType DataType,
bool Writable = true,
int StringLength = 254,
bool WriteIdempotent = false);
bool WriteIdempotent = false,
int? ArrayCount = null);
public enum S7DataType
{
@@ -36,12 +36,18 @@ public static class S7EquipmentTagParser
// Range-guard rather than truncate: an S7 string can't exceed 254 chars, and a
// negative length is meaningless — reject so a malformed blob can't slip through.
if (stringLength < 0 || stringLength > MaxStringLength) return false;
// Array intent: the canonical sink-side parse (DeploymentArtifact.ExtractTagArray)
// honours arrayLength ONLY when isArray is true AND the prop is a JSON number — mirror
// that here so the driver's transient def agrees byte-for-byte with the materialised
// OPC UA node's ValueRank/ArrayDimensions. Absent / isArray=false ⇒ null (scalar).
var arrayCount = ReadArrayCount(root);
def = new S7TagDefinition(
Name: reference,
Address: address,
DataType: dataType,
Writable: true, // node-level authz governs writes
StringLength: stringLength == 0 ? MaxStringLength : stringLength);
StringLength: stringLength == 0 ? MaxStringLength : stringLength,
ArrayCount: arrayCount);
return true;
}
catch (JsonException) { return false; }
@@ -56,4 +62,26 @@ public static class S7EquipmentTagParser
private static int ReadInt(JsonElement o, string name)
=> o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.Number
&& e.TryGetInt32(out var v) ? v : 0;
/// <summary>
/// Reads the optional 1-D array element count from the TagConfig blob. Returns the
/// <c>arrayLength</c> int ONLY when <c>isArray</c> is <c>true</c> AND <c>arrayLength</c>
/// is a positive JSON integer; <c>null</c> otherwise (scalar). Mirrors the byte-parity
/// contract of <c>DeploymentArtifact.ExtractTagArray</c> on the sink side.
/// </summary>
private static int? ReadArrayCount(JsonElement root)
{
var isArray = root.TryGetProperty("isArray", out var aEl)
&& (aEl.ValueKind == JsonValueKind.True || aEl.ValueKind == JsonValueKind.False)
&& aEl.GetBoolean();
if (!isArray) return null;
if (root.TryGetProperty("arrayLength", out var lEl)
&& lEl.ValueKind == JsonValueKind.Number
&& lEl.TryGetInt32(out var len)
&& len > 0)
{
return len;
}
return null;
}
}