Files
FAT-Servo-Load-Test/Python/plotScopeReworked.py
T
2026-09-07 12:58:43 +02:00

344 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
"<Configure>",
lambda e: signal_canvas.configure(scrollregion=signal_canvas.bbox("all")),
)
signal_canvas.bind(
"<Configure>",
lambda e: signal_canvas.itemconfigure(canvas_window, width=e.width),
)
signal_canvas.bind_all(
"<MouseWheel>",
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()