diff --git a/pxiii_bec/devices/NDArrayPreview.py b/pxiii_bec/devices/NDArrayPreview.py deleted file mode 100644 index f76c578..0000000 --- a/pxiii_bec/devices/NDArrayPreview.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -""" -``NDArrayPreview`` --- Standalone Preview for ImagePlugin -********************************************************* - -This module provides a standalone object to receive images to ophyd from the -AreaDetector's ImagePlugin. - -Created on Wed Jan 29 2025 - -@author: mohacsi_i -""" -import numpy as np -from ophyd import Device, Component, EpicsSignal, EpicsSignalWithRBV, Kind, Staged -from ophyd.areadetector import NDDerivedSignal - - -from bec_lib import bec_logger - -logger = bec_logger.logger - - -class SilentNDDerivedSignal(NDDerivedSignal): - """Silent version of NDDerivedSignal, it does not spam the terminal on - every defective frame (shit happens, ok?).""" - - def _array_shape_callback(self, **kwargs): - try: - super()._array_shape_callback(**kwargs) - except RuntimeError: - pass - - -class NDArrayPreview(Device): - """Wrapper class around AreaDetector's NDStdArray plugins - - This is a standalone class to display images from AreaDetector's - ImagePlugin without using a parent device. It also offers BEC exposed - methods to transfer image and change image array Kind-ness. - - NOTE: As an explicit request, it can toggle data recording - """ - - # Subscriptions for plotting image - USER_ACCESS = ["image", "savemode"] - SUB_MONITOR = "device_monitor_2d" - _default_sub = SUB_MONITOR - - # Status attributes - min_callback_time = Component( - EpicsSignalWithRBV, "MinCallbackTime", kind=Kind.config, put_complete=True - ) - array_size_x = Component(EpicsSignal, "ArraySize0_RBV", kind=Kind.config) - array_size_y = Component(EpicsSignal, "ArraySize1_RBV", kind=Kind.config) - array_size_z = Component(EpicsSignal, "ArraySize2_RBV", kind=Kind.config) - ndimensions = Component(EpicsSignal, "NDimensions_RBV", kind=Kind.config) - array_data = Component(EpicsSignal, "ArrayData", kind=Kind.omitted) - shaped_image = Component( - SilentNDDerivedSignal, - derived_from="array_data", - shape=("array_size_z", "array_size_y", "array_size_x"), - num_dimensions="ndimensions", - kind=Kind.omitted, - ) - - def read(self): - """Stream out data on every read()""" - if self._staged == Staged.yes: - image = self.shaped_image.get() - self._run_subs(sub_type=self.SUB_MONITOR, value=image) - return super().read() - - def savemode(self, save=False): - """Toggle save mode for the shaped image""" - # pylint: disable=protected-access - if save: - self.shaped_image._kind = Kind.normal - else: - self.shaped_image._kind = Kind.omitted - - def image(self): - """Fallback method in case image streaming fills up the BEC""" - array_size = (self.array_size_z.get(), self.array_size_y.get(), self.array_size_x.get()) - if array_size == (0, 0, 0): - raise RuntimeError("Invalid image; ensure array_callbacks are on") - - if array_size[-1] == 0: - array_size = array_size[:-1] - - image = self.array_data.get() - return np.array(image).reshape(array_size) - - -# Automatically connect to SAMCAM at PXIII if directly invoked -if __name__ == "__main__": - img = NDArrayPreview("X06DA-SAMCAM:image1:", name="samimg") - img.wait_for_connection() diff --git a/pxiii_bec/devices/StdDaqPreview.py b/pxiii_bec/devices/StdDaqPreview.py deleted file mode 100644 index afe4559..0000000 --- a/pxiii_bec/devices/StdDaqPreview.py +++ /dev/null @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Standard DAQ preview image stream module - -Created on Thu Jun 27 17:28:43 2024 - -@author: mohacsi_i -""" -import json -from time import sleep, time -from threading import Thread -import zmq -import numpy as np -from ophyd import Device, Signal, Component, Kind -from ophyd_devices.interfaces.base_classes.psi_detector_base import ( - CustomDetectorMixin, - PSIDetectorBase, -) - -from bec_lib import bec_logger - -logger = bec_logger.logger -ZMQ_TOPIC_FILTER = b"" - - -class StdDaqPreviewMixin(CustomDetectorMixin): - """Setup class for the standard DAQ preview stream - - Parent class: CustomDetectorMixin - """ - - _mon = None - - def on_stage(self): - """Start listening for preview data stream""" - if self._mon is not None: - self.parent.unstage() - sleep(0.5) - - logger.info(f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}") - self.parent.connect() - self._stop_polling = False - self._mon = Thread(target=self.poll, daemon=True) - self._mon.start() - - def on_unstage(self): - """Stop a running preview""" - if self._mon is not None: - self._stop_polling = True - # Might hang on recv_multipart - self._mon.join(timeout=1) - # So also disconnect the socket - try: - # pylint: disable=protected-access - self.parent._socket.disconnect(self.parent.url.get()) - except zmq.error.ZMQError: - # Might be already closed - pass - - def on_stop(self): - """Stop a running preview""" - self.on_unstage() - - def poll(self): - """Collect streamed updates""" - try: - t_last = time() - while True: - try: - # Exit loop and finish monitoring - if self._stop_polling: - logger.info(f"[{self.parent.name}]\tDetaching monitor") - break - - # pylint: disable=no-member - # pylint: disable=protected-access - r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) - - # Length and throtling checks - if len(r) != 2: - logger.warning( - f"[{self.parent.name}] Received malformed array of length {len(r)}" - ) - t_curr = time() - t_elapsed = t_curr - t_last - if t_elapsed < self.parent.throttle.get(): - sleep(0.1) - continue - - # Unpack the Array V1 reply to metadata and array data - meta, data = r - - # Update image and update subscribers - header = json.loads(meta) - image = np.frombuffer(data, dtype=header["type"]) - if image.size != np.prod(header["shape"]): - err = f"Unexpected array size of {image.size} for header: {header}" - raise ValueError(err) - image = image.reshape(header["shape"]) - - # Update image and update subscribers - self.parent.array_counter.put(header["frame"], force=True) - self.parent.ndimensions.put(len(header["shape"]), force=True) - self.parent.array_size.put(header["shape"], force=True) - # self.parent.array_data.put(data, force=True) - self.parent.shaped_image.put(image, force=True) - - # pylint: disable=protected-access - self.parent._last_image = image - self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) - t_last = t_curr - logger.info( - f"[{self.parent.name}] Updated frame {header['frame']}\t" - f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}" - ) - except ValueError: - # Happens when ZMQ partially delivers the multipart message - pass - except zmq.error.Again: - # Happens when receive queue is empty - sleep(0.1) - except Exception as ex: - logger.info(f"[{self.parent.name}]\t{str(ex)}") - raise - finally: - self._mon = None - logger.info(f"[{self.parent.name}]\tDetaching monitor") - - -class StdDaqPreviewDetector(PSIDetectorBase): - """Detector wrapper class around the StdDaq preview image stream. - - This was meant to provide live image stream directly from the StdDAQ but - also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin. - Note that the preview stream must be already throtled in order to cope with - the incoming data and the python class might throttle it further. - - NOTE: As an explicit request, it does not record the image data. - - You can add a preview widget to the dock by: - cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') - """ - - # Subscriptions for plotting image - USER_ACCESS = ["image", "savemode"] - SUB_MONITOR = "device_monitor_2d" - _default_sub = SUB_MONITOR - - custom_prepare_cls = StdDaqPreviewMixin - - # Configuration attributes - url = Component(Signal, kind=Kind.config, metadata={"write_access": False}) - throttle = Component(Signal, value=0.25, kind=Kind.config) - # Streamed data status - array_counter = Component(Signal, kind=Kind.hinted, metadata={"write_access": False}) - ndimensions = Component(Signal, kind=Kind.normal, metadata={"write_access": False}) - array_size = Component(Signal, kind=Kind.normal, metadata={"write_access": False}) - # array_data = Component(Signal, kind=Kind.omitted, metadata={"write_access": False}) - shaped_image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False}) - _last_image = None - - def __init__( - self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs - ) -> None: - super().__init__(*args, parent=parent, **kwargs) - self.url.set(url, force=True).wait() - # Connect to the DAQ - self.connect() - - def connect(self): - """Connect to te StDAQs PUB-SUB streaming interface - - StdDAQ may reject connection for a few seconds when it restarts, - so if it fails, wait a bit and try to connect again. - """ - # pylint: disable=no-member - # Socket to talk to server - context = zmq.Context() - self._socket = context.socket(zmq.SUB) - self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER) - try: - self._socket.connect(self.url.get()) - except ConnectionRefusedError: - sleep(1) - self._socket.connect(self.url.get()) - - def savemode(self, save=False): - """Toggle save mode for the shaped image""" - # pylint: disable=protected-access - if save: - self.shaped_image._kind = Kind.normal - else: - self.shaped_image._kind = Kind.omitted - - def image(self): - """ - Gets the last image as an attribute in case image must be abandoned - due to some caching on the BEC. - """ - return self._last_image - - -# Automatically connect to MicroSAXS testbench if directly invoked -if __name__ == "__main__": - daq = StdDaqPreviewDetector(url="tcp://129.129.95.111:20000", name="preview") - daq.wait_for_connection() diff --git a/pxiii_bec/devices/__init__.py b/pxiii_bec/devices/__init__.py index d9be43d..8d83b17 100644 --- a/pxiii_bec/devices/__init__.py +++ b/pxiii_bec/devices/__init__.py @@ -10,9 +10,7 @@ from mx_bec.devices.beam_steering import BeamSteerer from .A3200 import AerotechAbrStage from .A3200utils import A3200Axis -from .NDArrayPreview import NDArrayPreview from .PneumaticValve import PneumaticValve from .SamCamDetector import SamCamDetector from .SmarGonA import SmarGonAxis as SmarGonAxisA from .SmarGonB import SmarGonAxis as SmarGonAxisB -from .StdDaqPreview import StdDaqPreviewDetector