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 OxyPlot.Wpf; using PdfSharpCore.Drawing; using PdfSharpCore.Pdf; 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 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 _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 GetInitialResistanceAsync() //{ // if (!await _plcService.IsConnectedAsync()) return 0; // try // { // float rawResistance = await _plcService.ReadFloatAsync(_config.PlcRegisterAddresses.Resistance); // return rawResistance; // } // catch { return 0; } //} // 在类成员变量区域添加 private int currentSettleMs = 200; // 电流稳定等待时间(毫秒) [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 * 3; // 最多尝试次数,防止死循环 // 存储每次成功测量的结果(用于后续异常判断) List validLambdaList = new List(); List validAlphaList = new List(); List validCpList = new List(); while (validCount < requiredCount && attemptCount < maxAttempts && !_stopRequested) { attemptCount++; CurrentMeasurementIndex = attemptCount; // 显示当前尝试次数(不是有效次数) //StatusMessage = $"正在执行第{validCount}次测量)..."; // --- 步骤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 = 1.49; // 默认值 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(currentSettleMs, _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 == 1.49) // 基线无效 { 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($"测量 {validCount} 结果: λ={lambda:F6} W/(m·K), α={alpha:E6} m²/s, Cp={cp:F2} J/(kg·K)"); // ---- 异常值检测 ---- bool isOutlier = false; double deviationThreshold = 0.1; // 10% 偏差阈值 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 * 200, _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 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; } } // 滑动平均平滑(窗口大小改为 11) int windowSize = 40; // 原为 5 double[] smoothDeltaT = new double[n]; for (int i = 0; i < n; i++) { int start = Math.Max(0, i - windowSize / 2); int end = Math.Min(n - 1, i + windowSize / 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(); 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()); } (double slope, double intercept) = LinearRegression(points); if (slope <= 0.001) { Logger.Log($"警告:拟合斜率 {slope:E} 过小或为负,测量无效"); return (0, 0, deltaT, new List()); } // 打印前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()); 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(); 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); } /// /// 最小二乘法线性回归,返回 (斜率, 截距) /// private (double slope, double intercept) LinearRegression(List 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); } /// /// 查找时间数组中与目标时间最接近的索引 /// 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; } ///// ///// 最小二乘法拟合斜率 (X轴为横坐标,Y轴为纵坐标) — 用于加热段 ln(t) vs ΔT ///// //private double LeastSquaresSlope(List 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; //} ///// ///// 最小二乘法拟合斜率 (X轴为时间t,Y轴为 ln(ΔT)) — 用于冷却段 ///// //private double LeastSquaresSlopeOnTime(List 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 coolingPoints) { if (TemperatureCurveModel == null) { TemperatureCurveModel = new PlotModel { }; 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"); // 清空曲线:新建一个空白的 PlotModel 替换原来的 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 = "温升 (℃)" }); TemperatureCurveModel.InvalidatePlot(true); CurveTitle = "温升曲线"; } [RelayCommand] private async Task GenerateReportAsync() { if (Measurements.Count == 0) { MessageBox.Show("没有测试数据", "提示"); return; } // 选择保存路径 var saveFileDialog = new Microsoft.Win32.SaveFileDialog { Filter = "PDF files (*.pdf)|*.pdf", DefaultExt = ".pdf", FileName = $"报告_{SampleId}_{DateTime.Now:yyyyMMdd_HHmmss}.pdf" }; if (saveFileDialog.ShowDialog() != true) return; string pdfPath = saveFileDialog.FileName; try { // 先在 UI 线程导出曲线图为字节数组 byte[] chartImageBytes = null; await Application.Current.Dispatcher.InvokeAsync(() => { if (TemperatureCurveModel != null && TemperatureCurveModel.Series.Count > 0) { using (var stream = new MemoryStream()) { var exporter = new PngExporter { Width = 600, Height = 400 }; exporter.Export(TemperatureCurveModel, stream); chartImageBytes = stream.ToArray(); } } }); // 然后在后台线程生成 PDF(避免阻塞 UI) await Task.Run(() => GeneratePdfReport(pdfPath, chartImageBytes)); MessageBox.Show($"报表已生成: {pdfPath}", "成功"); } catch (Exception ex) { MessageBox.Show($"生成报表失败: {ex.Message}", "错误"); } } private void GeneratePdfReport(string filePath, byte[] chartImageBytes) { using (var document = new PdfDocument()) { // 创建第一个页面 var page = document.AddPage(); page.Width = XUnit.FromMillimeter(210); page.Height = XUnit.FromMillimeter(297); var gfx = XGraphics.FromPdfPage(page); var titleFont = new XFont("SimHei", 16, XFontStyle.Bold); var headerFont = new XFont("SimHei", 12, XFontStyle.Bold); var normalFont = new XFont("SimHei", 10, XFontStyle.Regular); double yPosition = 30; // 标题 gfx.DrawString("ASTM D7896-19 瞬态热线法测试报告", titleFont, XBrushes.Black, new XRect(0, yPosition, page.Width, 30), XStringFormats.TopCenter); yPosition += 40; // 基础信息(只显示一次) gfx.DrawString($"样品名称: {SampleId}", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"测试温度: {TestTemperature:F1} °C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"测试时间: {TestDateTime}", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"样品体积: {SampleVolume:F1} mL", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"样品密度: {SampleDensity:F0} kg/m^3", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"环境温度: {AmbientTemperature:F1} °C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"铂丝电阻温度系数: {AlphaPt:F4} /°C", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"铂丝长度: {_config.TestParameters.PlatinumWireLength:F3} m", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 22; gfx.DrawString($"铂丝直径: {_config.TestParameters.PlatinumWireDiameter * 1000:F2} mm", normalFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 35; // 曲线图(如果有) if (chartImageBytes != null) { using (var imgStream = new MemoryStream(chartImageBytes)) { var image = XImage.FromStream(() => new MemoryStream(imgStream.ToArray())); gfx.DrawImage(image, 40, yPosition, 500, 330); yPosition += 350; } } // 表格标题 gfx.DrawString("测量结果明细", headerFont, XBrushes.Black, new XPoint(40, yPosition)); yPosition += 25; // 表头(已替换特殊符号) string[] headers = { "序号", "热导率λ(W/(m.K))", "热扩散率α(10^-7 m^2/s)", "体积热容VHC(kJ/(m^3.K))", "比热容Cp(J/(kg.K))" }; double[] colWidths = { 50, 115, 125, 125, 125 }; double startX = 40; // 辅助函数:绘制表头(用于新页) void DrawHeader(XGraphics g, double y) { for (int i = 0; i < headers.Length; i++) { double cellX = startX + colWidths.Take(i).Sum(); g.DrawRectangle(XPens.Black, cellX, y, colWidths[i], 20); var textRect = new XRect(cellX, y, colWidths[i], 20); g.DrawString(headers[i], normalFont, XBrushes.Black, textRect, XStringFormats.Center); } } // 绘制第一页的表头 DrawHeader(gfx, yPosition); double currentRowY = yPosition + 20; // 表头高度20 const double rowHeight = 20; const double bottomMargin = 50; // 页面底部留白 // 遍历所有测量数据 foreach (var m in Measurements) { // 检查是否需要换页 if (currentRowY + rowHeight > page.Height - bottomMargin) { gfx.Dispose(); page = document.AddPage(); page.Width = XUnit.FromMillimeter(210); page.Height = XUnit.FromMillimeter(297); gfx = XGraphics.FromPdfPage(page); gfx.DrawString("ASTM D7896-19 瞬态热线法测试报告(续)", headerFont, XBrushes.Black, new XRect(0, 30, page.Width, 30), XStringFormats.TopCenter); currentRowY = 80; DrawHeader(gfx, currentRowY - 20); } string[] rowData = { m.Index.ToString(), m.ThermalConductivity.ToString("F6"), (m.ThermalDiffusivity * 1e7).ToString("F3"), m.VolumetricHeatCapacity.ToString("F2"), m.SpecificHeatCapacity.ToString("F2") }; for (int j = 0; j < rowData.Length; j++) { double cellX = startX + colWidths.Take(j).Sum(); gfx.DrawRectangle(XPens.Black, cellX, currentRowY, colWidths[j], rowHeight); var textRect = new XRect(cellX, currentRowY, colWidths[j], rowHeight); gfx.DrawString(rowData[j], normalFont, XBrushes.Black, textRect, XStringFormats.Center); } currentRowY += rowHeight; } // 添加空行,避免平均值与表格重叠 currentRowY += 60; // 检查是否需要换页(平均值部分) if (currentRowY + 80 > page.Height - bottomMargin) { gfx.Dispose(); page = document.AddPage(); page.Width = XUnit.FromMillimeter(210); page.Height = XUnit.FromMillimeter(297); gfx = XGraphics.FromPdfPage(page); currentRowY = 50; } // 平均值(单位已替换) gfx.DrawString($"平均热导率: {AverageThermalConductivity:F6} W/(m.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; gfx.DrawString($"平均热扩散率: {AverageThermalDiffusivity:E6} m^2/s", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; gfx.DrawString($"平均体积热容: {AverageVolumetricHeatCapacity:F2} kJ/(m^3.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 20; // 比热容转换:VHC 单位 kJ/(m^3.K) 先转为 J/(m^3.K) 再除以密度得到 J/(kg.K) double avgCp_J_per_kgK = (AverageVolumetricHeatCapacity * 1000) / SampleDensity; gfx.DrawString($"平均比热容: {avgCp_J_per_kgK:F0} J/(kg.K)", normalFont, XBrushes.Black, new XPoint(40, currentRowY)); currentRowY += 40; // 页脚 gfx.DrawString($"生成时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}", normalFont, XBrushes.Black, new XPoint(40, page.Height - 30)); gfx.Dispose(); document.Save(filePath); } } [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; } catch { } } }