refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
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
v2-ci / build (push) Failing after 37s
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
The OPC UA address-space build pipeline was named after a v2-roadmap milestone number rather than its domain. Rename the family to describe what it does (build/diff/apply the OPC UA address space): Phase7Composer -> AddressSpaceComposer Phase7CompositionResult -> AddressSpaceComposition Phase7Planner -> AddressSpacePlanner Phase7Plan -> AddressSpacePlan Phase7Applier -> AddressSpaceApplier Phase7ApplyOutcome -> AddressSpaceApplyOutcome The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests -> ScriptingEntitiesTests (it tests the scripting migration, not the pipeline). Log-message prefixes move to the new class names. Pure mechanical rename, no behavioral change. EF migration classes/IDs (AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are immutable and left untouched, as are historical design docs. Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities 12/12 green.
This commit is contained in:
@@ -73,7 +73,7 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
||||
/// <summary>Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the
|
||||
/// surgical capability. Returns false otherwise — before the real <c>SdkAddressSpaceSink</c> is
|
||||
/// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller
|
||||
/// (Phase7Applier) falls back to a full rebuild. Without this forward the surgical optimization is
|
||||
/// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is
|
||||
/// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink.</summary>
|
||||
/// <param name="variableNodeId">The node ID of the variable to update in place.</param>
|
||||
/// <param name="writable">Whether the node should be read/write.</param>
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is
|
||||
/// FOLDER-SCOPED (<c>{parent}/{Name}</c>), NOT the driver-side FullName — a driver wire ref is not
|
||||
/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the
|
||||
/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all
|
||||
/// materialiser (AddressSpaceApplier), the VirtualTag publish map, and the driver live-value router so all
|
||||
/// three agree on the exact NodeId a variable was placed at.
|
||||
/// </summary>
|
||||
public static class EquipmentNodeIds
|
||||
|
||||
@@ -31,7 +31,7 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// Materialise a real OPC UA Part 9 alarm-condition node under its equipment folder so clients
|
||||
/// can browse it as a proper condition (with basic Active/Ack state). The node id equals the
|
||||
/// alarm node id (the ScriptedAlarmId) so subsequent <see cref="WriteAlarmCondition"/> calls update
|
||||
/// it. Used by <c>Phase7Applier.MaterialiseScriptedAlarms</c>. Idempotent.
|
||||
/// it. Used by <c>AddressSpaceApplier.MaterialiseScriptedAlarms</c>. Idempotent.
|
||||
/// </summary>
|
||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId); becomes the condition's NodeId.</param>
|
||||
/// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
|
||||
@@ -44,7 +44,7 @@ public interface IOpcUaAddressSpaceSink
|
||||
void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false);
|
||||
|
||||
/// <summary>
|
||||
/// Ensure a folder node exists under the given parent. Used by <c>Phase7Applier</c> to
|
||||
/// Ensure a folder node exists under the given parent. Used by <c>AddressSpaceApplier</c> to
|
||||
/// materialise the UNS Area/Line/Equipment hierarchy in the address space. When
|
||||
/// <paramref name="parentNodeId"/> is null the folder is parented under the namespace
|
||||
/// root. Idempotent: calling twice with the same id is safe.
|
||||
@@ -58,7 +58,7 @@ public interface IOpcUaAddressSpaceSink
|
||||
/// Ensure a Variable node exists at <paramref name="variableNodeId"/>, parented under
|
||||
/// <paramref name="parentFolderNodeId"/> (or the namespace root when null). Created with
|
||||
/// Bad quality + null value; subsequent <see cref="WriteValue"/> calls update both.
|
||||
/// Used by <c>Phase7Applier</c> to materialise equipment-namespace tags ahead of any
|
||||
/// Used by <c>AddressSpaceApplier</c> to materialise equipment-namespace tags ahead of any
|
||||
/// driver-side subscribe so OPC UA clients can browse them. Idempotent.
|
||||
/// </summary>
|
||||
/// <param name="variableNodeId">The OPC UA node ID for the variable.</param>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
|
||||
/// <summary>Optional capability on an address-space sink: surgical in-place attribute updates on an
|
||||
/// EXISTING variable node, used by Phase7Applier to avoid a full RebuildAddressSpace for pure-property
|
||||
/// EXISTING variable node, used by AddressSpaceApplier to avoid a full RebuildAddressSpace for pure-property
|
||||
/// tag changes (Writable / Historizing). A sink that does not implement it ⇒ caller falls back to a
|
||||
/// full rebuild (safe default).</summary>
|
||||
public interface ISurgicalAddressSpaceSink
|
||||
|
||||
@@ -88,7 +88,7 @@ public static class EquipmentScriptPaths
|
||||
/// <summary>
|
||||
/// Distinct <c>ctx.GetTag("ref")</c> string literals in first-seen order — the
|
||||
/// dependency refs the <c>VirtualTagActor</c> subscribes to. The single shared copy
|
||||
/// formerly duplicated in <c>Phase7Composer</c> + <c>DeploymentArtifact</c>. GetTag
|
||||
/// formerly duplicated in <c>AddressSpaceComposer</c> + <c>DeploymentArtifact</c>. GetTag
|
||||
/// only (writes are not dependencies).
|
||||
/// </summary>
|
||||
/// <param name="scriptSource">The (already substituted) script source.</param>
|
||||
@@ -112,7 +112,7 @@ public static class EquipmentScriptPaths
|
||||
/// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen
|
||||
/// order, appended after the predicate reads, trimmed + non-empty). The reserved
|
||||
/// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live
|
||||
/// composer (<c>Phase7Composer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
|
||||
/// composer (<c>AddressSpaceComposer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
|
||||
/// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c>
|
||||
/// equality depends on. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual
|
||||
/// tags do) — pass the predicate source as-is.
|
||||
|
||||
@@ -54,7 +54,7 @@ public static class DraftValidator
|
||||
}
|
||||
|
||||
// Minimal reader for the top-level "FullName" string in a tag's schemaless TagConfig JSON
|
||||
// (mirrors Phase7Composer.ExtractTagFullName — a small local copy, consistent with this codebase
|
||||
// (mirrors AddressSpaceComposer.ExtractTagFullName — a small local copy, consistent with this codebase
|
||||
// where the composer keeps its own).
|
||||
private static string? ExtractTagConfigFullName(string? tagConfig)
|
||||
{
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
@* Driver-agnostic server-side HistoryRead intent. Distinct from the native-alarm
|
||||
"Historize to AVEVA" toggle below: THIS gates TAG-VALUE history (root keys
|
||||
`isHistorized` / `historianTagname`, read by Phase7Composer.ExtractTagHistorize),
|
||||
`isHistorized` / `historianTagname`, read by AddressSpaceComposer.ExtractTagHistorize),
|
||||
merged onto the canonical TagConfig via the pure TagHistorizeConfig seam so it
|
||||
composes with the typed editor's driver-specific fields (both preserve unknown keys).
|
||||
Shown for EVERY driver once one is picked. *@
|
||||
|
||||
@@ -41,15 +41,15 @@ public sealed record ScriptTagInfo(string Path, string Kind, string DataType, st
|
||||
/// <para>
|
||||
/// <b>Fidelity over breadth.</b> Verified: the live runtime resolves a <c>ctx.GetTag("X")</c>
|
||||
/// literal against the driver <c>FullName</c> — the resolution chain is
|
||||
/// <c>Phase7Composer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
|
||||
/// <c>AddressSpaceComposer</c> (via <c>EquipmentScriptPaths.ExtractDependencyRefs</c>) harvesting the <c>ctx.GetTag("…")</c> literals
|
||||
/// into <c>EquipmentVirtualTagPlan.DependencyRefs</c>
|
||||
/// (<c>src/Server/…OpcUaServer/Phase7Composer.cs</c>); those become
|
||||
/// (<c>src/Server/…OpcUaServer/AddressSpaceComposer.cs</c>); those become
|
||||
/// <c>VirtualTagActor._dependencyRefs</c>, registered with the
|
||||
/// <c>DependencyMuxActor</c>, whose <c>_byRef</c> map is keyed by
|
||||
/// <c>DriverInstanceActor.AttributeValuePublished.FullReference</c>
|
||||
/// (<c>src/Server/…Runtime/VirtualTags/DependencyMuxActor.cs:97</c>) — and that
|
||||
/// <c>FullReference</c> is the <c>FullName</c> field extracted from <c>Tag.TagConfig</c>
|
||||
/// (see <c>Phase7Composer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
|
||||
/// (see <c>AddressSpaceComposer.ExtractTagFullName</c> + <c>EquipmentNodeWalker.ExtractFullName</c>).
|
||||
/// The UNS-path engine (<c>Core.VirtualTags.VirtualTagEngine</c>, keyed by a slash-joined
|
||||
/// <c>Enterprise/Site/Area/Line/Equipment/TagName</c>) is dormant — it is NOT wired into the
|
||||
/// host — so UNS browse paths never resolve at runtime and are intentionally NOT suggested.
|
||||
@@ -175,7 +175,7 @@ public sealed class ScriptTagCatalog(IDbContextFactory<OtOpcUaConfigDbContext> d
|
||||
/// <summary>
|
||||
/// Extracts the driver-side full reference from a <c>Tag.TagConfig</c> JSON blob — the
|
||||
/// top-level <c>FullName</c> string every shipped driver stores. Mirrors
|
||||
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>Phase7Composer.ExtractTagFullName</c>
|
||||
/// <c>EquipmentNodeWalker.ExtractFullName</c> / <c>AddressSpaceComposer.ExtractTagFullName</c>
|
||||
/// (AdminUI does not reference those assemblies). Falls back to the raw blob when it is not
|
||||
/// a JSON object carrying a string <c>FullName</c>.
|
||||
/// </summary>
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class ScriptAnalysisService
|
||||
if (inv.Expression is not MemberAccessExpressionSyntax ma) return false;
|
||||
// Receiver guard: only ctx.GetTag(...) / ctx.SetVirtualTag(...) are real tag-path calls. Mirrors the
|
||||
// runtime harvest (EquipmentScriptPaths.GetTagRefRegex is syntactically `ctx`-anchored), so the editor
|
||||
// offers tag completions/hover for exactly what Phase7Composer harvests — not an unrelated x.GetTag(...).
|
||||
// offers tag completions/hover for exactly what AddressSpaceComposer harvests — not an unrelated x.GetTag(...).
|
||||
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false;
|
||||
var method = ma.Name.Identifier.ValueText;
|
||||
if (method is not ("GetTag" or "SetVirtualTag")) return false;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
/// the driver reads against). Preserves unrecognised JSON keys across a load→save.</summary>
|
||||
/// <remarks>
|
||||
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
|
||||
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
|
||||
/// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
|
||||
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
|
||||
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
|
||||
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
/// Typed working model for the optional native-alarm <c>alarm</c> sub-object inside a tag's
|
||||
/// <c>TagConfig</c> JSON. A tag whose <c>TagConfig</c> carries an <c>alarm</c> object is materialised
|
||||
/// as an OPC UA Part 9 condition (rather than a value variable); the fields here mirror what the
|
||||
/// server's <c>Phase7Composer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
|
||||
/// server's <c>AddressSpaceComposer.ExtractTagAlarm</c> / <c>DeploymentArtifact.ExtractTagAlarm</c> parse.
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="HistorizeToAveva"/> is the per-condition opt-out of the DURABLE AVEVA historian write
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
/// Preserves unrecognised JSON keys across a load→save.</summary>
|
||||
/// <remarks>
|
||||
/// The <c>FullName</c> key is intentionally PascalCase: the deploy-time composer + node walker
|
||||
/// (<c>Phase7Composer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
|
||||
/// (<c>AddressSpaceComposer.ExtractTagFullName</c>, <c>EquipmentNodeWalker</c>) read it via a
|
||||
/// case-sensitive <c>TryGetProperty("FullName", …)</c>, so the editor MUST persist that exact
|
||||
/// casing. The driver-agnostic server-side HistoryRead intent keys (<c>isHistorized</c> /
|
||||
/// <c>historianTagname</c>) are NOT modelled here — they are owned by the TagModal-merge seam
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
|
||||
/// Pure, driver-agnostic merge helper for the two server-side HistoryRead intent keys at the ROOT of a
|
||||
/// tag's <c>TagConfig</c> JSON: <c>isHistorized</c> (camelCase bool — omit/false default) and
|
||||
/// <c>historianTagname</c> (camelCase string override — omit when blank). These map to what the server's
|
||||
/// <c>Phase7Composer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>).
|
||||
/// <c>AddressSpaceComposer.ExtractTagHistorize</c> reads (see <c>docs/Historian.md</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// This is the TagModal-merge seam: the TagModal owns the canonical TagConfig JSON; the driver's typed
|
||||
|
||||
+30
-30
@@ -4,9 +4,9 @@ using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
/// <summary>
|
||||
/// Side-effecting orchestrator over <see cref="Phase7Plan"/>. Drives an
|
||||
/// Side-effecting orchestrator over <see cref="AddressSpacePlan"/>. Drives an
|
||||
/// <see cref="IOpcUaAddressSpaceSink"/> to materialise the diff between two
|
||||
/// <see cref="Phase7CompositionResult"/> snapshots:
|
||||
/// <see cref="AddressSpaceComposition"/> snapshots:
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>RemovedEquipment / RemovedAlarms — write Bad-quality on every removed
|
||||
@@ -23,15 +23,15 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
/// production binds a real SDK sink, dev/Mac binds <see cref="NullOpcUaAddressSpaceSink"/>,
|
||||
/// and tests can capture every call.
|
||||
/// </summary>
|
||||
public sealed class Phase7Applier
|
||||
public sealed class AddressSpaceApplier
|
||||
{
|
||||
private readonly IOpcUaAddressSpaceSink _sink;
|
||||
private readonly ILogger<Phase7Applier> _logger;
|
||||
private readonly ILogger<AddressSpaceApplier> _logger;
|
||||
|
||||
/// <summary>Initializes a new instance of the Phase7Applier class.</summary>
|
||||
/// <summary>Initializes a new instance of the AddressSpaceApplier class.</summary>
|
||||
/// <param name="sink">The OPC UA address space sink to apply changes to.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public Phase7Applier(IOpcUaAddressSpaceSink sink, ILogger<Phase7Applier> logger)
|
||||
public AddressSpaceApplier(IOpcUaAddressSpaceSink sink, ILogger<AddressSpaceApplier> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sink);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
@@ -44,15 +44,15 @@ public sealed class Phase7Applier
|
||||
/// callers (OpcUaPublishActor) can correlate the work back to the originating deployment.
|
||||
/// </summary>
|
||||
/// <param name="plan">The plan to apply.</param>
|
||||
/// <returns>A Phase7ApplyOutcome summarizing the applied changes.</returns>
|
||||
public Phase7ApplyOutcome Apply(Phase7Plan plan)
|
||||
/// <returns>A AddressSpaceApplyOutcome summarizing the applied changes.</returns>
|
||||
public AddressSpaceApplyOutcome Apply(AddressSpacePlan plan)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plan);
|
||||
|
||||
if (plan.IsEmpty)
|
||||
{
|
||||
_logger.LogDebug("Phase7Applier: plan is empty; skipping sink writes");
|
||||
return new Phase7ApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
|
||||
_logger.LogDebug("AddressSpaceApplier: plan is empty; skipping sink writes");
|
||||
return new AddressSpaceApplyOutcome(RemovedNodes: 0, AddedNodes: 0, ChangedNodes: 0, RebuildCalled: false);
|
||||
}
|
||||
|
||||
var ts = DateTime.UtcNow;
|
||||
@@ -68,7 +68,7 @@ public sealed class Phase7Applier
|
||||
removedCount++;
|
||||
}
|
||||
// Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write
|
||||
// before tear-down), but they ARE real removals — count them so Phase7ApplyOutcome.RemovedNodes
|
||||
// before tear-down), but they ARE real removals — count them so AddressSpaceApplyOutcome.RemovedNodes
|
||||
// is accurate on a removed-tag-only deploy, which now reaches the rebuild path below.
|
||||
removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count;
|
||||
|
||||
@@ -136,7 +136,7 @@ public sealed class Phase7Applier
|
||||
try { ok = surgical.UpdateTagAttributes(nodeId, writable, historian); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: surgical UpdateTagAttributes threw for {Node}", nodeId);
|
||||
_logger.LogError(ex, "AddressSpaceApplier: surgical UpdateTagAttributes threw for {Node}", nodeId);
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) { allApplied = false; break; }
|
||||
@@ -152,10 +152,10 @@ public sealed class Phase7Applier
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
|
||||
"AddressSpaceApplier: applied plan (added={Added}, removed={Removed}, changed={Changed}, surgicalTags={Surgical}, rebuild={Rebuild})",
|
||||
addedCount, removedCount, changedCount, rebuilt ? 0 : surgicalTagDeltas.Count, rebuilt);
|
||||
|
||||
return new Phase7ApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
|
||||
return new AddressSpaceApplyOutcome(removedCount, addedCount, changedCount, rebuilt);
|
||||
}
|
||||
|
||||
private void SafeRebuild()
|
||||
@@ -166,7 +166,7 @@ public sealed class Phase7Applier
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Phase7Applier: sink.RebuildAddressSpace threw");
|
||||
_logger.LogError(ex, "AddressSpaceApplier: sink.RebuildAddressSpace threw");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ public sealed class Phase7Applier
|
||||
/// present, so re-applies are cheap.
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the hierarchy to materialise.</param>
|
||||
public void MaterialiseHierarchy(Phase7CompositionResult composition)
|
||||
public void MaterialiseHierarchy(AddressSpaceComposition composition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(composition);
|
||||
|
||||
@@ -198,7 +198,7 @@ public sealed class Phase7Applier
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})",
|
||||
"AddressSpaceApplier: hierarchy materialised (areas={Areas}, lines={Lines}, equipment={Equipment})",
|
||||
composition.UnsAreas.Count, composition.UnsLines.Count, composition.EquipmentNodes.Count);
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ public sealed class Phase7Applier
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the equipment tags to materialise.</param>
|
||||
public void MaterialiseEquipmentTags(Phase7CompositionResult composition)
|
||||
public void MaterialiseEquipmentTags(AddressSpaceComposition composition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(composition);
|
||||
if (composition.EquipmentTags.Count == 0) return;
|
||||
@@ -276,7 +276,7 @@ public sealed class Phase7Applier
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
|
||||
"AddressSpaceApplier: equipment tags materialised (tags={Tags}, equipment={Equipment})",
|
||||
composition.EquipmentTags.Count,
|
||||
composition.EquipmentTags.Select(t => t.EquipmentId).Distinct(StringComparer.Ordinal).Count());
|
||||
}
|
||||
@@ -294,7 +294,7 @@ public sealed class Phase7Applier
|
||||
/// Idempotent (per-variable idempotency relies on the sink's own <c>EnsureVariable</c>).
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the equipment VirtualTags to materialise.</param>
|
||||
public void MaterialiseEquipmentVirtualTags(Phase7CompositionResult composition)
|
||||
public void MaterialiseEquipmentVirtualTags(AddressSpaceComposition composition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(composition);
|
||||
if (composition.EquipmentVirtualTags.Count == 0) return;
|
||||
@@ -328,7 +328,7 @@ public sealed class Phase7Applier
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
|
||||
"AddressSpaceApplier: equipment virtualtags materialised (vtags={Vtags}, equipment={Equipment})",
|
||||
composition.EquipmentVirtualTags.Count,
|
||||
composition.EquipmentVirtualTags.Select(v => v.EquipmentId).Distinct(StringComparer.Ordinal).Count());
|
||||
}
|
||||
@@ -343,7 +343,7 @@ public sealed class Phase7Applier
|
||||
/// <c>MaterialiseAlarmCondition</c> re-creates cleanly on re-apply).
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition result containing the scripted alarms to materialise.</param>
|
||||
public void MaterialiseScriptedAlarms(Phase7CompositionResult composition)
|
||||
public void MaterialiseScriptedAlarms(AddressSpaceComposition composition)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(composition);
|
||||
if (composition.EquipmentScriptedAlarms.Count == 0) return;
|
||||
@@ -357,7 +357,7 @@ public sealed class Phase7Applier
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Phase7Applier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
|
||||
"AddressSpaceApplier: scripted alarms materialised (alarms={Alarms}, equipment={Equipment})",
|
||||
materialised,
|
||||
composition.EquipmentScriptedAlarms.Where(a => a.Enabled)
|
||||
.Select(a => a.EquipmentId).Distinct(StringComparer.Ordinal).Count());
|
||||
@@ -366,13 +366,13 @@ public sealed class Phase7Applier
|
||||
private void SafeEnsureFolder(string nodeId, string? parentNodeId, string displayName)
|
||||
{
|
||||
try { _sink.EnsureFolder(nodeId, parentNodeId, displayName); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureFolder threw for {Node}", nodeId); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureFolder threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
private void SafeEnsureVariable(string nodeId, string? parentNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||
{
|
||||
try { _sink.EnsureVariable(nodeId, parentNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: EnsureVariable threw for {Node}", nodeId); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: EnsureVariable threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
// A VirtualTag's materialised OPC UA node (MaterialiseEquipmentVirtualTags) is derived ONLY from
|
||||
@@ -381,7 +381,7 @@ public sealed class Phase7Applier
|
||||
// so a delta changing ONLY those three leaves a byte-identical node and needs no address-space rebuild.
|
||||
// Whitelist-of-may-differ via `with` + the record's custom Equals: any OTHER field difference (current
|
||||
// or future) makes the override unequal → falls back to a full rebuild (safe default).
|
||||
private static bool VtagDeltaIsNodeIrrelevant(Phase7Plan.EquipmentVirtualTagDelta d) =>
|
||||
private static bool VtagDeltaIsNodeIrrelevant(AddressSpacePlan.EquipmentVirtualTagDelta d) =>
|
||||
(d.Previous with
|
||||
{
|
||||
Expression = d.Current.Expression,
|
||||
@@ -394,7 +394,7 @@ public sealed class Phase7Applier
|
||||
// ISurgicalAddressSpaceSink.UpdateTagAttributes, avoiding a full rebuild (preserving subscriptions).
|
||||
// DataType / IsArray / ArrayLength / FullName / DriverInstanceId / identity / alarm differences fall
|
||||
// through to a rebuild — the override-unequal default also covers any future field.
|
||||
private static bool TagDeltaIsSurgicalEligible(Phase7Plan.EquipmentTagDelta d) =>
|
||||
private static bool TagDeltaIsSurgicalEligible(AddressSpacePlan.EquipmentTagDelta d) =>
|
||||
d.Previous.Alarm is null && d.Current.Alarm is null &&
|
||||
(d.Previous with
|
||||
{
|
||||
@@ -418,18 +418,18 @@ public sealed class Phase7Applier
|
||||
private void SafeWriteAlarmCondition(string nodeId, AlarmConditionSnapshot state, DateTime ts)
|
||||
{
|
||||
try { _sink.WriteAlarmCondition(nodeId, state, ts); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: WriteAlarmCondition threw for {Node}", nodeId); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: WriteAlarmCondition threw for {Node}", nodeId); }
|
||||
}
|
||||
|
||||
private void SafeMaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative)
|
||||
{
|
||||
try { _sink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "Phase7Applier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
|
||||
catch (Exception ex) { _logger.LogWarning(ex, "AddressSpaceApplier: MaterialiseAlarmCondition threw for {Node}", alarmNodeId); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Summary of one apply pass. Useful for tests + audit-log entries on the deploy path.</summary>
|
||||
public sealed record Phase7ApplyOutcome(
|
||||
public sealed record AddressSpaceApplyOutcome(
|
||||
int RemovedNodes,
|
||||
int AddedNodes,
|
||||
int ChangedNodes,
|
||||
+13
-13
@@ -6,11 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
/// <summary>Outcome of <see cref="Phase7Composer.Compose"/> — pure value tuple, no side effects.
|
||||
/// <summary>Outcome of <see cref="AddressSpaceComposer.Compose"/> — pure value tuple, no side effects.
|
||||
/// <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
|
||||
/// its parent line id so the applier knows where to hang each equipment folder.</summary>
|
||||
public sealed record Phase7CompositionResult(
|
||||
public sealed record AddressSpaceComposition(
|
||||
IReadOnlyList<UnsAreaProjection> UnsAreas,
|
||||
IReadOnlyList<UnsLineProjection> UnsLines,
|
||||
IReadOnlyList<EquipmentNode> EquipmentNodes,
|
||||
@@ -21,7 +21,7 @@ public sealed record Phase7CompositionResult(
|
||||
/// <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(
|
||||
public AddressSpaceComposition(
|
||||
IReadOnlyList<EquipmentNode> equipmentNodes,
|
||||
IReadOnlyList<DriverInstancePlan> driverInstancePlans,
|
||||
IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
|
||||
@@ -32,7 +32,7 @@ public sealed record Phase7CompositionResult(
|
||||
|
||||
/// <summary>
|
||||
/// Equipment-namespace tags — a <see cref="Tag"/> with non-null <see cref="Tag.EquipmentId"/>
|
||||
/// in an <c>Equipment</c>-kind namespace. <c>Phase7Applier.MaterialiseEquipmentTags</c>
|
||||
/// in an <c>Equipment</c>-kind namespace. <c>AddressSpaceApplier.MaterialiseEquipmentTags</c>
|
||||
/// materialises each as a Variable under its existing equipment folder. Declared as an
|
||||
/// init-only member defaulting to empty (rather than a positional parameter) so every existing
|
||||
/// convenience constructor + call site keeps compiling unchanged; new producers set it via
|
||||
@@ -66,7 +66,7 @@ public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentI
|
||||
/// 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
|
||||
/// <see cref="TagId"/> (diff identity), the parent <see cref="EquipmentId"/> folder (already
|
||||
/// materialised by <c>Phase7Applier.MaterialiseHierarchy</c>) the variable hangs under, the
|
||||
/// materialised by <c>AddressSpaceApplier.MaterialiseHierarchy</c>) the variable hangs under, the
|
||||
/// optional <see cref="FolderPath"/> sub-folder, the leaf <see cref="Name"/> display, the OPC UA
|
||||
/// <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
|
||||
@@ -117,7 +117,7 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity, bool?
|
||||
/// <summary>
|
||||
/// One Equipment-namespace VirtualTag from a <see cref="VirtualTag"/> row (joined to its
|
||||
/// <see cref="Script"/> for the expression). The VirtualTag value analogue of
|
||||
/// <see cref="EquipmentTagPlan"/>: <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c>
|
||||
/// <see cref="EquipmentTagPlan"/>: <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c>
|
||||
/// materialises each as a Variable under its equipment folder with a folder-scoped NodeId
|
||||
/// (<c>EquipmentId/Name</c>, or <c>EquipmentId/FolderPath/Name</c> when a sub-folder is set),
|
||||
/// and <c>VirtualTagHostActor</c> spawns a <c>VirtualTagActor</c> per plan that evaluates
|
||||
@@ -252,19 +252,19 @@ public sealed record EquipmentScriptedAlarmPlan(
|
||||
/// startup (Task 53) consumes the result and hands it to the node-manager factory.
|
||||
///
|
||||
/// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> +
|
||||
/// <see cref="UnsLineProjection"/>) so <c>Phase7Applier</c> can build the
|
||||
/// <see cref="UnsLineProjection"/>) so <c>AddressSpaceApplier</c> can build the
|
||||
/// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy
|
||||
/// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
|
||||
/// (composer → applier → sink → node manager) chain.
|
||||
/// </summary>
|
||||
public static class Phase7Composer
|
||||
public static class AddressSpaceComposer
|
||||
{
|
||||
/// <summary>Convenience overload for legacy callers + tests that don't supply UNS topology or tags.</summary>
|
||||
/// <param name="equipment">The equipment.</param>
|
||||
/// <param name="driverInstances">The driver instances.</param>
|
||||
/// <param name="scriptedAlarms">The scripted alarms.</param>
|
||||
/// <returns>The composition result.</returns>
|
||||
public static Phase7CompositionResult Compose(
|
||||
public static AddressSpaceComposition Compose(
|
||||
IReadOnlyList<Equipment> equipment,
|
||||
IReadOnlyList<DriverInstance> driverInstances,
|
||||
IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
|
||||
@@ -278,7 +278,7 @@ public static class Phase7Composer
|
||||
/// <param name="driverInstances">The driver instances.</param>
|
||||
/// <param name="scriptedAlarms">The scripted alarms.</param>
|
||||
/// <returns>The composition result.</returns>
|
||||
public static Phase7CompositionResult Compose(
|
||||
public static AddressSpaceComposition Compose(
|
||||
IReadOnlyList<UnsArea> unsAreas,
|
||||
IReadOnlyList<UnsLine> unsLines,
|
||||
IReadOnlyList<Equipment> equipment,
|
||||
@@ -300,7 +300,7 @@ public static class Phase7Composer
|
||||
/// <param name="virtualTags">The Equipment-namespace virtual (calculated) tags. <c>null</c> = none.</param>
|
||||
/// <param name="scripts">The scripts joined to <paramref name="virtualTags"/> by ScriptId for the expression. <c>null</c> = none.</param>
|
||||
/// <returns>The composition result.</returns>
|
||||
public static Phase7CompositionResult Compose(
|
||||
public static AddressSpaceComposition Compose(
|
||||
IReadOnlyList<UnsArea> unsAreas,
|
||||
IReadOnlyList<UnsLine> unsLines,
|
||||
IReadOnlyList<Equipment> equipment,
|
||||
@@ -434,7 +434,7 @@ public static class Phase7Composer
|
||||
if (!scriptsById.TryGetValue(a.PredicateScriptId, out var s))
|
||||
{
|
||||
Trace.TraceWarning(
|
||||
"Phase7Composer: scripted alarm '{0}' (equipment '{1}') references predicate " +
|
||||
"AddressSpaceComposer: scripted alarm '{0}' (equipment '{1}') references predicate " +
|
||||
"script '{2}' which is not in the supplied scripts — skipping.",
|
||||
a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId);
|
||||
continue;
|
||||
@@ -458,7 +458,7 @@ public static class Phase7Composer
|
||||
Enabled: a.Enabled));
|
||||
}
|
||||
|
||||
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms)
|
||||
return new AddressSpaceComposition(areas, lines, nodes, plans, alarms)
|
||||
{
|
||||
EquipmentTags = equipmentTags,
|
||||
EquipmentVirtualTags = equipmentVirtualTags,
|
||||
+15
-15
@@ -1,7 +1,7 @@
|
||||
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
|
||||
/// <summary>
|
||||
/// Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
|
||||
/// Pure diff between two <see cref="AddressSpaceComposition"/> 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,
|
||||
@@ -12,22 +12,22 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
/// nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
|
||||
/// drastic schema flips.
|
||||
/// </summary>
|
||||
public sealed record Phase7Plan(
|
||||
public sealed record AddressSpacePlan(
|
||||
IReadOnlyList<EquipmentNode> AddedEquipment,
|
||||
IReadOnlyList<EquipmentNode> RemovedEquipment,
|
||||
IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
|
||||
IReadOnlyList<AddressSpacePlan.EquipmentDelta> ChangedEquipment,
|
||||
IReadOnlyList<DriverInstancePlan> AddedDrivers,
|
||||
IReadOnlyList<DriverInstancePlan> RemovedDrivers,
|
||||
IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
|
||||
IReadOnlyList<AddressSpacePlan.DriverDelta> ChangedDrivers,
|
||||
IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
|
||||
IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
|
||||
IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms)
|
||||
IReadOnlyList<AddressSpacePlan.AlarmDelta> ChangedAlarms)
|
||||
{
|
||||
/// <summary>
|
||||
/// Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
|
||||
/// init-only members (defaulting empty) rather than positional parameters so existing
|
||||
/// <c>Phase7Plan</c> construction sites compile unchanged — consistent with how
|
||||
/// <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an
|
||||
/// <c>AddressSpacePlan</c> construction sites compile unchanged — consistent with how
|
||||
/// <see cref="AddressSpaceComposition.EquipmentTags"/> was added. Without these, an
|
||||
/// incremental deploy that changes ONLY equipment tags produced an empty plan and
|
||||
/// <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
|
||||
/// </summary>
|
||||
@@ -67,7 +67,7 @@ public sealed record Phase7Plan(
|
||||
public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
|
||||
}
|
||||
|
||||
public static class Phase7Planner
|
||||
public static class AddressSpacePlanner
|
||||
{
|
||||
/// <summary>
|
||||
/// Diff two compositions, emitting Added/Removed/Changed sets per entity class.
|
||||
@@ -77,7 +77,7 @@ public static class Phase7Planner
|
||||
/// </summary>
|
||||
/// <param name="previous">The previous composition result.</param>
|
||||
/// <param name="next">The new composition result.</param>
|
||||
public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next)
|
||||
public static AddressSpacePlan Compute(AddressSpaceComposition previous, AddressSpaceComposition next)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(previous);
|
||||
ArgumentNullException.ThrowIfNull(next);
|
||||
@@ -85,22 +85,22 @@ public static class Phase7Planner
|
||||
var (addedEq, removedEq, changedEq) = DiffById(
|
||||
previous.EquipmentNodes, next.EquipmentNodes,
|
||||
n => n.EquipmentId,
|
||||
(a, b) => new Phase7Plan.EquipmentDelta(a, b));
|
||||
(a, b) => new AddressSpacePlan.EquipmentDelta(a, b));
|
||||
|
||||
var (addedDrv, removedDrv, changedDrv) = DiffById(
|
||||
previous.DriverInstancePlans, next.DriverInstancePlans,
|
||||
d => d.DriverInstanceId,
|
||||
(a, b) => new Phase7Plan.DriverDelta(a, b));
|
||||
(a, b) => new AddressSpacePlan.DriverDelta(a, b));
|
||||
|
||||
var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
|
||||
previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
|
||||
a => a.ScriptedAlarmId,
|
||||
(a, b) => new Phase7Plan.AlarmDelta(a, b));
|
||||
(a, b) => new AddressSpacePlan.AlarmDelta(a, b));
|
||||
|
||||
var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
|
||||
previous.EquipmentTags, next.EquipmentTags,
|
||||
t => t.TagId,
|
||||
(a, b) => new Phase7Plan.EquipmentTagDelta(a, b));
|
||||
(a, b) => new AddressSpacePlan.EquipmentTagDelta(a, b));
|
||||
|
||||
// VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan
|
||||
// overrides record equality to compare ALL fields by value — scalars (Expression/DataType/
|
||||
@@ -110,9 +110,9 @@ public static class Phase7Planner
|
||||
var (addedVTags, removedVTags, changedVTags) = DiffById(
|
||||
previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
|
||||
t => t.VirtualTagId,
|
||||
(a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b));
|
||||
(a, b) => new AddressSpacePlan.EquipmentVirtualTagDelta(a, b));
|
||||
|
||||
return new Phase7Plan(
|
||||
return new AddressSpacePlan(
|
||||
addedEq, removedEq, changedEq,
|
||||
addedDrv, removedDrv, changedDrv,
|
||||
addedAlarm, removedAlarm, changedAlarm)
|
||||
@@ -25,7 +25,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
///
|
||||
/// Node-id encoding uses the manager's default namespace + the caller-supplied string id
|
||||
/// as the identifier portion (e.g. <c>"ns=2;s=eq-1/temp"</c>). Equipment-folder hierarchy
|
||||
/// and OPC UA type metadata still come from the Phase7Applier / EquipmentNodeWalker
|
||||
/// and OPC UA type metadata still come from the AddressSpaceApplier / EquipmentNodeWalker
|
||||
/// integration (F14b, tracked under #85) — this manager treats every id as a flat
|
||||
/// <see cref="BaseDataVariableState"/> under the namespace root.
|
||||
/// </summary>
|
||||
@@ -1252,7 +1252,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// <summary>
|
||||
/// Ensure a folder node exists at <paramref name="folderNodeId"/> with the given display
|
||||
/// name, parented under <paramref name="parentNodeId"/> (or the namespace root when null).
|
||||
/// #85 — used by <see cref="Phase7Applier"/> to materialise the UNS Area/Line/Equipment
|
||||
/// #85 — used by <see cref="AddressSpaceApplier"/> to materialise the UNS Area/Line/Equipment
|
||||
/// folder hierarchy. Idempotent: the second call with the same id returns the cached
|
||||
/// folder so adding child variables under it still works.
|
||||
/// </summary>
|
||||
@@ -1424,7 +1424,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
_ => DataTypeIds.BaseDataType,
|
||||
};
|
||||
|
||||
/// <summary>Clear every registered variable + folder from the address space. Phase7Applier
|
||||
/// <summary>Clear every registered variable + folder from the address space. AddressSpaceApplier
|
||||
/// calls this when Equipment/Alarm topology changes; the populator then re-adds via
|
||||
/// EnsureFolder + WriteValue on the next pass.</summary>
|
||||
public void RebuildAddressSpace()
|
||||
|
||||
@@ -169,8 +169,8 @@ public static class DeploymentArtifact
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse the artifact into the projected <see cref="Phase7CompositionResult"/> used by
|
||||
/// <c>Phase7Planner</c> + <c>Phase7Applier</c>. Returns an empty composition for empty/
|
||||
/// Parse the artifact into the projected <see cref="AddressSpaceComposition"/> used by
|
||||
/// <c>AddressSpacePlanner</c> + <c>AddressSpaceApplier</c>. Returns an empty composition for empty/
|
||||
/// malformed blobs so callers can treat parse failure as a no-op deploy.
|
||||
///
|
||||
/// The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the
|
||||
@@ -179,7 +179,7 @@ public static class DeploymentArtifact
|
||||
/// nodes.
|
||||
/// </summary>
|
||||
/// <param name="blob">The deployment artifact blob to parse.</param>
|
||||
public static Phase7CompositionResult ParseComposition(ReadOnlySpan<byte> blob)
|
||||
public static AddressSpaceComposition ParseComposition(ReadOnlySpan<byte> blob)
|
||||
{
|
||||
if (blob.IsEmpty) return Empty();
|
||||
|
||||
@@ -197,7 +197,7 @@ public static class DeploymentArtifact
|
||||
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
|
||||
var equipmentScriptedAlarms = BuildEquipmentScriptedAlarmPlans(root);
|
||||
|
||||
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms)
|
||||
return new AddressSpaceComposition(areas, lines, equipment, drivers, alarms)
|
||||
{
|
||||
EquipmentTags = equipmentTags,
|
||||
EquipmentVirtualTags = equipmentVirtualTags,
|
||||
@@ -224,7 +224,7 @@ public static class DeploymentArtifact
|
||||
/// <param name="nodeId">This node's identity in "host:port" form.</param>
|
||||
/// <param name="onInconsistency">Optional diagnostic callback for cross-cluster orphan bindings; null disables the check.</param>
|
||||
/// <returns>The filtered composition per the node's scoping decision.</returns>
|
||||
public static Phase7CompositionResult ParseComposition(
|
||||
public static AddressSpaceComposition ParseComposition(
|
||||
ReadOnlySpan<byte> blob, string nodeId, Action<string>? onInconsistency = null)
|
||||
{
|
||||
var scope = ResolveClusterScope(blob, nodeId);
|
||||
@@ -253,7 +253,7 @@ public static class DeploymentArtifact
|
||||
}
|
||||
}
|
||||
|
||||
return new Phase7CompositionResult(
|
||||
return new AddressSpaceComposition(
|
||||
full.UnsAreas.Where(a => sets.AreaIds.Contains(a.UnsAreaId)).ToArray(),
|
||||
keptLines,
|
||||
keptEquipment,
|
||||
@@ -348,7 +348,7 @@ public static class DeploymentArtifact
|
||||
}
|
||||
}
|
||||
|
||||
private static Phase7CompositionResult Empty() => new(
|
||||
private static AddressSpaceComposition Empty() => new(
|
||||
Array.Empty<UnsAreaProjection>(),
|
||||
Array.Empty<UnsLineProjection>(),
|
||||
Array.Empty<EquipmentNode>(),
|
||||
@@ -359,7 +359,7 @@ public static class DeploymentArtifact
|
||||
/// 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 — so the compose-side + artifact-decode
|
||||
/// <c>AddressSpaceComposer.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>
|
||||
@@ -421,7 +421,7 @@ public static class DeploymentArtifact
|
||||
// AccessLevel → Writable. ConfigComposer serialises the TagAccessLevel enum WITHOUT a
|
||||
// string converter, so it lands as a number (Read = 0, ReadWrite = 1); tolerate the string
|
||||
// form ("ReadWrite") too — same defensive both-forms parse as the Kind gate above. MUST match
|
||||
// Phase7Composer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
|
||||
// AddressSpaceComposer's `AccessLevel == TagAccessLevel.ReadWrite` exactly (byte-parity). A missing
|
||||
// field defaults to non-writable (read-only).
|
||||
var writable = el.TryGetProperty("AccessLevel", out var alEl) && alEl.ValueKind switch
|
||||
{
|
||||
@@ -469,7 +469,7 @@ public static class DeploymentArtifact
|
||||
/// <summary>
|
||||
/// Join the artifact's VirtualTags array to its Scripts array (by ScriptId) to emit one
|
||||
/// <see cref="EquipmentVirtualTagPlan"/> per VirtualTag. The artifact-decode mirror of
|
||||
/// <c>Phase7Composer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
|
||||
/// <c>AddressSpaceComposer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
|
||||
/// plans agree. The reserved <c>{{equip}}</c> token in the joined Script's <c>SourceCode</c> is
|
||||
/// substituted with the owning equipment's tag base (derived from <paramref name="equipmentTags"/>'
|
||||
/// FullNames) BEFORE refs are extracted, byte-parity with the composer. <c>Expression</c> = the
|
||||
@@ -529,14 +529,14 @@ public static class DeploymentArtifact
|
||||
// Historize: the artifact carries a Pascal-case "Historize" bool (ConfigComposer serialises
|
||||
// the whole VirtualTag entity with DefaultIgnoreCondition.Never). Robust parse — default
|
||||
// false; only honoured when the JSON value is an actual boolean — so absent/non-bool ⇒ false,
|
||||
// byte-parity with Phase7Composer's entity-default-false behaviour.
|
||||
// byte-parity with AddressSpaceComposer's entity-default-false behaviour.
|
||||
var historize = el.TryGetProperty("Historize", out var hEl)
|
||||
&& (hEl.ValueKind == JsonValueKind.True || hEl.ValueKind == JsonValueKind.False)
|
||||
&& hEl.GetBoolean();
|
||||
|
||||
// Substitute the {{equip}} token with the owning equipment's tag base BEFORE extracting
|
||||
// refs, so both Expression and DependencyRefs are machine-specific — byte-parity with
|
||||
// Phase7Composer.Compose.
|
||||
// AddressSpaceComposer.Compose.
|
||||
var expanded = EquipmentScriptPaths.SubstituteEquipmentToken(
|
||||
source, baseByEquip.GetValueOrDefault(equipmentId!));
|
||||
|
||||
@@ -562,7 +562,7 @@ public static class DeploymentArtifact
|
||||
/// <summary>
|
||||
/// Join the artifact's ScriptedAlarms array to its Scripts array (by PredicateScriptId) to emit
|
||||
/// one <see cref="EquipmentScriptedAlarmPlan"/> per alarm. The artifact-decode mirror of
|
||||
/// <c>Phase7Composer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode
|
||||
/// <c>AddressSpaceComposer.Compose</c>'s scripted-alarm producer — so the compose-side + artifact-decode
|
||||
/// plans agree byte-for-byte. An alarm whose <c>PredicateScriptId</c> has no matching Script row is
|
||||
/// SKIPPED (matching the composer's skip behaviour) to preserve parity. <c>PredicateSource</c> = the
|
||||
/// joined script source ("" when missing — but such alarms are skipped above); <c>DependencyRefs</c>
|
||||
@@ -616,7 +616,7 @@ public static class DeploymentArtifact
|
||||
|
||||
if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue;
|
||||
|
||||
// Skip alarms whose predicate script is missing — matching Phase7Composer's skip behaviour
|
||||
// Skip alarms whose predicate script is missing — matching AddressSpaceComposer's skip behaviour
|
||||
// so both sides emit the same set (byte-parity).
|
||||
if (predicateScriptId is null || !scriptSourceById.TryGetValue(predicateScriptId, out var source))
|
||||
continue;
|
||||
@@ -646,7 +646,7 @@ public static class DeploymentArtifact
|
||||
|
||||
/// <summary>
|
||||
/// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName"
|
||||
/// field). The artifact-decode mirror of <c>Phase7Composer.ExtractTagFullName</c> /
|
||||
/// field). The artifact-decode mirror of <c>AddressSpaceComposer.ExtractTagFullName</c> /
|
||||
/// <c>EquipmentNodeWalker.ExtractFullName</c> — replicated because Runtime does not reference
|
||||
/// the Core driver assembly. Falls back to the raw blob when absent or non-JSON.
|
||||
/// </summary>
|
||||
@@ -669,7 +669,7 @@ public static class DeploymentArtifact
|
||||
|
||||
/// <summary>Parses the optional <c>alarm</c> object from a tag's <c>TagConfig</c> JSON. Returns null
|
||||
/// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The
|
||||
/// live-edit side (<c>Phase7Composer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// live-edit side (<c>AddressSpaceComposer.ExtractTagAlarm</c>) MUST parse identically (byte-parity).</summary>
|
||||
private static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return null;
|
||||
@@ -683,7 +683,7 @@ public static class DeploymentArtifact
|
||||
var sev = a.TryGetProperty("severity", out var sEl) && sEl.ValueKind == JsonValueKind.Number
|
||||
&& sEl.TryGetInt32(out var sv) ? sv : 500;
|
||||
// historizeToAveva (bool?, absent ⇒ null ⇒ historize): byte-parity with
|
||||
// Phase7Composer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write.
|
||||
// AddressSpaceComposer.ExtractTagAlarm — only an explicit false suppresses the durable AVEVA write.
|
||||
bool? historize = a.TryGetProperty("historizeToAveva", out var hEl)
|
||||
&& hEl.ValueKind is JsonValueKind.True or JsonValueKind.False
|
||||
? hEl.GetBoolean()
|
||||
@@ -699,7 +699,7 @@ public static class DeploymentArtifact
|
||||
/// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName,
|
||||
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
|
||||
/// <c>ExtractTagAlarm</c>. Never throws. The live-edit composer side
|
||||
/// (<c>Phase7Composer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// (<c>AddressSpaceComposer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
|
||||
private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
@@ -727,7 +727,7 @@ public static class DeploymentArtifact
|
||||
/// <c>arrayLength</c> uint (honoured ONLY when <c>isArray</c> is true AND the prop is a JSON number
|
||||
/// that fits <c>uint</c>; else <c>null</c>). Mirrors <see cref="ExtractTagHistorize"/> in structure +
|
||||
/// null/blank/non-object/malformed-JSON tolerance. Never throws. The live-edit composer side
|
||||
/// (<c>Phase7Composer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
|
||||
/// (<c>AddressSpaceComposer.ExtractTagArray</c>) MUST parse identically (byte-parity).</summary>
|
||||
private static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null);
|
||||
@@ -764,7 +764,7 @@ public static class DeploymentArtifact
|
||||
var item = reader(el);
|
||||
if (item is not null) result.Add(item);
|
||||
}
|
||||
// Match Phase7Composer's natural-key sort so plan diffs are deterministic across
|
||||
// Match AddressSpaceComposer's natural-key sort so plan diffs are deterministic across
|
||||
// artifact-decode + composer-compose passes.
|
||||
return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
@@ -939,7 +939,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
|
||||
return;
|
||||
}
|
||||
|
||||
Phase7CompositionResult composition;
|
||||
AddressSpaceComposition composition;
|
||||
try
|
||||
{
|
||||
composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value);
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
private readonly bool _subscribeRedundancyTopic;
|
||||
private readonly NodeId? _localNode;
|
||||
private readonly IDbContextFactory<OtOpcUaConfigDbContext>? _dbFactory;
|
||||
private readonly Phase7Applier? _applier;
|
||||
private readonly AddressSpaceApplier? _applier;
|
||||
private readonly IActorRef? _dbHealthProbe;
|
||||
private readonly TimeSpan _staleWindow;
|
||||
private readonly TimeSpan _probeFreshnessWindow;
|
||||
@@ -77,7 +77,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
private DbHealthProbeActor.DbHealthStatus? _lastDbHealth;
|
||||
private RedundancyStateChanged? _lastSnapshot;
|
||||
private (bool Ok, DateTime At)? _probeAboutMe;
|
||||
private Phase7CompositionResult _lastApplied = new(
|
||||
private AddressSpaceComposition _lastApplied = new(
|
||||
Array.Empty<UnsAreaProjection>(),
|
||||
Array.Empty<UnsLineProjection>(),
|
||||
Array.Empty<EquipmentNode>(),
|
||||
@@ -116,7 +116,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
IServiceLevelPublisher? serviceLevel = null,
|
||||
NodeId? localNode = null,
|
||||
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
|
||||
Phase7Applier? applier = null,
|
||||
AddressSpaceApplier? applier = null,
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
@@ -157,7 +157,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
bool subscribeRedundancyTopic = false,
|
||||
NodeId? localNode = null,
|
||||
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
|
||||
Phase7Applier? applier = null,
|
||||
AddressSpaceApplier? applier = null,
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
@@ -197,7 +197,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
bool subscribeRedundancyTopic,
|
||||
NodeId? localNode,
|
||||
IDbContextFactory<OtOpcUaConfigDbContext>? dbFactory = null,
|
||||
Phase7Applier? applier = null,
|
||||
AddressSpaceApplier? applier = null,
|
||||
IActorRef? dbHealthProbe = null,
|
||||
TimeSpan? staleWindow = null,
|
||||
TimeSpan? probeFreshnessWindow = null,
|
||||
@@ -308,7 +308,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
? DeploymentArtifact.ParseComposition(artifact, ln.Value,
|
||||
inconsistency => _log.Warning("OpcUaPublish {Node}: cross-cluster binding — {Message}", ln, inconsistency))
|
||||
: DeploymentArtifact.ParseComposition(artifact);
|
||||
var plan = Phase7Planner.Compute(_lastApplied, composition);
|
||||
var plan = AddressSpacePlanner.Compute(_lastApplied, composition);
|
||||
|
||||
if (plan.IsEmpty)
|
||||
{
|
||||
@@ -321,7 +321,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
|
||||
_lastApplied = composition;
|
||||
|
||||
// #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA
|
||||
// clients see Area/Line/Equipment as proper folders. Idempotent; Phase7Applier
|
||||
// clients see Area/Line/Equipment as proper folders. Idempotent; AddressSpaceApplier
|
||||
// skips folders that already exist with the same node id.
|
||||
_applier.MaterialiseHierarchy(composition);
|
||||
// T14 — scripted alarms get their own pass right after the hierarchy so the equipment
|
||||
|
||||
@@ -206,9 +206,9 @@ public static class ServiceCollectionExtensions
|
||||
registry.Register<DependencyMuxActorKey>(mux);
|
||||
|
||||
// OPC UA publish actor — pinned dispatcher, owns the address-space side of the
|
||||
// pipeline. Phase7Applier is constructed here so the actor + applier share the
|
||||
// pipeline. AddressSpaceApplier is constructed here so the actor + applier share the
|
||||
// same sink reference (when DeferredAddressSpaceSink swaps later, both see it).
|
||||
var applier = new Phase7Applier(addressSpaceSink, loggerFactory.CreateLogger<Phase7Applier>());
|
||||
var applier = new AddressSpaceApplier(addressSpaceSink, loggerFactory.CreateLogger<AddressSpaceApplier>());
|
||||
var publishActor = system.ActorOf(
|
||||
OpcUaPublishActor.Props(
|
||||
sink: addressSpaceSink,
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
|
||||
///
|
||||
/// <para>
|
||||
/// The published NodeId is computed by the shared <see cref="EquipmentNodeIds.Variable"/> —
|
||||
/// the single source of truth <c>Phase7Applier.MaterialiseEquipmentVirtualTags</c> also
|
||||
/// the single source of truth <c>AddressSpaceApplier.MaterialiseEquipmentVirtualTags</c> also
|
||||
/// materialises against — so the value always lands on a NodeId that exists.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
@@ -198,7 +198,7 @@ public sealed class VirtualTagHostActor : ReceiveActor
|
||||
}
|
||||
|
||||
/// <summary>Folder-scoped NodeId for a VirtualTag plan. The formula now lives in the shared
|
||||
/// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>Phase7Applier</c> also
|
||||
/// <see cref="EquipmentNodeIds"/> (the single source of truth that <c>AddressSpaceApplier</c> also
|
||||
/// materialises against), so the published value always lands on the NodeId that was materialised.</summary>
|
||||
private static string NodeIdFor(EquipmentVirtualTagPlan p) =>
|
||||
EquipmentNodeIds.Variable(p.EquipmentId, p.FolderPath, p.Name);
|
||||
|
||||
Reference in New Issue
Block a user