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:
parent
a3bbfac3cf
commit
668d88a181
153
CSVVisualizer.py
153
CSVVisualizer.py
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user