This commit is contained in:
GukSang.Jin
2026-05-20 10:16:02 +08:00
parent 070463ae8e
commit 61420da42e
5 changed files with 281 additions and 129 deletions

View File

@@ -46,6 +46,10 @@ namespace TabletTester2025.ViewModels
public ObservableCollection<DissolutionSamplePoint> DissolutionSamplePoints { get; } = new();
private DateTime _dissolution1StartTime = DateTime.MinValue;
private DateTime _dissolution2StartTime = DateTime.MinValue;
private DateTime _dissolution1LastRunUpdate = DateTime.MinValue;
private DateTime _dissolution2LastRunUpdate = DateTime.MinValue;
private double _dissolution1ElapsedRunMinutes;
private double _dissolution2ElapsedRunMinutes;
private bool _isDissolution1Running;
private bool _isDissolution2Running;
private bool _dissolution1SampleRequestActive;
@@ -155,6 +159,10 @@ namespace TabletTester2025.ViewModels
[ObservableProperty] private double _dissolutionMinPercentAt30Min = 80;
[ObservableProperty] private double _dissolutionElapsedTime;
[ObservableProperty] private double _dissolutionCountdown;
[ObservableProperty] private double _dissolution1ElapsedTime;
[ObservableProperty] private double _dissolution2ElapsedTime;
[ObservableProperty] private double _dissolution1Countdown;
[ObservableProperty] private double _dissolution2Countdown;
[ObservableProperty] private double _dissolutionRSquared;
[ObservableProperty] private double _dissolution1RSquared;
[ObservableProperty] private double _dissolution2RSquared;
@@ -441,41 +449,47 @@ namespace TabletTester2025.ViewModels
private async Task CheckDissolutionSampleAsync(int channel)
{
ushort coilAddress = channel == 1
? _plcConfig.Dissolution1SampleAckCoil
: _plcConfig.Dissolution2SampleAckCoil;
if (coilAddress == 0)
{
DissolutionCurveStatus = $"溶出{channel}取样确认线圈未配置";
return;
}
bool sampleRequested = await _plc.ReadCoilAsync(coilAddress);
if (!sampleRequested)
{
SetDissolutionSampleRequestActive(channel, false);
return;
}
if (IsDissolutionSampleRequestActive(channel) || IsDissolutionSamplePromptOpen(channel))
return;
AccumulateDissolutionRunTime(channel, DateTime.Now);
var nextSample = GetNextPendingDissolutionSample(channel);
if (nextSample == null || GetDissolutionElapsedMinutes(channel) + 0.0001 < nextSample.ScheduledTimeMin)
return;
SetDissolutionSampleRequestActive(channel, true);
SetDissolutionSamplePromptOpen(channel, true);
PauseDissolutionRunClock(channel);
try
{
await PulseCoilAsync(ResolveDissolutionStopCoil(channel));
double percent = await ShowDissolutionSampleDialogAsync(channel);
RecordDissolutionSample(channel, percent);
await _plc.WriteCoilAsync(coilAddress, false);
SetDissolutionSampleRequestActive(channel, false);
LocalAlarm = $"溶出{channel}已记录取样结果";
DissolutionCurveStatus = "";
if (IsDissolutionChannelComplete(channel))
{
await FinalizeDissolutionChannelAsync(channel);
SetDissolutionRunning(channel, false);
ResetDissolutionSampleState(channel);
Phase = IsAnyDissolutionRunning() ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
}
else
{
await PulseCoilAsync(ResolveDissolutionStartCoil(channel));
ResumeDissolutionRunClock(channel);
}
}
catch (Exception ex)
{
SetDissolutionSampleRequestActive(channel, false);
SetDissolutionRunning(channel, false);
Phase = IsAnyDissolutionRunning() ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
await App.Current.Dispatcher.InvokeAsync(() =>
MessageBox.Show($"溶出{channel}取样确认失败:{ex.Message}", "取样确认失败", MessageBoxButton.OK, MessageBoxImage.Error));
}
@@ -511,6 +525,96 @@ namespace TabletTester2025.ViewModels
_isDissolution2SamplePromptOpen = value;
}
private ushort ResolveDissolutionStartCoil(int channel)
{
return channel == 1 ? _plcConfig.Dissolution1StartCoil : _plcConfig.Dissolution2StartCoil;
}
private ushort ResolveDissolutionStopCoil(int channel)
{
return channel == 1 ? _plcConfig.Dissolution1StopCoil : _plcConfig.Dissolution2StopCoil;
}
private bool IsAnyDissolutionRunning()
{
return _isDissolution1Running || _isDissolution2Running;
}
private void SetDissolutionRunning(int channel, bool value)
{
if (channel == 1)
_isDissolution1Running = value;
else
_isDissolution2Running = value;
}
private void ResetDissolutionRunClock(int channel)
{
if (channel == 1)
{
_dissolution1ElapsedRunMinutes = 0;
_dissolution1LastRunUpdate = DateTime.MinValue;
Dissolution1ElapsedTime = 0;
Dissolution1Countdown = 0;
}
else
{
_dissolution2ElapsedRunMinutes = 0;
_dissolution2LastRunUpdate = DateTime.MinValue;
Dissolution2ElapsedTime = 0;
Dissolution2Countdown = 0;
}
UpdateDissolutionClock();
}
private void ResumeDissolutionRunClock(int channel)
{
if (channel == 1)
_dissolution1LastRunUpdate = DateTime.Now;
else
_dissolution2LastRunUpdate = DateTime.Now;
UpdateDissolutionClock();
}
private void PauseDissolutionRunClock(int channel)
{
AccumulateDissolutionRunTime(channel, DateTime.Now);
if (channel == 1)
_dissolution1LastRunUpdate = DateTime.MinValue;
else
_dissolution2LastRunUpdate = DateTime.MinValue;
UpdateDissolutionClock();
}
private void AccumulateDissolutionRunTime(int channel, DateTime now)
{
if (channel == 1)
{
if (_isDissolution1Running && _dissolution1LastRunUpdate != DateTime.MinValue)
{
_dissolution1ElapsedRunMinutes = Math.Max(0, _dissolution1ElapsedRunMinutes + (now - _dissolution1LastRunUpdate).TotalMinutes);
_dissolution1LastRunUpdate = now;
}
}
else
{
if (_isDissolution2Running && _dissolution2LastRunUpdate != DateTime.MinValue)
{
_dissolution2ElapsedRunMinutes = Math.Max(0, _dissolution2ElapsedRunMinutes + (now - _dissolution2LastRunUpdate).TotalMinutes);
_dissolution2LastRunUpdate = now;
}
}
}
private bool IsDissolutionChannelComplete(int channel)
{
return GetNextPendingDissolutionSample(channel) == null
|| GetDissolutionElapsedMinutes(channel) + 0.0001 >= GetDissolutionDurationMinutes(channel);
}
private async Task<double> ShowDissolutionSampleDialogAsync(int channel)
{
double? result = await App.Current.Dispatcher.InvokeAsync<double?>(() =>
@@ -623,7 +727,7 @@ namespace TabletTester2025.ViewModels
});
if (!result.HasValue)
throw new InvalidOperationException("取样结果未录入,未写入确认线圈");
throw new InvalidOperationException("取样结果未录入");
return result.Value;
}
@@ -694,35 +798,28 @@ namespace TabletTester2025.ViewModels
private List<double> ResolveDissolutionSampleTimes(int channel)
{
int durationMin = channel == 1 ? Dissolution1TimeMin : Dissolution2TimeMin;
double intervalMin = channel == 1 ? Dissolution1SampleIntervalMin : Dissolution2SampleIntervalMin;
durationMin = Math.Max(1, durationMin);
var configuredTimes = App.CurrentPharmaParams.DissolutionSampleTimes?
.Where(t => t > 0)
.Select(t => (double)t)
.ToList();
var times = configuredTimes != null && configuredTimes.Count > 0
? configuredTimes
: GenerateIntervalSampleTimes(durationMin, intervalMin);
times.Add(Math.Min(30, durationMin));
if (durationMin >= 30)
times.Add(30);
times.Add(durationMin);
return times
.Where(t => t > 0 && t <= durationMin)
.Distinct()
.OrderBy(t => t)
.ToList();
return GenerateIntervalSampleTimes(
GetDissolutionDurationMinutes(channel),
GetDissolutionIntervalMinutes(channel));
}
private static List<double> GenerateIntervalSampleTimes(int durationMin, double intervalMin)
private double GetDissolutionDurationMinutes(int channel)
{
return Math.Max(1, channel == 1 ? Dissolution1TimeMin : Dissolution2TimeMin);
}
private double GetDissolutionIntervalMinutes(int channel)
{
double intervalMin = channel == 1 ? Dissolution1SampleIntervalMin : Dissolution2SampleIntervalMin;
return double.IsFinite(intervalMin) && intervalMin > 0 ? intervalMin : 5;
}
private static List<double> GenerateIntervalSampleTimes(double durationMin, double intervalMin)
{
if (!double.IsFinite(intervalMin) || intervalMin <= 0)
intervalMin = 5;
if (!double.IsFinite(durationMin) || durationMin <= 0)
durationMin = 1;
var times = new List<double>();
for (double time = intervalMin; time <= durationMin + 0.0001; time += intervalMin)
@@ -753,8 +850,7 @@ namespace TabletTester2025.ViewModels
private double GetDissolutionElapsedMinutes(int channel)
{
DateTime startTime = channel == 1 ? _dissolution1StartTime : _dissolution2StartTime;
return startTime == DateTime.MinValue ? 0 : Math.Max(0, (DateTime.Now - startTime).TotalMinutes);
return channel == 1 ? _dissolution1ElapsedRunMinutes : _dissolution2ElapsedRunMinutes;
}
private void RecordDissolutionSample(int channel, double percent)
@@ -820,24 +916,44 @@ namespace TabletTester2025.ViewModels
private void UpdateDissolutionClock()
{
var now = DateTime.Now;
double elapsed1 = _isDissolution1Running && _dissolution1StartTime != DateTime.MinValue
? (now - _dissolution1StartTime).TotalMinutes
: 0;
double elapsed2 = _isDissolution2Running && _dissolution2StartTime != DateTime.MinValue
? (now - _dissolution2StartTime).TotalMinutes
: 0;
AccumulateDissolutionRunTime(1, now);
AccumulateDissolutionRunTime(2, now);
double elapsed1 = _dissolution1ElapsedRunMinutes;
double elapsed2 = _dissolution2ElapsedRunMinutes;
Dissolution1ElapsedTime = elapsed1;
Dissolution2ElapsedTime = elapsed2;
DissolutionElapsedTime = Math.Max(elapsed1, elapsed2);
Dissolution1Countdown = ResolveDissolutionCountdown(1);
Dissolution2Countdown = ResolveDissolutionCountdown(2);
var remaining = new List<double>();
if (_isDissolution1Running)
remaining.Add(Math.Max(0, Dissolution1TimeMin - elapsed1));
remaining.Add(Dissolution1Countdown);
if (_isDissolution2Running)
remaining.Add(Math.Max(0, Dissolution2TimeMin - elapsed2));
remaining.Add(Dissolution2Countdown);
DissolutionCountdown = remaining.Count == 0 ? 0 : remaining.Min();
}
private double ResolveDissolutionCountdown(int channel)
{
if (channel == 1 && !_isDissolution1Running)
return 0;
if (channel == 2 && !_isDissolution2Running)
return 0;
double elapsed = GetDissolutionElapsedMinutes(channel);
double totalRemaining = Math.Max(0, GetDissolutionDurationMinutes(channel) - elapsed);
var nextSample = GetNextPendingDissolutionSample(channel);
if (nextSample == null)
return totalRemaining;
return Math.Max(0, Math.Min(nextSample.ScheduledTimeMin - elapsed, totalRemaining));
}
partial void OnDissolution1TimeMinChanged(int value)
{
if (_isLoadingDissolution1Time || _plcConfig.Dissolution1Time == 0 || value <= 0)
@@ -927,7 +1043,7 @@ namespace TabletTester2025.ViewModels
try
{
_isLoadingDissolution1SampleInterval = true;
int value = await _plc.ReadIntAsync(_plcConfig.Dissolution1SampleInterval);
double value = await _plc.ReadFloatAsync(_plcConfig.Dissolution1SampleInterval);
if (value > 0)
{
Dissolution1SampleIntervalMin = value;
@@ -946,7 +1062,7 @@ namespace TabletTester2025.ViewModels
try
{
_isLoadingDissolution2SampleInterval = true;
int value = await _plc.ReadIntAsync(_plcConfig.Dissolution2SampleInterval);
double value = await _plc.ReadFloatAsync(_plcConfig.Dissolution2SampleInterval);
if (value > 0)
Dissolution2SampleIntervalMin = value;
}
@@ -965,7 +1081,7 @@ namespace TabletTester2025.ViewModels
try
{
await _plc.WriteRegisterAsync(registerAddress, (ushort)Math.Clamp(ToCompatibleSampleInterval(value), 1, ushort.MaxValue));
await _plc.WriteFloatAsync(registerAddress, (float)value);
}
catch { }
}
@@ -1735,9 +1851,11 @@ namespace TabletTester2025.ViewModels
DissolutionPass = false;
ResetDissolutionChannel(1);
ResetDissolutionSampleState(1);
ResetDissolutionRunClock(1);
CreateDissolutionSampleSchedule(1);
_dissolution1StartTime = DateTime.Now;
_isDissolution1Running = true;
ResumeDissolutionRunClock(1);
DissolutionPlotModel.Title = "溶出曲线";
await WriteDissolutionTimeAsync(_plcConfig.Dissolution1Time, Dissolution1TimeMin);
await WriteDissolutionSampleIntervalAsync(_plcConfig.Dissolution1SampleInterval, Dissolution1SampleIntervalMin);
@@ -1748,6 +1866,7 @@ namespace TabletTester2025.ViewModels
{
try
{
PauseDissolutionRunClock(1);
await PulseCoilAsync(_plcConfig.Dissolution1StopCoil);
await FinalizeDissolutionChannelAsync(1);
}
@@ -1755,6 +1874,7 @@ namespace TabletTester2025.ViewModels
{
_isDissolution1Running = false;
ResetDissolutionSampleState(1);
_dissolution1LastRunUpdate = DateTime.MinValue;
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
}
@@ -1771,6 +1891,7 @@ namespace TabletTester2025.ViewModels
_isDissolution1Running = false;
ResetDissolutionChannel(1);
ResetDissolutionSampleState(1);
ResetDissolutionRunClock(1);
Phase = _isDissolution2Running ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
}
@@ -1786,9 +1907,11 @@ namespace TabletTester2025.ViewModels
DissolutionPass = false;
ResetDissolutionChannel(2);
ResetDissolutionSampleState(2);
ResetDissolutionRunClock(2);
CreateDissolutionSampleSchedule(2);
_dissolution2StartTime = DateTime.Now;
_isDissolution2Running = true;
ResumeDissolutionRunClock(2);
DissolutionPlotModel.Title = "溶出曲线";
await WriteDissolutionTimeAsync(_plcConfig.Dissolution2Time, Dissolution2TimeMin);
await WriteDissolutionSampleIntervalAsync(_plcConfig.Dissolution2SampleInterval, Dissolution2SampleIntervalMin);
@@ -1799,6 +1922,7 @@ namespace TabletTester2025.ViewModels
{
try
{
PauseDissolutionRunClock(2);
await PulseCoilAsync(_plcConfig.Dissolution2StopCoil);
await FinalizeDissolutionChannelAsync(2);
}
@@ -1806,6 +1930,7 @@ namespace TabletTester2025.ViewModels
{
_isDissolution2Running = false;
ResetDissolutionSampleState(2);
_dissolution2LastRunUpdate = DateTime.MinValue;
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
}
@@ -1822,6 +1947,7 @@ namespace TabletTester2025.ViewModels
_isDissolution2Running = false;
ResetDissolutionChannel(2);
ResetDissolutionSampleState(2);
ResetDissolutionRunClock(2);
Phase = _isDissolution1Running ? TestPhase.Running : TestPhase.Idle;
UpdateDissolutionClock();
}