Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f6a1aa0
ux(ip): 256px compact IP editor, shared material circle chevrons, rou…
masarray Oct 23, 2026
e3ce74c
fix(ip): prepaint DPI-aware owner centering, fixed heights, automatic…
masarray Oct 23, 2026
4a04bf3
test(ip): lock 256px Material chevrons, rounded port, automated IPv6 …
masarray Oct 23, 2026
57bfca5
test(wpf): honor fixed prepaint IP dialog sizing while preserving SCL…
masarray Oct 23, 2026
3ab4cf8
test(startup): assert single compact Material field and prepaint manu…
masarray Oct 23, 2026
783493e
style(ip): compact vertical rhythm without initial scrollbar and bind…
masarray Oct 23, 2026
7b38bc3
fix(ip): match explicit non-scrolling first-paint dialog heights
masarray Oct 23, 2026
c8f5c74
fix(ip): paste full IPv6 literals without dots into automatic raw edi…
masarray Oct 23, 2026
01b703b
test(ip): assert full IPv6 clipboard paste bypasses IPv4-only octet r…
masarray Oct 23, 2026
2405b32
test(ip): use proper empty CheckBox collection assertion
masarray Oct 23, 2026
137efb6
feat(control): redesign compact ARSAS popup with pinned actions and p…
masarray Oct 23, 2026
ce3a991
feat(control): compact SBO-aware UX, originator settings, ctlNum and …
masarray Oct 23, 2026
f8d64ee
test(control): verify default station origin and explicit user-select…
masarray Oct 23, 2026
c44b8f2
fix(control): preserve explicit independent feedback warning in conci…
masarray Oct 23, 2026
9bbcfeb
fix(control): use a strict choice selector for discrete DPC/SPC/BSC v…
masarray Oct 23, 2026
6791b2c
fix(control): keep popup open during in-flight native command and avo…
masarray Oct 23, 2026
2292058
feat(control): disable modal close during in-flight command ownership
masarray Oct 23, 2026
ee83a2b
test(control): verify compact WPF layout, SBO action truth, origin/te…
masarray Oct 23, 2026
dcceb5a
fix(control): allow closing slow read-only inspection while protectin…
masarray Oct 23, 2026
7d1f9b4
testability(control): expose side-effect-free originator ID validator…
masarray Oct 23, 2026
7ed9aa7
fix(control): require operator authorization for Test=true as well as…
masarray Oct 23, 2026
177b872
test(control): verify Test=true still requires explicit authorization…
masarray Oct 23, 2026
4a9a784
merge(staging): preserve latest Control popup improvements plus prove…
masarray Oct 23, 2026
aaf9562
fix(control): update primary action label immediately on Test toggle
masarray Oct 23, 2026
29d5eff
fix(control): freeze editable command parameters throughout a native …
masarray Oct 23, 2026
1f857bb
fix(control): disable value, checks and origin edits while executing
masarray Oct 23, 2026
1837348
fix(control): distinguish ambiguous wire/timeout outcomes from confir…
masarray Oct 23, 2026
5e66acc
test(control): retain strict local NOT SENT vs uncertain/unresolved s…
masarray Oct 23, 2026
0d4e13c
merge(staging): exact latest Control popup + compact prepaint Smart I…
masarray Oct 23, 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
280 changes: 152 additions & 128 deletions ControlCommandWindow.xaml

Large diffs are not rendered by default.

144 changes: 126 additions & 18 deletions ControlCommandWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,31 @@ public partial class ControlCommandWindow : Window, INotifyPropertyChanged
private string _sequenceText = "Detecting the required IEC 61850 control sequence…";
private string _controlEvidenceText = string.Empty;
private string _selectedValue = string.Empty;
private string _modelShortText = "Inspecting…";
private string _sequenceHint = "Inspecting control…";
private string _originatorId = "ARSAS";
private string _selectedOriginCategory = "StationControl";
private string _controlNumberText = "Auto";
private string _terminationText = "—";
private Iec61850ControlModelKind _liveControlModel = Iec61850ControlModelKind.Unknown;
private bool _interlockCheck = true;
private bool _synchroCheck;
private bool _testMode;
private bool _confirmLiveCommand;
private bool _isBusy;
private bool _commandFlightActive;
private bool _isReady;
private string _commandStage = "Ready to inspect";
private string _commandStatus = "ArIED will read the live control model and current status before enabling Send Command.";

public event PropertyChangedEventHandler? PropertyChanged;

public ObservableCollection<string> ValueOptions { get; } = new();
public IReadOnlyList<string> OriginCategories { get; } = new[]
{
"StationControl", "BayControl", "RemoteControl", "AutomaticBay",
"AutomaticStation", "AutomaticRemote", "Maintenance", "Process"
};
public string DeviceSummary => $"{_device.Name} • {_device.EndpointText}";
public string SignalName => _signal.Name;
public string Telegram => Iec61850MonitorPoint.StripIedNamePrefix(_signal.ObjectReference, _device.Name);
Expand All @@ -43,6 +56,38 @@ public partial class ControlCommandWindow : Window, INotifyPropertyChanged
public string SequenceText { get => _sequenceText; private set => Set(ref _sequenceText, value); }
public string ControlEvidenceText { get => _controlEvidenceText; private set => Set(ref _controlEvidenceText, value); }
public string SelectedValue { get => _selectedValue; set { if (Set(ref _selectedValue, value)) Raise(nameof(CanSend)); } }
public string ModelShortText { get => _modelShortText; private set => Set(ref _modelShortText, value); }
public string SequenceHint { get => _sequenceHint; private set => Set(ref _sequenceHint, value); }
public string ControlNumberText { get => _controlNumberText; private set => Set(ref _controlNumberText, value); }
public string TerminationText { get => _terminationText; private set => Set(ref _terminationText, value); }
public string SelectedOriginCategory
{
get => _selectedOriginCategory;
set
{
if (Set(ref _selectedOriginCategory, value)) Raise(nameof(CanSend));
}
}
public string OriginatorId
{
get => _originatorId;
set
{
if (Set(ref _originatorId, value))
{
Raise(nameof(OriginValidationText));
Raise(nameof(CanSend));
}
}
}
public string OriginValidationText => IsValidOriginator(OriginatorId)
? string.Empty : "Originator ID requires 1–64 printable ASCII characters.";
public static bool IsValidOriginator(string? id) =>
!string.IsNullOrWhiteSpace(id) && id.Length <= 64 &&
id.All(c => c is >= ' ' and <= '~');
public bool AllowCustomValue
=> _controlCdcText is "APC" or "INC" or "ISC" or "INC/ISC";

public bool InterlockCheck { get => _interlockCheck; set => Set(ref _interlockCheck, value); }
public bool SynchroCheck { get => _synchroCheck; set => Set(ref _synchroCheck, value); }
public bool TestMode
Expand All @@ -53,6 +98,7 @@ public bool TestMode
if (!Set(ref _testMode, value)) return;
UpdateLiveWarning();
Raise(nameof(CanSend));
Raise(nameof(SendButtonText));
}
}
public bool ConfirmLiveCommand
Expand All @@ -71,9 +117,13 @@ private set
{
if (!Set(ref _isBusy, value)) return;
Raise(nameof(CanSend));
Raise(nameof(CanClose));
Raise(nameof(CanEdit));
Raise(nameof(SendButtonText));
}
}
public bool CanClose => !_commandFlightActive;
public bool CanEdit => IsReady && !IsBusy;
public bool IsReady
{
get => _isReady;
Expand All @@ -82,12 +132,17 @@ private set
if (!Set(ref _isReady, value)) return;
UpdateLiveWarning();
Raise(nameof(CanSend));
Raise(nameof(CanEdit));
}
}
public string CommandStage { get => _commandStage; private set => Set(ref _commandStage, value); }
public string CommandStatus { get => _commandStatus; private set => Set(ref _commandStatus, value); }
public string SendButtonText => IsBusy ? "Sending…" : TestMode ? "Send Test" : "Send Command";
public bool CanSend => IsReady && !IsBusy && !string.IsNullOrWhiteSpace(SelectedValue) && (TestMode || ConfirmLiveCommand);
public string SendButtonText => IsBusy ? "Working…" : TestMode ? "Send test" :
_liveControlModel is Iec61850ControlModelKind.SboNormal or
Iec61850ControlModelKind.SboEnhanced ? "Select → Operate" : "Operate";
public bool CanSend => IsReady && !IsBusy && !string.IsNullOrWhiteSpace(SelectedValue) &&
IsValidOriginator(OriginatorId) && OriginCategories.Contains(SelectedOriginCategory) &&
ConfirmLiveCommand;

public ControlCommandWindow(
Iec61850MonitorRuntime runtime,
Expand All @@ -107,8 +162,8 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
// Honor the actual DPI-aware working area. All details scroll while
// the command confirmation footer stays on-screen at 1366x768/150%.
var work = SystemParameters.WorkArea;
Height = Math.Max(MinHeight, Math.Min(590, work.Height - 28));
Width = Math.Max(MinWidth, Math.Min(630, work.Width - 28));
Height = Math.Max(MinHeight, Math.Min(500, work.Height - 24));
Width = Math.Max(MinWidth, Math.Min(565, work.Width - 24));
await InspectAsync();
}

Expand All @@ -126,7 +181,22 @@ private async Task InspectAsync()
_cancellation.Token);

ControlModelText = capabilities.ControlModelText;
_liveControlModel = capabilities.ControlModel;
ModelShortText = capabilities.ControlModel switch
{
Iec61850ControlModelKind.DirectNormal => "Direct · normal",
Iec61850ControlModelKind.DirectEnhanced => "Direct · enhanced",
Iec61850ControlModelKind.SboNormal => "SBO · normal",
Iec61850ControlModelKind.SboEnhanced => "SBO · enhanced",
Iec61850ControlModelKind.StatusOnly => "Status only",
_ => "Unverified"
};
SequenceHint = !capabilities.SupportsOperate ? "Not operable" :
capabilities.UsesSelectBeforeOperate
? "Native: Select → Operate" : "Native: Operate";
Raise(nameof(SendButtonText));
ControlCdcText = string.IsNullOrWhiteSpace(capabilities.ControlCdc) ? "CONTROL" : capabilities.ControlCdc;
Raise(nameof(AllowCustomValue));
CurrentValue = capabilities.CurrentValue;
EngineStatusText = capabilities.EngineControlServiceStatus;
SequenceText = capabilities.SequenceText;
Expand All @@ -143,7 +213,7 @@ private async Task InspectAsync()
{
CommandStage = capabilities.SupportsOperate ? "Ready" : "Control unavailable";
CommandStatus = capabilities.SupportsOperate
? $"{capabilities.ControlModelText}. {capabilities.SequenceText}. Select a command value and required check flags."
? "Choose a value and verify the checks before sending."
: capabilities.EngineControlServiceStatus;
SetResultTone(capabilities.SupportsOperate ? "Ready" : "Error");
}
Expand Down Expand Up @@ -171,6 +241,19 @@ private async void SendCommand_Click(object sender, RoutedEventArgs e)
if (!CanSend)
return;

// Keep explicit staged safety confirmation without cluttering the main popup.
// SBO selection is part of the native one-shot sequence, NOT an already-held lease.
var confirm = MessageBox.Show(this,
$"IED: {_device.Name}\nObject: {Telegram}\nValue: {SelectedValue}\n" +
$"Sequence: {ModelShortText}\nOrigin: {SelectedOriginCategory}/{OriginatorId.Trim()}\n\n" +
(TestMode ? "Send an IEC 61850 command with Test=true?" :
"Send a LIVE IEC 61850 command to this IED?"),
"Confirm control", MessageBoxButton.YesNo,
MessageBoxImage.Warning, MessageBoxResult.No);
if (confirm != MessageBoxResult.Yes) return;

_commandFlightActive = true;
Raise(nameof(CanClose));
IsBusy = true;
CommandStage = TestMode ? "Sending test command" : "Sending live command";
CommandStatus = "Executing the IEC 61850 control sequence selected from ctlModel…";
Expand All @@ -188,22 +271,34 @@ private async void SendCommand_Click(object sender, RoutedEventArgs e)
TestMode = TestMode,
FeedbackTimeoutMs = _signal.IsPositionControl ? 12000 : 8000,
CommandTerminationTimeoutMs = 10000,
OriginCategory = "StationControl"
Originator = OriginatorId.Trim(),
OriginCategory = SelectedOriginCategory
},
_cancellation.Token);

CommandStage = result.IsSuccess && !TestMode ? "Command accepted" : result.Stage;
CommandStatus = BuildCommandResultText(result);
if (result.IsSuccess && !TestMode)
{
CommandStatus += " Command accepted by the IEC 61850 control service. Waiting for independent IED process feedback; monitored stVal is not changed from the command path.";
}
ControlNumberText = result.ControlNumber;
TerminationText = result.CommandTerminationReceived
? result.PositiveTermination ? "Positive" : "Negative" : "Not reported";
CommandStage = result.IsSuccess
? result.FeedbackConfirmed ? "Feedback confirmed" : "Command accepted"
: result.CompletionState.Equals("NotSent", StringComparison.OrdinalIgnoreCase)
? "Not sent"
: result.CompletionState is "Unknown" or "TimedOut" or "AssociationLost"
? "Outcome uncertain" : "Control rejected";
CommandStatus = result.IsSuccess
? result.FeedbackConfirmed
? "IED response and independent process feedback verified."
: "IED accepted. Waiting for independent IED process feedback."
: Iec61850ControlFailureReason.Explain(result).Summary;
// Accepted MMS response is not process-image authority. Full stage,
// LastApplError, termination and wire details stay in diagnostics.
ControlEvidenceText += "\n\nRESULT\n" + BuildCommandResultText(result);
SetResultTone(result.IsSuccess ? "Success" : "Error");
}
catch (OperationCanceledException)
{
CommandStage = "Cancelled";
CommandStatus = "The command operation was cancelled.";
CommandStage = "Outcome uncertain";
CommandStatus = "Request may have reached the IED. Verify before another attempt.";
SetResultTone("Neutral");
}
catch (InvalidOperationException ex) when (
Expand All @@ -217,12 +312,15 @@ private async void SendCommand_Click(object sender, RoutedEventArgs e)
}
catch (Exception ex)
{
CommandStage = "Command failed";
CommandStatus = $"{ex.GetType().Name}: {ex.Message}";
SetResultTone("Error");
// An unexpected failure after dispatch cannot prove the command was NOT sent.
CommandStage = "Outcome unresolved";
CommandStatus = $"{ex.GetType().Name}: {ex.Message}. Verify IED state before retry.";
SetResultTone("Warning");
}
finally
{
_commandFlightActive = false;
Raise(nameof(CanClose));
IsBusy = false;
}
}
Expand Down Expand Up @@ -349,7 +447,9 @@ private void UpdateLiveWarning()
{
if (LiveCommandWarning == null)
return;
LiveCommandWarning.Visibility = IsReady && !TestMode ? Visibility.Visible : Visibility.Collapsed;
// Test=true is an MMS control flag, not an offline simulator.
// Some servers may still change equipment: authorization is mandatory.
LiveCommandWarning.Visibility = IsReady ? Visibility.Visible : Visibility.Collapsed;
if (TestMode)
ConfirmLiveCommand = false;
}
Expand Down Expand Up @@ -381,6 +481,14 @@ private void Close_Click(object sender, RoutedEventArgs e)

private void Window_Closing(object? sender, CancelEventArgs e)
{
// Never abandon a dispatched native control flight by simply closing
// its UI. Late responses need the owning session to resolve/quarantine.
if (_commandFlightActive)
{
e.Cancel = true;
CommandStatus = "IED operation still resolving. Close after the result is known.";
return;
}
_cancellation.Cancel();
_cancellation.Dispose();
}
Expand Down
Loading
Loading