From d34e81163a1379e5ea34a9fea812b518eac7e483 Mon Sep 17 00:00:00 2001 From: LucianoDev Date: Sat, 11 Apr 2026 15:16:15 +0200 Subject: [PATCH] feat: add AngleDisplay MVVM UserControl for advance monitoring Refactors the old AngleDisplay code-behind into a clean MVVM implementation with AngleDisplayViewModel (PSG/INJ/Manual encoder angles, zero references, lock angle calculation with tolerance color coding) and a pure XAML view. Wires LockAngleFaseReady and PsgModeFaseReady test phase events. Co-Authored-By: Claude Opus 4.6 --- MainWindow.xaml | 5 + ViewModels/AngleDisplayViewModel.cs | 271 ++++++++++++++++++++ ViewModels/MainViewModel.cs | 23 ++ Views/UserControls/AngleDisplayView.xaml | 134 ++++++++++ Views/UserControls/AngleDisplayView.xaml.cs | 16 ++ 5 files changed, 449 insertions(+) create mode 100644 ViewModels/AngleDisplayViewModel.cs create mode 100644 Views/UserControls/AngleDisplayView.xaml create mode 100644 Views/UserControls/AngleDisplayView.xaml.cs diff --git a/MainWindow.xaml b/MainWindow.xaml index dceda2d..5887615 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -112,6 +112,7 @@ + @@ -430,6 +431,10 @@ + + + diff --git a/ViewModels/AngleDisplayViewModel.cs b/ViewModels/AngleDisplayViewModel.cs new file mode 100644 index 0000000..8d10eaa --- /dev/null +++ b/ViewModels/AngleDisplayViewModel.cs @@ -0,0 +1,271 @@ +using System; +using System.Globalization; +using System.Windows.Media; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using HC_APTBS.Services; + +namespace HC_APTBS.ViewModels +{ + /// + /// ViewModel for the advance monitoring display showing PSG, Injector, and Manual + /// encoder angles with zero-reference tracking and lock angle calculation. + /// + public sealed partial class AngleDisplayViewModel : ObservableObject + { + private const string NaN = "- -"; + private const double LockAngleTolerance = 0.1; + + private readonly int _encoderResolution; + + // ── Internal state ──────────────────────────────────────────────────────── + + private double _psgRaw; + private bool _psgWorking; + private double _injRaw; + private bool _injWorking; + private double _manualRaw; + private double _currentRpm; + private bool _isDirectionRight; + + private double _zeroPsgEncoder; + private double _zeroInjDegrees; + private double _injEncoderDegrees; + private double _currentManualDegrees; + private double _lockAngleValue; + private bool _isLockSet; + private bool _isManualVisible; + + // ── Observable properties (bound by View) ───────────────────────────────── + + /// Formatted relative PSG angle or "- -" when sensor offline. + [ObservableProperty] private string _psgRelativeAngle = NaN; + + /// Formatted raw PSG encoder degrees or "- -" when sensor offline. + [ObservableProperty] private string _psgEncoderAngle = NaN; + + /// Foreground brush for PSG angle text (White = OK, Red = offline). + [ObservableProperty] private Brush _psgAngleForeground = Brushes.White; + + /// Formatted relative INJ angle or "- -" when sensor offline. + [ObservableProperty] private string _injRelativeAngle = NaN; + + /// Formatted raw INJ encoder degrees or "- -" when sensor offline. + [ObservableProperty] private string _injEncoderAngle = NaN; + + /// Foreground brush for INJ angle text (White = OK, Red = offline). + [ObservableProperty] private Brush _injAngleForeground = Brushes.White; + + /// Manual encoder angle in degrees, or "-" when RPM >= 30. + [ObservableProperty] private string _manualAngleText = "-"; + + /// Delta offset input for lock angle calculation (two-way bound to TextBox). + [ObservableProperty] private string _lockAngleDeltaInput = "0"; + + /// Computed lock angle result display. + [ObservableProperty] private string _lockAngleDisplay = "00.0"; + + /// Foreground brush for the lock angle display (Green/Red/White). + [ObservableProperty] private Brush _lockAngleForeground = Brushes.White; + + // ── Constructor ─────────────────────────────────────────────────────────── + + /// + /// Creates the angle display ViewModel, reading encoder resolution from configuration. + /// + public AngleDisplayViewModel(IConfigurationService configService) + { + _encoderResolution = configService.Settings.EncoderResolution; + } + + // ── Public update (called from MainViewModel.OnRefreshTick) ─────────────── + + /// + /// Feeds all encoder channel values and recalculates display properties. + /// Must be called on the UI thread. + /// + public void Update( + double psgRaw, bool psgWorking, + double injRaw, bool injWorking, + double manualRaw, double rpm, + bool isDirectionRight) + { + _psgRaw = psgRaw; + _psgWorking = psgWorking; + _injRaw = injRaw; + _injWorking = injWorking; + _manualRaw = manualRaw; + _currentRpm = rpm; + _isDirectionRight = isDirectionRight; + + RecalculatePsg(); + RecalculateInj(); + RecalculateManual(); + } + + // ── Commands ────────────────────────────────────────────────────────────── + + /// Sets the current PSG encoder value as the zero reference. + [RelayCommand] + private void SetPsgZero() + { + _zeroPsgEncoder = _psgRaw; + RecalculatePsg(); + } + + /// Sets the current INJ encoder degrees as the zero reference. + [RelayCommand] + private void SetInjZero() + { + _zeroInjDegrees = _injEncoderDegrees; + RecalculateInj(); + } + + // ── Public methods for test phase integration ───────────────────────────── + + /// + /// Sets the lock angle delta from the pump definition and returns the computed + /// lock angle result. Called when the LockAngleFaseReady event fires. + /// + /// Angle offset from . + /// Computed lock angle in degrees (0–360). + public double SetLockAngle(double pumpLockAngle) + { + LockAngleDeltaInput = pumpLockAngle.ToString(CultureInfo.InvariantCulture); + return _lockAngleValue; + } + + /// + /// Zeros the PSG encoder from a test phase (PsgModeFaseReady event). + /// + public void SetPsgZeroFromTest() + { + SetPsgZero(); + } + + // ── Lock angle delta change handler ─────────────────────────────────────── + + partial void OnLockAngleDeltaInputChanged(string value) + { + if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double delta)) + { + _lockAngleValue = CalculateLockAngle(delta); + _isLockSet = true; + LockAngleDisplay = FormatAngle(_lockAngleValue); + } + + RecalculateLockColor(); + } + + // ── Private recalculation methods ───────────────────────────────────────── + + private void RecalculatePsg() + { + if (!_psgWorking) + { + PsgRelativeAngle = NaN; + PsgEncoderAngle = NaN; + PsgAngleForeground = Brushes.Red; + return; + } + + PsgAngleForeground = Brushes.White; + + double sign = DirectionSign; + + // Raw encoder → degrees, normalized to ±180. + double rawDeg = _psgRaw * (360.0 / _encoderResolution); + if (rawDeg > 180) rawDeg = 360 - rawDeg; + rawDeg *= sign; + + // Relative angle from zero reference. + double relDeg = (_psgRaw - _zeroPsgEncoder) * (360.0 / _encoderResolution); + if (relDeg > 180) relDeg = 360 - relDeg; + relDeg *= sign; + + PsgEncoderAngle = FormatAngle(rawDeg); + PsgRelativeAngle = FormatAngle(relDeg); + } + + private void RecalculateInj() + { + if (!_injWorking) + { + InjRelativeAngle = NaN; + InjEncoderAngle = NaN; + InjAngleForeground = Brushes.Red; + return; + } + + InjAngleForeground = Brushes.White; + + double sign = DirectionSign; + + // INJ encoder → degrees (full 0–360, NO 180 normalization). + _injEncoderDegrees = _injRaw * (360.0 / _encoderResolution); + + // Relative angle from zero reference. + double relDeg = (_injEncoderDegrees - _zeroInjDegrees) * sign; + + InjEncoderAngle = FormatAngle(_injEncoderDegrees); + InjRelativeAngle = FormatAngle(relDeg); + + // Recompute lock angle if already set (INJ encoder changed). + if (_isLockSet && + double.TryParse(LockAngleDeltaInput, NumberStyles.Float, + CultureInfo.InvariantCulture, out double delta)) + { + _lockAngleValue = CalculateLockAngle(delta); + LockAngleDisplay = FormatAngle(_lockAngleValue); + } + } + + private void RecalculateManual() + { + _currentManualDegrees = _manualRaw * (360.0 / _encoderResolution); + + if (_currentRpm < 30) + { + ManualAngleText = FormatAngle(_currentManualDegrees); + _isManualVisible = true; + } + else if (_isManualVisible) + { + ManualAngleText = "-"; + _isManualVisible = false; + } + + RecalculateLockColor(); + } + + private void RecalculateLockColor() + { + if (!_isManualVisible) + { + LockAngleForeground = Brushes.White; + return; + } + + if (_isLockSet && Math.Abs(_lockAngleValue - _currentManualDegrees) <= LockAngleTolerance) + LockAngleForeground = Brushes.Green; + else if (_isLockSet) + LockAngleForeground = Brushes.Red; + else + LockAngleForeground = Brushes.White; + } + + private double CalculateLockAngle(double angleToAdd) + { + double result = (_injEncoderDegrees + angleToAdd) % 360; + if (result < 0) result += 360; + return result; + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private double DirectionSign => _isDirectionRight ? -1.0 : 1.0; + + private static string FormatAngle(double degrees) + => degrees.ToString("F1", CultureInfo.InvariantCulture); + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 1166e45..5e2c962 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -66,6 +66,9 @@ namespace HC_APTBS.ViewModels /// ViewModel for the two flowmeter real-time charts (Q-Delivery, Q-Over). public FlowmeterChartViewModel FlowmeterChart { get; } = new(); + /// ViewModel for the encoder angle monitoring display (PSG, INJ, Manual, Lock Angle). + public AngleDisplayViewModel AngleDisplay { get; } + /// ViewModel for the first pump status display (Status word). public StatusDisplayViewModel StatusDisplay1 { get; } = new(); @@ -98,6 +101,7 @@ namespace HC_APTBS.ViewModels DfiViewModel = new DfiManageViewModel(kwpService, configService); PumpControl = new PumpControlViewModel(benchService); BenchControl = new BenchControlViewModel(benchService, configService); + AngleDisplay = new AngleDisplayViewModel(configService); // React to pump changes from the identification child VM. PumpIdentification.PumpChanged += OnPumpChanged; @@ -144,6 +148,15 @@ namespace HC_APTBS.ViewModels FlowmeterChart.SetTolerance(paramName, value, tolerance); }); + // Angle display: lock angle and PSG zero from test phases + _bench.LockAngleFaseReady += () => App.Current.Dispatcher.Invoke(() => + { + if (CurrentPump != null) + CurrentPump.LockAngleResult = AngleDisplay.SetLockAngle(CurrentPump.LockAngle); + }); + _bench.PsgModeFaseReady += () => App.Current.Dispatcher.Invoke( + () => AngleDisplay.SetPsgZeroFromTest()); + // Unlock service status → verbose display _unlock.StatusChanged += msg => App.Current.Dispatcher.Invoke( () => VerboseStatus = msg); @@ -481,6 +494,16 @@ namespace HC_APTBS.ViewModels Pressure = _bench.ReadBenchParameter(BenchParameterNames.Pressure); PsgEncoderValue = _bench.ReadBenchParameter(BenchParameterNames.PsgEncoderValue); + // Feed the angle display with all three encoder channels + status. + AngleDisplay.Update( + PsgEncoderValue, + _bench.ReadBenchParameter(BenchParameterNames.PsgEncoderWorking) == 1, + _bench.ReadBenchParameter(BenchParameterNames.InjEncoderValue), + _bench.ReadBenchParameter(BenchParameterNames.InjEncoderWorking) == 1, + _bench.ReadBenchParameter(BenchParameterNames.ManualEncoderValue), + BenchRpm, + BenchControl.IsDirectionRight); + // Feed flowmeter charts and refresh bench controls. FlowmeterChart.AddSamples(QDelivery, QOver); BenchControl.RefreshFromTick(); diff --git a/Views/UserControls/AngleDisplayView.xaml b/Views/UserControls/AngleDisplayView.xaml new file mode 100644 index 0000000..4aa8bf1 --- /dev/null +++ b/Views/UserControls/AngleDisplayView.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + +