import os import sys import pandas as pd import plotly.graph_objects as go import tkinter as tk from plotly.subplots import make_subplots from tkinter import filedialog, messagebox # -------------------------------------------------- # Konstanten # -------------------------------------------------- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) REPO_ROOT = os.path.dirname(SCRIPT_DIR) DEFAULT_CSV = os.path.join( REPO_ROOT, "ScopeData", "Scope YT NC Project-10_Platten_reworked.csv" ) TIME_COL = "Time [ms]" # -------------------------------------------------- # CSV einlesen # -------------------------------------------------- def load_reworked_csv(path): """Liest den 'reworked' Scope-Export (Semikolon-getrennt) ein. Gibt (kurzer_name, DataFrame) zurueck. Zeilen mit EOF-Markern und Zeilen ohne Zeitwert werden verworfen, leere Signalzellen werden NaN. """ df = pd.read_csv(path, sep=";", low_memory=False) df.columns = [str(c).strip() for c in df.columns] if TIME_COL not in df.columns: raise ValueError( f"Spalte '{TIME_COL}' nicht gefunden in {os.path.basename(path)}" ) # Nur Zeilen mit numerischem Zeitwert behalten (filtert z.B. 'EOF'-Zeile) df[TIME_COL] = pd.to_numeric(df[TIME_COL], errors="coerce") df = df.dropna(subset=[TIME_COL]).sort_values(TIME_COL).reset_index(drop=True) # Signale numerisch konvertieren (leere Zellen -> NaN) for col in df.columns: if col != TIME_COL: df[col] = pd.to_numeric(df[col], errors="coerce") if df.empty: raise ValueError(f"Keine Daten in {path}") short_name = os.path.basename(path).replace(".csv", "") return short_name, df def signal_columns(df): """Alle Spalten, die als Signal geplotet werden koennen. Zeitspalten (z.B. 'Time [ms]' oder 'Current Time [ms]') werden als Signale ausgeschlossen. """ return [ col for col in df.columns if col != TIME_COL and "time [" not in col.lower() ] # -------------------------------------------------- # Plot erstellen # -------------------------------------------------- def create_plot(): files = file_list.get(0, "end") if not files: messagebox.showwarning("Hinweis", "Mindestens eine Datei laden.") return selected = [name for name, var in signal_vars.items() if var.get()] if not selected: messagebox.showwarning("Hinweis", "Mindestens ein Signal auswählen.") return # X-Range (Zoom) x_range = None try: xmin = xmin_entry.get().strip() xmax = xmax_entry.get().strip() if xmin and xmax: x_range = [float(xmin), float(xmax)] elif xmax: x_range = [0.0, float(xmax)] except ValueError: messagebox.showerror("Fehler", "Ungültige X-Achsen Werte") return # Y-Range if auto_scale_var.get(): y_range = None else: try: y_range = [float(ymin_entry.get()), float(ymax_entry.get())] except ValueError: messagebox.showerror("Fehler", "Ungültige Y-Achsen Werte") return subplot_mode = layout_var.get() == "subplots" n_rows = len(selected) if subplot_mode else 1 fig = make_subplots( rows=n_rows, cols=1, shared_xaxes=True, vertical_spacing=0.06 if n_rows > 1 else None, subplot_titles=selected if subplot_mode else None, ) for i, signal in enumerate(selected): row = i + 1 if subplot_mode else 1 for path in files: short_name, df = file_data[path] fig.add_trace( go.Scatter( x=df[TIME_COL], y=df[signal], mode="lines", name=f"{short_name}: {signal}", legendgroup=signal, showlegend=True, ), row=row, col=1, ) if y_range is not None: fig.update_yaxes(range=y_range, row=row, col=1) fig.update_yaxes(title_text=signal, row=row, col=1) fig.update_layout( legend=dict(orientation="h", yanchor="bottom", y=1.02) if subplot_mode else dict(orientation="v", yanchor="top"), title="Scope YT NC – Platten (reworked)", xaxis_title="Zeit [ms]", xaxis_range=x_range, height=360 * n_rows + 120, hovermode="x unified", template="plotly_white", ) fig.show() # -------------------------------------------------- # Datei-Liste (GUI) # -------------------------------------------------- def add_file(): paths = filedialog.askopenfilenames( title="Scope CSV(s) auswählen", initialdir=os.path.dirname(DEFAULT_CSV), filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")], ) for path in paths: path = os.path.abspath(path) if path not in file_list.get(0, "end"): try: short_name, df = load_reworked_csv(path) file_data[path] = (short_name, df) file_list.insert(tk.END, path) update_signal_checkboxes() except Exception as e: messagebox.showerror("Fehler", f"{path}\n\n{e}") def remove_file(): selection = file_list.curselection() if not selection: return # von unten nach oben entfernen, damit Indizes stabil bleiben for idx in reversed(selection): path = file_list.get(idx) file_data.pop(path, None) file_list.delete(idx) if not file_list.get(0, "end"): for var in signal_vars.values(): var.set(False) def update_signal_checkboxes(): """Signalliste aus allen geladenen Dateien aktualisieren (Vereinigung).""" union = [] for _, df in file_data.values(): for col in signal_columns(df): if col not in union: union.append(col) rebuild_signal_checkboxes(union) def rebuild_signal_checkboxes(columns): for widget in signal_frame.winfo_children(): widget.destroy() signal_vars.clear() for col in columns: var = tk.BooleanVar(value=True) tk.Checkbutton(signal_frame, text=col, variable=var).pack(anchor="w") signal_vars[col] = var # -------------------------------------------------- # Hauptprogramm # -------------------------------------------------- def main(): global file_list, file_data, signal_frame, signal_vars global layout_var, auto_scale_var global ymin_entry, ymax_entry, xmin_entry, xmax_entry # Kommandozeilen-Dateipfade (optional) cli_files = sys.argv[1:] root = tk.Tk() root.title("Scope Visualizer – YT NC Project (reworked)") root.geometry("480x560") # --- Dateien --- tk.Label(root, text="Dateien:").pack(anchor="w", padx=8, pady=(8, 0)) file_frame = tk.Frame(root) file_frame.pack(fill="x", padx=8) file_list = tk.Listbox(file_frame, height=4) file_list.pack(side="left", fill="both", expand=True) file_buttons = tk.Frame(file_frame) file_buttons.pack(side="left", padx=(6, 0)) tk.Button( file_buttons, text="Hinzufügen", width=12, command=add_file ).pack(pady=2) tk.Button( file_buttons, text="Entfernen", width=12, command=remove_file ).pack(pady=2) # --- Signale --- tk.Label(root, text="Signale:").pack(anchor="w", padx=8, pady=(8, 0)) signal_scroll = tk.Frame(root) signal_scroll.pack(fill="both", expand=True, padx=8) signal_canvas = tk.Canvas(signal_scroll, height=140, highlightthickness=0) signal_scrollbar = tk.Scrollbar( signal_scroll, orient="vertical", command=signal_canvas.yview ) signal_canvas.configure(yscrollcommand=signal_scrollbar.set) signal_scrollbar.pack(side="right", fill="y") signal_canvas.pack(side="left", fill="both", expand=True) signal_frame = tk.Frame(signal_canvas) canvas_window = signal_canvas.create_window( (0, 0), window=signal_frame, anchor="nw" ) signal_frame.bind( "", lambda e: signal_canvas.configure(scrollregion=signal_canvas.bbox("all")), ) signal_canvas.bind( "", lambda e: signal_canvas.itemconfigure(canvas_window, width=e.width), ) signal_canvas.bind_all( "", lambda e: signal_canvas.yview_scroll(-1 * (e.delta // 120), "units"), ) # --- Anzeige-Optionen --- options_frame = tk.LabelFrame(root, text="Anzeige") options_frame.pack(fill="x", padx=8, pady=8) layout_var = tk.StringVar(value="subplots") tk.Radiobutton( options_frame, text="Ein Plot pro Signal (Subplots)", variable=layout_var, value="subplots", ).pack(anchor="w", padx=6, pady=2) tk.Radiobutton( options_frame, text="Alle Signale überlagern", variable=layout_var, value="overlay", ).pack(anchor="w", padx=6, pady=2) auto_scale_var = tk.BooleanVar(value=True) tk.Checkbutton( options_frame, text="Automatische Y-Skalierung", variable=auto_scale_var ).pack(anchor="w", padx=6) y_frame = tk.Frame(options_frame) y_frame.pack(fill="x", padx=6, pady=2) tk.Label(y_frame, text="Y-Min").pack(side="left") ymin_entry = tk.Entry(y_frame, width=10) ymin_entry.pack(side="left", padx=(2, 12)) tk.Label(y_frame, text="Y-Max").pack(side="left") ymax_entry = tk.Entry(y_frame, width=10) ymax_entry.pack(side="left", padx=2) x_frame = tk.Frame(options_frame) x_frame.pack(fill="x", padx=6, pady=2) tk.Label(x_frame, text="X-Min [ms]").pack(side="left") xmin_entry = tk.Entry(x_frame, width=10) xmin_entry.pack(side="left", padx=(2, 12)) tk.Label(x_frame, text="X-Max [ms]").pack(side="left") xmax_entry = tk.Entry(x_frame, width=10) xmax_entry.pack(side="left", padx=2) tk.Button( root, text="Plot anzeigen", command=create_plot, padx=20 ).pack(pady=8) # --- Startdaten laden --- file_data = {} initial = ( cli_files if cli_files else ([DEFAULT_CSV] if os.path.exists(DEFAULT_CSV) else []) ) for path in initial: path = os.path.abspath(path) try: short_name, df = load_reworked_csv(path) file_data[path] = (short_name, df) file_list.insert(tk.END, path) except Exception as e: messagebox.showerror("Fehler", f"{path}\n\n{e}") signal_vars = {} if file_data: update_signal_checkboxes() root.mainloop() if __name__ == "__main__": main()