更新2026
This commit is contained in:
@@ -36,6 +36,9 @@ namespace TabletTester2025.ViewModels
|
||||
private bool _isLoadingFriabilityRounds;
|
||||
private bool _isUpdatingFriabilityWeightFromPlc;
|
||||
private bool _isReadingHardnessLiveForce;
|
||||
private bool _isHardnessRunning;
|
||||
private bool _isFriabilityRunning;
|
||||
private bool _isDisintegrationRunning;
|
||||
private int _hardnessGroupNo;
|
||||
private int _currentHardnessGroupNo;
|
||||
|
||||
@@ -103,6 +106,9 @@ namespace TabletTester2025.ViewModels
|
||||
// 崩解
|
||||
[ObservableProperty] private double _disintegrationTemp;
|
||||
[ObservableProperty] private int _disintegrationSeconds;
|
||||
[ObservableProperty] private double _disintegrationActualSeconds;
|
||||
[ObservableProperty] private string _disintegrationActualSecondsText = "0";
|
||||
[ObservableProperty] private bool _canSaveDisintegrationResult;
|
||||
[ObservableProperty] private bool _isBasketMovingUp;
|
||||
[ObservableProperty] private bool[] _tubesCompleted = new bool[6];
|
||||
[ObservableProperty] private int _remainingTubes;
|
||||
@@ -117,6 +123,7 @@ namespace TabletTester2025.ViewModels
|
||||
[ObservableProperty] private int _hardnessTestCount = 6;
|
||||
[ObservableProperty] private int _hardnessIntervalSec = 2;
|
||||
[ObservableProperty] private int _hardnessCurrentCount;
|
||||
[ObservableProperty] private int _hardnessTotalCount;
|
||||
[ObservableProperty] private double _hardnessMax;
|
||||
[ObservableProperty] private double _hardnessMin;
|
||||
|
||||
@@ -187,6 +194,7 @@ namespace TabletTester2025.ViewModels
|
||||
public IAsyncRelayCommand StopDisintegrationCommand { get; }
|
||||
public IAsyncRelayCommand ResetDisintegrationCommand { get; }
|
||||
public IAsyncRelayCommand PrintDisintegrationCommand { get; }
|
||||
public IAsyncRelayCommand SaveDisintegrationResultCommand { get; }
|
||||
|
||||
public PlotModel DissolutionPlotModel { get; }
|
||||
private readonly LineSeries _dissolution1Series;
|
||||
@@ -261,7 +269,8 @@ namespace TabletTester2025.ViewModels
|
||||
await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, true);
|
||||
await Task.Delay(100); // 脉冲宽度,可根据PLC程序调整20~100ms
|
||||
await _plc.WriteCoilAsync(_plcConfig.HardnessStartReset, false);
|
||||
Phase = TestPhase.Idle;
|
||||
_isHardnessRunning = false;
|
||||
RefreshOverallPhase();
|
||||
|
||||
});
|
||||
|
||||
@@ -289,7 +298,8 @@ namespace TabletTester2025.ViewModels
|
||||
await Task.Delay(100); // 脉冲宽度,可根据PLC程序调整20~100ms
|
||||
await _plc.WriteCoilAsync(_plcConfig.HardnessStartStop, false);
|
||||
|
||||
Phase = TestPhase.Idle;
|
||||
_isHardnessRunning = false;
|
||||
RefreshOverallPhase();
|
||||
});
|
||||
// 脆碎度命令
|
||||
StopFriabilityCommand = new AsyncRelayCommand(async () => {
|
||||
@@ -298,7 +308,8 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
if (_plcConfig.FriabilityStartCoilStop != 0)
|
||||
await PulseCoilAsync(_plcConfig.FriabilityStartCoilStop);
|
||||
Phase = TestPhase.Idle;
|
||||
_isFriabilityRunning = false;
|
||||
RefreshOverallPhase();
|
||||
});
|
||||
ResetFriabilityCommand = new AsyncRelayCommand(() =>
|
||||
{
|
||||
@@ -325,6 +336,7 @@ namespace TabletTester2025.ViewModels
|
||||
StopDisintegrationCommand = new AsyncRelayCommand(StopDisintegrationAsync);
|
||||
ResetDisintegrationCommand = new AsyncRelayCommand(ResetDisintegrationAsync);
|
||||
PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解"));
|
||||
SaveDisintegrationResultCommand = new AsyncRelayCommand(SaveDisintegrationResultAsync);
|
||||
|
||||
_ = LoadFriabilitySettingsAsync();
|
||||
}
|
||||
@@ -395,29 +407,26 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
public async Task UpdateRealTimeData()
|
||||
{
|
||||
if (Phase != TestPhase.Running) return;
|
||||
if (!IsAnyTestRunning()) return;
|
||||
try
|
||||
{
|
||||
switch (CurrentTest)
|
||||
if (_isDisintegrationRunning)
|
||||
{
|
||||
|
||||
case TestType.Disintegration:
|
||||
DisintegrationTemp = await _plc.ReadFloatAsync(_plcConfig.DisintegrationTemp);
|
||||
IsBasketMovingUp = await _plc.ReadCoilAsync(_plcConfig.DisintegrationMovingUpCoil);
|
||||
for (int i = 0; i < _plcConfig.DisintegrationCompleteCoils.Length; i++)
|
||||
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])
|
||||
{
|
||||
bool completed = await _plc.ReadCoilAsync(_plcConfig.DisintegrationCompleteCoils[i]);
|
||||
if (completed && !TubesCompleted[i])
|
||||
{
|
||||
TubesCompleted[i] = true;
|
||||
RemainingTubes = TubesCompleted.Length - TubesCompleted.Count(c => c);
|
||||
}
|
||||
TubesCompleted[i] = true;
|
||||
RemainingTubes = TubesCompleted.Length - TubesCompleted.Count(c => c);
|
||||
}
|
||||
break;
|
||||
case TestType.Dissolution:
|
||||
await UpdateDissolutionDataAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAnyDissolutionRunning())
|
||||
await UpdateDissolutionDataAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -475,7 +484,7 @@ namespace TabletTester2025.ViewModels
|
||||
await FinalizeDissolutionChannelAsync(channel);
|
||||
SetDissolutionRunning(channel, false);
|
||||
ResetDissolutionSampleState(channel);
|
||||
Phase = IsAnyDissolutionRunning() ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
else
|
||||
@@ -488,7 +497,7 @@ namespace TabletTester2025.ViewModels
|
||||
{
|
||||
SetDissolutionSampleRequestActive(channel, false);
|
||||
SetDissolutionRunning(channel, false);
|
||||
Phase = IsAnyDissolutionRunning() ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show($"溶出{channel}取样确认失败:{ex.Message}", "取样确认失败", MessageBoxButton.OK, MessageBoxImage.Error));
|
||||
@@ -540,12 +549,27 @@ namespace TabletTester2025.ViewModels
|
||||
return _isDissolution1Running || _isDissolution2Running;
|
||||
}
|
||||
|
||||
private bool IsAnyTestRunning()
|
||||
{
|
||||
return _isHardnessRunning
|
||||
|| _isFriabilityRunning
|
||||
|| _isDisintegrationRunning
|
||||
|| IsAnyDissolutionRunning();
|
||||
}
|
||||
|
||||
private void RefreshOverallPhase()
|
||||
{
|
||||
Phase = IsAnyTestRunning() ? TestPhase.Running : TestPhase.Idle;
|
||||
}
|
||||
|
||||
private void SetDissolutionRunning(int channel, bool value)
|
||||
{
|
||||
if (channel == 1)
|
||||
_isDissolution1Running = value;
|
||||
else
|
||||
_isDissolution2Running = value;
|
||||
|
||||
RefreshOverallPhase();
|
||||
}
|
||||
|
||||
private void ResetDissolutionRunClock(int channel)
|
||||
@@ -1099,6 +1123,7 @@ namespace TabletTester2025.ViewModels
|
||||
if (_isUpdatingFriabilityWeightFromPlc)
|
||||
return;
|
||||
|
||||
ApplyFriabilityLossFromWeights();
|
||||
_ = WriteFriabilityWeightAsync(ResolveFriabilityWeightBeforeRegister(), value);
|
||||
}
|
||||
|
||||
@@ -1107,6 +1132,7 @@ namespace TabletTester2025.ViewModels
|
||||
if (_isUpdatingFriabilityWeightFromPlc)
|
||||
return;
|
||||
|
||||
ApplyFriabilityLossFromWeights();
|
||||
_ = WriteFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), value);
|
||||
}
|
||||
|
||||
@@ -1121,7 +1147,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
partial void OnFriabilityTargetRoundsChanged(int value)
|
||||
{
|
||||
if (value > 0 && Phase != TestPhase.Running)
|
||||
if (value > 0 && !_isFriabilityRunning)
|
||||
FriabilityRemainingRounds = value;
|
||||
|
||||
if (_isLoadingFriabilityRounds || value <= 0)
|
||||
@@ -1137,7 +1163,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
FriabilityTargetTimeSec = (int)Math.Ceiling(FriabilityTargetTimeMin * 60);
|
||||
|
||||
if (Phase != TestPhase.Running)
|
||||
if (!_isFriabilityRunning)
|
||||
FriabilityRemainingRounds = FriabilityTargetRounds;
|
||||
}
|
||||
|
||||
@@ -1145,6 +1171,8 @@ namespace TabletTester2025.ViewModels
|
||||
{
|
||||
await LoadFriabilityRoundsAsync();
|
||||
await LoadFriabilityWeightsAsync();
|
||||
ApplyFriabilityLossFromWeights();
|
||||
await TryRefreshFriabilityLossPercentFromPlcAsync();
|
||||
}
|
||||
|
||||
private async Task LoadFriabilityRoundsAsync()
|
||||
@@ -1181,7 +1209,7 @@ namespace TabletTester2025.ViewModels
|
||||
{
|
||||
FriabilityTargetRounds = Math.Max(1, rounds);
|
||||
|
||||
if (Phase != TestPhase.Running)
|
||||
if (!_isFriabilityRunning)
|
||||
FriabilityRemainingRounds = FriabilityTargetRounds;
|
||||
}
|
||||
|
||||
@@ -1224,6 +1252,51 @@ namespace TabletTester2025.ViewModels
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void ApplyFriabilityLossFromWeights()
|
||||
{
|
||||
if (!TryCalculateFriabilityLossFromWeights(out double lossPercent))
|
||||
return;
|
||||
|
||||
LossPercent = lossPercent;
|
||||
FriabilityPass = LossPercent <= FriabilityMaxLossPercent;
|
||||
}
|
||||
|
||||
private bool TryCalculateFriabilityLossFromWeights(out double lossPercent)
|
||||
{
|
||||
try
|
||||
{
|
||||
lossPercent = TestCalculationService.CalculateFriabilityLossPercent(WeightBefore, WeightAfter);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
lossPercent = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryRefreshFriabilityLossPercentFromPlcAsync()
|
||||
{
|
||||
ushort registerAddress = ResolveFriabilityLossPercentRegister();
|
||||
if (registerAddress == 0)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
double lossPercent = await _plc.ReadFloatAsync(registerAddress);
|
||||
if (!double.IsFinite(lossPercent) || lossPercent < 0 || lossPercent > 100)
|
||||
return false;
|
||||
|
||||
LossPercent = lossPercent;
|
||||
FriabilityPass = LossPercent <= FriabilityMaxLossPercent;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteFriabilityRoundsAsync(int value)
|
||||
{
|
||||
ushort registerAddress = ResolveFriabilityRoundsRegister();
|
||||
@@ -1262,6 +1335,13 @@ namespace TabletTester2025.ViewModels
|
||||
: _plcConfig.WeightAfter;
|
||||
}
|
||||
|
||||
private ushort ResolveFriabilityLossPercentRegister()
|
||||
{
|
||||
return _plcConfig.FriabilityLossPercent != 0
|
||||
? _plcConfig.FriabilityLossPercent
|
||||
: (ushort)416;
|
||||
}
|
||||
|
||||
private void SetFriabilityWeightFromPlc(double? weightBefore = null, double? weightAfter = null)
|
||||
{
|
||||
_isUpdatingFriabilityWeightFromPlc = true;
|
||||
@@ -1301,6 +1381,28 @@ namespace TabletTester2025.ViewModels
|
||||
DisintegrationTimeMin = seconds / 60.0;
|
||||
}
|
||||
|
||||
partial void OnDisintegrationActualSecondsChanged(double value)
|
||||
{
|
||||
if (CanSaveDisintegrationResult)
|
||||
UpdateDisintegrationPassFromActualTime();
|
||||
}
|
||||
|
||||
partial void OnDisintegrationActualSecondsTextChanged(string value)
|
||||
{
|
||||
if (!CanSaveDisintegrationResult)
|
||||
return;
|
||||
|
||||
if (TryParseDisintegrationActualSeconds(out double seconds, out _))
|
||||
{
|
||||
DisintegrationActualSeconds = seconds;
|
||||
UpdateDisintegrationPassFromActualTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
DisintegrationPass = false;
|
||||
}
|
||||
}
|
||||
|
||||
private int ResolveDisintegrationLimitSeconds(string? dosageForm = null)
|
||||
{
|
||||
string form = string.IsNullOrWhiteSpace(dosageForm) ? DisintegrationDosageForm : dosageForm;
|
||||
@@ -1408,7 +1510,7 @@ namespace TabletTester2025.ViewModels
|
||||
_hardnessGlobalTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
_hardnessGlobalTimer.Tick += async (_, _) =>
|
||||
{
|
||||
if (_isReadingHardnessLiveForce || Phase == TestPhase.Running)
|
||||
if (_isReadingHardnessLiveForce || _isHardnessRunning)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -1430,7 +1532,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private Task ClearHardnessRecordsAsync()
|
||||
{
|
||||
if (Phase == TestPhase.Running && CurrentTest == TestType.Hardness)
|
||||
if (_isHardnessRunning)
|
||||
{
|
||||
MessageBox.Show("硬度测试运行中,不能清空记录。");
|
||||
return Task.CompletedTask;
|
||||
@@ -1438,6 +1540,7 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
ResetCurrentHardnessGroup();
|
||||
HardnessDisplaySamplePoints.Clear();
|
||||
HardnessTotalCount = 0;
|
||||
_hardnessGroupNo = 0;
|
||||
_currentHardnessGroupNo = 0;
|
||||
return Task.CompletedTask;
|
||||
@@ -1464,10 +1567,12 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private async Task RunHardnessAsync()
|
||||
{
|
||||
if (Phase != TestPhase.Idle) return;
|
||||
if (_isHardnessRunning) return;
|
||||
CurrentTest = TestType.Hardness;
|
||||
Phase = TestPhase.Running;
|
||||
_isHardnessRunning = true;
|
||||
RefreshOverallPhase();
|
||||
StartNewHardnessGroup();
|
||||
bool resultReady = false;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -1484,7 +1589,7 @@ namespace TabletTester2025.ViewModels
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessSudu, (float)HardnessSudu);
|
||||
await _plc.WriteFloatAsync(_plcConfig.HardnessWeiyi, (float)HardnessWeiyi);
|
||||
|
||||
while (Phase == TestPhase.Running && _hardnessResults.Count < count)
|
||||
while (_isHardnessRunning && _hardnessResults.Count < count)
|
||||
{
|
||||
bool completeWasActiveBeforeStart = await _plc.ReadCoilAsync(completeCoil);
|
||||
await PulseCoilAsync(startCoil);
|
||||
@@ -1502,20 +1607,19 @@ namespace TabletTester2025.ViewModels
|
||||
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
||||
|
||||
ApplyHardnessStatistics(count);
|
||||
Phase = TestPhase.Completed;
|
||||
resultReady = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show($"硬度测试出错: {ex.Message}"));
|
||||
Phase = TestPhase.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
bool resultReady = Phase == TestPhase.Completed;
|
||||
Phase = TestPhase.Idle;
|
||||
_isHardnessRunning = false;
|
||||
RefreshOverallPhase();
|
||||
if (resultReady)
|
||||
await SaveBatchResult();
|
||||
await SaveBatchResult(TestType.Hardness);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1555,7 +1659,7 @@ namespace TabletTester2025.ViewModels
|
||||
double peak = 0;
|
||||
DateTime deadline = DateTime.Now.AddSeconds(120);
|
||||
|
||||
while (Phase == TestPhase.Running && DateTime.Now <= deadline)
|
||||
while (_isHardnessRunning && DateTime.Now <= deadline)
|
||||
{
|
||||
double liveForce = await ReadHardnessLiveForceAsync();
|
||||
if (liveForce > peak)
|
||||
@@ -1576,7 +1680,7 @@ namespace TabletTester2025.ViewModels
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
if (Phase != TestPhase.Running)
|
||||
if (!_isHardnessRunning)
|
||||
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
||||
|
||||
throw new TimeoutException("等待硬度完成信号超时");
|
||||
@@ -1595,19 +1699,23 @@ namespace TabletTester2025.ViewModels
|
||||
Value = value,
|
||||
RecordedAt = recordedAt
|
||||
});
|
||||
|
||||
int cumulativeNo = HardnessDisplaySamplePoints.Count + 1;
|
||||
HardnessDisplaySamplePoints.Add(new HardnessDisplaySamplePoint
|
||||
{
|
||||
CumulativeNo = cumulativeNo,
|
||||
GroupNo = _currentHardnessGroupNo,
|
||||
SequenceNo = sequenceNo,
|
||||
Value = value,
|
||||
RecordedAt = recordedAt
|
||||
});
|
||||
HardnessTotalCount = HardnessDisplaySamplePoints.Count;
|
||||
}
|
||||
|
||||
private async Task WaitForCoilStateAsync(ushort coilAddress, bool expectedState, TimeSpan timeout, string timeoutMessage)
|
||||
{
|
||||
DateTime deadline = DateTime.Now.Add(timeout);
|
||||
while (Phase == TestPhase.Running && DateTime.Now <= deadline)
|
||||
while (_isHardnessRunning && DateTime.Now <= deadline)
|
||||
{
|
||||
if (await _plc.ReadCoilAsync(coilAddress) == expectedState)
|
||||
return;
|
||||
@@ -1615,7 +1723,7 @@ namespace TabletTester2025.ViewModels
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
if (Phase != TestPhase.Running)
|
||||
if (!_isHardnessRunning)
|
||||
throw new InvalidOperationException("硬度测试已停止,未保存结果");
|
||||
|
||||
throw new TimeoutException(timeoutMessage);
|
||||
@@ -1641,20 +1749,26 @@ namespace TabletTester2025.ViewModels
|
||||
sample.DeviationFromAverage = Math.Abs(sample.Value - stats.Average);
|
||||
|
||||
foreach (var sample in HardnessDisplaySamplePoints.Where(s => s.GroupNo == _currentHardnessGroupNo))
|
||||
{
|
||||
sample.DeviationFromAverage = Math.Abs(sample.Value - stats.Average);
|
||||
sample.GroupAverage = stats.Average;
|
||||
sample.GroupAverageDeviation = stats.AverageDeviation;
|
||||
sample.GroupRSD = stats.RsdPercent;
|
||||
}
|
||||
}
|
||||
|
||||
/// 脆碎度测试主逻辑(实时状态显示)
|
||||
|
||||
private async Task RunFriabilityAsync()
|
||||
{
|
||||
// 1. 防并发:如果设备不是空闲状态,直接退出
|
||||
if (Phase != TestPhase.Idle)
|
||||
// 1. 防并发:只阻止脆碎度重复启动,不影响其它测试项目
|
||||
if (_isFriabilityRunning)
|
||||
return;
|
||||
|
||||
// 2. 标记当前正在运行的是脆碎度测试
|
||||
CurrentTest = TestType.Friability;
|
||||
Phase = TestPhase.Running;
|
||||
_isFriabilityRunning = true;
|
||||
RefreshOverallPhase();
|
||||
FriabilityPass = false;
|
||||
bool resultReady = false;
|
||||
|
||||
@@ -1687,8 +1801,8 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
for (int i = 0; i < durationMs; i += 100)
|
||||
{
|
||||
// 如果用户点了停止,状态会被设为Idle,直接跳出循环
|
||||
if (Phase != TestPhase.Running)
|
||||
// 如果用户点了停止,只结束脆碎度测试,不影响其它测试
|
||||
if (!_isFriabilityRunning)
|
||||
break;
|
||||
|
||||
// 计算当前剩余圈数
|
||||
@@ -1702,24 +1816,31 @@ namespace TabletTester2025.ViewModels
|
||||
// 等待100ms,再更新下一次
|
||||
await Task.Delay(100);
|
||||
}
|
||||
if (Phase != TestPhase.Running)
|
||||
if (!_isFriabilityRunning)
|
||||
throw new InvalidOperationException("脆碎度测试已停止,未保存结果");
|
||||
|
||||
double weightAfter = await ReadFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), "脆碎后重量");
|
||||
SetFriabilityWeightFromPlc(weightAfter: weightAfter);
|
||||
FriabilityCurrentRpm = rpm;
|
||||
LossPercent = TestCalculationService.CalculateFriabilityLossPercent(WeightBefore, WeightAfter);
|
||||
FriabilityPass = LossPercent <= FriabilityMaxLossPercent; //标准值
|
||||
|
||||
bool localLossReady = TryCalculateFriabilityLossFromWeights(out double localLossPercent);
|
||||
if (localLossReady)
|
||||
{
|
||||
LossPercent = localLossPercent;
|
||||
FriabilityPass = LossPercent <= FriabilityMaxLossPercent;
|
||||
}
|
||||
|
||||
bool plcLossReady = await TryRefreshFriabilityLossPercentFromPlcAsync();
|
||||
if (!localLossReady && !plcLossReady)
|
||||
throw new InvalidOperationException("脆碎度失重率数据异常");
|
||||
|
||||
resultReady = true;
|
||||
// 标记测试为已完成
|
||||
Phase = TestPhase.Completed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show($"脆碎度测试出错: {ex.Message}"));
|
||||
Phase = TestPhase.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1729,10 +1850,11 @@ namespace TabletTester2025.ViewModels
|
||||
catch { }
|
||||
}
|
||||
|
||||
Phase = TestPhase.Idle;
|
||||
_isFriabilityRunning = false;
|
||||
RefreshOverallPhase();
|
||||
FriabilityRemainingRounds = FriabilityTargetRounds;
|
||||
if (resultReady)
|
||||
await SaveBatchResult();
|
||||
await SaveBatchResult(TestType.Friability);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1750,21 +1872,24 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
private async Task RunDisintegrationAsync()
|
||||
{
|
||||
if (Phase != TestPhase.Idle) return;
|
||||
if (_isDisintegrationRunning) return;
|
||||
CurrentTest = TestType.Disintegration;
|
||||
Phase = TestPhase.Running;
|
||||
_isDisintegrationRunning = true;
|
||||
RefreshOverallPhase();
|
||||
DisintegrationPass = false; // 添加这一行
|
||||
int tubeCount = Math.Max(1, _plcConfig.DisintegrationCompleteCoils.Length);
|
||||
TubesCompleted = new bool[tubeCount];
|
||||
RemainingTubes = tubeCount;
|
||||
DisintegrationSeconds = 0;
|
||||
DisintegrationActualSeconds = 0;
|
||||
CanSaveDisintegrationResult = false;
|
||||
bool resultReady = false;
|
||||
DateTime startedAt = DateTime.Now;
|
||||
_disintegrationTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_disintegrationTimer.Tick += (s, e) =>
|
||||
{
|
||||
if (Phase == TestPhase.Running)
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
if (_isDisintegrationRunning)
|
||||
SetDisintegrationElapsedSeconds(Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds)));
|
||||
};
|
||||
_disintegrationTimer.Start();
|
||||
|
||||
@@ -1775,52 +1900,107 @@ namespace TabletTester2025.ViewModels
|
||||
await PulseCoilAsync(_plcConfig.DisintegrationStartCoil);
|
||||
int maxSec = ResolveDisintegrationLimitSeconds();
|
||||
DisintegrationTimeMin = maxSec / 60.0;
|
||||
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && Phase == TestPhase.Running)
|
||||
while (RemainingTubes > 0 && DisintegrationSeconds < maxSec && _isDisintegrationRunning)
|
||||
{
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
SetDisintegrationElapsedSeconds(Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds)));
|
||||
await Task.Delay(500);
|
||||
}
|
||||
DisintegrationSeconds = Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds));
|
||||
SetDisintegrationElapsedSeconds(Math.Max(0, (int)Math.Floor((DateTime.Now - startedAt).TotalSeconds)));
|
||||
_disintegrationTimer.Stop();
|
||||
Phase = TestPhase.Completed;
|
||||
resultReady = true;
|
||||
resultReady = _isDisintegrationRunning;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_disintegrationTimer.Stop();
|
||||
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"崩解测试出错: {ex.Message}"));
|
||||
Phase = TestPhase.Error;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Phase = TestPhase.Idle;
|
||||
int maxSec = ResolveDisintegrationLimitSeconds();
|
||||
DisintegrationPass = !_discardDisintegrationResult && RemainingTubes == 0 && DisintegrationSeconds <= maxSec;
|
||||
|
||||
if (resultReady && !_discardDisintegrationResult)
|
||||
await SaveBatchResult();
|
||||
_isDisintegrationRunning = false;
|
||||
RefreshOverallPhase();
|
||||
UpdateDisintegrationPassFromActualTime();
|
||||
CanSaveDisintegrationResult = resultReady && !_discardDisintegrationResult;
|
||||
|
||||
_discardDisintegrationResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetDisintegrationElapsedSeconds(int seconds)
|
||||
{
|
||||
DisintegrationSeconds = seconds;
|
||||
DisintegrationActualSeconds = seconds;
|
||||
DisintegrationActualSecondsText = seconds.ToString("0");
|
||||
}
|
||||
|
||||
private void UpdateDisintegrationPassFromActualTime()
|
||||
{
|
||||
int maxSec = ResolveDisintegrationLimitSeconds();
|
||||
DisintegrationPass = !_discardDisintegrationResult
|
||||
&& RemainingTubes == 0
|
||||
&& double.IsFinite(DisintegrationActualSeconds)
|
||||
&& DisintegrationActualSeconds >= 0
|
||||
&& DisintegrationActualSeconds <= maxSec;
|
||||
}
|
||||
|
||||
private bool TryParseDisintegrationActualSeconds(out double seconds, out string message)
|
||||
{
|
||||
string text = DisintegrationActualSecondsText?.Trim() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
seconds = 0;
|
||||
message = "实际崩解时间不能为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!double.TryParse(text, out seconds) || !double.IsFinite(seconds) || seconds < 0)
|
||||
{
|
||||
message = "实际崩解时间必须为有效的非负秒数。";
|
||||
return false;
|
||||
}
|
||||
|
||||
message = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task SaveDisintegrationResultAsync()
|
||||
{
|
||||
if (!CanSaveDisintegrationResult)
|
||||
return;
|
||||
|
||||
if (!TryParseDisintegrationActualSeconds(out double seconds, out string message))
|
||||
{
|
||||
await App.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show(message, "输入错误", MessageBoxButton.OK, MessageBoxImage.Warning));
|
||||
return;
|
||||
}
|
||||
|
||||
DisintegrationActualSeconds = seconds;
|
||||
UpdateDisintegrationPassFromActualTime();
|
||||
bool saved = await SaveBatchResult(TestType.Disintegration);
|
||||
if (saved)
|
||||
CanSaveDisintegrationResult = false;
|
||||
}
|
||||
|
||||
private async Task StopDisintegrationAsync()
|
||||
{
|
||||
_discardDisintegrationResult = CurrentTest == TestType.Disintegration && Phase == TestPhase.Running;
|
||||
_discardDisintegrationResult = _isDisintegrationRunning;
|
||||
try
|
||||
{
|
||||
await PulseCoilAsync(_plcConfig.DisintegrationStopCoil);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Phase = TestPhase.Idle;
|
||||
_isDisintegrationRunning = false;
|
||||
RefreshOverallPhase();
|
||||
_disintegrationTimer?.Stop();
|
||||
if (_discardDisintegrationResult)
|
||||
CanSaveDisintegrationResult = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetDisintegrationAsync()
|
||||
{
|
||||
bool wasRunning = CurrentTest == TestType.Disintegration && Phase == TestPhase.Running;
|
||||
bool wasRunning = _isDisintegrationRunning;
|
||||
_discardDisintegrationResult = wasRunning;
|
||||
|
||||
try
|
||||
@@ -1833,8 +2013,12 @@ namespace TabletTester2025.ViewModels
|
||||
TubesCompleted = new bool[6];
|
||||
RemainingTubes = 6;
|
||||
DisintegrationSeconds = 0;
|
||||
DisintegrationActualSeconds = 0;
|
||||
DisintegrationActualSecondsText = "0";
|
||||
DisintegrationPass = false;
|
||||
Phase = TestPhase.Idle;
|
||||
CanSaveDisintegrationResult = false;
|
||||
_isDisintegrationRunning = false;
|
||||
RefreshOverallPhase();
|
||||
|
||||
if (!wasRunning)
|
||||
_discardDisintegrationResult = false;
|
||||
@@ -1847,14 +2031,13 @@ namespace TabletTester2025.ViewModels
|
||||
return;
|
||||
|
||||
CurrentTest = TestType.Dissolution;
|
||||
Phase = TestPhase.Running;
|
||||
DissolutionPass = false;
|
||||
ResetDissolutionChannel(1);
|
||||
ResetDissolutionSampleState(1);
|
||||
ResetDissolutionRunClock(1);
|
||||
CreateDissolutionSampleSchedule(1);
|
||||
_dissolution1StartTime = DateTime.Now;
|
||||
_isDissolution1Running = true;
|
||||
SetDissolutionRunning(1, true);
|
||||
ResumeDissolutionRunClock(1);
|
||||
DissolutionPlotModel.Title = "溶出曲线";
|
||||
await WriteDissolutionTimeAsync(_plcConfig.Dissolution1Time, Dissolution1TimeMin);
|
||||
@@ -1872,10 +2055,10 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDissolution1Running = false;
|
||||
SetDissolutionRunning(1, false);
|
||||
ResetDissolutionSampleState(1);
|
||||
_dissolution1LastRunUpdate = DateTime.MinValue;
|
||||
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
}
|
||||
@@ -1888,11 +2071,11 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDissolution1Running = false;
|
||||
SetDissolutionRunning(1, false);
|
||||
ResetDissolutionChannel(1);
|
||||
ResetDissolutionSampleState(1);
|
||||
ResetDissolutionRunClock(1);
|
||||
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
}
|
||||
@@ -1903,14 +2086,13 @@ namespace TabletTester2025.ViewModels
|
||||
return;
|
||||
|
||||
CurrentTest = TestType.Dissolution;
|
||||
Phase = TestPhase.Running;
|
||||
DissolutionPass = false;
|
||||
ResetDissolutionChannel(2);
|
||||
ResetDissolutionSampleState(2);
|
||||
ResetDissolutionRunClock(2);
|
||||
CreateDissolutionSampleSchedule(2);
|
||||
_dissolution2StartTime = DateTime.Now;
|
||||
_isDissolution2Running = true;
|
||||
SetDissolutionRunning(2, true);
|
||||
ResumeDissolutionRunClock(2);
|
||||
DissolutionPlotModel.Title = "溶出曲线";
|
||||
await WriteDissolutionTimeAsync(_plcConfig.Dissolution2Time, Dissolution2TimeMin);
|
||||
@@ -1928,10 +2110,10 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDissolution2Running = false;
|
||||
SetDissolutionRunning(2, false);
|
||||
ResetDissolutionSampleState(2);
|
||||
_dissolution2LastRunUpdate = DateTime.MinValue;
|
||||
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
}
|
||||
@@ -1944,11 +2126,11 @@ namespace TabletTester2025.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDissolution2Running = false;
|
||||
SetDissolutionRunning(2, false);
|
||||
ResetDissolutionChannel(2);
|
||||
ResetDissolutionSampleState(2);
|
||||
ResetDissolutionRunClock(2);
|
||||
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
|
||||
RefreshOverallPhase();
|
||||
UpdateDissolutionClock();
|
||||
}
|
||||
}
|
||||
@@ -2006,17 +2188,18 @@ namespace TabletTester2025.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
double rsquared = CalculateRSquared(times, values);
|
||||
_dissolutionResultRate30Min = rate30Min;
|
||||
_dissolutionResultRSquared = CalculateRSquared(times, values);
|
||||
DissolutionPercent = _dissolutionResultRate30Min;
|
||||
DissolutionRSquared = _dissolutionResultRSquared;
|
||||
_dissolutionResultRSquared = rsquared;
|
||||
DissolutionPercent = rate30Min;
|
||||
DissolutionRSquared = rsquared;
|
||||
if (channel == 1)
|
||||
Dissolution1RSquared = _dissolutionResultRSquared;
|
||||
Dissolution1RSquared = rsquared;
|
||||
else
|
||||
Dissolution2RSquared = _dissolutionResultRSquared;
|
||||
Dissolution2RSquared = rsquared;
|
||||
|
||||
DissolutionPass = _dissolutionResultRate30Min >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
|
||||
await SaveBatchResult();
|
||||
DissolutionPass = rate30Min >= App.CurrentPharmaParams.DissolutionMinPercentAt30min;
|
||||
await SaveBatchResult(TestType.Dissolution, _dissolutionResultChannel, rate30Min, rsquared);
|
||||
}
|
||||
|
||||
private async Task RunDissolutionAsync()
|
||||
@@ -2024,16 +2207,24 @@ namespace TabletTester2025.ViewModels
|
||||
await StartDissolution1Async();
|
||||
}
|
||||
|
||||
private async Task SaveBatchResult(bool? forcedQualified = null)
|
||||
private async Task<bool> SaveBatchResult(
|
||||
TestType testType,
|
||||
string dissolutionChannel = "",
|
||||
double? dissolutionRate30MinOverride = null,
|
||||
double? dissolutionRSquaredOverride = null,
|
||||
bool? forcedQualified = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
double dissolutionRate30Min = CurrentTest == TestType.Dissolution
|
||||
? _dissolutionResultRate30Min
|
||||
double dissolutionRate30Min = testType == TestType.Dissolution
|
||||
? dissolutionRate30MinOverride ?? _dissolutionResultRate30Min
|
||||
: DissolutionPercent;
|
||||
double rsquared = CurrentTest == TestType.Dissolution
|
||||
? _dissolutionResultRSquared
|
||||
double rsquared = testType == TestType.Dissolution
|
||||
? dissolutionRSquaredOverride ?? _dissolutionResultRSquared
|
||||
: DissolutionRSquared;
|
||||
string effectiveDissolutionChannel = testType == TestType.Dissolution
|
||||
? dissolutionChannel
|
||||
: "";
|
||||
|
||||
var batch = new TestBatch
|
||||
{
|
||||
@@ -2062,7 +2253,9 @@ namespace TabletTester2025.ViewModels
|
||||
WeightAfter = WeightAfter,
|
||||
|
||||
// 崩解
|
||||
DisintegrationTimeSec = DisintegrationSeconds,
|
||||
DisintegrationTimeSec = testType == TestType.Disintegration
|
||||
? DisintegrationActualSeconds
|
||||
: DisintegrationSeconds,
|
||||
RemainingTubesAtEnd = RemainingTubes,
|
||||
DisintegrationTargetFreq = DisintegrationSpeedRpm,
|
||||
DisintegrationTemp = DisintegrationTemp,
|
||||
@@ -2070,11 +2263,11 @@ namespace TabletTester2025.ViewModels
|
||||
DisintegrationLimitSeconds = ResolveDisintegrationLimitSeconds(),
|
||||
|
||||
// 溶出
|
||||
DissolutionChannel = CurrentTest == TestType.Dissolution ? _dissolutionResultChannel : "",
|
||||
DissolutionChannel = effectiveDissolutionChannel,
|
||||
DissolutionRate30Min = dissolutionRate30Min,
|
||||
DissolutionTargetRpm = DissolutionTargetRpm,
|
||||
DissolutionRSquared = rsquared,
|
||||
DissolutionSampleInterval = CurrentTest == TestType.Dissolution && _dissolutionResultChannel == "溶出2"
|
||||
DissolutionSampleInterval = testType == TestType.Dissolution && effectiveDissolutionChannel == "溶出2"
|
||||
? ToCompatibleSampleInterval(Dissolution2SampleIntervalMin)
|
||||
: ToCompatibleSampleInterval(Dissolution1SampleIntervalMin),
|
||||
Dissolution1SampleInterval = Dissolution1SampleIntervalMin,
|
||||
@@ -2084,7 +2277,7 @@ namespace TabletTester2025.ViewModels
|
||||
FriabilityPass = FriabilityPass,
|
||||
DisintegrationPass = DisintegrationPass,
|
||||
DissolutionPass = DissolutionPass,
|
||||
TestType = CurrentTest switch
|
||||
TestType = testType switch
|
||||
{
|
||||
TestType.Hardness => "硬度",
|
||||
TestType.Friability => "脆碎度",
|
||||
@@ -2093,19 +2286,19 @@ namespace TabletTester2025.ViewModels
|
||||
_ => ""
|
||||
},
|
||||
|
||||
IsQualified = TestCalculationService.ResolveCurrentTestQualified(
|
||||
CurrentTest,
|
||||
IsQualified = forcedQualified ?? TestCalculationService.ResolveCurrentTestQualified(
|
||||
testType,
|
||||
HardnessPass,
|
||||
FriabilityPass,
|
||||
DisintegrationPass,
|
||||
DissolutionPass)
|
||||
};
|
||||
var dissolutionSamples = CurrentTest == TestType.Dissolution
|
||||
var dissolutionSamples = testType == TestType.Dissolution
|
||||
? DissolutionSamplePoints
|
||||
.Where(s => s.ChannelName == _dissolutionResultChannel && s.Percent.HasValue)
|
||||
.Where(s => s.ChannelName == effectiveDissolutionChannel && s.Percent.HasValue)
|
||||
.ToList()
|
||||
: new List<DissolutionSamplePoint>();
|
||||
var hardnessSamples = CurrentTest == TestType.Hardness
|
||||
var hardnessSamples = testType == TestType.Hardness
|
||||
? HardnessSamplePoints.ToList()
|
||||
: new List<HardnessSamplePoint>();
|
||||
|
||||
@@ -2114,20 +2307,22 @@ namespace TabletTester2025.ViewModels
|
||||
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
string projectName = CurrentTest switch
|
||||
string projectName = testType switch
|
||||
{
|
||||
TestType.Hardness => "硬度",
|
||||
TestType.Friability => "脆碎度",
|
||||
TestType.Disintegration => "崩解",
|
||||
TestType.Dissolution => string.IsNullOrWhiteSpace(_dissolutionResultChannel) ? "溶出" : _dissolutionResultChannel,
|
||||
TestType.Dissolution => string.IsNullOrWhiteSpace(effectiveDissolutionChannel) ? "溶出" : effectiveDissolutionChannel,
|
||||
_ => ""
|
||||
};
|
||||
LocalAlarm = $"{projectName}测试完成";
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await App.Current.Dispatcher.InvokeAsync(() => MessageBox.Show($"保存测试结果失败:{ex.Message}"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user