From c6345c569de0e19bbfd5325ada788ec1c0747fe6 Mon Sep 17 00:00:00 2001 From: bartool Date: Mon, 13 Oct 2025 05:14:27 +0200 Subject: [PATCH] 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()