From 97ed8217bf2a2071264d4d2ff70a677ccce46a04 Mon Sep 17 00:00:00 2001 From: Vic Sergeev Date: Thu, 7 May 2026 21:13:00 +0300 Subject: [PATCH] working logic+working watching files+added calibration feature+instructions --- .idea/workspace.xml | 23 +- astro_settings.json | 3 + models/camera_profile.py | 106 +++ models/equipment.py | 35 + services/__pycache__/__init__.cpython-313.pyc | Bin 461 -> 461 bytes .../calibration_service.cpython-313.pyc | Bin 0 -> 5830 bytes .../config_service.cpython-313.pyc | Bin 11091 -> 12601 bytes .../__pycache__/file_service.cpython-313.pyc | Bin 5136 -> 7032 bytes .../session_service.cpython-313.pyc | Bin 8823 -> 9011 bytes .../__pycache__/watch_service.cpython-313.pyc | Bin 6100 -> 7653 bytes services/calibration_service.py | 113 +++ services/config_service.py | 149 ++-- services/file_service.py | 48 +- services/session_service.py | 14 +- services/watch_service.py | 24 +- ui/__pycache__/main_window.cpython-313.pyc | Bin 37831 -> 39354 bytes ui/dialogs/__init__.py | 5 +- .../__pycache__/__init__.cpython-313.pyc | Bin 424 -> 589 bytes .../calibration_dialog.cpython-313.pyc | Bin 0 -> 14819 bytes .../calibration_type_dialog.cpython-313.pyc | Bin 0 -> 40397 bytes .../equipment_dialog.cpython-313.pyc | Bin 12877 -> 22678 bytes ui/dialogs/calibration_dialog.py | 281 +++++++ ui/dialogs/calibration_type_dialog.py | 730 ++++++++++++++++++ ui/dialogs/equipment_dialog.py | 363 ++++++--- ui/main_window.py | 41 +- 25 files changed, 1743 insertions(+), 192 deletions(-) create mode 100644 models/camera_profile.py create mode 100644 models/equipment.py create mode 100644 services/__pycache__/calibration_service.cpython-313.pyc create mode 100644 services/calibration_service.py create mode 100644 ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc create mode 100644 ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc create mode 100644 ui/dialogs/calibration_dialog.py create mode 100644 ui/dialogs/calibration_type_dialog.py diff --git a/.idea/workspace.xml b/.idea/workspace.xml index bec30ab..137820f 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,27 +2,20 @@ - + + + + + - - - - - - - + - - - - - @@ -90,6 +83,6 @@ \ No newline at end of file diff --git a/astro_settings.json b/astro_settings.json index fd39922..6562295 100644 --- a/astro_settings.json +++ b/astro_settings.json @@ -9,6 +9,9 @@ "Юпитер-21м", "Tamron 18-200mm" ], + "telescopes": [ + "Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)" + ], "last_watch_folder": "C:/Users/Juliette/Documents/testwatcher", "last_camera": "Canon 40D", "last_lens": "MTO-500A" diff --git a/models/camera_profile.py b/models/camera_profile.py new file mode 100644 index 0000000..1b893d5 --- /dev/null +++ b/models/camera_profile.py @@ -0,0 +1,106 @@ +from dataclasses import dataclass, field +from typing import List, Dict +import json +from pathlib import Path + + +@dataclass +class ExposureProfile: + """Профиль выдержки для определённого ISO""" + iso: int + exposure_seconds: int + dark_count: int = 20 + flat_count: int = 30 + bias_count: int = 50 + + +@dataclass +class LensProfile: + """Профиль объектива""" + name: str + aperture: str # например "f/2.8" + flat_duration_minutes: int = 10 # когда снимать Flat (рассвет/закат) + + +@dataclass +class CameraProfile: + """Профиль камеры""" + name: str # "Canon EOS 600D" + sensor_type: str = "APS-C" # APS-C, Full Frame + pixel_size_um: float = 4.3 + read_noise_e: float = 2.5 + + # Настройки по умолчанию + default_iso: int = 800 + default_exposure: int = 120 + + # Профили выдержек + exposures: List[ExposureProfile] = field(default_factory=list) + + # Объективы + lenses: List[LensProfile] = field(default_factory=list) + + def save(self, config_service): + """Сохраняет профиль в конфиг""" + config_service.save_camera_profile(self) + + def to_dict(self) -> dict: + return { + 'name': self.name, + 'sensor_type': self.sensor_type, + 'pixel_size_um': self.pixel_size_um, + 'read_noise_e': self.read_noise_e, + 'default_iso': self.default_iso, + 'default_exposure': self.default_exposure, + 'exposures': [ + { + 'iso': e.iso, + 'exposure_seconds': e.exposure_seconds, + 'dark_count': e.dark_count, + 'flat_count': e.flat_count, + 'bias_count': e.bias_count + } + for e in self.exposures + ], + 'lenses': [ + { + 'name': l.name, + 'aperture': l.aperture, + 'flat_duration_minutes': l.flat_duration_minutes + } + for l in self.lenses + ] + } + + @classmethod + def from_dict(cls, data: dict) -> 'CameraProfile': + exposures = [ + ExposureProfile( + iso=e['iso'], + exposure_seconds=e['exposure_seconds'], + dark_count=e.get('dark_count', 20), + flat_count=e.get('flat_count', 30), + bias_count=e.get('bias_count', 50) + ) + for e in data.get('exposures', []) + ] + + lenses = [ + LensProfile( + name=l['name'], + aperture=l['aperture'], + flat_duration_minutes=l.get('flat_duration_minutes', 10) + ) + for l in data.get('lenses', []) + ] + + return cls( + name=data['name'], + sensor_type=data.get('sensor_type', 'APS-C'), + pixel_size_um=data.get('pixel_size_um', 4.3), + read_noise_e=data.get('read_noise_e', 2.5), + default_iso=data.get('default_iso', 800), + default_exposure=data.get('default_exposure', 120), + exposures=exposures, + lenses=lenses + ) \ No newline at end of file diff --git a/models/equipment.py b/models/equipment.py new file mode 100644 index 0000000..11fc4b1 --- /dev/null +++ b/models/equipment.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from typing import List, Optional +from enum import Enum + + +class EquipmentType(Enum): + LENS = "lens" + TELESCOPE = "telescope" + + +@dataclass +class Lens: + """Объектив""" + name: str + min_aperture: float # например 1.8 + max_aperture: float # например 22 + focal_length: int # например 50 + + +@dataclass +class Telescope: + """Телескоп""" + name: str + aperture_ratio: float # f/5, f/7, f/10 + focal_length: int # в мм + diameter: int # в мм + + +@dataclass +class Camera: + """Камера""" + name: str + sensor_size: str # "APS-C", "Full Frame", "4/3" + pixel_size_um: float = 4.3 + default_iso: int = 800 \ No newline at end of file diff --git a/services/__pycache__/__init__.cpython-313.pyc b/services/__pycache__/__init__.cpython-313.pyc index d667db5272d02644dca5c9071082be6e5aed1333..24f89071f1bf7e91dd1bef679e46879896a23b86 100644 GIT binary patch delta 19 ZcmX@he3qH(GcPX}0}!k`xRL8HBLF!f1+V}B delta 19 ZcmX@he3qH(GcPX}0}u%3Z{#}62mmwY1j7IT diff --git a/services/__pycache__/calibration_service.cpython-313.pyc b/services/__pycache__/calibration_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2256207dea692af228b00a6b8c7ddfa1357a2d2e GIT binary patch literal 5830 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 literal 0 HcmV?d00001 diff --git a/services/__pycache__/config_service.cpython-313.pyc b/services/__pycache__/config_service.cpython-313.pyc index 035f70fe193e5cf506e95949feaf8a423ebbaf16..9bd6f5e9689b5acd1a08c72240e62ebc0c163f67 100644 GIT binary patch literal 12601 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 literal 11091 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 delta 1342 zcmZuxO>7!R6rR~X3@ohqhh1!N96Kg%5-B!rYMfLpkrkUd2R68tAgZW~UD5#60+Fi3 zqEb~UZVx1#i>3C^Ll3^i7Kb>eSaKU#QLCER5@>~@NK)_xN9+NqHEM3TZ!^OCtw}P%_h1>8QJ#AoJF=P z)!0Rkrp3=?<* zoiJU6gXl}sGZ$I0)^wQAxVJb8<(OeT1Yi-yK$J#L%>$A4s29Yj=+T({See2<`Yp;2 z;+&Q)+NH5Wdkn;2)QFxs#8Q+$bZab#i;$LOaap0mkK$ZkwEO6|!{3et;uw zuhINJveed`G6HZ=l#AQ7z^GGWhxQnVd$dsuYEeggoaPHs9i1O6QhgvUYEe3n9Kf5? z#e48I={RKuAINL=WXAU>rZa;PMoa1kJ|J^h-8;HP*s%+60i36OU;zen#c`Yu-LQpgYeQ{#PA-nYuE6?O=lXn2p;yl0tat$JY5w_r&pIA z-h6PgdS-f8jNFbicxyTGRpdK*W%66&5Av$CI}qIC$LcoM^M>GV0ZQ*_@Rl9>xeez- z>4CIi+Kg_)&HRovvd6#Iu(|(fvRLvW@5Vd2_ikU<0=6uMi-6veq;QDd3bB~a?|*PC z0hPy&pCAmQJFelOgA-LQ;h6y2Iacx}RdzmC%oY?2=1K_t?wW+t=%o8cXhVOx7km-o zCRbV^4^o*RFoQv*(>Uk~`phHwW{FcGK$fPE*V_j2pOya$po(V>g~h30ld`a$l|mC> zvv#pV(pEPI(J$gDE>2QTeIo_LS8J(V6Pesc#p|gH%7-W{ P4GNbbbUXtD+Y9;+(klvM diff --git a/services/__pycache__/session_service.cpython-313.pyc b/services/__pycache__/session_service.cpython-313.pyc index 327b811af8815144b9dd81aa109732a30fc3eb42..5f88702dc216b97aedabd3d1b6d687fd5ada5081 100644 GIT binary patch delta 619 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 diff --git a/services/calibration_service.py b/services/calibration_service.py new file mode 100644 index 0000000..1dc46e5 --- /dev/null +++ b/services/calibration_service.py @@ -0,0 +1,113 @@ +""" +CalibrationService - управление съёмкой калибровочных кадров +""" +from pathlib import Path +from datetime import datetime +from typing import Optional, Callable +from PySide6.QtCore import QObject, Signal + +from services.file_service import FileService +from services.watch_service import WatchService + + +class CalibrationService(QObject): + """Сервис для съёмки калибровочных кадров с авто-остановкой""" + + # Сигналы для UI + progress_updated = Signal(int, int) # (current, target) + capture_completed = Signal(str) # тип кадра + capture_error = Signal(str) + + def __init__(self): + super().__init__() + self._watch_service = WatchService() + self._target_count = 0 + self._current_count = 0 + self._calibration_type = None + self._target_folder = None + self._stop_requested = False + + def start_calibration(self, calibration_type: str, target_folder: Path, + camera_name: str, target_count: int, + on_file_received: Callable) -> bool: + """ + Начинает съёмку калибровочных кадров + + calibration_type: 'bias', 'dark', 'flat' + """ + self._calibration_type = calibration_type + self._target_count = target_count + self._current_count = 0 + self._stop_requested = False + + # Создаём папку назначения + self._target_folder = target_folder + self._target_folder.mkdir(parents=True, exist_ok=True) + + # Очищаем папку наблюдения от старых файлов + watch_folder = self._get_watch_folder() + if watch_folder: + FileService.clear_watch_folder(watch_folder) + + # Запускаем отслеживание + def on_file(file_path: Path): + if self._stop_requested: + return + + # Обрабатываем файл + if self._process_calibration_file(file_path, camera_name): + self._current_count += 1 + self.progress_updated.emit(self._current_count, self._target_count) + + if self._current_count >= self._target_count: + self.stop_calibration() + self.capture_completed.emit(calibration_type) + + if on_file_received: + on_file_received(file_path) + + watch_path = Path(".") # Нужно получить реальный путь + return self._watch_service.start(watch_path, on_file) + + def _process_calibration_file(self, file_path: Path, camera_name: str) -> bool: + """Обрабатывает калибровочный файл""" + if not FileService.is_photo(file_path): + return False + + # Генерируем имя файла с типом кадра + timestamp = datetime.now() + suffix = file_path.suffix + + type_prefix = { + 'bias': 'Bias', + 'dark': 'Dark', + 'flat': 'Flat' + }.get(self._calibration_type, 'Calib') + + new_filename = f"{type_prefix}_{camera_name}_{timestamp.strftime('%Y%m%d_%H%M%S')}{suffix}" + + target_path = self._target_folder / new_filename + target_path = FileService.resolve_conflict(target_path) + + try: + import shutil + shutil.move(str(file_path), str(target_path)) + print(f"Калибровочный кадр сохранён: {target_path.name}") + return True + except Exception as e: + print(f"Ошибка сохранения кадра: {e}") + return False + + def stop_calibration(self): + """Останавливает съёмку калибровочных кадров""" + self._stop_requested = True + self._watch_service.stop() + + def _get_watch_folder(self) -> Optional[Path]: + """Возвращает папку наблюдения (нужно из main_window)""" + # TODO: Получить из главного окна + return None + + def get_progress(self) -> tuple: + """Возвращает прогресс (current, target)""" + return (self._current_count, self._target_count) \ No newline at end of file diff --git a/services/config_service.py b/services/config_service.py index 5ada103..a3d6cb9 100644 --- a/services/config_service.py +++ b/services/config_service.py @@ -4,18 +4,6 @@ ConfigService - управление настройками приложения import json import os from typing import List, Optional -from dataclasses import dataclass, asdict - - -@dataclass -class AppConfig: - """Конфигурация приложения""" - cameras: List[str] - lenses: List[str] - celestial_bodies: List[str] - last_watch_folder: str - last_camera: str - last_lens: str class ConfigService: @@ -25,14 +13,15 @@ class ConfigService: CELESTIAL_BODIES_FILE = "celestial_bodies.json" def __init__(self): - self.config = AppConfig( - cameras=[], - lenses=[], - celestial_bodies=[], - last_watch_folder="", - last_camera="", - last_lens="" - ) + self.config = { + 'cameras': [], + 'lenses': [], + 'telescopes': [], + 'celestial_bodies': [], + 'last_watch_folder': '', + 'last_camera': '', + 'last_lens': '' + } self.load_all() def load_all(self): @@ -40,8 +29,9 @@ class ConfigService: self._load_settings() self._load_celestial_bodies() - if not self.config.celestial_bodies: - self.config.celestial_bodies = [ + # Если список небесных тел пуст - добавляем стандартные + if not self.config['celestial_bodies']: + self.config['celestial_bodies'] = [ "M31 (Andromeda Galaxy)", "M42 (Orion Nebula)", "M45 (Pleiades)", @@ -54,108 +44,151 @@ class ConfigService: self._save_celestial_bodies() def _load_settings(self): + """Загружает основные настройки (камеры, объективы, телескопы, последнюю папку)""" if os.path.exists(self.SETTINGS_FILE): try: with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) - self.config.cameras = data.get('cameras', []) - self.config.lenses = data.get('lenses', []) - self.config.last_watch_folder = data.get('last_watch_folder', '') - self.config.last_camera = data.get('last_camera', '') - self.config.last_lens = data.get('last_lens', '') + self.config['cameras'] = data.get('cameras', []) + self.config['lenses'] = data.get('lenses', []) + self.config['telescopes'] = data.get('telescopes', []) + self.config['last_watch_folder'] = data.get('last_watch_folder', '') + self.config['last_camera'] = data.get('last_camera', '') + self.config['last_lens'] = data.get('last_lens', '') except Exception as e: print(f"Ошибка загрузки настроек: {e}") def save_settings(self): + """Сохраняет основные настройки""" try: with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f: json.dump({ - 'cameras': self.config.cameras, - 'lenses': self.config.lenses, - 'last_watch_folder': self.config.last_watch_folder, - 'last_camera': self.config.last_camera, - 'last_lens': self.config.last_lens + 'cameras': self.config['cameras'], + 'lenses': self.config['lenses'], + 'telescopes': self.config['telescopes'], + 'last_watch_folder': self.config['last_watch_folder'], + 'last_camera': self.config['last_camera'], + 'last_lens': self.config['last_lens'] }, f, ensure_ascii=False, indent=2) except Exception as e: print(f"Ошибка сохранения настроек: {e}") def _load_celestial_bodies(self): + """Загружает список небесных тел""" if os.path.exists(self.CELESTIAL_BODIES_FILE): try: with open(self.CELESTIAL_BODIES_FILE, 'r', encoding='utf-8') as f: - self.config.celestial_bodies = json.load(f) + self.config['celestial_bodies'] = json.load(f) except Exception as e: print(f"Ошибка загрузки небесных тел: {e}") def _save_celestial_bodies(self): + """Сохраняет список небесных тел""" try: with open(self.CELESTIAL_BODIES_FILE, 'w', encoding='utf-8') as f: - json.dump(self.config.celestial_bodies, f, ensure_ascii=False, indent=2) + json.dump(self.config['celestial_bodies'], f, ensure_ascii=False, indent=2) except Exception as e: print(f"Ошибка сохранения небесных тел: {e}") + # ===== Методы для работы с камерами ===== + def get_cameras(self) -> List[str]: - return self.config.cameras.copy() + return self.config['cameras'].copy() def add_camera(self, camera: str): - if camera and camera not in self.config.cameras: - self.config.cameras.append(camera) + if camera and camera not in self.config['cameras']: + self.config['cameras'].append(camera) self.save_settings() def remove_camera(self, camera: str): - if camera in self.config.cameras: - self.config.cameras.remove(camera) + if camera in self.config['cameras']: + self.config['cameras'].remove(camera) self.save_settings() + # ===== Методы для работы с объективами ===== + def get_lenses(self) -> List[str]: - return self.config.lenses.copy() + return self.config['lenses'].copy() def add_lens(self, lens: str): - if lens and lens not in self.config.lenses: - self.config.lenses.append(lens) + if lens and lens not in self.config['lenses']: + self.config['lenses'].append(lens) self.save_settings() def remove_lens(self, lens: str): - if lens in self.config.lenses: - self.config.lenses.remove(lens) + if lens in self.config['lenses']: + self.config['lenses'].remove(lens) self.save_settings() + def update_lens(self, old_name: str, new_name: str): + if old_name in self.config['lenses']: + idx = self.config['lenses'].index(old_name) + self.config['lenses'][idx] = new_name + self.save_settings() + + # ===== Методы для работы с телескопами ===== + + def get_telescopes(self) -> List[str]: + return self.config.get('telescopes', []).copy() + + def add_telescope(self, telescope: str): + if 'telescopes' not in self.config: + self.config['telescopes'] = [] + if telescope and telescope not in self.config['telescopes']: + self.config['telescopes'].append(telescope) + self.save_settings() + + def remove_telescope(self, telescope: str): + if 'telescopes' in self.config and telescope in self.config['telescopes']: + self.config['telescopes'].remove(telescope) + self.save_settings() + + def update_telescope(self, old_name: str, new_name: str): + if 'telescopes' in self.config and old_name in self.config['telescopes']: + idx = self.config['telescopes'].index(old_name) + self.config['telescopes'][idx] = new_name + self.save_settings() + + # ===== Методы для работы с небесными телами ===== + def get_celestial_bodies(self) -> List[str]: - return self.config.celestial_bodies.copy() + return self.config['celestial_bodies'].copy() def add_celestial_body(self, name: str): - if name and name not in self.config.celestial_bodies: - self.config.celestial_bodies.append(name) + if name and name not in self.config['celestial_bodies']: + self.config['celestial_bodies'].append(name) self._save_celestial_bodies() def remove_celestial_body(self, name: str): - if name in self.config.celestial_bodies: - self.config.celestial_bodies.remove(name) + if name in self.config['celestial_bodies']: + self.config['celestial_bodies'].remove(name) self._save_celestial_bodies() def update_celestial_body(self, old_name: str, new_name: str): - if old_name in self.config.celestial_bodies: - idx = self.config.celestial_bodies.index(old_name) - self.config.celestial_bodies[idx] = new_name + if old_name in self.config['celestial_bodies']: + idx = self.config['celestial_bodies'].index(old_name) + self.config['celestial_bodies'][idx] = new_name self._save_celestial_bodies() + # ===== Методы для работы с последней папкой и оборудованием ===== + def get_last_watch_folder(self) -> str: - return self.config.last_watch_folder + return self.config.get('last_watch_folder', '') def set_last_watch_folder(self, folder: str): - self.config.last_watch_folder = folder + self.config['last_watch_folder'] = folder self.save_settings() def get_last_camera(self) -> str: - return self.config.last_camera + return self.config.get('last_camera', '') def set_last_camera(self, camera: str): - self.config.last_camera = camera + self.config['last_camera'] = camera self.save_settings() def get_last_lens(self) -> str: - return self.config.last_lens + return self.config.get('last_lens', '') def set_last_lens(self, lens: str): - self.config.last_lens = lens + self.config['last_lens'] = lens self.save_settings() \ No newline at end of file diff --git a/services/file_service.py b/services/file_service.py index 68d7990..c308082 100644 --- a/services/file_service.py +++ b/services/file_service.py @@ -35,6 +35,22 @@ class FileService: return new_path counter += 1 + @classmethod + def generate_new_filename(cls, object_name: str, timestamp: datetime, original_suffix: str) -> str: + """ + Генерирует новое имя файла в формате: + ИмяОбъекта_ГГГГ-ММ-ДД_ЧЧ-ММ-СС.расширение + """ + # Очищаем имя объекта от недопустимых символов + safe_object_name = "".join(c for c in object_name if c.isalnum() or c in (' ', '-', '_')).strip() + safe_object_name = safe_object_name.replace(' ', '_') + + # Форматируем дату и время + date_str = timestamp.strftime("%Y-%m-%d") + time_str = timestamp.strftime("%H-%M-%S") + + return f"{safe_object_name}_{date_str}_{time_str}{original_suffix}" + @classmethod def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str, timestamp: Optional[datetime] = None) -> None: @@ -52,23 +68,45 @@ class FileService: print(f"Ошибка записи лога: {e}") @classmethod - def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str) -> bool: - """Перемещает файл в целевую папку""" + def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str, + object_name: str = None) -> bool: + """ + Перемещает файл в целевую папку с переименованием + Если object_name указан, файл переименовывается в формат: ИмяОбъекта_дата_время.расширение + """ if not source.exists(): + print(f"Файл не существует: {source}") return False if not cls.is_photo(source): + print(f"Неподдерживаемый формат: {source}") return False try: target_folder.mkdir(parents=True, exist_ok=True) + + # Получаем время создания файла creation_time = datetime.fromtimestamp(source.stat().st_ctime) - cls.write_object_log(target_folder, source.name, camera, optics, creation_time) - target_path = cls.resolve_conflict(target_folder / source.name) + + # Генерируем новое имя файла, если указано имя объекта + if object_name: + new_filename = cls.generate_new_filename(object_name, creation_time, source.suffix) + else: + new_filename = source.name + + # Записываем в лог (сохраняем оригинальное имя в логе для отслеживания) + cls.write_object_log(target_folder, f"{source.name} -> {new_filename}", camera, optics, creation_time) + + # Формируем целевой путь + target_path = cls.resolve_conflict(target_folder / new_filename) + + # Перемещаем файл shutil.move(str(source), str(target_path)) + print(f"Файл перемещён и переименован: {source.name} -> {target_path.name}") return True + except Exception as e: - print(f"Ошибка перемещения {source.name}: {e}") + print(f"Ошибка перемещения файла {source.name}: {e}") return False @classmethod diff --git a/services/session_service.py b/services/session_service.py index 9469d2c..cbcfccb 100644 --- a/services/session_service.py +++ b/services/session_service.py @@ -59,7 +59,7 @@ class SessionService: return astro_object def handle_file(self, file_path: Path) -> bool: - """Обрабатывает новый файл""" + """Обрабатывает новый файл: перемещает в папку текущего объекта с переименованием""" if not self._current_session: return False @@ -67,11 +67,13 @@ class SessionService: if not current_object: return False + # Передаём имя объекта для переименования файла success = self._file_service.move_file( file_path, current_object.folder, self._current_session.camera, - self._current_session.optics + self._current_session.optics, + current_object.name # Добавляем имя объекта для переименования ) if success: @@ -92,12 +94,14 @@ class SessionService: if watch_folder.exists(): for file_path in watch_folder.iterdir(): if file_path.is_file() and self._file_service.is_photo(file_path): - if self._file_service.move_file( + success = self._file_service.move_file( file_path, current_object.folder, self._current_session.camera, - self._current_session.optics - ): + self._current_session.optics, + current_object.name # Добавляем имя объекта для переименования + ) + if success: current_object.increment_photo_count() count += 1 if on_file_moved: diff --git a/services/watch_service.py b/services/watch_service.py index 9ef8119..74e687f 100644 --- a/services/watch_service.py +++ b/services/watch_service.py @@ -25,7 +25,17 @@ class PhotoHandler(FileSystemEventHandler): if not event.is_directory: src_path = Path(event.src_path) if FileService.is_photo(src_path): - time.sleep(0.1) # Даём время на запись файла + print(f"[Watchdog] Обнаружен файл: {src_path}") + time.sleep(0.1) + self._pending_files.put(src_path) + + def on_modified(self, event): + """Также обрабатываем modified, так как некоторые программы сначала создают временный файл""" + if not event.is_directory: + src_path = Path(event.src_path) + if FileService.is_photo(src_path): + print(f"[Watchdog] Изменён файл: {src_path}") + time.sleep(0.1) self._pending_files.put(src_path) def _process_queue(self): @@ -52,24 +62,33 @@ class WatchService: self._is_running = False def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool: + """Запускает отслеживание папки""" if self._is_running: + print("Watcher already running") return False if not watch_folder.exists(): + print(f"Папка не существует: {watch_folder}") return False try: + print(f"Запуск отслеживания папки: {watch_folder}") self._event_handler = PhotoHandler(on_new_file) self._observer = Observer() self._observer.schedule(self._event_handler, str(watch_folder), recursive=False) self._observer.start() self._is_running = True + print(f"Отслеживание успешно запущено для: {watch_folder}") return True except Exception as e: print(f"Ошибка запуска отслеживания: {e}") + import traceback + traceback.print_exc() return False def stop(self): + """Останавливает отслеживание""" + print("Остановка отслеживания...") if self._observer: self._observer.stop() self._observer.join() @@ -80,12 +99,13 @@ class WatchService: self._event_handler = None self._is_running = False + print("Отслеживание остановлено") def is_running(self) -> bool: return self._is_running def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int: - """Перемещает все существующие файлы""" + """Перемещает все существующие файлы из папки наблюдения""" count = 0 if watch_folder.exists(): for file_path in watch_folder.iterdir(): diff --git a/ui/__pycache__/main_window.cpython-313.pyc b/ui/__pycache__/main_window.cpython-313.pyc index 223c5b5753e00319d4c4b5ce92fbd7f881a2916c..b85de7acbc7bcf965986e284d7883a02ec639164 100644 GIT binary patch delta 3945 zcmZ`*3viQF7XEKuO_M*b=Fzlio22hh+HRrcVd-KEMO$eLgis!>CA4WNqzU|giUnOt zRkVN$?Vbe{TrKX-0PZ@L{nb?*WF2SKSw#}*lKMO2!1$OIbr;LRIy>s@J%8xKS7!3% z=A7@IbI-l!oO|CqAv+_;^w)Jd4MRVbuYJ?KbNgxi+cNSYh_X6kI>WJEZHum4=4I2F zEee<`FGQi$YiY4jp;Z!YmEWF1h1p&=m*Zoq2$!47c(R4R$lp!U1l0_4nDH^I2voN@ zglDujl^_~tDa}5nLIz>CQ)j6l=$m0&jE{xac3NSD-Ufv!S+Lt=BP<-#yWyHCSr*d4 z!zp`-4v52|_vvTM(!mv-$6!Z`2}>b^&j45SR$bZ*mSM{FLm6hckf}|WGWrbDq%Ovj zE|i(KlZD09(zT}^8FodFMn|KsL|=-Yj_rxQ8vS$h)!4()v#|rw7hKUZRP}spPwYTR zNy#qNk_Z*)3e!(H&A)k%zS$$FYi znW2S#+dQQaHv(R^Rg$1^!*;LCepzjatKBiRdrY14ab|8bZ*_Fd`iq$x;1heUtu9tv zH>R#1M^08uofUPKjj89rwzTKrK5epWH3KVmtnTlUdlbEQP{YwDWA{hT;DTu!qtO>) z_q(Fc)6g)$SEE8Apr42}ws$I4+yV}_*+WFrlIhTvN|7~s6k=v?cS#~nzC?Psq{LCH>Q68T~VGr|(ddd5y z?2+=gXHm?v=%T4=P&KaBAJXpA{#wUXfh##pC{BMgos7WW%7@7jp}C?$qdI~1kcf8_SRl=>gGV-MNEh@kNrTa_89v+2 zLQzdBMDLc?o*zo~B~O_sVi=#2E-jBnp;>zrjdjp7ZqP{6?<_FGc_W+dVwz-96pE>d z4my!pWryk|1yG{XN*;_$B@d>M)~B5!W>6xGI;>EpwDkM5(_7Oem$Gvabs2q>CZa&I z^MUA#x3V)fK+A0}Exb`$a>LQHlZAISIvN{@-7i(-%d`w%x`F3*?Wx~B>$)|1X2{j( zF(&d%+ZBk=GVBR(eqPjcLBrByr>I9^BoGYn?KH)B?i@8Bs{Gq%$#!r6txHR-hf$ z6h+iZ4iYxRKS!I{lIaHmVdsD@*X4qG|Y_)ZqH6Ci(*A!=SV{Goo`Z4ww)Xkv zFjQYddW4Jh7MbZ|D(C5@FfI;rnY#*KFL#mw!QQ}=tbbC8N5RcP#Rvsa!$)=o1K!Pn zK!h4Q)tFDNL$ooCJS1FgbdcGfCHS+EzZPKx;W)w%{d@ssE3(KTVZ{nNN%@L0JSvWM z28P4igqwg}D|3Iew$I_Ym9xlUL0q|(kUp5}o$rvpFlv{WD}LbPx^vP6^C5a?OIn2ZX~u3tmQE>N_%Rf$@FqLy{!4D4;$EPG3Zol{6| zeNL`ag)}}53@a`0ie8gG)ecm&9ca8-FQcFhr^%6^xV-FSUzj4&vYsYF8Eb>{8!faO z>Ez6+LfCFJ!y>;Ku58Q$BWHnhe>S|k(F{#~1FR@2P^{gyY7QLN(c6BDT29*$w6>O+ zbQRd&>Bq%%VfU(l<2hz{!l0eKVSYlvFkZtC`|=E(kxokZ?sH15PPXbc*u2JN>tfu@ zhDDP$k{Ca|*g`CHx8`M*1Sqc6hqu1D%_b4&e<{9fAFUJaP_31LZ0}!C|-> z$Tq)5hoX+3*~>uNnU~6WkkgD{AQb+EBbLCL&N`xpkxsW=>Rw7Nur0V#R3%yz_W@;r z*qM{dCOTT%4u7yWASUs>?d<`c7fFDdMXm9A%5y1QopH7}#ulFpj$eaJj<)u2Zzw|dC1Hib{N`rCA9_5pB$;rbXIw#-a+3QMu|qz; zK$V8-bm-uhS7TV@=sbW{tFBOMi8^aVcCi5C}_g;FPi3&D*bJ#n%@8_95vVMN;Q zP-mFykzS%6_4Hb~N>~=jQsFssC9ow@L2`wW$PW49JJCu3ink+RHi^piV8G9Dcr|kG zA-s!lfx_fNPMXD00j%pQC$+-CzA;kP@O@L#(&|wr*}xDcr%OzZ0oXNf3ucE)a^eg*R;pv`4sNq$a$fI;n^6YTFd<6AjXvI`N=r+Z2Iw`*oxO z&hNLA+u+mvg=D8-IPj8OJ0qvyy@Lg$1paq$E;$S(hmJ0nI*XJV^zF!S(shAP@HwS zpCf#W@Fl`L1gTHn`EV%}HGyP-%|mQE~4Qfekr z8i;b%#N9H5(l)`i%9O2n?1dout?gu{wYM`#b=vAo8td4!Wpo0ri4*ILooSmsNO9UUsXgCa79ErO z$8XR1-rqUj_nkej@RxtUTRsy6Bgek|!(W6)hR#{8@?;tke6y{`yoC3gT-;VI75ruw z2i2MqTaS|w9V#))?_|Uw&68%jAiymrbYUUqD^Px+`JI73V^9v7a`f=FC95d2hM;|p zi<&p{ ztr;?tlDWi!JsfO*sSJMKv=GL&Wx~Z5(nW1RJJ0rVT-T}wCLDC5pa>WHGTi~~gCqj{ zylfB01>hfjPIzvkgAed9*;D|hcDvnCT|hT4ga$k249khJ?ObPI+pLCZ`xf>_l zjn~{we@GMWnlhldW$haKwApre!~P9Z=E6yH;hUY;&5NhS+$phaQY@Pi>nFwfYhuIe z&9|)LiSBE8mD4uIxb=v2%2qyUD?dAW-By3s-CCj{qwscn zMgA#9rN@Up<@5#zZ@XW>_3y9v8HOUW+fY-blC+kkW}El?A@iz=#@ zKe3gJ4egyv=TqqJDkOWAJzXxccpGD|bW{&&c&hf-hW9bN*X?61__uBouRo6VFTnlH z5;?3$J$1Yxj+B3K@Yb_?a*|v|NNA(1G=02UM)Y$I8hTffqgsBvY$LQ!>9m0Q}>n3l4KRC#H^{M7oJ_~|I!*vvb-JA(0^D|*ne-T_7 zallyr0$9_Z34%?4%0o`5+~Lr6ZR@CnOWsWQ#GR%w28@t5P-QpPXqapEd09sVxQ=Ql zuw*VZJ?Qw&HAy}jispd9!d3HM%M+Nc8U|Skm-<|MM;+W4D6%h0DI2(8J)5y6unrdG zSpp{2(7bEE)z7thqUM0PV**`c;K-fAf zc#~)IOXh3zuFgm53>7Qz=Cd@|Hb z1SpLZQbLS=Z*byBr5Wr!}rmHM7G;7T=sd8Kq!$tMWx+*?c7rS5%A zqcx1Fwrf_siY@}QsjE(e$qjH=Mhw$j+b;?_>#rU#E$0>WI*9T4GXOJd{Oh8ZnSH2u%oU5uQT8MCm#PzHBx3mVOsN!&r)ne* zMd!S|G3Ik&3CsA8kR8q3mwcZ};e)Xf(xu!Rdskz6s2<_xM;8$f+&)@O;@~{?-8OYP zEW-H95Y#FCI6(nUM3DqTDhpDf60Akc zK&y&aKm;p@U;`2CK*CRxd*a3{PubE;y%ew^#d^sIbK@amVCA=zaHuH(+5=O_K3SYG rM^_MNFe4BbO9P1y%#4hTcNxsV$o4LS^<4&+Pb`ewmW}L1{6KjCWZ_Fz delta 109 zcmX@hvVxiKGcPX}0}yN}_>=jQVIrRdqrpUVO&)!QV3t6}BDQo^O}2?8PQuJZ%s@p& zEFgjvNcd^8PiADyn!JNCLzNfEX9VJ6VIc8=nURt4E`vE3**@ZOYh*9t28sg!J4+V) diff --git a/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5316e2341588fd61d5bad2347460876ed8a286bc GIT binary patch literal 14819 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 Le#POFPR4%&FVD;# literal 0 HcmV?d00001 diff --git a/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..764ab6402f4ea44749e845af5c6ed8230357f696 GIT binary patch literal 40397 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 Cw}4&% literal 12877 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 X>EAfG%l(nvcFcBq>L$k$4C;RY{6(Wn diff --git a/ui/dialogs/calibration_dialog.py b/ui/dialogs/calibration_dialog.py new file mode 100644 index 0000000..adcf9fc --- /dev/null +++ b/ui/dialogs/calibration_dialog.py @@ -0,0 +1,281 @@ +""" +CalibrationDialog - главный диалог калибровки +С выбором камеры, папки и типа кадров +""" +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, + QLabel, QComboBox, QLineEdit, QPushButton, QFrame, + QMessageBox, QFileDialog, QWidget +) +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QFont + +from services.config_service import ConfigService + + +class CalibrationDialog(QDialog): + """Главное окно калибровки""" + + def __init__(self, parent, config_service: ConfigService): + super().__init__(parent) + + self.config_service = config_service + + self.setWindowTitle("🌑 Калибровочные кадры") + self.setMinimumSize(600, 450) + self.resize(650, 500) + + self._create_ui() + self._load_saved_settings() + + # Таймер для мигания кнопки "Обзор" + self._browse_blink_timer = None + self._check_folder_path() + + def _create_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(20) + layout.setContentsMargins(25, 25, 25, 25) + + # Заголовок + title_label = QLabel("🌑 Калибровочные кадры") + title_font = QFont() + title_font.setPointSize(18) + title_font.setBold(True) + title_label.setFont(title_font) + layout.addWidget(title_label) + + # Основная сетка + grid = QGridLayout() + grid.setVerticalSpacing(15) + grid.setHorizontalSpacing(15) + + # Строка 0: Камера + camera_label = QLabel("📷 Камера:") + camera_label.setFont(QFont("", 10, QFont.Bold)) + grid.addWidget(camera_label, 0, 0) + + self.camera_combo = QComboBox() + self.camera_combo.setEditable(True) + self.camera_combo.setMinimumWidth(250) + grid.addWidget(self.camera_combo, 0, 1) + + # Строка 1: Папка + folder_label = QLabel("📁 Папка:") + folder_label.setFont(QFont("", 10, QFont.Bold)) + grid.addWidget(folder_label, 1, 0) + + folder_widget = QWidget() + folder_layout = QHBoxLayout(folder_widget) + folder_layout.setContentsMargins(0, 0, 0, 0) + folder_layout.setSpacing(10) + + self.folder_entry = QLineEdit() + self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров") + folder_layout.addWidget(self.folder_entry) + + self.browse_button = QPushButton("✨ Обзор") + self.browse_button.setFixedWidth(100) + self.browse_button.clicked.connect(self._browse_folder) + folder_layout.addWidget(self.browse_button) + + grid.addWidget(folder_widget, 1, 1) + + layout.addLayout(grid) + + # Разделитель + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setStyleSheet("background-color: #333333; max-height: 1px;") + layout.addWidget(separator) + + # Кнопки типов кадров + types_layout = QHBoxLayout() + types_layout.setSpacing(20) + types_layout.setAlignment(Qt.AlignCenter) + + self.bias_btn = QPushButton("⚪ BIAS") + self.bias_btn.setFixedSize(120, 50) + self.bias_btn.setStyleSheet(""" + QPushButton { + background-color: #2196F3; + color: white; + font-weight: bold; + font-size: 14px; + border-radius: 6px; + } + QPushButton:hover { + background-color: #1976D2; + } + """) + self.bias_btn.clicked.connect(lambda: self._open_calibration_type('bias')) + + self.dark_btn = QPushButton("🌑 DARK") + self.dark_btn.setFixedSize(120, 50) + self.dark_btn.setStyleSheet(""" + QPushButton { + background-color: #9C27B0; + color: white; + font-weight: bold; + font-size: 14px; + border-radius: 6px; + } + QPushButton:hover { + background-color: #7B1FA2; + } + """) + self.dark_btn.clicked.connect(lambda: self._open_calibration_type('dark')) + + self.flat_btn = QPushButton("📖 FLAT") + self.flat_btn.setFixedSize(120, 50) + self.flat_btn.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + font-size: 14px; + border-radius: 6px; + } + QPushButton:hover { + background-color: #388E3C; + } + """) + self.flat_btn.clicked.connect(lambda: self._open_calibration_type('flat')) + + types_layout.addWidget(self.bias_btn) + types_layout.addWidget(self.dark_btn) + types_layout.addWidget(self.flat_btn) + + layout.addLayout(types_layout) + + # Совет + tips_frame = QFrame() + tips_frame.setStyleSheet(""" + QFrame { + background-color: #2d2d2d; + border-radius: 8px; + padding: 10px; + } + """) + tips_layout = QVBoxLayout(tips_frame) + + tips_title = QLabel("💡 Совет") + tips_title.setFont(QFont("", 11, QFont.Bold)) + tips_layout.addWidget(tips_title) + + self.tips_label = QLabel( + "• BIAS снимаются один раз на месяц (можно дома)\n" + "• DARK снимаются на месте съёмки при той же температуре\n" + "• FLAT снимаются после сессии без изменения фокуса" + ) + self.tips_label.setWordWrap(True) + tips_layout.addWidget(self.tips_label) + + layout.addWidget(tips_frame) + + # Кнопки отмена/закрыть + buttons_layout = QHBoxLayout() + buttons_layout.addStretch() + + cancel_btn = QPushButton("❌ Отмена") + cancel_btn.clicked.connect(self.reject) + buttons_layout.addWidget(cancel_btn) + + layout.addLayout(buttons_layout) + + def _load_saved_settings(self): + """Загружает сохранённые камеры""" + cameras = self.config_service.get_cameras() + if cameras: + self.camera_combo.addItems(cameras) + + last_camera = self.config_service.get_last_camera() + if last_camera and last_camera in cameras: + self.camera_combo.setCurrentText(last_camera) + + def _browse_folder(self): + """Выбор папки для калибровочных кадров""" + folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров") + if folder: + self.folder_entry.setText(folder) + self._stop_browse_blinking() + + def _check_folder_path(self): + """Проверяет, заполнено ли поле пути и запускает мигание если нет""" + if not self.folder_entry.text(): + self._start_browse_blinking() + else: + self._stop_browse_blinking() + + def _start_browse_blinking(self): + """Запускает мигание кнопки 'Обзор' зелёным цветом""" + self._browse_blink_timer = QTimer() + self._browse_blink_timer.timeout.connect(self._do_browse_blink) + self._browse_blink_timer.start(500) + + def _do_browse_blink(self): + """Мигание кнопки""" + current_style = self.browse_button.styleSheet() + if "background-color: #4CAF50" in current_style: + self.browse_button.setStyleSheet(""" + QPushButton { + background-color: #2196F3; + color: white; + font-weight: bold; + border-radius: 4px; + } + """) + else: + self.browse_button.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + border-radius: 4px; + } + """) + + def _stop_browse_blinking(self): + """Останавливает мигание кнопки""" + if self._browse_blink_timer: + self._browse_blink_timer.stop() + self._browse_blink_timer = None + self.browse_button.setStyleSheet(""" + QPushButton { + background-color: #2196F3; + color: white; + font-weight: bold; + border-radius: 4px; + } + """) + + def _open_calibration_type(self, cal_type: str): + """Открывает дочернее окно для выбранного типа калибровки""" + if not self.folder_entry.text(): + QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!") + self._start_browse_blinking() + return + + camera_name = self.camera_combo.currentText() + if not camera_name: + QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!") + return + + from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog + dialog = CalibrationTypeDialog( + self, + cal_type, + self.folder_entry.text(), + camera_name, + self.config_service + ) + + if dialog.exec(): + # После успешной съёмки + QMessageBox.information(self, "Успех", f"Съёмка {cal_type.upper()} завершена!") + + def reject(self): + """Закрытие диалога""" + if hasattr(self, '_browse_blink_timer') and self._browse_blink_timer: + self._browse_blink_timer.stop() + super().reject() \ No newline at end of file diff --git a/ui/dialogs/calibration_type_dialog.py b/ui/dialogs/calibration_type_dialog.py new file mode 100644 index 0000000..1b3196a --- /dev/null +++ b/ui/dialogs/calibration_type_dialog.py @@ -0,0 +1,730 @@ +""" +CalibrationTypeDialog - диалог для конкретного типа калибровки +Dark / Bias / Flat с прогрессом, авто-остановкой и профилями +""" +import shutil +import os +import re +from pathlib import Path +from datetime import datetime +from typing import Optional + +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, + QLabel, QComboBox, QSpinBox, QPushButton, QFrame, + QProgressBar, QMessageBox, QGroupBox, + QInputDialog, QWidget, QFileDialog +) +from PySide6.QtCore import Qt, QTimer, QMetaObject, Q_ARG, Signal +from PySide6.QtGui import QFont + +from services.config_service import ConfigService +from services.file_service import FileService +from services.watch_service import WatchService + + +class CalibrationTypeDialog(QDialog): + """Диалог для съёмки калибровочных кадров определённого типа""" + + # Сигнал для безопасного обновления UI из другого потока + progress_updated = Signal(int, int) # current, target + capture_completed = Signal(str) # target_folder + + def __init__(self, parent, cal_type: str, base_folder: str, + camera_name: str, config_service: ConfigService): + super().__init__(parent) + + self.cal_type = cal_type # 'bias', 'dark', 'flat' + self.base_folder = Path(base_folder) + self.camera_name = camera_name + self.config_service = config_service + + # Состояние съёмки + self.is_capturing = False + self.current_count = 0 + self.target_count = 0 + self._calibration_watch_service = None + + # Настройки для разных типов + self.settings = self._get_default_settings() + + self.setWindowTitle(self._get_title()) + self.setMinimumSize(550, 600) + self.resize(600, 650) + + # Подключаем сигналы + self.progress_updated.connect(self._on_progress_updated) + self.capture_completed.connect(self._on_capture_completed) + + self._create_ui() + self._load_optics() + self._update_recommendations() + + def _get_title(self) -> str: + titles = { + 'bias': '⚪ BIAS (Кадры смещения)', + 'dark': '🌑 DARK (Тёмные кадры)', + 'flat': '📖 FLAT (Плоские поля)' + } + return titles.get(self.cal_type, 'Калибровочные кадры') + + def _get_default_settings(self) -> dict: + """Возвращает настройки по умолчанию для типа калибровки""" + base = { + 'bias': { + 'iso_values': [800, 1600, 3200], + 'default_iso': 800, + 'count': 50, + 'min_count': 30, + 'max_count': 100, + 'recommended_count': 50, + }, + 'dark': { + 'iso_values': [800, 1600, 3200], + 'default_iso': 800, + 'exposure_values': [30, 60, 120, 180, 300], + 'default_exposure': 120, + 'count': 20, + 'min_count': 10, + 'max_count': 50, + 'recommended_count': 20, + }, + 'flat': { + 'iso_values': [800, 1600, 3200], + 'default_iso': 800, + 'aperture_values': ['f/2.8', 'f/4', 'f/5.6', 'f/8'], + 'count': 30, + 'min_count': 20, + 'max_count': 60, + 'recommended_count': 30, + } + } + return base.get(self.cal_type, {}) + + def _create_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # Заголовок с кнопкой справки + header_layout = QHBoxLayout() + + title_label = QLabel(self._get_title()) + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + header_layout.addWidget(title_label) + + help_btn = QPushButton("❓") + help_btn.setFixedSize(30, 30) + help_btn.setToolTip("Показать справку") + help_btn.clicked.connect(self._show_help) + header_layout.addWidget(help_btn) + header_layout.addStretch() + + layout.addLayout(header_layout) + + # Группа параметров + params_group = QGroupBox("⚙️ Параметры съёмки") + params_layout = QGridLayout(params_group) + params_layout.setVerticalSpacing(12) + params_layout.setHorizontalSpacing(15) + + row = 0 + + # ISO + iso_label = QLabel("ISO:") + iso_label.setFont(QFont("", 10, QFont.Bold)) + params_layout.addWidget(iso_label, row, 0) + + self.iso_combo = QComboBox() + self.iso_combo.addItems([str(v) for v in self.settings['iso_values']]) + self.iso_combo.setCurrentText(str(self.settings['default_iso'])) + self.iso_combo.currentTextChanged.connect(self._update_recommendations) + params_layout.addWidget(self.iso_combo, row, 1) + + self.custom_iso_btn = QPushButton("➕ своё") + self.custom_iso_btn.setFixedWidth(60) + self.custom_iso_btn.clicked.connect(self._add_custom_iso) + params_layout.addWidget(self.custom_iso_btn, row, 2) + + row += 1 + + # Выдержка (только для DARK) + if self.cal_type == 'dark': + exposure_label = QLabel("Выдержка (сек):") + exposure_label.setFont(QFont("", 10, QFont.Bold)) + params_layout.addWidget(exposure_label, row, 0) + + self.exposure_combo = QComboBox() + self.exposure_combo.addItems([str(v) for v in self.settings['exposure_values']]) + self.exposure_combo.setCurrentText(str(self.settings['default_exposure'])) + self.exposure_combo.currentTextChanged.connect(self._update_recommendations) + params_layout.addWidget(self.exposure_combo, row, 1) + + self.custom_exposure_btn = QPushButton("➕ своё") + self.custom_exposure_btn.setFixedWidth(60) + self.custom_exposure_btn.clicked.connect(self._add_custom_exposure) + params_layout.addWidget(self.custom_exposure_btn, row, 2) + + row += 1 + + # Оптика (только для FLAT) + if self.cal_type == 'flat': + optics_label = QLabel("Оптика:") + optics_label.setFont(QFont("", 10, QFont.Bold)) + params_layout.addWidget(optics_label, row, 0) + + self.optics_combo = QComboBox() + self.optics_combo.setEditable(True) + params_layout.addWidget(self.optics_combo, row, 1, 1, 2) + + row += 1 + + aperture_label = QLabel("Диафрагма:") + aperture_label.setFont(QFont("", 10, QFont.Bold)) + params_layout.addWidget(aperture_label, row, 0) + + self.aperture_combo = QComboBox() + self.aperture_combo.setEditable(True) + self.aperture_combo.addItems(self.settings['aperture_values']) + params_layout.addWidget(self.aperture_combo, row, 1, 1, 2) + + row += 1 + + telescope_hint = QLabel("💡 Для телескопов диафрагма фиксированная и выбирается автоматически") + telescope_hint.setStyleSheet("color: #888888; font-size: 10px;") + params_layout.addWidget(telescope_hint, row, 0, 1, 3) + + row += 1 + + # Количество кадров + count_label = QLabel("Количество кадров:") + count_label.setFont(QFont("", 10, QFont.Bold)) + params_layout.addWidget(count_label, row, 0) + + self.count_spin = QSpinBox() + self.count_spin.setMinimum(self.settings['min_count']) + self.count_spin.setMaximum(self.settings['max_count']) + self.count_spin.setValue(self.settings['count']) + self.count_spin.setSuffix(" кадров") + params_layout.addWidget(self.count_spin, row, 1) + + self.recommended_label = QLabel(f"(рекомендуется {self.settings['recommended_count']})") + self.recommended_label.setStyleSheet("color: #888888;") + params_layout.addWidget(self.recommended_label, row, 2) + + layout.addWidget(params_group) + + # Группа рекомендаций + tips_group = QGroupBox("📖 Рекомендации") + tips_group.setStyleSheet(""" + QGroupBox { + font-weight: bold; + margin-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + """) + tips_layout = QVBoxLayout(tips_group) + + self.tips_text = QLabel() + self.tips_text.setWordWrap(True) + self.tips_text.setStyleSheet("color: #FFD700; padding: 5px;") + tips_layout.addWidget(self.tips_text) + + layout.addWidget(tips_group) + + # Группа прогресса + self.progress_group = QGroupBox("📊 Прогресс съёмки") + self.progress_group.setVisible(False) + progress_layout = QVBoxLayout(self.progress_group) + + self.progress_bar = QProgressBar() + self.progress_bar.setMinimum(0) + progress_layout.addWidget(self.progress_bar) + + self.progress_status = QLabel("Готов к съёмке") + self.progress_status.setAlignment(Qt.AlignCenter) + progress_layout.addWidget(self.progress_status) + + layout.addWidget(self.progress_group) + + # Информация о сохранении + save_info = QFrame() + save_info.setStyleSheet(""" + QFrame { + background-color: #2d2d2d; + border-radius: 6px; + padding: 8px; + } + """) + save_layout = QVBoxLayout(save_info) + + save_label = QLabel("💾 Сохранить в:") + save_label.setFont(QFont("", 10, QFont.Bold)) + save_layout.addWidget(save_label) + + self.save_path_label = QLabel() + self.save_path_label.setWordWrap(True) + self.save_path_label.setStyleSheet("color: #4CAF50; font-family: monospace;") + save_layout.addWidget(self.save_path_label) + + layout.addWidget(save_info) + + # Кнопки действий + buttons_layout = QHBoxLayout() + buttons_layout.addStretch() + + self.back_btn = QPushButton("◀ Назад") + self.back_btn.clicked.connect(self._on_back_clicked) + buttons_layout.addWidget(self.back_btn) + + self.start_btn = QPushButton("▶ Начать съёмку") + self.start_btn.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + padding: 8px 20px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #388E3C; + } + """) + self.start_btn.clicked.connect(self._start_capture) + buttons_layout.addWidget(self.start_btn) + + self.stop_btn = QPushButton("⏹️ Остановить") + self.stop_btn.setStyleSheet(""" + QPushButton { + background-color: #f44336; + color: white; + font-weight: bold; + padding: 8px 20px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #d32f2f; + } + """) + self.stop_btn.clicked.connect(self._on_stop_clicked) + self.stop_btn.setVisible(False) + buttons_layout.addWidget(self.stop_btn) + + layout.addLayout(buttons_layout) + + self._update_save_path() + + def _load_optics(self): + """Загружает список оптики (объективы + телескопы) для FLAT режима""" + if self.cal_type != 'flat': + return + + lenses = self.config_service.get_lenses() + telescopes = self.config_service.get_telescopes() + + all_optics = [] + for lens in lenses: + all_optics.append(f"🔭 {lens}") + for telescope in telescopes: + all_optics.append(f"🪐 {telescope}") + + self.optics_combo.addItems(all_optics) + + def on_optics_changed(): + current = self.optics_combo.currentText() + if current.startswith("🪐"): + self.aperture_combo.setEnabled(False) + match = re.search(r'f/(\d+\.?\d*)', current) + if match: + self.aperture_combo.setCurrentText(f"f/{match.group(1)}") + else: + self.aperture_combo.setEnabled(True) + + if all_optics: + self.optics_combo.currentTextChanged.connect(on_optics_changed) + + def _update_save_path(self): + """Обновляет отображение пути сохранения""" + iso = int(self.iso_combo.currentText()) + + if self.cal_type == 'bias': + path = self.base_folder / "Calibration" / self.camera_name / "Bias" / f"ISO{iso}" + elif self.cal_type == 'dark': + exposure = self.exposure_combo.currentText() + path = self.base_folder / "Calibration" / self.camera_name / "Dark" / f"ISO{iso}_{exposure}s" + elif self.cal_type == 'flat': + optics = self.optics_combo.currentText() + optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + invalid_chars = '<>:"/\\|?*' + for char in invalid_chars: + optics_name = optics_name.replace(char, '_') + date_str = datetime.now().strftime("%Y-%m-%d") + path = self.base_folder / "Calibration" / self.camera_name / "Flat" / optics_name / date_str + else: + path = self.base_folder + + self.save_path_label.setText(str(path)) + return path + + def _update_recommendations(self): + """Обновляет рекомендации в зависимости от типа калибровки""" + if self.cal_type == 'bias': + self.tips_text.setText( + "⚪ BIAS (Кадры смещения)\n\n" + "📌 КАК СНИМАТЬ:\n" + "• Закройте крышку объектива\n" + "• Выдержка: САМАЯ КОРОТКАЯ (1/4000 или 1/8000)\n" + "• ISO: тот же, что при съёмке световых кадров\n\n" + "💡 СОВЕТ:\n" + "• Можно снять дома в любое время\n" + "• Используются для всех объективов\n" + "• 50 кадров оптимально для хорошего усреднения" + ) + elif self.cal_type == 'dark': + self.tips_text.setText( + "🌑 DARK (Тёмные кадры)\n\n" + "⚠️ ВАЖНО: Снимайте ПОСЛЕ сессии на месте!\n\n" + "📌 КАК СНИМАТЬ:\n" + "• Закройте крышку объектива\n" + "• ТЕ ЖЕ параметры ISO и выдержки, что при съёмке\n" + "• Дождитесь, пока камера прогреется до ночной температуры\n\n" + "🌡️ ТЕМПЕРАТУРА:\n" + "• Снимайте ПРИ ТОЙ ЖЕ температуре, что и Light кадры\n" + "• Разница >5°C делает кадры бесполезными!\n" + "• Лучше снять сразу после сессии, пока камера не остыла" + ) + elif self.cal_type == 'flat': + self.tips_text.setText( + "📖 FLAT (Плоские поля)\n\n" + "⚠️ ВАЖНО: НЕ меняйте фокус и зум после съёмки!\n\n" + "📌 КАК СНИМАТЬ:\n" + "• Способ 1: LED-планшет (рекомендуется)\n" + "• Способ 2: Рассвет/закат, камера в зенит\n" + "• Способ 3: Белая футболка на объектив\n\n" + "🎯 ЦЕЛЬ:\n" + "• Убрать виньетирование и пыль на оптике\n" + "• Гистограмма должна быть на 50-70%\n" + "• 30 кадров достаточно для хорошего результата" + ) + + self._update_save_path() + + def _start_capture(self): + """Начинает съёмку калибровочных кадров""" + self.target_count = self.count_spin.value() + self.current_count = 0 + + target_folder = self._update_save_path() + + # Создаём папку с проверкой + try: + target_folder.mkdir(parents=True, exist_ok=True) + print(f"Папка создана: {target_folder}") + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось создать папку:\n{target_folder}\n\nОшибка: {e}") + return + + self.progress_group.setVisible(True) + self.progress_bar.setMaximum(self.target_count) + self.progress_bar.setValue(0) + self.progress_status.setText(f"0 из {self.target_count} кадров") + + # Меняем кнопки + self.start_btn.setVisible(False) + self.stop_btn.setVisible(True) + self.back_btn.setEnabled(False) + + # Блокируем изменение параметров + self.iso_combo.setEnabled(False) + self.count_spin.setEnabled(False) + if self.cal_type == 'dark': + self.exposure_combo.setEnabled(False) + if self.cal_type == 'flat': + self.optics_combo.setEnabled(False) + self.aperture_combo.setEnabled(False) + + self.is_capturing = True + + # Получаем папку наблюдения + watch_folder = self._get_watch_folder() + print(f"Получена папка наблюдения: {watch_folder}") + + if not watch_folder: + QMessageBox.critical(self, "Ошибка", + "Не удалось определить папку наблюдения!\nУбедитесь, что вы выбрали папку в главном окне.") + self._stop_capture() + return + + if not watch_folder.exists(): + QMessageBox.critical(self, "Ошибка", f"Папка наблюдения не существует:\n{watch_folder}") + self._stop_capture() + return + + # Очищаем папку наблюдения от старых файлов + FileService.clear_watch_folder(watch_folder) + + # Создаём НОВЫЙ WatchService для калибровки + self._calibration_watch_service = WatchService() + + # Функция обратного вызова при получении файла (выполняется в потоке WatchService) + def on_file_received(file_path: Path): + if not self.is_capturing: + return + + print(f"Обнаружен файл: {file_path}") + if self._process_calibration_file(file_path, target_folder): + # Увеличиваем счётчик + new_count = self.current_count + 1 + # Отправляем сигнал для обновления UI в главном потоке + self.progress_updated.emit(new_count, self.target_count) + + if new_count >= self.target_count: + # Отправляем сигнал о завершении + self.capture_completed.emit(str(target_folder)) + + print("Запуск WatchService для калибровки...") + success = self._calibration_watch_service.start(watch_folder, on_file_received) + print(f"Результат запуска: {success}") + + if not success: + QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки!") + self._stop_capture() + return + + self.progress_status.setText(f"Отслеживается папка: {watch_folder}\nОжидание новых файлов...") + + def _on_progress_updated(self, current: int, target: int): + """Обновляет прогресс (вызывается из основного потока по сигналу)""" + self.current_count = current + self.progress_bar.setValue(current) + self.progress_status.setText(f"Снято {current} из {target} кадров") + print(f"Прогресс: {current}/{target}") + + def _on_capture_completed(self, target_folder: str): + """Обработчик завершения съёмки (вызывается из основного потока по сигналу)""" + if self.is_capturing: + self._stop_capture() + QMessageBox.information(self, "Успех", + f"✅ Съёмка завершена!\n" + f"Сохранено {self.current_count} кадров в:\n{target_folder}") + # Скрываем группу прогресса после сообщения + self.progress_group.setVisible(False) + + def _process_calibration_file(self, file_path: Path, target_folder: Path) -> bool: + """Обрабатывает файл из папки наблюдения""" + if not FileService.is_photo(file_path): + print(f"Файл {file_path.name} не является фото, пропускаем") + return False + + try: + target_folder.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now() + date_str = timestamp.strftime("%Y-%m-%d") + time_str = timestamp.strftime("%H-%M-%S") + suffix = file_path.suffix + + if self.cal_type == 'bias': + iso = self.iso_combo.currentText() + prefix = f"Bias_{self.camera_name}_ISO{iso}" + elif self.cal_type == 'dark': + iso = self.iso_combo.currentText() + exposure = self.exposure_combo.currentText() + prefix = f"Dark_{self.camera_name}_ISO{iso}_{exposure}s" + elif self.cal_type == 'flat': + optics = self.optics_combo.currentText() + optics_name = optics.replace("🔭", "").replace("🪐", "").strip() + invalid_chars = '<>:"/\\|?*' + for char in invalid_chars: + optics_name = optics_name.replace(char, '_') + aperture = self.aperture_combo.currentText() + prefix = f"Flat_{optics_name}_{aperture}" + else: + prefix = "Calibration" + + for char in '<>:"/\\|?*': + prefix = prefix.replace(char, '_') + + new_filename = f"{prefix}_{date_str}_{time_str}{suffix}" + target_path = target_folder / new_filename + target_path = FileService.resolve_conflict(target_path) + + shutil.move(str(file_path), str(target_path)) + print(f"Файл сохранён: {target_path}") + return True + + except Exception as e: + print(f"Ошибка сохранения {file_path.name}: {e}") + return False + + def _stop_capture(self): + """Останавливает съёмку""" + self.is_capturing = False + + if self._calibration_watch_service: + self._calibration_watch_service.stop() + self._calibration_watch_service = None + + self.start_btn.setVisible(True) + self.stop_btn.setVisible(False) + self.back_btn.setEnabled(True) + + self.iso_combo.setEnabled(True) + self.count_spin.setEnabled(True) + if self.cal_type == 'dark': + self.exposure_combo.setEnabled(True) + if self.cal_type == 'flat': + self.optics_combo.setEnabled(True) + self.aperture_combo.setEnabled(True) + + self.progress_status.setText("Съёмка остановлена") + + # Скрываем группу прогресса через 2 секунды + QTimer.singleShot(2000, lambda: self.progress_group.setVisible(False)) + + def _on_back_clicked(self): + """Обработчик кнопки 'Назад'""" + if self.is_capturing: + QMessageBox.warning(self, "Внимание", "Сначала остановите съёмку!") + return + self.reject() + + def _on_stop_clicked(self): + """Обработчик кнопки 'Остановить' с подтверждением""" + if self.current_count < self.target_count and self.current_count > 0: + reply = QMessageBox.question(self, "Прервать съёмку?", + f"Вы не закончили съёмку (снято {self.current_count} из {self.target_count} кадров).\n" + f"Вы действительно хотите прервать?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self._stop_capture() + self.progress_group.setVisible(False) + elif self.current_count == 0: + reply = QMessageBox.question(self, "Прервать съёмку?", + "Съёмка ещё не начата. Вы действительно хотите выйти?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self._stop_capture() + self.progress_group.setVisible(False) + else: + self._stop_capture() + self.progress_group.setVisible(False) + + def _get_watch_folder(self) -> Optional[Path]: + """Возвращает папку наблюдения из главного окна""" + print("Поиск папки наблюдения...") + + parent = self.parent() + print(f"Родительское окно: {parent}") + + while parent and not hasattr(parent, 'folder_entry'): + parent = parent.parent() + print(f"Поднимаемся выше: {parent}") + + if parent and hasattr(parent, 'folder_entry'): + watch_folder = parent.folder_entry.text() + print(f"Нашли folder_entry, значение: {watch_folder}") + if watch_folder: + path = Path(watch_folder) + print(f"Папка наблюдения: {path}") + return path + + print("Не удалось найти папку наблюдения в родительском окне") + + folder = QFileDialog.getExistingDirectory(self, "Выберите папку наблюдения (куда камера сохраняет файлы)") + if folder: + print(f"Пользователь выбрал: {folder}") + return Path(folder) + + return None + + def _add_custom_iso(self): + custom_iso, ok = QInputDialog.getInt(self, "Свой ISO", + "Введите значение ISO:", 800, 100, 12800) + if ok and custom_iso: + iso_str = str(custom_iso) + if self.iso_combo.findText(iso_str) == -1: + self.iso_combo.addItem(iso_str) + self.iso_combo.setCurrentText(iso_str) + + def _add_custom_exposure(self): + custom_exp, ok = QInputDialog.getInt(self, "Своя выдержка", + "Введите выдержку (секунд):", 120, 1, 3600) + if ok and custom_exp: + exp_str = str(custom_exp) + if self.exposure_combo.findText(exp_str) == -1: + self.exposure_combo.addItem(exp_str) + self.exposure_combo.setCurrentText(exp_str) + + def _show_help(self): + if self.cal_type == 'bias': + help_text = ( + "Что такое BIAS?\n\n" + "Bias (кадры смещения) — это снимки с закрытой крышкой\n" + "на минимально возможной выдержке.\n\n" + "Зачем нужны:\n" + "• Убирают read noise (шум считывания)\n" + "• Корректируют смещение чёрного уровня\n\n" + "Сколько снимать:\n" + "• 50 кадров для хорошего усреднения\n" + "• Можно использовать весь месяц\n\n" + "Когда снимать:\n" + "• Дома в любое время\n" + "• Температура не важна" + ) + elif self.cal_type == 'dark': + help_text = ( + "Что такое DARK?\n\n" + "Dark (тёмные кадры) — это снимки с закрытой крышкой\n" + "с ТЕМИ ЖЕ параметрами ISO и выдержки, что и световые кадры.\n\n" + "Зачем нужны:\n" + "• Убирают тепловой шум матрицы\n" + "• Убирают горячие пиксели\n\n" + "Сколько снимать:\n" + "• 20-30 кадров для хорошего результата\n\n" + "⚠️ ВАЖНО про температуру:\n" + "• Снимайте ПОСЛЕ сессии на месте!\n" + "• Камера должна быть при той же температуре\n" + "• Разница >5°C делает кадры бесполезными!" + ) + else: + help_text = ( + "Что такое FLAT?\n\n" + "Flat (плоские поля) — это снимки равномерно освещённой\n" + "поверхности с ТЕМИ ЖЕ фокусом и зумом.\n\n" + "Зачем нужны:\n" + "• Убирают виньетирование объектива\n" + "• Убирают пыль на матрице и оптике\n\n" + "Как снимать:\n" + "1. LED-планшет (лучший вариант)\n" + "2. Рассвет/закат, камера в зенит\n" + "3. Белая футболка на объектив\n\n" + "Сколько снимать:\n" + "• 30 кадров для хорошего усреднения\n\n" + "⚠️ ВАЖНО:\n" + "• НЕ меняйте фокус!\n" + "• НЕ меняйте зум!\n" + "• Снимайте в конце сессии" + ) + + QMessageBox.information(self, "Справка", help_text) + + def closeEvent(self, event): + if self.is_capturing: + reply = QMessageBox.question(self, "Прервать съёмку?", + "Съёмка активна. Вы действительно хотите закрыть окно?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self._stop_capture() + event.accept() + else: + event.ignore() + else: + event.accept() \ No newline at end of file diff --git a/ui/dialogs/equipment_dialog.py b/ui/dialogs/equipment_dialog.py index b0d8784..0508566 100644 --- a/ui/dialogs/equipment_dialog.py +++ b/ui/dialogs/equipment_dialog.py @@ -1,10 +1,11 @@ """ -EquipmentDialog - диалог управления оборудованием (камеры и объективы) -Аналог EquipmentDialogController из JavaFX версии +EquipmentDialog - диалог управления оборудованием +Камеры, объективы и телескопы """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, - QPushButton, QInputDialog, QMessageBox, QListWidgetItem + QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget, + QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit ) from PySide6.QtCore import Qt from PySide6.QtGui import QFont @@ -13,26 +14,27 @@ from services.config_service import ConfigService class EquipmentDialog(QDialog): - """Диалог для управления списками камер и объективов""" + """Диалог для управления оборудованием""" def __init__(self, parent, config_service: ConfigService): super().__init__(parent) self.config_service = config_service self.setWindowTitle("Управление оборудованием") - self.setMinimumSize(600, 400) - self.resize(650, 450) + self.setMinimumSize(700, 500) + self.resize(800, 550) - # Загружаем текущие списки + # Загружаем данные self.cameras = self.config_service.get_cameras() self.lenses = self.config_service.get_lenses() + self.telescopes = self.config_service.get_telescopes() self._create_ui() self._update_cameras_list() self._update_lenses_list() + self._update_telescopes_list() def _create_ui(self): - """Создаёт интерфейс диалога""" layout = QVBoxLayout(self) layout.setSpacing(15) layout.setContentsMargins(20, 20, 20, 20) @@ -46,64 +48,22 @@ class EquipmentDialog(QDialog): title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label) - # Контейнер для двух колонок - columns_layout = QHBoxLayout() - columns_layout.setSpacing(20) + # Используем QTabWidget для трёх вкладок + tab_widget = QTabWidget() - # Левая колонка - Камеры - left_layout = QVBoxLayout() + # Вкладка: Камеры + cameras_tab = self._create_cameras_tab() + tab_widget.addTab(cameras_tab, "📷 Камеры") - cameras_label = QLabel("Камеры") - cameras_font = QFont() - cameras_font.setPointSize(12) - cameras_font.setBold(True) - cameras_label.setFont(cameras_font) - left_layout.addWidget(cameras_label) + # Вкладка: Объективы + lenses_tab = self._create_lenses_tab() + tab_widget.addTab(lenses_tab, "🔭 Объективы") - self.cameras_list = QListWidget() - self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text())) - left_layout.addWidget(self.cameras_list) + # Вкладка: Телескопы + telescopes_tab = self._create_telescopes_tab() + tab_widget.addTab(telescopes_tab, "🪐 Телескопы") - cameras_buttons_layout = QHBoxLayout() - - add_camera_btn = QPushButton("➕ Добавить") - add_camera_btn.clicked.connect(self._add_camera) - cameras_buttons_layout.addWidget(add_camera_btn) - - self.remove_camera_btn = QPushButton("❌ Удалить") - self.remove_camera_btn.setEnabled(False) - self.remove_camera_btn.clicked.connect(self._remove_camera) - cameras_buttons_layout.addWidget(self.remove_camera_btn) - - left_layout.addLayout(cameras_buttons_layout) - - # Правая колонка - Объективы - right_layout = QVBoxLayout() - - lenses_label = QLabel("Объективы") - lenses_label.setFont(cameras_font) - right_layout.addWidget(lenses_label) - - self.lenses_list = QListWidget() - self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text())) - right_layout.addWidget(self.lenses_list) - - lenses_buttons_layout = QHBoxLayout() - - add_lens_btn = QPushButton("➕ Добавить") - add_lens_btn.clicked.connect(self._add_lens) - lenses_buttons_layout.addWidget(add_lens_btn) - - self.remove_lens_btn = QPushButton("❌ Удалить") - self.remove_lens_btn.setEnabled(False) - self.remove_lens_btn.clicked.connect(self._remove_lens) - lenses_buttons_layout.addWidget(self.remove_lens_btn) - - right_layout.addLayout(lenses_buttons_layout) - - columns_layout.addLayout(left_layout) - columns_layout.addLayout(right_layout) - layout.addLayout(columns_layout) + layout.addWidget(tab_widget) # Кнопка закрытия close_btn = QPushButton("Закрыть") @@ -113,90 +73,289 @@ class EquipmentDialog(QDialog): close_layout.addWidget(close_btn) layout.addLayout(close_layout) + def _create_cameras_tab(self) -> QWidget: + """Создаёт вкладку с камерами""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # Список камер + self.cameras_list = QListWidget() + self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text())) + layout.addWidget(self.cameras_list) + + # Кнопки + buttons_layout = QHBoxLayout() + + add_btn = QPushButton("➕ Добавить камеру") + add_btn.clicked.connect(self._add_camera) + buttons_layout.addWidget(add_btn) + + self.remove_camera_btn = QPushButton("❌ Удалить") + self.remove_camera_btn.setEnabled(False) + self.remove_camera_btn.clicked.connect(self._remove_camera) + buttons_layout.addWidget(self.remove_camera_btn) + + layout.addLayout(buttons_layout) + + return tab + + def _create_lenses_tab(self) -> QWidget: + """Создаёт вкладку с объективами""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # Список объективов + self.lenses_list = QListWidget() + self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text())) + layout.addWidget(self.lenses_list) + + # Кнопки + buttons_layout = QHBoxLayout() + + add_btn = QPushButton("➕ Добавить объектив") + add_btn.clicked.connect(self._add_lens) + buttons_layout.addWidget(add_btn) + + self.remove_lens_btn = QPushButton("❌ Удалить") + self.remove_lens_btn.setEnabled(False) + self.remove_lens_btn.clicked.connect(self._remove_lens) + buttons_layout.addWidget(self.remove_lens_btn) + + edit_btn = QPushButton("✏ Редактировать") + edit_btn.clicked.connect(self._edit_lens) + buttons_layout.addWidget(edit_btn) + + layout.addLayout(buttons_layout) + + return tab + + def _create_telescopes_tab(self) -> QWidget: + """Создаёт вкладку с телескопами""" + tab = QWidget() + layout = QVBoxLayout(tab) + + # Список телескопов + self.telescopes_list = QListWidget() + self.telescopes_list.itemClicked.connect(lambda item: self._select_telescope(item.text())) + layout.addWidget(self.telescopes_list) + + # Кнопки + buttons_layout = QHBoxLayout() + + add_btn = QPushButton("➕ Добавить телескоп") + add_btn.clicked.connect(self._add_telescope) + buttons_layout.addWidget(add_btn) + + self.remove_telescope_btn = QPushButton("❌ Удалить") + self.remove_telescope_btn.setEnabled(False) + self.remove_telescope_btn.clicked.connect(self._remove_telescope) + buttons_layout.addWidget(self.remove_telescope_btn) + + edit_btn = QPushButton("✏ Редактировать") + edit_btn.clicked.connect(self._edit_telescope) + buttons_layout.addWidget(edit_btn) + + layout.addLayout(buttons_layout) + + return tab + + # ===== Методы для камер ===== + def _update_cameras_list(self): - """Обновляет отображение списка камер""" self.cameras_list.clear() for camera in self.cameras: self.cameras_list.addItem(camera) self._selected_camera = None self.remove_camera_btn.setEnabled(False) - def _update_lenses_list(self): - """Обновляет отображение списка объективов""" - self.lenses_list.clear() - for lens in self.lenses: - self.lenses_list.addItem(lens) - self._selected_lens = None - self.remove_lens_btn.setEnabled(False) - def _select_camera(self, camera: str): - """Выделяет камеру в списке""" self._selected_camera = camera self.remove_camera_btn.setEnabled(True) - - # Снимаем выделение с объективов self.lenses_list.clearSelection() + self.telescopes_list.clearSelection() self._selected_lens = None + self._selected_telescope = None self.remove_lens_btn.setEnabled(False) - - def _select_lens(self, lens: str): - """Выделяет объектив в списке""" - self._selected_lens = lens - self.remove_lens_btn.setEnabled(True) - - # Снимаем выделение с камер - self.cameras_list.clearSelection() - self._selected_camera = None - self.remove_camera_btn.setEnabled(False) + self.remove_telescope_btn.setEnabled(False) def _add_camera(self): - """Добавляет новую камеру""" - new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") - if ok and new_camera and new_camera.strip(): - new_name = new_camera.strip() + name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") + if ok and name and name.strip(): + new_name = name.strip() if new_name in self.cameras: QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!") return - self.cameras.append(new_name) self.config_service.add_camera(new_name) self._update_cameras_list() QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена") def _remove_camera(self): - """Удаляет выбранную камеру""" if hasattr(self, '_selected_camera') and self._selected_camera: reply = QMessageBox.question(self, "Подтверждение", - f"Удалить камеру '{self._selected_camera}'?", - QMessageBox.Yes | QMessageBox.No) + f"Удалить камеру '{self._selected_camera}'?", + QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.cameras.remove(self._selected_camera) self.config_service.remove_camera(self._selected_camera) self._update_cameras_list() - QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена") + + # ===== Методы для объективов ===== + + def _update_lenses_list(self): + self.lenses_list.clear() + for lens in self.lenses: + self.lenses_list.addItem(lens) + self._selected_lens = None + self.remove_lens_btn.setEnabled(False) + + def _select_lens(self, lens: str): + self._selected_lens = lens + self.remove_lens_btn.setEnabled(True) + self.cameras_list.clearSelection() + self.telescopes_list.clearSelection() + self._selected_camera = None + self._selected_telescope = None + self.remove_camera_btn.setEnabled(False) + self.remove_telescope_btn.setEnabled(False) def _add_lens(self): - """Добавляет новый объектив""" - new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") - if ok and new_lens and new_lens.strip(): - new_name = new_lens.strip() + name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") + if ok and name and name.strip(): + new_name = name.strip() if new_name in self.lenses: QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") return - self.lenses.append(new_name) self.config_service.add_lens(new_name) self._update_lenses_list() QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен") def _remove_lens(self): - """Удаляет выбранный объектив""" if hasattr(self, '_selected_lens') and self._selected_lens: reply = QMessageBox.question(self, "Подтверждение", - f"Удалить объектив '{self._selected_lens}'?", - QMessageBox.Yes | QMessageBox.No) + f"Удалить объектив '{self._selected_lens}'?", + QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.lenses.remove(self._selected_lens) self.config_service.remove_lens(self._selected_lens) self._update_lenses_list() - QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён") \ No newline at end of file + + def _edit_lens(self): + if hasattr(self, '_selected_lens') and self._selected_lens: + new_name, ok = QInputDialog.getText(self, "Редактировать объектив", + f"Изменить '{self._selected_lens}' на:", + text=self._selected_lens) + if ok and new_name and new_name.strip(): + new_name = new_name.strip() + if new_name != self._selected_lens: + if new_name in self.lenses: + QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") + return + idx = self.lenses.index(self._selected_lens) + self.lenses[idx] = new_name + # Обновляем в конфиге (пока просто удаляем старый и добавляем новый) + self.config_service.remove_lens(self._selected_lens) + self.config_service.add_lens(new_name) + self._update_lenses_list() + + # ===== Методы для телескопов ===== + + def _update_telescopes_list(self): + self.telescopes_list.clear() + for telescope in self.telescopes: + self.telescopes_list.addItem(telescope) + self._selected_telescope = None + self.remove_telescope_btn.setEnabled(False) + + def _select_telescope(self, telescope: str): + self._selected_telescope = telescope + self.remove_telescope_btn.setEnabled(True) + self.cameras_list.clearSelection() + self.lenses_list.clearSelection() + self._selected_camera = None + self._selected_lens = None + self.remove_camera_btn.setEnabled(False) + self.remove_lens_btn.setEnabled(False) + + def _add_telescope(self): + """Добавляет телескоп с указанием диафрагмы (фиксированной)""" + dialog = QDialog(self) + dialog.setWindowTitle("Добавить телескоп") + dialog.setMinimumWidth(400) + + layout = QVBoxLayout(dialog) + + form_layout = QFormLayout() + + name_edit = QLineEdit() + name_edit.setPlaceholderText("例如: Celestron 8\"") + form_layout.addRow("Название:", name_edit) + + aperture_spin = QDoubleSpinBox() + aperture_spin.setRange(0.5, 20.0) + aperture_spin.setSingleStep(0.1) + aperture_spin.setValue(5.0) + aperture_spin.setSuffix(" (f/)") + form_layout.addRow("Диафрагма (f/):", aperture_spin) + + focal_spin = QSpinBox() + focal_spin.setRange(100, 5000) + focal_spin.setSingleStep(50) + focal_spin.setSuffix(" мм") + form_layout.addRow("Фокусное расстояние:", focal_spin) + + diameter_spin = QSpinBox() + diameter_spin.setRange(50, 500) + diameter_spin.setSingleStep(10) + diameter_spin.setSuffix(" мм") + form_layout.addRow("Диаметр объектива:", diameter_spin) + + layout.addLayout(form_layout) + + buttons_layout = QHBoxLayout() + ok_btn = QPushButton("OK") + cancel_btn = QPushButton("Отмена") + buttons_layout.addWidget(ok_btn) + buttons_layout.addWidget(cancel_btn) + layout.addLayout(buttons_layout) + + ok_btn.clicked.connect(dialog.accept) + cancel_btn.clicked.connect(dialog.reject) + + if dialog.exec(): + name = name_edit.text().strip() + if name: + telescope_info = f"{name} (f/{aperture_spin.value()}, F={focal_spin.value()}mm, D={diameter_spin.value()}mm)" + if telescope_info not in self.telescopes: + self.telescopes.append(telescope_info) + self.config_service.add_telescope(telescope_info) + self._update_telescopes_list() + QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен") + + def _remove_telescope(self): + if hasattr(self, '_selected_telescope') and self._selected_telescope: + reply = QMessageBox.question(self, "Подтверждение", + f"Удалить телескоп '{self._selected_telescope}'?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.telescopes.remove(self._selected_telescope) + self.config_service.remove_telescope(self._selected_telescope) + self._update_telescopes_list() + + def _edit_telescope(self): + if hasattr(self, '_selected_telescope') and self._selected_telescope: + new_name, ok = QInputDialog.getText(self, "Редактировать телескоп", + f"Изменить '{self._selected_telescope}' на:", + text=self._selected_telescope) + if ok and new_name and new_name.strip(): + new_name = new_name.strip() + if new_name != self._selected_telescope: + if new_name in self.telescopes: + QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!") + return + idx = self.telescopes.index(self._selected_telescope) + self.telescopes[idx] = new_name + self.config_service.remove_telescope(self._selected_telescope) + self.config_service.add_telescope(new_name) + self._update_telescopes_list() \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index eff6d48..edb8183 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -119,6 +119,13 @@ class MainWindow(QMainWindow): new_object_action.triggered.connect(self.set_new_object) session_menu.addAction(new_object_action) + session_menu.addSeparator() + + calibration_action = QAction("🌑 Калибровочные кадры...", self) + calibration_action.setShortcut("Ctrl+K") + calibration_action.triggered.connect(self.open_calibration_dialog) + session_menu.addAction(calibration_action) + # Меню Помощь help_menu = menubar.addMenu("Помощь") @@ -134,6 +141,12 @@ class MainWindow(QMainWindow): about_action.triggered.connect(self.show_info) help_menu.addAction(about_action) + def open_calibration_dialog(self): + """Открывает диалог калибровочных кадров""" + from ui.dialogs.calibration_dialog import CalibrationDialog + dialog = CalibrationDialog(self, self.config_service) + dialog.exec() + def _create_main_content(self): central_widget = QWidget() self.setCentralWidget(central_widget) @@ -310,21 +323,34 @@ class MainWindow(QMainWindow): self.object_combo.lineEdit().blockSignals(False) def _load_saved_settings(self): + """Загружает сохранённые настройки""" cameras = self.config_service.get_cameras() lenses = self.config_service.get_lenses() + telescopes = self.config_service.get_telescopes() # <-- добавить celestial_bodies = self.config_service.get_celestial_bodies() + # Объединяем объективы и телескопы для выбора оптики + all_optics = [] + for lens in lenses: + all_optics.append(f"🔭 {lens}") + for telescope in telescopes: + all_optics.append(f"🪐 {telescope}") + if cameras: self.camera_combo.addItems(cameras) last_camera = self.config_service.get_last_camera() if last_camera and last_camera in cameras: self.camera_combo.setCurrentText(last_camera) - if lenses: - self.lens_combo.addItems(lenses) + if all_optics: + self.lens_combo.addItems(all_optics) last_lens = self.config_service.get_last_lens() - if last_lens and last_lens in lenses: - self.lens_combo.setCurrentText(last_lens) + if last_lens: + # Ищем последнюю использованную оптику + for opt in all_optics: + if last_lens in opt: + self.lens_combo.setCurrentText(opt) + break if celestial_bodies: self.object_combo.addItems(celestial_bodies) @@ -461,6 +487,13 @@ class MainWindow(QMainWindow): camera_val = camera if camera else "Unknown" lens_val = lens if lens else "Unknown" + optics_value = lens + if optics_value.startswith("🔭 "): + optics_value = optics_value[2:] + elif optics_value.startswith("🪐 "): + optics_value = optics_value[2:] + + self.config_service.set_last_lens(optics_value) self.session_service.start_session(watch_path, object_name, camera_val, lens_val)