Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
efa98ea
Fix all-passed IED card detection
masarray Jul 30, 2026
7c2a9ab
Simplify IO FAT PDF for customer readability
masarray Jul 30, 2026
ae8847f
Keep customer summary metrics inside report card
masarray Jul 30, 2026
75c00ef
Add customer report and all-pass badge regressions
masarray Jul 30, 2026
1d7c108
Bind IED PASS badge to total tested signal count
masarray Jul 30, 2026
83809ec
Preserve technical PDF metadata without visual report noise
masarray Jul 30, 2026
60e3992
Align PDF regressions with clean visible layout
masarray Jul 30, 2026
9ab6581
Make IO FAT PDF clean, lean, and timestamp focused
masarray Jul 30, 2026
3e8e247
Lock clean lean IO FAT report contract
masarray Jul 30, 2026
3de9648
Fix IO FAT preview text clipping and ellipsis
masarray Jul 30, 2026
55ab71a
Add regression guard against truncated FAT evidence text
masarray Jul 30, 2026
f98f6d8
Fit complete FAT evidence text without truncation
masarray Jul 30, 2026
3339867
Fix false ellipsis in IO FAT evidence rows
masarray Jul 30, 2026
b3252e2
Add regression guard against ellipsized FAT evidence
masarray Jul 30, 2026
df857ff
Preserve Rev3 document control and event-log traceability
masarray Jul 30, 2026
a60b19c
Import event-log-ready FAT workbook schema
masarray Jul 30, 2026
65acad5
Add document-controlled event-log FAT report layout
masarray Jul 30, 2026
392f102
Use executive FAT layout for native PDF
masarray Jul 30, 2026
c719f37
Preserve document control in selected IED preview
masarray Jul 30, 2026
79b638a
Use executive layout in native print preview
masarray Jul 30, 2026
73b49b9
Test Rev3 event-log-ready workbook import
masarray Jul 30, 2026
63f162f
Test executive FAT PDF traceability and signoff
masarray Jul 30, 2026
3df8132
Fix Rev3 IEC reference matching for IO FAT acquisition
masarray Jul 30, 2026
fbf58d0
Match Rev3 FAT points by exact event-log references
masarray Jul 30, 2026
387b762
Cover Rev3 Application and event-log reference matching
masarray Jul 30, 2026
769b858
Test Rev3 reference matching through public selection behavior
masarray Jul 30, 2026
d6032ec
Polish FAT evidence report hierarchy and status presentation
masarray Jul 30, 2026
b875f35
Use modern report font fallback stack
masarray Jul 30, 2026
18573e9
Lock final FAT report visual hierarchy and as-tested status
masarray Jul 30, 2026
c9c22d2
Add persistent workspace mode switch
masarray Jul 31, 2026
cc0538c
Add explicit IO FAT and Engineering mode selector
masarray Jul 31, 2026
bd41fae
Add regression tests for persistent workspace mode navigation
masarray Jul 31, 2026
665fd14
Fix WPF workspace switch thickness constructors
masarray Jul 31, 2026
c9fb4ca
Fix FAT workspace selector padding constructor
masarray Jul 31, 2026
f424a76
Keep loaded IO FAT workspace available across mode switches
masarray Jul 31, 2026
427de7b
Resume loaded FAT window without reimporting workbook
masarray Jul 31, 2026
685f950
Keep loaded FAT ownership registered only while window is alive
masarray Jul 31, 2026
fd2f8b6
Host IO FAT as a persistent non-modal loaded workspace
masarray Jul 31, 2026
e190547
Cover persistent loaded FAT workspace switching
masarray Jul 31, 2026
1301c90
Add one-click route-aware GOOSE capture from IED cards
masarray Jul 31, 2026
caa7a26
Expose real IO FAT preparation progress
masarray Jul 31, 2026
1ec8072
Use smoothed determinate progress on IO FAT IED cards
masarray Jul 31, 2026
136d2dc
Add fast workflow regression checks
masarray Jul 31, 2026
d836d80
Fix MainWindow show race while IO FAT owned window closes
masarray Jul 31, 2026
56b1bb8
Add regression contract for IO FAT close lifecycle
masarray Jul 31, 2026
c79bc11
Add one-click SMV capture from IED cards
masarray Jul 31, 2026
c8afd1f
Auto-start routed SMV snapshot and waveform
masarray Jul 31, 2026
c67f2c4
Add SMV fast workflow regression contract
masarray Jul 31, 2026
fa5bcfa
fix: preserve completed FAT rows during preparation
masarray Aug 1, 2026
8413931
test: protect completed FAT rows before preparation
masarray Aug 1, 2026
c812415
test: normalize FAT continuation source line endings
masarray Aug 1, 2026
57a7c13
Fix RCB DataSet evidence merge and logical-scope display (#135)
masarray Aug 12, 2026
ac1f8fd
Fix IED discovery and add relay FAT procedures
masarray Aug 12, 2026
9ba7877
Import IED SNTP scope metadata
masarray Aug 12, 2026
d0df0ba
Make FAT binding canonical and non-blocking
masarray Aug 12, 2026
97355d7
Cover non-blocking FAT findings
masarray Aug 12, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ build/
.vite/
.cache/

# Local FAT/IFAT exports and diagnostic captures
/output/
/outputs/

* text=auto

*.md text eol=lf
Expand Down
17 changes: 17 additions & 0 deletions App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,23 @@
<GradientStop Color="#75141D" Offset="1"/>
</LinearGradientBrush>

<!-- Reusable protection-relay fascia for IED cards. The artwork follows
the square panel-mount form, LCD, LED banks and cyan navigation keys
of a feeder relay while the state rail remains data-driven. -->
<ControlTemplate x:Key="IedRelayFrontPanelTemplate" TargetType="{x:Type Control}">
<Grid Width="50" Height="50" SnapsToDevicePixels="True">
<Border CornerRadius="2.4" ClipToBounds="True" Background="#F5F7FA">
<Image x:Name="RelayFasciaArtwork"
Source="Assets/ied-protection-relay-fascia.png"
Stretch="Uniform"
RenderOptions.BitmapScalingMode="HighQuality"/>
</Border>
<Border x:Name="RelayStateRail" Height="2.2" Margin="5,0,5,2.8"
VerticalAlignment="Bottom" Background="{TemplateBinding Foreground}"
CornerRadius="1.1" Opacity="0.82"/>
</Grid>
</ControlTemplate>

<!-- Lucide-compatible 24 px outline geometries used by every visible application action. -->
<Geometry x:Key="LucidePlay">M8,5 L19,12 L8,19 Z</Geometry>
<Geometry x:Key="LucideSquare">M6,6 H18 V18 H6 Z</Geometry>
Expand Down
7 changes: 4 additions & 3 deletions ArIED61850Tester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<Product>ARSAS</Product>
<AssemblyTitle>ARSAS - IEC 61850 Engineering Workstation</AssemblyTitle>
<Description>Open-source Windows IEC 61850 engineering workstation for MMS model discovery, reporting, independent multi-IED monitoring, GOOSE subscription, fault-record file transfer, Sampled Values engineering and evidence export, SCL workflows, diagnostics, sequence of events, and guarded control validation.</Description>
<Version>1.6.19</Version>
<AssemblyVersion>1.6.19.0</AssemblyVersion>
<FileVersion>1.6.19.0</FileVersion>
<Version>1.6.20</Version>
<AssemblyVersion>1.6.20.0</AssemblyVersion>
<FileVersion>1.6.20.0</FileVersion>
<PackageProjectUrl>https://github.com/masarray/arsas</PackageProjectUrl>
<RepositoryUrl>https://github.com/masarray/arsas</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand All @@ -40,6 +40,7 @@
<Resource Include="Assets\app-icon.ico" />
<Resource Include="Assets\app-icon-256.png" />
<Resource Include="Assets\gateway-hero.png" />
<Resource Include="Assets\ied-protection-relay-fascia.png" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Update="engines\ARIEC61850.lock.json"
CopyToOutputDirectory="PreserveNewest"
Expand Down
Binary file added Assets/ied-protection-relay-fascia.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,28 @@

Notable public changes to ARSAS are recorded here. Application releases must identify the exact ARIEC61850 engine commit used for build, tests, packaging, and release evidence.

## 1.6.19 — Development
## Unreleased

- Added a controlled relay time-synchronization procedure covering SNTP/NTP, PTP/IEEE 1588, IRIG-B, relay `TimeQuality`, ARSAS UTC capture time, offset, tolerance, and capability-gated acceptance. ARSAS is documented as an evidence client; it does not set the relay clock or act as an SNTP server.
- Added a capability-gated COMTRADE/disturbance-recording procedure and report scope for protection relays with a fault-record service. BCU and other devices without that capability are reported as N/A, not failed.

## 1.6.20 - Development

### Fixed

- Native FAT PDFs no longer claim `AS TESTED` or `CUSTOMER FAT RECORD` while any signal is pending, under review, failed, or absent from the report scope.
- Rev3 imports now retain the physical `Panel Tag` on each IED so the controlled PDF can declare the actual panel scope.

### Added

- Controlled FAT front matter covering document control, scope, test basis, acceptance criteria, executive IED summary, deviations, and handover signatures.
- Result-gated report classifications: `PREVIEW`, `PARTIAL`, `REVIEW REQUIRED`, `FAILED`, and `AS TESTED`.
- Detailed TRUE/FALSE evidence cells with IED timestamp, ARSAS capture timestamp, quality, acquisition source, observation sequence, and connection generation.
- Explicit timestamp-integrity warnings when TRUE and FALSE evidence carry identical IED timestamps.
- Automatic blank IFAT form output for a fresh, never-started scope, with customer-review status, planned signal schedule, expected states, and empty evidence/result fields.
- Automatic transition to a FAT test record as soon as any test attempt starts; final `AS TESTED` remains gated by all scoped signals passing.

## 1.6.19

### Fixed

Expand Down
8 changes: 4 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<Version>1.6.19</Version>
<AssemblyVersion>1.6.19.0</AssemblyVersion>
<FileVersion>1.6.19.0</FileVersion>
<InformationalVersion>1.6.19</InformationalVersion>
<Version>1.6.20</Version>
<AssemblyVersion>1.6.20.0</AssemblyVersion>
<FileVersion>1.6.20.0</FileVersion>
<InformationalVersion>1.6.20</InformationalVersion>
</PropertyGroup>
</Project>
53 changes: 29 additions & 24 deletions IoListTestingWindow.ContextUx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
return;

var selectedIed = SelectedIed;
var preflight = IoTestSessionPreflight.Validate(selectedIed);
if (!preflight.Succeeded)
if (selectedIed == null)
{
ShowActionResult(preflight, "FAT session scope is not ready");
var missingSelection = IoTestSessionPreflight.Validate(null);
ShowActionResult(missingSelection, "FAT session scope is not ready");
return;
}

var enabledReady = selectedIed!.TestPoints
var enabledReady = selectedIed.TestPoints
.Where(point => point.TestEnabled && point.ImportReady)
.ToList();
var completedPoints = enabledReady
Expand Down Expand Up @@ -188,9 +188,27 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
explicitRetest = true;
}

SetPreparingIed(selectedIed, $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102");
var protectedPoints = explicitRetest
? Array.Empty<IoTestPointPlan>()
: completedPoints.ToArray();

// Completed evidence is outside the continuation scope from the first preflight
// through live-model preparation and Session.Start. This prevents a completed row
// that has since disappeared from the relay model from blocking otherwise-valid
// pending rows. The original TestEnabled flags are restored in the outer finally.
foreach (var point in protectedPoints)
point.TestEnabled = false;

try
{
var preflight = IoTestSessionPreflight.Validate(selectedIed);
if (!preflight.Succeeded)
{
ShowActionResult(preflight, "FAT session scope is not ready");
return;
}

SetPreparingIed(selectedIed, $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102");
if (Owner is MainWindow engineeringWindow)
{
var progress = new Progress<string>(message =>
Expand All @@ -214,23 +232,7 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
}
}

var protectedPoints = explicitRetest
? Array.Empty<IoTestPointPlan>()
: completedPoints.ToArray();
IoTestSessionActionResult result;
try
{
foreach (var point in protectedPoints)
point.TestEnabled = false;

result = Session.Start(selectedIed);
}
finally
{
foreach (var point in protectedPoints)
point.TestEnabled = true;
}

var result = Session.Start(selectedIed);
ShowActionResult(result, "FAT evidence session could not start");
RaiseStatusProperties();
RaiseSelectedIedContextProperties();
Expand Down Expand Up @@ -258,6 +260,9 @@ private async void StartSelectedIedSafely_Click(object sender, RoutedEventArgs e
}
finally
{
foreach (var point in protectedPoints)
point.TestEnabled = true;

selectedIed.SetPreparationState(false, selectedIed.LiveStatusText);
SetPreparingIed(null, string.Empty);
RaiseSelectedIedContextProperties();
Expand Down Expand Up @@ -312,9 +317,9 @@ public sealed class IoFatAllPassedVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var enabled = values.Length > 0 && values[0] is int enabledCount ? enabledCount : 0;
var selectedCount = values.Length > 0 && values[0] is int count ? count : 0;
var passed = values.Length > 1 && values[1] is int passedCount ? passedCount : 0;
var allPassed = enabled > 0 && passed == enabled;
var allPassed = selectedCount > 0 && passed >= selectedCount;
if (string.Equals(parameter?.ToString(), "Inverse", StringComparison.OrdinalIgnoreCase))
allPassed = !allPassed;
return allPassed ? Visibility.Visible : Visibility.Collapsed;
Expand Down
179 changes: 179 additions & 0 deletions IoListTestingWindow.RealPreparationProgress.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2026 Ari Sulistiono
// SPDX-License-Identifier: Apache-2.0

using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using ArIED61850Tester.Models.IoTesting;

namespace ArIED61850Tester;

public partial class IoListTestingWindow
{
private static readonly bool RealPreparationProgressRegistered = RegisterRealPreparationProgress();
private readonly Dictionary<IoTestIedPlan, PreparationDisplayState> _preparationDisplayStates = new();
private DispatcherTimer? _preparationProgressTimer;

private static bool RegisterRealPreparationProgress()
{
EventManager.RegisterClassHandler(
typeof(IoListTestingWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(RealPreparationProgress_Loaded));
return true;
}

private static void RealPreparationProgress_Loaded(object sender, RoutedEventArgs e)
{
if (sender is IoListTestingWindow window)
window.InstallRealPreparationProgress();
}

private void InstallRealPreparationProgress()
{
if (_preparationProgressTimer != null)
return;

foreach (var ied in Project.Ieds)
_preparationDisplayStates[ied] = new PreparationDisplayState();

_preparationProgressTimer = new DispatcherTimer(DispatcherPriority.Render)
{
Interval = TimeSpan.FromMilliseconds(50)
};
_preparationProgressTimer.Tick += PreparationProgressTimer_Tick;
_preparationProgressTimer.Start();
Closed += RealPreparationProgress_Closed;

Dispatcher.BeginInvoke(
new Action(ApplyDeterminateCardProgressBars),
DispatcherPriority.Loaded);
}

private void PreparationProgressTimer_Tick(object? sender, EventArgs e)
{
foreach (var ied in Project.Ieds)
{
if (!_preparationDisplayStates.TryGetValue(ied, out var state))
{
state = new PreparationDisplayState();
_preparationDisplayStates[ied] = state;
}

var active = ied.IsPreparing || ReferenceEquals(_preparingIed, ied);
if (active && !state.WasActive)
state.Reset();

state.WasActive = active;
if (!active)
continue;

var snapshot = Owner is MainWindow engineeringWindow
? engineeringWindow.GetIoFatPreparationProgressSnapshot(ied)
: BuildFallbackPreparationSnapshot(ied);
state.Target = Math.Max(state.Target, Math.Clamp(snapshot.Percent, 0d, 100d));
state.Message = snapshot.Message;
state.StepText = snapshot.StepText;
state.AdvanceDisplay();
}

ApplyDeterminateCardProgressBars();
}

private void ApplyDeterminateCardProgressBars()
{
foreach (var progressBar in VisualDescendants<ProgressBar>(this)
.Where(progress => string.Equals(progress.Name, "CardProgress", StringComparison.Ordinal)))
{
// The XAML fallback is indeterminate so old project binaries remain safe.
// Once this behavior is installed every instantiated IED-card bar becomes
// determinate and is driven by real connection/discovery/acquisition state.
progressBar.IsIndeterminate = false;
progressBar.Minimum = 0d;
progressBar.Maximum = 100d;

if (progressBar.DataContext is not IoTestIedPlan ied ||
!_preparationDisplayStates.TryGetValue(ied, out var state))
{
progressBar.Value = 0d;
continue;
}

progressBar.Value = state.Display;
progressBar.ToolTip = string.Join(
" · ",
new[]
{
state.Message,
state.StepText,
$"{state.Display:0}%"
}.Where(value => !string.IsNullOrWhiteSpace(value)));
}
}

private static IoFatPreparationProgressSnapshot BuildFallbackPreparationSnapshot(IoTestIedPlan ied)
{
var message = string.IsNullOrWhiteSpace(ied.PreparationStatusText)
? $"Preparing {ied.IedName}"
: ied.PreparationStatusText;
var percent = message.Contains("live", StringComparison.OrdinalIgnoreCase)
? 100d
: message.Contains("validating", StringComparison.OrdinalIgnoreCase)
? 90d
: message.Contains("arming", StringComparison.OrdinalIgnoreCase)
? 80d
: message.Contains("matching", StringComparison.OrdinalIgnoreCase)
? 68d
: 8d;
return new IoFatPreparationProgressSnapshot(message, percent, "Preparing IED");
}

private void RealPreparationProgress_Closed(object? sender, EventArgs e)
{
Closed -= RealPreparationProgress_Closed;
if (_preparationProgressTimer == null)
return;

_preparationProgressTimer.Stop();
_preparationProgressTimer.Tick -= PreparationProgressTimer_Tick;
_preparationProgressTimer = null;
_preparationDisplayStates.Clear();
}

private sealed class PreparationDisplayState
{
public double Target { get; set; }
public double Display { get; private set; }
public string Message { get; set; } = string.Empty;
public string StepText { get; set; } = string.Empty;
public bool WasActive { get; set; }

public void Reset()
{
Target = 0d;
Display = 0d;
Message = string.Empty;
StepText = string.Empty;
}

public void AdvanceDisplay()
{
var remaining = Target - Display;
if (remaining <= 0.04d)
{
if (remaining > 0d)
Display = Target;
return;
}

// Same visual principle as Engineering discovery cards: 20 FPS,
// ease-out movement, bounded minimum speed, and no artificial loop.
var completing = Target >= 99.9d;
var movement = Math.Clamp(
remaining * (completing ? 0.16d : 0.115d),
0.10d,
completing ? 2.4d : 1.15d);
Display = Math.Min(Target, Display + movement);
}
}
}
Loading