2026-05-05 15:31:24 +08:00
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
using OxyPlot;
|
|
|
|
|
|
using OxyPlot.Series;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Threading.Tasks;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
using System.Windows;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
private readonly BalanceService _balance; // ✅ 新增天平服务
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private DispatcherTimer _disintegrationTimer;
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
private List<double> _dissolutionTimes = new List<double>();
|
|
|
|
|
|
private List<double> _dissolutionValues = new List<double>();
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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<double> _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;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 硬度相关新增
|
|
|
|
|
|
[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; }
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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; }
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
[ObservableProperty] private string _localAlarm;
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_balance = new BalanceService(); // 实例化天平服务(模拟)
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
|
|
|
|
|
StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync);
|
|
|
|
|
|
StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync);
|
|
|
|
|
|
StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync);
|
|
|
|
|
|
StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync);
|
|
|
|
|
|
ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync);
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
// 溶出曲线
|
2026-05-05 15:31:24 +08:00
|
|
|
|
DissolutionPlotModel = new PlotModel { Title = $"工位{StationId} 溶出曲线" };
|
|
|
|
|
|
_dissolutionSeries = new LineSeries { Title = "溶出度 (%)", Color = OxyColors.Green };
|
|
|
|
|
|
DissolutionPlotModel.Series.Add(_dissolutionSeries);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 硬度命令
|
|
|
|
|
|
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;
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
if (Phase != TestPhase.Idle) return;
|
|
|
|
|
|
CurrentTest = TestType.Hardness;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
|
|
|
|
|
HardnessPass = false; // 添加这一行
|
|
|
|
|
|
_hardnessResults.Clear();
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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++)
|
|
|
|
|
|
{
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"硬度测试出错:{ex.Message}"));
|
2026-05-05 15:31:24 +08:00
|
|
|
|
Phase = TestPhase.Error;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
Phase = TestPhase.Idle;
|
|
|
|
|
|
|
|
|
|
|
|
// 在保存前设置崩解合格标志:剩余管数为0 且 崩解时间未超时
|
|
|
|
|
|
//DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await SaveBatchResult();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunFriabilityAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Phase != TestPhase.Idle) return;
|
|
|
|
|
|
CurrentTest = TestType.Friability;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
FriabilityPass = false; // 添加这一行
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 通过天平读取前重
|
|
|
|
|
|
WeightBefore = await _balance.ReadWeightAsync();
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true);
|
|
|
|
|
|
|
|
|
|
|
|
// 根据设定的转速和总圈数计算运行时间
|
|
|
|
|
|
int totalRounds = 100;
|
|
|
|
|
|
double rpm = FriabilityTargetRpm;
|
|
|
|
|
|
int durationMs = (int)((totalRounds / rpm) * 60 * 1000);
|
|
|
|
|
|
await Task.Delay(durationMs);
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
WeightAfter = await _balance.ReadWeightAsync();
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
await SaveBatchResult();
|
|
|
|
|
|
}
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunDisintegrationAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Phase != TestPhase.Idle) return;
|
|
|
|
|
|
CurrentTest = TestType.Disintegration;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DisintegrationPass = false; // 添加这一行
|
2026-05-05 15:31:24 +08:00
|
|
|
|
TubesCompleted = new bool[6];
|
|
|
|
|
|
RemainingTubes = 6;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DisintegrationSeconds = 0;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_disintegrationTimer.Tick += (s, e) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Phase == TestPhase.Running)
|
|
|
|
|
|
DisintegrationSeconds++;
|
|
|
|
|
|
};
|
2026-05-05 15:31:24 +08:00
|
|
|
|
_disintegrationTimer.Start();
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunDissolutionAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Phase != TestPhase.Idle) return;
|
|
|
|
|
|
CurrentTest = TestType.Dissolution;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DissolutionPass = false; // 添加这一行
|
2026-05-05 15:31:24 +08:00
|
|
|
|
_dissolutionStartTime = DateTime.Now;
|
|
|
|
|
|
_dissolutionSeries.Points.Clear();
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_dissolutionTimes.Clear();
|
|
|
|
|
|
_dissolutionValues.Clear();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
try
|
2026-05-05 15:31:24 +08:00
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true);
|
|
|
|
|
|
var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes;
|
|
|
|
|
|
double prevMin = 0;
|
|
|
|
|
|
foreach (var t in sampleTimes)
|
2026-05-05 15:31:24 +08:00
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
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();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task SaveBatchResult(bool? forcedQualified = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
// 计算溶出曲线的 R²(需在 RunDissolutionAsync 中计算后存入 _dissolutionRSquared)
|
|
|
|
|
|
double rsquared = DissolutionRSquared; // 确保此属性在溶出结束后被赋值
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
var batch = new TestBatch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestTime = DateTime.Now,
|
|
|
|
|
|
StationId = StationId,
|
|
|
|
|
|
SampleName = $"样品-{StationId}",
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 硬度
|
2026-05-05 15:31:24 +08:00
|
|
|
|
HardnessAvg = HardnessAvg,
|
|
|
|
|
|
HardnessRSD = HardnessRSD,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
HardnessMax = HardnessMax,
|
|
|
|
|
|
HardnessMin = HardnessMin,
|
|
|
|
|
|
HardnessTestCount = HardnessTestCount,
|
|
|
|
|
|
|
|
|
|
|
|
// 脆碎度
|
2026-05-05 15:31:24 +08:00
|
|
|
|
FriabilityLoss = LossPercent,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
FriabilityTargetRpm = FriabilityTargetRpm,
|
|
|
|
|
|
FriabilityTargetTimeSec = FriabilityTargetTimeSec,
|
|
|
|
|
|
FriabilityClockwise = FriabilityClockwise,
|
|
|
|
|
|
FriabilityRemainingRounds = FriabilityRemainingRounds,
|
|
|
|
|
|
WeightBefore = WeightBefore,
|
|
|
|
|
|
WeightAfter = WeightAfter,
|
|
|
|
|
|
|
|
|
|
|
|
// 崩解
|
2026-05-05 15:31:24 +08:00
|
|
|
|
DisintegrationTimeSec = DisintegrationSeconds,
|
|
|
|
|
|
RemainingTubesAtEnd = RemainingTubes,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DisintegrationTargetFreq = DisintegrationTargetFreq,
|
|
|
|
|
|
DisintegrationTemp = DisintegrationTemp,
|
|
|
|
|
|
|
|
|
|
|
|
// 溶出
|
2026-05-05 15:31:24 +08:00
|
|
|
|
DissolutionRate30Min = DissolutionPercent,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
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
|
2026-05-05 15:31:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
await Task.Run(() => _db.InsertBatch(batch));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}测试不合格";
|
|
|
|
|
|
});
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}"));
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
private double CalculateRSquared(List<double> timeMinutes, List<double> 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"导出成功: {path}"));
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private double StandardDeviation(List<double> 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|