diff --git a/BrowserBatchWriteWindow.xaml b/BrowserBatchWriteWindow.xaml
index 3597b69cf..da2a31376 100644
--- a/BrowserBatchWriteWindow.xaml
+++ b/BrowserBatchWriteWindow.xaml
@@ -1,38 +1,37 @@
-
+ Background="#F5F7FB" FontFamily="{StaticResource AppFontFamily}" FontSize="11.5"
+ Icon="Assets/app-icon.ico" Loaded="Window_Loaded" Closed="Window_Closed">
+
-
-
+
-
-
-
+
+
-
-
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BrowserBatchWriteWindow.xaml.cs b/BrowserBatchWriteWindow.xaml.cs
index da9bd4413..ee1c5f58e 100644
--- a/BrowserBatchWriteWindow.xaml.cs
+++ b/BrowserBatchWriteWindow.xaml.cs
@@ -103,11 +103,42 @@ private void SetBusy(bool busy)
WriteButton.IsEnabled = !busy && _prepared;
}
- private void Window_Loaded(object sender, RoutedEventArgs e)
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
{
var work = SystemParameters.WorkArea;
- Height = Math.Max(MinHeight, Math.Min(580, work.Height - 26));
- Width = Math.Max(MinWidth, Math.Min(760, work.Width - 26));
+ Height = Math.Max(MinHeight, Math.Min(515, work.Height - 26));
+ Width = Math.Max(MinWidth, Math.Min(940, work.Width - 26));
+ await PrimeTypedRowsAsync();
+ }
+
+ // The first bounded visible batch is read-only hydrated on open, like
+ // IEDScout. No blank "new value" textbox and NO hidden Write/Operate.
+ // Larger DOs can use Read on an explicitly selected, unhydrated row.
+ private async Task PrimeTypedRowsAsync()
+ {
+ if (_busy || !CurrentAssociation) return;
+ var candidates = Rows.Where(r => r.Eligible).Take(24).ToArray();
+ SetBusy(true);
+ try
+ {
+ var done = 0;
+ foreach (var row in candidates)
+ {
+ if (!CurrentAssociation || _lifetime.IsCancellationRequested) break;
+ try
+ {
+ var preview = await _runtime.PreviewBrowserDataAttributeWriteAsync(
+ _device, row.Reference!.Value, _lifetime.Token);
+ if (!CurrentAssociation) break;
+ row.ApplyLivePreview(preview, preserveExplicitEdit: false);
+ done++;
+ }
+ catch (OperationCanceledException) { break; }
+ catch (Exception ex) { row.Status = "Read failed: " + ex.Message; }
+ }
+ StatusLabel.Text = $"Read {done}/{candidates.Length} typed DA(s). Choose a value; select its checkbox and press Read, then Write.";
+ }
+ finally { SetBusy(false); }
}
private BrowserBatchWriteRow[] CheckedRows()
@@ -124,10 +155,18 @@ private async void Inspect_Click(object sender, RoutedEventArgs e)
WriteButton.IsEnabled = false;
var selected = CheckedRows();
if (selected.Length is 0 or > IedBrowserTypedWriteBatch.MaxSelectedAttributes ||
- selected.Any(r => !r.Eligible || !r.HasEdited) || !CurrentAssociation)
+ selected.Any(r => !r.Eligible) || !CurrentAssociation)
{
- StatusLabel.Text =
- "Select 1–64 eligible DAs, explicitly edit each new value and keep the same online association. No Write sent.";
+ StatusLabel.Text = "Select 1–64 eligible attributes on the current IED; nothing was written.";
+ return;
+ }
+
+ // The first Read for an uninspected row populates its type-aware
+ // editor without forcing the operator to invent a value in advance.
+ var firstRead = selected.Any(r => r.Preview is null);
+ if (!firstRead && selected.Any(r => !r.HasEdited))
+ {
+ StatusLabel.Text = "Choose a different value for every selected DA before Write.";
return;
}
@@ -141,15 +180,11 @@ private async void Inspect_Click(object sender, RoutedEventArgs e)
if (!CurrentAssociation)
throw new InvalidOperationException("IED association changed during inspection.");
StatusLabel.Text = $"Read-only inspect {i + 1}/{selected.Length}: {row.ReferenceText}";
- row.Preview = null;
var preview = await _runtime.PreviewBrowserDataAttributeWriteAsync(
_device, row.Reference!.Value, _lifetime.Token);
if (!CurrentAssociation)
throw new InvalidOperationException("IED association changed during inspection.");
- row.Preview = preview.IsAvailable ? preview : null;
- row.Type = preview.Type?.MmsType ?? "unverified";
- row.Old = preview.CurrentText;
- row.Status = preview.Message;
+ row.ApplyLivePreview(preview, preserveExplicitEdit: !firstRead);
if (row.Preview is null)
{
StatusLabel.Text = "One DA has no valid live typed Read/GVA proof. No Write sent.";
@@ -157,6 +192,11 @@ private async void Inspect_Click(object sender, RoutedEventArgs e)
}
}
+ if (firstRead)
+ {
+ StatusLabel.Text = "Live values loaded. Choose a new value and click Read to verify changes.";
+ return;
+ }
if (!TryFreezePlan(selected, out _, out var rejection))
{
StatusLabel.Text = rejection + " No Write sent.";
@@ -306,6 +346,18 @@ public sealed class BrowserBatchWriteRow : INotifyPropertyChanged
private string _status;
private string _type = "Inspect first";
private string _old = "—";
+ private bool _canEdit;
+ private bool _isBoolean;
+ public IReadOnlyList BooleanOptions { get; } = new[] { "false", "true" };
+ public string ShortName =>
+ Reference?.Item.Split((char)36).LastOrDefault() ?? ReferenceText;
+ public bool CanEdit => _canEdit;
+ public Visibility BooleanEditorVisibility =>
+ _canEdit && _isBoolean ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility TextEditorVisibility =>
+ _canEdit && !_isBoolean ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility PlaceholderVisibility =>
+ _canEdit ? Visibility.Collapsed : Visibility.Visible;
public MmsObjectReference? Reference { get; }
public string ReferenceText { get; }
@@ -326,6 +378,42 @@ public BrowserBatchWriteRow(
_status = reason;
}
+ public void ApplyLivePreview(
+ IedBrowserNativeWritePreview preview, bool preserveExplicitEdit)
+ {
+ Preview = preview.IsAvailable && preview.Type is not null &&
+ preview.OldValue is not null ? preview : null;
+ Type = preview.Type?.MmsType ?? "unverified";
+ Old = preview.CurrentText;
+ _isBoolean = preview.Type?.MmsType == "boolean";
+ _canEdit = Eligible && Preview is not null &&
+ (preview.Type!.MmsType is "boolean" or "integer" or "unsigned" or
+ "visible-string" or "mms-string" ||
+ preview.Type.MmsType == "floating-point" && preview.Type.Size == 32);
+ Status = Preview is null ? preview.Message :
+ _canEdit ? "Ready to edit" : "Unsupported live MMS editor";
+ if (!preserveExplicitEdit)
+ {
+ // Use the exact current wire value; never guess a textual enum
+ // description or coerce a Boolean 0/1 behind the operator.
+ _newValue = preview.OldValue?.Value switch
+ {
+ bool b => b ? "true" : "false",
+ string str => str,
+ long or ulong or int or uint or float or double =>
+ Convert.ToString(preview.OldValue.Value,
+ System.Globalization.CultureInfo.InvariantCulture) ?? "",
+ _ => ""
+ };
+ HasEdited = false;
+ Changed(nameof(NewValue));
+ }
+ Changed(nameof(CanEdit));
+ Changed(nameof(BooleanEditorVisibility));
+ Changed(nameof(TextEditorVisibility));
+ Changed(nameof(PlaceholderVisibility));
+ }
+
public bool Selected
{
get => _selected;
diff --git a/BrowserDataWriteWindow.xaml b/BrowserDataWriteWindow.xaml
index 63664c816..eadeff6d7 100644
--- a/BrowserDataWriteWindow.xaml
+++ b/BrowserDataWriteWindow.xaml
@@ -1,85 +1,125 @@
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
+
diff --git a/BrowserDataWriteWindow.xaml.cs b/BrowserDataWriteWindow.xaml.cs
index 5a326d4ed..0b4a8978b 100644
--- a/BrowserDataWriteWindow.xaml.cs
+++ b/BrowserDataWriteWindow.xaml.cs
@@ -31,7 +31,225 @@ public BrowserDataWriteWindow(
ArgumentNullException.ThrowIfNull(attribute);
_reference = MmsObjectReference.Parse(
attribute.MmsReference, attribute.FunctionalConstraint);
- ReferenceLabel.Text = $"{_device.Name} • {_reference.Domain}/{_reference.Item} [{_reference.FunctionalConstraint}]";
+ ReferenceLabel.Text = $"{_device.Name} · {_reference.Domain}/{_reference.Item} [{_reference.FunctionalConstraint}]";
+ AttributeLabel.Text = _reference.Item.Split((char)36).Last() +
+ " [" + _reference.FunctionalConstraint + "]";
+ }
+
+ private async void Window_Loaded(object sender, RoutedEventArgs e)
+ => await ReadValueAsync();
+
+ private async void Read_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_busy) await ReadValueAsync();
+ }
+
+ private async Task ReadValueAsync()
+ {
+ _busy = true;
+ _preview = null;
+ WriteButton.IsEnabled = false;
+ ReadButton.IsEnabled = false;
+ NewValueBox.IsEnabled = false;
+ BooleanPicker.IsEnabled = false;
+ NewValueBox.Visibility = Visibility.Collapsed;
+ BooleanPicker.Visibility = Visibility.Collapsed;
+ UnsupportedLabel.Visibility = Visibility.Visible;
+ UnsupportedLabel.Text = "Reading IED…";
+ try
+ {
+ _preview = await _runtime.PreviewBrowserDataAttributeWriteAsync(
+ _device, _reference, _lifetime.Token);
+ if (_lifetime.IsCancellationRequested) return;
+ if (_preview is not { IsAvailable: true, Type: not null, OldValue: not null } p)
+ {
+ StatusLabel.Text = _preview?.Message ?? "Live GVA and value not available.";
+ DiffLabel.Text = "No Write is permitted. Select another live DA.";
+ return;
+ }
+
+ CurrentLabel.Text = p.CurrentText;
+ TypeLabel.Text = p.Type.SclBType.Length > 0
+ ? $"{p.Type.SclBType} ({p.Type.MmsType}, {p.Type.Size?.ToString() ?? "?"})"
+ : $"{p.Type.MmsType} ({p.Type.Size?.ToString() ?? "?"})";
+ var supported = p.Type.MmsType switch
+ {
+ "boolean" or "integer" or "unsigned" or "visible-string" or "mms-string" => true,
+ "floating-point" when p.Type.Size == 32 => true,
+ _ => false
+ };
+ if (supported && p.Type.MmsType == "boolean")
+ {
+ BooleanPicker.Visibility = Visibility.Visible;
+ BooleanPicker.SelectedItem = (p.OldValue.Value is true) ? "true" : "false";
+ BooleanPicker.IsEnabled = true;
+ UnsupportedLabel.Visibility = Visibility.Collapsed;
+ }
+ else if (supported)
+ {
+ // Prefill from the exact live read. Never present a blind empty box.
+ // Unsupported types remain read-only (no codec guessing).
+ NewValueBox.Visibility = Visibility.Visible;
+ NewValueBox.Text = p.Type.MmsType switch
+ {
+ "visible-string" or "mms-string" => p.OldValue.Value as string ?? "",
+ "integer" or "unsigned" or "floating-point" =>
+ Convert.ToString(p.OldValue.Value,
+ System.Globalization.CultureInfo.InvariantCulture) ?? "",
+ _ => ""
+ };
+ NewValueBox.IsEnabled = true;
+ UnsupportedLabel.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ UnsupportedLabel.Text = "Unsupported live MMS type";
+ }
+ StatusLabel.Text = supported
+ ? "Select or edit the value, then Write."
+ : "Unsupported type · no Write permitted";
+ DiffLabel.Text = "Current: " + p.CurrentText + " · Review the value before Write.";
+ if (supported && p.Type.MmsType == "boolean") BooleanPicker.Focus();
+ else if (supported) NewValueBox.Focus();
+ }
+ catch (OperationCanceledException)
+ {
+ if (!_lifetime.IsCancellationRequested)
+ StatusLabel.Text = "Preview cancelled; no Write sent.";
+ }
+ catch (Exception ex)
+ {
+ if (!_lifetime.IsCancellationRequested)
+ StatusLabel.Text = "Preview refused: " + ex.Message;
+ }
+ finally
+ {
+ _busy = false;
+ ReadButton.IsEnabled = true;
+ RefreshInput();
+ }
+ }
+
+ private void NewValueBox_TextChanged(object sender, TextChangedEventArgs e)
+ => RefreshInput();
+
+ private void BooleanPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ => RefreshInput();
+
+ private string SelectedInput() =>
+ BooleanPicker.Visibility == Visibility.Visible
+ ? (BooleanPicker.SelectedItem as string ?? "")
+ : NewValueBox.Text;
+
+ private void RefreshInput()
+ {
+ if (WriteButton is null || DiffLabel is null || NewValueBox is null ||
+ BooleanPicker is null) return;
+ WriteButton.IsEnabled = false;
+ if (_busy || _preview is not { IsAvailable: true, Type: not null, OldValue: not null } p ||
+ !(NewValueBox.IsEnabled || BooleanPicker.IsEnabled))
+ return;
+ if (!IedBrowserTypedWriteInput.TryParse(p.Type, SelectedInput(),
+ out var value, out var error) || value is null)
+ {
+ DiffLabel.Text = error;
+ return;
+ }
+ var gva = new MmsVariableAccessAttributesResult
+ {
+ IsSuccess = true, Reference = _reference, TypeSpecification = p.Type
+ };
+ var rejection = IedBrowserTypedWriteAdmission.RejectReason(
+ _reference, gva, p.GvaSignature, value);
+ if (rejection is not null)
+ {
+ DiffLabel.Text = rejection;
+ return;
+ }
+ var oldBytes = MmsDataCodec.Encode(p.OldValue);
+ var newBytes = MmsDataCodec.Encode(value);
+ var changed = !oldBytes.AsSpan().SequenceEqual(newBytes);
+ DiffLabel.Text = $"Current: {p.CurrentText} → New: {MmsDataCodec.ToDisplayString(value)}" +
+ (changed ? " · Ready to Write" : " · Unchanged");
+ WriteButton.IsEnabled = changed;
+ }
+
+ private async void Write_Click(object sender, RoutedEventArgs e)
+ {
+ if (_busy || _preview is not { IsAvailable: true, Type: not null, OldValue: not null } p ||
+ !WriteButton.IsEnabled ||
+ !IedBrowserTypedWriteInput.TryParse(p.Type, SelectedInput(),
+ out var proposed, out _) || proposed is null)
+ return;
+ var gva = new MmsVariableAccessAttributesResult
+ {
+ IsSuccess = true, Reference = _reference, TypeSpecification = p.Type
+ };
+ if (IedBrowserTypedWriteAdmission.RejectReason(
+ _reference, gva, p.GvaSignature, proposed) is not null)
+ {
+ RefreshInput();
+ return;
+ }
+
+ var confirm =
+ "Confirm ONE IEC 61850 MMS data Write on the connected IED?\n\n" +
+ $"IED: {_device.Name}\n" +
+ $"DA: {_reference.Domain}/{_reference.Item} [{_reference.FunctionalConstraint}]\n" +
+ $"Before: {p.CurrentText}\n" +
+ $"After: {MmsDataCodec.ToDisplayString(proposed)}\n\n" +
+ "No automatic retries, rollback, or control Operate. " +
+ "The server may reject Write; readback must independently match.";
+ if (MessageBox.Show(this, confirm, "Confirm live MMS Write",
+ MessageBoxButton.YesNo, MessageBoxImage.Warning,
+ MessageBoxResult.No) != MessageBoxResult.Yes)
+ return;
+
+ _busy = true;
+ WriteButton.IsEnabled = false;
+ NewValueBox.IsEnabled = false;
+ BooleanPicker.IsEnabled = false;
+ ReadButton.IsEnabled = false;
+ StatusLabel.Text = "Writing one exact MMS DA…";
+ try
+ {
+ var step = new IedBrowserTypedWriteStep(
+ _reference, p.GvaSignature, p.OldValue, proposed);
+ var outcome = await _runtime.WriteBrowserDataAttributesAsync(
+ _device, new[] { step }, true, _lifetime.Token);
+ if (_lifetime.IsCancellationRequested) return;
+ var result = outcome.Rows.Single();
+ StatusLabel.Text = $"{result.State}: {result.Message}";
+ DiffLabel.Text =
+ "Write completed or resolved as uncertain. Close and inspect the actual IED value " +
+ "before another attempt; physical process change is NOT implied.";
+ _preview = null; // A new attempt must acquire a NEW live GVA/read + confirmation.
+ }
+ catch (OperationCanceledException)
+ {
+ if (!_lifetime.IsCancellationRequested)
+ StatusLabel.Text = "Write may have been dispatched: outcome UNKNOWN. Read IED manually.";
+ }
+ catch (Exception ex)
+ {
+ if (!_lifetime.IsCancellationRequested)
+ StatusLabel.Text = "Write refused or outcome unresolved: " + ex.Message;
+ }
+ finally
+ {
+ _busy = false;
+ ReadButton.IsEnabled = true;
+ WriteButton.IsEnabled = false;
+ }
+ }
+
+ private void Close_Click(object sender, RoutedEventArgs e) => Close();
+
+ private void Window_Closed(object? sender, EventArgs e) => _lifetime.Cancel();
+}
+).Last() +
+ " [" + _reference.FunctionalConstraint + "]";
+ BooleanPicker.ItemsSource = new[] { "false", "true" };
}
private async void Window_Loaded(object sender, RoutedEventArgs e)
diff --git a/MainWindow.IedBrowser.TypedDataWrite.cs b/MainWindow.IedBrowser.TypedDataWrite.cs
index b132804fe..96e8e852a 100644
--- a/MainWindow.IedBrowser.TypedDataWrite.cs
+++ b/MainWindow.IedBrowser.TypedDataWrite.cs
@@ -127,8 +127,8 @@ private void RefreshIedBrowserWriteButton()
var selectedDo = ctlModel is null && selected is null ? BrowserWritableSelectedDo() : null;
IedBrowserWriteButton.IsEnabled =
ctlModel is not null || selected is not null || selectedDo is not null;
- IedBrowserWriteButton.Content = ctlModel is not null ? "Write ctlModel…" :
- selectedDo is not null ? "Write DAs…" : "Write DA…";
+ // The button always reads "Write". Selection changes the action/tooltip,
+ // never the caption, width or Material pencil visual.
IedBrowserWriteButton.ToolTip = ctlModel is not null
? "Read live ctlModel, choose IEC enum 0–4, confirm a guarded CF Write and verify readback."
: selected is not null
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 96b976935..99cdc4003 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -2050,10 +2050,18 @@
Style="{StaticResource SoftButton}" Margin="0,0,6,0"
IsEnabled="False" Click="IedBrowserControl_Click"
ToolTip="Only enabled when this online DO has an existing live-verified native IEC Control descriptor."/>
-
+ MinWidth="79" IsEnabled="False" Click="IedBrowserWrite_Click"
+ ToolTip="Write the selected live Data Attribute or the exact DO after verification.">
+
+
+
+
+
+
+