diff --git a/ConeCalorimeter/ViewModels/TestPageViewModel.cs b/ConeCalorimeter/ViewModels/TestPageViewModel.cs index be9761a..a3ae483 100644 --- a/ConeCalorimeter/ViewModels/TestPageViewModel.cs +++ b/ConeCalorimeter/ViewModels/TestPageViewModel.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using System.Diagnostics; -using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ConeCalorimeter.Models; @@ -15,31 +14,36 @@ namespace ConeCalorimeter.ViewModels; public sealed class TestPageViewModel : PageViewModel { private const string ResetAction = "复位"; + private const string TestAction = "测试控制"; + private const string TestStartDisplayText = "测试开始"; + private const string TestStopDisplayText = "测试结束"; private const string FanAction = "风机"; private const string IgniterAction = "点火器"; private const ushort ResetCoil = 88; - private const ushort ResetCompleteCoil = 89; + private const ushort TestStartCoil = 65; + private const ushort TestStopCoil = 67; private const ushort FanCoil = 54; private const ushort IgniterCoil = 53; private const double MinimumHeatReleaseAxisMaximum = 150; private const double MinimumTotalAxisMaximum = 150; private const double PlotAxisPaddingFactor = 1.1; private const int MaximumPlotPoints = 600; - private static readonly TimeSpan ResetPulseDuration = TimeSpan.FromMilliseconds(300); - private static readonly TimeSpan ResetCompletionTimeout = TimeSpan.FromSeconds(15); + private const int ControlPulseReleaseRetryCount = 3; + private static readonly TimeSpan ControlPulseDuration = TimeSpan.FromMilliseconds(300); + private static readonly TimeSpan ControlPulseReleaseRetryDelay = TimeSpan.FromMilliseconds(100); private readonly IExperimentDataService _experimentDataService; private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; private readonly LineSeries _heatReleaseSeries; private readonly LineSeries _totalHeatSeries; private readonly LineSeries _totalSmokeSeries; - private readonly DispatcherTimer _resetStatusTimer; private readonly Dictionary _deviceActionsByLabel = []; + private readonly HashSet _pulseActionsInProgress = []; + private DeviceActionViewModel _testAction = null!; private DeviceActionViewModel _resetAction = null!; private bool _flameDetected; private bool _resetInProgress; private int? _lastPlottedSeconds; - private DateTime _resetStartedAt; private string _lastAction = "待机"; public TestPageViewModel( @@ -83,6 +87,8 @@ public sealed class TestPageViewModel : PageViewModel ]; ExecuteDeviceActionCommand = new RelayCommand(ExecuteDeviceAction); + _testAction = new DeviceActionViewModel(TestAction, ExecuteDeviceActionCommand); + _testAction.SetDisplayText(TestStartDisplayText); _resetAction = new DeviceActionViewModel(ResetAction, ExecuteDeviceActionCommand); DeviceActions = [ @@ -90,8 +96,7 @@ public sealed class TestPageViewModel : PageViewModel new DeviceActionViewModel("辐射锥降", ExecuteDeviceActionCommand), new DeviceActionViewModel("称重台升", ExecuteDeviceActionCommand), new DeviceActionViewModel("称重台降", ExecuteDeviceActionCommand), - new DeviceActionViewModel("测试开始", ExecuteDeviceActionCommand), - new DeviceActionViewModel("测试结束", ExecuteDeviceActionCommand), + _testAction, new DeviceActionViewModel(FanAction, ExecuteDeviceActionCommand), new DeviceActionViewModel(IgniterAction, ExecuteDeviceActionCommand), _resetAction @@ -101,21 +106,10 @@ public sealed class TestPageViewModel : PageViewModel _deviceActionsByLabel[action.Label] = action; } + RefreshToggleActionStates(); HeatReleasePlot = CreatePlotModel(out _heatReleaseSeries, out _totalHeatSeries, out _totalSmokeSeries); UpdateSnapshot(_experimentDataService.CurrentSnapshot); _experimentDataService.SnapshotUpdated += (_, snapshot) => UpdateSnapshot(snapshot); - - _resetStatusTimer = new DispatcherTimer - { - Interval = TimeSpan.FromSeconds(1) - }; - _resetStatusTimer.Tick += (_, _) => - { - RefreshResetStatus(); - RefreshToggleActionStates(); - }; - RefreshToggleActionStates(); - _resetStatusTimer.Start(); } public ObservableCollection TopMetrics { get; } @@ -329,34 +323,6 @@ public sealed class TestPageViewModel : PageViewModel return Math.Max(minimumMaximum, Math.Ceiling(maximum * PlotAxisPaddingFactor)); } - public bool StartMomentaryDeviceAction(string? action) - { - return TryWriteMomentaryDeviceAction(action, true); - } - - public bool StopMomentaryDeviceAction(string? action) - { - return TryWriteMomentaryDeviceAction(action, false); - } - - private bool TryWriteMomentaryDeviceAction(string? action, bool isRunning) - { - if (string.IsNullOrWhiteSpace(action) - || !TryGetMomentaryDeviceActionCoil(action, out var coilAddress)) - { - return false; - } - - LastAction = isRunning ? action : $"停止:{action}"; - - if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, isRunning)) - { - Debug.WriteLine($"Momentary device action '{action}' write failed."); - } - - return true; - } - private void ExecuteDeviceAction(string? action) { if (string.IsNullOrWhiteSpace(action)) @@ -364,8 +330,9 @@ public sealed class TestPageViewModel : PageViewModel return; } - if (IsMomentaryDeviceAction(action)) + if (action == TestAction) { + ExecuteTestControlAction(); return; } @@ -376,33 +343,19 @@ public sealed class TestPageViewModel : PageViewModel return; } - if (IsToggleDeviceAction(action)) + if (TryGetToggleDeviceActionCoil(action, out var toggleCoilAddress)) { - ToggleDeviceAction(action); + ToggleDeviceAction(action, toggleCoilAddress); + return; + } + + if (TryGetPulseDeviceActionCoil(action, out var coilAddress)) + { + ExecutePulseDeviceAction(action, coilAddress); return; } LastAction = action; - - if (action == "测试开始") - { - _experimentDataService.StartTest(); - ClearPlotSeries(); - } - else if (action == "测试结束") - { - _experimentDataService.StopTest(); - } - - if (!TryGetDeviceActionCoil(action, out var coilAddress, out var value)) - { - return; - } - - if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, value)) - { - Debug.WriteLine($"Device action '{action}' write failed."); - } } private async void ExecuteResetAction() @@ -421,37 +374,18 @@ public sealed class TestPageViewModel : PageViewModel SetResetInProgress(true); LastAction = "复位中"; - _resetStartedAt = DateTime.Now; - await Task.Delay(ResetPulseDuration); - if (!_tcpDeviceConnectionService.TryWriteCoil(ResetCoil, false)) + await Task.Delay(ControlPulseDuration); + if (await ReleaseControlPulseAsync(ResetCoil, ResetAction)) { - Debug.WriteLine("Device reset pulse release failed."); - } - } - - private void RefreshResetStatus() - { - if (!_resetInProgress) - { - return; - } - - var statusRead = _tcpDeviceConnectionService.TryReadCoil(ResetCompleteCoil, out var resetComplete); - if (statusRead && resetComplete) - { - SetResetInProgress(false); LastAction = "复位完成"; - return; } - - if (DateTime.Now - _resetStartedAt < ResetCompletionTimeout) + else { - return; + LastAction = "复位线圈复位失败"; } SetResetInProgress(false); - LastAction = statusRead ? "复位超时" : "复位状态读取失败"; } private void SetResetInProgress(bool resetInProgress) @@ -474,6 +408,58 @@ public sealed class TestPageViewModel : PageViewModel HeatReleasePlot.InvalidatePlot(true); } + private async void ExecuteTestControlAction() + { + if (!TryBeginPulseAction(TestAction)) + { + return; + } + + var isStarting = !_testAction.IsActive; + var actionText = isStarting ? TestStartDisplayText : TestStopDisplayText; + var coilAddress = isStarting ? TestStartCoil : TestStopCoil; + + if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true)) + { + LastAction = $"{actionText}失败"; + Debug.WriteLine($"Device action '{actionText}' write failed."); + EndPulseAction(TestAction); + return; + } + + try + { + if (isStarting) + { + _experimentDataService.StartTest(); + ClearPlotSeries(); + } + else + { + _experimentDataService.StopTest(); + } + + SetTestRunning(isStarting); + LastAction = actionText; + + await Task.Delay(ControlPulseDuration); + if (!await ReleaseControlPulseAsync(coilAddress, actionText)) + { + LastAction = $"{actionText}线圈复位失败"; + } + } + finally + { + EndPulseAction(TestAction); + } + } + + private void SetTestRunning(bool isRunning) + { + _testAction.UpdateStatus(isRunning); + _testAction.SetDisplayText(isRunning ? TestStopDisplayText : TestStartDisplayText); + } + private void RefreshToggleActionStates() { UpdateToggleActionStatus(FanAction, FanCoil); @@ -497,17 +483,13 @@ public sealed class TestPageViewModel : PageViewModel return false; } - private void ToggleDeviceAction(string action) + private void ToggleDeviceAction(string action, ushort coilAddress) { - if (!TryGetToggleDeviceActionCoil(action, out var coilAddress)) - { - return; - } - if (!_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive)) { LastAction = $"{action}状态读取失败"; Debug.WriteLine($"Device action '{action}' state read failed."); + UpdateToggleActionStatus(action, coilAddress); return; } @@ -528,27 +510,71 @@ public sealed class TestPageViewModel : PageViewModel Debug.WriteLine($"Device action '{action}' write failed."); } - private static bool TryGetDeviceActionCoil(string action, out ushort coilAddress, out bool value) + private async void ExecutePulseDeviceAction(string action, ushort coilAddress) { - value = true; - - switch (action) + if (!TryBeginPulseAction(action)) { - case "测试开始": - coilAddress = 65; - return true; - case "测试结束": - coilAddress = 67; - return true; - default: - coilAddress = 0; - return false; + return; + } + + _deviceActionsByLabel.TryGetValue(action, out var actionViewModel); + + if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true)) + { + LastAction = $"{action}失败"; + Debug.WriteLine($"Device action '{action}' write failed."); + EndPulseAction(action); + return; + } + + try + { + actionViewModel?.UpdateStatus(true); + LastAction = action; + + await Task.Delay(ControlPulseDuration); + if (await ReleaseControlPulseAsync(coilAddress, action)) + { + actionViewModel?.UpdateStatus(false); + return; + } + + actionViewModel?.UpdateStatus(true, false); + LastAction = $"{action}线圈复位失败"; + } + finally + { + EndPulseAction(action); } } - private static bool IsToggleDeviceAction(string action) + private bool TryBeginPulseAction(string action) { - return TryGetToggleDeviceActionCoil(action, out _); + return _pulseActionsInProgress.Add(action); + } + + private void EndPulseAction(string action) + { + _pulseActionsInProgress.Remove(action); + } + + private async Task ReleaseControlPulseAsync(ushort coilAddress, string actionText) + { + for (var attempt = 1; attempt <= ControlPulseReleaseRetryCount; attempt++) + { + if (_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false)) + { + return true; + } + + if (attempt < ControlPulseReleaseRetryCount) + { + await Task.Delay(ControlPulseReleaseRetryDelay); + } + } + + Debug.WriteLine($"Device action '{actionText}' pulse release failed."); + return false; } private static bool TryGetToggleDeviceActionCoil(string action, out ushort coilAddress) @@ -567,12 +593,7 @@ public sealed class TestPageViewModel : PageViewModel } } - private static bool IsMomentaryDeviceAction(string action) - { - return TryGetMomentaryDeviceActionCoil(action, out _); - } - - private static bool TryGetMomentaryDeviceActionCoil(string action, out ushort coilAddress) + private static bool TryGetPulseDeviceActionCoil(string action, out ushort coilAddress) { switch (action) { diff --git a/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml b/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml index 233859c..9bd40ba 100644 --- a/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml +++ b/ConeCalorimeter/Views/SmokeDensitySettingsView.xaml @@ -95,7 +95,7 @@ FontWeight="SemiBold" Foreground="#1B2826" /> - + - - - + + +