diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs
index 9dd8fccb..6b5005fc 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs
@@ -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.
///
+///
+/// Element count when the tag is a 1-D array; null (or <= 1) for a scalar.
+/// For an equipment tag this is threaded from the TagConfig JSON's arrayLength
+/// (honoured only when isArray is true) by . When
+/// set, the driver issues a single contiguous block read of
+/// ArrayCount × element-bytes from the tag's start address and decodes each element
+/// into an element-typed CLR array (short[] / int[] / float[] / etc.).
+///
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
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7EquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7EquipmentTagParser.cs
index f16c5780..8430335c 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7EquipmentTagParser.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7EquipmentTagParser.cs
@@ -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;
+
+ ///
+ /// Reads the optional 1-D array element count from the TagConfig blob. Returns the
+ /// arrayLength int ONLY when isArray is true AND arrayLength
+ /// is a positive JSON integer; null otherwise (scalar). Mirrors the byte-parity
+ /// contract of DeploymentArtifact.ExtractTagArray on the sink side.
+ ///
+ 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;
+ }
}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
index de1d15d6..dbb9a475 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using S7.Net;
+using S7NetDataType = global::S7.Net.DataType;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
@@ -440,6 +441,14 @@ public sealed class S7Driver
var addr = _parsedByName.TryGetValue(tag.Name, out var parsed)
? parsed
: S7AddressParser.Parse(tag.Address);
+
+ // Array path: a tag with a declared count > 1 reads a CONTIGUOUS block of
+ // count × element-bytes in a SINGLE round-trip (Plc.ReadBytesAsync), then decodes each
+ // element from its big-endian slice into an element-typed CLR array. The scalar path
+ // (count null / <= 1) is left byte-for-byte unchanged below.
+ if (tag.ArrayCount is > 1)
+ return await ReadArrayAsync(plc, tag, addr, ct).ConfigureAwait(false);
+
// S7.Net's string-based ReadAsync returns object where the boxed .NET type depends on
// the size suffix: DBX=bool, DBB=byte, DBW=ushort, DBD=uint. Our S7DataType enum
// specifies the SEMANTIC type (Int16 vs UInt16 vs Float32 etc.); the reinterpret below
@@ -451,6 +460,154 @@ public sealed class S7Driver
return ReinterpretRawValue(tag, addr, raw);
}
+ ///
+ /// Reads a 1-D array tag as ONE contiguous block (count × element-bytes) via
+ /// S7.Net's buffer-based Plc.ReadBytesAsync(DataType, db, startByteAdr, count, ct)
+ /// — a single PLC round-trip, NOT N string reads — then hands the raw byte block
+ /// to the pure decode loop. Timer/Counter areas are
+ /// already rejected at init, so only DB/M/I/Q reach here.
+ ///
+ private async Task