MainCode/adalm1000_logger.py aktualisiert
Separate Log Files: You've successfully implemented separate log files for each cycle without cycle numbers in filenames.
1Hz UI Updates: The measurement display updates at 1Hz as requested.
Throttled Plot Updates: Plot updates are properly throttled to prevent lag.
Error Handling: Good error handling throughout the code.
Thread Safety: Proper use of threading for measurements and test sequences.
(Deepseek)
This commit is contained in:
parent
1c928e22fc
commit
24cc224138
@ -303,7 +303,9 @@ class BatteryTester:
|
|||||||
voltage_window = []
|
voltage_window = []
|
||||||
current_window = []
|
current_window = []
|
||||||
last_plot_update = 0
|
last_plot_update = 0
|
||||||
|
last_ui_update = 0
|
||||||
log_buffer = []
|
log_buffer = []
|
||||||
|
update_interval = 1.0 # Update UI at 1Hz max
|
||||||
|
|
||||||
# Initialize start_time for measurements
|
# Initialize start_time for measurements
|
||||||
if not hasattr(self, 'start_time'):
|
if not hasattr(self, 'start_time'):
|
||||||
@ -337,13 +339,21 @@ class BatteryTester:
|
|||||||
self.voltage_data.append(voltage)
|
self.voltage_data.append(voltage)
|
||||||
self.current_data.append(current)
|
self.current_data.append(current)
|
||||||
|
|
||||||
# Update UI with filtered values (throttled)
|
# Throttle UI updates to prevent lag
|
||||||
if current_time - last_plot_update > 0.5: # Update at 2Hz max
|
now = time.time()
|
||||||
self.root.after(0, lambda: self.update_measurement_display(voltage, current, current_time))
|
if now - last_ui_update > update_interval:
|
||||||
last_plot_update = current_time
|
self.root.after(0, lambda: self.update_measurement_display(
|
||||||
|
voltage, current, current_time
|
||||||
|
))
|
||||||
|
last_ui_update = now
|
||||||
|
|
||||||
|
# Throttle plot updates even more (1Hz max)
|
||||||
|
if now - last_plot_update > 1.0:
|
||||||
|
self.root.after(0, self.update_plot)
|
||||||
|
last_plot_update = now
|
||||||
|
|
||||||
# Buffered logging
|
# Buffered logging
|
||||||
if self.test_running and hasattr(self, 'filename'):
|
if self.test_running and hasattr(self, 'current_cycle_file'):
|
||||||
log_buffer.append([
|
log_buffer.append([
|
||||||
f"{current_time:.3f}",
|
f"{current_time:.3f}",
|
||||||
f"{voltage:.6f}",
|
f"{voltage:.6f}",
|
||||||
@ -372,7 +382,7 @@ class BatteryTester:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# Flush remaining buffer on exit
|
# Flush remaining buffer on exit
|
||||||
if log_buffer and hasattr(self, 'filename'):
|
if log_buffer and hasattr(self, 'current_cycle_file'):
|
||||||
with open(self.filename, 'a', newline='') as f:
|
with open(self.filename, 'a', newline='') as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f)
|
||||||
writer.writerows(log_buffer)
|
writer.writerows(log_buffer)
|
||||||
@ -412,14 +422,11 @@ class BatteryTester:
|
|||||||
self.coulomb_efficiency.set(0.0)
|
self.coulomb_efficiency.set(0.0)
|
||||||
self.cycle_count.set(0)
|
self.cycle_count.set(0)
|
||||||
|
|
||||||
# Setup new log file with buffered writer
|
# Generate base filename without cycle number
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self.filename = os.path.join(self.log_dir, f"battery_test_{timestamp}.csv")
|
self.base_filename = os.path.join(self.log_dir, f"battery_test_{timestamp}")
|
||||||
self.log_file = open(self.filename, 'w', newline='')
|
self.current_cycle_file = None
|
||||||
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
|
# Start test thread
|
||||||
self.test_running = True
|
self.test_running = True
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
@ -436,6 +443,40 @@ class BatteryTester:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", str(e))
|
messagebox.showerror("Error", str(e))
|
||||||
|
|
||||||
|
def create_cycle_log_file(self):
|
||||||
|
"""Create a new log file for the current cycle"""
|
||||||
|
# Close previous file if exists
|
||||||
|
if hasattr(self, 'current_cycle_file') and self.current_cycle_file:
|
||||||
|
try:
|
||||||
|
self.current_cycle_file.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing previous log file: {e}")
|
||||||
|
|
||||||
|
# Check write permissions
|
||||||
|
if not os.access(self.log_dir, os.W_OK):
|
||||||
|
messagebox.showerror("Error", f"No write permissions in {self.log_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create new log file with sequential suffix
|
||||||
|
suffix = 1
|
||||||
|
while True:
|
||||||
|
self.filename = f"{self.base_filename}_{suffix}.csv"
|
||||||
|
if not os.path.exists(self.filename):
|
||||||
|
break
|
||||||
|
suffix += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.current_cycle_file = open(self.filename, 'w', newline='')
|
||||||
|
self.log_writer = csv.writer(self.current_cycle_file)
|
||||||
|
self.log_writer.writerow(["Time(s)", "Voltage(V)", "Current(A)", "Phase",
|
||||||
|
"Discharge_Capacity(Ah)", "Charge_Capacity(Ah)",
|
||||||
|
"Coulomb_Eff(%)", "Cycle"])
|
||||||
|
self.log_buffer = []
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Error", f"Failed to create log file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_time(seconds):
|
def format_time(seconds):
|
||||||
@ -484,6 +525,9 @@ class BatteryTester:
|
|||||||
self.request_stop = False
|
self.request_stop = False
|
||||||
self.cycle_count.set(self.cycle_count.get() + 1)
|
self.cycle_count.set(self.cycle_count.get() + 1)
|
||||||
|
|
||||||
|
# Create new log file for this cycle
|
||||||
|
self.create_cycle_log_file()
|
||||||
|
|
||||||
# 1. Charge phase (constant current)
|
# 1. Charge phase (constant current)
|
||||||
self.test_phase.set("Charge")
|
self.test_phase.set("Charge")
|
||||||
self.status_var.set(f"Charging to {self.charge_cutoff.get()}V @ {test_current:.3f}A")
|
self.status_var.set(f"Charging to {self.charge_cutoff.get()}V @ {test_current:.3f}A")
|
||||||
@ -642,23 +686,29 @@ class BatteryTester:
|
|||||||
"""Final cleanup after test completes or is stopped"""
|
"""Final cleanup after test completes or is stopped"""
|
||||||
self.measuring = False
|
self.measuring = False
|
||||||
if hasattr(self, 'dev'):
|
if hasattr(self, 'dev'):
|
||||||
self.dev.channels['A'].constant(0)
|
try:
|
||||||
|
self.dev.channels['A'].constant(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error resetting device: {e}")
|
||||||
|
|
||||||
# Flush and close log file
|
# Flush and close current log file
|
||||||
if hasattr(self, 'log_buffer') and self.log_buffer and hasattr(self, 'log_writer'):
|
if hasattr(self, 'log_buffer') and self.log_buffer and hasattr(self, 'log_writer'):
|
||||||
self.log_writer.writerows(self.log_buffer)
|
try:
|
||||||
self.log_buffer.clear()
|
self.log_writer.writerows(self.log_buffer)
|
||||||
if hasattr(self, 'log_file'):
|
self.log_buffer.clear()
|
||||||
self.log_file.close()
|
except Exception as e:
|
||||||
|
print(f"Error flushing log buffer: {e}")
|
||||||
|
|
||||||
if hasattr(self, 'filename'):
|
if hasattr(self, 'current_cycle_file'):
|
||||||
self.write_cycle_summary()
|
try:
|
||||||
|
self.current_cycle_file.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing log file: {e}")
|
||||||
|
|
||||||
self.start_button.config(state=tk.NORMAL)
|
self.start_button.config(state=tk.NORMAL)
|
||||||
self.stop_button.config(state=tk.DISABLED)
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
self.request_stop = False
|
self.request_stop = False
|
||||||
|
|
||||||
# Erfolgsmeldung mit mehr Details
|
|
||||||
message = (
|
message = (
|
||||||
f"Test safely stopped after discharge phase | "
|
f"Test safely stopped after discharge phase | "
|
||||||
f"Cycle {self.cycle_count.get()} completed | "
|
f"Cycle {self.cycle_count.get()} completed | "
|
||||||
@ -666,7 +716,6 @@ class BatteryTester:
|
|||||||
)
|
)
|
||||||
self.status_var.set(message)
|
self.status_var.set(message)
|
||||||
|
|
||||||
# Optional: Messagebox anzeigen
|
|
||||||
self.root.after(0, lambda: messagebox.showinfo(
|
self.root.after(0, lambda: messagebox.showinfo(
|
||||||
"Test Completed",
|
"Test Completed",
|
||||||
f"Test was safely stopped after discharge phase.\n\n"
|
f"Test was safely stopped after discharge phase.\n\n"
|
||||||
@ -675,127 +724,96 @@ class BatteryTester:
|
|||||||
))
|
))
|
||||||
|
|
||||||
def update_measurement_display(self, voltage, current, current_time):
|
def update_measurement_display(self, voltage, current, current_time):
|
||||||
"""Update display with current measurements (optimized to only update changed values)"""
|
"""Update all measurement displays at once (throttled to 1Hz)"""
|
||||||
try:
|
try:
|
||||||
# Only update changed values
|
# Update all values regardless of change
|
||||||
voltage_text = f"{voltage:.4f}"
|
self.voltage_label.config(text=f"{voltage:.4f}")
|
||||||
if not hasattr(self, '_last_voltage_text') or self._last_voltage_text != voltage_text:
|
self.current_label.config(text=f"{current:.4f}")
|
||||||
self.voltage_label.config(text=voltage_text)
|
self.capacity_label.config(text=f"{self.capacity_ah.get():.4f}")
|
||||||
self._last_voltage_text = voltage_text
|
self.charge_capacity_label.config(text=f"{self.charge_capacity.get():.4f}")
|
||||||
|
self.efficiency_label.config(text=f"{self.coulomb_efficiency.get():.1f}")
|
||||||
capacity_text = f"{self.capacity_ah.get():.4f}"
|
self.cycle_label.config(text=f"{self.cycle_count.get()}")
|
||||||
if not hasattr(self, '_last_capacity_text') or self._last_capacity_text != capacity_text:
|
self.phase_label.config(text=self.test_phase.get())
|
||||||
self.capacity_label.config(text=capacity_text)
|
self.time_label.config(text=self.format_time(current_time))
|
||||||
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:
|
except Exception as e:
|
||||||
print(f"GUI update error: {e}")
|
print(f"GUI update error: {e}")
|
||||||
|
|
||||||
def write_cycle_summary(self):
|
def write_cycle_summary(self):
|
||||||
"""Write cycle summary to the log file"""
|
"""Write cycle summary to the current cycle's log file"""
|
||||||
if not hasattr(self, 'filename'):
|
if not hasattr(self, 'current_cycle_file') or not self.current_cycle_file:
|
||||||
return
|
return
|
||||||
|
|
||||||
summary_line = (
|
summary_line = (
|
||||||
f"Cycle {self.cycle_count.get()} - "
|
f"Cycle {self.cycle_count.get()} Summary - "
|
||||||
f"Discharge={self.capacity_ah.get():.4f}Ah, "
|
f"Discharge={self.capacity_ah.get():.4f}Ah, "
|
||||||
f"Charge={self.charge_capacity.get():.4f}Ah, "
|
f"Charge={self.charge_capacity.get():.4f}Ah, "
|
||||||
f"Efficiency={self.coulomb_efficiency.get():.1f}%"
|
f"Efficiency={self.coulomb_efficiency.get():.1f}%"
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(self.filename, 'a', newline='') as f:
|
# Ensure file is open and write summary
|
||||||
f.write(summary_line + "\n")
|
try:
|
||||||
|
if self.log_buffer:
|
||||||
|
self.log_writer.writerows(self.log_buffer)
|
||||||
|
self.log_buffer.clear()
|
||||||
|
self.current_cycle_file.write(summary_line + "\n")
|
||||||
|
self.current_cycle_file.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing cycle summary: {e}")
|
||||||
|
|
||||||
def update_plot(self):
|
def update_plot(self):
|
||||||
"""Update plot with proper scaling and limits (optimized)"""
|
"""Optimized plot update with change detection"""
|
||||||
if not self.time_data:
|
if not self.time_data or len(self.time_data) < 10: # Wait for at least 10 samples
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only update if there's significant new data
|
# Only update if there's significant new data
|
||||||
if hasattr(self, '_last_plot_len') and len(self.time_data) - self._last_plot_len < 10:
|
if hasattr(self, '_last_plot_time') and (self.time_data[-1] - self._last_plot_time < 1.0):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._last_plot_len = len(self.time_data)
|
self._last_plot_time = self.time_data[-1]
|
||||||
|
|
||||||
# Update plot data
|
# Update plot data
|
||||||
self.line_voltage.set_data(self.time_data, self.voltage_data)
|
self.line_voltage.set_data(self.time_data, self.voltage_data)
|
||||||
self.line_current.set_data(self.time_data, self.current_data)
|
self.line_current.set_data(self.time_data, self.current_data)
|
||||||
|
|
||||||
# Set x-axis to always show from 0 to current max time
|
# Auto-scale axes if needed
|
||||||
min_time = 0 # Always start from 0
|
self.auto_scale_axes()
|
||||||
max_time = self.time_data[-1] + 1 # Add 1 second padding
|
|
||||||
|
|
||||||
# Only adjust limits if needed
|
# Only redraw if needed
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def auto_scale_axes(self):
|
||||||
|
"""Auto-scale plot axes with appropriate padding"""
|
||||||
|
if not self.time_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# X-axis scaling
|
||||||
|
min_time = 0
|
||||||
|
max_time = self.time_data[-1] * 1.05 # 5% padding
|
||||||
current_xlim = self.ax.get_xlim()
|
current_xlim = self.ax.get_xlim()
|
||||||
if abs(current_xlim[1] - max_time) > 5: # Only adjust if significant change
|
if abs(current_xlim[1] - max_time) > (max_time * 0.1): # 10% change threshold
|
||||||
self.ax.set_xlim(min_time, max_time)
|
self.ax.set_xlim(min_time, max_time)
|
||||||
self.ax2.set_xlim(min_time, max_time)
|
self.ax2.set_xlim(min_time, max_time)
|
||||||
|
|
||||||
# Auto-scale y-axes but respect initial boundaries
|
# Voltage axis scaling
|
||||||
voltage_padding = 0.2
|
voltage_padding = 0.2
|
||||||
if self.voltage_data:
|
if self.voltage_data:
|
||||||
min_voltage = max(0, min(self.voltage_data) - voltage_padding)
|
min_voltage = max(0, min(self.voltage_data) - voltage_padding)
|
||||||
max_voltage = max(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()
|
current_ylim = self.ax.get_ylim()
|
||||||
if abs(current_ylim[0] - min_voltage) > 0.1 or abs(current_ylim[1] - max_voltage) > 0.1:
|
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)
|
self.ax.set_ylim(min_voltage, max_voltage)
|
||||||
|
|
||||||
|
# Current axis scaling
|
||||||
current_padding = 0.05
|
current_padding = 0.05
|
||||||
if self.current_data:
|
if self.current_data:
|
||||||
test_current = self.c_rate.get() * self.capacity.get()
|
|
||||||
min_current = min(self.current_data) - current_padding
|
min_current = min(self.current_data) - current_padding
|
||||||
max_current = max(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()
|
current_ylim2 = self.ax2.get_ylim()
|
||||||
if abs(current_ylim2[0] - min_current) > 0.02 or abs(current_ylim2[1] - max_current) > 0.02:
|
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)
|
self.ax2.set_ylim(min_current, max_current)
|
||||||
|
|
||||||
# Only redraw if needed
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
|
|
||||||
def handle_device_error(self, error):
|
def handle_device_error(self, error):
|
||||||
"""Handle device connection errors"""
|
"""Handle device connection errors"""
|
||||||
@ -912,17 +930,24 @@ class BatteryTester:
|
|||||||
if hasattr(self, 'measurement_event'):
|
if hasattr(self, 'measurement_event'):
|
||||||
self.measurement_event.clear()
|
self.measurement_event.clear()
|
||||||
|
|
||||||
|
# Give threads time to clean up
|
||||||
|
timeout = 2.0 # seconds
|
||||||
if hasattr(self, 'measurement_thread'):
|
if hasattr(self, 'measurement_thread'):
|
||||||
self.measurement_thread.join(timeout=1.0)
|
self.measurement_thread.join(timeout=timeout)
|
||||||
|
if self.measurement_thread.is_alive():
|
||||||
|
print("Warning: Measurement thread did not terminate cleanly")
|
||||||
|
|
||||||
if hasattr(self, 'test_thread'):
|
if hasattr(self, 'test_thread'):
|
||||||
self.test_thread.join(timeout=1.0)
|
self.test_thread.join(timeout=timeout)
|
||||||
|
if self.test_thread.is_alive():
|
||||||
|
print("Warning: Test thread did not terminate cleanly")
|
||||||
|
|
||||||
if hasattr(self, 'session') and self.session:
|
if hasattr(self, 'session') and self.session:
|
||||||
try:
|
try:
|
||||||
if self.session_active:
|
if self.session_active:
|
||||||
self.session.end()
|
self.session.end()
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
print(f"Error ending session: {e}")
|
||||||
|
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user