using System; using System.Collections.Generic; using System.Linq; using Serilog; using ZB.MOM.WW.OtOpcUa.Host.Domain; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa { /// /// Builds the tag reference mappings from Galaxy hierarchy and attributes. /// Testable without an OPC UA server. (OPC-002, OPC-003, OPC-004) /// public class AddressSpaceBuilder { private static readonly ILogger Log = Serilog.Log.ForContext(); /// /// Builds an in-memory model of the Galaxy hierarchy and attribute mappings before the OPC UA server materializes /// nodes. /// /// The Galaxy object hierarchy returned by the repository. /// The Galaxy attribute rows associated with the hierarchy. /// An address-space model containing roots, variables, and tag-reference mappings. public static AddressSpaceModel Build(List hierarchy, List attributes) { var model = new AddressSpaceModel(); var objectMap = hierarchy.ToDictionary(h => h.GobjectId); var attrsByObject = attributes .GroupBy(a => a.GobjectId) .ToDictionary(g => g.Key, g => g.ToList()); // Build parent→children map var childrenByParent = hierarchy.GroupBy(h => h.ParentGobjectId) .ToDictionary(g => g.Key, g => g.ToList()); // Find root objects (parent not in hierarchy) var knownIds = new HashSet(hierarchy.Select(h => h.GobjectId)); foreach (var obj in hierarchy) { var nodeInfo = BuildNodeInfo(obj, attrsByObject, childrenByParent, model); if (!knownIds.Contains(obj.ParentGobjectId)) model.RootNodes.Add(nodeInfo); } Log.Information("Address space model: {Objects} objects, {Variables} variables, {Mappings} tag refs", model.ObjectCount, model.VariableCount, model.NodeIdToTagReference.Count); return model; } private static NodeInfo BuildNodeInfo(GalaxyObjectInfo obj, Dictionary> attrsByObject, Dictionary> childrenByParent, AddressSpaceModel model) { var node = new NodeInfo { GobjectId = obj.GobjectId, TagName = obj.TagName, BrowseName = obj.BrowseName, ParentGobjectId = obj.ParentGobjectId, IsArea = obj.IsArea }; if (!obj.IsArea) model.ObjectCount++; if (attrsByObject.TryGetValue(obj.GobjectId, out var attrs)) foreach (var attr in attrs) { node.Attributes.Add(new AttributeNodeInfo { AttributeName = attr.AttributeName, FullTagReference = attr.FullTagReference, MxDataType = attr.MxDataType, IsArray = attr.IsArray, ArrayDimension = attr.ArrayDimension, PrimitiveName = attr.PrimitiveName ?? "", SecurityClassification = attr.SecurityClassification, IsHistorized = attr.IsHistorized, IsAlarm = attr.IsAlarm }); model.NodeIdToTagReference[GetNodeIdentifier(attr)] = attr.FullTagReference; model.VariableCount++; } return node; } private static string GetNodeIdentifier(GalaxyAttributeInfo attr) { if (!attr.IsArray) return attr.FullTagReference; return attr.FullTagReference.EndsWith("[]", StringComparison.Ordinal) ? attr.FullTagReference.Substring(0, attr.FullTagReference.Length - 2) : attr.FullTagReference; } /// /// Node info for the address space tree. /// public class NodeInfo { /// /// Gets or sets the Galaxy object identifier represented by this address-space node. /// public int GobjectId { get; set; } /// /// Gets or sets the runtime tag name used to tie the node back to Galaxy metadata. /// public string TagName { get; set; } = ""; /// /// Gets or sets the browse name exposed to OPC UA clients for this hierarchy node. /// public string BrowseName { get; set; } = ""; /// /// Gets or sets the parent Galaxy object identifier used to assemble the tree. /// public int ParentGobjectId { get; set; } /// /// Gets or sets a value indicating whether the node represents a Galaxy area folder. /// public bool IsArea { get; set; } /// /// Gets or sets the attribute nodes published beneath this object. /// public List Attributes { get; set; } = new(); /// /// Gets or sets the child nodes that appear under this branch of the Galaxy hierarchy. /// public List Children { get; set; } = new(); } /// /// Lightweight description of an attribute node that will become an OPC UA variable. /// public class AttributeNodeInfo { /// /// Gets or sets the Galaxy attribute name published under the object. /// public string AttributeName { get; set; } = ""; /// /// Gets or sets the fully qualified runtime reference used for reads, writes, and subscriptions. /// public string FullTagReference { get; set; } = ""; /// /// Gets or sets the Galaxy data type code used to pick the OPC UA variable type. /// public int MxDataType { get; set; } /// /// Gets or sets a value indicating whether the attribute is modeled as an array. /// public bool IsArray { get; set; } /// /// Gets or sets the declared array length when the attribute is a fixed-size array. /// public int? ArrayDimension { get; set; } /// /// Gets or sets the primitive name that groups the attribute under a sub-object node. /// Empty for root-level attributes. /// public string PrimitiveName { get; set; } = ""; /// /// Gets or sets the Galaxy security classification that determines OPC UA write access. /// public int SecurityClassification { get; set; } = 1; /// /// Gets or sets a value indicating whether the attribute is historized. /// public bool IsHistorized { get; set; } /// /// Gets or sets a value indicating whether the attribute is an alarm. /// public bool IsAlarm { get; set; } } /// /// Result of building the address space model. /// public class AddressSpaceModel { /// /// Gets or sets the root nodes that become the top-level browse entries in the Galaxy namespace. /// public List RootNodes { get; set; } = new(); /// /// Gets or sets the mapping from OPC UA node identifiers to runtime tag references. /// public Dictionary NodeIdToTagReference { get; set; } = new(StringComparer.OrdinalIgnoreCase); /// /// Gets or sets the number of non-area Galaxy objects included in the model. /// public int ObjectCount { get; set; } /// /// Gets or sets the number of variable nodes created from Galaxy attributes. /// public int VariableCount { get; set; } } } }