diff --git a/ophyd_devices/epics/devices/helgecams/HelgeCameraBase.py b/ophyd_devices/epics/devices/helgecams/HelgeCameraBase.py index cad83f4..a504eac 100644 --- a/ophyd_devices/epics/devices/helgecams/HelgeCameraBase.py +++ b/ophyd_devices/epics/devices/helgecams/HelgeCameraBase.py @@ -15,7 +15,7 @@ import numpy as np class HelgeCameraBase(Device): - """ Ophyd baseclass for Helge camera IOCs + """Ophyd baseclass for Helge camera IOCs This class provides wrappers for Helge's camera IOCs around SwissFEL and for high performance SLS 2.0 cameras. @@ -30,6 +30,14 @@ class HelgeCameraBase(Device): camType = Component(EpicsSignalRO, "QUERY", kind=Kind.omitted) camBoard = Component(EpicsSignalRO, "BOARD", kind=Kind.config) #camSerial = Component(EpicsSignalRO, "SERIALNR", kind=Kind.config) + + # ######################################################################## + # Acquisition commands + busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config) + camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config) + camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config) + camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config) + camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config) # ######################################################################## # Image size settings @@ -43,28 +51,21 @@ class HelgeCameraBase(Device): pxNumX = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config) pxNumY = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config) - # ######################################################################## - # Acquisition commands - - # ######################################################################## # Polled CamStatus - busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config) - camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config) - camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config) camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config) - camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config) - camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config) # Weird state maschine with separate transition states - camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config) + camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config) + camRemoved = Component(EpicsSignalRO, "REMOVAL", auto_monitor=True, kind=Kind.config) + camSetParam = Component(EpicsSignalRO, "SET_PARAM", auto_monitor=True, kind=Kind.config) camSetParamBusy = Component(EpicsSignalRO, "BUSY_SET_PARAM", auto_monitor=True, kind=Kind.config) camCamera = Component(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config) - #camCameraBusy = Component(EpicsSignalRO, "CAMERA_BUSY", auto_monitor=True, kind=Kind.config) + camCameraBusy = Component(EpicsSignalRO, "CAMERA_BUSY", auto_monitor=True, kind=Kind.config) camInit= Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config) - #camInitBusy = Component(EpicsSignalRO, "INIT_BUSY", auto_monitor=True, kind=Kind.config) + camInitBusy = Component(EpicsSignalRO, "INIT_BUSY", auto_monitor=True, kind=Kind.config) # ######################################################################## # Acquisition configuration @@ -83,23 +84,86 @@ class HelgeCameraBase(Device): image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted) + # File interface + camFileFormat = Component(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config) + camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config) + camFileName = Component(EpicsSignal, "FILENAME", put_complete=True, kind=Kind.config) + camFileNr = Component(EpicsSignal, "FILENR", put_complete=True, kind=Kind.config) + camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config) + camFileTransferStart = Component(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config) + camFileTransferStop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config) - def configure(self, exposure_time=None): + + + def configure(self, d: dict = {}) -> tuple: + if self.state in ["OFFLINE", "REMOVED", "RUNNING"]: + raise RuntimeError(f"Can't change configuration from state {self.state}") + + exposure_time = d['exptime'] + if exposure_time is not None: self.acqExpTime.set(exposure_time).wait() + @property + def state(self): + if self.camSetParamBusy.value: + return "BUSY" + if self.camStatusCode.value==2 and self.camInit.value==1: + return "IDLE" + if self.camStatusCode.value==6 and self.camInit.value==1: + return "RUNNING" + if self.camRemoval.value==0 and self.camInit.value==0: + return "OFFLINE" + if self.camRemoval.value: + return "REMOVED" + return "UNKNOWN" + + + @state.setter + def state(self): + raise ReadOnlyError("State is a ReadOnly property") - def stage(self): - """ State transitions are only allowed when the IOC is not busy """ - if self.camCameraBusy.value or self.camInitBusy.value or self.camSetParamBusy.value: - raise RuntimeErrror("Failed to stage, the camera appears busy.") + def stage(self) -> None: + """ Start acquisition""" + + # State transitions are only allowed when the IOC is not busy + if self.state not in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"): + raise RuntimeError(f"Camera in in state: {self.state}") + + # Start the acquisition self.camStatusCmd.set("Running").wait() + + super().stage() + + def kickoff(self, settle_time=0.2) -> DeviceStatus: + """ Start acquisition""" + + # State transitions are only allowed when the IOC is not busy + if self.state not in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"): + raise RuntimeError(f"Camera in in state: {self.state}") + + # Start the acquisition + self.camStatusCmd.set("Running").wait() + + # Subscribe and wait for update + def isRunning(*args, old_value, value, timestamp, **kwargs): + # result = bool(value==6 and self.camInit.value==1) + result = bool(self.state=="RUNNING") + return result + status = SubscriptionStatus(self.camStatusCode, isRunning, settle_time=0.2) + return status + + def stop(self): + """ Stop the running acquisition """ + self.camStatusCmd.set("Idle").wait() def unstage(self): - """ State transitions are only allowed when the IOC is not busy """ + """ Stop the running acquisition and unstage the device""" self.camStatusCmd.set("Idle").wait() + super().unstage() + # Automatically connect to test camera if directly invoked if __name__ == "__main__": diff --git a/ophyd_devices/epics/devices/helgecams/StdDaqBase.py b/ophyd_devices/epics/devices/helgecams/StdDaqBase.py new file mode 100644 index 0000000..2ac705f --- /dev/null +++ b/ophyd_devices/epics/devices/helgecams/StdDaqBase.py @@ -0,0 +1,127 @@ +import enum +import threading +import time +import numpy as np +import os + +from typing import Any + +from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV +from ophyd import Device +from ophyd import ADComponent as ADCpt + +from std_daq_client import StdDaqClient + +from bec_lib import threadlocked +from bec_lib.logger import bec_logger +from bec_lib import messages +from bec_lib.endpoints import MessageEndpoints + +from ophyd_devices.epics.devices.psi_detector_base import PSIDetectorBase, CustomDetectorMixin + +logger = bec_logger.logger + + + + +class StdDaqBase(Device): + """ + Standalone standard_daq base class from the Eiger 9M. + """ + std_numpoints = None + std_filename = None + + def __init__(self, prefix="", *, name, stddaq_filepath, kind=None, stddaq_rest_url="http://xbl-daq-29:5000", stddaq_timeout=5, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + + self.std_rest_url = stddaq_rest_url + self.std_timeout = stddaq_timeout + self.std_filepath = stddaq_filepath + + # Std client + self.std_client = StdDaqClient(url_base=self.std_rest_url) + + # Stop writer + self.std_client.stop_writer() + + # Change e-account + #eacc = self.parent.scaninfo.username + #self.update_std_cfg("writer_user_id", int(eacc.strip(" e"))) + + signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.std_timeout, + all_signals=True, + ): + raise TimeoutError(f"Std client not in READY state, returns: {self.std_client.get_status()}") + + def update_std_cfg(self, cfg_key: str, value: Any) -> None: + """ + Update std_daq config + + Checks that the new value matches the type of the former entry. + + Args: + cfg_key (str) : config key of value to be updated + value (Any) : value to be updated for the specified key + """ + + # Load config from client and check old value + cfg = self.std_client.get_config() + old_value = cfg.get(cfg_key) + if old_value is None: + raise KeyError( f"Tried to change entry for key {cfg_key} in std_config that does not exist") + if not isinstance(value, type(old_value)): + raise TypeError(f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}") + + # Update config with new value and send back to client + cfg.update({cfg_key: value}) + self.std_client.set_config(cfg) + + def configure(self, d: dict): + """Configure the standard_daq""" + + filename = str(d['filename']).split(".")[0] + + self.std_numpoints = int(d['numpoints']) + self.std_filename = f"{self.std_filepath}/{filename}.h5" + + + def stage(self) -> None: + """Start acquiring""" + + self.stop() + try: + self.std_client.start_writer_async( + {"output_file": self.std_filename, "n_images": int(self.std_numpoints)} + ) + except Exception as ex: + time.sleep(5) + if self.std_client.get_status()["state"] == "READY": + raise TimeoutError(f"Timeout of start_writer_async with {ex}") from ex + + # Check status of std_daq + signal_conditions = [(lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES")] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.std_timeout, + check_stopped=False, + all_signals=True, + ): + raise TimeoutError(f"Timeout of 5s reached for std_daq start_writer_async with std_daq client status {self.std_client.get_status()}") + + + def unstage(self) -> None: + """Close file writer""" + self.std_client.stop_writer() + + def stop(self) -> None: + """Close file writer""" + self.std_client.stop_writer() + + + + + +if __name__ == "__main__": + daq = StdDaqBase(name="daq", stddaq_filepath="~/Data10") diff --git a/ophyd_devices/epics/devices/helgecams/StdDaqProxy.py b/ophyd_devices/epics/devices/helgecams/StdDaqProxy.py deleted file mode 100644 index f2156b2..0000000 --- a/ophyd_devices/epics/devices/helgecams/StdDaqProxy.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Dec 6 11:33:54 2023 - -@author: mohacsi_i -""" - -from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import Status, SubscriptionStatus, StatusBase -from time import sleep -import warnings -import numpy as np - -import requests - -STDAQ_REST_ADDR = "http://127.0.0.1:5001" - - - - -data = {"sources":"eiger", "n_images":10, "output_file":"/tmp/test.h5"} -headers = {'Content-type': 'application/json'} -r = requests.post(url = "http://127.0.0.1:5000/write_sync", json=data, headers=headers) - - - - - -class StdDaqBase(Device): - """ Ophyd baseclass for Helge camera IOCs - - This class provides wrappers for Helge's camera IOCs around SwissFEL and - for high performance SLS 2.0 cameras. - - The IOC's operatio - - - - """ - # ######################################################################## - # General hardware info - camType = Component(EpicsSignalRO, "QUERY", kind=Kind.omitted) - camBoard = Component(EpicsSignalRO, "BOARD", kind=Kind.config) - #camSerial = Component(EpicsSignalRO, "SERIALNR", kind=Kind.config) - - - def configure(self, d: dict = {}): - self._n_images = d['n_images'] if 'n_images' in d else None - self._sources = d['sources'] if 'sources' in d else None - self._output_file = d['output_file'] if 'output_file' in d else None - - - - def kickoff(self): - data = {"sources": self._sources, "n_images": self._n_images, "output_file": self._output_file} - headers = {'Content-type': 'application/json'} - r = requests.post(url = "http://127.0.0.1:5000/write_async", json=data, headers=headers) - self.req_id = req_id = str(r.json()["request_id"]) \ No newline at end of file