862 lines
32 KiB
C#
862 lines
32 KiB
C#
using Avalonia.Threading;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Models;
|
||
using Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.Services;
|
||
using LiveChartsCore;
|
||
using LiveChartsCore.Defaults;
|
||
using LiveChartsCore.SkiaSharpView;
|
||
using LiveChartsCore.SkiaSharpView.Painting;
|
||
using Serilog;
|
||
using SkiaSharp;
|
||
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;
|
||
|
||
namespace Footwear_Test_methodsfor_wholeshoe_Slipresistanceperformance.ViewModels
|
||
{
|
||
public partial class MainWindowViewModel : ViewModelBase, IDisposable
|
||
{
|
||
private const double SampleIntervalSeconds = 1.0 / 50.0;
|
||
private const double DynamicWindowStartSeconds = 0.3;
|
||
private const double DynamicWindowEndSeconds = 0.6;
|
||
private const int MinimumDynamicWindowPointCount = 10;
|
||
private const int StandardTrialCount = 3;
|
||
|
||
private readonly SlipResistanceDeviceService deviceService = new();
|
||
private readonly SlipExcelExportService excelExportService = new();
|
||
private readonly DispatcherTimer refreshTimer;
|
||
private readonly Stopwatch runStopwatch = new();
|
||
private readonly List<SlipDataPoint> currentRun = [];
|
||
private readonly ObservableCollection<ObservablePoint> verticalLoadPoints = [];
|
||
private readonly ObservableCollection<ObservablePoint> horizontalFrictionPoints = [];
|
||
private readonly ObservableCollection<ObservablePoint> frictionCoefficientPoints = [];
|
||
private readonly ObservableCollection<ObservablePoint> displacementPoints = [];
|
||
|
||
private bool isLoadingDeviceSettings;
|
||
private bool wasRunning;
|
||
private List<SlipDataPoint> lastCompletedRun = [];
|
||
|
||
[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]
|
||
private string currentStatus = "设备连接中,等待 PLC 与 ADC 实时数据";
|
||
|
||
[ObservableProperty]
|
||
private int uploadProgress;
|
||
|
||
[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";
|
||
|
||
[ObservableProperty]
|
||
private string normalPressureZero = "0";
|
||
|
||
[ObservableProperty]
|
||
private string normalPressureCoefficient = "0.00";
|
||
|
||
[ObservableProperty]
|
||
private string frictionZero1 = "0";
|
||
|
||
[ObservableProperty]
|
||
private string frictionCoefficient1 = "0.00";
|
||
|
||
[ObservableProperty]
|
||
private string frictionZero2 = "0";
|
||
|
||
[ObservableProperty]
|
||
private string frictionCoefficient2 = "0.00";
|
||
|
||
[ObservableProperty]
|
||
private string plcPortName = "COM7";
|
||
|
||
[ObservableProperty]
|
||
private string adcPortName = "COM8";
|
||
|
||
[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 次有效试验";
|
||
|
||
[ObservableProperty]
|
||
private string staticCoefficient = "0.00";
|
||
|
||
[ObservableProperty]
|
||
private string dynamicCoefficient = "0.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";
|
||
|
||
public string TestSpeedText => $"{TestSpeed} m/s";
|
||
public string SampleRateText { get; } = "50 Hz";
|
||
public string BatchNumber { get; } = DateTime.Now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||
public string UploadProgressText => $"{UploadProgress}%";
|
||
public string StandardReference { get; } = "GB/T 3903.6-2024:实时采集、静/动摩擦系数、三次平均与重测判定";
|
||
|
||
public ObservableCollection<TestSample> Samples { get; } = [];
|
||
|
||
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"));
|
||
|
||
public ISeries[] Series { get; }
|
||
|
||
public Axis[] XAxes { get; } =
|
||
[
|
||
new Axis
|
||
{
|
||
Name = "时间(s)",
|
||
MinLimit = 0,
|
||
UnitWidth = 0.1,
|
||
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
|
||
{
|
||
Name = "压力 / 摩擦力 / 位移",
|
||
MinLimit = 0,
|
||
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,
|
||
UnitWidth = 0.1,
|
||
LabelsPaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
|
||
NamePaint = new SolidColorPaint(SKColor.Parse("#7E22CE")),
|
||
SeparatorsPaint = new SolidColorPaint(SKColor.Parse("#F1E7FF")) { StrokeThickness = 1 },
|
||
TextSize = 11,
|
||
NameTextSize = 12,
|
||
Padding = new LiveChartsCore.Drawing.Padding(3, 2, 2, 2)
|
||
}
|
||
];
|
||
|
||
public MainWindowViewModel()
|
||
{
|
||
Log.Information("初始化主页面 ViewModel");
|
||
Series =
|
||
[
|
||
CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0, 0),
|
||
CreateLineSeries("水平摩擦力(N)", horizontalFrictionPoints, "#16A34A", 0, 0),
|
||
CreateLineSeries("摩擦系数", frictionCoefficientPoints, "#C026D3", 1, 0),
|
||
CreateLineSeries("位移(mm)", displacementPoints, "#2563EB", 0, 0)
|
||
];
|
||
|
||
LoadDeviceSettings();
|
||
UpdateTargetLoad();
|
||
deviceService.Start(CurrentSettings());
|
||
_ = LoadPlcParametersAsync();
|
||
|
||
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
|
||
refreshTimer.Tick += (_, _) => RefreshFromDevice();
|
||
refreshTimer.Start();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task Clear()
|
||
{
|
||
await RunDeviceCommand(deviceService.PulseResetAsync(), "已发送复位指令 M90");
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void Preload()
|
||
{
|
||
CurrentStatus = "请在 0.2 s 内施加目标垂直载荷,载荷达到后启动滑动测试";
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StartTest()
|
||
{
|
||
BeginRun();
|
||
await RunDeviceCommand(deviceService.PulseStartTestAsync(), "已发送测试启动指令 M80,等待 M81 运行状态");
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task StopTest()
|
||
{
|
||
await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已发送测试停止指令 M83");
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ExportReport()
|
||
{
|
||
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
|
||
if (points.Count == 0)
|
||
{
|
||
Log.Warning("请求导出 Excel 时没有可导出的实时采样数据:TestNumber={TestNumber}", TestNumber);
|
||
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var report = new SlipReportExport(
|
||
TestNumber,
|
||
OperatorName,
|
||
MethodName,
|
||
ReportName,
|
||
SampleFeature,
|
||
CurrentShoeSize(),
|
||
CurrentLubricant(),
|
||
CurrentMode(),
|
||
CurrentSurface(),
|
||
TargetLoadText,
|
||
ActualLoadText,
|
||
TestSpeedText,
|
||
StandardReference,
|
||
Samples.ToList(),
|
||
points);
|
||
|
||
var path = excelExportService.Export(report);
|
||
UploadProgress = 100;
|
||
CurrentStatus = $"Excel 已导出:{path}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "Excel 导出失败:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
|
||
CurrentStatus = $"Excel 导出失败:{ex.Message}";
|
||
}
|
||
}
|
||
|
||
public Task StartLiftMotionAsync() => RunDeviceCommand(deviceService.StartLiftAsync(), "垂直架提升中,松开停止");
|
||
|
||
public Task StopLiftMotionAsync() => RunDeviceCommand(deviceService.StopLiftAsync(), "垂直架提升已停止");
|
||
|
||
public Task StartLowerMotionAsync() => RunDeviceCommand(deviceService.StartLowerAsync(), "垂直架下降中,松开停止");
|
||
|
||
public Task StopLowerMotionAsync() => RunDeviceCommand(deviceService.StopLowerAsync(), "垂直架下降已停止");
|
||
|
||
public Task StartMoveLeftMotionAsync() => RunDeviceCommand(deviceService.StartMoveLeftAsync(), "水平板左移中,松开停止");
|
||
|
||
public Task StopMoveLeftMotionAsync() => RunDeviceCommand(deviceService.StopMoveLeftAsync(), "水平板左移已停止");
|
||
|
||
public Task StartMoveRightMotionAsync() => RunDeviceCommand(deviceService.StartMoveRightAsync(), "水平板右移中,松开停止");
|
||
|
||
public Task StopMoveRightMotionAsync() => RunDeviceCommand(deviceService.StopMoveRightAsync(), "水平板右移已停止");
|
||
|
||
public Task StopAllMotionAsync() => RunDeviceCommand(deviceService.StopAllMotionAsync(), "全部运动已停止");
|
||
|
||
[RelayCommand]
|
||
private void DeleteSelectedSample()
|
||
{
|
||
var sample = Samples.FirstOrDefault(x => x.Index == SelectedSampleIndex);
|
||
if (sample is null)
|
||
{
|
||
CurrentStatus = $"未找到序号 {SelectedSampleIndex} 的实验数据";
|
||
return;
|
||
}
|
||
|
||
Samples.Remove(sample);
|
||
UpdateResultSummary();
|
||
CurrentStatus = $"已删除序号 {SelectedSampleIndex} 的实验数据";
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ShowSettingsDialog()
|
||
{
|
||
IsSettingsDialogOpen = true;
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void CloseSettingsDialog()
|
||
{
|
||
IsSettingsDialogOpen = false;
|
||
deviceService.UpdateSettings(CurrentSettings());
|
||
}
|
||
|
||
[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}";
|
||
}
|
||
}
|
||
|
||
partial void OnUploadProgressChanged(int value) => OnPropertyChanged(nameof(UploadProgressText));
|
||
|
||
partial void OnManualSpeedChanged(string value)
|
||
{
|
||
SaveAndApplySettings();
|
||
WriteNumericSetting(value, deviceService.WriteManualSpeedAsync, "手动速度");
|
||
}
|
||
|
||
partial void OnManualDisplacementChanged(string value)
|
||
{
|
||
SaveAndApplySettings();
|
||
WriteNumericSetting(value, deviceService.WriteManualDisplacementAsync, "手动位移");
|
||
}
|
||
|
||
partial void OnTestSpeedChanged(string value)
|
||
{
|
||
OnPropertyChanged(nameof(TestSpeedText));
|
||
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();
|
||
try
|
||
{
|
||
deviceService.StopAllMotionAsync().Wait(500);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Warning(ex, "关闭窗口时停止全部运动失败");
|
||
}
|
||
|
||
deviceService.Dispose();
|
||
}
|
||
|
||
private void RefreshFromDevice()
|
||
{
|
||
var device = deviceService.CurrentSnapshot;
|
||
DeviceStatus = device.IsConnected
|
||
? device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机"
|
||
: "离线";
|
||
|
||
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";
|
||
|
||
if (!device.IsConnected && !string.IsNullOrWhiteSpace(device.LastError))
|
||
{
|
||
CurrentStatus = $"设备离线:{device.LastError}";
|
||
}
|
||
|
||
var isRecording = device.IsConnected && device.IsTestRunning;
|
||
if (!wasRunning && isRecording)
|
||
{
|
||
BeginRun();
|
||
}
|
||
|
||
if (isRecording)
|
||
{
|
||
RecordPoint(device);
|
||
}
|
||
|
||
if (wasRunning && !isRecording)
|
||
{
|
||
if (device.IsConnected)
|
||
{
|
||
CompleteRun();
|
||
}
|
||
else
|
||
{
|
||
AbortRun("设备离线,本次采集已停止,未生成试验结果");
|
||
}
|
||
}
|
||
|
||
wasRunning = isRecording;
|
||
}
|
||
|
||
private void BeginRun()
|
||
{
|
||
currentRun.Clear();
|
||
verticalLoadPoints.Clear();
|
||
horizontalFrictionPoints.Clear();
|
||
frictionCoefficientPoints.Clear();
|
||
displacementPoints.Clear();
|
||
runStopwatch.Restart();
|
||
UploadProgress = 0;
|
||
CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数";
|
||
Log.Information("测试开始:TestNumber={TestNumber}, TargetLoad={TargetLoad}, TestSpeed={TestSpeed}", TestNumber, TargetLoadText, TestSpeedText);
|
||
}
|
||
|
||
private void RecordPoint(SlipDeviceSnapshot device)
|
||
{
|
||
var time = runStopwatch.Elapsed.TotalSeconds;
|
||
if (currentRun.Count > 0 && time - currentRun[^1].TimeSeconds < SampleIntervalSeconds)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var point = new SlipDataPoint(
|
||
device.Timestamp,
|
||
time,
|
||
device.VerticalLoadN,
|
||
device.HorizontalFrictionN,
|
||
device.DisplacementMm,
|
||
device.FrictionCoefficient);
|
||
|
||
currentRun.Add(point);
|
||
verticalLoadPoints.Add(new ObservablePoint(time, point.VerticalLoadN));
|
||
horizontalFrictionPoints.Add(new ObservablePoint(time, point.HorizontalFrictionN));
|
||
frictionCoefficientPoints.Add(new ObservablePoint(time, point.FrictionCoefficient));
|
||
displacementPoints.Add(new ObservablePoint(time, point.DisplacementMm));
|
||
|
||
UploadProgress = Math.Min(99, currentRun.Count);
|
||
}
|
||
|
||
private void CompleteRun()
|
||
{
|
||
runStopwatch.Stop();
|
||
if (currentRun.Count < 3)
|
||
{
|
||
Log.Warning("测试停止但采样点不足:TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, currentRun.Count);
|
||
CurrentStatus = "测试已停止,但有效采样点不足,未生成结果";
|
||
return;
|
||
}
|
||
|
||
lastCompletedRun = currentRun.ToList();
|
||
var peak = FindStaticPeak(currentRun);
|
||
var dynamicWindow = currentRun
|
||
.Where(point => point.TimeSeconds >= DynamicWindowStartSeconds && point.TimeSeconds <= DynamicWindowEndSeconds)
|
||
.ToList();
|
||
if (dynamicWindow.Count < MinimumDynamicWindowPointCount)
|
||
{
|
||
Log.Warning(
|
||
"测试停止但动摩擦窗口采样点不足:TestNumber={TestNumber}, PointCount={PointCount}, DynamicWindowPointCount={DynamicWindowPointCount}",
|
||
TestNumber,
|
||
currentRun.Count,
|
||
dynamicWindow.Count);
|
||
CurrentStatus = "测试已停止,0.3 s~0.6 s 有效采样点不足,未生成结果";
|
||
return;
|
||
}
|
||
|
||
var staticCoefficientValue = CalculateCoefficient(peak.HorizontalFrictionN, peak.VerticalLoadN);
|
||
var dynamicForce = dynamicWindow.Average(point => point.HorizontalFrictionN);
|
||
var dynamicLoad = dynamicWindow.Average(point => point.VerticalLoadN);
|
||
var dynamicCoefficientValue = CalculateCoefficient(dynamicForce, dynamicLoad);
|
||
var verdict = NeedsRetest(staticCoefficientValue, dynamicCoefficientValue) ? "需重测" : "有效";
|
||
var nextIndex = Samples.Count == 0 ? 1 : Samples.Max(sample => sample.Index) + 1;
|
||
|
||
Samples.Insert(0, new TestSample(
|
||
nextIndex,
|
||
DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
|
||
staticCoefficientValue.ToString("F2", CultureInfo.InvariantCulture),
|
||
dynamicCoefficientValue.ToString("F2", CultureInfo.InvariantCulture),
|
||
verdict,
|
||
staticCoefficientValue,
|
||
dynamicCoefficientValue));
|
||
|
||
StaticCoefficient = staticCoefficientValue.ToString("F2", CultureInfo.InvariantCulture);
|
||
DynamicCoefficient = dynamicCoefficientValue.ToString("F2", CultureInfo.InvariantCulture);
|
||
UpdateResultSummary();
|
||
UploadProgress = 100;
|
||
CurrentStatus = verdict == "有效"
|
||
? "测试完成:已按标准生成静/动摩擦系数"
|
||
: "测试完成:最近三次结果差异超过 10%,建议重新测试";
|
||
Log.Information(
|
||
"测试完成:TestNumber={TestNumber}, PointCount={PointCount}, StaticCoefficient={StaticCoefficient:F3}, DynamicCoefficient={DynamicCoefficient:F3}, Verdict={Verdict}",
|
||
TestNumber,
|
||
currentRun.Count,
|
||
staticCoefficientValue,
|
||
dynamicCoefficientValue,
|
||
verdict);
|
||
}
|
||
|
||
private void AbortRun(string message)
|
||
{
|
||
runStopwatch.Stop();
|
||
Log.Warning(
|
||
"测试采集中止:TestNumber={TestNumber}, PointCount={PointCount}, Message={Message}",
|
||
TestNumber,
|
||
currentRun.Count,
|
||
message);
|
||
CurrentStatus = message;
|
||
}
|
||
|
||
private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue)
|
||
{
|
||
var previous = Samples.Take(StandardTrialCount - 1).Reverse().ToList();
|
||
if (previous.Count < StandardTrialCount - 1)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
private static bool HasSystematicTrendExceedingTenPercent(double[] values)
|
||
{
|
||
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;
|
||
}
|
||
|
||
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()
|
||
{
|
||
var latest = Samples.Take(StandardTrialCount).ToList();
|
||
if (latest.Count == 0)
|
||
{
|
||
ResultSummary = "等待 3 次有效试验";
|
||
return;
|
||
}
|
||
|
||
if (latest.Count < StandardTrialCount)
|
||
{
|
||
ResultSummary = $"已完成 {latest.Count}/3 次有效试验";
|
||
return;
|
||
}
|
||
|
||
var staticAverage = latest.Average(sample => sample.StaticCoefficientValue);
|
||
var dynamicAverage = latest.Average(sample => sample.DynamicCoefficientValue);
|
||
ResultSummary = $"近 3 次平均 静 {staticAverage:F2} / 动 {dynamicAverage:F2}";
|
||
}
|
||
|
||
private static SlipDataPoint FindStaticPeak(IReadOnlyList<SlipDataPoint> points)
|
||
{
|
||
var preDynamicWindow = points
|
||
.Where(point => point.TimeSeconds <= DynamicWindowStartSeconds)
|
||
.ToList();
|
||
return (preDynamicWindow.Count > 0 ? preDynamicWindow : points)
|
||
.MaxBy(point => point.HorizontalFrictionN)
|
||
?? points[0];
|
||
}
|
||
|
||
private static double CalculateCoefficient(double frictionForce, double verticalLoad) =>
|
||
Math.Abs(verticalLoad) > 0.0001 ? frictionForce / verticalLoad : 0;
|
||
|
||
private async Task RunDeviceCommand(Task command, string successMessage)
|
||
{
|
||
try
|
||
{
|
||
await command;
|
||
CurrentStatus = successMessage;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Error(ex, "设备指令失败:{SuccessMessage}", successMessage);
|
||
CurrentStatus = $"设备指令失败:{ex.Message}";
|
||
}
|
||
}
|
||
|
||
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());
|
||
}
|
||
|
||
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 参数失败,将使用本地保存的设备设置");
|
||
}
|
||
}
|
||
|
||
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;
|
||
NormalPressureZero = settings.NormalPressureZero ?? NormalPressureZero;
|
||
NormalPressureCoefficient = settings.NormalPressureCoefficient ?? NormalPressureCoefficient;
|
||
FrictionZero1 = settings.FrictionZero1 ?? FrictionZero1;
|
||
FrictionCoefficient1 = settings.FrictionCoefficient1 ?? FrictionCoefficient1;
|
||
FrictionZero2 = settings.FrictionZero2 ?? FrictionZero2;
|
||
FrictionCoefficient2 = settings.FrictionCoefficient2 ?? FrictionCoefficient2;
|
||
PlcPortName = settings.PlcPortName ?? PlcPortName;
|
||
AdcPortName = settings.AdcPortName ?? AdcPortName;
|
||
BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Warning(ex, "读取设备设置失败:Path={Path}", DeviceSettingsPath);
|
||
}
|
||
finally
|
||
{
|
||
isLoadingDeviceSettings = false;
|
||
}
|
||
}
|
||
|
||
private void SaveDeviceSettings()
|
||
{
|
||
if (isLoadingDeviceSettings)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
Directory.CreateDirectory(Path.GetDirectoryName(DeviceSettingsPath)!);
|
||
var json = JsonSerializer.Serialize(CurrentSettings(), new JsonSerializerOptions { WriteIndented = true });
|
||
File.WriteAllText(DeviceSettingsPath, json);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Warning(ex, "保存设备设置失败:Path={Path}", DeviceSettingsPath);
|
||
}
|
||
}
|
||
|
||
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(含)以上"
|
||
};
|
||
|
||
private string CurrentLubricant() => SelectedLubricantIndex switch
|
||
{
|
||
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);
|
||
|
||
private static LineSeries<ObservablePoint> CreateLineSeries(
|
||
string name,
|
||
ObservableCollection<ObservablePoint> values,
|
||
string color,
|
||
int yAxis,
|
||
double smoothness) =>
|
||
new()
|
||
{
|
||
Name = name,
|
||
Values = values,
|
||
MiniatureShapeSize = 8,
|
||
MiniatureStrokeThickness = 2,
|
||
Stroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 3.2f },
|
||
Fill = new SolidColorPaint(SKColors.Transparent),
|
||
GeometryFill = new SolidColorPaint(SKColors.White),
|
||
GeometryStroke = new SolidColorPaint(SKColor.Parse(color)) { StrokeThickness = 2 },
|
||
GeometrySize = 4,
|
||
LineSmoothness = smoothness,
|
||
ScalesYAt = yAxis
|
||
};
|
||
|
||
private static string DeviceSettingsPath =>
|
||
Path.Combine(
|
||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||
"FootwearSlipResistance",
|
||
"device-settings.json");
|
||
}
|
||
}
|