import pyvisa from time import sleep # import pkg_resources import os from typing import Callable from ..base import MeasurementDevice from ...utility.visa import enumerate_devices import logging log = logging.getLogger(__name__) import numpy as np class Model7260(MeasurementDevice): """ Wrapper class for the Model 7260 DSP Lock-In controlled via pyvisa """ def __init__(self, instr, check_front_switch=True): self.instr = instr init_script = """ ' INPUT ' voltage IMODE 0 ' set input A VMODE 1 ' set float FLOAT 1 ' set AC coupling CP 0 ' Enable 1x and 2x Line notch filters LF 4 ' Enable synchronous filter < 200Hz ' SYNC 1 ' REFERENCE ' Set external TTL rear panel IE 1 ' Set detection harmonic to 1' REFN 1 ' TODO time constant, filter slope, """ self.run(init_script) self._buffer_length = None def __del__(self): """Properly close the instrument connection""" self.instr.close() # RUN COMMANDS ON THE DEVICE def run(self, code, split=True): """ Run SCPI code on the device by writing it. Empty lines, leading whitespaces and lines starting with ' or # are ignored. Parameters ---------- code : str SCPI commands """ script = [] if split else '' for line in code.strip(" ").split("\n"): l = line.strip(" ") if len(l) == 0 or l[0] in "#'": continue if split: script.append(l) else: script += l + "\n" if split: for cmd in script: self.run_and_check(cmd) else: self.run_and_check(script) def run_and_check(self, cmd): """Run a command and check for errors afterwards by reading the standard event status byte""" try: log.debug(f"Running command(s): --BEGIN--\n{cmd}\n--END--") ret = self.instr.write(cmd) status = int(self.query("ST")) if status & (1 << 1): raise RuntimeError(f"Invalid command(s):\n{cmd}\n") elif status & (1 << 5): raise RuntimeError(f"Command parameter error after command(s):\n{cmd}\n") # if ret != 8: # raise RuntimeError(f"Error while writing command(s):\n'{script}'\n\nDevice returned code {ret}") except pyvisa.VisaIOError as e: raise RuntimeError(f"VisaIOError raised while writing command(s):\n'{cmd}'\n\nVisaIOError:\n{e}") def get_frequency(self) -> float: # TODo pass def query(self, query): return self.instr.query(query, delay=0.05).strip("\n") def snap(self, what="3,4,5,7"): """ Parameters ---------- what : TYPE, optional 1 X 2 Y 3 R 4 θ 5 Aux In 1 6 Aux In 2 7 Aux In 3 8 Aux In 4 9 Reference Frequency 10 CH1 display 11 CH2 display The default is "3,4,5,7". Returns ------- vals : list[float] Values converted to float """ vals = self.query(f"SNAP? {what}").split(",") vals = map(float, vals) return vals def measureTODO(): pass def read_value(self): """Read the value of R""" return float(self.query("MAG.")) def reset(self): # TODO pass def test_connection(self): pass def buffer_setup(self, X=0, Y=0, MAG=1, PHASE=1, SENS=0, ADC1=1, ADC2=2, length=None, interval_ms=5): """ Prepare the device for a buffer (curve) measurement :param X: 1 if the output should be stored, 0 else :param Y: 1 if the output should be stored, 0 else :param MAG: 1 if the output should be stored, 0 else :param PHASE: 1 if the output should be stored, 0 else :param SENS: 1 if the output should be stored, 0 else :param ADC1: 1 if the output should be stored, 0 else :param ADC2: 1 if the output should be stored, 0 else :param length: Number of values to store in the buffer, or None to use the maximum number of values :param interval_ms: Interval in ms with resolution of 5ms """ self.buffer_cbd = (X << 0) | (Y << 1) | (MAG << 2) | (PHASE << 3) | (SENS << 4) | (ADC1 << 5) | (ADC2 << 6) self.run(f"CBD {self.buffer_cbd}") n_outputs = self.buffer_cbd.bit_count() # length max_length = 32768 // n_outputs log.info(f"Setting up buffer, n_outputs={n_outputs}, max_length={max_length}, CBD={self.buffer_cbd:016b}") if length is None: self._buffer_length = max_length else: self._buffer_length = length if self._buffer_length > max_length: raise ValueError(f"For {n_outputs} outputs, the maximum length is {max_length} but {length} was given.") self.run(f"LEN {self._buffer_length}") # set the interval self.run(f"STR {interval_ms}") # init the buffer (new curve) self.run("NC") def buffer_start_fill(self): if self._buffer_length is None: raise RuntimeError(f"Buffer not set up, call buffer_setup() first.") self.run("TD") # take data def buffer_is_done(self) -> bool: vals = self.query("M").split(",") vals = list(map(int, vals)) log.info(f"buffer status: {vals[0]}, {vals[1]}, {vals[2]:08b}, {vals[3]}") return vals[0] == 0 def buffer_get_data(self, raw=False): """ Get the data from the buffer. :return: """ if self._buffer_length is None: raise RuntimeError(f"Buffer not set up, call buffer_setup() first.") n_points = int(self.query("M").split(",")[3]) data = np.empty((n_points, self.buffer_cbd.bit_count()), dtype=int) self.instr.write(f"DCT {self.buffer_cbd}") # this is a query, we need to read the next lines now for i in range(n_points): vals = self.instr.read().strip("\n").split(",") data[i,:] = [int(x) for x in vals] # check if all values have been transmitted by issuing another command length = self.query("LEN") try: length = int(length) except ValueError: raise RuntimeError(f"Error buffer data transmission: Buffer length {length} is not an integer.") if length != self._buffer_length: raise RuntimeError(f"Error buffer data transmission: Buffer length reported as {length}, but should be {self._buffer_length}.") if raw: return data return self._buffer_convert_from_full_scale(data) def _buffer_convert_from_full_scale(self, data): """ Convert values from fullscale to Volts and Degrees See section 6.4.09 on page 141 in the manual :param data: full scale data :return: data converted to Volt and Degrees """ # get full voltage scale # if the sensitivity would have been stored (bit 4), its value could be used too fs = float(self.query("SEN.")) scales = [ 1E-4*fs, # X 1E-4*fs, # Y 1E-4*fs, # MAG 1E-2, # Phase FS=18000 = 180° 1, # Sens 1E-3, # ADC1 FS=10000 = 10 V 1E-3, # ADC2 FS=10000 = 10 V 1E-3, # ADC3 FS=10000 = 10 V ] data_conv = np.empty(data.shape, dtype=float) i_bit = 0 for i_val in range(self.buffer_cbd.bit_count()): while i_bit < len(scales) and self.buffer_cbd & (1 << i_bit) == 0: i_bit += 1 if i_bit == len(scales): raise RuntimeError(f"Could not find the output of value number {i_val+1}") # i_valth value is the quantity belonging to bit i_bit data_conv[:,i_val] = data[:,i_val] * scales[i_bit] return data_conv def auto_gain(self): self.instr.write("AGAN") # wait until the device responds again, meaning it is no longer busy self.instr.write("SENS?") i = 0 while i < 50: try: gain = self.instr.read().strip("\n") return gain except: i += 1 raise RuntimeError("Could not get response from device after sending auto gain command") @staticmethod def enumerate_devices(query="(GPIB)?*:INSTR"): return enumerate_devices("7260", query, cmd="ID") @staticmethod def connect_device(name): rm = pyvisa.ResourceManager() instr = rm.open_resource(name) return Model7260(instr) def __str__(self): return "SR830"