Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs
2026-06-04 09:22:18 +08:00

1101 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
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 const string DefaultPlcPortName = "COM3";
private const string DefaultAdcPortName = "COM4";
private const string LegacyPlcPortName = "COM7";
private const string LegacyAdcPortName = "COM8";
private static readonly TimeSpan ResetButtonPendingTimeout = TimeSpan.FromMilliseconds(800);
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 bool isResetButtonPending;
private bool hasObservedResetDeviceBusy;
private DateTime resetButtonPendingStartedAt = DateTime.MinValue;
private string activePlcPortName = string.Empty;
private string activeAdcPortName = string.Empty;
private int activeBaudRate;
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 = DefaultPlcPortName;
[ObservableProperty]
private string adcPortName = DefaultAdcPortName;
[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 testButtonText = "测试";
[ObservableProperty]
private string resetButtonText = "复位";
[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();
RestartDeviceConnection();
_ = LoadPlcParametersAsync();
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
refreshTimer.Tick += (_, _) => RefreshFromDevice();
refreshTimer.Start();
}
[RelayCommand]
private async Task Clear()
{
ApplyConnectionSettings();
BeginResetButtonFeedback();
try
{
await deviceService.PulseResetAsync();
CurrentStatus = "已按老代码逻辑发送复位指令 M90";
}
catch (Exception ex)
{
Log.Error(ex, "设备指令失败:已按老代码逻辑发送复位指令 M90");
CurrentStatus = $"设备指令失败:{ex.Message}";
ClearResetButtonFeedback();
UpdateResetButtonText(deviceService.CurrentSnapshot);
}
}
[RelayCommand]
private void Preload()
{
CurrentStatus = "请在 0.2 s 内施加目标垂直载荷,载荷达到后启动滑动测试";
}
[RelayCommand]
private async Task StartTest()
{
ApplyConnectionSettings();
var device = deviceService.CurrentSnapshot;
if (device.IsTestRunning)
{
await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已按老代码逻辑发送停止指令 M83");
return;
}
if (!ValidateAdcCoefficientsBeforeTest())
{
return;
}
await RunDeviceCommand(deviceService.PulseStartTestAsync(), "已按老代码逻辑发送测试启动指令 M80等待 M81 运行状态");
}
[RelayCommand]
private async Task StopTest()
{
await RunDeviceCommand(deviceService.PulseStopTestAsync(), "已发送测试停止指令 M83");
}
[RelayCommand]
private async Task ExportReport()
{
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
if (points.Count == 0)
{
Log.Warning("请求导出 Excel 时没有可导出的实时采样数据TestNumber={TestNumber}", TestNumber);
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
return;
}
try
{
CurrentStatus = "请选择 Excel 报告保存目录";
var exportDirectory = await SelectExportDirectoryAsync();
if (exportDirectory is null)
{
Log.Information("用户取消 Excel 导出目录选择TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
CurrentStatus = "已取消 Excel 导出";
return;
}
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, exportDirectory);
UploadProgress = 100;
CurrentStatus = $"Excel 已导出:{path}";
}
catch (Exception ex)
{
Log.Error(ex, "Excel 导出失败TestNumber={TestNumber}, PointCount={PointCount}", TestNumber, points.Count);
CurrentStatus = $"Excel 导出失败:{ex.Message}";
}
}
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;
}
[RelayCommand]
private Task LiftMotion() => RunDeviceCommand(deviceService.ApplyOldLiftAsync(), "提升按老代码逻辑执行M4=0, M5=1");
[RelayCommand]
private Task LowerMotion() => RunDeviceCommand(deviceService.ApplyOldLowerAsync(), "下降按老代码逻辑执行M5=0, M4=1");
[RelayCommand]
private Task MoveLeftMotion() => RunDeviceCommand(deviceService.ToggleOldMoveLeftAsync(), "左移按老代码逻辑切换 M1");
[RelayCommand]
private Task MoveRightMotion() => RunDeviceCommand(deviceService.ToggleOldMoveRightAsync(), "右移按老代码逻辑切换 M2");
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;
ApplyConnectionSettings();
}
[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;
if (device.IsConnected)
{
DeviceStatus = device.IsTestRunning ? "联机 / 测试中" : device.IsResetting ? "联机 / 复位中" : "联机 / 待机";
TestButtonText = device.IsTestRunning ? "停止" : "测试";
UpdateResetButtonText(device);
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) ? "数据无效" : "离线";
TestButtonText = device.IsTestRunning ? "停止" : "测试";
UpdateResetButtonText(device);
VerticalPressure = "--";
HorizontalForce = "--";
FrictionCoefficient = "--";
Distance = "--";
ActualLoadText = "-- N";
}
if (!device.IsConnected && !string.IsNullOrWhiteSpace(device.LastError))
{
CurrentStatus = IsAdcCalibrationError(device.LastError)
? $"数据无效:{device.LastError}"
: $"设备离线:{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 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 ? "复位中" : "复位";
}
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 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;
}
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 = NormalizeSavedPort(settings.PlcPortName, DefaultPlcPortName, LegacyPlcPortName);
AdcPortName = NormalizeSavedPort(settings.AdcPortName, DefaultAdcPortName, LegacyAdcPortName);
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 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;
}
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");
}
}