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;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
using OxyPlot.Axes;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
using OxyPlot.Series;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
using System.Collections.ObjectModel;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Threading.Tasks;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
using System.Windows;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
using System.Windows.Controls;
|
|
|
|
|
|
using System.Windows.Media;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
using System.Windows.Threading;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
using TabletTester2025.Helpers;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
using TabletTester2025.Models;
|
|
|
|
|
|
using TabletTester2025.Services;
|
|
|
|
|
|
|
|
|
|
|
|
namespace TabletTester2025.ViewModels
|
|
|
|
|
|
{
|
|
|
|
|
|
public partial class StationViewModel : ObservableObject
|
|
|
|
|
|
{
|
2026-05-18 11:55:25 +08:00
|
|
|
|
private DispatcherTimer _hardnessGlobalTimer;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private readonly IPlcService _plc;
|
|
|
|
|
|
private readonly PlcConfiguration _plcConfig;
|
|
|
|
|
|
private readonly DatabaseService _db;
|
|
|
|
|
|
private readonly ExcelExportService _excel;
|
|
|
|
|
|
private readonly AlarmService _alarm;
|
|
|
|
|
|
private DispatcherTimer _disintegrationTimer;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private bool _isLoadingDissolution1Time;
|
|
|
|
|
|
private bool _isLoadingDissolution2Time;
|
2026-05-18 09:47:22 +08:00
|
|
|
|
private bool _isLoadingDissolution1SampleInterval;
|
|
|
|
|
|
private bool _isLoadingDissolution2SampleInterval;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private bool _isLoadingDisintegrationTime;
|
|
|
|
|
|
private bool _isLoadingDisintegrationSpeed;
|
2026-05-19 18:44:56 +08:00
|
|
|
|
private bool _isLoadingFriabilityRounds;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
private bool _isUpdatingFriabilityWeightFromPlc;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
|
|
|
|
|
|
private readonly List<double> _dissolution1Times = new();
|
|
|
|
|
|
private readonly List<double> _dissolution1Values = new();
|
|
|
|
|
|
private readonly List<double> _dissolution2Times = new();
|
|
|
|
|
|
private readonly List<double> _dissolution2Values = new();
|
2026-05-18 14:06:04 +08:00
|
|
|
|
public ObservableCollection<DissolutionSamplePoint> DissolutionSamplePoints { get; } = new();
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private DateTime _dissolution1StartTime = DateTime.MinValue;
|
|
|
|
|
|
private DateTime _dissolution2StartTime = DateTime.MinValue;
|
|
|
|
|
|
private bool _isDissolution1Running;
|
|
|
|
|
|
private bool _isDissolution2Running;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
private bool _dissolution1SampleRequestActive;
|
|
|
|
|
|
private bool _dissolution2SampleRequestActive;
|
|
|
|
|
|
private bool _isDissolution1SamplePromptOpen;
|
|
|
|
|
|
private bool _isDissolution2SamplePromptOpen;
|
2026-05-18 09:47:22 +08:00
|
|
|
|
private bool _discardDisintegrationResult;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private string _dissolutionResultChannel = "";
|
|
|
|
|
|
private double _dissolutionResultRate30Min;
|
|
|
|
|
|
private double _dissolutionResultRSquared;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
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;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
[ObservableProperty] private double _hardnessInternalMin = 40;
|
|
|
|
|
|
[ObservableProperty] private double _hardnessInternalMax = 60;
|
2026-05-18 09:55:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//硬度新增
|
|
|
|
|
|
//[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
|
2026-05-18 10:31:59 +08:00
|
|
|
|
[ObservableProperty] private double _hardnessWeiyi = 100; // 硬度位移输入mm
|
|
|
|
|
|
//[ObservableProperty] private double _hardnessPoSun = 400; // 硬度破损判定输入N
|
2026-05-18 11:55:25 +08:00
|
|
|
|
//[ObservableProperty] private double _hardnessMaxN = 72; //最大力采集
|
|
|
|
|
|
[ObservableProperty] private double _hardnessShishilizhi = 0.0; //最大力采集
|
2026-05-18 09:55:00 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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-16 16:58:57 +08:00
|
|
|
|
[ObservableProperty] private double _dissolution1Percent;
|
|
|
|
|
|
[ObservableProperty] private double _dissolution2Percent;
|
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;
|
2026-05-18 14:57:15 +08:00
|
|
|
|
[ObservableProperty] private double _hardnessMin;
|
2026-05-18 11:55:25 +08:00
|
|
|
|
|
2026-05-18 11:28:52 +08:00
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty] private bool _disintegrationPass; // 新增
|
|
|
|
|
|
[ObservableProperty] private bool _dissolutionPass; // 新增
|
|
|
|
|
|
|
|
|
|
|
|
public IAsyncRelayCommand HardnessUpCommand { get; }
|
|
|
|
|
|
public IAsyncRelayCommand HardnessDownCommand { get; }
|
|
|
|
|
|
public IAsyncRelayCommand HardnessResetCommand { get; }
|
2026-05-18 10:48:01 +08:00
|
|
|
|
public IAsyncRelayCommand HardnessForward { get; }//前进
|
|
|
|
|
|
public IAsyncRelayCommand HardnessBack { get; }//后退
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 脆碎度新增
|
|
|
|
|
|
[ObservableProperty] private double _friabilityTargetRpm = 25;
|
2026-05-18 18:54:32 +08:00
|
|
|
|
[ObservableProperty] private double _friabilityTargetTimeMin = 4;
|
|
|
|
|
|
[ObservableProperty] private int _friabilityTargetTimeSec = 240;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
[ObservableProperty] private int _friabilityTargetRounds = 100;
|
|
|
|
|
|
[ObservableProperty] private double _friabilityMaxLossPercent = 1.0;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
[ObservableProperty] private bool _friabilityClockwise = true;
|
|
|
|
|
|
[ObservableProperty] private bool _friabilityCounterClockwise;
|
|
|
|
|
|
[ObservableProperty] private double _friabilityCurrentRpm;
|
|
|
|
|
|
[ObservableProperty] private int _friabilityRemainingRounds = 100;
|
2026-05-16 10:55:07 +08:00
|
|
|
|
public IAsyncRelayCommand StopHardnessCommand { get; }
|
2026-05-06 16:41:32 +08:00
|
|
|
|
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;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
[ObservableProperty] private int _dissolution1TimeMin = 30;
|
|
|
|
|
|
[ObservableProperty] private int _dissolution2TimeMin = 30;
|
2026-05-18 09:47:22 +08:00
|
|
|
|
[ObservableProperty] private double _dissolution1SampleIntervalMin = 5;
|
|
|
|
|
|
[ObservableProperty] private double _dissolution2SampleIntervalMin = 5;
|
2026-05-18 15:18:28 +08:00
|
|
|
|
[ObservableProperty] private double _dissolutionMinPercentAt30Min = 80;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
[ObservableProperty] private double _dissolutionElapsedTime;
|
|
|
|
|
|
[ObservableProperty] private double _dissolutionCountdown;
|
|
|
|
|
|
[ObservableProperty] private double _dissolutionRSquared;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
[ObservableProperty] private double _dissolution1RSquared;
|
|
|
|
|
|
[ObservableProperty] private double _dissolution2RSquared;
|
|
|
|
|
|
[ObservableProperty] private string _dissolutionCurveStatus = "";
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
public IAsyncRelayCommand DissolutionUpCommand { get; }
|
|
|
|
|
|
public IAsyncRelayCommand DissolutionDownCommand { get; }
|
|
|
|
|
|
public IAsyncRelayCommand StopDissolutionCommand { get; }
|
|
|
|
|
|
public IAsyncRelayCommand PrintDissolutionCommand { get; }
|
2026-05-16 16:58:57 +08:00
|
|
|
|
public IAsyncRelayCommand StartDissolution1Command { get; }
|
|
|
|
|
|
public IAsyncRelayCommand StopDissolution1Command { get; }
|
2026-05-18 09:47:22 +08:00
|
|
|
|
public IAsyncRelayCommand ResetDissolution1Command { get; }
|
2026-05-16 16:58:57 +08:00
|
|
|
|
public IAsyncRelayCommand StartDissolution2Command { get; }
|
|
|
|
|
|
public IAsyncRelayCommand StopDissolution2Command { get; }
|
2026-05-18 09:47:22 +08:00
|
|
|
|
public IAsyncRelayCommand ResetDissolution2Command { get; }
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 崩解新增
|
|
|
|
|
|
[ObservableProperty] private double _disintegrationTargetFreq = 31;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
[ObservableProperty] private double _disintegrationSpeedRpm = 31;
|
|
|
|
|
|
[ObservableProperty] private double _disintegrationTimeMin = 15;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
[ObservableProperty] private string _disintegrationDosageForm = "普通片";
|
2026-05-06 16:41:32 +08:00
|
|
|
|
public IAsyncRelayCommand StopDisintegrationCommand { get; }
|
2026-05-18 09:47:22 +08:00
|
|
|
|
public IAsyncRelayCommand ResetDisintegrationCommand { get; }
|
2026-05-06 16:41:32 +08:00
|
|
|
|
public IAsyncRelayCommand PrintDisintegrationCommand { get; }
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
public PlotModel DissolutionPlotModel { get; }
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private readonly LineSeries _dissolution1Series;
|
|
|
|
|
|
private readonly LineSeries _dissolution2Series;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 命令
|
|
|
|
|
|
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-18 14:06:04 +08:00
|
|
|
|
LoadPharmaDefaults();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
|
|
|
|
|
StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync);
|
|
|
|
|
|
StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync);
|
|
|
|
|
|
StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync);
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
StartDissolutionCommand = new AsyncRelayCommand(StartDissolution1Async);
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync);
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
// 溶出曲线
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-18 11:55:25 +08:00
|
|
|
|
|
2026-05-15 15:48:43 +08:00
|
|
|
|
|
2026-05-18 11:55:25 +08:00
|
|
|
|
HardnessDownCommand = new AsyncRelayCommand(async () =>
|
2026-05-06 16:41:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _plc.WriteCoilAsync(0x21, true);
|
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
await _plc.WriteCoilAsync(0x21, false);
|
|
|
|
|
|
});
|
2026-05-15 15:48:43 +08:00
|
|
|
|
|
2026-05-18 11:55:25 +08:00
|
|
|
|
//硬复位
|
2026-05-16 10:55:07 +08:00
|
|
|
|
HardnessResetCommand = new AsyncRelayCommand(async () =>
|
2026-05-06 16:41:32 +08:00
|
|
|
|
{
|
2026-05-16 10:55:07 +08:00
|
|
|
|
// 1. 标准PLC按钮脉冲逻辑:置1 → 延时 → 置0(模拟按下再松开按钮)
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, true);
|
|
|
|
|
|
await Task.Delay(100); // 脉冲宽度,可根据PLC程序调整20~100ms
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, false);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_hardnessResults.Clear();
|
|
|
|
|
|
HardnessMax = 0;
|
2026-05-18 14:57:15 +08:00
|
|
|
|
HardnessMin= 0;
|
2026-05-18 11:55:25 +08:00
|
|
|
|
HardnessShishilizhi = 0;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
Phase = TestPhase.Idle;
|
2026-05-16 10:55:07 +08:00
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
});
|
2026-05-15 15:48:43 +08:00
|
|
|
|
|
2026-05-18 10:48:01 +08:00
|
|
|
|
// 硬前进按钮命令
|
|
|
|
|
|
HardnessForward = new AsyncRelayCommand(async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessForward, true);
|
|
|
|
|
|
await Task.Delay(100); // 脉冲宽度,和复位按钮保持一致
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessForward, false);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 硬后退按钮命令
|
2026-05-18 14:06:04 +08:00
|
|
|
|
HardnessBack = new AsyncRelayCommand(async () =>
|
2026-05-18 10:48:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessBack, true);
|
|
|
|
|
|
await Task.Delay(100); // 脉冲宽度,和复位按钮保持一致
|
|
|
|
|
|
await _plc.WriteCoilAsync(_plcConfig.HardnessBack, false);
|
|
|
|
|
|
});
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-16 10:55:07 +08:00
|
|
|
|
// 硬度命令停止
|
|
|
|
|
|
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;
|
|
|
|
|
|
});
|
2026-05-06 16:41:32 +08:00
|
|
|
|
// 脆碎度命令
|
2026-05-18 14:06:04 +08:00
|
|
|
|
StopFriabilityCommand = new AsyncRelayCommand(async () => {
|
2026-05-16 10:55:07 +08:00
|
|
|
|
|
2026-05-15 17:47:11 +08:00
|
|
|
|
//测试停止
|
2026-05-16 10:55:07 +08:00
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
if (_plcConfig.FriabilityStartCoilStop != 0)
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.FriabilityStartCoilStop);
|
|
|
|
|
|
Phase = TestPhase.Idle;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
});
|
2026-05-06 16:41:32 +08:00
|
|
|
|
ResetFriabilityCommand = new AsyncRelayCommand(() =>
|
|
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
FriabilityRemainingRounds = FriabilityTargetRounds;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
LossPercent = 0; // 失重率清零
|
2026-05-18 14:06:04 +08:00
|
|
|
|
SetFriabilityWeightFromPlc(0, 0); // 清空界面结果,不向PLC写零
|
2026-05-06 16:41:32 +08:00
|
|
|
|
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));
|
2026-05-16 16:58:57 +08:00
|
|
|
|
StartDissolution1Command = new AsyncRelayCommand(StartDissolution1Async);
|
|
|
|
|
|
StopDissolution1Command = new AsyncRelayCommand(StopDissolution1Async);
|
2026-05-18 09:47:22 +08:00
|
|
|
|
ResetDissolution1Command = new AsyncRelayCommand(ResetDissolution1Async);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
StartDissolution2Command = new AsyncRelayCommand(StartDissolution2Async);
|
|
|
|
|
|
StopDissolution2Command = new AsyncRelayCommand(StopDissolution2Async);
|
2026-05-18 09:47:22 +08:00
|
|
|
|
ResetDissolution2Command = new AsyncRelayCommand(ResetDissolution2Async);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
StopDissolutionCommand = new AsyncRelayCommand(StopDissolution1Async);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
PrintDissolutionCommand = new AsyncRelayCommand(async () => await PrintReport("溶出度"));
|
|
|
|
|
|
|
|
|
|
|
|
// 崩解命令
|
2026-05-16 16:58:57 +08:00
|
|
|
|
StopDisintegrationCommand = new AsyncRelayCommand(StopDisintegrationAsync);
|
2026-05-18 09:47:22 +08:00
|
|
|
|
ResetDisintegrationCommand = new AsyncRelayCommand(ResetDisintegrationAsync);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解"));
|
2026-05-16 16:58:57 +08:00
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
_ = LoadFriabilitySettingsAsync();
|
2026-05-18 14:06:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 15:18:28 +08:00
|
|
|
|
public void ApplyPharmaDefaults()
|
2026-05-18 14:06:04 +08:00
|
|
|
|
{
|
|
|
|
|
|
var p = App.CurrentPharmaParams;
|
2026-05-18 15:18:28 +08:00
|
|
|
|
_isLoadingDisintegrationSpeed = true;
|
|
|
|
|
|
_isLoadingDisintegrationTime = true;
|
|
|
|
|
|
_isLoadingDissolution1Time = true;
|
|
|
|
|
|
_isLoadingDissolution2Time = true;
|
|
|
|
|
|
_isLoadingDissolution1SampleInterval = true;
|
|
|
|
|
|
_isLoadingDissolution2SampleInterval = true;
|
2026-05-19 18:44:56 +08:00
|
|
|
|
_isLoadingFriabilityRounds = true;
|
2026-05-18 15:18:28 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
HardnessInternalMin = p.HardnessMin_N;
|
|
|
|
|
|
HardnessInternalMax = p.HardnessMax_N;
|
|
|
|
|
|
HardnessTestCount = Math.Max(1, p.HardnessTestCount);
|
|
|
|
|
|
FriabilityTargetRpm = p.FriabilityTargetRpm > 0 ? p.FriabilityTargetRpm : 25;
|
2026-05-18 18:54:32 +08:00
|
|
|
|
double defaultRounds = p.FriabilityTargetRounds > 0 ? p.FriabilityTargetRounds : 100;
|
2026-05-19 18:44:56 +08:00
|
|
|
|
FriabilityTargetRounds = Math.Max(1, (int)Math.Round(defaultRounds, MidpointRounding.AwayFromZero));
|
2026-05-18 18:54:32 +08:00
|
|
|
|
FriabilityTargetTimeMin = p.FriabilityTargetTimeMin > 0
|
|
|
|
|
|
? p.FriabilityTargetTimeMin
|
|
|
|
|
|
: defaultRounds / FriabilityTargetRpm;
|
|
|
|
|
|
UpdateFriabilityTimingFromTime();
|
2026-05-18 15:18:28 +08:00
|
|
|
|
FriabilityMaxLossPercent = p.FriabilityMaxLossPercent;
|
|
|
|
|
|
FriabilityRemainingRounds = FriabilityTargetRounds;
|
|
|
|
|
|
DisintegrationDosageForm = string.IsNullOrWhiteSpace(p.DisintegrationDosageForm) ? "普通片" : p.DisintegrationDosageForm;
|
|
|
|
|
|
DisintegrationSpeedRpm = p.DisintegrationSpeedRpm > 0 ? p.DisintegrationSpeedRpm : 31;
|
|
|
|
|
|
DisintegrationTemp = p.DisintegrationTemperatureC > 0 ? p.DisintegrationTemperatureC : 37;
|
|
|
|
|
|
Dissolution1TimeMin = p.Dissolution1TimeMin > 0 ? p.Dissolution1TimeMin : 30;
|
|
|
|
|
|
Dissolution2TimeMin = p.Dissolution2TimeMin > 0 ? p.Dissolution2TimeMin : 30;
|
|
|
|
|
|
Dissolution1SampleIntervalMin = p.Dissolution1SampleIntervalMin > 0 ? p.Dissolution1SampleIntervalMin : 5;
|
|
|
|
|
|
Dissolution2SampleIntervalMin = p.Dissolution2SampleIntervalMin > 0 ? p.Dissolution2SampleIntervalMin : 5;
|
|
|
|
|
|
DissolutionMinPercentAt30Min = p.DissolutionMinPercentAt30min;
|
|
|
|
|
|
DissolutionSampleInterval = ToCompatibleSampleInterval(Dissolution1SampleIntervalMin);
|
|
|
|
|
|
int seconds = p.DisintegrationMaxSeconds > 0 ? p.DisintegrationMaxSeconds : ResolveDisintegrationLimitSeconds();
|
|
|
|
|
|
if (seconds > 0)
|
|
|
|
|
|
DisintegrationTimeMin = seconds / 60.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_isLoadingDisintegrationSpeed = false;
|
|
|
|
|
|
_isLoadingDisintegrationTime = false;
|
|
|
|
|
|
_isLoadingDissolution1Time = false;
|
|
|
|
|
|
_isLoadingDissolution2Time = false;
|
|
|
|
|
|
_isLoadingDissolution1SampleInterval = false;
|
|
|
|
|
|
_isLoadingDissolution2SampleInterval = false;
|
2026-05-19 18:44:56 +08:00
|
|
|
|
_isLoadingFriabilityRounds = false;
|
2026-05-18 15:18:28 +08:00
|
|
|
|
}
|
2026-05-18 18:54:32 +08:00
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
_ = WriteFriabilityRoundsAsync(FriabilityTargetRounds);
|
2026-05-18 15:18:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void LoadPharmaDefaults()
|
|
|
|
|
|
{
|
|
|
|
|
|
ApplyPharmaDefaults();
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task PrintReport(string testName)
|
|
|
|
|
|
{
|
|
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
MessageBox.Show($"打印{testName}报告", "打印", MessageBoxButton.OK, MessageBoxImage.Information);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
});
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task UpdateRealTimeData()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Phase != TestPhase.Running) return;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
switch (CurrentTest)
|
|
|
|
|
|
{
|
2026-05-18 10:48:01 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
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;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
RemainingTubes = TubesCompleted.Length - TubesCompleted.Count(c => c);
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case TestType.Dissolution:
|
2026-05-16 16:58:57 +08:00
|
|
|
|
await UpdateDissolutionDataAsync();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task UpdateDissolutionDataAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
DissolutionRpm = await _plc.ReadFloatAsync(_plcConfig.DissolutionRpm);
|
|
|
|
|
|
if (_plcConfig.DisintegrationTemp != 0)
|
|
|
|
|
|
DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp);
|
|
|
|
|
|
|
|
|
|
|
|
if (_isDissolution1Running)
|
2026-05-18 16:53:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
await ReadDissolutionChannelAsync(1);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
await CheckDissolutionSampleAsync(1);
|
2026-05-18 16:53:29 +08:00
|
|
|
|
}
|
2026-05-16 16:58:57 +08:00
|
|
|
|
|
|
|
|
|
|
if (_isDissolution2Running)
|
2026-05-18 16:53:29 +08:00
|
|
|
|
{
|
|
|
|
|
|
await ReadDissolutionChannelAsync(2);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
await CheckDissolutionSampleAsync(2);
|
2026-05-18 16:53:29 +08:00
|
|
|
|
}
|
2026-05-16 16:58:57 +08:00
|
|
|
|
|
|
|
|
|
|
UpdateDissolutionClock();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 18:16:25 +08:00
|
|
|
|
private async Task CheckDissolutionSampleAsync(int channel)
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (channel == 2 && _plcConfig.Dissolution2Percent == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
DissolutionCurveStatus = "溶出2溶出度寄存器未配置,未启用取样计算";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 18:16:25 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
if (GetNextPendingDissolutionSample(channel) == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _plc.WriteCoilAsync(coilAddress, true);
|
|
|
|
|
|
DissolutionCurveStatus = $"溶出{channel}已完成全部取样点";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 18:16:25 +08:00
|
|
|
|
SetDissolutionSampleRequestActive(channel, true);
|
|
|
|
|
|
SetDissolutionSamplePromptOpen(channel, true);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
double percent = await ShowDissolutionSampleDialogAsync(channel);
|
|
|
|
|
|
RecordDissolutionSample(channel, percent);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
await _plc.WriteCoilAsync(coilAddress, true);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
LocalAlarm = $"溶出{channel}已记录取样结果";
|
2026-05-16 18:16:25 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
private async Task<double> ShowDissolutionSampleDialogAsync(int channel)
|
2026-05-16 18:16:25 +08:00
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
double? result = await App.Current.Dispatcher.InvokeAsync<double?>(() =>
|
2026-05-16 18:16:25 +08:00
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
var nextSample = GetNextPendingDissolutionSample(channel);
|
|
|
|
|
|
double elapsed = GetDissolutionElapsedMinutes(channel);
|
|
|
|
|
|
string plannedText = nextSample == null
|
|
|
|
|
|
? "未找到待记录取样点"
|
|
|
|
|
|
: $"计划取样时间:{nextSample.ScheduledTimeMin:0.##} min";
|
|
|
|
|
|
|
2026-05-16 18:16:25 +08:00
|
|
|
|
var dialog = new Window
|
|
|
|
|
|
{
|
|
|
|
|
|
Title = $"溶出{channel}取样",
|
2026-05-18 14:06:04 +08:00
|
|
|
|
Width = 460,
|
2026-05-16 18:16:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
Text = $"请人工采集样品并完成分析,录入该时间点的溶出度(%)。\n{plannedText}\n当前运行时间:{elapsed:0.##} min",
|
2026-05-16 18:16:25 +08:00
|
|
|
|
FontSize = 16,
|
|
|
|
|
|
Foreground = new SolidColorBrush(Color.FromRgb(16, 42, 67)),
|
|
|
|
|
|
TextWrapping = TextWrapping.Wrap,
|
2026-05-18 14:06:04 +08:00
|
|
|
|
Margin = new Thickness(0, 0, 0, 14)
|
2026-05-16 18:16:25 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
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
|
2026-05-16 18:16:25 +08:00
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
Content = "记录取样",
|
|
|
|
|
|
Width = 112,
|
2026-05-16 18:16:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
};
|
2026-05-18 14:06:04 +08:00
|
|
|
|
confirmButton.Click += (_, _) =>
|
2026-05-16 18:16:25 +08:00
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
if (!double.TryParse(input.Text, out double value) || !IsValidDissolutionPercent(value))
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageBox.Show("请输入0-150之间的溶出度百分比。", "取样结果无效", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
input.Tag = value;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
dialog.DialogResult = true;
|
|
|
|
|
|
dialog.Close();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
buttons.Children.Add(cancelButton);
|
|
|
|
|
|
buttons.Children.Add(confirmButton);
|
|
|
|
|
|
panel.Children.Add(buttons);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
dialog.Content = panel;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
return dialog.ShowDialog() == true && input.Tag is double value ? value : null;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
if (!result.HasValue)
|
|
|
|
|
|
throw new InvalidOperationException("取样结果未录入,未写入确认线圈");
|
|
|
|
|
|
|
|
|
|
|
|
return result.Value;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task<bool> 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;
|
|
|
|
|
|
|
|
|
|
|
|
if (channel == 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
Dissolution1Percent = value;
|
|
|
|
|
|
DissolutionPercent = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Dissolution2Percent = value;
|
|
|
|
|
|
DissolutionPercent = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
private void CreateDissolutionSampleSchedule(int channel)
|
|
|
|
|
|
{
|
|
|
|
|
|
RemoveDissolutionSamples(channel);
|
|
|
|
|
|
|
2026-05-18 17:18:56 +08:00
|
|
|
|
foreach (double minute in ResolveDissolutionSampleTimes(channel))
|
2026-05-18 14:06:04 +08:00
|
|
|
|
{
|
|
|
|
|
|
DissolutionSamplePoints.Add(new DissolutionSamplePoint
|
|
|
|
|
|
{
|
|
|
|
|
|
Channel = channel,
|
|
|
|
|
|
ScheduledTimeMin = minute
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 17:18:56 +08:00
|
|
|
|
private List<double> ResolveDissolutionSampleTimes(int channel)
|
|
|
|
|
|
{
|
|
|
|
|
|
int durationMin = channel == 1 ? Dissolution1TimeMin : Dissolution2TimeMin;
|
|
|
|
|
|
double intervalMin = channel == 1 ? Dissolution1SampleIntervalMin : Dissolution2SampleIntervalMin;
|
|
|
|
|
|
durationMin = Math.Max(1, durationMin);
|
|
|
|
|
|
|
|
|
|
|
|
var configuredTimes = App.CurrentPharmaParams.DissolutionSampleTimes?
|
|
|
|
|
|
.Where(t => t > 0)
|
|
|
|
|
|
.Select(t => (double)t)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
var times = configuredTimes != null && configuredTimes.Count > 0
|
|
|
|
|
|
? configuredTimes
|
|
|
|
|
|
: GenerateIntervalSampleTimes(durationMin, intervalMin);
|
|
|
|
|
|
|
|
|
|
|
|
times.Add(Math.Min(30, durationMin));
|
|
|
|
|
|
if (durationMin >= 30)
|
|
|
|
|
|
times.Add(30);
|
|
|
|
|
|
times.Add(durationMin);
|
|
|
|
|
|
|
|
|
|
|
|
return times
|
|
|
|
|
|
.Where(t => t > 0 && t <= durationMin)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.OrderBy(t => t)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static List<double> GenerateIntervalSampleTimes(int durationMin, double intervalMin)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!double.IsFinite(intervalMin) || intervalMin <= 0)
|
|
|
|
|
|
intervalMin = 5;
|
|
|
|
|
|
|
|
|
|
|
|
var times = new List<double>();
|
|
|
|
|
|
for (double time = intervalMin; time <= durationMin + 0.0001; time += intervalMin)
|
|
|
|
|
|
times.Add(Math.Min(time, durationMin));
|
|
|
|
|
|
|
|
|
|
|
|
if (times.Count == 0 || Math.Abs(times[^1] - durationMin) > 0.0001)
|
|
|
|
|
|
times.Add(durationMin);
|
|
|
|
|
|
|
|
|
|
|
|
return times;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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<double>();
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:47:22 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-18 09:47:22 +08:00
|
|
|
|
|
|
|
|
|
|
await LoadDissolutionSampleIntervalsAsync();
|
2026-05-16 16:58:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:47:22 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
partial void OnWeightBeforeChanged(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_isUpdatingFriabilityWeightFromPlc)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
_ = WriteFriabilityWeightAsync(ResolveFriabilityWeightBeforeRegister(), value);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnWeightAfterChanged(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_isUpdatingFriabilityWeightFromPlc)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
_ = WriteFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFriabilityTargetTimeMinChanged(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
UpdateFriabilityTimingFromTime();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFriabilityTargetRpmChanged(double value)
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnFriabilityTargetRoundsChanged(int value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (value > 0 && Phase != TestPhase.Running)
|
|
|
|
|
|
FriabilityRemainingRounds = value;
|
|
|
|
|
|
|
|
|
|
|
|
if (_isLoadingFriabilityRounds || value <= 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
_ = WriteFriabilityRoundsAsync(value);
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateFriabilityTimingFromTime()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!double.IsFinite(FriabilityTargetTimeMin) || FriabilityTargetTimeMin <= 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
FriabilityTargetTimeSec = (int)Math.Ceiling(FriabilityTargetTimeMin * 60);
|
|
|
|
|
|
|
|
|
|
|
|
if (Phase != TestPhase.Running)
|
|
|
|
|
|
FriabilityRemainingRounds = FriabilityTargetRounds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task LoadFriabilitySettingsAsync()
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
await LoadFriabilityRoundsAsync();
|
2026-05-18 18:54:32 +08:00
|
|
|
|
await LoadFriabilityWeightsAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
private async Task LoadFriabilityRoundsAsync()
|
2026-05-18 18:54:32 +08:00
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
ushort registerAddress = ResolveFriabilityRoundsRegister();
|
2026-05-18 18:54:32 +08:00
|
|
|
|
if (registerAddress == 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
_isLoadingFriabilityRounds = true;
|
2026-05-19 16:55:00 +08:00
|
|
|
|
int value = await _plc.ReadIntAsync(registerAddress);
|
|
|
|
|
|
if (value > 0)
|
2026-05-18 18:54:32 +08:00
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
ApplyFriabilityRoundsFromPlc(value);
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
await WriteFriabilityRoundsAsync(FriabilityTargetRounds);
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
try { await WriteFriabilityRoundsAsync(FriabilityTargetRounds); }
|
2026-05-18 18:54:32 +08:00
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
_isLoadingFriabilityRounds = false;
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
2026-05-18 14:06:04 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
private void ApplyFriabilityRoundsFromPlc(int rounds)
|
|
|
|
|
|
{
|
|
|
|
|
|
FriabilityTargetRounds = Math.Max(1, rounds);
|
|
|
|
|
|
|
|
|
|
|
|
if (Phase != TestPhase.Running)
|
|
|
|
|
|
FriabilityRemainingRounds = FriabilityTargetRounds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
private async Task LoadFriabilityWeightsAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
_isUpdatingFriabilityWeightFromPlc = true;
|
|
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
ushort beforeRegister = ResolveFriabilityWeightBeforeRegister();
|
|
|
|
|
|
if (beforeRegister != 0)
|
2026-05-18 14:06:04 +08:00
|
|
|
|
{
|
2026-05-18 18:54:32 +08:00
|
|
|
|
double before = await ReadFriabilityWeightAsync(beforeRegister, "脆碎前重量");
|
2026-05-18 14:06:04 +08:00
|
|
|
|
WeightBefore = before;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
ushort afterRegister = ResolveFriabilityWeightAfterRegister();
|
|
|
|
|
|
if (afterRegister != 0)
|
2026-05-18 14:06:04 +08:00
|
|
|
|
{
|
2026-05-18 18:54:32 +08:00
|
|
|
|
double after = await ReadFriabilityWeightAsync(afterRegister, "脆碎后重量");
|
2026-05-18 14:06:04 +08:00
|
|
|
|
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 { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
private async Task WriteFriabilityRoundsAsync(int value)
|
2026-05-18 18:54:32 +08:00
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
ushort registerAddress = ResolveFriabilityRoundsRegister();
|
|
|
|
|
|
if (registerAddress == 0 || value <= 0)
|
2026-05-18 18:54:32 +08:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
await _plc.WriteRegisterAsync(registerAddress, (ushort)Math.Clamp(value, 0, ushort.MaxValue));
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
private ushort ResolveFriabilityRoundsRegister()
|
2026-05-18 18:54:32 +08:00
|
|
|
|
{
|
2026-05-19 18:44:56 +08:00
|
|
|
|
if (_plcConfig.FriabilityRounds != 0)
|
|
|
|
|
|
return _plcConfig.FriabilityRounds;
|
|
|
|
|
|
|
|
|
|
|
|
if (_plcConfig.FriabilityRoundsBox != 0)
|
|
|
|
|
|
return _plcConfig.FriabilityRoundsBox;
|
|
|
|
|
|
|
|
|
|
|
|
return _plcConfig.FriabilityTestTime != 0 ? _plcConfig.FriabilityTestTime : (ushort)410;
|
2026-05-18 18:54:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ushort ResolveFriabilityWeightBeforeRegister()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _plcConfig.FriabilityWeightBefore != 0
|
|
|
|
|
|
? _plcConfig.FriabilityWeightBefore
|
|
|
|
|
|
: _plcConfig.WeightBefore;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ushort ResolveFriabilityWeightAfterRegister()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _plcConfig.FriabilityWeightAfter != 0
|
|
|
|
|
|
? _plcConfig.FriabilityWeightAfter
|
|
|
|
|
|
: _plcConfig.WeightAfter;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
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;
|
2026-05-18 15:18:28 +08:00
|
|
|
|
if (string.Equals(form, App.CurrentPharmaParams.DisintegrationDosageForm, StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
|
&& App.CurrentPharmaParams.DisintegrationMaxSeconds > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return App.CurrentPharmaParams.DisintegrationMaxSeconds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
return form switch
|
|
|
|
|
|
{
|
|
|
|
|
|
"薄膜衣片" => 30 * 60,
|
|
|
|
|
|
"糖衣片" => 60 * 60,
|
|
|
|
|
|
"胶囊" => 30 * 60,
|
|
|
|
|
|
"普通片" => 15 * 60,
|
|
|
|
|
|
_ => App.CurrentPharmaParams.DisintegrationMaxSeconds > 0
|
|
|
|
|
|
? App.CurrentPharmaParams.DisintegrationMaxSeconds
|
|
|
|
|
|
: 15 * 60
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
int seconds = ResolveDisintegrationLimitSeconds();
|
|
|
|
|
|
if (seconds > 0)
|
|
|
|
|
|
DisintegrationTimeMin = seconds / 60.0;
|
|
|
|
|
|
await WriteDisintegrationTimeAsync(DisintegrationTimeMin);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_isLoadingDisintegrationTime = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task WriteDisintegrationTimeAsync(double value)
|
|
|
|
|
|
{
|
2026-05-19 16:55:00 +08:00
|
|
|
|
if (_plcConfig.DisintegrationTime == 0 || value <= 0 || !double.IsFinite(value))
|
2026-05-16 16:58:57 +08:00
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-19 16:55:00 +08:00
|
|
|
|
await _plc.WriteRegisterAsync(_plcConfig.DisintegrationTime, ToPlcTimeRegisterValue(value));
|
2026-05-16 16:58:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 16:55:00 +08:00
|
|
|
|
private static ushort ToPlcTimeRegisterValue(double value)
|
|
|
|
|
|
{
|
|
|
|
|
|
return (ushort)Math.Clamp(
|
|
|
|
|
|
(int)Math.Round(value, MidpointRounding.AwayFromZero),
|
|
|
|
|
|
0,
|
|
|
|
|
|
ushort.MaxValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private async Task RunHardnessAsync()
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
if (Phase != TestPhase.Idle) return;
|
|
|
|
|
|
CurrentTest = TestType.Hardness;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
HardnessPass = false;
|
|
|
|
|
|
_hardnessResults.Clear();
|
|
|
|
|
|
HardnessCurrentCount = 0;
|
|
|
|
|
|
HardnessAvg = 0;
|
|
|
|
|
|
HardnessRSD = 0;
|
|
|
|
|
|
HardnessMax = 0;
|
|
|
|
|
|
HardnessMin = 0;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
int count = Math.Max(1, HardnessTestCount);
|
2026-05-19 19:01:28 +08:00
|
|
|
|
ushort startCoil = ResolveHardnessStartCoil();
|
2026-05-18 16:53:29 +08:00
|
|
|
|
ushort completeCoil = ResolveHardnessCompleteCoil();
|
2026-05-19 19:01:28 +08:00
|
|
|
|
if (startCoil == 0)
|
|
|
|
|
|
throw new InvalidOperationException("硬度启动线圈未配置");
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (completeCoil == 0)
|
|
|
|
|
|
throw new InvalidOperationException("硬度完成线圈未配置");
|
|
|
|
|
|
if (_plcConfig.HardnessMax == 0)
|
|
|
|
|
|
throw new InvalidOperationException("硬度最大力寄存器未配置");
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)HardnessSudu);
|
|
|
|
|
|
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)HardnessWeiyi);
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-19 19:01:28 +08:00
|
|
|
|
bool completeWasActiveBeforeStart = await _plc.ReadCoilAsync(completeCoil);
|
|
|
|
|
|
await PulseCoilAsync(startCoil);
|
2026-05-18 09:55:00 +08:00
|
|
|
|
|
2026-05-19 19:01:28 +08:00
|
|
|
|
if (completeWasActiveBeforeStart)
|
|
|
|
|
|
await WaitForCoilStateAsync(completeCoil, false, TimeSpan.FromSeconds(10), "硬度完成信号未复位");
|
2026-05-18 09:55:00 +08:00
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
while (Phase == TestPhase.Running && _hardnessResults.Count < count)
|
|
|
|
|
|
{
|
|
|
|
|
|
await WaitForCoilStateAsync(completeCoil, true, TimeSpan.FromSeconds(120), "等待硬度完成信号超时");
|
|
|
|
|
|
double value = await ReadHardnessResultAsync();
|
|
|
|
|
|
_hardnessResults.Add(value);
|
|
|
|
|
|
ApplyHardnessStatistics(count);
|
|
|
|
|
|
await WaitForCoilStateAsync(completeCoil, false, TimeSpan.FromSeconds(10), "硬度完成信号未回落");
|
|
|
|
|
|
}
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (_hardnessResults.Count < count)
|
|
|
|
|
|
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
|
|
|
|
|
|
|
|
|
|
|
ApplyHardnessStatistics(count);
|
|
|
|
|
|
Phase = TestPhase.Completed;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
|
|
MessageBox.Show($"硬度测试出错: {ex.Message}"));
|
|
|
|
|
|
Phase = TestPhase.Error;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
bool resultReady = Phase == TestPhase.Completed;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
Phase = TestPhase.Idle;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (resultReady)
|
|
|
|
|
|
await SaveBatchResult();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private ushort ResolveHardnessCompleteCoil()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _plcConfig.HardnessOver != 0
|
|
|
|
|
|
? _plcConfig.HardnessOver
|
|
|
|
|
|
: _plcConfig.HardnessCompleteCoil;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 19:01:28 +08:00
|
|
|
|
private ushort ResolveHardnessStartCoil()
|
|
|
|
|
|
{
|
|
|
|
|
|
return _plcConfig.HardnessStartCoil != 0
|
|
|
|
|
|
? _plcConfig.HardnessStartCoil
|
|
|
|
|
|
: (ushort)70;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
private async Task<double> ReadHardnessResultAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
double value = await _plc.ReadFloatAsync(_plcConfig.HardnessMax);
|
|
|
|
|
|
if (!double.IsFinite(value) || value <= 0)
|
|
|
|
|
|
throw new InvalidOperationException("硬度最大力数据异常");
|
|
|
|
|
|
|
|
|
|
|
|
HardnessValue = value;
|
|
|
|
|
|
HardnessShishilizhi = value;
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task WaitForCoilStateAsync(ushort coilAddress, bool expectedState, TimeSpan timeout, string timeoutMessage)
|
|
|
|
|
|
{
|
|
|
|
|
|
DateTime deadline = DateTime.Now.Add(timeout);
|
|
|
|
|
|
while (Phase == TestPhase.Running && DateTime.Now <= deadline)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (await _plc.ReadCoilAsync(coilAddress) == expectedState)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Delay(100);
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
2026-05-18 16:53:29 +08:00
|
|
|
|
|
|
|
|
|
|
if (Phase != TestPhase.Running)
|
|
|
|
|
|
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
|
|
|
|
|
|
|
|
|
|
|
throw new TimeoutException(timeoutMessage);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ApplyHardnessStatistics(int requiredCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
var stats = TestCalculationService.CalculateHardness(
|
|
|
|
|
|
_hardnessResults,
|
|
|
|
|
|
HardnessInternalMin,
|
|
|
|
|
|
HardnessInternalMax,
|
|
|
|
|
|
requiredCount);
|
|
|
|
|
|
|
|
|
|
|
|
HardnessAvg = stats.Average;
|
|
|
|
|
|
HardnessRSD = stats.RsdPercent;
|
|
|
|
|
|
HardnessMax = stats.Maximum;
|
|
|
|
|
|
HardnessMin = stats.Minimum;
|
|
|
|
|
|
HardnessCurrentCount = stats.Count;
|
|
|
|
|
|
HardnessPass = stats.IsPass;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
/// 脆碎度测试主逻辑(实时状态显示)
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private async Task RunFriabilityAsync()
|
|
|
|
|
|
{
|
2026-05-15 17:47:11 +08:00
|
|
|
|
// 1. 防并发:如果设备不是空闲状态,直接退出
|
|
|
|
|
|
if (Phase != TestPhase.Idle)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 标记当前正在运行的是脆碎度测试
|
2026-05-05 15:31:24 +08:00
|
|
|
|
CurrentTest = TestType.Friability;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
FriabilityPass = false;
|
2026-05-16 17:47:46 +08:00
|
|
|
|
bool resultReady = false;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
ushort startCoil = _plcConfig.FriabilityStartCoil;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
|
|
|
|
|
if (startCoil == 0)
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
throw new InvalidOperationException("未配置脆碎度启动线圈地址");
|
2026-05-15 17:47:11 +08:00
|
|
|
|
}
|
2026-05-18 18:54:32 +08:00
|
|
|
|
if (!double.IsFinite(FriabilityTargetTimeMin) || FriabilityTargetTimeMin <= 0)
|
|
|
|
|
|
throw new InvalidOperationException("脆碎试验时间必须大于0");
|
|
|
|
|
|
|
2026-05-19 18:44:56 +08:00
|
|
|
|
UpdateFriabilityTimingFromTime();
|
|
|
|
|
|
await WriteFriabilityRoundsAsync(FriabilityTargetRounds);
|
2026-05-18 18:54:32 +08:00
|
|
|
|
|
|
|
|
|
|
double weightBefore = await ReadFriabilityWeightAsync(ResolveFriabilityWeightBeforeRegister(), "脆碎前重量");
|
2026-05-18 14:06:04 +08:00
|
|
|
|
SetFriabilityWeightFromPlc(weightBefore: weightBefore);
|
2026-05-16 17:47:46 +08:00
|
|
|
|
if (WeightBefore <= 0)
|
|
|
|
|
|
throw new InvalidOperationException("脆碎前重量必须大于0");
|
|
|
|
|
|
|
2026-05-18 14:06:04 +08:00
|
|
|
|
double rpm = FriabilityTargetRpm > 0 ? FriabilityTargetRpm : 25;
|
2026-05-18 18:54:32 +08:00
|
|
|
|
double testTimeMin = FriabilityTargetTimeMin > 0 ? FriabilityTargetTimeMin : 4;
|
|
|
|
|
|
int totalRounds = Math.Max(1, FriabilityTargetRounds);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
FriabilityRemainingRounds = totalRounds;
|
|
|
|
|
|
FriabilityCurrentRpm = rpm;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
await PulseCoilAsync(startCoil);
|
2026-05-18 18:54:32 +08:00
|
|
|
|
int durationMs = (int)Math.Ceiling(testTimeMin * 60 * 1000); // 总运行时间(毫秒)
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-15 17:47:11 +08:00
|
|
|
|
for (int i = 0; i < durationMs; i += 100)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 如果用户点了停止,状态会被设为Idle,直接跳出循环
|
|
|
|
|
|
if (Phase != TestPhase.Running)
|
|
|
|
|
|
break;
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
2026-05-15 17:47:11 +08:00
|
|
|
|
// 计算当前剩余圈数
|
|
|
|
|
|
double elapsedMs = i;
|
|
|
|
|
|
double elapsedRounds = rpm / 60 / 1000 * elapsedMs;
|
|
|
|
|
|
int remainingRounds = (int)Math.Ceiling(totalRounds - elapsedRounds);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新界面绑定的剩余圈数
|
|
|
|
|
|
FriabilityRemainingRounds = remainingRounds;
|
|
|
|
|
|
|
|
|
|
|
|
// 等待100ms,再更新下一次
|
|
|
|
|
|
await Task.Delay(100);
|
|
|
|
|
|
}
|
2026-05-16 17:47:46 +08:00
|
|
|
|
if (Phase != TestPhase.Running)
|
|
|
|
|
|
throw new InvalidOperationException("脆碎度测试已停止,未保存结果");
|
|
|
|
|
|
|
2026-05-18 18:54:32 +08:00
|
|
|
|
double weightAfter = await ReadFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), "脆碎后重量");
|
2026-05-18 14:06:04 +08:00
|
|
|
|
SetFriabilityWeightFromPlc(weightAfter: weightAfter);
|
|
|
|
|
|
FriabilityCurrentRpm = rpm;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
LossPercent = TestCalculationService.CalculateFriabilityLossPercent(WeightBefore, WeightAfter);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
FriabilityPass = LossPercent <= FriabilityMaxLossPercent; //标准值
|
2026-05-16 17:47:46 +08:00
|
|
|
|
resultReady = true;
|
2026-05-15 17:47:11 +08:00
|
|
|
|
// 标记测试为已完成
|
2026-05-06 16:41:32 +08:00
|
|
|
|
Phase = TestPhase.Completed;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-05-15 17:47:11 +08:00
|
|
|
|
|
|
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
|
|
MessageBox.Show($"脆碎度测试出错: {ex.Message}"));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
Phase = TestPhase.Error;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (_plcConfig.FriabilityStartCoil != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
try { await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, false); }
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
Phase = TestPhase.Idle;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
FriabilityRemainingRounds = FriabilityTargetRounds;
|
2026-05-16 17:47:46 +08:00
|
|
|
|
if (resultReady)
|
|
|
|
|
|
await SaveBatchResult();
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 17:47:46 +08:00
|
|
|
|
private async Task<double> 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-18 16:53:29 +08:00
|
|
|
|
int tubeCount = Math.Max(1, _plcConfig.DisintegrationCompleteCoils.Length);
|
|
|
|
|
|
TubesCompleted = new bool[tubeCount];
|
|
|
|
|
|
RemainingTubes = tubeCount;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DisintegrationSeconds = 0;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
bool resultReady = false;
|
|
|
|
|
|
DateTime startedAt = DateTime.Now;
|
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)
|
2026-05-18 16:53:29 +08:00
|
|
|
|
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
};
|
2026-05-05 15:31:24 +08:00
|
|
|
|
_disintegrationTimer.Start();
|
|
|
|
|
|
|
2026-05-06 16:41:32 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
await WriteDisintegrationSpeedAsync(DisintegrationSpeedRpm);
|
|
|
|
|
|
await WriteDisintegrationTimeAsync(DisintegrationTimeMin);
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.DisintegrationStartCoil);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
int maxSec = ResolveDisintegrationLimitSeconds();
|
|
|
|
|
|
DisintegrationTimeMin = maxSec / 60.0;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
await Task.Delay(500);
|
|
|
|
|
|
}
|
2026-05-18 16:53:29 +08:00
|
|
|
|
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_disintegrationTimer.Stop();
|
|
|
|
|
|
Phase = TestPhase.Completed;
|
2026-05-18 16:53:29 +08:00
|
|
|
|
resultReady = true;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
_disintegrationTimer.Stop();
|
|
|
|
|
|
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}"));
|
|
|
|
|
|
Phase = TestPhase.Error;
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
Phase = TestPhase.Idle;
|
2026-05-18 14:06:04 +08:00
|
|
|
|
int maxSec = ResolveDisintegrationLimitSeconds();
|
2026-05-18 09:47:22 +08:00
|
|
|
|
DisintegrationPass = !_discardDisintegrationResult && RemainingTubes == 0 && DisintegrationSeconds <= maxSec;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (resultReady && !_discardDisintegrationResult)
|
2026-05-18 09:47:22 +08:00
|
|
|
|
await SaveBatchResult();
|
|
|
|
|
|
|
|
|
|
|
|
_discardDisintegrationResult = false;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task StopDisintegrationAsync()
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
_discardDisintegrationResult = CurrentTest == TestType.Disintegration && Phase == TestPhase.Running;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.DisintegrationStopCoil);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
Phase = TestPhase.Idle;
|
|
|
|
|
|
_disintegrationTimer?.Stop();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:47:22 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task StartDissolution1Async()
|
2026-05-05 15:31:24 +08:00
|
|
|
|
{
|
|
|
|
|
|
CurrentTest = TestType.Dissolution;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
DissolutionPass = false;
|
|
|
|
|
|
ResetDissolutionChannel(1);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
ResetDissolutionSampleState(1);
|
2026-05-18 14:06:04 +08:00
|
|
|
|
CreateDissolutionSampleSchedule(1);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
_dissolution1StartTime = DateTime.Now;
|
|
|
|
|
|
_isDissolution1Running = true;
|
|
|
|
|
|
DissolutionPlotModel.Title = "溶出曲线";
|
|
|
|
|
|
await WriteDissolutionTimeAsync(_plcConfig.Dissolution1Time, Dissolution1TimeMin);
|
2026-05-18 09:47:22 +08:00
|
|
|
|
await WriteDissolutionFloatAsync(_plcConfig.Dissolution1SampleInterval, Dissolution1SampleIntervalMin);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution1StartCoil);
|
|
|
|
|
|
}
|
2026-05-05 15:31:24 +08:00
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task StopDissolution1Async()
|
|
|
|
|
|
{
|
2026-05-06 16:41:32 +08:00
|
|
|
|
try
|
2026-05-05 15:31:24 +08:00
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution1StopCoil);
|
|
|
|
|
|
await FinalizeDissolutionChannelAsync(1);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
2026-05-16 16:58:57 +08:00
|
|
|
|
finally
|
2026-05-06 16:41:32 +08:00
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
_isDissolution1Running = false;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
ResetDissolutionSampleState(1);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
|
|
|
|
|
|
UpdateDissolutionClock();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:47:22 +08:00
|
|
|
|
private async Task ResetDissolution1Async()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution1ResetCoil);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_isDissolution1Running = false;
|
|
|
|
|
|
ResetDissolutionChannel(1);
|
|
|
|
|
|
ResetDissolutionSampleState(1);
|
|
|
|
|
|
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
|
|
|
|
|
|
UpdateDissolutionClock();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task StartDissolution2Async()
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentTest = TestType.Dissolution;
|
|
|
|
|
|
Phase = TestPhase.Running;
|
|
|
|
|
|
DissolutionPass = false;
|
|
|
|
|
|
ResetDissolutionChannel(2);
|
2026-05-16 18:16:25 +08:00
|
|
|
|
ResetDissolutionSampleState(2);
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (_plcConfig.Dissolution2Percent != 0)
|
|
|
|
|
|
CreateDissolutionSampleSchedule(2);
|
|
|
|
|
|
else
|
|
|
|
|
|
DissolutionCurveStatus = "溶出2溶出度寄存器未配置,仅执行设备控制,不保存计算结果";
|
2026-05-16 16:58:57 +08:00
|
|
|
|
_dissolution2StartTime = DateTime.Now;
|
|
|
|
|
|
_isDissolution2Running = true;
|
|
|
|
|
|
DissolutionPlotModel.Title = "溶出曲线";
|
|
|
|
|
|
await WriteDissolutionTimeAsync(_plcConfig.Dissolution2Time, Dissolution2TimeMin);
|
2026-05-18 09:47:22 +08:00
|
|
|
|
await WriteDissolutionFloatAsync(_plcConfig.Dissolution2SampleInterval, Dissolution2SampleIntervalMin);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution2StartCoil);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task StopDissolution2Async()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution2StopCoil);
|
|
|
|
|
|
await FinalizeDissolutionChannelAsync(2);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
_isDissolution2Running = false;
|
2026-05-16 18:16:25 +08:00
|
|
|
|
ResetDissolutionSampleState(2);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
|
|
|
|
|
|
UpdateDissolutionClock();
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 09:47:22 +08:00
|
|
|
|
private async Task ResetDissolution2Async()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await PulseCoilAsync(_plcConfig.Dissolution2ResetCoil);
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_isDissolution2Running = false;
|
|
|
|
|
|
ResetDissolutionChannel(2);
|
|
|
|
|
|
ResetDissolutionSampleState(2);
|
|
|
|
|
|
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
|
|
|
|
|
|
UpdateDissolutionClock();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private void ResetDissolutionChannel(int channel)
|
|
|
|
|
|
{
|
2026-05-18 14:06:04 +08:00
|
|
|
|
RemoveDissolutionSamples(channel);
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 18:16:25 +08:00
|
|
|
|
private void ResetDissolutionSampleState(int channel)
|
|
|
|
|
|
{
|
|
|
|
|
|
SetDissolutionSampleRequestActive(channel, false);
|
|
|
|
|
|
SetDissolutionSamplePromptOpen(channel, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 16:58:57 +08:00
|
|
|
|
private async Task FinalizeDissolutionChannelAsync(int channel)
|
|
|
|
|
|
{
|
2026-05-18 17:18:56 +08:00
|
|
|
|
RefreshDissolutionSeries(channel);
|
2026-05-16 16:58:57 +08:00
|
|
|
|
var times = channel == 1 ? _dissolution1Times : _dissolution2Times;
|
|
|
|
|
|
var values = channel == 1 ? _dissolution1Values : _dissolution2Values;
|
|
|
|
|
|
if (values.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
DissolutionCurveStatus = $"溶出{channel}无有效曲线数据,未保存结果";
|
|
|
|
|
|
LocalAlarm = DissolutionCurveStatus;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_dissolutionResultChannel = $"溶出{channel}";
|
2026-05-18 16:53:29 +08:00
|
|
|
|
if (!TestCalculationService.TryGetDissolutionRateAt30Min(times, values, out double rate30Min))
|
|
|
|
|
|
{
|
|
|
|
|
|
DissolutionCurveStatus = $"溶出{channel}缺少有效30min溶出度,未保存结果";
|
|
|
|
|
|
LocalAlarm = DissolutionCurveStatus;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_dissolutionResultRate30Min = rate30Min;
|
2026-05-16 16:58:57 +08:00
|
|
|
|
_dissolutionResultRSquared = CalculateRSquared(times, values);
|
|
|
|
|
|
DissolutionPercent = _dissolutionResultRate30Min;
|
|
|
|
|
|
DissolutionRSquared = _dissolutionResultRSquared;
|
|
|
|
|
|
if (channel == 1)
|
|
|
|
|
|
Dissolution1RSquared = _dissolutionResultRSquared;
|
|
|
|
|
|
else
|
|
|
|
|
|
Dissolution2RSquared = _dissolutionResultRSquared;
|
|
|
|
|
|
|
|
|
|
|
|
DissolutionPass = _dissolutionResultRate30Min >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
|
|
|
|
|
|
await SaveBatchResult();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task RunDissolutionAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
await StartDissolution1Async();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private async Task SaveBatchResult(bool? forcedQualified = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
double dissolutionRate30Min = CurrentTest == TestType.Dissolution
|
|
|
|
|
|
? _dissolutionResultRate30Min
|
|
|
|
|
|
: DissolutionPercent;
|
|
|
|
|
|
double rsquared = CurrentTest == TestType.Dissolution
|
|
|
|
|
|
? _dissolutionResultRSquared
|
|
|
|
|
|
: DissolutionRSquared;
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
var batch = new TestBatch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestTime = DateTime.Now,
|
|
|
|
|
|
StationId = StationId,
|
2026-05-16 16:58:57 +08:00
|
|
|
|
SampleName = "样品",
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 硬度
|
2026-05-05 15:31:24 +08:00
|
|
|
|
HardnessAvg = HardnessAvg,
|
|
|
|
|
|
HardnessRSD = HardnessRSD,
|
2026-05-18 16:53:29 +08:00
|
|
|
|
HardnessMax = HardnessMax,
|
|
|
|
|
|
HardnessMin = HardnessMin,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
HardnessTestCount = HardnessTestCount,
|
2026-05-18 14:06:04 +08:00
|
|
|
|
HardnessInternalMin = HardnessInternalMin,
|
|
|
|
|
|
HardnessInternalMax = HardnessInternalMax,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 脆碎度
|
2026-05-05 15:31:24 +08:00
|
|
|
|
FriabilityLoss = LossPercent,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
FriabilityTargetRpm = FriabilityTargetRpm,
|
|
|
|
|
|
FriabilityTargetTimeSec = FriabilityTargetTimeSec,
|
2026-05-18 14:06:04 +08:00
|
|
|
|
FriabilityTargetRounds = FriabilityTargetRounds,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
FriabilityClockwise = FriabilityClockwise,
|
|
|
|
|
|
FriabilityRemainingRounds = FriabilityRemainingRounds,
|
|
|
|
|
|
WeightBefore = WeightBefore,
|
|
|
|
|
|
WeightAfter = WeightAfter,
|
|
|
|
|
|
|
|
|
|
|
|
// 崩解
|
2026-05-05 15:31:24 +08:00
|
|
|
|
DisintegrationTimeSec = DisintegrationSeconds,
|
|
|
|
|
|
RemainingTubesAtEnd = RemainingTubes,
|
2026-05-18 15:18:28 +08:00
|
|
|
|
DisintegrationTargetFreq = DisintegrationSpeedRpm,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DisintegrationTemp = DisintegrationTemp,
|
2026-05-18 14:06:04 +08:00
|
|
|
|
DisintegrationDosageForm = DisintegrationDosageForm,
|
|
|
|
|
|
DisintegrationLimitSeconds = ResolveDisintegrationLimitSeconds(),
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
// 溶出
|
2026-05-16 16:58:57 +08:00
|
|
|
|
DissolutionChannel = CurrentTest == TestType.Dissolution ? _dissolutionResultChannel : "",
|
|
|
|
|
|
DissolutionRate30Min = dissolutionRate30Min,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DissolutionTargetRpm = DissolutionTargetRpm,
|
|
|
|
|
|
DissolutionRSquared = rsquared,
|
2026-05-18 09:47:22 +08:00
|
|
|
|
DissolutionSampleInterval = CurrentTest == TestType.Dissolution && _dissolutionResultChannel == "溶出2"
|
|
|
|
|
|
? ToCompatibleSampleInterval(Dissolution2SampleIntervalMin)
|
|
|
|
|
|
: ToCompatibleSampleInterval(Dissolution1SampleIntervalMin),
|
|
|
|
|
|
Dissolution1SampleInterval = Dissolution1SampleIntervalMin,
|
|
|
|
|
|
Dissolution2SampleInterval = Dissolution2SampleIntervalMin,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
DissolutionUpDownFreq = DissolutionUpDownFreq,
|
|
|
|
|
|
HardnessPass = HardnessPass,
|
|
|
|
|
|
FriabilityPass = FriabilityPass,
|
|
|
|
|
|
DisintegrationPass = DisintegrationPass,
|
|
|
|
|
|
DissolutionPass = DissolutionPass,
|
|
|
|
|
|
TestType = CurrentTest switch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestType.Hardness => "硬度",
|
|
|
|
|
|
TestType.Friability => "脆碎度",
|
|
|
|
|
|
TestType.Disintegration => "崩解",
|
|
|
|
|
|
TestType.Dissolution => "溶出",
|
|
|
|
|
|
_ => ""
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-18 16:53:29 +08:00
|
|
|
|
IsQualified = TestCalculationService.ResolveCurrentTestQualified(
|
|
|
|
|
|
CurrentTest,
|
|
|
|
|
|
HardnessPass,
|
|
|
|
|
|
FriabilityPass,
|
|
|
|
|
|
DisintegrationPass,
|
|
|
|
|
|
DissolutionPass)
|
2026-05-05 15:31:24 +08:00
|
|
|
|
};
|
2026-05-18 14:06:04 +08:00
|
|
|
|
var dissolutionSamples = CurrentTest == TestType.Dissolution
|
|
|
|
|
|
? DissolutionSamplePoints
|
|
|
|
|
|
.Where(s => s.ChannelName == _dissolutionResultChannel && s.Percent.HasValue)
|
|
|
|
|
|
.ToList()
|
|
|
|
|
|
: new List<DissolutionSamplePoint>();
|
|
|
|
|
|
|
|
|
|
|
|
await Task.Run(() => _db.InsertBatch(batch, dissolutionSamples));
|
2026-05-06 16:41:32 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
string projectName = CurrentTest switch
|
|
|
|
|
|
{
|
|
|
|
|
|
TestType.Hardness => "硬度",
|
|
|
|
|
|
TestType.Friability => "脆碎度",
|
|
|
|
|
|
TestType.Disintegration => "崩解",
|
2026-05-16 16:58:57 +08:00
|
|
|
|
TestType.Dissolution => string.IsNullOrWhiteSpace(_dissolutionResultChannel) ? "溶出" : _dissolutionResultChannel,
|
2026-05-06 16:41:32 +08:00
|
|
|
|
_ => ""
|
|
|
|
|
|
};
|
2026-05-18 14:06:04 +08:00
|
|
|
|
LocalAlarm = $"{projectName}测试完成";
|
2026-05-06 16:41:32 +08:00
|
|
|
|
});
|
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)
|
|
|
|
|
|
{
|
2026-05-18 16:53:29 +08:00
|
|
|
|
return TestCalculationService.CalculateRSquared(timeMinutes, concentration);
|
2026-05-06 16:41:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
private async Task ExportHistoryAsync()
|
|
|
|
|
|
{
|
2026-05-16 16:58:57 +08:00
|
|
|
|
var batches = await Task.Run(() => _db.GetBatches(null, 100));
|
|
|
|
|
|
string path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"检测记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
2026-05-05 15:31:24 +08:00
|
|
|
|
_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));
|
2026-05-18 16:53:29 +08:00
|
|
|
|
return values.Count < 2 ? 0 : Math.Sqrt(sum / (values.Count - 1));
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
2026-05-15 15:48:43 +08:00
|
|
|
|
|
2026-05-05 15:31:24 +08:00
|
|
|
|
}
|
2026-05-16 16:58:57 +08:00
|
|
|
|
}
|