diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 9989460..3a4bf14 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -124,35 +124,8 @@ gfclient: readoutPriority: monitored softwareTrigger: true -daq: - description: Standard DAQ controls - deviceClass: tomcat_bec.devices.gigafrost.stddaq_ws.StdDaqWsClient - deviceConfig: - ws_url: 'ws://xbl-daq-29:8080' - rest_url: 'http://xbl-daq-29:5000' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - -daqcfg: - description: Standard DAQ config - deviceClass: tomcat_bec.devices.gigafrost.stddaq_rest.StdDaqRestClient - deviceConfig: - rest_url: 'http://xbl-daq-29:5000' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - daq_stream0: - description: Standard DAQ preview stream 2 frames every 1000 + description: Standard DAQ preview stream 2 frames every 1000 image deviceClass: tomcat_bec.devices.gigafrost.stddaq_preview.StdDaqPreview deviceConfig: url: 'tcp://129.129.95.38:20000' diff --git a/tomcat_bec/devices/gigafrost/gfconstants.py b/tomcat_bec/devices/gigafrost/gfconstants.py index e0eab37..0577422 100644 --- a/tomcat_bec/devices/gigafrost/gfconstants.py +++ b/tomcat_bec/devices/gigafrost/gfconstants.py @@ -12,7 +12,7 @@ from enum import IntEnum gf_valid_enable_modes = ("soft", "external", "soft+ext", "always") gf_valid_exposure_modes = ("external", "timer", "soft") gf_valid_trigger_modes = ("auto", "external", "timer", "soft") -gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") +gf_valid_fix_nframe_modes = ("off", "start", "end", "start+end") # STATUS diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 40c51e5..637804a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -6,9 +6,8 @@ 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 import Signal, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus from ophyd.device import Staged from ophyd_devices.interfaces.base_classes.psi_detector_base import ( @@ -37,23 +36,24 @@ except ModuleNotFoundError: class GigaFrostCameraMixin(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. - """ + This class will be called by the custom_prepare_cls attribute of the + detector class. + """ def _define_backend_ip(self): if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_IP, const.BE999_SOUTH_IP - else: - raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") + + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _define_backend_mac(self): if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: + if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC - else: - raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized. {(const.GF1, const.GF2, const.GF3)}") + + raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _set_udp_header_table(self): """Set the communication parameters for the camera module""" @@ -123,10 +123,10 @@ class GigaFrostCameraMixin(CustomDetectorMixin): 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) return super().on_init() - + def on_stage(self) -> None: """Specify actions to be executed during stage @@ -372,7 +372,6 @@ class GigaFrostCamera(PSIDetectorBase): *, name, auto_soft_enable=False, - timeout=10, backend_url=const.BE999_DAFL_CLIENT, kind=None, read_attrs=None, @@ -450,6 +449,9 @@ class GigaFrostCamera(PSIDetectorBase): * 4: Invert pixel values, but do not apply any linearity correction * 5: Apply the full linearity correction """ + # Stop acquisition + self.unstage() + # If Bluesky style configure if d is not None: nimages = d.get('nimages', 10) @@ -464,20 +466,14 @@ class GigaFrostCamera(PSIDetectorBase): scanid = d.get('scanid', 0) correction_mode = d.get('correction_mode', 5) - # Stop acquisition - self.unstage() - self.cmdStartCamera.set(0).wait() - if self.autoSoftEnable.get(): - self.cmdSoftEnable.set(0).wait() - - # change settings - self.cfgExposure.set(exposure).wait() - self.cfgFramerate.set(period).wait() - self.cfgRoiX.set(pixel_width).wait() - self.cfgRoiY.set(pixel_height).wait() - self.cfgScanId.set(scanid).wait() - self.cfgCntNum.set(nimages).wait() - self.cfgCorrMode.set(correction_mode).wait() + # change settings + self.cfgExposure.set(exposure).wait() + self.cfgFramerate.set(period).wait() + self.cfgRoiX.set(pixel_width).wait() + self.cfgRoiY.set(pixel_height).wait() + self.cfgScanId.set(scanid).wait() + self.cfgCntNum.set(nimages).wait() + self.cfgCorrMode.set(correction_mode).wait() # Commit parameter self.cmdSetParam.set(1).wait() @@ -498,12 +494,12 @@ class GigaFrostCamera(PSIDetectorBase): mode_external = self.cfgTrigExpExt.get() if mode_soft and not mode_timer and not mode_external: return "soft" - elif not mode_soft and mode_timer and not mode_external: + if not mode_soft and mode_timer and not mode_external: return "timer" - elif not mode_soft and not mode_timer and mode_external: + if not mode_soft and not mode_timer and mode_external: return "external" - else: - return None + + return None @exposure_mode.setter def exposure_mode(self, exp_mode): @@ -514,11 +510,6 @@ class GigaFrostCamera(PSIDetectorBase): exp_mode : {'external', 'timer', 'soft'} The exposure mode to be set. """ - if exp_mode not in const.gf_valid_exposure_modes: - raise ValueError( - f"Invalid exposure mode! Valid modes are:\n" "{const.gf_valid_exposure_modes}" - ) - if exp_mode == "external": self.cfgTrigExpExt.set(1).wait() self.cfgTrigExpSoft.set(0).wait() @@ -531,6 +522,11 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgTrigExpExt.set(0).wait() self.cfgTrigExpSoft.set(1).wait() self.cfgTrigExpTimer.set(0).wait() + else: + raise ValueError( + f"Invalid exposure mode! Valid modes are:\n{const.gf_valid_exposure_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -548,14 +544,14 @@ class GigaFrostCamera(PSIDetectorBase): if not start_bit and not end_bit: return "off" - elif start_bit and not end_bit: + if start_bit and not end_bit: return "start" - elif not start_bit and end_bit: + if not start_bit and end_bit: return "end" - elif start_bit and end_bit: + if start_bit and end_bit: return "start+end" - else: - return None + + return None @fix_nframes_mode.setter def fix_nframes_mode(self, mode): @@ -566,11 +562,6 @@ class GigaFrostCamera(PSIDetectorBase): mode : {'off', 'start', 'end', 'start+end'} The fixed number of frames mode to be applied. """ - if mode not in const.gf_valid_fix_nframe_modes: - raise ValueError( - f"Invalid fixed number of frames mode! Valid modes are:\n{const.gf_valid_fix_nframe_modes}" - ) - self._fix_nframes_mode = mode if self._fix_nframes_mode == "off": self.cfgCntStartBit.set(0).wait() @@ -584,6 +575,11 @@ class GigaFrostCamera(PSIDetectorBase): elif self._fix_nframes_mode == "start+end": self.cfgCntStartBit.set(1).wait() self.cfgCntEndBit.set(1).wait() + else: + raise ValueError( + f"Invalid fixed number of frames mode! Valid modes are:\n{const.gf_valid_fix_nframe_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -603,14 +599,14 @@ class GigaFrostCamera(PSIDetectorBase): mode_soft = self.cfgTrigSoft.get() if mode_auto: return "auto" - elif mode_soft: + if mode_soft: return "soft" - elif mode_timer: + if mode_timer: return "timer" - elif mode_external: + if mode_external: return "external" - else: - return None + + return None @trigger_mode.setter def trigger_mode(self, mode): @@ -621,11 +617,6 @@ class GigaFrostCamera(PSIDetectorBase): mode : {'auto', 'external', 'timer', 'soft'} The GigaFRoST trigger mode. """ - if mode not in const.gf_valid_trigger_modes: - raise ValueError( - "Invalid trigger mode! Valid modes are:\n" "{const.gf_valid_trigger_modes}" - ) - if mode == "auto": self.cfgTrigAuto.set(1).wait() self.cfgTrigSoft.set(0).wait() @@ -646,6 +637,11 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgTrigSoft.set(1).wait() self.cfgTrigTimer.set(0).wait() self.cfgTrigExt.set(0).wait() + else: + raise ValueError( + "Invalid trigger mode! Valid modes are:\n{const.gf_valid_trigger_modes}" + ) + # Commit parameters self.cmdSetParam.set(1).wait() @@ -670,8 +666,8 @@ class GigaFrostCamera(PSIDetectorBase): return "always" elif mode_external and not mode_soft and not mode_auto: return "external" - else: - return None + + return None @enable_mode.setter def enable_mode(self, mode): @@ -684,7 +680,7 @@ class GigaFrostCamera(PSIDetectorBase): """ if mode not in const.gf_valid_enable_modes: raise ValueError( - "Invalid enable mode! Valid modes are:\n" "{const.gf_valid_enable_modes}" + "Invalid enable mode! Valid modes are:\n{const.gf_valid_enable_modes}" ) if mode == "soft": diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index aff02f2..4d5d93a 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- """ -GigaFrost camera class module +GigaFrost client module that combines camera and DAQ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -from time import sleep -from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Component, DeviceStatus from ophyd.device import Staged from ophyd_devices.interfaces.base_classes.psi_detector_base import ( @@ -17,87 +16,26 @@ from ophyd_devices.interfaces.base_classes.psi_detector_base import ( try: import gfconstants as const -except ModuleNotFoundError: - import tomcat_bec.devices.gigafrost.gfconstants as const - - -try: from StdDaqClient import StdDaqClient -except ModuleNotFoundError: - from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqWsClient - -try: from gigafrostcamera import GigaFrostCamera except ModuleNotFoundError: + import tomcat_bec.devices.gigafrost.gfconstants as const + from tomcat_bec.devices.gigafrost.stddaq_ws import StdDaqClient 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): """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. """ - - def _define_backend_ip(self): - if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_IP, const.BE3_SOUTH_IP - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_IP, const.BE999_SOUTH_IP - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _define_backend_mac(self): - if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 - return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC - elif self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: - return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC - else: - raise RuntimeError(f"Backend not recognized. {(const.GF1, const.GF2, const.GF3)}") - - def _set_udp_header_table(self): - """Set the communication parameters for the camera module""" - self.parent.cfgConnectionParam.set(self._build_udp_header_table()).wait() - - def _build_udp_header_table(self): - """Build the header table for the communication""" - udp_header_table = [] - - for i in range(0, 64, 1): - for j in range(0, 8, 1): - dest_port = 2000 + 8 * i + j - source_port = 3000 + j - if j < 4: - extend_header_table( - udp_header_table, self.parent.macSouth, self.parent.ipSouth, dest_port, source_port - ) - else: - extend_header_table( - udp_header_table, self.parent.macNorth, self.parent.ipNorth, dest_port, source_port - ) - - return udp_header_table - - def on_init(self) -> None: """Initialize the camera, set channel values on_init is automatically called during __init__ of the sub devices. """ return super().on_init() - - - + def on_stage(self) -> None: """ Specify actions to be executed during stage in preparation for a scan. @@ -180,7 +118,7 @@ class GigaFrostClient(PSIDetectorBase): USER_ACCESS = [""] cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") - daq = Component(StdDaqWsClient, name="daq") + daq = Component(StdDaqClient, name="daq") def __init__( self, @@ -215,11 +153,15 @@ class GigaFrostClient(PSIDetectorBase): - def configure(self, d: dict=None, **kwargs): - """Configure the next scan with the GigaFRoST camera + def configure(self, d: dict=None): + """Configure the next scan with the GigaFRoST camera and standard DAQ backend Parameters ---------- + ntotal : int, optional + Total mumber of images to be taken during the whole scan. Set to -1 + for an unlimited number of images (limited by the ringbuffer size and + backend speed). (default = 10000) nimages : int, optional Number of images to be taken during each scan. Set to -1 for an unlimited number of images (limited by the ringbuffer size and @@ -263,10 +205,10 @@ class GigaFrostClient(PSIDetectorBase): px_gf_h = self.cam.cfgRoiY.get() if px_daq_h != px_gf_h or px_daq_w != px_gf_w: - raise RuntimeError(f"Different image size configured on GF and the DAQ") - + raise RuntimeError("Different image size configured on GF and the DAQ") + return super().stage() - + def trigger(self) -> DeviceStatus: status = super().trigger() return status diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index d7fb4f0..0f27705 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ preview image stream module Created on Thu Jun 27 17:28:43 2024 @@ -8,10 +8,10 @@ Created on Thu Jun 27 17:28:43 2024 """ import json import enum -import zmq -import numpy as np 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, @@ -89,24 +89,6 @@ class StdDaqPreview(Device): sleep(1) self._socket.connect(self.url.get()) - 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 - preview data stream is too fast. Perhaps later we can add some online - processing to ophyd. - - Example: - ---------- - std.configure(throttle=0.2) - - Parameters - ---------- - throttle : float, optional - Additional throtling for the ophyd device. (default = 0.05 sec) - """ - self._throttle = throttle - def stage(self) -> list: """Start listening for preview data stream""" self.connect() @@ -120,7 +102,7 @@ class StdDaqPreview(Device): self._stop_polling = True return super().unstage() - def stop(self): + def stop(self, success=False): """Stop a running preview""" self.unstage() @@ -154,7 +136,10 @@ class StdDaqPreview(Device): 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}] Updated frame {header['frame']}\tShape: {header['shape']}\tMean: {np.mean(image):.3f}") + logger.info( + f"[{self.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 diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index db1e4fc..e4ff21d 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -1,15 +1,11 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ configuration interface module through the latrest REST API Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys -import json -from time import sleep -from threading import Thread from ophyd import Device, Signal, Component, Kind import requests @@ -62,7 +58,11 @@ class StdDaqRestClient(Device): def read_daq_config(self): """Read the current configuration from the JSON file """ - r = requests.get(self._url_base + '/api/config/get', params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}) + r = requests.get( + self._url_base + '/api/config/get', + params={'config_file': "/etc/std_daq/configs/gf1.json", 'user':"ioc"}, + timeout=2 + ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") @@ -111,15 +111,20 @@ class StdDaqRestClient(Device): raise RuntimeError("Pleae read config before editing") config = self._build_config() - + params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} - r = requests.post(self._url_base +'/api/config/set', params=params, json=config, headers={"Content-Type": "application/json"}) + r = requests.post( + self._url_base +'/api/config/set', + params=params, + json=config, + timeout=2, + headers={"Content-Type": "application/json"} + ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") return r - def configure(self, d: dict=None): """Configure the next scan with the GigaFRoST camera @@ -134,7 +139,7 @@ class StdDaqRestClient(Device): # If Bluesky style configure if d is not None: - # Only reconfigure if we're instructed + # Only reconfigure if we're instructed if ('pixel_width' in d) or ('pixel_height' in d) or ('image_width' in d) or ('image_height' in d): pixel_width = d.get('pixel_width', 2016) pixel_height = d.get('pixel_height', 2016) diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 97901b6..d5806dd 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -1,12 +1,11 @@ # -*- coding: utf-8 -*- """ -Standard DAQ class module +Standard DAQ control interface module through the websocket API Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import sys import json from time import sleep from threading import Thread @@ -50,7 +49,12 @@ class StdDaqWsClient(Device): cfg = Component(StdDaqRestClient, kind=Kind.config) def __init__( - self, *args, ws_url: str = "ws://localhost:8080", rest_url="http://localhost:5000", parent: Device = None, **kwargs + self, + *args, + ws_url: str = "ws://localhost:8080", + rest_url="http://localhost:5000", + parent: Device = None, + **kwargs ) -> None: self.__class__.__dict__['cfg'].kwargs['rest_url'] = rest_url @@ -60,6 +64,7 @@ class StdDaqWsClient(Device): self._mon = None # Connect ro the DAQ + self._client = None self.connect() def connect(self): @@ -84,7 +89,7 @@ class StdDaqWsClient(Device): self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - def configure(self, d: dict={}) -> tuple: + def configure(self, d: dict=None) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -197,7 +202,7 @@ class StdDaqWsClient(Device): try: message = json.loads(msg) self.status.put(message["status"], force=True) - except (ConnectionClosedError, ConnectionClosedOK) as ex: + except (ConnectionClosedError, ConnectionClosedOK): return except Exception as ex: print(ex) @@ -208,7 +213,6 @@ class StdDaqWsClient(Device): class StdDaqClient(StdDaqWsClient): """Just an alias""" - pass # Automatically connect to MicroSAXS testbench if directly invoked