SolvePlate
This commit is contained in:
@@ -0,0 +1,974 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Astrometry.net GUI — vollständiges Frontend für solve-field
|
||||
Verbindet sich per SSH (Passwort) mit dem Remote-Server.
|
||||
|
||||
Abhängigkeiten:
|
||||
pip install asyncssh
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||||
import threading
|
||||
import os
|
||||
import shlex
|
||||
import queue
|
||||
import stat
|
||||
|
||||
try:
|
||||
import asyncssh
|
||||
HAS_ASYNCSSH = True
|
||||
except ImportError:
|
||||
HAS_ASYNCSSH = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tooltip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Tooltip:
|
||||
DELAY_MS = 600
|
||||
BG = "#fffde7"
|
||||
FG = "#1a1a1a"
|
||||
FONT = ("Segoe UI", 9)
|
||||
MAX_WIDTH = 420
|
||||
|
||||
def __init__(self, widget: tk.Widget, text: str):
|
||||
self._widget = widget
|
||||
self._text = text
|
||||
self._id = None
|
||||
self._win = None
|
||||
widget.bind("<Enter>", self._schedule, add="+")
|
||||
widget.bind("<Leave>", self._cancel, add="+")
|
||||
widget.bind("<ButtonPress>", self._cancel, add="+")
|
||||
|
||||
def _schedule(self, _=None):
|
||||
self._cancel()
|
||||
self._id = self._widget.after(self.DELAY_MS, self._show)
|
||||
|
||||
def _cancel(self, _=None):
|
||||
if self._id:
|
||||
self._widget.after_cancel(self._id)
|
||||
self._id = None
|
||||
if self._win:
|
||||
self._win.destroy()
|
||||
self._win = None
|
||||
|
||||
def _show(self):
|
||||
if self._win:
|
||||
return
|
||||
x = self._widget.winfo_rootx() + 20
|
||||
y = self._widget.winfo_rooty() + self._widget.winfo_height() + 4
|
||||
self._win = tw = tk.Toplevel(self._widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
tw.attributes("-topmost", True)
|
||||
tk.Label(
|
||||
tw, text=self._text, justify=tk.LEFT,
|
||||
background=self.BG, foreground=self.FG, font=self.FONT,
|
||||
relief=tk.SOLID, borderwidth=1,
|
||||
wraplength=self.MAX_WIDTH, padx=8, pady=6,
|
||||
).pack()
|
||||
|
||||
|
||||
def tip(widget: tk.Widget, text: str) -> tk.Widget:
|
||||
Tooltip(widget, text)
|
||||
return widget
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async SSH-Helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AsyncSSH:
|
||||
"""Wrapper um asyncssh — alle Methoden sind Coroutinen."""
|
||||
|
||||
def __init__(self, host, user, password, port=22):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.port = port
|
||||
self._conn: asyncssh.SSHClientConnection | None = None
|
||||
|
||||
async def connect(self):
|
||||
self._conn = await asyncssh.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=self.user,
|
||||
password=self.password,
|
||||
known_hosts=None,
|
||||
)
|
||||
|
||||
async def exec_stream(self, cmd: str, output_cb):
|
||||
"""Führt cmd aus und ruft output_cb(line) für jede Ausgabezeile auf."""
|
||||
if self._conn is None:
|
||||
await self.connect()
|
||||
async with self._conn.create_process(
|
||||
cmd,
|
||||
stderr=asyncssh.STDOUT,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
) as proc:
|
||||
async for line in proc.stdout:
|
||||
output_cb(line if line.endswith("\n") else line + "\n")
|
||||
output_cb(f"\n[Prozess beendet mit Code {proc.returncode}]\n")
|
||||
return proc.returncode
|
||||
|
||||
async def mkdir_p(self, remote_dir: str):
|
||||
if self._conn is None:
|
||||
await self.connect()
|
||||
async with self._conn.start_sftp_client() as sftp:
|
||||
parts = remote_dir.strip("/").split("/")
|
||||
path = ""
|
||||
for part in parts:
|
||||
path = f"/{path}/{part}" if path else f"/{part}"
|
||||
try:
|
||||
await sftp.stat(path)
|
||||
except asyncssh.SFTPError:
|
||||
await sftp.mkdir(path)
|
||||
|
||||
async def upload(self, local_path: str, remote_path: str, progress_cb=None):
|
||||
if self._conn is None:
|
||||
await self.connect()
|
||||
total = os.path.getsize(local_path)
|
||||
|
||||
def _progress(src, dst, done, _total):
|
||||
if progress_cb:
|
||||
progress_cb(done, _total or total)
|
||||
|
||||
async with self._conn.start_sftp_client() as sftp:
|
||||
await sftp.put(
|
||||
local_path,
|
||||
remote_path,
|
||||
progress_handler=_progress,
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
if self._conn:
|
||||
self._conn.close()
|
||||
await self._conn.wait_closed()
|
||||
self._conn = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Haupt-GUI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AstrometryGUI:
|
||||
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.title("Astrometry.net — solve-field GUI")
|
||||
self.root.geometry("1000x840")
|
||||
self.root.minsize(800, 620)
|
||||
|
||||
self._log_queue: queue.Queue = queue.Queue()
|
||||
|
||||
# Asyncio-Loop läuft in eigenem Daemon-Thread
|
||||
self._loop = asyncio.new_event_loop()
|
||||
threading.Thread(target=self._loop.run_forever, daemon=True, name="asyncio").start()
|
||||
|
||||
self._init_vars()
|
||||
self._build_ui()
|
||||
self._poll_log()
|
||||
|
||||
if not HAS_ASYNCSSH:
|
||||
self._log(
|
||||
"[WARNUNG] asyncssh nicht gefunden.\n"
|
||||
"Bitte installieren: pip install asyncssh\n\n"
|
||||
)
|
||||
|
||||
def _submit(self, coro):
|
||||
"""Coroutine an den Hintergrund-Loop übergeben."""
|
||||
return asyncio.run_coroutine_threadsafe(coro, self._loop)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Variablen
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _init_vars(self):
|
||||
self.v_host = tk.StringVar(value="10.42.44.25")
|
||||
self.v_port = tk.IntVar(value=22)
|
||||
self.v_user = tk.StringVar(value="eskimue")
|
||||
self.v_pass = tk.StringVar(value="Preside")
|
||||
|
||||
self.v_local_file = tk.StringVar()
|
||||
self.v_remote_dir = tk.StringVar(value="/tmp/astrometry")
|
||||
self.v_input_file = tk.StringVar()
|
||||
self.v_input_file.trace_add("write", self._on_input_file_changed)
|
||||
|
||||
self.v_out_prefix = tk.StringVar()
|
||||
self.v_out_dir = tk.StringVar(value="/tmp/astrometry")
|
||||
self.v_new_fits = tk.StringVar()
|
||||
self.v_wcs = tk.StringVar()
|
||||
self.v_corr = tk.StringVar()
|
||||
self.v_rdls = tk.StringVar()
|
||||
self.v_axy = tk.StringVar()
|
||||
self.v_solved = tk.StringVar()
|
||||
self.v_match = tk.StringVar()
|
||||
|
||||
self.v_scale_units = tk.StringVar(value="degwidth")
|
||||
self.v_scale_low = tk.StringVar()
|
||||
self.v_scale_high = tk.StringVar()
|
||||
self.v_scale_est = tk.StringVar()
|
||||
self.v_scale_err = tk.StringVar()
|
||||
|
||||
self.v_ra = tk.StringVar()
|
||||
self.v_dec = tk.StringVar()
|
||||
self.v_radius = tk.StringVar(value="1.0")
|
||||
|
||||
self.v_depth = tk.StringVar()
|
||||
self.v_cpulimit = tk.StringVar(value="300")
|
||||
self.v_downsample = tk.StringVar(value="6")
|
||||
self.v_tweak_order = tk.StringVar(value="2")
|
||||
self.v_odds_solve = tk.StringVar()
|
||||
self.v_odds_reject = tk.StringVar()
|
||||
self.v_odds_tune = tk.StringVar()
|
||||
self.v_objs = tk.StringVar()
|
||||
self.v_pixel_error = tk.StringVar()
|
||||
self.v_parity = tk.StringVar(value="both")
|
||||
|
||||
self.v_overwrite = tk.BooleanVar(value=True)
|
||||
self.v_no_plots = tk.BooleanVar(value=False)
|
||||
self.v_no_tweak = tk.BooleanVar(value=False)
|
||||
self.v_resort = tk.BooleanVar(value=False)
|
||||
self.v_verbose = tk.BooleanVar(value=True)
|
||||
self.v_timestamp = tk.BooleanVar(value=False)
|
||||
self.v_no_del_temp = tk.BooleanVar(value=False)
|
||||
self.v_continue = tk.BooleanVar(value=False)
|
||||
self.v_sextractor = tk.BooleanVar(value=False)
|
||||
self.v_fits_image = tk.BooleanVar(value=False)
|
||||
self.v_no_verify = tk.BooleanVar(value=False)
|
||||
self.v_just_augment = tk.BooleanVar(value=False)
|
||||
self.v_invert = tk.BooleanVar(value=False)
|
||||
|
||||
self.v_extra = tk.StringVar()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# UI aufbauen
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
top = ttk.Frame(self.root, padding=4)
|
||||
top.pack(fill=tk.X)
|
||||
self._build_conn_bar(top)
|
||||
|
||||
nb = ttk.Notebook(self.root)
|
||||
nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
|
||||
|
||||
for title, builder in [
|
||||
("Input / Output", self._tab_io),
|
||||
("Maßstab & Position", self._tab_scale),
|
||||
("Solver-Parameter", self._tab_solver),
|
||||
("Erweitert", self._tab_advanced),
|
||||
("Befehlsvorschau", self._tab_cmd),
|
||||
("⚙ Server Setup", self._tab_setup),
|
||||
]:
|
||||
f = ttk.Frame(nb, padding=6)
|
||||
nb.add(f, text=title)
|
||||
builder(f)
|
||||
|
||||
btn_bar = ttk.Frame(self.root, padding=4)
|
||||
btn_bar.pack(fill=tk.X)
|
||||
|
||||
tip(ttk.Button(btn_bar, text="Vorschau aktualisieren", command=self._refresh_cmd),
|
||||
"Zeigt den aktuell generierten solve-field-Befehl im Tab 'Befehlsvorschau' an."
|
||||
).pack(side=tk.LEFT, padx=4)
|
||||
|
||||
tip(ttk.Button(btn_bar, text="▶ solve-field starten", command=self._run),
|
||||
"Baut den solve-field-Befehl und führt ihn asynchron per SSH auf dem Server aus.\n"
|
||||
"Die Ausgabe erscheint live im Log-Bereich."
|
||||
).pack(side=tk.LEFT, padx=4)
|
||||
|
||||
tip(ttk.Button(btn_bar, text="Abbrechen", command=self._kill),
|
||||
"Bricht die aktuelle asynchrone Operation ab."
|
||||
).pack(side=tk.LEFT, padx=4)
|
||||
|
||||
tip(ttk.Button(btn_bar, text="Ausgabe löschen", command=self._clear_log),
|
||||
"Leert das Log-Fenster."
|
||||
).pack(side=tk.RIGHT, padx=4)
|
||||
|
||||
log_frame = ttk.LabelFrame(self.root, text="Ausgabe / Log", padding=4)
|
||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
|
||||
|
||||
self.log = scrolledtext.ScrolledText(
|
||||
log_frame, font=("Consolas", 9),
|
||||
bg="#1e1e1e", fg="#d4d4d4",
|
||||
insertbackground="white", wrap=tk.WORD, state=tk.DISABLED,
|
||||
)
|
||||
self.log.pack(fill=tk.BOTH, expand=True)
|
||||
self.log.tag_config("error", foreground="#f48771")
|
||||
self.log.tag_config("ok", foreground="#89d185")
|
||||
self.log.tag_config("info", foreground="#9cdcfe")
|
||||
|
||||
self._current_task: asyncio.Future | None = None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Verbindungs-Leiste
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _build_conn_bar(self, parent):
|
||||
lf = ttk.LabelFrame(parent, text="SSH-Verbindung", padding=4)
|
||||
lf.pack(fill=tk.X, pady=(0, 4))
|
||||
|
||||
for i, (lbl, var, w, tooltip) in enumerate([
|
||||
("Host:", self.v_host, 16, "IP-Adresse oder Hostname des Servers."),
|
||||
("Port:", self.v_port, 6, "SSH-Port. Standard: 22."),
|
||||
("Benutzer:", self.v_user, 12, "SSH-Benutzername auf dem Remote-Server."),
|
||||
("Passwort:", self.v_pass, 14, "SSH-Passwort. Wird nur im Speicher gehalten."),
|
||||
]):
|
||||
ttk.Label(lf, text=lbl).grid(row=0, column=i * 2, padx=(6, 2), sticky=tk.W)
|
||||
e = ttk.Entry(lf, textvariable=var, width=w, show="*" if "Passwort" in lbl else "")
|
||||
e.grid(row=0, column=i * 2 + 1, padx=(0, 8))
|
||||
tip(e, tooltip)
|
||||
|
||||
tip(ttk.Button(lf, text="Verbinden & testen", command=self._test_conn),
|
||||
"Öffnet eine asynchrone SSH-Verbindung und prüft ob solve-field verfügbar ist."
|
||||
).grid(row=0, column=8, padx=4)
|
||||
|
||||
self.conn_label = ttk.Label(lf, text="● nicht verbunden", foreground="gray")
|
||||
self.conn_label.grid(row=0, column=9, padx=6)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Input / Output
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_io(self, parent):
|
||||
uf = ttk.LabelFrame(parent, text="Lokale Datei hochladen (SFTP)", padding=6)
|
||||
uf.pack(fill=tk.X, pady=(0, 8))
|
||||
uf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(uf, text="Lokale Datei:").grid(row=0, column=0, sticky=tk.W, padx=4, pady=3)
|
||||
tip(ttk.Entry(uf, textvariable=self.v_local_file),
|
||||
"Pfad zur lokalen Bild-Datei (FITS, JPEG, PNG, TIFF).\n"
|
||||
"FITS ist das bevorzugte Format da es Metadaten enthält."
|
||||
).grid(row=0, column=1, sticky=tk.EW, padx=4, pady=3)
|
||||
tip(ttk.Button(uf, text="Durchsuchen…", command=self._browse_local),
|
||||
"Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen."
|
||||
).grid(row=0, column=2, padx=4)
|
||||
|
||||
ttk.Label(uf, text="Remote-Verzeichnis:").grid(row=1, column=0, sticky=tk.W, padx=4, pady=3)
|
||||
tip(ttk.Entry(uf, textvariable=self.v_remote_dir),
|
||||
"Verzeichnis auf dem Server wohin die Datei hochgeladen wird.\n"
|
||||
"Wird automatisch angelegt falls es nicht existiert."
|
||||
).grid(row=1, column=1, sticky=tk.EW, padx=4, pady=3)
|
||||
|
||||
self.upload_btn = tip(ttk.Button(uf, text="Hochladen ↑", command=self._upload),
|
||||
"Überträgt die lokale Datei asynchron per SFTP auf den Server.\n"
|
||||
"Nach dem Upload wird der Remote-Pfad automatisch eingetragen.")
|
||||
self.upload_btn.grid(row=1, column=2, padx=4)
|
||||
|
||||
self.upload_progress = ttk.Progressbar(uf, mode="determinate", maximum=100)
|
||||
self.upload_progress.grid(row=2, column=0, columnspan=3, sticky=tk.EW, padx=4, pady=4)
|
||||
tip(self.upload_progress, "Fortschritt des Datei-Uploads in Prozent.")
|
||||
|
||||
rf = ttk.LabelFrame(parent, text="Eingabedatei auf Remote-Rechner", padding=6)
|
||||
rf.pack(fill=tk.X, pady=(0, 8))
|
||||
rf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(rf, text="Dateipfad (remote):").grid(row=0, column=0, sticky=tk.W, padx=4, pady=3)
|
||||
tip(ttk.Entry(rf, textvariable=self.v_input_file),
|
||||
"Absoluter Pfad zur Eingabedatei auf dem Server.\n"
|
||||
"Wildcards erlaubt, z.B. /data/*.fits"
|
||||
).grid(row=0, column=1, sticky=tk.EW, padx=4, pady=3)
|
||||
tip(ttk.Label(rf, text="(FITS, JPEG, PNG, TIFF – Wildcards erlaubt)"),
|
||||
"Wildcards wie /pfad/*.fits verarbeiten alle passenden Dateien nacheinander."
|
||||
).grid(row=0, column=2, padx=4)
|
||||
|
||||
of = ttk.LabelFrame(parent, text="Ausgabe-Optionen", padding=6)
|
||||
of.pack(fill=tk.X)
|
||||
of.columnconfigure(1, weight=1)
|
||||
of.columnconfigure(3, weight=1)
|
||||
|
||||
for i, (lbl, var, tooltip) in enumerate([
|
||||
("Ausgabe-Präfix (--out, -o):", self.v_out_prefix,
|
||||
"Präfix für alle Ausgabedateien.\nBeispiel: 'mein_bild' → mein_bild.wcs, mein_bild.corr …"),
|
||||
("Ausgabe-Verzeichnis (--dir):", self.v_out_dir,
|
||||
"Verzeichnis für alle Ausgabedateien.\nStandard: gleiches Verzeichnis wie Eingabedatei."),
|
||||
("Neues FITS (--new-fits):", self.v_new_fits,
|
||||
"Ausgabe-FITS-Datei mit eingebetteten WCS-Headern.\nWird automatisch aus Eingabedateiname gesetzt."),
|
||||
("WCS-Datei (--wcs):", self.v_wcs,
|
||||
"FITS-Datei mit den berechneten Koordinaten-Transformationen.\nEnthält RA/Dek-Bezug für jeden Pixel."),
|
||||
("Korrespondenzen (--corr):", self.v_corr,
|
||||
"Zuordnungen zwischen Bild-Sternen und Referenz-Katalog.\nMit 'none' deaktivieren."),
|
||||
("RDLS-Datei (--rdls):", self.v_rdls,
|
||||
"FITS-Tabelle mit RA/Dek-Koordinaten aller erkannten Sterne.\nMit 'none' deaktivieren."),
|
||||
("AXY-Datei (--axy):", self.v_axy,
|
||||
"x/y-Pixelkoordinaten der erkannten Sterne.\nZwischenergebnis der Quellerkennung."),
|
||||
("Solved-Datei (--solved):", self.v_solved,
|
||||
"Leere Datei die nach erfolgreicher Lösung angelegt wird.\nNützlich als Signal für Skripte."),
|
||||
("Match-Datei (--match):", self.v_match,
|
||||
"Details zum gefundenen Treffer: Index-Datei, Quad-Match, Transformation."),
|
||||
]):
|
||||
row, col = divmod(i, 2)
|
||||
ttk.Label(of, text=lbl).grid(row=row, column=col * 2, sticky=tk.W, padx=4, pady=2)
|
||||
tip(ttk.Entry(of, textvariable=var),
|
||||
tooltip
|
||||
).grid(row=row, column=col * 2 + 1, sticky=tk.EW, padx=(0, 16), pady=2)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Maßstab & Position
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_scale(self, parent):
|
||||
sf = ttk.LabelFrame(parent, text="Bildmaßstab", padding=6)
|
||||
sf.pack(fill=tk.X, pady=(0, 8))
|
||||
sf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(sf, text="Maßstab-Einheit (--scale-units):").grid(row=0, column=0, sticky=tk.W, padx=4, pady=3)
|
||||
units_cb = ttk.Combobox(sf, textvariable=self.v_scale_units, state="readonly", width=18)
|
||||
units_cb["values"] = ["degwidth", "arcminwidth", "arcsecperpix", "focalmm"]
|
||||
units_cb.grid(row=0, column=1, sticky=tk.W, padx=4, pady=3)
|
||||
tip(units_cb,
|
||||
"Einheit für --scale-low / --scale-high:\n\n"
|
||||
"• degwidth — Breite des Bildfeldes in Grad\n"
|
||||
"• arcminwidth — Breite in Bogenminuten\n"
|
||||
"• arcsecperpix — Bogensekunden pro Pixel (Pixelskala)\n"
|
||||
"• focalmm — Brennweite in Millimeter")
|
||||
|
||||
for i, (lbl, var, tooltip) in enumerate([
|
||||
("Maßstab min (--scale-low):", self.v_scale_low,
|
||||
"Untergrenze des erwarteten Bildmaßstabs.\n"
|
||||
"Je enger der Bereich, desto schneller die Lösung.\n\n"
|
||||
"Beispiele (degwidth): Weitwinkel: 40 | Normalobjektiv: 10 | Teleskop: 0.5"),
|
||||
("Maßstab max (--scale-high):", self.v_scale_high,
|
||||
"Obergrenze des erwarteten Bildmaßstabs.\n"
|
||||
"Ohne Angabe wird der gesamte verfügbare Bereich durchsucht (langsam).\n"
|
||||
"Tipp: ±10–20% um den erwarteten Wert."),
|
||||
("Maßstab-Schätzung (--scale):", self.v_scale_est,
|
||||
"Einzelner Schätzwert als Alternative zu min/max.\n"
|
||||
"Wird mit --scale-err ±% als Suchbereich verwendet."),
|
||||
("Maßstab-Fehler % (--scale-err):", self.v_scale_err,
|
||||
"Toleranz für die Maßstab-Schätzung in Prozent.\n"
|
||||
"Beispiel: Schätzung=5, Fehler=10 → sucht zwischen 4.5 und 5.5 Grad.\n"
|
||||
"Standard: 25%."),
|
||||
], start=1):
|
||||
ttk.Label(sf, text=lbl).grid(row=i, column=0, sticky=tk.W, padx=4, pady=2)
|
||||
tip(ttk.Entry(sf, textvariable=var, width=14),
|
||||
tooltip
|
||||
).grid(row=i, column=1, sticky=tk.W, padx=4, pady=2)
|
||||
|
||||
pf = ttk.LabelFrame(parent, text="Himmelsposition (optional — beschleunigt die Suche erheblich)", padding=6)
|
||||
pf.pack(fill=tk.X)
|
||||
pf.columnconfigure(1, weight=1)
|
||||
|
||||
for i, (lbl, var, tooltip) in enumerate([
|
||||
("RA (--ra):", self.v_ra,
|
||||
"Rektaszension des Bildzentrums.\n"
|
||||
"Format: Dezimalgrad (83.8221) oder HH:MM:SS (05:35:17)\n\n"
|
||||
"Zusammen mit --dec und --radius wird nur ein Himmelsausschnitt durchsucht.\n"
|
||||
"Ohne Angabe: gesamter Himmel (deutlich langsamer)."),
|
||||
("Dek (--dec):", self.v_dec,
|
||||
"Deklination des Bildzentrums.\n"
|
||||
"Format: Dezimalgrad (-5.3911) oder ±DD:MM:SS (-05:23:28)\n"
|
||||
"Positiv = nördliche, negativ = südliche Hemisphäre."),
|
||||
("Suchradius (--radius, Grad):", self.v_radius,
|
||||
"Radius des Suchgebiets um RA/Dek in Grad.\n\n"
|
||||
"Klein (0.5°) → schnell, erfordert genaue RA/Dek-Angabe.\n"
|
||||
"Groß (5°) → toleranter gegenüber Ungenauigkeiten.\n"
|
||||
"Standard: 1.0°"),
|
||||
]):
|
||||
ttk.Label(pf, text=lbl).grid(row=i, column=0, sticky=tk.W, padx=4, pady=3)
|
||||
tip(ttk.Entry(pf, textvariable=var, width=22),
|
||||
tooltip
|
||||
).grid(row=i, column=1, sticky=tk.W, padx=4, pady=3)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Solver-Parameter
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_solver(self, parent):
|
||||
nf = ttk.LabelFrame(parent, text="Numerische Parameter", padding=6)
|
||||
nf.pack(fill=tk.X, pady=(0, 8))
|
||||
nf.columnconfigure(1, weight=1)
|
||||
nf.columnconfigure(3, weight=1)
|
||||
|
||||
for i, (lbl, var, tooltip) in enumerate([
|
||||
("Tiefe (--depth):", self.v_depth,
|
||||
"Anzahl der hellsten Sterne pro Lösungsversuch.\n"
|
||||
"Format: einzelner Wert (30) oder Liste (10,20,30,50).\n"
|
||||
"Mehr Sterne → genauer aber langsamer."),
|
||||
("CPU-Limit (--cpulimit, s):", self.v_cpulimit,
|
||||
"Maximale Rechenzeit in Sekunden.\n"
|
||||
"300s ist ein guter Kompromiss. 0 = kein Limit."),
|
||||
("Downsampling (--downsample):", self.v_downsample,
|
||||
"Verkleinert das Bild vor der Verarbeitung.\n"
|
||||
"6 = 1/6 der Auflösung. Beschleunigt die Quellerkennung stark.\n"
|
||||
"Kein Einfluss auf die Genauigkeit der WCS-Lösung."),
|
||||
("Tweak-Ordnung (--tweak-order):", self.v_tweak_order,
|
||||
"Grad des Polynoms für die WCS-Verfeinerung.\n"
|
||||
"1=linear, 2=quadratisch (Standard), 3-4 für Weitwinkelobjektive."),
|
||||
("Odds to solve (--odds-to-solve):", self.v_odds_solve,
|
||||
"Bayes'sche Schwelle ab der eine Lösung als gefunden gilt.\n"
|
||||
"Standard ~1e9. Für schwierige Bilder: 1e6 versuchen."),
|
||||
("Odds to reject (--odds-to-reject):", self.v_odds_reject,
|
||||
"Schwelle unterhalb derer ein Kandidat sofort verworfen wird.\n"
|
||||
"Standard: 1e-100."),
|
||||
("Odds to tune up (--odds-to-tune-up):", self.v_odds_tune,
|
||||
"Schwelle ab der eine genauere Verifikation (Tweak) gestartet wird.\n"
|
||||
"Standard: 1e6."),
|
||||
("Max. Objekte (--objs):", self.v_objs,
|
||||
"Maximale Sternanzahl aus dem Quellenverzeichnis.\n"
|
||||
"Standard: alle gefundenen Objekte."),
|
||||
("Pixelfehler (--pixel-error):", self.v_pixel_error,
|
||||
"Maximale Abweichung in Pixeln bei der Stern-Katalog-Zuordnung.\n"
|
||||
"Standard: 1.0 Pixel."),
|
||||
]):
|
||||
row, col = divmod(i, 2)
|
||||
ttk.Label(nf, text=lbl).grid(row=row, column=col * 2, sticky=tk.W, padx=4, pady=2)
|
||||
tip(ttk.Entry(nf, textvariable=var, width=16),
|
||||
tooltip
|
||||
).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=(0, 4), pady=2)
|
||||
|
||||
ff = ttk.LabelFrame(parent, text="Schalter (Flags)", padding=6)
|
||||
ff.pack(fill=tk.X, pady=(0, 8))
|
||||
|
||||
for i, (lbl, var, tooltip) in enumerate([
|
||||
("Überschreiben (-O / --overwrite)", self.v_overwrite,
|
||||
"Vorhandene Ausgabedateien ohne Nachfrage überschreiben.\n"
|
||||
"Empfohlen: aktiviert."),
|
||||
("Keine Plots (--no-plots)", self.v_no_plots,
|
||||
"Erzeugt keine Überlagerungsbilder (Annotierungen, Sternpositionen).\n"
|
||||
"Beschleunigt die Verarbeitung wenn nur WCS-Daten benötigt werden."),
|
||||
("Kein Tweak (--no-tweak)", self.v_no_tweak,
|
||||
"Deaktiviert die WCS-Verfeinerung. Nur einfache TAN-Projektion.\n"
|
||||
"Schneller, aber weniger genau bei verzerrenden Optiken."),
|
||||
("Sterne umsortieren (--resort)", self.v_resort,
|
||||
"Sortiert die erkannten Sterne neu nach Helligkeit."),
|
||||
("Ausführlich (--verbose)", self.v_verbose,
|
||||
"Detaillierte Fortschrittsinformationen.\n"
|
||||
"Sehr nützlich zur Fehlersuche wenn keine Lösung gefunden wird."),
|
||||
("Zeitstempel (--timestamp)", self.v_timestamp,
|
||||
"Fügt jedem Log-Eintrag einen Zeitstempel hinzu."),
|
||||
("Temp-Dateien behalten (--no-delete-temp)", self.v_no_del_temp,
|
||||
"Temporäre Zwischendateien nach der Verarbeitung behalten.\n"
|
||||
"Nützlich zur Fehlersuche."),
|
||||
("Weitermachen (--continue)", self.v_continue,
|
||||
"Überspringt bereits gelöste Bilder (prüft ob .solved-Datei existiert).\n"
|
||||
"Ideal für unterbrochene Batch-Verarbeitungen."),
|
||||
("SExtractor nutzen (--use-sextractor)", self.v_sextractor,
|
||||
"Verwendet SExtractor statt des eingebauten Quellenerkennungs-Algorithmus.\n"
|
||||
"Muss auf dem Server installiert sein. Oft besser bei schwierigen Bildern."),
|
||||
("Als FITS-Bild behandeln (--fits-image)", self.v_fits_image,
|
||||
"Erzwingt FITS-Behandlung auch wenn die Endung nicht .fits lautet."),
|
||||
("Keine Verifikation (--no-verify)", self.v_no_verify,
|
||||
"Überspringt den Verifikationsschritt. Schneller aber weniger zuverlässig."),
|
||||
("Nur augmentieren (--just-augment)", self.v_just_augment,
|
||||
"Führt nur die Quellerkennung durch, startet nicht den Solver."),
|
||||
("Bild invertieren (--invert)", self.v_invert,
|
||||
"Invertiert das Bild vor der Verarbeitung.\n"
|
||||
"Verwenden wenn dunkle Sterne auf hellem Hintergrund vorliegen."),
|
||||
]):
|
||||
row, col = divmod(i, 3)
|
||||
tip(ttk.Checkbutton(ff, text=lbl, variable=var),
|
||||
tooltip
|
||||
).grid(row=row, column=col, sticky=tk.W, padx=12, pady=2)
|
||||
|
||||
pf = ttk.LabelFrame(parent, text="Parität (--parity)", padding=6)
|
||||
pf.pack(fill=tk.X)
|
||||
|
||||
for lbl, val, tooltip in [
|
||||
("Beide (automatisch)", "both",
|
||||
"Testet beide Paritäten. Langsamer, aber funktioniert unabhängig\n"
|
||||
"ob das Bild gespiegelt ist. Standard."),
|
||||
("Positiv", "pos",
|
||||
"Nur positive Parität (nicht gespiegelt).\n"
|
||||
"Für Aufnahmen direkt am Fokus ohne Umlenkspiegel."),
|
||||
("Negativ", "neg",
|
||||
"Nur negative Parität (gespiegelt).\n"
|
||||
"Für Newton-Teleskop, Cassegrain oder andere spiegelnde Designs."),
|
||||
]:
|
||||
tip(ttk.Radiobutton(pf, text=lbl, variable=self.v_parity, value=val),
|
||||
tooltip
|
||||
).pack(side=tk.LEFT, padx=16)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Erweitert
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_advanced(self, parent):
|
||||
idxf = ttk.LabelFrame(parent, text="Index-Dateien (--index / -i) — eine pro Zeile", padding=6)
|
||||
idxf.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
|
||||
|
||||
self.idx_text = scrolledtext.ScrolledText(idxf, height=6, font=("Consolas", 9))
|
||||
self.idx_text.pack(fill=tk.BOTH, expand=True)
|
||||
tip(self.idx_text,
|
||||
"Index-Dateien die solve-field verwenden soll — eine pro Zeile.\n\n"
|
||||
"Namensschema:\n"
|
||||
" index-42xx: Felder von 2° bis 30° Breite\n"
|
||||
" index-41xx: Felder von 0.3° bis 8° Breite\n"
|
||||
" index-40xx: Felder von 10' bis 40' Breite\n\n"
|
||||
"Leer lassen → serverseitige Konfiguration aus astrometry.cfg wird genutzt.")
|
||||
|
||||
ttk.Label(idxf,
|
||||
text="Leer lassen → serverseitige Standard-Index-Dateien aus astrometry.cfg.",
|
||||
foreground="gray",
|
||||
).pack(anchor=tk.W, padx=4)
|
||||
|
||||
extraf = ttk.LabelFrame(parent, text="Zusätzliche Argumente", padding=6)
|
||||
extraf.pack(fill=tk.X)
|
||||
extraf.columnconfigure(1, weight=1)
|
||||
|
||||
ttk.Label(extraf, text="Weitere Flags:").grid(row=0, column=0, padx=4, pady=4)
|
||||
tip(ttk.Entry(extraf, textvariable=self.v_extra),
|
||||
"Beliebige zusätzliche Argumente die direkt ans Ende des Befehls angehängt werden.\n\n"
|
||||
"Beispiele:\n"
|
||||
" --sigma 5 — Erkennungsschwelle\n"
|
||||
" --uniformize 0 — Gleichmäßige Verteilung deaktivieren\n"
|
||||
" --no-background-subtraction — Hintergrundsubtraktion deaktivieren\n"
|
||||
" --crpix-center — Referenzpixel in Bildmitte"
|
||||
).grid(row=0, column=1, sticky=tk.EW, padx=4, pady=4)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Befehlsvorschau
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_cmd(self, parent):
|
||||
tip(ttk.Label(parent, text="Generierter Befehl (direkt bearbeitbar):"),
|
||||
"Dieser Befehl wird exakt so per SSH auf dem Server ausgeführt.\n"
|
||||
"Kann hier manuell angepasst werden."
|
||||
).pack(anchor=tk.W, pady=(0, 4))
|
||||
|
||||
self.cmd_text = scrolledtext.ScrolledText(parent, height=8, font=("Consolas", 9))
|
||||
self.cmd_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
btn_row = ttk.Frame(parent)
|
||||
btn_row.pack(fill=tk.X, pady=4)
|
||||
|
||||
tip(ttk.Button(btn_row, text="Aktualisieren", command=self._refresh_cmd),
|
||||
"Generiert den Befehl neu aus den aktuellen Formularfeldern."
|
||||
).pack(side=tk.LEFT, padx=4)
|
||||
tip(ttk.Button(btn_row, text="In Zwischenablage", command=self._copy_cmd),
|
||||
"Kopiert den Befehl in die Windows-Zwischenablage."
|
||||
).pack(side=tk.LEFT, padx=4)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Tab: Server Setup
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _tab_setup(self, parent):
|
||||
# SExtractor
|
||||
sf = ttk.LabelFrame(parent, text="SExtractor", padding=6)
|
||||
sf.pack(fill=tk.X, pady=(0, 8))
|
||||
|
||||
ttk.Label(sf, text="Source Extractor (sex) ist ein leistungsstarker Quellenerkennungs-\n"
|
||||
"algorithmus, der bei schwierigen Bildern bessere Ergebnisse liefert\n"
|
||||
"als der eingebaute image2xy. Aktiviere ihn im Tab 'Solver-Parameter'.",
|
||||
justify=tk.LEFT, foreground="gray").pack(anchor=tk.W, padx=4, pady=(0, 6))
|
||||
|
||||
btn_row = ttk.Frame(sf)
|
||||
btn_row.pack(fill=tk.X)
|
||||
|
||||
tip(ttk.Button(btn_row, text="SExtractor prüfen", command=self._check_sextractor),
|
||||
"Prüft ob SExtractor auf dem Server installiert ist und gibt die Version aus."
|
||||
).pack(side=tk.LEFT, padx=4, pady=4)
|
||||
|
||||
tip(ttk.Button(btn_row, text="SExtractor installieren (apt)", command=self._install_sextractor),
|
||||
"Installiert SExtractor auf dem Server via:\n"
|
||||
" sudo apt-get install -y source-extractor\n\n"
|
||||
"Benötigt sudo-Rechte auf dem Server.\n"
|
||||
"Nach der Installation im Tab 'Solver-Parameter' den Haken bei\n"
|
||||
"'SExtractor nutzen' setzen."
|
||||
).pack(side=tk.LEFT, padx=4, pady=4)
|
||||
|
||||
# Diagnose
|
||||
df = ttk.LabelFrame(parent, text="Server-Diagnose", padding=6)
|
||||
df.pack(fill=tk.X, pady=(0, 8))
|
||||
|
||||
tip(ttk.Button(df, text="Server-Status prüfen", command=self._diagnose_server),
|
||||
"Prüft auf dem Server:\n"
|
||||
" • solve-field Version\n"
|
||||
" • astrometry.cfg Inhalt\n"
|
||||
" • Installierte Index-Dateien\n"
|
||||
" • SExtractor verfügbar?\n"
|
||||
" • Freier Speicherplatz"
|
||||
).pack(side=tk.LEFT, padx=4, pady=4)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Befehl bauen
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _build_command(self) -> str:
|
||||
def q(s):
|
||||
return shlex.quote(s) if s else ""
|
||||
|
||||
parts = ["solve-field"]
|
||||
|
||||
def add(flag, var, quote=True):
|
||||
val = var.get() if hasattr(var, "get") else var
|
||||
if val:
|
||||
parts.append(flag)
|
||||
if quote:
|
||||
parts.append(q(str(val)))
|
||||
|
||||
def flag(f, v):
|
||||
if v.get():
|
||||
parts.append(f)
|
||||
|
||||
flag("--overwrite", self.v_overwrite)
|
||||
flag("--no-plots", self.v_no_plots)
|
||||
flag("--verbose", self.v_verbose)
|
||||
flag("--timestamp", self.v_timestamp)
|
||||
flag("--resort", self.v_resort)
|
||||
flag("--continue", self.v_continue)
|
||||
flag("--no-delete-temp", self.v_no_del_temp)
|
||||
flag("--use-sextractor", self.v_sextractor)
|
||||
flag("--fits-image", self.v_fits_image)
|
||||
flag("--no-verify", self.v_no_verify)
|
||||
flag("--just-augment", self.v_just_augment)
|
||||
flag("--invert", self.v_invert)
|
||||
|
||||
if self.v_no_tweak.get():
|
||||
parts.append("--no-tweak")
|
||||
elif self.v_tweak_order.get():
|
||||
parts += ["--tweak-order", self.v_tweak_order.get()]
|
||||
|
||||
if self.v_parity.get() != "both":
|
||||
parts += ["--parity", self.v_parity.get()]
|
||||
|
||||
add("--out", self.v_out_prefix)
|
||||
add("--dir", self.v_out_dir)
|
||||
add("--new-fits", self.v_new_fits)
|
||||
add("--wcs", self.v_wcs)
|
||||
add("--corr", self.v_corr)
|
||||
add("--rdls", self.v_rdls)
|
||||
add("--axy", self.v_axy)
|
||||
add("--solved", self.v_solved)
|
||||
add("--match", self.v_match)
|
||||
|
||||
s_low, s_high = self.v_scale_low.get(), self.v_scale_high.get()
|
||||
if s_low or s_high:
|
||||
parts += ["--scale-units", self.v_scale_units.get()]
|
||||
if s_low: parts += ["--scale-low", s_low]
|
||||
if s_high: parts += ["--scale-high", s_high]
|
||||
add("--scale", self.v_scale_est)
|
||||
add("--scale-err", self.v_scale_err)
|
||||
|
||||
if self.v_ra.get():
|
||||
parts += ["--ra", self.v_ra.get()]
|
||||
if self.v_dec.get():
|
||||
parts += ["--dec", self.v_dec.get()]
|
||||
if self.v_radius.get() and (self.v_ra.get() or self.v_dec.get()):
|
||||
parts += ["--radius", self.v_radius.get()]
|
||||
|
||||
add("--depth", self.v_depth)
|
||||
add("--cpulimit", self.v_cpulimit)
|
||||
add("--downsample", self.v_downsample)
|
||||
add("--odds-to-solve", self.v_odds_solve)
|
||||
add("--odds-to-reject", self.v_odds_reject)
|
||||
add("--odds-to-tune-up", self.v_odds_tune)
|
||||
add("--objs", self.v_objs)
|
||||
add("--pixel-error", self.v_pixel_error)
|
||||
|
||||
for line in self.idx_text.get("1.0", tk.END).strip().splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
parts += ["--index", q(line)]
|
||||
|
||||
if self.v_extra.get():
|
||||
parts.append(self.v_extra.get())
|
||||
|
||||
if self.v_input_file.get():
|
||||
parts.append(q(self.v_input_file.get()))
|
||||
|
||||
return " ".join(parts)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Async-Aktionen
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _make_ssh(self) -> AsyncSSH:
|
||||
return AsyncSSH(
|
||||
self.v_host.get(),
|
||||
self.v_user.get(),
|
||||
self.v_pass.get(),
|
||||
self.v_port.get(),
|
||||
)
|
||||
|
||||
def _test_conn(self):
|
||||
if not HAS_ASYNCSSH:
|
||||
messagebox.showerror("Fehler", "asyncssh nicht installiert.\npip install asyncssh")
|
||||
return
|
||||
self.conn_label.config(text="● verbinde …", foreground="orange")
|
||||
self._log("[INFO] Verbindungstest …\n", "info")
|
||||
self._submit(self._async_test_conn())
|
||||
|
||||
async def _async_test_conn(self):
|
||||
try:
|
||||
ssh = self._make_ssh()
|
||||
await ssh.connect()
|
||||
self._log("[OK] Verbunden.\n", "ok")
|
||||
await ssh.exec_stream(
|
||||
"echo '--- System ---'; uname -a; "
|
||||
"echo '--- solve-field ---'; "
|
||||
"which solve-field && solve-field --help 2>&1 | head -40 "
|
||||
"|| echo 'solve-field nicht im PATH'",
|
||||
self._log,
|
||||
)
|
||||
await ssh.close()
|
||||
self.root.after(0, lambda: self.conn_label.config(text="● verbunden", foreground="#89d185"))
|
||||
except Exception as exc:
|
||||
self._log(f"[FEHLER] {exc}\n", "error")
|
||||
self.root.after(0, lambda: self.conn_label.config(text="● Fehler", foreground="#f48771"))
|
||||
|
||||
def _upload(self):
|
||||
local = self.v_local_file.get()
|
||||
if not local or not os.path.isfile(local):
|
||||
messagebox.showwarning("Datei fehlt", "Bitte eine lokale Datei auswählen.")
|
||||
return
|
||||
self.upload_btn.config(state=tk.DISABLED)
|
||||
self.upload_progress["value"] = 0
|
||||
self._submit(self._async_upload(local))
|
||||
|
||||
async def _async_upload(self, local: str):
|
||||
remote_dir = self.v_remote_dir.get()
|
||||
remote_path = remote_dir.rstrip("/") + "/" + os.path.basename(local)
|
||||
self._log(f"[INFO] Lade hoch: {local} → {remote_path}\n", "info")
|
||||
try:
|
||||
ssh = self._make_ssh()
|
||||
await ssh.connect()
|
||||
await ssh.mkdir_p(remote_dir)
|
||||
|
||||
def _progress(done, total):
|
||||
pct = int(done / total * 100) if total else 0
|
||||
self.root.after(0, lambda: self.upload_progress.config(value=pct))
|
||||
|
||||
await ssh.upload(local, remote_path, _progress)
|
||||
await ssh.close()
|
||||
self._log(f"[OK] Hochladen abgeschlossen: {remote_path}\n", "ok")
|
||||
self.root.after(0, lambda: self.v_input_file.set(remote_path))
|
||||
except Exception as exc:
|
||||
self._log(f"[FEHLER] Upload: {exc}\n", "error")
|
||||
finally:
|
||||
self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL))
|
||||
|
||||
def _run(self):
|
||||
if not self.v_input_file.get():
|
||||
messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.")
|
||||
return
|
||||
cmd = self._build_command()
|
||||
self._log(f"\n{'='*60}\n{cmd}\n{'='*60}\n", "info")
|
||||
self._current_task = self._submit(self._async_run(cmd))
|
||||
|
||||
async def _async_run(self, cmd: str):
|
||||
try:
|
||||
ssh = self._make_ssh()
|
||||
await ssh.connect()
|
||||
await ssh.exec_stream(cmd, self._log)
|
||||
await ssh.close()
|
||||
except Exception as exc:
|
||||
self._log(f"[FEHLER] {exc}\n", "error")
|
||||
|
||||
def _kill(self):
|
||||
if self._current_task:
|
||||
self._current_task.cancel()
|
||||
self._log("[INFO] Aufgabe abgebrochen.\n", "info")
|
||||
self._current_task = None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Hilfsmethoden
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _on_input_file_changed(self, *_):
|
||||
inp = self.v_input_file.get().strip()
|
||||
if not inp:
|
||||
return
|
||||
basename = inp.rsplit("/", 1)[-1]
|
||||
stem = basename.rsplit(".", 1)[0] if "." in basename else basename
|
||||
out_dir = self.v_out_dir.get().rstrip("/")
|
||||
auto_path = f"{out_dir}/{stem}.fits"
|
||||
current = self.v_new_fits.get()
|
||||
if not current or (current.endswith(".fits") and current.startswith(out_dir)):
|
||||
self.v_new_fits.set(auto_path)
|
||||
|
||||
def _browse_local(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title="Astro-Bild auswählen",
|
||||
filetypes=[
|
||||
("Astro-Bilder", "*.fits *.fit *.fts *.jpg *.jpeg *.png *.tiff *.tif"),
|
||||
("Alle Dateien", "*.*"),
|
||||
],
|
||||
)
|
||||
if path:
|
||||
self.v_local_file.set(path)
|
||||
|
||||
def _refresh_cmd(self):
|
||||
cmd = self._build_command()
|
||||
self.cmd_text.delete("1.0", tk.END)
|
||||
self.cmd_text.insert("1.0", cmd)
|
||||
|
||||
def _copy_cmd(self):
|
||||
self.root.clipboard_clear()
|
||||
self.root.clipboard_append(self.cmd_text.get("1.0", tk.END).strip())
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Log (thread-sicher via Queue)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _log(self, text: str, tag: str = ""):
|
||||
self._log_queue.put((text, tag))
|
||||
|
||||
def _poll_log(self):
|
||||
try:
|
||||
while True:
|
||||
text, tag = self._log_queue.get_nowait()
|
||||
self.log.config(state=tk.NORMAL)
|
||||
self.log.insert(tk.END, text, tag or "")
|
||||
self.log.see(tk.END)
|
||||
self.log.config(state=tk.DISABLED)
|
||||
except queue.Empty:
|
||||
pass
|
||||
self.root.after(100, self._poll_log)
|
||||
|
||||
def _clear_log(self):
|
||||
self.log.config(state=tk.NORMAL)
|
||||
self.log.delete("1.0", tk.END)
|
||||
self.log.config(state=tk.DISABLED)
|
||||
|
||||
def _check_sextractor(self):
|
||||
self._submit(self._async_run(
|
||||
"echo '--- SExtractor ---'; "
|
||||
"which sex 2>/dev/null || which source-extractor 2>/dev/null || echo 'nicht gefunden'; "
|
||||
"sex --version 2>&1 || source-extractor --version 2>&1 || true"
|
||||
))
|
||||
|
||||
def _install_sextractor(self):
|
||||
self._submit(self._async_run(
|
||||
"echo Preside | sudo -S apt-get install -y source-extractor 2>&1"
|
||||
))
|
||||
|
||||
def _diagnose_server(self):
|
||||
self._submit(self._async_run(
|
||||
"echo '=== solve-field ==='; solve-field --version 2>&1 | head -3; "
|
||||
"echo '=== astrometry.cfg ==='; cat /etc/astrometry.cfg 2>/dev/null || echo 'nicht gefunden'; "
|
||||
"echo '=== Index-Dateien ==='; ls /usr/share/astrometry/index-*.fits 2>/dev/null | head -30 || echo 'keine gefunden'; "
|
||||
"echo '=== SExtractor ==='; (which sex || which source-extractor) 2>/dev/null && (sex --version || source-extractor --version) 2>&1 | head -2 || echo 'nicht installiert'; "
|
||||
"echo '=== Speicherplatz ==='; df -h /usr/share/astrometry 2>/dev/null || df -h /"
|
||||
))
|
||||
|
||||
def on_close(self):
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
self.root.destroy()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Einstiegspunkt
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
|
||||
style = ttk.Style(root)
|
||||
for preferred in ("clam", "alt", "default"):
|
||||
if preferred in style.theme_names():
|
||||
style.theme_use(preferred)
|
||||
break
|
||||
|
||||
app = AstrometryGUI(root)
|
||||
root.protocol("WM_DELETE_WINDOW", app.on_close)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user