This commit is contained in:
GukSang.Jin
2026-04-28 11:11:13 +08:00
parent 8b03b0039f
commit fcb5dc1c21
4 changed files with 306 additions and 55 deletions

View File

@@ -394,14 +394,14 @@
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="水平位移" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding Recipe.TravelMm, Mode=TwoWay, StringFormat=F0 mm}" />
Text="{Binding Recipe.TravelMm, Mode=TwoWay, StringFormat={}{0:F0} mm}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="水平速度" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding Recipe.SpeedMmPerMin, Mode=TwoWay, StringFormat=F0 mm/min}" />
Text="{Binding Recipe.SpeedMmPerMin, Mode=TwoWay, StringFormat={}{0:F0} mm/min}" />
</StackPanel>
</Border>
</UniformGrid>
@@ -612,17 +612,6 @@
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="暂停" />
</DockPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,10,10">
<DockPanel>
<Ellipse Width="12"
Height="12"
Fill="{Binding InterlockIndicatorBrush, Mode=OneWay}"
Stroke="#8DA0AE"
StrokeThickness="1"
Margin="0,2,8,0" />
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="联锁" />
</DockPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,0,10">
<DockPanel>
<Ellipse Width="12"
@@ -672,10 +661,6 @@
Style="{StaticResource PrimaryButtonStyle}"
Command="{Binding PlcStartCommand}"
Content="{Binding StartButtonText, Mode=OneWay}" />
<Button Width="132"
Style="{StaticResource SecondaryButtonStyle}"
Command="{Binding PauseCommand}"
Content="暂停" />
<Button Width="132"
Style="{StaticResource DangerButtonStyle}"
Command="{Binding PlcStopCommand}"
@@ -696,19 +681,31 @@
<WrapPanel>
<Button Width="108"
Style="{StaticResource CompactButtonStyle}"
Command="{Binding RiseCommand}"
Tag="Up"
PreviewMouseLeftButtonDown="TableMotionButton_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="TableMotionButton_PreviewMouseLeftButtonUp"
LostMouseCapture="TableMotionButton_LostMouseCapture"
Content="上升" />
<Button Width="108"
Style="{StaticResource CompactButtonStyle}"
Command="{Binding LowerCommand}"
Tag="Down"
PreviewMouseLeftButtonDown="TableMotionButton_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="TableMotionButton_PreviewMouseLeftButtonUp"
LostMouseCapture="TableMotionButton_LostMouseCapture"
Content="下降" />
<Button Width="108"
Style="{StaticResource CompactButtonStyle}"
Command="{Binding BackCommand}"
Tag="Left"
PreviewMouseLeftButtonDown="TableMotionButton_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="TableMotionButton_PreviewMouseLeftButtonUp"
LostMouseCapture="TableMotionButton_LostMouseCapture"
Content="后退" />
<Button Width="108"
Style="{StaticResource CompactButtonStyle}"
Command="{Binding ForwardCommand}"
Tag="Right"
PreviewMouseLeftButtonDown="TableMotionButton_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="TableMotionButton_PreviewMouseLeftButtonUp"
LostMouseCapture="TableMotionButton_LostMouseCapture"
Content="前进" />
</WrapPanel>
</StackPanel>
@@ -837,28 +834,28 @@
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="实时静摩擦系数" />
<TextBlock Style="{StaticResource MetricValueStyle}"
Text="{Binding CurrentStaticCoefficient, Mode=OneWay, StringFormat=F3}" />
Text="{Binding CurrentStaticCoefficient, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
<Border Style="{StaticResource MetricCardStyle}" Margin="0,0,0,10">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="实时动摩擦系数" />
<TextBlock Style="{StaticResource MetricValueStyle}"
Text="{Binding CurrentKineticCoefficient, Mode=OneWay, StringFormat=F3}" />
Text="{Binding CurrentKineticCoefficient, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
<Border Style="{StaticResource MetricCardStyle}" Margin="0,0,10,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="峰值力" />
<TextBlock Style="{StaticResource MetricValueStyle}"
Text="{Binding CurrentPeakForceN, Mode=OneWay, StringFormat=F3 N}" />
Text="{Binding CurrentPeakForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
</StackPanel>
</Border>
<Border Style="{StaticResource MetricCardStyle}">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="稳定段平均力" />
<TextBlock Style="{StaticResource MetricValueStyle}"
Text="{Binding CurrentAverageForceN, Mode=OneWay, StringFormat=F3 N}" />
Text="{Binding CurrentAverageForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
</StackPanel>
</Border>
</UniformGrid>
@@ -873,32 +870,46 @@
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="实时力值" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding CurrentForceN, Mode=OneWay, StringFormat=F3 N}" />
Text="{Binding CurrentForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,0,10">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="水平速度" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding CurrentSpeedMmPerMin, Mode=OneWay, StringFormat=F1 mm/min}" />
Text="{Binding CurrentSpeedMmPerMin, Mode=OneWay, StringFormat={}{0:F1} mm/min}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,10,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="升降速度" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding CurrentLiftSpeed, Mode=OneWay, StringFormat=F1 mm/min}" />
Text="{Binding CurrentLiftSpeed, Mode=OneWay, StringFormat={}{0:F1} mm/min}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="批次统计" />
<TextBlock FontWeight="SemiBold"
TextWrapping="Wrap"
Text="{Binding TraceabilitySummary, Mode=OneWay}" />
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="升降位置" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding CurrentLiftPosition, Mode=OneWay, StringFormat={}{0:F1} mm}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,10,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="水平位置" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding CurrentHorizontalPosition, Mode=OneWay, StringFormat={}{0:F1} mm}" />
</StackPanel>
</Border>
</UniformGrid>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,14,0,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="批次统计" />
<TextBlock FontWeight="SemiBold"
TextWrapping="Wrap"
Text="{Binding TraceabilitySummary, Mode=OneWay}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,14,0,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="导出摘要" />
@@ -912,28 +923,28 @@
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="静摩擦系数 1" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding StaticCoefficient1, Mode=OneWay, StringFormat=F3}" />
Text="{Binding StaticCoefficient1, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,0,10">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="静摩擦系数 2" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding StaticCoefficient2, Mode=OneWay, StringFormat=F3}" />
Text="{Binding StaticCoefficient2, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}" Margin="0,0,10,0">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="动摩擦系数 1" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding KineticCoefficient1, Mode=OneWay, StringFormat=F3}" />
Text="{Binding KineticCoefficient1, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
<Border Style="{StaticResource InsetCardStyle}">
<StackPanel>
<TextBlock Style="{StaticResource MetricLabelStyle}" Text="动摩擦系数 2" />
<TextBlock Style="{StaticResource SmallMetricValueStyle}"
Text="{Binding KineticCoefficient2, Mode=OneWay, StringFormat=F3}" />
Text="{Binding KineticCoefficient2, Mode=OneWay, StringFormat={}{0:F3}}" />
</StackPanel>
</Border>
</UniformGrid>
@@ -1070,8 +1081,8 @@
<DataGridTextColumn Header="轮次" Binding="{Binding RunIndex}" Width="0.8*" />
<DataGridTextColumn Header="时间" Binding="{Binding CompletedAtLabel}" Width="1.2*" />
<DataGridTextColumn Header="批次" Binding="{Binding BatchNumber}" Width="1.1*" />
<DataGridTextColumn Header="μs" Binding="{Binding StaticCoefficient, StringFormat=F3}" Width="0.9*" />
<DataGridTextColumn Header="μk" Binding="{Binding KineticCoefficient, StringFormat=F3}" Width="0.9*" />
<DataGridTextColumn Header="μs" Binding="{Binding StaticCoefficient, StringFormat={}{0:F3}}" Width="0.9*" />
<DataGridTextColumn Header="μk" Binding="{Binding KineticCoefficient, StringFormat={}{0:F3}}" Width="0.9*" />
<DataGridTextColumn Header="模式" Binding="{Binding TestMode}" Width="1.1*" />
</DataGrid.Columns>
</DataGrid>

View File

@@ -1,5 +1,7 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using COFTester.ViewModels;
namespace COFTester;
@@ -39,6 +41,44 @@ public partial class MainWindow : Window
MainWorkspaceTabs.SelectedIndex = 1;
}
private void TableMotionButton_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is not Button button || button.Tag is not string direction)
{
return;
}
button.CaptureMouse();
_viewModel.BeginTableMotion(direction);
e.Handled = true;
}
private void TableMotionButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
EndTableMotionFromButton(sender);
e.Handled = true;
}
private void TableMotionButton_LostMouseCapture(object sender, MouseEventArgs e)
{
EndTableMotionFromButton(sender);
}
private void EndTableMotionFromButton(object sender)
{
if (sender is not Button button || button.Tag is not string direction)
{
return;
}
_viewModel.EndTableMotion(direction);
if (Mouse.Captured == button)
{
button.ReleaseMouseCapture();
}
}
private void SetSidebarCollapsed(bool collapsed)
{
_isSidebarCollapsed = collapsed;

View File

@@ -4,8 +4,10 @@ namespace COFTester.Models;
public sealed class TestRecipe : ObservableObject
{
private string _productCode = string.Empty;
private string _batchNumber = string.Empty;
public const string DefaultProductCode = "COF-DEFAULT";
private string _productCode = DefaultProductCode;
private string _batchNumber = CreateDefaultBatchNumber();
private string _testMode = "膜/膜";
private string _counterfaceMaterial = "同材质";
private string _direction = "MD";
@@ -88,4 +90,22 @@ public sealed class TestRecipe : ObservableObject
get => _specimenDescription;
set => SetProperty(ref _specimenDescription, value);
}
public void ApplyDefaultIdentifiers()
{
if (string.IsNullOrWhiteSpace(ProductCode))
{
ProductCode = DefaultProductCode;
}
if (string.IsNullOrWhiteSpace(BatchNumber))
{
BatchNumber = CreateDefaultBatchNumber();
}
}
public static string CreateDefaultBatchNumber()
{
return $"BATCH-{DateTime.Now:yyyyMMdd}";
}
}

View File

@@ -121,6 +121,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool _isPlcCommandBusy;
private bool _isSyncingRecipeFromPlc;
private bool _activeRunStartedByPlc;
private ushort? _activeTableMotionCoil;
private ushort? _pendingTableMotionStopCoil;
private MachineRuntimeState _machineRuntimeState = MachineRuntimeState.Idle;
private string _plcCommandSummary = "等待 PLC 控制指令。";
@@ -319,6 +321,45 @@ public sealed class MainViewModel : ObservableObject, IDisposable
public RelayCommand ClearHistoryCommand => _clearHistoryCommand;
public void BeginTableMotion(string direction)
{
if (!TryGetTableMotionCoil(direction, out var actionName, out var coilAddress))
{
return;
}
if (_activeTableMotionCoil is not null || _isPlcCommandBusy)
{
return;
}
_activeTableMotionCoil = coilAddress;
_pendingTableMotionStopCoil = null;
_ = SetTableMotionCoilAsync(actionName, coilAddress, value: true);
}
public void EndTableMotion(string direction)
{
if (!TryGetTableMotionCoil(direction, out var actionName, out var coilAddress))
{
return;
}
if (_activeTableMotionCoil != coilAddress)
{
return;
}
if (_isPlcCommandBusy)
{
_pendingTableMotionStopCoil = coilAddress;
return;
}
_activeTableMotionCoil = null;
_ = SetTableMotionCoilAsync(actionName, coilAddress, value: false);
}
public string MachineState
{
get => _machineState;
@@ -603,6 +644,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable
OnPropertyChanged(nameof(DeviceLastConnectedAtLabel));
AddInfoEvent($"设备通信已连接: {_deviceConnectionService.Endpoint}");
await SyncRecipeFromPlcAsync();
_deviceDataReader.Initialize(Recipe);
EnsureRealtimeMonitorTimerRunning();
RaiseCommandStates();
if (_machineRuntimeState == MachineRuntimeState.Idle)
@@ -681,6 +724,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool TryValidateStartOrResume()
{
Recipe.ApplyDefaultIdentifiers();
var validationErrors = ValidateRecipe().ToArray();
if (validationErrors.Length == 0)
{
@@ -768,7 +813,6 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
_timer.Stop();
_machineRuntimeState = MachineRuntimeState.Paused;
MachineState = "暂停";
StateBrush = BrushFromHex("#C49645");
@@ -779,7 +823,6 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private void Stop()
{
_timer.Stop();
_isShowingHistoricalRun = false;
_displayedRecipeSnapshot = _activeRecipeSnapshot ?? TestRecipeSnapshot.FromRecipe(Recipe);
ResetActiveRunContext();
@@ -840,7 +883,6 @@ public sealed class MainViewModel : ObservableObject, IDisposable
return;
}
_timer.Stop();
_isShowingHistoricalRun = false;
_displayedRecipeSnapshot = _activeRecipeSnapshot ?? TestRecipeSnapshot.FromRecipe(Recipe);
ResetActiveRunContext();
@@ -877,20 +919,17 @@ public sealed class MainViewModel : ObservableObject, IDisposable
try
{
var frame = await ReadRealtimeFrameAsync();
if (!_isShowingHistoricalRun)
{
UpdateLiveProcessSnapshot(frame);
}
if (_machineRuntimeState != MachineRuntimeState.Running)
{
RaiseStatusProperties();
return;
}
CurrentForceN = Math.Max(frame.ForceN, 0.001);
CurrentDisplacementMm = frame.DisplacementMm;
CurrentSpeedMmPerMin = frame.SpeedMmPerMin;
CurrentLiftSpeed = frame.LiftSpeed;
CurrentLiftDisplacement = frame.LiftDisplacement;
CurrentLiftPosition = frame.LiftPosition;
CurrentHorizontalPosition = frame.HorizontalPosition;
_forceSamples.Add(new ObservablePoint(frame.DisplacementMm, CurrentForceN));
_currentRunSamples.Add(new RawSampleRecord
{
@@ -931,7 +970,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
catch (Exception ex)
{
HandleRealtimeSamplingFailure(ex);
if (_machineRuntimeState == MachineRuntimeState.Running || HasRecoverableActiveRunContext())
{
HandleRealtimeSamplingFailure(ex);
}
else
{
HandleRealtimeMonitorFailure(ex);
}
}
finally
{
@@ -939,10 +985,19 @@ public sealed class MainViewModel : ObservableObject, IDisposable
}
}
private void UpdateLiveProcessSnapshot(ProcessFrame frame)
{
CurrentForceN = Math.Max(frame.ForceN, 0.001);
CurrentDisplacementMm = frame.DisplacementMm;
CurrentSpeedMmPerMin = frame.SpeedMmPerMin;
CurrentLiftSpeed = frame.LiftSpeed;
CurrentLiftDisplacement = frame.LiftDisplacement;
CurrentLiftPosition = frame.LiftPosition;
CurrentHorizontalPosition = frame.HorizontalPosition;
}
private void FinalizeRun()
{
_timer.Stop();
var record = new RunRecord
{
RunId = _activeRunId,
@@ -1071,7 +1126,104 @@ public sealed class MainViewModel : ObservableObject, IDisposable
private bool CanExecutePlcCommand()
{
return !_isPlcCommandBusy;
return !_isPlcCommandBusy && _activeTableMotionCoil is null;
}
private static bool TryGetTableMotionCoil(string direction, out string actionName, out ushort coilAddress)
{
switch (direction)
{
case "Up":
actionName = "上";
coilAddress = CoilRise;
return true;
case "Down":
actionName = "下";
coilAddress = CoilLower;
return true;
case "Left":
actionName = "左";
coilAddress = CoilBack;
return true;
case "Right":
actionName = "右";
coilAddress = CoilForward;
return true;
default:
actionName = string.Empty;
coilAddress = 0;
return false;
}
}
private async Task<bool> SetTableMotionCoilAsync(string actionName, ushort coilAddress, bool value)
{
if (_isPlcCommandBusy)
{
if (!value)
{
_pendingTableMotionStopCoil = coilAddress;
}
return false;
}
_isPlcCommandBusy = true;
RaiseCommandStates();
try
{
if (!_deviceConnectionService.IsConnected)
{
await InitializeDeviceConnectionAsync();
}
if (!_deviceConnectionService.IsConnected)
{
PlcCommandSummary = $"PLC 台面{actionName}{(value ? "" : "")}失败: 设备未连接。";
if (value && _activeTableMotionCoil == coilAddress)
{
_activeTableMotionCoil = null;
}
return false;
}
await _deviceConnectionService.WriteSingleCoilAsync(ModbusSlaveAddress, coilAddress, value);
PlcCommandSummary = $"PLC 台面{actionName}{(value ? "" : "")}已发送: M{coilAddress}={(value ? "ON" : "OFF")}";
AddInfoEvent($"PLC 台面点动写入成功: {actionName} M{coilAddress}={(value ? "ON" : "OFF")}");
InterlockMessage = $"已发送 PLC 台面{actionName}{(value ? "" : "")}指令: M{coilAddress}={(value ? "ON" : "OFF")}";
return true;
}
catch (Exception ex)
{
PlcCommandSummary = $"PLC 台面{actionName}{(value ? "" : "")}失败: {ex.Message}";
AddWarningEvent($"PLC 台面点动写入失败 {actionName} M{coilAddress}={(value ? "ON" : "OFF")}: {ex.Message}");
if (value && _activeTableMotionCoil == coilAddress)
{
_activeTableMotionCoil = null;
}
return false;
}
finally
{
_isPlcCommandBusy = false;
RaiseCommandStates();
if (value && _pendingTableMotionStopCoil == coilAddress)
{
_pendingTableMotionStopCoil = null;
if (_activeTableMotionCoil == coilAddress)
{
_activeTableMotionCoil = null;
await SetTableMotionCoilAsync(actionName, coilAddress, value: false);
}
}
}
}
private async Task<bool> ExecutePlcPulseCommandAsync(string actionName, ushort coilAddress, bool updateLocalState = true)
@@ -1340,6 +1492,14 @@ public sealed class MainViewModel : ObservableObject, IDisposable
return await _deviceDataReader.ReadFrameAsync();
}
private void EnsureRealtimeMonitorTimerRunning()
{
if (_deviceConnectionService.IsConnected && !_timer.IsEnabled)
{
_timer.Start();
}
}
private void HandleRealtimeSamplingFailure(Exception ex)
{
_timer.Stop();
@@ -1369,6 +1529,26 @@ public sealed class MainViewModel : ObservableObject, IDisposable
RaiseStatusProperties();
}
private void HandleRealtimeMonitorFailure(Exception ex)
{
_timer.Stop();
_deviceConnectionService.Disconnect();
_deviceReconnectTimer.Start();
_deviceConnectionFailureLogged = true;
DeviceConnectionStatus = "重连中";
DeviceConnectionSummary = "实时监控读取中断,软件自动重连中。";
OnPropertyChanged(nameof(DeviceConnectionIndicatorBrush));
RaiseCommandStates();
if (_machineRuntimeState == MachineRuntimeState.Idle)
{
InterlockMessage = $"实时监控读取失败: {ex.Message}";
}
AddWarningEvent($"实时监控读取失败: {ex.Message}");
RaiseStatusProperties();
}
private async void OnRecipePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (_isSyncingRecipeFromPlc || string.IsNullOrWhiteSpace(e.PropertyName))