更新2026

This commit is contained in:
GukSang.Jin
2026-05-20 11:13:12 +08:00
parent 61420da42e
commit 954afaaf39
6 changed files with 429 additions and 181 deletions

View File

@@ -34,6 +34,7 @@
public ushort FriabilityTestTime { get; set; } public ushort FriabilityTestTime { get; set; }
public ushort FriabilityWeightBefore { get; set; } public ushort FriabilityWeightBefore { get; set; }
public ushort FriabilityWeightAfter { get; set; } public ushort FriabilityWeightAfter { get; set; }
public ushort FriabilityLossPercent { get; set; }
public ushort WeightBefore { get; set; } // 天平重量寄存器(可选) public ushort WeightBefore { get; set; } // 天平重量寄存器(可选)
public ushort WeightAfter { get; set; } public ushort WeightAfter { get; set; }
public ushort FriabilityStartCoil2 { get; set; } public ushort FriabilityStartCoil2 { get; set; }

View File

@@ -21,6 +21,7 @@ namespace TabletTester2025.Services
410 => 100f, // 脆碎圈数 410 => 100f, // 脆碎圈数
412 => 5.0f + (float)_rand.NextDouble() * 2, // 脆碎度前重 412 => 5.0f + (float)_rand.NextDouble() * 2, // 脆碎度前重
414 => 4.9f + (float)_rand.NextDouble() * 2, // 后重 414 => 4.9f + (float)_rand.NextDouble() * 2, // 后重
416 => 1.0f, // 失重率%
300 => 37.0f, // 温度 300 => 37.0f, // 温度
340 => 50f, // 溶出速度1(r/min) 340 => 50f, // 溶出速度1(r/min)
350 => 50f, // 溶出速度2(r/min) 350 => 50f, // 溶出速度2(r/min)

View File

@@ -6,9 +6,13 @@ namespace TabletTester2025.ViewModels
public class HardnessDisplaySamplePoint : ObservableObject public class HardnessDisplaySamplePoint : ObservableObject
{ {
private int _groupNo; private int _groupNo;
private int _cumulativeNo;
private int _sequenceNo; private int _sequenceNo;
private double _value; private double _value;
private double _deviationFromAverage; private double _deviationFromAverage;
private double _groupAverage;
private double _groupAverageDeviation;
private double _groupRSD;
private DateTime _recordedAt; private DateTime _recordedAt;
public int GroupNo public int GroupNo
@@ -17,6 +21,12 @@ namespace TabletTester2025.ViewModels
set => SetProperty(ref _groupNo, value); set => SetProperty(ref _groupNo, value);
} }
public int CumulativeNo
{
get => _cumulativeNo;
set => SetProperty(ref _cumulativeNo, value);
}
public int SequenceNo public int SequenceNo
{ {
get => _sequenceNo; get => _sequenceNo;
@@ -35,6 +45,24 @@ namespace TabletTester2025.ViewModels
set => SetProperty(ref _deviationFromAverage, value); set => SetProperty(ref _deviationFromAverage, value);
} }
public double GroupAverage
{
get => _groupAverage;
set => SetProperty(ref _groupAverage, value);
}
public double GroupAverageDeviation
{
get => _groupAverageDeviation;
set => SetProperty(ref _groupAverageDeviation, value);
}
public double GroupRSD
{
get => _groupRSD;
set => SetProperty(ref _groupRSD, value);
}
public DateTime RecordedAt public DateTime RecordedAt
{ {
get => _recordedAt; get => _recordedAt;

View File

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

View File

@@ -284,15 +284,14 @@
</UniformGrid> </UniformGrid>
</GroupBox> </GroupBox>
<Grid Grid.Row="1"> <GroupBox Header="测试结果" Grid.Row="1">
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<GroupBox Header="测试结果" Grid.Row="0"> <UniformGrid Grid.Row="0" Columns="4">
<StackPanel>
<UniformGrid Columns="4">
<Border Style="{StaticResource MetricCard}"> <Border Style="{StaticResource MetricCard}">
<StackPanel> <StackPanel>
<TextBlock Text="实时力(N)" Style="{StaticResource MetricLabel}"/> <TextBlock Text="实时力(N)" Style="{StaticResource MetricLabel}"/>
@@ -331,30 +330,43 @@
</Border> </Border>
<Border Style="{StaticResource MetricCard}"> <Border Style="{StaticResource MetricCard}">
<StackPanel> <StackPanel>
<TextBlock Text="测试次数" Style="{StaticResource MetricLabel}"/> <TextBlock Text="本组次数" Style="{StaticResource MetricLabel}"/>
<TextBlock Text="{Binding HardnessCurrentCount}" Style="{StaticResource MetricValue}"/> <TextBlock Text="{Binding HardnessCurrentCount}" Style="{StaticResource MetricValue}"/>
</StackPanel> </StackPanel>
</Border> </Border>
<Border Style="{StaticResource MetricCard}">
<StackPanel>
<TextBlock Text="累计次数" Style="{StaticResource MetricLabel}"/>
<TextBlock Text="{Binding HardnessTotalCount}" Style="{StaticResource MetricValue}"/>
</StackPanel>
</Border>
</UniformGrid> </UniformGrid>
<DataGrid ItemsSource="{Binding HardnessDisplaySamplePoints}"
<DataGrid Grid.Row="1"
ItemsSource="{Binding HardnessDisplaySamplePoints}"
AutoGenerateColumns="False" AutoGenerateColumns="False"
CanUserAddRows="False" CanUserAddRows="False"
CanUserDeleteRows="False" CanUserDeleteRows="False"
IsReadOnly="True" IsReadOnly="True"
HeadersVisibility="Column" HeadersVisibility="Column"
Margin="10,8,10,10" Margin="10,8,10,10"
MaxHeight="190"> MinHeight="160"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="组号" Binding="{Binding GroupNo}" Width="70"/> <DataGridTextColumn Header="累计次数" Binding="{Binding CumulativeNo}" Width="85"/>
<DataGridTextColumn Header="号" Binding="{Binding SequenceNo}" Width="80"/> <DataGridTextColumn Header="号" Binding="{Binding GroupNo}" Width="65"/>
<DataGridTextColumn Header="硬度值(N)" Binding="{Binding Value, StringFormat=F1}" Width="*"/> <DataGridTextColumn Header="组内序号" Binding="{Binding SequenceNo}" Width="80"/>
<DataGridTextColumn Header="与平均值偏差(N)" Binding="{Binding DeviationFromAverage, StringFormat=F2}" Width="*"/> <DataGridTextColumn Header="硬度值(N)" Binding="{Binding Value, StringFormat=F1}" Width="95"/>
<DataGridTextColumn Header="记录时间" Binding="{Binding RecordedAt, StringFormat=HH:mm:ss}" Width="120"/> <DataGridTextColumn Header="与平均值偏差(N)" Binding="{Binding DeviationFromAverage, StringFormat=F2}" Width="125"/>
<DataGridTextColumn Header="组平均值(N)" Binding="{Binding GroupAverage, StringFormat=F1}" Width="105"/>
<DataGridTextColumn Header="组平均偏差(N)" Binding="{Binding GroupAverageDeviation, StringFormat=F2}" Width="120"/>
<DataGridTextColumn Header="组RSD(%)" Binding="{Binding GroupRSD, StringFormat=F2}" Width="95"/>
<DataGridTextColumn Header="记录时间" Binding="{Binding RecordedAt, StringFormat=HH:mm:ss}" Width="105"/>
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
</StackPanel>
</GroupBox>
</Grid> </Grid>
</GroupBox>
<WrapPanel Grid.Row="2" Style="{StaticResource CommandBar}"> <WrapPanel Grid.Row="2" Style="{StaticResource CommandBar}">
<Button Command="{Binding StartHardnessCommand}" Content="开始" Style="{StaticResource StartButton}"/> <Button Command="{Binding StartHardnessCommand}" Content="开始" Style="{StaticResource StartButton}"/>
@@ -420,7 +432,7 @@
</Border> </Border>
<Border Style="{StaticResource MetricCard}"> <Border Style="{StaticResource MetricCard}">
<StackPanel> <StackPanel>
<TextBlock Text="脆碎度(%)" Style="{StaticResource MetricLabel}"/> <TextBlock Text="失重率(%)" Style="{StaticResource MetricLabel}"/>
<TextBlock Text="{Binding LossPercent, StringFormat=F2}" Foreground="#1565C0" Style="{StaticResource MetricValue}"/> <TextBlock Text="{Binding LossPercent, StringFormat=F2}" Foreground="#1565C0" Style="{StaticResource MetricValue}"/>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -631,11 +643,20 @@
</GroupBox> </GroupBox>
<GroupBox Header="测试结果" Grid.Row="1"> <GroupBox Header="测试结果" Grid.Row="1">
<UniformGrid Columns="2"> <UniformGrid Columns="1">
<Border Style="{StaticResource MetricCard}"> <Border Style="{StaticResource MetricCard}">
<StackPanel> <StackPanel>
<TextBlock Text="崩解时间(秒)" Style="{StaticResource MetricLabel}"/> <TextBlock Text="实际崩解时间(秒)" Style="{StaticResource MetricLabel}"/>
<TextBlock Text="{Binding DisintegrationSeconds}" Style="{StaticResource MetricValue}"/> <TextBox Text="{Binding DisintegrationActualSecondsText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="160"
Height="46"
Margin="0,8,0,0"
HorizontalAlignment="Center"
IsEnabled="{Binding CanSaveDisintegrationResult}"
TextAlignment="Center"
FontSize="26"
FontWeight="SemiBold"
Foreground="{StaticResource ValueBrush}"/>
</StackPanel> </StackPanel>
</Border> </Border>
<!--<Border Style="{StaticResource MetricCard}"> <!--<Border Style="{StaticResource MetricCard}">
@@ -650,6 +671,7 @@
<WrapPanel Grid.Row="2" Style="{StaticResource CommandBar}"> <WrapPanel Grid.Row="2" Style="{StaticResource CommandBar}">
<Button Command="{Binding StartDisintegrationCommand}" Content="开始" Style="{StaticResource StartButton}"/> <Button Command="{Binding StartDisintegrationCommand}" Content="开始" Style="{StaticResource StartButton}"/>
<Button Command="{Binding StopDisintegrationCommand}" Content="停止" Style="{StaticResource StopButton}"/> <Button Command="{Binding StopDisintegrationCommand}" Content="停止" Style="{StaticResource StopButton}"/>
<Button Command="{Binding SaveDisintegrationResultCommand}" Content="保存记录" Style="{StaticResource SecondaryButton}" IsEnabled="{Binding CanSaveDisintegrationResult}"/>
<!--<Button Command="{Binding ResetDisintegrationCommand}" Content="复位" Style="{StaticResource ResetButton}"/>--> <!--<Button Command="{Binding ResetDisintegrationCommand}" Content="复位" Style="{StaticResource ResetButton}"/>-->
</WrapPanel> </WrapPanel>
</Grid> </Grid>

View File

@@ -43,6 +43,7 @@
"FriabilityTestTime": 0, // 脆碎试验时间由药典参数计算D410用于脆碎圈数 "FriabilityTestTime": 0, // 脆碎试验时间由药典参数计算D410用于脆碎圈数
"FriabilityWeightBefore": 412, // 脆碎前质量(g) "FriabilityWeightBefore": 412, // 脆碎前质量(g)
"FriabilityWeightAfter": 414, // 脆碎后质量(g) "FriabilityWeightAfter": 414, // 脆碎后质量(g)
"FriabilityLossPercent": 416, // 失重率(%)
"WeightBefore": 412, "WeightBefore": 412,
"WeightAfter": 414, "WeightAfter": 414,
"DisintegrationTemp": 1430, // 所有温度显示D1430float类型 "DisintegrationTemp": 1430, // 所有温度显示D1430float类型