All checks were successful
CI-Build/Kettenoeler/pipeline/head This commit looks good
974 lines
43 KiB
Python
974 lines
43 KiB
Python
#!/usr/bin/env python3
|
||
import json
|
||
import os
|
||
import sys
|
||
import threading
|
||
import subprocess
|
||
import shutil
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox
|
||
import tempfile
|
||
|
||
SCRIPT_NAME = "can_universal_signal_finder.py"
|
||
SPLIT_SCRIPT = "can_split_by_id.py"
|
||
EXPLORE_SCRIPT = "id_signal_explorer.py"
|
||
TRACE_BATCH = "trace_batch_analyzer.py"
|
||
RANGE_FITTER = "trace_signal_fitter.py"
|
||
|
||
LOG_PATTERNS = ("*.log", "*.txt")
|
||
TRACE_PATTERNS = ("*.trace",)
|
||
|
||
|
||
# ---------------- helpers ----------------
|
||
def now_stamp():
|
||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
|
||
def find_logs(root: Path, rel_logs_dir: str):
|
||
base = (root / rel_logs_dir) if rel_logs_dir else root
|
||
found = []
|
||
if not base.exists():
|
||
return found
|
||
for pat in LOG_PATTERNS:
|
||
found += [str(p) for p in base.glob(pat)]
|
||
found += [str(p) for p in base.rglob(pat)] # include subdirs
|
||
return sorted(set(found))
|
||
|
||
|
||
def find_traces(base: Path):
|
||
"""Liste .trace im Basisordner und eine Ebene tiefer."""
|
||
files = []
|
||
if not base.exists():
|
||
return files
|
||
for pat in TRACE_PATTERNS:
|
||
files += [str(p) for p in base.glob(pat)]
|
||
files += [str(p) for p in base.glob(f"*/*{pat[1:]}")] # eine Ebene tiefer
|
||
return sorted(set(files))
|
||
|
||
|
||
def ensure_dir(p: Path):
|
||
p.mkdir(parents=True, exist_ok=True)
|
||
return p
|
||
|
||
|
||
def latest_subdir(base: Path) -> Path:
|
||
"""Neuester Unterordner in base, sonst base selbst."""
|
||
if not base.exists():
|
||
return base
|
||
subs = [p for p in base.iterdir() if p.is_dir()]
|
||
if not subs:
|
||
return base
|
||
return max(subs, key=lambda p: p.stat().st_mtime)
|
||
|
||
|
||
# ---------------- shared app state ----------------
|
||
class AppState:
|
||
def __init__(self):
|
||
# core paths
|
||
self.workdir = tk.StringVar(value="")
|
||
self.logs_dir = tk.StringVar(value="logs")
|
||
self.traces_dir = tk.StringVar(value="traces")
|
||
self.analyze_out_base = tk.StringVar(value="analyze_out")
|
||
|
||
# discovered logs
|
||
self.available_logs = [] # absolute paths
|
||
self.selected_log_indices = [] # indices in header listbox
|
||
|
||
# project defaults
|
||
self.timestamp_runs = tk.BooleanVar(value=True)
|
||
|
||
# shared traces directory + file list
|
||
self.traces_current_dir = tk.StringVar(value="") # absoluter Pfad zum aktuell angezeigten Traces-Ordner
|
||
self.traces_files = [] # Liste der .trace in current_dir (inkl. eine Ebene tiefer)
|
||
self._trace_observers = [] # callbacks, die Liste aktualisieren
|
||
|
||
# hook: wenn der Pfad geändert wird, scannen
|
||
self.traces_current_dir.trace_add("write", self._on_traces_dir_changed)
|
||
|
||
# --- path helpers ---
|
||
def workdir_path(self) -> Path:
|
||
wd = self.workdir.get().strip() or "."
|
||
return Path(wd)
|
||
|
||
def logs_base_path(self) -> Path:
|
||
return self.workdir_path() / (self.logs_dir.get().strip() or "logs")
|
||
|
||
def traces_base_path(self) -> Path:
|
||
return self.workdir_path() / (self.traces_dir.get().strip() or "traces")
|
||
|
||
def analyze_out_root(self) -> Path:
|
||
return self.workdir_path() / (self.analyze_out_base.get().strip() or "analyze_out")
|
||
|
||
# --- traces state ---
|
||
def add_trace_observer(self, cb):
|
||
if cb not in self._trace_observers:
|
||
self._trace_observers.append(cb)
|
||
|
||
def _notify_trace_observers(self):
|
||
for cb in list(self._trace_observers):
|
||
try:
|
||
cb(self.traces_files)
|
||
except Exception:
|
||
pass
|
||
|
||
def _on_traces_dir_changed(self, *_):
|
||
base = Path(self.traces_current_dir.get().strip() or str(self.traces_base_path()))
|
||
self.traces_files = find_traces(base)
|
||
self._notify_trace_observers()
|
||
|
||
def set_traces_dir(self, path: str):
|
||
self.traces_current_dir.set(path) # löst automatisch scan + notify aus
|
||
|
||
def refresh_traces(self):
|
||
# retrigger write to force refresh
|
||
self._on_traces_dir_changed()
|
||
|
||
def set_traces_to_default_or_latest(self):
|
||
base = self.traces_base_path()
|
||
target = latest_subdir(base)
|
||
self.set_traces_dir(str(target))
|
||
|
||
|
||
# ---------------- header (workdir + logs selection) ----------------
|
||
class Header(ttk.Frame):
|
||
def __init__(self, master, state: AppState):
|
||
super().__init__(master, padding=8)
|
||
self.state = state
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(1, weight=1)
|
||
self.columnconfigure(3, weight=1)
|
||
# row 0: workdir + scan
|
||
ttk.Label(self, text="Workdir:").grid(row=0, column=0, sticky="w")
|
||
self.ent_workdir = ttk.Entry(self, textvariable=self.state.workdir)
|
||
self.ent_workdir.grid(row=0, column=1, sticky="ew", padx=6)
|
||
ttk.Button(self, text="Wählen…", command=self.pick_workdir).grid(row=0, column=2, padx=5)
|
||
ttk.Button(self, text="Logs scannen", command=self.scan_logs).grid(row=0, column=3, padx=5)
|
||
|
||
# row 1: subfolders + timestamp checkbox
|
||
ttk.Label(self, text="Logs-Unterordner:").grid(row=1, column=0, sticky="w")
|
||
ttk.Entry(self, textvariable=self.state.logs_dir, width=24).grid(row=1, column=1, sticky="w", padx=6)
|
||
|
||
ttk.Label(self, text="Traces-Unterordner:").grid(row=1, column=2, sticky="w")
|
||
ttk.Entry(self, textvariable=self.state.traces_dir, width=24).grid(row=1, column=3, sticky="w", padx=6)
|
||
|
||
ttk.Label(self, text="Analyze-Output:").grid(row=2, column=0, sticky="w")
|
||
ttk.Entry(self, textvariable=self.state.analyze_out_base, width=24).grid(row=2, column=1, sticky="w", padx=6)
|
||
ttk.Checkbutton(self, text="Zeitstempel-Unterordner pro Run", variable=self.state.timestamp_runs).grid(row=2, column=2, columnspan=2, sticky="w")
|
||
|
||
# row 3: logs list
|
||
frm = ttk.LabelFrame(self, text="Gefundene Logdateien (Mehrfachauswahl möglich)")
|
||
frm.grid(row=3, column=0, columnspan=4, sticky="nsew", pady=(8,0))
|
||
self.rowconfigure(3, weight=1)
|
||
frm.columnconfigure(0, weight=1)
|
||
frm.rowconfigure(0, weight=1)
|
||
|
||
self.lst_logs = tk.Listbox(frm, height=6, selectmode=tk.EXTENDED)
|
||
self.lst_logs.grid(row=0, column=0, sticky="nsew", padx=(8,4), pady=8)
|
||
|
||
btns = ttk.Frame(frm)
|
||
btns.grid(row=0, column=1, sticky="ns", padx=(4,8), pady=8)
|
||
ttk.Button(btns, text="Alle wählen", command=self.select_all).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Keine", command=self.select_none).pack(fill="x", pady=2)
|
||
ttk.Separator(btns, orient="horizontal").pack(fill="x", pady=6)
|
||
ttk.Button(btns, text="Manuell hinzufügen…", command=self.add_logs_manual).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Entfernen", command=self.remove_selected_logs).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Liste leeren", command=self.clear_logs).pack(fill="x", pady=2)
|
||
ttk.Separator(btns, orient="horizontal").pack(fill="x", pady=6)
|
||
ttk.Button(btns, text="Projekt speichern…", command=self.save_project).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Projekt laden…", command=self.load_project).pack(fill="x", pady=2)
|
||
|
||
# ---- actions ----
|
||
def pick_workdir(self):
|
||
d = filedialog.askdirectory(title="Workdir auswählen")
|
||
if d:
|
||
self.state.workdir.set(d)
|
||
self.scan_logs()
|
||
# automatisch auch traces default/latest setzen
|
||
self.state.set_traces_to_default_or_latest()
|
||
|
||
def scan_logs(self):
|
||
wd = self.state.workdir_path()
|
||
logs_dir = self.state.logs_dir.get().strip()
|
||
found = find_logs(wd, logs_dir)
|
||
self.state.available_logs = found
|
||
self.lst_logs.delete(0, tk.END)
|
||
for p in found:
|
||
self.lst_logs.insert(tk.END, p)
|
||
# default-select all
|
||
self.lst_logs.select_set(0, tk.END)
|
||
self.state.selected_log_indices = list(range(len(found)))
|
||
|
||
def select_all(self):
|
||
self.lst_logs.select_set(0, tk.END)
|
||
self.state.selected_log_indices = list(range(self.lst_logs.size()))
|
||
|
||
def select_none(self):
|
||
self.lst_logs.select_clear(0, tk.END)
|
||
self.state.selected_log_indices = []
|
||
|
||
def add_logs_manual(self):
|
||
paths = filedialog.askopenfilenames(title="Logdateien auswählen", filetypes=[("Logfiles","*.log *.txt"),("Alle Dateien","*.*")])
|
||
if not paths: return
|
||
for p in paths:
|
||
if p not in self.state.available_logs:
|
||
self.state.available_logs.append(p)
|
||
self.lst_logs.insert(tk.END, p)
|
||
# if workdir empty, infer from first added
|
||
if not self.state.workdir.get().strip():
|
||
self.state.workdir.set(str(Path(paths[0]).resolve().parent))
|
||
# auch traces default/latest
|
||
self.state.set_traces_to_default_or_latest()
|
||
|
||
def remove_selected_logs(self):
|
||
sel = list(self.lst_logs.curselection())
|
||
sel.reverse()
|
||
for i in sel:
|
||
p = self.lst_logs.get(i)
|
||
if p in self.state.available_logs:
|
||
self.state.available_logs.remove(p)
|
||
self.lst_logs.delete(i)
|
||
self.state.selected_log_indices = [i for i in range(self.lst_logs.size()) if self.lst_logs.select_includes(i)]
|
||
|
||
def clear_logs(self):
|
||
self.state.available_logs = []
|
||
self.lst_logs.delete(0, tk.END)
|
||
self.state.selected_log_indices = []
|
||
|
||
def selected_logs(self):
|
||
idx = self.lst_logs.curselection()
|
||
if not idx:
|
||
return []
|
||
return [self.lst_logs.get(i) for i in idx]
|
||
|
||
# ---- project save/load ----
|
||
def collect_project(self):
|
||
return {
|
||
"workdir": self.state.workdir.get(),
|
||
"logs_dir": self.state.logs_dir.get(),
|
||
"traces_dir": self.state.traces_dir.get(),
|
||
"analyze_out_base": self.state.analyze_out_base.get(),
|
||
"timestamp_runs": bool(self.state.timestamp_runs.get()),
|
||
"available_logs": self.state.available_logs,
|
||
"selected_indices": list(self.lst_logs.curselection()),
|
||
"traces_current_dir": self.state.traces_current_dir.get(),
|
||
}
|
||
|
||
def apply_project(self, cfg):
|
||
self.state.workdir.set(cfg.get("workdir",""))
|
||
self.state.logs_dir.set(cfg.get("logs_dir","logs"))
|
||
self.state.traces_dir.set(cfg.get("traces_dir","traces"))
|
||
self.state.analyze_out_base.set(cfg.get("analyze_out_base","analyze_out"))
|
||
self.state.timestamp_runs.set(cfg.get("timestamp_runs", True))
|
||
# restore logs
|
||
self.scan_logs()
|
||
# If project contained explicit available_logs, merge
|
||
for p in cfg.get("available_logs", []):
|
||
if p not in self.state.available_logs:
|
||
self.state.available_logs.append(p)
|
||
self.lst_logs.insert(tk.END, p)
|
||
# re-select indices if valid
|
||
self.lst_logs.select_clear(0, tk.END)
|
||
for i in cfg.get("selected_indices", []):
|
||
if 0 <= i < self.lst_logs.size():
|
||
self.lst_logs.select_set(i)
|
||
# traces current dir: sofern vorhanden nutzen, sonst default/latest
|
||
tdir = cfg.get("traces_current_dir", "")
|
||
if tdir and Path(tdir).exists():
|
||
self.state.set_traces_dir(tdir)
|
||
else:
|
||
self.state.set_traces_to_default_or_latest()
|
||
|
||
def save_project(self):
|
||
cfg = self.collect_project()
|
||
path = filedialog.asksaveasfilename(title="Projekt speichern", defaultextension=".json", filetypes=[("Projektdatei","*.json")])
|
||
if not path: return
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(cfg, f, indent=2)
|
||
messagebox.showinfo("Gespeichert", f"Projekt gespeichert:\n{path}")
|
||
|
||
def load_project(self):
|
||
path = filedialog.askopenfilename(title="Projekt laden", filetypes=[("Projektdatei","*.json"),("Alle Dateien","*.*")])
|
||
if not path: return
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
cfg = json.load(f)
|
||
self.apply_project(cfg)
|
||
messagebox.showinfo("Geladen", f"Projekt geladen:\n{path}")
|
||
|
||
|
||
# ---------------- shared Trace Panel ----------------
|
||
class TracePanel(ttk.LabelFrame):
|
||
"""
|
||
Einheitliche Trace-Auswahl: Liste links, Buttons rechts.
|
||
Nutzt AppState.traces_current_dir + AppState.traces_files.
|
||
single_select=True => Listbox SINGLE, versteckt 'Alle/Keine'.
|
||
"""
|
||
def __init__(self, master, state: AppState, title="Traces", single_select=False, height=10):
|
||
super().__init__(master, text=title)
|
||
self.state = state
|
||
self.single_select = single_select
|
||
self.height = height
|
||
self._build_ui()
|
||
# subscribe to state updates
|
||
self.state.add_trace_observer(self._on_traces_updated)
|
||
# initial fill from state
|
||
self._on_traces_updated(self.state.traces_files)
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(0, weight=1)
|
||
self.rowconfigure(0, weight=1)
|
||
|
||
selectmode = tk.SINGLE if self.single_select else tk.EXTENDED
|
||
self.lst = tk.Listbox(self, height=self.height, selectmode=selectmode)
|
||
self.lst.grid(row=0, column=0, sticky="nsew", padx=(8,4), pady=8)
|
||
|
||
btns = ttk.Frame(self)
|
||
btns.grid(row=0, column=1, sticky="ns", padx=(4,8), pady=8)
|
||
|
||
ttk.Button(btns, text="Traces-Ordner wählen…", command=self._pick_traces_dir).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Workdir/traces", command=self._use_default_traces).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Neuester Split", command=self._use_latest_split).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Refresh", command=self._refresh_traces).pack(fill="x", pady=6)
|
||
if not self.single_select:
|
||
ttk.Button(btns, text="Alle wählen", command=lambda: self.lst.select_set(0, tk.END)).pack(fill="x", pady=2)
|
||
ttk.Button(btns, text="Keine", command=lambda: self.lst.select_clear(0, tk.END)).pack(fill="x", pady=2)
|
||
|
||
# --- state sync ---
|
||
def _on_traces_updated(self, files):
|
||
# refresh list content
|
||
cur_sel_paths = self.get_selected()
|
||
self.lst.delete(0, tk.END)
|
||
for p in files:
|
||
self.lst.insert(tk.END, p)
|
||
# try to restore selection
|
||
if cur_sel_paths:
|
||
path_to_index = {self.lst.get(i): i for i in range(self.lst.size())}
|
||
for p in cur_sel_paths:
|
||
if p in path_to_index:
|
||
self.lst.select_set(path_to_index[p])
|
||
|
||
def _pick_traces_dir(self):
|
||
d = filedialog.askdirectory(title="Traces-Ordner wählen", initialdir=str(self.state.traces_base_path()))
|
||
if d:
|
||
self.state.set_traces_dir(d)
|
||
|
||
def _use_default_traces(self):
|
||
# default or latest under Workdir/traces
|
||
self.state.set_traces_to_default_or_latest()
|
||
|
||
def _use_latest_split(self):
|
||
base = self.state.traces_base_path()
|
||
target = latest_subdir(base)
|
||
self.state.set_traces_dir(str(target))
|
||
|
||
def _refresh_traces(self):
|
||
self.state.refresh_traces()
|
||
|
||
def get_selected(self):
|
||
idx = self.lst.curselection()
|
||
return [self.lst.get(i) for i in idx]
|
||
|
||
|
||
# ---------------- Tab 1: Multi-Log Analyse (ranking optional) ----------------
|
||
class TabAnalyze(ttk.Frame):
|
||
def __init__(self, master, state: AppState, header: Header):
|
||
super().__init__(master, padding=10)
|
||
self.state = state
|
||
self.header = header
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(0, weight=1)
|
||
self.rowconfigure(2, weight=1)
|
||
|
||
# params
|
||
params = ttk.LabelFrame(self, text="Analyse-Parameter")
|
||
params.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||
for c in (1,3):
|
||
params.columnconfigure(c, weight=1)
|
||
|
||
ttk.Label(params, text="Include-IDs (z.B. 0x208,0x209):").grid(row=0, column=0, sticky="w")
|
||
self.include_var = tk.StringVar(value="")
|
||
ttk.Entry(params, textvariable=self.include_var).grid(row=0, column=1, sticky="ew", padx=5)
|
||
|
||
ttk.Label(params, text="Exclude-IDs:").grid(row=0, column=2, sticky="w")
|
||
self.exclude_var = tk.StringVar(value="")
|
||
ttk.Entry(params, textvariable=self.exclude_var).grid(row=0, column=3, sticky="ew", padx=5)
|
||
|
||
ttk.Label(params, text="Scale:").grid(row=1, column=0, sticky="w")
|
||
self.scale_var = tk.DoubleVar(value=1.0)
|
||
ttk.Entry(params, textvariable=self.scale_var, width=12).grid(row=1, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Offset:").grid(row=1, column=2, sticky="w")
|
||
self.offset_var = tk.DoubleVar(value=0.0)
|
||
ttk.Entry(params, textvariable=self.offset_var, width=12).grid(row=1, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Range-Min:").grid(row=2, column=0, sticky="w")
|
||
self.rmin_var = tk.StringVar(value="")
|
||
ttk.Entry(params, textvariable=self.rmin_var, width=12).grid(row=2, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Range-Max:").grid(row=2, column=2, sticky="w")
|
||
self.rmax_var = tk.StringVar(value="")
|
||
ttk.Entry(params, textvariable=self.rmax_var, width=12).grid(row=2, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Range-Hit-Ratio (0..1):").grid(row=3, column=0, sticky="w")
|
||
self.hit_ratio_var = tk.DoubleVar(value=0.6)
|
||
ttk.Entry(params, textvariable=self.hit_ratio_var, width=12).grid(row=3, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Top-N (Fallback):").grid(row=3, column=2, sticky="w")
|
||
self.top_var = tk.IntVar(value=20)
|
||
ttk.Entry(params, textvariable=self.top_var, width=12).grid(row=3, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(params, text="Per-ID-Limit:").grid(row=4, column=0, sticky="w")
|
||
self.per_id_limit_var = tk.IntVar(value=2)
|
||
ttk.Entry(params, textvariable=self.per_id_limit_var, width=12).grid(row=4, column=1, sticky="w", padx=5)
|
||
|
||
self.run_separately_var = tk.BooleanVar(value=False)
|
||
ttk.Checkbutton(params, text="Jede Logdatei separat laufen lassen", variable=self.run_separately_var).grid(row=4, column=2, columnspan=2, sticky="w", padx=5)
|
||
|
||
# run + console
|
||
run = ttk.Frame(self)
|
||
run.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||
ttk.Button(run, text="Analyse starten (Ranking)", command=self.on_run).pack(side="left", padx=5)
|
||
|
||
out = ttk.LabelFrame(self, text="Ausgabe")
|
||
out.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
||
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||
self.txt.configure(yscrollcommand=sb.set)
|
||
|
||
def on_run(self):
|
||
logs = self.header.selected_logs()
|
||
if not logs:
|
||
messagebox.showwarning("Hinweis", "Bitte oben im Header Logdateien auswählen.")
|
||
return
|
||
t = threading.Thread(target=self._run_worker, args=(logs,), daemon=True)
|
||
self.txt.delete("1.0", tk.END)
|
||
self._append("Starte Analyse…\n")
|
||
t.start()
|
||
|
||
def _run_worker(self, logs):
|
||
script_path = Path(__file__).parent / SCRIPT_NAME
|
||
if not script_path.exists():
|
||
self._append(f"[Fehler] Script nicht gefunden: {script_path}\n"); return
|
||
|
||
# output root: workdir/analyze_out/<ts>_multilog
|
||
out_root = self.state.analyze_out_root()
|
||
stamp = now_stamp() + "_multilog"
|
||
outdir = ensure_dir(out_root / stamp)
|
||
|
||
def build_args():
|
||
args = [sys.executable, str(script_path)]
|
||
if self.include_var.get().strip():
|
||
args += ["--include-ids", self.include_var.get().strip()]
|
||
if self.exclude_var.get().strip():
|
||
args += ["--exclude-ids", self.exclude_var.get().strip()]
|
||
args += ["--scale", str(self.scale_var.get()), "--offset", str(self.offset_var.get())]
|
||
if self.rmin_var.get().strip(): args += ["--range-min", self.rmin_var.get().strip()]
|
||
if self.rmax_var.get().strip(): args += ["--range-max", self.rmax_var.get().strip()]
|
||
args += ["--range-hit-ratio", str(self.hit_ratio_var.get())]
|
||
args += ["--top", str(self.top_var.get()), "--per-id-limit", str(self.per_id_limit_var.get())]
|
||
return args
|
||
|
||
if self.run_separately_var.get():
|
||
for p in logs:
|
||
sub = ensure_dir(outdir / Path(p).stem)
|
||
cmd = build_args() + ["--outdir", str(sub), p]
|
||
self._run_cmd(cmd)
|
||
else:
|
||
cmd = build_args() + ["--outdir", str(outdir)] + logs
|
||
self._run_cmd(cmd)
|
||
|
||
self._append(f"\nDone. Output: {outdir}\n")
|
||
|
||
def _run_cmd(self, cmd):
|
||
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||
try:
|
||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||
for line in proc.stdout: self._append(line)
|
||
rc = proc.wait()
|
||
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||
except Exception as e:
|
||
self._append(f"[Fehler] {e}\n")
|
||
|
||
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||
|
||
|
||
# ---------------- Tab 2: ID Explorer (split + single-ID analyze) ----------------
|
||
class TabExplorer(ttk.Frame):
|
||
def __init__(self, master, state: AppState, header: Header):
|
||
super().__init__(master, padding=10)
|
||
self.state = state
|
||
self.header = header
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(0, weight=1)
|
||
self.rowconfigure(3, weight=1)
|
||
|
||
# split controls
|
||
frm_split = ttk.LabelFrame(self, text="Split: Logs → per-ID Traces")
|
||
frm_split.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||
frm_split.columnconfigure(1, weight=1)
|
||
|
||
self.rx_only_var = tk.BooleanVar(value=False)
|
||
self.ts_split_var = tk.BooleanVar(value=True)
|
||
|
||
ttk.Label(frm_split, text="Ziel (Workdir/traces[/timestamp])").grid(row=0, column=0, sticky="w", padx=5)
|
||
ttk.Label(frm_split, textvariable=self.state.traces_dir).grid(row=0, column=1, sticky="w")
|
||
ttk.Checkbutton(frm_split, text="nur RX", variable=self.rx_only_var).grid(row=1, column=0, sticky="w", padx=5)
|
||
ttk.Checkbutton(frm_split, text="Zeitstempel-Unterordner", variable=self.ts_split_var).grid(row=1, column=1, sticky="w", padx=5)
|
||
ttk.Button(frm_split, text="Split starten", command=self.on_split).grid(row=1, column=2, sticky="e", padx=5)
|
||
|
||
# unified trace panel (multi-select)
|
||
self.trace_panel = TracePanel(self, self.state, title="Traces im ausgewählten Ordner", single_select=False, height=10)
|
||
self.trace_panel.grid(row=1, column=0, sticky="nsew", padx=5, pady=(8,10))
|
||
|
||
# single-ID analyze
|
||
frm_one = ttk.LabelFrame(self, text="Einzel-ID Analyse (Plots + summary_stats)")
|
||
frm_one.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
||
frm_one.columnconfigure(1, weight=1)
|
||
ttk.Label(frm_one, text="Output-Basis (unter Workdir/analyze_out):").grid(row=0, column=0, sticky="w")
|
||
self.one_out_base = tk.StringVar(value="id_explore")
|
||
ttk.Entry(frm_one, textvariable=self.one_out_base).grid(row=0, column=1, sticky="ew", padx=5)
|
||
self.ts_one = tk.BooleanVar(value=True)
|
||
ttk.Checkbutton(frm_one, text="Zeitstempel-Unterordner", variable=self.ts_one).grid(row=0, column=2, sticky="w", padx=5)
|
||
ttk.Button(frm_one, text="Analyse starten", command=self.on_one_analyze).grid(row=0, column=3, sticky="e", padx=5)
|
||
|
||
# console
|
||
out = ttk.LabelFrame(self, text="Ausgabe")
|
||
out.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||
self.txt.configure(yscrollcommand=sb.set)
|
||
|
||
def on_split(self):
|
||
logs = self.header.selected_logs()
|
||
if not logs:
|
||
messagebox.showwarning("Hinweis", "Bitte oben im Header Logdateien auswählen."); return
|
||
outdir = self.state.traces_base_path()
|
||
if self.ts_split_var.get(): outdir = outdir / now_stamp()
|
||
ensure_dir(outdir)
|
||
cmd = [sys.executable, str(Path(__file__).parent / SPLIT_SCRIPT), "--outdir", str(outdir)]
|
||
if self.rx_only_var.get(): cmd.append("--rx-only")
|
||
cmd += logs
|
||
self._run_cmd(cmd)
|
||
# nach dem Split: globalen Traces-Ordner setzen (neuester Ordner)
|
||
self.state.set_traces_dir(str(outdir))
|
||
|
||
def on_one_analyze(self):
|
||
sel = self.trace_panel.get_selected()
|
||
if not sel:
|
||
messagebox.showwarning("Hinweis", "Bitte mindestens eine .trace auswählen."); return
|
||
out_root = self.state.analyze_out_root()
|
||
stamp = now_stamp() + "_id_explore" if self.ts_one.get() else "id_explore"
|
||
outdir = ensure_dir(out_root / stamp)
|
||
for trace in sel:
|
||
cmd = [sys.executable, str(Path(__file__).parent / EXPLORE_SCRIPT), "--outdir", str(outdir), trace]
|
||
self._run_cmd(cmd)
|
||
self._append(f"\nDone. Output: {outdir}\n")
|
||
|
||
def _run_cmd(self, cmd):
|
||
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||
try:
|
||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||
for line in proc.stdout: self._append(line)
|
||
rc = proc.wait()
|
||
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||
except Exception as e:
|
||
self._append(f"[Fehler] {e}\n")
|
||
|
||
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||
|
||
|
||
# ---------------- Tab 3: Traces Batch-Analyse ----------------
|
||
class TabTraceBatch(ttk.Frame):
|
||
def __init__(self, master, state: AppState, header: Header):
|
||
super().__init__(master, padding=10)
|
||
self.state = state
|
||
self.header = header
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(0, weight=1)
|
||
self.rowconfigure(2, weight=1)
|
||
|
||
# unified trace panel (multi-select)
|
||
self.trace_panel = TracePanel(self, self.state, title="Traces (Ordner/Subset wählen)", single_select=False, height=10)
|
||
self.trace_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5,10))
|
||
|
||
# Params
|
||
pr = ttk.LabelFrame(self, text="Analyse-Parameter")
|
||
pr.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||
for c in (1,3):
|
||
pr.columnconfigure(c, weight=1)
|
||
|
||
self.rx_only = tk.BooleanVar(value=False)
|
||
ttk.Checkbutton(pr, text="nur RX", variable=self.rx_only).grid(row=0, column=0, sticky="w", padx=5)
|
||
|
||
ttk.Label(pr, text="Scale").grid(row=0, column=1, sticky="e")
|
||
self.scale = tk.DoubleVar(value=1.0)
|
||
ttk.Entry(pr, textvariable=self.scale, width=12).grid(row=0, column=2, sticky="w", padx=5)
|
||
|
||
ttk.Label(pr, text="Offset").grid(row=0, column=3, sticky="e")
|
||
self.offset = tk.DoubleVar(value=0.0)
|
||
ttk.Entry(pr, textvariable=self.offset, width=12).grid(row=0, column=4, sticky="w", padx=5)
|
||
|
||
ttk.Label(pr, text="Range-Min").grid(row=1, column=1, sticky="e")
|
||
self.rmin = tk.StringVar(value="")
|
||
ttk.Entry(pr, textvariable=self.rmin, width=12).grid(row=1, column=2, sticky="w", padx=5)
|
||
|
||
ttk.Label(pr, text="Range-Max").grid(row=1, column=3, sticky="e")
|
||
self.rmax = tk.StringVar(value="")
|
||
ttk.Entry(pr, textvariable=self.rmax, width=12).grid(row=1, column=4, sticky="w", padx=5)
|
||
|
||
ttk.Label(pr, text="Top pro Trace").grid(row=2, column=1, sticky="e")
|
||
self.top = tk.IntVar(value=8)
|
||
ttk.Entry(pr, textvariable=self.top, width=12).grid(row=2, column=2, sticky="w", padx=5)
|
||
|
||
self.use_ts = tk.BooleanVar(value=True)
|
||
ttk.Checkbutton(pr, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=2, column=3, sticky="w", padx=5)
|
||
|
||
# Run & console
|
||
run = ttk.Frame(self)
|
||
run.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
|
||
ttk.Button(run, text="Batch starten", command=self.on_run).pack(side="left", padx=5)
|
||
|
||
out = ttk.LabelFrame(self, text="Ausgabe")
|
||
out.grid(row=4, column=0, sticky="nsew", padx=5, pady=5)
|
||
out.columnconfigure(0, weight=1); out.rowconfigure(0, weight=1)
|
||
self.txt = tk.Text(out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
|
||
sb = ttk.Scrollbar(out, orient="vertical", command=self.txt.yview); sb.grid(row=0, column=1, sticky="ns")
|
||
self.txt.configure(yscrollcommand=sb.set)
|
||
|
||
def on_run(self):
|
||
# nutze Auswahl oder – falls leer – kompletten Ordner
|
||
selected = self.trace_panel.get_selected()
|
||
traces_dir = Path(self.state.traces_current_dir.get().strip() or str(self.state.traces_base_path()))
|
||
if not traces_dir.exists():
|
||
messagebox.showwarning("Hinweis", "Bitte gültigen Traces-Ordner wählen."); return
|
||
|
||
out_root = self.state.analyze_out_root()
|
||
label = "trace_batch"
|
||
stamp = now_stamp() + "_" + label if self.use_ts.get() else label
|
||
outdir = ensure_dir(out_root / stamp)
|
||
|
||
# falls Auswahl getroffen wurde, temporären Subset-Ordner bauen
|
||
subset_dir = None
|
||
if selected:
|
||
subset_dir = ensure_dir(outdir / "_subset")
|
||
for p in selected:
|
||
src = Path(p)
|
||
dst = subset_dir / src.name
|
||
try:
|
||
# versuchen Hardlink (schnell, platzsparend)
|
||
if dst.exists():
|
||
dst.unlink()
|
||
os.link(src, dst)
|
||
except Exception:
|
||
# Fallback: Kopieren
|
||
shutil.copy2(src, dst)
|
||
|
||
run_dir = subset_dir if subset_dir else traces_dir
|
||
|
||
cmd = [sys.executable, str(Path(__file__).parent/TRACE_BATCH),
|
||
"--traces-dir", str(run_dir), "--outdir", str(outdir),
|
||
"--scale", str(self.scale.get()), "--offset", str(self.offset.get()),
|
||
"--top", str(self.top.get()), "--plots"]
|
||
if self.rmin.get().strip(): cmd += ["--range-min", self.rmin.get().strip()]
|
||
if self.rmax.get().strip(): cmd += ["--range-max", self.rmax.get().strip()]
|
||
if self.rx_only.get(): cmd.append("--rx-only")
|
||
|
||
self._run_cmd(cmd)
|
||
self._append(f"\nDone. Output: {outdir}\n")
|
||
|
||
def _run_cmd(self, cmd):
|
||
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||
try:
|
||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||
for line in proc.stdout: self._append(line)
|
||
rc = proc.wait()
|
||
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
|
||
except Exception as e:
|
||
self._append(f"[Fehler] {e}\n")
|
||
|
||
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||
|
||
# ---------------- Tab 4: Range-Fit (supervised + unsupervised, mit Physik-Constraints) ----------------
|
||
class TabRangeFit(ttk.Frame):
|
||
def __init__(self, master, state: AppState, header: Header):
|
||
super().__init__(master, padding=10)
|
||
self.state = state
|
||
self.header = header
|
||
self._last_outdir = None
|
||
self._build_ui()
|
||
|
||
def _build_ui(self):
|
||
self.columnconfigure(0, weight=1)
|
||
self.rowconfigure(3, weight=1)
|
||
|
||
# unified trace panel (single-select)
|
||
self.trace_panel = TracePanel(self, self.state, title="Trace wählen (Single)", single_select=True, height=10)
|
||
self.trace_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5,10))
|
||
|
||
# Parameter Frames
|
||
frm_params = ttk.Frame(self)
|
||
frm_params.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||
for c in range(6):
|
||
frm_params.columnconfigure(c, weight=1)
|
||
|
||
# --- Supervised (Range & Physik) ---
|
||
box_sup = ttk.LabelFrame(frm_params, text="Supervised (Range-Fit) – lasse leer für Unsupervised")
|
||
box_sup.grid(row=0, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||
for c in range(6):
|
||
box_sup.columnconfigure(c, weight=1)
|
||
|
||
ttk.Label(box_sup, text="Range-Min").grid(row=0, column=0, sticky="e")
|
||
self.rmin = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.rmin, width=12).grid(row=0, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Range-Max").grid(row=0, column=2, sticky="e")
|
||
self.rmax = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.rmax, width=12).grid(row=0, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Min. Hit-Ratio (0..1)").grid(row=0, column=4, sticky="e")
|
||
self.min_hit = tk.DoubleVar(value=0.5)
|
||
ttk.Entry(box_sup, textvariable=self.min_hit, width=10).grid(row=0, column=5, sticky="w", padx=5)
|
||
|
||
self.allow_neg = tk.BooleanVar(value=False)
|
||
ttk.Checkbutton(box_sup, text="negative Scale erlauben", variable=self.allow_neg).grid(row=1, column=0, columnspan=2, sticky="w")
|
||
|
||
ttk.Label(box_sup, text="Rate-Min (Hz)").grid(row=1, column=2, sticky="e")
|
||
self.rate_min = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.rate_min, width=10).grid(row=1, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Rate-Max (Hz)").grid(row=1, column=4, sticky="e")
|
||
self.rate_max = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.rate_max, width=10).grid(row=1, column=5, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Jitter-Max (ms)").grid(row=2, column=0, sticky="e")
|
||
self.jitter_max = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.jitter_max, width=10).grid(row=2, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Max-Slope-Abs (phys/s)").grid(row=2, column=2, sticky="e")
|
||
self.slope_abs = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.slope_abs, width=12).grid(row=2, column=3, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Max-Slope-Frac (/s)").grid(row=2, column=4, sticky="e")
|
||
self.slope_frac = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.slope_frac, width=12).grid(row=2, column=5, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Slope-Quantile").grid(row=3, column=0, sticky="e")
|
||
self.slope_q = tk.DoubleVar(value=0.95) # 0.95 oder 0.99
|
||
ttk.Entry(box_sup, textvariable=self.slope_q, width=10).grid(row=3, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_sup, text="Min-Unique-Ratio").grid(row=3, column=2, sticky="e")
|
||
self.min_uniq = tk.StringVar(value="")
|
||
ttk.Entry(box_sup, textvariable=self.min_uniq, width=10).grid(row=3, column=3, sticky="w", padx=5)
|
||
|
||
# --- Unsupervised ---
|
||
box_uns = ttk.LabelFrame(frm_params, text="Unsupervised (ohne Range)")
|
||
box_uns.grid(row=1, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||
for c in range(6):
|
||
box_uns.columnconfigure(c, weight=1)
|
||
|
||
ttk.Label(box_uns, text="Min. Smoothness (0..1)").grid(row=0, column=0, sticky="e")
|
||
self.min_smooth = tk.DoubleVar(value=0.2)
|
||
ttk.Entry(box_uns, textvariable=self.min_smooth, width=12).grid(row=0, column=1, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_uns, text="Max-Slope-Frac-RAW (/s)").grid(row=0, column=2, sticky="e")
|
||
self.max_slope_frac_raw = tk.StringVar(value="")
|
||
ttk.Entry(box_uns, textvariable=self.max_slope_frac_raw, width=12).grid(row=0, column=3, sticky="w", padx=5)
|
||
|
||
# --- Allgemein/Output ---
|
||
box_out = ttk.LabelFrame(frm_params, text="Allgemein & Output")
|
||
box_out.grid(row=2, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
|
||
for c in range(6):
|
||
box_out.columnconfigure(c, weight=1)
|
||
|
||
self.rx_only = tk.BooleanVar(value=False)
|
||
ttk.Checkbutton(box_out, text="nur RX", variable=self.rx_only).grid(row=0, column=0, sticky="w")
|
||
|
||
ttk.Label(box_out, text="Plots Top-N").grid(row=0, column=1, sticky="e")
|
||
self.plots_top = tk.IntVar(value=8)
|
||
ttk.Entry(box_out, textvariable=self.plots_top, width=10).grid(row=0, column=2, sticky="w", padx=5)
|
||
|
||
ttk.Label(box_out, text="Output-Label").grid(row=0, column=3, sticky="e")
|
||
self.out_label = tk.StringVar(value="rangefit")
|
||
ttk.Entry(box_out, textvariable=self.out_label, width=18).grid(row=0, column=4, sticky="w", padx=5)
|
||
|
||
self.use_ts = tk.BooleanVar(value=True)
|
||
ttk.Checkbutton(box_out, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=0, column=5, sticky="w")
|
||
|
||
# Start + Konsole + Aktionen
|
||
frm_run = ttk.Frame(self)
|
||
frm_run.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
|
||
ttk.Button(frm_run, text="Start Range-/Unsupervised-Fit", command=self._on_run).pack(side="left", padx=5)
|
||
ttk.Button(frm_run, text="Report öffnen", command=self._open_last_report).pack(side="left", padx=5)
|
||
ttk.Button(frm_run, text="Output-Ordner öffnen", command=self._open_last_outdir).pack(side="left", padx=5)
|
||
|
||
frm_out = ttk.LabelFrame(self, text="Ausgabe")
|
||
frm_out.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
|
||
frm_out.columnconfigure(0, weight=1); frm_out.rowconfigure(0, weight=1)
|
||
self.txt = tk.Text(frm_out, height=14); self.txt.grid(row=0, column=0, sticky="nsew")
|
||
sbo = ttk.Scrollbar(frm_out, orient="vertical", command=self.txt.yview); sbo.grid(row=0, column=1, sticky="ns")
|
||
self.txt.configure(yscrollcommand=sbo.set)
|
||
|
||
# --- helpers ---
|
||
def _append(self, s):
|
||
self.txt.insert(tk.END, s); self.txt.see(tk.END)
|
||
|
||
def _stamp(self):
|
||
import datetime as _dt
|
||
return _dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
||
def _build_outdir(self, supervised: bool) -> Path:
|
||
out_root = self.state.analyze_out_root()
|
||
label = (self.out_label.get().strip() or ("rangefit" if supervised else "unsupervised"))
|
||
stamp = f"{self._stamp()}_{label}" if self.use_ts.get() else label
|
||
outdir = out_root / stamp
|
||
outdir.mkdir(parents=True, exist_ok=True)
|
||
self._last_outdir = outdir
|
||
return outdir
|
||
|
||
def _selected_trace(self):
|
||
sel = self.trace_panel.get_selected()
|
||
if not sel:
|
||
messagebox.showwarning("Hinweis", "Bitte genau eine .trace-Datei auswählen.")
|
||
return None
|
||
if len(sel) != 1:
|
||
messagebox.showwarning("Hinweis", "Range-Fit benötigt genau eine .trace-Datei (Single-Select).")
|
||
return None
|
||
return sel[0]
|
||
|
||
def _maybe(self, val: str, flag: str, args: list):
|
||
v = (val or "").strip()
|
||
if v != "":
|
||
args += [flag, v]
|
||
|
||
def _open_path(self, p: Path):
|
||
try:
|
||
if sys.platform.startswith("darwin"):
|
||
subprocess.Popen(["open", str(p)])
|
||
elif os.name == "nt":
|
||
os.startfile(str(p)) # type: ignore
|
||
else:
|
||
subprocess.Popen(["xdg-open", str(p)])
|
||
except Exception as e:
|
||
messagebox.showwarning("Fehler", f"Konnte nicht öffnen:\n{p}\n{e}")
|
||
|
||
def _open_last_outdir(self):
|
||
if self._last_outdir and self._last_outdir.exists():
|
||
self._open_path(self._last_outdir)
|
||
else:
|
||
messagebox.showinfo("Hinweis", "Noch kein Output-Ordner vorhanden.")
|
||
|
||
def _open_last_report(self):
|
||
if not (self._last_outdir and self._last_outdir.exists()):
|
||
messagebox.showinfo("Hinweis", "Noch kein Report erzeugt.")
|
||
return
|
||
# versuche ein *_report.md im letzten Outdir zu finden
|
||
md = list(Path(self._last_outdir).glob("*_report.md"))
|
||
if not md:
|
||
messagebox.showinfo("Hinweis", "Kein Report gefunden.")
|
||
return
|
||
self._open_path(md[0])
|
||
|
||
def _on_run(self):
|
||
trace = self._selected_trace()
|
||
if not trace:
|
||
return
|
||
|
||
# supervised?
|
||
rmin = self.rmin.get().strip()
|
||
rmax = self.rmax.get().strip()
|
||
supervised = bool(rmin) and bool(rmax)
|
||
|
||
outdir = self._build_outdir(supervised)
|
||
|
||
cmd = [
|
||
sys.executable,
|
||
str(Path(__file__).parent / RANGE_FITTER),
|
||
trace,
|
||
"--outdir", str(outdir),
|
||
"--plots-top", str(self.plots_top.get()),
|
||
]
|
||
if self.rx_only.get():
|
||
cmd.append("--rx-only")
|
||
|
||
if supervised:
|
||
cmd += ["--rmin", rmin, "--rmax", rmax, "--min-hit", str(self.min_hit.get())]
|
||
if self.allow_neg.get():
|
||
cmd.append("--allow-neg-scale")
|
||
self._maybe(self.rate_min.get(), "--rate-min", cmd)
|
||
self._maybe(self.rate_max.get(), "--rate-max", cmd)
|
||
self._maybe(self.jitter_max.get(), "--jitter-max-ms", cmd)
|
||
self._maybe(self.slope_abs.get(), "--max-slope-abs", cmd)
|
||
self._maybe(self.slope_frac.get(), "--max-slope-frac", cmd)
|
||
cmd += ["--slope-quantile", str(self.slope_q.get())]
|
||
self._maybe(self.min_uniq.get(), "--min-uniq-ratio", cmd)
|
||
else:
|
||
# unsupervised
|
||
cmd += ["--min-smooth", str(self.min_smooth.get())]
|
||
self._maybe(self.max_slope_frac_raw.get(), "--max-slope-frac-raw", cmd)
|
||
cmd += ["--slope-quantile", str(self.slope_q.get())] # wird intern für p95/p99 gewählt
|
||
|
||
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
|
||
t = threading.Thread(target=self._run_cmd, args=(cmd,), daemon=True)
|
||
t.start()
|
||
|
||
def _run_cmd(self, cmd):
|
||
try:
|
||
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
|
||
for line in proc.stdout:
|
||
self._append(line)
|
||
rc = proc.wait()
|
||
if rc != 0:
|
||
self._append(f"[Exit-Code {rc}]\n")
|
||
else:
|
||
self._append(f"\nDone. Output: {self._last_outdir}\n")
|
||
except Exception as e:
|
||
self._append(f"[Fehler] {e}\n")
|
||
|
||
# ---------------- App Shell ----------------
|
||
class App(tk.Tk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.title("CAN Universal Signal Finder – GUI")
|
||
self.geometry("1180x860")
|
||
self.configure(padx=8, pady=8)
|
||
|
||
# shared state
|
||
self.state = AppState()
|
||
|
||
# header (always visible)
|
||
self.header = Header(self, self.state)
|
||
self.header.pack(fill="x", side="top")
|
||
|
||
# Tabs
|
||
nb = ttk.Notebook(self)
|
||
nb.pack(fill="both", expand=True)
|
||
|
||
self.tab_analyze = TabAnalyze(nb, self.state, self.header)
|
||
self.tab_explorer = TabExplorer(nb, self.state, self.header)
|
||
self.tab_batch = TabTraceBatch(nb, self.state, self.header)
|
||
self.tab_rangefit = TabRangeFit(nb, self.state, self.header)
|
||
|
||
nb.add(self.tab_analyze, text="Multi-Log Analyse")
|
||
nb.add(self.tab_explorer, text="ID Explorer")
|
||
nb.add(self.tab_batch, text="Traces Batch-Analyse")
|
||
nb.add(self.tab_rangefit, text="Range-Fit")
|
||
|
||
# init: traces auf default/latest stellen
|
||
self.state.set_traces_to_default_or_latest()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = App()
|
||
app.mainloop()
|