From 1d58aba999c77e6955794883fb3a5c2ff3e3a652 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 23 May 2025 18:58:36 +0200 Subject: [PATCH] Dateien nach "MainCode" hochladen --- MainCode/adalm1000_logger.py | 777 +++++++++++++++++++++++++++++++++++ 1 file changed, 777 insertions(+) create mode 100644 MainCode/adalm1000_logger.py diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py new file mode 100644 index 0000000..ba8abd7 --- /dev/null +++ b/MainCode/adalm1000_logger.py @@ -0,0 +1,777 @@ +# -*- 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.interval = 0.1 # Measurement interval + 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) # Added for charge capacity tracking + self.coulomb_efficiency = tk.DoubleVar(value=0.0) # Added for efficiency calculation + + # 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.", "%"), + ] + + 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 + + # 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) + + # 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}" + ]) + + 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") + + # 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) + + # 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(%)"]) + + # 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): + """Stop the current test""" + self.test_running = False + self.measuring = False + if hasattr(self, 'dev'): + self.dev.channels['A'].constant(0) # Set zero current + + # Write final summary if we have a filename + if hasattr(self, 'filename'): + self.write_cycle_summary() + + self.start_button.config(state=tk.NORMAL) + self.stop_button.config(state=tk.DISABLED) + + if hasattr(self, 'filename'): + self.status_var.set(f"Test completed in {self.format_time(self.time_data[-1])} | Results saved to: {os.path.basename(self.filename)}") + else: + self.status_var.set("Test stopped | No data saved") + + 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): + """Run the complete test sequence (discharge-rest-charge-rest-discharge) once""" + try: + # Calculate target current + test_current = self.c_rate.get() * self.capacity.get() + + # 1. Initial Discharge (to known state) + #self.test_phase.set("Initial Discharge") + #self.status_var.set(f"Discharging 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.dev.channels['B'].mode = pysmu.Mode.HI_Z + + #while self.test_running: + # if not self.voltage_data: + # time.sleep(0.1) + # continue + + # current_voltage = self.voltage_data[-1] + # current_current = abs(self.current_data[-1]) + # self.status_var.set( + # f"Discharging: {current_voltage:.3f}V / {self.discharge_cutoff.get()}V | " + # f"Current: {current_current:.3f}A | " + # f"Time: {self.format_time(self.time_data[-1])}" + # ) + + # if current_voltage <= self.discharge_cutoff.get(): + # break + # time.sleep(0.5) + + #if not self.test_running: + # return + + # 2. Rest period after initial discharge + #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: {self.format_time(time_left)} | " + # f"Next: Charging to {self.charge_cutoff.get()}V" + # ) + # time.sleep(1) + + #if not self.test_running: + # return + + # 3. 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['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 + self.charge_capacity.set(self.charge_capacity.get() + measured_current * time_elapsed / 3600) + self.last_update_time = time.time() + + 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 + + # 4. 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 + + # 5. Final Discharge (capacity measurement) + self.test_phase.set("Final Discharge") + self.status_var.set(f"Final 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]) + + # Calculate discharged capacity + self.capacity_ah.set(self.capacity_ah.get() + current_current * (time.time() - self.last_update_time) / 3600) + self.last_update_time = time.time() + + 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 | " + f"Time: {self.time_data[-1]:.1f}s" + ) + + # Check for discharge completion + if current_voltage <= self.discharge_cutoff.get(): + break + + time.sleep(0.5) + + # 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("Test Complete") + self.status_var.set( + f"Test 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 + self.root.after(0, lambda: messagebox.showinfo("Test Complete", + f"Test 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 final summary to log file + self.write_cycle_summary() + + 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)) + finally: + # Automatically stop the test after completion + self.root.after(0, self.stop_test) + + 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}") + + # 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"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.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.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 \ No newline at end of file