更新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.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Models; using ConeCalorimeter.Models;
@@ -15,31 +14,36 @@ namespace ConeCalorimeter.ViewModels;
public sealed class TestPageViewModel : PageViewModel public sealed class TestPageViewModel : PageViewModel
{ {
private const string ResetAction = "复位"; private const string ResetAction = "复位";
private const string TestAction = "测试控制";
private const string TestStartDisplayText = "测试开始";
private const string TestStopDisplayText = "测试结束";
private const string FanAction = "风机"; private const string FanAction = "风机";
private const string IgniterAction = "点火器"; private const string IgniterAction = "点火器";
private const ushort ResetCoil = 88; 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 FanCoil = 54;
private const ushort IgniterCoil = 53; private const ushort IgniterCoil = 53;
private const double MinimumHeatReleaseAxisMaximum = 150; private const double MinimumHeatReleaseAxisMaximum = 150;
private const double MinimumTotalAxisMaximum = 150; private const double MinimumTotalAxisMaximum = 150;
private const double PlotAxisPaddingFactor = 1.1; private const double PlotAxisPaddingFactor = 1.1;
private const int MaximumPlotPoints = 600; private const int MaximumPlotPoints = 600;
private static readonly TimeSpan ResetPulseDuration = TimeSpan.FromMilliseconds(300); private const int ControlPulseReleaseRetryCount = 3;
private static readonly TimeSpan ResetCompletionTimeout = TimeSpan.FromSeconds(15); private static readonly TimeSpan ControlPulseDuration = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ControlPulseReleaseRetryDelay = TimeSpan.FromMilliseconds(100);
private readonly IExperimentDataService _experimentDataService; private readonly IExperimentDataService _experimentDataService;
private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
private readonly LineSeries _heatReleaseSeries; private readonly LineSeries _heatReleaseSeries;
private readonly LineSeries _totalHeatSeries; private readonly LineSeries _totalHeatSeries;
private readonly LineSeries _totalSmokeSeries; private readonly LineSeries _totalSmokeSeries;
private readonly DispatcherTimer _resetStatusTimer;
private readonly Dictionary<string, DeviceActionViewModel> _deviceActionsByLabel = []; private readonly Dictionary<string, DeviceActionViewModel> _deviceActionsByLabel = [];
private readonly HashSet<string> _pulseActionsInProgress = [];
private DeviceActionViewModel _testAction = null!;
private DeviceActionViewModel _resetAction = null!; private DeviceActionViewModel _resetAction = null!;
private bool _flameDetected; private bool _flameDetected;
private bool _resetInProgress; private bool _resetInProgress;
private int? _lastPlottedSeconds; private int? _lastPlottedSeconds;
private DateTime _resetStartedAt;
private string _lastAction = "待机"; private string _lastAction = "待机";
public TestPageViewModel( public TestPageViewModel(
@@ -83,6 +87,8 @@ public sealed class TestPageViewModel : PageViewModel
]; ];
ExecuteDeviceActionCommand = new RelayCommand<string>(ExecuteDeviceAction); ExecuteDeviceActionCommand = new RelayCommand<string>(ExecuteDeviceAction);
_testAction = new DeviceActionViewModel(TestAction, ExecuteDeviceActionCommand);
_testAction.SetDisplayText(TestStartDisplayText);
_resetAction = new DeviceActionViewModel(ResetAction, ExecuteDeviceActionCommand); _resetAction = new DeviceActionViewModel(ResetAction, ExecuteDeviceActionCommand);
DeviceActions = 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), new DeviceActionViewModel("称重台降", ExecuteDeviceActionCommand),
new DeviceActionViewModel("测试开始", ExecuteDeviceActionCommand), _testAction,
new DeviceActionViewModel("测试结束", ExecuteDeviceActionCommand),
new DeviceActionViewModel(FanAction, ExecuteDeviceActionCommand), new DeviceActionViewModel(FanAction, ExecuteDeviceActionCommand),
new DeviceActionViewModel(IgniterAction, ExecuteDeviceActionCommand), new DeviceActionViewModel(IgniterAction, ExecuteDeviceActionCommand),
_resetAction _resetAction
@@ -101,21 +106,10 @@ public sealed class TestPageViewModel : PageViewModel
_deviceActionsByLabel[action.Label] = action; _deviceActionsByLabel[action.Label] = action;
} }
RefreshToggleActionStates();
HeatReleasePlot = CreatePlotModel(out _heatReleaseSeries, out _totalHeatSeries, out _totalSmokeSeries); HeatReleasePlot = CreatePlotModel(out _heatReleaseSeries, out _totalHeatSeries, out _totalSmokeSeries);
UpdateSnapshot(_experimentDataService.CurrentSnapshot); UpdateSnapshot(_experimentDataService.CurrentSnapshot);
_experimentDataService.SnapshotUpdated += (_, snapshot) => UpdateSnapshot(snapshot); _experimentDataService.SnapshotUpdated += (_, snapshot) => UpdateSnapshot(snapshot);
_resetStatusTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_resetStatusTimer.Tick += (_, _) =>
{
RefreshResetStatus();
RefreshToggleActionStates();
};
RefreshToggleActionStates();
_resetStatusTimer.Start();
} }
public ObservableCollection<MetricDisplayViewModel> TopMetrics { get; } public ObservableCollection<MetricDisplayViewModel> TopMetrics { get; }
@@ -329,34 +323,6 @@ public sealed class TestPageViewModel : PageViewModel
return Math.Max(minimumMaximum, Math.Ceiling(maximum * PlotAxisPaddingFactor)); 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) private void ExecuteDeviceAction(string? action)
{ {
if (string.IsNullOrWhiteSpace(action)) if (string.IsNullOrWhiteSpace(action))
@@ -364,8 +330,9 @@ public sealed class TestPageViewModel : PageViewModel
return; return;
} }
if (IsMomentaryDeviceAction(action)) if (action == TestAction)
{ {
ExecuteTestControlAction();
return; return;
} }
@@ -376,33 +343,19 @@ public sealed class TestPageViewModel : PageViewModel
return; 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; return;
} }
LastAction = action; 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() private async void ExecuteResetAction()
@@ -421,37 +374,18 @@ public sealed class TestPageViewModel : PageViewModel
SetResetInProgress(true); SetResetInProgress(true);
LastAction = "复位中"; LastAction = "复位中";
_resetStartedAt = DateTime.Now;
await Task.Delay(ResetPulseDuration); await Task.Delay(ControlPulseDuration);
if (!_tcpDeviceConnectionService.TryWriteCoil(ResetCoil, false)) 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 = "复位完成"; LastAction = "复位完成";
return;
} }
else
if (DateTime.Now - _resetStartedAt < ResetCompletionTimeout)
{ {
return; LastAction = "复位线圈复位失败";
} }
SetResetInProgress(false); SetResetInProgress(false);
LastAction = statusRead ? "复位超时" : "复位状态读取失败";
} }
private void SetResetInProgress(bool resetInProgress) private void SetResetInProgress(bool resetInProgress)
@@ -474,6 +408,58 @@ public sealed class TestPageViewModel : PageViewModel
HeatReleasePlot.InvalidatePlot(true); 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() private void RefreshToggleActionStates()
{ {
UpdateToggleActionStatus(FanAction, FanCoil); UpdateToggleActionStatus(FanAction, FanCoil);
@@ -497,17 +483,13 @@ public sealed class TestPageViewModel : PageViewModel
return false; 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)) if (!_tcpDeviceConnectionService.TryReadCoil(coilAddress, out var isActive))
{ {
LastAction = $"{action}状态读取失败"; LastAction = $"{action}状态读取失败";
Debug.WriteLine($"Device action '{action}' state read failed."); Debug.WriteLine($"Device action '{action}' state read failed.");
UpdateToggleActionStatus(action, coilAddress);
return; return;
} }
@@ -528,27 +510,71 @@ public sealed class TestPageViewModel : PageViewModel
Debug.WriteLine($"Device action '{action}' write failed."); 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; if (!TryBeginPulseAction(action))
switch (action)
{ {
case "测试开始": return;
coilAddress = 65; }
return true;
case "测试结束": _deviceActionsByLabel.TryGetValue(action, out var actionViewModel);
coilAddress = 67;
return true; if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
default: {
coilAddress = 0; LastAction = $"{action}失败";
return false; 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) private static bool TryGetToggleDeviceActionCoil(string action, out ushort coilAddress)
@@ -567,12 +593,7 @@ public sealed class TestPageViewModel : PageViewModel
} }
} }
private static bool IsMomentaryDeviceAction(string action) private static bool TryGetPulseDeviceActionCoil(string action, out ushort coilAddress)
{
return TryGetMomentaryDeviceActionCoil(action, out _);
}
private static bool TryGetMomentaryDeviceActionCoil(string action, out ushort coilAddress)
{ {
switch (action) switch (action)
{ {

View File

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

View File

@@ -262,29 +262,20 @@
<ItemsControl ItemsSource="{Binding DeviceActions}"> <ItemsControl ItemsSource="{Binding DeviceActions}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<UniformGrid Columns="3" Rows="3" /> <UniformGrid Columns="2" Rows="4" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Button Content="{Binding DisplayText}" <Button Content="{Binding DisplayText}"
Command="{Binding Command}" Command="{Binding Command}"
CommandParameter="{Binding Label}" CommandParameter="{Binding Label}"
Margin="5,3" Margin="5,3"
MinHeight="40" 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}" /> Style="{StaticResource StatusDeviceActionButtonStyle}" />
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>

View File

@@ -1,159 +1,11 @@
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using ConeCalorimeter.ViewModels;
namespace ConeCalorimeter.Views; namespace ConeCalorimeter.Views;
public partial class TestPageView : UserControl public partial class TestPageView : UserControl
{ {
private readonly HashSet<string> _runningMomentaryActions = [];
public TestPageView() public TestPageView()
{ {
InitializeComponent(); 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();
}
} }