From 61420da42e50ff2656926e5d90aaacca7de63f67 Mon Sep 17 00:00:00 2001 From: "GukSang.Jin" Date: Wed, 20 May 2026 10:16:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ViewModels/StationViewModel.cs | 238 +++++++++++++++++++++++++-------- Views/MainWindow.xaml | 36 ++++- Views/SettingsWindow.xaml | 31 +---- Views/SettingsWindow.xaml.cs | 101 ++++++++------ appsettings.json | 4 +- 5 files changed, 281 insertions(+), 129 deletions(-) diff --git a/ViewModels/StationViewModel.cs b/ViewModels/StationViewModel.cs index a73971c..c7e54ce 100644 --- a/ViewModels/StationViewModel.cs +++ b/ViewModels/StationViewModel.cs @@ -46,6 +46,10 @@ namespace TabletTester2025.ViewModels public ObservableCollection 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 ShowDissolutionSampleDialogAsync(int channel) { double? result = await App.Current.Dispatcher.InvokeAsync(() => @@ -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 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 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 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(); 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(); 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(); } diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index bc58f26..0560d9c 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -449,6 +449,7 @@ + @@ -476,7 +477,36 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -505,7 +535,7 @@ - + @@ -533,7 +563,7 @@ - + - - - - - - - + + - - - - - - - @@ -170,11 +149,11 @@ - + - + () @@ -75,22 +75,24 @@ namespace TabletTester2025 //p.FriabilityTargetTimeMin = ParseFiniteDouble(FriabilityTimeBox.Text, "脆碎度试验时间"); p.FriabilityTargetRounds = ParsePositiveInt(FriabilityRoundsBox.Text, "脆碎圈数"); p.FriabilityMaxLossPercent = ParseFiniteDouble(FriabilityMaxLossBox.Text, "最大失重率"); - p.DisintegrationDosageForm = GetSelectedDisintegrationDosageForm(); - //p.DisintegrationMaxSeconds = int.Parse(DisintegrationMaxSecBox.Text); //崩解最长时间 - p.DisintegrationSpeedRpm = ParseFiniteDouble(DisintegrationSpeedBox.Text, "崩解升降频率"); + double disintegrationTimeMin = ParsePositiveDouble(DisintegrationTimeMinBox.Text, "崩解时间"); + p.DisintegrationMaxSeconds = ToDisintegrationSeconds(disintegrationTimeMin); //p.DisintegrationTemperatureC = ParseFiniteDouble(DisintegrationTempBox.Text, "崩解介质温度"); //p.DissolutionTemperatureC = ParseFiniteDouble(DissolutionTempBox.Text, "溶出介质温度"); double dissolution1Speed = ParsePositiveDouble(Dissolution1SpeedBox.Text, "溶出速度1"); double dissolution2Speed = ParsePositiveDouble(Dissolution2SpeedBox.Text, "溶出速度2"); - p.Dissolution1SampleIntervalMin = ParsePositiveInt(Dissolution1IntervalBox.Text, "溶出1取样间隔"); - p.Dissolution2SampleIntervalMin = ParsePositiveInt(Dissolution2IntervalBox.Text, "溶出2取样间隔"); + p.Dissolution1SampleIntervalMin = ParsePositiveDouble(Dissolution1IntervalBox.Text, "溶出1取样间隔"); + p.Dissolution2SampleIntervalMin = ParsePositiveDouble(Dissolution2IntervalBox.Text, "溶出2取样间隔"); ValidateParameters(p); await WriteHardnessPressureAsync(hardnessPressure); await WriteHardnessDamageThresholdAsync(hardnessDamageThreshold); await WriteFriabilityRpmAsync(friabilityRpm); + await WriteDisintegrationTimeAsync(disintegrationTimeMin); await WriteDissolution1SpeedAsync(dissolution1Speed); await WriteDissolution2SpeedAsync(dissolution2Speed); + await WriteDissolution1IntervalAsync(p.Dissolution1SampleIntervalMin); + await WriteDissolution2IntervalAsync(p.Dissolution2SampleIntervalMin); App.CurrentPharmaParams = p; App.SaveCurrentPharmaParameters(); @@ -110,33 +112,6 @@ namespace TabletTester2025 Close(); } - private void DisintegrationDosageFormBox_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - //if (DisintegrationDosageFormBox.SelectedItem is ComboBoxItem item && item.Tag is string seconds) - // DisintegrationMaxSecBox.Text = seconds; - } - - private void SelectDisintegrationDosageForm(string dosageForm) - { - foreach (ComboBoxItem item in DisintegrationDosageFormBox.Items) - { - if (string.Equals(item.Content?.ToString(), dosageForm, StringComparison.OrdinalIgnoreCase)) - { - DisintegrationDosageFormBox.SelectedItem = item; - return; - } - } - - DisintegrationDosageFormBox.SelectedIndex = 0; - } - - private string GetSelectedDisintegrationDosageForm() - { - return DisintegrationDosageFormBox.SelectedItem is ComboBoxItem item - ? item.Content?.ToString() ?? "普通片" - : "普通片"; - } - private static void ValidateParameters(PharmaParameters p) { if (!double.IsFinite(p.HardnessMin_N) || !double.IsFinite(p.HardnessMax_N)) @@ -306,9 +281,21 @@ namespace TabletTester2025 return App.PlcConfig.FriabilityTestTime != 0 ? App.PlcConfig.FriabilityTestTime : (ushort)410; } - private static ushort ResolveDisintegrationSpeedRegister() + private static ushort ResolveDisintegrationTimeRegister() { - return App.PlcConfig.DisintegrationSpeed != 0 ? App.PlcConfig.DisintegrationSpeed : (ushort)330; + return App.PlcConfig.DisintegrationTime != 0 ? App.PlcConfig.DisintegrationTime : (ushort)420; + } + + private static async Task WriteDisintegrationTimeAsync(double value) + { + ushort registerAddress = ResolveDisintegrationTimeRegister(); + if (registerAddress == 0) + throw new InvalidOperationException("崩解时间PLC寄存器地址未配置。"); + + await App.PlcService.WriteRegisterAsync(registerAddress, (ushort)Math.Clamp( + (int)Math.Round(value, MidpointRounding.AwayFromZero), + 1, + ushort.MaxValue)); } private static async Task WriteDissolution1SpeedAsync(double value) @@ -329,6 +316,24 @@ namespace TabletTester2025 await App.PlcService.WriteFloatAsync(registerAddress, (float)value); } + private static async Task WriteDissolution1IntervalAsync(double value) + { + ushort registerAddress = ResolveDissolution1IntervalRegister(); + if (registerAddress == 0) + throw new InvalidOperationException("溶出1取样间隔PLC寄存器地址未配置。"); + + await App.PlcService.WriteFloatAsync(registerAddress, (float)value); + } + + private static async Task WriteDissolution2IntervalAsync(double value) + { + ushort registerAddress = ResolveDissolution2IntervalRegister(); + if (registerAddress == 0) + throw new InvalidOperationException("溶出2取样间隔PLC寄存器地址未配置。"); + + await App.PlcService.WriteFloatAsync(registerAddress, (float)value); + } + private static ushort ResolveDissolution1SpeedRegister() { return App.PlcConfig.Dissolution1Speed != 0 ? App.PlcConfig.Dissolution1Speed : (ushort)340; @@ -368,5 +373,17 @@ namespace TabletTester2025 return 100; } + + private static double ResolveDisintegrationTimeMin(PharmaParameters p) + { + return p.DisintegrationMaxSeconds > 0 + ? p.DisintegrationMaxSeconds / 60.0 + : 15.0; + } + + private static int ToDisintegrationSeconds(double minutes) + { + return Math.Max(1, (int)Math.Round(minutes * 60, MidpointRounding.AwayFromZero)); + } } } diff --git a/appsettings.json b/appsettings.json index e24913e..3a5936c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -67,8 +67,8 @@ "Dissolution2SampleAckCoil": 34, "Dissolution1Time": 430, "Dissolution2Time": 440, - "Dissolution1SampleInterval": 432, // 溶出1取样间隔,int类型 - "Dissolution2SampleInterval": 442 // 溶出2取样间隔,int类型 + "Dissolution1SampleInterval": 432, // 溶出1取样间隔,float类型 + "Dissolution2SampleInterval": 442 // 溶出2取样间隔,float类型 }, "PharmaStandard": { "StandardVersion": "中国药典2025",