diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 066159e..40c51e5 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -6,6 +6,7 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ +import sys from time import sleep from ophyd import Signal, SignalRO, Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged @@ -175,22 +176,17 @@ class GigaFrostCameraMixin(CustomDetectorMixin): Specify actions to be executed upon receiving trigger signal. Return a DeviceStatus object or None """ - status = DeviceStatus(self.parent) + if self.parent.infoBusyFlag.get() in (0, 'IDLE'): + raise RuntimeError('GigaFrost must be running before triggering') # Soft triggering based on operation mode if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': # BEC teststand operation mode: posedge of SoftEnable if Started self.parent.cmdSoftEnable.set(0).wait() self.parent.cmdSoftEnable.set(1).wait() - sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 - # There's no status readback from the camera, so we just wait - sleep(sleep_time) - logger.info(f"[GF2] Slept for: {sleep_time} seconds") + else: self.parent.cmdSoftTrigger.set(1).wait() - status.set_finished() - - return status class GigaFrostCamera(PSIDetectorBase): @@ -411,6 +407,18 @@ class GigaFrostCamera(PSIDetectorBase): self.state.put(const.GfStatus.NEW, force=True) return super()._init() + def trigger(self) -> DeviceStatus: + + super().trigger() + + # There's no status readback from the camera, so we just wait + status = DeviceStatus(self) + sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050 + sleep(sleep_time) + logger.info(f"[GF2] Slept for: {sleep_time} seconds") + status.set_finished() + return status + def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index b34ea89..aff02f2 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -32,11 +32,18 @@ except ModuleNotFoundError: from tomcat_bec.devices.gigafrost.gigafrostcamera import GigaFrostCamera +try: + from bec_lib import bec_logger + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + logger = logging.getLogger("GfCam") -class GigafrostClientMixin(CustomDetectorMixin): + +class GigaFrostClientMixin(CustomDetectorMixin): """Mixin class to setup TOMCAT specific implementations of the detector. This class will be called by the custom_prepare_cls attribute of the detector class. @@ -83,51 +90,10 @@ class GigafrostClientMixin(CustomDetectorMixin): def on_init(self) -> None: - """Initialize the camera, set channel values""" - ## Stop acquisition - self.parent.cmdStartCamera.set(0).wait() + """Initialize the camera, set channel values - ### set entry to UDP table - # number of UDP ports to use - self.parent.cfgUdpNumPorts.set(2).wait() - # number of images to send to each UDP port before switching to next - self.parent.cfgUdpNumFrames.set(5).wait() - # offset in UDP table - where to find the first entry - self.parent.cfgUdpHtOffset.set(0).wait() - # activate changes - self.parent.cmdWriteService.set(1).wait() - - # Configure software triggering if needed - if self.parent._auto_soft_enable: - # trigger modes - self.parent.cfgCntStartBit.set(1).wait() - self.parent.cfgCntEndBit.set(0).wait() - - # set modes - self.parent.enable_mode = "soft" - self.parent.trigger_mode = "auto" - self.parent.exposure_mode = "timer" - - # line swap - on for west, off for east - self.parent.cfgLineSwapSW.set(1).wait() - self.parent.cfgLineSwapNW.set(1).wait() - self.parent.cfgLineSwapSE.set(0).wait() - self.parent.cfgLineSwapNE.set(0).wait() - - # Commit parameters - self.parent.cmdSetParam.set(1).wait() - - # Initialize data backend - n, s = self._define_backend_ip() - self.parent.ipNorth.put(n, force=True) - self.parent.ipSouth.put(s, force=True) - n, s = self._define_backend_mac() - self.parent.macNorth.put(n, force=True) - self.parent.macSouth.put(s, force=True) - # Set udp header table - self._set_udp_header_table() - - self.parent.state.put(const.GfStatus.INIT, force=True) + on_init is automatically called during __init__ of the sub devices. + """ return super().on_init() @@ -144,14 +110,13 @@ class GigafrostClientMixin(CustomDetectorMixin): It must be safe to assume that the device is ready for the scan to start immediately once this function is finished. """ - if self.parent.infoBusyFlag.value: - raise RuntimeError("Camera is already busy, unstage it first!") - # Switch to acquiring - self.parent.cmdStartCamera.set(1).wait() - self.parent.state.put(const.GfStatus.ACQUIRING, force=True) # Gigafrost can finish a run without explicit unstaging self.parent._staged = Staged.no + #self.parent.daq.stage() + #self.parent.cam.stage() + + def on_unstage(self) -> None: """ Specify actions to be executed during unstage. @@ -160,11 +125,8 @@ class GigafrostClientMixin(CustomDetectorMixin): and publishing the file location and file event message, with flagged done to BEC. """ - # Switch to idle - self.parent.cmdStartCamera.set(0).wait() - if self.parent.autoSoftEnable.get(): - self.cmdSoftEnable.set(0).wait() - self.parent.state.put(const.GfStatus.STOPPED, force=True) + self.parent.cam.unstage() + self.parent.daq.unstage() def on_stop(self) -> None: """ @@ -180,22 +142,7 @@ class GigafrostClientMixin(CustomDetectorMixin): Specify actions to be executed upon receiving trigger signal. Return a DeviceStatus object or None """ - status = DeviceStatus(self.parent) - - # Soft triggering based on operation mode - if self.parent.autoSoftEnable.get() and self.parent.trigger_mode=='auto' and self.parent.enable_mode=='soft': - # BEC teststand operation mode: posedge of SoftEnable if Started - self.parent.cmdSoftEnable.set(0).wait() - self.parent.cmdSoftEnable.set(1).wait() - sleep_time = self.parent.cfgFramerate.value*self.parent.cfgCntNum.value*0.001+0.050 - # There's no status readback from the camera, so we just wait - sleep(sleep_time) - print(f"[GF2] Slept for: {sleep_time} seconds", file=sys.stderr) - else: - self.parent.cmdSoftTrigger.set(1).wait() - status.set_finished() - - return status + return self.parent.cam.trigger() class GigaFrostClient(PSIDetectorBase): @@ -229,6 +176,8 @@ class GigaFrostClient(PSIDetectorBase): FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time """ # pylint: disable=too-many-instance-attributes + custom_prepare_cls = GigaFrostClientMixin + USER_ACCESS = [""] cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") daq = Component(StdDaqWsClient, name="daq") @@ -279,10 +228,10 @@ class GigaFrostClient(PSIDetectorBase): Exposure time [ms]. (default = 0.2) period : float, optional Exposure period [ms], ignored in soft trigger mode. (default = 1.0) - roix : int, optional - ROI size in the x-direction [pixels] (default = 2016) - roiy : int, optional - ROI size in the y-direction [pixels] (default = 2016) + pixel_width : int, optional + Image size in the x-direction [pixels] (default = 2016) + pixel_height : int, optional + Image size in the y-direction [pixels] (default = 2016) scanid : int, optional Scan identification number to be associated with the scan data (default = 0) @@ -297,6 +246,8 @@ class GigaFrostClient(PSIDetectorBase): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ + # Unstage camera (reconfiguration will anyway stop camera) + super().unstage() # If Bluesky style configure old = self.read_configuration() self.cam.configure(d) @@ -316,7 +267,9 @@ class GigaFrostClient(PSIDetectorBase): return super().stage() - + def trigger(self) -> DeviceStatus: + status = super().trigger() + return status # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 4c42fd3..d7fb4f0 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -53,7 +53,7 @@ class StdDaqPreview(Device): frame = Component(Signal, kind=Kind.normal) image_shape = Component(Signal, kind=Kind.omitted) value = Component(Signal, kind=Kind.hinted) - _throttle = 0.05 + _throttle = 0.2 def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -89,7 +89,7 @@ class StdDaqPreview(Device): sleep(1) self._socket.connect(self.url.get()) - def configure(self, throttle: float = 0.5) -> tuple: + def configure(self, throttle: float = 0.2) -> tuple: """Set the DAQ preview parameters Note that there's not much to do except for additional throtling if the @@ -98,7 +98,7 @@ class StdDaqPreview(Device): Example: ---------- - std.configure(throttle=0.05) + std.configure(throttle=0.2) Parameters ---------- @@ -130,31 +130,31 @@ class StdDaqPreview(Device): t_last = time() try: while True: + # Exit loop and finish monitoring + if self._stop_polling: + logger.info(f"[{self.name}]\tDetaching monitor") + break + try: # pylint: disable=no-member meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK) - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - if image.size != np.prod(header['shape']): - raise ValueError(f"Unexpected array size of {image.size} for header: {header}") - image = image.reshape(header['shape']) - - # Update image and update subscribers t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self._throttle: + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + if image.size != np.prod(header['shape']): + raise ValueError(f"Unexpected array size of {image.size} for header: {header}") + image = image.reshape(header['shape']) + + # Update image and update subscribers self.frame.put(header['frame'], force=True) self.image_shape.put(header['shape'], force=True) self.image.put(image, force=True) self._run_subs(sub_type=self.SUB_MONITOR, value=image) t_last=t_curr - logger.info(f"[{self.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}") - - # Exit loop and finish monitoring - if self._stop_polling: - logger.info(f"[{self.name}]\tDetaching monitor") - break + logger.info(f"[{self.name}] Updated frame {header['frame']}\tShape: {header['shape']}\tMean: {np.mean(image):.3f}") except ValueError: # Happens when ZMQ partially delivers the multipart message pass diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 97ed957..db1e4fc 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -14,6 +14,15 @@ from ophyd import Device, Signal, Component, Kind import requests +try: + from bec_lib import bec_logger + logger = bec_logger.logger +except ModuleNotFoundError: + import logging + logger = logging.getLogger("GfCam") + + + class StdDaqRestClient(Device): """Wrapper class around the new StdDaq REST interface. @@ -137,7 +146,7 @@ class StdDaqRestClient(Device): self.cfg_image_pixel_width.set(pixel_width).wait() self.write_daq_config() - + logger.info(f"[{self.name}] Reconfigured the StdDAQ") new = self.read_configuration() return old, new @@ -159,7 +168,7 @@ class StdDaqRestClient(Device): return super().unstage() - def stop(self): + def stop(self, success=False): """Stop op: Read the current configuration from the DAQ """ self.unstage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 4181927..97901b6 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -138,7 +138,7 @@ class StdDaqWsClient(Device): self.status.put(reply["status"], force=True) elif reply["status"] in ("rejected"): raise RuntimeError( - f"Start command rejected (might be already running): {reply['reason']}" + f"Start StdDAQ command rejected (might be already running): {reply['reason']}" ) self._mon = Thread(target=self.poll, daemon=True) @@ -154,7 +154,7 @@ class StdDaqWsClient(Device): _ = self.message(message, wait_reply=False) return super().unstage() - def stop(self): + def stop(self, success=False): """ Stop a running acquisition WARN: This will also close the connection!!!