Files
CSI-Z420-Tablet-Multi-Funct…/ViewModels/StationViewModel.cs

1373 lines
54 KiB
C#
Raw Normal View History

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;
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;
using TabletTester2025.Models;
using TabletTester2025.Services;
namespace TabletTester2025.ViewModels
{
public partial class StationViewModel : ObservableObject
{
private readonly IPlcService _plc;
private readonly PlcConfiguration _plcConfig;
private readonly DatabaseService _db;
private readonly ExcelExportService _excel;
private readonly AlarmService _alarm;
private DispatcherTimer _disintegrationTimer;
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;
private readonly List<double> _dissolution1Times = new();
private readonly List<double> _dissolution1Values = new();
private readonly List<double> _dissolution2Times = new();
private readonly List<double> _dissolution2Values = new();
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 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 09:55:00 +08:00
//[ObservableProperty] private int _hardnessMaxN = 72; //最大力采集
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;
[ObservableProperty] private double _hardnessMin;
[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;
[ObservableProperty] private int _friabilityTargetTimeSec = 240;
[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-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-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;
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-15 15:48:43 +08:00
//HardnessUpCommand = new AsyncRelayCommand(async () =>
//{
// await _plc.WriteCoilAsync(0x20, true);
// await Task.Delay(100);
// await _plc.WriteCoilAsync(0x20, false);
//});
2026-05-15 09:59:49 +08:00
2026-05-06 16:41:32 +08:00
HardnessDownCommand = new AsyncRelayCommand(async () =>
{
await _plc.WriteCoilAsync(0x21, true);
await Task.Delay(100);
await _plc.WriteCoilAsync(0x21, false);
});
2026-05-15 15:48:43 +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();
HardnessValue = 0;
HardnessAvg = 0;
HardnessRSD = 0;
HardnessCurrentCount = 0;
HardnessMax = 0;
HardnessMin = 0;
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);
});
// 硬后退按钮命令
HardnessForward = new AsyncRelayCommand(async () =>
{
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-15 17:47:11 +08:00
StopFriabilityCommand = new AsyncRelayCommand(() => {
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-15 17:47:11 +08:00
Phase = TestPhase.Idle; return Task.CompletedTask;
});
2026-05-06 16:41:32 +08:00
ResetFriabilityCommand = new AsyncRelayCommand(() =>
{
2026-05-15 17:47:11 +08:00
FriabilityRemainingRounds = 100; // 剩余圈数重置为默认值通常是100圈
LossPercent = 0; // 失重率清零
WeightBefore = 0; // 脆碎前重量清零
WeightAfter = 0;// 脆碎后重量清零
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
_ = LoadDisintegrationTimeAsync();
_ = LoadDisintegrationSpeedAsync();
_ = LoadDissolutionTimesAsync();
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;
RemainingTubes = 6 - TubesCompleted.Count(c => c);
}
}
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()
{
bool updated = false;
DissolutionRpm = await _plc.ReadFloatAsync(_plcConfig.DissolutionRpm);
if (_plcConfig.DisintegrationTemp != 0)
DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp);
if (_isDissolution1Running)
2026-05-16 18:16:25 +08:00
{
await CheckDissolutionSampleAsync(1);
2026-05-16 16:58:57 +08:00
updated |= await ReadDissolutionChannelAsync(1);
2026-05-16 18:16:25 +08:00
}
2026-05-16 16:58:57 +08:00
if (_isDissolution2Running)
2026-05-16 18:16:25 +08:00
{
await CheckDissolutionSampleAsync(2);
2026-05-16 16:58:57 +08:00
updated |= await ReadDissolutionChannelAsync(2);
2026-05-16 18:16:25 +08:00
}
2026-05-16 16:58:57 +08:00
UpdateDissolutionClock();
if (updated)
DissolutionPlotModel.InvalidatePlot(true);
}
2026-05-16 18:16:25 +08:00
private async Task CheckDissolutionSampleAsync(int channel)
{
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;
SetDissolutionSampleRequestActive(channel, true);
SetDissolutionSamplePromptOpen(channel, true);
try
{
await ShowDissolutionSampleDialogAsync(channel);
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 ShowDissolutionSampleDialogAsync(int channel)
{
bool confirmed = await App.Current.Dispatcher.InvokeAsync(() =>
{
var dialog = new Window
{
Title = $"溶出{channel}取样",
Width = 420,
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 = "请取样分析后,取样结束后点击“确定已取样”继续运行。",
FontSize = 16,
Foreground = new SolidColorBrush(Color.FromRgb(16, 42, 67)),
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 18)
});
var button = new Button
{
Content = "确定已取样",
Width = 128,
Height = 38,
HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
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
};
button.Click += (_, _) =>
{
dialog.DialogResult = true;
dialog.Close();
};
panel.Children.Add(button);
dialog.Content = panel;
return dialog.ShowDialog() == true;
});
if (!confirmed)
throw new InvalidOperationException("取样确认窗口已关闭,未写入确认线圈");
}
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;
double minutes = (DateTime.Now - startTime).TotalMinutes;
if (channel == 1)
{
Dissolution1Percent = value;
DissolutionPercent = value;
AddDissolutionPoint(_dissolution1Times, _dissolution1Values, _dissolution1Series, minutes, value);
Dissolution1RSquared = CalculateRSquared(_dissolution1Times, _dissolution1Values);
DissolutionRSquared = Dissolution1RSquared;
}
else
{
Dissolution2Percent = value;
DissolutionPercent = value;
AddDissolutionPoint(_dissolution2Times, _dissolution2Values, _dissolution2Series, minutes, value);
Dissolution2RSquared = CalculateRSquared(_dissolution2Times, _dissolution2Values);
DissolutionRSquared = Dissolution2RSquared;
}
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 static void AddDissolutionPoint(List<double> times, List<double> values, LineSeries series, double minutes, double value)
{
if (times.Count > 0 && minutes <= times[^1])
return;
times.Add(minutes);
values.Add(value);
series.Points.Add(new DataPoint(minutes, value));
}
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-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);
}
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;
float value = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTime);
if (value > 0)
DisintegrationTimeMin = value;
}
catch { }
finally
{
_isLoadingDisintegrationTime = false;
}
}
private async Task WriteDisintegrationTimeAsync(double value)
{
if (_plcConfig.DisintegrationTime == 0 || value <= 0)
return;
try
{
await _plc.WriteFloatAsync(_plcConfig.DisintegrationTime, (float)value);
}
catch { }
}
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 09:55:00 +08:00
HardnessPass = false;
2026-05-06 16:41:32 +08:00
_hardnessResults.Clear();
2026-05-05 15:31:24 +08:00
try
{
int count = App.CurrentPharmaParams.HardnessTestCount;
double min = App.CurrentPharmaParams.HardnessMin_N;
double max = App.CurrentPharmaParams.HardnessMax_N;
2026-05-15 17:47:11 +08:00
2026-05-18 09:55:00 +08:00
double currentSpeed = _hardnessSudu;
double currentWeiyi = _hardnessWeiyi;
2026-05-18 10:48:01 +08:00
2026-05-18 09:55:00 +08:00
// 如果你需要把这3个值发给PLC就在这里发一次测试开始用当前参数
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)currentSpeed);
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)currentWeiyi);
2026-05-18 10:48:01 +08:00
2026-05-18 09:55:00 +08:00
// 开始循环测试
for (int i = 0; i < count; i++)
{
2026-05-16 16:58:57 +08:00
await _plc.WriteCoilAsync(_plcConfig.HardnessStartCoil, true);
2026-05-18 09:55:00 +08:00
2026-05-05 15:31:24 +08:00
bool completed = false;
while (!completed && Phase == TestPhase.Running)
{
2026-05-18 09:55:00 +08:00
await Task.Delay(200);
2026-05-05 15:31:24 +08:00
completed = await _plc.ReadCoilAsync(_plcConfig.HardnessCompleteCoil);
}
2026-05-15 17:47:11 +08:00
2026-05-18 10:11:12 +08:00
double val = await _plc.ReadFloatAsync(_plcConfig.HardnessPoSun);
2026-05-18 09:55:00 +08:00
_hardnessResults.Add(val);
2026-05-18 10:48:01 +08:00
//HardnessPoSun = val;
2026-05-15 17:47:11 +08:00
2026-05-18 09:55:00 +08:00
await Task.Delay(1000);
2026-05-05 15:31:24 +08:00
}
2026-05-15 15:48:43 +08:00
2026-05-18 09:55:00 +08:00
// 计算结果
2026-05-05 15:31:24 +08:00
HardnessAvg = _hardnessResults.Average();
2026-05-18 09:55:00 +08:00
HardnessMax = _hardnessResults.Max();
2026-05-15 15:48:43 +08:00
HardnessMin = _hardnessResults.Min();
2026-05-15 17:47:11 +08:00
HardnessCurrentCount = count;
2026-05-05 15:31:24 +08:00
HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100;
HardnessPass = HardnessAvg >= min && HardnessAvg <= max;
2026-05-18 09:55:00 +08:00
Phase = TestPhase.Completed;
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
Phase = TestPhase.Error;
2026-05-06 16:41:32 +08:00
}
finally
{
Phase = TestPhase.Idle;
await SaveBatchResult();
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-16 17:47:46 +08:00
WeightBefore = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量");
if (WeightBefore <= 0)
throw new InvalidOperationException("脆碎前重量必须大于0");
2026-05-15 17:47:11 +08:00
await _plc.WriteCoilAsync(startCoil, true);
int totalRounds = 100; // 药典标准脆碎度总圈数100圈
double rpm = FriabilityTargetRpm; // 界面设置的目标转速r/min
int durationMs = (int)((totalRounds / rpm) * 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("脆碎度测试已停止,未保存结果");
WeightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量");
2026-05-15 17:47:11 +08:00
FriabilityCurrentRpm = FriabilityTargetRpm;
LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率
FriabilityPass = LossPercent <= App.CurrentPharmaParams.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
{
Phase = TestPhase.Idle;
2026-05-15 17:47:11 +08:00
FriabilityRemainingRounds = 100;
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-05 15:31:24 +08:00
TubesCompleted = new bool[6];
RemainingTubes = 6;
2026-05-06 16:41:32 +08:00
DisintegrationSeconds = 0;
2026-05-05 15:31:24 +08:00
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
2026-05-06 16:41:32 +08:00
_disintegrationTimer.Tick += (s, e) =>
{
if (Phase == TestPhase.Running)
DisintegrationSeconds++;
};
2026-05-05 15:31:24 +08:00
_disintegrationTimer.Start();
2026-05-06 16:41:32 +08:00
try
{
2026-05-16 16:58:57 +08:00
await WriteDisintegrationSpeedAsync(DisintegrationSpeedRpm);
await WriteDisintegrationTimeAsync(DisintegrationTimeMin);
await PulseCoilAsync(_plcConfig.DisintegrationStartCoil);
int maxSec = DisintegrationTimeMin > 0
? (int)Math.Ceiling(DisintegrationTimeMin * 60)
: App.CurrentPharmaParams.DisintegrationMaxSeconds;
2026-05-06 16:41:32 +08:00
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
{
await Task.Delay(500);
}
_disintegrationTimer.Stop();
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
_disintegrationTimer.Stop();
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
2026-05-16 16:58:57 +08:00
int maxSec = DisintegrationTimeMin > 0
? (int)Math.Ceiling(DisintegrationTimeMin * 60)
: App.CurrentPharmaParams.DisintegrationMaxSeconds;
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 09:47:22 +08:00
if (!_discardDisintegrationResult)
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()
{
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-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()
{
if (_plcConfig.Dissolution2Percent == 0)
{
LocalAlarm = "溶出2溶出度寄存器未配置";
DissolutionCurveStatus = LocalAlarm;
return;
}
CurrentTest = TestType.Dissolution;
Phase = TestPhase.Running;
DissolutionPass = false;
ResetDissolutionChannel(2);
2026-05-16 18:16:25 +08:00
ResetDissolutionSampleState(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)
{
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)
{
var times = channel == 1 ? _dissolution1Times : _dissolution2Times;
var values = channel == 1 ? _dissolution1Values : _dissolution2Values;
if (values.Count == 0)
{
DissolutionCurveStatus = $"溶出{channel}无有效曲线数据,未保存结果";
LocalAlarm = DissolutionCurveStatus;
return;
}
_dissolutionResultChannel = $"溶出{channel}";
_dissolutionResultRate30Min = GetDissolutionRateAt30Min(times, values);
_dissolutionResultRSquared = CalculateRSquared(times, values);
DissolutionPercent = _dissolutionResultRate30Min;
DissolutionRSquared = _dissolutionResultRSquared;
if (channel == 1)
Dissolution1RSquared = _dissolutionResultRSquared;
else
Dissolution2RSquared = _dissolutionResultRSquared;
DissolutionPass = _dissolutionResultRate30Min >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
await SaveBatchResult();
}
private static double GetDissolutionRateAt30Min(List<double> times, List<double> values)
{
if (values.Count == 0)
return 0;
int index = 0;
double nearestDistance = double.MaxValue;
for (int i = 0; i < times.Count; i++)
{
double distance = Math.Abs(times[i] - 30);
if (distance < nearestDistance)
{
nearestDistance = distance;
index = i;
}
}
return values[index];
}
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-06 16:41:32 +08:00
HardnessMax = HardnessMax,
HardnessMin = HardnessMin,
HardnessTestCount = HardnessTestCount,
// 脆碎度
2026-05-05 15:31:24 +08:00
FriabilityLoss = LossPercent,
2026-05-06 16:41:32 +08:00
FriabilityTargetRpm = FriabilityTargetRpm,
FriabilityTargetTimeSec = FriabilityTargetTimeSec,
FriabilityClockwise = FriabilityClockwise,
FriabilityRemainingRounds = FriabilityRemainingRounds,
WeightBefore = WeightBefore,
WeightAfter = WeightAfter,
// 崩解
2026-05-05 15:31:24 +08:00
DisintegrationTimeSec = DisintegrationSeconds,
RemainingTubesAtEnd = RemainingTubes,
2026-05-16 16:58:57 +08:00
DisintegrationTargetFreq = 0,
2026-05-06 16:41:32 +08:00
DisintegrationTemp = DisintegrationTemp,
// 溶出
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-15 15:48:43 +08:00
IsQualified = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass
2026-05-05 15:31:24 +08:00
};
await Task.Run(() => _db.InsertBatch(batch));
2026-05-06 16:41:32 +08:00
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 获取当前测试项目是否合格
bool currentPass = CurrentTest switch
{
TestType.Hardness => HardnessPass,
TestType.Friability => FriabilityPass,
TestType.Disintegration => DisintegrationPass,
TestType.Dissolution => DissolutionPass,
_ => false
};
string projectName = CurrentTest switch
{
TestType.Hardness => "硬度",
TestType.Friability => "脆碎度",
TestType.Disintegration => "崩解",
2026-05-16 16:58:57 +08:00
TestType.Dissolution => string.IsNullOrWhiteSpace(_dissolutionResultChannel) ? "溶出" : _dissolutionResultChannel,
2026-05-06 16:41:32 +08:00
_ => ""
};
LocalAlarm = currentPass ? $"{projectName}测试合格" : $"{projectName}测试不合格";
});
2026-05-05 15:31:24 +08:00
}
catch (Exception ex)
{
2026-05-06 16:41:32 +08:00
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}"));
2026-05-05 15:31:24 +08:00
}
}
2026-05-06 16:41:32 +08:00
private double CalculateRSquared(List<double> timeMinutes, List<double> concentration)
{
2026-05-16 16:58:57 +08:00
if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count) return 0;
2026-05-06 16:41:32 +08:00
int n = timeMinutes.Count;
double sumX = timeMinutes.Sum();
double sumY = concentration.Sum();
double sumXY = timeMinutes.Zip(concentration, (x, y) => x * y).Sum();
double sumX2 = timeMinutes.Select(x => x * x).Sum();
double sumY2 = concentration.Select(y => y * y).Sum();
double numerator = (n * sumXY - sumX * sumY);
double denominator = Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
2026-05-16 16:58:57 +08:00
if (denominator <= 0 || double.IsNaN(denominator))
return 0;
2026-05-06 16:41:32 +08:00
double r = numerator / denominator;
2026-05-16 16:58:57 +08:00
double result = r * r;
return double.IsFinite(result) ? result : 0;
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));
return Math.Sqrt(sum / values.Count);
}
2026-05-15 15:48:43 +08:00
2026-05-05 15:31:24 +08:00
}
2026-05-16 16:58:57 +08:00
}