This commit is contained in:
xyy
2026-05-06 16:41:32 +08:00
parent 11bf3f4827
commit 27aa99057f
11 changed files with 1393 additions and 300 deletions

View File

@@ -1,12 +1,13 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OxyPlot;
using OxyPlot.Series;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using OxyPlot;
using OxyPlot.Series;
using TabletTester2025.Models;
using TabletTester2025.Services;
@@ -19,8 +20,12 @@ namespace TabletTester2025.ViewModels
private readonly DatabaseService _db;
private readonly ExcelExportService _excel;
private readonly AlarmService _alarm;
private readonly BalanceService _balance; // ✅ 新增天平服务
private DispatcherTimer _disintegrationTimer;
private List<double> _dissolutionTimes = new List<double>();
private List<double> _dissolutionValues = new List<double>();
public int StationId { get; }
[ObservableProperty] private TestType _currentTest;
@@ -49,6 +54,53 @@ namespace TabletTester2025.ViewModels
// 溶出
[ObservableProperty] private double _dissolutionRpm;
[ObservableProperty] private double _dissolutionPercent;
// 硬度相关新增
[ObservableProperty] private int _hardnessTestCount = 6;
[ObservableProperty] private int _hardnessIntervalSec = 2;
[ObservableProperty] private int _hardnessCurrentCount;
[ObservableProperty] private double _hardnessMax;
[ObservableProperty] private double _hardnessMin;
[ObservableProperty] private bool _disintegrationPass; // 新增
[ObservableProperty] private bool _dissolutionPass; // 新增
public IAsyncRelayCommand HardnessUpCommand { get; }
public IAsyncRelayCommand HardnessDownCommand { get; }
public IAsyncRelayCommand HardnessResetCommand { get; }
public IAsyncRelayCommand PrintHardnessCommand { get; }
// 脆碎度新增
[ObservableProperty] private double _friabilityTargetRpm = 25;
[ObservableProperty] private int _friabilityTargetTimeSec = 240;
[ObservableProperty] private bool _friabilityClockwise = true;
[ObservableProperty] private bool _friabilityCounterClockwise;
[ObservableProperty] private double _friabilityCurrentRpm;
[ObservableProperty] private int _friabilityRemainingRounds = 100;
public IAsyncRelayCommand StopFriabilityCommand { get; }
public IAsyncRelayCommand ResetFriabilityCommand { get; }
public IAsyncRelayCommand PrintFriabilityCommand { get; }
// 溶出度新增
[ObservableProperty] private double _dissolutionUpDownFreq = 32;
[ObservableProperty] private int _dissolutionSampleInterval = 5;
[ObservableProperty] private double _dissolutionTargetRpm = 50;
[ObservableProperty] private double _dissolutionElapsedTime;
[ObservableProperty] private double _dissolutionCountdown;
[ObservableProperty] private double _dissolutionRSquared;
public IAsyncRelayCommand DissolutionUpCommand { get; }
public IAsyncRelayCommand DissolutionDownCommand { get; }
public IAsyncRelayCommand StopDissolutionCommand { get; }
public IAsyncRelayCommand PrintDissolutionCommand { get; }
// 崩解新增
[ObservableProperty] private double _disintegrationTargetFreq = 31;
public IAsyncRelayCommand StopDisintegrationCommand { get; }
public IAsyncRelayCommand PrintDisintegrationCommand { get; }
public PlotModel DissolutionPlotModel { get; }
private LineSeries _dissolutionSeries;
private DateTime _dissolutionStartTime;
@@ -60,6 +112,8 @@ namespace TabletTester2025.ViewModels
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;
@@ -68,6 +122,7 @@ namespace TabletTester2025.ViewModels
_db = db;
_excel = excel;
_alarm = alarm;
_balance = new BalanceService(); // 实例化天平服务(模拟)
StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync);
StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync);
@@ -75,10 +130,67 @@ namespace TabletTester2025.ViewModels
StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync);
ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync);
// 溶出曲线:时间 vs 溶出度
// 溶出曲线
DissolutionPlotModel = new PlotModel { Title = $"工位{StationId} 溶出曲线" };
_dissolutionSeries = new LineSeries { Title = "溶出度 (%)", Color = OxyColors.Green };
DissolutionPlotModel.Series.Add(_dissolutionSeries);
// 硬度命令
HardnessUpCommand = new AsyncRelayCommand(async () =>
{
await _plc.WriteCoilAsync(0x20, true);
await Task.Delay(100);
await _plc.WriteCoilAsync(0x20, false);
});
HardnessDownCommand = new AsyncRelayCommand(async () =>
{
await _plc.WriteCoilAsync(0x21, true);
await Task.Delay(100);
await _plc.WriteCoilAsync(0x21, false);
});
HardnessResetCommand = new AsyncRelayCommand(() =>
{
_hardnessResults.Clear();
HardnessValue = 0;
HardnessAvg = 0;
HardnessRSD = 0;
HardnessCurrentCount = 0;
HardnessMax = 0;
HardnessMin = 0;
Phase = TestPhase.Idle;
return Task.CompletedTask;
});
PrintHardnessCommand = new AsyncRelayCommand(async () => await PrintReport("硬度"));
// 脆碎度命令
StopFriabilityCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; });
ResetFriabilityCommand = new AsyncRelayCommand(() =>
{
FriabilityRemainingRounds = 100;
LossPercent = 0;
WeightBefore = 0;
WeightAfter = 0;
return Task.CompletedTask;
});
PrintFriabilityCommand = new AsyncRelayCommand(async () => await PrintReport("脆碎度"));
// 溶出度命令
DissolutionUpCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x22, true));
DissolutionDownCommand = new AsyncRelayCommand(async () => await _plc.WriteCoilAsync(0x23, true));
StopDissolutionCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; });
PrintDissolutionCommand = new AsyncRelayCommand(async () => await PrintReport("溶出度"));
// 崩解命令
StopDisintegrationCommand = new AsyncRelayCommand(() => { Phase = TestPhase.Idle; return Task.CompletedTask; });
PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解"));
}
private async Task PrintReport(string testName)
{
await App.Current.Dispatcher.InvokeAsync(() =>
{
MessageBox.Show($"打印{testName}报告 - 工位{StationId}", "打印", MessageBoxButton.OK, MessageBoxImage.Information);
});
}
public async Task UpdateRealTimeData()
@@ -122,13 +234,14 @@ namespace TabletTester2025.ViewModels
private async Task RunHardnessAsync()
{
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Hardness;
Phase = TestPhase.Running;
HardnessPass = false; // 添加这一行
_hardnessResults.Clear();
try
{
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Hardness;
Phase = TestPhase.Running;
_hardnessResults.Clear();
int count = App.CurrentPharmaParams.HardnessTestCount;
double min = App.CurrentPharmaParams.HardnessMin_N;
double max = App.CurrentPharmaParams.HardnessMax_N;
@@ -151,12 +264,21 @@ namespace TabletTester2025.ViewModels
HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100;
HardnessPass = HardnessAvg >= min && HardnessAvg <= max;
Phase = TestPhase.Completed;
await SaveBatchResult();
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"硬度测试出错:{ex.Message}"));
Phase = TestPhase.Error;
System.Windows.MessageBox.Show($"硬度测试出错:{ex.Message}\n{ex.StackTrace}", "错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
}
finally
{
Phase = TestPhase.Idle;
// 在保存前设置崩解合格标志剩余管数为0 且 崩解时间未超时
//DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds);
await SaveBatchResult();
}
}
@@ -165,16 +287,35 @@ namespace TabletTester2025.ViewModels
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Friability;
Phase = TestPhase.Running;
FriabilityPass = false; // 添加这一行
try
{
// 通过天平读取前重
WeightBefore = await _balance.ReadWeightAsync();
await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true);
// 模拟称重(实际可通过串口天平)
WeightBefore = 6.5;
await _plc.WriteCoilAsync(_plcConfig.FriabilityStartCoil, true);
await Task.Delay(TimeSpan.FromMinutes(4));
WeightAfter = WeightBefore * (1 - 0.008);
LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;
FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent;
Phase = TestPhase.Completed;
await SaveBatchResult();
// 根据设定的转速和总圈数计算运行时间
int totalRounds = 100;
double rpm = FriabilityTargetRpm;
int durationMs = (int)((totalRounds / rpm) * 60 * 1000);
await Task.Delay(durationMs);
WeightAfter = await _balance.ReadWeightAsync();
LossPercent = (WeightBefore - WeightAfter) / WeightBefore * 100;
FriabilityPass = LossPercent <= App.CurrentPharmaParams.FriabilityMaxLossPercent;
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"脆碎度测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
await SaveBatchResult();
}
}
private async Task RunDisintegrationAsync()
@@ -182,20 +323,42 @@ namespace TabletTester2025.ViewModels
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Disintegration;
Phase = TestPhase.Running;
DisintegrationPass = false; // 添加这一行
TubesCompleted = new bool[6];
RemainingTubes = 6;
_disintegrationSeconds = 0;
DisintegrationSeconds = 0;
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_disintegrationTimer.Tick += (s, e) => { if (Phase == TestPhase.Running) DisintegrationSeconds = _disintegrationSeconds + 1; };
_disintegrationTimer.Tick += (s, e) =>
{
if (Phase == TestPhase.Running)
DisintegrationSeconds++;
};
_disintegrationTimer.Start();
await _plc.WriteCoilAsync(_plcConfig.DisintegrationStartCoil, true);
int maxSec = App.CurrentPharmaParams.DisintegrationMaxSeconds;
while (RemainingTubes > 0 && _disintegrationSeconds < maxSec && Phase == TestPhase.Running)
await Task.Delay(500);
_disintegrationTimer.Stop();
Phase = TestPhase.Completed;
await SaveBatchResult();
try
{
await _plc.WriteCoilAsync(_plcConfig.DisintegrationStartCoil, true);
int maxSec = App.CurrentPharmaParams.DisintegrationMaxSeconds;
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
{
await Task.Delay(500);
}
_disintegrationTimer.Stop();
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
_disintegrationTimer.Stop();
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds);
await SaveBatchResult();
}
}
private async Task RunDissolutionAsync()
@@ -203,65 +366,159 @@ namespace TabletTester2025.ViewModels
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Dissolution;
Phase = TestPhase.Running;
DissolutionPass = false; // 添加这一行
_dissolutionStartTime = DateTime.Now;
_dissolutionSeries.Points.Clear();
await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true);
_dissolutionTimes.Clear();
_dissolutionValues.Clear();
var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes;
double prevMin = 0;
foreach (var t in sampleTimes)
try
{
int delayMs = (int)((t - prevMin) * 60 * 1000);
if (delayMs > 0) await Task.Delay(delayMs);
double value = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent);
DissolutionPercent = value;
prevMin = t;
// 弹出取样提示(可改为非阻塞提示)
App.Current.Dispatcher.Invoke(() =>
await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true);
var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes;
double prevMin = 0;
foreach (var t in sampleTimes)
{
System.Windows.MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%");
});
int delayMs = (int)((t - prevMin) * 60 * 1000);
if (delayMs > 0) await Task.Delay(delayMs);
double value = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent);
DissolutionPercent = value;
_dissolutionTimes.Add(t);
_dissolutionValues.Add(value);
prevMin = t;
await App.Current.Dispatcher.InvokeAsync(() =>
{
MessageBox.Show($"工位{StationId} 请在{t}分钟取样。当前溶出度: {value:F1}%");
});
}
// 计算 R²
_dissolutionRSquared = CalculateRSquared(_dissolutionTimes, _dissolutionValues);
bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
Phase = TestPhase.Completed;
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"溶出测试出错: {ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
DissolutionPass = (DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min);
await SaveBatchResult();
}
bool pass = DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
Phase = TestPhase.Completed;
await SaveBatchResult(pass);
}
private async Task SaveBatchResult(bool? forcedQualified = null)
{
try
{
// 计算溶出曲线的 R²需在 RunDissolutionAsync 中计算后存入 _dissolutionRSquared
double rsquared = DissolutionRSquared; // 确保此属性在溶出结束后被赋值
var batch = new TestBatch
{
TestTime = DateTime.Now,
StationId = StationId,
SampleName = $"样品-{StationId}",
// 硬度
HardnessAvg = HardnessAvg,
HardnessRSD = HardnessRSD,
HardnessMax = HardnessMax,
HardnessMin = HardnessMin,
HardnessTestCount = HardnessTestCount,
// 脆碎度
FriabilityLoss = LossPercent,
FriabilityTargetRpm = FriabilityTargetRpm,
FriabilityTargetTimeSec = FriabilityTargetTimeSec,
FriabilityClockwise = FriabilityClockwise,
FriabilityRemainingRounds = FriabilityRemainingRounds,
WeightBefore = WeightBefore,
WeightAfter = WeightAfter,
// 崩解
DisintegrationTimeSec = DisintegrationSeconds,
RemainingTubesAtEnd = RemainingTubes,
DisintegrationTargetFreq = DisintegrationTargetFreq,
DisintegrationTemp = DisintegrationTemp,
// 溶出
DissolutionRate30Min = DissolutionPercent,
IsQualified = forcedQualified ?? (HardnessPass && FriabilityPass && RemainingTubes == 0 && DissolutionPercent >= App.CurrentPharmaParams.DissolutionMinPercentAt30min)
DissolutionTargetRpm = DissolutionTargetRpm,
DissolutionRSquared = rsquared,
DissolutionSampleInterval = DissolutionSampleInterval,
DissolutionUpDownFreq = DissolutionUpDownFreq,
HardnessPass = HardnessPass,
FriabilityPass = FriabilityPass,
DisintegrationPass = DisintegrationPass,
DissolutionPass = DissolutionPass,
TestType = CurrentTest switch
{
TestType.Hardness => "硬度",
TestType.Friability => "脆碎度",
TestType.Disintegration => "崩解",
TestType.Dissolution => "溶出",
_ => ""
},
IsQualified = HardnessPass && FriabilityPass && DisintegrationPass && DissolutionPass
};
await Task.Run(() => _db.InsertBatch(batch));
if (!batch.IsQualified)
_alarm.RaiseAlarm($"工位{StationId} 测试不合格");
else
_alarm.ClearAlarm();
await Application.Current.Dispatcher.InvokeAsync(() =>
{
// 获取当前测试项目是否合格
bool currentPass = CurrentTest switch
{
TestType.Hardness => HardnessPass,
TestType.Friability => FriabilityPass,
TestType.Disintegration => DisintegrationPass,
TestType.Dissolution => DissolutionPass,
_ => false
};
string projectName = CurrentTest switch
{
TestType.Hardness => "硬度",
TestType.Friability => "脆碎度",
TestType.Disintegration => "崩解",
TestType.Dissolution => "溶出",
_ => ""
};
LocalAlarm = currentPass ? $"{projectName}测试合格" : $"{projectName}测试不合格";
});
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"保存测试结果失败:{ex.Message}\n{ex.InnerException?.Message}", "数据库错误", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}"));
}
}
private double CalculateRSquared(List<double> timeMinutes, List<double> concentration)
{
if (timeMinutes.Count < 2) return 1;
int n = timeMinutes.Count;
double sumX = timeMinutes.Sum();
double sumY = concentration.Sum();
double sumXY = timeMinutes.Zip(concentration, (x, y) => x * y).Sum();
double sumX2 = timeMinutes.Select(x => x * x).Sum();
double sumY2 = concentration.Select(y => y * y).Sum();
double numerator = (n * sumXY - sumX * sumY);
double denominator = Math.Sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
double r = numerator / denominator;
return r * r;
}
private async Task ExportHistoryAsync()
{
var batches = await Task.Run(() => _db.GetBatches(StationId, 100));
string path = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), $"工位{StationId}_检测记录_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
_excel.ExportToExcel(batches, path);
System.Windows.MessageBox.Show($"导出成功: {path}");
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"导出成功: {path}"));
}
private double StandardDeviation(List<double> values)