Phase 3 PR 62 -- Siemens S7 native driver project scaffold #61
@@ -9,6 +9,7 @@
|
|||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ZB.MOM.WW.OtOpcUa.Driver.Modbus.csproj"/>
|
||||||
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Driver.S7/ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.Shared/ZB.MOM.WW.OtOpcUa.Client.Shared.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj"/>
|
<Project Path="src/ZB.MOM.WW.OtOpcUa.Client.UI/ZB.MOM.WW.OtOpcUa.Client.UI.csproj"/>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.Modbus.IntegrationTests.csproj"/>
|
||||||
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests/ZB.MOM.WW.OtOpcUa.Driver.S7.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests/ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests/ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj"/>
|
<Project Path="tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests/ZB.MOM.WW.OtOpcUa.Client.UI.Tests.csproj"/>
|
||||||
|
|||||||
119
src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
Normal file
119
src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs
Normal file
112
src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverOptions.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using S7NetCpuType = global::S7.Net.CpuType;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Siemens S7 native (S7comm / ISO-on-TCP port 102) driver configuration. Bound from the
|
||||||
|
/// driver's <c>DriverConfig</c> JSON at <c>DriverHost.RegisterAsync</c>. Unlike the Modbus
|
||||||
|
/// driver the S7 driver uses the PLC's *native* protocol — port 102 ISO-on-TCP rather
|
||||||
|
/// than Modbus's 502, and S7-specific area codes (DB, M, I, Q) rather than holding-
|
||||||
|
/// register / coil tables.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The driver requires <b>PUT/GET communication enabled</b> in the TIA Portal
|
||||||
|
/// hardware config for S7-1200/1500. The factory default disables PUT/GET access,
|
||||||
|
/// so a driver configured against a freshly-flashed CPU will see a hard error
|
||||||
|
/// (S7.Net surfaces it as <c>Plc.ReadAsync</c> returning <c>ErrorCode.Accessing</c>).
|
||||||
|
/// The driver maps that specifically to <c>BadNotSupported</c> and flags it as a
|
||||||
|
/// configuration alert rather than a transient fault — blind Polly retry is wasted
|
||||||
|
/// effort when the PLC will keep refusing every request.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// See <c>docs/v2/driver-specs.md</c> §5 for the full specification.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class S7DriverOptions
|
||||||
|
{
|
||||||
|
/// <summary>PLC IP address or hostname.</summary>
|
||||||
|
public string Host { get; init; } = "127.0.0.1";
|
||||||
|
|
||||||
|
/// <summary>TCP port. ISO-on-TCP is 102 on every S7 model; override only for unusual NAT setups.</summary>
|
||||||
|
public int Port { get; init; } = 102;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CPU family. Determines the ISO-TSAP slot byte that S7.Net uses during connection
|
||||||
|
/// setup — pick the family that matches the target PLC exactly.
|
||||||
|
/// </summary>
|
||||||
|
public S7NetCpuType CpuType { get; init; } = S7NetCpuType.S71500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hardware rack number. Almost always 0; relevant only for distributed S7-400 racks
|
||||||
|
/// with multiple CPUs.
|
||||||
|
/// </summary>
|
||||||
|
public short Rack { get; init; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CPU slot. Conventions per family: S7-300 = slot 2, S7-400 = slot 2 or 3,
|
||||||
|
/// S7-1200 / S7-1500 = slot 0 (onboard PN). S7.Net uses this to build the remote
|
||||||
|
/// TSAP. Wrong slot → connection refused during handshake.
|
||||||
|
/// </summary>
|
||||||
|
public short Slot { get; init; } = 0;
|
||||||
|
|
||||||
|
/// <summary>Connect + per-operation timeout.</summary>
|
||||||
|
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>Pre-declared tag map. S7 has a symbol-table protocol but S7.Net does not expose it, so the driver operates off a static tag list configured per-site. Address grammar documented in S7AddressParser (PR 63).</summary>
|
||||||
|
public IReadOnlyList<S7TagDefinition> Tags { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Background connectivity-probe settings. When enabled, the driver runs a tick loop
|
||||||
|
/// that issues a cheap read against <see cref="S7ProbeOptions.ProbeAddress"/> every
|
||||||
|
/// <see cref="S7ProbeOptions.Interval"/> and raises <c>OnHostStatusChanged</c> on
|
||||||
|
/// Running ↔ Stopped transitions.
|
||||||
|
/// </summary>
|
||||||
|
public S7ProbeOptions Probe { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class S7ProbeOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; init; } = true;
|
||||||
|
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
|
||||||
|
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Address to probe for liveness. DB1.DBW0 is the convention if the PLC project
|
||||||
|
/// reserves a small fingerprint DB for health checks (per <c>docs/v2/s7.md</c>);
|
||||||
|
/// if not, pick any valid Merker word like <c>MW0</c>.
|
||||||
|
/// </summary>
|
||||||
|
public string ProbeAddress { get; init; } = "MW0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One S7 variable as exposed by the driver. Addresses use S7.Net syntax — see
|
||||||
|
/// <c>S7AddressParser</c> (PR 63) for the grammar.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Name">Tag name; OPC UA browse name + driver full reference.</param>
|
||||||
|
/// <param name="Address">S7 address string, e.g. <c>DB1.DBW0</c>, <c>M0.0</c>, <c>I0.0</c>, <c>QD4</c>. Grammar documented in <c>S7AddressParser</c> (PR 63).</param>
|
||||||
|
/// <param name="DataType">Logical data type — drives the underlying S7.Net read/write width.</param>
|
||||||
|
/// <param name="Writable">When true the driver accepts writes for this tag.</param>
|
||||||
|
/// <param name="StringLength">For <c>DataType = String</c>: S7-string max length. Default 254 (S7 max).</param>
|
||||||
|
public sealed record S7TagDefinition(
|
||||||
|
string Name,
|
||||||
|
string Address,
|
||||||
|
S7DataType DataType,
|
||||||
|
bool Writable = true,
|
||||||
|
int StringLength = 254);
|
||||||
|
|
||||||
|
public enum S7DataType
|
||||||
|
{
|
||||||
|
Bool,
|
||||||
|
Byte,
|
||||||
|
Int16,
|
||||||
|
UInt16,
|
||||||
|
Int32,
|
||||||
|
UInt32,
|
||||||
|
Int64,
|
||||||
|
UInt64,
|
||||||
|
Float32,
|
||||||
|
Float64,
|
||||||
|
String,
|
||||||
|
DateTime,
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.S7</RootNamespace>
|
||||||
|
<AssemblyName>ZB.MOM.WW.OtOpcUa.Driver.S7</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="S7netplus" Version="0.20.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ZB.MOM.WW.OtOpcUa.Driver.S7.Tests"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scaffold-level tests that don't need a live S7 PLC — exercise driver lifecycle shape,
|
||||||
|
/// default option values, and failure-mode transitions. PR 64 adds IReadable/IWritable
|
||||||
|
/// tests against a mock-server, PR 65 adds discovery + subscribe.
|
||||||
|
/// </summary>
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public sealed class S7DriverScaffoldTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Default_options_target_S7_1500_slot_0_on_port_102()
|
||||||
|
{
|
||||||
|
var opts = new S7DriverOptions();
|
||||||
|
opts.Port.ShouldBe(102, "ISO-on-TCP is always 102 for S7; documented in driver-specs.md §5");
|
||||||
|
opts.CpuType.ShouldBe(global::S7.Net.CpuType.S71500);
|
||||||
|
opts.Rack.ShouldBe((short)0);
|
||||||
|
opts.Slot.ShouldBe((short)0, "S7-1200/1500 onboard PN ports are slot 0 by convention");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Default_probe_interval_is_reasonable_for_S7_scan_cycle()
|
||||||
|
{
|
||||||
|
// S7 PLCs scan 2-10 ms but comms mailbox typically processed once per scan.
|
||||||
|
// 5 s default probe is lightweight — ~0.001% of comms budget.
|
||||||
|
new S7ProbeOptions().Interval.ShouldBe(TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tag_definition_defaults_to_writable_with_S7_max_string_length()
|
||||||
|
{
|
||||||
|
var tag = new S7TagDefinition("T", "DB1.DBW0", S7DataType.Int16);
|
||||||
|
tag.Writable.ShouldBeTrue();
|
||||||
|
tag.StringLength.ShouldBe(254, "S7 STRING type max length is 254 chars");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Driver_instance_reports_type_and_id_before_connect()
|
||||||
|
{
|
||||||
|
var opts = new S7DriverOptions { Host = "127.0.0.1" };
|
||||||
|
using var drv = new S7Driver(opts, "s7-test");
|
||||||
|
drv.DriverType.ShouldBe("S7");
|
||||||
|
drv.DriverInstanceId.ShouldBe("s7-test");
|
||||||
|
drv.GetHealth().State.ShouldBe(DriverState.Unknown, "health starts Unknown until InitializeAsync runs");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Initialize_against_unreachable_host_transitions_to_Faulted_and_throws()
|
||||||
|
{
|
||||||
|
// Pick an RFC 5737 reserved-for-documentation IP so the connect attempt fails fast
|
||||||
|
// (no DNS mismatch, no accidental traffic to a real PLC).
|
||||||
|
var opts = new S7DriverOptions { Host = "192.0.2.1", Timeout = TimeSpan.FromMilliseconds(250) };
|
||||||
|
using var drv = new S7Driver(opts, "s7-unreach");
|
||||||
|
|
||||||
|
await Should.ThrowAsync<Exception>(async () =>
|
||||||
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken));
|
||||||
|
|
||||||
|
var health = drv.GetHealth();
|
||||||
|
health.State.ShouldBe(DriverState.Faulted, "unreachable host must flip the driver to Faulted so operators see it");
|
||||||
|
health.LastError.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.S7.Tests</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="xunit.v3" Version="1.1.0"/>
|
||||||
|
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Driver.S7\ZB.MOM.WW.OtOpcUa.Driver.S7.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-37gx-xxp4-5rgx"/>
|
||||||
|
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-w3x6-4m5h-cxqf"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user