diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index 303b305..5116d4d 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -37,6 +37,7 @@ class BatteryTester: self.session_active = False self.measuring = False self.test_running = False + self.continuous_mode = False # New flag for continuous cycling self.interval = 0.1 # Measurement interval self.log_dir = os.path.expanduser("~/adalm1000/logs") os.makedirs(self.log_dir, exist_ok=True) @@ -53,6 +54,7 @@ class BatteryTester: 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 + self.cycle_count = tk.IntVar(value=0) # Added for cycle counting # Data buffers self.time_data = deque() @@ -122,6 +124,7 @@ class BatteryTester: ("Discharge Capacity", "Ah"), ("Charge Capacity", "Ah"), ("Coulomb Eff.", "%"), + ("Cycle Count", ""), ] for i, (label, unit) in enumerate(measurement_labels): @@ -145,6 +148,8 @@ class BatteryTester: 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) @@ -185,6 +190,10 @@ class BatteryTester: 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)) @@ -327,7 +336,8 @@ class BatteryTester: self.test_phase.get(), f"{self.capacity_ah.get():.4f}", f"{self.charge_capacity.get():.4f}", - f"{self.coulomb_efficiency.get():.1f}" + f"{self.coulomb_efficiency.get():.1f}", + f"{self.cycle_count.get()}" ]) time.sleep(max(0.05, self.interval)) @@ -351,6 +361,9 @@ class BatteryTester: 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() @@ -369,6 +382,7 @@ class BatteryTester: 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") @@ -376,7 +390,7 @@ class BatteryTester: 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(%)"]) + 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 @@ -406,6 +420,7 @@ class BatteryTester: def stop_test(self): """Stop the current test""" self.test_running = False + self.continuous_mode = False # Always stop continuous mode when stop is pressed self.measuring = False if hasattr(self, 'dev'): self.dev.channels['A'].constant(0) # Set zero current @@ -432,187 +447,213 @@ class BatteryTester: window.geometry(f'{width}x{height}+{x}+{y}') def run_test_sequence(self): - """Run the complete test sequence (discharge-rest-charge-rest-discharge) once""" + """Run the complete test sequence (discharge-rest-charge-rest-discharge) in a loop""" 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 + while self.test_running and (self.continuous_mode or self.cycle_count.get() == 0): + # Increment cycle count + self.cycle_count.set(self.cycle_count.get() + 1) + self.root.after(0, lambda: self.cycle_label.config(text=str(self.cycle_count.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])}" + ) - # 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 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 + if not self.test_running: + return - current_voltage = self.voltage_data[-1] - measured_current = abs(self.current_data[-1]) - time_elapsed = time.time() - self.last_update_time + # 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) - # 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) + 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) - 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 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") - if current_voltage >= target_voltage: - break + 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() - time.sleep(0.5) + 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 - - # 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 + if not self.test_running: + return - current_voltage = self.voltage_data[-1] - current_current = abs(self.current_data[-1]) + # 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) - # Calculate discharged capacity - now = time.time() - delta_t = now - self.last_update_time - self.last_update_time = now - self.capacity_ah.set(self.capacity_ah.get() + measured_current * delta_t / 3600) + 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 + 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 | " + 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("Cycle Complete") 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" + 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}%" ) - # Check for discharge completion - if current_voltage <= self.discharge_cutoff.get(): - break + # 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)")) - time.sleep(0.5) + # 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) - # 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() + # 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)) - finally: - # Automatically stop the test after completion self.root.after(0, self.stop_test) def update_measurement_display(self, voltage, current, current_time): @@ -625,6 +666,7 @@ class BatteryTester: 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() @@ -637,6 +679,7 @@ class BatteryTester: 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}%" @@ -690,6 +733,7 @@ class BatteryTester: 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) @@ -732,6 +776,7 @@ class BatteryTester: """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()