2026-06-04 09:22:18 +08:00
|
|
|
|
using Avalonia;
|
|
|
|
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
|
|
|
|
using Avalonia.Platform.Storage;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
using Avalonia.Threading;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
|
|
|
|
|
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
using LiveChartsCore;
|
|
|
|
|
|
using LiveChartsCore.Defaults;
|
|
|
|
|
|
using LiveChartsCore.SkiaSharpView;
|
|
|
|
|
|
using LiveChartsCore.SkiaSharpView.Painting;
|
2026-06-02 18:45:14 +08:00
|
|
|
|
using Serilog;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
using SkiaSharp;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
using System.Threading.Tasks;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels
|
|
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
public partial class MainWindowViewModel : ViewModelBase, IDisposable
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private const double SampleIntervalSeconds = 1.0 / 50.0;
|
|
|
|
|
|
private const double DynamicWindowStartSeconds = 0.3;
|
|
|
|
|
|
private const double DynamicWindowEndSeconds = 0.6;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private const double StaticPeakSearchEndSeconds = DynamicWindowEndSeconds;
|
|
|
|
|
|
private const double StaticPeakMinimumRiseN = 1.0;
|
|
|
|
|
|
private const double StaticPeakDropToleranceN = 0.5;
|
|
|
|
|
|
private const double SlidingStartDisplacementThresholdMm = 0.05;
|
|
|
|
|
|
private const double MinimumAnalysisLoadRatio = 0.8;
|
2026-06-22 10:16:43 +08:00
|
|
|
|
// 预触发缓冲:滑动检测前持续缓存的力/位移历史长度,用于回溯真实滑动起点(保证第一个摩擦力峰值被采到)。
|
|
|
|
|
|
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;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private const int StaticPeakDropConfirmationPointCount = 3;
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private const int MinimumDynamicWindowPointCount = 10;
|
|
|
|
|
|
private const int StandardTrialCount = 3;
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private const string DefaultPlcPortName = "COM3";
|
|
|
|
|
|
private const string DefaultAdcPortName = "COM4";
|
|
|
|
|
|
private const string LegacyPlcPortName = "COM7";
|
|
|
|
|
|
private const string LegacyAdcPortName = "COM8";
|
2026-06-04 09:22:18 +08:00
|
|
|
|
private static readonly TimeSpan ResetButtonPendingTimeout = TimeSpan.FromMilliseconds(800);
|
2026-06-08 18:49:29 +08:00
|
|
|
|
private static readonly TimeSpan TestButtonPendingTimeout = TimeSpan.FromSeconds(5);
|
2026-06-05 15:40:48 +08:00
|
|
|
|
private static readonly TimeSpan RealtimeCurveTraceInterval = TimeSpan.FromSeconds(1);
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private static readonly TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(1);
|
2026-06-02 19:16:50 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private readonly SlipResistanceDeviceService deviceService = new();
|
|
|
|
|
|
private readonly SlipExcelExportService excelExportService = new();
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private readonly MachineLicenseService licenseService;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private readonly DispatcherTimer refreshTimer;
|
|
|
|
|
|
private readonly Stopwatch runStopwatch = new();
|
|
|
|
|
|
private readonly List<SlipDataPoint> currentRun = [];
|
2026-06-22 10:16:43 +08:00
|
|
|
|
private readonly List<SlipDataPoint> preTriggerBuffer = [];
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private readonly ObservableCollection<ObservablePoint> verticalLoadPoints = [];
|
|
|
|
|
|
private readonly ObservableCollection<ObservablePoint> horizontalFrictionPoints = [];
|
|
|
|
|
|
private readonly ObservableCollection<ObservablePoint> frictionCoefficientPoints = [];
|
|
|
|
|
|
private readonly ObservableCollection<ObservablePoint> displacementPoints = [];
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
private bool isLoadingDeviceSettings;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private bool wasRunning;
|
2026-06-08 18:49:29 +08:00
|
|
|
|
private bool isTestStartPending;
|
|
|
|
|
|
private DateTime testStartPendingStartedAt = DateTime.MinValue;
|
2026-06-04 09:22:18 +08:00
|
|
|
|
private bool isResetButtonPending;
|
|
|
|
|
|
private bool hasObservedResetDeviceBusy;
|
|
|
|
|
|
private DateTime resetButtonPendingStartedAt = DateTime.MinValue;
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private string activePlcPortName = string.Empty;
|
|
|
|
|
|
private string activeAdcPortName = string.Empty;
|
|
|
|
|
|
private int activeBaudRate;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private List<SlipDataPoint> lastCompletedRun = [];
|
2026-06-05 15:40:48 +08:00
|
|
|
|
private DateTime lastRealtimeCurveTraceLoggedAt = DateTime.MinValue;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private double runStartDisplacementMm;
|
2026-06-08 11:38:57 +08:00
|
|
|
|
private double slidingStartDisplacementMm;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private double? slidingStartTimeSeconds;
|
2026-06-22 10:16:43 +08:00
|
|
|
|
private double slidingBaselineFrictionN;
|
|
|
|
|
|
private bool hasSlidingBaseline;
|
2026-06-08 18:08:30 +08:00
|
|
|
|
private int activeRunLubricantIndex;
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private DateTime lastLicenseCheckAt = DateTime.MinValue;
|
|
|
|
|
|
private bool isLicenseLockPending;
|
|
|
|
|
|
private bool licenseLockRequestedRaised;
|
|
|
|
|
|
private string licenseLockMessage = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
public event Action<string>? LicenseLockRequested;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string testNumber = $"SLIP-{DateTime.Now:yyyyMMdd-HHmm}";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string operatorName = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string methodName = "GB/T 3903.6-2024";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string reportName = string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string sampleFeature = "整鞋样品 / 瓷砖接触面 / 水平测试";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private string currentStatus = "设备连接中,等待 PLC 与 ADC 实时数据";
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private int uploadProgress;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string manualDistance = "0";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int selectedSampleIndex;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private bool isSettingsDialogOpen;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string manualSpeed = "0.00";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string manualDisplacement = "0.00";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string testSpeed = "0.30";
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string normalPressureZero = "0";
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string normalPressureCoefficient = "0.00";
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string frictionZero1 = "0";
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string frictionCoefficient1 = "0.00";
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string frictionZero2 = "0";
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string frictionCoefficient2 = "0.00";
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
[ObservableProperty]
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private string plcPortName = DefaultPlcPortName;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private string adcPortName = DefaultAdcPortName;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int baudRate = 115200;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int selectedShoeSizeIndex;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int selectedLubricantIndex;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int selectedModeIndex = 2;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int selectedSurfaceIndex;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string targetLoadText = "400 N";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string actualLoadText = "0.0 N";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string deviceStatus = "离线";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string activeMode = "水平测试模式";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string resultSummary = "等待 3 次有效试验";
|
|
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string testButtonText = "测试";
|
|
|
|
|
|
|
2026-06-08 18:49:29 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private bool isTestButtonEnabled = true;
|
|
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string resetButtonText = "复位";
|
|
|
|
|
|
|
2026-06-08 18:08:30 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int completedTestCount;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int dryTestCount;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int wetTestCount;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private int frostTestCount;
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
[ObservableProperty]
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private string staticCoefficient = "0.00";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private string dynamicCoefficient = "0.00";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string verticalPressure = "0.0";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string horizontalForce = "0.0";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string frictionCoefficient = "0.000";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string distance = "0.0";
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
public string TestSpeedText => $"{TestSpeed} m/s";
|
2026-06-02 19:16:50 +08:00
|
|
|
|
public string SampleRateText { get; } = "50 Hz";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
public string BatchNumber { get; } = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
2026-06-02 17:41:53 +08:00
|
|
|
|
public string UploadProgressText => $"{UploadProgress}%";
|
2026-06-02 19:23:51 +08:00
|
|
|
|
public string StandardReference { get; } = "GB/T 3903.6-2024:实时采集、静/动摩擦系数、三次平均与重测判定";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<TestSample> Samples { get; } = [];
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
public DrawMarginFrame ChartFrame { get; } = new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Fill = new SolidColorPaint(SKColor.Parse("#FFFFFF")),
|
|
|
|
|
|
Stroke = new SolidColorPaint(SKColor.Parse("#CBD5E1")) { StrokeThickness = 1.4f }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
public SolidColorPaint LegendTextPaint { get; } = new(SKColor.Parse("#334155"));
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
public ISeries[] Series { get; }
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
public Axis[] XAxes { get; } =
|
|
|
|
|
|
[
|
|
|
|
|
|
new Axis
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "时间(s)",
|
2026-06-02 18:14:01 +08:00
|
|
|
|
MinLimit = 0,
|
2026-06-08 11:56:58 +08:00
|
|
|
|
MinStep = 0.05,
|
2026-06-02 17:41:53 +08:00
|
|
|
|
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 },
|
|
|
|
|
|
SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 },
|
|
|
|
|
|
SubseparatorsCount = 4,
|
|
|
|
|
|
LabelsPaint = new SolidColorPaint(SKColor.Parse("#475569")),
|
|
|
|
|
|
NamePaint = new SolidColorPaint(SKColor.Parse("#334155")),
|
|
|
|
|
|
TextSize = 12,
|
|
|
|
|
|
NameTextSize = 13,
|
|
|
|
|
|
Padding = new LiveChartsCore.Drawing.Padding(4, 3, 4, 3)
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
public Axis[] YAxes { get; } =
|
|
|
|
|
|
[
|
|
|
|
|
|
new Axis
|
|
|
|
|
|
{
|
2026-06-08 11:56:58 +08:00
|
|
|
|
Name = "压力(N)/距离(mm)",
|
2026-06-02 17:41:53 +08:00
|
|
|
|
MinLimit = 0,
|
2026-06-08 11:56:58 +08:00
|
|
|
|
MaxLimit = 800,
|
|
|
|
|
|
MinStep = 50,
|
|
|
|
|
|
ForceStepToMin = true,
|
2026-06-02 17:41:53 +08:00
|
|
|
|
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#D7E0EA")) { StrokeThickness = 1 },
|
|
|
|
|
|
SubseparatorsPaint = new SolidColorPaint(SKColor.Parse("#EEF3F8")) { StrokeThickness = 1 },
|
|
|
|
|
|
SubseparatorsCount = 4,
|
|
|
|
|
|
LabelsPaint = new SolidColorPaint(SKColor.Parse("#475569")),
|
|
|
|
|
|
NamePaint = new SolidColorPaint(SKColor.Parse("#334155")),
|
|
|
|
|
|
TextSize = 11,
|
|
|
|
|
|
NameTextSize = 12,
|
|
|
|
|
|
Padding = new LiveChartsCore.Drawing.Padding(2, 2, 3, 2)
|
|
|
|
|
|
},
|
|
|
|
|
|
new Axis
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = "摩擦系数",
|
|
|
|
|
|
Position = LiveChartsCore.Measure.AxisPosition.End,
|
|
|
|
|
|
MinLimit = 0,
|
2026-06-08 11:56:58 +08:00
|
|
|
|
MaxLimit = 1.5,
|
|
|
|
|
|
MinStep = 0.1,
|
|
|
|
|
|
ForceStepToMin = true,
|
2026-06-02 17:41:53 +08:00
|
|
|
|
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
|
|
|
|
|
|
NamePaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
|
2026-06-08 11:38:57 +08:00
|
|
|
|
SeparatorsPaint = null,
|
2026-06-02 17:41:53 +08:00
|
|
|
|
TextSize = 11,
|
|
|
|
|
|
NameTextSize = 12,
|
|
|
|
|
|
Padding = new LiveChartsCore.Drawing.Padding(3, 2, 2, 2)
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-15 10:28:16 +08:00
|
|
|
|
public MainWindowViewModel() : this(new MachineLicenseService())
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public MainWindowViewModel(MachineLicenseService licenseService)
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-15 10:28:16 +08:00
|
|
|
|
this.licenseService = licenseService;
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information("初始化主页面 ViewModel");
|
2026-06-02 18:14:01 +08:00
|
|
|
|
Series =
|
|
|
|
|
|
[
|
2026-06-08 11:56:58 +08:00
|
|
|
|
CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0),
|
|
|
|
|
|
CreateLineSeries("水平拉力(N)", horizontalFrictionPoints, "#16A34A", 0),
|
|
|
|
|
|
CreateLineSeries("摩擦系数", frictionCoefficientPoints, "#C026D3", 1),
|
|
|
|
|
|
CreateLineSeries("距离(mm)", displacementPoints, "#2563EB", 0)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
LoadDeviceSettings();
|
2026-06-02 18:14:01 +08:00
|
|
|
|
UpdateTargetLoad();
|
2026-06-03 15:35:55 +08:00
|
|
|
|
RestartDeviceConnection();
|
2026-06-02 18:45:14 +08:00
|
|
|
|
_ = LoadPlcParametersAsync();
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
|
2026-06-02 18:14:01 +08:00
|
|
|
|
refreshTimer.Tick += (_, _) => RefreshFromDevice();
|
|
|
|
|
|
refreshTimer.Start();
|
2026-06-15 10:28:16 +08:00
|
|
|
|
RefreshLicenseState();
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task Clear()
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-03 15:35:55 +08:00
|
|
|
|
ApplyConnectionSettings();
|
2026-06-04 09:22:18 +08:00
|
|
|
|
BeginResetButtonFeedback();
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await deviceService.PulseResetAsync();
|
|
|
|
|
|
CurrentStatus = "已按老代码逻辑发送复位指令 M90";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Error(ex, "设备指令失败:已按老代码逻辑发送复位指令 M90");
|
|
|
|
|
|
CurrentStatus = $"设备指令失败:{ex.Message}";
|
|
|
|
|
|
ClearResetButtonFeedback();
|
|
|
|
|
|
UpdateResetButtonText(deviceService.CurrentSnapshot);
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void Preload()
|
|
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
CurrentStatus = "请在 0.2 s 内施加目标垂直载荷,载荷达到后启动滑动测试";
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task StartTest()
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-15 10:28:16 +08:00
|
|
|
|
if (!IsTestButtonEnabled || isLicenseLockPending)
|
2026-06-08 18:49:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
ApplyConnectionSettings();
|
|
|
|
|
|
var device = deviceService.CurrentSnapshot;
|
|
|
|
|
|
if (device.IsTestRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!ValidateAdcCoefficientsBeforeTest())
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 18:49:29 +08:00
|
|
|
|
BeginTestButtonFeedback();
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await deviceService.PulseStartTestAsync();
|
|
|
|
|
|
CurrentStatus = "已按老代码逻辑发送测试启动指令 M80,等待 M81 运行状态";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Error(ex, "设备指令失败:已按老代码逻辑发送测试启动指令 M80");
|
|
|
|
|
|
CurrentStatus = $"设备指令失败:{ex.Message}";
|
|
|
|
|
|
ClearTestButtonFeedback();
|
|
|
|
|
|
UpdateTestButtonState(deviceService.CurrentSnapshot);
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task StopTest()
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已发送测试停止指令 M83");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-04 09:22:18 +08:00
|
|
|
|
private async Task ExportReport()
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
|
|
|
|
|
|
if (points.Count == 0)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Warning("请求导出 Excel 时没有可导出的实时采样数据:TestNumber={TestNumber}", TestNumber);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-06-04 09:22:18 +08:00
|
|
|
|
CurrentStatus = "请选择 Excel 报告保存目录";
|
|
|
|
|
|
var exportDirectory = await SelectExportDirectoryAsync();
|
|
|
|
|
|
if (exportDirectory is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Information("用户取消 Excel 导出目录选择:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
|
|
|
|
|
|
CurrentStatus = "已取消 Excel 导出";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var report = new SlipReportExport(
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
OperatorName,
|
|
|
|
|
|
MethodName,
|
|
|
|
|
|
ReportName,
|
|
|
|
|
|
SampleFeature,
|
|
|
|
|
|
CurrentShoeSize(),
|
|
|
|
|
|
CurrentLubricant(),
|
|
|
|
|
|
CurrentMode(),
|
|
|
|
|
|
CurrentSurface(),
|
|
|
|
|
|
TargetLoadText,
|
|
|
|
|
|
ActualLoadText,
|
|
|
|
|
|
TestSpeedText,
|
|
|
|
|
|
StandardReference,
|
|
|
|
|
|
Samples.ToList(),
|
|
|
|
|
|
points);
|
|
|
|
|
|
|
2026-06-04 09:22:18 +08:00
|
|
|
|
var path = excelExportService.Export(report, exportDirectory);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
UploadProgress = 100;
|
|
|
|
|
|
CurrentStatus = $"Excel 已导出:{path}";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Error(ex, "Excel 导出失败:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
CurrentStatus = $"Excel 导出失败:{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-04 09:22:18 +08:00
|
|
|
|
private async Task<string?> SelectExportDirectoryAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var storageProvider = GetStorageProvider();
|
|
|
|
|
|
if (storageProvider is null || !storageProvider.CanPickFolder)
|
|
|
|
|
|
{
|
|
|
|
|
|
var defaultDirectory = SlipExcelExportService.GetDefaultExportDirectory();
|
|
|
|
|
|
Log.Warning("当前平台无法打开 Excel 导出目录选择器,将使用默认目录:Path={Path}", defaultDirectory);
|
|
|
|
|
|
return defaultDirectory;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Title = "选择报告保存目录",
|
|
|
|
|
|
AllowMultiple = false,
|
|
|
|
|
|
SuggestedStartLocation = await TryGetSuggestedExportFolderAsync(storageProvider)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var selectedFolder = folders.FirstOrDefault();
|
|
|
|
|
|
if (selectedFolder is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var localPath = selectedFolder.TryGetLocalPath();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(localPath) && selectedFolder.Path.IsFile)
|
|
|
|
|
|
{
|
|
|
|
|
|
localPath = selectedFolder.Path.LocalPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(localPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new InvalidOperationException("选择的目录不是本地文件夹,无法保存 Excel");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return localPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static async Task<IStorageFolder?> TryGetSuggestedExportFolderAsync(IStorageProvider storageProvider)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var defaultDirectory = SlipExcelExportService.GetDefaultExportDirectory();
|
|
|
|
|
|
if (Directory.Exists(defaultDirectory))
|
|
|
|
|
|
{
|
|
|
|
|
|
return await storageProvider.TryGetFolderFromPathAsync(defaultDirectory);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var parentDirectory = Path.GetDirectoryName(defaultDirectory);
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(parentDirectory) && Directory.Exists(parentDirectory))
|
|
|
|
|
|
{
|
|
|
|
|
|
return await storageProvider.TryGetFolderFromPathAsync(parentDirectory);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning(ex, "准备 Excel 默认导出目录失败,将不设置目录选择初始位置");
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static IStorageProvider? GetStorageProvider()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
|
|
|
|
|
|
&& desktop.MainWindow is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return desktop.MainWindow.StorageProvider;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[RelayCommand]
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private Task LiftMotion() => RunLicensedDeviceCommand(deviceService.ApplyOldLiftAsync, "提升按老代码逻辑执行:M4=0, M5=1");
|
2026-06-02 18:53:31 +08:00
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[RelayCommand]
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private Task LowerMotion() => RunLicensedDeviceCommand(deviceService.ApplyOldLowerAsync, "下降按老代码逻辑执行:M5=0, M4=1");
|
2026-06-02 18:53:31 +08:00
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[RelayCommand]
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private Task MoveLeftMotion() => RunLicensedDeviceCommand(deviceService.ToggleOldMoveLeftAsync, "左移按老代码逻辑切换 M1");
|
2026-06-02 18:53:31 +08:00
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
[RelayCommand]
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private Task MoveRightMotion() => RunLicensedDeviceCommand(deviceService.ToggleOldMoveRightAsync, "右移按老代码逻辑切换 M2");
|
2026-06-02 18:53:31 +08:00
|
|
|
|
|
|
|
|
|
|
public Task StopAllMotionAsync() => RunDeviceCommand(deviceService.StopAllMotionAsync(), "全部运动已停止");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void DeleteSelectedSample()
|
|
|
|
|
|
{
|
|
|
|
|
|
var sample = Samples.FirstOrDefault(x => x.Index == SelectedSampleIndex);
|
|
|
|
|
|
if (sample is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentStatus = $"未找到序号 {SelectedSampleIndex} 的实验数据";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Samples.Remove(sample);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
UpdateResultSummary();
|
2026-06-02 17:41:53 +08:00
|
|
|
|
CurrentStatus = $"已删除序号 {SelectedSampleIndex} 的实验数据";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void ShowSettingsDialog()
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSettingsDialogOpen = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void CloseSettingsDialog()
|
|
|
|
|
|
{
|
|
|
|
|
|
IsSettingsDialogOpen = false;
|
2026-06-03 15:35:55 +08:00
|
|
|
|
ApplyConnectionSettings();
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:45:14 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void CalibrateNormalPressureZero()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var zero = deviceService.CaptureCurrentAdcZero();
|
|
|
|
|
|
NormalPressureZero = zero.NormalPressureZero.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
CurrentStatus = "正压力零点已按当前 ADC 原始值采集";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Error(ex, "正压力零点采集失败");
|
|
|
|
|
|
CurrentStatus = $"正压力零点采集失败:{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void CalibrateFrictionZero()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var zero = deviceService.CaptureCurrentAdcZero();
|
|
|
|
|
|
FrictionZero1 = zero.FrictionZero1.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
FrictionZero2 = zero.FrictionZero2.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
CurrentStatus = "摩擦零点已按当前 ADC 原始值采集";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Error(ex, "摩擦零点采集失败");
|
|
|
|
|
|
CurrentStatus = $"摩擦零点采集失败:{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
partial void OnUploadProgressChanged(int value) => OnPropertyChanged(nameof(UploadProgressText));
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnManualSpeedChanged(string value)
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
WriteNumericSetting(value, deviceService.WriteManualSpeedAsync, "手动速度");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
partial void OnManualDisplacementChanged(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
WriteNumericSetting(value, deviceService.WriteManualDisplacementAsync, "手动位移");
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
partial void OnTestSpeedChanged(string value)
|
|
|
|
|
|
{
|
|
|
|
|
|
OnPropertyChanged(nameof(TestSpeedText));
|
2026-06-02 18:14:01 +08:00
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
WriteNumericSetting(value, deviceService.WriteTestSpeedAsync, "测试速度");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnNormalPressureZeroChanged(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnNormalPressureCoefficientChanged(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFrictionZero1Changed(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFrictionCoefficient1Changed(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFrictionZero2Changed(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFrictionCoefficient2Changed(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnPlcPortNameChanged(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnAdcPortNameChanged(string value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnBaudRateChanged(int value) => SaveAndApplySettings();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnSelectedShoeSizeIndexChanged(int value) => UpdateTargetLoad();
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnSelectedModeIndexChanged(int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
ActiveMode = CurrentMode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
|
|
|
|
|
refreshTimer.Stop();
|
2026-06-02 18:53:31 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceService.StopAllMotionAsync().Wait(500);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning(ex, "关闭窗口时停止全部运动失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
deviceService.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:28:16 +08:00
|
|
|
|
public void RefreshLicenseState()
|
|
|
|
|
|
{
|
|
|
|
|
|
var check = licenseService.Check(updateHeartbeat: true);
|
|
|
|
|
|
lastLicenseCheckAt = DateTime.UtcNow;
|
|
|
|
|
|
isLicenseLockPending = !check.CanUseSoftware;
|
|
|
|
|
|
licenseLockMessage = check.Message;
|
|
|
|
|
|
if (!isLicenseLockPending)
|
|
|
|
|
|
{
|
|
|
|
|
|
licenseLockRequestedRaised = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UpdateTestButtonState(deviceService.CurrentSnapshot);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void RefreshFromDevice()
|
|
|
|
|
|
{
|
|
|
|
|
|
var device = deviceService.CurrentSnapshot;
|
2026-06-15 10:28:16 +08:00
|
|
|
|
CheckRuntimeLicense(device);
|
2026-06-03 15:35:55 +08:00
|
|
|
|
if (device.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceStatus = device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机";
|
2026-06-04 09:22:18 +08:00
|
|
|
|
UpdateResetButtonText(device);
|
2026-06-03 15:35:55 +08:00
|
|
|
|
VerticalPressure = device.VerticalLoadN.ToString("F1", CultureInfo.InvariantCulture);
|
|
|
|
|
|
HorizontalForce = device.HorizontalFrictionN.ToString("F1", CultureInfo.InvariantCulture);
|
|
|
|
|
|
FrictionCoefficient = device.FrictionCoefficient.ToString("F3", CultureInfo.InvariantCulture);
|
|
|
|
|
|
Distance = device.DisplacementMm.ToString("F1", CultureInfo.InvariantCulture);
|
|
|
|
|
|
ActualLoadText = $"{VerticalPressure} N";
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
DeviceStatus = IsAdcCalibrationError(device.LastError) ? "数据无效" : "离线";
|
2026-06-04 09:22:18 +08:00
|
|
|
|
UpdateResetButtonText(device);
|
2026-06-03 15:35:55 +08:00
|
|
|
|
VerticalPressure = "--";
|
|
|
|
|
|
HorizontalForce = "--";
|
|
|
|
|
|
FrictionCoefficient = "--";
|
|
|
|
|
|
Distance = "--";
|
|
|
|
|
|
ActualLoadText = "-- N";
|
|
|
|
|
|
}
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
2026-06-08 18:49:29 +08:00
|
|
|
|
UpdateTestButtonState(device);
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
if (!device.IsConnected && !string.IsNullOrWhiteSpace(device.LastError))
|
|
|
|
|
|
{
|
2026-06-03 15:35:55 +08:00
|
|
|
|
CurrentStatus = IsAdcCalibrationError(device.LastError)
|
|
|
|
|
|
? $"数据无效:{device.LastError}"
|
|
|
|
|
|
: $"设备离线:{device.LastError}";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
var isRecording = device.IsConnected && device.IsTestRunning;
|
|
|
|
|
|
if (!wasRunning && isRecording)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
BeginRun();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
if (isRecording)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
RecordPoint(device);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
if (wasRunning && !isRecording)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
2026-06-02 19:16:50 +08:00
|
|
|
|
if (device.IsConnected)
|
|
|
|
|
|
{
|
|
|
|
|
|
CompleteRun();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
AbortRun("设备离线,本次采集已停止,未生成试验结果");
|
|
|
|
|
|
}
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
wasRunning = isRecording;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 18:49:29 +08:00
|
|
|
|
private void BeginTestButtonFeedback()
|
|
|
|
|
|
{
|
|
|
|
|
|
isTestStartPending = true;
|
|
|
|
|
|
testStartPendingStartedAt = DateTime.UtcNow;
|
|
|
|
|
|
TestButtonText = "测试中";
|
|
|
|
|
|
IsTestButtonEnabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearTestButtonFeedback()
|
|
|
|
|
|
{
|
|
|
|
|
|
isTestStartPending = false;
|
|
|
|
|
|
testStartPendingStartedAt = DateTime.MinValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateTestButtonState(SlipDeviceSnapshot device)
|
|
|
|
|
|
{
|
2026-06-15 10:28:16 +08:00
|
|
|
|
if (isLicenseLockPending)
|
|
|
|
|
|
{
|
|
|
|
|
|
ClearTestButtonFeedback();
|
|
|
|
|
|
TestButtonText = "授权到期";
|
|
|
|
|
|
IsTestButtonEnabled = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 18:49:29 +08:00
|
|
|
|
if (device.IsTestRunning)
|
|
|
|
|
|
{
|
|
|
|
|
|
ClearTestButtonFeedback();
|
|
|
|
|
|
TestButtonText = "测试中";
|
|
|
|
|
|
IsTestButtonEnabled = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isTestStartPending
|
|
|
|
|
|
&& DateTime.UtcNow - testStartPendingStartedAt < TestButtonPendingTimeout)
|
|
|
|
|
|
{
|
|
|
|
|
|
TestButtonText = "测试中";
|
|
|
|
|
|
IsTestButtonEnabled = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ClearTestButtonFeedback();
|
|
|
|
|
|
TestButtonText = "测试";
|
|
|
|
|
|
IsTestButtonEnabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private void CheckRuntimeLicense(SlipDeviceSnapshot device)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (DateTime.UtcNow - lastLicenseCheckAt >= LicenseCheckInterval)
|
|
|
|
|
|
{
|
|
|
|
|
|
var check = licenseService.Check(updateHeartbeat: true);
|
|
|
|
|
|
lastLicenseCheckAt = DateTime.UtcNow;
|
|
|
|
|
|
isLicenseLockPending = !check.CanUseSoftware;
|
|
|
|
|
|
licenseLockMessage = check.Message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isLicenseLockPending
|
|
|
|
|
|
&& !device.IsTestRunning
|
|
|
|
|
|
&& !device.IsResetting
|
|
|
|
|
|
&& !licenseLockRequestedRaised)
|
|
|
|
|
|
{
|
|
|
|
|
|
licenseLockRequestedRaised = true;
|
|
|
|
|
|
LicenseLockRequested?.Invoke(licenseLockMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 09:22:18 +08:00
|
|
|
|
private void BeginResetButtonFeedback()
|
|
|
|
|
|
{
|
|
|
|
|
|
isResetButtonPending = true;
|
|
|
|
|
|
hasObservedResetDeviceBusy = false;
|
|
|
|
|
|
resetButtonPendingStartedAt = DateTime.UtcNow;
|
|
|
|
|
|
ResetButtonText = "复位中";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClearResetButtonFeedback()
|
|
|
|
|
|
{
|
|
|
|
|
|
isResetButtonPending = false;
|
|
|
|
|
|
hasObservedResetDeviceBusy = false;
|
|
|
|
|
|
resetButtonPendingStartedAt = DateTime.MinValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateResetButtonText(SlipDeviceSnapshot device)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (device.IsResetting)
|
|
|
|
|
|
{
|
|
|
|
|
|
hasObservedResetDeviceBusy = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (!device.IsConnected
|
|
|
|
|
|
|| hasObservedResetDeviceBusy
|
|
|
|
|
|
|| (isResetButtonPending && DateTime.UtcNow - resetButtonPendingStartedAt >= ResetButtonPendingTimeout))
|
|
|
|
|
|
{
|
|
|
|
|
|
ClearResetButtonFeedback();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ResetButtonText = device.IsResetting || isResetButtonPending ? "复位中" : "复位";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void BeginRun()
|
|
|
|
|
|
{
|
|
|
|
|
|
currentRun.Clear();
|
2026-06-22 10:16:43 +08:00
|
|
|
|
preTriggerBuffer.Clear();
|
2026-06-02 18:14:01 +08:00
|
|
|
|
verticalLoadPoints.Clear();
|
|
|
|
|
|
horizontalFrictionPoints.Clear();
|
|
|
|
|
|
frictionCoefficientPoints.Clear();
|
|
|
|
|
|
displacementPoints.Clear();
|
2026-06-05 17:41:11 +08:00
|
|
|
|
runStartDisplacementMm = deviceService.CurrentSnapshot.DisplacementMm;
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartDisplacementMm = runStartDisplacementMm;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
slidingStartTimeSeconds = null;
|
2026-06-22 10:16:43 +08:00
|
|
|
|
slidingBaselineFrictionN = 0;
|
|
|
|
|
|
hasSlidingBaseline = false;
|
2026-06-08 18:08:30 +08:00
|
|
|
|
activeRunLubricantIndex = SelectedLubricantIndex;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
runStopwatch.Restart();
|
|
|
|
|
|
UploadProgress = 0;
|
2026-06-05 15:40:48 +08:00
|
|
|
|
lastRealtimeCurveTraceLoggedAt = DateTime.MinValue;
|
2026-06-08 11:38:57 +08:00
|
|
|
|
CurrentStatus = "测试运行:等待检测有效滑动开始,曲线尚未记录";
|
2026-06-05 17:41:11 +08:00
|
|
|
|
Log.Information(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
"测试开始:TestNumber={TestNumber}, TargetLoad={TargetLoad}, TestSpeed={TestSpeed}, StartDisplacement={StartDisplacement:F3}mm, CurveRecording=等待有效滑动开始",
|
2026-06-05 17:41:11 +08:00
|
|
|
|
TestNumber,
|
|
|
|
|
|
TargetLoadText,
|
|
|
|
|
|
TestSpeedText,
|
|
|
|
|
|
runStartDisplacementMm);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void RecordPoint(SlipDeviceSnapshot device)
|
|
|
|
|
|
{
|
2026-06-08 11:38:57 +08:00
|
|
|
|
var elapsedSinceTestStart = runStopwatch.Elapsed.TotalSeconds;
|
2026-06-22 10:16:43 +08:00
|
|
|
|
var samplePoint = new SlipDataPoint(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
device.Timestamp,
|
|
|
|
|
|
elapsedSinceTestStart,
|
|
|
|
|
|
device.VerticalLoadN,
|
|
|
|
|
|
device.HorizontalFrictionN,
|
|
|
|
|
|
device.DisplacementMm,
|
|
|
|
|
|
device.FrictionCoefficient);
|
|
|
|
|
|
|
2026-06-22 10:16:43 +08:00
|
|
|
|
// 滑动起点尚未确定:把样本缓存进滚动预触发缓冲,并尝试检测滑动起点。
|
2026-06-08 11:38:57 +08:00
|
|
|
|
if (!slidingStartTimeSeconds.HasValue)
|
|
|
|
|
|
{
|
2026-06-22 10:16:43 +08:00
|
|
|
|
UpdatePreTrigger(samplePoint);
|
2026-06-08 11:38:57 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 10:16:43 +08:00
|
|
|
|
var standardTime = elapsedSinceTestStart - slidingStartTimeSeconds.Value;
|
|
|
|
|
|
// 只保留滑动开始后 CurveEndSeconds 区间,剔除滑动结束后的回程/抬升段。
|
|
|
|
|
|
if (standardTime > CurveEndSeconds)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-06-08 11:38:57 +08:00
|
|
|
|
if (currentRun.Count > 0 && standardTime - currentRun[^1].TimeSeconds < SampleIntervalSeconds)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var point = new SlipDataPoint(
|
2026-06-22 10:16:43 +08:00
|
|
|
|
source.Timestamp,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
standardTime,
|
2026-06-22 10:16:43 +08:00
|
|
|
|
source.VerticalLoadN,
|
|
|
|
|
|
source.HorizontalFrictionN,
|
|
|
|
|
|
Math.Abs(source.DisplacementMm - slidingStartDisplacementMm),
|
|
|
|
|
|
source.FrictionCoefficient);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
currentRun.Add(point);
|
2026-06-08 11:38:57 +08:00
|
|
|
|
verticalLoadPoints.Add(new ObservablePoint(standardTime, point.VerticalLoadN));
|
|
|
|
|
|
horizontalFrictionPoints.Add(new ObservablePoint(standardTime, point.HorizontalFrictionN));
|
2026-06-08 14:35:37 +08:00
|
|
|
|
UpdateLinearDisplacementSeries(point);
|
2026-06-08 11:38:57 +08:00
|
|
|
|
frictionCoefficientPoints.Add(new ObservablePoint(standardTime, point.FrictionCoefficient));
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
UploadProgress = Math.Min(99, currentRun.Count);
|
2026-06-05 15:40:48 +08:00
|
|
|
|
TraceRealtimeCurvePoint(point);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void CompleteRun()
|
|
|
|
|
|
{
|
|
|
|
|
|
runStopwatch.Stop();
|
2026-06-05 15:40:48 +08:00
|
|
|
|
LogRealtimeCurveSummary("测试停止");
|
2026-06-08 18:08:30 +08:00
|
|
|
|
RecordCompletedTest();
|
2026-06-08 11:38:57 +08:00
|
|
|
|
if (!slidingStartTimeSeconds.HasValue)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
2026-06-08 11:38:57 +08:00
|
|
|
|
Log.Warning(
|
|
|
|
|
|
"测试停止但未检测到有效滑动开始:TestNumber={TestNumber}, StartDisplacement={StartDisplacement:F3}mm, DisplacementThreshold={DisplacementThreshold:F3}mm, MinimumAnalysisLoad={MinimumAnalysisLoad:F3}N, CurvePointCount={CurvePointCount}",
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
runStartDisplacementMm,
|
|
|
|
|
|
SlidingStartDisplacementThresholdMm,
|
|
|
|
|
|
GetMinimumAnalysisLoad(),
|
|
|
|
|
|
currentRun.Count);
|
|
|
|
|
|
CurrentStatus = "测试已停止,但未检测到有效滑动开始,曲线未记录且未生成结果";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:38:57 +08:00
|
|
|
|
if (currentRun.Count < 3)
|
2026-06-05 17:41:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
Log.Warning(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
"测试停止但滑动后采样点不足:TestNumber={TestNumber}, SlidingDetectionDelay={SlidingDetectionDelay:F3}s, PointCount={PointCount}",
|
2026-06-05 17:41:11 +08:00
|
|
|
|
TestNumber,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartTimeSeconds.Value,
|
|
|
|
|
|
currentRun.Count);
|
|
|
|
|
|
CurrentStatus = "测试已停止,但有效采样点不足,未生成结果";
|
2026-06-05 17:41:11 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:38:57 +08:00
|
|
|
|
lastCompletedRun = currentRun.ToList();
|
|
|
|
|
|
var minimumAnalysisLoad = GetMinimumAnalysisLoad();
|
|
|
|
|
|
const double slidingStartTime = 0;
|
|
|
|
|
|
var staticPeak = FindStaticPeak(currentRun);
|
2026-06-05 17:41:11 +08:00
|
|
|
|
var peak = staticPeak.Point;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var dynamicWindow = currentRun
|
2026-06-08 11:38:57 +08:00
|
|
|
|
.Where(IsInDynamicWindow)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
.ToList();
|
2026-06-02 19:16:50 +08:00
|
|
|
|
if (dynamicWindow.Count < MinimumDynamicWindowPointCount)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
2026-06-05 15:40:48 +08:00
|
|
|
|
var firstWindowPoint = dynamicWindow.FirstOrDefault();
|
|
|
|
|
|
var lastWindowPoint = dynamicWindow.LastOrDefault();
|
2026-06-02 19:16:50 +08:00
|
|
|
|
Log.Warning(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
"测试停止但动摩擦窗口采样点不足:TestNumber={TestNumber}, PointCount={PointCount}, SlidingDetectionDelay={SlidingDetectionDelay:F3}s, StandardTimeOrigin={StandardTimeOrigin:F3}s, DynamicWindow=滑动开始后0.300-0.600s, DynamicWindowPointCount={DynamicWindowPointCount}, RequiredPointCount={RequiredPointCount}, ActualWindowStart={ActualWindowStart}, ActualWindowEnd={ActualWindowEnd}, FirstSampleTime={FirstSampleTime:F3}s, LastSampleTime={LastSampleTime:F3}s",
|
2026-06-02 19:16:50 +08:00
|
|
|
|
TestNumber,
|
|
|
|
|
|
currentRun.Count,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartTimeSeconds.Value,
|
2026-06-05 17:41:11 +08:00
|
|
|
|
slidingStartTime,
|
2026-06-05 15:40:48 +08:00
|
|
|
|
dynamicWindow.Count,
|
|
|
|
|
|
MinimumDynamicWindowPointCount,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
FormatStandardTime(firstWindowPoint),
|
|
|
|
|
|
FormatStandardTime(lastWindowPoint),
|
2026-06-05 15:40:48 +08:00
|
|
|
|
currentRun[0].TimeSeconds,
|
|
|
|
|
|
currentRun[^1].TimeSeconds);
|
2026-06-02 19:16:50 +08:00
|
|
|
|
CurrentStatus = "测试已停止,0.3 s~0.6 s 有效采样点不足,未生成结果";
|
|
|
|
|
|
return;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var staticCoefficientValue = CalculateCoefficient(peak.HorizontalFrictionN, peak.VerticalLoadN);
|
|
|
|
|
|
var dynamicForce = dynamicWindow.Average(point => point.HorizontalFrictionN);
|
|
|
|
|
|
var dynamicLoad = dynamicWindow.Average(point => point.VerticalLoadN);
|
2026-06-05 17:41:11 +08:00
|
|
|
|
if (peak.VerticalLoadN < minimumAnalysisLoad || dynamicLoad < minimumAnalysisLoad)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
"测试停止但静/动摩擦窗口载荷不足:TestNumber={TestNumber}, SlidingDetectionDelay={SlidingDetectionDelay:F3}s, MinimumAnalysisLoad={MinimumAnalysisLoad:F3}N, StaticLoad={StaticLoad:F3}N, DynamicAvgLoad={DynamicAvgLoad:F3}N, StaticPoint={StaticPoint}, DynamicPointCount={DynamicPointCount}",
|
2026-06-05 17:41:11 +08:00
|
|
|
|
TestNumber,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartTimeSeconds.Value,
|
2026-06-05 17:41:11 +08:00
|
|
|
|
minimumAnalysisLoad,
|
|
|
|
|
|
peak.VerticalLoadN,
|
|
|
|
|
|
dynamicLoad,
|
|
|
|
|
|
FormatDataPoint(peak),
|
|
|
|
|
|
dynamicWindow.Count);
|
|
|
|
|
|
CurrentStatus = "测试已停止,静/动摩擦窗口载荷不足,未生成结果";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var dynamicCoefficientValue = CalculateCoefficient(dynamicForce, dynamicLoad);
|
|
|
|
|
|
var verdict = NeedsRetest(staticCoefficientValue, dynamicCoefficientValue) ? "需重测" : "有效";
|
|
|
|
|
|
var nextIndex = Samples.Count == 0 ? 1 : Samples.Max(sample => sample.Index) + 1;
|
2026-06-05 15:40:48 +08:00
|
|
|
|
var peakIndex = currentRun.IndexOf(peak) + 1;
|
2026-06-08 11:38:57 +08:00
|
|
|
|
var dynamicStart = dynamicWindow[0].TimeSeconds;
|
|
|
|
|
|
var dynamicEnd = dynamicWindow[^1].TimeSeconds;
|
2026-06-05 15:40:48 +08:00
|
|
|
|
|
|
|
|
|
|
Log.Information(
|
2026-06-08 11:38:57 +08:00
|
|
|
|
"静/动摩擦计算明细:TestNumber={TestNumber}, PointCount={PointCount}, SlidingDetectionDelay={SlidingDetectionDelay:F3}s, StandardTimeOrigin={StandardTimeOrigin:F3}s, AbsoluteSlidingStartDisplacement={AbsoluteSlidingStartDisplacement:F3}mm, StaticSearchWindow=滑动开始后首个峰值0.000-{StaticSearchEnd:F3}s, StaticPeakMode={StaticPeakMode}, StaticPointIndex={StaticPointIndex}, StaticTime={StaticTime:F3}s, StaticFriction={StaticFriction:F3}N, StaticLoad={StaticLoad:F3}N, StaticCoefficient={StaticCoefficient:F5}, DynamicWindow=滑动开始后{DynamicWindowStart:F3}-{DynamicWindowEnd:F3}s, DynamicActualWindow={DynamicActualStart:F3}-{DynamicActualEnd:F3}s, DynamicPointCount={DynamicPointCount}, DynamicAvgFriction={DynamicAvgFriction:F3}N, DynamicAvgLoad={DynamicAvgLoad:F3}N, DynamicCoefficient={DynamicCoefficient:F5}",
|
2026-06-05 15:40:48 +08:00
|
|
|
|
TestNumber,
|
|
|
|
|
|
currentRun.Count,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartTimeSeconds.Value,
|
2026-06-05 17:41:11 +08:00
|
|
|
|
slidingStartTime,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
slidingStartDisplacementMm,
|
2026-06-05 17:41:11 +08:00
|
|
|
|
StaticPeakSearchEndSeconds,
|
|
|
|
|
|
staticPeak.Mode,
|
2026-06-05 15:40:48 +08:00
|
|
|
|
peakIndex,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
peak.TimeSeconds,
|
2026-06-05 15:40:48 +08:00
|
|
|
|
peak.HorizontalFrictionN,
|
|
|
|
|
|
peak.VerticalLoadN,
|
|
|
|
|
|
staticCoefficientValue,
|
|
|
|
|
|
DynamicWindowStartSeconds,
|
|
|
|
|
|
DynamicWindowEndSeconds,
|
|
|
|
|
|
dynamicStart,
|
|
|
|
|
|
dynamicEnd,
|
|
|
|
|
|
dynamicWindow.Count,
|
|
|
|
|
|
dynamicForce,
|
|
|
|
|
|
dynamicLoad,
|
|
|
|
|
|
dynamicCoefficientValue);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
|
|
|
|
|
|
Samples.Insert(0, new TestSample(
|
|
|
|
|
|
nextIndex,
|
|
|
|
|
|
DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
|
2026-06-02 19:16:50 +08:00
|
|
|
|
staticCoefficientValue.ToString("F2", CultureInfo.InvariantCulture),
|
|
|
|
|
|
dynamicCoefficientValue.ToString("F2", CultureInfo.InvariantCulture),
|
2026-06-02 18:14:01 +08:00
|
|
|
|
verdict,
|
|
|
|
|
|
staticCoefficientValue,
|
|
|
|
|
|
dynamicCoefficientValue));
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
StaticCoefficient = staticCoefficientValue.ToString("F2", CultureInfo.InvariantCulture);
|
|
|
|
|
|
DynamicCoefficient = dynamicCoefficientValue.ToString("F2", CultureInfo.InvariantCulture);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
UpdateResultSummary();
|
|
|
|
|
|
UploadProgress = 100;
|
|
|
|
|
|
CurrentStatus = verdict == "有效"
|
|
|
|
|
|
? "测试完成:已按标准生成静/动摩擦系数"
|
|
|
|
|
|
: "测试完成:最近三次结果差异超过 10%,建议重新测试";
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Information(
|
|
|
|
|
|
"测试完成:TestNumber={TestNumber}, PointCount={PointCount}, StaticCoefficient={StaticCoefficient:F3}, DynamicCoefficient={DynamicCoefficient:F3}, Verdict={Verdict}",
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
currentRun.Count,
|
|
|
|
|
|
staticCoefficientValue,
|
|
|
|
|
|
dynamicCoefficientValue,
|
|
|
|
|
|
verdict);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 18:08:30 +08:00
|
|
|
|
private void RecordCompletedTest()
|
|
|
|
|
|
{
|
|
|
|
|
|
CompletedTestCount++;
|
|
|
|
|
|
switch (activeRunLubricantIndex)
|
|
|
|
|
|
{
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
WetTestCount++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
FrostTestCount++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
DryTestCount++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.Information(
|
|
|
|
|
|
"完整测试次数已更新:TestNumber={TestNumber}, Lubricant={Lubricant}, CompletedTestCount={CompletedTestCount}, DryTestCount={DryTestCount}, WetTestCount={WetTestCount}, FrostTestCount={FrostTestCount}",
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
CurrentLubricant(activeRunLubricantIndex),
|
|
|
|
|
|
CompletedTestCount,
|
|
|
|
|
|
DryTestCount,
|
|
|
|
|
|
WetTestCount,
|
|
|
|
|
|
FrostTestCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private void AbortRun(string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
runStopwatch.Stop();
|
|
|
|
|
|
Log.Warning(
|
|
|
|
|
|
"测试采集中止:TestNumber={TestNumber}, PointCount={PointCount}, Message={Message}",
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
currentRun.Count,
|
|
|
|
|
|
message);
|
|
|
|
|
|
CurrentStatus = message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue)
|
|
|
|
|
|
{
|
2026-06-02 19:16:50 +08:00
|
|
|
|
var previous = Samples.Take(StandardTrialCount - 1).Reverse().ToList();
|
|
|
|
|
|
if (previous.Count < StandardTrialCount - 1)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
var staticValues = previous.Select(sample => sample.StaticCoefficientValue).Append(staticCoefficientValue).ToArray();
|
|
|
|
|
|
var dynamicValues = previous.Select(sample => sample.DynamicCoefficientValue).Append(dynamicCoefficientValue).ToArray();
|
|
|
|
|
|
return HasSystematicTrendExceedingTenPercent(staticValues) || HasSystematicTrendExceedingTenPercent(dynamicValues);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
private static bool HasSystematicTrendExceedingTenPercent(double[] values)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
2026-06-02 19:16:50 +08:00
|
|
|
|
var isIncreasing = values.Zip(values.Skip(1), (left, right) => right > left).All(result => result);
|
|
|
|
|
|
var isDecreasing = values.Zip(values.Skip(1), (left, right) => right < left).All(result => result);
|
|
|
|
|
|
if (!isIncreasing && !isDecreasing)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var average = values.Average();
|
|
|
|
|
|
if (Math.Abs(average) < 0.0001)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (values.Max() - values.Min()) / Math.Abs(average) > 0.10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateResultSummary()
|
|
|
|
|
|
{
|
2026-06-02 19:16:50 +08:00
|
|
|
|
var latest = Samples.Take(StandardTrialCount).ToList();
|
2026-06-02 18:14:01 +08:00
|
|
|
|
if (latest.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultSummary = "等待 3 次有效试验";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 19:16:50 +08:00
|
|
|
|
if (latest.Count < StandardTrialCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultSummary = $"已完成 {latest.Count}/3 次有效试验";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var staticAverage = latest.Average(sample => sample.StaticCoefficientValue);
|
|
|
|
|
|
var dynamicAverage = latest.Average(sample => sample.DynamicCoefficientValue);
|
2026-06-02 19:16:50 +08:00
|
|
|
|
ResultSummary = $"近 3 次平均 静 {staticAverage:F2} / 动 {dynamicAverage:F2}";
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private double GetMinimumAnalysisLoad() =>
|
|
|
|
|
|
TryParseLoadValue(TargetLoadText, out var targetLoad)
|
|
|
|
|
|
? Math.Max(1, targetLoad * MinimumAnalysisLoadRatio)
|
|
|
|
|
|
: 1;
|
|
|
|
|
|
|
|
|
|
|
|
private static bool TryParseLoadValue(string value, out double load)
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
2026-06-05 17:41:11 +08:00
|
|
|
|
var numeric = new string(value
|
|
|
|
|
|
.Where(character => char.IsDigit(character) || character is '.' or '-' or '+')
|
|
|
|
|
|
.ToArray());
|
|
|
|
|
|
return double.TryParse(numeric, NumberStyles.Float, CultureInfo.InvariantCulture, out load);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 11:38:57 +08:00
|
|
|
|
private static StaticPeakSelection FindStaticPeak(IReadOnlyList<SlipDataPoint> points)
|
2026-06-05 17:41:11 +08:00
|
|
|
|
{
|
|
|
|
|
|
var searchWindow = points
|
2026-06-08 11:38:57 +08:00
|
|
|
|
.Where(point => point.TimeSeconds >= 0
|
|
|
|
|
|
&& point.TimeSeconds <= StaticPeakSearchEndSeconds)
|
2026-06-02 19:16:50 +08:00
|
|
|
|
.ToList();
|
2026-06-05 17:41:11 +08:00
|
|
|
|
if (searchWindow.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new StaticPeakSelection(points[0], "NoStaticWindowFallback");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var firstFriction = searchWindow[0].HorizontalFrictionN;
|
|
|
|
|
|
var peakIndex = 0;
|
|
|
|
|
|
for (var index = 1; index < searchWindow.Count; index++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (searchWindow[index].HorizontalFrictionN > searchWindow[peakIndex].HorizontalFrictionN)
|
|
|
|
|
|
{
|
|
|
|
|
|
peakIndex = index;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var peak = searchWindow[peakIndex];
|
|
|
|
|
|
var hasMeaningfulRise = peak.HorizontalFrictionN - firstFriction >= StaticPeakMinimumRiseN;
|
|
|
|
|
|
if (!hasMeaningfulRise)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var confirmationEnd = Math.Min(
|
|
|
|
|
|
searchWindow.Count - 1,
|
|
|
|
|
|
index + StaticPeakDropConfirmationPointCount - 1);
|
|
|
|
|
|
if (confirmationEnd - index + 1 < StaticPeakDropConfirmationPointCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var isConfirmedFalling = true;
|
|
|
|
|
|
for (var confirmationIndex = index; confirmationIndex <= confirmationEnd; confirmationIndex++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (searchWindow[confirmationIndex].HorizontalFrictionN > peak.HorizontalFrictionN - StaticPeakDropToleranceN)
|
|
|
|
|
|
{
|
|
|
|
|
|
isConfirmedFalling = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isConfirmedFalling)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new StaticPeakSelection(peak, "FirstConfirmedPeak");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new StaticPeakSelection(
|
|
|
|
|
|
searchWindow.MaxBy(point => point.HorizontalFrictionN) ?? searchWindow[0],
|
|
|
|
|
|
"MaxFallback");
|
2026-06-02 18:14:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 17:41:11 +08:00
|
|
|
|
private sealed record StaticPeakSelection(SlipDataPoint Point, string Mode);
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private static double CalculateCoefficient(double frictionForce, double verticalLoad) =>
|
|
|
|
|
|
Math.Abs(verticalLoad) > 0.0001 ? frictionForce / verticalLoad : 0;
|
|
|
|
|
|
|
2026-06-05 15:40:48 +08:00
|
|
|
|
private void LogRealtimeCurveSummary(string stage)
|
|
|
|
|
|
{
|
|
|
|
|
|
var expectedCount = currentRun.Count;
|
2026-06-08 14:35:37 +08:00
|
|
|
|
var lastPoint = currentRun.Count > 0 ? currentRun[^1] : null;
|
2026-06-05 15:40:48 +08:00
|
|
|
|
var countsMatch =
|
|
|
|
|
|
verticalLoadPoints.Count == expectedCount
|
|
|
|
|
|
&& horizontalFrictionPoints.Count == expectedCount
|
|
|
|
|
|
&& frictionCoefficientPoints.Count == expectedCount
|
2026-06-08 14:35:37 +08:00
|
|
|
|
&& IsLinearDisplacementSeriesSynchronized(lastPoint);
|
2026-06-05 15:40:48 +08:00
|
|
|
|
|
|
|
|
|
|
Log.Information(
|
2026-06-08 14:35:37 +08:00
|
|
|
|
"实时曲线同步汇总:Stage={Stage}, TestNumber={TestNumber}, DataPointCount={DataPointCount}, ChartCounts=[Vertical:{VerticalCount}, Friction:{FrictionCount}, Coefficient:{CoefficientCount}, DisplacementLine:{DisplacementCount}], DisplacementMode=零点到真实端点直线, CountsMatch={CountsMatch}, LastDataPoint={LastDataPoint}, ChartLast=[Vertical:{VerticalChart}, Friction:{FrictionChart}, Coefficient:{CoefficientChart}, DisplacementEndpoint:{DisplacementChart}]",
|
2026-06-05 15:40:48 +08:00
|
|
|
|
stage,
|
|
|
|
|
|
TestNumber,
|
|
|
|
|
|
expectedCount,
|
|
|
|
|
|
verticalLoadPoints.Count,
|
|
|
|
|
|
horizontalFrictionPoints.Count,
|
|
|
|
|
|
frictionCoefficientPoints.Count,
|
|
|
|
|
|
displacementPoints.Count,
|
|
|
|
|
|
countsMatch,
|
|
|
|
|
|
FormatDataPoint(lastPoint),
|
|
|
|
|
|
FormatChartPoint(verticalLoadPoints.Count > 0 ? verticalLoadPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(horizontalFrictionPoints.Count > 0 ? horizontalFrictionPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(frictionCoefficientPoints.Count > 0 ? frictionCoefficientPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(displacementPoints.Count > 0 ? displacementPoints[^1] : null));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void TraceRealtimeCurvePoint(SlipDataPoint point)
|
|
|
|
|
|
{
|
|
|
|
|
|
var expectedCount = currentRun.Count;
|
|
|
|
|
|
var countsMatch =
|
|
|
|
|
|
verticalLoadPoints.Count == expectedCount
|
|
|
|
|
|
&& horizontalFrictionPoints.Count == expectedCount
|
|
|
|
|
|
&& frictionCoefficientPoints.Count == expectedCount
|
2026-06-08 14:35:37 +08:00
|
|
|
|
&& IsLinearDisplacementSeriesSynchronized(point);
|
2026-06-05 15:40:48 +08:00
|
|
|
|
var valuesMatch =
|
|
|
|
|
|
countsMatch
|
|
|
|
|
|
&& ChartPointMatches(verticalLoadPoints[^1], point.TimeSeconds, point.VerticalLoadN)
|
|
|
|
|
|
&& ChartPointMatches(horizontalFrictionPoints[^1], point.TimeSeconds, point.HorizontalFrictionN)
|
|
|
|
|
|
&& ChartPointMatches(frictionCoefficientPoints[^1], point.TimeSeconds, point.FrictionCoefficient)
|
|
|
|
|
|
&& ChartPointMatches(displacementPoints[^1], point.TimeSeconds, point.DisplacementMm);
|
|
|
|
|
|
|
|
|
|
|
|
if (!countsMatch || !valuesMatch)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning(
|
2026-06-08 14:35:37 +08:00
|
|
|
|
"实时曲线数据不同步:TestNumber={TestNumber}, PointIndex={PointIndex}, ExpectedCount={ExpectedCount}, Counts=[Vertical:{VerticalCount}, Friction:{FrictionCount}, Coefficient:{CoefficientCount}, DisplacementLine:{DisplacementCount}], DisplacementMode=零点到真实端点直线, ValuesMatch={ValuesMatch}, DataPoint=[Time:{Time:F3}s, Vertical:{Vertical:F3}N, Friction:{Friction:F3}N, Coefficient:{Coefficient:F5}, Displacement:{Displacement:F3}mm], ChartLast=[Vertical:{VerticalChart}, Friction:{FrictionChart}, Coefficient:{CoefficientChart}, DisplacementEndpoint:{DisplacementChart}]",
|
2026-06-05 15:40:48 +08:00
|
|
|
|
TestNumber,
|
|
|
|
|
|
expectedCount,
|
|
|
|
|
|
expectedCount,
|
|
|
|
|
|
verticalLoadPoints.Count,
|
|
|
|
|
|
horizontalFrictionPoints.Count,
|
|
|
|
|
|
frictionCoefficientPoints.Count,
|
|
|
|
|
|
displacementPoints.Count,
|
|
|
|
|
|
valuesMatch,
|
|
|
|
|
|
point.TimeSeconds,
|
|
|
|
|
|
point.VerticalLoadN,
|
|
|
|
|
|
point.HorizontalFrictionN,
|
|
|
|
|
|
point.FrictionCoefficient,
|
|
|
|
|
|
point.DisplacementMm,
|
|
|
|
|
|
FormatChartPoint(verticalLoadPoints.Count > 0 ? verticalLoadPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(horizontalFrictionPoints.Count > 0 ? horizontalFrictionPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(frictionCoefficientPoints.Count > 0 ? frictionCoefficientPoints[^1] : null),
|
|
|
|
|
|
FormatChartPoint(displacementPoints.Count > 0 ? displacementPoints[^1] : null));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
|
if (expectedCount > 3 && now - lastRealtimeCurveTraceLoggedAt < RealtimeCurveTraceInterval)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastRealtimeCurveTraceLoggedAt = now;
|
|
|
|
|
|
Log.Debug(
|
2026-06-05 17:41:11 +08:00
|
|
|
|
"实时曲线点同步:TestNumber={TestNumber}, PointIndex={PointIndex}, Time={Time:F3}s, StandardTime={StandardTime}, Vertical={Vertical:F3}N, Friction={Friction:F3}N, Coefficient={Coefficient:F5}, Displacement={Displacement:F3}mm, InDynamicWindow={InDynamicWindow}, ChartCount={ChartCount}",
|
2026-06-05 15:40:48 +08:00
|
|
|
|
TestNumber,
|
|
|
|
|
|
expectedCount,
|
|
|
|
|
|
point.TimeSeconds,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
FormatStandardTime(point),
|
2026-06-05 15:40:48 +08:00
|
|
|
|
point.VerticalLoadN,
|
|
|
|
|
|
point.HorizontalFrictionN,
|
|
|
|
|
|
point.FrictionCoefficient,
|
|
|
|
|
|
point.DisplacementMm,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
IsInDynamicWindow(point),
|
2026-06-05 15:40:48 +08:00
|
|
|
|
expectedCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-08 14:35:37 +08:00
|
|
|
|
private void UpdateLinearDisplacementSeries(SlipDataPoint endpoint)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (displacementPoints.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
displacementPoints.Add(new ObservablePoint(0, 0));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (endpoint.TimeSeconds <= 0.000001)
|
|
|
|
|
|
{
|
|
|
|
|
|
while (displacementPoints.Count > 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
displacementPoints.RemoveAt(displacementPoints.Count - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var endpointPoint = new ObservablePoint(endpoint.TimeSeconds, endpoint.DisplacementMm);
|
|
|
|
|
|
if (displacementPoints.Count == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
displacementPoints.Add(endpointPoint);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
displacementPoints[1] = endpointPoint;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
while (displacementPoints.Count > 2)
|
|
|
|
|
|
{
|
|
|
|
|
|
displacementPoints.RemoveAt(displacementPoints.Count - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool IsLinearDisplacementSeriesSynchronized(SlipDataPoint? endpoint)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (endpoint is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return displacementPoints.Count == 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var expectedCount = endpoint.TimeSeconds <= 0.000001 ? 1 : 2;
|
|
|
|
|
|
return displacementPoints.Count == expectedCount
|
|
|
|
|
|
&& ChartPointMatches(displacementPoints[0], 0, 0)
|
|
|
|
|
|
&& (expectedCount == 1
|
|
|
|
|
|
|| ChartPointMatches(displacementPoints[^1], endpoint.TimeSeconds, endpoint.DisplacementMm));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 15:40:48 +08:00
|
|
|
|
private static bool ChartPointMatches(ObservablePoint chartPoint, double expectedX, double expectedY) =>
|
|
|
|
|
|
NearlyEquals(chartPoint.X, expectedX) && NearlyEquals(chartPoint.Y, expectedY);
|
|
|
|
|
|
|
|
|
|
|
|
private static bool NearlyEquals(double? actual, double expected) =>
|
|
|
|
|
|
actual.HasValue && Math.Abs(actual.Value - expected) < 0.000001;
|
|
|
|
|
|
|
|
|
|
|
|
private static string FormatChartPoint(ObservablePoint? point) =>
|
|
|
|
|
|
point is null
|
|
|
|
|
|
? "null"
|
|
|
|
|
|
: $"({FormatNullable(point.X)},{FormatNullable(point.Y)})";
|
|
|
|
|
|
|
|
|
|
|
|
private static string FormatNullable(double? value) =>
|
|
|
|
|
|
value.HasValue ? value.Value.ToString("F3", CultureInfo.InvariantCulture) : "null";
|
|
|
|
|
|
|
2026-06-08 11:38:57 +08:00
|
|
|
|
private static string FormatStandardTime(SlipDataPoint? point) =>
|
|
|
|
|
|
point is null ? "null" : point.TimeSeconds.ToString("F3", CultureInfo.InvariantCulture);
|
2026-06-05 17:41:11 +08:00
|
|
|
|
|
2026-06-08 11:38:57 +08:00
|
|
|
|
private static bool IsInDynamicWindow(SlipDataPoint point) =>
|
|
|
|
|
|
point.TimeSeconds >= DynamicWindowStartSeconds && point.TimeSeconds <= DynamicWindowEndSeconds;
|
2026-06-05 17:41:11 +08:00
|
|
|
|
|
2026-06-05 15:40:48 +08:00
|
|
|
|
private static string FormatDataPoint(SlipDataPoint? point) =>
|
|
|
|
|
|
point is null
|
|
|
|
|
|
? "null"
|
2026-06-08 11:38:57 +08:00
|
|
|
|
: $"StandardTime={point.TimeSeconds.ToString("F3", CultureInfo.InvariantCulture)}s, Vertical={point.VerticalLoadN.ToString("F3", CultureInfo.InvariantCulture)}N, Friction={point.HorizontalFrictionN.ToString("F3", CultureInfo.InvariantCulture)}N, Coefficient={point.FrictionCoefficient.ToString("F5", CultureInfo.InvariantCulture)}, SlidingDisplacement={point.DisplacementMm.ToString("F3", CultureInfo.InvariantCulture)}mm";
|
2026-06-05 15:40:48 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task RunDeviceCommand(Task command, string successMessage)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await command;
|
|
|
|
|
|
CurrentStatus = successMessage;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Error(ex, "设备指令失败:{SuccessMessage}", successMessage);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
CurrentStatus = $"设备指令失败:{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 10:28:16 +08:00
|
|
|
|
private Task RunLicensedDeviceCommand(Func<Task> command, string successMessage)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isLicenseLockPending)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentStatus = "软件使用时效已到,仅允许停止或复位设备。";
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return RunDeviceCommand(command(), successMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void WriteNumericSetting(string value, Func<double, Task> writer, string label)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isLoadingDeviceSettings || !TryParseDouble(value, out var numericValue))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_ = RunDeviceCommand(writer(numericValue), $"{label}已写入 PLC");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SaveAndApplySettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
SaveDeviceSettings();
|
|
|
|
|
|
deviceService.UpdateSettings(CurrentSettings());
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private void ApplyConnectionSettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
var settings = CurrentSettings();
|
|
|
|
|
|
if (ConnectionSettingsChanged(settings))
|
|
|
|
|
|
{
|
|
|
|
|
|
RestartDeviceConnection(settings);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deviceService.UpdateSettings(settings);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool ConnectionSettingsChanged(DeviceSettings settings) =>
|
|
|
|
|
|
!string.Equals(activePlcPortName, settings.PlcPortName, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
|| !string.Equals(activeAdcPortName, settings.AdcPortName, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
|| activeBaudRate != settings.BaudRate;
|
|
|
|
|
|
|
|
|
|
|
|
private void RestartDeviceConnection()
|
|
|
|
|
|
{
|
|
|
|
|
|
RestartDeviceConnection(CurrentSettings());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void RestartDeviceConnection(DeviceSettings settings)
|
|
|
|
|
|
{
|
|
|
|
|
|
deviceService.Start(settings);
|
|
|
|
|
|
activePlcPortName = settings.PlcPortName;
|
|
|
|
|
|
activeAdcPortName = settings.AdcPortName;
|
|
|
|
|
|
activeBaudRate = settings.BaudRate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool ValidateAdcCoefficientsBeforeTest()
|
|
|
|
|
|
{
|
|
|
|
|
|
var invalid = new List<string>();
|
|
|
|
|
|
AddInvalidCoefficient(NormalPressureCoefficient, "正压力校准系数", invalid);
|
|
|
|
|
|
AddInvalidCoefficient(FrictionCoefficient1, "摩擦1校准系数", invalid);
|
|
|
|
|
|
AddInvalidCoefficient(FrictionCoefficient2, "摩擦2校准系数", invalid);
|
|
|
|
|
|
|
|
|
|
|
|
if (invalid.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var message = "ADC 校准系数无效:" + string.Join("、", invalid) + ";请填写旧机实际标定系数后再开始测试";
|
|
|
|
|
|
Log.Warning("阻止测试启动:{Message}", message);
|
|
|
|
|
|
CurrentStatus = message;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:45:14 +08:00
|
|
|
|
private async Task LoadPlcParametersAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var parameters = await deviceService.ReadDeviceParametersAsync();
|
|
|
|
|
|
isLoadingDeviceSettings = true;
|
|
|
|
|
|
ManualSpeed = parameters.ManualSpeed.ToString("F2", CultureInfo.InvariantCulture);
|
|
|
|
|
|
ManualDisplacement = parameters.ManualDisplacement.ToString("F2", CultureInfo.InvariantCulture);
|
|
|
|
|
|
TestSpeed = parameters.TestSpeed.ToString("F2", CultureInfo.InvariantCulture);
|
|
|
|
|
|
isLoadingDeviceSettings = false;
|
|
|
|
|
|
SaveAndApplySettings();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
isLoadingDeviceSettings = false;
|
|
|
|
|
|
Log.Warning(ex, "启动时读取 PLC 参数失败,将使用本地保存的设备设置");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
private void LoadDeviceSettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!File.Exists(DeviceSettingsPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var json = File.ReadAllText(DeviceSettingsPath);
|
|
|
|
|
|
var settings = JsonSerializer.Deserialize<DeviceSettings>(json);
|
|
|
|
|
|
if (settings is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isLoadingDeviceSettings = true;
|
|
|
|
|
|
ManualSpeed = settings.ManualSpeed ?? ManualSpeed;
|
|
|
|
|
|
ManualDisplacement = settings.ManualDisplacement ?? ManualDisplacement;
|
|
|
|
|
|
TestSpeed = settings.TestSpeed ?? TestSpeed;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
NormalPressureZero = settings.NormalPressureZero ?? NormalPressureZero;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
NormalPressureCoefficient = settings.NormalPressureCoefficient ?? NormalPressureCoefficient;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
FrictionZero1 = settings.FrictionZero1 ?? FrictionZero1;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
FrictionCoefficient1 = settings.FrictionCoefficient1 ?? FrictionCoefficient1;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
FrictionZero2 = settings.FrictionZero2 ?? FrictionZero2;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
FrictionCoefficient2 = settings.FrictionCoefficient2 ?? FrictionCoefficient2;
|
2026-06-03 15:35:55 +08:00
|
|
|
|
PlcPortName = NormalizeSavedPort(settings.PlcPortName, DefaultPlcPortName, LegacyPlcPortName);
|
|
|
|
|
|
AdcPortName = NormalizeSavedPort(settings.AdcPortName, DefaultAdcPortName, LegacyAdcPortName);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
2026-06-02 18:45:14 +08:00
|
|
|
|
catch (Exception ex)
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Warning(ex, "读取设备设置失败:Path={Path}", DeviceSettingsPath);
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
isLoadingDeviceSettings = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SaveDeviceSettings()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isLoadingDeviceSettings)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(DeviceSettingsPath)!);
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var json = JsonSerializer.Serialize(CurrentSettings(), new JsonSerializerOptions { WriteIndented = true });
|
2026-06-02 17:41:53 +08:00
|
|
|
|
File.WriteAllText(DeviceSettingsPath, json);
|
|
|
|
|
|
}
|
2026-06-02 18:45:14 +08:00
|
|
|
|
catch (Exception ex)
|
2026-06-02 17:41:53 +08:00
|
|
|
|
{
|
2026-06-02 18:45:14 +08:00
|
|
|
|
Log.Warning(ex, "保存设备设置失败:Path={Path}", DeviceSettingsPath);
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private DeviceSettings CurrentSettings() =>
|
|
|
|
|
|
new(
|
|
|
|
|
|
ManualSpeed,
|
|
|
|
|
|
ManualDisplacement,
|
|
|
|
|
|
TestSpeed,
|
|
|
|
|
|
NormalPressureZero,
|
|
|
|
|
|
NormalPressureCoefficient,
|
|
|
|
|
|
FrictionZero1,
|
|
|
|
|
|
FrictionCoefficient1,
|
|
|
|
|
|
FrictionZero2,
|
|
|
|
|
|
FrictionCoefficient2,
|
|
|
|
|
|
PlcPortName,
|
|
|
|
|
|
AdcPortName,
|
|
|
|
|
|
BaudRate);
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateTargetLoad()
|
|
|
|
|
|
{
|
|
|
|
|
|
TargetLoadText = SelectedShoeSizeIndex switch
|
|
|
|
|
|
{
|
|
|
|
|
|
1 => "350 N",
|
|
|
|
|
|
2 => "160 N",
|
|
|
|
|
|
_ => "400 N"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private string CurrentShoeSize() => SelectedShoeSizeIndex switch
|
|
|
|
|
|
{
|
|
|
|
|
|
1 => "205-250",
|
|
|
|
|
|
2 => "205以下",
|
|
|
|
|
|
_ => "250(含)以上"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-08 18:08:30 +08:00
|
|
|
|
private string CurrentLubricant() => CurrentLubricant(SelectedLubricantIndex);
|
|
|
|
|
|
|
|
|
|
|
|
private static string CurrentLubricant(int lubricantIndex) => lubricantIndex switch
|
2026-06-02 18:14:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
1 => "湿态 - 蒸馏水",
|
|
|
|
|
|
2 => "湿态 - 洗涤剂",
|
|
|
|
|
|
3 => "冰霜",
|
|
|
|
|
|
_ => "干态"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private string CurrentMode() => SelectedModeIndex switch
|
|
|
|
|
|
{
|
|
|
|
|
|
0 => "后跟测试模式",
|
|
|
|
|
|
1 => "前掌测试模式",
|
|
|
|
|
|
_ => "水平测试模式"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private string CurrentSurface() => SelectedSurfaceIndex switch
|
|
|
|
|
|
{
|
|
|
|
|
|
1 => "木地板",
|
|
|
|
|
|
2 => "石材",
|
|
|
|
|
|
3 => "冰霜表面",
|
|
|
|
|
|
_ => "瓷砖"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private static bool TryParseDouble(string value, out double numericValue) =>
|
|
|
|
|
|
double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out numericValue)
|
|
|
|
|
|
|| double.TryParse(value, NumberStyles.Float, CultureInfo.CurrentCulture, out numericValue);
|
|
|
|
|
|
|
2026-06-03 15:35:55 +08:00
|
|
|
|
private static void AddInvalidCoefficient(string value, string label, List<string> invalid)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!TryParseDouble(value, out var coefficient) || Math.Abs(coefficient) < 0.0001)
|
|
|
|
|
|
{
|
|
|
|
|
|
invalid.Add($"{label}={value}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsAdcCalibrationError(string error) =>
|
|
|
|
|
|
error.Contains("ADC 校准参数无效", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
|
|
|
|
private static string NormalizeSavedPort(string? savedPort, string defaultPort, string legacyPort)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(savedPort) || string.Equals(savedPort, legacyPort, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
return defaultPort;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return savedPort;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private static LineSeries<ObservablePoint> CreateLineSeries(
|
|
|
|
|
|
string name,
|
|
|
|
|
|
ObservableCollection<ObservablePoint> values,
|
|
|
|
|
|
string color,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
int yAxis) =>
|
2026-06-02 18:14:01 +08:00
|
|
|
|
new()
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = name,
|
|
|
|
|
|
Values = values,
|
|
|
|
|
|
MiniatureShapeSize = 8,
|
2026-06-05 18:10:30 +08:00
|
|
|
|
MiniatureStrokeThickness = 1.6f,
|
|
|
|
|
|
Stroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 1.7f },
|
2026-06-02 18:14:01 +08:00
|
|
|
|
Fill = new SolidColorPaint(SKColors.Transparent),
|
2026-06-05 18:10:30 +08:00
|
|
|
|
GeometryFill = null,
|
|
|
|
|
|
GeometryStroke = null,
|
|
|
|
|
|
GeometrySize = 0,
|
2026-06-08 11:38:57 +08:00
|
|
|
|
LineSmoothness = 0,
|
2026-06-02 18:14:01 +08:00
|
|
|
|
ScalesYAt = yAxis
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
private static string DeviceSettingsPath =>
|
|
|
|
|
|
Path.Combine(
|
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
|
"FootwearSlipResistance",
|
|
|
|
|
|
"device-settings.json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|