using S7NetCpuType = global::S7.Net.CpuType;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
///
/// Siemens S7 native (S7comm / ISO-on-TCP port 102) driver configuration. Bound from the
/// driver's DriverConfig JSON at DriverHost.RegisterAsync. 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.
///
///
///
/// The driver requires PUT/GET communication enabled 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 Plc.ReadAsync returning ErrorCode.Accessing).
/// The driver maps that specifically to BadNotSupported 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.
///
///
/// See docs/v2/driver-specs.md §5 for the full specification.
///
///
public sealed class S7DriverOptions
{
/// PLC IP address or hostname.
public string Host { get; init; } = "127.0.0.1";
/// TCP port. ISO-on-TCP is 102 on every S7 model; override only for unusual NAT setups.
public int Port { get; init; } = 102;
///
/// CPU family. Determines the ISO-TSAP slot byte that S7.Net uses during connection
/// setup — pick the family that matches the target PLC exactly.
///
public S7NetCpuType CpuType { get; init; } = S7NetCpuType.S71500;
///
/// Hardware rack number. Almost always 0; relevant only for distributed S7-400 racks
/// with multiple CPUs.
///
public short Rack { get; init; } = 0;
///
/// 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.
///
public short Slot { get; init; } = 0;
/// Connect + per-operation timeout.
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(5);
/// 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).
public IReadOnlyList Tags { get; init; } = [];
///
/// Background connectivity-probe settings. When enabled, the driver runs a tick loop
/// that issues a cheap read against every
/// and raises OnHostStatusChanged on
/// Running ↔ Stopped transitions.
///
public S7ProbeOptions Probe { get; init; } = new();
///
/// Block-read coalescing gap-merge threshold (bytes). When two same-DB tags are
/// within this many bytes of each other the planner folds them into a single
/// Plc.ReadBytesAsync request and slices the response client-side. The
/// default = 16 bytes
/// trades a minor over-fetch for one fewer PDU round-trip — over-fetching 16
/// bytes is cheaper than the ~30-byte S7 request frame.
///
///
/// Raise the threshold for chatty PLCs where PDU round-trips dominate latency
/// (S7-1200 with default 240-byte PDU); lower it when DBs are sparsely populated
/// so the over-fetch cost outweighs the saved PDU. Setting to 0 disables gap
/// merging entirely — only literally adjacent ranges (gap == 0) coalesce.
/// STRING / WSTRING / CHAR / WCHAR / structured-timestamp / array tags always
/// opt out of merging regardless of this knob.
///
public int BlockCoalescingGapBytes { get; init; } = S7BlockCoalescingPlanner.DefaultGapMergeBytes;
///
/// ISO-on-TCP / S7comm "connection class" selector. Hardened S7-1500 CPUs and some
/// ET 200SP / S7-1200 firmware variants reject the default PG-class TSAP that
/// S7netplus picks under and require an OP-class
/// or S7-Basic-class TSAP instead. Picking the wrong class produces the same
/// failure shape as picking the wrong slot — connection refused at COTP handshake
/// time, before any S7comm PDU is sent. See docs/v2/s7.md "TSAP / Connection
/// Type" section for the raw-TSAP byte table and the hardened-CPU motivation.
///
///
///
/// preserves existing behaviour — S7netplus picks
/// the TSAP pair from the configured via its
/// TsapPair.GetDefaultTsapPair. Use /
/// / to force a specific
/// class, or together with
/// and for a fully-manual escape hatch. Explicit
/// / overrides win even under
/// / / .
///
///
public TsapMode TsapMode { get; init; } = TsapMode.Auto;
///
/// Optional fully-manual local TSAP override (16-bit big-endian word — high byte =
/// class selector, low byte = caller-defined). Required (together with
/// ) when is
/// . Wins over the class-derived default under any
/// non- mode.
///
public ushort? LocalTsap { get; init; }
///
/// Optional fully-manual remote TSAP override (16-bit big-endian word — high byte =
/// class selector, low byte = (rack << 5) | slot per the S7 spec).
/// Required (together with ) when is
/// . Wins over the class-derived default under any
/// non- mode.
///
public ushort? RemoteTsap { get; init; }
///
/// PR-S7-C3 — per-tag scan-group → publishing-interval map. When a tag declares a
/// ,
/// resolves its publishing rate by looking the group up in this dictionary; tags
/// without a group, or with a group not present here, fall back to the
/// subscription-default publishingInterval argument. Keys are matched
/// case-insensitively. See docs/v2/s7.md "Per-tag scan groups" section.
///
///
/// The driver still owns one Plc connection serialized through the per-driver
/// _gate, so partitioning into N poll loops does NOT parallelise wire-level
/// reads — every partition queues against the same semaphore. Operator value is
/// decoupling tick cadence: a 100 ms HMI tag isn't blocked behind a 10 s slow-poll
/// batch any more, because the slow batch's Task.Delay isn't holding the gate.
///
public IReadOnlyDictionary? ScanGroupIntervals { get; init; }
}
///
/// ISO-on-TCP / S7comm connection class. Picks the high byte of the TSAP pair used
/// during COTP handshake. See docs/v2/s7.md "TSAP / Connection Type" section
/// for the raw-byte table and the hardened-CPU motivation.
///
public enum TsapMode
{
/// S7netplus picks the TSAP pair from . Existing behaviour.
Auto,
/// PG class — high byte 0x01. Default for development laptops / TIA Portal.
Pg,
/// OP class — high byte 0x02. Required by some hardened S7-1500 / ET 200SP deployments.
Op,
/// S7-Basic class — high byte 0x03. Used by S7-Basic clients (e.g. some HMI panels and the WinCC BasicPanel SDK).
S7Basic,
/// Caller-supplied + . Both must be set or driver init throws.
Other,
}
///
/// Raw-TSAP byte constants per ISO-on-TCP / S7comm connection class. The "high byte"
/// is the class selector documented in the Siemens function manual; the local TSAP's
/// low byte is conventionally 0x00 (caller / unprivileged) and the remote TSAP's low
/// byte is (rack << 5) | slot per the spec. Mirrored in
/// docs/v2/s7.md "TSAP / Connection Type" table.
///
public static class S7TsapDefaults
{
/// PG-class high byte = 0x01.
public const byte PgClassHighByte = 0x01;
/// OP-class high byte = 0x02.
public const byte OpClassHighByte = 0x02;
/// S7-Basic-class high byte = 0x03.
public const byte S7BasicClassHighByte = 0x03;
/// Build the local TSAP (16-bit BE): class << 8 | 0x00.
public static ushort BuildLocalTsap(byte classHighByte) => (ushort)(classHighByte << 8);
///
/// Build the remote TSAP (16-bit BE): class << 8 | ((rack & 0x07) << 5 | (slot & 0x1F)).
/// Matches the convention used by S7netplus's TsapPair.GetDefaultTsapPair for the remote endpoint.
///
public static ushort BuildRemoteTsap(byte classHighByte, int rack, int slot)
{
if (rack < 0 || rack > 15)
throw new ArgumentOutOfRangeException(nameof(rack), rack, "rack must be 0..15");
if (slot < 0 || slot > 31)
throw new ArgumentOutOfRangeException(nameof(slot), slot, "slot must be 0..31");
return (ushort)((classHighByte << 8) | ((rack & 0x07) << 5) | (slot & 0x1F));
}
/// Pick the class high-byte for a non- / non- mode.
public static byte HighByteFor(TsapMode mode) => mode switch
{
TsapMode.Pg => PgClassHighByte,
TsapMode.Op => OpClassHighByte,
TsapMode.S7Basic => S7BasicClassHighByte,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode,
"HighByteFor only handles Pg / Op / S7Basic; Auto and Other are caller-handled"),
};
}
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);
///
/// Address to probe for liveness. DB1.DBW0 is the convention if the PLC project
/// reserves a small fingerprint DB for health checks (per docs/v2/s7.md);
/// if not, pick any valid Merker word like MW0.
///
public string ProbeAddress { get; init; } = "MW0";
}
///
/// One S7 variable as exposed by the driver. Addresses use S7.Net syntax — see
/// S7AddressParser (PR 63) for the grammar.
///
/// Tag name; OPC UA browse name + driver full reference.
/// S7 address string, e.g. DB1.DBW0, M0.0, I0.0, QD4. Grammar documented in S7AddressParser (PR 63).
/// Logical data type — drives the underlying S7.Net read/write width.
/// When true the driver accepts writes for this tag.
/// For DataType = String: S7-string max length. Default 254 (S7 max).
///
/// Per docs/v2/plan.md decisions #44, #45, #143 — flag a tag as safe to replay on
/// write timeout / failure. Default false; 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.
///
///
/// Optional 1-D array length. null (or 1) = scalar tag; > 1 = array.
/// The driver issues one byte-range read covering ElementCount × bytes-per-element
/// and slices client-side via the existing scalar codec. Multi-dim arrays are deferred;
/// array-of-UDT lands with PR-S7-D2. Variable-width element types
/// (STRING/WSTRING/CHAR/WCHAR) and BOOL (packed bits) are rejected at init time —
/// they need bespoke layout handling and are tracked as a follow-up. Capped at 8000 to
/// keep the byte-range request inside a single S7 PDU envelope.
///
///
/// PR-S7-C3 — optional scan-group identifier. When set, SubscribeAsync looks up
/// the group's publishing interval in
/// and partitions the input tag list so all tags sharing that interval poll together
/// in a dedicated background loop. Tags with no ScanGroup, or with a group not
/// present in the map, fall back to the subscription's default publishing interval
/// (legacy single-rate behaviour). Group names are matched case-insensitively. See
/// docs/v2/s7.md "Per-tag scan groups" section.
///
public sealed record S7TagDefinition(
string Name,
string Address,
S7DataType DataType,
bool Writable = true,
int StringLength = 254,
bool WriteIdempotent = false,
int? ElementCount = null,
string? ScanGroup = null);
public enum S7DataType
{
Bool,
Byte,
Int16,
UInt16,
Int32,
UInt32,
Int64,
UInt64,
Float32,
Float64,
String,
/// S7 WSTRING: 4-byte header (max-len + actual-len, both UInt16 big-endian) followed by N×2 UTF-16BE bytes; total wire length = 4 + 2 × StringLength.
WString,
/// S7 CHAR: single ASCII byte.
Char,
/// S7 WCHAR: two bytes UTF-16 big-endian.
WChar,
DateTime,
/// S7 DTL — 12-byte structured timestamp with year/mon/day/dow/h/m/s/ns; year range 1970-2554.
Dtl,
/// S7 DATE_AND_TIME (DT) — 8-byte BCD timestamp; year range 1990-2089.
DateAndTime,
/// S7 S5TIME — 16-bit BCD duration with 2-bit timebase; range 0..9990s. Surfaced as Int32 ms.
S5Time,
/// S7 TIME — signed Int32 ms big-endian. Surfaced as Int32 ms (negative durations allowed).
Time,
/// S7 TIME_OF_DAY (TOD) — UInt32 ms since midnight big-endian; range 0..86399999. Surfaced as Int32 ms.
TimeOfDay,
/// S7 DATE — UInt16 days since 1990-01-01 big-endian. Surfaced as DateTime.
Date,
}