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