Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b459f85
Static DataSet: select exact authoritative membership rows
masarray Sep 4, 2026
235e0b8
Static DataSet: pin selection to exact ARIEC memberships
masarray Sep 4, 2026
c637737
test: pin exact Static DataSet authority without MMS fallback
masarray Sep 4, 2026
3c0e0c8
Static DataSet: reconstruct structured report members without MMS
masarray Sep 4, 2026
0a0a4b9
Test schema-safe Static DataSet report aggregation
masarray Sep 4, 2026
2c9a750
Stage reviewed Static DataSet runtime patch 1/5
masarray Sep 4, 2026
939d4f1
Stage reviewed Static DataSet runtime patch 2/5
masarray Sep 4, 2026
f2b357d
Stage reviewed Static DataSet runtime patch 3/5
masarray Sep 4, 2026
f2a35af
Stage reviewed Static DataSet runtime patch 4/5
masarray Sep 4, 2026
7540df9
Stage reviewed Static DataSet runtime patch 5/5
masarray Sep 4, 2026
0142f39
Apply reviewed Static DataSet runtime wiring
masarray Sep 4, 2026
315adae
Normalize runtime patch line endings before apply
masarray Sep 4, 2026
c527b1c
Static DataSet: wire schema-safe report projection
github-actions[bot] Sep 4, 2026
e82f21c
Document Static DataSet report-only acceptance contract
masarray Sep 4, 2026
20b1043
Avoid per-keystroke IP wizard UI churn
masarray Sep 4, 2026
b6e6b2a
Preserve report q and timestamp across semantic value projection
masarray Sep 4, 2026
39a91b3
Add regressions for report q/t carry-forward and IP editing responsiv…
masarray Sep 4, 2026
747df48
Pin ARSAS to exact semantic report schema fix
masarray Sep 4, 2026
b7a06fa
Advance pinned semantic report engine test fix
masarray Sep 4, 2026
b6f9ddd
Keep immutable engine lock metadata compatible with CI
masarray Sep 4, 2026
9379a16
Make SNTP a global ARSAS server toggle
masarray Sep 4, 2026
7485d56
Run SNTP lifecycle globally across connected IEDs
masarray Sep 4, 2026
86a6355
Make FAT SNTP status passive under global server
masarray Sep 4, 2026
e94e50d
Guard global SNTP lifecycle and UI contract
masarray Sep 4, 2026
62a8274
Preserve explicit Static DataSet authority across FAT
masarray Sep 4, 2026
f5c5f7f
Keep FAT on shared Static DataSet runtime
masarray Sep 4, 2026
1964c64
Guard Static DataSet authority across FAT mode
masarray Sep 4, 2026
867fd0a
Make FAT consume shared SCL acquisition session
masarray Sep 4, 2026
25dc3c9
Guard shared SCL acquisition ownership across FAT
masarray Sep 4, 2026
c1bbbf4
Add causal Static DataSet RCB feasibility evidence
masarray Sep 4, 2026
5820894
Audit Static DataSet report feasibility from initial selection
masarray Sep 4, 2026
9e14e61
Align FAT P0 regression with shared acquisition ownership
masarray Sep 4, 2026
5374970
Treat FAT live rows as shared-session bindings
masarray Sep 4, 2026
577ac4b
Make IO FAT UI contract require shared SCL acquisition ownership
masarray Sep 4, 2026
cfe8fa1
Require cached FAT preparation to preserve shared monitor ownership
masarray Sep 4, 2026
dadc5ce
P0: keep IEC61850 lifecycle work off WPF dispatcher
masarray Sep 5, 2026
30d218c
P0: make FAT close journal sealing and save non-blocking
masarray Sep 5, 2026
00de5f3
test: lock P0 responsiveness and TotPF report-only regressions
masarray Sep 5, 2026
90c4cca
fix: install FAT P0 close guard without duplicate lifecycle override
masarray Sep 5, 2026
0468429
P0: make stop preempt hung IEC61850 lifecycle work
masarray Sep 5, 2026
f681896
P0: keep FAT state transitions on dispatcher, offload persistence only
masarray Sep 5, 2026
97a7e31
test: guard preemptive IEC lifecycle and WPF thread ownership
masarray Sep 5, 2026
0a7c619
P0: prevent reconnect racing preemptive IEC teardown
masarray Sep 5, 2026
b310112
Static DataSet: add deterministic configured-RCB report path
masarray Sep 5, 2026
2dd6fbc
test: lock deterministic Static DataSet reporting path
masarray Sep 5, 2026
7181d28
chore: stage deterministic Static DataSet route patch
masarray Sep 5, 2026
86a7593
Static DataSet: bypass adaptive Hybrid planner
github-actions[bot] Sep 5, 2026
14999f1
docs: lock deterministic configured-RCB Static path
masarray Sep 5, 2026
d903c85
chore: stage Static mode registry call fix
masarray Sep 5, 2026
f5cfca7
fix: use device identity for Static mode registry
github-actions[bot] Sep 5, 2026
0f7e660
test: bind Static mode route to device identity
masarray Sep 5, 2026
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
110 changes: 42 additions & 68 deletions IoListTestingWindow.ClockSyncUx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ namespace ArIED61850Tester;

public partial class IoListTestingWindow
{
private CheckBox? _clockSyncCheckBox;
private TextBlock? _clockSyncGlobalStatusText;
private TextBlock? _clockSyncEvidenceText;
private MainWindow? _clockSyncSnapshotOwner;
private bool _clockSyncCheckBoxRefreshing;

protected override void OnInitialized(EventArgs e)
{
Expand All @@ -21,9 +20,8 @@ protected override void OnInitialized(EventArgs e)

private void ClockSyncUx_Loaded(object sender, RoutedEventArgs e)
{
if (_clockSyncCheckBox != null)
if (_clockSyncGlobalStatusText != null)
{
RefreshClockSyncCheckBox();
AttachClockSyncSnapshotOwner();
return;
}
Expand All @@ -35,18 +33,17 @@ private void ClockSyncUx_Loaded(object sender, RoutedEventArgs e)
if (previewIndex < 0)
return;

var checkBox = new CheckBox
var globalStatus = new TextBlock
{
Name = "ClockSyncEnabledCheckBox",
Content = "Clock Sync",
Name = "GlobalSntpStatusTextBlock",
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
Padding = new Thickness(2, 0, 2, 0),
FontSize = 11.2,
FontWeight = FontWeights.SemiBold,
Foreground = TryFindResource("Ink") as Brush ?? Brushes.DimGray,
Focusable = false,
ToolTip = "SNTP laptop → IED. Checked: ARSAS serves laptop time using normal UDP/123 when available, with an Npcap RAW fallback when Windows already owns UDP/123. Unchecked: ARSAS stops only its Clock Sync service. IEC 61850 remains unaffected."
Text = "Global SNTP",
ToolTip = "Global SNTP is controlled from the ARSAS header. It is not owned by FAT and continues running when the FAT window closes while the global server toggle remains enabled."
};

var evidence = new TextBlock
Expand All @@ -57,15 +54,12 @@ private void ClockSyncUx_Loaded(object sender, RoutedEventArgs e)
FontSize = 10.4,
FontWeight = FontWeights.Medium,
Foreground = TryFindResource("MutedInk") as Brush ?? Brushes.SlateGray,
Text = "Clock: waiting"
Text = "SNTP: waiting"
};

_clockSyncCheckBox = checkBox;
_clockSyncGlobalStatusText = globalStatus;
_clockSyncEvidenceText = evidence;
RefreshClockSyncCheckBox();
checkBox.Checked += ClockSyncCheckBox_Changed;
checkBox.Unchecked += ClockSyncCheckBox_Changed;
actionPanel.Children.Insert(previewIndex + 1, checkBox);
actionPanel.Children.Insert(previewIndex + 1, globalStatus);
actionPanel.Children.Insert(previewIndex + 2, evidence);
AttachClockSyncSnapshotOwner();
}
Expand Down Expand Up @@ -100,28 +94,40 @@ private void ClockSyncSnapshotChanged(SntpClockServiceSnapshot snapshot)

private void RefreshClockSyncEvidence(SntpClockServiceSnapshot snapshot)
{
if (_clockSyncEvidenceText == null)
if (_clockSyncEvidenceText == null || _clockSyncGlobalStatusText == null)
return;

var enabled = _clockSyncSnapshotOwner?.IsClockSyncEnabled == true;
var transport = snapshot.TransportMode switch
{
SntpClockTransportMode.NpcapRaw => "RAW",
SntpClockTransportMode.UdpSocket => "UDP",
_ => "—"
};

_clockSyncEvidenceText.Text = snapshot.State switch
{
SntpClockServiceState.Serving =>
$"{transport} · B {snapshot.BroadcastCount} · Req {snapshot.ClientRequestCount} · Reply {snapshot.ReplyCount} · sync not proven",
SntpClockServiceState.Starting => "Clock: starting…",
SntpClockServiceState.Stopped => "Clock: off",
SntpClockServiceState.PortUnavailable => "Clock: unavailable",
SntpClockServiceState.Faulted => "Clock: fault",
_ => $"Clock: {snapshot.State}"
};

_clockSyncEvidenceText.ToolTip = BuildClockSyncEvidenceToolTip(snapshot, transport);
var localAddress = snapshot.Binding?.LocalAddress.ToString();

_clockSyncGlobalStatusText.Text = !enabled
? "Global SNTP · Off"
: snapshot.State == SntpClockServiceState.Serving
? $"Global SNTP · {localAddress ?? "Active"}"
: "Global SNTP · Enabled";

_clockSyncEvidenceText.Text = !enabled
? "SNTP: off"
: snapshot.State switch
{
SntpClockServiceState.Serving =>
$"{transport} · B {snapshot.BroadcastCount} · Req {snapshot.ClientRequestCount} · Reply {snapshot.ReplyCount} · sync not proven",
SntpClockServiceState.Starting => "SNTP: starting…",
SntpClockServiceState.Stopped => "SNTP: waiting for connected IED",
SntpClockServiceState.PortUnavailable => "SNTP: unavailable",
SntpClockServiceState.Faulted => "SNTP: fault",
_ => $"SNTP: {snapshot.State}"
};

var toolTip = BuildClockSyncEvidenceToolTip(snapshot, transport, enabled);
_clockSyncGlobalStatusText.ToolTip = toolTip;
_clockSyncEvidenceText.ToolTip = toolTip;
_clockSyncEvidenceText.Foreground = snapshot.State switch
{
SntpClockServiceState.PortUnavailable or SntpClockServiceState.Faulted => Brushes.DarkOrange,
Expand All @@ -130,60 +136,28 @@ private void RefreshClockSyncEvidence(SntpClockServiceSnapshot snapshot)
};
}

private static string BuildClockSyncEvidenceToolTip(SntpClockServiceSnapshot snapshot, string transport)
private static string BuildClockSyncEvidenceToolTip(
SntpClockServiceSnapshot snapshot,
string transport,
bool enabled)
{
var binding = snapshot.Binding == null ? "—" : snapshot.Binding.Summary;
var lastBroadcast = snapshot.LastBroadcastUtc?.ToLocalTime().ToString("HH:mm:ss.fff") ?? "—";
var lastRequest = snapshot.LastRequestUtc?.ToLocalTime().ToString("HH:mm:ss.fff") ?? "—";
var lastReply = snapshot.LastReplyUtc?.ToLocalTime().ToString("HH:mm:ss.fff") ?? "—";
return $"Clock Sync evidence\n" +
return $"Global ARSAS SNTP Server\n" +
$"Enabled: {enabled}\n" +
$"State: {snapshot.State}\n" +
$"Transport: {transport}\n" +
$"Binding: {binding}\n" +
$"Broadcast sent: {snapshot.BroadcastCount} (last {lastBroadcast})\n" +
$"Client request seen: {snapshot.ClientRequestCount} (last {lastRequest})\n" +
$"Mode 4 reply sent: {snapshot.ReplyCount} (last {lastReply})\n\n" +
"The global server is controlled from the ARSAS header and continues outside FAT while enabled. " +
"These counters prove packet activity only. ARSAS does not claim that the relay clock is synchronized without device-side evidence.\n\n" +
snapshot.Detail;
}

private void RefreshClockSyncCheckBox()
{
if (_clockSyncCheckBox == null || Owner is not MainWindow mainWindow)
return;

_clockSyncCheckBoxRefreshing = true;
try
{
_clockSyncCheckBox.IsChecked = mainWindow.IsClockSyncEnabled;
}
finally
{
_clockSyncCheckBoxRefreshing = false;
}
}

private async void ClockSyncCheckBox_Changed(object sender, RoutedEventArgs e)
{
if (_clockSyncCheckBoxRefreshing ||
_clockSyncCheckBox == null ||
Owner is not MainWindow mainWindow)
return;

var requested = _clockSyncCheckBox.IsChecked == true;
_clockSyncCheckBox.IsEnabled = false;
try
{
await mainWindow.SetClockSyncEnabledAsync(requested);
RefreshClockSyncCheckBox();
RefreshClockSyncEvidence(mainWindow.ClockSyncSnapshot);
}
finally
{
_clockSyncCheckBox.IsEnabled = true;
}
}

private void ClockSyncUx_Closed(object? sender, EventArgs e)
{
if (_clockSyncSnapshotOwner != null)
Expand Down
148 changes: 148 additions & 0 deletions IoListTestingWindow.P0Lifecycle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.ComponentModel;
using System.IO;
using System.Windows;

namespace ArIED61850Tester;

/// <summary>
/// P0 responsiveness guard for the FAT workspace lifecycle.
///
/// The legacy Closing handler flushed workspace persistence synchronously on the WPF
/// Dispatcher. Keep confirmation and evidence-session state transitions on the Dispatcher
/// (the session controllers publish UI-bound PropertyChanged notifications there), then move
/// only the durable workspace save to a worker thread. Native IEC 61850 teardown is handled
/// independently by the responsive runtime facade.
/// </summary>
public partial class IoListTestingWindow
{
private static readonly bool P0LifecycleRegistered = RegisterP0Lifecycle();

private bool _p0LifecycleInstalled;
private bool _p0CloseInProgress;
private bool _p0AllowClose;

private static bool RegisterP0Lifecycle()
{
// Avoid another Window lifecycle override in this already heavily split partial class.
// Loaded is a routed event, so one class handler can install the P0 Closing handler on
// every FAT workspace instance after XAML has wired its legacy Window_Closing handler.
EventManager.RegisterClassHandler(
typeof(IoListTestingWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(P0Lifecycle_Loaded));
return true;
}

private static void P0Lifecycle_Loaded(object sender, RoutedEventArgs e)
{
if (sender is IoListTestingWindow window)
window.InstallP0Lifecycle();
}

private void InstallP0Lifecycle()
{
if (_p0LifecycleInstalled)
return;

_p0LifecycleInstalled = true;

// XAML wires Window_Closing during InitializeComponent. Replace only that lifecycle
// edge; all existing OnClosed cleanup remains intact.
Closing -= Window_Closing;
Closing += P0Window_Closing;
}

private async void P0Window_Closing(object? sender, CancelEventArgs e)
{
if (_p0AllowClose)
return;

e.Cancel = true;
if (_p0CloseInProgress)
return;

if (IsPreparingIed)
{
var activeNames = string.Join(", ", Project.Ieds
.Where(ied => ied.IsPreparing)
.Select(ied => ied.IedName));
MessageBox.Show(
this,
$"ARSAS is still preparing {activeNames}. You can inspect or connect other IEDs while these independent workflows run, but finish preparation before closing this workspace.",
"IED preparation in progress",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}

if (Session.HasActiveSessions)
{
var activeCount = Session.ActiveSessionCount;
var answer = MessageBox.Show(
this,
$"{activeCount} FAT evidence session(s) are active. Returning to Engineering will stop every active IED session, seal each independent evidence journal, and save the current project progress.\n\nStop all sessions and return?",
"Stop active FAT sessions",
MessageBoxButton.YesNo,
MessageBoxImage.Warning,
MessageBoxResult.No);
if (answer != MessageBoxResult.Yes)
return;
}

_p0CloseInProgress = true;
IsEnabled = false;

try
{
if (Session.HasActiveSessions)
{
// Session.StopAll mutates controller/project state and raises UI-bound
// PropertyChanged notifications. Keep that state transition on Dispatcher;
// offloading the whole coordinator would create a cross-thread WPF defect.
var stopAll = Session.StopAll(
"Workspace closed by operator; per-IED evidence journal sealed.");
if (!stopAll.Succeeded)
{
MessageBox.Show(
this,
stopAll.Message,
"Evidence journals could not be sealed",
MessageBoxButton.OK,
MessageBoxImage.Error);
return;
}
}

// Persistence is pure durable I/O after the session state is sealed and is the
// portion that must not occupy the WPF Dispatcher.
if (Storage != null)
await Task.Run(Storage.SaveNow);

_p0AllowClose = true;
Close();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException)
{
var answer = MessageBox.Show(
this,
$"ARSAS could not save the latest IO FAT progress.\n\n{ex.Message}\n\nClose the workspace anyway?",
"Progress save failed",
MessageBoxButton.YesNo,
MessageBoxImage.Error,
MessageBoxResult.No);
if (answer == MessageBoxResult.Yes)
{
_p0AllowClose = true;
Close();
}
}
finally
{
if (!_p0AllowClose)
{
_p0CloseInProgress = false;
IsEnabled = true;
}
}
}
}
4 changes: 2 additions & 2 deletions IpConnectWizardWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@

<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Text="IED Runtime IP Address" FontSize="12.5" FontWeight="SemiBold" Foreground="{StaticResource Ink}" Margin="0,0,0,6"/>
<ComboBox x:Name="RelayIpBox" IsEditable="True" IsTextSearchEnabled="True"
<ComboBox x:Name="RelayIpBox" IsEditable="True" IsTextSearchEnabled="False"
ItemsSource="{Binding RecentRelayIps, RelativeSource={RelativeSource AncestorType=Window}}"
Text="{Binding RelayIpAddress, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=PropertyChanged}"/>
Text="{Binding RelayIpAddress, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=LostFocus}"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="2">
<TextBlock Text="MMS Port" FontSize="12.5" FontWeight="SemiBold" Foreground="{StaticResource Ink}" Margin="0,0,0,6"/>
Expand Down
Loading
Loading