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
25 changes: 25 additions & 0 deletions App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reflection;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace ArIED61850Tester;
Expand All @@ -19,6 +20,12 @@ protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

// Keep every IED card on one reusable template key while replacing the
// legacy bitmap fallback with the compact ARVREL-derived vector fascia.
// Installing this before StartupUri materializes also makes the portable
// smoke test validate that the vector ResourceDictionary is packaged.
InstallArvrelMiniIedFascia();

if (Array.Exists(e.Args, argument =>
string.Equals(argument, "--portable-smoke-test", StringComparison.OrdinalIgnoreCase)))
{
Expand All @@ -37,6 +44,24 @@ protected override void OnStartup(StartupEventArgs e)
new Action(() => _ = AppUpdateCoordinator.RunLazyAsync(_updateCancellation.Token)));
}

private void InstallArvrelMiniIedFascia()
{
var fasciaResources = new ResourceDictionary
{
Source = new Uri(
"/ARSAS;component/Resources/ArvrelMiniIedFascia.xaml",
UriKind.Relative)
};

if (fasciaResources["ArvrelMiniIedRelayFrontPanelTemplate"] is not ControlTemplate template)
{
throw new InvalidOperationException(
"The packaged ARVREL mini IED fascia resource is missing its ControlTemplate.");
}

Resources["IedRelayFrontPanelTemplate"] = template;
}

private static int RunPortableSmokeTest()
{
try
Expand Down
208 changes: 208 additions & 0 deletions Resources/ArvrelMiniIedFascia.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<!--
Compact IED-card adaptation of the ARVREL VirtualRelay visual language.
Source inspiration: masarray/arvrel · Controls/VirtualRelay.

Deliberately not a scaled copy of the full 560x710 operator relay. The card
keeps only the hardware identity that survives at 46-50 px: layered shell,
LED bank, LCD bezel/face, compact keypad and a data-driven state rail.
No microscopic labels or decorative text are rendered at card scale.
-->

<LinearGradientBrush x:Key="ArvrelMiniShellBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#59656C" Offset="0"/>
<GradientStop Color="#343F45" Offset="0.48"/>
<GradientStop Color="#1B2328" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArvrelMiniFaceBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#465158" Offset="0"/>
<GradientStop Color="#364148" Offset="0.58"/>
<GradientStop Color="#293238" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArvrelMiniModuleBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#303B41" Offset="0"/>
<GradientStop Color="#222B30" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArvrelMiniLcdBezelBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#0E1518" Offset="0"/>
<GradientStop Color="#344149" Offset="0.46"/>
<GradientStop Color="#0A1013" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArvrelMiniLcdFaceBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#EEF3EF" Offset="0"/>
<GradientStop Color="#D8E1DB" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArvrelMiniKeyBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#58646B" Offset="0"/>
<GradientStop Color="#303A40" Offset="1"/>
</LinearGradientBrush>

<ControlTemplate x:Key="ArvrelMiniIedRelayFrontPanelTemplate" TargetType="{x:Type Control}">
<Grid Width="50" Height="50" SnapsToDevicePixels="True" UseLayoutRounding="True">
<!-- ARVREL-like layered industrial enclosure. -->
<Border x:Name="ArvrelMiniShell"
Margin="1"
CornerRadius="3.4"
Background="{StaticResource ArvrelMiniShellBrush}"
BorderBrush="#7A878E"
BorderThickness="0.8">
<Border Margin="1.8"
CornerRadius="2.5"
Background="#151D21"
BorderBrush="#536168"
BorderThickness="0.6">
<Border Margin="1.3"
CornerRadius="1.8"
Background="{StaticResource ArvrelMiniFaceBrush}"
BorderBrush="#68757B"
BorderThickness="0.5">
<Grid Margin="2.4,2.5,2.4,4.8">
<Grid.RowDefinitions>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<!-- Calm blue ARVREL accent: visible as hardware identity, not branding text. -->
<Grid Grid.Row="0">
<Border Height="1.15" Width="13"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Background="#2E6F9E"
CornerRadius="0.6"/>
<Border Height="1" Width="5"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="#65737B"
CornerRadius="0.5"/>
</Grid>

<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2"/>
<ColumnDefinition Width="8"/>
</Grid.ColumnDefinitions>

<!-- Reduced ARVREL status lamp bank. The first lamp follows
the IED card Foreground so connection/test state remains live. -->
<Border x:Name="ArvrelMiniLedBank"
Grid.Column="0"
CornerRadius="1.5"
Background="{StaticResource ArvrelMiniModuleBrush}"
BorderBrush="#56636A"
BorderThickness="0.55">
<Grid Margin="1.5,2">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Ellipse Grid.Row="0" Width="2.8" Height="2.8"
HorizontalAlignment="Center"
Fill="{TemplateBinding Foreground}"
Stroke="#B8C2C7" StrokeThickness="0.35"/>
<Ellipse Grid.Row="1" Width="2.55" Height="2.55"
HorizontalAlignment="Center"
Fill="#D6A23B" Opacity="0.82"
Stroke="#89969C" StrokeThickness="0.3"/>
<Ellipse Grid.Row="2" Width="2.55" Height="2.55"
HorizontalAlignment="Center"
Fill="#52636D"
Stroke="#89969C" StrokeThickness="0.3"/>
<Ellipse Grid.Row="3" Width="2.55" Height="2.55"
HorizontalAlignment="Center"
Fill="#52636D"
Stroke="#89969C" StrokeThickness="0.3"/>
</Grid>
</Border>

<!-- LCD keeps the ARVREL pale industrial face and blue header,
but replaces unreadable miniature text with geometric telemetry. -->
<Border x:Name="ArvrelMiniLcd"
Grid.Column="2"
CornerRadius="1.4"
Background="{StaticResource ArvrelMiniLcdBezelBrush}"
BorderBrush="#69767C"
BorderThickness="0.65"
Padding="1.15">
<Border CornerRadius="0.8"
Background="{StaticResource ArvrelMiniLcdFaceBrush}"
BorderBrush="#111719"
BorderThickness="0.4">
<Grid ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="4.2"/>
<RowDefinition Height="*"/>
<RowDefinition Height="3.4"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#4B7086"/>
<Grid Grid.Row="1" Margin="2.2,2,2.2,1.6">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Height="1" Width="9"
HorizontalAlignment="Left"
Background="#5E7478" CornerRadius="0.5"/>
<Border Grid.Row="1" Height="1" Width="12"
HorizontalAlignment="Left"
Background="#718589" CornerRadius="0.5"/>
<Border Grid.Row="2" Height="1" Width="7"
HorizontalAlignment="Left"
Background="#5E7478" CornerRadius="0.5"/>
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="#426F8C"/>
<Border Grid.Column="1" Background="#C7D1CE" BorderBrush="#899995" BorderThickness="0.4,0,0,0"/>
<Border Grid.Column="2" Background="#C7D1CE" BorderBrush="#899995" BorderThickness="0.4,0,0,0"/>
</Grid>
</Grid>
</Border>
</Border>

<!-- Six tactile-key silhouettes preserve the relay/operator-console read
without rendering labels that would alias at card dimensions. -->
<Border x:Name="ArvrelMiniKeypad"
Grid.Column="4"
CornerRadius="1.5"
Background="{StaticResource ArvrelMiniModuleBrush}"
BorderBrush="#56636A"
BorderThickness="0.55">
<UniformGrid Rows="3" Columns="2" Margin="1.15">
<Border Margin="0.45" CornerRadius="0.65" Background="{StaticResource ArvrelMiniKeyBrush}" BorderBrush="#7C898F" BorderThickness="0.35"/>
<Border Margin="0.45" CornerRadius="0.65" Background="{StaticResource ArvrelMiniKeyBrush}" BorderBrush="#7C898F" BorderThickness="0.35"/>
<Border Margin="0.45" CornerRadius="0.65" Background="{StaticResource ArvrelMiniKeyBrush}" BorderBrush="#7C898F" BorderThickness="0.35"/>
<Border Margin="0.45" CornerRadius="0.65" Background="{StaticResource ArvrelMiniKeyBrush}" BorderBrush="#7C898F" BorderThickness="0.35"/>
<Border Margin="0.45" CornerRadius="0.65" Background="#416C85" BorderBrush="#82959E" BorderThickness="0.35"/>
<Border Margin="0.45" CornerRadius="0.65" Background="{StaticResource ArvrelMiniKeyBrush}" BorderBrush="#7C898F" BorderThickness="0.35"/>
</UniformGrid>
</Border>
</Grid>
</Grid>
</Border>
</Border>
</Border>

<!-- Data-driven card state survives the visual port. -->
<Border x:Name="RelayStateRail"
Height="2.2"
Margin="6,0,6,2.2"
VerticalAlignment="Bottom"
Background="{TemplateBinding Foreground}"
CornerRadius="1.1"
Opacity="0.92"/>
</Grid>
</ControlTemplate>
</ResourceDictionary>
86 changes: 86 additions & 0 deletions tests/ARSAS.Tests/ArvrelMiniIedFasciaTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Xml.Linq;

namespace ARSAS.Tests;

public sealed class ArvrelMiniIedFasciaTests
{
[Fact]
public void CompactArvrelFascia_IsVectorOnlyAndKeepsRecognizableRelayHardware()
{
var document = XDocument.Load(FindRepoFile("Resources/ArvrelMiniIedFascia.xaml"));
XNamespace presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml";

var template = document
.Descendants(presentation + "ControlTemplate")
.Single(node =>
(string?)node.Attribute(x + "Key") == "ArvrelMiniIedRelayFrontPanelTemplate");

var root = template.Elements(presentation + "Grid").Single();
Assert.Equal("50", (string?)root.Attribute("Width"));
Assert.Equal("50", (string?)root.Attribute("Height"));

var namedParts = template
.Descendants()
.Select(node => (string?)node.Attribute(x + "Name"))
.Where(name => !string.IsNullOrWhiteSpace(name))
.Cast<string>()
.ToHashSet(StringComparer.Ordinal);

Assert.Contains("ArvrelMiniShell", namedParts);
Assert.Contains("ArvrelMiniLedBank", namedParts);
Assert.Contains("ArvrelMiniLcd", namedParts);
Assert.Contains("ArvrelMiniKeypad", namedParts);
Assert.Contains("RelayStateRail", namedParts);

Assert.Empty(template.Descendants(presentation + "Image"));
Assert.Empty(template.Descendants(presentation + "TextBlock"));
Assert.Contains("{TemplateBinding Foreground}", template.ToString(), StringComparison.Ordinal);
Assert.Contains("#2E6F9E", document.ToString(), StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void AppStartup_ReplacesLegacyCardTemplateWithPackagedArvrelVector()
{
var source = File.ReadAllText(FindRepoFile("App.xaml.cs"));

Assert.Contains("InstallArvrelMiniIedFascia();", source, StringComparison.Ordinal);
Assert.Contains(
"/ARSAS;component/Resources/ArvrelMiniIedFascia.xaml",
source,
StringComparison.Ordinal);
Assert.Contains(
"fasciaResources[\"ArvrelMiniIedRelayFrontPanelTemplate\"]",
source,
StringComparison.Ordinal);
Assert.Contains(
"Resources[\"IedRelayFrontPanelTemplate\"] = template;",
source,
StringComparison.Ordinal);
}

[Fact]
public void ExplorerAndIoFatCards_ContinueToUseOneReusableTemplateKey()
{
var explorer = File.ReadAllText(FindRepoFile("MainWindow.xaml"));
var ioFat = File.ReadAllText(FindRepoFile("IoListTestingWindow.xaml"));

Assert.Contains("Template=\"{StaticResource IedRelayFrontPanelTemplate}\"", explorer, StringComparison.Ordinal);
Assert.Contains("Template=\"{StaticResource IedRelayFrontPanelTemplate}\"", ioFat, StringComparison.Ordinal);
}

private static string FindRepoFile(string relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory != null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
return candidate;
directory = directory.Parent;
}

throw new FileNotFoundException(
$"Could not locate repository file '{relativePath}' from '{AppContext.BaseDirectory}'.");
}
}
Loading