# -*- coding: utf-8 -*- import os import time import csv import threading from datetime import datetime import numpy as np from collections import deque # Warnungen unterdrücken os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' os.environ['LIBUSB_DEBUG'] = '0' from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QLineEdit, QFrame, QCheckBox, QMessageBox, QFileDialog) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject, QThread from PyQt5.QtGui import QColor, QPalette import pysmu import matplotlib matplotlib.use('Qt5Agg') from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class DeviceDisconnectedError(Exception): pass class MeasurementThread(QThread): update_signal = pyqtSignal(float, float, float) error_signal = pyqtSignal(str) def __init__(self, device, interval=0.1): super().__init__() self.device = device self.interval = interval self._running = False self.filter_window_size = 10 self.start_time = time.time() def run(self): self._running = True voltage_window = [] current_window = [] while self._running: try: samples = self.device.read(self.filter_window_size, 500, True) if not samples: raise DeviceDisconnectedError("Keine Samples empfangen") raw_voltage = np.mean([s[1][0] for s in samples]) raw_current = np.mean([s[0][1] for s in samples]) current_time = time.time() - self.start_time # Gleitender Mittelwertfilter voltage_window.append(raw_voltage) current_window.append(raw_current) if len(voltage_window) > self.filter_window_size: voltage_window.pop(0) current_window.pop(0) voltage = np.mean(voltage_window) current = np.mean(current_window) self.update_signal.emit(voltage, current, current_time) time.sleep(max(0.05, self.interval)) except Exception as e: self.error_signal.emit(str(e)) break def stop(self): self._running = False if self.isRunning(): self.quit() if not self.wait(500): # Wait up to 500ms for clean exit print("Warning: Thread didn't exit cleanly, terminating") self.terminate() class BatteryTester(QMainWindow): def __init__(self): super().__init__() # Initialisierung aller Attribute self.time_data = deque() self.voltage_data = deque() self.current_data = deque() self.phase_data = deque() # Testvariablen self.test_phase = "Bereit" self.capacity_ah = 0.0 self.charge_capacity = 0.0 self.coulomb_efficiency = 0.0 self.cycle_count = 0 # Farbschema self.bg_color = QColor(46, 52, 64) self.fg_color = QColor(216, 222, 233) self.accent_color = QColor(94, 129, 172) self.warning_color = QColor(191, 97, 106) self.success_color = QColor(163, 190, 140) # Gerätestatus self.session_active = False self.measuring = False self.test_running = False self.continuous_mode = False self.request_stop = False self.interval = 0.1 self.log_dir = os.path.expanduser("~/adalm1000/logs") os.makedirs(self.log_dir, exist_ok=True) # Thread-Management self.measurement_thread = None self.test_thread = None # UI initialisieren self.setup_ui() # Gerät verzögert initialisieren QTimer.singleShot(100, self.safe_init_device) def setup_ui(self): """Konfiguriert die Benutzeroberfläche""" self.setWindowTitle("ADALM1000 - Batteriekapazitätstester (CC-Test)") self.resize(1000, 800) self.setMinimumSize(800, 700) # Hintergrundfarbe setzen palette = self.palette() palette.setColor(QPalette.Window, self.bg_color) palette.setColor(QPalette.WindowText, self.fg_color) palette.setColor(QPalette.Base, QColor(59, 66, 82)) palette.setColor(QPalette.Text, self.fg_color) palette.setColor(QPalette.Button, self.accent_color) palette.setColor(QPalette.ButtonText, self.fg_color) self.setPalette(palette) # Hauptwidget und Layout self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QVBoxLayout(self.central_widget) self.main_layout.setContentsMargins(10, 10, 10, 10) self.main_layout.setSpacing(10) # Kopfbereich header_frame = QWidget() header_layout = QHBoxLayout(header_frame) header_layout.setContentsMargins(0, 0, 0, 0) self.title_label = QLabel("ADALM1000 Batteriekapazitätstester (CC-Test)") self.title_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {self.accent_color.name()};") header_layout.addWidget(self.title_label, 1) # Statusindikator self.connection_label = QLabel("Getrennt") header_layout.addWidget(self.connection_label) self.status_light = QLabel() self.status_light.setFixedSize(20, 20) self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") header_layout.addWidget(self.status_light) # Verbinden-Button self.reconnect_btn = QPushButton("Verbinden") self.reconnect_btn.clicked.connect(self.reconnect_device) header_layout.addWidget(self.reconnect_btn) self.main_layout.addWidget(header_frame) # Messanzeige display_frame = QFrame() display_frame.setFrameShape(QFrame.StyledPanel) display_layout = QGridLayout(display_frame) measurement_labels = [ ("Spannung (V)", "V"), ("Strom (A)", "A"), ("Testphase", ""), ("Verstrichene Zeit", "s"), ("Entladekapazität", "Ah"), ("Ladekapazität", "Ah"), ("Coulomb-Eff.", "%"), ("Zykluszählung", ""), ] for i, (label, unit) in enumerate(measurement_labels): row = i // 2 col = (i % 2) * 2 lbl = QLabel(f"{label}:") lbl.setStyleSheet("font-size: 11px;") display_layout.addWidget(lbl, row, col) value_label = QLabel("0.000") value_label.setStyleSheet("font-size: 12px; font-weight: bold;") display_layout.addWidget(value_label, row, col + 1) if unit: unit_label = QLabel(unit) display_layout.addWidget(unit_label, row, col + 2) if i == 0: self.voltage_label = value_label elif i == 1: self.current_label = value_label elif i == 2: self.phase_label = value_label self.phase_label.setText(self.test_phase) elif i == 3: self.time_label = value_label elif i == 4: self.capacity_label = value_label elif i == 5: self.charge_capacity_label = value_label elif i == 6: self.efficiency_label = value_label elif i == 7: self.cycle_label = value_label self.main_layout.addWidget(display_frame) # Steuerbereich controls_frame = QWidget() controls_layout = QHBoxLayout(controls_frame) controls_layout.setContentsMargins(0, 0, 0, 0) # Parameterrahmen params_frame = QFrame() params_frame.setFrameShape(QFrame.StyledPanel) params_layout = QGridLayout(params_frame) # Batteriekapazität params_layout.addWidget(QLabel("Batteriekapazität (Ah):"), 0, 0) self.capacity_input = QLineEdit("0.2") self.capacity_input.setFixedWidth(80) params_layout.addWidget(self.capacity_input, 0, 1) # Lade-Endspannung params_layout.addWidget(QLabel("Lade-Endspannung (V):"), 1, 0) self.charge_cutoff_input = QLineEdit("1.43") self.charge_cutoff_input.setFixedWidth(80) params_layout.addWidget(self.charge_cutoff_input, 1, 1) # Entlade-Endspannung params_layout.addWidget(QLabel("Entlade-Endspannung (V):"), 2, 0) self.discharge_cutoff_input = QLineEdit("0.9") self.discharge_cutoff_input.setFixedWidth(80) params_layout.addWidget(self.discharge_cutoff_input, 2, 1) # Ruhezeit params_layout.addWidget(QLabel("Ruhezeit (Stunden):"), 3, 0) self.rest_time_input = QLineEdit("0.25") self.rest_time_input.setFixedWidth(80) params_layout.addWidget(self.rest_time_input, 3, 1) # C-Rate für Test params_layout.addWidget(QLabel("Test-C-Rate:"), 0, 2) self.c_rate_input = QLineEdit("0.1") self.c_rate_input.setFixedWidth(60) params_layout.addWidget(self.c_rate_input, 0, 3) params_layout.addWidget(QLabel("(z.B. 0.2 für C/5)"), 0, 4) controls_layout.addWidget(params_frame, 1) # Button-Bereich button_frame = QWidget() button_layout = QVBoxLayout(button_frame) button_layout.setContentsMargins(0, 0, 0, 0) self.start_button = QPushButton("TEST STARTEN") self.start_button.clicked.connect(self.start_test) self.start_button.setStyleSheet(f"background-color: {self.accent_color.name()}; font-weight: bold;") self.start_button.setEnabled(False) button_layout.addWidget(self.start_button) self.stop_button = QPushButton("TEST STOPPEN") self.stop_button.clicked.connect(self.stop_test) self.stop_button.setStyleSheet(f"background-color: {self.warning_color.name()}; font-weight: bold;") self.stop_button.setEnabled(False) button_layout.addWidget(self.stop_button) # Kontinuierlicher Modus self.continuous_check = QCheckBox("Kontinuierlicher Modus") self.continuous_check.setChecked(True) button_layout.addWidget(self.continuous_check) controls_layout.addWidget(button_frame) self.main_layout.addWidget(controls_frame) # Plotbereich self.setup_plot() self.main_layout.addWidget(self.plot_widget, 1) # Statusleiste self.status_bar = QLabel("Bereit") self.status_bar.setStyleSheet("font-size: 10px; padding: 5px;") self.main_layout.addWidget(self.status_bar) def setup_plot(self): """Konfiguriert den Matplotlib-Plot""" self.plot_widget = QWidget() plot_layout = QVBoxLayout(self.plot_widget) self.fig = Figure(figsize=(8, 5), dpi=100, facecolor='#2E3440') self.fig.subplots_adjust(left=0.1, right=0.88, top=0.9, bottom=0.15) self.ax = self.fig.add_subplot(111) self.ax.set_facecolor('#3B4252') # Initiale Spannungsbereich voltage_padding = 0.2 min_voltage = max(0, float(self.discharge_cutoff_input.text()) - voltage_padding) max_voltage = float(self.charge_cutoff_input.text()) + voltage_padding self.ax.set_ylim(min_voltage, max_voltage) # Spannungsplot self.line_voltage, = self.ax.plot([], [], color='#00BFFF', label='Spannung (V)', linewidth=2) self.ax.set_ylabel("Spannung (V)", color='#00BFFF') self.ax.tick_params(axis='y', labelcolor='#00BFFF') # Stromplot (rechte Achse) self.ax2 = self.ax.twinx() current_padding = 0.05 test_current = float(self.c_rate_input.text()) * float(self.capacity_input.text()) max_current = test_current * 1.5 self.ax2.set_ylim(-max_current - current_padding, max_current + current_padding) self.line_current, = self.ax2.plot([], [], 'r-', label='Strom (A)', linewidth=2) self.ax2.set_ylabel("Strom (A)", color='r') self.ax2.tick_params(axis='y', labelcolor='r') self.ax.set_xlabel('Zeit (s)', color=self.fg_color.name()) self.ax.set_title('Batterietest (CC)', color=self.fg_color.name()) self.ax.tick_params(axis='x', colors=self.fg_color.name()) self.ax.grid(True, color='#4C566A') # Legenden positionieren self.ax.legend(loc='upper left', bbox_to_anchor=(0.01, 0.99)) self.ax2.legend(loc='upper right', bbox_to_anchor=(0.99, 0.99)) # Plot einbetten self.canvas = FigureCanvas(self.fig) plot_layout.addWidget(self.canvas) def safe_init_device(self): """Sichere Geräteinitialisierung mit Fehlerbehandlung""" try: self.init_device() except Exception as e: self.handle_device_error(str(e)) def init_device(self): """Initialisiert das ADALM1000-Gerät""" # Temporarily enable USB debugging os.environ['LIBUSB_DEBUG'] = '3' # Set to 0 in production self.cleanup_device() try: # Verzögerung zur Vermeidung von "Device busy" Fehlern time.sleep(1.5) self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000) if not self.session.devices: raise Exception("Kein ADALM1000 erkannt - Verbindung prüfen") self.dev = self.session.devices[0] # Kanäle zurücksetzen self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['B'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) self.dev.channels['B'].constant(0) self.session.start(0) self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") self.connection_label.setText("Verbunden") self.status_bar.setText("Gerät verbunden | Bereit zur Messung") self.session_active = True self.start_button.setEnabled(True) # Mess-Thread starten self.start_measurement_thread() except Exception as e: raise Exception(f"Geräteinitialisierung fehlgeschlagen: {str(e)}") def cleanup_device(self): """Bereinigt Geräteressourcen""" # Mess-Thread stoppen if self.measurement_thread is not None: try: self.measurement_thread.stop() # Wait for thread to finish if not self.measurement_thread.wait(1000): # 1 second timeout print("Warning: Measurement thread didn't stop cleanly") self.measurement_thread = None except Exception as e: print(f"Fehler beim Stoppen des Mess-Threads: {e}") # Sitzung bereinigen if hasattr(self, 'session'): try: if self.session_active: # Add small delay before ending session time.sleep(0.1) self.session.end() self.session_active = False del self.session except Exception as e: print(f"Fehler beim Bereinigen der Sitzung: {e}") finally: self.session_active = False # UI-Status zurücksetzen self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") self.connection_label.setText("Getrennt") self.start_button.setEnabled(False) self.stop_button.setEnabled(False) def start_measurement_thread(self): """Startet den Mess-Thread""" if self.measurement_thread is not None: self.measurement_thread.stop() self.measurement_thread = MeasurementThread(self.dev, self.interval) self.measurement_thread.update_signal.connect(self.update_measurements) self.measurement_thread.error_signal.connect(self.handle_device_error) self.measurement_thread.start() def reconnect_device(self): """Versucht, das Gerät erneut zu verbinden""" self.status_bar.setText("Versuche erneut zu verbinden...") self.cleanup_device() QTimer.singleShot(2000, self.safe_init_device) # Mit Verzögerung erneut versuchen def handle_device_error(self, error_msg): """Behandelt Gerätefehler""" print(f"Gerätefehler: {error_msg}") self.status_bar.setText(f"Gerätefehler: {error_msg}") self.cleanup_device() # Nur Meldung anzeigen, wenn Fenster sichtbar ist if self.isVisible(): QMessageBox.critical( self, "Gerätefehler", f"Gerätefehler aufgetreten:\n{error_msg}\n\n" "1. USB-Verbindung prüfen\n" "2. Manuell erneut verbinden\n" "3. Anwendung neu starten bei anhaltenden Problemen" ) def start_test(self): """Startet den vollständigen Batterietestzyklus""" if not self.test_running: try: # Eingabewerte holen und validieren capacity = float(self.capacity_input.text()) charge_cutoff = float(self.charge_cutoff_input.text()) discharge_cutoff = float(self.discharge_cutoff_input.text()) c_rate = float(self.c_rate_input.text()) if capacity <= 0: raise ValueError("Batteriekapazität muss positiv sein") if charge_cutoff <= discharge_cutoff: raise ValueError("Lade-Endspannung muss höher sein als Entlade-Endspannung") if c_rate <= 0: raise ValueError("C-Rate muss positiv sein") self.continuous_mode = self.continuous_check.isChecked() test_current = c_rate * capacity if test_current > 0.2: raise ValueError("Strom muss ≤200mA (0,2A) für ADALM1000 sein") # Daten zurücksetzen self.time_data.clear() self.voltage_data.clear() self.current_data.clear() self.capacity_ah = 0.0 self.charge_capacity = 0.0 self.coulomb_efficiency = 0.0 self.cycle_count = 0 self.reset_plot() # Protokolldatei vorbereiten timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.base_filename = os.path.join(self.log_dir, f"battery_test_{timestamp}") # Test starten self.test_running = True self.start_time = time.time() self.test_phase = "Initiale Entladung" self.phase_label.setText(self.test_phase) self.start_button.setEnabled(False) self.stop_button.setEnabled(True) self.status_bar.setText(f"Test gestartet | Entladen auf {discharge_cutoff}V @ {test_current:.3f}A") # Testsequenz in eigenem Thread starten self.test_thread = threading.Thread(target=self.run_test_sequence, daemon=True) self.test_thread.start() except Exception as e: QMessageBox.critical(self, "Fehler", str(e)) def run_test_sequence(self): """Haupttestsequenz für Lade-/Entladezyklen""" try: test_current = float(self.c_rate_input.text()) * float(self.capacity_input.text()) charge_cutoff = float(self.charge_cutoff_input.text()) discharge_cutoff = float(self.discharge_cutoff_input.text()) while self.test_running and (self.continuous_mode or self.cycle_count == 0): self.request_stop = False self.cycle_count += 1 self.cycle_label.setText(f"{self.cycle_count}") # Neue Protokolldatei für diesen Zyklus if not self.create_cycle_log_file(): break # Ladephase self.execute_charge_phase(test_current, charge_cutoff) if self.request_stop or not self.test_running: break # Ruhephase nach Ladung self.execute_rest_phase("Nachladung") if self.request_stop or not self.test_running: break # Entladephase self.execute_discharge_phase(test_current, discharge_cutoff) if not self.continuous_check.isChecked(): self.test_running = False break # Ruhephase nach Entladung if self.test_running and not self.request_stop: self.execute_rest_phase("Nach Entladung") # Coulomb-Effizienz berechnen if not self.request_stop and self.charge_capacity > 0: self.coulomb_efficiency = (self.capacity_ah / self.charge_capacity) * 100 self.efficiency_label.setText(f"{self.coulomb_efficiency:.1f}") # Status aktualisieren self.status_bar.setText( f"Zyklus {self.cycle_count} abgeschlossen | " f"Entladung: {self.capacity_ah:.3f}Ah | " f"Ladung: {self.charge_capacity:.3f}Ah | " f"Effizienz: {self.coulomb_efficiency:.1f}%" ) # Zyklusprotokoll schreiben self.write_cycle_summary() # Test abschließen self.finalize_test() except Exception as e: self.handle_device_error(str(e)) self.finalize_test() def execute_charge_phase(self, current, target_voltage): """Führt die Ladephase durch""" self.test_phase = "Laden" self.phase_label.setText(self.test_phase) self.status_bar.setText(f"Laden auf {target_voltage}V @ {current:.3f}A") self.measuring = True self.dev.channels['A'].mode = pysmu.Mode.SIMV self.dev.channels['A'].constant(current) self.charge_capacity = 0.0 self.charge_capacity_label.setText("0.000") last_update = time.time() while self.test_running and not self.request_stop: if not self.voltage_data: time.sleep(0.1) continue now = time.time() delta_t = now - last_update last_update = now measured_current = abs(self.current_data[-1]) self.charge_capacity += measured_current * delta_t / 3600 self.charge_capacity_label.setText(f"{self.charge_capacity:.4f}") current_voltage = self.voltage_data[-1] if current_voltage >= target_voltage or self.request_stop: break time.sleep(0.1) def execute_discharge_phase(self, current, target_voltage): """Führt die Entladephase durch""" self.test_phase = "Entladen" self.phase_label.setText(self.test_phase) self.status_bar.setText(f"Entladen auf {target_voltage}V @ {current:.3f}A") self.measuring = True self.dev.channels['A'].mode = pysmu.Mode.SIMV self.dev.channels['A'].constant(-current) self.capacity_ah = 0.0 self.capacity_label.setText("0.000") last_update = time.time() while self.test_running and not self.request_stop: if not self.current_data: time.sleep(0.1) continue now = time.time() delta_t = now - last_update last_update = now measured_current = abs(self.current_data[-1]) self.capacity_ah += measured_current * delta_t / 3600 self.capacity_label.setText(f"{self.capacity_ah:.4f}") current_voltage = self.voltage_data[-1] if current_voltage <= target_voltage or self.request_stop: break time.sleep(0.1) def execute_rest_phase(self, phase_name): """Führt eine Ruhephase durch""" self.test_phase = f"Ruhen ({phase_name})" self.phase_label.setText(self.test_phase) self.measuring = False self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) rest_time = float(self.rest_time_input.text()) * 3600 rest_end = time.time() + rest_time while time.time() < rest_end and self.test_running and not self.request_stop: time_left = max(0, rest_end - time.time()) self.status_bar.setText( f"Ruhen ({phase_name}) | " f"Verbleibende Zeit: {time_left/60:.1f} min" ) time.sleep(1) def create_cycle_log_file(self): """Erstellt eine neue Protokolldatei für den aktuellen Zyklus""" try: suffix = 1 while True: self.filename = f"{self.base_filename}_{suffix}.csv" if not os.path.exists(self.filename): break suffix += 1 self.current_cycle_file = open(self.filename, 'w', newline='') self.log_writer = csv.writer(self.current_cycle_file) self.log_writer.writerow([ "Zeit(s)", "Spannung(V)", "Strom(A)", "Phase", "Entladekapazität(Ah)", "Ladekapazität(Ah)", "Coulomb-Eff.(%)", "Zyklus" ]) self.log_buffer = [] return True except Exception as e: self.handle_device_error(f"Protokolldatei konnte nicht erstellt werden: {e}") return False def write_cycle_summary(self): """Schreibt eine Zykluszusammenfassung in die Protokolldatei""" if hasattr(self, 'current_cycle_file') and self.current_cycle_file: try: if self.log_buffer: self.log_writer.writerows(self.log_buffer) self.log_buffer.clear() summary = ( f"Zyklus {self.cycle_count} Zusammenfassung - " f"Entladung={self.capacity_ah:.4f}Ah, " f"Ladung={self.charge_capacity:.4f}Ah, " f"Effizienz={self.coulomb_efficiency:.1f}%" ) self.current_cycle_file.write(summary + "\n") self.current_cycle_file.flush() except Exception as e: print(f"Fehler beim Schreiben der Zykluszusammenfassung: {e}") def stop_test(self): """Stoppt den laufenden Test sicher""" if not self.test_running: return self.request_stop = True self.test_running = False self.measuring = False self.test_phase = "Bereit" self.phase_label.setText(self.test_phase) if hasattr(self, 'dev'): try: self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) except Exception as e: print(f"Fehler beim Zurücksetzen des Geräts: {e}") # UI aktualisieren self.status_bar.setText("Test gestoppt - Bereit für neuen Test") self.stop_button.setEnabled(False) self.start_button.setEnabled(True) # Testdaten finalisieren QTimer.singleShot(100, self.finalize_test) def finalize_test(self): """Finale Bereinigung nach Testende""" # Protokolldaten schreiben if hasattr(self, 'log_buffer') and self.log_buffer: try: self.log_writer.writerows(self.log_buffer) self.log_buffer.clear() except Exception as e: print(f"Fehler beim Schreiben des Protokollpuffers: {e}") # Protokolldatei schließen if hasattr(self, 'current_cycle_file'): try: self.current_cycle_file.close() except Exception as e: print(f"Fehler beim Schließen der Protokolldatei: {e}") # Benachrichtigung anzeigen QMessageBox.information( self, "Test abgeschlossen", f"Test wurde sicher beendet.\n\n" f"Entladekapazität: {self.capacity_ah:.3f}Ah\n" f"Abgeschlossene Zyklen: {self.cycle_count}" ) def update_measurements(self, voltage, current, current_time): """Aktualisiert die Messwerte im UI""" self.time_data.append(current_time) self.voltage_data.append(voltage) self.current_data.append(current) # UI aktualisieren self.voltage_label.setText(f"{voltage:.4f}") self.current_label.setText(f"{current:.4f}") self.time_label.setText(self.format_time(current_time)) # Plot regelmäßig aktualisieren if len(self.time_data) % 10 == 0: self.update_plot() # Daten protokollieren, wenn Test läuft if self.test_running and hasattr(self, 'current_cycle_file'): self.log_buffer.append([ f"{current_time:.3f}", f"{voltage:.6f}", f"{current:.6f}", self.test_phase, f"{self.capacity_ah:.4f}", f"{self.charge_capacity:.4f}", f"{self.coulomb_efficiency:.1f}", f"{self.cycle_count}" ]) # Daten in Blöcken schreiben if len(self.log_buffer) >= 10: with open(self.filename, 'a', newline='') as f: writer = csv.writer(f) writer.writerows(self.log_buffer) self.log_buffer.clear() def update_plot(self): """Aktualisiert den Plot mit neuen Daten""" if not self.time_data: return self.line_voltage.set_data(self.time_data, self.voltage_data) self.line_current.set_data(self.time_data, self.current_data) self.auto_scale_axes() self.canvas.draw_idle() def auto_scale_axes(self): """Passt die Plotachsen automatisch an""" if not self.time_data: return # X-Achse anpassen max_time = self.time_data[-1] current_xlim = self.ax.get_xlim() if max_time > current_xlim[1] * 0.95: self.ax.set_xlim(0, max_time * 1.05) self.ax2.set_xlim(0, max_time * 1.05) # Y-Achsen anpassen if self.voltage_data: voltage_padding = 0.2 min_v = max(0, min(self.voltage_data) - voltage_padding) max_v = min(5.0, max(self.voltage_data) + voltage_padding) self.ax.set_ylim(min_v, max_v) if self.current_data: current_padding = 0.05 min_c = max(-0.25, min(self.current_data) - current_padding) max_c = min(0.25, max(self.current_data) + current_padding) self.ax2.set_ylim(min_c, max_c) @staticmethod def format_time(seconds): """Formatiert Sekunden als HH:MM:SS""" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) seconds = int(seconds % 60) return f"{hours:02d}:{minutes:02d}:{seconds:02d}" def reset_plot(self): """Setzt den Plot zurück""" self.line_voltage.set_data([], []) self.line_current.set_data([], []) # Initiale Achsenbereiche setzen voltage_padding = 0.2 min_voltage = max(0, float(self.discharge_cutoff_input.text()) - voltage_padding) max_voltage = float(self.charge_cutoff_input.text()) + voltage_padding self.ax.set_xlim(0, 10) self.ax.set_ylim(min_voltage, max_voltage) current_padding = 0.05 test_current = float(self.c_rate_input.text()) * float(self.capacity_input.text()) max_current = test_current * 1.5 self.ax2.set_ylim(-max_current - current_padding, max_current + current_padding) self.canvas.draw() def closeEvent(self, event): """Bereinigt beim Schließen des Fensters""" self.cleanup_device() event.accept() if __name__ == "__main__": app = QApplication([]) app.setStyle('Fusion') window = BatteryTester() window.show() app.exec_()