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;
|
|
|
|
|
|
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 18:14:01 +08:00
|
|
|
|
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 = [];
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
private bool isLoadingDeviceSettings;
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private bool wasRunning;
|
|
|
|
|
|
private List<SlipDataPoint> lastCompletedRun = [];
|
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]
|
|
|
|
|
|
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";
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
public string TestSpeedText => $"{TestSpeed} m/s";
|
|
|
|
|
|
public string SampleRateText { get; } = "30 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 18:14:01 +08:00
|
|
|
|
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; } = [];
|
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,
|
|
|
|
|
|
UnitWidth = 0.1,
|
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-02 18:14:01 +08:00
|
|
|
|
Name = "压力 / 摩擦力 / 位移",
|
2026-06-02 17:41:53 +08:00
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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)
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
LoadDeviceSettings();
|
2026-06-02 18:14:01 +08:00
|
|
|
|
UpdateTargetLoad();
|
|
|
|
|
|
deviceService.Start(CurrentSettings());
|
|
|
|
|
|
|
|
|
|
|
|
refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(33) };
|
|
|
|
|
|
refreshTimer.Tick += (_, _) => RefreshFromDevice();
|
|
|
|
|
|
refreshTimer.Start();
|
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-02 18:14:01 +08:00
|
|
|
|
await RunDeviceCommand(deviceService.PulseResetAsync(), "已发送复位指令 M90");
|
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-02 18:14:01 +08:00
|
|
|
|
BeginRun();
|
|
|
|
|
|
await RunDeviceCommand(deviceService.PulseStartTestAsync(), "已发送测试启动指令 M80,等待 M81 运行状态");
|
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]
|
|
|
|
|
|
private void ExportReport()
|
|
|
|
|
|
{
|
2026-06-02 18:14:01 +08:00
|
|
|
|
var points = currentRun.Count > 0 ? currentRun.ToList() : lastCompletedRun;
|
|
|
|
|
|
if (points.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentStatus = "没有可导出的实时采样数据,请先完成一次测试";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task Lift() => await RunDeviceCommand(deviceService.LiftAsync(), "垂直架提升指令已发送 M5");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task Lower() => await RunDeviceCommand(deviceService.LowerAsync(), "垂直架下降指令已发送 M4");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task MoveLeft() => await RunDeviceCommand(deviceService.ToggleMoveLeftAsync(), "水平板左移状态已切换 M1");
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private async Task MoveRight() => await RunDeviceCommand(deviceService.ToggleMoveRightAsync(), "水平板右移状态已切换 M2");
|
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-02 18:14:01 +08:00
|
|
|
|
deviceService.UpdateSettings(CurrentSettings());
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
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;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
private void BeginRun()
|
|
|
|
|
|
{
|
|
|
|
|
|
currentRun.Clear();
|
|
|
|
|
|
verticalLoadPoints.Clear();
|
|
|
|
|
|
horizontalFrictionPoints.Clear();
|
|
|
|
|
|
frictionCoefficientPoints.Clear();
|
|
|
|
|
|
displacementPoints.Clear();
|
|
|
|
|
|
runStopwatch.Restart();
|
|
|
|
|
|
UploadProgress = 0;
|
|
|
|
|
|
CurrentStatus = "测试运行:按标准采集垂直载荷、摩擦力、位移与摩擦系数";
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-02 17:41:53 +08:00
|
|
|
|
|
2026-06-02 18:14:01 +08:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
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-02 18:14:01 +08:00
|
|
|
|
PlcPortName = settings.PlcPortName ?? PlcPortName;
|
|
|
|
|
|
AdcPortName = settings.AdcPortName ?? AdcPortName;
|
|
|
|
|
|
BaudRate = settings.BaudRate > 0 ? settings.BaudRate : BaudRate;
|
2026-06-02 17:41:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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(含)以上"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-02 17:41:53 +08:00
|
|
|
|
private static string DeviceSettingsPath =>
|
|
|
|
|
|
Path.Combine(
|
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
|
|
|
"FootwearSlipResistance",
|
|
|
|
|
|
"device-settings.json");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|