using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using OxyPlot; using OxyPlot.Series; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; 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 readonly BalanceService _balance; // ✅ 新增天平服务 private DispatcherTimer _disintegrationTimer; private List _dissolutionTimes = new List(); private List _dissolutionValues = new List(); 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; // 硬度相关新增 [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 PrintHardnessCommand { get; } // 脆碎度新增 [ObservableProperty] private double _friabilityTargetRpm = 25; [ObservableProperty] private int _friabilityTargetTimeSec = 240; [ObservableProperty] private bool _friabilityClockwise = true; [ObservableProperty] private bool _friabilityCounterClockwise; [ObservableProperty] private double _friabilityCurrentRpm; [ObservableProperty] private int _friabilityRemainingRounds = 100; 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 double _dissolutionElapsedTime; [ObservableProperty] private double _dissolutionCountdown; [ObservableProperty] private double _dissolutionRSquared; public IAsyncRelayCommand DissolutionUpCommand { get; } public IAsyncRelayCommand DissolutionDownCommand { get; } public IAsyncRelayCommand StopDissolutionCommand { get; } public IAsyncRelayCommand PrintDissolutionCommand { get; } // 崩解新增 [ObservableProperty] private double _disintegrationTargetFreq = 31; public IAsyncRelayCommand StopDisintegrationCommand { get; } public IAsyncRelayCommand PrintDisintegrationCommand { get; } 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; } [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; _balance = new BalanceService(); // 实例化天平服务(模拟) StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync); StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync); StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync); StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync); ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync); // 溶出曲线 DissolutionPlotModel = new PlotModel { Title = $"工位{StationId} 溶出曲线" }; _dissolutionSeries = new LineSeries { Title = "溶出度 (%)", Color = OxyColors.Green }; DissolutionPlotModel.Series.Add(_dissolutionSeries); // 硬度命令 //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(() => { _hardnessResults.Clear(); HardnessValue = 0; HardnessAvg = 0; HardnessRSD = 0; HardnessCurrentCount = 0; HardnessMax = 0; HardnessMin = 0; Phase = TestPhase.Idle; return Task.CompletedTask; }); PrintHardnessCommand = new AsyncRelayCommand(async () => await PrintReport("硬度")); // 脆碎度命令 StopFriabilityCommand = new AsyncRelayCommand(() => { //测试停止 Phase = TestPhase.Idle; return Task.CompletedTask; }); ResetFriabilityCommand = new AsyncRelayCommand(() => { FriabilityRemainingRounds = 100; // 剩余圈数重置为默认值(通常是100圈) LossPercent = 0; // 失重率清零 WeightBefore = 0; // 脆碎前重量清零 WeightAfter = 0;// 脆碎后重量清零 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)); StopDissolutionCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; }); PrintDissolutionCommand = new AsyncRelayCommand(async () => await PrintReport("溶出度")); // 崩解命令 StopDisintegrationCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; }); PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解")); } private async Task PrintReport(string testName) { await App.Current.Dispatcher.InvokeAsync(() => { MessageBox.Show($"打印{testName}报告 - 工位{StationId}", "打印", MessageBoxButton.OK, MessageBoxImage.Information); }); } 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() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Hardness; Phase = TestPhase.Running; HardnessPass = false; // 添加这一行 _hardnessResults.Clear(); try { int count = App.CurrentPharmaParams.HardnessTestCount; double min = App.CurrentPharmaParams.HardnessMin_N; double max = App.CurrentPharmaParams.HardnessMax_N; for (int i = 0; i < count; i++) { // 1. 给PLC发指令:启动单次硬度测试 await _plc.WriteCoilAsync((ushort)(StationId == 1 ? _plcConfig.HardnessStartCoil : StationId == 2 ? _plcConfig.HardnessStartCoil2 : StationId == 3 ? _plcConfig.HardnessStartCoil3 : 0), true); bool completed = false; while (!completed && Phase == TestPhase.Running) { await Task.Delay(200);// 每200ms轮询一次状态 completed = await _plc.ReadCoilAsync(_plcConfig.HardnessCompleteCoil); } double val = await _plc.ReadFloatAsync(_plcConfig.HardnessValue); _hardnessResults.Add(val); // 把结果存到列表里 HardnessValue = val; // 更新界面上的实时测试力值 await Task.Delay(1000); // 间隔1秒,再进行下一次测试 } 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; // 在保存前设置崩解合格标志:剩余管数为0 且 崩解时间未超时 //DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds); await SaveBatchResult(); } } /// 脆碎度测试主逻辑(适配3工位、实时状态显示) private async Task RunFriabilityAsync() { // 1. 防并发:如果设备不是空闲状态,直接退出 if (Phase != TestPhase.Idle) return; // 2. 标记当前正在运行的是脆碎度测试 CurrentTest = TestType.Friability; Phase = TestPhase.Running; FriabilityPass = false; try { ushort startCoil = StationId switch { 1 => _plcConfig.FriabilityStartCoil, // 工位1启动线圈 2 => _plcConfig.FriabilityStartCoil2, // 工位2启动线圈 3 => _plcConfig.FriabilityStartCoil3, // 工位3启动线圈 _ => 0 }; if (startCoil == 0) { throw new InvalidOperationException("当前工位未配置脆碎度启动线圈地址"); } WeightBefore = await _balance.ReadWeightAsync(); await _plc.WriteCoilAsync(startCoil, true); int totalRounds = 100; // 药典标准脆碎度总圈数:100圈 double rpm = FriabilityTargetRpm; // 界面设置的目标转速(r/min) 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); } WeightAfter = await _balance.ReadWeightAsync(); FriabilityCurrentRpm = FriabilityTargetRpm; LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率 FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; //标准值 // 标记测试为已完成 Phase = TestPhase.Completed; } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"脆碎度测试出错: {ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; FriabilityRemainingRounds = 100; await SaveBatchResult(); } } 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 _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; } catch (Exception ex) { _disintegrationTimer.Stop(); await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds); await SaveBatchResult(); } } private async Task RunDissolutionAsync() { if (Phase != TestPhase.Idle) return; CurrentTest = TestType.Dissolution; Phase = TestPhase.Running; DissolutionPass = false; // 添加这一行 _dissolutionStartTime = DateTime.Now; _dissolutionSeries.Points.Clear(); _dissolutionTimes.Clear(); _dissolutionValues.Clear(); try { 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; _dissolutionTimes.Add(t); _dissolutionValues.Add(value); prevMin = t; await App.Current.Dispatcher.InvokeAsync(() => { MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%"); }); } // 计算 R² _dissolutionRSquared = CalculateRSquared(_dissolutionTimes, _dissolutionValues); bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min; Phase = TestPhase.Completed; } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"溶出测试出错: {ex.Message}")); Phase = TestPhase.Error; } finally { Phase = TestPhase.Idle; DissolutionPass = (DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min); await SaveBatchResult(); } } private async Task SaveBatchResult(bool? forcedQualified = null) { try { // 计算溶出曲线的 R²(需在 RunDissolutionAsync 中计算后存入 _dissolutionRSquared) double rsquared = DissolutionRSquared; // 确保此属性在溶出结束后被赋值 var batch = new TestBatch { TestTime = DateTime.Now, StationId = StationId, SampleName = $"样品-{StationId}", // 硬度 HardnessAvg = HardnessAvg, HardnessRSD = HardnessRSD, HardnessMax = HardnessMax, HardnessMin = HardnessMin, HardnessTestCount = HardnessTestCount, // 脆碎度 FriabilityLoss = LossPercent, FriabilityTargetRpm = FriabilityTargetRpm, FriabilityTargetTimeSec = FriabilityTargetTimeSec, FriabilityClockwise = FriabilityClockwise, FriabilityRemainingRounds = FriabilityRemainingRounds, WeightBefore = WeightBefore, WeightAfter = WeightAfter, // 崩解 DisintegrationTimeSec = DisintegrationSeconds, RemainingTubesAtEnd = RemainingTubes, DisintegrationTargetFreq = DisintegrationTargetFreq, DisintegrationTemp = DisintegrationTemp, // 溶出 DissolutionRate30Min = DissolutionPercent, DissolutionTargetRpm = DissolutionTargetRpm, DissolutionRSquared = rsquared, DissolutionSampleInterval = DissolutionSampleInterval, 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 }; await Task.Run(() => _db.InsertBatch(batch)); await Application.Current.Dispatcher.InvokeAsync(() => { // 获取当前测试项目是否合格 bool currentPass = CurrentTest switch { TestType.Hardness => HardnessPass, TestType.Friability => FriabilityPass, TestType.Disintegration => DisintegrationPass, TestType.Dissolution => DissolutionPass, _ => false }; string projectName = CurrentTest switch { TestType.Hardness => "硬度", TestType.Friability => "脆碎度", TestType.Disintegration => "崩解", TestType.Dissolution => "溶出", _ => "" }; LocalAlarm = currentPass ? $"{projectName}测试合格" : $"{projectName}测试不合格"; }); } catch (Exception ex) { await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}")); } } private double CalculateRSquared(List timeMinutes, List concentration) { if (timeMinutes.Count < 2) return 1; 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)); double r = numerator / denominator; return r * r; } 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); 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); } } }