using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Commons.Browsing; /// /// Serves a captured discovery snapshot through the standard picker session interface. /// No I/O after open; the tree is point-in-time (picker offers Refresh = close + re-open). /// The tree may be shared by coalesced sessions — dispose drops only this session's /// reference. See design §4.3. /// public sealed class CapturedTreeBrowseSession(CapturedTree tree) : IBrowseSession { private CapturedTree? _tree = tree; /// public Guid Token { get; } = Guid.NewGuid(); /// public DateTime LastUsedUtc { get; private set; } = DateTime.UtcNow; /// public Task> RootAsync(CancellationToken cancellationToken) { var t = Tree(); var nodes = t.Root.Children.Select(Map).ToList(); if (t.Truncated) nodes.Add(new BrowseNode("__truncated__", "⚠ Results truncated — narrow the driver config or use manual entry", BrowseNodeKind.Folder, HasChildrenHint: false)); LastUsedUtc = DateTime.UtcNow; return Task.FromResult>(nodes); } /// public Task> ExpandAsync(string nodeId, CancellationToken cancellationToken) { var t = Tree(); LastUsedUtc = DateTime.UtcNow; return Task.FromResult>( t.ById.TryGetValue(nodeId, out var n) ? n.Children.Select(Map).ToList() : []); } /// public Task> AttributesAsync(string nodeId, CancellationToken cancellationToken) { var t = Tree(); LastUsedUtc = DateTime.UtcNow; if (!t.ById.TryGetValue(nodeId, out var n)) return Task.FromResult>([]); var attrs = new List(); if (n.Attribute is { } a) attrs.Add(new AttributeInfo(n.Name, a.DriverDataType.ToString(), a.IsArray, a.SecurityClass.ToString(), a.IsAlarm || n.IsAlarmMarked, NativeTypeFields(a))); attrs.AddRange(n.Properties.Select(p => new AttributeInfo(p.Name, p.Type.ToString(), false, "", false))); return Task.FromResult>(attrs); } /// public ValueTask DisposeAsync() { _tree = null; // drop this session's reference only — tree may be shared (coalescing) return ValueTask.CompletedTask; } private CapturedTree Tree() => _tree ?? throw new ObjectDisposedException(nameof(CapturedTreeBrowseSession)); /// /// Surfaces a leaf's driver-native type name as the dataType address field, so a /// browse-commit can stamp the driver-typed TagConfig with the type the controller /// actually reported (Gitea #536). /// /// The captured attribute. /// The address fields, or null when the driver reported no native type. /// /// It rides rather than /// because that field is the driver-AGNOSTIC /// type and several consumers already read it as such. The commit mapper cannot recover the /// native type from it: SInt/Int/DInt all arrive as Int32, so a /// reverse map would bind a 16-bit INT as a 32-bit DINT and read four bytes for two. /// /// Null when the driver stamped nothing — the mapper then leaves the model's own default in /// place, which is exactly the pre-#536 behaviour rather than a guess. /// /// private static IReadOnlyDictionary? NativeTypeFields(DriverAttributeInfo a) => string.IsNullOrWhiteSpace(a.NativeDataType) ? null : new Dictionary(StringComparer.Ordinal) { ["dataType"] = a.NativeDataType }; private static BrowseNode Map(CapturedNode n) => n.IsLeaf ? new BrowseNode(n.Id, n.DisplayName, BrowseNodeKind.Leaf, false) : new BrowseNode(n.Id, n.DisplayName, BrowseNodeKind.Folder, n.Children.Count > 0); }