Adalm1000_Battery_SMU/MainCode/adalm1000_logger.py
Jan f75d1dbcf7 MainCode/adalm1000_logger.py aktualisiert
error qgridlayout is not defined
2025-07-02 17:33:36 +02:00

987 lines
39 KiB
Python

# -*- coding: utf-8 -*-
import os
import time
import csv
import threading
from datetime import datetime
import numpy as np
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
from collections import deque
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel,
QPushButton, QLineEdit, QCheckBox, QFrame, QMessageBox, QFileDialog)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject
import pysmu
class DeviceDisconnectedError(Exception):
pass
class MeasurementThread(QObject):
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.voltage_window = []
self.current_window = []
self.start_time = time.time()
def run(self):
self.running = True
while self.running:
try:
samples = self.device.read(self.filter_window_size, 500, True)
if not samples:
raise DeviceDisconnectedError("No samples received")
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
self.voltage_window.append(raw_voltage)
self.current_window.append(raw_current)
if len(self.voltage_window) > self.filter_window_size:
self.voltage_window.pop(0)
self.current_window.pop(0)
voltage = np.mean(self.voltage_window)
current = np.mean(self.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
class BatteryTester(QMainWindow):
def __init__(self):
super().__init__()
# Color scheme
self.bg_color = "#2E3440"
self.fg_color = "#D8DEE9"
self.accent_color = "#5E81AC"
self.warning_color = "#BF616A"
self.success_color = "#A3BE8C"
# Device and measurement state
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)
# Data buffers
self.time_data = deque()
self.voltage_data = deque()
self.current_data = deque()
self.phase_data = deque()
# Initialize UI and device
self.setup_ui()
self.init_device()
# Set window properties
self.setWindowTitle("ADALM1000 - Battery Capacity Tester (CC Test)")
self.resize(1000, 800)
self.setMinimumSize(800, 700)
# Status update timer
self.status_timer = QTimer()
self.status_timer.timeout.connect(self.update_status)
self.status_timer.start(1000) # Update every second
def setup_ui(self):
"""Configure the user interface"""
# Main widget and 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)
# Header area
header_frame = QFrame()
header_frame.setFrameShape(QFrame.NoFrame)
header_layout = QHBoxLayout(header_frame)
header_layout.setContentsMargins(0, 0, 0, 0)
self.title_label = QLabel("ADALM1000 Battery Capacity Tester (CC Test)")
self.title_label.setStyleSheet(f"font-size: 14pt; font-weight: bold; color: {self.accent_color};")
header_layout.addWidget(self.title_label, 1)
# Status indicator
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)
self.connection_label = QLabel("Disconnected")
header_layout.addWidget(self.connection_label)
# Reconnect button
self.reconnect_btn = QPushButton("Reconnect")
self.reconnect_btn.clicked.connect(self.reconnect_device)
header_layout.addWidget(self.reconnect_btn)
self.main_layout.addWidget(header_frame)
# Measurement display
display_frame = QFrame()
display_frame.setFrameShape(QFrame.StyledPanel)
display_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}")
display_layout = QGridLayout(display_frame)
# Measurement values
measurement_labels = [
("Voltage (V)", "V"),
("Current (A)", "A"),
("Test Phase", ""),
("Elapsed Time", "s"),
("Discharge Capacity", "Ah"),
("Charge Capacity", "Ah"),
("Coulomb Eff.", "%"),
("Cycle Count", ""),
]
self.value_labels = {}
for i, (label, unit) in enumerate(measurement_labels):
row = i // 2
col = (i % 2) * 2
lbl = QLabel(f"{label}:")
lbl.setStyleSheet(f"color: {self.fg_color};")
display_layout.addWidget(lbl, row, col)
value_lbl = QLabel("0.000")
value_lbl.setStyleSheet(f"color: {self.fg_color}; font-weight: bold;")
display_layout.addWidget(value_lbl, row, col + 1)
if unit:
unit_lbl = QLabel(unit)
unit_lbl.setStyleSheet(f"color: {self.fg_color};")
display_layout.addWidget(unit_lbl, row, col + 2)
# Store references to important labels
if i == 0:
self.voltage_label = value_lbl
elif i == 1:
self.current_label = value_lbl
elif i == 2:
self.phase_label = value_lbl
elif i == 3:
self.time_label = value_lbl
elif i == 4:
self.capacity_label = value_lbl
elif i == 5:
self.charge_capacity_label = value_lbl
elif i == 6:
self.efficiency_label = value_lbl
elif i == 7:
self.cycle_label = value_lbl
self.main_layout.addWidget(display_frame)
# Control area
controls_frame = QFrame()
controls_frame.setFrameShape(QFrame.NoFrame)
controls_layout = QHBoxLayout(controls_frame)
controls_layout.setContentsMargins(0, 0, 0, 0)
# Parameters frame
params_frame = QFrame()
params_frame.setFrameShape(QFrame.StyledPanel)
params_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}")
params_layout = QGridLayout(params_frame)
# Battery capacity
self.capacity = 0.2
self.capacity_label_input = QLabel("Battery Capacity (Ah):")
self.capacity_label_input.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(self.capacity_label_input, 0, 0)
self.capacity_input = QLineEdit("0.2")
self.capacity_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};")
self.capacity_input.setFixedWidth(60)
params_layout.addWidget(self.capacity_input, 0, 1)
# Charge cutoff
self.charge_cutoff = 1.43
self.charge_cutoff_label = QLabel("Charge Cutoff (V):")
self.charge_cutoff_label.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(self.charge_cutoff_label, 1, 0)
self.charge_cutoff_input = QLineEdit("1.43")
self.charge_cutoff_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};")
self.charge_cutoff_input.setFixedWidth(60)
params_layout.addWidget(self.charge_cutoff_input, 1, 1)
# Discharge cutoff
self.discharge_cutoff = 0.9
self.discharge_cutoff_label = QLabel("Discharge Cutoff (V):")
self.discharge_cutoff_label.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(self.discharge_cutoff_label, 2, 0)
self.discharge_cutoff_input = QLineEdit("0.9")
self.discharge_cutoff_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};")
self.discharge_cutoff_input.setFixedWidth(60)
params_layout.addWidget(self.discharge_cutoff_input, 2, 1)
# Rest time
self.rest_time = 0.25
self.rest_time_label = QLabel("Rest Time (hours):")
self.rest_time_label.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(self.rest_time_label, 3, 0)
self.rest_time_input = QLineEdit("0.25")
self.rest_time_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};")
self.rest_time_input.setFixedWidth(60)
params_layout.addWidget(self.rest_time_input, 3, 1)
# C-rate for test
self.c_rate = 0.1
self.c_rate_label = QLabel("Test C-rate:")
self.c_rate_label.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(self.c_rate_label, 0, 2)
self.c_rate_input = QLineEdit("0.1")
self.c_rate_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};")
self.c_rate_input.setFixedWidth(40)
params_layout.addWidget(self.c_rate_input, 0, 3)
c_rate_note = QLabel("(e.g., 0.2 for C/5)")
c_rate_note.setStyleSheet(f"color: {self.fg_color};")
params_layout.addWidget(c_rate_note, 0, 4)
controls_layout.addWidget(params_frame, 1)
# Button frame
button_frame = QFrame()
button_frame.setFrameShape(QFrame.NoFrame)
button_layout = QVBoxLayout(button_frame)
button_layout.setContentsMargins(0, 0, 0, 0)
self.start_button = QPushButton("START TEST")
self.start_button.setStyleSheet(f"""
QPushButton {{
background-color: {self.accent_color};
color: {self.fg_color};
font-weight: bold;
padding: 6px;
border-radius: 4px;
}}
QPushButton:disabled {{
background-color: #4C566A;
color: #D8DEE9;
}}
""")
self.start_button.clicked.connect(self.start_test)
button_layout.addWidget(self.start_button)
self.stop_button = QPushButton("STOP TEST")
self.stop_button.setStyleSheet(f"""
QPushButton {{
background-color: {self.warning_color};
color: {self.fg_color};
font-weight: bold;
padding: 6px;
border-radius: 4px;
}}
QPushButton:disabled {{
background-color: #4C566A;
color: #D8DEE9;
}}
""")
self.stop_button.clicked.connect(self.stop_test)
self.stop_button.setEnabled(False)
button_layout.addWidget(self.stop_button)
# Continuous mode checkbox
self.continuous_mode_check = QCheckBox("Continuous Mode")
self.continuous_mode_check.setChecked(True)
self.continuous_mode_check.setStyleSheet(f"color: {self.fg_color};")
button_layout.addWidget(self.continuous_mode_check)
controls_layout.addWidget(button_frame)
self.main_layout.addWidget(controls_frame)
# Plot area
self.setup_plot()
# Status bar
self.status_bar = self.statusBar()
self.status_bar.setStyleSheet(f"color: {self.fg_color};")
self.status_bar.showMessage("Ready")
# Apply dark theme
self.setStyleSheet(f"""
QMainWindow {{
background-color: {self.bg_color};
}}
QLabel {{
color: {self.fg_color};
}}
QLineEdit {{
background-color: #3B4252;
color: {self.fg_color};
border: 1px solid #4C566A;
border-radius: 3px;
padding: 2px;
}}
""")
def setup_plot(self):
"""Configure the matplotlib plot"""
self.fig = Figure(figsize=(8, 5), dpi=100, facecolor=self.bg_color)
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')
# Set initial voltage range
voltage_padding = 0.2
min_voltage = max(0, 0.9 - voltage_padding)
max_voltage = 1.43 + voltage_padding
self.ax.set_ylim(min_voltage, max_voltage)
# Voltage plot
self.line_voltage, = self.ax.plot([], [], color='#00BFFF', label='Voltage (V)', linewidth=2)
self.ax.set_ylabel("Voltage (V)", color='#00BFFF')
self.ax.tick_params(axis='y', labelcolor='#00BFFF')
# Current plot (right axis)
self.ax2 = self.ax.twinx()
current_padding = 0.05
test_current = 0.1 * 0.2 # Default values
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='Current (A)', linewidth=2)
self.ax2.set_ylabel("Current (A)", color='r')
self.ax2.tick_params(axis='y', labelcolor='r')
self.ax.set_xlabel('Time (s)', color=self.fg_color)
self.ax.set_title('Battery Test (CC)', color=self.fg_color)
self.ax.tick_params(axis='x', colors=self.fg_color)
self.ax.grid(True, color='#4C566A')
# Position legends
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))
# Embed plot
self.canvas = FigureCanvas(self.fig)
self.canvas.setStyleSheet(f"background-color: {self.bg_color};")
self.main_layout.addWidget(self.canvas, 1)
def init_device(self):
"""Initialize the ADALM1000 device with continuous measurement"""
try:
# Clean up any existing session
if hasattr(self, 'session'):
try:
self.session.end()
del self.session
except:
pass
time.sleep(1)
self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000)
if not self.session.devices:
raise Exception("No ADALM1000 detected - check connections")
self.dev = self.session.devices[0]
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(f"background-color: green; border-radius: 10px;")
self.connection_label.setText("Connected")
self.status_bar.showMessage("Device connected | Ready to measure")
self.session_active = True
self.start_button.setEnabled(True)
# Start measurement thread
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.thread = threading.Thread(target=self.measurement_thread.run, daemon=True)
self.thread.start()
except Exception as e:
self.handle_device_error(str(e))
@pyqtSlot(float, float, float)
def update_measurements(self, voltage, current, current_time):
"""Update measurements from the measurement thread"""
self.time_data.append(current_time)
self.voltage_data.append(voltage)
self.current_data.append(current)
# Update display
self.voltage_label.setText(f"{voltage:.4f}")
self.current_label.setText(f"{current:.4f}")
self.time_label.setText(self.format_time(current_time))
# Update plot periodically
if len(self.time_data) % 10 == 0: # Update plot every 10 samples
self.update_plot()
def update_status(self):
"""Update status information periodically"""
if self.test_running:
# Update capacity calculations if in test mode
if self.measuring and self.time_data:
current_time = time.time() - self.start_time
delta_t = current_time - self.last_update_time
self.last_update_time = current_time
if self.test_phase == "Discharge":
current_current = abs(self.current_data[-1])
self.capacity_ah += current_current * delta_t / 3600
self.capacity_label.setText(f"{self.capacity_ah:.4f}")
elif self.test_phase == "Charge":
current_current = abs(self.current_data[-1])
self.charge_capacity += current_current * delta_t / 3600
self.charge_capacity_label.setText(f"{self.charge_capacity:.4f}")
def start_test(self):
"""Start the full battery test cycle"""
if not self.test_running:
try:
# Get parameters from UI
self.capacity = float(self.capacity_input.text())
self.charge_cutoff = float(self.charge_cutoff_input.text())
self.discharge_cutoff = float(self.discharge_cutoff_input.text())
self.rest_time = float(self.rest_time_input.text())
self.c_rate = float(self.c_rate_input.text())
# Validate inputs
if self.capacity <= 0:
raise ValueError("Battery capacity must be positive")
if self.charge_cutoff <= self.discharge_cutoff:
raise ValueError("Charge cutoff must be higher than discharge cutoff")
if self.c_rate <= 0:
raise ValueError("C-rate must be positive")
self.continuous_mode = self.continuous_mode_check.isChecked()
self.measurement_start_time = time.time()
self.test_start_time = time.time()
test_current = self.c_rate * self.capacity
if test_current > 0.2:
raise ValueError("Current must be ≤200mA (0.2A) for ADALM1000")
# Clear previous data
self.time_data.clear()
self.voltage_data.clear()
self.current_data.clear()
self.phase_data.clear()
self.capacity_ah = 0.0
self.charge_capacity = 0.0
self.coulomb_efficiency = 0.0
self.cycle_count = 0
# Reset plot
self.reset_plot()
# Generate filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.base_filename = os.path.join(self.log_dir, f"battery_test_{timestamp}")
self.current_cycle_file = None
# Start test
self.test_running = True
self.start_time = time.time()
self.last_update_time = time.time()
self.test_phase = "Initial Discharge"
self.phase_label.setText(self.test_phase)
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.status_bar.showMessage(f"Test started | Discharging to {self.discharge_cutoff}V @ {test_current:.3f}A")
# Start test sequence in a new thread
self.test_sequence_thread = threading.Thread(target=self.run_test_sequence, daemon=True)
self.test_sequence_thread.start()
except Exception as e:
QMessageBox.critical(self, "Error", str(e))
def create_cycle_log_file(self):
"""Create a new log file for the current cycle"""
if hasattr(self, 'current_cycle_file') and self.current_cycle_file:
try:
self.current_cycle_file.close()
except Exception as e:
print(f"Error closing previous log file: {e}")
if not os.access(self.log_dir, os.W_OK):
QMessageBox.critical(self, "Error", f"No write permissions in {self.log_dir}")
return False
suffix = 1
while True:
self.filename = f"{self.base_filename}_{suffix}.csv"
if not os.path.exists(self.filename):
break
suffix += 1
try:
self.current_cycle_file = open(self.filename, 'w', newline='')
self.log_writer = csv.writer(self.current_cycle_file)
self.log_writer.writerow(["Time(s)", "Voltage(V)", "Current(A)", "Phase",
"Discharge_Capacity(Ah)", "Charge_Capacity(Ah)",
"Coulomb_Eff(%)", "Cycle"])
self.log_buffer = []
return True
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to create log file: {e}")
return False
def format_time(self, seconds):
"""Convert seconds to hh:mm:ss format"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
def stop_test(self):
"""Request immediate stop of the test"""
if not self.test_running:
return
self.request_stop = True
self.test_running = False
self.measuring = False
self.test_phase = "Idle"
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"Error resetting device: {e}")
self.time_data.clear()
self.voltage_data.clear()
self.current_data.clear()
self.phase_data.clear()
self.capacity_ah = 0.0
self.charge_capacity = 0.0
self.coulomb_efficiency = 0.0
self.reset_plot()
self.status_bar.showMessage("Test stopped - Ready for new test")
self.stop_button.setEnabled(False)
self.start_button.setEnabled(True)
self.finalize_test()
def run_test_sequence(self):
try:
test_current = self.c_rate * self.capacity
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(str(self.cycle_count))
self.create_cycle_log_file()
# 1. Charge phase
self.test_phase = "Charge"
self.phase_label.setText(self.test_phase)
self.status_bar.showMessage(f"Charging to {self.charge_cutoff}V @ {test_current:.3f}A")
self.measuring = True
self.dev.channels['B'].mode = pysmu.Mode.HI_Z
self.dev.channels['A'].mode = pysmu.Mode.SIMV
self.dev.channels['A'].constant(test_current)
self.charge_capacity = 0.0
self.charge_capacity_label.setText(f"{self.charge_capacity:.4f}")
target_voltage = self.charge_cutoff
self.last_update_time = time.time()
while self.test_running and not self.request_stop:
if not self.voltage_data:
time.sleep(0.1)
continue
current_voltage = self.voltage_data[-1]
measured_current = abs(self.current_data[-1])
# Log data
if hasattr(self, 'current_cycle_file'):
self.log_buffer.append([
f"{time.time() - self.start_time:.3f}",
f"{current_voltage:.6f}",
f"{measured_current:.6f}",
self.test_phase,
f"{self.capacity_ah:.4f}",
f"{self.charge_capacity:.4f}",
f"{self.coulomb_efficiency:.1f}",
f"{self.cycle_count}"
])
if len(self.log_buffer) >= 10:
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
if current_voltage >= target_voltage or self.request_stop:
break
time.sleep(0.1)
if self.request_stop or not self.test_running:
break
# 2. Rest period after charge
self.test_phase = "Resting (Post-Charge)"
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_end_time = time.time() + (self.rest_time * 3600)
while time.time() < rest_end_time and self.test_running and not self.request_stop:
time_left = max(0, rest_end_time - time.time())
self.status_bar.showMessage(f"Resting after charge | Time left: {time_left/60:.1f} min")
time.sleep(1)
if self.request_stop or not self.test_running:
break
# 3. Discharge phase
self.test_phase = "Discharge"
self.phase_label.setText(self.test_phase)
self.status_bar.showMessage(f"Discharging to {self.discharge_cutoff}V @ {test_current:.3f}A")
self.measuring = True
self.dev.channels['A'].mode = pysmu.Mode.SIMV
self.dev.channels['A'].constant(-test_current)
self.capacity_ah = 0.0
self.capacity_label.setText(f"{self.capacity_ah:.4f}")
self.last_update_time = time.time()
while self.test_running and not self.request_stop:
if not self.current_data:
time.sleep(0.1)
continue
current_voltage = self.voltage_data[-1]
current_current = abs(self.current_data[-1])
# Log data
if hasattr(self, 'current_cycle_file'):
self.log_buffer.append([
f"{time.time() - self.start_time:.3f}",
f"{current_voltage:.6f}",
f"{current_current:.6f}",
self.test_phase,
f"{self.capacity_ah:.4f}",
f"{self.charge_capacity:.4f}",
f"{self.coulomb_efficiency:.1f}",
f"{self.cycle_count}"
])
if len(self.log_buffer) >= 10:
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
if current_voltage <= self.discharge_cutoff or self.request_stop:
break
if not self.continuous_mode_check.isChecked():
self.test_running = False
self.test_phase = "Idle"
self.phase_label.setText(self.test_phase)
break
# 4. Rest period after discharge
if self.test_running and not self.request_stop:
self.test_phase = "Resting (Post-Discharge)"
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_end_time = time.time() + (self.rest_time * 3600)
while time.time() < rest_end_time and self.test_running and not self.request_stop:
time_left = max(0, rest_end_time - time.time())
self.status_bar.showMessage(f"Resting after discharge | Time left: {time_left/60:.1f} min")
time.sleep(1)
# Calculate Coulomb efficiency
if not self.request_stop and self.charge_capacity > 0:
efficiency = (self.capacity_ah / self.charge_capacity) * 100
self.coulomb_efficiency = efficiency
self.efficiency_label.setText(f"{efficiency:.1f}")
self.status_bar.showMessage(
f"Cycle {self.cycle_count} complete | "
f"Discharge: {self.capacity_ah:.3f}Ah | "
f"Charge: {self.charge_capacity:.3f}Ah | "
f"Efficiency: {efficiency:.1f}%"
)
self.write_cycle_summary()
if self.log_buffer:
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
self.finalize_test()
except Exception as e:
self.status_bar.showMessage(f"Test error: {str(e)}")
self.finalize_test()
def finalize_test(self):
"""Final cleanup after test completes or is stopped"""
self.measuring = False
if hasattr(self, 'dev'):
try:
self.dev.channels['A'].constant(0)
except Exception as e:
print(f"Error resetting device: {e}")
if hasattr(self, 'log_buffer') and self.log_buffer and hasattr(self, 'log_writer'):
try:
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
except Exception as e:
print(f"Error flushing log buffer: {e}")
if hasattr(self, 'current_cycle_file'):
try:
self.current_cycle_file.close()
except Exception as e:
print(f"Error closing log file: {e}")
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
self.request_stop = False
message = (
f"Test safely stopped after discharge phase | "
f"Cycle {self.cycle_count} completed | "
f"Final capacity: {self.capacity_ah:.3f}Ah"
)
self.status_bar.showMessage(message)
QMessageBox.information(
self,
"Test Completed",
f"Test was safely stopped after discharge phase.\n\n"
f"Final discharge capacity: {self.capacity_ah:.3f}Ah\n"
f"Total cycles completed: {self.cycle_count}"
)
def reset_plot(self):
"""Reset the plot completely for a new test"""
self.line_voltage.set_data([], [])
self.line_current.set_data([], [])
self.time_data.clear()
self.voltage_data.clear()
self.current_data.clear()
voltage_padding = 0.2
min_voltage = max(0, self.discharge_cutoff - voltage_padding)
max_voltage = self.charge_cutoff + voltage_padding
self.ax.set_xlim(0, 10)
self.ax.set_ylim(min_voltage, max_voltage)
current_padding = 0.05
test_current = self.c_rate * self.capacity
max_current = test_current * 1.5
self.ax2.set_ylim(-max_current - current_padding, max_current + current_padding)
self.canvas.draw()
def write_cycle_summary(self):
"""Write cycle summary to the current cycle's log file"""
if not hasattr(self, 'current_cycle_file') or not self.current_cycle_file:
return
summary_line = (
f"Cycle {self.cycle_count} Summary - "
f"Discharge={self.capacity_ah:.4f}Ah, "
f"Charge={self.charge_capacity:.4f}Ah, "
f"Efficiency={self.coulomb_efficiency:.1f}%"
)
try:
if self.log_buffer:
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
self.current_cycle_file.write(summary_line + "\n")
self.current_cycle_file.flush()
except Exception as e:
print(f"Error writing cycle summary: {e}")
def update_plot(self):
"""Optimized plot update with change detection"""
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):
"""Auto-scale plot axes with appropriate padding and strict boundaries"""
if not self.time_data:
return
min_time = 0
max_time = self.time_data[-1]
current_xlim = self.ax.get_xlim()
if max_time > current_xlim[1] * 0.95:
new_max = max_time * 1.05
self.ax.set_xlim(min_time, new_max)
self.ax2.set_xlim(min_time, new_max)
voltage_padding = 0.2
if self.voltage_data:
min_voltage = max(0, min(self.voltage_data) - voltage_padding)
max_voltage = min(5.0, max(self.voltage_data) + voltage_padding)
current_ylim = self.ax.get_ylim()
if (abs(current_ylim[0] - min_voltage) > 0.1 or abs(current_ylim[1] - max_voltage) > 0.1):
self.ax.set_ylim(min_voltage, max_voltage)
current_padding = 0.05
if self.current_data:
min_current = max(-0.25, min(self.current_data) - current_padding)
max_current = min(0.25, max(self.current_data) + current_padding)
current_ylim2 = self.ax2.get_ylim()
if (abs(current_ylim2[0] - min_current) > 0.02 or abs(current_ylim2[1] - max_current) > 0.02):
self.ax2.set_ylim(min_current, max_current)
@pyqtSlot(str)
def handle_device_error(self, error):
"""Handle device connection errors"""
error_msg = str(error)
print(f"Device error: {error_msg}")
if hasattr(self, 'session'):
try:
if self.session_active:
self.session.end()
del self.session
except Exception as e:
print(f"Error cleaning up session: {e}")
self.status_light.setStyleSheet(f"background-color: red; border-radius: 10px;")
self.connection_label.setText("Disconnected")
self.status_bar.showMessage(f"Device error: {error_msg}")
self.session_active = False
self.test_running = False
self.continuous_mode = False
self.measuring = False
self.start_button.setEnabled(False)
self.stop_button.setEnabled(False)
self.time_data.clear()
self.voltage_data.clear()
self.current_data.clear()
if hasattr(self, 'line_voltage') and hasattr(self, 'line_current'):
self.line_voltage.set_data([], [])
self.line_current.set_data([], [])
self.ax.set_xlim(0, 1)
self.ax2.set_xlim(0, 1)
self.canvas.draw()
self.attempt_reconnect()
def attempt_reconnect(self):
"""Attempt to reconnect automatically"""
QMessageBox.critical(
self,
"Device Connection Error",
"Could not connect to ADALM1000\n\n"
"1. Check USB cable connection\n"
"2. The device will attempt to reconnect automatically"
)
QTimer.singleShot(1000, self.reconnect_device)
def reconnect_device(self):
"""Reconnect the device with proper cleanup"""
self.status_bar.showMessage("Attempting to reconnect...")
if hasattr(self, 'session'):
try:
if self.session_active:
self.session.end()
del self.session
except:
pass
self.test_running = False
self.continuous_mode = False
self.measuring = False
if hasattr(self, 'measurement_thread'):
self.measurement_thread.stop()
time.sleep(1.5)
try:
self.init_device()
if self.session_active:
self.status_bar.showMessage("Reconnected successfully")
return
except Exception as e:
print(f"Reconnect failed: {e}")
self.status_bar.showMessage("Reconnect failed - will retry...")
QTimer.singleShot(2000, self.reconnect_device)
def closeEvent(self, event):
"""Clean up on window close"""
self.test_running = False
self.measuring = False
self.session_active = False
if hasattr(self, 'measurement_thread'):
self.measurement_thread.stop()
if hasattr(self, 'session') and self.session:
try:
self.session.end()
except Exception as e:
print(f"Error ending session: {e}")
event.accept()
if __name__ == "__main__":
app = QApplication([])
try:
window = BatteryTester()
window.show()
app.exec_()
except Exception as e:
QMessageBox.critical(None, "Fatal Error", f"Application failed: {str(e)}")