Dies und das
This commit is contained in:
Binary file not shown.
+371
-6
@@ -15,6 +15,7 @@ import os
|
|||||||
import shlex
|
import shlex
|
||||||
import queue
|
import queue
|
||||||
import stat
|
import stat
|
||||||
|
import struct
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import asyncssh
|
import asyncssh
|
||||||
@@ -144,6 +145,26 @@ class AsyncSSH:
|
|||||||
progress_handler=_progress,
|
progress_handler=_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def download(self, remote_path: str, local_path: str, progress_cb=None):
|
||||||
|
if self._conn is None:
|
||||||
|
await self.connect()
|
||||||
|
local_path = os.path.normpath(local_path)
|
||||||
|
|
||||||
|
async with self._conn.start_sftp_client() as sftp:
|
||||||
|
attrs = await sftp.stat(remote_path)
|
||||||
|
total = int(getattr(attrs, "size", 0) or 0)
|
||||||
|
done = 0
|
||||||
|
async with sftp.open(remote_path, "rb") as remote_file:
|
||||||
|
with open(local_path, "wb") as local_file:
|
||||||
|
while True:
|
||||||
|
chunk = await remote_file.read(1024 * 1024)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
local_file.write(chunk)
|
||||||
|
done += len(chunk)
|
||||||
|
if progress_cb:
|
||||||
|
progress_cb(done, total)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
if self._conn:
|
if self._conn:
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
@@ -194,6 +215,7 @@ class AstrometryGUI:
|
|||||||
self.v_pass = tk.StringVar(value="Preside")
|
self.v_pass = tk.StringVar(value="Preside")
|
||||||
|
|
||||||
self.v_local_file = tk.StringVar()
|
self.v_local_file = tk.StringVar()
|
||||||
|
self.v_local_file.trace_add("write", self._on_local_file_changed)
|
||||||
self.v_remote_dir = tk.StringVar(value="/tmp/astrometry")
|
self.v_remote_dir = tk.StringVar(value="/tmp/astrometry")
|
||||||
self.v_input_file = tk.StringVar()
|
self.v_input_file = tk.StringVar()
|
||||||
self.v_input_file.trace_add("write", self._on_input_file_changed)
|
self.v_input_file.trace_add("write", self._on_input_file_changed)
|
||||||
@@ -220,6 +242,11 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
self.v_depth = tk.StringVar()
|
self.v_depth = tk.StringVar()
|
||||||
self.v_cpulimit = tk.StringVar(value="300")
|
self.v_cpulimit = tk.StringVar(value="300")
|
||||||
|
self.v_plot_scale = tk.StringVar()
|
||||||
|
self.v_plot_scale_auto = tk.BooleanVar(value=True)
|
||||||
|
self.v_plot_target_long_edge = tk.StringVar(value="900")
|
||||||
|
self.v_plot_target_long_edge.trace_add("write", self._on_plot_scale_settings_changed)
|
||||||
|
self.v_plot_scale_auto.trace_add("write", self._on_plot_scale_settings_changed)
|
||||||
self.v_downsample = tk.StringVar(value="6")
|
self.v_downsample = tk.StringVar(value="6")
|
||||||
self.v_tweak_order = tk.StringVar(value="2")
|
self.v_tweak_order = tk.StringVar(value="2")
|
||||||
self.v_odds_solve = tk.StringVar()
|
self.v_odds_solve = tk.StringVar()
|
||||||
@@ -244,6 +271,7 @@ class AstrometryGUI:
|
|||||||
self.v_invert = tk.BooleanVar(value=False)
|
self.v_invert = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
self.v_extra = tk.StringVar()
|
self.v_extra = tk.StringVar()
|
||||||
|
self.results_files: dict[str, dict] = {}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# UI aufbauen
|
# UI aufbauen
|
||||||
@@ -254,8 +282,8 @@ class AstrometryGUI:
|
|||||||
top.pack(fill=tk.X)
|
top.pack(fill=tk.X)
|
||||||
self._build_conn_bar(top)
|
self._build_conn_bar(top)
|
||||||
|
|
||||||
nb = ttk.Notebook(self.root)
|
self.nb = ttk.Notebook(self.root)
|
||||||
nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
|
self.nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
|
||||||
|
|
||||||
for title, builder in [
|
for title, builder in [
|
||||||
("Input / Output", self._tab_io),
|
("Input / Output", self._tab_io),
|
||||||
@@ -265,10 +293,14 @@ class AstrometryGUI:
|
|||||||
("Befehlsvorschau", self._tab_cmd),
|
("Befehlsvorschau", self._tab_cmd),
|
||||||
("⚙ Server Setup", self._tab_setup),
|
("⚙ Server Setup", self._tab_setup),
|
||||||
]:
|
]:
|
||||||
f = ttk.Frame(nb, padding=6)
|
f = ttk.Frame(self.nb, padding=6)
|
||||||
nb.add(f, text=title)
|
self.nb.add(f, text=title)
|
||||||
builder(f)
|
builder(f)
|
||||||
|
|
||||||
|
self.results_tab = ttk.Frame(self.nb, padding=6)
|
||||||
|
self._tab_results(self.results_tab)
|
||||||
|
self.nb.add(self.results_tab, text="Remote-Ergebnisse")
|
||||||
|
|
||||||
btn_bar = ttk.Frame(self.root, padding=4)
|
btn_bar = ttk.Frame(self.root, padding=4)
|
||||||
btn_bar.pack(fill=tk.X)
|
btn_bar.pack(fill=tk.X)
|
||||||
|
|
||||||
@@ -358,9 +390,14 @@ class AstrometryGUI:
|
|||||||
"Überträgt die lokale Datei asynchron per SFTP auf den Server.\n"
|
"Überträgt die lokale Datei asynchron per SFTP auf den Server.\n"
|
||||||
"Nach dem Upload wird der Remote-Pfad automatisch eingetragen.")
|
"Nach dem Upload wird der Remote-Pfad automatisch eingetragen.")
|
||||||
self.upload_btn.grid(row=1, column=2, padx=4)
|
self.upload_btn.grid(row=1, column=2, padx=4)
|
||||||
|
tip(ttk.Button(uf, text="Remote-Inhalt l\u00f6schen", command=self._delete_remote_dir),
|
||||||
|
"L\u00f6scht nur den Inhalt des angegebenen Remote-Verzeichnisses auf dem Server.\n"
|
||||||
|
"Das Verzeichnis selbst bleibt erhalten.\n"
|
||||||
|
"Vorher erscheint eine Sicherheitsabfrage."
|
||||||
|
).grid(row=1, column=3, padx=4)
|
||||||
|
|
||||||
self.upload_progress = ttk.Progressbar(uf, mode="determinate", maximum=100)
|
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)
|
self.upload_progress.grid(row=2, column=0, columnspan=4, sticky=tk.EW, padx=4, pady=4)
|
||||||
tip(self.upload_progress, "Fortschritt des Datei-Uploads in Prozent.")
|
tip(self.upload_progress, "Fortschritt des Datei-Uploads in Prozent.")
|
||||||
|
|
||||||
rf = ttk.LabelFrame(parent, text="Eingabedatei auf Remote-Rechner", padding=6)
|
rf = ttk.LabelFrame(parent, text="Eingabedatei auf Remote-Rechner", padding=6)
|
||||||
@@ -492,6 +529,10 @@ class AstrometryGUI:
|
|||||||
("CPU-Limit (--cpulimit, s):", self.v_cpulimit,
|
("CPU-Limit (--cpulimit, s):", self.v_cpulimit,
|
||||||
"Maximale Rechenzeit in Sekunden.\n"
|
"Maximale Rechenzeit in Sekunden.\n"
|
||||||
"300s ist ein guter Kompromiss. 0 = kein Limit."),
|
"300s ist ein guter Kompromiss. 0 = kein Limit."),
|
||||||
|
("Plot-Skalierung (--plot-scale):", self.v_plot_scale,
|
||||||
|
"Skaliert nur die erzeugten Plot-Bilder.\n"
|
||||||
|
"0.25 erzeugt kleinere Plots, 2.0 größere.\n"
|
||||||
|
"Hat keinen Einfluss auf die eigentliche Astrometrie."),
|
||||||
("Downsampling (--downsample):", self.v_downsample,
|
("Downsampling (--downsample):", self.v_downsample,
|
||||||
"Verkleinert das Bild vor der Verarbeitung.\n"
|
"Verkleinert das Bild vor der Verarbeitung.\n"
|
||||||
"6 = 1/6 der Auflösung. Beschleunigt die Quellerkennung stark.\n"
|
"6 = 1/6 der Auflösung. Beschleunigt die Quellerkennung stark.\n"
|
||||||
@@ -521,6 +562,23 @@ class AstrometryGUI:
|
|||||||
tooltip
|
tooltip
|
||||||
).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=(0, 4), pady=2)
|
).grid(row=row, column=col * 2 + 1, sticky=tk.W, padx=(0, 4), pady=2)
|
||||||
|
|
||||||
|
plotf = ttk.LabelFrame(parent, text="Plot-Helfer", padding=6)
|
||||||
|
plotf.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
plotf.columnconfigure(3, weight=1)
|
||||||
|
|
||||||
|
tip(ttk.Checkbutton(plotf, text="Plot-Skalierung automatisch aus lokaler Bildgröße", variable=self.v_plot_scale_auto),
|
||||||
|
"Berechnet --plot-scale aus der langen Bildkante der lokal ausgewählten Datei.\n"
|
||||||
|
"Manuelle Änderungen am Feld sind weiterhin möglich."
|
||||||
|
).grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=4, pady=2)
|
||||||
|
ttk.Label(plotf, text="Zielkantenlänge (px):").grid(row=0, column=2, sticky=tk.W, padx=4, pady=2)
|
||||||
|
tip(ttk.Entry(plotf, textvariable=self.v_plot_target_long_edge, width=10),
|
||||||
|
"Die lange Kante der erzeugten Plots wird grob auf diesen Wert skaliert.\n"
|
||||||
|
"Standard: 900 px."
|
||||||
|
).grid(row=0, column=3, sticky=tk.W, padx=4, pady=2)
|
||||||
|
tip(ttk.Button(plotf, text="Jetzt berechnen", command=self._recalculate_plot_scale),
|
||||||
|
"Liest die Größe der lokalen Datei und setzt --plot-scale neu."
|
||||||
|
).grid(row=0, column=4, sticky=tk.W, padx=4, pady=2)
|
||||||
|
|
||||||
ff = ttk.LabelFrame(parent, text="Schalter (Flags)", padding=6)
|
ff = ttk.LabelFrame(parent, text="Schalter (Flags)", padding=6)
|
||||||
ff.pack(fill=tk.X, pady=(0, 8))
|
ff.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
@@ -685,6 +743,34 @@ class AstrometryGUI:
|
|||||||
" • Freier Speicherplatz"
|
" • Freier Speicherplatz"
|
||||||
).pack(side=tk.LEFT, padx=4, pady=4)
|
).pack(side=tk.LEFT, padx=4, pady=4)
|
||||||
|
|
||||||
|
def _tab_results(self, parent):
|
||||||
|
head = ttk.Frame(parent)
|
||||||
|
head.pack(fill=tk.X, pady=(0, 4))
|
||||||
|
|
||||||
|
tip(ttk.Label(head, text="Remote-Ergebnisse nach solve-field:"),
|
||||||
|
"Zeigt den Inhalt des Remote-Verzeichnisses nach einem solve-field-Lauf an."
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
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="Herunterladen", command=self._download_selected_result),
|
||||||
|
"Lädt die ausgewählte Remote-Datei per SFTP herunter."
|
||||||
|
).pack(side=tk.RIGHT, padx=4)
|
||||||
|
|
||||||
|
self.results_info = ttk.Label(parent, text="Noch kein solve-field-Lauf abgeschlossen.", foreground="gray")
|
||||||
|
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.heading("name", text="Datei")
|
||||||
|
self.results_tree.heading("size", text="Größe")
|
||||||
|
self.results_tree.heading("remote", text="Remote-Pfad")
|
||||||
|
self.results_tree.column("name", width=240, anchor=tk.W)
|
||||||
|
self.results_tree.column("size", width=100, anchor=tk.E)
|
||||||
|
self.results_tree.column("remote", width=520, anchor=tk.W)
|
||||||
|
self.results_tree.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.results_tree.bind("<Double-1>", lambda _e: self._download_selected_result())
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Befehl bauen
|
# Befehl bauen
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
@@ -754,6 +840,7 @@ class AstrometryGUI:
|
|||||||
|
|
||||||
add("--depth", self.v_depth)
|
add("--depth", self.v_depth)
|
||||||
add("--cpulimit", self.v_cpulimit)
|
add("--cpulimit", self.v_cpulimit)
|
||||||
|
add("--plot-scale", self.v_plot_scale)
|
||||||
add("--downsample", self.v_downsample)
|
add("--downsample", self.v_downsample)
|
||||||
add("--odds-to-solve", self.v_odds_solve)
|
add("--odds-to-solve", self.v_odds_solve)
|
||||||
add("--odds-to-reject", self.v_odds_reject)
|
add("--odds-to-reject", self.v_odds_reject)
|
||||||
@@ -843,6 +930,163 @@ class AstrometryGUI:
|
|||||||
finally:
|
finally:
|
||||||
self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL))
|
self.root.after(0, lambda: self.upload_btn.config(state=tk.NORMAL))
|
||||||
|
|
||||||
|
def _delete_remote_dir(self):
|
||||||
|
remote_dir = self.v_remote_dir.get().strip()
|
||||||
|
if not remote_dir:
|
||||||
|
messagebox.showwarning("Remote-Verzeichnis fehlt", "Bitte ein Remote-Verzeichnis angeben.")
|
||||||
|
return
|
||||||
|
if remote_dir == "/":
|
||||||
|
messagebox.showerror("Ung\u00fcltiges Verzeichnis", "Das Root-Verzeichnis '/' darf nicht gel\u00f6scht werden.")
|
||||||
|
return
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"Remote-Inhalt l\u00f6schen",
|
||||||
|
f"Soll nur der Inhalt dieses Verzeichnisses auf dem Server rekursiv gel\u00f6scht werden?\n\n{remote_dir}"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
self._submit(self._async_delete_remote_dir(remote_dir))
|
||||||
|
|
||||||
|
async def _async_delete_remote_dir(self, remote_dir: str):
|
||||||
|
quoted_dir = shlex.quote(remote_dir)
|
||||||
|
cmd = (
|
||||||
|
f"dir={quoted_dir}; "
|
||||||
|
"if [ -z \"$dir\" ] || [ \"$dir\" = \"/\" ]; then "
|
||||||
|
"echo '[FEHLER] Unsicheres Remote-Verzeichnis.'; exit 1; "
|
||||||
|
"fi; "
|
||||||
|
"if [ ! -e \"$dir\" ]; then "
|
||||||
|
"echo '[INFO] Remote-Verzeichnis existiert nicht.'; exit 0; "
|
||||||
|
"fi; "
|
||||||
|
"find \"$dir\" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +; "
|
||||||
|
"echo \"[OK] Remote-Inhalt gel\u00f6scht: $dir\""
|
||||||
|
)
|
||||||
|
self._log(f"[INFO] L\u00f6sche Remote-Inhalt: {remote_dir}\n", "info")
|
||||||
|
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] Remote-Inhalt l\u00f6schen: {exc}\n", "error")
|
||||||
|
|
||||||
|
def _show_results_tab(self):
|
||||||
|
self.nb.select(self.results_tab)
|
||||||
|
|
||||||
|
def _refresh_results_tab(self):
|
||||||
|
remote_dir = self.v_remote_dir.get().strip()
|
||||||
|
self._submit(self._async_refresh_results_tab(remote_dir))
|
||||||
|
|
||||||
|
async def _async_refresh_results_tab(self, remote_dir: str, returncode: int | None = None):
|
||||||
|
if not remote_dir:
|
||||||
|
self.root.after(0, lambda: self._set_results_info("Kein Remote-Verzeichnis gesetzt."))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
return
|
||||||
|
|
||||||
|
quoted_dir = shlex.quote(remote_dir)
|
||||||
|
cmd = (
|
||||||
|
f"dir={quoted_dir}; "
|
||||||
|
"if [ ! -e \"$dir\" ]; then "
|
||||||
|
"echo 'Remote-Verzeichnis existiert nicht.'; exit 0; "
|
||||||
|
"fi; "
|
||||||
|
"find \"$dir\" -maxdepth 2 -type f -printf '%s\t%P\t%p\\n' | sort"
|
||||||
|
)
|
||||||
|
info = f"Remote-Verzeichnis: {remote_dir}"
|
||||||
|
if returncode is not None:
|
||||||
|
info += f" | solve-field Exit-Code: {returncode}"
|
||||||
|
self.root.after(0, lambda: self._set_results_info(info))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
|
||||||
|
entries: list[dict[str, str]] = []
|
||||||
|
|
||||||
|
def _capture(line: str):
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("[Prozess beendet"):
|
||||||
|
return
|
||||||
|
if "\t" not in stripped:
|
||||||
|
return
|
||||||
|
size, rel_path, full_path = stripped.split("\t", 2)
|
||||||
|
entries.append({
|
||||||
|
"name": rel_path or os.path.basename(full_path),
|
||||||
|
"size": size,
|
||||||
|
"remote": full_path,
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
ssh = self._make_ssh()
|
||||||
|
await ssh.connect()
|
||||||
|
await ssh.exec_stream(cmd, _capture)
|
||||||
|
await ssh.close()
|
||||||
|
self.root.after(0, lambda: self._set_results_items(entries))
|
||||||
|
except Exception as exc:
|
||||||
|
self.root.after(0, lambda: self._set_results_info(f"[FEHLER] {exc}"))
|
||||||
|
self.root.after(0, lambda: self._set_results_items([]))
|
||||||
|
|
||||||
|
def _set_results_info(self, text: str):
|
||||||
|
self.results_info.config(text=text, foreground="gray")
|
||||||
|
|
||||||
|
def _set_results_items(self, entries: list[dict[str, str]]):
|
||||||
|
self.results_files = {}
|
||||||
|
for item in self.results_tree.get_children():
|
||||||
|
self.results_tree.delete(item)
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
item_id = f"result_{i}"
|
||||||
|
self.results_files[item_id] = entry
|
||||||
|
self.results_tree.insert(
|
||||||
|
"",
|
||||||
|
tk.END,
|
||||||
|
iid=item_id,
|
||||||
|
values=(
|
||||||
|
entry["name"],
|
||||||
|
self._format_size(entry["size"]),
|
||||||
|
entry["remote"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_size(self, size_text: str) -> str:
|
||||||
|
try:
|
||||||
|
size = int(size_text)
|
||||||
|
except Exception:
|
||||||
|
return size_text
|
||||||
|
units = ["B", "KB", "MB", "GB", "TB"]
|
||||||
|
value = float(size)
|
||||||
|
for unit in units:
|
||||||
|
if value < 1024 or unit == units[-1]:
|
||||||
|
if unit == "B":
|
||||||
|
return f"{int(value)} {unit}"
|
||||||
|
return f"{value:.1f} {unit}"
|
||||||
|
value /= 1024
|
||||||
|
return size_text
|
||||||
|
|
||||||
|
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.")
|
||||||
|
return
|
||||||
|
entry = self.results_files.get(selection[0])
|
||||||
|
if not entry:
|
||||||
|
messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.")
|
||||||
|
return
|
||||||
|
suggested = os.path.basename(entry["remote"])
|
||||||
|
local_dir = filedialog.askdirectory(
|
||||||
|
title="Zielordner für Download auswählen",
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
|
||||||
|
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")
|
||||||
|
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)
|
||||||
|
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")
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
if not self.v_input_file.get():
|
if not self.v_input_file.get():
|
||||||
messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.")
|
messagebox.showwarning("Keine Eingabedatei", "Bitte einen Dateipfad angeben.")
|
||||||
@@ -855,10 +1099,14 @@ class AstrometryGUI:
|
|||||||
try:
|
try:
|
||||||
ssh = self._make_ssh()
|
ssh = self._make_ssh()
|
||||||
await ssh.connect()
|
await ssh.connect()
|
||||||
await ssh.exec_stream(cmd, self._log)
|
rc = await ssh.exec_stream(cmd, self._log)
|
||||||
await ssh.close()
|
await ssh.close()
|
||||||
|
self._current_task = None
|
||||||
|
self.root.after(0, self._show_results_tab)
|
||||||
|
await self._async_refresh_results_tab(self.v_remote_dir.get().strip(), rc)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"[FEHLER] {exc}\n", "error")
|
self._log(f"[FEHLER] {exc}\n", "error")
|
||||||
|
self._current_task = None
|
||||||
|
|
||||||
def _kill(self):
|
def _kill(self):
|
||||||
if self._current_task:
|
if self._current_task:
|
||||||
@@ -882,6 +1130,123 @@ class AstrometryGUI:
|
|||||||
if not current or (current.endswith(".fits") and current.startswith(out_dir)):
|
if not current or (current.endswith(".fits") and current.startswith(out_dir)):
|
||||||
self.v_new_fits.set(auto_path)
|
self.v_new_fits.set(auto_path)
|
||||||
|
|
||||||
|
def _on_local_file_changed(self, *_):
|
||||||
|
if self.v_plot_scale_auto.get():
|
||||||
|
self._recalculate_plot_scale()
|
||||||
|
|
||||||
|
def _on_plot_scale_settings_changed(self, *_):
|
||||||
|
if self.v_plot_scale_auto.get():
|
||||||
|
self._recalculate_plot_scale()
|
||||||
|
|
||||||
|
def _recalculate_plot_scale(self):
|
||||||
|
path = self.v_local_file.get().strip()
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
target = float(self.v_plot_target_long_edge.get().replace(",", "."))
|
||||||
|
if target <= 0:
|
||||||
|
return
|
||||||
|
width, height = self._read_image_size(path)
|
||||||
|
long_edge = max(width, height)
|
||||||
|
if long_edge <= 0:
|
||||||
|
return
|
||||||
|
scale = target / long_edge
|
||||||
|
self.v_plot_scale.set(f"{scale:.3f}".rstrip("0").rstrip("."))
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _read_image_size(self, path: str) -> tuple[int, int]:
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
if ext in (".fits", ".fit", ".fts"):
|
||||||
|
return self._read_fits_size(path)
|
||||||
|
if ext == ".png":
|
||||||
|
return self._read_png_size(path)
|
||||||
|
if ext in (".jpg", ".jpeg"):
|
||||||
|
return self._read_jpeg_size(path)
|
||||||
|
if ext == ".gif":
|
||||||
|
return self._read_gif_size(path)
|
||||||
|
if ext == ".bmp":
|
||||||
|
return self._read_bmp_size(path)
|
||||||
|
raise ValueError("Nicht unterstütztes Bildformat")
|
||||||
|
|
||||||
|
def _read_fits_size(self, path: str) -> tuple[int, int]:
|
||||||
|
width = height = None
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
block = f.read(2880)
|
||||||
|
if not block:
|
||||||
|
break
|
||||||
|
for i in range(0, len(block), 80):
|
||||||
|
card = block[i:i + 80].decode("ascii", errors="ignore")
|
||||||
|
key = card[:8].strip()
|
||||||
|
if key == "END":
|
||||||
|
if width and height:
|
||||||
|
return width, height
|
||||||
|
break
|
||||||
|
if "=" not in card:
|
||||||
|
continue
|
||||||
|
value = card[10:30].split("/")[0].strip()
|
||||||
|
if key == "NAXIS1":
|
||||||
|
width = int(value)
|
||||||
|
elif key == "NAXIS2":
|
||||||
|
height = int(value)
|
||||||
|
raise ValueError("FITS-Bildgröße nicht lesbar")
|
||||||
|
|
||||||
|
def _read_png_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(24)
|
||||||
|
if header[:8] != b"\x89PNG\r\n\x1a\n":
|
||||||
|
raise ValueError("Ungültige PNG-Datei")
|
||||||
|
return struct.unpack(">II", header[16:24])
|
||||||
|
|
||||||
|
def _read_gif_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(10)
|
||||||
|
if header[:6] not in (b"GIF87a", b"GIF89a"):
|
||||||
|
raise ValueError("Ungültige GIF-Datei")
|
||||||
|
return struct.unpack("<HH", header[6:10])
|
||||||
|
|
||||||
|
def _read_bmp_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
header = f.read(26)
|
||||||
|
if header[:2] != b"BM":
|
||||||
|
raise ValueError("Ungültige BMP-Datei")
|
||||||
|
width, height = struct.unpack("<ii", header[18:26])
|
||||||
|
return abs(width), abs(height)
|
||||||
|
|
||||||
|
def _read_jpeg_size(self, path: str) -> tuple[int, int]:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
if f.read(2) != b"\xff\xd8":
|
||||||
|
raise ValueError("Ungültige JPEG-Datei")
|
||||||
|
while True:
|
||||||
|
marker_prefix = f.read(1)
|
||||||
|
if not marker_prefix:
|
||||||
|
break
|
||||||
|
if marker_prefix != b"\xff":
|
||||||
|
continue
|
||||||
|
marker = f.read(1)
|
||||||
|
while marker == b"\xff":
|
||||||
|
marker = f.read(1)
|
||||||
|
if marker in (b"\xd8", b"\xd9"):
|
||||||
|
continue
|
||||||
|
size_bytes = f.read(2)
|
||||||
|
if len(size_bytes) != 2:
|
||||||
|
break
|
||||||
|
segment_size = struct.unpack(">H", size_bytes)[0]
|
||||||
|
if marker in (
|
||||||
|
b"\xc0", b"\xc1", b"\xc2", b"\xc3",
|
||||||
|
b"\xc5", b"\xc6", b"\xc7",
|
||||||
|
b"\xc9", b"\xca", b"\xcb",
|
||||||
|
b"\xcd", b"\xce", b"\xcf",
|
||||||
|
):
|
||||||
|
data = f.read(5)
|
||||||
|
if len(data) != 5:
|
||||||
|
break
|
||||||
|
height, width = struct.unpack(">HH", data[1:5])
|
||||||
|
return width, height
|
||||||
|
f.seek(segment_size - 2, os.SEEK_CUR)
|
||||||
|
raise ValueError("JPEG-Bildgröße nicht lesbar")
|
||||||
|
|
||||||
def _browse_local(self):
|
def _browse_local(self):
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
title="Astro-Bild auswählen",
|
title="Astro-Bild auswählen",
|
||||||
|
|||||||
Reference in New Issue
Block a user