feat(galaxy): nest gobject browse tree by parent_gobject_id (degrade-to-flat)
This commit is contained in:
@@ -17,9 +17,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
|
||||
/// <item>Folder per gobject; variables placed inside their owner folder.</item>
|
||||
/// <item>Variable's full reference = <c>tag_name.attribute_name</c> — the format MXAccess
|
||||
/// expects for read/write addressing (translated from the contained-name browse path).</item>
|
||||
/// <item>Hierarchy is rendered flat (one folder per gobject under the driver root).
|
||||
/// Nesting under <c>parent_gobject_id</c> for a true tree shape is not implemented;
|
||||
/// the flat layout is the current shipping behaviour.</item>
|
||||
/// <item>Hierarchy is nested by <c>parent_gobject_id</c>: each gobject's folder is created
|
||||
/// under the folder of the gobject named by its <c>parent_gobject_id</c> (resolved
|
||||
/// order-independently and memoised, so the parent can appear before or after the child).
|
||||
/// A gobject degrades to the driver root when its <c>parent_gobject_id</c> is <c>0</c>,
|
||||
/// self-referential, or names a gobject not present in the returned set — so a model
|
||||
/// that carries no parentage still renders flat.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class GalaxyDiscoverer
|
||||
@@ -44,12 +47,30 @@ public sealed class GalaxyDiscoverer
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
var objects = await _source.GetHierarchyAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Pass 1 — collect gobjects with a usable browse identity (order preserved), and index
|
||||
// them by gobject_id so pass 2 can resolve a parent the source returned AFTER the child
|
||||
// (order-independent). Only the FIRST gobject per non-zero id is indexed as a parent
|
||||
// target; the index excludes id 0 so identity-less / un-keyed objects can't collide.
|
||||
var usable = new List<GalaxyObject>();
|
||||
var byId = new Dictionary<int, GalaxyObject>();
|
||||
foreach (var obj in objects)
|
||||
{
|
||||
var browseName = string.IsNullOrEmpty(obj.ContainedName) ? obj.TagName : obj.ContainedName;
|
||||
if (string.IsNullOrEmpty(browseName)) continue; // skip objects with no usable identity
|
||||
usable.Add(obj);
|
||||
if (obj.GobjectId != 0)
|
||||
{
|
||||
byId.TryAdd(obj.GobjectId, obj);
|
||||
}
|
||||
}
|
||||
|
||||
var folder = builder.Folder(browseName, browseName);
|
||||
// Pass 2 — create each gobject's folder under its parent (memoised), then its variables.
|
||||
// Iterating the ordered list (not the index) keeps the flat case stable and emits a
|
||||
// folder even for objects whose gobject_id is 0 or duplicated.
|
||||
var folders = new Dictionary<int, IAddressSpaceBuilder>();
|
||||
foreach (var obj in usable)
|
||||
{
|
||||
var folder = EnsureFolder(obj, builder, byId, folders);
|
||||
|
||||
foreach (var attr in obj.Attributes)
|
||||
{
|
||||
@@ -82,6 +103,42 @@ public sealed class GalaxyDiscoverer
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve (and memoise) the folder for a gobject, recursively creating its parent's folder
|
||||
// first so the result nests under it. Falls back to the driver root when the parent_gobject_id
|
||||
// is 0, self-referential, absent from the returned set, or already mid-resolution (cycle guard).
|
||||
// Memoisation is keyed by gobject_id and only applies to non-zero ids — an id-0 object always
|
||||
// gets its own freshly-created folder (it can be neither a parent target nor shared).
|
||||
private static IAddressSpaceBuilder EnsureFolder(
|
||||
GalaxyObject obj,
|
||||
IAddressSpaceBuilder root,
|
||||
IReadOnlyDictionary<int, GalaxyObject> byId,
|
||||
Dictionary<int, IAddressSpaceBuilder> folders,
|
||||
HashSet<int>? building = null)
|
||||
{
|
||||
if (obj.GobjectId != 0 && folders.TryGetValue(obj.GobjectId, out var existing)) return existing;
|
||||
|
||||
var browseName = string.IsNullOrEmpty(obj.ContainedName) ? obj.TagName : obj.ContainedName;
|
||||
building ??= [];
|
||||
|
||||
IAddressSpaceBuilder parentBuilder = root;
|
||||
if (obj.ParentGobjectId != 0
|
||||
&& obj.ParentGobjectId != obj.GobjectId
|
||||
&& !building.Contains(obj.ParentGobjectId)
|
||||
&& byId.TryGetValue(obj.ParentGobjectId, out var parentObj))
|
||||
{
|
||||
building.Add(obj.GobjectId);
|
||||
parentBuilder = EnsureFolder(parentObj, root, byId, folders, building);
|
||||
building.Remove(obj.GobjectId);
|
||||
}
|
||||
|
||||
var folder = parentBuilder.Folder(browseName, browseName);
|
||||
if (obj.GobjectId != 0)
|
||||
{
|
||||
folders[obj.GobjectId] = folder;
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
// PR 5.W workaround for mxaccessgw GalaxyRepository.cs:173-175 — the gateway's
|
||||
// SQL appends `[]` to array-typed `full_tag_reference` values, but MxAccess COM
|
||||
// `IInstance.AddItem` doesn't accept `[]`-suffixed addresses (so any downstream
|
||||
|
||||
Reference in New Issue
Block a user