Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
75e41fb
feat: add operator timing semantics helpers
masarray Aug 9, 2026
97673d5
fix: separate BI timing from capture frame semantics
masarray Aug 9, 2026
238292b
feat: make frozen capture and timing rail explicit
masarray Aug 9, 2026
ececea6
ux: clarify relay LED live and latched semantics
masarray Aug 9, 2026
7c09182
fix: order closed-loop timeline by event timestamp
masarray Aug 9, 2026
f4d39d1
fix: separate relay and test-set timing events
masarray Aug 9, 2026
3bd09c4
feat: make closed-loop timing strip operator-visible
masarray Aug 9, 2026
d8b5865
fix: clarify native relay pickup and trip indicators
masarray Aug 9, 2026
3cb2586
fix: import WPF controls for frozen timing labels
masarray Aug 9, 2026
932029e
perf: keep ANY pickup check allocation-free
masarray Aug 9, 2026
23c1219
fix: scope timing tooltip to active closed-loop run
masarray Aug 9, 2026
b21b485
fix: clear prior timing tooltip on relay reset
masarray Aug 9, 2026
9e9bf9c
fix: make closed-loop relay reset a single settle transaction
masarray Aug 9, 2026
d8c7e75
fix: make relay reset wait for one-click re-arm postcondition
masarray Aug 9, 2026
775a416
fix: route P6 RESET through authoritative reset transaction
masarray Aug 9, 2026
5d4be17
test: require one-click reset to fully release desktop feedback
masarray Aug 9, 2026
5559a78
test: lock P6 reset to unified relay transaction
masarray Aug 9, 2026
7bd1582
test: keep alarm ACK separate from unified relay reset
masarray Aug 9, 2026
8ee5b4b
test: require P6 RESET to use unified equipment authority
masarray Aug 9, 2026
3f37c97
test: preserve evidence history through unified reset authority
masarray Aug 9, 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
104 changes: 104 additions & 0 deletions src/Arvrel.App/MainWindow.ClosedLoopOperatorClarity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Arvrel.App;

public partial class MainWindow
{
private const int ClosedLoopOperatorClarityMaximumAttempts = 24;
private bool _closedLoopTimingPanelReflowed;
private bool _closedLoopSetpointHeadersClarified;
private int _closedLoopOperatorClarityAttempts;

internal void InitializeClosedLoopOperatorClarity()
{
if (_closedLoopTimingPanelReflowed && _closedLoopSetpointHeadersClarified)
return;

if (!IsLoaded || _virtualInjectionView is null)
{
RetryClosedLoopOperatorClarity();
return;
}

if (!_closedLoopTimingPanelReflowed)
_closedLoopTimingPanelReflowed = TryReflowClosedLoopTimingPanel();
if (!_closedLoopSetpointHeadersClarified)
_closedLoopSetpointHeadersClarified = TryClarifyInjectionSetpointHeaders();

if (!_closedLoopTimingPanelReflowed || !_closedLoopSetpointHeadersClarified)
RetryClosedLoopOperatorClarity();
}

private void RetryClosedLoopOperatorClarity()
{
if (_closedLoopOperatorClarityAttempts++ >= ClosedLoopOperatorClarityMaximumAttempts)
return;

Dispatcher.BeginInvoke(
DispatcherPriority.ApplicationIdle,
new Action(InitializeClosedLoopOperatorClarity));
}

private bool TryReflowClosedLoopTimingPanel()
{
if (_virtualInjectionView is null || _testSetTimingPanel is null)
return false;

var footer = _virtualInjectionView.Children
.OfType<Grid>()
.FirstOrDefault(child => Grid.GetRow(child) == 2);
if (footer is null)
return false;

// The timing result is primary test-equipment information, not a narrow
// accessory beside CT controls. Give it one full row and keep source/CT
// actions on the lower row.
if (_virtualInjectionView.RowDefinitions.Count >= 3)
_virtualInjectionView.RowDefinitions[2].Height = new GridLength(92);

footer.RowDefinitions.Clear();
footer.RowDefinitions.Add(new RowDefinition { Height = new GridLength(51) });
footer.RowDefinitions.Add(new RowDefinition { Height = new GridLength(36) });

foreach (UIElement child in footer.Children.Cast<UIElement>().ToArray())
{
Grid.SetRowSpan(child, 1);
if (ReferenceEquals(child, _testSetTimingPanel))
continue;

Grid.SetRow(child, 1);
if (child is FrameworkElement element)
element.VerticalAlignment = VerticalAlignment.Center;
}

Grid.SetRow(_testSetTimingPanel, 0);
Grid.SetColumn(_testSetTimingPanel, 0);
Grid.SetColumnSpan(_testSetTimingPanel, Math.Max(1, footer.ColumnDefinitions.Count));
_testSetTimingPanel.Margin = new Thickness(0, 0, 0, 5);
_testSetTimingPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
_testSetTimingPanel.VerticalAlignment = VerticalAlignment.Stretch;

return true;
}

private bool TryClarifyInjectionSetpointHeaders()
{
if (_virtualInjectionView is null)
return false;

var table = _virtualInjectionView.Children
.OfType<DataGrid>()
.FirstOrDefault(child => Grid.GetRow(child) == 1);
if (table is null || table.Columns.Count < 4)
return false;

table.Columns[2].Header = "RMS SET";
table.Columns[3].Header = "ANGLE SET";
table.ToolTip =
"Configured source setpoints. After auto-stop these values are retained for repeatability; " +
"the OUTPUT OFF / FROZEN CAPTURE banner is the authority for actual output state.";
return true;
}
}
119 changes: 84 additions & 35 deletions src/Arvrel.App/MainWindow.ClosedLoopTestBench.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal void InitializeClosedLoopVirtualTestBench()
_timer.Tick += ClosedLoopTimer_Tick;

InstallClosedLoopEvidenceOverride();
InitializeClosedLoopOperatorClarity();
AddEvent("BACKPLANE", "Closed-loop active · causal relay ADC/DFT · 1 µs metrology clock · 10 kHz TESTSET BI");
EngineModeText.Text = SmvProcessBusController.IsAvailable
? "P0 METROLOGY · ARIEC61850 READY"
Expand All @@ -64,6 +65,8 @@ internal void StopClosedLoopVirtualTestBench()
_closedLoopEvidenceButton.Click += ExportVirtualInjectionEvidence_Click;
_closedLoopEvidenceButton = null;
}

StatusText.ToolTip = null;
_closedLoopBench = null;
}

Expand All @@ -74,19 +77,50 @@ private void ClosedLoopTimer_Tick(object? sender, EventArgs e)
if (_closedLoopBench is null)
return;

// Trip details belong to one completed test run only. As soon as a new
// run is armed/idle, discard the previous run's explanatory tooltip so
// unrelated status text can never inherit stale BI1/capture evidence.
if (_closedLoopBench.TestSetSnapshot.TimerState != VirtualTestSetTimerState.Completed &&
StatusText.ToolTip is not null)
{
StatusText.ToolTip = null;
}

if (_internalRunning)
{
var result = _closedLoopBench.Advance(TimeSpan.FromMilliseconds(40));
_snapshot = result.Protection;
ObserveTransitions(_snapshot);
ReportClosedLoopTestSetTransitions(result.TestSet);
// Advance one 250 µs relay quantum at a time from the WPF presentation
// slice. The bench still owns all timing authority; this only lets the
// desktop observe the exact first frame where generic relay pickup is
// asserted instead of trying to infer its source 40 ms later.
var remaining = TimeSpan.FromMilliseconds(40);
ClosedLoopVirtualTestBenchStep? result = null;
while (remaining > TimeSpan.Zero && _scenario.IsRunning)
{
var quantum = remaining > ClosedLoopBench.SimulationQuantum
? ClosedLoopBench.SimulationQuantum
: remaining;
result = _closedLoopBench.Advance(quantum);
remaining -= quantum;

_snapshot = result.Protection;
ObserveFirstAnyPickupSource(result.TestSet, result.Protection);
ObserveClosedLoopProtectionTransitions(result.TestSet, _snapshot);
ReportClosedLoopTestSetTransitions(result.TestSet);

if (!result.TestSet.OutputRunning)
break;
}

if (result is null)
return;

_internalRunning = _scenario.IsRunning;
var displayStep = _scenario.Project(result.Source, _pickupPosition, _tripPosition) with
{
Measurement = result.RelayMeasurement
};
RenderInternal(displayStep, _snapshot);
RefreshClosedLoopOperatorState(result.TestSet, _snapshot);

if (!_internalRunning)
{
Expand All @@ -97,6 +131,9 @@ private void ClosedLoopTimer_Tick(object? sender, EventArgs e)
return;
}

if (StatusText.ToolTip is not null)
StatusText.ToolTip = null;

_streamRefreshDivider++;
if (_streamRefreshDivider >= 6)
{
Expand All @@ -112,41 +149,26 @@ private void ReportClosedLoopTestSetTransitions(VirtualTestSetTimingSnapshot tes
{
_lastReportedTestSetPickup = pickup;
AddEvent(
"TEST PICKUP",
$"BI2 ANY PICKUP ↑ · START→BI2 {testSet.PickupTime?.TotalMilliseconds:0.000} ms · resolution {testSet.TimingResolutionMicroseconds} µs");
"TESTSET BI2",
$"{testSet.PickupTime?.TotalMilliseconds:0.000} ms · ACCEPT · from RELAY ANY [{FirstAnyPickupSourceFor(testSet)}]");
}

if (testSet.TripDetectedAt is not { } trip || trip == _lastReportedTestSetTrip)
return;

_lastReportedTestSetTrip = trip;
var pickupText = testSet.PickupTime is { } pickupTime
? $"BI2 ANY {pickupTime.TotalMilliseconds:0.000} ms"
: "BI2 ANY —";
var tripText = testSet.TripTime is { } tripTime
? $"BI1 {tripTime.TotalMilliseconds:0.000} ms"
: "BI1 —";

var operationTiming = RelayOperationTimingCorrelator.Correlate(testSet, _snapshot);
var relayText = operationTiming is { } timing
? timing.LiveTripRequestFromStart is { } liveTrip
? $"{timing.Element} pickup {timing.ElementPickupFromStart.TotalMilliseconds:0.000} ms · " +
$"P→T {timing.ElementPickupToTrip.TotalMilliseconds:0.000} ms · " +
$"relay START→TRIP {liveTrip.TotalMilliseconds:0.000} ms"
: $"{timing.Element} pickup {timing.ElementPickupFromStart.TotalMilliseconds:0.000} ms · " +
$"P→T {timing.ElementPickupToTrip.TotalMilliseconds:0.000} ms"
: testSet.RelayTripTime is { } relayTrip
? $"relay START→TRIP {relayTrip.TotalMilliseconds:0.000} ms · operated-element pickup unavailable"
: "relay live timing unavailable";
var outputPath = testSet.RelayTripToBi1 is { } external
? $" · relay TRIP→BI1 {external.TotalMilliseconds:0.000} ms"
: string.Empty;
var rail = BuildClosedLoopTimingRail(testSet, _snapshot);
var detail = BuildClosedLoopTimingDetail(testSet, _snapshot);

AddEvent(
"TEST TRIP",
$"BI1 ↑ · START→BI1 {testSet.TripTime?.TotalMilliseconds:0.000} ms · {testSet.TimingResolutionMicroseconds} µs resolution · output stopped");
StatusText.Text =
$"TESTSET measured trip · {tripText} · {pickupText} · {relayText}{outputPath} · output stopped · capture frozen at BI1 edge.";
"TESTSET BI1",
$"{testSet.TripTime?.TotalMilliseconds:0.000} ms · ACCEPT · OUTPUT OFF · {testSet.TimingResolutionMicroseconds} µs resolution");

StatusText.Text = rail;
StatusText.ToolTip =
$"{detail}\n" +
"TESTSET measurement authority: accepted wired BI1 edge.\n" +
"Frozen waveform/phasor: first relay processing frame after that accepted edge, not an interpolated frame at the exact BI sample instant.";
Comment thread
masarray marked this conversation as resolved.
}

private void InstallClosedLoopEvidenceOverride()
Expand Down Expand Up @@ -179,12 +201,30 @@ private void ExportClosedLoopEvidence_Click(object sender, RoutedEventArgs e)

var current = _closedLoopBench.Advance(TimeSpan.Zero);
var algorithmRuntime = AlgorithmRuntimeRegistry.Snapshot();
var testSetSnapshot = _closedLoopBench.TestSetSnapshot;
var operatingElementTiming = RelayOperationTimingCorrelator.Correlate(
_closedLoopBench.TestSetSnapshot,
testSetSnapshot,
_snapshot);
var tripCapture = _closedLoopBench.TripCapture;
long? captureFrameOffsetUs = TryGetTripCaptureFrameOffsetMicroseconds(tripCapture, out var offsetUs)
? offsetUs
: null;
object? tripCaptureTiming = tripCapture is null
? null
: new
{
measurementAuthority = "TESTSET.BI1 accepted wired rising edge",
bi1AcceptedAt = tripCapture.TestSet.TripDetectedAt,
bi1AcceptedMicroseconds = tripCapture.TestSet.TripDetectedMicroseconds,
captureFrameAt = tripCapture.CapturedAt,
captureFrameMicroseconds = tripCapture.TestSet.ObservedMicroseconds,
captureFrameOffsetMicroseconds = captureFrameOffsetUs,
displayFreezeSemantics = "Frozen waveform/phasor are the first relay processing frame in which the accepted BI1 edge is observable; they are not claimed to be an interpolated state at the exact 10 kHz BI sampling instant."
};

var evidence = new
{
schemaVersion = 8,
schemaVersion = 9,
exportedAt = DateTimeOffset.Now,
application = "ARVREL",
operatingMode = OperatingModeCombo.SelectedIndex == 1 ? "Research" : "Practitioner",
Expand Down Expand Up @@ -218,9 +258,17 @@ private void ExportClosedLoopEvidence_Click(object sender, RoutedEventArgs e)
legacyRelayFrontEnd = _closedLoopBench.FrontEndSnapshot,
causalRelayFrontEnd = _closedLoopBench.CausalFrontEndSnapshot,
relayContactProfile = _closedLoopBench.ContactProfile,
testSet = _closedLoopBench.TestSetSnapshot,
testSet = testSetSnapshot,
firstAnyPickup = new
{
source = FirstAnyPickupSourceFor(testSetSnapshot),
relayAssertFromStart = testSetSnapshot.RelayPickupTime,
testSetBi2FromStart = testSetSnapshot.PickupTime,
semantics = "Source is captured on the first 4 kHz relay frame that asserts generic ANY-PICKUP/BO2; BI2 is the later wired test-set acceptance."
},
operatingElementTiming,
tripCapture = _closedLoopBench.TripCapture,
tripCapture,
tripCaptureTiming,
relayMeasurement = current.RelayMeasurement,
protection = _snapshot,
protectionSettings = _settings,
Expand All @@ -234,6 +282,7 @@ private void ExportClosedLoopEvidence_Click(object sender, RoutedEventArgs e)
dialog.FileName,
JsonSerializer.Serialize(evidence, new JsonSerializerOptions { WriteIndented = true }));
AddEvent("EXPORT", System.IO.Path.GetFileName(dialog.FileName));
StatusText.ToolTip = null;
StatusText.Text = $"Closed-loop metrology evidence exported to {dialog.FileName}.";
}
}
Expand Down
Loading
Loading