namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
///
/// Minimal view over an ADS symbol that needs to walk a
/// struct/UDT/FB instance down to its addressable atomic leaves. An adapter in
/// AdsTwinCATClient.BrowseSymbolsAsync wraps the real TwinCAT.TypeSystem.ISymbol
/// onto this interface so the recursion is unit-testable without a live ADS target — the real
/// ISymbol / SymbolLoaderFactory surface is non-injectable.
///
public interface ITwinCATSymbolNode
{
/// Full dotted instance path of this symbol (e.g. MAIN.Motor1.Speed).
string InstancePath { get; }
///
/// true when this symbol is a struct/UDT/FB instance (DataType.Category == Struct)
/// and therefore has addressable members under rather than a value of
/// its own.
///
bool IsStruct { get; }
///
/// The symbol mapped onto the driver's atomic type surface plus an optional 1-D array length.
/// A null Type means an unsupported atomic leaf (pointer / reference / out-of-scope
/// type) which the expander drops.
///
(TwinCATDataType? Type, int? ArrayLength) Mapped { get; }
/// Struct members (empty for atomic leaves).
IReadOnlyList Children { get; }
/// true when the symbol's access rights forbid writes.
bool ReadOnly { get; }
}
///
/// Pure, dependency-free walker that flattens a forest of roots
/// into the addressable atomic leaves the driver can surface as OPC UA variable nodes. A struct
/// node recurses into its (depth-guarded); an atomic
/// leaf with a non-null mapped is yielded as a
/// ; unsupported leaves (null mapped type) and
/// depth-exceeded nodes are dropped. This is the §2 fix that makes discovered struct/UDT/FB
/// members individually addressable.
///
public static class TwinCATSymbolExpander
{
///
/// Maximum struct-nesting depth the walker descends before dropping a node. Guards against a
/// pathological / self-referential type graph turning discovery into an unbounded walk. Deep
/// nesting beyond this is exceedingly rare in real PLC code; the operator can still predeclare
/// a member-path tag directly for anything past the floor.
///
public const int MaxDepth = 8;
///
/// Walk to their atomic leaves. A struct node recurses its children
/// (depth-guarded by ); an atomic leaf with a non-null mapped
/// is yielded; unsupported leaves (null type) and depth-exceeded
/// nodes are dropped. De-duplicates by so any
/// Flat-vs-tree double-listing from the underlying loader collapses to a single emission.
///
/// The top-level symbols (e.g. loader.Symbols) to expand.
/// One per addressable atomic leaf.
public static IEnumerable ExpandLeaves(IEnumerable roots)
{
var seen = new HashSet(StringComparer.Ordinal);
foreach (var root in roots)
foreach (var leaf in Walk(root, depth: 0, seen))
yield return leaf;
}
private static IEnumerable Walk(
ITwinCATSymbolNode node, int depth, HashSet seen)
{
// Depth guard: drop a node whose nesting exceeds the floor rather than recursing further.
if (depth >= MaxDepth) yield break;
if (node.IsStruct)
{
// A struct contributes no value of its own — recurse its members. (Empty children =>
// nothing emitted, which is the correct "no addressable atomic" outcome.)
foreach (var child in node.Children)
foreach (var leaf in Walk(child, depth + 1, seen))
yield return leaf;
yield break;
}
var (type, arrayLength) = node.Mapped;
if (type is null) yield break; // unsupported atomic leaf (pointer / out-of-scope type) — drop.
// Dedup by InstancePath — neutralizes any Flat-vs-tree double-listing of the same leaf.
if (!seen.Add(node.InstancePath)) yield break;
yield return new TwinCATDiscoveredSymbol(node.InstancePath, type, node.ReadOnly, arrayLength);
}
}