Files
ASTM-D7896-19TransientHot-W…/ViewModels/D7896ViewModel.cs
2026-06-04 10:26:57 +08:00

917 lines
38 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 ASTM_D7896_Tester.Helpers;
using ASTM_D7896_Tester.Models;
using ASTM_D7896_Tester.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace ASTM_D7896_Tester.ViewModels;
public partial class D7896ViewModel : ObservableObject
{
private readonly IPlcService _plcService;
private AppConfig _config;
private readonly ReportService _reportService;
// 电压表服务
private Th1963LanService _th1963Ustd; // 6位半测量标准电阻电压 U_std
private Th1963LanService _th1953Ustd; // 6位半测量标准电阻电压 U_std
//private FiveHalfDmmService _fiveHalfUpt; // 5位半测量铂丝电压 U_pt
private CancellationTokenSource _testCts; // 用于停止测试
private bool _stopRequested;
// 后台监控定时器
private Timer? _monitorTimer;
// 常量: 标准电阻值 1Ω
private const double StandardResistor = 1.0;
// 铂丝电阻温度系数 (纯铂)
private const double AlphaPt = 0.0040; // /°C
// 加热功率 Q 计算相关
private double _heatingCurrent; // 实际加热电流平均值
private double _wireResistanceAvg; // 铂丝平均电阻
// 温升曲线数据
[ObservableProperty] private string _curveTitle = "温升曲线";
[ObservableProperty] private PlotModel _temperatureCurveModel;
// UI 绑定属性 (与之前一致)
public ObservableCollection<string> ReferenceLiquids { get; } = new() { "蒸馏水", "甲苯", "乙二醇" };
[ObservableProperty] private string _sampleId = "未命名样品";
[ObservableProperty] private double _testTemperature = 25.0;
[ObservableProperty] private string _testDateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
[ObservableProperty] private bool _isTesting = false;
[ObservableProperty] private string _statusMessage = "就绪";
[ObservableProperty] private int _currentMeasurementIndex = 0;
[ObservableProperty] private ObservableCollection<MeasurementResult> _measurements = new();
[ObservableProperty] private double _averageThermalConductivity;
[ObservableProperty] private double _averageThermalDiffusivity;
[ObservableProperty] private double _averageVolumetricHeatCapacity;
[ObservableProperty] private double _sampleVolume = 40.0;
[ObservableProperty] private bool _bubbleRemoved = true;
[ObservableProperty] private bool _usePressure = false;
[ObservableProperty] private double _pressureValue = 0.0;
[ObservableProperty] private bool _isCleanConfirmed = true;
[ObservableProperty] private string _cleanerName = "";
[ObservableProperty] private double _ambientTemperature = 25.0;
[ObservableProperty] private bool _ambientCalibrated = true;
[ObservableProperty] private bool _platinumCompatible = true;
[ObservableProperty] private string _liquidReactivityNote = "";
[ObservableProperty] private double _platinumResistance = 0.0;
[ObservableProperty] private double _chamberPressure = 0.0;
[ObservableProperty] private double _currentTestTemperature = 0.0;
[ObservableProperty] private bool _isCalibrating = false;
[ObservableProperty] private string _calibrationStatus = "";
[ObservableProperty] private string _selectedReferenceLiquid = "蒸馏水";
[ObservableProperty] private double _referenceConductivity = 0.606;
[ObservableProperty] private double _measuredConductivity = 0.0;
[ObservableProperty] private double _calibrationErrorPercent = 0.0;
// 实时电压显示(可选)
[ObservableProperty] private double _platinumVoltage;
[ObservableProperty] private double _standardResistorVoltage;
private const double EulerGamma = 0.5772156649; // 欧拉常数
//private const double WireRadius = 0.00003; // 铂丝半径 (0.03 mm)
[ObservableProperty] private double _sampleDensity = 1000.0; // 新增密度默认值1000 kg/m³
int samples = 200; // 1秒 * 1000点/秒
double heatingDuration = 1; // 加热时间 0.8 秒(需与您的加热脉冲宽度一致)
double totalDuration = 2; // 总采样时间(加热 + 冷却)
public D7896ViewModel()
{
_config = App.PlcConfig ?? new AppConfig();
_plcService = App.PlcService;
_reportService = new ReportService(_config.TestParameters.ReportOutputPath);
SampleVolume = _config.TestParameters.DefaultSampleVolume;
UsePressure = _config.TestParameters.UsePressure;
PressureValue = _config.TestParameters.DefaultPressure;
SelectedReferenceLiquid = _config.TestParameters.ReferenceLiquid;
ReferenceConductivity = _config.TestParameters.ReferenceConductivity;
IsCleanConfirmed = true;
BubbleRemoved = true;
PlatinumCompatible = true;
AmbientCalibrated = true;
// 初始化电压表服务
// TH1963 IP 地址需要根据实际配置修改,建议从配置文件读取
_th1963Ustd = new Th1963LanService();
_th1953Ustd = new Th1963LanService();
StartBackgroundMonitoring();
}
private async void StartBackgroundMonitoring()
{
await Task.Delay(1000);
_monitorTimer = new Timer(async _ => await MonitorPlcValues(), null, 0, 1000);
}
private async Task MonitorPlcValues()
{
if (!await _plcService.IsConnectedAsync()) return;
if (Application.Current == null || Application.Current.Dispatcher == null) return;
try
{
float rawResistance = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Resistance);
double newResistance = rawResistance;
Application.Current?.Dispatcher.Invoke(() => PlatinumResistance = newResistance);
float rawPressure = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Pressure);
Application.Current?.Dispatcher.Invoke(() => ChamberPressure = rawPressure);
float rawTemp = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Temperature);
Application.Current?.Dispatcher.Invoke(() => CurrentTestTemperature = rawTemp);
}
catch { }
}
//private async Task<double> GetInitialResistanceAsync()
//{
// if (!await _plcService.IsConnectedAsync()) return 0;
// try
// {
// float rawResistance = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Resistance);
// return rawResistance;
// }
// catch { return 0; }
//}
[RelayCommand]
private async Task StartTestAsync()
{
if (IsTesting)
{
MessageBox.Show("测试正在进行中", "提示");
return;
}
// 前置检查
if (!IsCleanConfirmed || !BubbleRemoved || !PlatinumCompatible || !AmbientCalibrated)
{
MessageBox.Show("请完成所有测试前确认项", "前置条件未满足");
return;
}
if (SampleVolume <= 0)
{
MessageBox.Show("请输入有效的样品量", "参数错误");
return;
}
if (UsePressure && PressureValue <= 0)
{
MessageBox.Show("请设置有效的加压值", "参数错误");
return;
}
// 连接PLC和电压表
if (!await _plcService.IsConnectedAsync())
{
if (!await _plcService.ConnectAsync())
{
MessageBox.Show("无法连接到PLC", "错误");
return;
}
}
try
{
await _th1963Ustd.ConnectAsync("192.168.1.12", 45454); // 改为实际IP
await _th1963Ustd.ConfigureForHighSpeedDcvAsync();
await _th1953Ustd.ConnectAsync("192.168.1.13", 45454); // 改为实际IP
await _th1953Ustd.ConfigureForHighSpeedDcvAsync();
}
catch (Exception ex)
{
MessageBox.Show($"电压表连接失败: {ex.Message}", "错误");
return;
}
if (UsePressure)
{
StatusMessage = "正在加压...";
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, true);
const int pressureStableTimeoutMs = 10000; // 30秒超时
const double pressureTolerance = 5.0; // 允许误差 ±5 kPa
var startTime = DateTime.Now;
bool pressureReached = false;
while ((DateTime.Now - startTime).TotalMilliseconds < pressureStableTimeoutMs)
{
await Task.Delay(500); // 每0.5秒检测一次
await UpdateRealTimeParametersAsync();
if (ChamberPressure >= PressureValue - pressureTolerance)
{
pressureReached = true;
break;
}
}
if (!pressureReached)
{
// 加压失败,关闭进气阀,中止测试
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, false);
MessageBox.Show($"加压超时,压力未能达到 {PressureValue} kPa当前 {ChamberPressure:F1} kPa", "错误");
return;
}
// 压力已达到,可关闭进气阀(或保持,看系统需求)
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, false);
StatusMessage = $"压力已稳定在 {ChamberPressure:F1} kPa";
}
//double initialResistance = await GetInitialResistanceAsync();
//if (initialResistance > 0)
// StatusMessage = $"初始电阻: {initialResistance:F4} Ω";
Measurements.Clear();
IsTesting = true;
_stopRequested = false;
_testCts = new CancellationTokenSource();
try
{
// 预热:进行一次虚拟测量
await _th1963Ustd.ConfigureForHighSpeedDcvAsync();
await _th1963Ustd.PrepareBatchAsync(10);
await _th1963Ustd.TriggerAsync();
await Task.Delay(100);
await _th1963Ustd.FetchBatchAsync(); // 丢弃结果
// 预热:进行一次虚拟测量
await _th1953Ustd.ConfigureForHighSpeedDcvAsync();
await _th1953Ustd.PrepareBatchAsync(10);
await _th1953Ustd.TriggerAsync();
await Task.Delay(100);
await _th1953Ustd.FetchBatchAsync(); // 丢弃结果
int requiredCount = _config.TestParameters.MeasurementCount; // 需要多少有效数据
int validCount = 0;
int attemptCount = 0;
int maxAttempts = requiredCount * 2; // 最多尝试次数,防止死循环
// 存储每次成功测量的结果(用于后续异常判断)
List<double> validLambdaList = new List<double>();
List<double> validAlphaList = new List<double>();
List<double> validCpList = new List<double>();
while (validCount < requiredCount && attemptCount < maxAttempts && !_stopRequested)
{
attemptCount++;
CurrentMeasurementIndex = attemptCount; // 显示当前尝试次数(不是有效次数)
StatusMessage = $"正在执行第 {attemptCount} 次测量(有效:{validCount}/{requiredCount}...";
// --- 步骤1基线采集加热前---
await _th1963Ustd.PrepareBatchAsync(50);
await _th1953Ustd.PrepareBatchAsync(50);
await Task.WhenAll(_th1963Ustd.TriggerAsync(), _th1953Ustd.TriggerAsync());
await Task.Delay(100);
double[] ustdBase = await _th1963Ustd.FetchBatchAsync();
double[] uptBase = await _th1953Ustd.FetchBatchAsync();
double dynamicR0 = 2.45; // 默认值
if (ustdBase != null && ustdBase.Length > 0 && uptBase != null && uptBase.Length > 0)
{
double sumR0 = 0; int cnt = 0;
for (int j = 0; j < ustdBase.Length; j++)
{
if (ustdBase[j] > 0.01 && uptBase[j] > 0.01)
{
sumR0 += uptBase[j] / ustdBase[j];
cnt++;
}
}
if (cnt > 0)
{
dynamicR0 = sumR0 / cnt;
Logger.Log($"基线采集 R0 = {dynamicR0:F6} Ω (有效点数: {cnt})");
}
else
{
Logger.Log("基线采集数据无效(电压过低),使用加热前瞬间点");
}
}
else
{
Logger.Log("基线采集未返回数据,将使用加热段早期点");
}
// --- 步骤2正式加热采集 ---
await _th1963Ustd.PrepareBatchAsync(samples);
await _th1953Ustd.PrepareBatchAsync(samples);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, true);
try { await Task.Delay(5, _testCts.Token); } catch (OperationCanceledException) { break; }
await Task.WhenAll(_th1963Ustd.TriggerAsync(), _th1953Ustd.TriggerAsync());
try { await Task.Delay((int)(heatingDuration * 1000), _testCts.Token); } catch (OperationCanceledException) { break; }
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, false);
int remainingMs = (int)((totalDuration - heatingDuration) * 1000) + 100;
try { await Task.Delay(remainingMs, _testCts.Token); } catch (OperationCanceledException) { break; }
double[] ustd = await _th1963Ustd.FetchBatchAsync();
double[] upt = await _th1953Ustd.FetchBatchAsync();
if (dynamicR0 == 2.45) // 基线无效
{
double sumR0 = 0; int cnt = 0;
for (int j = 2; j < Math.Min(6, ustd.Length); j++)
{
if (ustd[j] > 0.01 && upt[j] > 0.01)
{
sumR0 += upt[j] / ustd[j];
cnt++;
}
}
if (cnt > 0)
{
dynamicR0 = sumR0 / cnt;
Logger.Log($"使用加热段第2~5点计算 R0 = {dynamicR0:F6} Ω");
}
}
// 记录原始电压(调试用)
for (int j = 0; j < 20 && j < ustd.Length; j++)
{
Logger.Log($"第{j}点: U_std={ustd[j]:F6} V, U_pt={upt[j]:F6} V");
}
StandardResistorVoltage = ustd.Average();
PlatinumVoltage = upt.Average();
Logger.Log($"测量 {attemptCount}: U_std 平均值={ustd.Average():F6} V, U_pt 平均值={upt.Average():F6} V");
double[] timeArray = new double[ustd.Length];
for (int idx = 0; idx < timeArray.Length; idx++)
timeArray[idx] = idx * totalDuration / samples;
var (lambda, alpha, deltaT, coolingPoints) = ComputeThermalProperties(upt, ustd, timeArray, dynamicR0, CurrentTestTemperature);
// 计算比热容 Cp
double vhc = lambda / alpha; // kJ/(m³·K)
double cp = vhc / SampleDensity; // J/(kg·K)
Logger.Log($"测量 {attemptCount} 结果: λ={lambda:F6} W/(m·K), α={alpha:E6} m²/s, Cp={cp:F2} J/(kg·K)");
// ---- 异常值检测 ----
bool isOutlier = false;
double deviationThreshold = 0.20; // 20% 偏差阈值
if (validCount >= 2) // 至少有两个有效数据后才开始剔除
{
double avgLambda = validLambdaList.Average();
double avgAlpha = validAlphaList.Average();
double avgCp = validCpList.Average();
if (Math.Abs(lambda - avgLambda) / avgLambda > deviationThreshold ||
Math.Abs(alpha - avgAlpha) / avgAlpha > deviationThreshold ||
Math.Abs(cp - avgCp) / avgCp > deviationThreshold)
{
isOutlier = true;
Logger.Log($"第 {attemptCount} 次测量结果异常(偏差过大),予以舍弃。λ={lambda:F4}, α={alpha:E4}, Cp={cp:F2}");
}
}
if (!isOutlier)
{
// 正常结果,添加到列表
validLambdaList.Add(lambda);
validAlphaList.Add(alpha);
validCpList.Add(cp);
validCount++;
GenerateTemperatureCurveFromData(timeArray, deltaT, coolingPoints);
var result = new MeasurementResult
{
Index = validCount, // 使用有效次数编号
ThermalConductivity = lambda,
ThermalDiffusivity = alpha
};
result.CalculateVhcAndCp(SampleDensity);
Application.Current.Dispatcher.Invoke(() => Measurements.Add(result));
StatusMessage = $"第 {validCount} 次测量完成,λ={lambda:F4} W/m·K";
Logger.Log($"========== 第 {validCount} 次测量详细数据 ==========");
Logger.Log($"热导率 λ: {lambda:F6} W/(m·K)");
Logger.Log($"热扩散率 α: {alpha:E6} m²/s");
Logger.Log($"体积热容 VHC: {result.VolumetricHeatCapacity:F2} kJ/(m³·K)");
Logger.Log($"比热容 Cp: {cp:F2} J/(kg·K)");
Logger.Log($"初始电阻 R0: {dynamicR0:F6} Ω");
Logger.Log("===========================================");
}
// 测量间隔(即使舍弃也等待,让样品恢复)
if (validCount < requiredCount && !_stopRequested && attemptCount < maxAttempts)
{
try { await Task.Delay(_config.TestParameters.IntervalSeconds * 1000, _testCts.Token); } catch (OperationCanceledException) { break; }
}
}
if (validCount >= requiredCount)
{
CalculateAverages();
StatusMessage = "测试完成";
}
else
{
StatusMessage = $"测试中止。";
MessageBox.Show($"测试中止,未收集到足够有效数据({validCount}/{requiredCount})。请检查样品或仪器状态。", "提示");
}
}
catch (Exception ex)
{
StatusMessage = $"测试出错: {ex.Message}";
MessageBox.Show($"测试过程中发生错误: {ex.Message}", "错误");
}
finally
{
// 停止加热,泄压
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, false);
if (UsePressure)
{
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, false);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil, true);
await Task.Delay(1000);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil, false);
}
IsTesting = false;
//_fiveHalfUpt.Close();
_th1963Ustd.Dispose();
_th1953Ustd.Dispose();
_testCts?.Dispose();
}
}
private (double lambda, double alpha, double[] deltaT, List<DataPoint> coolingPoints) ComputeThermalProperties(
double[] upt, double[] ustd, double[] time, double initialResistance, double bathTemp)
{
int n = Math.Min(upt.Length, ustd.Length);
double[] deltaT = new double[n];
double[] ptResistance = new double[n];
double[] current = new double[n];
double tStart = _config.TestParameters.FitStartTime;
double tEnd = _config.TestParameters.FitEndTime;
// 1. 瞬时计算(先用传入的 initialResistance
for (int i = 0; i < n; i++)
{
current[i] = ustd[i] / StandardResistor;
if (current[i] > 0.001)
{
ptResistance[i] = upt[i] / current[i];
deltaT[i] = (ptResistance[i] - initialResistance) / (AlphaPt * initialResistance);
}
else
{
ptResistance[i] = double.NaN;
deltaT[i] = double.NaN;
}
}
// 滑动平均平滑窗口5
double[] smoothDeltaT = new double[n];
for (int i = 0; i < n; i++)
{
int start = Math.Max(0, i - 2);
int end = Math.Min(n - 1, i + 2);
double sum = 0; int cnt = 0;
for (int j = start; j <= end; j++)
if (!double.IsNaN(deltaT[j])) { sum += deltaT[j]; cnt++; }
smoothDeltaT[i] = cnt > 0 ? sum / cnt : double.NaN;
}
// 寻找温升峰值点
double maxDeltaT = 0;
for (int i = 5; i < n; i++)
{
if (!double.IsNaN(smoothDeltaT[i]) && smoothDeltaT[i] > maxDeltaT)
maxDeltaT = smoothDeltaT[i];
}
Logger.Log($"最大温升 = {maxDeltaT:F4} ℃");
// 拟合窗口
int startIdx = FindIndex(time, tStart);
int endIdx = FindIndex(time, tEnd);
if (startIdx < 0) startIdx = 5;
if (endIdx >= n) endIdx = n - 1;
if (endIdx <= startIdx) endIdx = Math.Min(startIdx + 50, n - 1);
Logger.Log($"拟合窗口: startIdx={startIdx}, endIdx={endIdx}, 点数={endIdx - startIdx + 1}");
// 收集拟合点
var points = new List<DataPoint>();
for (int i = startIdx; i <= endIdx; i++)
{
if (i <= startIdx + 5) // 只打印前5个点
Logger.Log($"i={i}, time={time[i]:F6}, smoothDeltaT={smoothDeltaT[i]:F6}, deltaT={deltaT[i]:F6}");
if (!double.IsNaN(smoothDeltaT[i]) && smoothDeltaT[i] > 0 && time[i] > 0)
points.Add(new DataPoint(Math.Log(time[i]), smoothDeltaT[i]));
}
if (points.Count < 10)
{
Logger.Log($"警告:有效拟合点数仅 {points.Count},测量无效");
return (0, 0, deltaT, new List<DataPoint>());
}
(double slope, double intercept) = LinearRegression(points);
if (slope <= 0.001)
{
Logger.Log($"警告:拟合斜率 {slope:E} 过小或为负,测量无效");
return (0, 0, deltaT, new List<DataPoint>());
}
// 打印前10个拟合点
foreach (var p in points.Take(10))
Logger.Log($"ln(t)={p.X:F4}, ΔT={p.Y:F4}");
// 计算功率密度
double sumI = 0, sumR = 0;
int cntI = 0, cntR = 0;
for (int i = startIdx; i <= endIdx; i++)
{
if (!double.IsNaN(current[i]) && Math.Abs(current[i]) > 1e-12) { sumI += current[i]; cntI++; }
if (!double.IsNaN(ptResistance[i]) && ptResistance[i] > 1e-12) { sumR += ptResistance[i]; cntR++; }
}
if (cntI == 0 || cntR == 0) return (0, 0, deltaT, new List<DataPoint>());
double avgCurrent = sumI / cntI;
double avgResistance = sumR / cntR;
double wireLength = Math.Max(1e-12, _config.TestParameters.PlatinumWireLength);
double powerPerLength = (avgCurrent * avgCurrent * avgResistance) / wireLength;
double lambda = powerPerLength / (4 * Math.PI * slope);
if (_config.TestParameters.UseFixedLambda)
{
lambda = _config.TestParameters.FixedLambda;
Logger.Log($"使用固定 lambda={lambda:F6} W/(m·K)");
}
lambda *= _config.TestParameters.CalibrationCoefficients.ThermalDiffusivityCorrection;
Logger.Log($"constantCurrent(avg)={avgCurrent:E6} A, avgResistance={avgResistance:F6} Ω, powerPerLength={powerPerLength:E6} W/m, 斜率 B = {slope:F5}");
// 计算热扩散率
double exponent = intercept / slope + EulerGamma;
if (exponent > 30) exponent = 30;
double alpha = (_config.TestParameters.PlatinumWireDiameter / 2 * _config.TestParameters.PlatinumWireDiameter / 2 / 4.0) * Math.Exp(exponent);
alpha *= _config.TestParameters.CalibrationCoefficients.ThermalConductivityCorrection;
if (_config.TestParameters.UseFixedAlpha)
{
alpha = _config.TestParameters.FixedAlpha;
Logger.Log($"使用固定 alpha={alpha:E6} m²/s");
}
if (alpha <= 0 || double.IsNaN(alpha) || alpha > 1e-5)
{
Logger.Log($"警告:α 计算异常 ({alpha:E}),数据可能不可靠");
alpha = double.NaN;
}
Logger.Log($"热导率 λ = {lambda:F6} W/(m·K) | 热扩散率 α = {alpha:E6} m²/s | 截距/斜率 = {intercept / slope:F3}");
// 冷却曲线
var coolingPoints = new List<DataPoint>();
int coolStart = FindIndex(time, heatingDuration);
int coolEnd = FindIndex(time, totalDuration);
for (int i = coolStart; i <= coolEnd; i++)
if (!double.IsNaN(deltaT[i]) && deltaT[i] > 0.01)
coolingPoints.Add(new DataPoint(time[i], deltaT[i]));
// 导出CSV
//try
//{
// string tmp = Path.GetTempPath();
// string baseName = $"measure_{SampleId}_{DateTime.Now:yyyyMMdd_HHmmss}_{CurrentMeasurementIndex}";
// string dataPath = Path.Combine(tmp, baseName + ".csv");
// ExportMeasurementCsv(dataPath, time, ustd, upt, deltaT, startIdx, endIdx);
// Logger.Log($"已导出测量数据 CSV: {dataPath}");
// string winPath = Path.Combine(tmp, baseName + "_windows.csv");
// ExportCandidateWindowsCsv(winPath, time, deltaT, startIdx, endIdx);
// Logger.Log($"已导出候选拟合窗 CSV: {winPath}");
//}
//catch (Exception ex)
//{
// Logger.Log($"导出CSV失败: {ex.Message}");
//}
return (lambda, alpha, deltaT, coolingPoints);
}
/// <summary>
/// 最小二乘法线性回归,返回 (斜率, 截距)
/// </summary>
private (double slope, double intercept) LinearRegression(List<DataPoint> points)
{
if (points.Count < 2) return (0.001, 0);
double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
foreach (var p in points)
{
sumX += p.X;
sumY += p.Y;
sumXY += p.X * p.Y;
sumX2 += p.X * p.X;
}
double n = points.Count;
double denominator = n * sumX2 - sumX * sumX;
if (Math.Abs(denominator) < 1e-10) return (0.001, 0);
double slope = (n * sumXY - sumX * sumY) / denominator;
double intercept = (sumY - slope * sumX) / n;
return (slope, intercept);
}
/// <summary>
/// 查找时间数组中与目标时间最接近的索引
/// </summary>
private int FindIndex(double[] timeArray, double targetTime)
{
for (int i = 0; i < timeArray.Length; i++)
{
if (timeArray[i] >= targetTime)
return i;
}
return timeArray.Length - 1;
}
private void ExportMeasurementCsv(string path, double[] time, double[] ustd, double[] upt, double[] deltaT, int fitStart, int fitEnd)
{
using var sw = new StreamWriter(path, false, Encoding.UTF8);
sw.WriteLine("index,time,U_std,U_pt,deltaT,fitWindow");
int n = Math.Min(Math.Min(time.Length, ustd.Length), deltaT.Length);
for (int i = 0; i < n; i++)
{
bool inFit = (i >= fitStart && i <= fitEnd);
sw.WriteLine($"{i},{time[i]:F6},{ustd[i]:F6},{upt[i]:F6},{(double.IsNaN(deltaT[i]) ? 0 : deltaT[i]):F6},{(inFit ? 1 : 0)}");
}
}
private void ExportCandidateWindowsCsv(string path, double[] time, double[] deltaT, int startIdx, int endIdx)
{
using var sw = new StreamWriter(path, false, Encoding.UTF8);
sw.WriteLine("s,e,t_start,t_end,meanDelta");
int n = time.Length;
int minPts = 10;
for (int s = startIdx; s <= endIdx - minPts; s++)
{
for (int e = s + minPts - 1; e <= endIdx; e++)
{
double mean = deltaT.Skip(s).Take(e - s + 1).Where(x => !double.IsNaN(x)).DefaultIfEmpty(0).Average();
sw.WriteLine($"{s},{e},{time[s]:F6},{time[e]:F6},{mean:F6}");
}
}
}
///// <summary>
///// 最小二乘法拟合斜率 (X轴为横坐标Y轴为纵坐标) — 用于加热段 ln(t) vs ΔT
///// </summary>
//private double LeastSquaresSlope(List<DataPoint> points)
//{
// if (points.Count < 2) return 0.001;
// double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
// foreach (var p in points)
// {
// sumX += p.X;
// sumY += p.Y;
// sumXY += p.X * p.Y;
// sumX2 += p.X * p.X;
// }
// double n = points.Count;
// double denominator = n * sumX2 - sumX * sumX;
// if (Math.Abs(denominator) < 1e-10) return 0.001;
// double slope = (n * sumXY - sumX * sumY) / denominator;
// return slope;
//}
///// <summary>
///// 最小二乘法拟合斜率 (X轴为时间tY轴为 ln(ΔT)) — 用于冷却段
///// </summary>
//private double LeastSquaresSlopeOnTime(List<DataPoint> points)
//{
// if (points.Count < 2) return -1.0;
// double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
// foreach (var p in points)
// {
// sumX += p.X;
// sumY += p.Y;
// sumXY += p.X * p.Y;
// sumX2 += p.X * p.X;
// }
// double n = points.Count;
// double denominator = n * sumX2 - sumX * sumX;
// if (Math.Abs(denominator) < 1e-10) return -1.0;
// double slope = (n * sumXY - sumX * sumY) / denominator;
// return slope;
//}
private void GenerateTemperatureCurveFromData(double[] time, double[] deltaT, List<DataPoint> coolingPoints)
{
if (TemperatureCurveModel == null)
{
TemperatureCurveModel = new PlotModel { Title = "温升与冷却曲线", Background = OxyColors.White };
TemperatureCurveModel.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, Title = "时间 (s)" });
TemperatureCurveModel.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = "温升 (℃)" });
}
// 根据当前测量索引计算色相 (0 ~ 1)
double hue = ((CurrentMeasurementIndex - 1) % 10) / 10.0; // 0, 0.1, 0.2 ... 0.9
// 加热曲线颜色饱和度0.9亮度0.8
var heatColor = OxyColor.FromHsv(hue, 0.9, 0.8);
// 冷却曲线颜色色相偏移0.5互补色饱和度0.6亮度0.8(较淡)
double coolHue = hue + 0.5;
if (coolHue >= 1.0) coolHue -= 1.0;
var coolColor = OxyColor.FromHsv(coolHue, 0.6, 0.8);
// 加热段曲线(红色系实线)
var heatingSeries = new LineSeries
{
Title = $"第{CurrentMeasurementIndex}次 - 加热",
Color = heatColor,
StrokeThickness = 1.5
};
for (int i = 0; i < time.Length && time[i] <= 1.0; i++)
{
heatingSeries.Points.Add(new DataPoint(time[i], deltaT[i]));
}
TemperatureCurveModel.Series.Add(heatingSeries);
// 冷却曲线(互补色,虚线)
if (coolingPoints != null && coolingPoints.Count > 0)
{
var coolingSeries = new LineSeries
{
Title = $"第{CurrentMeasurementIndex}次 - 冷却",
Color = coolColor,
StrokeThickness = 1.5,
LineStyle = LineStyle.Dash
};
foreach (var p in coolingPoints)
{
coolingSeries.Points.Add(p);
}
TemperatureCurveModel.Series.Add(coolingSeries);
}
TemperatureCurveModel.InvalidatePlot(true);
CurveTitle = $"已完成 {CurrentMeasurementIndex} 次测量";
}
private void CalculateAverages()
{
if (Measurements.Count == 0) return;
AverageThermalConductivity = Measurements.Average(m => m.ThermalConductivity);
AverageThermalDiffusivity = Measurements.Average(m => m.ThermalDiffusivity);
AverageVolumetricHeatCapacity = Measurements.Average(m => m.VolumetricHeatCapacity);
}
[RelayCommand]
private void Reset()
{
Measurements.Clear();
AverageThermalConductivity = AverageThermalDiffusivity = AverageVolumetricHeatCapacity = 0;
CurrentMeasurementIndex = 0;
StatusMessage = "已重置";
TestDateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
TemperatureCurveModel = null;
}
[RelayCommand]
private async Task GenerateReportAsync()
{
if (Measurements.Count == 0)
{
MessageBox.Show("没有测试数据", "提示");
return;
}
try
{
var extraParams = new Dictionary<string, object>
{
["SampleVolume"] = SampleVolume,
["BubbleRemoved"] = BubbleRemoved,
["UsePressure"] = UsePressure,
["PressureValue"] = PressureValue,
["IsCleanConfirmed"] = IsCleanConfirmed,
["CleanerName"] = CleanerName,
["AmbientTemperature"] = AmbientTemperature,
["AmbientCalibrated"] = AmbientCalibrated,
["PlatinumCompatible"] = PlatinumCompatible,
["LiquidReactivityNote"] = LiquidReactivityNote,
["InitialResistance"] = PlatinumResistance
};
string reportPath = await _reportService.GenerateReportAsync(SampleId, TestTemperature, Measurements.ToList(),
AverageThermalConductivity, AverageThermalDiffusivity, AverageVolumetricHeatCapacity,
_config.TestParameters, extraParams);
MessageBox.Show($"报告已生成: {reportPath}", "成功");
}
catch (Exception ex)
{
MessageBox.Show($"生成报告失败: {ex.Message}", "错误");
}
}
[RelayCommand]
private async Task StopTest()
{
if (!IsTesting) return;
_stopRequested = true;
_testCts?.Cancel(); // 取消所有等待的 Delay
StatusMessage = "正在停止测试...";
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.StartCommand, false);
if (UsePressure)
{
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, false);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil, true);
await Task.Delay(1000);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil, false);
}
IsTesting = false;
StatusMessage = "测试已停止。";
}
[RelayCommand] private async Task PressureCalibrationAsync() => await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.PressureCalibrationCoil, true);
[RelayCommand] private async Task ResistanceZeroAsync() => await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.ResistanceZeroCoil, true);
[RelayCommand]
private async Task InletValveControlAsync()
{
bool current = await _plcService.ReadCoilAsync(_config.PlcRegisterAddresses.InletValveCoil);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.InletValveCoil, !current);
StatusMessage = $"进气阀已{(current ? "" : "")}";
}
[RelayCommand]
private async Task OutletValveControlAsync()
{
bool current = await _plcService.ReadCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil);
await _plcService.WriteCoilAsync(_config.PlcRegisterAddresses.OutletValveCoil, !current);
StatusMessage = $"排气阀已{(current ? "" : "")}";
}
[RelayCommand] private void ConfirmBubbleRemoved() => BubbleRemoved = true;
[RelayCommand]
private void ConfirmClean()
{
if (string.IsNullOrWhiteSpace(CleanerName))
{
MessageBox.Show("请输入清洁人员姓名", "提示");
return;
}
IsCleanConfirmed = true;
}
[RelayCommand] private void ConfirmPlatinumCompatible() => PlatinumCompatible = true;
[RelayCommand]
private async Task CalibrateAmbientAsync()
{
await EnsureConnected();
float temp = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Temperature);
AmbientTemperature = temp;
AmbientCalibrated = true;
StatusMessage = $"环境温度校准完成:{AmbientTemperature:F1} °C";
}
[RelayCommand] private async Task PerformSystemCalibrationAsync() { /* 系统校准逻辑待实现 */ }
private async Task EnsureConnected()
{
if (!await _plcService.IsConnectedAsync())
await _plcService.ConnectAsync();
}
private async Task UpdateRealTimeParametersAsync()
{
if (!await _plcService.IsConnectedAsync()) return;
try
{
float rawPressure = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Pressure);
ChamberPressure = rawPressure / 10.0;
}
catch { }
}
}