From 51cfae82c4e0a0cca509e7bb0fd6353d4093b4bb Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 26 Jul 2024 12:17:50 +0200 Subject: [PATCH] GFclient instantiates but camera is not yet running --- .../devices/gigafrost/gigafrostcamera.py | 32 ++++---- .../devices/gigafrost/gigafrostclient.py | 33 +++++---- tomcat_bec/devices/gigafrost/stddaq_rest.py | 48 ++++++++---- tomcat_bec/devices/gigafrost/stddaq_ws.py | 74 ++++++++++++------- 4 files changed, 120 insertions(+), 67 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 9f5e5ee..78adc5f 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -223,15 +223,15 @@ class GigaFrostCamera(PSIDetectorBase): infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) infoSyncFlag = Component(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True) - cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True) - cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True) - cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True) + cmdSyncHw = Component(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True, kind=Kind.omitted) + cmdStartCamera = Component(EpicsSignal, "START_CAM", put_complete=True, kind=Kind.omitted) + cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted) # UDP header cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) cfgUdpHtOffset = Component(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config) - cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True) + cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted) # Standard camera configs cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) @@ -244,7 +244,7 @@ class GigaFrostCamera(PSIDetectorBase): # Software signals cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) # Trigger configuration PVs @@ -349,15 +349,15 @@ class GigaFrostCamera(PSIDetectorBase): ) # HW settings as read only - cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True) - cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True) - cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True) - cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True) - cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True) - cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True) - cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True) - cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True) - cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True) + cfgSyncFlag = Component(EpicsSignalRO, "PIXRATE", auto_monitor=True, kind=Kind.config) + cfgTrigDelay = Component(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True, kind=Kind.config) + cfgSyncoutDelay = Component(EpicsSignalRO, "SYNCOUT_DLY", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity0 = Component(EpicsSignalRO, "BNC0_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity1 = Component(EpicsSignalRO, "BNC1_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity2 = Component(EpicsSignalRO, "BNC2_RBV", auto_monitor=True, kind=Kind.config) + cfgOutputPolarity3 = Component(EpicsSignalRO, "BNC3_RBV", auto_monitor=True, kind=Kind.config) + cfgInputPolarity1 = Component(EpicsSignalRO, "BNC4_RBV", auto_monitor=True, kind=Kind.config) + cfgInputPolarity2 = Component(EpicsSignalRO, "BNC5_RBV", auto_monitor=True, kind=Kind.config) infoBoardTemp = Component(EpicsSignalRO, "T_BOARD", auto_monitor=True) USER_ACCESS = ["exposure_mode", "fix_nframes_mode", "trigger_mode", "enable_mode"] @@ -413,6 +413,7 @@ class GigaFrostCamera(PSIDetectorBase): def configure( self, + d: dict=None, nimages=10, exposure=0.2, period=1.0, @@ -452,8 +453,7 @@ class GigaFrostCamera(PSIDetectorBase): * 5: Apply the full linearity correction """ # If Bluesky style configure - if isinstance(nimages, dict): - d = nimages.copy() + if d is not None: nimages = d.get('nimages', 10) exposure = d.get('exposure', exposure) period = d.get('period', period) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index cced95a..c835af9 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -217,6 +217,13 @@ class GigaFrostClient(PSIDetectorBase): Backend url address necessary to set up the camera's udp header. (default: http://xbl-daq-23:8080) + Usage: + ---------- + gf = GigaFrostClient( + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True, + daq_ws_url="ws://xbl-daq-29:8080", daq_rest_url="http://xbl-daq-29:5000" + ) + Bugs: ---------- FRAMERATE : Ignored in soft trigger mode, period becomes 2xexposure time @@ -224,7 +231,7 @@ class GigaFrostClient(PSIDetectorBase): # pylint: disable=too-many-instance-attributes cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam") - #daq = Component(StdDaqWsClient, "") + daq = Component(StdDaqWsClient, name="daq") def __init__( self, @@ -233,20 +240,19 @@ class GigaFrostClient(PSIDetectorBase): name, auto_soft_enable=False, backend_url=const.BE999_DAFL_CLIENT, + daq_ws_url = "ws://localhost:8080", + daq_rest_url = "http://localhost:5000", kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs, ): - - self.__class__.__dict__["cam"].kwargs.update({'backend_url':"http://xbl-daq-28:8080",}) - self.__class__.__dict__["cam"].kwargs.update({'auto_soft_enable': True}) - - - - - + self.__class__.__dict__["cam"].kwargs['backend_url'] = backend_url + self.__class__.__dict__["cam"].kwargs['auto_soft_enable'] = auto_soft_enable + self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url + self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url + #self.__class__.__dict__["daq"].__class__.__dict__["cfg"].kwargs['rest_url'] = daq_rest_url super().__init__( prefix=prefix, @@ -299,11 +305,11 @@ class GigaFrostClient(PSIDetectorBase): return old, new def stage(self): - px_daq_h = self.daq.config.cfg_image_pixel_height.get() - px_daq_w = self.daq.config.cfg_image_pixel_width.get() + px_daq_h = self.daq.cfg.cfg_image_pixel_height.get() + px_daq_w = self.daq.cfg.cfg_image_pixel_width.get() px_gf_h = self.cam.cfgRoiX.get() - px_gf_y = self.cam.cfgRoiY.get() + px_gf_w = 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") @@ -315,6 +321,7 @@ class GigaFrostClient(PSIDetectorBase): # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": gf = GigaFrostClient( - "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True + "X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True, + daq_ws_url="ws://xbl-daq-29:8080", daq_rest_url="http://xbl-daq-29:5000" ) gf.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index 8560056..374b860 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -14,12 +14,12 @@ from ophyd import Device, Signal, Component, Kind import requests -class StdDaqRestConfig(Device): +class StdDaqRestClient(Device): """Wrapper class around the new StdDaq REST interface. - This was meant to replace the websocket inteface that replaced the - documented python client. We can finally read configuration through - standard HTTP requests, although the secondary server is ot reachable + This was meant to replace or extend the websocket inteface that replaced + the documented python client. We can finally read configuration through + standard HTTP requests, although the secondary server is not reachable at the time. """ # pylint: disable=too-many-instance-attributes @@ -39,12 +39,13 @@ class StdDaqRestConfig(Device): cfg_module_sync_queue_size = Component(Signal, kind=Kind.config) cfg_module_positions = Component(Signal, kind=Kind.config) + _config_read = False def __init__( - self, *args, url: str = "http://xbl-daq-29:5000", parent: Device = None, **kwargs + self, *args, rest_url: str = "http://localhost:5000", parent: Device = None, **kwargs ) -> None: super().__init__(*args, parent=parent, **kwargs) - self._url_base = url + self._url_base = rest_url # Connect ro the DAQ and initialize values self.read_daq_config() @@ -57,12 +58,31 @@ class StdDaqRestConfig(Device): raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") cfg = r.json() - for key, val in cfg.items(): - if isinstance(val, (int, float, str)): - getattr(self, "cfg_"+key).set(val).wait() + self.cfg_detector_name.set(cfg['detector_name']).wait() + self.cfg_detector_type.set(cfg['detector_type']).wait() + + self.cfg_n_modules.set(cfg['n_modules']).wait() + self.cfg_bit_depth.set(cfg['bit_depth']).wait() + self.cfg_image_pixel_height.set(cfg['image_pixel_height']).wait() + self.cfg_image_pixel_width.set(cfg['image_pixel_width']).wait() + self.cfg_start_udp_port.set(cfg['start_udp_port']).wait() + self.cfg_writer_user_id.set(cfg['writer_user_id']).wait() + self.cfg_submodule_info.set(cfg['submodule_info']).wait() + self.cfg_max_number_of_forwarders_spawned.set(cfg['max_number_of_forwarders_spawned']).wait() + self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait() + self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait() + self.cfg_module_positions.set(cfg['module_positions']).wait() + + self._config_read = True return cfg def write_daq_config(self): + """Write configuration ased on current PV values. Some fields might be + unchangeable. + """ + if not self._config_read: + raise RuntimeError("Pleae read config before editing") + config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), @@ -85,28 +105,30 @@ class StdDaqRestConfig(Device): if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + def read(self): + self.read_daq_config() def stage(self) -> list: - """Read the current configuration from the DAQ + """Stage op: Read the current configuration from the DAQ """ self.read_daq_config() return super().stage() def unstage(self): - """Read the current configuration from the DAQ + """Unstage op: Read the current configuration from the DAQ """ self.read_daq_config() return super().unstage() def stop(self): - """Read the current configuration from the DAQ + """Stop op: Read the current configuration from the DAQ """ self.unstage() # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daqcfg = StdDaqRestConfig(name="daqcfg", url="http://xbl-daq-29:5000") + daqcfg = StdDaqRestClient(name="daqcfg", rest_url="http://xbl-daq-29:5000") daqcfg.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 15db947..cd04be8 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -14,33 +14,49 @@ from ophyd import Device, Signal, Component, Kind from websockets.sync.client import connect from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError +try: + from stddaq_rest import StdDaqRestClient +except ModuleNotFoundError: + from tomcat_bec.devices.gigafrost.stddaq_rest import StdDaqRestClient + + + class StdDaqWsClient(Device): - """Wrapper class around the StdDaq websocket interface. + """StdDaq API - This was meant to replace the documented python client. We cannot read - or change the current configuration through this interface. - - A bit more about the Standard DAQ configuration: + This class combines the new websocket and REST interfaces that were meant + to replace the documented python client. The websocket interface starts + and stops the acquisition and provides status, while the REST interface + can read and write the configuration. The standard DAQ configuration is a single JSON file locally autodeployed - to the DAQ servers (as root!!!). Previously there was a service to offer - a REST API to write this file, but since there's no frontend group, this - is no longer available. + to the DAQ servers (as root!!!). It can only be written through a primary + REST API that is semi-supported, as there's no frontend group. The DAQ + might be distributed across several servers, meaning that the primary REST + interface will try to synchronize with secondary REST servers, but this + might fail, yielding a flawed configuration. + + daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + """ # pylint: disable=too-many-instance-attributes # Status attributes - status = Component(Signal, value="unknown", kind=Kind.hinted) - n_images = Component(Signal, value=10000, kind=Kind.config) + status = Component(Signal, value="unknown", kind=Kind.normal) + n_total = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) + cfg = Component(StdDaqRestClient, kind=Kind.config) + def __init__( - self, *args, url: str = "ws://localhost:8080", 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 + super().__init__(*args, parent=parent, **kwargs) self.status._metadata["write_access"] = False - self._ws_url = url + self._ws_url = ws_url self._mon = None # Connect ro the DAQ @@ -58,13 +74,17 @@ class StdDaqWsClient(Device): sleep(5) self._client = connect(self._ws_url) + def __del__(self): + """Try to close the socket""" + self._client.close_socket() + def monitor(self): """Attach monitoring to the DAQ""" self._client = connect(self._ws_url) self._mon = Thread(target=self.poll, daemon=True) self._mon.start() - def configure(self, n_images: int = None, file_path: str = None) -> tuple: + def configure(self, d: dict=None, n_total: int = None, file_path: str = None) -> tuple: """Set the standard DAQ parameters for the next run Note that full reconfiguration is not possible with the websocket @@ -73,13 +93,13 @@ class StdDaqWsClient(Device): Example: ---------- - std.configure(n_images=10000, file_path="/data/test/raw") + std.configure(n_total=10000, file_path="/data/test/raw") Parameters ---------- - n_images : 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 + n_total : int, optional + Total number of images to be taken during each scan. Set to -1 for + an unlimited number of images (limited by the ringbuffer size and backend speed). (default = 10000) file_path : string, optional Save file path. (default = '/gpfs/test/test-beamline') @@ -87,13 +107,12 @@ class StdDaqWsClient(Device): """ old_config = self.read_configuration() # If Bluesky style configure - if isinstance(n_images, dict): - d = n_images.copy() - n_images = d.get('n_images', None) + if d is not None: + n_total = d.get('n_total', None) file_path = d.get('file_path', None) - if n_images is not None: - self.n_images.set(int(n_images)) + if n_total is not None: + self.n_total.set(int(n_total)) if file_path is not None: self.output_file.set(str(file_path)) @@ -108,9 +127,9 @@ class StdDaqWsClient(Device): not, we can't query if not running. """ file_path = self.file_path.get() - n_image = self.n_images.get() + n_total = self.n_total.get() - message = {"command": "start", "path": file_path, "n_image": n_image} + message = {"command": "start", "path": file_path, "n_image": n_total} reply = self.message(message) reply = json.loads(reply) @@ -186,7 +205,12 @@ class StdDaqWsClient(Device): self._mon = None +class StdDaqClient(StdDaqWsClient): + """Just an alias""" + pass + + # Automatically connect to MicroSAXS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqWsClient(name="daq", url="ws://xbl-daq-29:8080") + daq = StdDaqWsClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") daq.wait_for_connection()