Skip to content
Draft
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
85 changes: 18 additions & 67 deletions BrowserBatchWriteWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
private readonly long _association;
private readonly CancellationTokenSource _lifetime = new();
private bool _busy;
private bool _prepared;

Check warning on line 26 in BrowserBatchWriteWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

The field 'BrowserBatchWriteWindow._prepared' is assigned but its value is never used

Check warning on line 26 in BrowserBatchWriteWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

The field 'BrowserBatchWriteWindow._prepared' is assigned but its value is never used

Check warning on line 26 in BrowserBatchWriteWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

The field 'BrowserBatchWriteWindow._prepared' is assigned but its value is never used
public long WriteAssociation { get; private set; }
public IReadOnlyList<(string Reference, string Fc)> AttemptedLeaves { get; private set; } =
Array.Empty<(string Reference, string Fc)>();
public string WriteStatus { get; private set; } = "";

public ObservableCollection<BrowserBatchWriteRow> Rows { get; } = new();

Expand Down Expand Up @@ -325,7 +329,11 @@
if (!CurrentAssociation)
throw new InvalidOperationException("IED association changed; no Write sent.");

StatusLabel.Text = $"Writing {steps.Count} changed DA(s) and verifying readback…";
WriteAssociation = _association;
AttemptedLeaves = steps.Select(step => (
step.Reference.Domain + "/" + step.Reference.Item,
step.Reference.FunctionalConstraint)).ToArray();
StatusLabel.Text = $"Writing {steps.Count} changed DA(s)…";
// Native typed admission checks every GVA/old-value/association;
// sequential, stop-on-failure, no automatic retry or rollback.
var result = await _runtime.WriteBrowserDataAttributesAsync(
Expand All @@ -341,83 +349,26 @@
}
}

// Refresh every selected DA regardless of success/rejection.
// Verify the newly displayed value against the staged exact MMS
// bytes; an independent mismatch can never earn a green toast.
var readbackMismatch = false;
for (var i = 0; i < selected.Length; i++)
{
var row = selected[i];
if (!CurrentAssociation)
{
row.Status += " · connection changed";
readbackMismatch = true;
break;
}
try
{
var actual = await _runtime.PreviewBrowserDataAttributeWriteAsync(
_device, row.Reference!.Value, _lifetime.Token);
if (!CurrentAssociation)
{
readbackMismatch = true;
break;
}
if (actual is { IsAvailable: true, OldValue: not null })
{
var matches = MmsDataCodec.Encode(actual.OldValue).AsSpan()
.SequenceEqual(MmsDataCodec.Encode(steps[i].ProposedValue));
row.Old = actual.CurrentText;
row.Preview = actual;
row.ResetToReadback(actual.CurrentText);
if (!matches)
{
row.Status += " · Readback differs: " + actual.CurrentText;
readbackMismatch = true;
}
row.NotifyEditorChanged();
}
else
{
row.Status += " · Readback unavailable";
readbackMismatch = true;
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
row.Status += " · Readback unavailable: " + ex.Message;
readbackMismatch = true;
}
}
var anyFailure = readbackMismatch ||
result.Rejected > 0 || result.Unknown > 0 ||
result.Unverified > 0 || result.Unattempted > 0;
var shortReason = result.Rows.LastOrDefault()?.Message ?? "IED refused the requested value.";
if (shortReason.Length > 110) shortReason = shortReason[..107] + "…";
StatusLabel.Text = anyFailure
? $"Write ditolak · {result.Verified} verified, {result.Rejected} rejected, " +
$"{result.Unknown} unknown · {shortReason}"
: $"Write verified · {result.Verified} DA(s) · values refreshed from IED.";
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
anyFailure ? System.Windows.Media.Color.FromRgb(255, 242, 228)
: System.Windows.Media.Color.FromRgb(232, 248, 237));
WriteStatus = result.Rejected > 0 || result.Unknown > 0 ||
result.Unverified > 0 || result.Unattempted > 0
? $"Write ditolak / unverified: {result.Rows.LastOrDefault()?.Message ?? "IED refused"}"
: $"Write verified · {result.Verified} DA(s)";
}
catch (OperationCanceledException)
{
StatusLabel.Text = "Write uncertain; read the actual IED state before manual retry.";
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 242, 228));
WriteStatus = "Write uncertain: connection interrupted.";
}
catch (Exception ex)
{
StatusLabel.Text = "Write ditolak / unresolved: " + ex.Message;
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 242, 228));
WriteStatus = "Write ditolak / unresolved: " + ex.Message;
}
finally
{
_prepared = false;
SetBusy(false);
// Never read/write Browser state from the dialog. Let the owner
// close the popup and run canonical exact-leaf Read + SSOT ingress.
if (AttemptedLeaves.Count > 0 && IsLoaded) Close();
}
}

Expand Down
80 changes: 17 additions & 63 deletions BrowserDataWriteWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public partial class BrowserDataWriteWindow : Window
!_lifetime.IsCancellationRequested;
private IedBrowserNativeWritePreview? _preview;
private bool _busy;
public bool WriteAttempted { get; private set; }
public long WriteAssociation { get; private set; }
public string WriteStatus { get; private set; } = "";
public (string Reference, string Fc) AttemptedLeaf =>
(_reference.Domain + "/" + _reference.Item, _reference.FunctionalConstraint);

public BrowserDataWriteWindow(
Iec61850MonitorRuntime runtime,
Expand Down Expand Up @@ -175,86 +180,35 @@ private async void Write_Click(object sender, RoutedEventArgs e)
NewValueBox.IsEnabled = false;
BoolPicker.IsEnabled = false;
StatusLabel.Text = "Writing…";
string feedback = "Write not verified.";
bool accepted = false;
bool readbackVerified = false;
bool verified = false;
string feedback = "IED did not verify Write.";
WriteAttempted = true;
WriteAssociation = _association;
try
{
var step = new IedBrowserTypedWriteStep(
_reference, p.GvaSignature, p.OldValue, proposed);
var outcome = await _runtime.WriteBrowserDataAttributesAsync(
_device, new[] { step }, true, _lifetime.Token);
if (!CurrentAssociation) return;
var result = outcome.Rows.Single();
accepted = outcome.Verified == 1;
feedback = accepted ? "Write accepted" : "Write ditolak: " + result.Message;
verified = outcome.Verified == 1;
feedback = verified ? "Write verified" :
"Write ditolak: " + (outcome.Rows.FirstOrDefault()?.Message ?? "IED refused");
}
catch (OperationCanceledException)
{
feedback = "Write uncertain: connection interrupted.";
feedback = "Write outcome unknown: connection interrupted.";
}
catch (Exception ex)
{
feedback = "Write ditolak / unknown: " + ex.Message;
}
finally
{
// Independent live GVA/Read after every attempted Write, even
// when the IED rejected it. Never send an automatic retry.
_preview = null;
if (CurrentAssociation)
{
try
{
var readback = await _runtime.PreviewBrowserDataAttributeWriteAsync(
_device, _reference, _lifetime.Token);
if (CurrentAssociation && readback is
{ IsAvailable: true, OldValue: not null, Type: not null } live)
{
CurrentLabel.Text = "Current: " + live.CurrentText;
TypeLabel.Text = live.Type.MmsType;
_preview = live;
if (live.Type.MmsType == "boolean")
{
BoolPicker.Visibility = Visibility.Visible;
NewValueBox.Visibility = Visibility.Collapsed;
BoolPicker.SelectedIndex = live.CurrentText.Equals(
"true", StringComparison.OrdinalIgnoreCase) ? 0 :
live.CurrentText.Equals("false", StringComparison.OrdinalIgnoreCase) ? 1 : -1;
BoolPicker.IsEnabled = BoolPicker.SelectedIndex >= 0;
}
else
{
BoolPicker.Visibility = Visibility.Collapsed;
NewValueBox.Visibility = Visibility.Visible;
NewValueBox.Text = live.CurrentText;
NewValueBox.IsEnabled = true;
}
var matches = MmsDataCodec.Encode(live.OldValue).AsSpan()
.SequenceEqual(MmsDataCodec.Encode(proposed));
readbackVerified = accepted && matches;
feedback = readbackVerified
? "Write verified · Readback: " + live.CurrentText
: accepted ? "Write not verified: readback differs (" + live.CurrentText + ")"
: feedback + " · Current: " + live.CurrentText;
}
else
{
feedback += " · Readback unavailable.";
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
feedback += " · Readback unavailable: " + ex.Message;
}
}
WriteStatus = feedback;
_busy = false;
RefreshInput();
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
readbackVerified
? System.Windows.Media.Color.FromRgb(232, 248, 237)
: System.Windows.Media.Color.FromRgb(255, 242, 228));
StatusLabel.Text = feedback.Length > 165 ? feedback[..162] + "…" : feedback;
// No duplicate read of the value inside this popup. Browser will
// issue its own exact live MMS Read and publish only into SSOT.
if (IsLoaded) Close();
}
}

Expand Down
31 changes: 13 additions & 18 deletions ControlModelWriteWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public sealed record ModelChoice(Iec61850ControlModelKind Model, string Label);
private readonly CancellationTokenSource _cancellation = new();
private bool _busy;
public bool VerifiedWriteOccurred { get; private set; }
public bool WriteAttempted { get; private set; }
public string WriteStatus { get; private set; } = "";
public long WriteAssociation { get; private set; }
private bool _liveModelReady;
private Iec61850ControlModelKind _liveModel = Iec61850ControlModelKind.Unknown;
// Read/GVA evidence belongs to one live MMS association and one immutable
Expand Down Expand Up @@ -151,10 +154,12 @@ private async void Write_Click(object sender, RoutedEventArgs e)
_liveModelReady = false;
ModelPicker.IsEnabled = false;
UpdateWriteButton();
StatusLabel.Text = "Writing and reading back ctlModel…";
StatusLabel.Text = "Writing ctlModel…";
ResultBanner.Background = System.Windows.Media.Brushes.Transparent;
string? failure = null;
bool verified = false;
WriteAttempted = true;
WriteAssociation = _inspectedAssociation;
try
{
// The pinned native dispatcher checks expected-old value and
Expand All @@ -180,24 +185,14 @@ private async void Write_Click(object sender, RoutedEventArgs e)
}
finally
{
// Always refresh the displayed DA from the live IED, including
// rejected and uncertain Writes. Read only; never retry Write.
if (!_cancellation.IsCancellationRequested && IsLoaded)
{
await ReadModelAsync();
if (verified && _liveModelReady && _liveModel == selected.Model)
{
StatusLabel.Text = $"Write verified · Readback: {(int)_liveModel} ({selected.Label})";
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(232, 248, 237));
}
else
{
ShowWriteFailure(failure ?? "Readback did not confirm the requested value.");
}
}
WriteStatus = verified
? "ctlModel Write verified; refreshing Data Model…"
: "ctlModel Write ditolak / unknown: " +
(string.IsNullOrWhiteSpace(failure) ? "IED did not verify Write." : failure);
_busy = false;
UpdateWriteButton();
// Browser owns the post-Write MMS Read and SSOT publication.
// Close the editor immediately, including on rejected Writes.
if (IsLoaded) Close();
}
}

Expand Down
11 changes: 10 additions & 1 deletion IedBrowserWriteWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public partial class IedBrowserWriteWindow : Window
// Invalidate stale Command Dock permission on modal close in that case.
public HashSet<string> PotentiallyChangedControlModels { get; } =
new(StringComparer.OrdinalIgnoreCase);
public long WriteAssociation => _association;
public List<(string Reference, string Fc)> AttemptedLeaves { get; } = new();
public string WriteStatus { get; private set; } = "";
public ObservableCollection<IedBrowserWriteRow> Rows { get; }

public IedBrowserWriteWindow(
Expand Down Expand Up @@ -203,6 +206,7 @@ private async void Write_Click(object sender, RoutedEventArgs e)
row.ExpireLiveRead(); // each attempt requires a NEW explicit Read
if (row.IsControlModel)
PotentiallyChangedControlModels.Add(row.Reference[..^".ctlModel".Length]);
AttemptedLeaves.Add((row.Reference, row.FunctionalConstraint));
var result = await _runtime.WriteBrowserConfigurationAsync(
_device.DeviceId, _model, _association, row, requested, _lifetime.Token);
row.Current = result.ReadbackVerified ? result.After : result.Before;
Expand Down Expand Up @@ -234,7 +238,12 @@ private async void Write_Click(object sender, RoutedEventArgs e)
ResultBanner.Background = new System.Windows.Media.SolidColorBrush(
System.Windows.Media.Color.FromRgb(255, 242, 228));
}
finally { SetBusy(false); }
finally
{
WriteStatus = StatusText.Text;
SetBusy(false);
if (AttemptedLeaves.Count > 0 && IsLoaded) Close();
}
}

private void Close_Click(object sender, RoutedEventArgs e) => Close();
Expand Down
53 changes: 53 additions & 0 deletions MainWindow.IedBrowser.TypedDataWrite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ private void IedBrowserWrite_Click(object sender, RoutedEventArgs e)
Owner = this
};
dialog.ShowDialog();
if (dialog.WriteAttempted && device.LiveDiscoveryModel is { } model)
QueueIedBrowserPostWriteRead(device, model, dialog.WriteAssociation,
new[] { (Reference: modelSignal.ObjectReference + ".ctlModel", Fc: "CF") },
dialog.WriteStatus);
if (dialog.VerifiedWriteOccurred && device.IsConnected)
_ = RefreshControlValuesAsync(device, force: true);
}
Expand All @@ -185,6 +189,9 @@ private void IedBrowserWrite_Click(object sender, RoutedEventArgs e)
Owner = this
};
dialog.ShowDialog();
if (dialog.WriteAttempted && device.LiveDiscoveryModel is { } model)
QueueIedBrowserPostWriteRead(device, model, dialog.WriteAssociation,
new[] { dialog.AttemptedLeaf }, dialog.WriteStatus);
}
else if (owner is not null && device.LiveDiscoveryModel is { } model)
{
Expand All @@ -193,6 +200,9 @@ private void IedBrowserWrite_Click(object sender, RoutedEventArgs e)
Owner = this
};
dialog.ShowDialog();
if (dialog.AttemptedLeaves.Count > 0)
QueueIedBrowserPostWriteRead(device, model, dialog.WriteAssociation,
dialog.AttemptedLeaves, dialog.WriteStatus);
}
RefreshIedBrowserWriteButton();
// A successful MMS Write is NOT process feedback. An explicit Read or
Expand All @@ -214,7 +224,50 @@ private void IedBrowserWriteDo_Click(object sender, RoutedEventArgs e)
Owner = this
};
dialog.ShowDialog();
if (dialog.AttemptedLeaves.Count > 0)
QueueIedBrowserPostWriteRead(device, model, dialog.WriteAssociation,
dialog.AttemptedLeaves, dialog.WriteStatus);
RefreshIedBrowserWriteButton();
}

private async void QueueIedBrowserPostWriteRead(
Iec61850MonitorDevice device, LiveIedModelDiscoveryDocument model,
long association, IReadOnlyCollection<(string Reference, string Fc)> attempted,
string status)
{
// Never push the popup's preview into Data Model. Resolve exact,
// uniquely declared live DA identities, then enter the SAME typed MMS
// Read → SSOT → visible-row pipeline as the Browser Read button.
var request = IedBrowserPostWriteReadRequest.Create(
device, model, association, attempted, status);
if (request is null || !ReferenceEquals(SelectedDevice, device))
{
IedBrowserDetailStatus.Text =
"Post-Write Read unavailable: IED association/model changed.";
return;
}
IedBrowserDetailStatus.Text = string.IsNullOrWhiteSpace(status)
? "Reading written DA from IED into Data Model…"
: status;
// A prior automatic Browser Read may still be draining when the
// modal closes. Do not silently discard the post-Write refresh or
// cancel that reader. Wait briefly for its bounded flight to finish.
for (var checks = 0;
(_iedBrowserReadOperation is not null || device.IsBusy) && checks < 200;
checks++)
{
await Task.Delay(100);
if (!request.Matches(device) || !ReferenceEquals(SelectedDevice, device))
return; // reconnect or navigation changed: no stale publication
}
if (!request.Matches(device) || !ReferenceEquals(SelectedDevice, device) ||
_iedBrowserReadOperation is not null || device.IsBusy)
{
IedBrowserDetailStatus.Text =
"Post-Write Read not started; previous Browser Read still active.";
return;
}
IedBrowserReadValues_Click(new Button { Tag = request }, new RoutedEventArgs());
}

}
Loading
Loading