diff --git a/csaxs_bec/device_configs/omny_config.yaml b/csaxs_bec/device_configs/omny_config.yaml index aed593c..4350bd1 100644 --- a/csaxs_bec/device_configs/omny_config.yaml +++ b/csaxs_bec/device_configs/omny_config.yaml @@ -1,95 +1,111 @@ ############################################################ -#################### flOMNI RT motors ###################### +#################### IDS Camera ###################### ############################################################ - -rtx: - description: OMNY rt - deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor +cam1: + description: Camera1 + deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera deviceConfig: - axis_Id: A - host: mpc3217.psi.ch - port: 3333 - sign: 1 + camera_ID: 202 + bits_per_pixel: 24 + channels: 3 + m_n_colormode: 1 enabled: true onFailure: buffer readOnly: false - readoutPriority: on_request - userParameter: - low_signal: 11000 - min_signal: 10000 - rt_pid_voltage: -0.06219 -rty: - description: OMNY rt - deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor - deviceConfig: - axis_Id: B - host: mpc3217.psi.ch - port: 3333 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: on_request - userParameter: - tomo_additional_offsety: 0 -rtz: - description: OMNY rt - deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor - deviceConfig: - axis_Id: C - host: mpc3217.psi.ch - port: 3333 - sign: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: on_request - -############################################################ -##################### OMNY samples ######################### -############################################################ -omny_samples: - description: OMNYSampleStorage - deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage - deviceConfig: {} - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline -############################################################ -##################### OMNY samples ######################### -############################################################ -omny_vcs: - description: OMNYVCS - deviceClass: csaxs_bec.devices.omny.omny_vcs.OMNYVCS - deviceConfig: {} - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline -############################################################ -##################### OMNY dewar ########################### -############################################################ -omny_dewar: - description: OMNY Dewar Information - deviceClass: csaxs_bec.devices.omny.omny_dewar.OMNYDewar - deviceConfig: {} - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline + readoutPriority: async -############################################################ -##################### OMNY temperatures #################### -############################################################ -omny_temperatures: - description: OMNY Temperatures and pressures - deviceClass: csaxs_bec.devices.omny.omny_temperatures.OMNYTemperatures - deviceConfig: {} - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: baseline +# ############################################################ +# #################### flOMNI RT motors ###################### +# ############################################################ + +# rtx: +# description: OMNY rt +# deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor +# deviceConfig: +# axis_Id: A +# host: mpc3217.psi.ch +# port: 3333 +# sign: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: on_request +# userParameter: +# low_signal: 11000 +# min_signal: 10000 +# rt_pid_voltage: -0.06219 +# rty: +# description: OMNY rt +# deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor +# deviceConfig: +# axis_Id: B +# host: mpc3217.psi.ch +# port: 3333 +# sign: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: on_request +# userParameter: +# tomo_additional_offsety: 0 +# rtz: +# description: OMNY rt +# deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor +# deviceConfig: +# axis_Id: C +# host: mpc3217.psi.ch +# port: 3333 +# sign: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: on_request + +# ############################################################ +# ##################### OMNY samples ######################### +# ############################################################ +# omny_samples: +# description: OMNYSampleStorage +# deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage +# deviceConfig: {} +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline +# ############################################################ +# ##################### OMNY samples ######################### +# ############################################################ +# omny_vcs: +# description: OMNYVCS +# deviceClass: csaxs_bec.devices.omny.omny_vcs.OMNYVCS +# deviceConfig: {} +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline +# ############################################################ +# ##################### OMNY dewar ########################### +# ############################################################ +# omny_dewar: +# description: OMNY Dewar Information +# deviceClass: csaxs_bec.devices.omny.omny_dewar.OMNYDewar +# deviceConfig: {} +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline + +# ############################################################ +# ##################### OMNY temperatures #################### +# ############################################################ +# omny_temperatures: +# description: OMNY Temperatures and pressures +# deviceClass: csaxs_bec.devices.omny.omny_temperatures.OMNYTemperatures +# deviceConfig: {} +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: baseline ############################################################ ##################### OMNY Galil motors #################### diff --git a/csaxs_bec/devices/ids_cameras/__init__.py b/csaxs_bec/devices/ids_cameras/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/devices/ids_cameras/ids_camera.py b/csaxs_bec/devices/ids_cameras/ids_camera.py new file mode 100644 index 0000000..c6cbdbf --- /dev/null +++ b/csaxs_bec/devices/ids_cameras/ids_camera.py @@ -0,0 +1,410 @@ +from ophyd import Device +from ophyd_devices.interfaces.base_classes.psi_detector_base import CustomDetectorMixin, PSIDetectorBase +from pyueye import ueye +from ophyd_devices.sim.sim_signals import SetableSignal +from ophyd import Kind, Component as Cpt +import threading +import numpy as np +import time + +class IDSCustomPrepare(CustomDetectorMixin): + + USER_ACCESS = ["pyueye"] + pyueye = ueye + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + super().__init__(*_args, parent=parent, **_kwargs) + self.ueye = ueye + self.h_cam = None + self.s_info =None + self._live_data_thread = None + + + + def on_connection_established(self): + self.hCam = self.ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID + self.sInfo = self.ueye.SENSORINFO() + self.cInfo = self.ueye.CAMINFO() + self.pcImageMemory = self.ueye.c_mem_p() + self.MemID = self.ueye.int() + self.rectAOI = self.ueye.IS_RECT() + self.pitch = self.ueye.INT() + self.nBitsPerPixel = self.ueye.INT(self.parent.bits_per_pixel) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome + self.channels = self.parent.channels #3: channels for color mode(RGB); take 1 channel for monochrome + self.m_nColorMode = self.ueye.INT(self.parent.m_n_colormode) # Y8/RGB16/RGB24/REG32 (1 for our color cameras) + self.bytes_per_pixel = int(self.nBitsPerPixel / 8) + + # Starts the driver and establishes the connection to the camera + nRet = self.ueye.is_InitCamera(self.hCam, None) + if nRet != self.ueye.IS_SUCCESS: + print("is_InitCamera ERROR") + + # Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that cInfo points to + nRet = self.ueye.is_GetCameraInfo(self.hCam, self.cInfo) + if nRet != self.ueye.IS_SUCCESS: + print("is_GetCameraInfo ERROR") + + # You can query additional information about the sensor type used in the camera + nRet = self.ueye.is_GetSensorInfo(self.hCam, self.sInfo) + if nRet != self.ueye.IS_SUCCESS: + print("is_GetSensorInfo ERROR") + + nRet = self.ueye.is_ResetToDefault(self.hCam) + if nRet != self.ueye.IS_SUCCESS: + print("is_ResetToDefault ERROR") + + # Set display mode to DIB + nRet = self.ueye.is_SetDisplayMode(self.hCam, self.ueye.IS_SET_DM_DIB) + + + + # Set the right color mode + if int.from_bytes(self.sInfo.nColorMode.value, byteorder='big') == self.ueye.IS_COLORMODE_BAYER: + # setup the color depth to the current windows setting + self.ueye.is_GetColorDepth(self.hCam, self.nBitsPerPixel, self.m_nColorMode) + bytes_per_pixel = int(self.nBitsPerPixel / 8) + print("IS_COLORMODE_BAYER: ", ) + print("\tm_nColorMode: \t\t", self.m_nColorMode) + print("\tnBitsPerPixel: \t\t", self.nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + + elif int.from_bytes(self.sInfo.nColorMode.value, byteorder='big') == self.ueye.IS_COLORMODE_CBYCRY: + # for color camera models use RGB32 mode + m_nColorMode = ueye.IS_CM_BGRA8_PACKED + nBitsPerPixel = ueye.INT(32) + bytes_per_pixel = int(self.nBitsPerPixel / 8) + print("IS_COLORMODE_CBYCRY: ", ) + print("\tm_nColorMode: \t\t", m_nColorMode) + print("\tnBitsPerPixel: \t\t", nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + + elif int.from_bytes(self.sInfo.nColorMode.value, byteorder='big') == self.ueye.IS_COLORMODE_MONOCHROME: + # for color camera models use RGB32 mode + m_nColorMode = self.ueye.IS_CM_MONO8 + nBitsPerPixel = self.ueye.INT(8) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("IS_COLORMODE_MONOCHROME: ", ) + print("\tm_nColorMode: \t\t", m_nColorMode) + print("\tnBitsPerPixel: \t\t", nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + + else: + # for monochrome camera models use Y8 mode + m_nColorMode = self.ueye.IS_CM_MONO8 + nBitsPerPixel = self.ueye.INT(8) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("else") + + # Can be used to set the size and position of an "area of interest"(AOI) within an image + nRet = self.ueye.is_AOI(self.hCam, ueye.IS_AOI_IMAGE_GET_AOI, self.rectAOI, self.ueye.sizeof(self.rectAOI)) + if nRet != self.ueye.IS_SUCCESS: + print("is_AOI ERROR") + + self.width = self.rectAOI.s32Width + self.height = self.rectAOI.s32Height + + # Prints out some information about the camera and the sensor + print("Camera model:\t\t", self.sInfo.strSensorName.decode('utf-8')) + print("Camera serial no.:\t", self.cInfo.SerNo.decode('utf-8')) + print("Maximum image width:\t", self.width) + print("Maximum image height:\t", self.height) + print() + + #--------------------------------------------------------------------------------------------------------------------------------------- + + # Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by nBitsPerPixel + nRet = self.ueye.is_AllocImageMem(self.hCam, self.width, self.height, self.nBitsPerPixel, self.pcImageMemory, self.MemID) + if nRet != self.ueye.IS_SUCCESS: + print("is_AllocImageMem ERROR") + else: + # Makes the specified image memory the active memory + nRet = self.ueye.is_SetImageMem(self.hCam, self.pcImageMemory, self.MemID) + if nRet != self.ueye.IS_SUCCESS: + print("is_SetImageMem ERROR") + else: + # Set the desired color mode + nRet = self.ueye.is_SetColorMode(self.hCam, self.m_nColorMode) + + + + # Activates the camera's live video mode (free run mode) + nRet = self.ueye.is_CaptureVideo(self.hCam, self.ueye.IS_DONT_WAIT) + if nRet != self.ueye.IS_SUCCESS: + print("is_CaptureVideo ERROR") + + # Enables the queue mode for existing image memory sequences + nRet = self.ueye.is_InquireImageMem(self.hCam, self.pcImageMemory, self.MemID, self.width, self.height, self.nBitsPerPixel, self.pitch) + if nRet != self.ueye.IS_SUCCESS: + print("is_InquireImageMem ERROR") + else: + print("Press q to leave the programm") + startmeasureframerate=True + Gain = False + + #start thread to receive data + self._start_data_thread() + + def _start_data_thread(self): + self._live_data_thread = threading.Thread(target=self._receive_data_from_camera, daemon = True) + self._live_data_thread.start() + + def _receive_data_from_camera(self): + while True: + + # In order to display the image in an OpenCV window we need to... + # ...extract the data of our image memory + array = ueye.get_data(self.pcImageMemory, self.width, self.height, self.nBitsPerPixel, self.pitch, copy=False) + + # bytes_per_pixel = int(nBitsPerPixel / 8) + + # ...reshape it in an numpy array... + frame = np.reshape(array,(self.height.value, self.width.value, self.bytes_per_pixel)) + self.parent.image_data.put(frame) + + if self.parent._destroyed is True: + break + time.sleep(0.1) + + def on_trigger(self): + self.parent._run_subs(self.parent.SUB_MONITOR, value=self.parent.image_data.get()) + + + +class IDSCamera(PSIDetectorBase): + USER_ACCESS = ["custom_prepare"] + + custom_prepare_cls = IDSCustomPrepare + + image_data = Cpt(SetableSignal, value=0, kind=Kind.omitted) + + SUB_MONITOR = "device_monitor_2d" + _default_sub = SUB_MONITOR + + def __init__(self, + prefix="", + *, + name:str, + camera_ID: int, + bits_per_pixel: int, + channels: int, + m_n_colormode: int, + kind=None, + parent=None, + device_manager=None, + **kwargs): + super().__init__(prefix, name=name, kind=kind, parent=parent, device_manager=device_manager, **kwargs) + self.camera_ID = camera_ID + self.bits_per_pixel = bits_per_pixel + self.channels = channels + self.m_n_colormode = m_n_colormode + self.wait_for_connection() + + def wait_for_connection(self, all_signals=False, timeout=10): + super().wait_for_connection(all_signals, timeout) + self.custom_prepare.on_connection_established() + + +"""from pyueye import ueye +import numpy as np +import cv2 +import sys +import time + +#--------------------------------------------------------------------------------------------------------------------------------------- + +#Variables +hCam = ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID +sInfo = ueye.SENSORINFO() +cInfo = ueye.CAMINFO() +pcImageMemory = ueye.c_mem_p() +MemID = ueye.int() +rectAOI = ueye.IS_RECT() +pitch = ueye.INT() +nBitsPerPixel = ueye.INT(24) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome +channels = 3 #3: channels for color mode(RGB); take 1 channel for monochrome +m_nColorMode = ueye.INT(1) # Y8/RGB16/RGB24/REG32 (1 for our color cameras) +bytes_per_pixel = int(nBitsPerPixel / 8) + +ids_cam + ... + deviceConfig: + camera_ID: 202 + bits_per_pixel: 24 + channels: 3 + m_n_colormode: 1 + +#--------------------------------------------------------------------------------------------------------------------------------------- +print("START") +print() + +# Starts the driver and establishes the connection to the camera +nRet = ueye.is_InitCamera(hCam, None) +if nRet != ueye.IS_SUCCESS: + print("is_InitCamera ERROR") + +# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that cInfo points to +nRet = ueye.is_GetCameraInfo(hCam, cInfo) +if nRet != ueye.IS_SUCCESS: + print("is_GetCameraInfo ERROR") + +# You can query additional information about the sensor type used in the camera +nRet = ueye.is_GetSensorInfo(hCam, sInfo) +if nRet != ueye.IS_SUCCESS: + print("is_GetSensorInfo ERROR") + +nRet = ueye.is_ResetToDefault( hCam) +if nRet != ueye.IS_SUCCESS: + print("is_ResetToDefault ERROR") + +# Set display mode to DIB +nRet = ueye.is_SetDisplayMode(hCam, ueye.IS_SET_DM_DIB) + + + +# Set the right color mode +if int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_BAYER: + # setup the color depth to the current windows setting + ueye.is_GetColorDepth(hCam, nBitsPerPixel, m_nColorMode) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("IS_COLORMODE_BAYER: ", ) + print("\tm_nColorMode: \t\t", m_nColorMode) + print("\tnBitsPerPixel: \t\t", nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + +elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_CBYCRY: + # for color camera models use RGB32 mode + m_nColorMode = ueye.IS_CM_BGRA8_PACKED + nBitsPerPixel = ueye.INT(32) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("IS_COLORMODE_CBYCRY: ", ) + print("\tm_nColorMode: \t\t", m_nColorMode) + print("\tnBitsPerPixel: \t\t", nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + +elif int.from_bytes(sInfo.nColorMode.value, byteorder='big') == ueye.IS_COLORMODE_MONOCHROME: + # for color camera models use RGB32 mode + m_nColorMode = ueye.IS_CM_MONO8 + nBitsPerPixel = ueye.INT(8) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("IS_COLORMODE_MONOCHROME: ", ) + print("\tm_nColorMode: \t\t", m_nColorMode) + print("\tnBitsPerPixel: \t\t", nBitsPerPixel) + print("\tbytes_per_pixel: \t\t", bytes_per_pixel) + print() + +else: + # for monochrome camera models use Y8 mode + m_nColorMode = ueye.IS_CM_MONO8 + nBitsPerPixel = ueye.INT(8) + bytes_per_pixel = int(nBitsPerPixel / 8) + print("else") + +# Can be used to set the size and position of an "area of interest"(AOI) within an image +nRet = ueye.is_AOI(hCam, ueye.IS_AOI_IMAGE_GET_AOI, rectAOI, ueye.sizeof(rectAOI)) +if nRet != ueye.IS_SUCCESS: + print("is_AOI ERROR") + +width = rectAOI.s32Width +height = rectAOI.s32Height + +# Prints out some information about the camera and the sensor +print("Camera model:\t\t", sInfo.strSensorName.decode('utf-8')) +print("Camera serial no.:\t", cInfo.SerNo.decode('utf-8')) +print("Maximum image width:\t", width) +print("Maximum image height:\t", height) +print() + +#--------------------------------------------------------------------------------------------------------------------------------------- + +# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by nBitsPerPixel +nRet = ueye.is_AllocImageMem(hCam, width, height, nBitsPerPixel, pcImageMemory, MemID) +if nRet != ueye.IS_SUCCESS: + print("is_AllocImageMem ERROR") +else: + # Makes the specified image memory the active memory + nRet = ueye.is_SetImageMem(hCam, pcImageMemory, MemID) + if nRet != ueye.IS_SUCCESS: + print("is_SetImageMem ERROR") + else: + # Set the desired color mode + nRet = ueye.is_SetColorMode(hCam, m_nColorMode) + + + +# Activates the camera's live video mode (free run mode) +nRet = ueye.is_CaptureVideo(hCam, ueye.IS_DONT_WAIT) +if nRet != ueye.IS_SUCCESS: + print("is_CaptureVideo ERROR") + +# Enables the queue mode for existing image memory sequences +nRet = ueye.is_InquireImageMem(hCam, pcImageMemory, MemID, width, height, nBitsPerPixel, pitch) +if nRet != ueye.IS_SUCCESS: + print("is_InquireImageMem ERROR") +else: + print("Press q to leave the programm") +startmeasureframerate=True +Gain = False +#--------------------------------------------------------------------------------------------------------------------------------------- + +# Continuous image display +while(nRet == ueye.IS_SUCCESS): + + # In order to display the image in an OpenCV window we need to... + # ...extract the data of our image memory + array = ueye.get_data(pcImageMemory, width, height, nBitsPerPixel, pitch, copy=False) + + # bytes_per_pixel = int(nBitsPerPixel / 8) + + # ...reshape it in an numpy array... + frame = np.reshape(array,(height.value, width.value, bytes_per_pixel)) + + # ...resize the image by a half + frame = cv2.resize(frame,(0,0),fx=0.5, fy=0.5) + +#--------------------------------------------------------------------------------------------------------------------------------------- + #Include image data processing here + +#--------------------------------------------------------------------------------------------------------------------------------------- + + #...and finally display it + cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame) + if startmeasureframerate: + starttime = time.time() + startmeasureframerate=False + framenumber=0 + if time.time() > starttime+5: + print(f"Caught {framenumber/5} frames per second") + startmeasureframerate=True + Gain = ~Gain + if Gain: + nRet = ueye.is_SetGainBoost(hCam, 1) + else: + nRet = ueye.is_SetGainBoost(hCam, 0) + print(f"Gain setting status {nRet}") + #...and finally display it + cv2.imshow("SimpleLive_Python_uEye_OpenCV", frame) + framenumber+=1 + time.sleep(0.1) + + # Press q if you want to end the loop + if (cv2.waitKey(1) & 0xFF) == ord('q'): + break +#--------------------------------------------------------------------------------------------------------------------------------------- + +# Releases an image memory that was allocated using is_AllocImageMem() and removes it from the driver management +ueye.is_FreeImageMem(hCam, pcImageMemory, MemID) + +# Disables the hCam camera handle and releases the data structures and memory areas taken up by the uEye camera +ueye.is_ExitCamera(hCam) + +# Destroys the OpenCv windows +cv2.destroyAllWindows() + +print() +print("END") +""" \ No newline at end of file diff --git a/csaxs_bec/devices/ids_cameras/ids_ueye_signals.py b/csaxs_bec/devices/ids_cameras/ids_ueye_signals.py new file mode 100644 index 0000000..9c1c215 --- /dev/null +++ b/csaxs_bec/devices/ids_cameras/ids_ueye_signals.py @@ -0,0 +1,83 @@ +import time + +import numpy as np +from bec_lib import bec_logger +from ophyd import Kind, Signal +from ophyd.utils import ReadOnlyError + +from ophyd_devices.utils.bec_device_base import BECDeviceBase + +logger = bec_logger.logger + +# Readout precision for Setable/ReadOnlySignal signals +PRECISION = 3 + + +class ReadOnlySignal(Signal): + """Setable signal for simulated devices. + + The signal will store the value in sim_state of the SimulatedData class of the parent device. + It will also return the value from sim_state when get is called. Compared to the ReadOnlySignal, + this signal can be written to. + The setable signal inherits from the Signal class of ophyd, thus the class attribute needs to be + initiated as a Component (class from ophyd). + + >>> signal = SetableSignal(name="signal", parent=parent, value=0) + + Parameters + ---------- + + name (string) : Name of the signal + parent (object) : Parent object of the signal, default none. + value (any) : Initial value of the signal, default 0. + kind (int) : Kind of the signal, default Kind.normal. + precision (float) : Precision of the signal, default PRECISION. + """ + + def __init__( + self, + name: str, + *args, + fcn: callable, + kind: int = Kind.normal, + precision: float = PRECISION, + **kwargs, + ): + super().__init__(*args, name=name, value=value, kind=kind, **kwargs) + self._metadata.update(connected=True, write_access=False) + self._value = None + self.precision = precision + self.fcn = fcn + + # pylint: disable=arguments-differ + def get(self): + """Get the current position of the simulated device. + + Core function for signal. + """ + self._value = self.fcn() + return self._value + + # pylint: disable=arguments-differ + def put(self, value): + """Put the value to the simulated device. + + Core function for signal. + """ + self._update_sim_state(value) + self._value = value + + def describe(self): + """Describe the readback signal. + + Core function for signal. + """ + res = super().describe() + if self.precision is not None: + res[self.name]["precision"] = self.precision + return res + + @property + def timestamp(self): + """Timestamp of the readback value""" + return self._get_timestamp()