next step
This commit is contained in:
parent
95fb45a636
commit
2103601efa
132
gui/main.py
132
gui/main.py
@ -155,6 +155,14 @@ class PHControllerGUI(QMainWindow):
|
|||||||
|
|
||||||
plot_controls_layout.addWidget(self.plot_enabled_checkbox)
|
plot_controls_layout.addWidget(self.plot_enabled_checkbox)
|
||||||
plot_controls_layout.addWidget(self.clear_plot_btn)
|
plot_controls_layout.addWidget(self.clear_plot_btn)
|
||||||
|
|
||||||
|
# pH Filter Control
|
||||||
|
self.ph_filter_checkbox = QCheckBox("pH-Filter (Motorstörungen)")
|
||||||
|
self.ph_filter_checkbox.setChecked(True)
|
||||||
|
self.ph_filter_checkbox.setToolTip("Filtert pH-Sprünge beim Motor Ein-/Ausschalten")
|
||||||
|
self.ph_filter_checkbox.toggled.connect(self.toggle_ph_filter)
|
||||||
|
plot_controls_layout.addWidget(self.ph_filter_checkbox)
|
||||||
|
|
||||||
plot_controls_layout.addStretch()
|
plot_controls_layout.addStretch()
|
||||||
|
|
||||||
# pH Plot Widget
|
# pH Plot Widget
|
||||||
@ -236,6 +244,14 @@ class PHControllerGUI(QMainWindow):
|
|||||||
self.total_volume = 0.0 # Total volume pumped in ml
|
self.total_volume = 0.0 # Total volume pumped in ml
|
||||||
self.last_flow_rate = 0.0 # Last known flow rate
|
self.last_flow_rate = 0.0 # Last known flow rate
|
||||||
|
|
||||||
|
# pH filtering for motor interference
|
||||||
|
self.ph_history = deque(maxlen=5) # Store last 5 pH values
|
||||||
|
self.motor_state_changed_time = None # Track when motor state changed
|
||||||
|
self.motor_interference_duration = 3.0 # seconds to filter after motor state change
|
||||||
|
self.ph_filter_enabled = True # Enable/disable pH filtering
|
||||||
|
self.outlier_threshold = 2.0 # pH units - values changing more than this are considered outliers
|
||||||
|
self.interference_threshold = 1.0 # pH units - stricter threshold during motor interference
|
||||||
|
|
||||||
# Auto dosing
|
# Auto dosing
|
||||||
self.auto_dosing = False
|
self.auto_dosing = False
|
||||||
self.target_volume = 0.0
|
self.target_volume = 0.0
|
||||||
@ -354,6 +370,7 @@ class PHControllerGUI(QMainWindow):
|
|||||||
self.clear_titration_btn.setEnabled(enabled)
|
self.clear_titration_btn.setEnabled(enabled)
|
||||||
self.titration_step_spin.setEnabled(enabled)
|
self.titration_step_spin.setEnabled(enabled)
|
||||||
self.titration_wait_spin.setEnabled(enabled)
|
self.titration_wait_spin.setEnabled(enabled)
|
||||||
|
self.ph_filter_checkbox.setEnabled(enabled)
|
||||||
|
|
||||||
def send_command(self, command):
|
def send_command(self, command):
|
||||||
print(command)
|
print(command)
|
||||||
@ -394,6 +411,9 @@ class PHControllerGUI(QMainWindow):
|
|||||||
self.last_flow_rate = flow_rate
|
self.last_flow_rate = flow_rate
|
||||||
|
|
||||||
def send_pump_command(self, state):
|
def send_pump_command(self, state):
|
||||||
|
# Record motor state change time for interference filtering
|
||||||
|
self.motor_state_changed_time = datetime.datetime.now()
|
||||||
|
|
||||||
if state == 1:
|
if state == 1:
|
||||||
self.send_command("<11>") # Pump on
|
self.send_command("<11>") # Pump on
|
||||||
self.pump_status.setText("Pumpe: Ein")
|
self.pump_status.setText("Pumpe: Ein")
|
||||||
@ -487,6 +507,86 @@ class PHControllerGUI(QMainWindow):
|
|||||||
|
|
||||||
self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml")
|
self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml")
|
||||||
|
|
||||||
|
def toggle_ph_filter(self, enabled):
|
||||||
|
"""Enable or disable pH filtering"""
|
||||||
|
self.ph_filter_enabled = enabled
|
||||||
|
if enabled:
|
||||||
|
self.status_bar.showMessage("pH-Filter aktiviert - Motorstörungen werden gefiltert")
|
||||||
|
else:
|
||||||
|
self.status_bar.showMessage("pH-Filter deaktiviert - Rohe pH-Werte werden angezeigt")
|
||||||
|
# Clear history when disabling filter
|
||||||
|
self.ph_history.clear()
|
||||||
|
|
||||||
|
def configure_ph_filter(self, interference_duration=3.0, outlier_threshold=2.0, interference_threshold=1.0):
|
||||||
|
"""
|
||||||
|
Configure pH filter parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interference_duration: Seconds to apply enhanced filtering after motor state change
|
||||||
|
outlier_threshold: pH units - normal outlier detection threshold
|
||||||
|
interference_threshold: pH units - stricter threshold during motor interference
|
||||||
|
"""
|
||||||
|
self.motor_interference_duration = interference_duration
|
||||||
|
self.outlier_threshold = outlier_threshold
|
||||||
|
self.interference_threshold = interference_threshold
|
||||||
|
|
||||||
|
if self.ph_filter_enabled:
|
||||||
|
self.status_bar.showMessage(f"pH-Filter konfiguriert: Störzeit={interference_duration}s, "
|
||||||
|
f"Normal={outlier_threshold}pH, Störung={interference_threshold}pH")
|
||||||
|
|
||||||
|
def filter_ph_value(self, ph_value):
|
||||||
|
"""
|
||||||
|
Filter pH values to reduce motor interference and outliers
|
||||||
|
Returns the filtered pH value or None if value should be ignored
|
||||||
|
"""
|
||||||
|
# If filtering is disabled, return raw value
|
||||||
|
if not self.ph_filter_enabled:
|
||||||
|
return ph_value
|
||||||
|
|
||||||
|
current_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Add to history
|
||||||
|
self.ph_history.append(ph_value)
|
||||||
|
|
||||||
|
# If motor state changed recently, apply enhanced filtering
|
||||||
|
if (self.motor_state_changed_time and
|
||||||
|
(current_time - self.motor_state_changed_time).total_seconds() < self.motor_interference_duration):
|
||||||
|
|
||||||
|
# During motor interference period, use more aggressive filtering
|
||||||
|
if len(self.ph_history) >= 3:
|
||||||
|
# Use median filter to remove spikes
|
||||||
|
recent_values = list(self.ph_history)[-3:]
|
||||||
|
filtered_value = sorted(recent_values)[1] # Median of last 3 values
|
||||||
|
|
||||||
|
# Additional outlier detection during interference period
|
||||||
|
if len(self.ph_history) >= 5:
|
||||||
|
older_values = list(self.ph_history)[:-1] # All except current
|
||||||
|
avg_older = sum(older_values) / len(older_values)
|
||||||
|
|
||||||
|
# If current value deviates more than threshold from recent average
|
||||||
|
if abs(ph_value - avg_older) > self.interference_threshold:
|
||||||
|
# Use the median instead of the raw value
|
||||||
|
return filtered_value
|
||||||
|
|
||||||
|
return filtered_value
|
||||||
|
else:
|
||||||
|
# Not enough history during interference, return current value
|
||||||
|
return ph_value
|
||||||
|
|
||||||
|
# Normal operation - light filtering
|
||||||
|
if len(self.ph_history) >= 2:
|
||||||
|
# Simple outlier detection
|
||||||
|
previous_value = self.ph_history[-2]
|
||||||
|
|
||||||
|
# If change is more than threshold, it's likely an outlier
|
||||||
|
if abs(ph_value - previous_value) > self.outlier_threshold:
|
||||||
|
# Use moving average of last few values instead
|
||||||
|
if len(self.ph_history) >= 3:
|
||||||
|
recent_values = list(self.ph_history)[:-1] # Exclude current outlier
|
||||||
|
return sum(recent_values) / len(recent_values)
|
||||||
|
|
||||||
|
return ph_value
|
||||||
|
|
||||||
def read_serial_data(self):
|
def read_serial_data(self):
|
||||||
if not self.serial_connection or not self.serial_connection.is_open:
|
if not self.serial_connection or not self.serial_connection.is_open:
|
||||||
return
|
return
|
||||||
@ -499,15 +599,29 @@ class PHControllerGUI(QMainWindow):
|
|||||||
data = line[1:-1]
|
data = line[1:-1]
|
||||||
if data.startswith('#'): # pH value
|
if data.startswith('#'): # pH value
|
||||||
try:
|
try:
|
||||||
ph_value = float(data[1:])
|
raw_ph_value = float(data[1:])
|
||||||
self.ph_label.setText(f"pH: {ph_value:.2f}")
|
|
||||||
# Add to plot if recording is enabled
|
# Apply filtering to reduce motor interference
|
||||||
if self.plot_enabled_checkbox.isChecked():
|
filtered_ph_value = self.filter_ph_value(raw_ph_value)
|
||||||
self.ph_plot.add_ph_value(ph_value)
|
|
||||||
# Add to titration plot if titration is active
|
if filtered_ph_value is not None:
|
||||||
if self.titration_active or self.auto_titration_active:
|
# Update display with filtered value
|
||||||
titrated_volume = self.total_volume - self.titration_start_volume
|
self.ph_label.setText(f"pH: {filtered_ph_value:.2f}")
|
||||||
self.titration_plot.add_titration_point(titrated_volume, ph_value)
|
|
||||||
|
# Show filtering status if motor interference is active
|
||||||
|
if (self.motor_state_changed_time and
|
||||||
|
(datetime.datetime.now() - self.motor_state_changed_time).total_seconds() < self.motor_interference_duration):
|
||||||
|
if abs(raw_ph_value - filtered_ph_value) > 0.1:
|
||||||
|
self.status_bar.showMessage(f"pH gefiltert: {raw_ph_value:.2f} → {filtered_ph_value:.2f} (Motorstörung)")
|
||||||
|
|
||||||
|
# Add to plot if recording is enabled
|
||||||
|
if self.plot_enabled_checkbox.isChecked():
|
||||||
|
self.ph_plot.add_ph_value(filtered_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, filtered_ph_value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
elif data.startswith('P'): # Pump status
|
elif data.startswith('P'): # Pump status
|
||||||
|
|||||||
88
pH_Filter_Dokumentation.md
Normal file
88
pH_Filter_Dokumentation.md
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# pH-Filter für Motorstörungen - Dokumentation
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
Beim Ein- und Ausschalten des Pumpmotors können kurzzeitige starke pH-Sprünge auftreten. Diese sind meist durch elektromagnetische Störungen (EMI) oder Spannungsspitzen verursacht und verfälschen die Messwerte.
|
||||||
|
|
||||||
|
## Lösung
|
||||||
|
Die Software wurde um einen intelligenten pH-Filter erweitert, der solche Störungen automatisch erkennt und filtert.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Automatische Störungserkennung
|
||||||
|
- Der Filter erkennt automatisch, wann der Motor ein- oder ausgeschaltet wird
|
||||||
|
- Für 3 Sekunden nach einer Motorschaltung wird verstärkte Filterung angewendet
|
||||||
|
|
||||||
|
### 2. Mehrstufige Filterung
|
||||||
|
|
||||||
|
#### Normale Betriebszeit:
|
||||||
|
- **Ausreißer-Erkennung**: pH-Sprünge > 2.0 Einheiten werden als Ausreißer erkannt
|
||||||
|
- **Gleitender Mittelwert**: Ausreißer werden durch den Durchschnitt der letzten Werte ersetzt
|
||||||
|
|
||||||
|
#### Motor-Störzeit (3 Sekunden nach Schalten):
|
||||||
|
- **Median-Filter**: Verwendet den Median der letzten 3 Werte
|
||||||
|
- **Verschärfte Schwelle**: pH-Sprünge > 1.0 Einheiten werden gefiltert
|
||||||
|
- **Status-Anzeige**: Zeigt in der Statusleiste an, wenn gefiltert wird
|
||||||
|
|
||||||
|
### 3. Benutzersteuerung
|
||||||
|
- **Ein/Aus-Schalter**: Checkbox "pH-Filter (Motorstörungen)" im Plot-Bereich
|
||||||
|
- **Echtzeit-Feedback**: Status wird in der Statusleiste angezeigt
|
||||||
|
- **Transparenz**: Zeigt sowohl rohe als auch gefilterte Werte an
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Standard-Parameter:
|
||||||
|
```python
|
||||||
|
# Störungszeit nach Motorschaltung
|
||||||
|
motor_interference_duration = 3.0 # Sekunden
|
||||||
|
|
||||||
|
# Normale Ausreißer-Schwelle
|
||||||
|
outlier_threshold = 2.0 # pH-Einheiten
|
||||||
|
|
||||||
|
# Verschärfte Schwelle während Störzeit
|
||||||
|
interference_threshold = 1.0 # pH-Einheiten
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anpassung der Parameter:
|
||||||
|
```python
|
||||||
|
# Beispiel für empfindlichere Filterung
|
||||||
|
gui.configure_ph_filter(
|
||||||
|
interference_duration=5.0, # Längere Störzeit
|
||||||
|
outlier_threshold=1.5, # Empfindlichere normale Filterung
|
||||||
|
interference_threshold=0.5 # Sehr empfindliche Störzeit-Filterung
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
1. **Aktivierung**: Checkbox "pH-Filter (Motorstörungen)" aktivieren (Standard: Ein)
|
||||||
|
2. **Betrieb**: Filter arbeitet automatisch im Hintergrund
|
||||||
|
3. **Überwachung**: Statusleiste zeigt Filteraktivität an
|
||||||
|
4. **Deaktivierung**: Checkbox deaktivieren für ungefilterte Rohwerte
|
||||||
|
|
||||||
|
## Technische Details
|
||||||
|
|
||||||
|
### Algorithmus:
|
||||||
|
1. **Wertspeicher**: Letzte 5 pH-Werte werden gespeichert
|
||||||
|
2. **Zeitüberwachung**: Motor-Schaltzeit wird aufgezeichnet
|
||||||
|
3. **Dynamische Filterung**:
|
||||||
|
- Normal: Einfache Ausreißer-Erkennung
|
||||||
|
- Störzeit: Median-Filter + verschärfte Schwellen
|
||||||
|
|
||||||
|
### Vorteile:
|
||||||
|
- ✅ Reduziert falsche pH-Sprünge erheblich
|
||||||
|
- ✅ Erhält echte pH-Änderungen
|
||||||
|
- ✅ Transparente Anzeige der Filterung
|
||||||
|
- ✅ Ein-/ausschaltbar je nach Bedarf
|
||||||
|
- ✅ Konfigurierbare Parameter
|
||||||
|
|
||||||
|
### Anwendungsfälle:
|
||||||
|
- **Titrationsmessungen**: Vermeidet falsche Äquivalenzpunkte
|
||||||
|
- **Kontinuierliche Überwachung**: Glättere pH-Kurven
|
||||||
|
- **Automatische Dosierung**: Zuverlässigere pH-Regelung
|
||||||
|
|
||||||
|
## Beispiel-Ausgabe
|
||||||
|
```
|
||||||
|
Status: pH gefiltert: 8.45 → 7.82 (Motorstörung)
|
||||||
|
```
|
||||||
|
|
||||||
|
Dies zeigt, dass ein pH-Sprung von 8.45 auf den gefilterten Wert 7.82 reduziert wurde.
|
||||||
Loading…
Reference in New Issue
Block a user