feat(opcua): compose Equipment VirtualTag plans from VirtualTag+Script rows
This commit is contained in:
@@ -175,6 +175,8 @@ public static class Phase7Composer
|
|||||||
/// <param name="scriptedAlarms">The scripted alarms.</param>
|
/// <param name="scriptedAlarms">The scripted alarms.</param>
|
||||||
/// <param name="tags">The tags.</param>
|
/// <param name="tags">The tags.</param>
|
||||||
/// <param name="namespaces">The namespaces.</param>
|
/// <param name="namespaces">The namespaces.</param>
|
||||||
|
/// <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>
|
/// <returns>The composition result.</returns>
|
||||||
public static Phase7CompositionResult Compose(
|
public static Phase7CompositionResult Compose(
|
||||||
IReadOnlyList<UnsArea> unsAreas,
|
IReadOnlyList<UnsArea> unsAreas,
|
||||||
@@ -183,8 +185,12 @@ public static class Phase7Composer
|
|||||||
IReadOnlyList<DriverInstance> driverInstances,
|
IReadOnlyList<DriverInstance> driverInstances,
|
||||||
IReadOnlyList<ScriptedAlarm> scriptedAlarms,
|
IReadOnlyList<ScriptedAlarm> scriptedAlarms,
|
||||||
IReadOnlyList<Tag> tags,
|
IReadOnlyList<Tag> tags,
|
||||||
IReadOnlyList<Namespace> namespaces)
|
IReadOnlyList<Namespace> namespaces,
|
||||||
|
IReadOnlyList<VirtualTag>? virtualTags = null,
|
||||||
|
IReadOnlyList<Script>? scripts = null)
|
||||||
{
|
{
|
||||||
|
var vtags = virtualTags ?? Array.Empty<VirtualTag>();
|
||||||
|
var scripts2 = scripts ?? Array.Empty<Script>();
|
||||||
var areas = unsAreas
|
var areas = unsAreas
|
||||||
.OrderBy(a => a.UnsAreaId, StringComparer.Ordinal)
|
.OrderBy(a => a.UnsAreaId, StringComparer.Ordinal)
|
||||||
.Select(a => new UnsAreaProjection(a.UnsAreaId, a.Name))
|
.Select(a => new UnsAreaProjection(a.UnsAreaId, a.Name))
|
||||||
@@ -258,9 +264,31 @@ public static class Phase7Composer
|
|||||||
FullName: ExtractTagFullName(t.TagConfig)))
|
FullName: ExtractTagFullName(t.TagConfig)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
// Equipment VirtualTags = each VirtualTag joined to its Script (by ScriptId) for the
|
||||||
|
// expression source. DependencyRefs = the distinct ctx.GetTag("…") literals the
|
||||||
|
// VirtualTagActor subscribes to. VirtualTag has no FolderPath today → "".
|
||||||
|
var scriptsById = scripts2.ToDictionary(s => s.ScriptId, StringComparer.Ordinal);
|
||||||
|
var equipmentVirtualTags = vtags
|
||||||
|
.OrderBy(v => v.EquipmentId, StringComparer.Ordinal)
|
||||||
|
.ThenBy(v => v.Name, StringComparer.Ordinal)
|
||||||
|
.Select(v =>
|
||||||
|
{
|
||||||
|
var src = scriptsById.TryGetValue(v.ScriptId, out var s) ? s.SourceCode : string.Empty;
|
||||||
|
return new EquipmentVirtualTagPlan(
|
||||||
|
VirtualTagId: v.VirtualTagId,
|
||||||
|
EquipmentId: v.EquipmentId,
|
||||||
|
FolderPath: string.Empty,
|
||||||
|
Name: v.Name,
|
||||||
|
DataType: v.DataType,
|
||||||
|
Expression: src,
|
||||||
|
DependencyRefs: ExtractDependencyRefs(src));
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms, galaxyTags)
|
return new Phase7CompositionResult(areas, lines, nodes, plans, alarms, galaxyTags)
|
||||||
{
|
{
|
||||||
EquipmentTags = equipmentTags,
|
EquipmentTags = equipmentTags,
|
||||||
|
EquipmentVirtualTags = equipmentVirtualTags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,4 +318,24 @@ public static class Phase7Composer
|
|||||||
catch (JsonException) { /* fall through to raw blob */ }
|
catch (JsonException) { /* fall through to raw blob */ }
|
||||||
return tagConfig;
|
return tagConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 dependency refs the VirtualTagActor subscribes to.</summary>
|
||||||
|
/// <param name="scriptSource">The VirtualTag's script source.</param>
|
||||||
|
/// <returns>The distinct dependency refs in first-seen order.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,77 @@ public sealed class Phase7ComposerPurityTests
|
|||||||
p.DependencyRefs.ShouldHaveSingleItem();
|
p.DependencyRefs.ShouldHaveSingleItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Compose joins a <see cref="VirtualTag"/> to its <see cref="Script"/> by ScriptId,
|
||||||
|
/// emitting one <see cref="EquipmentVirtualTagPlan"/> carrying the script source as the
|
||||||
|
/// Expression and the parsed <c>ctx.GetTag("…")</c> literals as DependencyRefs.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Compose_emits_equipment_virtualtag_plan_joined_to_script()
|
||||||
|
{
|
||||||
|
var vt = new VirtualTag
|
||||||
|
{
|
||||||
|
VirtualTagId = "vt-1",
|
||||||
|
EquipmentId = "eq-1",
|
||||||
|
Name = "speed-rpm",
|
||||||
|
DataType = "Float64",
|
||||||
|
ScriptId = "s-1",
|
||||||
|
};
|
||||||
|
var script = new Script
|
||||||
|
{
|
||||||
|
ScriptId = "s-1",
|
||||||
|
Name = "speed-script",
|
||||||
|
SourceCode = "return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;",
|
||||||
|
SourceHash = "hash-1",
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Phase7Composer.Compose(
|
||||||
|
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||||
|
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
|
||||||
|
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||||
|
virtualTags: new[] { vt },
|
||||||
|
scripts: new[] { script });
|
||||||
|
|
||||||
|
var plan = result.EquipmentVirtualTags.ShouldHaveSingleItem();
|
||||||
|
plan.VirtualTagId.ShouldBe("vt-1");
|
||||||
|
plan.EquipmentId.ShouldBe("eq-1");
|
||||||
|
plan.FolderPath.ShouldBe("");
|
||||||
|
plan.Name.ShouldBe("speed-rpm");
|
||||||
|
plan.DataType.ShouldBe("Float64");
|
||||||
|
plan.Expression.ShouldBe("return ctx.GetTag(\"TestMachine_001.TestDouble\").Value;");
|
||||||
|
plan.DependencyRefs.ShouldBe(new[] { "TestMachine_001.TestDouble" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>DependencyRefs are the distinct <c>ctx.GetTag("…")</c> literals in first-seen
|
||||||
|
/// order — a repeated ref collapses to one.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void Compose_extracts_distinct_dependency_refs()
|
||||||
|
{
|
||||||
|
var vt = new VirtualTag
|
||||||
|
{
|
||||||
|
VirtualTagId = "vt-1",
|
||||||
|
EquipmentId = "eq-1",
|
||||||
|
Name = "sum",
|
||||||
|
DataType = "Float64",
|
||||||
|
ScriptId = "s-1",
|
||||||
|
};
|
||||||
|
var script = new Script
|
||||||
|
{
|
||||||
|
ScriptId = "s-1",
|
||||||
|
Name = "sum-script",
|
||||||
|
SourceCode = "return ctx.GetTag(\"A.X\").Value + ctx.GetTag(\"B.Y\").Value + ctx.GetTag(\"A.X\").Value;",
|
||||||
|
SourceHash = "hash-1",
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Phase7Composer.Compose(
|
||||||
|
Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), Array.Empty<Equipment>(),
|
||||||
|
Array.Empty<DriverInstance>(), Array.Empty<ScriptedAlarm>(),
|
||||||
|
Array.Empty<Tag>(), Array.Empty<Namespace>(),
|
||||||
|
virtualTags: new[] { vt },
|
||||||
|
scripts: new[] { script });
|
||||||
|
|
||||||
|
var plan = result.EquipmentVirtualTags.ShouldHaveSingleItem();
|
||||||
|
plan.DependencyRefs.ShouldBe(new[] { "A.X", "B.Y" });
|
||||||
|
}
|
||||||
|
|
||||||
private static Equipment NewEquipment(string id) => new()
|
private static Equipment NewEquipment(string id) => new()
|
||||||
{
|
{
|
||||||
EquipmentId = id,
|
EquipmentId = id,
|
||||||
|
|||||||
Reference in New Issue
Block a user