diff --git a/Python/plotScope.py b/Python/plotScope.py index 34b1bc3..342d124 100644 --- a/Python/plotScope.py +++ b/Python/plotScope.py @@ -1,3 +1,4 @@ +import sys import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots @@ -32,14 +33,18 @@ def load_scope_csv(filename): raise Exception("Signaldefinition nicht gefunden") # Datenblock suchen + # Entweder folgt auf den Header ein
-Trenner (aeltere Scope-Exporte), + # oder die Daten folgen direkt nach dem Metadaten-Block. Im zweiten Fall + # starten wir einfach hinter dem Header; die Metadaten-Zeilen sowie ein + # moegliches EOF-Marker werden im numerischen Pruefschritt uebersprungen. data_start = None - for i in range(header_idx, len(lines)): + for i in range(header_idx + 1, len(lines)): if lines[i].startswith("
"): data_start = i + 1 break if data_start is None: - raise Exception("Datenblock nicht gefunden") + data_start = header_idx + 1 numeric_rows = [] @@ -130,65 +135,75 @@ def create_plot(): # -------------------------------------------------- -# Datei auswählen +# Hauptprogramm # -------------------------------------------------- -root = tk.Tk() -root.withdraw() +def main(): + global df, root, signal_vars, auto_scale_var, ymin_entry, ymax_entry -filename = filedialog.askopenfilename( - title="TwinCAT Scope CSV auswählen", - filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")] -) + # Datei auswählen + if len(sys.argv) > 1: + # Dateipfad als Kommandozeilenargument uebergeben + filename = sys.argv[1] + else: + temp_root = tk.Tk() + temp_root.withdraw() + filename = filedialog.askopenfilename( + title="TwinCAT Scope CSV auswählen", + filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")] + ) + temp_root.destroy() -if not filename: - raise SystemExit + if not filename: + raise SystemExit -df = load_scope_csv(filename) + df = load_scope_csv(filename) -# -------------------------------------------------- -# GUI -# -------------------------------------------------- -root = tk.Tk() -root.title("TwinCAT Scope Plotter") + # GUI + root = tk.Tk() + root.title("TwinCAT Scope Plotter") -signal_vars = {} + signal_vars = {} -tk.Label(root, text="Signale auswählen").pack(anchor="w") + tk.Label(root, text="Signale auswählen").pack(anchor="w") -for col in df.columns[1:]: - var = tk.BooleanVar(value=True) + for col in df.columns[1:]: + var = tk.BooleanVar(value=True) + + tk.Checkbutton( + root, + text=col, + variable=var + ).pack(anchor="w") + + signal_vars[col] = var + + auto_scale_var = tk.BooleanVar(value=True) tk.Checkbutton( root, - text=col, - variable=var - ).pack(anchor="w") + text="Automatische Y-Skalierung", + variable=auto_scale_var + ).pack(anchor="w", pady=10) - signal_vars[col] = var + frame = tk.Frame(root) + frame.pack() -auto_scale_var = tk.BooleanVar(value=True) + tk.Label(frame, text="Y-Min").grid(row=0, column=0) + ymin_entry = tk.Entry(frame, width=10) + ymin_entry.grid(row=0, column=1) -tk.Checkbutton( - root, - text="Automatische Y-Skalierung", - variable=auto_scale_var -).pack(anchor="w", pady=10) + tk.Label(frame, text="Y-Max").grid(row=0, column=2) + ymax_entry = tk.Entry(frame, width=10) + ymax_entry.grid(row=0, column=3) -frame = tk.Frame(root) -frame.pack() + tk.Button( + root, + text="Plot anzeigen", + command=create_plot + ).pack(pady=10) -tk.Label(frame, text="Y-Min").grid(row=0, column=0) -ymin_entry = tk.Entry(frame, width=10) -ymin_entry.grid(row=0, column=1) + root.mainloop() -tk.Label(frame, text="Y-Max").grid(row=0, column=2) -ymax_entry = tk.Entry(frame, width=10) -ymax_entry.grid(row=0, column=3) -tk.Button( - root, - text="Plot anzeigen", - command=create_plot -).pack(pady=10) - -root.mainloop() +if __name__ == "__main__": + main() diff --git a/Python/resync_csv.py b/Python/resync_csv.py new file mode 100644 index 0000000..6c9f73b --- /dev/null +++ b/Python/resync_csv.py @@ -0,0 +1,165 @@ +"""resync_csv.py + +Entfernt aus einem 'reworked' Scope-CSV alle Daten, in denen die Achse +stillsteht, und schreibt ein NEUES CSV-File. Das Ursprungsfile wird +nicht geloescht oder ueberschrieben. + +Wichtig: Die Stromspalte ('current_mA') hat eine andere Zeitachse +('Current Time [ms]', Sampling 10 ms) als die Achsenspalten +('Time [ms]', Sampling 2 ms). Beide Zeitspalten bleiben in jeder +Zeile unveraendert erhalten, damit der Stromkanal weiterhin auf seiner +eigenen Zeitbasis referenziert ist. Der Stillstand wird ausschließlich +aus der ACTPOS-Spalte erkannt. + +Stillstand-Erkennung: + Eine Zeile gilt als stillstehend, wenn die ACTPOS-Spanne + (max - min) in einem zentrierten Fenster um diese Zeile + (Standard: 100 ms) unterhalb der Schwelle (Standard: 0.001) liegt. + +Verwendung: + python resync_csv.py [--threshold 0.001] [--window-ms 100] +""" + +import argparse +import os +import sys + +import pandas as pd + +TIME_COL = "Time [ms]" +POS_COL = "ACTPOS" +OUTPUT_SUFFIX = "_resynced" + + +# -------------------------------------------------- +# CSV einlesen (wie im Scope-Viewer) +# -------------------------------------------------- +def load_reworked_csv(path): + """Liest den 'reworked' Scope-Export (Semikolon-getrennt) ein. + + Zeilen ohne numerischen Zeitwert (z.B. 'EOF') 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)}") + if POS_COL not in df.columns: + raise ValueError(f"Spalte '{POS_COL}' nicht gefunden in {os.path.basename(path)}") + + df[TIME_COL] = pd.to_numeric(df[TIME_COL], errors="coerce") + df = df.dropna(subset=[TIME_COL]) + + 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}") + + return df + + +# -------------------------------------------------- +# Stillstand-Erkennung +# -------------------------------------------------- +def find_moving_mask(df, window_ms, threshold, sample_ms=2.0): + """Gibt Boolesche Maske zurueck: True = Achse bewegt sich. + + Basis ist ausschliesslich ACTPOS (2-ms-Kanal). Der Stromkanal wird + nicht betrachtet, da er auf einer eigenen 10-ms-Zeitachse liegt. + + Eine Zeile gilt als bewegend, wenn die ACTPOS-Spanne (max-min) im + zentrierten Fenster um die Zeile die Schwelle erreicht. Zeilen + direkt am Rand einer Bewegung (halb Fensterbreite) bleiben daher + erhalten; ausgedehnte Stillstandsphasen werden komplett entfernt. + """ + window = max(1, int(round(window_ms / sample_ms))) + pos = df[POS_COL] + + rmax = pos.rolling(2 * window + 1, center=True, min_periods=1).max() + rmin = pos.rolling(2 * window + 1, center=True, min_periods=1).min() + + return (rmax - rmin) >= threshold + + +# -------------------------------------------------- +# Ausgabe +# -------------------------------------------------- +def unique_output_path(src_path): + """Zielt auf _resynced.csv, bei Belegung zahlt hoch (1, 2, ...).""" + directory = os.path.dirname(os.path.abspath(src_path)) + stem = os.path.splitext(os.path.basename(src_path))[0] + + candidate = os.path.join(directory, f"{stem}{OUTPUT_SUFFIX}.csv") + counter = 1 + while os.path.exists(candidate): + candidate = os.path.join(directory, f"{stem}{OUTPUT_SUFFIX}_{counter}.csv") + counter += 1 + return candidate + + +def main(): + parser = argparse.ArgumentParser( + description="Entfernt Stillstandsdaten aus einem reworked Scope-CSV " + "und schreibt ein neues CSV (Ursprungsfile bleibt unverändert)." + ) + parser.add_argument("csv", help="Pfad zum Quelldatei (.csv)") + parser.add_argument( + "--threshold", type=float, default=1e-3, + help="Minimale ACTPOS-Spanne im Fenster, ab der Bewegung gilt " + "(Standard: 0.001)" + ) + parser.add_argument( + "--window-ms", type=float, default=100.0, + help="Breite des zentrierten Erkennungs-Fensters in ms (Standard: 100)" + ) + parser.add_argument( + "--output", + help="Optionaler Ziel-Pfad. Standard: _resynced.csv im selben Ordner" + ) + args = parser.parse_args() + + src = os.path.abspath(args.csv) + if not os.path.isfile(src): + sys.exit(f"Fehler: Datei nicht gefunden: {src}") + + # Sicherheitsnetz: Ursprungsfile darf nie das Ziel werden + if args.output: + dst = os.path.abspath(args.output) + if dst == src: + sys.exit("Fehler: Output-Pfad ist identisch mit der Quelldatei.") + else: + dst = unique_output_path(src) + + df = load_reworked_csv(src) + n_in = len(df) + + moving = find_moving_mask(df, args.window_ms, args.threshold) + out = df[moving] + n_out = len(out) + + if n_out == 0: + print("Keine Bewegung erkannt – es wurde keine Datei geschrieben.") + sys.exit(1) + + # Zeilenreihenfolge (inkl. beider Zeitachsen) unverändert erhalten + out.to_csv(dst, sep=";", index=False, na_rep="") + # EOF-Marker wie in den Original-Exports anhaengen + with open(dst, "a", encoding="utf-8") as f: + f.write("\n" + ";".join(["EOF"] + [""] * (len(out.columns) - 1)) + "\n") + + removed = n_in - n_out + removed_time = df.loc[~moving, TIME_COL] + removed_ms = ( + float(removed_time.max() - removed_time.min()) if len(removed_time) else 0.0 + ) + print(f"Eingelesen: {n_in} Zeilen aus {os.path.basename(src)}") + print(f"Entfernt (still): {removed} Zeilen (ca. {removed_ms / 1000:.1f} s Zeitspanne)") + print(f"Behalten (bewegt): {n_out} Zeilen") + print(f"Neues File: {dst}") + + +if __name__ == "__main__": + main() diff --git a/Python/temperature_and_brake_log.csv b/Python/temperature_and_brake_log.csv index 0a46637..ac2dd01 100644 --- a/Python/temperature_and_brake_log.csv +++ b/Python/temperature_and_brake_log.csv @@ -1,37 +1,4 @@ Zeitstempel;Temperatur_C;Bremse_Manuell -2026-09-08 10:19:06;239.00;False -2026-09-08 10:19:07;239.00;False -2026-09-08 10:19:08;238.00;False -2026-09-08 10:19:09;239.00;False -2026-09-08 10:19:10;239.00;False -2026-09-08 10:19:11;239.00;False -2026-09-08 10:19:12;239.00;False -2026-09-08 10:19:13;239.00;False -2026-09-08 10:19:14;238.00;False -2026-09-08 10:19:15;239.00;False -2026-09-08 10:19:16;239.00;False -2026-09-08 10:19:17;239.00;False -2026-09-08 10:19:18;238.00;False -2026-09-08 10:19:19;239.00;False -2026-09-08 10:19:20;239.00;False -2026-09-08 10:19:43;23.90;False -2026-09-08 10:19:44;23.90;False -2026-09-08 10:19:45;23.90;False -2026-09-08 10:19:46;23.90;False -2026-09-08 10:19:47;23.90;False -2026-09-08 10:19:48;23.90;False -2026-09-08 10:19:49;23.90;False -2026-09-08 10:19:50;23.90;False -2026-09-08 10:19:51;23.90;False -2026-09-08 10:19:52;23.90;False -2026-09-08 10:19:53;23.90;False -2026-09-08 10:19:54;23.90;False -2026-09-08 10:19:55;23.90;False -2026-09-08 10:19:56;23.90;False -2026-09-08 10:19:57;23.90;False -2026-09-08 10:19:58;23.90;False -2026-09-08 10:19:59;23.90;False -2026-09-08 10:20:00;23.90;False 2026-09-08 10:22:23;23.90;0 2026-09-08 10:22:24;23.90;0 2026-09-08 10:22:25;23.90;0