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); } }