feat(s7): expose ReadMultipleVarsAsync through the IS7Plc seam

This commit is contained in:
Joseph Doherty
2026-08-17 03:48:15 -04:00
parent c4d8e4a200
commit 6ee553cf8f
5 changed files with 45 additions and 0 deletions
@@ -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
/// <returns>The raw byte block.</returns>
Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken);
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// ⚠️ <b>The CALLER owns chunking.</b> 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 <see cref="S7DataItem.Value"/> in place and hands the same list
/// back; the returned list is the authoritative result and is ordered as supplied.
/// </remarks>
/// <param name="items">The items to read; each carries its area / DB / offset / var-type and receives its value.</param>
/// <param name="cancellationToken">Cancellation token for the read.</param>
/// <returns>The read items, in the supplied order, with their values populated.</returns>
Task<IReadOnlyList<S7DataItem>> ReadMultipleVarsAsync(List<S7DataItem> items, CancellationToken cancellationToken);
/// <summary>Writes a scalar value by S7 address string; the value must be the unsigned wire type for the address size.</summary>
/// <param name="address">The S7 address string.</param>
/// <param name="value">The wire-typed value to write.</param>
@@ -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);
/// <inheritdoc />
public async Task<IReadOnlyList<S7DataItem>> ReadMultipleVarsAsync(List<S7DataItem> items, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(items);
// S7.Net returns the SAME list with each item's Value populated; await + widen because
// Task<List<T>> is not assignable to Task<IReadOnlyList<T>>.
return await S7OperationDeadline.RunAsync(
t => _plc.ReadMultipleVarsAsync(items, t),
_operationTimeout, $"multi-var read of {items.Count} item(s)", cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public Task WriteAsync(string address, object value, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
@@ -569,6 +569,10 @@ public sealed class S7DriverReconnectTests
public Task<byte[]> 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<IReadOnlyList<global::S7.Net.Types.DataItem>> ReadMultipleVarsAsync(
List<global::S7.Net.Types.DataItem> 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)
@@ -47,6 +47,10 @@ public sealed class S7PollEngineMigrationTests
return Task.FromResult(block);
}
public Task<IReadOnlyList<global::S7.Net.Types.DataItem>> ReadMultipleVarsAsync(
List<global::S7.Net.Types.DataItem> 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;
@@ -323,6 +323,11 @@ public sealed class S7SpanReadParityTests
return Task.FromResult(image[startByteAdr..(startByteAdr + count)]);
}
/// <inheritdoc />
public Task<IReadOnlyList<global::S7.Net.Types.DataItem>> ReadMultipleVarsAsync(
List<global::S7.Net.Types.DataItem> items, CancellationToken ct)
=> throw new NotSupportedException("ImageS7Plc: multi-var reads not served by the parity image fake");
/// <inheritdoc />
public Task<object?> ReadAsync(string address, CancellationToken ct)
{
@@ -92,6 +92,10 @@ internal sealed class ConnectingFakeS7PlcFactory : IS7PlcFactory
public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken ct)
=> throw new NotSupportedException("ConnectingFakeS7Plc serves no data");
/// <summary>Not exercised by these tests.</summary>
public Task<IReadOnlyList<global::S7.Net.Types.DataItem>> ReadMultipleVarsAsync(
List<global::S7.Net.Types.DataItem> items, CancellationToken ct)
=> throw new NotSupportedException("ConnectingFakeS7Plc serves no data");
/// <summary>Not exercised by these tests.</summary>
public Task WriteAsync(string address, object value, CancellationToken ct)
=> throw new NotSupportedException("ConnectingFakeS7Plc serves no data");
/// <summary>Not exercised by these tests.</summary>