Some checks failed
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Closes the gap where Tag rows with EquipmentId=NULL + Namespace.Kind=SystemPlatform (Galaxy hierarchy) existed in ConfigDb but were never surfaced in the OPC UA address space. Now they materialise as Variable nodes under a folder named for their FolderPath, browseable through any OPC UA client. Layers touched: - IOpcUaAddressSpaceSink: new EnsureVariable(nodeId, parentFolderId, displayName, dataType) signature on the sink interface, NullSink, DeferredSink, SdkSink. - OtOpcUaNodeManager.EnsureVariable: creates a BaseDataVariableState parented under the named folder (or root), initial Value=null + StatusCode=BadWaitingForInitialData; resolves Tag.DataType strings to the matching OPC UA built-in NodeId. Idempotent. - Phase7CompositionResult: new GalaxyTags collection of GalaxyTagPlan records carrying (TagId, DriverInstanceId, FolderPath, DisplayName, DataType, MxAccessRef). Constructor overloads keep existing call sites compiling. - Phase7Composer.Compose: now takes Tag + Namespace inputs, filters for SystemPlatform-namespace tags with EquipmentId=NULL, emits GalaxyTagPlan rows with MXAccess ref "FolderPath.Name". - Phase7Plan: new AddedGalaxyTags / RemovedGalaxyTags / ChangedGalaxyTags collections + GalaxyTagDelta record; IsEmpty + needsRebuild updated. - Phase7Planner.Compute: diffs GalaxyTags by TagId via existing DiffById helper. - DeploymentArtifact.ParseComposition: reads the Tags + Namespaces + DriverInstances arrays the ConfigComposer already emits, applies the same SystemPlatform filter, returns the same GalaxyTagPlan list as the composer so artifact-side and compose-side plans agree. - Phase7Applier: new MaterialiseGalaxyTags pass that ensures one folder per distinct FolderPath then one Variable per tag. NodeId for the variable is "<FolderPath>.<Name>" matching the MXAccess ref so the future Galaxy SubscribeBulk wiring can address them directly. - OpcUaPublishActor.RebuildAddressSpace: invokes MaterialiseGalaxyTags after MaterialiseHierarchy. _lastApplied initialiser updated for the new ctor. - seed-clusters.sql: pre-existing TestMachine_001.TestAlarm001..003 rows needed no change — the composer/applier now picks them up automatically. Verified end-to-end via docker-dev: deploy click → driver-a logs "Phase7Applier: Galaxy tags materialised (tags=3, folders=1)" → OPC UA Client CLI browses the three Variable nodes under TestMachine_001 folder. Reads return BadWaitingForInitialData status (expected — Galaxy driver's SubscribeBulk wiring to push values into the nodes is the remaining follow-up).
110 lines
5.1 KiB
C#
110 lines
5.1 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
|
|
|
/// <summary>
|
|
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
|
|
/// <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied
|
|
/// deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
|
|
/// captured by stable identity: added items are new, removed items have to be torn down,
|
|
/// changed items have the same identity but at least one field differs.
|
|
///
|
|
/// OpcUaPublishActor's <c>RebuildAddressSpace</c> consumes this against a real
|
|
/// <see cref="Commons.OpcUa.IOpcUaAddressSpaceSink"/> binding so re-applies only mutate the
|
|
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
|
|
/// drastic schema flips.
|
|
/// </summary>
|
|
public sealed record Phase7Plan(
|
|
IReadOnlyList<EquipmentNode> AddedEquipment,
|
|
IReadOnlyList<EquipmentNode> RemovedEquipment,
|
|
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
|
|
IReadOnlyList<DriverInstancePlan> AddedDrivers,
|
|
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
|
|
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
|
|
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
|
|
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
|
|
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms,
|
|
IReadOnlyList<GalaxyTagPlan> AddedGalaxyTags,
|
|
IReadOnlyList<GalaxyTagPlan> RemovedGalaxyTags,
|
|
IReadOnlyList<Phase7Plan.GalaxyTagDelta> ChangedGalaxyTags)
|
|
{
|
|
public bool IsEmpty =>
|
|
AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
|
|
AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
|
|
AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
|
|
AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0;
|
|
|
|
public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
|
|
public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
|
|
public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
|
|
public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
|
|
}
|
|
|
|
public static class Phase7Planner
|
|
{
|
|
/// <summary>
|
|
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
|
|
/// Identity is the entity's stable id (EquipmentId, DriverInstanceId, ScriptedAlarmId).
|
|
/// Element equality on the projection records doubles as the "did this change" check,
|
|
/// so any field difference moves an item from "stable" to ChangedX.
|
|
/// </summary>
|
|
public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(previous);
|
|
ArgumentNullException.ThrowIfNull(next);
|
|
|
|
var (addedEq, removedEq, changedEq) = DiffById(
|
|
previous.EquipmentNodes, next.EquipmentNodes,
|
|
n => n.EquipmentId,
|
|
(a, b) => new Phase7Plan.EquipmentDelta(a, b));
|
|
|
|
var (addedDrv, removedDrv, changedDrv) = DiffById(
|
|
previous.DriverInstancePlans, next.DriverInstancePlans,
|
|
d => d.DriverInstanceId,
|
|
(a, b) => new Phase7Plan.DriverDelta(a, b));
|
|
|
|
var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
|
|
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
|
|
a => a.ScriptedAlarmId,
|
|
(a, b) => new Phase7Plan.AlarmDelta(a, b));
|
|
|
|
var (addedGalaxy, removedGalaxy, changedGalaxy) = DiffById(
|
|
previous.GalaxyTags, next.GalaxyTags,
|
|
t => t.TagId,
|
|
(a, b) => new Phase7Plan.GalaxyTagDelta(a, b));
|
|
|
|
return new Phase7Plan(
|
|
addedEq, removedEq, changedEq,
|
|
addedDrv, removedDrv, changedDrv,
|
|
addedAlarm, removedAlarm, changedAlarm,
|
|
addedGalaxy, removedGalaxy, changedGalaxy);
|
|
}
|
|
|
|
private static (IReadOnlyList<T> Added, IReadOnlyList<T> Removed, IReadOnlyList<TDelta> Changed)
|
|
DiffById<T, TDelta>(
|
|
IReadOnlyList<T> previous,
|
|
IReadOnlyList<T> next,
|
|
Func<T, string> identity,
|
|
Func<T, T, TDelta> deltaFactory) where T : class
|
|
{
|
|
var prevById = previous.ToDictionary(identity, StringComparer.Ordinal);
|
|
var nextById = next.ToDictionary(identity, StringComparer.Ordinal);
|
|
|
|
var added = new List<T>();
|
|
var removed = new List<T>();
|
|
var changed = new List<TDelta>();
|
|
|
|
foreach (var (id, p) in prevById)
|
|
{
|
|
if (!nextById.TryGetValue(id, out var n)) { removed.Add(p); continue; }
|
|
if (!EqualityComparer<T>.Default.Equals(p, n)) changed.Add(deltaFactory(p, n));
|
|
}
|
|
foreach (var (id, n) in nextById)
|
|
{
|
|
if (!prevById.ContainsKey(id)) added.Add(n);
|
|
}
|
|
|
|
added.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
|
|
removed.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
|
|
return (added, removed, changed);
|
|
}
|
|
}
|