Files
HC_APTBS/ViewModels/AngleDisplayViewModel.cs
LucianoDev d34e81163a 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 <noreply@anthropic.com>
2026-04-11 15:16:15 +02:00

272 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// ViewModel for the advance monitoring display showing PSG, Injector, and Manual
/// encoder angles with zero-reference tracking and lock angle calculation.
/// </summary>
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) ─────────────────────────────────
/// <summary>Formatted relative PSG angle or "- -" when sensor offline.</summary>
[ObservableProperty] private string _psgRelativeAngle = NaN;
/// <summary>Formatted raw PSG encoder degrees or "- -" when sensor offline.</summary>
[ObservableProperty] private string _psgEncoderAngle = NaN;
/// <summary>Foreground brush for PSG angle text (White = OK, Red = offline).</summary>
[ObservableProperty] private Brush _psgAngleForeground = Brushes.White;
/// <summary>Formatted relative INJ angle or "- -" when sensor offline.</summary>
[ObservableProperty] private string _injRelativeAngle = NaN;
/// <summary>Formatted raw INJ encoder degrees or "- -" when sensor offline.</summary>
[ObservableProperty] private string _injEncoderAngle = NaN;
/// <summary>Foreground brush for INJ angle text (White = OK, Red = offline).</summary>
[ObservableProperty] private Brush _injAngleForeground = Brushes.White;
/// <summary>Manual encoder angle in degrees, or "-" when RPM >= 30.</summary>
[ObservableProperty] private string _manualAngleText = "-";
/// <summary>Delta offset input for lock angle calculation (two-way bound to TextBox).</summary>
[ObservableProperty] private string _lockAngleDeltaInput = "0";
/// <summary>Computed lock angle result display.</summary>
[ObservableProperty] private string _lockAngleDisplay = "00.0";
/// <summary>Foreground brush for the lock angle display (Green/Red/White).</summary>
[ObservableProperty] private Brush _lockAngleForeground = Brushes.White;
// ── Constructor ───────────────────────────────────────────────────────────
/// <summary>
/// Creates the angle display ViewModel, reading encoder resolution from configuration.
/// </summary>
public AngleDisplayViewModel(IConfigurationService configService)
{
_encoderResolution = configService.Settings.EncoderResolution;
}
// ── Public update (called from MainViewModel.OnRefreshTick) ───────────────
/// <summary>
/// Feeds all encoder channel values and recalculates display properties.
/// Must be called on the UI thread.
/// </summary>
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 ──────────────────────────────────────────────────────────────
/// <summary>Sets the current PSG encoder value as the zero reference.</summary>
[RelayCommand]
private void SetPsgZero()
{
_zeroPsgEncoder = _psgRaw;
RecalculatePsg();
}
/// <summary>Sets the current INJ encoder degrees as the zero reference.</summary>
[RelayCommand]
private void SetInjZero()
{
_zeroInjDegrees = _injEncoderDegrees;
RecalculateInj();
}
// ── Public methods for test phase integration ─────────────────────────────
/// <summary>
/// Sets the lock angle delta from the pump definition and returns the computed
/// lock angle result. Called when the LockAngleFaseReady event fires.
/// </summary>
/// <param name="pumpLockAngle">Angle offset from <see cref="Models.PumpDefinition.LockAngle"/>.</param>
/// <returns>Computed lock angle in degrees (0360).</returns>
public double SetLockAngle(double pumpLockAngle)
{
LockAngleDeltaInput = pumpLockAngle.ToString(CultureInfo.InvariantCulture);
return _lockAngleValue;
}
/// <summary>
/// Zeros the PSG encoder from a test phase (PsgModeFaseReady event).
/// </summary>
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 0360, 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);
}
}