Renames all 11 projects (5 src + 6 tests), the .slnx solution file, all source-file namespaces, all axaml namespace references, and all v1 documentation references in CLAUDE.md and docs/*.md (excluding docs/v2/ which is already in OtOpcUa form). Also updates the TopShelf service registration name from "LmxOpcUa" to "OtOpcUa" per Phase 0 Task 0.6.
Preserves runtime identifiers per Phase 0 Out-of-Scope rules to avoid breaking v1/v2 client trust during coexistence: OPC UA `ApplicationUri` defaults (`urn:{GalaxyName}:LmxOpcUa`), server `EndpointPath` (`/LmxOpcUa`), `ServerName` default (feeds cert subject CN), `MxAccessConfiguration.ClientName` default (defensive — stays "LmxOpcUa" for MxAccess audit-trail consistency), client OPC UA identifiers (`ApplicationName = "LmxOpcUaClient"`, `ApplicationUri = "urn:localhost:LmxOpcUaClient"`, cert directory `%LocalAppData%\LmxOpcUaClient\pki\`), and the `LmxOpcUaServer` class name (class rename out of Phase 0 scope per Task 0.5 sed pattern; happens in Phase 1 alongside `LmxNodeManager → GenericDriverNodeManager` Core extraction). 23 LmxOpcUa references retained, all enumerated and justified in `docs/v2/implementation/exit-gate-phase-0.md`.
Build clean: 0 errors, 30 warnings (lower than baseline 167). Tests at strict improvement over baseline: 821 passing / 1 failing vs baseline 820 / 2 (one flaky pre-existing failure passed this run; the other still fails — both pre-existing and unrelated to the rename). `Client.UI.Tests`, `Historian.Aveva.Tests`, `Client.Shared.Tests`, `IntegrationTests` all match baseline exactly. Exit gate compliance results recorded in `docs/v2/implementation/exit-gate-phase-0.md` with all 7 checks PASS or DEFERRED-to-PR-review (#7 service install verification needs Windows service permissions on the reviewer's box).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
224 lines
8.8 KiB
C#
224 lines
8.8 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Builds the tag reference mappings from Galaxy hierarchy and attributes.
|
|
/// Testable without an OPC UA server. (OPC-002, OPC-003, OPC-004)
|
|
/// </summary>
|
|
public class AddressSpaceBuilder
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext<AddressSpaceBuilder>();
|
|
|
|
/// <summary>
|
|
/// Builds an in-memory model of the Galaxy hierarchy and attribute mappings before the OPC UA server materializes
|
|
/// nodes.
|
|
/// </summary>
|
|
/// <param name="hierarchy">The Galaxy object hierarchy returned by the repository.</param>
|
|
/// <param name="attributes">The Galaxy attribute rows associated with the hierarchy.</param>
|
|
/// <returns>An address-space model containing roots, variables, and tag-reference mappings.</returns>
|
|
public static AddressSpaceModel Build(List<GalaxyObjectInfo> hierarchy, List<GalaxyAttributeInfo> 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<int>(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<int, List<GalaxyAttributeInfo>> attrsByObject,
|
|
Dictionary<int, List<GalaxyObjectInfo>> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Node info for the address space tree.
|
|
/// </summary>
|
|
public class NodeInfo
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the Galaxy object identifier represented by this address-space node.
|
|
/// </summary>
|
|
public int GobjectId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the runtime tag name used to tie the node back to Galaxy metadata.
|
|
/// </summary>
|
|
public string TagName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the browse name exposed to OPC UA clients for this hierarchy node.
|
|
/// </summary>
|
|
public string BrowseName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the parent Galaxy object identifier used to assemble the tree.
|
|
/// </summary>
|
|
public int ParentGobjectId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the node represents a Galaxy area folder.
|
|
/// </summary>
|
|
public bool IsArea { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the attribute nodes published beneath this object.
|
|
/// </summary>
|
|
public List<AttributeNodeInfo> Attributes { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Gets or sets the child nodes that appear under this branch of the Galaxy hierarchy.
|
|
/// </summary>
|
|
public List<NodeInfo> Children { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lightweight description of an attribute node that will become an OPC UA variable.
|
|
/// </summary>
|
|
public class AttributeNodeInfo
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the Galaxy attribute name published under the object.
|
|
/// </summary>
|
|
public string AttributeName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the fully qualified runtime reference used for reads, writes, and subscriptions.
|
|
/// </summary>
|
|
public string FullTagReference { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Galaxy data type code used to pick the OPC UA variable type.
|
|
/// </summary>
|
|
public int MxDataType { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the attribute is modeled as an array.
|
|
/// </summary>
|
|
public bool IsArray { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the declared array length when the attribute is a fixed-size array.
|
|
/// </summary>
|
|
public int? ArrayDimension { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the primitive name that groups the attribute under a sub-object node.
|
|
/// Empty for root-level attributes.
|
|
/// </summary>
|
|
public string PrimitiveName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Galaxy security classification that determines OPC UA write access.
|
|
/// </summary>
|
|
public int SecurityClassification { get; set; } = 1;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the attribute is historized.
|
|
/// </summary>
|
|
public bool IsHistorized { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the attribute is an alarm.
|
|
/// </summary>
|
|
public bool IsAlarm { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of building the address space model.
|
|
/// </summary>
|
|
public class AddressSpaceModel
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets the root nodes that become the top-level browse entries in the Galaxy namespace.
|
|
/// </summary>
|
|
public List<NodeInfo> RootNodes { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Gets or sets the mapping from OPC UA node identifiers to runtime tag references.
|
|
/// </summary>
|
|
public Dictionary<string, string> NodeIdToTagReference { get; set; } =
|
|
new(StringComparer.OrdinalIgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of non-area Galaxy objects included in the model.
|
|
/// </summary>
|
|
public int ObjectCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of variable nodes created from Galaxy attributes.
|
|
/// </summary>
|
|
public int VariableCount { get; set; }
|
|
}
|
|
}
|
|
} |