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."/> -