From 0c8c60cfe3a33fac7bf1fd34a0e417518983535f Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 29 Aug 2024 16:34:23 +0200 Subject: [PATCH] Seems to be working configuration --- .../device_configs/microxas_test_bed.yaml | 37 +++------ tomcat_bec/devices/__init__.py | 2 +- .../devices/gigafrost/gigafrostcamera.py | 8 +- .../devices/gigafrost/gigafrostclient.py | 3 +- .../devices/gigafrost/stddaq_preview.py | 42 +++++++--- tomcat_bec/devices/gigafrost/stddaq_rest.py | 24 ++++-- tomcat_bec/devices/gigafrost/stddaq_ws.py | 19 +++-- tomcat_bec/devices/psimotor.py | 83 +++++++------------ tomcat_bec/scans/gigafrost_test.py | 19 +++-- 9 files changed, 120 insertions(+), 117 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 7f305b0..40dcc9a 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -1,7 +1,7 @@ eyex: readoutPriority: baseline - description: X-ray eye translation x - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis X + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M1 deviceTags: @@ -12,8 +12,8 @@ eyex: softwareTrigger: false eyey: readoutPriority: baseline - description: X-ray eye translation y - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis Y + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M2 deviceTags: @@ -24,8 +24,8 @@ eyey: softwareTrigger: false eyez: readoutPriority: baseline - description: X-ray eye translation z - deviceClass: ophyd.EpicsMotor + description: X-ray eye axis Z + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC deviceConfig: prefix: MTEST-X05LA-ES2-XRAYEYE:M3 deviceTags: @@ -93,7 +93,7 @@ femto_mean_curr: # softwareTrigger: true gfclient: - description: GigaFrost camera controls + description: GigaFrost camera client deviceClass: tomcat_bec.devices.GigaFrostClient deviceConfig: prefix: 'X02DA-CAM-GF2:' @@ -110,8 +110,8 @@ gfclient: softwareTrigger: true daq_stream0: - description: Standard DAQ preview stream 2 frames every 1000 image - deviceClass: tomcat_bec.devices.StdDaqPreview + description: stdDAQ preview (2 every 555) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector deviceConfig: url: 'tcp://129.129.95.38:20002' deviceTags: @@ -123,8 +123,8 @@ daq_stream0: softwareTrigger: false daq_stream1: - description: Standard DAQ preview stream 4 frames at 10 Hz - deviceClass: tomcat_bec.devices.StdDaqPreview + description: stdDAQ preview (4 at 10 Hz) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector deviceConfig: url: 'tcp://129.129.95.38:20001' deviceTags: @@ -134,18 +134,3 @@ daq_stream1: readOnly: false readoutPriority: monitored softwareTrigger: false - -daq_stream2: - description: Standard DAQ preview stream from first server - deviceClass: tomcat_bec.devices.StdDaqPreview - deviceConfig: - url: 'tcp://129.129.95.40:20001' - deviceTags: - - std-daq - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false - - diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index 03ee446..ba25c64 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -12,4 +12,4 @@ from .grashopper_tomcat import GrashopperTOMCAT from .psimotor import EpicsMotorMR, EpicsMotorEC from .gigafrost.gigafrostclient import GigaFrostClient -from .gigafrost.stddaq_preview import StdDaqPreview +from .gigafrost.stddaq_preview import StdDaqPreview, StdDaqPreviewDetector diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index aec94a0..cb6fc88 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -149,8 +149,14 @@ class GigaFrostCameraMixin(CustomDetectorMixin): It must be safe to assume that the device is ready for the scan to start immediately once this function is finished. """ + # Either an acquisition is running or it's already done if self.parent.infoBusyFlag.value: - raise RuntimeError("Camera is already busy, unstage it first!") + logger.warn("Camera is already busy, unstage it first!") + self.parent.unstage() + sleep(0.5) + # Sync if out of sync + if self.parent.infoSyncFlag.value == 0: + self.parent.cmdSyncHw.set(1).wait() # Switch to acquiring self.parent.cmdStartCamera.set(1).wait() self.parent.state.put(const.GfStatus.ACQUIRING, force=True) diff --git a/tomcat_bec/devices/gigafrost/gigafrostclient.py b/tomcat_bec/devices/gigafrost/gigafrostclient.py index 3f2d3ce..08ad971 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostclient.py +++ b/tomcat_bec/devices/gigafrost/gigafrostclient.py @@ -42,7 +42,8 @@ class GigaFrostClientMixin(CustomDetectorMixin): to start immediately once this function is finished. """ # Gigafrost can finish a run without explicit unstaging - self.parent._staged = Staged.no + if self.parent._staged: + self.parent.unstage() #self.parent.daq.stage() #self.parent.cam.stage() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index a87d381..720ea20 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -167,8 +167,13 @@ class StdDaqPreviewMixin(CustomDetectorMixin): 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) + self.parent.connect() self._stop_polling = False self._mon = Thread(target=self.poll, daemon=True) @@ -176,8 +181,13 @@ class StdDaqPreviewMixin(CustomDetectorMixin): def on_unstage(self): """Stop a running preview""" - self._stop_polling = True - + 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 + self.parent._socket.disconnect() + def on_stop(self): """Stop a running preview""" self.on_unstage() @@ -191,27 +201,39 @@ class StdDaqPreviewMixin(CustomDetectorMixin): try: # Exit loop and finish monitoring if self._stop_polling: + logger.info(f"[{self.parent.name}]\tDetaching monitor") break # pylint: disable=no-member - meta, data = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) - header = json.loads(meta) - if header["type"]=="uint16": - image = np.frombuffer(data, dtype=np.uint16) - image = image.reshape(header['shape']) + r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK) + if len(r)==2: + meta, data = r + else: + sleep(0.1) + continue # Update image and update subscribers t_curr = time() t_elapsed = t_curr - t_last if t_elapsed > self.parent.throttle.get(): + header = json.loads(meta) + if header["type"]=="uint16": + image = np.frombuffer(data, dtype=np.uint16) + 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.frame.put(header['frame'], force=True) self.parent.image_shape.put(header['shape'], force=True) self.parent.image.put(image, force=True) self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) t_last=t_curr - name = self.parent.name - nfo = f"[{name}]\tFrameNo: {header['frame']}\tMean: {np.mean(image)}" - logger.info(nfo) + 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 diff --git a/tomcat_bec/devices/gigafrost/stddaq_rest.py b/tomcat_bec/devices/gigafrost/stddaq_rest.py index f4f0827..822df51 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_rest.py +++ b/tomcat_bec/devices/gigafrost/stddaq_rest.py @@ -70,8 +70,8 @@ class StdDaqRestClient(Device): except Exception as ex: logger.error(f"Failed to connect to the StdDAQ REST API\n{ex}") - def read_daq_config(self) -> dict: - """Read the current configuration from the JSON file + def get_daq_config(self) -> dict: + """Read the current configuration from the DAQ """ r = requests.get( self.rest_url.get() + '/api/config/get', @@ -80,8 +80,12 @@ class StdDaqRestClient(Device): ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") + return r.json() - cfg = r.json() + def read_daq_config(self) -> None: + """Extract the current configuration from the JSON file + """ + cfg = self.get_daq_config() self.cfg_detector_name.set(cfg['detector_name']).wait() self.cfg_detector_type.set(cfg['detector_type']).wait() @@ -98,9 +102,8 @@ class StdDaqRestClient(Device): #self.cfg_module_positions.set(cfg['module_positions']).wait() self._config_read = True - return r - def _build_config(self) -> dict: + def _build_config(self, orig) -> dict: config = { 'detector_name': str(self.cfg_detector_name.get()), 'detector_type': str(self.cfg_detector_type.get()), @@ -110,13 +113,15 @@ class StdDaqRestClient(Device): 'image_pixel_width': int(self.cfg_pixel_width.get()), 'start_udp_port': int(self.cfg_start_udp_port.get()), 'writer_user_id': int(self.cfg_writer_user_id.get()), - 'log_level': "debug", + 'log_level': "info", 'submodule_info': {}, 'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders.get()), 'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()), 'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()), - 'module_positions': {} + 'module_positions': {}, + 'number_of_writers': 14 } + config = orig.update(config) return config def write_daq_config(self): @@ -126,7 +131,8 @@ class StdDaqRestClient(Device): if not self._config_read: raise RuntimeError("Pleae read config before editing") - config = self._build_config() + orig = self.get_daq_config() + config = self._build_config(orig) #params = {"user": "ioc", "config_file": "/etc/std_daq/configs/gf1.json"} params = {"user": "ioc"} @@ -153,7 +159,7 @@ class StdDaqRestClient(Device): """ # Reads the current config old = self.read_configuration() - + self.read_daq_config() # If Bluesky style configure if d is not None: # Only reconfigure if we're instructed diff --git a/tomcat_bec/devices/gigafrost/stddaq_ws.py b/tomcat_bec/devices/gigafrost/stddaq_ws.py index 62ad95c..415c3a4 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_ws.py +++ b/tomcat_bec/devices/gigafrost/stddaq_ws.py @@ -144,19 +144,24 @@ class StdDaqClient(Device): by calling unstage. So it might start from an already running state or not, we can't query if not running. """ + if self._staged: + self.unstage() + self._client.close() + file_path = self.file_path.get() n_total = self.n_total.get() message = {"command": "start", "path": file_path, "n_image": n_total} reply = self.message(message) - reply = json.loads(reply) - if reply["status"] in ("creating_file"): - self.status.put(reply["status"], force=True) - elif reply["status"] in ("rejected"): - raise RuntimeError( - f"Start StdDAQ command rejected (might be already running): {reply['reason']}" - ) + if reply is not None: + reply = json.loads(reply) + if reply["status"] in ("creating_file"): + self.status.put(reply["status"], force=True) + elif reply["status"] in ("rejected"): + raise RuntimeError( + f"Start StdDAQ command rejected (might be already running): {reply['reason']}" + ) self._mon = Thread(target=self.poll, daemon=True) self._mon.start() diff --git a/tomcat_bec/devices/psimotor.py b/tomcat_bec/devices/psimotor.py index f06a7be..da02287 100644 --- a/tomcat_bec/devices/psimotor.py +++ b/tomcat_bec/devices/psimotor.py @@ -9,7 +9,9 @@ detailed interface for motors using the new ECMC-based motion systems at PSI. import warnings from ophyd import Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import MoveStatus +from ophyd.status import DeviceStatus, MoveStatus +from ophyd.utils.errors import UnknownStatusFailure +from ophyd.utils.epics_pvs import AlarmSeverity class SpmgStates: @@ -21,6 +23,7 @@ class SpmgStates: GO = 3 + class EpicsMotorMR(EpicsMotor): """ Extended EPICS Motor class @@ -28,39 +31,16 @@ class EpicsMotorMR(EpicsMotor): It extends EpicsMotor base class to provide some simple status checks before movement. """ + tolerated_alarm = AlarmSeverity.INVALID - SUB_PROGRESS = "progress" motor_deadband = Component( EpicsSignalRO, ".RDBD", auto_monitor=True, kind=Kind.config) motor_mode = Component( EpicsSignal, ".SPMG", auto_monitor=True, put_complete=True, kind=Kind.omitted) - - _start_position = None - _target_position = None - - # pylint: disable=too-many-arguments - def __init__( - self, - prefix="", - *, - name, - kind=None, - read_attrs=None, - configuration_attrs=None, - parent=None, - **kwargs, - ): - super().__init__( - prefix=prefix, - name=name, - kind=kind, - read_attrs=read_attrs, - configuration_attrs=configuration_attrs, - parent=parent, - **kwargs, - ) - - self.subscribe(self._progress_update, run=False) + motor_status = Component( + EpicsSignal, ".STAT", auto_monitor=True, kind=Kind.omitted) + motor_enable = Component( + EpicsSignal, ".CNEN", auto_monitor=True, kind=Kind.omitted) def move(self, position, wait=True, **kwargs) -> MoveStatus: """ Extended move function with a few sanity checks @@ -70,29 +50,25 @@ class EpicsMotorMR(EpicsMotor): # Reset SPMG before move spmg = self.motor_mode.get() if spmg != SpmgStates.GO: - self.motor_mode.put(SpmgStates.GO).wait() - #Warni if trying to move beyond an active limit - if self.high_limit_switch and position > self.position: - warnings.warn("Attempting to move above active HLS", RuntimeWarning) - if self.low_limit_switch and position < self.position: - warnings.warn("Attempting to move below active LLS", RuntimeWarning) + self.motor_mode.set(SpmgStates.GO).wait() + # Warn if EPIC motorRecord claims an error + status = self.motor_status.get() + if status: + warnings.warn(f"EPICS MotorRecord is in alarm state {status}, ophyd will raise", RuntimeWarning) + # Warni if trying to move beyond an active limit + # if self.high_limit_switch and position > self.position: + # warnings.warn("Attempting to move above active HLS", RuntimeWarning) + # if self.low_limit_switch and position < self.position: + # warnings.warn("Attempting to move below active LLS", RuntimeWarning) - self._start_position = self.position - self._target_position = position + try: + status = super().move(position, wait, **kwargs) + return status + except UnknownStatusFailure: + status = DeviceStatus(self) + status.set_finished() + return status - return super().move(position, wait, **kwargs) - - def _progress_update(self, value, **kwargs) -> None: - """Progress update on the current movement""" - if (self._start_position is None) or (self._target_position is None) or (not self.moving): - self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) - return - - progress = abs( - (value - self._start_position) / (self._target_position - self._start_position) - ) - self._run_subs( - sub_type=self.SUB_PROGRESS, value=progress, max_value=1, done=self.moving) class EpicsMotorEC(EpicsMotorMR): @@ -101,7 +77,6 @@ class EpicsMotorEC(EpicsMotorMR): Special motor class to provide additional functionality for ECMC based motors. It exposes additional diagnostic fields and includes basic error management. """ - USER_ACCESS = ['reset'] enable_readback = Component(EpicsSignalRO, "-EnaAct", auto_monitor=True, kind=Kind.normal) enable = Component( @@ -113,7 +88,7 @@ class EpicsMotorEC(EpicsMotorMR): #high_interlock = Component(EpicsSignalRO, "-SumIlockFwd", auto_monitor=True, kind=Kind.normal) #low_interlock = Component(EpicsSignalRO, "-SumIlockBwd", auto_monitor=True, kind=Kind.normal) - ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal) + #ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal) error = Component(EpicsSignalRO, "-ErrId", auto_monitor=True, kind=Kind.normal) error_msg = Component(EpicsSignalRO, "-MsgTxt", auto_monitor=True, kind=Kind.normal) error_reset = Component(EpicsSignal, "-ErrRst", put_complete=True, kind=Kind.omitted) @@ -123,7 +98,7 @@ class EpicsMotorEC(EpicsMotorMR): Note that the default EpicsMotor only supports the 'GO' movement mode. """ - # Reset SPMG before move + # Check ECMC error status before move error = self.error.get() if error: raise RuntimeError(f"Motor is in error state with message: '{self.error_msg.get()}'") @@ -146,4 +121,4 @@ class EpicsMotorEC(EpicsMotorMR): self.error_reset.set(1, settle_time=0.1).wait() # Check if it disappeared if self.error.get(): - raise RuntimeError(f"Failed to reset axis error: '{self.error_msg.get()}'") + raise RuntimeError(f"Failed to reset axis, error still present: '{self.error_msg.get()}'") diff --git a/tomcat_bec/scans/gigafrost_test.py b/tomcat_bec/scans/gigafrost_test.py index e817d14..e9a7b6c 100644 --- a/tomcat_bec/scans/gigafrost_test.py +++ b/tomcat_bec/scans/gigafrost_test.py @@ -1,5 +1,4 @@ import time - import numpy as np from bec_lib import bec_logger @@ -87,12 +86,16 @@ class GigaFrostStepScan(AsyncFlyScanBase): yield from self.stubs.pre_scan() def stage(self): - yield from self.stubs.send_rpc_and_wait( - "gf2", "configure", {"nimages": self.scan_exp_b, "exposure": self.scan_exp_t, "period": self.scan_exp_p, "roix": 480, "roiy": 128} - ) - yield from self.stubs.send_rpc_and_wait( - "daq", "configure", {"n_images": self.scan_steps * self.scan_exp_b} - ) + d= { + "ntotal": self.scan_steps * self.scan_exp_b, + "nimages": self.scan_exp_b, + "exposure": self.scan_exp_t, + "period": self.scan_exp_p, + "pixel_width": 480, + "pixel_height": 128 + } + yield from self.stubs.send_rpc_and_wait("gfclient", "configure", d) + # For god, NO! yield from super().stage() def scan_core(self): @@ -101,7 +104,7 @@ class GigaFrostStepScan(AsyncFlyScanBase): print(f"Point: {ii}") st = yield from self.stubs.send_rpc_and_wait(self.scan_motors[0], "move", self.positions[ii]) st.wait() - st = yield from self.stubs.send_rpc_and_wait("gf2", "trigger") + st = yield from self.stubs.send_rpc_and_wait("gfclient", "trigger") st.wait() self.pointID += 1 time.sleep(0.2)