From 668d88a1815610e17fc92dcc648e766d2e1537d7 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 10 Jul 2025 18:38:44 +0200 Subject: [PATCH] CSVVisualizer.py aktualisiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CSVVisualizer.py | 155 ++++++++++++++++++++++------------------------- 1 file changed, 74 insertions(+), 81 deletions(-) diff --git a/CSVVisualizer.py b/CSVVisualizer.py index b41789f..085b260 100644 --- a/CSVVisualizer.py +++ b/CSVVisualizer.py @@ -13,17 +13,18 @@ class CSVVisualizer: def __init__(self, root): self.root = root 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") - # Farben für die Phasen + # Farben für die Phasen (angepasst an adalm1000_logger.py) self.phase_colors = { "Charge": "#4E79A7", # Blau "Discharge": "#E15759", # Rot "Resting (Post-Charge)": "#59A14F", # Grün "Resting (Post-Discharge)": "#EDC948", # Gelb "Resting Between Cycles": "#B07AA1", # Lila - "Initial Discharge": "#FF9DA7" # Rosa + "Initial Discharge": "#FF9DA7", # Rosa + "Idle": "#CCCCCC" # Grau für inaktive Phasen } self.setup_ui() @@ -49,7 +50,7 @@ class CSVVisualizer: self.plot_frame.pack(fill=tk.BOTH, expand=True) # 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.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)) 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( - title="Select CSV File", + title="ADALM1000 Log-Datei auswählen", filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")] ) if not filepath: return - self.graph_title = "Battery Test Analysis" # Default title + # Extrahiere Testparameter aus der Log-Datei + test_params = {} with open(filepath, 'r') as f: - reader = csv.reader(f) - for row in reader: - if row and row[0].startswith("Cycle"): - self.graph_title = ", ".join(row).strip() + for line in f: + if line.startswith('# - '): + key, value = line[4:].strip().split(': ', 1) + test_params[key] = value + elif line.startswith('Time(s)'): 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: - # First detect problematic lines - self.status_var.set("Datei geladen – prüfe Zeilen…") - self.root.update_idletasks() - good_lines = [] + # Lese nur Datenzeilen (ignoriere Kommentare und leere Zeilen) + skip_rows = 0 with open(filepath, 'r') as f: - reader = csv.reader(f) - headers = next(reader) # Keep header - - 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() + for line in f: + if line.startswith('Time(s)'): + break + skip_rows += 1 + self.df = pd.read_csv( filepath, - skiprows=lambda x: x not in good_lines and x != 0, # keep header + skiprows=skip_rows, dtype={ 'Time(s)': 'float32', 'Voltage(V)': 'float32', 'Current(A)': 'float32', - 'Phase': 'category', + 'Phase': 'str', 'Discharge_Capacity(Ah)': 'float32', 'Charge_Capacity(Ah)': 'float32', 'Coulomb_Eff(%)': 'float32', '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.status_var.set(f"Working with: {len(self.df)} valid measurements...") - self.root.update_idletasks() + self.status_var.set(f"Daten geladen: {len(self.df)} Messungen") self.update_plot() except Exception as e: - messagebox.showerror("Error", f"Failed to load file:\n{str(e)}") - self.status_var.set("Error loading file") + messagebox.showerror("Fehler", f"Fehler beim Laden:\n{str(e)}") + self.status_var.set("Fehler beim Laden") def update_plot(self): """Aktualisiert den Plot mit den aktuellen Daten""" @@ -142,44 +131,56 @@ class CSVVisualizer: # Zeit relativ zum Start berechnen 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 - self.ax.clear() + # Plots zurücksetzen + self.ax_voltage.clear() + self.ax_current.clear() - # Spannung plotten - self.ax.plot(df_clean["Relative_Time(s)"], df_clean["Voltage(V)"], - label="Voltage (V)", color="black", linewidth=1.5) - #self.ax.margins(x=0) + # Spannung plotten (oberer Plot) + self.ax_voltage.plot(df_clean["Relative_Time"], df_clean["Voltage(V)"], + label="Spannung (V)", color="black", linewidth=1) - # 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 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: end_idx = i - start_time_rel = df_clean.iloc[start_idx]["Relative_Time(s)"] - end_time_rel = df_clean.iloc[end_idx]["Relative_Time(s)"] + start_time_rel = df_clean.iloc[start_idx]["Relative_Time"] + end_time_rel = df_clean.iloc[end_idx]["Relative_Time"] phase = df_clean.iloc[start_idx]["Phase"] - # Verwende Standardfarbe falls Phase nicht definiert ist 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 # Legende erstellen patches = [mpatches.Patch(color=self.phase_colors[phase], label=phase) for phase in self.phase_colors if phase in df_clean["Phase"].unique()] - # Füge Spannungs-Linie zur Legende hinzu - patches.append(plt.Line2D([0], [0], color='black', label='Voltage (V)')) + self.ax_voltage.legend(handles=patches, loc="upper right") + 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") - self.ax.set_xlabel("Time (s) since start") - self.ax.set_ylabel("Voltage (V)") - self.ax.set_title(getattr(self, 'graph_title')) - self.ax.grid(True) + # Gitternetz für beide Plots + self.ax_voltage.grid(True, alpha=0.3) + self.ax_current.grid(True, alpha=0.3) - # 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.status_var.set("Grafik aktualisiert") @@ -191,17 +192,11 @@ class CSVVisualizer: filetypes = [ ('PNG Image', '*.png'), - ('JPEG Image', '*.jpg'), ('PDF Document', '*.pdf'), ('SVG Vector', '*.svg') ] - default_filename = "battery_test_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" - + default_filename = "adalm1000_plot.png" filepath = filedialog.asksaveasfilename( title="Grafik speichern", initialfile=default_filename, @@ -212,14 +207,12 @@ class CSVVisualizer: if filepath: try: self.fig.savefig(filepath, dpi=300, bbox_inches='tight') - self.status_var.set(f"Grafik gespeichert als: {os.path.basename(filepath)}") - messagebox.showinfo("Erfolg", "Grafik wurde erfolgreich gespeichert") + messagebox.showinfo("Erfolg", f"Grafik gespeichert als:\n{filepath}") except Exception as e: - messagebox.showerror("Fehler", f"Konnte Grafik nicht speichern:\n{str(e)}") - self.status_var.set("Fehler beim Speichern") + messagebox.showerror("Fehler", f"Speichern fehlgeschlagen:\n{str(e)}") def cleanup(self): - """Perform cleanup before closing""" + """Aufräumen vor dem Schließen""" if hasattr(self, 'fig'): plt.close(self.fig) self.root.destroy()