diff --git a/AstroMac.json b/AstroMac.json new file mode 100644 index 0000000..281147c --- /dev/null +++ b/AstroMac.json @@ -0,0 +1,6 @@ +{ + "host": "10.42.42.102", + "port": 22, + "user": "tm", + "password": "heute1234" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..619e083 --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ +# Solve-Field-GUI + +Grafische Windows-Oberfläche für `solve-field` aus Astrometry.net mit Ausführung auf einem Linux-Server per SSH. + +Die Software richtet sich an Anwender, die Astrometrie komfortabel über eine GUI steuern möchten, ohne die `solve-field`-Befehle jedes Mal von Hand zusammenzubauen. + +## Was die Software macht + +Die Anwendung läuft lokal unter Windows und verbindet sich per SSH mit einem entfernten Linux-Server. + +Dort kann sie: + +- Bilddateien auf den Server hochladen +- `solve-field` mit vielen Parametern starten +- die Ausgabe live im Log anzeigen +- Ergebnisdateien im Remote-Verzeichnis anzeigen +- einzelne oder mehrere Ergebnisdateien wieder lokal herunterladen +- Serverfunktionen wie SExtractor-Prüfung und Diagnose ausführen + +## Hauptfunktionen + +- SSH-Verbindung zu einem Linux-Server mit Benutzername, Passwort und Port +- Upload lokaler Bilddateien per SFTP +- grafische Konfiguration fast aller wichtigen `solve-field`-Parameter +- automatische Befehlsvorschau des erzeugten `solve-field`-Aufrufs +- Unterstützung für Maßstab, RA/Dek, Suchradius, Downsampling, CPU-Limit, Tweak-Order und viele weitere Optionen +- Unterstützung für Source Extractor per `--use-source-extractor` +- automatische Berechnung von `--plot-scale` aus der lokalen Bildgröße +- serverseitige Diagnose für `solve-field`, `astrometry.cfg`, Indexdateien, SExtractor und Speicherplatz +- Remote-Ergebnis-Tab mit Dateiliste +- Mehrfachdownload ausgewählter Ergebnisdateien +- Profilspeicherung und Profilladen für komplette GUI-Einstellungen + +## Unterstützte Dateitypen + +Als Eingabedatei lokal: + +- FITS +- FIT +- FTS +- JPG / JPEG +- PNG +- TIFF / TIF + +Für die automatische Plot-Skalierung kann die Anwendung Bildgrößen direkt aus mehreren Formaten lesen, darunter FITS, PNG, JPG, GIF und BMP. + +## Voraussetzungen + +## Lokal unter Windows + +- Python 3 +- `asyncssh` +- Netzwerkzugriff auf den Linux-Server per SSH + +Installation von `asyncssh`: + +```bash +pip install asyncssh +``` + +## Auf dem Linux-Server + +- Astrometry.net mit `solve-field` +- passende Indexdateien +- SSH-Zugang + +Optional: + +- `source-extractor`, wenn SExtractor zur Quellenerkennung genutzt werden soll + +## Starten + +Direkt mit Python: + +```bash +python astrometry_gui.py +``` + +Oder unter Windows über: + +```bash +astrometry_gui.bat +``` + +## Bedienung + +## 1. SSH eintragen + +Oben in der GUI: + +- Host +- Port +- Benutzer +- Passwort + +Mit `Verbinden & testen` kann geprüft werden, ob der Server erreichbar ist und `solve-field` vorhanden ist. + +## 2. Bild auswählen und hochladen + +Im Tab `Input / Output`: + +- lokale Datei auswählen +- Remote-Verzeichnis festlegen +- Datei per `Hochladen` auf den Server übertragen + +Nach dem Upload wird der Remote-Dateipfad automatisch als Eingabedatei gesetzt. + +## 3. Solver-Parameter einstellen + +Je nach Bedarf können unter anderem gesetzt werden: + +- Maßstabsbereich +- RA / Dek / Radius +- CPU-Limit +- Downsampling +- Plot-Skalierung +- Tweak-Order +- Verifikation +- Invertierung +- SExtractor-Nutzung + +## 4. Befehl prüfen und starten + +Im Tab `Befehlsvorschau` sieht man den tatsächlich erzeugten `solve-field`-Befehl. + +Mit `solve-field starten` wird der Befehl auf dem Server ausgeführt. + +## Tabs im Überblick + +## Input / Output + +- lokale Bilddatei +- Remote-Verzeichnis +- Upload +- Eingabedatei auf dem Server +- Ausgabeoptionen wie `--wcs`, `--corr`, `--rdls`, `--match`, `--new-fits` + +## Maßstab & Position + +- `--scale-low` +- `--scale-high` +- `--scale` +- `--scale-err` +- `--ra` +- `--dec` +- `--radius` + +## Solver-Parameter + +- `--depth` +- `--cpulimit` +- `--plot-scale` +- `--downsample` +- `--tweak-order` +- `--odds-*` +- `--objs` +- `--pixel-error` +- zahlreiche Flags wie `--no-plots`, `--invert`, `--no-verify` + +Zusätzlich gibt es einen Plot-Helfer: + +- automatische Berechnung von `plot-scale` +- Zielkantenlänge einstellbar + +## Erweitert + +- manuelle Indexdateien +- zusätzliche freie Kommandozeilenargumente + +## Befehlsvorschau + +- zeigt den generierten Befehl +- kann aktualisiert werden +- kann in die Zwischenablage kopiert werden + +## Server Setup + +- SExtractor prüfen +- SExtractor installieren +- Server-Diagnose ausführen + +## Remote-Ergebnisse + +- zeigt Dateien im aktuellen Remote-Verzeichnis +- lädt den Inhalt beim Öffnen des Tabs automatisch neu +- erlaubt Auswahl und Download von einer oder mehreren Dateien + +## Remote-Ergebnisse herunterladen + +Im Tab `Remote-Ergebnisse`: + +- eine oder mehrere Dateien markieren +- `Herunterladen` klicken +- Zielordner auswählen + +Standardmäßig wird als Zielordner der Ordner der ursprünglich lokal ausgewählten Bilddatei vorgeschlagen. + +## Remote-Verzeichnis leeren + +Im Tab `Input / Output` gibt es die Funktion: + +- `Remote-Inhalt löschen` + +Dabei wird nur der Inhalt des Remote-Verzeichnisses gelöscht, nicht das Verzeichnis selbst. + +## Profile speichern und laden + +Über die Menühleiste: + +- `Profil -> Profil speichern…` +- `Profil -> Profil laden…` + +Gespeichert werden: + +- SSH-Daten +- Solver-Parameter +- Ein-/Ausgabe-Einstellungen +- Zusatzargumente +- Indexliste + +Das Passwort wird nicht im Klartext gespeichert, sondern mit einer einfachen internen Verschlüsselung abgelegt. + +## Typischer Workflow + +1. Serververbindung eintragen +2. Lokales Bild auswählen +3. Datei hochladen +4. Maßstab und Solver-Parameter setzen +5. Optional SExtractor aktivieren +6. `solve-field` starten +7. Ergebnisse im Tab `Remote-Ergebnisse` prüfen +8. Gewünschte Dateien lokal herunterladen + +## Hinweise + +- Die GUI baut nur den Befehl und führt ihn per SSH aus. Die eigentliche Astrometrie läuft vollständig auf dem Server. +- Die installierte Astrometry-Version auf dem Server bestimmt, welche Optionen tatsächlich verfügbar sind. +- Für SExtractor muss auf dem Server in der Regel `source-extractor` installiert sein. +- Für gute Ergebnisse müssen die passenden Indexdateien serverseitig vorhanden und in `astrometry.cfg` korrekt eingebunden sein. + +## Dateien im Projekt + +- [astrometry_gui.py](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.py) +- [astrometry_gui.bat](C:/Users/ich/source/repos/Solve-Field-GUI/astrometry_gui.bat) + +## Lizenz / Nutzung + +Dieses Repository enthält eine GUI zur Bedienung von Astrometry.net. Die Lizenz und Bedingungen von Astrometry.net sowie von eventuell installierten Serverpaketen gelten zusätzlich zu diesem Projekt. diff --git a/__pycache__/astrometry_gui.cpython-312.pyc b/__pycache__/astrometry_gui.cpython-312.pyc index 84a327f..70b2bbd 100644 Binary files a/__pycache__/astrometry_gui.cpython-312.pyc and b/__pycache__/astrometry_gui.cpython-312.pyc differ diff --git a/astrometry_gui.py b/astrometry_gui.py index fa15cd1..43aadcf 100644 --- a/astrometry_gui.py +++ b/astrometry_gui.py @@ -11,6 +11,7 @@ import asyncio import base64 import json import tkinter as tk +import tkinter.font as tkfont from tkinter import ttk, filedialog, messagebox, scrolledtext import threading import os @@ -26,6 +27,9 @@ except ImportError: HAS_ASYNCSSH = False +SSH_CREDENTIALS_JSON = "ssh_zugangsdaten.json" + + # --------------------------------------------------------------------------- # Tooltip # --------------------------------------------------------------------------- @@ -215,6 +219,7 @@ class AstrometryGUI: self.v_port = tk.IntVar(value=22) self.v_user = tk.StringVar(value="eskimue") self.v_pass = tk.StringVar(value="Preside") + self._load_startup_credentials() self.v_local_file = tk.StringVar() self.v_local_file.trace_add("write", self._on_local_file_changed) @@ -275,6 +280,27 @@ class AstrometryGUI: self.v_extra = tk.StringVar() self.results_files: dict[str, dict] = {} + def _load_startup_credentials(self): + path = os.path.join(os.getcwd(), SSH_CREDENTIALS_JSON) + if not os.path.isfile(path): + return + try: + self._load_credentials_from_file(path) + except Exception: + pass + + def _load_credentials_from_file(self, path: str): + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + if "host" in data: + self.v_host.set(str(data["host"])) + if "port" in data: + self.v_port.set(int(data["port"])) + if "user" in data: + self.v_user.set(str(data["user"])) + if "password" in data: + self.v_pass.set(str(data["password"])) + # ----------------------------------------------------------------------- # UI aufbauen # ----------------------------------------------------------------------- @@ -345,6 +371,8 @@ class AstrometryGUI: menubar = tk.Menu(self.root) profile_menu = tk.Menu(menubar, tearoff=False) + profile_menu.add_command(label="Verbindung laden…", command=self._menu_load_credentials) + profile_menu.add_separator() profile_menu.add_command(label="Profil speichern…", command=self._save_profile) profile_menu.add_command(label="Profil laden…", command=self._load_profile) menubar.add_cascade(label="Profil", menu=profile_menu) @@ -399,6 +427,9 @@ class AstrometryGUI: "Öffnet einen Datei-Dialog um die lokale Astro-Bild-Datei auszuwählen." ).grid(row=0, column=2, padx=4) + self.local_image_size_label = ttk.Label(uf, text="Größe: —", foreground="gray") + self.local_image_size_label.grid(row=0, column=3, sticky=tk.W, padx=4, pady=3) + 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" @@ -772,6 +803,9 @@ class AstrometryGUI: tip(ttk.Button(head, text="Aktualisieren", command=self._refresh_results_tab), "Liest den Inhalt des aktuell eingestellten Remote-Verzeichnisses erneut vom Server ein." ).pack(side=tk.RIGHT, padx=4) + tip(ttk.Button(head, text="Remote-Inhalt löschen", command=self._delete_remote_dir), + "Löscht den Inhalt des aktuell eingestellten Remote-Verzeichnisses auf dem Server." + ).pack(side=tk.RIGHT, padx=4) tip(ttk.Button(head, text="Herunterladen", command=self._download_selected_result), "Lädt die ausgewählte Remote-Datei per SFTP herunter." ).pack(side=tk.RIGHT, padx=4) @@ -780,7 +814,7 @@ class AstrometryGUI: self.results_info.pack(anchor=tk.W, pady=(0, 4)) cols = ("name", "size", "remote") - self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=16) + self.results_tree = ttk.Treeview(parent, columns=cols, show="headings", height=16, selectmode="extended") self.results_tree.heading("name", text="Datei") self.results_tree.heading("size", text="Größe") self.results_tree.heading("remote", text="Remote-Pfad") @@ -1060,6 +1094,20 @@ class AstrometryGUI: entry["remote"], ), ) + self._autosize_results_columns() + + def _autosize_results_columns(self): + body_font = tkfont.nametofont("TkDefaultFont") + head_font = tkfont.nametofont("TkHeadingFont") + padding = 24 + + for col in ("name", "size", "remote"): + header = self.results_tree.heading(col, "text") or col + width = head_font.measure(header) + padding + for item_id in self.results_tree.get_children(): + value = self.results_tree.set(item_id, col) + width = max(width, body_font.measure(str(value)) + padding) + self.results_tree.column(col, width=width) def _clear_wait_cursor(self): self.root.config(cursor="") @@ -1084,13 +1132,16 @@ class AstrometryGUI: def _download_selected_result(self): selection = self.results_tree.selection() if not selection: - messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine Datei im Ergebnis-Tab auswählen.") + messagebox.showwarning("Keine Datei ausgewählt", "Bitte zuerst eine oder mehrere Dateien im Ergebnis-Tab auswählen.") return - entry = self.results_files.get(selection[0]) - if not entry: - messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.") + entries = [] + for item_id in selection: + entry = self.results_files.get(item_id) + if entry: + entries.append(entry) + if not entries: + messagebox.showwarning("Datei fehlt", "Die ausgewählten Dateien konnten nicht gefunden werden.") return - suggested = os.path.basename(entry["remote"]) source_dir = os.path.dirname(self.v_local_file.get().strip()) if self.v_local_file.get().strip() else "" initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd() local_dir = filedialog.askdirectory( @@ -1099,20 +1150,25 @@ class AstrometryGUI: ) if not local_dir: return - local_path = os.path.normpath(os.path.join(local_dir, suggested)) - self._submit(self._async_download_result(entry["remote"], local_path)) + downloads = [] + for entry in entries: + suggested = os.path.basename(entry["remote"]) + local_path = os.path.normpath(os.path.join(local_dir, suggested)) + downloads.append((entry["remote"], local_path)) + self._submit(self._async_download_results(downloads)) - async def _async_download_result(self, remote_path: str, local_path: str): - self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info") + async def _async_download_results(self, downloads: list[tuple[str, str]]): try: - os.makedirs(os.path.dirname(local_path), exist_ok=True) ssh = self._make_ssh() await ssh.connect() - await ssh.download(remote_path, local_path) + for remote_path, local_path in downloads: + self._log(f"[INFO] Lade Ergebnis herunter: {remote_path} → {local_path}\n", "info") + os.makedirs(os.path.dirname(local_path), exist_ok=True) + await ssh.download(remote_path, local_path) + if not os.path.isfile(local_path): + raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") + self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") await ssh.close() - if not os.path.isfile(local_path): - raise FileNotFoundError(f"Datei nach Download nicht gefunden: {local_path}") - self._log(f"[OK] Download abgeschlossen: {local_path}\n", "ok") except Exception as exc: self._log(f"[FEHLER] Download: {exc}\n", "error") @@ -1159,6 +1215,15 @@ class AstrometryGUI: self.v_new_fits.set(auto_path) def _on_local_file_changed(self, *_): + path = self.v_local_file.get().strip() + if not path or not os.path.isfile(path): + self.local_image_size_label.config(text="Größe: —") + else: + try: + width, height = self._read_image_size(path) + self.local_image_size_label.config(text=f"Größe: {width} × {height}") + except Exception: + self.local_image_size_label.config(text="Größe: unbekannt") if self.v_plot_scale_auto.get(): self._recalculate_plot_scale() @@ -1285,6 +1350,8 @@ class AstrometryGUI: ) if path: self.v_local_file.set(path) + if messagebox.askyesno("Datei hochladen", "Soll die ausgewählte Datei jetzt direkt auf den Server hochgeladen werden?"): + self._upload() def _refresh_cmd(self): cmd = self._build_command() @@ -1385,6 +1452,20 @@ class AstrometryGUI: except Exception as exc: messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}") + def _menu_load_credentials(self): + path = filedialog.askopenfilename( + title="Zugangsdaten laden", + initialdir=os.getcwd(), + filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")], + ) + if not path: + return + try: + self._load_credentials_from_file(path) + self._log(f"[OK] Zugangsdaten geladen: {path}\n", "ok") + except Exception as exc: + messagebox.showerror("Zugangsdaten laden", f"Laden fehlgeschlagen:\n{exc}") + # ----------------------------------------------------------------------- # Log (thread-sicher via Queue) # ----------------------------------------------------------------------- diff --git a/ssh_zugangsdaten.json b/ssh_zugangsdaten.json new file mode 100644 index 0000000..646df60 --- /dev/null +++ b/ssh_zugangsdaten.json @@ -0,0 +1,6 @@ +{ + "host": "10.42.44.25", + "port": 22, + "user": "eskimue", + "password": "Preside" +}