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