From 0c6af7c7dffc2214fb5c5e2fce115c0ff3f51df5 Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Tue, 17 Nov 2015 18:43:03 -0600 Subject: [PATCH 01/61] Lantz Setup guide Starting guide for new Lantz users --- LantzSetup.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 LantzSetup.md diff --git a/LantzSetup.md b/LantzSetup.md new file mode 100644 index 0000000..b23b32f --- /dev/null +++ b/LantzSetup.md @@ -0,0 +1,5 @@ +# A Quick Primer on Setting Up Lantz # +Version: 0.0.1 +Author: Peter Mintun (pmintun@uchicago.edu) +Date: 11/17/2015 + From 563726cd000c4ed7cc12762523d7b05e0a01754b Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Tue, 17 Nov 2015 22:17:15 -0600 Subject: [PATCH 02/61] Added Lakeshore 332 and Signal Recovery 7265 Adding files for Lakeshore 332 and Signal Recovery 7265 drivers. These drivers are going to be heavily reworked as I actually learn how to properly use lantz. Please use with caution! --- Lakeshore332.py | 81 ++++++ SignalRecovery7265.py | 655 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 736 insertions(+) create mode 100644 Lakeshore332.py create mode 100644 SignalRecovery7265.py diff --git a/Lakeshore332.py b/Lakeshore332.py new file mode 100644 index 0000000..3f2a354 --- /dev/null +++ b/Lakeshore332.py @@ -0,0 +1,81 @@ +# Lakeshore 332 Temperature Controller Driver +# Peter Mintun + +# This file is a driver for the Lakeshore 332 series temperature controller. + +# Some sort of license information goes here. + +from lantz import Feat +from lantz.messagebased import MessageBasedDriver + +class Lakeshore332(MessageBasedDriver): + """ + Lakeshore 332 Temperature controlller. + + This class implements a set of basic controls for the Lakeshore 332 series + temperature controller. + + Full documentation of the device is available at: + http://www.lakeshore.com/ObsoleteAndResearchDocs/332_Manual.pdf + """ + + def __init__(self, name, address, reset=False): + MessageBasedDriver.__init__(self,name) + self._address = address + + + + + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n'}} + + + + + + + + @Feat() + def idn(self): + """ + Returns the instrument identification + """ + return self.query('*IDN?') + + @Feat() + def get_kelvin(self, channel): + """ + Returns Kelvin reading from specified channel + """ + return self.query('KRDG? {}'.format(channel)) + + @Feat() + def heater_range(self): + """ + Queries the instrument and prints the current heater range setting. + """ + ans = self.query('RANGE?') + if ans == '0': + print('Heater range: off') + elif ans == '1': + print('Heater range: low') + elif ans == '2': + print('Heater range: medium') + elif ans == '3': + print('Heater range: high') + else: + print('Error: Heater not responding correctly') + + @heater_range.setter + def heater_range(self, heater_setting): + """ + + """ + + + + + +if __name__ == '__main__': + with Lakeshore332('GPIB0::GPIB::16') as inst: + print('The identification of this instrument is : ' + inst.idn) diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py new file mode 100644 index 0000000..6218cd5 --- /dev/null +++ b/SignalRecovery7265.py @@ -0,0 +1,655 @@ +from lantz import Feat, DictFeat +from lantz.messagebased import MessageBasedDriver + +class SignalRecovery7265(MessageBasedDriver): + """Signal Recovery 7265 + DSP Lock-in Amplifier + + Author: P. Mintun + Version: 0.0.1 + Date: 11/13/2015 + + "Stay safe and happy locking in..." + + This driver assumes that the COMM settings of the 7265 are set up as follows: + + ADDRESS = N + TERMINATOR = [CR],[LF] + TECH ECHO = DISABLED + + SDC=ADF 1 ENABLED + """ + + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n'}} + + + + # Instrument Identification + @Feat() + def idn(self): + """Identification, should output 7265. + """ + return self.query('ID') + + @Feat() + def firmware_rev(self): + print(self.query('REV')) + return 0 + #return self.query('REV') + + @Feat() + def firmware_ver(self): + return self.query('VER') + + + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + answer = super().query(command, send_args=send_args, recv_args=recv_args) + if answer == 'ERROR': + raise InstrumentError + return answer + + + + # 6.4.05 Instrument outputs + @Feat() + def x(self): + """ + Returns x demodulator output in volts or amps, depending on mode + """ + return self.query('X.') + + @Feat() + def y(self): + """ + Returns y demodulator output in volts or amps, depending on mode + """ + return self.query('Y.') + + @Feat() + def xy(self): + """ + Returns x and y demodulator output in volts or amps, depending on mode + """ + return self.query('XY.') + + @Feat() + def magnitude(self): + """ + Returns signal magnitude value in volts or amps, depending on mode + """ + return self.query('MAG.') + + @Feat() + def phase(self): + """ + Returns signal phase in degrees. + """ + return self.query('PHA.') + + @Feat() + def mag_phase(self): + """ + Returns signal magnitude (volts or amps) and phase (degrees). + """ + return self.query('MP.') + + @Feat() + def sqrt_noise_spectral_density(self): + """ + Returns square root of noise spectral density measured at y channel + output, in V/sqrt(Hz) or A/sqrt(Hz) depending on mode. + + This assumes that the y channel output is Gaussian w/ zero mean. + """ + return self.query('NHZ.') + + @Feat() + def equiv_noise_bandwidth(self): + """ + Returns the equivalent noise bandwidth of the output low pass filter at + current time constant setting (in Hz) + """ + return self.query('ENBW.') + + + ## 6.4.05 Unimplemented commands: + # X - fixed point X + # Y - fixed point Y + # XY - fixed point XY + # MAG - fixed point magnitude + # PHA - fixed point phase + # MP - fixed point magnitude and phase + # RT[.] - ratio output + # LR[.] - log ratio output + # ENBW - fixed point equivalent noise bandwidth + # NN - fixed point noise output + + ## Unimplemented user equation functionality + # DEFEQU - define user equation + # C1 - user equation C1 + # C2 - user equation C2 + # EQU n - output of equation 1/2 + + ## Unimplemented fast setup commands + # STAR - fast setup command + + # 6.4.01 Signal channel information + # Should these be implemented as DictFeat? + @Feat(limits=(0,2,1)) + def imode(self): + """ + Query current/voltage mode + Return values correspond to the following table. + + n Input mode + 0 Current mode off - voltage mode input ENABLED + 1 High bandwidth (HB) current mode enabled + 2 Low noise (LN) current mode enabled + """ + return self.query('IMODE') + + @imode.setter + def imode(self, value): + """ + Current/voltage mode input selector + + n Input mode + 0 Current mode off - voltage mode input ENABLED + 1 High bandwidth (HB) current mode enabled + 2 Low noise (LN) current mode enabled + """ + return self.query('IMODE{}'.format(value)) + + @Feat(limits=(0,3,1)) + def vmode(self): + """ + Query voltage input configuration. + + Value returned corresponds to the following table: + n Input configuration + 0: Both inputs grounded (test mode) + 1: A input only + 2: -B input only + 3: A-B differential mode + """ + return self.query('VMODE') + + @vmode.setter + def vmode(self, value): + """ + Set voltage input configuration according to the following table: + + n Input configuration + 0: Both inputs grounded (test mode) + 1: A input only + 2: -B input only + 3: A-B differential mode + """ + return self.query('VMODE{}'.format(value)) + + @Feat(limits=(0,1,1)) + def mode_FET(self): + """ + Returns voltage mode input device control, with values corresponding + to the following table: + + 0: Bipolar device, 10 kΩ input impedance, 2 nV/√Hz voltage noise at 1 kHz + 1: FET, 10 MΩ input impedance, 5 nV/√Hz voltage noise at 1 kHz + + """ + return self.query('FET') + + @mode_FET.setter + def mode_FET(self, value): + """ + Sets voltage mode input device control according to following table: + + 0: Bipolar device, 10 kΩ input impedance, 2 nV/√Hz voltage noise at 1 kHz + 1: FET, 10 MΩ input impedance, 5 nV/√Hz voltage noise at 1 kHz + """ + return self.query('FET{}'.format(value)) + + @Feat(limits=(0,1,1)) + def mode_float(self): + """ + Queries connector shield switch setting, with returned value corresponding + to the following table: + + n: Selection + 0: Ground + 1: Float (connected to ground via a 1 kΩ resistor) + """ + return self.query('FLOAT') + + @mode_float.setter + def mode_float(self,value): + """ + Sets connector shield switch setting, with values corresponding to the + following table: + + n: Selection + 0: Ground + 1: Float (connected to ground via a 1 kΩ resistor) + """ + return self.query('FLOAT{}'.format(value)) + + @Feat(limits=(0,1,1)) + def mode_CP(self): + """ + Reads out input coupling mode, with return value corresponding to the + following table: + + n: Coupling Mode + 0: AC + 1: DC + """ + return self.query('CP') + + @mode_CP.setter + def mode_CP(self, value): + """ + Sets input coupling mode according to the following table: + + n: Coupling Mode + 0: AC + 1: DC + """ + return self.query('CP{}'.format(value)) + + @Feat() + def sensitivity(self): + """ + Reads the sensitivity in floating point mode. Return value units are + either volts or amps depending on the IMODE selected. + """ + return self.query('SEN.') + + @Feat(limits=(0,27,1)) + def sensitivity_n(self): + """ + Reads the sensitivty according to the following lookup table. + Note that the physical values change depending on the value of IMODE. + + n full-scale sensitivity + IMODE=0 IMODE=1 IMODE=2 + 1: 2 nV 2 fA n/a + 2: 5 nV 5 fA n/a + 3: 10 nV 10 fA n/a + 4: 20 nV 20 fA n/a + 5: 50 nV 50 fA n/a + 6: 100 nV 100 fA n/a + 7: 200 nV 200 fA 2 fA + 8: 500 nV 500 fA 5 fA + 9: 1 µV 1 pA 10 fA + 10: 2 µV 2 pA 20 fA + 11: 5 µV 5 pA 50 fA + 12: 10 µV 10 pA 100 fA + 13: 20 µV 20 pA 200 fA + 14: 50 µV 50 pA 500 fA + 15: 100 µV 100 pA 1 pA + 16: 200 µV 200 pA 2 pA + 17: 500 µV 500 pA 5 pA + 18: 1 mV 1 nA 10 pA + 19: 2 mV 2 nA 20 pA + 20: 5 mV 5 nA 50 pA + 21: 10 mV 10 nA 100 pA + 22: 20 mV 20 nA 200 pA + 23: 50 mV 50 nA 500 pA + 24: 100 mV 100 nA 1 nA + 25: 200 mV 200 nA 2 nA + 26: 500 mV 500 nA 5 nA + 27: 1 V 1 µA 10 nA + + """ + return self.query('SEN') + + @sensitivity_n.setter + def sensitivity_n(self, value): + """ + Set the sensitivty according to the following lookup table. + Note that the sensitivity changes depending on the value of IMODE. + + n full-scale sensitivity + IMODE=0 IMODE=1 IMODE=2 + 1: 2 nV 2 fA n/a + 2: 5 nV 5 fA n/a + 3: 10 nV 10 fA n/a + 4: 20 nV 20 fA n/a + 5: 50 nV 50 fA n/a + 6: 100 nV 100 fA n/a + 7: 200 nV 200 fA 2 fA + 8: 500 nV 500 fA 5 fA + 9: 1 µV 1 pA 10 fA + 10: 2 µV 2 pA 20 fA + 11: 5 µV 5 pA 50 fA + 12: 10 µV 10 pA 100 fA + 13: 20 µV 20 pA 200 fA + 14: 50 µV 50 pA 500 fA + 15: 100 µV 100 pA 1 pA + 16: 200 µV 200 pA 2 pA + 17: 500 µV 500 pA 5 pA + 18: 1 mV 1 nA 10 pA + 19: 2 mV 2 nA 20 pA + 20: 5 mV 5 nA 50 pA + 21: 10 mV 10 nA 100 pA + 22: 20 mV 20 nA 200 pA + 23: 50 mV 50 nA 500 pA + 24: 100 mV 100 nA 1 nA + 25: 200 mV 200 nA 2 nA + 26: 500 mV 500 nA 5 nA + 27: 1 V 1 µA 10 nA + + """ + self.write('TC{}'.format(value)) + + + + @Feat() + def auto_sensitivity(self): + """ + The instrument adjusts its full-scale sensitivity so that the magnitude + output lies between 30% and 90% of full-scale. + """ + return self.query('AS') + + @Feat() + def auto_measure(self): + """ + The instrument adjusts its full-scale sensitivity so that the magnitude + output lies between 30% and 90% of full-scale, and then performs an + auto-phase operation to maximize the X channel output and minimize the + Y channel output. + """ + return self.query('ASM') + + @Feat(limits=(0,9,1)) + def ac_gain(self): + """ + Reads out the gain of the signal channel amplifier, in range 0 to 9, + corresponding to 0dB - 90dB in 10dB steps. + """ + return self.query('ACGAIN') + + @ac_gain.setter + def ac_gain(self,value): + """ + Sets AC gain of signal channel amplifier in range 0 to 9, corresponding + to 0dB - 90dB in 10dB steps. + """ + return self.query('ACGAIN{}'.format(value)) + + @Feat(limits=(0,1,1)) + def ac_gain_auto(self): + """ + Returns if AC Gain is manually or automatically controlled. + + Values correspond to the following table: + + n: Status + 0: AC Gain is under manual control + 1: AC Gain is under automatic control + """ + return self.query('AUTOMATIC') + + @ac_gain_auto.setter + def ac_gain_auto(self, value): + """ + Sets AC gain to manual or automatic based on the following table: + + n: Status + 0: AC Gain is under manual control + 1: AC Gain is under automatic control + """ + return self.query('AUTOMATIC{}'.format(value)) + + # Unimplemented features + # LF[n1 n2] - signal channel line rejection filter control + # SAMPLE[n] - main ADC sample rate control + # RANGE[n] - signal recovery/vector voltmeter mode selector + + # 6.4.02 Reference Channel + + @Feat(limits=(0,2,1)) + def ref_mode(self): + """ + Returns the instrument reference mode, with return values corresponding + to the following table: + + n: Mode + 0: Single Reference / Virtual Reference mode + 1: Dual Harmonic mode + 2: Dual Reference mode + """ + return self.query('REFMODE') + + @ref_mode.setter + def ref_mode(self,value): + """ + Sets the instrument reference mode, according to the following table: + + n: Mode + 0: Single Reference / Virtual Reference mode + 1: Dual Harmonic mode + 2: Dual Reference mode + """ + return self.query('REFMODE{}'.format(value)) + + + @Feat(limits=(0,2,1)) + def ie(self): + """ + Reads out reference channel source according to the following table: + + n: Selection + 0: INT (internal) + 1: EXT LOGIC (external rear panel TTL input) + 2: EXT (external front panel analog input) + """ + return self.query('IE') + + @ie.setter + def ie(self,value): + """ + Selects reference channel source according to the following table: + + n: Selection + 0: INT (internal) + 1: EXT LOGIC (external rear panel TTL input) + 2: EXT (external front panel analog input) + """ + return self.query('IE{}'.format(value)) + + @Feat(limits=(0,65535,1)) + def refn(self): + """ + Returns n,measurement harmonic mode + + """ + return self.query('REFN') + + + def refn(self, value): + """ + + + """ + return self.query('REFN{}'.format(value)) + + @Feat(limits=(-360,360)) + def refp(self): + """ + Sets + """ + + + + + + + + # 6.4.03 Signal Channel Output Filters + @Feat(units='sec') + def time_constant(self): + return self.query('TC.') + + + @Feat(limits=(0,29,1)) + def time_constant_n(self): + return self.query('TC') + + @time_constant_n.setter + def time_constant_n(self, value): + """ + Sets lock-in time constant according to the following lookup table: + + 0: 10 µs + 1: 20 µs + 2: 40 µs + 3: 80 µs + 4: 160 µs + 5: 320 µs + 6: 640 µs + 7: 5 ms + 8: 10 ms + 9: 20 ms + 10: 50 ms + 11: 100 ms + 12: 200 ms + 13: 500 ms + 14: 1 s + 15: 2 s + 16: 5 s + 17: 10 s + 18: 20 s + 19: 50 s + 20: 100 s + 21: 200 s + 22: 500 s + 23: 1 ks + 24: 2 ks + 25: 5 ks + 26: 10 ks + 27: 20 ks + 28: 50 ks + 29: 100 ks + + """ + self.write('TC{}'.format(value)) + + # Unimplemented commands + # SYNC [n] - synchronous time constant control + # SLOPE [n] - output low-pass filter slope control + + + # 6.4.04 Signal Channel Output Amplifiers + # TODO implement this + + # 6.4.06 Internal Oscillator + # TODO implement this + + # 6.4.07 Auxiliary Outputs + # Not implemeting this section yet + # Missing commands list: + # DAC[.]n1[n2] Auxilliary DAC output controls + # BYTE [n] Digital output port controls + + # 6.4.08 Auxiliary Inputs + # Not implementing this section yet + # Missing commands list + # ADC[.]n - read auxiliary analog-to-digital Inputs + # ADC3TIME[n] - ADC3 sample time + # TADC[n] - Auxiliary ADC trigger mode control + # BURSTTPP [n] - set burst mode time per point rate for ADC1 and ADC2 + + # Output Data Curve Buffer + # Not implementing this section yet + # Missing commands list: + # CBD[n] - curve buffer define + # LEN[n] - curve length control + # NC - new curve + # STR[n] - storage interval control + # TD - take data + # TDT - take data triggered + # TDC - take data continually + # EVENT[n] - event marker control + # HC - halt curve acquisition + # M - curve acquisition status monitor + # DC[.]n - dump acquired curves to computer + # DCB n - dump acquired curves to computer in binary format + # DCT n - dump acquired curves to computer in table format + + ## 6.4.10 Computer interfaces (RS232 and GPIB) + # Not implementing yet + # Missing functions + # RS[n1[n2]] - set/read RS232 parameters + # GP[n1[n2]] - set/read GPIB parameters + # n1 sets GPIB address in the range 0 to 31 + # n2 sets GPIB terminator and test echo function + #\N n - Address command for daisy chaining + # DD[n] - define delimiter control + # ST - report status byte + # N - report overload byte + # MSK - set/read service request mask byte + # REMOTE[n] - remote only (front panel lock-out) control + + # 6.4.12 Front Panel + # Not implementing this yet + # Could be used to auto set up user interface for local operation + + # 6.4.13 Auto Default + # Not implementing this yet + # ADF[n] Can be used to reset to factor default settings + + # 6.4.14 Dual Mode Commands + # Used for either dual reference or dual harmonic modes + # Check manual to see which channels can be controlled independently + +if __name__ == '__main__': + + from lantz import Q_ + + + #Define units + volt = Q_(1, 'V') + sec = Q_(1, 'sec') + + # Define GPIB location + GPIB_host = 'GPIB0' + GPIB_address = '10' + + LIA_name = GPIB_host + '::' + GPIB_address + '::INSTR' + + with SignalRecovery7265(LIA_name) as inst: + print('=== Instrument Identification ===') + print('idn:{}' .format(inst.idn)) + #print('Firmware revision:{}'.format(inst.firmware_rev)) + print('Firmware version: {}'.format(inst.firmware_ver)) + + + print('=== Time Constant Settings ===') + print('Time constant: {}'.format(inst.time_constant)) + inst.time_constant_n = 11 + print('Time constant [n]: {}'.format(inst.time_constant_n)) + inst.time_constant_n = 12 + + + + + print('=== Oscillator Settings ===') + print('Reference in/Ext:') + print('') + + + + + print('=== Instrument Measurements ===') + print('X: {}'.format(inst.x)) + print('Y: {}'.format(inst.y)) + print('X,Y: {}'.format(inst.xy)) + print('Magnitude: {}'.format(inst.magnitude)) + print('Phase: {}'.format(inst.phase)) + print('Magnitude, Phase: {}'.format(inst.mag_phase)) + print('Sqrt(Noise Spectral Density): {}'.format(inst.sqrt_noise_spectral_density)) + print('Equivalent Noise Bandwidth: {}'.format(inst.equiv_noise_bandwidth)) From 4c037318dcd155cd8b368d4f2749e4e3a079b821 Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Wed, 18 Nov 2015 00:47:03 -0600 Subject: [PATCH 03/61] Progress on Lakeshore 332 driver based on QTLab original Making progress porting Lakeshore 332 driver from QTLab over to Lantz...at least I think --- Lakeshore332.py | 189 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 22 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index 3f2a354..8463eb5 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -5,7 +5,7 @@ # Some sort of license information goes here. -from lantz import Feat +from lantz import Feat, Action from lantz.messagebased import MessageBasedDriver class Lakeshore332(MessageBasedDriver): @@ -22,60 +22,205 @@ class Lakeshore332(MessageBasedDriver): def __init__(self, name, address, reset=False): MessageBasedDriver.__init__(self,name) self._address = address + self._verbose = True + self.channel_list = ['A','B'] + self.heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} + self.heater_status_vals = {'no error':0, 'open load':1, 'short':2} + self.controller_modes = {'local': 0, 'remote':1, 'remote, local lockout': 2} - + if reset: + self.reset() + else: + self.get_all() DEFAULTS = {'COMMON': {'write_termination': '\n', 'read_termination': '\n'}} + @Action() + def reset(self): + """ + Resets the Lakeshore 332 temperature controller. + """ + self.write('*RST') - - - + def get_all(self): + """ + Gets the instrument identification and mode. + """ + print(self.idn()) + print(self.mode()) + return None @Feat() - def idn(self): + def get_idn(self): """ - Returns the instrument identification + Returns the instrument identification. """ return self.query('*IDN?') @Feat() def get_kelvin(self, channel): """ - Returns Kelvin reading from specified channel + Returns temperature reading from specified channel in Kelvin. """ - return self.query('KRDG? {}'.format(channel)) + if channel in channel_list: + msg_str = 'KRDG?' + channel + return self.query(msg_str.format(channel)) + else: + print('Error: invalid channel') + return None @Feat() - def heater_range(self): + def get_sensor(self, channel): """ - Queries the instrument and prints the current heater range setting. + Returns sensor reading from specified channel. """ - ans = self.query('RANGE?') - if ans == '0': - print('Heater range: off') - elif ans == '1': - print('Heater range: low') - elif ans == '2': - print('Heater range: medium') - elif ans == '3': - print('Heater range: high') + if channel in channel_list: + msg_str = 'SRDG?' + channel + return self.query(msg_str.format(channel)) else: - print('Error: Heater not responding correctly') + print('Error: invalid channel') + return None + + @Feat(values=self.heater_status_vals) + def get_heater_status(self): + """ + Returns the header status. + """ + ans = self.query('HTRST?') + if self._verbose: + if ans == '0': + print('Heater status: no error) + elif ans == '1': + print('Heater status: open load') + elif ans == '2': + print('Heater status: short') + else: + print('Error: Heater status invalid') + return ans + + @Feat(values=self.heater_range_vals) + def get_heater_range(self): + """ + Queries the instrument, prints a message describing the current heater + range setting, then returns the heater range value. + """ + ans = self.query('RANGE?') + if self._verbose: + if ans == '0': + print('Heater range: off') + elif ans == '1': + print('Heater range: low') + elif ans == '2': + print('Heater range: medium') + elif ans == '3': + print('Heater range: high') + else: + print('Error: Heater not responding correctly') + return ans @heater_range.setter def heater_range(self, heater_setting): """ + Sets heater range to heater_setting. + heater_setting must be an integer between 0 and 3 inclusive. + """ + return self.write('RANGE {}'.format(heater_setting)) + + + @Feat(values=controller_modes) + def get_mode(self): + """ + Reads the mode setting of the controller. """ + return self.query('MODE?') + + @get_mode.setter + def get_mode(self, mode): + """ + Sets controller mode, valid mode inputs are: + local + remote + remote, local lockout + """ + return self.query('MODE {}'.format(value)) + + def local(self): + self.set_mode('local') + + def remote(self): + self.set_mode('remote') + + @Feat() + def pid(self, channel): + ans = self.query('PID? {}'.format(channel)) + fields = ans.split(',') + if len(fields) != 3: + return None + fields = [float(f) for f in fields] + return fields + + @get_pid.setter + def pid(self, val, channel): + """ + Need to actually implement this! + """ + print('This feature is actually unimplemented! Sorry!') + + @Feat() + def setpoint(self, channel): + """ + Return the feedback controller's setpoint. + """ + if channel in self.channel_list: + return self.query('SETP?{}'.format(channel)) + else: + return None + + @setpoint.setter + def setpoint(self, channel, value): + """ + Sets the setpoint of channel channel to value value + """ + #TODO: actually implement this! + print('Not yet implemented!') + + @Feat() + def mout(self): + """ + """ + #TODO: actually implement this! + print('Not yet implemented!') + + @mout.setter + def mout(self, value): + """ + """ + #TODO: actually implement this! + print('Not yet implemented!') + + @Feat() + def cmd_mode(self): + """ + """ + #TODO: actually implement this! + print('Not yet implemented!') + + @cmd_mode.setter + def cmd_mode(self, value): + """ + """ + #TODO: implement this! + print('Not yet implemented!') + if __name__ == '__main__': - with Lakeshore332('GPIB0::GPIB::16') as inst: + with Lakeshore332('GPIB::GPIB0::16::INSTR') as inst: print('The identification of this instrument is : ' + inst.idn) From 0bfd4647e4409d759b37c4ad189e7bd19d505bc0 Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Wed, 18 Nov 2015 10:57:07 -0600 Subject: [PATCH 04/61] Explicit instructions on installing lantz using miniconda3 Wrote this for the Python newbs out there, this should give you a minimal lantz installation using Miniconda3. Needs to include information about what National Instruments software needs to be specifically installed (this is probably the useful thing to include here!) - will write that up when I set up a data computer from scratch --- LantzSetup.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/LantzSetup.md b/LantzSetup.md index b23b32f..b56ca7f 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -1,5 +1,41 @@ # A Quick Primer on Setting Up Lantz # -Version: 0.0.1 +Version: 1.0 + Author: Peter Mintun (pmintun@uchicago.edu) + Date: 11/17/2015 +I wrote this as a test file for GitHub before writing any real drivers. This is essentially a more explicit version of the documentation in Section 1.6: http://lantz.readthedocs.org/en/latest/tutorial/installing.html, it should be helpful for any Python novices hoping to get involved with writing drivers for the Lantz system. At some point, this will be updated to include more details about what National Instruments drivers need to be installed at a bare minimum (pending setting up a data computer from scratch). + +Prerequisites: Don't have Windows 10! (at least not yet...) + +# 1. Install Python 3 using Miniconda # +Install Python 3 using Miniconda. This will create a secondary Python distribution that is separate from other versions of Python that you might already have. + +Download Python 3.5 from this link: http://conda.pydata.org/miniconda.html + +Install Miniconda3 by following the instructions in the wizard. Make sure to note the directory that you install Miniconda3 to, you will need it for the next step. + +# 2. Install some lantz dependencies using conda # +Open the Windows command prompt. + +From the command line, cd to the directory that you installed Miniconda3 to in the previous step. Then enter the Scripts folder. If you list the files, there should be a file conda.exe. + +Now install the packages for lantz using the command: + + > conda install pip numpy sphinx pyqt + + +This command will install a series of packages: pip (used for installation), numpy (used for numerical analysis), sphinx (a documentation tool), and pyqt (Python bindings for the Qt application framework). In addition, conda figures out the missing dependencies for these packages and install/configure them automatically. + +After this is installed, you should have all the pieces necessary to run the basic functionality of lantz. + +# 3. Install other packages using pip # +From the command line, run the command: + + > pip install colorama pyserial pyusb lantz + +This command installs the colorama (used for producing colorful terminal output), pyserial (interfacing with serial devices), pyusb(interfacing with usb devices), and lantz (what you're supposedly hoping to install) packages to your Miniconda3 installation. + +# 4. Install National Instruments Drivers # +TODO: write this section! From c3752f875ee1b316619d753fea1ed7f0f5090afa Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Wed, 18 Nov 2015 11:03:23 -0600 Subject: [PATCH 05/61] added testing section Added basic code to test if lantz is installed --- LantzSetup.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/LantzSetup.md b/LantzSetup.md index b56ca7f..52d5673 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -31,11 +31,23 @@ This command will install a series of packages: pip (used for installation), num After this is installed, you should have all the pieces necessary to run the basic functionality of lantz. # 3. Install other packages using pip # + From the command line, run the command: - > pip install colorama pyserial pyusb lantz + > pip install colorama pyserial pyusb lantz This command installs the colorama (used for producing colorful terminal output), pyserial (interfacing with serial devices), pyusb(interfacing with usb devices), and lantz (what you're supposedly hoping to install) packages to your Miniconda3 installation. # 4. Install National Instruments Drivers # TODO: write this section! + +# 5 . Test your installation # +From the command prompt, move up a directory into your main Miniconda3 installation folder, then run `python.exe` + +This should give you a Python 3.x command prompt! + +Now run the command: + + >>> import lantz + +This should import the lantz module. If this runs successfully, then you probably have installed lantz correctly. From 0d6a05dea6bb9dde236ef2aa28d6090c8ccc38ac Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Fri, 20 Nov 2015 11:16:33 -0600 Subject: [PATCH 06/61] added fix for qt installation failure cpanderson tip for how to fix qt installation not working --- LantzSetup.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/LantzSetup.md b/LantzSetup.md index 52d5673..42c5876 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -9,14 +9,14 @@ I wrote this as a test file for GitHub before writing any real drivers. This is Prerequisites: Don't have Windows 10! (at least not yet...) -# 1. Install Python 3 using Miniconda # +## 1. Install Python 3 using Miniconda ## Install Python 3 using Miniconda. This will create a secondary Python distribution that is separate from other versions of Python that you might already have. Download Python 3.5 from this link: http://conda.pydata.org/miniconda.html Install Miniconda3 by following the instructions in the wizard. Make sure to note the directory that you install Miniconda3 to, you will need it for the next step. -# 2. Install some lantz dependencies using conda # +## 2. Install some lantz dependencies using conda ## Open the Windows command prompt. From the command line, cd to the directory that you installed Miniconda3 to in the previous step. Then enter the Scripts folder. If you list the files, there should be a file conda.exe. @@ -30,7 +30,7 @@ This command will install a series of packages: pip (used for installation), num After this is installed, you should have all the pieces necessary to run the basic functionality of lantz. -# 3. Install other packages using pip # +## 3. Install other packages using pip ## From the command line, run the command: @@ -38,10 +38,10 @@ From the command line, run the command: This command installs the colorama (used for producing colorful terminal output), pyserial (interfacing with serial devices), pyusb(interfacing with usb devices), and lantz (what you're supposedly hoping to install) packages to your Miniconda3 installation. -# 4. Install National Instruments Drivers # +## 4. Install National Instruments Drivers ## TODO: write this section! -# 5 . Test your installation # +## 5 . Test your installation ## From the command prompt, move up a directory into your main Miniconda3 installation folder, then run `python.exe` This should give you a Python 3.x command prompt! From 22bdd431c049cfc598fe3599733c5e5111c0333a Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Sat, 21 Nov 2015 00:09:31 -0600 Subject: [PATCH 07/61] not sure did a lot of stuff to try and figure out what's going on. but the basic lantz query function they suggest in the driver tutorial makes it timeout and not communicate with the instrument. still want to improve the design to follow proper coding practice better but not playing nice with whatever the @Feats() and __init__ functions do. basically confused by lanz --- Lakeshore332.py | 143 ++++++++++++++++++++------------ qtlab_Lakeshore332.py | 189 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 55 deletions(-) create mode 100644 qtlab_Lakeshore332.py diff --git a/Lakeshore332.py b/Lakeshore332.py index 8463eb5..e0c80d2 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -12,65 +12,93 @@ class Lakeshore332(MessageBasedDriver): """ Lakeshore 332 Temperature controlller. - This class implements a set of basic controls for the Lakeshore 332 series - temperature controller. + This class, based off of the Lantz MessageBasedDriver class, implements a + set of basic controls for the Lakeshore 332 series temperature controller. + It essentially a port of a nice driver written for QtLab by Reinier Heeres. Full documentation of the device is available at: http://www.lakeshore.com/ObsoleteAndResearchDocs/332_Manual.pdf """ - def __init__(self, name, address, reset=False): - MessageBasedDriver.__init__(self,name) - self._address = address - self._verbose = True - - self.channel_list = ['A','B'] - self.heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} - self.heater_status_vals = {'no error':0, 'open load':1, 'short':2} - self.controller_modes = {'local': 0, 'remote':1, 'remote, local lockout': 2} - - if reset: - self.reset() - else: - self.get_all() - - DEFAULTS = {'COMMON': {'write_termination': '\n', - 'read_termination': '\n'}} - + # These defaults assume that you have set the IEEE Term setting to: Lf Cr + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n'}} + + channel_list = ['A','B'] + heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} + heater_status_vals = {'no error':0, 'open load':1, 'short':2} + controller_modes = {'local': 0, 'remote':1, 'remote, local lockout': 2} + + # def __init__(self, GPIB_name, GPIB_addr, reset=False): + # res_name = GPIB_name + '::' + GPIB_addr + '::INSTR' + # super().__init__(res_name, name='Lakeshore332') + # self._address = res_name + _verbose = True + # self._idn = None + # + # print(res_name) + # + # print(self.channel_list) + # print(self.heater_range_vals) + # print(self.heater_status_vals) + # print(self.controller_modes) + + + + # def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + # answer = super().query(command, send_args=send_args, recv_args=recv_args) + # if answer == 'ERROR': + # raise InstrumentError + # return answer + + #def initialize(self, reset=False): + # """ + # Initialization code for Lakeshore 332 should go in this function, since + # if you define functions as @Feats() they cannot be executed in the + # __init__ function. + # """ + #if (self._verbose): + #print('initializing Lakeshore 332...') + + #if reset: + #if self._verbose: + #print('Resetting...') + #self.reset + #else: + #self.idn() + #self.mode() + + # def get_all(self): + # """ + # Gets the instrument identification and mode. + # """ + # self.idn() + # self.mode() @Action() def reset(self): - """ - Resets the Lakeshore 332 temperature controller. - """ - self.write('*RST') - - def get_all(self): - """ - Gets the instrument identification and mode. - """ - print(self.idn()) - print(self.mode()) - return None + """ + Resets the Lakeshore 332 temperature controller. + """ + self.write('*RST') @Feat() - def get_idn(self): + def idn(self): """ Returns the instrument identification. """ return self.query('*IDN?') @Feat() - def get_kelvin(self, channel): - """ - Returns temperature reading from specified channel in Kelvin. - """ - if channel in channel_list: - msg_str = 'KRDG?' + channel - return self.query(msg_str.format(channel)) - else: - print('Error: invalid channel') - return None + def kelvin(self, channel): + """ + Returns temperature reading from specified channel in Kelvin. + """ + if channel in channel_list: + return self.query('KRDG?{}'.format(channel)) + else: + print('Error: invalid channel') + return None @Feat() def get_sensor(self, channel): @@ -84,15 +112,16 @@ def get_sensor(self, channel): print('Error: invalid channel') return None - @Feat(values=self.heater_status_vals) + #@Feat(values = self.heater_status_vals) + @Feat() def get_heater_status(self): """ - Returns the header status. + Returns the heater status. """ ans = self.query('HTRST?') if self._verbose: if ans == '0': - print('Heater status: no error) + print('Heater status: no error') elif ans == '1': print('Heater status: open load') elif ans == '2': @@ -101,8 +130,9 @@ def get_heater_status(self): print('Error: Heater status invalid') return ans - @Feat(values=self.heater_range_vals) - def get_heater_range(self): + #@Feat(values=self.heater_range_vals) + @Feat() + def heater_range(self): """ Queries the instrument, prints a message describing the current heater range setting, then returns the heater range value. @@ -131,15 +161,16 @@ def heater_range(self, heater_setting): return self.write('RANGE {}'.format(heater_setting)) - @Feat(values=controller_modes) - def get_mode(self): + #@Feat(values=self.controller_modes) + @Feat() + def mode(self): """ Reads the mode setting of the controller. """ return self.query('MODE?') - @get_mode.setter - def get_mode(self, mode): + @mode.setter + def mode(self, mode): """ Sets controller mode, valid mode inputs are: local @@ -163,7 +194,7 @@ def pid(self, channel): fields = [float(f) for f in fields] return fields - @get_pid.setter + @pid.setter def pid(self, val, channel): """ Need to actually implement this! @@ -222,5 +253,7 @@ def cmd_mode(self, value): if __name__ == '__main__': - with Lakeshore332('GPIB::GPIB0::16::INSTR') as inst: - print('The identification of this instrument is : ' + inst.idn) + with Lakeshore332('GPIB0::16::INSTR') as inst: + print('The instrument identification is ' + inst.idn) + #print('The current temperature is '+ inst.kelvin(channel='A')) + #inst.initialize(reset=False) diff --git a/qtlab_Lakeshore332.py b/qtlab_Lakeshore332.py new file mode 100644 index 0000000..bab1cf9 --- /dev/null +++ b/qtlab_Lakeshore332.py @@ -0,0 +1,189 @@ +# Lakeshore 332, Lakeshore 332 temperature controller driver +# Reinier Heeres , 2010 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from instrument import Instrument +import visa +import types +import logging +import re +import math +import time + +class Lakeshore_332(Instrument): + + def __init__(self, name, address, reset=False): + Instrument.__init__(self, name) + + self._address = address + self._visa = visa.instrument(self._address) + self._channels = ('A', 'B') + + self.add_parameter('identification', + flags=Instrument.FLAG_GET) + + self.add_parameter('kelvin', + flags=Instrument.FLAG_GET, + type=types.FloatType, + channels=self._channels, + units='K') + self.add_parameter('heater_status', + flags=Instrument.FLAG_GET, + type=types.IntType) + + self.add_parameter('sensor', + flags=Instrument.FLAG_GET, + type=types.FloatType, + channels=self._channels, + units='') + + self.add_parameter('heater_range', + flags=Instrument.FLAG_GETSET, + type=types.IntType, + format_map={ + 0: 'off', + 1: 'low', + 2: 'med', + 3: 'high' + }) + + self.add_parameter('heater_output', + flags=Instrument.FLAG_GET, + type=types.FloatType, + units='%') + + self.add_parameter('mode', + flags=Instrument.FLAG_GETSET, + type=types.IntType, + format_map={0: 'Local', 1: 'Remote', 2: 'Remote, local lock'}) + + self.add_parameter('pid', + flags=Instrument.FLAG_GETSET, + type=types.TupleType, + channels=(1,2)) + + self.add_parameter('setpoint', + flags=Instrument.FLAG_GETSET, + type=types.FloatType, + channels=(1,2)) + # Manual output + self.add_parameter('mout', + flags=Instrument.FLAG_GETSET, + type=types.FloatType, + channels=(1,2)) + # Control mode + # : 1 = Manual PID, 2 = Zone, + #3 = Open Loop, 4 = AutoTune PID, 5 = AutoTune PI, 6 = AutoTune P. + self.add_parameter('cmode', + flags=Instrument.FLAG_GETSET, + type=types.IntType, + channels=(1,2)) + + self.add_function('local') + self.add_function('remote') + + if reset: + self.reset() + else: + self.get_all() + + def reset(self): + self._visa.write('*RST') + + def get_all(self): + self.get_identification() + self.get_mode() + + def do_get_identification(self): + return self._visa.ask('*IDN?') + + def do_get_kelvin(self, channel): + ans = self._visa.ask('KRDG? %s' % channel) + return float(ans) + + def do_get_sensor(self, channel): + ans = self._visa.ask('SRDG? %s' % channel) + return float(ans) + + def do_get_heater_status(self): + ans = self._visa.ask('HTRST?') + return int(ans) + + def do_get_heater_range(self): + ans = self._visa.ask('RANGE?') + if ans == '0': + print 'OFF' + elif ans == '1': + print 'LOW' + elif ans == '2': + print 'MED' + elif ans == '3': + print 'HIGH' + else: + print 'HEATER NOT RESPONDING CORRECTLY' + #return ans + + def do_set_heater_range(self, val): + self._visa.write('RANGE %d' % val) + + def do_get_heater_output(self): + ans = self._visa.ask('HTR?') + return ans + + def do_get_mode(self): + ans = self._visa.ask('MODE?') + return int(ans) + + def do_set_mode(self, mode): + self._visa.write('MODE %d' % mode) + + def local(self): + self.set_mode(1) + + def remote(self): + self.set_mode(2) + + def do_get_pid(self, channel): + ans = self._visa.ask('PID? %d' % channel) + fields = ans.split(',') + if len(fields) != 3: + return None + fields = [float(f) for f in fields] + return fields + + def do_set_pid(self, val, channel): + pass + + def do_get_setpoint(self, channel): + ans = self._visa.ask('SETP? %s' % channel) + return float(ans) + + def do_set_setpoint(self, val, channel): + self._visa.write('SETP %s, %f' % (channel, val)) + + def do_get_mout(self, channel): + ans = self._visa.ask('MOUT? %s' % channel) + return float(ans) + + def do_set_mout(self, val, channel): + self._visa.write('MOUT %s,%f' % (channel, val)) + + def do_get_cmode(self, channel): + ans = self._visa.ask('CMODE? %s' % channel) + return float(ans) + + def do_set_cmode(self, val, channel): + self._visa.write('CMODE %s,%f' % (channel, val)) From 69745228196ddc07bd9acf9807fa06ddf634a41b Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Thu, 3 Dec 2015 14:54:34 -0600 Subject: [PATCH 08/61] added info for installing National Instruments drivers added basic instructions for installing a NI USB -> GPIB interface and a NI DAQ --- LantzSetup.md | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/LantzSetup.md b/LantzSetup.md index 42c5876..9ce8106 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -30,6 +30,10 @@ This command will install a series of packages: pip (used for installation), num After this is installed, you should have all the pieces necessary to run the basic functionality of lantz. +### Possible Issues and Solutions ### + +If `pyqt` fails to install, put a copy of `qt.conf` in `Library\Bin`. Thanks to @varses for this tip! + ## 3. Install other packages using pip ## From the command line, run the command: @@ -38,10 +42,7 @@ From the command line, run the command: This command installs the colorama (used for producing colorful terminal output), pyserial (interfacing with serial devices), pyusb(interfacing with usb devices), and lantz (what you're supposedly hoping to install) packages to your Miniconda3 installation. -## 4. Install National Instruments Drivers ## -TODO: write this section! - -## 5 . Test your installation ## +## 4 . Test your installation ## From the command prompt, move up a directory into your main Miniconda3 installation folder, then run `python.exe` This should give you a Python 3.x command prompt! @@ -51,3 +52,27 @@ Now run the command: >>> import lantz This should import the lantz module. If this runs successfully, then you probably have installed lantz correctly. + +## 5. Install National Instruments Drivers ## +This seciton will might vary, depending on what NI device(s) you're trying to install, but I've included my setup information here for help. + +### 5.1 Installing GPIB Devices ### +I needed to install NI-488.2 version 15.0.0 for my National Instruments GPIB-USB-HS device (USB to GPIB interface). This package includes the drivers needed to interface with GPIB hardware, including the very useful NIMAX utility. + +First, download it from: http://www.ni.com/download/ni-488.2-15.0/5427/en. + +Next, right click on the file you downloaded and select "Run as Administrator". Select a destination folder (change it from the default if you want) to unzip the download files into. This will then start the installation wizard. This will install a bunch of stuff by default, so configure the installation to not install stuff you won't be needing (anything to interface with LabVIEW, for starters =P ). + +Once the wizard completes, you'll need to restart. I would also recommend installing an NI-488.2 patches that come up from the National Instruments software update. + +After doing this, you should be able run NI MAX and see a GPIB device that's connected by clicking on the Scan for Instruments button after selecting your GPIB device. + +### 5.2 Installing DAQ Devices ### +The DAQ I set up was a NI USB-6343. + +First, install the NIDAQmx 15.0.1 software, downloaded from: http://www.ni.com/download/ni-daqmx-15.0.1/5353/en. + +Next, right click on the file you downloaded and select "Run as Administrator". Select a destination folder (change it from the default if you want) to unzip the download files into. This will then start the installation wizard. This might install a bunch of stuff by default, so configure the installation to not install stuff you won't be needing (anything to interface with LabVIEW, for starters =P ). + +# 6. Start running Lantz! # +Stay safe and happy data acquisition! From 5700a806ca5227ea874e003da3e951477d821aa3 Mon Sep 17 00:00:00 2001 From: PeterMintun Date: Thu, 3 Dec 2015 16:50:05 -0600 Subject: [PATCH 09/61] add info on DAQ startup added info for verifying that DAQ works after reboot --- LantzSetup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LantzSetup.md b/LantzSetup.md index 9ce8106..100fe35 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -74,5 +74,7 @@ First, install the NIDAQmx 15.0.1 software, downloaded from: http://www.ni.com/d Next, right click on the file you downloaded and select "Run as Administrator". Select a destination folder (change it from the default if you want) to unzip the download files into. This will then start the installation wizard. This might install a bunch of stuff by default, so configure the installation to not install stuff you won't be needing (anything to interface with LabVIEW, for starters =P ). +After the installation wizard completes, you'll need to restart. Windows should recognize your DAQ device after rebooting. From there, you should be able to use the NI Device Manager/Test Panel applications to query the ports on your device. + # 6. Start running Lantz! # Stay safe and happy data acquisition! From 19b348ce68b3950a18f99b79f93e406dfa9406c6 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sun, 6 Dec 2015 15:51:02 -0600 Subject: [PATCH 10/61] finally getting comm fixed looks like the read termination I was using was wrong. things should be working now. --- Lakeshore332.py | 337 +++++++++++++++++++++++++----------------------- 1 file changed, 176 insertions(+), 161 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index e0c80d2..fb5ad8b 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -5,7 +5,7 @@ # Some sort of license information goes here. -from lantz import Feat, Action +from lantz import Feat, Action, DictFeat from lantz.messagebased import MessageBasedDriver class Lakeshore332(MessageBasedDriver): @@ -22,12 +22,15 @@ class Lakeshore332(MessageBasedDriver): # These defaults assume that you have set the IEEE Term setting to: Lf Cr DEFAULTS = {'COMMON': {'write_termination': '\n', - 'read_termination': '\n'}} + 'read_termination': ''}} + + GPIB_name = None + GPIB_address = -1 channel_list = ['A','B'] heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} heater_status_vals = {'no error':0, 'open load':1, 'short':2} - controller_modes = {'local': 0, 'remote':1, 'remote, local lockout': 2} + controller_modes = {'local': 0, 'remote': 1, 'remote, local lockout': 2} # def __init__(self, GPIB_name, GPIB_addr, reset=False): # res_name = GPIB_name + '::' + GPIB_addr + '::INSTR' @@ -74,186 +77,198 @@ class Lakeshore332(MessageBasedDriver): # """ # self.idn() # self.mode() - - @Action() - def reset(self): - """ - Resets the Lakeshore 332 temperature controller. - """ - self.write('*RST') - @Feat() def idn(self): """ Returns the instrument identification. """ + print('getting IDN') return self.query('*IDN?') - @Feat() - def kelvin(self, channel): + @Action() + def reset(self): """ - Returns temperature reading from specified channel in Kelvin. + Resets the Lakeshore 332 temperature controller. """ - if channel in channel_list: - return self.query('KRDG?{}'.format(channel)) - else: - print('Error: invalid channel') - return None - - @Feat() - def get_sensor(self, channel): - """ - Returns sensor reading from specified channel. - """ - if channel in channel_list: - msg_str = 'SRDG?' + channel - return self.query(msg_str.format(channel)) - else: - print('Error: invalid channel') - return None - - #@Feat(values = self.heater_status_vals) - @Feat() - def get_heater_status(self): - """ - Returns the heater status. - """ - ans = self.query('HTRST?') - if self._verbose: - if ans == '0': - print('Heater status: no error') - elif ans == '1': - print('Heater status: open load') - elif ans == '2': - print('Heater status: short') - else: - print('Error: Heater status invalid') - return ans - - #@Feat(values=self.heater_range_vals) - @Feat() - def heater_range(self): - """ - Queries the instrument, prints a message describing the current heater - range setting, then returns the heater range value. - """ - ans = self.query('RANGE?') - if self._verbose: - if ans == '0': - print('Heater range: off') - elif ans == '1': - print('Heater range: low') - elif ans == '2': - print('Heater range: medium') - elif ans == '3': - print('Heater range: high') - else: - print('Error: Heater not responding correctly') - return ans - - @heater_range.setter - def heater_range(self, heater_setting): - """ - Sets heater range to heater_setting. - - heater_setting must be an integer between 0 and 3 inclusive. - """ - return self.write('RANGE {}'.format(heater_setting)) - - - #@Feat(values=self.controller_modes) - @Feat() + print("resetting") + self.write('*RST') + print("reset") + + # @Feat() + # def kelvin(self, channel): + # """ + # Returns temperature reading from specified channel in Kelvin. + # """ + # if channel in channel_list: + # return self.query('KRDG?{}'.format(channel)) + # else: + # print('Error: invalid channel') + # return None + # + # @Feat() + # def get_sensor(self, channel): + # """ + # Returns sensor reading from specified channel. + # """ + # if channel in channel_list: + # msg_str = 'SRDG?' + channel + # return self.query(msg_str.format(channel)) + # else: + # print('Error: invalid channel') + # return None + # + # #@Feat(values = self.heater_status_vals) + # @Feat() + # def get_heater_status(self): + # """ + # Returns the heater status. + # """ + # ans = self.query('HTRST?') + # if self._verbose: + # if ans == '0': + # print('Heater status: no error') + # elif ans == '1': + # print('Heater status: open load') + # elif ans == '2': + # print('Heater status: short') + # else: + # print('Error: Heater status invalid') + # return ans + # + # #@Feat(values=self.heater_range_vals) + # @Feat() + # def heater_range(self): + # """ + # Queries the instrument, prints a message describing the current heater + # range setting, then returns the heater range value. + # """ + # ans = self.query('RANGE?') + # if self._verbose: + # if ans == '0': + # print('Heater range: off') + # elif ans == '1': + # print('Heater range: low') + # elif ans == '2': + # print('Heater range: medium') + # elif ans == '3': + # print('Heater range: high') + # else: + # print('Error: Heater not responding correctly') + # return ans + # + # @heater_range.setter + # def heater_range(self, heater_setting): + # """ + # Sets heater range to heater_setting. + # + # heater_setting must be an integer between 0 and 3 inclusive. + # """ + # return self.write('RANGE {}'.format(heater_setting)) + # + # + @Feat(values=controller_modes) def mode(self): """ Reads the mode setting of the controller. """ - return self.query('MODE?') + return int(self.query('MODE?')) @mode.setter def mode(self, mode): """ Sets controller mode, valid mode inputs are: - local - remote - remote, local lockout - """ - return self.query('MODE {}'.format(value)) - - def local(self): - self.set_mode('local') - - def remote(self): - self.set_mode('remote') - - @Feat() - def pid(self, channel): - ans = self.query('PID? {}'.format(channel)) - fields = ans.split(',') - if len(fields) != 3: - return None - fields = [float(f) for f in fields] - return fields - - @pid.setter - def pid(self, val, channel): - """ - Need to actually implement this! - """ - print('This feature is actually unimplemented! Sorry!') - - @Feat() - def setpoint(self, channel): - """ - Return the feedback controller's setpoint. - """ - if channel in self.channel_list: - return self.query('SETP?{}'.format(channel)) - else: - return None - - @setpoint.setter - def setpoint(self, channel, value): - """ - Sets the setpoint of channel channel to value value - """ - #TODO: actually implement this! - print('Not yet implemented!') - - @Feat() - def mout(self): - """ - """ - #TODO: actually implement this! - print('Not yet implemented!') - - @mout.setter - def mout(self, value): - """ - """ - #TODO: actually implement this! - print('Not yet implemented!') - - @Feat() - def cmd_mode(self): - """ - """ - #TODO: actually implement this! - print('Not yet implemented!') - - @cmd_mode.setter - def cmd_mode(self, value): - """ + local (0) + remote (1) + remote, local lockout (2) """ - #TODO: implement this! - print('Not yet implemented!') - - - - - + return self.query('MODE{}'.format(mode)) + # + # def local(self): + # self.set_mode('local') + # + # def remote(self): + # self.set_mode('remote') + # + # @Feat() + # def pid(self, channel): + # ans = self.query('PID? {}'.format(channel)) + # fields = ans.split(',') + # if len(fields) != 3: + # return None + # fields = [float(f) for f in fields] + # return fields + # + # @pid.setter + # def pid(self, val, channel): + # """ + # Need to actually implement this! + # """ + # print('This feature is actually unimplemented! Sorry!') + # + # @Feat() + # def setpoint(self, channel): + # """ + # Return the feedback controller's setpoint. + # """ + # if channel in self.channel_list: + # return self.query('SETP?{}'.format(channel)) + # else: + # return None + # + # @setpoint.setter + # def setpoint(self, channel, value): + # """ + # Sets the setpoint of channel channel to value value + # """ + # #TODO: actually implement this! + # print('Not yet implemented!') + # + # @Feat() + # def mout(self): + # """ + # """ + # #TODO: actually implement this! + # print('Not yet implemented!') + # + # @mout.setter + # def mout(self, value): + # """ + # """ + # #TODO: actually implement this! + # print('Not yet implemented!') + # + # @Feat() + # def cmd_mode(self): + # """ + # """ + # #TODO: actually implement this! + # print('Not yet implemented!') + # + # @cmd_mode.setter + # def cmd_mode(self, value): + # """ + # """ + # #TODO: implement this! + # print('Not yet implemented!') + # + # + # + # + # if __name__ == '__main__': with Lakeshore332('GPIB0::16::INSTR') as inst: + print('Getting instrument identification...') + #inst.query('*IDN?') print('The instrument identification is ' + inst.idn) + print('reset?') + inst.reset + print('reset.') + print('The current mode is ' + inst.mode + '.') + print(inst.controller_modes) + inst.mode = 'remote, local lockout' + print('Now the mode is ' + inst.mode + '.') + inst.mode = 'remote' + print('Now the mode is ' + inst.mode + '.') #print('The current temperature is '+ inst.kelvin(channel='A')) #inst.initialize(reset=False) From 21372633540bba851a3f87e929a6a96af799f1f7 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sun, 6 Dec 2015 22:44:02 -0600 Subject: [PATCH 11/61] adding commands expanding library of commands in the driver, and adding code debugging/to prove that they work --- Lakeshore332.py | 163 ++++++++++++++++++++---------------------------- 1 file changed, 68 insertions(+), 95 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index fb5ad8b..a361f5a 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -27,11 +27,14 @@ class Lakeshore332(MessageBasedDriver): GPIB_name = None GPIB_address = -1 - channel_list = ['A','B'] + channels = ['a', 'b'] heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} heater_status_vals = {'no error':0, 'open load':1, 'short':2} controller_modes = {'local': 0, 'remote': 1, 'remote, local lockout': 2} + T_min = 0 + T_max = 350 + # def __init__(self, GPIB_name, GPIB_addr, reset=False): # res_name = GPIB_name + '::' + GPIB_addr + '::INSTR' # super().__init__(res_name, name='Lakeshore332') @@ -94,78 +97,45 @@ def reset(self): self.write('*RST') print("reset") - # @Feat() - # def kelvin(self, channel): - # """ - # Returns temperature reading from specified channel in Kelvin. - # """ - # if channel in channel_list: - # return self.query('KRDG?{}'.format(channel)) - # else: - # print('Error: invalid channel') - # return None - # - # @Feat() - # def get_sensor(self, channel): - # """ - # Returns sensor reading from specified channel. - # """ - # if channel in channel_list: - # msg_str = 'SRDG?' + channel - # return self.query(msg_str.format(channel)) - # else: - # print('Error: invalid channel') - # return None - # - # #@Feat(values = self.heater_status_vals) - # @Feat() - # def get_heater_status(self): - # """ - # Returns the heater status. - # """ - # ans = self.query('HTRST?') - # if self._verbose: - # if ans == '0': - # print('Heater status: no error') - # elif ans == '1': - # print('Heater status: open load') - # elif ans == '2': - # print('Heater status: short') - # else: - # print('Error: Heater status invalid') - # return ans - # - # #@Feat(values=self.heater_range_vals) - # @Feat() - # def heater_range(self): - # """ - # Queries the instrument, prints a message describing the current heater - # range setting, then returns the heater range value. - # """ - # ans = self.query('RANGE?') - # if self._verbose: - # if ans == '0': - # print('Heater range: off') - # elif ans == '1': - # print('Heater range: low') - # elif ans == '2': - # print('Heater range: medium') - # elif ans == '3': - # print('Heater range: high') - # else: - # print('Error: Heater not responding correctly') - # return ans - # - # @heater_range.setter - # def heater_range(self, heater_setting): - # """ - # Sets heater range to heater_setting. - # - # heater_setting must be an integer between 0 and 3 inclusive. - # """ - # return self.write('RANGE {}'.format(heater_setting)) - # - # + @DictFeat(limits=(T_min,T_max), keys=channels) + def kelvin_meas(self, channel): + """ + Returns measured temperature reading from specified channel in Kelvin. + """ + return float(self.query('KRDG?{}'.format(channel))) + + @DictFeat(keys=channels) + def sensor(self, channel): + """ + Returns sensor reading from specified channel. + """ + return float(self.query('SRDG?{}'.format(channel))) + + @Feat(values=heater_status_vals) + def heater_status(self): + """ + Returns the heater status. + """ + return int(self.query('HTRST?')) + + @Feat(values=heater_range_vals) + def heater_range(self): + """ + Queries the instrument, prints a message describing the current heater + range setting, then returns the heater range value. + """ + return int(self.query('RANGE?')) + + @heater_range.setter + def heater_range(self, heater_setting): + """ + Sets heater range to heater_setting. + + heater_setting must be an integer between 0 and 3 inclusive. + """ + return self.write('RANGE {}'.format(heater_setting)) + + @Feat(values=controller_modes) def mode(self): """ @@ -237,38 +207,41 @@ def mode(self, mode): # #TODO: actually implement this! # print('Not yet implemented!') # - # @Feat() - # def cmd_mode(self): - # """ - # """ - # #TODO: actually implement this! - # print('Not yet implemented!') - # - # @cmd_mode.setter - # def cmd_mode(self, value): - # """ - # """ - # #TODO: implement this! - # print('Not yet implemented!') - # - # - # - # - # + if __name__ == '__main__': with Lakeshore332('GPIB0::16::INSTR') as inst: print('Getting instrument identification...') #inst.query('*IDN?') print('The instrument identification is ' + inst.idn) - print('reset?') + + print('resetting...') inst.reset print('reset.') + + # Testing mode switching functionality print('The current mode is ' + inst.mode + '.') - print(inst.controller_modes) inst.mode = 'remote, local lockout' print('Now the mode is ' + inst.mode + '.') inst.mode = 'remote' print('Now the mode is ' + inst.mode + '.') - #print('The current temperature is '+ inst.kelvin(channel='A')) - #inst.initialize(reset=False) + + # Testing Kelvin read functionality + print('Current temperature on channel a is ' + str(inst.kelvin_meas['a']) + + ' Kelvin') + print('Current temperature on channel b is ' + str(inst.kelvin_meas['b']) + + ' Kelvin') + + # Testing sensor reading functionality + print('Sensor reading on channel a is ' + str(inst.sensor['a'])) + print('Sensor reading on channel b is ' + str(inst.sensor['b'])) + + # Testing heater status + print('Heater status is ' + str(inst.heater_status)) + + # Testing heater range + print('Heater range is ' + str(inst.heater_range)) + inst.heater_range = 'low' + print('Heater range is ' + str(inst.heater_range)) + inst.heater_range = 'off' + print('Heater range is ' + str(inst.heater_range)) From 2c6ba680ea8004967c865988b0c0fcf01192c2ba Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sun, 6 Dec 2015 23:11:14 -0600 Subject: [PATCH 12/61] cleaned up comments got rid of some old extraneous code in there, tried to clean up style too --- Lakeshore332.py | 85 ++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index a361f5a..b88be01 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -35,21 +35,14 @@ class Lakeshore332(MessageBasedDriver): T_min = 0 T_max = 350 + T_min_set = 1.8 + T_max_set = 350 + # def __init__(self, GPIB_name, GPIB_addr, reset=False): # res_name = GPIB_name + '::' + GPIB_addr + '::INSTR' # super().__init__(res_name, name='Lakeshore332') # self._address = res_name _verbose = True - # self._idn = None - # - # print(res_name) - # - # print(self.channel_list) - # print(self.heater_range_vals) - # print(self.heater_status_vals) - # print(self.controller_modes) - - # def query(self, command, *, send_args=(None, None), recv_args=(None, None)): # answer = super().query(command, send_args=send_args, recv_args=recv_args) @@ -152,15 +145,14 @@ def mode(self, mode): remote, local lockout (2) """ return self.query('MODE{}'.format(mode)) - # - # def local(self): - # self.set_mode('local') - # - # def remote(self): - # self.set_mode('remote') - # - # @Feat() - # def pid(self, channel): + + @Feat() + def pid(self, channel): + """ + Get parameters for PID loop. + """ + print('PID loop not yet implemented') + return 0 # ans = self.query('PID? {}'.format(channel)) # fields = ans.split(',') # if len(fields) != 3: @@ -168,30 +160,28 @@ def mode(self, mode): # fields = [float(f) for f in fields] # return fields # - # @pid.setter - # def pid(self, val, channel): - # """ - # Need to actually implement this! - # """ - # print('This feature is actually unimplemented! Sorry!') - # - # @Feat() - # def setpoint(self, channel): - # """ - # Return the feedback controller's setpoint. - # """ - # if channel in self.channel_list: - # return self.query('SETP?{}'.format(channel)) - # else: - # return None - # - # @setpoint.setter - # def setpoint(self, channel, value): - # """ - # Sets the setpoint of channel channel to value value - # """ - # #TODO: actually implement this! - # print('Not yet implemented!') + @pid.setter + def pid(self, val, channel): + """ + Get parameters for PID loop + """ + print('This feature is actually unimplemented! Sorry!') + return 0 + + @DictFeat(limits=(T_min_set,T_max_set), keys=channels) + def setpoint(self, channel): + """ + Return the temperature controller setpoint. + """ + return float(self.query('SETP?{}'.format(channel))) + + @setpoint.setter + def setpoint(self, channel, T_set): + """ + Sets the setpoint of channel channel to value value + """ + print('Error: not implemented correctly') + return self.query('SETP{} {}'.format(channel,T_set)) # # @Feat() # def mout(self): @@ -245,3 +235,12 @@ def mode(self, mode): print('Heater range is ' + str(inst.heater_range)) inst.heater_range = 'off' print('Heater range is ' + str(inst.heater_range)) + + # Testing setpoint + print('Controller a setpoint is ' + str(inst.setpoint['a'])) + inst.setpoint['a'] = 50 + print('Controller a channel setpoint is ' + str(inst.setpoint['a'])) + inst.setpoint['a'] = 300 + print('Controller a channel setpoint is ' + str(inst.setpoint['a'])) + + print('Controller b setpoint is ' + str(inst.setpoint['b'])) From c68b41c12d36c8accd7240b6eb287aa29d4569e3 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 7 Dec 2015 10:26:42 -0600 Subject: [PATCH 13/61] finished implementing all functionality I think this one is done as far as functionality goes. Items to work on: test integration with other python code. Add some sort of basic logging functionality --- Lakeshore332.py | 242 +++++++++++++++++++++++++++--------------------- 1 file changed, 138 insertions(+), 104 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index b88be01..88b93ed 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -7,6 +7,8 @@ from lantz import Feat, Action, DictFeat from lantz.messagebased import MessageBasedDriver +from time import sleep + class Lakeshore332(MessageBasedDriver): """ @@ -22,15 +24,18 @@ class Lakeshore332(MessageBasedDriver): # These defaults assume that you have set the IEEE Term setting to: Lf Cr DEFAULTS = {'COMMON': {'write_termination': '\n', - 'read_termination': ''}} + 'read_termination': ''}} GPIB_name = None GPIB_address = -1 channels = ['a', 'b'] + loops = ['1', '2'] heater_range_vals = {'off': 0, 'low': 1, 'medium': 2, 'high': 3} - heater_status_vals = {'no error':0, 'open load':1, 'short':2} + heater_status_vals = {'no error': 0, 'open load': 1, 'short': 2} controller_modes = {'local': 0, 'remote': 1, 'remote, local lockout': 2} + cmodes = {'manual PID': 1, 'zone': 2, 'open loop': 3, 'AutoTune PID': 4, + 'AutoTune PI': 5, 'AutoTune P': 6} T_min = 0 T_max = 350 @@ -38,41 +43,8 @@ class Lakeshore332(MessageBasedDriver): T_min_set = 1.8 T_max_set = 350 - # def __init__(self, GPIB_name, GPIB_addr, reset=False): - # res_name = GPIB_name + '::' + GPIB_addr + '::INSTR' - # super().__init__(res_name, name='Lakeshore332') - # self._address = res_name _verbose = True - # def query(self, command, *, send_args=(None, None), recv_args=(None, None)): - # answer = super().query(command, send_args=send_args, recv_args=recv_args) - # if answer == 'ERROR': - # raise InstrumentError - # return answer - - #def initialize(self, reset=False): - # """ - # Initialization code for Lakeshore 332 should go in this function, since - # if you define functions as @Feats() they cannot be executed in the - # __init__ function. - # """ - #if (self._verbose): - #print('initializing Lakeshore 332...') - - #if reset: - #if self._verbose: - #print('Resetting...') - #self.reset - #else: - #self.idn() - #self.mode() - - # def get_all(self): - # """ - # Gets the instrument identification and mode. - # """ - # self.idn() - # self.mode() @Feat() def idn(self): """ @@ -83,19 +55,17 @@ def idn(self): @Action() def reset(self): - """ - Resets the Lakeshore 332 temperature controller. - """ - print("resetting") - self.write('*RST') - print("reset") - - @DictFeat(limits=(T_min,T_max), keys=channels) + """ + Resets the Lakeshore 332 temperature controller. + """ + self.write('*RST') + + @DictFeat(limits=(T_min, T_max), keys=channels) def kelvin_meas(self, channel): - """ - Returns measured temperature reading from specified channel in Kelvin. - """ - return float(self.query('KRDG?{}'.format(channel))) + """ + Returns measured temperature reading from specified channel in Kelvin. + """ + return float(self.query('KRDG?{}'.format(channel))) @DictFeat(keys=channels) def sensor(self, channel): @@ -128,6 +98,19 @@ def heater_range(self, heater_setting): """ return self.write('RANGE {}'.format(heater_setting)) + @Feat() + def heater_output_1(self): + """ + Returns Loop 1 heater output in percent (%). + """ + return float(self.query('HTR?')) + + @Feat() + def heater_output_2(self): + """ + Returns Loop 2 heater output in percent (%). + """ + return float(self.query('AOUT?')) @Feat(values=controller_modes) def mode(self): @@ -146,63 +129,82 @@ def mode(self, mode): """ return self.query('MODE{}'.format(mode)) - @Feat() - def pid(self, channel): + @DictFeat(keys=loops) + def pid(self, loop): """ Get parameters for PID loop. """ - print('PID loop not yet implemented') - return 0 - # ans = self.query('PID? {}'.format(channel)) - # fields = ans.split(',') - # if len(fields) != 3: - # return None - # fields = [float(f) for f in fields] - # return fields - # + return self.query('PID?{}'.format(loop)) + @pid.setter - def pid(self, val, channel): - """ - Get parameters for PID loop - """ - print('This feature is actually unimplemented! Sorry!') - return 0 - - @DictFeat(limits=(T_min_set,T_max_set), keys=channels) + def pid(self, loop, pid): + """ + Get parameters for PID loop + """ + p = pid[0] + i = pid[1] + d = pid[2] + return self.query('PID{},{},{},{}'.format(loop, p, i, d)) + + @DictFeat(limits=(T_min_set, T_max_set), keys=loops) def setpoint(self, channel): - """ - Return the temperature controller setpoint. - """ - return float(self.query('SETP?{}'.format(channel))) + """ + Return the temperature controller setpoint. + """ + return float(self.query('SETP?{}'.format(channel))) @setpoint.setter - def setpoint(self, channel, T_set): - """ - Sets the setpoint of channel channel to value value - """ - print('Error: not implemented correctly') - return self.query('SETP{} {}'.format(channel,T_set)) - # - # @Feat() - # def mout(self): - # """ - # """ - # #TODO: actually implement this! - # print('Not yet implemented!') - # - # @mout.setter - # def mout(self, value): - # """ - # """ - # #TODO: actually implement this! - # print('Not yet implemented!') - # + def setpoint(self, loop, T_set): + """ + Sets the setpoint of channel channel to value value + """ + self.query('SETP{},{}'.format(loop, T_set)) + sleep(0.05) + return + + @DictFeat(limits=(0, 100), keys=loops) + def mout(self, loop): + """ + Returns loop manual heater power output. + """ + return self.query('MOUT?{}'.format(loop)) + + @mout.setter + def mout(self, loop, percent): + """ + Sets loop manual heater power output in percent. + """ + return self.query('MOUT{},{}'.format(loop, percent)) + + @DictFeat(limits=(1, 6, 1), keys=loops) + def cmode(self, loop): + """ + Returns controller command mode according to the table: + 1: Manual PID + 2: Zone + 3: Open Loop + 4: AutoTune PID + 5: AutoTune PI + 6: AutoTune P + """ + return self.query('CMODE?{}'.format(loop)) + + @cmode.setter + def cmode(self, loop, value): + """ + Sets controller command mode according to the table: + 1: Manual PID + 2: Zone + 3: Open Loop + 4: AutoTune PID + 5: AutoTune PI + 6: AutoTune P + """ + return self.query('CMODE{},{}'.format(loop, value)) if __name__ == '__main__': with Lakeshore332('GPIB0::16::INSTR') as inst: - print('Getting instrument identification...') - #inst.query('*IDN?') print('The instrument identification is ' + inst.idn) print('resetting...') @@ -217,10 +219,10 @@ def setpoint(self, channel, T_set): print('Now the mode is ' + inst.mode + '.') # Testing Kelvin read functionality - print('Current temperature on channel a is ' + str(inst.kelvin_meas['a']) - + ' Kelvin') - print('Current temperature on channel b is ' + str(inst.kelvin_meas['b']) - + ' Kelvin') + print('Current temperature on channel a is ' + + str(inst.kelvin_meas['a']) + ' Kelvin') + print('Current temperature on channel b is ' + + str(inst.kelvin_meas['b']) + ' Kelvin') # Testing sensor reading functionality print('Sensor reading on channel a is ' + str(inst.sensor['a'])) @@ -236,11 +238,43 @@ def setpoint(self, channel, T_set): inst.heater_range = 'off' print('Heater range is ' + str(inst.heater_range)) - # Testing setpoint - print('Controller a setpoint is ' + str(inst.setpoint['a'])) - inst.setpoint['a'] = 50 - print('Controller a channel setpoint is ' + str(inst.setpoint['a'])) - inst.setpoint['a'] = 300 - print('Controller a channel setpoint is ' + str(inst.setpoint['a'])) + # Testing heater output + print('Loop 1 heater output ' + str(inst.heater_output_1) + '%') + print('Loop 2 heater output ' + str(inst.heater_output_2) + '%') + + # Testing manual output + print('Loop 1 manual output ' + str(inst.mout['1'])) + print('Loop 2 manual output ' + str(inst.mout['2'])) + inst.mout['1'] = 50 + inst.mout['2'] = 50 + print('Loop 1 manual output ' + str(inst.mout['1'])) + print('Loop 2 manual output ' + str(inst.mout['2'])) + inst.mout['1'] = 0 + inst.mout['2'] = 0 + print('Loop 1 manual output ' + str(inst.mout['1'])) + print('Loop 2 manual output ' + str(inst.mout['2'])) + + # Testing cmode + print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) + inst.cmode['1'] = 3 + print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) + inst.cmode['1'] = 1 + print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) + print('Loop 2 Command Mode: ' + str(inst.cmode['2'])) - print('Controller b setpoint is ' + str(inst.setpoint['b'])) + # Testing setpoint + inst.setpoint['1'] = 25 + print('Loop 1 setpoint is ' + str(inst.setpoint['1'])) + inst.setpoint['1'] = 50 + print('Loop 1 setpoint is ' + str(inst.setpoint['1'])) + inst.setpoint['1'] = 300 + print('Loop 1 setpoint is ' + str(inst.setpoint['1'])) + inst.setpoint['2'] = 300 + print('Loop 2 setpoint is ' + str(inst.setpoint['2'])) + + # Testing PID + inst.pid['1'] = list([10.0, 10.0, 10.0]) + print('Loop 1 PID parameters:' + str(inst.pid['1'])) + inst.pid['1'] = list([50.0, 20.0, 1.0]) + print('Loop 1 PID parameters:' + str(inst.pid['1'])) + print('Loop 2 PID parameters:' + str(inst.pid['2'])) From 0dc4de5a8e40d4584646bdcdb79cf0fcc0550cca Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 7 Dec 2015 10:32:34 -0600 Subject: [PATCH 14/61] fixed issue w/ CMODE changed CMODE to properly implement dictionary functionality --- Lakeshore332.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Lakeshore332.py b/Lakeshore332.py index 88b93ed..71ef8af 100644 --- a/Lakeshore332.py +++ b/Lakeshore332.py @@ -176,29 +176,29 @@ def mout(self, loop, percent): """ return self.query('MOUT{},{}'.format(loop, percent)) - @DictFeat(limits=(1, 6, 1), keys=loops) + @DictFeat(values=cmodes, keys=loops) def cmode(self, loop): """ - Returns controller command mode according to the table: - 1: Manual PID - 2: Zone - 3: Open Loop - 4: AutoTune PID - 5: AutoTune PI - 6: AutoTune P + Returns the control mode according to the following table. + 'manual PID' (1) + 'zone'(2) + 'open loop' (3) + 'AutoTune PID' (4) + 'AutoTune PI' (5) + 'AutoTune P' (6) """ - return self.query('CMODE?{}'.format(loop)) + return int(self.query('CMODE?{}'.format(loop))) @cmode.setter def cmode(self, loop, value): """ - Sets controller command mode according to the table: - 1: Manual PID - 2: Zone - 3: Open Loop - 4: AutoTune PID - 5: AutoTune PI - 6: AutoTune P + Sets the control mode according to the following table. + 'manual PID' (1) + 'zone'(2) + 'open loop' (3) + 'AutoTune PID' (4) + 'AutoTune PI' (5) + 'AutoTune P' (6) """ return self.query('CMODE{},{}'.format(loop, value)) @@ -256,9 +256,9 @@ def cmode(self, loop, value): # Testing cmode print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) - inst.cmode['1'] = 3 + inst.cmode['1'] = 'open loop' print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) - inst.cmode['1'] = 1 + inst.cmode['1'] = 'AutoTune P' print('Loop 1 Command Mode: ' + str(inst.cmode['1'])) print('Loop 2 Command Mode: ' + str(inst.cmode['2'])) From dc391fe00537923cefffe77cd2e17f7177b15927 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 8 Dec 2015 00:36:29 -0600 Subject: [PATCH 15/61] working on lockin and monochromater wrote most of functionality for lockin and monochromater, need to work on this to iron out some kinks, and more importantly, test some of this code! --- SP2150i.py | 92 ++++++ SignalRecovery7265.py | 649 ++++++------------------------------------ 2 files changed, 172 insertions(+), 569 deletions(-) create mode 100644 SP2150i.py diff --git a/SP2150i.py b/SP2150i.py new file mode 100644 index 0000000..5288af3 --- /dev/null +++ b/SP2150i.py @@ -0,0 +1,92 @@ +from lantz import Feat, DictFeat, Action +from lantz.messagebased import MessageBasedDriver + + +class SP2150i(MessageBasedDriver): + + + max_speed = 100 + wavelength_min = 380 + wavelength_max = 520 + + @Feat(limits=(0,max_speed)) + def scan_speed(self): + """ + Get scan rate in nm/min. + """ + return float(self.query('?NM/MIN')) + + @scan_speed.setter + def scan_speed(self, speed): + """ + Sets current scan speed in nm/min. + """ + return self.query('{} NM/MIN'.format(speed)) + + @Feat(limits=(wavelength_min, wavelength_max)) + def nm(self): + """ + Returns wavelength to nm. + """ + return float(self.query('?NM')) + + @nm.setter + def nm(self, wavelength): + """ + Sets output to specified wavelength, traveling at the current scan rate. + """ + return self.query('{} NM'.format(wavelength)) + + @Feat(limits=(1,2,1)) + def grating(self): + """ + Returns the current grating position + """ + return int(self.query('?GRATING')) + + @grating.setter + def grating(self, grating_num): + """ + Sets the current grating to be grating_num + """ + print('Warning: will wait 20 seconds to change grating.') + self.query('{} GRATING'.format(grating_num)) + sleep(20) + + @Feat(limits=(1,3,1)) + def turret(self): + """ + Returns the selected turret number. + """ + return int(self.query('?TURRET')) + + @turret.setter + def turret(self, turr_set): + """ + Selects the parameters for the grating on turret turr_set + """ + return self.query('{} TURRET'.format(turr_set)) + + @Feat() + def turret_spacing(self): + """ + Returns the groove spacing of the grating for each turret. + """ + return self.query('?TURRETS') + + @Feat() + def grating_settings(self): + """ + Returns the groove spacing and blaze wavelength of grating positions + 1-6. This corresponds to 2 grating positions for each of the 3 turrets + """ + return self.query('?GRATINGS') + + + + + + + if __name__ == '__main__': + with SP2150i('USB derp derp derp') as inst: + print('The instrument identification is ' + inst.idn) diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py index 6218cd5..1e9e2a9 100644 --- a/SignalRecovery7265.py +++ b/SignalRecovery7265.py @@ -1,6 +1,9 @@ -from lantz import Feat, DictFeat +from lantz import Feat, DictFeat, Action from lantz.messagebased import MessageBasedDriver +from time import sleep + + class SignalRecovery7265(MessageBasedDriver): """Signal Recovery 7265 DSP Lock-in Amplifier @@ -11,7 +14,8 @@ class SignalRecovery7265(MessageBasedDriver): "Stay safe and happy locking in..." - This driver assumes that the COMM settings of the 7265 are set up as follows: + This driver assumes that the COMM settings of the 7265 are set up as + follows: ADDRESS = N TERMINATOR = [CR],[LF] @@ -23,633 +27,140 @@ class SignalRecovery7265(MessageBasedDriver): DEFAULTS = {'COMMON': {'write_termination': '\n', 'read_termination': '\n'}} + sensitivities = {} + + time_constants = {0: '10e-6', 1: '20e-6', 2: '40e-6', 3: '80e-6', + 4: '160e-6', 5: '320e-6', 6: '640e-6', 7: '5e-3', + 8: '10e-3', 9: '20e-3', 10: '50e-3', 11: '100e-3', + 12: '200e-3', 13: '500e-3', 14: '1', 15: '2', 16: '5', + 17: '10', 18: '20', 19: '50', 20: '100', 21: '200', + 22: '500', 23: '1e3', 24: '2e3', 25: '5e3', 26: '10e3', + 27: '20e3', 28: '50e3', 29: '100'} + int_ext_refs = {'int': 0, 'rear ext': 1, 'ext': 2} - # Instrument Identification @Feat() def idn(self): - """Identification, should output 7265. + """ + Returns current instrument identification. """ return self.query('ID') - @Feat() - def firmware_rev(self): - print(self.query('REV')) - return 0 - #return self.query('REV') - - @Feat() - def firmware_ver(self): - return self.query('VER') - - - - def query(self, command, *, send_args=(None, None), recv_args=(None, None)): - answer = super().query(command, send_args=send_args, recv_args=recv_args) - if answer == 'ERROR': - raise InstrumentError - return answer - - - - # 6.4.05 Instrument outputs @Feat() def x(self): """ - Returns x demodulator output in volts or amps, depending on mode + Read x value from lockin. """ - return self.query('X.') + return float(self.query('X.')) @Feat() def y(self): """ - Returns y demodulator output in volts or amps, depending on mode + Read y value from lockin. """ - return self.query('Y.') + return float(self.query('Y.')) @Feat() def xy(self): """ - Returns x and y demodulator output in volts or amps, depending on mode + Read x and y values from lockin simultaneously. """ return self.query('XY.') @Feat() def magnitude(self): """ - Returns signal magnitude value in volts or amps, depending on mode + Read signal magnitude from lockin. """ - return self.query('MAG.') + return float(self.query('MAG.')) @Feat() def phase(self): """ - Returns signal phase in degrees. + Read signal phase from lockin. """ - return self.query('PHA.') + return float(self.query('PHA.')) @Feat() def mag_phase(self): """ - Returns signal magnitude (volts or amps) and phase (degrees). + Read signal magnitude and phase from lockin. """ return self.query('MP.') + # TODO: time constant @Feat() - def sqrt_noise_spectral_density(self): - """ - Returns square root of noise spectral density measured at y channel - output, in V/sqrt(Hz) or A/sqrt(Hz) depending on mode. - - This assumes that the y channel output is Gaussian w/ zero mean. - """ - return self.query('NHZ.') - - @Feat() - def equiv_noise_bandwidth(self): - """ - Returns the equivalent noise bandwidth of the output low pass filter at - current time constant setting (in Hz) - """ - return self.query('ENBW.') - - - ## 6.4.05 Unimplemented commands: - # X - fixed point X - # Y - fixed point Y - # XY - fixed point XY - # MAG - fixed point magnitude - # PHA - fixed point phase - # MP - fixed point magnitude and phase - # RT[.] - ratio output - # LR[.] - log ratio output - # ENBW - fixed point equivalent noise bandwidth - # NN - fixed point noise output - - ## Unimplemented user equation functionality - # DEFEQU - define user equation - # C1 - user equation C1 - # C2 - user equation C2 - # EQU n - output of equation 1/2 - - ## Unimplemented fast setup commands - # STAR - fast setup command - - # 6.4.01 Signal channel information - # Should these be implemented as DictFeat? - @Feat(limits=(0,2,1)) - def imode(self): - """ - Query current/voltage mode - Return values correspond to the following table. - - n Input mode - 0 Current mode off - voltage mode input ENABLED - 1 High bandwidth (HB) current mode enabled - 2 Low noise (LN) current mode enabled - """ - return self.query('IMODE') - - @imode.setter - def imode(self, value): - """ - Current/voltage mode input selector - - n Input mode - 0 Current mode off - voltage mode input ENABLED - 1 High bandwidth (HB) current mode enabled - 2 Low noise (LN) current mode enabled - """ - return self.query('IMODE{}'.format(value)) - - @Feat(limits=(0,3,1)) - def vmode(self): - """ - Query voltage input configuration. - - Value returned corresponds to the following table: - n Input configuration - 0: Both inputs grounded (test mode) - 1: A input only - 2: -B input only - 3: A-B differential mode - """ - return self.query('VMODE') - - @vmode.setter - def vmode(self, value): - """ - Set voltage input configuration according to the following table: - - n Input configuration - 0: Both inputs grounded (test mode) - 1: A input only - 2: -B input only - 3: A-B differential mode - """ - return self.query('VMODE{}'.format(value)) - - @Feat(limits=(0,1,1)) - def mode_FET(self): - """ - Returns voltage mode input device control, with values corresponding - to the following table: - - 0: Bipolar device, 10 kΩ input impedance, 2 nV/√Hz voltage noise at 1 kHz - 1: FET, 10 MΩ input impedance, 5 nV/√Hz voltage noise at 1 kHz - - """ - return self.query('FET') - - @mode_FET.setter - def mode_FET(self, value): - """ - Sets voltage mode input device control according to following table: - - 0: Bipolar device, 10 kΩ input impedance, 2 nV/√Hz voltage noise at 1 kHz - 1: FET, 10 MΩ input impedance, 5 nV/√Hz voltage noise at 1 kHz - """ - return self.query('FET{}'.format(value)) - - @Feat(limits=(0,1,1)) - def mode_float(self): - """ - Queries connector shield switch setting, with returned value corresponding - to the following table: - - n: Selection - 0: Ground - 1: Float (connected to ground via a 1 kΩ resistor) - """ - return self.query('FLOAT') - - @mode_float.setter - def mode_float(self,value): - """ - Sets connector shield switch setting, with values corresponding to the - following table: - - n: Selection - 0: Ground - 1: Float (connected to ground via a 1 kΩ resistor) - """ - return self.query('FLOAT{}'.format(value)) - - @Feat(limits=(0,1,1)) - def mode_CP(self): - """ - Reads out input coupling mode, with return value corresponding to the - following table: - - n: Coupling Mode - 0: AC - 1: DC - """ - return self.query('CP') - - @mode_CP.setter - def mode_CP(self, value): - """ - Sets input coupling mode according to the following table: - - n: Coupling Mode - 0: AC - 1: DC - """ - return self.query('CP{}'.format(value)) - - @Feat() - def sensitivity(self): - """ - Reads the sensitivity in floating point mode. Return value units are - either volts or amps depending on the IMODE selected. - """ - return self.query('SEN.') - - @Feat(limits=(0,27,1)) - def sensitivity_n(self): - """ - Reads the sensitivty according to the following lookup table. - Note that the physical values change depending on the value of IMODE. - - n full-scale sensitivity - IMODE=0 IMODE=1 IMODE=2 - 1: 2 nV 2 fA n/a - 2: 5 nV 5 fA n/a - 3: 10 nV 10 fA n/a - 4: 20 nV 20 fA n/a - 5: 50 nV 50 fA n/a - 6: 100 nV 100 fA n/a - 7: 200 nV 200 fA 2 fA - 8: 500 nV 500 fA 5 fA - 9: 1 µV 1 pA 10 fA - 10: 2 µV 2 pA 20 fA - 11: 5 µV 5 pA 50 fA - 12: 10 µV 10 pA 100 fA - 13: 20 µV 20 pA 200 fA - 14: 50 µV 50 pA 500 fA - 15: 100 µV 100 pA 1 pA - 16: 200 µV 200 pA 2 pA - 17: 500 µV 500 pA 5 pA - 18: 1 mV 1 nA 10 pA - 19: 2 mV 2 nA 20 pA - 20: 5 mV 5 nA 50 pA - 21: 10 mV 10 nA 100 pA - 22: 20 mV 20 nA 200 pA - 23: 50 mV 50 nA 500 pA - 24: 100 mV 100 nA 1 nA - 25: 200 mV 200 nA 2 nA - 26: 500 mV 500 nA 5 nA - 27: 1 V 1 µA 10 nA - - """ - return self.query('SEN') - - @sensitivity_n.setter - def sensitivity_n(self, value): + def time_constant(self): """ - Set the sensitivty according to the following lookup table. - Note that the sensitivity changes depending on the value of IMODE. - - n full-scale sensitivity - IMODE=0 IMODE=1 IMODE=2 - 1: 2 nV 2 fA n/a - 2: 5 nV 5 fA n/a - 3: 10 nV 10 fA n/a - 4: 20 nV 20 fA n/a - 5: 50 nV 50 fA n/a - 6: 100 nV 100 fA n/a - 7: 200 nV 200 fA 2 fA - 8: 500 nV 500 fA 5 fA - 9: 1 µV 1 pA 10 fA - 10: 2 µV 2 pA 20 fA - 11: 5 µV 5 pA 50 fA - 12: 10 µV 10 pA 100 fA - 13: 20 µV 20 pA 200 fA - 14: 50 µV 50 pA 500 fA - 15: 100 µV 100 pA 1 pA - 16: 200 µV 200 pA 2 pA - 17: 500 µV 500 pA 5 pA - 18: 1 mV 1 nA 10 pA - 19: 2 mV 2 nA 20 pA - 20: 5 mV 5 nA 50 pA - 21: 10 mV 10 nA 100 pA - 22: 20 mV 20 nA 200 pA - 23: 50 mV 50 nA 500 pA - 24: 100 mV 100 nA 1 nA - 25: 200 mV 200 nA 2 nA - 26: 500 mV 500 nA 5 nA - 27: 1 V 1 µA 10 nA - + Read current lockin time constant """ - self.write('TC{}'.format(value)) - - + print('not implemented yet') + return 0 - @Feat() - def auto_sensitivity(self): + @time_constant.setter + def time_constant(self, value): """ - The instrument adjusts its full-scale sensitivity so that the magnitude - output lies between 30% and 90% of full-scale. + Set lockin time constant. """ - return self.query('AS') + print('not implemented yet') + return 0 + # TODO: frequency @Feat() - def auto_measure(self): - """ - The instrument adjusts its full-scale sensitivity so that the magnitude - output lies between 30% and 90% of full-scale, and then performs an - auto-phase operation to maximize the X channel output and minimize the - Y channel output. - """ - return self.query('ASM') - - @Feat(limits=(0,9,1)) - def ac_gain(self): - """ - Reads out the gain of the signal channel amplifier, in range 0 to 9, - corresponding to 0dB - 90dB in 10dB steps. - """ - return self.query('ACGAIN') - - @ac_gain.setter - def ac_gain(self,value): - """ - Sets AC gain of signal channel amplifier in range 0 to 9, corresponding - to 0dB - 90dB in 10dB steps. - """ - return self.query('ACGAIN{}'.format(value)) - - @Feat(limits=(0,1,1)) - def ac_gain_auto(self): - """ - Returns if AC Gain is manually or automatically controlled. - - Values correspond to the following table: - - n: Status - 0: AC Gain is under manual control - 1: AC Gain is under automatic control - """ - return self.query('AUTOMATIC') - - @ac_gain_auto.setter - def ac_gain_auto(self, value): - """ - Sets AC gain to manual or automatic based on the following table: - - n: Status - 0: AC Gain is under manual control - 1: AC Gain is under automatic control - """ - return self.query('AUTOMATIC{}'.format(value)) - - # Unimplemented features - # LF[n1 n2] - signal channel line rejection filter control - # SAMPLE[n] - main ADC sample rate control - # RANGE[n] - signal recovery/vector voltmeter mode selector - - # 6.4.02 Reference Channel - - @Feat(limits=(0,2,1)) - def ref_mode(self): - """ - Returns the instrument reference mode, with return values corresponding - to the following table: - - n: Mode - 0: Single Reference / Virtual Reference mode - 1: Dual Harmonic mode - 2: Dual Reference mode + def frequency(self): """ - return self.query('REFMODE') - - @ref_mode.setter - def ref_mode(self,value): + Read current signal frequency. """ - Sets the instrument reference mode, according to the following table: + return float(self.query('FRQ.')) - n: Mode - 0: Single Reference / Virtual Reference mode - 1: Dual Harmonic mode - 2: Dual Reference mode + @frequency.setter + def frequency(self, freq): """ - return self.query('REFMODE{}'.format(value)) - - - @Feat(limits=(0,2,1)) - def ie(self): - """ - Reads out reference channel source according to the following table: - - n: Selection - 0: INT (internal) - 1: EXT LOGIC (external rear panel TTL input) - 2: EXT (external front panel analog input) - """ - return self.query('IE') - - @ie.setter - def ie(self,value): - """ - Selects reference channel source according to the following table: - - n: Selection - 0: INT (internal) - 1: EXT LOGIC (external rear panel TTL input) - 2: EXT (external front panel analog input) + Returns the frequency of the reference signal (either internal or + externally sourced) in Hz. """ - return self.query('IE{}'.format(value)) + print('not implemented yet') + return 0 - @Feat(limits=(0,65535,1)) - def refn(self): - """ - Returns n,measurement harmonic mode + # TODO: gain - """ - return self.query('REFN') + # TODO: sensitivity - - def refn(self, value): + # TODO: reference internal/external + @Feat(values=int_ext_refs) + def int_ext_ref(self): """ - - + Check if lockin is internally or externally referenced """ - return self.query('REFN{}'.format(value)) + return int(self.query('IE')) - @Feat(limits=(-360,360)) - def refp(self): + @int_ext_ref.setter + def int_ext_ref(self, value): """ - Sets + Set lockin to be internal or external reference """ + # TODO: fix this! + print('Warning: fails intermittently, need to fix') + return self.query('IE {}'.format(value)) - - - - - - # 6.4.03 Signal Channel Output Filters - @Feat(units='sec') - def time_constant(self): - return self.query('TC.') - - - @Feat(limits=(0,29,1)) - def time_constant_n(self): - return self.query('TC') - - @time_constant_n.setter - def time_constant_n(self, value): - """ - Sets lock-in time constant according to the following lookup table: - - 0: 10 µs - 1: 20 µs - 2: 40 µs - 3: 80 µs - 4: 160 µs - 5: 320 µs - 6: 640 µs - 7: 5 ms - 8: 10 ms - 9: 20 ms - 10: 50 ms - 11: 100 ms - 12: 200 ms - 13: 500 ms - 14: 1 s - 15: 2 s - 16: 5 s - 17: 10 s - 18: 20 s - 19: 50 s - 20: 100 s - 21: 200 s - 22: 500 s - 23: 1 ks - 24: 2 ks - 25: 5 ks - 26: 10 ks - 27: 20 ks - 28: 50 ks - 29: 100 ks - - """ - self.write('TC{}'.format(value)) - - # Unimplemented commands - # SYNC [n] - synchronous time constant control - # SLOPE [n] - output low-pass filter slope control - - - # 6.4.04 Signal Channel Output Amplifiers - # TODO implement this - - # 6.4.06 Internal Oscillator - # TODO implement this - - # 6.4.07 Auxiliary Outputs - # Not implemeting this section yet - # Missing commands list: - # DAC[.]n1[n2] Auxilliary DAC output controls - # BYTE [n] Digital output port controls - - # 6.4.08 Auxiliary Inputs - # Not implementing this section yet - # Missing commands list - # ADC[.]n - read auxiliary analog-to-digital Inputs - # ADC3TIME[n] - ADC3 sample time - # TADC[n] - Auxiliary ADC trigger mode control - # BURSTTPP [n] - set burst mode time per point rate for ADC1 and ADC2 - - # Output Data Curve Buffer - # Not implementing this section yet - # Missing commands list: - # CBD[n] - curve buffer define - # LEN[n] - curve length control - # NC - new curve - # STR[n] - storage interval control - # TD - take data - # TDT - take data triggered - # TDC - take data continually - # EVENT[n] - event marker control - # HC - halt curve acquisition - # M - curve acquisition status monitor - # DC[.]n - dump acquired curves to computer - # DCB n - dump acquired curves to computer in binary format - # DCT n - dump acquired curves to computer in table format - - ## 6.4.10 Computer interfaces (RS232 and GPIB) - # Not implementing yet - # Missing functions - # RS[n1[n2]] - set/read RS232 parameters - # GP[n1[n2]] - set/read GPIB parameters - # n1 sets GPIB address in the range 0 to 31 - # n2 sets GPIB terminator and test echo function - #\N n - Address command for daisy chaining - # DD[n] - define delimiter control - # ST - report status byte - # N - report overload byte - # MSK - set/read service request mask byte - # REMOTE[n] - remote only (front panel lock-out) control - - # 6.4.12 Front Panel - # Not implementing this yet - # Could be used to auto set up user interface for local operation - - # 6.4.13 Auto Default - # Not implementing this yet - # ADF[n] Can be used to reset to factor default settings - - # 6.4.14 Dual Mode Commands - # Used for either dual reference or dual harmonic modes - # Check manual to see which channels can be controlled independently - if __name__ == '__main__': - - from lantz import Q_ - - - #Define units - volt = Q_(1, 'V') - sec = Q_(1, 'sec') - - # Define GPIB location - GPIB_host = 'GPIB0' - GPIB_address = '10' - - LIA_name = GPIB_host + '::' + GPIB_address + '::INSTR' - - with SignalRecovery7265(LIA_name) as inst: - print('=== Instrument Identification ===') - print('idn:{}' .format(inst.idn)) - #print('Firmware revision:{}'.format(inst.firmware_rev)) - print('Firmware version: {}'.format(inst.firmware_ver)) - - - print('=== Time Constant Settings ===') - print('Time constant: {}'.format(inst.time_constant)) - inst.time_constant_n = 11 - print('Time constant [n]: {}'.format(inst.time_constant_n)) - inst.time_constant_n = 12 - - - - - print('=== Oscillator Settings ===') - print('Reference in/Ext:') - print('') - - - - - print('=== Instrument Measurements ===') - print('X: {}'.format(inst.x)) - print('Y: {}'.format(inst.y)) - print('X,Y: {}'.format(inst.xy)) - print('Magnitude: {}'.format(inst.magnitude)) - print('Phase: {}'.format(inst.phase)) - print('Magnitude, Phase: {}'.format(inst.mag_phase)) - print('Sqrt(Noise Spectral Density): {}'.format(inst.sqrt_noise_spectral_density)) - print('Equivalent Noise Bandwidth: {}'.format(inst.equiv_noise_bandwidth)) + with SignalRecovery7265('GPIB0::7::INSTR') as inst: + print('The instrument identification is ' + inst.idn) + + print('Testing signal readings') + print('Signal X component is ' + str(inst.x) + 'V') + print('Signal Y component is ' + str(inst.y) + 'V') + print('Signal magnitude is ' + str(inst.magnitude) + 'V') + print('Signal phase is ' + str(inst.phase) + 'degrees') + + print('Testing full quadrature readings') + print('XY measurement: ' + inst.xy) + print('magnitude and phase measurement: ' + inst.mag_phase) + + print('Internal External Reference check') + print('Internal/ext reference: ' + inst.int_ext_ref) + #inst.int_ext_ref = 'ext' + print('Internal/ext reference: ' + inst.int_ext_ref) + #inst.int_ext_ref = 'int' + print('Internal/ext reference: ' + inst.int_ext_ref) From 5d3cb5ed71b53fef0df67645916077433dd26824 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 8 Dec 2015 14:48:09 -0600 Subject: [PATCH 16/61] adding more functionality to LIA added more function to lockin driver, also testing slack integration --- SignalRecovery7265.py | 126 ++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 41 deletions(-) diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py index 1e9e2a9..7c2f6c4 100644 --- a/SignalRecovery7265.py +++ b/SignalRecovery7265.py @@ -18,24 +18,31 @@ class SignalRecovery7265(MessageBasedDriver): follows: ADDRESS = N - TERMINATOR = [CR],[LF] + TERMINATOR = [EOI] TECH ECHO = DISABLED SDC=ADF 1 ENABLED """ - DEFAULTS = {'COMMON': {'write_termination': '\n', - 'read_termination': '\n'}} + DEFAULTS = {'COMMON': {'write_termination': '', + 'read_termination': ''}} sensitivities = {} - time_constants = {0: '10e-6', 1: '20e-6', 2: '40e-6', 3: '80e-6', - 4: '160e-6', 5: '320e-6', 6: '640e-6', 7: '5e-3', - 8: '10e-3', 9: '20e-3', 10: '50e-3', 11: '100e-3', - 12: '200e-3', 13: '500e-3', 14: '1', 15: '2', 16: '5', - 17: '10', 18: '20', 19: '50', 20: '100', 21: '200', - 22: '500', 23: '1e3', 24: '2e3', 25: '5e3', 26: '10e3', - 27: '20e3', 28: '50e3', 29: '100'} + time_constants = {0: 10e-6, 1: 20e-6, 2: 40e-6, 3: 80e-6, 4: 160e-6, + 5: 320e-6, 6: 640e-6, 7: 5e-3, 8: 10e-3, 9: 20e-3, + 10: 50e-3, 11: 100e-3, 12: 0.2, 13: 0.5, 14: 1, 15: 2, + 16: 5, 17: 10, 18: 20, 19: 50, 20: 100, 21: 200, 22: 500, + 23: 1e3, 24: 2e3, 25: 5e3, 26: 10e3, 27: 20e3, 28: 50e3, + 29: 100} + + # time_constants = {10e-6: 0, 20e-6: 1, 40e-6: 2, 80e-6: 3, + # 160e-6: 4, 320e-6: 5, 640e-6: 6, 5e-3: 7, + # 10e-3: 8, 20e-3: 9, 50e-3: 10, 100e-3: 11, + # 200e-3: 12, 500e-3: 13, 1: 14, 2: 15, 5: 16, + # 10: 17, 20: 18, 50: 19, 100: 20, 200: 21, + # 500: 22, 1e3: 23, 2e3: 24, 5e3: 25, 10e3: 26, + # 20e3: 27, 50e3: 28, 100: 29} int_ext_refs = {'int': 0, 'rear ext': 1, 'ext': 2} @@ -51,82 +58,104 @@ def x(self): """ Read x value from lockin. """ - return float(self.query('X.')) + read = self.query('X.') + return float(read.replace('\x00', '')) @Feat() def y(self): """ Read y value from lockin. """ - return float(self.query('Y.')) + read = self.query('Y.') + return float(read.replace('\x00', '')) @Feat() def xy(self): """ Read x and y values from lockin simultaneously. """ - return self.query('XY.') + read = self.query('XY.') + return [float(x) for x in read.replace('\x00', '').split(',')] @Feat() def magnitude(self): """ Read signal magnitude from lockin. """ - return float(self.query('MAG.')) + read = self.query('MAG.') + return float(read.replace('\x00', '')) @Feat() def phase(self): """ Read signal phase from lockin. """ - return float(self.query('PHA.')) + read = self.query('PHA.') + return float(read.replace('\x00', '')) @Feat() def mag_phase(self): """ Read signal magnitude and phase from lockin. """ - return self.query('MP.') + read = self.query('MP.') + return [float(x) for x in read.replace('\x00', '').split(',')] # TODO: time constant - @Feat() + @Feat(limits=(0, 29, 1)) + def time_constant_integer(self): + """ + Read current lockin time constant mode setting + """ + return int(self.query('TC')) + + @time_constant_integer.setter + def time_constant_integer(self, integer): + """ + Set lockin time constant. + """ + return self.write('TC{}'.format(integer)) + + @Feat(values=time_constants) def time_constant(self): """ - Read current lockin time constant + Returns current time constant setting (in seconds). """ - print('not implemented yet') - return 0 + return float(self.query('TC.')) @time_constant.setter - def time_constant(self, value): + def time_constant(self, time_const): """ - Set lockin time constant. + Sets the current time constant setting (in seconds). """ - print('not implemented yet') - return 0 + return self.write('TC.{}'.format(time_const)) - # TODO: frequency @Feat() def frequency(self): """ Read current signal frequency. """ - return float(self.query('FRQ.')) + read = self.query('FRQ.') + return float(read.replace('\x00', '')) - @frequency.setter - def frequency(self, freq): + @Feat(limits=(0, 250e3)) + def oscillator_freq(self): """ - Returns the frequency of the reference signal (either internal or - externally sourced) in Hz. + Read internal oscillator frequency. """ - print('not implemented yet') - return 0 + return float(self.query('OF.')) + + @oscillator_freq.setter + def oscillator_freq(self, frequency): + """ + Set internal oscillator frequency. + """ + return self.write('OF.{}'.format(frequency)) # TODO: gain # TODO: sensitivity - # TODO: reference internal/external @Feat(values=int_ext_refs) def int_ext_ref(self): """ @@ -139,9 +168,7 @@ def int_ext_ref(self, value): """ Set lockin to be internal or external reference """ - # TODO: fix this! - print('Warning: fails intermittently, need to fix') - return self.query('IE {}'.format(value)) + return self.write('IE {}'.format(value)) if __name__ == '__main__': @@ -155,12 +182,29 @@ def int_ext_ref(self, value): print('Signal phase is ' + str(inst.phase) + 'degrees') print('Testing full quadrature readings') - print('XY measurement: ' + inst.xy) - print('magnitude and phase measurement: ' + inst.mag_phase) - + print('XY measurement: ' + str(inst.xy)) + print('magnitude and phase measurement: ' + str(inst.mag_phase)) + + print('What\'s the frequency, Kenneth?') + print('Reference frequency: ' + str(inst.frequency) + 'Hz') + inst.oscillator_freq = 137.0 + print('Reference frequency: ' + str(inst.frequency) + 'Hz') + inst.oscillator_freq = 17 + print('Reference frequency: ' + str(inst.frequency) + 'Hz') + # print('Internal External Reference check') print('Internal/ext reference: ' + inst.int_ext_ref) - #inst.int_ext_ref = 'ext' + inst.int_ext_ref = 'ext' print('Internal/ext reference: ' + inst.int_ext_ref) - #inst.int_ext_ref = 'int' + inst.int_ext_ref = 'int' print('Internal/ext reference: ' + inst.int_ext_ref) + + print('Time constant check') + print('Int TC: ' + str(inst.time_constant_integer)) + print('TC (sec): ' + str(inst.time_constant)) + inst.time_constant_integer = 15 + print('Int TC: ' + str(inst.time_constant_integer)) + print('TC (sec): ' + str(inst.time_constant)) + inst.time_constant = 100e-3 + print('Int TC: ' + str(inst.time_constant_integer)) + print('TC (sec): ' + str(inst.time_constant)) From 36a8d91aabde4d47983ef4c7e4826aec710740ca Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 8 Dec 2015 15:06:26 -0600 Subject: [PATCH 17/61] testing slack test slack --- SignalRecovery7265.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py index 7c2f6c4..bf9faff 100644 --- a/SignalRecovery7265.py +++ b/SignalRecovery7265.py @@ -116,6 +116,8 @@ def time_constant_integer(self, integer): """ return self.write('TC{}'.format(integer)) + # DERP DERP DERP test + @Feat(values=time_constants) def time_constant(self): """ From 58c4740359e600474836763b283658b0126fec68 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 9 Dec 2015 15:45:12 -0600 Subject: [PATCH 18/61] added sensitivities to SR7265, SP2150 is still derp this driver should be done...still missing comments and some logging functionality, but it should be able to be useful for most stuff you might want to do experimentally. SP2150 I can communicate w/ over USB, but there are some weird things going on where it drops commands or something. still debugging this. --- SP2150i.py | 46 ++++++--- SignalRecovery7265.py | 224 +++++++++++++++++++++++++++++++++--------- 2 files changed, 206 insertions(+), 64 deletions(-) diff --git a/SP2150i.py b/SP2150i.py index 5288af3..c729158 100644 --- a/SP2150i.py +++ b/SP2150i.py @@ -1,43 +1,60 @@ from lantz import Feat, DictFeat, Action from lantz.messagebased import MessageBasedDriver +from time import sleep + class SP2150i(MessageBasedDriver): + """ + + """ + MANUFACTURER_ID = '0x0647' + MODEL_CODE = '0x0100' + DEFAULTS = {'COMMON': {'write_termination': '\r', + 'read_termination': ''}} max_speed = 100 wavelength_min = 380 wavelength_max = 520 - @Feat(limits=(0,max_speed)) + @Feat(limits=(0, max_speed)) def scan_speed(self): """ Get scan rate in nm/min. """ - return float(self.query('?NM/MIN')) + return self.query('?NM/MIN') @scan_speed.setter def scan_speed(self, speed): """ Sets current scan speed in nm/min. """ - return self.query('{} NM/MIN'.format(speed)) + read = self.query('{} NM/MIN'.format(speed)) + read2 = read.replace('nm/min ok', '') + print('nm/min read:' + read2) + sleep(1) + return read @Feat(limits=(wavelength_min, wavelength_max)) def nm(self): """ - Returns wavelength to nm. """ - return float(self.query('?NM')) + print('Sending ?NM...') + read = self.query('?NM') + read = read.replace('nm ok', '') + read = read.replace('1` ', '') + return float(read) @nm.setter def nm(self, wavelength): """ - Sets output to specified wavelength, traveling at the current scan rate. + Sets output to specified wavelength, traveling at the current scan + rate. """ return self.query('{} NM'.format(wavelength)) - @Feat(limits=(1,2,1)) + @Feat(limits=(1, 2, 1)) def grating(self): """ Returns the current grating position @@ -53,7 +70,7 @@ def grating(self, grating_num): self.query('{} GRATING'.format(grating_num)) sleep(20) - @Feat(limits=(1,3,1)) + @Feat(limits=(1, 3, 1)) def turret(self): """ Returns the selected turret number. @@ -83,10 +100,9 @@ def grating_settings(self): return self.query('?GRATINGS') - - - - - if __name__ == '__main__': - with SP2150i('USB derp derp derp') as inst: - print('The instrument identification is ' + inst.idn) +if __name__ == '__main__': + with SP2150i('USB0::0x0647::0x0100::NI-VISA-120001::RAW') as inst: + print('Testing 1, 2, 3') + print('Wavelength: {}'.format(inst.nm)) + print('Scan rate: {}'.format(inst.scan_speed)) + # inst.nm = 400.0 diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py index bf9faff..00ffaa1 100644 --- a/SignalRecovery7265.py +++ b/SignalRecovery7265.py @@ -1,6 +1,8 @@ from lantz import Feat, DictFeat, Action from lantz.messagebased import MessageBasedDriver +from collections import OrderedDict + from time import sleep @@ -27,24 +29,87 @@ class SignalRecovery7265(MessageBasedDriver): DEFAULTS = {'COMMON': {'write_termination': '', 'read_termination': ''}} - sensitivities = {} - - time_constants = {0: 10e-6, 1: 20e-6, 2: 40e-6, 3: 80e-6, 4: 160e-6, - 5: 320e-6, 6: 640e-6, 7: 5e-3, 8: 10e-3, 9: 20e-3, - 10: 50e-3, 11: 100e-3, 12: 0.2, 13: 0.5, 14: 1, 15: 2, - 16: 5, 17: 10, 18: 20, 19: 50, 20: 100, 21: 200, 22: 500, - 23: 1e3, 24: 2e3, 25: 5e3, 26: 10e3, 27: 20e3, 28: 50e3, - 29: 100} - - # time_constants = {10e-6: 0, 20e-6: 1, 40e-6: 2, 80e-6: 3, - # 160e-6: 4, 320e-6: 5, 640e-6: 6, 5e-3: 7, - # 10e-3: 8, 20e-3: 9, 50e-3: 10, 100e-3: 11, - # 200e-3: 12, 500e-3: 13, 1: 14, 2: 15, 5: 16, - # 10: 17, 20: 18, 50: 19, 100: 20, 200: 21, - # 500: 22, 1e3: 23, 2e3: 24, 5e3: 25, 10e3: 26, - # 20e3: 27, 50e3: 28, 100: 29} - - int_ext_refs = {'int': 0, 'rear ext': 1, 'ext': 2} + INT_EXT_REF = OrderedDict([ + ('int', 0), + ('rear ext', 1), + ('ext', 2) + ]) + + TIME_CONSTANTS = OrderedDict([ + (0, 10e-6), + (1, 20e-6), + (2, 40e-6), + (3, 80e-6), + (4, 160e-6), + (5, 320e-6), + (6, 640e-6), + (7, 5e-3), + (8, 10e-3), + (9, 20e-3), + (10, 50e-3), + (11, 100e-3), + (12, 0.2), + (13, 0.5), + (14, 1), + (15, 2), + (16, 5), + (17, 10), + (18, 20), + (19, 50), + (20, 100), + (21, 200), + (22, 500), + (23, 1e3), + (24, 2e3), + (25, 5e3), + (26, 10e3), + (27, 20e3), + (28, 50e3), + (29, 100) + ]) + + AC_GAINS = OrderedDict([ + ('0dB', 0), + ('10dB', 1), + ('20dB', 2), + ('30dB', 3), + ('40dB', 4), + ('50dB', 5), + ('60dB', 6), + ('70dB', 7), + ('80dB', 8), + ('90dB', 9), + ]) + + SENSITIVITIES = OrderedDict([ + ('2e-9V', 1), + ('5e-9V', 2), + ('1e-8V', 3), + ('2e-8V', 4), + ('5e-8V', 5), + ('1e-7V', 6), + ('2e-7V', 7), + ('5e-7V', 8), + ('1e-6V', 9), + ('2e-6V', 10), + ('5e-6V', 11), + ('1e-5V', 12), + ('2e-5V', 13), + ('5e-5V', 14), + ('1e-4V', 15), + ('2e-4V', 16), + ('5e-4V', 17), + ('1e-3V', 18), + ('2e-3V', 19), + ('5e-3V', 20), + ('1e-2V', 21), + ('2e-2V', 22), + ('5e-2V', 23), + ('1e-1V', 24), + ('2e-1V', 25), + ('5e-1V', 26), + ('1V', 27), + ]) @Feat() def idn(self): @@ -101,7 +166,6 @@ def mag_phase(self): read = self.query('MP.') return [float(x) for x in read.replace('\x00', '').split(',')] - # TODO: time constant @Feat(limits=(0, 29, 1)) def time_constant_integer(self): """ @@ -116,9 +180,7 @@ def time_constant_integer(self, integer): """ return self.write('TC{}'.format(integer)) - # DERP DERP DERP test - - @Feat(values=time_constants) + @Feat(values=TIME_CONSTANTS) def time_constant(self): """ Returns current time constant setting (in seconds). @@ -128,9 +190,10 @@ def time_constant(self): @time_constant.setter def time_constant(self, time_const): """ - Sets the current time constant setting (in seconds). + Not implemented, not a built in functionality. """ - return self.write('TC.{}'.format(time_const)) + print('Error: invalid operation, cannot directly set TC') + return 0 @Feat() def frequency(self): @@ -154,11 +217,61 @@ def oscillator_freq(self, frequency): """ return self.write('OF.{}'.format(frequency)) - # TODO: gain + @Feat(values=AC_GAINS) + def gain(self): + """ + Read current AC gain (dB). + """ + return int(self.query('ACGAIN')) + + @gain.setter + def gain(self, gain_value): + """ + Set current AC gain (dB) + """ + return self.write('ACGAIN{}'.format(gain_value)) + + @Feat(values=SENSITIVITIES) + def sensitivity(self): + """ + Gets sensitivity according to the SENSITIVITIES table. + """ + return int(self.query('SEN')) + + # @Feat(values=SENSITIVITIES) + # def sensitivity_int(self): + # """ + # Returns integer value of sensitivity as described by SENSITIVITIES. + # """ + # return int(self.query('SEN')) + + @sensitivity.setter + def sensitivity(self, sen_it): + """ + Sets value of sensitivity as described by SENSITIVITIES. + """ + return self.write('SEN{}'.format(sen_it)) + + @Action() + def autosensitivity(self): + """ + Runs an autosensitivity operation. + + The instrument adjusts its full-scale sensitivity so that the magnitude + output lies between 30-90 percent of full-scale + """ + a = self.write('AS') + sleep(7.5) # wait for operation to complete + return a - # TODO: sensitivity + @Action() + def autogain(self): + """ + Set current AC gain to automatic. + """ + return self.write('AUTOMATIC1') - @Feat(values=int_ext_refs) + @Feat(values=INT_EXT_REF) def int_ext_ref(self): """ Check if lockin is internally or externally referenced @@ -172,41 +285,54 @@ def int_ext_ref(self, value): """ return self.write('IE {}'.format(value)) - if __name__ == '__main__': - with SignalRecovery7265('GPIB0::7::INSTR') as inst: + with SignalRecovery7265.via_gpib(7) as inst: print('The instrument identification is ' + inst.idn) print('Testing signal readings') - print('Signal X component is ' + str(inst.x) + 'V') - print('Signal Y component is ' + str(inst.y) + 'V') - print('Signal magnitude is ' + str(inst.magnitude) + 'V') - print('Signal phase is ' + str(inst.phase) + 'degrees') + print('Signal X: {}V'.format(inst.x)) + print('Signal Y: {}V'.format(inst.y)) + print('Signal magnitude: {}V'.format(inst.magnitude)) + print('Signal phase: {}degrees'.format(inst.phase)) print('Testing full quadrature readings') - print('XY measurement: ' + str(inst.xy)) - print('magnitude and phase measurement: ' + str(inst.mag_phase)) + print('X,Y: {}V'.format(list(inst.xy))) + print('Magnitude, Phase: {}'.format(list(inst.mag_phase))) - print('What\'s the frequency, Kenneth?') - print('Reference frequency: ' + str(inst.frequency) + 'Hz') + print('Testing frequency code') + print('Ref f: {}Hz'.format(inst.frequency)) inst.oscillator_freq = 137.0 - print('Reference frequency: ' + str(inst.frequency) + 'Hz') + print('Ref f: {}Hz'.format(inst.frequency)) inst.oscillator_freq = 17 - print('Reference frequency: ' + str(inst.frequency) + 'Hz') + print('Ref f: {}Hz'.format(inst.frequency)) # print('Internal External Reference check') - print('Internal/ext reference: ' + inst.int_ext_ref) + print('Internal/ext reference: {}'.format(inst.int_ext_ref)) inst.int_ext_ref = 'ext' - print('Internal/ext reference: ' + inst.int_ext_ref) + print('Internal/ext reference: {}'.format(inst.int_ext_ref)) inst.int_ext_ref = 'int' - print('Internal/ext reference: ' + inst.int_ext_ref) + print('Internal/ext reference: {}'.format(inst.int_ext_ref)) print('Time constant check') - print('Int TC: ' + str(inst.time_constant_integer)) - print('TC (sec): ' + str(inst.time_constant)) + print('Int TC: {}'.format(inst.time_constant_integer)) + print('TC (sec): {}s'.format(inst.time_constant)) inst.time_constant_integer = 15 - print('Int TC: ' + str(inst.time_constant_integer)) - print('TC (sec): ' + str(inst.time_constant)) - inst.time_constant = 100e-3 - print('Int TC: ' + str(inst.time_constant_integer)) - print('TC (sec): ' + str(inst.time_constant)) + print('Int TC: {}'.format(inst.time_constant_integer)) + print('TC (sec): {}s'.format(inst.time_constant)) + inst.time_constant_integer = 10 + print('Int TC: {}'.format(inst.time_constant_integer)) + print('TC (sec): {}s'.format(inst.time_constant)) + + print('AC Gain Check') + print('AC Gain: {}'.format(inst.gain)) + inst.gain = '30dB' + print('AC Gain: {}'.format(inst.gain)) + inst.autogain + print('AC Gain: {}'.format(inst.gain)) + + print('Sensitivity Check') + print('Sen: {}'.format(inst.sensitivity)) + inst.sensitivity = '2e-8V' + print('Sen: {}'.format(inst.sensitivity)) + inst.autosensitivity() + print('Sen: {}'.format(inst.sensitivity)) From b80c117f87484b25f9de5158acd212d72c594e1d Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 9 Dec 2015 16:31:44 -0600 Subject: [PATCH 19/61] adding DAQ_test code Added testing code from lantz examples to codebase. Note that this code requires you to manually download the ni/nidaqmx folder from LabPy\lantz on GitHub, then copy it to the appropriate folder in your python library --- NI/DAQ_test.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 NI/DAQ_test.py diff --git a/NI/DAQ_test.py b/NI/DAQ_test.py new file mode 100644 index 0000000..6cba208 --- /dev/null +++ b/NI/DAQ_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from lantz.drivers.ni.daqmx import System + + +def bulleted_list(title, elements, indent=2, bullet='-', details=False): + elements = tuple(elements) + sp = ' ' * indent + print('{}{} {}: {}'.format(sp, bullet, title, len(elements))) + if details: + sp += ' ' + for element in elements: + print(' {}{} {}'.format(sp, bullet, element)) + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--debug', action='store_true', + help='Print debug information') + parser.add_argument('-l', '--list', action='store_true', + help='List details') + + args = parser.parse_args() + + if args.debug: + import lantz.log + lantz.log.log_to_screen(lantz.log.DEBUG) + + inst = System() + print('DAQmx Version: {}.{}'.format(inst.version[0], inst.version[1])) + + print('') + print('System Devices: {}'.format(len(inst.devices))) + for key, value in inst.devices.items(): + print('-- {} --'.format(key)) + print(' - Is Simulated: {}'.format(value.is_simulated)) + print(' - Product Type: {}'.format(value.product_type)) + print(' - Product Number: {}'.format(value.product_number)) + print(' - Product Type: {}'.format(value.product_type)) + print(' - Serial Number: {}'.format(value.serial_number)) + print(' - Bus Info: {}'.format(value.bus_info)) + bulleted_list('AI Channels', value.analog_input_channels, + details=args.list) + bulleted_list('AI Channels', value.analog_input_channels, + details=args.list) + bulleted_list('AO Channels', value.analog_output_channels, + details=args.list) + bulleted_list('DI Lines', value.digital_input_lines, details=args.list) + bulleted_list('DO Lines', value.digital_output_lines, + details=args.list) + bulleted_list('DI Ports', value.digital_input_ports, details=args.list) + bulleted_list('DI Ports', value.digital_output_ports, + details=args.list) + bulleted_list('CI Ports', value.counter_input_channels, + details=args.list) + bulleted_list('CO Ports', value.counter_output_channels, + details=args.list) + print('') + print('System Tasks: {}'.format(len(inst.tasks))) + for key, value in inst.tasks.items(): + print('-- {} --'.format(key)) + print(' - Type: {}'.format(value.io_type)) + print(' - Task Complete: {}'.format(value.is_done)) + bulleted_list('Devices', value.devices.keys(), details=args.list) + bulleted_list('Channels', value.channels.keys(), details=args.list) + print('') + print('System Channels: {}'.format(len(inst.channels))) From b52f907a1c639aa0c823416aaf811ee3d3d15ebb Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 9 Dec 2015 16:32:43 -0600 Subject: [PATCH 20/61] rename DAQ_test to DAQ_info better reflects what this code actually does. --- NI/{DAQ_test.py => DAQ_info.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename NI/{DAQ_test.py => DAQ_info.py} (100%) diff --git a/NI/DAQ_test.py b/NI/DAQ_info.py similarity index 100% rename from NI/DAQ_test.py rename to NI/DAQ_info.py From 8e071e09fa493433ed822593a751d896320e6a42 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 10 Dec 2015 21:00:04 -0600 Subject: [PATCH 21/61] added autophase added autophase operation to lockin driver --- SignalRecovery7265.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SignalRecovery7265.py b/SignalRecovery7265.py index 00ffaa1..8a3c0a1 100644 --- a/SignalRecovery7265.py +++ b/SignalRecovery7265.py @@ -166,6 +166,14 @@ def mag_phase(self): read = self.query('MP.') return [float(x) for x in read.replace('\x00', '').split(',')] + @Action() + def autophase(self): + """ + Adds an offset to the phase of the lockin to minimize the y-channel + signal and maximize the x-channel signal. + """ + return self.write('AQN') + @Feat(limits=(0, 29, 1)) def time_constant_integer(self): """ @@ -298,6 +306,9 @@ def int_ext_ref(self, value): print('Testing full quadrature readings') print('X,Y: {}V'.format(list(inst.xy))) print('Magnitude, Phase: {}'.format(list(inst.mag_phase))) + inst.autophase + sleep(2) + print('Magnitude, Phase: {}'.format(list(inst.mag_phase))) print('Testing frequency code') print('Ref f: {}Hz'.format(inst.frequency)) From abc55c518e11602059c35ac22fcc7934d829a19a Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Fri, 11 Dec 2015 11:48:08 -0600 Subject: [PATCH 22/61] adding document describing lantz bug fixes adding document for future lantz bug fixes, will probably add more to this once I can diagnose problem w/ NI driver --- LantzMods.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 LantzMods.md diff --git a/LantzMods.md b/LantzMods.md new file mode 100644 index 0000000..c4d224b --- /dev/null +++ b/LantzMods.md @@ -0,0 +1,13 @@ +# Internal bug fixes to lantz code # +Here's some modifications I've made to the stock Lantz codes to eliminate some bugs. + +## Missing colorama imports ## +lantz/log.py add the line: + +`` +from colorama import Fore, Back, Style +`` + +This issue was actually originally addressed here: https://github.com/LabPy/lantz/issues/51 but not fixed in the next release. + +TODO: put in a formal push to fix this in the main distribution. From 46624480cb9d251e0ab1dc64f6e456c61fc74314 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sat, 12 Dec 2015 18:29:34 -0600 Subject: [PATCH 23/61] updating on progress re:DAQ brief explanation of current issue affecting lantz NI driver on 64-bit systems --- LantzMods.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/LantzMods.md b/LantzMods.md index c4d224b..a21d131 100644 --- a/LantzMods.md +++ b/LantzMods.md @@ -11,3 +11,12 @@ from colorama import Fore, Back, Style This issue was actually originally addressed here: https://github.com/LabPy/lantz/issues/51 but not fixed in the next release. TODO: put in a formal push to fix this in the main distribution. + + +## Changes to NIDAQ driver ## + +Still working on a bug fix for this, but can tell what's going wrong. + +Basically the GetTaskHandle() call fails because it truncates the TaskHandle to be a 32-bit integer, when the true value is 64-bit. + +If anyone has an idea of how to fix this, that would be great. From 5a27af2da69bb42fb2e9cd738ad837b65ff934c7 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sun, 13 Dec 2015 21:27:43 -0600 Subject: [PATCH 24/61] Acton install guide adding guide for installing/setting up Acton --- Acton_SP2150i_Install.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Acton_SP2150i_Install.md diff --git a/Acton_SP2150i_Install.md b/Acton_SP2150i_Install.md new file mode 100644 index 0000000..766bb50 --- /dev/null +++ b/Acton_SP2150i_Install.md @@ -0,0 +1,5 @@ +# Acton SP2150i Install Guide # +Author: Peter Mintun +Date: 12/13/2015 + +Pro tip: use http://juluribk.com/2014/09/19/controlling-sp2150i-monochromator-with-pythonpyvisa/ From 318fd3074f7e793414083ad60ae781989b1a712a Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 14 Dec 2015 10:12:11 -0600 Subject: [PATCH 25/61] DAQ demo code working! Figured out how to fix bug for DAQ demo code, basically was an issue w/ the 64-bit TaskHandle getting truncated. Put in a hack-y fix for this to avoid an integer overflow error, but the driver may still have other bugs. If anyone has a specific use case that should be developed specifically, let me know ASAP --- NI/DAQ_demo.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 NI/DAQ_demo.py diff --git a/NI/DAQ_demo.py b/NI/DAQ_demo.py new file mode 100644 index 0000000..053da89 --- /dev/null +++ b/NI/DAQ_demo.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from lantz.drivers.ni.daqmx import AnalogInputTask, Task, DigitalInputTask +from lantz.drivers.ni.daqmx import VoltageInputChannel + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--debug', action='store_true', + help='Print debug information') + parser.add_argument('phys_channel', type=str, + help='Physical channel') + args = parser.parse_args() + + if args.debug: + import lantz.log + lantz.log.log_to_screen(lantz.log.DEBUG) + + task = AnalogInputTask('test') + task.add_channel(VoltageInputChannel(args.phys_channel)) + task.start() + print('Voltage reading:{}'.format(task.read_scalar())) + task.stop() From 9a2e97112b9586d04e4291798382e7683a2d8b11 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 14 Dec 2015 11:58:23 -0600 Subject: [PATCH 26/61] pushing DAQ changes added file describing how to modify the DAQ code and pushed all changes made to get it running --- NI/DAQ Installation.md | 44 + NI/DAQ_info.py | 3 - NI/NI64/base.py | 1864 ++++++++++++++++++++++++++++++++++++++++ NI/NI64/channels.py | 741 ++++++++++++++++ NI/NI64/constants.py | 1541 +++++++++++++++++++++++++++++++++ NI/NI64/tasks.py | 525 +++++++++++ NI/foreign.py | 266 ++++++ 7 files changed, 4981 insertions(+), 3 deletions(-) create mode 100644 NI/DAQ Installation.md create mode 100644 NI/NI64/base.py create mode 100644 NI/NI64/channels.py create mode 100644 NI/NI64/constants.py create mode 100644 NI/NI64/tasks.py create mode 100644 NI/foreign.py diff --git a/NI/DAQ Installation.md b/NI/DAQ Installation.md new file mode 100644 index 0000000..5a31acd --- /dev/null +++ b/NI/DAQ Installation.md @@ -0,0 +1,44 @@ +# DAQ Installation Guide # +Author: Peter Mintun (pmintun@uchicago.edu) + +Date: 12/14/2015 + +This guide details the steps needed to hack the existing Lantz driver to work on a 64-bit system. + +## Installing Modified Lantz NI driver ## +If you are using Miniconda, the Lantz NI drivers will be installed at: `\Lib\site-packages\lantz\drivers\ni` + +You will need to download the files in the NI64 folder from this repository and copy them to this location. + +Basically, there's only one change to the existing driver on GitHub: +base.py - changed return value to uInt64 to accommodate 64-bit pointer type. +constants.py - no changes +channels.py - no changes +tasks.py - no changes + +Also take the `foreign.py` file and replace the base lantz `foreign.py` file, this is located in + `\Lib\site-packages\lantz\` + + +## Copy over 64-bit Libraries ## +*I'm not sure if this step is necessary, but this will ensure that your code actually links to the 64-bit versions of the DAQ libraries. Someone should actually try this guide without doing this step just to see.* + +Search for the 64-bit version of nicaiu.dll and NIDAQmx.lib and add them to the Lantz `daqmx` folder: `\Lib\site-packages\lantz\drivers\ni\daqmx` + + +## Run Test Codes ## +There are two test scripts I've adapted from the Lantz library for testing out the DAQ. + +`DAQ_info.py` will output a list of attributes for the DAQ, you can run it using the command: +``` +python DAQ_info.py +``` + +`DAQ_demo.py` will read a single analog input data point from one of the analog in system channels, you can run it with: +``` +python DAQ_demo.py 'dev1/ai0' +``` +where 'dev1' is your DAQ device name and 'ai0' is the analog input channel you want to read. + +## More Advanced Testing ## +Currently working on a more advanced test script to demonstrate some more advanced features of the DAQ. diff --git a/NI/DAQ_info.py b/NI/DAQ_info.py index 6cba208..63681f7 100644 --- a/NI/DAQ_info.py +++ b/NI/DAQ_info.py @@ -38,11 +38,8 @@ def bulleted_list(title, elements, indent=2, bullet='-', details=False): print(' - Is Simulated: {}'.format(value.is_simulated)) print(' - Product Type: {}'.format(value.product_type)) print(' - Product Number: {}'.format(value.product_number)) - print(' - Product Type: {}'.format(value.product_type)) print(' - Serial Number: {}'.format(value.serial_number)) print(' - Bus Info: {}'.format(value.bus_info)) - bulleted_list('AI Channels', value.analog_input_channels, - details=args.list) bulleted_list('AI Channels', value.analog_input_channels, details=args.list) bulleted_list('AO Channels', value.analog_output_channels, diff --git a/NI/NI64/base.py b/NI/NI64/base.py new file mode 100644 index 0000000..457cddc --- /dev/null +++ b/NI/NI64/base.py @@ -0,0 +1,1864 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.ni.daqmx.base + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation of base classes for Channels, Tasks and Devices + + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from lantz import Feat, Action +from lantz.errors import InstrumentError +from lantz.foreign import LibraryDriver, RetValue, RetStr + +from .constants import Constants, Types +from ctypes import cast + +from time import sleep + +default_buf_size = 2048 + +_SAMPLE_MODES = {'finite': Constants.Val_FiniteSamps, + 'continuous': Constants.Val_ContSamps, + 'hwtimed': Constants.Val_HWTimedSinglePoint} + +_BUS_TYPES = {'PCI': Constants.Val_PCI, 'PCIe': Constants.Val_PCIe, + 'PXI': Constants.Val_PXI, 'SCXI': Constants.Val_SCXI, + 'PCCard': Constants.Val_PCCard, 'USB': Constants.Val_USB, + 'UNKNOWN': Constants.Val_Unknown} + +_SIGNAL_TYPES = {'sample_clock': Constants.Val_SampleClock, + 'sample_complete': Constants.Val_SampleCompleteEvent, + 'change_direction': Constants.Val_ChangeDetectionEvent, + 'counter_output': Constants.Val_CounterOutputEvent} + +_EDGE_TYPES = {'rising': Constants.Val_Rising, 'falling': Constants.Val_Falling} + +_SLOPE_TYPES = {'rising': Constants.Val_RisingSlope, 'falling': Constants.Val_FallingSlope} + +_WHEN_WINDOW = {'entering': Constants.Val_EnteringWin, 'leaving': Constants.Val_LeavingWin} + +_WHEN_TRIGGER_DIG = {'high': Constants.Val_High, 'low': Constants.Val_Low} + +_WHEN_TRIGGER_ALVL = {'above': Constants.Val_AboveLvl, 'below': Constants.Val_BelowLvl} + +_WHEN_TRIGGER_AWIN = {'inside': Constants.Val_InsideWin, 'outside': Constants.Val_OutsideWin} + +_WHEN_MATCH = {True: Constants.Val_PatternMatches, False: Constants.Val_PatternDoesNotMatch} + +_TRIGGER_TYPES = {'digital_level': Constants.Val_DigLvl, + 'analog_level': Constants.Val_AnlgLvl, + 'analog_window': Constants.Val_AnlgWin} + +_CHANNEL_TYPES = {'AI': Constants.Val_AI, 'AO': Constants.Val_AO, + 'DI': Constants.Val_DI, 'DO': Constants.Val_DO, + 'CI': Constants.Val_CI, 'CO': Constants.Val_CO} + +class _Base(LibraryDriver): + """Base class for NIDAQmx + """ + + LIBRARY_NAME = 'nicaiu', 'nidaqmx' + LIBRARY_PREFIX = 'DAQmx' + + _DEVICES = {} + _TASKS = {} + _CHANNELS = {} + + def _get_error_string(self, error_code): + size = self.lib.GetErrorString(error_code, None, 0) + if size <= 0: + raise InstrumentError('Could not retrieve error string.') + err, msg = self.lib.GetErrorString(error_code, *RetStr(size)) + if err < 0: + raise InstrumentError('Could not retrieve error string.') + return msg + + def _get_error_extended_error_info(self): + size = self.lib.GetExtendedErrorInfo(None, 0) + if size <= 0: + raise InstrumentError('Could not retrieve extended error info.') + err, msg = self.lib.GetExtendedErrorInfo(*RetStr(size)) + if err < 0: + raise InstrumentError('Could not retrieve extended error info.') + return msg + + def _return_handler(self, func_name, ret_value): + if ret_value < 0 and func_name not in ('GetErrorString', 'GetExtendedErrorInfo'): + msg = self._get_error_string(ret_value) + raise InstrumentError(msg) + return ret_value + + def __get_fun(self, name): + return getattr(self.lib, name.format(self.operation_direction.title())) + + def _add_types(self): + + super()._add_types() + T = Types + self.lib.CreateAIVoltageChan.argtypes = [T.TaskHandle, T.string, T.string, T.int32, T.float64, T.float64, T.int32, T.string] + self.lib.ReadAnalogScalarF64.argtypes = [T.TaskHandle, T.float64, T._, T._] + +class _ObjectDict(object): + + def __init__(self, key_fun, obj_creator, dictionary=None): + self.key_fun = key_fun + self.obj_creator = obj_creator + self._internal = {} if dictionary is None else dictionary + + def __getitem__(self, item): + + if item in self._internal: + return self._internal[item] + + if item not in self: + raise KeyError('{} not found'.format(item)) + + value = self.obj_creator(item) + + self._internal[item] = value + + return value + + def __len__(self): + return sum((1 for key in self.keys())) + + def __contains__(self, item): + return item in self.key_fun() + + def keys(self): + for key in self.key_fun(): + yield key + + def values(self): + for key in self.keys(): + yield self[key] + + def items(self): + for key in self.keys(): + yield key, self[key] + + +class System(_Base): + """NI-DAQmx System + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.devices = _ObjectDict(self._device_names, Device, self._DEVICES) + self.tasks = _ObjectDict(self._task_names, Task, self._TASKS) + self.channels = _ObjectDict(self._channel_names, Channel, self._CHANNELS) + + @Feat(read_once=True) + def version(self): + """Version of installed NI-DAQ library. + """ + err, major = self.lib.GetSysNIDAQMajorVersion(RetValue('u32')) + err, minor = self.lib.GetSysNIDAQMinorVersion(RetValue('u32')) + return major, minor + + def _device_names(self): + """Return a tuple containing the names of all global devices installed in the system. + """ + err, buf = self.lib.GetSysDevNames(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + def _task_names(self): + """Return a tuple containing the names of all global tasks saved in the system. + """ + err, buf = self.lib.GetSysTasks(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + def _channel_names(self): + """Return a tuple containing the names of all global channels saved in the system. + """ + err, buf = self.lib.GetSysGlobalChans(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + +class Device(_Base): + """Device + """ + + def __init__(self, device_name, *args, **kwargs): + super().__init__(*args, **kwargs) + self.device_name = device_name + + def _preprocess_args(self, name, *args): + """Injects device_name to all call to the library + """ + if name in ('GetErrorString', 'GetExtendedErrorInfo'): + return super()._preprocess_args(name, *args) + else: + return super()._preprocess_args(name, *((self.device_name, ) + args)) + + @Feat(read_once=True) + def is_simulated(self): + """Return True if it is a simulated device + """ + err, value = self.lib.GetDevIsSimulated(RetValue('u32')) + return value != 0 + + @Feat(read_once=True) + def product_type(self): + """Return the product name of the device. + """ + err, buf = self.lib.GetDevProductType(*RetStr(default_buf_size)) + return buf + + @Feat(read_once=True) + def product_number(self): + """Return the unique hardware identification number for the device. + """ + err, value = self.lib.GetDevProductNum(RetValue('u32')) + return value + + @Feat(read_once=True) + def serial_number (self): + """Return the serial number of the device. This value is zero + if the device does not have a serial number. + """ + err, value = self.lib.GetDevSerialNum(RetValue('u32')) + return value + + @Feat(read_once=True) + def analog_input_channels(self): + """Return a tuple with the names of the analog input + physical channels available on the device. + """ + + err, buf = self.lib.GetDevAIPhysicalChans(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def analog_output_channels(self): + """Return a tuple with the names of the analog output + physical channels available on the device. + """ + err, buf = self.lib.GetDevAOPhysicalChans(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def digital_input_lines(self): + """Return a tuple with the names of the digital input lines + physical channels available on the device. + """ + err, buf = self.lib.GetDevDILines(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def digital_output_lines(self): + """Return a tuple with the names of the digital lines + ports available on the device. + """ + err, buf = self.lib.GetDevDOLines(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def digital_input_ports(self): + """Return a tuple with the names of the digital input + ports available on the device. + """ + err, buf = self.lib.GetDevDIPorts(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def digital_output_ports(self): + """Return a tuple with the names of the digital output + ports available on the device. + """ + err, buf = self.lib.GetDevDOPorts(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def counter_input_channels(self): + """Return a tuple with the names of the counter input + physical channels available on the device. + """ + err, buf = self.lib.GetDevCIPhysicalChans(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True) + def counter_output_channels(self): + """Return a tuple with the names of the counter input + physical channels available on the device. + """ + err, buf = self.lib.GetDevCOPhysicalChans(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat(read_once=True, values=_BUS_TYPES) + def bus_type(self): + """Return the bus type of the device. + """ + err, value = self.lib.GetDevBusType(RetValue('i32')) + return value + + @Feat(read_once=True) + def pci_bus_number (self): + """Return the PCI bus number of the device. + """ + err, value = self.lib.GetDevPCIBusNum(RetValue('i32')) + return value + + @Feat(read_once=True) + def pci_device_number (self): + """Return the PCI slot number of the device. + """ + err, value = self.lib.GetDevPCIDevNum(RetValue('i32')) + return value + + @Feat(read_once=True) + def pxi_slot_number(self): + """Return the PXI slot number of the device. + """ + err, value = self.lib.GetDevPXISlotNum(RetValue('u32')) + return value + + @Feat(read_once=True) + def pxi_chassis_number(self): + """Return the PXI chassis number of the device, as identified + in MAX. + """ + err, value = self.lib.GetDevPXIChassisNum(RetValue('u32')) + return value + + @Feat(read_once=True) + def bus_info(self): + t = self.bus_type + if t in ('PCI', 'PCIe'): + return '%s (bus=%s, device=%s)' % (t, self.pci_bus_number, self.pci_device_number) + if t == 'PXI': + return '%s (chassis=%s, slot=%s)' % (t, self.pxi_chassis_number, self.pxi_slot_number) + return t + + @Action() + def reset(self): + """Stops and deletes all tasks on a device and rests outputs to their defaults + """ + return self.lib.ResetDevice() + + +class Task(_Base): + """A task is a collection of one or more virtual channels with timing, + triggering, and other properties. Conceptually, a task represents a + measurement or generation you want to perform. All channels in a task + must be of the same I/O type, such as analog input or counter output. + + However, a task can include channels of different measurement types, + such as an analog input temperature channel and an analog input voltage + channel. For most devices, only one task per subsystem can run at once, + but some devices can run multiple tasks simultaneously. With some devices, + you can include channels from multiple devices in a task. + + :param name: Name assigned to the task (This can be changed by the + library. The final name will be stored in name attribute) + + """ + + _REGISTRY = {} + + @classmethod + def register_class(cls, klass): + cls._REGISTRY[klass.IO_TYPE] = klass + + @classmethod + def typed_task(cls, io_type): + return cls._REGISTRY[io_type] + + def _load_task(self, name): + err, self.__task_handle = self.lib.LoadTask(name, RetValue('u64')) + self.name = name + self.log_debug('Loaded task with {} ({})'.format(self.name, self.__task_handle)) + + def _create_task(self, name): + err, self.__task_handle = self.lib.CreateTask(name, RetValue('u64')) + err, self.name = self.lib.GetTaskName(*RetStr(default_buf_size)) + self.log_debug('Created task with {} ({})'.format(self.name, self.__task_handle)) + + def __init__(self, name='', *args, **kwargs): + super().__init__(name, *args, **kwargs) + if not name: + self._create_task(name) + else: + try: + self._load_task(name) + except Exception as e: + self._create_task(name) + + self.sample_mode = None + self.channels = _ObjectDict(self._channel_names, self._create_channel_from_name, self._CHANNELS) + self.devices = _ObjectDict(self._device_names, Device, self._DEVICES) + + @property + def task_handle(self): + return self.__task_handle + + def _preprocess_args(self, name, *args): + """Injects device_name to all call to the library + """ + if name in ('GetErrorString', 'GetExtendedErrorInfo', 'LoadTask', 'CreateTask'): + return super()._preprocess_args(name, *args) + else: + return super()._preprocess_args(name, *((self.task_handle, ) + args)) + + def _create_channel_from_name(self, name): + return Channel(self, name=name) + + def _channel_names(self): + """Return a tuple with the names of all virtual channels in the task. + """ + err, buf = self.lib.GetTaskChannels(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + def _device_names(self): + """Return a tuple with the names of all devices in the task. + """ + err, buf = self.lib.GetTaskDevices(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return names + + @Feat() + def io_type(self): + for name, channel in self.channels.items(): + return channel.io_type + else: + return None + + def add_channel(self, channel): + if not isinstance(channel, Channel): + raise TypeError('Only channels may be added to a task.') + + if channel.task is self.channels: + return + elif channel.task is None: + channel.task = self + else: + raise ValueError('Cannot add a channel that is already in another task') + + def execute_fun(self, func_name, *args): + return getattr(self.lib, func_name)(*args) + + def clear(self): + """Clear the task. + + Before clearing, this function stops the task, if necessary, + and releases any resources reserved by the task. You cannot + use a task once you clear the task without recreating or + reloading the task. + + If you use the DAQmxCreateTask function or any of the NI-DAQmx + Create Channel functions within a loop, use this function + within the loop after you finish with the task to avoid + allocating unnecessary memory. + """ + if self.task_handle: + self.lib.ClearTask() + self.__task_handle = None + + __del__ = clear + + @Feat() + def is_done(self): + """Queries the status of the task and indicates if it completed + execution. Use this function to ensure that the specified + operation is complete before you stop the task. + """ + err, value = self.lib.IsTaskDone(RetValue('u32')) + return value != 0 + + # States + + @Action() + def start(self): + """Start the task + + Transitions the task from the committed state to the running + state, which begins measurement or generation. Using this + function is required for some applications and optional for + others. + + If you do not use this function, a measurement task starts + automatically when a read operation begins. The autoStart + parameter of the NI-DAQmx Write functions determines if a + generation task starts automatically when you use an NI-DAQmx + Write function. + + If you do not call StartTask and StopTask when you + call NI-DAQmx Read functions or NI-DAQmx Write functions + multiple times, such as in a loop, the task starts and stops + repeatedly. Starting and stopping a task repeatedly reduces + the performance of the application. + """ + self.lib.StartTask() + + @Action() + def stop(self): + """Stop the task. + + Stop the task and returns it to the state it was in before + you called StartTask or called an NI-DAQmx Write function with + autoStart set to TRUE. + + If you do not call StartTask and StopTask when you call + NI-DAQmx Read functions or NI-DAQmx Write functions multiple + times, such as in a loop, the task starts and stops + repeatedly. Starting and stopping a task repeatedly reduces + the performance of the application. + + """ + self.lib.StopTask() + + @Action() + def verify(self): + """Verifies that all task parameters are valid for the hardware. + """ + self.alter_state('verify') + + @Action() + def commit(self): + """Programs the hardware as much as possible according + to the task configuration. + """ + self.alter_state('commit') + + @Action() + def reserve(self): + """Reserves the hardware resources needed for the + task. No other tasks can reserve these same resources. + """ + self.alter_state('reserve') + + @Action() + def unreserve(self): + """Release all reserved resources. + """ + self.alter_state('unreserve') + + @Action() + def abort(self): + """Abort an operation, such as Read or Write, that is currently active. + + The task is put into an unstable but recoverable state. To recover the + task, call Start to restart the task or call Stop to reset the task + without starting it. + """ + self.alter_state('abort') + + @Action(values={'start': Constants.Val_Task_Start, 'stop': Constants.Val_Task_Stop, + 'verify': Constants.Val_Task_Verify, 'commit': Constants.Val_Task_Commit, + 'reserve': Constants.Val_Task_Reserve, 'unreserve': Constants.Val_Task_Unreserve, + 'abort': Constants.Val_Task_Abort}) + def alter_state(self, new_state): + """Alters the state of a task according to the action you + specify. To minimize the time required to start a task, for + example, DAQmxTaskControl can commit the task prior to + starting. + + :param new_state: + """ + self.lib.TaskControl(new_state) + + _register_every_n_samples_event_cache = None + + def register_every_n_samples_event(self, func, samples=1, options=0, cb_data=None): + """Register a callback function to receive an event when the + specified number of samples is written from the device to the + buffer or from the buffer to the device. This function only + works with devices that support buffered tasks. + + When you stop a task explicitly any pending events are + discarded. For example, if you call DAQmxStopTask then you do + not receive any pending events. + + :param func: The function that you want DAQmx to call when the event + occurs. The function you pass in this parameter must have + the following prototype:: + + def func(task, event_type, samples, cb_data): + ... + return 0 + + Upon entry to the callback, the task parameter contains the + handle to the task on which the event occurred. The + event_type parameter contains the value you passed in the + event_type parameter of this function. The samples parameter + contains the value you passed in the samples parameter of + this function. The cb_data parameter contains the value you + passed in the cb_data parameter of this function. + + :param samples: The number of samples after which each event should occur. + + :param options: + + :param cb_data: + + See also: register_signal_event, register_done_event + """ + + if self.operation_direction == 'input': + event_type = Constants.Val_Acquired_Into_Buffer + else: + event_type = Constants.Val_Transferred_From_Buffer + + if options == 'sync': + options = Constants.Val_SynchronousEventCallbacks + + if func is None: + c_func = None # to unregister func + else: + if self._register_every_n_samples_event_cache is not None: + # unregister: + self.register_every_n_samples_event(None, samples=samples, options=options, cb_data=cb_data) + # TODO: check the validity of func signature + # TODO: use wrapper function that converts cb_data argument to given Python object + c_func = EveryNSamplesEventCallback_map[self.CHANNEL_TYPE](func) + + self._register_every_n_samples_event_cache = c_func + + self.lib.RegisterEveryNSamplesEvent(event_type, uInt32(samples), uInt32(options), c_func, cb_data) + + _register_done_event_cache = None + + def register_done_event(self, func, options=0, cb_data=None): + """Register a callback function to receive an event when a task + stops due to an error or when a finite acquisition task or + finite generation task completes execution. A Done event does + not occur when a task is stopped explicitly, such as by + calling DAQmxStopTask. + + + :param func: + + The function that you want DAQmx to call when the event + occurs. The function you pass in this parameter must have + the following prototype:: + + def func(task, status, cb_data = None): + ... + return 0 + + Upon entry to the callback, the taskHandle parameter + contains the handle to the task on which the event + occurred. The status parameter contains the status of the + task when the event occurred. If the status value is + negative, it indicates an error. If the status value is + zero, it indicates no error. If the status value is + positive, it indicates a warning. The callbackData parameter + contains the value you passed in the callbackData parameter + of this function. + + :param options : {int, 'sync'} + + Use this parameter to set certain options. You can + combine flags with the bitwise-OR operator ('|') to set + multiple options. Pass a value of zero if no options need to + be set. + + 'sync' - The callback function is called in the thread which + registered the event. In order for the callback to occur, + you must be processing messages. If you do not set this + flag, the callback function is called in a DAQmx thread by + default. + + Note: If you are receiving synchronous events faster than + you are processing them, then the user interface of your + application might become unresponsive. + + :param cb_data: + + A value that you want DAQmx to pass to the callback function + as the function data parameter. Do not pass the address of a + local variable or any other variable that might not be valid + when the function is executed. + + See also + + register_signal_event, register_every_n_samples_event + """ + if options=='sync': + options = Constants.Val_SynchronousEventCallbacks + + if func is None: + c_func = None + else: + if self._register_done_event_cache is not None: + self.register_done_event(None, options=options, cb_data=cb_data) + # TODO: check the validity of func signature + c_func = DoneEventCallback_map[self.CHANNEL_TYPE](func) + self._register_done_event_cache = c_func + + self.lib.RegisterDoneEvent(uInt32(options), c_func, cb_data) + + def operation_direction(self): + return 'input' if self.CHANNEL_TYPE else 'output' + + _register_signal_event_cache = None + + @Action(values=(None, _SIGNAL_TYPES, None, None)) + def register_signal_event(self, func, signal, options=0, cb_data=None): + """Registers a callback function to receive an event when the + specified hardware event occurs. + + When you stop a task explicitly any pending events are + discarded. For example, if you call DAQmxStopTask then you do + not receive any pending events. + + :param func: + + The function that you want DAQmx to call when the event + occurs. The function you pass in this parameter must have the + following prototype:: + + def func(task, signalID, cb_data): + ... + return 0 + + Upon entry to the callback, the task parameter contains the + handle to the task on which the event occurred. The signalID + parameter contains the value you passed in the signal + parameter of this function. The cb_data parameter contains + the value you passed in the cb_data parameter of this + function. + + :param signal: {'sample_clock', 'sample_complete', 'change_detection', 'counter_output'} + + The signal for which you want to receive results: + + 'sample_clock' - Sample clock + 'sample_complete' - Sample complete event + 'change_detection' - Change detection event + 'counter_output' - Counter output event + + :param options: + + :param cb_data: + + See also: register_done_event, register_every_n_samples_event + """ + + if options == 'sync': + options = Constants.Val_SynchronousEventCallbacks + + if func is None: + c_func = None + else: + if self._register_signal_event_cache is not None: + self._register_signal_event(None, signal=signal, options=options, cb_data=cb_data) + # TODO: check the validity of func signature + c_func = SignalEventCallback_map[self.CHANNEL_TYPE](func) + self._register_signal_event_cache = c_func + self.lib.RegisterSignalEvent(signal, uInt32(options), c_func, cb_data) + + + @Action(values=(str, str, _SAMPLE_MODES, None)) + def configure_timing_change_detection(self, rising_edge_channel='', falling_edge_channel='', + sample_mode='continuous', samples_per_channel=1000): + """Configures the task to acquire samples on the rising and/or + falling edges of the lines or ports you specify. + """ + + self.lib.CfgChangeDetectionTiming(rising_edge_channel, falling_edge_channel, + sample_mode, samples_per_channel) + + + @Action(values=(_SAMPLE_MODES, None)) + def configure_timing_handshaking(self, sample_mode='continuous', samples_per_channel=1000): + """Determines the number of digital samples to acquire or + generate using digital handshaking between the device and a + peripheral device. + """ + self.samples_per_channel = samples_per_channel + self.sample_mode = sample_mode + self.lib.CfgHandshakingTiming(sample_mode, samples_per_channel) + + @Action(values=(_SAMPLE_MODES, None)) + def configure_timing_implicit(self, sample_mode='continuous', samples_per_channel=1000): + """Sets only the number of samples to acquire or generate without + specifying timing. Typically, you should use this function + when the task does not require sample timing, such as tasks + that use counters for buffered frequency measurement, buffered + period measurement, or pulse train generation. + """ + self.samples_per_channel = samples_per_channel + self.sample_mode = sample_mode + self.lib.CfgImplicitTiming(self, sample_mode, samples_per_channel) + + @Action(values=(str, None, _EDGE_TYPES, _SAMPLE_MODES, None)) + def configure_timing_sample_clock(self, source='on_board_clock', rate=1, active_edge='rising', + sample_mode='continuous', samples_per_channel=1000): + """Set the source of the Sample Clock, the rate of the Sample + Clock, and the number of samples to acquire or generate. + + :param source: + + The source terminal of the Sample Clock. To use the + internal clock of the device, use None or use + 'OnboardClock'. + + :param rate: + + The sampling rate in samples per second. If you use an + external source for the Sample Clock, set this value to + the maximum expected rate of that clock. + + :param active_edge: + + Specifies on which edge of the clock to + acquire or generate samples: + + 'rising' - Acquire or generate samples on the rising edges + of the Sample Clock. + + 'falling' - Acquire or generate samples on the falling + edges of the Sample Clock. + + :param sample_mode: {'finite', 'continuous', 'hwtimed'} + + Specifies whether the task acquires or + generates samples continuously or if it acquires or + generates a finite number of samples: + + 'finite' - Acquire or generate a finite number of samples. + + 'continuous' - Acquire or generate samples until you stop the task. + + 'hwtimed' - Acquire or generate samples continuously + using hardware timing without a buffer. Hardware timed + single point sample mode is supported only for the + sample clock and change detection timing types. + + :param samples_per_channel: + + The number of samples to acquire or generate for each + channel in the task if `sample_mode` is 'finite'. If + sample_mode is 'continuous', NI-DAQmx uses this value to + determine the buffer size. + """ + if source == 'on_board_clock': + source = None + self.samples_per_channel = samples_per_channel + self.sample_mode = sample_mode + self.lib.CfgSampClkTiming(source, float64(rate), active_edge, sample_mode, samples_per_channel) + + def configure_timing_burst_handshaking_export_clock(self, *args, **kws): + """ + Configures when the DAQ device transfers data to a peripheral + device, using the DAQ device's onboard sample clock to control + burst handshaking timing. + """ + raise NotImplementedError + + def configure_timing_burst_handshaking_import_clock(self, *args, **kws): + """ + Configures when the DAQ device transfers data to a peripheral + device, using an imported sample clock to control burst + handshaking timing. + """ + raise NotImplementedError + + @Action(values=(str, _SLOPE_TYPES, None)) + def configure_trigger_analog_edge_start(self, source, slope='rising', level=1.0): + """ + Configures the task to start acquiring or generating samples + when an analog signal crosses the level you specify. + + :param source: + + The name of a channel or terminal where there is an analog + signal to use as the source of the trigger. For E Series + devices, if you use a channel name, the channel must be the + first channel in the task. The only terminal you can use for + E Series devices is PFI0. + + :param slope: + + Specifies on which slope of the signal to start acquiring or + generating samples when the signal crosses trigger level: + + 'rising' - Trigger on the rising slope of the signal. + + 'falling' - Trigger on the falling slope of the signal. + + :param level: + + The threshold at which to start acquiring or generating + samples. Specify this value in the units of the measurement + or generation. Use trigger slope to specify on which slope + to trigger at this threshold. + """ + + self.lib.CfgAnlgEdgeStartTrig(source, slope, level) + + @Action(values=(str, _WHEN_WINDOW, None, None)) + def configure_trigger_analog_window_start(self, source, when='entering', top=1.0, bottom=-1.0): + """Configure the task to start acquiring or generating samples + when an analog signal enters or leaves a range you specify. + + :param source: + + The name of a virtual channel or terminal where there + is an analog signal to use as the source of the trigger. + + For E Series devices, if you use a virtual channel, it must + be the first channel in the task. The only terminal you can + use for E Series devices is PFI0. + + :param when : {'entering', 'leaving'} + + Specifies whether the task starts measuring or generating + samples when the signal enters the window or when it leaves + the window. Use `bottom` and `top` to specify the limits of + the window. + + :param top : float + + The upper limit of the window. Specify this value in the + units of the measurement or generation. + + :param bottom : float + + The lower limit of the window. Specify this value in the + units of the measurement or generation. + """ + self.lib.CfgAnlgWindowStartTrig(self, source, when, top, bottom) + + @Action(values=(str, _EDGE_TYPES)) + def configure_trigger_digital_edge_start(self, source, edge='rising'): + """Configure the task to start acquiring or generating samples + on a rising or falling edge of a digital signal. + + :param source: + + The name of a terminal where there is a digital signal to + use as the source of the trigger. + + :param edge: {'rising', 'falling'} + + Specifies on which edge of a digital signal to start + acquiring or generating samples: rising or falling edge(s). + """ + + self.lib.CfgDigEdgeStartTrig(self, source, edge) + + @Action(values=(str, str, _WHEN_MATCH)) + def configure_trigger_digital_pattern_start(self, source, pattern, match=True): + """Configure a task to start acquiring or generating samples + when a digital pattern is matched. + + :param source: + + Specifies the physical channels to use for pattern + matching. The order of the physical channels determines the + order of the pattern. If a port is included, the order of + the physical channels within the port is in ascending order. + + :param pattern: + + Specifies the digital pattern that must be met for the + trigger to occur. + + :param when: {'matches', 'does_not_match'} + + Specifies the conditions under which the trigger + occurs: pattern matches or not. + """ + self.lib.CfgDigPatternStartTrig(self, source, pattern, match) + + def configure_trigger_disable_start(self): + """ + Configures the task to start acquiring or generating samples + immediately upon starting the task. + + Returns + ------- + + success_status : bool + """ + self.lib.DisableStartTrig() + + @Action(values=(str, _SLOPE_TYPES, None, None)) + def configure_analog_edge_reference_trigger(self, source, slope='rising',level=1.0, pre_trigger_samps=0): + """Configure the task to stop the acquisition when the device + acquires all pretrigger samples, an analog signal reaches the + level you specify, and the device acquires all post-trigger samples. + + :param source: + + The name of a channel or terminal where there is an analog + signal to use as the source of the trigger. For E Series + devices, if you use a channel name, the channel must be the + first channel in the task. The only terminal you can use for + E Series devices is PFI0. + + :param slope: + + Specifies on which slope of the signal to start acquiring or + generating samples when the signal crosses trigger level: + + 'rising' - Trigger on the rising slope of the signal. + + 'falling' - Trigger on the falling slope of the signal. + + :param level: + The threshold at which to start acquiring or generating + samples. Specify this value in the units of the measurement + or generation. Use trigger slope to specify on which slope + to trigger at this threshold. + + :param pre_trigger_samps: + + The minimum number of samples per channel to acquire before + recognizing the Reference Trigger. The number of posttrigger + samples per channel is equal to number of samples per channel + in the NI-DAQmx Timing functions minus pretriggerSamples. + """ + + self.lib.CfgAnlgEdgeRefTrig(source, slope, level, pre_trigger_samps) + + + @Feat(values=(str, _WHEN_WINDOW, None, None, None)) + def configure_analog_window_reference_trigger(self, source, when='entering',top=1.0, bottom=1.0, pre_trigger_samps=0): + """Configure the task to stop the acquisition when the device + acquires all pretrigger samples, an analog signal enters or + leaves a range you specify, and the device acquires all + post-trigger samples. + + + :param source : str + + The name of a channel or terminal where there is an analog + signal to use as the source of the trigger. For E Series + devices, if you use a channel name, the channel must be the + first channel in the task. The only terminal you can use for + E Series devices is PFI0. + + :param when : {'entering', 'leaving'} + + Specifies whether the Reference Trigger occurs when the signal + enters the window or when it leaves the window. Use + bottom and top to specify the limits of the window. + + 'entering' - Trigger when the signal enters the window. + + 'leaving' - Trigger when the signal leaves the window. + + :param top : float + + The upper limit of the window. Specify this value in the + units of the measurement or generation. + + :param bottom : float + + The lower limit of the window. Specify this value in the + units of the measurement or generation. + + :param pre_trigger_samps : uint32 + + The minimum number of samples per channel to acquire before + recognizing the Reference Trigger. The number of posttrigger + samples per channel is equal to number of samples per channel + in the NI-DAQmx Timing functions minus pretriggerSamples. + """ + + self.lib.CfgAnlgWindowRefTrig(source, when, top, bottom, pre_trigger_samps) + + @Action(values=(str, _SLOPE_TYPES, None)) + def configure_digital_edge_reference_trigger(self, source, slope='rising', pre_trigger_samps=0): + """Configures the task to stop the acquisition when the device + acquires all pretrigger samples, detects a rising or falling + edge of a digital signal, and acquires all posttrigger samples. + + :param source: + + The name of a channel or terminal where there is an analog + signal to use as the source of the trigger. For E Series + devices, if you use a channel name, the channel must be the + first channel in the task. The only terminal you can use for + E Series devices is PFI0. + + :param slope: + + Specifies on which slope of the signal to start acquiring or + generating samples when the signal crosses trigger level: + + 'rising' - Trigger on the rising slope of the signal. + + 'falling' - Trigger on the falling slope of the signal. + + :param pre_trigger_samps: + + The minimum number of samples per channel to acquire before + recognizing the Reference Trigger. The number of posttrigger + samples per channel is equal to number of samples per channel + in the NI-DAQmx Timing functions minus pretriggerSamples. + """ + if not source.startswith('/'): # source needs to start with a '/' TODO WHY? + source = '/' + source + self.lib.CfgDigEdgeRefTrig(source, slope, pre_trigger_samps) + + + @Action(values=(str, str, _WHEN_MATCH, None)) + def configure_digital_pattern_reference_trigger(self, source, pattern, match=True, pre_trigger_samps=0): + """Configure the task to stop the acquisition when the device + acquires all pretrigger samples, matches or does not match + a digital pattern, and acquires all posttrigger samples. + + :param source: + + The name of a channel or terminal where there is an analog + signal to use as the source of the trigger. For E Series + devices, if you use a channel name, the channel must be the + first channel in the task. The only terminal you can use for + E Series devices is PFI0. + + :param pattern: + + Specifies the digital pattern that must be met for the trigger to occur. + + :param match: Specifies if the conditions under which the trigger occurs + + 'match' - Trigger when the signal matches the pattern + + 'nomatch' - Trigger when the signal does NOT match the pattern + + :param pre_trigger_samps : uint32 + + The minimum number of samples per channel to acquire before + recognizing the Reference Trigger. The number of posttrigger + samples per channel is equal to number of samples per channel + in the NI-DAQmx Timing functions minus pretriggerSamples. + """ + + if not source.startswith('/'): # source needs to start with a '/' + source = '/' + source + + self.lib.CfgDigPatternRefTrig(self, source, pattern, match, pre_trigger_samps) + + @Action() + def disable_reference_trigger(self): + """ + Disables reference triggering for the measurement or generation. + + Returns + ------- + + success_status : bool + """ + return self.lib.DisableRefTrig(self) == 0 + + + #TODO CHECK + def set_buffer(self, samples_per_channel): + """ + Overrides the automatic I/O buffer allocation that NI-DAQmx performs. + + Parameters + ---------- + + samples_per_channel : int + + The number of samples the buffer can hold for each channel + in the task. Zero indicates no buffer should be + allocated. Use a buffer size of 0 to perform a + hardware-timed operation without using a buffer. + + Returns + ------- + + success_status : bool + """ + #channel_io_type = self.channel_io_type + #return CALL('Cfg%sBuffer' % (channel_io_type.title()), self, uInt32(samples_per_channel)) == 0 + pass + + @Feat(units='Hz') + def sample_clock_rate(self): + """Sample clock rate. + + Set to None to reset. + """ + + err, value = self.lib.GetSampClkRate(self, RetValue('f64')) + return value + + @sample_clock_rate.setter + def sample_clock_rate(self, value): + if value is None: + self.lib.ResetSampClkRate() + else: + self.lib.SetSampClkRate(value) + + @Feat() + def convert_clock_rate(self): + """Convert clock rate. + + The rate at which to clock the analog-to-digital + converter. This clock is specific to the analog input section + of multiplexed devices. + + By default, NI-DAQmx selects the maximum convert rate + supported by the device, plus 10 microseconds per channel + settling time. Other task settings, such as high channel + counts or setting Delay, can result in a faster default + convert rate. + + Set to None to reset + """ + err, value = self.lib.GetAIConvRate(RetValue('f64')) + return value + + @convert_clock_rate.setter + def convert_clock_rate(self, value): + if value is None: + self.lib.ResetAIConvRate() + else: + self.lib.SetAIConvRate(value) + + def sample_clock_max_rate(self): + """Maximum Sample Clock rate supported by the task, + based on other timing settings. For output tasks, the maximum + Sample Clock rate is the maximum rate of the DAC. For input + tasks, NI-DAQmx calculates the maximum sampling rate + differently for multiplexed devices than simultaneous sampling + devices. + + For multiplexed devices, NI-DAQmx calculates the maximum + sample clock rate based on the maximum AI Convert Clock rate + unless you set Rate. If you set that property, NI-DAQmx + calculates the maximum sample clock rate based on that + setting. Use Maximum Rate to query the maximum AI Convert + Clock rate. NI-DAQmx also uses the minimum sample clock delay + to calculate the maximum sample clock rate unless you set + Delay. + + For simultaneous sampling devices, the maximum Sample Clock + rate is the maximum rate of the ADC. + """ + err, value = self.lib.GetSampClkMaxRate(RetValue('f64')) + return value + + # Not implemented: + # DAQmxReadBinary*, DAQmxReadCounter*, DAQmxReadDigital* + # DAQmxGetNthTaskReadChannel, DAQmxReadRaw + # DAQmxWrite* + # DAQmxExportSignal + # DAQmxCalculateReversePolyCoeff, DAQmxCreateLinScale + # DAQmxWaitForNextSampleClock + # DAQmxSwitch* + # DAQmxConnectTerms, DAQmxDisconnectTerms, DAQmxTristateOutputTerm + # DAQmxResetDevice + # DAQmxControlWatchdog* + + # DAQmxAOSeriesCalAdjust, DAQmxESeriesCalAdjust, DAQmxGet*, + # DAQmxMSeriesCalAdjust, DAQmxPerformBridgeOffsetNullingCal, DAQmxRestoreLastExtCalConst + # DAQmxSelfCal, DAQmxSetAIChanCalCalDate, DAQmxSetAIChanCalExpDate, DAQmxSSeriesCalAdjust + # External Calibration, DSA Calibration, PXI-42xx Calibration, SCXI Calibration + # Storage, TEDS + # DAQmxSetAnalogPowerUpStates, DAQmxSetDigitalPowerUpStates + # DAQmxGetExtendedErrorInfo + + @Feat(values={True: Constants.Val_AllowRegen, False: Constants.Val_DoNotAllowRegen}) + def regeneration_enabled(self): + """Generating the same data more than once is allowed. + + Set to None to reset. + """ + err, value = self.lib.GetWriteRegenMode(RetValue('i32')) + return value + + @regeneration_enabled.setter + def regeneration_enabled(self, value): + if value is None: + self.lib.ResetWriteRegenMode() + else: + self.lib.SetWriteRegenMode(value) + + #TODO CHECK + @Feat(values={'digital_edge': Constants.Val_DigEdge, 'disable': Constants.Val_None, None: None}) + def arm_start_trigger_type(self): + """the type of trigger to use to arm the task for a + Start Trigger. If you configure an Arm Start Trigger, the task + does not respond to a Start Trigger until the device receives + the Arm Start Trigger. + + Use None to reset + """ + + err, value = self.lib.GetArmStartTrigType(RetValue('i32')) + return value + + @arm_start_trigger_type.setter + def arm_start_trigger_type(self, trigger_type): + if trigger_type is None: + self.lib.ResetArmStartTrigType() + else: + self.lib.SetArmStartTrigType(trigger_type) + + + @Feat() + def arm_start_trigger_source(self): + """Rhe name of a terminal where there is a digital + signal to use as the source of the Arm Start Trigger + + Use None to Reset + """ + err, value = self.lib.GetDigEdgeArmStartTrigSrc(RetStr(default_buf_size)) + return value + + @arm_start_trigger_source.setter + @arm_start_trigger_source.setter + def arm_start_trigger_source(self, source): + source = str (source) + if source is None: + self.lib.ResetDigEdgeArmStartTrigSrc() + else: + self.lib.SetDigEdgeArmStartTrigSrc(source) + + @Feat(values={None: None}.update(_EDGE_TYPES)) + def arm_start_trigger_edge(self): + """on which edge of a digital signal to arm the task + for a Start Trigger + + Set to None to reset + """ + err, value = self.lib.GetDigEdgeArmStartTrigEdge(RetValue('i32')) + + @arm_start_trigger_edge.setter + def arm_start_trigger_edge(self, edge): + if edge is None: + self.lib.ResetDigEdgeArmStartTrigEdge() + else: + self.lib.SetDigEdgeArmStartTrigEdge(edge) + + @Feat(values={None: None}.update(_TRIGGER_TYPES)) + def pause_trigger_type(self): + """The type of trigger to use to pause a task. + + Set to None to Reset + """ + err, value = self.lib.GetPauseTrigType(RetValue('i32')) + return value + + @pause_trigger_type.setter + def pause_trigger_type(self, trigger_type): + if trigger_type is None: + self.lib.ResetPauseTrigType() + else: + self.lib.SetPauseTrigType(trigger_type) + + @Feat() + def pause_trigger_source(self): + """The name of a virtual channel or terminal where + there is an analog signal to use as the source of the trigger. + + For E Series devices, if you use a channel name, the channel + must be the only channel in the task. The only terminal you + can use for E Series devices is PFI0. + """ + + type = self.pause_trigger_type + if type == 'digital_level': + fun = self.lib.GetDigLvlPauseTrigSrc + elif type == 'analog_level': + fun = self.lib.GetAnlgLvlPauseTrigSrc + elif type == 'analog_window': + fun = self.lib.GetAnlgWinPauseTrigSrc + else: + raise InstrumentError('Pause trigger type is not specified') + + err, value = fun(*RetStr(default_buf_size)) + return value + + @pause_trigger_source.setter + def pause_trigger_source(self, source): + + type = self.pause_trigger_type + if type == 'digital_level': + fun = self.lib.SetDigLvlPauseTrigSrc + elif type == 'analog_level': + fun = self.lib.SetAnlgLvlPauseTrigSrc + elif type == 'analog_window': + fun = self.lib.SetAnlgWinPauseTrigSrc + else: + raise InstrumentError('Pause trigger type is not specified') + + fun(source) + + @Feat() + def pause_trigger_when(self): + """ + Specifies whether the task pauses above or below the threshold + you specify with Level. + + Specifies whether the task pauses while the trigger signal is + inside or outside the window you specify with Bottom and Top. + + Specifies whether the task pauses while the signal is high or + low. + + Set To None to reset + """ + type = self.pause_trigger_type + if type == 'digital_level': + fun = self.lib.SetDigLvlPauseTrigWhen + convert = _WHEN_TRIGGER_DIG + elif type == 'analog_level': + fun = self.lib.SetAnlgLvlPauseTrigWhen + convert = _WHEN_TRIGGER_ALVL + elif type == 'analog_window': + fun = self.lib.SetAnlgWinPauseTrigWhen + convert = _WHEN_TRIGGER_AWIN + else: + raise InstrumentError('Pause trigger type is not specified') + + err, val = fun(RetValue('i32')) + for key, value in convert.items(): + if key == val: + return val + else: + raise ValueError(val) + + @pause_trigger_when.setter + def pause_trigger_when (self, when=None): + + if when is None: + self.lib.ResetDigLvlPauseTrigWhen() + return + + type = self.pause_trigger_type + if type == 'digital_level': + fun = self.lib.SetDigLvlPauseTrigWhen + convert = _WHEN_TRIGGER_DIG + elif type == 'analog_level': + fun = self.lib.SetAnlgLvlPauseTrigWhen + convert = _WHEN_TRIGGER_ALVL + elif type == 'analog_window': + fun = self.lib.SetAnlgWinPauseTrigWhen + convert = _WHEN_TRIGGER_AWIN + else: + raise InstrumentError('Pause trigger type is not specified') + + fun(convert[when]) + + def read_current_position (self): + """Samples per channel the current position in the buffer. + """ + err, value = self.lib.GetReadCurrReadPos(*RetValue('u64')) + return value + + def samples_per_channel_available(self): + """The number of samples available to read per channel. + + This value is the same for all channels in the task. + """ + err, value = self.lib.GetReadAvailSampPerChan(*RetValue('u32')) + return value + + def samples_per_channel_acquired(self): + """The total number of samples acquired by each channel. + + NI-DAQmx returns a single value because this value is + the same for all channels. + """ + err, value = self.lib.GetReadTotalSampPerChanAcquired(*RetValue('u32')) + return value + + @Action(units='seconds') + def wait_until_done(self, timeout=-1): + """Wait for the measurement or generation to complete. Use this + function to ensure that the specified operation is complete + before you stop the task. + + :param timeout: The maximum amount of time, in seconds, to wait for the + measurement or generation to complete. The function returns + an error if the time elapses before the measurement or + generation is complete. + + A value of -1 (Constants.Val_WaitInfinitely) means to wait + indefinitely. + + If you set timeout to 0, the function checks once and + returns an error if the measurement or generation is not + done. + """ + if timeout < 0: + timeout = Constants.Val_WaitInfinitely + return self.lib.WaitUntilTaskDone(timeout) + + +class Channel(_Base): + """A virtual channel is a collection of settings such as a name, + a physical channel, input terminal connections, the type of measurement + or generation, and can include scaling information. + """ + + IO_TYPE = None + + def __init__(self, task=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self._task = None + if task == 'create': + task = Task.typed_task(self.IO_TYPE)() + print(task) + self.task = task + + @property + def task(self): + return self._task + + @task.setter + def task(self, value): + if not self._task is None: + raise Exception + self._task = value + if value is not None: + self.log_debug('Creating channel {} with {}'.format(self.CREATE_FUN, self._create_args)) + value.execute_fun(self.CREATE_FUN, *self._create_args) + + def _preprocess_args(self, name, *args): + """Injects device_name to all call to the library + """ + return super()._preprocess_args(name, *((self.task.task_handle, self.name) + args)) + + @Feat(read_once=True, values=_CHANNEL_TYPES) + def io_type(self): + """The type of the virtual channel. + """ + err, value = self.lib.GetChanType(RetValue('i32')) + return value + + @Feat(read_once=True) + def physical_channel_name(self,): + """Name of the physical channel upon which this virtual channel is based. + """ + err, value = self.lib.GetPhysicalChanName(*RetStr(default_buf_size)) + return value + + def operation_direction(self): + return 'input' if self.CHANNEL_TYPE else 'output' + + @Feat(read_once=True) + def is_global(self): + """Indicates whether the channel is a global channel. + """ + err, value = self.lib.GetChanIsGlobal(RetValue('u32')) + return value + + # TODO: DAQmx*ChanDescr + + @Feat() + def buffer_size_on_board(self): + """The number of samples the I/O buffer can hold for each + channel in the task. + + If on_board is True then specifies in samples per channel the + size of the onboard I/O buffer of the device. + + See also + -------- + set_buffer_size, reset_buffer_size + """ + fun = self.__get_fun('GetBuf{}OnbrdBufSize') + err, value = fun(RetValue('u32')) + return value + + @Feat() + def buffer_size(self): + """The number of samples the I/O buffer can hold for each + channel in the task. + + Set to 0 to perform a hardware-timed operation without + using a buffer. Setting this property overrides the automatic I/O + buffer allocation that NI-DAQmx performs. + + Set to None to reset. + """ + fun = self.__get_fun('GetBuf{}BufSize') + err, value = fun(RetValue('u32')) + return value + + @buffer_size.setter + def buffer_size(self, size): + if size is None: + fun = self.__get_fun('ResetBuf{}BufSize') + err = fun() + else: + fun = self.__get_fun('SetBuf{}BufSize') + err = fun(size) + + @Feat() + def max(self): + """Maximum value value. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}Max') + err, value = fun(RetValue('f64')) + return value + + @max.setter + def max(self, value): + if value is None: + fun = self.__get_fun('Reset{}Max') + err = fun() + else: + fun = self.__get_fun('Set{}Max') + err = fun(value) + + @Feat() + def min(self): + """Minimum value value. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}Min') + err, value = fun(RetValue('f64')) + return value + + @min.setter + def min(self, value): + if value is None: + fun = self.__get_fun('Reset{}Min') + err = fun() + else: + fun = self.__get_fun('Set{}Min') + err = fun(value) + + @Feat() + def range_high(self): + """The upper limit of the input range of the + device. This value is in the native units of the device. On E + Series devices, for example, the native units is volts. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}RngHigh') + err, value = fun(RetValue('f64')) + return value + + @range_high.setter + def range_high(self, value): + if value is None: + fun = self.__get_fun('Reset{}RngHigh') + err = fun() + else: + fun = self.__get_fun('Set{}RngHigh') + err = fun(value) + + @Feat() + def range_low(self): + """The lower limit of the input range of the + device. This value is in the native units of the device. On E + Series devices, for example, the native units is volts. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}RngLow') + err, value = fun(RetValue('f64')) + return value + + @range_low.setter + def range_low(self, value): + if value is None: + fun = self.__get_fun('Reset{}RngLow') + err = fun() + else: + fun = self.__get_fun('Set{}RngLow') + err = fun(value) + + @Feat() + def gain(self): + """Gain factor to apply to the channel. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}Gain') + err, value = fun(RetValue('f64')) + return value + + @gain.setter + def gain(self, value): + if value is None: + fun = self.__get_fun('Reset{}Gain') + err = fun() + else: + fun = self.__get_fun('Set{}Gain') + err = fun(value) + + @Feat() + def number_of_lines(self): + """Number of digital lines in the channel. + """ + err, value = self.__get_fun('Get{}NumLines')(RetValue('u32')) + return value + + + def get_units(self, channel_name): + """ + Specifies in what units to generate voltage on the + channel. Write data to the channel in the units you select. + + Specifies in what units to generate current on the + channel. Write data to the channel is in the units you select. + + See also + -------- + set_units, reset_units + """ + channel_name = str(channel_name) + mt = self.get_measurment_type(channel_name) + channel_type = self.channel_type + if mt=='voltage': + d = int32(0) + CALL('Get%sVoltageUnits' % (channel_type), self, channel_name, ctypes.byref(d)) + units_map = {Contants.Val_Volts:'volts', + #Constants.Val_FromCustomScale:'custom_scale', + #Constants.Val_FromTEDS:'teds', + } + return units_map[d.value] + raise NotImplementedError('{} {}'.format(channel_name, mt)) + + + @Feat(values={'none': Constants.Val_None, 'once': Constants.Val_Once, + 'every_sample': Constants.Val_EverySample}) + def auto_zero_mode(self): + """When to measure ground. NI-DAQmx subtracts the + measured ground voltage from every sample. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}AutoZeroMode') + err, value = fun(RetValue('i32')) + return value + + @auto_zero_mode.setter + def auto_zero_mode(self, value): + if value is None: + fun = self.__get_fun('Reset{}AutoZeroMode') + err = fun() + else: + fun = self.__get_fun('Set{}AutoZeroMode') + err = fun(value) + + @Feat(values={'dma': Constants.Val_DMA, 'interrupts': Constants.Val_Interrupts, + 'programmed_io': Constants.Val_ProgrammedIO, + 'usb': Constants.Val_USBbulk}) + def data_transfer_mode(self): + """When to measure ground. NI-DAQmx subtracts the + measured ground voltage from every sample. + + Set to None to reset. + """ + fun = self.__get_fun('Get{}DataXferMech') + err, value = fun(RetValue('i32')) + return value + + @data_transfer_mode.setter + def data_transfer_mode(self, value): + if value is None: + fun = self.__get_fun('Reset{}DataXferMech') + err = fun() + else: + fun = self.__get_fun('Set{}DataXferMech') + err = fun(value) + + @Feat(values={True: 1, False: 0, None: None}) + def duplicate_count_prevention_enabled(self): + """Duplicate count prevention enabled. + + Set to None to Reset + """ + err, value = self.lib.GetCIDupCountPrevent(RetValue('u32')) + return value != 0 + + @duplicate_count_prevention_enabled.setter + def duplicate_count_prevention_enabled(self, value): + if value is None: + err = self.lib.ResetCIDupCountPrevent() + else: + err = self.lib.SetCIDupCountPrevent(value) + + @Feat() + def timebase_rate(self): + """Frequency of the counter timebase (Hz) + + TODO Can I put units and still None + + Set to None to reset. + See also + -------- + set_timebase_rate, reset_timebase_rate + """ + err, value = self.lib.GetCICtrTimebaseRate(RetValue('f64')) + return value + + @timebase_rate.setter + def timebase_rate(self, value): + if value is None: + err = self.lib.ResetCICtrTimebaseRate() + else: + err = self.lib.SetCICtrTimebaseRate(value) + + + @Feat(None) + def terminal_pulse (self, terminal): + """Terminal to generate pulses. + """ + self.lib.SetCOPulseTerm(terminal) + + + @Feat(None) + def terminal_count_edges(self, channel, terminal): + """Input terminal of the signal to measure. + """ + self.lib.SetCICountEdgesTerm(terminal) +""" + +DoneEventCallback_map = dict(AI=ctypes.CFUNCTYPE (int32, AnalogInputTask, int32, void_p), + AO=ctypes.CFUNCTYPE (int32, AnalogOutputTask, int32, void_p), + DI=ctypes.CFUNCTYPE (int32, DigitalInputTask, int32, void_p), + DO=ctypes.CFUNCTYPE (int32, DigitalOutputTask, int32, void_p), + CI=ctypes.CFUNCTYPE (int32, CounterInputTask, int32, void_p), + CO=ctypes.CFUNCTYPE (int32, CounterOutputTask, int32, void_p), + ) +EveryNSamplesEventCallback_map = dict(AI=ctypes.CFUNCTYPE (int32, AnalogInputTask, int32, uInt32, void_p), + AO=ctypes.CFUNCTYPE (int32, AnalogOutputTask, int32, uInt32, void_p), + DI=ctypes.CFUNCTYPE (int32, DigitalInputTask, int32, uInt32, void_p), + DO=ctypes.CFUNCTYPE (int32, DigitalOutputTask, int32, uInt32, void_p), + CI=ctypes.CFUNCTYPE (int32, CounterInputTask, int32, uInt32, void_p), + CO=ctypes.CFUNCTYPE (int32, CounterOutputTask, int32, uInt32, void_p), + ) +SignalEventCallback_map = dict(AI=ctypes.CFUNCTYPE (int32, AnalogInputTask, int32, void_p), + AO=ctypes.CFUNCTYPE (int32, AnalogOutputTask, int32, void_p), + DI=ctypes.CFUNCTYPE (int32, DigitalInputTask, int32, void_p), + DO=ctypes.CFUNCTYPE (int32, DigitalOutputTask, int32, void_p), + CI=ctypes.CFUNCTYPE (int32, CounterInputTask, int32, void_p), + CO=ctypes.CFUNCTYPE (int32, CounterOutputTask, int32, void_p), + ) + +""" + +if __name__ == '__main__': + + import lantz.log + + lantz.log.log_to_screen(lantz.log.DEBUG) + + inst = System() + print(inst.version) + print(inst.device_names) diff --git a/NI/NI64/channels.py b/NI/NI64/channels.py new file mode 100644 index 0000000..3a19073 --- /dev/null +++ b/NI/NI64/channels.py @@ -0,0 +1,741 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.ni.daqmx.channels + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation of specialized channel classes. + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from .base import Channel, Task +from .constants import Constants + + +class VoltageInputChannel(Channel): + """Creates channel(s) to measure voltage and adds the channel(s) + to the task you specify with taskHandle. + + If your measurement requires the use of internal excitation or you need + the voltage to be scaled by excitation, call DAQmxCreateAIVoltageChanWithExcit. + + :param phys_channel: The names of the physical channels to use + to create virtual channels. You can specify + a list or range of physical channels. + :param channel_name: The name(s) to assign to the created virtual channel(s). + If you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + :param terminal: {'default', 'rse', 'nrse', 'diff', 'pseudodiff'} + The input terminal configuration for the channel: + + 'default' + At run time, NI-DAQmx chooses the default terminal + configuration for the channel. + + 'rse' + Referenced single-ended mode + + 'nrse' + Nonreferenced single-ended mode + + 'diff' + Differential mode + + 'pseudodiff' + Pseudodifferential mode + + :param min_val: The minimum value, in units, that you expect to measure. + :param max_val: The maximum value, in units, that you expect to measure. + :param units: units to use to return the voltage measurements + """ + + IO_TYPE = 'AI' + + CREATE_FUN = 'CreateAIVoltageChan' + + terminal_map = dict (default = Constants.Val_Cfg_Default, + rse = Constants.Val_RSE, + nrse = Constants.Val_NRSE, + diff = Constants.Val_Diff, + pseudodiff = Constants.Val_PseudoDiff) + + def __init__(self, phys_channel, name='', terminal='default', + min_max=(-10., 10.), units='volts', task=None): + + if not name: + name = ''#phys_channel + + terminal_val = self.terminal_map[terminal] + + if units != 'volts': + custom_scale_name = units + units = Constants.Val_FromCustomScale + else: + custom_scale_name = None + units = Constants.Val_Volts + + self._create_args = (phys_channel, name, terminal_val, + min_max[0], min_max[1], units, custom_scale_name) + + super().__init__(task=task, name=name) + + +class VoltageOutputChannel(Channel): + """Creates channel(s) to generate voltage and adds the channel(s) + to the task you specify with taskHandle. + + See VoltageOutputChannel + """ + + CHANNEL_TYPE = 'AO' + + def __init__(self, phys_channel, channel_name='', terminal='default', min_max=(-1, -1), units='volts'): + + terminal_val = self.terminal_map[terminal] + + if units != 'volts': + custom_scale_name = units + units = Constants.FROM_CUSTOM_SCALE + else: + custom_scale_name = None + units = Constants.VOLTS + + err = self.lib.CreateAOVoltageChan(phys_channel, channel_name, + min_max[0], min_max[1], units, custom_scale_name) + +# Not implemented: +# DAQmxCreateAIAccelChan, DAQmxCreateAICurrentChan, DAQmxCreateAIFreqVoltageChan, +# DAQmxCreateAIMicrophoneChan, DAQmxCreateAIResistanceChan, DAQmxCreateAIRTDChan, +# DAQmxCreateAIStrainGageChan, DAQmxCreateAITempBuiltInSensorChan, +# DAQmxCreateAIThrmcplChan, DAQmxCreateAIThrmstrChanIex, DAQmxCreateAIThrmstrChanVex, +# DAQmxCreateAIVoltageChanWithExcit +# DAQmxCreateAIPosLVDTChan, DAQmxCreateAIPosRVDTChan/ +# DAQmxCreateTEDSAI* + +# Not implemented: DAQmxCreateAOCurrentChan +# DAQmxCreateDIChan, DAQmxCreateDOChan +# DAQmxCreateCI*, DAQmxCreateCO* + + +class DigitalInputChannel(Channel): + """ + Creates channel(s) to measure digital signals and adds the + channel(s) to the task you specify with taskHandle. You can + group digital lines into one digital channel or separate them + into multiple digital channels. If you specify one or more + entire ports in lines by using port physical channel names, + you cannot separate the ports into multiple channels. To + separate ports into multiple channels, use this function + multiple times with a different port each time. + + Parameters + ---------- + + lines : str + + The names of the digital lines used to create a virtual + channel. You can specify a list or range of lines. + + name : str + + The name of the created virtual channel(s). If you create + multiple virtual channels with one call to this function, + you can specify a list of names separated by commas. If you + do not specify a name, NI-DAQmx uses the physical channel + name as the virtual channel name. If you specify your own + names for name, you must use the names when you refer to + these channels in other NI-DAQmx functions. + + group_by : {'line', 'all_lines'} + + Specifies whether to group digital lines into one or more + virtual channels. If you specify one or more entire ports in + lines, you must set grouping to 'for_all_lines': + + 'line' - One channel for each line + + 'all_lines' - One channel for all lines + """ + + + def __init__(self, lines, name='', group_by='line'): + + if group_by == 'line': + grouping_val = Constants.ChanPerLine + self.one_channel_for_all_lines = False + else: + grouping_val = Constants.ChanForAllLines + self.one_channel_for_all_lines = True + + self.lib.CreateDIChan(lines, name, grouping_val) + + +class DigitalOutputChannel(Channel): + """ + Creates channel(s) to generate digital signals and adds the + channel(s) to the task you specify with taskHandle. You can + group digital lines into one digital channel or separate them + into multiple digital channels. If you specify one or more + entire ports in lines by using port physical channel names, + you cannot separate the ports into multiple channels. To + separate ports into multiple channels, use this function + multiple times with a different port each time. + + See DigitalInputChannel + """ + + def __init__(self, lines, name='', group_by='line'): + + if group_by == 'line': + grouping_val = Constants.ChanPerLine + self.one_channel_for_all_lines = False + else: + grouping_val = Constants.ChanForAllLines + self.one_channel_for_all_lines = True + + self.lib.CreateDOChan(lines, name, grouping_val) + + +class CountEdgesChannel(Channel): + """ + Creates a channel to count the number of rising or falling + edges of a digital signal and adds the channel to the task you + specify with taskHandle. You can create only one counter input + channel at a time with this function because a task can + include only one counter input channel. To read from multiple + counters simultaneously, use a separate task for each + counter. Connect the input signal to the default input + terminal of the counter unless you select a different input + terminal. + + Parameters + ---------- + + counter : str + + The name of the counter to use to create virtual channels. + + name : str + + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + edge : {'rising', 'falling'} + + Specifies on which edges of the input signal to increment or + decrement the count, rising or falling edge(s). + + init : int + + The value from which to start counting. + + direction : {'up', 'down', 'ext'} + + Specifies whether to increment or decrement the + counter on each edge: + + 'up' - Increment the count register on each edge. + + 'down' - Decrement the count register on each edge. + + 'ext' - The state of a digital line controls the count + direction. Each counter has a default count direction + terminal. + """ + + CHANNEL_TYPE = 'CI' + + + def __init__ (self, counter, name="", edge='rising', init=0, direction='up'): + + if edge == 'rising': + edge_val = Constants.RISING + else: + edge_val = Constants.FALLING + + if direction == 'up': + direction_val = Constants.COUNT_UP + else: + direction_val = Constants.COUNT_DOWN + + self.lib.CreateCICountEdgesChan(counter, name, edge_val, direction_val) + + +class LinearEncoderChannel(Channel): + """ + Creates a channel that uses a linear encoder to measure linear position. + You can create only one counter input channel at a time with this function + because a task can include only one counter input channel. To read from + multiple counters simultaneously, use a separate task for each counter. + Connect the input signals to the default input terminals of the counter + unless you select different input terminals. + + Parameters + ---------- + + counter : str + + The name of the counter to use to create virtual channels. + + name : str + + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + decodingType : {'X1', 'X2', 'X4', 'TwoPulseCounting'} + + Specifies how to count and interpret the pulses that the encoder + generates on signal A and signal B. X1, X2, and X4 are valid for + quadrature encoders only. TwoPulseCounting is valid only for + two-pulse encoders. + + X2 and X4 decoding are more sensitive to smaller changes in position + than X1 encoding, with X4 being the most sensitive. However, more + sensitive decoding is more likely to produce erroneous measurements + if there is vibration in the encoder or other noise in the signals. + + ZidxEnable : bool + + Specifies whether to enable z indexing for the measurement. + + ZidxVal : float + + The value, in units, to which to reset the measurement when signal Z + is high and signal A and signal B are at the states you specify with + ZidxPhase. + + ZidxPhase : {'AHighBHigh', 'AHighBLow', 'ALowBHigh', 'ALowBLow'} + + The states at which signal A and signal B must be while signal Z is high + for NI-DAQmx to reset the measurement. If signal Z is never high while + the signal A and signal B are high, for example, you must choose a phase + other than Constants.Val_AHighBHigh. + + When signal Z goes high and how long it stays high varies from encoder to + encoder. Refer to the documentation for the encoder to determine the + timing of signal Z with respect to signal A and signal B. + + units : {'Meters', 'Inches', 'Ticks', 'FromCustomScale'} + + The units to use to return linear position measurements from the channel. + + distPerPulse : float + + The distance measured for each pulse the encoder generates. Specify this + value in units. + + init : float + + The position of the encoder when the measurement begins. This value is + in units. + + customScaleName : str + + The name of a custom scale to apply to the channel. To use this parameter, + you must set units to Constants.Val_FromCustomScale. If you do not set units + to FromCustomScale, you must set customScaleName to NULL. + """ + + def __init__( + self, + counter, + name="", + decodingType='X1', + ZidxEnable=False, + ZidxVal=0.0, + ZidxPhase='AHighBHigh', + units='Ticks', + distPerPulse=1.0, + init=0.0, + customScaleName=None + ): + counter = str(counter) + name = str(name) + + decodingType_map = dict(X1=Constants.Val_X1, X2=Constants.Val_X2, X4=Constants.Val_X4, + TwoPulseCounting=Constants.Val_TwoPulseCounting) + ZidxPhase_map = dict(AHighBHigh=Constants.Val_AHighBHigh, AHighBLow=Constants.Val_AHighBLow, + ALowBHigh=Constants.Val_ALowBHigh, ALowBLow=Constants.Val_ALowBLow) + units_map = dict(Meters=Constants.Val_Meters, Inches=Constants.Val_Inches, + Ticks=Constants.Val_Ticks, FromCustomScale=Constants.Val_FromCustomScale) + + decodingType_val = self._get_map_value ('decodingType', decodingType_map, decodingType) + ZidxPhase_val = self._get_map_value ('ZidxPhase', ZidxPhase_map, ZidxPhase) + units_val = self._get_map_value ('units', units_map, units) + + if units_val != Constants.Val_FromCustomScale: + customScaleName = None + + CALL( + 'CreateCILinEncoderChan', + self, + counter, + name, + decodingType_val, + bool32(ZidxEnable), + float64(ZidxVal), + ZidxPhase_val, + units_val, + float64(distPerPulse), + float64(init), + customScaleName + )==0 + + +class MeasureFrequencyChannel(Channel): + """ + Creates a channel to measure the frequency of a digital signal + and adds the channel to the task. You can create only one + counter input channel at a time with this function because a + task can include only one counter input channel. To read from + multiple counters simultaneously, use a separate task for each + counter. Connect the input signal to the default input + terminal of the counter unless you select a different input + terminal. + + Parameters + ---------- + + counter : str + The name of the counter to use to create virtual channels. + + name : str + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + min_val : float + The minimum value, in units, that you expect to measure. + + max_val : float + The maximum value, in units, that you expect to measure. + + units : {'hertz', 'ticks', 'custom'} + Units to use to return the measurement and to specify the + min/max expected value. + + 'hertz' - Hertz, cycles per second + 'ticks' - timebase ticks + 'custom' - use custom_scale_name to specify units + + edge : {'rising', 'falling'} + Specifies which edges to measure the frequency or period of the signal. + + method : {'low_freq', 'high_freq', 'large_range'} + The method used to calculate the period or frequency of the + signal. See the M series DAQ User Manual (371022K-01), page + 7-9 for more information. + + 'low_freq' + Use one counter that uses a constant timebase to measure + the input signal. + + 'high_freq' + Use two counters, one of which counts pulses of the + signal to measure during the specified measurement time. + + 'large_range' + Use one counter to divide the frequency of the input + signal to create a lower frequency signal that the + second counter can more easily measure. + + meas_time : float + The length of time to measure the frequency or period of the + signal, when meas_method is 'high_freq'. Measurement accuracy + increases with increased meas_time and with increased signal + frequency. Ensure that the meas_time is low enough to prevent + the counter register from overflowing. + + divisor : int + The value by which to divide the input signal, when + meas_method is 'large_range'. The larger this value, the more + accurate the measurement, but too large a value can cause the + count register to roll over, resulting in an incorrect + measurement. + + custom_scale_name : str + The name of a custom scale to apply to the channel. To use + this parameter, you must set units to 'custom'. If you do + not set units to 'custom', you must set custom_scale_name to + None. + """ + + def __init__(self, counter, name='', min_val=1e2, max_val=1e3, + units="hertz", edge="rising", method="low_freq", + meas_time=1.0, divisor=1, custom_scale_name=None): + + self.data_type = float + + assert divisor > 0 + + if method == 'low_freq': + meas_meth_val = Constants.LOW_FREQ1_CTR + elif method == 'high_freq': + meas_meth_val = Constants.HIGH_FREQ2_CTR + elif method == 'large_range': + meas_meth_val = Constants.LARGE_RANGE2_CTR + + + if units != ('hertz', 'ticks'): + custom_scale_name = units + units = Constants.FROM_CUSTOM_SCALE + else: + custom_scale_name = None + if units == 'hertz': + units = Constants.HZ + else: + units = Contstants.TICKS + + self.lib.CreateCIFreqChan(counter, name, min_max[0], min_max[1], + units_val, edge_val, meas_meth_val, + meas_time, divisor, custom_scale_name) + + + +def create_channel_frequency(self, counter, name="", units='hertz', idle_state='low', + delay=0.0, freq=1.0, duty_cycle=0.5): + """ + Creates channel(s) to generate digital pulses that freq and + duty_cycle define and adds the channel to the task. The + pulses appear on the default output terminal of the counter + unless you select a different output terminal. + + Parameters + ---------- + + counter : str + + The name of the counter to use to create virtual + channels. You can specify a list or range of physical + channels. + + name : str + + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + units : {'hertz'} + + The units in which to specify freq: + + 'hertz' - hertz + + idle_state : {'low', 'high'} + + The resting state of the output terminal. + + delay : float + + The amount of time in seconds to wait before generating the + first pulse. + + freq : float + + The frequency at which to generate pulses. + + duty_cycle : float + + The width of the pulse divided by the pulse period. NI-DAQmx + uses this ratio, combined with frequency, to determine pulse + width and the interval between pulses. + + Returns + ------- + + success_status : bool + """ + counter = str(counter) + name = str(name) + units_map = dict (hertz = Constants.Val_Hz) + idle_state_map = dict (low=Constants.Val_Low, high=Constants.Val_High) + units_val = self._get_map_value('units', units_map, units) + idle_state_val = self._get_map_value('idle_state', idle_state_map, idle_state) + self.lib.CreateCOPulseChanFreq(counter, name, units_val, idle_state_val, + delay, freq, (duty_cycle)) + +def create_channel_ticks(self, counter, name="", source="", idle_state='low', + delay = 0, low_ticks=1, high_ticks=1): + """ + Creates channel(s) to generate digital pulses defined by the + number of timebase ticks that the pulse is at a high state and + the number of timebase ticks that the pulse is at a low state + and also adds the channel to the task. The pulses appear on + the default output terminal of the counter unless you select a + different output terminal. + + Parameters + ---------- + + counter : str + + The name of the counter to use to create virtual + channels. You can specify a list or range of physical + channels. + + name : str + + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + source : str + + The terminal to which you connect an external timebase. You + also can specify a source terminal by using a terminal name. + + idle_state : {'low', 'high'} + + The resting state of the output terminal. + + delay : int + + The number of timebase ticks to wait before generating the + first pulse. + + low_ticks : int + + The number of timebase ticks that the pulse is low. + + high_ticks : int + + The number of timebase ticks that the pulse is high. + + Returns + ------- + + success_status : bool + """ + counter = str(counter) + name = str(name) + idle_state_map = dict (low=Constants.Val_Low, high=Constants.Val_High) + idle_state_val = self._get_map_value('idle_state', idle_state_map, idle_state) + return CALL('CreateCOPulseChanTicks', self, counter, name, source, idle_state_val, + int32 (delay), int32 (low_ticks), int32 (high_ticks))==0 + +def create_channel_time(self, counter, name="", units="seconds", idle_state='low', + delay = 0, low_time=1, high_time=1): + """ + Creates channel(s) to generate digital pulses defined by the + number of timebase ticks that the pulse is at a high state and + the number of timebase ticks that the pulse is at a low state + and also adds the channel to the task. The pulses appear on + the default output terminal of the counter unless you select a + different output terminal. + + Parameters + ---------- + + counter : str + + The name of the counter to use to create virtual + channels. You can specify a list or range of physical + channels. + + name : str + + The name(s) to assign to the created virtual channel(s). If + you do not specify a name, NI-DAQmx uses the physical + channel name as the virtual channel name. If you specify + your own names for nameToAssignToChannel, you must use the + names when you refer to these channels in other NI-DAQmx + functions. + + If you create multiple virtual channels with one call to + this function, you can specify a list of names separated by + commas. If you provide fewer names than the number of + virtual channels you create, NI-DAQmx automatically assigns + names to the virtual channels. + + units : {'seconds'} + + The units in which to specify high and low time. + + idle_state : {'low', 'high'} + + The resting state of the output terminal. + + delay : float + + The amount of time in seconds to wait before generating the + first pulse. + + low_time : float + + The amount of time the pulse is low, in seconds. + + high_time : float + + The amount of time the pulse is high, in seconds. + + Returns + ------- + + success_status : bool + """ + counter = str(counter) + name = str(name) + units_map = dict (seconds = Constants.Val_Seconds) + idle_state_map = dict (low=Constants.Val_Low, high=Constants.Val_High) + units_val = self._get_map_value('units', units_map, units) + idle_state_val = self._get_map_value('idle_state', idle_state_map, idle_state) + return CALL('CreateCOPulseChanTime', self, counter, name, units_val, idle_state_val, + float64 (delay), float64(low_time), float64(high_time))==0 diff --git a/NI/NI64/constants.py b/NI/NI64/constants.py new file mode 100644 index 0000000..3fa2c11 --- /dev/null +++ b/NI/NI64/constants.py @@ -0,0 +1,1541 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.ni.daqmx.constants + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Constants and types for DAQmx + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from lantz.drivers.legacy.visalib import RichEnum + +class Constants(metaclass=RichEnum): + + _PREFIX = 'DAQMX_' + + Buf_Input_BufSize = 0x186C + Buf_Input_OnbrdBufSize = 0x230A + Buf_Output_BufSize = 0x186D + Buf_Output_OnbrdBufSize = 0x230B + SelfCal_Supported = 0x1860 + SelfCal_LastTemp = 0x1864 + ExtCal_RecommendedInterval = 0x1868 + ExtCal_LastTemp = 0x1867 + Cal_UserDefinedInfo = 0x1861 + Cal_UserDefinedInfo_MaxSize = 0x191C + Cal_DevTemp = 0x223B + Cal_AccConnectionCount = 0x2FEB + Cal_RecommendedAccConnectionCountLimit = 0x2FEC + + AI_Max = 0x17DD + AI_Min = 0x17DE + AI_CustomScaleName = 0x17E0 + AI_MeasType = 0x0695 + AI_Voltage_Units = 0x1094 + AI_Voltage_dBRef = 0x29B0 + AI_Voltage_ACRMS_Units = 0x17E2 + AI_Temp_Units = 0x1033 + AI_Thrmcpl_Type = 0x1050 + AI_Thrmcpl_ScaleType = 0x29D0 + AI_Thrmcpl_CJCSrc = 0x1035 + AI_Thrmcpl_CJCVal = 0x1036 + AI_Thrmcpl_CJCChan = 0x1034 + AI_RTD_Type = 0x1032 + AI_RTD_R0 = 0x1030 + AI_RTD_A = 0x1010 + AI_RTD_B = 0x1011 + AI_RTD_C = 0x1013 + AI_Thrmstr_A = 0x18C9 + AI_Thrmstr_B = 0x18CB + AI_Thrmstr_C = 0x18CA + AI_Thrmstr_R1 = 0x1061 + AI_ForceReadFromChan = 0x18F8 + AI_Current_Units = 0x0701 + AI_Current_ACRMS_Units = 0x17E3 + AI_Strain_Units = 0x0981 + AI_StrainGage_GageFactor = 0x0994 + AI_StrainGage_PoissonRatio = 0x0998 + AI_StrainGage_Cfg = 0x0982 + AI_Resistance_Units = 0x0955 + AI_Freq_Units = 0x0806 + AI_Freq_ThreshVoltage = 0x0815 + AI_Freq_Hyst = 0x0814 + AI_LVDT_Units = 0x0910 + AI_LVDT_Sensitivity = 0x0939 + AI_LVDT_SensitivityUnits = 0x219A + AI_RVDT_Units = 0x0877 + AI_RVDT_Sensitivity = 0x0903 + AI_RVDT_SensitivityUnits = 0x219B + AI_EddyCurrentProxProbe_Units = 0x2AC0 + AI_EddyCurrentProxProbe_Sensitivity = 0x2ABE + AI_EddyCurrentProxProbe_SensitivityUnits = 0x2ABF + AI_SoundPressure_MaxSoundPressureLvl = 0x223A + AI_SoundPressure_Units = 0x1528 + AI_SoundPressure_dBRef = 0x29B1 + AI_Microphone_Sensitivity = 0x1536 + AI_Accel_Units = 0x0673 + AI_Accel_dBRef = 0x29B2 + AI_Accel_Sensitivity = 0x0692 + AI_Accel_SensitivityUnits = 0x219C + AI_Force_Units = 0x2F75 + AI_Force_IEPESensor_Sensitivity = 0x2F81 + AI_Force_IEPESensor_SensitivityUnits = 0x2F82 + AI_Pressure_Units = 0x2F76 + AI_Torque_Units = 0x2F77 + AI_Bridge_Units = 0x2F92 + AI_Bridge_ElectricalUnits = 0x2F87 + AI_Bridge_PhysicalUnits = 0x2F88 + AI_Bridge_ScaleType = 0x2F89 + AI_Bridge_TwoPointLin_First_ElectricalVal = 0x2F8A + AI_Bridge_TwoPointLin_First_PhysicalVal = 0x2F8B + AI_Bridge_TwoPointLin_Second_ElectricalVal = 0x2F8C + AI_Bridge_TwoPointLin_Second_PhysicalVal = 0x2F8D + AI_Bridge_Table_ElectricalVals = 0x2F8E + AI_Bridge_Table_PhysicalVals = 0x2F8F + AI_Bridge_Poly_ForwardCoeff = 0x2F90 + AI_Bridge_Poly_ReverseCoeff = 0x2F91 + AI_Is_TEDS = 0x2983 + AI_TEDS_Units = 0x21E0 + AI_Coupling = 0x0064 + AI_Impedance = 0x0062 + AI_TermCfg = 0x1097 + AI_InputSrc = 0x2198 + AI_ResistanceCfg = 0x1881 + AI_LeadWireResistance = 0x17EE + AI_Bridge_Cfg = 0x0087 + AI_Bridge_NomResistance = 0x17EC + AI_Bridge_InitialVoltage = 0x17ED + AI_Bridge_InitialRatio = 0x2F86 + AI_Bridge_ShuntCal_Enable = 0x0094 + AI_Bridge_ShuntCal_Select = 0x21D5 + AI_Bridge_ShuntCal_GainAdjust = 0x193F + AI_Bridge_ShuntCal_ShuntCalAResistance = 0x2F78 + AI_Bridge_ShuntCal_ShuntCalAActualResistance = 0x2F79 + AI_Bridge_Balance_CoarsePot = 0x17F1 + AI_Bridge_Balance_FinePot = 0x18F4 + AI_CurrentShunt_Loc = 0x17F2 + AI_CurrentShunt_Resistance = 0x17F3 + AI_Excit_Src = 0x17F4 + AI_Excit_Val = 0x17F5 + AI_Excit_UseForScaling = 0x17FC + AI_Excit_UseMultiplexed = 0x2180 + AI_Excit_ActualVal = 0x1883 + AI_Excit_DCorAC = 0x17FB + AI_Excit_VoltageOrCurrent = 0x17F6 + AI_ACExcit_Freq = 0x0101 + AI_ACExcit_SyncEnable = 0x0102 + AI_ACExcit_WireMode = 0x18CD + AI_OpenThrmcplDetectEnable = 0x2F72 + AI_Thrmcpl_LeadOffsetVoltage = 0x2FB8 + AI_Atten = 0x1801 + AI_ProbeAtten = 0x2A88 + AI_Lowpass_Enable = 0x1802 + AI_Lowpass_CutoffFreq = 0x1803 + AI_Lowpass_SwitchCap_ClkSrc = 0x1884 + AI_Lowpass_SwitchCap_ExtClkFreq = 0x1885 + AI_Lowpass_SwitchCap_ExtClkDiv = 0x1886 + AI_Lowpass_SwitchCap_OutClkDiv = 0x1887 + AI_FilterDelay = 0x2FED + AI_RemoveFilterDelay = 0x2FBD + AI_AveragingWinSize = 0x2FEE + AI_ResolutionUnits = 0x1764 + AI_Resolution = 0x1765 + AI_RawSampSize = 0x22DA + AI_RawSampJustification = 0x0050 + AI_ADCTimingMode = 0x29F9 + AI_ADCCustomTimingMode = 0x2F6B + AI_Dither_Enable = 0x0068 + AI_ChanCal_HasValidCalInfo = 0x2297 + AI_ChanCal_EnableCal = 0x2298 + AI_ChanCal_ApplyCalIfExp = 0x2299 + AI_ChanCal_ScaleType = 0x229C + AI_ChanCal_Table_PreScaledVals = 0x229D + AI_ChanCal_Table_ScaledVals = 0x229E + AI_ChanCal_Poly_ForwardCoeff = 0x229F + AI_ChanCal_Poly_ReverseCoeff = 0x22A0 + AI_ChanCal_OperatorName = 0x22A3 + AI_ChanCal_Desc = 0x22A4 + AI_ChanCal_Verif_RefVals = 0x22A1 + AI_ChanCal_Verif_AcqVals = 0x22A2 + AI_Rng_High = 0x1815 + AI_Rng_Low = 0x1816 + AI_DCOffset = 0x2A89 + AI_Gain = 0x1818 + AI_SampAndHold_Enable = 0x181A + AI_AutoZeroMode = 0x1760 + AI_DataXferMech = 0x1821 + AI_DataXferReqCond = 0x188B + AI_DataXferCustomThreshold = 0x230C + AI_UsbXferReqSize = 0x2A8E + AI_MemMapEnable = 0x188C + AI_RawDataCompressionType = 0x22D8 + AI_LossyLSBRemoval_CompressedSampSize = 0x22D9 + AI_DevScalingCoeff = 0x1930 + AI_EnhancedAliasRejectionEnable = 0x2294 + AO_Max = 0x1186 + AO_Min = 0x1187 + AO_CustomScaleName = 0x1188 + AO_OutputType = 0x1108 + AO_Voltage_Units = 0x1184 + AO_Voltage_CurrentLimit = 0x2A1D + AO_Current_Units = 0x1109 + AO_FuncGen_Type = 0x2A18 + AO_FuncGen_Freq = 0x2A19 + AO_FuncGen_Amplitude = 0x2A1A + AO_FuncGen_Offset = 0x2A1B + AO_FuncGen_Square_DutyCycle = 0x2A1C + AO_FuncGen_ModulationType = 0x2A22 + AO_FuncGen_FMDeviation = 0x2A23 + AO_OutputImpedance = 0x1490 + AO_LoadImpedance = 0x0121 + AO_IdleOutputBehavior = 0x2240 + AO_TermCfg = 0x188E + AO_ResolutionUnits = 0x182B + AO_Resolution = 0x182C + AO_DAC_Rng_High = 0x182E + AO_DAC_Rng_Low = 0x182D + AO_DAC_Ref_ConnToGnd = 0x0130 + AO_DAC_Ref_AllowConnToGnd = 0x1830 + AO_DAC_Ref_Src = 0x0132 + AO_DAC_Ref_ExtSrc = 0x2252 + AO_DAC_Ref_Val = 0x1832 + AO_DAC_Offset_Src = 0x2253 + AO_DAC_Offset_ExtSrc = 0x2254 + AO_DAC_Offset_Val = 0x2255 + AO_ReglitchEnable = 0x0133 + AO_Gain = 0x0118 + AO_UseOnlyOnBrdMem = 0x183A + AO_DataXferMech = 0x0134 + AO_DataXferReqCond = 0x183C + AO_UsbXferReqSize = 0x2A8F + AO_MemMapEnable = 0x188F + AO_DevScalingCoeff = 0x1931 + AO_EnhancedImageRejectionEnable = 0x2241 + + DI_InvertLines = 0x0793 + DI_NumLines = 0x2178 + DI_DigFltr_Enable = 0x21D6 + DI_DigFltr_MinPulseWidth = 0x21D7 + DI_DigFltr_EnableBusMode = 0x2EFE + DI_DigFltr_TimebaseSrc = 0x2ED4 + DI_DigFltr_TimebaseRate = 0x2ED5 + DI_DigSync_Enable = 0x2ED6 + DI_Tristate = 0x1890 + DI_LogicFamily = 0x296D + DI_DataXferMech = 0x2263 + DI_DataXferReqCond = 0x2264 + DI_UsbXferReqSize = 0x2A90 + DI_MemMapEnable = 0x296A + DI_AcquireOn = 0x2966 + DO_OutputDriveType = 0x1137 + DO_InvertLines = 0x1133 + DO_NumLines = 0x2179 + DO_Tristate = 0x18F3 + DO_LineStates_StartState = 0x2972 + DO_LineStates_PausedState = 0x2967 + DO_LineStates_DoneState = 0x2968 + DO_LogicFamily = 0x296E + DO_Overcurrent_Limit = 0x2A85 + DO_Overcurrent_AutoReenable = 0x2A86 + DO_Overcurrent_ReenablePeriod = 0x2A87 + DO_UseOnlyOnBrdMem = 0x2265 + DO_DataXferMech = 0x2266 + DO_DataXferReqCond = 0x2267 + DO_UsbXferReqSize = 0x2A91 + DO_MemMapEnable = 0x296B + DO_GenerateOn = 0x2969 + + CI_Max = 0x189C + CI_Min = 0x189D + CI_CustomScaleName = 0x189E + CI_MeasType = 0x18A0 + CI_Freq_Units = 0x18A1 + CI_Freq_Term = 0x18A2 + CI_Freq_StartingEdge = 0x0799 + CI_Freq_MeasMeth = 0x0144 + CI_Freq_EnableAveraging = 0x2ED0 + CI_Freq_MeasTime = 0x0145 + CI_Freq_Div = 0x0147 + CI_Freq_DigFltr_Enable = 0x21E7 + CI_Freq_DigFltr_MinPulseWidth = 0x21E8 + CI_Freq_DigFltr_TimebaseSrc = 0x21E9 + CI_Freq_DigFltr_TimebaseRate = 0x21EA + CI_Freq_DigSync_Enable = 0x21EB + CI_Period_Units = 0x18A3 + CI_Period_Term = 0x18A4 + CI_Period_StartingEdge = 0x0852 + CI_Period_MeasMeth = 0x192C + CI_Period_EnableAveraging = 0x2ED1 + CI_Period_MeasTime = 0x192D + CI_Period_Div = 0x192E + CI_Period_DigFltr_Enable = 0x21EC + CI_Period_DigFltr_MinPulseWidth = 0x21ED + CI_Period_DigFltr_TimebaseSrc = 0x21EE + CI_Period_DigFltr_TimebaseRate = 0x21EF + CI_Period_DigSync_Enable = 0x21F0 + CI_CountEdges_Term = 0x18C7 + CI_CountEdges_Dir = 0x0696 + CI_CountEdges_DirTerm = 0x21E1 + CI_CountEdges_CountDir_DigFltr_Enable = 0x21F1 + CI_CountEdges_CountDir_DigFltr_MinPulseWidth = 0x21F2 + CI_CountEdges_CountDir_DigFltr_TimebaseSrc = 0x21F3 + CI_CountEdges_CountDir_DigFltr_TimebaseRate = 0x21F4 + CI_CountEdges_CountDir_DigSync_Enable = 0x21F5 + CI_CountEdges_InitialCnt = 0x0698 + CI_CountEdges_ActiveEdge = 0x0697 + CI_CountEdges_CountReset_Enable = 0x2FAF + CI_CountEdges_CountReset_ResetCount = 0x2FB0 + CI_CountEdges_CountReset_Term = 0x2FB1 + CI_CountEdges_CountReset_ActiveEdge = 0x2FB2 + CI_CountEdges_CountReset_DigFltr_Enable = 0x2FB3 + CI_CountEdges_CountReset_DigFltr_MinPulseWidth = 0x2FB4 + CI_CountEdges_CountReset_DigFltr_TimebaseSrc = 0x2FB5 + CI_CountEdges_CountReset_DigFltr_TimebaseRate = 0x2FB6 + CI_CountEdges_CountReset_DigSync_Enable = 0x2FB7 + CI_CountEdges_DigFltr_Enable = 0x21F6 + CI_CountEdges_DigFltr_MinPulseWidth = 0x21F7 + CI_CountEdges_DigFltr_TimebaseSrc = 0x21F8 + CI_CountEdges_DigFltr_TimebaseRate = 0x21F9 + CI_CountEdges_DigSync_Enable = 0x21FA + CI_AngEncoder_Units = 0x18A6 + CI_AngEncoder_PulsesPerRev = 0x0875 + CI_AngEncoder_InitialAngle = 0x0881 + CI_LinEncoder_Units = 0x18A9 + CI_LinEncoder_DistPerPulse = 0x0911 + CI_LinEncoder_InitialPos = 0x0915 + CI_Encoder_DecodingType = 0x21E6 + CI_Encoder_AInputTerm = 0x219D + CI_Encoder_AInput_DigFltr_Enable = 0x21FB + CI_Encoder_AInput_DigFltr_MinPulseWidth = 0x21FC + CI_Encoder_AInput_DigFltr_TimebaseSrc = 0x21FD + CI_Encoder_AInput_DigFltr_TimebaseRate = 0x21FE + CI_Encoder_AInput_DigSync_Enable = 0x21FF + CI_Encoder_BInputTerm = 0x219E + CI_Encoder_BInput_DigFltr_Enable = 0x2200 + CI_Encoder_BInput_DigFltr_MinPulseWidth = 0x2201 + CI_Encoder_BInput_DigFltr_TimebaseSrc = 0x2202 + CI_Encoder_BInput_DigFltr_TimebaseRate = 0x2203 + CI_Encoder_BInput_DigSync_Enable = 0x2204 + CI_Encoder_ZInputTerm = 0x219F + CI_Encoder_ZInput_DigFltr_Enable = 0x2205 + CI_Encoder_ZInput_DigFltr_MinPulseWidth = 0x2206 + CI_Encoder_ZInput_DigFltr_TimebaseSrc = 0x2207 + CI_Encoder_ZInput_DigFltr_TimebaseRate = 0x2208 + CI_Encoder_ZInput_DigSync_Enable = 0x2209 + CI_Encoder_ZIndexEnable = 0x0890 + CI_Encoder_ZIndexVal = 0x0888 + CI_Encoder_ZIndexPhase = 0x0889 + CI_PulseWidth_Units = 0x0823 + CI_PulseWidth_Term = 0x18AA + CI_PulseWidth_StartingEdge = 0x0825 + CI_PulseWidth_DigFltr_Enable = 0x220A + CI_PulseWidth_DigFltr_MinPulseWidth = 0x220B + CI_PulseWidth_DigFltr_TimebaseSrc = 0x220C + CI_PulseWidth_DigFltr_TimebaseRate = 0x220D + CI_PulseWidth_DigSync_Enable = 0x220E + CI_TwoEdgeSep_Units = 0x18AC + CI_TwoEdgeSep_FirstTerm = 0x18AD + CI_TwoEdgeSep_FirstEdge = 0x0833 + CI_TwoEdgeSep_First_DigFltr_Enable = 0x220F + CI_TwoEdgeSep_First_DigFltr_MinPulseWidth = 0x2210 + CI_TwoEdgeSep_First_DigFltr_TimebaseSrc = 0x2211 + CI_TwoEdgeSep_First_DigFltr_TimebaseRate = 0x2212 + CI_TwoEdgeSep_First_DigSync_Enable = 0x2213 + CI_TwoEdgeSep_SecondTerm = 0x18AE + CI_TwoEdgeSep_SecondEdge = 0x0834 + CI_TwoEdgeSep_Second_DigFltr_Enable = 0x2214 + CI_TwoEdgeSep_Second_DigFltr_MinPulseWidth = 0x2215 + CI_TwoEdgeSep_Second_DigFltr_TimebaseSrc = 0x2216 + CI_TwoEdgeSep_Second_DigFltr_TimebaseRate = 0x2217 + CI_TwoEdgeSep_Second_DigSync_Enable = 0x2218 + CI_SemiPeriod_Units = 0x18AF + CI_SemiPeriod_Term = 0x18B0 + CI_SemiPeriod_StartingEdge = 0x22FE + CI_SemiPeriod_DigFltr_Enable = 0x2219 + CI_SemiPeriod_DigFltr_MinPulseWidth = 0x221A + CI_SemiPeriod_DigFltr_TimebaseSrc = 0x221B + CI_SemiPeriod_DigFltr_TimebaseRate = 0x221C + CI_SemiPeriod_DigSync_Enable = 0x221D + CI_Pulse_Freq_Units = 0x2F0B + CI_Pulse_Freq_Term = 0x2F04 + CI_Pulse_Freq_Start_Edge = 0x2F05 + CI_Pulse_Freq_DigFltr_Enable = 0x2F06 + CI_Pulse_Freq_DigFltr_MinPulseWidth = 0x2F07 + CI_Pulse_Freq_DigFltr_TimebaseSrc = 0x2F08 + CI_Pulse_Freq_DigFltr_TimebaseRate = 0x2F09 + CI_Pulse_Freq_DigSync_Enable = 0x2F0A + CI_Pulse_Time_Units = 0x2F13 + CI_Pulse_Time_Term = 0x2F0C + CI_Pulse_Time_StartEdge = 0x2F0D + CI_Pulse_Time_DigFltr_Enable = 0x2F0E + CI_Pulse_Time_DigFltr_MinPulseWidth = 0x2F0F + CI_Pulse_Time_DigFltr_TimebaseSrc = 0x2F10 + CI_Pulse_Time_DigFltr_TimebaseRate = 0x2F11 + CI_Pulse_Time_DigSync_Enable = 0x2F12 + CI_Pulse_Ticks_Term = 0x2F14 + CI_Pulse_Ticks_StartEdge = 0x2F15 + CI_Pulse_Ticks_DigFltr_Enable = 0x2F16 + CI_Pulse_Ticks_DigFltr_MinPulseWidth = 0x2F17 + CI_Pulse_Ticks_DigFltr_TimebaseSrc = 0x2F18 + CI_Pulse_Ticks_DigFltr_TimebaseRate = 0x2F19 + CI_Pulse_Ticks_DigSync_Enable = 0x2F1A + CI_Timestamp_Units = 0x22B3 + CI_Timestamp_InitialSeconds = 0x22B4 + CI_GPS_SyncMethod = 0x1092 + CI_GPS_SyncSrc = 0x1093 + CI_CtrTimebaseSrc = 0x0143 + CI_CtrTimebaseRate = 0x18B2 + CI_CtrTimebaseActiveEdge = 0x0142 + CI_CtrTimebase_DigFltr_Enable = 0x2271 + CI_CtrTimebase_DigFltr_MinPulseWidth = 0x2272 + CI_CtrTimebase_DigFltr_TimebaseSrc = 0x2273 + CI_CtrTimebase_DigFltr_TimebaseRate = 0x2274 + CI_CtrTimebase_DigSync_Enable = 0x2275 + CI_Count = 0x0148 + CI_OutputState = 0x0149 + CI_TCReached = 0x0150 + CI_CtrTimebaseMasterTimebaseDiv = 0x18B3 + CI_DataXferMech = 0x0200 + CI_DataXferReqCond = 0x2EFB + CI_UsbXferReqSize = 0x2A92 + CI_MemMapEnable = 0x2ED2 + CI_NumPossiblyInvalidSamps = 0x193C + CI_DupCountPrevent = 0x21AC + CI_Prescaler = 0x2239 + + CO_OutputType = 0x18B5 + CO_Pulse_IdleState = 0x1170 + CO_Pulse_Term = 0x18E1 + CO_Pulse_Time_Units = 0x18D6 + CO_Pulse_HighTime = 0x18BA + CO_Pulse_LowTime = 0x18BB + CO_Pulse_Time_InitialDelay = 0x18BC + CO_Pulse_DutyCyc = 0x1176 + CO_Pulse_Freq_Units = 0x18D5 + CO_Pulse_Freq = 0x1178 + CO_Pulse_Freq_InitialDelay = 0x0299 + CO_Pulse_HighTicks = 0x1169 + CO_Pulse_LowTicks = 0x1171 + CO_Pulse_Ticks_InitialDelay = 0x0298 + CO_CtrTimebaseSrc = 0x0339 + CO_CtrTimebaseRate = 0x18C2 + CO_CtrTimebaseActiveEdge = 0x0341 + CO_CtrTimebase_DigFltr_Enable = 0x2276 + CO_CtrTimebase_DigFltr_MinPulseWidth = 0x2277 + CO_CtrTimebase_DigFltr_TimebaseSrc = 0x2278 + CO_CtrTimebase_DigFltr_TimebaseRate = 0x2279 + CO_CtrTimebase_DigSync_Enable = 0x227A + CO_Count = 0x0293 + CO_OutputState = 0x0294 + CO_AutoIncrCnt = 0x0295 + CO_CtrTimebaseMasterTimebaseDiv = 0x18C3 + CO_PulseDone = 0x190E + CO_EnableInitialDelayOnRetrigger = 0x2EC9 + CO_ConstrainedGenMode = 0x29F2 + CO_UseOnlyOnBrdMem = 0x2ECB + CO_DataXferMech = 0x2ECC + CO_DataXferReqCond = 0x2ECD + CO_UsbXferReqSize = 0x2A93 + CO_MemMapEnable = 0x2ED3 + CO_Prescaler = 0x226D + CO_RdyForNewVal = 0x22FF + + ChanType = 0x187F + PhysicalChanName = 0x18F5 + ChanDescr = 0x1926 + ChanIsGlobal = 0x2304 + Exported_AIConvClk_OutputTerm = 0x1687 + Exported_AIConvClk_Pulse_Polarity = 0x1688 + Exported_10MHzRefClk_OutputTerm = 0x226E + Exported_20MHzTimebase_OutputTerm = 0x1657 + Exported_SampClk_OutputBehavior = 0x186B + Exported_SampClk_OutputTerm = 0x1663 + Exported_SampClk_DelayOffset = 0x21C4 + Exported_SampClk_Pulse_Polarity = 0x1664 + Exported_SampClkTimebase_OutputTerm = 0x18F9 + Exported_DividedSampClkTimebase_OutputTerm = 0x21A1 + Exported_AdvTrig_OutputTerm = 0x1645 + Exported_AdvTrig_Pulse_Polarity = 0x1646 + Exported_AdvTrig_Pulse_WidthUnits = 0x1647 + Exported_AdvTrig_Pulse_Width = 0x1648 + Exported_PauseTrig_OutputTerm = 0x1615 + Exported_PauseTrig_Lvl_ActiveLvl = 0x1616 + Exported_RefTrig_OutputTerm = 0x0590 + Exported_RefTrig_Pulse_Polarity = 0x0591 + Exported_StartTrig_OutputTerm = 0x0584 + Exported_StartTrig_Pulse_Polarity = 0x0585 + Exported_AdvCmpltEvent_OutputTerm = 0x1651 + Exported_AdvCmpltEvent_Delay = 0x1757 + Exported_AdvCmpltEvent_Pulse_Polarity = 0x1652 + Exported_AdvCmpltEvent_Pulse_Width = 0x1654 + Exported_AIHoldCmpltEvent_OutputTerm = 0x18ED + Exported_AIHoldCmpltEvent_PulsePolarity = 0x18EE + Exported_ChangeDetectEvent_OutputTerm = 0x2197 + Exported_ChangeDetectEvent_Pulse_Polarity = 0x2303 + Exported_CtrOutEvent_OutputTerm = 0x1717 + Exported_CtrOutEvent_OutputBehavior = 0x174F + Exported_CtrOutEvent_Pulse_Polarity = 0x1718 + Exported_CtrOutEvent_Toggle_IdleState = 0x186A + Exported_HshkEvent_OutputTerm = 0x22BA + Exported_HshkEvent_OutputBehavior = 0x22BB + Exported_HshkEvent_Delay = 0x22BC + Exported_HshkEvent_Interlocked_AssertedLvl = 0x22BD + Exported_HshkEvent_Interlocked_AssertOnStart = 0x22BE + Exported_HshkEvent_Interlocked_DeassertDelay = 0x22BF + Exported_HshkEvent_Pulse_Polarity = 0x22C0 + Exported_HshkEvent_Pulse_Width = 0x22C1 + Exported_RdyForXferEvent_OutputTerm = 0x22B5 + Exported_RdyForXferEvent_Lvl_ActiveLvl = 0x22B6 + Exported_RdyForXferEvent_DeassertCond = 0x2963 + Exported_RdyForXferEvent_DeassertCondCustomThreshold = 0x2964 + Exported_DataActiveEvent_OutputTerm = 0x1633 + Exported_DataActiveEvent_Lvl_ActiveLvl = 0x1634 + Exported_RdyForStartEvent_OutputTerm = 0x1609 + Exported_RdyForStartEvent_Lvl_ActiveLvl = 0x1751 + Exported_SyncPulseEvent_OutputTerm = 0x223C + Exported_WatchdogExpiredEvent_OutputTerm = 0x21AA + + Dev_IsSimulated = 0x22CA + Dev_ProductCategory = 0x29A9 + Dev_ProductType = 0x0631 + Dev_ProductNum = 0x231D + Dev_SerialNum = 0x0632 + Dev_Accessory_ProductTypes = 0x2F6D + Dev_Accessory_ProductNums = 0x2F6E + Dev_Accessory_SerialNums = 0x2F6F + Carrier_SerialNum = 0x2A8A + Dev_Chassis_ModuleDevNames = 0x29B6 + Dev_AnlgTrigSupported = 0x2984 + Dev_DigTrigSupported = 0x2985 + Dev_AI_PhysicalChans = 0x231E + Dev_AI_SupportedMeasTypes = 0x2FD2 + Dev_AI_MaxSingleChanRate = 0x298C + Dev_AI_MaxMultiChanRate = 0x298D + Dev_AI_MinRate = 0x298E + Dev_AI_SimultaneousSamplingSupported = 0x298F + Dev_AI_SampModes = 0x2FDC + Dev_AI_TrigUsage = 0x2986 + Dev_AI_VoltageRngs = 0x2990 + Dev_AI_VoltageIntExcitDiscreteVals = 0x29C9 + Dev_AI_VoltageIntExcitRangeVals = 0x29CA + Dev_AI_CurrentRngs = 0x2991 + Dev_AI_CurrentIntExcitDiscreteVals = 0x29CB + Dev_AI_BridgeRngs = 0x2FD0 + Dev_AI_ResistanceRngs = 0x2A15 + Dev_AI_FreqRngs = 0x2992 + Dev_AI_Gains = 0x2993 + Dev_AI_Couplings = 0x2994 + Dev_AI_LowpassCutoffFreqDiscreteVals = 0x2995 + Dev_AI_LowpassCutoffFreqRangeVals = 0x29CF + Dev_AO_PhysicalChans = 0x231F + Dev_AO_SupportedOutputTypes = 0x2FD3 + Dev_AO_SampClkSupported = 0x2996 + Dev_AO_SampModes = 0x2FDD + Dev_AO_MaxRate = 0x2997 + Dev_AO_MinRate = 0x2998 + Dev_AO_TrigUsage = 0x2987 + Dev_AO_VoltageRngs = 0x299B + Dev_AO_CurrentRngs = 0x299C + Dev_AO_Gains = 0x299D + Dev_DI_Lines = 0x2320 + Dev_DI_Ports = 0x2321 + Dev_DI_MaxRate = 0x2999 + Dev_DI_TrigUsage = 0x2988 + Dev_DO_Lines = 0x2322 + Dev_DO_Ports = 0x2323 + Dev_DO_MaxRate = 0x299A + Dev_DO_TrigUsage = 0x2989 + Dev_CI_PhysicalChans = 0x2324 + Dev_CI_SupportedMeasTypes = 0x2FD4 + Dev_CI_TrigUsage = 0x298A + Dev_CI_SampClkSupported = 0x299E + Dev_CI_SampModes = 0x2FDE + Dev_CI_MaxSize = 0x299F + Dev_CI_MaxTimebase = 0x29A0 + Dev_CO_PhysicalChans = 0x2325 + Dev_CO_SupportedOutputTypes = 0x2FD5 + Dev_CO_SampClkSupported = 0x2F5B + Dev_CO_SampModes = 0x2FDF + Dev_CO_TrigUsage = 0x298B + Dev_CO_MaxSize = 0x29A1 + Dev_CO_MaxTimebase = 0x29A2 + Dev_TEDS_HWTEDSSupported = 0x2FD6 + Dev_NumDMAChans = 0x233C + Dev_BusType = 0x2326 + Dev_PCI_BusNum = 0x2327 + Dev_PCI_DevNum = 0x2328 + Dev_PXI_ChassisNum = 0x2329 + Dev_PXI_SlotNum = 0x232A + Dev_CompactDAQ_ChassisDevName = 0x29B7 + Dev_CompactDAQ_SlotNum = 0x29B8 + Dev_TCPIP_Hostname = 0x2A8B + Dev_TCPIP_EthernetIP = 0x2A8C + Dev_TCPIP_WirelessIP = 0x2A8D + Dev_Terminals = 0x2A40 + + Read_RelativeTo = 0x190A + Read_Offset = 0x190B + Read_ChannelsToRead = 0x1823 + Read_ReadAllAvailSamp = 0x1215 + Read_AutoStart = 0x1826 + Read_OverWrite = 0x1211 + Read_CurrReadPos = 0x1221 + Read_AvailSampPerChan = 0x1223 + Logging_FilePath = 0x2EC4 + Logging_Mode = 0x2EC5 + Logging_TDMS_GroupName = 0x2EC6 + Logging_TDMS_Operation = 0x2EC7 + Logging_Pause = 0x2FE3 + Logging_SampsPerFile = 0x2FE4 + Logging_FileWriteSize = 0x2FC3 + Logging_FilePreallocationSize = 0x2FC6 + Read_TotalSampPerChanAcquired = 0x192A + Read_CommonModeRangeErrorChansExist = 0x2A98 + Read_CommonModeRangeErrorChans = 0x2A99 + Read_OvercurrentChansExist = 0x29E6 + Read_OvercurrentChans = 0x29E7 + Read_OpenCurrentLoopChansExist = 0x2A09 + Read_OpenCurrentLoopChans = 0x2A0A + Read_OpenThrmcplChansExist = 0x2A96 + Read_OpenThrmcplChans = 0x2A97 + Read_OverloadedChansExist = 0x2174 + Read_OverloadedChans = 0x2175 + Read_AccessoryInsertionOrRemovalDetected = 0x2F70 + Read_DevsWithInsertedOrRemovedAccessories = 0x2F71 + Read_ChangeDetect_HasOverflowed = 0x2194 + Read_RawDataWidth = 0x217A + Read_NumChans = 0x217B + Read_DigitalLines_BytesPerChan = 0x217C + Read_WaitMode = 0x2232 + Read_SleepTime = 0x22B0 + RealTime_ConvLateErrorsToWarnings = 0x22EE + RealTime_NumOfWarmupIters = 0x22ED + RealTime_WaitForNextSampClkWaitMode = 0x22EF + RealTime_ReportMissedSamp = 0x2319 + RealTime_WriteRecoveryMode = 0x231A + SwitchChan_Usage = 0x18E4 + SwitchChan_AnlgBusSharingEnable = 0x2F9E + SwitchChan_MaxACCarryCurrent = 0x0648 + SwitchChan_MaxACSwitchCurrent = 0x0646 + SwitchChan_MaxACCarryPwr = 0x0642 + SwitchChan_MaxACSwitchPwr = 0x0644 + SwitchChan_MaxDCCarryCurrent = 0x0647 + SwitchChan_MaxDCSwitchCurrent = 0x0645 + SwitchChan_MaxDCCarryPwr = 0x0643 + SwitchChan_MaxDCSwitchPwr = 0x0649 + SwitchChan_MaxACVoltage = 0x0651 + SwitchChan_MaxDCVoltage = 0x0650 + SwitchChan_WireMode = 0x18E5 + SwitchChan_Bandwidth = 0x0640 + SwitchChan_Impedance = 0x0641 + SwitchDev_SettlingTime = 0x1244 + SwitchDev_AutoConnAnlgBus = 0x17DA + SwitchDev_PwrDownLatchRelaysAfterSettling = 0x22DB + SwitchDev_Settled = 0x1243 + SwitchDev_RelayList = 0x17DC + SwitchDev_NumRelays = 0x18E6 + SwitchDev_SwitchChanList = 0x18E7 + SwitchDev_NumSwitchChans = 0x18E8 + SwitchDev_NumRows = 0x18E9 + SwitchDev_NumColumns = 0x18EA + SwitchDev_Topology = 0x193D + SwitchScan_BreakMode = 0x1247 + SwitchScan_RepeatMode = 0x1248 + SwitchScan_WaitingForAdv = 0x17D9 + Scale_Descr = 0x1226 + Scale_ScaledUnits = 0x191B + Scale_PreScaledUnits = 0x18F7 + Scale_Type = 0x1929 + Scale_Lin_Slope = 0x1227 + Scale_Lin_YIntercept = 0x1228 + Scale_Map_ScaledMax = 0x1229 + Scale_Map_PreScaledMax = 0x1231 + Scale_Map_ScaledMin = 0x1230 + Scale_Map_PreScaledMin = 0x1232 + Scale_Poly_ForwardCoeff = 0x1234 + Scale_Poly_ReverseCoeff = 0x1235 + Scale_Table_ScaledVals = 0x1236 + Scale_Table_PreScaledVals = 0x1237 + Sys_GlobalChans = 0x1265 + Sys_Scales = 0x1266 + Sys_Tasks = 0x1267 + Sys_DevNames = 0x193B + Sys_NIDAQMajorVersion = 0x1272 + Sys_NIDAQMinorVersion = 0x1923 + Sys_NIDAQUpdateVersion = 0x2F22 + Task_Name = 0x1276 + Task_Channels = 0x1273 + Task_NumChans = 0x2181 + Task_Devices = 0x230E + Task_NumDevices = 0x29BA + Task_Complete = 0x1274 + SampQuant_SampMode = 0x1300 + SampQuant_SampPerChan = 0x1310 + SampTimingType = 0x1347 + SampClk_Rate = 0x1344 + SampClk_MaxRate = 0x22C8 + SampClk_Src = 0x1852 + SampClk_ActiveEdge = 0x1301 + SampClk_OverrunBehavior = 0x2EFC + SampClk_UnderflowBehavior = 0x2961 + SampClk_TimebaseDiv = 0x18EB + SampClk_Term = 0x2F1B + SampClk_Timebase_Rate = 0x1303 + SampClk_Timebase_Src = 0x1308 + SampClk_Timebase_ActiveEdge = 0x18EC + SampClk_Timebase_MasterTimebaseDiv = 0x1305 + SampClkTimebase_Term = 0x2F1C + SampClk_DigFltr_Enable = 0x221E + SampClk_DigFltr_MinPulseWidth = 0x221F + SampClk_DigFltr_TimebaseSrc = 0x2220 + SampClk_DigFltr_TimebaseRate = 0x2221 + SampClk_DigSync_Enable = 0x2222 + Hshk_DelayAfterXfer = 0x22C2 + Hshk_StartCond = 0x22C3 + Hshk_SampleInputDataWhen = 0x22C4 + ChangeDetect_DI_RisingEdgePhysicalChans = 0x2195 + ChangeDetect_DI_FallingEdgePhysicalChans = 0x2196 + ChangeDetect_DI_Tristate = 0x2EFA + OnDemand_SimultaneousAOEnable = 0x21A0 + Implicit_UnderflowBehavior = 0x2EFD + AIConv_Rate = 0x1848 + AIConv_MaxRate = 0x22C9 + AIConv_Src = 0x1502 + AIConv_ActiveEdge = 0x1853 + AIConv_TimebaseDiv = 0x1335 + AIConv_Timebase_Src = 0x1339 + DelayFromSampClk_DelayUnits = 0x1304 + DelayFromSampClk_Delay = 0x1317 + AIConv_DigFltr_Enable = 0x2EDC + AIConv_DigFltr_MinPulseWidth = 0x2EDD + AIConv_DigFltr_TimebaseSrc = 0x2EDE + AIConv_DigFltr_TimebaseRate = 0x2EDF + AIConv_DigSync_Enable = 0x2EE0 + MasterTimebase_Rate = 0x1495 + MasterTimebase_Src = 0x1343 + RefClk_Rate = 0x1315 + RefClk_Src = 0x1316 + SyncPulse_Src = 0x223D + SyncPulse_SyncTime = 0x223E + SyncPulse_MinDelayToStart = 0x223F + SyncPulse_ResetTime = 0x2F7C + SyncPulse_ResetDelay = 0x2F7D + SyncPulse_Term = 0x2F85 + SyncClk_Interval = 0x2F7E + SampTimingEngine = 0x2A26 + StartTrig_Type = 0x1393 + StartTrig_Term = 0x2F1E + DigEdge_StartTrig_Src = 0x1407 + DigEdge_StartTrig_Edge = 0x1404 + DigEdge_StartTrig_DigFltr_Enable = 0x2223 + DigEdge_StartTrig_DigFltr_MinPulseWidth = 0x2224 + DigEdge_StartTrig_DigFltr_TimebaseSrc = 0x2225 + DigEdge_StartTrig_DigFltr_TimebaseRate = 0x2226 + DigEdge_StartTrig_DigSync_Enable = 0x2227 + DigPattern_StartTrig_Src = 0x1410 + DigPattern_StartTrig_Pattern = 0x2186 + DigPattern_StartTrig_When = 0x1411 + AnlgEdge_StartTrig_Src = 0x1398 + AnlgEdge_StartTrig_Slope = 0x1397 + AnlgEdge_StartTrig_Lvl = 0x1396 + AnlgEdge_StartTrig_Hyst = 0x1395 + AnlgEdge_StartTrig_Coupling = 0x2233 + AnlgEdge_StartTrig_DigFltr_Enable = 0x2EE1 + AnlgEdge_StartTrig_DigFltr_MinPulseWidth = 0x2EE2 + AnlgEdge_StartTrig_DigFltr_TimebaseSrc = 0x2EE3 + AnlgEdge_StartTrig_DigFltr_TimebaseRate = 0x2EE4 + AnlgEdge_StartTrig_DigSync_Enable = 0x2EE5 + AnlgWin_StartTrig_Src = 0x1400 + AnlgWin_StartTrig_When = 0x1401 + AnlgWin_StartTrig_Top = 0x1403 + AnlgWin_StartTrig_Btm = 0x1402 + AnlgWin_StartTrig_Coupling = 0x2234 + AnlgWin_StartTrig_DigFltr_Enable = 0x2EFF + AnlgWin_StartTrig_DigFltr_MinPulseWidth = 0x2F00 + AnlgWin_StartTrig_DigFltr_TimebaseSrc = 0x2F01 + AnlgWin_StartTrig_DigFltr_TimebaseRate = 0x2F02 + AnlgWin_StartTrig_DigSync_Enable = 0x2F03 + StartTrig_Delay = 0x1856 + StartTrig_DelayUnits = 0x18C8 + StartTrig_Retriggerable = 0x190F + RefTrig_Type = 0x1419 + RefTrig_PretrigSamples = 0x1445 + RefTrig_Term = 0x2F1F + DigEdge_RefTrig_Src = 0x1434 + DigEdge_RefTrig_Edge = 0x1430 + DigEdge_RefTrig_DigFltr_Enable = 0x2ED7 + DigEdge_RefTrig_DigFltr_MinPulseWidth = 0x2ED8 + DigEdge_RefTrig_DigFltr_TimebaseSrc = 0x2ED9 + DigEdge_RefTrig_DigFltr_TimebaseRate = 0x2EDA + DigEdge_RefTrig_DigSync_Enable = 0x2EDB + DigPattern_RefTrig_Src = 0x1437 + DigPattern_RefTrig_Pattern = 0x2187 + DigPattern_RefTrig_When = 0x1438 + AnlgEdge_RefTrig_Src = 0x1424 + AnlgEdge_RefTrig_Slope = 0x1423 + AnlgEdge_RefTrig_Lvl = 0x1422 + AnlgEdge_RefTrig_Hyst = 0x1421 + AnlgEdge_RefTrig_Coupling = 0x2235 + AnlgEdge_RefTrig_DigFltr_Enable = 0x2EE6 + AnlgEdge_RefTrig_DigFltr_MinPulseWidth = 0x2EE7 + AnlgEdge_RefTrig_DigFltr_TimebaseSrc = 0x2EE8 + AnlgEdge_RefTrig_DigFltr_TimebaseRate = 0x2EE9 + AnlgEdge_RefTrig_DigSync_Enable = 0x2EEA + AnlgWin_RefTrig_Src = 0x1426 + AnlgWin_RefTrig_When = 0x1427 + AnlgWin_RefTrig_Top = 0x1429 + AnlgWin_RefTrig_Btm = 0x1428 + AnlgWin_RefTrig_Coupling = 0x1857 + AnlgWin_RefTrig_DigFltr_Enable = 0x2EEB + AnlgWin_RefTrig_DigFltr_MinPulseWidth = 0x2EEC + AnlgWin_RefTrig_DigFltr_TimebaseSrc = 0x2EED + AnlgWin_RefTrig_DigFltr_TimebaseRate = 0x2EEE + AnlgWin_RefTrig_DigSync_Enable = 0x2EEF + RefTrig_AutoTrigEnable = 0x2EC1 + RefTrig_AutoTriggered = 0x2EC2 + RefTrig_Delay = 0x1483 + AdvTrig_Type = 0x1365 + DigEdge_AdvTrig_Src = 0x1362 + DigEdge_AdvTrig_Edge = 0x1360 + DigEdge_AdvTrig_DigFltr_Enable = 0x2238 + HshkTrig_Type = 0x22B7 + Interlocked_HshkTrig_Src = 0x22B8 + Interlocked_HshkTrig_AssertedLvl = 0x22B9 + PauseTrig_Type = 0x1366 + PauseTrig_Term = 0x2F20 + AnlgLvl_PauseTrig_Src = 0x1370 + AnlgLvl_PauseTrig_When = 0x1371 + AnlgLvl_PauseTrig_Lvl = 0x1369 + AnlgLvl_PauseTrig_Hyst = 0x1368 + AnlgLvl_PauseTrig_Coupling = 0x2236 + AnlgLvl_PauseTrig_DigFltr_Enable = 0x2EF0 + AnlgLvl_PauseTrig_DigFltr_MinPulseWidth = 0x2EF1 + AnlgLvl_PauseTrig_DigFltr_TimebaseSrc = 0x2EF2 + AnlgLvl_PauseTrig_DigFltr_TimebaseRate = 0x2EF3 + AnlgLvl_PauseTrig_DigSync_Enable = 0x2EF4 + AnlgWin_PauseTrig_Src = 0x1373 + AnlgWin_PauseTrig_When = 0x1374 + AnlgWin_PauseTrig_Top = 0x1376 + AnlgWin_PauseTrig_Btm = 0x1375 + AnlgWin_PauseTrig_Coupling = 0x2237 + AnlgWin_PauseTrig_DigFltr_Enable = 0x2EF5 + AnlgWin_PauseTrig_DigFltr_MinPulseWidth = 0x2EF6 + AnlgWin_PauseTrig_DigFltr_TimebaseSrc = 0x2EF7 + AnlgWin_PauseTrig_DigFltr_TimebaseRate = 0x2EF8 + AnlgWin_PauseTrig_DigSync_Enable = 0x2EF9 + DigLvl_PauseTrig_Src = 0x1379 + DigLvl_PauseTrig_When = 0x1380 + DigLvl_PauseTrig_DigFltr_Enable = 0x2228 + DigLvl_PauseTrig_DigFltr_MinPulseWidth = 0x2229 + DigLvl_PauseTrig_DigFltr_TimebaseSrc = 0x222A + DigLvl_PauseTrig_DigFltr_TimebaseRate = 0x222B + DigLvl_PauseTrig_DigSync_Enable = 0x222C + DigPattern_PauseTrig_Src = 0x216F + DigPattern_PauseTrig_Pattern = 0x2188 + DigPattern_PauseTrig_When = 0x2170 + ArmStartTrig_Type = 0x1414 + ArmStart_Term = 0x2F7F + DigEdge_ArmStartTrig_Src = 0x1417 + DigEdge_ArmStartTrig_Edge = 0x1415 + DigEdge_ArmStartTrig_DigFltr_Enable = 0x222D + DigEdge_ArmStartTrig_DigFltr_MinPulseWidth = 0x222E + DigEdge_ArmStartTrig_DigFltr_TimebaseSrc = 0x222F + DigEdge_ArmStartTrig_DigFltr_TimebaseRate = 0x2230 + DigEdge_ArmStartTrig_DigSync_Enable = 0x2231 + Trigger_SyncType = 0x2F80 + Watchdog_Timeout = 0x21A9 + WatchdogExpirTrig_Type = 0x21A3 + DigEdge_WatchdogExpirTrig_Src = 0x21A4 + DigEdge_WatchdogExpirTrig_Edge = 0x21A5 + Watchdog_DO_ExpirState = 0x21A7 + Watchdog_HasExpired = 0x21A8 + Write_RelativeTo = 0x190C + Write_Offset = 0x190D + Write_RegenMode = 0x1453 + Write_CurrWritePos = 0x1458 + Write_OvercurrentChansExist = 0x29E8 + Write_OvercurrentChans = 0x29E9 + Write_OvertemperatureChansExist = 0x2A84 + Write_OpenCurrentLoopChansExist = 0x29EA + Write_OpenCurrentLoopChans = 0x29EB + Write_PowerSupplyFaultChansExist = 0x29EC + Write_PowerSupplyFaultChans = 0x29ED + Write_SpaceAvail = 0x1460 + Write_TotalSampPerChanGenerated = 0x192B + Write_RawDataWidth = 0x217D + Write_NumChans = 0x217E + Write_WaitMode = 0x22B1 + Write_SleepTime = 0x22B2 + Write_NextWriteIsLast = 0x296C + Write_DigitalLines_BytesPerChan = 0x217F + PhysicalChan_AI_SupportedMeasTypes = 0x2FD7 + PhysicalChan_AI_TermCfgs = 0x2342 + PhysicalChan_AI_InputSrcs = 0x2FD8 + PhysicalChan_AO_SupportedOutputTypes = 0x2FD9 + PhysicalChan_AO_TermCfgs = 0x29A3 + PhysicalChan_AO_ManualControlEnable = 0x2A1E + PhysicalChan_AO_ManualControl_ShortDetected = 0x2EC3 + PhysicalChan_AO_ManualControlAmplitude = 0x2A1F + PhysicalChan_AO_ManualControlFreq = 0x2A20 + PhysicalChan_DI_PortWidth = 0x29A4 + PhysicalChan_DI_SampClkSupported = 0x29A5 + PhysicalChan_DI_SampModes = 0x2FE0 + PhysicalChan_DI_ChangeDetectSupported = 0x29A6 + PhysicalChan_DO_PortWidth = 0x29A7 + PhysicalChan_DO_SampClkSupported = 0x29A8 + PhysicalChan_DO_SampModes = 0x2FE1 + PhysicalChan_CI_SupportedMeasTypes = 0x2FDA + PhysicalChan_CO_SupportedOutputTypes = 0x2FDB + PhysicalChan_TEDS_MfgID = 0x21DA + PhysicalChan_TEDS_ModelNum = 0x21DB + PhysicalChan_TEDS_SerialNum = 0x21DC + PhysicalChan_TEDS_VersionNum = 0x21DD + PhysicalChan_TEDS_VersionLetter = 0x21DE + PhysicalChan_TEDS_BitStream = 0x21DF + PhysicalChan_TEDS_TemplateIDs = 0x228F + PersistedTask_Author = 0x22CC + PersistedTask_AllowInteractiveEditing = 0x22CD + PersistedTask_AllowInteractiveDeletion = 0x22CE + PersistedChan_Author = 0x22D0 + PersistedChan_AllowInteractiveEditing = 0x22D1 + PersistedChan_AllowInteractiveDeletion = 0x22D2 + PersistedScale_Author = 0x22D4 + PersistedScale_AllowInteractiveEditing = 0x22D5 + PersistedScale_AllowInteractiveDeletion = 0x22D6 + ReadWaitMode = Read_WaitMode + + Val_Task_Start = 0 + Val_Task_Stop = 1 + Val_Task_Verify = 2 + Val_Task_Commit = 3 + Val_Task_Reserve = 4 + Val_Task_Unreserve = 5 + Val_Task_Abort = 6 + Val_SynchronousEventCallbacks = (1<<0) + Val_Acquired_Into_Buffer = 1 + Val_Transferred_From_Buffer = 2 + Val_ResetTimer = 0 + Val_ClearExpiration = 1 + Val_ChanPerLine = 0 + Val_ChanForAllLines = 1 + Val_GroupByChannel = 0 + Val_GroupByScanNumber = 1 + Val_DoNotInvertPolarity = 0 + Val_InvertPolarity = 1 + Val_Action_Commit = 0 + Val_Action_Cancel = 1 + Val_AdvanceTrigger = 12488 + Val_Rising = 10280 + Val_Falling = 10171 + Val_PathStatus_Available = 10431 + Val_PathStatus_AlreadyExists = 10432 + Val_PathStatus_Unsupported = 10433 + Val_PathStatus_ChannelInUse = 10434 + Val_PathStatus_SourceChannelConflict = 10435 + Val_PathStatus_ChannelReservedForRouting = 10436 + Val_DegC = 10143 + Val_DegF = 10144 + Val_Kelvins = 10325 + Val_DegR = 10145 + Val_High = 10192 + Val_Low = 10214 + Val_Tristate = 10310 + Val_PullUp = 15950 + Val_PullDown = 15951 + Val_ChannelVoltage = 0 + Val_ChannelCurrent = 1 + Val_Open = 10437 + Val_Closed = 10438 + Val_Loopback0 = 0 + Val_Loopback180 = 1 + Val_Ground = 2 + Val_Cfg_Default = -1 + Val_Default = -1 + Val_WaitInfinitely = -1.0 + Val_Auto = -1 + Val_Save_Overwrite = (1<<0) + Val_Save_AllowInteractiveEditing = (1<<1) + Val_Save_AllowInteractiveDeletion = (1<<2) + Val_Bit_TriggerUsageTypes_Advance = (1<<0) + Val_Bit_TriggerUsageTypes_Pause = (1<<1) + Val_Bit_TriggerUsageTypes_Reference = (1<<2) + Val_Bit_TriggerUsageTypes_Start = (1<<3) + Val_Bit_TriggerUsageTypes_Handshake = (1<<4) + Val_Bit_TriggerUsageTypes_ArmStart = (1<<5) + Val_Bit_CouplingTypes_AC = (1<<0) + Val_Bit_CouplingTypes_DC = (1<<1) + Val_Bit_CouplingTypes_Ground = (1<<2) + Val_Bit_CouplingTypes_HFReject = (1<<3) + Val_Bit_CouplingTypes_LFReject = (1<<4) + Val_Bit_CouplingTypes_NoiseReject = (1<<5) + Val_Bit_TermCfg_RSE = (1<<0) + Val_Bit_TermCfg_NRSE = (1<<1) + Val_Bit_TermCfg_Diff = (1<<2) + Val_Bit_TermCfg_PseudoDIFF = (1<<3) + Val_4Wire = 4 + Val_5Wire = 5 + Val_HighResolution = 10195 + Val_HighSpeed = 14712 + Val_Best50HzRejection = 14713 + Val_Best60HzRejection = 14714 + Val_Custom = 10137 + Val_Voltage = 10322 + Val_VoltageRMS = 10350 + Val_Current = 10134 + Val_CurrentRMS = 10351 + Val_Voltage_CustomWithExcitation = 10323 + Val_Bridge = 15908 + Val_Freq_Voltage = 10181 + Val_Resistance = 10278 + Val_Temp_TC = 10303 + Val_Temp_Thrmstr = 10302 + Val_Temp_RTD = 10301 + Val_Temp_BuiltInSensor = 10311 + Val_Strain_Gage = 10300 + Val_Position_LVDT = 10352 + Val_Position_RVDT = 10353 + Val_Position_EddyCurrentProximityProbe = 14835 + Val_Accelerometer = 10356 + Val_Force_Bridge = 15899 + Val_Force_IEPESensor = 15895 + Val_Pressure_Bridge = 15902 + Val_SoundPressure_Microphone = 10354 + Val_Torque_Bridge = 15905 + Val_TEDS_Sensor = 12531 + Val_ZeroVolts = 12526 + Val_HighImpedance = 12527 + Val_MaintainExistingValue = 12528 + Val_Voltage = 10322 + Val_Current = 10134 + Val_FuncGen = 14750 + Val_mVoltsPerG = 12509 + Val_VoltsPerG = 12510 + Val_AccelUnit_g = 10186 + Val_MetersPerSecondSquared = 12470 + Val_InchesPerSecondSquared = 12471 + Val_FromCustomScale = 10065 + Val_FiniteSamps = 10178 + Val_ContSamps = 10123 + Val_HWTimedSinglePoint = 12522 + Val_AboveLvl = 10093 + Val_BelowLvl = 10107 + Val_Degrees = 10146 + Val_Radians = 10273 + Val_FromCustomScale = 10065 + Val_Degrees = 10146 + Val_Radians = 10273 + Val_Ticks = 10304 + Val_FromCustomScale = 10065 + Val_None = 10230 + Val_Once = 10244 + Val_EverySample = 10164 + Val_NoAction = 10227 + Val_BreakBeforeMake = 10110 + Val_FullBridge = 10182 + Val_HalfBridge = 10187 + Val_QuarterBridge = 10270 + Val_NoBridge = 10228 + Val_VoltsPerVolt = 15896 + Val_mVoltsPerVolt = 15897 + Val_Newtons = 15875 + Val_Pounds = 15876 + Val_KilogramForce = 15877 + Val_Pascals = 10081 + Val_PoundsPerSquareInch = 15879 + Val_Bar = 15880 + Val_NewtonMeters = 15881 + Val_InchOunces = 15882 + Val_InchPounds = 15883 + Val_FootPounds = 15884 + Val_VoltsPerVolt = 15896 + Val_mVoltsPerVolt = 15897 + Val_FromCustomScale = 10065 + Val_FromTEDS = 12516 + Val_PCI = 12582 + Val_PCIe = 13612 + Val_PXI = 12583 + Val_PXIe = 14706 + Val_SCXI = 12584 + Val_SCC = 14707 + Val_PCCard = 12585 + Val_USB = 12586 + Val_CompactDAQ = 14637 + Val_TCPIP = 14828 + Val_Unknown = 12588 + Val_SwitchBlock = 15870 + Val_CountEdges = 10125 + Val_Freq = 10179 + Val_Period = 10256 + Val_PulseWidth = 10359 + Val_SemiPeriod = 10289 + Val_PulseFrequency = 15864 + Val_PulseTime = 15865 + Val_PulseTicks = 15866 + Val_Position_AngEncoder = 10360 + Val_Position_LinEncoder = 10361 + Val_TwoEdgeSep = 10267 + Val_GPS_Timestamp = 10362 + Val_BuiltIn = 10200 + Val_ConstVal = 10116 + Val_Chan = 10113 + Val_Pulse_Time = 10269 + Val_Pulse_Freq = 10119 + Val_Pulse_Ticks = 10268 + Val_AI = 10100 + Val_AO = 10102 + Val_DI = 10151 + Val_DO = 10153 + Val_CI = 10131 + Val_CO = 10132 + Val_Unconstrained = 14708 + Val_FixedHighFreq = 14709 + Val_FixedLowFreq = 14710 + Val_Fixed50PercentDutyCycle = 14711 + Val_CountUp = 10128 + Val_CountDown = 10124 + Val_ExtControlled = 10326 + Val_LowFreq1Ctr = 10105 + Val_HighFreq2Ctr = 10157 + Val_LargeRng2Ctr = 10205 + Val_AC = 10045 + Val_DC = 10050 + Val_GND = 10066 + Val_AC = 10045 + Val_DC = 10050 + Val_Internal = 10200 + Val_External = 10167 + Val_Amps = 10342 + Val_FromCustomScale = 10065 + Val_FromTEDS = 12516 + Val_Amps = 10342 + Val_FromCustomScale = 10065 + Val_RightJustified = 10279 + Val_LeftJustified = 10209 + Val_DMA = 10054 + Val_Interrupts = 10204 + Val_ProgrammedIO = 10264 + Val_USBbulk = 12590 + Val_OnbrdMemMoreThanHalfFull = 10237 + Val_OnbrdMemFull = 10236 + Val_OnbrdMemCustomThreshold = 12577 + Val_ActiveDrive = 12573 + Val_OpenCollector = 12574 + Val_High = 10192 + Val_Low = 10214 + Val_Tristate = 10310 + Val_NoChange = 10160 + Val_PatternMatches = 10254 + Val_PatternDoesNotMatch = 10253 + Val_SampClkPeriods = 10286 + Val_Seconds = 10364 + Val_Ticks = 10304 + Val_Seconds = 10364 + Val_Ticks = 10304 + Val_Seconds = 10364 + Val_mVoltsPerMil = 14836 + Val_VoltsPerMil = 14837 + Val_mVoltsPerMillimeter = 14838 + Val_VoltsPerMillimeter = 14839 + Val_mVoltsPerMicron = 14840 + Val_Rising = 10280 + Val_Falling = 10171 + Val_X1 = 10090 + Val_X2 = 10091 + Val_X4 = 10092 + Val_TwoPulseCounting = 10313 + Val_AHighBHigh = 10040 + Val_AHighBLow = 10041 + Val_ALowBHigh = 10042 + Val_ALowBLow = 10043 + Val_DC = 10050 + Val_AC = 10045 + Val_Internal = 10200 + Val_External = 10167 + Val_None = 10230 + Val_Voltage = 10322 + Val_Current = 10134 + Val_Pulse = 10265 + Val_Toggle = 10307 + Val_Pulse = 10265 + Val_Lvl = 10210 + Val_Interlocked = 12549 + Val_Pulse = 10265 + Val_mVoltsPerNewton = 15891 + Val_mVoltsPerPound = 15892 + Val_Newtons = 15875 + Val_Pounds = 15876 + Val_KilogramForce = 15877 + Val_FromCustomScale = 10065 + Val_Hz = 10373 + Val_FromCustomScale = 10065 + Val_Hz = 10373 + Val_Hz = 10373 + Val_Ticks = 10304 + Val_FromCustomScale = 10065 + Val_Sine = 14751 + Val_Triangle = 14752 + Val_Square = 14753 + Val_Sawtooth = 14754 + Val_IRIGB = 10070 + Val_PPS = 10080 + Val_None = 10230 + Val_Immediate = 10198 + Val_WaitForHandshakeTriggerAssert = 12550 + Val_WaitForHandshakeTriggerDeassert = 12551 + Val_OnBrdMemMoreThanHalfFull = 10237 + Val_OnBrdMemNotEmpty = 10241 + Val_OnbrdMemCustomThreshold = 12577 + Val_WhenAcqComplete = 12546 + Val_RSE = 10083 + Val_NRSE = 10078 + Val_Diff = 10106 + Val_PseudoDiff = 12529 + Val_mVoltsPerVoltPerMillimeter = 12506 + Val_mVoltsPerVoltPerMilliInch = 12505 + Val_Meters = 10219 + Val_Inches = 10379 + Val_FromCustomScale = 10065 + Val_Meters = 10219 + Val_Inches = 10379 + Val_Ticks = 10304 + Val_FromCustomScale = 10065 + Val_High = 10192 + Val_Low = 10214 + Val_Off = 10231 + Val_Log = 15844 + Val_LogAndRead = 15842 + Val_Open = 10437 + Val_OpenOrCreate = 15846 + Val_CreateOrReplace = 15847 + Val_Create = 15848 + Val_2point5V = 14620 + Val_3point3V = 14621 + Val_5V = 14619 + Val_SameAsSampTimebase = 10284 + Val_100MHzTimebase = 15857 + Val_SameAsMasterTimebase = 10282 + Val_20MHzTimebase = 12537 + Val_80MHzTimebase = 14636 + Val_AM = 14756 + Val_FM = 14757 + Val_None = 10230 + Val_OnBrdMemEmpty = 10235 + Val_OnBrdMemHalfFullOrLess = 10239 + Val_OnBrdMemNotFull = 10242 + Val_RSE = 10083 + Val_Diff = 10106 + Val_PseudoDiff = 12529 + Val_StopTaskAndError = 15862 + Val_IgnoreOverruns = 15863 + Val_OverwriteUnreadSamps = 10252 + Val_DoNotOverwriteUnreadSamps = 10159 + Val_ActiveHigh = 10095 + Val_ActiveLow = 10096 + Val_Pascals = 10081 + Val_PoundsPerSquareInch = 15879 + Val_Bar = 15880 + Val_FromCustomScale = 10065 + Val_MSeriesDAQ = 14643 + Val_XSeriesDAQ = 15858 + Val_ESeriesDAQ = 14642 + Val_SSeriesDAQ = 14644 + Val_BSeriesDAQ = 14662 + Val_SCSeriesDAQ = 14645 + Val_USBDAQ = 14646 + Val_AOSeries = 14647 + Val_DigitalIO = 14648 + Val_TIOSeries = 14661 + Val_DynamicSignalAcquisition = 14649 + Val_Switches = 14650 + Val_CompactDAQChassis = 14658 + Val_CSeriesModule = 14659 + Val_SCXIModule = 14660 + Val_SCCConnectorBlock = 14704 + Val_SCCModule = 14705 + Val_NIELVIS = 14755 + Val_NetworkDAQ = 14829 + Val_SCExpress = 15886 + Val_Unknown = 12588 + Val_Pt3750 = 12481 + Val_Pt3851 = 10071 + Val_Pt3911 = 12482 + Val_Pt3916 = 10069 + Val_Pt3920 = 10053 + Val_Pt3928 = 12483 + Val_Custom = 10137 + Val_mVoltsPerVoltPerDegree = 12507 + Val_mVoltsPerVoltPerRadian = 12508 + Val_None = 10230 + Val_LosslessPacking = 12555 + Val_LossyLSBRemoval = 12556 + Val_FirstSample = 10424 + Val_CurrReadPos = 10425 + Val_RefTrig = 10426 + Val_FirstPretrigSamp = 10427 + Val_MostRecentSamp = 10428 + Val_AllowRegen = 10097 + Val_DoNotAllowRegen = 10158 + Val_2Wire = 2 + Val_3Wire = 3 + Val_4Wire = 4 + Val_Ohms = 10384 + Val_FromCustomScale = 10065 + Val_FromTEDS = 12516 + Val_Ohms = 10384 + Val_FromCustomScale = 10065 + Val_Bits = 10109 + Val_SCXI1124Range0to1V = 14629 + Val_SCXI1124Range0to5V = 14630 + Val_SCXI1124Range0to10V = 14631 + Val_SCXI1124RangeNeg1to1V = 14632 + Val_SCXI1124RangeNeg5to5V = 14633 + Val_SCXI1124RangeNeg10to10V = 14634 + Val_SCXI1124Range0to20mA = 14635 + Val_SampClkActiveEdge = 14617 + Val_SampClkInactiveEdge = 14618 + Val_HandshakeTriggerAsserts = 12552 + Val_HandshakeTriggerDeasserts = 12553 + Val_SampClk = 10388 + Val_BurstHandshake = 12548 + Val_Handshake = 10389 + Val_Implicit = 10451 + Val_OnDemand = 10390 + Val_ChangeDetection = 12504 + Val_PipelinedSampClk = 14668 + Val_Linear = 10447 + Val_MapRanges = 10448 + Val_Polynomial = 10449 + Val_Table = 10450 + Val_Polynomial = 10449 + Val_Table = 10450 + Val_Polynomial = 10449 + Val_Table = 10450 + Val_None = 10230 + Val_None = 10230 + Val_TwoPointLinear = 15898 + Val_Table = 10450 + Val_Polynomial = 10449 + Val_A = 12513 + Val_B = 12514 + Val_AandB = 12515 + Val_R1 = 12465 + Val_R2 = 12466 + Val_R3 = 12467 + Val_R4 = 14813 + Val_None = 10230 + Val_AIConvertClock = 12484 + Val_10MHzRefClock = 12536 + Val_20MHzTimebaseClock = 12486 + Val_SampleClock = 12487 + Val_AdvanceTrigger = 12488 + Val_ReferenceTrigger = 12490 + Val_StartTrigger = 12491 + Val_AdvCmpltEvent = 12492 + Val_AIHoldCmpltEvent = 12493 + Val_CounterOutputEvent = 12494 + Val_ChangeDetectionEvent = 12511 + Val_WDTExpiredEvent = 12512 + Val_SampleCompleteEvent = 12530 + Val_CounterOutputEvent = 12494 + Val_ChangeDetectionEvent = 12511 + Val_SampleClock = 12487 + Val_RisingSlope = 10280 + Val_FallingSlope = 10171 + Val_Pascals = 10081 + Val_FromCustomScale = 10065 + Val_Internal = 10200 + Val_External = 10167 + Val_FullBridgeI = 10183 + Val_FullBridgeII = 10184 + Val_FullBridgeIII = 10185 + Val_HalfBridgeI = 10188 + Val_HalfBridgeII = 10189 + Val_QuarterBridgeI = 10271 + Val_QuarterBridgeII = 10272 + Val_Strain = 10299 + Val_FromCustomScale = 10065 + Val_Finite = 10172 + Val_Cont = 10117 + Val_Source = 10439 + Val_Load = 10440 + Val_ReservedForRouting = 10441 + Val_None = 10230 + Val_Master = 15888 + Val_Slave = 15889 + Val_FromCustomScale = 10065 + Val_FromTEDS = 12516 + Val_DegC = 10143 + Val_DegF = 10144 + Val_Kelvins = 10325 + Val_DegR = 10145 + Val_FromCustomScale = 10065 + Val_J_Type_TC = 10072 + Val_K_Type_TC = 10073 + Val_N_Type_TC = 10077 + Val_R_Type_TC = 10082 + Val_S_Type_TC = 10085 + Val_T_Type_TC = 10086 + Val_B_Type_TC = 10047 + Val_E_Type_TC = 10055 + Val_Seconds = 10364 + Val_FromCustomScale = 10065 + Val_Seconds = 10364 + Val_Seconds = 10364 + Val_Ticks = 10304 + Val_FromCustomScale = 10065 + Val_SingleCycle = 14613 + Val_Multicycle = 14614 + Val_NewtonMeters = 15881 + Val_InchOunces = 15882 + Val_InchPounds = 15883 + Val_FootPounds = 15884 + Val_FromCustomScale = 10065 + Val_DigEdge = 10150 + Val_None = 10230 + Val_DigEdge = 10150 + Val_Software = 10292 + Val_None = 10230 + Val_AnlgLvl = 10101 + Val_AnlgWin = 10103 + Val_DigLvl = 10152 + Val_DigPattern = 10398 + Val_None = 10230 + Val_AnlgEdge = 10099 + Val_DigEdge = 10150 + Val_DigPattern = 10398 + Val_AnlgWin = 10103 + Val_None = 10230 + Val_Interlocked = 12549 + Val_None = 10230 + Val_HaltOutputAndError = 14615 + Val_PauseUntilDataAvailable = 14616 + Val_Volts = 10348 + Val_Amps = 10342 + Val_DegF = 10144 + Val_DegC = 10143 + Val_DegR = 10145 + Val_Kelvins = 10325 + Val_Strain = 10299 + Val_Ohms = 10384 + Val_Hz = 10373 + Val_Seconds = 10364 + Val_Meters = 10219 + Val_Inches = 10379 + Val_Degrees = 10146 + Val_Radians = 10273 + Val_Ticks = 10304 + Val_g = 10186 + Val_MetersPerSecondSquared = 12470 + Val_Pascals = 10081 + Val_Newtons = 15875 + Val_Pounds = 15876 + Val_KilogramForce = 15877 + Val_PoundsPerSquareInch = 15879 + Val_Bar = 15880 + Val_NewtonMeters = 15881 + Val_InchOunces = 15882 + Val_InchPounds = 15883 + Val_FootPounds = 15884 + Val_VoltsPerVolt = 15896 + Val_mVoltsPerVolt = 15897 + Val_FromTEDS = 12516 + Val_Volts = 10348 + Val_FromCustomScale = 10065 + Val_FromTEDS = 12516 + Val_Volts = 10348 + Val_FromCustomScale = 10065 + Val_WaitForInterrupt = 12523 + Val_Poll = 12524 + Val_Yield = 12525 + Val_Sleep = 12547 + Val_Poll = 12524 + Val_Yield = 12525 + Val_Sleep = 12547 + Val_WaitForInterrupt = 12523 + Val_Poll = 12524 + Val_WaitForInterrupt = 12523 + Val_Poll = 12524 + Val_EnteringWin = 10163 + Val_LeavingWin = 10208 + Val_InsideWin = 10199 + Val_OutsideWin = 10251 + Val_WriteToEEPROM = 12538 + Val_WriteToPROM = 12539 + Val_DoNotWrite = 12540 + Val_FirstSample = 10424 + Val_CurrWritePos = 10430 + Val_Switch_Topology_1127_Independent = "1127/Independent" + Val_Switch_Topology_1128_Independent = "1128/Independent" + Val_Switch_Topology_1130_Independent = "1130/Independent" + Val_Switch_Topology_1160_16_SPDT = "1160/16-SPDT" + Val_Switch_Topology_1161_8_SPDT = "1161/8-SPDT" + Val_Switch_Topology_1166_32_SPDT = "1166/32-SPDT" + Val_Switch_Topology_1166_16_DPDT = "1166/16-DPDT" + Val_Switch_Topology_1167_Independent = "1167/Independent" + Val_Switch_Topology_1169_100_SPST = "1169/100-SPST" + Val_Switch_Topology_1169_50_DPST = "1169/50-DPST" + Val_Switch_Topology_1192_8_SPDT = "1192/8-SPDT" + Val_Switch_Topology_1193_Independent = "1193/Independent" + Val_Switch_Topology_2510_Independent = "2510/Independent" + Val_Switch_Topology_2512_Independent = "2512/Independent" + Val_Switch_Topology_2514_Independent = "2514/Independent" + Val_Switch_Topology_2515_Independent = "2515/Independent" + Val_Switch_Topology_2527_Independent = "2527/Independent" + Val_Switch_Topology_2530_Independent = "2530/Independent" + Val_Switch_Topology_2548_4_SPDT = "2548/4-SPDT" + Val_Switch_Topology_2558_4_SPDT = "2558/4-SPDT" + Val_Switch_Topology_2564_16_SPST = "2564/16-SPST" + Val_Switch_Topology_2564_8_DPST = "2564/8-DPST" + Val_Switch_Topology_2565_16_SPST = "2565/16-SPST" + Val_Switch_Topology_2566_16_SPDT = "2566/16-SPDT" + Val_Switch_Topology_2566_8_DPDT = "2566/8-DPDT" + Val_Switch_Topology_2567_Independent = "2567/Independent" + Val_Switch_Topology_2568_31_SPST = "2568/31-SPST" + Val_Switch_Topology_2568_15_DPST = "2568/15-DPST" + Val_Switch_Topology_2569_100_SPST = "2569/100-SPST" + Val_Switch_Topology_2569_50_DPST = "2569/50-DPST" + Val_Switch_Topology_2570_40_SPDT = "2570/40-SPDT" + Val_Switch_Topology_2570_20_DPDT = "2570/20-DPDT" + Val_Switch_Topology_2571_66_SPDT = "2571/66-SPDT" + Val_Switch_Topology_2576_Independent = "2576/Independent" + Val_Switch_Topology_2584_Independent = "2584/Independent" + Val_Switch_Topology_2586_10_SPST = "2586/10-SPST" + Val_Switch_Topology_2586_5_DPST = "2586/5-DPST" + Val_Switch_Topology_2593_Independent = "2593/Independent" + Val_Switch_Topology_2599_2_SPDT = "2599/2-SPDT" + +import ctypes as ct + +class DoNothing(object): + + def from_param(self, param): + return param + +class Types(metaclass=RichEnum): + + _ = DoNothing() + + void_p = ct.c_void_p + + TaskHandle = ct.c_void_p + #TaskHandle = ct.c_uint32 + bool32 = ct.c_uint32 + + string = ct.c_char_p + + int8 = ct.c_int8 + uInt8 = ct.c_uint8 + + int16 = ct.c_int16 + uInt16 = ct.c_uint16 + + int32 = ct.c_int32 + uInt32 = ct.c_uint32 + + int64 = ct.c_int64 + uInt64 = ct.c_uint64 + + float32 = ct.c_float + float64 = ct.c_double diff --git a/NI/NI64/tasks.py b/NI/NI64/tasks.py new file mode 100644 index 0000000..eddfc73 --- /dev/null +++ b/NI/NI64/tasks.py @@ -0,0 +1,525 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.ni.daqmx.tasks + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation of specialized tasks clases. + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +import numpy as np + +from lantz import Feat, Action +from lantz.foreign import RetStr, RetTuple, RetValue + +from .base import Task, Channel +from .constants import Constants + +_GROUP_BY = {'scan': Constants.Val_GroupByScanNumber, + 'channel': Constants.Val_GroupByChannel} + + + +class AnalogInputTask(Task): + """Analog Input Task + """ + + IO_TYPE = 'AI' + + @Feat() + def max_convert_rate(self): + """Maximum convert rate supported by the task, given the current + devices and channel count. + + This rate is generally faster than the default AI Convert + Clock rate selected by NI-DAQmx, because NI-DAQmx adds in an + additional 10 microseconds per channel settling time to + compensate for most potential system settling constraints. + + For single channel tasks, the maximum AI Convert Clock rate is the + maximum rate of the ADC. For multiple channel tasks, the maximum + AI Convert Clock rate is the maximum convert rate of the analog + hardware, including the ADC, filters, multiplexers, and amplifiers. + Signal conditioning accessories can further constrain the maximum AI + Convert Clock based on timing and settling requirements. + """ + err, value = self.lib.GetAIConvMaxRate(RetValue('f64')) + return value + + def read_scalar(self, timeout=10.0): + """Return a single floating-point sample from a task that + contains a single analog input channel. + + :param timeout: The amount of time, in seconds, to wait for the function to + read the sample(s). The default value is 10.0 seconds. To + specify an infinite wait, pass -1 (DAQmx_Val_WaitInfinitely). + This function returns an error if the timeout elapses. + + A value of 0 indicates to try once to read the requested + samples. If all the requested samples are read, the function + is successful. Otherwise, the function returns a timeout error + and returns the samples that were actually read. + + :rtype: float + """ + + err, value = self.lib.ReadAnalogScalarF64(timeout, RetValue('f64'), None) + return value + + @Action(units=(None, 'seconds', None), values=(None, None, _GROUP_BY)) + def read(self, samples_per_channel=None, timeout=10.0, group_by='channel'): + """Reads multiple floating-point samples from a task that + contains one or more analog input channels. + + :param samples_per_channel: + The number of samples, per channel, to read. The default + value of -1 (DAQmx_Val_Auto) reads all available samples. If + readArray does not contain enough space, this function + returns as many samples as fit in readArray. + + NI-DAQmx determines how many samples to read based on + whether the task acquires samples continuously or acquires a + finite number of samples. + + If the task acquires samples continuously and you set this + parameter to -1, this function reads all the samples + currently available in the buffer. + + If the task acquires a finite number of samples and you set + this parameter to -1, the function waits for the task to + acquire all requested samples, then reads those samples. If + you set the Read All Available Samples property to TRUE, the + function reads the samples currently available in the buffer + and does not wait for the task to acquire all requested + samples. + + :param timeout: float + The amount of time, in seconds, to wait for the function to + read the sample(s). The default value is 10.0 seconds. To + specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error + if the timeout elapses. + + A value of 0 indicates to try once to read the requested + samples. If all the requested samples are read, the function + is successful. Otherwise, the function returns a timeout + error and returns the samples that were actually read. + + :param group_by: + + 'channel' + Group by channel (non-interleaved):: + + ch0:s1, ch0:s2, ..., ch1:s1, ch1:s2,..., ch2:s1,.. + + 'scan' + Group by scan number (interleaved):: + + ch0:s1, ch1:s1, ch2:s1, ch0:s2, ch1:s2, ch2:s2,... + + :rtype: numpy.ndarray + """ + + if samples_per_channel is None: + samples_per_channel = self.samples_per_channel_available() + + number_of_channels = self.number_of_channels() + if group_by == Constants.Val_GroupByScanNumber: + data = np.zeros((samples_per_channel, number_of_channels), dtype=np.float64) + else: + data = np.zeros((number_of_channels, samples_per_channel), dtype=np.float64) + + err, data, count = self.lib.ReadAnalogF64(samples_per_channel, timeout, group_by, + data.ctypes.data, data.size, RetValue('i32'), None) + + if samples_per_channel < count: + if group_by == 'scan': + return data[:count] + else: + return data[:,:count] + + return data + + +class AnalogOutputTask(Task): + """Analog Output Task + """ + + CHANNEL_TYPE = 'AO' + + @Action(units=(None, None, 'seconds', None), values=(None, None, None, _GROUP_BY)) + def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): + """Write multiple floating-point samples or a scalar to a task + that contains one or more analog output channels. + + Note: If you configured timing for your task, your write is + considered a buffered write. Buffered writes require a minimum + buffer size of 2 samples. If you do not configure the buffer + size using DAQmxCfgOutputBuffer, NI-DAQmx automatically + configures the buffer when you configure sample timing. If you + attempt to write one sample for a buffered write without + configuring the buffer, you will receive an error. + + :param data: The array of 64-bit samples to write to the task + or a scalar. + + :param auto_start: Whether or not this function automatically starts + the task if you do not start it. + + :param timeout: The amount of time, in seconds, to wait for this + function to write all the samples. The default value is 10.0 + seconds. To specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error + if the timeout elapses. + + A value of 0 indicates to try once to write the submitted + samples. If this function successfully writes all submitted + samples, it does not return an error. Otherwise, the + function returns a timeout error and returns the number of + samples actually written. + + :param group_by: how the samples are arranged, either interleaved or noninterleaved + + 'channel' - Group by channel (non-interleaved). + + 'scan' - Group by scan number (interleaved). + + :return: The actual number of samples per channel successfully + written to the buffer. + + """ + if np.isscalar(data): + err = self.lib.WriteAnalogScalarF64(bool32(auto_start), + float64(timeout), + float64(data), None) + return 1 + + data = np.asarray(data, dtype = np.float64) + + number_of_channels = self.number_of_channels() + + if data.ndims == 1: + if number_of_channels == 1: + samples_per_channel = data.shape[0] + shape = (samples_per_channel, 1) + else: + samples_per_channel = data.size / number_of_channels + shape = (samples_per_channel, number_of_channels) + + if not group_by == Constants.Val_GroupByScanNumber: + shape = tuple(reversed(shape)) + + data.reshape(shape) + else: + if group_by == Constants.Val_GroupByScanNumber: + samples_per_channel = data.shape[0] + else: + samples_per_channel = data.shape[-1] + + err, count = self.lib.WriteAnalogF64(samples_per_channel, auto_start, + timeout, group_by, + data.ctypes.data, RetValue('i32'), + None) + + return count + + +class DigitalTask(Task): + + @Action(units=(None, 'seconds', None), values=(None, None, _GROUP_BY)) + def read(self, samples_per_channel=None, timeout=10.0, group_by='scan'): + """ + Reads multiple samples from each digital line in a task. Each + line in a channel gets one byte per sample. + + :param samples_per_channel: int or None + + The number of samples, per channel, to + read. The default value of -1 (DAQmx_Val_Auto) reads all + available samples. If readArray does not contain enough + space, this function returns as many samples as fit in + readArray. + + NI-DAQmx determines how many samples to read based on + whether the task acquires samples continuously or acquires a + finite number of samples. + + If the task acquires samples continuously and you set this + parameter to -1, this function reads all the samples + currently available in the buffer. + + If the task acquires a finite number of samples and you set + this parameter to -1, the function waits for the task to + acquire all requested samples, then reads those samples. If + you set the Read All Available Data property to TRUE, the + function reads the samples currently available in the buffer + and does not wait for the task to acquire all requested + samples. + + :param timeout: float + + The amount of time, in seconds, to wait for the function to + read the sample(s). The default value is 10.0 seconds. To + specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error + if the timeout elapses. + + A value of 0 indicates to try once to read the requested + samples. If all the requested samples are read, the function + is successful. Otherwise, the function returns a timeout + error and returns the samples that were actually read. + + :param group_by: {'group', 'scan'} + + Specifies whether or not the samples are interleaved: + + 'channel' - Group by channel (non-interleaved). + + 'scan' - Group by scan number (interleaved). + + Returns + ------- + + data : array + + The array to read samples into. Each `bytes_per_sample` + corresponds to one sample per channel, with each element + in that grouping corresponding to a line in that channel, + up to the number of lines contained in the channel. + + bytes_per_sample : int + + The number of elements in returned `data` that constitutes + a sample per channel. For each sample per channel, + `bytes_per_sample` is the number of bytes that channel + consists of. + + """ + + if samples_per_channel in (None, -1): + samples_per_channel = self.samples_per_channel_available() + + if self.one_channel_for_all_lines: + nof_lines = [] + for channel in self.names_of_channels(): + nof_lines.append(self.number_of_lines (channel)) + c = int (max (nof_lines)) + dtype = getattr(np, 'uint%s' % (8 * c)) + else: + c = 1 + dtype = np.uint8 + + number_of_channels = self.number_of_channels() + + if group_by == Constants.Val_GroupByScanNumber: + data = np.zeros((samples_per_channel, number_of_channels),dtype=dtype) + else: + data = np.zeros((number_of_channels, samples_per_channel),dtype=dtype) + + err, count, bps = self.lib.ReadDigitalLines(samples_per_channel, float64 (timeout), + group_by, data.ctypes.data, uInt32 (data.size * c), + RetValue('i32'), RetValue('i32'), + None + ) + if count < samples_per_channel: + if group_by == 'scan': + return data[:count], bps + else: + return data[:,:count], bps + return data, bps + + +class DigitalInputTask(DigitalTask): + """Exposes NI-DAQmx digital input task to Python. + """ + + CHANNEL_TYPE = 'DI' + + +class DigitalOutputTask(DigitalTask): + """Exposes NI-DAQmx digital output task to Python. + """ + + CHANNEL_TYPE = 'DO' + + @Action(units=(None, None, 'seconds', None), values=(None, {True, False}, None, _GROUP_BY)) + def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): + """ + Writes multiple samples to each digital line in a task. When + you create your write array, each sample per channel must + contain the number of bytes returned by the + DAQmx_Read_DigitalLines_BytesPerChan property. + + Note: If you configured timing for your task, your write is + considered a buffered write. Buffered writes require a minimum + buffer size of 2 samples. If you do not configure the buffer + size using DAQmxCfgOutputBuffer, NI-DAQmx automatically + configures the buffer when you configure sample timing. If you + attempt to write one sample for a buffered write without + configuring the buffer, you will receive an error. + + Parameters + ---------- + + data : array + + The samples to write to the task. + + auto_start : bool + + Specifies whether or not this function automatically starts + the task if you do not start it. + + timeout : float + + The amount of time, in seconds, to wait for this function to + write all the samples. The default value is 10.0 seconds. To + specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error + if the timeout elapses. + + A value of 0 indicates to try once to write the submitted + samples. If this function successfully writes all submitted + samples, it does not return an error. Otherwise, the + function returns a timeout error and returns the number of + samples actually written. + + layout : {'group_by_channel', 'group_by_scan_number'} + + Specifies how the samples are arranged, either interleaved + or noninterleaved: + + 'group_by_channel' - Group by channel (non-interleaved). + + 'group_by_scan_number' - Group by scan number (interleaved). + """ + + number_of_channels = self.get_number_of_channels() + + if np.isscalar(data): + data = np.array([data]*number_of_channels, dtype = np.uint8) + else: + data = np.asarray(data, dtype = np.uint8) + + if data.ndims == 1: + if number_of_channels == 1: + samples_per_channel = data.shape[0] + shape = (samples_per_channel, 1) + else: + samples_per_channel = data.size / number_of_channels + shape = (samples_per_channel, number_of_channels) + + if not group_by == Constants.Val_GroupByScanNumber: + shape = tuple(reversed(shape)) + + data.reshape(shape) + else: + if group_by == Constants.Val_GroupByScanNumber: + samples_per_channel = data.shape[0] + else: + samples_per_channel = data.shape[-1] + + err, count = self.lib.WriteDigitalLines(samples_per_channel, + bool32(auto_start), + float64(timeout), group_by, + data.ctypes.data, RetValue('u32'), None) + + return count + + # NotImplemented: WriteDigitalU8, WriteDigitalU16, WriteDigitalU32, WriteDigitalScalarU32 + +class CounterInputTask(Task): + """Exposes NI-DAQmx counter input task to Python. + """ + + CHANNEL_TYPE = 'CI' + + def read_scalar(self, timeout=10.0): + """Read a single floating-point sample from a counter task. Use + this function when the counter sample is scaled to a + floating-point value, such as for frequency and period + measurement. + + :param float: + The amount of time, in seconds, to wait for the function to + read the sample(s). The default value is 10.0 seconds. To + specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error if + the timeout elapses. + + A value of 0 indicates to try once to read the requested + samples. If all the requested samples are read, the function + is successful. Otherwise, the function returns a timeout error + and returns the samples that were actually read. + + :return: The sample read from the task. + """ + + err, value = self.lib.ReadCounterScalarF64(timeout, RetValue('f64'), None) + return value + + def read(self, samples_per_channel=None, timeout=10.0): + """Read multiple 32-bit integer samples from a counter task. + Use this function when counter samples are returned unscaled, + such as for edge counting. + + :param samples_per_channel: + The number of samples, per channel, to read. The default + value of -1 (DAQmx_Val_Auto) reads all available samples. If + readArray does not contain enough space, this function + returns as many samples as fit in readArray. + + NI-DAQmx determines how many samples to read based on + whether the task acquires samples continuously or acquires a + finite number of samples. + + If the task acquires samples continuously and you set this + parameter to -1, this function reads all the samples + currently available in the buffer. + + If the task acquires a finite number of samples and you set + this parameter to -1, the function waits for the task to + acquire all requested samples, then reads those samples. If + you set the Read All Available Samples property to TRUE, the + function reads the samples currently available in the buffer + and does not wait for the task to acquire all requested + samples. + + :param timeout: + The amount of time, in seconds, to wait for the function to + read the sample(s). The default value is 10.0 seconds. To + specify an infinite wait, pass -1 + (DAQmx_Val_WaitInfinitely). This function returns an error + if the timeout elapses. + + A value of 0 indicates to try once to read the requested + samples. If all the requested samples are read, the function + is successful. Otherwise, the function returns a timeout + error and returns the samples that were actually read. + + + :return: The array of samples read. + """ + + if samples_per_channel is None: + samples_per_channel = self.samples_per_channel_available() + + data = np.zeros((samples_per_channel,),dtype=np.int32) + + err, count = self.lib.ReadCounterU32(samples_per_channel, float64(timeout), + data.ctypes.data, data.size, RetValue('i32'), None) + + return data[:count] + + +class CounterOutputTask(Task): + + """Exposes NI-DAQmx counter output task to Python. + """ + + channel_type = 'CO' + + +Task.register_class(AnalogInputTask) diff --git a/NI/foreign.py b/NI/foreign.py new file mode 100644 index 0000000..2fae29b --- /dev/null +++ b/NI/foreign.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +""" + lantz.foreign + ~~~~~~~~~~~~~ + + Implements classes and methods to interface to foreign functions. + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +import os +import ctypes +import inspect +from ctypes.util import find_library +from ctypes import cast, POINTER, c_uint64 +from itertools import chain + +from lantz import Driver + + +class Wrapper(object): + + def __init__(self, name, wrapped, wrapper): + self.name = name + self.wrapped = wrapped + self.wrapper = wrapper + + def __call__(self, *args): + return self.wrapper(self.name, self.wrapped, *args) + + @property + def argtypes(self): + return self.wrapped.argtypes + + @argtypes.setter + def argtypes(self, value): + self.wrapped.argtypes = value + + @property + def restype(self): + return self.wrapped.restype + + @restype.setter + def restype(self, value): + self.wrapped.restype = value + + +class Library(object): + """Library wrapper + + :param library: ctypes library + :param wrapper: callable that takes two arguments the name of the function + and the function itself. It should return a callable. + """ + + def __init__(self, library, prefix='', wrapper=None): + if isinstance(library, str): + self.library_name = library + + if os.name == 'nt': + library = ctypes.WinDLL(library) + else: + library = ctypes.CDLL(library) + + self.wrapper = wrapper + self.prefix = prefix + self.internal = library + + def __get_func(self, name): + if self.prefix: + try: + return getattr(self.internal, self.prefix + name) + except: + pass + + try: + return getattr(self.internal, name) + except Exception: + if self.prefix: + raise AttributeError('Could not find ({}){} in {}'.format(self.prefix, name, self.internal)) + raise AttributeError('Could not find {} in {}'.format(name, self.internal)) + + def __getattr__(self, name): + if name.startswith('__') and name.endswith('__'): + raise AttributeError(name) + + func = self.__get_func(name) + + if self.wrapper: + func = Wrapper(name, func, self.wrapper) + + setattr(self, name, func) + return func + + +TYPES = {'c': ctypes.c_char, + 'b': ctypes.c_byte, + 'B': ctypes.c_ubyte, + '?': ctypes.c_bool, + 'h': ctypes.c_short, + 'H': ctypes.c_ushort, + 'i': ctypes.c_int, + 'I': ctypes.c_uint, + 'l': ctypes.c_long, + 'L': ctypes.c_ulong, + 'q': ctypes.c_longlong, + 'Q': ctypes.c_ulonglong, + 'f': ctypes.c_float, + 'd': ctypes.c_double, + 'u32': ctypes.c_uint32, + 'u64': ctypes.c_uint64, + 'i32': ctypes.c_int32, + 'f32': ctypes.c_float, + 'f64': ctypes.c_double} + + +class RetStr(object): + + def __init__(self, length, encoding='ascii'): + self.length = length + self.buffer = ctypes.create_string_buffer(b'', length) + self.encoding = encoding + + def __iter__(self): + yield self + yield self.length + + @property + def value(self): + if self.encoding: + return self.buffer.value.decode(self.encoding) + else: + return self.buffer.value + + +class RetValue(object): + + def __init__(self, type): + try: + self.buffer = (TYPES[type] * 1)() + except KeyError: + raise KeyError('The type {} is not defined ()'.format(type, TYPES.keys())) + + def __iter__(self): + yield self + + @property + def value(self): + return self.buffer[0] + + +class RetTuple(object): + + def __init__(self, type, length=1): + try: + self.buffer = (TYPES[type] * length)() + except KeyError: + raise KeyError('The type {} is not defined ()'.format(type, TYPES.keys())) + self.length = length + + def __iter__(self): + yield self + yield self.length + + @property + def value(self): + return tuple(self.buffer[:]) + + +class LibraryDriver(Driver): + """Base class for drivers that communicate with instruments + calling a library (dll or others) + + To use this class you must override LIBRARY_NAME + """ + + #: Name of the library + LIBRARY_NAME = '' + LIBRARY_PREFIX = '' + + def __init__(self, *args, **kwargs): + library_name = kwargs.pop('library_name', None) + super().__init__(*args, **kwargs) + + folder = os.path.dirname(inspect.getfile(self.__class__)) + for name in chain(iter_lib(library_name, folder), iter_lib(self.LIBRARY_NAME, folder)): + if name is None: + continue + self.log_debug('Trying to open library: {}'.format(name)) + try: + self.lib = Library(name, self.LIBRARY_PREFIX, self._wrapper) + break + except OSError: + pass + else: + raise OSError('While instantiating {}: library not found'.format(self.__class__.__name__)) + + self.log_info('LibraryDriver created with {} from {}', name, folder) + self._add_types() + + + def _add_types(self): + pass + + def _return_handler(self, func_name, ret_value): + return ret_value + + def _preprocess_args(self, name, *args): + new_args = [] + collect = [] + for arg in args: + if isinstance(arg, (RetStr, RetTuple, RetValue)): + collect.append(arg) + new_args.append(arg.buffer) + elif isinstance(arg, str): + new_args.append(bytes(arg, 'ascii')) + elif isinstance(arg, int): # hack to avoid issue w/ overflowing 64-bit integer when passing args to ctypes + if arg > 0xFFFFFFFF: + new_args.append(cast(arg, POINTER(c_uint64))) + else: + new_args.append(arg) + else: + new_args.append(arg) + + return new_args, collect + + def _wrapper(self, name, func, *args): + new_args, collect = self._preprocess_args(name, *args) + + try: + ret = func(*new_args) + + except Exception as e: + raise Exception('While calling {} with {} (was {}): {}'.format(name, new_args, args, e)) + + ret = self._return_handler(name, ret) + + return self._postprocess(name, ret, collect) + + def _postprocess(self, name, ret, collect): + if collect: + values = [item.value for item in collect] + values.insert(0, ret) + self.log_debug('Function call {} returned {}. Collected: {}', name, ret, collect) + return tuple(values) + + self.log_debug('Function call {} returned {}.', name, ret) + return ret + + +def iter_lib(library_name, folder=''): + if not library_name: + raise StopIteration + if isinstance(library_name, str): + if folder: + yield os.path.join(folder, library_name) + yield library_name + yield find_library(library_name.split('.')[0]) + else: + for name in library_name: + if folder: + yield os.path.join(folder, name) + for name in library_name: + yield name + yield find_library(name.split('.')[0]) From 90dff2697ce1ebf7556622ad763385db222674cc Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 14 Dec 2015 13:30:59 -0600 Subject: [PATCH 27/61] Update log.py --- lantz/log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lantz/log.py b/lantz/log.py index aec47e3..7eaf20a 100644 --- a/lantz/log.py +++ b/lantz/log.py @@ -23,6 +23,7 @@ from socketserver import (ThreadingUDPServer, DatagramRequestHandler, ThreadingTCPServer, StreamRequestHandler) +from colorama import Fore, Back, Style from stringparser import Parser From a86768c660d2cee3155a122eca03eb6b4d5948a7 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 14 Dec 2015 17:27:58 -0600 Subject: [PATCH 28/61] more fun with the DAQ added some more fancy DAQ test code, including live plotting w/ matplotlib --- LantzMods.md | 6 +----- NI/DAQ Installation.md | 2 ++ NI/DAQ_blit.py | 31 +++++++++++++++++++++++++++++++ NI/DAQ_many.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 NI/DAQ_blit.py create mode 100644 NI/DAQ_many.py diff --git a/LantzMods.md b/LantzMods.md index a21d131..4b92ad1 100644 --- a/LantzMods.md +++ b/LantzMods.md @@ -15,8 +15,4 @@ TODO: put in a formal push to fix this in the main distribution. ## Changes to NIDAQ driver ## -Still working on a bug fix for this, but can tell what's going wrong. - -Basically the GetTaskHandle() call fails because it truncates the TaskHandle to be a 32-bit integer, when the true value is 64-bit. - -If anyone has an idea of how to fix this, that would be great. +See the folder NI/DAQ Installation.md for details. diff --git a/NI/DAQ Installation.md b/NI/DAQ Installation.md index 5a31acd..0f93ade 100644 --- a/NI/DAQ Installation.md +++ b/NI/DAQ Installation.md @@ -40,5 +40,7 @@ python DAQ_demo.py 'dev1/ai0' ``` where 'dev1' is your DAQ device name and 'ai0' is the analog input channel you want to read. +`DAQ_blit.py` is a demo of sampling from the DAQ and live plotting the data w/ matplotlib. + ## More Advanced Testing ## Currently working on a more advanced test script to demonstrate some more advanced features of the DAQ. diff --git a/NI/DAQ_blit.py b/NI/DAQ_blit.py new file mode 100644 index 0000000..99fe552 --- /dev/null +++ b/NI/DAQ_blit.py @@ -0,0 +1,31 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import time +from numpy import arange +from numpy.random import rand + +from lantz.drivers.ni.daqmx import AnalogInputTask, Task, DigitalInputTask +from lantz.drivers.ni.daqmx import VoltageInputChannel +task = AnalogInputTask('test') +task.add_channel(VoltageInputChannel('dev1/ai0')) +task.start() + +fig = plt.figure() +ax1 = fig.add_subplot(1, 1, 1) +t0 = time.clock() +xar = [] +yar = [] + +def animate(i): + xar.append(time.clock()-t0) + yar.append(task.read_scalar()) + ax1.clear() + ax1.plot(xar, yar, 'o') + plt.xlabel('Time (s)') + plt.ylabel('Voltage (V)') +ani = animation.FuncAnimation(fig, animate, interval=10) + +plt.show() + + +task.stop() diff --git a/NI/DAQ_many.py b/NI/DAQ_many.py new file mode 100644 index 0000000..bc89803 --- /dev/null +++ b/NI/DAQ_many.py @@ -0,0 +1,38 @@ +import matplotlib.pyplot as plt +import time +import random +from collections import deque +import numpy as np + +from lantz.drivers.ni.daqmx import AnalogInputTask, Task, DigitalInputTask +from lantz.drivers.ni.daqmx import VoltageInputChannel +# simulates input from serial port +def next_V_pt(task): + while True: + val = task.read_scalar() + yield val + + +task = AnalogInputTask('test') +task.add_channel(VoltageInputChannel('dev1/ai0')) +task.start() + +a1 = deque([0]*1000) +ax = plt.axes(xlim=(0, 1000), ylim=(-1, 1)) +d = next_V_pt(task) + +line, = plt.plot(a1) +plt.ion() +plt.ylim([-1, 1]) +plt.show() + +task.stop() + +for i in range(0, 1000): + a1.appendleft(next(d)) + datatoplot = a1.pop() + line.set_ydata(a1) + plt.draw() + i += 1 + time.sleep(0.1) + plt.pause(0.0001) From e6d0caca6afc59dcd486c513e7a8f0cc86446a19 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 12:19:03 -0600 Subject: [PATCH 29/61] adding changes to NI Driver Fixed some issues w/ lantz NI driver, added the ability to create AI voltage channel Also added a fun demo where you can plot a voltage input of the DAQ in real time with an animated matplotlibplot --- Acton SP2150i/SP2150i.py | 115 ++++++++++++++++++++++++++++++++++++ Acton_SP2150i_Install.md | 5 -- NI/DAQ_FSM_demo.py | 123 +++++++++++++++++++++++++++++++++++++++ NI/DAQ_blit.py | 1 - NI/NI64/base.py | 3 - NI/NI64/channels.py | 22 ++++--- 6 files changed, 252 insertions(+), 17 deletions(-) create mode 100644 Acton SP2150i/SP2150i.py delete mode 100644 Acton_SP2150i_Install.md create mode 100644 NI/DAQ_FSM_demo.py diff --git a/Acton SP2150i/SP2150i.py b/Acton SP2150i/SP2150i.py new file mode 100644 index 0000000..97bfe7b --- /dev/null +++ b/Acton SP2150i/SP2150i.py @@ -0,0 +1,115 @@ +from lantz import Feat, DictFeat, Action +from lantz.messagebased import MessageBasedDriver + +from time import sleep + + +class SP2150i(MessageBasedDriver): + """ + + """ + MANUFACTURER_ID = '0x0647' + MODEL_CODE = '0x0100' + + DEFAULTS = {'COMMON': {'write_termination': '\r', + 'read_termination': ''}} + + max_speed = 100 + wavelength_min = 380 + wavelength_max = 520 + + def clear_buffer(self): + """ + Clears buffer to avoid issues w/ commands being stuck in buffer. + """ + return 0 + + @Feat(limits=(0, max_speed)) + def scan_speed(self): + """ + Get scan rate in nm/min. + """ + return self.query('?NM/MIN') + + @scan_speed.setter + def scan_speed(self, speed): + """ + Sets current scan speed in nm/min. + """ + self.clear_buffer() + read = self.query('{} NM/MIN'.format(speed)) + read2 = read.replace('nm/min ok', '') + print('nm/min read:' + read2) + sleep(1) + return read + + @Feat(limits=(wavelength_min, wavelength_max)) + def nm(self): + """ + """ + print('Sending ?NM...') + read = self.query('?NM') + read = read.replace('nm ok', '') + read = read.replace('1` ', '') + return float(read) + + @nm.setter + def nm(self, wavelength): + """ + Sets output to specified wavelength, traveling at the current scan + rate. + """ + return self.query('{} NM'.format(wavelength)) + + @Feat(limits=(1, 2, 1)) + def grating(self): + """ + Returns the current grating position + """ + return int(self.query('?GRATING')) + + @grating.setter + def grating(self, grating_num): + """ + Sets the current grating to be grating_num + """ + print('Warning: will wait 20 seconds to change grating.') + self.query('{} GRATING'.format(grating_num)) + sleep(20) + + @Feat(limits=(1, 3, 1)) + def turret(self): + """ + Returns the selected turret number. + """ + return int(self.query('?TURRET')) + + @turret.setter + def turret(self, turr_set): + """ + Selects the parameters for the grating on turret turr_set + """ + return self.query('{} TURRET'.format(turr_set)) + + @Feat() + def turret_spacing(self): + """ + Returns the groove spacing of the grating for each turret. + """ + return self.query('?TURRETS') + + @Feat() + def grating_settings(self): + """ + Returns the groove spacing and blaze wavelength of grating positions + 1-6. This corresponds to 2 grating positions for each of the 3 turrets + """ + return self.query('?GRATINGS') + + +if __name__ == '__main__': + with SP2150i('USB0::0x0647::0x0100::NI-VISA-120001::RAW') as inst: + print('Testing 1, 2, 3') + print('Wavelength: {}'.format(inst.nm)) + print('Scan rate: {}'.format(inst.scan_speed)) + # inst.nm = 400.0 diff --git a/Acton_SP2150i_Install.md b/Acton_SP2150i_Install.md deleted file mode 100644 index 766bb50..0000000 --- a/Acton_SP2150i_Install.md +++ /dev/null @@ -1,5 +0,0 @@ -# Acton SP2150i Install Guide # -Author: Peter Mintun -Date: 12/13/2015 - -Pro tip: use http://juluribk.com/2014/09/19/controlling-sp2150i-monochromator-with-pythonpyvisa/ diff --git a/NI/DAQ_FSM_demo.py b/NI/DAQ_FSM_demo.py new file mode 100644 index 0000000..0be4e35 --- /dev/null +++ b/NI/DAQ_FSM_demo.py @@ -0,0 +1,123 @@ +# Demo code to control Newport FSM, based off of original by David J. Christle +# Original code here: https://github.com/dchristle/qtlab/blob/master/instrument_plugins/Newport_FSM.py +from lantz.drivers.ni.daqmx import System, AnalogOutputTask, VoltageOutputChannel + +from lantz import Feat, DictFeat, Action + +class Newport_FSM(System): + """ + Class for controlling Newport FSM using National Instruments DAQ. + """ + + + + def initialize(self): + """ + Creates AO tasks for controlling FSM. + """ + self.task = AnalogOutputTask('FSM') + + self.dev_name = 'dev1/' + + self.fsm_dimensions = { + 'x': {'micron_per_volt': 9.5768, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao0'}, + 'y': {'micron_per_volt': 7.1759, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao1'} + } + + for dim in self.fsm_dimensions: + chan_name = dim + phys_chan = self.dev_name + self.fsm_dimensions[dim]['ao_channel'] + V_min = self.fsm_dimensions[dim]['min_v'] + V_max = self.fsm_dimensions[dim]['max_v'] + print(phys_chan) + + print('Creating voltage output channel') + print(V_min) + print(V_max) + + chan = VoltageOutputChannel(phys_chan, name=chan_name, + min_max=(V_min, V_max), units='volts', + task=self.task) + self.task.start() + print('Started task!') + + def finalize(self): + """ + Stops AO tasks for controlling FSM. + """ + + + @Feat() + def abs_position_pair(self): + """ + Absolute position of scanning + """ + return 0 + + @abs_position_pair.setter + def abs_position_pair(self, xy_pos): + """ + Sets absolute position of scanning mirror, as a micron pair. + """ + return 0 + + @DictFeat() + def abs_position_single(self, dimension): + """ + Returns absolute position of scanning mirror along dimension. + """ + return 0 + + @abs_position_single.setter + def abs_position_single(self, dimension, pos): + """ + Sets absolute position of scanning mirror along dimension to be pos. + """ + return 0 + + + @Feat() + def scan_speed(self): + """ + Returns current FSM scan speed in V/s. + """ + return 0 + + @scan_speed.setter + def scan_speed(self, speed): + """ + Sets the current scan speed of the analog output in V/s. + """ + return 0 + + def convert_V_to_um(self, dimension, V): + """ + Returns micron position corresponding to channel voltage. + """ + return 0 + + def convert_um_to_V(self, dimension, um): + """ + Returns voltage corresponding to micron position. + """ + return 0 + + def set_V_to_zero(self): + """ + Sets output voltages to 0. + """ + + +if __name__ == '__main__': + with Newport_FSM() as inst: + print('testing!') diff --git a/NI/DAQ_blit.py b/NI/DAQ_blit.py index 99fe552..c20454a 100644 --- a/NI/DAQ_blit.py +++ b/NI/DAQ_blit.py @@ -27,5 +27,4 @@ def animate(i): plt.show() - task.stop() diff --git a/NI/NI64/base.py b/NI/NI64/base.py index 457cddc..1b7383e 100644 --- a/NI/NI64/base.py +++ b/NI/NI64/base.py @@ -15,9 +15,6 @@ from lantz.foreign import LibraryDriver, RetValue, RetStr from .constants import Constants, Types -from ctypes import cast - -from time import sleep default_buf_size = 2048 diff --git a/NI/NI64/channels.py b/NI/NI64/channels.py index 3a19073..06654a4 100644 --- a/NI/NI64/channels.py +++ b/NI/NI64/channels.py @@ -73,11 +73,11 @@ class VoltageInputChannel(Channel): def __init__(self, phys_channel, name='', terminal='default', min_max=(-10., 10.), units='volts', task=None): + terminal_val = self.terminal_map[terminal] + if not name: name = ''#phys_channel - terminal_val = self.terminal_map[terminal] - if units != 'volts': custom_scale_name = units units = Constants.Val_FromCustomScale @@ -100,19 +100,25 @@ class VoltageOutputChannel(Channel): CHANNEL_TYPE = 'AO' - def __init__(self, phys_channel, channel_name='', terminal='default', min_max=(-1, -1), units='volts'): + CREATE_FUN = 'CreateAOVoltageChan' - terminal_val = self.terminal_map[terminal] + def __init__(self, phys_channel, name='', min_max=(-10., 10.), + units='volts', task=None): + + if not name: + name = '' # phys_channel if units != 'volts': custom_scale_name = units - units = Constants.FROM_CUSTOM_SCALE + units = Constants.Val_FromCustomScale else: custom_scale_name = None - units = Constants.VOLTS + units = Constants.Val_Volts - err = self.lib.CreateAOVoltageChan(phys_channel, channel_name, - min_max[0], min_max[1], units, custom_scale_name) + self._create_args = (phys_channel, name, min_max[0], min_max[1], units, + custom_scale_name) + + super().__init__(task=task, name=name) # Not implemented: # DAQmxCreateAIAccelChan, DAQmxCreateAICurrentChan, DAQmxCreateAIFreqVoltageChan, From c3ce9b7b7a3b2f03bc36ecb1a9c16c8ef5d010f2 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 15:34:38 -0600 Subject: [PATCH 30/61] fixing DAQ write/read methods added number_of_channels to Task class, this function was missing for some reason. also added some code to demo writing from AO on multiple channels as a prelude towards a full on FSM driver. --- Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf | Bin 0 -> 8200 bytes Acton SP2150i/Acton_SP2150i_Install.md | 5 + Acton SP2150i/amd64/libusb0.sys | Bin 0 -> 52832 bytes Acton SP2150i/ia64/libusb0.sys | Bin 0 -> 110176 bytes .../license/libusb0/installer_license.txt | 851 ++++++++++++++++++ Acton SP2150i/x86/libusb0.sys | Bin 0 -> 42592 bytes Lakeshore332.py | 4 +- NI/DAQ_FSM_demo.py | 101 ++- NI/DAQ_double_blit.py | 34 + NI/NI64/base.py | 7 + NI/NI64/channels.py | 2 + NI/NI64/tasks.py | 6 +- Old code/SR7265.py | 656 ++++++++++++++ .../qtlab_Lakeshore332.py | 0 SP2150i.py | 108 --- __pycache__/SignalRecovery7265.cpython-35.pyc | Bin 0 -> 7047 bytes 16 files changed, 1628 insertions(+), 146 deletions(-) create mode 100644 Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf create mode 100644 Acton SP2150i/Acton_SP2150i_Install.md create mode 100644 Acton SP2150i/amd64/libusb0.sys create mode 100644 Acton SP2150i/ia64/libusb0.sys create mode 100644 Acton SP2150i/license/libusb0/installer_license.txt create mode 100644 Acton SP2150i/x86/libusb0.sys create mode 100644 NI/DAQ_double_blit.py create mode 100644 Old code/SR7265.py rename qtlab_Lakeshore332.py => Old code/qtlab_Lakeshore332.py (100%) delete mode 100644 SP2150i.py create mode 100644 __pycache__/SignalRecovery7265.cpython-35.pyc diff --git a/Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf b/Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf new file mode 100644 index 0000000000000000000000000000000000000000..ac551f824e0cdcf4fdf9253c4ffa0a9f9d53afed GIT binary patch literal 8200 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 literal 0 HcmV?d00001 diff --git a/Acton SP2150i/license/libusb0/installer_license.txt b/Acton SP2150i/license/libusb0/installer_license.txt new file mode 100644 index 0000000..56bb2cd --- /dev/null +++ b/Acton SP2150i/license/libusb0/installer_license.txt @@ -0,0 +1,851 @@ +Copyright (c) 2002-2004 Stephan Meyer, +Copyright (c) 2000-2004 Johannes Erdfelt, +Copyright (c) 2000-2004 Thomas Sailer, +Copyright (c) 2010 Travis Robinson, + +This software is distributed under the following licenses: +Driver: GNU General Public License (GPL) +Library, Test Files, Installer: GNU Lesser General Public License (LGPL) + +*********************************************************************** + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/Acton SP2150i/x86/libusb0.sys b/Acton SP2150i/x86/libusb0.sys new file mode 100644 index 0000000000000000000000000000000000000000..5322e5b978a964f0fb601ebb75fae4aae34bbd87 GIT binary patch literal 42592 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 WA4Cd1_&tGd^|-BALmt1R9{&dsiV_F_ literal 0 HcmV?d00001 From 614fd4d186ca5ddb4f541c1cbdcb41c68be6d6c0 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 15:34:59 -0600 Subject: [PATCH 31/61] Revert "fixing DAQ write/read methods" This reverts commit c3ce9b7b7a3b2f03bc36ecb1a9c16c8ef5d010f2. --- Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf | Bin 8200 -> 0 bytes Acton SP2150i/Acton_SP2150i_Install.md | 5 - Acton SP2150i/amd64/libusb0.sys | Bin 52832 -> 0 bytes Acton SP2150i/ia64/libusb0.sys | Bin 110176 -> 0 bytes .../license/libusb0/installer_license.txt | 851 ------------------ Acton SP2150i/x86/libusb0.sys | Bin 42592 -> 0 bytes Lakeshore332.py | 4 +- NI/DAQ_FSM_demo.py | 101 +-- NI/DAQ_double_blit.py | 34 - NI/NI64/base.py | 7 - NI/NI64/channels.py | 2 - NI/NI64/tasks.py | 6 +- Old code/SR7265.py | 656 -------------- SP2150i.py | 108 +++ __pycache__/SignalRecovery7265.cpython-35.pyc | Bin 7047 -> 0 bytes ...b_Lakeshore332.py => qtlab_Lakeshore332.py | 0 16 files changed, 146 insertions(+), 1628 deletions(-) delete mode 100644 Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf delete mode 100644 Acton SP2150i/Acton_SP2150i_Install.md delete mode 100644 Acton SP2150i/amd64/libusb0.sys delete mode 100644 Acton SP2150i/ia64/libusb0.sys delete mode 100644 Acton SP2150i/license/libusb0/installer_license.txt delete mode 100644 Acton SP2150i/x86/libusb0.sys delete mode 100644 NI/DAQ_double_blit.py delete mode 100644 Old code/SR7265.py create mode 100644 SP2150i.py delete mode 100644 __pycache__/SignalRecovery7265.cpython-35.pyc rename Old code/qtlab_Lakeshore332.py => qtlab_Lakeshore332.py (100%) diff --git a/Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf b/Acton SP2150i/ACTON_RESEARCH_CONTROLLER.inf deleted file mode 100644 index ac551f824e0cdcf4fdf9253c4ffa0a9f9d53afed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8200 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 diff --git a/Acton SP2150i/license/libusb0/installer_license.txt b/Acton SP2150i/license/libusb0/installer_license.txt deleted file mode 100644 index 56bb2cd..0000000 --- a/Acton SP2150i/license/libusb0/installer_license.txt +++ /dev/null @@ -1,851 +0,0 @@ -Copyright (c) 2002-2004 Stephan Meyer, -Copyright (c) 2000-2004 Johannes Erdfelt, -Copyright (c) 2000-2004 Thomas Sailer, -Copyright (c) 2010 Travis Robinson, - -This software is distributed under the following licenses: -Driver: GNU General Public License (GPL) -Library, Test Files, Installer: GNU Lesser General Public License (LGPL) - -*********************************************************************** - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. - diff --git a/Acton SP2150i/x86/libusb0.sys b/Acton SP2150i/x86/libusb0.sys deleted file mode 100644 index 5322e5b978a964f0fb601ebb75fae4aae34bbd87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42592 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 WA4Cd1_&tGd^|-BALmt1R9{&dsiV_F_ diff --git a/Old code/qtlab_Lakeshore332.py b/qtlab_Lakeshore332.py similarity index 100% rename from Old code/qtlab_Lakeshore332.py rename to qtlab_Lakeshore332.py From adcabd5f87776afec7495b1edfdc409b09c7c273 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 15:39:00 -0600 Subject: [PATCH 32/61] Revert "adding changes to NI Driver" This reverts commit e6d0caca6afc59dcd486c513e7a8f0cc86446a19. --- Acton SP2150i/SP2150i.py | 115 ------------------------------------ Acton_SP2150i_Install.md | 5 ++ NI/DAQ_FSM_demo.py | 123 --------------------------------------- NI/DAQ_blit.py | 1 + NI/NI64/base.py | 3 + NI/NI64/channels.py | 22 +++---- 6 files changed, 17 insertions(+), 252 deletions(-) delete mode 100644 Acton SP2150i/SP2150i.py create mode 100644 Acton_SP2150i_Install.md delete mode 100644 NI/DAQ_FSM_demo.py diff --git a/Acton SP2150i/SP2150i.py b/Acton SP2150i/SP2150i.py deleted file mode 100644 index 97bfe7b..0000000 --- a/Acton SP2150i/SP2150i.py +++ /dev/null @@ -1,115 +0,0 @@ -from lantz import Feat, DictFeat, Action -from lantz.messagebased import MessageBasedDriver - -from time import sleep - - -class SP2150i(MessageBasedDriver): - """ - - """ - MANUFACTURER_ID = '0x0647' - MODEL_CODE = '0x0100' - - DEFAULTS = {'COMMON': {'write_termination': '\r', - 'read_termination': ''}} - - max_speed = 100 - wavelength_min = 380 - wavelength_max = 520 - - def clear_buffer(self): - """ - Clears buffer to avoid issues w/ commands being stuck in buffer. - """ - return 0 - - @Feat(limits=(0, max_speed)) - def scan_speed(self): - """ - Get scan rate in nm/min. - """ - return self.query('?NM/MIN') - - @scan_speed.setter - def scan_speed(self, speed): - """ - Sets current scan speed in nm/min. - """ - self.clear_buffer() - read = self.query('{} NM/MIN'.format(speed)) - read2 = read.replace('nm/min ok', '') - print('nm/min read:' + read2) - sleep(1) - return read - - @Feat(limits=(wavelength_min, wavelength_max)) - def nm(self): - """ - """ - print('Sending ?NM...') - read = self.query('?NM') - read = read.replace('nm ok', '') - read = read.replace('1` ', '') - return float(read) - - @nm.setter - def nm(self, wavelength): - """ - Sets output to specified wavelength, traveling at the current scan - rate. - """ - return self.query('{} NM'.format(wavelength)) - - @Feat(limits=(1, 2, 1)) - def grating(self): - """ - Returns the current grating position - """ - return int(self.query('?GRATING')) - - @grating.setter - def grating(self, grating_num): - """ - Sets the current grating to be grating_num - """ - print('Warning: will wait 20 seconds to change grating.') - self.query('{} GRATING'.format(grating_num)) - sleep(20) - - @Feat(limits=(1, 3, 1)) - def turret(self): - """ - Returns the selected turret number. - """ - return int(self.query('?TURRET')) - - @turret.setter - def turret(self, turr_set): - """ - Selects the parameters for the grating on turret turr_set - """ - return self.query('{} TURRET'.format(turr_set)) - - @Feat() - def turret_spacing(self): - """ - Returns the groove spacing of the grating for each turret. - """ - return self.query('?TURRETS') - - @Feat() - def grating_settings(self): - """ - Returns the groove spacing and blaze wavelength of grating positions - 1-6. This corresponds to 2 grating positions for each of the 3 turrets - """ - return self.query('?GRATINGS') - - -if __name__ == '__main__': - with SP2150i('USB0::0x0647::0x0100::NI-VISA-120001::RAW') as inst: - print('Testing 1, 2, 3') - print('Wavelength: {}'.format(inst.nm)) - print('Scan rate: {}'.format(inst.scan_speed)) - # inst.nm = 400.0 diff --git a/Acton_SP2150i_Install.md b/Acton_SP2150i_Install.md new file mode 100644 index 0000000..766bb50 --- /dev/null +++ b/Acton_SP2150i_Install.md @@ -0,0 +1,5 @@ +# Acton SP2150i Install Guide # +Author: Peter Mintun +Date: 12/13/2015 + +Pro tip: use http://juluribk.com/2014/09/19/controlling-sp2150i-monochromator-with-pythonpyvisa/ diff --git a/NI/DAQ_FSM_demo.py b/NI/DAQ_FSM_demo.py deleted file mode 100644 index 0be4e35..0000000 --- a/NI/DAQ_FSM_demo.py +++ /dev/null @@ -1,123 +0,0 @@ -# Demo code to control Newport FSM, based off of original by David J. Christle -# Original code here: https://github.com/dchristle/qtlab/blob/master/instrument_plugins/Newport_FSM.py -from lantz.drivers.ni.daqmx import System, AnalogOutputTask, VoltageOutputChannel - -from lantz import Feat, DictFeat, Action - -class Newport_FSM(System): - """ - Class for controlling Newport FSM using National Instruments DAQ. - """ - - - - def initialize(self): - """ - Creates AO tasks for controlling FSM. - """ - self.task = AnalogOutputTask('FSM') - - self.dev_name = 'dev1/' - - self.fsm_dimensions = { - 'x': {'micron_per_volt': 9.5768, - 'min_v': -10.0, - 'max_v': +10.0, - 'default': 0.0, - 'origin': 0.0, - 'ao_channel': 'ao0'}, - 'y': {'micron_per_volt': 7.1759, - 'min_v': -10.0, - 'max_v': +10.0, - 'default': 0.0, - 'origin': 0.0, - 'ao_channel': 'ao1'} - } - - for dim in self.fsm_dimensions: - chan_name = dim - phys_chan = self.dev_name + self.fsm_dimensions[dim]['ao_channel'] - V_min = self.fsm_dimensions[dim]['min_v'] - V_max = self.fsm_dimensions[dim]['max_v'] - print(phys_chan) - - print('Creating voltage output channel') - print(V_min) - print(V_max) - - chan = VoltageOutputChannel(phys_chan, name=chan_name, - min_max=(V_min, V_max), units='volts', - task=self.task) - self.task.start() - print('Started task!') - - def finalize(self): - """ - Stops AO tasks for controlling FSM. - """ - - - @Feat() - def abs_position_pair(self): - """ - Absolute position of scanning - """ - return 0 - - @abs_position_pair.setter - def abs_position_pair(self, xy_pos): - """ - Sets absolute position of scanning mirror, as a micron pair. - """ - return 0 - - @DictFeat() - def abs_position_single(self, dimension): - """ - Returns absolute position of scanning mirror along dimension. - """ - return 0 - - @abs_position_single.setter - def abs_position_single(self, dimension, pos): - """ - Sets absolute position of scanning mirror along dimension to be pos. - """ - return 0 - - - @Feat() - def scan_speed(self): - """ - Returns current FSM scan speed in V/s. - """ - return 0 - - @scan_speed.setter - def scan_speed(self, speed): - """ - Sets the current scan speed of the analog output in V/s. - """ - return 0 - - def convert_V_to_um(self, dimension, V): - """ - Returns micron position corresponding to channel voltage. - """ - return 0 - - def convert_um_to_V(self, dimension, um): - """ - Returns voltage corresponding to micron position. - """ - return 0 - - def set_V_to_zero(self): - """ - Sets output voltages to 0. - """ - - -if __name__ == '__main__': - with Newport_FSM() as inst: - print('testing!') diff --git a/NI/DAQ_blit.py b/NI/DAQ_blit.py index c20454a..99fe552 100644 --- a/NI/DAQ_blit.py +++ b/NI/DAQ_blit.py @@ -27,4 +27,5 @@ def animate(i): plt.show() + task.stop() diff --git a/NI/NI64/base.py b/NI/NI64/base.py index 1b7383e..457cddc 100644 --- a/NI/NI64/base.py +++ b/NI/NI64/base.py @@ -15,6 +15,9 @@ from lantz.foreign import LibraryDriver, RetValue, RetStr from .constants import Constants, Types +from ctypes import cast + +from time import sleep default_buf_size = 2048 diff --git a/NI/NI64/channels.py b/NI/NI64/channels.py index 06654a4..3a19073 100644 --- a/NI/NI64/channels.py +++ b/NI/NI64/channels.py @@ -73,11 +73,11 @@ class VoltageInputChannel(Channel): def __init__(self, phys_channel, name='', terminal='default', min_max=(-10., 10.), units='volts', task=None): - terminal_val = self.terminal_map[terminal] - if not name: name = ''#phys_channel + terminal_val = self.terminal_map[terminal] + if units != 'volts': custom_scale_name = units units = Constants.Val_FromCustomScale @@ -100,25 +100,19 @@ class VoltageOutputChannel(Channel): CHANNEL_TYPE = 'AO' - CREATE_FUN = 'CreateAOVoltageChan' - - def __init__(self, phys_channel, name='', min_max=(-10., 10.), - units='volts', task=None): + def __init__(self, phys_channel, channel_name='', terminal='default', min_max=(-1, -1), units='volts'): - if not name: - name = '' # phys_channel + terminal_val = self.terminal_map[terminal] if units != 'volts': custom_scale_name = units - units = Constants.Val_FromCustomScale + units = Constants.FROM_CUSTOM_SCALE else: custom_scale_name = None - units = Constants.Val_Volts + units = Constants.VOLTS - self._create_args = (phys_channel, name, min_max[0], min_max[1], units, - custom_scale_name) - - super().__init__(task=task, name=name) + err = self.lib.CreateAOVoltageChan(phys_channel, channel_name, + min_max[0], min_max[1], units, custom_scale_name) # Not implemented: # DAQmxCreateAIAccelChan, DAQmxCreateAICurrentChan, DAQmxCreateAIFreqVoltageChan, From a5883e131a2cfb015bf1fc46012b1d48710cbcda Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 15:49:28 -0600 Subject: [PATCH 33/61] finally fixing chaos reverting all changes and making everything happy again --- NI/DAQ_FSM_Demo.py | 156 ++++++++++++++++++++++++++++++++++++++++++ NI/DAQ_double_blit.py | 34 +++++++++ NI/NI64/base.py | 10 ++- NI/NI64/channels.py | 24 ++++--- NI/NI64/tasks.py | 6 +- 5 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 NI/DAQ_FSM_Demo.py create mode 100644 NI/DAQ_double_blit.py diff --git a/NI/DAQ_FSM_Demo.py b/NI/DAQ_FSM_Demo.py new file mode 100644 index 0000000..5e83894 --- /dev/null +++ b/NI/DAQ_FSM_Demo.py @@ -0,0 +1,156 @@ +# Demo code to control Newport FSM, based off of original by David J. Christle +# Original code here: +# https://github.com/dchristle/qtlab/blob/master/instrument_plugins/Newport_FSM.py + +from lantz.drivers.ni.daqmx import System +from lantz.drivers.ni.daqmx import AnalogOutputTask, VoltageOutputChannel + +from lantz import Feat, DictFeat, Action + +from numpy import abs, ceil, pi, linspace, cos, ones + +class Newport_FSM(System): + """ + Class for controlling Newport FSM using National Instruments DAQ. + """ + + def initialize(self): + """ + Creates AO tasks for controlling FSM. + """ + self.task = AnalogOutputTask('FSM') + + self.dev_name = 'dev1/' + + self.fsm_dimensions = { + 'x': {'micron_per_volt': 9.5768, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao0'}, + 'y': {'micron_per_volt': 7.1759, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao1'} + } + + for dim in self.fsm_dimensions: + chan_name = dim + phys_chan = self.dev_name + self.fsm_dimensions[dim]['ao_channel'] + V_min = self.fsm_dimensions[dim]['min_v'] + V_max = self.fsm_dimensions[dim]['max_v'] + + chan = VoltageOutputChannel(phys_chan, name=chan_name, + min_max=(V_min, V_max), units='volts', + task=self.task) + self.task.start() + + from numpy.random import randn + + data = randn(1000, 1000) + + self.task.write(data, auto_start=False, timeout=10.0, group_by='scan') + + print('Task started!') + + def finalize(self): + """ + Stops AO tasks for controlling FSM. + """ + self.task.stop() + + @Feat() + def abs_position_pair(self): + x_loc = abs_position_single['x'] + y_loc = abs_position_single['y'] + print('FSM at ({}um,{}um)'.format(x_loc, y_loc)) + return [x_loc, y_loc] + + @abs_position_pair.setter + def abs_position_pair(self, xy_pos): + """ + Sets absolute position of scanning mirror, as a micron pair. + """ + abs_position_single['x'] = xy_pos[0] + abs_position_single['y'] = xy_pos[1] + print('Set FSM to ({}um,{}um).'.format(xy_pos[0], xy_pos[1])) + return 0 + + @DictFeat() + def abs_position_single(self, dimension): + """ + Returns absolute position of scanning mirror along dimension. + """ + return 0 + + @abs_position_single.setter + def abs_position_single(self, dimension, pos): + """ + Sets absolute position of scanning mirror along dimension to be pos. + """ + return 0 + + # @Feat() + # def scan_speed(self): + # """ + # Returns current FSM scan speed in V/s. + # """ + # print('Bold faced lie') + # return 0 + # + # @scan_speed.setter + # def scan_speed(self, speed): + # """ + # Sets the current scan speed of the analog output in V/s. + # """ + # print('Bold faced lie') + # return 0 + + def convert_V_to_um(self, dime, V): + """ + Returns micron position corresponding to channel voltage. + """ + um = V*self.fsm_dimensions[dim]['micron_per_volt'] + ( + self.fsm_dimensions[dim]['origin']) + return um + + def convert_um_to_V(self, dim, um): + """ + Returns voltage corresponding to micron position. + """ + V = (um - self.fsm_dimensions[dim]['origin'])/( + self.fsm_dimensions[dim]['micron_per_volt']) + return V + + def set_V_to_zero(self): + """ + Sets output voltages to 0. + """ + + @Action() + def ao_smooth(self, x_init, x_final, channel): + """ + Smooths output of DAQ to avoid hysteresis w/ moving FSM mirror. + + Copy of dchristle's algorithm. (I know what you're trying to do, and + just stop.) + """ + ao_smooth_rate = 50000.0 # Hz + ao_smooth_steps_per_volt = 1000.0 # steps/V + + v_init = self.convert_um_to_V(x_init, channel) + v_final = self.convert_un_to_V(x_final, channel) + + n_steps = ceil(abs(v_final-v_init)*ao_smooth_steps_per_volt) + + v_data = v_init*ones(n_steps) + (v_final - v_init) * (1.0 - cos( + linspace(0, pi, n_steps)/2.0)) + + print(v_data) + +if __name__ == '__main__': + with Newport_FSM() as inst: + print('testing!') diff --git a/NI/DAQ_double_blit.py b/NI/DAQ_double_blit.py new file mode 100644 index 0000000..8bac64a --- /dev/null +++ b/NI/DAQ_double_blit.py @@ -0,0 +1,34 @@ +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import time +from numpy import arange +from numpy.random import rand + +from lantz.drivers.ni.daqmx import AnalogInputTask, Task, DigitalInputTask +from lantz.drivers.ni.daqmx import VoltageInputChannel +task = AnalogInputTask('test') +task.add_channel(VoltageInputChannel('dev1/ai0')) +task.add_channel(VoltageInputChannel('dev1/ai1')) +task.start() + +fig = plt.figure() +ax1 = fig.add_subplot(1, 1, 1) +t0 = time.clock() +xar = [] +yar0 = [] +yar1 = [] +print(task.read(samples_per_channel=1, timeout=10.0, group_by='scan')) + +# def animate(i): +# xar.append(time.clock()-t0) +# yar0.append(task.read) +# ax1.clear() +# ax1.plot(xar, yar0, 'bo') +# ax1.plot(xar, yar1, 'ro') +# plt.xlabel('Time (s)') +# plt.ylabel('Voltage (V)') +# ani = animation.FuncAnimation(fig, animate, interval=10) +# +# plt.show() +# +# task.stop() diff --git a/NI/NI64/base.py b/NI/NI64/base.py index 457cddc..9c39389 100644 --- a/NI/NI64/base.py +++ b/NI/NI64/base.py @@ -15,9 +15,6 @@ from lantz.foreign import LibraryDriver, RetValue, RetStr from .constants import Constants, Types -from ctypes import cast - -from time import sleep default_buf_size = 2048 @@ -425,6 +422,13 @@ def _channel_names(self): names = tuple(n.strip() for n in buf.split(',') if n.strip()) return names + def number_of_channels(self): + """Returns the number of virtual channels in the task. + """ + err, buf = self.lib.GetTaskChannels(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return len(names) + def _device_names(self): """Return a tuple with the names of all devices in the task. """ diff --git a/NI/NI64/channels.py b/NI/NI64/channels.py index 3a19073..754452c 100644 --- a/NI/NI64/channels.py +++ b/NI/NI64/channels.py @@ -73,11 +73,11 @@ class VoltageInputChannel(Channel): def __init__(self, phys_channel, name='', terminal='default', min_max=(-10., 10.), units='volts', task=None): + terminal_val = self.terminal_map[terminal] + if not name: name = ''#phys_channel - terminal_val = self.terminal_map[terminal] - if units != 'volts': custom_scale_name = units units = Constants.Val_FromCustomScale @@ -100,19 +100,27 @@ class VoltageOutputChannel(Channel): CHANNEL_TYPE = 'AO' - def __init__(self, phys_channel, channel_name='', terminal='default', min_max=(-1, -1), units='volts'): + CREATE_FUN = 'CreateAOVoltageChan' - terminal_val = self.terminal_map[terminal] + def __init__(self, phys_channel, name='', min_max=(-10., 10.), + units='volts', task=None): + + if not name: + name = '' # phys_channel if units != 'volts': custom_scale_name = units - units = Constants.FROM_CUSTOM_SCALE + units = Constants.Val_FromCustomScale else: custom_scale_name = None - units = Constants.VOLTS + units = Constants.Val_Volts + + self._create_args = (phys_channel, name, min_max[0], min_max[1], units, + custom_scale_name) + + super().__init__(task=task, name=name) + - err = self.lib.CreateAOVoltageChan(phys_channel, channel_name, - min_max[0], min_max[1], units, custom_scale_name) # Not implemented: # DAQmxCreateAIAccelChan, DAQmxCreateAICurrentChan, DAQmxCreateAIFreqVoltageChan, diff --git a/NI/NI64/tasks.py b/NI/NI64/tasks.py index eddfc73..aeebe7b 100644 --- a/NI/NI64/tasks.py +++ b/NI/NI64/tasks.py @@ -149,9 +149,11 @@ class AnalogOutputTask(Task): CHANNEL_TYPE = 'AO' + @Action(units=(None, None, 'seconds', None), values=(None, None, None, _GROUP_BY)) def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): - """Write multiple floating-point samples or a scalar to a task + """ + Write multiple floating-point samples or a scalar to a task that contains one or more analog output channels. Note: If you configured timing for your task, your write is @@ -200,7 +202,7 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): number_of_channels = self.number_of_channels() - if data.ndims == 1: + if data.ndim == 1: if number_of_channels == 1: samples_per_channel = data.shape[0] shape = (samples_per_channel, 1) From 0241628b0f9474c0ab23cb1e420189a4bce8c0ad Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 16:25:44 -0600 Subject: [PATCH 34/61] more DAQ demo codes able to read and write multiple channels using DAQ_FSM_Demo and DAQ_double_blit, will be using this to test FSM smooth next --- NI/DAQ_FSM_Demo.py | 15 ++++++++------- NI/DAQ_double_blit.py | 29 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/NI/DAQ_FSM_Demo.py b/NI/DAQ_FSM_Demo.py index 5e83894..faa5045 100644 --- a/NI/DAQ_FSM_Demo.py +++ b/NI/DAQ_FSM_Demo.py @@ -9,6 +9,7 @@ from numpy import abs, ceil, pi, linspace, cos, ones + class Newport_FSM(System): """ Class for controlling Newport FSM using National Instruments DAQ. @@ -113,16 +114,16 @@ def convert_V_to_um(self, dime, V): """ Returns micron position corresponding to channel voltage. """ - um = V*self.fsm_dimensions[dim]['micron_per_volt'] + ( - self.fsm_dimensions[dim]['origin']) + um = V * self.fsm_dimensions[dim]['micron_per_volt'] + ( + self.fsm_dimensions[dim]['origin']) return um def convert_um_to_V(self, dim, um): """ Returns voltage corresponding to micron position. """ - V = (um - self.fsm_dimensions[dim]['origin'])/( - self.fsm_dimensions[dim]['micron_per_volt']) + V = (um - self.fsm_dimensions[dim]['origin']) / ( + self.fsm_dimensions[dim]['micron_per_volt']) return V def set_V_to_zero(self): @@ -144,10 +145,10 @@ def ao_smooth(self, x_init, x_final, channel): v_init = self.convert_um_to_V(x_init, channel) v_final = self.convert_un_to_V(x_final, channel) - n_steps = ceil(abs(v_final-v_init)*ao_smooth_steps_per_volt) + n_steps = ceil(abs(v_final - v_init) * ao_smooth_steps_per_volt) - v_data = v_init*ones(n_steps) + (v_final - v_init) * (1.0 - cos( - linspace(0, pi, n_steps)/2.0)) + v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( + linspace(0, pi, n_steps) / 2.0)) print(v_data) diff --git a/NI/DAQ_double_blit.py b/NI/DAQ_double_blit.py index 8bac64a..2028ebc 100644 --- a/NI/DAQ_double_blit.py +++ b/NI/DAQ_double_blit.py @@ -17,18 +17,19 @@ xar = [] yar0 = [] yar1 = [] -print(task.read(samples_per_channel=1, timeout=10.0, group_by='scan')) -# def animate(i): -# xar.append(time.clock()-t0) -# yar0.append(task.read) -# ax1.clear() -# ax1.plot(xar, yar0, 'bo') -# ax1.plot(xar, yar1, 'ro') -# plt.xlabel('Time (s)') -# plt.ylabel('Voltage (V)') -# ani = animation.FuncAnimation(fig, animate, interval=10) -# -# plt.show() -# -# task.stop() +def animate(i): + xar.append(time.clock()-t0) + vals = task.read(samples_per_channel=1, timeout=10.0, group_by='scan') + yar0.append(vals[0][0]) + yar1.append(vals[0][1]) + ax1.clear() + ax1.plot(xar, yar0, 'bo') + ax1.plot(xar, yar1, 'ro') + plt.xlabel('Time (s)') + plt.ylabel('Voltage (V)') +ani = animation.FuncAnimation(fig, animate, interval=10) + +plt.show() + +task.stop() From df51ddd6a67a0069a63ecbe457e371bc76e40870 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 15 Dec 2015 18:46:47 -0600 Subject: [PATCH 35/61] more work on DAQ able to read/write array of analog voltages, but need to synchronize w/ timing --- NI/DAQ_FSM_Demo.py | 33 ++++++++++++++++++++++++++++----- NI/DAQ_double_blit.py | 2 ++ NI/NI64/base.py | 2 +- NI/NI64/tasks.py | 14 +++++++++----- NI/foreign.py | 30 ++++++++++++++++++++---------- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/NI/DAQ_FSM_Demo.py b/NI/DAQ_FSM_Demo.py index faa5045..6d29c81 100644 --- a/NI/DAQ_FSM_Demo.py +++ b/NI/DAQ_FSM_Demo.py @@ -7,6 +7,8 @@ from lantz import Feat, DictFeat, Action +import numpy as np + from numpy import abs, ceil, pi, linspace, cos, ones @@ -47,11 +49,13 @@ def initialize(self): chan = VoltageOutputChannel(phys_chan, name=chan_name, min_max=(V_min, V_max), units='volts', task=self.task) + + self.task.start() from numpy.random import randn - data = randn(1000, 1000) + data = randn(1, 1000) self.task.write(data, auto_start=False, timeout=10.0, group_by='scan') @@ -118,7 +122,7 @@ def convert_V_to_um(self, dime, V): self.fsm_dimensions[dim]['origin']) return um - def convert_um_to_V(self, dim, um): + def convert_um_to_V(self, um, dim): """ Returns voltage corresponding to micron position. """ @@ -130,8 +134,9 @@ def set_V_to_zero(self): """ Sets output voltages to 0. """ + self.abs_position_pair = (self.convert_V_to_um(0, 'x'), + self.convert_V_to_um(0, 'y')) - @Action() def ao_smooth(self, x_init, x_final, channel): """ Smooths output of DAQ to avoid hysteresis w/ moving FSM mirror. @@ -143,15 +148,33 @@ def ao_smooth(self, x_init, x_final, channel): ao_smooth_steps_per_volt = 1000.0 # steps/V v_init = self.convert_um_to_V(x_init, channel) - v_final = self.convert_un_to_V(x_final, channel) + v_final = self.convert_um_to_V(x_final, channel) n_steps = ceil(abs(v_final - v_init) * ao_smooth_steps_per_volt) v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( - linspace(0, pi, n_steps) / 2.0)) + linspace(0.0, pi, n_steps))) / 2.0 print(v_data) + print(v_data.size) + if channel == 'x': + v_data = np.vstack((v_data, np.zeros(v_data.size))) + else: + v_data = np.vstack((np.zeros(v_data.size), v_data)) + + + + self.task.write(v_data, auto_start=False, timeout=10.0, + group_by='channel') if __name__ == '__main__': with Newport_FSM() as inst: print('testing!') + + inst.ao_smooth(-5.0, 5.0, 'x') + inst.ao_smooth(5.0, -25.0, 'x') + inst.ao_smooth(-25.0, 25.0, 'x') + + inst.ao_smooth(-5.0, 5.0, 'y') + inst.ao_smooth(5.0, -25.0, 'y') + inst.ao_smooth(-25.0, 25.0, 'y') diff --git a/NI/DAQ_double_blit.py b/NI/DAQ_double_blit.py index 2028ebc..870dbbd 100644 --- a/NI/DAQ_double_blit.py +++ b/NI/DAQ_double_blit.py @@ -18,6 +18,7 @@ yar0 = [] yar1 = [] + def animate(i): xar.append(time.clock()-t0) vals = task.read(samples_per_channel=1, timeout=10.0, group_by='scan') @@ -28,6 +29,7 @@ def animate(i): ax1.plot(xar, yar1, 'ro') plt.xlabel('Time (s)') plt.ylabel('Voltage (V)') + ani = animation.FuncAnimation(fig, animate, interval=10) plt.show() diff --git a/NI/NI64/base.py b/NI/NI64/base.py index 9c39389..ba7c511 100644 --- a/NI/NI64/base.py +++ b/NI/NI64/base.py @@ -857,7 +857,7 @@ def configure_timing_sample_clock(self, source='on_board_clock', rate=1, active_ source = None self.samples_per_channel = samples_per_channel self.sample_mode = sample_mode - self.lib.CfgSampClkTiming(source, float64(rate), active_edge, sample_mode, samples_per_channel) + self.lib.CfgSampClkTiming(source, rate, active_edge, sample_mode, samples_per_channel) def configure_timing_burst_handshaking_export_clock(self, *args, **kws): """ diff --git a/NI/NI64/tasks.py b/NI/NI64/tasks.py index aeebe7b..278ed4d 100644 --- a/NI/NI64/tasks.py +++ b/NI/NI64/tasks.py @@ -127,18 +127,22 @@ def read(self, samples_per_channel=None, timeout=10.0, group_by='channel'): number_of_channels = self.number_of_channels() if group_by == Constants.Val_GroupByScanNumber: - data = np.zeros((samples_per_channel, number_of_channels), dtype=np.float64) + data = np.zeros((samples_per_channel, number_of_channels), + dtype=np.float64) else: - data = np.zeros((number_of_channels, samples_per_channel), dtype=np.float64) + data = np.zeros((number_of_channels, samples_per_channel), + dtype=np.float64) - err, data, count = self.lib.ReadAnalogF64(samples_per_channel, timeout, group_by, - data.ctypes.data, data.size, RetValue('i32'), None) + err, count = self.lib.ReadAnalogF64(samples_per_channel, timeout, + group_by, data.ctypes.data, + data.size, RetValue('i32'), + None) if samples_per_channel < count: if group_by == 'scan': return data[:count] else: - return data[:,:count] + return data[:, :count] return data diff --git a/NI/foreign.py b/NI/foreign.py index 2fae29b..a5108ab 100644 --- a/NI/foreign.py +++ b/NI/foreign.py @@ -13,7 +13,7 @@ import ctypes import inspect from ctypes.util import find_library -from ctypes import cast, POINTER, c_uint64 +from ctypes import cast, POINTER, c_uint64, c_double from itertools import chain from lantz import Driver @@ -78,8 +78,10 @@ def __get_func(self, name): return getattr(self.internal, name) except Exception: if self.prefix: - raise AttributeError('Could not find ({}){} in {}'.format(self.prefix, name, self.internal)) - raise AttributeError('Could not find {} in {}'.format(name, self.internal)) + raise AttributeError('Could not find ({}){} in {}'.format( + self.prefix, name, self.internal)) + raise AttributeError( + 'Could not find {} in {}'.format(name, self.internal)) def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): @@ -140,7 +142,8 @@ def __init__(self, type): try: self.buffer = (TYPES[type] * 1)() except KeyError: - raise KeyError('The type {} is not defined ()'.format(type, TYPES.keys())) + raise KeyError( + 'The type {} is not defined ()'.format(type, TYPES.keys())) def __iter__(self): yield self @@ -156,7 +159,8 @@ def __init__(self, type, length=1): try: self.buffer = (TYPES[type] * length)() except KeyError: - raise KeyError('The type {} is not defined ()'.format(type, TYPES.keys())) + raise KeyError( + 'The type {} is not defined ()'.format(type, TYPES.keys())) self.length = length def __iter__(self): @@ -194,12 +198,12 @@ def __init__(self, *args, **kwargs): except OSError: pass else: - raise OSError('While instantiating {}: library not found'.format(self.__class__.__name__)) + raise OSError('While instantiating {}: library not found'.format( + self.__class__.__name__)) self.log_info('LibraryDriver created with {} from {}', name, folder) self._add_types() - def _add_types(self): pass @@ -215,11 +219,15 @@ def _preprocess_args(self, name, *args): new_args.append(arg.buffer) elif isinstance(arg, str): new_args.append(bytes(arg, 'ascii')) - elif isinstance(arg, int): # hack to avoid issue w/ overflowing 64-bit integer when passing args to ctypes + # hack to avoid issue w/ overflowing 64-bit integer when passing + # args to ctypes + elif isinstance(arg, int): if arg > 0xFFFFFFFF: new_args.append(cast(arg, POINTER(c_uint64))) else: new_args.append(arg) + elif isinstance(arg, float): + new_args.append(c_double(arg)) else: new_args.append(arg) @@ -232,7 +240,8 @@ def _wrapper(self, name, func, *args): ret = func(*new_args) except Exception as e: - raise Exception('While calling {} with {} (was {}): {}'.format(name, new_args, args, e)) + raise Exception('While calling {} with {} (was {}): {}'.format( + name, new_args, args, e)) ret = self._return_handler(name, ret) @@ -242,7 +251,8 @@ def _postprocess(self, name, ret, collect): if collect: values = [item.value for item in collect] values.insert(0, ret) - self.log_debug('Function call {} returned {}. Collected: {}', name, ret, collect) + self.log_debug( + 'Function call {} returned {}. Collected: {}', name, ret, collect) return tuple(values) self.log_debug('Function call {} returned {}.', name, ret) From c0ef5e31a53b804ef9d663c997e645b5b798e41b Mon Sep 17 00:00:00 2001 From: kcmiao Date: Wed, 16 Dec 2015 13:41:11 -0600 Subject: [PATCH 36/61] Added installation instructions for awsch/lantz --- LantzSetup.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/LantzSetup.md b/LantzSetup.md index 100fe35..de006b4 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -34,7 +34,7 @@ After this is installed, you should have all the pieces necessary to run the bas If `pyqt` fails to install, put a copy of `qt.conf` in `Library\Bin`. Thanks to @varses for this tip! -## 3. Install other packages using pip ## +## 3a. Install other packages using pip ## From the command line, run the command: @@ -42,6 +42,23 @@ From the command line, run the command: This command installs the colorama (used for producing colorful terminal output), pyserial (interfacing with serial devices), pyusb(interfacing with usb devices), and lantz (what you're supposedly hoping to install) packages to your Miniconda3 installation. +## 3b. Install awsch/lantz instead of the official Lantz release + +For those interested in using custom drivers in the group's modified release, this step will allow pip to install a version of Lantz that is synced with the group Github. + +Git is a prequisite for this step. Obtain it here: https://git-scm.com/download/win + +Once installed, navigate to the location where you would like to have the group's modified Lantz release reside. Then, run: + + > git clone https://github.com/awsch/lantz.git + > cd lantz + > pip uninstall lantz :: let's remove the old version to ensure no collisions occur + > pip install -e . + +Changes made on Github, however, will not be automatically synced with your local clone of Lantz. To perform a sync, navigate to this directory and perform: + + > git pull origin master + ## 4 . Test your installation ## From the command prompt, move up a directory into your main Miniconda3 installation folder, then run `python.exe` From b903dd7b308c5699f1193bb4208d2b540c033b49 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 4 Jan 2016 10:43:34 -0600 Subject: [PATCH 37/61] updating on progress for DAQ programming This update includes code for edge counting, and fast FSM controlled by the hardware clock. Next steps: - make counter read fast - write_and_count function Longer term goals: - clean up interface - uses two inconsistent ways of interfacing w/ NI dlls - make wrapping w/ foreign.py nicer and possibly push these changes to the main lantz repository --- NI/DAQ_FSM_Demo.py | 74 +++++++++++++++++++++++++----------------- NI/DAQ_Poisson_demo.py | 19 +++++++++++ 2 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 NI/DAQ_Poisson_demo.py diff --git a/NI/DAQ_FSM_Demo.py b/NI/DAQ_FSM_Demo.py index 6d29c81..cba7633 100644 --- a/NI/DAQ_FSM_Demo.py +++ b/NI/DAQ_FSM_Demo.py @@ -9,7 +9,9 @@ import numpy as np -from numpy import abs, ceil, pi, linspace, cos, ones +from numpy import abs, ceil, pi, linspace, cos, ones, size + +from time import sleep class Newport_FSM(System): @@ -51,15 +53,6 @@ def initialize(self): task=self.task) - self.task.start() - - from numpy.random import randn - - data = randn(1, 1000) - - self.task.write(data, auto_start=False, timeout=10.0, group_by='scan') - - print('Task started!') def finalize(self): """ @@ -98,22 +91,6 @@ def abs_position_single(self, dimension, pos): """ return 0 - # @Feat() - # def scan_speed(self): - # """ - # Returns current FSM scan speed in V/s. - # """ - # print('Bold faced lie') - # return 0 - # - # @scan_speed.setter - # def scan_speed(self, speed): - # """ - # Sets the current scan speed of the analog output in V/s. - # """ - # print('Bold faced lie') - # return 0 - def convert_V_to_um(self, dime, V): """ Returns micron position corresponding to channel voltage. @@ -144,7 +121,7 @@ def ao_smooth(self, x_init, x_final, channel): Copy of dchristle's algorithm. (I know what you're trying to do, and just stop.) """ - ao_smooth_rate = 50000.0 # Hz + ao_smooth_rate = 5000.0 # Hz ao_smooth_steps_per_volt = 1000.0 # steps/V v_init = self.convert_um_to_V(x_init, channel) @@ -155,6 +132,8 @@ def ao_smooth(self, x_init, x_final, channel): v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( linspace(0.0, pi, n_steps))) / 2.0 + samples = size(v_data) + print(v_data) print(v_data.size) if channel == 'x': @@ -162,19 +141,54 @@ def ao_smooth(self, x_init, x_final, channel): else: v_data = np.vstack((np.zeros(v_data.size), v_data)) + # TODO: Figure out relevant parameters for this function! + # Probably will have to rewrite the function. + self.task.configure_timing_sample_clock(source='OnboardClock', rate=ao_smooth_rate, sample_mode='finite') + + self.task.write(v_data, auto_start=False, timeout=10.0, group_by='channel') + try: + self.task.start() + print('Started FSM Scan') + sleep(samples*1.0/ao_smooth_rate) + print('Expected scan time:{} sec'.format(samples*1.0/ao_smooth_rate)) + self.task.stop() + + except e: + print('Failed in counter start phase:{}'.format(e)) + + #self.task.stop() + + def write_and_count(self, devchan, ctrchan, src, aochan, vdata, freq=10000.0, minv=-10.0, + maxv=10.0, timeout=10.0): + """ + :param devchan (string): device channel specifier + :param ctrchan: + :param src: + :param aochan: + :param vdata: + :param freq: + :param minv: + :param maxv: + :param timeout: + :return: + """ - self.task.write(v_data, auto_start=False, timeout=10.0, - group_by='channel') if __name__ == '__main__': with Newport_FSM() as inst: print('testing!') inst.ao_smooth(-5.0, 5.0, 'x') + sleep(1) inst.ao_smooth(5.0, -25.0, 'x') + sleep(1) inst.ao_smooth(-25.0, 25.0, 'x') - + sleep(1) inst.ao_smooth(-5.0, 5.0, 'y') + sleep(1) inst.ao_smooth(5.0, -25.0, 'y') + sleep(1) inst.ao_smooth(-25.0, 25.0, 'y') + sleep(1) + diff --git a/NI/DAQ_Poisson_demo.py b/NI/DAQ_Poisson_demo.py new file mode 100644 index 0000000..b34fea5 --- /dev/null +++ b/NI/DAQ_Poisson_demo.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from lantz.drivers.ni.daqmx import CounterOutputTask, CounterOutTicksChannel +from lantz.drivers.ni.daqmx import CounterInputTask, CountEdgesChannel + +poisson = CounterOutputTask('poisson') +poiss_chan2 = CounterOutTicksChannel('dev1/ctr1') +poisson.add_channel(poiss_chan2) + + +counts = CounterInputTask('counter') +chan1 = CountEdgesChannel('dev1/ctr0') +counts.add_channel(chan1) +counts.start() + +print('Counts:{}'.format(counts.read(samples_per_channel=1))) + +counts.stop() From 9bdc2c75a89bb776aa1f62330adf19e109b56714 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Fri, 8 Jan 2016 14:50:31 -0600 Subject: [PATCH 38/61] added slack integration added basic example code to push to a slack channel, could be used for providing status updates on experiments, etc. --- slack/bot.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 slack/bot.py diff --git a/slack/bot.py b/slack/bot.py new file mode 100644 index 0000000..b230bb5 --- /dev/null +++ b/slack/bot.py @@ -0,0 +1,27 @@ +import requests as rq +import json + +if __name__ == '__main__': + + channel_name = 'private_chan_name' + bot_name = 'bot_name' + text = 'This is posted to #{} and comes from {}'.format(channel_name, bot_name) + + print(text) + emoji = ':smiley:' + + webhook_url = 'https://hooks.slack.com/services/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' + + data = {} + + data['text'] = text + data['username'] = bot_name + data['channel'] = channel_name + data['icon_emoji'] = emoji + + payload = json.dumps(data) + + + r = rq.post(webhook_url, data=payload) + + print(r.text) From d7ba8b947ddb3676b4e5334b3e77b99e99ee71b4 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sat, 9 Jan 2016 18:26:46 -0600 Subject: [PATCH 39/61] added image file upload and example python bot added little interface for posting status messages to slack for an experiment. feel free to improve or modify the code for your own purposes. there are a few changes you need to make, most notably creating a token for yourself, but I've included documentation - make sure to read it. --- slack/bot.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 13 deletions(-) diff --git a/slack/bot.py b/slack/bot.py index b230bb5..965f725 100644 --- a/slack/bot.py +++ b/slack/bot.py @@ -1,27 +1,155 @@ -import requests as rq -import json +from requests import post, get +from json import dumps +from time import strftime -if __name__ == '__main__': +def slack_file_upload(directory, filename, slack_channel): + """ + This function uploads a file to a slack channel from the account that + corresponds to the token specified the the function body below. Note that + this file can be acccessed publicly by someone with the url, so do not post + any information (or data) that might be sensitive. + + You will need to create a web token using the page at: + https://api.slack.com/web + + Note that this token is yours, and should remain private - please take + care to not commmit your token to GitHub, since this lets people have + access to your entire slack account. + + Note that executing this function will send a message to your slackbot + channel alerting you that the file permissions have been changed. + + Parameters: + directory (str): location of the file + filename (str): filename + slack_channel (str): slack channel to post the file to + + Returns: + + file_url (str): public location of the file + """ + print('Did you change the token??') + token = 'xxxx-xxxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxx' + channel = slack_channel + + upload_url = 'https://slack.com/api/files.upload' + + data = {'token': token, 'channels': slack_channel, 'is_public': 'true', + 'public_url_shared': 'true'} + + f_loc = directory + filename + + file_dict = {'file': open(f_loc, 'rb')} + + r = post(upload_url, params=data, files=file_dict) + json = r.json() + + file_url = json['file']['permalink_public'] + + r = post(file_url) #pinging this link is actually what enables the public link + + address = file_url.replace('https://slack-files.com/','') + keys = address.split('-') + + file_url = 'https://files.slack.com/files-pri/{}-{}/{}?pub_secret={}'.format(keys[0], keys[1], filename, keys[2]) + return file_url + +def post_status_message(message, slack_channel, bot_name='status_bot', + emoji=':robot_face:', title='', attachment_text='', + color='#000000', directory='', filename='', + fallback_message=''): + """ + Posts a status message to Slack. + + Parameters: + + message (str): message text to post to channel + slack_channel (str): name of Slack channel to post to - channel_name = 'private_chan_name' - bot_name = 'bot_name' - text = 'This is posted to #{} and comes from {}'.format(channel_name, bot_name) + bot_name (str): name of bot to post as + emoji (str): emoji to use for bot icon - print(text) - emoji = ':smiley:' + title (str): attachment title text + attachment_text (str): attachment text + color (hex color): hexadecimal color to display with message + + directory (str): directory where file to post is located + filename (str): name of file to post + fallback_message (str): message to display if file does not load + + Returns: + none + """ + image_url = '' + + print(directory) + + print(filename) + + if not filename or directory: + + image_url = slack_file_upload(directory=directory, filename=filename, + slack_channel=slack_channel) + + message = message + '<{}>'.format(image_url) webhook_url = 'https://hooks.slack.com/services/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' data = {} - data['text'] = text + data['text'] = message data['username'] = bot_name - data['channel'] = channel_name + data['channel'] = slack_channel data['icon_emoji'] = emoji - payload = json.dumps(data) + attachments = {} + attachments['fallback'] = fallback_message + attachments['title'] = title + attachments['text'] = attachment_text + attachments['color'] = color + + if not image_url: + attachments['image_url'] = image_url + + att = [attachments] + + data['attachments'] = att + + payload = dumps(data) + + r = post(webhook_url, data=payload) + +def send_cryostat_cooldown_complete(temperature, slack_channel, directory, filename): + """ + Example settings for sending a Janis status message that the cryostat has + cooled down. + """ + bot_name = 'janis-bot' + message = '#janis_status ({})\nThe cyrostat is now cold, current temp: {}K'.format(strftime('%x %X %Z'),3.1) + emoji = ':snowman:' + + + title = 'Cooldown complete' + attachment_text = 'Cooldown data' + fallback_text = 'Plot of cryostat cooldown' + color = '#439FE0' + + post_status_message(message=message, slack_channel=slack_channel, + bot_name='janis-bot', emoji=emoji, title=title, + attachment_text=attachment_text, color=color, + directory=directory, filename=filename, + fallback_message=fallback_text) + + +if __name__ == '__main__': + """ + Set slack channel here for testing. + Can also select an image file for upload here to test. + """ - r = rq.post(webhook_url, data=payload) + slack_channel = '#your-channel' + directory = 'E:\\your\\directory\\name\\here' + filename = 'image_file.jpg' - print(r.text) + send_cryostat_cooldown_complete(3.1, slack_channel, directory, filename) From 4cee16586f39cfe2a88553b2f6048ff3008421b5 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Fri, 15 Jan 2016 09:36:39 -0600 Subject: [PATCH 40/61] adding updates to mono w/ working communication, added laser and arc lamp adding updated software for laser, now that comm is working well + no address error, adding new files to manage CoBrite DX1 telecom laser and Newport 69907 arc lamp controller --- CoBriteDX1.py | 28 +++++++++++ Newport69907.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++ SP2150i.py | 81 +++++++++++++++++++----------- 3 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 CoBriteDX1.py create mode 100644 Newport69907.py diff --git a/CoBriteDX1.py b/CoBriteDX1.py new file mode 100644 index 0000000..3b21cd2 --- /dev/null +++ b/CoBriteDX1.py @@ -0,0 +1,28 @@ +from lantz import Feat, DictFeat, Action +from lantz.messagebased import MessageBasedDriver + +from time import sleep + + +class CoBriteDX1(MessageBasedDriver): + """ + + """ + DEFAULTS = {'ASRL': {'write_termination': '\r', + 'read_termination': '', + }} + + def initialize(self): + """ + """ + super().initialize() + + + + + + +if __name__ == '__main__': + with CoBriteDX1('USB:XXYYZZ') as inst: + + # inst.nm = 400.0 diff --git a/Newport69907.py b/Newport69907.py new file mode 100644 index 0000000..8ef4f1a --- /dev/null +++ b/Newport69907.py @@ -0,0 +1,130 @@ +from lantz import Feat, DictFeat, Action +from lantz.messagebased import MessageBasedDriver + +from pyvisa import constants + +from time import sleep + + +class SP2150i(MessageBasedDriver): + """ + + """ + DEFAULTS = {'ASRL': {'write_termination': '\r', + 'read_termination': '', + 'baud_rate': 9600, + 'data_bits': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'latin-1', + 'timeout': 10000}} + + max_speed = 100 + wavelength_min = 380 + wavelength_max = 520 + + def initialize(self): + """ + """ + super().initialize() + self.clear_buffer() + + def clear_buffer(self): + """ + This function sends an empty query just to clear any junk from the read + buffer...This could probably be done more elegantly...but it works, for + now at least. + """ + result = self.resource.query('') + + @Feat(limits=(wavelength_min, wavelength_max)) + def nm(self): + """ + Returns current wavelength of monochromater. + """ + self.clear_buffer() + read = self.query('?NM') + wavelength = read.replace('nm ok', '') + return float(wavelength) + + @nm.setter + def nm(self, wavelength): + """ + Sets output to specified wavelength, traveling at the current scan + rate. + """ + self.clear_buffer() + return self.query('{} NM'.format(wavelength)) + + @Feat(limits=(0, max_speed)) + def scan_speed(self): + """ + Get scan rate in nm/min. + """ + self.clear_buffer() + read = self.query('?NM/MIN') + speed = read.replace('nm/min ok', '') + return float(speed) + + @scan_speed.setter + def scan_speed(self, speed): + """ + Sets current scan speed in nm/min. + """ + self.clear_buffer() + read = self.query('{}NM/MIN'.format(speed)) + speed = read.replace('nm/min ok', '') + return float(speed) + + @Feat(limits=(1, 2, 1)) + def grating(self): + """ + Returns the current grating position + """ + self.clear_buffer() + return int(self.query('?GRATING')) + + @grating.setter + def grating(self, grating_num): + """ + Sets the current grating to be grating_num + """ + print('Warning: will wait 20 seconds to change grating.') + self.query('{} GRATING'.format(grating_num)) + sleep(20) + + @Feat(limits=(1, 3, 1)) + def turret(self): + """ + Returns the selected turret number. + """ + return int(self.query('?TURRET')) + + @turret.setter + def turret(self, turr_set): + """ + Selects the parameters for the grating on turret turr_set + """ + return self.query('{}TURRET'.format(turr_set)) + + @Feat() + def turret_spacing(self): + """ + Returns the groove spacing of the grating for each turret. + """ + return self.query('?TURRETS') + + @Feat() + def grating_settings(self): + """ + Returns the groove spacing and blaze wavelength of grating positions + 1-6. This corresponds to 2 grating positions for each of the 3 turrets + """ + return self.query('?GRATINGS') + + +if __name__ == '__main__': + with SP2150i('ASRL4::INSTR') as inst: + print('Wavelength: {}nm'.format(inst.nm)) + print('Scan rate: {}nm/min'.format(inst.scan_speed)) + #inst.nm = 400.0 diff --git a/SP2150i.py b/SP2150i.py index c729158..79aabc7 100644 --- a/SP2150i.py +++ b/SP2150i.py @@ -1,6 +1,10 @@ from lantz import Feat, DictFeat, Action from lantz.messagebased import MessageBasedDriver +from pyvisa import constants + +from numpy import abs + from time import sleep @@ -8,43 +12,42 @@ class SP2150i(MessageBasedDriver): """ """ - MANUFACTURER_ID = '0x0647' - MODEL_CODE = '0x0100' - - DEFAULTS = {'COMMON': {'write_termination': '\r', - 'read_termination': ''}} + DEFAULTS = {'ASRL': {'write_termination': '\r', + 'read_termination': '', + 'baud_rate': 9600, + 'data_bits': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'latin-1', + 'timeout': 10000}} max_speed = 100 wavelength_min = 380 wavelength_max = 520 - @Feat(limits=(0, max_speed)) - def scan_speed(self): + def initialize(self): """ - Get scan rate in nm/min. """ - return self.query('?NM/MIN') + super().initialize() + self.clear_buffer() - @scan_speed.setter - def scan_speed(self, speed): + def clear_buffer(self): """ - Sets current scan speed in nm/min. + This function sends an empty query just to clear any junk from the read + buffer...This could probably be done more elegantly...but it works, for + now at least. """ - read = self.query('{} NM/MIN'.format(speed)) - read2 = read.replace('nm/min ok', '') - print('nm/min read:' + read2) - sleep(1) - return read + result = self.resource.query('') @Feat(limits=(wavelength_min, wavelength_max)) def nm(self): """ + Returns current wavelength of monochromater. """ - print('Sending ?NM...') + self.clear_buffer() read = self.query('?NM') - read = read.replace('nm ok', '') - read = read.replace('1` ', '') - return float(read) + wavelength = read.replace('nm ok', '') + return float(wavelength) @nm.setter def nm(self, wavelength): @@ -52,13 +55,36 @@ def nm(self, wavelength): Sets output to specified wavelength, traveling at the current scan rate. """ - return self.query('{} NM'.format(wavelength)) + curr_wavelength = self.nm + delta_lambda = abs(curr_wavelength - wavelength) + + + @Feat(limits=(0, max_speed)) + def scan_speed(self): + """ + Get scan rate in nm/min. + """ + self.clear_buffer() + read = self.query('?NM/MIN') + speed = read.replace('nm/min ok', '') + return float(speed) + + @scan_speed.setter + def scan_speed(self, speed): + """ + Sets current scan speed in nm/min. + """ + self.clear_buffer() + read = self.query('{}NM/MIN'.format(speed)) + speed = read.replace('nm/min ok', '') + return float(speed) @Feat(limits=(1, 2, 1)) def grating(self): """ Returns the current grating position """ + self.clear_buffer() return int(self.query('?GRATING')) @grating.setter @@ -82,7 +108,7 @@ def turret(self, turr_set): """ Selects the parameters for the grating on turret turr_set """ - return self.query('{} TURRET'.format(turr_set)) + return self.query('{}TURRET'.format(turr_set)) @Feat() def turret_spacing(self): @@ -101,8 +127,7 @@ def grating_settings(self): if __name__ == '__main__': - with SP2150i('USB0::0x0647::0x0100::NI-VISA-120001::RAW') as inst: - print('Testing 1, 2, 3') - print('Wavelength: {}'.format(inst.nm)) - print('Scan rate: {}'.format(inst.scan_speed)) - # inst.nm = 400.0 + with SP2150i('ASRL4::INSTR') as inst: + print('Wavelength: {}nm'.format(inst.nm)) + print('Scan rate: {}nm/min'.format(inst.scan_speed)) + #inst.nm = 400.0 From 7229f107a5e582987f48706ae04eab72195789e2 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Sun, 17 Jan 2016 22:33:19 -0600 Subject: [PATCH 41/61] added more commands to arc lamp control added full library of commands to arc lamp. Still needs to be tested with an actual instrument connected - will try and work this out tomorrow. --- Newport69907.py | 266 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 190 insertions(+), 76 deletions(-) diff --git a/Newport69907.py b/Newport69907.py index 8ef4f1a..328f951 100644 --- a/Newport69907.py +++ b/Newport69907.py @@ -1,14 +1,12 @@ -from lantz import Feat, DictFeat, Action +from lantz import Feat, Action from lantz.messagebased import MessageBasedDriver - -from pyvisa import constants - from time import sleep -class SP2150i(MessageBasedDriver): +class Newport69907(MessageBasedDriver): """ - + This class implements serial control of a Newport69907 arc lamp power + supply. """ DEFAULTS = {'ASRL': {'write_termination': '\r', 'read_termination': '', @@ -19,112 +17,228 @@ class SP2150i(MessageBasedDriver): 'encoding': 'latin-1', 'timeout': 10000}} - max_speed = 100 - wavelength_min = 380 - wavelength_max = 520 + max_lamp_amps = 3 # amps + max_lamp_power = 100 # watts + + def parse_status_register(self, status_register): + """ + Parses the status registry codes bit by bit into meaningful statement. + These error bits correspond to whether or not the corresponding LEDs + on the front panel are illuminated. + + Bit 7 - Lamp on + Bit 6 - Ext + Bit 5 - Power/Current mode + Bit 4 - Cal mode + Bit 3 - Fault + Bit 2 - Communication + Bit 1 - Limit + Bit 0 - Interlock + """ + status_code = int(('0x' + error_register.replace('STB', '')), 16) + print('Status code:{}'.format(status_code)) + if status_code > 128: + print('Status: Lamp on') + status_code -= 128 + if status_code > 64: + print('Status: Ext') + status_code -= 64 + if status_code > 32: + print('Status: Power Mode') + status_code -= 32 + else: + print('Status: Current Mode') + if status_code > 16: + print('Status: Cal Mode') + status_code -= 16 + if status_code > 8: + print('Status: Fault') + status_code -= 8 + if status_code > 4: + print('Status: Comm') + status_code -= 4 + if status_code > 2: + print('Status: Limit') + status_code -= 2 + if status_code > 1: + print('Status: Interlock') + return 0 + + def parse_error_register(self, error_register): + """ + Parses the status registry codes bit by bit into meaningful statement. + + Bit 7 - Power on + Bit 6 - User request + Bit 5 - Command error + Bit 4 - Execution error + Bit 3 - Device dependent error + Bit 2 - Query error + Bit 1 - Request control + Bit 0 - Operation complete + """ + # convert hex string into integer + err_code = int(('0x' + error_register.replace('ESR', '')), 16) + print('error code') + if err_code > 128: + print('Error: Power on') + err_code -= 128 + if err_code > 64: + print('Error: User request') + err_code -= 64 + if err_code > 32: + print('Error: Command error') + err_code -= 32 + if err_code > 16: + print('Error: Execution error') + err_code -= 16 + if err_code > 8: + print('Error: Device dependent error.') + err_code -= 8 + if err_code > 4: + print('Error: Query error.') + err_code -= 4 + if err_code > 2: + print('Error: Request control') + err_code -= 2 + if err_code > 1: + print('Error: Operation complete') + return 0 - def initialize(self): + @Feat() + def idn(self): """ + Return power supply model number """ - super().initialize() - self.clear_buffer() + return self.query('IDN?') - def clear_buffer(self): + @Feat() + def status(self): """ - This function sends an empty query just to clear any junk from the read - buffer...This could probably be done more elegantly...but it works, for - now at least. + Returns status information of the instrument. """ - result = self.resource.query('') + status = self.query('STB?') + self.parse_status_register(status) - @Feat(limits=(wavelength_min, wavelength_max)) - def nm(self): + @Feat() + def error_status(self): """ - Returns current wavelength of monochromater. + Returns the instrument error status. """ - self.clear_buffer() - read = self.query('?NM') - wavelength = read.replace('nm ok', '') - return float(wavelength) + error_register = self.query('ESR?') + self.parse_error_register(error_register) - @nm.setter - def nm(self, wavelength): + @Feat() + def amps(self): """ - Sets output to specified wavelength, traveling at the current scan - rate. + Returns output amperage displayed on the front panel. """ - self.clear_buffer() - return self.query('{} NM'.format(wavelength)) + return float(self.query('AMPS?')) - @Feat(limits=(0, max_speed)) - def scan_speed(self): + @Feat() + def volts(self): """ - Get scan rate in nm/min. + Return output voltage displayed on front panel. """ - self.clear_buffer() - read = self.query('?NM/MIN') - speed = read.replace('nm/min ok', '') - return float(speed) + return float(self.query('VOLTS?')) - @scan_speed.setter - def scan_speed(self, speed): + @Feat() + def watts(self): """ - Sets current scan speed in nm/min. + Returns output wattage displayed on front panel. """ - self.clear_buffer() - read = self.query('{}NM/MIN'.format(speed)) - speed = read.replace('nm/min ok', '') - return float(speed) + return int(self.query('WATTS?')) - @Feat(limits=(1, 2, 1)) - def grating(self): + @Feat() + def lamp_hrs(self): """ - Returns the current grating position + Returns the number of hours on the current lamp. """ - self.clear_buffer() - return int(self.query('?GRATING')) + return int(self.query('LAMP\sHRS')) - @grating.setter - def grating(self, grating_num): + @Feat() + def amp_preset(self): """ - Sets the current grating to be grating_num + Returns the lamp amperage preset value. """ - print('Warning: will wait 20 seconds to change grating.') - self.query('{} GRATING'.format(grating_num)) - sleep(20) + return float(self.query('A-PRESET?')) - @Feat(limits=(1, 3, 1)) - def turret(self): + @amp_preset.setter + def amp_preset(self, preset_val): """ - Returns the selected turret number. + Sets the lamp amperage preset value to preset_val. """ - return int(self.query('?TURRET')) + error_status = self.query('A-PRESET={0:.1f}'.format(preset_val)) + return self.parse_error_register(error_status) - @turret.setter - def turret(self, turr_set): + @Feat() + def power_preset(self): """ - Selects the parameters for the grating on turret turr_set + Returns the lamp power preset value (in watts). """ - return self.query('{}TURRET'.format(turr_set)) + return = int(self.query('P-PRESET?')) - @Feat() - def turret_spacing(self): + @power_preset.setter + def power_preset(self, power_preset): """ - Returns the groove spacing of the grating for each turret. + Sets the lamp powr preset value (in watts). """ - return self.query('?TURRETS') + error_status = self.query('P-PRESET={}'.format(ceil(power_preset))) + return self.parse_error_register(error_status) - @Feat() - def grating_settings(self): + @Feat(limits=(0, max_lamp_amps)) + def amp_lim(self): """ - Returns the groove spacing and blaze wavelength of grating positions - 1-6. This corresponds to 2 grating positions for each of the 3 turrets + Return current lamp amperage limit. """ - return self.query('?GRATINGS') + return float(self.query('A-LIM?')) + + @amp_lim.setter + def amp_lim(self, max_amps): + """ + Sets current amperage limit to max_amps + """ + error_register = self.query('A-LIM={0:.1f}') + + @Feat(limits=(0, max_lamp_power)) + def power_lim(self): + """ + Return current power limit. + """ + return int(self.query('P-LIM?')) + + @power_lim.setter + def power_lim(self, max_power): + """ + Sets the power limit of the lamp to max_power. + """ + error_register = self.query('P-LIM={}'.format(floor(max_power))) + + @Action() + def start_lamp(self): + """ + Turns on the arc lamp. + """ + error_register = self.query('START') + + @Action() + def stop_lamp(self): + """ + Turns off the arc lamp. + """ + error_register = self.query('STOP') + + @Action() + def reset(self): + """ + Returns the arc lamp to factory default settings. + """ + error_register = self.query('RST') if __name__ == '__main__': - with SP2150i('ASRL4::INSTR') as inst: - print('Wavelength: {}nm'.format(inst.nm)) - print('Scan rate: {}nm/min'.format(inst.scan_speed)) - #inst.nm = 400.0 + with Newport69907('ASRL10::INSTR') as inst: + print('Serial number:{}'.format(inst.idn)) + print('Status:{}'.format(inst.status)) + + # TODO: add more commands here. From 2984b590e1cb625c4c3d20ec02f56d03566961c1 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 18 Jan 2016 09:52:31 -0600 Subject: [PATCH 42/61] tested driver commands pretty much everything done here except for logging --- Newport69907.py | 141 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 35 deletions(-) diff --git a/Newport69907.py b/Newport69907.py index 328f951..a4b2539 100644 --- a/Newport69907.py +++ b/Newport69907.py @@ -1,6 +1,7 @@ from lantz import Feat, Action from lantz.messagebased import MessageBasedDriver from time import sleep +from pyvisa import constants class Newport69907(MessageBasedDriver): @@ -9,7 +10,7 @@ class Newport69907(MessageBasedDriver): supply. """ DEFAULTS = {'ASRL': {'write_termination': '\r', - 'read_termination': '', + 'read_termination': '\r', 'baud_rate': 9600, 'data_bits': 8, 'parity': constants.Parity.none, @@ -17,7 +18,7 @@ class Newport69907(MessageBasedDriver): 'encoding': 'latin-1', 'timeout': 10000}} - max_lamp_amps = 3 # amps + max_lamp_amps = 12 # amps max_lamp_power = 100 # watts def parse_status_register(self, status_register): @@ -35,34 +36,35 @@ def parse_status_register(self, status_register): Bit 1 - Limit Bit 0 - Interlock """ - status_code = int(('0x' + error_register.replace('STB', '')), 16) - print('Status code:{}'.format(status_code)) + status_code = int(('0x' + status_register.replace('STB', '')), 16) + status = [] + # print('Status code:{}'.format(status_code)) if status_code > 128: - print('Status: Lamp on') + status.append('Lamp on') status_code -= 128 if status_code > 64: - print('Status: Ext') + status.append('Ext') status_code -= 64 if status_code > 32: - print('Status: Power Mode') + status.append('Power Mode') status_code -= 32 else: - print('Status: Current Mode') + status.append('Current Mode') if status_code > 16: - print('Status: Cal Mode') + status.append('Cal Mode') status_code -= 16 if status_code > 8: - print('Status: Fault') + status.append('Fault') status_code -= 8 if status_code > 4: - print('Status: Comm') + status.append('Comm') status_code -= 4 if status_code > 2: - print('Status: Limit') + status.append('Limit') status_code -= 2 - if status_code > 1: - print('Status: Interlock') - return 0 + if status_code == 1: + status.append('Interlock') + return status def parse_error_register(self, error_register): """ @@ -79,31 +81,32 @@ def parse_error_register(self, error_register): """ # convert hex string into integer err_code = int(('0x' + error_register.replace('ESR', '')), 16) - print('error code') + errors = [] + # print('error code:{}'.format(err_code)) if err_code > 128: - print('Error: Power on') + errors.append('Power on') err_code -= 128 if err_code > 64: - print('Error: User request') + errors.append('User request') err_code -= 64 if err_code > 32: - print('Error: Command error') + errors.append('Command error') err_code -= 32 if err_code > 16: - print('Error: Execution error') + errors.append('Execution error') err_code -= 16 if err_code > 8: - print('Error: Device dependent error.') + errors.append('Device dependent error.') err_code -= 8 if err_code > 4: - print('Error: Query error.') + errors.append('Query error.') err_code -= 4 if err_code > 2: - print('Error: Request control') + errors.append('Request control') err_code -= 2 - if err_code > 1: - print('Error: Operation complete') - return 0 + # if err_code == 1: + # no error + return errors @Feat() def idn(self): @@ -118,7 +121,7 @@ def status(self): Returns status information of the instrument. """ status = self.query('STB?') - self.parse_status_register(status) + return self.parse_status_register(status) @Feat() def error_status(self): @@ -126,7 +129,7 @@ def error_status(self): Returns the instrument error status. """ error_register = self.query('ESR?') - self.parse_error_register(error_register) + return self.parse_error_register(error_register) @Feat() def amps(self): @@ -154,7 +157,7 @@ def lamp_hrs(self): """ Returns the number of hours on the current lamp. """ - return int(self.query('LAMP\sHRS')) + return self.query('LAMP HRS?') @Feat() def amp_preset(self): @@ -176,14 +179,14 @@ def power_preset(self): """ Returns the lamp power preset value (in watts). """ - return = int(self.query('P-PRESET?')) + return int(self.query('P-PRESET?')) @power_preset.setter def power_preset(self, power_preset): """ Sets the lamp powr preset value (in watts). """ - error_status = self.query('P-PRESET={}'.format(ceil(power_preset))) + error_status = self.query('P-PRESET={0:04d}'.format(int(power_preset))) return self.parse_error_register(error_status) @Feat(limits=(0, max_lamp_amps)) @@ -198,7 +201,8 @@ def amp_lim(self, max_amps): """ Sets current amperage limit to max_amps """ - error_register = self.query('A-LIM={0:.1f}') + error_register = self.query('A-LIM={0:.1f}'.format(max_amps)) + return self.parse_error_register(error_register) @Feat(limits=(0, max_lamp_power)) def power_lim(self): @@ -212,7 +216,8 @@ def power_lim(self, max_power): """ Sets the power limit of the lamp to max_power. """ - error_register = self.query('P-LIM={}'.format(floor(max_power))) + error_register = self.query('P-LIM={0:04d}'.format(int(max_power))) + return self.parse_error_register(error_register) @Action() def start_lamp(self): @@ -220,6 +225,7 @@ def start_lamp(self): Turns on the arc lamp. """ error_register = self.query('START') + return self.parse_error_register(error_register) @Action() def stop_lamp(self): @@ -227,6 +233,7 @@ def stop_lamp(self): Turns off the arc lamp. """ error_register = self.query('STOP') + return self.parse_error_register(error_register) @Action() def reset(self): @@ -234,11 +241,75 @@ def reset(self): Returns the arc lamp to factory default settings. """ error_register = self.query('RST') + return self.parse_error_register(error_register) if __name__ == '__main__': - with Newport69907('ASRL10::INSTR') as inst: + with Newport69907('ASRL9::INSTR') as inst: + print('== Instrument Information ==') print('Serial number:{}'.format(inst.idn)) print('Status:{}'.format(inst.status)) + print('Errors:{}'.format(inst.error_status)) - # TODO: add more commands here. + print('== Current lamp status ==') + print('Amps:{}A'.format(inst.amps)) + print('Volts:{}V'.format(inst.volts)) + print('Watts:{}W'.format(inst.watts)) + print('Lamp hours:{}hrs'.format(inst.lamp_hrs)) + + print('== Lamp Settings ==') + print('Current preset:{}A'.format(inst.amp_preset)) + print('Power preset:{}W'.format(inst.power_preset)) + print('Current limit:{}A'.format(inst.amp_lim)) + print('Power limit:{}W'.format(inst.power_lim)) + + print('== Lamp Control ==') + print('Starting lamp...') + inst.start_lamp() + sleep(5) + print('Amps:{}A'.format(inst.amps)) + print('Volts:{}V'.format(inst.volts)) + print('Watts:{}W'.format(inst.watts)) + print('Stopping lamp...') + inst.stop_lamp() + + print('== Changing Lamp Settings ==') + inst.amp_lim = 12.0 + inst.amp_preset = 7.5 + inst.power_lim = 98 + inst.power_preset = 95 + + print('Current preset:{}A'.format(inst.amp_preset)) + print('Power preset:{}W'.format(inst.power_preset)) + print('Current limit:{}A'.format(inst.amp_lim)) + print('Power limit:{}W'.format(inst.power_lim)) + + inst.amp_lim = 10.0 + inst.amp_preset = 5.5 + inst.power_lim = 80 + inst.power_preset = 75 + + print('Current preset:{}A'.format(inst.amp_preset)) + print('Power preset:{}W'.format(inst.power_preset)) + print('Current limit:{}A'.format(inst.amp_lim)) + print('Power limit:{}W'.format(inst.power_lim)) + + print('Starting lamp...') + inst.start_lamp() + sleep(5) + print('Amps:{}A'.format(inst.amps)) + print('Volts:{}V'.format(inst.volts)) + print('Watts:{}W'.format(inst.watts)) + print('Stopping lamp...') + inst.stop_lamp() + sleep(1) + + inst.amp_lim = 12.0 + inst.amp_preset = 7.5 + inst.power_lim = 98 + inst.power_preset = 95 + + print('Current preset:{}A'.format(inst.amp_preset)) + print('Power preset:{}W'.format(inst.power_preset)) + print('Current limit:{}A'.format(inst.amp_lim)) + print('Power limit:{}W'.format(inst.power_lim)) From b43139c092c8485453505b752c23a5275445902e Mon Sep 17 00:00:00 2001 From: Kevin Miao Date: Mon, 18 Jan 2016 18:47:59 -0600 Subject: [PATCH 43/61] initial commit, implements basic functionality --- lantz/drivers/kepco/bop.py | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 lantz/drivers/kepco/bop.py diff --git a/lantz/drivers/kepco/bop.py b/lantz/drivers/kepco/bop.py new file mode 100644 index 0000000..ab5d8f1 --- /dev/null +++ b/lantz/drivers/kepco/bop.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.kepco.bop + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation of Kepco BOP power supply + Author: Kevin Miao + Date: 1/18/2016 +""" + +from lantz import Action, Feat +from lantz.messagebased import MessageBasedDriver + +class BOP(MessageBasedDriver): + + @Feat() + def idn(self): + return self.query('*IDN?') + + @Action(values={'curr', 'volt'}) + def mode(self, value): + """ + sets operation mode + curr - constant current mode + volt - constant voltage mode + """ + return self.write('FUNC:MODE {}'.format(value)) + + @Feat(units='A') + def current(self): + return self.query('MEAS:CURR?') + + @current.setter + def current(self, value): + self.write('CURR {:1.1f}'.format(value)) + + @Feat(units='V') + def voltage(self): + return self.query('MEAS:VOLT?') + + @voltage.setter + def voltage(self, value): + self.write('VOLT {:1.1f}'.format(value)) From 271cf131d22883c547020bbd1cb843b6ae01c456 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 21 Jan 2016 22:23:39 -0600 Subject: [PATCH 44/61] got communication working nice w/ SP2150 about time. communication should be good and I have it set to wait for the monochromater to finish scanning or changing gratings before restoring control to the user. always check the most recent version of the manual is the lesson here! --- SP2150i.py | 108 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/SP2150i.py b/SP2150i.py index 79aabc7..d306e12 100644 --- a/SP2150i.py +++ b/SP2150i.py @@ -3,7 +3,7 @@ from pyvisa import constants -from numpy import abs +from numpy import abs, ceil from time import sleep @@ -13,18 +13,20 @@ class SP2150i(MessageBasedDriver): """ DEFAULTS = {'ASRL': {'write_termination': '\r', - 'read_termination': '', + 'read_termination': 'ok\r\n', 'baud_rate': 9600, 'data_bits': 8, 'parity': constants.Parity.none, 'stop_bits': constants.StopBits.one, 'encoding': 'latin-1', - 'timeout': 10000}} + 'timeout': 2000}} max_speed = 100 wavelength_min = 380 wavelength_max = 520 + num_gratings = 8 + def initialize(self): """ """ @@ -37,7 +39,7 @@ def clear_buffer(self): buffer...This could probably be done more elegantly...but it works, for now at least. """ - result = self.resource.query('') + return self.query('') @Feat(limits=(wavelength_min, wavelength_max)) def nm(self): @@ -46,7 +48,7 @@ def nm(self): """ self.clear_buffer() read = self.query('?NM') - wavelength = read.replace('nm ok', '') + wavelength = read.replace('nm', '') return float(wavelength) @nm.setter @@ -56,7 +58,17 @@ def nm(self, wavelength): rate. """ curr_wavelength = self.nm + scan_rate = self.scan_speed delta_lambda = abs(curr_wavelength - wavelength) + if delta_lambda > 0.1: + scan_time = ceil(delta_lambda / scan_rate * 60) + 2 # convert to seconds + self.clear_buffer() + print('Scanning from {}nm to {}nm, will take {}sec'.format( + curr_wavelength, wavelength, scan_time)) + return self.write_message_wait('{0:.1f} NM'.format(wavelength), + scan_time) + else: + return @Feat(limits=(0, max_speed)) @@ -66,7 +78,7 @@ def scan_speed(self): """ self.clear_buffer() read = self.query('?NM/MIN') - speed = read.replace('nm/min ok', '') + speed = read.replace('nm/min', '') return float(speed) @scan_speed.setter @@ -74,60 +86,104 @@ def scan_speed(self, speed): """ Sets current scan speed in nm/min. """ - self.clear_buffer() - read = self.query('{}NM/MIN'.format(speed)) - speed = read.replace('nm/min ok', '') - return float(speed) + return self.write_message_wait('{0:.1f} NM/MIN'.format(speed), 0.1) @Feat(limits=(1, 2, 1)) def grating(self): """ Returns the current grating position """ - self.clear_buffer() - return int(self.query('?GRATING')) + response = self.query('?GRATING') + return int(response) @grating.setter def grating(self, grating_num): """ - Sets the current grating to be grating_num + Changes the current grating to be the one in slot grating_num. """ - print('Warning: will wait 20 seconds to change grating.') - self.query('{} GRATING'.format(grating_num)) - sleep(20) + print('Warning: Untested code! No other gratings were installed.') + print('Changing grating, please wait 20 seconds...') + return self.write_message_wait('{} GRATING'.format(grating_num), 20) @Feat(limits=(1, 3, 1)) def turret(self): """ Returns the selected turret number. """ - return int(self.query('?TURRET')) + response = self.query('?TURRET') + return int(response.replace(' ok', '')) @turret.setter def turret(self, turr_set): """ Selects the parameters for the grating on turret turr_set """ - return self.query('{}TURRET'.format(turr_set)) + print('Warning: untested. No other turrets were installed.') + return self.write_message_wait('{} TURRET'.format(turr_set), 1) @Feat() def turret_spacing(self): """ Returns the groove spacing of the grating for each turret. """ - return self.query('?TURRETS') + print('Warning: This command does\'t do anything?') + return self.write_message_wait('?TURRETS', 0.5) @Feat() def grating_settings(self): """ Returns the groove spacing and blaze wavelength of grating positions - 1-6. This corresponds to 2 grating positions for each of the 3 turrets - """ - return self.query('?GRATINGS') - + for all possible gratings. + """ + gratings = [] + num_gratings = 8 + self.write('?GRATINGS') + self.read() + for i in range(0, num_gratings): + gratings.append(self.read()) + self.read() + return gratings + + def write_message_wait(self, message, wait_time): + self.write(message) + sleep(wait_time) + return self.read() if __name__ == '__main__': with SP2150i('ASRL4::INSTR') as inst: - print('Wavelength: {}nm'.format(inst.nm)) - print('Scan rate: {}nm/min'.format(inst.scan_speed)) - #inst.nm = 400.0 + print('== Monochromater Wavelength ==') + print('Wavelength:{}nm'.format(inst.nm)) + print('Scan rate:{}nm/min'.format(inst.scan_speed)) + + print('== Monochromater Information ==') + print('Selected turret:{}'.format(inst.turret)) + print('Selected grating:{}'.format(inst.grating)) + #inst.turret = 3 + #inst.grating = 2 + print('Selected turret:{}'.format(inst.turret)) + print('Selected grating:{}'.format(inst.grating)) + print('Turret Spacing:{}'.format(inst.turret_spacing)) + print('Gratings:{}'.format(inst.grating_settings)) + #print('Grating information:{}'.format(inst.grating_settings)) + + for i in range(1, 20): + inst.scan_speed = 100.0 + print('Scan rate:{}nm/min'.format(inst.scan_speed)) + print('Wavelength:{}nm'.format(inst.nm)) + inst.nm = 400.0 + print('Wavelength:{}nm'.format(inst.nm)) + inst.scan_speed = 50.0 + print('Scan rate:{}nm/min'.format(inst.scan_speed)) + inst.nm = 500.0 + print('Wavelength:{}nm'.format(inst.nm)) + print('Cycle {} complete.'.format(i)) + + + + #turret = 2 + #grating = 2 + #print('Selected turret:{}'.format(inst.turret)) + #print('Selected grating:{}'.format(inst.grating)) + + + #inst.nm = 500.0 From 0eae3e1dc8b8039bcf9cc88f0afc5bbfc0c73671 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 21 Jan 2016 22:29:15 -0600 Subject: [PATCH 45/61] added more doc sorry for committing twice, cleaned up some comments and added more comments for future users of the code --- SP2150i.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/SP2150i.py b/SP2150i.py index d306e12..6f2847e 100644 --- a/SP2150i.py +++ b/SP2150i.py @@ -10,7 +10,15 @@ class SP2150i(MessageBasedDriver): """ + Implements controls for the Princeton Instruments Acton Series SP2150i + Monochromater over the internal USB virtual serial port. + Communication with the device is a little finnicky, so if you run into + problems, I would suggest adding more buffer clears to avoid garbage + accumulating in the buffer and messing up your commands. + + Author: Peter Mintun (pmintun@uchicago.edu) + Date: 1/21/2016 """ DEFAULTS = {'ASRL': {'write_termination': '\r', 'read_termination': 'ok\r\n', @@ -61,7 +69,7 @@ def nm(self, wavelength): scan_rate = self.scan_speed delta_lambda = abs(curr_wavelength - wavelength) if delta_lambda > 0.1: - scan_time = ceil(delta_lambda / scan_rate * 60) + 2 # convert to seconds + scan_time = ceil(delta_lambda / scan_rate * 60) + 2 # need seconds self.clear_buffer() print('Scanning from {}nm to {}nm, will take {}sec'.format( curr_wavelength, wavelength, scan_time)) @@ -136,10 +144,9 @@ def grating_settings(self): for all possible gratings. """ gratings = [] - num_gratings = 8 self.write('?GRATINGS') self.read() - for i in range(0, num_gratings): + for i in range(0, self.num_gratings): gratings.append(self.read()) self.read() return gratings @@ -158,13 +165,12 @@ def write_message_wait(self, message, wait_time): print('== Monochromater Information ==') print('Selected turret:{}'.format(inst.turret)) print('Selected grating:{}'.format(inst.grating)) - #inst.turret = 3 - #inst.grating = 2 + # inst.turret = 3 + # inst.grating = 2 print('Selected turret:{}'.format(inst.turret)) print('Selected grating:{}'.format(inst.grating)) print('Turret Spacing:{}'.format(inst.turret_spacing)) print('Gratings:{}'.format(inst.grating_settings)) - #print('Grating information:{}'.format(inst.grating_settings)) for i in range(1, 20): inst.scan_speed = 100.0 @@ -177,13 +183,3 @@ def write_message_wait(self, message, wait_time): inst.nm = 500.0 print('Wavelength:{}nm'.format(inst.nm)) print('Cycle {} complete.'.format(i)) - - - - #turret = 2 - #grating = 2 - #print('Selected turret:{}'.format(inst.turret)) - #print('Selected grating:{}'.format(inst.grating)) - - - #inst.nm = 500.0 From f4a7f33cd7c7f5d9c1b125e15ad7e20697fc4e79 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Fri, 22 Jan 2016 11:26:09 -0600 Subject: [PATCH 46/61] now handles laser parameter setting correctly added code that queries if the laser is done with an operation and then restores control to the user after it finishes tuning for command setting. also tested everything and seems to work well --- CoBriteDX1.py | 377 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 371 insertions(+), 6 deletions(-) diff --git a/CoBriteDX1.py b/CoBriteDX1.py index 3b21cd2..a680296 100644 --- a/CoBriteDX1.py +++ b/CoBriteDX1.py @@ -1,5 +1,6 @@ from lantz import Feat, DictFeat, Action from lantz.messagebased import MessageBasedDriver +from pyvisa import constants from time import sleep @@ -8,21 +9,385 @@ class CoBriteDX1(MessageBasedDriver): """ """ - DEFAULTS = {'ASRL': {'write_termination': '\r', - 'read_termination': '', - }} + + # Set these parameters from the datasheet of your model. + channels = ['1,1,1'] # can add additional channels here! + + freq_min = 191.3 # THz + freq_max = 196.25 # THz + + min_power = 6 # dBm + max_power = 15.5 # dBm + offset_lim = 12.0 # GHz + + sbs_lims = (0, 1, 0.1) + + THz_to_nm = 299792 # 1 THz in nm + lambda_min = THz_to_nm/freq_max + lambda_max = THz_to_nm/freq_min + + on_off = {'off': 0, 'on': 1} + yes_no = {'no': 0, 'yes': 1} + + DEFAULTS = {'ASRL': {'write_termination': ';', + 'read_termination': ';', + 'baud_rate': 115200, + 'data_bits': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'utf-8', + 'timeout': 2000}} def initialize(self): """ """ super().initialize() + sleep(1) + self.echo = 'off' # need to do this to drop command echoing + + @Feat(values=on_off) + def echo(self): + """ + Checks to see if ECHO mode is enabled. Note that the code in this + driver assumes that the command echo is disabled. + """ + return int(self.query(':SYS:ECHO?')) + + @echo.setter + def echo(self, status): + """ + Sets echo mode to be controlled by on_off. + """ + print(':SYS:ECHO {}'.format(status)) + return self.query(':SYS:ECHO {}'.format(status)) + + @Feat() + def idn(self): + """ + Returns the laser firmware and hardware information. + """ + return self.query('*idn?') + + @Feat() + def layout(self): + """ + Returns the configuration of the attached system. + + This will be in format: + SYSTEM , , + ,...\n , , \n + , \n... + """ + return self.query(':SYS:LAY?') + + @DictFeat(keys=channels, limits=(lambda_min, lambda_max)) + def wavelength(self, channel): + """ + Returns the current laser wavelength, in nm. + """ + result = self.query(':SOUR:WAV? {}'.format(channel)) + return float(result.replace(channel + ',', '')) + + @wavelength.setter + def wavelength(self, channel, lambd): + """ + Sets the current laser wavelength to lambda (nm). + """ + delay = 1 + value = self.query(':SOUR:WAV {}, {:.4f}'.format(channel, lambd)) + while not self.operation_complete: + sleep(delay) + #self.read() + return value + + @DictFeat(keys=channels, limits=(lambda_min, lambda_max)) + def wavelength_lim(self, channel): + """ + Returns wavelength limits of the laser at channel in nm. + """ + result = self.query(':SOUR:WAV:LIM? {}'.format(channel)) + return self.split_floats(channel, result) - + @DictFeat(keys=channels, limits=(freq_min, freq_max)) + def frequency(self, channel): + """ + Returns the frequency of the laser at channel in THz. + """ + return float(self.query(':SOUR:FREQ? {}'.format(channel))) + + @frequency.setter + def frequency(self, channel, THz): + """ + Sets the frequency of the laser at channel to THz. + """ + delay = 1 + value = self.write(':SOUR:FREQ {},{:.4f}'.format(channel, THz)) + while not self.operation_complete: + sleep(delay) + self.read() + return value + @DictFeat(keys=channels, limits=(freq_min, freq_max)) + def frequency_lim(self, channel): + """ + Return the frequency limits of the laser at channel in THz. + """ + result = self.query(':SOUR:FREQ:LIM? {}'.format(channel)) + return self.split_floats(channel, result) + @DictFeat(keys=channels, limits=(0, offset_lim)) + def offset(self, channel): + """ + Returns the offset of the laser at channel in GHz. + """ + return float(self.query(':SOUR:OFF? {}'.format(channel))) + @offset.setter + def offset(self, channel, GHz): + """ + Sets the offset of the laser at channel to GHz. + """ + delay = 1 + value = self.write(':SOUR:OFF {},{:.4f}'.format(channel, GHz)) + while not self.operation_complete: + sleep(delay) + self.read() + return value + + @DictFeat(keys=channels, limits=(0, offset_lim)) + def offset_lim(self, channel): + """ + Returns the fine tuning limits of the laser at channel in GHz. + """ + return float(self.query(':SOUR:OFF:LIM? {}'.format(channel))) + + @DictFeat(keys=channels, limits=(min_power, max_power)) + def power(self, channel): + """ + Returns the laser power. + """ + return float(self.query(':SOUR:POW? {}'.format(channel))) + + @power.setter + def power(self, channel, dBm): + """ + Sets the power of the laser at channel to dBm. + """ + delay = 1 + value = self.write(':SOUR:POW {},{:.4f}'.format(channel, dBm)) + while not self.operation_complete: + sleep(delay) + self.read() + return value + + @DictFeat(keys=channels, limits=(0, max_power)) + def power_lim(self, channel): + """ + Returns maximum power of the laser at channel in dBm. + """ + result = self.query(':SOUR:FREQ:LIM? {}'.format(channel)) + return self.split_floats(channel, result) + + @DictFeat(keys=channels) + def limits(self, channel): + """ + Returns all limits of the laser at channel in form: + [Min freq (THz), Max freq(THz), Tune range (GHz), Min pow (dBm), + Max power (dBm)] + """ + result = self.query(':SOUR:LIM? {}'.format(channel)) + return self.split_floats(channel, result) + + @DictFeat(keys=channels, values=on_off) + def state(self, channel): + """ + Returns the current state of the laser at channel. + """ + return int(self.query(':SOUR:STAT? {}'.format(channel))) + + @state.setter + def state(self, channel, on_status): + """ + Sets the state of the laser at channel to on_status + """ + delay = 1 + value = self.write(':SOUR:STAT {},{}'.format(channel, on_status)) + while not self.operation_complete: + sleep(delay) + self.read() + return value + + @DictFeat(keys=channels) + def configuration(self, channel): + """ + Returns the current laser configuration in format: + [Freq (THz), Fine tuning (GHz), Power (dBm), On/off (0/1), Busy (0/1), + Dither (0/1)] + """ + result = self.query(':SOUR:CONF? {}'.format(channel)).split(',') + return (list(map(float, result[0:3])) + list(map(int, result[3:6]))) + + @DictFeat(keys=channels, values=yes_no) + def busy(self, channel): + """ + Query state of a device. + If 1, the laser is currently tuning and not settled. + """ + return int(self.query(':SOUR:BUSY? {}'.format(channel))) + + @DictFeat(keys=channels) + def monitor(self, channel): + """ + Returns monitor information from laser in format: + [LD chip temp (deg C), LD base temp (deg C), LD chip current (mA), + TEC current (mA)] + """ + result = self.query(':SOUR:MON? {}'.format(channel)) + return self.split_floats(channel, result) + + @DictFeat(keys=channels, values=on_off) + def dither(self, channel): + """ + Queries whether or not the laser at channel is on or off. + """ + return int(self.query(':SOUR:DIT? {}'.format(channel))) + + @dither.setter + def dither(self, channel, dither_status): + """ + Sets the dither tone of the laser at channel to be dither_status. + """ + return self.query(':SOUR:DIT {},{}'.format(channel, dither_status)) + + @DictFeat(keys=channels, values=on_off) + def sbs(self, channel): + """ + Queries whether or not the stimulated Brillouin scattering (SBS) + supression is on or off. + """ + return int(self.query(':SOUR:SBS:STAT? {}'.format(channel))) + + @sbs.setter + def sbs(self, channel, sbs_status): + """ + Sets the stimulated Brillouin scattering (SBS) supression to be + sbs_status. + """ + return self.query(':SOUR:SBS:STAT {},{}'.format(channel, sbs_status)) + + @Feat(limits=sbs_lims) + def sbs_freq(self): + """ + Queries the stimulated Brillouin scattering (SBS) supression amplitude + for all lasers in GHz. + """ + return float(self.query(':SOUR:SBS:FREQ?')) + + @sbs_freq.setter + def sbs_freq(self, GHz): + """ + Sets the stimulated Brillouin scattering (SBS) supression amplitude + for all lasers to GHz. + """ + return self.query(':SOUR:SBS:FREQ {}'.format(GHz)) + + @Feat() + def operation_complete(self): + """ + Operation complete query, checks to see if all lasers are settled. + """ + if self.query('*opc?') == '1': + return True + else: + return False + + @Action() + def save_curr_state(self, channel): + """ + Permanently saves the current laser port state for channel, which will + be loaded again after a power on/off cycle or reset. Autostart must be + enabled for this to work. + """ + if channel in self.channels: + return self.query(':SYS:SCSTAT'.format(channel)) + else: + print('Invalid channel.') + return 0 + + @Feat(values=on_off) + def autostart(self): + """ + Check to see if laser autostart is enabled. + """ + return int(self.query(':SYS:AUTOSTA?')) + + @autostart.setter + def autostart(self, autostart_status): + """ + Sets the laser autostart to be autostart_status. + """ + return self.query(':SYS:AUTOSTA'.format(autostart_status)) + + def split_floats(self, channel, result): + """ + Helper function for returning tuples returned during communication. + """ + ret_vals = [] + for val in result.replace(channel + ',', '').split(','): + ret_vals.append(float(val)) + return ret_vals if __name__ == '__main__': - with CoBriteDX1('USB:XXYYZZ') as inst: + with CoBriteDX1.via_serial(8) as inst: + inst.echo = 'off' + print('== System Information ==') + print('IDN:{}'.format(inst.idn)) + print('Layout:{}'.format(inst.layout)) + print('Autostart:{}'.format(inst.autostart)) + + print('== System Control Demo ==') + + for chan in inst.channels: + print('Laser at channel: {}'.format(chan)) + print('== Laser Parameters ==') + print('Wavelength: {} nm'.format(inst.wavelength[chan])) + print('Frequency: {} THz'.format(inst.frequency[chan])) + print('Power: {} dBm'.format(inst.power[chan])) + print('Offset: {} GHz'.format(inst.offset[chan])) + print('State: {}'.format(inst.state[chan])) + print('Busy?: {}'.format(inst.busy[chan])) + print('Configuration: {}'.format(inst.configuration[chan])) + print('Dither?: {}'.format(inst.dither[chan])) + print('SBS?: {}'.format(inst.sbs[chan])) + print('SBS Frequency: {} GHz'.format(inst.sbs_freq)) + + print('== Laser limits ==') + print('Wavelength limits: {} nm'.format(inst.wavelength_lim[chan])) + print('Frequency limits: {} THz'.format(inst.frequency_lim[chan])) + print('Offset limit: {} GHz'.format(inst.offset_lim[chan])) + print('Power limit: {} dBm'.format(inst.power_lim[chan])) + print('All limits: {}'.format(inst.limits[chan])) + + print('== Laser Control Demo ==') + #print('Saving current state...') + #inst.save_curr_state(channel=chan) - # inst.nm = 400.0 + inst.frequency[chan] = 195.3 + print('Configuration: {}'.format(inst.configuration[chan])) + inst.wavelength[chan] = 1530.1234 + print('Configuration: {}'.format(inst.configuration[chan])) + inst.frequency[chan] = 191.3 + inst.offset[chan] = 6.5 + inst.state[chan] = 'on' + print('Configuration: {}'.format(inst.configuration[chan])) + print('Monitor: {}'.format(inst.monitor[chan])) + inst.offset[chan] = 12 + inst.power[chan] = 6.1 + print('Configuration: {}'.format(inst.configuration[chan])) + print('Busy:{}'.format(inst.busy[chan])) + inst.power[chan] = 15.5 + print('Configuration: {}'.format(inst.configuration[chan])) + inst.offset[chan] = 0.0 + print('Configuration: {}'.format(inst.configuration[chan])) + inst.state[chan] = 'off' + print('Configuration: {}'.format(inst.configuration[chan])) From 52b0502804934985620030bcf89ab701f6787c19 Mon Sep 17 00:00:00 2001 From: kcmiao Date: Tue, 26 Jan 2016 15:15:53 -0600 Subject: [PATCH 47/61] Update LantzSetup.md --- LantzSetup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LantzSetup.md b/LantzSetup.md index de006b4..4f27ad1 100644 --- a/LantzSetup.md +++ b/LantzSetup.md @@ -46,7 +46,7 @@ This command installs the colorama (used for producing colorful terminal output) For those interested in using custom drivers in the group's modified release, this step will allow pip to install a version of Lantz that is synced with the group Github. -Git is a prequisite for this step. Obtain it here: https://git-scm.com/download/win +Git is a prerequisite for this step. Obtain it here: https://git-scm.com/download/win Once installed, navigate to the location where you would like to have the group's modified Lantz release reside. Then, run: From cc060c6056b6bd8cfa06395f723378cfa81c6ed1 Mon Sep 17 00:00:00 2001 From: AlexBourassa Date: Wed, 3 Feb 2016 17:56:30 -0600 Subject: [PATCH 48/61] ITC4020 drivers main functionalities completed --- lantz/drivers/thorlabs/ITC4020.py | 114 ++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 lantz/drivers/thorlabs/ITC4020.py diff --git a/lantz/drivers/thorlabs/ITC4020.py b/lantz/drivers/thorlabs/ITC4020.py new file mode 100644 index 0000000..3e37102 --- /dev/null +++ b/lantz/drivers/thorlabs/ITC4020.py @@ -0,0 +1,114 @@ + + +from lantz import Feat, DictFeat, Action +from lantz.errors import InstrumentError +from lantz.messagebased import MessageBasedDriver + +from time import sleep + +class ITC4020(MessageBasedDriver): + + + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n'}} + + COM_delay = 0.2 + + def write(self, *args, **kwargs): + super(ITC4020, self).write(*args, **kwargs) + sleep(self.COM_delay) + + @Feat(read_once=True) + def idn(self): + return self.query('*IDN?') + + @Feat + def key_locked(self): + return bool(int(self.query("OUTP:PROT:KEYL:TRIP?"))) + + @Feat + def temperature(self): + return float(self.query("MEAS:SCAL:TEMP?")) + + @Feat + def temperature_setpoint(self): + return float(self.query("SOUR2:TEMP?")) + + @Feat + def LD_current_setpoint(self): + return float(self.query("SOUR:CURR?")) + + @Feat(limits=(0,1)) + def LD_current(self): + return float(self.query("MEAS:CURR?")) + + @LD_current.setter + def LD_current(self, value): + inst.write("SOUR:CURR {:.5f}".format(value)) + + @Feat + def output_state(self): + return bool(int(self.query("OUTP:STAT?"))) + + @Feat + def PD_power(self): + return float(self.query("MEAS:POW2?")) + + @Action() + def read_error_queue(self): + no_error = '+0,"No error"' + error = inst.query("SYST:ERR:NEXT?") + while(error != no_error): + print(error) + error = inst.query("SYST:ERR:NEXT?") + + @Action() + def turn_on_seq(self, temp_error=0.05, current_error=0.005): + if self.output_state: + print("Laser is already ON!") + return + + #Turn ON sequence: + # 1. TEC ON + # 2. Wait for temperature == set_temperature + # 3. LD ON + # 4. Wait for current == set_current + + # 1. TEC ON + self.write("OUTP2:STAT ON") + + # 2. Wait + setpoint = self.temperature_setpoint + while(abs(setpoint-self.temperature)>temp_error):pass + + + # 3. LD ON + self.write("OUTP1:STAT ON") + + # 4. Wait + setpoint = self.LD_current_setpoint + while(abs(setpoint-self.LD_current)>current_error):pass + + @Action() + def turn_off_seq(self, current_error=0.005): + #Turn OFF sequence: + # 1. LD OFF + # 2. Wait for current == 0 + # 3. TEC OFF + + # 1. LD OFF + self.write("OUTP1:STAT OFF") + + # 2. Wait + while(abs(self.LD_current)>current_error):pass + + # 1. TEC OFF + self.write("OUTP2:STAT OFF") + + + + +if __name__ == '__main__': + inst = ITC4020('USB0::0x1313::0x804A::M00336070::INSTR') + inst.initialize() + print(inst.query('*IDN?')) From 6b0a507e024e9615a728f0c48ffd079226854f74 Mon Sep 17 00:00:00 2001 From: AlexBourassa Date: Thu, 4 Feb 2016 18:58:54 -0600 Subject: [PATCH 49/61] testing the removal of one line --- lantz/drivers/thorlabs/ITC4020.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lantz/drivers/thorlabs/ITC4020.py b/lantz/drivers/thorlabs/ITC4020.py index 3e37102..bac16a4 100644 --- a/lantz/drivers/thorlabs/ITC4020.py +++ b/lantz/drivers/thorlabs/ITC4020.py @@ -107,7 +107,6 @@ def turn_off_seq(self, current_error=0.005): - if __name__ == '__main__': inst = ITC4020('USB0::0x1313::0x804A::M00336070::INSTR') inst.initialize() From 168f6681babc0759b354034b0d3b9ab6bcda9b5e Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 9 Feb 2016 21:17:12 -0600 Subject: [PATCH 50/61] original PEM90 commit, adding DAQ code here for posterity basically prepping for big merge of all files, wish me luck! --- HindsPEM90.py | 120 ++++++++++++++++++++++ NI/DAQ_FSM_Demo.py | 244 +++++++++++++++++++++++++++++++++++--------- NI/NI64/base.py | 43 +++++++- NI/NI64/channels.py | 134 +++++++++++++++++++++--- NI/NI64/tasks.py | 11 +- NI/foreign.py | 6 +- 6 files changed, 484 insertions(+), 74 deletions(-) create mode 100644 HindsPEM90.py diff --git a/HindsPEM90.py b/HindsPEM90.py new file mode 100644 index 0000000..249c2a2 --- /dev/null +++ b/HindsPEM90.py @@ -0,0 +1,120 @@ +from lantz import Feat, DictFeat, Action +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants + +from numpy import ceil + +from time import sleep + + +class HindsPEM90(MessageBasedDriver): + """ + + """ + + # Set paramters here + on_off = {'off': 0, 'on': 1} + waves_limits = (0.0, 19999.9) + retardation_limits = (0.0, 1.0) + + DEFAULTS = {'ASRL': {'write_termination': '\n', + 'read_termination': '\n', + 'baud_rate': 9600, + 'data_bits': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'utf-8', + 'timeout': 2000}} + + def initialize(self): + """ + """ + super().initialize() + + @Feat(values=on_off) + def echo(self): + """ + Checks to see if ECHO mode is enabled. Note that the code in this + driver assumes that the command echo is disabled. + """ + return int(self.query(':SYS:ECHO?')) + + @echo.setter + def echo(self, status): + """ + Sets echo mode to be controlled by on_off. + """ + print('E:{}'.format(status)) + return self.query('E:{}'.format(status)) + + + @Feat(limits=waves_limits) + def wavelength(self): + """ + Reads out current wavelength in nm + """ + return float(self.query('W')) + + @wavelength.setter + def wavelength(self, nm): + """ + Sets wavelength in nm. + """ + print('W:{}'.format(nm)) + return self.query('W:{0:0>7.1f}'.format(nm)) + + @Feat(limits=retardation_limits) + def retardation(self): + """ + Reads out current retardation in wave units + """ + return float(self.query('R')/1000.0) + + @retardation.setter + def retardation(self, wave_units): + """ + Sets retardation in wave units. + """ + print('R:{}'.format(wave_units)) + return self.query('R:{0:04}'.format(ceil(wave_units*1000))) + + @Feat() + def frequency(self): + """ + Reads out the reference frequency in hertz + """ + return float(self.query('F')) + + @Feat() + def frequency2(self): + """ + Reads out the reference frequency2 in hertz + """ + return float(self.query('2F')) + + @Feat(values=on_off) + def inhibit(self): + """ + Returns 0 for the retardation inhibitor + """ + return 0 + + @inhibitor.setter + def inhibitor(self, status): + """ + Sets the mode to be controlled by on_off. + """ + print('I:{}'.format(status)) + return self.query('I:{}'.format(status)) + + @Action() + def reset(self): + """ + Resets PEM-90 to default factory settings. + """ + return self.query('Z') + +if __name__ == '__main__': + with HindsPEM90.via_serial(10) as inst: + + echo = 'off' diff --git a/NI/DAQ_FSM_Demo.py b/NI/DAQ_FSM_Demo.py index cba7633..4d03489 100644 --- a/NI/DAQ_FSM_Demo.py +++ b/NI/DAQ_FSM_Demo.py @@ -4,15 +4,18 @@ from lantz.drivers.ni.daqmx import System from lantz.drivers.ni.daqmx import AnalogOutputTask, VoltageOutputChannel +from lantz.drivers.ni.daqmx import AnalogInputTask, VoltageInputChannel #CounterInputTask, CountEdgesChannel, DigitalOutputChannel, DigitalOutputTask from lantz import Feat, DictFeat, Action import numpy as np -from numpy import abs, ceil, pi, linspace, cos, ones, size +from numpy import abs, ceil, pi, linspace, cos, ones from time import sleep +from ctypes import c_uint32 + class Newport_FSM(System): """ @@ -42,6 +45,10 @@ def initialize(self): 'ao_channel': 'ao1'} } + self.ao_smooth_rate = 50000.0 # Hz + self.ao_smooth_steps_per_volt = 1000.0 # steps/V + + for dim in self.fsm_dimensions: chan_name = dim phys_chan = self.dev_name + self.fsm_dimensions[dim]['ao_channel'] @@ -58,7 +65,7 @@ def finalize(self): """ Stops AO tasks for controlling FSM. """ - self.task.stop() + #self.task.stop() @Feat() def abs_position_pair(self): @@ -91,7 +98,7 @@ def abs_position_single(self, dimension, pos): """ return 0 - def convert_V_to_um(self, dime, V): + def convert_V_to_um(self, dim, V): """ Returns micron position corresponding to channel voltage. """ @@ -121,74 +128,211 @@ def ao_smooth(self, x_init, x_final, channel): Copy of dchristle's algorithm. (I know what you're trying to do, and just stop.) """ - ao_smooth_rate = 5000.0 # Hz - ao_smooth_steps_per_volt = 1000.0 # steps/V - v_init = self.convert_um_to_V(x_init, channel) - v_final = self.convert_um_to_V(x_final, channel) - - n_steps = ceil(abs(v_final - v_init) * ao_smooth_steps_per_volt) - - v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( - linspace(0.0, pi, n_steps))) / 2.0 + v_data = self.AO_smooth_func(x_init, x_final, channel) + n_steps = v_data.size - samples = size(v_data) + samp_arr = np.empty(n_steps * 2, dtype=float) - print(v_data) - print(v_data.size) if channel == 'x': - v_data = np.vstack((v_data, np.zeros(v_data.size))) + samp_arr[0::2] = v_data + samp_arr[1::2] = np.zeros(n_steps) else: - v_data = np.vstack((np.zeros(v_data.size), v_data)) + samp_arr[0::2] = np.zeros(n_steps) + samp_arr[1::2] = v_data + + + + print(samp_arr) + print(n_steps) + + #samp_arr = 1.5 - # TODO: Figure out relevant parameters for this function! - # Probably will have to rewrite the function. - self.task.configure_timing_sample_clock(source='OnboardClock', rate=ao_smooth_rate, sample_mode='finite') - self.task.write(v_data, auto_start=False, timeout=10.0, group_by='channel') + self.task.configure_timing_sample_clock(source='OnboardClock', rate=self.ao_smooth_rate, + sample_mode='finite', + samples_per_channel=int(n_steps)) + + self.task.write(data=samp_arr, auto_start=False, timeout=0, group_by='scan') + try: self.task.start() print('Started FSM Scan') - sleep(samples*1.0/ao_smooth_rate) - print('Expected scan time:{} sec'.format(samples*1.0/ao_smooth_rate)) + print('Expected scan time:{} sec'.format(n_steps*1.0/self.ao_smooth_rate)) + print('Number of scan points:{}'.format(n_steps)) + sleep(n_steps*1.0/self.ao_smooth_rate) self.task.stop() - except e: - print('Failed in counter start phase:{}'.format(e)) + except: + print('Failed in counter start phase...') + + + def AO_smooth_func(self, x_init, x_final, channel): + """ + :param x_min: minimum x coordinate (in microns) for scan + :param x_max: maximum x coordinate (in microns) for scan + :param channel: channel to scan over (i.e. x or y) + + :return: array of voltages to write + """ + + v_init = self.convert_um_to_V(x_init, channel) + v_final = self.convert_um_to_V(x_final, channel) + + n_steps = int(ceil(abs(v_final - v_init) * self.ao_smooth_steps_per_volt)) + + v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( + linspace(0.0, pi, n_steps))) / 2.0 + + return v_data - #self.task.stop() - def write_and_count(self, devchan, ctrchan, src, aochan, vdata, freq=10000.0, minv=-10.0, - maxv=10.0, timeout=10.0): + def FSM_2D_counts(self, x_min, x_max, y_min, y_max, freq, input_voltage_chan): """ - :param devchan (string): device channel specifier - :param ctrchan: - :param src: - :param aochan: - :param vdata: - :param freq: - :param minv: - :param maxv: - :param timeout: - :return: + Scan FSM by writing an array of points over analog out + + Args: + :param ctrchan: (string) device channel for counter + :param x_min: (float) minimum x position (in microns) for scan + :param x_max: (float) maximum x position (in microns) for scan + :param y_min: (float) minimum y position (in microns) for scan + :param y_max: (float) maximum y position (in microns) for scan + :param freq: (float) frequency for FSM scan + + :param input_voltage_chan: (str) device channel for analog input voltage to read (i.e. 'dev1/ai0') + + :return: returns a tuple containing two float arrays. + The first array contains the x-y positions used for the scan. + The second array contains the counts corresponding to each position. """ + x_voltages = self.AO_smooth_func(x_min, x_max, 'x') + nx_steps = x_voltages.size + y_voltages = self.AO_smooth_func(y_min, y_max, 'y') + ny_steps = y_voltages.size + + x_pts = self.convert_V_to_um('x', x_voltages) + y_pts = self.convert_V_to_um('y', y_voltages) + + + # Create a 2D array of tuples for the xy points in the scan. + # Note that the AO smooth procedure means these points do not fall on a perfect grid + # That's why I've calculated them here and returned them to be available to the user. + + # Note: change counts data type to be int, if using edge count APD + count_tuple = np.dtype([('x', 'float'), ('y', 'float'), ('counts', 'float')]) + xy_pts = np.zeros([nx_steps, ny_steps], dtype=count_tuple) + + # This avoids a nested for loop (if you have large array of points, O(N^2)) + # I assumed that numpy does both of these operations relatively fast... + xy_pts['x'] = np.tile(x_pts, ny_steps).reshape(nx_steps, ny_steps) + xy_pts['y'] = np.repeat(y_pts, nx_steps).reshape(nx_steps, ny_steps) + + n_steps = (nx_steps + 1) * ny_steps + v_data = np.zeros(2*n_steps) + + # Avoids piezo hysteresis by ensuring they always take data scanning in the same direction + hyst_offset = 0.002 #volts + x_voltages = np.hstack([x_voltages[0] - hyst_offset, x_voltages]) + + v_data[0::2] = np.tile(x_voltages, ny_steps) #scan in x repeatedly (x_min to x_max, x_min to x_max, ...) + v_data[1::2] = np.repeat(y_voltages, nx_steps + 1) #scan in y slowly (y_min, y_min, ..., y_min+1, y_min+1, ..., y_max) + + + #import matplotlib.pyplot as plt + + #plt.plot(v_data[0::2]) + #plt.plot(v_data[1::2]) + #plt.plot(v_data[3::2]) + #plt.show() + + print(v_data.shape) + counts = np.zeros(n_steps) + + count_task = AnalogInputTask('counter') + counter_channel = VoltageInputChannel(input_voltage_chan) + count_task.add_channel(counter_channel) + + + # Synchronize tasks to both read at a rate, which is controlled by the internal DAQ clock. + sample_clock = 'OnboardClock' + self.task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=n_steps) + + count_task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=n_steps) + + # Write the array of voltages that will be used to control the DAQ here. + self.task.write(v_data, auto_start=False, timeout=0, group_by='channel') + + # Synchronize tasks to both start on the AnalogInput start trigger + trigger_src = 'ai/StartTrigger' + self.task.configure_trigger_digital_edge_start(trigger_src) + + # This arms the trigger, thereby starting the AnalogOutputTask (FSM) simultaneously + # with the AnalogInputTask + self.task.start() + + print('Tasks configured.') + + + try: + print('Starting FSM Scan...') + sleep(10) + scan_time = n_steps*1.0/freq + print('Expected scan time:{} sec'.format(scan_time)) + print('Number of scan points:{}'.format(n_steps)) + # Executing the read task executes the two tasks synchronously + t_extra = 1.0 # adds 1 second grace period, probably unnecessary + counts = count_task.read(samples_per_channel=n_steps, timeout=scan_time + t_extra) + + # Stop both tasks + self.task.stop() + count_task.stop() + + except: + print('FSM: 2D Scan Failed in counter start phase.') + + counts = counts.reshape([nx_steps + 1, ny_steps]) + + xy_pts['counts'] = counts[1:,:] #drop first column + + # This then returns a FSM 2D scan image as a tuple (x_coord, y_coord, counts) + return xy_pts + + + + + + + if __name__ == '__main__': with Newport_FSM() as inst: print('testing!') + print('Sweep from -5.0 to 5.0') + #inst.ao_smooth(-5.0, 5.0, 'x') + # print('Sweep from 5.0 to -25.0') + # inst.ao_smooth(5.0, -25.0, 'x') + # sleep(1) + # print('Sweep from -25.0 to 25.0') + # inst.ao_smooth(-25.0, 25.0, 'x') + # sleep(1) + # inst.ao_smooth(-5.0, 5.0, 'y') + # sleep(1) + # inst.ao_smooth(5.0, -25.0, 'y') + # sleep(1) + # inst.ao_smooth(-25.0, 25.0, 'y') + # sleep(1) + + print('testing 2D count and scan...') + inst.FSM_2D_counts(-1.0, 1.0, -1.0, 1.0, 500.0, 'dev1/ai0') + + print('derp derp derp derp!') + + - inst.ao_smooth(-5.0, 5.0, 'x') - sleep(1) - inst.ao_smooth(5.0, -25.0, 'x') - sleep(1) - inst.ao_smooth(-25.0, 25.0, 'x') - sleep(1) - inst.ao_smooth(-5.0, 5.0, 'y') - sleep(1) - inst.ao_smooth(5.0, -25.0, 'y') - sleep(1) - inst.ao_smooth(-25.0, 25.0, 'y') - sleep(1) diff --git a/NI/NI64/base.py b/NI/NI64/base.py index ba7c511..8c0b730 100644 --- a/NI/NI64/base.py +++ b/NI/NI64/base.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -""" +""" lantz.drivers.ni.daqmx.base ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -855,8 +854,9 @@ def configure_timing_sample_clock(self, source='on_board_clock', rate=1, active_ """ if source == 'on_board_clock': source = None - self.samples_per_channel = samples_per_channel self.sample_mode = sample_mode + + self.lib.CfgSampClkTiming(source, rate, active_edge, sample_mode, samples_per_channel) def configure_timing_burst_handshaking_export_clock(self, *args, **kws): @@ -956,8 +956,15 @@ def configure_trigger_digital_edge_start(self, source, edge='rising'): Specifies on which edge of a digital signal to start acquiring or generating samples: rising or falling edge(s). """ + from ctypes import c_wchar_p + + if edge == 'falling': + edge_val = int(Constants.Val_Falling) + else: + edge_val = int(Constants.Val_Rising) - self.lib.CfgDigEdgeStartTrig(self, source, edge) + + self.lib.CfgDigEdgeStartTrig(source, edge_val) @Action(values=(str, str, _WHEN_MATCH)) def configure_trigger_digital_pattern_start(self, source, pattern, match=True): @@ -981,7 +988,7 @@ def configure_trigger_digital_pattern_start(self, source, pattern, match=True): Specifies the conditions under which the trigger occurs: pattern matches or not. """ - self.lib.CfgDigPatternStartTrig(self, source, pattern, match) + self.lib.CfgDigPatternStartTrig(source, pattern, match) def configure_trigger_disable_start(self): """ @@ -1034,6 +1041,18 @@ def configure_analog_edge_reference_trigger(self, source, slope='rising',level=1 self.lib.CfgAnlgEdgeRefTrig(source, slope, level, pre_trigger_samps) + @Action() + def send_software_trigger(self, triggerID=Constants.Val_AdvanceTrigger): + """ + + :param triggerID: (int32) Specifies which software trigger to generate. + + DAQmx_Val_AdvanceTrigger Generate the advance trigger + + """ + + self.lib.SendSoftwareTrigger(triggerID) + @Feat(values=(str, _WHEN_WINDOW, None, None, None)) def configure_analog_window_reference_trigger(self, source, when='entering',top=1.0, bottom=1.0, pre_trigger_samps=0): @@ -1153,6 +1172,20 @@ def configure_digital_pattern_reference_trigger(self, source, pattern, match=Tru self.lib.CfgDigPatternRefTrig(self, source, pattern, match, pre_trigger_samps) + @Action() + def configure_output_buffer(self, samples_per_channel=1000): + """ + + :params: taskHandle (TaskHandle): task to configure + :params: num_samps_per_chan (uInt32): number of samples to output per channel. + Zero indicates no buffer should be allocated. Use a buffer size of 0 to perform + a hardware-timed operation without using a buffer. + + :return: status (int32) - error code returned by the function in the event of an + error or warning + """ + return self.lib.CfgOutputBuffer(self, samples_per_channel) + @Action() def disable_reference_trigger(self): """ diff --git a/NI/NI64/channels.py b/NI/NI64/channels.py index 754452c..fe70cd6 100644 --- a/NI/NI64/channels.py +++ b/NI/NI64/channels.py @@ -274,20 +274,24 @@ class CountEdgesChannel(Channel): CHANNEL_TYPE = 'CI' + CREATE_FUN = 'CreateCICountEdgesChan' - def __init__ (self, counter, name="", edge='rising', init=0, direction='up'): + + def __init__ (self, phys_counter, name="", edge='rising', init=0, direction='up', task=None): if edge == 'rising': - edge_val = Constants.RISING + edge_val = Constants.Val_Rising else: - edge_val = Constants.FALLING + edge_val = Constants.Val_Falling if direction == 'up': - direction_val = Constants.COUNT_UP + direction_val = Constants.Val_CountUp else: - direction_val = Constants.COUNT_DOWN + direction_val = Constants.Val_CountDown + + self._create_args = (phys_counter, name, edge_val, init, direction_val) - self.lib.CreateCICountEdgesChan(counter, name, edge_val, direction_val) + super().__init__(task=task, name=name) class LinearEncoderChannel(Channel): @@ -508,37 +512,117 @@ class MeasureFrequencyChannel(Channel): None. """ + CREATE_FUN = 'CreateCIFreqChan' + + def __init__(self, counter, name='', min_val=1e2, max_val=1e3, units="hertz", edge="rising", method="low_freq", - meas_time=1.0, divisor=1, custom_scale_name=None): + meas_time=1.0, divisor=1, custom_scale_name=None, + task=None): + self.data_type = float + name='test' + + assert divisor > 0 if method == 'low_freq': - meas_meth_val = Constants.LOW_FREQ1_CTR + meas_meth_val = Constants.Val_LowFreq1Ctr elif method == 'high_freq': - meas_meth_val = Constants.HIGH_FREQ2_CTR + meas_meth_val = Constants.Val_HighFreq2Ctr elif method == 'large_range': - meas_meth_val = Constants.LARGE_RANGE2_CTR + meas_meth_val = Constants.Val_LargeRng2Ctr if units != ('hertz', 'ticks'): custom_scale_name = units - units = Constants.FROM_CUSTOM_SCALE + units_val = Constants.Val_FromCustomScale else: custom_scale_name = None if units == 'hertz': - units = Constants.HZ + units_val = Constants.Val_Hz else: - units = Contstants.TICKS + units_val = Constants.Val_Ticks + + if edge == 'rising': + edge_val = Constants.Val_Rising + else: + edge_val = Constants.Val_Falling + + self._create_args = (counter, name, min_val, max_val, units_val, + edge_val, meas_meth_val, meas_time, divisor, + custom_scale_name) + + super().__init__(task=task, name=name) + + +class CounterOutTicksChannel(Channel): + """ + Class for CounterOutputTicks Channel + + :class: + """ + + CREATE_FUN = 'DAQmxCreateCOPulseChanTicks' + + + + def __init__(self, counter, name='', source_terminal='', idle_state='high', init_delay=1, low_ticks=10, high_ticks=10, + task=None): + """ + Create channel(s) to generate digital pulses defined by the number of timebase ticks that the pulse is at a + high state and the number of timebase ticks that the pulse is at a low state and also adds the channel to the + task you specify with taskHandle. The pulses appear on the default output terminal of the counter unless you + select a different output terminal. + - self.lib.CreateCIFreqChan(counter, name, min_max[0], min_max[1], - units_val, edge_val, meas_meth_val, - meas_time, divisor, custom_scale_name) + Args: + taskHandle: TaskHandle to which to add the channels that this function creates. + counter: Name of the counter to use to create virtual channels. You can specify a list or range of physical + channels. + nameToAssignToChannel: (string) name(s) to assign to the created virtual channel(s). If you do not specify + a name, NI-DAQmx uses the physical channel name as the virtual channel name. If you specify your own names + for nameToAssignToChannel, you must use the names when you refer to these channels in other NI-DAQmx + functions. If you create multiple virtual channels with one call to this function, you can specify a list + of names separated by commas. If you provide fewer names than the number of virtual channels you create, + NI-DAQmx automatically assigns names to the virtual channels. + + sourceTerminal: (string) terminal to which you connect an external timebase. You also can specify a source + terminal by using a terminal name. + + idleState: (int32) resting state of the output terminal. + Value Description + DAQmx_Val_High High state. + DAQmx_Val_Low Low state. + + initialDelay: (int32) number of timebase ticks to wait before generating the first pulse. + + lowTicks: (int32) The number of timebase ticks that the pulse is low. + + highTicks: (int32) number of timebase ticks that the pulse is high. + + Returns: + + status: The error code returned by the function in the event of an error or warning. A value of 0 + indicates success. A positive value indicates a warning. A negative value indicates an error. + + """ + + if idle_state == 'high': + idle_state_val = Constants.Val_High + else: + idle_state_val == 'low' + + + + self._create_args = (counter, name, source_terminal, idle_state_val, init_delay, low_ticks, high_ticks) + + + super().__init__(name=name, task=task) def create_channel_frequency(self, counter, name="", units='hertz', idle_state='low', delay=0.0, freq=1.0, duty_cycle=0.5): @@ -747,3 +831,21 @@ def create_channel_time(self, counter, name="", units="seconds", idle_state='low idle_state_val = self._get_map_value('idle_state', idle_state_map, idle_state) return CALL('CreateCOPulseChanTime', self, counter, name, units_val, idle_state_val, float64 (delay), float64(low_time), float64(high_time))==0 + + + +class CounterOutPulseChannel(Channel): + """ + Counter output pulse channel + """ + + def __init__(self): + """ + + + + Returns + -------- + + status: int32 + """ \ No newline at end of file diff --git a/NI/NI64/tasks.py b/NI/NI64/tasks.py index 278ed4d..e273336 100644 --- a/NI/NI64/tasks.py +++ b/NI/NI64/tasks.py @@ -204,6 +204,7 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): data = np.asarray(data, dtype = np.float64) + number_of_channels = self.number_of_channels() if data.ndim == 1: @@ -224,7 +225,11 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): else: samples_per_channel = data.shape[-1] - err, count = self.lib.WriteAnalogF64(samples_per_channel, auto_start, + print(data) + + samps_per_channel = int(samples_per_channel) + + err, count = self.lib.WriteAnalogF64(samps_per_channel, auto_start, timeout, group_by, data.ctypes.data, RetValue('i32'), None) @@ -232,6 +237,7 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): return count + class DigitalTask(Task): @Action(units=(None, 'seconds', None), values=(None, None, _GROUP_BY)) @@ -442,6 +448,7 @@ class CounterInputTask(Task): CHANNEL_TYPE = 'CI' + def read_scalar(self, timeout=10.0): """Read a single floating-point sample from a counter task. Use this function when the counter sample is scaled to a @@ -514,7 +521,7 @@ def read(self, samples_per_channel=None, timeout=10.0): data = np.zeros((samples_per_channel,),dtype=np.int32) - err, count = self.lib.ReadCounterU32(samples_per_channel, float64(timeout), + err, count = self.lib.ReadCounterU32(samples_per_channel, float(timeout), data.ctypes.data, data.size, RetValue('i32'), None) return data[:count] diff --git a/NI/foreign.py b/NI/foreign.py index a5108ab..f4fb8a4 100644 --- a/NI/foreign.py +++ b/NI/foreign.py @@ -223,7 +223,7 @@ def _preprocess_args(self, name, *args): # args to ctypes elif isinstance(arg, int): if arg > 0xFFFFFFFF: - new_args.append(cast(arg, POINTER(c_uint64))) + new_args.append(cast(arg,POINTER(c_uint64))) else: new_args.append(arg) elif isinstance(arg, float): @@ -235,6 +235,10 @@ def _preprocess_args(self, name, *args): def _wrapper(self, name, func, *args): new_args, collect = self._preprocess_args(name, *args) + #print(name) + #print(func) + #print('Old args:{}'.format(*args)) + #print('New args:{}'.format(new_args)) try: ret = func(*new_args) From ee985b9cc4c871cc488e03503b6b5cf4763cc6bd Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 10 Feb 2016 19:06:58 -0600 Subject: [PATCH 51/61] changed filenames to be consistent with other drivers --- lantz/drivers/acton/{SP2150i.py => sp2150i.py} | 0 lantz/drivers/cobrite/{CoBriteDX1.py => cobritedx1.py} | 0 lantz/drivers/hinds/{HindsPEM90.py => hindspem90.py} | 0 lantz/drivers/lakeshore/{Lakeshore332.py => lakeshore332.py} | 0 lantz/drivers/newport/{Newport69907.py => newport69907.py} | 0 .../{SignalRecovery7265.py => signalrecovery7265.py} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename lantz/drivers/acton/{SP2150i.py => sp2150i.py} (100%) rename lantz/drivers/cobrite/{CoBriteDX1.py => cobritedx1.py} (100%) rename lantz/drivers/hinds/{HindsPEM90.py => hindspem90.py} (100%) rename lantz/drivers/lakeshore/{Lakeshore332.py => lakeshore332.py} (100%) rename lantz/drivers/newport/{Newport69907.py => newport69907.py} (100%) rename lantz/drivers/signalrecovery/{SignalRecovery7265.py => signalrecovery7265.py} (100%) diff --git a/lantz/drivers/acton/SP2150i.py b/lantz/drivers/acton/sp2150i.py similarity index 100% rename from lantz/drivers/acton/SP2150i.py rename to lantz/drivers/acton/sp2150i.py diff --git a/lantz/drivers/cobrite/CoBriteDX1.py b/lantz/drivers/cobrite/cobritedx1.py similarity index 100% rename from lantz/drivers/cobrite/CoBriteDX1.py rename to lantz/drivers/cobrite/cobritedx1.py diff --git a/lantz/drivers/hinds/HindsPEM90.py b/lantz/drivers/hinds/hindspem90.py similarity index 100% rename from lantz/drivers/hinds/HindsPEM90.py rename to lantz/drivers/hinds/hindspem90.py diff --git a/lantz/drivers/lakeshore/Lakeshore332.py b/lantz/drivers/lakeshore/lakeshore332.py similarity index 100% rename from lantz/drivers/lakeshore/Lakeshore332.py rename to lantz/drivers/lakeshore/lakeshore332.py diff --git a/lantz/drivers/newport/Newport69907.py b/lantz/drivers/newport/newport69907.py similarity index 100% rename from lantz/drivers/newport/Newport69907.py rename to lantz/drivers/newport/newport69907.py diff --git a/lantz/drivers/signalrecovery/SignalRecovery7265.py b/lantz/drivers/signalrecovery/signalrecovery7265.py similarity index 100% rename from lantz/drivers/signalrecovery/SignalRecovery7265.py rename to lantz/drivers/signalrecovery/signalrecovery7265.py From f2fd706d668c0b523ae636902912fc663204fa4c Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 22 Feb 2016 16:21:44 -0600 Subject: [PATCH 52/61] adding Hinds PEM-90 driver --- lantz/drivers/hinds/hindspem90.py | 92 ++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/lantz/drivers/hinds/hindspem90.py b/lantz/drivers/hinds/hindspem90.py index 249c2a2..db5e401 100644 --- a/lantz/drivers/hinds/hindspem90.py +++ b/lantz/drivers/hinds/hindspem90.py @@ -6,6 +6,8 @@ from time import sleep +from lantz.log import log_to_screen, DEBUG + class HindsPEM90(MessageBasedDriver): """ @@ -13,39 +15,45 @@ class HindsPEM90(MessageBasedDriver): """ # Set paramters here - on_off = {'off': 0, 'on': 1} + off_on = {'off': 1, 'on': 0} waves_limits = (0.0, 19999.9) retardation_limits = (0.0, 1.0) - DEFAULTS = {'ASRL': {'write_termination': '\n', - 'read_termination': '\n', + DEFAULTS = {'ASRL': {'write_termination': '\r', + 'read_termination': '\r\n', 'baud_rate': 9600, 'data_bits': 8, 'parity': constants.Parity.none, 'stop_bits': constants.StopBits.one, 'encoding': 'utf-8', - 'timeout': 2000}} + 'timeout': 3000}} def initialize(self): """ """ super().initialize() + self.reset() + self.echo = 'off' - @Feat(values=on_off) + @Feat(values=off_on) def echo(self): """ Checks to see if ECHO mode is enabled. Note that the code in this driver assumes that the command echo is disabled. """ - return int(self.query(':SYS:ECHO?')) + + print('Can\'t read this, can only set.') + return 0 @echo.setter def echo(self, status): """ Sets echo mode to be controlled by on_off. """ - print('E:{}'.format(status)) - return self.query('E:{}'.format(status)) + self.clear_buffer() + result = self.write('E:{}'.format(status)) + self.read() + return result @Feat(limits=waves_limits) @@ -53,47 +61,58 @@ def wavelength(self): """ Reads out current wavelength in nm """ - return float(self.query('W')) + self.clear_buffer() + self.write('W') + self.read() + return float(self.read())/10.0 @wavelength.setter def wavelength(self, nm): """ Sets wavelength in nm. """ - print('W:{}'.format(nm)) - return self.query('W:{0:0>7.1f}'.format(nm)) + result = self.write('W:{0:0>6.0f}'.format(nm*10.0)) + self.read() + return result @Feat(limits=retardation_limits) def retardation(self): """ Reads out current retardation in wave units """ - return float(self.query('R')/1000.0) + self.clear_buffer() + self.write('R') + self.read() + return float(self.read())/1000.0 @retardation.setter def retardation(self, wave_units): """ Sets retardation in wave units. """ - print('R:{}'.format(wave_units)) - return self.query('R:{0:04}'.format(ceil(wave_units*1000))) + return self.write('R:{0:04.0f}'.format(ceil(wave_units*1000))) @Feat() def frequency(self): """ Reads out the reference frequency in hertz """ - return float(self.query('F')) + self.clear_buffer() + self.write('F') + self.read() + return float(self.read()) @Feat() def frequency2(self): """ Reads out the reference frequency2 in hertz """ - return float(self.query('2F')) + self.clear_buffer() + self.query('2F') + return float(self.read()) - @Feat(values=on_off) - def inhibit(self): + @Feat(values=off_on) + def inhibitor(self): """ Returns 0 for the retardation inhibitor """ @@ -112,9 +131,40 @@ def reset(self): """ Resets PEM-90 to default factory settings. """ - return self.query('Z') + print('Resetting PEM90...') + self.clear_buffer() + return self.write('Z') + + def clear_buffer(self): + """ + Clears the buffer of PEM-90. + """ + while True: + try: + self.read() + except: + print('Timeout?') + return + + if __name__ == '__main__': - with HindsPEM90.via_serial(10) as inst: + print('Hi') + log_to_screen(DEBUG) + with HindsPEM90.via_serial(12) as inst: + for a in range(0, 100): + print('Frequency:{}Hz'.format(inst.frequency)) + print('Wavelength:{}nm'.format(inst.wavelength)) + print('Retardation:{}'.format(inst.retardation)) + + inst.wavelength = 500.0 + print('Wavelength:{}nm'.format(inst.wavelength)) + + inst.retardation = 0.5 + print('Retardation:{}'.format(inst.retardation)) + + inst.wavelength = 400.0 + print('Wavelength:{}nm'.format(inst.wavelength)) - echo = 'off' + inst.retardation = 0.25 + print('Retardation:{}'.format(inst.retardation)) From d2be56fd185435c524780bb3785ba3e06cd76a75 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 3 Aug 2016 01:20:48 -0500 Subject: [PATCH 53/61] added commands to interface with socket communication protocol hosted by Cryostation software --- lantz/drivers/montana/cryostation.py | 223 +++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 lantz/drivers/montana/cryostation.py diff --git a/lantz/drivers/montana/cryostation.py b/lantz/drivers/montana/cryostation.py new file mode 100644 index 0000000..dd3a3e6 --- /dev/null +++ b/lantz/drivers/montana/cryostation.py @@ -0,0 +1,223 @@ +from lantz.driver import Driver +from lantz import Feat, DictFeat, Action + +import socket + +class Cryostation(Driver): + + + def __init__(self, address, port=7773, timeout=20.0): + super().__init__() + self.address = address + self.port = port + + def initialize(self): + """ + Initialize function for Cryostation communication port, uses Python + sockets library to open socket communication with Cryostation. + """ + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((self.address, self.port)) + self.socket.settimeout(2) + + def finalize(self): + """ + Closes socket communication with Cryostation software. + """ + self.socket.close() + + @Feat() + def alarm_state(self): + """ + Returns true or false, indicating the presence (T) or absence (F) of + a system error. + """ + return self.send_and_recv('GAS') + + @Feat() + def chamber_pressure(self): + """ + Returns the chamber pressure in mTorr, or -0.1 if the pressure is + unavailable. + """ + return float(self.send_and_recv('GCP')) + + @Feat() + def platform_temperature(self): + """ + Returns the current platform temperature in K, or -0.100 if the current + temperature is unavailable. + """ + return float(self.send_and_recv('GPT')) + + @Feat() + def platform_stability(self): + """ + Returns the platform stability in K, or -0.100 if unavailable. + """ + return float(self.send_and_recv('GPS')) + + @Feat() + def platform_heater_pow(self): + """ + Returns the current platform heater power in W, or -0.100 if + unavailable. + """ + return float(self.send_and_recv('GPHP')) + + @Feat() + def stage_1_temperature(self): + """ + Returns the current stage 1 temperature in K, or -0.100 if the current + temperature is unavailable. + """ + return float(self.send_and_recv('GS1T')) + + @Feat() + def stage_1_heater_pow(self): + """ + Returns the current stage 1 heater power in W, or -0.100 if + unavailable. + """ + return float(self.send_and_recv('GS1HP')) + + @Feat() + def stage_2_temperature(self): + """ + Returns the current stage 2 temperature in K, or -0.100 if the current + temperature is unavailable. + """ + return float(self.send_and_recv('GS2T')) + + @Feat() + def sample_stability(self): + """ + Returns the sample stability in K, or -0.100 if unavailable. + """ + return float(self.send_and_recv('GSS')) + + @Feat() + def sample_temperature(self): + """ + Returns the sample temperature in K, or -0.100 if unavailable. + """ + return float(self.send_and_recv('GST')) + + @Feat() + def temp_set_point(self): + """ + Returns the temperature setpoint of the Cryostation software. + """ + return float(self.send_and_recv('GTSP')) + + @temp_set_point.setter + def temp_set_point(self, setpoint_kelvin): + """ + Sets the temperature setpoint of the Cryostation software. + """ + return self.send_and_recv('STSP{0:.2f}'.format(setpoint_kelvin)) + + @Feat() + def user_temperature(self): + """ + Returns the user thermometer temperature in K, or -0.100 if unavailable. + """ + return float(self.send_and_recv('GUT')) + + @Feat() + def user_stability(self): + """ + Returns the user thermometer stability in K, or -0.100 if unavailable. + """ + return float(self.send_and_recv('GUS')) + + @Action() + def start_cool_down(self): + """ + Initializes cooldown to temperature setpoint. + """ + return self.send_and_recv('SCD') + + @Action() + def start_standby(self): + """ + Puts the system into standby mode (see manual for details). + """ + return self.send_and_recv('SSB') + + @Feat() + def SToP(self): + """ + Returns the status of the last command. + + If OK, the system cannot stop at this time. + """ + return self.send_and_recv('STP') + + @Action() + def start_warm_up(self): + """ + Starts the system warmup to room temperature. + """ + return self.send_and_recv('SWU') + + def send_and_recv(self, message): + """ + Params: + message = command to be sent to Cryostation + return_bytes = number of bytes to be stripped off return string + """ + buffer_size = 1024 + m1 = '0{}{}'.format(str(len(message)), message) + + self.socket.send(m1.encode()) + data = str() + message = self.socket.recv(buffer_size) + if message: + data = message.decode() + + return data[2:] + +def main(): + + address = '192.168.1.100' + port = 7773 + + with Cryostation(address, port) as inst: + print('Alarm state: {}'.format(inst.alarm_state)) + print('Chamber pressure: {}mTorr'.format(inst.chamber_pressure)) + + print('Temperature and stabiity metrics') + + print('Platform temperature: {}K'.format(inst.platform_temperature)) + print('Platform stability: {}K'.format(inst.platform_stability)) + print('Stage 1 temperature: {}K'.format(inst.stage_1_temperature)) + print('Stage 2 temperature: {}K'.format(inst.stage_2_temperature)) + + print('Sample temperature :{}K'.format(inst.sample_temperature)) + print('Sample stabiity: {}K'.format(inst.sample_stability)) + + print('User temperature: {}K'.format(inst.sample_temperature)) + print('User stabiity: {}K'.format(inst.sample_stability)) + + print('Heater metrics') + print('Platform heater power: {}W'.format(inst.platform_heater_pow)) + print('Stage 1 heater power: {}W'.format(inst.stage_1_heater_pow)) + + print('Testing temperature set point...') + print('System set point: {}K'.format(inst.temp_set_point)) + inst.temp_set_point = 20.0 + print('System set point: {}K'.format(inst.temp_set_point)) + inst.temp_set_point = 3.2 + print('System set point: {}K'.format(inst.temp_set_point)) + + print('Test system actions') + print('Starting cooldown?: {}'.format(inst.start_cool_down())) + print('Starting standby?: {}'.format(inst.start_standby())) + print('Starting warmup?: {}'.format(inst.start_warm_up())) + + + + +if __name__ == '__main__': + main() From aeb3e3b669b41c0b32d07fb1b539ebcaaea2cc34 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Wed, 3 Aug 2016 16:25:36 -0500 Subject: [PATCH 54/61] added units and warnings to Montana cryostation driver as per @kmiao's request --- lantz/drivers/montana/cryostation.py | 116 +++++++++++++++++---------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/lantz/drivers/montana/cryostation.py b/lantz/drivers/montana/cryostation.py index dd3a3e6..50c0894 100644 --- a/lantz/drivers/montana/cryostation.py +++ b/lantz/drivers/montana/cryostation.py @@ -1,9 +1,28 @@ from lantz.driver import Driver from lantz import Feat, DictFeat, Action +from lantz import Q_ + import socket +import warnings + +# class MontanaWarning(Warning): +# """ +# """ +# def __init__(self): +# super().__init__() class Cryostation(Driver): + """ + Lantz driver for interfacing with Montana instruments cryostat. + + Includes testing code, which should work out of the box assuming you give + it the correct IP address. + + Author: P. Mintun + Date: 8/3/2016 + Version: 0.1 + """ def __init__(self, address, port=7773, timeout=20.0): @@ -32,83 +51,89 @@ def alarm_state(self): Returns true or false, indicating the presence (T) or absence (F) of a system error. """ - return self.send_and_recv('GAS') + error = self.send_and_recv('GAS') + if (error == 'T'): - @Feat() + warnings.warn('Montana in alarm state.') + return True + + return False + + @Feat(units='millitorr') def chamber_pressure(self): """ Returns the chamber pressure in mTorr, or -0.1 if the pressure is unavailable. """ - return float(self.send_and_recv('GCP')) + return float(self.send_and_recv('GCP', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def platform_temperature(self): """ Returns the current platform temperature in K, or -0.100 if the current temperature is unavailable. """ - return float(self.send_and_recv('GPT')) + return float(self.send_and_recv('GPT', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def platform_stability(self): """ Returns the platform stability in K, or -0.100 if unavailable. """ return float(self.send_and_recv('GPS')) - @Feat() + @Feat(units='watts') def platform_heater_pow(self): """ Returns the current platform heater power in W, or -0.100 if unavailable. """ - return float(self.send_and_recv('GPHP')) + return float(self.send_and_recv('GPHP', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def stage_1_temperature(self): """ Returns the current stage 1 temperature in K, or -0.100 if the current temperature is unavailable. """ - return float(self.send_and_recv('GS1T')) + return float(self.send_and_recv('GS1T', raise_warning=True)) - @Feat() + @Feat(units='watts') def stage_1_heater_pow(self): """ Returns the current stage 1 heater power in W, or -0.100 if unavailable. """ - return float(self.send_and_recv('GS1HP')) + return float(self.send_and_recv('GS1HP', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def stage_2_temperature(self): """ Returns the current stage 2 temperature in K, or -0.100 if the current temperature is unavailable. """ - return float(self.send_and_recv('GS2T')) + return float(self.send_and_recv('GS2T', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def sample_stability(self): """ Returns the sample stability in K, or -0.100 if unavailable. """ return float(self.send_and_recv('GSS')) - @Feat() + @Feat(units='kelvin') def sample_temperature(self): """ Returns the sample temperature in K, or -0.100 if unavailable. """ - return float(self.send_and_recv('GST')) + return float(self.send_and_recv('GST', raise_warning=True)) - @Feat() + @Feat(units='kelvin') def temp_set_point(self): """ Returns the temperature setpoint of the Cryostation software. """ - return float(self.send_and_recv('GTSP')) + return float(self.send_and_recv('GTSP', raise_warning=True)) @temp_set_point.setter def temp_set_point(self, setpoint_kelvin): @@ -117,14 +142,14 @@ def temp_set_point(self, setpoint_kelvin): """ return self.send_and_recv('STSP{0:.2f}'.format(setpoint_kelvin)) - @Feat() + @Feat(units='kelvin') def user_temperature(self): """ Returns the user thermometer temperature in K, or -0.100 if unavailable. """ return float(self.send_and_recv('GUT')) - @Feat() + @Feat(units='kelvin') def user_stability(self): """ Returns the user thermometer stability in K, or -0.100 if unavailable. @@ -161,7 +186,7 @@ def start_warm_up(self): """ return self.send_and_recv('SWU') - def send_and_recv(self, message): + def send_and_recv(self, message, raise_warning=False): """ Params: message = command to be sent to Cryostation @@ -172,9 +197,14 @@ def send_and_recv(self, message): self.socket.send(m1.encode()) data = str() - message = self.socket.recv(buffer_size) - if message: - data = message.decode() + received = self.socket.recv(buffer_size) + if received: + data = received.decode() + + if ((data[2:] == '-0.100') & raise_warning): + + warnings.warn('Unable to return parameter from command {}.'.format(message)) + return 0 return data[2:] @@ -183,33 +213,35 @@ def main(): address = '192.168.1.100' port = 7773 + kelvin = Q_(1, 'kelvin') + + warnings.simplefilter('always', UserWarning) + + with Cryostation(address, port) as inst: print('Alarm state: {}'.format(inst.alarm_state)) - print('Chamber pressure: {}mTorr'.format(inst.chamber_pressure)) + print('Chamber pressure: {}'.format(inst.chamber_pressure)) print('Temperature and stabiity metrics') - print('Platform temperature: {}K'.format(inst.platform_temperature)) - print('Platform stability: {}K'.format(inst.platform_stability)) - print('Stage 1 temperature: {}K'.format(inst.stage_1_temperature)) - print('Stage 2 temperature: {}K'.format(inst.stage_2_temperature)) - - print('Sample temperature :{}K'.format(inst.sample_temperature)) - print('Sample stabiity: {}K'.format(inst.sample_stability)) + print('Platform temperature: {}'.format(inst.platform_temperature)) + print('Platform stability: {}'.format(inst.platform_stability)) + print('Stage 1 temperature: {}'.format(inst.stage_1_temperature)) + print('Stage 2 temperature: {}'.format(inst.stage_2_temperature)) - print('User temperature: {}K'.format(inst.sample_temperature)) - print('User stabiity: {}K'.format(inst.sample_stability)) + print('Sample temperature :{}'.format(inst.sample_temperature)) + print('Sample stabiity: {}'.format(inst.sample_stability)) print('Heater metrics') - print('Platform heater power: {}W'.format(inst.platform_heater_pow)) - print('Stage 1 heater power: {}W'.format(inst.stage_1_heater_pow)) + print('Platform heater power: {}'.format(inst.platform_heater_pow)) + print('Stage 1 heater power: {}'.format(inst.stage_1_heater_pow)) print('Testing temperature set point...') - print('System set point: {}K'.format(inst.temp_set_point)) - inst.temp_set_point = 20.0 - print('System set point: {}K'.format(inst.temp_set_point)) - inst.temp_set_point = 3.2 - print('System set point: {}K'.format(inst.temp_set_point)) + print('System set point: {}'.format(inst.temp_set_point)) + inst.temp_set_point = 20.0 * kelvin + print('System set point: {}'.format(inst.temp_set_point)) + inst.temp_set_point = 3.2 * kelvin + print('System set point: {}'.format(inst.temp_set_point)) print('Test system actions') print('Starting cooldown?: {}'.format(inst.start_cool_down())) From da62bc767f9fbc577082ba7cbf5ef7caa8034008 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 1 Sep 2016 02:05:12 -0500 Subject: [PATCH 55/61] added driver for agilent n51xx vector signal generator series --- lantz/drivers/agilent/n51xx.py | 156 +++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 lantz/drivers/agilent/n51xx.py diff --git a/lantz/drivers/agilent/n51xx.py b/lantz/drivers/agilent/n51xx.py new file mode 100644 index 0000000..9ff3447 --- /dev/null +++ b/lantz/drivers/agilent/n51xx.py @@ -0,0 +1,156 @@ +from lantz.messagebased import MessageBasedDriver +from lantz import Feat, DictFeat, Action + +#from lantz import Q_ + +import socket +import warnings + +# class MontanaWarning(Warning): +# """ +# """ +# def __init__(self): +# super().__init__() + +class N51xx(MessageBasedDriver): + """ + Lantz driver for interfacing with AR SG6000 vector signal generator. + + Includes testing code, which should work out of the box assuming you give + it the correct IP address. + + Author: P. Mintun + Date: 8/31/2016 + Version: 0.1 + """ + + DEFAULTS = { + 'COMMON': { + 'write_termination': '\n', + 'read_termination': '\n', + } + } + + freq_limits = (1e5,6e9) + power_limits = (-200,0) + + + @Feat() + def idn(self): + """ + Identifiies the instrument. + """ + return self.query('*IDN?') + + + @Feat(values={True: 1, False: 0}) + def rf_toggle(self): + """ + enable or disable RF output + """ + return float(self.query('OUTP:STAT?')) + + @rf_toggle.setter + def rf_toggle(self,value): + self.write('OUTP:STAT {}'.format(value)) + + @Feat(units='Hz', limits=freq_limits) + def frequency(self): + """ + RF output frequency, in Hz. + """ + return self.query('SOUR:FREQ:CW?') + + @frequency.setter + def frequency(self, frequency): + """ + RF output frequency, in Hz. + """ + return self.write('SOUR:FREQ:CW {}Hz'.format(frequency)) + + @Feat(limits=power_limits) + def power(self): + """ + RF output power, in dBm. + """ + return self.query('SOUR:POW?') + + @power.setter + def power(self, value): + """ + Sets RF output power, in dBm. + """ + return self.write('SOUR:POW {}DBM'.format(value)) + + @Feat(units='radians',limits=(-3.14,3.14)) + def phase(self): + """ + Returns RF signal carrier phase in radians + """ + return self.query('SOUR:PHAS?') + + @phase.setter + def phase(self, radians): + """ + Sets RF signal carrier phase in degrees + """ + return self.write('SOUR:PHAS {}RAD'.format(radians)) + + @Feat(limits=(-200,30)) + def power_limit(self): + """ + Returns the user set output limit of the signal generator, in dBm. + """ + print('For some reason, this command doesn\'t seem to work...') + return self.write('SOUR:POW:USER:MAX?') + + @power_limit.setter + def power_limit(self, max_dBm): + """ + Sets the maximum output of the signal generator in dBm. + """ + self.write('SOUR:POW:USER:ENAB 0') + return self.write('SOUR:POW:USER:MAX {}'.format(max_dBm)) + + + + + + +if __name__ == '__main__': + address = '192.168.1.102' + inst_num = 'inst0' + + with N51xx('TCPIP0::{}::{}::INSTR'.format(address, inst_num)) as inst: + print('Identification: {}'.format(inst.idn)) + + inst.power_limit = 0.0 + print('Power limit:{}dBm'.format(inst.power_limit)) + + print('RF output settings') + print('RF on?:{}'.format(inst.rf_toggle)) + print('Output frequency: {}'.format(inst.frequency)) + inst.frequency = 1e6 + print('Output frequency: {}'.format(inst.frequency)) + inst.frequency = 2.87e9 + print('Output frequency: {}'.format(inst.frequency)) + + print('Output power: {}dBm'.format(inst.power)) + inst.power = -10.0 + print('Output power: {}dBm'.format(inst.power)) + inst.power = 0.0 + print('Output power: {}dBm'.format(inst.power)) + inst.power = -10.0 + print('Output power: {}dBm'.format(inst.power)) + + print('Phase:{}'.format(inst.phase)) + inst.phase = 3.14159/2.0 + print('Phase:{}'.format(inst.phase)) + inst.phase = 0.0 + print('Phase:{}'.format(inst.phase)) + + + inst.rf_toggle = 1 + print('RF on?:{}'.format(inst.rf_toggle)) + inst.rf_toggle = 0 + print('RF on?:{}'.format(inst.rf_toggle)) From d184942596c3149dcec4b8b5924d5e0bae74ab21 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 8 Sep 2016 19:58:46 -0500 Subject: [PATCH 56/61] added Newport FSM driver with some hacky DAQ code --- lantz/drivers/ni/Newport_FSM.py | 486 ++++++++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 lantz/drivers/ni/Newport_FSM.py diff --git a/lantz/drivers/ni/Newport_FSM.py b/lantz/drivers/ni/Newport_FSM.py new file mode 100644 index 0000000..db67c23 --- /dev/null +++ b/lantz/drivers/ni/Newport_FSM.py @@ -0,0 +1,486 @@ +# Demo code to control Newport FSM, based off of original by David J. Christle +# Original code here: +# https://github.com/dchristle/qtlab/blob/master/instrument_plugins/Newport_FSM.py + +from lantz.drivers.ni.daqmx import System +from lantz.drivers.ni.daqmx import AnalogOutputTask, VoltageOutputChannel +from lantz.drivers.ni.daqmx import AnalogInputTask, VoltageInputChannel #CounterInputTask, CountEdgesChannel, DigitalOutputChannel, DigitalOutputTask + +from lantz import Feat, DictFeat, Action + +import numpy as np + +from numpy import abs, ceil, pi, linspace, cos, ones + +from time import sleep + +from ctypes import c_uint32 + + +class Newport_FSM(System): + """ + Class for controlling Newport FSM using National Instruments DAQ. + """ + + def initialize(self): + """ + Creates AO tasks for controlling FSM. + """ + self.task = AnalogOutputTask('FSM') + + self.dev_name = 'dev1/' + + self.fsm_dimensions = { + 'x': {'micron_per_volt': 9.5768, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao0'}, + 'y': {'micron_per_volt': 7.1759, + 'min_v': -10.0, + 'max_v': +10.0, + 'default': 0.0, + 'origin': 0.0, + 'ao_channel': 'ao1'} + } + + self.ao_smooth_rate = 50000.0 # Hz + self.ao_smooth_steps_per_volt = 1000.0 # steps/V + #self.ao_smooth_steps_per_volt = 200.0 # steps/V + self.count_task = None + + + for dim in self.fsm_dimensions: + chan_name = dim + phys_chan = self.dev_name + self.fsm_dimensions[dim]['ao_channel'] + V_min = self.fsm_dimensions[dim]['min_v'] + V_max = self.fsm_dimensions[dim]['max_v'] + + chan = VoltageOutputChannel(phys_chan, name=chan_name, + min_max=(V_min, V_max), units='volts', + task=self.task) + + + + def finalize(self): + """ + Stops AO tasks for controlling FSM. + """ + #self.task.stop() + + @Feat() + def abs_position_pair(self): + x_loc = abs_position_single['x'] + y_loc = abs_position_single['y'] + print('FSM at ({}um,{}um)'.format(x_loc, y_loc)) + return [x_loc, y_loc] + + @abs_position_pair.setter + def abs_position_pair(self, xy_pos): + """ + Sets absolute position of scanning mirror, as a micron pair. + """ + abs_position_single['x'] = xy_pos[0] + abs_position_single['y'] = xy_pos[1] + print('Set FSM to ({}um,{}um).'.format(xy_pos[0], xy_pos[1])) + return 0 + + @DictFeat() + def abs_position_single(self, dimension): + """ + Returns absolute position of scanning mirror along dimension. + """ + return 0 + + @abs_position_single.setter + def abs_position_single(self, dimension, pos): + """ + Sets absolute position of scanning mirror along dimension to be pos. + """ + return 0 + + def convert_V_to_um(self, dim, V): + """ + Returns micron position corresponding to channel voltage. + """ + um = V * self.fsm_dimensions[dim]['micron_per_volt'] + ( + self.fsm_dimensions[dim]['origin']) + return um + + def convert_um_to_V(self, um, dim): + """ + Returns voltage corresponding to micron position. + """ + V = (um - self.fsm_dimensions[dim]['origin']) / ( + self.fsm_dimensions[dim]['micron_per_volt']) + return V + + def set_V_to_zero(self): + """ + Sets output voltages to 0. + """ + self.abs_position_pair = (self.convert_V_to_um(0, 'x'), + self.convert_V_to_um(0, 'y')) + + def ao_smooth(self, x_init, x_final, channel): + """ + Smooths output of DAQ to avoid hysteresis w/ moving FSM mirror. + + Copy of dchristle's algorithm. (I know what you're trying to do, and + just stop.) + """ + + v_data = self.AO_smooth_func(x_init, x_final, channel) + n_steps = v_data.size + + samp_arr = np.empty(n_steps * 2, dtype=float) + + if channel == 'x': + samp_arr[0::2] = v_data + samp_arr[1::2] = np.zeros(n_steps) + else: + samp_arr[0::2] = np.zeros(n_steps) + samp_arr[1::2] = v_data + + + + print(samp_arr) + print(n_steps) + + #samp_arr = 1.5 + + + self.task.configure_timing_sample_clock(source='OnboardClock', rate=self.ao_smooth_rate, + sample_mode='finite', + samples_per_channel=int(n_steps)) + + self.task.write(data=samp_arr, auto_start=False, timeout=0, group_by='scan') + + + try: + self.task.start() + print('Started FSM Scan') + print('Expected scan time:{} sec'.format(n_steps*1.0/self.ao_smooth_rate)) + print('Number of scan points:{}'.format(n_steps)) + sleep(n_steps*1.0/self.ao_smooth_rate) + self.task.stop() + + except: + print('Failed in counter start phase...') + + + def AO_smooth_func(self, x_init, x_final, channel): + """ + :param x_min: minimum x coordinate (in microns) for scan + :param x_max: maximum x coordinate (in microns) for scan + :param channel: channel to scan over (i.e. x or y) + + :return: array of voltages to write + """ + + v_init = self.convert_um_to_V(x_init, channel) + v_final = self.convert_um_to_V(x_final, channel) + + n_steps = int(ceil(abs(v_final - v_init) * self.ao_smooth_steps_per_volt)) + + v_data = v_init * ones(n_steps) + (v_final - v_init) * (1.0 - cos( + linspace(0.0, pi, n_steps))) / 2.0 + + return v_data + + + def fsm_line_generator(self, x_min, x_max, y_min, y_max, freq, input_voltage_chan): + """ + Scan FSM by writing an array of points over analog out + + Args: + :param ctrchan: (string) device channel for counter + :param x_min: (float) minimum x position (in microns) for scan + :param x_max: (float) maximum x position (in microns) for scan + :param y_min: (float) minimum y position (in microns) for scan + :param y_max: (float) maximum y position (in microns) for scan + :param freq: (float) frequency for FSM scan + + :param input_voltage_chan: (str) device channel for analog input voltage to read (i.e. 'dev1/ai0') + + :return: returns a tuple containing two float arrays. + The first array contains the x-y positions used for the scan. + The second array contains the counts corresponding to each position. + """ + v_data, nx_steps, ny_steps = self.generate_2D_scan_array(x_min, x_max, y_min, y_max) + + v_chunks = np.array_split(v_data, ny_steps) + + counts = np.zeros(nx_steps) + + if self.count_task is None: + self.count_task = AnalogInputTask('voltage') + counter_channel = VoltageInputChannel(input_voltage_chan) + self.count_task.add_channel(counter_channel) + + sample_clock = 'OnboardClock' + self.task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=nx_steps) + + self.count_task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=nx_steps) + trigger_src = 'ai/StartTrigger' + self.task.configure_trigger_digital_edge_start(trigger_src) + + + for chunk in v_chunks: + # Write the array of voltages that will be used to control the DAQ here. + self.task.write(chunk, auto_start=False, timeout=0, group_by='scan') + + # Synchronize tasks to both start on the AnalogInput start trigger + + # This arms the trigger, thereby starting the AnalogOutputTask (FSM) + # simultaneously with the AnalogInputTask + self.task.start() + + + scan_time = nx_steps*3.0/freq + # Executing the read task executes the two tasks synchronously + t_extra = 0.0 # can add grace period, probably unnecessary + counts = self.count_task.read(samples_per_channel=nx_steps, timeout=scan_time + t_extra) + + yield np.ndarray.flatten(counts) + + self.task.stop() + self.count_task.stop() + + return + + + def generate_2D_scan_array(self, x_min, x_max, y_min, y_max): + """ + Generates array of points to write to analog output for x, y channels + + Args: + :param ctrchan: (string) device channel for counter + :param x_min: (float) minimum x position (in microns) for scan + :param x_max: (float) maximum x position (in microns) for scan + :param y_min: (float) minimum y position (in microns) for scan + :param y_max: (float) maximum y position (in microns) for scan + + """ + + x_voltages = self.AO_smooth_func(x_min, x_max, 'x') + nx_steps = x_voltages.size + y_voltages = self.AO_smooth_func(y_min, y_max, 'y') + ny_steps = y_voltages.size + + x_pts = self.convert_V_to_um('x', x_voltages) + y_pts = self.convert_V_to_um('y', y_voltages) + + + # Create a 2D array of tuples for the xy points in the scan. + # Note that the AO smooth procedure means these points do not fall on a perfect grid + # That's why I've calculated them here and returned them to be available to the user. + + # Note: change counts data type to be int, if using edge count APD + count_tuple = np.dtype([('x', 'float'), ('y', 'float'), ('counts', 'float')]) + xy_pts = np.zeros([nx_steps, ny_steps], dtype=count_tuple) + + # This avoids a nested for loop (if you have large array of points, O(N^2)) + # I assumed that numpy does both of these operations relatively fast... + xy_pts['x'] = np.tile(x_pts, ny_steps).reshape(nx_steps, ny_steps) + xy_pts['y'] = np.repeat(y_pts, nx_steps).reshape(nx_steps, ny_steps) + + n_steps = (nx_steps + 1) * ny_steps + v_data = np.zeros(2*n_steps) + + # Avoids piezo hysteresis by ensuring they always take data scanning in the same direction + hyst_offset = 0.2 #volts, your MMV with this particular choice + x_voltages = np.hstack([x_voltages[0] - hyst_offset, x_voltages]) + + v_data[0::2] = np.tile(x_voltages, ny_steps) #scan in x repeatedly (x_min to x_max, x_min to x_max, ...) + v_data[1::2] = np.repeat(y_voltages, nx_steps + 1) #scan in y slowly (y_min, y_min, ..., y_min+1, y_min+1, ..., y_max) + + + return v_data, nx_steps, ny_steps + + + def FSM_2D_counts(self, x_min, x_max, y_min, y_max, freq, input_voltage_chan): + """ + Scan FSM by writing an array of points over analog out + + Args: + :param ctrchan: (string) device channel for counter + :param x_min: (float) minimum x position (in microns) for scan + :param x_max: (float) maximum x position (in microns) for scan + :param y_min: (float) minimum y position (in microns) for scan + :param y_max: (float) maximum y position (in microns) for scan + :param freq: (float) frequency for FSM scan + + :param input_voltage_chan: (str) device channel for analog input voltage to read (i.e. 'dev1/ai0') + + :return: returns a tuple containing two float arrays. + The first array contains the x-y positions used for the scan. + The second array contains the counts corresponding to each position. + """ + + x_voltages = self.AO_smooth_func(x_min, x_max, 'x') + nx_steps = x_voltages.size + y_voltages = self.AO_smooth_func(y_min, y_max, 'y') + ny_steps = y_voltages.size + + x_pts = self.convert_V_to_um('x', x_voltages) + y_pts = self.convert_V_to_um('y', y_voltages) + + + # Create a 2D array of tuples for the xy points in the scan. + # Note that the AO smooth procedure means these points do not fall on a perfect grid + # That's why I've calculated them here and returned them to be available to the user. + + # Note: change counts data type to be int, if using edge count APD + count_tuple = np.dtype([('x', 'float'), ('y', 'float'), ('counts', 'float')]) + xy_pts = np.zeros([nx_steps, ny_steps], dtype=count_tuple) + + # This avoids a nested for loop (if you have large array of points, O(N^2)) + # I assumed that numpy does both of these operations relatively fast... + xy_pts['x'] = np.tile(x_pts, ny_steps).reshape(nx_steps, ny_steps) + xy_pts['y'] = np.repeat(y_pts, nx_steps).reshape(nx_steps, ny_steps) + + n_steps = (nx_steps + 1) * ny_steps + v_data = np.zeros(2*n_steps) + + # Avoids piezo hysteresis by ensuring they always take data scanning in the same direction + hyst_offset = 0.2 #volts, your MMV with this particular choice + x_voltages = np.hstack([x_voltages[0] - hyst_offset, x_voltages]) + + v_data[0::2] = np.tile(x_voltages, ny_steps) #scan in x repeatedly (x_min to x_max, x_min to x_max, ...) + v_data[1::2] = np.repeat(y_voltages, nx_steps + 1) #scan in y slowly (y_min, y_min, ..., y_min+1, y_min+1, ..., y_max) + + counts = np.zeros(n_steps) + + self.count_task = AnalogInputTask('voltage') + counter_channel = VoltageInputChannel(input_voltage_chan) + self.count_task.add_channel(counter_channel) + + + # Synchronize tasks to both read at a rate, which is controlled by the internal DAQ clock. + sample_clock = 'OnboardClock' + self.task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=n_steps) + + self.count_task.configure_timing_sample_clock(source=sample_clock, rate=freq, + sample_mode='finite', + samples_per_channel=n_steps) + + # Write the array of voltages that will be used to control the DAQ here. + self.task.write(v_data, auto_start=False, timeout=0, group_by='scan') + + # Synchronize tasks to both start on the AnalogInput start trigger + trigger_src = 'ai/StartTrigger' + self.task.configure_trigger_digital_edge_start(trigger_src) + + # This arms the trigger, thereby starting the AnalogOutputTask (FSM) simultaneously + # with the AnalogInputTask + self.task.start() + + print('Tasks configured.') + + + try: + print('Starting FSM Scan...') + scan_time = n_steps*1.0/freq + print('Expected scan time:{} sec'.format(scan_time)) + print('Number of scan points:{}'.format(n_steps)) + # Executing the read task executes the two tasks synchronously + t_extra = 1.0 # adds 1 second grace period, probably unnecessary + counts = self.count_task.read(samples_per_channel=n_steps, timeout=scan_time + t_extra) + + # Stop both tasks + self.task.stop() + self.count_task.stop() + + except: + print('FSM: 2D Scan Failed in counter start phase.') + + counts = counts.reshape([nx_steps + 1, ny_steps]) + + xy_pts['counts'] = counts[1:,:] #drop first column + + print(counts.shape) + print(xy_pts.shape) + + # This then returns a FSM 2D scan image as a tuple (x_coord, y_coord, counts) + return counts + + + + # Burst mode code below + + # Write the array of voltages that will be used to control the DAQ here. + self.task.write(v_data, auto_start=False, timeout=0, group_by='scan') + + # Synchronize tasks to both start on the AnalogInput start trigger + trigger_src = 'ai/StartTrigger' + self.task.configure_trigger_digital_edge_start(trigger_src) + + # This arms the trigger, thereby starting the AnalogOutputTask (FSM) simultaneously + # with the AnalogInputTask + self.task.start() + + print('Tasks configured.') + + + try: + print('Starting FSM Scan...') + scan_time = n_steps*1.0/freq + print('Expected scan time:{} sec'.format(scan_time)) + print('Number of scan points:{}'.format(n_steps)) + # Executing the read task executes the two tasks synchronously + t_extra = 1.0 # adds 1 second grace period, probably unnecessary + counts = self.count_task.read(samples_per_channel=n_steps, timeout=scan_time + t_extra) + + # Stop both tasks + self.task.stop() + self.count_task.stop() + + except: + print('FSM: 2D Scan Failed in counter start phase.') + + counts = counts.reshape([nx_steps + 1, ny_steps]) + + xy_pts['counts'] = counts[1:,:] #drop first column + + print(counts.shape) + print(xy_pts.shape) + + # This then returns a FSM 2D scan image as a tuple (x_coord, y_coord, counts) + return counts + + + + + + + +if __name__ == '__main__': + with Newport_FSM() as inst: + print('testing!') + print('Sweep from -5.0 to 5.0') + #inst.ao_smooth(-5.0, 5.0, 'x') + # print('Sweep from 5.0 to -25.0') + # inst.ao_smooth(5.0, -25.0, 'x') + # sleep(1) + # print('Sweep from -25.0 to 25.0') + # inst.ao_smooth(-25.0, 25.0, 'x') + # sleep(1) + # inst.ao_smooth(-5.0, 5.0, 'y') + # sleep(1) + # inst.ao_smooth(5.0, -25.0, 'y') + # sleep(1) + # inst.ao_smooth(-25.0, 25.0, 'y') + # sleep(1) + + print('testing 2D count and scan...') + inst.FSM_2D_counts(-10.0, 10.0, -10.0, 10.0, 50000.0, 'dev1/ai0') + + print('derp derp derp derp!') From 66442f850a7cb511e47e720201db05626de25d36 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 24 Oct 2016 10:24:39 -0500 Subject: [PATCH 57/61] added driver for Agilent 33220A function generator --- lantz/drivers/agilent/__init__.py | 19 +++ lantz/drivers/agilent/ag33220A.py | 201 ++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 lantz/drivers/agilent/__init__.py create mode 100644 lantz/drivers/agilent/ag33220A.py diff --git a/lantz/drivers/agilent/__init__.py b/lantz/drivers/agilent/__init__.py new file mode 100644 index 0000000..779fbc7 --- /dev/null +++ b/lantz/drivers/agilent/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.aeroflex + ~~~~~~~~~~~~~~~~~~~~~~ + + :company: Aeroflex + :description: Test and measurement equipment and microelectronic solutions. + :website: http://www.aeroflex.com/ + + ---- + + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from .n51xx import N51xx +from .ag33220A import Ag33220A + +__all__ = ['N51xx', 'Ag33220A'] diff --git a/lantz/drivers/agilent/ag33220A.py b/lantz/drivers/agilent/ag33220A.py new file mode 100644 index 0000000..4bcefc7 --- /dev/null +++ b/lantz/drivers/agilent/ag33220A.py @@ -0,0 +1,201 @@ +from lantz.messagebased import MessageBasedDriver +from lantz import Feat, DictFeat, Action + +#from lantz import Q_ + +import socket +import warnings + +class Ag33220A(MessageBasedDriver): + """ + Lantz driver for interfacing with Aiglent 33220A function generator. + + Includes testing code, which should work out of the box assuming you give + it the correct IP address. + + Author: P. Mintun + Date: 10/21/2016 + Version: 0.1 + """ + + DEFAULTS = { + 'COMMON': { + 'write_termination': '\n', + 'read_termination': '\n', + } + } + + function_dict = { + 'sine': 'SIN', + 'square': 'SQU', + 'ramp': 'RAMP', + 'pulse': 'PULS', + 'noise': 'NOIS', + 'DC': 'DC', + 'user': 'USER' + } + + @Feat() + def idn(self): + """ + Identifiies the instrument. + """ + return self.query('*IDN?') + + @Feat(values=function_dict) + def function(self): + """ + Returns the selected output function. + """ + return self.query('FUNC?') + + @function.setter + def function(self, func_type): + """ + Sets the output function. + """ + return self.write('FUNC {}'.format(func_type)) + + @Feat() + def frequency(self): + """ + Queries the output frequency of the function generator. + """ + return float(self.query('FREQ?')) + + @frequency.setter + def frequency(self, Hz): + """ + Sets the output frequency of the function generator (in Hz). + """ + return self.write('FREQ {}'.format(Hz)) + + @Feat(values={'on': 1, 'off': 0}) + def output_toggle(self): + """ + Returns whether or not the output is on or off. + """ + return int(self.query('OUTP?')) + + @output_toggle.setter + def output_toggle(self, on_off): + """ + Sets the output to be either on, or off. + """ + return self.write('OUTP {}'.format(on_off)) + + @Feat(limits=(0.01,10)) + def voltage(self): + """ + Returns the amplitude voltage setting. + """ + return float(self.query('VOLT?')) + + @voltage.setter + def voltage(self, volts): + """ + Sets the amplitude voltage setting to volts. + """ + return self.write('VOLT {}'.format(volts)) + + @Feat() + def voltage_offset(self): + """ + Returns the DC offset voltage, in volts. + """ + return float(self.query('VOLT:OFFS?')) + + @voltage_offset.setter + def voltage_offset(self, V_dc): + """ + Sets the DC offset voltage to be V_dc, in volts. + """ + return self.write('VOLT:OFFS {}'.format(V_dc)) + + # special commands for pulse mode + @Feat() + def pulse_width(self): + """ + Returns the pulse width in nanoseconds. + """ + return float(self.query('FUNC:PULS:WIDT?')) + + @pulse_width.setter + def pulse_width(self, sec): + """ + Sets the pulse width in seconds. + """ + return self.write('FUNC:PULS:WIDT {}'.format(sec)) + + # special functions for square waves + @Feat() + def square_duty_cycle(self): + """ + Returns the duty cycle of a square wave output, in percent. + """ + return float(self.query('FUNC:SQU:DCYC?')) + + @square_duty_cycle.setter + def square_duty_cycle(self, duty_cycle): + """ + Sets the square wave duty cycle to be duty_cycle. + """ + return self.write('FUNC:SQU:DCYC {}'.format(duty_cycle)) + + +if __name__ == '__main__': + address = 'xxx.yyy.zzz.aaa' + inst_num = 'instYY' + + print('Need to set IP address and inst_num!') + + with Ag33220A('TCPIP0::{}::{}::INSTR'.format(address, inst_num)) as inst: + print('Identification: {}'.format(inst.idn)) + + print('Function: {}'.format(inst.function)) + inst.function = 'sine' + print('Function: {}'.format(inst.function)) + inst.function = 'pulse' + print('Function: {}'.format(inst.function)) + inst.function = 'square' + print('Function: {}'.format(inst.function)) + + + print('Frequency:{}Hz'.format(inst.frequency)) + inst.frequency = 1000.0 + print('Frequency:{}Hz'.format(inst.frequency)) + inst.frequency = 20000.0 + print('Frequency:{}Hz'.format(inst.frequency)) + + + print('Output:{}'.format(inst.output_toggle)) + inst.output_toggle = 'off' + print('Output:{}'.format(inst.output_toggle)) + inst.output_toggle = 'on' + print('Output:{}'.format(inst.output_toggle)) + + print('Amplitude voltage:{}V'.format(inst.voltage)) + inst.voltage = 2.5 + print('Amplitude voltage:{}V'.format(inst.voltage)) + inst.voltage = 5.0 + print('Amplitude voltage:{}V'.format(inst.voltage)) + + print('Offset voltage:{}'.format(inst.voltage_offset)) + inst.voltage_offset = 2.1 + print('Offset voltage:{}'.format(inst.voltage_offset)) + inst.voltage_offset = 2.5 + print('Offset voltage:{}'.format(inst.voltage_offset)) + + inst.function = 'pulse' + print('Pulse width:{}s'.format(inst.pulse_width)) + inst.pulse_width = 50e-9 + print('Pulse width:{}s'.format(inst.pulse_width)) + inst.pulse_width = 20e-9 + print('Pulse width:{}s'.format(inst.pulse_width)) + + inst.function = 'square' + print('Duty cycle:{}'.format(inst.square_duty_cycle)) + inst.square_duty_cycle = 25.0 + print('Duty cycle:{}'.format(inst.square_duty_cycle)) + inst.square_duty_cycle = 50.0 + print('Duty cycle:{}'.format(inst.square_duty_cycle)) From 1ba517a532cfddd40a2c38706ab38a6b43810854 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Mon, 24 Oct 2016 16:13:08 -0500 Subject: [PATCH 58/61] added code to read counters from daq (daqs vs. counts --JHeremans) : --- lantz/drivers/ni/__init__.py | 4 +- lantz/drivers/ni/daqmx/base.py | 63 ++++++++-- lantz/drivers/ni/daqmx/channels.py | 185 ++++++++++++++++++++++++----- lantz/drivers/ni/daqmx/tasks.py | 50 +++++--- 4 files changed, 244 insertions(+), 58 deletions(-) diff --git a/lantz/drivers/ni/__init__.py b/lantz/drivers/ni/__init__.py index d981148..aec810c 100644 --- a/lantz/drivers/ni/__init__.py +++ b/lantz/drivers/ni/__init__.py @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~ :company: National Instruments - :description: + :description: :website: http://www.ni.com/ ---- @@ -15,3 +15,5 @@ from . import daqmx + +# diff --git a/lantz/drivers/ni/daqmx/base.py b/lantz/drivers/ni/daqmx/base.py index 50be24f..b3dc79b 100644 --- a/lantz/drivers/ni/daqmx/base.py +++ b/lantz/drivers/ni/daqmx/base.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -""" +""" lantz.drivers.ni.daqmx.base ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -151,10 +150,10 @@ def __init__(self, *args, **kwargs): @Feat(read_once=True) def version(self): - """Version of installed NI-DAQ library. + """Version of installed NI-DAQ library. """ - err, major = self.lib.GetSysNIDAQMajorVersion(RetValue('u32')) - err, minor = self.lib.GetSysNIDAQMinorVersion(RetValue('u32')) + err, major = self.lib.GetSysNIDAQMajorVersion(RetValue('u32')) + err, minor = self.lib.GetSysNIDAQMinorVersion(RetValue('u32')) return major, minor def _device_names(self): @@ -377,12 +376,12 @@ def typed_task(cls, io_type): return cls._REGISTRY[io_type] def _load_task(self, name): - err, self.__task_handle = self.lib.LoadTask(name, RetValue('u32')) + err, self.__task_handle = self.lib.LoadTask(name, RetValue('u64')) self.name = name self.log_debug('Loaded task with {} ({})'.format(self.name, self.__task_handle)) def _create_task(self, name): - err, self.__task_handle = self.lib.CreateTask(name, RetValue('u32')) + err, self.__task_handle = self.lib.CreateTask(name, RetValue('u64')) err, self.name = self.lib.GetTaskName(*RetStr(default_buf_size)) self.log_debug('Created task with {} ({})'.format(self.name, self.__task_handle)) @@ -422,6 +421,13 @@ def _channel_names(self): names = tuple(n.strip() for n in buf.split(',') if n.strip()) return names + def number_of_channels(self): + """Returns the number of virtual channels in the task. + """ + err, buf = self.lib.GetTaskChannels(*RetStr(default_buf_size)) + names = tuple(n.strip() for n in buf.split(',') if n.strip()) + return len(names) + def _device_names(self): """Return a tuple with the names of all devices in the task. """ @@ -501,7 +507,6 @@ def start(self): repeatedly. Starting and stopping a task repeatedly reduces the performance of the application. """ - self.lib.StartTask() @Action() @@ -849,9 +854,10 @@ def configure_timing_sample_clock(self, source='on_board_clock', rate=1, active_ """ if source == 'on_board_clock': source = None - self.samples_per_channel = samples_per_channel self.sample_mode = sample_mode - self.lib.CfgSampClkTiming(source, float64(rate), active_edge, sample_mode, samples_per_channel) + + + self.lib.CfgSampClkTiming(source, rate, active_edge, sample_mode, samples_per_channel) def configure_timing_burst_handshaking_export_clock(self, *args, **kws): """ @@ -951,7 +957,13 @@ def configure_trigger_digital_edge_start(self, source, edge='rising'): acquiring or generating samples: rising or falling edge(s). """ - self.lib.CfgDigEdgeStartTrig(self, source, edge) + if edge == Constants.Val_Falling: + edge_val = int(Constants.Val_Falling) + else: + edge_val = int(Constants.Val_Rising) + + + self.lib.CfgDigEdgeStartTrig(source, edge_val) @Action(values=(str, str, _WHEN_MATCH)) def configure_trigger_digital_pattern_start(self, source, pattern, match=True): @@ -975,7 +987,7 @@ def configure_trigger_digital_pattern_start(self, source, pattern, match=True): Specifies the conditions under which the trigger occurs: pattern matches or not. """ - self.lib.CfgDigPatternStartTrig(self, source, pattern, match) + self.lib.CfgDigPatternStartTrig(source, pattern, match) def configure_trigger_disable_start(self): """ @@ -1028,6 +1040,18 @@ def configure_analog_edge_reference_trigger(self, source, slope='rising',level=1 self.lib.CfgAnlgEdgeRefTrig(source, slope, level, pre_trigger_samps) + @Action() + def send_software_trigger(self, triggerID=Constants.Val_AdvanceTrigger): + """ + + :param triggerID: (int32) Specifies which software trigger to generate. + + DAQmx_Val_AdvanceTrigger Generate the advance trigger + + """ + + self.lib.SendSoftwareTrigger(triggerID) + @Feat(values=(str, _WHEN_WINDOW, None, None, None)) def configure_analog_window_reference_trigger(self, source, when='entering',top=1.0, bottom=1.0, pre_trigger_samps=0): @@ -1147,6 +1171,20 @@ def configure_digital_pattern_reference_trigger(self, source, pattern, match=Tru self.lib.CfgDigPatternRefTrig(self, source, pattern, match, pre_trigger_samps) + @Action() + def configure_output_buffer(self, samples_per_channel=1000): + """ + + :params: taskHandle (TaskHandle): task to configure + :params: num_samps_per_chan (uInt32): number of samples to output per channel. + Zero indicates no buffer should be allocated. Use a buffer size of 0 to perform + a hardware-timed operation without using a buffer. + + :return: status (int32) - error code returned by the function in the event of an + error or warning + """ + return self.lib.CfgOutputBuffer(self, samples_per_channel) + @Action() def disable_reference_trigger(self): """ @@ -1318,7 +1356,6 @@ def arm_start_trigger_source(self): err, value = self.lib.GetDigEdgeArmStartTrigSrc(RetStr(default_buf_size)) return value - @arm_start_trigger_source.setter @arm_start_trigger_source.setter def arm_start_trigger_source(self, source): source = str (source) diff --git a/lantz/drivers/ni/daqmx/channels.py b/lantz/drivers/ni/daqmx/channels.py index 3a19073..fa9c080 100644 --- a/lantz/drivers/ni/daqmx/channels.py +++ b/lantz/drivers/ni/daqmx/channels.py @@ -73,11 +73,11 @@ class VoltageInputChannel(Channel): def __init__(self, phys_channel, name='', terminal='default', min_max=(-10., 10.), units='volts', task=None): + terminal_val = self.terminal_map[terminal] + if not name: name = ''#phys_channel - terminal_val = self.terminal_map[terminal] - if units != 'volts': custom_scale_name = units units = Constants.Val_FromCustomScale @@ -100,19 +100,27 @@ class VoltageOutputChannel(Channel): CHANNEL_TYPE = 'AO' - def __init__(self, phys_channel, channel_name='', terminal='default', min_max=(-1, -1), units='volts'): + CREATE_FUN = 'CreateAOVoltageChan' - terminal_val = self.terminal_map[terminal] + def __init__(self, phys_channel, name='', min_max=(-10., 10.), + units='volts', task=None): + + if not name: + name = '' # phys_channel if units != 'volts': custom_scale_name = units - units = Constants.FROM_CUSTOM_SCALE + units = Constants.Val_FromCustomScale else: custom_scale_name = None - units = Constants.VOLTS + units = Constants.Val_Volts + + self._create_args = (phys_channel, name, min_max[0], min_max[1], units, + custom_scale_name) + + super().__init__(task=task, name=name) + - err = self.lib.CreateAOVoltageChan(phys_channel, channel_name, - min_max[0], min_max[1], units, custom_scale_name) # Not implemented: # DAQmxCreateAIAccelChan, DAQmxCreateAICurrentChan, DAQmxCreateAIFreqVoltageChan, @@ -168,17 +176,26 @@ class DigitalInputChannel(Channel): 'all_lines' - One channel for all lines """ + CHANNEL_TYPE = 'DI' + + CREATE_FUN = 'CreateDIChan' + + def __init__(self, lines, name='', group_by='line'): if group_by == 'line': - grouping_val = Constants.ChanPerLine + grouping_val = Constants.Val_ChanPerLine self.one_channel_for_all_lines = False else: - grouping_val = Constants.ChanForAllLines + grouping_val = Constants.Val_ChanForAllLines self.one_channel_for_all_lines = True - self.lib.CreateDIChan(lines, name, grouping_val) + self._create_args = (lines, name, grouping_val) + + super().__init__()#task=task, name=name) + + #self.lib.CreateDIChan(lines, name, grouping_val) class DigitalOutputChannel(Channel): @@ -195,16 +212,22 @@ class DigitalOutputChannel(Channel): See DigitalInputChannel """ + CHANNEL_TYPE = 'DO' + + CREATE_FUN = 'CreateDOChan' + def __init__(self, lines, name='', group_by='line'): if group_by == 'line': - grouping_val = Constants.ChanPerLine + grouping_val = Constants.Val_ChanPerLine self.one_channel_for_all_lines = False else: - grouping_val = Constants.ChanForAllLines + grouping_val = Constants.Val_ChanForAllLines self.one_channel_for_all_lines = True - self.lib.CreateDOChan(lines, name, grouping_val) + self._create_args = (lines, name, grouping_val) + + super().__init__()#task=task, name=name) class CountEdgesChannel(Channel): @@ -266,20 +289,24 @@ class CountEdgesChannel(Channel): CHANNEL_TYPE = 'CI' + CREATE_FUN = 'CreateCICountEdgesChan' + - def __init__ (self, counter, name="", edge='rising', init=0, direction='up'): + def __init__ (self, phys_counter, name="", edge='rising', init=0, direction='up', task=None): if edge == 'rising': - edge_val = Constants.RISING + edge_val = Constants.Val_Rising else: - edge_val = Constants.FALLING + edge_val = Constants.Val_Falling if direction == 'up': - direction_val = Constants.COUNT_UP + direction_val = Constants.Val_CountUp else: - direction_val = Constants.COUNT_DOWN + direction_val = Constants.Val_CountDown - self.lib.CreateCICountEdgesChan(counter, name, edge_val, direction_val) + self._create_args = (phys_counter, name, edge_val, init, direction_val) + + super().__init__(task=task, name=name) class LinearEncoderChannel(Channel): @@ -500,37 +527,117 @@ class MeasureFrequencyChannel(Channel): None. """ + CREATE_FUN = 'CreateCIFreqChan' + + def __init__(self, counter, name='', min_val=1e2, max_val=1e3, units="hertz", edge="rising", method="low_freq", - meas_time=1.0, divisor=1, custom_scale_name=None): + meas_time=1.0, divisor=1, custom_scale_name=None, + task=None): + self.data_type = float + name='test' + + assert divisor > 0 if method == 'low_freq': - meas_meth_val = Constants.LOW_FREQ1_CTR + meas_meth_val = Constants.Val_LowFreq1Ctr elif method == 'high_freq': - meas_meth_val = Constants.HIGH_FREQ2_CTR + meas_meth_val = Constants.Val_HighFreq2Ctr elif method == 'large_range': - meas_meth_val = Constants.LARGE_RANGE2_CTR + meas_meth_val = Constants.Val_LargeRng2Ctr if units != ('hertz', 'ticks'): custom_scale_name = units - units = Constants.FROM_CUSTOM_SCALE + units_val = Constants.Val_FromCustomScale else: custom_scale_name = None if units == 'hertz': - units = Constants.HZ + units_val = Constants.Val_Hz else: - units = Contstants.TICKS + units_val = Constants.Val_Ticks + + if edge == 'rising': + edge_val = Constants.Val_Rising + else: + edge_val = Constants.Val_Falling - self.lib.CreateCIFreqChan(counter, name, min_max[0], min_max[1], - units_val, edge_val, meas_meth_val, - meas_time, divisor, custom_scale_name) + self._create_args = (counter, name, min_val, max_val, units_val, + edge_val, meas_meth_val, meas_time, divisor, + custom_scale_name) + + super().__init__(task=task, name=name) +class CounterOutTicksChannel(Channel): + """ + Class for CounterOutputTicks Channel + + :class: + """ + + CREATE_FUN = 'DAQmxCreateCOPulseChanTicks' + + + + def __init__(self, counter, name='', source_terminal='', idle_state='high', init_delay=1, low_ticks=10, high_ticks=10, + task=None): + """ + Create channel(s) to generate digital pulses defined by the number of timebase ticks that the pulse is at a + high state and the number of timebase ticks that the pulse is at a low state and also adds the channel to the + task you specify with taskHandle. The pulses appear on the default output terminal of the counter unless you + select a different output terminal. + + + Args: + taskHandle: TaskHandle to which to add the channels that this function creates. + + counter: Name of the counter to use to create virtual channels. You can specify a list or range of physical + channels. + + nameToAssignToChannel: (string) name(s) to assign to the created virtual channel(s). If you do not specify + a name, NI-DAQmx uses the physical channel name as the virtual channel name. If you specify your own names + for nameToAssignToChannel, you must use the names when you refer to these channels in other NI-DAQmx + functions. If you create multiple virtual channels with one call to this function, you can specify a list + of names separated by commas. If you provide fewer names than the number of virtual channels you create, + NI-DAQmx automatically assigns names to the virtual channels. + + sourceTerminal: (string) terminal to which you connect an external timebase. You also can specify a source + terminal by using a terminal name. + + idleState: (int32) resting state of the output terminal. + Value Description + DAQmx_Val_High High state. + DAQmx_Val_Low Low state. + + initialDelay: (int32) number of timebase ticks to wait before generating the first pulse. + + lowTicks: (int32) The number of timebase ticks that the pulse is low. + + highTicks: (int32) number of timebase ticks that the pulse is high. + + Returns: + + status: The error code returned by the function in the event of an error or warning. A value of 0 + indicates success. A positive value indicates a warning. A negative value indicates an error. + + """ + + if idle_state == 'high': + idle_state_val = Constants.Val_High + else: + idle_state_val == 'low' + + + + self._create_args = (counter, name, source_terminal, idle_state_val, init_delay, low_ticks, high_ticks) + + + super().__init__(name=name, task=task) def create_channel_frequency(self, counter, name="", units='hertz', idle_state='low', delay=0.0, freq=1.0, duty_cycle=0.5): @@ -739,3 +846,21 @@ def create_channel_time(self, counter, name="", units="seconds", idle_state='low idle_state_val = self._get_map_value('idle_state', idle_state_map, idle_state) return CALL('CreateCOPulseChanTime', self, counter, name, units_val, idle_state_val, float64 (delay), float64(low_time), float64(high_time))==0 + + + +class CounterOutPulseChannel(Channel): + """ + Counter output pulse channel + """ + + def __init__(self): + """ + + + + Returns + -------- + + status: int32 + """ diff --git a/lantz/drivers/ni/daqmx/tasks.py b/lantz/drivers/ni/daqmx/tasks.py index eddfc73..676aade 100644 --- a/lantz/drivers/ni/daqmx/tasks.py +++ b/lantz/drivers/ni/daqmx/tasks.py @@ -127,18 +127,22 @@ def read(self, samples_per_channel=None, timeout=10.0, group_by='channel'): number_of_channels = self.number_of_channels() if group_by == Constants.Val_GroupByScanNumber: - data = np.zeros((samples_per_channel, number_of_channels), dtype=np.float64) + data = np.zeros((samples_per_channel, number_of_channels), + dtype=np.float64) else: - data = np.zeros((number_of_channels, samples_per_channel), dtype=np.float64) + data = np.zeros((number_of_channels, samples_per_channel), + dtype=np.float64) - err, data, count = self.lib.ReadAnalogF64(samples_per_channel, timeout, group_by, - data.ctypes.data, data.size, RetValue('i32'), None) + err, count = self.lib.ReadAnalogF64(samples_per_channel, timeout, + group_by, data.ctypes.data, + data.size, RetValue('i32'), + None) if samples_per_channel < count: if group_by == 'scan': return data[:count] else: - return data[:,:count] + return data[:, :count] return data @@ -149,9 +153,11 @@ class AnalogOutputTask(Task): CHANNEL_TYPE = 'AO' + @Action(units=(None, None, 'seconds', None), values=(None, None, None, _GROUP_BY)) def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): - """Write multiple floating-point samples or a scalar to a task + """ + Write multiple floating-point samples or a scalar to a task that contains one or more analog output channels. Note: If you configured timing for your task, your write is @@ -198,9 +204,10 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): data = np.asarray(data, dtype = np.float64) + number_of_channels = self.number_of_channels() - if data.ndims == 1: + if data.ndim == 1: if number_of_channels == 1: samples_per_channel = data.shape[0] shape = (samples_per_channel, 1) @@ -218,7 +225,11 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): else: samples_per_channel = data.shape[-1] - err, count = self.lib.WriteAnalogF64(samples_per_channel, auto_start, + #print(data) + + samps_per_channel = int(samples_per_channel) + + err, count = self.lib.WriteAnalogF64(samps_per_channel, auto_start, timeout, group_by, data.ctypes.data, RetValue('i32'), None) @@ -226,6 +237,7 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): return count + class DigitalTask(Task): @Action(units=(None, 'seconds', None), values=(None, None, _GROUP_BY)) @@ -335,7 +347,8 @@ class DigitalInputTask(DigitalTask): """Exposes NI-DAQmx digital input task to Python. """ - CHANNEL_TYPE = 'DI' + IO_TYPE = 'DI' + class DigitalOutputTask(DigitalTask): @@ -344,6 +357,8 @@ class DigitalOutputTask(DigitalTask): CHANNEL_TYPE = 'DO' + + @Action(units=(None, None, 'seconds', None), values=(None, {True, False}, None, _GROUP_BY)) def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): """ @@ -396,14 +411,16 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): 'group_by_scan_number' - Group by scan number (interleaved). """ - number_of_channels = self.get_number_of_channels() + number_of_channels = self.number_of_channels() if np.isscalar(data): data = np.array([data]*number_of_channels, dtype = np.uint8) else: data = np.asarray(data, dtype = np.uint8) - if data.ndims == 1: + print(data) + + if data.ndim == 1: if number_of_channels == 1: samples_per_channel = data.shape[0] shape = (samples_per_channel, 1) @@ -422,8 +439,8 @@ def write(self, data, auto_start=True, timeout=10.0, group_by='scan'): samples_per_channel = data.shape[-1] err, count = self.lib.WriteDigitalLines(samples_per_channel, - bool32(auto_start), - float64(timeout), group_by, + auto_start, + timeout, group_by, data.ctypes.data, RetValue('u32'), None) return count @@ -436,6 +453,7 @@ class CounterInputTask(Task): CHANNEL_TYPE = 'CI' + def read_scalar(self, timeout=10.0): """Read a single floating-point sample from a counter task. Use this function when the counter sample is scaled to a @@ -503,14 +521,17 @@ def read(self, samples_per_channel=None, timeout=10.0): :return: The array of samples read. """ + if samples_per_channel is None: samples_per_channel = self.samples_per_channel_available() data = np.zeros((samples_per_channel,),dtype=np.int32) - err, count = self.lib.ReadCounterU32(samples_per_channel, float64(timeout), + + err, count = self.lib.ReadCounterU32(samples_per_channel, float(timeout), data.ctypes.data, data.size, RetValue('i32'), None) + return data[:count] @@ -523,3 +544,4 @@ class CounterOutputTask(Task): Task.register_class(AnalogInputTask) +Task.register_class(DigitalInputTask) From 946c2ea906bbc29560c706b5ad36ada04da277a2 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Tue, 25 Oct 2016 15:05:35 -0500 Subject: [PATCH 59/61] fixed setuptools and __init__.py to install daqmx files --- lantz/drivers/ni/__init__.py | 7 ++++--- lantz/drivers/ni/daqmx/__init__.py | 5 ++--- setup.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lantz/drivers/ni/__init__.py b/lantz/drivers/ni/__init__.py index aec810c..9d25891 100644 --- a/lantz/drivers/ni/__init__.py +++ b/lantz/drivers/ni/__init__.py @@ -13,7 +13,8 @@ :license: BSD, see LICENSE for more details. """ -from . import daqmx - - +#__all__ = ['daqmx'] +#import lantz.drivers.ni.daqmx +#import daqmx +#from . import daqmx # diff --git a/lantz/drivers/ni/daqmx/__init__.py b/lantz/drivers/ni/daqmx/__init__.py index a749b22..d44629d 100644 --- a/lantz/drivers/ni/daqmx/__init__.py +++ b/lantz/drivers/ni/daqmx/__init__.py @@ -14,7 +14,7 @@ :company: National Instruments - :description: + :description: :website: http://www.ni.com/ ---- @@ -23,9 +23,8 @@ :license: BSD, see LICENSE for more details. """ +__all__ = ['base', 'channels', 'tasks', 'constants'] from .base import System, Task, Channel, Device from .channels import * from .tasks import * from .constants import Constants, Types - - diff --git a/setup.py b/setup.py index 2091f50..509508a 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def read(filename): 'lantz.utils', 'lantz.drivers'] + ['lantz.drivers.' + company for company in companies] + + ['lantz.drivers.ni.daqmx'] + ['lantz.drivers.legacy.' + company for company in legacy_companies], test_suite='lantz.testsuite.testsuite', install_requires=['pint>=0.6', From 1d8d7b5612808e6510aaeaf875d67d948e16e3e4 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 27 Oct 2016 16:37:54 -0500 Subject: [PATCH 60/61] changed IO_TYPE for counter input task to standardize --- lantz/drivers/ni/daqmx/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lantz/drivers/ni/daqmx/tasks.py b/lantz/drivers/ni/daqmx/tasks.py index 676aade..a9a14c1 100644 --- a/lantz/drivers/ni/daqmx/tasks.py +++ b/lantz/drivers/ni/daqmx/tasks.py @@ -451,7 +451,7 @@ class CounterInputTask(Task): """Exposes NI-DAQmx counter input task to Python. """ - CHANNEL_TYPE = 'CI' + IO_TYPE = 'CI' def read_scalar(self, timeout=10.0): From bbe083ac713cc549699a3d0161fe9c1b9261fd03 Mon Sep 17 00:00:00 2001 From: Peter Mintun Date: Thu, 27 Oct 2016 16:39:29 -0500 Subject: [PATCH 61/61] some changes for interfacing with counter input + FSM scan, interface could probably be better unified between CI and AI input choices, also really slow + probably unnecessary overhead with reconfiguring tasks --- lantz/drivers/newport/fsm300.py | 198 ++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 lantz/drivers/newport/fsm300.py diff --git a/lantz/drivers/newport/fsm300.py b/lantz/drivers/newport/fsm300.py new file mode 100644 index 0000000..79eeb86 --- /dev/null +++ b/lantz/drivers/newport/fsm300.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.fsm300 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation of FSM300 using NI DAQ controller + + Author: Kevin Miao + Date: 9/27/2016 +""" + +from lantz import Driver +from lantz.driver import Feat, DictFeat, Action +from lantz.drivers.ni.daqmx import AnalogOutputTask, VoltageOutputChannel + +from lantz import Q_ + +import time + +import numpy as np + +def enforce_point_units(point, units='um'): + x, y = point + if not isinstance(x, Q_): + x = Q_(x, 'um') + if not isinstance(y, Q_): + y = Q_(y, 'um') + point = x, y + return point + + +class FSM300(Driver): + + def __init__(self, x_ao_ch, y_ao_ch, + ao_smooth_rate=Q_('10 kHz'), ao_smooth_steps=Q_('1000 1/V'), + limits=((Q_(-10, 'V'), Q_(10, 'V')), (Q_(-10, 'V'), Q_(10, 'V'))), + cal=(Q_(9.5768, 'um/V'), Q_(7.1759, 'um/V'))): + x_limits_mag = tuple(float(val / Q_('1 V')) for val in limits[0]) + y_limits_mag = tuple(float(val / Q_('1 V')) for val in limits[1]) + self.task = AnalogOutputTask('fsm300') + VoltageOutputChannel(x_ao_ch, name='fsm_x', min_max=x_limits_mag, units='volts', task=self.task) + VoltageOutputChannel(y_ao_ch, name='fsm_y', min_max=y_limits_mag, units='volts', task=self.task) + self.ao_smooth_rate = ao_smooth_rate + self.ao_smooth_steps = ao_smooth_steps + self.cal = cal + + self._position = (Q_('0 um'), Q_('0 um')) + + super().__init__() + + return + + def ao_smooth_func(self, init_point, final_point): + init_x, init_y = init_point + final_x, final_y = final_point + + init_x_voltage, final_x_voltage = init_x / self.cal[0], final_x / self.cal[0] + init_y_voltage, final_y_voltage = init_y / self.cal[1], final_y / self.cal[1] + diff_x_voltage = final_x_voltage - init_x_voltage + diff_y_voltage = final_y_voltage - init_y_voltage + + diff_voltage = max(abs(diff_x_voltage), abs(diff_y_voltage)) + steps = int(np.ceil(diff_voltage * self.ao_smooth_steps)) + init = np.array([val.to('V').magnitude for val in [init_x_voltage, init_y_voltage]]) + diff = np.array([val.to('V').magnitude for val in [diff_x_voltage, diff_y_voltage]]) + + versine_steps = (1.0 - np.cos(np.linspace(0.0, np.pi, steps))) / 2.0 + + step_voltages = np.outer(np.ones(steps), init) + np.outer(versine_steps, diff) + return step_voltages + + def ao_linear_func(self, init_point, final_point, steps): + init_x, init_y = init_point + final_x, final_y = final_point + + init_x_voltage, final_x_voltage = init_x / self.cal[0], final_x / self.cal[0] + init_y_voltage, final_y_voltage = init_y / self.cal[1], final_y / self.cal[1] + diff_x_voltage = final_x_voltage - init_x_voltage + diff_y_voltage = final_y_voltage - init_y_voltage + + diff_voltage = max(abs(diff_x_voltage), abs(diff_y_voltage)) + init = np.array([val.to('V').magnitude for val in [init_x_voltage, init_y_voltage]]) + diff = np.array([val.to('V').magnitude for val in [diff_x_voltage, diff_y_voltage]]) + + linear_steps = np.linspace(0.0, 1.0, steps) + + step_voltages = np.outer(np.ones(steps), init) + np.outer(linear_steps, diff) + return step_voltages + + @Feat() + def abs_position(self): + return self._position + + + @abs_position.setter + def abs_position(self, point): + point = enforce_point_units(point) + step_voltages = self.ao_smooth_func(self._position, point) + if step_voltages.size: + steps = step_voltages.shape[0] + clock_config = { + 'source': 'OnboardClock', + 'rate': self.ao_smooth_rate.to('Hz').magnitude, + 'sample_mode': 'finite', + 'samples_per_channel': steps, + } + self.task.configure_timing_sample_clock(**clock_config) + task_config = { + 'data': step_voltages, + 'auto_start': False, + 'timeout': 0, + 'group_by': 'scan', + } + self.task.write(**task_config) + self.task.start() + time.sleep((steps / self.ao_smooth_rate).to('s').magnitude) + self.task.stop() + self._position = point + + @Action() + def line_scan(self, init_point, final_point, steps, acq_task, acq_rate=Q_('10 kHz'), pts_per_pos=100): + init_point = enforce_point_units(init_point) + final_point = enforce_point_units(final_point) + step_voltages = self.ao_linear_func(init_point, final_point, steps) + step_voltages = np.repeat(step_voltages, pts_per_pos, axis=0) + + if acq_task.IO_TYPE == 'CI': + + # add extra sample for taking diff + clock_config = { + #'source': '/Dev1/ao/SampleClock', + 'rate': acq_rate.to('Hz').magnitude, + 'sample_mode': 'finite', + 'samples_per_channel': len(step_voltages) + 1, + } + self.task.configure_timing_sample_clock(**clock_config) + clock_config = { + 'source': '/Dev1/ao/SampleClock', + 'rate': acq_rate.to('Hz').magnitude, + 'sample_mode': 'finite', + 'samples_per_channel': len(step_voltages) + 1, + } + acq_task.configure_timing_sample_clock(**clock_config) + task_config = { + 'data': step_voltages, + 'auto_start': False, + 'timeout': 0, + 'group_by': 'scan', + } + acq_task.arm_start_trigger_source = '/Dev1/ao/StartTrigger' + acq_task.arm_start_trigger_type = 'digital_edge' + acq_task.start() + + self.task.write(**task_config) + + self.task.start() + time.sleep(len(step_voltages)/acq_rate.to('Hz').magnitude) + #acq_task.arm_start_trigger_source = 'Dev1/PFI15' + #acq_task.arm_start_trigger_type = 'digital_edge' + + scanned = acq_task.read(samples_per_channel=len(step_voltages)+1, + timeout=(len(step_voltages) + 1)/acq_rate.to('Hz').magnitude) + + #delta_cts = np.insert(np.diff(scanned), scanned[0], 0) + delta_cts = np.diff(scanned) + rate = delta_cts * acq_rate.to('Hz').magnitude + acq_task.stop() + self.task.stop() + nb_chan = scanned.shape[0] + + # TODO: check to make sure that this reshapes the way it should + return rate.reshape((pts_per_pos,steps)) + + else: + + clock_config = { + 'source': 'OnboardClock', + 'rate': acq_rate.to('Hz').magnitude, + 'sample_mode': 'finite', + 'samples_per_channel': len(step_voltages), + } + self.task.configure_timing_sample_clock(**clock_config) + acq_task.configure_timing_sample_clock(**clock_config) + task_config = { + 'data': step_voltages, + 'auto_start': False, + 'timeout': 0, + 'group_by': 'scan', + } + self.task.write(**task_config) + self.task.configure_trigger_digital_edge_start('ai/StartTrigger') + self.task.start() + acq_task.start() + scanned = acq_task.read(samples_per_channel=len(step_voltages)) + acq_task.stop() + self.task.stop() + nb_chan = scanned.shape[0] + return scanned.reshape((nb_chan,steps,pts_per_pos)).mean(axis=2)