Adalm1000_Battery_SMU/MainCode/adalm1000_logger.py
Jan 6db656c71b MainCode/adalm1000_logger.py aktualisiert
change cyclecount to beginning

This will ensure your plot:

    Starts with a reasonable view of your expected voltage range

    Maintains good visibility of the key areas (charge/discharge cutoffs)

    Doesn't zoom out too far when there are measurement spikes

    Has better overall framing of the data
2025-05-24 01:33:36 +02:00

904 lines
39 KiB
Python

# -*- coding: utf-8 -*-
import os
import time
import csv
import threading
from datetime import datetime
import tkinter as tk
from tkinter import ttk, messagebox
import pysmu
import numpy as np
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from collections import deque
class DeviceDisconnectedError(Exception):
pass
class BatteryTester:
def __init__(self, root):
# Color scheme
self.bg_color = "#2E3440"
self.fg_color = "#D8DEE9"
self.accent_color = "#5E81AC"
self.warning_color = "#BF616A"
self.success_color = "#A3BE8C"
# Main window configuration
self.root = root
self.root.title("ADALM1000 - Battery Capacity Tester (CC Test)")
self.root.geometry("1000x800")
self.root.minsize(800, 700)
self.root.configure(bg=self.bg_color)
# 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)
# Battery test parameters
self.capacity = tk.DoubleVar(value=0.2) # Battery capacity in Ah
self.charge_cutoff = tk.DoubleVar(value=1.45) # Charge cutoff voltage
self.discharge_cutoff = tk.DoubleVar(value=0.9) # Discharge cutoff voltage
self.rest_time = tk.DoubleVar(value=0.1) # Rest time in hours
self.c_rate = tk.DoubleVar(value=0.1) # C-rate for test (default C/5 = 0.2)
# Test progress tracking
self.test_phase = tk.StringVar(value="Idle")
self.capacity_ah = tk.DoubleVar(value=0.0)
self.charge_capacity = tk.DoubleVar(value=0.0) # capacity tracking
self.coulomb_efficiency = tk.DoubleVar(value=0.0) # efficiency calculation
self.cycle_count = tk.IntVar(value=0) # cycle counting
# 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()
# Ensure proper cleanup
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def setup_ui(self):
"""Configure the user interface"""
self.style = ttk.Style()
self.style.theme_use('clam')
# Configure styles
self.style.configure('.', background=self.bg_color, foreground=self.fg_color)
self.style.configure('TFrame', background=self.bg_color)
self.style.configure('TLabel', background=self.bg_color, foreground=self.fg_color)
self.style.configure('TButton', background=self.accent_color, foreground=self.fg_color,
padding=6, font=('Helvetica', 10, 'bold'))
self.style.map('TButton',
background=[('active', self.accent_color), ('disabled', '#4C566A')],
foreground=[('active', self.fg_color), ('disabled', '#D8DEE9')])
self.style.configure('TEntry', fieldbackground="#3B4252", foreground=self.fg_color)
self.style.configure('Header.TLabel', font=('Helvetica', 14, 'bold'), foreground=self.accent_color)
self.style.configure('Value.TLabel', font=('Helvetica', 12, 'bold'))
self.style.configure('Status.TLabel', font=('Helvetica', 10))
self.style.configure('Warning.TButton', background=self.warning_color)
self.style.configure('Success.TButton', background=self.success_color)
# Main layout
self.content_frame = ttk.Frame(self.root)
self.content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Header area
header_frame = ttk.Frame(self.content_frame)
header_frame.pack(fill=tk.X, pady=(0, 20))
ttk.Label(header_frame, text="ADALM1000 Battery Capacity Tester (CC Test)", style='Header.TLabel').pack(side=tk.LEFT)
# Status indicator
self.status_light = tk.Canvas(header_frame, width=20, height=20, bg=self.bg_color, bd=0, highlightthickness=0)
self.status_light.pack(side=tk.RIGHT, padx=10)
self.status_indicator = self.status_light.create_oval(2, 2, 18, 18, fill='red')
self.connection_label = ttk.Label(header_frame, text="Disconnected")
self.connection_label.pack(side=tk.RIGHT)
# Reconnect button
self.reconnect_btn = ttk.Button(header_frame, text="Reconnect", command=self.reconnect_device)
self.reconnect_btn.pack(side=tk.RIGHT, padx=10)
# Measurement display
display_frame = ttk.LabelFrame(self.content_frame, text=" Live Measurements ", padding=15)
display_frame.pack(fill=tk.BOTH, expand=False)
# 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", ""),
]
for i, (label, unit) in enumerate(measurement_labels):
ttk.Label(display_frame, text=f"{label}:", font=('Helvetica', 11)).grid(row=i//2, column=(i%2)*2, sticky=tk.W, pady=5)
value_label = ttk.Label(display_frame, text="0.000", style='Value.TLabel')
value_label.grid(row=i//2, column=(i%2)*2+1, sticky=tk.W, padx=10)
if unit:
ttk.Label(display_frame, text=unit).grid(row=i//2, column=(i%2)*2+2, sticky=tk.W)
if i == 0:
self.voltage_label = value_label
elif i == 1:
self.current_label = value_label
elif i == 2:
self.phase_label = value_label
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
# Control area
controls_frame = ttk.Frame(self.content_frame)
controls_frame.pack(fill=tk.X, pady=(10, 10), padx=0)
# Parameters frame
params_frame = ttk.LabelFrame(controls_frame, text="Test Parameters", padding=10)
params_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Battery capacity
ttk.Label(params_frame, text="Battery Capacity (Ah):").grid(row=0, column=0, sticky=tk.W)
ttk.Entry(params_frame, textvariable=self.capacity, width=6).grid(row=0, column=1, padx=5, sticky=tk.W)
# Charge cutoff
ttk.Label(params_frame, text="Charge Cutoff (V):").grid(row=1, column=0, sticky=tk.W)
ttk.Entry(params_frame, textvariable=self.charge_cutoff, width=6).grid(row=1, column=1, padx=5, sticky=tk.W)
# Discharge cutoff
ttk.Label(params_frame, text="Discharge Cutoff (V):").grid(row=2, column=0, sticky=tk.W)
ttk.Entry(params_frame, textvariable=self.discharge_cutoff, width=6).grid(row=2, column=1, padx=5, sticky=tk.W)
# Rest time
ttk.Label(params_frame, text="Rest Time (hours):").grid(row=3, column=0, sticky=tk.W)
ttk.Entry(params_frame, textvariable=self.rest_time, width=6).grid(row=3, column=1, padx=5, sticky=tk.W)
# C-rate for test (C/5 by default)
ttk.Label(params_frame, text="Test C-rate:").grid(row=0, column=2, sticky=tk.W, padx=(10,0))
ttk.Entry(params_frame, textvariable=self.c_rate, width=4).grid(row=0, column=3, padx=5, sticky=tk.W)
ttk.Label(params_frame, text="(e.g., 0.2 for C/5)").grid(row=0, column=4, sticky=tk.W)
# Start/Stop buttons
button_frame = ttk.Frame(controls_frame)
button_frame.pack(side=tk.RIGHT, padx=10)
self.start_button = ttk.Button(button_frame, text="START TEST", command=self.start_test, style='TButton')
self.start_button.pack(side=tk.TOP, pady=5)
self.stop_button = ttk.Button(button_frame, text="STOP TEST", command=self.stop_test, style='Warning.TButton', state=tk.DISABLED)
self.stop_button.pack(side=tk.TOP, pady=5)
# Continuous mode checkbox
self.continuous_var = tk.BooleanVar(value=True)
ttk.Checkbutton(button_frame, text="Continuous Mode", variable=self.continuous_var).pack(side=tk.TOP, pady=5)
# Plot area
self.plot_frame = ttk.Frame(self.content_frame)
self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=(0, 5))
self.setup_plot()
# Status bar
self.status_var = tk.StringVar()
self.status_var.set("Ready")
self.status_label = ttk.Label(self.root, textvariable=self.status_var, style='Status.TLabel', padding=(0, 5), anchor=tk.W)
self.status_label.place(x=20, relx=0, rely=1.0, anchor='sw', relwidth=0.96, height=28)
def setup_plot(self):
"""Configure the matplotlib plot"""
self.fig = Figure(figsize=(8, 5), dpi=100, facecolor='#2E3440')
# Adjust margins to give more space on right side
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 based on charge/discharge cutoffs ±0.2V
voltage_padding = 0.2
min_voltage = max(0, self.discharge_cutoff.get() - voltage_padding)
max_voltage = self.charge_cutoff.get() + 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) - set initial range based on test current
self.ax2 = self.ax.twinx()
current_padding = 0.05
test_current = self.c_rate.get() * self.capacity.get()
max_current = test_current * 1.5 # Add 50% padding
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 to avoid overlap
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 = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.draw()
canvas_widget = self.canvas.get_tk_widget()
canvas_widget.configure(bg='#2E3440', bd=0, highlightthickness=0)
canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True, pady=(10, 0))
def init_device(self):
"""Initialize the ADALM1000 device with continuous measurement"""
try:
# First try to clean up any existing session
if hasattr(self, 'session'):
try:
self.session.end()
del self.session
except:
pass
# Add small delay to allow device to reset
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]
# Reset channels
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.itemconfig(self.status_indicator, fill='green')
self.connection_label.config(text="Connected")
self.status_var.set("Device connected | Ready to measure")
self.session_active = True
self.start_button.config(state=tk.NORMAL)
# Start continuous measurement thread
self.measurement_event = threading.Event()
self.measurement_event.set()
self.measurement_thread = threading.Thread(
target=self.continuous_measurement,
daemon=True
)
self.measurement_thread.start()
except Exception as e:
self.handle_device_error(e)
def continuous_measurement(self):
"""Continuous measurement with moving average filtering and optimized I/O"""
filter_window_size = 10
voltage_window = []
current_window = []
last_plot_update = 0
log_buffer = []
# Initialize start_time for measurements
if not hasattr(self, 'start_time'):
self.start_time = time.time()
while self.measurement_event.is_set() and self.root.winfo_exists():
try:
# Read multiple samples for better accuracy
samples = self.dev.read(filter_window_size, 500, True)
if not samples:
raise DeviceDisconnectedError("No samples received")
# Get voltage from Channel B (HI_Z mode) and current from Channel A
raw_voltage = np.mean([s[1][0] for s in samples]) # Channel B voltage
raw_current = np.mean([s[0][1] for s in samples]) # Channel A current
current_time = time.time() - self.start_time
# Apply moving average filter
voltage_window.append(raw_voltage)
current_window.append(raw_current)
if len(voltage_window) > filter_window_size:
voltage_window.pop(0)
current_window.pop(0)
voltage = np.mean(voltage_window)
current = np.mean(current_window)
# Store filtered data
self.time_data.append(current_time)
self.voltage_data.append(voltage)
self.current_data.append(current)
# Update UI with filtered values (throttled)
if current_time - last_plot_update > 0.5: # Update at 2Hz max
self.root.after(0, lambda: self.update_measurement_display(voltage, current, current_time))
last_plot_update = current_time
# Buffered logging
if self.test_running and hasattr(self, 'filename'):
log_buffer.append([
f"{current_time:.3f}",
f"{voltage:.6f}",
f"{current:.6f}",
self.test_phase.get(),
f"{self.capacity_ah.get():.4f}",
f"{self.charge_capacity.get():.4f}",
f"{self.coulomb_efficiency.get():.1f}",
f"{self.cycle_count.get()}"
])
# Write in chunks of 10 samples
if len(log_buffer) >= 10:
with open(self.filename, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(log_buffer)
log_buffer.clear()
time.sleep(max(0.05, self.interval))
except Exception as e:
error_msg = str(e)
if self.root.winfo_exists():
self.root.after(0, lambda msg=error_msg:
self.handle_device_error(f"Measurement error: {msg}") if self.root.winfo_exists() else None)
break
# Flush remaining buffer on exit
if log_buffer and hasattr(self, 'filename'):
with open(self.filename, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerows(log_buffer)
def start_test(self):
"""Start the full battery test cycle"""
if not self.test_running:
try:
# Validate inputs
if self.capacity.get() <= 0:
raise ValueError("Battery capacity must be positive")
if self.charge_cutoff.get() <= self.discharge_cutoff.get():
raise ValueError("Charge cutoff must be higher than discharge cutoff")
if self.c_rate.get() <= 0:
raise ValueError("C-rate must be positive")
# Set continuous mode based on checkbox
self.continuous_mode = self.continuous_var.get()
# Reset timing for new test
self.measurement_start_time = time.time()
self.test_start_time = time.time()
# Calculate target current
test_current = self.c_rate.get() * self.capacity.get()
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.set(0.0)
self.charge_capacity.set(0.0)
self.coulomb_efficiency.set(0.0)
self.cycle_count.set(0)
# Setup new log file with buffered writer
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.filename = os.path.join(self.log_dir, f"battery_test_{timestamp}.csv")
self.log_file = open(self.filename, 'w', newline='')
self.log_writer = csv.writer(self.log_file)
self.log_writer.writerow(["Time(s)", "Voltage(V)", "Current(A)", "Phase", "Discharge_Capacity(Ah)", "Charge_Capacity(Ah)", "Coulomb_Eff(%)", "Cycle"])
self.log_buffer = []
# Start test thread
self.test_running = True
self.start_time = time.time()
self.last_update_time = time.time()
self.test_phase.set("Initial Discharge")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.status_var.set(f"Test started | Discharging to {self.discharge_cutoff.get()}V @ {test_current:.3f}A")
# Start test sequence in a new thread
self.test_thread = threading.Thread(target=self.run_test_sequence, daemon=True)
self.test_thread.start()
except Exception as e:
messagebox.showerror("Error", str(e))
@staticmethod
def format_time(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 # This will break out of all test loops
self.measuring = False
# Immediately set device to safe state
if hasattr(self, 'dev'):
self.dev.channels['A'].mode = pysmu.Mode.HI_Z
self.dev.channels['A'].constant(0)
self.status_var.set("Test stopped immediately")
self.stop_button.config(state=tk.DISABLED)
self.start_button.config(state=tk.NORMAL)
# Finalize test data
self.root.after(100, self.finalize_test)
def center_window(self, window):
"""Center a window on screen"""
window.update_idletasks()
width = window.winfo_width()
height = window.winfo_height()
x = (window.winfo_screenwidth() // 2) - (width // 2)
y = (window.winfo_screenheight() // 2) - (height // 2)
window.geometry(f'{width}x{height}+{x}+{y}')
def run_test_sequence(self):
try:
test_current = self.c_rate.get() * self.capacity.get()
while self.test_running and (self.continuous_mode or self.cycle_count.get() == 0):
# Reset stop request at start of each cycle
self.request_stop = False
self.cycle_count.set(self.cycle_count.get() + 1)
# 1. Charge phase (constant current)
self.test_phase.set("Charge")
self.status_var.set(f"Charging to {self.charge_cutoff.get()}V @ {test_current:.3f}A")
self.root.update() # Force UI update
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.set(0.0)
target_voltage = self.charge_cutoff.get()
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])
# Update charge capacity
now = time.time()
delta_t = now - self.last_update_time
self.last_update_time = now
self.charge_capacity.set(self.charge_capacity.get() + measured_current * delta_t / 3600)
self.status_var.set(
f"Charging: {current_voltage:.3f}V / {target_voltage}V | "
f"Current: {measured_current:.3f}A | "
f"Capacity: {self.charge_capacity.get():.4f}Ah"
)
self.root.update() # Force UI update
if current_voltage >= target_voltage or self.request_stop:
break
time.sleep(0.1) # More frequent checks
if self.request_stop or not self.test_running:
break
# 2. Rest period after charge
self.test_phase.set("Resting (Post-Charge)")
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.get() * 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_var.set(
f"Resting after charge | "
f"Time left: {time_left/60:.1f} min"
)
self.root.update()
time.sleep(1) # Check every second for stop request
if self.request_stop or not self.test_running:
break
# 3. Discharge phase (capacity measurement)
self.test_phase.set("Discharge")
self.status_var.set(f"Discharging to {self.discharge_cutoff.get()}V @ {test_current:.3f}A")
self.root.update()
self.measuring = True
self.dev.channels['A'].mode = pysmu.Mode.SIMV
self.dev.channels['A'].constant(-test_current)
self.capacity_ah.set(0.0)
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])
# Capacity calculation
now = time.time()
delta_t = now - self.last_update_time
self.last_update_time = now
self.capacity_ah.set(self.capacity_ah.get() + current_current * delta_t / 3600)
self.status_var.set(
f"Discharging: {current_voltage:.3f}V / {self.discharge_cutoff.get()}V | "
f"Current: {current_current:.3f}A | "
f"Capacity: {self.capacity_ah.get():.4f}Ah"
)
self.root.update()
if current_voltage <= self.discharge_cutoff.get() or self.request_stop:
break
time.sleep(0.1) # More frequent checks
# 4. Rest period after discharge (only if not stopping)
if self.test_running and not self.request_stop:
self.test_phase.set("Resting (Post-Discharge)")
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.get() * 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_var.set(
f"Resting after discharge | "
f"Time left: {time_left/60:.1f} min"
)
self.root.update()
time.sleep(1)
# Calculate Coulomb efficiency if not stopping
if not self.request_stop and self.charge_capacity.get() > 0:
efficiency = (self.capacity_ah.get() / self.charge_capacity.get()) * 100
self.coulomb_efficiency.set(efficiency)
# Update cycle info
self.status_var.set(
f"Cycle {self.cycle_count.get()} complete | "
f"Discharge: {self.capacity_ah.get():.3f}Ah | "
f"Charge: {self.charge_capacity.get():.3f}Ah | "
f"Efficiency: {self.coulomb_efficiency.get():.1f}%"
)
self.root.update()
# Write cycle summary to log file
self.write_cycle_summary()
# Short rest between cycles (only in continuous mode)
if self.continuous_mode and self.test_running and not self.request_stop:
rest_end_time = time.time() + (self.rest_time.get() * 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.test_phase.set("Resting Between Cycles")
self.status_var.set(
f"Resting between cycles | "
f"Time left: {time_left/60:.1f} min"
)
self.root.update()
time.sleep(1)
# Finalize test if stopped or completed
self.root.after(0, self.finalize_test)
except Exception as e:
error_msg = str(e)
if self.root.winfo_exists():
self.root.after(0, lambda msg=error_msg: messagebox.showerror("Test Error", msg))
self.root.after(0, self.finalize_test)
def finalize_test(self):
"""Final cleanup after test completes or is stopped"""
self.measuring = False
if hasattr(self, 'dev'):
self.dev.channels['A'].constant(0)
# Flush and close log file
if hasattr(self, 'log_buffer') and self.log_buffer and hasattr(self, 'log_writer'):
self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear()
if hasattr(self, 'log_file'):
self.log_file.close()
if hasattr(self, 'filename'):
self.write_cycle_summary()
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.request_stop = False
# Erfolgsmeldung mit mehr Details
message = (
f"Test safely stopped after discharge phase | "
f"Cycle {self.cycle_count.get()} completed | "
f"Final capacity: {self.capacity_ah.get():.3f}Ah"
)
self.status_var.set(message)
# Optional: Messagebox anzeigen
self.root.after(0, lambda: messagebox.showinfo(
"Test Completed",
f"Test was safely stopped after discharge phase.\n\n"
f"Final discharge capacity: {self.capacity_ah.get():.3f}Ah\n"
f"Total cycles completed: {self.cycle_count.get()}"
))
def update_measurement_display(self, voltage, current, current_time):
"""Update display with current measurements (optimized to only update changed values)"""
try:
# Only update changed values
voltage_text = f"{voltage:.4f}"
if not hasattr(self, '_last_voltage_text') or self._last_voltage_text != voltage_text:
self.voltage_label.config(text=voltage_text)
self._last_voltage_text = voltage_text
capacity_text = f"{self.capacity_ah.get():.4f}"
if not hasattr(self, '_last_capacity_text') or self._last_capacity_text != capacity_text:
self.capacity_label.config(text=capacity_text)
self._last_capacity_text = capacity_text
charge_capacity_text = f"{self.charge_capacity.get():.4f}"
if not hasattr(self, '_last_charge_capacity_text') or self._last_charge_capacity_text != charge_capacity_text:
self.charge_capacity_label.config(text=charge_capacity_text)
self._last_charge_capacity_text = charge_capacity_text
efficiency_text = f"{self.coulomb_efficiency.get():.1f}"
if not hasattr(self, '_last_efficiency_text') or self._last_efficiency_text != efficiency_text:
self.efficiency_label.config(text=efficiency_text)
self._last_efficiency_text = efficiency_text
cycle_text = f"{self.cycle_count.get()}"
if not hasattr(self, '_last_cycle_text') or self._last_cycle_text != cycle_text:
self.cycle_label.config(text=cycle_text)
self._last_cycle_text = cycle_text
current_text = f"{current:.4f}"
if not hasattr(self, '_last_current_text') or self._last_current_text != current_text:
self.current_label.config(text=current_text)
self._last_current_text = current_text
phase_text = self.test_phase.get()
if not hasattr(self, '_last_phase_text') or self._last_phase_text != phase_text:
self.phase_label.config(text=phase_text)
self._last_phase_text = phase_text
time_text = self.format_time(current_time)
if not hasattr(self, '_last_time_text') or self._last_time_text != time_text:
self.time_label.config(text=time_text)
self._last_time_text = time_text
# Update plot with proper scaling (throttled)
if not hasattr(self, '_last_plot_update') or (time.time() - self._last_plot_update > 1.0):
self.update_plot()
self._last_plot_update = time.time()
except Exception as e:
print(f"GUI update error: {e}")
def write_cycle_summary(self):
"""Write cycle summary to the log file"""
if not hasattr(self, 'filename'):
return
summary_line = (
f"Cycle {self.cycle_count.get()} - "
f"Discharge={self.capacity_ah.get():.4f}Ah, "
f"Charge={self.charge_capacity.get():.4f}Ah, "
f"Efficiency={self.coulomb_efficiency.get():.1f}%"
)
with open(self.filename, 'a', newline='') as f:
f.write(summary_line + "\n")
def update_plot(self):
"""Update plot with proper scaling and limits (optimized)"""
if not self.time_data:
return
# Only update if there's significant new data
if hasattr(self, '_last_plot_len') and len(self.time_data) - self._last_plot_len < 10:
return
self._last_plot_len = len(self.time_data)
# Update plot data
self.line_voltage.set_data(self.time_data, self.voltage_data)
self.line_current.set_data(self.time_data, self.current_data)
# Set x-axis to always show from 0 to current max time
min_time = 0 # Always start from 0
max_time = self.time_data[-1] + 1 # Add 1 second padding
# Only adjust limits if needed
current_xlim = self.ax.get_xlim()
if abs(current_xlim[1] - max_time) > 5: # Only adjust if significant change
self.ax.set_xlim(min_time, max_time)
self.ax2.set_xlim(min_time, max_time)
# Auto-scale y-axes but respect initial boundaries
voltage_padding = 0.2
if self.voltage_data:
min_voltage = max(0, min(self.voltage_data) - voltage_padding)
max_voltage = max(self.voltage_data) + voltage_padding
# Ensure we don't go below discharge cutoff - padding or above charge cutoff + padding
min_voltage = max(min_voltage, max(0, self.discharge_cutoff.get() - voltage_padding*2))
max_voltage = min(max_voltage, self.charge_cutoff.get() + voltage_padding*2)
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:
test_current = self.c_rate.get() * self.capacity.get()
min_current = min(self.current_data) - current_padding
max_current = max(self.current_data) + current_padding
# Ensure we don't go too far beyond test current
min_current = max(min_current, -test_current*1.5)
max_current = min(max_current, test_current*1.5)
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)
# Only redraw if needed
self.canvas.draw_idle()
def handle_device_error(self, error):
"""Handle device connection errors"""
if not self.root.winfo_exists(): # Check if window still exists
return
error_msg = str(error)
print(f"Device error: {error_msg}")
self.root.after_idle(lambda: self.status_light.itemconfig(self.status_indicator, fill='red'))
self.connection_label.config(text="Disconnected")
self.status_var.set(f"Device error: {error_msg}")
self.session_active = False
self.test_running = False
self.continuous_mode = False
self.measuring = False
if hasattr(self, 'start_button'):
self.start_button.config(state=tk.DISABLED)
if hasattr(self, 'stop_button'):
self.stop_button.config(state=tk.DISABLED)
# Clear plot + buffers
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()
# Clean up session
if hasattr(self, 'session'):
try:
if self.session_active:
self.session.end()
del self.session
except:
pass
if self.root.winfo_exists(): # Double-check before showing message
try:
messagebox.showerror(
"Device Connection Error",
f"Could not connect to ADALM1000:\n\n{error_msg}\n\n"
"1. Check USB cable connection\n"
"2. Try the Reconnect button\n"
"3. Restart the application if problem persists"
)
except:
pass # Ignore errors if window is being destroyed
def reconnect_device(self):
"""Reconnect the device"""
self.status_var.set("Attempting to reconnect...")
self.test_running = False
self.continuous_mode = False
self.measuring = False
if hasattr(self, 'measurement_event'):
self.measurement_event.clear()
# Wait for threads to finish
if hasattr(self, 'measurement_thread'):
self.measurement_thread.join(timeout=1.0)
if hasattr(self, 'test_thread'):
self.test_thread.join(timeout=1.0)
# Reset before reinitializing
self.handle_device_error("Reconnecting...")
self.init_device()
def on_close(self):
"""Clean up on window close"""
if hasattr(self, 'measurement_event'):
self.measurement_event.clear()
if hasattr(self, 'measurement_thread'):
self.measurement_thread.join(timeout=1.0)
if hasattr(self, 'test_thread'):
self.test_thread.join(timeout=1.0)
if hasattr(self, 'session') and self.session:
try:
if self.session_active:
self.session.end()
except:
pass
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
try:
app = BatteryTester(root)
root.mainloop()
except Exception as e:
if root.winfo_exists():
messagebox.showerror("Fatal Error", f"Application failed: {str(e)}")
else:
print(f"Fatal Error: {e}")
try:
root.destroy()
except:
pass