120 lines
5.2 KiB
C#
120 lines
5.2 KiB
C#
using S7.Net;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
|
|
|
/// <summary>
|
|
/// Siemens S7 native driver — speaks S7comm over ISO-on-TCP (port 102) via the S7netplus
|
|
/// library. First implementation of <see cref="IDriver"/> for an in-process .NET Standard
|
|
/// PLC protocol that is NOT Modbus, validating that the v2 driver-capability interfaces
|
|
/// generalize beyond Modbus + Galaxy.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// PR 62 ships the scaffold: <see cref="IDriver"/> only (Initialize / Reinitialize /
|
|
/// Shutdown / GetHealth). <see cref="ITagDiscovery"/>, <see cref="IReadable"/>,
|
|
/// <see cref="IWritable"/>, <see cref="ISubscribable"/>, <see cref="IHostConnectivityProbe"/>
|
|
/// land in PRs 63-65 once the address parser (PR 63) is in place.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Single-connection policy</b>: S7netplus documented pattern is one
|
|
/// <c>Plc</c> instance per PLC, serialized with a <see cref="SemaphoreSlim"/>.
|
|
/// Parallelising reads against a single S7 CPU doesn't help — the CPU scans the
|
|
/// communication mailbox at most once per cycle (2-10 ms) and queues concurrent
|
|
/// requests wire-side anyway. Multiple client-side connections just waste the CPU's
|
|
/// 8-64 connection-resource budget.
|
|
/// </para>
|
|
/// </remarks>
|
|
public sealed class S7Driver(S7DriverOptions options, string driverInstanceId)
|
|
: IDriver, IDisposable, IAsyncDisposable
|
|
{
|
|
private readonly S7DriverOptions _options = options;
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
|
|
/// <summary>
|
|
/// Per-connection gate. Internal so PRs 63-65 (read/write/subscribe) can serialize on
|
|
/// the same semaphore without exposing it publicly. Single-connection-per-PLC is a
|
|
/// hard requirement of S7netplus — see class remarks.
|
|
/// </summary>
|
|
internal SemaphoreSlim Gate => _gate;
|
|
|
|
/// <summary>
|
|
/// Active S7.Net PLC connection. Null until <see cref="InitializeAsync"/> returns; null
|
|
/// after <see cref="ShutdownAsync"/>. Read-only outside this class; PR 64's Read/Write
|
|
/// will take the <see cref="_gate"/> before touching it.
|
|
/// </summary>
|
|
internal Plc? Plc { get; private set; }
|
|
|
|
private DriverHealth _health = new(DriverState.Unknown, null, null);
|
|
private bool _disposed;
|
|
|
|
public string DriverInstanceId => driverInstanceId;
|
|
public string DriverType => "S7";
|
|
|
|
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
|
{
|
|
_health = new DriverHealth(DriverState.Initializing, null, null);
|
|
try
|
|
{
|
|
var plc = new Plc(_options.CpuType, _options.Host, _options.Rack, _options.Slot);
|
|
// S7netplus writes timeouts into the underlying TcpClient via Plc.WriteTimeout /
|
|
// Plc.ReadTimeout (milliseconds). Set before OpenAsync so the handshake itself
|
|
// honours the bound.
|
|
plc.WriteTimeout = (int)_options.Timeout.TotalMilliseconds;
|
|
plc.ReadTimeout = (int)_options.Timeout.TotalMilliseconds;
|
|
|
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
cts.CancelAfter(_options.Timeout);
|
|
await plc.OpenAsync(cts.Token).ConfigureAwait(false);
|
|
|
|
Plc = plc;
|
|
_health = new DriverHealth(DriverState.Healthy, DateTime.UtcNow, null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Clean up a partially-constructed Plc so a retry from the caller doesn't leak
|
|
// the TcpClient. S7netplus's Close() is best-effort and idempotent.
|
|
try { Plc?.Close(); } catch { }
|
|
Plc = null;
|
|
_health = new DriverHealth(DriverState.Faulted, null, ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
|
|
{
|
|
await ShutdownAsync(cancellationToken).ConfigureAwait(false);
|
|
await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public Task ShutdownAsync(CancellationToken cancellationToken)
|
|
{
|
|
try { Plc?.Close(); } catch { /* best-effort — tearing down anyway */ }
|
|
Plc = null;
|
|
_health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public DriverHealth GetHealth() => _health;
|
|
|
|
/// <summary>
|
|
/// Approximate memory footprint. The Plc instance + one 240-960 byte PDU buffer is
|
|
/// under 4 KB; return 0 because the <see cref="IDriver"/> contract asks for a
|
|
/// driver-attributable growth number and S7.Net doesn't expose one.
|
|
/// </summary>
|
|
public long GetMemoryFootprint() => 0;
|
|
|
|
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
try { await ShutdownAsync(CancellationToken.None).ConfigureAwait(false); }
|
|
catch { /* disposal is best-effort */ }
|
|
_gate.Dispose();
|
|
}
|
|
}
|