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