Files
CSI-Z420-Tablet-Multi-Funct…/ViewModels/StationViewModel.cs
2026-05-15 17:47:11 +08:00

590 lines
26 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.Series;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
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 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;
[ObservableProperty] private TestPhase _phase = TestPhase.Idle;
// 硬度
[ObservableProperty] private double _hardnessValue;
[ObservableProperty] private bool _hardnessPass;
[ObservableProperty] private double _hardnessAvg;
[ObservableProperty] private double _hardnessRSD;
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 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;
// 命令
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;
_balance = new BalanceService(); // 实例化天平服务(模拟)
StartHardnessCommand = new AsyncRelayCommand(RunHardnessAsync);
StartFriabilityCommand = new AsyncRelayCommand(RunFriabilityAsync);
StartDisintegrationCommand = new AsyncRelayCommand(RunDisintegrationAsync);
StartDissolutionCommand = new AsyncRelayCommand(RunDissolutionAsync);
ExportHistoryCommand = new AsyncRelayCommand(ExportHistoryAsync);
// 溶出曲线
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; // 剩余圈数重置为默认值通常是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()
{
if (Phase != TestPhase.Running) return;
try
{
switch (CurrentTest)
{
case TestType.Hardness:
HardnessValue = await _plc.ReadFloatAsync(_plcConfig.HardnessValue);
break;
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:
DissolutionRpm = await _plc.ReadFloatAsync(_plcConfig.DissolutionRpm);
DissolutionPercent = await _plc.ReadFloatAsync(_plcConfig.DissolutionPercent);
if (_dissolutionStartTime != DateTime.MinValue)
{
double minutes = (DateTime.Now - _dissolutionStartTime).TotalMinutes;
_dissolutionSeries.Points.Add(new DataPoint(minutes, DissolutionPercent));
if (_dissolutionSeries.Points.Count > 200) _dissolutionSeries.Points.RemoveAt(0);
DissolutionPlotModel.InvalidatePlot(true);
}
break;
}
}
catch { }
}
private async Task RunHardnessAsync()
{
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Hardness;
Phase = TestPhase.Running;
HardnessPass = false; // 添加这一行
_hardnessResults.Clear();
try
{
int count = App.CurrentPharmaParams.HardnessTestCount;
double min = App.CurrentPharmaParams.HardnessMin_N;
double max = App.CurrentPharmaParams.HardnessMax_N;
for (int i = 0; i < count; i++)
{ // 1. 给PLC发指令启动单次硬度测试
await _plc.WriteCoilAsync((ushort)(StationId == 1 ? _plcConfig.HardnessStartCoil :
StationId == 2 ? _plcConfig.HardnessStartCoil2
: StationId == 3 ? _plcConfig.HardnessStartCoil3 : 0), true);
bool completed = false;
while (!completed && Phase == TestPhase.Running)
{
await Task.Delay(200);// 每200ms轮询一次状态
completed = await _plc.ReadCoilAsync(_plcConfig.HardnessCompleteCoil);
}
double val = await _plc.ReadFloatAsync(_plcConfig.HardnessValue);
_hardnessResults.Add(val); // 把结果存到列表里
HardnessValue = val; // 更新界面上的实时测试力值
await Task.Delay(1000); // 间隔1秒再进行下一次测试
}
HardnessAvg = _hardnessResults.Average();
HardnessMax= _hardnessResults.Max();
HardnessMin = _hardnessResults.Min();
HardnessCurrentCount = count;
HardnessRSD = (StandardDeviation(_hardnessResults) / HardnessAvg) * 100;
HardnessPass = HardnessAvg >= min && HardnessAvg <= max;
Phase = TestPhase.Completed;//恢复启动按钮
}
catch (Exception ex)
{
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"硬度测试出错:{ex.Message}"));
Phase = TestPhase.Error;
}
finally
{
Phase = TestPhase.Idle;
// 在保存前设置崩解合格标志剩余管数为0 且 崩解时间未超时
//DisintegrationPass = (RemainingTubes == 0 && DisintegrationSeconds <= App.CurrentPharmaParams.DisintegrationMaxSeconds);
await SaveBatchResult();
}
}
/// 脆碎度测试主逻辑适配3工位、实时状态显示
private async Task RunFriabilityAsync()
{
// 1. 防并发:如果设备不是空闲状态,直接退出
if (Phase != TestPhase.Idle)
return;
// 2. 标记当前正在运行的是脆碎度测试
CurrentTest = TestType.Friability;
Phase = TestPhase.Running;
FriabilityPass = false;
try
{
ushort startCoil = StationId switch
{
1 => _plcConfig.FriabilityStartCoil, // 工位1启动线圈
2 => _plcConfig.FriabilityStartCoil2, // 工位2启动线圈
3 => _plcConfig.FriabilityStartCoil3, // 工位3启动线圈
_ => 0
};
if (startCoil == 0)
{
throw new InvalidOperationException("当前工位未配置脆碎度启动线圈地址");
}
WeightBefore = await _balance.ReadWeightAsync();
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);
}
WeightAfter = await _balance.ReadWeightAsync();
FriabilityCurrentRpm = FriabilityTargetRpm;
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;
FriabilityRemainingRounds = 100;
await SaveBatchResult();
}
}
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 _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()
{
if (Phase != TestPhase.Idle) return;
CurrentTest = TestType.Dissolution;
Phase = TestPhase.Running;
DissolutionPass = false; // 添加这一行
_dissolutionStartTime = DateTime.Now;
_dissolutionSeries.Points.Clear();
_dissolutionTimes.Clear();
_dissolutionValues.Clear();
try
{
await _plc.WriteCoilAsync(_plcConfig.DissolutionStartCoil, true);
var sampleTimes = App.CurrentPharmaParams.DissolutionSampleTimes;
double prevMin = 0;
foreach (var t in sampleTimes)
{
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();
}
}
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,
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));
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)
{
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);
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);
}
}
}