Files
CSI-Z420-Tablet-Multi-Funct…/ViewModels/StationViewModel.cs
GukSang.Jin 8ed011f91e 更新2025
2026-05-19 18:44:56 +08:00

1898 lines
75 KiB
C#
Raw Blame History

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