diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs
index ca367d95..9e01f6f0 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs
@@ -1,4 +1,5 @@
using S7.Net;
+using S7DataItem = global::S7.Net.Types.DataItem;
using S7NetDataType = global::S7.Net.DataType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
@@ -43,6 +44,22 @@ public interface IS7Plc : IDisposable
/// The raw byte block.
Task ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken);
+ ///
+ /// Reads several individually-addressed items in ONE PDU round-trip — the multi-var request
+ /// the span planner's leftovers batch onto instead of issuing an address read apiece.
+ ///
+ ///
+ /// ⚠️ The CALLER owns chunking. S7.Net does NOT split a multi-var request: both the item
+ /// count and the total payload are bounded by the negotiated PDU size, and an oversized request
+ /// fails on the wire rather than being paged. Split the item list to PDU capacity before calling.
+ /// The library populates each in place and hands the same list
+ /// back; the returned list is the authoritative result and is ordered as supplied.
+ ///
+ /// The items to read; each carries its area / DB / offset / var-type and receives its value.
+ /// Cancellation token for the read.
+ /// The read items, in the supplied order, with their values populated.
+ Task> ReadMultipleVarsAsync(List items, CancellationToken cancellationToken);
+
/// Writes a scalar value by S7 address string; the value must be the unsigned wire type for the address size.
/// The S7 address string.
/// The wire-typed value to write.
@@ -132,6 +149,17 @@ internal sealed class S7PlcAdapter : IS7Plc
t => _plc.ReadBytesAsync(area, db, startByteAdr, count, t),
_operationTimeout, $"block read of {area} db{db} @{startByteAdr}+{count}", cancellationToken);
+ ///
+ public async Task> ReadMultipleVarsAsync(List items, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(items);
+ // S7.Net returns the SAME list with each item's Value populated; await + widen because
+ // Task> is not assignable to Task>.
+ return await S7OperationDeadline.RunAsync(
+ t => _plc.ReadMultipleVarsAsync(items, t),
+ _operationTimeout, $"multi-var read of {items.Count} item(s)", cancellationToken).ConfigureAwait(false);
+ }
+
///
public Task WriteAsync(string address, object value, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverReconnectTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverReconnectTests.cs
index 6fdabdfa..0faa1125 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverReconnectTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7DriverReconnectTests.cs
@@ -569,6 +569,10 @@ public sealed class S7DriverReconnectTests
public Task ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken) =>
throw new NotSupportedException("FakeS7Plc: block reads not needed for the reconnect tests");
+ public Task> ReadMultipleVarsAsync(
+ List items, CancellationToken cancellationToken) =>
+ throw new NotSupportedException("FakeS7Plc: multi-var reads not needed for the reconnect tests");
+
public async Task WriteAsync(string address, object value, CancellationToken cancellationToken)
{
if (WriteHangsUntilCancelled)
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs
index 9da4db9d..368229d5 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7PollEngineMigrationTests.cs
@@ -47,6 +47,10 @@ public sealed class S7PollEngineMigrationTests
return Task.FromResult(block);
}
+ public Task> ReadMultipleVarsAsync(
+ List items, CancellationToken ct)
+ => throw new NotSupportedException("ConstantArrayPlc: multi-var reads not needed for the poll-engine tests");
+
public Task WriteAsync(string address, object value, CancellationToken ct) => Task.CompletedTask;
public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken ct) => Task.CompletedTask;
public Task ReadStatusAsync(CancellationToken ct) => Task.CompletedTask;
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs
index 4f8361f6..4c82e3b2 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7SpanReadParityTests.cs
@@ -323,6 +323,11 @@ public sealed class S7SpanReadParityTests
return Task.FromResult(image[startByteAdr..(startByteAdr + count)]);
}
+ ///
+ public Task> ReadMultipleVarsAsync(
+ List items, CancellationToken ct)
+ => throw new NotSupportedException("ImageS7Plc: multi-var reads not served by the parity image fake");
+
///
public Task