feat: redesign dashboard with Fluent KPI tiles, connection strip, and devices column

- Replace LCD-style readings with a 3×2 KPI tile grid (Fluent card surfaces, 52pt values)
- Add persistent top connection strip with horizontal chips + pump name badge
- Add elapsed test timer (DispatcherTimer, mm:ss) to Test Summary card
- Restyle Test Summary and Active Alarms with Fluent brushes/iconography
- Add Devices column (CAN / K-Line / Bench tiles) between KPI grid and test/alarms
  - Enumerates attached PCAN USB channels via PCAN_ATTACHED_CHANNELS API
  - Enumerates FTDI K-Line adapters via existing FtdiInterface helpers
  - Click to connect/disconnect; confirmation dialog when session active or test running
  - Hover tint: blue = will connect, red = will disconnect; Bench row is read-only stub
- Extend ICanService with SelectedChannel + EnumerateAttachedChannels()
- Expose IKwpService.ConnectedPort for active session device tracking
- Add DeviceRow button style with MultiDataTrigger hover colour logic
- Add 30+ new localization keys (ES + EN) for KPI labels, devices, confirmations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:25:00 +02:00
parent 0280a2fad1
commit 197e9d1775
26 changed files with 1638 additions and 515 deletions

View File

@@ -1,13 +1,21 @@
<Application x:Class="HC_APTBS.App" <Application x:Class="HC_APTBS.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:HC_APTBS"> xmlns:local="clr-namespace:HC_APTBS">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<!-- WPF-UI: must come first so app styles can override -->
<ui:ThemesDictionary Theme="Light"/>
<ui:ControlsDictionary/>
<!-- App resources -->
<ResourceDictionary Source="Resources/Strings.es.xaml"/> <ResourceDictionary Source="Resources/Strings.es.xaml"/>
<ResourceDictionary Source="Resources/Styles.xaml"/> <ResourceDictionary Source="Resources/Styles.xaml"/>
<ResourceDictionary Source="Resources/NavStyles.xaml"/> <ResourceDictionary Source="Resources/NavStyles.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>

View File

@@ -1,5 +1,7 @@
using System.Windows; using System.Windows;
using System.Windows.Media;
using HC_APTBS.Infrastructure.Logging; using HC_APTBS.Infrastructure.Logging;
using Wpf.Ui.Appearance;
using HC_APTBS.Infrastructure.Pcan; using HC_APTBS.Infrastructure.Pcan;
using HC_APTBS.Models; using HC_APTBS.Models;
using HC_APTBS.Services; using HC_APTBS.Services;
@@ -27,6 +29,10 @@ public partial class App : Application
{ {
base.OnStartup(e); base.OnStartup(e);
// Apply WPF-UI theme and accent colour before any UI is constructed.
ApplicationThemeManager.Apply(ApplicationTheme.Light);
ApplicationAccentColorManager.Apply(Color.FromRgb(0x21, 0x96, 0xF3));
var services = new ServiceCollection(); var services = new ServiceCollection();
ConfigureServices(services); ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider(); _serviceProvider = services.BuildServiceProvider();

View File

@@ -40,7 +40,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
<PackageReference Include="System.IO.Ports" Version="10.0.5" /> <PackageReference Include="System.IO.Ports" Version="10.0.5" />
<PackageReference Include="ToggleSwitch" Version="1.2.0" /> <!-- WPF-UI: FluentWindow chrome + unified control theming -->
<PackageReference Include="WPF-UI" Version="3.0.5" />
</ItemGroup> </ItemGroup>
<!-- Embedded images — default report logo fallback --> <!-- Embedded images — default report logo fallback -->

View File

@@ -31,7 +31,7 @@ namespace HC_APTBS.Infrastructure.Pcan
// ── State ──────────────────────────────────────────────────────────────── // ── State ────────────────────────────────────────────────────────────────
private readonly TPCANHandle _channel; private TPCANHandle _channel;
private TPCANBaudrate _baudrate; private TPCANBaudrate _baudrate;
private readonly IAppLogger _log; private readonly IAppLogger _log;
@@ -75,6 +75,19 @@ namespace HC_APTBS.Infrastructure.Pcan
/// <inheritdoc/> /// <inheritdoc/>
public bool IsConnected => !_stopRead; public bool IsConnected => !_stopRead;
/// <inheritdoc/>
public TPCANHandle SelectedChannel
{
get => _channel;
set
{
if (IsConnected)
throw new System.InvalidOperationException(
"Cannot change the CAN channel while connected. Call Disconnect() first.");
_channel = value;
}
}
// ── Construction ───────────────────────────────────────────────────────── // ── Construction ─────────────────────────────────────────────────────────
/// <summary> /// <summary>
@@ -91,6 +104,45 @@ namespace HC_APTBS.Infrastructure.Pcan
_log = logger; _log = logger;
} }
// ── ICanService: discovery ────────────────────────────────────────────────
/// <inheritdoc/>
public System.Collections.Generic.IReadOnlyList<AttachedPcanChannel> EnumerateAttachedChannels()
{
var result = new System.Collections.Generic.List<AttachedPcanChannel>();
try
{
var countStatus = PCANBasic.GetValue(
PCANBasic.PCAN_NONEBUS,
TPCANParameter.PCAN_ATTACHED_CHANNELS_COUNT,
out uint count,
sizeof(uint));
if (countStatus != TPCANStatus.PCAN_ERROR_OK || count == 0)
return result;
var buffer = new TPCANChannelInformation[count];
var infoStatus = PCANBasic.GetValue(
PCANBasic.PCAN_NONEBUS,
TPCANParameter.PCAN_ATTACHED_CHANNELS,
buffer);
if (infoStatus != TPCANStatus.PCAN_ERROR_OK)
return result;
foreach (var ch in buffer)
{
if (ch.device_type == TPCANDevice.PCAN_USB)
result.Add(new AttachedPcanChannel(ch.channel_handle, ch.device_name ?? $"PCAN-USB ({ch.channel_handle:X})"));
}
}
catch (Exception ex)
{
_log.Warning(LogId, $"EnumerateAttachedChannels failed: {ex.Message}");
}
return result;
}
// ── ICanService: lifecycle ──────────────────────────────────────────────── // ── ICanService: lifecycle ────────────────────────────────────────────────
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -1,6 +1,7 @@
<Window x:Class="HC_APTBS.MainWindow" <ui:FluentWindow x:Class="HC_APTBS.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:HC_APTBS.ViewModels" xmlns:vm="clr-namespace:HC_APTBS.ViewModels"
@@ -13,13 +14,18 @@
WindowState="Maximized" WindowState="Maximized"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
FontFamily="Ebrima" FontFamily="Ebrima"
Background="#FFEDEDED"
Closing="OnWindowClosing"> Closing="OnWindowClosing">
<DockPanel> <DockPanel>
<!-- ── WPF-UI custom title bar (replaces OS chrome) ──────────────────── -->
<ui:TitleBar DockPanel.Dock="Top"
Title="{DynamicResource App.Title}"
ShowMaximize="True"
ShowMinimize="True"/>
<!-- ── Persistent app header: pump identification + connection state ──── --> <!-- ── Persistent app header: pump identification + connection state ──── -->
<Border DockPanel.Dock="Top" Background="#FFEDEDED" <Border DockPanel.Dock="Top" Background="Transparent"
BorderBrush="#999" BorderThickness="0,0,0,1" Visibility="Collapsed"> BorderBrush="#999" BorderThickness="0,0,0,1" Visibility="Collapsed">
<Grid Margin="4,2"> <Grid Margin="4,2">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -204,4 +210,4 @@
</TabControl> </TabControl>
</Grid> </Grid>
</DockPanel> </DockPanel>
</Window> </ui:FluentWindow>

View File

@@ -1,6 +1,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Windows; using System.Windows;
using HC_APTBS.ViewModels; using HC_APTBS.ViewModels;
using Wpf.Ui.Controls;
namespace HC_APTBS; namespace HC_APTBS;
@@ -8,7 +9,7 @@ namespace HC_APTBS;
/// Code-behind for MainWindow — minimal: sets DataContext and forwards the /// Code-behind for MainWindow — minimal: sets DataContext and forwards the
/// Closing event to a ViewModel command if needed. /// Closing event to a ViewModel command if needed.
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : FluentWindow
{ {
public MainWindow(MainViewModel viewModel) public MainWindow(MainViewModel viewModel)
{ {

View File

@@ -0,0 +1,13 @@
using Peak.Can.Basic;
using TPCANHandle = System.UInt16;
namespace HC_APTBS.Models
{
/// <summary>
/// Describes a PCAN USB channel that is physically attached to the system,
/// as reported by the PCAN-Basic <c>PCAN_ATTACHED_CHANNELS</c> API call.
/// </summary>
/// <param name="Handle">PCAN channel handle (e.g. <c>PCANBasic.PCAN_USBBUS1</c>).</param>
/// <param name="DisplayName">Human-readable name returned by the PCAN driver (e.g. "PCAN-USB (1)").</param>
public sealed record AttachedPcanChannel(TPCANHandle Handle, string DisplayName);
}

View File

@@ -42,6 +42,40 @@
<sys:String x:Key="Dashboard.Action.Stop">Stop</sys:String> <sys:String x:Key="Dashboard.Action.Stop">Stop</sys:String>
<sys:String x:Key="Dashboard.Action.EmergencyStop">EMERGENCY STOP</sys:String> <sys:String x:Key="Dashboard.Action.EmergencyStop">EMERGENCY STOP</sys:String>
<!-- ── Dashboard KPI tile labels ──────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Kpi.Rpm">Bench RPM</sys:String>
<sys:String x:Key="Dashboard.Kpi.Qdelivery">Q delivery</sys:String>
<sys:String x:Key="Dashboard.Kpi.P1">Pressure P1</sys:String>
<sys:String x:Key="Dashboard.Kpi.P2">Pressure P2</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tin">Oil in T</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tout">Oil out T</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Rpm">rpm</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.CcS">cc/s</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Celsius">°C</sys:String>
<!-- Dashboard test summary extras -->
<sys:String x:Key="Dashboard.TestSummary.Elapsed">Elapsed:</sys:String>
<!-- Dashboard connection chip extras -->
<sys:String x:Key="Dashboard.Conn.Pump.Label">Pump:</sys:String>
<sys:String x:Key="Dashboard.Conn.NoPump">No pump</sys:String>
<!-- ── Dashboard Devices column ─────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Devices">Devices</sys:String>
<sys:String x:Key="Dashboard.Devices.Can">CAN</sys:String>
<sys:String x:Key="Dashboard.Devices.Kline">K-Line</sys:String>
<sys:String x:Key="Dashboard.Devices.Bench">Bench</sys:String>
<sys:String x:Key="Dashboard.Devices.Refresh">Refresh</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Idle">Available</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Connected">Connected</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Active">Session active</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Failed">Error</sys:String>
<sys:String x:Key="Dashboard.Devices.None">No devices found</sys:String>
<sys:String x:Key="Dashboard.Devices.BenchRow">Bench controller</sys:String>
<!-- Confirmation dialogs -->
<sys:String x:Key="Devices.Confirm.Title">Confirm device change</sys:String>
<sys:String x:Key="Devices.Confirm.Body.Active">The {0} session is active. Disconnect?</sys:String>
<sys:String x:Key="Devices.Confirm.Body.TestRunning">A test is running. Changing device state may abort it. Continue?</sys:String>
<!-- ── Status bar / connection indicators ───────────────────────────── --> <!-- ── Status bar / connection indicators ───────────────────────────── -->
<sys:String x:Key="Status.Label">Status:</sys:String> <sys:String x:Key="Status.Label">Status:</sys:String>
<sys:String x:Key="Status.Can">CAN</sys:String> <sys:String x:Key="Status.Can">CAN</sys:String>

View File

@@ -42,6 +42,40 @@
<sys:String x:Key="Dashboard.Action.Stop">Detener</sys:String> <sys:String x:Key="Dashboard.Action.Stop">Detener</sys:String>
<sys:String x:Key="Dashboard.Action.EmergencyStop">PARADA DE EMERGENCIA</sys:String> <sys:String x:Key="Dashboard.Action.EmergencyStop">PARADA DE EMERGENCIA</sys:String>
<!-- ── Dashboard KPI tile labels ──────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Kpi.Rpm">RPM del banco</sys:String>
<sys:String x:Key="Dashboard.Kpi.Qdelivery">Q. caudal</sys:String>
<sys:String x:Key="Dashboard.Kpi.P1">Presión P1</sys:String>
<sys:String x:Key="Dashboard.Kpi.P2">Presión P2</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tin">T. entrada</sys:String>
<sys:String x:Key="Dashboard.Kpi.Tout">T. salida</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Rpm">rpm</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.CcS">cc/s</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Bar">bar</sys:String>
<sys:String x:Key="Dashboard.Kpi.Unit.Celsius">°C</sys:String>
<!-- Dashboard test summary extras -->
<sys:String x:Key="Dashboard.TestSummary.Elapsed">Duración:</sys:String>
<!-- Dashboard connection chip extras -->
<sys:String x:Key="Dashboard.Conn.Pump.Label">Bomba:</sys:String>
<sys:String x:Key="Dashboard.Conn.NoPump">Sin bomba</sys:String>
<!-- ── Dashboard Devices column ─────────────────────────────────────── -->
<sys:String x:Key="Dashboard.Devices">Dispositivos</sys:String>
<sys:String x:Key="Dashboard.Devices.Can">CAN</sys:String>
<sys:String x:Key="Dashboard.Devices.Kline">K-Line</sys:String>
<sys:String x:Key="Dashboard.Devices.Bench">Banco</sys:String>
<sys:String x:Key="Dashboard.Devices.Refresh">Actualizar</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Idle">Disponible</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Connected">Conectado</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Active">Sesión activa</sys:String>
<sys:String x:Key="Dashboard.Devices.State.Failed">Error</sys:String>
<sys:String x:Key="Dashboard.Devices.None">Sin dispositivos</sys:String>
<sys:String x:Key="Dashboard.Devices.BenchRow">Controlador del banco</sys:String>
<!-- Confirmation dialogs -->
<sys:String x:Key="Devices.Confirm.Title">Confirmar cambio de dispositivo</sys:String>
<sys:String x:Key="Devices.Confirm.Body.Active">La sesión {0} está activa. ¿Desea desconectar?</sys:String>
<sys:String x:Key="Devices.Confirm.Body.TestRunning">Hay una prueba en ejecución. Cambiar el estado del dispositivo puede interrumpirla. ¿Continuar?</sys:String>
<!-- ── Status bar / connection indicators ───────────────────────────── --> <!-- ── Status bar / connection indicators ───────────────────────────── -->
<sys:String x:Key="Status.Label">Estado:</sys:String> <sys:String x:Key="Status.Label">Estado:</sys:String>
<sys:String x:Key="Status.Can">CAN</sys:String> <sys:String x:Key="Status.Can">CAN</sys:String>

View File

@@ -42,11 +42,169 @@
<Setter Property="Margin" Value="2,4"/> <Setter Property="Margin" Value="2,4"/>
</Style> </Style>
<!-- Relay toggle button style --> <!-- Relay toggle button style — inherits WPF-UI Button appearance -->
<Style x:Key="RelayButton" TargetType="Button"> <Style x:Key="RelayButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Padding" Value="6,3"/> <Setter Property="Padding" Value="6,3"/>
<Setter Property="Margin" Value="3,2"/> <Setter Property="Margin" Value="3,2"/>
<Setter Property="FontSize" Value="11"/> <Setter Property="FontSize" Value="11"/>
</Style> </Style>
<!--
Base style for state-indicating toggle buttons (on/off with colour feedback).
Uses a custom ControlTemplate so that TemplateBinding Background is honoured
in ALL states — including IsChecked=True. Derived styles add IsChecked triggers
with a custom Background colour (green, amber, blue, etc.) and those colours are
guaranteed to propagate, unlike WPF-UI's own ToggleButton template which has an
internal IsChecked trigger that would override a simple BasedOn + Background setter.
Hover/pressed feedback is applied via a transparent overlay so it works on any
background colour without conflicting with the checked state.
-->
<Style x:Key="FluentStateToggle" TargetType="ToggleButton">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Border x:Name="Root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="True"/>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
<!-- Transparent overlay — darkens on hover/pressed over any background colour -->
<Border x:Name="HoverOverlay" CornerRadius="4" Background="Transparent"
IsHitTestVisible="False"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="HoverOverlay" Property="Background" Value="#18000000"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="HoverOverlay" Property="Background" Value="#30000000"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Dashboard KPI tile styles ─────────────────────────────────────── -->
<Style x:Key="KpiTile" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="6"/>
<Setter Property="MinHeight" Value="140"/>
</Style>
<Style x:Key="KpiHeaderText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style x:Key="KpiValueText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="52"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style>
<Style x:Key="KpiUnitText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Margin" Value="6,0,0,6"/>
</Style>
<!-- Connection strip chip -->
<Style x:Key="ConnChip" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="14"/>
<Setter Property="Padding" Value="14,6"/>
<Setter Property="Margin" Value="0,0,8,0"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
</Style>
<!-- Status dot (10 px ellipse; Fill overridden by DataTrigger for state colour) -->
<Style x:Key="StatusDot" TargetType="Ellipse">
<Setter Property="Width" Value="10"/>
<Setter Property="Height" Value="10"/>
<Setter Property="Fill" Value="{DynamicResource SystemFillColorNeutralBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="6,0,4,0"/>
</Style>
<!-- ── Device row button — hover tint indicates intent (connect=blue, disconnect=red) -->
<Style x:Key="DeviceRow" TargetType="Button">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Margin" Value="0,0,0,4"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<!-- Hover + disconnected → accent blue (will connect) -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsMouseOver}" Value="True"/>
<Condition Binding="{Binding IsConnected}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentFillColorSecondaryBrush}"/>
</MultiDataTrigger>
<!-- Hover + connected → critical red (will disconnect) -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsMouseOver}" Value="True"/>
<Condition Binding="{Binding IsConnected}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SystemFillColorCriticalBackgroundBrush}"/>
</MultiDataTrigger>
<!-- Disabled → muted (bench placeholder) -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Opacity" Value="0.5"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -41,6 +41,22 @@ namespace HC_APTBS.Services
/// <summary>True when the CAN read thread is running and the channel is open.</summary> /// <summary>True when the CAN read thread is running and the channel is open.</summary>
bool IsConnected { get; } bool IsConnected { get; }
/// <summary>
/// The PCAN channel handle that will be used on the next <see cref="Connect"/> call.
/// Defaults to the channel supplied at construction.
/// Throws <see cref="System.InvalidOperationException"/> when set while <see cref="IsConnected"/> is true.
/// </summary>
TPCANHandle SelectedChannel { get; set; }
// ── Discovery ─────────────────────────────────────────────────────────────
/// <summary>
/// Enumerates PCAN USB channels that are physically attached to the system.
/// Returns an empty list if no adapters are connected or if the PCAN-Basic DLL
/// is unavailable. Never throws.
/// </summary>
System.Collections.Generic.IReadOnlyList<AttachedPcanChannel> EnumerateAttachedChannels();
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
/// <summary> /// <summary>

View File

@@ -110,6 +110,12 @@ namespace HC_APTBS.Services
/// </summary> /// </summary>
string? DetectKLinePort(); string? DetectKLinePort();
/// <summary>
/// The FTDI serial number of the device that is currently holding an open
/// K-Line session, or <see langword="null"/> when no session is active.
/// </summary>
string? ConnectedPort { get; }
// ── Mid-read notifications ──────────────────────────────────────────── // ── Mid-read notifications ────────────────────────────────────────────
/// <summary> /// <summary>

View File

@@ -73,6 +73,9 @@ namespace HC_APTBS.Services.Impl
/// <inheritdoc/> /// <inheritdoc/>
public KLineConnectionState KLineState => _kLineState; public KLineConnectionState KLineState => _kLineState;
/// <inheritdoc/>
public string? ConnectedPort => _connectedPort;
// ── Constructor ─────────────────────────────────────────────────────────── // ── Constructor ───────────────────────────────────────────────────────────
/// <param name="logger">Application logger.</param> /// <param name="logger">Application logger.</param>
@@ -289,7 +292,7 @@ namespace HC_APTBS.Services.Impl
Report(85, "Reading fault codes..."); Report(85, "Reading fault codes...");
kwp.KeepAlive(); kwp.KeepAlive();
var faultCodes = kwp.ReadFaultCodes(); var faultCodes = kwp.ReadFaultCodes();
result[KlineKeys.Errors] = faultCodes.Count > 0 result[KlineKeys.Errors] = faultCodes?.Count > 0
? string.Join(Environment.NewLine, faultCodes) ? string.Join(Environment.NewLine, faultCodes)
: KlineKeys.NoErrors; : KlineKeys.NoErrors;
@@ -418,7 +421,7 @@ namespace HC_APTBS.Services.Impl
Report(85, "Reading fault codes..."); Report(85, "Reading fault codes...");
kwp.KeepAlive(); kwp.KeepAlive();
var faultCodes = kwp.ReadFaultCodes(); var faultCodes = kwp.ReadFaultCodes();
result[KlineKeys.Errors] = faultCodes.Count > 0 result[KlineKeys.Errors] = faultCodes?.Count > 0
? string.Join(Environment.NewLine, faultCodes) ? string.Join(Environment.NewLine, faultCodes)
: KlineKeys.NoErrors; : KlineKeys.NoErrors;
@@ -621,14 +624,15 @@ namespace HC_APTBS.Services.Impl
return await Task.Run(() => return await Task.Run(() =>
{ {
_busLock.Wait();
try try
{ {
_log.Info(LogId, "TryFastUnlock: sending unlock command over K-Line"); _log.Info(LogId, "TryFastUnlock: sending unlock command over K-Line");
var packets = _sessionKwp.SendCustom( var packets = _sessionKwp!.SendCustom(
new List<byte> { 0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00 }); new List<byte> { 0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00 });
bool nak = packets.Count == 1 bool nak = packets.Count == 1
&& packets[0] is HC_APTBS.Infrastructure.Kwp.Packets.NakPacket; && packets[0] is NakPacket;
_log.Info(LogId, $"TryFastUnlock: {(nak ? "NAK pump rejected" : "ACK pump unlocked")}"); _log.Info(LogId, $"TryFastUnlock: {(nak ? "NAK pump rejected" : "ACK pump unlocked")}");
return !nak; return !nak;
@@ -638,6 +642,10 @@ namespace HC_APTBS.Services.Impl
_log.Warning(LogId, $"TryFastUnlock failed: {ex.Message}"); _log.Warning(LogId, $"TryFastUnlock failed: {ex.Message}");
return false; return false;
} }
finally
{
_busLock.Release();
}
}); });
} }
@@ -686,33 +694,33 @@ namespace HC_APTBS.Services.Impl
{ {
while (!ct.IsCancellationRequested) while (!ct.IsCancellationRequested)
{ {
// Non-blocking try-acquire: if an operation holds the lock
// we skip this cycle — the operation itself keeps the bus alive.
if (await _busLock.WaitAsync(0, ct))
{
try
{
_sessionKwp!.KeepAlive();
}
catch (OperationCanceledException) { return; }
catch (Exception ex)
{
_log.Error(LogId, $"Keep-alive failed: {ex.Message}");
CleanupSession();
SetState(KLineConnectionState.Failed);
return;
}
finally
{
_busLock.Release();
}
}
try try
{ {
await Task.Delay(KeepAliveIntervalMs, ct); await Task.Delay(KeepAliveIntervalMs, ct);
} }
catch (OperationCanceledException) { return; } catch (OperationCanceledException) { return; }
// Non-blocking try-acquire: if an operation holds the lock
// we skip this cycle — the operation itself keeps the bus alive.
if (!await _busLock.WaitAsync(0, ct))
continue;
try
{
_sessionKwp!.KeepAlive();
}
catch (OperationCanceledException) { return; }
catch (Exception ex)
{
_log.Error(LogId, $"Keep-alive failed: {ex.Message}");
CleanupSession();
SetState(KLineConnectionState.Failed);
return;
}
finally
{
_busLock.Release();
}
} }
} }
@@ -746,7 +754,7 @@ namespace HC_APTBS.Services.Impl
var codes = _sessionKwp.ReadFaultCodes(); var codes = _sessionKwp.ReadFaultCodes();
_sessionKwp.KeepAlive(); _sessionKwp.KeepAlive();
Report(100, "Done."); Report(100, "Done.");
return codes.Count > 0 return codes?.Count > 0
? string.Join(Environment.NewLine, codes) ? string.Join(Environment.NewLine, codes)
: KlineKeys.NoErrors; : KlineKeys.NoErrors;
} }
@@ -773,7 +781,7 @@ namespace HC_APTBS.Services.Impl
var codes = _sessionKwp.ReadFaultCodes(); var codes = _sessionKwp.ReadFaultCodes();
_sessionKwp.KeepAlive(); _sessionKwp.KeepAlive();
Report(100, "Done."); Report(100, "Done.");
return codes.Count > 0 return codes?.Count > 0
? string.Join(Environment.NewLine, codes) ? string.Join(Environment.NewLine, codes)
: KlineKeys.NoErrors; : KlineKeys.NoErrors;
} }

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Models; using HC_APTBS.Models;
@@ -65,6 +66,14 @@ namespace HC_APTBS.ViewModels
private CancellationTokenSource? _testCts; private CancellationTokenSource? _testCts;
// ── Test elapsed timer ────────────────────────────────────────────────────
/// <summary>Ticks every second while a test is running to update <see cref="TestElapsed"/>.</summary>
private DispatcherTimer? _testTimer;
/// <summary>UTC start time of the current test; used by the timer to compute elapsed duration.</summary>
private DateTime _testStartedUtc;
// ── Unlock tracking ────────────────────────────────────────────────────── // ── Unlock tracking ──────────────────────────────────────────────────────
/// <summary>CTS for the currently running immobilizer unlock, if any.</summary> /// <summary>CTS for the currently running immobilizer unlock, if any.</summary>
@@ -139,9 +148,6 @@ namespace HC_APTBS.ViewModels
/// <summary>Diagnostic Trouble Code list for the Pump page §3.b sub-section.</summary> /// <summary>Diagnostic Trouble Code list for the Pump page §3.b sub-section.</summary>
public DtcListViewModel DtcList { get; } public DtcListViewModel DtcList { get; }
/// <summary>Auth gate for the Pump page §3.d Adaptation sub-section.</summary>
public AuthGateViewModel AdaptationAuth { get; }
// ── Page ViewModels (thin façades over the child VMs above) ─────────────── // ── Page ViewModels (thin façades over the child VMs above) ───────────────
/// <summary>Dashboard navigation page VM.</summary> /// <summary>Dashboard navigation page VM.</summary>
@@ -203,14 +209,12 @@ namespace HC_APTBS.ViewModels
AngleDisplay = new AngleDisplayViewModel(configService); AngleDisplay = new AngleDisplayViewModel(configService);
DashboardAlarms = new DashboardAlarmsViewModel(configService.Settings.Alarms); DashboardAlarms = new DashboardAlarmsViewModel(configService.Settings.Alarms);
DtcList = new DtcListViewModel(kwpService, localizationService, logger); DtcList = new DtcListViewModel(kwpService, localizationService, logger);
AdaptationAuth = new AuthGateViewModel(configService, localizationService);
// Page ViewModels are thin façades over the child VMs above; they hold a // Page ViewModels are thin façades over the child VMs above; they hold a
// reference back to this coordinator so page XAML can bind MainViewModel-owned // reference back to this coordinator so page XAML can bind MainViewModel-owned
// values via {Binding Root.X}. // values via {Binding Root.X}.
DashboardPage = new DashboardPageViewModel(this); DashboardPage = new DashboardPageViewModel(this, canService, kwpService);
BenchPage = new BenchPageViewModel(this, benchService, configService); BenchPage = new BenchPageViewModel(this, benchService, configService);
PumpPage = new PumpPageViewModel(this, DtcList, AdaptationAuth); PumpPage = new PumpPageViewModel(this, DtcList);
TestsPage = new TestsPageViewModel(this, configService, localizationService); TestsPage = new TestsPageViewModel(this, configService, localizationService);
SettingsPage = new SettingsPageViewModel(configService, localizationService); SettingsPage = new SettingsPageViewModel(configService, localizationService);
SettingsPage.SettingsSaved += OnSettingsSaved; SettingsPage.SettingsSaved += OnSettingsSaved;
@@ -548,6 +552,9 @@ namespace HC_APTBS.ViewModels
/// <summary>True when the current test results have been saved to a report.</summary> /// <summary>True when the current test results have been saved to a report.</summary>
[ObservableProperty] private bool _isTestSaved = true; [ObservableProperty] private bool _isTestSaved = true;
/// <summary>Elapsed time since the current test started. Updated every second; retains last value when idle.</summary>
[ObservableProperty] private TimeSpan _testElapsed;
// ── Commands: test ──────────────────────────────────────────────────────── // ── Commands: test ────────────────────────────────────────────────────────
/// <summary>Starts the test sequence for the current pump.</summary> /// <summary>Starts the test sequence for the current pump.</summary>
@@ -806,6 +813,14 @@ namespace HC_APTBS.ViewModels
{ {
IsTestRunning = true; IsTestRunning = true;
VerboseStatus = _loc.GetString("Test.Started"); VerboseStatus = _loc.GetString("Test.Started");
_testStartedUtc = DateTime.UtcNow;
TestElapsed = TimeSpan.Zero;
_testTimer = new DispatcherTimer(
TimeSpan.FromSeconds(1),
DispatcherPriority.Normal,
(_, _) => TestElapsed = DateTime.UtcNow - _testStartedUtc,
App.Current.Dispatcher);
TestPanel.IsRunning = true; TestPanel.IsRunning = true;
TestPanel.ResetResults(); TestPanel.ResetResults();
ResultDisplay.Clear(); ResultDisplay.Clear();
@@ -817,6 +832,9 @@ namespace HC_APTBS.ViewModels
private void OnTestFinished(bool interrupted, bool success) private void OnTestFinished(bool interrupted, bool success)
=> App.Current.Dispatcher.Invoke(() => => App.Current.Dispatcher.Invoke(() =>
{ {
_testTimer?.Stop();
_testTimer = null;
IsTestRunning = false; IsTestRunning = false;
LastTestSuccess = !interrupted && success; LastTestSuccess = !interrupted && success;
VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail")); VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail"));

View File

@@ -0,0 +1,287 @@
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using HC_APTBS.Infrastructure.Kwp;
using HC_APTBS.Models;
using HC_APTBS.Services;
using HC_APTBS.ViewModels.Dialogs;
using HC_APTBS.Views.Dialogs;
namespace HC_APTBS.ViewModels.Pages
{
/// <summary>Kind of physical device represented by a <see cref="DeviceItem"/>.</summary>
public enum DeviceKind { Can, KLine, Bench }
/// <summary>
/// Represents a single detected device row in the Devices column.
/// </summary>
public sealed partial class DeviceItem : ObservableObject
{
/// <summary>What kind of adapter this row represents.</summary>
public DeviceKind Kind { get; init; }
/// <summary>
/// Device identity string: PCAN handle as hex for CAN, FTDI serial for K-Line,
/// empty for the single Bench placeholder row.
/// </summary>
public string Id { get; init; } = "";
/// <summary>PCAN channel handle (only meaningful when <see cref="Kind"/> is <see cref="DeviceKind.Can"/>).</summary>
public ushort CanHandle { get; init; }
/// <summary>Display name shown in the tile row.</summary>
[ObservableProperty] private string _name = "";
/// <summary>Short state label (right-aligned in the row).</summary>
[ObservableProperty] private string _stateLabel = "";
/// <summary>True when this device is the currently active connection.</summary>
[ObservableProperty] private bool _isConnected;
/// <summary>False for the Bench placeholder, which cannot be clicked.</summary>
public bool IsEnabled { get; init; } = true;
}
/// <summary>
/// ViewModel for the Devices column on the Dashboard.
///
/// <para>Owns three observable collections of <see cref="DeviceItem"/> (one per kind)
/// and exposes toggle/refresh commands. Communicates with CAN and K-Line services
/// through <see cref="ICanService"/> and <see cref="IKwpService"/> and reads
/// cross-cutting state (test running, connection flags) via <see cref="MainViewModel"/>.</para>
/// </summary>
public sealed partial class DashboardDevicesViewModel : ObservableObject
{
private readonly MainViewModel _root;
private readonly ICanService _can;
private readonly IKwpService _kwp;
/// <summary>Detected PCAN USB channels.</summary>
public ObservableCollection<DeviceItem> CanDevices { get; } = new();
/// <summary>Detected FTDI K-Line adapters.</summary>
public ObservableCollection<DeviceItem> KLineDevices { get; } = new();
/// <summary>Single bench-controller placeholder row.</summary>
public ObservableCollection<DeviceItem> BenchDevices { get; } = new();
public DashboardDevicesViewModel(MainViewModel root, ICanService can, IKwpService kwp)
{
_root = root;
_can = can;
_kwp = kwp;
root.PropertyChanged += OnRootPropertyChanged;
RefreshCanDevices();
RefreshKLineDevices();
RefreshBenchDevices();
}
// ── Refresh commands ──────────────────────────────────────────────────────
/// <summary>Re-enumerates attached PCAN USB channels.</summary>
[RelayCommand]
private void RefreshCanDevices()
{
CanDevices.Clear();
try
{
var channels = _can.EnumerateAttachedChannels();
foreach (var ch in channels)
{
CanDevices.Add(new DeviceItem
{
Kind = DeviceKind.Can,
Id = ch.Handle.ToString("X"),
CanHandle = ch.Handle,
Name = ch.DisplayName,
IsConnected = _root.IsCanConnected && _can.SelectedChannel == ch.Handle,
StateLabel = GetCanStateLabel(_root.IsCanConnected && _can.SelectedChannel == ch.Handle),
});
}
}
catch { /* PCAN DLL missing — leave list empty */ }
}
/// <summary>Re-enumerates connected FTDI K-Line adapters.</summary>
[RelayCommand]
private void RefreshKLineDevices()
{
KLineDevices.Clear();
try
{
uint count = FtdiInterface.GetDevicesCount();
if (count == 0) return;
var list = new FT_DEVICE_INFO_NODE[count];
FtdiInterface.GetDeviceList(list);
foreach (var dev in list)
{
if (string.IsNullOrEmpty(dev.SerialNumber)) continue;
bool connected = _kwp.KLineState == KLineConnectionState.Connected
&& _kwp.ConnectedPort == dev.SerialNumber;
bool failed = _kwp.KLineState == KLineConnectionState.Failed
&& _kwp.ConnectedPort == dev.SerialNumber;
KLineDevices.Add(new DeviceItem
{
Kind = DeviceKind.KLine,
Id = dev.SerialNumber,
Name = string.IsNullOrEmpty(dev.Description)
? dev.SerialNumber
: $"{dev.Description} ({dev.SerialNumber})",
IsConnected = connected,
StateLabel = GetKLineStateLabel(connected, failed),
});
}
}
catch { /* FTDI DLL not loaded — leave list empty */ }
}
private void RefreshBenchDevices()
{
BenchDevices.Clear();
BenchDevices.Add(new DeviceItem
{
Kind = DeviceKind.Bench,
Id = "bench",
Name = Str("Dashboard.Devices.BenchRow"),
IsConnected = _root.IsBenchConnected,
StateLabel = GetBenchStateLabel(_root.IsBenchConnected),
IsEnabled = false,
});
}
// ── Toggle command ────────────────────────────────────────────────────────
/// <summary>
/// Connects or disconnects the device represented by <paramref name="item"/>.
/// Shows a confirmation dialog when a session is active or a test is running.
/// </summary>
[RelayCommand]
private async Task ToggleDevice(DeviceItem? item)
{
if (item is null || !item.IsEnabled) return;
bool testRunning = _root.IsTestRunning;
bool sessionActive = item.IsConnected;
if (testRunning)
{
if (!Confirm(Str("Devices.Confirm.Title"), Str("Devices.Confirm.Body.TestRunning")))
return;
}
else if (sessionActive)
{
string body = string.Format(Str("Devices.Confirm.Body.Active"),
item.Kind == DeviceKind.Can ? "CAN" : "K-Line");
if (!Confirm(Str("Devices.Confirm.Title"), body))
return;
}
switch (item.Kind)
{
case DeviceKind.Can:
await ToggleCanAsync(item);
break;
case DeviceKind.KLine:
await ToggleKLineAsync(item);
break;
}
}
private async Task ToggleCanAsync(DeviceItem item)
{
if (item.IsConnected)
{
_root.DisconnectCanCommand.Execute(null);
}
else
{
try { _can.SelectedChannel = item.CanHandle; }
catch { return; }
_root.ConnectCanCommand.Execute(null);
}
await Task.Delay(600); // allow liveness event propagation
RefreshCanDevices();
}
private async Task ToggleKLineAsync(DeviceItem item)
{
if (item.IsConnected)
{
_kwp.Disconnect();
}
else
{
try { await _kwp.ConnectAsync(item.Id, CancellationToken.None); }
catch { /* ConnectAsync throws on init failure — leave state as-is */ }
}
RefreshKLineDevices();
}
// ── State change wiring ───────────────────────────────────────────────────
private void OnRootPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(MainViewModel.IsCanConnected):
App.Current.Dispatcher.Invoke(RefreshCanDevices);
break;
case nameof(MainViewModel.KLineState):
App.Current.Dispatcher.Invoke(RefreshKLineDevices);
break;
case nameof(MainViewModel.IsBenchConnected):
App.Current.Dispatcher.Invoke(SyncBenchState);
break;
}
}
private void SyncBenchState()
{
if (BenchDevices.Count == 0) return;
var row = BenchDevices[0];
row.IsConnected = _root.IsBenchConnected;
row.StateLabel = GetBenchStateLabel(_root.IsBenchConnected);
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static bool Confirm(string title, string message)
{
var vm = new ConfirmDialogViewModel
{
Title = title,
Message = message,
ConfirmText = Str("Common.Yes"),
CancelText = Str("Common.Cancel"),
};
var dlg = new ConfirmDialog(vm) { Owner = Application.Current.MainWindow };
dlg.ShowDialog();
return vm.Accepted;
}
private static string Str(string key)
=> Application.Current.TryFindResource(key) as string ?? key;
private static string GetCanStateLabel(bool connected)
=> connected ? Str("Dashboard.Devices.State.Connected") : Str("Dashboard.Devices.State.Idle");
private static string GetKLineStateLabel(bool connected, bool failed)
{
if (connected) return Str("Dashboard.Devices.State.Active");
if (failed) return Str("Dashboard.Devices.State.Failed");
return Str("Dashboard.Devices.State.Idle");
}
private static string GetBenchStateLabel(bool connected)
=> connected ? Str("Dashboard.Devices.State.Connected") : Str("Dashboard.Devices.State.Idle");
}
}

View File

@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using HC_APTBS.Services;
namespace HC_APTBS.ViewModels.Pages namespace HC_APTBS.ViewModels.Pages
{ {
@@ -17,9 +18,13 @@ namespace HC_APTBS.ViewModels.Pages
/// <summary>Active alarm aggregator bound to the Dashboard alarm list.</summary> /// <summary>Active alarm aggregator bound to the Dashboard alarm list.</summary>
public DashboardAlarmsViewModel Alarms => Root.DashboardAlarms; public DashboardAlarmsViewModel Alarms => Root.DashboardAlarms;
public DashboardPageViewModel(MainViewModel root) /// <summary>Devices column ViewModel — CAN, K-Line, and Bench device tiles.</summary>
public DashboardDevicesViewModel Devices { get; }
public DashboardPageViewModel(MainViewModel root, ICanService can, IKwpService kwp)
{ {
Root = root; Root = root;
Devices = new DashboardDevicesViewModel(root, can, kwp);
} }
} }
} }

View File

@@ -42,9 +42,6 @@ namespace HC_APTBS.ViewModels.Pages
/// <summary>Diagnostic Trouble Code list (§3.b).</summary> /// <summary>Diagnostic Trouble Code list (§3.b).</summary>
public DtcListViewModel DtcList { get; } public DtcListViewModel DtcList { get; }
/// <summary>Adaptation sub-section auth gate (§3.d).</summary>
public AuthGateViewModel AdaptationAuth { get; }
/// <summary>DFI management (§3.d).</summary> /// <summary>DFI management (§3.d).</summary>
public DfiManageViewModel DfiViewModel => Root.DfiViewModel; public DfiManageViewModel DfiViewModel => Root.DfiViewModel;
@@ -82,12 +79,10 @@ namespace HC_APTBS.ViewModels.Pages
/// <summary>Constructs the page VM and subscribes to relevant Root state changes.</summary> /// <summary>Constructs the page VM and subscribes to relevant Root state changes.</summary>
public PumpPageViewModel( public PumpPageViewModel(
MainViewModel root, MainViewModel root,
DtcListViewModel dtcList, DtcListViewModel dtcList)
AuthGateViewModel adaptationAuth)
{ {
Root = root; Root = root;
DtcList = dtcList; DtcList = dtcList;
AdaptationAuth = adaptationAuth;
// Initialise derived flags from the current Root state. // Initialise derived flags from the current Root state.
RefreshDerivedFlags(); RefreshDerivedFlags();
@@ -120,11 +115,6 @@ namespace HC_APTBS.ViewModels.Pages
IsUnlockApplicable = Root.CurrentPump != null && Root.CurrentPump.UnlockType != 0; IsUnlockApplicable = Root.CurrentPump != null && Root.CurrentPump.UnlockType != 0;
OnPropertyChanged(nameof(UnlockVm)); OnPropertyChanged(nameof(UnlockVm));
// When the pump changes, re-lock the adaptation gate — a new operator
// may be handling a different pump.
if (AdaptationAuth.IsAuthenticated)
AdaptationAuth.LockCommand.Execute(null);
// Drop any stale DTCs from the previous pump. // Drop any stale DTCs from the previous pump.
DtcList.Reset(); DtcList.Reset();
} }

View File

@@ -1,240 +1,345 @@
<UserControl x:Class="HC_APTBS.Views.Pages.DashboardPage" <UserControl x:Class="HC_APTBS.Views.Pages.DashboardPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls" xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="700" d:DesignWidth="980"> d:DesignHeight="860" d:DesignWidth="1740">
<!-- <!--
Dashboard — operator "at a glance" landing page. Dashboard — operator "at a glance" landing page.
DataContext: DashboardPageViewModel. Read-first. Only Start / Stop / E-Stop are interactive. DataContext: DashboardPageViewModel.
Layout: connection strip (top) | KPI tiles (left) + devices column (centre) + test/alarms column (right) | action bar (bottom)
Connect/Disconnect for CAN and K-Line is performed via the Devices column tiles.
--> -->
<UserControl.Resources> <UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis"/> <BooleanToVisibilityConverter x:Key="BoolToVis"/>
<!-- Section card style -->
<Style x:Key="DashCard" TargetType="Border">
<Setter Property="Background" Value="#FAFAFA"/>
<Setter Property="BorderBrush" Value="#DDD"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="Margin" Value="4"/>
</Style>
<Style x:Key="DashHeader" TargetType="TextBlock">
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#333"/>
<Setter Property="Margin" Value="0,0,0,6"/>
</Style>
</UserControl.Resources> </UserControl.Resources>
<Grid Margin="6"> <Grid Margin="12">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"/> <RowDefinition Height="Auto"/> <!-- connection strip -->
<RowDefinition Height="Auto"/> <!-- footer: quick actions --> <RowDefinition Height="*"/> <!-- main content -->
<RowDefinition Height="Auto"/> <!-- action bar -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="340"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ── Left column: readings ───────────────────────────────────────── --> <!-- ── Row 0: Connection strip ─────────────────────────────────────── -->
<Border Grid.Row="0" Grid.Column="0" Style="{StaticResource DashCard}"> <uc:DashboardConnectionView Grid.Row="0"/>
<StackPanel>
<TextBlock Text="{DynamicResource Dashboard.Readings}" Style="{StaticResource DashHeader}"/>
<uc:DashboardReadingsView/>
</StackPanel>
</Border>
<!-- ── Right column: connections + test summary + alarms ───────────── --> <!-- ── Row 1: KPI grid (left) + devices column (centre) + test/alarms (right) -->
<Grid Grid.Row="0" Grid.Column="1"> <Grid Grid.Row="1" Margin="0,0,0,8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="2.2*"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="0.9*" MinWidth="260"/>
<ColumnDefinition Width="1*" MinWidth="380"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Connections --> <!-- KPI readings grid -->
<Border Grid.Row="0" Grid.Column="0" Style="{StaticResource DashCard}"> <uc:DashboardReadingsView Grid.Column="0" Margin="0,0,4,0"/>
<uc:DashboardConnectionView/>
</Border>
<!-- Test summary --> <!-- Devices column: CAN / K-Line / Bench device tiles -->
<Border Grid.Row="0" Grid.Column="1" Style="{StaticResource DashCard}"> <uc:DashboardDevicesView Grid.Column="1" Margin="4,0,4,0"/>
<StackPanel>
<TextBlock Text="{DynamicResource Dashboard.TestSummary}" Style="{StaticResource DashHeader}"/>
<!-- Active test view (when running) --> <!-- Right column: test summary + alarms -->
<StackPanel Visibility="{Binding Root.IsTestRunning, Converter={StaticResource BoolToVis}}"> <Grid Grid.Column="2" Margin="4,0,0,0">
<Grid Margin="0,2"> <Grid.RowDefinitions>
<Grid.ColumnDefinitions> <RowDefinition Height="Auto"/>
<ColumnDefinition Width="80"/> <RowDefinition Height="*"/>
<ColumnDefinition Width="*"/> </Grid.RowDefinitions>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.TestActive}" Foreground="#555" FontSize="12"/>
<TextBlock Grid.Column="1" Text="{Binding Root.TestPanel.TestName}"
FontWeight="SemiBold" FontSize="13" TextTrimming="CharacterEllipsis"/>
</Grid>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.TestPhase}" Foreground="#555" FontSize="12"/>
<TextBlock Grid.Column="1" Text="{Binding Root.CurrentPhaseName}"
FontFamily="Consolas" FontSize="13" TextTrimming="CharacterEllipsis"/>
</Grid>
<TextBlock Text="{Binding Root.VerboseStatus}" FontStyle="Italic"
Foreground="#666" FontSize="11" Margin="0,6,0,0"
TextWrapping="Wrap"/>
</StackPanel>
<!-- Idle view (last result or "no test run") --> <!-- ── Test summary card ────────────────────────────────────── -->
<Border Grid.Row="0"
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
BorderThickness="1" CornerRadius="8" Padding="16" Margin="0,0,0,8">
<StackPanel> <StackPanel>
<StackPanel.Style> <!-- Card header -->
<Style TargetType="StackPanel"> <DockPanel Margin="0,0,0,12">
<Setter Property="Visibility" Value="Visible"/> <ui:SymbolIcon DockPanel.Dock="Left" Symbol="Timer24" FontSize="16"
<Style.Triggers> Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
<DataTrigger Binding="{Binding Root.IsTestRunning}" Value="True"> Margin="0,0,8,0" VerticalAlignment="Center"/>
<Setter Property="Visibility" Value="Collapsed"/> <TextBlock Text="{DynamicResource Dashboard.TestSummary}"
</DataTrigger> FontSize="14" FontWeight="SemiBold"
</Style.Triggers> Foreground="{DynamicResource TextFillColorPrimaryBrush}"
</Style> VerticalAlignment="Center"/>
</StackPanel.Style> </DockPanel>
<TextBlock Text="{DynamicResource Dashboard.NoTestRunning}" <!-- Running view -->
Foreground="#888" FontStyle="Italic" FontSize="12"/> <Grid Visibility="{Binding Root.IsTestRunning, Converter={StaticResource BoolToVis}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Margin="0,8,0,0" Padding="8,4" CornerRadius="3" <!-- Test name + spinner -->
HorizontalAlignment="Left"> <DockPanel Grid.Row="0" Margin="0,0,0,8">
<Border.Style> <ui:ProgressRing DockPanel.Dock="Right" Width="22" Height="22"
<Style TargetType="Border"> IsIndeterminate="True"
<Setter Property="Background" Value="#D62828"/> VerticalAlignment="Center" Margin="8,0,0,0"/>
<Setter Property="Visibility" Value="Collapsed"/> <TextBlock Text="{Binding Root.TestPanel.TestName}"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
</DockPanel>
<!-- Phase name -->
<Grid Grid.Row="1" Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.TestPhase}"
FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Root.CurrentPhaseName}"
FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
</Grid>
<!-- Elapsed timer -->
<Grid Grid.Row="2" Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.TestSummary.Elapsed}"
FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Text="{Binding Root.TestElapsed, StringFormat={}{0:mm\\:ss}}"
FontSize="13" FontWeight="SemiBold" FontFamily="Consolas"
Foreground="{DynamicResource AccentTextFillColorPrimaryBrush}"
VerticalAlignment="Center"/>
</Grid>
<!-- Verbose status -->
<TextBlock Grid.Row="3" Text="{Binding Root.VerboseStatus}"
FontSize="11" FontStyle="Italic"
Foreground="{DynamicResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap" MaxHeight="32"
TextTrimming="CharacterEllipsis"/>
</Grid>
<!-- Idle view -->
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers> <Style.Triggers>
<MultiDataTrigger> <DataTrigger Binding="{Binding Root.IsTestRunning}" Value="True">
<MultiDataTrigger.Conditions> <Setter Property="Visibility" Value="Collapsed"/>
<Condition Binding="{Binding Root.IsTestSaved}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
<DataTrigger Binding="{Binding Root.LastTestSuccess}" Value="True">
<Setter Property="Background" Value="#26C200"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </StackPanel.Style>
<TextBlock Foreground="White" FontWeight="Bold" FontSize="12">
<TextBlock.Style> <TextBlock Text="{DynamicResource Dashboard.NoTestRunning}"
<Style TargetType="TextBlock"> Foreground="{DynamicResource TextFillColorSecondaryBrush}"
<Setter Property="Text" Value="{DynamicResource Dashboard.LastTestFail}"/> FontStyle="Italic" FontSize="12" Margin="0,0,0,10"/>
<!-- Last test result pill — only visible while result is unsaved -->
<Border Padding="10,6" CornerRadius="6" HorizontalAlignment="Left">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{DynamicResource SystemFillColorCriticalBackgroundBrush}"/>
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers> <Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Root.IsTestSaved}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible"/>
</MultiDataTrigger>
<DataTrigger Binding="{Binding Root.LastTestSuccess}" Value="True"> <DataTrigger Binding="{Binding Root.LastTestSuccess}" Value="True">
<Setter Property="Text" Value="{DynamicResource Dashboard.LastTestPass}"/> <Setter Property="Background" Value="{DynamicResource SystemFillColorSuccessBackgroundBrush}"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</TextBlock.Style> </Border.Style>
</TextBlock> <StackPanel Orientation="Horizontal">
</Border> <ui:SymbolIcon FontSize="14" Margin="0,0,6,0" VerticalAlignment="Center">
<ui:SymbolIcon.Style>
<Style TargetType="ui:SymbolIcon">
<Setter Property="Symbol" Value="DismissCircle24"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorCriticalBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.LastTestSuccess}" Value="True">
<Setter Property="Symbol" Value="CheckmarkCircle24"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ui:SymbolIcon.Style>
</ui:SymbolIcon>
<TextBlock FontWeight="SemiBold" FontSize="12" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.LastTestFail}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorCriticalBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.LastTestSuccess}" Value="True">
<Setter Property="Text" Value="{DynamicResource Dashboard.LastTestPass}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </Border>
</Border>
<!-- Alarms (spans both columns of the right grid) --> <!-- ── Active alarms card ───────────────────────────────────── -->
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource DashCard}"> <Border Grid.Row="1"
<DockPanel> Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
<TextBlock DockPanel.Dock="Top" Text="{DynamicResource Dashboard.Alarms}" BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
Style="{StaticResource DashHeader}"/> BorderThickness="1" CornerRadius="8" Padding="16">
<DockPanel>
<!-- Card header -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,12">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="AlertUrgent24" FontSize="16"
Foreground="{DynamicResource SystemFillColorCautionBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Alarms}"
FontSize="14" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
VerticalAlignment="Center"/>
</DockPanel>
<!-- "System OK" banner when no alarms --> <!-- System OK banner (no active alarms) -->
<Border Background="#26C200" CornerRadius="3" Padding="10,6" <Border DockPanel.Dock="Top"
HorizontalAlignment="Left" VerticalAlignment="Top" Background="{DynamicResource SystemFillColorSuccessBackgroundBrush}"
Visibility="{Binding Alarms.IsClear, Converter={StaticResource BoolToVis}}"> BorderBrush="{DynamicResource SystemFillColorSuccessBrush}"
<TextBlock Text="{DynamicResource Dashboard.AlarmsNone}" BorderThickness="1" CornerRadius="6" Padding="12,8" Margin="0,0,0,4"
Foreground="White" FontWeight="Bold" FontSize="12"/> Visibility="{Binding Alarms.IsClear, Converter={StaticResource BoolToVis}}">
</Border> <StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="CheckmarkCircle24" FontSize="16"
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.AlarmsNone}"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource SystemFillColorSuccessBrush}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- Active alarm list --> <!-- Active alarm list -->
<ItemsControl ItemsSource="{Binding Alarms.ActiveAlarms}"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl.Style> <ScrollViewer.Style>
<Style TargetType="ItemsControl"> <Style TargetType="ScrollViewer">
<Setter Property="Visibility" Value="Visible"/> <Setter Property="Visibility" Value="Visible"/>
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding Alarms.IsClear}" Value="True"> <DataTrigger Binding="{Binding Alarms.IsClear}" Value="True">
<Setter Property="Visibility" Value="Collapsed"/> <Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</ItemsControl.Style> </ScrollViewer.Style>
<ItemsControl.ItemTemplate> <ItemsControl ItemsSource="{Binding Alarms.ActiveAlarms}">
<DataTemplate> <ItemsControl.ItemTemplate>
<Border Margin="0,2" Padding="8,4" CornerRadius="3"> <DataTemplate>
<Border.Style> <Border Margin="0,3" Padding="10,8" CornerRadius="6">
<Style TargetType="Border"> <Border.Style>
<Setter Property="Background" Value="#FFB020"/> <!-- warning --> <Style TargetType="Border">
<Style.Triggers> <Setter Property="Background" Value="{DynamicResource SystemFillColorCautionBackgroundBrush}"/>
<DataTrigger Binding="{Binding IsCritical}" Value="True"> <Style.Triggers>
<Setter Property="Background" Value="#D62828"/> <!-- critical --> <DataTrigger Binding="{Binding IsCritical}" Value="True">
</DataTrigger> <Setter Property="Background" Value="{DynamicResource SystemFillColorCriticalBackgroundBrush}"/>
</Style.Triggers> </DataTrigger>
</Style> </Style.Triggers>
</Border.Style> </Style>
<Grid> </Border.Style>
<Grid.ColumnDefinitions> <Grid>
<ColumnDefinition Width="Auto"/> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/>
<TextBlock Text="●" Foreground="White" FontSize="14" VerticalAlignment="Center" Margin="0,0,6,0"/> </Grid.ColumnDefinitions>
<TextBlock Grid.Column="1" Text="{Binding Description}" <ui:SymbolIcon VerticalAlignment="Center" Margin="0,0,8,0">
Foreground="White" FontWeight="SemiBold" FontSize="12" <ui:SymbolIcon.Style>
VerticalAlignment="Center" TextWrapping="Wrap"/> <Style TargetType="ui:SymbolIcon">
<TextBlock Grid.Column="2" Foreground="#FFF" FontSize="11" FontFamily="Consolas" <Setter Property="Symbol" Value="AlertOn24"/>
VerticalAlignment="Center" Margin="6,0,0,0"> <Setter Property="FontSize" Value="16"/>
<Run Text="bit "/><Run Text="{Binding Bit, Mode=OneWay}"/> <Setter Property="Foreground" Value="{DynamicResource SystemFillColorCautionBrush}"/>
</TextBlock> <Style.Triggers>
</Grid> <DataTrigger Binding="{Binding IsCritical}" Value="True">
</Border> <Setter Property="Symbol" Value="ErrorCircle24"/>
</DataTemplate> <Setter Property="Foreground" Value="{DynamicResource SystemFillColorCriticalBrush}"/>
</ItemsControl.ItemTemplate> </DataTrigger>
</ItemsControl> </Style.Triggers>
</DockPanel> </Style>
</Border> </ui:SymbolIcon.Style>
</ui:SymbolIcon>
<TextBlock Grid.Column="1" Text="{Binding Description}"
FontWeight="SemiBold" FontSize="12"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
VerticalAlignment="Center" TextWrapping="Wrap"/>
<TextBlock Grid.Column="2"
FontSize="11" FontFamily="Consolas"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
VerticalAlignment="Center" Margin="10,0,0,0">
<Run Text="bit "/><Run Text="{Binding Bit, Mode=OneWay}"/>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
</Grid>
</Grid> </Grid>
<!-- ── Footer: quick actions ───────────────────────────────────────── --> <!-- ── Row 2: Action bar ────────────────────────────────────────────── -->
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" <Border Grid.Row="2"
Background="#F0F0F0" BorderBrush="#CCC" BorderThickness="0,1,0,0" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}"
Padding="8" Margin="4,4,4,4"> BorderThickness="0,1,0,0" Padding="4,10">
<DockPanel LastChildFill="False"> <DockPanel LastChildFill="False">
<Button DockPanel.Dock="Left"
Content="{DynamicResource Dashboard.Action.StartTest}"
Command="{Binding Root.StartTestCommand}"
Height="44" MinWidth="140" FontSize="13" FontWeight="Bold"
Foreground="DarkGreen" Margin="0,0,6,0"
ToolTipService.ShowOnDisabled="True"
ToolTip="{DynamicResource Dashboard.Action.StartTest.Tip}"/>
<Button DockPanel.Dock="Left" <!-- E-Stop — pinned right, visually dominant -->
Content="{DynamicResource Dashboard.Action.Stop}" <ui:Button DockPanel.Dock="Right"
Command="{Binding Root.StopTestCommand}" Appearance="Danger"
Height="44" MinWidth="120" FontSize="13" FontWeight="Bold" Content="{DynamicResource Dashboard.Action.EmergencyStop}"
Foreground="DarkRed" Margin="0,0,6,0"/> Command="{Binding Root.EmergencyStopCommand}"
FontSize="15" FontWeight="Bold"
MinWidth="240" Height="50">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="DismissCircle24"/>
</ui:Button.Icon>
</ui:Button>
<!-- Start Test -->
<ui:Button DockPanel.Dock="Left"
Appearance="Primary"
Content="{DynamicResource Dashboard.Action.StartTest}"
Command="{Binding Root.StartTestCommand}"
MinWidth="160" Height="46"
Margin="0,0,8,0"
ToolTipService.ShowOnDisabled="True"
ToolTip="{DynamicResource Dashboard.Action.StartTest.Tip}">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="PlayCircle24"/>
</ui:Button.Icon>
</ui:Button>
<!-- Stop -->
<ui:Button DockPanel.Dock="Left"
Appearance="Secondary"
Content="{DynamicResource Dashboard.Action.Stop}"
Command="{Binding Root.StopTestCommand}"
MinWidth="120" Height="46">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="RecordStop24"/>
</ui:Button.Icon>
</ui:Button>
<!-- E-Stop pinned right, red, always visible -->
<Button DockPanel.Dock="Right"
Content="{DynamicResource Dashboard.Action.EmergencyStop}"
Command="{Binding Root.EmergencyStopCommand}"
Height="44" MinWidth="200" FontSize="14" FontWeight="Bold"
Foreground="White" Background="#D62828" BorderBrush="#8B0000"
BorderThickness="2"/>
</DockPanel> </DockPanel>
</Border> </Border>
</Grid> </Grid>

View File

@@ -151,25 +151,8 @@
</ListBoxItem> </ListBoxItem>
<ListBoxItem Tag="{x:Static vm:PumpSubPage.Adaptation}" <ListBoxItem Tag="{x:Static vm:PumpSubPage.Adaptation}"
IsEnabled="{Binding IsPumpSelected}"> IsEnabled="{Binding IsPumpSelected}">
<StackPanel Orientation="Horizontal"> <TextBlock Text="{DynamicResource PumpSub.Adaptation}"
<TextBlock Text="{DynamicResource PumpSub.Adaptation}" Style="{StaticResource SubNavText}"/>
Style="{StaticResource SubNavText}"/>
<TextBlock Text=" 🔒" FontSize="11"
VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.AdaptationAuth.IsAuthenticated,
RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</ListBoxItem> </ListBoxItem>
<ListBoxItem Tag="{x:Static vm:PumpSubPage.Unlock}"> <ListBoxItem Tag="{x:Static vm:PumpSubPage.Unlock}">
<ListBoxItem.Style> <ListBoxItem.Style>
@@ -216,27 +199,21 @@
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<!-- 3d Adaptation (auth-gated) --> <!-- 3d Adaptation -->
<TabItem> <TabItem>
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<uc:AuthGateView DataContext="{Binding AdaptationAuth}"> <Border Background="#FAFAFA" BorderBrush="#DDD"
<uc:AuthGateView.GatedContent> BorderThickness="1" CornerRadius="4"
<Border Background="#FAFAFA" BorderBrush="#DDD" Padding="12" Margin="6">
BorderThickness="1" CornerRadius="4" <StackPanel>
Padding="12" Margin="6"> <TextBlock Text="{DynamicResource PumpSub.Adaptation}"
<StackPanel> FontSize="15" FontWeight="SemiBold"
<TextBlock Text="{DynamicResource PumpSub.Adaptation}" Foreground="#333" Margin="0,0,0,8"/>
FontSize="15" FontWeight="SemiBold" <uc:DfiManageView DataContext="{Binding DfiViewModel}"/>
Foreground="#333" Margin="0,0,0,8"/> <Separator Margin="0,10"/>
<uc:DfiManageView DataContext="{Binding DataContext.DfiViewModel, <uc:PumpControlView DataContext="{Binding PumpControl}"/>
RelativeSource={RelativeSource AncestorType=UserControl}}"/> </StackPanel>
<Separator Margin="0,10"/> </Border>
<uc:PumpControlView DataContext="{Binding DataContext.PumpControl,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</StackPanel>
</Border>
</uc:AuthGateView.GatedContent>
</uc:AuthGateView>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>

View File

@@ -17,7 +17,7 @@
<ToggleButton IsChecked="{Binding BenchControl.IsDirectionRight}" <ToggleButton IsChecked="{Binding BenchControl.IsDirectionRight}"
Height="32" FontSize="12" FontWeight="SemiBold"> Height="32" FontSize="12" FontWeight="SemiBold">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton" BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Content" Value="{DynamicResource Bench.Left}"/> <Setter Property="Content" Value="{DynamicResource Bench.Left}"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
@@ -72,13 +72,13 @@
<ToggleButton IsChecked="{Binding BenchControl.IsOilPumpOn}" <ToggleButton IsChecked="{Binding BenchControl.IsOilPumpOn}"
Height="32" FontSize="12" FontWeight="SemiBold"> Height="32" FontSize="12" FontWeight="SemiBold">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton" BasedOn="{StaticResource FluentStateToggle}">
<Setter Property="Content" Value="{DynamicResource Bench.OilOff}"/> <Setter Property="Content" Value="{DynamicResource Bench.OilOff}"/>
<Setter Property="Background" Value="LightGray"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{DynamicResource Bench.OilOn}"/> <Setter Property="Content" Value="{DynamicResource Bench.OilOn}"/>
<Setter Property="Background" Value="#80FF80"/> <Setter Property="Background" Value="#26C200"/>
<Setter Property="Foreground" Value="White"/>
</Trigger> </Trigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>

View File

@@ -1,158 +1,232 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.DashboardConnectionView" <UserControl x:Class="HC_APTBS.Views.UserControls.DashboardConnectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:HC_APTBS.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:HC_APTBS.Models"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="160" d:DesignWidth="220"> d:DesignHeight="72" d:DesignWidth="1100">
<!-- <!--
Connection status block for the Dashboard. Connection status strip for the Dashboard — four horizontal chips + pump name badge.
DataContext is DashboardPageViewModel; pills read from Root.X. DataContext is DashboardPageViewModel; reads Root.IsCanConnected, Root.IsBenchConnected,
Pill uses the shared ConnIndicator style. Gray = offline, green = live, red = K-Line failed. Root.IsPumpConnected, Root.KLineState via DataTriggers.
--> -->
<StackPanel> <Border Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
<TextBlock Text="{DynamicResource Dashboard.Connections}" BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
FontSize="13" FontWeight="SemiBold" Foreground="#333" BorderThickness="1" CornerRadius="8"
Margin="0,0,0,6"/> Padding="14,10" Margin="0,0,0,8">
<DockPanel LastChildFill="False">
<!-- CAN bus --> <!-- ── Pump name badge (right-docked) ──────────────────────────── -->
<Grid Margin="0,2"> <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
<Grid.ColumnDefinitions> <TextBlock Text="{DynamicResource Dashboard.Conn.Pump.Label}"
<ColumnDefinition Width="*"/> FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
<ColumnDefinition Width="80"/> VerticalAlignment="Center" Margin="0,0,6,0"/>
</Grid.ColumnDefinitions> <!-- Model name — shown when pump is selected -->
<TextBlock Text="{DynamicResource Dashboard.Conn.Can}" VerticalAlignment="Center" FontSize="12"/> <TextBlock Text="{Binding Root.PumpIdentification.CurrentPump.Model}"
<Border Grid.Column="1" MinWidth="72" Height="22"> FontSize="13" FontWeight="SemiBold"
<Border.Style> Foreground="{DynamicResource TextFillColorPrimaryBrush}"
<Style TargetType="Border" BasedOn="{StaticResource ConnIndicator}"> VerticalAlignment="Center">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsCanConnected}" Value="True">
<Setter Property="Background" Value="#26C200"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="10" FontWeight="SemiBold" Foreground="White"
Text="{DynamicResource Dashboard.StateOnline}">
<TextBlock.Style> <TextBlock.Style>
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/> <Setter Property="Visibility" Value="Visible"/>
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding Root.IsCanConnected}" Value="True"> <DataTrigger Binding="{Binding Root.PumpIdentification.CurrentPump}" Value="{x:Null}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/> <Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</TextBlock.Style> </TextBlock.Style>
</TextBlock> </TextBlock>
</Border> <!-- Placeholder — shown when no pump is selected -->
</Grid> <TextBlock Text="{DynamicResource Dashboard.Conn.NoPump}"
FontSize="12" FontStyle="Italic"
<!-- Bench liveness --> Foreground="{DynamicResource TextFillColorTertiaryBrush}"
<Grid Margin="0,2"> VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.Conn.Bench}" VerticalAlignment="Center" FontSize="12"/>
<Border Grid.Column="1" MinWidth="72" Height="22">
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource ConnIndicator}">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsBenchConnected}" Value="True">
<Setter Property="Background" Value="#26C200"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="10" FontWeight="SemiBold" Foreground="White">
<TextBlock.Style> <TextBlock.Style>
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/> <Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding Root.IsBenchConnected}" Value="True"> <DataTrigger Binding="{Binding Root.PumpIdentification.CurrentPump}" Value="{x:Null}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/> <Setter Property="Visibility" Value="Visible"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</TextBlock.Style> </TextBlock.Style>
</TextBlock> </TextBlock>
</Border> </StackPanel>
</Grid>
<!-- Pump liveness --> <!-- ── Connection chips ─────────────────────────────────────────── -->
<Grid Margin="0,2"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- CAN bus chip -->
<ColumnDefinition Width="80"/> <Border Style="{StaticResource ConnChip}">
</Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Dashboard.Conn.Pump}" VerticalAlignment="Center" FontSize="12"/> <ui:SymbolIcon Symbol="PlugConnected24" FontSize="15"
<Border Grid.Column="1" MinWidth="72" Height="22"> Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"/>
<Border.Style> <TextBlock Text="{DynamicResource Dashboard.Conn.Can}" FontSize="12"
<Style TargetType="Border" BasedOn="{StaticResource ConnIndicator}"> Foreground="{DynamicResource TextFillColorPrimaryBrush}" VerticalAlignment="Center"
<Style.Triggers> Margin="0,0,10,0"/>
<DataTrigger Binding="{Binding Root.IsPumpConnected}" Value="True"> <Ellipse>
<Setter Property="Background" Value="#26C200"/> <Ellipse.Style>
</DataTrigger> <Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
</Style.Triggers> <Style.Triggers>
</Style> <DataTrigger Binding="{Binding Root.IsCanConnected}" Value="True">
</Border.Style> <Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" </DataTrigger>
FontSize="10" FontWeight="SemiBold" Foreground="White"> </Style.Triggers>
<TextBlock.Style> </Style>
<Style TargetType="TextBlock"> </Ellipse.Style>
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/> </Ellipse>
<TextBlock FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsCanConnected}" Value="True">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
<!-- Bench controller chip -->
<Border Style="{StaticResource ConnChip}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="DesktopPulse24" FontSize="15"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"/>
<TextBlock Text="{DynamicResource Dashboard.Conn.Bench}" FontSize="12"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" VerticalAlignment="Center"
Margin="0,0,10,0"/>
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsBenchConnected}" Value="True">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsBenchConnected}" Value="True">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
<!-- Pump ECU chip -->
<Border Style="{StaticResource ConnChip}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<ui:SymbolIcon Symbol="Server24" FontSize="15"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"/>
<TextBlock Text="{DynamicResource Dashboard.Conn.Pump}" FontSize="12"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" VerticalAlignment="Center"
Margin="0,0,10,0"/>
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsPumpConnected}" Value="True">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOffline}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.IsPumpConnected}" Value="True">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
<!-- K-Line session chip -->
<Border>
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource ConnChip}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding Root.IsPumpConnected}" Value="True"> <DataTrigger Binding="{Binding Root.KLineState}"
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOnline}"/> Value="{x:Static models:KLineConnectionState.Failed}">
<Setter Property="Background" Value="{DynamicResource SystemFillColorCautionBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource SystemFillColorCautionBrush}"/>
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</TextBlock.Style> </Border.Style>
</TextBlock> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
</Border> <ui:SymbolIcon Symbol="UsbPlug24" FontSize="15"
</Grid> Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"/>
<TextBlock Text="{DynamicResource Dashboard.Conn.KLine}" FontSize="12"
Foreground="{DynamicResource TextFillColorPrimaryBrush}" VerticalAlignment="Center"
Margin="0,0,10,0"/>
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.KLineState}"
Value="{x:Static models:KLineConnectionState.Connected}">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Root.KLineState}"
Value="{x:Static models:KLineConnectionState.Failed}">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorCautionBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateClosed}"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.KLineState}"
Value="{x:Static models:KLineConnectionState.Connected}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOpen}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Root.KLineState}"
Value="{x:Static models:KLineConnectionState.Failed}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateFailed}"/>
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorCautionBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
<!-- K-Line session --> </StackPanel>
<Grid Margin="0,2"> </DockPanel>
<Grid.ColumnDefinitions> </Border>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Dashboard.Conn.KLine}" VerticalAlignment="Center" FontSize="12"/>
<Border Grid.Column="1" MinWidth="72" Height="22">
<Border.Style>
<Style TargetType="Border" BasedOn="{StaticResource ConnIndicator}">
<Style.Triggers>
<DataTrigger Binding="{Binding Root.KLineState}" Value="{x:Static models:KLineConnectionState.Connected}">
<Setter Property="Background" Value="#26C200"/>
</DataTrigger>
<DataTrigger Binding="{Binding Root.KLineState}" Value="{x:Static models:KLineConnectionState.Failed}">
<Setter Property="Background" Value="#FF3333"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="10" FontWeight="SemiBold" Foreground="White">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateClosed}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Root.KLineState}" Value="{x:Static models:KLineConnectionState.Connected}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateOpen}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Root.KLineState}" Value="{x:Static models:KLineConnectionState.Failed}">
<Setter Property="Text" Value="{DynamicResource Dashboard.StateFailed}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
</Grid>
</StackPanel>
</UserControl> </UserControl>

View File

@@ -0,0 +1,272 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.DashboardDevicesView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:vm="clr-namespace:HC_APTBS.ViewModels.Pages"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="280">
<!--
Devices column — three equal-height tiles (CAN / K-Line / Bench).
DataContext is DashboardPageViewModel; all commands/collections are under Devices.
-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ── CAN tile ─────────────────────────────────────────────────────── -->
<Border Grid.Row="0" Style="{StaticResource KpiTile}" Margin="0,0,0,4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Tile header -->
<DockPanel Grid.Row="0" Margin="0,0,0,8">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="PlugConnected24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Devices.Can}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
<!-- Refresh button -->
<Button DockPanel.Dock="Right"
Command="{Binding Devices.RefreshCanDevicesCommand}"
Background="Transparent" BorderThickness="0"
Padding="4" Cursor="Hand"
ToolTip="{DynamicResource Dashboard.Devices.Refresh}">
<ui:SymbolIcon Symbol="ArrowClockwise24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</Button>
</DockPanel>
<!-- Device list or empty placeholder -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<Grid>
<!-- Placeholder when no devices are found -->
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Text" Value="{DynamicResource Dashboard.Devices.None}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Devices.CanDevices.Count}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ItemsControl ItemsSource="{Binding Devices.CanDevices}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:DeviceItem}">
<Button Style="{StaticResource DeviceRow}"
Command="{Binding DataContext.Devices.ToggleDeviceCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
IsEnabled="{Binding IsEnabled}">
<DockPanel>
<TextBlock Text="{Binding StateLabel}"
DockPanel.Dock="Right"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
VerticalAlignment="Center" Margin="6,0,0,0"/>
<Ellipse DockPanel.Dock="Left">
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="{Binding Name}"
FontSize="13"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
</DockPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</Border>
<!-- ── K-Line tile ──────────────────────────────────────────────────── -->
<Border Grid.Row="1" Style="{StaticResource KpiTile}" Margin="0,4,0,4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Tile header -->
<DockPanel Grid.Row="0" Margin="0,0,0,8">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="UsbPlug24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Devices.Kline}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
<Button DockPanel.Dock="Right"
Command="{Binding Devices.RefreshKLineDevicesCommand}"
Background="Transparent" BorderThickness="0"
Padding="4" Cursor="Hand"
ToolTip="{DynamicResource Dashboard.Devices.Refresh}">
<ui:SymbolIcon Symbol="ArrowClockwise24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
</Button>
</DockPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<Grid>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Text" Value="{DynamicResource Dashboard.Devices.None}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Devices.KLineDevices.Count}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ItemsControl ItemsSource="{Binding Devices.KLineDevices}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:DeviceItem}">
<Button Style="{StaticResource DeviceRow}"
Command="{Binding DataContext.Devices.ToggleDeviceCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
IsEnabled="{Binding IsEnabled}">
<DockPanel>
<TextBlock Text="{Binding StateLabel}"
DockPanel.Dock="Right"
FontSize="11" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="6,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Ellipse DockPanel.Dock="Left">
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="{Binding Name}"
FontSize="13"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
</DockPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</Border>
<!-- ── Bench tile ───────────────────────────────────────────────────── -->
<Border Grid.Row="2" Style="{StaticResource KpiTile}" Margin="0,4,0,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Tile header (no refresh button — nothing to enumerate) -->
<DockPanel Grid.Row="0" Margin="0,0,0,8">
<ui:SymbolIcon DockPanel.Dock="Left" Symbol="DesktopPulse24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Margin="0,0,6,0" VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Devices.Bench}"
Style="{StaticResource KpiHeaderText}" VerticalAlignment="Center"/>
</DockPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Devices.BenchDevices}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:DeviceItem}">
<!-- IsEnabled=false disables hover; Bench rows are not clickable -->
<Button Style="{StaticResource DeviceRow}"
IsEnabled="{Binding IsEnabled}">
<DockPanel>
<TextBlock Text="{Binding StateLabel}"
DockPanel.Dock="Right"
FontSize="11" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="6,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Foreground" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Ellipse DockPanel.Dock="Left">
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Fill" Value="{DynamicResource SystemFillColorSuccessBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="{Binding Name}"
FontSize="13"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"/>
</DockPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,16 @@
using System.Windows.Controls;
namespace HC_APTBS.Views.UserControls
{
/// <summary>
/// Devices column for the Dashboard page — CAN, K-Line, and Bench device tiles.
/// DataContext is expected to be a <see cref="HC_APTBS.ViewModels.Pages.DashboardPageViewModel"/>.
/// </summary>
public partial class DashboardDevicesView : UserControl
{
public DashboardDevicesView()
{
InitializeComponent();
}
}
}

View File

@@ -1,103 +1,142 @@
<UserControl x:Class="HC_APTBS.Views.UserControls.DashboardReadingsView" <UserControl x:Class="HC_APTBS.Views.UserControls.DashboardReadingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="320"> d:DesignHeight="340" d:DesignWidth="900">
<!-- <!--
Dashboard-only compact LCD panel. Dashboard KPI readings grid — 6 Fluent tiles in a 2-row × 3-column uniform grid.
DataContext is DashboardPageViewModel; binds via Root.X to MainViewModel live readings. DataContext is DashboardPageViewModel; binds via Root.X to MainViewModel live readings.
Read-first: no controls, no popups.
--> -->
<StackPanel> <UniformGrid Rows="2" Columns="3">
<!-- RPM: oversized single-line read --> <!-- Tile 1: RPM -->
<Border Style="{StaticResource LcdBlue}" Margin="0,0,0,4" Padding="10,6"> <Border Style="{StaticResource KpiTile}">
<Grid Height="76">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Root.BenchRpm, StringFormat=F0}"
FontSize="60" FontWeight="UltraBold" Foreground="#EBEBFF"
HorizontalAlignment="Right" VerticalAlignment="Center"
FontFamily="Consolas"/>
<TextBlock Grid.Column="1" Text="{DynamicResource Bench.Rpm}"
FontSize="18" Foreground="#FFFFEB6E"
VerticalAlignment="Center" Margin="6,0,0,0"/>
</Grid>
</Border>
<!-- Temperatures + Pressures side by side -->
<Grid Margin="0,0,0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Pressures -->
<Border Grid.Column="0" Style="{StaticResource LcdBlue}" Margin="0,0,2,0" Padding="8,4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="{DynamicResource Bench.P1}" Grid.Row="0" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
<TextBlock Text="{DynamicResource Bench.P2}" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
<TextBlock Text="{Binding Root.Pressure, StringFormat=F1}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
<TextBlock Text="{Binding Root.Pressure2, StringFormat=F1}" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
<TextBlock Text="bar" Grid.Row="0" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/>
<TextBlock Text="bar" Grid.Row="1" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/>
</Grid>
</Border>
<!-- Temperatures -->
<Border Grid.Column="1" Style="{StaticResource LcdBlue}" Margin="2,0,0,0" Padding="8,4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="{DynamicResource Bench.TempIn}" Grid.Row="0" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
<TextBlock Text="{DynamicResource Bench.TempOut}" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
<TextBlock Text="{DynamicResource Bench.TempTank}" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
<TextBlock Text="{Binding Root.TempIn, StringFormat=F1}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
<TextBlock Text="{Binding Root.TempOut, StringFormat=F1}" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
<TextBlock Text="{Binding Root.BenchTemp, StringFormat=F1}" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
<TextBlock Text="°C" Grid.Row="0" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/>
<TextBlock Text="°C" Grid.Row="1" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/>
<TextBlock Text="°C" Grid.Row="2" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/>
</Grid>
</Border>
</Grid>
<!-- Flow: Q-Delivery -->
<Border Style="{StaticResource LcdBlue}" Padding="8,4">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.RowDefinitions>
<ColumnDefinition Width="90"/> <RowDefinition Height="Auto"/>
<ColumnDefinition/> <RowDefinition Height="*"/>
<ColumnDefinition Width="46"/> </Grid.RowDefinitions>
</Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal">
<TextBlock Text="{DynamicResource Bench.QDelivery}" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/> <ui:SymbolIcon Symbol="Gauge24" FontSize="14"
<TextBlock Text="{Binding Root.QDelivery, StringFormat=F1}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" VerticalAlignment="Center"/>
Foreground="#EBEBFF" FontSize="22" FontFamily="Consolas"/> <TextBlock Text="{DynamicResource Dashboard.Kpi.Rpm}" Style="{StaticResource KpiHeaderText}"
<TextBlock Text="cc/s" Grid.Column="2" Foreground="#EBEBFF" FontSize="13" VerticalAlignment="Center" Margin="4,0"/> VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.BenchRpm, StringFormat=F0}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.Rpm}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid> </Grid>
</Border> </Border>
</StackPanel> <!-- Tile 2: Q-Delivery -->
<Border Style="{StaticResource KpiTile}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Drop24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Qdelivery}" Style="{StaticResource KpiHeaderText}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.QDelivery, StringFormat=F1}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.CcS}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<!-- Tile 3: Pressure P1 -->
<Border Style="{StaticResource KpiTile}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowTrendingLines24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.P1}" Style="{StaticResource KpiHeaderText}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.Pressure, StringFormat=F1}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.Bar}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<!-- Tile 4: Pressure P2 -->
<Border Style="{StaticResource KpiTile}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowTrendingLines24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.P2}" Style="{StaticResource KpiHeaderText}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.Pressure2, StringFormat=F1}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.Bar}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<!-- Tile 5: Oil inlet temperature -->
<Border Style="{StaticResource KpiTile}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Temperature24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Tin}" Style="{StaticResource KpiHeaderText}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.TempIn, StringFormat=F1}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.Celsius}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
<!-- Tile 6: Oil outlet temperature -->
<Border Style="{StaticResource KpiTile}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="ArrowUpload24" FontSize="14"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,0,6,0"
VerticalAlignment="Center"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Tout}" Style="{StaticResource KpiHeaderText}"
VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Root.TempOut, StringFormat=F1}" Style="{StaticResource KpiValueText}"/>
<TextBlock Text="{DynamicResource Dashboard.Kpi.Unit.Celsius}" Style="{StaticResource KpiUnitText}"/>
</StackPanel>
</Grid>
</Border>
</UniformGrid>
</UserControl> </UserControl>

View File

@@ -10,13 +10,13 @@
DataContext = RelayBankViewModel. DataContext = RelayBankViewModel.
--> -->
<UserControl.Resources> <UserControl.Resources>
<Style x:Key="RelayToggle" TargetType="ToggleButton"> <Style x:Key="RelayToggle" TargetType="ToggleButton"
BasedOn="{StaticResource FluentStateToggle}">
<Setter Property="Height" Value="28"/> <Setter Property="Height" Value="28"/>
<Setter Property="FontSize" Value="11"/> <Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="SemiBold"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,2,0,0"/> <Setter Property="Margin" Value="0,2,0,0"/>
<Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/>
<Setter Property="Background" Value="LightGray"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/>

View File

@@ -50,9 +50,8 @@
<TextBlock Text="{DynamicResource Bench.DepositHeater}" FontSize="10" Foreground="DimGray" Margin="0,10,0,2"/> <TextBlock Text="{DynamicResource Bench.DepositHeater}" FontSize="10" Foreground="DimGray" Margin="0,10,0,2"/>
<ToggleButton IsChecked="{Binding IsHeaterOn}" Height="28" FontSize="11" FontWeight="SemiBold"> <ToggleButton IsChecked="{Binding IsHeaterOn}" Height="28" FontSize="11" FontWeight="SemiBold">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton" BasedOn="{StaticResource FluentStateToggle}">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/>
<Setter Property="Background" Value="LightGray"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/>
@@ -67,9 +66,8 @@
<TextBlock Text="{DynamicResource Bench.DepositCooler}" FontSize="10" Foreground="DimGray" Margin="0,6,0,2"/> <TextBlock Text="{DynamicResource Bench.DepositCooler}" FontSize="10" Foreground="DimGray" Margin="0,6,0,2"/>
<ToggleButton IsChecked="{Binding IsDepositCoolerOn}" Height="28" FontSize="11" FontWeight="SemiBold"> <ToggleButton IsChecked="{Binding IsDepositCoolerOn}" Height="28" FontSize="11" FontWeight="SemiBold">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton" BasedOn="{StaticResource FluentStateToggle}">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/>
<Setter Property="Background" Value="LightGray"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/>
@@ -84,9 +82,8 @@
<TextBlock Text="{DynamicResource Bench.TinCooler}" FontSize="10" Foreground="DimGray" Margin="0,6,0,2"/> <TextBlock Text="{DynamicResource Bench.TinCooler}" FontSize="10" Foreground="DimGray" Margin="0,6,0,2"/>
<ToggleButton IsChecked="{Binding IsTinCoolerOn}" Height="28" FontSize="11" FontWeight="SemiBold"> <ToggleButton IsChecked="{Binding IsTinCoolerOn}" Height="28" FontSize="11" FontWeight="SemiBold">
<ToggleButton.Style> <ToggleButton.Style>
<Style TargetType="ToggleButton"> <Style TargetType="ToggleButton" BasedOn="{StaticResource FluentStateToggle}">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOff}"/>
<Setter Property="Background" Value="LightGray"/>
<Style.Triggers> <Style.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/> <Setter Property="Content" Value="{DynamicResource Bench.RelayOn}"/>