working logic+working watching files+added calibration feature+instructions
This commit is contained in:
parent
09d181eba8
commit
97ed8217bf
25 changed files with 1743 additions and 192 deletions
23
.idea/workspace.xml
generated
23
.idea/workspace.xml
generated
|
|
@ -2,27 +2,20 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="">
|
<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$/.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$/astro_settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/astro_settings.json" 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$/services/config_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/config_service.py" 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/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/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$/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/__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/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$/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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
|
@ -82,7 +75,7 @@
|
||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1778143911036</updated>
|
<updated>1778143911036</updated>
|
||||||
<workItem from="1778143912090" duration="6772000" />
|
<workItem from="1778143912090" duration="14992000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|
@ -90,6 +83,6 @@
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
<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>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
"Юпитер-21м",
|
"Юпитер-21м",
|
||||||
"Tamron 18-200mm"
|
"Tamron 18-200mm"
|
||||||
],
|
],
|
||||||
|
"telescopes": [
|
||||||
|
"Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)"
|
||||||
|
],
|
||||||
"last_watch_folder": "C:/Users/Juliette/Documents/testwatcher",
|
"last_watch_folder": "C:/Users/Juliette/Documents/testwatcher",
|
||||||
"last_camera": "Canon 40D",
|
"last_camera": "Canon 40D",
|
||||||
"last_lens": "MTO-500A"
|
"last_lens": "MTO-500A"
|
||||||
|
|
|
||||||
106
models/camera_profile.py
Normal file
106
models/camera_profile.py
Normal 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
35
models/equipment.py
Normal 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
|
||||||
Binary file not shown.
BIN
services/__pycache__/calibration_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/calibration_service.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
113
services/calibration_service.py
Normal file
113
services/calibration_service.py
Normal 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)
|
||||||
|
|
@ -4,18 +4,6 @@ ConfigService - управление настройками приложения
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
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:
|
class ConfigService:
|
||||||
|
|
@ -25,14 +13,15 @@ class ConfigService:
|
||||||
CELESTIAL_BODIES_FILE = "celestial_bodies.json"
|
CELESTIAL_BODIES_FILE = "celestial_bodies.json"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = AppConfig(
|
self.config = {
|
||||||
cameras=[],
|
'cameras': [],
|
||||||
lenses=[],
|
'lenses': [],
|
||||||
celestial_bodies=[],
|
'telescopes': [],
|
||||||
last_watch_folder="",
|
'celestial_bodies': [],
|
||||||
last_camera="",
|
'last_watch_folder': '',
|
||||||
last_lens=""
|
'last_camera': '',
|
||||||
)
|
'last_lens': ''
|
||||||
|
}
|
||||||
self.load_all()
|
self.load_all()
|
||||||
|
|
||||||
def load_all(self):
|
def load_all(self):
|
||||||
|
|
@ -40,8 +29,9 @@ class ConfigService:
|
||||||
self._load_settings()
|
self._load_settings()
|
||||||
self._load_celestial_bodies()
|
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)",
|
"M31 (Andromeda Galaxy)",
|
||||||
"M42 (Orion Nebula)",
|
"M42 (Orion Nebula)",
|
||||||
"M45 (Pleiades)",
|
"M45 (Pleiades)",
|
||||||
|
|
@ -54,108 +44,151 @@ class ConfigService:
|
||||||
self._save_celestial_bodies()
|
self._save_celestial_bodies()
|
||||||
|
|
||||||
def _load_settings(self):
|
def _load_settings(self):
|
||||||
|
"""Загружает основные настройки (камеры, объективы, телескопы, последнюю папку)"""
|
||||||
if os.path.exists(self.SETTINGS_FILE):
|
if os.path.exists(self.SETTINGS_FILE):
|
||||||
try:
|
try:
|
||||||
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
self.config.cameras = data.get('cameras', [])
|
self.config['cameras'] = data.get('cameras', [])
|
||||||
self.config.lenses = data.get('lenses', [])
|
self.config['lenses'] = data.get('lenses', [])
|
||||||
self.config.last_watch_folder = data.get('last_watch_folder', '')
|
self.config['telescopes'] = data.get('telescopes', [])
|
||||||
self.config.last_camera = data.get('last_camera', '')
|
self.config['last_watch_folder'] = data.get('last_watch_folder', '')
|
||||||
self.config.last_lens = data.get('last_lens', '')
|
self.config['last_camera'] = data.get('last_camera', '')
|
||||||
|
self.config['last_lens'] = data.get('last_lens', '')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка загрузки настроек: {e}")
|
print(f"Ошибка загрузки настроек: {e}")
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
|
"""Сохраняет основные настройки"""
|
||||||
try:
|
try:
|
||||||
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
||||||
json.dump({
|
json.dump({
|
||||||
'cameras': self.config.cameras,
|
'cameras': self.config['cameras'],
|
||||||
'lenses': self.config.lenses,
|
'lenses': self.config['lenses'],
|
||||||
'last_watch_folder': self.config.last_watch_folder,
|
'telescopes': self.config['telescopes'],
|
||||||
'last_camera': self.config.last_camera,
|
'last_watch_folder': self.config['last_watch_folder'],
|
||||||
'last_lens': self.config.last_lens
|
'last_camera': self.config['last_camera'],
|
||||||
|
'last_lens': self.config['last_lens']
|
||||||
}, f, ensure_ascii=False, indent=2)
|
}, f, ensure_ascii=False, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка сохранения настроек: {e}")
|
print(f"Ошибка сохранения настроек: {e}")
|
||||||
|
|
||||||
def _load_celestial_bodies(self):
|
def _load_celestial_bodies(self):
|
||||||
|
"""Загружает список небесных тел"""
|
||||||
if os.path.exists(self.CELESTIAL_BODIES_FILE):
|
if os.path.exists(self.CELESTIAL_BODIES_FILE):
|
||||||
try:
|
try:
|
||||||
with open(self.CELESTIAL_BODIES_FILE, 'r', encoding='utf-8') as f:
|
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:
|
except Exception as e:
|
||||||
print(f"Ошибка загрузки небесных тел: {e}")
|
print(f"Ошибка загрузки небесных тел: {e}")
|
||||||
|
|
||||||
def _save_celestial_bodies(self):
|
def _save_celestial_bodies(self):
|
||||||
|
"""Сохраняет список небесных тел"""
|
||||||
try:
|
try:
|
||||||
with open(self.CELESTIAL_BODIES_FILE, 'w', encoding='utf-8') as f:
|
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:
|
except Exception as e:
|
||||||
print(f"Ошибка сохранения небесных тел: {e}")
|
print(f"Ошибка сохранения небесных тел: {e}")
|
||||||
|
|
||||||
|
# ===== Методы для работы с камерами =====
|
||||||
|
|
||||||
def get_cameras(self) -> List[str]:
|
def get_cameras(self) -> List[str]:
|
||||||
return self.config.cameras.copy()
|
return self.config['cameras'].copy()
|
||||||
|
|
||||||
def add_camera(self, camera: str):
|
def add_camera(self, camera: str):
|
||||||
if camera and camera not in self.config.cameras:
|
if camera and camera not in self.config['cameras']:
|
||||||
self.config.cameras.append(camera)
|
self.config['cameras'].append(camera)
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
def remove_camera(self, camera: str):
|
def remove_camera(self, camera: str):
|
||||||
if camera in self.config.cameras:
|
if camera in self.config['cameras']:
|
||||||
self.config.cameras.remove(camera)
|
self.config['cameras'].remove(camera)
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
|
# ===== Методы для работы с объективами =====
|
||||||
|
|
||||||
def get_lenses(self) -> List[str]:
|
def get_lenses(self) -> List[str]:
|
||||||
return self.config.lenses.copy()
|
return self.config['lenses'].copy()
|
||||||
|
|
||||||
def add_lens(self, lens: str):
|
def add_lens(self, lens: str):
|
||||||
if lens and lens not in self.config.lenses:
|
if lens and lens not in self.config['lenses']:
|
||||||
self.config.lenses.append(lens)
|
self.config['lenses'].append(lens)
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
def remove_lens(self, lens: str):
|
def remove_lens(self, lens: str):
|
||||||
if lens in self.config.lenses:
|
if lens in self.config['lenses']:
|
||||||
self.config.lenses.remove(lens)
|
self.config['lenses'].remove(lens)
|
||||||
self.save_settings()
|
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]:
|
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):
|
def add_celestial_body(self, name: str):
|
||||||
if name and name not in self.config.celestial_bodies:
|
if name and name not in self.config['celestial_bodies']:
|
||||||
self.config.celestial_bodies.append(name)
|
self.config['celestial_bodies'].append(name)
|
||||||
self._save_celestial_bodies()
|
self._save_celestial_bodies()
|
||||||
|
|
||||||
def remove_celestial_body(self, name: str):
|
def remove_celestial_body(self, name: str):
|
||||||
if name in self.config.celestial_bodies:
|
if name in self.config['celestial_bodies']:
|
||||||
self.config.celestial_bodies.remove(name)
|
self.config['celestial_bodies'].remove(name)
|
||||||
self._save_celestial_bodies()
|
self._save_celestial_bodies()
|
||||||
|
|
||||||
def update_celestial_body(self, old_name: str, new_name: str):
|
def update_celestial_body(self, old_name: str, new_name: str):
|
||||||
if old_name in self.config.celestial_bodies:
|
if old_name in self.config['celestial_bodies']:
|
||||||
idx = self.config.celestial_bodies.index(old_name)
|
idx = self.config['celestial_bodies'].index(old_name)
|
||||||
self.config.celestial_bodies[idx] = new_name
|
self.config['celestial_bodies'][idx] = new_name
|
||||||
self._save_celestial_bodies()
|
self._save_celestial_bodies()
|
||||||
|
|
||||||
|
# ===== Методы для работы с последней папкой и оборудованием =====
|
||||||
|
|
||||||
def get_last_watch_folder(self) -> str:
|
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):
|
def set_last_watch_folder(self, folder: str):
|
||||||
self.config.last_watch_folder = folder
|
self.config['last_watch_folder'] = folder
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
def get_last_camera(self) -> str:
|
def get_last_camera(self) -> str:
|
||||||
return self.config.last_camera
|
return self.config.get('last_camera', '')
|
||||||
|
|
||||||
def set_last_camera(self, camera: str):
|
def set_last_camera(self, camera: str):
|
||||||
self.config.last_camera = camera
|
self.config['last_camera'] = camera
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
def get_last_lens(self) -> str:
|
def get_last_lens(self) -> str:
|
||||||
return self.config.last_lens
|
return self.config.get('last_lens', '')
|
||||||
|
|
||||||
def set_last_lens(self, lens: str):
|
def set_last_lens(self, lens: str):
|
||||||
self.config.last_lens = lens
|
self.config['last_lens'] = lens
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
@ -35,6 +35,22 @@ class FileService:
|
||||||
return new_path
|
return new_path
|
||||||
counter += 1
|
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
|
@classmethod
|
||||||
def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str,
|
def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str,
|
||||||
timestamp: Optional[datetime] = None) -> None:
|
timestamp: Optional[datetime] = None) -> None:
|
||||||
|
|
@ -52,23 +68,45 @@ class FileService:
|
||||||
print(f"Ошибка записи лога: {e}")
|
print(f"Ошибка записи лога: {e}")
|
||||||
|
|
||||||
@classmethod
|
@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():
|
if not source.exists():
|
||||||
|
print(f"Файл не существует: {source}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not cls.is_photo(source):
|
if not cls.is_photo(source):
|
||||||
|
print(f"Неподдерживаемый формат: {source}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_folder.mkdir(parents=True, exist_ok=True)
|
target_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Получаем время создания файла
|
||||||
creation_time = datetime.fromtimestamp(source.stat().st_ctime)
|
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))
|
shutil.move(str(source), str(target_path))
|
||||||
|
print(f"Файл перемещён и переименован: {source.name} -> {target_path.name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка перемещения {source.name}: {e}")
|
print(f"Ошибка перемещения файла {source.name}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class SessionService:
|
||||||
return astro_object
|
return astro_object
|
||||||
|
|
||||||
def handle_file(self, file_path: Path) -> bool:
|
def handle_file(self, file_path: Path) -> bool:
|
||||||
"""Обрабатывает новый файл"""
|
"""Обрабатывает новый файл: перемещает в папку текущего объекта с переименованием"""
|
||||||
if not self._current_session:
|
if not self._current_session:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -67,11 +67,13 @@ class SessionService:
|
||||||
if not current_object:
|
if not current_object:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Передаём имя объекта для переименования файла
|
||||||
success = self._file_service.move_file(
|
success = self._file_service.move_file(
|
||||||
file_path,
|
file_path,
|
||||||
current_object.folder,
|
current_object.folder,
|
||||||
self._current_session.camera,
|
self._current_session.camera,
|
||||||
self._current_session.optics
|
self._current_session.optics,
|
||||||
|
current_object.name # Добавляем имя объекта для переименования
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -92,12 +94,14 @@ class SessionService:
|
||||||
if watch_folder.exists():
|
if watch_folder.exists():
|
||||||
for file_path in watch_folder.iterdir():
|
for file_path in watch_folder.iterdir():
|
||||||
if file_path.is_file() and self._file_service.is_photo(file_path):
|
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,
|
file_path,
|
||||||
current_object.folder,
|
current_object.folder,
|
||||||
self._current_session.camera,
|
self._current_session.camera,
|
||||||
self._current_session.optics
|
self._current_session.optics,
|
||||||
):
|
current_object.name # Добавляем имя объекта для переименования
|
||||||
|
)
|
||||||
|
if success:
|
||||||
current_object.increment_photo_count()
|
current_object.increment_photo_count()
|
||||||
count += 1
|
count += 1
|
||||||
if on_file_moved:
|
if on_file_moved:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,17 @@ class PhotoHandler(FileSystemEventHandler):
|
||||||
if not event.is_directory:
|
if not event.is_directory:
|
||||||
src_path = Path(event.src_path)
|
src_path = Path(event.src_path)
|
||||||
if FileService.is_photo(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)
|
self._pending_files.put(src_path)
|
||||||
|
|
||||||
def _process_queue(self):
|
def _process_queue(self):
|
||||||
|
|
@ -52,24 +62,33 @@ class WatchService:
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
|
|
||||||
def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool:
|
def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool:
|
||||||
|
"""Запускает отслеживание папки"""
|
||||||
if self._is_running:
|
if self._is_running:
|
||||||
|
print("Watcher already running")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not watch_folder.exists():
|
if not watch_folder.exists():
|
||||||
|
print(f"Папка не существует: {watch_folder}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print(f"Запуск отслеживания папки: {watch_folder}")
|
||||||
self._event_handler = PhotoHandler(on_new_file)
|
self._event_handler = PhotoHandler(on_new_file)
|
||||||
self._observer = Observer()
|
self._observer = Observer()
|
||||||
self._observer.schedule(self._event_handler, str(watch_folder), recursive=False)
|
self._observer.schedule(self._event_handler, str(watch_folder), recursive=False)
|
||||||
self._observer.start()
|
self._observer.start()
|
||||||
self._is_running = True
|
self._is_running = True
|
||||||
|
print(f"Отслеживание успешно запущено для: {watch_folder}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Ошибка запуска отслеживания: {e}")
|
print(f"Ошибка запуска отслеживания: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
"""Останавливает отслеживание"""
|
||||||
|
print("Остановка отслеживания...")
|
||||||
if self._observer:
|
if self._observer:
|
||||||
self._observer.stop()
|
self._observer.stop()
|
||||||
self._observer.join()
|
self._observer.join()
|
||||||
|
|
@ -80,12 +99,13 @@ class WatchService:
|
||||||
self._event_handler = None
|
self._event_handler = None
|
||||||
|
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
|
print("Отслеживание остановлено")
|
||||||
|
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
return self._is_running
|
return self._is_running
|
||||||
|
|
||||||
def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int:
|
def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int:
|
||||||
"""Перемещает все существующие файлы"""
|
"""Перемещает все существующие файлы из папки наблюдения"""
|
||||||
count = 0
|
count = 0
|
||||||
if watch_folder.exists():
|
if watch_folder.exists():
|
||||||
for file_path in watch_folder.iterdir():
|
for file_path in watch_folder.iterdir():
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,5 +1,8 @@
|
||||||
from ui.dialogs.equipment_dialog import EquipmentDialog
|
from ui.dialogs.equipment_dialog import EquipmentDialog
|
||||||
from ui.dialogs.celestial_dialog import CelestialDialog
|
from ui.dialogs.celestial_dialog import CelestialDialog
|
||||||
from ui.dialogs.instructions_dialog import InstructionsDialog
|
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']
|
||||||
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
281
ui/dialogs/calibration_dialog.py
Normal file
281
ui/dialogs/calibration_dialog.py
Normal 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()
|
||||||
730
ui/dialogs/calibration_type_dialog.py
Normal file
730
ui/dialogs/calibration_type_dialog.py
Normal 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()
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
"""
|
"""
|
||||||
EquipmentDialog - диалог управления оборудованием (камеры и объективы)
|
EquipmentDialog - диалог управления оборудованием
|
||||||
Аналог EquipmentDialogController из JavaFX версии
|
Камеры, объективы и телескопы
|
||||||
"""
|
"""
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
|
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.QtCore import Qt
|
||||||
from PySide6.QtGui import QFont
|
from PySide6.QtGui import QFont
|
||||||
|
|
@ -13,26 +14,27 @@ from services.config_service import ConfigService
|
||||||
|
|
||||||
|
|
||||||
class EquipmentDialog(QDialog):
|
class EquipmentDialog(QDialog):
|
||||||
"""Диалог для управления списками камер и объективов"""
|
"""Диалог для управления оборудованием"""
|
||||||
|
|
||||||
def __init__(self, parent, config_service: ConfigService):
|
def __init__(self, parent, config_service: ConfigService):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.config_service = config_service
|
self.config_service = config_service
|
||||||
self.setWindowTitle("Управление оборудованием")
|
self.setWindowTitle("Управление оборудованием")
|
||||||
self.setMinimumSize(600, 400)
|
self.setMinimumSize(700, 500)
|
||||||
self.resize(650, 450)
|
self.resize(800, 550)
|
||||||
|
|
||||||
# Загружаем текущие списки
|
# Загружаем данные
|
||||||
self.cameras = self.config_service.get_cameras()
|
self.cameras = self.config_service.get_cameras()
|
||||||
self.lenses = self.config_service.get_lenses()
|
self.lenses = self.config_service.get_lenses()
|
||||||
|
self.telescopes = self.config_service.get_telescopes()
|
||||||
|
|
||||||
self._create_ui()
|
self._create_ui()
|
||||||
self._update_cameras_list()
|
self._update_cameras_list()
|
||||||
self._update_lenses_list()
|
self._update_lenses_list()
|
||||||
|
self._update_telescopes_list()
|
||||||
|
|
||||||
def _create_ui(self):
|
def _create_ui(self):
|
||||||
"""Создаёт интерфейс диалога"""
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(15)
|
||||||
layout.setContentsMargins(20, 20, 20, 20)
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
@ -46,64 +48,22 @@ class EquipmentDialog(QDialog):
|
||||||
title_label.setAlignment(Qt.AlignCenter)
|
title_label.setAlignment(Qt.AlignCenter)
|
||||||
layout.addWidget(title_label)
|
layout.addWidget(title_label)
|
||||||
|
|
||||||
# Контейнер для двух колонок
|
# Используем QTabWidget для трёх вкладок
|
||||||
columns_layout = QHBoxLayout()
|
tab_widget = QTabWidget()
|
||||||
columns_layout.setSpacing(20)
|
|
||||||
|
|
||||||
# Левая колонка - Камеры
|
# Вкладка: Камеры
|
||||||
left_layout = QVBoxLayout()
|
cameras_tab = self._create_cameras_tab()
|
||||||
|
tab_widget.addTab(cameras_tab, "📷 Камеры")
|
||||||
|
|
||||||
cameras_label = QLabel("Камеры")
|
# Вкладка: Объективы
|
||||||
cameras_font = QFont()
|
lenses_tab = self._create_lenses_tab()
|
||||||
cameras_font.setPointSize(12)
|
tab_widget.addTab(lenses_tab, "🔭 Объективы")
|
||||||
cameras_font.setBold(True)
|
|
||||||
cameras_label.setFont(cameras_font)
|
|
||||||
left_layout.addWidget(cameras_label)
|
|
||||||
|
|
||||||
self.cameras_list = QListWidget()
|
# Вкладка: Телескопы
|
||||||
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
|
telescopes_tab = self._create_telescopes_tab()
|
||||||
left_layout.addWidget(self.cameras_list)
|
tab_widget.addTab(telescopes_tab, "🪐 Телескопы")
|
||||||
|
|
||||||
cameras_buttons_layout = QHBoxLayout()
|
layout.addWidget(tab_widget)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Кнопка закрытия
|
# Кнопка закрытия
|
||||||
close_btn = QPushButton("Закрыть")
|
close_btn = QPushButton("Закрыть")
|
||||||
|
|
@ -113,90 +73,289 @@ class EquipmentDialog(QDialog):
|
||||||
close_layout.addWidget(close_btn)
|
close_layout.addWidget(close_btn)
|
||||||
layout.addLayout(close_layout)
|
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):
|
def _update_cameras_list(self):
|
||||||
"""Обновляет отображение списка камер"""
|
|
||||||
self.cameras_list.clear()
|
self.cameras_list.clear()
|
||||||
for camera in self.cameras:
|
for camera in self.cameras:
|
||||||
self.cameras_list.addItem(camera)
|
self.cameras_list.addItem(camera)
|
||||||
self._selected_camera = None
|
self._selected_camera = None
|
||||||
self.remove_camera_btn.setEnabled(False)
|
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):
|
def _select_camera(self, camera: str):
|
||||||
"""Выделяет камеру в списке"""
|
|
||||||
self._selected_camera = camera
|
self._selected_camera = camera
|
||||||
self.remove_camera_btn.setEnabled(True)
|
self.remove_camera_btn.setEnabled(True)
|
||||||
|
|
||||||
# Снимаем выделение с объективов
|
|
||||||
self.lenses_list.clearSelection()
|
self.lenses_list.clearSelection()
|
||||||
|
self.telescopes_list.clearSelection()
|
||||||
self._selected_lens = None
|
self._selected_lens = None
|
||||||
|
self._selected_telescope = None
|
||||||
self.remove_lens_btn.setEnabled(False)
|
self.remove_lens_btn.setEnabled(False)
|
||||||
|
self.remove_telescope_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)
|
|
||||||
|
|
||||||
def _add_camera(self):
|
def _add_camera(self):
|
||||||
"""Добавляет новую камеру"""
|
name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
|
||||||
new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
|
if ok and name and name.strip():
|
||||||
if ok and new_camera and new_camera.strip():
|
new_name = name.strip()
|
||||||
new_name = new_camera.strip()
|
|
||||||
if new_name in self.cameras:
|
if new_name in self.cameras:
|
||||||
QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!")
|
QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.cameras.append(new_name)
|
self.cameras.append(new_name)
|
||||||
self.config_service.add_camera(new_name)
|
self.config_service.add_camera(new_name)
|
||||||
self._update_cameras_list()
|
self._update_cameras_list()
|
||||||
QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена")
|
QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена")
|
||||||
|
|
||||||
def _remove_camera(self):
|
def _remove_camera(self):
|
||||||
"""Удаляет выбранную камеру"""
|
|
||||||
if hasattr(self, '_selected_camera') and self._selected_camera:
|
if hasattr(self, '_selected_camera') and self._selected_camera:
|
||||||
reply = QMessageBox.question(self, "Подтверждение",
|
reply = QMessageBox.question(self, "Подтверждение",
|
||||||
f"Удалить камеру '{self._selected_camera}'?",
|
f"Удалить камеру '{self._selected_camera}'?",
|
||||||
QMessageBox.Yes | QMessageBox.No)
|
QMessageBox.Yes | QMessageBox.No)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
self.cameras.remove(self._selected_camera)
|
self.cameras.remove(self._selected_camera)
|
||||||
self.config_service.remove_camera(self._selected_camera)
|
self.config_service.remove_camera(self._selected_camera)
|
||||||
self._update_cameras_list()
|
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):
|
def _add_lens(self):
|
||||||
"""Добавляет новый объектив"""
|
name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
|
||||||
new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
|
if ok and name and name.strip():
|
||||||
if ok and new_lens and new_lens.strip():
|
new_name = name.strip()
|
||||||
new_name = new_lens.strip()
|
|
||||||
if new_name in self.lenses:
|
if new_name in self.lenses:
|
||||||
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
|
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.lenses.append(new_name)
|
self.lenses.append(new_name)
|
||||||
self.config_service.add_lens(new_name)
|
self.config_service.add_lens(new_name)
|
||||||
self._update_lenses_list()
|
self._update_lenses_list()
|
||||||
QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен")
|
QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен")
|
||||||
|
|
||||||
def _remove_lens(self):
|
def _remove_lens(self):
|
||||||
"""Удаляет выбранный объектив"""
|
|
||||||
if hasattr(self, '_selected_lens') and self._selected_lens:
|
if hasattr(self, '_selected_lens') and self._selected_lens:
|
||||||
reply = QMessageBox.question(self, "Подтверждение",
|
reply = QMessageBox.question(self, "Подтверждение",
|
||||||
f"Удалить объектив '{self._selected_lens}'?",
|
f"Удалить объектив '{self._selected_lens}'?",
|
||||||
QMessageBox.Yes | QMessageBox.No)
|
QMessageBox.Yes | QMessageBox.No)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
self.lenses.remove(self._selected_lens)
|
self.lenses.remove(self._selected_lens)
|
||||||
self.config_service.remove_lens(self._selected_lens)
|
self.config_service.remove_lens(self._selected_lens)
|
||||||
self._update_lenses_list()
|
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()
|
||||||
|
|
@ -119,6 +119,13 @@ class MainWindow(QMainWindow):
|
||||||
new_object_action.triggered.connect(self.set_new_object)
|
new_object_action.triggered.connect(self.set_new_object)
|
||||||
session_menu.addAction(new_object_action)
|
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("Помощь")
|
help_menu = menubar.addMenu("Помощь")
|
||||||
|
|
||||||
|
|
@ -134,6 +141,12 @@ class MainWindow(QMainWindow):
|
||||||
about_action.triggered.connect(self.show_info)
|
about_action.triggered.connect(self.show_info)
|
||||||
help_menu.addAction(about_action)
|
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):
|
def _create_main_content(self):
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
|
|
@ -310,21 +323,34 @@ class MainWindow(QMainWindow):
|
||||||
self.object_combo.lineEdit().blockSignals(False)
|
self.object_combo.lineEdit().blockSignals(False)
|
||||||
|
|
||||||
def _load_saved_settings(self):
|
def _load_saved_settings(self):
|
||||||
|
"""Загружает сохранённые настройки"""
|
||||||
cameras = self.config_service.get_cameras()
|
cameras = self.config_service.get_cameras()
|
||||||
lenses = self.config_service.get_lenses()
|
lenses = self.config_service.get_lenses()
|
||||||
|
telescopes = self.config_service.get_telescopes() # <-- добавить
|
||||||
celestial_bodies = self.config_service.get_celestial_bodies()
|
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:
|
if cameras:
|
||||||
self.camera_combo.addItems(cameras)
|
self.camera_combo.addItems(cameras)
|
||||||
last_camera = self.config_service.get_last_camera()
|
last_camera = self.config_service.get_last_camera()
|
||||||
if last_camera and last_camera in cameras:
|
if last_camera and last_camera in cameras:
|
||||||
self.camera_combo.setCurrentText(last_camera)
|
self.camera_combo.setCurrentText(last_camera)
|
||||||
|
|
||||||
if lenses:
|
if all_optics:
|
||||||
self.lens_combo.addItems(lenses)
|
self.lens_combo.addItems(all_optics)
|
||||||
last_lens = self.config_service.get_last_lens()
|
last_lens = self.config_service.get_last_lens()
|
||||||
if last_lens and last_lens in lenses:
|
if last_lens:
|
||||||
self.lens_combo.setCurrentText(last_lens)
|
# Ищем последнюю использованную оптику
|
||||||
|
for opt in all_optics:
|
||||||
|
if last_lens in opt:
|
||||||
|
self.lens_combo.setCurrentText(opt)
|
||||||
|
break
|
||||||
|
|
||||||
if celestial_bodies:
|
if celestial_bodies:
|
||||||
self.object_combo.addItems(celestial_bodies)
|
self.object_combo.addItems(celestial_bodies)
|
||||||
|
|
@ -461,6 +487,13 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
camera_val = camera if camera else "Unknown"
|
camera_val = camera if camera else "Unknown"
|
||||||
lens_val = lens if lens 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)
|
self.session_service.start_session(watch_path, object_name, camera_val, lens_val)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue