diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs
index d95bafb7..d34284d7 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs
@@ -1,53 +1,60 @@
-using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
///
-/// Resolves a driver subscription/read/write fullReference to a driver tag-definition,
-/// bridging the two authoring models: a legacy authored tag-table entry (looked up by name)
-/// OR an equipment tag whose reference is its raw TagConfig JSON (parsed on first use
-/// and cached). Negative results are cached too, so a genuinely-unknown reference is parsed once.
+/// v3: resolves a driver subscription/read/write reference — which is now always a
+/// RawPath — to the driver's internal tag-definition. The lookup is a single authored-table
+/// hit: each deploy hands the driver its authored raw tags keyed by RawPath (see
+/// ), the driver builds a RawPath → TDef table from them, and this
+/// resolver wraps that table's lookup. The pre-v3 blob-parse fallback (an equipment tag whose
+/// reference WAS its raw TagConfig JSON) is retired: a miss is a miss —
+/// returns and the driver surfaces Bad quality, never a parse attempt.
///
/// The driver's internal tag-definition type.
public sealed class EquipmentTagRefResolver where TDef : class
{
- private readonly Func _byName;
- private readonly Func _parseRef;
- private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal);
+ private readonly Func _byRawPath;
/// Initializes a new instance of the class.
- /// Authored tag-table lookup (returns null on miss).
- /// Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null.
- public EquipmentTagRefResolver(Func byName, Func parseRef)
+ /// The driver's authored-table lookup (RawPath → definition; null on miss).
+ public EquipmentTagRefResolver(Func byRawPath)
{
- ArgumentNullException.ThrowIfNull(byName);
- ArgumentNullException.ThrowIfNull(parseRef);
- _byName = byName;
- _parseRef = parseRef;
+ ArgumentNullException.ThrowIfNull(byRawPath);
+ _byRawPath = byRawPath;
}
- /// True when resolves to a def (authored or equipment).
- /// The wire reference handed to the driver.
+ /// True when resolves to an authored tag-definition.
+ /// The RawPath wire reference handed to the driver.
///
/// The resolved tag-definition when this returns . When this returns
/// the value is undefined — callers must not use it.
- /// [MaybeNullWhen(false)] informs the nullable-reference-types (NRT) analyser so
- /// callers in NRT-enabled contexts do not need to suppress warnings.
///
/// when a definition was found.
- public bool TryResolve(string fullReference, [MaybeNullWhen(false)] out TDef def)
+ public bool TryResolve(string rawPath, [MaybeNullWhen(false)] out TDef def)
{
- var authored = _byName(fullReference);
- if (authored is not null) { def = authored; return true; }
-
- var resolved = _cache.GetOrAdd(fullReference, _parseRef);
- if (resolved is not null) { def = resolved; return true; }
-
- def = default;
- return false;
+ def = _byRawPath(rawPath);
+ return def is not null;
}
- /// Drops the transient-parse cache (call on driver reinitialise so a config change re-parses).
- public void Clear() => _cache.Clear();
+ ///
+ /// No-op retained for call-site compatibility. v3 holds no transient parse cache — the driver
+ /// rebuilds its authored RawPath → TDef table on reinitialise, and this resolver reads
+ /// the driver's live table by closure, so there is nothing to clear here.
+ ///
+ public void Clear()
+ {
+ }
}
+
+///
+/// The unit of tag delivery from the deploy artifact to a driver: one authored raw
+/// Tag, identified by its (the v3 identity), carrying the
+/// driver-specific address blob () the driver dials and the platform flags
+/// the driver needs. A driver maps each entry to its typed definition (keyed by RawPath) via its
+/// own TagConfig → definition factory at table-build time.
+///
+/// The tag's cluster-scoped RawPath — the driver wire reference / identity.
+/// The tag's schemaless driver-specific TagConfig JSON (the dialled address).
+/// Whether writes to this tag are safe to replay (R2 resilience contract).
+public sealed record RawTagEntry(string RawPath, string TagConfig, bool WriteIdempotent);
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs
index 96eacf2b..5360df87 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs
@@ -1,80 +1,37 @@
-using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa;
///
-/// Materializes the canonical Unified Namespace browse tree for an Equipment-kind
-/// from the Config DB's
-/// UnsArea / UnsLine / Equipment / Tag rows. Runs during
-/// address-space build per whose
-/// Namespace.Kind = Equipment; non-Equipment namespaces are
-/// exempt and reach this walker only indirectly through
-/// .
+/// Materializes the canonical Unified Namespace browse tree (Area → Line → Equipment) for a
+/// cluster from its UnsArea / UnsLine / Equipment rows, plus the
+/// per-equipment VirtualTag + ScriptedAlarm nodes.
+///
+/// v3: equipment no longer binds a driver or authors tags. Raw tags are surfaced into
+/// equipment by UnsTagReference and materialized as the UNS-namespace fan-out of their
+/// backing raw nodes — that projection lands with the dual-namespace address space (Batch 4),
+/// NOT here. This walker therefore emits the equipment hierarchy + VirtualTags + ScriptedAlarms
+/// only; raw-tag reference variables are intentionally absent (the address space is deliberately
+/// dark until Batch 4). It remains a pure, SDK-free helper retained for unit-test support; real
+/// server deployments compose through the AddressSpaceComposer → Applier → Sink → NodeManager
+/// chain.
+///
///
-///
-///
-/// Composition strategy. Accepted Option A — Config
-/// primary. The walker treats the supplied
-/// snapshot as the authoritative published surface. Every Equipment row becomes a
-/// folder node at the UNS level-5 segment; every bound to an
-/// Equipment (non-null ) becomes a variable node under
-/// it. Driver-discovered tags that have no Config-DB row are not added by this
-/// walker — the ITagDiscovery path continues to exist for FolderPath-scoped tags +
-/// for enrichment, but Equipment-kind composition is fully Tag-row-driven.
-///
-///
-///
-/// Under each Equipment node. Five identifier properties
-/// (EquipmentId, EquipmentUuid, MachineCode, ZTag,
-/// SAPID) are added as OPC UA properties — external systems (ERP, SAP PM)
-/// resolve equipment by whichever identifier they natively use without a sidecar.
-/// materializes the OPC 40010
-/// Identification sub-folder with the nine fields when at least one
-/// is non-null; when all nine are null the sub-folder is omitted rather than
-/// appearing empty.
-///
-///
-///
-/// Address resolution. Variable nodes carry the driver-side full reference
-/// in copied from Tag.TagConfig
-/// (the wire-level address JSON blob whose interpretation is driver-specific). At
-/// runtime the dispatch layer routes Read/Write calls through the configured
-/// capability invoker; an unreachable address surfaces as an OPC UA Bad status via
-/// the natural driver-read failure path, NOT as a build-time reject. The ADR calls
-/// this "BadNotFound placeholder" behavior — legible to operators via their Admin
-/// UI + OPC UA client inspection of node status.
-///
-///
-///
-/// Pure function. This class has no dependency on the OPC UA SDK, no
-/// Config-DB access, no state. It consumes pre-loaded EF Core rows + streams calls
-/// into the supplied . The server-side wiring
-/// (load snapshot → invoke walker → per-tag capability probe) lives in the Task B
-/// PR alongside NodeScopeResolver's Config-DB join.
-///
-///
public static class EquipmentNodeWalker
{
///
- /// Walk into .
- /// The builder is scoped to the Equipment-kind namespace root; the walker emits
- /// Area → Line → Equipment folders under it, then identifier properties + the
- /// Identification sub-folder + variable nodes per bound Tag under each Equipment.
+ /// Walk into : Area → Line →
+ /// Equipment folders, identifier properties + the OPC 40010 Identification sub-folder per
+ /// equipment, and the equipment's VirtualTag + ScriptedAlarm variable nodes.
///
- ///
- /// The builder scoped to the Equipment-kind namespace root. Caller is responsible for
- /// creating this (e.g. rootBuilder.Folder(namespace.NamespaceId, namespace.NamespaceUri)).
- ///
- /// Pre-loaded + pre-filtered rows for a single published generation.
+ /// The builder scoped to the UNS namespace root.
+ /// Pre-loaded + pre-filtered rows for a single cluster.
public static void Walk(IAddressSpaceBuilder namespaceBuilder, EquipmentNamespaceContent content)
{
ArgumentNullException.ThrowIfNull(namespaceBuilder);
ArgumentNullException.ThrowIfNull(content);
- // Group lines by area + equipment by line + tags by equipment up-front. Avoids an
- // O(N·M) re-scan at each UNS level on large fleets.
var linesByArea = content.Lines
.GroupBy(l => l.UnsAreaId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.OrderBy(l => l.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);
@@ -83,11 +40,6 @@ public static class EquipmentNodeWalker
.GroupBy(e => e.UnsLineId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.OrderBy(e => e.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);
- var tagsByEquipment = content.Tags
- .Where(t => !string.IsNullOrEmpty(t.EquipmentId))
- .GroupBy(t => t.EquipmentId!, StringComparer.OrdinalIgnoreCase)
- .ToDictionary(g => g.Key, g => g.OrderBy(t => t.Name, StringComparer.Ordinal).ToList(), StringComparer.OrdinalIgnoreCase);
-
var virtualTagsByEquipment = (content.VirtualTags ?? [])
.Where(v => v.Enabled)
.GroupBy(v => v.EquipmentId, StringComparer.OrdinalIgnoreCase)
@@ -114,10 +66,6 @@ public static class EquipmentNodeWalker
AddIdentifierProperties(equipmentBuilder, equipment);
IdentificationFolderBuilder.Build(equipmentBuilder, equipment);
- if (tagsByEquipment.TryGetValue(equipment.EquipmentId, out var equipmentTags))
- foreach (var tag in equipmentTags)
- AddTagVariable(equipmentBuilder, tag);
-
if (virtualTagsByEquipment.TryGetValue(equipment.EquipmentId, out var vTags))
foreach (var vtag in vTags)
AddVirtualTagVariable(equipmentBuilder, vtag);
@@ -131,11 +79,9 @@ public static class EquipmentNodeWalker
}
///
- /// Adds the five operator-facing identifiers as OPC UA properties
- /// on the Equipment node. EquipmentId + EquipmentUuid are always populated;
- /// MachineCode is required per ; ZTag + SAPID are nullable in
- /// the data model so they're skipped when null to avoid empty-string noise in the
- /// browse tree.
+ /// Adds the operator-facing identifiers as OPC UA properties on the Equipment node.
+ /// EquipmentId + EquipmentUuid + MachineCode are always populated; ZTag + SAPID are skipped
+ /// when null to avoid empty-string noise in the browse tree.
///
private static void AddIdentifierProperties(IAddressSpaceBuilder equipmentBuilder, Equipment equipment)
{
@@ -149,67 +95,13 @@ public static class EquipmentNodeWalker
}
///
- /// Emit a single Tag row as an . The driver
- /// full reference lives in Tag.TagConfig (wire-level address, driver-specific
- /// JSON blob); the variable node's data type derives from Tag.DataType.
- /// Unreachable-address behavior: the variable is created; the
- /// driver's natural Read failure surfaces an OPC UA Bad status at runtime.
- ///
- private static void AddTagVariable(IAddressSpaceBuilder equipmentBuilder, Tag tag)
- {
- // SecurityClass and IsHistorized are intentionally not extracted from TagConfig here.
- // In production, EquipmentNodeWalker.Walk is superseded by the
- // AddressSpaceComposer → AddressSpaceApplier → Sink → NodeManager chain, which reads
- // both fields from TagConfig JSON directly (via DeploymentArtifact.ExtractTagHistorize
- // and the SecurityClassification column). This walker is retained for unit-test support
- // only; real server deployments never invoke Walk to build live nodes.
- var attr = new DriverAttributeInfo(
- FullName: ExtractFullName(tag.TagConfig),
- DriverDataType: ParseDriverDataType(tag.DataType),
- IsArray: false,
- ArrayDim: null,
- SecurityClass: SecurityClassification.FreeAccess,
- IsHistorized: false);
- equipmentBuilder.Variable(tag.Name, tag.Name, attr);
- }
-
- ///
- /// Cross-driver TagConfig convention — the Config DB's CK_Tag_TagConfig_IsJson
- /// check constraint requires TagConfig to be a JSON object, and every shipped driver
- /// (Galaxy / Modbus / AB CIP / S7 / FOCAS / TwinCAT / ABLegacy) stores the wire-level
- /// address in a top-level FullName field. Extracting it here keeps the walker
- /// driver-agnostic while giving the driver the plain address string its backend
- /// expects at read-time — the raw JSON would otherwise be passed verbatim to
- /// IReadable.ReadAsync and the driver would fail to resolve the tag.
- ///
- ///
- /// Falls back to the raw if it doesn't parse as JSON or
- /// the FullName field is absent. This preserves the pre-refactor behaviour for
- /// any legacy row that slipped past the check constraint or any future driver that
- /// wants an opaque non-JSON reference.
- ///
- /// The tag configuration JSON or string.
- /// The extracted FullName value, or the raw if it can't be extracted.
- internal static string ExtractFullName(string tagConfig) =>
- TagConfigIntent.Parse(tagConfig).FullName;
-
- ///
- /// Parse (stored as the enum
- /// name string) into the enum value. Unknown names fall back to
- /// so a one-off driver-specific type doesn't
- /// abort the whole walk; the underlying driver still sees the original TagConfig
- /// address + can surface its own typed value via the OPC UA variant at read time.
+ /// Parse a data-type name (stored as the enum name) into the
+ /// enum value; unknown names fall back to .
///
private static DriverDataType ParseDriverDataType(string raw) =>
Enum.TryParse(raw, ignoreCase: true, out var parsed) ? parsed : DriverDataType.String;
- ///
- /// Emit a row as a
- /// variable node. FullName doubles as the UNS path Phase 7's VirtualTagEngine
- /// addresses its engine-side entries by. The VirtualTagId discriminator lets
- /// the DriverNodeManager dispatch Reads/Subscribes to the engine rather than any
- /// driver.
- ///
+ /// Emit a row as a variable node.
private static void AddVirtualTagVariable(IAddressSpaceBuilder equipmentBuilder, VirtualTag vtag)
{
var attr = new DriverAttributeInfo(
@@ -227,14 +119,7 @@ public static class EquipmentNodeWalker
equipmentBuilder.Variable(vtag.Name, vtag.Name, attr);
}
- ///
- /// Emit a row as a
- /// variable node. The OPC UA Part 9 alarm-condition materialization happens at the
- /// node-manager level (which wires the concrete AlarmConditionState subclass
- /// per ); this walker provides the browse-level
- /// anchor + the flag that triggers that
- /// materialization path.
- ///
+ /// Emit a row as a variable node.
private static void AddScriptedAlarmVariable(IAddressSpaceBuilder equipmentBuilder, ScriptedAlarm alarm)
{
var attr = new DriverAttributeInfo(
@@ -254,15 +139,14 @@ public static class EquipmentNodeWalker
}
///
-/// Pre-loaded + pre-filtered snapshot of one Equipment-kind namespace's worth of Config
-/// DB rows. All four collections are scoped to the same
-/// row. The walker assumes this filter
-/// was applied by the caller + does no cross-namespace validation.
+/// Pre-loaded + pre-filtered snapshot of one cluster's UNS content (Area / Line / Equipment +
+/// per-equipment VirtualTags / ScriptedAlarms). v3: raw-tag references (UnsTagReference)
+/// are materialized by the Batch-4 dual-namespace fan-out, not by this walker, so they are not
+/// part of this snapshot.
///
public sealed record EquipmentNamespaceContent(
IReadOnlyList Areas,
IReadOnlyList Lines,
IReadOnlyList Equipment,
- IReadOnlyList Tags,
IReadOnlyList? VirtualTags = null,
IReadOnlyList? ScriptedAlarms = null);