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">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/models/camera_profile.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/models/equipment.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/services/calibration_service.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/dialogs/calibration_dialog.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/ui/dialogs/calibration_type_dialog.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/build_exe.spec" beforeDir="false" afterPath="$PROJECT_DIR$/build_exe.spec" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/models/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/models/astro_object.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/astro_object.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/models/session.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/session.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/services/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/astro_settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/astro_settings.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/services/config_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/config_service.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/services/file_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/file_service.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/services/session_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/session_service.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/services/watch_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/watch_service.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/dialogs/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/main_window.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/sound_manager.py" beforeDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
|
|
@ -82,7 +75,7 @@
|
|||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1778143911036</updated>
|
||||
<workItem from="1778143912090" duration="6772000" />
|
||||
<workItem from="1778143912090" duration="14992000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
|
@ -90,6 +83,6 @@
|
|||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778151943591" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778176299697" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
"Юпитер-21м",
|
||||
"Tamron 18-200mm"
|
||||
],
|
||||
"telescopes": [
|
||||
"Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)"
|
||||
],
|
||||
"last_watch_folder": "C:/Users/Juliette/Documents/testwatcher",
|
||||
"last_camera": "Canon 40D",
|
||||
"last_lens": "MTO-500A"
|
||||
|
|
|
|||
106
models/camera_profile.py
Normal file
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 os
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""Конфигурация приложения"""
|
||||
cameras: List[str]
|
||||
lenses: List[str]
|
||||
celestial_bodies: List[str]
|
||||
last_watch_folder: str
|
||||
last_camera: str
|
||||
last_lens: str
|
||||
|
||||
|
||||
class ConfigService:
|
||||
|
|
@ -25,14 +13,15 @@ class ConfigService:
|
|||
CELESTIAL_BODIES_FILE = "celestial_bodies.json"
|
||||
|
||||
def __init__(self):
|
||||
self.config = AppConfig(
|
||||
cameras=[],
|
||||
lenses=[],
|
||||
celestial_bodies=[],
|
||||
last_watch_folder="",
|
||||
last_camera="",
|
||||
last_lens=""
|
||||
)
|
||||
self.config = {
|
||||
'cameras': [],
|
||||
'lenses': [],
|
||||
'telescopes': [],
|
||||
'celestial_bodies': [],
|
||||
'last_watch_folder': '',
|
||||
'last_camera': '',
|
||||
'last_lens': ''
|
||||
}
|
||||
self.load_all()
|
||||
|
||||
def load_all(self):
|
||||
|
|
@ -40,8 +29,9 @@ class ConfigService:
|
|||
self._load_settings()
|
||||
self._load_celestial_bodies()
|
||||
|
||||
if not self.config.celestial_bodies:
|
||||
self.config.celestial_bodies = [
|
||||
# Если список небесных тел пуст - добавляем стандартные
|
||||
if not self.config['celestial_bodies']:
|
||||
self.config['celestial_bodies'] = [
|
||||
"M31 (Andromeda Galaxy)",
|
||||
"M42 (Orion Nebula)",
|
||||
"M45 (Pleiades)",
|
||||
|
|
@ -54,108 +44,151 @@ class ConfigService:
|
|||
self._save_celestial_bodies()
|
||||
|
||||
def _load_settings(self):
|
||||
"""Загружает основные настройки (камеры, объективы, телескопы, последнюю папку)"""
|
||||
if os.path.exists(self.SETTINGS_FILE):
|
||||
try:
|
||||
with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.config.cameras = data.get('cameras', [])
|
||||
self.config.lenses = data.get('lenses', [])
|
||||
self.config.last_watch_folder = data.get('last_watch_folder', '')
|
||||
self.config.last_camera = data.get('last_camera', '')
|
||||
self.config.last_lens = data.get('last_lens', '')
|
||||
self.config['cameras'] = data.get('cameras', [])
|
||||
self.config['lenses'] = data.get('lenses', [])
|
||||
self.config['telescopes'] = data.get('telescopes', [])
|
||||
self.config['last_watch_folder'] = data.get('last_watch_folder', '')
|
||||
self.config['last_camera'] = data.get('last_camera', '')
|
||||
self.config['last_lens'] = data.get('last_lens', '')
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки настроек: {e}")
|
||||
|
||||
def save_settings(self):
|
||||
"""Сохраняет основные настройки"""
|
||||
try:
|
||||
with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
'cameras': self.config.cameras,
|
||||
'lenses': self.config.lenses,
|
||||
'last_watch_folder': self.config.last_watch_folder,
|
||||
'last_camera': self.config.last_camera,
|
||||
'last_lens': self.config.last_lens
|
||||
'cameras': self.config['cameras'],
|
||||
'lenses': self.config['lenses'],
|
||||
'telescopes': self.config['telescopes'],
|
||||
'last_watch_folder': self.config['last_watch_folder'],
|
||||
'last_camera': self.config['last_camera'],
|
||||
'last_lens': self.config['last_lens']
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Ошибка сохранения настроек: {e}")
|
||||
|
||||
def _load_celestial_bodies(self):
|
||||
"""Загружает список небесных тел"""
|
||||
if os.path.exists(self.CELESTIAL_BODIES_FILE):
|
||||
try:
|
||||
with open(self.CELESTIAL_BODIES_FILE, 'r', encoding='utf-8') as f:
|
||||
self.config.celestial_bodies = json.load(f)
|
||||
self.config['celestial_bodies'] = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки небесных тел: {e}")
|
||||
|
||||
def _save_celestial_bodies(self):
|
||||
"""Сохраняет список небесных тел"""
|
||||
try:
|
||||
with open(self.CELESTIAL_BODIES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config.celestial_bodies, f, ensure_ascii=False, indent=2)
|
||||
json.dump(self.config['celestial_bodies'], f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Ошибка сохранения небесных тел: {e}")
|
||||
|
||||
# ===== Методы для работы с камерами =====
|
||||
|
||||
def get_cameras(self) -> List[str]:
|
||||
return self.config.cameras.copy()
|
||||
return self.config['cameras'].copy()
|
||||
|
||||
def add_camera(self, camera: str):
|
||||
if camera and camera not in self.config.cameras:
|
||||
self.config.cameras.append(camera)
|
||||
if camera and camera not in self.config['cameras']:
|
||||
self.config['cameras'].append(camera)
|
||||
self.save_settings()
|
||||
|
||||
def remove_camera(self, camera: str):
|
||||
if camera in self.config.cameras:
|
||||
self.config.cameras.remove(camera)
|
||||
if camera in self.config['cameras']:
|
||||
self.config['cameras'].remove(camera)
|
||||
self.save_settings()
|
||||
|
||||
# ===== Методы для работы с объективами =====
|
||||
|
||||
def get_lenses(self) -> List[str]:
|
||||
return self.config.lenses.copy()
|
||||
return self.config['lenses'].copy()
|
||||
|
||||
def add_lens(self, lens: str):
|
||||
if lens and lens not in self.config.lenses:
|
||||
self.config.lenses.append(lens)
|
||||
if lens and lens not in self.config['lenses']:
|
||||
self.config['lenses'].append(lens)
|
||||
self.save_settings()
|
||||
|
||||
def remove_lens(self, lens: str):
|
||||
if lens in self.config.lenses:
|
||||
self.config.lenses.remove(lens)
|
||||
if lens in self.config['lenses']:
|
||||
self.config['lenses'].remove(lens)
|
||||
self.save_settings()
|
||||
|
||||
def update_lens(self, old_name: str, new_name: str):
|
||||
if old_name in self.config['lenses']:
|
||||
idx = self.config['lenses'].index(old_name)
|
||||
self.config['lenses'][idx] = new_name
|
||||
self.save_settings()
|
||||
|
||||
# ===== Методы для работы с телескопами =====
|
||||
|
||||
def get_telescopes(self) -> List[str]:
|
||||
return self.config.get('telescopes', []).copy()
|
||||
|
||||
def add_telescope(self, telescope: str):
|
||||
if 'telescopes' not in self.config:
|
||||
self.config['telescopes'] = []
|
||||
if telescope and telescope not in self.config['telescopes']:
|
||||
self.config['telescopes'].append(telescope)
|
||||
self.save_settings()
|
||||
|
||||
def remove_telescope(self, telescope: str):
|
||||
if 'telescopes' in self.config and telescope in self.config['telescopes']:
|
||||
self.config['telescopes'].remove(telescope)
|
||||
self.save_settings()
|
||||
|
||||
def update_telescope(self, old_name: str, new_name: str):
|
||||
if 'telescopes' in self.config and old_name in self.config['telescopes']:
|
||||
idx = self.config['telescopes'].index(old_name)
|
||||
self.config['telescopes'][idx] = new_name
|
||||
self.save_settings()
|
||||
|
||||
# ===== Методы для работы с небесными телами =====
|
||||
|
||||
def get_celestial_bodies(self) -> List[str]:
|
||||
return self.config.celestial_bodies.copy()
|
||||
return self.config['celestial_bodies'].copy()
|
||||
|
||||
def add_celestial_body(self, name: str):
|
||||
if name and name not in self.config.celestial_bodies:
|
||||
self.config.celestial_bodies.append(name)
|
||||
if name and name not in self.config['celestial_bodies']:
|
||||
self.config['celestial_bodies'].append(name)
|
||||
self._save_celestial_bodies()
|
||||
|
||||
def remove_celestial_body(self, name: str):
|
||||
if name in self.config.celestial_bodies:
|
||||
self.config.celestial_bodies.remove(name)
|
||||
if name in self.config['celestial_bodies']:
|
||||
self.config['celestial_bodies'].remove(name)
|
||||
self._save_celestial_bodies()
|
||||
|
||||
def update_celestial_body(self, old_name: str, new_name: str):
|
||||
if old_name in self.config.celestial_bodies:
|
||||
idx = self.config.celestial_bodies.index(old_name)
|
||||
self.config.celestial_bodies[idx] = new_name
|
||||
if old_name in self.config['celestial_bodies']:
|
||||
idx = self.config['celestial_bodies'].index(old_name)
|
||||
self.config['celestial_bodies'][idx] = new_name
|
||||
self._save_celestial_bodies()
|
||||
|
||||
# ===== Методы для работы с последней папкой и оборудованием =====
|
||||
|
||||
def get_last_watch_folder(self) -> str:
|
||||
return self.config.last_watch_folder
|
||||
return self.config.get('last_watch_folder', '')
|
||||
|
||||
def set_last_watch_folder(self, folder: str):
|
||||
self.config.last_watch_folder = folder
|
||||
self.config['last_watch_folder'] = folder
|
||||
self.save_settings()
|
||||
|
||||
def get_last_camera(self) -> str:
|
||||
return self.config.last_camera
|
||||
return self.config.get('last_camera', '')
|
||||
|
||||
def set_last_camera(self, camera: str):
|
||||
self.config.last_camera = camera
|
||||
self.config['last_camera'] = camera
|
||||
self.save_settings()
|
||||
|
||||
def get_last_lens(self) -> str:
|
||||
return self.config.last_lens
|
||||
return self.config.get('last_lens', '')
|
||||
|
||||
def set_last_lens(self, lens: str):
|
||||
self.config.last_lens = lens
|
||||
self.config['last_lens'] = lens
|
||||
self.save_settings()
|
||||
|
|
@ -35,6 +35,22 @@ class FileService:
|
|||
return new_path
|
||||
counter += 1
|
||||
|
||||
@classmethod
|
||||
def generate_new_filename(cls, object_name: str, timestamp: datetime, original_suffix: str) -> str:
|
||||
"""
|
||||
Генерирует новое имя файла в формате:
|
||||
ИмяОбъекта_ГГГГ-ММ-ДД_ЧЧ-ММ-СС.расширение
|
||||
"""
|
||||
# Очищаем имя объекта от недопустимых символов
|
||||
safe_object_name = "".join(c for c in object_name if c.isalnum() or c in (' ', '-', '_')).strip()
|
||||
safe_object_name = safe_object_name.replace(' ', '_')
|
||||
|
||||
# Форматируем дату и время
|
||||
date_str = timestamp.strftime("%Y-%m-%d")
|
||||
time_str = timestamp.strftime("%H-%M-%S")
|
||||
|
||||
return f"{safe_object_name}_{date_str}_{time_str}{original_suffix}"
|
||||
|
||||
@classmethod
|
||||
def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str,
|
||||
timestamp: Optional[datetime] = None) -> None:
|
||||
|
|
@ -52,23 +68,45 @@ class FileService:
|
|||
print(f"Ошибка записи лога: {e}")
|
||||
|
||||
@classmethod
|
||||
def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str) -> bool:
|
||||
"""Перемещает файл в целевую папку"""
|
||||
def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str,
|
||||
object_name: str = None) -> bool:
|
||||
"""
|
||||
Перемещает файл в целевую папку с переименованием
|
||||
Если object_name указан, файл переименовывается в формат: ИмяОбъекта_дата_время.расширение
|
||||
"""
|
||||
if not source.exists():
|
||||
print(f"Файл не существует: {source}")
|
||||
return False
|
||||
|
||||
if not cls.is_photo(source):
|
||||
print(f"Неподдерживаемый формат: {source}")
|
||||
return False
|
||||
|
||||
try:
|
||||
target_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Получаем время создания файла
|
||||
creation_time = datetime.fromtimestamp(source.stat().st_ctime)
|
||||
cls.write_object_log(target_folder, source.name, camera, optics, creation_time)
|
||||
target_path = cls.resolve_conflict(target_folder / source.name)
|
||||
|
||||
# Генерируем новое имя файла, если указано имя объекта
|
||||
if object_name:
|
||||
new_filename = cls.generate_new_filename(object_name, creation_time, source.suffix)
|
||||
else:
|
||||
new_filename = source.name
|
||||
|
||||
# Записываем в лог (сохраняем оригинальное имя в логе для отслеживания)
|
||||
cls.write_object_log(target_folder, f"{source.name} -> {new_filename}", camera, optics, creation_time)
|
||||
|
||||
# Формируем целевой путь
|
||||
target_path = cls.resolve_conflict(target_folder / new_filename)
|
||||
|
||||
# Перемещаем файл
|
||||
shutil.move(str(source), str(target_path))
|
||||
print(f"Файл перемещён и переименован: {source.name} -> {target_path.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка перемещения {source.name}: {e}")
|
||||
print(f"Ошибка перемещения файла {source.name}: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class SessionService:
|
|||
return astro_object
|
||||
|
||||
def handle_file(self, file_path: Path) -> bool:
|
||||
"""Обрабатывает новый файл"""
|
||||
"""Обрабатывает новый файл: перемещает в папку текущего объекта с переименованием"""
|
||||
if not self._current_session:
|
||||
return False
|
||||
|
||||
|
|
@ -67,11 +67,13 @@ class SessionService:
|
|||
if not current_object:
|
||||
return False
|
||||
|
||||
# Передаём имя объекта для переименования файла
|
||||
success = self._file_service.move_file(
|
||||
file_path,
|
||||
current_object.folder,
|
||||
self._current_session.camera,
|
||||
self._current_session.optics
|
||||
self._current_session.optics,
|
||||
current_object.name # Добавляем имя объекта для переименования
|
||||
)
|
||||
|
||||
if success:
|
||||
|
|
@ -92,12 +94,14 @@ class SessionService:
|
|||
if watch_folder.exists():
|
||||
for file_path in watch_folder.iterdir():
|
||||
if file_path.is_file() and self._file_service.is_photo(file_path):
|
||||
if self._file_service.move_file(
|
||||
success = self._file_service.move_file(
|
||||
file_path,
|
||||
current_object.folder,
|
||||
self._current_session.camera,
|
||||
self._current_session.optics
|
||||
):
|
||||
self._current_session.optics,
|
||||
current_object.name # Добавляем имя объекта для переименования
|
||||
)
|
||||
if success:
|
||||
current_object.increment_photo_count()
|
||||
count += 1
|
||||
if on_file_moved:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,17 @@ class PhotoHandler(FileSystemEventHandler):
|
|||
if not event.is_directory:
|
||||
src_path = Path(event.src_path)
|
||||
if FileService.is_photo(src_path):
|
||||
time.sleep(0.1) # Даём время на запись файла
|
||||
print(f"[Watchdog] Обнаружен файл: {src_path}")
|
||||
time.sleep(0.1)
|
||||
self._pending_files.put(src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
"""Также обрабатываем modified, так как некоторые программы сначала создают временный файл"""
|
||||
if not event.is_directory:
|
||||
src_path = Path(event.src_path)
|
||||
if FileService.is_photo(src_path):
|
||||
print(f"[Watchdog] Изменён файл: {src_path}")
|
||||
time.sleep(0.1)
|
||||
self._pending_files.put(src_path)
|
||||
|
||||
def _process_queue(self):
|
||||
|
|
@ -52,24 +62,33 @@ class WatchService:
|
|||
self._is_running = False
|
||||
|
||||
def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool:
|
||||
"""Запускает отслеживание папки"""
|
||||
if self._is_running:
|
||||
print("Watcher already running")
|
||||
return False
|
||||
|
||||
if not watch_folder.exists():
|
||||
print(f"Папка не существует: {watch_folder}")
|
||||
return False
|
||||
|
||||
try:
|
||||
print(f"Запуск отслеживания папки: {watch_folder}")
|
||||
self._event_handler = PhotoHandler(on_new_file)
|
||||
self._observer = Observer()
|
||||
self._observer.schedule(self._event_handler, str(watch_folder), recursive=False)
|
||||
self._observer.start()
|
||||
self._is_running = True
|
||||
print(f"Отслеживание успешно запущено для: {watch_folder}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Ошибка запуска отслеживания: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def stop(self):
|
||||
"""Останавливает отслеживание"""
|
||||
print("Остановка отслеживания...")
|
||||
if self._observer:
|
||||
self._observer.stop()
|
||||
self._observer.join()
|
||||
|
|
@ -80,12 +99,13 @@ class WatchService:
|
|||
self._event_handler = None
|
||||
|
||||
self._is_running = False
|
||||
print("Отслеживание остановлено")
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self._is_running
|
||||
|
||||
def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int:
|
||||
"""Перемещает все существующие файлы"""
|
||||
"""Перемещает все существующие файлы из папки наблюдения"""
|
||||
count = 0
|
||||
if watch_folder.exists():
|
||||
for file_path in watch_folder.iterdir():
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,5 +1,8 @@
|
|||
from ui.dialogs.equipment_dialog import EquipmentDialog
|
||||
from ui.dialogs.celestial_dialog import CelestialDialog
|
||||
from ui.dialogs.instructions_dialog import InstructionsDialog
|
||||
from ui.dialogs.calibration_dialog import CalibrationDialog
|
||||
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
|
||||
|
||||
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog']
|
||||
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog',
|
||||
'CalibrationDialog', 'CalibrationTypeDialog']
|
||||
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 - диалог управления оборудованием (камеры и объективы)
|
||||
Аналог EquipmentDialogController из JavaFX версии
|
||||
EquipmentDialog - диалог управления оборудованием
|
||||
Камеры, объективы и телескопы
|
||||
"""
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
|
||||
QPushButton, QInputDialog, QMessageBox, QListWidgetItem
|
||||
QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget,
|
||||
QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
|
|
@ -13,26 +14,27 @@ from services.config_service import ConfigService
|
|||
|
||||
|
||||
class EquipmentDialog(QDialog):
|
||||
"""Диалог для управления списками камер и объективов"""
|
||||
"""Диалог для управления оборудованием"""
|
||||
|
||||
def __init__(self, parent, config_service: ConfigService):
|
||||
super().__init__(parent)
|
||||
|
||||
self.config_service = config_service
|
||||
self.setWindowTitle("Управление оборудованием")
|
||||
self.setMinimumSize(600, 400)
|
||||
self.resize(650, 450)
|
||||
self.setMinimumSize(700, 500)
|
||||
self.resize(800, 550)
|
||||
|
||||
# Загружаем текущие списки
|
||||
# Загружаем данные
|
||||
self.cameras = self.config_service.get_cameras()
|
||||
self.lenses = self.config_service.get_lenses()
|
||||
self.telescopes = self.config_service.get_telescopes()
|
||||
|
||||
self._create_ui()
|
||||
self._update_cameras_list()
|
||||
self._update_lenses_list()
|
||||
self._update_telescopes_list()
|
||||
|
||||
def _create_ui(self):
|
||||
"""Создаёт интерфейс диалога"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
|
@ -46,64 +48,22 @@ class EquipmentDialog(QDialog):
|
|||
title_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Контейнер для двух колонок
|
||||
columns_layout = QHBoxLayout()
|
||||
columns_layout.setSpacing(20)
|
||||
# Используем QTabWidget для трёх вкладок
|
||||
tab_widget = QTabWidget()
|
||||
|
||||
# Левая колонка - Камеры
|
||||
left_layout = QVBoxLayout()
|
||||
# Вкладка: Камеры
|
||||
cameras_tab = self._create_cameras_tab()
|
||||
tab_widget.addTab(cameras_tab, "📷 Камеры")
|
||||
|
||||
cameras_label = QLabel("Камеры")
|
||||
cameras_font = QFont()
|
||||
cameras_font.setPointSize(12)
|
||||
cameras_font.setBold(True)
|
||||
cameras_label.setFont(cameras_font)
|
||||
left_layout.addWidget(cameras_label)
|
||||
# Вкладка: Объективы
|
||||
lenses_tab = self._create_lenses_tab()
|
||||
tab_widget.addTab(lenses_tab, "🔭 Объективы")
|
||||
|
||||
self.cameras_list = QListWidget()
|
||||
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
|
||||
left_layout.addWidget(self.cameras_list)
|
||||
# Вкладка: Телескопы
|
||||
telescopes_tab = self._create_telescopes_tab()
|
||||
tab_widget.addTab(telescopes_tab, "🪐 Телескопы")
|
||||
|
||||
cameras_buttons_layout = QHBoxLayout()
|
||||
|
||||
add_camera_btn = QPushButton("➕ Добавить")
|
||||
add_camera_btn.clicked.connect(self._add_camera)
|
||||
cameras_buttons_layout.addWidget(add_camera_btn)
|
||||
|
||||
self.remove_camera_btn = QPushButton("❌ Удалить")
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_camera_btn.clicked.connect(self._remove_camera)
|
||||
cameras_buttons_layout.addWidget(self.remove_camera_btn)
|
||||
|
||||
left_layout.addLayout(cameras_buttons_layout)
|
||||
|
||||
# Правая колонка - Объективы
|
||||
right_layout = QVBoxLayout()
|
||||
|
||||
lenses_label = QLabel("Объективы")
|
||||
lenses_label.setFont(cameras_font)
|
||||
right_layout.addWidget(lenses_label)
|
||||
|
||||
self.lenses_list = QListWidget()
|
||||
self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
|
||||
right_layout.addWidget(self.lenses_list)
|
||||
|
||||
lenses_buttons_layout = QHBoxLayout()
|
||||
|
||||
add_lens_btn = QPushButton("➕ Добавить")
|
||||
add_lens_btn.clicked.connect(self._add_lens)
|
||||
lenses_buttons_layout.addWidget(add_lens_btn)
|
||||
|
||||
self.remove_lens_btn = QPushButton("❌ Удалить")
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
self.remove_lens_btn.clicked.connect(self._remove_lens)
|
||||
lenses_buttons_layout.addWidget(self.remove_lens_btn)
|
||||
|
||||
right_layout.addLayout(lenses_buttons_layout)
|
||||
|
||||
columns_layout.addLayout(left_layout)
|
||||
columns_layout.addLayout(right_layout)
|
||||
layout.addLayout(columns_layout)
|
||||
layout.addWidget(tab_widget)
|
||||
|
||||
# Кнопка закрытия
|
||||
close_btn = QPushButton("Закрыть")
|
||||
|
|
@ -113,90 +73,289 @@ class EquipmentDialog(QDialog):
|
|||
close_layout.addWidget(close_btn)
|
||||
layout.addLayout(close_layout)
|
||||
|
||||
def _create_cameras_tab(self) -> QWidget:
|
||||
"""Создаёт вкладку с камерами"""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
# Список камер
|
||||
self.cameras_list = QListWidget()
|
||||
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
|
||||
layout.addWidget(self.cameras_list)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
add_btn = QPushButton("➕ Добавить камеру")
|
||||
add_btn.clicked.connect(self._add_camera)
|
||||
buttons_layout.addWidget(add_btn)
|
||||
|
||||
self.remove_camera_btn = QPushButton("❌ Удалить")
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_camera_btn.clicked.connect(self._remove_camera)
|
||||
buttons_layout.addWidget(self.remove_camera_btn)
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
return tab
|
||||
|
||||
def _create_lenses_tab(self) -> QWidget:
|
||||
"""Создаёт вкладку с объективами"""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
# Список объективов
|
||||
self.lenses_list = QListWidget()
|
||||
self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
|
||||
layout.addWidget(self.lenses_list)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
add_btn = QPushButton("➕ Добавить объектив")
|
||||
add_btn.clicked.connect(self._add_lens)
|
||||
buttons_layout.addWidget(add_btn)
|
||||
|
||||
self.remove_lens_btn = QPushButton("❌ Удалить")
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
self.remove_lens_btn.clicked.connect(self._remove_lens)
|
||||
buttons_layout.addWidget(self.remove_lens_btn)
|
||||
|
||||
edit_btn = QPushButton("✏ Редактировать")
|
||||
edit_btn.clicked.connect(self._edit_lens)
|
||||
buttons_layout.addWidget(edit_btn)
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
return tab
|
||||
|
||||
def _create_telescopes_tab(self) -> QWidget:
|
||||
"""Создаёт вкладку с телескопами"""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
# Список телескопов
|
||||
self.telescopes_list = QListWidget()
|
||||
self.telescopes_list.itemClicked.connect(lambda item: self._select_telescope(item.text()))
|
||||
layout.addWidget(self.telescopes_list)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
add_btn = QPushButton("➕ Добавить телескоп")
|
||||
add_btn.clicked.connect(self._add_telescope)
|
||||
buttons_layout.addWidget(add_btn)
|
||||
|
||||
self.remove_telescope_btn = QPushButton("❌ Удалить")
|
||||
self.remove_telescope_btn.setEnabled(False)
|
||||
self.remove_telescope_btn.clicked.connect(self._remove_telescope)
|
||||
buttons_layout.addWidget(self.remove_telescope_btn)
|
||||
|
||||
edit_btn = QPushButton("✏ Редактировать")
|
||||
edit_btn.clicked.connect(self._edit_telescope)
|
||||
buttons_layout.addWidget(edit_btn)
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
return tab
|
||||
|
||||
# ===== Методы для камер =====
|
||||
|
||||
def _update_cameras_list(self):
|
||||
"""Обновляет отображение списка камер"""
|
||||
self.cameras_list.clear()
|
||||
for camera in self.cameras:
|
||||
self.cameras_list.addItem(camera)
|
||||
self._selected_camera = None
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
|
||||
def _update_lenses_list(self):
|
||||
"""Обновляет отображение списка объективов"""
|
||||
self.lenses_list.clear()
|
||||
for lens in self.lenses:
|
||||
self.lenses_list.addItem(lens)
|
||||
self._selected_lens = None
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
|
||||
def _select_camera(self, camera: str):
|
||||
"""Выделяет камеру в списке"""
|
||||
self._selected_camera = camera
|
||||
self.remove_camera_btn.setEnabled(True)
|
||||
|
||||
# Снимаем выделение с объективов
|
||||
self.lenses_list.clearSelection()
|
||||
self.telescopes_list.clearSelection()
|
||||
self._selected_lens = None
|
||||
self._selected_telescope = None
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
|
||||
def _select_lens(self, lens: str):
|
||||
"""Выделяет объектив в списке"""
|
||||
self._selected_lens = lens
|
||||
self.remove_lens_btn.setEnabled(True)
|
||||
|
||||
# Снимаем выделение с камер
|
||||
self.cameras_list.clearSelection()
|
||||
self._selected_camera = None
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_telescope_btn.setEnabled(False)
|
||||
|
||||
def _add_camera(self):
|
||||
"""Добавляет новую камеру"""
|
||||
new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
|
||||
if ok and new_camera and new_camera.strip():
|
||||
new_name = new_camera.strip()
|
||||
name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
|
||||
if ok and name and name.strip():
|
||||
new_name = name.strip()
|
||||
if new_name in self.cameras:
|
||||
QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!")
|
||||
return
|
||||
|
||||
self.cameras.append(new_name)
|
||||
self.config_service.add_camera(new_name)
|
||||
self._update_cameras_list()
|
||||
QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена")
|
||||
|
||||
def _remove_camera(self):
|
||||
"""Удаляет выбранную камеру"""
|
||||
if hasattr(self, '_selected_camera') and self._selected_camera:
|
||||
reply = QMessageBox.question(self, "Подтверждение",
|
||||
f"Удалить камеру '{self._selected_camera}'?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
f"Удалить камеру '{self._selected_camera}'?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.cameras.remove(self._selected_camera)
|
||||
self.config_service.remove_camera(self._selected_camera)
|
||||
self._update_cameras_list()
|
||||
QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена")
|
||||
|
||||
# ===== Методы для объективов =====
|
||||
|
||||
def _update_lenses_list(self):
|
||||
self.lenses_list.clear()
|
||||
for lens in self.lenses:
|
||||
self.lenses_list.addItem(lens)
|
||||
self._selected_lens = None
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
|
||||
def _select_lens(self, lens: str):
|
||||
self._selected_lens = lens
|
||||
self.remove_lens_btn.setEnabled(True)
|
||||
self.cameras_list.clearSelection()
|
||||
self.telescopes_list.clearSelection()
|
||||
self._selected_camera = None
|
||||
self._selected_telescope = None
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_telescope_btn.setEnabled(False)
|
||||
|
||||
def _add_lens(self):
|
||||
"""Добавляет новый объектив"""
|
||||
new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
|
||||
if ok and new_lens and new_lens.strip():
|
||||
new_name = new_lens.strip()
|
||||
name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
|
||||
if ok and name and name.strip():
|
||||
new_name = name.strip()
|
||||
if new_name in self.lenses:
|
||||
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
|
||||
return
|
||||
|
||||
self.lenses.append(new_name)
|
||||
self.config_service.add_lens(new_name)
|
||||
self._update_lenses_list()
|
||||
QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен")
|
||||
|
||||
def _remove_lens(self):
|
||||
"""Удаляет выбранный объектив"""
|
||||
if hasattr(self, '_selected_lens') and self._selected_lens:
|
||||
reply = QMessageBox.question(self, "Подтверждение",
|
||||
f"Удалить объектив '{self._selected_lens}'?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
f"Удалить объектив '{self._selected_lens}'?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.lenses.remove(self._selected_lens)
|
||||
self.config_service.remove_lens(self._selected_lens)
|
||||
self._update_lenses_list()
|
||||
QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён")
|
||||
|
||||
def _edit_lens(self):
|
||||
if hasattr(self, '_selected_lens') and self._selected_lens:
|
||||
new_name, ok = QInputDialog.getText(self, "Редактировать объектив",
|
||||
f"Изменить '{self._selected_lens}' на:",
|
||||
text=self._selected_lens)
|
||||
if ok and new_name and new_name.strip():
|
||||
new_name = new_name.strip()
|
||||
if new_name != self._selected_lens:
|
||||
if new_name in self.lenses:
|
||||
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
|
||||
return
|
||||
idx = self.lenses.index(self._selected_lens)
|
||||
self.lenses[idx] = new_name
|
||||
# Обновляем в конфиге (пока просто удаляем старый и добавляем новый)
|
||||
self.config_service.remove_lens(self._selected_lens)
|
||||
self.config_service.add_lens(new_name)
|
||||
self._update_lenses_list()
|
||||
|
||||
# ===== Методы для телескопов =====
|
||||
|
||||
def _update_telescopes_list(self):
|
||||
self.telescopes_list.clear()
|
||||
for telescope in self.telescopes:
|
||||
self.telescopes_list.addItem(telescope)
|
||||
self._selected_telescope = None
|
||||
self.remove_telescope_btn.setEnabled(False)
|
||||
|
||||
def _select_telescope(self, telescope: str):
|
||||
self._selected_telescope = telescope
|
||||
self.remove_telescope_btn.setEnabled(True)
|
||||
self.cameras_list.clearSelection()
|
||||
self.lenses_list.clearSelection()
|
||||
self._selected_camera = None
|
||||
self._selected_lens = None
|
||||
self.remove_camera_btn.setEnabled(False)
|
||||
self.remove_lens_btn.setEnabled(False)
|
||||
|
||||
def _add_telescope(self):
|
||||
"""Добавляет телескоп с указанием диафрагмы (фиксированной)"""
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle("Добавить телескоп")
|
||||
dialog.setMinimumWidth(400)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
form_layout = QFormLayout()
|
||||
|
||||
name_edit = QLineEdit()
|
||||
name_edit.setPlaceholderText("例如: Celestron 8\"")
|
||||
form_layout.addRow("Название:", name_edit)
|
||||
|
||||
aperture_spin = QDoubleSpinBox()
|
||||
aperture_spin.setRange(0.5, 20.0)
|
||||
aperture_spin.setSingleStep(0.1)
|
||||
aperture_spin.setValue(5.0)
|
||||
aperture_spin.setSuffix(" (f/)")
|
||||
form_layout.addRow("Диафрагма (f/):", aperture_spin)
|
||||
|
||||
focal_spin = QSpinBox()
|
||||
focal_spin.setRange(100, 5000)
|
||||
focal_spin.setSingleStep(50)
|
||||
focal_spin.setSuffix(" мм")
|
||||
form_layout.addRow("Фокусное расстояние:", focal_spin)
|
||||
|
||||
diameter_spin = QSpinBox()
|
||||
diameter_spin.setRange(50, 500)
|
||||
diameter_spin.setSingleStep(10)
|
||||
diameter_spin.setSuffix(" мм")
|
||||
form_layout.addRow("Диаметр объектива:", diameter_spin)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
ok_btn = QPushButton("OK")
|
||||
cancel_btn = QPushButton("Отмена")
|
||||
buttons_layout.addWidget(ok_btn)
|
||||
buttons_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
ok_btn.clicked.connect(dialog.accept)
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
|
||||
if dialog.exec():
|
||||
name = name_edit.text().strip()
|
||||
if name:
|
||||
telescope_info = f"{name} (f/{aperture_spin.value()}, F={focal_spin.value()}mm, D={diameter_spin.value()}mm)"
|
||||
if telescope_info not in self.telescopes:
|
||||
self.telescopes.append(telescope_info)
|
||||
self.config_service.add_telescope(telescope_info)
|
||||
self._update_telescopes_list()
|
||||
QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен")
|
||||
|
||||
def _remove_telescope(self):
|
||||
if hasattr(self, '_selected_telescope') and self._selected_telescope:
|
||||
reply = QMessageBox.question(self, "Подтверждение",
|
||||
f"Удалить телескоп '{self._selected_telescope}'?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if reply == QMessageBox.Yes:
|
||||
self.telescopes.remove(self._selected_telescope)
|
||||
self.config_service.remove_telescope(self._selected_telescope)
|
||||
self._update_telescopes_list()
|
||||
|
||||
def _edit_telescope(self):
|
||||
if hasattr(self, '_selected_telescope') and self._selected_telescope:
|
||||
new_name, ok = QInputDialog.getText(self, "Редактировать телескоп",
|
||||
f"Изменить '{self._selected_telescope}' на:",
|
||||
text=self._selected_telescope)
|
||||
if ok and new_name and new_name.strip():
|
||||
new_name = new_name.strip()
|
||||
if new_name != self._selected_telescope:
|
||||
if new_name in self.telescopes:
|
||||
QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!")
|
||||
return
|
||||
idx = self.telescopes.index(self._selected_telescope)
|
||||
self.telescopes[idx] = new_name
|
||||
self.config_service.remove_telescope(self._selected_telescope)
|
||||
self.config_service.add_telescope(new_name)
|
||||
self._update_telescopes_list()
|
||||
|
|
@ -119,6 +119,13 @@ class MainWindow(QMainWindow):
|
|||
new_object_action.triggered.connect(self.set_new_object)
|
||||
session_menu.addAction(new_object_action)
|
||||
|
||||
session_menu.addSeparator()
|
||||
|
||||
calibration_action = QAction("🌑 Калибровочные кадры...", self)
|
||||
calibration_action.setShortcut("Ctrl+K")
|
||||
calibration_action.triggered.connect(self.open_calibration_dialog)
|
||||
session_menu.addAction(calibration_action)
|
||||
|
||||
# Меню Помощь
|
||||
help_menu = menubar.addMenu("Помощь")
|
||||
|
||||
|
|
@ -134,6 +141,12 @@ class MainWindow(QMainWindow):
|
|||
about_action.triggered.connect(self.show_info)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def open_calibration_dialog(self):
|
||||
"""Открывает диалог калибровочных кадров"""
|
||||
from ui.dialogs.calibration_dialog import CalibrationDialog
|
||||
dialog = CalibrationDialog(self, self.config_service)
|
||||
dialog.exec()
|
||||
|
||||
def _create_main_content(self):
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
|
@ -310,21 +323,34 @@ class MainWindow(QMainWindow):
|
|||
self.object_combo.lineEdit().blockSignals(False)
|
||||
|
||||
def _load_saved_settings(self):
|
||||
"""Загружает сохранённые настройки"""
|
||||
cameras = self.config_service.get_cameras()
|
||||
lenses = self.config_service.get_lenses()
|
||||
telescopes = self.config_service.get_telescopes() # <-- добавить
|
||||
celestial_bodies = self.config_service.get_celestial_bodies()
|
||||
|
||||
# Объединяем объективы и телескопы для выбора оптики
|
||||
all_optics = []
|
||||
for lens in lenses:
|
||||
all_optics.append(f"🔭 {lens}")
|
||||
for telescope in telescopes:
|
||||
all_optics.append(f"🪐 {telescope}")
|
||||
|
||||
if cameras:
|
||||
self.camera_combo.addItems(cameras)
|
||||
last_camera = self.config_service.get_last_camera()
|
||||
if last_camera and last_camera in cameras:
|
||||
self.camera_combo.setCurrentText(last_camera)
|
||||
|
||||
if lenses:
|
||||
self.lens_combo.addItems(lenses)
|
||||
if all_optics:
|
||||
self.lens_combo.addItems(all_optics)
|
||||
last_lens = self.config_service.get_last_lens()
|
||||
if last_lens and last_lens in lenses:
|
||||
self.lens_combo.setCurrentText(last_lens)
|
||||
if last_lens:
|
||||
# Ищем последнюю использованную оптику
|
||||
for opt in all_optics:
|
||||
if last_lens in opt:
|
||||
self.lens_combo.setCurrentText(opt)
|
||||
break
|
||||
|
||||
if celestial_bodies:
|
||||
self.object_combo.addItems(celestial_bodies)
|
||||
|
|
@ -461,6 +487,13 @@ class MainWindow(QMainWindow):
|
|||
|
||||
camera_val = camera if camera else "Unknown"
|
||||
lens_val = lens if lens else "Unknown"
|
||||
optics_value = lens
|
||||
if optics_value.startswith("🔭 "):
|
||||
optics_value = optics_value[2:]
|
||||
elif optics_value.startswith("🪐 "):
|
||||
optics_value = optics_value[2:]
|
||||
|
||||
self.config_service.set_last_lens(optics_value)
|
||||
|
||||
self.session_service.start_session(watch_path, object_name, camera_val, lens_val)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue