using S7.Net;
using S7DataItem = global::S7.Net.Types.DataItem;
using S7NetDataType = global::S7.Net.DataType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
///
/// Wire-layer abstraction over a single S7comm / ISO-on-TCP connection — the exact subset of
/// members uses. One instance per PLC, reused across
/// reads / writes / probes and serialized by the driver's single connection gate.
///
///
/// The seam exists so 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 without a
/// live PLC (S7.Net ships no in-process fake and is sealed ). Mirrors
/// the TwinCAT driver's ITwinCATClient / ITwinCATClientFactory pattern.
///
public interface IS7Plc : IDisposable
{
/// True when the underlying TCP connection is established.
bool IsConnected { get; }
/// Opens the S7 connection (ISO-on-TCP handshake). Honours the ambient read/write timeouts baked in at construction.
/// Cancellation token for the connect attempt.
/// A task that completes when the connection is established.
Task OpenAsync(CancellationToken cancellationToken);
/// Closes the S7 connection. Best-effort and idempotent.
void Close();
/// Reads a scalar value by S7 address string (e.g. DB1.DBW0 ); the boxed type follows the size suffix.
/// The S7 address string.
/// Cancellation token for the read.
/// The boxed value, or null when the read produced no data.
Task ReadAsync(string address, CancellationToken cancellationToken);
/// Reads a contiguous byte block from an area — a single wire round-trip S7.Net chunks past the PDU limit.
/// The S7 area (DataBlock / Memory / Input / Output / Timer / Counter).
/// Data-block number (0 for non-DB areas).
/// Start byte offset (or timer/counter number for those areas).
/// Byte count to read.
/// Cancellation token for the read.
/// 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.
/// Cancellation token for the write.
/// A task that completes when the write is acknowledged.
Task WriteAsync(string address, object value, CancellationToken cancellationToken);
/// Writes a contiguous byte block to an area (buffer-based wide/structured write).
/// The S7 area.
/// Data-block number (0 for non-DB areas).
/// Start byte offset.
/// The byte block to write.
/// Cancellation token for the write.
/// A task that completes when the write is acknowledged.
Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken);
/// Reads the CPU status (Run/Stop) — the light-weight probe PDU. The result is discarded; success means the PLC answered.
/// Cancellation token for the probe.
/// A task that completes when the CPU status PDU round-trips.
Task ReadStatusAsync(CancellationToken cancellationToken);
}
/// Factory for s — one connection per PLC. Tests swap in a fake.
public interface IS7PlcFactory
{
/// Creates a new, not-yet-opened for the given target.
/// The CPU family (drives the ISO-TSAP slot byte).
/// PLC IP address or hostname.
/// TCP port (102 on every S7 model).
/// Hardware rack number.
/// CPU slot.
/// Connect + per-operation timeout, applied to the underlying read/write timeouts.
/// A new, not-yet-opened .
IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout);
}
///
/// Default — a thin adapter over a real S7.Net . Bakes the
/// read/write timeouts into the connection at construction so
/// 's reopen reproduces the exact same configuration.
///
internal sealed class S7PlcAdapter : IS7Plc
{
private readonly Plc _plc;
private readonly TimeSpan _operationTimeout;
/// Initializes a new instance of the class.
/// The CPU family.
/// PLC IP address or hostname.
/// TCP port.
/// Hardware rack number.
/// CPU slot.
/// Connect + per-operation timeout.
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;
}
///
public bool IsConnected => _plc.IsConnected;
///
public Task OpenAsync(CancellationToken cancellationToken) => _plc.OpenAsync(cancellationToken);
///
public void Close() => _plc.Close();
///
public Task ReadAsync(string address, CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
t => _plc.ReadAsync(address, t)!, _operationTimeout, $"read of '{address}'", cancellationToken);
///
public Task 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);
///
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(
t => _plc.WriteAsync(address, value, t), _operationTimeout, $"write of '{address}'", cancellationToken);
///
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);
///
public Task ReadStatusAsync(CancellationToken cancellationToken) =>
S7OperationDeadline.RunAsync(
t => _plc.ReadStatusAsync(t), _operationTimeout, "status read", cancellationToken);
///
// Plc implements IDisposable explicitly, so cast to invoke it; disposal tears down the TcpClient.
public void Dispose() => ((IDisposable)_plc).Dispose();
}
/// Default producing real S7.Net-backed s.
internal sealed class S7PlcFactory : IS7PlcFactory
{
///
public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout) =>
new S7PlcAdapter(cpuType, host, port, rack, slot, timeout);
}