Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/IS7Plc.cs
T

191 lines
11 KiB
C#

using S7.Net;
using S7DataItem = global::S7.Net.Types.DataItem;
using S7NetDataType = global::S7.Net.DataType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
/// <summary>
/// Wire-layer abstraction over a single S7comm / ISO-on-TCP connection — the exact subset of
/// <see cref="Plc"/> members <see cref="S7Driver"/> uses. One instance per PLC, reused across
/// reads / writes / probes and serialized by the driver's single connection gate.
/// </summary>
/// <remarks>
/// The seam exists so <see cref="S7Driver.EnsureConnectedAsync"/> can dispose a dead handle and
/// re-open a fresh one after a transient PLC reboot / network blip — the reconnect path
/// — and so that path is unit-testable via a fake <see cref="IS7PlcFactory"/> without a
/// live PLC (S7.Net ships no in-process fake and <see cref="Plc"/> is <c>sealed</c>). Mirrors
/// the TwinCAT driver's <c>ITwinCATClient</c> / <c>ITwinCATClientFactory</c> pattern.
/// </remarks>
public interface IS7Plc : IDisposable
{
/// <summary>True when the underlying TCP connection is established.</summary>
bool IsConnected { get; }
/// <summary>Opens the S7 connection (ISO-on-TCP handshake). Honours the ambient read/write timeouts baked in at construction.</summary>
/// <param name="cancellationToken">Cancellation token for the connect attempt.</param>
/// <returns>A task that completes when the connection is established.</returns>
Task OpenAsync(CancellationToken cancellationToken);
/// <summary>Closes the S7 connection. Best-effort and idempotent.</summary>
void Close();
/// <summary>Reads a scalar value by S7 address string (e.g. <c>DB1.DBW0</c>); the boxed type follows the size suffix.</summary>
/// <param name="address">The S7 address string.</param>
/// <param name="cancellationToken">Cancellation token for the read.</param>
/// <returns>The boxed value, or <c>null</c> when the read produced no data.</returns>
Task<object?> ReadAsync(string address, CancellationToken cancellationToken);
/// <summary>Reads a contiguous byte block from an area — a single wire round-trip S7.Net chunks past the PDU limit.</summary>
/// <param name="area">The S7 area (DataBlock / Memory / Input / Output / Timer / Counter).</param>
/// <param name="db">Data-block number (0 for non-DB areas).</param>
/// <param name="startByteAdr">Start byte offset (or timer/counter number for those areas).</param>
/// <param name="count">Byte count to read.</param>
/// <param name="cancellationToken">Cancellation token for the read.</param>
/// <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>
/// <param name="cancellationToken">Cancellation token for the write.</param>
/// <returns>A task that completes when the write is acknowledged.</returns>
Task WriteAsync(string address, object value, CancellationToken cancellationToken);
/// <summary>Writes a contiguous byte block to an area (buffer-based wide/structured write).</summary>
/// <param name="area">The S7 area.</param>
/// <param name="db">Data-block number (0 for non-DB areas).</param>
/// <param name="startByteAdr">Start byte offset.</param>
/// <param name="value">The byte block to write.</param>
/// <param name="cancellationToken">Cancellation token for the write.</param>
/// <returns>A task that completes when the write is acknowledged.</returns>
Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken);
/// <summary>Reads the CPU status (Run/Stop) — the light-weight probe PDU. The result is discarded; success means the PLC answered.</summary>
/// <param name="cancellationToken">Cancellation token for the probe.</param>
/// <returns>A task that completes when the CPU status PDU round-trips.</returns>
Task ReadStatusAsync(CancellationToken cancellationToken);
}
/// <summary>Factory for <see cref="IS7Plc"/>s — one connection per PLC. Tests swap in a fake.</summary>
public interface IS7PlcFactory
{
/// <summary>Creates a new, not-yet-opened <see cref="IS7Plc"/> for the given target.</summary>
/// <param name="cpuType">The CPU family (drives the ISO-TSAP slot byte).</param>
/// <param name="host">PLC IP address or hostname.</param>
/// <param name="port">TCP port (102 on every S7 model).</param>
/// <param name="rack">Hardware rack number.</param>
/// <param name="slot">CPU slot.</param>
/// <param name="timeout">Connect + per-operation timeout, applied to the underlying read/write timeouts.</param>
/// <returns>A new, not-yet-opened <see cref="IS7Plc"/>.</returns>
IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout);
}
/// <summary>
/// Default <see cref="IS7Plc"/> — a thin adapter over a real S7.Net <see cref="Plc"/>. Bakes the
/// read/write timeouts into the connection at construction so
/// <see cref="S7Driver.EnsureConnectedAsync"/>'s reopen reproduces the exact same configuration.
/// </summary>
internal sealed class S7PlcAdapter : IS7Plc
{
private readonly Plc _plc;
private readonly TimeSpan _operationTimeout;
/// <summary>Initializes a new instance of the <see cref="S7PlcAdapter"/> class.</summary>
/// <param name="cpuType">The CPU family.</param>
/// <param name="host">PLC IP address or hostname.</param>
/// <param name="port">TCP port.</param>
/// <param name="rack">Hardware rack number.</param>
/// <param name="slot">CPU slot.</param>
/// <param name="timeout">Connect + per-operation timeout.</param>
public S7PlcAdapter(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout)
{
_plc = new Plc(S7CpuTypeMap.ToS7Net(cpuType), host, port, rack, slot);
_operationTimeout = timeout;
// S7netplus writes timeouts into the underlying TcpClient via ReadTimeout / WriteTimeout
// (milliseconds). Set before OpenAsync so the handshake itself honours the bound — matches
// the original inline construction in S7Driver.InitializeAsync. NOTE: these map to the
// TcpClient's ReceiveTimeout / SendTimeout, which govern only SYNCHRONOUS socket calls; the
// async read/write paths S7.Net actually uses ignore them, so every data-plane op below is
// additionally bounded by S7OperationDeadline (a real wall-clock ceiling). OpenAsync is left
// to S7Driver.EnsureConnectedAsync's own CancelAfter, so it is not double-bounded here.
var ms = (int)timeout.TotalMilliseconds;
_plc.ReadTimeout = ms;
_plc.WriteTimeout = ms;
}
/// <inheritdoc />
public bool IsConnected => _plc.IsConnected;
/// <inheritdoc />
public Task OpenAsync(CancellationToken cancellationToken) => _plc.OpenAsync(cancellationToken);
/// <inheritdoc />
public void Close() => _plc.Close();
/// <inheritdoc />
public Task<object?> ReadAsync(string address, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
t => _plc.ReadAsync(address, t)!, _operationTimeout, $"read of '{address}'", cancellationToken);
/// <inheritdoc />
public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
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(
t => _plc.WriteAsync(address, value, t), _operationTimeout, $"write of '{address}'", cancellationToken);
/// <inheritdoc />
public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
t => _plc.WriteBytesAsync(area, db, startByteAdr, value, t),
_operationTimeout, $"block write of {area} db{db} @{startByteAdr}", cancellationToken);
/// <inheritdoc />
public Task ReadStatusAsync(CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
t => _plc.ReadStatusAsync(t), _operationTimeout, "status read", cancellationToken);
/// <inheritdoc />
// Plc implements IDisposable explicitly, so cast to invoke it; disposal tears down the TcpClient.
public void Dispose() => ((IDisposable)_plc).Dispose();
}
/// <summary>Default <see cref="IS7PlcFactory"/> producing real S7.Net-backed <see cref="S7PlcAdapter"/>s.</summary>
internal sealed class S7PlcFactory : IS7PlcFactory
{
/// <inheritdoc />
public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout) =>
new S7PlcAdapter(cpuType, host, port, rack, slot, timeout);
}