Skip to content
Merged
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
10 changes: 9 additions & 1 deletion MainWindow.IoTesting.SclAppend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,15 @@ await ApplyManualSelectionToFatProjectAsync(
if (device is null)
continue;

ApplyStaticDataSetSelection(device);
device.BeginReportingProgress("Preparing Static DataSet FAT selection…");
try
{
await ApplyStaticDataSetSelection(device);
}
finally
{
device.CompleteReportingProgress();
}

// The new FAT IED has deliberately not been published yet, so project-wide
// selection projection cannot see it. Project the authoritative Engineering
Expand Down
64 changes: 36 additions & 28 deletions MainWindow.SclQuickActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ internal void OpenSclComtradeDownload(Iec61850MonitorDevice device)
internal async Task OpenIedWorkspaceActionsAsync(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);
if (device.IsBusy)
if (device.IsBusy || device.IsReportingProgressActive)
{
SetStatus($"{device.Name}: wait for the current IED task to finish before opening IED Actions.");
return;
Expand Down Expand Up @@ -112,37 +112,45 @@ internal async Task OpenIedWorkspaceActionsAsync(Iec61850MonitorDevice device)

if (dialog.Selection == IedWorkspaceTaskChoice.StaticDataSet)
{
if (device.IsMonitoring)
await StopDeviceMonitorAsync(device);

ApplyStaticDataSetSelection(device);
if (device.SelectedLiveSignalCount == 0)
device.BeginReportingProgress("Preparing Static DataSet reporting…");
try
{
SetStatus($"{device.Name}: Static DataSet selected, but no report-authoritative process leaf is available to monitor.");
return;
}
if (device.IsMonitoring)
await StopDeviceMonitorAsync(device);

if (!device.IsConnected)
{
var connected = device.HasDiscoveryCache && device.Signals.Count > 0
? await ConnectUsingSavedModelAsync(device)
: await ConnectAndConfigureDeviceAsync(device, openWizard: false);
if (!connected)
if (device.IsReportingProgressCanceled) return;
await ApplyStaticDataSetSelection(device);
if (device.IsReportingProgressCanceled) return;

if (device.SelectedLiveSignalCount == 0)
{
SetStatus($"{device.Name}: Static DataSet selected, but no report-authoritative process leaf is available to monitor.");
return;
}

if (!device.IsConnected)
{
var connected = device.HasDiscoveryCache && device.Signals.Count > 0
? await ConnectUsingSavedModelAsync(device)
: await ConnectAndConfigureDeviceAsync(device, openWizard: false);
if (!connected || device.IsReportingProgressCanceled)
return;
}

device.UpdateReportingProgress(80, "Verifying measurement engineering units…");
SetStatus($"{device.Name}: verifying engineering units for selected Static DataSet measurements…");
await _runtime.EnrichSelectedStaticDataSetUnitsAsync(
device, _applicationCancellation.Token);
device.RefreshComputed();
if (device.IsReportingProgressCanceled) return;

if (!device.IsMonitoring)
await StartDeviceMonitorAsync(device);
}
finally
{
device.CompleteReportingProgress();
}

// Unit metadata belongs to the same source-neutral Static DataSet workflow.
// Resolve only selected measurement owners on the active MMS session, before
// any RCB is armed, so discovery and opened-SCL paths expose the same verified
// engineering-unit behavior without disturbing active reporting.
SetStatus($"{device.Name}: verifying engineering units for selected Static DataSet measurements…");
await _runtime.EnrichSelectedStaticDataSetUnitsAsync(
device,
_applicationCancellation.Token);
device.RefreshComputed();

if (!device.IsMonitoring)
await StartDeviceMonitorAsync(device);
return;
}

Expand Down
89 changes: 71 additions & 18 deletions MainWindow.SharedSclWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,50 +63,79 @@ private bool IsSharedStaticDataSetAuthority(Iec61850MonitorDevice device)
return mode;
}

private void ApplyStaticDataSetSelection(Iec61850MonitorDevice device)
private async Task ApplyStaticDataSetSelection(Iec61850MonitorDevice device)
{
// Static DataSet remains the protocol authority established by the report-only
// baseline. Materialize every ARIEC-owned member first, then select exactly one
// presentation/runtime row per literal static membership. A browsed alias carrying
// DataSetReference is not enough authority and must not inflate the live plan.
// WPF-owned SignalDefinition collections stay on their Dispatcher.
// Cooperative slices keep input/render live; matching is indexed by
// exact ARIEC DataSet owner, not a background thread over live bindings.
var clock = System.Diagnostics.Stopwatch.StartNew();
device.UpdateReportingProgress(4, "Preparing ARIEC Static DataSet members…");
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
Comment on lines +72 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard the yielded setup against device removal

If the operator removes an IED while Static DataSet setup yields to the dispatcher, the setup can resume and reconnect/start monitoring that removed device. BeginReportingProgress does not set IsBusy, so the card's Remove button remains enabled and IedRemove_Click accepts the removal. Neither this method nor its caller in MainWindow.SclQuickActions.cs checks cancellation or Devices.Contains(device) before continuing into connection and monitor startup. Previously, selection completed synchronously before another input event could run. Give this workflow an explicit per-device lifetime/admission guard and abort it when removal ends ownership, so an invisible IED cannot retain an active MMS/report session.

AGENTS.md reference: AGENTS.md:L139-L141

Useful? React with 👍 / 👎.

var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(device);
RegisterRecoveredDataSetSignals(device, merge);
var inventoryMs = clock.Elapsed.TotalMilliseconds;
device.UpdateReportingProgress(20, "Matching exact DataSet authority…");
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);

var authoritativeSignals = Iec61850StaticDataSetAuthoritySelection.Build(device);
var authorityMs = clock.Elapsed.TotalMilliseconds;
Iec61850MonitoringModeRegistry.UseStaticDataSetReportOnly(device);
device.UpdateReportingProgress(30, "Selecting verified DataSet member rows…");
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);

// Snapshot object identities before yielding: even a pre-emptive Stop
// cannot invalidate a live WPF collection enumerator mid-operation.
var signalRows = device.Signals.ToArray();
device.BeginBulkSignalSelection();
try
{
foreach (var signal in device.Signals)
var index = 0;
var total = signalRows.Length;
foreach (var signal in signalRows)
{
signal.IsSelected = authoritativeSignals.Contains(signal);
index++;
if ((index & 127) == 0 && index < total)
{
device.UpdateReportingProgress(30d + 30d * index / Math.Max(1, total),
$"Selecting Static DataSet members · {index:N0}/{total:N0}…");
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
}
}
}
finally
{
device.EndBulkSignalSelection();
}
var selectionMs = clock.Elapsed.TotalMilliseconds;
device.UpdateReportingProgress(63, "Synchronizing FAT signal selection…");
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);

SynchronizeAllEngineeringSelectionsToFat(device);
if (_ioFatSelectionBridgeProject is not null)
{
var index = 0;
foreach (var signal in signalRows)
{
SynchronizeEngineeringSelectionToFat(signal, device);
if ((++index & 127) == 0)
await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Background);
}
}
_sharedSclSelectionAuthorityDeviceIds.Add(device.DeviceId);
_sharedSclStaticDataSetAuthorityDeviceIds.Add(device.DeviceId);
SaveSignalSelectionMemory(device);
device.RefreshComputed();
if (_pendingSharedStaticSelectionAssignments > 0)
_pendingSharedStaticSelectionAssignments--;

AddLog(
"INFO",
device.Name,
AddLog("INFO", device.Name,
$"Static DataSet report-only authority selected: {device.SelectedLiveSignalCount} exact runtime member row(s) from {merge.MandatoryCatalogCount} ARIEC static membership descriptor(s); cyclic MMS process polling and dynamic DataSet writes remain disabled.");
AddLog("INFO", device.Name,
$"P0_STATIC_UI_WORK inventoryMs={inventoryMs:0.###} exactAuthorityMs={authorityMs - inventoryMs:0.###} selectionMs={selectionMs - authorityMs:0.###} totalMs={clock.Elapsed.TotalMilliseconds:0.###} signalRows={device.Signals.Count} selected={authoritativeSignals.Count}");

// Make feasibility and first-report proof visible from the initial Engineering
// workflow rather than waiting until FAT is opened. The observer waits for the
// shared monitor to start and never changes acquisition method.
device.UpdateReportingProgress(75, "Checking configured RCB reporting coverage…");
LogStaticDataSetReportFeasibility(device);
_ = ObserveInitialStaticReportEvidenceAsync(device);

// P5 native FAT is a thin view over SelectedDevice.Points. Re-synchronize after
// static DataSet authority refresh so same-IED SCL refreshes are visible without
// reviving the retired Engineering -> legacy IoTest bootstrap.
SynchronizeProductionFatSelectedIed();
}

Expand All @@ -131,7 +160,31 @@ private void MarkSharedSelectionAuthority(Iec61850MonitorDevice device)
// report-only authority instead of silently demoting it to Hybrid.
if (_pendingSharedStaticSelectionAssignments > 0)
{
ApplyStaticDataSetSelection(device);
// Legacy synchronous FAT hook retains exact membership semantics.
// Card/SCL actions use the cooperative responsive path above.
var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(device);
RegisterRecoveredDataSetSignals(device, merge);
var selected = Iec61850StaticDataSetAuthoritySelection.Build(device);
Iec61850MonitoringModeRegistry.UseStaticDataSetReportOnly(device);
device.BeginBulkSignalSelection();
try
{
foreach (var signal in device.Signals)
signal.IsSelected = selected.Contains(signal);
}
finally
{
device.EndBulkSignalSelection();
}
SynchronizeAllEngineeringSelectionsToFat(device);
_sharedSclSelectionAuthorityDeviceIds.Add(device.DeviceId);
_sharedSclStaticDataSetAuthorityDeviceIds.Add(device.DeviceId);
SaveSignalSelectionMemory(device);
device.RefreshComputed();
_pendingSharedStaticSelectionAssignments--;
LogStaticDataSetReportFeasibility(device);
_ = ObserveInitialStaticReportEvidenceAsync(device);
SynchronizeProductionFatSelectedIed();
return;
}

Expand Down
4 changes: 4 additions & 0 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,10 @@
<!-- Opacity does not affect measure/arrange: zero card jiggle. -->
<Setter TargetName="BrowserReadProgressLine" Property="Opacity" Value="1"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsReportingProgressActive}" Value="True">
<Setter TargetName="BrowserReadProgressLine" Property="Opacity" Value="1"/>
<Setter TargetName="BrowserReadProgressLine" Property="Foreground" Value="#16A34A"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
Expand Down
50 changes: 37 additions & 13 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,22 +346,42 @@ private async void OpenScl_Click(object sender, RoutedEventArgs e)
}
else if (selectionMode == SclSignalSelectionMode.StaticDataSet)
{
foreach (var importedDevice in selectableDevices)
ApplyStaticDataSetSelection(importedDevice);

AddLog("INFO", "SCL", $"Static DataSet selection is shared by Engineering and FAT for {selectableDevices.Length} IED workspace(s).");
if (firstImported is { SelectedLiveSignalCount: > 0 })
var staged = new List<Iec61850MonitorDevice>();
try
{
var connected = firstImported.IsConnected ||
await ConnectUsingSavedModelAsync(firstImported);
if (connected && !firstImported.IsMonitoring)
foreach (var importedDevice in selectableDevices)
{
await _runtime.EnrichSelectedStaticDataSetUnitsAsync(
firstImported,
_applicationCancellation.Token);
firstImported.RefreshComputed();
await StartDeviceMonitorAsync(firstImported);
importedDevice.BeginReportingProgress("Preparing Static DataSet reporting…");
staged.Add(importedDevice);
await ApplyStaticDataSetSelection(importedDevice);
if (importedDevice.IsReportingProgressCanceled)
return; // preempted by operator Stop
if (!ReferenceEquals(importedDevice, firstImported))
importedDevice.CompleteReportingProgress();
}

AddLog("INFO", "SCL", $"Static DataSet selection is shared by Engineering and FAT for {selectableDevices.Length} IED workspace(s).");
if (firstImported is { SelectedLiveSignalCount: > 0 } &&
!firstImported.IsReportingProgressCanceled)
{
var connected = firstImported.IsConnected ||
await ConnectUsingSavedModelAsync(firstImported);
if (connected && !firstImported.IsReportingProgressCanceled &&
!firstImported.IsMonitoring)
{
firstImported.UpdateReportingProgress(80, "Verifying engineering units…");
await _runtime.EnrichSelectedStaticDataSetUnitsAsync(
firstImported, _applicationCancellation.Token);
firstImported.RefreshComputed();
if (!firstImported.IsReportingProgressCanceled)
await StartDeviceMonitorAsync(firstImported);
}
}
}
finally
{
foreach (var stagedDevice in staged)
stagedDevice.CompleteReportingProgress();
}
}
else if (selectionMode == SclSignalSelectionMode.BrowseIed)
Expand Down Expand Up @@ -1387,6 +1407,8 @@ private async void IedStopAction_Click(object sender, RoutedEventArgs e)
// Stop is a preemptive safety lane even while Connect/Start is busy.
if (!TryGetDeviceFromButton(sender, out var device) || device.IsStopping) return;
SelectedDevice = device;
if (device.IsReportingProgressActive)
device.CancelReportingProgress(); // Stop preempts report setup.
if (device.IsDemo)
{
StopDemoDevice(device);
Expand Down Expand Up @@ -2405,6 +2427,7 @@ private async Task<bool> StartDeviceMonitorAsync(Iec61850MonitorDevice device, b
RemoveDevicePoints(device.DeviceId);
device.Points.Clear();

device.UpdateReportingProgress(84, "Arming configured BRCB/URCB reports…");
SetStatus($"{device.Name}: arming static/dynamic IEC 61850 reporting…");
var points = await _runtime.StartMonitoringAsync(
device,
Expand All @@ -2425,6 +2448,7 @@ private async Task<bool> StartDeviceMonitorAsync(Iec61850MonitorDevice device, b
foreach (var point in points)
signalProjection.ProjectReportedSignals(device.LiveValues,
projectionAssociation, point.IecReference, point.FunctionalConstraint);
device.UpdateReportingProgress(96, "Applying initial report-backed signal grid…");
device.RefreshComputed();
RaiseWorkspaceCounts();
if (navigateToExplorer)
Expand Down
Loading
Loading