Files
CSI-Z420-Tablet-Multi-Funct…/ViewModels/StationViewModel.cs
2026-05-18 11:28:52 +08:00

1341 lines
53 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.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
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;
private bool _isLoadingDissolution1Time;
private bool _isLoadingDissolution2Time;
private bool _isLoadingDissolution1SampleInterval;
private bool _isLoadingDissolution2SampleInterval;
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;
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 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 = 72; //最大力采集
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 int _friabilityTargetTimeSec = 240;
[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 _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;
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;
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);
// 硬度命令
//HardnessUpCommand = new AsyncRelayCommand(async () =>
//{
// await _plc.WriteCoilAsync(0x20, true);
// await Task.Delay(100);
// await _plc.WriteCoilAsync(0x20, false);
//});
HardnessDownCommand = new AsyncRelayCommand(async () =>
{
await _plc.WriteCoilAsync(0x21, true);
await Task.Delay(100);
await _plc.WriteCoilAsync(0x21, false);
});
//复位
HardnessResetCommand = new AsyncRelayCommand(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();
HardnessValue = 0;
HardnessAvg = 0;
HardnessRSD = 0;
HardnessCurrentCount = 0;
HardnessMax = 0;
HardnessMin = 0;
Phase = TestPhase.Idle;
});
// 硬前进按钮命令
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);
});
// 硬度命令停止
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(() => {
//测试停止
Phase = TestPhase.Idle; return Task.CompletedTask;
});
ResetFriabilityCommand = new AsyncRelayCommand(() =>
{
FriabilityRemainingRounds = 100; // 剩余圈数重置为默认值通常是100圈
LossPercent = 0; // 失重率清零
WeightBefore = 0; // 脆碎前重量清零
WeightAfter = 0;// 脆碎后重量清零
return Task.CompletedTask;
});
PrintFriabilityCommand = new AsyncRelayCommand(async () => await PrintReport("脆碎度"));
// 溶出度命令
DissolutionUpCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x22, true));
DissolutionDownCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x23, true));
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("崩解"));
_ = LoadDisintegrationTimeAsync();
_ = LoadDisintegrationSpeedAsync();
_ = LoadDissolutionTimesAsync();
}
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 = 6 - TubesCompleted.Count(c => c);
}
}
break;
case TestType.Dissolution:
await UpdateDissolutionDataAsync();
break;
}
}
catch { }
}
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)
{
await CheckDissolutionSampleAsync(1);
updated |= await ReadDissolutionChannelAsync(1);
}
if (_isDissolution2Running)
{
await CheckDissolutionSampleAsync(2);
updated |= await ReadDissolutionChannelAsync(2);
}
UpdateDissolutionClock();
if (updated)
DissolutionPlotModel.InvalidatePlot(true);
}
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("取样确认窗口已关闭,未写入确认线圈");
}
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);
}
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 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);
}
private async Task RunHardnessAsync()
{
// 点击启动才执行一次
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Hardness;
Phase = TestPhase.Running;
try
{
double currentSpeed = HardnessSudu;
double currentWeiyi = HardnessWeiyi;
// 写入PLC
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)currentSpeed);
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)currentWeiyi);
await _plc.WriteCoilAsync(_plcConfig.HardnessStartCoil, true);//启动
}
catch (Exception ex)
{
}
finally
{
Phase = TestPhase.Idle;
await SaveBatchResult();
}
}
/// 脆碎度测试主逻辑(实时状态显示)
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("未配置脆碎度启动线圈地址");
}
WeightBefore = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量");
if (WeightBefore <= 0)
throw new InvalidOperationException("脆碎前重量必须大于0");
await _plc.WriteCoilAsync(startCoil, true);
int totalRounds = 100; // 药典标准脆碎度总圈数100圈
double rpm = FriabilityTargetRpm; // 界面设置的目标转速r/min
int durationMs = (int)((totalRounds / rpm) * 60 * 1000); // 总运行时间(毫秒)
for (int i = 0; i < durationMs; i += 100)
{
// 如果用户点了停止状态会被设为Idle直接跳出循环
if (Phase != TestPhase.Running)
break;
// 计算当前剩余圈数
double elapsedMs = i;
double elapsedRounds = rpm / 60 / 1000 * elapsedMs;
int remainingRounds = (int)Math.Ceiling(totalRounds - elapsedRounds);
// 更新界面绑定的剩余圈数
FriabilityRemainingRounds = remainingRounds;
// 等待100ms再更新下一次
await Task.Delay(100);
}
if (Phase != TestPhase.Running)
throw new InvalidOperationException("脆碎度测试已停止,未保存结果");
WeightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量");
FriabilityCurrentRpm = FriabilityTargetRpm;
LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;//失重率
FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent; //标准值
resultReady = true;
// 标记测试为已完成
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() =>
MessageBox.Show($"脆碎度测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
FriabilityRemainingRounds = 100;
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; // 添加这一行
TubesCompleted = new bool[6];
RemainingTubes = 6;
DisintegrationSeconds = 0;
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_disintegrationTimer.Tick += (s, e) =>
{
if (Phase == TestPhase.Running)
DisintegrationSeconds++;
};
_disintegrationTimer.Start();
try
{
await WriteDisintegrationSpeedAsync(DisintegrationSpeedRpm);
await WriteDisintegrationTimeAsync(DisintegrationTimeMin);
await PulseCoilAsync(_plcConfig.DisintegrationStartCoil);
int maxSec = DisintegrationTimeMin > 0
? (int)Math.Ceiling(DisintegrationTimeMin * 60)
: App.CurrentPharmaParams.DisintegrationMaxSeconds;
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
{
await Task.Delay(500);
}
_disintegrationTimer.Stop();
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
_disintegrationTimer.Stop();
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
int maxSec = DisintegrationTimeMin > 0
? (int)Math.Ceiling(DisintegrationTimeMin * 60)
: App.CurrentPharmaParams.DisintegrationMaxSeconds;
DisintegrationPass = !_discardDisintegrationResult && RemainingTubes == 0 && DisintegrationSeconds <= maxSec;
if (!_discardDisintegrationResult)
await SaveBatchResult();
_discardDisintegrationResult = false;
}
}
private async Task StopDisintegrationAsync()
{
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);
_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()
{
if (_plcConfig.Dissolution2Percent == 0)
{
LocalAlarm = "溶出2溶出度寄存器未配置";
DissolutionCurveStatus = LocalAlarm;
return;
}
CurrentTest = TestType.Dissolution;
Phase = TestPhase.Running;
DissolutionPass = false;
ResetDissolutionChannel(2);
ResetDissolutionSampleState(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)
{
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)
{
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();
}
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,
HardnessTestCount = HardnessTestCount,
// 脆碎度
FriabilityLoss = LossPercent,
FriabilityTargetRpm = FriabilityTargetRpm,
FriabilityTargetTimeSec = FriabilityTargetTimeSec,
FriabilityClockwise = FriabilityClockwise,
FriabilityRemainingRounds = FriabilityRemainingRounds,
WeightBefore = WeightBefore,
WeightAfter = WeightAfter,
// 崩解
DisintegrationTimeSec = DisintegrationSeconds,
RemainingTubesAtEnd = RemainingTubes,
DisintegrationTargetFreq = 0,
DisintegrationTemp = DisintegrationTemp,
// 溶出
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 = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass
};
await Task.Run(() => _db.InsertBatch(batch));
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 获取当前测试项目是否合格
bool currentPass = CurrentTest switch
{
TestType.Hardness => HardnessPass,
TestType.Friability => FriabilityPass,
TestType.Disintegration => DisintegrationPass,
TestType.Dissolution => DissolutionPass,
_ => false
};
string projectName = CurrentTest switch
{
TestType.Hardness => "硬度",
TestType.Friability => "脆碎度",
TestType.Disintegration => "崩解",
TestType.Dissolution => string.IsNullOrWhiteSpace(_dissolutionResultChannel) ? "溶出" : _dissolutionResultChannel,
_ => ""
};
LocalAlarm = currentPass ? $"{projectName}测试合格" : $"{projectName}测试不合格";
});
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}"));
}
}
private double CalculateRSquared(List<double> timeMinutes, List<double> concentration)
{
if (timeMinutes.Count < 2 || timeMinutes.Count != concentration.Count) return 0;
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));
if (denominator <= 0 || double.IsNaN(denominator))
return 0;
double r = numerator / denominator;
double result = r * r;
return double.IsFinite(result) ? result : 0;
}
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 Math.Sqrt(sum / values.Count);
}
}
}