using S7.Net; using S7.Net.Types; namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// /// Multi-variable PDU packer for S7 reads. Replaces the per-tag Plc.ReadAsync /// loop with batched Plc.ReadMultipleVarsAsync calls so that N scalar tags fit /// into ⌈N / 19⌉ PDU round-trips on a default 240-byte negotiated PDU instead of N. /// /// /// /// Packing budget: Siemens S7 read response budget is /// negotiatedPduSize - 18 - 12·N, where the 18 bytes cover the response /// header / parameter headers and 12 bytes per item carry the per-variable item /// response (return code + data header + value). For a 240-byte PDU the absolute /// ceiling is ~19 items per request before the response overflows; we apply that /// as a conservative cap regardless of negotiated PDU since S7.Net does not /// expose the negotiated size and 240 is the default for every CPU family. /// /// /// Packable types only: only fixed-width scalars where the wire layout /// maps 1-to-1 onto an the multi-var path natively decodes /// (Bool, Byte, Int16/UInt16, Int32/UInt32, Float32, Float64). Strings, dates, /// arrays, 64-bit ints, and UDT-shaped types stay on the per-tag /// ReadOneAsync path because their decode requires /// Plc.ReadBytesAsync + bespoke codec rather than a single /// . /// /// internal static class S7ReadPacker { /// /// Default negotiated S7 PDU size (bytes). Every S7 CPU family negotiates 240 by /// default; the extended-PDU 480 / 960 byte settings need an explicit COTP /// parameter that S7.Net does not expose. Stay conservative. /// internal const int DefaultPduSize = 240; /// /// Per-item response overhead in bytes — return code + data type code + length /// field. The S7 spec calls this 4 bytes minimum but rounds up to 12 once the /// payload alignment + worst-case 8-byte LReal value field are included. /// internal const int PerItemResponseBytes = 12; /// Fixed response-header bytes regardless of item count. internal const int ResponseHeaderBytes = 18; /// /// Maximum items per PDU at the default 240-byte negotiated size. Derived from /// floor((240 - 18) / 12) = 18.5 rounded down to 18 plus 1 for a /// response-header slack the S7 spec rounds up; the practical Siemens limit /// documented in TIA Portal is 19 items per PUT/GET call so we cap /// at 19 and rely on the budget calculation only when a non-default PDU is in /// play. /// internal const int MaxItemsPerPdu240 = 19; /// /// Compute how many items can fit in one Plc.ReadMultipleVarsAsync /// call at the given negotiated PDU size, capped at the practical Siemens /// ceiling of 19 items. /// internal static int ItemBudget(int negotiatedPduSize) { if (negotiatedPduSize <= ResponseHeaderBytes + PerItemResponseBytes) return 1; var byBudget = (negotiatedPduSize - ResponseHeaderBytes) / PerItemResponseBytes; return Math.Min(byBudget, MaxItemsPerPdu240); } /// /// True if the tag can be packed into a single for /// Plc.ReadMultipleVarsAsync. Returns false for everything that /// needs a custom byte-range decode (strings, dates, arrays, UDTs, 64-bit ints /// where S7.Net's has no entry). /// internal static bool IsPackable(S7TagDefinition tag, S7ParsedAddress addr) { if (tag.ElementCount is int n && n > 1) return false; // arrays go through ReadOneAsync return tag.DataType switch { S7DataType.Bool when addr.Size == S7Size.Bit => true, S7DataType.Byte when addr.Size == S7Size.Byte => true, S7DataType.Int16 or S7DataType.UInt16 when addr.Size == S7Size.Word => true, S7DataType.Int32 or S7DataType.UInt32 when addr.Size == S7Size.DWord => true, S7DataType.Float32 when addr.Size == S7Size.DWord => true, S7DataType.Float64 when addr.Size == S7Size.LWord => true, // Int64 / UInt64 have no native VarType; S7.Net's multi-var path can't decode // them without falling back to byte-range reads. Route to ReadOneAsync. _ => false, }; } /// /// Build a for a packable tag. is /// chosen so that S7.Net's multi-var path decodes the wire bytes into a .NET type /// this driver can reinterpret without a second PLC round-trip /// (Word→ushort, DWord→uint, etc.). /// internal static DataItem BuildDataItem(S7TagDefinition tag, S7ParsedAddress addr) { var dataType = MapArea(addr.Area); var varType = tag.DataType switch { S7DataType.Bool => VarType.Bit, S7DataType.Byte => VarType.Byte, // Int16 read via Word (UInt16 wire) and reinterpreted to short in // DecodePackedValue; gives identical wire behaviour to the single-tag path. S7DataType.Int16 => VarType.Word, S7DataType.UInt16 => VarType.Word, S7DataType.Int32 => VarType.DWord, S7DataType.UInt32 => VarType.DWord, S7DataType.Float32 => VarType.Real, S7DataType.Float64 => VarType.LReal, _ => throw new InvalidOperationException( $"S7ReadPacker: tag '{tag.Name}' DataType {tag.DataType} is not packable; IsPackable check skipped"), }; return new DataItem { DataType = dataType, VarType = varType, DB = addr.DbNumber, StartByteAdr = addr.ByteOffset, BitAdr = (byte)addr.BitOffset, Count = 1, }; } /// /// Convert the boxed value S7.Net's multi-var path returns into the .NET type /// declared by . Mirrors the reinterpret table in /// S7Driver.ReadOneAsync so packed reads and single-tag reads produce /// identical snapshots for the same input. /// internal static object DecodePackedValue(S7TagDefinition tag, object raw) { return (tag.DataType, raw) switch { (S7DataType.Bool, bool b) => b, (S7DataType.Byte, byte by) => by, (S7DataType.UInt16, ushort u16) => u16, (S7DataType.Int16, ushort u16) => unchecked((short)u16), (S7DataType.UInt32, uint u32) => u32, (S7DataType.Int32, uint u32) => unchecked((int)u32), (S7DataType.Float32, float f) => f, (S7DataType.Float64, double d) => d, // S7.Net occasionally hands back the underlying integer type for Real/LReal // when the bytes were marshalled raw — reinterpret defensively. (S7DataType.Float32, uint u32) => BitConverter.UInt32BitsToSingle(u32), (S7DataType.Float64, ulong u64) => BitConverter.UInt64BitsToDouble(u64), _ => throw new System.IO.InvalidDataException( $"S7ReadPacker: tag '{tag.Name}' declared {tag.DataType} but multi-var returned {raw.GetType().Name}"), }; } /// /// Bin-pack into batches of at most /// items. Order within each batch matches the /// input order so the per-item response from S7.Net maps back 1-to-1. /// internal static List> BinPack(IReadOnlyList indices, int itemBudget) { var batches = new List>(); var current = new List(itemBudget); foreach (var idx in indices) { current.Add(idx); if (current.Count >= itemBudget) { batches.Add(current); current = new List(itemBudget); } } if (current.Count > 0) batches.Add(current); return batches; } private static DataType MapArea(S7Area area) => area switch { S7Area.DataBlock => DataType.DataBlock, S7Area.Memory => DataType.Memory, S7Area.Input => DataType.Input, S7Area.Output => DataType.Output, S7Area.Timer => DataType.Timer, S7Area.Counter => DataType.Counter, _ => throw new InvalidOperationException($"Unknown S7Area {area}"), }; }