Auto: s7-d2 — UDT / STRUCT / nested-DB fan-out

Closes #300
This commit is contained in:
Joseph Doherty
2026-04-26 06:50:26 -04:00
parent 7e62a1158f
commit 5f8d84db43
13 changed files with 1139 additions and 16 deletions
@@ -0,0 +1,50 @@
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.SymbolImport;
/// <summary>
/// PR-S7-D2 — one named member of an <see cref="S7UdtDefinition"/>. Mirrors the
/// STEP 7 / TIA Portal UDT layout: each member sits at a fixed byte offset relative
/// to the UDT's base, has a primitive S7 type (or another UDT for nested STRUCTs),
/// and may be a 1-D array.
/// </summary>
/// <param name="Name">Member name; concatenated with the parent tag's name via dot-separator at fan-out.</param>
/// <param name="Offset">Byte offset within the parent UDT. Must be ascending and non-overlapping across the member list.</param>
/// <param name="DataType">Primitive S7 type. Ignored when <paramref name="UdtName"/> is set (nested-UDT case).</param>
/// <param name="ArrayDim">Optional 1-D array length. <c>null</c> / <c>1</c> = scalar; <c>&gt; 1</c> emits indexed sub-tags <c>Member[0]</c>, <c>Member[1]</c>, ...</param>
/// <param name="UdtName">When set, this member is itself a UDT — the fan-out recurses into the named UDT's layout. Mutually exclusive with the primitive interpretation of <paramref name="DataType"/>.</param>
public sealed record S7UdtMember(
string Name,
int Offset,
S7DataType DataType,
int? ArrayDim = null,
string? UdtName = null);
/// <summary>
/// PR-S7-D2 — declarative description of a Siemens UDT (User-Defined Type) /
/// STRUCT layout. Tags whose <see cref="S7TagDefinition.UdtName"/> matches
/// <see cref="Name"/> get fanned-out into one scalar leaf tag per recursive
/// member at <see cref="S7Driver.InitializeAsync"/> time, so the rest of the
/// read/write/subscribe pipeline never has to know about UDTs.
/// </summary>
/// <param name="Name">UDT name. Case-insensitively matched against <see cref="S7TagDefinition.UdtName"/> and against nested members' <see cref="S7UdtMember.UdtName"/>.</param>
/// <param name="Members">Ordered member list. Members must have ascending non-overlapping offsets; offsets that re-use bytes are rejected at fan-out time.</param>
/// <param name="SizeBytes">Total UDT byte size, used as a sanity bound for the last member's offset + width.</param>
/// <remarks>
/// <para>
/// <b>Optimized block access</b>: TIA Portal can mark a DB or UDT as
/// "Optimized block access" which lets the runtime reorder members for
/// memory alignment. The static-offset model used here REQUIRES "Optimized
/// block access" turned OFF on the parent DB; otherwise the declared
/// offsets won't match the runtime layout. Same constraint applies to
/// general absolute-offset DB addressing (see <c>docs/v2/s7.md</c>).
/// </para>
/// <para>
/// <b>Nesting depth</b>: nested UDT-of-UDT is supported up to 4 levels.
/// The fan-out throws <see cref="InvalidOperationException"/> on the 5th
/// level — picked as a generous-but-still-bounded ceiling that catches
/// pathological / accidentally-recursive declarations early.
/// </para>
/// </remarks>
public sealed record S7UdtDefinition(
string Name,
IReadOnlyList<S7UdtMember> Members,
int SizeBytes);