-
Notifications
You must be signed in to change notification settings - Fork 4
Merge FAT current-pair PASS/live FO state into current main #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba2ef13
e46a622
d20ad82
2237ec3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<IoTestIedPlan>()) | ||
| { | ||
| // 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<Control>(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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an older workspace already stores a terminal legacy verdict, this guard prevents the advertised current-pair migration from running. Useful? React with 👍 / 👎. |
||
| { | ||
| 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<T>(DependencyObject root, string name) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(IoTestPointState.Passed, decision.State); | ||
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
LiveDeviceIdis empty or stale and the name match fails, this fallback selects the first device sharing the plan's IP address.IoTestLiveBindingService.FindDevicedeliberately accepts an IP-only match only when it is unique, so a multi-IED/shared-endpoint workspace that was correctly left unbound can nevertheless display another IED's ONLINE/OFFLINE state and status. Require a unique IP match or reuse the binding resolver's matching semantics.Useful? React with 👍 / 👎.