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, }