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

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

View file

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

View file

@ -0,0 +1,281 @@
"""
CalibrationDialog - главный диалог калибровки
С выбором камеры, папки и типа кадров
"""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QComboBox, QLineEdit, QPushButton, QFrame,
QMessageBox, QFileDialog, QWidget
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QFont
from services.config_service import ConfigService
class CalibrationDialog(QDialog):
"""Главное окно калибровки"""
def __init__(self, parent, config_service: ConfigService):
super().__init__(parent)
self.config_service = config_service
self.setWindowTitle("🌑 Калибровочные кадры")
self.setMinimumSize(600, 450)
self.resize(650, 500)
self._create_ui()
self._load_saved_settings()
# Таймер для мигания кнопки "Обзор"
self._browse_blink_timer = None
self._check_folder_path()
def _create_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(20)
layout.setContentsMargins(25, 25, 25, 25)
# Заголовок
title_label = QLabel("🌑 Калибровочные кадры")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
# Основная сетка
grid = QGridLayout()
grid.setVerticalSpacing(15)
grid.setHorizontalSpacing(15)
# Строка 0: Камера
camera_label = QLabel("📷 Камера:")
camera_label.setFont(QFont("", 10, QFont.Bold))
grid.addWidget(camera_label, 0, 0)
self.camera_combo = QComboBox()
self.camera_combo.setEditable(True)
self.camera_combo.setMinimumWidth(250)
grid.addWidget(self.camera_combo, 0, 1)
# Строка 1: Папка
folder_label = QLabel("📁 Папка:")
folder_label.setFont(QFont("", 10, QFont.Bold))
grid.addWidget(folder_label, 1, 0)
folder_widget = QWidget()
folder_layout = QHBoxLayout(folder_widget)
folder_layout.setContentsMargins(0, 0, 0, 0)
folder_layout.setSpacing(10)
self.folder_entry = QLineEdit()
self.folder_entry.setPlaceholderText("Выберите папку для сохранения калибровочных кадров")
folder_layout.addWidget(self.folder_entry)
self.browse_button = QPushButton("✨ Обзор")
self.browse_button.setFixedWidth(100)
self.browse_button.clicked.connect(self._browse_folder)
folder_layout.addWidget(self.browse_button)
grid.addWidget(folder_widget, 1, 1)
layout.addLayout(grid)
# Разделитель
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setStyleSheet("background-color: #333333; max-height: 1px;")
layout.addWidget(separator)
# Кнопки типов кадров
types_layout = QHBoxLayout()
types_layout.setSpacing(20)
types_layout.setAlignment(Qt.AlignCenter)
self.bias_btn = QPushButton("⚪ BIAS")
self.bias_btn.setFixedSize(120, 50)
self.bias_btn.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
font-weight: bold;
font-size: 14px;
border-radius: 6px;
}
QPushButton:hover {
background-color: #1976D2;
}
""")
self.bias_btn.clicked.connect(lambda: self._open_calibration_type('bias'))
self.dark_btn = QPushButton("🌑 DARK")
self.dark_btn.setFixedSize(120, 50)
self.dark_btn.setStyleSheet("""
QPushButton {
background-color: #9C27B0;
color: white;
font-weight: bold;
font-size: 14px;
border-radius: 6px;
}
QPushButton:hover {
background-color: #7B1FA2;
}
""")
self.dark_btn.clicked.connect(lambda: self._open_calibration_type('dark'))
self.flat_btn = QPushButton("📖 FLAT")
self.flat_btn.setFixedSize(120, 50)
self.flat_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
font-size: 14px;
border-radius: 6px;
}
QPushButton:hover {
background-color: #388E3C;
}
""")
self.flat_btn.clicked.connect(lambda: self._open_calibration_type('flat'))
types_layout.addWidget(self.bias_btn)
types_layout.addWidget(self.dark_btn)
types_layout.addWidget(self.flat_btn)
layout.addLayout(types_layout)
# Совет
tips_frame = QFrame()
tips_frame.setStyleSheet("""
QFrame {
background-color: #2d2d2d;
border-radius: 8px;
padding: 10px;
}
""")
tips_layout = QVBoxLayout(tips_frame)
tips_title = QLabel("💡 Совет")
tips_title.setFont(QFont("", 11, QFont.Bold))
tips_layout.addWidget(tips_title)
self.tips_label = QLabel(
"• BIAS снимаются один раз на месяц (можно дома)\n"
"• DARK снимаются на месте съёмки при той же температуре\n"
"• FLAT снимаются после сессии без изменения фокуса"
)
self.tips_label.setWordWrap(True)
tips_layout.addWidget(self.tips_label)
layout.addWidget(tips_frame)
# Кнопки отмена/закрыть
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
cancel_btn = QPushButton("❌ Отмена")
cancel_btn.clicked.connect(self.reject)
buttons_layout.addWidget(cancel_btn)
layout.addLayout(buttons_layout)
def _load_saved_settings(self):
"""Загружает сохранённые камеры"""
cameras = self.config_service.get_cameras()
if cameras:
self.camera_combo.addItems(cameras)
last_camera = self.config_service.get_last_camera()
if last_camera and last_camera in cameras:
self.camera_combo.setCurrentText(last_camera)
def _browse_folder(self):
"""Выбор папки для калибровочных кадров"""
folder = QFileDialog.getExistingDirectory(self, "Выберите папку для калибровочных кадров")
if folder:
self.folder_entry.setText(folder)
self._stop_browse_blinking()
def _check_folder_path(self):
"""Проверяет, заполнено ли поле пути и запускает мигание если нет"""
if not self.folder_entry.text():
self._start_browse_blinking()
else:
self._stop_browse_blinking()
def _start_browse_blinking(self):
"""Запускает мигание кнопки 'Обзор' зелёным цветом"""
self._browse_blink_timer = QTimer()
self._browse_blink_timer.timeout.connect(self._do_browse_blink)
self._browse_blink_timer.start(500)
def _do_browse_blink(self):
"""Мигание кнопки"""
current_style = self.browse_button.styleSheet()
if "background-color: #4CAF50" in current_style:
self.browse_button.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
font-weight: bold;
border-radius: 4px;
}
""")
else:
self.browse_button.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
border-radius: 4px;
}
""")
def _stop_browse_blinking(self):
"""Останавливает мигание кнопки"""
if self._browse_blink_timer:
self._browse_blink_timer.stop()
self._browse_blink_timer = None
self.browse_button.setStyleSheet("""
QPushButton {
background-color: #2196F3;
color: white;
font-weight: bold;
border-radius: 4px;
}
""")
def _open_calibration_type(self, cal_type: str):
"""Открывает дочернее окно для выбранного типа калибровки"""
if not self.folder_entry.text():
QMessageBox.warning(self, "Внимание", "Сначала выберите папку для сохранения!")
self._start_browse_blinking()
return
camera_name = self.camera_combo.currentText()
if not camera_name:
QMessageBox.warning(self, "Внимание", "Введите или выберите название камеры!")
return
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
dialog = CalibrationTypeDialog(
self,
cal_type,
self.folder_entry.text(),
camera_name,
self.config_service
)
if dialog.exec():
# После успешной съёмки
QMessageBox.information(self, "Успех", f"Съёмка {cal_type.upper()} завершена!")
def reject(self):
"""Закрытие диалога"""
if hasattr(self, '_browse_blink_timer') and self._browse_blink_timer:
self._browse_blink_timer.stop()
super().reject()

View file

@ -0,0 +1,730 @@
"""
CalibrationTypeDialog - диалог для конкретного типа калибровки
Dark / Bias / Flat с прогрессом, авто-остановкой и профилями
"""
import shutil
import os
import re
from pathlib import Path
from datetime import datetime
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QComboBox, QSpinBox, QPushButton, QFrame,
QProgressBar, QMessageBox, QGroupBox,
QInputDialog, QWidget, QFileDialog
)
from PySide6.QtCore import Qt, QTimer, QMetaObject, Q_ARG, Signal
from PySide6.QtGui import QFont
from services.config_service import ConfigService
from services.file_service import FileService
from services.watch_service import WatchService
class CalibrationTypeDialog(QDialog):
"""Диалог для съёмки калибровочных кадров определённого типа"""
# Сигнал для безопасного обновления UI из другого потока
progress_updated = Signal(int, int) # current, target
capture_completed = Signal(str) # target_folder
def __init__(self, parent, cal_type: str, base_folder: str,
camera_name: str, config_service: ConfigService):
super().__init__(parent)
self.cal_type = cal_type # 'bias', 'dark', 'flat'
self.base_folder = Path(base_folder)
self.camera_name = camera_name
self.config_service = config_service
# Состояние съёмки
self.is_capturing = False
self.current_count = 0
self.target_count = 0
self._calibration_watch_service = None
# Настройки для разных типов
self.settings = self._get_default_settings()
self.setWindowTitle(self._get_title())
self.setMinimumSize(550, 600)
self.resize(600, 650)
# Подключаем сигналы
self.progress_updated.connect(self._on_progress_updated)
self.capture_completed.connect(self._on_capture_completed)
self._create_ui()
self._load_optics()
self._update_recommendations()
def _get_title(self) -> str:
titles = {
'bias': '⚪ BIAS (Кадры смещения)',
'dark': '🌑 DARK (Тёмные кадры)',
'flat': '📖 FLAT (Плоские поля)'
}
return titles.get(self.cal_type, 'Калибровочные кадры')
def _get_default_settings(self) -> dict:
"""Возвращает настройки по умолчанию для типа калибровки"""
base = {
'bias': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'count': 50,
'min_count': 30,
'max_count': 100,
'recommended_count': 50,
},
'dark': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'exposure_values': [30, 60, 120, 180, 300],
'default_exposure': 120,
'count': 20,
'min_count': 10,
'max_count': 50,
'recommended_count': 20,
},
'flat': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'aperture_values': ['f/2.8', 'f/4', 'f/5.6', 'f/8'],
'count': 30,
'min_count': 20,
'max_count': 60,
'recommended_count': 30,
}
}
return base.get(self.cal_type, {})
def _create_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Заголовок с кнопкой справки
header_layout = QHBoxLayout()
title_label = QLabel(self._get_title())
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
title_label.setFont(title_font)
header_layout.addWidget(title_label)
help_btn = QPushButton("")
help_btn.setFixedSize(30, 30)
help_btn.setToolTip("Показать справку")
help_btn.clicked.connect(self._show_help)
header_layout.addWidget(help_btn)
header_layout.addStretch()
layout.addLayout(header_layout)
# Группа параметров
params_group = QGroupBox("⚙️ Параметры съёмки")
params_layout = QGridLayout(params_group)
params_layout.setVerticalSpacing(12)
params_layout.setHorizontalSpacing(15)
row = 0
# ISO
iso_label = QLabel("ISO:")
iso_label.setFont(QFont("", 10, QFont.Bold))
params_layout.addWidget(iso_label, row, 0)
self.iso_combo = QComboBox()
self.iso_combo.addItems([str(v) for v in self.settings['iso_values']])
self.iso_combo.setCurrentText(str(self.settings['default_iso']))
self.iso_combo.currentTextChanged.connect(self._update_recommendations)
params_layout.addWidget(self.iso_combo, row, 1)
self.custom_iso_btn = QPushButton(" своё")
self.custom_iso_btn.setFixedWidth(60)
self.custom_iso_btn.clicked.connect(self._add_custom_iso)
params_layout.addWidget(self.custom_iso_btn, row, 2)
row += 1
# Выдержка (только для DARK)
if self.cal_type == 'dark':
exposure_label = QLabel("Выдержка (сек):")
exposure_label.setFont(QFont("", 10, QFont.Bold))
params_layout.addWidget(exposure_label, row, 0)
self.exposure_combo = QComboBox()
self.exposure_combo.addItems([str(v) for v in self.settings['exposure_values']])
self.exposure_combo.setCurrentText(str(self.settings['default_exposure']))
self.exposure_combo.currentTextChanged.connect(self._update_recommendations)
params_layout.addWidget(self.exposure_combo, row, 1)
self.custom_exposure_btn = QPushButton(" своё")
self.custom_exposure_btn.setFixedWidth(60)
self.custom_exposure_btn.clicked.connect(self._add_custom_exposure)
params_layout.addWidget(self.custom_exposure_btn, row, 2)
row += 1
# Оптика (только для FLAT)
if self.cal_type == 'flat':
optics_label = QLabel("Оптика:")
optics_label.setFont(QFont("", 10, QFont.Bold))
params_layout.addWidget(optics_label, row, 0)
self.optics_combo = QComboBox()
self.optics_combo.setEditable(True)
params_layout.addWidget(self.optics_combo, row, 1, 1, 2)
row += 1
aperture_label = QLabel("Диафрагма:")
aperture_label.setFont(QFont("", 10, QFont.Bold))
params_layout.addWidget(aperture_label, row, 0)
self.aperture_combo = QComboBox()
self.aperture_combo.setEditable(True)
self.aperture_combo.addItems(self.settings['aperture_values'])
params_layout.addWidget(self.aperture_combo, row, 1, 1, 2)
row += 1
telescope_hint = QLabel("💡 Для телескопов диафрагма фиксированная и выбирается автоматически")
telescope_hint.setStyleSheet("color: #888888; font-size: 10px;")
params_layout.addWidget(telescope_hint, row, 0, 1, 3)
row += 1
# Количество кадров
count_label = QLabel("Количество кадров:")
count_label.setFont(QFont("", 10, QFont.Bold))
params_layout.addWidget(count_label, row, 0)
self.count_spin = QSpinBox()
self.count_spin.setMinimum(self.settings['min_count'])
self.count_spin.setMaximum(self.settings['max_count'])
self.count_spin.setValue(self.settings['count'])
self.count_spin.setSuffix(" кадров")
params_layout.addWidget(self.count_spin, row, 1)
self.recommended_label = QLabel(f"(рекомендуется {self.settings['recommended_count']})")
self.recommended_label.setStyleSheet("color: #888888;")
params_layout.addWidget(self.recommended_label, row, 2)
layout.addWidget(params_group)
# Группа рекомендаций
tips_group = QGroupBox("📖 Рекомендации")
tips_group.setStyleSheet("""
QGroupBox {
font-weight: bold;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
""")
tips_layout = QVBoxLayout(tips_group)
self.tips_text = QLabel()
self.tips_text.setWordWrap(True)
self.tips_text.setStyleSheet("color: #FFD700; padding: 5px;")
tips_layout.addWidget(self.tips_text)
layout.addWidget(tips_group)
# Группа прогресса
self.progress_group = QGroupBox("📊 Прогресс съёмки")
self.progress_group.setVisible(False)
progress_layout = QVBoxLayout(self.progress_group)
self.progress_bar = QProgressBar()
self.progress_bar.setMinimum(0)
progress_layout.addWidget(self.progress_bar)
self.progress_status = QLabel("Готов к съёмке")
self.progress_status.setAlignment(Qt.AlignCenter)
progress_layout.addWidget(self.progress_status)
layout.addWidget(self.progress_group)
# Информация о сохранении
save_info = QFrame()
save_info.setStyleSheet("""
QFrame {
background-color: #2d2d2d;
border-radius: 6px;
padding: 8px;
}
""")
save_layout = QVBoxLayout(save_info)
save_label = QLabel("💾 Сохранить в:")
save_label.setFont(QFont("", 10, QFont.Bold))
save_layout.addWidget(save_label)
self.save_path_label = QLabel()
self.save_path_label.setWordWrap(True)
self.save_path_label.setStyleSheet("color: #4CAF50; font-family: monospace;")
save_layout.addWidget(self.save_path_label)
layout.addWidget(save_info)
# Кнопки действий
buttons_layout = QHBoxLayout()
buttons_layout.addStretch()
self.back_btn = QPushButton("◀ Назад")
self.back_btn.clicked.connect(self._on_back_clicked)
buttons_layout.addWidget(self.back_btn)
self.start_btn = QPushButton("▶ Начать съёмку")
self.start_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
font-weight: bold;
padding: 8px 20px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #388E3C;
}
""")
self.start_btn.clicked.connect(self._start_capture)
buttons_layout.addWidget(self.start_btn)
self.stop_btn = QPushButton("⏹️ Остановить")
self.stop_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
font-weight: bold;
padding: 8px 20px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
self.stop_btn.clicked.connect(self._on_stop_clicked)
self.stop_btn.setVisible(False)
buttons_layout.addWidget(self.stop_btn)
layout.addLayout(buttons_layout)
self._update_save_path()
def _load_optics(self):
"""Загружает список оптики (объективы + телескопы) для FLAT режима"""
if self.cal_type != 'flat':
return
lenses = self.config_service.get_lenses()
telescopes = self.config_service.get_telescopes()
all_optics = []
for lens in lenses:
all_optics.append(f"🔭 {lens}")
for telescope in telescopes:
all_optics.append(f"🪐 {telescope}")
self.optics_combo.addItems(all_optics)
def on_optics_changed():
current = self.optics_combo.currentText()
if current.startswith("🪐"):
self.aperture_combo.setEnabled(False)
match = re.search(r'f/(\d+\.?\d*)', current)
if match:
self.aperture_combo.setCurrentText(f"f/{match.group(1)}")
else:
self.aperture_combo.setEnabled(True)
if all_optics:
self.optics_combo.currentTextChanged.connect(on_optics_changed)
def _update_save_path(self):
"""Обновляет отображение пути сохранения"""
iso = int(self.iso_combo.currentText())
if self.cal_type == 'bias':
path = self.base_folder / "Calibration" / self.camera_name / "Bias" / f"ISO{iso}"
elif self.cal_type == 'dark':
exposure = self.exposure_combo.currentText()
path = self.base_folder / "Calibration" / self.camera_name / "Dark" / f"ISO{iso}_{exposure}s"
elif self.cal_type == 'flat':
optics = self.optics_combo.currentText()
optics_name = optics.replace("🔭", "").replace("🪐", "").strip()
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
optics_name = optics_name.replace(char, '_')
date_str = datetime.now().strftime("%Y-%m-%d")
path = self.base_folder / "Calibration" / self.camera_name / "Flat" / optics_name / date_str
else:
path = self.base_folder
self.save_path_label.setText(str(path))
return path
def _update_recommendations(self):
"""Обновляет рекомендации в зависимости от типа калибровки"""
if self.cal_type == 'bias':
self.tips_text.setText(
"⚪ BIAS (Кадры смещения)\n\n"
"📌 КАК СНИМАТЬ:\n"
"• Закройте крышку объектива\n"
"• Выдержка: САМАЯ КОРОТКАЯ (1/4000 или 1/8000)\n"
"• ISO: тот же, что при съёмке световых кадров\n\n"
"💡 СОВЕТ:\n"
"• Можно снять дома в любое время\n"
"• Используются для всех объективов\n"
"• 50 кадров оптимально для хорошего усреднения"
)
elif self.cal_type == 'dark':
self.tips_text.setText(
"🌑 DARK (Тёмные кадры)\n\n"
"⚠️ ВАЖНО: Снимайте ПОСЛЕ сессии на месте!\n\n"
"📌 КАК СНИМАТЬ:\n"
"• Закройте крышку объектива\n"
"ТЕ ЖЕ параметры ISO и выдержки, что при съёмке\n"
"• Дождитесь, пока камера прогреется до ночной температуры\n\n"
"🌡️ ТЕМПЕРАТУРА:\n"
"• Снимайте ПРИ ТОЙ ЖЕ температуре, что и Light кадры\n"
"• Разница >5°C делает кадры бесполезными!\n"
"• Лучше снять сразу после сессии, пока камера не остыла"
)
elif self.cal_type == 'flat':
self.tips_text.setText(
"📖 FLAT (Плоские поля)\n\n"
"⚠️ ВАЖНО: НЕ меняйте фокус и зум после съёмки!\n\n"
"📌 КАК СНИМАТЬ:\n"
"• Способ 1: LED-планшет (рекомендуется)\n"
"• Способ 2: Рассвет/закат, камера в зенит\n"
"• Способ 3: Белая футболка на объектив\n\n"
"🎯 ЦЕЛЬ:\n"
"• Убрать виньетирование и пыль на оптике\n"
"• Гистограмма должна быть на 50-70%\n"
"• 30 кадров достаточно для хорошего результата"
)
self._update_save_path()
def _start_capture(self):
"""Начинает съёмку калибровочных кадров"""
self.target_count = self.count_spin.value()
self.current_count = 0
target_folder = self._update_save_path()
# Создаём папку с проверкой
try:
target_folder.mkdir(parents=True, exist_ok=True)
print(f"Папка создана: {target_folder}")
except Exception as e:
QMessageBox.critical(self, "Ошибка", f"Не удалось создать папку:\n{target_folder}\n\nОшибка: {e}")
return
self.progress_group.setVisible(True)
self.progress_bar.setMaximum(self.target_count)
self.progress_bar.setValue(0)
self.progress_status.setText(f"0 из {self.target_count} кадров")
# Меняем кнопки
self.start_btn.setVisible(False)
self.stop_btn.setVisible(True)
self.back_btn.setEnabled(False)
# Блокируем изменение параметров
self.iso_combo.setEnabled(False)
self.count_spin.setEnabled(False)
if self.cal_type == 'dark':
self.exposure_combo.setEnabled(False)
if self.cal_type == 'flat':
self.optics_combo.setEnabled(False)
self.aperture_combo.setEnabled(False)
self.is_capturing = True
# Получаем папку наблюдения
watch_folder = self._get_watch_folder()
print(f"Получена папка наблюдения: {watch_folder}")
if not watch_folder:
QMessageBox.critical(self, "Ошибка",
"Не удалось определить папку наблюдения!\nУбедитесь, что вы выбрали папку в главном окне.")
self._stop_capture()
return
if not watch_folder.exists():
QMessageBox.critical(self, "Ошибка", f"Папка наблюдения не существует:\n{watch_folder}")
self._stop_capture()
return
# Очищаем папку наблюдения от старых файлов
FileService.clear_watch_folder(watch_folder)
# Создаём НОВЫЙ WatchService для калибровки
self._calibration_watch_service = WatchService()
# Функция обратного вызова при получении файла (выполняется в потоке WatchService)
def on_file_received(file_path: Path):
if not self.is_capturing:
return
print(f"Обнаружен файл: {file_path}")
if self._process_calibration_file(file_path, target_folder):
# Увеличиваем счётчик
new_count = self.current_count + 1
# Отправляем сигнал для обновления UI в главном потоке
self.progress_updated.emit(new_count, self.target_count)
if new_count >= self.target_count:
# Отправляем сигнал о завершении
self.capture_completed.emit(str(target_folder))
print("Запуск WatchService для калибровки...")
success = self._calibration_watch_service.start(watch_folder, on_file_received)
print(f"Результат запуска: {success}")
if not success:
QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки!")
self._stop_capture()
return
self.progress_status.setText(f"Отслеживается папка: {watch_folder}\nОжидание новых файлов...")
def _on_progress_updated(self, current: int, target: int):
"""Обновляет прогресс (вызывается из основного потока по сигналу)"""
self.current_count = current
self.progress_bar.setValue(current)
self.progress_status.setText(f"Снято {current} из {target} кадров")
print(f"Прогресс: {current}/{target}")
def _on_capture_completed(self, target_folder: str):
"""Обработчик завершения съёмки (вызывается из основного потока по сигналу)"""
if self.is_capturing:
self._stop_capture()
QMessageBox.information(self, "Успех",
f"✅ Съёмка завершена!\n"
f"Сохранено {self.current_count} кадров в:\n{target_folder}")
# Скрываем группу прогресса после сообщения
self.progress_group.setVisible(False)
def _process_calibration_file(self, file_path: Path, target_folder: Path) -> bool:
"""Обрабатывает файл из папки наблюдения"""
if not FileService.is_photo(file_path):
print(f"Файл {file_path.name} не является фото, пропускаем")
return False
try:
target_folder.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now()
date_str = timestamp.strftime("%Y-%m-%d")
time_str = timestamp.strftime("%H-%M-%S")
suffix = file_path.suffix
if self.cal_type == 'bias':
iso = self.iso_combo.currentText()
prefix = f"Bias_{self.camera_name}_ISO{iso}"
elif self.cal_type == 'dark':
iso = self.iso_combo.currentText()
exposure = self.exposure_combo.currentText()
prefix = f"Dark_{self.camera_name}_ISO{iso}_{exposure}s"
elif self.cal_type == 'flat':
optics = self.optics_combo.currentText()
optics_name = optics.replace("🔭", "").replace("🪐", "").strip()
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
optics_name = optics_name.replace(char, '_')
aperture = self.aperture_combo.currentText()
prefix = f"Flat_{optics_name}_{aperture}"
else:
prefix = "Calibration"
for char in '<>:"/\\|?*':
prefix = prefix.replace(char, '_')
new_filename = f"{prefix}_{date_str}_{time_str}{suffix}"
target_path = target_folder / new_filename
target_path = FileService.resolve_conflict(target_path)
shutil.move(str(file_path), str(target_path))
print(f"Файл сохранён: {target_path}")
return True
except Exception as e:
print(f"Ошибка сохранения {file_path.name}: {e}")
return False
def _stop_capture(self):
"""Останавливает съёмку"""
self.is_capturing = False
if self._calibration_watch_service:
self._calibration_watch_service.stop()
self._calibration_watch_service = None
self.start_btn.setVisible(True)
self.stop_btn.setVisible(False)
self.back_btn.setEnabled(True)
self.iso_combo.setEnabled(True)
self.count_spin.setEnabled(True)
if self.cal_type == 'dark':
self.exposure_combo.setEnabled(True)
if self.cal_type == 'flat':
self.optics_combo.setEnabled(True)
self.aperture_combo.setEnabled(True)
self.progress_status.setText("Съёмка остановлена")
# Скрываем группу прогресса через 2 секунды
QTimer.singleShot(2000, lambda: self.progress_group.setVisible(False))
def _on_back_clicked(self):
"""Обработчик кнопки 'Назад'"""
if self.is_capturing:
QMessageBox.warning(self, "Внимание", "Сначала остановите съёмку!")
return
self.reject()
def _on_stop_clicked(self):
"""Обработчик кнопки 'Остановить' с подтверждением"""
if self.current_count < self.target_count and self.current_count > 0:
reply = QMessageBox.question(self, "Прервать съёмку?",
f"Вы не закончили съёмку (снято {self.current_count} из {self.target_count} кадров).\n"
f"Вы действительно хотите прервать?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self._stop_capture()
self.progress_group.setVisible(False)
elif self.current_count == 0:
reply = QMessageBox.question(self, "Прервать съёмку?",
"Съёмка ещё не начата. Вы действительно хотите выйти?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self._stop_capture()
self.progress_group.setVisible(False)
else:
self._stop_capture()
self.progress_group.setVisible(False)
def _get_watch_folder(self) -> Optional[Path]:
"""Возвращает папку наблюдения из главного окна"""
print("Поиск папки наблюдения...")
parent = self.parent()
print(f"Родительское окно: {parent}")
while parent and not hasattr(parent, 'folder_entry'):
parent = parent.parent()
print(f"Поднимаемся выше: {parent}")
if parent and hasattr(parent, 'folder_entry'):
watch_folder = parent.folder_entry.text()
print(f"Нашли folder_entry, значение: {watch_folder}")
if watch_folder:
path = Path(watch_folder)
print(f"Папка наблюдения: {path}")
return path
print("Не удалось найти папку наблюдения в родительском окне")
folder = QFileDialog.getExistingDirectory(self, "Выберите папку наблюдения (куда камера сохраняет файлы)")
if folder:
print(f"Пользователь выбрал: {folder}")
return Path(folder)
return None
def _add_custom_iso(self):
custom_iso, ok = QInputDialog.getInt(self, "Свой ISO",
"Введите значение ISO:", 800, 100, 12800)
if ok and custom_iso:
iso_str = str(custom_iso)
if self.iso_combo.findText(iso_str) == -1:
self.iso_combo.addItem(iso_str)
self.iso_combo.setCurrentText(iso_str)
def _add_custom_exposure(self):
custom_exp, ok = QInputDialog.getInt(self, "Своя выдержка",
"Введите выдержку (секунд):", 120, 1, 3600)
if ok and custom_exp:
exp_str = str(custom_exp)
if self.exposure_combo.findText(exp_str) == -1:
self.exposure_combo.addItem(exp_str)
self.exposure_combo.setCurrentText(exp_str)
def _show_help(self):
if self.cal_type == 'bias':
help_text = (
"Что такое BIAS?\n\n"
"Bias (кадры смещения) — это снимки с закрытой крышкой\n"
"на минимально возможной выдержке.\n\n"
"Зачем нужны:\n"
"• Убирают read noise (шум считывания)\n"
"• Корректируют смещение чёрного уровня\n\n"
"Сколько снимать:\n"
"• 50 кадров для хорошего усреднения\n"
"• Можно использовать весь месяц\n\n"
"Когда снимать:\n"
"• Дома в любое время\n"
"• Температура не важна"
)
elif self.cal_type == 'dark':
help_text = (
"Что такое DARK?\n\n"
"Dark (тёмные кадры) — это снимки с закрытой крышкой\n"
"с ТЕМИ ЖЕ параметрами ISO и выдержки, что и световые кадры.\n\n"
"Зачем нужны:\n"
"• Убирают тепловой шум матрицы\n"
"• Убирают горячие пиксели\n\n"
"Сколько снимать:\n"
"• 20-30 кадров для хорошего результата\n\n"
"⚠️ ВАЖНО про температуру:\n"
"• Снимайте ПОСЛЕ сессии на месте!\n"
"• Камера должна быть при той же температуре\n"
"• Разница >5°C делает кадры бесполезными!"
)
else:
help_text = (
"Что такое FLAT?\n\n"
"Flat (плоские поля) — это снимки равномерно освещённой\n"
"поверхности с ТЕМИ ЖЕ фокусом и зумом.\n\n"
"Зачем нужны:\n"
"• Убирают виньетирование объектива\n"
"• Убирают пыль на матрице и оптике\n\n"
"Как снимать:\n"
"1. LED-планшет (лучший вариант)\n"
"2. Рассвет/закат, камера в зенит\n"
"3. Белая футболка на объектив\n\n"
"Сколько снимать:\n"
"• 30 кадров для хорошего усреднения\n\n"
"⚠️ ВАЖНО:\n"
"НЕ меняйте фокус!\n"
"НЕ меняйте зум!\n"
"• Снимайте в конце сессии"
)
QMessageBox.information(self, "Справка", help_text)
def closeEvent(self, event):
if self.is_capturing:
reply = QMessageBox.question(self, "Прервать съёмку?",
"Съёмка активна. Вы действительно хотите закрыть окно?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self._stop_capture()
event.accept()
else:
event.ignore()
else:
event.accept()

View file

@ -1,10 +1,11 @@
"""
EquipmentDialog - диалог управления оборудованием (камеры и объективы)
Аналог EquipmentDialogController из JavaFX версии
EquipmentDialog - диалог управления оборудованием
Камеры, объективы и телескопы
"""
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
QPushButton, QInputDialog, QMessageBox, QListWidgetItem
QPushButton, QInputDialog, QMessageBox, QWidget, QTabWidget,
QFormLayout, QDoubleSpinBox, QSpinBox, QLineEdit
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
@ -13,26 +14,27 @@ from services.config_service import ConfigService
class EquipmentDialog(QDialog):
"""Диалог для управления списками камер и объективов"""
"""Диалог для управления оборудованием"""
def __init__(self, parent, config_service: ConfigService):
super().__init__(parent)
self.config_service = config_service
self.setWindowTitle("Управление оборудованием")
self.setMinimumSize(600, 400)
self.resize(650, 450)
self.setMinimumSize(700, 500)
self.resize(800, 550)
# Загружаем текущие списки
# Загружаем данные
self.cameras = self.config_service.get_cameras()
self.lenses = self.config_service.get_lenses()
self.telescopes = self.config_service.get_telescopes()
self._create_ui()
self._update_cameras_list()
self._update_lenses_list()
self._update_telescopes_list()
def _create_ui(self):
"""Создаёт интерфейс диалога"""
layout = QVBoxLayout(self)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
@ -46,64 +48,22 @@ class EquipmentDialog(QDialog):
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Контейнер для двух колонок
columns_layout = QHBoxLayout()
columns_layout.setSpacing(20)
# Используем QTabWidget для трёх вкладок
tab_widget = QTabWidget()
# Левая колонка - Камеры
left_layout = QVBoxLayout()
# Вкладка: Камеры
cameras_tab = self._create_cameras_tab()
tab_widget.addTab(cameras_tab, "📷 Камеры")
cameras_label = QLabel("Камеры")
cameras_font = QFont()
cameras_font.setPointSize(12)
cameras_font.setBold(True)
cameras_label.setFont(cameras_font)
left_layout.addWidget(cameras_label)
# Вкладка: Объективы
lenses_tab = self._create_lenses_tab()
tab_widget.addTab(lenses_tab, "🔭 Объективы")
self.cameras_list = QListWidget()
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
left_layout.addWidget(self.cameras_list)
# Вкладка: Телескопы
telescopes_tab = self._create_telescopes_tab()
tab_widget.addTab(telescopes_tab, "🪐 Телескопы")
cameras_buttons_layout = QHBoxLayout()
add_camera_btn = QPushButton(" Добавить")
add_camera_btn.clicked.connect(self._add_camera)
cameras_buttons_layout.addWidget(add_camera_btn)
self.remove_camera_btn = QPushButton("❌ Удалить")
self.remove_camera_btn.setEnabled(False)
self.remove_camera_btn.clicked.connect(self._remove_camera)
cameras_buttons_layout.addWidget(self.remove_camera_btn)
left_layout.addLayout(cameras_buttons_layout)
# Правая колонка - Объективы
right_layout = QVBoxLayout()
lenses_label = QLabel("Объективы")
lenses_label.setFont(cameras_font)
right_layout.addWidget(lenses_label)
self.lenses_list = QListWidget()
self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
right_layout.addWidget(self.lenses_list)
lenses_buttons_layout = QHBoxLayout()
add_lens_btn = QPushButton(" Добавить")
add_lens_btn.clicked.connect(self._add_lens)
lenses_buttons_layout.addWidget(add_lens_btn)
self.remove_lens_btn = QPushButton("❌ Удалить")
self.remove_lens_btn.setEnabled(False)
self.remove_lens_btn.clicked.connect(self._remove_lens)
lenses_buttons_layout.addWidget(self.remove_lens_btn)
right_layout.addLayout(lenses_buttons_layout)
columns_layout.addLayout(left_layout)
columns_layout.addLayout(right_layout)
layout.addLayout(columns_layout)
layout.addWidget(tab_widget)
# Кнопка закрытия
close_btn = QPushButton("Закрыть")
@ -113,90 +73,289 @@ class EquipmentDialog(QDialog):
close_layout.addWidget(close_btn)
layout.addLayout(close_layout)
def _create_cameras_tab(self) -> QWidget:
"""Создаёт вкладку с камерами"""
tab = QWidget()
layout = QVBoxLayout(tab)
# Список камер
self.cameras_list = QListWidget()
self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
layout.addWidget(self.cameras_list)
# Кнопки
buttons_layout = QHBoxLayout()
add_btn = QPushButton(" Добавить камеру")
add_btn.clicked.connect(self._add_camera)
buttons_layout.addWidget(add_btn)
self.remove_camera_btn = QPushButton("❌ Удалить")
self.remove_camera_btn.setEnabled(False)
self.remove_camera_btn.clicked.connect(self._remove_camera)
buttons_layout.addWidget(self.remove_camera_btn)
layout.addLayout(buttons_layout)
return tab
def _create_lenses_tab(self) -> QWidget:
"""Создаёт вкладку с объективами"""
tab = QWidget()
layout = QVBoxLayout(tab)
# Список объективов
self.lenses_list = QListWidget()
self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
layout.addWidget(self.lenses_list)
# Кнопки
buttons_layout = QHBoxLayout()
add_btn = QPushButton(" Добавить объектив")
add_btn.clicked.connect(self._add_lens)
buttons_layout.addWidget(add_btn)
self.remove_lens_btn = QPushButton("❌ Удалить")
self.remove_lens_btn.setEnabled(False)
self.remove_lens_btn.clicked.connect(self._remove_lens)
buttons_layout.addWidget(self.remove_lens_btn)
edit_btn = QPushButton("✏ Редактировать")
edit_btn.clicked.connect(self._edit_lens)
buttons_layout.addWidget(edit_btn)
layout.addLayout(buttons_layout)
return tab
def _create_telescopes_tab(self) -> QWidget:
"""Создаёт вкладку с телескопами"""
tab = QWidget()
layout = QVBoxLayout(tab)
# Список телескопов
self.telescopes_list = QListWidget()
self.telescopes_list.itemClicked.connect(lambda item: self._select_telescope(item.text()))
layout.addWidget(self.telescopes_list)
# Кнопки
buttons_layout = QHBoxLayout()
add_btn = QPushButton(" Добавить телескоп")
add_btn.clicked.connect(self._add_telescope)
buttons_layout.addWidget(add_btn)
self.remove_telescope_btn = QPushButton("❌ Удалить")
self.remove_telescope_btn.setEnabled(False)
self.remove_telescope_btn.clicked.connect(self._remove_telescope)
buttons_layout.addWidget(self.remove_telescope_btn)
edit_btn = QPushButton("✏ Редактировать")
edit_btn.clicked.connect(self._edit_telescope)
buttons_layout.addWidget(edit_btn)
layout.addLayout(buttons_layout)
return tab
# ===== Методы для камер =====
def _update_cameras_list(self):
"""Обновляет отображение списка камер"""
self.cameras_list.clear()
for camera in self.cameras:
self.cameras_list.addItem(camera)
self._selected_camera = None
self.remove_camera_btn.setEnabled(False)
def _update_lenses_list(self):
"""Обновляет отображение списка объективов"""
self.lenses_list.clear()
for lens in self.lenses:
self.lenses_list.addItem(lens)
self._selected_lens = None
self.remove_lens_btn.setEnabled(False)
def _select_camera(self, camera: str):
"""Выделяет камеру в списке"""
self._selected_camera = camera
self.remove_camera_btn.setEnabled(True)
# Снимаем выделение с объективов
self.lenses_list.clearSelection()
self.telescopes_list.clearSelection()
self._selected_lens = None
self._selected_telescope = None
self.remove_lens_btn.setEnabled(False)
def _select_lens(self, lens: str):
"""Выделяет объектив в списке"""
self._selected_lens = lens
self.remove_lens_btn.setEnabled(True)
# Снимаем выделение с камер
self.cameras_list.clearSelection()
self._selected_camera = None
self.remove_camera_btn.setEnabled(False)
self.remove_telescope_btn.setEnabled(False)
def _add_camera(self):
"""Добавляет новую камеру"""
new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
if ok and new_camera and new_camera.strip():
new_name = new_camera.strip()
name, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
if ok and name and name.strip():
new_name = name.strip()
if new_name in self.cameras:
QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!")
return
self.cameras.append(new_name)
self.config_service.add_camera(new_name)
self._update_cameras_list()
QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена")
def _remove_camera(self):
"""Удаляет выбранную камеру"""
if hasattr(self, '_selected_camera') and self._selected_camera:
reply = QMessageBox.question(self, "Подтверждение",
f"Удалить камеру '{self._selected_camera}'?",
QMessageBox.Yes | QMessageBox.No)
f"Удалить камеру '{self._selected_camera}'?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.cameras.remove(self._selected_camera)
self.config_service.remove_camera(self._selected_camera)
self._update_cameras_list()
QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена")
# ===== Методы для объективов =====
def _update_lenses_list(self):
self.lenses_list.clear()
for lens in self.lenses:
self.lenses_list.addItem(lens)
self._selected_lens = None
self.remove_lens_btn.setEnabled(False)
def _select_lens(self, lens: str):
self._selected_lens = lens
self.remove_lens_btn.setEnabled(True)
self.cameras_list.clearSelection()
self.telescopes_list.clearSelection()
self._selected_camera = None
self._selected_telescope = None
self.remove_camera_btn.setEnabled(False)
self.remove_telescope_btn.setEnabled(False)
def _add_lens(self):
"""Добавляет новый объектив"""
new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
if ok and new_lens and new_lens.strip():
new_name = new_lens.strip()
name, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
if ok and name and name.strip():
new_name = name.strip()
if new_name in self.lenses:
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
return
self.lenses.append(new_name)
self.config_service.add_lens(new_name)
self._update_lenses_list()
QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен")
def _remove_lens(self):
"""Удаляет выбранный объектив"""
if hasattr(self, '_selected_lens') and self._selected_lens:
reply = QMessageBox.question(self, "Подтверждение",
f"Удалить объектив '{self._selected_lens}'?",
QMessageBox.Yes | QMessageBox.No)
f"Удалить объектив '{self._selected_lens}'?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.lenses.remove(self._selected_lens)
self.config_service.remove_lens(self._selected_lens)
self._update_lenses_list()
QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён")
def _edit_lens(self):
if hasattr(self, '_selected_lens') and self._selected_lens:
new_name, ok = QInputDialog.getText(self, "Редактировать объектив",
f"Изменить '{self._selected_lens}' на:",
text=self._selected_lens)
if ok and new_name and new_name.strip():
new_name = new_name.strip()
if new_name != self._selected_lens:
if new_name in self.lenses:
QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
return
idx = self.lenses.index(self._selected_lens)
self.lenses[idx] = new_name
# Обновляем в конфиге (пока просто удаляем старый и добавляем новый)
self.config_service.remove_lens(self._selected_lens)
self.config_service.add_lens(new_name)
self._update_lenses_list()
# ===== Методы для телескопов =====
def _update_telescopes_list(self):
self.telescopes_list.clear()
for telescope in self.telescopes:
self.telescopes_list.addItem(telescope)
self._selected_telescope = None
self.remove_telescope_btn.setEnabled(False)
def _select_telescope(self, telescope: str):
self._selected_telescope = telescope
self.remove_telescope_btn.setEnabled(True)
self.cameras_list.clearSelection()
self.lenses_list.clearSelection()
self._selected_camera = None
self._selected_lens = None
self.remove_camera_btn.setEnabled(False)
self.remove_lens_btn.setEnabled(False)
def _add_telescope(self):
"""Добавляет телескоп с указанием диафрагмы (фиксированной)"""
dialog = QDialog(self)
dialog.setWindowTitle("Добавить телескоп")
dialog.setMinimumWidth(400)
layout = QVBoxLayout(dialog)
form_layout = QFormLayout()
name_edit = QLineEdit()
name_edit.setPlaceholderText("例如: Celestron 8\"")
form_layout.addRow("Название:", name_edit)
aperture_spin = QDoubleSpinBox()
aperture_spin.setRange(0.5, 20.0)
aperture_spin.setSingleStep(0.1)
aperture_spin.setValue(5.0)
aperture_spin.setSuffix(" (f/)")
form_layout.addRow("Диафрагма (f/):", aperture_spin)
focal_spin = QSpinBox()
focal_spin.setRange(100, 5000)
focal_spin.setSingleStep(50)
focal_spin.setSuffix(" мм")
form_layout.addRow("Фокусное расстояние:", focal_spin)
diameter_spin = QSpinBox()
diameter_spin.setRange(50, 500)
diameter_spin.setSingleStep(10)
diameter_spin.setSuffix(" мм")
form_layout.addRow("Диаметр объектива:", diameter_spin)
layout.addLayout(form_layout)
buttons_layout = QHBoxLayout()
ok_btn = QPushButton("OK")
cancel_btn = QPushButton("Отмена")
buttons_layout.addWidget(ok_btn)
buttons_layout.addWidget(cancel_btn)
layout.addLayout(buttons_layout)
ok_btn.clicked.connect(dialog.accept)
cancel_btn.clicked.connect(dialog.reject)
if dialog.exec():
name = name_edit.text().strip()
if name:
telescope_info = f"{name} (f/{aperture_spin.value()}, F={focal_spin.value()}mm, D={diameter_spin.value()}mm)"
if telescope_info not in self.telescopes:
self.telescopes.append(telescope_info)
self.config_service.add_telescope(telescope_info)
self._update_telescopes_list()
QMessageBox.information(self, "Успех", f"Телескоп '{name}' добавлен")
def _remove_telescope(self):
if hasattr(self, '_selected_telescope') and self._selected_telescope:
reply = QMessageBox.question(self, "Подтверждение",
f"Удалить телескоп '{self._selected_telescope}'?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.telescopes.remove(self._selected_telescope)
self.config_service.remove_telescope(self._selected_telescope)
self._update_telescopes_list()
def _edit_telescope(self):
if hasattr(self, '_selected_telescope') and self._selected_telescope:
new_name, ok = QInputDialog.getText(self, "Редактировать телескоп",
f"Изменить '{self._selected_telescope}' на:",
text=self._selected_telescope)
if ok and new_name and new_name.strip():
new_name = new_name.strip()
if new_name != self._selected_telescope:
if new_name in self.telescopes:
QMessageBox.warning(self, "Ошибка", "Такой телескоп уже существует!")
return
idx = self.telescopes.index(self._selected_telescope)
self.telescopes[idx] = new_name
self.config_service.remove_telescope(self._selected_telescope)
self.config_service.add_telescope(new_name)
self._update_telescopes_list()

View file

@ -119,6 +119,13 @@ class MainWindow(QMainWindow):
new_object_action.triggered.connect(self.set_new_object)
session_menu.addAction(new_object_action)
session_menu.addSeparator()
calibration_action = QAction("🌑 Калибровочные кадры...", self)
calibration_action.setShortcut("Ctrl+K")
calibration_action.triggered.connect(self.open_calibration_dialog)
session_menu.addAction(calibration_action)
# Меню Помощь
help_menu = menubar.addMenu("Помощь")
@ -134,6 +141,12 @@ class MainWindow(QMainWindow):
about_action.triggered.connect(self.show_info)
help_menu.addAction(about_action)
def open_calibration_dialog(self):
"""Открывает диалог калибровочных кадров"""
from ui.dialogs.calibration_dialog import CalibrationDialog
dialog = CalibrationDialog(self, self.config_service)
dialog.exec()
def _create_main_content(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
@ -310,21 +323,34 @@ class MainWindow(QMainWindow):
self.object_combo.lineEdit().blockSignals(False)
def _load_saved_settings(self):
"""Загружает сохранённые настройки"""
cameras = self.config_service.get_cameras()
lenses = self.config_service.get_lenses()
telescopes = self.config_service.get_telescopes() # <-- добавить
celestial_bodies = self.config_service.get_celestial_bodies()
# Объединяем объективы и телескопы для выбора оптики
all_optics = []
for lens in lenses:
all_optics.append(f"🔭 {lens}")
for telescope in telescopes:
all_optics.append(f"🪐 {telescope}")
if cameras:
self.camera_combo.addItems(cameras)
last_camera = self.config_service.get_last_camera()
if last_camera and last_camera in cameras:
self.camera_combo.setCurrentText(last_camera)
if lenses:
self.lens_combo.addItems(lenses)
if all_optics:
self.lens_combo.addItems(all_optics)
last_lens = self.config_service.get_last_lens()
if last_lens and last_lens in lenses:
self.lens_combo.setCurrentText(last_lens)
if last_lens:
# Ищем последнюю использованную оптику
for opt in all_optics:
if last_lens in opt:
self.lens_combo.setCurrentText(opt)
break
if celestial_bodies:
self.object_combo.addItems(celestial_bodies)
@ -461,6 +487,13 @@ class MainWindow(QMainWindow):
camera_val = camera if camera else "Unknown"
lens_val = lens if lens else "Unknown"
optics_value = lens
if optics_value.startswith("🔭 "):
optics_value = optics_value[2:]
elif optics_value.startswith("🪐 "):
optics_value = optics_value[2:]
self.config_service.set_last_lens(optics_value)
self.session_service.start_session(watch_path, object_name, camera_val, lens_val)