From a4f4fdd007b37194a52081f327b5a484a7eeff18 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Mon, 22 Jun 2026 10:16:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B020260622?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Models/SlipDeviceSnapshot.cs | 6 +- .../Services/SlipResistanceDeviceService.cs | 29 +++- .../ViewModels/MainWindowViewModel.cs | 151 +++++++++++++----- 3 files changed, 143 insertions(+), 43 deletions(-) diff --git a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Models/SlipDeviceSnapshot.cs b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Models/SlipDeviceSnapshot.cs index b5aca2b..01b3cfc 100644 --- a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Models/SlipDeviceSnapshot.cs +++ b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Models/SlipDeviceSnapshot.cs @@ -12,7 +12,11 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models bool IsConnected, string LastError) { - public double FrictionCoefficient => Math.Abs(VerticalLoadN) > 0.0001 + // 低载荷段(加载/抬升阶段)垂直力很小,H/V 会放大成无意义尖峰(现场曲线冲到 1.4)。 + // GB/T 3903.6 初始压力为 50N,载荷低于该量级时摩擦系数无意义,置零以保证曲线干净。 + public const double MinimumLoadForCoefficientN = 30.0; + + public double FrictionCoefficient => Math.Abs(VerticalLoadN) >= MinimumLoadForCoefficientN ? HorizontalFrictionN / VerticalLoadN : 0; diff --git a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs index d4bb00a..88b6da7 100644 --- a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs +++ b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/Services/SlipResistanceDeviceService.cs @@ -50,6 +50,12 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services private double verticalLoadN; private double horizontalFrictionN; private double displacementMm; + + // 轻量滑动平均:滤掉力信号的高频抖动,但窗口很短(默认 3 点≈30ms@100Hz)以保留静摩擦首峰, + // 满足 GB/T 3903.6 数据采集频率(≥30Hz、0.3~0.6s 内≥10 点)的前提下让曲线更接近标准。 + private const int ForceFilterWindow = 3; + private readonly Queue pressureFilterSamples = new(); + private readonly Queue frictionFilterSamples = new(); private int pressureRawValue; private int frictionRawValue1; private int frictionRawValue2; @@ -315,8 +321,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services hasAdcRawValues = true; if (conversion.IsValid) { - verticalLoadN = conversion.Pressure; - horizontalFrictionN = conversion.Friction; + verticalLoadN = MovingAverage(pressureFilterSamples, conversion.Pressure); + horizontalFrictionN = MovingAverage(frictionFilterSamples, conversion.Friction); adcLastError = string.Empty; isAdcConnected = true; } @@ -324,6 +330,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services { verticalLoadN = 0; horizontalFrictionN = 0; + pressureFilterSamples.Clear(); + frictionFilterSamples.Clear(); adcLastError = conversion.Error; isAdcConnected = false; } @@ -633,6 +641,23 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services private static double ConvertAdc(int rawValue, double zero, double coefficient) => (rawValue - zero) / coefficient; + private static double MovingAverage(Queue window, double sample) + { + window.Enqueue(sample); + while (window.Count > ForceFilterWindow) + { + window.Dequeue(); + } + + var sum = 0.0; + foreach (var value in window) + { + sum += value; + } + + return sum / window.Count; + } + private static void TryParseSetting(string value, string label, bool requireNonZero, List invalid, out double numericValue) { if (!TryParseDouble(value, out numericValue)) diff --git a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs index f537701..639fe88 100644 --- a/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs +++ b/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs @@ -34,6 +34,14 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel private const double StaticPeakDropToleranceN = 0.5; private const double SlidingStartDisplacementThresholdMm = 0.05; private const double MinimumAnalysisLoadRatio = 0.8; + // 预触发缓冲:滑动检测前持续缓存的力/位移历史长度,用于回溯真实滑动起点(保证第一个摩擦力峰值被采到)。 + private const double PreTriggerWindowSeconds = 0.5; + // 滑动检测:载荷达标后,水平摩擦力较静置接触基线上升超过该值即判定已进入滑动。 + private const double SlidingFrictionTriggerRiseN = 5.0; + // 回溯滑动起点:从检测点向前回溯,直到摩擦力回到基线附近(该裕度内)即认定为 onset(t=0)。 + private const double SlidingOnsetFrictionMarginN = 2.0; + // 曲线/分析窗口:滑动开始后只保留该时长,剔除加载段与滑动后回程,对应标准曲线图的有效区间。 + private const double CurveEndSeconds = 1.0; private const int StaticPeakDropConfirmationPointCount = 3; private const int MinimumDynamicWindowPointCount = 10; private const int StandardTrialCount = 3; @@ -52,6 +60,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel private readonly DispatcherTimer refreshTimer; private readonly Stopwatch runStopwatch = new(); private readonly List currentRun = []; + private readonly List preTriggerBuffer = []; private readonly ObservableCollection verticalLoadPoints = []; private readonly ObservableCollection horizontalFrictionPoints = []; private readonly ObservableCollection frictionCoefficientPoints = []; @@ -72,6 +81,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel private double runStartDisplacementMm; private double slidingStartDisplacementMm; private double? slidingStartTimeSeconds; + private double slidingBaselineFrictionN; + private bool hasSlidingBaseline; private int activeRunLubricantIndex; private DateTime lastLicenseCheckAt = DateTime.MinValue; private bool isLicenseLockPending; @@ -811,6 +822,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel private void BeginRun() { currentRun.Clear(); + preTriggerBuffer.Clear(); verticalLoadPoints.Clear(); horizontalFrictionPoints.Clear(); frictionCoefficientPoints.Clear(); @@ -818,6 +830,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel runStartDisplacementMm = deviceService.CurrentSnapshot.DisplacementMm; slidingStartDisplacementMm = runStartDisplacementMm; slidingStartTimeSeconds = null; + slidingBaselineFrictionN = 0; + hasSlidingBaseline = false; activeRunLubricantIndex = SelectedLubricantIndex; runStopwatch.Restart(); UploadProgress = 0; @@ -834,7 +848,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel private void RecordPoint(SlipDeviceSnapshot device) { var elapsedSinceTestStart = runStopwatch.Elapsed.TotalSeconds; - var detectionPoint = new SlipDataPoint( + var samplePoint = new SlipDataPoint( device.Timestamp, elapsedSinceTestStart, device.VerticalLoadN, @@ -842,25 +856,114 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel device.DisplacementMm, device.FrictionCoefficient); - TryMarkSlidingStart(detectionPoint); + // 滑动起点尚未确定:把样本缓存进滚动预触发缓冲,并尝试检测滑动起点。 if (!slidingStartTimeSeconds.HasValue) + { + UpdatePreTrigger(samplePoint); + return; + } + + var standardTime = elapsedSinceTestStart - slidingStartTimeSeconds.Value; + // 只保留滑动开始后 CurveEndSeconds 区间,剔除滑动结束后的回程/抬升段。 + if (standardTime > CurveEndSeconds) { return; } - var standardTime = Math.Max(0, elapsedSinceTestStart - slidingStartTimeSeconds.Value); + AppendCurvePoint(samplePoint, standardTime); + } + + // 滑动检测前持续缓存样本,并在载荷达标后用水平摩擦力陡升判定滑动起点。 + private void UpdatePreTrigger(SlipDataPoint samplePoint) + { + preTriggerBuffer.Add(samplePoint); + var oldestAllowed = samplePoint.TimeSeconds - PreTriggerWindowSeconds; + while (preTriggerBuffer.Count > 0 && preTriggerBuffer[0].TimeSeconds < oldestAllowed) + { + preTriggerBuffer.RemoveAt(0); + } + + var minimumAnalysisLoad = GetMinimumAnalysisLoad(); + if (samplePoint.VerticalLoadN < minimumAnalysisLoad) + { + // 载荷尚未到达规定压力(静置接触前),基线需在载荷达标后重新捕获。 + hasSlidingBaseline = false; + return; + } + + if (!hasSlidingBaseline) + { + // 载荷首次达标:以当前(仅法向加载、尚未水平滑动)的摩擦力为静置接触基线。 + slidingBaselineFrictionN = samplePoint.HorizontalFrictionN; + hasSlidingBaseline = true; + return; + } + + var frictionRose = samplePoint.HorizontalFrictionN - slidingBaselineFrictionN >= SlidingFrictionTriggerRiseN; + var displacementMoved = Math.Abs(samplePoint.DisplacementMm - runStartDisplacementMm) >= SlidingStartDisplacementThresholdMm; + if (frictionRose || displacementMoved) + { + MarkSlidingStartFromBuffer(frictionRose ? "FrictionRise" : "DisplacementMove"); + } + } + + // 回溯预触发缓冲到摩擦力刚离开基线的那一刻设为 t=0,并把该时刻起的缓冲点补进曲线,保证第一个峰值被采到。 + private void MarkSlidingStartFromBuffer(string trigger) + { + var onsetIndex = preTriggerBuffer.Count - 1; + while (onsetIndex > 0 + && preTriggerBuffer[onsetIndex - 1].HorizontalFrictionN > slidingBaselineFrictionN + SlidingOnsetFrictionMarginN) + { + onsetIndex--; + } + + // 多回退一个点(≈基线水平)作为 t=0,使曲线起点的摩擦力接近基线、随后升至首峰,与标准曲线一致。 + onsetIndex = Math.Max(0, onsetIndex - 1); + var onset = preTriggerBuffer[onsetIndex]; + slidingStartTimeSeconds = onset.TimeSeconds; + slidingStartDisplacementMm = onset.DisplacementMm; + + for (var index = onsetIndex; index < preTriggerBuffer.Count; index++) + { + var buffered = preTriggerBuffer[index]; + var standardTime = buffered.TimeSeconds - onset.TimeSeconds; + if (standardTime > CurveEndSeconds) + { + break; + } + + AppendCurvePoint(buffered, standardTime); + } + + preTriggerBuffer.Clear(); + + CurrentStatus = "已检测到有效滑动开始:曲线从 0.000 s 开始记录"; + Log.Information( + "检测到有效滑动开始并建立曲线零点:TestNumber={TestNumber}, Trigger={Trigger}, SlidingStartTime={SlidingStartTime:F3}s, BaselineFriction={BaselineFriction:F3}N, OnsetDisplacement={OnsetDisplacement:F3}mm, FlushedPointCount={FlushedPointCount}", + TestNumber, + trigger, + onset.TimeSeconds, + slidingBaselineFrictionN, + slidingStartDisplacementMm, + currentRun.Count); + } + + // 把一个样本按标准时间(相对滑动起点)追加到数据与曲线集合,保持各曲线序列同步(节流到 SampleIntervalSeconds)。 + private void AppendCurvePoint(SlipDataPoint source, double standardTime) + { + standardTime = Math.Max(0, standardTime); if (currentRun.Count > 0 && standardTime - currentRun[^1].TimeSeconds < SampleIntervalSeconds) { return; } var point = new SlipDataPoint( - device.Timestamp, + source.Timestamp, standardTime, - device.VerticalLoadN, - device.HorizontalFrictionN, - Math.Abs(device.DisplacementMm - slidingStartDisplacementMm), - device.FrictionCoefficient); + source.VerticalLoadN, + source.HorizontalFrictionN, + Math.Abs(source.DisplacementMm - slidingStartDisplacementMm), + source.FrictionCoefficient); currentRun.Add(point); verticalLoadPoints.Add(new ObservablePoint(standardTime, point.VerticalLoadN)); @@ -1091,34 +1194,6 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel ResultSummary = $"近 3 次平均 静 {staticAverage:F2} / 动 {dynamicAverage:F2}"; } - private void TryMarkSlidingStart(SlipDataPoint point) - { - if (slidingStartTimeSeconds.HasValue) - { - return; - } - - var minimumAnalysisLoad = GetMinimumAnalysisLoad(); - if (!IsSlidingStartPoint(point, runStartDisplacementMm, minimumAnalysisLoad)) - { - return; - } - - slidingStartTimeSeconds = point.TimeSeconds; - slidingStartDisplacementMm = point.DisplacementMm; - CurrentStatus = "已检测到有效滑动开始:曲线从 0.000 s 开始记录"; - Log.Information( - "检测到有效滑动开始并建立曲线零点:TestNumber={TestNumber}, SlidingStartTime={SlidingStartTime:F3}s, SlidingDetectionDelay={SlidingDetectionDelay:F3}s, StandardTimeOrigin=0.000s, StartDisplacement={StartDisplacement:F3}mm, SlidingStartDisplacement={SlidingStartDisplacement:F3}mm, DisplacementDelta={DisplacementDelta:F3}mm, VerticalLoad={VerticalLoad:F3}N, MinimumAnalysisLoad={MinimumAnalysisLoad:F3}N", - TestNumber, - point.TimeSeconds, - point.TimeSeconds, - runStartDisplacementMm, - slidingStartDisplacementMm, - Math.Abs(point.DisplacementMm - runStartDisplacementMm), - point.VerticalLoadN, - minimumAnalysisLoad); - } - private double GetMinimumAnalysisLoad() => TryParseLoadValue(TargetLoadText, out var targetLoad) ? Math.Max(1, targetLoad * MinimumAnalysisLoadRatio) @@ -1132,10 +1207,6 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel return double.TryParse(numeric, NumberStyles.Float, CultureInfo.InvariantCulture, out load); } - private static bool IsSlidingStartPoint(SlipDataPoint point, double startDisplacementMm, double minimumAnalysisLoad) => - point.VerticalLoadN >= minimumAnalysisLoad - && Math.Abs(point.DisplacementMm - startDisplacementMm) >= SlidingStartDisplacementThresholdMm; - private static StaticPeakSelection FindStaticPeak(IReadOnlyList points) { var searchWindow = points