From 26816fd17eccb4935256c3007c8afa11302ff961 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 06:26:59 -0400 Subject: [PATCH] =?UTF-8?q?feat(commons):=20EquipmentNodeIds=20=E2=80=94?= =?UTF-8?q?=20single=20source=20of=20truth=20for=20folder-scoped=20equipme?= =?UTF-8?q?nt=20NodeIds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpcUa/EquipmentNodeIds.cs | 31 +++++++++++++++++++ .../OpcUa/EquipmentNodeIdsTests.cs | 24 ++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs create mode 100644 tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs new file mode 100644 index 00000000..f6e1c724 --- /dev/null +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs @@ -0,0 +1,31 @@ +namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa; + +/// +/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is +/// FOLDER-SCOPED ({parent}/{Name}), NOT the driver-side FullName — a driver wire ref is not +/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the +/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all +/// three agree on the exact NodeId a variable was placed at. +/// +public static class EquipmentNodeIds +{ + /// The sub-folder NodeId under an equipment for a non-empty FolderPath: {equipmentId}/{folderPath}. + /// The owning equipment's NodeId. + /// The tag/vtag FolderPath (must be non-empty for this to be meaningful). + /// The sub-folder NodeId string. + public static string SubFolder(string equipmentId, string folderPath) => $"{equipmentId}/{folderPath}"; + + /// + /// The folder-scoped variable NodeId: {parent}/{name} where parent = equipmentId when + /// is null/empty, else . + /// + /// The owning equipment's NodeId. + /// The tag/vtag FolderPath, or null/empty for "directly under the equipment". + /// The tag/vtag Name (the leaf browse segment). + /// The folder-scoped variable NodeId string. + public static string Variable(string equipmentId, string? folderPath, string name) + { + var parent = string.IsNullOrWhiteSpace(folderPath) ? equipmentId : SubFolder(equipmentId, folderPath); + return $"{parent}/{name}"; + } +} diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs new file mode 100644 index 00000000..4601771c --- /dev/null +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs @@ -0,0 +1,24 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; + +namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.OpcUa; + +public class EquipmentNodeIdsTests +{ + [Fact] + public void Variable_with_no_folder_is_equipment_slash_name() + => EquipmentNodeIds.Variable("eq-1", "", "speed").ShouldBe("eq-1/speed"); + + [Fact] + public void Variable_with_null_folder_is_equipment_slash_name() + => EquipmentNodeIds.Variable("eq-1", null, "speed").ShouldBe("eq-1/speed"); + + [Fact] + public void Variable_with_folder_is_equipment_slash_folder_slash_name() + => EquipmentNodeIds.Variable("eq-1", "registers", "speed").ShouldBe("eq-1/registers/speed"); + + [Fact] + public void SubFolder_is_equipment_slash_folder() + => EquipmentNodeIds.SubFolder("eq-1", "registers").ShouldBe("eq-1/registers"); +}