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

View File

@@ -86,6 +86,11 @@ public static class S7DriverFactoryExtensions
LocalTsap = dto.LocalTsap,
RemoteTsap = dto.RemoteTsap,
ScanGroupIntervals = scanGroupMap,
// PR-S7-D2 — UDT layout declarations referenced by tags whose UdtName is set.
// Empty list when the config doesn't declare any UDTs (the typical scalar-only case).
Udts = dto.Udts is { Count: > 0 }
? [.. dto.Udts.Select(u => BuildUdt(u, driverInstanceId))]
: [],
};
return new S7Driver(options, driverInstanceId);
@@ -95,16 +100,49 @@ public static class S7DriverFactoryExtensions
new(
Name: t.Name ?? throw new InvalidOperationException(
$"S7 config for '{driverInstanceId}' has a tag missing Name"),
// PR-S7-D2 — UDT-typed tags use the UDT name as their type rather than a primitive
// S7 data type; Address is still required (the parent base address that the
// fan-out adds member offsets to). Address may legitimately be DBn.DBX0.0 for an
// entire-DB UDT pointer; the parser accepts that and the fan-out walks from there.
Address: t.Address ?? throw new InvalidOperationException(
$"S7 tag '{t.Name}' in '{driverInstanceId}' missing Address"),
DataType: ParseEnum<S7DataType>(t.DataType, driverInstanceId, "DataType",
tagName: t.Name),
DataType: string.IsNullOrWhiteSpace(t.UdtName)
? ParseEnum<S7DataType>(t.DataType, driverInstanceId, "DataType", tagName: t.Name)
: ParseEnum<S7DataType>(t.DataType, driverInstanceId, "DataType", tagName: t.Name,
fallback: S7DataType.Byte),
Writable: t.Writable ?? true,
StringLength: t.StringLength ?? 254,
WriteIdempotent: t.WriteIdempotent ?? false,
ScanGroup: string.IsNullOrWhiteSpace(t.ScanGroup) ? null : t.ScanGroup,
DeadbandAbsolute: t.DeadbandAbsolute,
DeadbandPercent: t.DeadbandPercent);
DeadbandPercent: t.DeadbandPercent,
UdtName: string.IsNullOrWhiteSpace(t.UdtName) ? null : t.UdtName);
private static S7UdtDefinition BuildUdt(S7UdtDto u, string driverInstanceId)
{
if (string.IsNullOrWhiteSpace(u.Name))
throw new InvalidOperationException(
$"S7 config for '{driverInstanceId}' has a UDT entry missing Name");
var members = (u.Members ?? new List<S7UdtMemberDto>()).Select(m => BuildUdtMember(m, u.Name!, driverInstanceId)).ToList();
return new S7UdtDefinition(u.Name!, members, u.SizeBytes ?? 0);
}
private static S7UdtMember BuildUdtMember(S7UdtMemberDto m, string udtName, string driverInstanceId)
{
if (string.IsNullOrWhiteSpace(m.Name))
throw new InvalidOperationException(
$"S7 UDT '{udtName}' in '{driverInstanceId}' has a member missing Name");
var dataType = string.IsNullOrWhiteSpace(m.UdtName)
? ParseEnum<S7DataType>(m.DataType, driverInstanceId, $"UDT '{udtName}' member '{m.Name}' DataType")
: ParseEnum<S7DataType>(m.DataType, driverInstanceId, $"UDT '{udtName}' member '{m.Name}' DataType",
fallback: S7DataType.Byte);
return new S7UdtMember(
Name: m.Name!,
Offset: m.Offset ?? 0,
DataType: dataType,
ArrayDim: m.ArrayDim,
UdtName: string.IsNullOrWhiteSpace(m.UdtName) ? null : m.UdtName);
}
/// <summary>
/// PR-S7-D1 / #299 — append TIA Portal "Show all tags" CSV rows to
@@ -225,6 +263,7 @@ public static class S7DriverFactoryExtensions
LocalTsap = options.LocalTsap,
RemoteTsap = options.RemoteTsap,
ScanGroupIntervals = options.ScanGroupIntervals,
Udts = options.Udts,
};
}
@@ -285,6 +324,14 @@ public static class S7DriverFactoryExtensions
/// <c>docs/v2/s7.md</c> "Per-tag scan groups" section.
/// </summary>
public Dictionary<string, int>? ScanGroupIntervalsMs { get; init; }
/// <summary>
/// PR-S7-D2 — UDT / STRUCT layout declarations. Tags whose
/// <see cref="S7TagDto.UdtName"/> matches a UDT here get fanned out into
/// scalar leaf member tags at <c>S7Driver.InitializeAsync</c> time.
/// See <c>docs/v2/s7.md</c> "UDT / STRUCT support" section.
/// </summary>
public List<S7UdtDto>? Udts { get; init; }
}
internal sealed class S7TagDto
@@ -320,6 +367,39 @@ public static class S7DriverFactoryExtensions
/// set the filters are OR'd — publish if EITHER threshold triggers.
/// </summary>
public double? DeadbandPercent { get; init; }
/// <summary>
/// PR-S7-D2 — when set, the tag is a UDT / STRUCT-typed pointer. The driver
/// looks up the named UDT in <see cref="S7DriverConfigDto.Udts"/> and fans the
/// tag out into scalar leaf tags at <c>InitializeAsync</c> time. The primitive
/// <see cref="DataType"/> field is ignored when <c>UdtName</c> is set; the
/// leaves take their data types from the UDT member declarations.
/// </summary>
public string? UdtName { get; init; }
}
/// <summary>
/// PR-S7-D2 — JSON wire form for <see cref="S7UdtDefinition"/>. See
/// <c>docs/v2/s7.md</c> "UDT / STRUCT support" for the round-trip example.
/// </summary>
internal sealed class S7UdtDto
{
public string? Name { get; init; }
public List<S7UdtMemberDto>? Members { get; init; }
public int? SizeBytes { get; init; }
}
/// <summary>
/// PR-S7-D2 — JSON wire form for <see cref="S7UdtMember"/>. <c>UdtName</c> is set
/// for nested-UDT members; <c>DataType</c> is set for primitive members.
/// </summary>
internal sealed class S7UdtMemberDto
{
public string? Name { get; init; }
public int? Offset { get; init; }
public string? DataType { get; init; }
public int? ArrayDim { get; init; }
public string? UdtName { get; init; }
}
internal sealed class S7ProbeDto