-
Notifications
You must be signed in to change notification settings - Fork 2
Fix IO FAT selected-IED context, grid layout, and evidence safety #126
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
Open
masarray
wants to merge
32
commits into
main
Choose a base branch
from
fix/io-fat-context-layout-evidence
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
7a97078
Make IO FAT workspace selected-IED aware and protect completed evidence
masarray e5cfa46
Polish IO FAT card, grid, and selected IED workspace layout
masarray 3f27b25
Cover selected IED context, grid layout, and evidence protection
masarray efa98ea
Fix all-passed IED card detection
masarray 7c2a9ab
Simplify IO FAT PDF for customer readability
masarray ae8847f
Keep customer summary metrics inside report card
masarray 75c00ef
Add customer report and all-pass badge regressions
masarray 1d7c108
Bind IED PASS badge to total tested signal count
masarray 83809ec
Preserve technical PDF metadata without visual report noise
masarray 60e3992
Align PDF regressions with clean visible layout
masarray 9ab6581
Make IO FAT PDF clean, lean, and timestamp focused
masarray 3e8e247
Lock clean lean IO FAT report contract
masarray 3de9648
Fix IO FAT preview text clipping and ellipsis
masarray 55ab71a
Add regression guard against truncated FAT evidence text
masarray f98f6d8
Fit complete FAT evidence text without truncation
masarray 3339867
Fix false ellipsis in IO FAT evidence rows
masarray b3252e2
Add regression guard against ellipsized FAT evidence
masarray df857ff
Preserve Rev3 document control and event-log traceability
masarray a60b19c
Import event-log-ready FAT workbook schema
masarray 65acad5
Add document-controlled event-log FAT report layout
masarray 392f102
Use executive FAT layout for native PDF
masarray c719f37
Preserve document control in selected IED preview
masarray 79b638a
Use executive layout in native print preview
masarray 73b49b9
Test Rev3 event-log-ready workbook import
masarray 63f162f
Test executive FAT PDF traceability and signoff
masarray 3df8132
Fix Rev3 IEC reference matching for IO FAT acquisition
masarray fbf58d0
Match Rev3 FAT points by exact event-log references
masarray 387b762
Cover Rev3 Application and event-log reference matching
masarray 769b858
Test Rev3 reference matching through public selection behavior
masarray d6032ec
Polish FAT evidence report hierarchy and status presentation
masarray b875f35
Use modern report font fallback stack
masarray 18573e9
Lock final FAT report visual hierarchy and as-tested status
masarray File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,325 @@ | ||
| using System.ComponentModel; | ||
| using System.Globalization; | ||
| using System.Windows; | ||
| using System.Windows.Controls; | ||
| using System.Windows.Data; | ||
| using System.Windows.Threading; | ||
| using ArIED61850Tester.Models.IoTesting; | ||
| using ArIED61850Tester.Services.IoTesting; | ||
|
|
||
| namespace ArIED61850Tester; | ||
|
|
||
| public partial class IoListTestingWindow | ||
| { | ||
| private bool _selectedIedContextInstalled; | ||
|
|
||
| public bool SelectedCanStartWorkflow => | ||
| SelectedIed != null && !IsPreparingIed && Session.CanStart; | ||
|
|
||
| public bool SelectedCanPause => | ||
| IsSelectedSessionIed && Session.CanPause; | ||
|
|
||
| public bool SelectedCanResume => | ||
| IsSelectedSessionIed && Session.CanResume; | ||
|
|
||
| public bool SelectedCanStop => | ||
| IsSelectedSessionIed && Session.CanStop; | ||
|
|
||
| public string SelectedStartWorkflowText | ||
| { | ||
| get | ||
| { | ||
| if (SelectedIed == null) | ||
| return "Select IED"; | ||
|
|
||
| if (_preparingIed != null) | ||
| { | ||
| return ReferenceEquals(_preparingIed, SelectedIed) | ||
| ? $"Connecting {SelectedIed.IedName}…" | ||
| : $"{_preparingIed.IedName} connecting…"; | ||
| } | ||
|
|
||
| if (Session.IsSessionActive) | ||
| { | ||
| return IsSelectedSessionIed | ||
| ? "IED session active" | ||
| : $"{Session.ActiveIed?.IedName ?? "Another IED"} active"; | ||
| } | ||
|
|
||
| var enabled = EnabledPoints(SelectedIed); | ||
| if (enabled.Count > 0 && enabled.All(point => point.Runtime.State == IoTestPointState.Passed)) | ||
| return "Reconnect / Retest"; | ||
|
|
||
| return enabled.Any(point => point.Runtime.IsComplete) | ||
| ? "Connect & Continue IED" | ||
| : "Connect & Start IED"; | ||
| } | ||
| } | ||
|
|
||
| public string SelectedFooterStatusText | ||
| { | ||
| get | ||
| { | ||
| if (SelectedIed == null) | ||
| return "Select an imported IED."; | ||
|
|
||
| if (ReferenceEquals(_preparingIed, SelectedIed)) | ||
| return PreparationStatusText; | ||
|
|
||
| if (IsSelectedSessionIed && Session.State != IoTestSessionState.Idle) | ||
| return Session.StatusText; | ||
|
|
||
| var enabled = EnabledPoints(SelectedIed); | ||
| var complete = enabled.Count(point => point.Runtime.IsComplete); | ||
| var passed = enabled.Count(point => point.Runtime.State == IoTestPointState.Passed); | ||
|
|
||
| if (enabled.Count > 0 && passed == enabled.Count) | ||
| return $"{SelectedIed.IedName} complete · all {enabled.Count} enabled points PASS · evidence protected."; | ||
|
|
||
| if (complete > 0) | ||
| return $"{SelectedIed.IedName} selected · {complete}/{enabled.Count} complete · completed evidence will be preserved."; | ||
|
|
||
| return $"{SelectedIed.IedName} selected · {SelectedIed.LiveStatusText}"; | ||
| } | ||
| } | ||
|
|
||
| public string SelectedProgressText | ||
| { | ||
| get | ||
| { | ||
| if (SelectedIed == null) | ||
| return "0 / 0 complete"; | ||
|
|
||
| var enabled = EnabledPoints(SelectedIed); | ||
| var complete = enabled.Count(point => point.Runtime.IsComplete); | ||
| var passed = enabled.Count(point => point.Runtime.State == IoTestPointState.Passed); | ||
| var review = enabled.Count(point => point.Runtime.State == IoTestPointState.Review); | ||
| var failed = enabled.Count(point => point.Runtime.State == IoTestPointState.Failed); | ||
| return $"{complete} / {enabled.Count} complete · {passed} PASS · {review} review · {failed} fail"; | ||
| } | ||
| } | ||
|
|
||
| public int SelectedEvidenceCount => SelectedIed?.TestPoints.Sum(point => | ||
| (point.Runtime.OnEvidence == null ? 0 : 1) + | ||
| (point.Runtime.OffEvidence == null ? 0 : 1)) ?? 0; | ||
|
|
||
| private bool IsSelectedSessionIed => | ||
| SelectedIed != null && ReferenceEquals(Session.ActiveIed, SelectedIed); | ||
|
|
||
| private void IoListTestingWindow_ContentRendered(object? sender, EventArgs e) | ||
| { | ||
| Dispatcher.BeginInvoke( | ||
| new Action(InstallSelectedIedContext), | ||
| DispatcherPriority.ContextIdle); | ||
| } | ||
|
|
||
| private void InstallSelectedIedContext() | ||
| { | ||
| AdoptWorkspacePreviewToggle(); | ||
| if (_selectedIedContextInstalled) | ||
| { | ||
| RaiseSelectedIedContextProperties(); | ||
| return; | ||
| } | ||
|
|
||
| foreach (var ied in Project.Ieds) | ||
| ied.PropertyChanged += ContextIed_PropertyChanged; | ||
|
|
||
| PropertyChanged += ContextWindow_PropertyChanged; | ||
| Session.PropertyChanged += ContextSession_PropertyChanged; | ||
| Closed += ContextWindow_Closed; | ||
| _selectedIedContextInstalled = true; | ||
| RaiseSelectedIedContextProperties(); | ||
| } | ||
|
|
||
| private void AdoptWorkspacePreviewToggle() | ||
| { | ||
| if (_printPreviewToggle != null && !ReferenceEquals(_printPreviewToggle, WorkspacePreviewToggle)) | ||
| { | ||
| if (LogicalTreeHelper.GetParent(_printPreviewToggle) is Panel parent) | ||
| parent.Children.Remove(_printPreviewToggle); | ||
| } | ||
|
|
||
| _printPreviewToggle = WorkspacePreviewToggle; | ||
| _printPreviewToggle.Content = _printPreviewActive ? "Signals View" : "Print Preview"; | ||
| _printPreviewToggle.ToolTip = "Toggle the selected IED between signal evidence and native print preview"; | ||
| } | ||
|
|
||
| private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e) | ||
| { | ||
| if (IsPreparingIed) | ||
| return; | ||
|
|
||
| var selectedIed = SelectedIed; | ||
| var preflight = IoTestSessionPreflight.Validate(selectedIed); | ||
| if (!preflight.Succeeded) | ||
| { | ||
| ShowActionResult(preflight, "FAT session scope is not ready"); | ||
| return; | ||
| } | ||
|
|
||
| var enabledReady = selectedIed!.TestPoints | ||
| .Where(point => point.TestEnabled && point.ImportReady) | ||
| .ToList(); | ||
| var completedPoints = enabledReady | ||
| .Where(point => point.Runtime.IsComplete) | ||
| .ToList(); | ||
| var incompletePoints = enabledReady | ||
| .Where(point => !point.Runtime.IsComplete) | ||
| .ToList(); | ||
|
|
||
| var explicitRetest = false; | ||
| if (completedPoints.Count > 0 && incompletePoints.Count == 0) | ||
| { | ||
| var answer = MessageBox.Show( | ||
| this, | ||
| $"All {completedPoints.Count} enabled rows for {selectedIed.IedName} already contain completed evidence.\n\nA normal Connect click will not erase them. Choose Yes only when you intentionally want to retest every completed row and replace its ON/OFF evidence.", | ||
| "Retest completed evidence?", | ||
| MessageBoxButton.YesNo, | ||
| MessageBoxImage.Warning, | ||
| MessageBoxResult.No); | ||
| if (answer != MessageBoxResult.Yes) | ||
| { | ||
| PreparationStatusText = $"{selectedIed.IedName} evidence preserved · no retest started."; | ||
| RaiseSelectedIedContextProperties(); | ||
| return; | ||
| } | ||
|
|
||
| explicitRetest = true; | ||
| } | ||
|
|
||
| SetPreparingIed(selectedIed, $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102"); | ||
| try | ||
| { | ||
| if (Owner is MainWindow engineeringWindow) | ||
| { | ||
| var progress = new Progress<string>(message => | ||
| { | ||
| selectedIed.SetPreparationState(true, message); | ||
| PreparationStatusText = message; | ||
| RaiseStatusProperties(); | ||
| RaiseSelectedIedContextProperties(); | ||
| }); | ||
| var preparation = await engineeringWindow.PrepareIoTestIedForFatAsync( | ||
| Project, | ||
| selectedIed, | ||
| progress); | ||
| RaiseStatusProperties(); | ||
| RaiseSelectedIedContextProperties(); | ||
| if (!preparation.Succeeded) | ||
| { | ||
| PreparationStatusText = preparation.Message; | ||
| ShowActionResult(preparation, "IED acquisition could not start"); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| var protectedPoints = explicitRetest | ||
| ? Array.Empty<IoTestPointPlan>() | ||
| : completedPoints.ToArray(); | ||
| IoTestSessionActionResult result; | ||
| try | ||
| { | ||
| foreach (var point in protectedPoints) | ||
| point.TestEnabled = false; | ||
|
|
||
| result = Session.Start(selectedIed); | ||
| } | ||
| finally | ||
| { | ||
| foreach (var point in protectedPoints) | ||
| point.TestEnabled = true; | ||
| } | ||
|
|
||
| ShowActionResult(result, "FAT evidence session could not start"); | ||
| RaiseStatusProperties(); | ||
| RaiseSelectedIedContextProperties(); | ||
| if (result.Succeeded) | ||
| { | ||
| PreparationStatusText = protectedPoints.Length > 0 | ||
| ? $"{selectedIed.IedName} live · {protectedPoints.Length} completed row(s) preserved · waiting for pending OFF → ON → OFF tests" | ||
| : $"{selectedIed.IedName} live · waiting for OFF → ON → OFF"; | ||
| Storage?.ScheduleSave(); | ||
| } | ||
| else | ||
| { | ||
| PreparationStatusText = result.Message; | ||
| } | ||
| } | ||
| catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException) | ||
| { | ||
| PreparationStatusText = ex.Message; | ||
| MessageBox.Show( | ||
| this, | ||
| ex.Message, | ||
| "Connect and start IED failed", | ||
| MessageBoxButton.OK, | ||
| MessageBoxImage.Error); | ||
| } | ||
| finally | ||
| { | ||
| selectedIed.SetPreparationState(false, selectedIed.LiveStatusText); | ||
| SetPreparingIed(null, string.Empty); | ||
| RaiseSelectedIedContextProperties(); | ||
| } | ||
| } | ||
|
|
||
| private void ContextWindow_PropertyChanged(object? sender, PropertyChangedEventArgs e) | ||
| { | ||
| if (e.PropertyName is nameof(SelectedIed) or nameof(IsPreparingIed) or nameof(PreparationStatusText)) | ||
| RaiseSelectedIedContextProperties(); | ||
| } | ||
|
|
||
| private void ContextSession_PropertyChanged(object? sender, PropertyChangedEventArgs e) | ||
| => RaiseSelectedIedContextProperties(); | ||
|
|
||
| private void ContextIed_PropertyChanged(object? sender, PropertyChangedEventArgs e) | ||
| { | ||
| if (!ReferenceEquals(sender, SelectedIed)) | ||
| return; | ||
|
|
||
| Raise(nameof(SelectedIedSummary)); | ||
| RaiseSelectedIedContextProperties(); | ||
| } | ||
|
|
||
| private void RaiseSelectedIedContextProperties() | ||
| { | ||
| Raise(nameof(SelectedCanStartWorkflow)); | ||
| Raise(nameof(SelectedCanPause)); | ||
| Raise(nameof(SelectedCanResume)); | ||
| Raise(nameof(SelectedCanStop)); | ||
| Raise(nameof(SelectedStartWorkflowText)); | ||
| Raise(nameof(SelectedFooterStatusText)); | ||
| Raise(nameof(SelectedProgressText)); | ||
| Raise(nameof(SelectedEvidenceCount)); | ||
| } | ||
|
|
||
| private void ContextWindow_Closed(object? sender, EventArgs e) | ||
| { | ||
| foreach (var ied in Project.Ieds) | ||
| ied.PropertyChanged -= ContextIed_PropertyChanged; | ||
|
|
||
| PropertyChanged -= ContextWindow_PropertyChanged; | ||
| Session.PropertyChanged -= ContextSession_PropertyChanged; | ||
| Closed -= ContextWindow_Closed; | ||
| } | ||
|
|
||
| private static List<IoTestPointPlan> EnabledPoints(IoTestIedPlan ied) | ||
| => ied.TestPoints.Where(point => point.TestEnabled).ToList(); | ||
| } | ||
|
|
||
| public sealed class IoFatAllPassedVisibilityConverter : IMultiValueConverter | ||
| { | ||
| public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) | ||
| { | ||
| var selectedCount = values.Length > 0 && values[0] is int count ? count : 0; | ||
| var passed = values.Length > 1 && values[1] is int passedCount ? passedCount : 0; | ||
| var allPassed = selectedCount > 0 && passed >= selectedCount; | ||
| if (string.Equals(parameter?.ToString(), "Inverse", StringComparison.OrdinalIgnoreCase)) | ||
| allPassed = !allPassed; | ||
| return allPassed ? Visibility.Visible : Visibility.Collapsed; | ||
| } | ||
|
|
||
| public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) | ||
| => targetTypes.Select(_ => Binding.DoNothing).ToArray(); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 a mixed IED contains a completed signal that is no longer available in the live model, this preparation call still includes it because completed points remain enabled until immediately before
Session.Start.PrepareIoTestIedForFatAsyncsnapshots every enabled point (MainWindow.IoTesting.AutoConnect.cs:24-26) and fails unless all requested points become live (MainWindow.IoTesting.AutoConnect.cs:203-213), so the method returns before pending, otherwise-valid rows can resume. Apply the protected-row scope during preflight and preparation as well as duringSession.Start.Useful? React with 👍 / 👎.