228 lines
8.8 KiB
Python
228 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
import os
|
||
import csv
|
||
import pandas as pd
|
||
import matplotlib.pyplot as plt
|
||
import matplotlib.patches as mpatches
|
||
from tkinter import Tk, filedialog, messagebox
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||
import tkinter as tk
|
||
from tkinter import ttk
|
||
|
||
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.geometry("1000x700")
|
||
|
||
# Farben für die Phasen
|
||
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
|
||
}
|
||
|
||
self.setup_ui()
|
||
|
||
def setup_ui(self):
|
||
"""Erstellt die Benutzeroberfläche"""
|
||
main_frame = ttk.Frame(self.root)
|
||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
# Steuerleiste oben
|
||
control_frame = ttk.Frame(main_frame)
|
||
control_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
||
ttk.Button(control_frame, text="CSV auswählen", command=self.load_csv).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(control_frame, text="Grafik speichern", command=self.save_plot).pack(side=tk.LEFT, padx=5)
|
||
|
||
# Anzeige des aktuellen Dateipfads
|
||
self.file_label = ttk.Label(control_frame, text="Keine Datei ausgewählt")
|
||
self.file_label.pack(side=tk.LEFT, padx=10, expand=True)
|
||
|
||
# Plot-Bereich
|
||
self.plot_frame = ttk.Frame(main_frame)
|
||
self.plot_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Platzhalter für den Plot
|
||
self.fig, self.ax = plt.subplots(figsize=(10, 5))
|
||
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
|
||
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Statusleiste
|
||
self.status_var = tk.StringVar()
|
||
self.status_var.set("Bereit")
|
||
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"""
|
||
filepath = filedialog.askopenfilename(
|
||
title="Select CSV File",
|
||
filetypes=[("CSV Files", "*.csv"), ("All Files", "*.*")]
|
||
)
|
||
|
||
if not filepath:
|
||
return
|
||
|
||
with open(filepath, 'r') as f:
|
||
reader = csv.reader(f)
|
||
headers = next(reader) # Zeile 0
|
||
second_line = next(reader, []) # Zeile 1 (Titel)
|
||
self.graph_title = second_line[0] if second_line else "Battery Test Analysis"
|
||
|
||
try:
|
||
# First detect problematic lines
|
||
self.status_var.set("Datei geladen – prüfe Zeilen…")
|
||
self.root.update_idletasks()
|
||
good_lines = []
|
||
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 Zeilen…")
|
||
self.root.update_idletasks()
|
||
self.df = pd.read_csv(
|
||
filepath,
|
||
skiprows=lambda x: x not in good_lines and x != 0, # keep header
|
||
dtype={
|
||
'Time(s)': 'float32',
|
||
'Voltage(V)': 'float32',
|
||
'Current(A)': 'float32',
|
||
'Phase': 'category',
|
||
'Discharge_Capacity(Ah)': 'float32',
|
||
'Charge_Capacity(Ah)': 'float32',
|
||
'Coulomb_Eff(%)': 'float32',
|
||
'Cycle': 'int32'
|
||
},
|
||
engine='c',
|
||
memory_map=True
|
||
)
|
||
|
||
self.file_label.config(text=os.path.basename(filepath))
|
||
self.status_var.set(f"Loaded: {len(self.df)} valid measurements")
|
||
self.root.update_idletasks()
|
||
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")
|
||
|
||
def update_plot(self):
|
||
"""Aktualisiert den Plot mit den aktuellen Daten"""
|
||
if not hasattr(self, 'df'):
|
||
return
|
||
|
||
df_clean = self.df.copy()
|
||
|
||
# Zeit relativ zum Start berechnen
|
||
start_time = df_clean["Time(s)"].min()
|
||
df_clean["Relative_Time(s)"] = df_clean["Time(s)"] - start_time
|
||
|
||
# Plot zurücksetzen
|
||
self.ax.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)
|
||
|
||
# Phasen als farbige Hintergründe
|
||
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)"]
|
||
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)
|
||
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.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)
|
||
|
||
# Aktualisiere den Canvas
|
||
self.canvas.draw()
|
||
self.status_var.set("Grafik aktualisiert")
|
||
|
||
def save_plot(self):
|
||
"""Speichert den aktuellen Plot als Bilddatei"""
|
||
if not hasattr(self, 'df'):
|
||
messagebox.showwarning("Warnung", "Keine Daten zum Speichern vorhanden")
|
||
return
|
||
|
||
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"
|
||
|
||
filepath = filedialog.asksaveasfilename(
|
||
title="Grafik speichern",
|
||
initialfile=default_filename,
|
||
filetypes=filetypes,
|
||
defaultextension=".png"
|
||
)
|
||
|
||
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")
|
||
except Exception as e:
|
||
messagebox.showerror("Fehler", f"Konnte Grafik nicht speichern:\n{str(e)}")
|
||
self.status_var.set("Fehler beim Speichern")
|
||
|
||
def cleanup(self):
|
||
"""Perform cleanup before closing"""
|
||
if hasattr(self, 'fig'):
|
||
plt.close(self.fig)
|
||
self.root.destroy()
|
||
|
||
if __name__ == "__main__":
|
||
root = Tk()
|
||
app = CSVVisualizer(root)
|
||
root.mainloop() |