From ba2ef13a1541a7807e08b3ec1594d15c7d534bc0 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 3 Sep 2026 17:13:17 +0700 Subject: [PATCH 1/3] Fix FAT current-pair PASS and live connection badge --- IoListTestingWindow.CommissioningStatus.cs | 93 ++++++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/IoListTestingWindow.CommissioningStatus.cs b/IoListTestingWindow.CommissioningStatus.cs index f95d67f2..c0410314 100644 --- a/IoListTestingWindow.CommissioningStatus.cs +++ b/IoListTestingWindow.CommissioningStatus.cs @@ -2,7 +2,9 @@ using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; +using ArIED61850Tester.Models; using ArIED61850Tester.Models.IoTesting; +using ArIED61850Tester.Services.IoTesting; namespace ArIED61850Tester; @@ -36,6 +38,11 @@ private void CommissioningConnectionBadgeTimer_Tick(object? sender, EventArgs e) { foreach (var plan in FatIedList.Items.OfType()) { + // Older/saved FAT workspaces can contain a complete generic Value 1 / Value 2 + // pair while Runtime.State still reflects the earlier transition-only contract. + // Re-assess only those complete generic digital pairs; raw evidence is untouched. + RefreshCurrentPairVerdicts(plan); + if (FatIedList.ItemContainerGenerator.ContainerFromItem(plan) is not ListBoxItem container) continue; @@ -44,13 +51,23 @@ private void CommissioningConnectionBadgeTimer_Tick(object? sender, EventArgs e) if (badge == null || text == null) continue; + // The FAT card must reflect the live Engineering runtime, not the last copied + // IoTestIedPlan flags. That removes the old Refresh dependency after an FO/network + // loss: as soon as the monitor marks the transport down, the card follows it. + var device = ResolveCommissioningRuntimeDevice(plan); var state = plan.IsPreparing ? "CONNECTING" - : plan.IsLiveConnected - ? "ONLINE" - : plan.IsLiveMonitoring - ? "RECONNECTING" - : "OFFLINE"; + : device != null + ? device.IsConnected + ? "ONLINE" + : device.IsMonitoring + ? "RECONNECTING" + : "OFFLINE" + : plan.IsLiveConnected + ? "ONLINE" + : plan.IsLiveMonitoring + ? "RECONNECTING" + : "OFFLINE"; var palette = state switch { @@ -66,15 +83,77 @@ private void CommissioningConnectionBadgeTimer_Tick(object? sender, EventArgs e) badge.Visibility = Visibility.Visible; badge.Background = ConnectionBadgeBrushFromHex(palette.Background); badge.BorderBrush = ConnectionBadgeBrushFromHex(palette.Border); - badge.ToolTip = string.IsNullOrWhiteSpace(plan.LiveStatusText) + var liveDetail = device == null ? plan.LiveStatusText : device.Status; + badge.ToolTip = string.IsNullOrWhiteSpace(liveDetail) ? state - : $"{state} · {plan.LiveStatusText}"; + : $"{state} · {liveDetail}"; text.Text = state; text.Foreground = ConnectionBadgeBrushFromHex(palette.Foreground); if (FindNamedVisual(container, "RelayIcon") is { } relayIcon) relayIcon.Foreground = ConnectionBadgeBrushFromHex(palette.Foreground); } + + // Keep Boolean presentation canonical without rewriting relay evidence or persisted + // RawValue. SetCurrentValue preserves the existing WPF binding, so a new sample can + // still replace the cell normally on the next runtime update. + if (_fatSignalsGrid != null) + NormalizeFatBooleanPresentation(_fatSignalsGrid); + } + + private Iec61850MonitorDevice? ResolveCommissioningRuntimeDevice(IoTestIedPlan plan) + { + if (Owner is not MainWindow engineeringWindow) + return null; + + if (!string.IsNullOrWhiteSpace(plan.LiveDeviceId)) + { + var byId = engineeringWindow.Devices.FirstOrDefault(device => + device.DeviceId.Equals(plan.LiveDeviceId, StringComparison.OrdinalIgnoreCase)); + if (byId != null) + return byId; + } + + return engineeringWindow.Devices.FirstOrDefault(device => + device.IpAddress.Equals(plan.IpAddress, StringComparison.OrdinalIgnoreCase) && + (device.Name.Equals(plan.IedName, StringComparison.OrdinalIgnoreCase) || + device.SclIedName.Equals(plan.IedName, StringComparison.OrdinalIgnoreCase))) + ?? engineeringWindow.Devices.FirstOrDefault(device => + device.IpAddress.Equals(plan.IpAddress, StringComparison.OrdinalIgnoreCase)); + } + + private static void RefreshCurrentPairVerdicts(IoTestIedPlan plan) + { + foreach (var point in plan.TestPoints) + { + if (point.CaptureMode != FatCaptureMode.AutomaticTransition || + point.Runtime.Value1Evidence == null || + point.Runtime.Value2Evidence == null || + point.Runtime.State is IoTestPointState.Passed or IoTestPointState.Review or IoTestPointState.Failed) + { + continue; + } + + FatCurrentEvidenceAssessmentService.Apply(point); + } + } + + private static void NormalizeFatBooleanPresentation(DependencyObject root) + { + var count = VisualTreeHelper.GetChildrenCount(root); + for (var index = 0; index < count; index++) + { + var child = VisualTreeHelper.GetChild(root, index); + if (child is TextBlock textBlock) + { + if (textBlock.Text.Equals("true", StringComparison.OrdinalIgnoreCase)) + textBlock.SetCurrentValue(TextBlock.TextProperty, "True"); + else if (textBlock.Text.Equals("false", StringComparison.OrdinalIgnoreCase)) + textBlock.SetCurrentValue(TextBlock.TextProperty, "False"); + } + + NormalizeFatBooleanPresentation(child); + } } private static T? FindNamedVisual(DependencyObject root, string name) From e46a622b1ae5e3cc25459f7b52c754b604453720 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 3 Sep 2026 17:13:37 +0700 Subject: [PATCH 2/3] Regress FAT current pair PASS and live badge updates --- ...tCurrentPairAndLiveBadgeRegressionTests.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs diff --git a/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs b/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs new file mode 100644 index 00000000..c13bec4c --- /dev/null +++ b/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs @@ -0,0 +1,93 @@ +using ArIED61850Tester.Models.IoTesting; +using ArIED61850Tester.Services.IoTesting; + +namespace ARSAS.Tests; + +public sealed class IoFatCurrentPairAndLiveBadgeRegressionTests +{ + [Fact] + public void CompletedGenericDigitalPair_TrueThenFalse_IsPass() + { + var point = NewDigitalPoint(); + point.Runtime.SetFatValueEvidence(Evidence(FatValueSlot.Value1, "True", 1)); + point.Runtime.SetFatValueEvidence(Evidence(FatValueSlot.Value2, "false", 2)); + + var decision = FatCurrentEvidenceAssessmentService.Apply(point); + + Assert.Equal(FatCurrentEvidenceVerdict.Passed, decision.Verdict); + Assert.Equal(IoTestPointState.Passed, point.Runtime.State); + Assert.Equal("✔ PASS", point.FatResultText); + Assert.True(point.IsFatEvidenceComplete); + } + + [Fact] + public void CommissioningCard_UsesActualEngineeringRuntime_NotCachedPlanFlags() + { + var source = ReadRepoFile("IoListTestingWindow.CommissioningStatus.cs"); + + Assert.Contains("engineeringWindow.Devices", source, StringComparison.Ordinal); + Assert.Contains("device.IsConnected", source, StringComparison.Ordinal); + Assert.Contains("device.IsMonitoring", source, StringComparison.Ordinal); + Assert.Contains("\"RECONNECTING\"", source, StringComparison.Ordinal); + Assert.Contains("RefreshCurrentPairVerdicts(plan);", source, StringComparison.Ordinal); + Assert.Contains("FatCurrentEvidenceAssessmentService.Apply(point);", source, StringComparison.Ordinal); + } + + [Fact] + public void FatBooleanPresentation_CanonicalizesCaseWithoutRewritingEvidence() + { + var source = ReadRepoFile("IoListTestingWindow.CommissioningStatus.cs"); + + Assert.Contains("NormalizeFatBooleanPresentation", source, StringComparison.Ordinal); + Assert.Contains("SetCurrentValue(TextBlock.TextProperty, \"True\")", source, StringComparison.Ordinal); + Assert.Contains("SetCurrentValue(TextBlock.TextProperty, \"False\")", source, StringComparison.Ordinal); + Assert.Contains("without rewriting relay evidence", source, StringComparison.OrdinalIgnoreCase); + } + + private static IoTestPointPlan NewDigitalPoint() + => new() + { + TestPointId = "DI-PAIR", + IedName = "IED1", + IpAddress = "192.0.2.10", + SignalName = "TimeSynchrnz", + ObjectReference = "IED1LD0/GGIO1.TimeSynchrnz.stVal", + FunctionalConstraint = "ST", + ExpectedOnText = "TRUE", + ExpectedOffText = "FALSE", + DataType = "Boolean", + SignalKind = FatSignalKind.Discrete, + CaptureMode = FatCaptureMode.AutomaticTransition, + WorkspaceSelected = true, + TestEnabled = true, + ImportReady = true, + BindingStatus = IoTestSignalSelectionService.SclWorkspaceAuthorityBindingStatus + }; + + private static FatValueEvidence Evidence(FatValueSlot slot, string rawValue, long sequence) + => new( + Guid.NewGuid(), + slot, + FatEvidenceCaptureKind.AutomaticValue, + rawValue, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddMilliseconds(-2), + "Good", + "BRCB", + sequence, + 1); + + private static string ReadRepoFile(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return File.ReadAllText(candidate).Replace("\r\n", "\n", StringComparison.Ordinal); + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +} From d20ad8244186963a77be7fa1f0439e49c4801702 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 3 Sep 2026 17:16:36 +0700 Subject: [PATCH 3/3] Fix FAT current-pair regression assertion --- .../ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs b/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs index c13bec4c..f80fc721 100644 --- a/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs +++ b/tests/ARSAS.Tests/IoFatCurrentPairAndLiveBadgeRegressionTests.cs @@ -14,7 +14,7 @@ public void CompletedGenericDigitalPair_TrueThenFalse_IsPass() var decision = FatCurrentEvidenceAssessmentService.Apply(point); - Assert.Equal(FatCurrentEvidenceVerdict.Passed, decision.Verdict); + Assert.Equal(IoTestPointState.Passed, decision.State); Assert.Equal(IoTestPointState.Passed, point.Runtime.State); Assert.Equal("✔ PASS", point.FatResultText); Assert.True(point.IsFatEvidenceComplete);