working logic+working watching files+added calibration feature+instructions

This commit is contained in:
Vic Sergeev 2026-05-07 21:13:00 +03:00
parent 09d181eba8
commit 97ed8217bf
25 changed files with 1743 additions and 192 deletions

23
.idea/workspace.xml generated
View file

@ -2,27 +2,20 @@
<project version="4">
<component name="ChangeListManager">
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/camera_profile.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/equipment.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/services/calibration_service.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ui/dialogs/calibration_dialog.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ui/dialogs/calibration_type_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build_exe.spec" beforeDir="false" afterPath="$PROJECT_DIR$/build_exe.spec" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/models/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/models/astro_object.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/astro_object.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/models/session.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/session.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/services/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/astro_settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/astro_settings.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/services/config_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/config_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/services/file_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/file_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/services/session_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/session_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/services/watch_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/watch_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/main_window.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/utils/sound_manager.py" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -82,7 +75,7 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1778143911036</updated>
<workItem from="1778143912090" duration="6772000" />
<workItem from="1778143912090" duration="14992000" />
</task>
<servers />
</component>
@ -90,6 +83,6 @@
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778151943591" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778176299697" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

View file

@ -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"

106
models/camera_profile.py Normal file
View file

@ -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
)

35
models/equipment.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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:

View file

@ -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():

View file

@ -1,5 +1,8 @@
from ui.dialogs.equipment_dialog import EquipmentDialog
from ui.dialogs.celestial_dialog import CelestialDialog
from ui.dialogs.instructions_dialog import InstructionsDialog
from ui.dialogs.calibration_dialog import CalibrationDialog
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog']
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog',
'CalibrationDialog', 'CalibrationTypeDialog']

View file

@ -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()

View file

@ -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()

View file

@ -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}' удалён")
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()

View file

@ -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)