Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 123 additions & 19 deletions IoListTestingWindow.ContextUx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public partial class IoListTestingWindow
private bool _selectedIedContextInstalled;

public bool SelectedCanStartWorkflow =>
SelectedIed != null && !IsPreparingIed && Session.CanStart;
SelectedIed != null && !SelectedIed.IsPreparing && Session.CanStart;

public bool SelectedCanPause =>
IsSelectedSessionIed && Session.CanPause;
Expand All @@ -32,27 +32,30 @@ public string SelectedStartWorkflowText
if (SelectedIed == null)
return "Select IED";

if (_preparingIed != null)
{
return ReferenceEquals(_preparingIed, SelectedIed)
? $"Connecting {SelectedIed.IedName}…"
: $"{_preparingIed.IedName} connecting…";
}
if (SelectedIed.IsPreparing)
return $"Connecting {SelectedIed.IedName}…";

if (Session.IsSessionActive)
{
return IsSelectedSessionIed
? "IED session active"
: $"{Session.ActiveIed?.IedName ?? "Another IED"} active";
: $"{Session.ActiveIed?.IedName ?? "Another IED"} FAT active";
}

var enabled = EnabledPoints(SelectedIed);
if (enabled.Count > 0 && enabled.All(point => point.Runtime.State == IoTestPointState.Passed))
return "Reconnect / Retest";
var allPassed = enabled.Count > 0 && enabled.All(point => point.Runtime.State == IoTestPointState.Passed);
var hasCompleted = enabled.Any(point => point.Runtime.IsComplete);

return enabled.Any(point => point.Runtime.IsComplete)
? "Connect & Continue IED"
: "Connect & Start IED";
if (SelectedIed.IsLiveMonitoring)
{
if (allPassed)
return "Retest FAT";
return hasCompleted ? "Continue FAT" : "Start FAT";
}

if (allPassed)
return "Reconnect / Retest";
return hasCompleted ? "Connect & Continue IED" : "Connect & Start IED";
}
}

Expand All @@ -63,8 +66,8 @@ public string SelectedFooterStatusText
if (SelectedIed == null)
return "Select an imported IED.";

if (ReferenceEquals(_preparingIed, SelectedIed))
return PreparationStatusText;
if (SelectedIed.IsPreparing)
return SelectedIed.PreparationStatusText;

if (IsSelectedSessionIed && Session.State != IoTestSessionState.Idle)
return Session.StatusText;
Expand Down Expand Up @@ -152,12 +155,100 @@ private void AdoptWorkspacePreviewToggle()
_printPreviewToggle.ToolTip = "Toggle the selected IED between signal evidence and native print preview";
}

private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e)
/// <summary>
/// Card-local connection action. Several different IED cards can run this method at
/// once; each device owns its own MainWindow connection workflow and model progress.
/// Evidence capture remains a separate, single-active session selected by Start FAT.
/// </summary>
private async void ConnectIed_Click(object sender, RoutedEventArgs e)
{
if (IsPreparingIed)
var targetIed = (sender as FrameworkElement)?.DataContext as IoTestIedPlan;
if (targetIed == null || targetIed.IsPreparing)
return;

var enabledReady = targetIed.TestPoints
.Where(point => point.TestEnabled && point.ImportReady)
.ToList();
if (enabledReady.Count == 0)
{
ShowActionResult(
IoTestSessionActionResult.Failure("No import-ready IO-list signal is enabled for this IED."),
"IED connection scope is not ready");
return;
}

// For a continuation, connect only what still needs evidence. Completed rows keep
// their sealed evidence and do not have to exist in a replacement relay model.
// If every row is already complete, refresh the complete enabled scope instead.
var pendingScope = enabledReady.Where(point => !point.Runtime.IsComplete).ToList();
IReadOnlyCollection<IoTestPointPlan> connectionScope = pendingScope.Count > 0
? pendingScope
: enabledReady;

if (ReferenceEquals(SelectedIed, targetIed))
PreparationStatusText = $"Connecting {targetIed.IedName} · {targetIed.IpAddress}:102";
RaisePreparationProperties();
RaiseSelectedIedContextProperties();

try
{
if (Owner is not MainWindow engineeringWindow)
return;

var progress = new Progress<string>(message =>
{
if (ReferenceEquals(SelectedIed, targetIed))
PreparationStatusText = message;
RaiseStatusProperties();
RaiseSelectedIedContextProperties();
});

var preparation = await PrepareIndependentIedConnectionAsync(
engineeringWindow,
targetIed,
progress,
connectionScope);

RaiseStatusProperties();
RaiseSelectedIedContextProperties();
if (!preparation.Succeeded)
{
if (ReferenceEquals(SelectedIed, targetIed))
PreparationStatusText = preparation.Message;
ShowActionResult(preparation, $"{targetIed.IedName} acquisition could not start");
return;
}

await CaptureTimeSyncEvidenceAfterPreparationAsync(engineeringWindow, targetIed);
if (ReferenceEquals(SelectedIed, targetIed))
PreparationStatusText = $"{targetIed.IedName} live · ready for FAT evidence";
Storage?.ScheduleSave();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException or ArgumentException)
{
if (ReferenceEquals(SelectedIed, targetIed))
PreparationStatusText = ex.Message;
MessageBox.Show(
this,
ex.Message,
$"Connect {targetIed.IedName} failed",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
finally
{
targetIed.SetPreparationState(false, targetIed.LiveStatusText);
RaisePreparationProperties();
RaiseSelectedIedContextProperties();
}
}

private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e)
{
var selectedIed = SelectedIed;
if (selectedIed?.IsPreparing == true)
return;

if (selectedIed == null)
{
var missingSelection = IoTestSessionPreflight.Validate(null);
Expand All @@ -180,7 +271,7 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
{
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.",
$"All {completedPoints.Count} enabled rows for {selectedIed.IedName} already contain completed evidence.\n\nA normal Start FAT 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,
Expand Down Expand Up @@ -220,7 +311,6 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
{
var progress = new Progress<string>(message =>
{
selectedIed.SetPreparationState(true, message);
PreparationStatusText = message;
RaiseStatusProperties();
RaiseSelectedIedContextProperties();
Expand Down Expand Up @@ -278,6 +368,17 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
}
}

private Task<IoTestSessionActionResult> PrepareIndependentIedConnectionAsync(
MainWindow engineeringWindow,
IoTestIedPlan targetIed,
IProgress<string> progress,
IReadOnlyCollection<IoTestPointPlan> connectionScope)
=> engineeringWindow.PrepareIoTestIedForFatAsync(
Project,
targetIed,
progress,
connectionScope);

private void ContextWindow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(SelectedIed) or nameof(IsPreparingIed) or nameof(PreparationStatusText))
Expand All @@ -289,6 +390,9 @@ private void ContextSession_PropertyChanged(object? sender, PropertyChangedEvent

private void ContextIed_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(IoTestIedPlan.IsPreparing))
RaisePreparationProperties();

if (!ReferenceEquals(sender, SelectedIed))
return;

Expand Down
83 changes: 83 additions & 0 deletions IoListTestingWindow.MultiIedConnectionUx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2026 Ari Sulistiono
// SPDX-License-Identifier: Apache-2.0

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using ArIED61850Tester.Models.IoTesting;

namespace ArIED61850Tester;

/// <summary>
/// Installs a compact Connect action beside the selected IED workflow controls without
/// changing the evidence-session contract. The button follows SelectedIed, so the
/// operator can start IED B while IED A is still preparing; each card continues to show
/// its own progress and live state.
/// </summary>
public partial class IoListTestingWindow
{
private static readonly bool MultiIedConnectionUxRegistered = RegisterMultiIedConnectionUx();
private Button? _selectedIedConnectButton;

private static bool RegisterMultiIedConnectionUx()
{
EventManager.RegisterClassHandler(
typeof(IoListTestingWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(MultiIedConnectionUx_Loaded));
return true;
}

private static void MultiIedConnectionUx_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not IoListTestingWindow window)
return;

window.Dispatcher.BeginInvoke(
new Action(window.InstallSelectedIedConnectButton),
DispatcherPriority.Loaded);
}

private void InstallSelectedIedConnectButton()
{
if (_selectedIedConnectButton != null ||
LogicalTreeHelper.GetParent(WorkspacePreviewToggle) is not Panel actionBar)
{
return;
}

var button = new Button
{
Style = FindResource("SoftButton") as Style,
Padding = new Thickness(11, 8, 11, 8),
Margin = new Thickness(0, 0, 6, 0),
ToolTip = "Connect or refresh the selected IED independently. Other IED connection workflows keep running."
};
button.SetBinding(
FrameworkElement.DataContextProperty,
new Binding(nameof(SelectedIed)) { Source = this });
button.SetBinding(
ContentControl.ContentProperty,
new Binding(nameof(IoTestIedPlan.ConnectionActionText)));
button.SetBinding(
UIElement.IsEnabledProperty,
new Binding(nameof(IoTestIedPlan.CanPrepareConnection)));
button.Click += ConnectIed_Click;

var previewIndex = actionBar.Children.IndexOf(WorkspacePreviewToggle);
actionBar.Children.Insert(Math.Max(0, previewIndex + 1), button);
_selectedIedConnectButton = button;
Closed += MultiIedConnectionUx_Closed;
}

private void MultiIedConnectionUx_Closed(object? sender, EventArgs e)
{
Closed -= MultiIedConnectionUx_Closed;
if (_selectedIedConnectButton == null)
return;

_selectedIedConnectButton.Click -= ConnectIed_Click;
_selectedIedConnectButton = null;
}
}
19 changes: 0 additions & 19 deletions IoListTestingWindow.PrintPreview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public partial class IoListTestingWindow
private Button? _printPreviewToggle;
private TextBlock? _printPreviewZoomText;
private TextBlock? _printPreviewPageText;
private DispatcherTimer? _preparationStateGuard;

protected override void OnContentRendered(EventArgs e)
{
Expand All @@ -31,7 +30,6 @@ protected override void OnContentRendered(EventArgs e)
return;

InstallPerIedPrintPreview();
InstallPreparationStateGuard();
PropertyChanged += PrintPreviewWindow_PropertyChanged;
Session.PropertyChanged += PrintPreviewSession_PropertyChanged;
Closed += PrintPreviewWindow_Closed;
Expand Down Expand Up @@ -441,22 +439,6 @@ private static DataTemplate CenteredTemplate(
return new DataTemplate { VisualTree = text };
}

private void InstallPreparationStateGuard()
{
_preparationStateGuard = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(180) };
_preparationStateGuard.Tick += (_, _) => ClearStalePreparationFlags();
_preparationStateGuard.Start();
}

private void ClearStalePreparationFlags()
{
foreach (var ied in Project.Ieds)
{
if (ied.IsPreparing && !ReferenceEquals(ied, _preparingIed))
ied.SetPreparationState(false, ied.LiveStatusText);
}
}

private void PrintPreviewWindow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_printPreviewActive && e.PropertyName == nameof(SelectedIed))
Expand All @@ -471,7 +453,6 @@ private void PrintPreviewSession_PropertyChanged(object? sender, PropertyChanged

private void PrintPreviewWindow_Closed(object? sender, EventArgs e)
{
_preparationStateGuard?.Stop();
PropertyChanged -= PrintPreviewWindow_PropertyChanged;
Session.PropertyChanged -= PrintPreviewSession_PropertyChanged;
Closed -= PrintPreviewWindow_Closed;
Expand Down
2 changes: 1 addition & 1 deletion IoListTestingWindow.RealPreparationProgress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private void PreparationProgressTimer_Tick(object? sender, EventArgs e)
_preparationDisplayStates[ied] = state;
}

var active = ied.IsPreparing || ReferenceEquals(_preparingIed, ied);
var active = ied.IsPreparing;
if (active && !state.WasActive)
state.Reset();

Expand Down
Loading
Loading