Adalm1000_Battery_SMU/MainCode/adalm1000_logger.py
Jan 06c99bae38 MainCode/adalm1000_logger.py aktualisiert
Der Stop-Button setzt nur ein Flag (request_stop) statt sofort zu stoppen

    Die Entladephase überprüft dieses Flag und bricht ab, wenn es gesetzt ist

    Nach der Entladephase wird der Test nur beendet, wenn request_stop True ist

    Neue finalize_test Methode für konsistente Aufräumarbeiten

    Klare Statusmeldungen, die den Stop-Request anzeigen
(Deepseek)
2025-05-23 23:34:30 +02:00

822 lines
35 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')
self.fig.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.15)
self.ax = self.fig.add_subplot(111)
self.ax.set_facecolor('#3B4252')
# 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()
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')
self.ax.legend(loc='upper left')
self.ax2.legend(loc='upper right')
# 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"""
filter_window_size = 10
voltage_window = []
current_window = []
# 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 if hasattr(self, 'start_time') else time.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
if self.root.winfo_exists():
self.root.after(0, lambda: self.update_measurement_display(voltage, current, current_time))
# Save data if in active test
if self.test_running and hasattr(self, 'filename'):
with open(self.filename, 'a', newline='') as f:
writer = csv.writer(f)
writer.writerow([
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()}"
])
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
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
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.filename = os.path.join(self.log_dir, f"battery_test_{timestamp}.csv")
with open(self.filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(["Time(s)", "Voltage(V)", "Current(A)", "Phase", "Discharge_Capacity(Ah)", "Charge_Capacity(Ah)", "Coulomb_Eff(%)", "Cycle"])
# 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 stop after current discharge phase completes"""
if not self.test_running:
return
self.request_stop = True
self.status_var.set("Stop requested - will complete after current discharge phase")
self.stop_button.config(state=tk.DISABLED)
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):
# Zurücksetzen des Stop-Requests zu Beginn jedes Zyklus
self.request_stop = False
# 1. Charge (constant current)
self.test_phase.set("Charge")
self.status_var.set(f"Charging to {self.charge_cutoff.get()}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.set(0.0)
target_voltage = self.charge_cutoff.get()
self.last_update_time = time.time()
while self.test_running:
if not self.voltage_data:
time.sleep(0.1)
continue
current_voltage = self.voltage_data[-1]
measured_current = abs(self.current_data[-1])
time_elapsed = time.time() - self.last_update_time
# 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 | "
f"Time: {self.time_data[-1]:.1f}s"
)
if current_voltage >= target_voltage:
break
time.sleep(0.5)
if not self.test_running:
return
# 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:
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 | "
f"Next: Final discharge to {self.discharge_cutoff.get()}V"
)
time.sleep(1)
if not self.test_running:
return
# 3. Discharge (capacity measurement)
self.test_phase.set("Discharge")
self.status_var.set(f"discharge to {self.discharge_cutoff.get()}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.set(0.0)
self.last_update_time = time.time()
while self.test_running:
if not self.current_data:
time.sleep(0.1)
continue
current_voltage = self.voltage_data[-1]
current_current = abs(self.current_data[-1])
# Kapazitätsberechnung
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)
# Statusupdate
status_msg = (
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"
)
if self.request_stop:
status_msg += " | FINALIZING - completing discharge..."
self.status_var.set(status_msg)
if current_voltage <= self.discharge_cutoff.get() or self.request_stop:
break
time.sleep(0.5)
if self.request_stop:
time.sleep(0.5)
self.test_running = False
self.root.after(0, self.finalize_test)
return
# 4. Rest period after charge
if self.test_running:
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:
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 | "
f"Next: Charge to {self.charge_cutoff.get()}V"
)
time.sleep(1)
if not self.test_running:
return
# Calculate Coulomb efficiency
if self.charge_capacity.get() > 0:
efficiency = (self.capacity_ah.get() / self.charge_capacity.get()) * 100
self.coulomb_efficiency.set(efficiency)
# Update GUI and show results
self.test_phase.set("Cycle Complete")
self.status_var.set(
f"Cycle {self.cycle_count.get()} complete | "
f"Discharge Capacity: {self.capacity_ah.get():.3f}Ah | "
f"Charge Capacity: {self.charge_capacity.get():.3f}Ah | "
f"Efficiency: {self.coulomb_efficiency.get():.1f}%"
)
# Show summary dialog only for the first cycle or when stopping
if not self.continuous_mode or not self.test_running:
self.root.after(0, lambda: messagebox.showinfo("Cycle Complete",
f"Cycle {self.cycle_count.get()} complete\n\n"
f"Discharge Capacity: {self.capacity_ah.get():.3f}Ah\n"
f"Charge Capacity: {self.charge_capacity.get():.3f}Ah\n"
f"Coulomb Efficiency: {self.coulomb_efficiency.get():.1f}%\n\n"
f"({self.capacity_ah.get()/self.capacity.get()*100:.1f}% of rated capacity)"))
# Write cycle summary to log file
self.write_cycle_summary()
# Reset capacities for next cycle
self.capacity_ah.set(0.0)
self.charge_capacity.set(0.0)
# Check if we should continue with another cycle
if self.continuous_mode and self.test_running:
# Short rest between cycles
rest_end_time = time.time() + (self.rest_time.get() * 3600)
while time.time() < rest_end_time and self.test_running:
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 | "
f"Next cycle will start soon"
)
time.sleep(1)
# Automatically stop the test after completion if not in continuous mode
if not self.continuous_mode:
self.root.after(0, self.stop_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.stop_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)
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"""
try:
self.voltage_label.config(text=f"{voltage:.4f}")
self.current_label.config(text=f"{current:.4f}")
self.phase_label.config(text=self.test_phase.get())
self.time_label.config(text=self.format_time(current_time))
self.capacity_label.config(text=f"{self.capacity_ah.get():.4f}")
self.charge_capacity_label.config(text=f"{self.charge_capacity.get():.4f}")
self.efficiency_label.config(text=f"{self.coulomb_efficiency.get():.1f}")
self.cycle_label.config(text=str(self.cycle_count.get()))
# Update plot with proper scaling
self.update_plot()
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"""
if not self.time_data:
return
# 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
self.ax.set_xlim(min_time, max_time)
self.ax2.set_xlim(min_time, max_time)
# Auto-scale y-axes with some margin
if self.voltage_data:
voltage_margin = 0.2
min_voltage = max(0, min(self.voltage_data) - voltage_margin)
max_voltage = max(self.voltage_data) + voltage_margin
self.ax.set_ylim(min_voltage, max_voltage)
if self.current_data:
current_margin = 0.05
min_current = min(self.current_data) - current_margin
max_current = max(self.current_data) + current_margin
self.ax2.set_ylim(min_current, max_current)
self.canvas.draw()
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