From 7e9b8972b06008ef62b1c151b4f2d64744bfb369 Mon Sep 17 00:00:00 2001 From: gac-x01dc Date: Tue, 23 Sep 2025 15:29:04 +0200 Subject: [PATCH] - added camera viewer device - fixed some issues in flomni sample storage device --- csaxs_bec/device_configs/flomni_config.yaml | 26 ++++++++ .../devices/omny/flomni_sample_storage.py | 7 +- csaxs_bec/devices/omny/omny_sample_storage.py | 28 ++++---- csaxs_bec/devices/omny/webcam_viewer.py | 65 +++++++++++++++++++ 4 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 csaxs_bec/devices/omny/webcam_viewer.py diff --git a/csaxs_bec/device_configs/flomni_config.yaml b/csaxs_bec/device_configs/flomni_config.yaml index 3221331..24e9679 100644 --- a/csaxs_bec/device_configs/flomni_config.yaml +++ b/csaxs_bec/device_configs/flomni_config.yaml @@ -364,3 +364,29 @@ rtz: onFailure: buffer readOnly: false readoutPriority: on_request + + + +cam_flomni_gripper: + description: Camera sample changer + deviceClass: csaxs_bec.devices.omny.webcam_viewer.WebcamViewer + deviceConfig: + url: http://flomnicamserver:5000/video_high + num_rotation_90: 3 + transpose: false + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: on_request + +cam_flomni_overview: + description: Camera flomni overview + deviceClass: csaxs_bec.devices.omny.webcam_viewer.WebcamViewer + deviceConfig: + url: http://flomnicamserver:5001/video_high + num_rotation_90: 3 + transpose: false + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: on_request \ No newline at end of file diff --git a/csaxs_bec/devices/omny/flomni_sample_storage.py b/csaxs_bec/devices/omny/flomni_sample_storage.py index bf154fa..ca8c940 100644 --- a/csaxs_bec/devices/omny/flomni_sample_storage.py +++ b/csaxs_bec/devices/omny/flomni_sample_storage.py @@ -24,24 +24,25 @@ class FlomniSampleStorage(Device): SUB_VALUE = "value" _default_sub = SUB_VALUE sample_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {"auto_monitor": True}) for i in range(21) } sample_placed = Dcpt(sample_placed) sample_names = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True, "auto_monitor": True}) for i in range(21) } sample_names = Dcpt(sample_names) sample_in_gripper = Cpt( - EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET" + EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET", auto_monitor=True ) sample_in_gripper_name = Cpt( EpicsSignal, name="sample_in_gripper_name", read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC", string=True, + auto_monitor=True ) def __init__(self, prefix="", *, name, **kwargs): diff --git a/csaxs_bec/devices/omny/omny_sample_storage.py b/csaxs_bec/devices/omny/omny_sample_storage.py index 22da15c..4675176 100644 --- a/csaxs_bec/devices/omny/omny_sample_storage.py +++ b/csaxs_bec/devices/omny/omny_sample_storage.py @@ -37,84 +37,86 @@ class OMNYSampleStorage(Device): _default_sub = SUB_VALUE sample_shuttle_A_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}", {}) for i in range(1, 7) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}", {"auto_monitor": True}) for i in range(1, 7) } sample_shuttle_A_placed = Dcpt(sample_shuttle_A_placed) sample_shuttle_B_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}", {}) for i in range(1, 7) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}", {"auto_monitor": True}) for i in range(1, 7) } sample_shuttle_B_placed = Dcpt(sample_shuttle_B_placed) sample_shuttle_C_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {"auto_monitor": True}) for i in range(1, 7) } sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed) sample_shuttle_C_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {"auto_monitor": True}) for i in range(1, 7) } sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed) parking_placed = { - f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}", {}) for i in range(1, 7) + f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}", {"auto_monitor": True}) for i in range(1, 7) } parking_placed = Dcpt(parking_placed) sample_placed = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {"auto_monitor": True}) for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101] } sample_placed = Dcpt(sample_placed) sample_shuttle_A_names = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}.DESC", {"string": True}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}.DESC", {"string": True, "auto_monitor": True}) for i in range(1, 7) } sample_shuttle_A_names = Dcpt(sample_shuttle_A_names) sample_shuttle_B_names = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}.DESC", {"string": True}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}.DESC", {"string": True, "auto_monitor": True}) for i in range(1, 7) } sample_shuttle_B_names = Dcpt(sample_shuttle_B_names) sample_shuttle_C_names = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}.DESC", {"string": True}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}.DESC", {"string": True, "auto_monitor": True}) for i in range(1, 7) } sample_shuttle_C_names = Dcpt(sample_shuttle_C_names) parking_names = { - f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}.DESC", {"string": True}) + f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}.DESC", {"string": True, "auto_monitor": True}) for i in range(1, 7) } parking_names = Dcpt(parking_names) sample_names = { - f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}.DESC", {"string": True}) + f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}.DESC", {"string": True, "auto_monitor": True}) for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101] } sample_names = Dcpt(sample_names) sample_in_gripper = Cpt( - EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_omny:110.VAL" + EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_omny:110.VAL", auto_monitor=True ) sample_in_gripper_name = Cpt( EpicsSignal, name="sample_in_gripper_name", read_pv="XOMNY-SAMPLE_DB_omny:110.DESC", string=True, + auto_monitor=True ) sample_in_samplestage = Cpt( - EpicsSignal, name="sample_in_samplestage", read_pv="XOMNY-SAMPLE_DB_omny:0.VAL" + EpicsSignal, name="sample_in_samplestage", read_pv="XOMNY-SAMPLE_DB_omny:0.VAL", auto_monitor=True ) sample_in_samplestage_name = Cpt( EpicsSignal, name="sample_in_samplestage_name", read_pv="XOMNY-SAMPLE_DB_omny:0.DESC", string=True, + auto_monitor=True ) def __init__(self, prefix="", *, name, **kwargs): diff --git a/csaxs_bec/devices/omny/webcam_viewer.py b/csaxs_bec/devices/omny/webcam_viewer.py new file mode 100644 index 0000000..3bfecb2 --- /dev/null +++ b/csaxs_bec/devices/omny/webcam_viewer.py @@ -0,0 +1,65 @@ +import requests +import threading +import cv2 +import numpy as np +from ophyd import Device, Component as Cpt +from ophyd_devices import PreviewSignal +import traceback + +from bec_lib.logger import bec_logger + +logger = bec_logger.logger + +class WebcamViewer(Device): + USER_ACCESS = ["start", "stop"] + preview = Cpt(PreviewSignal, ndim=2, num_rotation_90=0, transpose=False) + + def __init__(self, url:str, name:str, num_rotation_90=0, transpose=False, **kwargs) -> None: + super().__init__(name=name, **kwargs) + self.url = url + self._connection = None + self._update_thread = None + self._buffer = b"" + self._shutdown_event = threading.Event() + self.preview.num_rotation_90 = num_rotation_90 + self.preview.transpose = transpose + + def start(self) -> None: + if self._connection is not None: + return + self._update_thread = threading.Thread(target=self._update_loop, daemon=True) + self._update_thread.start() + + def _update_loop(self) -> None: + while not self._shutdown_event.is_set(): + try: + self._connection = requests.get(self.url, stream=True) + for chunk in self._connection.iter_content(chunk_size=1024): + self._buffer += chunk + start = self._buffer.find(b'\xff\xd8') # JPEG start + end = self._buffer.find(b'\xff\xd9') # JPEG end + if start == -1 or end == -1: + continue + jpg = self._buffer[start:end+2] + self._buffer = self._buffer[end+2:] + + image = cv2.imdecode(np.frombuffer(jpg, np.uint8), cv2.IMREAD_COLOR) + if image is not None: + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + self.preview.put(image) + except Exception as exc: + content = traceback.format_exc() + logger.error(f"Image update loop failed: {content}") + + def stop(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