From 6ee553cf8f7682e5f5d551a6980aedb4b3b138f4 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 17 Aug 2026 03:48:15 -0400 Subject: [PATCH] feat(s7): expose ReadMultipleVarsAsync through the IS7Plc seam --- .../ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs | 28 +++++++++++++++++++ .../S7DriverReconnectTests.cs | 4 +++ .../S7PollEngineMigrationTests.cs | 4 +++ .../S7SpanReadParityTests.cs | 5 ++++ .../S7TestBuilders.cs | 4 +++ 5 files changed, 45 insertions(+) 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 ReadAsync(string address, CancellationToken ct) { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TestBuilders.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TestBuilders.cs index cb857905..6483fa5c 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TestBuilders.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/S7TestBuilders.cs @@ -92,6 +92,10 @@ internal sealed class ConnectingFakeS7PlcFactory : IS7PlcFactory public Task ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken ct) => throw new NotSupportedException("ConnectingFakeS7Plc serves no data"); /// Not exercised by these tests. + public Task> ReadMultipleVarsAsync( + List items, CancellationToken ct) + => throw new NotSupportedException("ConnectingFakeS7Plc serves no data"); + /// Not exercised by these tests. public Task WriteAsync(string address, object value, CancellationToken ct) => throw new NotSupportedException("ConnectingFakeS7Plc serves no data"); /// Not exercised by these tests.