refactor(driver-s7): extract S7DriverOptions to .Contracts with parallel CpuType enum

Introduces Driver.S7.Contracts (dependency-free POCO project) and moves
S7DriverOptions / S7ProbeOptions / S7TagDefinition / S7DataType into it.
Adds S7CpuType enum mirroring S7.Net.CpuType exactly (7 values with
explicit integer codes). Runtime S7CpuTypeMap bridges S7CpuType →
S7.Net.CpuType at the single Plc construction site in S7Driver.InitializeAsync.
S7DriverFactoryExtensions and S7CommandBase updated to use S7CpuType; test
files updated to match (S7_1500Profile, S7DriverScaffoldTests). AdminUI can
now reference Driver.S7.Contracts without pulling in S7netplus.
This commit is contained in:
Joseph Doherty
2026-05-28 09:08:27 -04:00
parent a88721ce31
commit 9f62f2c242
11 changed files with 57 additions and 13 deletions
@@ -0,0 +1,18 @@
using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
internal static class S7CpuTypeMap
{
public static S7NetCpuType ToS7Net(S7CpuType type) => type switch
{
S7CpuType.S7200 => S7NetCpuType.S7200,
S7CpuType.Logo0BA8 => S7NetCpuType.Logo0BA8,
S7CpuType.S7200Smart => S7NetCpuType.S7200Smart,
S7CpuType.S7300 => S7NetCpuType.S7300,
S7CpuType.S7400 => S7NetCpuType.S7400,
S7CpuType.S71200 => S7NetCpuType.S71200,
S7CpuType.S71500 => S7NetCpuType.S71500,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
@@ -138,7 +138,7 @@ public sealed class S7Driver(S7DriverOptions options, string driverInstanceId, I
// type is wired through.
RejectUnsupportedTagDataTypes();
var plc = new Plc(_options.CpuType, _options.Host, _options.Port, _options.Rack, _options.Slot);
var plc = new Plc(S7CpuTypeMap.ToS7Net(_options.CpuType), _options.Host, _options.Port, _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.
@@ -1,7 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
@@ -60,8 +59,8 @@ public static class S7DriverFactoryExtensions
{
Host = dto.Host!,
Port = dto.Port ?? 102,
CpuType = ParseEnum<S7NetCpuType>(dto.CpuType, driverInstanceId, "CpuType",
fallback: S7NetCpuType.S71500),
CpuType = ParseEnum<S7CpuType>(dto.CpuType, driverInstanceId, "CpuType",
fallback: S7CpuType.S71500),
Rack = dto.Rack ?? 0,
Slot = dto.Slot ?? 0,
Timeout = TimeSpan.FromMilliseconds(dto.TimeoutMs ?? 5_000),
@@ -1,121 +0,0 @@
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 <c>S7.Net.Plc.ReadStatusAsync</c> (a CPU-status PDU) 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
{
/// <summary>Gets or sets a value indicating whether probing is enabled.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Gets or sets the probe interval.</summary>
public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
/// <summary>Gets or sets the probe timeout.</summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
// Driver.S7-012: ProbeAddress was configured and documented but was never read by the
// probe loop. ProbeLoopAsync uses S7.Net's ReadStatusAsync (a CPU-status PDU), not a
// DB/Merker read — it does not consume an explicit address. Rather than ship dead config
// surface, ProbeAddress has been removed. The liveness check is purely ReadStatusAsync-based.
}
/// <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>
/// <param name="WriteIdempotent">
/// Per <c>docs/v2/plan.md</c> decisions #44, #45, #143 — flag a tag as safe to replay on
/// write timeout / failure. Default <c>false</c>; writes do not auto-retry. Safe candidates
/// on S7: DB word/dword set-points holding analog values, configuration DBs where the same
/// value can be written again without side-effects. Unsafe: M (merker) bits or Q (output)
/// coils that drive edge-triggered routines in the PLC program.
/// </param>
public sealed record S7TagDefinition(
string Name,
string Address,
S7DataType DataType,
bool Writable = true,
int StringLength = 254,
bool WriteIdempotent = false);
public enum S7DataType
{
Bool,
Byte,
Int16,
UInt16,
Int32,
UInt32,
Int64,
UInt64,
Float32,
Float64,
String,
DateTime,
}
@@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core.Abstractions\ZB.MOM.WW.OtOpcUa.Core.Abstractions.csproj"/>
<ProjectReference Include="..\..\Core\ZB.MOM.WW.OtOpcUa.Core\ZB.MOM.WW.OtOpcUa.Core.csproj"/>
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts\ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts.csproj"/>
</ItemGroup>
<ItemGroup>