Adalm1000_Battery_SMU/MainCode/adalm1000_logger.py

843 lines
32 KiB
Python

# -*- 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()
self.wait(500)
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"""
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()
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:
self.session.end()
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_()