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); }