refactor: update camera control signals and improve live view handling

This commit is contained in:
2025-10-13 05:14:27 +02:00
parent 73b51c696e
commit c6345c569d
3 changed files with 104 additions and 147 deletions

View File

@@ -112,7 +112,7 @@ class MainController:
@Slot() @Slot()
def on_camera_started(self): def on_camera_started(self):
"""Updates UI when the camera stream starts.""" """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") self.welcome_view.set_button_text("Stop Camera")
# Re-route button click to stop the camera # Re-route button click to stop the camera
self.welcome_view.camera_start_btn.clicked.disconnect() self.welcome_view.camera_start_btn.clicked.disconnect()

View File

@@ -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 from PySide6.QtGui import QImage, QPixmap
import cv2 import cv2
@@ -9,6 +9,7 @@ class CameraWorker(QObject):
frame_ready = Signal(QPixmap) frame_ready = Signal(QPixmap)
photo_ready = Signal(QPixmap) photo_ready = Signal(QPixmap)
error_occurred = Signal(str) error_occurred = Signal(str)
camera_ready = Signal(bool)
def __init__(self, parent: QObject | None = None) -> None: def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent) super().__init__(parent)
@@ -44,8 +45,10 @@ class CameraWorker(QObject):
if self.camera.connect(): if self.camera.connect():
self.is_connected = True self.is_connected = True
self.camera_ready.emit(True)
else: else:
self.is_connected = False self.is_connected = False
self.camera_ready.emit(False)
self.error_occurred.emit(self.camera.get_error_msg()) self.error_occurred.emit(self.camera.get_error_msg())
@Slot() @Slot()
@@ -98,14 +101,13 @@ class CameraWorker(QObject):
pixmap = QPixmap.fromImage(qimg) pixmap = QPixmap.fromImage(qimg)
self.frame_ready.emit(pixmap) self.frame_ready.emit(pixmap)
def isConnected(self):
return self.is_connected
class CameraController(QObject): class CameraController(QObject):
frame_ready = Signal(QPixmap) frame_ready = Signal(QPixmap)
photo_ready = Signal(QPixmap) photo_ready = Signal(QPixmap)
error_occurred = Signal(str) error_occurred = Signal(str)
camera_ready = Signal(bool)
# Signals to command the worker # Signals to command the worker
_set_camera_requested = Signal(BaseCamera, int) _set_camera_requested = Signal(BaseCamera, int)
@@ -125,6 +127,7 @@ class CameraController(QObject):
self._worker.frame_ready.connect(self.frame_ready) self._worker.frame_ready.connect(self.frame_ready)
self._worker.photo_ready.connect(self.photo_ready) self._worker.photo_ready.connect(self.photo_ready)
self._worker.error_occurred.connect(self.error_occurred) 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 # Connect controller's command signals to worker's slots
self._set_camera_requested.connect(self._worker.set_camera) self._set_camera_requested.connect(self._worker.set_camera)
@@ -157,5 +160,3 @@ class CameraController(QObject):
def stop_stream(self) -> None: def stop_stream(self) -> None:
self._stop_stream_requested.emit() self._stop_stream_requested.emit()
def is_connected(self):
return self._worker.isConnected()

View File

@@ -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 PySide6.QtGui import QPixmap
from .camera_controller import CameraController from .camera_controller import CameraController
@@ -7,159 +7,115 @@ from .opencv_camera import OpenCvCamera
from .base_camera import BaseCamera 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): class CameraManager(QObject):
""" """
Zarządza wszystkimi operacjami związanymi z kamerami, Zarządza wszystkimi operacjami związanymi z kamerami,
stanowiąc fasadę dla reszty aplikacji. stanowiąc fasadę dla reszty aplikacji.
""" """
# --- Public API Signals --- frame_ready = Signal(QPixmap)
frame_ready = Signal(QPixmap) error_occurred = Signal(str)
error_occurred = Signal(str) cameras_detected = Signal(list)
detection_started = Signal() camera_started = Signal()
cameras_detected = Signal(list) camera_stopped = Signal()
camera_started = Signal()
camera_stopped = Signal()
# --- Internal signals to communicate with worker thread --- def __init__(self, parent: QObject | None = None) -> None:
_request_set_camera = Signal(BaseCamera, int) super().__init__(parent)
_request_start_camera = Signal() self._camera_controller = CameraController()
_request_stop_camera = Signal() self._detected_cameras: list[dict] = []
_request_start_stream = Signal() self._active_camera: BaseCamera | None = None
_request_stop_stream = Signal() self._active_camera_info: dict | None = None
def __init__(self, parent: QObject | None = None) -> None: # Przekazywanie sygnałów z kontrolera kamery na zewnątrz
super().__init__(parent) 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)
self._camera_thread = QThread() def detect_cameras(self) -> None:
self._camera_controller = CameraController() """Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV)."""
self._camera_controller.moveToThread(self._camera_thread) self._detected_cameras.clear()
# --- Connections --- # Wykryj kamery GPhoto
# Connect signals from controller to be re-emitted by manager try:
self._camera_controller.frame_ready.connect(self.frame_ready) gphoto_cameras = GPhotoCamera.detect()
self._camera_controller.error_occurred.connect(self.error_occurred) 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}")
# Connect internal requests to controller slots # Wykryj kamery OpenCV
self._request_set_camera.connect(self._camera_controller.set_camera) try:
self._request_start_camera.connect(self._camera_controller.start_camera) opencv_cameras = OpenCvCamera.detect()
self._request_stop_camera.connect(self._camera_controller.stop_camera) for index, info in opencv_cameras.items():
self._request_start_stream.connect(self._camera_controller.start_stream) self._detected_cameras.append({
self._request_stop_stream.connect(self._camera_controller.stop_stream) "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}")
# Connect thread management self.cameras_detected.emit(self._detected_cameras)
self._camera_thread.started.connect(self._camera_controller.run)
self._camera_thread.start() def get_detected_cameras(self) -> list[dict]:
return self._detected_cameras
self._detected_cameras: list[dict] = [] def start_camera(self, camera_id: str, fps: int = 15) -> None:
self._active_camera_info: dict | None = None """Uruchamia wybraną kamerę."""
self.thread_pool = QThreadPool.globalInstance() # For detection worker if self._active_camera:
self.stop_camera()
def detect_cameras(self) -> None: camera_info = next(
self.detection_started.emit() (c for c in self._detected_cameras if c['id'] == camera_id), None)
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): if not camera_info:
self._detected_cameras = detected_cameras self.error_occurred.emit(
self.cameras_detected.emit(self._detected_cameras) f"Nie znaleziono kamery o ID: {camera_id}")
return
def get_detected_cameras(self) -> list[dict]: camera_type = camera_info['type']
return self._detected_cameras camera_index = camera_info['index']
def start_camera(self, camera_id: str, fps: int = 15) -> None: if camera_type == "gphoto":
if self._active_camera_info and self._active_camera_info['id'] == camera_id: self._active_camera = GPhotoCamera()
return elif camera_type == "opencv":
self._active_camera = OpenCvCamera()
else:
self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}")
return
if self._active_camera_info: self._active_camera_info = camera_info
self.stop_camera()
camera_info = next((c for c in self._detected_cameras if c['id'] == camera_id), None) self._camera_controller.set_camera(self._active_camera, fps)
self._camera_controller.start_camera()
if not camera_info: def start_liveview(self, connected):
self.error_occurred.emit(f"Nie znaleziono kamery o ID: {camera_id}") if connected:
return self._camera_controller.start_stream()
self.camera_started.emit()
else:
self._active_camera = None
self._active_camera_info = None
camera_type = camera_info['type'] def stop_camera(self) -> None:
camera_index = camera_info['index'] """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()
camera_instance: BaseCamera | None = None def get_active_camera_info(self) -> dict | None:
if camera_type == "gphoto": return self._active_camera_info
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 def shutdown(self) -> None:
"""Zamyka kontroler kamery i jego wątek."""
# Emit signals to trigger slots in the worker thread self.stop_camera()
self._request_set_camera.emit(camera_instance, fps) self._camera_controller.stop()
self._request_start_camera.emit()
self._request_start_stream.emit()
self.camera_started.emit()
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 get_active_camera_info(self) -> dict | None:
return self._active_camera_info
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()