Loading noche/daemon.py +72 −44 Original line number Diff line number Diff line Loading @@ -9,6 +9,46 @@ from astropy import log from noche import Noche def convert_single_file(input_path: Path, output_dir: Path, observatory_config, header_template=None, use_auto_filename=False) -> Path: """ Processes a single FITS file using Noche. Returns ------- Path The path to the written output file. """ # Ensure paths are Path objects input_path = Path(input_path) output_dir = Path(output_dir) noche = Noche(header_template_path=header_template) if isinstance(observatory_config, Path) and observatory_config.is_file(): noche.load_observatory(path=str(observatory_config), fits_file=input_path) elif isinstance(observatory_config, str): noche.load_noctis_observatory(name=observatory_config, fits_file=input_path) else: log.warning("No observatory configuration specified. Header will be built from template only.") if use_auto_filename: # Generate filename based on header data prefix = "RAW" # Use .get with defaults to prevent crashes if keys are missing tel = noche.header.get("TELESCOP", "UNKNOWN").replace(" ", "_").upper() timestamp = noche.header.get("DATE-OBS", "UNKNOWN").replace(":", "_") output_filename = f"{prefix}.{tel}.{timestamp}.fits" else: output_filename = f"NOCTIS.{input_path.name}" output_path = output_dir / output_filename noche.write_noctis_fits(filename=str(output_path), overwrite=True) return output_path def load_daemon_config(config_file): """ Loads daemon configuration from an INI file. Loading Loading @@ -88,9 +128,6 @@ class FitsFileHandler(FileSystemEventHandler): self.output_dir.mkdir(parents=True, exist_ok=True) self.noche = Noche(header_template_path=self.header_template) def on_created(self, event): """ Called when a file or directory is created. Loading @@ -109,12 +146,13 @@ class FitsFileHandler(FileSystemEventHandler): if file_path not in self.processed_files: log.info(f"Detected new file: {file_path.name}") self.processed_files.add(file_path) # Add a small delay to ensure file write is complete time.sleep(0.5) try: self.process_fits_file(file_path) except Exception as e: log.error(f"Error processing {file_path.name}: {e}") def process_fits_file(self, input_fits_path: Path): """ Processes a single FITS file by loading it into Noche, applying Loading @@ -125,36 +163,27 @@ class FitsFileHandler(FileSystemEventHandler): input_fits_path : Path The path to the input FITS file. """ log.info(f"Processing: {input_fits_path.name}") try: if isinstance(self.observatory_config, Path) and self.observatory_config.is_file(): self.noche.load_observatory(path=str(self.observatory_config), fits_file=input_fits_path) elif isinstance(self.observatory_config, str): self.noche.load_noctis_observatory(name=self.observatory_config, fits_file=input_fits_path) else: log.warning("No observatory configuration specified. Header will be built from template only.") output_path = convert_single_file( input_path=input_fits_path, output_dir=self.output_dir, observatory_config=self.observatory_config, header_template=self.header_template, use_auto_filename=self.auto_filename ) log.info(f"Successfully processed and saved: {output_path.name}") return output_path except Exception as e: log.error(f"Failed to load observatory configuration: {e}") log.error(f"Failed to convert {input_fits_path.name}: {e}") raise e try: if self.auto_filename: # Have to double from noche.py as there is also the directory log.info(f"AUTO filename in the form: RAW.telescop_key.time_stamp.fits") prefix = "RAW" tel = self.noche.header["TELESCOP"].replace(" ","_").upper() timestamp = self.noche.header["DATE-OBS"].replace(":","_") output_filename = self.output_dir / f"{prefix}.{tel}.{timestamp}.fits" else: output_filename = self.output_dir / f"processed_{input_fits_path.name}" log.info(f"Writing processed file to: {output_filename}") self.noche.write_noctis_fits(filename=str(output_filename), overwrite=True) log.info(f"Successfully processed and saved: {output_filename.name}") except Exception as e: log.error(f"An unexpected error occurred processing {input_fits_path.name}: {e}") def stop_daemon(observer): if observer: observer.stop() observer.join(timeout=5) def start_daemon(daemon_settings): Loading @@ -178,8 +207,6 @@ def start_daemon(daemon_settings): log.info("Starting Noche daemon.") log.info(f"Monitoring directory: {input_dir.resolve()}") log.info(f"Output directory: {output_dir.resolve()}") log.info(f"Observatory Configuration: {obs_config}") log.info(f"Header Template: {header_template}") event_handler = FitsFileHandler( output_dir=str(output_dir), Loading @@ -196,27 +223,28 @@ def start_daemon(daemon_settings): while True: time.sleep(1) except KeyboardInterrupt: log.info("Shutting down Noche daemon...") observer.stop() log.warning("Shutting down Noche daemon...") stop_daemon(observer) except Exception as e: log.error(f"Daemon encountered an unhandled exception: {e}") observer.stop() stop_daemon(observer) finally: observer.join() log.info("Noche daemon stopped.") def run(): try: daemon_settings = load_daemon_config(sys.argv[1]) except IndexError: log.warning("Missing config file, trying default") # Ensure we can find the default config relative to this file default_daemon_config = Path(__file__).parent / "config" / "daemon_config.ini" if not default_daemon_config.exists(): log.error("Default config not found.") sys.exit(1) daemon_settings = load_daemon_config(default_daemon_config) except SystemExit: log.error("Missing config file") log.error("Configuration error.") sys.exit(1) start_daemon(daemon_settings) Loading noche/gui.py 0 → 100644 +387 −0 Original line number Diff line number Diff line import sys from pathlib import Path from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets from watchdog.observers import Observer from noche import Noche from noche.daemon2 import convert_single_file, FitsFileHandler # SUPPORTED_OBS = ["abobservatory", "ogg", "grt", "opc", "oarpaf", "ossfoligno"] SUPPORTED_OBS = Noche().noctis_observatories class ConversionWorker(QtCore.QObject): """ Worker for Manual Conversion Tab. Uses convert_single_file directly from daemon module. """ progress = QtCore.pyqtSignal(int, str) finished = QtCore.pyqtSignal() message = QtCore.pyqtSignal(str) def __init__(self, files, output_dir, observatory, use_auto_name=True): super().__init__() self.files = [Path(f) for f in files] self.output_dir = Path(output_dir) self.observatory = observatory self.use_auto_name = use_auto_name self._abort = False @QtCore.pyqtSlot() def run(self): total = len(self.files) if total == 0: self.progress.emit(0, "") self.finished.emit() return for idx, f in enumerate(self.files, start=1): if self._abort: break try: self.message.emit(f"Conversione di {f.name}...") # USE DAEMON FUNCTION out = convert_single_file( input_path=f, output_dir=self.output_dir, observatory_config=self.observatory, use_auto_filename=self.use_auto_name ) self.message.emit(f"OK -> {out.name}") except Exception as e: self.message.emit(f"ERRORE su {f.name}: {e!r}") percent = int(idx * 100 / total) self.progress.emit(percent, str(f)) self.finished.emit() def abort(self): self._abort = True class GuiFitsHandler(FitsFileHandler): """ Inherits from the Daemon's FitsFileHandler to reuse logic, but bridges the Thread gap to update the GUI via a callback/signal. """ def __init__(self, callback, *args, **kwargs): super().__init__(*args, **kwargs) self.callback = callback def process_fits_file(self, input_fits_path: Path): """ Override the processing method to capture result and notify GUI. """ try: # Call the daemon's original logic output_path = super().process_fits_file(input_fits_path) # Notify Success msg = f"Convertito: {input_fits_path.name} → {output_path.name}" self.callback(input_fits_path, msg, True) return output_path except Exception as e: # Notify Error msg = f"ERRORE su {input_fits_path.name}: {e!r}" self.callback(input_fits_path, msg, False) raise e class MainWindow(QtWidgets.QMainWindow): # Signal to bridge background thread -> Main GUI thread monitor_signal = QtCore.pyqtSignal(object, str, bool) def __init__(self): super().__init__() self.setWindowTitle("Noche FITS Converter") self.resize(900, 600) self.setWindowIcon(QtGui.QIcon()) self.settings = QtCore.QSettings("ABObservatory", "NocheGuiTool") self.observer: Optional[Observer] = None self.watch_handler: Optional[GuiFitsHandler] = None # Connect signal self.monitor_signal.connect(self._on_monitor_update) self._build_ui() self._load_settings() def _build_ui(self): tabs = QtWidgets.QTabWidget() self.setCentralWidget(tabs) self.tab_monitor = QtWidgets.QWidget() tabs.addTab(self.tab_monitor, "Monitor cartella") self._build_tab_monitor() self.tab_manual = QtWidgets.QWidget() tabs.addTab(self.tab_manual, "Conversione manuale") self._build_tab_manual() self.statusBar().showMessage("Pronto") def _build_tab_monitor(self): layout = QtWidgets.QVBoxLayout(self.tab_monitor) form = QtWidgets.QFormLayout() self.combo_obs_monitor = QtWidgets.QComboBox() self.combo_obs_monitor.addItems(SUPPORTED_OBS) form.addRow("Osservatorio NOCTIS:", self.combo_obs_monitor) folder_layout = QtWidgets.QHBoxLayout() self.edit_watch_dir = QtWidgets.QLineEdit() btn_browse_watch = QtWidgets.QPushButton("Sfoglia...") btn_browse_watch.clicked.connect(self.browse_watch_dir) folder_layout.addWidget(self.edit_watch_dir) folder_layout.addWidget(btn_browse_watch) form.addRow("Cartella da monitorare:", folder_layout) out_layout = QtWidgets.QHBoxLayout() self.edit_output_dir_monitor = QtWidgets.QLineEdit() btn_browse_out_mon = QtWidgets.QPushButton("Sfoglia...") btn_browse_out_mon.clicked.connect(self.browse_output_dir_monitor) out_layout.addWidget(self.edit_output_dir_monitor) out_layout.addWidget(btn_browse_out_mon) form.addRow("Cartella output:", out_layout) self.check_auto_name_monitor = QtWidgets.QCheckBox("Usa nome automatico NOCTIS (RAW.TEL.DATE-OBS.fits)") self.check_auto_name_monitor.setChecked(True) form.addRow("", self.check_auto_name_monitor) layout.addLayout(form) h = QtWidgets.QHBoxLayout() self.btn_toggle_watch = QtWidgets.QPushButton("Avvia monitoraggio") self.btn_toggle_watch.setCheckable(True) self.btn_toggle_watch.clicked.connect(self.toggle_watch) self.label_watch_status = QtWidgets.QLabel("Stato: FERMO") self.label_watch_status.setStyleSheet("color: red; font-weight: bold;") h.addWidget(self.btn_toggle_watch) h.addStretch() h.addWidget(self.label_watch_status) layout.addLayout(h) self.text_log_monitor = QtWidgets.QPlainTextEdit() self.text_log_monitor.setReadOnly(True) self.text_log_monitor.setMinimumHeight(250) layout.addWidget(self.text_log_monitor) layout.addStretch() def _build_tab_manual(self): layout = QtWidgets.QVBoxLayout(self.tab_manual) form = QtWidgets.QFormLayout() self.combo_obs_manual = QtWidgets.QComboBox() self.combo_obs_manual.addItems(SUPPORTED_OBS) form.addRow("Osservatorio NOCTIS:", self.combo_obs_manual) out_layout = QtWidgets.QHBoxLayout() self.edit_output_dir_manual = QtWidgets.QLineEdit() btn_browse_out = QtWidgets.QPushButton("Sfoglia...") btn_browse_out.clicked.connect(self.browse_output_dir_manual) out_layout.addWidget(self.edit_output_dir_manual) out_layout.addWidget(btn_browse_out) form.addRow("Cartella output:", out_layout) self.check_auto_name_manual = QtWidgets.QCheckBox("Usa nome automatico NOCTIS (RAW.TEL.DATE-OBS.fits)") self.check_auto_name_manual.setChecked(True) form.addRow("", self.check_auto_name_manual) layout.addLayout(form) files_group = QtWidgets.QGroupBox("File da convertire") files_layout = QtWidgets.QVBoxLayout(files_group) self.list_files = QtWidgets.QListWidget() files_layout.addWidget(self.list_files) btns_layout = QtWidgets.QHBoxLayout() btn_add = QtWidgets.QPushButton("Aggiungi file...") btn_add.clicked.connect(self.add_files) btn_remove = QtWidgets.QPushButton("Rimuovi selezionati") btn_remove.clicked.connect(self.remove_selected_files) btn_clear = QtWidgets.QPushButton("Svuota lista") btn_clear.clicked.connect(self.list_files.clear) btns_layout.addWidget(btn_add) btns_layout.addWidget(btn_remove) btns_layout.addWidget(btn_clear) btns_layout.addStretch() files_layout.addLayout(btns_layout) layout.addWidget(files_group) bottom_layout = QtWidgets.QHBoxLayout() self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.btn_start_manual = QtWidgets.QPushButton("Avvia conversione") self.btn_start_manual.clicked.connect(self.start_manual_conversion) bottom_layout.addWidget(self.progress_bar, stretch=1) bottom_layout.addWidget(self.btn_start_manual) layout.addLayout(bottom_layout) self.text_log_manual = QtWidgets.QPlainTextEdit() self.text_log_manual.setReadOnly(True) self.text_log_manual.setMinimumHeight(200) layout.addWidget(self.text_log_manual) layout.addStretch() def _load_settings(self): last_watch = self.settings.value("watch_dir", "", str) last_out_mon = self.settings.value("output_dir_monitor", "", str) last_out_man = self.settings.value("output_dir_manual", "", str) last_obs = self.settings.value("observatory", "abobservatory", str) idx_monitor = self.combo_obs_monitor.findText(last_obs) if idx_monitor >= 0: self.combo_obs_monitor.setCurrentIndex(idx_monitor) idx_manual = self.combo_obs_manual.findText(last_obs) if idx_manual >= 0: self.combo_obs_manual.setCurrentIndex(idx_manual) self.edit_watch_dir.setText(last_watch) self.edit_output_dir_monitor.setText(last_out_mon) self.edit_output_dir_manual.setText(last_out_man) def closeEvent(self, event: QtGui.QCloseEvent): self.settings.setValue("watch_dir", self.edit_watch_dir.text()) self.settings.setValue("output_dir_monitor", self.edit_output_dir_monitor.text()) self.settings.setValue("output_dir_manual", self.edit_output_dir_manual.text()) self.settings.setValue("observatory", self.combo_obs_monitor.currentText()) self.stop_watch() super().closeEvent(event) def browse_watch_dir(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella da monitorare", self.edit_watch_dir.text()) if d: self.edit_watch_dir.setText(d) def browse_output_dir_monitor(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella output", self.edit_output_dir_monitor.text()) if d: self.edit_output_dir_monitor.setText(d) def log_monitor(self, msg: str): ts = QtCore.QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss") self.text_log_monitor.appendPlainText(f"[{ts}] {msg}") self.text_log_monitor.verticalScrollBar().setValue(self.text_log_monitor.verticalScrollBar().maximum()) def toggle_watch(self, checked: bool): if checked: if self.start_watch(): self.btn_toggle_watch.setText("Ferma monitoraggio") self.label_watch_status.setText("Stato: IN ESECUZIONE") self.label_watch_status.setStyleSheet("color: green; font-weight: bold;") else: self.btn_toggle_watch.setChecked(False) else: self.stop_watch() self.btn_toggle_watch.setText("Avvia monitoraggio") self.label_watch_status.setText("Stato: FERMO") self.label_watch_status.setStyleSheet("color: red; font-weight: bold;") def start_watch(self) -> bool: if self.observer is not None: return True watch_dir = self.edit_watch_dir.text().strip() output_dir = self.edit_output_dir_monitor.text().strip() observatory = self.combo_obs_monitor.currentText() if not watch_dir or not output_dir: QtWidgets.QMessageBox.warning(self, "Errore", "Devi specificare sia la cartella da monitorare che la cartella output.") return False watch_dir = Path(watch_dir) output_dir = Path(output_dir) if not watch_dir.is_dir(): QtWidgets.QMessageBox.warning(self, "Errore", f"La cartella da monitorare non esiste:\n{watch_dir}") return False output_dir.mkdir(parents=True, exist_ok=True) use_auto = self.check_auto_name_monitor.isChecked() # Define bridge function def gui_callback(path_obj, message, success): self.monitor_signal.emit(path_obj, message, success) # Initialize the Handler using the DAEMON CLASS (Subclassed) self.watch_handler = GuiFitsHandler( callback=gui_callback, output_dir=str(output_dir), observatory_config=observatory, auto_filename=use_auto ) self.observer = Observer() self.observer.schedule(self.watch_handler, str(watch_dir), recursive=False) self.observer.start() self.log_monitor(f"Monitoraggio avviato su {watch_dir} → output {output_dir} (obs={observatory}, auto_name={use_auto})") return True def stop_watch(self): if self.observer is not None: self.log_monitor("Interruzione monitoraggio...") self.observer.stop() self.observer.join(timeout=5) self.observer = None self.watch_handler = None self.log_monitor("Monitoraggio fermato.") def _on_monitor_update(self, path: Path, msg: str, success: bool): """Slot to update UI from background thread events""" self.log_monitor(msg) if success: self.statusBar().showMessage(f"Ultimo convertito: {path.name}", 5000) else: self.statusBar().showMessage(f"Errore su: {path.name}", 5000) def browse_output_dir_manual(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella output", self.edit_output_dir_manual.text()) if d: self.edit_output_dir_manual.setText(d) def add_files(self): files, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Seleziona file FITS", "", "FITS files (*.fits *.fit);;Tutti i file (*.*)") for f in files: items = self.list_files.findItems(f, QtCore.Qt.MatchExactly) if not items: self.list_files.addItem(f) def remove_selected_files(self): for item in self.list_files.selectedItems(): row = self.list_files.row(item) self.list_files.takeItem(row) def log_manual(self, msg: str): ts = QtCore.QDateTime.currentDateTime().toString("dd-MM-yyyy hh:mm:ss") self.text_log_manual.appendPlainText(f"[{ts}] {msg}") self.text_log_manual.verticalScrollBar().setValue(self.text_log_manual.verticalScrollBar().maximum()) def start_manual_conversion(self): if self.list_files.count() == 0: QtWidgets.QMessageBox.information(self, "Nessun file", "Aggiungi almeno un file da convertire.") return output_dir = self.edit_output_dir_manual.text().strip() if not output_dir: QtWidgets.QMessageBox.warning(self, "Errore", "Devi specificare la cartella output.") return output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) files = [self.list_files.item(i).text() for i in range(self.list_files.count())] observatory = self.combo_obs_manual.currentText() use_auto = self.check_auto_name_manual.isChecked() self.btn_start_manual.setEnabled(False) self.progress_bar.setValue(0) self.log_manual(f"Avvio conversione di {len(files)} file → {output_dir} (obs={observatory}, auto_name={use_auto})") self.manual_thread = QtCore.QThread() self.manual_worker = ConversionWorker(files, output_dir, observatory, use_auto) self.manual_worker.moveToThread(self.manual_thread) self.manual_thread.started.connect(self.manual_worker.run) self.manual_worker.finished.connect(self.manual_thread.quit) self.manual_worker.finished.connect(self.manual_worker.deleteLater) self.manual_thread.finished.connect(self.manual_thread.deleteLater) self.manual_worker.progress.connect(self.on_manual_progress) self.manual_worker.message.connect(self.log_manual) self.manual_worker.finished.connect(self.on_manual_finished) self.manual_thread.start() @QtCore.pyqtSlot(int, str) def on_manual_progress(self, percent: int, current_file: str): self.progress_bar.setValue(percent) if current_file: self.statusBar().showMessage(f"Conversione: {current_file} ({percent}%)") @QtCore.pyqtSlot() def on_manual_finished(self): self.btn_start_manual.setEnabled(True) self.statusBar().showMessage("Conversione completata", 5000) self.log_manual("Conversione manuale completata.") def run(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("Noche FITS Converter") QtWidgets.QApplication.setStyle("Fusion") w = MainWindow() w.show() sys.exit(app.exec_()) if __name__ == "__main__": run() pyproject.toml +2 −1 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "astropy>=7.2", "numpy>=2.3.5", "pyqt5", "watchdog>=6.0.0" ] Loading @@ -36,7 +37,7 @@ dependencies = [ [project.scripts] noche-daemon = "noche.daemon:run" noche-gui = "noche.gui:run" [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) Loading setup.py +7 −6 Original line number Diff line number Diff line Loading @@ -7,15 +7,16 @@ setup( author='Davide Ricci', packages=find_packages(), include_package_data=True, install_requires=[ 'numpy', 'astropy', 'watchdog' ], classifiers=[ 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], python_requires='>=3.12', # install_requires=[ # 'numpy>=2.3.5', # 'astropy>=7.2.0', # 'pyqt5', # 'watchdog' # ], # python_requires='>=3.12', ) Loading
noche/daemon.py +72 −44 Original line number Diff line number Diff line Loading @@ -9,6 +9,46 @@ from astropy import log from noche import Noche def convert_single_file(input_path: Path, output_dir: Path, observatory_config, header_template=None, use_auto_filename=False) -> Path: """ Processes a single FITS file using Noche. Returns ------- Path The path to the written output file. """ # Ensure paths are Path objects input_path = Path(input_path) output_dir = Path(output_dir) noche = Noche(header_template_path=header_template) if isinstance(observatory_config, Path) and observatory_config.is_file(): noche.load_observatory(path=str(observatory_config), fits_file=input_path) elif isinstance(observatory_config, str): noche.load_noctis_observatory(name=observatory_config, fits_file=input_path) else: log.warning("No observatory configuration specified. Header will be built from template only.") if use_auto_filename: # Generate filename based on header data prefix = "RAW" # Use .get with defaults to prevent crashes if keys are missing tel = noche.header.get("TELESCOP", "UNKNOWN").replace(" ", "_").upper() timestamp = noche.header.get("DATE-OBS", "UNKNOWN").replace(":", "_") output_filename = f"{prefix}.{tel}.{timestamp}.fits" else: output_filename = f"NOCTIS.{input_path.name}" output_path = output_dir / output_filename noche.write_noctis_fits(filename=str(output_path), overwrite=True) return output_path def load_daemon_config(config_file): """ Loads daemon configuration from an INI file. Loading Loading @@ -88,9 +128,6 @@ class FitsFileHandler(FileSystemEventHandler): self.output_dir.mkdir(parents=True, exist_ok=True) self.noche = Noche(header_template_path=self.header_template) def on_created(self, event): """ Called when a file or directory is created. Loading @@ -109,12 +146,13 @@ class FitsFileHandler(FileSystemEventHandler): if file_path not in self.processed_files: log.info(f"Detected new file: {file_path.name}") self.processed_files.add(file_path) # Add a small delay to ensure file write is complete time.sleep(0.5) try: self.process_fits_file(file_path) except Exception as e: log.error(f"Error processing {file_path.name}: {e}") def process_fits_file(self, input_fits_path: Path): """ Processes a single FITS file by loading it into Noche, applying Loading @@ -125,36 +163,27 @@ class FitsFileHandler(FileSystemEventHandler): input_fits_path : Path The path to the input FITS file. """ log.info(f"Processing: {input_fits_path.name}") try: if isinstance(self.observatory_config, Path) and self.observatory_config.is_file(): self.noche.load_observatory(path=str(self.observatory_config), fits_file=input_fits_path) elif isinstance(self.observatory_config, str): self.noche.load_noctis_observatory(name=self.observatory_config, fits_file=input_fits_path) else: log.warning("No observatory configuration specified. Header will be built from template only.") output_path = convert_single_file( input_path=input_fits_path, output_dir=self.output_dir, observatory_config=self.observatory_config, header_template=self.header_template, use_auto_filename=self.auto_filename ) log.info(f"Successfully processed and saved: {output_path.name}") return output_path except Exception as e: log.error(f"Failed to load observatory configuration: {e}") log.error(f"Failed to convert {input_fits_path.name}: {e}") raise e try: if self.auto_filename: # Have to double from noche.py as there is also the directory log.info(f"AUTO filename in the form: RAW.telescop_key.time_stamp.fits") prefix = "RAW" tel = self.noche.header["TELESCOP"].replace(" ","_").upper() timestamp = self.noche.header["DATE-OBS"].replace(":","_") output_filename = self.output_dir / f"{prefix}.{tel}.{timestamp}.fits" else: output_filename = self.output_dir / f"processed_{input_fits_path.name}" log.info(f"Writing processed file to: {output_filename}") self.noche.write_noctis_fits(filename=str(output_filename), overwrite=True) log.info(f"Successfully processed and saved: {output_filename.name}") except Exception as e: log.error(f"An unexpected error occurred processing {input_fits_path.name}: {e}") def stop_daemon(observer): if observer: observer.stop() observer.join(timeout=5) def start_daemon(daemon_settings): Loading @@ -178,8 +207,6 @@ def start_daemon(daemon_settings): log.info("Starting Noche daemon.") log.info(f"Monitoring directory: {input_dir.resolve()}") log.info(f"Output directory: {output_dir.resolve()}") log.info(f"Observatory Configuration: {obs_config}") log.info(f"Header Template: {header_template}") event_handler = FitsFileHandler( output_dir=str(output_dir), Loading @@ -196,27 +223,28 @@ def start_daemon(daemon_settings): while True: time.sleep(1) except KeyboardInterrupt: log.info("Shutting down Noche daemon...") observer.stop() log.warning("Shutting down Noche daemon...") stop_daemon(observer) except Exception as e: log.error(f"Daemon encountered an unhandled exception: {e}") observer.stop() stop_daemon(observer) finally: observer.join() log.info("Noche daemon stopped.") def run(): try: daemon_settings = load_daemon_config(sys.argv[1]) except IndexError: log.warning("Missing config file, trying default") # Ensure we can find the default config relative to this file default_daemon_config = Path(__file__).parent / "config" / "daemon_config.ini" if not default_daemon_config.exists(): log.error("Default config not found.") sys.exit(1) daemon_settings = load_daemon_config(default_daemon_config) except SystemExit: log.error("Missing config file") log.error("Configuration error.") sys.exit(1) start_daemon(daemon_settings) Loading
noche/gui.py 0 → 100644 +387 −0 Original line number Diff line number Diff line import sys from pathlib import Path from typing import Optional from PyQt5 import QtCore, QtGui, QtWidgets from watchdog.observers import Observer from noche import Noche from noche.daemon2 import convert_single_file, FitsFileHandler # SUPPORTED_OBS = ["abobservatory", "ogg", "grt", "opc", "oarpaf", "ossfoligno"] SUPPORTED_OBS = Noche().noctis_observatories class ConversionWorker(QtCore.QObject): """ Worker for Manual Conversion Tab. Uses convert_single_file directly from daemon module. """ progress = QtCore.pyqtSignal(int, str) finished = QtCore.pyqtSignal() message = QtCore.pyqtSignal(str) def __init__(self, files, output_dir, observatory, use_auto_name=True): super().__init__() self.files = [Path(f) for f in files] self.output_dir = Path(output_dir) self.observatory = observatory self.use_auto_name = use_auto_name self._abort = False @QtCore.pyqtSlot() def run(self): total = len(self.files) if total == 0: self.progress.emit(0, "") self.finished.emit() return for idx, f in enumerate(self.files, start=1): if self._abort: break try: self.message.emit(f"Conversione di {f.name}...") # USE DAEMON FUNCTION out = convert_single_file( input_path=f, output_dir=self.output_dir, observatory_config=self.observatory, use_auto_filename=self.use_auto_name ) self.message.emit(f"OK -> {out.name}") except Exception as e: self.message.emit(f"ERRORE su {f.name}: {e!r}") percent = int(idx * 100 / total) self.progress.emit(percent, str(f)) self.finished.emit() def abort(self): self._abort = True class GuiFitsHandler(FitsFileHandler): """ Inherits from the Daemon's FitsFileHandler to reuse logic, but bridges the Thread gap to update the GUI via a callback/signal. """ def __init__(self, callback, *args, **kwargs): super().__init__(*args, **kwargs) self.callback = callback def process_fits_file(self, input_fits_path: Path): """ Override the processing method to capture result and notify GUI. """ try: # Call the daemon's original logic output_path = super().process_fits_file(input_fits_path) # Notify Success msg = f"Convertito: {input_fits_path.name} → {output_path.name}" self.callback(input_fits_path, msg, True) return output_path except Exception as e: # Notify Error msg = f"ERRORE su {input_fits_path.name}: {e!r}" self.callback(input_fits_path, msg, False) raise e class MainWindow(QtWidgets.QMainWindow): # Signal to bridge background thread -> Main GUI thread monitor_signal = QtCore.pyqtSignal(object, str, bool) def __init__(self): super().__init__() self.setWindowTitle("Noche FITS Converter") self.resize(900, 600) self.setWindowIcon(QtGui.QIcon()) self.settings = QtCore.QSettings("ABObservatory", "NocheGuiTool") self.observer: Optional[Observer] = None self.watch_handler: Optional[GuiFitsHandler] = None # Connect signal self.monitor_signal.connect(self._on_monitor_update) self._build_ui() self._load_settings() def _build_ui(self): tabs = QtWidgets.QTabWidget() self.setCentralWidget(tabs) self.tab_monitor = QtWidgets.QWidget() tabs.addTab(self.tab_monitor, "Monitor cartella") self._build_tab_monitor() self.tab_manual = QtWidgets.QWidget() tabs.addTab(self.tab_manual, "Conversione manuale") self._build_tab_manual() self.statusBar().showMessage("Pronto") def _build_tab_monitor(self): layout = QtWidgets.QVBoxLayout(self.tab_monitor) form = QtWidgets.QFormLayout() self.combo_obs_monitor = QtWidgets.QComboBox() self.combo_obs_monitor.addItems(SUPPORTED_OBS) form.addRow("Osservatorio NOCTIS:", self.combo_obs_monitor) folder_layout = QtWidgets.QHBoxLayout() self.edit_watch_dir = QtWidgets.QLineEdit() btn_browse_watch = QtWidgets.QPushButton("Sfoglia...") btn_browse_watch.clicked.connect(self.browse_watch_dir) folder_layout.addWidget(self.edit_watch_dir) folder_layout.addWidget(btn_browse_watch) form.addRow("Cartella da monitorare:", folder_layout) out_layout = QtWidgets.QHBoxLayout() self.edit_output_dir_monitor = QtWidgets.QLineEdit() btn_browse_out_mon = QtWidgets.QPushButton("Sfoglia...") btn_browse_out_mon.clicked.connect(self.browse_output_dir_monitor) out_layout.addWidget(self.edit_output_dir_monitor) out_layout.addWidget(btn_browse_out_mon) form.addRow("Cartella output:", out_layout) self.check_auto_name_monitor = QtWidgets.QCheckBox("Usa nome automatico NOCTIS (RAW.TEL.DATE-OBS.fits)") self.check_auto_name_monitor.setChecked(True) form.addRow("", self.check_auto_name_monitor) layout.addLayout(form) h = QtWidgets.QHBoxLayout() self.btn_toggle_watch = QtWidgets.QPushButton("Avvia monitoraggio") self.btn_toggle_watch.setCheckable(True) self.btn_toggle_watch.clicked.connect(self.toggle_watch) self.label_watch_status = QtWidgets.QLabel("Stato: FERMO") self.label_watch_status.setStyleSheet("color: red; font-weight: bold;") h.addWidget(self.btn_toggle_watch) h.addStretch() h.addWidget(self.label_watch_status) layout.addLayout(h) self.text_log_monitor = QtWidgets.QPlainTextEdit() self.text_log_monitor.setReadOnly(True) self.text_log_monitor.setMinimumHeight(250) layout.addWidget(self.text_log_monitor) layout.addStretch() def _build_tab_manual(self): layout = QtWidgets.QVBoxLayout(self.tab_manual) form = QtWidgets.QFormLayout() self.combo_obs_manual = QtWidgets.QComboBox() self.combo_obs_manual.addItems(SUPPORTED_OBS) form.addRow("Osservatorio NOCTIS:", self.combo_obs_manual) out_layout = QtWidgets.QHBoxLayout() self.edit_output_dir_manual = QtWidgets.QLineEdit() btn_browse_out = QtWidgets.QPushButton("Sfoglia...") btn_browse_out.clicked.connect(self.browse_output_dir_manual) out_layout.addWidget(self.edit_output_dir_manual) out_layout.addWidget(btn_browse_out) form.addRow("Cartella output:", out_layout) self.check_auto_name_manual = QtWidgets.QCheckBox("Usa nome automatico NOCTIS (RAW.TEL.DATE-OBS.fits)") self.check_auto_name_manual.setChecked(True) form.addRow("", self.check_auto_name_manual) layout.addLayout(form) files_group = QtWidgets.QGroupBox("File da convertire") files_layout = QtWidgets.QVBoxLayout(files_group) self.list_files = QtWidgets.QListWidget() files_layout.addWidget(self.list_files) btns_layout = QtWidgets.QHBoxLayout() btn_add = QtWidgets.QPushButton("Aggiungi file...") btn_add.clicked.connect(self.add_files) btn_remove = QtWidgets.QPushButton("Rimuovi selezionati") btn_remove.clicked.connect(self.remove_selected_files) btn_clear = QtWidgets.QPushButton("Svuota lista") btn_clear.clicked.connect(self.list_files.clear) btns_layout.addWidget(btn_add) btns_layout.addWidget(btn_remove) btns_layout.addWidget(btn_clear) btns_layout.addStretch() files_layout.addLayout(btns_layout) layout.addWidget(files_group) bottom_layout = QtWidgets.QHBoxLayout() self.progress_bar = QtWidgets.QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) self.btn_start_manual = QtWidgets.QPushButton("Avvia conversione") self.btn_start_manual.clicked.connect(self.start_manual_conversion) bottom_layout.addWidget(self.progress_bar, stretch=1) bottom_layout.addWidget(self.btn_start_manual) layout.addLayout(bottom_layout) self.text_log_manual = QtWidgets.QPlainTextEdit() self.text_log_manual.setReadOnly(True) self.text_log_manual.setMinimumHeight(200) layout.addWidget(self.text_log_manual) layout.addStretch() def _load_settings(self): last_watch = self.settings.value("watch_dir", "", str) last_out_mon = self.settings.value("output_dir_monitor", "", str) last_out_man = self.settings.value("output_dir_manual", "", str) last_obs = self.settings.value("observatory", "abobservatory", str) idx_monitor = self.combo_obs_monitor.findText(last_obs) if idx_monitor >= 0: self.combo_obs_monitor.setCurrentIndex(idx_monitor) idx_manual = self.combo_obs_manual.findText(last_obs) if idx_manual >= 0: self.combo_obs_manual.setCurrentIndex(idx_manual) self.edit_watch_dir.setText(last_watch) self.edit_output_dir_monitor.setText(last_out_mon) self.edit_output_dir_manual.setText(last_out_man) def closeEvent(self, event: QtGui.QCloseEvent): self.settings.setValue("watch_dir", self.edit_watch_dir.text()) self.settings.setValue("output_dir_monitor", self.edit_output_dir_monitor.text()) self.settings.setValue("output_dir_manual", self.edit_output_dir_manual.text()) self.settings.setValue("observatory", self.combo_obs_monitor.currentText()) self.stop_watch() super().closeEvent(event) def browse_watch_dir(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella da monitorare", self.edit_watch_dir.text()) if d: self.edit_watch_dir.setText(d) def browse_output_dir_monitor(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella output", self.edit_output_dir_monitor.text()) if d: self.edit_output_dir_monitor.setText(d) def log_monitor(self, msg: str): ts = QtCore.QDateTime.currentDateTime().toString("yyyy-MM-dd hh:mm:ss") self.text_log_monitor.appendPlainText(f"[{ts}] {msg}") self.text_log_monitor.verticalScrollBar().setValue(self.text_log_monitor.verticalScrollBar().maximum()) def toggle_watch(self, checked: bool): if checked: if self.start_watch(): self.btn_toggle_watch.setText("Ferma monitoraggio") self.label_watch_status.setText("Stato: IN ESECUZIONE") self.label_watch_status.setStyleSheet("color: green; font-weight: bold;") else: self.btn_toggle_watch.setChecked(False) else: self.stop_watch() self.btn_toggle_watch.setText("Avvia monitoraggio") self.label_watch_status.setText("Stato: FERMO") self.label_watch_status.setStyleSheet("color: red; font-weight: bold;") def start_watch(self) -> bool: if self.observer is not None: return True watch_dir = self.edit_watch_dir.text().strip() output_dir = self.edit_output_dir_monitor.text().strip() observatory = self.combo_obs_monitor.currentText() if not watch_dir or not output_dir: QtWidgets.QMessageBox.warning(self, "Errore", "Devi specificare sia la cartella da monitorare che la cartella output.") return False watch_dir = Path(watch_dir) output_dir = Path(output_dir) if not watch_dir.is_dir(): QtWidgets.QMessageBox.warning(self, "Errore", f"La cartella da monitorare non esiste:\n{watch_dir}") return False output_dir.mkdir(parents=True, exist_ok=True) use_auto = self.check_auto_name_monitor.isChecked() # Define bridge function def gui_callback(path_obj, message, success): self.monitor_signal.emit(path_obj, message, success) # Initialize the Handler using the DAEMON CLASS (Subclassed) self.watch_handler = GuiFitsHandler( callback=gui_callback, output_dir=str(output_dir), observatory_config=observatory, auto_filename=use_auto ) self.observer = Observer() self.observer.schedule(self.watch_handler, str(watch_dir), recursive=False) self.observer.start() self.log_monitor(f"Monitoraggio avviato su {watch_dir} → output {output_dir} (obs={observatory}, auto_name={use_auto})") return True def stop_watch(self): if self.observer is not None: self.log_monitor("Interruzione monitoraggio...") self.observer.stop() self.observer.join(timeout=5) self.observer = None self.watch_handler = None self.log_monitor("Monitoraggio fermato.") def _on_monitor_update(self, path: Path, msg: str, success: bool): """Slot to update UI from background thread events""" self.log_monitor(msg) if success: self.statusBar().showMessage(f"Ultimo convertito: {path.name}", 5000) else: self.statusBar().showMessage(f"Errore su: {path.name}", 5000) def browse_output_dir_manual(self): d = QtWidgets.QFileDialog.getExistingDirectory(self, "Seleziona cartella output", self.edit_output_dir_manual.text()) if d: self.edit_output_dir_manual.setText(d) def add_files(self): files, _ = QtWidgets.QFileDialog.getOpenFileNames(self, "Seleziona file FITS", "", "FITS files (*.fits *.fit);;Tutti i file (*.*)") for f in files: items = self.list_files.findItems(f, QtCore.Qt.MatchExactly) if not items: self.list_files.addItem(f) def remove_selected_files(self): for item in self.list_files.selectedItems(): row = self.list_files.row(item) self.list_files.takeItem(row) def log_manual(self, msg: str): ts = QtCore.QDateTime.currentDateTime().toString("dd-MM-yyyy hh:mm:ss") self.text_log_manual.appendPlainText(f"[{ts}] {msg}") self.text_log_manual.verticalScrollBar().setValue(self.text_log_manual.verticalScrollBar().maximum()) def start_manual_conversion(self): if self.list_files.count() == 0: QtWidgets.QMessageBox.information(self, "Nessun file", "Aggiungi almeno un file da convertire.") return output_dir = self.edit_output_dir_manual.text().strip() if not output_dir: QtWidgets.QMessageBox.warning(self, "Errore", "Devi specificare la cartella output.") return output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) files = [self.list_files.item(i).text() for i in range(self.list_files.count())] observatory = self.combo_obs_manual.currentText() use_auto = self.check_auto_name_manual.isChecked() self.btn_start_manual.setEnabled(False) self.progress_bar.setValue(0) self.log_manual(f"Avvio conversione di {len(files)} file → {output_dir} (obs={observatory}, auto_name={use_auto})") self.manual_thread = QtCore.QThread() self.manual_worker = ConversionWorker(files, output_dir, observatory, use_auto) self.manual_worker.moveToThread(self.manual_thread) self.manual_thread.started.connect(self.manual_worker.run) self.manual_worker.finished.connect(self.manual_thread.quit) self.manual_worker.finished.connect(self.manual_worker.deleteLater) self.manual_thread.finished.connect(self.manual_thread.deleteLater) self.manual_worker.progress.connect(self.on_manual_progress) self.manual_worker.message.connect(self.log_manual) self.manual_worker.finished.connect(self.on_manual_finished) self.manual_thread.start() @QtCore.pyqtSlot(int, str) def on_manual_progress(self, percent: int, current_file: str): self.progress_bar.setValue(percent) if current_file: self.statusBar().showMessage(f"Conversione: {current_file} ({percent}%)") @QtCore.pyqtSlot() def on_manual_finished(self): self.btn_start_manual.setEnabled(True) self.statusBar().showMessage("Conversione completata", 5000) self.log_manual("Conversione manuale completata.") def run(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("Noche FITS Converter") QtWidgets.QApplication.setStyle("Fusion") w = MainWindow() w.show() sys.exit(app.exec_()) if __name__ == "__main__": run()
pyproject.toml +2 −1 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "astropy>=7.2", "numpy>=2.3.5", "pyqt5", "watchdog>=6.0.0" ] Loading @@ -36,7 +37,7 @@ dependencies = [ [project.scripts] noche-daemon = "noche.daemon:run" noche-gui = "noche.gui:run" [tool.setuptools.packages.find] where = ["."] # list of folders that contain the packages (["."] by default) Loading
setup.py +7 −6 Original line number Diff line number Diff line Loading @@ -7,15 +7,16 @@ setup( author='Davide Ricci', packages=find_packages(), include_package_data=True, install_requires=[ 'numpy', 'astropy', 'watchdog' ], classifiers=[ 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], python_requires='>=3.12', # install_requires=[ # 'numpy>=2.3.5', # 'astropy>=7.2.0', # 'pyqt5', # 'watchdog' # ], # python_requires='>=3.12', )