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
66 changes: 66 additions & 0 deletions IoListTestingWindow.P0Presentation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Windows;
using System.Windows.Controls;
using ArIED61850Tester.Services.IoTesting;

namespace ArIED61850Tester;

public partial class IoListTestingWindow
{
private static readonly bool P0FatPresentationRegistered = RegisterP0FatPresentation();
private bool _p0FatPresentationInstalled;

private static bool RegisterP0FatPresentation()
{
EventManager.RegisterClassHandler(
typeof(IoListTestingWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(P0FatPresentation_Loaded));
EventManager.RegisterClassHandler(
typeof(Button),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(P0FatCaptureButton_Loaded));
return true;
}

private static void P0FatPresentation_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not IoListTestingWindow window || window._p0FatPresentationInstalled)
return;

window._p0FatPresentationInstalled = true;

// A same-source snapshot can contain stale WorkspaceSelected=false values from an
// earlier FAT session. When the current shared Engineering device explicitly owns
// Static DataSet authority, that authority is stronger than the stale snapshot. Do
// one guarded batch repair before FAT V2 installs its CollectionView filter, so rows
// never paint and then disappear and the repair does not fan out one refresh per row.
if (window.Owner is MainWindow engineeringWindow)
{
foreach (var ied in window.Project.Ieds)
engineeringWindow.RestoreP0SharedStaticDataSetMembership(ied);
}

// The bind-time image may come directly from ARIEC as lowercase Boolean text.
// Canonicalize it before the operator starts interacting with FAT. Subsequent live
// snapshots pass through the same formatter in MainWindow.IoFatRuntimeAuthority.
foreach (var point in window.Project.Ieds.SelectMany(ied => ied.TestPoints))
point.Runtime.CurrentValue = IoFatValuePresentation.Canonicalize(point.Runtime.CurrentValue);
}

private static void P0FatCaptureButton_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not Button button ||
!Equals(button.Content, "✓ Capture") ||
Window.GetWindow(button) is not IoListTestingWindow)
{
return;
}

// Analog Value 1 / Value 2 is automatic in the normal FAT workflow. Keep the
// explicit Recapture context-menu path as an audit/operator override, but never
// expose a per-cell manual Capture button.
button.Visibility = Visibility.Collapsed;
button.IsTabStop = false;
button.IsHitTestVisible = false;
}
}
205 changes: 205 additions & 0 deletions MainWindow.IoFatRuntimeAuthority.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using System.Collections.Concurrent;
using System.Windows;
using System.Windows.Threading;
using ArIED61850Tester.Models;
using ArIED61850Tester.Models.IoTesting;
using ArIED61850Tester.Services.IoTesting;

namespace ArIED61850Tester;

public partial class MainWindow
{
private const int IoFatRuntimeProjectionDrainLimit = 128;
private static readonly bool IoFatRuntimeAuthorityRegistered = RegisterIoFatRuntimeAuthority();

private readonly ConcurrentDictionary<string, Iec61850PointSnapshot> _ioFatPendingRuntimeSnapshots =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, List<IoTestPointPlan>> _ioFatRuntimePointIndex =
new(StringComparer.OrdinalIgnoreCase);
private IoTestProject? _ioFatRuntimeIndexedProject;
private int _ioFatRuntimeIndexedPointCount = -1;
private int _ioFatRuntimeProjectionScheduled;
private bool _ioFatRuntimeAuthorityAttached;

private static bool RegisterIoFatRuntimeAuthority()
{
EventManager.RegisterClassHandler(
typeof(MainWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(IoFatRuntimeAuthority_Loaded));
return true;
}

private static void IoFatRuntimeAuthority_Loaded(object sender, RoutedEventArgs e)
{
if (sender is MainWindow window)
window.AttachIoFatRuntimeAuthority();
}

private void AttachIoFatRuntimeAuthority()
{
if (_ioFatRuntimeAuthorityAttached)
return;

_ioFatRuntimeAuthorityAttached = true;
_runtime.PointUpdated += Runtime_IoFatRuntimeAuthorityPointUpdated;
Closed += IoFatRuntimeAuthorityWindow_Closed;
}

private void IoFatRuntimeAuthorityWindow_Closed(object? sender, EventArgs e)
{
Closed -= IoFatRuntimeAuthorityWindow_Closed;
_runtime.PointUpdated -= Runtime_IoFatRuntimeAuthorityPointUpdated;
_ioFatPendingRuntimeSnapshots.Clear();
_ioFatRuntimePointIndex.Clear();
_ioFatRuntimeIndexedProject = null;
_ioFatRuntimeIndexedPointCount = -1;
_ioFatRuntimeAuthorityAttached = false;
}

private void Runtime_IoFatRuntimeAuthorityPointUpdated(Iec61850PointSnapshot snapshot)
{
if (snapshot?.Point == null || _ioFatSelectionBridgeProject == null)
return;

var key = IoFatRuntimeKey(snapshot.Point.DeviceId, snapshot.Point.IecReference);
if (key.Length == 0)
return;

_ioFatPendingRuntimeSnapshots.AddOrUpdate(
key,
snapshot,
(_, current) => snapshot.Sequence >= current.Sequence ? snapshot : current);
ScheduleIoFatRuntimeProjection();
}

private void ScheduleIoFatRuntimeProjection()
{
if (Interlocked.Exchange(ref _ioFatRuntimeProjectionScheduled, 1) != 0)
return;

Dispatcher.BeginInvoke(
new Action(DrainIoFatRuntimeProjection),
DispatcherPriority.Background);
}

private void DrainIoFatRuntimeProjection()
{
try
{
EnsureIoFatRuntimePointIndex();
var processed = 0;
while (processed < IoFatRuntimeProjectionDrainLimit)
{
var key = _ioFatPendingRuntimeSnapshots.Keys.FirstOrDefault();
if (key == null || !_ioFatPendingRuntimeSnapshots.TryRemove(key, out var snapshot))
break;

ApplyIoFatRuntimeSnapshot(key, snapshot);
processed++;
}
}
finally
{
Interlocked.Exchange(ref _ioFatRuntimeProjectionScheduled, 0);
if (!_ioFatPendingRuntimeSnapshots.IsEmpty && _ioFatSelectionBridgeProject != null)
ScheduleIoFatRuntimeProjection();
}
}

private void EnsureIoFatRuntimePointIndex(bool force = false)
{
var project = _ioFatSelectionBridgeProject;
if (project == null)
{
_ioFatRuntimePointIndex.Clear();
_ioFatRuntimeIndexedProject = null;
_ioFatRuntimeIndexedPointCount = -1;
return;
}

var pointCount = project.Ieds.Sum(ied => ied.TestPoints.Count);
if (!force &&
ReferenceEquals(project, _ioFatRuntimeIndexedProject) &&
pointCount == _ioFatRuntimeIndexedPointCount)
{
return;
}

_ioFatRuntimePointIndex.Clear();
foreach (var ied in project.Ieds)
{
var device = ResolveIoTestDevice(ied.LiveDeviceId)
?? ResolveIoTestDevice(ied.IpAddress)
?? ResolveIoTestDevice(ied.IedName);
if (device == null)
continue;

foreach (var point in ied.TestPoints)
{
foreach (var reference in IoTestLiveBindingService.ImportedReferences(point))
AddIoFatRuntimePointIndex(device.DeviceId, reference, point);

if (!string.IsNullOrWhiteSpace(point.LiveSignalReference))
AddIoFatRuntimePointIndex(device.DeviceId, point.LiveSignalReference, point);
}
}

_ioFatRuntimeIndexedProject = project;
_ioFatRuntimeIndexedPointCount = pointCount;
}

private void AddIoFatRuntimePointIndex(string? deviceId, string? reference, IoTestPointPlan point)
{
var key = IoFatRuntimeKey(deviceId, reference);
if (key.Length == 0)
return;

if (!_ioFatRuntimePointIndex.TryGetValue(key, out var points))
{
points = new List<IoTestPointPlan>();
_ioFatRuntimePointIndex[key] = points;
}

if (!points.Contains(point))
points.Add(point);
}

private void ApplyIoFatRuntimeSnapshot(string key, Iec61850PointSnapshot snapshot)
{
if (!_ioFatRuntimePointIndex.TryGetValue(key, out var points))
{
// A FAT project can be attached before the corresponding Engineering device
// or live binding has finished. Rebuild once on the first unmatched runtime
// sample so newly attached device/reference identities become visible without
// polling, per-cell subscriptions, or collection scans on every update.
EnsureIoFatRuntimePointIndex(force: true);
if (!_ioFatRuntimePointIndex.TryGetValue(key, out points))
return;
}

var value = IoFatValuePresentation.Canonicalize(snapshot.Value);
var quality = string.IsNullOrWhiteSpace(snapshot.Quality) ? "Unknown" : snapshot.Quality.Trim();
var source = string.IsNullOrWhiteSpace(snapshot.SourceMode) ? "Unknown" : snapshot.SourceMode.Trim();
var iedTimestamp = string.IsNullOrWhiteSpace(snapshot.DeviceTimestamp) || snapshot.DeviceTimestamp == "-"
? "—"
: snapshot.DeviceTimestamp.Trim();

foreach (var point in points)
{
point.Runtime.CurrentValue = value;
point.Runtime.CurrentQuality = quality;
point.Runtime.CurrentSource = source;
point.Runtime.CurrentIedTimestamp = iedTimestamp;
}
}

private static string IoFatRuntimeKey(string? deviceId, string? reference)
{
var id = (deviceId ?? string.Empty).Trim();
var normalizedReference = IoTestLiveBindingService.NormalizeReference(reference);
return id.Length == 0 || normalizedReference.Length == 0
? string.Empty
: id + "|" + normalizedReference;
}
}
15 changes: 15 additions & 0 deletions MainWindow.IoTesting.SclAppend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ await ApplyManualSelectionToFatProjectAsync(
window,
resetSelection: true);
}
else if (selectionMode == SclSignalSelectionMode.StaticDataSet && !selectionAlreadyApplied)
{
// A programmatic Static DataSet append does not pass through the selection
// dialog, so establish the report-only authority explicitly here. The same
// device, selected signals and acquisition mode are then visible in both
// Engineering and FAT without any live discovery pass.
foreach (var ied in addedIeds)
{
var device = ResolveIoTestDevice(ied.LiveDeviceId)
?? ResolveIoTestDevice(ied.IpAddress)
?? ResolveIoTestDevice(ied.IedName);
if (device is not null)
ApplyStaticDataSetSelection(device);
}
}
else
{
foreach (var ied in addedIeds)
Expand Down
Loading
Loading