titration start

This commit is contained in:
jens 2025-07-13 18:17:17 +02:00
parent 918e102a0c
commit 16b0273ac8

View File

@ -11,6 +11,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import datetime
from collections import deque
import numpy as np
class PHControllerGUI(QMainWindow):
def __init__(self):
@ -165,6 +166,47 @@ class PHControllerGUI(QMainWindow):
main_layout.addWidget(plot_group)
# Titration Plot Group
titration_group = QGroupBox("Titration")
titration_layout = QVBoxLayout()
# Titration controls
titration_controls_layout = QHBoxLayout()
# Titration parameters
self.titration_step_spin = QDoubleSpinBox()
self.titration_step_spin.setRange(0.1, 10.0)
self.titration_step_spin.setValue(0.5)
self.titration_step_spin.setSuffix(" ml")
self.titration_step_spin.setDecimals(1)
self.titration_wait_spin = QDoubleSpinBox()
self.titration_wait_spin.setRange(5.0, 60.0)
self.titration_wait_spin.setValue(10.0)
self.titration_wait_spin.setSuffix(" s")
self.titration_wait_spin.setDecimals(1)
self.start_titration_btn = QPushButton("Automatische Titration starten")
self.start_titration_btn.clicked.connect(self.toggle_auto_titration)
self.clear_titration_btn = QPushButton("Titrationsdaten löschen")
self.clear_titration_btn.clicked.connect(self.clear_titration_plot)
titration_controls_layout.addWidget(QLabel("Schritt:"))
titration_controls_layout.addWidget(self.titration_step_spin)
titration_controls_layout.addWidget(QLabel("Warten:"))
titration_controls_layout.addWidget(self.titration_wait_spin)
titration_controls_layout.addWidget(self.start_titration_btn)
titration_controls_layout.addWidget(self.clear_titration_btn)
# Titration Plot Widget
self.titration_plot = TitrationPlotWidget()
titration_layout.addLayout(titration_controls_layout)
titration_layout.addWidget(self.titration_plot)
titration_group.setLayout(titration_layout)
main_layout.addWidget(titration_group)
# Status Bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
@ -200,6 +242,17 @@ class PHControllerGUI(QMainWindow):
self.auto_dose_timer = QTimer()
self.auto_dose_timer.timeout.connect(self.check_auto_dose_progress)
# Titration tracking
self.titration_active = False
self.titration_start_volume = 0.0
self.auto_titration_active = False
self.titration_step_size = 0.5 # ml per step
self.titration_wait_time = 10.0 # seconds to wait between steps
self.titration_timer = QTimer()
self.titration_timer.timeout.connect(self.titration_step)
self.titration_current_step = 0
self.titration_target_volume = 0.0
# Enable/disable controls based on connection
self.set_controls_enabled(False)
@ -266,6 +319,10 @@ class PHControllerGUI(QMainWindow):
self.auto_dose_timer.stop()
self.auto_dosing = False
# Stop titration timer
self.titration_timer.stop()
self.auto_titration_active = False
self.connect_btn.setText("Verbinden")
self.status_bar.showMessage("Getrennt")
self.set_controls_enabled(False)
@ -278,6 +335,8 @@ class PHControllerGUI(QMainWindow):
self.last_flow_rate = 0.0
self.auto_dosing = False
self.auto_dose_timer.stop()
self.auto_titration_active = False
self.titration_timer.stop()
def set_controls_enabled(self, enabled):
self.pump_on_btn.setEnabled(enabled)
@ -291,6 +350,10 @@ class PHControllerGUI(QMainWindow):
self.reset_volume_checkbox.setEnabled(enabled)
self.clear_plot_btn.setEnabled(enabled)
self.plot_enabled_checkbox.setEnabled(enabled)
self.start_titration_btn.setEnabled(enabled)
self.clear_titration_btn.setEnabled(enabled)
self.titration_step_spin.setEnabled(enabled)
self.titration_wait_spin.setEnabled(enabled)
def send_command(self, command):
print(command)
@ -339,8 +402,8 @@ class PHControllerGUI(QMainWindow):
self.total_volume_label.setVisible(True)
# Check if volume should be reset or continue accumulating
if self.reset_volume_checkbox.isChecked() or self.auto_dosing:
# Reset volume counter (always reset for auto dosing)
if (self.reset_volume_checkbox.isChecked() or self.auto_dosing) and not self.auto_titration_active:
# Reset volume counter (always reset for auto dosing, but NOT for titration)
self.total_volume = 0.0
self.total_volume_label.setText("Gesamt: 0.0 ml")
else:
@ -399,6 +462,11 @@ class PHControllerGUI(QMainWindow):
self.pump_off_btn.setEnabled(True)
return
# Don't start if auto titration is running
if self.auto_titration_active:
QMessageBox.warning(self, "Warnung", "Automatische Titration läuft bereits!")
return
volume = self.volume_spin.value()
self.target_volume = volume
self.auto_dosing = True
@ -419,39 +487,6 @@ class PHControllerGUI(QMainWindow):
self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml")
def check_auto_dose_progress(self):
"""Check if target volume has been reached during auto dosing"""
if not self.auto_dosing:
return
# Check if we've reached or exceeded the target volume
if self.total_volume >= self.target_volume:
# Stop dosing
self.auto_dosing = False
self.auto_dose_timer.stop()
self.send_pump_command(0) # Turn off pump
# Re-enable manual controls
self.pump_on_btn.setEnabled(True)
self.pump_off_btn.setEnabled(True)
self.auto_dose_btn.setText("Automatisch dosieren")
# Show completion message
actual_volume = self.total_volume
difference = actual_volume - self.target_volume
message = f"Dosierung abgeschlossen!\n"
message += f"Zielvolumen: {self.target_volume:.2f} ml\n"
message += f"Tatsächliches Volumen: {actual_volume:.2f} ml\n"
message += f"Abweichung: {difference:+.2f} ml"
QMessageBox.information(self, "Automatische Dosierung", message)
self.status_bar.showMessage(f"Dosierung abgeschlossen: {actual_volume:.2f} ml von {self.target_volume:.2f} ml")
else:
# Update progress in status bar
progress = (self.total_volume / self.target_volume) * 100
self.status_bar.showMessage(f"Dosierung: {self.total_volume:.2f}/{self.target_volume:.2f} ml ({progress:.1f}%)")
def read_serial_data(self):
if not self.serial_connection or not self.serial_connection.is_open:
return
@ -469,6 +504,10 @@ class PHControllerGUI(QMainWindow):
# Add to plot if recording is enabled
if self.plot_enabled_checkbox.isChecked():
self.ph_plot.add_ph_value(ph_value)
# Add to titration plot if titration is active
if self.titration_active or self.auto_titration_active:
titrated_volume = self.total_volume - self.titration_start_volume
self.titration_plot.add_titration_point(titrated_volume, ph_value)
except ValueError:
pass
elif data.startswith('P'): # Pump status
@ -522,6 +561,7 @@ class PHControllerGUI(QMainWindow):
self.tube_request_timer.stop()
self.flow_rate_timer.stop()
self.auto_dose_timer.stop()
self.titration_timer.stop()
self.close_connection()
event.accept()
@ -530,6 +570,148 @@ class PHControllerGUI(QMainWindow):
self.ph_plot.clear_data()
self.status_bar.showMessage("pH-Diagramm gelöscht")
def toggle_titration(self):
"""Start or stop titration recording"""
if self.titration_active:
# Stop titration
self.titration_active = False
self.start_titration_btn.setText("Titration starten")
self.status_bar.showMessage("Titration gestoppt")
else:
# Start titration
self.titration_active = True
self.titration_start_volume = self.total_volume
self.start_titration_btn.setText("Titration stoppen")
self.status_bar.showMessage("Titration gestartet")
def toggle_auto_titration(self):
"""Start or stop automatic titration"""
if self.auto_titration_active:
# Stop auto titration
self.auto_titration_active = False
self.titration_timer.stop()
self.send_pump_command(0) # Turn off pump
self.start_titration_btn.setText("Automatische Titration starten")
# Re-enable controls
self.pump_on_btn.setEnabled(True)
self.pump_off_btn.setEnabled(True)
self.auto_dose_btn.setEnabled(True)
self.status_bar.showMessage("Automatische Titration gestoppt")
return
# Don't start if auto dosing is running
if self.auto_dosing:
QMessageBox.warning(self, "Warnung", "Automatische Dosierung läuft bereits!")
return
# Get parameters
self.titration_step_size = self.titration_step_spin.value()
self.titration_wait_time = self.titration_wait_spin.value()
# Start auto titration
self.auto_titration_active = True
self.titration_start_volume = self.total_volume # Remember starting volume
self.titration_current_step = 0
# Update button and disable controls
self.start_titration_btn.setText("Automatische Titration stoppen")
self.pump_on_btn.setEnabled(False)
self.pump_off_btn.setEnabled(False)
self.auto_dose_btn.setEnabled(False)
# Start first titration step
self.titration_step()
self.status_bar.showMessage(f"Automatische Titration gestartet - Schritt: {self.titration_step_size} ml, Warten: {self.titration_wait_time} s")
def titration_step(self):
"""Perform one titration step"""
if not self.auto_titration_active:
return
self.titration_current_step += 1
# Calculate step target volume (only the additional volume for this step)
step_target_volume = self.titration_step_size
# Calculate absolute target volume (total from start of titration)
absolute_target_volume = self.titration_start_volume + (self.titration_current_step * self.titration_step_size)
# Start pumping for this step
self.auto_dosing = True # Use auto dosing mechanism
self.target_volume = absolute_target_volume # Set absolute target
self.send_pump_command(1)
self.auto_dose_timer.start(500)
current_titrated_volume = self.total_volume - self.titration_start_volume
total_target_titrated = self.titration_current_step * self.titration_step_size
self.status_bar.showMessage(f"Titration Schritt {self.titration_current_step}: Dosiere {step_target_volume} ml (Titration gesamt: {total_target_titrated} ml)")
def check_auto_dose_progress(self):
"""Check if target volume has been reached during auto dosing"""
if not self.auto_dosing:
return
# Check if we've reached or exceeded the target volume
if self.total_volume >= self.target_volume:
# Stop dosing
self.auto_dosing = False
self.auto_dose_timer.stop()
self.send_pump_command(0) # Turn off pump
# If this was part of auto titration, schedule next step
if self.auto_titration_active:
# Wait for pH to stabilize, then continue with next step
wait_time_ms = int(self.titration_wait_time * 1000)
QTimer.singleShot(wait_time_ms, self.titration_step)
self.status_bar.showMessage(f"Warte {self.titration_wait_time} s auf pH-Stabilisierung...")
else:
# Re-enable manual controls for regular auto dosing
self.pump_on_btn.setEnabled(True)
self.pump_off_btn.setEnabled(True)
self.auto_dose_btn.setText("Automatisch dosieren")
# Show completion message
actual_volume = self.total_volume
difference = actual_volume - self.target_volume
message = f"Dosierung abgeschlossen!\n"
message += f"Zielvolumen: {self.target_volume:.2f} ml\n"
message += f"Tatsächliches Volumen: {actual_volume:.2f} ml\n"
message += f"Abweichung: {difference:+.2f} ml"
QMessageBox.information(self, "Automatische Dosierung", message)
self.status_bar.showMessage(f"Dosierung abgeschlossen: {actual_volume:.2f} ml von {self.target_volume:.2f} ml")
else:
# Update progress in status bar
progress = (self.total_volume / self.target_volume) * 100
if self.auto_titration_active:
current_titrated_volume = self.total_volume - self.titration_start_volume
target_titrated_volume = self.titration_current_step * self.titration_step_size
self.status_bar.showMessage(f"Titration Schritt {self.titration_current_step}: Gesamt {current_titrated_volume:.2f}/{target_titrated_volume:.2f} ml titriert ({progress:.1f}%)")
else:
self.status_bar.showMessage(f"Dosierung: {self.total_volume:.2f}/{self.target_volume:.2f} ml ({progress:.1f}%)")
def clear_titration_plot(self):
"""Clear the titration plot data"""
try:
# Clear the plot data
self.titration_plot.clear_data()
# Reset titration state variables
self.titration_active = False
self.titration_start_volume = 0.0
# Update button text if not in auto titration mode
if not self.auto_titration_active:
self.start_titration_btn.setText("Automatische Titration starten")
self.status_bar.showMessage("Titrationsdaten gelöscht")
except Exception as e:
self.status_bar.showMessage(f"Fehler beim Löschen der Titrationsdaten: {str(e)}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Löschen der Titrationsdaten:\n{str(e)}")
class PHPlotWidget(QWidget):
def __init__(self):
super().__init__()
@ -638,6 +820,90 @@ class PHPlotWidget(QWidget):
self.ax.xaxis.set_major_formatter(ticker.ScalarFormatter())
self.canvas.draw()
class TitrationPlotWidget(QWidget):
def __init__(self):
super().__init__()
self.figure = Figure(figsize=(8, 4))
self.canvas = FigureCanvas(self.figure)
self.ax = self.figure.add_subplot(111)
# Data storage
self.volumes = []
self.ph_values = []
# Setup plot
self.ax.set_xlabel('Dosierte Menge (ml)')
self.ax.set_ylabel('pH-Wert')
self.ax.set_title('Titrationskurve')
self.ax.grid(True, alpha=0.3)
self.ax.set_ylim(0, 14) # pH range 0-14
self.ax.set_xlim(0, 1) # Initial volume range
# Layout
layout = QVBoxLayout()
layout.addWidget(self.canvas)
self.setLayout(layout)
# Plot line and points
self.line, = self.ax.plot([], [], 'r-', linewidth=2, label='pH-Verlauf')
self.points = self.ax.scatter([], [], c='red', s=30, zorder=5, label='Messpunkte')
self.ax.legend()
# Initial draw
self.canvas.draw()
def add_titration_point(self, volume, ph_value):
"""Add new titration point (volume vs pH)"""
self.volumes.append(volume)
self.ph_values.append(ph_value)
self.update_plot()
def update_plot(self):
"""Update the plot with current data"""
if len(self.volumes) > 0 and len(self.ph_values) > 0:
# Update line data
self.line.set_data(self.volumes, self.ph_values)
# Update scatter points
self.points.set_offsets(list(zip(self.volumes, self.ph_values)))
# Auto-scale x-axis
if len(self.volumes) > 1:
max_vol = max(self.volumes)
self.ax.set_xlim(0, max_vol * 1.1) # Add 10% margin
# Auto-scale y-axis around data with some margin
if len(self.ph_values) > 0:
min_ph = min(self.ph_values)
max_ph = max(self.ph_values)
margin = 0.5
self.ax.set_ylim(max(0, min_ph - margin), min(14, max_ph + margin))
# Redraw
self.canvas.draw()
def clear_data(self):
"""Clear all titration data points"""
try:
# Clear data lists
self.volumes.clear()
self.ph_values.clear()
# Reset plot elements safely
self.line.set_data([], [])
# Clear scatter points properly - use empty 2D array
self.points.set_offsets(np.empty((0, 2)))
# Reset axis limits
self.ax.set_xlim(0, 1)
self.ax.set_ylim(0, 14)
# Redraw canvas
self.canvas.draw()
except Exception as e:
print(f"Fehler beim Löschen der Titrationsdaten: {str(e)}")
def main():
app = QApplication(sys.argv)
window = PHControllerGUI()