更新0528

This commit is contained in:
GukSang.Jin
2026-05-28 11:46:06 +08:00
parent 73023e2b44
commit 047f0f1029
4 changed files with 150 additions and 286 deletions

View File

@@ -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<string, DeviceActionViewModel> _deviceActionsByLabel = [];
private readonly HashSet<string> _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<string>(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<MetricDisplayViewModel> 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<bool> 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)
{

View File

@@ -95,7 +95,7 @@
FontWeight="SemiBold"
Foreground="#1B2826" />
<TextBlock Margin="0,22,0,0"
Text="1. 0%为不光校准;"
Text="1. 0%为不光校准;"
FontSize="22"
Foreground="#111A18" />
<TextBlock Margin="0,14,0,0"

View File

@@ -262,29 +262,20 @@
<ItemsControl ItemsSource="{Binding DeviceActions}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" Rows="3" />
<UniformGrid Columns="2" Rows="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding DisplayText}"
Command="{Binding Command}"
CommandParameter="{Binding Label}"
Margin="5,3"
MinHeight="40"
PreviewMouseLeftButtonDown="DeviceActionButton_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="DeviceActionButton_PreviewMouseLeftButtonUp"
MouseLeave="DeviceActionButton_MouseLeave"
LostMouseCapture="DeviceActionButton_LostMouseCapture"
LostKeyboardFocus="DeviceActionButton_LostKeyboardFocus"
TouchDown="DeviceActionButton_TouchDown"
TouchUp="DeviceActionButton_TouchUp"
TouchLeave="DeviceActionButton_TouchLeave"
LostTouchCapture="DeviceActionButton_LostTouchCapture"
Style="{StaticResource StatusDeviceActionButtonStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
</Border>

View File

@@ -1,159 +1,11 @@
using System.Windows.Controls;
using System.Windows.Input;
using ConeCalorimeter.ViewModels;
namespace ConeCalorimeter.Views;
public partial class TestPageView : UserControl
{
private readonly HashSet<string> _runningMomentaryActions = [];
public TestPageView()
{
InitializeComponent();
}
private void DeviceActionButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (StartMomentaryAction(sender))
{
if (sender is Button button)
{
button.CaptureMouse();
}
e.Handled = true;
}
}
private void DeviceActionButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (StopMomentaryAction(sender))
{
ReleaseInputCapture(sender);
e.Handled = true;
}
}
private void DeviceActionButton_MouseLeave(object sender, MouseEventArgs e)
{
if (StopMomentaryAction(sender))
{
ReleaseInputCapture(sender);
}
}
private void DeviceActionButton_LostMouseCapture(object sender, MouseEventArgs e)
{
StopMomentaryAction(sender);
}
private void DeviceActionButton_LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (StopMomentaryAction(sender))
{
ReleaseInputCapture(sender);
}
}
private void DeviceActionButton_TouchDown(object sender, TouchEventArgs e)
{
if (StartMomentaryAction(sender))
{
if (sender is Button button)
{
button.CaptureTouch(e.TouchDevice);
}
e.Handled = true;
}
}
private void DeviceActionButton_TouchUp(object sender, TouchEventArgs e)
{
if (StopMomentaryAction(sender))
{
ReleaseInputCapture(sender);
e.Handled = true;
}
}
private void DeviceActionButton_TouchLeave(object sender, TouchEventArgs e)
{
if (StopMomentaryAction(sender))
{
ReleaseInputCapture(sender);
}
}
private void DeviceActionButton_LostTouchCapture(object sender, TouchEventArgs e)
{
StopMomentaryAction(sender);
}
private bool StartMomentaryAction(object sender)
{
if (!TryGetDeviceAction(sender, out var label, out var viewModel))
{
return false;
}
if (_runningMomentaryActions.Contains(label))
{
return true;
}
if (!viewModel.StartMomentaryDeviceAction(label))
{
return false;
}
_runningMomentaryActions.Add(label);
return true;
}
private bool StopMomentaryAction(object sender)
{
if (!TryGetDeviceAction(sender, out var label, out var viewModel)
|| !_runningMomentaryActions.Remove(label))
{
return false;
}
return viewModel.StopMomentaryDeviceAction(label);
}
private bool TryGetDeviceAction(
object sender,
out string label,
out TestPageViewModel viewModel)
{
label = string.Empty;
viewModel = null!;
if (sender is not Button { DataContext: DeviceActionViewModel action }
|| DataContext is not TestPageViewModel testPageViewModel)
{
return false;
}
label = action.Label;
viewModel = testPageViewModel;
return true;
}
private static void ReleaseInputCapture(object sender)
{
if (sender is not Button button)
{
return;
}
if (button.IsMouseCaptured)
{
button.ReleaseMouseCapture();
}
button.ReleaseAllTouchCaptures();
}
}