From 3d73b0a1603bea37ec0acea2c5bee56d87471d0b Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 16 Dec 2025 00:19:52 +0100 Subject: [PATCH] wip unify the live mode on cameras --- .../devices/epics/allied_vision_camera.py | 44 +++++++++---- .../ids_cameras/base_integration/camera.py | 1 - csaxs_bec/devices/ids_cameras/ids_camera.py | 43 ++++++------ csaxs_bec/devices/omny/webcam_viewer.py | 66 +++++++++++++------ 4 files changed, 101 insertions(+), 53 deletions(-) diff --git a/csaxs_bec/devices/epics/allied_vision_camera.py b/csaxs_bec/devices/epics/allied_vision_camera.py index bcba5e0..870bca5 100644 --- a/csaxs_bec/devices/epics/allied_vision_camera.py +++ b/csaxs_bec/devices/epics/allied_vision_camera.py @@ -6,7 +6,7 @@ from enum import IntEnum import numpy as np from bec_lib.logger import bec_logger -from ophyd import Component as Cpt +from ophyd import Component as Cpt, Kind, Signal from ophyd.areadetector import ADComponent as ADCpt from ophyd.areadetector import DetectorBase from ophyd_devices import PreviewSignal @@ -44,6 +44,14 @@ class AlliedVisionCamera(PSIDeviceBase, DetectorBase): doc="Preview signal of the AlliedVision camera.", ) + live_mode_enabled = Cpt( + Signal, + name="live_mode_enabled", + value=False, + doc="Enable or disable live mode.", + kind=Kind.config, + ) + def __init__( self, *, @@ -68,24 +76,36 @@ class AlliedVisionCamera(PSIDeviceBase, DetectorBase): self._poll_rate = poll_rate self._unique_array_id = 0 self._pv_timeout = 2.0 - self.r_lock = threading.RLock() self.image: ImagePlugin + self._live_mode_lock = threading.RLock() + self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False) def start_live_mode(self) -> None: """Start live mode.""" - if not self._poll_start_event.is_set(): - self._poll_start_event.set() - self.cam.acquire.put(ACQUIRE_MODES.ACQUIRING.value) # Start acquisition - else: - logger.info(f"Live mode already started for {self.name}.") + self.live_mode_enabled.put(True) def stop_live_mode(self) -> None: """Stop live mode.""" - if self._poll_start_event.is_set(): - self._poll_start_event.clear() - self.cam.acquire.put(ACQUIRE_MODES.DONE.value) # Stop acquisition - else: - logger.info(f"Live mode already stopped for {self.name}.") + self.live_mode_enabled.put(False) + + def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None: + self._apply_live_mode(bool(value)) + + def _apply_live_mode(self, enabled: bool) -> None: + with self._live_mode_lock: + if enabled: + if not self._poll_start_event.is_set(): + self._poll_start_event.set() + self.cam.acquire.put(ACQUIRE_MODES.ACQUIRING.value) # Start acquisition + else: + logger.info(f"Live mode already started for {self.name}.") + return + + if self._poll_start_event.is_set(): + self._poll_start_event.clear() + self.cam.acquire.put(ACQUIRE_MODES.DONE.value) # Stop acquisition + else: + logger.info(f"Live mode already stopped for {self.name}.") def on_connected(self): """Reset the unique array ID on connection.""" diff --git a/csaxs_bec/devices/ids_cameras/base_integration/camera.py b/csaxs_bec/devices/ids_cameras/base_integration/camera.py index bc73210..5ca067b 100644 --- a/csaxs_bec/devices/ids_cameras/base_integration/camera.py +++ b/csaxs_bec/devices/ids_cameras/base_integration/camera.py @@ -156,7 +156,6 @@ class Camera: camera_id (int): The ID of the camera device. m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera. bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera. - live_mode (bool): Whether to enable live mode for the camera. """ def __init__( diff --git a/csaxs_bec/devices/ids_cameras/ids_camera.py b/csaxs_bec/devices/ids_cameras/ids_camera.py index 1876447..e531b17 100644 --- a/csaxs_bec/devices/ids_cameras/ids_camera.py +++ b/csaxs_bec/devices/ids_cameras/ids_camera.py @@ -37,9 +37,15 @@ class IDSCamera(PSIDeviceBase): doc="Signal for the region of interest (ROI).", async_update={"type": "add", "max_shape": [None]}, ) - live_mode_enabled = Cpt(Signal, name="live_mode_enabled", value=False ,doc="Enable or disable live mode.",kind=Kind.config) + live_mode_enabled = Cpt( + Signal, + name="live_mode_enabled", + value=False, + doc="Enable or disable live mode.", + kind=Kind.config, + ) - USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"] + USER_ACCESS = ["start_live_mode", "stop_live_mode", "mask", "set_rect_roi", "get_last_image"] def __init__( self, @@ -81,11 +87,17 @@ class IDSCamera(PSIDeviceBase): self.image.num_rotation_90 = num_rotation_90 self.image.transpose = transpose self._force_monochrome = force_monochrome - self.live_mode_enabled.subscribe(self.on_live_mode_changed,run=False) - self.live_mode_enabled.put(live_mode) + self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False) + self.live_mode_enabled.put(bool(live_mode)) ############## Live Mode Methods ############## + def start_live_mode(self) -> None: + self.live_mode_enabled.put(True) + + def stop_live_mode(self) -> None: + self.live_mode_enabled.put(False) + @property def mask(self) -> np.ndarray: """Return the current region of interest (ROI) for the camera.""" @@ -108,23 +120,12 @@ class IDSCamera(PSIDeviceBase): ) self._mask = value - @property - def live_mode(self) -> bool: - """Return whether the camera is in live mode.""" - return bool(self.live_mode_enabled.get()) - - @live_mode.setter - def live_mode(self, value: bool): - """Set the live mode for the camera.""" - if value != bool(self.live_mode_enabled.get()): - self.live_mode_enabled.put(bool(value)) - - def on_live_mode_changed(self, *args, value, **kwargs): + def _on_live_mode_enabled_changed(self, *args, value, **kwargs): """Callback for when live mode is changed.""" - if self.cam._connected is False: # $ pylint: disable=protected-access + enabled = bool(value) + if enabled and self.cam._connected is False: # pylint: disable=protected-access self.cam.on_connect() - self.live_mode_enabled.put(bool(value)) - if value: + if enabled: self._start_live() else: self._stop_live() @@ -192,7 +193,7 @@ class IDSCamera(PSIDeviceBase): """Connect to the camera.""" self.cam.force_monochrome = self._force_monochrome self.cam.on_connect() - self.live_mode = self._inputs.get("live_mode", False) + self.live_mode_enabled.put(bool(self._inputs.get("live_mode", False))) self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value) def on_destroy(self): @@ -202,7 +203,7 @@ class IDSCamera(PSIDeviceBase): def on_trigger(self): """Handle the trigger event.""" - if not self.live_mode: + if not bool(self.live_mode_enabled.get()): return image = self.image.get() if image is not None: diff --git a/csaxs_bec/devices/omny/webcam_viewer.py b/csaxs_bec/devices/omny/webcam_viewer.py index a15b1fd..a150b27 100644 --- a/csaxs_bec/devices/omny/webcam_viewer.py +++ b/csaxs_bec/devices/omny/webcam_viewer.py @@ -2,7 +2,7 @@ import requests import threading import cv2 import numpy as np -from ophyd import Device, Component as Cpt +from ophyd import Device, Component as Cpt, Kind, Signal from ophyd_devices import PreviewSignal import traceback @@ -13,6 +13,13 @@ logger = bec_logger.logger class WebcamViewer(Device): USER_ACCESS = ["start_live_mode", "stop_live_mode"] preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False) + live_mode_enabled = Cpt( + Signal, + name="live_mode_enabled", + value=False, + doc="Enable or disable live mode.", + kind=Kind.config, + ) def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None: super().__init__(name=name, **kwargs) @@ -21,20 +28,54 @@ class WebcamViewer(Device): self._update_thread = None self._buffer = b"" self._shutdown_event = threading.Event() + self._live_mode_lock = threading.RLock() self.preview.num_rotation_90 = num_rotation_90 self.preview.transpose = transpose + self.live_mode_enabled.subscribe(self._on_live_mode_enabled_changed, run=False) def start_live_mode(self) -> None: - if self._connection is not None: - return - self._update_thread = threading.Thread(target=self._update_loop, daemon=True) - self._update_thread.start() + self.live_mode_enabled.put(True) + + def stop_live_mode(self) -> None: + self.live_mode_enabled.put(False) + + def _on_live_mode_enabled_changed(self, *args, value, **kwargs) -> None: + self._apply_live_mode(bool(value)) + + def _apply_live_mode(self, enabled: bool) -> None: + with self._live_mode_lock: + if enabled: + if self._update_thread is not None and self._update_thread.is_alive(): + return + self._shutdown_event.clear() + self._update_thread = threading.Thread(target=self._update_loop, daemon=True) + self._update_thread.start() + return + + if self._update_thread is None: + return + self._shutdown_event.set() + if self._connection is not None: + try: + self._connection.close() + except Exception: # pylint: disable=broad-except + pass + self._connection = None + self._update_thread.join(timeout=2) + if self._update_thread.is_alive(): + logger.warning("Webcam live mode thread did not stop within timeout.") + return + self._update_thread = None + self._buffer = b"" + self._shutdown_event.clear() def _update_loop(self) -> None: while not self._shutdown_event.is_set(): try: - self._connection = requests.get(self.url, stream=True) + self._connection = requests.get(self.url, stream=True, timeout=5) for chunk in self._connection.iter_content(chunk_size=1024): + if self._shutdown_event.is_set(): + break self._buffer += chunk start = self._buffer.find(b'\xff\xd8') # JPEG start end = self._buffer.find(b'\xff\xd9') # JPEG end @@ -50,16 +91,3 @@ class WebcamViewer(Device): except Exception as exc: content = traceback.format_exc() logger.error(f"Image update loop failed: {content}") - - def stop_live_mode(self) -> None: - if self._connection is None: - return - self._shutdown_event.set() - if self._connection is not None: - self._connection.close() - self._connection = None - if self._update_thread is not None: - self._update_thread.join() - self._update_thread = None - - self._shutdown_event.clear() \ No newline at end of file