fix(opcua): equipment-tag planner diff + folder-scoped NodeIds (review findings)
Two bundle-review fixes + idempotency coverage: - CRITICAL: the planner ignored EquipmentTags, so an incremental deploy changing only equipment tags produced an empty plan and HandleRebuild short-circuited before materialising them. Add TagId to EquipmentTagPlan + Added/Removed/ChangedEquipmentTags to Phase7Plan (diffed by TagId, in IsEmpty, driving Apply's needsRebuild) — mirroring the GalaxyTags treatment. - IMPORTANT: equipment variable NodeId was the raw driver FullName, which collides across identical machines (e.g. two PLCs both exposing register 40001) — the second variable was silently dropped. NodeId is now folder-scoped (parent/Name); FullName stays on EquipmentTagPlan for the later values-routing milestone. - Task 4: SDK-backed idempotency test (double-apply -> single variable); restart-safety confirmed (RestoreApplied reuses the same RebuildAddressSpace -> HandleRebuild path). - Minor: align composer equipment-tag sort with the artifact decoder (coalesce FolderPath).
This commit is contained in:
@@ -81,14 +81,18 @@ public sealed record GalaxyTagPlan(
|
||||
|
||||
/// <summary>
|
||||
/// 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 parent
|
||||
/// <see cref="EquipmentId"/> folder (already materialised by <c>Phase7Applier.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>) used as the variable
|
||||
/// NodeId + read/write routing key. The equipment-signal analogue of <see cref="GalaxyTagPlan"/>.
|
||||
/// 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
|
||||
/// 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
|
||||
/// is folder-scoped (<c>parent/Name</c>), NOT <see cref="FullName"/>, because a raw driver ref
|
||||
/// (e.g. a Modbus register) is not unique across identical machines. The equipment-signal
|
||||
/// analogue of <see cref="GalaxyTagPlan"/>.
|
||||
/// </summary>
|
||||
public sealed record EquipmentTagPlan(
|
||||
string TagId,
|
||||
string EquipmentId,
|
||||
string DriverInstanceId,
|
||||
string FolderPath,
|
||||
@@ -218,9 +222,10 @@ public static class Phase7Composer
|
||||
&& namespacesById.TryGetValue(di.NamespaceId, out var ns)
|
||||
&& ns.Kind == NamespaceKind.Equipment)
|
||||
.OrderBy(t => t.EquipmentId, StringComparer.Ordinal)
|
||||
.ThenBy(t => t.FolderPath, StringComparer.Ordinal)
|
||||
.ThenBy(t => t.FolderPath ?? string.Empty, StringComparer.Ordinal) // coalesce so the sort matches the artifact-decode side exactly
|
||||
.ThenBy(t => t.Name, StringComparer.Ordinal)
|
||||
.Select(t => new EquipmentTagPlan(
|
||||
TagId: t.TagId,
|
||||
EquipmentId: t.EquipmentId!,
|
||||
DriverInstanceId: t.DriverInstanceId,
|
||||
FolderPath: t.FolderPath ?? string.Empty,
|
||||
|
||||
Reference in New Issue
Block a user