Per-tag opt-in for write-retry per docs/v2/plan.md decisions #44, #45, #143. Default is false — writes never auto-retry unless the driver author has marked the tag as safe to replay. Core.Abstractions: - DriverAttributeInfo gains `bool WriteIdempotent = false` at the end of the positional record (back-compatible; every existing call site uses the default). Driver.Modbus: - ModbusTagDefinition gains `bool WriteIdempotent = false`. Safe candidates documented in the param XML: holding-register set-points, configuration registers. Unsafe: edge-triggered coils, counter-increment addresses. - ModbusDriver.DiscoverAsync propagates t.WriteIdempotent into DriverAttributeInfo.WriteIdempotent. Driver.S7: - S7TagDefinition gains `bool WriteIdempotent = false`. Safe candidates: DB word/dword set-points, configuration DBs. Unsafe: M/Q bits that drive edge-triggered program routines. - S7Driver.DiscoverAsync propagates the flag. Stream A.5 integration tests (FlakeyDriverIntegrationTests, 4 new) exercise the invoker + flaky-driver contract the plan enumerates: - Read with 5 transient failures succeeds on the 6th attempt (RetryCount=10). - Non-idempotent write with RetryCount=5 configured still fails on the first failure — no replay (decision #44 guard at the ExecuteWriteAsync surface). - Idempotent write with 2 transient failures succeeds on the 3rd attempt. - Two hosts on the same driver have independent breakers — dead-host trips its breaker but live-host's first call still succeeds. Propagation tests: - ModbusDriverTests: SetPoint WriteIdempotent=true flows into DriverAttributeInfo; PulseCoil default=false. - S7DiscoveryAndSubscribeTests: same pattern for DBx SetPoint vs M-bit. Full solution dotnet test: 947 passing (baseline 906, +41 net across Stream A so far). Pre-existing Client.CLI Subscribe flake unchanged. Stream A's remaining work (wiring CapabilityInvoker into DriverNodeManager's OnReadValue / OnWriteValue / History / Subscribe dispatch paths) is the server-side integration piece + needs DI wiring for the pipeline builder — lands in the next PR on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
5.3 KiB
C#
121 lines
5.3 KiB
C#
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>
|
|
/// <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,
|
|
}
|