Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3eb9d35
G2.5-A2.1 add command-bound high-speed read-only witness
masarray Aug 21, 2026
7d4e1a9
G2.5-A2.1 add command-bound witness evidence window
masarray Aug 21, 2026
1b75439
G2.5-A2.1 add explicit command-bound witness UI
masarray Aug 21, 2026
c3e83c8
G2.5-A2.1 add command-bound witness regressions
masarray Aug 21, 2026
945d87c
G2.5-A2.1 avoid partial result helper collisions
masarray Aug 21, 2026
f3100b3
G2.5-A2.1 install explicit witness hotkey behavior
masarray Aug 21, 2026
e9c35d1
G2.5-A2.1 observe command intent without touching control path
masarray Aug 21, 2026
ee0359a
G2.5-A2.1 expose read-only control dialog context
masarray Aug 21, 2026
0534d90
G2.5-A2.1 capture both ARSAS control UI paths
masarray Aug 21, 2026
7468285
G2.5-A2.1 observe ControlCommandWindow without control changes
masarray Aug 21, 2026
487e970
G2.5-A2.1 regress dedicated control dialog observer path
masarray Aug 21, 2026
6b01bd7
G2.5-A2.1 clarify both safe ARSAS control UI paths
masarray Aug 21, 2026
0219b9a
G2.5-A2.1 keep observer adapter source-only
masarray Aug 21, 2026
e0e0bc0
G2.5-A2.1 V3 expose read-only runtime observer surface
masarray Aug 24, 2026
d4f263b
G2.5-A2.1 V3 capture command from existing runtime diagnostics
masarray Aug 24, 2026
9d01df4
G2.5-A2.1 V3 use runtime diagnostic command authority
masarray Aug 24, 2026
1d9e348
G2.5-A2.1 V3 regress runtime diagnostic command capture
masarray Aug 24, 2026
6064b6a
G2.5-A2.1 V3 update observer regression authority
masarray Aug 24, 2026
e386f48
G2.5-A2.1 V3 align UI regression wording
masarray Aug 24, 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
1 change: 1 addition & 0 deletions App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ protected override void OnStartup(StartupEventArgs e)
// isolated dynamic-DataSet qualification; it is never invoked by normal startup,
// Connect/Play, monitoring, reconnect or report-planner paths.
DynamicReportQualificationUiBehavior.Install();
DynamicReportCommandBoundWitnessUiBehavior.Install();
DispatcherUnhandledException += OnDispatcherUnhandledException;
TaskScheduler.UnobservedTaskException += (_, args) => args.SetObserved();

Expand Down
13 changes: 13 additions & 0 deletions ControlCommandWindow.A21Witness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ArIED61850Tester.Models;

namespace ArIED61850Tester;

/// <summary>
/// Read-only A2.1 adapter. This partial exposes context only; it adds no command
/// behavior and leaves the existing IEC 61850 control transaction completely untouched.
/// </summary>
public partial class ControlCommandWindow
{
internal SignalDefinition A21WitnessSignal => _signal;
internal Iec61850MonitorDevice A21WitnessDevice => _device;
}
134 changes: 134 additions & 0 deletions DynamicReportCommandBoundWitnessUiBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

internal static class DynamicReportCommandBoundWitnessUiBehavior
{
private static int _installed;
private static int _busy;

public static void Install()
{
if (Interlocked.Exchange(ref _installed, 1) != 0)
return;

EventManager.RegisterClassHandler(
typeof(MainWindow),
Keyboard.PreviewKeyDownEvent,
new KeyEventHandler(OnPreviewKeyDown),
handledEventsToo: true);

// Legacy observer-only fallback for the dedicated ControlCommandWindow path.
// V3 command authority is the already-existing runtime Diagnostic event; this
// routed observer is retained only as non-authoritative fallback evidence.
EventManager.RegisterClassHandler(
typeof(Button),
Button.ClickEvent,
new RoutedEventHandler(OnAnyButtonClick),
handledEventsToo: true);
}

private static void OnAnyButtonClick(object sender, RoutedEventArgs e)
{
if (sender is not Button button || Window.GetWindow(button) is not ControlCommandWindow commandWindow)
return;

var label = button.Content?.ToString()?.Trim() ?? string.Empty;
if (!label.Equals("Send Command", StringComparison.OrdinalIgnoreCase) &&
!label.Equals("Send Test", StringComparison.OrdinalIgnoreCase))
return;

if (!commandWindow.CanSend)
return;

DynamicReportCommandIntentObservation.Publish(new DynamicReportObservedCommandIntent(
commandWindow.A21WitnessDevice,
commandWindow.A21WitnessSignal,
commandWindow.SelectedValue,
"ControlCommandWindow.RoutedButtonClick",
DateTimeOffset.UtcNow));
}

private static async void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (sender is not MainWindow window ||
Keyboard.Modifiers != (ModifierKeys.Control | ModifierKeys.Shift) ||
e.Key != Key.F)
return;

e.Handled = true;
var device = window.SelectedDevice;
if (device is null)
{
MessageBox.Show(
window,
"Select one IEC 61850 IED first. G2.5-A2.1 is intentionally bound to one explicit IED and one explicit ARSAS command.",
"G2.5-A2.1 Command-Bound Witness",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}

if (Interlocked.Exchange(ref _busy, 1) != 0)
{
MessageBox.Show(
window,
"G2.5-A2.1 is already armed/running.",
"G2.5-A2.1 Command-Bound Witness",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
}

try
{
var answer = MessageBox.Show(
window,
$"Arm G2.5-A2.1 V3 command-bound high-speed stimulus witness for {device.Name} ({device.EndpointText})?\n\n" +
"READ-ONLY MMS WITNESS + ONE EXISTING ARSAS CONTROL COMMAND\n\n" +
"V3 captures the exact command from the ALREADY-EXISTING Iec61850MonitorRuntime Diagnostic event 'Control execution requested:' that is emitted before native control execution. It does not add a hook to the SBOw/Operate transaction.\n\n" +
"After the status shows 'G2.5-A2.1 READY — ISSUE ONE ARSAS COMMAND', issue exactly ONE already-proven safe OPEN or CLOSE using the normal ARSAS control UI you normally use. Do NOT use an external/manual stimulus for this phase.\n\n" +
"Once the runtime diagnostic identifies the exact control object, the isolated MMS witness narrows read-only sampling to at most six points around the exact ControlStatusReference. Existing ExecuteControlAsync, SBOw, Operate and CommandTermination behavior is NOT modified, delayed, wrapped or re-issued.\n\n" +
"Once 'G2.5-A2.1 COMMAND CAPTURED' appears, do not issue another command. If a transition is seen, A2.1 samples briefly to classify persistent/latched versus momentary/pulse behavior.\n\n" +
"The witness does not access/mutate RCB or DataSet state, does not send GI, does not save/advance the qualification profile, and production dynamic reporting remains OFF. Do not run another G2 hotkey while A2.1 is armed.\n\n" +
"Continue?",
"G2.5-A2.1 V3 Runtime-Diagnostic Witness",
MessageBoxButton.YesNo,
MessageBoxImage.Warning,
MessageBoxResult.No);
if (answer != MessageBoxResult.Yes)
return;

window.LastStatusText = $"G2.5-A2.1 V3: opening isolated read-only MMS witness for {device.Name} and preparing pre-command baseline…";
var progress = new Progress<string>(text => window.LastStatusText = text);
var service = new DynamicReportCommandBoundStimulusWitnessServiceV3();
var result = await service.RunAsync(
window.A21WitnessRuntime,
device,
device.Signals.ToArray(),
progress,
CancellationToken.None);
window.LastStatusText = result.Summary;
var evidenceWindow = new DynamicReportQualificationResultWindow(result) { Owner = window };
evidenceWindow.ShowDialog();
}
catch (Exception ex)
{
window.LastStatusText = "G2.5-A2.1 V3 stopped locally; production dynamic reporting remains OFF.";
MessageBox.Show(
window,
"G2.5-A2.1 V3 stopped. The witness did not change production reporting policy.\n\n" + ex,
"G2.5-A2.1 V3 Runtime-Diagnostic Witness",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
finally
{
Interlocked.Exchange(ref _busy, 0);
}
}
}
114 changes: 114 additions & 0 deletions DynamicReportQualificationResultWindow.G25A21.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using System.Globalization;
using System.Text;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

internal partial class DynamicReportQualificationResultWindow
{
internal DynamicReportQualificationResultWindow(DynamicReportCommandBoundStimulusWitnessResult result)
{
ArgumentNullException.ThrowIfNull(result);
InitializeComponent();

Title = "G2.5-A2.1 Command-Bound Stimulus Witness Evidence";
HeaderText.Text = "G2.5-A2.1 Command-Bound High-Speed Witness";
SummaryText.Text = result.Summary;
StateText.Text = result.IsSuccess
? "Command-Bound Transition Proven"
: result.IsBlocked
? "Blocked"
: "Command-Bound Transition Not Proven";
EvidenceTextBox.Text = BuildG25A21Evidence(result);

if (result.IsSuccess)
SetPassBadge();
}

private static string BuildG25A21Evidence(DynamicReportCommandBoundStimulusWitnessResult result)
{
var builder = new StringBuilder();
builder.AppendLine("ARSAS G2.5-A2.1 COMMAND-BOUND HIGH-SPEED STIMULUS WITNESS EVIDENCE");
builder.AppendLine(new string('=', 92));
builder.AppendLine($"Result: {result.Summary}");
builder.AppendLine($"Blocked: {result.IsBlocked}");
builder.AppendLine($"G2.5-A2.1 success: {result.IsSuccess}");
builder.AppendLine($"Stimulus witness proven: {result.StimulusWitnessProven}");

if (result.Identity is not null)
{
builder.AppendLine();
builder.AppendLine("IED IDENTITY");
builder.AppendLine($"Stable identity: {result.Identity.StableIdentityKey}");
builder.AppendLine($"Model fingerprint: {result.Identity.ModelFingerprint}");
builder.AppendLine($"Model: {result.Identity.Model}");
builder.AppendLine($"Firmware: {result.Identity.FirmwareRevision}");
builder.AppendLine($"Profile revision: {result.Identity.ProfileRevision}");
}

builder.AppendLine();
builder.AppendLine("COMMAND BINDING");
builder.AppendLine($"Input profile state: {result.InputProfile?.State.ToString() ?? "-"}");
builder.AppendLine($"Pre-command baseline captured: {result.BaselineCaptured}");
builder.AppendLine($"Command captured: {result.CommandCaptured}");
builder.AppendLine($"Command signal: {G25A21TextOrDash(result.CommandSignalReference)}");
builder.AppendLine($"ControlStatusReference: {G25A21TextOrDash(result.ControlStatusReference)}");
builder.AppendLine($"Control model: {G25A21TextOrDash(result.ControlModelText)}");
builder.AppendLine($"Pre-command baseline points: {result.PreCommandBaselineCount}");
builder.AppendLine($"Focused candidates: {result.FocusCandidateCount}");
builder.AppendLine($"Sample cycles: {result.SampleCycles}");
builder.AppendLine($"Read failures: {result.ReadFailures}");
builder.AppendLine($"Association healthy: {result.AssociationHealthy}");

builder.AppendLine();
builder.AppendLine("ELIGIBLE COMMAND-BOUND CANDIDATES");
if (result.EligibleCandidates.Count == 0)
{
builder.AppendLine(" none");
}
else
{
foreach (var candidate in result.EligibleCandidates)
{
builder.AppendLine($" #{candidate.Rank} {candidate.Reference}");
builder.AppendLine($" MMS: {candidate.MmsReference}");
builder.AppendLine($" Exact ControlStatusReference: {candidate.ExactControlStatus}");
builder.AppendLine($" Kind: {candidate.Kind}");
builder.AppendLine($" Baseline -> final: {candidate.BaselineValue} -> {candidate.FinalValue}");
builder.AppendLine($" Transitions: {candidate.TransitionCount}");
builder.AppendLine($" Observed active/pulse duration ms: {G25A21FormatDuration(candidate.ObservedActiveMilliseconds)}");
foreach (var transition in candidate.Transitions)
builder.AppendLine($" {transition.ObservedAtUtc:O} | {transition.BeforeValue} -> {transition.AfterValue}");
}
}

builder.AppendLine();
builder.AppendLine("ALL FOCUSED OBSERVATIONS");
foreach (var candidate in result.Observations)
{
builder.AppendLine($" exact={candidate.ExactControlStatus,-5} | transitions={candidate.TransitionCount,2} | {candidate.Kind,-20} | {candidate.Reference} | {candidate.BaselineValue} -> {candidate.FinalValue}");
}

builder.AppendLine();
builder.AppendLine("WIRE / DIAGNOSTIC EVIDENCE");
foreach (var line in result.EvidenceLines)
builder.AppendLine(line);

builder.AppendLine();
builder.AppendLine("SAFETY STATE");
builder.AppendLine("G2.5-A2.1 witness is read-only and does not alter, delay, wrap or re-issue the existing ARSAS control transaction.");
builder.AppendLine("The one OPEN/CLOSE command is the operator-requested existing ARSAS control action; the witness itself performs no control write.");
builder.AppendLine("G2.5-A2.1 does not access/mutate RCB/DataSet state, send GI, save the InformationReportProven profile, or enable production dynamic reporting.");
builder.AppendLine("G2.5-A2.1 PASS identifies a command-bound physical MMS candidate only; it does NOT prove spontaneous dchg reporting.");
builder.AppendLine("Production automatic dynamic reporting remains OFF.");
return builder.ToString();
}

private static string G25A21FormatDuration(double? milliseconds)
=> milliseconds.HasValue
? milliseconds.Value.ToString("0.0", CultureInfo.InvariantCulture)
: "-";

private static string G25A21TextOrDash(string? value)
=> string.IsNullOrWhiteSpace(value) ? "-" : value.Trim();
}
13 changes: 13 additions & 0 deletions MainWindow.A21Witness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

/// <summary>
/// Read-only commissioning observer surface. Exposes the already-existing runtime
/// instance so G2.5-A2.1 can subscribe to Diagnostic events without changing the
/// control transaction implementation.
/// </summary>
public partial class MainWindow
{
internal Iec61850MonitorRuntime A21WitnessRuntime => _runtime;
}
Loading
Loading