diff --git a/COFTester/ViewModels/MainViewModel.cs b/COFTester/ViewModels/MainViewModel.cs index 449a8c5..11647bb 100644 --- a/COFTester/ViewModels/MainViewModel.cs +++ b/COFTester/ViewModels/MainViewModel.cs @@ -51,6 +51,8 @@ public sealed class MainViewModel : ObservableObject, IDisposable private const ushort RegisterHorizontalDisplacement = 360; private const ushort RegisterLiftSpeed = 310; private const ushort RegisterLiftDisplacement = 320; + private static readonly TimeSpan RealtimeChartRefreshInterval = TimeSpan.FromMilliseconds(250); + private const double RealtimeChartDisplacementStepMm = 0.5; private readonly DispatcherTimer _timer; private readonly DispatcherTimer _deviceReconnectTimer; @@ -125,6 +127,11 @@ public sealed class MainViewModel : ObservableObject, IDisposable private ushort? _pendingTableMotionStopCoil; private MachineRuntimeState _machineRuntimeState = MachineRuntimeState.Idle; private string _plcCommandSummary = "等待 PLC 控制指令。"; + private double _runningPeakForceN; + private double _kineticForceSum; + private int _kineticSampleCount; + private double _lastRealtimeChartDisplacementMm = double.NaN; + private DateTime _lastRealtimeChartRefreshAt = DateTime.MinValue; public MainViewModel() { @@ -758,6 +765,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable _peakLineSamples.Clear(); _averageLineSamples.Clear(); _currentPointSample.Clear(); + ResetRealtimeSamplingMetrics(); CurrentForceN = 0; CurrentDisplacementMm = 0; CurrentPeakForceN = 0; @@ -930,7 +938,6 @@ public sealed class MainViewModel : ObservableObject, IDisposable return; } - _forceSamples.Add(new ObservablePoint(frame.DisplacementMm, CurrentForceN)); _currentRunSamples.Add(new RawSampleRecord { RunId = _activeRunId, @@ -940,8 +947,9 @@ public sealed class MainViewModel : ObservableObject, IDisposable ForceN = CurrentForceN, SpeedMmPerMin = frame.SpeedMmPerMin }); + AddRealtimeChartSample(frame.DisplacementMm, CurrentForceN); UpdateCurrentPoint(frame.DisplacementMm, CurrentForceN); - UpdatePreviewResult(); + UpdatePreviewResult(frame.DisplacementMm, CurrentForceN); StaticCoefficient1 = frame.StaticCoefficient1; KineticCoefficient1 = frame.KineticCoefficient1; StandardDeviation1 = frame.StandardDeviation1; @@ -952,19 +960,16 @@ public sealed class MainViewModel : ObservableObject, IDisposable CurrentStaticCoefficient = ResolveRepresentativeValue(activeReplicateCount, StaticCoefficient1, StaticCoefficient2); CurrentKineticCoefficient = ResolveRepresentativeValue(activeReplicateCount, KineticCoefficient1, KineticCoefficient2); TrialProgressPercent = Math.Min(100, frame.DisplacementMm / Math.Max(Recipe.TravelMm, 1) * 100); - UpdateReferenceLines(); - - ForceXAxes[0].MaxLimit = Math.Max(Recipe.TravelMm, frame.DisplacementMm + 5); - ForceYAxes[0].MaxLimit = Math.Max(CurrentPeakForceN * 1.15, 0.5); - _kineticBand.Yj = Math.Max(CurrentPeakForceN * 1.15, 0.5); - OnPropertyChanged(nameof(ForceXAxes)); - OnPropertyChanged(nameof(ForceYAxes)); - OnPropertyChanged(nameof(ForceSections)); if (frame.IsCompleted) { + RefreshRealtimeChartPresentation(force: true); FinalizeRun(); } + else + { + RefreshRealtimeChartPresentation(force: false); + } RaiseStatusProperties(); } @@ -1038,15 +1043,43 @@ public sealed class MainViewModel : ObservableObject, IDisposable RaiseStatusProperties(); } - private void UpdatePreviewResult() + private void AddRealtimeChartSample(double displacementMm, double forceN) { - CurrentPeakForceN = _forceSamples.Count == 0 ? 0 : _forceSamples.Max(sample => sample.Coordinate.SecondaryValue); - - if (_forceSamples.Count > 5) + var sample = new ObservablePoint(displacementMm, forceN); + if (_forceSamples.Count == 0 || !IsFinite(_lastRealtimeChartDisplacementMm)) { - var kineticWindow = _forceSamples.Skip((int)(_forceSamples.Count * 0.35)).ToArray(); - CurrentAverageForceN = kineticWindow.Average(sample => sample.Coordinate.SecondaryValue); + _forceSamples.Add(sample); + _lastRealtimeChartDisplacementMm = displacementMm; + return; } + + if (Math.Abs(displacementMm - _lastRealtimeChartDisplacementMm) >= RealtimeChartDisplacementStepMm) + { + _forceSamples.Add(sample); + _lastRealtimeChartDisplacementMm = displacementMm; + return; + } + + _forceSamples[^1] = sample; + } + + private void UpdatePreviewResult(double displacementMm, double forceN) + { + _runningPeakForceN = Math.Max(_runningPeakForceN, forceN); + CurrentPeakForceN = _runningPeakForceN; + + var kineticStartMm = GetActiveTravelMm() * 0.35; + if (displacementMm >= kineticStartMm) + { + _kineticForceSum += forceN; + _kineticSampleCount++; + CurrentAverageForceN = _kineticForceSum / _kineticSampleCount; + } + } + + private double GetActiveTravelMm() + { + return _activeRecipeSnapshot?.TravelMm ?? Recipe.TravelMm; } private int GetActiveReplicateCount() @@ -1072,6 +1105,25 @@ public sealed class MainViewModel : ObservableObject, IDisposable UpdateReferenceLine(_averageLineSamples, CurrentAverageForceN); } + private void RefreshRealtimeChartPresentation(bool force) + { + var now = DateTime.UtcNow; + if (!force && now - _lastRealtimeChartRefreshAt < RealtimeChartRefreshInterval) + { + return; + } + + _lastRealtimeChartRefreshAt = now; + UpdateReferenceLines(); + + ForceXAxes[0].MaxLimit = Math.Max(GetActiveTravelMm(), CurrentDisplacementMm + 5); + ForceYAxes[0].MaxLimit = Math.Max(CurrentPeakForceN * 1.15, 0.5); + _kineticBand.Yj = Math.Max(CurrentPeakForceN * 1.15, 0.5); + OnPropertyChanged(nameof(ForceXAxes)); + OnPropertyChanged(nameof(ForceYAxes)); + OnPropertyChanged(nameof(ForceSections)); + } + private void UpdateReferenceLine(ObservableCollection target, double y) { if (y <= 0) @@ -1080,12 +1132,28 @@ public sealed class MainViewModel : ObservableObject, IDisposable return; } - var xEnd = Math.Max(Recipe.TravelMm, CurrentDisplacementMm); + var xEnd = Math.Max(GetDisplayRecipeSnapshot().TravelMm, CurrentDisplacementMm); + if (target.Count == 2) + { + target[0] = new ObservablePoint(0, y); + target[1] = new ObservablePoint(xEnd, y); + return; + } + target.Clear(); target.Add(new ObservablePoint(0, y)); target.Add(new ObservablePoint(xEnd, y)); } + private void ResetRealtimeSamplingMetrics() + { + _runningPeakForceN = 0; + _kineticForceSum = 0; + _kineticSampleCount = 0; + _lastRealtimeChartDisplacementMm = double.NaN; + _lastRealtimeChartRefreshAt = DateTime.MinValue; + } + private TestRecipeSnapshot GetDisplayRecipeSnapshot() { return _isShowingHistoricalRun && _displayedRecipeSnapshot is not null @@ -1435,6 +1503,7 @@ public sealed class MainViewModel : ObservableObject, IDisposable _peakLineSamples.Clear(); _averageLineSamples.Clear(); _currentPointSample.Clear(); + ResetRealtimeSamplingMetrics(); CurrentForceN = 0; CurrentDisplacementMm = 0;