using System; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HC_APTBS.Models; using HC_APTBS.Services; using HC_APTBS.ViewModels.Dialogs; using HC_APTBS.Views.Dialogs; namespace HC_APTBS.ViewModels { /// /// ViewModel for manual bench controls: direction toggle, RPM start/stop with /// PID ramp, oil pump toggle, and turn downcounter. /// Created by as a child ViewModel. /// public sealed partial class BenchControlViewModel : ObservableObject { private readonly IBenchService _bench; private readonly IConfigurationService _config; // ── Direction ───────────────────────────────────────────────────────────── /// True when the bench rotates clockwise (right). False for counter-clockwise (left). [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsDirectionLeft))] private bool _isDirectionRight = true; /// True when the bench rotates counter-clockwise (left). Computed inverse of . public bool IsDirectionLeft => !IsDirectionRight; // ── Oil pump ────────────────────────────────────────────────────────────── /// True when the oil pump relay is energised. [ObservableProperty] private bool _isOilPumpOn; // ── RPM control ─────────────────────────────────────────────────────────── /// True when the bench motor is running (last RPM command > 0). [ObservableProperty] private bool _isBenchRunning; /// Controls the RPM quick-select popup visibility. [ObservableProperty] private bool _isRpmPopupOpen; /// Text in the RPM input field. [ObservableProperty] private string _rpmInputText = string.Empty; /// Last RPM setpoint commanded via PID or direct set. [ObservableProperty] private double _targetRpm; /// Last voltage sent to the motor CAN parameter. [ObservableProperty] private double _commandVoltage; // ── Counter ─────────────────────────────────────────────────────────────── /// Controls the counter popup visibility. [ObservableProperty] private bool _isCounterPopupOpen; /// Text in the counter input field. [ObservableProperty] private string _counterInputText = string.Empty; /// Live counter count-down value read from CAN. [ObservableProperty] private double _benchCounterValue; // ── Constructor ─────────────────────────────────────────────────────────── /// /// Creates the bench control ViewModel and subscribes to service events. /// /// Bench service for RPM, relay, and parameter operations. /// Configuration service for bench parameters. public BenchControlViewModel(IBenchService benchService, IConfigurationService configService) { _bench = benchService; _config = configService; _bench.RpmCommandSent += () => Application.Current.Dispatcher.Invoke(() => { TargetRpm = _bench.LastTargetRpm; CommandVoltage = _bench.LastCommandVoltage; }); } // ── Direction toggle ────────────────────────────────────────────────────── partial void OnIsDirectionRightChanged(bool value) { _bench.SetRelay(RelayNames.DirectionRight, value); _bench.SetRelay(RelayNames.DirectionLeft, !value); } /// Sets the bench rotation direction to clockwise (right). [RelayCommand] private void SetDirectionRight() => IsDirectionRight = true; /// Sets the bench rotation direction to counter-clockwise (left). [RelayCommand] private void SetDirectionLeft() => IsDirectionRight = false; // ── Oil pump toggle ─────────────────────────────────────────────────────── 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); // The dialog's ShowDialog call runs a nested dispatcher message pump. // While it was blocking, RefreshFromTick may have fired and written // _isOilPumpOn back to the stale relay.State value (false, because // SetRelay above hadn't run yet). Re-assert the backing field now // that relay.State is committed so IsOilPumpOn reports the correct // value to callers downstream (e.g. TestsPageViewModel.StartTestAsync // which guards on it right after this setter returns). // See docs/gotcha-oil-pump-dialog-race.md. if (_isOilPumpOn != value) { _isOilPumpOn = value; OnPropertyChanged(nameof(IsOilPumpOn)); } } /// /// Energises the oil-pump relay and flags without /// presenting the leak-check confirmation dialog. Used by the Dashboard /// "Connect & Auto Test" flow when the operator has opted in via /// . Writes the backing /// field directly to avoid re-entering . /// public void TurnOilPumpOnSilent() { if (_isOilPumpOn) return; _bench.SetRelay(RelayNames.OilPump, true); _isOilPumpOn = true; OnPropertyChanged(nameof(IsOilPumpOn)); } // ── RPM commands ────────────────────────────────────────────────────────── /// Opens the RPM quick-select popup. [RelayCommand] private void OpenRpmPopup() { IsRpmPopupOpen = true; } /// /// Starts the bench motor at the RPM specified in . /// Shows a safety warning dialog if the oil pump is off. /// [RelayCommand] private void StartBench() { if (!int.TryParse(RpmInputText, out int rpm) || rpm <= 0) return; // Safety warning if oil pump is not running (WCareOnRpmOn equivalent). if (!IsOilPumpOn) { var vm = new RpmSafetyWarningViewModel(); var dlg = new RpmSafetyWarningDialog(vm) { Owner = Application.Current.MainWindow }; dlg.ShowDialog(); switch (vm.Result) { case RpmSafetyResult.Cancel: return; case RpmSafetyResult.ProceedWithOil: IsOilPumpOn = true; break; case RpmSafetyResult.ProceedWithoutOil: // Operator accepted the risk. break; } } // Ensure direction relays are set. _bench.SetRelay(RelayNames.DirectionRight, IsDirectionRight); _bench.SetRelay(RelayNames.DirectionLeft, !IsDirectionRight); _bench.StartRpmPid(rpm); IsBenchRunning = true; IsRpmPopupOpen = false; } /// /// Stops the bench motor via PID stop and clears direction relays. /// [RelayCommand] private void StopBench() { _bench.StopRpmPid(); _bench.SetRelay(RelayNames.DirectionLeft, false); _bench.SetRelay(RelayNames.DirectionRight, false); IsBenchRunning = false; } /// /// Applies the RPM value from and starts the bench. /// Bound to the inline Apply button in BenchRpmCommandCard. /// [RelayCommand] private void ApplyRpm() => StartBench(); /// /// Quick-select button handler: sets the RPM input and starts the bench. /// /// RPM value as a string from the button content. [RelayCommand] private void SetQuickRpm(string rpmString) { RpmInputText = rpmString; StartBench(); } // ── Counter commands ────────────────────────────────────────────────────── /// Toggles the counter popup visibility. [RelayCommand] private void ToggleCounterPopup() { IsCounterPopupOpen = !IsCounterPopupOpen; } /// /// Sends the counter value over CAN and activates the counter relay. /// [RelayCommand] private void SendCounter() { if (!int.TryParse(CounterInputText, out int count) || count <= 0) return; // Set the counter parameter value and transmit. _bench.SetParameter(BenchParameterNames.Counter, count); if (_config.Bench.ParametersByName.TryGetValue( BenchParameterNames.Counter, out var counterParam)) { _bench.SendParameters(counterParam.MessageId); } // Activate the counter relay. _bench.SetRelay(RelayNames.Counter, true); } // ── Refresh (called from MainViewModel timer tick) ──────────────────────── /// /// Updates live counter readback from CAN, and mirrors the oil-pump relay /// state so this VM's stays in sync even when the /// relay is toggled outside the manual Bench page (e.g. the Dashboard /// auto-test orchestrator). Writes through the backing field to avoid /// re-triggering the confirmation dialog in . /// Called on the UI thread from . /// public void RefreshFromTick() { BenchCounterValue = _bench.ReadBenchParameter(BenchParameterNames.BenchCounter); bool relayOn = _config.Bench.Relays.TryGetValue(RelayNames.OilPump, out var oilRelay) && oilRelay.State; if (_isOilPumpOn != relayOn) { _isOilPumpOn = relayOn; OnPropertyChanged(nameof(IsOilPumpOn)); } } } }