titration start
This commit is contained in:
parent
918e102a0c
commit
16b0273ac8
336
gui/main.py
336
gui/main.py
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user