370 lines
13 KiB
C#
370 lines
13 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using FootwearTest.Models;
|
||
using FootwearTest.Services;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace FootwearTest.ViewModels;
|
||
|
||
public partial class MethodAViewModel : ViewModelBase
|
||
{
|
||
private const int MethodAMinimumSampleCount = 30;
|
||
|
||
private readonly IDeviceClient _deviceClient;
|
||
private readonly DeviceSettings _settings;
|
||
private readonly TestFormulaService _formulaService;
|
||
private readonly TestRunRepository _repository;
|
||
private readonly ILogger<MethodAViewModel> _logger;
|
||
|
||
public MethodAViewModel(
|
||
IDeviceClient deviceClient,
|
||
DeviceSettings settings,
|
||
TestFormulaService formulaService,
|
||
TestRunRepository repository,
|
||
ILogger<MethodAViewModel> logger)
|
||
{
|
||
_deviceClient = deviceClient;
|
||
_settings = settings;
|
||
_formulaService = formulaService;
|
||
_repository = repository;
|
||
_logger = logger;
|
||
MeasureSkinResistanceCommand = new AsyncRelayCommand(MeasureSkinResistanceAsync);
|
||
RunWholeShoeCommand = new AsyncRelayCommand(RunWholeShoeAsync);
|
||
StopCommand = new AsyncRelayCommand(StopAsync);
|
||
}
|
||
|
||
public ObservableCollection<MethodASampleRecord> Samples { get; } = [];
|
||
public IAsyncRelayCommand MeasureSkinResistanceCommand { get; }
|
||
public IAsyncRelayCommand RunWholeShoeCommand { get; }
|
||
public IAsyncRelayCommand StopCommand { get; }
|
||
|
||
private string _sampleDescription = "整鞋样品 1# / 2#";
|
||
private string _statusText = "待机";
|
||
private bool _isRunning;
|
||
private double _skinMoistureResistance = 6.0;
|
||
private double _averageMoistureResistance;
|
||
private double _averageThermalResistance;
|
||
private double _moistureCvPercent;
|
||
private double _thermalCvPercent;
|
||
private string _resultText = "尚未开始试验";
|
||
|
||
public string SampleDescription
|
||
{
|
||
get => _sampleDescription;
|
||
set => SetProperty(ref _sampleDescription, value);
|
||
}
|
||
|
||
public string StatusText
|
||
{
|
||
get => _statusText;
|
||
set => SetProperty(ref _statusText, value);
|
||
}
|
||
|
||
public bool IsRunning
|
||
{
|
||
get => _isRunning;
|
||
set => SetProperty(ref _isRunning, value);
|
||
}
|
||
|
||
public double SkinMoistureResistance
|
||
{
|
||
get => _skinMoistureResistance;
|
||
set => SetProperty(ref _skinMoistureResistance, value);
|
||
}
|
||
|
||
public double AverageMoistureResistance
|
||
{
|
||
get => _averageMoistureResistance;
|
||
set => SetProperty(ref _averageMoistureResistance, value);
|
||
}
|
||
|
||
public double AverageThermalResistance
|
||
{
|
||
get => _averageThermalResistance;
|
||
set => SetProperty(ref _averageThermalResistance, value);
|
||
}
|
||
|
||
public double MoistureCvPercent
|
||
{
|
||
get => _moistureCvPercent;
|
||
set => SetProperty(ref _moistureCvPercent, value);
|
||
}
|
||
|
||
public double ThermalCvPercent
|
||
{
|
||
get => _thermalCvPercent;
|
||
set => SetProperty(ref _thermalCvPercent, value);
|
||
}
|
||
|
||
public string ResultText
|
||
{
|
||
get => _resultText;
|
||
set => SetProperty(ref _resultText, value);
|
||
}
|
||
|
||
private async Task MeasureSkinResistanceAsync()
|
||
{
|
||
if (IsRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
_logger.LogInformation("Method A skin moisture resistance started. SampleDescription={SampleDescription}", SampleDescription);
|
||
await EnsureConnectedAsync();
|
||
IsRunning = true;
|
||
StatusText = "等待假脚/环境稳定";
|
||
Samples.Clear();
|
||
await _deviceClient.SetOutputsAsync(true, true, true);
|
||
await WaitForMethodAStabilityAsync();
|
||
if (!IsRunning)
|
||
{
|
||
StatusText = "已停止";
|
||
return;
|
||
}
|
||
|
||
StatusText = "皮肤湿阻测定中";
|
||
|
||
while (IsRunning)
|
||
{
|
||
await Task.Delay(GetMethodASamplingInterval());
|
||
if (!IsRunning)
|
||
{
|
||
break;
|
||
}
|
||
|
||
var snapshot = await _deviceClient.ReadSnapshotAsync();
|
||
var sample = _formulaService.CalculateSkinMoistureResistanceSample(Samples.Count + 1, snapshot, _settings.FootAreaSquareMeters);
|
||
Samples.Add(sample);
|
||
SkinMoistureResistance = Round(_formulaService.Average(Samples.Select(item => item.MoistureResistance)), 3);
|
||
MoistureCvPercent = Round(_formulaService.CoefficientOfVariationPercent(Samples.Select(item => item.MoistureResistance)), 2);
|
||
ResultText = $"皮肤湿阻: 已采集 {Samples.Count} 次,Res {SkinMoistureResistance:F3} Pa·m²/W,CV {MoistureCvPercent:F2}%";
|
||
|
||
if (HasReachedVariationCoefficient(Samples.Select(item => item.MoistureResistance)))
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!IsRunning)
|
||
{
|
||
StatusText = "已停止";
|
||
return;
|
||
}
|
||
|
||
await _deviceClient.SetOutputsAsync(false, true, false);
|
||
ThermalCvPercent = 0;
|
||
AverageMoistureResistance = 0;
|
||
AverageThermalResistance = 0;
|
||
ResultText = $"皮肤湿阻: Res {SkinMoistureResistance:F3} Pa·m²/W,CV {MoistureCvPercent:F2}%";
|
||
var id = await SaveRunAsync("方法 A - 皮肤湿阻", true);
|
||
StatusText = "皮肤湿阻测定完成";
|
||
_logger.LogInformation("Method A skin moisture resistance completed. RecordId={RecordId}, Samples={Samples}, Result={ResultText}", id, Samples.Count, ResultText);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
StatusText = $"皮肤湿阻测定失败: {ex.Message}";
|
||
_logger.LogError(ex, "Method A skin moisture resistance failed. SampleDescription={SampleDescription}", SampleDescription);
|
||
await StopOutputsAfterFailureAsync("方法 A 皮肤湿阻失败后关闭输出");
|
||
}
|
||
finally
|
||
{
|
||
IsRunning = false;
|
||
}
|
||
}
|
||
|
||
private async Task RunWholeShoeAsync()
|
||
{
|
||
if (IsRunning)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
_logger.LogInformation("Method A whole shoe test started. SampleDescription={SampleDescription}", SampleDescription);
|
||
await EnsureConnectedAsync();
|
||
IsRunning = true;
|
||
StatusText = "等待假脚/环境稳定";
|
||
Samples.Clear();
|
||
await _deviceClient.SetOutputsAsync(true, true, true);
|
||
await WaitForMethodAStabilityAsync();
|
||
if (!IsRunning)
|
||
{
|
||
StatusText = "已停止";
|
||
return;
|
||
}
|
||
|
||
StatusText = "整鞋热阻/湿阻测定中";
|
||
|
||
while (IsRunning)
|
||
{
|
||
await Task.Delay(GetMethodASamplingInterval());
|
||
if (!IsRunning)
|
||
{
|
||
break;
|
||
}
|
||
|
||
var snapshot = await _deviceClient.ReadSnapshotAsync();
|
||
Samples.Add(_formulaService.CalculateMethodASample(Samples.Count + 1, snapshot, _settings.FootAreaSquareMeters, SkinMoistureResistance));
|
||
UpdateMethodAResult("整鞋热阻/湿阻");
|
||
|
||
if (HasReachedWholeShoeVariationCoefficient())
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!IsRunning)
|
||
{
|
||
StatusText = "已停止";
|
||
return;
|
||
}
|
||
|
||
await _deviceClient.SetOutputsAsync(false, true, false);
|
||
var passed =
|
||
_formulaService.PassesTenPercentDeviation(Samples.Select(sample => sample.MoistureResistance)) &&
|
||
_formulaService.PassesTenPercentDeviation(Samples.Select(sample => sample.ThermalResistance));
|
||
UpdateMethodAResult("整鞋热阻/湿阻");
|
||
var id = await SaveRunAsync("方法 A - 整鞋热阻/湿阻", passed);
|
||
StatusText = passed ? "试验完成" : "结果偏差超过 ±10%,建议重测";
|
||
_logger.LogInformation("Method A whole shoe test completed. RecordId={RecordId}, Passed={Passed}, Samples={Samples}, Result={ResultText}", id, passed, Samples.Count, ResultText);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
StatusText = $"整鞋热阻/湿阻测定失败: {ex.Message}";
|
||
_logger.LogError(ex, "Method A whole shoe test failed. SampleDescription={SampleDescription}", SampleDescription);
|
||
await StopOutputsAfterFailureAsync("方法 A 整鞋试验失败后关闭输出");
|
||
}
|
||
finally
|
||
{
|
||
IsRunning = false;
|
||
}
|
||
}
|
||
|
||
private async Task StopAsync()
|
||
{
|
||
try
|
||
{
|
||
_logger.LogWarning("Operator stopped Method A test");
|
||
await _deviceClient.SetOutputsAsync(false, false, false);
|
||
IsRunning = false;
|
||
StatusText = "已停止";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
StatusText = $"停止失败: {ex.Message}";
|
||
_logger.LogError(ex, "Method A stop command failed");
|
||
}
|
||
}
|
||
|
||
private TimeSpan GetMethodASamplingInterval()
|
||
{
|
||
return _settings.UseSimulator ? TimeSpan.FromMilliseconds(100) : TimeSpan.FromMinutes(1);
|
||
}
|
||
|
||
private TimeSpan GetMethodAStabilityPollInterval()
|
||
{
|
||
return _settings.UseSimulator ? TimeSpan.FromMilliseconds(100) : TimeSpan.FromSeconds(5);
|
||
}
|
||
|
||
private async Task WaitForMethodAStabilityAsync()
|
||
{
|
||
while (IsRunning)
|
||
{
|
||
var snapshot = await _deviceClient.ReadSnapshotAsync();
|
||
if (IsMethodAStable(snapshot))
|
||
{
|
||
return;
|
||
}
|
||
|
||
StatusText = $"等待稳定: 假脚 {snapshot.FootTemperatureC:F1} ℃,环境 {snapshot.EnvironmentTemperatureC:F1} ℃ / {snapshot.EnvironmentHumidityPercent:F0}%";
|
||
await Task.Delay(GetMethodAStabilityPollInterval());
|
||
}
|
||
}
|
||
|
||
private bool IsMethodAStable(DeviceSnapshot snapshot)
|
||
{
|
||
return Math.Abs(snapshot.FootTemperatureC - _settings.MethodATargetTemperatureC) <= 0.3 &&
|
||
Math.Abs(snapshot.EnvironmentTemperatureC - _settings.EnvironmentTemperatureC) <= 2.0 &&
|
||
Math.Abs(snapshot.EnvironmentHumidityPercent - _settings.EnvironmentHumidityPercent) <= 5.0;
|
||
}
|
||
|
||
private bool HasReachedVariationCoefficient(IEnumerable<double> values)
|
||
{
|
||
var list = values.ToArray();
|
||
return list.Length >= MethodAMinimumSampleCount &&
|
||
_formulaService.CoefficientOfVariationPercent(list) <= _settings.CoefficientOfVariationLimitPercent;
|
||
}
|
||
|
||
private bool HasReachedWholeShoeVariationCoefficient()
|
||
{
|
||
return Samples.Count >= MethodAMinimumSampleCount &&
|
||
HasReachedVariationCoefficient(Samples.Select(sample => sample.MoistureResistance)) &&
|
||
HasReachedVariationCoefficient(Samples.Select(sample => sample.ThermalResistance));
|
||
}
|
||
|
||
private async Task EnsureConnectedAsync()
|
||
{
|
||
if (!_deviceClient.IsConnected)
|
||
{
|
||
await _deviceClient.ConnectAsync();
|
||
}
|
||
}
|
||
|
||
private void UpdateMethodAResult(string stage)
|
||
{
|
||
AverageMoistureResistance = Round(_formulaService.Average(Samples.Select(sample => sample.MoistureResistance)), 3);
|
||
AverageThermalResistance = Round(_formulaService.Average(Samples.Select(sample => sample.ThermalResistance)), 4);
|
||
MoistureCvPercent = Round(_formulaService.CoefficientOfVariationPercent(Samples.Select(sample => sample.MoistureResistance)), 2);
|
||
ThermalCvPercent = Round(_formulaService.CoefficientOfVariationPercent(Samples.Select(sample => sample.ThermalResistance)), 2);
|
||
ResultText = $"{stage}: 湿阻 {AverageMoistureResistance:F3} Pa·m²/W,热阻 {AverageThermalResistance:F4} m²·℃/W,CV {ThermalCvPercent:F2}%";
|
||
}
|
||
|
||
private static double Round(double value, int digits)
|
||
{
|
||
return Math.Round(value, digits, MidpointRounding.AwayFromZero);
|
||
}
|
||
|
||
private async Task StopOutputsAfterFailureAsync(string reason)
|
||
{
|
||
try
|
||
{
|
||
await _deviceClient.SetOutputsAsync(false, false, false);
|
||
_logger.LogWarning("Outputs forced off after Method A failure. Reason={Reason}", reason);
|
||
}
|
||
catch (Exception stopEx)
|
||
{
|
||
_logger.LogError(stopEx, "Failed to force outputs off after Method A failure. Reason={Reason}", reason);
|
||
}
|
||
}
|
||
|
||
private async Task<long> SaveRunAsync(string method, bool isValid)
|
||
{
|
||
var result = new MethodAResult(
|
||
method,
|
||
SkinMoistureResistance,
|
||
AverageMoistureResistance,
|
||
AverageThermalResistance,
|
||
MoistureCvPercent,
|
||
ThermalCvPercent,
|
||
isValid);
|
||
var data = JsonSerializer.Serialize(new { Result = result, Samples }, new JsonSerializerOptions { WriteIndented = true });
|
||
return await _repository.SaveAsync(new TestRunRecord(
|
||
0,
|
||
method,
|
||
SampleDescription,
|
||
$"假脚 {_settings.MethodATargetTemperatureC:F1} ℃;环境 {_settings.EnvironmentTemperatureC:F1} ℃ / {_settings.EnvironmentHumidityPercent:F0}%;风速 {_settings.MethodAAirSpeedMetersPerSecond:F2} m/s",
|
||
ResultText,
|
||
data,
|
||
isValid,
|
||
DateTime.Now));
|
||
}
|
||
}
|