feat: add Ford VP44 unlock progress dialog, K-Line fast unlock, localization, safety dialogs, and settings
Unlock progress UI:
- UnlockProgressDialog with dark-themed progress ring, phase indicator, elapsed
time, and cancel/close buttons (non-modal, draggable borderless window)
- UnlockProgressViewModel with event-driven progress tracking via IUnlockService
- Triggers on pump selection (manual or K-Line auto-detect), not test start
UnlockService rewrite:
- Persistent CAN senders that outlive the unlock sequence (StopSenders on pump change)
- Concurrent K-Line fast unlock: awaits session Connected, sends RAM timer shortcut
({02 88 02 03 A8 01 00}), verifies via CAN TestUnlock before skipping wait
- Fix Type 1 verification (Value == 0 means unlocked, was inverted)
K-Line fast unlock support:
- IKwpService.TryFastUnlockAsync / KwpService implementation
Additional features:
- ILocalizationService with ES/EN resource dictionaries and runtime switching
- Safety dialogs: VoltageWarning, OilPumpConfirm, RpmSafetyWarning
- SettingsDialog for app configuration
- BenchService enhancements, ConfigurationService improvements, PDF report updates
- All UI strings localized via DynamicResource
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5
App.xaml
5
App.xaml
@@ -3,5 +3,10 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:HC_APTBS">
|
xmlns:local="clr-namespace:HC_APTBS">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Resources/Strings.es.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public partial class App : Application
|
|||||||
services.AddSingleton<IUnlockService, UnlockService>();
|
services.AddSingleton<IUnlockService, UnlockService>();
|
||||||
services.AddSingleton<ICalibrationService, CalibrationService>();
|
services.AddSingleton<ICalibrationService, CalibrationService>();
|
||||||
services.AddSingleton<IPdfService, PdfService>();
|
services.AddSingleton<IPdfService, PdfService>();
|
||||||
|
services.AddSingleton<ILocalizationService, LocalizationService>();
|
||||||
|
|
||||||
// ── ViewModels ────────────────────────────────────────────────────────
|
// ── ViewModels ────────────────────────────────────────────────────────
|
||||||
services.AddSingleton<MainViewModel>();
|
services.AddSingleton<MainViewModel>();
|
||||||
|
|||||||
10
CLAUDE.md
10
CLAUDE.md
@@ -83,11 +83,14 @@ ViewModels/ — [ObservableProperty] / [RelayCommand], no UI lo
|
|||||||
FlowmeterChartViewModel.cs — Real-time flowmeter charts
|
FlowmeterChartViewModel.cs — Real-time flowmeter charts
|
||||||
AngleDisplayViewModel.cs — Encoder angle monitoring (PSG, INJ, Manual, Lock Angle)
|
AngleDisplayViewModel.cs — Encoder angle monitoring (PSG, INJ, Manual, Lock Angle)
|
||||||
StatusDisplayViewModel.cs — Pump status word display
|
StatusDisplayViewModel.cs — Pump status word display
|
||||||
Dialogs/ — KlineErrors, Progress, Report, UserCheck ViewModels
|
Dialogs/ — KlineErrors, OilPumpConfirm, Progress, Report, RpmSafetyWarning,
|
||||||
|
Settings, UnlockProgress, UserCheck, VoltageWarning ViewModels
|
||||||
|
|
||||||
Views/
|
Views/
|
||||||
MainWindow.xaml — Root UI (multi-panel layout)
|
MainWindow.xaml — Root UI (multi-panel layout)
|
||||||
Dialogs/ — KlineErrorsDialog, ProgressDialog, ReportDialog, UserCheckDialog
|
Dialogs/ — KlineErrorsDialog, OilPumpConfirmDialog, ProgressDialog,
|
||||||
|
ReportDialog, RpmSafetyWarningDialog, SettingsDialog,
|
||||||
|
UnlockProgressDialog, UserCheckDialog, VoltageWarningDialog
|
||||||
UserControls/ — AngleDisplay, BenchParamConfig, DfiManage, FlowmeterChart,
|
UserControls/ — AngleDisplay, BenchParamConfig, DfiManage, FlowmeterChart,
|
||||||
PumpControl, PumpIdentification, ResultDisplay, StatusDisplay,
|
PumpControl, PumpIdentification, ResultDisplay, StatusDisplay,
|
||||||
TestDisplay, TestPanel
|
TestDisplay, TestPanel
|
||||||
@@ -147,14 +150,11 @@ See `docs/` guidelines for full specs. Priority: CRITICAL > HIGH > MEDIUM > LOW.
|
|||||||
|
|
||||||
**HIGH — Missing safety features:**
|
**HIGH — Missing safety features:**
|
||||||
- No QOver zero-flow safety check (old: emergency stop if QOver==0 while RPM>300 + oil pump on)
|
- No QOver zero-flow safety check (old: emergency stop if QOver==0 while RPM>300 + oil pump on)
|
||||||
- No safety dialogs: 27V warning, oil pump confirmation, RPM warning
|
|
||||||
- Alarm bit collection during tests not wired up (`PhaseDefinition.RecordErrorBit` never called)
|
- Alarm bit collection during tests not wired up (`PhaseDefinition.RecordErrorBit` never called)
|
||||||
- No per-sample real-time UI callback during measurement (old fired per-sample events for live charts)
|
- No per-sample real-time UI callback during measurement (old fired per-sample events for live charts)
|
||||||
- Pump parameters (ME/FBKW/PreIn) not zeroed between test phases
|
- Pump parameters (ME/FBKW/PreIn) not zeroed between test phases
|
||||||
|
|
||||||
**HIGH — Missing features:**
|
**HIGH — Missing features:**
|
||||||
- Ford unlock progress UI (service exists, no View — old had WUnlocker.xaml with visual ring + progress bar)
|
|
||||||
- No localization system (old had Spanish/English resource dictionaries with runtime switching)
|
|
||||||
- No encryption (old encrypted user passwords with AES-256 + pump data with Rijndael; new stores plaintext)
|
- No encryption (old encrypted user passwords with AES-256 + pump data with Rijndael; new stores plaintext)
|
||||||
- No KlineIDs auto-mapping (old remembered K-Line ID → pump ID associations)
|
- No KlineIDs auto-mapping (old remembered K-Line ID → pump ID associations)
|
||||||
|
|
||||||
|
|||||||
103
MainWindow.xaml
103
MainWindow.xaml
@@ -7,7 +7,7 @@
|
|||||||
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
|
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
|
||||||
xmlns:models="clr-namespace:HC_APTBS.Models"
|
xmlns:models="clr-namespace:HC_APTBS.Models"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="HC_APTBS — Herlic Test Bench"
|
Title="{DynamicResource App.Title}"
|
||||||
Height="1080" Width="1920"
|
Height="1080" Width="1920"
|
||||||
WindowState="Maximized"
|
WindowState="Maximized"
|
||||||
WindowStartupLocation="CenterScreen"
|
WindowStartupLocation="CenterScreen"
|
||||||
@@ -68,7 +68,8 @@
|
|||||||
|
|
||||||
<!-- ── Menu bar ──────────────────────────────────────────────────── -->
|
<!-- ── Menu bar ──────────────────────────────────────────────────── -->
|
||||||
<Menu DockPanel.Dock="Top" Background="#FFEDEDED">
|
<Menu DockPanel.Dock="Top" Background="#FFEDEDED">
|
||||||
<MenuItem Header="Settings" Command="{Binding SaveSettingsCommand}"/>
|
<MenuItem Header="{DynamicResource Menu.Settings}" Command="{Binding OpenSettingsCommand}"/>
|
||||||
|
<MenuItem Header="{DynamicResource App.LanguageLabel}" Command="{Binding ToggleLanguageCommand}"/>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<!-- ── Status bar ─────────────────────────────────────────────────── -->
|
<!-- ── Status bar ─────────────────────────────────────────────────── -->
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
<!-- ══════════════════════════════════════════════════════════════
|
<!-- ══════════════════════════════════════════════════════════════
|
||||||
LEFT PANEL — bench status and controls
|
LEFT PANEL — bench status and controls
|
||||||
══════════════════════════════════════════════════════════════ -->
|
══════════════════════════════════════════════════════════════ -->
|
||||||
<Expander Header="Bench" IsExpanded="True" Margin="0,2,0,0">
|
<Expander Header="{DynamicResource Bench.Header}" IsExpanded="True" Margin="0,2,0,0">
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<Grid Margin="5">
|
<Grid Margin="5">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Text="Status:" VerticalAlignment="Center"
|
<TextBlock Text="{DynamicResource Status.Label}" VerticalAlignment="Center"
|
||||||
FontSize="10" Margin="0,0,6,0"/>
|
FontSize="10" Margin="0,0,6,0"/>
|
||||||
|
|
||||||
<Border Grid.Column="1">
|
<Border Grid.Column="1">
|
||||||
@@ -139,7 +140,7 @@
|
|||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Border.Style>
|
</Border.Style>
|
||||||
<TextBlock Text="CAN" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource Status.Can}" HorizontalAlignment="Center"
|
||||||
FontSize="10" Padding="2"/>
|
FontSize="10" Padding="2"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@
|
|||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Border.Style>
|
</Border.Style>
|
||||||
<TextBlock Text="Bench" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource Status.Bench}" HorizontalAlignment="Center"
|
||||||
FontSize="10" Padding="2"/>
|
FontSize="10" Padding="2"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Border.Style>
|
</Border.Style>
|
||||||
<TextBlock Text="Pump" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource Status.Pump}" HorizontalAlignment="Center"
|
||||||
FontSize="10" Padding="2"/>
|
FontSize="10" Padding="2"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@
|
|||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Border.Style>
|
</Border.Style>
|
||||||
<TextBlock Text="K-Line" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource Status.KLine}" HorizontalAlignment="Center"
|
||||||
FontSize="10" Padding="2"/>
|
FontSize="10" Padding="2"/>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -192,9 +193,9 @@
|
|||||||
|
|
||||||
<!-- ── Row 1: CAN connect / disconnect ─────────────────── -->
|
<!-- ── Row 1: CAN connect / disconnect ─────────────────── -->
|
||||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,4">
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,4">
|
||||||
<Button Content="Connect CAN" Width="110" Margin="0,0,6,0"
|
<Button Content="{DynamicResource Bench.ConnectCan}" Width="110" Margin="0,0,6,0"
|
||||||
Command="{Binding ConnectCanCommand}"/>
|
Command="{Binding ConnectCanCommand}"/>
|
||||||
<Button Content="Disconnect CAN" Width="120"
|
<Button Content="{DynamicResource Bench.DisconnectCan}" Width="120"
|
||||||
Command="{Binding DisconnectCanCommand}"
|
Command="{Binding DisconnectCanCommand}"
|
||||||
IsEnabled="{Binding IsCanConnected}"/>
|
IsEnabled="{Binding IsCanConnected}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -222,14 +223,14 @@
|
|||||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
FontFamily="Consolas"/>
|
FontFamily="Consolas"/>
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0">
|
||||||
<TextBlock Text="rpm" FontSize="18" Foreground="#FFFFEB6E"/>
|
<TextBlock Text="{DynamicResource Bench.Rpm}" FontSize="18" Foreground="#FFFFEB6E"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<TextBlock Text="Target:" Foreground="#EBEBFF" FontSize="11" Margin="0,0,4,0"/>
|
<TextBlock Text="{DynamicResource Bench.Target}" Foreground="#EBEBFF" FontSize="11" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="{Binding BenchControl.TargetRpm, StringFormat=F0}"
|
<TextBlock Text="{Binding BenchControl.TargetRpm, StringFormat=F0}"
|
||||||
Foreground="#FFFFEB6E" FontSize="11" FontFamily="Consolas" Margin="0,0,8,0"/>
|
Foreground="#FFFFEB6E" FontSize="11" FontFamily="Consolas" Margin="0,0,8,0"/>
|
||||||
<TextBlock Text="V:" Foreground="#EBEBFF" FontSize="11" Margin="0,0,4,0"/>
|
<TextBlock Text="{DynamicResource Bench.Voltage}" Foreground="#EBEBFF" FontSize="11" Margin="0,0,4,0"/>
|
||||||
<TextBlock Text="{Binding BenchControl.CommandVoltage, StringFormat=F3}"
|
<TextBlock Text="{Binding BenchControl.CommandVoltage, StringFormat=F3}"
|
||||||
Foreground="#FFFFEB6E" FontSize="11" FontFamily="Consolas"/>
|
Foreground="#FFFFEB6E" FontSize="11" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -253,12 +254,12 @@
|
|||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="T. In:" Grid.Row="0" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.TempIn}" Grid.Row="0" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="T. Out:" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.TempOut}" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="T. 4:" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.Temp4}" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="T. Tank:" Grid.Row="3" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.TempTank}" Grid.Row="3" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="P1:" Grid.Row="4" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.P1}" Grid.Row="4" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="P2:" Grid.Row="5" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.P2}" Grid.Row="5" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
|
|
||||||
<TextBlock Text="{Binding TempIn, StringFormat=F1}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding TempIn, StringFormat=F1}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
|
||||||
<TextBlock Text="{Binding TempOut, StringFormat=F1}" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding TempOut, StringFormat=F1}" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" Foreground="#EBEBFF" FontSize="20" FontFamily="Consolas"/>
|
||||||
@@ -288,12 +289,12 @@
|
|||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Text="Q-Del.:" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.QDelivery}" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="13"/>
|
||||||
<TextBlock Text="Q-Over:" Grid.Row="1" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Bench.QOver}" Grid.Row="1" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="13"/>
|
||||||
<TextBlock Text="{Binding QDelivery, StringFormat=F3}" Grid.Column="1" HorizontalAlignment="Right" Foreground="#FFFFEB6E" FontSize="22" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding QDelivery, StringFormat=F3}" Grid.Column="1" HorizontalAlignment="Right" Foreground="#FFFFEB6E" FontSize="22" FontFamily="Consolas"/>
|
||||||
<TextBlock Text="{Binding QOver, StringFormat=F3}" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Foreground="#FFFFEB6E" FontSize="22" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding QOver, StringFormat=F3}" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Foreground="#FFFFEB6E" FontSize="22" FontFamily="Consolas"/>
|
||||||
<TextBlock Text="cc/stroke" Grid.Column="2" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="11" Margin="4,0"/>
|
<TextBlock Text="{DynamicResource Bench.CcStroke}" Grid.Column="2" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="11" Margin="4,0"/>
|
||||||
<TextBlock Text="cc/stroke" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="11" Margin="4,0"/>
|
<TextBlock Text="{DynamicResource Bench.CcStroke}" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" Foreground="#FFFFEB6E" FontSize="11" Margin="4,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -310,10 +311,10 @@
|
|||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock Text="P-RPM:" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
<TextBlock Text="{DynamicResource Bench.PumpRpm}" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
||||||
<TextBlock Text="P-Temp:" Grid.Row="1" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
<TextBlock Text="{DynamicResource Bench.PumpTemp}" Grid.Row="1" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
||||||
<TextBlock Text="P-ME:" Grid.Row="2" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
<TextBlock Text="{DynamicResource Bench.PumpMe}" Grid.Row="2" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
||||||
<TextBlock Text="P-FBkW:" Grid.Row="3" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
<TextBlock Text="{DynamicResource Bench.PumpFbkw}" Grid.Row="3" VerticalAlignment="Center" FontSize="12" Foreground="DimGray"/>
|
||||||
<TextBlock Text="{Binding PumpRpm, StringFormat=F0}" Grid.Column="1" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding PumpRpm, StringFormat=F0}" Grid.Column="1" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
||||||
<TextBlock Text="{Binding PumpTemp, StringFormat=F1}" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding PumpTemp, StringFormat=F1}" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
||||||
<TextBlock Text="{Binding PumpMe, StringFormat=F2}" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding PumpMe, StringFormat=F2}" Grid.Column="1" Grid.Row="2" HorizontalAlignment="Right" FontSize="16" FontFamily="Consolas"/>
|
||||||
@@ -323,7 +324,7 @@
|
|||||||
|
|
||||||
<!-- PSG encoder value -->
|
<!-- PSG encoder value -->
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,4" Visibility="Collapsed">
|
<StackPanel Orientation="Horizontal" Margin="0,4" Visibility="Collapsed">
|
||||||
<TextBlock Text="PSG Encoder:" VerticalAlignment="Center" FontSize="12" Margin="0,0,8,0"/>
|
<TextBlock Text="{DynamicResource Bench.PsgEncoder}" VerticalAlignment="Center" FontSize="12" Margin="0,0,8,0"/>
|
||||||
<TextBlock Text="{Binding PsgEncoderValue, StringFormat=F2}"
|
<TextBlock Text="{Binding PsgEncoderValue, StringFormat=F2}"
|
||||||
FontSize="16" FontFamily="Consolas" VerticalAlignment="Center"/>
|
FontSize="16" FontFamily="Consolas" VerticalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -334,15 +335,15 @@
|
|||||||
<StackPanel Grid.Column="1" Width="160" Margin="6,0,0,0">
|
<StackPanel Grid.Column="1" Width="160" Margin="6,0,0,0">
|
||||||
|
|
||||||
<!-- Direction toggle -->
|
<!-- Direction toggle -->
|
||||||
<TextBlock Text="Direction" FontSize="10" Foreground="DimGray" Margin="0,4,0,2"/>
|
<TextBlock Text="{DynamicResource Bench.Direction}" FontSize="10" Foreground="DimGray" Margin="0,4,0,2"/>
|
||||||
<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">
|
||||||
<Setter Property="Content" Value="LEFT"/>
|
<Setter Property="Content" Value="{DynamicResource Bench.Left}"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter Property="Content" Value="RIGHT"/>
|
<Setter Property="Content" Value="{DynamicResource Bench.Right}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
@@ -350,18 +351,18 @@
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|
||||||
<!-- Start / Stop bench -->
|
<!-- Start / Stop bench -->
|
||||||
<TextBlock Text="Bench Motor" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
<TextBlock Text="{DynamicResource Bench.Motor}" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
||||||
<Button Content="START" FontSize="13" FontWeight="Bold" Height="36"
|
<Button Content="{DynamicResource Bench.Start}" FontSize="13" FontWeight="Bold" Height="36"
|
||||||
Foreground="DarkGreen" Margin="0,0,0,4"
|
Foreground="DarkGreen" Margin="0,0,0,4"
|
||||||
Command="{Binding BenchControl.OpenRpmPopupCommand}"/>
|
Command="{Binding BenchControl.OpenRpmPopupCommand}"/>
|
||||||
<Popup StaysOpen="False" Placement="Left"
|
<Popup StaysOpen="False" Placement="Left"
|
||||||
IsOpen="{Binding BenchControl.IsRpmPopupOpen, Mode=TwoWay}">
|
IsOpen="{Binding BenchControl.IsRpmPopupOpen, Mode=TwoWay}">
|
||||||
<Border Background="White" BorderBrush="Black" BorderThickness="1" Padding="8">
|
<Border Background="White" BorderBrush="Black" BorderThickness="1" Padding="8">
|
||||||
<StackPanel Width="200">
|
<StackPanel Width="200">
|
||||||
<TextBlock Text="Set RPM:" FontSize="12" Margin="0,0,0,4"/>
|
<TextBlock Text="{DynamicResource Bench.SetRpm}" FontSize="12" Margin="0,0,0,4"/>
|
||||||
<TextBox Text="{Binding BenchControl.RpmInputText, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding BenchControl.RpmInputText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
FontSize="16" FontFamily="Consolas" Height="28" Margin="0,0,0,6"/>
|
FontSize="16" FontFamily="Consolas" Height="28" Margin="0,0,0,6"/>
|
||||||
<Button Content="GO" FontSize="13" FontWeight="Bold" Height="30"
|
<Button Content="{DynamicResource Bench.Go}" FontSize="13" FontWeight="Bold" Height="30"
|
||||||
Foreground="DarkGreen" Margin="0,0,0,6"
|
Foreground="DarkGreen" Margin="0,0,0,6"
|
||||||
Command="{Binding BenchControl.StartBenchCommand}"/>
|
Command="{Binding BenchControl.StartBenchCommand}"/>
|
||||||
<UniformGrid Columns="5">
|
<UniformGrid Columns="5">
|
||||||
@@ -384,21 +385,21 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Popup>
|
</Popup>
|
||||||
<Button Content="STOP" FontSize="13" FontWeight="Bold" Height="36"
|
<Button Content="{DynamicResource Bench.Stop}" FontSize="13" FontWeight="Bold" Height="36"
|
||||||
Foreground="DarkRed"
|
Foreground="DarkRed"
|
||||||
Command="{Binding BenchControl.StopBenchCommand}"/>
|
Command="{Binding BenchControl.StopBenchCommand}"/>
|
||||||
|
|
||||||
<!-- Oil pump toggle -->
|
<!-- Oil pump toggle -->
|
||||||
<TextBlock Text="Oil Pump" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
<TextBlock Text="{DynamicResource Bench.OilPump}" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
||||||
<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">
|
||||||
<Setter Property="Content" Value="OIL OFF"/>
|
<Setter Property="Content" Value="{DynamicResource Bench.OilOff}"/>
|
||||||
<Setter Property="Background" Value="LightGray"/>
|
<Setter Property="Background" Value="LightGray"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsChecked" Value="True">
|
||||||
<Setter Property="Content" Value="OIL ON"/>
|
<Setter Property="Content" Value="{DynamicResource Bench.OilOn}"/>
|
||||||
<Setter Property="Background" Value="#80FF80"/>
|
<Setter Property="Background" Value="#80FF80"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
@@ -407,18 +408,18 @@
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|
||||||
<!-- Turn counter -->
|
<!-- Turn counter -->
|
||||||
<TextBlock Text="Counter" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
<TextBlock Text="{DynamicResource Bench.Counter}" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
||||||
<ToggleButton Content="Counter"
|
<ToggleButton Content="{DynamicResource Bench.Counter}"
|
||||||
IsChecked="{Binding BenchControl.IsCounterPopupOpen}"
|
IsChecked="{Binding BenchControl.IsCounterPopupOpen}"
|
||||||
Height="28" FontSize="11"/>
|
Height="28" FontSize="11"/>
|
||||||
<Popup StaysOpen="False" Placement="Left"
|
<Popup StaysOpen="False" Placement="Left"
|
||||||
IsOpen="{Binding BenchControl.IsCounterPopupOpen, Mode=TwoWay}">
|
IsOpen="{Binding BenchControl.IsCounterPopupOpen, Mode=TwoWay}">
|
||||||
<Border Background="White" BorderBrush="Black" BorderThickness="1" Padding="8">
|
<Border Background="White" BorderBrush="Black" BorderThickness="1" Padding="8">
|
||||||
<StackPanel Width="160">
|
<StackPanel Width="160">
|
||||||
<TextBlock Text="Turns:" FontSize="12" Margin="0,0,0,4"/>
|
<TextBlock Text="{DynamicResource Bench.Turns}" FontSize="12" Margin="0,0,0,4"/>
|
||||||
<TextBox Text="{Binding BenchControl.CounterInputText, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding BenchControl.CounterInputText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
FontSize="16" FontFamily="Consolas" Height="28" Margin="0,0,0,4"/>
|
FontSize="16" FontFamily="Consolas" Height="28" Margin="0,0,0,4"/>
|
||||||
<Button Content="Send" FontSize="12" Height="28"
|
<Button Content="{DynamicResource Bench.Send}" FontSize="12" Height="28"
|
||||||
Command="{Binding BenchControl.SendCounterCommand}"/>
|
Command="{Binding BenchControl.SendCounterCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -427,11 +428,11 @@
|
|||||||
Text="{Binding BenchControl.BenchCounterValue, StringFormat=00000000}"/>
|
Text="{Binding BenchControl.BenchCounterValue, StringFormat=00000000}"/>
|
||||||
|
|
||||||
<!-- Relay toggles -->
|
<!-- Relay toggles -->
|
||||||
<TextBlock Text="Relays" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
<TextBlock Text="{DynamicResource Bench.Relays}" FontSize="10" Foreground="DimGray" Margin="0,8,0,2"/>
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Button Content="Electronic" Style="{StaticResource RelayButton}" Command="{Binding ToggleElectronicCommand}"/>
|
<Button Content="{DynamicResource Bench.Electronic}" Style="{StaticResource RelayButton}" Command="{Binding ToggleElectronicCommand}"/>
|
||||||
<Button Content="Deposit Cooler" Style="{StaticResource RelayButton}" Command="{Binding ToggleDepositCoolerCommand}"/>
|
<Button Content="{DynamicResource Bench.DepositCooler}" Style="{StaticResource RelayButton}" Command="{Binding ToggleDepositCoolerCommand}"/>
|
||||||
<Button Content="Deposit Heater" Style="{StaticResource RelayButton}" Command="{Binding ToggleDepositHeaterCommand}"/>
|
<Button Content="{DynamicResource Bench.DepositHeater}" Style="{StaticResource RelayButton}" Command="{Binding ToggleDepositHeaterCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -493,9 +494,9 @@
|
|||||||
<RowDefinition Height="1.2*"/>
|
<RowDefinition Height="1.2*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="T-hyb" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Pump.THyb}" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="RPM" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Pump.Rpm}" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
<TextBlock Text="T-ein" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
<TextBlock Text="{DynamicResource Pump.TEin}" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="13"/>
|
||||||
|
|
||||||
<TextBlock Text="{Binding PumpTemp, StringFormat=F2}" Grid.Column="1"
|
<TextBlock Text="{Binding PumpTemp, StringFormat=F2}" Grid.Column="1"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
@@ -508,7 +509,7 @@
|
|||||||
Foreground="#EBEBFF" FontSize="20" FontWeight="Bold" FontFamily="Consolas"/>
|
Foreground="#EBEBFF" FontSize="20" FontWeight="Bold" FontFamily="Consolas"/>
|
||||||
|
|
||||||
<TextBlock Text="°C" Grid.Column="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
<TextBlock Text="°C" Grid.Column="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
||||||
<TextBlock Text="1/min" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
<TextBlock Text="{DynamicResource Pump.UnitRpm}" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
||||||
<TextBlock Text="µs" Grid.Column="2" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
<TextBlock Text="µs" Grid.Column="2" Grid.Row="2" VerticalAlignment="Center" Foreground="#EBEBFF" FontSize="12" Margin="4,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ namespace HC_APTBS.Models
|
|||||||
public string ReportLogoPath { get; set; } = string.Empty;
|
public string ReportLogoPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Comma-separated <c>user:password</c> credential pairs for operator authentication
|
/// Comma-separated <c>user:salt:hash</c> credential entries for operator authentication
|
||||||
/// before report generation.
|
/// before report generation. Salt is 16-byte Base64, hash is PBKDF2-HMAC-SHA256 (600 000 iterations, 32-byte output) Base64.
|
||||||
|
/// Legacy <c>user:password</c> entries are auto-migrated on first load.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Users { get; set; } = "admin:admin";
|
public string Users { get; set; } = string.Empty;
|
||||||
|
|
||||||
// ── K-Line port ───────────────────────────────────────────────────────
|
// ── K-Line port ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
322
Resources/Strings.en.xaml
Normal file
322
Resources/Strings.en.xaml
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════
|
||||||
|
English (ENG)
|
||||||
|
══════════════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- ── App ──────────────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="App.Title">HC_APTBS — Herlic Test Bench</sys:String>
|
||||||
|
<sys:String x:Key="App.LanguageLabel">ESP</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Menu ─────────────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Menu.Settings">Settings</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Status bar / connection indicators ───────────────────────────── -->
|
||||||
|
<sys:String x:Key="Status.Label">Status:</sys:String>
|
||||||
|
<sys:String x:Key="Status.Can">CAN</sys:String>
|
||||||
|
<sys:String x:Key="Status.Bench">Bench</sys:String>
|
||||||
|
<sys:String x:Key="Status.Pump">Pump</sys:String>
|
||||||
|
<sys:String x:Key="Status.KLine">K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Status.Connected">Connected</sys:String>
|
||||||
|
<sys:String x:Key="Status.ConnectionFailed">Connection failed</sys:String>
|
||||||
|
<sys:String x:Key="Status.Disconnected">Disconnected</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench section ────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Bench.Header">Bench</sys:String>
|
||||||
|
<sys:String x:Key="Bench.ConnectCan">Connect CAN</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DisconnectCan">Disconnect CAN</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Rpm">rpm</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Target">Target:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Voltage">V:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempIn">T. In:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempOut">T. Out:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Temp4">T. 4:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempTank">T. Tank:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.P1">P1:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.P2">P2:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.QDelivery">Q-Del.:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.QOver">Q-Over:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.CcStroke">cc/stroke</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpRpm">P-RPM:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpTemp">P-Temp:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpMe">P-ME:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpFbkw">P-FBkW:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PsgEncoder">PSG Encoder:</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench controls ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Bench.Direction">Direction</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Left">LEFT</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Right">RIGHT</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Motor">Bench Motor</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Start">START</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Stop">STOP</sys:String>
|
||||||
|
<sys:String x:Key="Bench.SetRpm">Set RPM:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Go">GO</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilPump">Oil Pump</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilOff">OIL OFF</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilOn">OIL ON</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Counter">Counter</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Turns">Turns:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Send">Send</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Relays">Relays</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Electronic">Electronic</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DepositCooler">Deposit Cooler</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DepositHeater">Deposit Heater</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump live data ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Pump.THyb">T-hyb</sys:String>
|
||||||
|
<sys:String x:Key="Pump.Rpm">RPM</sys:String>
|
||||||
|
<sys:String x:Key="Pump.TEin">T-ein</sys:String>
|
||||||
|
<sys:String x:Key="Pump.UnitRpm">1/min</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump identification ──────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="PumpId.Label">Pump:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Dfi">DFI:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.PumpId">Pump ID:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SerialNo">Serial No:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ModelRef">Model Ref:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.DataRecord">Data Record:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.PumpCtrl">Pump Ctrl:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ModelIndex">Model Index:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SwVer1">SW Ver 1:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SwVer2">SW Ver 2:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Errors">Errors:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Error">Error:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ReadKLine">Read K-Line</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Disconnect">Disconnect</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.NoKLineDevice">No K-Line device found</sys:String>
|
||||||
|
|
||||||
|
<!-- ── DFI management ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dfi.Read">READ</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Write">WRITE</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Label">DFI:</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Auto">AUTO</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump control sliders ─────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="PumpCtrl.Fbkw">FBKW - Advance Control</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Me">ME - Quantity Control</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.PreInj">ME - Pre-inj Quantity</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.MinStepMax">Min / Step / Max</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Min">Min</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Step">Step</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Max">Max</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench parameter config ───────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="BenchParam.CanBusId">CAN-Bus ID (0x)</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.ByteL">Byte L</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.ByteH">Byte H</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.FilterAlpha">Filter α</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.EnableFormula">Enable formula</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Angle display ────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Angle.Header">ADVANCE MONITORING</sys:String>
|
||||||
|
<sys:String x:Key="Angle.Psg">PSG:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.Inj">INJ:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.AbsDeg">ABS °:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.LockDeg">LOCK °:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.SetPsgZero">Set PSG zero reference</sys:String>
|
||||||
|
<sys:String x:Key="Angle.SetInjZero">Set INJ zero reference</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Test panel ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Test.StartTest">▶ START TEST</sys:String>
|
||||||
|
<sys:String x:Key="Test.Stop">■ STOP</sys:String>
|
||||||
|
<sys:String x:Key="Test.Report">📄 Report</sys:String>
|
||||||
|
<sys:String x:Key="Test.ShowValues">Show values</sys:String>
|
||||||
|
<sys:String x:Key="Test.CheckAll">Check All</sys:String>
|
||||||
|
<sys:String x:Key="Test.SecondsRemaining">s remaining</sys:String>
|
||||||
|
<sys:String x:Key="Test.Condition">Cond:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Measurement">Meas:</sys:String>
|
||||||
|
<sys:String x:Key="Test.MeasPerSec">M/s:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Required">Required:</sys:String>
|
||||||
|
<sys:String x:Key="Test.TestLabel">Test:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Critical">Critical</sys:String>
|
||||||
|
<sys:String x:Key="Test.Started">Test started...</sys:String>
|
||||||
|
<sys:String x:Key="Test.Stopped">Test stopped.</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Test types ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="TestType.Warmup">Warm-up</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Adjustment">Adjustment</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Flow">Flow</sys:String>
|
||||||
|
<sys:String x:Key="TestType.ServoValve">Servo valve</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Upstroke">Upstroke</sys:String>
|
||||||
|
<sys:String x:Key="TestType.PreInjection">Pre-injection</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Result display ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Result.Phase">Phase</sys:String>
|
||||||
|
<sys:String x:Key="Result.Parameter">Parameter</sys:String>
|
||||||
|
<sys:String x:Key="Result.Target">Target</sys:String>
|
||||||
|
<sys:String x:Key="Result.Tolerance">Tol ±</sys:String>
|
||||||
|
<sys:String x:Key="Result.Average">Average</sys:String>
|
||||||
|
<sys:String x:Key="Result.ResultHeader">Result</sys:String>
|
||||||
|
<sys:String x:Key="Result.AllTests">All Tests</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Common strings ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Common.Pass">PASS</sys:String>
|
||||||
|
<sys:String x:Key="Common.Fail">FAIL</sys:String>
|
||||||
|
<sys:String x:Key="Common.Accept">Accept</sys:String>
|
||||||
|
<sys:String x:Key="Common.Cancel">Cancel</sys:String>
|
||||||
|
<sys:String x:Key="Common.Close">Close</sys:String>
|
||||||
|
<sys:String x:Key="Common.Save">Save</sys:String>
|
||||||
|
<sys:String x:Key="Common.Ok">OK</sys:String>
|
||||||
|
<sys:String x:Key="Common.Yes">Yes</sys:String>
|
||||||
|
<sys:String x:Key="Common.No">No</sys:String>
|
||||||
|
<sys:String x:Key="Common.Warning">WARNING</sys:String>
|
||||||
|
<sys:String x:Key="Common.Disabled">disabled</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Report ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Report.Title">Generate Report</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientList">Client List</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientData">Client Data</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.CompanyData">Company Data</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Name">Name:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientInfo">Client information</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Observations">Observations</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Operator">Operator:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Company">Company:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.CompanyInfo">Company information</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.DeleteClient">Delete Client</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Generate">Generate</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: User authentication ──────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Title">User Authentication</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Username">Username:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Password">Password:</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Voltage warning ──────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Voltage.Title">Power Supply Warning</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.VoltageLabel">VOLTAGE: </sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.PumpRequires">The selected pump requires </sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.PowerSupply"> power supply.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.SwitchTo">SWITCH THE POWER SUPPLY TO </sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Oil pump confirmation ────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.OilPump.Title">Oil Pump Activation</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.OilPump.Message">You are about to activate the oil pump. Confirm that the oil level is adequate and all connections are secure before proceeding.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.OilPump.LeaksChecked">I have checked — no leaks, connections are ready</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: RPM safety warning ───────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.Title">RPM Safety Warning</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.Message">The oil pump is OFF. Running the bench motor without oil circulation can cause bearing damage to the pump.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.OilAndProceed">Turn on oil pump and start the bench</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.ProceedWithout">I know what I'm doing — start without oil</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Unlock progress ──────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Title">Immobilizer Unlock</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Progress">PROGRESS</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase1">Phase 1: Sending unlock signals</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase2Testing">Phase 2: Testing</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase2Sending">Phase 2: Sending...</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Cancelled">CANCELLED</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Unlocked">UNLOCKED</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Failed">UNLOCK FAILED</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.TypeLabel">Type {0}</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: K-Line errors ────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Title">K-Line Fault Codes</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Header">Fault codes:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Read">Read</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Clear">Clear</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Settings ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Settings.Title">Settings</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.General">General</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Safety">Safety</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Pid">PID</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Motor">Motor</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Company">Company</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.KLine">K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Advanced">Advanced</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.Language">Language:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.DaysKeepLogs">Days to keep logs:</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.TempMax">Max. temperature (°C):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.TempMin">Min. temperature (°C):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.SecurityRpmLimit">Safety RPM limit:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.MaxPressureBar">Max. pressure (bar):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.ToleranceUp">UP tolerance extension:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.TolerancePfp">PFP tolerance extension:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.IgnoreTin">Ignore T-in by default</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidP">Proportional (P):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidI">Integral (I):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidD">Derivative (D):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidLoopMs">Loop period (ms):</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.EncoderRes">Encoder resolution:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.VoltMaxRpm">Voltage for max RPM (V):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.MaxRpm">Max RPM:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RightRelay">Right = relay ON</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Relations">RPM-Voltage Table</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RelRpm">RPM</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RelVoltage">Voltage (V)</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.AddRow">Add</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RemoveRow">Remove</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.CompanyName">Company name:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.CompanyInfo">Company information:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.ReportLogo">Report logo path:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.BrowseLogoTitle">Select Logo Image</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.KLinePort">K-Line port (FTDI):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPorts">Refresh</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.KLineHint">Select an FTDI device or type the serial number manually.</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshBench">Bench interface (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshReading">While reading K-Line (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshCanBus">CAN bus read (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPumpReq">Pump request (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPumpParams">Pump params (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.BlinkInterval">Blink interval (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.FlasherInterval">Flasher interval (ms):</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Error messages ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Error.ReportGeneration">Failed to generate report:\n{0}</sys:String>
|
||||||
|
<sys:String x:Key="Error.ReportTitle">Report Error</sys:String>
|
||||||
|
<sys:String x:Key="Error.PsgSync">PSG sync pulse not detected. Check encoder connection.</sys:String>
|
||||||
|
<sys:String x:Key="Error.PsgTitle">PSG Error</sys:String>
|
||||||
|
<sys:String x:Key="Error.EmergencyStop">EMERGENCY STOP: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Error.KLineNotFound">K-Line device not found. Check that the FTDI adapter is connected.</sys:String>
|
||||||
|
<sys:String x:Key="Error.KLineTitle">K-Line Error</sys:String>
|
||||||
|
<sys:String x:Key="Error.AuthInvalid">Invalid username or password.\n(Both are case-sensitive.)</sys:String>
|
||||||
|
<sys:String x:Key="Error.AuthTitle">Authentication Error</sys:String>
|
||||||
|
|
||||||
|
<!-- ── PDF Report ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Pdf.ReportTitle">VP44 INJECTION PUMP TEST REPORT</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.GeneratedBy">Generated by HC-APTBS</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Page">Page </sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Of"> of </sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Date">Date: {0:dd/MM/yyyy HH:mm}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Operator">Operator: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Client">Client: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PumpIdentification">PUMP IDENTIFICATION</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PumpId">Pump ID:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Model">Model:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.SerialNo">Serial No.:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Injector">Injector:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Tube">Tube:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Valve">Valve:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Tension">Tension:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Rotation">Rotation:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.LockAngle">Lock Angle:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Measured">Measured:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Chaveta">Chaveta:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PreInj">Pre-Inj.:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.EcuData">ECU DATA (K-Line)</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.OverallResult">OVERALL TEST RESULT</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.TestsExecuted">Tests executed: {0} of {1}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ParamsEvaluated">Parameters evaluated: {0} / {1} passed</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.TestHeader">TEST: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Phase">Phase</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Parameter">Parameter</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Target">Target</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ToleranceHeader">Tolerance ±</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Average">Average</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Result">Result</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ErrorBits"> ⚠ Error bits: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.NoSampleData">No sample data available for graphical display.</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ChartSamples">Samples: {0} | Target: {1} ± {2} | Average: {3} | Result: {4}</sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
322
Resources/Strings.es.xaml
Normal file
322
Resources/Strings.es.xaml
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════════════════════════════════════════
|
||||||
|
Spanish (ESP) — Default language
|
||||||
|
══════════════════════════════════════════════════════════════════ -->
|
||||||
|
|
||||||
|
<!-- ── App ──────────────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="App.Title">HC_APTBS — Herlic Banco de Pruebas</sys:String>
|
||||||
|
<sys:String x:Key="App.LanguageLabel">ENG</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Menu ─────────────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Menu.Settings">Configuración</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Status bar / connection indicators ───────────────────────────── -->
|
||||||
|
<sys:String x:Key="Status.Label">Estado:</sys:String>
|
||||||
|
<sys:String x:Key="Status.Can">CAN</sys:String>
|
||||||
|
<sys:String x:Key="Status.Bench">Banco</sys:String>
|
||||||
|
<sys:String x:Key="Status.Pump">Bomba</sys:String>
|
||||||
|
<sys:String x:Key="Status.KLine">K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Status.Connected">Conectado</sys:String>
|
||||||
|
<sys:String x:Key="Status.ConnectionFailed">Fallo de conexión</sys:String>
|
||||||
|
<sys:String x:Key="Status.Disconnected">Desconectado</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench section ────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Bench.Header">Banco</sys:String>
|
||||||
|
<sys:String x:Key="Bench.ConnectCan">Conectar CAN</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DisconnectCan">Desconectar CAN</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Rpm">rpm</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Target">Objetivo:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Voltage">V:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempIn">T. Ent.:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempOut">T. Sal.:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Temp4">T. 4:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.TempTank">T. Tanq.:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.P1">P1:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.P2">P2:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.QDelivery">Q-Del.:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.QOver">Q-Over:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.CcStroke">cc/golpe</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpRpm">P-RPM:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpTemp">P-Temp:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpMe">P-ME:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PumpFbkw">P-FBkW:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.PsgEncoder">Encoder PSG:</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench controls ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Bench.Direction">Dirección</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Left">IZQUIERDA</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Right">DERECHA</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Motor">Motor del Banco</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Start">ARRANCAR</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Stop">PARAR</sys:String>
|
||||||
|
<sys:String x:Key="Bench.SetRpm">Fijar RPM:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Go">IR</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilPump">Bomba de Aceite</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilOff">ACEITE OFF</sys:String>
|
||||||
|
<sys:String x:Key="Bench.OilOn">ACEITE ON</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Counter">Contador</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Turns">Vueltas:</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Send">Enviar</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Relays">Relés</sys:String>
|
||||||
|
<sys:String x:Key="Bench.Electronic">Electrónico</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DepositCooler">Enfriador Depósito</sys:String>
|
||||||
|
<sys:String x:Key="Bench.DepositHeater">Calentador Depósito</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump live data ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Pump.THyb">T-hyb</sys:String>
|
||||||
|
<sys:String x:Key="Pump.Rpm">RPM</sys:String>
|
||||||
|
<sys:String x:Key="Pump.TEin">T-ein</sys:String>
|
||||||
|
<sys:String x:Key="Pump.UnitRpm">1/min</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump identification ──────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="PumpId.Label">Bomba:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Dfi">DFI:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.PumpId">ID Bomba:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SerialNo">Nro. Serie:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ModelRef">Ref. Modelo:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.DataRecord">Registro Datos:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.PumpCtrl">Ctrl. Bomba:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ModelIndex">Índice Modelo:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SwVer1">SW Ver 1:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.SwVer2">SW Ver 2:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Errors">Errores:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Error">Error:</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.ReadKLine">Leer K-Line</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.Disconnect">Desconectar</sys:String>
|
||||||
|
<sys:String x:Key="PumpId.NoKLineDevice">No se encontró dispositivo K-Line</sys:String>
|
||||||
|
|
||||||
|
<!-- ── DFI management ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dfi.Read">LEER</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Write">ESCRIBIR</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Label">DFI:</sys:String>
|
||||||
|
<sys:String x:Key="Dfi.Auto">AUTO</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Pump control sliders ─────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="PumpCtrl.Fbkw">FBKW - Control de Avance</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Me">ME - Control de Caudal</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.PreInj">ME - Caudal Pre-inyección</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.MinStepMax">Mín / Paso / Máx</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Min">Mín</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Step">Paso</sys:String>
|
||||||
|
<sys:String x:Key="PumpCtrl.Max">Máx</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Bench parameter config ───────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="BenchParam.CanBusId">CAN-Bus ID (0x)</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.ByteL">Byte L</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.ByteH">Byte H</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.FilterAlpha">Filtro α</sys:String>
|
||||||
|
<sys:String x:Key="BenchParam.EnableFormula">Habilitar fórmula</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Angle display ────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Angle.Header">MONITOREO DE AVANCE</sys:String>
|
||||||
|
<sys:String x:Key="Angle.Psg">PSG:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.Inj">INJ:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.AbsDeg">ABS º:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.LockDeg">LOCK º:</sys:String>
|
||||||
|
<sys:String x:Key="Angle.SetPsgZero">Fijar referencia cero PSG</sys:String>
|
||||||
|
<sys:String x:Key="Angle.SetInjZero">Fijar referencia cero INJ</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Test panel ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Test.StartTest">▶ INICIAR TEST</sys:String>
|
||||||
|
<sys:String x:Key="Test.Stop">■ PARAR</sys:String>
|
||||||
|
<sys:String x:Key="Test.Report">📄 Informe</sys:String>
|
||||||
|
<sys:String x:Key="Test.ShowValues">Mostrar valores</sys:String>
|
||||||
|
<sys:String x:Key="Test.CheckAll">Marcar Todos</sys:String>
|
||||||
|
<sys:String x:Key="Test.SecondsRemaining">s restantes</sys:String>
|
||||||
|
<sys:String x:Key="Test.Condition">Cond:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Measurement">Med:</sys:String>
|
||||||
|
<sys:String x:Key="Test.MeasPerSec">M/s:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Required">Requerido:</sys:String>
|
||||||
|
<sys:String x:Key="Test.TestLabel">Test:</sys:String>
|
||||||
|
<sys:String x:Key="Test.Critical">Crítico</sys:String>
|
||||||
|
<sys:String x:Key="Test.Started">Test iniciado...</sys:String>
|
||||||
|
<sys:String x:Key="Test.Stopped">Test detenido.</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Test types ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="TestType.Warmup">Calentamiento</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Adjustment">Ajuste</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Flow">Caudal</sys:String>
|
||||||
|
<sys:String x:Key="TestType.ServoValve">Servoválvula</sys:String>
|
||||||
|
<sys:String x:Key="TestType.Upstroke">Carrera Ascendente</sys:String>
|
||||||
|
<sys:String x:Key="TestType.PreInjection">Pre-inyección</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Result display ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Result.Phase">Fase</sys:String>
|
||||||
|
<sys:String x:Key="Result.Parameter">Parámetro</sys:String>
|
||||||
|
<sys:String x:Key="Result.Target">Objetivo</sys:String>
|
||||||
|
<sys:String x:Key="Result.Tolerance">Tol ±</sys:String>
|
||||||
|
<sys:String x:Key="Result.Average">Promedio</sys:String>
|
||||||
|
<sys:String x:Key="Result.ResultHeader">Resultado</sys:String>
|
||||||
|
<sys:String x:Key="Result.AllTests">Todos los Tests</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Common strings ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Common.Pass">APROBADO</sys:String>
|
||||||
|
<sys:String x:Key="Common.Fail">REPROBADO</sys:String>
|
||||||
|
<sys:String x:Key="Common.Accept">Aceptar</sys:String>
|
||||||
|
<sys:String x:Key="Common.Cancel">Cancelar</sys:String>
|
||||||
|
<sys:String x:Key="Common.Close">Cerrar</sys:String>
|
||||||
|
<sys:String x:Key="Common.Save">Guardar</sys:String>
|
||||||
|
<sys:String x:Key="Common.Ok">OK</sys:String>
|
||||||
|
<sys:String x:Key="Common.Yes">Sí</sys:String>
|
||||||
|
<sys:String x:Key="Common.No">No</sys:String>
|
||||||
|
<sys:String x:Key="Common.Warning">ADVERTENCIA</sys:String>
|
||||||
|
<sys:String x:Key="Common.Disabled">deshabilitado</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Report ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Report.Title">Generar Informe</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientList">Lista de Clientes</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientData">Datos del Cliente</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.CompanyData">Datos de la Empresa</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Name">Nombre:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.ClientInfo">Información del cliente</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Observations">Observaciones</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Operator">Operador:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Company">Empresa:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.CompanyInfo">Información de la empresa</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.DeleteClient">Eliminar Cliente</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Report.Generate">Generar</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: User authentication ──────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Title">Autenticación de Usuario</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Username">Usuario:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.UserCheck.Password">Contraseña:</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Voltage warning ──────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Voltage.Title">Advertencia de Fuente de Alimentación</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.VoltageLabel">VOLTAJE: </sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.PumpRequires">La bomba seleccionada requiere </sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.PowerSupply"> de alimentación.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Voltage.SwitchTo">CAMBIE LA FUENTE DE ALIMENTACIÓN A </sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Oil pump confirmation ────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.OilPump.Title">Activación de Bomba de Aceite</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.OilPump.Message">Está a punto de activar la bomba de aceite. Confirme que el nivel de aceite es adecuado y que todas las conexiones están seguras antes de continuar.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.OilPump.LeaksChecked">He verificado — sin fugas, conexiones listas</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: RPM safety warning ───────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.Title">Advertencia de Seguridad RPM</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.Message">La bomba de aceite está APAGADA. Hacer funcionar el motor del banco sin circulación de aceite puede causar daño a los rodamientos de la bomba.</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.OilAndProceed">Encender bomba de aceite e iniciar el banco</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.RpmSafety.ProceedWithout">Sé lo que hago — iniciar sin aceite</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Unlock progress ──────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Title">Desbloqueo de Inmovilizador</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Progress">PROGRESO</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase1">Fase 1: Enviando señales de desbloqueo</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase2Testing">Fase 2: Probando</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Phase2Sending">Fase 2: Enviando...</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Cancelled">CANCELADO</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Unlocked">DESBLOQUEADO</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.Failed">DESBLOQUEO FALLIDO</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Unlock.TypeLabel">Tipo {0}</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: K-Line errors ────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Title">Códigos de Falla K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Header">Códigos de falla:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Read">Leer</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.KlineErrors.Clear">Borrar</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Dialog: Settings ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Dialog.Settings.Title">Configuración</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.General">General</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Safety">Seguridad</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Pid">PID</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Motor">Motor</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Company">Empresa</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.KLine">K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Tab.Advanced">Avanzado</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.Language">Idioma:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.DaysKeepLogs">Días de retención de logs:</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.TempMax">Temperatura máx. (°C):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.TempMin">Temperatura mín. (°C):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.SecurityRpmLimit">Límite RPM de seguridad:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.MaxPressureBar">Presión máx. (bar):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.ToleranceUp">Extensión tolerancia UP:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.TolerancePfp">Extensión tolerancia PFP:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.IgnoreTin">Ignorar T-in por defecto</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidP">Proporcional (P):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidI">Integral (I):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidD">Derivativo (D):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.PidLoopMs">Período del lazo (ms):</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.EncoderRes">Resolución encoder:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.VoltMaxRpm">Voltaje para RPM máx. (V):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.MaxRpm">RPM máximo:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RightRelay">Derecha = relé ON</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.Relations">Tabla RPM-Voltaje</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RelRpm">RPM</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RelVoltage">Voltaje (V)</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.AddRow">Agregar</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RemoveRow">Eliminar</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.CompanyName">Nombre empresa:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.CompanyInfo">Información empresa:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.ReportLogo">Ruta logo de informe:</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.BrowseLogoTitle">Seleccionar imagen de logo</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.KLinePort">Puerto K-Line (FTDI):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPorts">Refrescar</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.KLineHint">Seleccione un dispositivo FTDI o ingrese el número de serie manualmente.</sys:String>
|
||||||
|
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshBench">Interfaz banco (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshReading">Durante lectura K-Line (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshCanBus">Lectura CAN bus (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPumpReq">Solicitud bomba (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.RefreshPumpParams">Parámetros bomba (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.BlinkInterval">Intervalo parpadeo (ms):</sys:String>
|
||||||
|
<sys:String x:Key="Dialog.Settings.FlasherInterval">Intervalo flasher (ms):</sys:String>
|
||||||
|
|
||||||
|
<!-- ── Error messages ───────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Error.ReportGeneration">Error al generar informe:\n{0}</sys:String>
|
||||||
|
<sys:String x:Key="Error.ReportTitle">Error de Informe</sys:String>
|
||||||
|
<sys:String x:Key="Error.PsgSync">No se detectó el pulso de sincronización PSG. Verifique la conexión del encoder.</sys:String>
|
||||||
|
<sys:String x:Key="Error.PsgTitle">Error PSG</sys:String>
|
||||||
|
<sys:String x:Key="Error.EmergencyStop">PARADA DE EMERGENCIA: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Error.KLineNotFound">Dispositivo K-Line no encontrado. Verifique que el adaptador FTDI esté conectado.</sys:String>
|
||||||
|
<sys:String x:Key="Error.KLineTitle">Error K-Line</sys:String>
|
||||||
|
<sys:String x:Key="Error.AuthInvalid">Usuario o contraseña inválidos.\n(Ambos distinguen mayúsculas y minúsculas.)</sys:String>
|
||||||
|
<sys:String x:Key="Error.AuthTitle">Error de Autenticación</sys:String>
|
||||||
|
|
||||||
|
<!-- ── PDF Report ───────────────────────────────────────────────────── -->
|
||||||
|
<sys:String x:Key="Pdf.ReportTitle">INFORME DE PRUEBA DE BOMBA INYECTORA VP44</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.GeneratedBy">Generado por HC-APTBS</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Page">Página </sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Of"> de </sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Date">Fecha: {0:dd/MM/yyyy HH:mm}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Operator">Operador: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Client">Cliente: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PumpIdentification">IDENTIFICACIÓN DE BOMBA</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PumpId">ID Bomba:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Model">Modelo:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.SerialNo">Nro. Serie:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Injector">Inyector:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Tube">Tubo:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Valve">Válvula:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Tension">Tensión:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Rotation">Rotación:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.LockAngle">Ángulo de Bloqueo:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Measured">Medido:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Chaveta">Chaveta:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.PreInj">Pre-Iny.:</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.EcuData">DATOS ECU (K-Line)</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.OverallResult">RESULTADO GENERAL DEL TEST</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.TestsExecuted">Tests ejecutados: {0} de {1}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ParamsEvaluated">Parámetros evaluados: {0} / {1} aprobados</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.TestHeader">TEST: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Phase">Fase</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Parameter">Parámetro</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Target">Objetivo</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ToleranceHeader">Tolerancia ±</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Average">Promedio</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.Result">Resultado</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ErrorBits"> ⚠ Bits de error: {0}</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.NoSampleData">No hay datos de muestra disponibles para visualización gráfica.</sys:String>
|
||||||
|
<sys:String x:Key="Pdf.ChartSamples">Muestras: {0} | Objetivo: {1} ± {2} | Promedio: {3} | Resultado: {4}</sys:String>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -36,6 +36,13 @@ namespace HC_APTBS.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<string , bool >? PhaseCompleted; // phaseName,passed
|
event Action<string , bool >? PhaseCompleted; // phaseName,passed
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when a safety check triggers an emergency stop. The bench motor
|
||||||
|
/// and pump parameters are already stopped when this fires.
|
||||||
|
/// Fires on a background thread — consumers must marshal to the UI thread.
|
||||||
|
/// </summary>
|
||||||
|
event Action<string >? EmergencyStopTriggered; //reason
|
||||||
|
|
||||||
// ── Active pump ───────────────────────────────────────────────────────────
|
// ── Active pump ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -213,5 +220,11 @@ namespace HC_APTBS.Services
|
|||||||
/// Raised so the chart view can draw tolerance bands for the specified parameter.
|
/// Raised so the chart view can draw tolerance bands for the specified parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<string , double , double >? ToleranceUpdated; //parameterName, value, tolerance
|
event Action<string , double , double >? ToleranceUpdated; //parameterName, value, tolerance
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised for each individual measurement sample collected during a test phase.
|
||||||
|
/// Fires on a background thread — consumers must marshal to the UI thread.
|
||||||
|
/// </summary>
|
||||||
|
event Action<string , double >? MeasurementSampled; //parameterName, value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
|
||||||
namespace HC_APTBS.Services
|
namespace HC_APTBS.Services
|
||||||
@@ -73,8 +74,8 @@ namespace HC_APTBS.Services
|
|||||||
/// <summary>Validates a username/password pair against stored credentials.</summary>
|
/// <summary>Validates a username/password pair against stored credentials.</summary>
|
||||||
bool ValidateUser(string username, string password);
|
bool ValidateUser(string username, string password);
|
||||||
|
|
||||||
/// <summary>Returns all stored user credentials as a dictionary.</summary>
|
/// <summary>Returns all stored usernames (passwords are never exposed).</summary>
|
||||||
IReadOnlyDictionary<string, string> GetUsers();
|
IReadOnlyList<string> GetUsers();
|
||||||
|
|
||||||
/// <summary>Replaces all stored user credentials and persists them.</summary>
|
/// <summary>Replaces all stored user credentials and persists them.</summary>
|
||||||
void UpdateUsers(Dictionary<string, string> users);
|
void UpdateUsers(Dictionary<string, string> users);
|
||||||
|
|||||||
@@ -126,6 +126,16 @@ namespace HC_APTBS.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<double>? DfiRead;
|
event Action<double>? DfiRead;
|
||||||
|
|
||||||
|
// ── Fast immobilizer unlock ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts a fast immobilizer unlock by sending a KWP custom command
|
||||||
|
/// over an existing K-Line session. Returns <see langword="true"/> if the
|
||||||
|
/// command was acknowledged (pump already unlocked), <see langword="false"/>
|
||||||
|
/// if it was rejected or no session is active.
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> TryFastUnlockAsync();
|
||||||
|
|
||||||
// ── Power cycle callbacks ─────────────────────────────────────────────────
|
// ── Power cycle callbacks ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
39
Services/ILocalizationService.cs
Normal file
39
Services/ILocalizationService.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides runtime language switching and localised string retrieval.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// XAML bindings use <c>{DynamicResource Key}</c> which update automatically
|
||||||
|
/// when the merged <see cref="System.Windows.ResourceDictionary"/> is swapped.
|
||||||
|
/// C# code uses <see cref="GetString"/> for the same keys.
|
||||||
|
/// </remarks>
|
||||||
|
public interface ILocalizationService
|
||||||
|
{
|
||||||
|
/// <summary>Current language code — <c>"ESP"</c> or <c>"ENG"</c>.</summary>
|
||||||
|
string CurrentLanguage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Switches the active UI language by swapping the merged resource dictionary
|
||||||
|
/// and persisting the choice to <c>config.xml</c>.
|
||||||
|
/// Must be called from the UI thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="languageCode"><c>"ESP"</c> for Spanish or <c>"ENG"</c> for English.</param>
|
||||||
|
void SetLanguage(string languageCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a localised string by resource key.
|
||||||
|
/// Returns the key itself when no matching resource is found (fail-visible).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Resource key defined in <c>Resources/Strings.*.xaml</c>.</param>
|
||||||
|
string GetString(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised after the active language dictionary has been swapped.
|
||||||
|
/// ViewModels subscribe to refresh any cached localised strings.
|
||||||
|
/// </summary>
|
||||||
|
event Action? LanguageChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,18 @@ namespace HC_APTBS.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the immobilizer unlock sequence for the given pump.
|
/// Runs the immobilizer unlock sequence for the given pump.
|
||||||
/// Returns immediately if <see cref="PumpDefinition.UnlockType"/> is 0 (no unlock needed).
|
/// Returns immediately if <see cref="PumpDefinition.UnlockType"/> is 0 (no unlock needed).
|
||||||
|
/// The persistent CAN senders remain active after this method returns;
|
||||||
|
/// call <see cref="StopSenders"/> when the pump is deselected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pump">Pump definition with unlock type and CAN parameters.</param>
|
/// <param name="pump">Pump definition with unlock type and CAN parameters.</param>
|
||||||
/// <param name="ct">Cancellation token to abort the unlock sequence.</param>
|
/// <param name="ct">Cancellation token to abort the unlock sequence.</param>
|
||||||
Task UnlockAsync(PumpDefinition pump, CancellationToken ct);
|
Task UnlockAsync(PumpDefinition pump, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the persistent CAN unlock senders. Call this when the pump is
|
||||||
|
/// deselected or the application is shutting down. Safe to call when no
|
||||||
|
/// senders are active.
|
||||||
|
/// </summary>
|
||||||
|
void StopSenders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ namespace HC_APTBS.Services.Impl
|
|||||||
private CancellationTokenSource? _relaySendCts;
|
private CancellationTokenSource? _relaySendCts;
|
||||||
private volatile bool _relaySendActive;
|
private volatile bool _relaySendActive;
|
||||||
|
|
||||||
|
// Alarm bitmask snapshot for edge detection during test phases
|
||||||
|
private int _lastAlarmMask;
|
||||||
|
|
||||||
|
// QOver zero-flow safety debounce (elapsed ms from phase stopwatch)
|
||||||
|
private long _qOverZeroSinceMs;
|
||||||
|
private const int QOverDebounceSec = 3;
|
||||||
|
|
||||||
// RPM PID ramp controller
|
// RPM PID ramp controller
|
||||||
private BenchPidController? _pidController;
|
private BenchPidController? _pidController;
|
||||||
private double _lastTargetRpm;
|
private double _lastTargetRpm;
|
||||||
@@ -92,6 +99,10 @@ namespace HC_APTBS.Services.Impl
|
|||||||
public event Action<string, double>? PumpControlValueSet;
|
public event Action<string, double>? PumpControlValueSet;
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event Action? RpmCommandSent;
|
public event Action? RpmCommandSent;
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<string, double>? MeasurementSampled;
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event Action<string>? EmergencyStopTriggered;
|
||||||
|
|
||||||
// ── Constructor ───────────────────────────────────────────────────────────
|
// ── Constructor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -615,6 +626,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
{
|
{
|
||||||
_cts?.Cancel();
|
_cts?.Cancel();
|
||||||
SetRpm(0);
|
SetRpm(0);
|
||||||
|
ZeroPumpParameters();
|
||||||
_log.Info(LogId, "Test sequence stopped by operator.");
|
_log.Info(LogId, "Test sequence stopped by operator.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,6 +645,8 @@ namespace HC_APTBS.Services.Impl
|
|||||||
phase.Success = true;
|
phase.Success = true;
|
||||||
phase.ClearResults();
|
phase.ClearResults();
|
||||||
phase.ErrorBits.Clear();
|
phase.ErrorBits.Clear();
|
||||||
|
_lastAlarmMask = (int)ReadBenchParameter(BenchParameterNames.Alarms);
|
||||||
|
_qOverZeroSinceMs = 0;
|
||||||
|
|
||||||
// SVME test: check that the PSG encoder sync pulse is present before proceeding.
|
// SVME test: check that the PSG encoder sync pulse is present before proceeding.
|
||||||
if (!phase.Name.Contains("Lock Angle") &&
|
if (!phase.Name.Contains("Lock Angle") &&
|
||||||
@@ -727,6 +741,8 @@ namespace HC_APTBS.Services.Impl
|
|||||||
for (int i = 0; i * 1000 < conditioningRemainMs; i++)
|
for (int i = 0; i * 1000 < conditioningRemainMs; i++)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
CheckQOverSafety(i * 1000L);
|
||||||
|
PollAlarms(phase);
|
||||||
int remaining = (int)(conditioningRemainMs / 1000) - i;
|
int remaining = (int)(conditioningRemainMs / 1000) - i;
|
||||||
VerboseMessage?.Invoke($"{phase.Name} — Conditioning... {remaining}s");
|
VerboseMessage?.Invoke($"{phase.Name} — Conditioning... {remaining}s");
|
||||||
await Task.Delay(1000, ct);
|
await Task.Delay(1000, ct);
|
||||||
@@ -768,12 +784,14 @@ namespace HC_APTBS.Services.Impl
|
|||||||
if (phase.IsCritical && !phase.Success)
|
if (phase.IsCritical && !phase.Success)
|
||||||
{
|
{
|
||||||
SetRpm(0);
|
SetRpm(0);
|
||||||
|
ZeroPumpParameters();
|
||||||
VerboseMessage?.Invoke($"CRITICAL failure in {phase.Name} — test halted.");
|
VerboseMessage?.Invoke($"CRITICAL failure in {phase.Name} — test halted.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop pump between phases (motor cool-down).
|
// Stop pump between phases (motor cool-down).
|
||||||
SetRpm(0);
|
SetRpm(0);
|
||||||
|
ZeroPumpParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
@@ -799,6 +817,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
while (sw.ElapsedMilliseconds <= measureMs)
|
while (sw.ElapsedMilliseconds <= measureMs)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
CheckQOverSafety(sw.ElapsedMilliseconds);
|
||||||
|
|
||||||
foreach (var tp in phase.Receives)
|
foreach (var tp in phase.Receives)
|
||||||
{
|
{
|
||||||
@@ -808,8 +827,10 @@ namespace HC_APTBS.Services.Impl
|
|||||||
Timestamp = DateTime.Now.ToString(TestDefinition.TimestampFormat)
|
Timestamp = DateTime.Now.ToString(TestDefinition.TimestampFormat)
|
||||||
};
|
};
|
||||||
tp.Result!.AddSample(sample);
|
tp.Result!.AddSample(sample);
|
||||||
|
MeasurementSampled?.Invoke(tp.Name, sample.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PollAlarms(phase);
|
||||||
await Task.Delay(sleepMs, ct);
|
await Task.Delay(sleepMs, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,6 +900,114 @@ namespace HC_APTBS.Services.Impl
|
|||||||
return target.Result?.Passed ?? false;
|
return target.Result?.Passed ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Safety helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immediately zeroes all pump control parameters (ME, FBKW, PreIn) and
|
||||||
|
/// transmits the zero values over CAN. Bypasses the slew-rate IIR filter
|
||||||
|
/// by writing directly to parameter values and clearing the target fields.
|
||||||
|
/// </summary>
|
||||||
|
private void ZeroPumpParameters()
|
||||||
|
{
|
||||||
|
if (_activePump == null) return;
|
||||||
|
|
||||||
|
// Zero the slew-rate targets so the periodic sender doesn't ramp back up.
|
||||||
|
_targetMe = 0;
|
||||||
|
_targetFbkw = 0;
|
||||||
|
_targetPreIn = 0;
|
||||||
|
|
||||||
|
// Write zero directly to the parameter values (bypassing the IIR filter).
|
||||||
|
CanBusParameter? meParam = null;
|
||||||
|
if (_activePump.ParametersByName.TryGetValue(PumpParameterNames.Me, out meParam))
|
||||||
|
{
|
||||||
|
meParam.Value = 0;
|
||||||
|
_can.SendMessageById(meParam.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activePump.ParametersByName.TryGetValue(PumpParameterNames.Fbkw, out var fbkwParam))
|
||||||
|
{
|
||||||
|
fbkwParam.Value = 0;
|
||||||
|
if (meParam == null || fbkwParam.MessageId != meParam.MessageId)
|
||||||
|
_can.SendMessageById(fbkwParam.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activePump.HasPreInjection &&
|
||||||
|
_activePump.ParametersByName.TryGetValue(PumpParameterNames.PreIn, out var preinParam))
|
||||||
|
{
|
||||||
|
preinParam.Value = 0;
|
||||||
|
_can.SendMessageById(preinParam.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Debug(LogId, "ZeroPumpParameters: ME, FBKW, PreIn zeroed and transmitted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the QOver zero-flow condition. If QOver reads 0 while the bench
|
||||||
|
/// motor is above 300 RPM and the oil pump relay is energised, and this
|
||||||
|
/// condition persists for <see cref="QOverDebounceSec"/> seconds, triggers
|
||||||
|
/// an emergency stop.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="elapsedMs">Current elapsed milliseconds (from the phase stopwatch or loop counter).</param>
|
||||||
|
private void CheckQOverSafety(long elapsedMs)
|
||||||
|
{
|
||||||
|
double qOver = ReadBenchParameter(BenchParameterNames.QOver);
|
||||||
|
double benchRpm = ReadBenchParameter(BenchParameterNames.BenchRpm);
|
||||||
|
bool oilPumpOn = _config.Bench.Relays.TryGetValue(RelayNames.OilPump, out var relay)
|
||||||
|
&& relay.State;
|
||||||
|
|
||||||
|
if (qOver == 0 && benchRpm > 300 && oilPumpOn)
|
||||||
|
{
|
||||||
|
if (_qOverZeroSinceMs == 0)
|
||||||
|
_qOverZeroSinceMs = elapsedMs;
|
||||||
|
else if (elapsedMs - _qOverZeroSinceMs >= QOverDebounceSec * 1000)
|
||||||
|
{
|
||||||
|
_log.Error(LogId,
|
||||||
|
$"QOver zero-flow safety: QOver=0, BenchRPM={benchRpm:F0}, " +
|
||||||
|
$"OilPump=ON for {QOverDebounceSec}s — emergency stop.");
|
||||||
|
PerformEmergencyStop("QOver zero-flow: oil flow blocked while motor running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_qOverZeroSinceMs = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immediately stops the bench motor, zeroes pump parameters, cancels the
|
||||||
|
/// test sequence, and fires <see cref="EmergencyStopTriggered"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void PerformEmergencyStop(string reason)
|
||||||
|
{
|
||||||
|
SetRpm(0);
|
||||||
|
ZeroPumpParameters();
|
||||||
|
_cts?.Cancel();
|
||||||
|
EmergencyStopTriggered?.Invoke(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the current alarm bitmask from the Alarms CAN parameter, detects
|
||||||
|
/// bits that transitioned 0→1 since the last snapshot, and records them
|
||||||
|
/// in the given phase via <see cref="PhaseDefinition.RecordErrorBit"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void PollAlarms(PhaseDefinition phase)
|
||||||
|
{
|
||||||
|
int currentMask = (int)ReadBenchParameter(BenchParameterNames.Alarms);
|
||||||
|
int newBits = currentMask & ~_lastAlarmMask;
|
||||||
|
if (newBits != 0)
|
||||||
|
{
|
||||||
|
for (int bit = 0; bit < 16; bit++)
|
||||||
|
{
|
||||||
|
if ((newBits & (1 << bit)) != 0)
|
||||||
|
{
|
||||||
|
phase.RecordErrorBit(bit);
|
||||||
|
_log.Debug(LogId, $"Alarm bit {bit} recorded in phase {phase.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastAlarmMask = currentMask;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task WaitForParameter(
|
private async Task WaitForParameter(
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
using Peak.Can.Basic;
|
using Peak.Can.Basic;
|
||||||
@@ -460,6 +462,18 @@ namespace HC_APTBS.Services.Impl
|
|||||||
_log.Error(LogId, $"LoadSettings failed: {ex.Message}");
|
_log.Error(LogId, $"LoadSettings failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed default admin account if no users are configured.
|
||||||
|
if (string.IsNullOrEmpty(_settings.Users))
|
||||||
|
{
|
||||||
|
var (salt, hash) = HashPassword("admin");
|
||||||
|
_settings.Users = $"admin:{salt}:{hash}";
|
||||||
|
_log.Info(LogId, "No users configured — created default 'admin' account.");
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate plaintext user:password entries to hashed format.
|
||||||
|
MigrateUsersIfNeeded();
|
||||||
|
|
||||||
LoadSensors();
|
LoadSensors();
|
||||||
LoadClients();
|
LoadClients();
|
||||||
LoadAlarms();
|
LoadAlarms();
|
||||||
@@ -742,7 +756,83 @@ namespace HC_APTBS.Services.Impl
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Users ─────────────────────────────────────────────────────────────────
|
// ── Users (PBKDF2-HMAC-SHA256 hashed credentials) ─────────────────────────
|
||||||
|
|
||||||
|
private const int SaltBytes = 16;
|
||||||
|
private const int HashBytes = 32;
|
||||||
|
private const int Pbkdf2Iterations = 600_000;
|
||||||
|
|
||||||
|
/// <summary>Generates a random salt and computes the PBKDF2-HMAC-SHA256 hash for <paramref name="password"/>.</summary>
|
||||||
|
private static (string salt, string hash) HashPassword(string password)
|
||||||
|
{
|
||||||
|
byte[] salt = RandomNumberGenerator.GetBytes(SaltBytes);
|
||||||
|
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256, HashBytes);
|
||||||
|
return (Convert.ToBase64String(salt), Convert.ToBase64String(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies <paramref name="password"/> against the given Base64 <paramref name="salt"/> and <paramref name="expectedHash"/>.</summary>
|
||||||
|
private static bool VerifyPassword(string password, string salt, string expectedHash)
|
||||||
|
{
|
||||||
|
byte[] saltBytes = Convert.FromBase64String(salt);
|
||||||
|
byte[] computed = Rfc2898DeriveBytes.Pbkdf2(
|
||||||
|
password, saltBytes, Pbkdf2Iterations, HashAlgorithmName.SHA256, HashBytes);
|
||||||
|
return CryptographicOperations.FixedTimeEquals(computed, Convert.FromBase64String(expectedHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detects whether <see cref="AppSettings.Users"/> contains legacy plaintext
|
||||||
|
/// <c>user:password</c> entries and migrates them to <c>user:salt:hash</c>.
|
||||||
|
/// </summary>
|
||||||
|
private void MigrateUsersIfNeeded()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Settings.Users))
|
||||||
|
return;
|
||||||
|
|
||||||
|
string[] entries = Settings.Users.Split(',');
|
||||||
|
bool hasLegacy = false;
|
||||||
|
|
||||||
|
foreach (string entry in entries)
|
||||||
|
{
|
||||||
|
// New format always has exactly 3 colon-separated parts (user:salt:hash).
|
||||||
|
// Legacy format has exactly 2 parts (user:password).
|
||||||
|
// Base64 salt/hash never contain commas but may contain '=' padding —
|
||||||
|
// they will NOT contain additional colons, so Split(':') count is reliable.
|
||||||
|
string[] parts = entry.Split(':');
|
||||||
|
if (parts.Length == 2)
|
||||||
|
{
|
||||||
|
hasLegacy = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasLegacy) return;
|
||||||
|
|
||||||
|
var migrated = new List<string>(entries.Length);
|
||||||
|
foreach (string entry in entries)
|
||||||
|
{
|
||||||
|
string[] parts = entry.Split(':');
|
||||||
|
if (parts.Length == 2 && parts[0].Length > 0)
|
||||||
|
{
|
||||||
|
// Legacy entry — hash the plaintext password.
|
||||||
|
var (salt, hash) = HashPassword(parts[1]);
|
||||||
|
migrated.Add($"{parts[0]}:{salt}:{hash}");
|
||||||
|
}
|
||||||
|
else if (parts.Length == 3 && parts[0].Length > 0)
|
||||||
|
{
|
||||||
|
// Already migrated entry — keep as-is.
|
||||||
|
migrated.Add(entry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_log.Warning(LogId, $"Skipped malformed user entry during migration: '{entry}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.Users = string.Join(",", migrated);
|
||||||
|
SaveSettings();
|
||||||
|
_log.Info(LogId, $"Migrated {entries.Length} user credential(s) from plaintext to PBKDF2 hashed format.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool ValidateUser(string username, string password)
|
public bool ValidateUser(string username, string password)
|
||||||
@@ -750,25 +840,26 @@ namespace HC_APTBS.Services.Impl
|
|||||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
string check = username + ":" + password;
|
|
||||||
foreach (string entry in Settings.Users.Split(','))
|
foreach (string entry in Settings.Users.Split(','))
|
||||||
{
|
{
|
||||||
if (entry == check) return true;
|
string[] parts = entry.Split(':');
|
||||||
|
if (parts.Length == 3 && parts[0] == username)
|
||||||
|
return VerifyPassword(password, parts[1], parts[2]);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyDictionary<string, string> GetUsers()
|
public IReadOnlyList<string> GetUsers()
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<string, string>();
|
var names = new List<string>();
|
||||||
foreach (string kv in Settings.Users.Split(','))
|
foreach (string entry in Settings.Users.Split(','))
|
||||||
{
|
{
|
||||||
string[] parts = kv.Split(':');
|
string[] parts = entry.Split(':');
|
||||||
if (parts.Length == 2 && parts[0].Length > 0)
|
if (parts.Length == 3 && parts[0].Length > 0)
|
||||||
dict[parts[0]] = parts[1];
|
names.Add(parts[0]);
|
||||||
}
|
}
|
||||||
return dict;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -776,7 +867,10 @@ namespace HC_APTBS.Services.Impl
|
|||||||
{
|
{
|
||||||
var entries = new List<string>(users.Count);
|
var entries = new List<string>(users.Count);
|
||||||
foreach (var kv in users)
|
foreach (var kv in users)
|
||||||
entries.Add(kv.Key + ":" + kv.Value);
|
{
|
||||||
|
var (salt, hash) = HashPassword(kv.Value);
|
||||||
|
entries.Add($"{kv.Key}:{salt}:{hash}");
|
||||||
|
}
|
||||||
|
|
||||||
Settings.Users = string.Join(",", entries);
|
Settings.Users = string.Join(",", entries);
|
||||||
SaveSettings();
|
SaveSettings();
|
||||||
|
|||||||
@@ -608,6 +608,39 @@ namespace HC_APTBS.Services.Impl
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── IKwpService: fast immobilizer unlock ──────────────────────────────────
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<bool> TryFastUnlockAsync()
|
||||||
|
{
|
||||||
|
if (_kLineState != KLineConnectionState.Connected || _sessionKwp == null)
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "TryFastUnlock: no active K-Line session — skipping");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "TryFastUnlock: sending unlock command over K-Line");
|
||||||
|
var packets = _sessionKwp.SendCustom(
|
||||||
|
new List<byte> { 0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00 });
|
||||||
|
|
||||||
|
bool nak = packets.Count == 1
|
||||||
|
&& packets[0] is HC_APTBS.Infrastructure.Kwp.Packets.NakPacket;
|
||||||
|
|
||||||
|
_log.Info(LogId, $"TryFastUnlock: {(nak ? "NAK — pump rejected" : "ACK — pump unlocked")}");
|
||||||
|
return !nak;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Warning(LogId, $"TryFastUnlock failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── IKwpService: device detection ────────────────────────────────────────
|
// ── IKwpService: device detection ────────────────────────────────────────
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
83
Services/Impl/LocalizationService.cs
Normal file
83
Services/Impl/LocalizationService.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Services.Impl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages runtime language switching by swapping a merged
|
||||||
|
/// <see cref="ResourceDictionary"/> in <see cref="Application.Current"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// On construction the service reads <see cref="Models.AppSettings.Language"/>
|
||||||
|
/// and loads the corresponding dictionary. Subsequent calls to
|
||||||
|
/// <see cref="SetLanguage"/> replace it in-place and persist the preference.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class LocalizationService : ILocalizationService
|
||||||
|
{
|
||||||
|
private const string EspUri = "pack://application:,,,/Resources/Strings.es.xaml";
|
||||||
|
private const string EngUri = "pack://application:,,,/Resources/Strings.en.xaml";
|
||||||
|
|
||||||
|
private readonly IConfigurationService _config;
|
||||||
|
private ResourceDictionary? _currentDictionary;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string CurrentLanguage { get; private set; } = "ESP";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Action? LanguageChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialises the localization service and loads the language
|
||||||
|
/// stored in <see cref="Models.AppSettings.Language"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LocalizationService(IConfigurationService config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
// Load persisted language without saving (already persisted).
|
||||||
|
LoadDictionary(_config.Settings.Language);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetLanguage(string languageCode)
|
||||||
|
{
|
||||||
|
var code = NormaliseCode(languageCode);
|
||||||
|
if (code == CurrentLanguage)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoadDictionary(code);
|
||||||
|
|
||||||
|
// Persist the choice.
|
||||||
|
_config.Settings.Language = code;
|
||||||
|
_config.SaveSettings();
|
||||||
|
|
||||||
|
LanguageChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetString(string key)
|
||||||
|
{
|
||||||
|
return Application.Current.Resources[key]?.ToString() ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void LoadDictionary(string languageCode)
|
||||||
|
{
|
||||||
|
var code = NormaliseCode(languageCode);
|
||||||
|
var uri = code == "ENG" ? EngUri : EspUri;
|
||||||
|
|
||||||
|
var dict = new ResourceDictionary { Source = new Uri(uri, UriKind.Absolute) };
|
||||||
|
|
||||||
|
var merged = Application.Current.Resources.MergedDictionaries;
|
||||||
|
if (_currentDictionary != null)
|
||||||
|
merged.Remove(_currentDictionary);
|
||||||
|
|
||||||
|
merged.Add(dict);
|
||||||
|
_currentDictionary = dict;
|
||||||
|
CurrentLanguage = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormaliseCode(string code) =>
|
||||||
|
string.Equals(code, "ENG", StringComparison.OrdinalIgnoreCase) ? "ENG" : "ESP";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,12 +28,15 @@ namespace HC_APTBS.Services.Impl
|
|||||||
public sealed class PdfService : IPdfService
|
public sealed class PdfService : IPdfService
|
||||||
{
|
{
|
||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
private readonly byte[]? _defaultLogo;
|
private readonly byte[]? _defaultLogo;
|
||||||
|
|
||||||
/// <param name="configService">Provides company name, logo path, and report settings.</param>
|
/// <param name="configService">Provides company name, logo path, and report settings.</param>
|
||||||
public PdfService(IConfigurationService configService)
|
/// <param name="localizationService">Provides localised strings for report text.</param>
|
||||||
|
public PdfService(IConfigurationService configService, ILocalizationService localizationService)
|
||||||
{
|
{
|
||||||
_config = configService;
|
_config = configService;
|
||||||
|
_loc = localizationService;
|
||||||
|
|
||||||
// QuestPDF community licence — required for open-source use.
|
// QuestPDF community licence — required for open-source use.
|
||||||
QuestPDF.Settings.License = LicenseType.Community;
|
QuestPDF.Settings.License = LicenseType.Community;
|
||||||
@@ -121,11 +124,11 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// Date / operator / client block.
|
// Date / operator / client block.
|
||||||
row.ConstantItem(140).AlignRight().Column(col =>
|
row.ConstantItem(140).AlignRight().Column(col =>
|
||||||
{
|
{
|
||||||
col.Item().Text($"Date: {reportDate:dd/MM/yyyy HH:mm}")
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Date"), reportDate))
|
||||||
.FontSize(ReportTheme.CaptionSize + 1);
|
.FontSize(ReportTheme.CaptionSize + 1);
|
||||||
col.Item().Text($"Operator: {operatorName}")
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Operator"), operatorName))
|
||||||
.FontSize(ReportTheme.CaptionSize + 1);
|
.FontSize(ReportTheme.CaptionSize + 1);
|
||||||
col.Item().Text($"Client: {clientName}")
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Client"), clientName))
|
||||||
.FontSize(ReportTheme.CaptionSize + 1).Bold();
|
.FontSize(ReportTheme.CaptionSize + 1).Bold();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,7 +140,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// Report title.
|
// Report title.
|
||||||
outer.Item().PaddingTop(4).PaddingBottom(2)
|
outer.Item().PaddingTop(4).PaddingBottom(2)
|
||||||
.AlignCenter()
|
.AlignCenter()
|
||||||
.Text("VP44 INJECTION PUMP TEST REPORT")
|
.Text(_loc.GetString("Pdf.ReportTitle"))
|
||||||
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
||||||
.FontColor(ReportTheme.HeaderNavy);
|
.FontColor(ReportTheme.HeaderNavy);
|
||||||
});
|
});
|
||||||
@@ -146,23 +149,23 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// ── Footer ────────────────────────────────────────────────────────────────
|
// ── Footer ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Renders the page footer: divider, attribution, and page numbers.</summary>
|
/// <summary>Renders the page footer: divider, attribution, and page numbers.</summary>
|
||||||
private static void ComposeFooter(IContainer container)
|
private void ComposeFooter(IContainer container)
|
||||||
{
|
{
|
||||||
container.Column(col =>
|
container.Column(col =>
|
||||||
{
|
{
|
||||||
col.Item().LineHorizontal(0.5f).LineColor(ReportTheme.DividerLine);
|
col.Item().LineHorizontal(0.5f).LineColor(ReportTheme.DividerLine);
|
||||||
col.Item().PaddingTop(3).Row(row =>
|
col.Item().PaddingTop(3).Row(row =>
|
||||||
{
|
{
|
||||||
row.RelativeItem().Text("Generated by HC-APTBS")
|
row.RelativeItem().Text(_loc.GetString("Pdf.GeneratedBy"))
|
||||||
.FontSize(ReportTheme.FooterSize)
|
.FontSize(ReportTheme.FooterSize)
|
||||||
.FontColor(ReportTheme.HeaderGrey);
|
.FontColor(ReportTheme.HeaderGrey);
|
||||||
|
|
||||||
row.ConstantItem(100).AlignRight().Text(t =>
|
row.ConstantItem(100).AlignRight().Text(t =>
|
||||||
{
|
{
|
||||||
t.DefaultTextStyle(x => x.FontSize(ReportTheme.FooterSize));
|
t.DefaultTextStyle(x => x.FontSize(ReportTheme.FooterSize));
|
||||||
t.Span("Page ");
|
t.Span(_loc.GetString("Pdf.Page"));
|
||||||
t.CurrentPageNumber();
|
t.CurrentPageNumber();
|
||||||
t.Span(" of ");
|
t.Span(_loc.GetString("Pdf.Of"));
|
||||||
t.TotalPages();
|
t.TotalPages();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -172,7 +175,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// ── Content ───────────────────────────────────────────────────────────────
|
// ── Content ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Composes the full report body: pump info, ECU data, verdict, test sections.</summary>
|
/// <summary>Composes the full report body: pump info, ECU data, verdict, test sections.</summary>
|
||||||
private static void ComposeContent(IContainer container, PumpDefinition pump)
|
private void ComposeContent(IContainer container, PumpDefinition pump)
|
||||||
{
|
{
|
||||||
container.PaddingTop(6).Column(col =>
|
container.PaddingTop(6).Column(col =>
|
||||||
{
|
{
|
||||||
@@ -202,7 +205,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// ── Pump info table ───────────────────────────────────────────────────────
|
// ── Pump info table ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Renders the pump identification table with alternating row stripes.</summary>
|
/// <summary>Renders the pump identification table with alternating row stripes.</summary>
|
||||||
private static void ComposePumpInfoTable(IContainer container, PumpDefinition pump)
|
private void ComposePumpInfoTable(IContainer container, PumpDefinition pump)
|
||||||
{
|
{
|
||||||
container.Table(table =>
|
container.Table(table =>
|
||||||
{
|
{
|
||||||
@@ -219,7 +222,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
header.Cell().ColumnSpan(4)
|
header.Cell().ColumnSpan(4)
|
||||||
.Background(ReportTheme.HeaderNavy)
|
.Background(ReportTheme.HeaderNavy)
|
||||||
.Padding(5)
|
.Padding(5)
|
||||||
.Text("PUMP IDENTIFICATION")
|
.Text(_loc.GetString("Pdf.PumpIdentification"))
|
||||||
.FontColor(Colors.White).Bold()
|
.FontColor(Colors.White).Bold()
|
||||||
.FontSize(ReportTheme.SectionHeaderSize);
|
.FontSize(ReportTheme.SectionHeaderSize);
|
||||||
});
|
});
|
||||||
@@ -239,20 +242,20 @@ namespace HC_APTBS.Services.Impl
|
|||||||
.Text(value2).FontSize(ReportTheme.BodySize);
|
.Text(value2).FontSize(ReportTheme.BodySize);
|
||||||
}
|
}
|
||||||
|
|
||||||
AddRow("Pump ID:", pump.Id, "Model:", pump.Model);
|
AddRow(_loc.GetString("Pdf.PumpId"), pump.Id, _loc.GetString("Pdf.Model"), pump.Model);
|
||||||
AddRow("Serial No.:", pump.SerialNumber, "Injector:", pump.Injector);
|
AddRow(_loc.GetString("Pdf.SerialNo"), pump.SerialNumber, _loc.GetString("Pdf.Injector"), pump.Injector);
|
||||||
AddRow("Tube:", pump.Tube, "Valve:", pump.Valve);
|
AddRow(_loc.GetString("Pdf.Tube"), pump.Tube, _loc.GetString("Pdf.Valve"), pump.Valve);
|
||||||
AddRow("Tension:", pump.Tension, "Rotation:", pump.Rotation);
|
AddRow(_loc.GetString("Pdf.Tension"), pump.Tension, _loc.GetString("Pdf.Rotation"), pump.Rotation);
|
||||||
AddRow("Lock Angle:", $"{pump.LockAngle:F2}\u00B0",
|
AddRow(_loc.GetString("Pdf.LockAngle"), $"{pump.LockAngle:F2}\u00B0",
|
||||||
"Measured:", $"{pump.LockAngleResult:F2}\u00B0");
|
_loc.GetString("Pdf.Measured"), $"{pump.LockAngleResult:F2}\u00B0");
|
||||||
AddRow("Chaveta:", pump.Chaveta, "Pre-Inj.:", pump.HasPreInjection ? "Yes" : "No");
|
AddRow(_loc.GetString("Pdf.Chaveta"), pump.Chaveta, _loc.GetString("Pdf.PreInj"), pump.HasPreInjection ? _loc.GetString("Common.Yes") : _loc.GetString("Common.No"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── K-Line table ──────────────────────────────────────────────────────────
|
// ── K-Line table ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Renders the K-Line ECU data table with alternating row stripes.</summary>
|
/// <summary>Renders the K-Line ECU data table with alternating row stripes.</summary>
|
||||||
private static void ComposeKlineTable(IContainer container, PumpDefinition pump)
|
private void ComposeKlineTable(IContainer container, PumpDefinition pump)
|
||||||
{
|
{
|
||||||
container.Table(table =>
|
container.Table(table =>
|
||||||
{
|
{
|
||||||
@@ -269,7 +272,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
header.Cell().ColumnSpan(4)
|
header.Cell().ColumnSpan(4)
|
||||||
.Background(ReportTheme.HeaderNavy)
|
.Background(ReportTheme.HeaderNavy)
|
||||||
.Padding(5)
|
.Padding(5)
|
||||||
.Text("ECU DATA (K-Line)")
|
.Text(_loc.GetString("Pdf.EcuData"))
|
||||||
.FontColor(Colors.White).Bold()
|
.FontColor(Colors.White).Bold()
|
||||||
.FontSize(ReportTheme.SectionHeaderSize);
|
.FontSize(ReportTheme.SectionHeaderSize);
|
||||||
});
|
});
|
||||||
@@ -301,7 +304,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// ── Verdict section ───────────────────────────────────────────────────────
|
// ── Verdict section ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Renders the overall test result badge with summary statistics.</summary>
|
/// <summary>Renders the overall test result badge with summary statistics.</summary>
|
||||||
private static void ComposeVerdictSection(IContainer container, PumpDefinition pump)
|
private void ComposeVerdictSection(IContainer container, PumpDefinition pump)
|
||||||
{
|
{
|
||||||
// Compute summary statistics.
|
// Compute summary statistics.
|
||||||
var testsWithResults = pump.Tests.Where(t => t.HasResults()).ToList();
|
var testsWithResults = pump.Tests.Where(t => t.HasResults()).ToList();
|
||||||
@@ -339,16 +342,16 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// Summary statistics.
|
// Summary statistics.
|
||||||
row.RelativeItem().PaddingLeft(12).Column(col =>
|
row.RelativeItem().PaddingLeft(12).Column(col =>
|
||||||
{
|
{
|
||||||
col.Item().Text("OVERALL TEST RESULT")
|
col.Item().Text(_loc.GetString("Pdf.OverallResult"))
|
||||||
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
||||||
.FontColor(ReportTheme.HeaderNavy);
|
.FontColor(ReportTheme.HeaderNavy);
|
||||||
|
|
||||||
col.Item().PaddingTop(4).Text(
|
col.Item().PaddingTop(4).Text(
|
||||||
$"Tests executed: {testedCount} of {totalTests}")
|
string.Format(_loc.GetString("Pdf.TestsExecuted"), testedCount, totalTests))
|
||||||
.FontSize(ReportTheme.BodySize);
|
.FontSize(ReportTheme.BodySize);
|
||||||
|
|
||||||
col.Item().Text(
|
col.Item().Text(
|
||||||
$"Parameters evaluated: {passedPhases} / {totalPhases} passed")
|
string.Format(_loc.GetString("Pdf.ParamsEvaluated"), passedPhases, totalPhases))
|
||||||
.FontSize(ReportTheme.BodySize);
|
.FontSize(ReportTheme.BodySize);
|
||||||
|
|
||||||
// Per-test mini indicators.
|
// Per-test mini indicators.
|
||||||
@@ -383,7 +386,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
// ── Test results section ──────────────────────────────────────────────────
|
// ── Test results section ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Renders a single test: results table followed by measurement charts.</summary>
|
/// <summary>Renders a single test: results table followed by measurement charts.</summary>
|
||||||
private static void ComposeTestSection(IContainer container, TestDefinition test)
|
private void ComposeTestSection(IContainer container, TestDefinition test)
|
||||||
{
|
{
|
||||||
container.Column(col =>
|
container.Column(col =>
|
||||||
{
|
{
|
||||||
@@ -391,7 +394,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
col.Item()
|
col.Item()
|
||||||
.Background(ReportTheme.HeaderNavy)
|
.Background(ReportTheme.HeaderNavy)
|
||||||
.Padding(5)
|
.Padding(5)
|
||||||
.Text($"TEST: {test.Name}")
|
.Text(string.Format(_loc.GetString("Pdf.TestHeader"), test.Name))
|
||||||
.FontColor(Colors.White).Bold()
|
.FontColor(Colors.White).Bold()
|
||||||
.FontSize(ReportTheme.SectionHeaderSize);
|
.FontSize(ReportTheme.SectionHeaderSize);
|
||||||
|
|
||||||
@@ -405,7 +408,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Renders the pass/fail results table for one test.</summary>
|
/// <summary>Renders the pass/fail results table for one test.</summary>
|
||||||
private static void ComposeResultsTable(IContainer container, TestDefinition test)
|
private void ComposeResultsTable(IContainer container, TestDefinition test)
|
||||||
{
|
{
|
||||||
container.Table(table =>
|
container.Table(table =>
|
||||||
{
|
{
|
||||||
@@ -421,7 +424,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
|
|
||||||
table.Header(header =>
|
table.Header(header =>
|
||||||
{
|
{
|
||||||
foreach (var h in new[] { "Phase", "Parameter", "Target", "Tolerance \u00B1", "Average", "Result" })
|
foreach (var h in new[] { _loc.GetString("Pdf.Phase"), _loc.GetString("Pdf.Parameter"), _loc.GetString("Pdf.Target"), _loc.GetString("Pdf.ToleranceHeader"), _loc.GetString("Pdf.Average"), _loc.GetString("Pdf.Result") })
|
||||||
header.Cell()
|
header.Cell()
|
||||||
.Background(ReportTheme.AccentBlue)
|
.Background(ReportTheme.AccentBlue)
|
||||||
.Padding(ReportTheme.CellPad)
|
.Padding(ReportTheme.CellPad)
|
||||||
@@ -440,7 +443,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
if (tp.Result == null) continue;
|
if (tp.Result == null) continue;
|
||||||
|
|
||||||
bool passed = tp.Result.Passed;
|
bool passed = tp.Result.Passed;
|
||||||
string resultText = passed ? "PASS" : "FAIL";
|
string resultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||||
|
|
||||||
// Alternating base row colour, tinted by pass/fail.
|
// Alternating base row colour, tinted by pass/fail.
|
||||||
string bgColor = passed
|
string bgColor = passed
|
||||||
@@ -471,7 +474,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
table.Cell().ColumnSpan(6)
|
table.Cell().ColumnSpan(6)
|
||||||
.Background(ReportTheme.WarningBg)
|
.Background(ReportTheme.WarningBg)
|
||||||
.Padding(ReportTheme.CellPad)
|
.Padding(ReportTheme.CellPad)
|
||||||
.Text($" \u26A0 Error bits: {string.Join(", ", phase.ErrorBits)}")
|
.Text(string.Format(_loc.GetString("Pdf.ErrorBits"), string.Join(", ", phase.ErrorBits)))
|
||||||
.FontSize(ReportTheme.CaptionSize + 1)
|
.FontSize(ReportTheme.CaptionSize + 1)
|
||||||
.FontColor(ReportTheme.WarningText);
|
.FontColor(ReportTheme.WarningText);
|
||||||
}
|
}
|
||||||
@@ -480,7 +483,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Renders measurement charts for each parameter that has sample data.</summary>
|
/// <summary>Renders measurement charts for each parameter that has sample data.</summary>
|
||||||
private static void ComposeTestCharts(IContainer container, TestDefinition test)
|
private void ComposeTestCharts(IContainer container, TestDefinition test)
|
||||||
{
|
{
|
||||||
container.Column(col =>
|
container.Column(col =>
|
||||||
{
|
{
|
||||||
@@ -512,11 +515,13 @@ namespace HC_APTBS.Services.Impl
|
|||||||
.FitWidth();
|
.FitWidth();
|
||||||
|
|
||||||
// Chart caption.
|
// Chart caption.
|
||||||
|
string passFailText = tp.Result.Passed
|
||||||
|
? _loc.GetString("Common.Pass")
|
||||||
|
: _loc.GetString("Common.Fail");
|
||||||
col.Item().PaddingBottom(ReportTheme.SubsectionGap)
|
col.Item().PaddingBottom(ReportTheme.SubsectionGap)
|
||||||
.Text($"Samples: {tp.Result.Samples.Count} | " +
|
.Text(string.Format(_loc.GetString("Pdf.ChartSamples"),
|
||||||
$"Target: {tp.Value:F2} \u00B1 {tp.Tolerance:F2} | " +
|
tp.Result.Samples.Count, tp.Value, tp.Tolerance,
|
||||||
$"Average: {tp.Result.Average:F2} | " +
|
tp.Result.Average, passFailText))
|
||||||
$"Result: {(tp.Result.Passed ? "PASS" : "FAIL")}")
|
|
||||||
.FontSize(ReportTheme.CaptionSize)
|
.FontSize(ReportTheme.CaptionSize)
|
||||||
.FontColor(ReportTheme.HeaderGrey);
|
.FontColor(ReportTheme.HeaderGrey);
|
||||||
}
|
}
|
||||||
@@ -525,7 +530,7 @@ namespace HC_APTBS.Services.Impl
|
|||||||
if (!anyChart)
|
if (!anyChart)
|
||||||
{
|
{
|
||||||
col.Item().PaddingTop(2).PaddingBottom(4)
|
col.Item().PaddingTop(2).PaddingBottom(4)
|
||||||
.Text("No sample data available for graphical display.")
|
.Text(_loc.GetString("Pdf.NoSampleData"))
|
||||||
.FontSize(ReportTheme.CaptionSize).Italic()
|
.FontSize(ReportTheme.CaptionSize).Italic()
|
||||||
.FontColor(ReportTheme.HeaderGrey);
|
.FontColor(ReportTheme.HeaderGrey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,43 @@ namespace HC_APTBS.Services.Impl
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements the immobilizer unlock sequence for Ford VP44 pump ECUs.
|
/// Implements the immobilizer unlock sequence for Ford VP44 pump ECUs.
|
||||||
/// The unlock has two phases:
|
/// <para>The CAN flood messages must start before unlocking and continue running
|
||||||
|
/// after unlock completes — stopping them causes the pump to re-lock. Call
|
||||||
|
/// <see cref="StopSenders"/> only when the pump is deselected.</para>
|
||||||
/// <list type="number">
|
/// <list type="number">
|
||||||
/// <item>Continuous CAN message sends for ~10 minutes (600.5 s)</item>
|
/// <item>Start persistent CAN senders (run until explicitly stopped)</item>
|
||||||
/// <item>A state-machine handshake that cycles through command bytes on 0x700</item>
|
/// <item>Begin the 600 s CAN wait with progress reporting</item>
|
||||||
|
/// <item>In parallel, wait for K-Line to become Connected, then try the fast
|
||||||
|
/// unlock (RAM timer shortcut) — if the pump verifies unlocked, cancel
|
||||||
|
/// the remaining wait</item>
|
||||||
|
/// <item>TestUnlock state-machine handshake on 0x700</item>
|
||||||
|
/// <item>Verify via CAN TestUnlock parameter</item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UnlockService : IUnlockService
|
public sealed class UnlockService : IUnlockService
|
||||||
{
|
{
|
||||||
private readonly ICanService _can;
|
private readonly ICanService _can;
|
||||||
|
private readonly IKwpService _kwp;
|
||||||
private readonly IAppLogger _log;
|
private readonly IAppLogger _log;
|
||||||
private const string LogId = "UnlockService";
|
private const string LogId = "UnlockService";
|
||||||
|
|
||||||
/// <summary>Total duration of the Phase 1 continuous send (milliseconds).</summary>
|
/// <summary>Total duration of the Phase 1 wait (milliseconds).</summary>
|
||||||
private const int UnlockDurationMs = 600_500;
|
private const int UnlockDurationMs = 600_500;
|
||||||
|
|
||||||
|
/// <summary>CTS for the persistent CAN senders — lives beyond <see cref="UnlockAsync"/>.</summary>
|
||||||
|
private CancellationTokenSource? _senderCts;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event Action<string>? StatusChanged;
|
public event Action<string>? StatusChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event Action<bool>? UnlockCompleted;
|
public event Action<bool>? UnlockCompleted;
|
||||||
|
|
||||||
/// <summary>Creates the unlock service wired to the CAN bus.</summary>
|
/// <summary>Creates the unlock service wired to the CAN and K-Line buses.</summary>
|
||||||
public UnlockService(ICanService canService, IAppLogger logger)
|
public UnlockService(ICanService canService, IKwpService kwpService, IAppLogger logger)
|
||||||
{
|
{
|
||||||
_can = canService;
|
_can = canService;
|
||||||
|
_kwp = kwpService;
|
||||||
_log = logger;
|
_log = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,17 +53,26 @@ namespace HC_APTBS.Services.Impl
|
|||||||
if (pump.UnlockType == 0) return;
|
if (pump.UnlockType == 0) return;
|
||||||
|
|
||||||
_log.Info(LogId, $"Starting immobilizer unlock (type {pump.UnlockType}) for {pump.Id}");
|
_log.Info(LogId, $"Starting immobilizer unlock (type {pump.UnlockType}) for {pump.Id}");
|
||||||
|
|
||||||
|
// ── Start persistent CAN senders FIRST ───────────────────────────────
|
||||||
|
// These must be active before any unlock attempt and must continue
|
||||||
|
// running after the unlock completes to prevent re-locking.
|
||||||
|
StartSenders(pump.UnlockType);
|
||||||
StatusChanged?.Invoke("Unlocking...");
|
StatusChanged?.Invoke("Unlocking...");
|
||||||
|
|
||||||
// ── Phase 1: Continuous sends for ~10 minutes ─────────────────────────
|
// ── 600 s CAN wait + parallel K-Line fast unlock attempt ─────────────
|
||||||
await RunPhase1Async(pump.UnlockType, ct);
|
// The fast unlock shortens the pump's internal 10 min timer via K-Line.
|
||||||
|
// It can only be attempted once the K-Line session is Connected (the
|
||||||
|
// read-all-info must finish first). If the fast unlock succeeds AND
|
||||||
|
// the CAN TestUnlock parameter confirms it, we skip the remaining wait.
|
||||||
|
await WaitWithFastUnlockAsync(pump, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// ── Phase 2: TestUnlock state machine ─────────────────────────────────
|
// ── Phase 2: TestUnlock state machine ────────────────────────────────
|
||||||
StatusChanged?.Invoke("Testing unlock...");
|
StatusChanged?.Invoke("Testing unlock...");
|
||||||
RunTestUnlockSequence(pump.UnlockType);
|
RunTestUnlockSequence(pump.UnlockType);
|
||||||
|
|
||||||
// ── Verify unlock status ──────────────────────────────────────────────
|
// ── Verify unlock status via CAN TestUnlock parameter ────────────────
|
||||||
bool success = VerifyUnlock(pump);
|
bool success = VerifyUnlock(pump);
|
||||||
|
|
||||||
_log.Info(LogId, $"Unlock complete — success={success}");
|
_log.Info(LogId, $"Unlock complete — success={success}");
|
||||||
@@ -59,11 +80,27 @@ namespace HC_APTBS.Services.Impl
|
|||||||
UnlockCompleted?.Invoke(success);
|
UnlockCompleted?.Invoke(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 1 ──────────────────────────────────────────────────────────────
|
/// <inheritdoc/>
|
||||||
|
public void StopSenders()
|
||||||
private async Task RunPhase1Async(int unlockType, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
// Build message payloads based on unlock type.
|
if (_senderCts == null) return;
|
||||||
|
_log.Info(LogId, "Stopping persistent CAN unlock senders");
|
||||||
|
_senderCts.Cancel();
|
||||||
|
_senderCts.Dispose();
|
||||||
|
_senderCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Persistent CAN senders ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the two continuous CAN message senders. They run indefinitely
|
||||||
|
/// until <see cref="StopSenders"/> is called (on pump deselection).
|
||||||
|
/// </summary>
|
||||||
|
private void StartSenders(int unlockType)
|
||||||
|
{
|
||||||
|
// Stop any leftover senders from a previous unlock.
|
||||||
|
StopSenders();
|
||||||
|
|
||||||
byte[] msg1Data = new byte[8];
|
byte[] msg1Data = new byte[8];
|
||||||
uint msg1Id;
|
uint msg1Id;
|
||||||
byte[] msg2Data = new byte[8];
|
byte[] msg2Data = new byte[8];
|
||||||
@@ -88,66 +125,174 @@ namespace HC_APTBS.Services.Impl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run two parallel senders for the full unlock duration.
|
_senderCts = new CancellationTokenSource();
|
||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
var senderCt = _senderCts.Token;
|
||||||
cts.CancelAfter(UnlockDurationMs);
|
|
||||||
var linkedCt = cts.Token;
|
|
||||||
|
|
||||||
var sender1 = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!linkedCt.IsCancellationRequested)
|
while (!senderCt.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_can.SendRawMessage(msg1Id, msg1Data);
|
_can.SendRawMessage(msg1Id, msg1Data);
|
||||||
await Task.Delay(500, linkedCt);
|
await Task.Delay(500, senderCt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
}, linkedCt);
|
}, senderCt);
|
||||||
|
|
||||||
var sender2 = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (!linkedCt.IsCancellationRequested)
|
while (!senderCt.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_can.SendRawMessage(msg2Id, msg2Data);
|
_can.SendRawMessage(msg2Id, msg2Data);
|
||||||
await Task.Delay(50, linkedCt);
|
await Task.Delay(50, senderCt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
}, linkedCt);
|
}, senderCt);
|
||||||
|
|
||||||
// Report progress periodically.
|
_log.Info(LogId, $"Persistent CAN senders started (type {unlockType})");
|
||||||
var progressTask = Task.Run(async () =>
|
}
|
||||||
{
|
|
||||||
var start = DateTime.UtcNow;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!linkedCt.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000, linkedCt);
|
|
||||||
var elapsed = DateTime.UtcNow - start;
|
|
||||||
int pct = (int)(elapsed.TotalMilliseconds * 100 / UnlockDurationMs);
|
|
||||||
string time = $"{(int)elapsed.TotalMinutes:D2}:{elapsed.Seconds:D2}";
|
|
||||||
StatusChanged?.Invoke($"Unlocking... {Math.Min(pct, 100)}% ({time})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { }
|
|
||||||
}, linkedCt);
|
|
||||||
|
|
||||||
await Task.WhenAll(sender1, sender2, progressTask);
|
// ── Wait with parallel fast-unlock ───────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the 600 s progress wait. In parallel, monitors the K-Line session:
|
||||||
|
/// once it becomes Connected, checks if the pump is still locked, sends the
|
||||||
|
/// fast unlock command, and if the pump verifies unlocked, cancels the
|
||||||
|
/// remaining wait time.
|
||||||
|
/// </summary>
|
||||||
|
private async Task WaitWithFastUnlockAsync(PumpDefinition pump, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var waitCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
waitCts.CancelAfter(UnlockDurationMs);
|
||||||
|
var waitCt = waitCts.Token;
|
||||||
|
|
||||||
|
// Progress reporting task.
|
||||||
|
var progressTask = ReportProgressAsync(waitCt);
|
||||||
|
|
||||||
|
// Parallel fast-unlock task — awaits K-Line session, then attempts shortcut.
|
||||||
|
var fastTask = TryFastUnlockWhenReadyAsync(pump, waitCts, ct);
|
||||||
|
|
||||||
|
// Wait for either: the full duration elapses, or the fast unlock succeeds
|
||||||
|
// and cancels the wait CTS.
|
||||||
|
await Task.WhenAll(progressTask, fastTask);
|
||||||
|
|
||||||
// If the outer ct was cancelled (user stop), propagate.
|
// If the outer ct was cancelled (user stop), propagate.
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reports progress every second until the wait token is cancelled.</summary>
|
||||||
|
private async Task ReportProgressAsync(CancellationToken waitCt)
|
||||||
|
{
|
||||||
|
var start = DateTime.UtcNow;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!waitCt.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000, waitCt);
|
||||||
|
var elapsed = DateTime.UtcNow - start;
|
||||||
|
int pct = (int)(elapsed.TotalMilliseconds * 100 / UnlockDurationMs);
|
||||||
|
string time = $"{(int)elapsed.TotalMinutes:D2}:{elapsed.Seconds:D2}";
|
||||||
|
StatusChanged?.Invoke($"Unlocking... {Math.Min(pct, 100)}% ({time})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the K-Line session to become Connected, then attempts the
|
||||||
|
/// fast unlock. If the pump verifies unlocked afterward, cancels <paramref name="waitCts"/>
|
||||||
|
/// to skip the remaining 600 s wait.
|
||||||
|
/// </summary>
|
||||||
|
private async Task TryFastUnlockWhenReadyAsync(
|
||||||
|
PumpDefinition pump, CancellationTokenSource waitCts, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Wait for K-Line session to become Connected.
|
||||||
|
if (_kwp.KLineState != KLineConnectionState.Connected)
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "Waiting for K-Line session to connect...");
|
||||||
|
var connectedTcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
void OnStateChanged(KLineConnectionState state)
|
||||||
|
{
|
||||||
|
if (state == KLineConnectionState.Connected)
|
||||||
|
connectedTcs.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwp.KLineStateChanged += OnStateChanged;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Check again after subscribing (race guard).
|
||||||
|
if (_kwp.KLineState == KLineConnectionState.Connected)
|
||||||
|
connectedTcs.TrySetResult(true);
|
||||||
|
|
||||||
|
// Wait for connection or cancellation (user cancel or 600 s elapsed).
|
||||||
|
using var reg = ct.Register(() => connectedTcs.TrySetCanceled());
|
||||||
|
using var waitReg = waitCts.Token.Register(() => connectedTcs.TrySetCanceled());
|
||||||
|
await connectedTcs.Task;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_kwp.KLineStateChanged -= OnStateChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// K-Line is now connected. Check if the pump is still locked.
|
||||||
|
if (VerifyUnlock(pump))
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "Pump already unlocked — skipping wait");
|
||||||
|
waitCts.Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pump is locked — attempt the fast K-Line unlock (RAM timer shortcut).
|
||||||
|
_log.Info(LogId, "Attempting K-Line fast unlock (timer shortcut)...");
|
||||||
|
StatusChanged?.Invoke("Fast unlock attempt...");
|
||||||
|
|
||||||
|
bool ack = await _kwp.TryFastUnlockAsync();
|
||||||
|
if (!ack)
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "Fast unlock NAK or failed — continuing normal wait");
|
||||||
|
StatusChanged?.Invoke("Unlocking...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Info(LogId, "Fast unlock ACK — waiting briefly for pump to process");
|
||||||
|
|
||||||
|
// Give the pump a moment to process the timer shortcut, then verify.
|
||||||
|
await Task.Delay(2000, ct);
|
||||||
|
|
||||||
|
if (VerifyUnlock(pump))
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "Fast unlock verified — skipping remaining wait");
|
||||||
|
waitCts.Cancel();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_log.Info(LogId, "Fast unlock ACK'd but pump still locked — continuing normal wait");
|
||||||
|
StatusChanged?.Invoke("Unlocking...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Wait elapsed or user cancelled — fast unlock window closed, that's fine.
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_log.Warning(LogId, $"Fast unlock attempt error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 2: TestUnlock state machine ────────────────────────────────────
|
// ── Phase 2: TestUnlock state machine ────────────────────────────────────
|
||||||
|
|
||||||
private void RunTestUnlockSequence(int unlockType)
|
private void RunTestUnlockSequence(int unlockType)
|
||||||
{
|
{
|
||||||
// The state machine cycles through 4 command bytes, twice.
|
|
||||||
byte[][] type1Cmds =
|
byte[][] type1Cmds =
|
||||||
{
|
{
|
||||||
new byte[] { 0xB2, 0, 0, 0, 0, 0, 0, 0 },
|
new byte[] { 0xB2, 0, 0, 0, 0, 0, 0, 0 },
|
||||||
@@ -188,8 +333,9 @@ namespace HC_APTBS.Services.Impl
|
|||||||
switch (pump.UnlockType)
|
switch (pump.UnlockType)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
// Type 1: unlocked when TestUnlock value is non-zero.
|
// Type 1: unlocked when TestUnlock value is zero.
|
||||||
return unlockParam.Value != 0;
|
// Old code: Lock = valor != 0 (non-zero = locked).
|
||||||
|
return unlockParam.Value == 0;
|
||||||
case 2:
|
case 2:
|
||||||
// Type 2: unlocked when TestUnlock value equals 0xE4 (228).
|
// Type 2: unlocked when TestUnlock value equals 0xE4 (228).
|
||||||
return (int)unlockParam.Value == 0xE4;
|
return (int)unlockParam.Value == 0xE4;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
using HC_APTBS.Services;
|
using HC_APTBS.Services;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
using HC_APTBS.Views.Dialogs;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -86,6 +88,22 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
partial void OnIsOilPumpOnChanged(bool value)
|
partial void OnIsOilPumpOnChanged(bool value)
|
||||||
{
|
{
|
||||||
|
// Show confirmation dialog when turning oil pump ON (WAcceptOilTurnOn equivalent).
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
var vm = new OilPumpConfirmViewModel();
|
||||||
|
var dlg = new OilPumpConfirmDialog(vm) { Owner = Application.Current.MainWindow };
|
||||||
|
dlg.ShowDialog();
|
||||||
|
|
||||||
|
if (!vm.Accepted)
|
||||||
|
{
|
||||||
|
// Revert without re-triggering this handler.
|
||||||
|
_isOilPumpOn = false;
|
||||||
|
OnPropertyChanged(nameof(IsOilPumpOn));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_bench.SetRelay(RelayNames.OilPump, value);
|
_bench.SetRelay(RelayNames.OilPump, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,22 +118,31 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the bench motor at the RPM specified in <see cref="RpmInputText"/>.
|
/// Starts the bench motor at the RPM specified in <see cref="RpmInputText"/>.
|
||||||
/// Warns the operator if the oil pump is off.
|
/// Shows a safety warning dialog if the oil pump is off.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void StartBench()
|
private void StartBench()
|
||||||
{
|
{
|
||||||
if (!int.TryParse(RpmInputText, out int rpm) || rpm <= 0) return;
|
if (!int.TryParse(RpmInputText, out int rpm) || rpm <= 0) return;
|
||||||
|
|
||||||
// Safety warning if oil pump is not running.
|
// Safety warning if oil pump is not running (WCareOnRpmOn equivalent).
|
||||||
if (!IsOilPumpOn)
|
if (!IsOilPumpOn)
|
||||||
{
|
{
|
||||||
var result = MessageBox.Show(
|
var vm = new RpmSafetyWarningViewModel();
|
||||||
"Oil pump is OFF. Start bench without oil circulation?",
|
var dlg = new RpmSafetyWarningDialog(vm) { Owner = Application.Current.MainWindow };
|
||||||
"Oil Pump Warning",
|
dlg.ShowDialog();
|
||||||
MessageBoxButton.YesNo,
|
|
||||||
MessageBoxImage.Warning);
|
switch (vm.Result)
|
||||||
if (result != MessageBoxResult.Yes) return;
|
{
|
||||||
|
case RpmSafetyResult.Cancel:
|
||||||
|
return;
|
||||||
|
case RpmSafetyResult.ProceedWithOil:
|
||||||
|
IsOilPumpOn = true;
|
||||||
|
break;
|
||||||
|
case RpmSafetyResult.ProceedWithoutOil:
|
||||||
|
// Operator accepted the risk.
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure direction relays are set.
|
// Ensure direction relays are set.
|
||||||
|
|||||||
@@ -22,15 +22,17 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
private readonly IKwpService _kwp;
|
private readonly IKwpService _kwp;
|
||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
private const string LogId = "DfiManageViewModel";
|
private const string LogId = "DfiManageViewModel";
|
||||||
|
|
||||||
// ── Constructor ───────────────────────────────────────────────────────────
|
// ── Constructor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Initialises the ViewModel with the required services.</summary>
|
/// <summary>Initialises the ViewModel with the required services.</summary>
|
||||||
public DfiManageViewModel(IKwpService kwpService, IConfigurationService configService)
|
public DfiManageViewModel(IKwpService kwpService, IConfigurationService configService, ILocalizationService loc)
|
||||||
{
|
{
|
||||||
_kwp = kwpService;
|
_kwp = kwpService;
|
||||||
_config = configService;
|
_config = configService;
|
||||||
|
_loc = loc;
|
||||||
|
|
||||||
// Update the slider and LCD display in real time when the DFI is
|
// Update the slider and LCD display in real time when the DFI is
|
||||||
// read during a full K-Line read (PumpIdentificationViewModel flow).
|
// read during a full K-Line read (PumpIdentificationViewModel flow).
|
||||||
@@ -106,8 +108,8 @@ namespace HC_APTBS.ViewModels
|
|||||||
string? port = _kwp.DetectKLinePort();
|
string? port = _kwp.DetectKLinePort();
|
||||||
if (string.IsNullOrEmpty(port))
|
if (string.IsNullOrEmpty(port))
|
||||||
{
|
{
|
||||||
MessageBox.Show("K-Line device not found. Check that the FTDI adapter is connected.",
|
MessageBox.Show(_loc.GetString("Error.KLineNotFound"),
|
||||||
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +140,8 @@ namespace HC_APTBS.ViewModels
|
|||||||
string? port = _kwp.DetectKLinePort();
|
string? port = _kwp.DetectKLinePort();
|
||||||
if (string.IsNullOrEmpty(port))
|
if (string.IsNullOrEmpty(port))
|
||||||
{
|
{
|
||||||
MessageBox.Show("K-Line device not found. Check that the FTDI adapter is connected.",
|
MessageBox.Show(_loc.GetString("Error.KLineNotFound"),
|
||||||
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace HC_APTBS.ViewModels.Dialogs
|
|||||||
|
|
||||||
private readonly IKwpService _kwp;
|
private readonly IKwpService _kwp;
|
||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
// ── Constructor ───────────────────────────────────────────────────────────
|
// ── Constructor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,10 +28,12 @@ namespace HC_APTBS.ViewModels.Dialogs
|
|||||||
public KlineErrorsViewModel(
|
public KlineErrorsViewModel(
|
||||||
IKwpService kwpService,
|
IKwpService kwpService,
|
||||||
IConfigurationService configService,
|
IConfigurationService configService,
|
||||||
|
ILocalizationService loc,
|
||||||
string initialErrors = "")
|
string initialErrors = "")
|
||||||
{
|
{
|
||||||
_kwp = kwpService;
|
_kwp = kwpService;
|
||||||
_config = configService;
|
_config = configService;
|
||||||
|
_loc = loc;
|
||||||
ErrorText = initialErrors;
|
ErrorText = initialErrors;
|
||||||
|
|
||||||
_kwp.ProgressChanged += OnProgress;
|
_kwp.ProgressChanged += OnProgress;
|
||||||
@@ -120,8 +123,8 @@ namespace HC_APTBS.ViewModels.Dialogs
|
|||||||
if (!string.IsNullOrEmpty(port)) return port;
|
if (!string.IsNullOrEmpty(port)) return port;
|
||||||
|
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
"K-Line device not found. Check that the FTDI adapter is connected.",
|
_loc.GetString("Error.KLineNotFound"),
|
||||||
"K-Line Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
_loc.GetString("Error.KLineTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
ViewModels/Dialogs/OilPumpConfirmViewModel.cs
Normal file
51
ViewModels/Dialogs/OilPumpConfirmViewModel.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace HC_APTBS.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for the oil pump confirmation dialog shown before activating
|
||||||
|
/// the oil pump relay. The operator must confirm that oil level and
|
||||||
|
/// connections have been checked.
|
||||||
|
/// Equivalent to the old <c>WAcceptOilTurnOn</c> dialog.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class OilPumpConfirmViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
// ── Dialog result ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>True if the operator confirmed and accepted.</summary>
|
||||||
|
public bool Accepted { get; private set; }
|
||||||
|
|
||||||
|
// ── Checkbox ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>True when the "I have checked for leaks" checkbox is ticked.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||||
|
private bool _leaksChecked;
|
||||||
|
|
||||||
|
// ── Commands ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Confirms the oil pump activation and closes the dialog.</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(CanAccept))]
|
||||||
|
private void Accept()
|
||||||
|
{
|
||||||
|
Accepted = true;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanAccept() => LeaksChecked;
|
||||||
|
|
||||||
|
/// <summary>Cancels the oil pump activation and closes the dialog.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
Accepted = false;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Raised when the dialog should close itself.</summary>
|
||||||
|
public event System.Action? RequestClose;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
ViewModels/Dialogs/RpmSafetyWarningViewModel.cs
Normal file
72
ViewModels/Dialogs/RpmSafetyWarningViewModel.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace HC_APTBS.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Result of the RPM safety warning dialog.
|
||||||
|
/// </summary>
|
||||||
|
public enum RpmSafetyResult
|
||||||
|
{
|
||||||
|
/// <summary>User cancelled — do not start the motor.</summary>
|
||||||
|
Cancel,
|
||||||
|
|
||||||
|
/// <summary>Turn on oil pump first, then start the motor.</summary>
|
||||||
|
ProceedWithOil,
|
||||||
|
|
||||||
|
/// <summary>Proceed without oil pump (operator acknowledges risk).</summary>
|
||||||
|
ProceedWithoutOil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for the RPM safety warning dialog shown when the operator
|
||||||
|
/// starts the bench motor while the oil pump is OFF.
|
||||||
|
/// Equivalent to the old <c>WCareOnRpmOn</c> dialog.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class RpmSafetyWarningViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
// ── Dialog result ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>The operator's chosen action.</summary>
|
||||||
|
public RpmSafetyResult Result { get; private set; } = RpmSafetyResult.Cancel;
|
||||||
|
|
||||||
|
// ── Radio button selection ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>True when the "turn on oil pump and proceed" option is selected.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||||
|
private bool _isOilAndProceedSelected;
|
||||||
|
|
||||||
|
/// <summary>True when the "proceed without oil" option is selected.</summary>
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(AcceptCommand))]
|
||||||
|
private bool _isProceedWithoutOilSelected;
|
||||||
|
|
||||||
|
// ── Commands ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Accepts the selected option and closes the dialog.</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(CanAccept))]
|
||||||
|
private void Accept()
|
||||||
|
{
|
||||||
|
Result = IsOilAndProceedSelected
|
||||||
|
? RpmSafetyResult.ProceedWithOil
|
||||||
|
: RpmSafetyResult.ProceedWithoutOil;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanAccept() => IsOilAndProceedSelected || IsProceedWithoutOilSelected;
|
||||||
|
|
||||||
|
/// <summary>Cancels and closes the dialog.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
Result = RpmSafetyResult.Cancel;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Raised when the dialog should close itself.</summary>
|
||||||
|
public event System.Action? RequestClose;
|
||||||
|
}
|
||||||
|
}
|
||||||
290
ViewModels/Dialogs/SettingsViewModel.cs
Normal file
290
ViewModels/Dialogs/SettingsViewModel.cs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using HC_APTBS.Infrastructure.Kwp;
|
||||||
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
|
namespace HC_APTBS.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for the application settings dialog.
|
||||||
|
/// Loads a local copy of every <see cref="AppSettings"/> property so that
|
||||||
|
/// Cancel discards all changes.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class SettingsViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
// ── Dialog result ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>True when the user clicked Accept.</summary>
|
||||||
|
public bool Accepted { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Raised to close the owning dialog window.</summary>
|
||||||
|
public event Action? RequestClose;
|
||||||
|
|
||||||
|
// ── Collections ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Available language codes for the language dropdown.</summary>
|
||||||
|
public ObservableCollection<string> AvailableLanguages { get; } = new() { "ESP", "ENG" };
|
||||||
|
|
||||||
|
/// <summary>RPM-voltage lookup table, editable via DataGrid.</summary>
|
||||||
|
public ObservableCollection<RpmVoltageRelation> Relations { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Available FTDI device serial numbers for K-Line port selection.</summary>
|
||||||
|
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||||
|
|
||||||
|
// ── General ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private string _selectedLanguage = "ESP";
|
||||||
|
[ObservableProperty] private int _daysKeepLogs = 7;
|
||||||
|
|
||||||
|
// ── Safety ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private int _tempMax = 45;
|
||||||
|
[ObservableProperty] private int _tempMin = 35;
|
||||||
|
[ObservableProperty] private int _securityRpmLimit = 2500;
|
||||||
|
[ObservableProperty] private int _maxPressureBar = 26;
|
||||||
|
[ObservableProperty] private double _toleranceUpExtension = 0.08;
|
||||||
|
[ObservableProperty] private double _tolerancePfpExtension = 0.1;
|
||||||
|
[ObservableProperty] private bool _defaultIgnoreTin = true;
|
||||||
|
|
||||||
|
// ── PID ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private double _pidP = 0.1;
|
||||||
|
[ObservableProperty] private double _pidI = 0.1;
|
||||||
|
[ObservableProperty] private double _pidD = 0.04;
|
||||||
|
[ObservableProperty] private int _pidLoopMs = 250;
|
||||||
|
|
||||||
|
// ── Motor ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private int _encoderResolution = 4096;
|
||||||
|
[ObservableProperty] private double _voltageForMaxRpm = 10;
|
||||||
|
[ObservableProperty] private int _maxRpm = 2500;
|
||||||
|
[ObservableProperty] private bool _rightRelayValue = true;
|
||||||
|
|
||||||
|
// ── Company ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private string _companyName = string.Empty;
|
||||||
|
[ObservableProperty] private string _companyInfo = string.Empty;
|
||||||
|
[ObservableProperty] private string _reportLogoPath = string.Empty;
|
||||||
|
|
||||||
|
// ── K-Line ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private string _selectedKLinePort = string.Empty;
|
||||||
|
|
||||||
|
// ── Advanced (refresh intervals) ──────────────────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private int _refreshBenchInterfaceMs = 20;
|
||||||
|
[ObservableProperty] private int _refreshWhileReadingMs = 1500;
|
||||||
|
[ObservableProperty] private int _refreshCanBusReadMs = 2;
|
||||||
|
[ObservableProperty] private int _refreshPumpRequestMs = 250;
|
||||||
|
[ObservableProperty] private int _refreshPumpParamsMs = 4;
|
||||||
|
[ObservableProperty] private int _blinkIntervalMs = 1000;
|
||||||
|
[ObservableProperty] private int _flasherIntervalMs = 800;
|
||||||
|
|
||||||
|
// ── Constructor ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <param name="configService">Configuration service for loading/saving settings.</param>
|
||||||
|
/// <param name="localizationService">Localization service for language switching.</param>
|
||||||
|
public SettingsViewModel(IConfigurationService configService, ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_config = configService;
|
||||||
|
_loc = localizationService;
|
||||||
|
|
||||||
|
var s = configService.Settings;
|
||||||
|
|
||||||
|
// General
|
||||||
|
_selectedLanguage = s.Language;
|
||||||
|
_daysKeepLogs = s.DaysKeepLogs;
|
||||||
|
|
||||||
|
// Safety
|
||||||
|
_tempMax = s.TempMax;
|
||||||
|
_tempMin = s.TempMin;
|
||||||
|
_securityRpmLimit = s.SecurityRpmLimit;
|
||||||
|
_maxPressureBar = s.MaxPressureBar;
|
||||||
|
_toleranceUpExtension = s.ToleranceUpExtension;
|
||||||
|
_tolerancePfpExtension = s.TolerancePfpExtension;
|
||||||
|
_defaultIgnoreTin = s.DefaultIgnoreTin;
|
||||||
|
|
||||||
|
// PID
|
||||||
|
_pidP = s.PidP;
|
||||||
|
_pidI = s.PidI;
|
||||||
|
_pidD = s.PidD;
|
||||||
|
_pidLoopMs = s.PidLoopMs;
|
||||||
|
|
||||||
|
// Motor
|
||||||
|
_encoderResolution = s.EncoderResolution;
|
||||||
|
_voltageForMaxRpm = s.VoltageForMaxRpm;
|
||||||
|
_maxRpm = s.MaxRpm;
|
||||||
|
_rightRelayValue = s.RightRelayValue;
|
||||||
|
|
||||||
|
// Company
|
||||||
|
_companyName = s.CompanyName;
|
||||||
|
_companyInfo = s.CompanyInfo;
|
||||||
|
_reportLogoPath = s.ReportLogoPath;
|
||||||
|
|
||||||
|
// K-Line
|
||||||
|
_selectedKLinePort = s.KLinePort;
|
||||||
|
|
||||||
|
// Advanced
|
||||||
|
_refreshBenchInterfaceMs = s.RefreshBenchInterfaceMs;
|
||||||
|
_refreshWhileReadingMs = s.RefreshWhileReadingMs;
|
||||||
|
_refreshCanBusReadMs = s.RefreshCanBusReadMs;
|
||||||
|
_refreshPumpRequestMs = s.RefreshPumpRequestMs;
|
||||||
|
_refreshPumpParamsMs = s.RefreshPumpParamsMs;
|
||||||
|
_blinkIntervalMs = s.BlinkIntervalMs;
|
||||||
|
_flasherIntervalMs = s.FlasherIntervalMs;
|
||||||
|
|
||||||
|
// Deep-copy the RPM-voltage relation table
|
||||||
|
foreach (var r in s.Relations)
|
||||||
|
Relations.Add(new RpmVoltageRelation(r.Voltage, r.Rpm));
|
||||||
|
|
||||||
|
// Enumerate connected FTDI devices
|
||||||
|
EnumerateFtdiDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Commands ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Copies all local values back to AppSettings, saves, and closes.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Accept()
|
||||||
|
{
|
||||||
|
var s = _config.Settings;
|
||||||
|
|
||||||
|
// General
|
||||||
|
s.DaysKeepLogs = DaysKeepLogs;
|
||||||
|
|
||||||
|
// Safety
|
||||||
|
s.TempMax = TempMax;
|
||||||
|
s.TempMin = TempMin;
|
||||||
|
s.SecurityRpmLimit = SecurityRpmLimit;
|
||||||
|
s.MaxPressureBar = MaxPressureBar;
|
||||||
|
s.ToleranceUpExtension = ToleranceUpExtension;
|
||||||
|
s.TolerancePfpExtension = TolerancePfpExtension;
|
||||||
|
s.DefaultIgnoreTin = DefaultIgnoreTin;
|
||||||
|
|
||||||
|
// PID
|
||||||
|
s.PidP = PidP;
|
||||||
|
s.PidI = PidI;
|
||||||
|
s.PidD = PidD;
|
||||||
|
s.PidLoopMs = PidLoopMs;
|
||||||
|
|
||||||
|
// Motor
|
||||||
|
s.EncoderResolution = EncoderResolution;
|
||||||
|
s.VoltageForMaxRpm = VoltageForMaxRpm;
|
||||||
|
s.MaxRpm = MaxRpm;
|
||||||
|
s.RightRelayValue = RightRelayValue;
|
||||||
|
s.Relations = Relations.Select(r => new RpmVoltageRelation(r.Voltage, r.Rpm)).ToList();
|
||||||
|
|
||||||
|
// Company
|
||||||
|
s.CompanyName = CompanyName;
|
||||||
|
s.CompanyInfo = CompanyInfo;
|
||||||
|
s.ReportLogoPath = ReportLogoPath;
|
||||||
|
|
||||||
|
// K-Line
|
||||||
|
s.KLinePort = SelectedKLinePort;
|
||||||
|
|
||||||
|
// Advanced
|
||||||
|
s.RefreshBenchInterfaceMs = RefreshBenchInterfaceMs;
|
||||||
|
s.RefreshWhileReadingMs = RefreshWhileReadingMs;
|
||||||
|
s.RefreshCanBusReadMs = RefreshCanBusReadMs;
|
||||||
|
s.RefreshPumpRequestMs = RefreshPumpRequestMs;
|
||||||
|
s.RefreshPumpParamsMs = RefreshPumpParamsMs;
|
||||||
|
s.BlinkIntervalMs = BlinkIntervalMs;
|
||||||
|
s.FlasherIntervalMs = FlasherIntervalMs;
|
||||||
|
|
||||||
|
// Language — switch if changed (also persists via LocalizationService)
|
||||||
|
if (SelectedLanguage != _loc.CurrentLanguage)
|
||||||
|
_loc.SetLanguage(SelectedLanguage);
|
||||||
|
|
||||||
|
_config.SaveSettings();
|
||||||
|
|
||||||
|
Accepted = true;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Discards changes and closes.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
Accepted = false;
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Opens a file dialog to select a company logo image.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void BrowseLogo()
|
||||||
|
{
|
||||||
|
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||||
|
{
|
||||||
|
Title = _loc.GetString("Dialog.Settings.BrowseLogoTitle"),
|
||||||
|
Filter = "Image files|*.png;*.jpg;*.jpeg;*.bmp|All files|*.*"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(ReportLogoPath))
|
||||||
|
{
|
||||||
|
try { dlg.InitialDirectory = System.IO.Path.GetDirectoryName(ReportLogoPath); }
|
||||||
|
catch { /* ignore invalid path */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
ReportLogoPath = dlg.FileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Re-enumerates connected FTDI devices into <see cref="AvailablePorts"/>.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void RefreshPorts()
|
||||||
|
{
|
||||||
|
EnumerateFtdiDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Appends a new empty row to the RPM-voltage relation table.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddRelation()
|
||||||
|
{
|
||||||
|
Relations.Add(new RpmVoltageRelation(0.0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Removes the selected row from the RPM-voltage relation table.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveRelation(RpmVoltageRelation? relation)
|
||||||
|
{
|
||||||
|
if (relation != null)
|
||||||
|
Relations.Remove(relation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Populates <see cref="AvailablePorts"/> with serial numbers of connected
|
||||||
|
/// FTDI devices. Fails silently if the FTDI driver DLL is not present.
|
||||||
|
/// </summary>
|
||||||
|
private void EnumerateFtdiDevices()
|
||||||
|
{
|
||||||
|
AvailablePorts.Clear();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint count = FtdiInterface.GetDevicesCount();
|
||||||
|
if (count == 0) return;
|
||||||
|
|
||||||
|
var list = new FT_DEVICE_INFO_NODE[count];
|
||||||
|
FtdiInterface.GetDeviceList(list);
|
||||||
|
|
||||||
|
foreach (var device in list)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(device.SerialNumber))
|
||||||
|
AvailablePorts.Add(device.SerialNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// FTDI DLL not loaded or no devices — leave list empty.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
ViewModels/Dialogs/UnlockProgressViewModel.cs
Normal file
152
ViewModels/Dialogs/UnlockProgressViewModel.cs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
|
namespace HC_APTBS.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for the Ford VP44 immobilizer unlock progress dialog.
|
||||||
|
/// Tracks Phase 1 (CAN flood ~600 s), Phase 2 (handshake ~4 s), and verification.
|
||||||
|
/// Equivalent to the old <c>WUnlocker</c> window.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class UnlockProgressViewModel : ObservableObject, IDisposable
|
||||||
|
{
|
||||||
|
private readonly IUnlockService _unlockService;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
|
||||||
|
/// <summary>Regex to extract percentage and elapsed time from Phase 1 status messages.</summary>
|
||||||
|
private static readonly Regex ProgressRegex =
|
||||||
|
new(@"Unlocking\.\.\. (\d+)% \((\d{2}:\d{2})\)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>Creates the ViewModel and subscribes to unlock service events.</summary>
|
||||||
|
/// <param name="unlockService">The unlock service to monitor.</param>
|
||||||
|
/// <param name="unlockType">Pump unlock type (1 or 2).</param>
|
||||||
|
/// <param name="cts">Cancellation token source to cancel the unlock.</param>
|
||||||
|
public UnlockProgressViewModel(IUnlockService unlockService, int unlockType, CancellationTokenSource cts, ILocalizationService loc)
|
||||||
|
{
|
||||||
|
_unlockService = unlockService;
|
||||||
|
_loc = loc;
|
||||||
|
_cts = cts;
|
||||||
|
_unlockTypeLabel = string.Format(_loc.GetString("Dialog.Unlock.TypeLabel"), unlockType);
|
||||||
|
_phaseText = _loc.GetString("Dialog.Unlock.Phase1");
|
||||||
|
_elapsedTime = "00:00";
|
||||||
|
_isCancellable = true;
|
||||||
|
|
||||||
|
_unlockService.StatusChanged += OnStatusChanged;
|
||||||
|
_unlockService.UnlockCompleted += OnUnlockCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Observable properties ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Progress percentage (0–100).</summary>
|
||||||
|
[ObservableProperty] private int _progress;
|
||||||
|
|
||||||
|
/// <summary>Elapsed time formatted as MM:SS.</summary>
|
||||||
|
[ObservableProperty] private string _elapsedTime;
|
||||||
|
|
||||||
|
/// <summary>Current phase description.</summary>
|
||||||
|
[ObservableProperty] private string _phaseText;
|
||||||
|
|
||||||
|
/// <summary>Result text shown after completion.</summary>
|
||||||
|
[ObservableProperty] private string _resultText = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Label for unlock type (e.g. "Type 1").</summary>
|
||||||
|
[ObservableProperty] private string _unlockTypeLabel;
|
||||||
|
|
||||||
|
/// <summary>True when the unlock sequence has finished (success, failure, or cancelled).</summary>
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(CloseCommand))]
|
||||||
|
[ObservableProperty] private bool _isComplete;
|
||||||
|
|
||||||
|
/// <summary>True while cancellation is allowed (Phase 1 only).</summary>
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(CancelCommand))]
|
||||||
|
[ObservableProperty] private bool _isCancellable;
|
||||||
|
|
||||||
|
/// <summary>Tri-state result: null = in progress, true = success, false = failure.</summary>
|
||||||
|
[ObservableProperty] private bool? _isSuccess;
|
||||||
|
|
||||||
|
// ── Commands ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Cancels the unlock sequence (only available during Phase 1).</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsCancellable))]
|
||||||
|
private void Cancel()
|
||||||
|
{
|
||||||
|
_cts.Cancel();
|
||||||
|
IsCancellable = false;
|
||||||
|
IsComplete = true;
|
||||||
|
IsSuccess = false;
|
||||||
|
ResultText = _loc.GetString("Dialog.Unlock.Cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Closes the dialog (only available after completion).</summary>
|
||||||
|
[RelayCommand(CanExecute = nameof(IsComplete))]
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Raised when the dialog should close itself.</summary>
|
||||||
|
public event Action? RequestClose;
|
||||||
|
|
||||||
|
// ── Service event handlers ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void OnStatusChanged(string msg)
|
||||||
|
{
|
||||||
|
Application.Current?.Dispatcher?.Invoke(() =>
|
||||||
|
{
|
||||||
|
var match = ProgressRegex.Match(msg);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
Progress = int.Parse(match.Groups[1].Value);
|
||||||
|
ElapsedTime = match.Groups[2].Value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg == "Fast unlock attempt...")
|
||||||
|
{
|
||||||
|
PhaseText = _loc.GetString("Dialog.Unlock.FastAttempt");
|
||||||
|
}
|
||||||
|
else if (msg == "Unlocking...")
|
||||||
|
{
|
||||||
|
PhaseText = _loc.GetString("Dialog.Unlock.Phase1");
|
||||||
|
}
|
||||||
|
else if (msg == "Testing unlock...")
|
||||||
|
{
|
||||||
|
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Testing");
|
||||||
|
IsCancellable = false;
|
||||||
|
Progress = 100;
|
||||||
|
}
|
||||||
|
else if (msg == "Sending...")
|
||||||
|
{
|
||||||
|
PhaseText = _loc.GetString("Dialog.Unlock.Phase2Sending");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnlockCompleted(bool success)
|
||||||
|
{
|
||||||
|
Application.Current?.Dispatcher?.Invoke(() =>
|
||||||
|
{
|
||||||
|
IsComplete = true;
|
||||||
|
IsCancellable = false;
|
||||||
|
IsSuccess = success;
|
||||||
|
ResultText = success ? _loc.GetString("Dialog.Unlock.Unlocked") : _loc.GetString("Dialog.Unlock.Failed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── IDisposable ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Unsubscribes from service events to prevent leaks.</summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_unlockService.StatusChanged -= OnStatusChanged;
|
||||||
|
_unlockService.UnlockCompleted -= OnUnlockCompleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,13 @@ namespace HC_APTBS.ViewModels.Dialogs
|
|||||||
public sealed partial class UserCheckViewModel : ObservableObject
|
public sealed partial class UserCheckViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
/// <summary>Initialises the dialog, optionally pre-filling the last used username.</summary>
|
/// <summary>Initialises the dialog, optionally pre-filling the last used username.</summary>
|
||||||
public UserCheckViewModel(IConfigurationService config, string lastUsername = "")
|
public UserCheckViewModel(IConfigurationService config, ILocalizationService loc, string lastUsername = "")
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_loc = loc;
|
||||||
_username = lastUsername;
|
_username = lastUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +57,8 @@ namespace HC_APTBS.ViewModels.Dialogs
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
"Invalid username or password.\n(Both are case-sensitive.)",
|
_loc.GetString("Error.AuthInvalid"),
|
||||||
"Authentication Error",
|
_loc.GetString("Error.AuthTitle"),
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Stop);
|
MessageBoxImage.Stop);
|
||||||
}
|
}
|
||||||
|
|||||||
41
ViewModels/Dialogs/VoltageWarningViewModel.cs
Normal file
41
ViewModels/Dialogs/VoltageWarningViewModel.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace HC_APTBS.ViewModels.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for the voltage warning dialog shown when a pump requiring
|
||||||
|
/// a specific supply voltage (27 V or 13.5 V) is selected.
|
||||||
|
/// Equivalent to the old <c>WAlert27v</c> dialog.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class VoltageWarningViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the voltage warning ViewModel for the specified voltage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="voltage">Voltage string to display (e.g. "27 V" or "13.5 V").</param>
|
||||||
|
public VoltageWarningViewModel(string voltage)
|
||||||
|
{
|
||||||
|
Voltage = voltage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Display properties ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>The required voltage string shown in the dialog (e.g. "27 V").</summary>
|
||||||
|
[ObservableProperty] private string _voltage = string.Empty;
|
||||||
|
|
||||||
|
// ── Commands ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Acknowledges the warning and closes the dialog.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void Acknowledge()
|
||||||
|
{
|
||||||
|
RequestClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Raised when the dialog should close itself.</summary>
|
||||||
|
public event System.Action? RequestClose;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
private readonly IPdfService _pdf;
|
private readonly IPdfService _pdf;
|
||||||
private readonly IUnlockService _unlock;
|
private readonly IUnlockService _unlock;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
private readonly IAppLogger _log;
|
private readonly IAppLogger _log;
|
||||||
private const string LogId = "MainViewModel";
|
private const string LogId = "MainViewModel";
|
||||||
|
|
||||||
@@ -46,9 +47,23 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
private CancellationTokenSource? _testCts;
|
private CancellationTokenSource? _testCts;
|
||||||
|
|
||||||
|
// ── Unlock tracking ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>CTS for the currently running immobilizer unlock, if any.</summary>
|
||||||
|
private CancellationTokenSource? _unlockCts;
|
||||||
|
|
||||||
|
/// <summary>ViewModel for the non-modal unlock progress window.</summary>
|
||||||
|
private UnlockProgressViewModel? _unlockVm;
|
||||||
|
|
||||||
|
/// <summary>The non-modal unlock progress window, if open.</summary>
|
||||||
|
private UnlockProgressDialog? _unlockDlg;
|
||||||
|
|
||||||
/// <summary>Remembers the last authenticated username to pre-fill the next auth dialog.</summary>
|
/// <summary>Remembers the last authenticated username to pre-fill the next auth dialog.</summary>
|
||||||
private string _lastAuthenticatedUser = string.Empty;
|
private string _lastAuthenticatedUser = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Tracks whether the last selected pump required 27 V, for transition-based voltage warnings.</summary>
|
||||||
|
private bool _lastPumpWas27V;
|
||||||
|
|
||||||
// ── Child ViewModels ──────────────────────────────────────────────────────
|
// ── Child ViewModels ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>ViewModel for pump selection and K-Line ECU identification.</summary>
|
/// <summary>ViewModel for pump selection and K-Line ECU identification.</summary>
|
||||||
@@ -58,10 +73,10 @@ namespace HC_APTBS.ViewModels
|
|||||||
public DfiManageViewModel DfiViewModel { get; }
|
public DfiManageViewModel DfiViewModel { get; }
|
||||||
|
|
||||||
/// <summary>ViewModel for the test panel showing all test sections and phase cards.</summary>
|
/// <summary>ViewModel for the test panel showing all test sections and phase cards.</summary>
|
||||||
public TestPanelViewModel TestPanel { get; } = new();
|
public TestPanelViewModel TestPanel { get; }
|
||||||
|
|
||||||
/// <summary>ViewModel for the measurement results table.</summary>
|
/// <summary>ViewModel for the measurement results table.</summary>
|
||||||
public ResultDisplayViewModel ResultDisplay { get; } = new();
|
public ResultDisplayViewModel ResultDisplay { get; }
|
||||||
|
|
||||||
/// <summary>ViewModel for the manual pump control sliders (FBKW, ME, PreIn).</summary>
|
/// <summary>ViewModel for the manual pump control sliders (FBKW, ME, PreIn).</summary>
|
||||||
public PumpControlViewModel PumpControl { get; private set; } = null!;
|
public PumpControlViewModel PumpControl { get; private set; } = null!;
|
||||||
@@ -94,6 +109,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
IConfigurationService configService,
|
IConfigurationService configService,
|
||||||
IPdfService pdfService,
|
IPdfService pdfService,
|
||||||
IUnlockService unlockService,
|
IUnlockService unlockService,
|
||||||
|
ILocalizationService localizationService,
|
||||||
IAppLogger logger)
|
IAppLogger logger)
|
||||||
{
|
{
|
||||||
_can = canService;
|
_can = canService;
|
||||||
@@ -102,10 +118,15 @@ namespace HC_APTBS.ViewModels
|
|||||||
_config = configService;
|
_config = configService;
|
||||||
_pdf = pdfService;
|
_pdf = pdfService;
|
||||||
_unlock = unlockService;
|
_unlock = unlockService;
|
||||||
|
_loc = localizationService;
|
||||||
_log = logger;
|
_log = logger;
|
||||||
|
|
||||||
PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, logger);
|
_loc.LanguageChanged += RefreshLocalisedStrings;
|
||||||
DfiViewModel = new DfiManageViewModel(kwpService, configService);
|
|
||||||
|
TestPanel = new TestPanelViewModel(localizationService);
|
||||||
|
ResultDisplay = new ResultDisplayViewModel(localizationService);
|
||||||
|
PumpIdentification = new PumpIdentificationViewModel(kwpService, configService, localizationService, logger);
|
||||||
|
DfiViewModel = new DfiManageViewModel(kwpService, configService, localizationService);
|
||||||
PumpControl = new PumpControlViewModel(benchService);
|
PumpControl = new PumpControlViewModel(benchService);
|
||||||
BenchControl = new BenchControlViewModel(benchService, configService);
|
BenchControl = new BenchControlViewModel(benchService, configService);
|
||||||
AngleDisplay = new AngleDisplayViewModel(configService);
|
AngleDisplay = new AngleDisplayViewModel(configService);
|
||||||
@@ -159,6 +180,18 @@ namespace HC_APTBS.ViewModels
|
|||||||
FlowmeterChart.SetTolerance(paramName, value, tolerance);
|
FlowmeterChart.SetTolerance(paramName, value, tolerance);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_bench.MeasurementSampled += (name, value) => App.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
if (name == BenchParameterNames.QDelivery)
|
||||||
|
FlowmeterChart.Delivery.AddValue(value);
|
||||||
|
else if (name == BenchParameterNames.QOver)
|
||||||
|
FlowmeterChart.Over.AddValue(value);
|
||||||
|
});
|
||||||
|
_bench.EmergencyStopTriggered += reason => App.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
VerboseStatus = string.Format(_loc.GetString("Error.EmergencyStop"), reason);
|
||||||
|
});
|
||||||
|
|
||||||
// Angle display: lock angle and PSG zero from test phases
|
// Angle display: lock angle and PSG zero from test phases
|
||||||
_bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() =>
|
_bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
@@ -226,12 +259,73 @@ namespace HC_APTBS.ViewModels
|
|||||||
// Notify commands that depend on pump availability.
|
// Notify commands that depend on pump availability.
|
||||||
StartTestCommand.NotifyCanExecuteChanged();
|
StartTestCommand.NotifyCanExecuteChanged();
|
||||||
GenerateReportCommand.NotifyCanExecuteChanged();
|
GenerateReportCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
|
// Show voltage warning on 27V ↔ 13.5V transitions (WAlert27v equivalent).
|
||||||
|
CheckVoltageWarning(pump);
|
||||||
|
|
||||||
|
// Start immobilizer unlock if this pump requires it (Ford VP44).
|
||||||
|
StartUnlockIfRequired(pump);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Immobilizer unlock ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the immobilizer unlock sequence in a non-modal window if the pump
|
||||||
|
/// requires it (UnlockType != 0). Cancels any previously running unlock first.
|
||||||
|
/// </summary>
|
||||||
|
private void StartUnlockIfRequired(PumpDefinition pump)
|
||||||
|
{
|
||||||
|
// Cancel and close any previous unlock window.
|
||||||
|
CloseUnlockDialog();
|
||||||
|
|
||||||
|
if (pump.UnlockType == 0) return;
|
||||||
|
|
||||||
|
_unlockCts = new CancellationTokenSource();
|
||||||
|
_unlockVm = new UnlockProgressViewModel(_unlock, pump.UnlockType, _unlockCts, _loc);
|
||||||
|
_unlockDlg = new UnlockProgressDialog(_unlockVm)
|
||||||
|
{ Owner = Application.Current.MainWindow };
|
||||||
|
|
||||||
|
// Start unlock in background — ViewModel tracks via event subscriptions.
|
||||||
|
var unlockTask = _unlock.UnlockAsync(pump, _unlockCts.Token);
|
||||||
|
_ = unlockTask.ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
|
||||||
|
_unlockDlg.Show(); // Non-modal — user can continue working.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cancels any running unlock, stops persistent CAN senders, closes the
|
||||||
|
/// window, and disposes resources. Safe to call when no unlock is active.
|
||||||
|
/// </summary>
|
||||||
|
private void CloseUnlockDialog()
|
||||||
|
{
|
||||||
|
// Stop the persistent CAN unlock senders (prevents re-lock until
|
||||||
|
// this point — only called when the pump is deselected).
|
||||||
|
_unlock.StopSenders();
|
||||||
|
|
||||||
|
if (_unlockCts != null)
|
||||||
|
{
|
||||||
|
_unlockCts.Cancel();
|
||||||
|
_unlockCts.Dispose();
|
||||||
|
_unlockCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unlockVm != null)
|
||||||
|
{
|
||||||
|
_unlockVm.Dispose();
|
||||||
|
_unlockVm = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_unlockDlg != null)
|
||||||
|
{
|
||||||
|
_unlockDlg.ForceClose();
|
||||||
|
_unlockDlg = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CAN connection ────────────────────────────────────────────────────────
|
// ── CAN connection ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>CAN bus status display text.</summary>
|
/// <summary>CAN bus status display text.</summary>
|
||||||
[ObservableProperty] private string _canStatusText = "Disconnected";
|
[ObservableProperty] private string _canStatusText = string.Empty;
|
||||||
|
|
||||||
/// <summary>True when the CAN bus adapter is connected.</summary>
|
/// <summary>True when the CAN bus adapter is connected.</summary>
|
||||||
[ObservableProperty] private bool _isCanConnected;
|
[ObservableProperty] private bool _isCanConnected;
|
||||||
@@ -243,7 +337,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
_can.SetParameters(_config.Bench.ParametersById);
|
_can.SetParameters(_config.Bench.ParametersById);
|
||||||
_can.RegisterBenchMessageIds(GetReceiveMessageIds(_config.Bench.ParametersById));
|
_can.RegisterBenchMessageIds(GetReceiveMessageIds(_config.Bench.ParametersById));
|
||||||
bool ok = _can.Connect();
|
bool ok = _can.Connect();
|
||||||
CanStatusText = ok ? "Connected" : "Connection failed";
|
CanStatusText = ok ? _loc.GetString("Status.Connected") : _loc.GetString("Status.ConnectionFailed");
|
||||||
IsCanConnected = ok;
|
IsCanConnected = ok;
|
||||||
|
|
||||||
if (ok)
|
if (ok)
|
||||||
@@ -265,7 +359,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
_bench.StopPumpSender();
|
_bench.StopPumpSender();
|
||||||
_can.Disconnect();
|
_can.Disconnect();
|
||||||
IsCanConnected = false;
|
IsCanConnected = false;
|
||||||
CanStatusText = "Disconnected";
|
CanStatusText = _loc.GetString("Status.Disconnected");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Live bench readings ───────────────────────────────────────────────────
|
// ── Live bench readings ───────────────────────────────────────────────────
|
||||||
@@ -361,18 +455,24 @@ namespace HC_APTBS.ViewModels
|
|||||||
{
|
{
|
||||||
if (CurrentPump == null) return;
|
if (CurrentPump == null) return;
|
||||||
|
|
||||||
|
// Block test start if an unlock is still in progress.
|
||||||
|
if (_unlockVm != null && !_unlockVm.IsComplete)
|
||||||
|
{
|
||||||
|
VerboseStatus = _loc.GetString("Status.UnlockInProgress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block test start if the unlock failed or was cancelled.
|
||||||
|
if (CurrentPump.UnlockType != 0 && _unlockVm?.IsSuccess != true)
|
||||||
|
{
|
||||||
|
VerboseStatus = _loc.GetString("Status.UnlockRequired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_testCts = new CancellationTokenSource();
|
_testCts = new CancellationTokenSource();
|
||||||
IsTestRunning = true;
|
IsTestRunning = true;
|
||||||
IsTestSaved = false;
|
IsTestSaved = false;
|
||||||
|
|
||||||
// Run immobilizer unlock if required (e.g. Ford pumps).
|
|
||||||
if (CurrentPump.UnlockType != 0)
|
|
||||||
{
|
|
||||||
VerboseStatus = "Immobilizer unlock in progress...";
|
|
||||||
await _unlock.UnlockAsync(CurrentPump, _testCts.Token);
|
|
||||||
if (_testCts.Token.IsCancellationRequested) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _bench.RunTestsAsync(CurrentPump, _testCts.Token);
|
await _bench.RunTestsAsync(CurrentPump, _testCts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +518,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
if (CurrentPump == null) return;
|
if (CurrentPump == null) return;
|
||||||
|
|
||||||
// Step 1: Authenticate operator.
|
// Step 1: Authenticate operator.
|
||||||
var authVm = new UserCheckViewModel(_config, _lastAuthenticatedUser);
|
var authVm = new UserCheckViewModel(_config, _loc, _lastAuthenticatedUser);
|
||||||
var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
|
var authDlg = new UserCheckDialog(authVm) { Owner = Application.Current.MainWindow };
|
||||||
authDlg.ShowDialog();
|
authDlg.ShowDialog();
|
||||||
if (!authVm.Accepted) return;
|
if (!authVm.Accepted) return;
|
||||||
@@ -444,22 +544,43 @@ namespace HC_APTBS.ViewModels
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_log.Error(LogId, $"GenerateReport: {ex.Message}");
|
_log.Error(LogId, $"GenerateReport: {ex.Message}");
|
||||||
MessageBox.Show($"Failed to generate report:\n{ex.Message}",
|
MessageBox.Show(string.Format(_loc.GetString("Error.ReportGeneration"), ex.Message),
|
||||||
"Report Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
_loc.GetString("Error.ReportTitle"), MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanGenerateReport()
|
private bool CanGenerateReport()
|
||||||
=> CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0;
|
=> CurrentPump != null && !IsTestRunning && CurrentPump.Tests.Count > 0;
|
||||||
|
|
||||||
|
// ── Commands: language toggle ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Toggles the UI language between Spanish and English.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleLanguage()
|
||||||
|
{
|
||||||
|
_loc.SetLanguage(_loc.CurrentLanguage == "ESP" ? "ENG" : "ESP");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Refreshes all ViewModel-cached localised strings after a language change.</summary>
|
||||||
|
private void RefreshLocalisedStrings()
|
||||||
|
{
|
||||||
|
CanStatusText = IsCanConnected
|
||||||
|
? _loc.GetString("Status.Connected")
|
||||||
|
: _loc.GetString("Status.Disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Commands: settings ────────────────────────────────────────────────────
|
// ── Commands: settings ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Saves all current settings and bench configuration to disk.</summary>
|
/// <summary>Opens the settings dialog for editing application configuration.</summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SaveSettings()
|
private void OpenSettings()
|
||||||
{
|
{
|
||||||
_config.SaveSettings();
|
var vm = new SettingsViewModel(_config, _loc);
|
||||||
_config.SaveBench();
|
var dlg = new SettingsDialog(vm) { Owner = Application.Current.MainWindow };
|
||||||
|
dlg.ShowDialog();
|
||||||
|
|
||||||
|
if (vm.Accepted && _refreshTimer != null)
|
||||||
|
_refreshTimer.Interval = TimeSpan.FromMilliseconds(_config.Settings.RefreshBenchInterfaceMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Initialisation ────────────────────────────────────────────────────────
|
// ── Initialisation ────────────────────────────────────────────────────────
|
||||||
@@ -571,7 +692,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
=> App.Current.Dispatcher.Invoke(() =>
|
=> App.Current.Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
IsTestRunning = true;
|
IsTestRunning = true;
|
||||||
VerboseStatus = "Test started...";
|
VerboseStatus = _loc.GetString("Test.Started");
|
||||||
TestPanel.IsRunning = true;
|
TestPanel.IsRunning = true;
|
||||||
TestPanel.ResetResults();
|
TestPanel.ResetResults();
|
||||||
ResultDisplay.Clear();
|
ResultDisplay.Clear();
|
||||||
@@ -585,7 +706,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
{
|
{
|
||||||
IsTestRunning = false;
|
IsTestRunning = false;
|
||||||
LastTestSuccess = !interrupted && success;
|
LastTestSuccess = !interrupted && success;
|
||||||
VerboseStatus = interrupted ? "Test stopped." : (success ? "PASS" : "FAIL");
|
VerboseStatus = interrupted ? _loc.GetString("Test.Stopped") : (success ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail"));
|
||||||
TestPanel.IsRunning = false;
|
TestPanel.IsRunning = false;
|
||||||
_bench.StopPumpSender();
|
_bench.StopPumpSender();
|
||||||
StartTestCommand.NotifyCanExecuteChanged();
|
StartTestCommand.NotifyCanExecuteChanged();
|
||||||
@@ -611,10 +732,40 @@ namespace HC_APTBS.ViewModels
|
|||||||
_bench.SetRelay(RelayNames.Electronic, true);
|
_bench.SetRelay(RelayNames.Electronic, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
private static void ShowPsgSyncError()
|
private void ShowPsgSyncError()
|
||||||
=> MessageBox.Show(
|
=> MessageBox.Show(
|
||||||
"PSG sync pulse not detected. Check encoder connection.",
|
_loc.GetString("Error.PsgSync"),
|
||||||
"PSG Error", MessageBoxButton.OK, MessageBoxImage.Warning);
|
_loc.GetString("Error.PsgTitle"), MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
|
||||||
|
// ── Voltage warning ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a voltage warning dialog when the pump supply voltage requirement
|
||||||
|
/// changes between 27 V and 13.5 V (or vice versa). Only triggers on
|
||||||
|
/// state transitions, matching the old <c>WAlert27v</c> behaviour.
|
||||||
|
/// </summary>
|
||||||
|
private void CheckVoltageWarning(PumpDefinition pump)
|
||||||
|
{
|
||||||
|
bool is27V = !string.IsNullOrEmpty(pump.Tension)
|
||||||
|
&& pump.Tension.Contains("27");
|
||||||
|
|
||||||
|
if (is27V && !_lastPumpWas27V)
|
||||||
|
{
|
||||||
|
var vm = new Dialogs.VoltageWarningViewModel("27 V");
|
||||||
|
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
||||||
|
{ Owner = Application.Current.MainWindow };
|
||||||
|
dlg.ShowDialog();
|
||||||
|
_lastPumpWas27V = true;
|
||||||
|
}
|
||||||
|
else if (!is27V && _lastPumpWas27V)
|
||||||
|
{
|
||||||
|
var vm = new Dialogs.VoltageWarningViewModel("13.5 V");
|
||||||
|
var dlg = new Views.Dialogs.VoltageWarningDialog(vm)
|
||||||
|
{ Owner = Application.Current.MainWindow };
|
||||||
|
dlg.ShowDialog();
|
||||||
|
_lastPumpWas27V = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,10 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class PhaseCardViewModel : ObservableObject
|
public sealed partial class PhaseCardViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new phase card with a localization service.</summary>
|
||||||
|
public PhaseCardViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
// ── Identity ──────────────────────────────────────────────────────────────
|
// ── Identity ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Display name of the phase (e.g. "1 - S_001").</summary>
|
/// <summary>Display name of the phase (e.g. "1 - S_001").</summary>
|
||||||
@@ -82,7 +87,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
if (Source != null)
|
if (Source != null)
|
||||||
Source.Enabled = value;
|
Source.Enabled = value;
|
||||||
|
|
||||||
ResultText = value ? "\u2013" : "disabled";
|
ResultText = value ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||||
|
|
||||||
// Notify parent.
|
// Notify parent.
|
||||||
EnabledChanged?.Invoke(this);
|
EnabledChanged?.Invoke(this);
|
||||||
@@ -96,7 +101,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
IsActive = false;
|
IsActive = false;
|
||||||
IsPassed = false;
|
IsPassed = false;
|
||||||
IsFailed = false;
|
IsFailed = false;
|
||||||
ResultText = IsEnabled ? "\u2013" : "disabled";
|
ResultText = IsEnabled ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||||
|
|
||||||
foreach (var indicator in ResultIndicators)
|
foreach (var indicator in ResultIndicators)
|
||||||
indicator.Reset();
|
indicator.Reset();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
private readonly IKwpService _kwp;
|
private readonly IKwpService _kwp;
|
||||||
private readonly IConfigurationService _config;
|
private readonly IConfigurationService _config;
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
private readonly IAppLogger _log;
|
private readonly IAppLogger _log;
|
||||||
private const string LogId = "PumpIdentVM";
|
private const string LogId = "PumpIdentVM";
|
||||||
|
|
||||||
@@ -35,10 +36,12 @@ namespace HC_APTBS.ViewModels
|
|||||||
public PumpIdentificationViewModel(
|
public PumpIdentificationViewModel(
|
||||||
IKwpService kwpService,
|
IKwpService kwpService,
|
||||||
IConfigurationService configService,
|
IConfigurationService configService,
|
||||||
|
ILocalizationService loc,
|
||||||
IAppLogger logger)
|
IAppLogger logger)
|
||||||
{
|
{
|
||||||
_kwp = kwpService;
|
_kwp = kwpService;
|
||||||
_config = configService;
|
_config = configService;
|
||||||
|
_loc = loc;
|
||||||
_log = logger;
|
_log = logger;
|
||||||
|
|
||||||
// Wire KWP progress events to local properties.
|
// Wire KWP progress events to local properties.
|
||||||
@@ -171,7 +174,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
if (string.IsNullOrEmpty(port))
|
if (string.IsNullOrEmpty(port))
|
||||||
{
|
{
|
||||||
App.Current.Dispatcher.Invoke(() =>
|
App.Current.Dispatcher.Invoke(() =>
|
||||||
KlineConnectError = "No K-Line device found");
|
KlineConnectError = _loc.GetString("Error.KLineNotFound"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -10,6 +11,11 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class ResultRowViewModel : ObservableObject
|
public sealed partial class ResultRowViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new result row with a localization service.</summary>
|
||||||
|
public ResultRowViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
|
|
||||||
[ObservableProperty] private string _phaseName = string.Empty;
|
[ObservableProperty] private string _phaseName = string.Empty;
|
||||||
[ObservableProperty] private string _parameterName = string.Empty;
|
[ObservableProperty] private string _parameterName = string.Empty;
|
||||||
[ObservableProperty] private double _target;
|
[ObservableProperty] private double _target;
|
||||||
@@ -17,8 +23,8 @@ namespace HC_APTBS.ViewModels
|
|||||||
[ObservableProperty] private double _average;
|
[ObservableProperty] private double _average;
|
||||||
[ObservableProperty] private bool _passed;
|
[ObservableProperty] private bool _passed;
|
||||||
|
|
||||||
/// <summary>"PASS" or "FAIL".</summary>
|
/// <summary>Localised "PASS" or "FAIL" label.</summary>
|
||||||
public string ResultLabel => Passed ? "PASS" : "FAIL";
|
public string ResultLabel => Passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -31,6 +37,11 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class ResultDisplayViewModel : ObservableObject
|
public sealed partial class ResultDisplayViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new result display with a localization service.</summary>
|
||||||
|
public ResultDisplayViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
|
|
||||||
// ── Properties ────────────────────────────────────────────────────────────
|
// ── Properties ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Name of the test whose results are displayed.</summary>
|
/// <summary>Name of the test whose results are displayed.</summary>
|
||||||
@@ -60,7 +71,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
{
|
{
|
||||||
if (tp.Result == null) continue;
|
if (tp.Result == null) continue;
|
||||||
allPassed = allPassed && tp.Result.Passed;
|
allPassed = allPassed && tp.Result.Passed;
|
||||||
Results.Add(new ResultRowViewModel
|
Results.Add(new ResultRowViewModel(_loc)
|
||||||
{
|
{
|
||||||
PhaseName = phase.Name,
|
PhaseName = phase.Name,
|
||||||
ParameterName = tp.Name,
|
ParameterName = tp.Name,
|
||||||
@@ -86,7 +97,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Results.Add(new ResultRowViewModel
|
Results.Add(new ResultRowViewModel(_loc)
|
||||||
{
|
{
|
||||||
PhaseName = phaseName,
|
PhaseName = phaseName,
|
||||||
ParameterName = paramName,
|
ParameterName = paramName,
|
||||||
@@ -115,7 +126,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
{
|
{
|
||||||
if (tp.Result == null) continue;
|
if (tp.Result == null) continue;
|
||||||
allPassed = allPassed && tp.Result.Passed;
|
allPassed = allPassed && tp.Result.Passed;
|
||||||
Results.Add(new ResultRowViewModel
|
Results.Add(new ResultRowViewModel(_loc)
|
||||||
{
|
{
|
||||||
PhaseName = phase.Name,
|
PhaseName = phase.Name,
|
||||||
ParameterName = tp.Name,
|
ParameterName = tp.Name,
|
||||||
@@ -128,7 +139,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TestName = tests.Count > 0 ? "All Tests" : string.Empty;
|
TestName = tests.Count > 0 ? _loc.GetString("Result.AllTests") : string.Empty;
|
||||||
OverallPassed = allPassed && Results.Count > 0;
|
OverallPassed = allPassed && Results.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -39,6 +40,10 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class TestDisplayViewModel : ObservableObject
|
public sealed partial class TestDisplayViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new test display with a localization service.</summary>
|
||||||
|
public TestDisplayViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
// ── Properties ────────────────────────────────────────────────────────────
|
// ── Properties ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>Name of the test currently being executed (e.g. "F", "SVME").</summary>
|
/// <summary>Name of the test currently being executed (e.g. "F", "SVME").</summary>
|
||||||
@@ -72,7 +77,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
{
|
{
|
||||||
Name = phase.Name,
|
Name = phase.Name,
|
||||||
IsEnabled = phase.Enabled,
|
IsEnabled = phase.Enabled,
|
||||||
ResultText = phase.Enabled ? "–" : "disabled"
|
ResultText = phase.Enabled ? "\u2013" : _loc.GetString("Common.Disabled")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +103,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
row.IsActive = false;
|
row.IsActive = false;
|
||||||
row.IsPassed = passed;
|
row.IsPassed = passed;
|
||||||
row.IsFailed = !passed;
|
row.IsFailed = !passed;
|
||||||
row.ResultText = passed ? "PASS" : "FAIL";
|
row.ResultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +116,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
row.IsActive = false;
|
row.IsActive = false;
|
||||||
row.IsPassed = false;
|
row.IsPassed = false;
|
||||||
row.IsFailed = false;
|
row.IsFailed = false;
|
||||||
row.ResultText = row.IsEnabled ? "–" : "disabled";
|
row.ResultText = row.IsEnabled ? "\u2013" : _loc.GetString("Common.Disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,11 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class TestPanelViewModel : ObservableObject
|
public sealed partial class TestPanelViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new test panel with a localization service.</summary>
|
||||||
|
public TestPanelViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
|
|
||||||
// ── Cached active phase for fast live-indicator lookup ─────────────────────
|
// ── Cached active phase for fast live-indicator lookup ─────────────────────
|
||||||
|
|
||||||
private PhaseCardViewModel? _activePhaseCard;
|
private PhaseCardViewModel? _activePhaseCard;
|
||||||
@@ -91,7 +97,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
foreach (var testDef in pump.Tests)
|
foreach (var testDef in pump.Tests)
|
||||||
{
|
{
|
||||||
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues);
|
var section = TestSectionViewModel.FromDefinition(testDef, ShowOperationValues, _loc);
|
||||||
Tests.Add(section);
|
Tests.Add(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +158,7 @@ namespace HC_APTBS.ViewModels
|
|||||||
phase.IsActive = false;
|
phase.IsActive = false;
|
||||||
phase.IsPassed = passed;
|
phase.IsPassed = passed;
|
||||||
phase.IsFailed = !passed;
|
phase.IsFailed = !passed;
|
||||||
phase.ResultText = passed ? "PASS" : "FAIL";
|
phase.ResultText = passed ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using HC_APTBS.Models;
|
using HC_APTBS.Models;
|
||||||
|
using HC_APTBS.Services;
|
||||||
|
|
||||||
namespace HC_APTBS.ViewModels
|
namespace HC_APTBS.ViewModels
|
||||||
{
|
{
|
||||||
@@ -11,6 +12,11 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class TestSectionViewModel : ObservableObject
|
public sealed partial class TestSectionViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ILocalizationService _loc;
|
||||||
|
|
||||||
|
/// <summary>Initialises a new test section with a localization service.</summary>
|
||||||
|
public TestSectionViewModel(ILocalizationService loc) => _loc = loc;
|
||||||
|
|
||||||
// ── Suppress cascade guard ────────────────────────────────────────────────
|
// ── Suppress cascade guard ────────────────────────────────────────────────
|
||||||
|
|
||||||
private bool _suppressCascade;
|
private bool _suppressCascade;
|
||||||
@@ -105,12 +111,13 @@ namespace HC_APTBS.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="test">Source test definition.</param>
|
/// <param name="test">Source test definition.</param>
|
||||||
/// <param name="showValues">Initial show-operation-values state.</param>
|
/// <param name="showValues">Initial show-operation-values state.</param>
|
||||||
public static TestSectionViewModel FromDefinition(TestDefinition test, bool showValues)
|
/// <param name="loc">Localization service for user-facing strings.</param>
|
||||||
|
public static TestSectionViewModel FromDefinition(TestDefinition test, bool showValues, ILocalizationService loc)
|
||||||
{
|
{
|
||||||
var section = new TestSectionViewModel
|
var section = new TestSectionViewModel(loc)
|
||||||
{
|
{
|
||||||
TestName = test.Name,
|
TestName = test.Name,
|
||||||
Description = MapDescription(test.Name),
|
Description = loc.GetString(MapDescriptionKey(test.Name)),
|
||||||
ConditioningTimeSec = test.ConditioningTimeSec,
|
ConditioningTimeSec = test.ConditioningTimeSec,
|
||||||
MeasurementTimeSec = test.MeasurementTimeSec,
|
MeasurementTimeSec = test.MeasurementTimeSec,
|
||||||
MeasurementsPerSecond = test.MeasurementsPerSecond,
|
MeasurementsPerSecond = test.MeasurementsPerSecond,
|
||||||
@@ -119,12 +126,12 @@ namespace HC_APTBS.ViewModels
|
|||||||
|
|
||||||
foreach (var phaseDef in test.Phases)
|
foreach (var phaseDef in test.Phases)
|
||||||
{
|
{
|
||||||
var card = new PhaseCardViewModel
|
var card = new PhaseCardViewModel(loc)
|
||||||
{
|
{
|
||||||
Name = phaseDef.Name,
|
Name = phaseDef.Name,
|
||||||
IsCritical = phaseDef.IsCritical,
|
IsCritical = phaseDef.IsCritical,
|
||||||
IsEnabled = phaseDef.Enabled,
|
IsEnabled = phaseDef.Enabled,
|
||||||
ResultText = phaseDef.Enabled ? "\u2013" : "disabled",
|
ResultText = phaseDef.Enabled ? "\u2013" : loc.GetString("Common.Disabled"),
|
||||||
ShowOperationValues = showValues,
|
ShowOperationValues = showValues,
|
||||||
Source = phaseDef,
|
Source = phaseDef,
|
||||||
EnabledChanged = section.OnChildEnabledChanged
|
EnabledChanged = section.OnChildEnabledChanged
|
||||||
@@ -157,16 +164,17 @@ namespace HC_APTBS.ViewModels
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a test type identifier to a human-readable description.
|
/// Maps a test type identifier to a localization resource key.
|
||||||
|
/// Returns the test name itself for unknown types (fail-visible).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string MapDescription(string testName) => testName switch
|
private static string MapDescriptionKey(string testName) => testName switch
|
||||||
{
|
{
|
||||||
TestType.Wl => "Warm-up",
|
TestType.Wl => "TestType.Warmup",
|
||||||
TestType.Dfi => "Adjustment",
|
TestType.Dfi => "TestType.Adjustment",
|
||||||
TestType.F => "Flow",
|
TestType.F => "TestType.Flow",
|
||||||
TestType.Svme => "Servo valve",
|
TestType.Svme => "TestType.ServoValve",
|
||||||
TestType.Up => "Upstroke",
|
TestType.Up => "TestType.Upstroke",
|
||||||
TestType.Pfp => "Pre-injection",
|
TestType.Pfp => "TestType.PreInjection",
|
||||||
_ => testName
|
_ => testName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="K-Line Fault Codes"
|
Title="{DynamicResource Dialog.KlineErrors.Title}"
|
||||||
Height="400" Width="600"
|
Height="400" Width="600"
|
||||||
ResizeMode="CanResize"
|
ResizeMode="CanResize"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Text="Fault codes:" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold"/>
|
<TextBlock Text="{DynamicResource Dialog.KlineErrors.Header}" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
<Button Grid.Column="1" Content="Read" Margin="0,2,8,2"
|
<Button Grid.Column="1" Content="{DynamicResource Dialog.KlineErrors.Read}" Margin="0,2,8,2"
|
||||||
Width="75" Height="24"
|
Width="75" Height="24"
|
||||||
Command="{Binding ReadErrorsCommand}"/>
|
Command="{Binding ReadErrorsCommand}"/>
|
||||||
<Button Grid.Column="2" Content="Clear" Margin="0,2,0,2"
|
<Button Grid.Column="2" Content="{DynamicResource Dialog.KlineErrors.Clear}" Margin="0,2,0,2"
|
||||||
Width="75" Height="24"
|
Width="75" Height="24"
|
||||||
Command="{Binding ClearErrorsCommand}"/>
|
Command="{Binding ClearErrorsCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<ProgressBar Value="{Binding ProgressPercent, Mode=OneWay}"
|
<ProgressBar Value="{Binding ProgressPercent, Mode=OneWay}"
|
||||||
Minimum="0" Maximum="100"
|
Minimum="0" Maximum="100"
|
||||||
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
<Button Grid.Column="1" Content="Close" Width="80" Height="24"
|
<Button Grid.Column="1" Content="{DynamicResource Common.Close}" Width="80" Height="24"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding CloseCommand}"/>
|
Command="{Binding CloseCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
57
Views/Dialogs/OilPumpConfirmDialog.xaml
Normal file
57
Views/Dialogs/OilPumpConfirmDialog.xaml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<Window x:Class="HC_APTBS.Views.Dialogs.OilPumpConfirmDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{DynamicResource Dialog.OilPump.Title}"
|
||||||
|
Height="220" Width="440"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid Margin="16,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="48"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Warning icon -->
|
||||||
|
<TextBlock Grid.RowSpan="4" Text="⚠"
|
||||||
|
FontSize="36" Foreground="DarkOrange"
|
||||||
|
VerticalAlignment="Top" HorizontalAlignment="Center"
|
||||||
|
Margin="0,4,8,0"/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock Grid.Column="1" Text="{DynamicResource Common.Warning}"
|
||||||
|
FontSize="18" FontWeight="Bold" Foreground="DarkOrange"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- Warning message -->
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" TextWrapping="Wrap"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Text="{DynamicResource Dialog.OilPump.Message}"/>
|
||||||
|
|
||||||
|
<!-- Confirmation checkbox -->
|
||||||
|
<CheckBox Grid.Row="2" Grid.Column="1" Margin="0,4"
|
||||||
|
IsChecked="{Binding LeaksChecked}"
|
||||||
|
FontSize="14" FontWeight="SemiBold">
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.OilPump.LeaksChecked}" TextWrapping="Wrap"/>
|
||||||
|
</CheckBox>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<StackPanel Grid.Row="4" Grid.Column="1"
|
||||||
|
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
|
<Button Content="{DynamicResource Common.Accept}" Width="80" Height="26" Margin="0,0,8,0"
|
||||||
|
Command="{Binding AcceptCommand}"/>
|
||||||
|
<Button Content="{DynamicResource Common.Cancel}" Width="80" Height="26"
|
||||||
|
Command="{Binding CancelCommand}" IsCancel="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
20
Views/Dialogs/OilPumpConfirmDialog.xaml.cs
Normal file
20
Views/Dialogs/OilPumpConfirmDialog.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Confirmation dialog shown before activating the oil pump relay.
|
||||||
|
/// Equivalent to the old <c>WAcceptOilTurnOn</c>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class OilPumpConfirmDialog : Window
|
||||||
|
{
|
||||||
|
/// <summary>Creates the dialog and wires the ViewModel.</summary>
|
||||||
|
public OilPumpConfirmDialog(OilPumpConfirmViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = vm;
|
||||||
|
vm.RequestClose += Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Generate Report"
|
Title="{DynamicResource Dialog.Report.Title}"
|
||||||
Height="450" Width="800"
|
Height="450" Width="800"
|
||||||
ResizeMode="NoResize"
|
ResizeMode="NoResize"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
@@ -28,11 +28,11 @@
|
|||||||
Grid.Column="2" Grid.RowSpan="5" Margin="-3,10,0,0"/>
|
Grid.Column="2" Grid.RowSpan="5" Margin="-3,10,0,0"/>
|
||||||
|
|
||||||
<!-- ── Section headers ─────────────────────────────────────────────── -->
|
<!-- ── Section headers ─────────────────────────────────────────────── -->
|
||||||
<TextBlock Text="Client List" FontSize="14" FontWeight="SemiBold"
|
<TextBlock Text="{DynamicResource Dialog.Report.ClientList}" FontSize="14" FontWeight="SemiBold"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
||||||
<TextBlock Grid.Column="1" Text="Client Data" FontSize="20" FontWeight="SemiBold"
|
<TextBlock Grid.Column="1" Text="{DynamicResource Dialog.Report.ClientData}" FontSize="20" FontWeight="SemiBold"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
||||||
<TextBlock Grid.Column="2" Text="Company Data" FontSize="20" FontWeight="SemiBold"
|
<TextBlock Grid.Column="2" Text="{DynamicResource Dialog.Report.CompanyData}" FontSize="20" FontWeight="SemiBold"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,10"/>
|
||||||
|
|
||||||
<!-- ── Client list ─────────────────────────────────────────────────── -->
|
<!-- ── Client list ─────────────────────────────────────────────────── -->
|
||||||
@@ -47,17 +47,17 @@
|
|||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label Content="Name:" VerticalAlignment="Center" FontSize="13"/>
|
<Label Content="{DynamicResource Dialog.Report.Name}" VerticalAlignment="Center" FontSize="13"/>
|
||||||
<ComboBox Grid.Column="1" IsEditable="True"
|
<ComboBox Grid.Column="1" IsEditable="True"
|
||||||
ItemsSource="{Binding ClientNames}"
|
ItemsSource="{Binding ClientNames}"
|
||||||
Text="{Binding SelectedClientName, UpdateSourceTrigger=PropertyChanged}"
|
Text="{Binding SelectedClientName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
VerticalAlignment="Center" FontSize="13" Margin="4,0"/>
|
VerticalAlignment="Center" FontSize="13" Margin="4,0"/>
|
||||||
<Button Grid.Column="2" Content="Save" Width="60" Height="24"
|
<Button Grid.Column="2" Content="{DynamicResource Common.Save}" Width="60" Height="24"
|
||||||
Command="{Binding SaveClientCommand}"/>
|
Command="{Binding SaveClientCommand}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- ── Client info text ─────────────────────────────────────────────── -->
|
<!-- ── Client info text ─────────────────────────────────────────────── -->
|
||||||
<GroupBox Grid.Column="1" Grid.Row="2" Header="Client information"
|
<GroupBox Grid.Column="1" Grid.Row="2" Header="{DynamicResource Dialog.Report.ClientInfo}"
|
||||||
Margin="8,0,8,4" FontSize="13">
|
Margin="8,0,8,4" FontSize="13">
|
||||||
<TextBox Text="{Binding ClientInfo, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding ClientInfo, UpdateSourceTrigger=PropertyChanged}"
|
||||||
TextWrapping="Wrap" AcceptsReturn="True"
|
TextWrapping="Wrap" AcceptsReturn="True"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<!-- ── Observations ─────────────────────────────────────────────────── -->
|
<!-- ── Observations ─────────────────────────────────────────────────── -->
|
||||||
<GroupBox Grid.Column="1" Grid.Row="3" Header="Observations"
|
<GroupBox Grid.Column="1" Grid.Row="3" Header="{DynamicResource Dialog.Report.Observations}"
|
||||||
Margin="8,0,8,4" FontSize="13">
|
Margin="8,0,8,4" FontSize="13">
|
||||||
<TextBox Text="{Binding Observations, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding Observations, UpdateSourceTrigger=PropertyChanged}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label Content="Operator:" VerticalAlignment="Center" FontSize="13"/>
|
<Label Content="{DynamicResource Dialog.Report.Operator}" VerticalAlignment="Center" FontSize="13"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Grid.Column="1" Text="{Binding OperatorName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
VerticalAlignment="Center" FontSize="13" Margin="4,0" Height="24"
|
VerticalAlignment="Center" FontSize="13" Margin="4,0" Height="24"
|
||||||
IsReadOnly="True" Background="#F0F0F0"/>
|
IsReadOnly="True" Background="#F0F0F0"/>
|
||||||
@@ -95,11 +95,11 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Label Content="Company:" VerticalAlignment="Center" FontSize="13"/>
|
<Label Content="{DynamicResource Dialog.Report.Company}" VerticalAlignment="Center" FontSize="13"/>
|
||||||
<TextBox Grid.Column="1" Text="{Binding CompanyName, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Grid.Column="1" Text="{Binding CompanyName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
VerticalAlignment="Center" FontSize="13" Margin="4,0" Height="24"/>
|
VerticalAlignment="Center" FontSize="13" Margin="4,0" Height="24"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<GroupBox Grid.Row="1" Header="Company information"
|
<GroupBox Grid.Row="1" Header="{DynamicResource Dialog.Report.CompanyInfo}"
|
||||||
Margin="0,4,0,0" FontSize="13">
|
Margin="0,4,0,0" FontSize="13">
|
||||||
<TextBox Text="{Binding CompanyInfo, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding CompanyInfo, UpdateSourceTrigger=PropertyChanged}"
|
||||||
TextWrapping="Wrap" AcceptsReturn="True"
|
TextWrapping="Wrap" AcceptsReturn="True"
|
||||||
@@ -109,15 +109,15 @@
|
|||||||
|
|
||||||
<!-- ── Buttons row ──────────────────────────────────────────────────── -->
|
<!-- ── Buttons row ──────────────────────────────────────────────────── -->
|
||||||
<Button Grid.Row="4" Grid.Column="0"
|
<Button Grid.Row="4" Grid.Column="0"
|
||||||
Content="Delete Client" Padding="6,2" Margin="10,8,0,8"
|
Content="{DynamicResource Dialog.Report.DeleteClient}" Padding="6,2" Margin="10,8,0,8"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
Command="{Binding DeleteClientCommand}"/>
|
Command="{Binding DeleteClientCommand}"/>
|
||||||
|
|
||||||
<StackPanel Grid.Row="4" Grid.Column="2"
|
<StackPanel Grid.Row="4" Grid.Column="2"
|
||||||
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,10,8">
|
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,10,8">
|
||||||
<Button Content="Generate" Width="80" Height="24" Margin="0,0,8,0"
|
<Button Content="{DynamicResource Dialog.Report.Generate}" Width="80" Height="24" Margin="0,0,8,0"
|
||||||
Command="{Binding AcceptCommand}"/>
|
Command="{Binding AcceptCommand}"/>
|
||||||
<Button Content="Cancel" Width="80" Height="24"
|
<Button Content="{DynamicResource Common.Cancel}" Width="80" Height="24"
|
||||||
Command="{Binding CancelCommand}"/>
|
Command="{Binding CancelCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
63
Views/Dialogs/RpmSafetyWarningDialog.xaml
Normal file
63
Views/Dialogs/RpmSafetyWarningDialog.xaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<Window x:Class="HC_APTBS.Views.Dialogs.RpmSafetyWarningDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{DynamicResource Dialog.RpmSafety.Title}"
|
||||||
|
Height="260" Width="460"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid Margin="16,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="48"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Warning icon -->
|
||||||
|
<TextBlock Grid.RowSpan="5" Text="⚠"
|
||||||
|
FontSize="36" Foreground="DarkOrange"
|
||||||
|
VerticalAlignment="Top" HorizontalAlignment="Center"
|
||||||
|
Margin="0,4,8,0"/>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock Grid.Column="1" Text="{DynamicResource Common.Warning}"
|
||||||
|
FontSize="18" FontWeight="Bold" Foreground="DarkOrange"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- Warning message -->
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" TextWrapping="Wrap"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
Text="{DynamicResource Dialog.RpmSafety.Message}"/>
|
||||||
|
|
||||||
|
<!-- Option 1: Turn on oil and proceed -->
|
||||||
|
<RadioButton Grid.Row="2" Grid.Column="1" Margin="0,4"
|
||||||
|
GroupName="RpmSafety"
|
||||||
|
IsChecked="{Binding IsOilAndProceedSelected}"
|
||||||
|
Content="{DynamicResource Dialog.RpmSafety.OilAndProceed}"/>
|
||||||
|
|
||||||
|
<!-- Option 2: Proceed without oil -->
|
||||||
|
<RadioButton Grid.Row="3" Grid.Column="1" Margin="0,4"
|
||||||
|
GroupName="RpmSafety"
|
||||||
|
IsChecked="{Binding IsProceedWithoutOilSelected}"
|
||||||
|
Content="{DynamicResource Dialog.RpmSafety.ProceedWithout}"/>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<StackPanel Grid.Row="5" Grid.Column="1"
|
||||||
|
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
|
<Button Content="{DynamicResource Common.Accept}" Width="80" Height="26" Margin="0,0,8,0"
|
||||||
|
Command="{Binding AcceptCommand}"/>
|
||||||
|
<Button Content="{DynamicResource Common.Cancel}" Width="80" Height="26"
|
||||||
|
Command="{Binding CancelCommand}" IsCancel="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
20
Views/Dialogs/RpmSafetyWarningDialog.xaml.cs
Normal file
20
Views/Dialogs/RpmSafetyWarningDialog.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Safety warning dialog shown when the operator starts the bench motor
|
||||||
|
/// while the oil pump is OFF. Equivalent to the old <c>WCareOnRpmOn</c>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class RpmSafetyWarningDialog : Window
|
||||||
|
{
|
||||||
|
/// <summary>Creates the dialog and wires the ViewModel.</summary>
|
||||||
|
public RpmSafetyWarningDialog(RpmSafetyWarningViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = vm;
|
||||||
|
vm.RequestClose += Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
Views/Dialogs/SettingsDialog.xaml
Normal file
333
Views/Dialogs/SettingsDialog.xaml
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
<Window x:Class="HC_APTBS.Views.Dialogs.SettingsDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="{DynamicResource Dialog.Settings.Title}"
|
||||||
|
Height="560" Width="680"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
FontFamily="Ebrima"
|
||||||
|
Background="#FFEDEDED">
|
||||||
|
|
||||||
|
<DockPanel Margin="8">
|
||||||
|
|
||||||
|
<!-- ── Bottom button bar ─────────────────────────────────────────── -->
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||||
|
<Button Content="{DynamicResource Common.Accept}" Width="80" Height="26"
|
||||||
|
Margin="0,0,8,0" Command="{Binding AcceptCommand}" IsDefault="True"/>
|
||||||
|
<Button Content="{DynamicResource Common.Cancel}" Width="80" Height="26"
|
||||||
|
Command="{Binding CancelCommand}" IsCancel="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ── Tab control ───────────────────────────────────────────────── -->
|
||||||
|
<TabControl>
|
||||||
|
|
||||||
|
<!-- ══ General ══════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.General}">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.Language}"
|
||||||
|
FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<ComboBox ItemsSource="{Binding AvailableLanguages}"
|
||||||
|
SelectedItem="{Binding SelectedLanguage}"
|
||||||
|
Width="120" HorizontalAlignment="Left"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.DaysKeepLogs}"
|
||||||
|
FontWeight="SemiBold" Margin="0,16,0,4"/>
|
||||||
|
<TextBox Text="{Binding DaysKeepLogs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Width="80" HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ Safety ═══════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.Safety}">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource Dialog.Settings.TempMax}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TempMax, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource Dialog.Settings.TempMin}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TempMin, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource Dialog.Settings.SecurityRpmLimit}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SecurityRpmLimit, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="{DynamicResource Dialog.Settings.MaxPressureBar}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding MaxPressureBar, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="{DynamicResource Dialog.Settings.ToleranceUp}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding ToleranceUpExtension, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="{DynamicResource Dialog.Settings.TolerancePfp}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding TolerancePfpExtension, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<CheckBox Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="2"
|
||||||
|
Content="{DynamicResource Dialog.Settings.IgnoreTin}"
|
||||||
|
IsChecked="{Binding DefaultIgnoreTin}" Margin="0,4,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ PID ══════════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.Pid}">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource Dialog.Settings.PidP}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding PidP, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource Dialog.Settings.PidI}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding PidI, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource Dialog.Settings.PidD}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding PidD, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="{DynamicResource Dialog.Settings.PidLoopMs}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding PidLoopMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ Motor ════════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.Motor}">
|
||||||
|
<DockPanel Margin="16">
|
||||||
|
<!-- Top: motor parameters -->
|
||||||
|
<Grid DockPanel.Dock="Top">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource Dialog.Settings.EncoderRes}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding EncoderResolution, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource Dialog.Settings.VoltMaxRpm}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding VoltageForMaxRpm, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource Dialog.Settings.MaxRpm}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding MaxRpm, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<CheckBox Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
|
||||||
|
Content="{DynamicResource Dialog.Settings.RightRelay}"
|
||||||
|
IsChecked="{Binding RightRelayValue}" Margin="0,4,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- RPM-Voltage relation table -->
|
||||||
|
<GroupBox Margin="0,12,0,0">
|
||||||
|
<GroupBox.Header>
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.Relations}" FontWeight="SemiBold"/>
|
||||||
|
</GroupBox.Header>
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Margin="0,4,0,0">
|
||||||
|
<Button Content="{DynamicResource Dialog.Settings.AddRow}"
|
||||||
|
Command="{Binding AddRelationCommand}"
|
||||||
|
Width="75" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="{DynamicResource Dialog.Settings.RemoveRow}"
|
||||||
|
Command="{Binding RemoveRelationCommand}"
|
||||||
|
CommandParameter="{Binding SelectedItem, ElementName=RelationsGrid}"
|
||||||
|
Width="75"/>
|
||||||
|
</StackPanel>
|
||||||
|
<DataGrid x:Name="RelationsGrid"
|
||||||
|
ItemsSource="{Binding Relations}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
CanUserAddRows="False"
|
||||||
|
CanUserDeleteRows="False"
|
||||||
|
SelectionMode="Single"
|
||||||
|
HeadersVisibility="Column"
|
||||||
|
GridLinesVisibility="Horizontal"
|
||||||
|
MinHeight="120">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTemplateColumn Width="*">
|
||||||
|
<DataGridTemplateColumn.HeaderTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.RelRpm}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.HeaderTemplate>
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Rpm}" VerticalAlignment="Center" Margin="4,0"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataGridTemplateColumn.CellEditingTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBox Text="{Binding Rpm, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellEditingTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTemplateColumn Width="*">
|
||||||
|
<DataGridTemplateColumn.HeaderTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.RelVoltage}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.HeaderTemplate>
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Voltage}" VerticalAlignment="Center" Margin="4,0"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataGridTemplateColumn.CellEditingTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBox Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellEditingTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
</DockPanel>
|
||||||
|
</GroupBox>
|
||||||
|
</DockPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ Company ══════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.Company}">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.CompanyName}"
|
||||||
|
FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<TextBox Text="{Binding CompanyName, UpdateSourceTrigger=LostFocus}"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.CompanyInfo}"
|
||||||
|
FontWeight="SemiBold" Margin="0,12,0,4"/>
|
||||||
|
<TextBox Text="{Binding CompanyInfo, UpdateSourceTrigger=LostFocus}"
|
||||||
|
TextWrapping="Wrap" AcceptsReturn="True" Height="80"
|
||||||
|
VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.ReportLogo}"
|
||||||
|
FontWeight="SemiBold" Margin="0,12,0,4"/>
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="..." Width="30"
|
||||||
|
Margin="4,0,0,0" Command="{Binding BrowseLogoCommand}"/>
|
||||||
|
<TextBox Text="{Binding ReportLogoPath, UpdateSourceTrigger=LostFocus}"
|
||||||
|
IsReadOnly="True" Background="#F0F0F0"/>
|
||||||
|
</DockPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ K-Line ═══════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.KLine}">
|
||||||
|
<StackPanel Margin="16">
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.KLinePort}"
|
||||||
|
FontWeight="SemiBold" Margin="0,0,0,4"/>
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right"
|
||||||
|
Content="{DynamicResource Dialog.Settings.RefreshPorts}"
|
||||||
|
Margin="8,0,0,0" Width="80"
|
||||||
|
Command="{Binding RefreshPortsCommand}"/>
|
||||||
|
<ComboBox ItemsSource="{Binding AvailablePorts}"
|
||||||
|
SelectedItem="{Binding SelectedKLinePort}"
|
||||||
|
IsEditable="True"
|
||||||
|
Text="{Binding SelectedKLinePort, UpdateSourceTrigger=LostFocus}"/>
|
||||||
|
</DockPanel>
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Settings.KLineHint}"
|
||||||
|
FontStyle="Italic" Foreground="Gray" Margin="0,8,0,0"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- ══ Advanced ═════════════════════════════════════════════════ -->
|
||||||
|
<TabItem Header="{DynamicResource Dialog.Settings.Tab.Advanced}">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="120"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="{DynamicResource Dialog.Settings.RefreshBench}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding RefreshBenchInterfaceMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Text="{DynamicResource Dialog.Settings.RefreshReading}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding RefreshWhileReadingMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Text="{DynamicResource Dialog.Settings.RefreshCanBus}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding RefreshCanBusReadMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Text="{DynamicResource Dialog.Settings.RefreshPumpReq}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding RefreshPumpRequestMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" Text="{DynamicResource Dialog.Settings.RefreshPumpParams}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding RefreshPumpParamsMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Grid.Column="0" Text="{DynamicResource Dialog.Settings.BlinkInterval}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding BlinkIntervalMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="6" Grid.Column="0" Text="{DynamicResource Dialog.Settings.FlasherInterval}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,6"/>
|
||||||
|
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding FlasherIntervalMs, UpdateSourceTrigger=LostFocus}"
|
||||||
|
Margin="0,0,0,6"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
</TabControl>
|
||||||
|
</DockPanel>
|
||||||
|
</Window>
|
||||||
18
Views/Dialogs/SettingsDialog.xaml.cs
Normal file
18
Views/Dialogs/SettingsDialog.xaml.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dialog for editing all application settings.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SettingsDialog : Window
|
||||||
|
{
|
||||||
|
public SettingsDialog(SettingsViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = vm;
|
||||||
|
vm.RequestClose += Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Views/Dialogs/UnlockProgressDialog.xaml
Normal file
138
Views/Dialogs/UnlockProgressDialog.xaml
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<Window x:Class="HC_APTBS.Views.Dialogs.UnlockProgressDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{DynamicResource Dialog.Unlock.Title}"
|
||||||
|
Height="360" Width="340"
|
||||||
|
WindowStyle="None" ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Topmost="True"
|
||||||
|
Background="#FF2B2929"
|
||||||
|
MouseLeftButtonDown="OnMouseDrag"
|
||||||
|
Closing="OnWindowClosing">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="210"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Unlock type label -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{Binding UnlockTypeLabel, Mode=OneWay}"
|
||||||
|
FontSize="14" Foreground="#AAAAAA"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
|
<!-- Progress ring area -->
|
||||||
|
<Grid Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<!-- Background ring -->
|
||||||
|
<Ellipse Width="200" Height="200"
|
||||||
|
Stroke="#4D4D4D" StrokeThickness="10"
|
||||||
|
Fill="Transparent"/>
|
||||||
|
|
||||||
|
<!-- Content inside ring -->
|
||||||
|
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{DynamicResource Dialog.Unlock.Progress}"
|
||||||
|
FontSize="12" Foreground="#888888"
|
||||||
|
HorizontalAlignment="Center" Margin="0,0,0,4"/>
|
||||||
|
|
||||||
|
<!-- Percentage -->
|
||||||
|
<TextBlock FontSize="60" FontFamily="Courier New"
|
||||||
|
Foreground="White" HorizontalAlignment="Center">
|
||||||
|
<TextBlock.Text>
|
||||||
|
<Binding Path="Progress" Mode="OneWay"
|
||||||
|
StringFormat="{}{0}%"/>
|
||||||
|
</TextBlock.Text>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Elapsed time -->
|
||||||
|
<TextBlock Text="{Binding ElapsedTime, Mode=OneWay}"
|
||||||
|
FontSize="16" FontFamily="Courier New"
|
||||||
|
Foreground="#CCCCCC" HorizontalAlignment="Center"
|
||||||
|
Margin="0,2,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Phase text -->
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="{Binding PhaseText, Mode=OneWay}"
|
||||||
|
FontSize="16" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" Margin="0,4,0,6"/>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<ProgressBar Grid.Row="3"
|
||||||
|
Value="{Binding Progress, Mode=OneWay}"
|
||||||
|
Minimum="0" Maximum="100"
|
||||||
|
Height="12" Margin="8,0"
|
||||||
|
Foreground="#00EC00" Background="#3D3D3D"/>
|
||||||
|
|
||||||
|
<!-- Result text — overlays the spacer row so it never displaces buttons -->
|
||||||
|
<TextBlock Grid.Row="4"
|
||||||
|
Text="{Binding ResultText, Mode=OneWay}"
|
||||||
|
FontSize="22" FontWeight="Bold"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Visibility="{Binding IsComplete, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#FF5858"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSuccess}" Value="True">
|
||||||
|
<Setter Property="Foreground" Value="#00EC00"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<StackPanel Grid.Row="5" Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Center" Margin="0,8,0,0">
|
||||||
|
<!-- Cancel button -->
|
||||||
|
<Button Content="{DynamicResource Common.Cancel}" Width="90" Height="30"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Command="{Binding CancelCommand}"
|
||||||
|
Foreground="White" FontWeight="Bold">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#FF5858"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Background" Value="#4D4D4D"/>
|
||||||
|
<Setter Property="Foreground" Value="#888888"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Close button -->
|
||||||
|
<Button Content="{DynamicResource Common.Close}" Width="90" Height="30"
|
||||||
|
Command="{Binding CloseCommand}"
|
||||||
|
Foreground="White" FontWeight="Bold">
|
||||||
|
<Button.Style>
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="#4D4D4D"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="True">
|
||||||
|
<Setter Property="Background" Value="#337AB7"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Foreground" Value="#888888"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Button.Style>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
49
Views/Dialogs/UnlockProgressDialog.xaml.cs
Normal file
49
Views/Dialogs/UnlockProgressDialog.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Non-modal window showing immobilizer unlock progress.
|
||||||
|
/// Prevents user-initiated closing until the unlock sequence completes;
|
||||||
|
/// programmatic close via <see cref="ForceClose"/> always succeeds.
|
||||||
|
/// Equivalent to the old <c>WUnlocker</c> window.
|
||||||
|
/// </summary>
|
||||||
|
public partial class UnlockProgressDialog : Window
|
||||||
|
{
|
||||||
|
private bool _forceClose;
|
||||||
|
|
||||||
|
/// <summary>Creates the dialog and wires the ViewModel.</summary>
|
||||||
|
public UnlockProgressDialog(UnlockProgressViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = vm;
|
||||||
|
vm.RequestClose += ForceClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Closes the window unconditionally (bypasses the completion guard).</summary>
|
||||||
|
public void ForceClose()
|
||||||
|
{
|
||||||
|
_forceClose = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Allows dragging the borderless window by clicking anywhere.</summary>
|
||||||
|
private void OnMouseDrag(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ChangedButton == MouseButton.Left)
|
||||||
|
DragMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Prevents user-initiated closing while the unlock sequence is still running.</summary>
|
||||||
|
private void OnWindowClosing(object? sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_forceClose) return;
|
||||||
|
|
||||||
|
if (DataContext is UnlockProgressViewModel vm && !vm.IsComplete)
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="User Authentication"
|
Title="{DynamicResource Dialog.UserCheck.Title}"
|
||||||
Height="170" Width="420"
|
Height="170" Width="420"
|
||||||
ResizeMode="NoResize"
|
ResizeMode="NoResize"
|
||||||
WindowStartupLocation="CenterOwner">
|
WindowStartupLocation="CenterOwner">
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Username -->
|
<!-- Username -->
|
||||||
<Label Content="Username:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
<Label Content="{DynamicResource Dialog.UserCheck.Username}" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||||
<TextBox Grid.Column="1" Margin="8,4" Height="26" VerticalContentAlignment="Center"
|
<TextBox Grid.Column="1" Margin="8,4" Height="26" VerticalContentAlignment="Center"
|
||||||
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}"/>
|
Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
|
|
||||||
<!-- Password -->
|
<!-- Password -->
|
||||||
<Label Grid.Row="1" Content="Password:" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
<Label Grid.Row="1" Content="{DynamicResource Dialog.UserCheck.Password}" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||||
<PasswordBox x:Name="PBPassword" Grid.Row="1" Grid.Column="1"
|
<PasswordBox x:Name="PBPassword" Grid.Row="1" Grid.Column="1"
|
||||||
Margin="8,4" Height="26" VerticalContentAlignment="Center"
|
Margin="8,4" Height="26" VerticalContentAlignment="Center"
|
||||||
PasswordChanged="OnPasswordChanged"/>
|
PasswordChanged="OnPasswordChanged"/>
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<StackPanel Grid.Row="2" Grid.Column="1"
|
<StackPanel Grid.Row="2" Grid.Column="1"
|
||||||
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||||
<Button Content="Accept" Width="80" Height="26" Margin="0,0,8,0"
|
<Button Content="{DynamicResource Common.Accept}" Width="80" Height="26" Margin="0,0,8,0"
|
||||||
Command="{Binding AcceptCommand}" IsDefault="True"/>
|
Command="{Binding AcceptCommand}" IsDefault="True"/>
|
||||||
<Button Content="Cancel" Width="80" Height="26"
|
<Button Content="{DynamicResource Common.Cancel}" Width="80" Height="26"
|
||||||
Command="{Binding CancelCommand}" IsCancel="True"/>
|
Command="{Binding CancelCommand}" IsCancel="True"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
59
Views/Dialogs/VoltageWarningDialog.xaml
Normal file
59
Views/Dialogs/VoltageWarningDialog.xaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<Window x:Class="HC_APTBS.Views.Dialogs.VoltageWarningDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{DynamicResource Dialog.Voltage.Title}"
|
||||||
|
Height="200" Width="420"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid Margin="16,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="48"/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Voltage icon -->
|
||||||
|
<TextBlock Grid.RowSpan="4" Text="⚡"
|
||||||
|
FontSize="36" Foreground="#D4A017"
|
||||||
|
VerticalAlignment="Top" HorizontalAlignment="Center"
|
||||||
|
Margin="0,4,8,0"/>
|
||||||
|
|
||||||
|
<!-- Title with voltage -->
|
||||||
|
<TextBlock Grid.Column="1" FontSize="18" FontWeight="Bold" Foreground="DarkOrange"
|
||||||
|
Margin="0,0,0,8">
|
||||||
|
<Run Text="VOLTAGE: "/>
|
||||||
|
<Run Text="{Binding Voltage, Mode=OneWay}"/>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Info message -->
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" TextWrapping="Wrap" Margin="0,0,0,4">
|
||||||
|
<Run Text="The selected pump requires "/>
|
||||||
|
<Run Text="{Binding Voltage, Mode=OneWay}" FontWeight="Bold"/>
|
||||||
|
<Run Text=" power supply."/>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Action text -->
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" TextWrapping="Wrap"
|
||||||
|
FontSize="14" FontWeight="Bold" Foreground="DarkRed"
|
||||||
|
Margin="0,4,0,0">
|
||||||
|
<Run Text="SWITCH THE POWER SUPPLY TO "/>
|
||||||
|
<Run Text="{Binding Voltage, Mode=OneWay}"/>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<!-- Acknowledge button -->
|
||||||
|
<Button Grid.Row="4" Grid.Column="1"
|
||||||
|
Content="{DynamicResource Common.Ok}" Width="80" Height="26"
|
||||||
|
HorizontalAlignment="Right" Margin="0,12,0,0"
|
||||||
|
Command="{Binding AcknowledgeCommand}" IsDefault="True"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
20
Views/Dialogs/VoltageWarningDialog.xaml.cs
Normal file
20
Views/Dialogs/VoltageWarningDialog.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using HC_APTBS.ViewModels.Dialogs;
|
||||||
|
|
||||||
|
namespace HC_APTBS.Views.Dialogs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Informational dialog warning the operator to switch the power supply
|
||||||
|
/// voltage before testing a pump. Equivalent to the old <c>WAlert27v</c>.
|
||||||
|
/// </summary>
|
||||||
|
public partial class VoltageWarningDialog : Window
|
||||||
|
{
|
||||||
|
/// <summary>Creates the dialog and wires the ViewModel.</summary>
|
||||||
|
public VoltageWarningDialog(VoltageWarningViewModel vm)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = vm;
|
||||||
|
vm.RequestClose += Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<TextBlock Grid.ColumnSpan="3" Text="ADVANCE MONITORING"
|
<TextBlock Grid.ColumnSpan="3" Text="{DynamicResource Angle.Header}"
|
||||||
HorizontalAlignment="Center" FontSize="20"
|
HorizontalAlignment="Center" FontSize="20"
|
||||||
FontWeight="Bold" FontStyle="Italic"
|
FontWeight="Bold" FontStyle="Italic"
|
||||||
Foreground="DimGray" Margin="0,0,0,4"/>
|
Foreground="DimGray" Margin="0,0,0,4"/>
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
<Button Content="0" Width="28" Height="28" FontWeight="Bold"
|
<Button Content="0" Width="28" Height="28" FontWeight="Bold"
|
||||||
VerticalAlignment="Center" Margin="0,0,4,0"
|
VerticalAlignment="Center" Margin="0,0,4,0"
|
||||||
Command="{Binding SetPsgZeroCommand}"
|
Command="{Binding SetPsgZeroCommand}"
|
||||||
ToolTip="Set PSG zero reference"/>
|
ToolTip="{DynamicResource Angle.SetPsgZero}"/>
|
||||||
<Border Style="{DynamicResource LcdBlue}" Padding="4,0" Width="170">
|
<Border Style="{DynamicResource LcdBlue}" Padding="4,0" Width="170">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock Text="PSG:" DockPanel.Dock="Left"
|
<TextBlock Text="{DynamicResource Angle.Psg}" DockPanel.Dock="Left"
|
||||||
VerticalAlignment="Bottom" Margin="2,0,0,8"
|
VerticalAlignment="Bottom" Margin="2,0,0,8"
|
||||||
FontSize="10" FontWeight="Bold"
|
FontSize="10" FontWeight="Bold"
|
||||||
Foreground="{Binding PsgAngleForeground}"/>
|
Foreground="{Binding PsgAngleForeground}"/>
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
<Button Content="0" Width="28" Height="28" FontWeight="Bold"
|
<Button Content="0" Width="28" Height="28" FontWeight="Bold"
|
||||||
VerticalAlignment="Center" Margin="0,0,4,0"
|
VerticalAlignment="Center" Margin="0,0,4,0"
|
||||||
Command="{Binding SetInjZeroCommand}"
|
Command="{Binding SetInjZeroCommand}"
|
||||||
ToolTip="Set INJ zero reference"/>
|
ToolTip="{DynamicResource Angle.SetInjZero}"/>
|
||||||
<Border Style="{DynamicResource LcdBlue}" Padding="4,0" Width="170">
|
<Border Style="{DynamicResource LcdBlue}" Padding="4,0" Width="170">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock Text="INJ:" DockPanel.Dock="Left"
|
<TextBlock Text="{DynamicResource Angle.Inj}" DockPanel.Dock="Left"
|
||||||
VerticalAlignment="Bottom" Margin="2,0,0,8"
|
VerticalAlignment="Bottom" Margin="2,0,0,8"
|
||||||
FontSize="10" FontWeight="Bold"
|
FontSize="10" FontWeight="Bold"
|
||||||
Foreground="{Binding InjAngleForeground}"/>
|
Foreground="{Binding InjAngleForeground}"/>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<!-- ABS (manual encoder) -->
|
<!-- ABS (manual encoder) -->
|
||||||
<Border Style="{DynamicResource LcdBlue}" Height="56" Margin="0,2" Padding="4,0">
|
<Border Style="{DynamicResource LcdBlue}" Height="56" Margin="0,2" Padding="4,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock Text="ABS º:" DockPanel.Dock="Left"
|
<TextBlock Text="{DynamicResource Angle.AbsDeg}" DockPanel.Dock="Left"
|
||||||
VerticalAlignment="Bottom" Margin="4,0,0,8"
|
VerticalAlignment="Bottom" Margin="4,0,0,8"
|
||||||
FontSize="10" FontWeight="Bold" Foreground="#FFEBEBFF"/>
|
FontSize="10" FontWeight="Bold" Foreground="#FFEBEBFF"/>
|
||||||
<TextBlock Text="{Binding ManualAngleText}"
|
<TextBlock Text="{Binding ManualAngleText}"
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
<!-- LOCK angle result -->
|
<!-- LOCK angle result -->
|
||||||
<Border Style="{DynamicResource LcdBlue}" Height="56" Margin="0,2" Padding="4,0">
|
<Border Style="{DynamicResource LcdBlue}" Height="56" Margin="0,2" Padding="4,0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<TextBlock Text="LOCK º:" DockPanel.Dock="Left"
|
<TextBlock Text="{DynamicResource Angle.LockDeg}" DockPanel.Dock="Left"
|
||||||
VerticalAlignment="Bottom" Margin="4,0,0,8"
|
VerticalAlignment="Bottom" Margin="4,0,0,8"
|
||||||
FontSize="10" FontWeight="Bold" Foreground="#FFEBEBFF"/>
|
FontSize="10" FontWeight="Bold" Foreground="#FFEBEBFF"/>
|
||||||
<TextBlock Text="{Binding LockAngleDisplay}"
|
<TextBlock Text="{Binding LockAngleDisplay}"
|
||||||
|
|||||||
@@ -20,27 +20,27 @@
|
|||||||
|
|
||||||
<!-- CAN frame fields -->
|
<!-- CAN frame fields -->
|
||||||
<WrapPanel Margin="0,0,0,4">
|
<WrapPanel Margin="0,0,0,4">
|
||||||
<Label Content="CAN-Bus ID (0x)" VerticalAlignment="Bottom" Foreground="Black"/>
|
<Label Content="{DynamicResource BenchParam.CanBusId}" VerticalAlignment="Bottom" Foreground="Black"/>
|
||||||
<TextBox Text="{Binding MessageIdHex, UpdateSourceTrigger=LostFocus}"
|
<TextBox Text="{Binding MessageIdHex, UpdateSourceTrigger=LostFocus}"
|
||||||
Width="40" VerticalAlignment="Center"
|
Width="40" VerticalAlignment="Center"
|
||||||
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
||||||
|
|
||||||
<Label Content="Byte L" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
<Label Content="{DynamicResource BenchParam.ByteL}" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
||||||
<TextBox Text="{Binding ByteL, UpdateSourceTrigger=LostFocus}"
|
<TextBox Text="{Binding ByteL, UpdateSourceTrigger=LostFocus}"
|
||||||
Width="35" VerticalAlignment="Center"
|
Width="35" VerticalAlignment="Center"
|
||||||
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
||||||
|
|
||||||
<Label Content="Byte H" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
<Label Content="{DynamicResource BenchParam.ByteH}" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
||||||
<TextBox Text="{Binding ByteH, UpdateSourceTrigger=LostFocus}"
|
<TextBox Text="{Binding ByteH, UpdateSourceTrigger=LostFocus}"
|
||||||
Width="35" VerticalAlignment="Center"
|
Width="35" VerticalAlignment="Center"
|
||||||
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
||||||
|
|
||||||
<Label Content="Filter α" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
<Label Content="{DynamicResource BenchParam.FilterAlpha}" VerticalAlignment="Bottom" Foreground="Black" Margin="8,0,0,0"/>
|
||||||
<TextBox Text="{Binding Alpha, UpdateSourceTrigger=LostFocus}"
|
<TextBox Text="{Binding Alpha, UpdateSourceTrigger=LostFocus}"
|
||||||
Width="40" VerticalAlignment="Center"
|
Width="40" VerticalAlignment="Center"
|
||||||
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
Background="#66FFFFFF" BorderBrush="{x:Null}" Height="19"/>
|
||||||
|
|
||||||
<CheckBox Content="Enable formula" IsChecked="{Binding FormulaEnabled}"
|
<CheckBox Content="{DynamicResource BenchParam.EnableFormula}" IsChecked="{Binding FormulaEnabled}"
|
||||||
VerticalAlignment="Center" Foreground="Black" Margin="12,0,0,0"/>
|
VerticalAlignment="Center" Foreground="Black" Margin="12,0,0,0"/>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignHeight="150" d:DesignWidth="800" MaxHeight="150">
|
d:DesignHeight="150" d:DesignWidth="460" MaxHeight="150">
|
||||||
|
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<Style x:Key="LcdGreen" TargetType="Border">
|
<Style x:Key="LcdGreen" TargetType="Border">
|
||||||
@@ -29,18 +29,18 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Grid Margin="50,0">
|
<Grid Margin="16,8">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="75"/>
|
<RowDefinition Height="75"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="120"/>
|
<ColumnDefinition Width="144" MinWidth="60"/>
|
||||||
<ColumnDefinition Width="450"/>
|
<ColumnDefinition MinWidth="200"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- LEFT: version picker + read/write buttons -->
|
<!-- LEFT: version picker + read/write buttons -->
|
||||||
<ComboBox Margin="5" VerticalAlignment="Bottom"
|
<ComboBox Margin="4" VerticalAlignment="Bottom"
|
||||||
SelectedIndex="{Binding VersionIndex}">
|
SelectedIndex="{Binding VersionIndex}">
|
||||||
<ComboBoxItem Content="V1"/>
|
<ComboBoxItem Content="V1"/>
|
||||||
<ComboBoxItem Content="V2"/>
|
<ComboBoxItem Content="V2"/>
|
||||||
@@ -48,40 +48,51 @@
|
|||||||
<ComboBoxItem Content="V4"/>
|
<ComboBoxItem Content="V4"/>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1" Margin="4">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition Width="4"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button Content="READ" Margin="2"
|
<Button Content="{DynamicResource Dfi.Read}"
|
||||||
Command="{Binding ReadDfiCommand}"
|
Command="{Binding ReadDfiCommand}"
|
||||||
FontSize="12" FontWeight="Bold" Padding="4"/>
|
FontSize="12" FontWeight="Bold" Padding="4"/>
|
||||||
<Button Grid.Column="1" Content="WRITE" Margin="2"
|
<Button Grid.Column="2" Content="{DynamicResource Dfi.Write}"
|
||||||
Command="{Binding WriteDfiCommand}"
|
Command="{Binding WriteDfiCommand}"
|
||||||
FontSize="12" FontWeight="Bold" Padding="4"/>
|
FontSize="12" FontWeight="Bold" Padding="4"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- TOP RIGHT: DFI value LCD + auto checkbox -->
|
<!-- TOP RIGHT: DFI value LCD + auto checkbox -->
|
||||||
<Grid Grid.Row="0" Grid.Column="1" Margin="150,5">
|
<Grid Grid.Row="0" Grid.Column="1" Margin="4">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition Width="Auto" MinWidth="200"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition MinWidth="100"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border Grid.ColumnSpan="2" Style="{StaticResource LcdGreen}"/>
|
<Border Grid.ColumnSpan="1" Style="{StaticResource LcdGreen}" Margin="0,0,12,0" >
|
||||||
<Border Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="Black" SnapsToDevicePixels="True"/>
|
<Grid Grid.Row="1" >
|
||||||
<TextBlock Text="DFI:"
|
<Grid.ColumnDefinitions>
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
<ColumnDefinition/>
|
||||||
Foreground="Black" FontSize="18" FontFamily="Consolas"/>
|
<ColumnDefinition/>
|
||||||
<TextBlock Text="{Binding CurrentDfi, StringFormat=F2, Mode=OneWay}"
|
</Grid.ColumnDefinitions>
|
||||||
Grid.Column="1"
|
<TextBlock Text="{DynamicResource Dfi.Label}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
FontSize="26" FontWeight="Bold" Foreground="Black"/>
|
Foreground="Black" FontSize="18" FontFamily="Consolas"/>
|
||||||
|
<TextBlock Text="{Binding CurrentDfi, StringFormat=F2, Mode=OneWay}"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
FontSize="26" FontWeight="Bold" Foreground="Black"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.ColumnSpan="1" BorderThickness="1" BorderBrush="Black" Margin="0,0,12,0" SnapsToDevicePixels="True"/>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- AUTO checkbox — sits outside the column pair; placed in Column=1 outside normal layout -->
|
<!-- AUTO checkbox — sits outside the column pair; placed in Column=1 outside normal layout -->
|
||||||
<CheckBox IsChecked="{Binding IsAutoMode}"
|
<CheckBox IsChecked="{Binding IsAutoMode}"
|
||||||
Content="AUTO"
|
Content="{DynamicResource Dfi.Auto}"
|
||||||
Grid.Row="0" Grid.Column="1"
|
Grid.Row="0" Grid.Column="1"
|
||||||
|
Height="Auto"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
Margin="0,0,10,0"
|
Margin="0,0,10,0"
|
||||||
Foreground="Black" FontSize="20"/>
|
Foreground="Black" FontSize="20"/>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="FBKW - Advance Control" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource PumpCtrl.Fbkw}" HorizontalAlignment="Center"
|
||||||
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
||||||
|
|
||||||
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
Width="28" Height="28" Margin="0,0,4,0"
|
Width="28" Height="28" Margin="0,0,4,0"
|
||||||
Background="Transparent" BorderBrush="Transparent"
|
Background="Transparent" BorderBrush="Transparent"
|
||||||
Content="..." FontWeight="Bold" FontSize="14"
|
Content="..." FontWeight="Bold" FontSize="14"
|
||||||
ToolTip="Min / Step / Max"/>
|
ToolTip="{DynamicResource PumpCtrl.MinStepMax}"/>
|
||||||
<!-- Numeric text box -->
|
<!-- Numeric text box -->
|
||||||
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
||||||
TextAlignment="Center" VerticalContentAlignment="Center"
|
TextAlignment="Center" VerticalContentAlignment="Center"
|
||||||
@@ -68,17 +68,17 @@
|
|||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding FbkwMin, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding FbkwMin, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Min" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Min}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding FbkwStep, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding FbkwStep, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Step" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Step}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding FbkwMax, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding FbkwMax, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Max" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Max}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UniformGrid>
|
</UniformGrid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="ME - Quantity Control" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource PumpCtrl.Me}" HorizontalAlignment="Center"
|
||||||
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
||||||
|
|
||||||
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
Width="28" Height="28" Margin="0,0,4,0"
|
Width="28" Height="28" Margin="0,0,4,0"
|
||||||
Background="Transparent" BorderBrush="Transparent"
|
Background="Transparent" BorderBrush="Transparent"
|
||||||
Content="..." FontWeight="Bold" FontSize="14"
|
Content="..." FontWeight="Bold" FontSize="14"
|
||||||
ToolTip="Min / Step / Max"/>
|
ToolTip="{DynamicResource PumpCtrl.MinStepMax}"/>
|
||||||
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
||||||
TextAlignment="Center" VerticalContentAlignment="Center"
|
TextAlignment="Center" VerticalContentAlignment="Center"
|
||||||
FontWeight="Bold" FontSize="13"
|
FontWeight="Bold" FontSize="13"
|
||||||
@@ -120,17 +120,17 @@
|
|||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding MeMin, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding MeMin, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Min" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Min}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding MeStep, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding MeStep, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Step" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Step}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding MeMax, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding MeMax, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Max" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Max}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UniformGrid>
|
</UniformGrid>
|
||||||
</Border>
|
</Border>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="ME - Pre-inj Quantity" HorizontalAlignment="Center"
|
<TextBlock Text="{DynamicResource PumpCtrl.PreInj}" HorizontalAlignment="Center"
|
||||||
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
FontSize="13" Foreground="Black" Margin="0,0,0,2"/>
|
||||||
|
|
||||||
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
<DockPanel Grid.Row="1" Margin="4,0,4,2">
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
Width="28" Height="28" Margin="0,0,4,0"
|
Width="28" Height="28" Margin="0,0,4,0"
|
||||||
Background="Transparent" BorderBrush="Transparent"
|
Background="Transparent" BorderBrush="Transparent"
|
||||||
Content="..." FontWeight="Bold" FontSize="14"
|
Content="..." FontWeight="Bold" FontSize="14"
|
||||||
ToolTip="Min / Step / Max"/>
|
ToolTip="{DynamicResource PumpCtrl.MinStepMax}"/>
|
||||||
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
<TextBox DockPanel.Dock="Right" Width="50" Height="28" Margin="4,0,0,0"
|
||||||
TextAlignment="Center" VerticalContentAlignment="Center"
|
TextAlignment="Center" VerticalContentAlignment="Center"
|
||||||
FontWeight="Bold" FontSize="13"
|
FontWeight="Bold" FontSize="13"
|
||||||
@@ -173,17 +173,17 @@
|
|||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding PreInMin, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding PreInMin, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Min" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Min}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding PreInStep, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding PreInStep, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Step" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Step}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center">
|
<StackPanel HorizontalAlignment="Center">
|
||||||
<TextBox Text="{Binding PreInMax, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox Text="{Binding PreInMax, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Style="{StaticResource SettingsTextBox}"/>
|
Style="{StaticResource SettingsTextBox}"/>
|
||||||
<TextBlock Text="Max" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
<TextBlock Text="{DynamicResource PumpCtrl.Max}" Style="{StaticResource SettingsLabel}" HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UniformGrid>
|
</UniformGrid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Text="Pump:" VerticalAlignment="Center" Margin="0,0,8,0" FontSize="14"/>
|
<TextBlock Text="{DynamicResource PumpId.Label}" VerticalAlignment="Center" Margin="0,0,8,0" FontSize="14"/>
|
||||||
<ComboBox Grid.Column="1"
|
<ComboBox Grid.Column="1"
|
||||||
ItemsSource="{Binding PumpIds}"
|
ItemsSource="{Binding PumpIds}"
|
||||||
SelectedItem="{Binding SelectedPumpId}"
|
SelectedItem="{Binding SelectedPumpId}"
|
||||||
@@ -36,43 +36,43 @@
|
|||||||
|
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="DFI:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.Dfi}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineDfi}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineDfi}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Pump ID:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.PumpId}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlinePumpId}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlinePumpId}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Serial No:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.SerialNo}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineSerialNumber}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineSerialNumber}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Model Ref:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.ModelRef}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineModelRef}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineModelRef}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Data Record:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.DataRecord}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineDataRecord}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineDataRecord}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Pump Ctrl:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.PumpCtrl}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlinePumpControl}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlinePumpControl}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Model Index:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.ModelIndex}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineModelIndex}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineModelIndex}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="SW Ver 1:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.SwVer1}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineSwVersion1}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineSwVersion1}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="SW Ver 2:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.SwVer2}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineSwVersion2}" FontSize="12" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding KlineSwVersion2}" FontSize="12" FontFamily="Consolas"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock Text="Errors:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.Errors}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineErrors}" FontSize="12" FontFamily="Consolas" Foreground="DarkRed"/>
|
<TextBlock Text="{Binding KlineErrors}" FontSize="12" FontFamily="Consolas" Foreground="DarkRed"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<!-- ConnectError row: auto-collapses when empty via DataTrigger -->
|
<!-- ConnectError row: auto-collapses when empty via DataTrigger -->
|
||||||
@@ -86,15 +86,15 @@
|
|||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</StackPanel.Style>
|
</StackPanel.Style>
|
||||||
<TextBlock Text="Error:" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
<TextBlock Text="{DynamicResource PumpId.Error}" FontSize="12" Margin="0,0,4,0" Foreground="Gray" Width="90"/>
|
||||||
<TextBlock Text="{Binding KlineConnectError}" FontSize="12" FontFamily="Consolas" Foreground="Red"/>
|
<TextBlock Text="{Binding KlineConnectError}" FontSize="12" FontFamily="Consolas" Foreground="Red"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,0,0">
|
||||||
<Button Content="Read K-Line" Width="90" Margin="0,2"
|
<Button Content="{DynamicResource PumpId.ReadKLine}" Width="90" Margin="0,2"
|
||||||
Command="{Binding ReadKlineCommand}"/>
|
Command="{Binding ReadKlineCommand}"/>
|
||||||
<Button Content="Disconnect" Width="90" Margin="0,2"
|
<Button Content="{DynamicResource PumpId.Disconnect}" Width="90" Margin="0,2"
|
||||||
Command="{Binding DisconnectKLineCommand}"/>
|
Command="{Binding DisconnectKLineCommand}"/>
|
||||||
<!-- Progress bar shown during K-Line read -->
|
<!-- Progress bar shown during K-Line read -->
|
||||||
<ProgressBar Value="{Binding ProgressPercent, Mode=OneWay}"
|
<ProgressBar Value="{Binding ProgressPercent, Mode=OneWay}"
|
||||||
|
|||||||
@@ -72,12 +72,12 @@
|
|||||||
<ColumnDefinition Width="1*"/>
|
<ColumnDefinition Width="1*"/>
|
||||||
<ColumnDefinition Width="1*"/>
|
<ColumnDefinition Width="1*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<TextBlock Text="Phase" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray"/>
|
<TextBlock Text="{DynamicResource Result.Phase}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray"/>
|
||||||
<TextBlock Text="Parameter" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="1"/>
|
<TextBlock Text="{DynamicResource Result.Parameter}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="1"/>
|
||||||
<TextBlock Text="Target" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="2"/>
|
<TextBlock Text="{DynamicResource Result.Target}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="2"/>
|
||||||
<TextBlock Text="Tol ±" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="3"/>
|
<TextBlock Text="{DynamicResource Result.Tolerance}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="3"/>
|
||||||
<TextBlock Text="Average" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="4"/>
|
<TextBlock Text="{DynamicResource Result.Average}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="4"/>
|
||||||
<TextBlock Text="Result" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="5"/>
|
<TextBlock Text="{DynamicResource Result.ResultHeader}" FontSize="10" FontWeight="Bold" Margin="3,0" Foreground="Gray" Grid.Column="5"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Result rows -->
|
<!-- Result rows -->
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
Margin="16,0,0,0"/>
|
Margin="16,0,0,0"/>
|
||||||
|
|
||||||
<!-- Critical indicator -->
|
<!-- Critical indicator -->
|
||||||
<TextBlock Text="Critical" FontSize="9"
|
<TextBlock Text="{DynamicResource Test.Critical}" FontSize="9"
|
||||||
Foreground="#E65100" FontWeight="Bold"
|
Foreground="#E65100" FontWeight="Bold"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Visibility="{Binding IsCritical, Converter={StaticResource BoolToVis}}"/>
|
Visibility="{Binding IsCritical, Converter={StaticResource BoolToVis}}"/>
|
||||||
@@ -178,13 +178,13 @@
|
|||||||
Margin="0,3,0,0">
|
Margin="0,3,0,0">
|
||||||
<!-- Ready values -->
|
<!-- Ready values -->
|
||||||
<StackPanel Visibility="{Binding ReadyValues.Count, FallbackValue=Collapsed}">
|
<StackPanel Visibility="{Binding ReadyValues.Count, FallbackValue=Collapsed}">
|
||||||
<TextBlock Text="Required:" FontSize="9" Foreground="#666"
|
<TextBlock Text="{DynamicResource Test.Required}" FontSize="9" Foreground="#666"
|
||||||
FontWeight="SemiBold" Margin="0,1,0,0"/>
|
FontWeight="SemiBold" Margin="0,1,0,0"/>
|
||||||
<ItemsControl ItemsSource="{Binding ReadyValues}"/>
|
<ItemsControl ItemsSource="{Binding ReadyValues}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Send values -->
|
<!-- Send values -->
|
||||||
<TextBlock Text="Test:" FontSize="9" Foreground="#666"
|
<TextBlock Text="{DynamicResource Test.TestLabel}" FontSize="9" Foreground="#666"
|
||||||
FontWeight="SemiBold" Margin="0,2,0,0"/>
|
FontWeight="SemiBold" Margin="0,2,0,0"/>
|
||||||
<ItemsControl ItemsSource="{Binding OperationValues}"/>
|
<ItemsControl ItemsSource="{Binding OperationValues}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -265,17 +265,17 @@
|
|||||||
<StackPanel Grid.Column="2" Orientation="Horizontal"
|
<StackPanel Grid.Column="2" Orientation="Horizontal"
|
||||||
VerticalAlignment="Center" Margin="16,0,0,0">
|
VerticalAlignment="Center" Margin="16,0,0,0">
|
||||||
<TextBlock FontSize="10" Foreground="DimGray">
|
<TextBlock FontSize="10" Foreground="DimGray">
|
||||||
<Run Text="Cond: "/>
|
<Run Text="{DynamicResource Test.Condition}"/>
|
||||||
<Run Text="{Binding ConditioningTimeSec, Mode=OneWay}"/>
|
<Run Text="{Binding ConditioningTimeSec, Mode=OneWay}"/>
|
||||||
<Run Text="s"/>
|
<Run Text="s"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock FontSize="10" Foreground="DimGray" Margin="10,0,0,0">
|
<TextBlock FontSize="10" Foreground="DimGray" Margin="10,0,0,0">
|
||||||
<Run Text="Meas: "/>
|
<Run Text="{DynamicResource Test.Measurement}"/>
|
||||||
<Run Text="{Binding MeasurementTimeSec, Mode=OneWay}"/>
|
<Run Text="{Binding MeasurementTimeSec, Mode=OneWay}"/>
|
||||||
<Run Text="s"/>
|
<Run Text="s"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
<TextBlock FontSize="10" Foreground="DimGray" Margin="10,0,0,0">
|
<TextBlock FontSize="10" Foreground="DimGray" Margin="10,0,0,0">
|
||||||
<Run Text="M/s: "/>
|
<Run Text="{DynamicResource Test.MeasPerSec}"/>
|
||||||
<Run Text="{Binding MeasurementsPerSecond, StringFormat=F1, Mode=OneWay}"/>
|
<Run Text="{Binding MeasurementsPerSecond, StringFormat=F1, Mode=OneWay}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -323,17 +323,17 @@
|
|||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Button Content="▶ START TEST" FontSize="15" FontWeight="Bold"
|
<Button Content="{DynamicResource Test.StartTest}" FontSize="15" FontWeight="Bold"
|
||||||
Height="44" Margin="0,0,4,0"
|
Height="44" Margin="0,0,4,0"
|
||||||
Command="{Binding DataContext.StartTestCommand,
|
Command="{Binding DataContext.StartTestCommand,
|
||||||
RelativeSource={RelativeSource AncestorType=Window}}"
|
RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
Foreground="DarkGreen"/>
|
Foreground="DarkGreen"/>
|
||||||
<Button Grid.Column="1" Content="■ STOP" FontSize="15" FontWeight="Bold"
|
<Button Grid.Column="1" Content="{DynamicResource Test.Stop}" FontSize="15" FontWeight="Bold"
|
||||||
Height="44" Margin="4,0"
|
Height="44" Margin="4,0"
|
||||||
Command="{Binding DataContext.StopTestCommand,
|
Command="{Binding DataContext.StopTestCommand,
|
||||||
RelativeSource={RelativeSource AncestorType=Window}}"
|
RelativeSource={RelativeSource AncestorType=Window}}"
|
||||||
Foreground="DarkRed"/>
|
Foreground="DarkRed"/>
|
||||||
<Button Grid.Column="2" Content="📄 Report" FontSize="13"
|
<Button Grid.Column="2" Content="{DynamicResource Test.Report}" FontSize="13"
|
||||||
Height="44" Margin="4,0,0,0"
|
Height="44" Margin="4,0,0,0"
|
||||||
Command="{Binding DataContext.GenerateReportCommand,
|
Command="{Binding DataContext.GenerateReportCommand,
|
||||||
RelativeSource={RelativeSource AncestorType=Window}}"/>
|
RelativeSource={RelativeSource AncestorType=Window}}"/>
|
||||||
@@ -353,14 +353,14 @@
|
|||||||
<!-- Show values toggle -->
|
<!-- Show values toggle -->
|
||||||
<CheckBox IsChecked="{Binding ShowOperationValues}"
|
<CheckBox IsChecked="{Binding ShowOperationValues}"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<TextBlock Text="Show values" FontSize="12"/>
|
<TextBlock Text="{DynamicResource Test.ShowValues}" FontSize="12"/>
|
||||||
</CheckBox>
|
</CheckBox>
|
||||||
|
|
||||||
<!-- Check all button -->
|
<!-- Check all button -->
|
||||||
<Button Grid.Column="1" Margin="12,0,0,0"
|
<Button Grid.Column="1" Margin="12,0,0,0"
|
||||||
Command="{Binding ToggleCheckAllCommand}"
|
Command="{Binding ToggleCheckAllCommand}"
|
||||||
Padding="6,2" ToolTip="Enable/disable all phases">
|
Padding="6,2" ToolTip="Enable/disable all phases">
|
||||||
<TextBlock Text="Check All" FontSize="11"/>
|
<TextBlock Text="{DynamicResource Test.CheckAll}" FontSize="11"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Remaining time -->
|
<!-- Remaining time -->
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
Foreground="DimGray" FontSize="12" Margin="0,0,4,0">
|
Foreground="DimGray" FontSize="12" Margin="0,0,4,0">
|
||||||
<Run Text="~"/>
|
<Run Text="~"/>
|
||||||
<Run Text="{Binding RemainingSeconds, Mode=OneWay}"/>
|
<Run Text="{Binding RemainingSeconds, Mode=OneWay}"/>
|
||||||
<Run Text="s remaining"/>
|
<Run Text="{DynamicResource Test.SecondsRemaining}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,50 +1,37 @@
|
|||||||
# Gap: Ford Unlock Progress UI
|
# Gap: Ford Unlock Progress UI — RESOLVED
|
||||||
|
|
||||||
## Problem
|
## Status: Implemented
|
||||||
The `UnlockService` backend is fully functional (Phase 1 + Phase 2 + verification), but there is no dedicated UI for displaying unlock progress. The old app had `WUnlocker.xaml` — a modal dialog with a visual progress ring and status text.
|
|
||||||
|
|
||||||
## Current State
|
The unlock progress dialog, service refactoring, and K-Line fast unlock are fully implemented.
|
||||||
- `UnlockService.StatusChanged` fires every 1000ms with `"Unlocking... {pct}% ({MM:SS})"`
|
|
||||||
- `UnlockService.UnlockCompleted` fires once with `true`/`false`
|
|
||||||
- `MainViewModel` subscribes and pipes status into `VerboseStatus` (displayed as plain text somewhere in MainWindow)
|
|
||||||
- No progress bar, no percentage display, no cancel button, no dedicated dialog
|
|
||||||
|
|
||||||
## Old UI Reference (`WUnlocker.xaml`)
|
## What was done
|
||||||
- Standalone modal `Window` (300x400px), dark background (#FF2B2929), Topmost, centered on owner
|
|
||||||
- Decorative `Ellipse` ring (200x200, #4D4D4D stroke, 10px thick) as the focal point
|
|
||||||
- Inside the ring: large percentage (Courier New 60pt), "P R O G R E S S" label, elapsed time (MM:SS)
|
|
||||||
- `LBLState` at top: live lock/immo status from CAN feedback ("Bloqueada/Desbloqueada")
|
|
||||||
- `LBLVerbose` at bottom: phase description ("Unlocking...", "Testing...", "Sending")
|
|
||||||
- "Cerrar" (Close) button disabled until progress reaches 100%
|
|
||||||
- Window close prevented via `OnWindowClosing` until completion
|
|
||||||
|
|
||||||
## Spec for New Implementation
|
### 1. UnlockProgressDialog (View + ViewModel)
|
||||||
|
- **`Views/Dialogs/UnlockProgressDialog.xaml`** — Dark-themed non-modal window (#2B2929), borderless with drag support, 200x200 ellipse progress ring, Courier New 60pt percentage, MM:SS elapsed time, phase indicator, linear progress bar, result text (green/red), Cancel + Close buttons
|
||||||
|
- **`Views/Dialogs/UnlockProgressDialog.xaml.cs`** — `ForceClose()` for programmatic close, `OnWindowClosing` prevents user close until `IsComplete`, `OnMouseDrag` for window dragging
|
||||||
|
- **`ViewModels/Dialogs/UnlockProgressViewModel.cs`** — `ObservableObject` + `IDisposable`, parses `StatusChanged` events via regex, marshals to UI thread, `CancelCommand` (Phase 1 only) / `CloseCommand` (after completion), `[NotifyCanExecuteChangedFor]` wiring
|
||||||
|
|
||||||
### UnlockDialog.xaml (View)
|
### 2. UnlockService rewrite (`Services/Impl/UnlockService.cs`)
|
||||||
- Modal dialog (MVVM, no code-behind logic)
|
- **Persistent CAN senders** — Start before unlock, run indefinitely until `StopSenders()` is called on pump deselection. Prevents pump from re-locking after unlock.
|
||||||
- Progress bar (0-100%) + percentage text
|
- **Concurrent fast unlock** — While the 600s CAN wait runs with progress reporting, a parallel task awaits K-Line session Connected state, then:
|
||||||
- Elapsed time display (MM:SS)
|
1. Checks if pump is already unlocked (via `VerifyUnlock`)
|
||||||
- Phase indicator: "Phase 1: Sending unlock signals" / "Phase 2: Testing" / "Verifying..."
|
2. Sends K-Line fast unlock command (`{0x02, 0x88, 0x02, 0x03, 0xA8, 0x01, 0x00}`) which writes to pump RAM to fast-forward the internal 10 min timer
|
||||||
- Current unlock type indicator (Type 1 / Type 2)
|
3. Waits 2s, then re-checks `VerifyUnlock`
|
||||||
- Cancel button (disabled during Phase 2 — it cannot be cancelled once started)
|
4. If verified → cancels remaining wait, proceeds to Phase 2 immediately
|
||||||
- Close button (enabled only after completion)
|
- **`IUnlockService.StopSenders()`** — New interface method, called from `MainViewModel.CloseUnlockDialog()` on pump change
|
||||||
- Result indicator: green checkmark (success) / red X (failed)
|
|
||||||
|
|
||||||
### UnlockViewModel.cs (ViewModel)
|
### 3. K-Line fast unlock support
|
||||||
- `[ObservableProperty] double Progress`
|
- **`IKwpService.TryFastUnlockAsync()`** — New interface method
|
||||||
- `[ObservableProperty] string ElapsedTime`
|
- **`KwpService.TryFastUnlockAsync()`** — Sends custom command over active session, returns true if no NAK (command accepted, not unlock confirmation)
|
||||||
- `[ObservableProperty] string Phase`
|
|
||||||
- `[ObservableProperty] string Result`
|
|
||||||
- `[ObservableProperty] bool IsComplete`
|
|
||||||
- `[ObservableProperty] bool CanCancel`
|
|
||||||
- `[RelayCommand] Cancel()` — calls `CancellationTokenSource.Cancel()`
|
|
||||||
- Subscribe to `IUnlockService.StatusChanged` — parse percentage from status string
|
|
||||||
- Subscribe to `IUnlockService.UnlockCompleted` — set result and enable close
|
|
||||||
|
|
||||||
### Integration
|
### 4. MainViewModel integration
|
||||||
- Trigger: button in MainViewModel (currently exists but needs to open the dialog)
|
- **Trigger on pump selection** — `OnPumpChanged()` calls `StartUnlockIfRequired()` for pumps with `UnlockType != 0` (both manual and K-Line auto-detect)
|
||||||
- The dialog should be shown via a dialog service or `Window.ShowDialog()` from MainViewModel
|
- **Non-modal window** — `.Show()` instead of `.ShowDialog()`, user can interact with main UI during 10 min unlock
|
||||||
- Marshal all event handlers to UI thread
|
- **Test start guards** — `StartTestAsync` blocks if unlock is in progress or failed
|
||||||
|
- **Cleanup on pump change** — `CloseUnlockDialog()` stops senders, cancels CTS, disposes ViewModel, force-closes window
|
||||||
|
|
||||||
|
### 5. Bug fix: Type 1 verification
|
||||||
|
**Fixed.** Old code: `Lock = valor != 0` (non-zero = LOCKED). New code had `return Value != 0` (non-zero = SUCCESS). Changed to `return Value == 0`. Type 2 (`== 0xE4`) was already correct.
|
||||||
|
|
||||||
## Protocol Reference
|
## Protocol Reference
|
||||||
|
|
||||||
@@ -54,7 +41,7 @@ The `UnlockService` backend is fully functional (Phase 1 + Phase 2 + verificatio
|
|||||||
| Msg1 | 0x700 | `B2 00 00 00 00 00 00 00` | 500 ms |
|
| Msg1 | 0x700 | `B2 00 00 00 00 00 00 00` | 500 ms |
|
||||||
| Msg2 | 0x300 | `01 48 50 C3 00 00 00 00` | 50 ms |
|
| Msg2 | 0x300 | `01 48 50 C3 00 00 00 00` | 50 ms |
|
||||||
| TestUnlock states | 0x700 | `B2`, `B6`, `23`, `24` (byte[0]) x2 | 500 ms each |
|
| TestUnlock states | 0x700 | `B2`, `B6`, `23`, `24` (byte[0]) x2 | 500 ms each |
|
||||||
| Verify | TestUnlock param | Success when value != 0 | One-shot |
|
| Verify | TestUnlock param | Success when value == 0 | One-shot |
|
||||||
|
|
||||||
### Type 2 (CAN IDs 0x700 + 0x500)
|
### Type 2 (CAN IDs 0x700 + 0x500)
|
||||||
| Phase | ID | Data | Interval |
|
| Phase | ID | Data | Interval |
|
||||||
@@ -64,11 +51,21 @@ The `UnlockService` backend is fully functional (Phase 1 + Phase 2 + verificatio
|
|||||||
| TestUnlock states | 0x700 | `B2`, `24`, `24`, `24` (byte[3]) x2 | 500 ms each |
|
| TestUnlock states | 0x700 | `B2`, `24`, `24`, `24` (byte[3]) x2 | 500 ms each |
|
||||||
| Verify | TestUnlock param | Success when value == 0xE4 | One-shot |
|
| Verify | TestUnlock param | Success when value == 0xE4 | One-shot |
|
||||||
|
|
||||||
|
### K-Line fast unlock (timer shortcut)
|
||||||
|
| Command | `02 88 02 03 A8 01 00` |
|
||||||
|
|---------|------------------------|
|
||||||
|
| Effect | Writes pump RAM to fast-forward the internal 10 min timer |
|
||||||
|
| Prerequisite | Active K-Line session + CAN flood senders already running |
|
||||||
|
| ACK meaning | Command accepted (NOT unlock confirmed — must still verify via CAN TestUnlock) |
|
||||||
|
|
||||||
### Duration
|
### Duration
|
||||||
Phase 1: 600,500 ms (10 min 0.5 sec). Phase 2: ~4 sec (8 messages x 500ms). Total: ~604.5 sec.
|
- Phase 1 (normal): 600,500 ms (10 min 0.5 sec)
|
||||||
|
- Phase 1 (fast unlock): ~2 sec after K-Line ACK (+ K-Line read time)
|
||||||
|
- Phase 2: ~4 sec (8 messages x 500 ms)
|
||||||
|
- CAN senders: persist until pump deselection
|
||||||
|
|
||||||
## Known Issue in Unlock Verification
|
### Critical: CAN sender lifecycle
|
||||||
The **Type 1 verification logic may be inverted** compared to the old code. Old: `Lock = (valor != 0)` meant non-zero = LOCKED. New: `Value != 0` returned as SUCCESS (unlocked). Needs hardware testing to confirm which is correct.
|
CAN flood messages must be active **before** the fast unlock attempt (otherwise the timer resets instantly) and must **continue running after** unlock completes. Stopping them causes the pump to re-lock. `StopSenders()` is only called when the pump is deselected.
|
||||||
|
|
||||||
## Missing Feature: TestImmo Check
|
## Remaining gap: TestImmo check
|
||||||
Old code tracked both `TestUnlock` and `TestImmo` CAN parameters and displayed combined status. New code only checks `TestUnlock`, ignoring `TestImmo` entirely. Consider adding the immobilizer state check for completeness.
|
Old code tracked both `TestUnlock` and `TestImmo` CAN parameters and displayed combined status ("Inmovilizada/Liberada | Bloqueada/Desbloqueada"). New code only checks `TestUnlock`. Consider adding immobilizer state display for completeness.
|
||||||
|
|||||||
Reference in New Issue
Block a user