feat(twincat): expand discovered struct/UDT symbols into addressable member leaves

This commit is contained in:
Joseph Doherty
2026-06-17 20:05:01 -04:00
parent d0a0661f6a
commit 3699fc16a8
3 changed files with 288 additions and 4 deletions
@@ -326,7 +326,12 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
var loader = SymbolLoaderFactory.Create(_client, settings);
await Task.Yield(); // honors the async surface; pragmatic given the loader itself is sync
foreach (ISymbol symbol in loader.Symbols)
// Expand struct/UDT/FB-instance symbols down to their addressable atomic members via the
// pure TwinCATSymbolExpander (unit-proven; the real ISymbol surface is non-injectable so the
// recursion is tested through the ITwinCATSymbolNode abstraction, then adapted here). The
// expander dedups by InstancePath, so it also neutralizes any Flat-vs-tree double-listing.
foreach (var ds in TwinCATSymbolExpander.ExpandLeaves(
loader.Symbols.Select(s => new AdsSymbolNode(s))))
{
// ThrowIfCancellationRequested — not yield break — so a cancelled browse propagates
// as OperationCanceledException rather than a silent clean completion. DiscoverAsync
@@ -334,12 +339,33 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
// distinctly from a genuine browse failure; a yield break would let a partial
// symbol set appear as a fully successful discovery (Driver.TwinCAT-010).
cancellationToken.ThrowIfCancellationRequested();
var (mapped, arrayLength) = MapSymbolType(symbol.DataType);
var readOnly = !IsSymbolWritable(symbol);
yield return new TwinCATDiscoveredSymbol(symbol.InstancePath, mapped, readOnly, arrayLength);
yield return ds;
}
}
/// <summary>
/// Adapts Beckhoff's <see cref="ISymbol"/> onto <see cref="ITwinCATSymbolNode"/> so the pure
/// <see cref="TwinCATSymbolExpander"/> can walk it. Children are materialized eagerly per node
/// (the loader has already downloaded the whole symbol-info blob, so descending sub-symbols is
/// a local operation). Reuses the existing <see cref="MapSymbolType"/> + <see cref="IsSymbolWritable"/>
/// so atomic-type mapping and access-rights handling stay identical to the pre-expansion path.
/// </summary>
private sealed class AdsSymbolNode(ISymbol symbol) : ITwinCATSymbolNode
{
public string InstancePath => symbol.InstancePath;
public bool IsStruct => symbol.DataType?.Category == DataTypeCategory.Struct;
public (TwinCATDataType? Type, int? ArrayLength) Mapped => MapSymbolType(symbol.DataType);
public IReadOnlyList<ITwinCATSymbolNode> Children =>
symbol.SubSymbols is { } subs
? subs.Select(s => (ITwinCATSymbolNode)new AdsSymbolNode(s)).ToList()
: [];
public bool ReadOnly => !IsSymbolWritable(symbol);
}
/// <summary>
/// Resolves a symbol's <see cref="IDataType"/> to the driver's atomic
/// <see cref="TwinCATDataType"/> plus an optional 1-D array length.