diff --git a/README.rst b/README.rst index 1ff7b03..ee6e2ff 100644 --- a/README.rst +++ b/README.rst @@ -3,86 +3,36 @@ pymodaq_plugins_template .. the following must be adapted to your developed package, links to pypi, github description... -.. image:: https://img.shields.io/pypi/v/pymodaq_plugins_template.svg - :target: https://pypi.org/project/pymodaq_plugins_template/ - :alt: Latest Version +.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_asi/workflows/Upload%20Python%20Package/badge.svg + :target: https://github.com/PyMoDAQ/pymodaq_plugins_asi + :alt: Publication Status .. image:: https://readthedocs.org/projects/pymodaq/badge/?version=latest - :target: https://pymodaq.readthedocs.io/en/stable/?badge=latest + :target: https://pymodaq.readthedocs.io/en/stable/?badge=latest :alt: Documentation Status -.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/workflows/Upload%20Python%20Package/badge.svg - :target: https://github.com/PyMoDAQ/pymodaq_plugins_template - :alt: Publication Status - -.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml/badge.svg - :target: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml - - -Use this template to create a repository on your account and start the development of your own PyMoDAQ plugin! +.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_asi/actions/workflows/Test.yml/badge.svg + :target: https://github.com/PyMoDAQ/pymodaq_plugins_asi/actions/workflows/Test.yml Authors ======= -* First Author (myemail@xxx.org) -* Other author (myotheremail@xxx.org) - -.. if needed use this field - - Contributors - ============ - - * First Contributor - * Other Contributors - -.. if needed use this field - - Depending on the plugin type, delete/complete the fields below - +* Adrien Teurtrie Instruments =========== -Below is the list of instruments included in this plugin - -Actuators -+++++++++ - -* **yyy**: control of yyy actuators -* **xxx**: control of xxx actuators - -Viewer0D -++++++++ - -* **yyy**: control of yyy 0D detector -* **xxx**: control of xxx 0D detector - -Viewer1D -++++++++ - -* **yyy**: control of yyy 1D detector -* **xxx**: control of xxx 1D detector - +The Cheetah3 of Amsterdam Scientific Instruments can be interfaced using this plugin. Viewer2D ++++++++ -* **yyy**: control of yyy 2D detector -* **xxx**: control of xxx 2D detector - - -PID Models -========== - - -Extensions -========== - +* **DAQ_2DViewer_Cheetah3**: control of the Cheetah3 2D detector Installation instructions ========================= -* PyMoDAQ’s version. -* Operating system’s version. -* What manufacturer’s drivers should be installed to make this plugin run? +* PyMoDAQ’s version : 5.0.18 +* Operating system’s version : Windows 11 +* Serval ASI's server dedicated to the Cheetah3 should be installed (3.3.0 tested). The communication with Serval is done through http. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 9616235..ed8eecd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,20 +9,19 @@ scanners = false # true if plugin contains custom scan layout (daq_scan extensi package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_asi' #todo modify url by your plugin url [project] -name = "pymodaq_plugins_asi" #todo modify template by your plugin short name -description = 'some word about your plugin' +name = "pymodaq_plugins_asi" +description = 'Pymodaq plugin for controlling Amsterdam Scientific Instruments hardware.' dependencies = [ "pymodaq>=5.0.0", - #todo: list here all dependencies your package may have + requests, + pillow ] authors = [ {name = "Teurtrie Adrien", email = "adrien.teurtrie@cemes.fr"}, - #todo: list here all authors of your plugin ] maintainers = [ - {name = "Name Surname", email = "myname@test.fr"}, - #todo: list here all maintainers of your plugin + {name = "Adrien Teurtrie", email = "adrien.teurtrie@cemes.fr"}, ] # nottodo: leave everything below as is! diff --git a/src/pymodaq_plugins_asi/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Cheetah3.py b/src/pymodaq_plugins_asi/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Cheetah3.py index f1c3a12..6e9553d 100644 --- a/src/pymodaq_plugins_asi/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Cheetah3.py +++ b/src/pymodaq_plugins_asi/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Cheetah3.py @@ -10,7 +10,8 @@ from pymodaq_utils.logger import set_logger, get_module_name import collections -from pymodaq_plugins_asi.hardware.cheetah3 import Cheetah3 +from pymodaq_plugins_asi.hardware.cheetah3 import Cheetah3, config +from pymodaq_plugins_asi.hardware.camera_utils import bin2d, get_bin_list logger = set_logger(get_module_name(__file__)) @@ -21,7 +22,8 @@ # I. DAQ_2DViewer_Cheetah3 # I. 1. Parameters # I. 2. Initialisation -# I. 3. Data acquisition +# I. 3. Data acquisition and axes +# I. 4. Properties # II. Callback class # III. Local testing code @@ -72,13 +74,19 @@ class DAQ_2DViewer_Cheetah3(DAQ_Viewer_base): params = comon_parameters + [ {'title' : "Frame-based camera settings", 'name' : 'camera_settings', 'type' : 'group', 'expanded' : True, 'children' : [ {'title' : 'Exposure time', 'name' : 'exposure_time', 'type' : 'float', 'value' : 0.5}, - {'title' : 'Full binning', 'name' : 'binning', 'type' : 'itemselect', 'value' : dict(all_items = ['None','Vertical', 'Horizontal' ], selected = ['None']) } + {'title' : 'x binning', 'name' : 'x_binning', 'type' : 'list', 'value' : 1, 'limits' : [1]}, + {'title' : 'y binning', 'name' : 'y_binning', 'type' : 'list', 'value' : 1, 'limits' : [1]}, ]}, - {'title' : 'File paths', 'name' : 'file_paths', 'type' : 'group', 'expanded' : False, 'children' : [ - {'title' : 'bpc file path', 'name' : 'bpc_file_path', 'type' : 'str', 'value' : '/home/asi/bpc/'}, - {'title' : 'dacs file path', 'name' : 'dacs_file_path', 'type' : 'str', 'value' : '/home/asi/dacs/'}, - {'title' : 'save folder path', 'name' : 'save_folder_path', 'type' : 'str', 'value' : '/home/asi/data/'}, - ]} + {'title' : 'File paths input', 'name' : 'file_paths', 'type' : 'group', 'expanded' : False, 'children' : [ + {'title' : 'Add bpc file path', 'name' : 'bpc_file_path', 'type' : 'str', 'value' : '/home/asi/'}, + {'title' : 'Add dacs file path', 'name' : 'dacs_file_path', 'type' : 'str', 'value' : '/home/asi/'}, + {'title' : 'Add save folder path', 'name' : 'save_folder_path', 'type' : 'str', 'value' : '/home/asi/'}, + ]}, + {'title' : 'Current file paths', 'name' : 'file_paths_lists', 'type' : 'group', 'expanded' : True, 'children' : [ + {'title' : 'bpc file paths', 'name' : 'bpc_file_paths_list', 'type' : 'list', 'value' : '', 'limits' : ['']}, + {'title' : 'dacs file paths', 'name' : 'dacs_file_paths_list', 'type' : 'list', 'value' : '', 'limits' : ['']}, + {'title' : 'save folder paths', 'name' : 'save_folder_paths_list', 'type' : 'list', 'value' : '', 'limits' : ['']}, + ]} ] def commit_settings(self, param: Parameter): @@ -89,18 +97,35 @@ def commit_settings(self, param: Parameter): param: Parameter A given parameter (within detector_settings) whose value has been changed by the user """ + # There is probably a better way to select params, e.g. based on groups. But the current version is simpler even though quite verbose. if param.name() == "exposure_time": self.controller.exposure_time = param.value() - elif param.name() == 'binning' : - self.binning = param.value()['selected'] - elif param.name() == 'bpc_file_path' : - self.controller.config.add_bpc_file(param.value()) - elif param.name() == 'dacs_file_path' : - self.controller.config.add_dacs_file(param.value()) - elif param.name() == 'save_folder_path' : - self.controller.config.add_save_folder(param.value()) - elif param.name() == 'destination' : - self.controller.config.build_destination(param.value()["selected"]) + elif param.name() == 'x_binning' : + self.x_binning = param.value() + self.set_axes() + elif param.name() == 'y_binning' : + self.y_binning = param.value() + self.set_axes() + elif param.name() == 'bpc_file_path' : + self.controller.cheetah3_config.add_bpc_file(param.value()) + self.controller.cheetah3_config.refresh() + self.settings.child('file_paths_lists','bpc_file_paths_list').setLimits(config("CHEETAH3","file_paths",'bpc')) + elif param.name() == 'dacs_file_path' : + self.controller.cheetah3_config.add_dacs_file(param.value()) + self.controller.cheetah3_config.refresh() + self.settings.child('file_paths_lists','dacs_file_paths_list').setLimits(config("CHEETAH3","file_paths",'dacs')) + elif param.name() == 'save_folder_path' : + self.controller.cheetah3_config.add_save_folder(param.value()) + self.controller.cheetah3_config.refresh() + self.settings.child('file_paths_lists','save_folder_paths_list').setLimits(config("CHEETAH3","file_paths",'data')) + elif param.name() == 'destination' : + self.controller.cheetah3_config.build_destination(param.value()["selected"]) + elif param.name() == 'bpc_file_paths_list' : + self.controller.bpc_file = param.value() + elif param.name() == 'dacs_file_paths_list' : + self.controller.dacs_file = param.value() + elif param.name() == 'save_folder_paths_list' : + self.controller.save_folder = param.value() ######################## # I. 1. Initialisation # @@ -108,16 +133,17 @@ def commit_settings(self, param: Parameter): live_mode_available = True # CT01. We create a signal object to start the execution of the callback thread. - startup_callback_signal = QtCore.Signal() - callback_signal = QtCore.Signal() + callback_signal = QtCore.Signal(int) - def ini_attributes(self): self.controller: Cheetah3 = None self.x_axis = None self.y_axis = None - self.binning = 'None' + self.x_binning = 1 + self.y_binning = 1 + self._x_size = 1 + self._y_size = 1 def ini_detector(self, controller=None): """Detector communication initialization @@ -134,13 +160,10 @@ def ini_detector(self, controller=None): initialized: bool False if initialization failed otherwise True """ - self.controller = self.ini_detector_init(slave_controller = controller, new_controller = Cheetah3() ) if self.is_master: + self.controller = Cheetah3() initialized = self.controller.check_connection() info = "The DAQ_viewer Cheetah3 has successfully started" - self.x_axis = Axis(data=np.linspace(0, 512 - 1, 512, dtype=int), label='Pixels', index=1) - self.y_axis = Axis(data=np.linspace(0, 512 - 1, 512, dtype=int), label='Pixels', index=1) - # CT02. An object (called callback), is instanciated. self.callback = Cheetah3Callback(self.controller) # CT03. A thread object is created (callback_thread) @@ -152,11 +175,9 @@ def ini_detector(self, controller=None): # CT06. We make the thread ready to execute self.callback_thread.start() # CT07. We connect the signal to the execution of data read-out from the detector - self.startup_callback_signal.connect(self.callback.start_readout) self.callback_signal.connect(self.callback.readout) # CT08. We connect the signal of the callback to the execution of PyMoDAQ GUI to display data. self.callback.data_sig.connect(self.emit_data) - self.callback.data_sig_startup.connect(self.emit_data) else: self.controller = controller initialized = True @@ -165,21 +186,78 @@ def ini_detector(self, controller=None): self.settings.addChild({'title' : 'Data destination', 'name' : 'destination', 'type' : 'itemselect', 'value' : dict( all_items = profile_names, selected =['live_preview'] ), 'checkbox' : True}) + self._x_size = self.controller.x_size + self._y_size = self.controller.y_size + self.settings.child('camera_settings','x_binning').setLimits(get_bin_list(self.x_size)) + self.settings.child('camera_settings','y_binning').setLimits(get_bin_list(self.y_size)) + self.settings.child('file_paths_lists','bpc_file_paths_list').setLimits(config("CHEETAH3","file_paths",'bpc')) + self.settings.child('file_paths_lists','dacs_file_paths_list').setLimits(config("CHEETAH3","file_paths",'dacs')) + self.settings.child('file_paths_lists','save_folder_paths_list').setLimits(config("CHEETAH3","file_paths",'data')) return info, initialized def close(self): """Terminate the communication protocol""" - ## TODO for your custom plugin - pass - # raise NotImplementedError # when writing your own plugin remove this line - # if self.is_master: - # # self.controller.your_method_to_terminate_the_communication() # when writing your own plugin replace this line - # ... + self.controller.stop() - ########################## - # I. 3. Data acquisition # - ########################## + ################################### + # I. 3. Data acquisition and axes # + ################################### + + def set_axes(self) : + """ + Set the axes for display depending on binning values. Can change the representation from 2D to 1D or 0D. + """ + data_x_axis = np.linspace(start= 0, stop = self.x_size//self.x_binning, num = self.x_size//self.x_binning) + data_y_axis = np.linspace(start= 0, stop = self.y_size//self.y_binning, num = self.y_size//self.y_binning) + # Case 1 : full binning both directions -> 0D data + if self.x_binning == self.x_size and self.y_binning == self.y_size : + dummy_data = np.array([0.0]) + # Case 2 : full binning in the x direction -> 1D data + elif self.x_binning == self.x_size and self.y_binning != self.y_size : + dummy_data = np.zeros((self.y_size//self.y_binning,)) + self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) + # Case 3 : full binning in the y direction -> 1D data + elif self.y_binning == self.y_size and self.x_binning != self.x_size : + dummy_data = np.zeros((self.x_size//self.x_binning,)) + self.x_axis = Axis(data=data_x_axis, label='', units='', index=0) + # Case 4 : No full binning -> 2D data + else : + dummy_data = np.zeros((self.y_size//self.y_binning,self.x_size//self.x_binning)) + self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) + self.x_axis = Axis(data=data_x_axis, label='', units='', index=1) + + dfp = self.prepare_dfp(dummy_data) + # Prepares the viewer + self.dte_signal_temp.emit(DataToExport('Cheetah3', + data=dfp)) + + def prepare_dfp (self, data : np.ndarray) -> DataFromPlugins : + """ + Prepares DataFromPlugins for display. Chooses the right axes and data size based on the data shape. + + Parameters + ---------- + data : np.ndarray + input data. It can be any shape up to 2D. + """ + if self.x_binning == self.x_size and self.y_binning == self.y_size : + dfp = [DataFromPlugins(name = 'Cheetah3 full sum', + data = data, + dim = 'Data0D')] + elif self.x_binning == self.x_size and self.y_binning != self.y_size : + dfp = [DataFromPlugins(name = 'Cheetah3 sum x', + data = [np.atleast_1d(data)], + dim = 'Data1D',axes=[self.y_axis])] + elif self.y_binning == self.y_size and self.x_binning != self.x_size : + dfp = [DataFromPlugins(name = 'Cheetah3 sum y', + data = [np.atleast_1d(data)], + dim = 'Data1D',axes=[self.x_axis])] + else : + dfp = [DataFromPlugins(name = 'Cheetah3', + data = [np.atleast_1d(data)], + dim = 'Data2D',axes=[self.x_axis, self.y_axis])] + return dfp def emit_data(self,data : np.ndarray): # Add a bool as arg so that I can pick finishing acquisition or current @@ -193,30 +271,11 @@ def emit_data(self,data : np.ndarray): """ # CT13. The callback emitted a signal to display data try: - - image = data.reshape((512,512)).astype(float) - dtp =[] - if self.binning == 'None' : - dtp.append(DataFromPlugins(name='Cheetah3 image', - data=[np.atleast_1d( - image) ])) - elif self.binning == 'Vertical' : - dtp.append(DataFromPlugins(name = 'Cheetah3 sum X', - data = [np.atleast_1d(image.sum(axis = 0))], - dim = 'Data1D')) - - elif self.binning == 'Horizontal' : - dtp.append(DataFromPlugins(name = 'Cheetah3 sum Y', - data = [np.atleast_1d(image.sum(axis = 1))], - dim = 'Data1D')) - + binned_data = bin2d(data,self.x_binning,self.y_binning) + dfp = self.prepare_dfp(data=binned_data) self.dte_signal.emit(DataToExport('Cheetah3', - data=dtp)) - # QtWidgets.QApplication.processEvents() - # CT14. Once the data are displayed we come back to the callback to fetch additional data. - self.callback_signal.emit() + data=dfp)) except Exception as e: - print("An exception occured in emit data") self.emit_status(ThreadCommand('Update_Status', [str(e), 'log'])) def grab_data(self, Naverage=1, **kwargs): @@ -230,21 +289,16 @@ def grab_data(self, Naverage=1, **kwargs): kwargs: dict others optionals arguments """ - ## TODO for your custom plugin: you should choose EITHER the synchrone or the asynchrone version following - - ##synchrone version (blocking function) + self.controller.ntriggers = int(2e9) try: - if kwargs.get('live',False) == True : - self.controller.ntriggers = int(2e9) - self.controller.start() + self.controller.start(timeout = 0.0) # CT9. We trigger the execution of the callback thread start_readout function. - self.startup_callback_signal.emit() + self.callback_signal.emit(0) else: - self.controller.ntriggers = 1 - self.controller.start() - self.startup_callback_signal.emit() + self.controller.start(timeout = 5.0) + self.callback_signal.emit(1) except Exception as e: @@ -253,8 +307,19 @@ def grab_data(self, Naverage=1, **kwargs): def stop(self): """Stop the current grab hardware wise if necessary""" - ## TODO for your custom plugin self.controller.stop() + + #################### + # I. 4. Properties # + #################### + + @property + def x_size(self) : + return self._x_size + + @property + def y_size(self) : + return self._y_size ###################### # II. Callback class # @@ -264,37 +329,37 @@ class Cheetah3Callback(QtCore.QObject): """ """ - data_sig_startup = QtCore.Signal(np.ndarray) data_sig = QtCore.Signal(np.ndarray) def __init__(self, controller): - super(Cheetah3Callback, self).__init__() - self.buffer = collections.deque(maxlen=100) self.controller = controller + super().__init__() - def start_readout(self): - while True : - # CT10. We start a blocking function. It waits until data are avaible. - current_image = self.controller.preview() - self.buffer.append(current_image) - if len(self.buffer) > 0 : - # CT11. One data are ready we want them displayed, so we signal the main thread - self.data_sig_startup.emit(self.buffer.pop()) - if self.controller.get_status() == "DA_STOPPING" or self.controller.get_status() == "DA_IDLE" : - logger.info("Acquisition finished") - break - - # CT12. The side thread continues to pile up data in the rolling buffer - - def readout(self) : - # CT15. Since the loop is still running - while True : - if len(self.buffer) > 0 : - break - else : - if self.controller.get_status() == "DA_STOPPING" or self.controller.get_status() == "DA_IDLE" : - return - self.data_sig.emit(self.buffer.pop()) + def readout(self,num_frames : int): + if num_frames == 0 : + while True : + try : + # CT10. We start a blocking function. It waits until data are avaible. + current_image = self.controller.preview() + self.data_sig.emit(current_image) + if self.controller.get_status() == "DA_IDLE" : + logger.info("Acquisition finished") + break + except BrokenPipeError : + logger.info('Acquistion stopped.') + break + else : + for i in range(num_frames) : + try : + # CT10. We start a blocking function. It waits until data are avaible. + current_image = self.controller.preview() + self.data_sig.emit(current_image) + if self.controller.get_status() == "DA_IDLE" : + logger.info("Acquisition finished") + break + except BrokenPipeError : + logger.info('Acquistion stopped.') + break ########################### # III. Local testing code # diff --git a/src/pymodaq_plugins_asi/hardware/camera_utils.py b/src/pymodaq_plugins_asi/hardware/camera_utils.py new file mode 100644 index 0000000..a2c0154 --- /dev/null +++ b/src/pymodaq_plugins_asi/hardware/camera_utils.py @@ -0,0 +1,62 @@ +import numpy as np +import math + +def bin2d(array : np.ndarray, x : int,y : int) -> np.ndarray: + """ + Summing over 2d arrays for SNR improvement. + + Parameters + ---------- + array : np.ndarray + input bi-dimensional data. + x : int + Size reduction factor of the 1-st axis + y : int + Size reduction factor of the 0-th axis + + Returns + ------- + binned_arrray : np.ndarray + Summed array with reduced size + """ + x_bins = array.shape[1]//x + y_bins = array.shape[0]//y + binned_array = array.reshape(y_bins,y,x_bins,x).sum(axis=3).sum(axis=1) + # We want array that have shape e.g. (y,1) to reduce to (y,). + if binned_array.shape[1] == 1 : + binned_array = binned_array.sum(axis = 1) + if binned_array.shape[0] == 1 : + binned_array = binned_array.sum(axis = 0) + return binned_array + # https://stackoverflow.com/questions/61325586/fast-way-to-bin-a-2d-array-in-python + +def get_bin_list(size) : + """ + Produces the list of binning factors for powers of 2 shape (has a default value for other shapes) + TODO : Improve this function to produce binning factors for any integer. + + Parameters + ---------- + size : int + size of the data array to be binned. e.g. data.shape[0] + + Returns + ------- + bin_list : list[int] + The list of binning factors + + Notes + ----- + For example `get_bin_list(256)` will return [1,2,4,8,16,32,64,128,256] + """ + bin_list = [] + # Either we get the power of two of the input size (integer) or the input number is not a power of two. + power = math.log(size,2) + if power.is_integer() : + for i in range(round(power + 1)) : + bin_list.append(size//(2**i)) + else : + bin_list = [size,1] + # Better to reverse for display reasons + bin_list.reverse() + return bin_list \ No newline at end of file diff --git a/src/pymodaq_plugins_asi/hardware/cheetah3.py b/src/pymodaq_plugins_asi/hardware/cheetah3.py index f8acb07..c536c18 100644 --- a/src/pymodaq_plugins_asi/hardware/cheetah3.py +++ b/src/pymodaq_plugins_asi/hardware/cheetah3.py @@ -1,12 +1,16 @@ -import requests +from io import BytesIO import json -from pymodaq_plugins_asi.utils import Config +import time +import threading + +import requests from pymodaq_utils.logger import set_logger, get_module_name from pint import Quantity from PIL import Image -from io import BytesIO import numpy as np +from pymodaq_plugins_asi.utils import Config + logger = set_logger(get_module_name(__file__)) ################ @@ -21,6 +25,8 @@ # II. 4. Cheetah3 start/stop functions # III. Local testing code +config = Config() + ############################ # I. Cheetah3 config class # ############################ @@ -36,7 +42,6 @@ class Cheetah3Config : :destination: A dictionnary containing the serval-readable destinations for the serval data outputs. """ - def __init__(self): """ Reads and stores values of the config_cheetah3.toml file from the user preference folder. @@ -47,7 +52,6 @@ def __init__(self): Result ------ """ - self.config = Config() self.build_destination() def build_destination(self, destination_names = ['live_preview']) -> None : @@ -66,7 +70,7 @@ def build_destination(self, destination_names = ['live_preview']) -> None : """ self.destination = dict() for destination_name in destination_names : - self.destination.update(self.config['CHEETAH3']['destinations'][destination_name]) + self.destination.update(config('CHEETAH3','destinations',destination_name)) def add_destination(self, destination : dict, destination_name = '') -> None : """ @@ -85,8 +89,8 @@ def add_destination(self, destination : dict, destination_name = '') -> None : """ self.destination.update(destination) if len(destination_name) > 0 : - self.config['CHEETAH3']['destinations'][destination_name].update(destination) - self.config.save() + config('CHEETAH3','destinations',destination_name).update(destination) + config.save() def destination_names_list(self) -> list[str] : """ @@ -98,7 +102,7 @@ def destination_names_list(self) -> list[str] : :profile_name_list: list of the destination profiles. """ profile_name_list = [] - for key in self.config["CHEETAH3"]["destinations"] : + for key in config("CHEETAH3","destinations") : profile_name_list.append(key) return profile_name_list @@ -116,10 +120,10 @@ def add_bpc_file(self, file_path : str) -> None : None """ - bpc_files = self.config["CHEETAH3"]["file_paths"]['bpc'] + bpc_files = config("CHEETAH3","file_paths",'bpc') bpc_files.append(file_path) - self.config["CHEETAH3"]["file_paths"]['bpc'] = bpc_files - self.config.save() + config("CHEETAH3","file_paths",'bpc') = bpc_files + config.save() def add_dacs_file(self, file_path : str) -> None : """ @@ -135,10 +139,10 @@ def add_dacs_file(self, file_path : str) -> None : None """ - dacs_files = self.config["CHEETAH3"]["file_paths"]['dacs'] + dacs_files = config("CHEETAH3","file_paths",'dacs') dacs_files.append(file_path) - self.config["CHEETAH3"]["file_paths"]['dacs'] = dacs_files - self.config.save() + config("CHEETAH3","file_paths",'dacs') = dacs_files + config.save() def add_save_folder(self, folder_path : str) -> None : """ @@ -154,10 +158,17 @@ def add_save_folder(self, folder_path : str) -> None : None """ - save_folders = self.config["CHEETAH3"]["file_paths"]['data'] + save_folders = config("CHEETAH3","file_paths",'data') save_folders.append(folder_path) - self.config["CHEETAH3"]["file_paths"]['dacs'] = save_folders - self.config.save() + config("CHEETAH3","file_paths",'data') = save_folders + config.save() + + def refresh(self) : + """ + Recreates a Config object so that updates to the file are accessible. + """ + global config + config = Config() ################################# # II. Cheetah3 controller class # @@ -192,10 +203,11 @@ def __init__(self): """ Instantiate the camera object that controls the hardware through Serval. """ - self.config = Cheetah3Config() - self.serverurl = self.config.config['CHEETAH3']['connection']['serverurl'] + self.cheetah3_config = Cheetah3Config() + self.serverurl = config('CHEETAH3','connection','serverurl') self.dashboard = self.get_dashboard() self.detector_config = self.get_detector_config() + self.detector_info = self.get_detector_info() self._bpc_file = None self._dacs_file = None self._save_folder = None @@ -203,7 +215,9 @@ def __init__(self): self._readout_time = Quantity('10ms') self._ntriggers = 1 self._destination_profiles = ['live_preview'] - + self._x_size = None + self._y_size = None + self._start_time = 0.0 ####################################### # II. 1. `requests` generic functions # @@ -225,9 +239,9 @@ def get_request(self, url : str, expected_status=200) -> requests.Response: :Response: Response object from the server """ - response = requests.get(url=url) + response = requests.get(url=url, timeout=10.0) if response.status_code != expected_status: - raise Exception("Failed GET request: {}, response: {} {}".format(url, response.status_code, response.text)) + raise BrokenPipeError("Failed GET request: %s, response: %s %s",url, response.status_code, response.text) return response @@ -249,9 +263,9 @@ def put_request(self, url : str, data : str, expected_status=200) -> requests.Re :Response: Response object from the server """ - response = requests.put(url=url, data=data) + response = requests.put(url=url, data=data, timeout=10.0) if response.status_code != expected_status: - raise Exception("Failed PUT request: {}, response: {} {}".format(url, response.status_code, response.text)) + raise BrokenPipeError("Failed PUT request: %s, response: %s %s",url, response.status_code, response.text) return response @@ -299,6 +313,19 @@ def get_detector_config(self) -> dict : response = self.get_request(url=self.serverurl + '/detector/config') detector_config = json.loads(response.text) return detector_config + + def get_detector_info(self) -> dict : + """ + Gets the Cheetah3 detector info. + + Results + ------- + + :detector_config: Dictionnary of the current configuration of the detector. + """ + response = self.get_request(url=self.serverurl + '/detector/info') + detector_config = json.loads(response.text) + return detector_config def load_pixel_config(self) -> None: """ @@ -310,11 +337,11 @@ def load_pixel_config(self) -> None: """ # load a binary pixel configuration exported by SoPhy, the file should exist on the server response = self.get_request(url=self.serverurl + '/config/load?format=pixelconfig&file=' + self.bpc_file) - logger.debug(f'Response of loading binary pixel configuration file:{response.text}' ) + logger.debug('Response of loading binary pixel configuration file: %s',response.text ) # .... and the corresponding DACs file response = self.get_request(url=self.serverurl + '/config/load?format=dacs&file=' + self.dacs_file) - logger.debug(f'Response of loading DACs file: {response.text}') + logger.debug('Response of loading DACs file: %s',response.text) def set_detector_config(self,trigger_mode = 'continuous',ntriggers=1,trigger_period=0.5) -> None : """ @@ -382,9 +409,11 @@ def set_destination(self, profile_list : list[str]) -> None: Sets the destination of the data """ for profile in profile_list : - assert profile in self.config.destination_names_list(), f"You have to first add this profile : {profile} to the available list of profiles : {self.config.destination_names_list()}" - self.config.build_destination(profile_list) - self.put_request(url = self.serverurl + '/server/destination', data = json.dumps(self.config.destination)) + assert profile in self.cheetah3_config.destination_names_list(), f"You have to first add this profile : {profile} to the available list of profiles : {self.cheetah3_config.destination_names_list()}" + self.cheetah3_config.build_destination(profile_list) + if 'Preview' in self.cheetah3_config.destination.keys() : + self.cheetah3_config.destination['Preview']['Period'] = max(self.exposure_time.magnitude,0.05) + self.put_request(url = self.serverurl + '/server/destination', data = json.dumps(self.cheetah3_config.destination)) ############################## # II. 3. Cheetah3 properties # @@ -393,50 +422,53 @@ def set_destination(self, profile_list : list[str]) -> None: @property def bpc_file(self) -> str: if self._bpc_file is None : - self._bpc_file = self.config.config['CHEETAH3']['file_paths']['bpc'][0] + self._bpc_file = config('CHEETAH3','file_paths','bpc')[0] return self._bpc_file @bpc_file.setter def bpc_file(self, filename : str) -> None : - if filename in self.config.config['CHEETAH3']['file_paths']['bpc'] : + if filename in config('CHEETAH3','file_paths','bpc') : self._bpc_file = filename else : - logger.info(f'the bpc file : {filename} is not part of the available files.') + logger.info('the bpc file : %s is not part of the available files.',filename) @property def dacs_file(self) -> str : if self._dacs_file is None : - self._dacs_file = self.config.config['CHEETAH3']['file_paths']['dacs'][0] + self._dacs_file = config('CHEETAH3','file_paths','dacs')[0] return self._dacs_file @dacs_file.setter def dacs_file(self, filename : str) -> None : - if filename in self.config.config['CHEETAH3']['file_paths']['dacs'] : + if filename in config('CHEETAH3','file_paths','dacs') : self._dacs_file = filename else : - logger.info(f'the dacs file : {filename} is not part of the available files.') + logger.info('the dacs file : %s is not part of the available files.', filename) @property def save_folder(self) -> str : if self._save_folder is None : - self._save_folder = self.config.config['CHEETAH3']['file_paths']['data'][0] + self._save_folder = config('CHEETAH3','file_paths','data')[0] return self._save_folder @save_folder.setter def save_folder(self, folder_name : str) -> None : - if folder_name in self.config.config['CHEETAH3']['file_paths']['data'] : + if folder_name in config('CHEETAH3','file_paths','data') : self._save_folder = folder_name else : - logger.info(f'the dacs file : {folder_name} is not part of the available files.') + logger.info('the dacs file : %s is not part of the available files.', folder_name) @property - def exposure_time (self) -> float : + def exposure_time (self) -> Quantity : return self._exposure_time.to('s') @exposure_time.setter - def exposure_time(self,value : float) -> None : - q = Quantity(value,'s') - self._exposure_time = q.to('s') + def exposure_time(self,value) -> None : + if isinstance(value,str) : + q = Quantity(value) + else : + q = Quantity(value,'s') + self._exposure_time = q @property def ntriggers(self) -> int : @@ -453,21 +485,55 @@ def destination_profiles(self) -> list[str] : @destination_profiles.setter def destination_profiles(self,value : list[str]) -> None : self._destination_profiles = value + + @property + def x_size(self) -> int : + if self._x_size is None : + self._x_size = self.detector_info["PixCount"]//self.detector_info["NumberOfRows"] + return self._x_size + + @property + def y_size(self) -> int : + if self._y_size is None : + self._y_size = self.detector_info["NumberOfRows"] + return self._y_size ######################################## # II. 4. Cheetah3 start/stop functions # ######################################## - - def start(self): + + def count_time(self,timeout : float) : + while True : + current_time = time.time() + if (current_time - self._start_time) > timeout : + self.stop() + break + + def start(self,timeout : float = 0.0): """Perform acquisition Keyword arguments: serverurl -- the URL of the running SERVAL (string) + + Parameters + ---------- + timeout : float + time until camera stop is automatically called """ - self.set_detector_config(ntriggers=self.ntriggers, trigger_mode='automatic') - self.set_destination(profile_list=self.destination_profiles) - response = self.get_request(url=self.serverurl + '/measurement/start') - logger.info('Response of acquisition start: ' + response.text) + if self.get_status() == "DA_RECORDING" : + self._start_time = time.time() + else : + self.set_detector_config(ntriggers=self.ntriggers, trigger_mode='automatic') + self.set_destination(profile_list=self.destination_profiles) + response = self.get_request(url=self.serverurl + '/measurement/start') + logger.info('Response of acquisition start: %s', response.text) + if timeout > 0.0 : + self._start_time = time.time() + timer = threading.Thread(target=self.count_time, args=(timeout,)) + timer.start() + # The snap mode of the grab_data method of the DAQ viewer would technically call many times start and stop + # Since the camera takes some time to start and stop, it is better to stop only after a timeout. + # In case of a DAQ scan, the snap is called repeatdly which can cause some issue if the camera is started/stopped too fast. def preview(self): """Preview of collected data @@ -490,32 +556,33 @@ def get_status(self) : else : return self.get_dashboard()["Measurement"]["Status"] - def wait_for_acq(self) : - while True : - status = self.get_status() - if status == "DA_RECORDING" : - return 1 - elif status == "DA_IDLE" : - pass - elif status == "DA_PREPARING" : - pass - elif status == "DA_STOPPING" : - return 0 - def stop(self) : response = self.get_request(url=self.serverurl + '/measurement/stop') data = response.text - logger.info('Response of acquisition stop : ' + data) + logger.info('Response of acquisition stop : %s',data) ########################### # III. Local testing code # ########################### if __name__ == '__main__' : - cc = Cheetah3Config() - cc.config - # cam = Cheetah3() - # cam.check_connection() + # cc = Cheetah3Config() + # cc.config + cam = Cheetah3() + cam.check_connection() + cam.ntriggers = 300 + cam.exposure_time = 2 + cam.start() + # print('started') + while True : + try : + cam.preview() + print(cam.get_status()) + except KeyboardInterrupt : + cam.stop() + print('stop') + break + # # cam.start_listening() # # print(cam.get_dashboard()) # # print(cam.bpc_file) diff --git a/src/pymodaq_plugins_asi/hardware/spim_cheetah3.py b/src/pymodaq_plugins_asi/hardware/spim_cheetah3.py new file mode 100644 index 0000000..5623561 --- /dev/null +++ b/src/pymodaq_plugins_asi/hardware/spim_cheetah3.py @@ -0,0 +1,69 @@ +from pymodaq_plugins_asi.hardware.cheetah3 import Cheetah3 +import json +import socket +import time + +class Tp3toolsConfig: + + def __init__(self): + self.bin = False + self.bytedepth = 0 + self.cumul = False + self.mode = 0 + self.xspim_size = 0 + self.yspim_size = 0 + self.xscan_size = 0 + self.yscan_size = 0 + self.pixel_time = 0 + self.time_delay = 0 + self.time_width = 0 + self.time_resolved = False + self.save_locally = False + self.pixel_mask = 0 + self.video_time = 0 + self.threshold = 0 + self.bias_voltage = 0 + self.destination_port = 0 + self.acquisition_us = 1000 #1 ms + self.sup0 = 0.0 + self.sup1 = 0.0 + #self.__custom_meas = False + #self.__custom_shape = None + + + + def create_configuration_bytes(self): + return json.dumps(self.__dict__).encode() + + +class Tp3toolsCheetah3(Cheetah3) : + + def __init__(self): + self.tp3config = Tp3toolsConfig() + super().__init__() + + def acquistion(self) : + + self.tp3config.mode = 2 + self.tp3config.bytedepth = 4 + self.ntriggers = 500 + self.set_detector_config(ntriggers=self.ntriggers, trigger_mode='automatic') + self.set_destination(['tp3_tools']) + response = self.get_request(url=self.serverurl + '/measurement/start') + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + address = ('10.35.44.20',8088) + client.connect(address) + client.send(self.tp3config.create_configuration_bytes()) + while True : + try : + # time.sleep(0.1) + data = bytearray(client.recv(128)) + print(data) + except KeyboardInterrupt : + print('stop') + break + + +if __name__ == '__main__' : + t = Tp3toolsCheetah3() + t.acquistion() \ No newline at end of file diff --git a/src/pymodaq_plugins_asi/resources/config_cheetah3.toml b/src/pymodaq_plugins_asi/resources/config_template.toml similarity index 84% rename from src/pymodaq_plugins_asi/resources/config_cheetah3.toml rename to src/pymodaq_plugins_asi/resources/config_template.toml index 586a949..38b27c1 100644 --- a/src/pymodaq_plugins_asi/resources/config_cheetah3.toml +++ b/src/pymodaq_plugins_asi/resources/config_template.toml @@ -30,5 +30,14 @@ title = 'ASI Cheetah3 configuration file' {Base = "file:/data/raw", FilePattern = "f%Hms_", SplitStrategy = "FRAME", QueueSize = 16384} ] + [CHEETAH3.destinations.tp3_tools] + + Raw = [ + {Base = "tcp://connect@127.0.0.1:8098"} + ] + + + + diff --git a/src/pymodaq_plugins_asi/utils.py b/src/pymodaq_plugins_asi/utils.py index c48453a..4981b95 100644 --- a/src/pymodaq_plugins_asi/utils.py +++ b/src/pymodaq_plugins_asi/utils.py @@ -11,5 +11,5 @@ class Config(BaseConfig): """Main class to deal with configuration values for this plugin""" - config_template_path = Path(__file__).parent.joinpath('resources/config_cheetah3.toml') + config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml') config_name = f"config_{__package__.split('pymodaq_plugins_')[1]}"