From 09c3e395de258f393e4e228a774c35c45946c5a9 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 4 Sep 2025 17:57:48 +0200 Subject: [PATCH] refactor(pilatus): update config, add live mode --- debye_bec/device_configs/x01da_pilatus.yaml | 31 +++++++- debye_bec/devices/pilatus/pilatus.py | 83 +++++++++++---------- 2 files changed, 71 insertions(+), 43 deletions(-) diff --git a/debye_bec/device_configs/x01da_pilatus.yaml b/debye_bec/device_configs/x01da_pilatus.yaml index 261cfa9..33986f3 100644 --- a/debye_bec/device_configs/x01da_pilatus.yaml +++ b/debye_bec/device_configs/x01da_pilatus.yaml @@ -1,9 +1,34 @@ - pilatus: +pilatus: readoutPriority: async description: Pilatus - deviceClass: debye_bec.devices.pilatus.PilatusDetector + deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus + deviceTags: + - detector deviceConfig: prefix: "X01DA-ES2-PIL:" onFailure: retry enabled: true - softwareTrigger: false \ No newline at end of file + softwareTrigger: true +samx: + readoutPriority: baseline + deviceClass: ophyd_devices.SimPositioner + deviceConfig: + delay: 1 + limits: + - -50 + - 50 + tolerance: 0.01 + update_frequency: 400 + deviceTags: + - user motors + enabled: true + readOnly: false +bpm4i: + readoutPriority: monitored + deviceClass: ophyd_devices.SimMonitor + deviceConfig: + deviceTags: + - beamline + enabled: true + readOnly: false + diff --git a/debye_bec/devices/pilatus/pilatus.py b/debye_bec/devices/pilatus/pilatus.py index c2fbad9..e287da7 100644 --- a/debye_bec/devices/pilatus/pilatus.py +++ b/debye_bec/devices/pilatus/pilatus.py @@ -15,6 +15,7 @@ from ophyd import Component as Cpt from ophyd import EpicsSignal, Kind from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin +from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin from ophyd.status import WaitTimeoutError from ophyd_devices import ( AndStatusWithList, @@ -23,9 +24,7 @@ from ophyd_devices import ( FileEventSignal, PreviewSignal, ) -from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35 as ImagePlugin from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase -from ophyd_devices.utils.psi_device_base_utils import TaskStatus if TYPE_CHECKING: # pragma: no cover from bec_lib.devicemanager import ScanInfo @@ -171,17 +170,19 @@ class Pilatus(PSIDeviceBase, ADBase): self.device_manager = device_manager self._readout_time = PILATUS_READOUT_TIME self._full_path = "" + self._poll_thread = threading.Thread( + target=self._poll_array_data, daemon=True, name=f"{self.name}_poll_thread" + ) self._poll_thread_stop_event = threading.Event() - self._task_status: TaskStatus | None = None - self._poll_rate = 1 # 1Hz + self._poll_rate = 1 # Poll rate in Hz ######################################## # Custom Beamline Methods # ######################################## def _poll_array_data(self): + """Poll the array data for preview updates.""" while not self._poll_thread_stop_event.wait(1 / self._poll_rate): - logger.debug("Polling Pilatus array data for preview...") try: value = self.image1.array_data.get() if value is None: @@ -191,14 +192,16 @@ class Pilatus(PSIDeviceBase, ADBase): # Geometry correction for the image data = np.reshape(value, (height, width)) last_image: DevicePreviewMessage = self.preview.get() - if last_image is None: - return - elif np.array_equal(data, last_image.data): - # No update if image is the same, ~2.5ms on 2400x2400 image (6M) - logger.debug( - f"Pilatus preview image for {self.name} is the same as last one, not updating." - ) - return + + if last_image is not None: + if np.array_equal(data, last_image.data): + # No update if image is the same, ~2.5ms on 2400x2400 image (6M) + logger.debug( + f"Pilatus preview image for {self.name} is the same as last one, not updating." + ) + continue + + logger.debug(f"Setting preview data for {self.name}") self.preview.put(data) except Exception: # pylint: disable=broad-except content = traceback.format_exc() @@ -243,13 +246,14 @@ class Pilatus(PSIDeviceBase, ADBase): self.hdf.lazy_open.set(1).wait(5) self.hdf.compression.set(COMPRESSIONALGORITHM.NONE.value).wait(5) # To test which to use # Start polling thread... - self._task_status = self.task_handler.submit_task(task=self._poll_array_data, run=True) + self._poll_thread.start() def on_stage(self) -> DeviceStatus | None: """ Called while staging the device. - Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. + Information about the upcoming scan can be accessed from the scan_info + (self.scan_info.msg) object. """ scan_msg: ScanStatusMessage = self.scan_info.msg if scan_msg.scan_name.startswith("xas"): @@ -327,32 +331,30 @@ class Pilatus(PSIDeviceBase, ADBase): if status.success: status.device.file_event.put( file_path=status.device._full_path, done=True, successful=True - ) + ) # pylint: disable:protected-access else: status.device.file_event.put( file_path=status.device._full_path, done=True, successful=False - ) + ) # pylint: disable:protected-access def on_complete(self) -> DeviceStatus | None: """Called to inquire if a device has completed a scans.""" if self.scan_info.msg.scan_name.startswith("xas"): # TODO implement logic for 'xas' scans return None - else: - status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) - status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) - status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) - num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( - "frames_per_trigger", 1 - ) - status_img_written = CompareStatus(self.hdf.num_captured, num_images) - status = AndStatusWithList( - device=self, - status_list=[status_hdf, status_cam, status_img_written, status_cam_server], - ) - status.add_callback(self._complete_callback) # Callback that writing was successful - self.cancel_on_stop(status) - return status + status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value) + status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value) + status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value) + num_images = self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters.get( + "frames_per_trigger", 1 + ) + status_img_written = CompareStatus(self.hdf.num_captured, num_images) + status = AndStatusWithList( + device=self, status_list=[status_hdf, status_cam, status_img_written, status_cam_server] + ) + status.add_callback(self._complete_callback) # Callback that writing was successful + self.cancel_on_stop(status) + return status def on_kickoff(self) -> None: """Called to kickoff a device for a fly scan. Has to be called explicitly.""" @@ -364,19 +366,20 @@ class Pilatus(PSIDeviceBase, ADBase): def on_destroy(self) -> None: """Called when the device is destroyed. Cleanup resources here.""" - self.on_stop() self._poll_thread_stop_event.set() + # TODO do we need to clean the poll thread ourselves? + self.on_stop() if __name__ == "__main__": try: pilatus = Pilatus(name="pilatus", prefix="X01DA-ES2-PIL:") - logger.info(f"Calling wait for connection") + logger.info("Calling wait for connection") # pilatus.wait_for_connection(all_signals=True, timeout=20) - logger.info(f"Connecting to pilatus...") + logger.info("Connecting to pilatus...") pilatus.on_connected() for exp_time, scan_number, n_pnts in zip([0.5, 1.0, 2.0], [1, 2, 3], [30, 20, 10]): - logger.info(f"Sleeping for 5s") + logger.info("Sleeping for 5s") time.sleep(5) pilatus.scan_info.msg.num_points = n_pnts pilatus.scan_info.msg.scan_parameters["exp_time"] = exp_time @@ -386,9 +389,9 @@ if __name__ == "__main__": "h5", ) pilatus.on_stage() - logger.info(f"Stage done") + logger.info("Stage done") pilatus.on_pre_scan().wait(timeout=5) - logger.info(f"Pre-scan done") + logger.info("Pre-scan done") for ii in range(pilatus.scan_info.msg.num_points): # if ii == 0: # time.sleep(1) @@ -401,8 +404,8 @@ if __name__ == "__main__": f"Preview shape: {p.data.shape}, max: {np.max(p.data)}, min: {np.min(p.data)}, mean: {np.mean(p.data)}" ) pilatus.on_complete().wait(timeout=5) - logger.info(f"Complete done") + logger.info("Complete done") pilatus.on_unstage() - logger.info(f"Unstage done") + logger.info("Unstage done") finally: pilatus.on_destroy()