diff --git a/Assets/RelayFascia.svg b/Assets/RelayFascia.svg
index a5302ad8b..9a69c173b 100644
--- a/Assets/RelayFascia.svg
+++ b/Assets/RelayFascia.svg
@@ -1,47 +1,156 @@
-
-
diff --git a/Models/IoTesting/IoSignalDisplayName.cs b/Models/IoTesting/IoSignalDisplayName.cs
index 78303a978..e0130642f 100644
--- a/Models/IoTesting/IoSignalDisplayName.cs
+++ b/Models/IoTesting/IoSignalDisplayName.cs
@@ -16,15 +16,132 @@ public static string Format(string? signalName, string? iecReference)
{
var name = string.IsNullOrWhiteSpace(signalName) ? "Signal" : signalName.Trim();
var reference = iecReference?.Trim() ?? string.Empty;
- var match = PhaseToken().Match(reference);
- if (!match.Success)
- return name;
- var phase = match.Groups["phase"].Value.ToUpperInvariant();
- var suffix = $"Phs{phase}";
- if (name.EndsWith($" {suffix}", StringComparison.OrdinalIgnoreCase))
- return name;
+ var phaseMatch = PhaseToken().Match(reference);
+ if (phaseMatch.Success)
+ {
+ var phase = phaseMatch.Groups["phase"].Value.ToUpperInvariant();
+ var suffix = $"Phs{phase}";
+ if (name.EndsWith($" {suffix}", StringComparison.OrdinalIgnoreCase))
+ return name;
- return $"{name} {suffix}";
+ return $"{name} {suffix}";
+ }
+
+ if (TryExtractLogicalNodeContext(reference, out var logicalNodeClass, out var dataObject)
+ && RequiresLogicalNodeOwner(dataObject)
+ && name.Equals(dataObject, StringComparison.OrdinalIgnoreCase))
+ {
+ return $"{logicalNodeClass} {name}";
+ }
+
+ return name;
+ }
+
+ private static bool TryExtractLogicalNodeContext(
+ string reference,
+ out string logicalNodeClass,
+ out string dataObject)
+ {
+ logicalNodeClass = string.Empty;
+ dataObject = string.Empty;
+
+ if (string.IsNullOrWhiteSpace(reference))
+ return false;
+
+ var slashIndex = reference.LastIndexOf('/');
+ var tail = slashIndex >= 0 && slashIndex + 1 < reference.Length
+ ? reference[(slashIndex + 1)..]
+ : reference;
+
+ var dotIndex = tail.IndexOf('.');
+ var dollarIndex = tail.IndexOf('$');
+ var separatorIndex = dotIndex switch
+ {
+ >= 0 when dollarIndex >= 0 => Math.Min(dotIndex, dollarIndex),
+ >= 0 => dotIndex,
+ _ => dollarIndex
+ };
+
+ if (separatorIndex <= 0 || separatorIndex + 1 >= tail.Length)
+ return false;
+
+ var logicalNodeToken = tail[..separatorIndex].Trim();
+ if (!TryExtractLogicalNodeClass(logicalNodeToken, out logicalNodeClass))
+ return false;
+
+ var separator = tail[separatorIndex];
+ var remainder = tail[(separatorIndex + 1)..];
+ if (separator == '$')
+ {
+ var tokens = remainder.Split('$', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (tokens.Length == 0)
+ return false;
+
+ var dataObjectIndex = IsFunctionalConstraint(tokens[0]) ? 1 : 0;
+ if (dataObjectIndex >= tokens.Length)
+ return false;
+
+ dataObject = NormalizeDataObjectToken(tokens[dataObjectIndex]);
+ }
+ else
+ {
+ var tokenEnd = remainder.IndexOfAny(new[] { '.', '$', '[', '(' });
+ var token = tokenEnd >= 0 ? remainder[..tokenEnd] : remainder;
+ dataObject = NormalizeDataObjectToken(token);
+ }
+
+ return !string.IsNullOrWhiteSpace(dataObject);
}
+
+ private static bool TryExtractLogicalNodeClass(string logicalNodeToken, out string logicalNodeClass)
+ {
+ logicalNodeClass = string.Empty;
+ if (string.IsNullOrWhiteSpace(logicalNodeToken))
+ return false;
+
+ var token = logicalNodeToken.Trim();
+ if (token.Equals("LLN0", StringComparison.OrdinalIgnoreCase))
+ {
+ logicalNodeClass = "LLN0";
+ return true;
+ }
+
+ var end = token.Length;
+ while (end > 0 && char.IsDigit(token[end - 1]))
+ end--;
+
+ if (end == 0)
+ return false;
+
+ var withoutInstance = token[..end];
+ if (withoutInstance.Length >= 4)
+ {
+ var candidate = withoutInstance[^4..];
+ if (candidate.All(char.IsLetter))
+ {
+ logicalNodeClass = candidate.ToUpperInvariant();
+ return true;
+ }
+ }
+
+ logicalNodeClass = withoutInstance.ToUpperInvariant();
+ return true;
+ }
+
+ private static string NormalizeDataObjectToken(string token)
+ {
+ var value = token.Trim();
+ var end = value.IndexOfAny(new[] { '.', '$', '[', '(' });
+ return (end >= 0 ? value[..end] : value).Trim();
+ }
+
+ private static bool RequiresLogicalNodeOwner(string dataObject)
+ => dataObject.ToUpperInvariant() is "POS" or "MOD" or "BEH" or "HEALTH" or "LOC" or "OPCNT";
+
+ private static bool IsFunctionalConstraint(string token)
+ => token.ToUpperInvariant() is
+ "ST" or "MX" or "CO" or "SP" or "SV" or "CF" or "DC" or "SG" or "SE" or
+ "SR" or "OR" or "BL" or "EX" or "RP" or "BR" or "LG" or "GO" or "GS" or
+ "MS" or "US";
}
diff --git a/Resources/ArvrelMiniIedFascia.xaml b/Resources/ArvrelMiniIedFascia.xaml
index bbe693f8b..e9d3e2aa9 100644
--- a/Resources/ArvrelMiniIedFascia.xaml
+++ b/Resources/ArvrelMiniIedFascia.xaml
@@ -3,84 +3,255 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+
+
-
-