Path-based NodeIds — decouple client contract from driver address
The pre-refactor design minted OPC UA NodeIds directly from the driver's
FullReference (the native-address string). That had three long-term
problems:
1. OPC UA Part 3 §5.2.2 requires NodeIds to be immutable across a node's
lifetime. A rename of the underlying device address — Galaxy attribute,
S7 tag, Modbus register alias — changed the NodeId and broke every
client that had pinned the previous identifier.
2. Two drivers with coincidentally-matching native addresses (e.g. `temp`
in Modbus and `temp` in S7 under different Equipment rows) collided on
the NodeId identifier.
3. TagConfig was being placed verbatim on the wire; for drivers whose
TagConfig is JSON (every driver shipped today, per the
CK_Tag_TagConfig_IsJson check constraint), clients saw the raw JSON
blob as the NodeId string.
Refactor:
* DriverNodeManager.Variable now mints a stable path-based NodeId
`{driverId}/{folder-path}/{browseName}` and records the driver-side
FullReference in a new _fullRefByNodeId map. OnReadValue / OnWriteValue
/ ResolveFullRef look the FullReference up via that map instead of
casting NodeId.Identifier. The old cast path is preserved as a
fallback so any test fixture that still registers variables with
FullRef-shaped NodeIds keeps working.
* EquipmentNodeWalker.AddTagVariable now extracts the cross-driver
`FullName` field from Tag.TagConfig before handing the address to
DriverAttributeInfo. Every shipped driver stores the wire reference in
TagConfig[FullName]; falling back to the raw string covers any future
driver that wants an opaque non-JSON address. ExtractFullName is
exposed internal for unit coverage.
* scripts/e2e/test-galaxy.ps1 defaults updated to the new path-based
NodeIds. Verified live against p7-smoke-galaxy on the dev box:
`ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source` reads
return Status=0x00000000 with a real Galaxy byte-array value.
Test suite: 195/195 Core.Tests + 283/283 Server.Tests green. Five new
ExtractFullName / FullName-passthrough tests added.
Task #112 GA-3 — golden-path read verified end-to-end; remaining E2E
script stages still blocked on pre-existing issues (ScriptedAlarm
predicate NRE on empty upstream cache, PowerShell $changeLines.Count
guard), tracked separately.
Task #134 — complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
@@ -157,7 +158,7 @@ public static class EquipmentNodeWalker
|
||||
private static void AddTagVariable(IAddressSpaceBuilder equipmentBuilder, Tag tag)
|
||||
{
|
||||
var attr = new DriverAttributeInfo(
|
||||
FullName: tag.TagConfig,
|
||||
FullName: ExtractFullName(tag.TagConfig),
|
||||
DriverDataType: ParseDriverDataType(tag.DataType),
|
||||
IsArray: false,
|
||||
ArrayDim: null,
|
||||
@@ -166,6 +167,38 @@ public static class EquipmentNodeWalker
|
||||
equipmentBuilder.Variable(tag.Name, tag.Name, attr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross-driver TagConfig convention — the Config DB's <c>CK_Tag_TagConfig_IsJson</c>
|
||||
/// 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 <c>FullName</c> 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
|
||||
/// <c>IReadable.ReadAsync</c> and the driver would fail to resolve the tag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Falls back to the raw <paramref name="tagConfig"/> if it doesn't parse as JSON or
|
||||
/// the <c>FullName</c> 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.
|
||||
/// </remarks>
|
||||
internal static string ExtractFullName(string tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(tagConfig);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object
|
||||
&& doc.RootElement.TryGetProperty("FullName", out var fullName)
|
||||
&& fullName.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return fullName.GetString() ?? tagConfig;
|
||||
}
|
||||
}
|
||||
catch (JsonException) { /* fall through */ }
|
||||
return tagConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse <see cref="Tag.DataType"/> (stored as the <see cref="DriverDataType"/> enum
|
||||
/// name string, decision #138) into the enum value. Unknown names fall back to
|
||||
|
||||
Reference in New Issue
Block a user