diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
index 669601e4..e5411e87 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs
@@ -59,9 +59,8 @@ public sealed record ScriptTagInfo(string Path, string Kind, string DataType, st
///
/// Equipment driver tag (EquipmentId != null) → the driver FullName
/// extracted from Tag.TagConfig (the verified DependencyMux key).
-/// SystemPlatform tag (EquipmentId == null) → the MXAccess dot-ref
-/// (FolderPath.Name when a folder is set, else Name) — see
-/// Phase7Composer's GalaxyTagPlan.MxAccessRef.
+/// Galaxy points are ordinary equipment tags now (GalaxyMxGateway is a standard
+/// Equipment-kind driver), so they resolve by this same FullName key.
/// VirtualTag → its leaf Name only, as a BEST-EFFORT key (the live resolution
/// of virtual-tag cascade/write targets is unconfirmed).
///
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs
index 738f66c1..7e3c64da 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs
@@ -70,22 +70,20 @@ public sealed class Phase7Applier
var changedCount =
plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count +
- plan.ChangedGalaxyTags.Count + plan.ChangedEquipmentTags.Count +
+ plan.ChangedEquipmentTags.Count +
plan.ChangedEquipmentVirtualTags.Count;
var addedCount =
plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count +
- plan.AddedGalaxyTags.Count + plan.AddedEquipmentTags.Count +
+ plan.AddedEquipmentTags.Count +
plan.AddedEquipmentVirtualTags.Count;
- // Any add/remove of Equipment, ScriptedAlarm, Galaxy tag, Equipment tag, or Equipment
- // VirtualTag topology requires a real address-space rebuild. Driver-instance changes don't
- // touch the address-space topology directly — they go through DriverHostActor's spawn-plan
- // in Runtime.
+ // Any add/remove of Equipment, ScriptedAlarm, Equipment tag, or Equipment VirtualTag topology
+ // requires a real address-space rebuild. Driver-instance changes don't touch the address-space
+ // topology directly — they go through DriverHostActor's spawn-plan in Runtime.
// TODO(equipment-virtualtags): when MaterialiseEquipmentVirtualTags drives per-delta sink work, revisit whether ChangedEquipmentVirtualTags should also force needsRebuild.
var needsRebuild =
plan.AddedEquipment.Count > 0 || plan.RemovedEquipment.Count > 0 ||
plan.AddedAlarms.Count > 0 || plan.RemovedAlarms.Count > 0 ||
- plan.AddedGalaxyTags.Count > 0 || plan.RemovedGalaxyTags.Count > 0 ||
plan.AddedEquipmentTags.Count > 0 || plan.RemovedEquipmentTags.Count > 0 ||
plan.AddedEquipmentVirtualTags.Count > 0 || plan.RemovedEquipmentVirtualTags.Count > 0;
@@ -141,46 +139,8 @@ public sealed class Phase7Applier
}
///
- /// Materialise Galaxy / SystemPlatform-namespace tags from a composition snapshot:
- /// for each , ensure its FolderPath segment exists (a folder
- /// under the namespace root), then ensure a Variable node sits inside that folder for
- /// the leaf . Variable starts with BadWaitingForInitialData;
- /// the Galaxy driver's OnDataChange path fills the value in once SubscribeBulk lands.
- /// Idempotent.
- ///
- /// The composition result containing the Galaxy tags to materialise.
- public void MaterialiseGalaxyTags(Phase7CompositionResult composition)
- {
- ArgumentNullException.ThrowIfNull(composition);
- if (composition.GalaxyTags.Count == 0) return;
-
- // Folders first — each distinct FolderPath becomes one folder under the root.
- var foldersCreated = new HashSet(StringComparer.Ordinal);
- foreach (var tag in composition.GalaxyTags)
- {
- if (string.IsNullOrWhiteSpace(tag.FolderPath)) continue;
- if (!foldersCreated.Add(tag.FolderPath)) continue;
- SafeEnsureFolder(tag.FolderPath, parentNodeId: null, displayName: tag.FolderPath);
- }
-
- // Variables: NodeId is "." so it matches the MXAccess ref the
- // Galaxy driver subscribes to. Browse-path lookup via OPC UA Translate is the canonical
- // resolution; flat NodeId keeps the address space lookup cheap.
- foreach (var tag in composition.GalaxyTags)
- {
- var nodeId = string.IsNullOrWhiteSpace(tag.FolderPath) ? tag.DisplayName : tag.MxAccessRef;
- var parent = string.IsNullOrWhiteSpace(tag.FolderPath) ? null : tag.FolderPath;
- SafeEnsureVariable(nodeId, parent, tag.DisplayName, tag.DataType);
- }
-
- _logger.LogInformation(
- "Phase7Applier: Galaxy tags materialised (tags={Tags}, folders={Folders})",
- composition.GalaxyTags.Count, foldersCreated.Count);
- }
-
- ///
- /// Materialise Equipment-namespace tags from a composition snapshot — the equipment-signal
- /// analogue of . For each ,
+ /// Materialise Equipment-namespace tags from a composition snapshot.
+ /// For each ,
/// ensure its optional FolderPath sub-folder under the existing equipment folder, then
/// ensure a Variable (NodeId = FullName, the driver-side ref) inside it. Variables
/// start BadWaitingForInitialData; the driver fills live values in a later milestone.
@@ -222,7 +182,7 @@ public sealed class Phase7Applier
// would collide in the sink (EnsureVariable keys on NodeId) and drop all but one machine's
// signal. The driver-side FullName lives on EquipmentTagPlan for the later values milestone to
// route by. Parent is the FolderPath sub-folder when set, else the equipment folder directly.
- // Like the Galaxy pass, per-variable idempotency relies on the sink's own EnsureVariable.
+ // Per-variable idempotency relies on the sink's own EnsureVariable.
foreach (var tag in composition.EquipmentTags)
{
var parent = string.IsNullOrWhiteSpace(tag.FolderPath)
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
index a0189cab..79da6bd1 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs
@@ -9,18 +9,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
/// Outcome of — pure value tuple, no side effects.
/// + carry the UNS topology so the applier can
/// materialise the Area/Line/Equipment folder hierarchy in the address space; equipment carries
-/// its parent line id so the applier knows where to hang each equipment folder.
-/// carries SystemPlatform-namespace tags (Galaxy hierarchy) so the
-/// applier can materialise their FolderPath + Variable nodes ahead of any driver subscribe.
+/// its parent line id so the applier knows where to hang each equipment folder.
public sealed record Phase7CompositionResult(
IReadOnlyList UnsAreas,
IReadOnlyList UnsLines,
IReadOnlyList EquipmentNodes,
IReadOnlyList DriverInstancePlans,
- IReadOnlyList ScriptedAlarmPlans,
- IReadOnlyList GalaxyTags)
+ IReadOnlyList ScriptedAlarmPlans)
{
- /// Convenience constructor for tests + earlier callers that don't carry UNS or Galaxy data.
+ /// Convenience constructor for tests + earlier callers that don't carry UNS data.
/// The equipment nodes.
/// The driver instance plans.
/// The scripted alarm plans.
@@ -29,33 +26,17 @@ public sealed record Phase7CompositionResult(
IReadOnlyList driverInstancePlans,
IReadOnlyList scriptedAlarmPlans)
: this(Array.Empty(), Array.Empty(),
- equipmentNodes, driverInstancePlans, scriptedAlarmPlans, Array.Empty())
- {
- }
-
- /// Convenience constructor for callers carrying UNS but not Galaxy data.
- /// The UNS areas.
- /// The UNS lines.
- /// The equipment nodes.
- /// The driver instance plans.
- /// The scripted alarm plans.
- public Phase7CompositionResult(
- IReadOnlyList unsAreas,
- IReadOnlyList unsLines,
- IReadOnlyList equipmentNodes,
- IReadOnlyList driverInstancePlans,
- IReadOnlyList scriptedAlarmPlans)
- : this(unsAreas, unsLines, equipmentNodes, driverInstancePlans, scriptedAlarmPlans, Array.Empty())
+ equipmentNodes, driverInstancePlans, scriptedAlarmPlans)
{
}
///
/// Equipment-namespace tags — a with non-null
- /// in an Equipment-kind namespace. Mirror of for the UNS
- /// equipment-signal path: Phase7Applier.MaterialiseEquipmentTags materialises each as
- /// a Variable under its existing equipment folder. Declared as an init-only member defaulting
- /// to empty (rather than a 7th positional parameter) so every existing convenience
- /// constructor + call site keeps compiling unchanged; new producers set it via initializer.
+ /// in an Equipment-kind namespace. Phase7Applier.MaterialiseEquipmentTags
+ /// materialises each as a Variable under its existing equipment folder. Declared as an
+ /// init-only member defaulting to empty (rather than a positional parameter) so every existing
+ /// convenience constructor + call site keeps compiling unchanged; new producers set it via
+ /// initializer.
///
public IReadOnlyList EquipmentTags { get; init; } = Array.Empty();
@@ -81,21 +62,6 @@ public sealed record EquipmentNode(string EquipmentId, string DisplayName, strin
public sealed record DriverInstancePlan(string DriverInstanceId, string DriverType, string ConfigJson);
public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentId, string PredicateScriptId, string MessageTemplate);
-///
-/// One Galaxy / SystemPlatform-namespace tag from a row where
-/// is null. Carries the FolderPath segment that the applier
-/// turns into a folder, the leaf for the Variable, the OPC UA
-/// , and the dot-form MXAccess reference ()
-/// that the Galaxy driver consumes when subscribing.
-///
-public sealed record GalaxyTagPlan(
- string TagId,
- string DriverInstanceId,
- string FolderPath,
- string DisplayName,
- string DataType,
- string MxAccessRef);
-
///
/// One Equipment-namespace tag from a row whose
/// is non-null and whose owning driver's namespace is Equipment-kind. Carries the stable
@@ -105,8 +71,7 @@ public sealed record GalaxyTagPlan(
/// , and the driver-side reference (extracted from
/// Tag.TagConfig) the later values milestone routes reads/writes by. The variable's NodeId
/// is folder-scoped (parent/Name), NOT , because a raw driver ref
-/// (e.g. a Modbus register) is not unique across identical machines. The equipment-signal
-/// analogue of .
+/// (e.g. a Modbus register) is not unique across identical machines.
///
public sealed record EquipmentTagPlan(
string TagId,
@@ -253,7 +218,7 @@ public sealed record EquipmentScriptedAlarmPlan(
///
public static class Phase7Composer
{
- /// Convenience overload for legacy callers + tests that don't yet supply UNS / Galaxy data.
+ /// Convenience overload for legacy callers + tests that don't supply UNS topology or tags.
/// The equipment.
/// The driver instances.
/// The scripted alarms.
@@ -265,7 +230,7 @@ public static class Phase7Composer
Compose(Array.Empty(), Array.Empty(), equipment, driverInstances, scriptedAlarms,
Array.Empty(), Array.Empty());
- /// UNS-aware overload that doesn't yet supply Galaxy tags.
+ /// UNS-aware overload that doesn't supply tags.
/// The UNS areas.
/// The UNS lines.
/// The equipment.
@@ -440,7 +405,7 @@ public static class Phase7Composer
Enabled: a.Enabled));
}
- return new Phase7CompositionResult(areas, lines, nodes, plans, alarms, Array.Empty())
+ return new Phase7CompositionResult(areas, lines, nodes, plans, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Plan.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Plan.cs
index 84ded60b..0641f75d 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Plan.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Plan.cs
@@ -21,10 +21,7 @@ public sealed record Phase7Plan(
IReadOnlyList ChangedDrivers,
IReadOnlyList AddedAlarms,
IReadOnlyList RemovedAlarms,
- IReadOnlyList ChangedAlarms,
- IReadOnlyList AddedGalaxyTags,
- IReadOnlyList RemovedGalaxyTags,
- IReadOnlyList ChangedGalaxyTags)
+ IReadOnlyList ChangedAlarms)
{
///
/// Equipment-namespace tag diff sets, keyed by . Added as
@@ -60,14 +57,12 @@ public sealed record Phase7Plan(
AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
- AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0 &&
AddedEquipmentTags.Count == 0 && RemovedEquipmentTags.Count == 0 && ChangedEquipmentTags.Count == 0 &&
AddedEquipmentVirtualTags.Count == 0 && RemovedEquipmentVirtualTags.Count == 0 && ChangedEquipmentVirtualTags.Count == 0;
public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
- public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
}
@@ -102,11 +97,6 @@ public static class Phase7Planner
a => a.ScriptedAlarmId,
(a, b) => new Phase7Plan.AlarmDelta(a, b));
- var (addedGalaxy, removedGalaxy, changedGalaxy) = DiffById(
- previous.GalaxyTags, next.GalaxyTags,
- t => t.TagId,
- (a, b) => new Phase7Plan.GalaxyTagDelta(a, b));
-
var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
previous.EquipmentTags, next.EquipmentTags,
t => t.TagId,
@@ -125,8 +115,7 @@ public static class Phase7Planner
return new Phase7Plan(
addedEq, removedEq, changedEq,
addedDrv, removedDrv, changedDrv,
- addedAlarm, removedAlarm, changedAlarm,
- addedGalaxy, removedGalaxy, changedGalaxy)
+ addedAlarm, removedAlarm, changedAlarm)
{
AddedEquipmentTags = addedEqTags,
RemovedEquipmentTags = removedEqTags,
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
index 7b5d0394..50ae20d0 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
@@ -192,12 +192,11 @@ public static class DeploymentArtifact
var equipment = ReadArray(root, "Equipment", ReadEquipmentNode);
var drivers = ReadArray(root, "DriverInstances", ReadDriverPlan);
var alarms = ReadArray(root, "ScriptedAlarms", ReadAlarmPlan);
- var galaxyTags = BuildGalaxyTagPlans(root, drivers);
var equipmentTags = BuildEquipmentTagPlans(root);
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
- return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms, galaxyTags)
+ return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms)
{
EquipmentTags = equipmentTags,
EquipmentVirtualTags = equipmentVirtualTags,
@@ -258,8 +257,7 @@ public static class DeploymentArtifact
keptLines,
keptEquipment,
full.DriverInstancePlans.Where(d => sets.DriverIds.Contains(d.DriverInstanceId)).ToArray(),
- full.ScriptedAlarmPlans.Where(a => sets.EquipmentIds.Contains(a.EquipmentId)).ToArray(),
- full.GalaxyTags.Where(t => sets.DriverIds.Contains(t.DriverInstanceId)).ToArray())
+ full.ScriptedAlarmPlans.Where(a => sets.EquipmentIds.Contains(a.EquipmentId)).ToArray())
{
EquipmentTags = full.EquipmentTags.Where(t => sets.DriverIds.Contains(t.DriverInstanceId)).ToArray(),
EquipmentVirtualTags = full.EquipmentVirtualTags.Where(v => sets.EquipmentIds.Contains(v.EquipmentId)).ToArray(),
@@ -354,95 +352,15 @@ public static class DeploymentArtifact
Array.Empty(),
Array.Empty(),
Array.Empty(),
- Array.Empty(),
- Array.Empty());
-
- ///
- /// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
- /// SystemPlatform-namespace tags (Galaxy hierarchy), then emit one
- /// per qualifying tag. Mirrors Phase7Composer.Compose's filter so a compose-side
- /// plan and an artifact-decode plan agree on the same set of tags.
- ///
- private static IReadOnlyList BuildGalaxyTagPlans(JsonElement root, IReadOnlyList drivers)
- {
- if (!root.TryGetProperty("Tags", out var tagsArr) || tagsArr.ValueKind != JsonValueKind.Array)
- return Array.Empty();
- if (!root.TryGetProperty("Namespaces", out var nsArr) || nsArr.ValueKind != JsonValueKind.Array)
- return Array.Empty();
- if (!root.TryGetProperty("DriverInstances", out var diArr) || diArr.ValueKind != JsonValueKind.Array)
- return Array.Empty();
-
- // namespaceId → Kind ("SystemPlatform"/"Equipment"/"Simulated") — enum serialises as int by default,
- // but ConfigComposer's snapshot uses default JsonSerializer which writes numbers. Tolerate both.
- var systemPlatformNamespaces = new HashSet(StringComparer.Ordinal);
- foreach (var el in nsArr.EnumerateArray())
- {
- if (el.ValueKind != JsonValueKind.Object) continue;
- var id = el.TryGetProperty("NamespaceId", out var idEl) ? idEl.GetString() : null;
- if (string.IsNullOrWhiteSpace(id)) continue;
- if (!el.TryGetProperty("Kind", out var kindEl)) continue;
- var isSystemPlatform = kindEl.ValueKind switch
- {
- JsonValueKind.Number => kindEl.GetInt32() == 1, // NamespaceKind.SystemPlatform = 1
- JsonValueKind.String => string.Equals(kindEl.GetString(), "SystemPlatform", StringComparison.Ordinal),
- _ => false,
- };
- if (isSystemPlatform) systemPlatformNamespaces.Add(id!);
- }
-
- // driverInstanceId → namespaceId
- var driverToNamespace = new Dictionary(StringComparer.Ordinal);
- foreach (var el in diArr.EnumerateArray())
- {
- if (el.ValueKind != JsonValueKind.Object) continue;
- var id = el.TryGetProperty("DriverInstanceId", out var idEl) ? idEl.GetString() : null;
- var ns = el.TryGetProperty("NamespaceId", out var nsEl) ? nsEl.GetString() : null;
- if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(ns))
- driverToNamespace[id!] = ns!;
- }
-
- var result = new List(tagsArr.GetArrayLength());
- foreach (var el in tagsArr.EnumerateArray())
- {
- if (el.ValueKind != JsonValueKind.Object) continue;
- // Skip tags with non-null EquipmentId (Equipment-namespace tags belong to a different path).
- if (el.TryGetProperty("EquipmentId", out var eqEl) && eqEl.ValueKind != JsonValueKind.Null) continue;
-
- var tagId = el.TryGetProperty("TagId", out var tEl) ? tEl.GetString() : null;
- var di = el.TryGetProperty("DriverInstanceId", out var diEl) ? diEl.GetString() : null;
- var name = el.TryGetProperty("Name", out var nmEl) ? nmEl.GetString() : null;
- var folder = el.TryGetProperty("FolderPath", out var fpEl) && fpEl.ValueKind != JsonValueKind.Null
- ? fpEl.GetString() : null;
- var dataType = el.TryGetProperty("DataType", out var dtEl) ? dtEl.GetString() : null;
-
- if (string.IsNullOrWhiteSpace(tagId) || string.IsNullOrWhiteSpace(di) || string.IsNullOrWhiteSpace(name))
- continue;
- if (!driverToNamespace.TryGetValue(di!, out var nsId)) continue;
- if (!systemPlatformNamespaces.Contains(nsId)) continue;
-
- var folderPath = folder ?? string.Empty;
- var mxRef = string.IsNullOrWhiteSpace(folderPath) ? name! : $"{folderPath}.{name}";
- result.Add(new GalaxyTagPlan(tagId!, di!, folderPath, name!, dataType ?? "BaseDataType", mxRef));
- }
-
- result.Sort((a, b) =>
- {
- var byDriver = string.CompareOrdinal(a.DriverInstanceId, b.DriverInstanceId);
- if (byDriver != 0) return byDriver;
- var byFolder = string.CompareOrdinal(a.FolderPath, b.FolderPath);
- if (byFolder != 0) return byFolder;
- return string.CompareOrdinal(a.DisplayName, b.DisplayName);
- });
- return result;
- }
+ Array.Empty());
///
/// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
/// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then
/// emit one per qualifying tag. The artifact-decode mirror of
- /// Phase7Composer.Compose's equipment filter — the inverse of
- /// — so the compose-side + artifact-decode plans agree on the same set of tags. FullName is
- /// read from each tag's TagConfig blob (top-level "FullName" field).
+ /// Phase7Composer.Compose's equipment filter — so the compose-side + artifact-decode
+ /// plans agree on the same set of tags. FullName is read from each tag's TagConfig blob
+ /// (top-level "FullName" field).
///
private static IReadOnlyList BuildEquipmentTagPlans(JsonElement root)
{
@@ -454,7 +372,7 @@ public static class DeploymentArtifact
return Array.Empty();
// namespaceId → Equipment-kind. Kind serialises as a number by default (Equipment = 0);
- // tolerate the string form too (matches BuildGalaxyTagPlans's number/string handling).
+ // tolerate the string form too.
var equipmentNamespaces = new HashSet(StringComparer.Ordinal);
foreach (var el in nsArr.EnumerateArray())
{
@@ -471,11 +389,8 @@ public static class DeploymentArtifact
if (isEquipment) equipmentNamespaces.Add(id!);
}
- // driverInstanceId → namespaceId, and driverInstanceId → DriverType. The DriverType map admits
- // a Galaxy alias (a GalaxyMxGateway-backed equipment-scoped tag) that lives in a SystemPlatform
- // namespace — byte-parity with the composer's `di.DriverType == "GalaxyMxGateway"` clause.
+ // driverInstanceId → namespaceId.
var driverToNamespace = new Dictionary(StringComparer.Ordinal);
- var driverToType = new Dictionary(StringComparer.Ordinal);
foreach (var el in diArr.EnumerateArray())
{
if (el.ValueKind != JsonValueKind.Object) continue;
@@ -483,9 +398,6 @@ public static class DeploymentArtifact
var ns = el.TryGetProperty("NamespaceId", out var nsEl) ? nsEl.GetString() : null;
if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(ns))
driverToNamespace[id!] = ns!;
- var dtype = el.TryGetProperty("DriverType", out var dtEl) ? dtEl.GetString() : null;
- if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(dtype))
- driverToType[id!] = dtype!;
}
var result = new List(tagsArr.GetArrayLength());
@@ -508,10 +420,10 @@ public static class DeploymentArtifact
if (string.IsNullOrWhiteSpace(tagId) || string.IsNullOrWhiteSpace(di) || string.IsNullOrWhiteSpace(name)) continue;
if (!driverToNamespace.TryGetValue(di!, out var nsId)) continue;
- // A GalaxyMxGateway-backed alias qualifies even though its namespace is SystemPlatform-kind
- // (not Equipment) — byte-parity with the composer's broadened equipment-tag filter.
- var isGalaxyAlias = driverToType.TryGetValue(di!, out var dtype2) && dtype2 == "GalaxyMxGateway";
- if (!equipmentNamespaces.Contains(nsId) && !isGalaxyAlias) continue;
+ // Equipment-kind namespace only — byte-parity with the composer's pure
+ // `ns.Kind == NamespaceKind.Equipment` predicate (no Galaxy exception). Galaxy points are
+ // ordinary equipment tags now (GalaxyMxGateway is a standard Equipment-kind driver).
+ if (!equipmentNamespaces.Contains(nsId)) continue;
result.Add(new EquipmentTagPlan(
TagId: tagId!,
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
index 6e2247b6..f71b3a08 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
@@ -547,8 +547,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
}
///
- /// SubscribeBulk pass. After an apply, read the deployment's SystemPlatform / Galaxy tags,
- /// group their dot-form MXAccess references by driver instance, and hand each running driver
+ /// SubscribeBulk pass. After an apply, read the deployment's Equipment-namespace tags,
+ /// group their driver-side FullName references by driver instance, and hand each running driver
/// child its desired subscription set via .
/// The child retains the set and (re)subscribes on every Connected entry, so values stream into
/// the OPC UA sink and resume after reconnects. Drivers with no configured tags get an empty set
@@ -582,11 +582,11 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return;
}
- var refsByDriver = composition.GalaxyTags
+ var refsByDriver = composition.EquipmentTags
.GroupBy(t => t.DriverInstanceId, StringComparer.Ordinal)
.ToDictionary(
g => g.Key,
- g => (IReadOnlyList)g.Select(t => t.MxAccessRef)
+ g => (IReadOnlyList)g.Select(t => t.FullName)
.Distinct(StringComparer.Ordinal)
.ToArray(),
StringComparer.Ordinal);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
index a6deca75..abde4509 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
@@ -63,8 +63,7 @@ public sealed class OpcUaPublishActor : ReceiveActor
Array.Empty(),
Array.Empty(),
Array.Empty(),
- Array.Empty(),
- Array.Empty());
+ Array.Empty());
/// Gets the number of writes performed.
public int WriteCount => _writes;
@@ -242,10 +241,6 @@ public sealed class OpcUaPublishActor : ReceiveActor
// nodes (keyed by ScriptedAlarmId so AlarmStateUpdate writes target them); disabled
// alarms are skipped.
_applier.MaterialiseScriptedAlarms(composition);
- // Galaxy / SystemPlatform tags get their own pass: ensures their FolderPath folder
- // + Variable node exist so clients can browse them. The Galaxy driver fills values
- // on a future SubscribeBulk pass; until then variables show BadWaitingForInitialData.
- _applier.MaterialiseGalaxyTags(composition);
// Equipment-namespace tags get their own pass: ensures each signal's Variable (and any
// FolderPath sub-folder) exists under its already-materialised equipment folder so
// clients can browse them. Live values arrive in a later milestone; until then the
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs
index 5d7b23ef..2a51c508 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierHierarchyTests.cs
@@ -36,8 +36,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
UnsLines: new[] { new UnsLineProjection("line-1", "area-1", "Cell A") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Pump-1", "line-1") },
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty());
+ ScriptedAlarmPlans: Array.Empty());
applier.MaterialiseHierarchy(composition);
@@ -60,8 +59,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
UnsLines: Array.Empty(),
EquipmentNodes: new[] { new EquipmentNode("eq-orphan", "Orphan", UnsLineId: "") },
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty());
+ ScriptedAlarmPlans: Array.Empty());
applier.MaterialiseHierarchy(composition);
@@ -95,8 +93,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty()));
+ ScriptedAlarmPlans: Array.Empty()));
sdkServer.NodeManager!.FolderCount.ShouldBe(5); // 2 areas + 1 line + 2 equipment
@@ -106,8 +103,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
UnsLines: new[] { new UnsLineProjection("line-1", "area-A", "Line 1") },
EquipmentNodes: new[] { new EquipmentNode("eq-1", "Eq 1", "line-1"), new EquipmentNode("eq-2", "Eq 2", "line-1") },
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty()));
+ ScriptedAlarmPlans: Array.Empty()));
sdkServer.NodeManager!.FolderCount.ShouldBe(5);
}
@@ -143,8 +139,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
UnsLines: Array.Empty(),
EquipmentNodes: new[] { new EquipmentNode("eq-1", "filling-eq", UnsLineId: "") },
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty())
+ ScriptedAlarmPlans: Array.Empty())
{
EquipmentTags = new[]
{
@@ -201,7 +196,7 @@ public sealed class Phase7ApplierHierarchyTests : IDisposable
new[] { area }, new[] { line }, new[] { equipment }, new[] { driver },
Array.Empty(), new[] { tag }, new[] { ns });
- // Compose-side EquipmentTags extraction (the inverse of the Galaxy filter).
+ // Compose-side EquipmentTags extraction.
var planned = composition.EquipmentTags.ShouldHaveSingleItem();
planned.EquipmentId.ShouldBe("eq-1");
planned.FullName.ShouldBe("40001");
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
index 387ba14e..6820cd42 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
@@ -59,10 +59,7 @@ public sealed class Phase7ApplierTests
ChangedDrivers: Array.Empty(),
AddedAlarms: Array.Empty(),
RemovedAlarms: Array.Empty(),
- ChangedAlarms: Array.Empty(),
- AddedGalaxyTags: Array.Empty(),
- RemovedGalaxyTags: Array.Empty(),
- ChangedGalaxyTags: Array.Empty());
+ ChangedAlarms: Array.Empty());
var outcome = applier.Apply(plan);
@@ -93,10 +90,7 @@ public sealed class Phase7ApplierTests
},
AddedAlarms: Array.Empty(),
RemovedAlarms: Array.Empty(),
- ChangedAlarms: Array.Empty(),
- AddedGalaxyTags: Array.Empty(),
- RemovedGalaxyTags: Array.Empty(),
- ChangedGalaxyTags: Array.Empty());
+ ChangedAlarms: Array.Empty());
var outcome = applier.Apply(plan);
@@ -118,66 +112,6 @@ public sealed class Phase7ApplierTests
outcome.RebuildCalled.ShouldBeTrue();
}
- /// Verifies MaterialiseGalaxyTags creates one folder per distinct FolderPath and one
- /// variable per tag, with root-level tags hung directly under the namespace root.
- [Fact]
- public void MaterialiseGalaxyTags_creates_folder_per_distinct_path_and_variable_per_tag()
- {
- var sink = new RecordingSink();
- var applier = new Phase7Applier(sink, NullLogger.Instance);
-
- var composition = new Phase7CompositionResult(
- UnsAreas: Array.Empty(),
- UnsLines: Array.Empty(),
- EquipmentNodes: Array.Empty(),
- DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: new[]
- {
- new GalaxyTagPlan("t1", "drv", "section.area", "Temperature", "Float", "section.area.Temperature"),
- new GalaxyTagPlan("t2", "drv", "", "Pressure", "Int32", "Pressure"),
- });
-
- applier.MaterialiseGalaxyTags(composition);
-
- // One folder for the single distinct non-empty FolderPath; the root-level tag adds none.
- sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("section.area", null, "section.area"));
-
- // Foldered tag → NodeId is its MxAccessRef under the FolderPath parent.
- // Root-level tag → NodeId is its DisplayName under the root (null parent).
- sink.VariableCalls.ShouldContain(("section.area.Temperature", "section.area", "Temperature", "Float"));
- sink.VariableCalls.ShouldContain(("Pressure", (string?)null, "Pressure", "Int32"));
- sink.VariableCalls.Count.ShouldBe(2);
- }
-
- /// Verifies that two tags sharing a FolderPath produce a single EnsureFolder call
- /// (deduped) but one EnsureVariable per tag.
- [Fact]
- public void MaterialiseGalaxyTags_dedupes_folders_for_tags_sharing_a_path()
- {
- var sink = new RecordingSink();
- var applier = new Phase7Applier(sink, NullLogger.Instance);
-
- var composition = new Phase7CompositionResult(
- UnsAreas: Array.Empty(),
- UnsLines: Array.Empty(),
- EquipmentNodes: Array.Empty(),
- DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: new[]
- {
- new GalaxyTagPlan("t1", "drv", "line.cell", "Speed", "Float", "line.cell.Speed"),
- new GalaxyTagPlan("t2", "drv", "line.cell", "Torque", "Float", "line.cell.Torque"),
- });
-
- applier.MaterialiseGalaxyTags(composition);
-
- sink.FolderCalls.ShouldHaveSingleItem().ShouldBe(("line.cell", null, "line.cell"));
- sink.VariableCalls.Count.ShouldBe(2);
- sink.VariableCalls.ShouldContain(("line.cell.Speed", "line.cell", "Speed", "Float"));
- sink.VariableCalls.ShouldContain(("line.cell.Torque", "line.cell", "Torque", "Float"));
- }
-
/// Verifies MaterialiseEquipmentTags creates one Variable per equipment tag directly
/// under its existing equipment folder, with a folder-scoped NodeId (parent/Name — NOT the raw
/// FullName), parent == EquipmentId, displayName == Name, and does NOT re-create the equipment
@@ -193,8 +127,7 @@ public sealed class Phase7ApplierTests
UnsLines: Array.Empty(),
EquipmentNodes: Array.Empty(),
DriverInstancePlans: Array.Empty(),
- ScriptedAlarmPlans: Array.Empty(),
- GalaxyTags: Array.Empty())
+ ScriptedAlarmPlans: Array.Empty())
{
EquipmentTags = new[]
{
@@ -345,8 +278,8 @@ public sealed class Phase7ApplierTests
}
/// Verifies that added equipment tags in an otherwise-empty plan trigger an
- /// address-space rebuild (parity with the Galaxy-tag path — the planner now diffs equipment
- /// tags, so a tags-only deploy is no longer a silent no-op).
+ /// address-space rebuild (the planner now diffs equipment tags, so a tags-only deploy is no
+ /// longer a silent no-op).
[Fact]
public void Added_equipment_tags_trigger_rebuild()
{
@@ -393,42 +326,10 @@ public sealed class Phase7ApplierTests
sink.RebuildCalls.ShouldBe(1);
}
- /// Verifies that added Galaxy tags in an otherwise-empty plan trigger an address-space rebuild.
- [Fact]
- public void Added_galaxy_tags_trigger_rebuild()
- {
- var sink = new RecordingSink();
- var applier = new Phase7Applier(sink, NullLogger.Instance);
-
- var plan = new Phase7Plan(
- AddedEquipment: Array.Empty(),
- RemovedEquipment: Array.Empty(),
- ChangedEquipment: Array.Empty(),
- AddedDrivers: Array.Empty(),
- RemovedDrivers: Array.Empty(),
- ChangedDrivers: Array.Empty(),
- AddedAlarms: Array.Empty(),
- RemovedAlarms: Array.Empty(),
- ChangedAlarms: Array.Empty(),
- AddedGalaxyTags: new[]
- {
- new GalaxyTagPlan("t1", "drv", "section.area", "Temperature", "Float", "section.area.Temperature"),
- },
- RemovedGalaxyTags: Array.Empty(),
- ChangedGalaxyTags: Array.Empty());
-
- var outcome = applier.Apply(plan);
-
- outcome.RebuildCalled.ShouldBeTrue();
- outcome.AddedNodes.ShouldBe(1);
- sink.RebuildCalls.ShouldBe(1);
- }
-
private static Phase7Plan EmptyPlan => new(
Array.Empty(), Array.Empty(), Array.Empty(),
Array.Empty(), Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(), Array.Empty(),
- Array.Empty(), Array.Empty(), Array.Empty());
+ Array.Empty(), Array.Empty(), Array.Empty());
private static Phase7Plan WithEquipmentRemoval(params string[] ids) => new(
AddedEquipment: Array.Empty(),
@@ -439,10 +340,7 @@ public sealed class Phase7ApplierTests
ChangedDrivers: Array.Empty(),
AddedAlarms: Array.Empty(),
RemovedAlarms: Array.Empty(),
- ChangedAlarms: Array.Empty(),
- AddedGalaxyTags: Array.Empty(),
- RemovedGalaxyTags: Array.Empty(),
- ChangedGalaxyTags: Array.Empty());
+ ChangedAlarms: Array.Empty());
private sealed class RecordingSink : IOpcUaAddressSpaceSink
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerAliasTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerAliasTagTests.cs
index f5ad9e28..da7dcee8 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerAliasTagTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerAliasTagTests.cs
@@ -11,16 +11,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// Equipment-kind driver. An equipment-scoped (non-null
/// ) bound to a GalaxyMxGateway driver living in an
/// Equipment-kind namespace must surface under
-/// (carrying its driver-side FullName), and
-/// the retired SystemPlatform-mirror producer means
-/// is always empty.
+/// (carrying its driver-side FullName). The
+/// SystemPlatform-mirror GalaxyTags contract is retired entirely.
///
public sealed class Phase7ComposerAliasTagTests
{
/// A GalaxyMxGateway driver in an Equipment-kind namespace carries an
/// equipment-scoped Galaxy tag (EquipmentId set, FolderPath null, TagConfig FullName = the Galaxy
- /// ref). Compose must put it in EquipmentTags with its FullName, and GalaxyTags must be empty
- /// (the SystemPlatform mirror producer is gone).
+ /// ref). Compose must put it in EquipmentTags with its FullName, coalescing the null FolderPath to
+ /// string.Empty (the SystemPlatform mirror producer is gone entirely).
[Fact]
public void Compose_admits_galaxy_equipment_tag_in_equipment_tags()
{
@@ -77,8 +76,8 @@ public sealed class Phase7ComposerAliasTagTests
tag.Name.ShouldBe("TestChangingInt");
tag.DataType.ShouldBe("Int32");
tag.FullName.ShouldBe("TestMachine_020.TestChangingInt");
-
- // The SystemPlatform-mirror producer is retired → GalaxyTags is always empty.
- result.GalaxyTags.ShouldBeEmpty();
+ // The input Tag.FolderPath is null; the composer coalesces it to string.Empty (the explicit
+ // byte-parity null-coalesce the artifact-decode side mirrors).
+ tag.FolderPath.ShouldBe(string.Empty);
}
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
index 5e619e16..61bafad2 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
@@ -7,19 +7,65 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
///
/// Verifies the artifact-decode mirror ()
-/// admits a Galaxy alias tag — an equipment-scoped tag (non-null EquipmentId) bound to a
-/// GalaxyMxGateway driver in a SystemPlatform-kind namespace — into the decoded
-/// EquipmentTags with byte-parity to the live-edit composer path: same FullName, EquipmentId,
-/// DriverInstanceId, Name, DataType. The composer broadens the same filter by DriverType, so both
-/// data-contract sites must agree on which tags qualify.
+/// treats a Galaxy point as an ordinary equipment tag — an equipment-scoped tag (non-null
+/// EquipmentId) bound to a GalaxyMxGateway driver in an Equipment-kind namespace —
+/// into the decoded EquipmentTags with byte-parity to the live-edit composer path: same FullName,
+/// EquipmentId, DriverInstanceId, Name, DataType. Both data-contract sites gate purely on the namespace
+/// Kind being Equipment (no Galaxy/DriverType exception — the SystemPlatform-mirror contract is
+/// retired), so they agree on which tags qualify.
///
public sealed class DeploymentArtifactAliasParityTests
{
- /// An artifact JSON blob with a GalaxyMxGateway driver in a SystemPlatform (Kind=1)
- /// namespace and one equipment-scoped alias tag (EquipmentId set, FolderPath null, FullName = the
- /// Galaxy ref). Decode must surface the alias in EquipmentTags carrying its driver-side FullName.
+ /// An artifact JSON blob with a GalaxyMxGateway driver in an Equipment (Kind=0) namespace and
+ /// one equipment-scoped tag (EquipmentId set, FolderPath null, FullName = the Galaxy ref). Decode must
+ /// surface the tag in EquipmentTags carrying its driver-side FullName, coalescing the null FolderPath to
+ /// string.Empty.
[Fact]
- public void ParseComposition_admits_galaxy_alias_tag_in_equipment_tags()
+ public void ParseComposition_admits_galaxy_equipment_tag_in_equipment_tags()
+ {
+ var blob = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ Namespaces = new[]
+ {
+ new { NamespaceId = "ns-eq", Kind = 0 }, // NamespaceKind.Equipment
+ },
+ DriverInstances = new[]
+ {
+ new { DriverInstanceId = "drv-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", NamespaceId = "ns-eq" },
+ },
+ Tags = new object[]
+ {
+ new
+ {
+ TagId = "tag-galaxy",
+ DriverInstanceId = "drv-galaxy",
+ EquipmentId = "eq-1",
+ Name = "TestChangingInt",
+ FolderPath = (string?)null,
+ DataType = "Int32",
+ TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}",
+ },
+ },
+ });
+
+ var c = DeploymentArtifact.ParseComposition(blob);
+
+ var tag = c.EquipmentTags.ShouldHaveSingleItem();
+ tag.TagId.ShouldBe("tag-galaxy");
+ tag.EquipmentId.ShouldBe("eq-1");
+ tag.DriverInstanceId.ShouldBe("drv-galaxy");
+ tag.Name.ShouldBe("TestChangingInt");
+ tag.DataType.ShouldBe("Int32");
+ tag.FolderPath.ShouldBe(string.Empty);
+ tag.FullName.ShouldBe("TestMachine_020.TestChangingInt");
+ }
+
+ /// An equipment-scoped GalaxyMxGateway tag in a SystemPlatform-kind namespace must NOT surface
+ /// in EquipmentTags — byte-parity with the composer's pure ns.Kind == NamespaceKind.Equipment
+ /// predicate. The retired SystemPlatform-mirror contract no longer carried a DriverType exception, so a
+ /// non-Equipment namespace excludes the tag regardless of driver type.
+ [Fact]
+ public void ParseComposition_excludes_galaxy_tag_in_non_equipment_namespace()
{
var blob = JsonSerializer.SerializeToUtf8Bytes(new
{
@@ -32,54 +78,11 @@ public sealed class DeploymentArtifactAliasParityTests
new { DriverInstanceId = "drv-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", NamespaceId = "ns-sp" },
},
Tags = new object[]
- {
- new
- {
- TagId = "tag-alias",
- DriverInstanceId = "drv-galaxy",
- EquipmentId = "eq-1",
- Name = "TestChangingInt",
- FolderPath = (string?)null,
- DataType = "Int32",
- TagConfig = "{\"FullName\":\"TestMachine_020.TestChangingInt\"}",
- },
- },
- });
-
- var c = DeploymentArtifact.ParseComposition(blob);
-
- var alias = c.EquipmentTags.ShouldHaveSingleItem();
- alias.TagId.ShouldBe("tag-alias");
- alias.EquipmentId.ShouldBe("eq-1");
- alias.DriverInstanceId.ShouldBe("drv-galaxy");
- alias.Name.ShouldBe("TestChangingInt");
- alias.DataType.ShouldBe("Int32");
- alias.FolderPath.ShouldBe(string.Empty);
- alias.FullName.ShouldBe("TestMachine_020.TestChangingInt");
- }
-
- /// An equipment-scoped tag bound to a non-Galaxy driver in a SystemPlatform namespace is
- /// NOT a Galaxy alias and must stay excluded from EquipmentTags — the broadened clause keys on the
- /// GalaxyMxGateway DriverType, not on the namespace kind, so the contract narrows correctly.
- [Fact]
- public void ParseComposition_excludes_non_galaxy_systemplatform_equipment_tag()
- {
- var blob = JsonSerializer.SerializeToUtf8Bytes(new
- {
- Namespaces = new[]
- {
- new { NamespaceId = "ns-sp", Kind = 1 }, // NamespaceKind.SystemPlatform
- },
- DriverInstances = new[]
- {
- new { DriverInstanceId = "drv-modbus", DriverType = "Modbus", DriverConfig = "{}", NamespaceId = "ns-sp" },
- },
- Tags = new object[]
{
new
{
TagId = "tag-x",
- DriverInstanceId = "drv-modbus",
+ DriverInstanceId = "drv-galaxy",
EquipmentId = "eq-1",
Name = "Source",
FolderPath = (string?)null,
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs
index e20fd048..832af534 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs
@@ -193,9 +193,10 @@ public sealed class DeploymentArtifactTests
///
/// Verifies ParseComposition surfaces Equipment-namespace tags (non-null EquipmentId in an
/// Equipment-kind namespace) as EquipmentTags, with FullName extracted
- /// from the tag's TagConfig blob — the equipment-signal mirror of the Galaxy-tag path. A
- /// SystemPlatform (Galaxy) tag in the same blob must NOT leak into EquipmentTags and must
- /// still route to GalaxyTags.
+ /// from the tag's TagConfig blob. A tag in a non-Equipment (SystemPlatform) namespace with a
+ /// null EquipmentId must NOT surface in EquipmentTags — byte-parity with the composer's pure
+ /// ns.Kind == NamespaceKind.Equipment predicate (the SystemPlatform-mirror contract is
+ /// retired, so such a tag routes nowhere).
///
[Fact]
public void ParseComposition_reads_EquipmentTags_from_equipment_namespace()
@@ -247,8 +248,9 @@ public sealed class DeploymentArtifactTests
tag.DataType.ShouldBe("Float");
tag.FullName.ShouldBe("40001"); // extracted from TagConfig, not the raw blob
- // The Galaxy tag still routes to GalaxyTags and does NOT leak into EquipmentTags.
- c.GalaxyTags.ShouldContain(g => g.TagId == "tag-gx");
+ // The SystemPlatform tag (null EquipmentId, non-Equipment namespace) does NOT leak into
+ // EquipmentTags — byte-parity with the composer's pure ns.Kind == Equipment predicate.
+ c.EquipmentTags.ShouldNotContain(t => t.TagId == "tag-gx");
}
///
@@ -387,15 +389,18 @@ public sealed class DeploymentArtifactTests
new { DriverInstanceId = "main-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
new { DriverInstanceId = "sa-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
},
+ // Galaxy points are ordinary equipment tags now — Equipment-kind namespaces with non-null
+ // EquipmentId, so the cluster-scoped decode filters them via EquipmentTags (by their driver's
+ // cluster), exactly as it filtered the retired GalaxyTags.
Namespaces = new[]
{
- new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 1 },
- new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 1 },
+ new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 0 },
+ new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 0 },
},
Tags = new[]
{
- new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)null, Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
- new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)null, Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)"eq-main", Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)"eq-sa", Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
},
};
@@ -406,18 +411,18 @@ public sealed class DeploymentArtifactTests
var main = DeploymentArtifact.ParseComposition(blob, "central-1:4053");
main.DriverInstancePlans.Select(d => d.DriverInstanceId).ShouldBe(new[] { "main-galaxy" });
- main.GalaxyTags.Select(t => t.TagId).ShouldBe(new[] { "t-main" });
+ main.EquipmentTags.Select(t => t.TagId).ShouldBe(new[] { "t-main" });
var siteA = DeploymentArtifact.ParseComposition(blob, "site-a-1:4053");
siteA.DriverInstancePlans.Select(d => d.DriverInstanceId).ShouldBe(new[] { "sa-galaxy" });
- siteA.GalaxyTags.Select(t => t.TagId).ShouldBe(new[] { "t-sa" });
+ siteA.EquipmentTags.Select(t => t.TagId).ShouldBe(new[] { "t-sa" });
}
[Fact]
public void ParseComposition_scoped_unknown_node_is_empty()
{
var comp = DeploymentArtifact.ParseComposition(BlobOf(MultiClusterSnapshotWithTags()), "ghost-9:4053");
- comp.GalaxyTags.ShouldBeEmpty();
+ comp.EquipmentTags.ShouldBeEmpty();
comp.DriverInstancePlans.ShouldBeEmpty();
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
index d524f602..c399e456 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
@@ -102,10 +102,11 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
/// Wiring proof for per-ClusterId scoping (Task 4): a multi-cluster artifact must
/// materialise ONLY the local node's cluster slice. Mirrors the multi-cluster artifact
/// shape exercised in DeploymentArtifactTests (MAIN + SITE-A, one Galaxy driver +
- /// one SystemPlatform tag each). The scoped rebuild for the SITE-A node must surface the
- /// SITE-A tag (t-sa → variable F.S1) and NOT MAIN's (t-main →
- /// F.M1); the mirror holds for the MAIN node. Without the production scoping edit,
- /// the unscoped parse would materialise BOTH variables on every node.
+ /// one equipment tag each — Galaxy points are ordinary equipment tags now). The scoped
+ /// rebuild for the SITE-A node must surface the SITE-A tag (t-sa → folder-scoped
+ /// variable eq-sa/F/S1) and NOT MAIN's (t-main → eq-main/F/M1); the
+ /// mirror holds for the MAIN node. Without the production scoping edit, the unscoped parse
+ /// would materialise BOTH variables on every node.
///
[Fact]
public void Rebuild_materialises_only_the_nodes_cluster()
@@ -125,10 +126,10 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
siteActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
AwaitAssert(() => sinkA.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
- // t-sa (Name "S1", FolderPath "F") → MxAccessRef "F.S1" → variable node "F.S1".
- sinkA.Calls.ShouldContain("EV:F.S1");
+ // t-sa (EquipmentId "eq-sa", FolderPath "F", Name "S1") → folder-scoped variable "eq-sa/F/S1".
+ sinkA.Calls.ShouldContain("EV:eq-sa/F/S1");
// t-main (MAIN cluster) must NOT leak onto the SITE-A node.
- sinkA.Calls.ShouldNotContain("EV:F.M1");
+ sinkA.Calls.ShouldNotContain("EV:eq-main/F/M1");
// --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. ---
var dbM = NewInMemoryDbFactory();
@@ -145,15 +146,15 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
mainActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
AwaitAssert(() => sinkM.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
- sinkM.Calls.ShouldContain("EV:F.M1");
- sinkM.Calls.ShouldNotContain("EV:F.S1");
+ sinkM.Calls.ShouldContain("EV:eq-main/F/M1");
+ sinkM.Calls.ShouldNotContain("EV:eq-sa/F/S1");
}
///
/// Seal a 2-cluster deployment (MAIN + SITE-A) whose artifact mirrors the multi-cluster
- /// shape the composer emits: a Clusters + Nodes map, one SystemPlatform
- /// namespace + Galaxy driver + Galaxy tag per cluster. Used by
- /// .
+ /// shape the composer emits: a Clusters + Nodes map, one Equipment namespace +
+ /// Galaxy driver + equipment tag per cluster (Galaxy points are ordinary equipment tags now).
+ /// Used by .
///
private static void SeedMultiClusterDeployment(IDbContextFactory dbFactory)
{
@@ -165,6 +166,21 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
new { NodeId = "central-1:4053", ClusterId = "MAIN" },
new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
},
+ UnsAreas = new[]
+ {
+ new { UnsAreaId = "area-main", ClusterId = "MAIN", Name = "main-area" },
+ new { UnsAreaId = "area-sa", ClusterId = "SITE-A", Name = "sa-area" },
+ },
+ UnsLines = new[]
+ {
+ new { UnsLineId = "line-main", UnsAreaId = "area-main", Name = "main-line" },
+ new { UnsLineId = "line-sa", UnsAreaId = "area-sa", Name = "sa-line" },
+ },
+ Equipment = new[]
+ {
+ new { EquipmentId = "eq-main", DriverInstanceId = "main-galaxy", UnsLineId = "line-main", Name = "eq-main", MachineCode = "EQ-MAIN" },
+ new { EquipmentId = "eq-sa", DriverInstanceId = "sa-galaxy", UnsLineId = "line-sa", Name = "eq-sa", MachineCode = "EQ-SA" },
+ },
DriverInstances = new[]
{
new { DriverInstanceId = "main-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
@@ -172,13 +188,13 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
},
Namespaces = new[]
{
- new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 1 }, // NamespaceKind.SystemPlatform
- new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 1 },
+ new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 0 }, // NamespaceKind.Equipment
+ new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 0 },
},
Tags = new[]
{
- new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)null, Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
- new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)null, Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)"eq-main", Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)"eq-sa", Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
},
ScriptedAlarms = Array.Empty