feat(runtime): DeploymentArtifact substitutes {{equip}} (parity with composer)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||||
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||||
@@ -193,7 +194,7 @@ public static class DeploymentArtifact
|
|||||||
var alarms = ReadArray(root, "ScriptedAlarms", ReadAlarmPlan);
|
var alarms = ReadArray(root, "ScriptedAlarms", ReadAlarmPlan);
|
||||||
var galaxyTags = BuildGalaxyTagPlans(root, drivers);
|
var galaxyTags = BuildGalaxyTagPlans(root, drivers);
|
||||||
var equipmentTags = BuildEquipmentTagPlans(root);
|
var equipmentTags = BuildEquipmentTagPlans(root);
|
||||||
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root);
|
var equipmentVirtualTags = BuildEquipmentVirtualTagPlans(root, equipmentTags);
|
||||||
|
|
||||||
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms, galaxyTags)
|
return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms, galaxyTags)
|
||||||
{
|
{
|
||||||
@@ -525,16 +526,31 @@ public static class DeploymentArtifact
|
|||||||
/// Join the artifact's VirtualTags array to its Scripts array (by ScriptId) to emit one
|
/// 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
|
/// <see cref="EquipmentVirtualTagPlan"/> per VirtualTag. The artifact-decode mirror of
|
||||||
/// <c>Phase7Composer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
|
/// <c>Phase7Composer.Compose</c>'s VirtualTag producer — so the compose-side + artifact-decode
|
||||||
/// plans agree. <c>Expression</c> = the joined Script's <c>SourceCode</c> (empty when the
|
/// plans agree. The reserved <c>{{equip}}</c> token in the joined Script's <c>SourceCode</c> is
|
||||||
/// ScriptId is absent); <c>DependencyRefs</c> = the distinct <c>ctx.GetTag("…")</c> literals in
|
/// substituted with the owning equipment's tag base (derived from <paramref name="equipmentTags"/>'
|
||||||
/// that source; <c>FolderPath</c> is always "" (VirtualTag has no FolderPath today). Ordered by
|
/// FullNames) BEFORE refs are extracted, byte-parity with the composer. <c>Expression</c> = the
|
||||||
/// EquipmentId then Name to match the composer's deterministic ordering.
|
/// substituted source (empty when the ScriptId is absent); <c>DependencyRefs</c> = the distinct
|
||||||
|
/// <c>ctx.GetTag("…")</c> literals in that source; <c>FolderPath</c> is always "" (VirtualTag has
|
||||||
|
/// no FolderPath today). Ordered by EquipmentId then Name to match the composer's deterministic
|
||||||
|
/// ordering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static IReadOnlyList<EquipmentVirtualTagPlan> BuildEquipmentVirtualTagPlans(JsonElement root)
|
private static IReadOnlyList<EquipmentVirtualTagPlan> BuildEquipmentVirtualTagPlans(
|
||||||
|
JsonElement root, IReadOnlyList<EquipmentTagPlan> equipmentTags)
|
||||||
{
|
{
|
||||||
if (!root.TryGetProperty("VirtualTags", out var vtArr) || vtArr.ValueKind != JsonValueKind.Array)
|
if (!root.TryGetProperty("VirtualTags", out var vtArr) || vtArr.ValueKind != JsonValueKind.Array)
|
||||||
return Array.Empty<EquipmentVirtualTagPlan>();
|
return Array.Empty<EquipmentVirtualTagPlan>();
|
||||||
|
|
||||||
|
// Per-equipment tag base = the shared substring-before-first-dot across each equipment's
|
||||||
|
// child-tag FullNames, used to expand the reserved {{equip}} token in shared VirtualTag
|
||||||
|
// scripts (equipment-relative tag paths). Derived from equipmentTags so the artifact-decode
|
||||||
|
// base matches the composer's exactly.
|
||||||
|
var baseByEquip = equipmentTags
|
||||||
|
.GroupBy(t => t.EquipmentId, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => EquipmentScriptPaths.DeriveEquipmentBase(g.Select(t => t.FullName)),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
// scriptId → SourceCode (the expression source the VirtualTagActor evaluates).
|
// scriptId → SourceCode (the expression source the VirtualTagActor evaluates).
|
||||||
var scriptSourceById = new Dictionary<string, string>(StringComparer.Ordinal);
|
var scriptSourceById = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
if (root.TryGetProperty("Scripts", out var scriptsArr) && scriptsArr.ValueKind == JsonValueKind.Array)
|
if (root.TryGetProperty("Scripts", out var scriptsArr) && scriptsArr.ValueKind == JsonValueKind.Array)
|
||||||
@@ -566,14 +582,20 @@ public static class DeploymentArtifact
|
|||||||
var source = scriptId is not null && scriptSourceById.TryGetValue(scriptId, out var src)
|
var source = scriptId is not null && scriptSourceById.TryGetValue(scriptId, out var src)
|
||||||
? src : string.Empty;
|
? src : string.Empty;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
var expanded = EquipmentScriptPaths.SubstituteEquipmentToken(
|
||||||
|
source, baseByEquip.GetValueOrDefault(equipmentId!));
|
||||||
|
|
||||||
result.Add(new EquipmentVirtualTagPlan(
|
result.Add(new EquipmentVirtualTagPlan(
|
||||||
VirtualTagId: virtualTagId!,
|
VirtualTagId: virtualTagId!,
|
||||||
EquipmentId: equipmentId!,
|
EquipmentId: equipmentId!,
|
||||||
FolderPath: string.Empty,
|
FolderPath: string.Empty,
|
||||||
Name: name!,
|
Name: name!,
|
||||||
DataType: dataType ?? "BaseDataType",
|
DataType: dataType ?? "BaseDataType",
|
||||||
Expression: source,
|
Expression: expanded,
|
||||||
DependencyRefs: ExtractDependencyRefs(source)));
|
DependencyRefs: EquipmentScriptPaths.ExtractDependencyRefs(expanded)));
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Sort((a, b) =>
|
result.Sort((a, b) =>
|
||||||
@@ -584,28 +606,6 @@ public static class DeploymentArtifact
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly System.Text.RegularExpressions.Regex GetTagRefRegex =
|
|
||||||
new(@"ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)", System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Distinct <c>ctx.GetTag("ref")</c> string literals in a VirtualTag script source, in
|
|
||||||
/// first-seen order. The artifact-decode mirror of <c>Phase7Composer.ExtractDependencyRefs</c>
|
|
||||||
/// — replicated (with the same regex) because Runtime does not reference the OpcUaServer
|
|
||||||
/// compose assembly; kept in sync with that copy.
|
|
||||||
/// </summary>
|
|
||||||
private static IReadOnlyList<string> ExtractDependencyRefs(string scriptSource)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(scriptSource)) return Array.Empty<string>();
|
|
||||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
var result = new List<string>();
|
|
||||||
foreach (System.Text.RegularExpressions.Match m in GetTagRefRegex.Matches(scriptSource))
|
|
||||||
{
|
|
||||||
var r = m.Groups[1].Value;
|
|
||||||
if (seen.Add(r)) result.Add(r);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extract the driver-side full reference from a tag's TagConfig JSON (top-level "FullName"
|
/// 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>Phase7Composer.ExtractTagFullName</c> /
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the artifact-decode mirror substitutes the reserved <c>{{equip}}</c> token in a
|
||||||
|
/// VirtualTag script's <c>ctx.GetTag("…")</c> literals with the owning equipment's tag base
|
||||||
|
/// (derived from its child Equipment-namespace tag's FullName) — byte-parity with
|
||||||
|
/// <c>Phase7Composer.Compose</c>'s live-edit path, using the same shared
|
||||||
|
/// <c>EquipmentScriptPaths</c> helper and the same equipmentTags-derived base.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeploymentArtifactEquipTokenTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ParseComposition_substitutes_equip_token_in_virtual_tag_expression()
|
||||||
|
{
|
||||||
|
var blob = JsonSerializer.SerializeToUtf8Bytes(new
|
||||||
|
{
|
||||||
|
Namespaces = new[]
|
||||||
|
{
|
||||||
|
new { NamespaceId = "ns-eq", Kind = 0 }, // NamespaceKind.Equipment
|
||||||
|
},
|
||||||
|
DriverInstances = new[]
|
||||||
|
{
|
||||||
|
new { DriverInstanceId = "drv-modbus", DriverType = "Modbus", DriverConfig = "{}", NamespaceId = "ns-eq" },
|
||||||
|
},
|
||||||
|
Tags = new object[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
TagId = "tag-source",
|
||||||
|
DriverInstanceId = "drv-modbus",
|
||||||
|
EquipmentId = "TestMachine_001",
|
||||||
|
Name = "Source",
|
||||||
|
FolderPath = (string?)null,
|
||||||
|
DataType = "Float",
|
||||||
|
TagConfig = "{\"FullName\":\"TestMachine_001.Source\"}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Scripts = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
ScriptId = "s-equip",
|
||||||
|
SourceCode = "return System.Convert.ToInt32(ctx.GetTag(\"{{equip}}.Source\").Value) > 50;",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VirtualTags = new[]
|
||||||
|
{
|
||||||
|
new
|
||||||
|
{
|
||||||
|
VirtualTagId = "vt-equip",
|
||||||
|
EquipmentId = "TestMachine_001",
|
||||||
|
Name = "OverThreshold",
|
||||||
|
DataType = "Boolean",
|
||||||
|
ScriptId = "s-equip",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var c = DeploymentArtifact.ParseComposition(blob);
|
||||||
|
|
||||||
|
var vt = c.EquipmentVirtualTags.ShouldHaveSingleItem();
|
||||||
|
vt.Expression.ShouldContain("ctx.GetTag(\"TestMachine_001.Source\")");
|
||||||
|
vt.Expression.ShouldNotContain("{{equip}}");
|
||||||
|
vt.DependencyRefs.ShouldBe(new[] { "TestMachine_001.Source" });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user