"""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()