using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using OxyPlot; using OxyPlot.Axes; using OxyPlot.Series; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; using TabletTester2025.Helpers; using TabletTester2025.Models; using TabletTester2025.Services; namespace TabletTester2025.ViewModels { public partial class StationViewModel : ObservableObject { private readonly IPlcService _plc; private readonly PlcConfiguration _plcConfig; private readonly DatabaseService _db; private readonly ExcelExportService _excel; private readonly AlarmService _alarm; private DispatcherTimer _disintegrationTimer; private bool _isLoadingDissolution1Time; private bool _isLoadingDissolution2Time; private bool _isLoadingDissolution1SampleInterval; private bool _isLoadingDissolution2SampleInterval; private bool _isLoadingDisintegrationTime; private bool _isLoadingDisintegrationSpeed; private bool _isUpdatingFriabilityWeightFromPlc; private readonly List _dissolution1Times = new(); private readonly List _dissolution1Values = new(); private readonly List _dissolution2Times = new(); private readonly List _dissolution2Values = new(); public ObservableCollection DissolutionSamplePoints { get; } = new(); private DateTime _dissolution1StartTime = DateTime.MinValue; private DateTime _dissolution2StartTime = DateTime.MinValue; private bool _isDissolution1Running; private bool _isDissolution2Running; private bool _dissolution1SampleRequestActive; private bool _dissolution2SampleRequestActive; private bool _isDissolution1SamplePromptOpen; private bool _isDissolution2SamplePromptOpen; private bool _discardDisintegrationResult; private string _dissolutionResultChannel = ""; private double _dissolutionResultRate30Min; private double _dissolutionResultRSquared; public int StationId { get; } [ObservableProperty] private TestType _currentTest; [ObservableProperty] private TestPhase _phase = TestPhase.Idle; // 硬度 [ObservableProperty] private double _hardnessValue; [ObservableProperty] private bool _hardnessPass; [ObservableProperty] private double _hardnessAvg; [ObservableProperty] private double _hardnessRSD; [ObservableProperty] private double _hardnessInternalMin = 40; [ObservableProperty] private double _hardnessInternalMax = 60; //硬度新增 //[ObservableProperty] private int _hardnessForward = 0;//硬前进 //[ObservableProperty] private int _hardnessBack = 1;//硬后退 //[ObservableProperty] private int _hardnessOver = 72; //硬度完成 //[ObservableProperty] private int _hardnessStartOver = 92;// 硬复位完成 //[ObservableProperty] private int _hardnessLimit = 298;// 硬度电机极限输入 [ObservableProperty] private double _hardnessSudu = 300;// 硬度速度输入mm/min [ObservableProperty] private double _hardnessWeiyi = 100; // 硬度位移输入mm //[ObservableProperty] private double _hardnessPoSun = 400; // 硬度破损判定输入N //[ObservableProperty] private int _hardnessMaxN = 72; //最大力采集 private List _hardnessResults = new(); // 脆碎度 [ObservableProperty] private double _weightBefore; [ObservableProperty] private double _weightAfter; [ObservableProperty] private double _lossPercent; [ObservableProperty] private bool _friabilityPass; // 崩解 [ObservableProperty] private double _disintegrationTemp; [ObservableProperty] private int _disintegrationSeconds; [ObservableProperty] private bool _isBasketMovingUp; [ObservableProperty] private bool[] _tubesCompleted = new bool[6]; [ObservableProperty] private int _remainingTubes; // 溶出 [ObservableProperty] private double _dissolutionRpm; [ObservableProperty] private double _dissolutionPercent; [ObservableProperty] private double _dissolution1Percent; [ObservableProperty] private double _dissolution2Percent; // 硬度相关新增 [ObservableProperty] private int _hardnessTestCount = 6; [ObservableProperty] private int _hardnessIntervalSec = 2; [ObservableProperty] private int _hardnessCurrentCount; [ObservableProperty] private double _hardnessMax; [ObservableProperty] private double _hardnessMin; [ObservableProperty] private bool _disintegrationPass; // 新增 [ObservableProperty] private bool _dissolutionPass; // 新增 public IAsyncRelayCommand HardnessUpCommand { get; } public IAsyncRelayCommand HardnessDownCommand { get; } public IAsyncRelayCommand HardnessResetCommand { get; } public IAsyncRelayCommand HardnessForward { get; }//前进 public IAsyncRelayCommand HardnessBack { get; }//后退 // 脆碎度新增 [ObservableProperty] private double _friabilityTargetRpm = 25; [ObservableProperty] private int _friabilityTargetTimeSec = 4; [ObservableProperty] private int _friabilityTargetRounds = 100; [ObservableProperty] private double _friabilityMaxLossPercent = 1.0; [ObservableProperty] private bool _friabilityClockwise = true; [ObservableProperty] private bool _friabilityCounterClockwise; [ObservableProperty] private double _friabilityCurrentRpm; [ObservableProperty] private int _friabilityRemainingRounds = 100; public IAsyncRelayCommand StopHardnessCommand { get; } public IAsyncRelayCommand StopFriabilityCommand { get; } public IAsyncRelayCommand ResetFriabilityCommand { get; } public IAsyncRelayCommand PrintFriabilityCommand { get; } // 溶出度新增 [ObservableProperty] private double _dissolutionUpDownFreq = 32; [ObservableProperty] private int _dissolutionSampleInterval = 5; [ObservableProperty] private double _dissolutionTargetRpm = 50; [ObservableProperty] private int _dissolution1TimeMin = 30; [ObservableProperty] private int _dissolution2TimeMin = 30; [ObservableProperty] private double _dissolution1SampleIntervalMin = 5; [ObservableProperty] private double _dissolution2SampleIntervalMin = 5; [ObservableProperty] private double _dissolutionElapsedTime; [ObservableProperty] private double _dissolutionCountdown; [ObservableProperty] private double _dissolutionRSquared; [ObservableProperty] private double _dissolution1RSquared; [ObservableProperty] private double _dissolution2RSquared; [ObservableProperty] private string _dissolutionCurveStatus = ""; public IAsyncRelayCommand DissolutionUpCommand { get; } public IAsyncRelayCommand DissolutionDownCommand { get; } public IAsyncRelayCommand StopDissolutionCommand { get; } public IAsyncRelayCommand PrintDissolutionCommand { get; } public IAsyncRelayCommand StartDissolution1Command { get; } public IAsyncRelayCommand StopDissolution1Command { get; } public IAsyncRelayCommand ResetDissolution1Command { get; } public IAsyncRelayCommand StartDissolution2Command { get; } public IAsyncRelayCommand StopDissolution2Command { get; } public IAsyncRelayCommand ResetDissolution2Command { get; } // 崩解新增 [ObservableProperty] private double _disintegrationTargetFreq = 31; [ObservableProperty] private double _disintegrationSpeedRpm = 31; [ObservableProperty] private double _disintegrationTimeMin = 15; [ObservableProperty] private string _disintegrationDosageForm = "普通片"; public IAsyncRelayCommand StopDisintegrationCommand { get; } public IAsyncRelayCommand ResetDisintegrationCommand { get; } public IAsyncRelayCommand PrintDisintegrationCommand { get; } public PlotModel DissolutionPlotModel { get; } private readonly LineSeries _dissolution1Series; private readonly LineSeries _dissolution2Series; // 命令 public IAsyncRelayCommand StartHardnessCommand { get; } public IAsyncRelayCommand StartFriabilityCommand { get; } public IAsyncRelayCommand StartDisintegrationCommand { get; } public IAsyncRelayCommand StartDissolutionCommand { get; } public IAsyncRelayCommand ExportHistoryCommand { get; } [ObservableProperty] private string _localAlarm; public StationViewModel(int id, IPlcService plc, PlcConfiguration plcConfig, DatabaseService db, ExcelExportService excel, AlarmService alarm) { StationId = id; _plc = plc; _plcConfig = plcConfig; _db = db; _excel = excel; _alarm = alarm; LoadPharmaDefaults(); StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync); StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync); StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync); StartDissolutionCommand = new AsyncRelayCommand(StartDissolution1Async); ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync); // 溶出曲线 DissolutionPlotModel = new PlotModel { Title = "溶出曲线" }; DissolutionPlotModel.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, Title = "时间 (min)", Minimum = 0, MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.Dot }); DissolutionPlotModel.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = "溶出度 (%)", Minimum = 0, Maximum = 150, MajorGridlineStyle = LineStyle.Solid, MinorGridlineStyle = LineStyle.Dot }); _dissolution1Series = new LineSeries { Title = "溶出1", Color = OxyColors.SeaGreen, StrokeThickness = 2 }; _dissolution2Series = new LineSeries { Title = "溶出2", Color = OxyColors.DodgerBlue, StrokeThickness = 2 }; DissolutionPlotModel.Series.Add(_dissolution1Series); DissolutionPlotModel.Series.Add(_dissolution2Series); // 硬度命令 //HardnessUpCommand = new AsyncRelayCommand(async () => //{ // await _plc.WriteCoilAsync(0x20, true); // await Task.Delay(100); // await _plc.WriteCoilAsync(0x20, false); //}); HardnessDownCommand = new AsyncRelayCommand(async () => { await _plc.WriteCoilAsync(0x21, true); await Task.Delay(100); await _plc.WriteCoilAsync(0x21, false); }); //复位 HardnessResetCommand = new AsyncRelayCommand(async () => { // 1. 标准PLC按钮脉冲逻辑:置1 → 延时 → 置0(模拟按下再松开按钮) await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, true); await Task.Delay(100); // 脉冲宽度,可根据PLC程序调整20~100ms await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, false); _hardnessResults.Clear(); HardnessValue = 0; HardnessAvg = 0; HardnessRSD = 0; HardnessCurrentCount = 0; HardnessMax = 0; HardnessMin = 0; Phase = TestPhase.Idle; }); // 硬前进按钮命令 HardnessForward = new AsyncRelayCommand(async () => { await _plc.WriteCoilAsync(_plcConfig.HardnessForward, true); await Task.Delay(100); // 脉冲宽度,和复位按钮保持一致 await _plc.WriteCoilAsync(_plcConfig.HardnessForward, false); }); // 硬后退按钮命令 HardnessBack = new AsyncRelayCommand(async () => { await _plc.WriteCoilAsync(_plcConfig.HardnessBack, true); await Task.Delay(100); // 脉冲宽度,和复位按钮保持一致 await _plc.WriteCoilAsync(_plcConfig.HardnessBack, false); }); // 硬度命令停止 StopHardnessCommand = new AsyncRelayCommand(async() => { await _plc.WriteCoilAsync(_plcConfig.HardnessStartStop, true); await Task.Delay(100); // 脉冲宽度,可根据PLC程序调整20~100ms await _plc.WriteCoilAsync(_plcConfig.HardnessStartStop, false); Phase = TestPhase.Idle; }); // 脆碎度命令 StopFriabilityCommand = new AsyncRelayCommand(async () => { //测试停止 if (_plcConfig.FriabilityStartCoilStop != 0) await PulseCoilAsync(_plcConfig.FriabilityStartCoilStop); Phase = TestPhase.Idle; }); ResetFriabilityCommand = new AsyncRelayCommand(() => { FriabilityRemainingRounds = FriabilityTargetRounds; LossPercent = 0; // 失重率清零 SetFriabilityWeightFromPlc(0, 0); // 清空界面结果,不向PLC写零 return Task.CompletedTask; }); PrintFriabilityCommand = new AsyncRelayCommand(async () => await PrintReport("脆碎度")); // 溶出度命令 DissolutionUpCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x22, true)); DissolutionDownCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x23, true)); StartDissolution1Command = new AsyncRelayCommand(StartDissolution1Async); StopDissolution1Command = new AsyncRelayCommand(StopDissolution1Async); ResetDissolution1Command = new AsyncRelayCommand(ResetDissolution1Async); StartDissolution2Command = new AsyncRelayCommand(StartDissolution2Async); StopDissolution2Command = new AsyncRelayCommand(StopDissolution2Async); ResetDissolution2Command = new AsyncRelayCommand(ResetDissolution2Async); StopDissolutionCommand = new AsyncRelayCommand(StopDissolution1Async); PrintDissolutionCommand = new AsyncRelayCommand(async () => await PrintReport("溶出度")); // 崩解命令 StopDisintegrationCommand = new AsyncRelayCommand(StopDisintegrationAsync); ResetDisintegrationCommand = new AsyncRelayCommand(ResetDisintegrationAsync); PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解")); _ = LoadDisintegrationTimeAsync(); _ = LoadDisintegrationSpeedAsync(); _ = LoadDissolutionTimesAsync(); _ = LoadFriabilityWeightsAsync(); } private void LoadPharmaDefaults() { var p = App.CurrentPharmaParams; HardnessInternalMin = p.HardnessMin_N; HardnessInternalMax = p.HardnessMax_N; HardnessTestCount = Math.Max(1, p.HardnessTestCount); FriabilityTargetRpm = p.FriabilityTargetRpm > 0 ? p.FriabilityTargetRpm : 25; FriabilityTargetRounds = p.FriabilityTargetRounds > 0 ? p.FriabilityTargetRounds : 100; FriabilityMaxLossPercent = p.FriabilityMaxLossPercent; FriabilityRemainingRounds = FriabilityTargetRounds; DisintegrationDosageForm = string.IsNullOrWhiteSpace(p.DisintegrationDosageForm) ? "普通片" : p.DisintegrationDosageForm; int seconds = ResolveDisintegrationLimitSeconds(); if (seconds > 0) DisintegrationTimeMin = seconds / 60.0; } private async Task PrintReport(string testName) { await App.Current.Dispatcher.InvokeAsync(() => { MessageBox.Show($"打印{testName}报告", "打印", MessageBoxButton.OK, MessageBoxImage.Information); }); } public async Task UpdateRealTimeData() { if (Phase != TestPhase.Running) return; try { switch (CurrentTest) { case TestType.Disintegration: DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp); IsBasketMovingUp = await _plc.ReadCoilAsync(_plcConfig.DisintegrationMovingUpCoil); for (int i = 0; i < _plcConfig.DisintegrationCompleteCoils.Length; i++) { bool completed = await _plc.ReadCoilAsync(_plcConfig.DisintegrationCompleteCoils[i]); if (completed && !TubesCompleted[i]) { TubesCompleted[i] = true; RemainingTubes = 6 - TubesCompleted.Count(c => c); } } break; case TestType.Dissolution: await UpdateDissolutionDataAsync(); break; } } catch { } } private async Task UpdateDissolutionDataAsync() { DissolutionRpm = await _plc.ReadFloatAsync(_plcConfig.DissolutionRpm); if (_plcConfig.DisintegrationTemp != 0) DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp); if (_isDissolution1Running) await CheckDissolutionSampleAsync(1); if (_isDissolution2Running) await CheckDissolutionSampleAsync(2); UpdateDissolutionClock(); } private async Task CheckDissolutionSampleAsync(int channel) { ushort coilAddress = channel == 1 ? _plcConfig.Dissolution1SampleAckCoil : _plcConfig.Dissolution2SampleAckCoil; if (coilAddress == 0) { DissolutionCurveStatus = $"溶出{channel}取样确认线圈未配置"; return; } bool sampleConfirmed = await _plc.ReadCoilAsync(coilAddress); if (sampleConfirmed) { SetDissolutionSampleRequestActive(channel, false); return; } if (IsDissolutionSampleRequestActive(channel) || IsDissolutionSamplePromptOpen(channel)) return; if (GetNextPendingDissolutionSample(channel) == null) { await _plc.WriteCoilAsync(coilAddress, true); DissolutionCurveStatus = $"溶出{channel}已完成全部取样点"; return; } SetDissolutionSampleRequestActive(channel, true); SetDissolutionSamplePromptOpen(channel, true); try { double percent = await ShowDissolutionSampleDialogAsync(channel); RecordDissolutionSample(channel, percent); await _plc.WriteCoilAsync(coilAddress, true); LocalAlarm = $"溶出{channel}已记录取样结果"; DissolutionCurveStatus = ""; } catch (Exception ex) { SetDissolutionSampleRequestActive(channel, false); await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"溶出{channel}取样确认失败:{ex.Message}", "取样确认失败", MessageBoxButton.OK, MessageBoxImage.Error)); } finally { SetDissolutionSamplePromptOpen(channel, false); } } private bool IsDissolutionSampleRequestActive(int channel) { return channel == 1 ? _dissolution1SampleRequestActive : _dissolution2SampleRequestActive; } private void SetDissolutionSampleRequestActive(int channel, bool value) { if (channel == 1) _dissolution1SampleRequestActive = value; else _dissolution2SampleRequestActive = value; } private bool IsDissolutionSamplePromptOpen(int channel) { return channel == 1 ? _isDissolution1SamplePromptOpen : _isDissolution2SamplePromptOpen; } private void SetDissolutionSamplePromptOpen(int channel, bool value) { if (channel == 1) _isDissolution1SamplePromptOpen = value; else _isDissolution2SamplePromptOpen = value; } private async Task ShowDissolutionSampleDialogAsync(int channel) { double? result = await App.Current.Dispatcher.InvokeAsync(() => { var nextSample = GetNextPendingDissolutionSample(channel); double elapsed = GetDissolutionElapsedMinutes(channel); string plannedText = nextSample == null ? "未找到待记录取样点" : $"计划取样时间:{nextSample.ScheduledTimeMin:0.##} min"; var dialog = new Window { Title = $"溶出{channel}取样", Width = 460, SizeToContent = SizeToContent.Height, WindowStartupLocation = WindowStartupLocation.CenterOwner, ResizeMode = ResizeMode.NoResize, Background = Brushes.White, Owner = Application.Current.MainWindow }; var panel = new StackPanel { Margin = new Thickness(24) }; panel.Children.Add(new TextBlock { Text = $"请人工采集样品并完成分析,录入该时间点的溶出度(%)。\n{plannedText}\n当前运行时间:{elapsed:0.##} min", FontSize = 16, Foreground = new SolidColorBrush(Color.FromRgb(16, 42, 67)), TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 14) }); panel.Children.Add(new TextBlock { Text = "溶出度(%)", FontSize = 14, FontWeight = System.Windows.FontWeights.SemiBold, Foreground = new SolidColorBrush(Color.FromRgb(51, 65, 85)), Margin = new Thickness(0, 0, 0, 6) }); var input = new TextBox { Height = 42, FontSize = 18, Padding = new Thickness(10, 5, 10, 5), Text = nextSample?.Percent?.ToString("0.###") ?? "", Margin = new Thickness(0, 0, 0, 18) }; NumericInput.SetIsEnabled(input, true); NumericInput.SetAllowDecimal(input, true); NumericInput.SetAllowNegative(input, false); panel.Children.Add(input); var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = System.Windows.HorizontalAlignment.Right }; var cancelButton = new Button { Content = "取消", Width = 96, Height = 38, Margin = new Thickness(0, 0, 10, 0), Background = Brushes.White, Foreground = new SolidColorBrush(Color.FromRgb(51, 65, 85)), BorderBrush = new SolidColorBrush(Color.FromRgb(203, 213, 225)), BorderThickness = new Thickness(1), FontSize = 15, FontWeight = System.Windows.FontWeights.SemiBold, Cursor = System.Windows.Input.Cursors.Hand }; cancelButton.Click += (_, _) => { dialog.DialogResult = false; dialog.Close(); }; var confirmButton = new Button { Content = "记录取样", Width = 112, Height = 38, Background = new SolidColorBrush(Color.FromRgb(21, 101, 169)), Foreground = Brushes.White, BorderThickness = new Thickness(0), FontSize = 15, FontWeight = System.Windows.FontWeights.SemiBold, Cursor = System.Windows.Input.Cursors.Hand }; confirmButton.Click += (_, _) => { if (!double.TryParse(input.Text, out double value) || !IsValidDissolutionPercent(value)) { MessageBox.Show("请输入0-150之间的溶出度百分比。", "取样结果无效", MessageBoxButton.OK, MessageBoxImage.Warning); return; } input.Tag = value; dialog.DialogResult = true; dialog.Close(); }; buttons.Children.Add(cancelButton); buttons.Children.Add(confirmButton); panel.Children.Add(buttons); dialog.Content = panel; return dialog.ShowDialog() == true && input.Tag is double value ? value : null; }); if (!result.HasValue) throw new InvalidOperationException("取样结果未录入,未写入确认线圈"); return result.Value; } private async Task ReadDissolutionChannelAsync(int channel) { ushort registerAddress = channel == 1 ? ResolveDissolution1PercentAddress() : _plcConfig.Dissolution2Percent; if (registerAddress == 0) { DissolutionCurveStatus = $"溶出{channel}溶出度寄存器未配置"; return false; } double value = await _plc.ReadFloatAsync(registerAddress); if (!IsValidDissolutionPercent(value)) { DissolutionCurveStatus = $"溶出{channel}溶出度数据异常"; return false; } DateTime startTime = channel == 1 ? _dissolution1StartTime : _dissolution2StartTime; if (startTime == DateTime.MinValue) return false; double minutes = (DateTime.Now - startTime).TotalMinutes; if (channel == 1) { Dissolution1Percent = value; DissolutionPercent = value; AddDissolutionPoint(_dissolution1Times, _dissolution1Values, _dissolution1Series, minutes, value); Dissolution1RSquared = CalculateRSquared(_dissolution1Times, _dissolution1Values); DissolutionRSquared = Dissolution1RSquared; } else { Dissolution2Percent = value; DissolutionPercent = value; AddDissolutionPoint(_dissolution2Times, _dissolution2Values, _dissolution2Series, minutes, value); Dissolution2RSquared = CalculateRSquared(_dissolution2Times, _dissolution2Values); DissolutionRSquared = Dissolution2RSquared; } DissolutionCurveStatus = ""; return true; } private ushort ResolveDissolution1PercentAddress() { return _plcConfig.Dissolution1Percent != 0 ? _plcConfig.Dissolution1Percent : _plcConfig.DissolutionPercent; } private static bool IsValidDissolutionPercent(double value) { return double.IsFinite(value) && value >= 0 && value <= 150; } private static void AddDissolutionPoint(List times, List values, LineSeries series, double minutes, double value) { if (times.Count > 0 && minutes <= times[^1]) return; times.Add(minutes); values.Add(value); series.Points.Add(new DataPoint(minutes, value)); } private void CreateDissolutionSampleSchedule(int channel) { RemoveDissolutionSamples(channel); var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes? .Where(t => t > 0) .Distinct() .OrderBy(t => t) .ToArray(); if (sampleTimes == null || sampleTimes.Length == 0) sampleTimes = new[] { 5, 10, 15, 30, 45, 60 }; foreach (int minute in sampleTimes) { DissolutionSamplePoints.Add(new DissolutionSamplePoint { Channel = channel, ScheduledTimeMin = minute }); } } private void RemoveDissolutionSamples(int channel) { for (int i = DissolutionSamplePoints.Count - 1; i >= 0; i--) { if (DissolutionSamplePoints[i].Channel == channel) DissolutionSamplePoints.RemoveAt(i); } } private DissolutionSamplePoint? GetNextPendingDissolutionSample(int channel) { return DissolutionSamplePoints .Where(s => s.Channel == channel && !s.Percent.HasValue) .OrderBy(s => s.ScheduledTimeMin) .FirstOrDefault(); } private double GetDissolutionElapsedMinutes(int channel) { DateTime startTime = channel == 1 ? _dissolution1StartTime : _dissolution2StartTime; return startTime == DateTime.MinValue ? 0 : Math.Max(0, (DateTime.Now - startTime).TotalMinutes); } private void RecordDissolutionSample(int channel, double percent) { var sample = GetNextPendingDissolutionSample(channel) ?? new DissolutionSamplePoint { Channel = channel, ScheduledTimeMin = GetDissolutionElapsedMinutes(channel) }; if (!DissolutionSamplePoints.Contains(sample)) DissolutionSamplePoints.Add(sample); sample.ActualTimeMin = GetDissolutionElapsedMinutes(channel); sample.Percent = percent; sample.RecordedAt = DateTime.Now; RefreshDissolutionSeries(channel); } private void RefreshDissolutionSeries(int channel) { var times = channel == 1 ? _dissolution1Times : _dissolution2Times; var values = channel == 1 ? _dissolution1Values : _dissolution2Values; var series = channel == 1 ? _dissolution1Series : _dissolution2Series; var recordedSamples = DissolutionSamplePoints .Where(s => s.Channel == channel && s.Percent.HasValue) .OrderBy(s => s.ScheduledTimeMin) .ToList(); times.Clear(); values.Clear(); series.Points.Clear(); foreach (var sample in recordedSamples) { double time = sample.ScheduledTimeMin; double value = sample.Percent!.Value; times.Add(time); values.Add(value); series.Points.Add(new DataPoint(time, value)); } double rsquared = CalculateRSquared(times, values); double latestValue = recordedSamples.LastOrDefault()?.Percent ?? 0; if (channel == 1) { Dissolution1Percent = latestValue; Dissolution1RSquared = rsquared; } else { Dissolution2Percent = latestValue; Dissolution2RSquared = rsquared; } DissolutionPercent = latestValue; DissolutionRSquared = rsquared; DissolutionPlotModel.InvalidatePlot(true); } private void UpdateDissolutionClock() { var now = DateTime.Now; double elapsed1 = _isDissolution1Running && _dissolution1StartTime != DateTime.MinValue ? (now - _dissolution1StartTime).TotalMinutes : 0; double elapsed2 = _isDissolution2Running && _dissolution2StartTime != DateTime.MinValue ? (now - _dissolution2StartTime).TotalMinutes : 0; DissolutionElapsedTime = Math.Max(elapsed1, elapsed2); var remaining = new List(); if (_isDissolution1Running) remaining.Add(Math.Max(0, Dissolution1TimeMin - elapsed1)); if (_isDissolution2Running) remaining.Add(Math.Max(0, Dissolution2TimeMin - elapsed2)); DissolutionCountdown = remaining.Count == 0 ? 0 : remaining.Min(); } partial void OnDissolution1TimeMinChanged(int value) { if (_isLoadingDissolution1Time || _plcConfig.Dissolution1Time == 0 || value <= 0) return; _ = WriteDissolutionTimeAsync(_plcConfig.Dissolution1Time, value); } partial void OnDissolution2TimeMinChanged(int value) { if (_isLoadingDissolution2Time || _plcConfig.Dissolution2Time == 0 || value <= 0) return; _ = WriteDissolutionTimeAsync(_plcConfig.Dissolution2Time, value); } partial void OnDissolution1SampleIntervalMinChanged(double value) { if (_isLoadingDissolution1SampleInterval || _plcConfig.Dissolution1SampleInterval == 0 || value <= 0) return; _ = WriteDissolutionFloatAsync(_plcConfig.Dissolution1SampleInterval, value); DissolutionSampleInterval = ToCompatibleSampleInterval(value); } partial void OnDissolution2SampleIntervalMinChanged(double value) { if (_isLoadingDissolution2SampleInterval || _plcConfig.Dissolution2SampleInterval == 0 || value <= 0) return; _ = WriteDissolutionFloatAsync(_plcConfig.Dissolution2SampleInterval, value); } private async Task LoadDissolutionTimesAsync() { if (_plcConfig.Dissolution1Time != 0) { try { _isLoadingDissolution1Time = true; int value = await _plc.ReadIntAsync(_plcConfig.Dissolution1Time); if (value > 0) Dissolution1TimeMin = value; } catch { } finally { _isLoadingDissolution1Time = false; } } if (_plcConfig.Dissolution2Time != 0) { try { _isLoadingDissolution2Time = true; int value = await _plc.ReadIntAsync(_plcConfig.Dissolution2Time); if (value > 0) Dissolution2TimeMin = value; } catch { } finally { _isLoadingDissolution2Time = false; } } await LoadDissolutionSampleIntervalsAsync(); } private async Task WriteDissolutionTimeAsync(ushort registerAddress, int value) { if (registerAddress == 0 || value <= 0) return; try { await _plc.WriteRegisterAsync(registerAddress, (ushort)Math.Min(value, ushort.MaxValue)); } catch { } } private async Task LoadDissolutionSampleIntervalsAsync() { if (_plcConfig.Dissolution1SampleInterval != 0) { try { _isLoadingDissolution1SampleInterval = true; float value = await _plc.ReadFloatAsync(_plcConfig.Dissolution1SampleInterval); if (float.IsFinite(value) && value > 0) { Dissolution1SampleIntervalMin = value; DissolutionSampleInterval = ToCompatibleSampleInterval(value); } } catch { } finally { _isLoadingDissolution1SampleInterval = false; } } if (_plcConfig.Dissolution2SampleInterval != 0) { try { _isLoadingDissolution2SampleInterval = true; float value = await _plc.ReadFloatAsync(_plcConfig.Dissolution2SampleInterval); if (float.IsFinite(value) && value > 0) Dissolution2SampleIntervalMin = value; } catch { } finally { _isLoadingDissolution2SampleInterval = false; } } } private async Task WriteDissolutionFloatAsync(ushort registerAddress, double value) { if (registerAddress == 0 || value <= 0 || !double.IsFinite(value)) return; try { await _plc.WriteFloatAsync(registerAddress, (float)value); } catch { } } private static int ToCompatibleSampleInterval(double value) { if (!double.IsFinite(value) || value <= 0) return 0; return (int)Math.Min(int.MaxValue, Math.Round(value, MidpointRounding.AwayFromZero)); } partial void OnWeightBeforeChanged(double value) { if (_isUpdatingFriabilityWeightFromPlc) return; _ = WriteFriabilityWeightAsync(_plcConfig.WeightBefore, value); } partial void OnWeightAfterChanged(double value) { if (_isUpdatingFriabilityWeightFromPlc) return; _ = WriteFriabilityWeightAsync(_plcConfig.WeightAfter, value); } private async Task LoadFriabilityWeightsAsync() { try { _isUpdatingFriabilityWeightFromPlc = true; if (_plcConfig.WeightBefore != 0) { double before = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量"); WeightBefore = before; } if (_plcConfig.WeightAfter != 0) { double after = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量"); WeightAfter = after; } } catch { } finally { _isUpdatingFriabilityWeightFromPlc = false; } } private async Task WriteFriabilityWeightAsync(ushort registerAddress, double value) { if (registerAddress == 0 || !double.IsFinite(value) || value < 0) return; try { await _plc.WriteFloatAsync(registerAddress, (float)value); } catch { } } private void SetFriabilityWeightFromPlc(double? weightBefore = null, double? weightAfter = null) { _isUpdatingFriabilityWeightFromPlc = true; try { if (weightBefore.HasValue) WeightBefore = weightBefore.Value; if (weightAfter.HasValue) WeightAfter = weightAfter.Value; } finally { _isUpdatingFriabilityWeightFromPlc = false; } } partial void OnDisintegrationTimeMinChanged(double value) { if (_isLoadingDisintegrationTime || _plcConfig.DisintegrationTime == 0 || value <= 0) return; _ = WriteDisintegrationTimeAsync(value); } partial void OnDisintegrationSpeedRpmChanged(double value) { if (_isLoadingDisintegrationSpeed || _plcConfig.DisintegrationSpeed == 0 || value <= 0) return; _ = WriteDisintegrationSpeedAsync(value); } partial void OnDisintegrationDosageFormChanged(string value) { int seconds = ResolveDisintegrationLimitSeconds(value); if (seconds > 0) DisintegrationTimeMin = seconds / 60.0; } private int ResolveDisintegrationLimitSeconds(string? dosageForm = null) { string form = string.IsNullOrWhiteSpace(dosageForm) ? DisintegrationDosageForm : dosageForm; return form switch { "薄膜衣片" => 30 * 60, "糖衣片" => 60 * 60, "胶囊" => 30 * 60, "普通片" => 15 * 60, _ => App.CurrentPharmaParams.DisintegrationMaxSeconds > 0 ? App.CurrentPharmaParams.DisintegrationMaxSeconds : 15 * 60 }; } private async Task LoadDisintegrationSpeedAsync() { if (_plcConfig.DisintegrationSpeed == 0) return; try { _isLoadingDisintegrationSpeed = true; float value = await _plc.ReadFloatAsync(_plcConfig.DisintegrationSpeed); if (value > 0) DisintegrationSpeedRpm = value; } catch { } finally { _isLoadingDisintegrationSpeed = false; } } private async Task LoadDisintegrationTimeAsync() { if (_plcConfig.DisintegrationTime == 0) return; try { _isLoadingDisintegrationTime = true; int seconds = ResolveDisintegrationLimitSeconds(); if (seconds > 0) DisintegrationTimeMin = seconds / 60.0; await WriteDisintegrationTimeAsync(DisintegrationTimeMin); } catch { } finally { _isLoadingDisintegrationTime = false; } } private async Task WriteDisintegrationTimeAsync(double value) { if (_plcConfig.DisintegrationTime == 0 || value <= 0) return; try { await _plc.WriteFloatAsync(_plcConfig.DisintegrationTime, (float)value); } catch { } } private async Task WriteDisintegrationSpeedAsync(double value) { if (_plcConfig.DisintegrationSpeed == 0 || value <= 0) return; try { await _plc.WriteFloatAsync(_plcConfig.DisintegrationSpeed, (float)value); } catch { } } private async Task PulseCoilAsync(ushort coilAddress) { if (coilAddress == 0) throw new InvalidOperationException("PLC线圈地址未配置"); await _plc.WriteCoilAsync(coilAddress, true); await Task.Delay(100); await _plc.WriteCoilAsync(coilAddress, false); } private async Task RunHardnessAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Hardness; Phase = TestPhase.Running; HardnessPass = false; _hardnessResults.Clear(); try { int count = Math.Max(1, HardnessTestCount); double min = HardnessInternalMin; double max = HardnessInternalMax; double currentSpeed = HardnessSudu; double currentWeiyi = HardnessWeiyi; // 如果你需要把这3个值发给PLC,就在这里发一次(测试开始用当前参数) await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)currentSpeed); await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)currentWeiyi); // 开始循环测试 for (int i = 0; i < count; i++) { await _plc.WriteCoilAsync(_plcConfig.HardnessStartCoil, true); bool completed = false; while (!completed && Phase == TestPhase.Running) { await Task.Delay(200); completed = await _plc.ReadCoilAsync(_plcConfig.HardnessCompleteCoil); } double val = await _plc.ReadFloatAsync(_plcConfig.HardnessPoSun); _hardnessResults.Add(val); //HardnessPoSun = val; await Task.Delay(1000); } // 计算结果 HardnessAvg = _hardnessResults.Average(); HardnessMax = _hardnessResults.Max(); HardnessMin = _hardnessResults.Min(); HardnessCurrentCount = count; HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100; HardnessPass = HardnessAvg >= min && HardnessAvg <= max; Phase = TestPhase.Completed; } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"硬度测试出错:{ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; await SaveBatchResult(); } } /// 脆碎度测试主逻辑(实时状态显示) private async Task RunFriabilityAsync() { // 1. 防并发:如果设备不是空闲状态,直接退出 if (Phase != TestPhase.Idle) return; // 2. 标记当前正在运行的是脆碎度测试 CurrentTest = TestType.Friability; Phase = TestPhase.Running; FriabilityPass = false; bool resultReady = false; try { ushort startCoil = _plcConfig.FriabilityStartCoil; if (startCoil == 0) { throw new InvalidOperationException("未配置脆碎度启动线圈地址"); } double weightBefore = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量"); SetFriabilityWeightFromPlc(weightBefore: weightBefore); if (WeightBefore <= 0) throw new InvalidOperationException("脆碎前重量必须大于0"); int totalRounds = Math.Max(1, FriabilityTargetRounds); double rpm = FriabilityTargetRpm > 0 ? FriabilityTargetRpm : 25; FriabilityTargetTimeSec = (int)Math.Ceiling(totalRounds / rpm * 60); FriabilityRemainingRounds = totalRounds; FriabilityCurrentRpm = rpm; await _plc.WriteCoilAsync(startCoil, true); int durationMs = (int)((totalRounds / rpm) * 60 * 1000); // 总运行时间(毫秒) for (int i = 0; i < durationMs; i += 100) { // 如果用户点了停止,状态会被设为Idle,直接跳出循环 if (Phase != TestPhase.Running) break; // 计算当前剩余圈数 double elapsedMs = i; double elapsedRounds = rpm / 60 / 1000 * elapsedMs; int remainingRounds = (int)Math.Ceiling(totalRounds - elapsedRounds); // 更新界面绑定的剩余圈数 FriabilityRemainingRounds = remainingRounds; // 等待100ms,再更新下一次 await Task.Delay(100); } if (Phase != TestPhase.Running) throw new InvalidOperationException("脆碎度测试已停止,未保存结果"); double weightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量"); SetFriabilityWeightFromPlc(weightAfter: weightAfter); if (WeightAfter > WeightBefore) throw new InvalidOperationException("脆碎后重量不能大于初始重量"); FriabilityCurrentRpm = rpm; LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率 FriabilityPass = LossPercent <= FriabilityMaxLossPercent; //标准值 resultReady = true; // 标记测试为已完成 Phase = TestPhase.Completed; } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"脆碎度测试出错: {ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; FriabilityRemainingRounds = FriabilityTargetRounds; if (resultReady) await SaveBatchResult(); } } private async Task ReadFriabilityWeightAsync(ushort registerAddress, string label) { if (registerAddress == 0) throw new InvalidOperationException($"{label}寄存器未配置"); double value = await _plc.ReadFloatAsync(registerAddress); if (!double.IsFinite(value) || value < 0) throw new InvalidOperationException($"{label}数据异常"); return value; } private async Task RunDisintegrationAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Disintegration; Phase = TestPhase.Running; DisintegrationPass = false; // 添加这一行 TubesCompleted = new bool[6]; RemainingTubes = 6; DisintegrationSeconds = 0; _disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _disintegrationTimer.Tick += (s, e) => { if (Phase == TestPhase.Running) DisintegrationSeconds++; }; _disintegrationTimer.Start(); try { await WriteDisintegrationSpeedAsync(DisintegrationSpeedRpm); await WriteDisintegrationTimeAsync(DisintegrationTimeMin); await PulseCoilAsync(_plcConfig.DisintegrationStartCoil); int maxSec = ResolveDisintegrationLimitSeconds(); DisintegrationTimeMin = maxSec / 60.0; while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running) { await Task.Delay(500); } _disintegrationTimer.Stop(); Phase = TestPhase.Completed; } catch (Exception ex) { _disintegrationTimer.Stop(); await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; int maxSec = ResolveDisintegrationLimitSeconds(); DisintegrationPass = !_discardDisintegrationResult && RemainingTubes == 0 && DisintegrationSeconds <= maxSec; if (!_discardDisintegrationResult) await SaveBatchResult(); _discardDisintegrationResult = false; } } private async Task StopDisintegrationAsync() { try { await PulseCoilAsync(_plcConfig.DisintegrationStopCoil); } finally { Phase = TestPhase.Idle; _disintegrationTimer?.Stop(); } } private async Task ResetDisintegrationAsync() { bool wasRunning = CurrentTest == TestType.Disintegration && Phase == TestPhase.Running; _discardDisintegrationResult = wasRunning; try { await PulseCoilAsync(_plcConfig.DisintegrationResetCoil); } finally { _disintegrationTimer?.Stop(); TubesCompleted = new bool[6]; RemainingTubes = 6; DisintegrationSeconds = 0; DisintegrationPass = false; Phase = TestPhase.Idle; if (!wasRunning) _discardDisintegrationResult = false; } } private async Task StartDissolution1Async() { CurrentTest = TestType.Dissolution; Phase = TestPhase.Running; DissolutionPass = false; ResetDissolutionChannel(1); ResetDissolutionSampleState(1); CreateDissolutionSampleSchedule(1); _dissolution1StartTime = DateTime.Now; _isDissolution1Running = true; DissolutionPlotModel.Title = "溶出曲线"; await WriteDissolutionTimeAsync(_plcConfig.Dissolution1Time, Dissolution1TimeMin); await WriteDissolutionFloatAsync(_plcConfig.Dissolution1SampleInterval, Dissolution1SampleIntervalMin); await PulseCoilAsync(_plcConfig.Dissolution1StartCoil); } private async Task StopDissolution1Async() { try { await PulseCoilAsync(_plcConfig.Dissolution1StopCoil); await FinalizeDissolutionChannelAsync(1); } finally { _isDissolution1Running = false; ResetDissolutionSampleState(1); Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle; UpdateDissolutionClock(); } } private async Task ResetDissolution1Async() { try { await PulseCoilAsync(_plcConfig.Dissolution1ResetCoil); } finally { _isDissolution1Running = false; ResetDissolutionChannel(1); ResetDissolutionSampleState(1); Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle; UpdateDissolutionClock(); } } private async Task StartDissolution2Async() { CurrentTest = TestType.Dissolution; Phase = TestPhase.Running; DissolutionPass = false; ResetDissolutionChannel(2); ResetDissolutionSampleState(2); CreateDissolutionSampleSchedule(2); _dissolution2StartTime = DateTime.Now; _isDissolution2Running = true; DissolutionPlotModel.Title = "溶出曲线"; await WriteDissolutionTimeAsync(_plcConfig.Dissolution2Time, Dissolution2TimeMin); await WriteDissolutionFloatAsync(_plcConfig.Dissolution2SampleInterval, Dissolution2SampleIntervalMin); await PulseCoilAsync(_plcConfig.Dissolution2StartCoil); } private async Task StopDissolution2Async() { try { await PulseCoilAsync(_plcConfig.Dissolution2StopCoil); await FinalizeDissolutionChannelAsync(2); } finally { _isDissolution2Running = false; ResetDissolutionSampleState(2); Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle; UpdateDissolutionClock(); } } private async Task ResetDissolution2Async() { try { await PulseCoilAsync(_plcConfig.Dissolution2ResetCoil); } finally { _isDissolution2Running = false; ResetDissolutionChannel(2); ResetDissolutionSampleState(2); Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle; UpdateDissolutionClock(); } } private void ResetDissolutionChannel(int channel) { RemoveDissolutionSamples(channel); if (channel == 1) { _dissolution1Times.Clear(); _dissolution1Values.Clear(); _dissolution1Series.Points.Clear(); _dissolution1StartTime = DateTime.MinValue; Dissolution1Percent = 0; Dissolution1RSquared = 0; } else { _dissolution2Times.Clear(); _dissolution2Values.Clear(); _dissolution2Series.Points.Clear(); _dissolution2StartTime = DateTime.MinValue; Dissolution2Percent = 0; Dissolution2RSquared = 0; } DissolutionCurveStatus = ""; DissolutionPlotModel.InvalidatePlot(true); } private void ResetDissolutionSampleState(int channel) { SetDissolutionSampleRequestActive(channel, false); SetDissolutionSamplePromptOpen(channel, false); } private async Task FinalizeDissolutionChannelAsync(int channel) { var times = channel == 1 ? _dissolution1Times : _dissolution2Times; var values = channel == 1 ? _dissolution1Values : _dissolution2Values; if (values.Count == 0) { DissolutionCurveStatus = $"溶出{channel}无有效曲线数据,未保存结果"; LocalAlarm = DissolutionCurveStatus; return; } _dissolutionResultChannel = $"溶出{channel}"; _dissolutionResultRate30Min = GetDissolutionRateAt30Min(times, values); _dissolutionResultRSquared = CalculateRSquared(times, values); DissolutionPercent = _dissolutionResultRate30Min; DissolutionRSquared = _dissolutionResultRSquared; if (channel == 1) Dissolution1RSquared = _dissolutionResultRSquared; else Dissolution2RSquared = _dissolutionResultRSquared; DissolutionPass = _dissolutionResultRate30Min >= App.CurrentPharmaParams.DissolutionMinPercentAt30min; await SaveBatchResult(); } private static double GetDissolutionRateAt30Min(List times, List values) { if (values.Count == 0) return 0; int index = 0; double nearestDistance = double.MaxValue; for (int i = 0; i < times.Count; i++) { double distance = Math.Abs(times[i] - 30); if (distance < nearestDistance) { nearestDistance = distance; index = i; } } return values[index]; } private async Task RunDissolutionAsync() { await StartDissolution1Async(); } private async Task SaveBatchResult(bool? forcedQualified = null) { try { double dissolutionRate30Min = CurrentTest == TestType.Dissolution ? _dissolutionResultRate30Min : DissolutionPercent; double rsquared = CurrentTest == TestType.Dissolution ? _dissolutionResultRSquared : DissolutionRSquared; var batch = new TestBatch { TestTime = DateTime.Now, StationId = StationId, SampleName = "样品", // 硬度 HardnessAvg = HardnessAvg, HardnessRSD = HardnessRSD, HardnessMax = HardnessMax, HardnessMin = HardnessMin, HardnessTestCount = HardnessTestCount, HardnessInternalMin = HardnessInternalMin, HardnessInternalMax = HardnessInternalMax, // 脆碎度 FriabilityLoss = LossPercent, FriabilityTargetRpm = FriabilityTargetRpm, FriabilityTargetTimeSec = FriabilityTargetTimeSec, FriabilityTargetRounds = FriabilityTargetRounds, FriabilityClockwise = FriabilityClockwise, FriabilityRemainingRounds = FriabilityRemainingRounds, WeightBefore = WeightBefore, WeightAfter = WeightAfter, // 崩解 DisintegrationTimeSec = DisintegrationSeconds, RemainingTubesAtEnd = RemainingTubes, DisintegrationTargetFreq = 0, DisintegrationTemp = DisintegrationTemp, DisintegrationDosageForm = DisintegrationDosageForm, DisintegrationLimitSeconds = ResolveDisintegrationLimitSeconds(), // 溶出 DissolutionChannel = CurrentTest == TestType.Dissolution ? _dissolutionResultChannel : "", DissolutionRate30Min = dissolutionRate30Min, DissolutionTargetRpm = DissolutionTargetRpm, DissolutionRSquared = rsquared, DissolutionSampleInterval = CurrentTest == TestType.Dissolution && _dissolutionResultChannel == "溶出2" ? ToCompatibleSampleInterval(Dissolution2SampleIntervalMin) : ToCompatibleSampleInterval(Dissolution1SampleIntervalMin), Dissolution1SampleInterval = Dissolution1SampleIntervalMin, Dissolution2SampleInterval = Dissolution2SampleIntervalMin, DissolutionUpDownFreq = DissolutionUpDownFreq, HardnessPass = HardnessPass, FriabilityPass = FriabilityPass, DisintegrationPass = DisintegrationPass, DissolutionPass = DissolutionPass, TestType = CurrentTest switch { TestType.Hardness => "硬度", TestType.Friability => "脆碎度", TestType.Disintegration => "崩解", TestType.Dissolution => "溶出", _ => "" }, IsQualified = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass }; var dissolutionSamples = CurrentTest == TestType.Dissolution ? DissolutionSamplePoints .Where(s => s.ChannelName == _dissolutionResultChannel && s.Percent.HasValue) .ToList() : new List(); await Task.Run(() => _db.InsertBatch(batch, dissolutionSamples)); await Application.Current.Dispatcher.InvokeAsync(() => { string projectName = CurrentTest switch { TestType.Hardness => "硬度", TestType.Friability => "脆碎度", TestType.Disintegration => "崩解", TestType.Dissolution => string.IsNullOrWhiteSpace(_dissolutionResultChannel) ? "溶出" : _dissolutionResultChannel, _ => "" }; LocalAlarm = $"{projectName}测试完成"; }); } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}")); } } private double CalculateRSquared(List timeMinutes, List concentration) { if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count) return 0; int n = timeMinutes.Count; double sumX = timeMinutes.Sum(); double sumY = concentration.Sum(); double sumXY = timeMinutes.Zip(concentration, (x, y) => x * y).Sum(); double sumX2 = timeMinutes.Select(x => x * x).Sum(); double sumY2 = concentration.Select(y => y * y).Sum(); double numerator = (n * sumXY - sumX * sumY); double denominator = Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); if (denominator <= 0 || double.IsNaN(denominator)) return 0; double r = numerator / denominator; double result = r * r; return double.IsFinite(result) ? result : 0; } private async Task ExportHistoryAsync() { var batches = await Task.Run(() => _db.GetBatches(null, 100)); string path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"检测记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); _excel.ExportToExcel(batches, path); await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"导出成功: {path}")); } private double StandardDeviation(List values) { if (values.Count == 0) return 0; double avg = values.Average(); double sum = values.Sum(v => Math.Pow(v - avg, 2)); return Math.Sqrt(sum / values.Count); } } }