diff --git a/Models/PharmaParameters.cs b/Models/PharmaParameters.cs index 30758c2..dcbbfe1 100644 --- a/Models/PharmaParameters.cs +++ b/Models/PharmaParameters.cs @@ -7,6 +7,7 @@ namespace TabletTester2025.Models public double HardnessMax_N { get; set; } = 60; public int HardnessTestCount { get; set; } = 6; public double FriabilityTargetRpm { get; set; } = 25.0; + public double FriabilityTargetTimeMin { get; set; } = 4.0; public int FriabilityTargetRounds { get; set; } = 100; public double FriabilityMaxLossPercent { get; set; } = 1.0; public string DisintegrationDosageForm { get; set; } = "普通片"; diff --git a/Models/PlcConfiguration.cs b/Models/PlcConfiguration.cs index e519e87..ac77530 100644 --- a/Models/PlcConfiguration.cs +++ b/Models/PlcConfiguration.cs @@ -25,6 +25,9 @@ public ushort HardnessPoSun { get; set; } // 脆碎度 public ushort FriabilityStartCoil { get; set; } + public ushort FriabilityTestTime { get; set; } + public ushort FriabilityWeightBefore { get; set; } + public ushort FriabilityWeightAfter { get; set; } public ushort WeightBefore { get; set; } // 天平重量寄存器(可选) public ushort WeightAfter { get; set; } public ushort FriabilityStartCoil2 { get; set; } diff --git a/Models/TestBatch.cs b/Models/TestBatch.cs index ab386a9..7ff28c5 100644 --- a/Models/TestBatch.cs +++ b/Models/TestBatch.cs @@ -32,6 +32,9 @@ namespace TabletTester2025.Models public double WeightBefore { get; set; } public double WeightAfter { get; set; } + [NotMapped] + public double FriabilityTargetTimeMin => FriabilityTargetTimeSec / 60.0; + // 崩解 public double DisintegrationTimeSec { get; set; } public int RemainingTubesAtEnd { get; set; } diff --git a/Services/ExcelExportService.cs b/Services/ExcelExportService.cs index c5e474b..acb0caf 100644 --- a/Services/ExcelExportService.cs +++ b/Services/ExcelExportService.cs @@ -92,7 +92,7 @@ namespace TabletTester2025.Services { var data = batches.ToList(); var sheet = package.Workbook.Worksheets.Add("脆碎度报表"); - WriteHeader(sheet, "检测时间", "样品名称", "失重率(%)", "设定转速(r/min)", "试验转数", "前重(g)", "后重(g)", "判定"); + WriteHeader(sheet, "检测时间", "样品名称", "失重率(%)", "设定转速(r/min)", "试验时间(min)", "试验转数", "前重(g)", "后重(g)", "判定"); if (data.Count == 0) { @@ -108,10 +108,11 @@ namespace TabletTester2025.Services sheet.Cells[row, 2].Value = b.SampleName; sheet.Cells[row, 3].Value = b.FriabilityLoss; sheet.Cells[row, 4].Value = b.FriabilityTargetRpm; - sheet.Cells[row, 5].Value = b.FriabilityTargetRounds; - sheet.Cells[row, 6].Value = b.WeightBefore; - sheet.Cells[row, 7].Value = b.WeightAfter; - sheet.Cells[row, 8].Value = b.FriabilityPassText; + sheet.Cells[row, 5].Value = b.FriabilityTargetTimeMin; + sheet.Cells[row, 6].Value = b.FriabilityTargetRounds; + sheet.Cells[row, 7].Value = b.WeightBefore; + sheet.Cells[row, 8].Value = b.WeightAfter; + sheet.Cells[row, 9].Value = b.FriabilityPassText; row++; } diff --git a/Services/PlcSimulator.cs b/Services/PlcSimulator.cs index d734dbf..6cb9217 100644 --- a/Services/PlcSimulator.cs +++ b/Services/PlcSimulator.cs @@ -18,6 +18,7 @@ namespace TabletTester2025.Services float value = startAddress switch { 100 => 40 + (float)_rand.NextDouble() * 20, // 硬度 40~60N + 410 => 4.0f, // 脆碎试验时间(min) 412 => 5.0f + (float)_rand.NextDouble() * 2, // 脆碎度前重 414 => 4.9f + (float)_rand.NextDouble() * 2, // 后重 300 => 37.0f, // 温度 diff --git a/ViewModels/StationViewModel.cs b/ViewModels/StationViewModel.cs index ec206fb..3802e48 100644 --- a/ViewModels/StationViewModel.cs +++ b/ViewModels/StationViewModel.cs @@ -33,6 +33,7 @@ namespace TabletTester2025.ViewModels private bool _isLoadingDissolution2SampleInterval; private bool _isLoadingDisintegrationTime; private bool _isLoadingDisintegrationSpeed; + private bool _isLoadingFriabilityTime; private bool _isUpdatingFriabilityWeightFromPlc; private readonly List _dissolution1Times = new(); @@ -123,7 +124,8 @@ namespace TabletTester2025.ViewModels // 脆碎度新增 [ObservableProperty] private double _friabilityTargetRpm = 25; - [ObservableProperty] private int _friabilityTargetTimeSec = 4; + [ObservableProperty] private double _friabilityTargetTimeMin = 4; + [ObservableProperty] private int _friabilityTargetTimeSec = 240; [ObservableProperty] private int _friabilityTargetRounds = 100; [ObservableProperty] private double _friabilityMaxLossPercent = 1.0; [ObservableProperty] private bool _friabilityClockwise = true; @@ -311,7 +313,7 @@ namespace TabletTester2025.ViewModels ResetDisintegrationCommand = new AsyncRelayCommand(ResetDisintegrationAsync); PrintDisintegrationCommand = new AsyncRelayCommand(async () => await PrintReport("崩解")); - _ = LoadFriabilityWeightsAsync(); + _ = LoadFriabilitySettingsAsync(); } public void ApplyPharmaDefaults() @@ -323,13 +325,18 @@ namespace TabletTester2025.ViewModels _isLoadingDissolution2Time = true; _isLoadingDissolution1SampleInterval = true; _isLoadingDissolution2SampleInterval = true; + _isLoadingFriabilityTime = true; try { HardnessInternalMin = p.HardnessMin_N; HardnessInternalMax = p.HardnessMax_N; HardnessTestCount = Math.Max(1, p.HardnessTestCount); FriabilityTargetRpm = p.FriabilityTargetRpm > 0 ? p.FriabilityTargetRpm : 25; - FriabilityTargetRounds = p.FriabilityTargetRounds > 0 ? p.FriabilityTargetRounds : 100; + double defaultRounds = p.FriabilityTargetRounds > 0 ? p.FriabilityTargetRounds : 100; + FriabilityTargetTimeMin = p.FriabilityTargetTimeMin > 0 + ? p.FriabilityTargetTimeMin + : defaultRounds / FriabilityTargetRpm; + UpdateFriabilityTimingFromTime(); FriabilityMaxLossPercent = p.FriabilityMaxLossPercent; FriabilityRemainingRounds = FriabilityTargetRounds; DisintegrationDosageForm = string.IsNullOrWhiteSpace(p.DisintegrationDosageForm) ? "普通片" : p.DisintegrationDosageForm; @@ -353,7 +360,10 @@ namespace TabletTester2025.ViewModels _isLoadingDissolution2Time = false; _isLoadingDissolution1SampleInterval = false; _isLoadingDissolution2SampleInterval = false; + _isLoadingFriabilityTime = false; } + + _ = WriteFriabilityTimeAsync(FriabilityTargetTimeMin); } private void LoadPharmaDefaults() @@ -975,7 +985,7 @@ namespace TabletTester2025.ViewModels if (_isUpdatingFriabilityWeightFromPlc) return; - _ = WriteFriabilityWeightAsync(_plcConfig.WeightBefore, value); + _ = WriteFriabilityWeightAsync(ResolveFriabilityWeightBeforeRegister(), value); } partial void OnWeightAfterChanged(double value) @@ -983,7 +993,72 @@ namespace TabletTester2025.ViewModels if (_isUpdatingFriabilityWeightFromPlc) return; - _ = WriteFriabilityWeightAsync(_plcConfig.WeightAfter, value); + _ = WriteFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), value); + } + + partial void OnFriabilityTargetTimeMinChanged(double value) + { + UpdateFriabilityTimingFromTime(); + + if (_isLoadingFriabilityTime) + return; + + _ = WriteFriabilityTimeAsync(value); + } + + partial void OnFriabilityTargetRpmChanged(double value) + { + UpdateFriabilityTimingFromTime(); + } + + private void UpdateFriabilityTimingFromTime() + { + if (!double.IsFinite(FriabilityTargetTimeMin) || FriabilityTargetTimeMin <= 0) + return; + + double rpm = FriabilityTargetRpm > 0 ? FriabilityTargetRpm : 25; + FriabilityTargetTimeSec = (int)Math.Ceiling(FriabilityTargetTimeMin * 60); + FriabilityTargetRounds = Math.Max(1, (int)Math.Round(FriabilityTargetTimeMin * rpm, MidpointRounding.AwayFromZero)); + + if (Phase != TestPhase.Running) + FriabilityRemainingRounds = FriabilityTargetRounds; + } + + private async Task LoadFriabilitySettingsAsync() + { + await LoadFriabilityTimeAsync(); + await LoadFriabilityWeightsAsync(); + } + + private async Task LoadFriabilityTimeAsync() + { + ushort registerAddress = ResolveFriabilityTestTimeRegister(); + if (registerAddress == 0) + return; + + try + { + _isLoadingFriabilityTime = true; + float value = await _plc.ReadFloatAsync(registerAddress); + if (float.IsFinite(value) && value > 0) + { + FriabilityTargetTimeMin = value; + } + else + { + await WriteFriabilityTimeAsync(FriabilityTargetTimeMin); + } + } + catch + { + try { await WriteFriabilityTimeAsync(FriabilityTargetTimeMin); } + catch { } + } + finally + { + _isLoadingFriabilityTime = false; + UpdateFriabilityTimingFromTime(); + } } private async Task LoadFriabilityWeightsAsync() @@ -992,15 +1067,17 @@ namespace TabletTester2025.ViewModels { _isUpdatingFriabilityWeightFromPlc = true; - if (_plcConfig.WeightBefore != 0) + ushort beforeRegister = ResolveFriabilityWeightBeforeRegister(); + if (beforeRegister != 0) { - double before = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量"); + double before = await ReadFriabilityWeightAsync(beforeRegister, "脆碎前重量"); WeightBefore = before; } - if (_plcConfig.WeightAfter != 0) + ushort afterRegister = ResolveFriabilityWeightAfterRegister(); + if (afterRegister != 0) { - double after = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量"); + double after = await ReadFriabilityWeightAsync(afterRegister, "脆碎后重量"); WeightAfter = after; } } @@ -1023,6 +1100,38 @@ namespace TabletTester2025.ViewModels catch { } } + private async Task WriteFriabilityTimeAsync(double value) + { + ushort registerAddress = ResolveFriabilityTestTimeRegister(); + if (registerAddress == 0 || !double.IsFinite(value) || value <= 0) + return; + + try + { + await _plc.WriteFloatAsync(registerAddress, (float)value); + } + catch { } + } + + private ushort ResolveFriabilityTestTimeRegister() + { + return _plcConfig.FriabilityTestTime; + } + + private ushort ResolveFriabilityWeightBeforeRegister() + { + return _plcConfig.FriabilityWeightBefore != 0 + ? _plcConfig.FriabilityWeightBefore + : _plcConfig.WeightBefore; + } + + private ushort ResolveFriabilityWeightAfterRegister() + { + return _plcConfig.FriabilityWeightAfter != 0 + ? _plcConfig.FriabilityWeightAfter + : _plcConfig.WeightAfter; + } + private void SetFriabilityWeightFromPlc(double? weightBefore = null, double? weightAfter = null) { _isUpdatingFriabilityWeightFromPlc = true; @@ -1289,18 +1398,24 @@ namespace TabletTester2025.ViewModels { throw new InvalidOperationException("未配置脆碎度启动线圈地址"); } - double weightBefore = await ReadFriabilityWeightAsync(_plcConfig.WeightBefore, "脆碎前重量"); + if (!double.IsFinite(FriabilityTargetTimeMin) || FriabilityTargetTimeMin <= 0) + throw new InvalidOperationException("脆碎试验时间必须大于0"); + + await WriteFriabilityTimeAsync(FriabilityTargetTimeMin); + + double weightBefore = await ReadFriabilityWeightAsync(ResolveFriabilityWeightBeforeRegister(), "脆碎前重量"); SetFriabilityWeightFromPlc(weightBefore: weightBefore); if (WeightBefore <= 0) throw new InvalidOperationException("脆碎前重量必须大于0"); - int totalRounds = Math.Max(1, FriabilityTargetRounds); + UpdateFriabilityTimingFromTime(); double rpm = FriabilityTargetRpm > 0 ? FriabilityTargetRpm : 25; - FriabilityTargetTimeSec = (int)Math.Ceiling(totalRounds / rpm * 60); + double testTimeMin = FriabilityTargetTimeMin > 0 ? FriabilityTargetTimeMin : 4; + int totalRounds = Math.Max(1, FriabilityTargetRounds); FriabilityRemainingRounds = totalRounds; FriabilityCurrentRpm = rpm; await PulseCoilAsync(startCoil); - int durationMs = (int)((totalRounds / rpm) * 60 * 1000); // 总运行时间(毫秒) + int durationMs = (int)Math.Ceiling(testTimeMin * 60 * 1000); // 总运行时间(毫秒) for (int i = 0; i < durationMs; i += 100) { @@ -1322,7 +1437,7 @@ namespace TabletTester2025.ViewModels if (Phase != TestPhase.Running) throw new InvalidOperationException("脆碎度测试已停止,未保存结果"); - double weightAfter = await ReadFriabilityWeightAsync(_plcConfig.WeightAfter, "脆碎后重量"); + double weightAfter = await ReadFriabilityWeightAsync(ResolveFriabilityWeightAfterRegister(), "脆碎后重量"); SetFriabilityWeightFromPlc(weightAfter: weightAfter); FriabilityCurrentRpm = rpm; LossPercent = TestCalculationService.CalculateFriabilityLossPercent(WeightBefore, WeightAfter); diff --git a/Views/HistoryWindow.xaml b/Views/HistoryWindow.xaml index 33a8b43..04e0a7c 100644 --- a/Views/HistoryWindow.xaml +++ b/Views/HistoryWindow.xaml @@ -197,6 +197,7 @@ + diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index 138ba34..01e6760 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -344,6 +344,10 @@ + + + + @@ -363,14 +367,24 @@ - - + + - - + + diff --git a/Views/SettingsWindow.xaml b/Views/SettingsWindow.xaml index 0c1dad8..f31cfd4 100644 --- a/Views/SettingsWindow.xaml +++ b/Views/SettingsWindow.xaml @@ -97,18 +97,27 @@ - + - - + + + + + + - diff --git a/Views/SettingsWindow.xaml.cs b/Views/SettingsWindow.xaml.cs index 333adfd..6aa79c0 100644 --- a/Views/SettingsWindow.xaml.cs +++ b/Views/SettingsWindow.xaml.cs @@ -21,7 +21,10 @@ namespace TabletTester2025 HardnessMaxBox.Text = p.HardnessMax_N.ToString(); HardnessCountBox.Text = p.HardnessTestCount.ToString(); FriabilityRpmBox.Text = p.FriabilityTargetRpm.ToString(); - FriabilityRoundsBox.Text = p.FriabilityTargetRounds.ToString(); + FriabilityTimeBox.Text = ResolveFriabilityTargetTimeMin(p).ToString("0.###"); + FriabilityRoundsBox.Text = CalculateFriabilityRounds( + ResolveFriabilityTargetTimeMin(p), + p.FriabilityTargetRpm > 0 ? p.FriabilityTargetRpm : 25).ToString(); FriabilityMaxLossBox.Text = p.FriabilityMaxLossPercent.ToString(); SelectDisintegrationDosageForm(p.DisintegrationDosageForm); DisintegrationMaxSecBox.Text = p.DisintegrationMaxSeconds.ToString(); @@ -45,7 +48,8 @@ namespace TabletTester2025 p.HardnessMax_N = double.Parse(HardnessMaxBox.Text); p.HardnessTestCount = int.Parse(HardnessCountBox.Text); p.FriabilityTargetRpm = double.Parse(FriabilityRpmBox.Text); - p.FriabilityTargetRounds = int.Parse(FriabilityRoundsBox.Text); + p.FriabilityTargetTimeMin = double.Parse(FriabilityTimeBox.Text); + p.FriabilityTargetRounds = CalculateFriabilityRounds(p.FriabilityTargetTimeMin, p.FriabilityTargetRpm); p.FriabilityMaxLossPercent = double.Parse(FriabilityMaxLossBox.Text); p.DisintegrationDosageForm = GetSelectedDisintegrationDosageForm(); p.DisintegrationMaxSeconds = int.Parse(DisintegrationMaxSecBox.Text); @@ -87,6 +91,24 @@ namespace TabletTester2025 DisintegrationMaxSecBox.Text = seconds; } + private void FriabilityCalculationBox_TextChanged(object sender, TextChangedEventArgs e) + { + if (FriabilityRoundsBox == null) + return; + + if (double.TryParse(FriabilityTimeBox?.Text, out double timeMin) + && double.TryParse(FriabilityRpmBox?.Text, out double rpm) + && timeMin > 0 + && rpm > 0) + { + FriabilityRoundsBox.Text = CalculateFriabilityRounds(timeMin, rpm).ToString(); + } + else + { + FriabilityRoundsBox.Text = ""; + } + } + private void SelectDisintegrationDosageForm(string dosageForm) { foreach (ComboBoxItem item in DisintegrationDosageFormBox.Items) @@ -114,7 +136,7 @@ namespace TabletTester2025 throw new InvalidOperationException("硬度内控上限必须大于下限。"); if (p.HardnessTestCount <= 0) throw new InvalidOperationException("硬度测试次数必须大于0。"); - if (p.FriabilityTargetRpm <= 0 || p.FriabilityTargetRounds <= 0 || p.FriabilityMaxLossPercent <= 0) + if (p.FriabilityTargetRpm <= 0 || p.FriabilityTargetTimeMin <= 0 || p.FriabilityTargetRounds <= 0 || p.FriabilityMaxLossPercent <= 0) throw new InvalidOperationException("脆碎度参数必须大于0。"); if (p.DisintegrationMaxSeconds <= 0 || p.DisintegrationSpeedRpm <= 0 || p.DisintegrationTemperatureC <= 0) throw new InvalidOperationException("崩解参数必须大于0。"); @@ -127,5 +149,25 @@ namespace TabletTester2025 if (p.DissolutionSampleTimes == null || p.DissolutionSampleTimes.Length == 0 || p.DissolutionSampleTimes.Any(t => t <= 0)) throw new InvalidOperationException("溶出取样时间点必须为大于0的分钟数。"); } + + private static double ResolveFriabilityTargetTimeMin(PharmaParameters p) + { + if (p.FriabilityTargetTimeMin > 0) + return p.FriabilityTargetTimeMin; + + double rpm = p.FriabilityTargetRpm > 0 ? p.FriabilityTargetRpm : 25; + if (p.FriabilityTargetRounds > 0 && rpm > 0) + return p.FriabilityTargetRounds / rpm; + + return 4.0; + } + + private static int CalculateFriabilityRounds(double timeMin, double rpm) + { + if (timeMin <= 0 || rpm <= 0) + return 0; + + return Math.Max(1, (int)Math.Round(timeMin * rpm, MidpointRounding.AwayFromZero)); + } } } diff --git a/appsettings.json b/appsettings.json index a89c60f..dd85afd 100644 --- a/appsettings.json +++ b/appsettings.json @@ -35,6 +35,9 @@ "FriabilityStartCoilReset": 95, // 脆碎复位启动 + "FriabilityTestTime": 410, // 脆碎试验时间(min) + "FriabilityWeightBefore": 412, // 脆碎前质量(g) + "FriabilityWeightAfter": 414, // 脆碎后质量(g) "WeightBefore": 412, "WeightAfter": 414, "DisintegrationTemp": 1430, @@ -64,6 +67,7 @@ "HardnessMax_N": 60, "HardnessTestCount": 6, "FriabilityTargetRpm": 25.0, + "FriabilityTargetTimeMin": 4.0, "FriabilityTargetRounds": 100, "FriabilityMaxLossPercent": 1.0, "DisintegrationDosageForm": "普通片",