From d63d6166757b5ecf01d0652e9241e4d45e9fdc4b Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 10:31:40 +0200 Subject: [PATCH 1/7] working on camera-manager --- controllers/main_controller.py | 69 ++++++++++++++++-- core/camera/camera_manager.py | 123 +++++++++++++++++++++++++++++---- 2 files changed, 175 insertions(+), 17 deletions(-) diff --git a/controllers/main_controller.py b/controllers/main_controller.py index 6453a84..3853eb4 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -4,7 +4,7 @@ from core.database import DatabaseManager from core.media import MediaRepository from ui.widgets.color_list_widget import ColorListWidget from ui.widgets.thumbnail_list_widget import ThumbnailListWidget -from ui.widgets.split_view_widget import SplitView +from ui.widgets.split_view_widget import SplitView, CameraPlaceholder # from .camera_controller import CameraController from core.camera.camera_controller import CameraController from core.camera.camera_manager import CameraManager @@ -23,8 +23,9 @@ class MainController: # camera = GPhotoCamera() # self.manager = CameraController(camera) manager = CameraManager() - manager.detect_gphoto() - manager.detect_opencv() + # manager.detect_gphoto() + # manager.detect_opencv() + manager.detect_camera() # self.camera_controller = CameraController() @@ -32,6 +33,7 @@ class MainController: self.color_list: ColorListWidget = view.color_list_widget self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget self.split_view: SplitView = view.preview_widget + self.welcome_view: CameraPlaceholder = self.split_view.widget_start self.photo_button: QPushButton = view.photo_button self.photo_button.clicked.connect(self.take_photo) @@ -50,6 +52,9 @@ class MainController: # self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview) + self.welcome_view.set_button_text("tada") + self.welcome_view.set_error_text("errorsy") + def start_camera(self): pass @@ -97,4 +102,60 @@ class MainController: def shutdown(self): pass - # self.manager.stop() \ No newline at end of file + # self.manager.stop() + + + + # @Slot(list) + def on_cameras_detected(self, cameras: list[dict]): + print("Wykryto kamery:", cameras) + # Tutaj zaktualizuj swój ComboBox w UI, np.: + # self.main_window.camera_combobox.clear() + # for camera in cameras: + # self.main_window.camera_combobox.addItem(camera['name'], userData=camera['id']) + + # @Slot(QPixmap) + def on_frame_ready(self, pixmap: QPixmap): + # Tutaj zaktualizuj widget wyświetlający obraz, np. QLabel + # self.main_window.video_label.setPixmap(pixmap) + pass + + # @Slot(str) + def on_camera_error(self, error_message: str): + print(f"Błąd kamery: {error_message}") + # Wyświetl błąd w UI, np. w status barze + # self.main_window.statusBar().showMessage(error_message, 5000) + + # @Slot() + def on_camera_started(self): + # Zmień stan UI, np. tekst przycisku na "Stop" + # self.main_window.toggle_camera_button.setText("Stop") + pass + + # @Slot() + def on_camera_stopped(self): + # Zmień stan UI, np. tekst przycisku na "Start" + # self.main_window.toggle_camera_button.setText("Start") + pass + + def toggle_camera(self): + # Logika do przełączania start/stop + if self.camera_manager.get_active_camera_info(): + self.camera_manager.stop_camera() + else: + # Pobierz ID kamery z ComboBoxa + # camera_id = self.main_window.camera_combobox.currentData() + # if camera_id: + # self.camera_manager.start_camera(camera_id) + pass + + def on_camera_selected(self, index: int): + # Automatycznie uruchom kamerę po wybraniu z listy + # camera_id = self.main_window.camera_combobox.itemData(index) + # if camera_id: + # self.camera_manager.start_camera(camera_id) + pass + + def cleanup(self): + # Wywołaj to przy zamykaniu aplikacji + self.camera_manager.shutdown() \ No newline at end of file diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index 5fbea5c..e3b7e2c 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -1,20 +1,117 @@ +from PySide6.QtCore import QObject, Signal +from PySide6.QtGui import QPixmap - +from .camera_controller import CameraController from .gphoto_camera import GPhotoCamera from .opencv_camera import OpenCvCamera -from .camera_controller import CameraController +from .base_camera import BaseCamera + +class CameraManager(QObject): + """ + Zarządza wszystkimi operacjami związanymi z kamerami, + stanowiąc fasadę dla reszty aplikacji. + """ + frame_ready = Signal(QPixmap) + error_occurred = Signal(str) + cameras_detected = Signal(list) + camera_started = Signal() + camera_stopped = Signal() + + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self._camera_controller = CameraController() + self._detected_cameras: list[dict] = [] + self._active_camera: BaseCamera | None = None + self._active_camera_info: dict | None = None + + # Przekazywanie sygnałów z kontrolera kamery na zewnątrz + self._camera_controller.frame_ready.connect(self.frame_ready) + self._camera_controller.error_occurred.connect(self.error_occurred) + + def detect_cameras(self) -> None: + """Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV).""" + self._detected_cameras.clear() + + # Wykryj kamery GPhoto + try: + gphoto_cameras = GPhotoCamera.detect() + for index, info in gphoto_cameras.items(): + self._detected_cameras.append({ + "id": f"gphoto_{index}", + "name": f"{info['name']} ({info['port']})", + "type": "gphoto", + "index": index + }) + except Exception as e: + self.error_occurred.emit(f"Błąd podczas wykrywania kamer GPhoto: {e}") + + # Wykryj kamery OpenCV + try: + opencv_cameras = OpenCvCamera.detect() + for index, info in opencv_cameras.items(): + self._detected_cameras.append({ + "id": f"opencv_{index}", + "name": f"OpenCV: {info['name']}", + "type": "opencv", + "index": index + }) + except Exception as e: + self.error_occurred.emit(f"Błąd podczas wykrywania kamer OpenCV: {e}") + + self.cameras_detected.emit(self._detected_cameras) + + def get_detected_cameras(self) -> list[dict]: + return self._detected_cameras + + def start_camera(self, camera_id: str, fps: int = 15) -> None: + """Uruchamia wybraną kamerę.""" + if self._active_camera: + self.stop_camera() + + camera_info = next((c for c in self._detected_cameras if c['id'] == camera_id), None) + + if not camera_info: + self.error_occurred.emit(f"Nie znaleziono kamery o ID: {camera_id}") + return + + camera_type = camera_info['type'] + camera_index = camera_info['index'] + + if camera_type == "gphoto": + self._active_camera = GPhotoCamera() + elif camera_type == "opencv": + self._active_camera = OpenCvCamera() + else: + self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}") + return + + self._active_camera_info = camera_info + + self._camera_controller.set_camera(self._active_camera, fps) + self._camera_controller.start_camera() + + # Trzeba sprawdzić, czy połączenie się udało + if self._camera_controller.is_connected: + self._camera_controller.start_stream() + self.camera_started.emit() + else: + # Błąd został już wyemitowany przez CameraController + self._active_camera = None + self._active_camera_info = None -class CameraManager: - def __init__(self) -> None: - pass + def stop_camera(self) -> None: + """Zatrzymuje aktywną kamerę.""" + if self._active_camera: + self._camera_controller.stop_camera() + self._active_camera = None + self._active_camera_info = None + self.camera_stopped.emit() - def detect_gphoto(self): - camera_list = GPhotoCamera.detect() - print(camera_list) - return camera_list + def get_active_camera_info(self) -> dict | None: + return self._active_camera_info - def detect_opencv(self): - camera_list = OpenCvCamera.detect() - print(camera_list) - return camera_list + def shutdown(self) -> None: + """Zamyka kontroler kamery i jego wątek.""" + self.stop_camera() + self._camera_controller.stop() \ No newline at end of file From 2a5f570e5e3379fe6d90c78f95cb0b191a972233 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 13:08:40 +0200 Subject: [PATCH 2/7] feat: implement CameraDetectionWorker for asynchronous camera detection --- core/camera/camera_manager.py | 92 +++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index e3b7e2c..ab5fb0c 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QObject, Signal, QRunnable, QThreadPool from PySide6.QtGui import QPixmap from .camera_controller import CameraController @@ -6,6 +6,50 @@ from .gphoto_camera import GPhotoCamera from .opencv_camera import OpenCvCamera from .base_camera import BaseCamera + +class CameraDetectionWorker(QRunnable): + """ + Worker thread for detecting cameras to avoid blocking the GUI. + """ + class WorkerSignals(QObject): + finished = Signal(list) + error = Signal(str) + + def __init__(self): + super().__init__() + self.signals = self.WorkerSignals() + + def run(self) -> None: + """The main work of the worker.""" + detected_cameras = [] + + try: + gphoto_cameras = GPhotoCamera.detect() + for index, info in gphoto_cameras.items(): + detected_cameras.append({ + "id": f"gphoto_{index}", + "name": f"{info['name']} ({info['port']})", + "type": "gphoto", + "index": index + }) + except Exception as e: + self.signals.error.emit(f"Błąd podczas wykrywania kamer GPhoto: {e}") + + try: + opencv_cameras = OpenCvCamera.detect() + for index, info in opencv_cameras.items(): + detected_cameras.append({ + "id": f"opencv_{index}", + "name": f"OpenCV: {info['name']}", + "type": "opencv", + "index": index + }) + except Exception as e: + self.signals.error.emit(f"Błąd podczas wykrywania kamer OpenCV: {e}") + + self.signals.finished.emit(detected_cameras) + + class CameraManager(QObject): """ Zarządza wszystkimi operacjami związanymi z kamerami, @@ -13,7 +57,10 @@ class CameraManager(QObject): """ frame_ready = Signal(QPixmap) error_occurred = Signal(str) + + detection_started = Signal() cameras_detected = Signal(list) + camera_started = Signal() camera_stopped = Signal() @@ -23,41 +70,26 @@ class CameraManager(QObject): self._detected_cameras: list[dict] = [] self._active_camera: BaseCamera | None = None self._active_camera_info: dict | None = None + self.thread_pool = QThreadPool.globalInstance() - # Przekazywanie sygnałów z kontrolera kamery na zewnątrz self._camera_controller.frame_ready.connect(self.frame_ready) self._camera_controller.error_occurred.connect(self.error_occurred) def detect_cameras(self) -> None: - """Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV).""" - self._detected_cameras.clear() - - # Wykryj kamery GPhoto - try: - gphoto_cameras = GPhotoCamera.detect() - for index, info in gphoto_cameras.items(): - self._detected_cameras.append({ - "id": f"gphoto_{index}", - "name": f"{info['name']} ({info['port']})", - "type": "gphoto", - "index": index - }) - except Exception as e: - self.error_occurred.emit(f"Błąd podczas wykrywania kamer GPhoto: {e}") - - # Wykryj kamery OpenCV - try: - opencv_cameras = OpenCvCamera.detect() - for index, info in opencv_cameras.items(): - self._detected_cameras.append({ - "id": f"opencv_{index}", - "name": f"OpenCV: {info['name']}", - "type": "opencv", - "index": index - }) - except Exception as e: - self.error_occurred.emit(f"Błąd podczas wykrywania kamer OpenCV: {e}") + """ + Rozpoczyna asynchroniczne wykrywanie kamer w osobnym wątku. + """ + self.detection_started.emit() + worker = CameraDetectionWorker() + worker.signals.finished.connect(self._on_detection_finished) + worker.signals.error.connect(self.error_occurred) + self.thread_pool.start(worker) + def _on_detection_finished(self, detected_cameras: list): + """ + Slot wywoływany po zakończeniu pracy workera wykrywającego kamery. + """ + self._detected_cameras = detected_cameras self.cameras_detected.emit(self._detected_cameras) def get_detected_cameras(self) -> list[dict]: From bbdc2af605edf158318b76471c251cf0a2c9b520 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 13:41:13 +0200 Subject: [PATCH 3/7] refactor: change CameraController to inherit from QObject and manage threading in CameraManager --- core/camera/camera_controller.py | 113 +++++++++++++++---------------- core/camera/camera_manager.py | 88 ++++++++++++++---------- 2 files changed, 106 insertions(+), 95 deletions(-) diff --git a/core/camera/camera_controller.py b/core/camera/camera_controller.py index bb8b3c9..826a367 100644 --- a/core/camera/camera_controller.py +++ b/core/camera/camera_controller.py @@ -1,115 +1,110 @@ -from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot, QMutex, QMutexLocker +from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker from PySide6.QtGui import QImage, QPixmap import cv2 from .base_camera import BaseCamera -class CameraController(QThread): +class CameraController(QObject): + """ + A QObject worker for handling camera operations in a separate thread. + This object should be moved to a QThread. + """ frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) - _enable_timer = Signal(bool) - def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) - self.camera = None - self.timer = None + self.camera: BaseCamera | None = None + self.timer: QTimer | None = None self.fps = 15 self.is_streaming = False self.is_connected = False - self._camera_mutex = QMutex() - self.start() - - def run(self) -> None: + @Slot() + def run(self): + """ + Initializes resources in the worker thread. + This should be connected to the QThread.started signal. + """ self.timer = QTimer() self.timer.timeout.connect(self._update_frame) - self._enable_timer.connect(self._set_timer) - self.exec() - - def stop(self): - self.stop_camera() - self.quit() - self.wait() + @Slot(BaseCamera, int) def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: with QMutexLocker(self._camera_mutex): - self.stop_stream() - self.stop_camera() + if self.is_streaming: + self.stop_stream() + if self.is_connected: + self.stop_camera() self.camera = camera self.fps = fps + @Slot() def start_camera(self) -> None: - if self.camera is None or self.is_connected: - return + with QMutexLocker(self._camera_mutex): + if self.camera is None or self.is_connected: + return - if self.camera.connect(): - self.is_connected = True - else: - self.is_connected = False - self.error_occurred.emit(self.camera.get_error_msg()) + if self.camera.connect(): + self.is_connected = True + else: + self.is_connected = False + self.error_occurred.emit(self.camera.get_error_msg()) + @Slot() def stop_camera(self) -> None: - if self.is_streaming: - self.stop_stream() + with QMutexLocker(self._camera_mutex): + if self.is_streaming: + self.stop_stream() - if self.camera is not None: - self.camera.disconnect() + if self.camera is not None and self.is_connected: + self.camera.disconnect() - self.is_connected = False + self.is_connected = False + @Slot() def start_stream(self): - if not self.is_connected: - return + with QMutexLocker(self._camera_mutex): + if not self.is_connected or self.is_streaming or self.timer is None: + return - if self.is_streaming: - return - - if self.timer: self.is_streaming = True - # self.timer.start() - self._enable_timer.emit(True) + self.timer.setInterval(int(1000 / self.fps)) + self.timer.start() + @Slot() def stop_stream(self) -> None: - if self.is_streaming: + with QMutexLocker(self._camera_mutex): + if not self.is_streaming or self.timer is None: + return + self.is_streaming = False - if self.timer: - # self.timer.stop() - self._enable_timer.emit(False) + self.timer.stop() def _update_frame(self) -> None: + # This method is called by the timer, which is in the same thread. + # A mutex is still good practice for accessing the shared camera object. with QMutexLocker(self._camera_mutex): - if self.camera is None or not self.is_connected: + if self.camera is None or not self.is_connected or not self.is_streaming: return - if not self.is_streaming: - return - ret, frame = self.camera.get_frame() if not ret: - self.error_occurred.emit(self.camera.get_error_msg()) + error_msg = self.camera.get_error_msg() + if error_msg: + self.error_occurred.emit(error_msg) return if frame is not None: + # Process the frame and emit it. rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) pixmap = QPixmap.fromImage(qimg) - self.frame_ready.emit(pixmap) - - def _set_timer(self, enable: bool): - if not self.timer: - return - - if enable: - self.timer.setInterval(int(1000 / self.fps)) - self.timer.start() - else: - self.timer.stop() diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index ab5fb0c..1b2a56c 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QObject, Signal, QRunnable, QThreadPool +from PySide6.QtCore import QObject, Signal, QRunnable, QThreadPool, QThread from PySide6.QtGui import QPixmap from .camera_controller import CameraController @@ -55,30 +55,50 @@ class CameraManager(QObject): Zarządza wszystkimi operacjami związanymi z kamerami, stanowiąc fasadę dla reszty aplikacji. """ + # --- Public API Signals --- frame_ready = Signal(QPixmap) error_occurred = Signal(str) - detection_started = Signal() cameras_detected = Signal(list) - camera_started = Signal() camera_stopped = Signal() + # --- Internal signals to communicate with worker thread --- + _request_set_camera = Signal(BaseCamera, int) + _request_start_camera = Signal() + _request_stop_camera = Signal() + _request_start_stream = Signal() + _request_stop_stream = Signal() + def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) + + self._camera_thread = QThread() self._camera_controller = CameraController() - self._detected_cameras: list[dict] = [] - self._active_camera: BaseCamera | None = None - self._active_camera_info: dict | None = None - self.thread_pool = QThreadPool.globalInstance() + self._camera_controller.moveToThread(self._camera_thread) + # --- Connections --- + # Connect signals from controller to be re-emitted by manager self._camera_controller.frame_ready.connect(self.frame_ready) self._camera_controller.error_occurred.connect(self.error_occurred) + + # Connect internal requests to controller slots + self._request_set_camera.connect(self._camera_controller.set_camera) + self._request_start_camera.connect(self._camera_controller.start_camera) + self._request_stop_camera.connect(self._camera_controller.stop_camera) + self._request_start_stream.connect(self._camera_controller.start_stream) + self._request_stop_stream.connect(self._camera_controller.stop_stream) + + # Connect thread management + self._camera_thread.started.connect(self._camera_controller.run) + + self._camera_thread.start() + + self._detected_cameras: list[dict] = [] + self._active_camera_info: dict | None = None + self.thread_pool = QThreadPool.globalInstance() # For detection worker def detect_cameras(self) -> None: - """ - Rozpoczyna asynchroniczne wykrywanie kamer w osobnym wątku. - """ self.detection_started.emit() worker = CameraDetectionWorker() worker.signals.finished.connect(self._on_detection_finished) @@ -86,9 +106,6 @@ class CameraManager(QObject): self.thread_pool.start(worker) def _on_detection_finished(self, detected_cameras: list): - """ - Slot wywoływany po zakończeniu pracy workera wykrywającego kamery. - """ self._detected_cameras = detected_cameras self.cameras_detected.emit(self._detected_cameras) @@ -96,8 +113,10 @@ class CameraManager(QObject): return self._detected_cameras def start_camera(self, camera_id: str, fps: int = 15) -> None: - """Uruchamia wybraną kamerę.""" - if self._active_camera: + if self._active_camera_info and self._active_camera_info['id'] == camera_id: + return + + if self._active_camera_info: self.stop_camera() camera_info = next((c for c in self._detected_cameras if c['id'] == camera_id), None) @@ -109,34 +128,28 @@ class CameraManager(QObject): camera_type = camera_info['type'] camera_index = camera_info['index'] + camera_instance: BaseCamera | None = None if camera_type == "gphoto": - self._active_camera = GPhotoCamera() + camera_instance = GPhotoCamera() elif camera_type == "opencv": - self._active_camera = OpenCvCamera() + camera_instance = OpenCvCamera() else: self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}") return self._active_camera_info = camera_info - self._camera_controller.set_camera(self._active_camera, fps) - self._camera_controller.start_camera() - - # Trzeba sprawdzić, czy połączenie się udało - if self._camera_controller.is_connected: - self._camera_controller.start_stream() - self.camera_started.emit() - else: - # Błąd został już wyemitowany przez CameraController - self._active_camera = None - self._active_camera_info = None - + # Emit signals to trigger slots in the worker thread + self._request_set_camera.emit(camera_instance, fps) + self._request_start_camera.emit() + self._request_start_stream.emit() + self.camera_started.emit() def stop_camera(self) -> None: - """Zatrzymuje aktywną kamerę.""" - if self._active_camera: - self._camera_controller.stop_camera() - self._active_camera = None + if self._active_camera_info: + # Emit signals to trigger slots in the worker thread + self._request_stop_stream.emit() + self._request_stop_camera.emit() self._active_camera_info = None self.camera_stopped.emit() @@ -144,6 +157,9 @@ class CameraManager(QObject): return self._active_camera_info def shutdown(self) -> None: - """Zamyka kontroler kamery i jego wątek.""" - self.stop_camera() - self._camera_controller.stop() \ No newline at end of file + if self._camera_thread.isRunning(): + self._camera_thread.quit() + if not self._camera_thread.wait(5000): + print("Camera thread did not finish gracefully, terminating.") + self._camera_thread.terminate() + self._camera_thread.wait() From 46734208e73645b445656a15eda419025b11554a Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 18:51:37 +0200 Subject: [PATCH 4/7] refactor. new qobjct thread approuch insted qthread --- core/camera/camera_controller.py | 117 +++++++++++++++++++------------ 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/core/camera/camera_controller.py b/core/camera/camera_controller.py index bb8b3c9..0e431da 100644 --- a/core/camera/camera_controller.py +++ b/core/camera/camera_controller.py @@ -5,36 +5,27 @@ import cv2 from .base_camera import BaseCamera -class CameraController(QThread): +class CameraWorker(QObject): frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) - _enable_timer = Signal(bool) - def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) - self.camera = None - self.timer = None + self.camera: BaseCamera | None = None + self.timer: QTimer | None = None self.fps = 15 self.is_streaming = False self.is_connected = False - self._camera_mutex = QMutex() - self.start() - - def run(self) -> None: + @Slot() + def initialize_worker(self): + """Initializes the timer in the worker's thread.""" self.timer = QTimer() self.timer.timeout.connect(self._update_frame) - self._enable_timer.connect(self._set_timer) - self.exec() - - def stop(self): - self.stop_camera() - self.quit() - self.wait() + @Slot(BaseCamera, int) def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: with QMutexLocker(self._camera_mutex): self.stop_stream() @@ -43,6 +34,7 @@ class CameraController(QThread): self.camera = camera self.fps = fps + @Slot() def start_camera(self) -> None: if self.camera is None or self.is_connected: return @@ -53,6 +45,7 @@ class CameraController(QThread): self.is_connected = False self.error_occurred.emit(self.camera.get_error_msg()) + @Slot() def stop_camera(self) -> None: if self.is_streaming: self.stop_stream() @@ -62,31 +55,25 @@ class CameraController(QThread): self.is_connected = False + @Slot() def start_stream(self): - if not self.is_connected: + if not self.is_connected or self.is_streaming or self.timer is None: return - if self.is_streaming: - return - - if self.timer: - self.is_streaming = True - # self.timer.start() - self._enable_timer.emit(True) + self.is_streaming = True + self.timer.setInterval(int(1000 / self.fps)) + self.timer.start() + @Slot() def stop_stream(self) -> None: - if self.is_streaming: + if self.is_streaming and self.timer is not None: self.is_streaming = False - if self.timer: - # self.timer.stop() - self._enable_timer.emit(False) + self.timer.stop() + @Slot() def _update_frame(self) -> None: with QMutexLocker(self._camera_mutex): - if self.camera is None or not self.is_connected: - return - - if not self.is_streaming: + if self.camera is None or not self.is_connected or not self.is_streaming: return ret, frame = self.camera.get_frame() @@ -100,16 +87,60 @@ class CameraController(QThread): h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) pixmap = QPixmap.fromImage(qimg) - self.frame_ready.emit(pixmap) - def _set_timer(self, enable: bool): - if not self.timer: - return - - if enable: - self.timer.setInterval(int(1000 / self.fps)) - self.timer.start() - else: - self.timer.stop() - + +class CameraController(QObject): + frame_ready = Signal(QPixmap) + photo_ready = Signal(QPixmap) + error_occurred = Signal(str) + + # Signals to command the worker + _set_camera_requested = Signal(BaseCamera, int) + _start_camera_requested = Signal() + _stop_camera_requested = Signal() + _start_stream_requested = Signal() + _stop_stream_requested = Signal() + + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self._thread = QThread() + self._worker = CameraWorker() + + self._worker.moveToThread(self._thread) + + # Connect worker signals to controller signals + self._worker.frame_ready.connect(self.frame_ready) + self._worker.photo_ready.connect(self.photo_ready) + self._worker.error_occurred.connect(self.error_occurred) + + # Connect controller's command signals to worker's slots + self._set_camera_requested.connect(self._worker.set_camera) + self._start_camera_requested.connect(self._worker.start_camera) + self._stop_camera_requested.connect(self._worker.stop_camera) + self._start_stream_requested.connect(self._worker.start_stream) + self._stop_stream_requested.connect(self._worker.stop_stream) + + # Initialize worker when thread starts + self._thread.started.connect(self._worker.initialize_worker) + + self._thread.start() + + def stop(self): + self._thread.quit() + self._thread.wait() + + def set_camera(self, camera: BaseCamera, fps: int = 15) -> None: + self._set_camera_requested.emit(camera, fps) + + def start_camera(self) -> None: + self._start_camera_requested.emit() + + def stop_camera(self) -> None: + self._stop_camera_requested.emit() + + def start_stream(self): + self._start_stream_requested.emit() + + def stop_stream(self) -> None: + self._stop_stream_requested.emit() \ No newline at end of file From ce1b864b17ebecdf098cdff95375a54a949b6959 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 19:02:53 +0200 Subject: [PATCH 5/7] refactor: streamline MainController initialization and signal connections --- controllers/main_controller.py | 269 ++++++++++++++++----------------- 1 file changed, 131 insertions(+), 138 deletions(-) diff --git a/controllers/main_controller.py b/controllers/main_controller.py index 3853eb4..77b99ba 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -1,161 +1,154 @@ -from PySide6.QtWidgets import QPushButton from pathlib import Path +from PySide6.QtCore import Slot +from PySide6.QtGui import QPixmap +from PySide6.QtWidgets import QPushButton + from core.database import DatabaseManager from core.media import MediaRepository +from core.camera.camera_manager import CameraManager from ui.widgets.color_list_widget import ColorListWidget from ui.widgets.thumbnail_list_widget import ThumbnailListWidget from ui.widgets.split_view_widget import SplitView, CameraPlaceholder -# from .camera_controller import CameraController -from core.camera.camera_controller import CameraController -from core.camera.camera_manager import CameraManager -from core.camera.gphoto_camera import GPhotoCamera -from core.camera.camera_controller import CameraController - class MainController: - def __init__(self, view): - self.db = DatabaseManager() - self.db.connect() - self.media_repo = MediaRepository(self.db) - self.media_repo.sync_media() + def __init__(self, view): + self.view = view + self.db = DatabaseManager() + self.media_repo = MediaRepository(self.db) + self.camera_manager = CameraManager() - # camera = GPhotoCamera() - # self.manager = CameraController(camera) - manager = CameraManager() - # manager.detect_gphoto() - # manager.detect_opencv() - manager.detect_camera() + # --- UI Widgets --- + self.color_list: ColorListWidget = view.color_list_widget + self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget + self.split_view: SplitView = view.preview_widget + self.welcome_view: CameraPlaceholder = self.split_view.widget_start + self.photo_button: QPushButton = view.photo_button + self.record_button: QPushButton = view.record_button - # self.camera_controller = CameraController() + self._connect_signals() - self.view = view - self.color_list: ColorListWidget = view.color_list_widget - self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget - self.split_view: SplitView = view.preview_widget - self.welcome_view: CameraPlaceholder = self.split_view.widget_start + self.db.connect() + self.media_repo.sync_media() + self.camera_manager.detect_cameras() - self.photo_button: QPushButton = view.photo_button - self.photo_button.clicked.connect(self.take_photo) + def _connect_signals(self): + """Connects all signals to slots.""" + # Database and media signals + self.color_list.colorSelected.connect(self.on_color_selected) + self.color_list.editColor.connect(self.on_edit_color) + self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) - self.record_button: QPushButton = view.record_button - # self.record_button.clicked.connect(self.fun_test) + # Camera signals + self.camera_manager.cameras_detected.connect(self.on_cameras_detected) + self.camera_manager.frame_ready.connect(self.on_frame_ready) + self.camera_manager.error_occurred.connect(self.on_camera_error) + self.camera_manager.camera_started.connect(self.on_camera_started) + self.camera_manager.camera_stopped.connect(self.on_camera_stopped) - self.color_list.colorSelected.connect(self.on_color_selected) - self.color_list.editColor.connect(self.on_edit_color) - self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected) + # UI control signals + self.photo_button.clicked.connect(self.take_photo) + # self.record_button.clicked.connect(self.toggle_record) # Placeholder + self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview) + # You will need a way to select a camera, e.g., a combobox. + # self.view.camera_combobox.currentIndexChanged.connect(self.on_camera_selected_in_ui) - # self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text) - # self.manager.error_occurred.connect(self.split_view.widget_start.set_info_text) - # self.camera_controller.frameReady.connect(self.split_view.set_live_image) - # self.manager.frame_ready.connect(self.split_view.set_live_image) - # self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start) - self.split_view.widget_start.camera_start_btn.clicked.connect(self.start_liveview) + def load_colors(self) -> None: + """Loads colors from the database and populates the list.""" + colors = self.db.get_all_colors() + self.color_list.set_colors(colors) - self.welcome_view.set_button_text("tada") - self.welcome_view.set_error_text("errorsy") + def shutdown(self): + """Cleans up resources before application exit.""" + self.camera_manager.shutdown() + self.db.disconnect() + + # --- Slots for Database/Media --- + + @Slot(str) + def on_color_selected(self, color_name: str): + color_id = self.db.get_color_id(color_name) + if color_id is not None: + media_items = self.db.get_media_for_color(color_id) + self.thumbnail_list.list_widget.clear() + for media in media_items: + if media['file_type'] == 'photo': + file_name = Path(media['media_path']).name + self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) + + @Slot(str) + def on_edit_color(self, color_name: str): + print(f"Edycja koloru: {color_name}") # Placeholder + + @Slot(int) + def on_thumbnail_selected(self, media_id: int): + media = self.db.get_media(media_id) + if media: + self.split_view.set_reference_image(media['media_path']) + + # --- Slots for CameraManager --- + + @Slot(list) + def on_cameras_detected(self, cameras: list[dict]): + """Handles the list of detected cameras.""" + print("Detected cameras:", cameras) + self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.") + # Populate a combobox in the UI here + # self.view.camera_combobox.clear() + # for camera in cameras: + # self.view.camera_combobox.addItem(camera['name'], userData=camera['id']) + + @Slot(QPixmap) + def on_frame_ready(self, pixmap: QPixmap): + """Displays a new frame from the camera.""" + self.split_view.set_live_image(pixmap) + + @Slot(str) + def on_camera_error(self, error_message: str): + """Shows an error message from the camera.""" + print(f"Camera Error: {error_message}") + self.welcome_view.set_error_text(error_message) + + @Slot() + def on_camera_started(self): + """Updates UI when the camera stream starts.""" + # self.split_view.show_live_view() + self.welcome_view.set_button_text("Stop Camera") + # Re-route button click to stop the camera + self.welcome_view.camera_start_btn.clicked.disconnect() + self.welcome_view.camera_start_btn.clicked.connect(self.stop_liveview) - def start_camera(self): - pass + @Slot() + def on_camera_stopped(self): + """Updates UI when the camera stream stops.""" + # self.split_view.show_placeholder() + self.welcome_view.set_button_text("Start Camera") + # Re-route button click to start the camera + self.welcome_view.camera_start_btn.clicked.disconnect() + self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview) - def load_colors(self) -> None: - colors = self.db.get_all_colors() - print("Loaded colors:", colors) - self.color_list.set_colors(colors) + # --- UI Actions --- + def start_liveview(self): + """Starts the camera feed.""" + detected_cameras = self.camera_manager.get_detected_cameras() + if not detected_cameras: + self.on_camera_error("No cameras detected.") + return + + # For now, just start the first detected camera. + # In a real app, you'd get the selected camera ID from the UI. + camera_id = detected_cameras[0]['id'] + self.camera_manager.start_camera(camera_id) - def on_color_selected(self, color_name: str): - print(f"Wybrano kolor: {color_name}") - color_id = self.db.get_color_id(color_name) - if color_id is not None: - media_items = self.db.get_media_for_color(color_id) - print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items) + def stop_liveview(self): + """Stops the camera feed.""" + self.camera_manager.stop_camera() - self.thumbnail_list.list_widget.clear() - for media in media_items: - if media['file_type'] == 'photo': - file_name = Path(media['media_path']).name - self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id']) - else: - print(f"Nie znaleziono koloru o nazwie: {color_name}") - - def on_edit_color(self, color_name: str): - print(f"Edycja koloru: {color_name}") - - def on_thumbnail_selected(self, media_id: int): - media = self.db.get_media(media_id) - if media: - print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}") - self.split_view.set_reference_image(media['media_path']) - else: - print(f"Nie znaleziono mediów o ID: {media_id}") - - def take_photo(self): - print("Robienie zdjęcia...") - self.split_view.toglle_live_view() - - def start_liveview(self): - pass - # self.manager.start_camera() - # self.manager.start_stream() - - def shutdown(self): - pass - # self.manager.stop() - - - - # @Slot(list) - def on_cameras_detected(self, cameras: list[dict]): - print("Wykryto kamery:", cameras) - # Tutaj zaktualizuj swój ComboBox w UI, np.: - # self.main_window.camera_combobox.clear() - # for camera in cameras: - # self.main_window.camera_combobox.addItem(camera['name'], userData=camera['id']) - - # @Slot(QPixmap) - def on_frame_ready(self, pixmap: QPixmap): - # Tutaj zaktualizuj widget wyświetlający obraz, np. QLabel - # self.main_window.video_label.setPixmap(pixmap) - pass - - # @Slot(str) - def on_camera_error(self, error_message: str): - print(f"Błąd kamery: {error_message}") - # Wyświetl błąd w UI, np. w status barze - # self.main_window.statusBar().showMessage(error_message, 5000) - - # @Slot() - def on_camera_started(self): - # Zmień stan UI, np. tekst przycisku na "Stop" - # self.main_window.toggle_camera_button.setText("Stop") - pass - - # @Slot() - def on_camera_stopped(self): - # Zmień stan UI, np. tekst przycisku na "Start" - # self.main_window.toggle_camera_button.setText("Start") - pass - - def toggle_camera(self): - # Logika do przełączania start/stop - if self.camera_manager.get_active_camera_info(): - self.camera_manager.stop_camera() - else: - # Pobierz ID kamery z ComboBoxa - # camera_id = self.main_window.camera_combobox.currentData() - # if camera_id: - # self.camera_manager.start_camera(camera_id) - pass - - def on_camera_selected(self, index: int): - # Automatycznie uruchom kamerę po wybraniu z listy - # camera_id = self.main_window.camera_combobox.itemData(index) - # if camera_id: - # self.camera_manager.start_camera(camera_id) - pass - - def cleanup(self): - # Wywołaj to przy zamykaniu aplikacji - self.camera_manager.shutdown() \ No newline at end of file + def take_photo(self): + """Takes a photo with the active camera.""" + print("Taking photo...") # Placeholder + # This needs to be implemented in CameraManager and called here. + # e.g., self.camera_manager.take_photo() + self.split_view.toggle_live_view() # This seems like a UI toggle, maybe rename? \ No newline at end of file From 511e668cb3ce8a77db8b54a7c27207011a388e19 Mon Sep 17 00:00:00 2001 From: bartool Date: Sun, 12 Oct 2025 19:29:23 +0200 Subject: [PATCH 6/7] feat: add isConnected method to CameraWorker and is_connected method to CameraController --- core/camera/camera_controller.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/camera/camera_controller.py b/core/camera/camera_controller.py index 0e431da..9230cb4 100644 --- a/core/camera/camera_controller.py +++ b/core/camera/camera_controller.py @@ -88,6 +88,9 @@ class CameraWorker(QObject): qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) pixmap = QPixmap.fromImage(qimg) self.frame_ready.emit(pixmap) + + def isConnected(self): + return self.is_connected class CameraController(QObject): @@ -143,4 +146,7 @@ class CameraController(QObject): self._start_stream_requested.emit() def stop_stream(self) -> None: - self._stop_stream_requested.emit() \ No newline at end of file + self._stop_stream_requested.emit() + + def is_connected(self): + return self._worker.isConnected() \ No newline at end of file From c6345c569de0e19bbfd5325ada788ec1c0747fe6 Mon Sep 17 00:00:00 2001 From: bartool Date: Mon, 13 Oct 2025 05:14:27 +0200 Subject: [PATCH 7/7] refactor: update camera control signals and improve live view handling --- controllers/main_controller.py | 2 +- core/camera/camera_controller.py | 15 +- core/camera/camera_manager.py | 234 +++++++++++++------------------ 3 files changed, 104 insertions(+), 147 deletions(-) diff --git a/controllers/main_controller.py b/controllers/main_controller.py index 77b99ba..26068cd 100644 --- a/controllers/main_controller.py +++ b/controllers/main_controller.py @@ -112,7 +112,7 @@ class MainController: @Slot() def on_camera_started(self): """Updates UI when the camera stream starts.""" - # self.split_view.show_live_view() + self.split_view.toggle_live_view() self.welcome_view.set_button_text("Stop Camera") # Re-route button click to stop the camera self.welcome_view.camera_start_btn.clicked.disconnect() diff --git a/core/camera/camera_controller.py b/core/camera/camera_controller.py index bc46ec1..fd39c8b 100644 --- a/core/camera/camera_controller.py +++ b/core/camera/camera_controller.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker +from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker, QThread from PySide6.QtGui import QImage, QPixmap import cv2 @@ -9,6 +9,7 @@ class CameraWorker(QObject): frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) + camera_ready = Signal(bool) def __init__(self, parent: QObject | None = None) -> None: super().__init__(parent) @@ -44,8 +45,10 @@ class CameraWorker(QObject): if self.camera.connect(): self.is_connected = True + self.camera_ready.emit(True) else: self.is_connected = False + self.camera_ready.emit(False) self.error_occurred.emit(self.camera.get_error_msg()) @Slot() @@ -97,15 +100,14 @@ class CameraWorker(QObject): qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888) pixmap = QPixmap.fromImage(qimg) self.frame_ready.emit(pixmap) - - def isConnected(self): - return self.is_connected + class CameraController(QObject): frame_ready = Signal(QPixmap) photo_ready = Signal(QPixmap) error_occurred = Signal(str) + camera_ready = Signal(bool) # Signals to command the worker _set_camera_requested = Signal(BaseCamera, int) @@ -125,6 +127,7 @@ class CameraController(QObject): self._worker.frame_ready.connect(self.frame_ready) self._worker.photo_ready.connect(self.photo_ready) self._worker.error_occurred.connect(self.error_occurred) + self._worker.camera_ready.connect(self.camera_ready) # Connect controller's command signals to worker's slots self._set_camera_requested.connect(self._worker.set_camera) @@ -156,6 +159,4 @@ class CameraController(QObject): def stop_stream(self) -> None: self._stop_stream_requested.emit() - - def is_connected(self): - return self._worker.isConnected() \ No newline at end of file + \ No newline at end of file diff --git a/core/camera/camera_manager.py b/core/camera/camera_manager.py index 1b2a56c..dc1ed86 100644 --- a/core/camera/camera_manager.py +++ b/core/camera/camera_manager.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QObject, Signal, QRunnable, QThreadPool, QThread +from PySide6.QtCore import QObject, Signal from PySide6.QtGui import QPixmap from .camera_controller import CameraController @@ -7,159 +7,115 @@ from .opencv_camera import OpenCvCamera from .base_camera import BaseCamera -class CameraDetectionWorker(QRunnable): - """ - Worker thread for detecting cameras to avoid blocking the GUI. - """ - class WorkerSignals(QObject): - finished = Signal(list) - error = Signal(str) - - def __init__(self): - super().__init__() - self.signals = self.WorkerSignals() - - def run(self) -> None: - """The main work of the worker.""" - detected_cameras = [] - - try: - gphoto_cameras = GPhotoCamera.detect() - for index, info in gphoto_cameras.items(): - detected_cameras.append({ - "id": f"gphoto_{index}", - "name": f"{info['name']} ({info['port']})", - "type": "gphoto", - "index": index - }) - except Exception as e: - self.signals.error.emit(f"Błąd podczas wykrywania kamer GPhoto: {e}") - - try: - opencv_cameras = OpenCvCamera.detect() - for index, info in opencv_cameras.items(): - detected_cameras.append({ - "id": f"opencv_{index}", - "name": f"OpenCV: {info['name']}", - "type": "opencv", - "index": index - }) - except Exception as e: - self.signals.error.emit(f"Błąd podczas wykrywania kamer OpenCV: {e}") - - self.signals.finished.emit(detected_cameras) - - class CameraManager(QObject): - """ - Zarządza wszystkimi operacjami związanymi z kamerami, - stanowiąc fasadę dla reszty aplikacji. - """ - # --- Public API Signals --- - frame_ready = Signal(QPixmap) - error_occurred = Signal(str) - detection_started = Signal() - cameras_detected = Signal(list) - camera_started = Signal() - camera_stopped = Signal() + """ + Zarządza wszystkimi operacjami związanymi z kamerami, + stanowiąc fasadę dla reszty aplikacji. + """ + frame_ready = Signal(QPixmap) + error_occurred = Signal(str) + cameras_detected = Signal(list) + camera_started = Signal() + camera_stopped = Signal() - # --- Internal signals to communicate with worker thread --- - _request_set_camera = Signal(BaseCamera, int) - _request_start_camera = Signal() - _request_stop_camera = Signal() - _request_start_stream = Signal() - _request_stop_stream = Signal() + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent) + self._camera_controller = CameraController() + self._detected_cameras: list[dict] = [] + self._active_camera: BaseCamera | None = None + self._active_camera_info: dict | None = None - def __init__(self, parent: QObject | None = None) -> None: - super().__init__(parent) - - self._camera_thread = QThread() - self._camera_controller = CameraController() - self._camera_controller.moveToThread(self._camera_thread) + # Przekazywanie sygnałów z kontrolera kamery na zewnątrz + self._camera_controller.frame_ready.connect(self.frame_ready) + self._camera_controller.error_occurred.connect(self.error_occurred) + self._camera_controller.camera_ready.connect(self.start_liveview) - # --- Connections --- - # Connect signals from controller to be re-emitted by manager - self._camera_controller.frame_ready.connect(self.frame_ready) - self._camera_controller.error_occurred.connect(self.error_occurred) - - # Connect internal requests to controller slots - self._request_set_camera.connect(self._camera_controller.set_camera) - self._request_start_camera.connect(self._camera_controller.start_camera) - self._request_stop_camera.connect(self._camera_controller.stop_camera) - self._request_start_stream.connect(self._camera_controller.start_stream) - self._request_stop_stream.connect(self._camera_controller.stop_stream) + def detect_cameras(self) -> None: + """Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV).""" + self._detected_cameras.clear() - # Connect thread management - self._camera_thread.started.connect(self._camera_controller.run) - - self._camera_thread.start() + # Wykryj kamery GPhoto + try: + gphoto_cameras = GPhotoCamera.detect() + for index, info in gphoto_cameras.items(): + self._detected_cameras.append({ + "id": f"gphoto_{index}", + "name": f"{info['name']} ({info['port']})", + "type": "gphoto", + "index": index + }) + except Exception as e: + self.error_occurred.emit( + f"Błąd podczas wykrywania kamer GPhoto: {e}") - self._detected_cameras: list[dict] = [] - self._active_camera_info: dict | None = None - self.thread_pool = QThreadPool.globalInstance() # For detection worker + # Wykryj kamery OpenCV + try: + opencv_cameras = OpenCvCamera.detect() + for index, info in opencv_cameras.items(): + self._detected_cameras.append({ + "id": f"opencv_{index}", + "name": f"OpenCV: {info['name']}", + "type": "opencv", + "index": index + }) + except Exception as e: + self.error_occurred.emit( + f"Błąd podczas wykrywania kamer OpenCV: {e}") - def detect_cameras(self) -> None: - self.detection_started.emit() - worker = CameraDetectionWorker() - worker.signals.finished.connect(self._on_detection_finished) - worker.signals.error.connect(self.error_occurred) - self.thread_pool.start(worker) + self.cameras_detected.emit(self._detected_cameras) - def _on_detection_finished(self, detected_cameras: list): - self._detected_cameras = detected_cameras - self.cameras_detected.emit(self._detected_cameras) + def get_detected_cameras(self) -> list[dict]: + return self._detected_cameras - def get_detected_cameras(self) -> list[dict]: - return self._detected_cameras + def start_camera(self, camera_id: str, fps: int = 15) -> None: + """Uruchamia wybraną kamerę.""" + if self._active_camera: + self.stop_camera() - def start_camera(self, camera_id: str, fps: int = 15) -> None: - if self._active_camera_info and self._active_camera_info['id'] == camera_id: - return + camera_info = next( + (c for c in self._detected_cameras if c['id'] == camera_id), None) - if self._active_camera_info: - self.stop_camera() + if not camera_info: + self.error_occurred.emit( + f"Nie znaleziono kamery o ID: {camera_id}") + return - camera_info = next((c for c in self._detected_cameras if c['id'] == camera_id), None) + camera_type = camera_info['type'] + camera_index = camera_info['index'] - if not camera_info: - self.error_occurred.emit(f"Nie znaleziono kamery o ID: {camera_id}") - return + if camera_type == "gphoto": + self._active_camera = GPhotoCamera() + elif camera_type == "opencv": + self._active_camera = OpenCvCamera() + else: + self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}") + return - camera_type = camera_info['type'] - camera_index = camera_info['index'] + self._active_camera_info = camera_info - camera_instance: BaseCamera | None = None - if camera_type == "gphoto": - camera_instance = GPhotoCamera() - elif camera_type == "opencv": - camera_instance = OpenCvCamera() - else: - self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}") - return - - self._active_camera_info = camera_info - - # Emit signals to trigger slots in the worker thread - self._request_set_camera.emit(camera_instance, fps) - self._request_start_camera.emit() - self._request_start_stream.emit() - self.camera_started.emit() + self._camera_controller.set_camera(self._active_camera, fps) + self._camera_controller.start_camera() - def stop_camera(self) -> None: - if self._active_camera_info: - # Emit signals to trigger slots in the worker thread - self._request_stop_stream.emit() - self._request_stop_camera.emit() - self._active_camera_info = None - self.camera_stopped.emit() + def start_liveview(self, connected): + if connected: + self._camera_controller.start_stream() + self.camera_started.emit() + else: + self._active_camera = None + self._active_camera_info = None - def get_active_camera_info(self) -> dict | None: - return self._active_camera_info + def stop_camera(self) -> None: + """Zatrzymuje aktywną kamerę.""" + if self._active_camera: + self._camera_controller.stop_camera() + self._active_camera = None + self._active_camera_info = None + self.camera_stopped.emit() - def shutdown(self) -> None: - if self._camera_thread.isRunning(): - self._camera_thread.quit() - if not self._camera_thread.wait(5000): - print("Camera thread did not finish gracefully, terminating.") - self._camera_thread.terminate() - self._camera_thread.wait() + def get_active_camera_info(self) -> dict | None: + return self._active_camera_info + + def shutdown(self) -> None: + """Zamyka kontroler kamery i jego wątek.""" + self.stop_camera() + self._camera_controller.stop()