fix(twincat): update BrowseSymbolsAsync doc + cache adapter fields + Flat-mode note (review)
- ITwinCATClient.BrowseSymbolsAsync XML doc updated: states the implementation now
expands struct/UDT/FB symbols into atomic member leaves via TwinCATSymbolExpander;
callers receive only atomic/array leaves with full InstancePaths, never struct containers.
- AdsSymbolNode: cache IsStruct, Mapped, Children, ReadOnly as readonly fields computed
once in the ctor so repeated property access during recursive expansion doesn't
re-materialize or re-invoke MapSymbolType/IsSymbolWritable.
- BrowseSymbolsAsync: add operator-gated live risk note next to SymbolsLoadMode.Flat
warning that a real TC3 target may not populate SubSymbols in Flat mode, with
guidance to switch to VirtualTree if members don't surface — do not change mode now.
- TwinCATSymbolExpanderTests: simplify confusing `new string('.', 0)` no-op to `""`.
This commit is contained in:
@@ -322,6 +322,14 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
|||||||
// SymbolLoaderFactory downloads the symbol-info blob once then iterates locally — the
|
// SymbolLoaderFactory downloads the symbol-info blob once then iterates locally — the
|
||||||
// async surface on this interface is for our callers, not for the underlying call which
|
// async surface on this interface is for our callers, not for the underlying call which
|
||||||
// is effectively sync on top of the already-open AdsClient.
|
// is effectively sync on top of the already-open AdsClient.
|
||||||
|
//
|
||||||
|
// LIVE RISK (operator-gated, not yet verified against a real TC3 target): Flat mode
|
||||||
|
// may not populate ISymbol.SubSymbols on struct/UDT/FB symbols on some TC3 firmware
|
||||||
|
// versions — in that case TwinCATSymbolExpander sees no children and drops those
|
||||||
|
// symbols silently. If struct members don't surface on a real target, switch to
|
||||||
|
// SymbolsLoadMode.VirtualTree which is documented to always populate SubSymbols via
|
||||||
|
// the hierarchy. Do NOT change the mode here until confirmed against live ADS — Flat is
|
||||||
|
// required for Phase 4c array discovery to work correctly.
|
||||||
var settings = new SymbolLoaderSettings(SymbolsLoadMode.Flat);
|
var settings = new SymbolLoaderSettings(SymbolsLoadMode.Flat);
|
||||||
var loader = SymbolLoaderFactory.Create(_client, settings);
|
var loader = SymbolLoaderFactory.Create(_client, settings);
|
||||||
await Task.Yield(); // honors the async surface; pragmatic given the loader itself is sync
|
await Task.Yield(); // honors the async surface; pragmatic given the loader itself is sync
|
||||||
@@ -350,20 +358,36 @@ internal sealed class AdsTwinCATClient : ITwinCATClient
|
|||||||
/// a local operation). Reuses the existing <see cref="MapSymbolType"/> + <see cref="IsSymbolWritable"/>
|
/// 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.
|
/// so atomic-type mapping and access-rights handling stay identical to the pre-expansion path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed class AdsSymbolNode(ISymbol symbol) : ITwinCATSymbolNode
|
private sealed class AdsSymbolNode : ITwinCATSymbolNode
|
||||||
{
|
{
|
||||||
public string InstancePath => symbol.InstancePath;
|
// Computed once in the ctor so repeated property access during recursive expansion
|
||||||
|
// doesn't re-materialize Children or re-invoke MapSymbolType / IsSymbolWritable.
|
||||||
|
private readonly ISymbol _symbol;
|
||||||
|
private readonly bool _isStruct;
|
||||||
|
private readonly (TwinCATDataType? Type, int? ArrayLength) _mapped;
|
||||||
|
private readonly IReadOnlyList<ITwinCATSymbolNode> _children;
|
||||||
|
private readonly bool _readOnly;
|
||||||
|
|
||||||
public bool IsStruct => symbol.DataType?.Category == DataTypeCategory.Struct;
|
public AdsSymbolNode(ISymbol symbol)
|
||||||
|
{
|
||||||
public (TwinCATDataType? Type, int? ArrayLength) Mapped => MapSymbolType(symbol.DataType);
|
_symbol = symbol;
|
||||||
|
_isStruct = symbol.DataType?.Category == DataTypeCategory.Struct;
|
||||||
public IReadOnlyList<ITwinCATSymbolNode> Children =>
|
_mapped = MapSymbolType(symbol.DataType);
|
||||||
symbol.SubSymbols is { } subs
|
_children = symbol.SubSymbols is { } subs
|
||||||
? subs.Select(s => (ITwinCATSymbolNode)new AdsSymbolNode(s)).ToList()
|
? subs.Select(s => (ITwinCATSymbolNode)new AdsSymbolNode(s)).ToList()
|
||||||
: [];
|
: [];
|
||||||
|
_readOnly = !IsSymbolWritable(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
public bool ReadOnly => !IsSymbolWritable(symbol);
|
public string InstancePath => _symbol.InstancePath;
|
||||||
|
|
||||||
|
public bool IsStruct => _isStruct;
|
||||||
|
|
||||||
|
public (TwinCATDataType? Type, int? ArrayLength) Mapped => _mapped;
|
||||||
|
|
||||||
|
public IReadOnlyList<ITwinCATSymbolNode> Children => _children;
|
||||||
|
|
||||||
|
public bool ReadOnly => _readOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -99,11 +99,14 @@ public interface ITwinCATClient : IDisposable
|
|||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Walk the target's symbol table via the TwinCAT <c>SymbolLoaderFactory</c> (flat mode).
|
/// Walk the target's symbol table via the TwinCAT <c>SymbolLoaderFactory</c> (flat mode)
|
||||||
/// Yields each top-level symbol the PLC exposes — global variables, program-scope locals,
|
/// and expand all struct / UDT / function-block symbols to their atomic member leaves.
|
||||||
/// function-block instance fields. Filters for our atomic type surface; structured /
|
/// The implementation uses <see cref="TwinCATSymbolExpander.ExpandLeaves"/> to recursively
|
||||||
/// UDT / function-block typed symbols surface with <c>DataType = null</c> so callers can
|
/// descend each struct/UDT/FB-instance symbol into its addressable atomic members,
|
||||||
/// decide whether to drill in via their own walker.
|
/// yielding only atomic leaves (scalars or 1-D arrays with a non-null
|
||||||
|
/// <see cref="TwinCATDiscoveredSymbol.DataType"/>) and dropping struct/UDT/FB containers.
|
||||||
|
/// Callers receive fully-qualified <c>InstancePath</c>s (e.g. <c>MAIN.Motor1.Speed</c>)
|
||||||
|
/// and never see struct-typed entries — expansion is performed here, not in the caller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">Cancellation token for the enumeration operation.</param>
|
/// <param name="cancellationToken">Cancellation token for the enumeration operation.</param>
|
||||||
IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(CancellationToken cancellationToken);
|
IAsyncEnumerable<TwinCATDiscoveredSymbol> BrowseSymbolsAsync(CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ public sealed class TwinCATSymbolExpanderTests
|
|||||||
// atomic leaf at the very bottom. The leaf sits at depth == number-of-struct-levels.
|
// atomic leaf at the very bottom. The leaf sits at depth == number-of-struct-levels.
|
||||||
// Place the leaf at depth MaxDepth (== one past the deepest descended level) so it is dropped,
|
// Place the leaf at depth MaxDepth (== one past the deepest descended level) so it is dropped,
|
||||||
// and a sibling leaf at a shallow depth so we prove the walk itself still works.
|
// and a sibling leaf at a shallow depth so we prove the walk itself still works.
|
||||||
ITwinCATSymbolNode deep = Atomic("Root" + new string('.', 0) + "Leaf.Deep", TwinCATDataType.DInt);
|
ITwinCATSymbolNode deep = Atomic("RootLeaf.Deep", TwinCATDataType.DInt);
|
||||||
// Wrap the atomic in MaxDepth struct levels so the atomic ends up at depth == MaxDepth.
|
// Wrap the atomic in MaxDepth struct levels so the atomic ends up at depth == MaxDepth.
|
||||||
for (var i = 0; i < TwinCATSymbolExpander.MaxDepth; i++)
|
for (var i = 0; i < TwinCATSymbolExpander.MaxDepth; i++)
|
||||||
deep = Struct($"Level{i}", deep);
|
deep = Struct($"Level{i}", deep);
|
||||||
|
|||||||
Reference in New Issue
Block a user