diff --git a/COFTester/MainWindow.xaml b/COFTester/MainWindow.xaml
index a02d01f..4e4ff76 100644
--- a/COFTester/MainWindow.xaml
+++ b/COFTester/MainWindow.xaml
@@ -394,14 +394,14 @@
+ Text="{Binding Recipe.TravelMm, Mode=TwoWay, StringFormat={}{0:F0} mm}" />
+ Text="{Binding Recipe.SpeedMmPerMin, Mode=TwoWay, StringFormat={}{0:F0} mm/min}" />
@@ -612,17 +612,6 @@
-
-
-
-
-
-
-
+ Text="{Binding CurrentKineticCoefficient, Mode=OneWay, StringFormat={}{0:F3}}" />
+ Text="{Binding CurrentPeakForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
+ Text="{Binding CurrentAverageForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
@@ -873,32 +870,46 @@
+ Text="{Binding CurrentForceN, Mode=OneWay, StringFormat={}{0:F3} N}" />
+ Text="{Binding CurrentSpeedMmPerMin, Mode=OneWay, StringFormat={}{0:F1} mm/min}" />
+ Text="{Binding CurrentLiftSpeed, Mode=OneWay, StringFormat={}{0:F1} mm/min}" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -912,28 +923,28 @@
+ Text="{Binding StaticCoefficient1, Mode=OneWay, StringFormat={}{0:F3}}" />
+ Text="{Binding StaticCoefficient2, Mode=OneWay, StringFormat={}{0:F3}}" />
+ Text="{Binding KineticCoefficient1, Mode=OneWay, StringFormat={}{0:F3}}" />
+ Text="{Binding KineticCoefficient2, Mode=OneWay, StringFormat={}{0:F3}}" />
@@ -1070,8 +1081,8 @@
-
-
+
+
diff --git a/COFTester/MainWindow.xaml.cs b/COFTester/MainWindow.xaml.cs
index 2ff87bb..dfc22da 100644
--- a/COFTester/MainWindow.xaml.cs
+++ b/COFTester/MainWindow.xaml.cs
@@ -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;
diff --git a/COFTester/Models/TestRecipe.cs b/COFTester/Models/TestRecipe.cs
index 304b20a..a18fd03 100644
--- a/COFTester/Models/TestRecipe.cs
+++ b/COFTester/Models/TestRecipe.cs
@@ -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}";
+ }
}
diff --git a/COFTester/ViewModels/MainViewModel.cs b/COFTester/ViewModels/MainViewModel.cs
index 794ef01..a3a0fbb 100644
--- a/COFTester/ViewModels/MainViewModel.cs
+++ b/COFTester/ViewModels/MainViewModel.cs
@@ -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 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 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))