Files
FootwearTest-20260602/Footwear Test methodsfor wholeshoe Slipresistanceperformance/ViewModels/MainWindowViewModel.cs
2026-06-02 18:14:01 +08:00

738 lines
26 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.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 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 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.000";
[ObservableProperty]
private string dynamicCoefficient = "0.000";
[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; } = "30 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 5.1.3、7.3.1.4、8.1-8.3:实时采集、静/动摩擦系数、三次平均与重测判定";
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()
{
Series =
[
CreateLineSeries("垂直压力(N)", verticalLoadPoints, "#DC2626", 0, 0.65),
CreateLineSeries("水平摩擦力(N)", horizontalFrictionPoints, "#16A34A", 0, 0.65),
CreateLineSeries("摩擦系数", frictionCoefficientPoints, "#C026D3", 1, 0.65),
CreateLineSeries("位移(mm)", displacementPoints, "#2563EB", 0, 0.35)
];
LoadDeviceSettings();
UpdateTargetLoad();
deviceService.Start(CurrentSettings());
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) };
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)
{
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)
{
CurrentStatus = $"Excel 导出失败:{ex.Message}";
}
}
[RelayCommand]
private async Task Lift() => await RunDeviceCommand(deviceService.LiftAsync(), "垂直架提升指令已发送 M5");
[RelayCommand]
private async Task Lower() => await RunDeviceCommand(deviceService.LowerAsync(), "垂直架下降指令已发送 M4");
[RelayCommand]
private async Task MoveLeft() => await RunDeviceCommand(deviceService.ToggleMoveLeftAsync(), "水平板左移状态已切换 M1");
[RelayCommand]
private async Task MoveRight() => await RunDeviceCommand(deviceService.ToggleMoveRightAsync(), "水平板右移状态已切换 M2");
[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());
}
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();
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}";
}
if (!wasRunning && device.IsTestRunning)
{
BeginRun();
}
if (device.IsTestRunning)
{
RecordPoint(device);
}
if (wasRunning && !device.IsTestRunning)
{
CompleteRun();
}
wasRunning = device.IsTestRunning;
}
private void BeginRun()
{
currentRun.Clear();
verticalLoadPoints.Clear();
horizontalFrictionPoints.Clear();
frictionCoefficientPoints.Clear();
displacementPoints.Clear();
runStopwatch.Restart();
UploadProgress = 0;
CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数";
}
private void RecordPoint(SlipDeviceSnapshot device)
{
var time = runStopwatch.Elapsed.TotalSeconds;
if (currentRun.Count > 0 && time - currentRun[^1].TimeSeconds < 1.0 / 30.0)
{
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)
{
CurrentStatus = "测试已停止,但有效采样点不足,未生成结果";
return;
}
lastCompletedRun = currentRun.ToList();
var peak = FindStaticPeak(currentRun);
var dynamicWindow = currentRun
.Where(point => point.TimeSeconds >= 0.3 && point.TimeSeconds <= 0.6)
.ToList();
if (dynamicWindow.Count == 0)
{
dynamicWindow = currentRun.ToList();
}
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("F3", CultureInfo.InvariantCulture),
dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture),
verdict,
staticCoefficientValue,
dynamicCoefficientValue));
StaticCoefficient = staticCoefficientValue.ToString("F3", CultureInfo.InvariantCulture);
DynamicCoefficient = dynamicCoefficientValue.ToString("F3", CultureInfo.InvariantCulture);
UpdateResultSummary();
UploadProgress = 100;
CurrentStatus = verdict == "有效"
? "测试完成:已按标准生成静/动摩擦系数"
: "测试完成:最近三次结果差异超过 10%,建议重新测试";
}
private bool NeedsRetest(double staticCoefficientValue, double dynamicCoefficientValue)
{
var latest = Samples.Take(2).ToList();
if (latest.Count < 2)
{
return false;
}
var staticValues = latest.Select(sample => sample.StaticCoefficientValue).Append(staticCoefficientValue).ToArray();
var dynamicValues = latest.Select(sample => sample.DynamicCoefficientValue).Append(dynamicCoefficientValue).ToArray();
return ExceedsTenPercent(staticValues) || ExceedsTenPercent(dynamicValues);
}
private static bool ExceedsTenPercent(double[] values)
{
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(3).ToList();
if (latest.Count == 0)
{
ResultSummary = "等待 3 次有效试验";
return;
}
var staticAverage = latest.Average(sample => sample.StaticCoefficientValue);
var dynamicAverage = latest.Average(sample => sample.DynamicCoefficientValue);
ResultSummary = $"近 {latest.Count} 次平均 静 {staticAverage:F3} / 动 {dynamicAverage:F3}";
}
private static SlipDataPoint FindStaticPeak(IReadOnlyList<SlipDataPoint> points)
{
for (var index = 1; index < points.Count - 1; index++)
{
var previous = points[index - 1].HorizontalFrictionN;
var current = points[index].HorizontalFrictionN;
var next = points[index + 1].HorizontalFrictionN;
if (current >= previous && current >= next && current > 0)
{
return points[index];
}
}
return 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)
{
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 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
{
}
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
{
}
}
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");
}
}