更新20260622

This commit is contained in:
GukSang.Jin
2026-06-22 10:16:43 +08:00
parent 7158942669
commit a4f4fdd007
3 changed files with 143 additions and 43 deletions

View File

@@ -12,7 +12,11 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models
bool IsConnected, bool IsConnected,
string LastError) 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 ? HorizontalFrictionN / VerticalLoadN
: 0; : 0;

View File

@@ -50,6 +50,12 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
private double verticalLoadN; private double verticalLoadN;
private double horizontalFrictionN; private double horizontalFrictionN;
private double displacementMm; private double displacementMm;
// 轻量滑动平均:滤掉力信号的高频抖动,但窗口很短(默认 3 点≈30ms@100Hz以保留静摩擦首峰
// 满足 GB/T 3903.6 数据采集频率≥30Hz、0.3~0.6s 内≥10 点)的前提下让曲线更接近标准。
private const int ForceFilterWindow = 3;
private readonly Queue<double> pressureFilterSamples = new();
private readonly Queue<double> frictionFilterSamples = new();
private int pressureRawValue; private int pressureRawValue;
private int frictionRawValue1; private int frictionRawValue1;
private int frictionRawValue2; private int frictionRawValue2;
@@ -315,8 +321,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
hasAdcRawValues = true; hasAdcRawValues = true;
if (conversion.IsValid) if (conversion.IsValid)
{ {
verticalLoadN = conversion.Pressure; verticalLoadN = MovingAverage(pressureFilterSamples, conversion.Pressure);
horizontalFrictionN = conversion.Friction; horizontalFrictionN = MovingAverage(frictionFilterSamples, conversion.Friction);
adcLastError = string.Empty; adcLastError = string.Empty;
isAdcConnected = true; isAdcConnected = true;
} }
@@ -324,6 +330,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
{ {
verticalLoadN = 0; verticalLoadN = 0;
horizontalFrictionN = 0; horizontalFrictionN = 0;
pressureFilterSamples.Clear();
frictionFilterSamples.Clear();
adcLastError = conversion.Error; adcLastError = conversion.Error;
isAdcConnected = false; isAdcConnected = false;
} }
@@ -633,6 +641,23 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services
private static double ConvertAdc(int rawValue, double zero, double coefficient) => private static double ConvertAdc(int rawValue, double zero, double coefficient) =>
(rawValue - zero) / coefficient; (rawValue - zero) / coefficient;
private static double MovingAverage(Queue<double> 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<string> invalid, out double numericValue) private static void TryParseSetting(string value, string label, bool requireNonZero, List<string> invalid, out double numericValue)
{ {
if (!TryParseDouble(value, out numericValue)) if (!TryParseDouble(value, out numericValue))

View File

@@ -34,6 +34,14 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private const double StaticPeakDropToleranceN = 0.5; private const double StaticPeakDropToleranceN = 0.5;
private const double SlidingStartDisplacementThresholdMm = 0.05; private const double SlidingStartDisplacementThresholdMm = 0.05;
private const double MinimumAnalysisLoadRatio = 0.8; private const double MinimumAnalysisLoadRatio = 0.8;
// 预触发缓冲:滑动检测前持续缓存的力/位移历史长度,用于回溯真实滑动起点(保证第一个摩擦力峰值被采到)。
private const double PreTriggerWindowSeconds = 0.5;
// 滑动检测:载荷达标后,水平摩擦力较静置接触基线上升超过该值即判定已进入滑动。
private const double SlidingFrictionTriggerRiseN = 5.0;
// 回溯滑动起点:从检测点向前回溯,直到摩擦力回到基线附近(该裕度内)即认定为 onsett=0
private const double SlidingOnsetFrictionMarginN = 2.0;
// 曲线/分析窗口:滑动开始后只保留该时长,剔除加载段与滑动后回程,对应标准曲线图的有效区间。
private const double CurveEndSeconds = 1.0;
private const int StaticPeakDropConfirmationPointCount = 3; private const int StaticPeakDropConfirmationPointCount = 3;
private const int MinimumDynamicWindowPointCount = 10; private const int MinimumDynamicWindowPointCount = 10;
private const int StandardTrialCount = 3; private const int StandardTrialCount = 3;
@@ -52,6 +60,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private readonly DispatcherTimer refreshTimer; private readonly DispatcherTimer refreshTimer;
private readonly Stopwatch runStopwatch = new(); private readonly Stopwatch runStopwatch = new();
private readonly List<SlipDataPoint> currentRun = []; private readonly List<SlipDataPoint> currentRun = [];
private readonly List<SlipDataPoint> preTriggerBuffer = [];
private readonly ObservableCollection<ObservablePoint> verticalLoadPoints = []; private readonly ObservableCollection<ObservablePoint> verticalLoadPoints = [];
private readonly ObservableCollection<ObservablePoint> horizontalFrictionPoints = []; private readonly ObservableCollection<ObservablePoint> horizontalFrictionPoints = [];
private readonly ObservableCollection<ObservablePoint> frictionCoefficientPoints = []; private readonly ObservableCollection<ObservablePoint> frictionCoefficientPoints = [];
@@ -72,6 +81,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private double runStartDisplacementMm; private double runStartDisplacementMm;
private double slidingStartDisplacementMm; private double slidingStartDisplacementMm;
private double? slidingStartTimeSeconds; private double? slidingStartTimeSeconds;
private double slidingBaselineFrictionN;
private bool hasSlidingBaseline;
private int activeRunLubricantIndex; private int activeRunLubricantIndex;
private DateTime lastLicenseCheckAt = DateTime.MinValue; private DateTime lastLicenseCheckAt = DateTime.MinValue;
private bool isLicenseLockPending; private bool isLicenseLockPending;
@@ -811,6 +822,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private void BeginRun() private void BeginRun()
{ {
currentRun.Clear(); currentRun.Clear();
preTriggerBuffer.Clear();
verticalLoadPoints.Clear(); verticalLoadPoints.Clear();
horizontalFrictionPoints.Clear(); horizontalFrictionPoints.Clear();
frictionCoefficientPoints.Clear(); frictionCoefficientPoints.Clear();
@@ -818,6 +830,8 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
runStartDisplacementMm = deviceService.CurrentSnapshot.DisplacementMm; runStartDisplacementMm = deviceService.CurrentSnapshot.DisplacementMm;
slidingStartDisplacementMm = runStartDisplacementMm; slidingStartDisplacementMm = runStartDisplacementMm;
slidingStartTimeSeconds = null; slidingStartTimeSeconds = null;
slidingBaselineFrictionN = 0;
hasSlidingBaseline = false;
activeRunLubricantIndex = SelectedLubricantIndex; activeRunLubricantIndex = SelectedLubricantIndex;
runStopwatch.Restart(); runStopwatch.Restart();
UploadProgress = 0; UploadProgress = 0;
@@ -834,7 +848,7 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
private void RecordPoint(SlipDeviceSnapshot device) private void RecordPoint(SlipDeviceSnapshot device)
{ {
var elapsedSinceTestStart = runStopwatch.Elapsed.TotalSeconds; var elapsedSinceTestStart = runStopwatch.Elapsed.TotalSeconds;
var detectionPoint = new SlipDataPoint( var samplePoint = new SlipDataPoint(
device.Timestamp, device.Timestamp,
elapsedSinceTestStart, elapsedSinceTestStart,
device.VerticalLoadN, device.VerticalLoadN,
@@ -842,25 +856,114 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
device.DisplacementMm, device.DisplacementMm,
device.FrictionCoefficient); device.FrictionCoefficient);
TryMarkSlidingStart(detectionPoint); // 滑动起点尚未确定:把样本缓存进滚动预触发缓冲,并尝试检测滑动起点。
if (!slidingStartTimeSeconds.HasValue) if (!slidingStartTimeSeconds.HasValue)
{
UpdatePreTrigger(samplePoint);
return;
}
var standardTime = elapsedSinceTestStart - slidingStartTimeSeconds.Value;
// 只保留滑动开始后 CurveEndSeconds 区间,剔除滑动结束后的回程/抬升段。
if (standardTime > CurveEndSeconds)
{ {
return; 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) if (currentRun.Count > 0 && standardTime - currentRun[^1].TimeSeconds < SampleIntervalSeconds)
{ {
return; return;
} }
var point = new SlipDataPoint( var point = new SlipDataPoint(
device.Timestamp, source.Timestamp,
standardTime, standardTime,
device.VerticalLoadN, source.VerticalLoadN,
device.HorizontalFrictionN, source.HorizontalFrictionN,
Math.Abs(device.DisplacementMm - slidingStartDisplacementMm), Math.Abs(source.DisplacementMm - slidingStartDisplacementMm),
device.FrictionCoefficient); source.FrictionCoefficient);
currentRun.Add(point); currentRun.Add(point);
verticalLoadPoints.Add(new ObservablePoint(standardTime, point.VerticalLoadN)); verticalLoadPoints.Add(new ObservablePoint(standardTime, point.VerticalLoadN));
@@ -1091,34 +1194,6 @@ namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModel
ResultSummary = $"近 3 次平均 静 {staticAverage:F2} / 动 {dynamicAverage:F2}"; 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() => private double GetMinimumAnalysisLoad() =>
TryParseLoadValue(TargetLoadText, out var targetLoad) TryParseLoadValue(TargetLoadText, out var targetLoad)
? Math.Max(1, targetLoad * MinimumAnalysisLoadRatio) ? 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); 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<SlipDataPoint> points) private static StaticPeakSelection FindStaticPeak(IReadOnlyList<SlipDataPoint> points)
{ {
var searchWindow = points var searchWindow = points