class Driver(): """Driver class""" def reset(self): """ Create factory reset""" self.device.write('*RST') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class :gpib_address: GPIB-address of spectrum analyzer, e.g. 'GPIB0::12::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: And instance of a LogClient """ # Instantiate log self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # reset to factory settings self.reset() def turn_laser_off(self): """ Power off display """ self.device.write('OUTPut1:STATe 0') # temp control self.device.write('OUTPut2:STATe 0') # diode self.log.info("Laser off.") def turn_laser_on(self): """ Power on display """ self.device.write('OUTPut2:STATe 1') # temp control self.device.write('OUTPut1:STATe 1') # diode self.log.info("Laser on.") def set_current(self, amps): """ Sets current setpoint in mA :amps: """ if not 0 <= amps <= 60: self.log.error( f'Invalid current ({amps}mA). Attenuation must be between 0A and 60mA' ) self.device.write(f'SOURce1:CURRent:LEVel:AMPLitude {amps*1e-3}') self.log.info(f'Current setpoint set to {amps}mA.')
class StaticLineGUIGeneric(): """Static Line GUI :config: (str) Path to a config file specifying the staticlines to be included in this GUI. :staticline_clients: (dict) Dictionary of {hardware type (str) : instance of device Client}. The hardware type must be same as those listed in the config files. :logger_client: (object) Instance of logger client. """ def __init__(self, config, staticline_clients=None, logger_client=None, host=None, port=None): self.log = LogHandler(logger=logger_client) self.config = config self.host = host self.port = port self.config_dict = load_script_config('staticline', config, logger=self.log) self.initialize_drivers(staticline_clients, logger_client) def initialize_drivers(self, staticline_clients, logger_client): # Dictionary storing {device name : dict of staticline Drivers} self.staticlines = {} for device_name, device_params in self.config_dict['lines'].items(): # If the device name is duplicated, we ignore this hardware client. if device_name in self.staticlines: self.log.error(f"Device name {device_name} has been matched to multiple hardware clients." "Subsequent matched hardware clients are ignored.") continue # Create a dict to store the staticlines for this device else: self.staticlines[device_name] = dict() hardware_type = device_params['hardware_type'] hardware_config = device_params['config_name'] #Try to find if we have a matching device client in staticline_clients try: hardware_client = find_client(staticline_clients, self.config_dict, hardware_type, hardware_config, logger_client) if (hardware_client == None): logger_client.error('No staticline device found for device name: ' + device_name) hardware_client_found = False else: hardware_client_found = True except NameError: logger_client.error('No staticline device found for device name: ' + device_name) hardware_client_found = False # Iterate over all staticlines for that device and create a # driver instance for each line. if (hardware_client_found): for staticline_idx in range(len(device_params["staticline_names"])): staticline_name = device_params["staticline_names"][staticline_idx] # Store the staticline driver under the specified device name self.staticlines[device_name][staticline_name] = staticline.Driver( name=device_name + "_" + staticline_name, logger=logger_client, hardware_client=hardware_client, hardware_type=hardware_type, config=device_params["staticline_configs"][staticline_idx] ) #If it has an initial default value, set that initially using set_value command if "default" in device_params["staticline_configs"][staticline_idx]: defaultValue = device_params["staticline_configs"][staticline_idx]["default"] sl_type = device_params["staticline_configs"][staticline_idx]['type'] if (sl_type == 'analog'): self.staticlines[device_name][staticline_name].set_value(defaultValue) elif (sl_type == 'adjustable_digital'): self.staticlines[device_name][staticline_name].set_dig_value(defaultValue) else: #Didn't find the hardware client, so will remove the entry from the staticline dictionary so that the GUI is not updated self.staticlines.pop(device_name, None) def initialize_buttons(self): """Binds the function of each button of each device to the functions set up by each the device's staticline driver. """ def set_value_fn(driver, text_widget): """ Helper function that we use to bind to text buttons for analog inputs, in order to avoid lambda scoping issues. """ return lambda: driver.set_value(text_widget['AIN'].text()) def set_dig_value_fn(driver, text_widget): """ Helper function that we use to bind to text buttons for adjustable digital inputs, in order to avoid lambda scoping issues. """ return lambda: driver.set_dig_value(text_widget['AIN'].text()) # Iterate through all devices in the config file for device_name, device_params in self.config_dict['lines'].items(): if type(device_params) != dict: continue if (not device_name in self.staticlines): self.log.error('No button initialization for staticline device: ' + device_name) continue for staticline_idx in range(len(device_params["staticline_names"])): staticline_name = device_params["staticline_names"][staticline_idx] staticline_driver = self.staticlines[device_name][staticline_name] staticline_configs = device_params["staticline_configs"][staticline_idx] staticline_type = staticline_configs["type"] # Digital: Have an up and down button if staticline_type == 'digital': widget = self.widgets[device_name][staticline_name] widget['on'].clicked.connect(staticline_driver.up) widget['off'].clicked.connect(staticline_driver.down) # Analog: "Apply" does something based on the text field value elif staticline_type == 'analog': widget = self.widgets[device_name][staticline_name] # Cannot use a lambda directly because this would lead to # the values of staticline_driver and widget being referenced # only at time of button click. widget['apply'].clicked.connect( set_value_fn(staticline_driver, widget)) #Set text of analog to default value if "default" in staticline_configs: #set initial text to initial value specified in config file widget['AIN'].setText(str(staticline_configs["default"])) self.gui.upd_cur_val(device_name=device_name, staticline_name=staticline_name) # Have both types of buttons elif staticline_type == 'adjustable_digital': analog_widget = self.widgets[device_name][staticline_name + "_analog"] digital_widget = self.widgets[device_name][staticline_name + "_digital"] digital_widget['on'].clicked.connect(staticline_driver.up) digital_widget['off'].clicked.connect(staticline_driver.down) analog_widget['apply'].clicked.connect( set_dig_value_fn(staticline_driver, analog_widget)) #Set text of analog to default value if "default" in staticline_configs: #set initial text to initial value specified in config file analog_widget['AIN'].setText(str(staticline_configs["default"])) self.gui.upd_cur_val(device_name=device_name, staticline_name=staticline_name + "_analog") else: self.log.error(f'Invalid staticline type for device {device_name}. ' 'Should be analog or digital.') def run(self): """Starts up the staticline GUI and initializes the buttons. """ # Starts up an application for the window if get_os() == 'Windows': ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('pylabnet') self.app = QtWidgets.QApplication(sys.argv) self.app.setWindowIcon( QtGui.QIcon(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'devices.ico')) ) # Create a GUI window with layout determined by the config file self.gui = GUIWindowFromConfig(config=self.config, host=self.host, port=self.port, staticlines=self.staticlines) self.gui.show() self.widgets = self.gui.widgets # Binds the function of the buttons to the staticline Driver functions self.initialize_buttons() self.app.exec_()
class Driver: def __init__(self, gpib_address=None, logger=None): """Instantiate driver class. :gpib_address: GPIB-address of the scope, e.g. 'GPIB0::12::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: An instance of a LogClient. """ # Instantiate log. self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {device_id}.") # We set a more forgiving timeout of 10s (default: 2s). # self.device.timeout = 10000 except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") def get_power(self, channel): """ Returns the current power in watts on a desired channel :param channel: (int) channel to read power of (either 1 or 2) :return: (float) power in watts """ power = self.device.query(f':POW{channel}:VAL?') return float(power) def get_wavelength(self, channel): """ Returns the current wavelength in nm for the desired channel :param channel: (int) channel to read wavelength of :return: (int) wavelength """ wavelength = self.device.query(f':WAVEL{channel}:VAL?') return int(float(wavelength)) def get_range(self, channel): """ Returns the current power range for the channel :param channel: (int) channel to read range of :return: (str) range """ pr = self.device.query(f':PRANGE{channel}?') return pr def set_wavelength(self, channel, wavelength): """ Sets the wavelength :param channel: (int) channel to set wavelength of """ self.device.write(f':WAVEL{channel}:VAL {wavelength}') def set_range(self, channel, p_range): """ Sets the range :param channel: (int) channel to set range of :param p_range: (str) range string identifier, can be anything in 'AUTO', 'R1NW', 'R10NW', 'R100NW', 'R1UW', 'R10UW', 'R100UW', 'R1MW', 'R10MW', 'R100MW', 'R1W', 'R10W', 'R100W', 'R1KW' """ self.device.write(f':PRANGE{channel} {p_range}')
class Driver(): def __init__(self, device_num, logger): """Instantiate driver class. device_num is numbering of devices connected via USB. Drivrt then finds serial numbers of polarization paddle by Driver, e.g. b'38154354' """ # Instantiate log. self.log = LogHandler(logger=logger) #Loads polarization contorolles DLL and define arguments and result 5types for c function self._polarizationdll = ctypes.cdll.LoadLibrary( 'Thorlabs.MotionControl.Polarizer.dll') self._devmanagerdll = ctypes.cdll.LoadLibrary( 'Thorlabs.MotionControl.DeviceManager.dll') self._configure_functions() #get device list size if self._polarizationdll.TLI_BuildDeviceList() == 0: num_devs = self._polarizationdll.TLI_GetDeviceListSize() #print(f"There are {num_devs} devices connected") #Get devices serial numbers serialNos = ctypes.create_string_buffer( 100) #the way to have a mutable buffer serialNosSize = ctypes.c_ulong(ctypes.sizeof(serialNos)) List = self._polarizationdll.TLI_GetDeviceListByTypeExt( serialNos, serialNosSize, 38) #if List: # print("Failed to get device list") #else: # print("Device list created succesfully") #change these massages to interact with logger self.dev_name = serialNos.value.decode("utf-8") #.strip().split(',') #print(f"Connected to device {self.dev_name}") #get device info including serial number self.device_info = TLI_DeviceInfo() # container for device info self._polarizationdll.TLI_GetDeviceInfo( serialNos[(device_num - 1) * 9:(device_num * 9) - 1], ctypes.byref(self.device_info) ) #when there will be a few devices figure out how to seperate and access each one self.device = serialNos[(device_num - 1) * 9:(device_num * 9) - 1] #print("Description: ", self.device_info.description) #print("Serial No: ", self.device_info.serialNo) #print("Motor Type: ", self.device_info.motorType) #print("USB PID: ", self.device_info.PID) #print("Max Number of Paddles: ", self.device_info.maxPaddles) #establising connection to device self.paddles = [paddle1, paddle3, paddle2] connection = self._polarizationdll.MPC_Open(self.device) if connection == 0: self.log.info(f"Successfully connected to {self.device}.") else: self.log.error( f"Connection to {self.device} failed due to error {connection}." ) #technical methods def _configure_functions(self): """ Defines arguments and results for c functions """ self._polarizationdll.TLI_BuildDeviceList.argtype = None self._polarizationdll.TLI_BuildDeviceList.restype = ctypes.c_short self._polarizationdll.TLI_GetDeviceListSize.argtype = None self._polarizationdll.TLI_GetDeviceListSize.restype = ctypes.c_short self._polarizationdll.TLI_GetDeviceInfo.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.POINTER(TLI_DeviceInfo) ] self._polarizationdll.TLI_GetDeviceInfo.restype = ctypes.c_short self._polarizationdll.TLI_GetDeviceListByTypeExt.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.c_ulong, ctypes.c_int ] self._polarizationdll.TLI_GetDeviceListByTypeExt.restype = ctypes.c_short self._polarizationdll.MPC_Open.argtype = ctypes.POINTER(ctypes.c_char) self._polarizationdll.MPC_Open.restype = ctypes.c_short self._polarizationdll.MPC_Close.argtype = ctypes.POINTER(ctypes.c_char) self._polarizationdll.MPC_Close.restype = ctypes.c_short self._polarizationdll.MPC_CheckConnection.argtype = ctypes.c_char_p self._polarizationdll.MPC_CheckConnection.restype = ctypes.c_bool self._polarizationdll.MPC_GetPosition.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles ] self._polarizationdll.MPC_GetPosition.restype = ctypes.c_double self._polarizationdll.MPC_RequestPolParams.argtype = ctypes.POINTER( ctypes.c_char) self._polarizationdll.MPC_RequestPolParams.restype = ctypes.c_short self._polarizationdll.MPC_GetPolParams.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.POINTER(TLI_PolarizerParameters) ] self._polarizationdll.MPC_GetPolParams.restype = ctypes.c_short self._polarizationdll.MPC_SetPolParams.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.POINTER(TLI_PolarizerParameters) ] self._polarizationdll.MPC_SetPolParams.restype = ctypes.c_short self._polarizationdll.MPC_SetJogSize.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles, ctypes.c_double ] self._polarizationdll.MPC_SetJogSize.restype = ctypes.c_short self._polarizationdll.MPC_Jog.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles, MOT_TravelDirection ] self._polarizationdll.MPC_Jog.restype = ctypes.c_short self._polarizationdll.MPC_GetMaxTravel.argtype = ctypes.POINTER( ctypes.c_char) self._polarizationdll.MPC_GetMaxTravel.restype = ctypes.c_double self._polarizationdll.MPC_MoveToPosition.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles, ctypes.c_double ] self._polarizationdll.MPC_MoveToPosition.restype = ctypes.c_short self._polarizationdll.MPC_Stop.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles ] self._polarizationdll.MPC_Stop.restype = ctypes.c_short self._polarizationdll.MPC_Home.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles ] self._polarizationdll.MPC_Home.restype = ctypes.c_short self._polarizationdll.MPC_Jog.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.POINTER(TLI_PolarizerParameters), MOT_TravelDirection ] self._polarizationdll.MPC_Jog.restype = ctypes.c_short self._polarizationdll.MPC_StartPolling.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.c_int ] self._polarizationdll.MPC_StartPolling.restype = ctypes.c_bool self._polarizationdll.MPC_StopPolling.argtype = ctypes.POINTER( ctypes.c_char) self._polarizationdll.MPC_StopPolling.restype = ctypes.c_void_p #did not find the a c_void with no pointer as needed self._polarizationdll.MPC_SetVelocity.argtypes = [ ctypes.POINTER(ctypes.c_char), ctypes.c_short ] self._polarizationdll.MPC_SetVelocity.restype = ctypes.c_short self._polarizationdll.MPC_MoveRelative.argtypes = [ ctypes.POINTER(ctypes.c_char), POL_Paddles, ctypes.c_double ] self._polarizationdll.MPC_MoveRelative.restype = ctypes.c_short self._polarizationdll.MPC_GetStepsPerDegree.argtype = [ ctypes.POINTER(ctypes.c_char) ] self._polarizationdll.MPC_GetStepsPerDegree.result = ctypes.c_double #wrap function for external use def open(self): result = self._polarizationdll.MPC_Open(self.device) if result == 0: print("Connected succesfully to device") else: print("A problem occured when trying to connect to device") def close(self): resultc = self._polarizationdll.MPC_Close(self.device) if resultc == 0: print("Closed connection to device") else: print("A problem occured when trying to diconnect from device") def home(self, paddle_num): home_result = self._polarizationdll.MPC_Home(self.device, self.paddles[paddle_num]) return home_result def set_velocity(self, velocity): velocity = self._polarizationdll.MPC_SetVelocity(self.device, velocity) def move(self, paddle_num, pos, sleep_time): #posinitial = self._polarizationdll.MPC_GetPosition(self.device, self.paddles[paddle_num]) move_result = self._polarizationdll.MPC_MoveToPosition( self.device, self.paddles[paddle_num], pos) time.sleep(abs(sleep_time * pos / 170)) #posfinal = self._polarizationdll.MPC_GetPosition(self.device, self.paddles[paddle_num]) return move_result #, posinitial, posfinal def move_rel(self, paddle_num, step, sleep_time): #posinitial = self._polarizationdll.MPC_GetPosition(self.device, self.paddles[paddle_num]) move_result = self._polarizationdll.MPC_MoveRelative( self.device, self.paddles[paddle_num], step) time.sleep(abs(sleep_time * step / 170)) #posfinal = self._polarizationdll.MPC_GetPosition(self.device, self.paddles[paddle_num]) return move_result #, posinitial, posfinal def get_angle(self, paddle_num): currentpos = self._polarizationdll.MPC_GetPosition( self.device, self.paddles[paddle_num]) return currentpos
class Driver(): def __init__(self, name, logger, hardware_client, hardware_type, config): '''High level staticline class. This class is used in conjunction with hardware modules to send out digital signals ('voltage low' and 'voltage high'). This top level class is hardware agnostic. With the use of a StaticLineHardwareHandler, this class will be associated with the necessary setup functions and output functions of a hardware module. :name:(str) A easily recognizable name for this staticline, ideally referring to the device being controlled by it, e.g. 'Shutter 1'. :logger: (object) An instance of a LogClient. :hardware_client: (object) An instance of a hardware Client. :hardware_type: (str) Name of the hardware to be controlled, naming is determined by the device server name. :config: (dict) Contains parameters needed to setup the hardware as a staticline. ''' self.name = name self.log = LogHandler(logger=logger) # Check that the provided class is a valid StaticLine class if hardware_type not in registered_staticline_modules: self.log.error( f"Setup of staticline using module {hardware_type} failed.\n" f"Compatible modules are: {registered_staticline_modules.keys()}" ) # Acquire the correct handler for the hardware type HardwareHandler = registered_staticline_modules[hardware_type] # Instantiate hardware handler. The hardware_handler will handle any # calls to the staticline functions like up/down. self.hardware_handler = HardwareHandler( name=name, log=self.log, hardware_client=hardware_client, config=config) def up(self): '''Set output to high.''' self.hardware_handler.up() self.log.info(f"Staticline {self.name} set to high.") def down(self): '''Set output to low.''' self.hardware_handler.down() self.log.info(f"Staticline {self.name} set to low.") def set_dig_value(self, value): '''Sets output level for adjustable digital values''' self.hardware_handler.set_dig_value(value) self.log.info( f"Staticline {self.name} adjustable output set to {value}.") def set_value(self, value): '''Set output to a specified value.''' self.hardware_handler.set_value(value) self.log.info(f"Staticline {self.name} set to {value}.") def get_name(self): return self.name
class MCS2: # property keys and other constants PKEY_NUM_BUS = int("0x020F0016", 16) PKEY_NUM_CH = int('0x020F0017', 16) PKEY_NUM_BUS_CH = int('0x02030017', 16) PKEY_MOVE_MODE = int('0x03050087', 16) PKEY_STEP_FREQ = int('0x0305002E', 16) PKEY_STEP_AMP = int('0x03050030', 16) PKEY_VOLT = int('0x0305001F', 16) PKEY_DC_VEL = int('0x0305002A', 16) PKEY_CURRENT_STATE = int('0x0305000F', 16) MOVE_MODE_STEP = 4 MOVE_MODE_DC_REL = 3 MOVE_MODE_DC_ABS = 2 SCALE = 65535 DC_VEL_MIN = 100 / 56635 DC_VEL_MAX = 10**8 def __init__(self, logger=None): """ Instantiate Nanopositioners""" self.log = LogHandler(logger) self.dummy = False try: # Loads Nanopositioners DLL and define arguments and result types for c function self._nanopositionersdll = ctypes.windll.LoadLibrary( 'SmarActCTL.dll') self._configure_functions() #Finds devices connected to controller buffer = ctypes.create_string_buffer( 4096) #the way to have a mutable buffer buffersize = ctypes.c_size_t(ctypes.sizeof( buffer)) #size _t gives c_ulonglong, not as in manual result = self._nanopositionersdll.SA_CTL_FindDevices( None, buffer, buffersize) # Handle errors if result: msg_str = 'No MCS2 devices found' self.log.error(msg_str) #Establishes a connection to a device self.dev_name = buffer.value.decode("utf-8") dhandle = ctypes.c_uint32() connect = self._nanopositionersdll.SA_CTL_Open( dhandle, buffer.value, None) if connect == 0: self.dhandle = dhandle.value self.log.info( f'Connected to device {self.dev_name} with handle {self.dhandle}' ) else: msg_str = f'Failed to connect to device {self.dev_name}' self.log.error(msg_str) # Get channel information channel_buffer = ctypes.c_int32() channel_buffer_size = ctypes.c_size_t( ctypes.sizeof(channel_buffer)) channel_result = self._nanopositionersdll.SA_CTL_GetProperty_i32( self.dhandle, 0, self.PKEY_NUM_CH, channel_buffer, channel_buffer_size) self.num_ch = channel_buffer.value if channel_result == 0 and self.num_ch > 0: self.log.info( f'Found {self.num_ch} channels on {self.dev_name}') else: msg_str = f'Failed to find channels on {self.dev_name}' self.log.error(msg_str) except WindowsError: self.log.warn('Did not find MCS2 DLL, entering dummy mode') self.dummy = True def close(self): """ Closes connection to device""" connect = self._nanopositionersdll.SA_CTL_Close(self.dhandle) if connect: msg_str = f'Failed to properly close connection to {self.dev_name}' self.log.warn(msg_str) else: self.log.info(f'Disconnected from {self.dev_name}') def set_parameters(self, channel, mode=None, frequency=None, amplitude=None, dc_vel=None): """ Sets parameters for motion Leave parameter as None in order to leave un-changed :param channel: (int) index of channel from 0 to self.num_ch :param mode: (str) default is 'step', can use 'dc', 'dc_rel' to set abs or rel DC voltage :param freq: (int) frequency in Hz from 1 to 20000 :param amp: (float) amplitude in volts from 0 to 100 :param dc_vel: (float) velocity for DC steps in volts/sec """ # Set movement mode if mode is not None: if mode == 'dc': result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_DC_ABS) if result_mode: self.log.warn( f'Failed to set mode to DC for positioner {self.dev_name},' f' channel {channel}') elif mode == 'dc_rel': result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_DC_REL) if result_mode: self.log.warn( f'Failed to set mode to DC for positioner {self.dev_name},' f' channel {channel}') else: result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_STEP) if result_mode: self.log.warn( f'Failed to set mode to step for positioner {self.dev_name},' f' channel {channel}') # Set frequency if frequency is not None: # Check for reasonable range if 1 <= frequency <= 20000: result_freq = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_STEP_FREQ, int(frequency)) if result_freq: self.log.warn( f'Failed to set step frequency to {frequency} for positioner ' f'{self.dev_name}, channel {channel}') # Handle out of range request else: self.log.warn( 'Warning, can only set frequency within 1 Hz and 20 kHz') # Set amplitude if amplitude is not None: # Check for reasonable range bit_amp = value_to_bitval(amplitude, bits=16, min=0, max=100) if 1 <= bit_amp <= 65535: result_amp = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_STEP_AMP, bit_amp) if result_amp: self.log.warn( f'Failed to set step amplitude to {amplitude} for positioner ' f'{self.dev_name}, channel {channel}') else: self.log.warn( 'Warning, can only set amplitude in the range of 0 to 100 V' ) # Set DC velocity if dc_vel is not None: # Check for reasonable range bit_vel = int(dc_vel * (self.SCALE / 100)) if 1 <= bit_vel <= self.SCALE * 10**6: result_vel = self._nanopositionersdll.SA_CTL_SetProperty_i64( self.dhandle, channel, self.PKEY_DC_VEL, bit_vel) if result_vel: self.log.warn( f'Failed to set DC velocity to {dc_vel} V/s for positioner ' f'{self.dev_name}, channel {channel}') else: self.log.warn('Warning, can only set velocity in the range of ' f'{self.DC_VEL_MIN} to {self.DC_VEL_MAX} V/s') def get_voltage(self, channel): """ Returns the current DC voltage on a piezo :param channel: (int) channel index (from 0) """ voltage_buffer = ctypes.c_int64() voltage_buffer_size = ctypes.c_size_t(ctypes.sizeof(voltage_buffer)) voltage_result = self._nanopositionersdll.SA_CTL_GetProperty_i64( self.dhandle, channel, self.PKEY_VOLT, voltage_buffer, voltage_buffer_size) # Handle error if voltage_result: self.log.warn(f'Could not retrieve voltage for channel {channel}' f'on device {self.dev_name}') return bitval_to_value(voltage_buffer.value, bits=16, min=0, max=100) def set_voltage(self, channel, voltage=50): """ Sets an absolute voltage to the piezo :param channel: (int) channel index (from 0) :param voltage: (float) voltage to set from 0 to 100 V (default is 50) """ # Change the move mode to voltage absolute self.set_parameters(channel, mode='dc') # Move to desired voltage bit_voltage = value_to_bitval(voltage, bits=16, min=0, max=100) move_result = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, bit_voltage, 0) # Check success if move_result: self.log.warn( f'Failed to set DC voltage to {voltage} V on channel {channel} of {self.dev_name}' ) def n_steps(self, channel, n=1): """ Takes n steps :param channel: (int) channel index (from 0) :param n: (int) number of steps to take, negative is in opposite direction """ # Take the step self.set_parameters(channel, mode='step') result_step = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, n, 0) # Handle error if result_step: self.log.warn( f'Failed to take {n} steps on device {self.dev_name}, channel {channel}' ) def move(self, channel, backward=False): """ Takes the maximum number of steps (quasi continuous) :param channel: (int) channel index (from 0) :param backward: (bool) whether or not to step in backwards direction (default False) """ # Configure move self.set_parameters(channel, mode='step') if backward: MOVE_STEPS = -100000 else: MOVE_STEPS = 100000 # Send move command result_move = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, MOVE_STEPS, 0) # Handle error if result_move: self.log.warn( f'Failed to take move on device {self.dev_name}, channel {channel}' ) def stop(self, channel): """ Terminates any ongoing movement :param channel: (int) channel index (from 0) """ result_stop = self._nanopositionersdll.SA_CTL_Stop( self.dhandle, channel, 0) if result_stop: self.log.warn( f'Failed to stop movement on device {self.dev_name}, channel {channel}' ) def is_moving(self, channel): """ Returns whether or not the positioner is moving :param channel: (int) channel index (from 0) :return: (bool) true if moving """ # Get the state bit current_state_buffer = ctypes.c_int32() current_state_buffer_size = ctypes.c_size_t( ctypes.sizeof(current_state_buffer)) state_result = self._nanopositionersdll.SA_CTL_GetProperty_i32( self.dhandle, channel, self.PKEY_CURRENT_STATE, current_state_buffer, current_state_buffer_size) # Handle an error if state_result: self.log.warn( f'Failed to check if positioner {self.dev_name} is moving on' f'Channel {channel}') # Decode state bit if current_state_buffer.value % 2 == 0: return False else: return True # Technical methods def _configure_functions(self): """ Defines arguments and results for c functions """ # Device connection, disconnection self._nanopositionersdll.SA_CTL_GetFullVersionString.restype = ctypes.c_char_p self._nanopositionersdll.SA_CTL_FindDevices.argtypes = [ ctypes.c_char_p, # options for find procedure ctypes.POINTER( ctypes.c_char), # pointer to buffer for writing device locator ctypes.POINTER(ctypes.c_ulonglong ) # pointer to variable holding size of buffer ] self._nanopositionersdll.SA_CTL_FindDevices.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Open.argtypes = [ ctypes.POINTER( ctypes.c_uint32 ), # pointer to device handle for use in future calls ctypes.c_char_p, # device locator ctypes.c_char_p # config (unused) ] self._nanopositionersdll.SA_CTL_Open.restypes = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Close.argtype = ctypes.c_uint32 # device handle self._nanopositionersdll.SA_CTL_Close.restype = ctypes.c_uint32 # result status # Getting device properties self._nanopositionersdll.SA_CTL_GetProperty_i32.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.POINTER( ctypes.c_int32), # pointer to buffer where result is written ctypes.POINTER( ctypes.c_ulonglong ) # pointer to size of value buffer (number of elements) ] self._nanopositionersdll.SA_CTL_GetProperty_i32.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_GetProperty_i64.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.POINTER( ctypes.c_int64), # pointer to buffer where result is written ctypes.POINTER(ctypes.c_ulonglong ) # pointer to size of value buffer (# of elements) ] self._nanopositionersdll.SA_CTL_GetProperty_i64.restype = ctypes.c_uint32 # result status # Settting device properties self._nanopositionersdll.SA_CTL_SetProperty_i32.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.c_int32, # value to write ] self._nanopositionersdll.SA_CTL_SetProperty_i32.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_SetProperty_i64.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.c_int64 # value to write ] self._nanopositionersdll.SA_CTL_SetProperty_i64.restype = ctypes.c_uint32 # result status # Movement self._nanopositionersdll.SA_CTL_Move.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_int64, # move value ctypes.c_uint32 # transmit handle ] self._nanopositionersdll.SA_CTL_Move.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Stop.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32 # transmit handle ] self._nanopositionersdll.SA_CTL_Stop.restype = ctypes.c_uint32 # result status
class FW102CFilterWheel: def __init__(self, port_name, device_name, filters, logger=None): """Instantiate Hardware class for FW102c Filterwheel by instantiating a FW102C class :device_name: Readable name of device, e.g. 'Collection Filters' :port_name: Port name over which Filter wheel is connect via USB :filters: A dictionary where the keys are the numbered filter positions and the values are strings describing the filter, e.g. '4 ND' """ # Instantiate log self.log = LogHandler(logger=logger) # Retrieve name and filter options self.device_name = device_name self.filters = filters # Instantiate FW102C self.filter_wheel = FW102C(port=port_name, logger=self.log) if not self.filter_wheel.isOpen: self.log.error("Filterwheel {} connection failed".format( self.device_name)) else: self.log.info("Filterwheel {} connection successfully".format( self.device_name)) def get_name(self): return self.device_name def get_pos(self): """Returns current position of filterwheel """ return self.filter_wheel.query('pos?') def change_filter(self, new_pos, protect_shutter_client=None): """ Update filter wheel position :new_pos: Target filter wheel position 1-6 or 1-12 :protect_shutter_client: (optional) SC20Shutter instance. If provided, shutter will be closed during a filter change """ # Close protection shutter if open if protect_shutter_client is not None: # Check if shutter is initially open shutter_open = protect_shutter_client.get_is_open() if shutter_open: protect_shutter_client.close() # Update Position self.filter_wheel.command('pos={}'.format(new_pos)) successful_changed = False # Check if update was successful if int(self.get_pos()) == int(new_pos): self.log.info( "Filterwheel {device_name} changed to position {position} ({filter})" "".format(device_name=self.device_name, position=new_pos, filter=self.filters.get('{}'.format(new_pos)))) successful_changed = False # Open protection shutter if shutter was originally open if protect_shutter_client is not None and shutter_open: protect_shutter_client.open() else: self.log.error( "Filterwheel {device_name}: changing to position failed" "".format(device_name=self.device_name)) return successful_changed # returns filter dictionary def get_filter_dict(self): return self.filters
class ANC300: # compiled regular expression for finding numerical values in reply strings _reg_value = re.compile(r"\w+\s+=\s+(\w+)") def __init__(self, host, port=0, query_delay=0.001, passwd=None, limits=DEFAULT_LIMITS, logger=None): """ Instantiate ANC300 objcet. :param host: IP of telnet connection. :param port: Port of telnet connection. :param query_delay: Delay between queries (ins s). :param passwd: Telnet login password. :param limits: Voltage limit dictionary. :param logger: Log client. """ self.log = LogHandler(logger) self.query_delay = query_delay self.lastcommand = "" self.freq_lim = limits["freq_lim"] self.step_voltage_lim = limits["step_voltage_lim"] # Instantiate Telnet Connection self.connection = Telnet(host, port) # Setup terminations self.read_termination = '\r\n' self.write_termination = self.read_termination # Log into telnet client time.sleep(query_delay) ret = self._read(check_ack=False) self._write(passwd, check_ack=False) time.sleep(self.query_delay) ret = self._read(check_ack=False) authmsg = ret.split(self.read_termination)[1] if authmsg != 'Authorization success': self.log.error(f"Attocube authorization failed '{authmsg}'") else: # Read board serial number board_ver = self._write("getcser") # Check how many exes are available valid_axes, num_axes = self._check_num_channels() self.axes = valid_axes self.num_axes = num_axes self.log.info( f"Connected to {board_ver} with {self.num_axes} available axes." ) def _check_num_channels(self): """ Checks how many axes are available :returns: valid_axis, list containing the axis indices (1-indexed), num_axis, integer """ valid_axis = [] for i in range(1, 8): axis_serial = self._write(f"getser {i}", check_axes=True) if axis_serial != 'Wrong axis type': valid_axis.append(i) num_axis = len(valid_axis) return valid_axis, num_axis def channel_valid(self, channel): """ Checks if channel number matches total number of detected channels. :returns: channel_valid, boolean. If true, the channel is valid. """ channel_valid = False if channel not in self.axes: self.log.error( f"Channel {channel} not valid, available channels are {self.axes}." ) else: channel_valid = True return channel_valid def _check_acknowledgement(self, reply, msg=""): """ checks the last reply of the instrument to be 'OK', a log error is raised :param reply: last reply string of the instrument :param msg: optional message for the eventual error """ if reply != 'OK': if msg == "": # clear buffer msg = reply self._read() self.log.error("AttocubeConsoleAdapter: Error after command " f"{self.lastcommand} with message {msg}") def _read(self, check_ack=True, check_axes=False): """ Reads a reply of the instrument which consists of two or more lines. The first ones are the reply to the command while the last one is 'OK' or 'ERROR' to indicate any problem. In case the reply is not OK a ValueError is raised. :param check_axes: Supressed error message (only for check axis command). :returns: Cleaned up response of the instruments (stripped by the status indicator and initial command). """ time.sleep(self.query_delay) ret = self.connection.read_some().decode() + \ self.connection.read_very_eager().decode() raw = ret.strip(self.read_termination) if check_ack: check = "" split_return = raw.rsplit(sep='\n') if len(split_return) == 3: # No argument returned check = split_return[-2].strip("\r") ret = split_return[0].strip("\r") elif len(split_return) == 4: check = split_return[-2].strip("\r") ret = split_return[1].strip("\r") if not check_axes: self._check_acknowledgement(check, ret) return ret def _extract_value(self, reply): """ preprocess_reply function for the Attocube console. This function tries to extract <value> from 'name = <value> [unit]'. If <value> can not be identified the original string is returned. :param reply: reply string :returns: float with only the numerical value, or the original string """ r = self._reg_value.search(reply) if r: return r.groups()[0] else: return reply def _write(self, command, check_ack=True, check_axes=False): """ Writes a command to the instrument :param command: command string to be sent to the instrument :param check_ack: boolean flag to decide if the acknowledgement is read back from the instrument. This should be True for set pure commands and False otherwise. :param check_axes: Supressed error message (only for check axis command). :return: Returns cleaned up intrument response if check_ack is chosen, 'None' otherwise. """ time.sleep(self.query_delay) self.lastcommand = command command = command + self.write_termination self.connection.write(command.encode()) if check_ack: reply = self._read(check_ack=check_ack, check_axes=check_axes) else: reply = None return reply @check_channel def _set_mode(self, channel, mode): """ Set mode of controller :param channel: (int) index of channel from 1 to self.num_ch :param mode: String indicating mode, which can be gnd (grounded), cap, (capacitance measurement), and stp (Step mode) """ self._write(f"setm {str(channel)} {str(mode)}") @check_channel def ground(self, channel): """ Grounds channel :param channel: (int) index of channel from 1 to self.num_ch """ self._set_mode(channel, 'gnd') self.log.info(f"Grounded channel {channel}.") @check_channel def set_parameters(self, channel, mode=None, frequency=None, amplitude=None): """ Sets parameters for motion Leave parameter as None in order to leave un-changed :param channel: (int) index of channel from 1 to self.num_ch :param mode: (str) default is 'step', :param freq: (int) frequency in Hz from 1 to 20000 :param amp: (float) amplitude in volts from 0 to 100 """ @check_channel def get_step_voltage(self, channel): """ Returns the step voltage in V :param channel: (int) channel index (from 1) """ return self._extract_value(self._write(f"getv {str(channel)}")) @check_channel def set_step_voltage(self, channel, voltage=30): """ Sets the step voltage to the piezo :param channel: (int) channel index from 1 to self.num_ch :param voltage: (float) voltage to set from 0 to 150 V (default is 30) """ if not (0 <= voltage <= self.step_voltage_lim): self.log.error( f"Step voltage has to be between 0 V and {self.step_voltage_lim} V." ) return self._write(f"setv {str(channel)} {str(voltage)}") self.log.info( f"Change step voltage of channel {channel} to {voltage} V.") @check_channel def get_step_frequency(self, channel): """ Returns the step frequency on channel :param channel: (int) channel index from 1 to self.num_ch """ return self._extract_value(self._write(f"getf {str(channel)}")) @check_channel def set_step_frequency(self, channel, freq=1000): """ Sets the step voltage to the piezo :param channel: (int) channel index from 1 to self.num_ch :param voltage: (float) voltage to set from 0 to 10000 Hz (default is 1000 Hz) """ if not (0 <= freq <= self.freq_lim): self.log.error( f"Frequency has to be between 0 Hz and {self.freq_lim} Hz") return self._write(f"setf {str(channel)} {str(freq)}") self.log.info( f"Change step frequency of channel {channel} to {freq} Hz.") @check_channel def n_steps(self, channel, n=1): """ Takes n steps :param channel: (int) channel index from 1 to self.num_ch :param n: (int) number of steps to take, negative is in opposite direction """ # Set into stepping mode self._set_mode(channel, 'stp') if n > 0: self._write(f"stepu {str(channel)} {str(n)}", check_ack=False) else: self._write(f"stepd {str(channel)} {str(abs(n))}", check_ack=False) self.log.info(f"Took {n} steps on channel {channel}.") @check_channel def get_capacitance(self, channel): """ Measures capacitance of positioner :param channel: (int) channel index from 1 to self.num_ch :return: Returns C in nF """ # Set into stepping mode self._set_mode(channel, 'cap') time.sleep(1) cap = float(self._extract_value(self._write(f"getc {str(channel)}"))) self.log.info(f"Capacitance measured on chanel {channel}: {cap} nF.") return cap @check_channel def get_output_voltage(self, channel): """ Get output voltage :param channel: (int) channel index from 1 to self.num_ch :return: Returns step voltage in volt """ return float(self._extract_value(self._write(f"geto {str(channel)}"))) @check_channel def move(self, channel, backward=False): """ Moves continously :param channel: (int) channel index from 1 to self.num_ch :param backward: (bool) whether or not to step in backwards direction (default False) """ # Set into stepping mode self._set_mode(channel, 'stp') if not backward: self._write(f"stepu {str(channel)} c", check_ack=False) else: self._write(f"stepd {str(channel)} c", check_ack=False) @check_channel def stop(self, channel): """ Terminates any ongoing movement :param channel: (int) channel index from 1 to self.num_ch """ self._write(f"stop {str(channel)}") self.log.info(f"Stopped channel {channel}.") @check_channel def is_moving(self, channel): """ Returns whether or not the positioner is moving :param channel: (int) channel index from 1 to self.num_ch :return: (bool) true if moving """ output_voltage = self.get_output_voltage(channel) if output_voltage < 1E-3: return False else: return True def stop_all(self): """ Terminates any ongoing movement on all axes""" for i in self.axes: self.stop(i) def ground_all(self): """ Grounds all positioners""" for i in self.axes: self.ground(i)
class Wrap(GatedCtrInterface): def __init__(self, tagger, click_ch, gate_ch, logger=None): """Instantiate gated counter :param tagger: instance of TimeTagger class :param click_ch: (int|list of int) clicks on all specified channels will be summed into one logical channel :param gate_ch: (int) positive/negative channel number - count while gate is high/low """ # Log self.log = LogHandler(logger=logger) # Reference to tagger self._tagger = tagger # Log device ID information to demonstrate that connection indeed works serial = self._tagger.getSerial() model = self._tagger.getModel() self.log.info( 'Got reference to Swabian Instruments TimeTagger device \n' 'Serial number: {0}, Model: {1}' ''.format(serial, model)) # Gated Counter # reference to the TT.CountBetweenMarkers measurement instance self._ctr = None # number of count bins: # length of returned 1D count array, the expected number of gate pulses, # the size of allocated memory buffer. # must be given as argument of init_ctr() call self._bin_number = 0 # Channel assignments self._click_ch = 0 self._gate_ch = 0 # reference to Combiner object # (if _click_ch is a list - then counts on all channels are summed # into virtual channel - self._combiner.getChannel()) self._combiner = None # apply channel assignment self.set_ch_assignment(click_ch=click_ch, gate_ch=gate_ch) # Module status code # -1 "void" # 0 "idle" # 1 "in_progress" # 2 "finished" self._status = -1 self._set_status(-1) # Once __init__() call is complete, # the counter is ready to be initialized by the above-lying logic though init_ctr() call # ---------------- Interface --------------------------- def activate_interface(self): return 0 def init_ctr(self, bin_number, gate_type): # Device-specific fix explanation: # # CountBetweenMarkers measurement configured for n_value bins # indeed fills-up buffer after n_value gate pules, but call of # self._ctr.ready() still gives False. Only after one additional # gate pulse it gives True, such that self.get_status() # gives 2 and self.get_count_ar() returns: # # device always needs an additional pulse to complete # (even for n_values = 1 it needs 2 gate pulses). # # Since this is very counter-intuitive and confuses # above-lying logic, a fix is made here: # # For given bin_number, CountBetweenMarkers measurement # is instantiated with n_values = (bin_number - 1) such # that it completes after receiving bin_number physical # gate pulses as expected. # # As a result, in the returned count_ar # (still of length bin_number, as logic expects), the last # value is just a copy of (bin_number - 1)-st element. # # The last physical bin is not actually measured, what can lead # to confusions when bin_number is on the order of one. # The warning below reminds about it: if bin_number <= 5: self.log.warn( 'init_ctr(): due to strange behaviour of TT.CountBetweenMarkers ' 'measurement, this driver makes a hack: counter is configured to ' 'measure bin_number-1 pulses and the last element of the returned ' 'count_ar is just a copy of the preceding one. \n' 'With bin_number={}, only the first {} gate windows will actually be ' 'measured.' ''.format(bin_number, bin_number - 1)) # Close existing counter, if it was initialized before if self.get_status() != -1: self.close_ctr() # Instantiate counter measurement try: if gate_type == 'RF': self._ctr = TT.CountBetweenMarkers( tagger=self._tagger, click_channel=self._click_ch, begin_channel=self._gate_ch, end_channel=-self._gate_ch, n_values=bin_number - 1) elif gate_type == 'RR': self._ctr = TT.CountBetweenMarkers( tagger=self._tagger, click_channel=self._click_ch, begin_channel=self._gate_ch, n_values=bin_number - 1) else: msg_str = 'init_ctr(): unknown gate type "{}" \n' \ 'Valid types are: \v' \ ' "RR" - Raising-Raising \n' \ ' "RF" - Raising-Falling' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # set status to "idle" self._set_status(0) # save bin_number in internal variable self._bin_number = bin_number # handle NotImplementedError (typical error, produced by TT functions) except NotImplementedError: # remove reference to the counter measurement self._ctr = None # set status to "void" self._set_status(-1) msg_str = 'init_ctr(): instantiation of CountBetweenMarkers measurement failed' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Prepare counter to be started by start_counting() # (CountBetweenMarkers measurement starts running immediately after instantiation, # so it is necessary to stop it and erase all counts collected between instantiation and stop() call) self._ctr.stop() self._ctr.clear() return 0 def close_ctr(self): # Try to stop and to clear TT.CountBetweenMarkers measurement instance try: self._ctr.stop() self._ctr.clear() except: pass # Remove reference, set status to "void" self._ctr = None self._set_status(-1) return 0 def start_counting(self): current_status = self.get_status() # Sanity check: ensure that counter is not "void" if current_status == -1: msg_str = 'start_counting(): ' \ 'counter is in "void" state - it ether was not initialized or was closed. \n' \ 'Initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Terminate counting if it is already running if current_status == 1: self.terminate_counting() # Try stopping and restarting counter measurement try: self._ctr.stop( ) # does not fail even if the measurement is not running self._ctr.clear() self._ctr.start() # set status to "in_progress" self._set_status(1) # Wait until the counter is actually ready to count time.sleep(0.1) return 0 # handle exception in TT function calls [NotImplementedError] except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'start_counting(): call failed. Counter was closed. \n'\ 'Re-initialize counter by calling init_ctr() again' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def terminate_counting(self): # Action of this method is non-trivial for "in_progress" state only if self.get_status() != 1: return 0 # Try stopping and clearing counter measurement try: # stop counter, clear count array self._ctr.stop() self._ctr.clear() # set status to "idle" self._set_status(0) return 0 # handle exception in TT.stop()/TT.clear() except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'terminate_counting(): call failed. Counter was closed. \n' \ 'Re-initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def get_status(self): # Check that counter measurement was initialized and that the connection works # by calling isRunning() # -- if self._ctr is None or if connection is broken, call will rise some # exception. In this case "void" status should be set # -- if counter was initialized and connection works, it will return successfully # (True or False, but the result does not matter) # and further choice between "idle", "in_progress", and "finished" should be made try: self._ctr.isRunning() except: # set status to "void" self._status = -1 # No handling of "idle" and "finished" status is needed: # it will be returned by self._status as is # Handle "in_progress" status # This status means that measurement was started before. # Now one needs to check if it is already finished or not. # If measurement is complete, change status to "finished". if self._status == 1: if self._ctr.ready(): self._ctr.stop() self._status = 2 return copy.deepcopy(self._status) def get_count_ar(self, timeout=-1): # If current status is "in_progress", # wait for transition to some other state: # "finished" if measurement completes successfully, # "idle" if measurement is terminated, # "void" if counter breaks start_time = time.time() sleep_time = abs(timeout) / 100 while self.get_status() == 1: # stop waiting if timeout elapses if time.time() - start_time > timeout >= 0: break time.sleep(sleep_time) # Analyze current status and return correspondingly status = self.get_status() # return data only in the case of "finished" state if status == 2: count_array = np.array(self._ctr.getData(), dtype=np.uint32) # Fix of the issue with an additional gate pulse needed to complete # measurement (see comment in init_ctr() for explanation): # the last element of returned array is just a copy # of the last physically measured bin count_array = np.append(count_array, count_array[-1]) return count_array # return empty list for all other states ("in_progress", "idle", and "void") else: if status == 1: self.log.warn( 'get_count_ar(): operation timed out, but counter is still running. \n' 'Try calling get_count_ar() later or terminate process by terminate_counting().' ) elif status == 0: self.log.warn( 'get_count_ar(): counter is "idle" - nothing to read') else: msg_str = 'get_count_ar(): counter broke and was deleted \n' \ 'Re-initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) return [] # ------------------------------------------------------ def _set_status(self, new_status): """Method to set new status in a clean way. This method compares the requested new_status with current status and checks if this transition is possible. If transition is possible, the change is applied to self._status. Otherwise, no status change is applied, -1 is returned, and error message is logged. :param new_status: (int) new status value -1 - "void" 0 - "idle" 1 - "in_progress" 2 - "finished" :return: (int) operation status code: 0 - OK, change was accepted and applied -1 - Error, impossible transition was requested, no state change was applied """ # Transition to "void" is always possible # by calling close_ctr() if new_status == -1: self._status = -1 return 0 # Transition to "idle" is possible from # "void" by calling init_ctr() # "in_progress" by calling terminate_counting() if new_status == 0: if self._status == -1 or self._status == 1: self._status = 0 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Transition to "in_progress" is possible from # "idle" by calling start_counting() # "finished" by calling start_counting() if new_status == 1: if self._status == 0 or self._status == 2: self._status = 1 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Transition to "finished" is only possible from "in_progress" # by successful completion of count_array accumulation if new_status == 2: if self._status == 1: self._status = 2 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) def get_ch_assignment(self): """Returns dictionary containing current channel assignment: { 'click_ch': (int) click_channel_number_including_edge_sign 'gate_ch': (int) gate_channel_number_including_edge_sign } :return: dict('click_ch': _, 'gate_ch': _) """ click_ch = copy.deepcopy(self._click_ch) gate_ch = copy.deepcopy(self._gate_ch) return dict(click_ch=click_ch, gate_ch=gate_ch) def set_ch_assignment(self, click_ch=None, gate_ch=None): """Sets click channel and and gate channel. This method only changes internal variables self._click_ch and self._gate_ch. To apply the channel update, call init_ctr() again. :param click_ch: (int|list of int) click channel number positive/negative values - rising/falling edge detection if list is given, clicks on all specified channels will be merged into one logic channel :param gate_ch: (int) channel number positive/negative - count during high/low gate level :return: (dict) actually channel assignment: { 'click_channel': (int) click_chnl_num, 'gate_channel': (int) gate_chnl_num } """ if click_ch is not None: # for convenience bring int type of input to list of int if isinstance(click_ch, list): click_ch_list = click_ch elif isinstance(click_ch, int): click_ch_list = [click_ch] else: # unknown input type msg_str = 'set_ch_assignment(click_ch={0}): invalid argument type'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # sanity check: all requested channels are available on the device all_chs = self.get_all_chs() for channel in click_ch_list: if channel not in all_chs: msg_str = 'set_ch_assignment(): '\ 'click_ch={0} - this channel is not available on the device'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # If several channel numbers were passed, create virtual Combiner channel if len(click_ch_list) > 1: self._combiner = TT.Combiner(tagger=self._tagger, channels=click_ch_list) # Obtain int channel number for the virtual channel click_ch_list = [self._combiner.getChannel()] # Set new value for click channel self._click_ch = int(click_ch_list[0]) if gate_ch is not None: # sanity check: channel is available on the device if gate_ch not in self.get_all_chs(): msg_str = 'set_ch_assignment(): '\ 'gate_ch={0} - this channel is not available on the device'\ ''.format(gate_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Set new value for gate channel self._gate_ch = int(gate_ch) return self.get_ch_assignment() def get_all_chs(self): """Returns list of all channels available on the device, including edge type sign. Positive/negative numbers correspond to detection of rising/falling edges. For example: 1 means 'rising edge on connector 1' -1 means 'falling edge on connector 1 :return: (list of int) list of channel numbers including edge sign. Example: [-8, -7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8] Empty list is returned in the case of error. """ # Sanity check: check that connection to the device was established if self._tagger is None: msg_str = 'get_all_chs(): not connected to the device yet' self.log.error(msg_str=msg_str) raise CtrError(msg_str) channel_list = list( self._tagger.getChannelList( TT.TT_CHANNEL_RISING_AND_FALLING_EDGES)) return channel_list
class Driver: BOARDS = 8 def __init__(self, address=None, logger=None): """Instantiate driver class. :address: Address of the device, e.g. 'ASRL3::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: An instance of a LogClient. """ # Instantiate log self.log = LogHandler(logger=logger) self.addr = address self.rm = ResourceManager() try: self.device = self.rm.open_resource(self.addr) self.log.info(f"Successfully connected to {self.device}.") # Configure device grammar self.device.write_termination = ';' except VisaIOError: self.log.error(f"Connection to {self.addr} failed.") raise def measure_voltage(self, board, channel): """ Measures the current voltage on a particular channel of a particular board :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in volts (into open-loop) """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('v').split()[-1]), bits=12, min=0, max=10) def set_high_voltage(self, board, channel, voltage): """ Sets a channel's high voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return self._set_high_voltage(voltage) def set_low_voltage(self, board, channel, voltage): """ Sets a channel's low voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return self._set_low_voltage(voltage) def get_high_voltage(self, board, channel): """ Gets a channel's high voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in V from 0 to 10 """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('h').split()[-1]), bits=16, min=0, max=10) def get_low_voltage(self, board, channel): """ Gets a channel's low voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in V from 0 to 10 """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('l').split()[-1]), bits=16, min=0, max=10) def save(self): """ Saves current state of low/high for all channels to non-volatile memory :return: (int) 0 if successful """ # This fails randomly due to nondeterministic response, which we need to handle read = 0 while read < 10: self.device.write('S') try: self.device.read() read = 11 except VisaIOError: read += 1 except UnicodeDecodeError: read += 1 if read > 10: self.log.info('Saved current DIO breakout settings successfully') return 0 else: self.log.warn('Failed to save DIO breakout settings.' 'Connection may be corrupted.') return 1 def override(self, board, channel, state=True): """ Overrides HDAWG output :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param state: (bool) whether or not to force hi or lo :return: (int) 0 if successful """ if self._set_board(board) + self._set_channel(channel): self.log.warn(f'Did not override board {board} channel {channel}') return float(-777) self.device.write(f'F {1 if state else 0}') if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn( f'Error in overriding board {board} channel {channel}') return float(-1) self.log.info(f'Board {board} channel {channel} in override mode') return 0 def disable_override(self, board, channel): """ Disables the override :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (int) 0 if successful """ if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not disable override for board {board} channel {channel}' ) return float(-777) self.device.write('F -1') if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn( f'Error in disabling override for board {board} channel {channel}' ) return float(-1) self.log.info( f'Board {board} channel {channel} override has been disabled') return 0 def close(self): """ Closes the connection to the device """ self.device.close() self.log.info(f'Closed connection to device at {self.addr}') # Technical methods (not to be exposed) def _set_board(self, board): """ Sets the current board (to update) :param board: (int) integer between 0 and 7 (assuming 8 boards) :return: (int) 0 if successful """ board = int(board) # Check within bounds if board < 0 or board > self.BOARDS - 1: self.log.warn( f'Board to set must be an integer between 0 and {self.BOARDS-1}' ) return 1 self.device.write(f'B {board}') # Check successful write (and clear the buffer) if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn(f'Failed to set current board to {board}') return 2 return 0 def _set_channel(self, channel): """ Sets the current channel (to update) :param channel: (int) integer between 0 and 3 :return: (int) 0 if successful """ channel = int(channel) # Check within bounds if channel < 0 or channel > 3: self.log.warn(f'Channel to set must be an integer between 0 and 3') return 1 self.device.write(f'C {channel}') # Check successful write (and clear the buffer) if int(self.device.query('c').rstrip()[-1]) != channel: self.log.warn(f'Failed to set current channel to {channel}') return 2 return 0 def _set_high_voltage(self, voltage): """ Sets the current channel's high voltage :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ voltage = float(voltage) # Check within bounds if voltage < 0 or voltage > 10: self.log.warn(f'Can only set voltage between 0 and 10 V') return 1 bitval = value_to_bitval(voltage, bits=16, min=0, max=10) self.device.write(f'H {bitval}') # Check successful write (and clear the buffer) if int(self.device.query('h').split()[-1]) != bitval: self.log.warn(f'Failed to set high voltage to {voltage} V') return 2 return 0 def _set_low_voltage(self, voltage): """ Sets the current channel's low voltage :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ voltage = float(voltage) # Check within bounds if voltage < 0 or voltage > 10: self.log.warn(f'Can only set voltage between 0 and 10 V') return 1 bitval = value_to_bitval(voltage, bits=16, min=0, max=10) self.device.write(f'L {bitval}') # Check successful write (and clear the buffer) if int(self.device.query('l').split()[-1]) != bitval: self.log.warn(f'Failed to set low voltage to {voltage} V') return 2 return 0
class Driver(): POWER_RANGE = [-120, 30] # acceptable power range in dBm POWER_PRECISION = 2 # number of digits of precision for power FREQ_RANGE = [1e7, 2e10] # acceptable frequency range in Hz def __init__(self, gpib_address, logger): """ Instantiate driver class, connects to device :param gpib_address: GPIB-address of the device, can be found with pyvisa.ResourceManager.list_resources() :param logger: instance of LogClient """ self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) self.device.read_termination = '\n' self.device_id = self.device.query('*IDN?') self.log.info(f'Successfully connected to {self.device_id}') except VisaIOError: self.log.error(f'Connection to {gpib_address} failed') raise def set_power(self, power): """ Sets power of MW source :param power: (float) output power to set in dBm """ power = round(power, self.POWER_PRECISION) # Check for valid range if power < self.POWER_RANGE[0] or power > self.POWER_RANGE[1]: self.log.warn( f'Warning, power outside acceptable range {self.POWER_RANGE}. ' f'Output power was not updated.') # Set power else: self.device.write(f'POW {power}') self.log.info(f'Set MW power to {power}') def set_freq(self, freq): """ Sets power of MW source :param freq: (float) output frequency in Hz """ freq = int(round(freq)) # Check for valid range if freq < self.FREQ_RANGE[0] or freq > self.FREQ_RANGE[1]: self.log.warn( f'Warning, frequency outside acceptable range {self.FREQ_RANGE}. ' f'Output frequency was not updated.') # Set freq else: self.device.write(f'FREQ {freq}') self.log.info(f'Set MW freq to {freq}') def output_on(self): """ Turn output on """ self.device.write('OUTP ON') self.log.info('MW output turned on') def output_off(self): """ Turn output off """ self.device.write('OUTP OFF') self.log.info('MW output turned off')
class Driver(): def __init__(self, device_id, logger, dummy=False, api_level=6, reset_dio=False, disable_everything=False, **kwargs): """ Instantiate AWG :logger: instance of LogClient class :device_id: Device id of connceted ZI HDAWG, for example 'dev8060' :api_level: API level of zhins API """ # Instantiate log self.log = LogHandler(logger=logger) # Store dummy flag self.dummy = dummy # Setup HDAWG self._setup_hdawg(device_id, logger, api_level, reset_dio, disable_everything) @dummy_wrap def reset_DIO_outputs(self): """Sets all DIO outputs to low""" self.seti('dios/0/output', 0) self.log.info("Set all DIO outputs to low.") @dummy_wrap def disable_everything(self): """ Create a base configuration. Disable all available outputs, awgs, demods, scopes, etc. """ zhinst.utils.disable_everything(self.daq, self.device_id) self.log.info("Disabled everything.") @log_standard_output @dummy_wrap def log_stdout(self, function): """ Execute function and log print output to self.log This statement is needed for an inline call where any zhinst command is executed and the standard output should be logged :function: The function to be executed. """ return function() def _convert_to_list(self, input_argument): """Checks if input is list and if not, converts it to list.""" if type(input_argument) is not list: input_argument = [input_argument] return input_argument @dummy_wrap def _setup_hdawg(self, device_id, logger, api_level, reset_dio, disable_everything): ''' Sets up HDAWG ''' err_msg = "This example can only be run on an HDAWG." # Connect to device and log print output, not the lambda expression. (daq, device, props) = self.log_stdout( lambda: zhinst.utils.create_api_session( device_id, api_level, required_devtype='HDAWG', required_err_msg=err_msg ) ) self.log_stdout(lambda: zhinst.utils.api_server_version_check(daq)) self.daq = daq self.device_id = device if disable_everything: # Create a base configuration self.disable_everything() if reset_dio: self.reset_DIO_outputs() # read out number of channels from property dictionary self.num_outputs = int( re.compile('HDAWG(4|8{1})').match(props['devicetype']).group(1) ) @log_standard_output @dummy_wrap def seti(self, node, new_int): """ Warapper for daq.setInt commands. For instance, instead of daq.setInt('/dev8040/sigouts/0/on', 1), write hdawg.seti('sigouts/0/on, 1) :node: Node which will be appended to '/device_id/' :new_int: New value for integer """ self.daq.setInt(f'/{self.device_id}/{node}', new_int) @log_standard_output @dummy_wrap def setd(self, node, new_double): """ Warapper for daq.setDouble commands. For instance, instead of daq.setDouble('/dev8040/sigouts/0/range', 0.8), write hdawg.setd('sigouts/0/range') :node: Node which will be appended to '/device_id/' :new_double: New value for double. """ self.daq.setDouble(f'/{self.device_id}/{node}', new_double) @log_standard_output @dummy_wrap def getd(self, node): """ Warapper for daq.setDouble commands. For instance, instead of daq.getDouble('/dev8040/sigouts/0/range'), write hdawg.getd('sigouts/0/range') :node: Node which will be appended to '/device_id/' """ return self.daq.getDouble(f'/{self.device_id}/{node}') @log_standard_output @dummy_wrap def setv(self, node, vector): """ Warapper for daq.setVector commands. For instance, instead of daq.setVector('/dev8060/awgs/0/waveform/waves/1', vector), write hdawg.setd('sigouts/awgs/0/waveform/waves/1', vector) :node: Node which will be appended to '/device_id/' :new_double: New value for double. """ self.daq.setVector(f'/{self.device_id}/{node}', vector) @log_standard_output @dummy_wrap def geti(self, node): """ Wrapper for daq.getInt commands. For instance, instead of daq.getInt('/dev8040/sigouts/0/busy'), write hdawg.geti('sigouts/0/busy') :node: Node which will be appended to '/device_id/' """ return self.daq.getInt(f'/{self.device_id}/{node}') @log_standard_output @dummy_wrap def gets(self, path): """ Wrapper for daq.getString commands. Get a string value from the specified node. | :path: Path string of the node. """ return self.daq.getString(path) @dummy_wrap def set_channel_grouping(self, index): """ Specifies channel grouping. :index: Integer indicating channel grouping: 0 : 4x2 with HDAWG8; 2x2 with HDAWG4. 1 : 2x4 with HDAWG8; 1x4 with HDAWG4. 2 : 1x8 with HDAWG8. """ self.seti('system/awg/channelgrouping', index) time.sleep(2) # Functions related to wave outputs: @dummy_wrap def _toggle_output(self, output_indices, target_index): """ Local function enabeling/disabeling wave output. """ # If single integer is given, convert to list. output_indices = self._convert_to_list(output_indices) for output_index in output_indices: if output_index in range(self.num_outputs): self.seti(f'sigouts/{output_index}/on', target_index) if target_index == 1: self.log.info(f"Enabled wave output {output_index}.") elif target_index == 0: self.log.info(f"Disable wave output {output_index}.") else: self.log.error( f"This device has only {self.num_outputs} channels, \ channel index {output_index} is invalid." ) @dummy_wrap def enable_output(self, output_indices): """ Enables wave output. Channel designation uses channel index (0 to 7), not channel number (1 to 8). :output_index: List or int containing integers indicating wave output 0 to 7 """ self._toggle_output(output_indices, 1) @dummy_wrap def disable_output(self, output_indices): """ Disables wave output. :output_index: List or int containing integers indicating wave output 0 to 7 """ self._toggle_output(output_indices, 0) @dummy_wrap def set_output_range(self, output_index, output_range): """ Set the output range. :output_index: List or int containing integers indicating wave output 0 to 7 :output_range: Double indicating the range of wave output, in Volt. All waveforms (ranging from 0 to 1 in value) will be multiplied with this value. Possible ranges are: 0.2, 0.4, 0.6, 0.8, 1, 2, 3, 4, 5 (V) """ allowed_ranges = [0.2, 0.4, 0.6, 0.8, 1, 2, 3, 4, 5] if output_index in range(self.num_outputs): if output_range in allowed_ranges: # Send change range command. self.setd(f'sigouts/{output_index}/range', output_range) # Wait for HDAWG to be ready, try 100 times before timeout. max_tries = 100 num_tries = 0 while self.geti(f'sigouts/{output_index}/busy') and num_tries < max_tries: time.sleep(0.2) num_tries += 1 if num_tries is max_tries: self.log.error( f"Range change timeout after {max_tries} tries." ) else: self.log.info( f"Changed range of wave output {output_index} to {output_range} V." ) else: self.log.error( f"Range {output_range} is not valid, allowed values for range are {allowed_ranges}" ) else: self.log.error( f"This device has only {self.num_outputs} channels, channel index {output_index} is invalid." ) def set_direct_user_register(self, awg_num, reg_index, value): """ Sets a user register to a desired value :param awg_num: (int) index of awg module :param reg_index: (int) index of user register (from 0-15) :param value: (int) value to set user register to """ self.setd(f'awgs/{awg_num}/userregs/{reg_index}', int(value)) def get_direct_user_register(self, awg_num, reg_index): """ Gets a user register to a desired value :param awg_num: (int) index of awg module :param reg_index: (int) index of user register (from 0-15) """ return int(self.getd(f'awgs/{awg_num}/userregs/{reg_index}'))
class Driver(MWSrcInterface): """Adapted from Qudi <https://github.com/Ulm-IQO/qudi/> """ def __init__(self, addr_str, logger=None): self.log = LogHandler(logger=logger) # Connect to the device self.rm = visa.ResourceManager() try: self._dev = self.rm.open_resource(addr_str) except Exception as exc_obj: self.log.exception( msg_str='Could not connect to the address >>{}<<. \n'.format( addr_str)) raise exc_obj # Log confirmation info message id_str = self._dev.query('*IDN?').replace(',', ' ') id_str = id_str.strip('\n') self.log.info(msg_str='{} initialised and connected.'.format(id_str)) # Reset device self.reset() # Error check self._er_chk() def reset(self): # Reset self._cmd_wait('*RST') # Clear status register self._cmd_wait('*CLS') return 0 def activate_interface(self): # Store hardware settings which are not controlled by logic, # to restore them after reset() # [logic does not know anything about this params, so it should not # introduce any changes to them by calling activate_interface()]. tmp_trig_dict = self.get_trig() # Reset device self.reset() # Restore hardware settings which are not controlled by logic # but were changed by self._dev.reset() self.set_trig(src_str=tmp_trig_dict['src_str'], slope_str=tmp_trig_dict['slope_str']) return 0 # Output control def on(self): if self.get_mode() == 'sweep': self.reset_swp_pos() return self._cmd_wait(cmd_str=':OUTP:STAT ON') def off(self): return self._cmd_wait(cmd_str=':OUTP:STAT OFF') def get_status(self): status = int(self._dev.query('OUTP:STAT?')) return status # Power def get_pwr(self): return float(self._dev.query(':POW?')) def set_pwr(self, pwr): self._cmd_wait(':POW {0:f}'.format(pwr)) return self.get_pwr() # Frequency def get_freq(self): mode = self.get_mode() if mode == 'cw': ret_val = float(self._dev.query(':FREQ?')) elif mode == 'sweep': start = float(self._dev.query(':FREQ:STAR?')) stop = float(self._dev.query(':FREQ:STOP?')) step = float(self._dev.query(':SWE:STEP?')) n_pts = int((stop - start) / step) + 2 ret_val = dict(start=start, stop=stop, n_pts=n_pts) else: raise MWSrcError('get_freq(): got unknown mode {}'.format(mode)) return ret_val def set_freq(self, freq): if self.get_status() == 1: self.off() # Activate CW mode self._cmd_wait(':FREQ:MODE CW') # Set CW frequency self._cmd_wait(':FREQ {0:f}'.format(freq)) return self.get_freq() def set_freq_swp(self, start, stop, n_pts): if self.get_status() == 1: self.off() # Set mode to Sweep self._cmd_wait(':FREQ:MODE SWEEP') # Set frequency sweep step = (stop - start) / (n_pts - 1) self._cmd_wait(':SWE:MODE STEP') self._cmd_wait(':SWE:SPAC LIN') self._cmd_wait(':FREQ:START {0:f}'.format(start)) self._cmd_wait(':FREQ:STOP {0:f}'.format(stop)) self._cmd_wait(':SWE:STEP:LIN {0:f}'.format(step)) return self.get_freq() def reset_swp_pos(self): """Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ self._cmd_wait(':ABOR:SWE') return 0 def get_mode(self): mode_str = self._dev.query(':FREQ:MODE?').strip('\n').lower() if 'cw' in mode_str: return 'cw' elif 'swe' in mode_str: return 'sweep' else: msg_str = 'get_mode(): unknown mode string {} was returned' self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) # Technical methods def _cmd_wait(self, cmd_str): """Writes the command in command_str via resource manager and waits until the device has finished processing it. @param cmd_str: The command to be written """ self._dev.write(cmd_str) # Block command queue until cmd_str execution is complete self._dev.write('*WAI') # Block Python process until cmd_str execution is complete self._dev.query('*OPC?') # Error check self._er_chk() return 0 def _er_chk(self): # Block command queue until all previous commands are complete self._dev.write('*WAI') # Block Python process until all previous commands are complete self._dev.query('*OPC?') # Read all messages out_str = self._dev.query(':SYSTem:ERRor:ALL?') out_str += ',' out_str += self._dev.query('SYST:SERR?') out_str = out_str.replace('\n', '') out_str = out_str.replace('\r', '') out_str = out_str.replace('"', '') out_list = out_str.split(',') # Collect all warns and errors er_list = [] warn_list = [] msg_n = int(len(out_list) / 2) for idx in range(msg_n): msg_code = int(out_list[2 * idx]) if msg_code == 0: # No error continue elif msg_code > 0: # Warning warn_list.append(out_list[2 * idx + 1]) else: # Error er_list.append(out_list[2 * idx + 1]) # Construct Warn message string if len(warn_list) > 0: warn_str = '' for warn in warn_list: warn_str += (warn + ' \n') warn_str = warn_str.rstrip('\n') self.log.warn(msg_str=warn_str) # Construct Error message string if len(er_list) > 0: er_str = '' for er in er_list: er_str += (er + ' \n') er_str = er_str.rstrip('\n') self.log.error(msg_str=er_str) raise MWSrcError(er_str) return 0 def get_trig(self): # # Get trigger source # src_str = self._dev.query('TRIG:FSW:SOUR?') if 'EXT' in src_str: src_str = 'ext' elif 'AUTO' in src_str: src_str = 'int' else: msg_str = 'get_trig(): unknown trigger source was returned "{}" \n' \ ''.format(src_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) # # Get edge slope # slope_str = self._dev.query(':TRIG1:SLOP?') if 'POS' in slope_str: slope_str = 'r' elif 'NEG' in slope_str: slope_str = 'f' else: msg_str = 'get_trig(): unknown slope was returned "{}" \n' \ ''.format(slope_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) return dict(src_str=src_str, slope_str=slope_str) def set_trig(self, src_str='ext', slope_str='r'): if self.get_status() == 1: self.off() # # Set trigger source # if src_str == 'ext': src_str = 'EXT' elif src_str == 'int': src_str = 'AUTO' else: msg_str = 'set_trig(): unknown trigger source "{}" \n' \ 'Valid values are "ext" - external, "int" - internal' \ ''.format(src_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) self._cmd_wait('TRIG:FSW:SOUR {}'.format(src_str)) # # Set trigger edge # if slope_str == 'r': edge = 'POS' elif slope_str == 'f': edge = 'NEG' else: msg_str = 'set_trig(): invalid argument slope_str={} \n' \ 'Valid values are: "r" - raising, "f" - falling' \ ''.format(slope_str) self.log.error(msg_str=msg_str) raise ValueError(msg_str) self._cmd_wait(':TRIG1:SLOP {0}'.format(edge)) return self.get_trig() def force_trig(self): """ Trigger the next element in the list or sweep mode programmatically. @return int: error code (0:OK, -1:error) Ensure that the Frequency was set AFTER the function returns, or give the function at least a save waiting time. """ self._cmd_wait('*TRG') return 0
class Driver(): def reset(self): """ Create factory reset""" self.device.write('*RST') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class. :gpib_address: GPIB-address of the device, e.g. 'COM8' Can ba found in the Windows device manager. :logger: And instance of a LogClient. """ # Instantiate log. self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) self.device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {self.device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # Reset to factory settings. self.reset() # Read and store min and max power. self.power_min, self.power_max = [ float( self.device.query(f'pow? {string}') ) for string in ['min', 'max'] ] # Read and store min and max frequency. self.freq_min, self.freq_max = [ float( self.device.query(f'freq? {string}') ) for string in ['min', 'max'] ] def output_on(self): """ Turn output on.""" self.device.write('OUTPut ON') self.log.info(f"Output of {self.device_id} turned on.") def output_off(self): """ Turn output off.""" self.device.write('OUTP OFF') self.log.info(f"Output of {self.device_id} turned off.") def check_power_out_of_range(self): """ Returns True if current power is outside of calibration range, False otherwise""" if not self.is_output_on(): warn = "Please enable output to check if calibration out of range" self.log.warn(warn) return warn # Query status register and do bitwise AND to check status of bit 3 if int(self.device.query('Status:Questionable:Condition?')) & 8: power_out_of_range = True else: power_out_of_range = False return power_out_of_range def is_output_on(self): """Returns True if output is enabled, False otherwise.""" return bool(int(self.device.query('OUTPut?'))) def set_freq(self, freq): """ Set frequency (in Hz) :freq: Target frequency in Hz """ if not self.freq_min <= freq <= self.freq_max: self.log.error( f"Frequency must be between {self.freq_min} Hz and {self.freq_max} Hz" ) self.device.write(f'freq {freq}') self.log.info(f"Frequency of {self.device_id} set to {freq} Hz.") def get_freq(self): """Returns current frequency setting.""" return float(self.device.query('freq?')) def set_power(self, power): """Set output power (in dBm) :power: Target power in dBm """ if not self.power_min <= power <= self.power_max: self.log.error( f"Power must be between {self.power_min} dBm and {self.power_max} dBm" ) self.device.write(f'pow {power}') self.log.info(f"Output power of {self.device_id} set to {power} dBm.") def get_power(self): """Returns current output power setting.""" return float(self.device.query('pow?'))
class Driver(): def __init__(self, channels, logger=None): """ Initializes connection to all TP Kasa smart plugs in the network. :channels: list of channels accessaable via this smart plug interface """ # Instantiate log. self.log = LogHandler(logger=logger) self.channels = channels # Discover smart plugs. self.found_devices = asyncio.run(Discover.discover()) # Store aliases of found devices. self.found_device_aliases = [ dev.alias for dev in self.found_devices.values() ] self.log.info( f"Discovered {len(self.found_device_aliases)} smart plugs.") def retrieve_device(self, channel_id): """ Returns kasa.SmartPlug object corresponding to a specific plug. The plugs are identified with channel_ids, which usually correspond to human readable names of the plug location, e.g. "Lights Kitchen". The config file then matches the channel_ids to the plug aliases, as they have been defined using the Kasa app. :channel_id: (str) Human readable device ID. """ _, alias = self.get_device_info(channel_id) if not alias in self.found_device_aliases: self.log.error( f"Smart Plug at location '{channel_id}' with plug alias '{alias}' not found. Connected devices are: {self.found_device_aliases}" ) else: self.log.info( f"Smart Plug at location '{channel_id}' with plug alias '{alias}' discovered." ) device = [ dev for dev in self.found_devices.values() if dev.alias == alias ][0] # Run update. asyncio.run(device.update()) return device def turn_on(self, channel_id): """Turn plug on. :channel_id: (str) Human readable device ID. """ device = self.retrieve_device(channel_id) asyncio.run(device.turn_on()) self.log.info(f"Smart Plug at location '{channel_id}' turned on.") def turn_off(self, channel_id): """Turn plug off. :channel_id: (str) Human readable device ID. """ device = self.retrieve_device(channel_id) asyncio.run(device.turn_off()) self.log.info(f"Smart Plug at location '{channel_id}' turned off.") def is_on(self, channel_id): """ Returns True is plug is on. :channel_id: (str) Human readable channel ID. """ device = self.retrieve_device(channel_id) return device.is_on def get_device_info(self, channel_id): """ Read config dict and find alias matching to channel_id. :channel_id: (str) Human readable device ID. """ alias, current_plug_type = None, None for location_dict in self.channels: if location_dict['channel_id'] == channel_id: alias = location_dict['alias'] break if alias is not None: return current_plug_type, alias else: self.log.error( f"Could not find plug location {channel_id} in smart_plug_config.json." )
class Driver(): """Driver class for GPIB controlled Agilent EE405 Spectrum analyser""" def reset(self): """ Create factory reset""" self.device.write('*RST') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class :gpib_address: GPIB-address of spectrum analyzer, e.g. 'GPIB0::12::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: And instance of a LogClient """ # Instantiate log self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # reset to factory settings self.reset() def display_off(self): """ Power off display """ self.device.write(':DISPlay:ENABle OFF') self.log.info("Display off.") def display_on(self): """ Power on display """ self.device.write(':DISPlay:ENABle ON') self.log.info("Display on.") def set_attenuation(self, db): """ Set input attenuation :db: Target attenuation in dB, must be between 0 and 75 """ if not 0 <= db <= 75: self.log.error( f'Invalid attenuation ({db}dB). Attenuation must be between 0dB and 75dB' ) self.device.write(f'POW:ATT {int(db)}dB') self.log.info(f'Input attenuation set to {db}dB.') def set_reference_level(self, db): """Set reference level :db: Target reference level in dB """ self.device.write(f':DISPlay:WINDow:TRACe:Y:RLEVel {int(db)}') self.log.info(f'Reference level set to {db}dB.') def set_center_frequency(self, center_frequency): """ Set center frequency of trace. :center_frequency: Frequency in Hz (from 0 to 13.3 GHz) """ if not 0 <= center_frequency <= 13.2 * 1e9: self.log.error( f'Invalid center frequency ({center_frequency} Hz). Must be within 0 and 13.2 GHz' ) self.device.write(f':SENSe:FREQuency:CENTer {center_frequency}') self.log.info(f'Center frequency set to {center_frequency} Hz') def set_frequency_span(self, frequency_span): """ Set frequency span of trace. :frequency_span: Frequency span in Hz (from 0 to 13.3 GHz) """ if not 0 <= frequency_span <= 13.2 * 1e9: self.log.error( f'Invalid frequency span ({frequency_span} Hz). Must be within 0 and 13.2 GHz' ) self.device.write(f':SENSe:FREQuency:SPAN {frequency_span}') self.log.info(f'Frequency span set {frequency_span} Hz') def toggle_cont(self, target_state): """Switch between single shot and continuous acquisition mode :target_state: Index of targe stat. 1 for continuous mode, 0 for single shot mode. """ self.device.write(f'INIT:CONT {target_state}') def get_frequency_array(self): """Constructs array of frequencies associated with trace points""" # Sweep start frequency. start_freq = float(self.device.query(':SENSe:FREQuency:STARt?')) # Sweep end frequency. end_freq = float(self.device.query(':SENSe:FREQuency:STOP?')) # Number of sweep points. num_sweep_points = int(self.device.query('SENSE:SWEEP:POINTS?')) # Array containing frequencies of each sweep point. frequencies = np.linspace(start_freq, end_freq, num_sweep_points) return frequencies def read_trace(self): """ Read and return trace Retruns array trace contaning frequencies (in Hz) of data points in trace[:,0] and power levels (in dBm) in trace[:,1] """ # Set to single shot mode. self.toggle_cont(0) # Trigger a sweep and wait for sweep to complete. self.device.write('INIT:IMM;*WAI') # Specify units in dBm. self.device.write('UNIT:POW DBM') # Specify data format as ASCII. self.device.write('FORM:DATA ASC') # Trigger a sweep and wait for sweep to complete. self.device.write('INIT:IMM;*WAI') # Query trace data dbm_measurement = self.device.query_ascii_values('TRAC:DATA? TRACE1') # Return to continuos monitoring mode self.toggle_cont(1) # Read frequency axis frequencies = self.get_frequency_array() # Combine trace data trace = np.stack((frequencies, dbm_measurement), axis=-1) return trace def acquire_background_spectrum(self, num_points=100): """Acquires an average background trace. :num_traces: How many traces to sample. :nocheck: Boolean indicating if user confirmation that no input signal is present will be omitted. """ traces = [] self.display_off() for i in range(num_points): traces.append(self.read_trace()) self.display_on() noise_average = np.average(np.asarray(traces)[:, :, 1], 0) noise_trace = np.array([traces[0][:, 0], noise_average]) self.noise_trace = np.transpose(noise_trace) def get_background(self): try: noise_trace = self.noise_trace return noise_trace except AttributeError: error_msg = 'No background acquired. Run acquire_background_spectrum() to acquire background.' self.log.warn(error_msg) return error_msg
class DataTaker: def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Configure list of missing clients self.missing_clients = {} # Setup Autosave # First check whether autosave is specified in config file if 'auto_save' in self.config: if self.config['auto_save']: self.gui.autosave.setChecked(True) # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): self.missing_clients[f"{client_type}_{client_config}"] = [ client_type, client_config ] else: self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) for client_name, client_config in self.missing_clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setForeground(Qt.gray) self.gui.clients.addItem(client_item) self.log.error("Datataker missing client: " + client_name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.clearData.clicked.connect(self.clear_data) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet() def update_experiment_list(self): """ Updates list of experiments """ self.gui.exp.clear() for filename in os.listdir(self.exp_path): if filename.endswith('.py'): self.gui.exp.addItem(filename[:-3]) self.gui.exp.itemClicked.connect(self.display_experiment) def display_experiment(self, item): """ Displays the currently clicked experiment in the text browser :param item: (QlistWidgetItem) with label of name of experiment to display """ with open(os.path.join(self.exp_path, f'{item.text()}.py'), 'r') as exp_file: exp_content = exp_file.read() self.gui.exp_preview.setText(exp_content) self.gui.exp_preview.setStyleSheet('font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(0, 0, 0);') self.log.update_metadata(experiment_file=exp_content) def configure(self): """ Configures the currently selected experiment + dataset """ # If the experiment is running, do nothing try: if self.experiment_thread.isRunning(): self.log.warn('Did not configure experiment, since it ' 'is still in progress') return except: pass # Load the config self.reload_config() # Set all experiments to normal state and highlight configured expt for item_no in range(self.gui.exp.count()): self.gui.exp.item(item_no).setBackground( QtGui.QBrush(QtGui.QColor('black'))) self.gui.exp.currentItem().setBackground( QtGui.QBrush(QtGui.QColor('darkRed'))) exp_name = self.gui.exp.currentItem().text() self.module = importlib.import_module(exp_name) self.module = importlib.reload(self.module) # Clear graph area and set up new or cleaned up dataset for index in reversed(range(self.gui.graph_layout.count())): try: self.gui.graph_layout.itemAt(index).widget().deleteLater() except AttributeError: try: self.gui.graph_layout.itemAt(index).layout().deleteLater() except AttributeError: pass self.gui.windows = {} # If we're not setting up a new measurement type, just clear the data # We are reading in the required base-dataset by looking at the define_dataset() as defined in the experiment script. try: classname = self.module.define_dataset() except AttributeError: error_msg = "No 'define_dataset' method found in experiment script." self.log.error( "No 'define_dataset' method found in experiment script.") return try: self.dataset = getattr(datasets, classname)(gui=self.gui, log=self.log, config=self.config) except AttributeError: error_msg = f"Dataset name {classname} as provided in 'define_dataset' method in experiment script is not valid." self.log.error(error_msg) return # Run any pre-experiment configuration try: self.module.configure(dataset=self.dataset, **self.clients) except AttributeError: pass self.experiment = self.module.experiment self.log.info(f'Experiment {exp_name} configured') self.gui.exp_preview.setStyleSheet( 'font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def clear_data(self): """ Clears all data from curves""" self.log.info("Clearing data") self.dataset.clear_all_data() def run(self): """ Runs/stops the experiment """ # Run experiment if self.gui.run.text() == 'Run': self.gui.run.setStyleSheet('background-color: red') self.gui.run.setText('Stop') self.log.info('Experiment started') # Run update thread self.update_thread = UpdateThread( autosave=self.gui.autosave.isChecked(), save_time=self.gui.autosave_interval.value()) self.update_thread.data_updated.connect(self.dataset.update) self.update_thread.save_flag.connect(self.save) self.gui.autosave.toggled.connect( self.update_thread.update_autosave) self.gui.autosave_interval.valueChanged.connect( self.update_thread.update_autosave_interval) ''' # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # Step 6: Start the thread self.thread.start() # Final resets self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True) ) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0") ) ''' self.experiment_thread = ExperimentThread(self.experiment, dataset=self.dataset, gui=self.gui, **self.clients) self.experiment_thread.status_flag.connect( self.dataset.interpret_status) self.experiment_thread.finished.connect(self.stop) self.log.update_metadata( exp_start_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Stop experiment else: self.experiment_thread.running = False def stop(self): """ Stops the experiment""" self.gui.run.setStyleSheet('background-color: green') self.gui.run.setText('Run') self.log.info('Experiment stopped') self.update_thread.running = False self.log.update_metadata( exp_stop_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Autosave if relevant if self.gui.autosave.isChecked(): self.save() def save(self): """ Saves data """ self.log.update_metadata(notes=self.gui.notes.toPlainText()) filename = self.gui.save_name.text() directory = self.config['save_path'] self.dataset.save(filename=filename, directory=directory, date_dir=True) save_metadata(self.log, filename, directory, True) self.log.info('Data saved') def reload_config(self): """ Loads a new config file """ self.config = load_script_config(script='data_taker', config=self.gui.config.text(), logger=self.log)
class Wrap: def __init__(self, tagger, click_ch, start_ch, logger=None): """Instantiate gated counter :param tagger: instance of TimeTagger class :param click_ch: (int|list of int) clicks on all specified channels will be summed into one logical channel :param start_ch: (int) start trigger channel number """ # Log self.log = LogHandler(logger=logger) # Reference to tagger self._tagger = tagger # Log device ID information to demonstrate that connection indeed works serial = self._tagger.getSerial() model = self._tagger.getModel() self.log.info( 'Got reference to Swabian Instruments TimeTagger device \n' 'Serial number: {0}, Model: {1}' ''.format(serial, model)) # Gated Counter # reference to the TT.CountBetweenMarkers measurement instance self._ctr = None # number of count bins: # length of returned 1D count array, the size of allocated memory buffer. # must be given as argument of init_ctr() call self._bin_n = 0 # Channel assignments self._click_ch = 0 self._start_ch = 0 # reference to Combiner object # (if _click_ch is a list - then counts on all channels are summed # into virtual channel - self._combiner.getChannel()) self._combiner = None # apply channel assignment self.set_ch_assignment(click_ch=click_ch, start_ch=start_ch) # ---------------- Interface --------------------------- def activate_interface(self): return 0 def init_ctr(self, bin_n, bin_w): bin_w = int(bin_w / 1e-12) # Close existing counter, if it was initialized before self.close_ctr() # Instantiate counter measurement try: self._ctr = TT.TimeDifferences(tagger=self._tagger, click_channel=self._click_ch, start_channel=self._start_ch, n_bins=bin_n, binwidth=bin_w) # save bin_number in internal variable self._bin_n = bin_n # handle NotImplementedError (typical error, produced by TT functions) except NotImplementedError: # remove reference to the counter measurement self._ctr = None msg_str = 'init_ctr(): instantiation of TimeDifferences measurement failed' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Prepare counter to be started by start_counting() # (TimeDifferences measurement starts running immediately after instantiation, # so it is necessary to stop it and erase all counts collected between instantiation and stop() call) self._ctr.stop() self._ctr.clear() return 0 def close_ctr(self): # Try to stop and to clear TT.CountBetweenMarkers measurement instance try: self._ctr.stop() self._ctr.clear() except: pass # Remove reference, set status to "void" self._ctr = None return 0 def start_counting(self): # Try stopping and restarting counter measurement try: self._ctr.stop( ) # does not fail even if the measurement is not running self._ctr.clear() self._ctr.start() # Wait until the counter is actually ready to count time.sleep(0.1) return 0 # handle exception in TT function calls [NotImplementedError] except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'start_counting(): call failed. Counter was closed. \n'\ 'Re-initialize counter by calling init_ctr() again' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def stop_counting(self): # Try stopping counter measurement try: # stop counter self._ctr.stop() return 0 # handle exception in TT.stop()/TT.clear() except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'terminate_counting(): call failed. Counter was closed. \n' \ 'Re-initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def get_count_trace(self): return self._ctr.getData()[0] # ------------------------------------------------------ def get_ch_assignment(self): """Returns dictionary containing current channel assignment: { 'click_ch': (int) click_channel_number_including_edge_sign 'gate_ch': (int) gate_channel_number_including_edge_sign } :return: dict('click_ch': _, 'gate_ch': _) """ click_ch = copy.deepcopy(self._click_ch) start_ch = copy.deepcopy(self._start_ch) return dict(click_ch=click_ch, start_ch=start_ch) def set_ch_assignment(self, click_ch=None, start_ch=None): """Sets click channel and and gate channel. This method only changes internal variables self._click_ch and self._gate_ch. To apply the channel update, call init_ctr() again. :param click_ch: (int|list of int) click channel number positive/negative values - rising/falling edge detection if list is given, clicks on all specified channels will be merged into one logic channel :param start_ch: (int) channel number positive/negative - count during high/low gate level :return: (dict) actually channel assignment: { 'click_channel': (int) click_chnl_num, 'gate_channel': (int) gate_chnl_num } """ if click_ch is not None: # for convenience bring int type of input to list of int if isinstance(click_ch, list): click_ch_list = click_ch elif isinstance(click_ch, int): click_ch_list = [click_ch] else: # unknown input type msg_str = 'set_ch_assignment(click_ch={0}): invalid argument type'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # sanity check: all requested channels are available on the device all_chs = self.get_all_chs() for channel in click_ch_list: if channel not in all_chs: msg_str = 'set_ch_assignment(): '\ 'click_ch={0} - this channel is not available on the device'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # If several channel numbers were passed, create virtual Combiner channel if len(click_ch_list) > 1: self._combiner = TT.Combiner(tagger=self._tagger, channels=click_ch_list) # Obtain int channel number for the virtual channel click_ch_list = [self._combiner.getChannel()] # Set new value for click channel self._click_ch = int(click_ch_list[0]) if start_ch is not None: # sanity check: channel is available on the device if start_ch not in self.get_all_chs(): msg_str = 'set_ch_assignment(): '\ 'start_ch={0} - this channel is not available on the device'\ ''.format(start_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Set new value for gate channel self._start_ch = int(start_ch) return self.get_ch_assignment() def get_all_chs(self): """Returns list of all channels available on the device, including edge type sign. Positive/negative numbers correspond to detection of rising/falling edges. For example: 1 means 'rising edge on connector 1' -1 means 'falling edge on connector 1 :return: (list of int) list of channel numbers including edge sign. Example: [-8, -7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8] Empty list is returned in the case of error. """ # Sanity check: check that connection to the device was established if self._tagger is None: msg_str = 'get_all_chs(): not connected to the device yet' self.log.error(msg_str=msg_str) raise CtrError(msg_str) channel_list = list( self._tagger.getChannelList( TT.TT_CHANNEL_RISING_AND_FALLING_EDGES)) return channel_list
class DLC_Pro: """ Driver class for Toptica DLC Pro """ def __init__(self, host, port=1998, logger=None, num_lasers=2): """ Instantiates DLC_Pro object :param host: (str) hostname of laser (IP address) :param port: (int) port number, toptica defaults to 1998 :num_lasers: Number of installed lasers :param logger: (LogClient) """ self.host = host self.port = port self.log = LogHandler(logger) self.dlc = None self.laser_nums = range(1, num_lasers + 1) # Check connection try: # Check laser connection self.dlc = Telnet(host=self.host, port=self.port) self.dlc.read_until(b'>', timeout=1) for laser_num in self.laser_nums: self._check_laser_connection(laser_num) except ConnectionRefusedError: self.log.error('Could not connect to Toptica DLC Pro at ' f'IP address: {self.host}, port: {self.port}') def _check_laser_connection(self, laser_num=1): """ Read out laser number :laser_num: (int) 1 or 2, indicating laser 1 or laser 2. """ self.dlc.read_until(b'>', timeout=1) self.dlc.write( f"(param-disp 'laser{laser_num}:dl:type)\n".encode('utf')) laser_type = self.dlc.read_until( b'>', timeout=1).split()[-3].decode('utf')[1:-1] self.dlc.write( f"(param-disp 'laser{laser_num}:dl:serial-number)\n".encode('utf')) serial = int( self.dlc.read_until(b'>', timeout=1).split()[-3].decode('utf')[1:-1]) self.log.info( f'Connected to Toptica {laser_type} {laser_num}, S/N {serial}') def is_laser_on(self, laser_num=1): """ Checks if the laser is on or off :return: (bool) whether or not emission is on or off """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:emission)\n".encode('utf')) result = self.dlc.read_until(b'>', timeout=1).split()[-3].decode('utf') status = result[1] if status == 't': return True elif status == 'f': return False else: self.log.warn( 'Could not determine properly whether the laser is on or off') return False def turn_on(self, laser_num=1): """ Turns on the laser """ # Check if laser is on already if self.is_laser_on(laser_num): self.log.info(f'Laser {laser_num} is already on') else: self.dlc.write( f"(param-set! 'laser{laser_num}:dl:cc:enabled #t)\n".encode( 'utf')) self.dlc.read_until(b'>', timeout=1) if self.is_laser_on(laser_num): self.log.info(f'Turned on Toptica DL-Pro laser {laser_num}') else: self.log.warn( f'Laser {laser_num} could not be turned on. Physical emission button may need to be pressed' ) def turn_off(self, laser_num=1): """ Turns off the laser """ if self.is_laser_on(laser_num): self.dlc.write( f"(param-set! 'laser{laser_num}:dl:cc:enabled #f)\n".encode( 'utf')) self.dlc.read_until(b'>', timeout=1) if self.is_laser_on(laser_num): self.log.warn( f'Failed to verify that DL-Pro laser {laser_num} turned off' ) else: self.log.info(f'Turned off Toptica DL-Pro laser {laser_num}') else: self.log.info(f'Laser {laser_num} is already off') def voltage(self, laser_num=1): """ Gets current voltage on laser piezo :return: (float) current voltage on piezo """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:pc:voltage-set)\n".encode( 'utf')) voltage = float(self.dlc.read_until(b'>', timeout=1).split()[-3]) return voltage def set_voltage(self, voltage, laser_num=1): """ Sets voltage to the piezo :param voltage: (float) voltage to set """ v = deepcopy(voltage) write_data = f"(param-set! 'laser{laser_num}:dl:pc:voltage-set {v})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def current_sp(self, laser_num=1): """ Gets current setpoint :return: (float) value of current setpoint """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:current-set)\n".encode( 'utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def current_act(self, laser_num=1): """ Gets measured current :return: (float) value of actual current """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:current-act)\n".encode( 'utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def set_current(self, current, laser_num=1): """ Sets the current to desired value :param current: (float) value of current to set as setpoint """ c = deepcopy(current) write_data = f"(param-set! 'laser{laser_num}:dl:cc:current-set {c})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def temp_sp(self, laser_num=1): """ Gets temperature setpoint :return: (float) value of temperature setpoint """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:tc:temp-set)\n".encode('utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def temp_act(self, laser_num=1): """ Gets actual DL temp :return: (float) value of temperature """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:tc:temp-act)\n".encode('utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def set_temp(self, temp, laser_num=1): """ Sets the current to desired value :param temp: (float) value of temperature to set to in Celsius """ t = deepcopy(temp) write_data = f"(param-set! 'laser{laser_num}:dl:tc:temp-set {t})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def configure_scan(self, offset=65, amplitude=100, frequency=0.2, laser_num=1): """ Sets the scan parameters for piezo scanning :param offset: (float) scan offset (center value) in volts (between 0 and 130) :param amplitude: (float) scan amplitude (peak to peak) in volts :param frequency: (Float) scan frequency in Hz """ # Check that parameters are within range if (offset + (amplitude / 2) > 130) or (offset - (amplitude / 2) < -1): self.log.warn('Warning, invalid scan parameters set.' 'Make sure voltages are between -1 and 130 V') else: o = deepcopy(offset) write_data = f"(param-set! 'laser{laser_num}:scan:offset {o})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) a = deepcopy(amplitude) write_data = f"(param-set! 'laser{laser_num}:scan:amplitude {a})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) f = deepcopy(frequency) write_data = f"(param-set! 'laser{laser_num}:scan:frequency {f})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) self.log.info( f'Scan with offset {o}, amplitude {a}, frequency {f} for laser {laser_num} successfully configured.' ) def start_scan(self, laser_num=1): """ Starts a piezo scan """ write_data = f"(param-set! 'laser{laser_num}:scan:enabled #t)\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def stop_scan(self, laser_num=1): """ Stops piezo scan """ write_data = f"(param-set! 'laser{laser_num}:scan:enabled #f)\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1)
class Sweep1D: def __init__(self, logger=None, sweep_type='triangle'): """ Instantiates sweeper :param logger: instance of LogClient """ self.log = LogHandler(logger) self.min = 0 self.max = 1 self.pts = 51 self.experiment = None self.fixed_params = {} self.iplot_fwd = None self.hplot_fwd = None self.iplot_bwd = None self.hplot_bwd = None self.sweep_type = sweep_type self.reps = 0 self.stop_flag = False self.stop_end_flag = False self.x_label = None self.y_label = None self.autosave = False # Setup stylesheet. #self.gui.apply_stylesheet() def set_parameters(self, **kwargs): """ Configures all parameters :param kwargs: (dict) containing parameters :min: (float) minimum value to sweep from :max: (float) maximum value to sweep to :pts: (int) number of points to use :reps: (int) number of experiment repetitions :sweep_type: (str) 'triangle' or 'sawtooth' supported :x_label: (str) Label of x axis :y_label: (str) label of y axis """ if 'min' in kwargs: self.min = kwargs['min'] if 'max' in kwargs: self.max = kwargs['max'] if 'pts' in kwargs: self.pts = kwargs['pts'] if 'sweep_type' in kwargs: sweep_str = kwargs['sweep_type'] if sweep_str not in ['sawtooth', 'triangle']: self.log.error( 'Sweep type must be either "sawtooth" or "triangle".' ) self.sweep_type = sweep_str if 'reps' in kwargs: self.reps = kwargs['reps'] if 'x_label' in kwargs: self.x_label = kwargs['x_label'] if 'y_label' in kwargs: self.y_label = kwargs['y_label'] def configure_experiment( self, experiment, experiment_params={} ): """ Sets the experimental script to a provided module :param experiment: (callable) method to run :param experiment_params: (dict) containing name and value of fixed parameters """ self.experiment = experiment self.fixed_params = experiment_params def run_once(self, param_value): """ Runs the experiment once for a parameter value :param_value: (float) value of parameter to use :return: (float) value resulting from experiment call """ result = self.experiment( param_value, **self.fixed_params ) if not result: self.widgets['rep_tracker'].setValue(0) self.widgets['reps'].setValue(0) for button in self.widgets['avg']: button.setText('Avg only') self.widgets['run'].setStyleSheet('background-color: green') self.widgets['run'].setText('Run') self.stop() self.log.info('Sweep experiment auto-aborted') return result def run(self, plot=False, autosave=None, filename=None, directory=None, date_dir=True): """ Runs the sweeper :param plot: (bool) whether or not to display the plotly plot :param autosave: (bool) whether or not to autosave :param filename: (str) name of file identifier :param directory: (str) filepath to save to :param date_dir: (bool) whether or not to store in date-specific sub-directory """ if autosave is not None: self.autosave = autosave sweep_points = self._generate_x_axis() if self.sweep_type != 'sawtooth': bw_sweep_points = self._generate_x_axis(backward=True) self._configure_plots(plot) reps_done = 0 self.stop_flag = False while (reps_done < self.reps or self.reps <= 0 and not self.stop_flag): self._reset_plots() for x_value in sweep_points: if self.stop_flag: break self._run_and_plot(x_value) if self.sweep_type != 'sawtooth': for x_value in bw_sweep_points: if self.stop_flag: break self._run_and_plot(x_value, backward=True) if self.stop_flag: break reps_done += 1 self._update_hmaps(reps_done) self._update_integrated(reps_done) # Autosave at every iteration if self.autosave: self.save(filename, directory, date_dir) # Print progress print(f'Finished {reps_done} out of {self.reps} sweeps.') def stop(self): """ Terminates the sweeper immediately """ self.stop_flag = True def set_reps(self, reps=1): """ Sets the repetitions to be done :param reps: (int) number of refs default = 1, can be used to terminate at end of current rep """ self.reps = reps def save(self, filename=None, directory=None, date_dir=True): """ Saves the dataset :param filename: (str) name of file identifier :param directory: (str) filepath to save to :param date_dir: (bool) whether or not to store in date-specific sub-directory """ if filename is None: filename = 'sweeper_data' # Save heatmap generic_save( data=self.hplot_fwd._fig.data[0].z, filename=f'{filename}_fwd_scans', directory=directory, date_dir=date_dir ) # Save heatmap png # plotly_figure_save( # self.hplot_fwd._fig, # filename=f'{filename}_fwd_scans', # directory=directory, # date_dir=date_dir # ) # Save average generic_save( data = np.array( [self.iplot_fwd._fig.data[1].x, self.iplot_fwd._fig.data[1].y] ), filename=f'{filename}_fwd_avg', directory=directory, date_dir=date_dir ) if self.sweep_type != 'sawtooth': # Save heatmap generic_save( data=self.hplot_bwd._fig.data[0].z, filename=f'{filename}_bwd_scans', directory=directory, date_dir=date_dir ) # Save average generic_save( data = np.array( [self.iplot_fwd._fig.data[1].x, self.iplot_fwd._fig.data[1].y] ), filename=f'{filename}_bwd_avg', directory=directory, date_dir=date_dir ) def _generate_x_axis(self, backward=False): """ Generates an x-axis based on the type of sweep Currently only implements triangle :param backward: (bool) whether or not it is a backward scan :return: (np.array) containing points to scan over """ if backward: return np.linspace(self.max, self.min, self.pts) else: return np.linspace(self.min, self.max, self.pts) def _configure_plots(self, plot): """ Configures all plots :param plot: (bool) whether or not to display the plotly plot """ # single-trace scans self.iplot_fwd = MultiTraceFig(title_str='Forward Scan', ch_names=['Single', 'Average']) self.iplot_fwd.set_data(x_ar=np.array([]), y_ar=np.array([]), ind=0) self.iplot_fwd.set_data(x_ar=np.array([]), y_ar=np.array([]), ind=1) self.iplot_fwd.set_lbls(x_str=self.x_label, y_str=self.y_label) # heat map self.hplot_fwd = HeatMapFig(title_str='Forward Scans') self.hplot_fwd.set_data( x_ar=np.linspace(self.min, self.max, self.pts), y_ar=np.array([]), z_ar=np.array([[]]) ) self.hplot_fwd.set_lbls( x_str=self.x_label, y_str='Repetition number', z_str=self.y_label ) # Show plots if enabled if plot: self.iplot_fwd.show() self.hplot_fwd.show() if self.sweep_type != 'sawtooth': self.iplot_bwd = MultiTraceFig(title_str='Backward Scan', ch_names=['Single', 'Average']) self.iplot_bwd.set_data(x_ar=np.array([]), y_ar=np.array([]), ind=0) self.iplot_bwd.set_data(x_ar=np.array([]), y_ar=np.array([]), ind=1) self.iplot_bwd.set_lbls(x_str=self.x_label, y_str=self.y_label) # heat map self.hplot_bwd = HeatMapFig(title_str='Backward Scans') self.hplot_bwd.set_data( x_ar=np.linspace(self.max, self.min, self.pts), y_ar=np.array([]), z_ar=np.array([[]]) ) self.hplot_bwd.set_lbls( x_str=self.x_label, y_str='Repetition number', z_str=self.y_label ) # Show plots if enabled self.iplot_bwd.show() self.hplot_bwd.show() def _run_and_plot(self, x_value, backward=False): """ Runs the experiment for an x value and adds to plot :param x_value: (double) experiment parameter :param backward: (bool) whether or not backward or forward """ y_value = self.run_once(x_value) if backward: self.iplot_bwd.append_data(x_ar=x_value, y_ar=y_value, ind=0) else: self.iplot_fwd.append_data(x_ar=x_value, y_ar=y_value, ind=0) def _update_hmaps(self, reps_done): """ Updates heat map plots :param reps_done: (int) number of repetitions done """ if reps_done == 1: self.hplot_fwd.set_data( y_ar=np.array([1]), z_ar=[self.iplot_fwd._y_ar] ) if self.sweep_type != 'sawtooth': self.hplot_bwd.set_data( y_ar=np.array([1]), z_ar=[self.iplot_bwd._y_ar] ) else: self.hplot_fwd.append_row(y_val=reps_done, z_ar=self.iplot_fwd._fig.data[0].y) if self.sweep_type != 'sawtooth': self.hplot_bwd.append_row(y_val=reps_done, z_ar=self.iplot_bwd._fig.data[0].y) def _reset_plots(self): """ Resets single scan traces """ self.iplot_fwd.set_data(x_ar=np.array([]), y_ar=np.array([])) if self.sweep_type != 'sawtooth': self.iplot_bwd.set_data(x_ar=np.array([]), y_ar=np.array([])) def _update_integrated(self, reps_done): """ Updates integrated plots :param reps_done: (int) number of repetitions completed """ if reps_done==1: self.iplot_fwd.set_data( x_ar=np.linspace(self.min, self.max, self.pts), y_ar=self.iplot_fwd._fig.data[0].y, ind=1 ) if self.sweep_type != 'sawtooth': self.iplot_bwd.set_data( x_ar=np.linspace(self.max, self.min, self.pts), y_ar=self.iplot_bwd._fig.data[0].y, ind=1 ) else: self.iplot_fwd.set_data( x_ar=np.linspace(self.min, self.max, self.pts), y_ar=((self.iplot_fwd._fig.data[1].y*(reps_done-1)/reps_done) +self.iplot_fwd._fig.data[0].y/reps_done), ind=1 ) if self.sweep_type != 'sawtooth': self.iplot_bwd.set_data( x_ar=np.linspace(self.max, self.min, self.pts), y_ar=((self.iplot_bwd._fig.data[1].y*(reps_done-1)/reps_done) +self.iplot_bwd._fig.data[0].y/reps_done), ind=1 )
class Driver(WavemeterInterface): """ Hardware class to control High Finesse Wavemeter.""" def __init__(self, logger=None): """ Instantiate wavemeter :param logger: instance of LogClient class (optional) """ # Log self.log = LogHandler(logger=logger) # Load WLM DLL try: self._wavemeterdll = ctypes.windll.LoadLibrary('wlmData.dll') except: msg_str = 'High-Finesse WS7 Wavemeter is not properly installed on this computer' self.log.error(msg_str) raise WavemeterError(msg_str) # Set all DLL function parameters and return value types self._wavemeterdll.GetWLMVersion.restype = ctypes.c_long self._wavemeterdll.GetWLMVersion.argtype = ctypes.c_long self._wavemeterdll.GetWLMCount.restype = ctypes.c_long self._wavemeterdll.GetWLMCount.argtype = ctypes.c_long self._wavemeterdll.GetWavelengthNum.restype = ctypes.c_double self._wavemeterdll.GetWavelengthNum.argtypes = [ ctypes.c_long, ctypes.c_double ] self._wavemeterdll.GetFrequencyNum.restype = ctypes.c_double self._wavemeterdll.GetFrequencyNum.argtypes = [ ctypes.c_long, ctypes.c_double ] # Check that WLM is running and log details self._is_running = self._wavemeterdll.GetWLMCount(0) if self._is_running > 0: self._wlm_version = self._wavemeterdll.GetWLMVersion(0) self.log.info('Connected to High-Finesse Wavemeter WS-{0}'.format( self._wlm_version)) else: msg_str = 'High-Finesse WS7 Wavemeter software not running.\n' 'Please run the Wavemeter software and try again.' self.log.warn(msg_str) # raise WavemeterError(msg_str) def get_wavelength(self, channel=1, units='Frequency (THz)'): """ Returns the wavelength in specified units for a given channel :param channel: Channel number from 1-8 :param units: "Frequency (THz)" or "Wavelength (nm)". Defaults to frequency. """ if units == 'Wavelength (nm)': return self._wavemeterdll.GetWavelengthNum(channel, 0) else: return self._wavemeterdll.GetFrequencyNum(channel, 0)
class Driver(): def reset(self): """ Create factory reset""" self.device.write('FAC;WAIT') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class. :gpib_address: GPIB-address of the scope, e.g. 'GPIB0::12::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: And instance of a LogClient. """ # Instantiate log. self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # We set a more forgiving timeout of 10s (default: 2s). self.device.timeout = 10000 # Reset to factory settings. self.reset() # Set all attenuations to 1x. for channel in CHANNEL_LIST: self.set_channel_attenuation(channel, 1) def get_trigger_source(self): """ Return Trigger source.""" # Query trigger source. res = self.device.query('TRIG:MAI:EDGE:SOU?') # Tidy up response using regex. trig_channel = re.compile( ':TRIGGER:MAIN:EDGE:SOURCE[ ]([^\\n]+)' ).match(res).group(1) return trig_channel def set_trigger_source(self, trigger_source): """ Set trigger source.""" if trigger_source not in TRIGGER_SOURCE_LIST: self.log.error( f"'{trigger_source}' no found, available trigger sources are {TRIGGER_SOURCE_LIST}.'" ) # Set trigger source. self.device.write(f'TRIG:MAI:EDGE:SOU {trigger_source}') def set_timing_scale(self, scale): """ Set the time base. This defines the available display window, as 10 divisions are displayed. :scale: Time per division (in s) """ self.device.write(":HORIZONTAL:MAIN:SCALE {:e}".format(scale)) def extract_params(self, command, value): """ Uses regex to extract float values from return values. :command: The command used to query, without the final '?' :value: The return value of a query. """ value = float(re.compile( f'{command}[ ]([0-9\.\+Ee-]+)' ).match(value).group(1)) return value def get_timing_scale(self): """ Get time base in secs per division.""" command = ":HORIZONTAL:MAIN:SCALE" timing_res = self.device.query(f"{command}?") timing_res = self.extract_params(command, timing_res) return timing_res def set_single_run_acq(self): """Set acquisition mode to single run.""" self.device.write('acquire:stopafter sequence') def acquire_single_run(self): """ Run single acquisition.""" self.device.write('acquire:state on') def _check_channel(self, channel): """ CHeck if channel is in CHANNEL list.""" if channel not in CHANNEL_LIST: self.log.error( f"The channel '{channel}' is not available, available channels are {CHANNEL_LIST}." ) def unitize_trace(self, trace, trace_preamble): """Transform unitless trace to trace with units, constructs time array. :trace: (np.array) Unitless array as provided by oscilloscope. :trace_preamble: (string) Waveform preamble. Returns trace, a np.array in correct units, ts, the time array in seconds, and y_unit, the unit of the Y-axis. """ # Overcharged regex extracting all relevant paramters. wave_pre_regex = 'NR_PT (?P<n_points>[0-9\.\+Ee-]+).+XINCR (?P<x_incr>[0-9\.\+Ee-]+).+PT_OFF (?P<pt_off>[0-9\.\+Ee-]+).+XZERO (?P<x_zero>[0-9\.\+Ee-]+).+XUNIT "(?P<x_unit>[^"]+).+YMULT (?P<y_mult>[0-9\.\+Ee-]+).+YZERO (?P<y_zero>[0-9\.\+Ee-]+).+YOFF (?P<y_off>[0-9\.\+Ee-]+).+YUNIT "(?P<y_unit>[^"]+)' wave_pre_matches = re.search(wave_pre_regex, trace_preamble) # Adjust trace as shown in the coding manual 2-255. trace = ( trace - float(wave_pre_matches['y_off']) ) * \ float(wave_pre_matches['y_mult']) + \ float(wave_pre_matches['y_zero']) # Construct timing array as shown in the coding manual 2-250. ts = float(wave_pre_matches['x_zero']) + \ ( np.arange(int(wave_pre_matches['n_points'])) - int(wave_pre_matches['pt_off']) ) * float(wave_pre_matches['x_incr']) x_unit = wave_pre_matches['x_unit'] y_unit = wave_pre_matches['y_unit'] # Construct trace dictionary. trace_dict = { 'trace': trace, 'ts': ts, 'x_unit': x_unit, 'y_unit': y_unit } return trace_dict def read_out_trace(self, channel, curve_res=1): """ Read out trace :channel: Channel to read out (must be in CHANNEL_LIST). :curve_res: Bit resolution for returned data. If 1, value range is from -127 to 127, if 2, the value range is from -32768 to 32768. Returns np.array of sample points (in unit of Voltage divisions) and corresponding array of times (in seconds). """ self._check_channel(channel) # Enable trace. self.show_trace(channel) # Run acquisition. self.acquire_single_run() if curve_res not in [1, 2]: self.log.error("The bit resolution of the curve data must be either 1 or 2.") # Set curve data to desired bit. self.device.write(f'DATa:WIDth {curve_res}') # Set trace we want to look at. self.device.write(f'DATa:SOUrce {channel}') # Set encoding. self.device.write('data:encdg ascii') # Read out trace. res = self.device.query('curve?') # Tidy up curve. raw_curve = res.replace(':CURVE', '').replace(' ', '').replace('\n', '') # Transform in numpy array. trace = np.fromstring(raw_curve, dtype=int, sep=',') # Read wave preamble. wave_pre = self.device.query('WFMPre?') # Transform units of trace. trace_dict = self.unitize_trace(trace, wave_pre) return trace_dict def show_trace(self, channel): """Display trace. Required for trace readout. """ self._check_channel(channel) self.device.write(f'SELect:{channel} 1') def hide_trace(self, channel): """Hide trace.""" self._check_channel(channel) self.device.write(f'SELect:{channel} 0') def _check_channel_attenuation(self, attenuation): """Check if attenuation is within option set.""" if attenuation not in ATTENUATIONS: self.log.error( f"The attenuation '{attenuation}x' is not available, available attenuations are {ATTENUATIONS}." ) def get_channel_attenuation(self, channel): """Get the attenuation of the channel. :channel: (str) Channel, possible values see CHANNEL_LIST. """ # Check if channel and attenuation is valid. self._check_channel(channel) # Get attenuation. command = f":{channel}:PROBE" attenuation = self.device.query(f"{command}?") # Extract float. attenuation = self.extract_params(command, attenuation) return attenuation def set_channel_attenuation(self, channel, attenuation): """Set the attenuation of the channel. This setting will scale the y-axis unit accordingly :channel: (str) Channel, possible values see CHANNEL_LIST. :attenuation: (int) Attenuation, possible values see ATTENUATIONS. """ # Check if channel and attenuation is valid. self._check_channel(channel) self._check_channel_attenuation(attenuation) # Set attenuation. self.device.write(f'{channel}:PRObe {attenuation}') def get_channel_scale(self, channel): """ Return vertical scale of channel. :channel: (str) Channel, possible values see CHANNEL_LIST. """ self._check_channel(channel) command = f":{channel}:SCALE" scale = self.device.query(f"{command}?") # Extract float. scale = self.extract_params(command, scale) return scale def set_channel_scale(self, channel, range): """ Return vertical scale of channel. :channel: (str) Channel, possible values see CHANNEL_LIST. :range: (float) Vertical range, in Volt/vertical division. Corresponds to 'Scale' turning knob. Must be between 5 mv/div and 5 V/div. """ self._check_channel(channel) if not (5e-3 <= range <= 5): self.log.error('Range must be between 5 mv/div and 5 V/div.') # Set scale. self.device.write(f'{channel}:SCAle {range}') def get_channel_pos(self, channel): """Get vertical position of channel trace. :channel: (str) Channel, possible values see CHANNEL_LIST. """ self._check_channel(channel) command = f":{channel}:POSITION" pos = self.device.query(f"{command}?") # Extract float. pos = self.extract_params(command, pos) return pos def set_channel_pos(self, channel, pos): """Set vertical position of channel trace. :channel: (str) Channel, possible values see CHANNEL_LIST. :pos: (str) Vertical postion, in divs above center graticule. The maximum and minimum value of pos depends on the channel scale. """ self._check_channel(channel) self.device.write(f'{channel}:POS {pos}') def get_horizontal_position(self): """Get the horizontal position of the traces. The return value in seconds is the difference between the trigger point ant the center graticule. """ command = ":HORIZONTAL:MAIN:POSITION" hor_pos = self.device.query(f"{command}?") hor_pos = self.extract_params(command, hor_pos) return hor_pos def set_horizontal_position(self, hor_pos): """Set the horizontal position of the traces. The return value in seconds is the difference between the trigger point ant the center graticule. :hor_pos: (float) Horizontal position in s. """ command = ":HORIZONTAL:MAIN:POSITION" self.device.write(f"{command} {hor_pos}") def trig_level_to_fifty(self): """Set main trigger level to 50%""" self.device.write('TRIGger:MAIn SETLEVel') def get_trigger_level(self): """Get trigger level.""" trig_level = self.device.query(':TRIGGER:MAIN:LEVEL?') trig_level = self.extract_params(':TRIGGER:MAIN:LEVEL', trig_level) return trig_level def set_trigger_level(self, trigger_level): """Set trigger level. :trigger_level: (float) Trigger level in Volts. """ self.device.write(f':TRIGGER:MAIN:LEVEL {trigger_level}')
class Driver: """Driver class for NI PXI 654x HSDIO card """ def __init__(self, dev_name_str, logger=None): # # Define internal variables # self.log = LogHandler(logger=logger) self.map_dict = dict() self.writn_wfm_set = set() # # "Load" niHSDIO DLL # try: self.dll = ctypes.WinDLL(NI_HSDIO_DLL_PATH) except OSError: msg_str = 'DLL loading failed. \n' \ 'Ensure that niHSDIO DLL path is correct: \n' \ 'it should be specified in pylabnet.hardware.p_gen.ni_hsdio.__init__.py \n' \ 'Current value is: \n' \ '"{}"'.format(NI_HSDIO_DLL_PATH) self.log.error(msg_str=msg_str) raise PGenError(msg_str) # Build C-prototypes (in particular, specify the return # types such that Python reads results correctly) build_c_func_prototypes(self.dll) # # Connect to device # # Create blank session handle self._handle = NITypes.ViSession() # Convert dev_name to the DLL-readable format self._dev_name = NITypes.ViRsrc(dev_name_str.encode('ascii')) self._er_chk( self.dll.niHSDIO_InitGenerationSession( self._dev_name, # ViRsrc resourceName NIConst.VI_TRUE, # ViBoolean IDQuery NIConst.VI_TRUE, # ViBoolean resetDevice NIConst. VI_NULL, # ViConstString optionString - not used, set to VI_NULL ctypes.byref(self._handle) # ViSession * session_handle )) # Log info message serial_str = self._get_attr_str(NIConst.NIHSDIO_ATTR_SERIAL_NUMBER) self.log.info( 'Connected to NI HSDIO card {0}. Serial number: {1}. Session handle: {2}' ''.format(dev_name_str, serial_str, self._handle)) def reset(self): self.writn_wfm_set = set() self.map_dict = dict() return self._er_chk(self.dll.niHSDIO_reset(self._handle)) def start(self): return self._er_chk(self.dll.niHSDIO_Initiate(self._handle)) def stop(self): return self._er_chk(self.dll.niHSDIO_Abort(self._handle)) def disconnect(self): self.reset() op_status = self._er_chk(self.dll.niHSDIO_close(self._handle)) return op_status # ================================================================ # Hardware settings # ================================================================ def get_samp_rate(self): return self._get_attr_real64(NIConst.NIHSDIO_ATTR_SAMPLE_CLOCK_RATE) def set_samp_rate(self, samp_rate): # Sanity check if not samp_rate <= self.constraints['samp_rate']['max']: self.log.warn( 'set_samp_rate({0} MHz): the requested value exceeds hardware constraint max={1} MHz.\n' 'The max possible value will be set instead.' ''.format(samp_rate / 1e6, self.constraints['samp_rate']['max'] / 1e6)) samp_rate = self.constraints['samp_rate']['max'] elif not self.constraints['samp_rate']['min'] <= samp_rate: self.log.warn( 'set_samp_rate({0} Hz): the requested value is below the hardware constraint min={1} Hz.\n' 'The min possible value will be set instead.' ''.format(samp_rate, self.constraints['samp_rate']['min'])) samp_rate = self.constraints['samp_rate']['min'] # Call DLL function # Currently, the onboard clock is used as the sample clock source self._er_chk( self.dll.niHSDIO_ConfigureSampleClock( self._handle, # ViSession vi NIConst. NIHSDIO_VAL_ON_BOARD_CLOCK_STR, # ViConstString clockSource NITypes.ViReal64(samp_rate) # ViReal64 clockRate )) # Return the actual final sample rate return self.get_samp_rate() def get_active_chs(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_DYNAMIC_CHANNELS) def set_active_chs(self, chs_str=None): if chs_str is None: # un-assign all channels chs_str = 'none' chs_str = NITypes.ViString(chs_str.encode('ascii')) self._er_chk( self.dll.niHSDIO_AssignDynamicChannels( self._handle, # ViSession vi, chs_str # ViConstString channelList )) return self.get_active_chs() def get_mode(self): """ :return: (str) "W" - Waveform, "S" - Scripted """ mode_id = self._get_attr_int32( attr_id=NIConst.NIHSDIO_ATTR_GENERATION_MODE) if mode_id == NIConst.NIHSDIO_VAL_WAVEFORM.value: return 'W' elif mode_id == NIConst.NIHSDIO_VAL_SCRIPTED.value: return 'S' else: msg_str = 'get_mode(): self._get_attr_int32(NIHSDIO_ATTR_GENERATION_MODE) ' \ 'returned unknown mode_id = {0}'.format(mode_id) self.log.error(msg_str) raise PGenError(msg_str) def set_mode(self, mode_string): """ :param mode_string: (str) "W" - Waveform, "S" - Scripted :return: actual run mode string ("W"/"S") """ if mode_string == 'W': run_mode = NIConst.NIHSDIO_VAL_WAVEFORM elif mode_string == 'S': run_mode = NIConst.NIHSDIO_VAL_SCRIPTED else: msg_str = 'set_mode({0}): invalid argument. Valid values: "W" - Waveform, "S" - scripted. \n' \ 'Run mode was not changed. Actual run mode string was returned.'.format(mode_string) self.log.error(msg_str=msg_str) raise PGenError(msg_str) # Call DLL function self._er_chk( self.dll.niHSDIO_ConfigureGenerationMode( self._handle, # ViSession vi run_mode # ViInt32 generationMode )) # Return actual run mode return self.get_mode() @property def constraints(self): # Total memory size # [in samples; one sample contains 32 bits and covers all channels] max_wfm_len = self._get_attr_int32( attr_id=NIConst.NIHSDIO_ATTR_TOTAL_GENERATION_MEMORY_SIZE) constr_dict = dict(samp_rate=dict(min=48, max=100e6), wfm_len=dict(min=2, step=2, max=max_wfm_len)) return constr_dict def get_status(self): try: # Record current samp_rate to restore it later current_rate = self.get_samp_rate() rate_lims = self.constraints['samp_rate'] test_rate = (rate_lims['min'] + rate_lims['max']) / 2 # Try changing samp_rate op_status = self.dll.niHSDIO_ConfigureSampleClock( self._handle, # ViSession vi NIConst. NIHSDIO_VAL_ON_BOARD_CLOCK_STR, # ViConstString clockSource NITypes.ViReal64(test_rate) # ViReal64 clockRate ) # If device is idle, operation should be successful: # op_status = 0. # Restore original samp rate and return 0 - "idle" if op_status == 0: self.set_samp_rate(samp_rate=current_rate) return 0 # If device is running, attempt to change samp_rate should return # the following error code: # -1074118617 # "Specified property cannot be set while the session is running. # Set the property prior to initiating the session, # or abort the session prior to setting the property." elif op_status == -1074118617: # Device is running return 1 # This method cannot interpret any other error/warning code and has # to raise an exception else: raise PGenError( 'get_status(): the attempt to test-change samp_rate returned unknown error code {}' ''.format(op_status)) # If connection to the device is lost # or any operation fails, raise an exception. except Exception as exc_obj: self.log.exception( 'get_status(): an exception was produced. \n' 'This might mean that connection to the device is lost ' 'or there is some bug in the get_status() method. \n') raise exc_obj # ================================================================ # Waveform Generation # ================================================================ def write_wfm(self, pb_obj, len_adj=True): # # Sanity checks # # Only data_width=32 write is currently implemented # (DLL function niHSDIO_WriteNamedWaveformU32) hrdw_data_width = 8 * self._get_attr_int32( NIConst.NIHSDIO_ATTR_DATA_WIDTH) if hrdw_data_width != 32: msg_txt = 'write_wfm(): the card you use has data_width = {0} bits. \n' \ 'The method was written assuming 32-bit width and have to be modified for your card. \n' \ 'Rewrite bit_ar construction part and use niHSDIO_WriteNamedWaveformU{1}() DLL function' \ ''.format(hrdw_data_width, hrdw_data_width) self.log.error(msg_txt) raise PGenError(msg_txt) # # Sample PulseBlock # # Map user-friendly names onto physical channel numbers pb_obj = copy.deepcopy(pb_obj) pb_obj.ch_map(map_dict=self.map_dict) # Sample pulse block samp_rate = self.get_samp_rate() samp_dict, n_pts, add_pts = pb_sample( pb_obj=pb_obj, samp_rate=samp_rate, len_min=self.constraints['wfm_len']['min'], len_max=self.constraints['wfm_len']['max'], len_step=self.constraints['wfm_len']['step'], len_adj=len_adj) wfm_name = pb_obj.name del pb_obj self.log.info( 'write_wfm(): sampled PulseBlock "{}". \n' 'Sample array has {} points. {} samples were added to match hardware wfm len step' ''.format(wfm_name, n_pts, add_pts)) # # Pack samp_dict into bit_ar # # Create a blank bit_ar - all elements zero (all channels off) bit_ar = np.zeros(shape=n_pts, dtype=np.uint32) # Iterate through each channel and set corresponding bit to '1' # if value is True for ch_num in samp_dict.keys(): # This number in uint32 representation has all zeros and # exactly one '1' at the ch_num-th bit from the LSB ch_bit_one = 2**ch_num for idx, val in enumerate(samp_dict[ch_num]): if val: bit_ar[idx] += ch_bit_one # TODO: consider making very fast with numpy: # ch_bit_ar = samp_dict[ch_num].astype(int) * 2**ch_num # bit_ar = np.add(bit_ar, ch_bit_ar) # # Load bit_ar to memory # # Delete waveform with the same name, # if it is already present in the memory if wfm_name in self.writn_wfm_set: self.del_wfm(wfm_name=wfm_name) # Create C-pointer to bit_ar using numpy.ndarray.ctypes attribute bit_ar_ptr = bit_ar.ctypes.data_as(ctypes.POINTER(NITypes.ViUInt32)) # Call DLL function self._er_chk( self.dll.niHSDIO_WriteNamedWaveformU32( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')), # ViConstString waveformName NITypes.ViInt32(n_pts), # ViInt32 samplesToWrite bit_ar_ptr # ViUInt32 data[] )) self.writn_wfm_set.add(wfm_name) return 0 def del_wfm(self, wfm_name): self._er_chk( self.dll.niHSDIO_DeleteNamedWaveform( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')) # ViConstString waveformName )) self.writn_wfm_set.remove(wfm_name) return 0 def clr_mem(self): wfm_set = copy.deepcopy(self.writn_wfm_set) for wfm_name in wfm_set: self.del_wfm(wfm_name=wfm_name) return 0 def get_rep(self): """Returns number of repetitions in Waveform generation mode. On the hardware level, it is just a pair of attributes NIHSDIO_ATTR_REPEAT_MODE and NIHSDIO_ATTR_REPEAT_COUNT which are not bound to any specific waveform. :return: (int) repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions PGenError exception - error """ rep_mode = self._get_attr_int32(NIConst.NIHSDIO_ATTR_REPEAT_MODE) if rep_mode == NIConst.NIHSDIO_VAL_CONTINUOUS.value: rep_num = 0 elif rep_mode == NIConst.NIHSDIO_VAL_FINITE.value: rep_num = self._get_attr_int32(NIConst.NIHSDIO_ATTR_REPEAT_COUNT) else: msg_str = 'get_rep(): DLL call returned unknown repetition mode code {}'.format( rep_mode) self.log.error(msg_str=msg_str) raise PGenError(msg_str) return rep_num def set_rep(self, rep_num): """Set repeat mode and number of repetitions :param rep_num: (int) repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions :return: (int) actual repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions PGenError exception - error """ if rep_num == 0: rep_mode = NIConst.NIHSDIO_VAL_CONTINUOUS rep_num = NIConst.VI_NULL elif rep_num > 0: rep_mode = NIConst.NIHSDIO_VAL_FINITE rep_num = NITypes.ViInt32(rep_num) else: msg_str = 'set_rep() invalid argument {} \n' \ 'Valid values: 0 - infinite, positive integer - finite' \ ''.format(rep_num) self.log.error(msg_str=msg_str) raise PGenError(msg_str) self._er_chk( self.dll.niHSDIO_ConfigureGenerationRepeat( self._handle, # ViSession vi rep_mode, # ViInt32 repeatMode rep_num # ViInt32 repeatCount )) return self.get_rep() def get_wfm_to_gen(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_WAVEFORM_TO_GENERATE) def set_wfm_to_gen(self, wfm_name): self._er_chk( self.dll.niHSDIO_ConfigureWaveformToGenerate( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')) # ViConstString waveformName )) return self.get_wfm_to_gen() # ================================================================ # Script Generation # ================================================================ def write_script(self, script_str): # Sanity check: script_str is a string if not isinstance(script_str, str): msg_str = 'write_script(): passed argument is not a string' self.log.error(msg_str=msg_str) raise PGenError(msg_str) plain_script_str = script_str.replace('\n', ' ') # Convert into C-string c_script_str = NITypes.ViConstString(plain_script_str.encode('ascii')) op_status = self._er_chk( self.dll.niHSDIO_WriteScript( self._handle, # ViSession vi c_script_str # ViConstString script )) return op_status def get_scr_to_gen(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_SCRIPT_TO_GENERATE) def set_scr_to_gen(self, script_name): # Convert script_name into C-string script_name = NITypes.ViConstString(script_name.encode('ascii')) self._er_chk( self.dll.niHSDIO_ConfigureScriptToGenerate( self._handle, # ViSession vi script_name # ViConstString scriptName )) return self.get_scr_to_gen() # ================================================================ # Wrappers for C DLL helper functions # ================================================================ def _er_chk(self, error_code): # C:\Program Files\National Instruments\Shared\Errors\English\IVI-errors.txt if error_code == 0: # Success return 0 else: # Warning or Error # Create buffer for DLL function to output the error message string msg_buf = ctypes.create_string_buffer(256) # Call DLL function to generate readable error message self.dll.niHSDIO_error_message( self._handle, # ViSession vi NITypes.ViStatus(error_code), # ViStatus errorCode msg_buf # ViChar errorMessage[256] ) msg_str = msg_buf.value.decode('ascii') if error_code > 0: # Warning self.log.warn(msg_str=msg_str) return error_code else: # Error self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_int32(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (int) obtained value in the case of success PGenError exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf = NITypes.ViInt32() # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViInt32( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute ctypes.byref(buf) # ViInt32 *value )) return buf.value except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_int32(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_str(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (str) obtained value in the case of success Exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf_size = 1024 # Buffer size of 1024 was chosen for no specific reason. Increase if needed. buf = ctypes.create_string_buffer(buf_size) # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViString( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute NITypes.ViInt32(buf_size), # ViInt32 bufSize buf # ViChar value[] )) return buf.value.decode('ascii') except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_str(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_real64(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (float) obtained value in the case of success Exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf = NITypes.ViReal64() # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViReal64( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute ctypes.byref(buf) # ViReal64 *value )) return buf.value except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_real64(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str)
class Driver: """Driver for NI DAQmx card. Currently only implements setting AO voltage""" # TODO implement counter def __init__(self, device_name, logger=None, dummy=False): """Instantiate NI DAQ mx card :device_name: (str) Name of NI DAQ mx card, as displayed in the measurement and automation explorer """ # Device name self.dev = device_name # Log self.log = LogHandler(logger=logger) self.dummy = dummy # Try to get info of DAQ device to verify connection try: ni_daq_device = nidaqmx.system.device.Device(name=device_name) self.log.info( "Successfully connected to NI DAQ '{device_name}' (type: {product_type}) \n" "".format(device_name=ni_daq_device.name, product_type=ni_daq_device.product_type)) # If failed, provide info about connected DAQs except (nidaqmx.DaqError, OSError): # Log exception message # - get names of all connected NI DAQs try: ni_daqs_names = nidaqmx.system._collections.device_collection.\ DeviceCollection().device_names # - exception message self.log.error("NI DAQ card {} not found. \n" "There are {} NI DAQs available: \n " " {}" "".format(device_name, len(ni_daqs_names), ni_daqs_names)) except: self.log.error('No NI modules found') if self.dummy: self.log.info('Entering dummy mode instead') self.counters = {} @dummy_wrap def set_ao_voltage(self, ao_channel, voltages): """Set analog output of NI DAQ mx card to a series of voltages :ao_channel: (str) Name of output channel (e.g. 'ao1', 'ao2') :voltages: (list of int) list of voltages which will be output """ # TODO: Understand the timing between output voltages (sample-wise?) channel = self._gen_ch_path(ao_channel) with nidaqmx.Task() as task: task.ao_channels.add_ao_voltage_chan(channel) task.write(voltages, auto_start=True) def get_ai_voltage(self, ai_channel, num_samples=1, max_range=10.0): """Measures the analog input voltage of NI DAQ mx card :param ao_channel: (str) Name of output channel (e.g. 'ao1', 'ao2') :aram num_samplies: (int) Number of samples to take :param max_range: (float) Maximum range of voltage that will be measured """ channel = self._gen_ch_path(ai_channel) with nidaqmx.Task() as task: task.ai_channels.add_ai_voltage_chan(channel) task.ai_channels[0].ai_rng_high = max_range return task.read(number_of_samples_per_channel=num_samples) return -1 def get_di_state(self, port, di_channel): """Measures the state of a digital Input of a of NI DAQ mx card :param port: (str) port name ['port0'] :param channel: (str) channel name ['line1'] """ channel = self._gen_di_ch_path(port, di_channel) with nidaqmx.Task() as task: task.di_channels.add_di_chan( channel, line_grouping=nidaqmx.constants.LineGrouping.CHAN_PER_LINE) return task.read(number_of_samples_per_channel=1) return -1 def create_timed_counter(self, counter_channel, physical_channel, duration=0.1, name=None): """ Creates a software timed counter channel :param counter_channel: (str) channel of counter to use e.g. 'Dev1/ctr0' :param physical_channel: (str) physical channel of counter e.g. 'Dev1/PFI0' :param duration: (float) number of seconds for software-timed counting inverval :param name: (str) Name to use as a reference for counter in future calls :return: (str) name of the counter to use in future calls """ # Create a default name if necessary if name is None: name = f'counter_{len(self.counters)}' # Create counter task and assign parameters self.counters[name] = TimedCounter( logger=self.log, counter_channel=self._gen_ch_path(counter_channel), physical_channel='/' + self._gen_ch_path(physical_channel)) self.counters[name].set_parameters(duration) return name def start_timed_counter(self, name): """ Starts a timed counter :param name: (str) name of counter to start Should be return value of create_timed_counter() """ self.counters[name].start() def stop_timed_counter(self, name): """ Stops a timed counter :param name: (str) name of counter to stop Should be return value of create_timed_counter() """ self.counters[name].terminate_counting() def close_timed_counter(self, name): """ Closes a timed counter :param name: (str) name of counter to close """ self.counters[name].close() def get_count(self, name): """ Returns the count :param name: (str) name of the counter to use :return: (int) value of the count """ return int(self.counters[name].count) # Technical methods def _gen_ch_path(self, channel): """ Auxiliary method to build channel path string. :param channel: (str) channel name ['ao1'] :return: (str) full channel name ['Dev1/ao1'] """ return "{device_name}/{channel}".format(device_name=self.dev, channel=channel) def _gen_di_ch_path(self, port, di_channel): """ Auxiliary method to build channel path string for digital inputs. :param port: (str) port name ['port0'] :param channel: (str) channel name ['line0:1'] :return: (str) full channel name ['Dev1/port0/line0:1'] """ return f"{self.dev}/{port}/{di_channel}"