using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Threading; using OxyPlot; using OxyPlot.Series; 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; 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; 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; public PlotModel DissolutionPlotModel { get; } private LineSeries _dissolutionSeries; private DateTime _dissolutionStartTime; // 命令 public IAsyncRelayCommand StartHardnessCommand { get; } public IAsyncRelayCommand StartFriabilityCommand { get; } public IAsyncRelayCommand StartDisintegrationCommand { get; } public IAsyncRelayCommand StartDissolutionCommand { get; } public IAsyncRelayCommand ExportHistoryCommand { get; } 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; StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync); StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync); StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync); StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync); ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync); // 溶出曲线:时间 vs 溶出度 DissolutionPlotModel = new PlotModel { Title = $"工位{StationId} 溶出曲线" }; _dissolutionSeries = new LineSeries { Title = "溶出度 (%)", Color = OxyColors.Green }; DissolutionPlotModel.Series.Add(_dissolutionSeries); } public async Task UpdateRealTimeData() { if (Phase != TestPhase.Running) return; try { switch (CurrentTest) { case TestType.Hardness: HardnessValue = await _plc.ReadFloatAsync(_plcConfig.HardnessValue); break; 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: DissolutionRpm = await _plc.ReadFloatAsync(_plcConfig.DissolutionRpm); DissolutionPercent = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent); if (_dissolutionStartTime != DateTime.MinValue) { double minutes = (DateTime.Now - _dissolutionStartTime).TotalMinutes; _dissolutionSeries.Points.Add(new DataPoint(minutes, DissolutionPercent)); if (_dissolutionSeries.Points.Count > 200) _dissolutionSeries.Points.RemoveAt(0); DissolutionPlotModel.InvalidatePlot(true); } break; } } catch { } } private async Task RunHardnessAsync() { try { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Hardness; Phase = TestPhase.Running; _hardnessResults.Clear(); int count = App.CurrentPharmaParams.HardnessTestCount; double min = App.CurrentPharmaParams.HardnessMin_N; double max = App.CurrentPharmaParams.HardnessMax_N; 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.HardnessValue); _hardnessResults.Add(val); HardnessValue = val; await Task.Delay(1000); } HardnessAvg = _hardnessResults.Average(); HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100; HardnessPass = HardnessAvg >= min && HardnessAvg <= max; Phase = TestPhase.Completed; await SaveBatchResult(); } catch (Exception ex) { Phase = TestPhase.Error; System.Windows.MessageBox.Show($"硬度测试出错:{ex.Message}\n{ex.StackTrace}", "错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); } } private async Task RunFriabilityAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Friability; Phase = TestPhase.Running; // 模拟称重(实际可通过串口天平) WeightBefore = 6.5; await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true); await Task.Delay(TimeSpan.FromMinutes(4)); WeightAfter = WeightBefore * (1 - 0.008); LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100; FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; Phase = TestPhase.Completed; await SaveBatchResult(); } private async Task RunDisintegrationAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Disintegration; Phase = TestPhase.Running; TubesCompleted = new bool[6]; RemainingTubes = 6; _disintegrationSeconds = 0; _disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _disintegrationTimer.Tick += (s, e) => { if (Phase == TestPhase.Running) DisintegrationSeconds = _disintegrationSeconds + 1; }; _disintegrationTimer.Start(); await _plc.WriteCoilAsync(_plcConfig.DisintegrationStartCoil, true); int maxSec = App.CurrentPharmaParams.DisintegrationMaxSeconds; while (RemainingTubes > 0 && _disintegrationSeconds < maxSec && Phase == TestPhase.Running) await Task.Delay(500); _disintegrationTimer.Stop(); Phase = TestPhase.Completed; await SaveBatchResult(); } private async Task RunDissolutionAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Dissolution; Phase = TestPhase.Running; _dissolutionStartTime = DateTime.Now; _dissolutionSeries.Points.Clear(); await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true); var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes; double prevMin = 0; foreach (var t in sampleTimes) { int delayMs = (int)((t - prevMin) * 60 * 1000); if (delayMs > 0) await Task.Delay(delayMs); double value = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent); DissolutionPercent = value; prevMin = t; // 弹出取样提示(可改为非阻塞提示) App.Current.Dispatcher.Invoke(() => { System.Windows.MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%"); }); } bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min; Phase = TestPhase.Completed; await SaveBatchResult(pass); } private async Task SaveBatchResult(bool? forcedQualified = null) { try { var batch = new TestBatch { TestTime = DateTime.Now, StationId = StationId, SampleName = $"样品-{StationId}", HardnessAvg = HardnessAvg, HardnessRSD = HardnessRSD, FriabilityLoss = LossPercent, DisintegrationTimeSec = DisintegrationSeconds, RemainingTubesAtEnd = RemainingTubes, DissolutionRate30Min = DissolutionPercent, IsQualified = forcedQualified ?? (HardnessPass && FriabilityPass && RemainingTubes == 0 && DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min) }; await Task.Run(() => _db.InsertBatch(batch)); if (!batch.IsQualified) _alarm.RaiseAlarm($"工位{StationId} 测试不合格"); else _alarm.ClearAlarm(); } catch (Exception ex) { System.Windows.MessageBox.Show($"保存测试结果失败:{ex.Message}\n{ex.InnerException?.Message}", "数据库错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); } } private async Task ExportHistoryAsync() { var batches = await Task.Run(() => _db.GetBatches(StationId, 100)); string path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"工位{StationId}_检测记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); _excel.ExportToExcel(batches, path); System.Windows.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); } } }