CSVVisualizer.py aktualisiert

Neue Plot-Struktur:

        Zwei Subplots (Spannung oben, Strom unten)

        Synchronisierte X-Achsen (Zeit)

    Phasen-Handling:

        Konsistente Farben mit dem Logger

        Bessere Erkennung von Phasenübergängen

    Datenverarbeitung:

        Automatische Extraktion von Testparametern aus dem Log-Header

        Robustere Spaltenerkennung

    Benutzerfreundlichkeit:

        Klarere Statusmeldungen

        Bessere Fehlerbehandlung

    Visualisierung:

        Phasen als semi-transparente Hintergründe

        Automatische Skalierung der Achsen
(D)
This commit is contained in:
Jan 2025-07-10 18:38:44 +02:00
parent a3bbfac3cf
commit 668d88a181

View File

@ -13,17 +13,18 @@ class CSVVisualizer:
def __init__(self, root): def __init__(self, root):
self.root = root self.root = root
self.root.protocol("WM_DELETE_WINDOW", self.cleanup) self.root.protocol("WM_DELETE_WINDOW", self.cleanup)
self.root.title("CSV to Graph Converter") self.root.title("ADALM1000 Log Visualizer")
self.root.geometry("1000x700") self.root.geometry("1000x700")
# Farben für die Phasen # Farben für die Phasen (angepasst an adalm1000_logger.py)
self.phase_colors = { self.phase_colors = {
"Charge": "#4E79A7", # Blau "Charge": "#4E79A7", # Blau
"Discharge": "#E15759", # Rot "Discharge": "#E15759", # Rot
"Resting (Post-Charge)": "#59A14F", # Grün "Resting (Post-Charge)": "#59A14F", # Grün
"Resting (Post-Discharge)": "#EDC948", # Gelb "Resting (Post-Discharge)": "#EDC948", # Gelb
"Resting Between Cycles": "#B07AA1", # Lila "Resting Between Cycles": "#B07AA1", # Lila
"Initial Discharge": "#FF9DA7" # Rosa "Initial Discharge": "#FF9DA7", # Rosa
"Idle": "#CCCCCC" # Grau für inaktive Phasen
} }
self.setup_ui() self.setup_ui()
@ -49,7 +50,7 @@ class CSVVisualizer:
self.plot_frame.pack(fill=tk.BOTH, expand=True) self.plot_frame.pack(fill=tk.BOTH, expand=True)
# Platzhalter für den Plot # Platzhalter für den Plot
self.fig, self.ax = plt.subplots(figsize=(10, 5)) self.fig, (self.ax_voltage, self.ax_current) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame) self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
@ -59,79 +60,67 @@ class CSVVisualizer:
ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN).pack(fill=tk.X, pady=(5, 0)) ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN).pack(fill=tk.X, pady=(5, 0))
def load_csv(self): def load_csv(self):
"""Load CSV file with robust error handling for Raspberry Pi""" """Lädt CSV-Datei mit spezifischer Handhabung für ADALM1000-Logs"""
filepath = filedialog.askopenfilename( filepath = filedialog.askopenfilename(
title="Select CSV File", title="ADALM1000 Log-Datei auswählen",
filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")] filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")]
) )
if not filepath: if not filepath:
return return
self.graph_title = "Battery Test Analysis" # Default title # Extrahiere Testparameter aus der Log-Datei
test_params = {}
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
reader = csv.reader(f) for line in f:
for row in reader: if line.startswith('# - '):
if row and row[0].startswith("Cycle"): key, value = line[4:].strip().split(': ', 1)
self.graph_title = ", ".join(row).strip() test_params[key] = value
elif line.startswith('Time(s)'):
break break
self.graph_title = "ADALM1000 Battery Test"
if test_params:
self.graph_title = (
f"ADALM1000 Test | "
f"Capacity: {test_params.get('Battery Capacity', 'N/A')} | "
f"Current: {test_params.get('Test Current', 'N/A')}"
)
try: try:
# First detect problematic lines # Lese nur Datenzeilen (ignoriere Kommentare und leere Zeilen)
self.status_var.set("Datei geladen prüfe Zeilen…") skip_rows = 0
self.root.update_idletasks()
good_lines = []
with open(filepath, 'r') as f: with open(filepath, 'r') as f:
reader = csv.reader(f) for line in f:
headers = next(reader) # Keep header if line.startswith('Time(s)'):
break
skip_rows += 1
for i, row in enumerate(reader):
# Skip summary lines and malformed rows
if not row or len(row) < 4 or row[0].startswith('Cycle'):
continue
# Validate numeric columns
try:
float(row[0]) # Time(s)
float(row[1]) # Voltage(V)
float(row[2]) # Current(A)
good_lines.append(i+1) # +1 to account for header
except ValueError:
continue
if not good_lines:
messagebox.showwarning("Warnung", "Keine gültigen Datenzeilen gefunden.")
self.status_var.set("Keine gültigen Daten")
self.root.update_idletasks()
return
# Now read only valid lines
self.status_var.set("Verarbeite gültige Zeile...")
self.root.update_idletasks()
self.df = pd.read_csv( self.df = pd.read_csv(
filepath, filepath,
skiprows=lambda x: x not in good_lines and x != 0, # keep header skiprows=skip_rows,
dtype={ dtype={
'Time(s)': 'float32', 'Time(s)': 'float32',
'Voltage(V)': 'float32', 'Voltage(V)': 'float32',
'Current(A)': 'float32', 'Current(A)': 'float32',
'Phase': 'category', 'Phase': 'str',
'Discharge_Capacity(Ah)': 'float32', 'Discharge_Capacity(Ah)': 'float32',
'Charge_Capacity(Ah)': 'float32', 'Charge_Capacity(Ah)': 'float32',
'Coulomb_Eff(%)': 'float32', 'Coulomb_Eff(%)': 'float32',
'Cycle': 'int32' 'Cycle': 'int32'
}, }
engine='c',
memory_map=True
) )
# Bereinige Phasen-Namen
self.df['Phase'] = self.df['Phase'].str.strip()
self.file_label.config(text=os.path.basename(filepath)) self.file_label.config(text=os.path.basename(filepath))
self.status_var.set(f"Working with: {len(self.df)} valid measurements...") self.status_var.set(f"Daten geladen: {len(self.df)} Messungen")
self.root.update_idletasks()
self.update_plot() self.update_plot()
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Failed to load file:\n{str(e)}") messagebox.showerror("Fehler", f"Fehler beim Laden:\n{str(e)}")
self.status_var.set("Error loading file") self.status_var.set("Fehler beim Laden")
def update_plot(self): def update_plot(self):
"""Aktualisiert den Plot mit den aktuellen Daten""" """Aktualisiert den Plot mit den aktuellen Daten"""
@ -142,44 +131,56 @@ class CSVVisualizer:
# Zeit relativ zum Start berechnen # Zeit relativ zum Start berechnen
start_time = df_clean["Time(s)"].min() start_time = df_clean["Time(s)"].min()
df_clean["Relative_Time(s)"] = df_clean["Time(s)"] - start_time df_clean["Relative_Time"] = df_clean["Time(s)"] - start_time
# Plot zurücksetzen # Plots zurücksetzen
self.ax.clear() self.ax_voltage.clear()
self.ax_current.clear()
# Spannung plotten # Spannung plotten (oberer Plot)
self.ax.plot(df_clean["Relative_Time(s)"], df_clean["Voltage(V)"], self.ax_voltage.plot(df_clean["Relative_Time"], df_clean["Voltage(V)"],
label="Voltage (V)", color="black", linewidth=1.5) label="Spannung (V)", color="black", linewidth=1)
#self.ax.margins(x=0)
# Phasen als farbige Hintergründe # Strom plotten (unterer Plot)
self.ax_current.plot(df_clean["Relative_Time"], df_clean["Current(A)"],
label="Strom (A)", color="#D76364", linewidth=1)
# Phasen als farbige Hintergründe (für beide Plots)
start_idx = 0 start_idx = 0
for i in range(1, len(df_clean)): for i in range(1, len(df_clean)):
if df_clean.iloc[i]["Phase"] != df_clean.iloc[i-1]["Phase"] or i == len(df_clean) - 1: if df_clean.iloc[i]["Phase"] != df_clean.iloc[i-1]["Phase"] or i == len(df_clean) - 1:
end_idx = i end_idx = i
start_time_rel = df_clean.iloc[start_idx]["Relative_Time(s)"] start_time_rel = df_clean.iloc[start_idx]["Relative_Time"]
end_time_rel = df_clean.iloc[end_idx]["Relative_Time(s)"] end_time_rel = df_clean.iloc[end_idx]["Relative_Time"]
phase = df_clean.iloc[start_idx]["Phase"] phase = df_clean.iloc[start_idx]["Phase"]
# Verwende Standardfarbe falls Phase nicht definiert ist
color = self.phase_colors.get(phase, "#CCCCCC") color = self.phase_colors.get(phase, "#CCCCCC")
self.ax.axvspan(start_time_rel, end_time_rel, facecolor=color, alpha=0.3) self.ax_voltage.axvspan(start_time_rel, end_time_rel, facecolor=color, alpha=0.2)
self.ax_current.axvspan(start_time_rel, end_time_rel, facecolor=color, alpha=0.2)
start_idx = i start_idx = i
# Legende erstellen # Legende erstellen
patches = [mpatches.Patch(color=self.phase_colors[phase], label=phase) patches = [mpatches.Patch(color=self.phase_colors[phase], label=phase)
for phase in self.phase_colors if phase in df_clean["Phase"].unique()] for phase in self.phase_colors if phase in df_clean["Phase"].unique()]
# Füge Spannungs-Linie zur Legende hinzu self.ax_voltage.legend(handles=patches, loc="upper right")
patches.append(plt.Line2D([0], [0], color='black', label='Voltage (V)')) self.ax_voltage.set_ylabel("Spannung (V)")
self.ax_current.set_ylabel("Strom (A)")
self.ax_current.set_xlabel("Zeit (s) seit Start")
self.ax_voltage.set_title(self.graph_title)
self.ax.legend(handles=patches, loc="upper right") # Gitternetz für beide Plots
self.ax.set_xlabel("Time (s) since start") self.ax_voltage.grid(True, alpha=0.3)
self.ax.set_ylabel("Voltage (V)") self.ax_current.grid(True, alpha=0.3)
self.ax.set_title(getattr(self, 'graph_title'))
self.ax.grid(True)
# Aktualisiere den Canvas # Automatische Skalierung
self.ax_voltage.relim()
self.ax_voltage.autoscale_view()
self.ax_current.relim()
self.ax_current.autoscale_view()
# Canvas aktualisieren
self.fig.tight_layout()
self.canvas.draw() self.canvas.draw()
self.status_var.set("Grafik aktualisiert") self.status_var.set("Grafik aktualisiert")
@ -191,17 +192,11 @@ class CSVVisualizer:
filetypes = [ filetypes = [
('PNG Image', '*.png'), ('PNG Image', '*.png'),
('JPEG Image', '*.jpg'),
('PDF Document', '*.pdf'), ('PDF Document', '*.pdf'),
('SVG Vector', '*.svg') ('SVG Vector', '*.svg')
] ]
default_filename = "battery_test_plot.png" default_filename = "adalm1000_plot.png"
if hasattr(self, 'file_label'):
base = os.path.splitext(self.file_label.cget("text"))[0]
if base:
default_filename = f"{base}_plot.png"
filepath = filedialog.asksaveasfilename( filepath = filedialog.asksaveasfilename(
title="Grafik speichern", title="Grafik speichern",
initialfile=default_filename, initialfile=default_filename,
@ -212,14 +207,12 @@ class CSVVisualizer:
if filepath: if filepath:
try: try:
self.fig.savefig(filepath, dpi=300, bbox_inches='tight') self.fig.savefig(filepath, dpi=300, bbox_inches='tight')
self.status_var.set(f"Grafik gespeichert als: {os.path.basename(filepath)}") messagebox.showinfo("Erfolg", f"Grafik gespeichert als:\n{filepath}")
messagebox.showinfo("Erfolg", "Grafik wurde erfolgreich gespeichert")
except Exception as e: except Exception as e:
messagebox.showerror("Fehler", f"Konnte Grafik nicht speichern:\n{str(e)}") messagebox.showerror("Fehler", f"Speichern fehlgeschlagen:\n{str(e)}")
self.status_var.set("Fehler beim Speichern")
def cleanup(self): def cleanup(self):
"""Perform cleanup before closing""" """Aufräumen vor dem Schließen"""
if hasattr(self, 'fig'): if hasattr(self, 'fig'):
plt.close(self.fig) plt.close(self.fig)
self.root.destroy() self.root.destroy()