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 _logger; public MethodAViewModel( IDeviceClient deviceClient, DeviceSettings settings, TestFormulaService formulaService, TestRunRepository repository, ILogger 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 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 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 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)); } }