feat(opcua): remove SystemPlatform-mirror GalaxyTags contract end-to-end (composer+applier+artifact, byte-parity)
This commit is contained in:
@@ -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<UnsLineProjection>(),
|
||||
Array.Empty<EquipmentNode>(),
|
||||
Array.Empty<DriverInstancePlan>(),
|
||||
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;
|
||||
}
|
||||
Array.Empty<ScriptedAlarmPlan>());
|
||||
|
||||
/// <summary>
|
||||
/// Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
|
||||
/// Equipment-namespace tags (non-null EquipmentId, owning namespace Kind == Equipment), then
|
||||
/// 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"/>
|
||||
/// — 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).
|
||||
/// <c>Phase7Composer.Compose</c>'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).
|
||||
/// </summary>
|
||||
private static IReadOnlyList<EquipmentTagPlan> BuildEquipmentTagPlans(JsonElement root)
|
||||
{
|
||||
@@ -454,7 +372,7 @@ public static class DeploymentArtifact
|
||||
return Array.Empty<EquipmentTagPlan>();
|
||||
|
||||
// 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);
|
||||
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<string, string>(StringComparer.Ordinal);
|
||||
var driverToType = new Dictionary<string, string>(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<EquipmentTagPlan>(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!,
|
||||
|
||||
@@ -547,8 +547,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="DriverInstanceActor.SetDesiredSubscriptions"/>.
|
||||
/// 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<string>)g.Select(t => t.MxAccessRef)
|
||||
g => (IReadOnlyList<string>)g.Select(t => t.FullName)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray(),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
@@ -63,8 +63,7 @@ public sealed class OpcUaPublishActor : ReceiveActor
|
||||
Array.Empty<UnsLineProjection>(),
|
||||
Array.Empty<EquipmentNode>(),
|
||||
Array.Empty<DriverInstancePlan>(),
|
||||
Array.Empty<ScriptedAlarmPlan>(),
|
||||
Array.Empty<GalaxyTagPlan>());
|
||||
Array.Empty<ScriptedAlarmPlan>());
|
||||
|
||||
/// <summary>Gets the number of writes performed.</summary>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user