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; [ObservableProperty] [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))] [NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))] private double _currentManualDegrees; private double _lockAngleValue; [ObservableProperty] [NotifyPropertyChangedFor(nameof(TargetAngleForGauge))] 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; // ── Numeric doubles for radial advance-monitor control ──────────────────── /// PSG relative angle as a double for gauge binding. [ObservableProperty] [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))] private double _psgRelativeDegrees; /// INJ encoder absolute degrees (0–360) as a double. [ObservableProperty] private double _injEncoderDegreesValue; /// Lock angle target (0–360 degrees) as a double for gauge binding. [ObservableProperty] [NotifyPropertyChangedFor(nameof(TargetAngleForGauge))] private double _targetLockAngle; /// True when bench RPM ≥ 31, hysteresis-cleared below 29. [ObservableProperty] [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))] [NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))] private bool _isRunningMode; // ── Constructor ─────────────────────────────────────────────────────────── /// /// Creates the angle display ViewModel, reading encoder resolution from configuration. /// public AngleDisplayViewModel(IConfigurationService configService) { _encoderResolution = configService.Settings.EncoderResolution; } // ── Computed for radial gauge ───────────────────────────────────────────── /// Primary thumb angle: manual-wheel angle in hand-mode, PSG relative angle when running. public double PrimaryGaugeAngle => IsRunningMode ? PsgRelativeDegrees : CurrentManualDegrees; /// Target thumb angle for the radial gauge, or null when no lock has been set yet. public double? TargetAngleForGauge => IsLockSet ? TargetLockAngle : (double?)null; /// Ghost secondary thumb angle: manual-wheel position shown when running, null in hand-mode (primary already shows it). public double? SecondaryGaugeAngle => IsRunningMode ? CurrentManualDegrees : (double?)null; // ── 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; if (!IsRunningMode && rpm >= 31.0) IsRunningMode = true; else if (IsRunningMode && rpm < 29.0) IsRunningMode = false; 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); TargetLockAngle = _lockAngleValue; 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; PsgRelativeDegrees = relDeg; 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); InjEncoderDegreesValue = _injEncoderDegrees; // 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); TargetLockAngle = _lockAngleValue; 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); } }