Source code for PyICe.lab_instruments

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Instrument Drivers
##################

Supported lab instruments
-------------------------
agilent_34970a_chassis
  collection instrument for all 34970 plugins
agilent_34970a_20ch
  20ch meter
agilent_34970a_34908A_40ch
  40ch meter
agilent_34970a_dacs
  2 dacs
agilent_34970a_dig_out8
  8 bit digital output
agilent_34970a_dig_in8
  8 bit digital input
agilent_34970a_actuator
  34970a actuator module
agilent_e3631a
  triple supply
agilent_e53181a
  frequency counter
agilent_34401a
  multimeter
agilent_34461a
  multimeter
bus_pirate_gpio
  general purpose input/output using the bus pirate
bk 8500
  single channel 300W electronic load
hp_3478a
  multimeter
agilent_n3301
  2 channel electronic load
htx9000
  usb serial load box (Steve Martin)
htx9001
  configurator pro (Steve Martin)
htx9001a
  configurator pro A (Steve Martin)
kikusui_plz334w
  Kikusui PLZ 334W single channel 330w load (Almost certainly also supports PLZ 664WA)
kikusui_pbz20_20
  Kikusui PBZ20-20 single channel bipolar power supply (20V/20A)
kikusui_pbz40_10
  Kikusui PBZ40-10 single channel bipolar power supply (40V/10A)
hameg_4040
  Hameg 4 channel 10A 384W supply
sorensen_lhp_100_10
  1000w supply 100v 10a
sorensen_xt_250_25
  250V 250mA DC supply
tektronix_3054
  Oscilloscope
autonicstk
  Autonics TK series RS-485 Modbus PID Temperature Controller
modbus_relay
  Two-channel Modbus RTU RS-485 controllable relay
temptronic_4310
  ThermoStream (max temp 175)
delta_9039
  GPIB Oven
Sun EC1x
  Temp chamber
Sun EX0x
  Temp chamber
Franken_oven
  Temperature chamber controlled by autonicstk and modbus_relay
oven_w4sd
  Vermont Lab Oven controller
krohnhite_523
  DC Source/Calibrator
krohnhite_526
  DC Source/Calibrator with LAN
powermux
  8x8 power switching matrix board (Steve Martin)
keithley_7002
  Switch system
hp_3458a
  8.5 digit multimeter
RL1000
  Reay Labs Efficiency Meter.
agilent_3034a
  4-channel oscilloscope
hp_4195a
  network analyzer
keithley_4200
  semiconductor parameter analyzer (HP4145 command set)
hp_4155b
  semiconductor parameter analyzer (HP4145 command set)
ADT7410
  Analog Devices I2C Silicon Temperature Sensor
AD5272
  Analog Devices I2C 10-bit Potentiometer
PCF8574
  I2C GPIO chip, multiple vendors
TDS640A
  Tektronix Digital Scope
fluke45
  multimeter
saleae
  Logic Pro 8/16 Analog DAQ. Digital support possible later
agilent_e4433b
  Agilent E4433B ESG-D Series Signal Generator 250kHz - 4GHz
firmata
  Firmata universal embedded system (Arduino) control protocol

Virtual Instruments
-------------------
instrument_humanoid
  Notification helper to put human control of a manual instrument into an otherwise automated measurement.
delay_loop
  Instrument wrapper for lab_utils.delay_loop enables logging of delay diagnostic vairables.
clipboard
  Store and retrieve data to/from windows/linux/osx clipboard.
accumulator
  Accumulator Virtual Instrument
timer
  Timer Virtual Instrument
integrator
  Integrator Virtual Instrument
differencer
  Differencer Virtual Instrument
differentiator
  Differentiator Virtual Instrument
servo
  Servo controller, for kelvins etc
servo_group
  Group Servo Control, servos multiple servos into regulation
ramp_to
  Move forcing channels incrementally to control overshoot
threshold_finder
  Automatically find thresholds and hysteresis
calibrator
  Remap write values through two-point or univariate transform
smart_battery_emulator
  A pair of threaded writers to output smart battery commands to a smart-battery charger


'''

from lab_core import *
import lab_interfaces, visa_wrappers, lab_utils
import sys, time, datetime, math, atexit
import tempfile, struct, os
from collections import OrderedDict
from abc import ABCMeta, abstractmethod


[docs]class a3497xa_instrument(scpi_instrument,delegator): '''superclass of all Agilent 34970 plugin instruments''' def __init__(self,name, automatic_monitor): scpi_instrument.__init__(self,name) self._base_name = 'a3497xa' delegator.__init__(self) self.scan_active = False self._last_scan_internal_addresses = None self._scanlist_ordered = [] self.monitor_channel_num = None self.enable_automatic_monitor(automatic_monitor) #This doesn't work!!! Monitor update rate is also affected by channel delay for some reason. self._automatic_monitor = automatic_monitor #Why twice???
[docs] def enable_automatic_monitor(self, enable): #This doesn't work!!! Monitor update rate is also affected by channel delay for some reason. '''set to True to enable monitor channel auto-switching after single-channel scanlist read. After first reading, front panel display will continuously update with new results and successive reads will generally be faster without mux switching. set False to force traditional scanlist behavior and manual monitor channel selection via set_monitor and get_monitor_data methods.''' self._automatic_monitor = enable
def read_delegated_channel_list(self,channel_list): #channel_list is a list of channel objects # returns a dictionary of read data by channel name results = collections.OrderedDict() #special case for reading the moniotor #This doesn't work!!! Monitor update rate is also affected by channel delay for some reason. if False and self._automatic_monitor \ and len(channel_list) == 1 \ and channel_list[0].get_attribute('internal_address') == self.monitor_channel_num \ and channel_list[0].get_attribute('34970_type') in ['volts_dc', 'current_dc', 'thermocouple']: az_multiplier = 4 if channel_list[0].get_attribute('auto_zero') else 2 #wait for two full measurement cycles in case first one was just getting started when input changed. Then add one more below for mismatched clocks. max_time = time.time() + (az_multiplier+1)*0.017*max(channel_list[0].get_attribute('NPLC'),1) #emergency exit in the case of excessive quantization noise to thermal noise ratio. #measurements take ~2x the expected NPLC time with autozero and ~1x the expected NPLC time with autozero disabled, #but don't scale well below NPLC=1 stale_result = self.get_monitor_data() result = stale_result while result == stale_result and time.time() < max_time: #wait result = self.get_monitor_data() questionable_result = result #may have changed forcing conditions during last conversion while result == questionable_result and time.time() < max_time: #wait some more result = self.get_monitor_data() #if time.time() > max_time: # print 'WARNING: 3497x monitor channel: {} read timed out without value change. This might be normal if quantization noise exceeds thermal noise.'.format(channel_list[0].get_name()) results[channel_list[0].get_name()] = self.get_monitor_data() return results self.scan_active = True scan_internal_addresses = [channel.get_attribute('internal_address') for channel in channel_list] if self.monitor_channel_num is not None: scan_internal_addresses.append(self.monitor_channel_num) scan_internal_addresses = list(set(scan_internal_addresses)) # remove duplicates # note cannot store last scan and avoid writing it reconfiguring channels breaks it self._last_scan_internal_addresses = scan_internal_addresses cmd = "ROUTe:SCAN (@" for internal_address in scan_internal_addresses: cmd += str(internal_address) + ',' cmd = cmd.rstrip(',') + ")" self.get_interface().write(cmd) #then get the list back to learn channel order txt_scanlist = self.get_interface().ask("ROUTe:SCAN?") try: txt_scanlist = txt_scanlist.split("(@")[1] except: print 'Communication problem; attempting resyc.' self.get_interface().resync() raise Exception('Resync complete; better luck next time.') txt_scanlist = txt_scanlist.strip(")'") self._scanlist_ordered = map(int, txt_scanlist.split(",")) self.init() self.operation_complete() txt = self.fetch() vals = txt.split(",") self.scan_results = {} for (internal_address,val) in zip(self._scanlist_ordered,vals): self.scan_results[internal_address] = val for channel in channel_list: results[channel.get_name()] = channel.read_without_delegator() #if self._automatic_monitor and len(channel_list) == 1: if len(channel_list) == 1: self.monitor_channel_num = channel_list[0].get_attribute('internal_address') self.get_interface().write("ROUTe:MONitor (@{})".format(self.monitor_channel_num)) self.get_interface().write("ROUTe:MONitor:STATe ON") return results def read_raw(self,internal_address): # the scan list is in the delegator, not the creating instrument assert internal_address in self.resolve_delegator().scan_results return self.resolve_delegator().scan_results[internal_address] def read_apply_function(self,internal_address,function): return function(self.read_raw(internal_address)) def _add_bay_number(self,channel_object,bay,number): channel_object.set_attribute('bay',bay) channel_object.set_attribute('number',number) channel_object.set_attribute('internal_address',number+100*bay) def _get_internal_address_by_name(self,channel_name): channel = self.get_channel(channel_name) return channel.get_attribute('internal_address')
[docs] def set_monitor(self,monitor_channel_name): '''View named channel measurement on the front panel whenever scan is idle''' channel = self.get_channel(monitor_channel_name) channel_number = channel.get_attribute('internal_address') if channel_number != self.monitor_channel_num: cmd = "ROUTe:SCAN (@{})".format(channel_number) self.get_interface().write(cmd) self.monitor_channel_num = channel_number self.get_interface().write("ROUTe:MONitor (@{})".format(channel_number)) self.get_interface().write("ROUTe:MONitor:STATe ON")
def get_monitor_data(self,channel_name = None): if channel_name is not None: self.set_monitor(channel_name) '''return data from last monitor reading''' return float(self.get_interface().ask('ROUTe:MONitor:DATA?'))
[docs]class agilent_3497xa_chassis(a3497xa_instrument): '''A lab_bench-like container object to speed up operation of the 34970 instrument. If each plugin from the three expansion bays is added individually to the lab_bench, the scanlist must be modified for each plugin to run separate scans on each individual plugin. This object will construct a composite scanlist, then appropriately parse the results back to the individual instruments. ''' def __init__(self, interface_visa, automatic_monitor=True): '''Agilent 34970 collection object.''' self._base_name = '34970a_chasis' a3497xa_instrument.__init__(self,'34970a_chasis @ {}'.format(interface_visa), automatic_monitor=automatic_monitor) self.add_interface_visa(interface_visa)
[docs] def add(self,new_instrument): '''only appropriate to add instantiated 34907 plugin instrument objects to this class (20ch, 40ch, dacs, dig-in, dig-out, etc)''' if not isinstance(new_instrument, a3497xa_instrument): raise Exception("{} doesn't fit inside 34970 expansion bay. If you push too hard, you might break something".format(instrument)) channel_group.add(self,new_instrument) new_instrument.set_delegator(self)
class agilent_34970a_chassis(agilent_3497xa_chassis): pass class agilent_34972a_chassis(agilent_3497xa_chassis): pass
[docs]class agilent_3497xa_20ch_40ch(a3497xa_instrument): '''Superclass for the 34901A, 34902A and 34908A input measurement multiplexers All functionality common to the 20Ch and 40Ch is implemented here and inherited by the appropriate subclasses. Capabilities: 34901A, 34902A: Scanning and direct measurement of temperature, voltage, resistance, frequency, and current (34901A only) using the internal DMM. 34908A: Scanning and direct measurement of temperature, voltage, and resistance using the internal DMM. ''' def __init__(self,interface_visa, bay, automatic_monitor=True): '''"interface_visa" bay is 1-3. 1 is top slot, 3 is bottom slot.''' self._base_name = '34970a_mux' self.bay = bay a3497xa_instrument.__init__(self,'34970a_mux bay: {} @ {}'.format(bay,interface_visa), automatic_monitor=automatic_monitor) self.add_interface_visa(interface_visa)
[docs] def add_channel(self,channel_name,channel_num): '''Register a named channel. No configuration takes place. When the channel is read directly, or through read_channels(), an appropriate scanlist will be written to the 34970. channel_num is 1-22 for the 20Ch mux (1-20 no current, 21-22 current only) channel_num is 1-40 for the 40Ch mux''' internal_address = self.bay*100 + channel_num new_channel = channel(channel_name,read_function=lambda: self.read_apply_function(internal_address,float)) new_channel.set_delegator(self) self._add_bay_number(new_channel,self.bay,channel_num) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_dc_voltage(self,channel_name,channel_num,NPLC=1,range="AUTO",high_z=True,delay=None,disable_autozero=True,Rsource=None): '''Shortcut method to add voltage channel and configure in one step.''' #TODO: add_extended_channels argument to add nplc, delay, etc all at once??? channel = self.add_channel(channel_name, channel_num) channel.set_attribute('34970_type','volts_dc') channel.set_display_format_function(function = lambda float_data: lab_utils.eng_string(float_data, fmt=':3.6g',si=True) + 'V') if Rsource is not None: if delay is not None: print ("WARNING: Rsource({Rsource:.3g} ohms) and Delay({delay:.3g} s) " "BOTH specified in add_channel_dc_voltage('{chname}')\n" " hence I'm IGNORING Rsource and setting Delay to {delay:g} s" ).format(Rsource=Rsource, delay=delay, chname=channel_name) else: delay = max(round(9 * Rsource * 1390e-12, 3), 0.002) # per Steve's memo SM87, 2ms minimum. Delay resolution = 1ms per Agilent manual. channel.set_attribute("Rsource", Rsource) self._config_dc_voltage(channel,NPLC,range,high_z,delay,disable_autozero) channel.set_attribute("delay", delay) channel.set_description(self.get_name() + ': ' + self.add_channel_dc_voltage.__doc__) return channel
[docs] def add_channel_thermocouple(self,channel_name,channel_num,tcouple_type,NPLC=1,disable_autozero=True): '''Shortcut method to add thermistor measurement channel and configure in one step.''' new_channel = self.add_channel(channel_name,channel_num) new_channel.set_attribute('34970_type', 'thermocouple') new_channel.set_display_format_function(function = lambda float_data: lab_utils.eng_string(float_data, fmt=':3.6g',si=True) + u'°C') internal_address = new_channel.get_attribute('internal_address') self._config_thermocouple(internal_address,tcouple_type) self._configure_channel_nplc(new_channel,NPLC) self._configure_channel_autozero(new_channel, disable_autozero) new_channel.set_description(self.get_name() + ': ' + self.add_channel_thermocouple.__doc__) return new_channel
def _set_impedance_10GOhm(self, channel, high_z=True): '''set channel impedance to >1GOhm if high_z is True and voltage range allows, otherwise 10M impedance always 10M if argument is false''' internal_address = channel.get_attribute('internal_address') channel.set_attribute('input_impedance_hiz',high_z) if (high_z == True): self.get_interface().write("INPut:IMPedance:AUTO ON , (@{})".format(internal_address)) else: self.get_interface().write("INPut:IMPedance:AUTO OFF , (@{})".format(internal_address)) def _config_dc_voltage(self,channel,NPLC,range,high_z,delay,disable_autozero): '''Reconfigure channel to measure DC voltage, with input impedance >10G if range allows. Optionally specify number of powerline cycles integration period and attenuator range.''' internal_address = channel.get_attribute('internal_address') self.get_interface().write("CONFigure:VOLTage:DC {} , (@{})".format(range,internal_address)) self._configure_channel_nplc(channel,NPLC) self._set_impedance_10GOhm(channel, high_z) self._configure_channel_autozero(channel, disable_autozero) if delay is not None: self._config_channel_delay(channel,delay) def _config_thermocouple(self,internal_address,thermocouple_type): if thermocouple_type.upper() not in ['J','K','T']: raise Exception('Invalid thermocouple type, valid types are J,K,T') self.get_interface().write("CONFigure:TEMPerature TCouple,{},(@{})".format(thermocouple_type.upper(),internal_address)) def _config_channel_delay(self,channel,delay): '''Delay specified number of seconds between closing relay and starting DMM measurement for channel.''' internal_address = channel.get_attribute('internal_address') channel.set_attribute('delay',delay) self.get_interface().write("ROUT:CHAN:DELAY {},(@{})".format(delay,internal_address)) def _configure_channel_autozero(self,channel,disable_autozero): '''Disable or enable (default) the autozero mode. The OFF and ONCE parameters have a similar effect. Autozero OFF does not issue a new zero measurement until the next time the instrument goes to the "wait-for-trigger" state. Autozero ONCE issues an immediate zero measurement. The :AUTO? query the autozero mode. Returns "0" (OFF or ONCE) or "1" (ON). Autozero OFF Operation Following instrument warm-up at calibration temperature ±1 °C and < 10 minutes, add 0.0002% range additional error + 5 μV. ''' internal_address = channel.get_attribute('internal_address') self.get_interface().write("SENSe:ZERO:AUTO {},(@{})".format("ONCE" if disable_autozero else "ON",internal_address)) channel.set_attribute('auto_zero',not(disable_autozero)) def _config_channel_scaling(self,channel,gain=1,offset=0,unit=None): '''Perform y=mx+b scaling to channel inside instrument and change displayed units''' internal_address = channel.get_attribute('internal_address') if gain is not None: self.get_interface().write("CALCulate:SCALe:GAIN {}, (@{})".format(gain,internal_address)) channel.set_attribute('gain', gain) if offset is not None: self.get_interface().write("CALCulate:SCALe:OFFSet {}, (@{})".format(offset,internal_address)) channel.set_attribute('offset', offset) if unit is not None: self.get_interface().write('CALCulate:SCALe:UNIT "{}", (@{})'.format(unit,internal_address)) channel.set_attribute('unit', unit) channel.set_display_format_function(function = lambda float_data: lab_utils.eng_string(float_data, fmt=':3.6g',si=True) + unit) self.get_interface().write("CALCulate:SCALe:STATe ON ,(@{})".format(internal_address)) def _configure_channel_nplc(self,channel,nplc): try: channel_type = channel.get_attribute('34970_type') internal_address = channel.get_attribute('internal_address') channel.set_attribute('NPLC',nplc) except ChannelAttributeError: raise Exception('Cannot configure nplc for channel {}'.format(channel)) if channel_type == 'volts_dc': self.get_interface().write("VOLTage:DC:NPLC {}, (@{})".format(nplc,internal_address)) elif channel_type == 'current_dc': self.get_interface().write("CURRent:DC:NPLC {},(@{})".format(nplc,internal_address)) elif channel_type == 'thermocouple': self.get_interface().write("SENSe:TEMPerature:NPLC {},(@{})".format(nplc,internal_address)) else: raise Exception('Unkown 34970_type, cannot set NPLC for this type of channel')
[docs] def add_channel_nplc(self,channel_name,base_channel): '''adds a secondary channel that can modify the nplc setting of an existing channel.''' new_channel = channel(channel_name,write_function=lambda nplc: self._configure_channel_nplc(base_channel,nplc) ) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_nplc.__doc__) new_channel.write(base_channel.get_attribute('NPLC')) return self._add_channel(new_channel)
[docs] def add_channel_delay(self,channel_name,base_channel): '''adds a secondary channel that can modify the delay of an existing channel''' new_channel = channel(channel_name,write_function=lambda delay: self._config_channel_delay(base_channel,delay)) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delay.__doc__) new_channel.write(base_channel.get_attribute('delay')) return self._add_channel(new_channel)
[docs] def add_channel_input_hiz(self,channel_name,base_channel): '''adds a secondary channel that can modify the input impedance of an existing channel. Write channel to True for >10G mode (<~10V), False for 10Meg mode.''' new_channel = integer_channel(channel_name,size=1,write_function=lambda hiz: self._set_impedance_10GOhm(base_channel,hiz)) self._add_channel(new_channel) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_input_hiz.__doc__) new_channel.write(base_channel.get_attribute('input_impedance_hiz')) return new_channel
[docs] def add_channel_autozero(self,channel_name,base_channel): '''adds a secondary channel that can modify the auto-zero mode of an existing channel. Write channel to True autozero every measurement (doubling measurement time), False for one-time autozero.''' new_channel = integer_channel(channel_name,size=1,write_function=lambda enable_autozero: self._configure_channel_autozero(base_channel,disable_autozero=not(enable_autozero))) self._add_channel(new_channel) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_autozero.__doc__) new_channel.write(base_channel.get_attribute('auto_zero')) return new_channel
[docs] def add_channel_gain(self,channel_name,base_channel): '''adds a secondary channel that can modify the gain (span multiplier) of an existing channel''' new_channel = channel(channel_name,write_function=lambda gain: self._config_channel_scaling(base_channel,gain=gain,offset=None,unit=None)) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_gain.__doc__) try: new_channel.write(base_channel.get_attribute('gain')) except KeyError: new_channel.write(1) return self._add_channel(new_channel)
[docs] def add_channel_offset(self,channel_name,base_channel): '''adds a secondary channel that can modify the offset of an existing channel''' new_channel = channel(channel_name,write_function=lambda offset: self._config_channel_scaling(base_channel,gain=None,offset=offset,unit=None)) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_offset.__doc__) try: new_channel.write(base_channel.get_attribute('offset')) except KeyError: new_channel.write(0) return self._add_channel(new_channel)
[docs] def add_channel_unit(self,channel_name,base_channel): '''adds a secondary channel that can modify the displayed unit (V/A/etc) of an existing channel''' new_channel = channel(channel_name,write_function=lambda unit: self._config_channel_scaling(base_channel,gain=None,offset=None,unit=unit)) new_channel.set_attribute('measurement_channel', base_channel.get_name()) new_channel.set_description(self.get_name() + ': ' + self.add_channel_offset.__doc__) try: new_channel.write(base_channel.get_attribute('unit')) except KeyError: new_channel.write('V') return self._add_channel(new_channel)
[docs]class agilent_3497xa_20ch(agilent_3497xa_20ch_40ch): '''Extends base class to add methods specific to the 20-channel mux that are not appropriate for the 40-channel mux such as frequency, current measurement (internal shunt), and current measurement (external sense resistor).''' def __init__(self, *args, **kwargs): agilent_3497xa_20ch_40ch.__init__(self, *args, **kwargs) self.plugin_type = "34901A"
[docs] def add_channel_dc_current(self,channel_name,channel_num,NPLC=1,range='AUTO',delay=None,disable_autozero=True): '''DC current measurement only allowed on 34901A channels 21 and 22''' if channel_num not in [21,22]: raise Exception('Invalid channel number, channel cannot be used for current') channel = self.add_channel(channel_name, channel_num) channel.set_attribute('34970_type','current_dc') channel.set_display_format_function(function = lambda float_data: lab_utils.eng_string(float_data, fmt=':3.6g',si=True) + 'A') self._config_dc_current(channel,range) self._configure_channel_nplc(channel,NPLC) self._configure_channel_autozero(channel,disable_autozero) if delay is not None: self._config_channel_delay(channel,delay) channel.set_description(self.get_name() + ': ' + self.add_channel_dc_current.__doc__) return channel
def _config_dc_current(self,channel,range="AUTO"): '''DC current measurement only allowed on 34901A channels 21 and 22''' internal_address = channel.get_attribute('internal_address') channel.set_attribute('range', range) self.get_interface().write("CONFigure:CURRent:DC {},(@{})".format(range, internal_address))
[docs] def add_channel_ammeter_range(self, channel_name, base_channel): '''Modify ammeter current range shunt.''' assert base_channel.get_attribute('number') == 21 or base_channel.get_attribute('number') == 22 range_channel = channel(channel_name, write_function = lambda range: self._config_dc_current(base_channel, range)) range_channel.set_description(self.get_name() + ': ' + self.add_channel_ammeter_range.__doc__) range_channel.set_attribute('measurement_channel', base_channel.get_name()) range_channel.write(base_channel.get_attribute('range')) return self._add_channel(range_channel)
[docs] def config_freq(self,channel_name): '''Configure a channel to measure frequency.''' print 'config_freq expect this to change and become an add_channel' internal_address = self._get_internal_address_by_name(channel_name) self.get_interface().write("CONFigure:FREQuency (@{})".format(internal_address))
[docs] def config_res(self,channel_name): '''DC resistance measurement ''' print 'config_res expect this to change and become an add_channel' ch_list = "(@{})".format(self._get_internal_address_by_name(channel_name)) self.get_interface().write("CONFigure:RESistance " + ch_list)
[docs] def add_channel_current_sense(self,channel_name,channel_num,gain=1,NPLC=10,range="AUTO",resistance=None,delay=None,disable_autozero=True,Rsource=None): '''Configure channel to return current measurement by scaling voltage measured across user-supplied sense resistor. Specify either gain or its reciprocal resistance.''' if Rsource is None: Rsource = resistance channel = self.add_channel_dc_voltage(channel_name = channel_name, channel_num = channel_num, NPLC = NPLC, range = range, disable_autozero = disable_autozero, Rsource = Rsource ) if resistance != None and gain != 1: raise Exception('Resistance and Gain cannot both be specified') if (resistance is not None): gain = 1.0/resistance channel.set_attribute("shunt_resistance", resistance) channel.set_attribute("gain", gain) self._config_channel_scaling(channel,gain,0,"A") if delay is not None: self._config_channel_delay(channel,delay) channel.set_description(self.get_name() + ': ' + self.add_channel_current_sense.__doc__) channel.set_display_format_function(function = lambda float_data: lab_utils.eng_string(float_data, fmt=':3.6g',si=True) + 'A') return channel
class agilent_34970a_20ch(agilent_3497xa_20ch): pass class agilent_34972a_20ch(agilent_3497xa_20ch): pass
[docs]class agilent_3497xa_40ch(agilent_3497xa_20ch_40ch): '''Implement any methods specific to the 40-channel mux here.''' def __init__(self, *args, **kwargs): agilent_3497xa_20ch_40ch.__init__(self, *args, **kwargs) self.plugin_type = "34908A"
class agilent_34970a_40ch(agilent_3497xa_40ch): pass class agilent_34972a_40ch(agilent_3497xa_40ch): pass
[docs]class agilent_34970a_34908A_40ch(agilent_3497xa_40ch): '''Extend base class to add module name to class name for compatibility.''' pass
[docs]class agilent_34972a_34908A_40ch(agilent_3497xa_40ch): '''Extend base class to add module name to class name for compatibility.''' pass
[docs]class agilent_34970a_34901A_20ch(agilent_3497xa_20ch): '''Extend base class to add module name to class name for compatibility.''' pass
[docs]class agilent_34972a_34901A_20ch(agilent_3497xa_20ch): '''Extend base class to add module name to class name for compatibility.''' pass
[docs]class agilent_3497xa_dacs(a3497xa_instrument): '''control of the two dacs in the multifunction module''' def __init__(self,interface_visa, bay): '''Bay is numbered (1,2,3). 1 is the upper bay. 3 is the lower bay.''' self._base_name = 'agilent_3497xa_dacs' self.bay = bay self.plugin_type = "34907A" a3497xa_instrument.__init__(self,'34970a_dacs bay: {} @ {} '.format(bay,interface_visa) ) self.add_interface_visa(interface_visa)
[docs] def add_channel(self,channel_name,channel_num): '''Add named DAC channel to instrument. num is 1-2, mapping to physical channel 4-5.''' if((channel_num != 1) & (channel_num != 2)): print("ERROR invalid dac " + self.get_name() + ", " + channel_num) channel_num += 3 internal_address = channel_num + self.bay*100 new_channel = channel(channel_name,write_function=lambda voltage: self.write_voltage(internal_address,voltage)) new_channel.set_attribute('34970_type','DAC') self._add_bay_number(new_channel,self.bay,channel_num) self._add_channel(new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return new_channel
[docs] def write_voltage(self,internal_address,voltage): '''Set named DAC to voltage. Range is +/-12V with 16bit (366uV) resolution.''' txt = "SOURCE:VOLT " + str(voltage) + ", (@" + str(internal_address) + ")" self.get_interface().write(txt)
class agilent_34970a_dacs(agilent_3497xa_dacs): pass class agilent_34972a_dacs(agilent_3497xa_dacs): pass
[docs]class agilent_3497xa_actuator(a3497xa_instrument): '''agilent_a34970a_actuator 20 channel general purpose actuator plugin module each channel is a relay which can be toggled from open to closed. note that the physical open button on the unit switches the relay to the NC position.''' def __init__(self,interface_visa,bay): '''interface_visa is a interface_visa bay is 34970 plugin bay 1-3. ''' self._base_name = 'agilent_3497xa_actuator' self.bay = bay self.plugin_type = "34903A" a3497xa_instrument.__init__(self,'34970a_actuator: {} @ {} '.format(bay,interface_visa), automatic_monitor=False) self.add_interface_visa(interface_visa)
[docs] def add_channel(self,channel_name,channel_num): '''channel_num is 1-20''' internal_address = channel_num + self.bay*100 new_channel = channel(channel_name,write_function=lambda state: self._write_relay(internal_address,state)) new_channel.set_attribute('34970_type','actuator') self._add_bay_number(new_channel,self.bay,channel_num) self._add_channel(new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return new_channel
def _close(self,internal_address): self.get_interface().write("ROUTe:CLOSe (@{})".format(internal_address)) def _open(self,internal_address): self.get_interface().write("ROUTe:OPEN (@{})".format(internal_address)) def _write_relay(self,internal_address,state): '''boolean True closes relay channel_name, boolean False opens relay channel_name''' if state: self._close(internal_address) else: self._open(internal_address) def open_all_relays(self): #NB #CB fixed to use self.bay intead of hard coded base address = 200 base = self.bay*100 for internal_address in range(base+1, base+21): self._open(internal_address)
class agilent_34970a_actuator(agilent_3497xa_actuator): pass class agilent_34972a_actuator(agilent_3497xa_actuator): pass
[docs]class agilent_3497xa_dig_out8(a3497xa_instrument): '''agilent_a34970a_dig_out8 8 bit digital output of the 34907A plugin module each multifunction module has 2 8 bit digital ports or 1 16 bit port each 8 bit port may be input or output but not both.''' def __init__(self,interface_visa,bay,ch): '''interface_visa bay is 34970 plugin bay 1-3. ch is digital bank 1-2.''' self._base_name = 'agilent_3497xa_dig_out8' self.bay = bay self.plugin_type = "34907A" a3497xa_instrument.__init__(self,'34970a_digital bay: {} @ {} '.format(bay,interface_visa), automatic_monitor=False) self.add_interface_visa(interface_visa) self.channel_number = ch self.internal_address = bay*100+ch self.data = 0 self._defined_bit_mask = 0
[docs] def add_channel(self,channel_name,start=0,size=8): '''add channel by channel_name, shifted left by start bits and masked to size bits. ie to create a 3 bit digital channel on bits 1,2,3 add_channel("channel_name",1,3)''' mask = pow(2,size)-1 << start if mask & self._defined_bit_mask: raise Exception("{} {}: bit defined in multiple channels. Prev Mask: {}, Channel Mask: {} ".format(self.get_name(), channel_name, self._defined_bit_mask, mask)) self._defined_bit_mask |= mask if (start+size) > 8 or start < 0 or size < 1: raise Exception("{}: only 8 bits allowed".format(self.get_name())) new_channel = integer_channel(channel_name,size=size,write_function=lambda value: self._write_bits(start,size,value)) new_channel.set_attribute('34970_type','digital_output') self._add_bay_number(new_channel,self.bay,self.channel_number) self._add_channel(new_channel) new_channel.write(0) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return new_channel
def _write_bits(self,start,size,value): '''Write named channel to value. Value is an integer which counts by "1". The value is automatically truncated and shifted according to the location information provided to add_channel(). The remainder of the digital word not included in the channel remains unchanged.''' #construct mask mask = pow(2,size)-1 << start self.data = self.data & ~mask self.data |= (value << start) & mask self.get_interface().write("SOURce:DIGital:DATA {},(@{})".format(self.data,self.internal_address))
class agilent_34970a_dig_out8(agilent_3497xa_dig_out8): pass class agilent_34972a_dig_out8(agilent_3497xa_dig_out8): pass
[docs]class agilent_3497xa_dig_in8(a3497xa_instrument): '''agilent_a34970a_dig_in8 8 bit digital input of the 34907A plugin module each multifunction module has 2 8 bit digital ports or 1 16 bit port each 8 bit port may be input or output but not both''' def __init__(self,interface_visa,bay,ch): '''interface_visa bay is 34970 plugin bay 1-3. ch is digital bank 1-2.''' self._base_name = 'agilent_3497xa_dig_in8' self.bay = bay self.plugin_type = "34907A" self.channel_number = ch self.internal_address = self.bay*100 + self.channel_number a3497xa_instrument.__init__(self,'34970a_digital bay: {},{} @ {}'.format(self.bay,self.channel_number,interface_visa), automatic_monitor=False) self.add_interface_visa(interface_visa) self.get_interface().write('CONFigure:DIGital:BYTE (@{})'.format(self.internal_address))
[docs] def add_channel(self,channel_name,start=0,size=8): '''Add channel by name, shifted left by start bits and masked to size bits. ie to create a 3 bit digital channel on bits 1,2,3 add_channel("channel_name",1,3)''' if (start+size) > 8: raise Exception("{}: only 8 bits allowed".format(self.get_name())) conversion_function = lambda data: self._read_bits(start,size,data) read_function = lambda: self.read_apply_function(self.internal_address, conversion_function) new_channel = integer_channel(channel_name,size=size,read_function=read_function) new_channel.set_delegator(self) new_channel.set_attribute('34970_type','digital_input') self._add_bay_number(new_channel,self.bay,self.channel_number) self._add_channel(new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return new_channel
def _read_bits(self,start,size,data): '''Return the measured value for the named channel. Value is shifted right to count by "1" independent of the actual location of the channel within the physical byte.''' mask = pow(2,size)-1 << start data = (int(float(data)) & mask) >> start #data string from instrument is f.p. (ex '+2.55E+02') return data
class agilent_34970a_dig_in8(agilent_3497xa_dig_in8): pass class agilent_34972a_dig_in8(agilent_3497xa_dig_in8): pass
[docs]class agilent_3497xa_totalizer(a3497xa_instrument): '''Implement this if you need it 26-bit totalizer on physical channel s03 of the 34907A plugin module''' #self.plugin_type = "34907A" pass
class agilent_34970a_totalizer(agilent_3497xa_totalizer): pass class agilent_34972a_totalizer(agilent_3497xa_totalizer): pass
[docs]class agilent_n3301(scpi_instrument): '''Agilent N3301 Electronic Load with two channels This is a minimal class to interface with an Agilent N3301 electronic load. Only immediate constant current mode is supported, which means you can only control setting a constant current load and the new setpoint takes effect right away.''' def __init__(self,interface_visa): '''Constructor takes visa GPIB address or interface object (visa,rl1009, rs232) as parameter. Ex: "GPIB0::3"''' self._base_name = 'agilent_n3301' instrument.__init__(self,'n3300: @ {}'.format(interface_visa) ) self.add_interface_visa(interface_visa,timeout=5) #Reset the instrument to put it in a known state, turn #ON all the loads, and set them to zero. self.get_interface().write("*RST") def __del__(self): '''Reset the instrument to quickly set all loads to zero. (Draw no power)''' self.get_interface().write("*RST") self.get_interface().close()
[docs] def add_channel(self,channel_name,channel_num, add_sense_channel=True): '''add current force writable channel. Optionally add current readback _isense channel''' self.add_channel_current(channel_name,channel_num) if add_sense_channel: self.add_channel_isense(channel_name + "_isense",channel_num)
def add_channel_current(self,channel_name,channel_num): new_channel = channel(channel_name,write_function=lambda current: self._write_current(channel_num,current) ) self._add_channel(new_channel) def add_channel_isense(self,channel_name,channel_num): new_channel = channel(channel_name,read_function=lambda: self._read_isense(channel_num) ) self._add_channel(new_channel) def _write_current(self,channel_num,current): self.get_interface().write("INSTRUMENT {}".format(channel_num)) self.get_interface().write("CURRENT {}".format(current)) def _read_isense(self,channel_num): self.get_interface().write("INSTRUMENT {}".format(channel_num) ) self.get_interface().write("MEASURE:CURRENT?") return float( self.get_interface().read() )
[docs]class agilent_e36xxa(scpi_instrument): '''Generic base class for Agilent programmable DC power supply''' def add_channel_voltage(self,channel_name,num): voltage_channel = channel(channel_name,write_function=lambda voltage: self.set_voltage(num,voltage)) voltage_channel.set_write_delay(self._default_write_delay) return self._add_channel(voltage_channel) def add_channel_current(self,channel_name,num): current_channel = channel(channel_name,write_function=lambda current: self.set_current(num,current)) current_channel.set_write_delay(self._default_write_delay) return self._add_channel(current_channel) def add_channel_vsense(self,channel_name,num): vsense_channel = channel(channel_name,read_function=lambda: self.read_vsense(num)) return self._add_channel(vsense_channel) def add_channel_isense(self,channel_name,num): isense_channel = channel(channel_name,read_function=lambda: self.read_isense(num)) return self._add_channel(isense_channel) def set_voltage(self,num,voltage): self.get_interface().write("INSTrument:SELect " + num ) self.get_interface().write("VOLTage " + str(voltage)) time.sleep(0.2) def set_current(self,num,current): self.get_interface().write("INSTrument:SELect " + num ) self.get_interface().write("CURRent " + str(current)) time.sleep(0.2)
[docs] def read_vsense(self,num): '''Query the instrument and return float representing actual measured terminal voltage.''' self.get_interface().write("\n") # Clear out instrument's input buffer time.sleep(0.2) self.get_interface().write(":INSTrument:SELect " + num ) time.sleep(0.2) return float(self.get_interface().ask(":MEASure:VOLTage?"))
[docs] def read_isense(self,num): '''Query the instrument and return float representing actual measured terminal current.''' self.get_interface().write("\n") # Clear out instrument's input buffer time.sleep(0.2) self.get_interface().write(":INSTrument:SELect " + num ) time.sleep(0.2) return float(self.get_interface().ask(":MEASure:CURRent?"))
def set_ilim(self,channel_name,ilim): raise Exception('removed, write to the appropriate channel instead') def enable_output(self,state): self.get_interface().write("\n") # Clear out instrument's input buffer time.sleep(0.2) if state: self.get_interface().write(":OUTput:STATe ON") else: self.get_interface().write(":OUTput:STATe OFF") def output_enabled(self): return self.get_interface().ask("OUTput:STATe?") def _set_remote_mode(self,remote=True): '''Required for RS-232 control. Not allowed for GPIB control''' self.get_interface().write("\n") # Clear out instrument's input buffer time.sleep(0.2) if remote: self.get_interface().write(":SYSTem:REMote") else: self.get_interface().write(":SYSTem:LOCal")
[docs]class agilent_e3631a(agilent_e36xxa): '''Triple-channel programmable DC power supply''' def __init__(self,interface_visa): self._base_name = 'agilent_e3631a' self.name = "{} @ {}".format(self._base_name,interface_visa) instrument.__init__(self,self.name) self.add_interface_visa(interface_visa) if isinstance(self.get_interface(), lab_interfaces.interface_visa_serial): self._set_remote_mode() time.sleep(0.05) self._default_write_delay = 0.5 self.get_interface().write("*RST") # track function was being left enabled in some cases time.sleep(0.05) #initialize to instrument on, all voltages 0 self.get_interface().write("APPLy P6V, 0.0, 0.0") self.get_interface().write("APPLy P25V, 0.0, 0.0") self.get_interface().write("APPLy N25V, 0.0, 0.0") time.sleep(0.05) self.enable_output(True)
[docs] def add_channel(self,channel_name,num,ilim=1,add_sense_channels=True): '''Register a named channel with the instrument. channel_name is a user-supplied string num is "P6V", "P25V", "N25V", P50V has been removed, refer to virtual instrument optionally add _isense and _vsense readback channels''' num = num.upper() if num not in ['P6V','P25V','N25V']: raise Exception('Invalid channel number "{}"'.format(num)) v_chan = self.add_channel_voltage(channel_name,num) self.add_channel_current(channel_name + "_ilim",num) self.write(channel_name + "_ilim",ilim) if add_sense_channels: self.add_channel_vsense(channel_name + "_vsense",num) self.add_channel_isense(channel_name + "_isense",num) return v_chan
[docs]class agilent_e364xa(agilent_e36xxa): '''Dual-channel programmable DC power supply''' def __init__(self,interface_visa,resetoutputs=True): self._base_name = 'agilent_e3648a' self.name = "{} @ {}".format(self._base_name,interface_visa) instrument.__init__(self,self.name) self.add_interface_visa(interface_visa) if isinstance(self.get_interface(), lab_interfaces.interface_visa_serial): self._set_remote_mode() self._default_write_delay = 0.5 #initialize to instrument on, all voltages 0 if resetoutputs: #NB, original scpi was incorrect format self.get_interface().write("INSTrument:SELect OUT1") self.get_interface().write("APPLy 0.0, 0.0") self.get_interface().write("INSTrument:SELect OUT2") self.get_interface().write("APPLy 0.0, 0.0") self.enable_output(True)
[docs] def add_channel(self,channel_name,num,ilim=1,add_extended_channels=True): '''Register a named channel with the instrument. channel_name is a user-supplied string num must be either "OUT1" or "OUT2" optionally add _ilim, _isense and _vsense channels''' num = num.upper() if num not in ['OUT1','OUT2']: raise Exception('Invalid channel number "{}"'.format(num)) self.add_channel_voltage(channel_name,num) if add_extended_channels: self.add_channel_current(channel_name + "_ilim",num) self.write(channel_name + "_ilim",ilim) self.add_channel_vsense(channel_name + "_vsense",num) self.add_channel_isense(channel_name + "_isense",num) else: self.set_current(num,ilim)
def set_ovp_voltage(self,voltage,num): #NB self.select_output(num) self.get_interface().write('VOLT:PROT {}'.format(voltage)) self.get_interface().write('VOLT:PROT:STAT ON') def select_output(self,num): #NB num = num.upper() if num not in ['OUT1','OUT2']: raise Exception('Invalid channel number "{}"'.format(num)) self.get_interface().write('INSTrument:SELect {}'.format(num))
class agilent_e3648a(agilent_e364xa): #NB pass class agilent_e3649a(agilent_e364xa): #NB pass
[docs]class agilent_34401a(scpi_instrument): '''single channel agilent_34401a meter defaults to dc voltage, note this instrument currently does not support using multiple measurement types at the same time''' def __init__(self,interface_visa): '''interface_visa''' self._base_name = 'agilent_34401a' scpi_instrument.__init__(self,"a34401 @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) if isinstance(self.get_interface(), lab_interfaces.interface_visa_serial): self._set_remote_mode() self.config_dc_voltage()
[docs] def config_dc_voltage(self, NPLC=1, range="AUTO", BW=20): '''Set meter to measure DC volts. Optionally set number of powerline cycles for integration to [.02,.2,1,10,100] and set range to [0.1, 1, 10, 100, 1000]''' #DJS Todo: Move this stuff to channel wrappers like 34970 if NPLC not in [.02,.2,1,10,100]: raise Exception("Error: Not a valid NPLC setting, valid settings are 0.02, 0.2, 1, 10, 100") if BW not in [3, 20, 200]: raise Exception("Error: Not a valid BW setting, valid settings are 3, 20, 200") self.get_interface().write("FUNCtion \"VOLT:DC\"") #range is optional string value that is the manual range the meter should operate in. #valid values are in volts: [0.01, 0.1, 1, 3] self.get_interface().write("CONFigure:VOLTage:DC " + str(range)) self.get_interface().write("VOLTage:DC:NPLC " + str(NPLC)) self.get_interface().write("SENSe:DETector:BANDwidth " + str(BW)) self.get_interface().write("INPut:IMPedance:AUTO ON")
[docs] def config_dc_current(self, NPLC=1, range=None, BW=20): '''Configure meter for DC current measurement NPLC is an optional number of integration powerline cycles Valid values are: [.02,.2,1,10,100] range is optional string value that is the manual range the meter should operate in. Valid values are in amps: [0.01, 0.1, 1, 3]''' if NPLC not in [.02,.2,1,10,100]: raise Exception("Error: Not a valid NPLC setting, valid settings are 0.02, 0.2, 1, 10, 100") self.get_interface().write("FUNCtion \"CURRent:DC\"") if (range is not None): self.get_interface().write("CONFigure:CURRent:DC " + str(range)) self.get_interface().write("CURRent:DC:NPLC " + str(NPLC)) self.get_interface().write("SENSe:DETector:BANDwidth " + str(BW))
[docs] def config_ac_voltage(self): '''Configure meter for AC voltage measurement''' self.get_interface().write("INPut:IMPedance:AUTO ON") self.get_interface().write("FUNCtion \"VOLT:AC\"")
[docs] def config_ac_current(self): '''Configure meter for AC current measurement''' self.get_interface().write("FUNCtion \"CURRent:AC\"")
[docs] def add_channel(self,channel_name): '''Add named channel to instrument without configuring measurement type.''' meter_channel = channel(channel_name,read_function=self.read_meter) return self._add_channel(meter_channel)
[docs] def read_meter(self): '''Return float representing meter measurement. Units are V,A,Ohm, etc depending on meter configuration.''' return float(self.get_interface().ask("READ?"))
def _set_remote_mode(self,remote=True): '''Required for RS-232 control. Not allowed for GPIB control''' if remote: self.get_interface().write("SYSTem:REMote") else: self.get_interface().write("SYSTem:LOCal")
[docs]class agilent_34461A(agilent_34401a): '''LXI, histogram, Truevolt 34401 modernized replacement 6.5 digit DMM''' pass
[docs]class agilent_e53181a(scpi_instrument): '''Agilent e53181a frequency counter single channel, only uses channel 1 (front) you may need to set an expected value for autotriggering not recommended below 20hz defaults to 1Meg input R, 10x attenuation''' def __init__(self,interface_visa): self._base_name = 'agilent_e53181a' instrument.__init__(self,"agilent_e53181a @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.config_expect(1e6) self.get_interface().write("*CLS") self.get_interface().write("*RST") self.config_input_attenuation_1x() self.config_input_impedance_1Meg()
[docs] def config_input_attenuation_1x(self): '''set input attenuator to 1x''' self.get_interface().write(":INPut1:ATTenuation 1")
[docs] def config_input_attenuation_10x(self): '''set input attenuator to 10x (divide by 10)''' self.get_interface().write(":INPut1:ATTenuation 10")
[docs] def config_input_impedance_50(self): '''set input impedance to 50 Ohm''' self.get_interface().write(":INPut1:IMPedance 50")
[docs] def config_input_impedance_1Meg(self): '''set input impedance to 1 MegOhm''' self.get_interface().write(":INPut1:IMPedance 1e6")
[docs] def config_expect(self,expected_frequency): '''specify expected frequency to help with counting very low frequencies.''' t = 1000 * 1/float(expected_frequency) if t > 30: self.get_interface().timeout = int(t) else: self.get_interface().timeout = 30 self.expect = expected_frequency
[docs] def add_channel(self,channel_name): '''Add named channels to instrument.''' self.add_channel_dutycycle(channel_name + "_dutycycle") return self.add_channel_freq(channel_name)
[docs] def add_channel_freq(self,channel_name): '''Add named frequency channel to instrument''' freq_channel = channel(channel_name,read_function=self.read_frequency) return self._add_channel(freq_channel)
[docs] def add_channel_dutycycle(self,channel_name): '''Add named dutycycle channel to instrument''' dutycycle_channel = channel(channel_name,read_function=self.read_dutycycle) return self._add_channel(dutycycle_channel)
[docs] def read_frequency(self,channel_name): '''Return float representing measured frequency of named channel.''' txt = ":MEASure:FREQuency? %3.0f, 1, (@1)" % self.expect while True: try: return(self.get_interface().ask(txt)) break except Exception as e: print "Waiting on frequency meter" print e
[docs] def read_dutycycle(self,channel_name): '''Return float representing measured duty cycle of named channel.''' txt = ":MEASure:DCYCle? %3.0f, 1, (@1)" % self.expect while True: try: return(self.get_interface().ask(txt)) break except Exception as e: print "Waiting on frequency meter" print e
[docs]class bus_pirate_gpio(instrument): '''bus pirate board as a gpio driver, uses binary mode to control and read up to 5 bits.''' def __init__(self,interface_raw_serial): '''creates a bus_pirate_gpio object, serial_port can be a pyserial object or a string''' self._base_name = 'bus_pirate_gpio' instrument.__init__(self,"BUS PIRATE {}".format(interface_raw_serial)) self.add_interface_raw_serial(interface_raw_serial) self.ser = interface_raw_serial self._config_bus_pirate_binary() self.output_data = 0 self.output_enable_mask = 0b00011111 # start as inputs, inputs are 1 self.pin_names = ["AUX","MISO","CS","MOSI","CLK"] self.pin_mask = {"AUX":16,"MOSI":8,"CLK":4,"MISO":2,"CS":1} def _config_bus_pirate_binary(self): self.ser.write('\n'*10) #exit any menus self.ser.write('#') #reset self.ser.read(self.ser.inWaiting()) #get into binary mode resp = '' tries = 0 while (resp != 'BBIO1'): tries += 1 if (tries > 20): raise Exception('Buspirate failed to enter binary mode after 20 attempts') #print 'Buspirate entering binary mode attempt {}: '.format(tries), self.ser.write('\x00') #enter binary mode time.sleep(0.02) resp = self.ser.read(self.ser.inWaiting()) def _bus_pirate_set_as_outputs(self,pin_names): #both writes and reads, its how the bus pirate works self.ser.read(self.ser.inWaiting()) for pin_name in pin_names: self.output_enable_mask &= ~self._get_mask(pin_name) byte = chr(0x40 | self.output_enable_mask) self.ser.write(byte) # write bits time.sleep(0.01) read_data = self.ser.read(self.ser.inWaiting()) return read_data def _bus_pirate_get_pin_state(self): return ord(self._bus_pirate_set_as_outputs([])) def _bus_pirate_write_outputs(self): byte = chr(0xC0 | self.output_data) byte = chr(0xE0 | self.output_data) self.ser.write(byte) # write bits time.sleep(0.01) resp = self.ser.read(self.ser.inWaiting()) #print bin(ord(resp)) def _get_mask(self,pin_name): return self.pin_mask[pin_name]
[docs] def add_channel(self,channel_name,pin_names,output,value=0): '''add channel by channel_name, ie to create a 3 bit digital output channel on pins CLK,MISO,MOSI add_channel("channel_name",["CLK",MISO",MOSI"],output=1) as always the FIRST pin is the MSB''' if type(pin_names) == type(""): pin_names = [pin_names] #convert to a list if a string was given pin_names = [pin_name.upper() for pin_name in pin_names] for pin_name in pin_names: if pin_name not in self.pin_names: raise Exception("{}: not a valid pin name".format(pin_name)) if output: new_channel = channel(channel_name,write_function=lambda value: self._write_pins(pin_names,value)) new_channel.write(value) self._bus_pirate_set_as_outputs(pin_names) else: new_channel = channel(channel_name,read_function=lambda: self._read_pins(pin_names)) return self._add_channel(new_channel)
def _write_pins(self,pin_names,value): '''Write named channel to value. Value is an integer which counts by "1". The value is automatically truncated and shifted according to the location information provided to add_channel(). The remainder of the digital word not included in the channel remains unchanged.''' if value > pow(2,len(pin_names)): raise Exception('Buspirate {}: value {} too large'.format(pin_names,value)) for pin_name in reversed(pin_names): pin_mask = self._get_mask(pin_name) if value & 1 == 1: self.output_data |= pin_mask else: self.output_data &= ~pin_mask value >>= 1 self._bus_pirate_write_outputs() def _read_pins(self,pin_names): '''Return the forcing value for the named channel.''' data = 0 pin_data = self._bus_pirate_get_pin_state() for pin_name in pin_names: data <<= 1 if pin_data & self.pin_mask[pin_name]: data |= 1 return data
[docs]class bk8500(instrument): ''' The below license only applies to most of the code in the bk8500 instrument: Provides the interface to a 26 byte instrument along with utility functions. This is based on provided drivers, minor style changes were made and lab.py fucntions were added. The original license and documentation are included below. Open Source Initiative OSI - The MIT License:Licensing Tue, 2006-10-31 04:56 - nelson The MIT License Copyright (c) 2009 BK Precision Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This python module provides a functional interface to a B&K DC load through the DCLoad object. This object can also be used as a COM server by running this module as a script to register it. All the DCLoad object methods return strings. All units into and out of the DCLoad object's methods are in SI units. See the documentation file that came with this script. $RCSfile: dcload.py $ $Revision: 1.0 $ $Date: 2008/05/17 15:57:15 $ $Author: Don Peterson $ ''' def __init__(self, interface_raw_serial, address=0): self._base_name = 'bk8500' instrument.__init__(self,"BK8500 @ {}".format(interface_raw_serial)) self.add_interface_raw_serial(interface_raw_serial,timeout=1) self.sp = interface_raw_serial self.address = address self._set_constants() self.SetRemoteControl() self.SetRemoteSense(False) self.mode = 'Off' self.nl = '\n' #debug output? def _set_constants(self): #most of this is from the manufacturer's class self.debug = 0 # Set to 1 to see dumps of commands and responses self.length_packet = 26 # Number of bytes in a packet self.convert_current = 1e4 # Convert current in A to 0.1 mA self.convert_voltage = 1e3 # Convert voltage in V to mV self.convert_power = 1e3 # Convert power in W to mW self.convert_resistance = 1e3 # Convert resistance in ohm to mohm self.to_ms = 1000 # Converts seconds to ms self.retries = 5 # Number of settings storage registers self.lowest_register = 1 self.highest_register = 25 # Values for setting modes of CC, CV, CW, or CR self.modes = {"cc":0, "cv":1, "cw":2, "cr":3} self.out = sys.stdout.write
[docs] def add_channel(self,channel_name,add_extended_channels=True): '''Sortcut function adds CC force channel. if add_extended_channels, additionally add _isense,_vsense,_psense,_mode readback channels Add CV,CW,CR,remote_sense channels separately if you need them.''' ch = self.add_channel_current(channel_name) if add_extended_channels: self.add_channel_isense(channel_name + '_isense') self.add_channel_vsense(channel_name + '_vsense') self.add_channel_psense(channel_name + '_psense') self.add_channel_mode(channel_name + '_mode') return ch
[docs] def add_channel_current(self,channel_name): '''add single CC forcing channel and force zero current''' new_channel = channel(channel_name,write_function=self._SetCCCurrent) self._add_channel(new_channel) new_channel.set_write_delay(0.4) new_channel.write(0) # self.TurnLoadOn() Is now handled in self._SetCCCurrent return new_channel
[docs] def add_channel_isense(self,channel_name): '''add single current readback channel''' new_channel = channel(channel_name,read_function=lambda: self._read_isense(channel_name)) return self._add_channel(new_channel)
[docs] def add_channel_vsense(self,channel_name): '''add single voltage readback channel''' new_channel = channel(channel_name,read_function=lambda: self._read_vsense(channel_name)) return self._add_channel(new_channel)
[docs] def add_channel_psense(self,channel_name): '''read back computed power dissipated in load''' new_channel = channel(channel_name,read_function=lambda: self._read_psense(channel_name)) return self._add_channel(new_channel)
[docs] def add_channel_mode(self, channel_name): '''read back operating mode (Off, Constant Current, Constant Voltage, Constant Power, Constant Resistance)''' new_channel = channel(channel_name,read_function=lambda: self.mode) return self._add_channel(new_channel)
def _read_vsense(self,channel_name): '''Return measured voltage float.''' return self.GetInputValues()['voltage'] def _read_isense(self,channel_name): '''Return measured current float.''' return self.GetInputValues()['current'] def _read_psense(self,channel_name): '''Return measured power float.''' return self.GetInputValues()['power']
[docs] def add_channel_remote_sense(self,channel_name): '''Enable/disable remote voltage sense through rear panel connectors''' new_channel = integer_channel(channel_name, size=1, write_function=self.SetRemoteSense) new_channel.write(False) return self._add_channel(new_channel)
[docs] def add_channel_voltage(self,channel_name): '''add single CV forcing channel''' new_channel = channel(channel_name,write_function=self._SetCVVoltage) return self._add_channel(new_channel)
[docs] def add_channel_power(self,channel_name): '''add single CW forcing channel''' new_channel = channel(channel_name,write_function=self._SetCWPower) return self._add_channel(new_channel)
[docs] def add_channel_resistance(self,channel_name): '''add single CR forcing channel''' new_channel = channel(channel_name,write_function=self._SetCRResistance) return self._add_channel(new_channel)
def _SetCCCurrent(self, current): self.SetMode("cc") self.mode = 'Constant Current' if current == 0: self.TurnLoadOff() # Don't trust setting of 0 to not drop out load. else: self.SetRemoteControl() #just in case somebody pushed front panel "Local" button self.TurnLoadOn() # Because it could be off self.SetCCCurrent(current) def _SetCVVoltage(self, voltage): self.SetMode("cv") self.mode = 'Constant Voltage' if voltage is None: self.TurnLoadOff() else: self.SetRemoteControl() #just in case somebody pushed front panel "Local" button self.TurnLoadOn() # Because it could be off self.SetCVVoltage(voltage) def _SetCWPower(self, power): self.SetMode("cw") self.mode = 'Constant Power' if power == 0: self.TurnLoadOff() else: self.SetRemoteControl() #just in case somebody pushed front panel "Local" button self.TurnLoadOn() # Because it could be off self.SetCWPower(power) def _SetCRResistance(self, resistance): self.SetMode("cr") self.mode = 'Constant Resistance' if resistance is None: self.TurnLoadOff() else: self.SetRemoteControl() #just in case somebody pushed front panel "Local" button self.TurnLoadOn() # Because it could be off self.SetCRResistance(resistance) # below is mostly code from manufacturer
[docs] def DumpCommand(self, bytes): '''Print out the contents of a 26 byte command. Example: aa .. 20 01 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. cb ''' assert(len(bytes) == self.length_packet) header = " "*3 self.out(header) for i in xrange(self.length_packet): if i % 10 == 0 and i != 0: self.out(self.nl + header) if i % 5 == 0: self.out(" ") s = "%02x" % ord(bytes[i]) if s == "00": # Use the decimal point character if you see an # unattractive printout on your machine. #s = "."*2 # The following alternate character looks nicer # in a console window on Windows. s = chr(250)*2 self.out(s) self.out(self.nl)
[docs] def CommandProperlyFormed(self, cmd): '''Return 1 if a command is properly formed; otherwise, return 0. ''' commands = ( 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x12 ) # Must be proper length if len(cmd) != self.length_packet: self.out("Command length = " + str(len(cmd)) + "-- should be " + \ str(self.length_packet) + self.nl) return 0 # First character must be 0xaa if ord(cmd[0]) != 0xaa: self.out("First byte should be 0xaa" + self.nl) return 0 # Second character (address) must not be 0xff if ord(cmd[1]) == 0xff: self.out("Second byte cannot be 0xff" + self.nl) return 0 # Third character must be valid command byte3 = "%02X" % ord(cmd[2]) if ord(cmd[2]) not in commands: self.out("Third byte not a valid command: %s\n" % byte3) return 0 # Calculate checksum and validate it checksum = self.CalculateChecksum(cmd) if checksum != ord(cmd[-1]): self.out("Incorrect checksum" + self.nl) return 0 return 1
[docs] def CalculateChecksum(self, cmd): '''Return the sum of the bytes in cmd modulo 256. ''' assert((len(cmd) == self.length_packet - 1) or (len(cmd) == self.length_packet)) checksum = 0 for i in xrange(self.length_packet - 1): checksum += ord(cmd[i]) checksum %= 256 return checksum
def StartCommand(self, byte): return chr(0xaa) + chr(self.address) + chr(byte)
[docs] def SendCommand(self, command): '''Sends the command to the serial stream and returns the 26 byte response. ''' assert(len(command) == self.length_packet) for attempt in range(self.retries): self.sp.write(command) response = self.sp.read(self.length_packet) if len(response) == self.length_packet: success = True break else: success = False self.sp.read(self.sp.inWaiting()) if not success: print "BK8500 bombed because these don't match:" print "command: ", for byte in command: print '0x' + str(ord(byte)), print "\nresponse: ", for byte in response: print '0x' + str(ord(byte)), print print print assert(len(response) == self.length_packet) return response
[docs] def ResponseStatus(self, response): '''Return a message string about what the response meant. The empty string means the response was OK. ''' responses = { 0x90 : "Wrong checksum", 0xA0 : "Incorrect parameter value", 0xB0 : "Command cannot be carried out", 0xC0 : "Invalid command", 0x80 : "", } assert(len(response) == self.length_packet) assert(ord(response[2]) == 0x12) return responses[ord(response[3])]
[docs] def CodeInteger(self, value, num_bytes=4): '''Construct a little endian string for the indicated value. Two and 4 byte integers are the only ones allowed. ''' assert(num_bytes == 1 or num_bytes == 2 or num_bytes == 4) value = int(value) # Make sure it's an integer s = chr(value & 0xff) if num_bytes >= 2: s += chr((value & (0xff << 8)) >> 8) if num_bytes == 4: s += chr((value & (0xff << 16)) >> 16) s += chr((value & (0xff << 24)) >> 24) assert(len(s) == 4) return s
[docs] def DecodeInteger(self, str): '''Construct an integer from the little endian string. 1, 2, and 4 byte strings are the only ones allowed. ''' assert(len(str) == 1 or len(str) == 2 or len(str) == 4) n = ord(str[0]) if len(str) >= 2: n += (ord(str[1]) << 8) if len(str) == 4: n += (ord(str[2]) << 16) n += (ord(str[3]) << 24) return n
[docs] def GetReserved(self, num_used): '''Construct a string of nul characters of such length to pad a command to one less than the packet size (leaves room for the checksum byte. ''' num = self.length_packet - num_used - 1 assert(num > 0) return chr(0)*num
[docs] def PrintCommandAndResponse(self, cmd, response, cmd_name): '''Print the command and its response if debugging is on. ''' assert(cmd_name) if self.debug: self.out(cmd_name + " command:" + self.nl) self.DumpCommand(cmd) self.out(cmd_name + " response:" + self.nl) self.DumpCommand(response)
[docs] def GetCommand(self, command, value, num_bytes=4): '''Construct the command with an integer value of 0, 1, 2, or 4 bytes. ''' cmd = self.StartCommand(command) if num_bytes > 0: r = num_bytes + 3 cmd += self.CodeInteger(value)[:num_bytes] + self.Reserved(r) else: cmd += self.Reserved(0) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) return cmd
[docs] def GetData(self, data, num_bytes=4): '''Extract the little endian integer from the data and return it. ''' assert(len(data) == self.length_packet) if num_bytes == 1: return ord(data[3]) elif num_bytes == 2: return self.DecodeInteger(data[3:5]) elif num_bytes == 4: return self.DecodeInteger(data[3:7]) else: raise Exception("Bad number of bytes: %d" % num_bytes)
def Reserved(self, num_used): assert(num_used >= 3 and num_used < self.length_packet - 1) return chr(0)*(self.length_packet - num_used - 1)
[docs] def SendIntegerToLoad(self, byte, value, msg, num_bytes=4): '''Send the indicated command along with value encoded as an integer of the specified size. Return the instrument's response status. ''' cmd = self.GetCommand(byte, value, num_bytes) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, msg) return self.ResponseStatus(response)
[docs] def GetIntegerFromLoad(self, cmd_byte, msg, num_bytes=4): '''Construct a command from the byte in cmd_byte, send it, get the response, then decode the response into an integer with the number of bytes in num_bytes. msg is the debugging string for the printout. Return the integer. ''' assert(num_bytes == 1 or num_bytes == 2 or num_bytes == 4) cmd = self.StartCommand(cmd_byte) cmd += self.Reserved(3) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, msg) return self.DecodeInteger(response[3:3 + num_bytes])
[docs] def TurnLoadOn(self): "Turns the load on" msg = "Turn load on" on = 1 return self.SendIntegerToLoad(0x21, on, msg, num_bytes=1)
[docs] def TurnLoadOff(self): "Turns the load off" msg = "Turn load off" off = 0 return self.SendIntegerToLoad(0x21, off, msg, num_bytes=1)
[docs] def SetRemoteControl(self): "Sets the load to remote control" msg = "Set remote control" remote = 1 return self.SendIntegerToLoad(0x20, remote, msg, num_bytes=1)
[docs] def SetLocalControl(self): "Sets the load to local control" msg = "Set local control" local = 0 return self.SendIntegerToLoad(0x20, local, msg, num_bytes=1)
[docs] def SetMaxCurrent(self, current): "Sets the maximum current the load will sink" msg = "Set max current" return self.SendIntegerToLoad(0x24, current*self.convert_current, msg, num_bytes=4)
[docs] def GetMaxCurrent(self): "Returns the maximum current the load will sink" msg = "Set max current" return self.GetIntegerFromLoad(0x25, msg, num_bytes=4)/float(self.convert_current)
[docs] def SetMaxVoltage(self, voltage): "Sets the maximum voltage the load will allow" msg = "Set max voltage" return self.SendIntegerToLoad(0x22, voltage*self.convert_voltage, msg, num_bytes=4)
[docs] def GetMaxVoltage(self): "Gets the maximum voltage the load will allow" msg = "Get max voltage" return self.GetIntegerFromLoad(0x23, msg, num_bytes=4)/float(self.convert_voltage)
[docs] def SetMaxPower(self, power): "Sets the maximum power the load will allow" msg = "Set max power" return self.SendIntegerToLoad(0x26, power*self.convert_power, msg, num_bytes=4)
[docs] def GetMaxPower(self): "Gets the maximum power the load will allow" msg = "Get max power" return self.GetIntegerFromLoad(0x27, msg, num_bytes=4)/float(self.convert_power)
[docs] def SetMode(self, mode): "Sets the mode (constant current, constant voltage, etc." if mode.lower() not in self.modes: raise Exception("Unknown mode") msg = "Set mode" return self.SendIntegerToLoad(0x28, self.modes[mode.lower()], msg, num_bytes=1)
[docs] def GetMode(self): "Gets the mode (constant current, constant voltage, etc." msg = "Get mode" mode = self.GetIntegerFromLoad(0x29, msg, num_bytes=1) modes_inv = {0:"cc", 1:"cv", 2:"cw", 3:"cr"} return modes_inv[mode]
[docs] def SetCCCurrent(self, current): "Sets the constant current mode's current level" msg = "Set CC current" return self.SendIntegerToLoad(0x2A, current*self.convert_current, msg, num_bytes=4)
[docs] def GetCCCurrent(self): "Gets the constant current mode's current level" msg = "Get CC current" return self.GetIntegerFromLoad(0x2B, msg, num_bytes=4)/float(self.convert_current)
[docs] def SetCVVoltage(self, voltage): "Sets the constant voltage mode's voltage level" msg = "Set CV voltage" return self.SendIntegerToLoad(0x2C, voltage*self.convert_voltage, msg, num_bytes=4)
[docs] def GetCVVoltage(self): "Gets the constant voltage mode's voltage level" msg = "Get CV voltage" return self.GetIntegerFromLoad(0x2D, msg, num_bytes=4)/float(self.convert_voltage)
[docs] def SetCWPower(self, power): "Sets the constant power mode's power level" msg = "Set CW power" return self.SendIntegerToLoad(0x2E, power*self.convert_power, msg, num_bytes=4)
[docs] def GetCWPower(self): "Gets the constant power mode's power level" msg = "Get CW power" return self.GetIntegerFromLoad(0x2F, msg, num_bytes=4)/float(self.convert_power)
[docs] def SetCRResistance(self, resistance): "Sets the constant resistance mode's resistance level" msg = "Set CR resistance" return self.SendIntegerToLoad(0x30, resistance*self.convert_resistance, msg, num_bytes=4)
[docs] def GetCRResistance(self): "Gets the constant resistance mode's resistance level" msg = "Get CR resistance" return self.GetIntegerFromLoad(0x31, msg, num_bytes=4)/float(self.convert_resistance)
[docs] def SetTransient(self, mode, A, A_time_s, B, B_time_s, operation="continuous"): '''Sets up the transient operation mode. mode is one of "CC", "CV", "CW", or "CR". ''' if mode.lower() not in self.modes: raise Exception("Unknown mode") opcodes = {"cc":0x32, "cv":0x34, "cw":0x36, "cr":0x38} if mode.lower() == "cc": const = self.convert_current elif mode.lower() == "cv": const = self.convert_voltage elif mode.lower() == "cw": const = self.convert_power else: const = self.convert_resistance cmd = self.StartCommand(opcodes[mode.lower()]) cmd += self.CodeInteger(A*const, num_bytes=4) cmd += self.CodeInteger(A_time_s*self.to_ms, num_bytes=2) cmd += self.CodeInteger(B*const, num_bytes=4) cmd += self.CodeInteger(B_time_s*self.to_ms, num_bytes=2) transient_operations = {"continuous":0, "pulse":1, "toggled":2} cmd += self.CodeInteger(transient_operations[operation], num_bytes=1) cmd += self.Reserved(16) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, "Set %s transient" % mode) return self.ResponseStatus(response)
[docs] def GetTransient(self, mode): "Gets the transient mode settings" if mode.lower() not in self.modes: raise Exception("Unknown mode") opcodes = {"cc":0x33, "cv":0x35, "cw":0x37, "cr":0x39} cmd = self.StartCommand(opcodes[mode.lower()]) cmd += self.Reserved(3) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, "Get %s transient" % mode) A = self.DecodeInteger(response[3:7]) A_timer_ms = self.DecodeInteger(response[7:9]) B = self.DecodeInteger(response[9:13]) B_timer_ms = self.DecodeInteger(response[13:15]) operation = self.DecodeInteger(response[15]) time_const = 1e3 transient_operations_inv = {0:"continuous", 1:"pulse", 2:"toggled"} if mode.lower() == "cc": return str((A/float(self.convert_current), A_timer_ms/float(time_const), B/float(self.convert_current), B_timer_ms/float(time_const), transient_operations_inv[operation])) elif mode.lower() == "cv": return str((A/float(self.convert_voltage), A_timer_ms/float(time_const), B/float(self.convert_voltage), B_timer_ms/float(time_const), transient_operations_inv[operation])) elif mode.lower() == "cw": return str((A/float(self.convert_power), A_timer_ms/float(time_const), B/float(self.convert_power), B_timer_ms/float(time_const), transient_operations_inv[operation])) else: return str((A/float(self.convert_resistance), A_timer_ms/float(time_const), B/float(self.convert_resistance), B_timer_ms/float(time_const), transient_operations_inv[operation]))
[docs] def SetBatteryTestVoltage(self, min_voltage): "Sets the battery test voltage" msg = "Set battery test voltage" return self.SendIntegerToLoad(0x4E, min_voltage*self.convert_voltage, msg, num_bytes=4)
[docs] def GetBatteryTestVoltage(self): "Gets the battery test voltage" msg = "Get battery test voltage" return self.GetIntegerFromLoad(0x4F, msg, num_bytes=4)/float(self.convert_voltage)
[docs] def SetLoadOnTimer(self, time_in_s): "Sets the time in seconds that the load will be on" msg = "Set load on timer" return self.SendIntegerToLoad(0x50, time_in_s, msg, num_bytes=2)
[docs] def GetLoadOnTimer(self): "Gets the time in seconds that the load will be on" msg = "Get load on timer" return self.GetIntegerFromLoad(0x51, msg, num_bytes=2)
[docs] def SetLoadOnTimerState(self, enabled=0): "Enables or disables the load on timer state" msg = "Set load on timer state" return self.SendIntegerToLoad(0x50, enabled, msg, num_bytes=1)
[docs] def GetLoadOnTimerState(self): "Gets the load on timer state" msg = "Get load on timer" state = self.GetIntegerFromLoad(0x53, msg, num_bytes=1) if state == 0: return "disabled" else: return "enabled"
[docs] def SetCommunicationAddress(self, address=0): '''Sets the communication address. Note: this feature is not currently supported. The communication address should always be set to 0. ''' msg = "Set communication address" return self.SendIntegerToLoad(0x54, address, msg, num_bytes=1)
[docs] def EnableLocalControl(self): "Enable local control (i.e., key presses work) of the load" msg = "Enable local control" enabled = 1 return self.SendIntegerToLoad(0x55, enabled, msg, num_bytes=1)
[docs] def DisableLocalControl(self): "Disable local control of the load" msg = "Disable local control" disabled = 0 return self.SendIntegerToLoad(0x55, disabled, msg, num_bytes=1)
[docs] def SetRemoteSense(self, enabled=0): "Enable or disable remote sensing" msg = "Set remote sense" return self.SendIntegerToLoad(0x56, enabled, msg, num_bytes=1)
[docs] def GetRemoteSense(self): "Get the state of remote sensing" msg = "Get remote sense" return self.GetIntegerFromLoad(0x57, msg, num_bytes=1)
[docs] def SetTriggerSource(self, source="immediate"): '''Set how the instrument will be triggered. "immediate" means triggered from the front panel. "external" means triggered by a TTL signal on the rear panel. "bus" means a software trigger (see TriggerLoad()). ''' trigger = {"immediate":0, "external":1, "bus":2} if source not in trigger: raise Exception("Trigger type %s not recognized" % source) msg = "Set trigger type" return self.SendIntegerToLoad(0x54, trigger[source], msg, num_bytes=1)
[docs] def GetTriggerSource(self): "Get how the instrument will be triggered" msg = "Get trigger source" t = self.GetIntegerFromLoad(0x59, msg, num_bytes=1) trigger_inv = {0:"immediate", 1:"external", 2:"bus"} return trigger_inv[t]
[docs] def TriggerLoad(self): '''Provide a software trigger. This is only of use when the trigger mode is set to "bus". ''' cmd = self.StartCommand(0x5A) cmd += self.Reserved(3) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, "Trigger load (trigger = bus)") return self.ResponseStatus(response)
[docs] def SaveSettings(self, register=0): "Save instrument settings to a register" assert(self.lowest_register <= register <= self.highest_register) msg = "Save to register %d" % register return self.SendIntegerToLoad(0x5B, register, msg, num_bytes=1)
[docs] def RecallSettings(self, register=0): "Restore instrument settings from a register" assert(self.lowest_register <= register <= self.highest_register) cmd = self.GetCommand(0x5C, register, num_bytes=1) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, "Recall register %d" % register) return self.ResponseStatus(response)
[docs] def SetFunction(self, function="fixed"): '''Set the function (type of operation) of the load. function is one of "fixed", "short", "transient", or "battery". Note "list" is intentionally left out for now. ''' msg = "Set function to %s" % function functions = {"fixed":0, "short":1, "transient":2, "battery":4} return self.SendIntegerToLoad(0x5D, functions[function], msg, num_bytes=1)
[docs] def GetFunction(self): "Get the function (type of operation) of the load" msg = "Get function" fn = self.GetIntegerFromLoad(0x5E, msg, num_bytes=1) functions_inv = {0:"fixed", 1:"short", 2:"transient", 4:"battery"} return functions_inv[fn]
[docs] def GetInputValues(self): '''Returns voltage in V, current in A, and power in W, op_state byte, and demand_state byte. ''' cmd = self.StartCommand(0x5F) cmd += self.Reserved(3) cmd += chr(self.CalculateChecksum(cmd)) assert(self.CommandProperlyFormed(cmd)) response = self.SendCommand(cmd) self.PrintCommandAndResponse(cmd, response, "Get input values") values = {} values["voltage"] = self.DecodeInteger(response[3:7])/float(self.convert_voltage) values["current"] = self.DecodeInteger(response[7:11])/float(self.convert_current) values["power"] = self.DecodeInteger(response[11:15])/float(self.convert_power) return values
[docs]class hameg_4040(scpi_instrument): '''Hameg Lab Supply, model HMP 4040 Four channel lab supply with GPIB interface. This instrument works by selecting the desired output with one command then sending "source" or "measure" commands to that output to set or measure voltage and current. ''' def __init__(self,interface_visa): self._base_name = 'hameg_4040' instrument.__init__(self,'HMP4040 @ {}'.format(interface_visa) ) self.add_interface_visa(interface_visa) #Reset the instrument to all outputs on, all voltages zero. #turn on all channels with zero output voltage #and one amp current limits (why?) self.get_interface().write("*RST") time.sleep(1) #print self.get_interface().resync() self.retries = 0 #self.hameg_suck_time = 0.03 self.hameg_suck_time = .03 def __del__(self): '''turn OFF all channels''' for i in [1,2,3,4]: self.get_interface().write("INSTRUMENT:SELECT OUTPUT{}".format(i)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().write("OUTPUT:STATE OFF") time.sleep(0.25) # do no remove self.get_interface().close()
[docs] def set_retries(self, retries): '''attempt to be robust to communication interface problems''' self.retries = retries
[docs] def add_channel(self, channel_name, num, ilim = 1, delay = 0.5, add_extended_channels=True): '''add voltage forcing channel optionally add voltage force channel, current force channel "_ilim", enable "_enable", voltage sense "_vsense" and current sense "_isense" channel_name is channel name, ex: input_voltage num is channel number, allowed values are 1, 2, 3, 4 ilim is optional current limit for this output, defaults to 1 amp.''' voltage_channel = self.add_channel_voltage(channel_name, num) self.write_channel(channel_name,0) voltage_channel.set_write_delay(delay) if add_extended_channels: current_channel = self.add_channel_current(channel_name + "_ilim", num) enable_channel = self.add_channel_enable(channel_name + "_enable", num) self.write_channel(channel_name + "_ilim",ilim) self.write_channel(channel_name + "_enable",True) self.add_channel_vsense(channel_name + "_vsense", num) self.add_channel_isense(channel_name + "_isense", num) current_channel.set_write_delay(delay) enable_channel.set_write_delay(delay) else: self._write_current(num,ilim) self._write_enable(num,True) return voltage_channel
def add_channel_voltage(self, channel_name, num): new_channel = channel(channel_name, write_function=lambda voltage: self._write_voltage(num,voltage)) new_channel.set_attribute('hameg_number',num) new_channel.set_max_write_limit(32.05) new_channel.set_min_write_limit(0) return self._add_channel(new_channel) def add_channel_current(self, channel_name, num): new_channel = channel(channel_name, write_function=lambda current: self._write_current(num,current)) new_channel.set_attribute('hameg_number',num) new_channel.set_max_write_limit(10) new_channel.set_min_write_limit(0) return self._add_channel(new_channel) def add_channel_vsense(self, channel_name, num): new_channel = channel(channel_name, read_function=lambda: self._read_vsense(num)) new_channel.set_attribute('hameg_number',num) return self._add_channel(new_channel) def add_channel_isense(self, channel_name, num): new_channel = channel(channel_name, read_function=lambda: self._read_isense(num)) new_channel.set_attribute('hameg_number',num) return self._add_channel(new_channel) def add_channel_voltage_readback(self, channel_name, num): new_channel = channel(channel_name, read_function=lambda: self._read_voltage_readback(num)) new_channel.set_attribute('hameg_number',num) return self._add_channel(new_channel) def add_channel_ovp(self, channel_name, num): new_channel = channel(channel_name, write_function=lambda voltage: self._write_ovp(num, voltage)) new_channel.set_attribute('hameg_number', num) return self._add_channel(new_channel) def add_channel_ovp_status(self, channel_name, num): new_channel = integer_channel(channel_name, size=1, read_function=lambda: self._read_ovp_status(num)) new_channel.set_attribute('hameg_number', num) return self._add_channel(new_channel) def add_channel_fuse_status(self, channel_name, num): new_channel = integer_channel(channel_name, size=1, read_function= lambda: self._read_fuse_status(num)) new_channel.set_attribute('hameg_number', num) return self._add_channel(new_channel) def add_channel_enable(self, channel_name, num): new_channel = integer_channel(channel_name, size=1, write_function= lambda state: self._write_enable(num,state)) new_channel.set_attribute('hameg_number', num) return self._add_channel(new_channel) def add_channel_fuse_enable(self, channel_name, num, fuse_delay = 0): new_channel = integer_channel(channel_name, size=1, write_function= lambda state: self._write_fuse_enable(num, state, fuse_delay)) new_channel.set_attribute('hameg_number', num) new_channel.set_write_delay(0.5) return self._add_channel(new_channel) def add_channel_fuse_link(self, channel_name, num): new_channel = channel(channel_name, write_function= lambda link_list: self._write_fuse_links(num, link_list)) new_channel.set_attribute('hameg_number', num) new_channel.set_write_delay(0.5) return self._add_channel(new_channel) def add_channel_master_enable(self, channel_name): new_channel = integer_channel(channel_name, size=1, write_function=self._write_master_enable) new_channel.set_write_delay(0.5) return self._add_channel(new_channel) def _write_fuse_enable(self, num, state, fuse_delay): self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux if state: self.get_interface().write("FUSE:STATe ON") time.sleep(self.hameg_suck_time) else: self.get_interface().write("FUSE:STATe OFF") time.sleep(self.hameg_suck_time) self.get_interface().write("FUSE:DELay {}".format(fuse_delay)) time.sleep(self.hameg_suck_time) def _write_fuse_links(self, num, link_list): self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux link_string = "" for supply in link_list: self.get_interface().write("FUSE:LINK {}".format(supply)) time.sleep(self.hameg_suck_time) def _write_voltage(self,num,voltage): self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().write("SOURce:VOLTage {}".format(voltage)) time.sleep(self.hameg_suck_time) def _write_current(self,num,current): self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().write("SOURce:CURRent {}".format(current)) time.sleep(self.hameg_suck_time) def _read_voltage_readback(self,num): '''returns the voltage setting as known by the instrument''' retry = 0 while(retry <= self.retries): try: self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux data = float(self.get_interface().ask("SOURce:VOLTage?")) time.sleep(self.hameg_suck_time) return data except Exception as e: print e print "Resync {}: {}".format(self.get_name(),self.get_interface().resync()) retry += 1 raise e def _read_vsense(self,num): '''returns the voltage measured by the instrument''' retry = 0 while(retry <= self.retries): try: self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux data = float(self.get_interface().ask("MEASure:SCALar:VOLT:DC?")) time.sleep(self.hameg_suck_time) return data except Exception as e: print e print "Resync {}: {}".format(self.get_name(),self.get_interface().resync()) retry += 1 raise e def _read_isense(self,num): '''returns the current measured by the instrument''' retry = 0 while(retry <= self.retries): try: self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux data = float( self.get_interface().ask("MEASure:SCALar:CURRent:DC?")) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux return data except Exception as e: print e print "Resync {}: {}".format(self.get_name(),self.get_interface().resync()) retry += 1 raise e def _write_ovp(self, num, voltage): '''Set a channel OVP level''' self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().write("VOLTage:PROTection:LEVel {}".format(voltage)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux def _read_ovp(self, num): '''Read channel OVP level''' retry = 0 while(retry <= self.retries): try: self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) data = float(self.get_interface().ask("VOLTage:PROTection:LEVel?")) time.sleep(self.hameg_suck_time) return data except Exception as e: print e print "Resync {}: {}".format(self.get_name(),self.get_interface().resync()) retry += 1 raise e def _read_ovp_status(self, num): '''Read channel OVP level''' self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) data = self.get_interface().ask("VOLTage:PROTection:TRIPped?") time.sleep(self.hameg_suck_time) return data def _read_fuse_status(self, num): '''read if fuse is tripped''' self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) data = int(self.get_interface().ask("FUSE:TRIPed?")) time.sleep(self.hameg_suck_time) return data def _write_enable(self, num, state): #state is true/false self.get_interface().write("INST:NSEL {}".format(num)) time.sleep(self.hameg_suck_time) #needed for hw serial on fast linux self.get_interface().ask("*OPC?") time.sleep(self.hameg_suck_time) if state: self.get_interface().write("OUTPUT:STATE ON") time.sleep(self.hameg_suck_time) else: self.get_interface().write("OUTPUT:STATE OFF") time.sleep(self.hameg_suck_time) def _write_master_enable(self, state): "True -> turn on all enabled channels / False -> turn off all channels" if state: self.get_interface().write("OUTPut:GENeral ON") time.sleep(self.hameg_suck_time) else: self.get_interface().write("OUTPut:GENeral OFF") time.sleep(self.hameg_suck_time)
[docs]class hp_3478a(instrument): '''single channel hp_3478a meter defaults to dc voltage''' def __init__(self,interface_visa): self._base_name = 'hp_3478a' scpi_instrument.__init__(self,"hp_3478a @ " + str(interface_visa)) self.add_interface_visa(interface_visa) self.config_dc_voltage()
[docs] def config_dc_voltage(self): '''Configure meter for DC voltage measurement''' self.get_interface().write("F1")
[docs] def config_dc_current(self): '''Configure meter for DC current measurement''' self.get_interface().write("F5")
[docs] def config_ac_voltage(self): '''Configure meter for AC voltage measurement''' self.get_interface().write("F2")
[docs] def config_ac_current(self): '''Configure meter for AC current measurement''' self.get_interface().write("F6")
[docs] def add_channel(self,channel_name): '''Add named channel to instrument without configuring measurement type.''' meter_channel = channel(channel_name,read_function=self._read_meter) return self._add_channel(meter_channel)
def _read_meter(self): '''Return float representing meter measurement. Units are V,A,Ohm, etc depending on meter configuration.''' return float(self.get_interface().read())
[docs]class htx9000(scpi_instrument): ''' Single Channel Hypertronix (Steve Martin) HTX9000 400nA < IL < 2.5A, up to 60V, 20W Max on fanless version.''' def __init__(self,interface_visa): self._base_name = 'htx9000' scpi_instrument.__init__(self,"HTX9000 {}".format(interface_visa)) self.add_interface_visa(interface_visa,timeout=0.5) self._write_swipepad_lock(True) atexit.register(lambda: self._write_swipepad_lock(False)) def __del__(self): '''Close interface (serial) port on exit''' self.get_interface().close()
[docs] def add_channel(self,channel_name, add_extended_channels=True): '''Helper function adds current forcing channel of channel_name optionally also adds _dropout and _readback channels.''' if add_extended_channels: self.add_channel_current_readback(channel_name + "_readback") self.add_channel_dropout(channel_name + "_dropout") # self.add_channel_swipepad_lock(channel_name + "_swipepad_lock") return self.add_channel_current(channel_name)
def add_channel_current(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current) new_channel.write(0) return self._add_channel(new_channel) def add_channel_current_readback(self,channel_name): new_channel = channel(channel_name,read_function=self._readback_current) return self._add_channel(new_channel) def add_channel_dropout(self,channel_name): new_channel = channel(channel_name,read_function=self._read_dropout) return self._add_channel(new_channel) def add_channel_temp_heatsink(self,channel_name): new_channel = channel(channel_name,read_function=self._read_heatsink_temp) return self._add_channel(new_channel) def add_channel_temp_board(self,channel_name): new_channel = channel(channel_name,read_function=self._read_board_temp) return self._add_channel(new_channel) def add_channel_swipepad_lock(self,channel_name): new_channel = channel(channel_name,write_function=self._write_swipepad_lock) new_channel.write(0) return self._add_channel(new_channel) def _write_current(self,value, range=None): '''Write channel to value.''' #TODO: instrument currently broken - sets range to low if under-range on input Steve to fix. if value < 0: # ooops need to fix instrument, does bad things when asked to make negative SM. value = 0 if (range is not None): if (range.upper() == "HIGH" or range.upper() == "HI"): cmd = "SOURce:CURRent:RANGe:HIgh {:7.5e}".format(value) elif (range.upper() == "LOW" or range.upper() == "LO"): cmd = "SOURce:CURRent:RANGe:LOw {:7.5e}".format(value) else: raise Exception('Valid ranges are "HIGH" and "LOW"') else: cmd = "SOURce:CURRent {:7.5e}".format(float(value)) self.get_interface().write(cmd) try: response = self.get_interface().ask("SYSTem:ERRor?") #print "HTX9000 err {}".format(response) err = int(response.split(",")[0]) if (err != 0): raise Exception("WARNING, SCPI Error: {}".format(response)) except Exception as e: print "{} from inside HTX9000 {} write_channel ".format(e, self._name) flush_chars = self.get_interface().resync() print "saw {} extra characters: {}".format(len(flush_chars),flush_chars) def _read_dropout(self): '''Return False if in regulation, True if in dropout.''' while True: data = self.get_interface().ask("DROPout?") try: data = int(data) if (data == 0 or data == 1): return bool(data) else: raise Exception("WARNING, Bad Response: {}".format(data)) except Exception as e: flush_chars = self.get_interface().resync() print "{} from inside HTX9000 {} read_dropout. SCPI Error: {}".format(e, self._name, self.get_interface().ask("SYSTem:ERRor?")) print "saw {} extra characters: {}".format(len(flush_chars),flush_chars) flush_chars = self.get_interface().resync() def _readback_current(self): '''Return current setting from instrument. Steve verify that this is just a rounded version of what was previously written.''' while True: data = self.get_interface().ask("SOURce:CURRent?") try: return float(data) except Exception as e: flush_chars = self.get_interface().resync() print "{} from inside HTX9000 {} read_current. SCPI Error: {}".format(e, self._name, self.get_interface().ask("SYSTem:ERRor?")) print "saw {} extra characters: {}".format(len(flush_chars),flush_chars) flush_chars = self.get_interface().resync() def _read_heatsink_temp(self): return self.get_interface().ask("TEMP:HEATsink?") def _read_board_temp(self): return self.get_interface().ask("TEMP:BOARD?") def _write_swipepad_lock(self,value): '''Turn on or off the swipe pad lock so incidental contact doesn't change the value.''' self.get_interface().write("SYSTem:LOCK" if value else "SYSTem:LOCK:RELease") try: response = self.get_interface().ask("SYSTem:ERRor?") err = int(response.split(",")[0]) if (err != 0): raise Exception("WARNING, SCPI Error: {}".format(response)) except Exception as e: print "{} from inside HTX9000 {} write_channel ".format(e, self._name) flush_chars = self.get_interface().resync() print "saw {} extra characters: {} during SYSTem:LOCK or SYSTem:LOCK:RELease".format(len(flush_chars),flush_chars)
[docs]class htx9001(scpi_instrument): ''' HTX9001 Configurator Pro (Steve Martin) Breakout/Edge connector board for ATE Bench, with i2c Supports 4 types of channels: gpio - 10 Channels, Possible values are 0,1(5V),Z (HiZ), P (Weak Pull Up) test_hook - 5 channels, 1,0 pullup to 12V NO CURRENT LIMIT relay - Channels 1-4 and 9-12, correspond to supply numbers, 0 or 1 (1 is supply connected) dvcc - Controls I2C/SMBus DVCC voltage ''' def __init__(self,interface_visa,interface_twi, calibrating = False): '''Creates a htx9001 object''' self._base_name = 'htx9001' #work with both serial port strings and pyserial objects scpi_instrument.__init__(self,"HTX9001 {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.add_interface_twi(interface_twi) self._twi = interface_twi self.tries = 3 self.test_hook_pins = {1:'PC6',2:'PC7',3:'PD2',4:'PD3',5:'PD4'} self.gpio_pins = {1:'PB0',2:'PB1',3:'PB2',4:'PB3',5:'PB4',6:'PB5',7:'PB6',8:'PB7',9:'PD5',10:'PD6'} self.relay_pins = {1:'PF4',2:'PD7',3:'PF5',4:'PE6',9:'PF6',10:'PF0',11:'PF7',12:'PF1'} self.initialized_pins = [] self._write_gpio(self.gpio_pins.keys(),len(self.gpio_pins)*'Z') #HiZ all GPIO Pins self.check_calibration_valid(calibrating) def _disable_i2c(self): write_str = ':I2C:PORT:DISable;' self.get_interface().write(write_str)
[docs] def add_channel_dvcc(self,channel_name): '''Adds a channel controlling the dvcc voltage''' dvcc = channel(channel_name,write_function=self._set_dvcc) dvcc.set_write_delay(0.2) return self._add_channel(dvcc)
[docs] def add_channel_relay(self,channel_name,relay_number): '''Adds a relay channel, channel_name is the name of the channel, relay_number is the number of the relay (same number as the supply being switched) valid relays are 1-4 and 9-12''' if relay_number not in self.relay_pins: raise Exception("Invalid relay number {}".format(relay_number)) if self.relay_pins[relay_number] in self.initialized_pins: raise Exception("relay number {} already used in another channel!".format(relay_number)) new_channel = integer_channel(channel_name,size=1,write_function=lambda data: self._write_relay(relay_number,data)) self._add_channel(new_channel) new_channel.set_write_delay(0.2) self.initialized_pins.append(self.relay_pins[relay_number]) return new_channel
[docs] def add_channel_test_hook(self,channel_name,test_hook_number): '''Adds a test hook channel, channel_name is the name of the channel test_hook_number is the number of the test hook (valid test hooks are 1-5''' if test_hook_number not in self.test_hook_pins: raise Exception("Invalid test hook number {}".format(test_hook_number)) if self.test_hook_pins[test_hook_number] in self.initialized_pins: raise Exception("test hook number {} already used in another channel!".format(test_hook_number)) new_channel = integer_channel(channel_name,size=1,write_function=lambda data: self._write_test_hook(test_hook_number,data)) self._add_channel(new_channel) self.initialized_pins.append(self.test_hook_pins[test_hook_number]) return new_channel
[docs] def add_channel_gpio(self,channel_name,gpio_list,output=True,pin_state = "Z"): '''Adds a GPIO channel, can be a single bit or a bus of bits channel_name is the name of the channel gpio pins is either a single integer for a single bit or a list of integers ordered msb to lsb valid gpio_numbers are 1-10, valid settings are [{integer},'z','Z','p','P','H','L']''' if type(gpio_list) is not list: gpio_list = [gpio_list] for gpio_pin in gpio_list: if gpio_pin not in self.gpio_pins: raise Exception("Invalid gpio {}".format(gpio_pin)) if self.gpio_pins[gpio_pin] in self.initialized_pins: print "HTX9001(A) Warning: Non GPIO pin {} being redefined as a GPIO pin.".format(gpio_pin) # raise Exception("gpio number {} already used in another channel!".format(gpio_pin)) self.initialized_pins.append(self.gpio_pins[gpio_pin]) if output: #can't use integer channel because of possible P,Z values. new_channel = channel(channel_name,write_function=lambda value: self._write_gpio(gpio_list,value)) new_channel.write(pin_state) else: new_channel = channel(channel_name,read_function=lambda: self._read_pins_values(gpio_list)) self._write_gpio(gpio_list, pin_state) return self._add_channel(new_channel)
def _set_dvcc(self, voltage): for i in range(3): value = int(max(min(voltage, 5), 0.8) / 5.0 * 63.0) & 0x3F try: self._twi.send_byte(0x74, value) return except Exception as e: print "HTX9001 Configurator Communication error setting DVCC, retrying...." print e self._twi.resync_communication() print "Sorry, couldn't fix it with resync_communication()" raise e def read_channel_pin(self,channel_name): return self.read_channel_generic(channel_name,function=self.read_pins_values) def resync(self): self._twi.init_i2c() def _clean_value(self,value): if (value == True or value == 1 or value == '1'): value = 1 elif (value == False or value == 0 or value == '0'): value = 0 else: raise Exception("Can't parse htx9001 input: {}".format(value)) return value def _pin_response_valid(self,ret_str): if (len(ret_str) != 6): return False if (ret_str[4:] != '\r\n'): return False return True def _write_pin(self,pin,value,tries=0): if tries == self.tries: raise Exception("Failed to write pin {} to {}".format(pin,value)) if value not in [0,1,'z','Z','p','P','H','L']: raise Exception('Bad value for pin: {}'.format(value)) #set the pin and read back its state to make sure there were no usb communication problems value_str = str(value).upper() if value == 1: value_str = "H" elif value == 0: value_str = "L" write_str = ':SETPin:%s(@%s);:SETPin?(@%s);' % (value_str,pin,pin) self.get_interface().write(write_str) ret_str = self.get_interface().readline() if not self._pin_response_valid(ret_str) or (ret_str[2] != str(value).upper()): self.resync() self._write_pin(pin,value,tries+1) def _read_pin_setting(self,pin,tries=0): if tries == self.tries: raise Exception("Failed to read pin {}".format(pin)) read_str = ':SETPin?(@{});'.format(pin) self.get_interface().write(read_str) ret_str = self.get_interface().readline() if not self._pin_response_valid(ret_str): self.resync() self._read_pin_setting(pin,tries+1) try: ret_val = int(ret_str[2]) except: ret_val = ret_str[2] return ret_val def _read_pin_value(self,pin,tries=0): if tries == self.tries: raise Exception("Failed to read pin {}".format(pin)) read_str = ':PINVal?(@{});'.format(pin) self.get_interface().write(read_str) ret_str = self.get_interface().readline() if not self._pin_response_valid(ret_str): self.resync() return self._read_pin_value(pin,tries+1) try: ret_val = int(ret_str[2]) except: ret_val = ret_str[2] return ret_val def _to_bit_list(self,value,out_length): out_list = None if type(value) is bool: if value: out_list = [1] else: out_list = [0] if type(value) is float: if int(value) == value: value = int(value) else: raise Exception("Bad data for pins {}".format(value)) if type(value) is int: value = bin(value).lstrip('0b').rjust(out_length,'0') if isinstance(value,str): out_list = [] value = value.upper() for char in value: if char in ['T','H','1']: char = 1 if char in ['F','L','0']: char = 0 out_list.append(char) for item in out_list: if item not in [1,0,'P','Z']: raise Exception("Bad data {}".format(out_list)) if out_list == None: raise Exception("Bad data for pins {}".format(value)) while len(out_list) < out_length: out_list.reverse() out_list.append(0) out_list.reverse() while len(out_list) > out_length: out_list.reverse() out_list.pop() out_list.reverse() return out_list def _from_bit_list(self,bit_list): #attempt to build an integer from the bit list, otherwise return a string out = 0 for value in bit_list: try: out *= 2 out += int(value) except: out = None break if out == None: out = "" for value in bit_list: out += str(value) return out def _write_test_hook(self,test_hook,value): value = self._clean_value(value) if value not in [0,1,'z','Z']: raise Exception('Bad value for test hook: {}'.format(value)) self._write_pin(self.test_hook_pins[test_hook],value) def _write_gpio(self,gpio_list,value): bit_list = self._to_bit_list(value,len(gpio_list)) pin_values = zip(gpio_list,bit_list) for pin_name,pin_value in pin_values: self._write_pin(self.gpio_pins[pin_name],pin_value) def _write_relay(self,relay_number,value): value = self._clean_value(value) if value not in [0,1]: raise Exception('Bad value for relay: {}'.format(value)) if value == 0: value = 1 elif value == 1: value = 0 self._write_pin(self.relay_pins[relay_number],value) def _read_pins_values(self,pins,invert=False): pin_names = [self.gpio_pins[pin] for pin in pins] return self._read_pins_generic(pin_names,invert=invert,function=self._read_pin_value) def _read_pins_generic(self,pins,invert=False,function=None): if type(pins) is not list: pins = [pins] output = [] for pin in pins: value = function(pin) if invert: if value == 1: value = 0 elif value == 0: value = 1 output.append(value) return self._from_bit_list(output) def set_resistor_calibration(self,resistor_number,value): try: float(value) except: raise Exception("Invalid Calibration Data") write_str = "CAL:DATA ({},{});".format(resistor_number,value) self.get_interface().write(write_str) def get_resistor_calibration(self,resistor_number): read_str = "CAL:DATA?({});".format(resistor_number) self.get_interface().write(read_str) data = self.get_interface().readline() return float(data) def get_calibration_date(self): datestr = self.get_interface().ask('CAL:DATE?') #datetime.datetime.now().strftime("%Y-%m-%d") try: y,m,d = datestr.split('-') date = datetime.date(int(y),int(m),int(d)) return date except: print 'Board calibration date invalid: {}.'.format(datestr) return None def get_days_since_calibration(self): cal_date = self.get_calibration_date() if cal_date is not None: return (datetime.date.today()-cal_date).days def check_calibration_valid(self, calibrating): cal_duration = 365 #days days = self.get_days_since_calibration() if days is None and not calibrating: raise Exception("Current sense resistor calibration date invalid. Was board ever calibrated?") elif days > cal_duration: for i in range(10): print "******** HTX9001 Calibration Expired ********" print "Current sense resistor calibration required every {} days.".format(cal_duration) print "Calibration last performed {}.".format(self.get_calibration_date()) print "Calibration overdue by {} days.".format(self.get_days_since_calibration()-cal_duration) resp = raw_input("Continue anyway?") if not resp.lower().startswith('y'): raise Exception('HTX9001 Calibration Expired. Calibration required every {} days'.format(cal_duration)) def set_all_relays(self,value): for relay in self.relay_pins: self._write_relay(relay,value) def _rip(self,write_list): '''write_list format [(pin,value),(pin,value)....] this function uses raw pin names and builds a single query sting without readback for maximum speed there is currently no way to connect this with channels so the pin is the raw pin name like PB1 etc''' write_str = '' for pin,value in write_list: if value not in [0,1,'z','Z','p','P','H','L']: raise Exception('Bad value for pin: {}'.format(value)) #set the pin and read back its state to make sure there were no usb communication problems value_str = str(value).upper() if value == 1: value_str = "H" elif value == 0: value_str = "L" write_str += ':SETPin:%s(@%s);' % (value_str,pin) self.get_interface().write(write_str)
[docs]class htx9001a(htx9001): ''' HTX9001 Configurator Pro A(Steve Martin) Breakout/Edge connector board for ATE Bench, with i2c Supports 5 types of channels: gpio - 10 Channels, Possible values are 0,1(5V),Z (HiZ), P (Weak Pull Up) test_hook - 5 channels, 1,0 pullup to 12V NO CURRENT LIMIT relay - Channels 1-12, correspond to supply numbers, 0 or 1 (1 is supply connected) ammeter relay - Channels 5-8 dvcc - Controls I2C/SMBus DVCC voltage ''' def __init__(self,interface_visa, calibrating = False): '''Creates a htx9001a object''' self._base_name = 'htx9001a' scpi_instrument.__init__(self,"HTX9001A {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.tries = 3 self.test_hook_pins = {1:'PC6',2:'PC7',3:'PD2',4:'PD3',5:'PD4'} self.gpio_pins = {1:'PB0',2:'PB1',3:'PB2',4:'PB3',5:'PB4',6:'PB5',7:'PB6',8:'PB7',9:'PD5',10:'PD6','SCL':'PD0','SDA':'PD1'} self.relay_pins = {1:'PF4',2:'PD7',3:'PF5',4:'PE6',5:'PA4',6:'PA6',7:'PA5',8:'PA3',9:'PF6',10:'PF0',11:'PF7',12:'PF1'} self.irelay_pins = {5:'PE1',6:'PE0',7:'PF3',8:'PF2'} self.pwm_pins = {6:'PB5',7:'PB6',8:'PB7'} self.FCLK = 16e6 # crystal frequency self.initialized_pins = [] self._write_gpio(self.gpio_pins.keys(),len(self.gpio_pins)*'Z') #HiZ all GPIO Pins self.check_calibration_valid(calibrating) self.pwm_duty_cycle = {} self.pwm_frequency = {} self.prescale = {} self.top = {} self.pwm_enable = {} def _disable_i2c(self): self.get_interface().write(":I2C:PORT:DISable;") def resync(self): line = self.get_interface().readline() while len(line): print "HTX9001 resync clearing out serial port data '{}'".format(line) line = self.get_interface().readline()
[docs] def add_channel_irelay(self,channel_name,irelay_number): '''Adds an irelay channel, channel_name is the name of the channel, irelay_number is the number of the irelay (same number as the supply being switched) valid irelays are 5-8''' if irelay_number not in self.irelay_pins: raise Exception("Invalid irelay number {}".format(irelay_number)) if self.irelay_pins[irelay_number] in self.initialized_pins: raise Exception("irelay number {} already used in another channel!".format(irelay_number)) new_channel = integer_channel(channel_name,size=1,write_function=lambda data: self._write_irelay(irelay_number,data)) self._add_channel(new_channel) self.initialized_pins.append(self.irelay_pins[irelay_number]) return new_channel
def set_all_irelays(self,value): for irelay in self.irelay_pins: self._write_irelay(irelay,value) def get_resistor_calibration(self,resistor_number): read_str = "CAL:DATA? {};".format(resistor_number) self.get_interface().write(read_str) data = self.get_interface().readline() try: return float(data) except: return data # may return a string like "bad checksum" def _write_irelay(self,irelay_number,value): value = self._clean_value(value) if value not in [0,1]: raise Exception('Bad value for irelay: {}'.format(value)) self._write_pin(self.irelay_pins[irelay_number],value) def _set_dvcc(self, voltage): self.get_interface().write("VOLT:DVCC {}".format(voltage)) def add_channel_pwm(self, channel_name, pin): if pin not in self.pwm_pins: raise Exception("Invalid HTX9001A PWM pin number {}. Must be one of: {}".format(pin, self.pwm_pins)) if self.pwm_pins[pin] in self.initialized_pins: print "HTX9001A Warning: Non PWM pin {} being redefined as a PWM pin.".format(pin) # raise Exception("HTX9001A pin number {} already in use by another channel!".format(pin)) else: self.initialized_pins.append(self.pwm_pins[pin]) self._add_channel_pwm_frequency(channel_name+"_frequency", pin) self._add_channel_pwm_duty_cycle(channel_name+"_dutycycle", pin) self._add_channel_pwm_enable(channel_name+"_enable", pin) self._add_channel_pwm_freq_readback(channel_name+"_freq_readback", pin) self.pwm_duty_cycle[pin] = 0.5 self.pwm_frequency[pin] = 1e6 self.pwm_enable[pin] = 0 self._update_pwm_channel(pin) def _add_channel_pwm_frequency(self, channel_name, pin): def set_pwm_frequency(value): flow = 16e6/1024/65536 fhigh = 8e6 # datasheet, fmax = fclkio / 2 if value < flow or value > fhigh: raise Exception("Invalid HTX9001A frequency {}. Must be between {} and {} Hz.".format(value, flow, fhigh)) self.pwm_frequency[pin] = value self._update_pwm_channel(pin) new_channel = channel(channel_name, write_function=set_pwm_frequency) self._add_channel(new_channel) def _add_channel_pwm_duty_cycle(self, channel_name, pin): def set_pwm_duty_cycle(value): self.pwm_duty_cycle[pin] = value self._update_pwm_channel(pin) new_channel = channel(channel_name, write_function=set_pwm_duty_cycle) self._add_channel(new_channel) def _add_channel_pwm_enable(self, channel_name, pin): def set_pwm_enable(value): value = self._clean_value(value) if value not in [0,1]: raise Exception('Bad value for HTX9001A pwm_enable: {}. Try one of: 0,"0",False,1,"1",True.'.format(value)) self.pwm_enable[pin] = value self._update_pwm_channel(pin) new_channel = channel(channel_name, write_function=set_pwm_enable) self._add_channel(new_channel) def _add_channel_pwm_freq_readback(self, channel_name, pin): def compute_f(pin): return self.FCLK / float(self.prescale[pin]) / float(1 + self.top[pin]) new_channel = channel(channel_name, read_function = lambda: compute_f(pin)) self._add_channel(new_channel) def _update_pwm_channel(self, pin): prescale_list = [1,8,64,256,1024] for prescale_choice in prescale_list[::-1]: top = int(round(self.FCLK / self.pwm_frequency[pin] / prescale_choice - 1)) if top <= 65535 and top >= 0: self.prescale[pin] = prescale_choice self.top[pin] = int(round(self.FCLK / self.pwm_frequency[pin] / self.prescale[pin] - 1)) compare = int(self.pwm_duty_cycle[pin] * self.top[pin]) if self.pwm_enable[pin] == 1: self.get_interface().write('PWM:PREScale {}'.format(int(self.prescale[pin]))) self.get_interface().write('PWM:TOP {}'.format(self.top[pin])) self.get_interface().write('PWM:COMPare ({},{})'.format(self.pwm_pins[pin], compare)) self.get_interface().write('PWM:MODE ({},CLEAR)'.format(self.pwm_pins[pin])) else: self.get_interface().write('PWM:MODE ({},DISABLE)'.format(self.pwm_pins[pin])) def add_channel_servo(self,channel_name,servo_number): if servo_number not in self.pwm_pins: raise Exception("Invalid HTX9001A servo pin number {}.".format(servo_number)) if self.pwm_pins[servo_number] in self.initialized_pins: print "HTX9001A Warning: Non Servo pin {} being redefined as a Servo pin.".format(pin) # raise Exception("HTX9001A servo number {} already used in another channel!".format(servo_number)) new_channel = channel(channel_name,write_function=lambda value: self._write_servo(servo_number,value)) self._write_servo_enable(servo_number, True) self._add_channel(new_channel) self.initialized_pins.append(self.pwm_pins[servo_number]) return new_channel def add_channel_servo_enable(self,channel_name,servo_number): if servo_number not in self.pwm_pins: raise Exception("HTX9001A Invalid servo pin number {}".format(servo_number)) new_channel = channel(channel_name,write_function=lambda value: self._write_servo_enable(servo_number,value)) return self._add_channel(new_channel) def _write_servo_enable(self,servo_number,value): value = self._clean_value(value) if value not in [0,1]: raise Exception('Bad value for HTX9001A servo_enable: {}.'.format(value)) if value: self.get_interface().write('PWM:TOP 39999') self.get_interface().write('PWM:PREScale 8') self.get_interface().write('PWM:COMPare ({},3000)'.format(self.pwm_pins[servo_number])) self.get_interface().write('PWM:MODE ({},CLEAR)'.format(self.pwm_pins[servo_number])) else: self.get_interface().write('PWM:MODE ({},DISABLE)'.format(self.pwm_pins[servo_number])) def _write_servo(self,servo_number,value): value = float(value) if value >= 1.51 or value <= -0.01: raise Exception('Bad value for HTX9001A servo: {}.'.format(value)) self.get_interface().write('PWM:COMPare ({},{})'.format(self.pwm_pins[servo_number],value*2000+2000)) def set_all_relays(self,value): for relay in self.relay_pins: self._write_relay(relay, value) for relay in self.irelay_pins: self._write_irelay(relay, value)
[docs]class kikusui_plz(scpi_instrument): '''Kikusui single channel electronic load superclass Instrument Family: PLZ 164W PLZ 164WA PLZ 334W PLZ 664WA PLZ1004W''' #note that this superclass was developed and tested with only the PLZ 334W instrument #some methods may need to be duplicated and moved to the instrument-specific classes #to resolve any operational/feature differences such as range selection def __init__(self,interface_visa): self._base_name = 'kikusui_plz' scpi_instrument.__init__(self,"{} @ {}".format(self.kikusui_plz_name, interface_visa) ) self.add_interface_visa(interface_visa) self.clear_status() self.reset() self.get_interface().write('SOURce:FUNCtion:MODE CCCV') #self.get_interface().write("CCCR 1") #constant current self.get_interface().write("CURR 0") self.get_interface().write("VOLT 0") self.get_interface().write("OUTPUT 1") #SM: Change from high range if you need it #self.get_interface().write("CURRent:RANGe LOW") #self.get_interface().write("CURRent:RANGe MED") #DJS This was inadvertantly changed by Greg in commit 2539. Changing back. Not sure what it should be. self._range = self._read_range() self._mode = self._read_mode()
[docs] def add_channel(self,channel_name,add_sense_channels=True): '''Helper function adds primary current forcing channel of channel_name plus _vsense and _isense readback channels.''' self.add_channel_current(channel_name) if add_sense_channels: self.add_channel_vsense(channel_name + "_vsense") self.add_channel_isense(channel_name + "_isense") #the old add_channel added more, however I'm changing the default to this to speed up reading # add channels independently if you want something different self.write_channel(channel_name,0) #default to zero current
def add_channel_current(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current) self._add_channel(new_channel) def add_channel_voltage(self,channel_name): new_channel = channel(channel_name,write_function=self._write_voltage) self._add_channel(new_channel) def add_channel_vsense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_vsense) self._add_channel(new_channel) def add_channel_isense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_isense) self._add_channel(new_channel) def add_channel_power(self,channel_name): new_channel = channel(channel_name,read_function=self._read_power) self._add_channel(new_channel) def add_channel_range_readback(self,channel_name): new_channel = channel(channel_name,read_function=self._read_range) self._add_channel(new_channel) def add_channel_range(self,channel_name): new_channel = channel(channel_name,write_function=self._write_range) self._add_channel(new_channel) def add_channel_slew_rate(self,channel_name): new_channel = channel(channel_name,write_function=self._write_slew_rate) self._add_channel(new_channel) def add_channel_pulse_on(self,channel_name): new_channel = channel(channel_name,write_function=self._write_pulse_on) self._add_channel(new_channel) def add_channel_duty_cycle(self,channel_name): # Duty cycle, frequency and current level are used for Switch operation new_channel = channel(channel_name,write_function=self._write_duty_cycle) self._add_channel(new_channel) def add_channel_frequency(self,channel_name): new_channel = channel(channel_name,write_function=self._write_frequency) self._add_channel(new_channel) def add_channel_current_level(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current_level) self._add_channel(new_channel) def add_channel_short(self,channel_name): # Short will only admit as much current as the current range. So before any short test new_channel = channel(channel_name,write_function=self._write_short) # Remember to input a high current in your own code to force the change in Range self._add_channel(new_channel) def add_channel_enable(self,channel_name): new_channel = channel(channel_name,write_function=self._write_enable) self._add_channel(new_channel) def _read_vsense(self): '''Return channel measured voltage float.''' return float(self.get_interface().ask("MEAS:VOLT?")) def _read_power(self): '''Return channel measured power float.''' return float(self.get_interface().ask("MEAS:POW?")) def _read_isense(self): '''Return channel measured current float.''' return float(self.get_interface().ask("MEAS:CURR?")) def _read_range(self): '''Return channel range string.''' return self.get_interface().ask("CURRent:RANGe?") def _read_load(self): '''Return load state (1 -> "On", 0 -> "Off").''' return self.get_interface().ask("OUTPut?") def _read_mode(self): '''Return operation mode ( CC, CV, etc).''' return self.get_interface().ask("SOURce:FUNCtion:MODE?") def _write_current(self,current,autorange=False): '''Write channel to force value current. Optionally set range manually. Valid ranges are "HIGH", "MED", and "LOW"''' self._write_mode("CC") self._mode = "CC" if autorange: if (current <= self.kikusui_low_threshold): best_range = "LOW" elif (current > self.kikusui_low_threshold and current <= self.kikusui_high_threshold): best_range = "MED" elif (current > self.kikusui_high_threshold): best_range = "HIGH" self._write_enable(0) time.sleep(0.3) self._write_range(best_range) self._write_enable(1) self.get_interface().write("CURR {}".format(current)) def _write_voltage(self,voltage): if self._mode != "CV": self._write_enable(0) time.sleep(0.3) self._write_mode("CV") self._write_range("HIGH") self._write_enable(1) self.get_interface().write("VOLT {}".format(voltage)) def _write_slew_rate(self,slew_rate): self.get_interface().write("CURR:SLEW {}".format(slew_rate)) def _write_pulse_on(self,pulse_on): self.get_interface().write("PULSe {}".format(pulse_on)) def _write_duty_cycle(self,duty_cycle): #duty_cycle is a percent from 5 to 95 % self.get_interface().write("PULSe:DCYCle {}".format(duty_cycle)) def _write_frequency(self,frequency): self.get_interface().write("PULSe:FREQuency {}".format(frequency)) def _write_current_level(self,current_level): self.get_interface().write("PULSe:LEVel:CURRent {}".format(current_level)) def _write_short(self,short): self.get_interface().write("OUTPut:SHORt {}".format(short)) def _write_enable(self,output): if output == True or output == 1: self.get_interface().write("OUTPut 1") if output == False or output == 0: self.get_interface().write("OUTPut 0") timeout1 = time.time() + .5 while int(self._read_load()) != output: if timeout1 < time.time(): print "Timeout1 for load on/off exceeded. Bump in lab_instruments.py if it is a consistent problem " def _write_range(self,range): if (range is not None): if range.upper() in ["LOW", "MED", "HIGH"]: self._write_enable(0) time.sleep(0.3) self.get_interface().write("CURRent:RANGe {}".format(range.upper())) self._write_enable(1) else: raise Exception('Valid ranges are "HIGH", "MED", and "LOW"') self._range = self._read_range() def _write_mode(self,mode): self.get_interface().write("SOURce:FUNCtion:MODE {}".format(mode)) self._mode = mode
[docs]class kikusui_plz334w(kikusui_plz): '''single channel kikusui_plz334w electronic load''' def __init__(self,interface_visa): self.kikusui_plz_name = 'kikusui_plz334w' self.kikusui_low_threshold = 0.66 self.kikusui_high_threshold = 6.66 kikusui_plz.__init__(self,interface_visa)
[docs]class kikusui_plz664wa(kikusui_plz): '''single channel kikusui_plz664wa electronic load''' def __init__(self,interface_visa): self.kikusui_plz_name = 'kikusui_plz664w' self.kikusui_low_threshold = 1.32 self.kikusui_high_threshold = 13.2 kikusui_plz.__init__(self,interface_visa)
[docs]class kikusui_pbz(scpi_instrument): '''single channel kikusui_pbz20-20, pbz40-10 bipolar power supply parent class''' def __init__(self,interface_visa): self.add_interface_visa(interface_visa) #initialize to instrument on, current 0 self.clear_status() self.reset() self.get_interface().write("CURR:LEV:IMM:AMPL 0.0A") self.get_interface().write("VOLT:LEV:IMM:AMPL 0V") self._write_output_enable(True)
[docs] def add_channel(self,channel_name,ilim=1,delay=0.5,add_extended_channels=True): '''Helper channel adds primary voltage forcing channel. Optionally specify channel current limit. Valid range is [???-???] optionally also adds _ilim_source and _ilim_sink limit forcing channels''' voltage_channel = self.add_channel_voltage(channel_name) voltage_channel.set_write_delay(delay) if add_extended_channels: current_source_channel = self.add_channel_current_source(channel_name + "_ilim_source") current_source_channel.set_write_delay(delay) current_sink_channel = self.add_channel_current_sink(channel_name + "_ilim_sink") current_sink_channel.set_write_delay(delay) output_enable_channel = self.add_channel_output_enable(channel_name + "_enable") output_enable_channel.write(True) output_enable_channel.set_write_delay(delay) voltage_sense_channel = self.add_channel_vsense(channel_name + "_vsense") current_sense_channel = self.add_channel_isense(channel_name + "_isense") self.write_channel(channel_name + "_ilim_source", ilim) self.write_channel(channel_name + "_ilim_sink", -ilim) else: self._write_output_enable(True) self._write_current_source(ilim) self._write_current_sink(-ilim) return voltage_channel
def add_channel_voltage(self,channel_name): new_channel = channel(channel_name,write_function=self._write_voltage) return self._add_channel(new_channel) def add_channel_current_source(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current_source) return self._add_channel(new_channel) def add_channel_current_sink(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current_sink) return self._add_channel(new_channel) def add_channel_output_enable(self,channel_name): new_channel = channel(channel_name,write_function=self._write_output_enable) return self._add_channel(new_channel) def add_channel_vsense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_vsense) return self._add_channel(new_channel) def add_channel_isense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_isense) return self._add_channel(new_channel) def add_channel_voltage_readback(self,channel_name): new_channel = channel(channel_name,read_function=self._read_voltage_readback) return self._add_channel(new_channel) def _write_current_source(self,current): self.get_interface().write("CURR:PROT:UPP {}".format(current)) def _write_current_sink(self,current): self.get_interface().write("CURR:PROT:LOW {}".format(current)) def _write_voltage(self,voltage): '''set output voltage''' self.get_interface().write("VOLTage {}".format(voltage)) def _write_output_enable(self,enable): '''set output enable''' if enable: self.get_interface().write("OUTP 1") else: self.get_interface().write("OUTP 0") def _read_voltage_readback(self): '''Returns instrument's actual setopint. May differ by commanded value by rounding/range error''' return float(self.get_interface().ask("VOLT?")) def _read_vsense(self): '''Returns instrument's measured output voltage.''' return float(self.get_interface().ask("MEAS:VOLT?")) def _read_isense(self): '''Returns instrument's measured current output.''' return float(self.get_interface().ask("MEAS:CURR?"))
[docs]class kikusui_pbz20_20(kikusui_pbz): '''Kikusui single channel 20V/20A bipolar power supply.''' def __init__(self,interface_visa): self._base_name = 'kikusui_pbz20_20' scpi_instrument.__init__(self,"kikusui_pbz20-20 @ {}".format(interface_visa)) kikusui_pbz.__init__(self,interface_visa)
[docs]class kikusui_pbz40_10(kikusui_pbz): '''Kikusui single channel 40V/10A bipolar power supply.''' def __init__(self,interface_visa): self._base_name = 'kikusui_pbz40_10' scpi_instrument.__init__(self,"kikusui_pbz40_10 @ {}".format(interface_visa)) kikusui_pbz.__init__(self,interface_visa)
[docs]class kikusui_pwr(scpi_instrument): '''Kikusui single channel unipolar power supply superclass Instrument Family: PWR400L PWR400M PWR400H PWR800L PWR800M PWR800H PWR1600L PWR1600M PWR1600H''' #note that this superclass was developed and tested with only the PWR800l instrument #some methods may need to be duplicated and moved to the instrument-specific classes #to resolve any operational/feature differences such as range selection def __init__(self,interface_visa,node,ch): '''node is a ??? ch is a ???''' self._base_name = 'kikusui_pwr' scpi_instrument.__init__(self, "kikusui_pwr800l {} @ {}:Node {}: Ch{}".format(self.kikusui_pwr_name,interface_visa,node,ch) ) self.add_interface_visa(interface_visa) self.node = node self.ch = ch #initialize to instrument on, current 0 self.get_interface().write("NODE {};CH {};*CLS".format(self.node,self.ch)) self.get_interface().write("NODE {};CH {};*RST".format(self.node,self.ch)) self.get_interface().write("NODE {};CH {};VSET 0.000".format(self.node,self.ch)) self.get_interface().write("NODE {};CH {};ISET 0.000".format(self.node,self.ch)) self.get_interface().write("OUT 1")
[docs] def add_channel(self,channel_name,ilim=1,delay=0.5,add_extended_channels=True): '''Helper function adds primary voltage forcing channel channel_name optionally also adds _ilim forcing channel and _vsense and _isense readback channels.''' voltage_channel = self.add_channel_voltage(channel_name) self.write_channel(channel_name,0) voltage_channel.set_write_delay(delay) if add_extended_channels: current_channel = self.add_channel_current(channel_name + "_ilim") current_channel.set_write_delay(delay) self.add_channel_vsense(channel_name + "_vsense") self.add_channel_isense(channel_name + "_isense") self.write_channel(channel_name + "_ilim", ilim) else: self._write_current(ilim) return voltage_channel
def add_channel_voltage(self,channel_name): new_channel = channel(channel_name,write_function=self._write_voltage) return self._add_channel(new_channel) def add_channel_current(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current) return self._add_channel(new_channel) def add_channel_vsense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_vsense) return self._add_channel(new_channel) def add_channel_isense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_isense) return self._add_channel(new_channel) def add_channel_power(self,channel_name): new_channel = channel(channel_name,read_function=self._read_power) return self._add_channel(new_channel) def add_channel_enable(self,channel_name): new_channel = channel(channel_name,write_function=self._enable) return self._add_channel(new_channel) def _write_voltage(self,voltage): self.get_interface().write("NODE {};CH {};VSET {}".format(self.node, self.ch, voltage)) def _write_current(self,current): self.get_interface().write("NODE {};CH {};ISET {}".format(self.node, self.ch, current)) def _enable(self,enable): if enable: self.get_interface().write("NODE {};CH {};OUT 1".format(self.node, self.ch)) else: self.get_interface().write("NODE {};CH {};OUT 0".format(self.node, self.ch)) def _read_vsense(self): '''Returns instrument's measured output voltage.''' return float(self.get_interface().ask("NODE {};CH {};VOUT?".format(self.node,self.ch))) def _read_power(self): '''Returns instrument's measured power output.''' return float(self.get_interface().ask("NODE {};CH {};POUT?".format(self.node,self.ch))) def _read_isense(self): '''Returns instrument's measured current output.''' return float(self.get_interface().ask("NODE {};CH {};IOUT?".format(self.node,self.ch)))
[docs]class kikusui_pwr800l(kikusui_pwr): '''single channel kikusui PWR800l electronic load''' def __init__(self,addr,node,ch): self.kikusui_pwr_name = "kikusui_pwr800l" kikusui_pwr.__init__(self,addr,node,ch) self._base_name = 'kikusui_pwr800l'
class sorensen_generic_supply(instrument): def __init__(self,interface_visa): '''interface_visa''' self._base_name = 'sorensen_generic_supply' instrument.__init__(self, "{} @ {}".format(self.sorensen_name,interface_visa) ) self.add_interface_visa(interface_visa) #initialize to instrument on, all voltages 0 self.get_interface().write("VSET 0") self.get_interface().write("ISET 0") self.get_interface().write("OUT 1") def add_channel(self,channel_name,ilim=1,add_extended_channels=True): '''Helper method adds primary voltage forcing channel channe_name. optionally also adds _ilim forcing channel and _vsense and _isense readback channels.''' voltage_channel = self.add_channel_voltage(channel_name) if add_extended_channels: self.add_channel_current(channel_name + "_ilim") self.add_channel_vsense(channel_name + "_vsense") self.add_channel_isense(channel_name + "_isense") self.write_channel(channel_name + "_ilim", ilim) else: self._write_current(ilim) return voltage_channel def add_channel_voltage(self,channel_name): new_channel = channel(channel_name,write_function=self._write_voltage) self._add_channel(new_channel) def add_channel_current(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current) self._add_channel(new_channel) def add_channel_vsense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_vsense) self._add_channel(new_channel) def add_channel_isense(self,channel_name): new_channel = channel(channel_name,read_function=self._read_isense) self._add_channel(new_channel) def _write_voltage(self,voltage): '''Set named channel to force voltage, optionally with ilim compliance current''' self.get_interface().write("VSET {}".format(voltage)) def _write_current(self,ilim): '''Set named channel's compliance current''' self.get_interface().write("ISET {}".format(ilim)) def _read_vsense(self,channel_name): '''Returns instrument's measured output voltage.''' return float( self.get_interface().ask("VOUT?").lstrip("VOUT ") ) def _read_isense(self,channel_name): '''Returns instrument's measured output current.''' return float( self.get_interface().ask("IOUT? ").lstrip("IOUT ") )
[docs]class sorensen_lhp_100_10(sorensen_generic_supply): '''single channel sorensen_lhp_100_10''' def __init__(self,interface_visa): self.sorensen_name = "sorensen_lhp_100_10" sorensen_generic_supply.__init__(self, interface_visa) self._base_name = 'sorensen_lhp_100_10'
[docs]class sorensen_xt_250_25(sorensen_generic_supply): '''single channel sorensen_xt_250_25''' def __init__(self,interface_visa): self.sorensen_name = "sorensen_xt_250_25" sorensen_generic_supply.__init__(self, interface_visa) self._base_name = 'sorensen_xt_250_25'
[docs]class temptronic_4310(instrument): #DJS: TODO - Merge into temperature_chamber class when able to test that nothing gets broken. '''single channel temptronic_4310 thermostream special methods: set_window(air_window), set_soak(soak_time), off() use wait_settle to wait for the soak to complete defaults to window = 3, soak=30 extra data _sense - the sensed temperature _window - the temperature window _time - the total settling time (including soak) _soak - the programmed soak time''' def __init__(self,interface_visa,en_compressor=True): '''Optionally disable compressor on startup''' #needs enable/compressor channel work self._base_name = 'temptronic_4310' instrument.__init__(self,"temptronic_4310 @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.setpoint = 25 self.soak = 90 self.window = 1 self.air2dut = 50 self.maxair = 170 self.time = 0 self.get_interface().write("DUTM 1") #use dut measurement if en_compressor: self.get_interface().write("COOL 1") print("Enabling Compressor... ") time.sleep(70) print("Compressor Enabled")
[docs] def add_channel(self,channel_name,add_extended_channels=True): '''Helper method to add most commonly used channels. channel_name represents temperature setpoint. optionlayy also adds _sense_dut, _sense_air, _soak, _window, and _soak_settling_time channels.''' temp_channel = self.add_channel_temp(channel_name) if add_extended_channels: self.add_channel_sense_dut(channel_name + "_sense_dut") self.add_channel_sense_air(channel_name + "_sense_air") self.add_channel_soak(channel_name + "_soak") self.add_channel_window(channel_name + "_window") self.add_channel_soak_settling_time(channel_name + "_soak_settling_time") return temp_channel
[docs] def add_channel_temp(self,channel_name): '''Channel_name represents PID loop forcing temperature setpoint.''' new_channel = channel(channel_name,write_function=self._write_temperature) new_channel.write(self.setpoint) return self._add_channel(new_channel)
[docs] def add_channel_sense_dut(self,channel_name): '''channel_name represents primary PID control loop thermocouple readback.''' new_channel = channel(channel_name,read_function=lambda: float(self.get_interface().ask("TMPD?"))) return self._add_channel(new_channel)
[docs] def add_channel_sense_air(self,channel_name): '''channel_name represents secondary air stream thermocouple readback.''' new_channel = channel(channel_name,read_function=lambda: float(self.get_interface().ask("TMPA?"))) return self._add_channel(new_channel)
[docs] def add_channel_soak(self,channel_name): '''channel_name represents soak time setpoint in seconds. Soak timer runs while temperature is continuously within 'window' and resets to zero otherwise.''' new_channel = channel(channel_name,write_function=self._set_soak) new_channel.write(self.soak) return self._add_channel(new_channel)
[docs] def add_channel_window(self,channel_name): '''channel_name represents width setpoint of tolerance window to start soak timer. Setpoint is total window width in degrees (temp must be +/-window/2).''' new_channel = channel(channel_name,write_function=self._set_window) new_channel.write(self.window) return self._add_channel(new_channel)
[docs] def add_channel_soak_settling_time(self,channel_name): '''channel_name represents soak timer elapsed time readback.''' new_channel = channel(channel_name,read_function=lambda: self.time) return self._add_channel(new_channel)
[docs] def add_channel_max_air(self,channel_name): '''channel_name represents maximum airflow temperature setting.''' new_channel = channel(channel_name,write_function=self._set_max_air) new_channel.write(self.maxair) return self._add_channel(new_channel)
[docs] def add_channel_max_air2dut(self,channel_name): '''channel_name represents maximum allowed temperature difference between airflow and dut setting.''' new_channel = channel(channel_name,write_function=self._set_max_air2dut) new_channel.write(self.air2dut) return self._add_channel(new_channel)
def _set_max_air(self,value): self.air2dut = value def _set_max_air2dut(self,value): self.maxair = value def _set_window(self,value): '''Set allowed window to start soak timer.''' self.window = value txt = "WNDW " + str(self.window) self.get_interface().write(txt) def _set_soak(self,value): '''Set soak time in seconds''' self.soak = value txt = "SOAK " + str(self.soak) self.get_interface().write(txt)
[docs] def off(self): '''Turn off airflow and compressor, lift head, reset limits''' self.get_interface().write("FLOW 0;") self.get_interface().write("HEAD 0;") self.get_interface().write("COOL 0;") self.get_interface().write("ULIM {};".format(155))
def _write_temperature(self,value): '''Set temperature''' self.setpoint = value if value < 20: self.range = 2 elif value < 30: self.range = 1 else: self.range = 0 self.get_interface().write("SETN " + str(self.range)) txt = "SETP " + str(self.setpoint) + ";WNDW " + str(self.window) + ";ADMD " + str(self.air2dut) txt += ";ULIM " + str(self.maxair) + "; SOAK " + str(self.soak) +";" self.get_interface().write(txt) self.get_interface().write("FLOW 1") self.time = 0 self._wait_settle() def _wait_settle(self): '''Block until temperature has been within window for soak time.''' settled = False while(settled == False): time.sleep(5) self.time += 5 print "Waiting To Settle to " + str(self.setpoint) + " : "+ str(self.time) + "s", tecr = self.get_interface().ask("TECR?") if ((int(tecr) & 1) == 1): settled = True
class autonicstk(instrument): def __init__(self, interface_raw_serial, modbus_address): from PyICe.deps.modbus import minimalmodbus #minimalmodbus.BAUDRATE = 38400 minimalmodbus.BAUDRATE = 9600 self._base_name = 'Autonics TK Series PID' instrument.__init__(self,"Autonics PID @ {}:{}".format(interface_raw_serial,modbus_address)) #self.add_interface_raw_serial(interface_raw_serial,timeout=5) self.sp = interface_raw_serial self.modbus_address = modbus_address self.modbus_pid = minimalmodbus.Instrument(interface_raw_serial,modbus_address) self.modbus_pid.serial.stopbits = 1 self.modbus_pid.serial.timeout = 5 def add_basic_channels(self, channel_name): self.add_channel_setpoint(channel_name) self.add_channel_measured(channel_name) self.add_channel_enable_output(channel_name) def add_advanced_channels(self, channel_name): self.add_channel_mode(channel_name) self.add_channel_units(channel_name) self.add_channel_presets(channel_name) self.add_channel_heat_mv(channel_name) self.add_channel_cool_mv(channel_name) self.add_channel_alarm1(channel_name) self.add_channel_alarm2(channel_name) self.add_channel_autotune(channel_name) self.add_channels_tuning(channel_name) self.add_channel_sensor_type(channel_name) self.add_channels_alarm_config(channel_name) def get_decimal(self): return self.modbus_pid.read_register(1001,functioncode=4) def add_channel_measured(self, channel_name): '''Measured Temperature Readback (PV)''' new_register = integer_channel('{}_PV'.format(channel_name),size=16,read_function=self._read_temperature_sense) new_register.set_category('Measure') new_register.set_description(self.add_channel_measured.__doc__) return self._add_channel(new_register) def _read_temperature_sense(self): return self.modbus_pid.read_register(1000,numberOfDecimals=self.get_decimal(),functioncode=4,signed=True) def add_channel_units(self, channel_name): '''Select Celsius or Farenheit. CAUTION: Units also change PID gains.''' new_register = register('{}_Units'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(151,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(151,int(data),functioncode=6)) new_register.add_preset('Celsius',0) new_register.add_preset('Farenheit',1) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Set') new_register.set_description(self.add_channel_units.__doc__) return self._add_channel(new_register) def add_channel_setpoint(self, channel_name): '''Target Temperature Setpoint (SV)''' new_register = register(channel_name, size=16, read_function=lambda: self.retry(lambda: self.modbus_pid.read_register(0,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), retry_count=1), write_function=self._write_temperature) new_register.set_category('Set') new_register.set_description(self.add_channel_setpoint.__doc__) new_register.set_min_write_limit(-199) new_register.set_max_write_limit(165) return self._add_channel(new_register) def _write_temperature(self, value): self.modbus_pid.write_register(0,float(value),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True) def add_channel_heat_mv(self, channel_name): '''Heater percent power manipulated variable (MV). Can be written directly in manual mode.''' new_register = register('{}_MV_Heat'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(1,numberOfDecimals=1,functioncode=3,signed=False), write_function=lambda data: self.modbus_pid.write_register(1,float(data),numberOfDecimals=1,functioncode=6,signed=False)) new_register.set_category('Heat') new_register.set_description(self.add_channel_heat_mv.__doc__) return self._add_channel(new_register) def add_channel_cool_mv(self, channel_name): '''Cooler percent power manipulated variable (MV). Can be written directly in manual mode.''' new_register = register('{}_MV_Cool'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(2,numberOfDecimals=1,functioncode=3,signed=False), write_function=lambda data: self.modbus_pid.write_register(2,float(data),numberOfDecimals=1,functioncode=6,signed=False)) new_register.set_category('Cool') new_register.set_description(self.add_channel_cool_mv.__doc__) return self._add_channel(new_register) def add_channel_mode(self, channel_name): '''Automatic/Manual mode selector. Automatic uses temperature setpoint (SV). Manual uses heat and cool MV setpoints.''' new_register = register('{}_Mode'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(3,functioncode=3,signed=False), write_function=lambda data: self.modbus_pid.write_register(3,int(data),functioncode=6,signed=False)) new_register.add_preset('Auto',0) new_register.add_preset('Manual',1) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Enables') new_register.set_description(self.add_channel_mode.__doc__) return self._add_channel(new_register) def add_channel_presets(self, channel_name): '''Select one of 4 pre-selected temperatures.''' new_register = register('{}_Preset'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(51,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(51,int(data),functioncode=6)) #new_register.add_preset('Beef',0) #new_register.add_preset('Pork',1) #new_register.add_preset('Vegetable',2) #new_register.add_preset('Yogurt',3) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Set') new_register.set_description(self.add_channel_presets.__doc__) return self._add_channel(new_register) def add_channel_alarm1(self, channel_name): '''Alarm 1 output status.''' new_register = integer_channel('{}_Alarm1'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(1006,functioncode=4)>>9&1) new_register.add_preset('OK',0) new_register.add_preset('Alarm',1) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Alarm1') new_register.set_description(self.add_channel_alarm1.__doc__) return self._add_channel(new_register) def add_channel_alarm2(self, channel_name): '''Alarm 2 output status.''' new_register = integer_channel('{}_Alarm2'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(1006,functioncode=4)>>10&1) new_register.add_preset('OK',0) new_register.add_preset('Alarm',1) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Alarm2') new_register.set_description(self.add_channel_alarm2.__doc__) return self._add_channel(new_register) def add_channel_enable_output(self, channel_name): '''Enable/Disable heat and cool outputs.''' new_register = register('{}_enable'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(50,functioncode=3), write_function=self._enable) new_register.add_preset('Run',0) new_register.add_preset('Stop',1) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Enables') new_register.set_description(self.add_channel_enable_output.__doc__) return self._add_channel(new_register) def _enable(self, enable): self.modbus_pid.write_register(50,0 if enable else 1,functioncode=6) def add_channel_heat_cool_mode(self, channel_name): '''enable heat only, cool only or heat-cool mode''' new_register = register('{}_heat_cool_mode'.format(channel_name), size=2, read_function=lambda: self.modbus_pid.read_register(162,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(162,int(data),functioncode=6)) new_register.add_preset('Heat',0) new_register.add_preset('Cool',1) new_register.add_preset('Heat-Cool',2) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Enables') new_register.set_description(self.add_channel_heat_cool_mode.__doc__) return self._add_channel(new_register) def add_channel_autotune(self, channel_name): '''Start autotune sequence.''' new_register = register('{}_Autotune'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(100,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(100,int(data),functioncode=6)) new_register.add_preset('Run',1) new_register.add_preset('Stop',0) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('PID_tuning') new_register.set_description(self.add_channel_autotune.__doc__) return self._add_channel(new_register) def add_channels_tuning(self, channel_name): '''PID control gain settings. See: https://en.wikipedia.org/wiki/PID_controller#Alternative_nomenclature_and_PID_forms''' new_register = register('{}_Heat_Proportional'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(101,numberOfDecimals=1,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(101,float(data),numberOfDecimals=1,functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Reciprocal heat proportional gain. Number of degrees over which the heat output transistion from 0%-100% output due to proportional error alone.') self._add_channel(new_register) new_register = register('{}_Cool_Proportional'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(102,numberOfDecimals=1,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(102,float(data),numberOfDecimals=1,functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Reciprocal cool proportional gain. Number of degrees over which the heat output transistion from 100%-0% output due to proportional error alone.') self._add_channel(new_register) new_register = register('{}_Heat_Integral'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(103,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(103,int(data),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Heat integral time. Number of seconds over which the (measured) process variable will change from proportial residual error to zero error.') self._add_channel(new_register) new_register = register('{}_Cool_Integral'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(104,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(104,int(data),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Cool integral time. Number of seconds over which the (measured) process variable will change from proportial residual error to zero error.') self._add_channel(new_register) new_register = register('{}_Heat_Derivative'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(105,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(105,int(data),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Heat derivitive time. Scaling constant to map degrees/s (rate) into degrees of apparent error.') self._add_channel(new_register) new_register = register('{}_Cool_Derivative'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(106,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(106,int(data),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Cool derivitive time. Scaling constant to map degrees/s (rate) into degrees of apparent error.') self._add_channel(new_register) new_register = register('{}_Heat_Hysteresis'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(109,numberOfDecimals=self.get_decimal(),functioncode=3), write_function=lambda data: self.modbus_pid.write_register(109,float(data),numberOfDecimals=self.get_decimal(),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Heat hysteresis. Dead band for heat loop only. Not used with PID proportional output mode.') self._add_channel(new_register) new_register = register('{}_Cool_Hysteresis'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(111,numberOfDecimals=self.get_decimal(),functioncode=3), write_function=lambda data: self.modbus_pid.write_register(111,float(data),numberOfDecimals=self.get_decimal(),functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description('Cool hysteresis. Dead band for cool loop only. Not used with PID proportional output mode.') self._add_channel(new_register) new_register = register('{}_Deadband'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(107,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), write_function=lambda data: self.modbus_pid.write_register(107,float(data),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True)) new_register.set_category('PID_tuning') new_register.set_min_write_limit(-199) new_register.set_max_write_limit(150) new_register.set_description('Dead band between heat and cool loops. Negative value causes class-A operation.') self._add_channel(new_register) new_register = register('{}_SSR1_Mode'.format(channel_name), size=2, read_function=lambda: self.modbus_pid.read_register(166,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(166,int(data),functioncode=6)) new_register.add_preset('Standard',0) new_register.add_preset('Cycle',1) new_register.add_preset('Phase',2) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('PID_tuning') new_register.set_description("Heat PWM mode. 'Standard' uses 'Heating_Control_Time' setting. 'Cycle' powers/unpowers the heater over whole power line cycles. 'Phase' synchronously dims the heater by delaying turn-on during each power line cycle.") self._add_channel(new_register) new_register = register('{}_Heating_Control_Time'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(170,numberOfDecimals=1,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(170,float(data),numberOfDecimals=1,functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description("Heat control PWM period when in 'Standard' SSR1_Mode.") self._add_channel(new_register) new_register = register('{}_Cooling_Control_Time'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(171,numberOfDecimals=1,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(171,float(data),numberOfDecimals=1,functioncode=6)) new_register.set_category('PID_tuning') new_register.set_description("Cool control PWM period.") self._add_channel(new_register) def add_channel_sensor_type(self, channel_name): new_register = register('{}_Sensor'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(150,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(150,int(data),functioncode=6)) new_register.add_preset('Thermocouple_K_high',0) new_register.add_preset('Thermocouple_K_low',1) new_register.add_preset('DIN_Pt100_high',24) new_register.add_preset('DIN_Pt100_low',25) #new_register.add_preset('PT100',0) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('PID_tuning') new_register.set_description('Temperature sensor type selection.') self._add_channel(new_register) def _write_alarm1_high(self, data): self.modbus_pid.write_register(54,float(data),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True) def _write_alarm2_high(self, data): self.modbus_pid.write_register(56,float(data),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True) def add_channels_alarm_config(self, channel_name): new_register = register('{}_Alarm1_low_limit'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(53,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), write_function=lambda data: self.modbus_pid.write_register(53,float(data),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True)) new_register.set_category('Alarm_config') new_register.set_min_write_limit(-199) new_register.set_max_write_limit(500) self._add_channel(new_register) new_register = register('{}_Alarm1_high_limit'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(54,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), write_function=self._write_alarm1_high) new_register.set_category('Alarm_config') new_register.set_min_write_limit(-199) new_register.set_max_write_limit(500) self._add_channel(new_register) new_register = register('{}_Alarm1_hysteresis'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(202,numberOfDecimals=self.get_decimal(),functioncode=3), write_function=lambda data: self.modbus_pid.write_register(202,float(data),numberOfDecimals=self.get_decimal(),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm1_Relay'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(203,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(203,int(data),functioncode=6)) new_register.set_category('Alarm_config') new_register.add_preset('Normally Open',0) new_register.add_preset('Normally Closed',1) new_register.use_presets_read(True) new_register.use_presets_write(True) self._add_channel(new_register) new_register = register('{}_Alarm1_on_delay'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(204,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(204,int(data),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm1_off_delay'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(205,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(205,int(data),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm1_mode'.format(channel_name), size=4, read_function=lambda: self.modbus_pid.read_register(200,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(200,int(data),functioncode=6)) new_register.add_preset('Off',0) new_register.add_preset('Deviation High Limit',1) new_register.add_preset('Deviation Low Limit',2) new_register.add_preset('Deviation High/Low Limit',3) new_register.add_preset('Deviation High/Low Limit Reverse',4) new_register.add_preset('Absolute High Limit',5) new_register.add_preset('Absolute Low Limit',6) new_register.add_preset('Loop Break',7) new_register.add_preset('Sensor Break',8) new_register.add_preset('Heater Burnout',9) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm2_low_limit'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(55,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), write_function=lambda data: self.modbus_pid.write_register(55,float(data),numberOfDecimals=self.get_decimal(),functioncode=6,signed=True)) new_register.set_category('Alarm_config') new_register.set_min_write_limit(-199) new_register.set_max_write_limit(500) self._add_channel(new_register) new_register = register('{}_Alarm2_high_limit'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(56,numberOfDecimals=self.get_decimal(),functioncode=3,signed=True), write_function=self._write_alarm2_high) new_register.set_category('Alarm_config') new_register.set_min_write_limit(-199) new_register.set_max_write_limit(500) self._add_channel(new_register) new_register = register('{}_Alarm2_hysteresis'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(208,numberOfDecimals=self.get_decimal(),functioncode=3), write_function=lambda data: self.modbus_pid.write_register(208,float(data),numberOfDecimals=self.get_decimal(),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm2_Relay'.format(channel_name), size=1, read_function=lambda: self.modbus_pid.read_register(209,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(209,int(data),functioncode=6)) new_register.set_category('Alarm_config') new_register.add_preset('Normally Open',0) new_register.add_preset('Normally Closed',1) new_register.use_presets_read(True) new_register.use_presets_write(True) self._add_channel(new_register) new_register = register('{}_Alarm2_on_delay'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(210,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(210,int(data),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm2_off_delay'.format(channel_name), size=16, read_function=lambda: self.modbus_pid.read_register(211,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(211,int(data),functioncode=6)) new_register.set_category('Alarm_config') self._add_channel(new_register) new_register = register('{}_Alarm2_mode'.format(channel_name), size=4, read_function=lambda: self.modbus_pid.read_register(206,functioncode=3), write_function=lambda data: self.modbus_pid.write_register(206,int(data),functioncode=6)) new_register.add_preset('Off',0) new_register.add_preset('Deviation High Limit',1) new_register.add_preset('Deviation Low Limit',2) new_register.add_preset('Deviation High/Low Limit',3) new_register.add_preset('Deviation High/Low Limit Reverse',4) new_register.add_preset('Absolute High Limit',5) new_register.add_preset('Absolute Low Limit',6) new_register.add_preset('Loop Break',7) new_register.add_preset('Sensor Break',8) new_register.add_preset('Heater Burnout',9) new_register.use_presets_read(True) new_register.use_presets_write(True) new_register.set_category('Alarm_config') self._add_channel(new_register) def retry(self, cmd, retry_count): try: return cmd() except Exception as e: if retry_count > 0: print e print 'Flushed: {}'.format(self.flush()) self.retry(cmd, retry_count=retry_count-1) else: print 'Flushed: {}'.format(self.flush()) raise e def flush(self): return self.modbus_pid.serial.read(self.modbus_pid.serial.inWaiting()) class modbus_relay(instrument): def __init__(self, serial_port, modbus_address): from PyICe.deps.modbus import minimalmodbus minimalmodbus.BAUDRATE = 9600 minimalmodbus.TIMEOUT = 5 self._base_name = '2-channel Modbus RTU relay' instrument.__init__(self,"Modbus Dual Relay @ {}:{}".format(serial_port, modbus_address)) self.modbus_relay = minimalmodbus.Instrument(serial_port, modbus_address) #self.modbus_relay.debug = True #self.modbus_relay.serial.stopbits = 1 #self.modbus_relay.serial.timeout = 1 def add_channel_relay1(self, channel_name='relay1'): new_register = register(channel_name, size=1, read_function=lambda: self.modbus_relay.read_register(registeraddress=1, functioncode=3), write_function=lambda data, relay_number=1: self._write_relay(data, relay_number)) new_register.set_category('relay') return self._add_channel(new_register) def add_channel_relay2(self, channel_name='relay2'): new_register = register(channel_name, size=1, read_function=lambda: self.modbus_relay.read_register(registeraddress=2, functioncode=3), write_function=lambda data, relay_number=2: self._write_relay(data, relay_number)) new_register.set_category('relay') return self._add_channel(new_register) def _write_relay(self, data, relay_number): self.modbus_relay.write_register(registeraddress=relay_number, value=int(256 if data else 512), functioncode=6) def flush(self): return self.modbus_relay.serial.read(self.modbus_relay.serial.inWaiting())
[docs]class temperature_chamber(instrument): '''generic temperature chamber parent class to handle common tasks''' __metaclass__ = ABCMeta def __init__(self): instrument.__init__(self,self._base_name) self.setpoint = None self.soak = 450 self.settle_time_limit = None self.window = 2.5 self.time = 0 self.set_blocking_mode(True)
[docs] def add_channel(self,channel_name,add_extended_channels=True): '''Helper method to add most commonly used channels. channel_name represents temperature setpoint. optionally also adds _sense, _soak, _window, and _soak_settling_time channels.''' temp_ch = self.add_channel_temp(channel_name) if add_extended_channels: self.add_channel_sense(channel_name + "_sense") self.add_channel_soak(channel_name + "_soak") self.add_channel_window(channel_name + "_window") self.add_channel_soak_settling_time(channel_name + "_soak_settling_time") self.add_channel_blocking(channel_name + "_blocking") self.add_channel_enable(channel_name + "_enable") return temp_ch
[docs] def add_channel_temp(self,channel_name): '''Channel_name represents PID loop forcing temperature setpoint.''' new_channel = channel(channel_name,write_function=self._write_temperature) new_channel.set_description(self.get_name() + ': ' + self.add_channel_temp.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_sense(self,channel_name): '''channel_name represents primary PID control loop thermocouple readback.''' new_channel = channel(channel_name,read_function=self._read_temperature_sense) new_channel.set_description(self.get_name() + ': ' + self.add_channel_sense.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_soak(self,channel_name): '''channel_name represents soak time setpoint in seconds. Soak timer runs while temperature is continuously within 'window' and resets to zero otherwise.''' new_channel = channel(channel_name,write_function=self._set_soak) new_channel.write(self.soak) new_channel.set_description(self.get_name() + ': ' + self.add_channel_soak.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_window(self,channel_name): '''channel_name represents width setpoint of tolerance window to start soak timer. Setpoint is total window width in degrees (temp must be +/-window/2).''' new_channel = channel(channel_name,write_function=self._set_window) new_channel.write(self.window) new_channel.set_description(self.get_name() + ': ' + self.add_channel_window.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_soak_settling_time(self,channel_name): '''channel_name represents soak timer elapsed time readback.''' new_channel = channel(channel_name,read_function=lambda: self.time ) new_channel.set_description(self.get_name() + ': ' + self.add_channel_soak_settling_time.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_settle_time_limit(self, channel_name): '''channel_name represents max time to wait for oven to settle to within window before raising Exception.''' new_channel = channel(channel_name,write_function=self._set_settle_time_limit) new_channel.write(self.settle_time_limit) new_channel.set_description(self.get_name() + ': ' + self.add_channel_settle_time_limit.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_blocking(self,channel_name): '''allow Python to continue immediately for gui/interactive use without waiting for slew/settle''' new_channel = integer_channel(channel_name,size=1, write_function=self.set_blocking_mode) new_channel.set_description(self.get_name() + ': ' + self.add_channel_blocking.__doc__) new_channel.write(self._blocking) return self._add_channel(new_channel)
[docs] def add_channel_enable(self,channel_name): '''channel name represents oven enable/disable setting. Accepts boolean and True enables the oven. Heat and cool only settings also accepted if temperature chamber supports that.''' new_channel = integer_channel(channel_name,size=2,write_function=self._enable) new_channel.set_description(self.get_name() + ': ' + self.add_channel_enable.__doc__) new_channel.add_preset('True', True) new_channel.add_preset('False', False) new_channel.add_preset('Heat_only', 2) new_channel.add_preset('Cool_only', 3) return self._add_channel(new_channel)
[docs] def set_blocking_mode(self,blocking): '''allow Python to continue immediately for gui/interactive use without waiting for slew/settle''' self._blocking = blocking
def _set_window(self,value): '''Set allowed window to start soak timer.''' self.window = value def _set_settle_time_limit(self, seconds): '''set oven to standby and raise exception if oven does not settle within specified time''' self.settle_time_limit = seconds def _set_soak(self,value): '''Set soak time in seconds''' self.soak = value def _wait_settle(self): '''Block until temperature has been within window for soak time. Optionally abort, set oven to standby, and raise exception if oven temp fails to converge in specified time''' if not self._blocking: return settled = 0 self.time = 0 progress_chars = ['-', '\\', '|', '/'] while(settled <= self.soak): time.sleep(1) # 1 second delay temp_current = self._read_temperature_sense() #print "\rSettling {}/{}s Current Temp:{}°C Target Temp:{}±{}°C Total time this setting:{}s {}".format(settled,self.soak,temp_current,self.setpoint,self.window/2.0,self.time,progress_chars[self.time%len(progress_chars)]), #comma supresses newline sys.stdout.flush() #print doesn't make it to screen in a timely fashion without newline; windows OEM codepage doesn't support UNICODE print "\rSettling {}/{}s Current Temp:{:3.1f}C Target Temp:{:3.1f}+/-{:3.1f}C Total time this setting:{:3d}s {}".format(settled,self.soak,temp_current,self.setpoint,self.window/2.0,self.time,progress_chars[self.time%len(progress_chars)]), #comma supresses newline sys.stdout.flush() #print doesn't make it to screen in a timely fashion without newline window_upper = float(self.setpoint) + float(self.window)/2 window_lower = float(self.setpoint) - float(self.window)/2 if (float(temp_current) < window_upper) and (float(temp_current) > window_lower): settled += 1 else: settled = 0 if (self.settle_time_limit is not None and self.time > self.settle_time_limit): self._enable(False) print raise Exception('Oven failed to settle to {}C in {} seconds. Final Temp: {}C.\n Oven Disabled. Test aborted.'.format(self.setpoint, self.time, temp_current)) self.time += 1 print #newline avoids overwriting oven status message with next print @abstractmethod def _write_temperature(self, value): '''Program tempertaure setpoint to value. Implement for specific hardware.''' self.setpoint = value @abstractmethod def _read_temperature_sense(self): '''read back actual chamber temperature. Implement for specific hardware.''' @abstractmethod def _enable(self, enable): '''enable/disable temperature chamber heating and cooling. Also accepts heat/cool only arguments if chamber supports it.'''
[docs] def shutdown(self, shutdown): '''separate method to turn off temperature chamber. overload if possible for individual hardware. otherwise, default to disable heating and cooling. ''' self._enable(not shutdown)
[docs]class delta_9039(temperature_chamber): '''single channel delta 9039 oven use wait_settle to wait for the soak to complete defaults to window = 1, soak=90 extra data _sense - the sensed temperature _window - the temperature window _time - the total settling time (including soak) _soak - the programmed soak time''' def __init__(self,interface_visa): self._base_name = 'delta_9039' temperature_chamber.__init__(self) self.add_interface_visa(interface_visa) self._enable(False) time.sleep(1) def _write_temperature(self,value): '''Set named channel to new temperature "value"''' self.setpoint = value self._enable(False) time.sleep(1) self.get_interface().write("SEtpoint " + str(self.setpoint)) time.sleep(1) self._enable(True) self.time = 0 self._wait_settle() def _read_temperature_sense(self): '''read back actual chamber temperature''' return float(self.get_interface().ask("Temperature?")) def _enable(self, enable): '''enable/disable temperature chamber heating and cooling''' if enable: self.get_interface().write("Active") else: self.get_interface().write("STANdby")
[docs]class sun_ecxx(temperature_chamber): '''sun ecXx oven instrument base class implements all methods common to sun ec0x and ec1x ovens''' def __init__(self,interface_visa): temperature_chamber.__init__(self) self.add_interface_visa(interface_visa) def _read_temperature_sense(self): '''read back actual chamber temperature''' return float(self.get_interface().ask("TEMP?"))
[docs]class sun_ec0x(sun_ecxx): '''sun ec0 oven use wait_settle to wait for the soak to complete defaults to window = 1, soak=90 extra data _sense - the sensed temperature _window - the temperature window _time - the total settling time (including soak) _soak - the programmed soak time''' def __init__(self,interface_visa): #instrument.__init__(self,"sun_ec0x @ {}".format(interface_visa)) self._base_name = 'sun_ec0x' sun_ecxx.__init__(self,interface_visa) def _write_temperature(self,value): '''Set named channel to new temperature "value"''' #self._standby() self.setpoint = value time.sleep(1) self.get_interface().write(str(value)+"C") time.sleep(1) #self._active() self.time = 0 self._wait_settle() #def _enable(self, enable): # ''''enable temperature chamber heating and cooling''' # raise Exception('unimplemented. fix me!')
[docs]class sun_ec1x(sun_ecxx): '''sun ec1x oven use wait_settle to wait for the soak to complete defaults to window = 1, soak=90 extra data _sense - the sensed temperature _window - the temperature window _time - the total settling time (including soak) _soak - the programmed soak time upper_temp_limit (default 165) and lower_temp_limit (default -65) can be modified as properties of the sun_ec1x object outside the PyICe channel framework''' def __init__(self,interface_visa): self._base_name = 'sun_ec1x' sun_ecxx.__init__(self,interface_visa) self.upper_temp_limit = 165 self.lower_temp_limit = -65 self.get_interface().write('SINT=NNNNNNNNNN0') time.sleep(1) slag = self.get_interface().resync() print "Flushed {} characters: {}.".format(len(slag),slag) self.shutdown(False) self._enable(True)
[docs] def add_channel_user_sense(self,channel_name): '''channel_name represents secondary non-control thermocouple readback.''' new_channel = channel(channel_name,read_function=lambda: float(self.get_interface().ask("UCHAN?"))) new_channel.set_description(self.get_name() + ': ' + self.add_channel_user_sense.__doc__) return self._add_channel(new_channel)
def _write_temperature(self,value): '''Set named channel to new temperature "value"''' self.setpoint = value time.sleep(1) self.get_interface().write("SET={}".format(value)) time.sleep(1) self.time = 0 self._wait_settle() def _enable(self, enable): '''individually control heat/cool outputs. Usually used through channel framework''' if enable==False or enable==0: time.sleep(0.5) self.get_interface().write('HOFF') time.sleep(0.5) self.get_interface().write('COFF') time.sleep(0.5) elif enable==True or enable==1: time.sleep(0.5) self.get_interface().write('HON') time.sleep(0.5) self.get_interface().write('CON') time.sleep(0.5) elif enable==2: #heat only time.sleep(0.5) self.get_interface().write('HON') time.sleep(0.5) self.get_interface().write('COFF') time.sleep(0.5) elif enable==3: #cool only time.sleep(0.5) self.get_interface().write('HOFF') time.sleep(0.5) self.get_interface().write('CON') time.sleep(0.5) else: raise Exception('Unknown oven enable value: {}'.format(enable))
[docs] def shutdown(self, shutdown): '''turn entire temp controller on or off. This is different than enabling/disabling the heat and cool outputs''' if shutdown: time.sleep(0.5) self.get_interface().write('OFF') time.sleep(0.5) else: time.sleep(0.5) self.get_interface().write('ON') time.sleep(0.5)
upper_temp_limit = property(lambda self: float(self.get_interface().ask('UTL?')), lambda self,temp: self.get_interface().write('UTL={}'.format(temp))) lower_temp_limit = property(lambda self: float(self.get_interface().ask('LTL?')), lambda self,temp: self.get_interface().write('LTL={}'.format(temp)))
[docs]class Franken_oven(autonicstk, modbus_relay, temperature_chamber): '''Autonics controlled temperature chamber with Modbus remote-power relay''' def __init__(self, interface_raw_serial): from PyICe.deps.modbus import minimalmodbus minimalmodbus.BAUDRATE = 9600 minimalmodbus.TIMEOUT = 5 self._base_name = 'Autonics_Oven' #instrument.__init__(self,"Autonics PID @ {}:1; relay @ {}:2".format(interface_raw_serial,interface_raw_serial)) temperature_chamber.__init__(self) self.add_interface_raw_serial(interface_raw_serial) self.modbus_pid = minimalmodbus.Instrument(interface_raw_serial,slaveaddress=1) self.modbus_relay = minimalmodbus.Instrument(interface_raw_serial,slaveaddress=2) self.shutdown(False) self._enable(False) # def add_channels(self, channel_name): # self.add_channel_power_relay('{}_power_relay'.format(channel_name)) # self.add_basic_channels(channel_name) def add_channel(self, channel_name,add_extended_channels=True): temp_channel = temperature_chamber.add_channel(self, channel_name, add_extended_channels) if add_extended_channels: self.add_channel_power_relay('{}_power_relay'.format(channel_name)) return temp_channel def add_channel_power_relay(self, channel_name): self.add_channel_relay1(channel_name).set_category(self._base_name) def _enable(self, enable): autonicstk._enable(self, enable) #The following uses the Alarm1 output relay to enable/disable the heat SSR and the Alarm2 output relay to enable/disable the cool SSR #Autonics Alarm wiring requires than modes both be set to "Absolute High Limit" if enable == 1: #heat/cool autonicstk._write_alarm1_high(self, -199) #alarm1 enables heat autonicstk._write_alarm2_high(self, -199) #alarm2 enables cool if enable == 2: #heat only autonicstk._write_alarm1_high(self, -199) #alarm1 enables heat autonicstk._write_alarm2_high(self, 300) #~alarm2 disables cool elif enable == 3: #cool only autonicstk._write_alarm1_high(self, 300) #~alarm1 disables heat autonicstk._write_alarm2_high(self, -199) #alarm2 enables cool self._enabled = enable def _write_temperature(self, value): self.setpoint = value autonicstk._write_temperature(self, value) if not self._enabled: self._enable(True) self._wait_settle()
[docs] def shutdown(self, shutdown): '''turn entire temp controller on or off with modbus relay.''' self._write_relay(not shutdown, relay_number=1) # class oven_w4sd(instrument): # '''Burlington, VT Modbus oven. Somebody in VT needs to document this instrument better. # This instrument needs lots of help. It looks like its mostly used through set_temp which as it is structured is incompatible with the channel setup # most of the arguments to set temp need to be properties/attributes of this object so it can be used as a channel without sending all that crap. # also, consider using PyICe.deps.modbus.minimalmodbus instead of implementing RTU protocol from scratch within driver # inheritance from temperature_chamber may simplify implementation''' # def __init__(self,interface_raw_serial,address): # raise Exception("Oven_w4sd does not meet minimum quality requirements, do not use, I will try to fix this later, if someone will tell me how to use it.") # '''what do you give this a string or an object for the serial_port''' # self._base_name = 'oven_w4sd' # instrument.__init__(self,"oven_w4sd @ {}".format(interface_raw_serial)) # self.address=address # this is the modbus address, not the serial port # self.add_interface_raw_serial(interface_raw_serial) # self.get_interface().term_chars = "" # def add_channel(self,channel_name): # self.add_channel_temp(channel_name) # #self.add_channel_sense(channel_name + "_sense") #need to write this method! # def add_channel_temp(self,channel_name): # '''Temp forcing channel.''' # new_channel = channel(channel_name,write_function=self._write_temperature) # self._add_channel(new_channel) # def _write_temperature(self,value): # value = int(value) # if (value < -60): # print("WARNING: Temperature below -60C not allowed: " + str(value)) # print("Setting temperature to -60C instead.") # value = -60 # self.negtemp = 0xFF # elif -60 <= value < 0: # self.negtemp = 0xFF # elif 0 <= value <= 180: # self.negtemp = 0x00 # else: # print("WARNING: Temperature above 180C not allowed: " + str(value)) # print("Setting Temperature to 180C instead.") # value = 180 # self.negtemp = 0x00 # #find twos comp for neg temp # if value < 0: #check for neg temp # value = abs(value) #remove sign # value ^= 0xFF #Restricted to unsigned 8bit but python is 32bit, so convert # value = value + 0x01 #xor and add one. exp. -1, two's complement # self.modbus_write.write(300,self.negtemp,value) # def off(self): # self._write_temperature(25) # def set_temp(self,temp_setting,lb,temp_meas_channel="temp_meas",settletime=120,\ # settle_limit_temp_meas=2.0,oven_channel=None): # # method for setting oven temperature and waiting to settle # # default: settling is defined as temp_meas staying within +/- 2 for 120 sec # # reads temperature (channel name default "temp_meas") # # temp_meas can be a thermocouple or die temp reading, etc, but must have units of degrees C # # 30 min max settling time, or 2X settle time, whichever is longer # # returns true if temperature settles in time limit, otherwise false # time_limit = max(30*60.0,2*settletime) # time_step = 15.0 # check temp every 15 seconds # settle_steps = int(float(settletime)/time_step) # init_force_delta = 5.0 # initially set oven 5C past target temp to speed up settling # init_force_delta_hot = 10.0 # initial setting 10C pas target for temp > 100 # settle_limit_temp_meas_init = 0 # force >0 overshoot # if oven_channel is None: # oven_channel = self.channels[0] # # # if temp_setting > float(lb.read_channel(temp_meas_channel)): # if temp_setting < 100: # temp_oven = temp_setting + init_force_delta # else: # temp_oven = temp_setting + init_force_delta_hot # else: # temp_oven = temp_setting - init_force_delta # self.write_channel(oven_channel,temp_oven) # start_time = time.time() # print("Monitoring temperature for settling") # settled = False # check_time = start_time # # initial temp drive only if off by more than 0.5*tolerance # if abs(float(lb.read_channel(temp_meas_channel)) - \ # (temp_setting-settle_limit_temp_meas_init)) > \ # 0.5*settle_limit_temp_meas: # while (time.time() < start_time + time_limit) and not settled: # check_time += time_step # while time.time() < check_time: # pass # if temp_oven > temp_setting: # #ramping up, check for > setting-delta # settled = float(lb.read_channel(temp_meas_channel)) \ # > (temp_setting-settle_limit_temp_meas_init) # else: # #ramping down, check for < setting+delta # settled = float(lb.read_channel(temp_meas_channel)) \ # < (temp_setting+settle_limit_temp_meas_init) # # now adjust the final oven setting empirically # if temp_setting < 0: # temp_oven = temp_setting - 3.0 # elif temp_setting < 50: # temp_oven = temp_setting # elif temp_setting < 100: # temp_oven = temp_setting + 2.0 # else: # temp_oven = temp_setting + 3.0 # self.write_channel(oven_channel,temp_oven) # # monitor for settling # settled = self.monitor_temp_settle(temp_setting,start_time,time_limit,time_step,settle_steps,\ # oven_channel,temp_meas_channel,settle_limit_temp_meas,lb) # return settled # def monitor_temp_settle(self,temp_setting,start_time,time_limit,time_step,settle_steps,\ # oven_channel,temp_meas_channel,settle_limit_temp_meas,lb): # check_time = time.time() # temp_meas_list = [] # settled = False # while (time.time() < start_time + time_limit) and not settled: # check_time += time_step # #print("Waiting "+str(time_step)+" sec before checking temp") # while time.time() < check_time: # pass # temp_meas = float(lb.read_channel(temp_meas_channel)) # print("temp_meas: "+str(round(temp_meas,1))+"C") # if abs(temp_meas-temp_setting) > settle_limit_temp_meas: # temp_meas_list = [] # temp_oven = self.read_channel(oven_channel) # # detect major overshoot, set temperature to target if so # if ((temp_meas < temp_setting-settle_limit_temp_meas) \ # and (temp_oven < temp_setting)) or \ # ((temp_meas > temp_setting+settle_limit_temp_meas) \ # and (temp_oven > temp_setting)): # print("NOTE: Temperature overshoot with magnitude greater than "\ # +str(settle_limit_temp_meas)+" detected.") # print("Setting oven temp = temp_setting") # self.write_channel(oven_channel,temp_setting) # else: # temp_meas_list.append(temp_meas) # if len(temp_meas_list) >= settle_steps: # settled = True # return settled # def modbus_write(self,reg,data1,data0): # regH = (reg & 0xFF00)>>8 # regL = reg & 0x00FF # moddata = [self.address, 0x06, regH, regL, data1, data0] # CRCReg = 0xFFFF # # first 4 numbers represent oven address register in oven # # last 2 number are determined below # for item in moddata: # CRCRegH = CRCReg & 0xFF00 # CRCRegL = CRCReg & 0x00FF # y = int(item) # y = y ^ CRCRegL # z = CRCRegH + y # # this is the 8X routine, CRC # count = 0 # while (count < 8): #do this 8 times, once for each bit # x = z & 1 #clear all but LSB # z = z >> 1 #rotate right once # if x == 1: #test bit # z = z^0xA001 #if bit a 1 then OR, else continue # count = count + 1 # CRCReg = z # moddata.append(CRCReg&0x00FF) # moddata.append((CRCReg&0xFF00)>>8) # sendstring = "" # for item in moddata: # sendstring += chr(item) # self.get_interface().write(sendstring)
[docs]class agilent_e4433b(instrument): '''Agilent E4433B Signal Generator''' def __init__(self,interface_visa): self._base_name = 'agilent_e4433b' instrument.__init__(self,"agilent_e4433b @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.get_interface().write("*RST") def add_channel(self,channel_name,add_extended_channels=True): new_channel = channel(channel_name,write_function=self.write_output) if add_extended_channels: self.add_channel_freq(channel_name + "_freq") self.add_channel_power(channel_name + "_power") return self._add_channel(new_channel) def write_output(self,freq,power): self._write_power(power) self._write_freq(freq) def _write_power(self,channel_name,power): self.get_interface().write("POWER " + str(power) + " DBM") def _write_freq(self,channel_name,freq): self.get_interface().write("FREQuency " + str(freq) + "MHZ") def add_channel_freq(self, channel_name): freq_channel = channel(channel_name,read_function=self.read_freq) return self._add_channel(freq_channel) def add_channel_power(self, channel_name): power_channel = channel(channel_name,read_function=self.read_power) return self._add_channel(power_channel) def read_freq(self): return self.get_interface().ask("FREQ?") def read_power(self): return self.get_interface().ask("POWER?") def enable_output(self): self.get_interface().write("OUTP:STAT ON") def disable_output(self): self.get_interface().write("OUTP:STAT OFF")
[docs]class krohnhite_523(instrument): '''Krohn-Hite Model 523 Precision DC Source/Calibrator''' def __init__(self,interface_visa): self._base_name = 'krohnhite_523' instrument.__init__(self,"krohnhite_523 @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) #initialize to instrument on, value 0 self.float_lo_terminal() ## float the lo terminal by default self.disable_crowbar() ## turn off crowbar by default def ground_lo_terminal(self): self.get_interface().write("g") def float_lo_terminal(self): self.get_interface().write("f") def disable_crowbar(self): self.get_interface().write("v") def add_channel_voltage(self,channel_name): new_channel = channel(channel_name,write_function=self._write_voltage) return self._add_channel(new_channel) def add_channel_current(self,channel_name): new_channel = channel(channel_name,write_function=self._write_current) return self._add_channel(new_channel) def add_channel_voltage_compliance(self,channel_name): new_channel = channel(channel_name,write_function=self._write_compliance_voltage) return self._add_channel(new_channel) def _write_current(self,current): current = float(current) * 1000 ## current must be specified in mA self.current = float(current) self.get_interface().write("{}mA".format(current) ) def _write_voltage(self,voltage): voltage = float(voltage) if voltage < 1e-3: self.get_interface().write("{}uV".format(voltage*1e6)) elif voltage < 1.0: self.get_interface().write("{}mV".format(voltage*1e3)) else: self.get_interface().write("{}V".format(voltage)) def _write_compliance_voltage(self,compliance_voltage): compliance_voltage = int(float(compliance_voltage)) if 1 <= compliance_voltage and compliance_voltage <= 110: self.get_interface().write("{}C".format(compliance_voltage) ) else: raise Exception("Krohn-Hite 523: Invalid Compliance voltage: {}. Must be 1V to 110V".format(compliance_voltage) )
[docs]class krohnhite_526(instrument): '''Krohn-Hite Model 526 Precision DC Source/Calibrator Driver uses 526 protocol (other protocols do not support LAN operation) Voltage Ranges are +/- 0.1V, 1V, 10V, 100V Current Ranges are +/- 10mA, 100mA CHAR FN ASCII CODE 1 Polarity + = Positive 0 = Crowbar - = Negative 2 MSD 0 to 10 (use 'J' for decimal 10) 3 2SD 0 to 10 (use 'J' for decimal 10) 4 3SD 0 to 10 (use 'J' for decimal 10) 5 4SD 0 to 10 (use 'J' for decimal 10) 6 5SD 0 to 10 (use 'J' for decimal 10) 7 6SD 0 to 10 (use 'J' for decimal 10) 8 Range 0 = 100mV 1 = 1V 2 = 10V 3 = 100V 4 = 10mA 5 = 100mA 9(OPT) Sense 2 = 2-Wire Mode 4 = 4-Wire Mode ''' def __init__(self,interface_visa): self._base_name = 'Krohn-Hite 526' instrument.__init__(self,"{} @ {}".format(self._base_name, interface_visa) ) self.add_interface_visa(interface_visa) self._valid_vranges = {0.1:0, 1:1, 10:2, 100:3} self._valid_iranges = {0.01:4, 0.1:5} self.v_channel = None self.i_channel = None self._set_vrange(10) # 10 V default range self._set_irange(0.01) # 10 mA default range self._sense = '2' # default to 2-wire mode self._voltage = 0 self._current = 0
[docs] def add_channel_current(self,channel_name): '''Write channel to switch instrument to current mode and program current. Default current range is +/-10mA. Create a irange channel to adjust range to (10, 100)mA ''' self.i_channel = channel(channel_name,write_function=self._write_current) self.i_channel.set_max_write_limit(self._irange) self.i_channel.set_min_write_limit(-self._irange) return self._add_channel(self.i_channel)
[docs] def add_channel_irange(self,channel_name): '''Write channel to set current mode full scale range to +/-(10,100)mA. Won't take effect until the current is programmed.''' new_channel = channel(channel_name,write_function=self._set_irange) return self._add_channel(new_channel)
def _set_irange(self, range): if range not in self._valid_iranges: raise Exception('Current range {} invalid for instrument {}. Valid ranges are {}'.format(range,self.get_name(),self._valid_iranges.keys())) self._irange = range if self.i_channel is not None: self.i_channel.set_max_write_limit(self._irange) self.i_channel.set_min_write_limit(-self._irange) #write new range out to instrument here???? #print self._irange def _write_current(self,current): #determine polarity and produce _pol_char if current >= 0: _pol_char = '+' elif current <0: _pol_char = '-' #always 7 digits of magnitude, decimal point ignored, pad left side with zeros if self._irange == 0.01: cmd = '{:06.5f}'.format(current*1000) if current == 0.01: cmd = 'J' + '{:05.4f}'.format(current*1000-10) if current == -0.01: cmd = 'J' + '{:05.4f}'.format(current*1000+10) elif self._irange == 0.1: cmd = '{:07.5f}'.format(current*100) if current == 0.1: cmd = 'J' + '{:06.4f}'.format(current*100-10) if current == -0.1: cmd = 'J' + '{:06.4f}'.format(current*100+10) cmd = str(cmd) cmd = cmd.replace('.','' ) cmd = cmd.replace('-','' ) command = "{pol}{cmd}{rng}{sns}".format(pol=_pol_char,cmd=cmd,rng=self._valid_iranges[self._irange],sns=self._sense) self.get_interface().write(command) def add_channel_voltage(self,channel_name): self.v_channel = channel(channel_name,write_function=self._write_voltage) self.v_channel.set_max_write_limit(self._vrange) self.v_channel.set_min_write_limit(-self._vrange) return self._add_channel(self.v_channel)
[docs] def add_channel_vrange(self,channel_name): '''Write channel to set voltage mode full scale range to +/-(0.1,1,10,100)V or +/-(10,100)mA. Won't take effect until the voltage is programmed.''' new_channel = channel(channel_name,write_function=self._set_vrange) return self._add_channel(new_channel)
def _set_vrange(self, range): if range not in self._valid_vranges: raise Exception('Voltage range {} invalid for instrument {}. Valid ranges are {}'.format(range,self.get_name(),self._valid_vranges.keys())) self._vrange = range if self.v_channel is not None: self.v_channel.set_max_write_limit(self._vrange) self.v_channel.set_min_write_limit(-self._vrange) #write new range out to instrument here???? def _write_voltage(self,voltage): #determine polarity and produce _pol_char if voltage >= 0: _pol_char = '+' elif voltage <0: _pol_char = '-' #always 7 digits of magnitude, decimal point ignored, pad left side with zeros if self._vrange == 0.1: #seven digits after decimal cmd = '{:06.0f}'.format(voltage*10**7) if voltage == 0.1: cmd = 'J' + '{:05.0f}'.format(voltage*10**7-1000000) if voltage == -0.1: cmd = 'J' + '{:05.0f}'.format(voltage*10**7+1000000) elif self._vrange == 1: #two digits before decimal, five after cmd = '{:07.5f}'.format(voltage*10) if voltage == 1: cmd = 'J' + '{:06.4f}'.format(voltage*10-10) if voltage == -1: cmd = 'J' + '{:06.4f}'.format(voltage*10+10) elif self._vrange == 10: #one digit before decimal, five after cmd = '{:07.5f}'.format(voltage) if voltage == 10: cmd = 'J' + '{:06.4f}'.format(voltage-10) if voltage == -10: cmd = 'J' + '{:06.4f}'.format(voltage+10) elif self._vrange == 100: # #four digits before decimal, three after # cmd = '{:07.4f}'.format(voltage) # if voltage == 100: # cmd = 'J' + '{:06.4f}'.format(voltage-100) # if voltage == -100: # cmd = 'J' + '{:06.4f}'.format(voltage+100) #four digits before decimal, three after if (voltage<0)|(voltage>-10): cmd = '{:08.4f}'.format(voltage) elif voltage <= -10: cmd = '{:07.4f}'.format(voltage) if voltage >= 0: cmd = '{:07.4f}'.format(voltage) if voltage == 100: cmd = 'J' + '{:06.4f}'.format(voltage-100) if voltage == -100: cmd = 'J' + '{:06.4f}'.format(voltage+100) cmd = str(cmd) cmd = cmd.replace('.','' ) cmd = cmd.replace('-','' ) command = "{pol}{cmd}{rng}{sns}".format(pol=_pol_char,cmd=cmd,rng=self._valid_vranges[self._vrange],sns=self._sense) self.get_interface().write(command)
[docs]class data_precision_8200(instrument): '''Data Precision GPIB controlled precision DC Voltage/Current Source Voltage Ranges are +/- 1V, 10V, 100V, 1000V Current Range is +/- 100mA ''' def __init__(self,interface_visa): self._base_name = 'Data Precision 8200' instrument.__init__(self,"{} @ {}".format(self._base_name, interface_visa)) self.add_interface_visa(interface_visa) self._valid_vranges = (0.1,10,100,1000) self._vrange = 10 #volt self._voltage = None self._current = None self._mode = None def __del__(self): self.local_control() self.get_interface().close() def local_control(self): term_chars = self.get_interface().term_chars self.get_interface().term_chars = "" #need to remove any carriage return or line feed added by visa for this to work correctly self.get_interface().write("L") self.get_interface().term_chars = term_chars
[docs] def add_channel_voltage(self,channel_name): '''Write channel to switch instrument to voltage mode and program voltage. Default voltage range is +/-10V. Create a vrange channel to adjust range to (0.1,10,100,1000)V +/-100mV range has 0.1uV resolution, output impedance 100 Ohm +/-10V range has 10uV resolution, output impedance 10 mOhm, max current 100mA +/-100V range has 100uV resolution, output impedance 20 mOhm, max current 10mA +/-1000V range specifications are unknown ''' new_channel = channel(channel_name,write_function=self._write_voltage) return self._add_channel(new_channel)
[docs] def add_channel_current(self,channel_name): '''Write channel to switch instrument to current mode and program current between +/-100mA Voltage compliance is +/-10V''' new_channel = channel(channel_name,write_function=self._write_current) return self._add_channel(new_channel)
[docs] def add_channel_vrange(self,channel_name): '''Write channel to set voltage mode full scale range to +/-(0.1,10,100,1000)V Takes immediate effect when in voltage mode, cached until switch to voltage mode when in current mode ''' new_channel = channel(channel_name,write_function=self._set_vrange) return self._add_channel(new_channel)
[docs] def add_channel_mode(self,channel_name): '''Channel returns 'V' when in Voltage mode and 'A' when in current mode''' new_channel = channel(channel_name,read_function=self._get_mode) return self._add_channel(new_channel)
def _write_voltage(self,voltage): if voltage > self._vrange or voltage < -1*self._vrange: raise Exception('Voltage setting {}V invalid for instrument {} range {}V'.format(voltage,self.get_name(),self._vrange)) #always 7 digits of magnitude, decimal point ignored if self._vrange == 0.1: #seven digits after decimal cmd = 'V0{:+08.0f}'.format(voltage*10**7) elif self._vrange == 10: #two digits before decimal, five after cmd = 'V1{:+09.5f}'.format(voltage) elif self._vrange == 100: #three digits before decimal, four after cmd = 'V2{:+09.4f}'.format(voltage) elif self._vrange == 1000: #four digits before decimal, three after cmd = 'V3{:+09.3f}'.format(voltage) self._voltage = voltage self._current = None self._mode = 'V' self.get_interface().write(cmd) def _write_current(self,current): if current > 0.1 or current < -0.1: raise Exception('Current setting {}A invalid for instrument {} (+/-100mA max)'.format(current,self.get_name())) #always 6 digits of magnitude, decimal point ignored cmd = 'A{:+07.0f}'.format(current*10**6) self._voltage = None self._current = current self._mode = 'A' self.get_interface().write(cmd) def _set_vrange(self, range): if range in self._valid_vranges: self._vrange = range else: raise Exception('Voltage range {} invalid for instrument {}. Valid ranges are {}'.format(range,self.get_name(),self._valid_vranges)) if self._voltage is not None: self._write_voltage(self._voltage) def _get_mode(self): return self._mode
[docs]class agilent_35670a(scpi_instrument): '''Agilent 35670a 100kHz Signal Analyzer This driver is not complete and only supports noise measurements at this time.''' def __init__(self,interface_visa): '''interface_visa''' self._base_name = 'agilent_35670a' scpi_instrument.__init__(self,"a35670 @ " + str(interface_visa)) self.add_interface_visa(interface_visa) def add_channel_noise(self,channel_name,channel_num=1,freqs=[0,12.5,100,1000,10000,100000], res=[800,800,800,800,800], count=[150,600,600,600,600]): self.freqs = freqs self.res = res self.count = count self.progress_chars = ['-', '\\', '|', '/'] self.mask = 0b000100010000 self.timer=0 self.get_interface().write("SYSTem:PRESet") self.get_interface().write("INSTrument:SELect FFT") self.get_interface().write("INP2 OFF") self.get_interface().write("SENSe:REJect:STATe ON") self.get_interface().write("INPut1:LOW FLOat") self.get_interface().write("INPut1:COUPling DC") # self.interface.write("VOLTage1:RANGe:AUTO 1") self.get_interface().write("VOLTage1:RANGe 1.0 VPK") self.get_interface().write("SENSe:AVERage ON") self.get_interface().write("SENSe:AVERage:TYPE RMS") self.get_interface().write("SENSe:SWEep:OVERlap 0") self.get_interface().write("CALC1:UNIT:VOLTage \"V/RTHZ\"") self.get_interface().write("CALibration:AUTO ONCE") self.get_interface().write("ABORt;:INIT") self.get_interface().write("FORMat:DATA ASCii") self.add_channel(channel_name, channel_num)
[docs] def add_channel(self,channel_name,channel_num): '''Add named channel to instrument.''' meter_channel = channel(channel_name,read_function=lambda: self.read_channel(channel_num) ) return self._add_channel(meter_channel)
[docs] def read_channel(self,channel_num): '''Return float representing meter measurement. Units are {} etc depending on meter configuration.''' for ii in range(0,len(self.count)): print('Start : {}, Stop : {}' .format(self.freqs[ii], self.freqs[ii+1])) self.get_interface().write("SENSe:FREQuency:STARt " + str(self.freqs[ii])) self.get_interface().write("SENSe:FREQuency:STOP " + str(self.freqs[ii+1])) self.get_interface().write("SENSe:FREQuency:RESolution " + str(self.res[ii])) self.get_interface().write("SENSe:AVERage:COUNt " + str(self.count[ii])) self.get_interface().write("ABORt;:INIT:IMM") status = int(self.get_interface().ask("STATus:OPERation:CONDition?")) #condition? returns the sum of the decimal weights of all bits currently set to 1 i the operation status condition register #BIT(WEIGHT)Description #0(1)Calibrating #1(2)Settling #2(4)Ranging #3(8) #4(16)Measuring #5(32)Waiting for Trig #6(64)Waiting for Arm #7(128) #8(256)Averaging #9(512)Hardcopy in Progress #10(1024)Waiting for Accept/Reject #11(2048)Loading Waterfall #12(4096) #13(8192) #14(16384)Program Running time.sleep(1) test = int(status) & int(self.mask) #loop waiting for averaging to complete while test != 0: status = int(self.get_interface().ask("STATus:OPERation:CONDition?")) time.sleep(1) print "\r MEASURING {}" .format(self.progress_chars[self.timer%len(self.progress_chars)]), sys.stdout.flush() test = int(status) & int(self.mask) #will only equal zero if averaging is complete self.timer += 1 xdata = (self.get_interface().ask("CALC" + str(channel_num) + ":X:DATA?")) ydata = (self.get_interface().ask("CALC" + str(channel_num) + ":DATA?")) xdata = xdata.replace('+','') xlist = xdata.split(',') ylist = ydata.split(',') #An undocumented "feature" of the 35670A is that it returns more X data points than you have set up for #lines of resolution, while all you see on the display is one point per line of resolution continuously #connected. The extra data is aliased data that is not used in the display. Unfortunately, the programmer #has to take care of this data when the trace data is transferred across GPIB. #RES | Array Size #400 | 513 #800 | 1025 #1600 | 2049 #the if statements below take care of this test = len(ylist) if test == 101: xlist = xlist[:101] elif test == 201: xlist = xlist[:201] elif test == 401: xlist = xlist[:401] elif test == 801: xlist = xlist[:801] elif test == 1601: xlist = xlist[:1601] if ii == 0: xlist_final = xlist ylist_final = ylist elif ii > 0: xlist_final = xlist_final + xlist ylist_final = ylist_final + ylist dictionary = {"freq":xlist_final,"noise":ylist_final} return (dictionary)
[docs]class powermux(scpi_instrument): '''Boston Design Center 8x8 crosspoint relay mux + 4 aux channels, this needs an example of how to use AUX channels''' def __init__(self,interface_visa): self._base_name = 'powermux' scpi_instrument.__init__(self,"powermux @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.columns = {} self.columns["aux"] = 0 ## aux relays are treated as x=0 self.rows = {} self.board = 0 def add_channel_relay_names(self,channel_name,column_name,row_name): relay_channel = channel(channel_name,write_function=lambda closed: self.set_relay(column_name, row_name, closed) ) return self._add_channel(relay_channel) def add_channel_relay(self,channel_name,column_number,row_number): relay_channel = channel(channel_name,write_function=lambda closed: self._set_relay(column_number, row_number, closed) ) return self._add_channel(relay_channel)
[docs] def add_column(self,column_name,num): '''register named column. num is physical column number. valid range is [1-8] and [0] for auxiliary channels column "aux" is predefined ''' self.columns[column_name] = num
[docs] def add_row(self,row_name,num): '''register named row. num is physical row number. valid range is [1-8] and [1-4] for auxiliary channels column "aux" is predefined ''' self.rows[row_name] = num
[docs] def set_relay(self,column_name,row_name,closed): '''open and close a relay by row/column names''' if closed: self.close_relay(column_name,row_name) else: self.open_relay(column_name,row_name)
def _set_relay(self,column_number,row_number,closed): cmd = "{}{}{}".format(self.board,column_number,row_number) if closed: self.get_interface().write("CLOSe (@{})".format(cmd)) else: self.get_interface().write("OPEN (@{})".format(cmd))
[docs] def close_relay(self,column_name,row_name): '''close relay at named (column, row)''' self._set_relay(self.columns[column_name], self.rows[row_name], closed=True)
[docs] def open_relay(self,column_name,row_name): '''open relay at named (column, row)''' self._set_relay(self.columns[column_name], self.rows[row_name], closed=False)
def _set_relay_wdelay(self,delay,relay_list,closed): '''close or open list of relays at named (column, row) with delay between each''' if closed: command_string = "CLOSe" else: command_string = "OPEN" command_string += ":DELay (@{}".format(delay) for relay in relay_list: column_number = relay[0] row_number = relay[1] command_string += ",{}{}{}".format(self.board,column_number,row_number) command_string += ")" self.get_interface().write(command_string)
[docs] def close_relay_wdelay(self,delay,relay_list): '''close list of relays at named (column, row) with delay between each''' self._set_relay_wdelay(delay,relay_list,closed=True)
[docs] def open_relay_wdelay(self,delay,relay_list): '''open list of relays at named (column, row) with delay between each''' self._set_relay_wdelay(delay,relay_list,closed=False)
[docs] def open_all(self,sync_channels=False): '''open all relays, set sync_channels to true to keep the channels synced (no need to do this if shutting down)''' if sync_channels: for relay_channel in self.get_all_channels_list(): relay_channel.write(False) else: self.get_interface().write("OPEN ALL")
[docs] def test(self): '''run the built in test routine''' self.get_interface().write("*TST?")
[docs]class hp_3458a(instrument): '''HP 3458A MULTIMETER''' def __init__(self,interface_visa): '''interface_visa"''' self._base_name = 'hp_3458a' instrument.__init__(self,"hp_3458a @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.get_interface().write('RESET') self.get_interface().write('END 1') # EOI line set true with last byte of last reading self.config_dc_voltage()
[docs] def config_dc_voltage(self, NPLC=50, range=None): '''Set meter to measure DC volts. Optionally set number of powerline cycles for integration to [0-1000] and set range to [AUTO or 0.12, 1.2, 12, 120, 1000]''' self.NPLC = NPLC #valid NPLC range is 0 to 1000; Default is set to 50 self.get_interface().write('NPLC ' + str(self.NPLC)) #range is actually the maximum input voltage the user will apply to the input. The meter then #selects the best fixed range. Not 'range' in arguments to keep things backwards compatible if (range is not None): self.get_interface().write("FUNC DCV, " + str(range)) else: self.get_interface().write('FUNC DCV, AUTO') self.get_interface().write('TARM HOLD') self.get_interface().write('TRIG AUTO')
[docs] def config_dc_current(self, NPLC=50, range=None): '''Set meter to measure DC current. Optionally set number of powerline cycles for integration to [0-1000] and set range to [AUTO or 0.12E-6, 1.2E-6, 12E-6, 120E-6, 1.2E-3, 12E-3, 120E-3, 1.2]''' self.NPLC = NPLC #valid NPLC range is 0 to 1000; Default is set to 50 self.get_interface().write('NPLC ' + str(self.NPLC)) #range is optional string value that is the manual range the meter should operate in. #valid values are in volts: 0.01, 0.1, 1, 3 if (range is not None): self.get_interface().write("FUNC DCI, " + str(range)) else: self.get_interface().write('FUNC DCI, AUTO') self.get_interface().write('TARM HOLD') self.get_interface().write('TRIG AUTO')
[docs] def config_ac_voltage(self, NPLC=50, range=None): '''Set meter to measure AC volts. Optionally set number of powerline cycles for integration to [0-1000] and set range to [AUTO or 0.012, 0.12, 1.2, 12, 120, 1000]''' self.NPLC = NPLC #valid NPLC range is 0 to 1000; Default is set to 50 self.get_interface().write('NPLC ' + str(self.NPLC)) #range is optional string value that is the manual range the meter should operate in. #valid values are in volts: 0.01, 0.1, 1, 3 if (range is not None): self.get_interface().write("FUNC ACV, " + str(range)) else: self.get_interface().write('FUNC ACV, AUTO') self.get_interface().write('TARM HOLD') self.get_interface().write('TRIG AUTO')
[docs] def config_ac_current(self, NPLC=50, range=None): '''Set meter to measure AC current. Optionally set number of powerline cycles for integration to [0-1000] and set range to [AUTO or 0.12E-6, 1.2E-6, 12E-6, 120E-6, 1.2E-3, 12E-3, 120E-3, 1.2]''' #TODO: confirm meter ranges. 3458 users guide inconsistent with missing range on page 222 self.NPLC = NPLC #valid NPLC range is 0 to 1000; Default is set to 50 self.get_interface().write('NPLC ' + str(self.NPLC)) #range is optional string value that is the manual range the meter should operate in. #valid values are in volts: 0.01, 0.1, 1, 3 if (range is not None): self.get_interface().write("FUNC ACI, " + str(range)) else: self.get_interface().write('FUNC ACI, AUTO') self.get_interface().write('TARM HOLD') self.get_interface().write('TRIG AUTO')
[docs] def config_ohmf(self, NPLC=50, range=None): '''Set meter to measure 4 terminal ohm. Optionally set number of powerline cycles for integration to [0-1000] and se t range to [AUTO or 12, 120 1.2e3, 1.2e4, 1.2e5, 1.2e6, 1.2e7, 1.2e8, 1.2e9]''' self.NPLC = NPLC #valid NPLC range is 0 to 1000; Default is set to 50 self.get_interface().write('NPLC ' + str(self.NPLC)) #range is optional string value that is the manual range the meter should operate in. #valid values are in ohms: 0.01, 0.1, 1, 3 if (range is not None): self.get_interface().write("FUNC OHMF, " + str(range)) else: self.get_interface().write('FUNC OHMF, AUTO') self.get_interface().write('TARM HOLD') self.get_interface().write('TRIG AUTO')
[docs] def add_channel(self,channel_name): '''Add named channel to instrument. Defaults to DC volts.''' meter_channel = channel(channel_name,read_function=self.read_meter) return self._add_channel(meter_channel)
[docs] def read_meter(self): '''return float representing meter measurement.''' #why does float conversion raise exception? - TODO: Debug! self.get_interface().write('TARM SGL, 1') return float(self.get_interface().read())
[docs] def display(self,message): '''Write message to instrument front panel display.''' self.get_interface().write('DISP MSG,"' + message + '"')
class rl1000(instrument, delegator): def __init__(self,serial_number=None): '''Serial number is unique to every RL1000 and printed on a label attached to the enclosure''' import PyICe.deps.RL1000.RL1000_CLASS as RL1000_CLASS self._base_name = 'Reay_Labs_RL1000_Efficieny_Meter' delegator.__init__(self) self.serial_number = serial_number try: self._rl1000_lib = RL1000_CLASS.rl1000(rlib=None) except Exception as e: print 'Problem loading RL1000 WIN32 32-bit DLL.' print 'Are you running 32-bit python on a windows platform?' print 'Try copying "RL1000.dll" into c:\windows\system' raise e meter_count = self._rl1000_lib.initialize() #Open all meters. Returns the number found. self.meter_serial_numbers = {} meter_str = '' for meter in range(meter_count): self._rl1000_lib.set_meter_number(meter) sn = self.get_serial_number() self.meter_serial_numbers[sn] = meter meter_str += '{},'.format(sn) meter_str = meter_str[:-1] if meter_count == 0: raise Exception('No RL1000 Meters Found!') elif meter_count == 1 and self.serial_number is None: print 'Connected to RL1000 Efficiency Meter serial number: {}'.format(meter_str) self.serial_number = int(meter_str) elif meter_count > 1 and self.serial_number is None: raise Exception('Multiple RL1000 meters present and serial number not specified: {}'.format(meter_str)) elif self.serial_number not in self.meter_serial_numbers: raise Exception('RL1000 meter serial number {} not present: {}'.format(self.serial_number,meter_str)) else: self.set_active_meter() instrument.__init__(self, '{}_{}'.format(self._base_name,self.get_serial_number())) #choose RL1000 first by s/n self.pwm_frequency = None self.pwm_duty_cycle = None self.pwm_amplitude = None self.clock_frequency = None self.clock_amplitude = None #omit all dll calibration methods for now #omit all direct manipulation of voltage and current dacs for now #omit set_timeouts #omit set_current_load_digit #omit set_current_load_variables #omit set_autorange_sample_rate #omit measure_pulse def _dummy_read(self, index): return self.results[index] def get_serial_number(self): '''Return serial number of connected/selected RL1000 instrument.''' return self._rl1000_lib.serial_number() #not necessarily the meter specified for thsi instance! def set_active_meter(self): self._rl1000_lib.set_meter_number(self.meter_serial_numbers[self.serial_number]) def add_channel_current_meter(self, channel_name, channel_number): '''Measures current through "CURRENT1" or "CURRENT2" terminals. Auto-ranging on power-up. Valid channel_numbers are [1,2].''' current_channel = channel(channel_name, read_function=lambda: self._dummy_read(channel_number+1)) assert channel_number == 1 or channel_number == 2 current_channel.set_attribute('index',channel_number+1) #array index from measure_all() return value current_channel.set_delegator(self) current_channel.set_description(self.get_name() + ': ' + self.add_channel_current_meter.__doc__) return self._add_channel(current_channel) def add_channel_voltage_meter(self, channel_name, channel_number): '''Measures voltage across "VOLTAGE1" or "VOLTAGE2" terminals. Valid channel_numbers are [1,2].''' voltage_channel = channel(channel_name, read_function=lambda: self._dummy_read(channel_number-1)) assert channel_number == 1 or channel_number == 2 voltage_channel.set_attribute('index',channel_number-1) #array index from measure_all() return value voltage_channel.set_delegator(self) voltage_channel.set_description(self.get_name() + ': ' + self.add_channel_voltage_meter.__doc__) return self._add_channel(voltage_channel) def add_channel_current_meter_range(self, channel_name, channel_number): '''Select full scale current meter range. Valid channel_numbers are [1,2]. Range upper limits are [10uA, 100uA, 1mA, 10mA, 100mA, 1A, 10A, 30A]. Write channel to None to re-enable auto-ranging.''' def set_meter1_range(range): self.set_active_meter() if range is None: self._rl1000_lib.enable_current_meter1_autorange() else: self._rl1000_lib.disable_current_meter1_autorange() self._rl1000_lib.set_current_meter1_range(self._rl1000_lib.calculate_current_meter_range(range)) def set_meter2_range(range): self.set_active_meter() if range is None: self._rl1000_lib.enable_current_meter2_autorange() else: self._rl1000_lib.disable_current_meter2_autorange() self._rl1000_lib.set_current_meter2_range(self._rl1000_lib.calculate_current_meter_range(range)) if channel_number == 1: range_channel = channel(channel_name,write_function=set_meter1_range) elif channel_number == 2: range_channel = channel(channel_name,write_function=set_meter2_range) else: raise Exception('valid RL1000 current meter range channels are 1 and 2') range_channel.set_description(self.get_name() + ': ' + self.add_channel_current_meter_range.__doc__) return self._add_channel(range_channel) def add_channel_current_load(self, channel_name): '''Set load current on "CURRENT 2" terminals. Load enabled and set to 0 on power-up. Load times out and returns to 0 setting after 20 seconds.''' def set_load_current(current): self.set_active_meter() self._rl1000_lib.set_load_current(current) load_channel = self._add_channel(channel(channel_name,write_function=set_load_current)) load_channel.write(0) load_channel.set_description(self.get_name() + ': ' + self.add_channel_current_load.__doc__) return load_channel #reset_current_load() not used def add_channel_current_load_enable(self, channel_name): '''Enable or disables current load on "CURRENT 2" terminals. Load enabled and set to 0 on power up. Write to False to change "CURRENT 2" terminals to pure ammeter. Setting persists across 20-second timeouts.''' #channel 2 only def load_enable(enable): self.set_active_meter() if enable: self._rl1000_lib.enable_current_load() else: self._rl1000_lib.disable_current_load() load_en_channel = self._add_channel(channel(channel_name,write_function=load_enable)) load_en_channel.write(True) load_en_channel.set_description(self.get_name() + ': ' + self.add_channel_current_load_enable.__doc__) return load_en_channel def add_channel_regulator(self, channel_name): '''Program series drop across "REGULATOR" terminals. Defaults to 0V. Most useful with a servo wrapper to keep a constant voltage downstream of shunt. Resets to 0V after 20-second timeout.''' def set_regulator_voltage(voltage): self.set_active_meter() self._rl1000_lib.set_regulator_voltage(voltage) regulator_channel = self._add_channel(channel(channel_name,write_function=set_regulator_voltage)) regulator_channel.write(0) regulator_channel.set_description(self.get_name() + ': ' + self.add_channel_regulator.__doc__) return regulator_channel def add_channel_vout(self, channel_name, channel_number): '''Program analog voltage output on V1+,V2+ rear banana terminals. Valid channel_numbers are [1,2]. Valid settings are 0-5V. Resets to 0V after 20-second timeout.''' def set_vout1(voltage): self.set_active_meter() self._rl1000_lib.set_vout_1(voltage) def set_vout2(voltage): self.set_active_meter() self._rl1000_lib.set_vout_2(voltage) if channel_number == 1: vout_channel = channel(channel_name,write_function=set_vout1) elif channel_number == 2: vout_channel = channel(channel_name,write_function=set_vout2) else: raise Exception('valid RL1000 vout channels are 1 and 2') vout_channel = self._add_channel(vout_channel) vout_channel.write(0) vout_channel.set_description(self.get_name() + ': ' + self.add_channel_vout.__doc__) return vout_channel def add_channel_pwm_frequency(self, channel_name): '''Programs PWM output frequency on rear panel BNC jack. Valid frequencies are 50Hz-10kHz. Also requires pwm_duty_cycle and pwm_amplitude channels to be configured to enable output. Output disabled after 20-second timeout.''' #frequency=50-10khz def check_frequency(frequency): if 50 <= frequency <= 10000: self._set_pwm(frequency=frequency) else: raise Exception('PWM frequency {} outside valid range 50-10000Hz'.format(frequency)) pwm_f_channel = self._add_channel(channel(channel_name,write_function=check_frequency)) pwm_f_channel.set_description(self.get_name() + ': ' + self.add_channel_pwm_frequency.__doc__) return pwm_f_channel def add_channel_pwm_amplitude(self, channel_name): '''Programs PWM output amplitude on rear panel BNC jack. Valid amplitudes are 1.2V-5.25V. Also requires pwm_frequency pwm_duty_cycle and channels to be configured to enable output. Output disabled after 20-second timeout.''' #Amplitude=1.2-5.25V def check_amplitude(amplitude): if 1.2 <= amplitude <= 5.25: self._set_pwm(amplitude=amplitude) else: raise Exception('PWM amplitude {} outside valid range 1.2-5.25V'.format(amplitude)) pwm_a_channel = self._add_channel(channel(channel_name,write_function=check_amplitude)) pwm_a_channel.set_description(self.get_name() + ': ' + self.add_channel_pwm_amplitude.__doc__) return pwm_a_channel def add_channel_pwm_duty_cycle(self, channel_name): '''Programs PWM output duty cycle on rear panel BNC jack. Valid duty_cycles are 0-1. Also requires pwm_frequency and pwm_amplitude channels to be configured to enable output. Output disabled after 20-second timeout.''' #duty_cycle=0-1 def check_duty_cycle(duty_cycle): if 0 <= duty_cycle <= 1: self._set_pwm(duty_cycle=duty_cycle) else: raise Exception('PWM duty cycle {} outside valid range 0-1'.format(duty_cycle)) pwm_d_channel = self._add_channel(channel(channel_name,write_function=check_duty_cycle)) pwm_d_channel.set_description(self.get_name() + ': ' + self.add_channel_pwm_duty_cycle.__doc__) return pwm_d_channel def _set_pwm(self, frequency=None, duty_cycle=None, amplitude=None): self.set_active_meter() if frequency is None and duty_cycle is None and amplitude is None: self._rl1000_lib.disable_pwm() self.pwm_frequency = None self.pwm_duty_cycle = None self.pwm_amplitude = None return if frequency is not None: self.pwm_frequency = frequency if duty_cycle is not None: self.pwm_duty_cycle = duty_cycle if amplitude is not None: self.pwm_amplitude = amplitude if self.pwm_frequency is not None and self.pwm_duty_cycle is not None and self.pwm_amplitude is not None: #check clock variables before enabling pwm? self._rl1000_lib.set_pwm(self.pwm_frequency,self.pwm_duty_cycle,self.pwm_amplitude) #Turn on the PWM. Amplitude=1.2-5.25V,duty_cycle=0-1,frequency=50-10khz else: if self.pwm_frequency is None: print 'Set PWM frequency to enable PWM.' if self.pwm_duty_cycle is None: print 'Set PWM duty cycle to enable PWM.' if self.pwm_amplitude is None: print 'Set PWM amplitude to enable PWM.' def add_channel_clock_frequency(self, channel_name): '''Programs clock output frequency on rear panel BNC jack. Valid frequencies are 1kHz-50MHz. Also requires clock_amplitude channel to be configured to enable output. Output disabled after 20-second timeout.''' #frequency=1k-50Mhz def check_frequency(frequency): if 1000 <= frequency <= 50000000: self._set_clock(frequency=frequency) else: raise Exception('Clock frequency {} outside valid range 1kHz-50MHz'.format(frequency)) clk_f_channel = self._add_channel(channel(channel_name,write_function=check_frequency)) clk_f_channel.set_description(self.get_name() + ': ' + self.add_channel_clock_frequency.__doc__) return clk_f_channel def add_channel_clock_amplitude(self, channel_name): '''Programs clock output frequency on rear panel BNC jack. Valid amplitudes are 1.2V-5.25V. Also requires clock_frequency channel to be configured to enable output. Output disabled after 20-second timeout.''' #Amplitude=1.2-5.25V def check_amplitude(amplitude): if 1.2 <= amplitude <= 5.25: self._set_clock(amplitude=amplitude) else: raise Exception('Clock amplitude {} outside valid range 1.2-5.25V'.format(amplitude)) clk_a_channel = self._add_channel(channel(channel_name,write_function=check_amplitude)) clk_a_channel.set_description(self.get_name() + ': ' + self.add_channel_clock_amplitude.__doc__) return clk_a_channel def _set_clock(self, frequency=None, amplitude=None): self.set_active_meter() if frequency is None and amplitude is None: self._rl1000_lib.disable_pwm() self.clock_frequency = None self.clock_amplitude = None return if frequency is not None: self.clock_frequency = frequency if amplitude is not None: self.clock_amplitude = amplitude if self.clock_frequency is not None and self.clock_amplitude is not None: #check pwm variables before enabling clock? self._rl1000_lib.set_clock(self.clock_frequency,self.clock_amplitude) #Turn on the PWM. Amplitude=1.2-5.25V,frequency=1k-50Mhz elif self.clock_frequency is None: print 'Set clock frequency to enable PWM.' elif self.clock_amplitude is None: print 'Set clock amplitude to enable PWM.' def add_channel_sample_rate(self, channel_name): '''Not yet implemented.''' raise NotImplementedError('Not yet implemented.') #set_measurement_sample_rate() Set the sample rate of the V and I measurements #1=6.875hz, 2=13.75hz, 3=27.5hz, 4=55hz, 5=110hz, 6=220hz, 7=440hz, 8=880hz #9=1.76khz, 10=3,52khz. #Sample periods faster than 55hz start to introduce noticeable noise. #Default on power up=6.875hz #set_autorange_sample_rate(self,sample_rate_index) #Set the sample rate background autoranging measurements def identify(self): '''Blink front panel LED twice.''' self.set_active_meter() self._rl1000_lib.identify() def ping(self): '''Check valid connection to RL1000 and reset 20 second timeout counter.''' self.set_active_meter() return self._rl1000_lib.ping() def add_channel_load_heatsink_temperature(self, channel_name): '''Report heatsink temperature for "CHANNEL 2" electronic load in degrees Celsius.''' def measure_load_heatsink(): self.set_active_meter() return self._rl1000_lib.measure_load_heatsink_temperature() load_heatsink_temp_channel = self._add_channel(channel(channel_name,read_function=measure_load_heatsink)) load_heatsink_temp_channel.set_description(self.get_name() + ': ' + self.add_channel_load_heatsink_temperature.__doc__) return load_heatsink_temp_channel def add_channel_regulator_heatsink_temperature(self, channel_name): '''Report heatsink temperature for "REGULATOR" series regulator in degrees Celsius.''' def measure_regulator_heatsink(): self.set_active_meter() return self._rl1000_lib.measure_regulator_heatsink_temperature() regulator_heatsink_temp_channel = self._add_channel(channel(channel_name,read_function=measure_regulator_heatsink)) regulator_heatsink_temp_channel.set_description(self.get_name() + ': ' + self.add_channel_regulator_heatsink_temperature.__doc__) return regulator_heatsink_temp_channel def read_delegated_channel_list(self,channels): '''private''' self.set_active_meter() self.results = self._rl1000_lib.measure_all() results_dict = {} for channel in channels: results_dict[channel.get_name()] = channel.read_without_delegator() return results_dict
[docs]class keithley_7002(scpi_instrument): '''KEITHLEY 7002 SWITCH SYSTEM Superclass for the 7011S Quad 10 to 1 multiplexers Additional Cards possible in future note - this setup does not change channel types unless a config_ is called ''' def __init__(self,interface_visa): '''interface_visa''' self._base_name = 'keithley_7002' scpi_instrument.__init__(self, self._base_name) self.add_interface_visa(interface_visa,timeout=10) # why was this commented then not commited? # scpi_instrument.__init__(self,"7002_mux @: {}".format(self.get_interface()) ) self.get_interface().write("*rst")
[docs] def add_channel_relay(self,channel_name,bay,number): '''Add named channel at bay and num bay valid range [1-10] number valid range [1-40] for 7011S Quad 10 to 1 multiplexer card ''' relay_channel = channel(channel_name,write_function=lambda closed: self._set_relay(bay, number, closed) ) return self._add_channel(relay_channel)
def _close_relay(self,bay,number): '''close named channel relay''' self.get_interface().write("CLOSE (@{}!{})".format(bay,number) ) def _open_relay(self,bay,number): '''open named channel relay''' self.get_interface().write("OPEN (@{}!{})".format(bay,number) ) def _set_relay(self,bay,number,state): if state: self._close_relay(bay,number) else: self._open_relay(bay,number)
[docs] def open_all(self,sync_channels=False): '''open all relays, set sync_channels to true to keep the channels synced (no need to do this if shutting down)''' if sync_channels: for relay_channel in self.get_all_channels_list(): relay_channel.write(False) else: self.get_interface().write("OPEN ALL")
[docs]class keithley_7002_meter(keithley_7002): '''Combines 7002 switch system and any multimeter instrument into a virtual super 34970.''' def __init__(self, interface_visa, multimeter_channel): '''interface_visa for the Keithley 7002 mux system multimeter_channel is channel object, lb.get_channel(channel_name) or some_meter.get_channel(channel_name) will return ones delay is the number of seconds between closing the channel relay and triggering the meter measurement. ''' keithley_7002.__init__(self, interface_visa) self._base_name = 'keithley_7002_meter' self.multimeter_channel = multimeter_channel self.open_all() #just to be sure...
[docs] def add_channel_meter(self,channel_name,bay,num,pre_calls=[],post_calls=[],multimeter_channel=None,delay=0): '''add named channel to instrument bay is the switch system plugin bay. Valid range [1-10] num valid range [1-40] for 7011S Quad 10 to 1 multiplexer card pre_calls is a list of functions taking exactly 0 arguments to call after closing channel relay but before triggering multimeter measurement post_calls is a list of functions taking exactly 0 arguments to call after triggering multimeter measurement but before opening channel relay multimeter_channel is a channel with the meter on it, if not specified the instrument meter is used ''' meter_channel = channel(channel_name, read_function= lambda: self._read_meter(multimeter_channel,bay,num,pre_calls,post_calls,delay)) return self._add_channel(meter_channel)
def _read_meter(self, multimeter_channel, bay, num, pre_calls, post_calls,delay): '''close relay to named channel, run any pre_calls associated with channel, trigger measurement, run any post_calls associated with channel, and return the measurment result pre_ and post_calls can be used, for example, to set different ranges or integration powerline cycles for different channels using the same multimeter. ''' self.open_all() #just to be sure... self._close_relay(bay,num) for func in pre_calls: func() time.sleep(delay) if multimeter_channel is not None: result = multimeter_channel.read() else: result = self.multimeter_channel.read() for func in post_calls: func() self._open_relay(bay,num) return result
[docs]class agilent_3034a(scpi_instrument, delegator): '''Agilent 4-channel mixed signal DSO''' def __init__(self, interface_visa, force_trigger = False, timeout = 1): '''interface_visa''' self._base_name = 'agilent_3034a' scpi_instrument.__init__(self,"agilent_3034a @ {}".format(interface_visa)) delegator.__init__(self) # Clears self._interfaces list, so must happen before add_interface_visa(). --FL 12/21/2016 self.add_interface_visa(interface_visa, timeout = timeout) self.get_interface().write(':WAVeform:FORMat ASCII') self.get_interface().write(':WAVeform:POINts:MODE RAW') #maximum number of points by default (scope must be stopped) self.force_trigger = force_trigger
[docs] def set_points(self, points): ''' set the number of points returned by read_channel() or read_channels() points must be in range [100,250,500] or [1000,2000,5000]*10^[0-4] or [8000000] ''' allowed_points = [100,250,500] allowed_points.extend(lab_utils.decadeListRange([1000,2000,5000],4)) allowed_points.extend((8000000,)) if points not in allowed_points: raise Exception('{}: set_points: points argument muse be in range: {}'.format(self.get_name(), allowed_points)) self.get_interface().write(':WAVeform:POINts {}'.format(points))
def add_channel_time(self,channel_name): def compute_x_points(self): '''Data conversion: voltage = [(data value - yreference) * yincrement] + yorigin time = [(data point number - xreference) * xincrement] + xorigin''' xpoints = map(lambda x: (x - self.time_info['reference']) * self.time_info['increment'] + self.time_info['origin'], range(self.time_info['points'])) return xpoints time_channel = channel(channel_name, read_function=lambda: compute_x_points(self)) time_channel.set_delegator(self) self._add_channel(time_channel) def get_time_info(self): return self.time_info time_info = channel(channel_name + "_info", read_function=lambda: get_time_info(self)) time_info.set_delegator(self) self._add_channel(time_info) return time_channel def get_channel_enable_status(self, scope_channel_number): return int(self.get_interface().ask(':CHANnel{}:DISPlay?'.format(scope_channel_number)))
[docs] def add_channel(self, channel_name, scope_channel_number): '''Add named channel to instrument. num is 1-4.''' scope_channel = channel(channel_name, read_function=lambda: self._read_scope_channel(scope_channel_number)) scope_channel.set_delegator(self) self._add_channel(scope_channel) self.get_interface().write(':WAVeform:SOURce CHANnel{}'.format(scope_channel_number)) #make sure one of the selected channels is always active to get time info def get_channel_settings(scope_channel_number): result = {} result['scale'] = float(self.get_interface().ask(":CHANnel{}:SCALe?".format(scope_channel_number))) result['offset'] = float(self.get_interface().ask(":CHANnel{}:OFFSet?".format(scope_channel_number)))# This is the value represented by the screen center. result['units'] = self.get_interface().ask(":CHANnel{}:UNITs?".format(scope_channel_number))[0]# Pick up just the first letter A for AMP or V for VOLT. result['label'] = self.get_interface().ask(':CHANnel{}:LABel?'.format(scope_channel_number)).strip('"') result['bwlimit'] = self.get_interface().ask(':CHANnel{}:BWLimit?'.format(scope_channel_number)) result['coupling'] = self.get_interface().ask(':CHANnel{}:COUPling?'.format(scope_channel_number)) result['impedance'] = self.get_interface().ask(':CHANnel{}:IMPedance?'.format(scope_channel_number)) return result trace_info = channel(channel_name + "_info", read_function=lambda: get_channel_settings(scope_channel_number)) trace_info.set_delegator(self) self._add_channel(trace_info) return scope_channel
def get_time_base(self): return float(self.get_interface().ask(':TIMebase:RANGe?')) / 10 # Always 10 horizontal divisions def trigger_force(self): self.get_interface().write(':RUN;:TRIGger:FORCe') def digitize(self): self.get_interface().write(':DIGitize') def _read_scope_time_info(self): self.time_info = {} self.time_info['points'] = int(self.get_interface().ask(":WAVeform:POINts?")) # int(preamble[2]) self.time_info['increment'] = float(self.get_interface().ask(":WAVeform:XINCrement?")) # float(preamble[4]) self.time_info['origin'] = float(self.get_interface().ask(":WAVeform:XORigin?")) # float(preamble[5]) self.time_info['reference'] = float(self.get_interface().ask(":WAVeform:XREFerence?")) # float(preamble[6]) self.time_info['scale'] = self.time_info['increment'] * self.time_info['points'] / 10 self.time_info['enable_status'] = {} for scope_channel_number in range(1,5): self.time_info['enable_status'][scope_channel_number] = int(self.get_interface().ask(':CHANnel{}:DISPlay?'.format(scope_channel_number))) def _read_scope_channel(self, scope_channel_number): '''return list of y-axis points for named channel list will be datalogged by logger as a string in a single cell in the table trigger=False can by used to suppress acquisition of new data by the instrument so that data from a single trigger may be retrieved from each of the four channels in turn by read_channels() ''' self.get_interface().write(':WAVeform:SOURce CHANnel{}'.format(scope_channel_number)) raw_data = self.get_interface().ask(':WAVeform:DATA?') #Example: '#800027579 4.03266e-002, 1.25647e-004, 1.25647e-004, 1.25647e-004,.......' raw_data = raw_data[10:] #remove header raw_data = raw_data.split(',') data = map(lambda x: float(x), raw_data) #TODO - implement binary transfer if speed becomes a problem return data def read_delegated_channel_list(self, channels): if self.force_trigger: self.trigger_force() # self.digitize() # self.get_interface().write(':STOP')# scope will timeout on :WAVeform:PREamble? if not 'STOPped' self._read_scope_time_info() results = {} for channel in channels: results[channel.get_name()] = channel.read_without_delegator() return results
[docs]class tektronix_3054(scpi_instrument,delegator): '''Tek 4-channel DSO''' def __init__(self, interface_visa, force_trigger=True): '''interface_visa"''' self._base_name = 'tektronix_3054' delegator.__init__(self) scpi_instrument.__init__(self,"tektronix_3054 @ {}".format(interface_visa)) self.add_interface_visa(interface_visa,timeout=10) self.get_interface().write('DATA:ENCdg ASCIi') self.get_interface().write('DATA:WID 2') self.get_interface().write('HEADER 1') #data headers help parse results of wfmoutpre? query, since different scopes return different length responses! self.force_trigger = force_trigger def add_channel_time(self,channel_name): time_channel = channel(channel_name, read_function=self._read_scope_time) time_channel.set_delegator(self) return self._add_channel(time_channel)
[docs] def add_channel(self,channel_name,scope_channel_number): '''Add named channel to instrument. num is 1-4.''' scope_channel = channel(channel_name,read_function=lambda: self._read_scope_channel(scope_channel_number)) scope_channel.set_delegator(self) return self._add_channel(scope_channel)
[docs] def trigger_force(self): '''Creates a trigger event. If TRIGger:STATE is set to READy, the acquisition will complete. Otherwise, this command will be ignored.''' self.get_interface().write('TRIGger FORCe')
def _read_scope_time(self): ''' Data conversion: voltage = [(data value - yreference) * yincrement] + yorigin time = [(data point number - xreference) * xincrement] + xorigin ''' self.get_interface().write('WFMPRE?') preamble = self.get_interface().read().split(';') preamble[0] = preamble[0].split(':')[-1] #remove junk that doesn't really belong to first field preamble_dict = {} for field in preamble: name, value = field.split(' ', 1) preamble_dict[name] = value preamble_dict['NR_PT'] = int(preamble_dict['NR_PT']) preamble_dict['XINCR'] = float(preamble_dict['XINCR']) preamble_dict['PT_OFF'] = float(preamble_dict['PT_OFF']) preamble_dict['XZERO'] = float(preamble_dict['XZERO']) xpoints = map(lambda x: (x - preamble_dict['PT_OFF'])*preamble_dict['XINCR']+preamble_dict['XZERO'],range(preamble_dict['NR_PT'])) return xpoints def _read_scope_channel(self,scope_channel_number): '''return list of y-axis points for named channel list will be datalogged by logger as a string in a single cell in the table trigger=False can by used to suppress acquisition of new data by the instrument so that data from a single trigger may be retrieved from each of the four channels in turn by read_channels() ''' #trigger / single arm sequence commands need investigation. Forcing trigger here is not correct # if trigger: # self.get_interface().write('TRIGger') # Examples WFMOUTPRE? ? might return the waveform formatting data as: # [0] :WFMOUTPRE:BYT_NR 2; # [1] BIT_NR 16; # [2] ENCDG ASCII; # [3] BN_FMT RI; # [4] BYT_OR MSB; # [5] WFID "Ch1, DC coupling, 100.0mV/div, 4.000us/div, 10000 points, Sample mode"; # [6] NR_PT 10000; # [7] PT_FMT Y; # [8] XUNIT "s"; # [9] XINCR 4.0000E-9; # [10] XZERO - 20.0000E-6; # [11] PT_OFF 0; # [12] YUNIT "V"; # [13] YMULT 15.6250E-6; # [14] YOFF :"6.4000E+3; # [15] YZERO 0.0000 self.get_interface().write('DATA:SOUrce CH{}'.format(scope_channel_number)) preamble = self.get_interface().ask('WFMPRE?').split(';') preamble[0] = preamble[0].split(':')[-1] #remove junk that doesn't really belong to first field preamble_dict = {} for field in preamble: name, value = field.split(' ', 1) preamble_dict[name] = value preamble_dict['YMULT'] = float(preamble_dict['YMULT']) #scale int to volts preamble_dict['YZERO'] = float(preamble_dict['YZERO']) #offset set into scope, subtract from raw_data before scaling if you want data offset preamble_dict['YOFF'] = float(preamble_dict['YOFF']) #waveform position raw_data = self.get_interface().ask('CURVe?') raw_data = raw_data.split(',') #not sure where y_zero goes in eqn! #offset seems to be display only raw_data[0] = raw_data[0].split(' ')[-1] data = map(lambda x: (int(x)-preamble_dict['YOFF'])*preamble_dict['YMULT']+preamble_dict['YZERO'], raw_data) #TODO - implement binary transfer if speed becomes a problem return data def read_delegated_channel_list(self,channels): if self.force_trigger: self.trigger_force() results = {} for channel in channels: results[channel.get_name()] = channel.read_without_delegator() return results
[docs]class hp_4195a(scpi_instrument): '''HP4195A Network Analyzer Current Driver Only Collects Data; no configuration or measurement trigger''' def __init__(self, interface_visa): '''interface_visa''' self._base_name = 'hp_4195a' scpi_instrument.__init__(self,"h4195a @ {}".format(interface_visa)) self.add_interface_visa(interface_visa)
[docs] def add_channel(self,channel_name,register): '''register must be X - frequency A - A register B - B register C - C register D - D register''' register = register.upper() if register.upper() not in ['X','A','B','C','D']: raise Exception('Bad register {} for 4195a'.format(register)) new_channel = channel(channel_name,read_function = lambda: self._read_4195a_register(register)) return self._add_channel(new_channel)
def _read_4195a_register(self,register): '''read from one of the five hardware registers associated with this channel_name. Return list of scalars representing points.''' data = self.get_interface().ask('{}?'.format(register)) return map(float, data.split(','))
[docs] def config_network(self, start = 0.1, stop = 500e6, RBW = 'AUTO', NOP = 401, OSCA = -50): '''Configure the 4195 for network analysis with start, stop, sweep type and resolution''' self.get_interface().write("RST") self.get_interface().write("OSC1={}" .format(OSCA)) self.get_interface().write("FNC1") #set Network self.get_interface().write("START={}" .format(start)) #start freq self.get_interface().write("STOP={}" .format(stop)) #stop freq if RBW == 'AUTO': self.get_interface().write('CPL1') else: self.get_interface().write("RBW={}" .format(RBW)) #resolution bandwidth self.get_interface().write("NOP={}" .format(NOP)) #number of points in sweet self.get_interface().write("SWT2") #log sweep self.get_interface().write("SWM2") #single trigger mode
[docs] def config_spectrum(self, start = 0.1, stop = 500e6, RBW = 'AUTO', NOP = 401): '''Configure the 4195 for spectrum analysis (noise here) with start, stop, sweep type and resolution''' self.get_interface().write("RST") self.get_interface().write("FNC2") #set Spectrum self.get_interface().write("START={}" .format(start)) #start freq self.get_interface().write("STOP={}" .format(stop)) #stop freq if RBW == 'AUTO': self.get_interface().write('CPL1') else: self.get_interface().write("RBW={}" .format(RBW)) #resolution bandwidth self.get_interface().write("NOP={}" .format(NOP)) #number of points in sweet self.get_interface().write("SWT2") #log sweep self.get_interface().write("SAP6") #uv/rthz self.get_interface().write("SWM2") #singletrigger mode
[docs] def trigger(self): '''Return the sweep time and trigger once''' ttime=self.get_interface().ask('ST?') self.get_interface().write('SWTRG') return ttime
[docs]class semiconductor_parameter_analyzer(scpi_instrument): '''Generic parameter analyzer speaking HP4145 Command Set in user mode (US page)''' def _set_user_mode(self): self.get_interface().write('US') def _set_smu_voltage(self,smu_number,v_output=None,i_compliance=None,v_output_range=None): '''set smu voltage and current compliance if enough arguments specified''' if smu_number not in self._smu_configuration.keys(): self._smu_configuration[smu_number] = {'v_output_range':0,'i_output_range':0} if v_output is not None: self._smu_configuration[smu_number]['v_output'] = v_output if i_compliance is not None: self._smu_configuration[smu_number]['i_compliance'] = i_compliance if v_output_range is not None: if isinstance(v_output_range, str) and v_output_range.upper() == 'AUTO': self._smu_configuration[smu_number]['v_output_range'] = 0 else: self._smu_configuration[smu_number]['v_output_range'] = self._lookup_output_range(v_output_range, self._smu_voltage_range) if 'v_output' in self._smu_configuration[smu_number].keys() and 'i_compliance' in self._smu_configuration[smu_number].keys(): self.get_interface().write("DV {:G}, {:G}, {:G}, {:G}".format(self._smu_voltage_measure_channels[smu_number],self._smu_configuration[smu_number]['v_output_range'],self._smu_configuration[smu_number]['v_output'], self._smu_configuration[smu_number]['i_compliance'])) #print 'writing these values to voltage SMU:' #print self._smu_configuration[smu_number] else: print 'SMU{} disabled. Write both output_voltage and current_compliance channels to enable output'.format(smu_number) self._disable_smu(smu_number) def _set_smu_current(self,smu_number,i_output=None,v_compliance=None,i_output_range=None): '''set smu current and voltage compliance if enough arguments specified''' if smu_number not in self._smu_configuration.keys(): self._smu_configuration[smu_number] = {'i_output_range':0,'v_output_range':0} if i_output is not None: self._smu_configuration[smu_number]['i_output'] = i_output if v_compliance is not None: self._smu_configuration[smu_number]['v_compliance'] = v_compliance if i_output_range is not None: if isinstance(i_output_range, str) and i_output_range.upper() == 'AUTO': self._smu_configuration[smu_number]['i_output_range'] = 0 else: self._smu_configuration[smu_number]['i_output_range'] = self._lookup_output_range(i_output_range, self._smu_current_range) if 'i_output' in self._smu_configuration[smu_number].keys() and 'v_compliance' in self._smu_configuration[smu_number].keys(): self.get_interface().write("DI {:G}, {:G}, {:G}, {:G}".format(smu_number,self._smu_configuration[smu_number]['i_output_range'],self._smu_configuration[smu_number]['i_output'],self._smu_configuration[smu_number]['v_compliance'])) #print 'writing these values to current SMU:' #print self._smu_configuration[smu_number] else: print 'SMU{} disabled. Write both output_current and voltage_compliance channels to enable output'.format(smu_number) self._disable_smu(smu_number) def _disable_smu(self,smu_number): self.get_interface().write("DV {}".format(smu_number)) def _lookup_output_range(self, max, range_dict): import bisect ranges = sorted(range_dict.keys()) index = bisect.bisect_left(ranges, abs(max)) print 'range select chose +/-{}:{} to match input of {}'.format(ranges[index],range_dict[ranges[index]],max) return range_dict[ranges[index]] def _set_vsource_voltage(self,vsource_number,output): '''set vsource voltage''' self.get_interface().write("DS {:G}, {:G}".format(vsource_number,output)) def _disable_vs(self,vs_number): self.get_interface().write("DS {}".format(vs_number)) def shutdown(self): for smu in self.smu_numbers: self._disable_smu(smu) for vs in self.vs_numbers: self._disable_vs(vs) def _read_voltage(self,channel_number): ret_str = self.get_interface().ask("TV {}".format(channel_number)) result = self._check_measurement_result(ret_str) if result is not None: print result #raise exception? #log in error channel? return float(ret_str[3:]) def _read_current(self,channel_number): ret_str = self.get_interface().ask("TI {}".format(channel_number)) result = self._check_measurement_result(ret_str) if result is not None: print result #raise exception? #log in error channel? return float(ret_str[3:]) def _check_measurement_result(self,ret_str): if ret_str[0] == 'N': return if ret_str[2] == 'V': instrument_dict = self._voltage_measure_channels elif ret_str[2] == 'I': instrument_dict = self._current_measure_channels else: raise Exception('Unknown data format: {}'.format(ret_str)) if ret_str[0] == 'L': return 'WARNING! {}: Interval too short'.format(instrument_dict[ret_str[1]]) if ret_str[0] == 'V': return 'WARNING! {}: A/D Converter Saturated - Overflow'.format(instrument_dict[ret_str[1]]) if ret_str[0] == 'X': return 'WARNING! {}: Oscillation'.format(instrument_dict[ret_str[1]]) if ret_str[0] == 'C': return 'WARNING! {}: This channel in compliance'.format(instrument_dict[ret_str[1]]) if ret_str[0] == 'T': return 'WARNING! {}: Other channel in compliance'.format(instrument_dict[ret_str[1]]) raise Exception('Unknown data format: {}'.format(ret_str)) def _set_integration_time(self,time): if (isinstance(time,str) and time.upper() == "SHORT") or time == 1: self.get_interface().write("IT1") elif (isinstance(time,str) and time.upper() == "MEDIUM") or time == 2: self.get_interface().write("IT2") elif (isinstance(time,str) and time.upper() == "LONG") or time == 3: self.get_interface().write("IT3") else: raise Exception('Valid integration times are "SHORT", "MEDIUM" or "LONG"') def _add_channels_smu_voltage(self,smu_number,voltage_force_channel_name,current_compliance_channel_name): voltage_force_channel = channel(voltage_force_channel_name,write_function = lambda output: self._set_smu_voltage(smu_number,v_output=output)) current_compliance_channel = channel(current_compliance_channel_name,write_function = lambda compliance: self._set_smu_voltage(smu_number,i_compliance=compliance)) self._add_channel(voltage_force_channel) self._add_channel(current_compliance_channel) return [voltage_force_channel,current_compliance_channel] def _add_channel_smu_voltage_output_range(self,smu_number,output_range_channel_name): output_range_channel = channel(output_range_channel_name,write_function = lambda output_range: self._set_smu_voltage(smu_number,v_output_range=output_range)) self._add_channel(output_range_channel) return output_range_channel def _add_channels_smu_current(self,smu_number,current_force_channel_name,voltage_compliance_channel_name): current_force_channel = channel(current_force_channel_name,write_function = lambda output: self._set_smu_current(smu_number,i_output=output)) voltage_compliance_channel = channel(voltage_compliance_channel_name,write_function = lambda compliance: self._set_smu_current(smu_number,v_compliance=compliance)) self._add_channel(current_force_channel) self._add_channel(voltage_compliance_channel) return [current_force_channel,voltage_compliance_channel] def _add_channel_smu_current_output_range(self,smu_number,output_range_channel_name): output_range_channel = channel(output_range_channel_name,write_function = lambda output_range: self._set_smu_current(smu_number,i_output_range=output_range)) self._add_channel(output_range_channel) return output_range_channel def _add_channel_smu_voltage_sense(self,smu_number,voltage_sense_channel_name): voltage_sense_channel = channel(voltage_sense_channel_name,read_function = lambda: self._read_voltage(self._smu_voltage_measure_channels[smu_number])) self._add_channel(voltage_sense_channel) return voltage_sense_channel def _add_channel_smu_current_sense(self,smu_number,current_sense_channel_name): current_sense_channel = channel(current_sense_channel_name,read_function = lambda: self._read_current(self._smu_current_measure_channels[smu_number])) self._add_channel(current_sense_channel) return current_sense_channel def _add_channel_vsource(self,vsource_number,vsource_channel_name): vsource_channel = channel(vsource_channel_name,write_function = lambda output: self._set_vsource_voltage(vsource_number,output)) self._add_channel(vsource_channel) return vsource_channel def _add_channel_vmeter(self,vmeter_number,vmeter_channel_name): vmeter_channel = channel(vmeter_channel_name,read_function = lambda: self._read_voltage(self._vm_voltage_measure_channels[vmeter_number])) self._add_channel(vmeter_channel) return vmeter_channel def add_channel_integration_time(self,integration_time_channel_name): integration_time_channel = channel(integration_time_channel_name,write_function =self._set_integration_time) self._add_channel(integration_time_channel) return integration_time_channel
[docs]class keithley_4200(semiconductor_parameter_analyzer): '''Keithley Model 4200-SCS Semiconductor Characterization System''' def __init__(self,interface_visa): '''interface_visa"''' self._base_name = 'keithley_4200-scs' scpi_instrument.__init__(self,"keithley_4200-scs @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self._set_user_mode() self._smu_voltage_measure_channels = {1:1,2:2,3:3,4:4,5:7,6:8,7:9,8:10} self._smu_current_measure_channels = {1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8} self._vm_voltage_measure_channels = {1:5,2:6,3:11,4:12,5:13,6:14,7:15,8:16} self._smu_voltage_range = {20:1,200:2} #'AUTO':0 self._smu_current_range = {1e-9:1,10e-9:2,100e-9:3,1e-6:4,10e-6:5,100e-6:6,1e-3:7,10e-3:8,100e-3:9,1:10,10e-12:11,100e-12:12} #'AUTO':0 self._voltage_measure_channels = {'A':'SMU1','B':'SMU2','C':'SMU3','D':'SMU4','E':'VM1','F':'VM2','G':'SMU5','H':'SMU6','I':'SMU7','J':'SMU8','K':'VM3','L':'VM4','M':'VM5','N':'VM6','O':'VM7','P':'VM8'} self._current_measure_channels = {'A':'SMU1','B':'SMU2','C':'SMU3','D':'SMU4','E':'SMU5','F':'SMU6','G':'SMU7','H':'SMU8'} self.smu_numbers = [] self.vs_numbers = [] self._smu_configuration = {} self.get_slot_configuration() #self._smu_voltage_compliance = 210 #+/- #self._smu_current_compliance = 0.105 #4200-SMU #self._smu_current_compliance = 1.05 #4210-SMU def get_slot_configuration(self): self.get_interface().write("EM 1,0") self.slot_conf = self.get_interface().ask("*OPT?").strip('"').split(",") self.get_interface().write("EM 0,0") self.smu_numbers = [] self.vs_numbers = [] for slot in range(8): print "Slot {} ".format(slot+1), if self.slot_conf[slot].startswith('SMU'): self.smu_numbers.append(int(self.slot_conf[slot][3])) #what about 10 having 2 digits??? print '{}: Medium Power SMU without preamp'.format(self.slot_conf[slot]) elif self.slot_conf[slot].startswith('HPSMU'): self.smu_numbers.append(int(self.slot_conf[slot][5])) print '{}: High Power SMU without preamp'.format(self.slot_conf[slot]) elif self.slot_conf[slot].startswith('SMUPA'): self.smu_numbers.append(int(self.slot_conf[slot][5])) print '{}: Medium Power SMU with preamp'.format(self.slot_conf[slot]) elif self.slot_conf[slot].startswith('HPSMUPA'): self.smu_numbers.append(int(self.slot_conf[slot][7])) print '{}: High Power SMU with preamp'.format(self.slot_conf[slot]) elif self.slot_conf[slot].startswith('VS'): self.vs_numbers.append(int(self.slot_conf[slot][2])) print '{}: Voltage Source'.format(self.slot_conf[slot]) elif self.slot_conf[slot].startswith('VM'): print '{}: Voltage Meter'.format(self.slot_conf[slot]) elif self.slot_conf[slot] == '': print 'Empty Slot' else: raise Exception('Problem parsing Keithley 4200 slot {} configuration: {}'.format(slot,self.slot_conf))
[docs] def configure_slot_smu(self,slot_number,smu_number): '''reconfigure smu instrument in slot_number to act as an smu''' assert slot_number >= 1 assert slot_number <= 8 assert self.slot_conf[slot_number-1] != '' #empty slot shouldn't be configured assert smu_number >= 1 assert smu_number <= 8 self.get_interface().write("MP {}, SMU{}".format(slot_number,smu_number)) self.get_slot_configuration()
[docs] def configure_slot_vs(self,slot_number,vsource_number): '''reconfigure smu instrument in slot_number to act as a vs''' assert slot_number >= 1 assert slot_number <= 8 assert self.slot_conf[slot_number-1] != '' #empty slot shouldn't be configured assert vsource_number >= 1 assert vsource_number <= 8 self.get_interface().write("MP {}, VS{}".format(slot_number,vsource_number)) self.get_slot_configuration()
[docs] def configure_slot_vm(self,slot_number,vmeter_number): '''reconfigure smu instrument in slot_number to act as a vm''' assert slot_number >= 1 assert slot_number <= 8 assert self.slot_conf[slot_number-1] != '' #empty slot shouldn't be configured assert vmeter_number >= 1 assert vmeter_number <= 8 self.get_interface().write("MP {}, VM{}".format(slot_number,vmeter_number)) self.get_slot_configuration()
def add_channels_smu_voltage(self, smu_number, voltage_force_channel_name, current_compliance_channel_name): #check smu_number is valid return self._add_channels_smu_voltage(smu_number, voltage_force_channel_name, current_compliance_channel_name) def add_channel_smu_voltage_output_range(self,smu_number,output_range_channel_name): #check smu_number is valid return self._add_channel_smu_voltage_output_range(smu_number,output_range_channel_name) def add_channels_smu_current(self,smu_number,current_force_channel_name,voltage_compliance_channel_name): #check smu_number is valid return self._add_channels_smu_current(smu_number,current_force_channel_name,voltage_compliance_channel_name) def add_channel_smu_current_output_range(self,smu_number,output_range_channel_name): #check smu_number is valid return self._add_channel_smu_current_output_range(smu_number,output_range_channel_name) def add_channel_smu_voltage_sense(self,smu_number,voltage_sense_channel_name): #check smu_number is valid return self._add_channel_smu_voltage_sense(smu_number,voltage_sense_channel_name) def add_channel_smu_current_sense(self,smu_number,current_sense_channel_name): #check smu_number is valid return self._add_channel_smu_current_sense(smu_number,current_sense_channel_name) def add_channel_vsource(self,vsource_number,vsource_channel_name): #check vsource_number is valid return self._add_channel_vsource(vsource_number,vsource_channel_name) def add_channel_vmeter(self,vmeter_number,vmeter_channel_name): #check vmeter_number is valid return self._add_channel_vmeter(vmeter_number,vmeter_channel_name)
[docs]class hp_4155b(semiconductor_parameter_analyzer): '''Hewlett Packard Semiconductor Parameter Analyzer speaking HP4145 Command Set Set System->MISCELLANEOUS->COMMAND SET = HP4145 Set System->MISCELLANEOUS->DELIMITER = COMMA Set System->MISCELLANEOUS->EOI = ON''' def __init__(self,interface_visa): '''interface_visa"''' self._base_name = 'hewlett_packard_4155B-scs' scpi_instrument.__init__(self,"hewlett_packard_4155B-scs @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self._smu_voltage_measure_channels = {1:1,2:2,3:3,4:4,5:7,6:8} #PGU1:9,PGU2:10 pulse generator not supported self._smu_current_measure_channels = {1:1,2:2,3:3,4:4,5:7,6:8} self._vm_voltage_measure_channels = {1:5,2:6} self._smu_voltage_range = {20:1,40:2,100:3,200:4,2:-1} #'AUTO':0 self._smu_current_range = {1e-9:1,10e-9:2,100e-9:3,1e-6:4,10e-6:5,100e-6:6,1e-3:7,10e-3:8,100e-3:9,1:10,10e-12:-2,100e-12:-1} #'AUTO':0 self._voltage_measure_channels = {'A':'SMU1','B':'SMU2','C':'SMU3','D':'SMU4','E':'VM1','F':'VM2','G':'SMU5','H':'SMU6'} self._current_measure_channels = {'A':'SMU1','B':'SMU2','C':'SMU3','D':'SMU4','G':'SMU5','H':'SMU6'} self._smu_configuration = {} self.smu_numbers = range(1,7) self.vs_numbers = range(1,3) #print 'Changing HP4145 Command Set (wait 5 seconds).' #self.get_interface().write('*RST') #time.sleep(3) #wait for restart #self.get_interface().write(':SYSTem:LANGuage COMPatibility') #switch to 4145 command set #time.sleep(10) #wait for restart #print self.get_interface().ask('CMD?') #if int(self.get_interface().ask('CMD?')) != 2: #0=SCPI,1=FLEX,2=4145. We want 2=4145 # raise Exception('Error: HP4155 must be set to HP4145 Command Set in System:Misc menu') # try: # self.get_interface().ask('*IDN?') #instrument only responds to this query in SCPI command mode # raise Exception('Error: HP4155 must be set to HP4145 Command Set in System:Misc menu') # except visa_wrappers.visaWrapperException as e: # self.get_interface().clear() #normal to not respond; rl1009 locks up with no response # except Exception as e: # print "Expect timeout error here..." # print e #normal to not respond; HP82357 doesn't seem to lock up with no response # #self.get_interface().clear() self._set_user_mode() def add_channels_smu_voltage(self, smu_number, voltage_force_channel_name, current_compliance_channel_name): assert 1 <= smu_number <= 6 return self._add_channels_smu_voltage(smu_number, voltage_force_channel_name, current_compliance_channel_name) def add_channel_smu_voltage_output_range(self,smu_number,output_range_channel_name): assert 1 <= smu_number <= 6 return self._add_channel_smu_voltage_output_range(smu_number,output_range_channel_name) def add_channels_smu_current(self,smu_number,current_force_channel_name,voltage_compliance_channel_name): assert 1 <= smu_number <= 6 return self._add_channels_smu_current(smu_number,current_force_channel_name,voltage_compliance_channel_name) def add_channel_smu_current_output_range(self,smu_number,output_range_channel_name): assert 1 <= smu_number <= 6 return self._add_channel_smu_current_output_range(smu_number,output_range_channel_name) def add_channel_smu_voltage_sense(self,smu_number,voltage_sense_channel_name): assert 1 <= smu_number <= 6 return self._add_channel_smu_voltage_sense(smu_number,voltage_sense_channel_name) def add_channel_smu_current_sense(self,smu_number,current_sense_channel_name): assert 1 <= smu_number <= 6 return self._add_channel_smu_current_sense(smu_number,current_sense_channel_name) def add_channel_vsource(self,vsource_number,vsource_channel_name): assert 1 <= vsource_number <= 2 return self._add_channel_vsource(vsource_number,vsource_channel_name) def add_channel_vmeter(self,vmeter_number,vmeter_channel_name): assert 1 <= vmeter_number <= 2 return self._add_channel_vmeter(vmeter_number,vmeter_channel_name)
[docs]class ADT7410(instrument): '''Analog Devices Silicon Temperature Sensor http://www.analog.com/static/imported-files/data_sheets/ADT7410.pdf''' def __init__(self, interface_twi, addr7): '''interface_twi is a interface_twi addr7 is the 7-bit I2C address of the ADT7410 set by pinstrapping. Choose addr7 from 0x48, 0x49, 0x4A, 0x4B ''' instrument.__init__(self, 'Analog Devices ADT7410 Silicon Temperature Sensor at {:X}'.format(addr7)) self._base_name = 'ADT7410' self.add_interface_twi(interface_twi) self.twi = interface_twi self.addr7 = addr7 self.registers = {} self.registers['t_msb'] = 0x00 self.registers['t_lsb'] = 0x01 self.registers['status'] = 0x02 self.registers['config'] = 0x03 self.registers['thigh_msb'] = 0x04 self.registers['thigh_lsb'] = 0x05 self.registers['tlow_msb'] = 0x06 self.registers['tlow_lsb'] = 0x07 self.registers['tcrit_msb'] = 0x08 self.registers['tcrit_lsb'] = 0x09 self.registers['t_hyst'] = 0x0A self.registers['ID'] = 0x0B self.registers['reset'] = 0x2F self.enable()
[docs] def enable(self, enabled = True): '''Place ADT7410 into shutdown by writing enabled=False Re-enable by writing enabled=True''' if enabled: self.twi.write_byte(self.addr7, self.registers['config'], (0b1<<7)) #16-bit mode else: #shutdown self.twi.write_byte(self.addr7, self.registers['config'], (0b1<<7 + 0b11<<5))
[docs] def read_temp(self): '''Return free-running temperature conversion result. 16-bit conversion result scaled to signed degrees Celsius''' data = self.twi.read_word(self.addr7, self.registers['t_msb']) #command code counter autoincrements data = lab_utils.swap_endian(data, elementCount = 2) #msb comes out first, not SMBus compatible! data = lab_utils.twosComplementToSigned(data, bitCount=16) return data / 128.0 #lsb size adjustment
[docs] def read_id(self): '''Return Manufacturer ID and chip revision ID''' data = self.twi.read_byte(self.addr7, self.registers['ID']) results = {} results['revision'] = data & 0b111 results['manufacturer'] = data >> 3 return results
def add_channel(self,channel_name): temp_channel = channel(channel_name, read_function=self.read_temp) return self._add_channel(temp_channel)
[docs]class AD5693R(instrument): '''Analog Devices 16 bit DAC http://www.analog.com/en/products/digital-to-analog-converters/da-converters/ad5693r.html''' def __init__(self, interface_twi, addr7): '''interface_twi is a interface_twi addr7 is the 7-bit I2C address of the ADT5693R set by pinstrapping. A0 = 0: 1001100 (4C) A1 = 1: 1001110 (4E) ''' instrument.__init__(self, 'Analog Devices AD5693R Digital to Ananlog Converter at {:X}'.format(addr7)) self._base_name = 'AD5693R' self.add_interface_twi(interface_twi) self.twi = interface_twi self.addr7 = addr7 # user's address input. self.dac_reg = 0x30 # send dac codes to this address for auto update. self.control_reg = 0x40 # sets up gain and power down modes. self.gain_code = 0x0800 # 0 to 5V rather than 0 to 2.5V. self.impedance = 0x0000 # start with output on, this sets and r to GND for disable mode. self.vref = 2.5 # an internal constant. self.refenable = 0 # default 0 is reference on, 1 is off. self.reset = 0x8000 # Use carefully, gums up ACKs. self._update_controlreg() # Clear the whole thing out. self._set_voltage(0) # In lieu of using the reset bit which nukes the I2C port. def _update_controlreg(self): self.twi.write_word(self.addr7, self.control_reg, lab_utils.swap_endian(self.impedance | self.refenable | self.gain_code, elementCount = 2)) def _set_code(self, code): '''Set the code of the AD5693R''' self.twi.write_word(self.addr7, self.dac_reg, lab_utils.swap_endian(code, elementCount = 2)) def _set_gain(self, gain): if gain == 2: self.gain_code = 0x0800 elif gain == 1: self.gain_code = 0x0000 else: raise Exception("AD5693R gain setting should be either 1 or 2.") self._update_controlreg() def _set_outputz(self, z): if z in [0, "0"]: self.impedance = 0x0000 elif z in ["1k", "1K", 1000, 1e3]: self.impedance = 0x2000 elif z in ["100k", "100K", 100000, 1e5]: self.impedance = 0x4000 elif z in ["z", "Z"]: self.impedance = 0x6000 else: raise Exception("AD5693R impedance setting must be one of: {}.".format(["0", "1k", "100k", "z"])) self._update_controlreg() def _set_voltage(self, voltage): '''Set the voltage of the AD5693R''' if self.gain_code == 0x0800: gain = 2.0 else: gain = 1.0 code = int(voltage / gain / self.vref * 65536.0) if code > 65535 or code < 0: raise Exception("AD5693R code: {} out of range 0 to 65535. Gain = {}, Requested voltage = {}.".format(code, gain, voltage)) self._set_code(code) def add_channel(self, channel_name): output = channel(channel_name, write_function =self._set_voltage) return self._add_channel(output) def add_channel_outputz(self, channel_name): output = channel(channel_name, write_function =self._set_outputz) return self._add_channel(output) def add_channel_gain(self, channel_name): output = channel(channel_name, write_function =self._set_gain) return self._add_channel(output)
[docs]class AD5272(instrument): '''Analog Devices I2C Precision Potentiometer / Rheostat http://www.analog.com/static/imported-files/data_sheets/AD5272_5274.pdf''' def __init__(self, interface_twi, addr7, full_scale_ohms = 100000): '''interface_twi is a interface_twi addr7 is the 7-bit I2C address of the AD5272 set by pinstrapping. Choose addr7 from 0x2F, 0x2C, 0x2E ''' instrument.__init__(self, 'Analog Devices I2C 10-bit Potentiometer at {:X}'.format(addr7)) self._base_name = 'AD5272' self.add_interface_twi(interface_twi) self.twi = interface_twi if addr7 not in [0x2C, 0x2E, 0x2F]: raise ValueError, "\n\n\nAD5272 only supports addresses 0x2C, 0x2E, 0x2F" self.addr7 = addr7 self.tries = 3 self.enable() self.full_scale_ohms = full_scale_ohms def _write_byte(self, addr7, subaddr, data): tries = self.tries while tries: try: tries -= 1 self.twi.write_byte(addr7, subaddr, data) return except Exception as e: print e self.twi.resync_communication() if not tries: raise e
[docs] def enable(self, enable = True): '''Place AD5272 into shutdown by writing enabled=False Re-enable by writing enabled=True''' if enable: self._write_byte(self.addr7, 0x9<<2, 0x00) self._write_byte(self.addr7, 0x7<<2, 0x02) #RDAC register write protect disable (allow i2c update) else: #shutdown self.twi.write_byte(self.addr7, 0x9<<2, 0x01)
def set_output(self, value): assert value >= 0 assert value <= 2**10-1 lsbyte = value & 0xFF msbyte = 0x1<<2 | value>>8 self._write_byte(self.addr7, 0x7<<2, 0x02) #RDAC register write protect disable (allow i2c update) self._write_byte(self.addr7, msbyte, lsbyte) def get_output(self): tries = self.tries while tries: try: tries -= 1 self.twi.write_byte(self.addr7, 0x2<<2, 0x00) self.twi.start() self.twi.write(self.twi.read_addr(self.addr7)) msbyte = self.twi.read_ack() lsbyte = self.twi.read_nack() self.twi.stop() return (msbyte & 0b11) << 8 | lsbyte except Exception as e: raise e self.twi.resync_communication() raise Exception("AD5272 Comunication Failed.") def _write_percent(self,percent): '''value is between 0 and 1. DAC is biased toward 0 so that full scale is not achievable''' assert percent >= 0 assert percent <= 1 code = min(int(round(percent * 2**10)), 2**10-1) self.set_output(code) def add_channel_code(self,channel_name): code_channel = channel(channel_name, write_function =self.set_output) return self._add_channel(code_channel) def add_channel_percent(self, channel_name): percent_channel = channel(channel_name, write_function =self._write_percent) return self._add_channel(percent_channel) def add_channel_resistance(self, channel_name): resistance_channel = channel(channel_name, write_function = lambda resistance: self._write_percent(float(resistance) / self.full_scale_ohms)) return self._add_channel(resistance_channel) def add_channel_code_readback(self, channel_name): code_channel = channel(channel_name, read_function =self.get_output) return self._add_channel(code_channel) def add_channel_percent_readback(self, channel_name): percent_channel = channel(channel_name, read_function = lambda: self.get_output() / float(2**10 - 1)) return self._add_channel(percent_channel) def add_channel_resistance_readback(self, channel_name): resistance_channel = channel(channel_name, read_function = lambda: self.get_output() * self.full_scale_ohms / float(2**10 - 1)) return self._add_channel(resistance_channel) def add_channel_enable(self, channel_name): enable_channel = integer_channel(channel_name, size = 1, write_function = self.enable) return self._add_channel(enable_channel)
[docs]class CAT5140(instrument): '''ONSemi/Catalyst I2C 256 Tap Potentiometer''' def __init__(self, interface_twi): self.addr7 = 0b0101000 instrument.__init__(self, 'ONSemi/Catalyst I2C 8-bit Potentiometer at {:X}'.format(self.addr7)) self._base_name = 'CAT5140' self.add_interface_twi(interface_twi) self.twi = interface_twi self.tries = 3 def _write_byte(self, addr7, subaddr, data): tries = self.tries while tries: try: tries -= 1 self.twi.write_byte(addr7, subaddr, data) return except Exception as e: print e self.twi.resync_communication() if not tries: raise e def set_output(self, value): assert value >= 0 assert value <= 2**8-1 self._write_byte(self.addr7, 0x00, value) def get_output(self): tries = self.tries while tries: try: tries -= 1 value = self.twi.read_byte(self.addr7, 0x00) return value except Exception as e: raise e self.twi.resync_communication() raise Exception("CAT5140 Communication Failed.") def _write_percent(self,percent): '''value is between 0 and 1. DAC is biased toward 0 so that full scale is not achievable''' assert percent >= 0 assert percent <= 1 code = min(int(round(percent * 2**8)), 2**8-1) self.set_output(code) def add_channel_code(self,channel_name): code_channel = channel(channel_name, write_function =self.set_output) return self._add_channel(code_channel) def add_channel_percent(self, channel_name): percent_channel = channel(channel_name, write_function =self._write_percent) return self._add_channel(percent_channel) def add_channel_code_readback(self, channel_name): code_channel = channel(channel_name, read_function =self.get_output) return self._add_channel(code_channel) def add_channel_percent_readback(self, channel_name): percent_channel = channel(channel_name, read_function = lambda: self.get_output() / float(2**8 - 1)) return self._add_channel(percent_channel) def _select_nonvolatile_register(self): self.twi.write_byte(self.addr7, 0x08, 0x00) def _select_volatile_register(self): self.twi.write_byte(self.addr7, 0x08, 0x01) def add_channel_select_nonvolatile_register(self, channel_name): nvselect_channel = channel(channel_name, write_function = lambda x: _select_nonvolatile_register()) return self._add_channel(nvselect_channel) def add_channel_select_volatile_register(self, channel_name): volselect_channel = channel(channel_name, write_function = lambda x: _select_volatile_register()) return self._add_channel(volselect_channel)
[docs]class PCF8574(instrument): '''Multi-vendor 8bit I2C GPIO http://www.ti.com/lit/ds/symlink/pcf8574.pdf''' def __init__(self, interface_twi, addr7): '''interface_twi is a interface_twi addr7 is the 7-bit I2C address of the PCF8574 set by pinstrapping. PCF8574 has addr7 from 0x20 - 0x27 PCF8574A has addr7 from 0x38 - 0x3F''' instrument.__init__(self, 'Multi-vendor I2C GPIO at {:X}'.format(addr7)) self._base_name = 'PCF8574' self.add_interface_twi(interface_twi) self.twi = interface_twi self.addr7 = addr7 def add_channel(self, channel_name): new_channel = channel(channel_name, write_function = lambda data: self.twi.send_byte(self.addr7, data)) return self._add_channel(new_channel) def add_channel_readback(self, channel_name): new_channel = channel(channel_name, read_function = lambda: self.twi.receive_byte(self.addr7)) return self._add_channel(new_channel)
[docs]class TDS640A(scpi_instrument,delegator): '''Tek Digitizing Oscilloscope''' def __init__(self, interface_visa, force_trigger = False): '''interface_visa"''' self._base_name = 'TDS640A' scpi_instrument.__init__(self,"TDS640A @ {}".format(interface_visa)) self.add_interface_visa(interface_visa, timeout=10) delegator.__init__(self) self.get_interface().write('DATA:ENCdg ASCIi') self.get_interface().write('DATA:WID 2') self.get_interface().write('HEADER 1') #data headers help parse results of wfmoutpre? query, since different scopes return different length responses! self.force_trigger = force_trigger def add_channel_time(self,channel_name): time_channel = channel(channel_name, read_function=self._read_scope_time) time_channel.set_delegator(self) return self._add_channel(time_channel)
[docs] def add_channel(self,channel_name,scope_channel_number): '''Add named channel to instrument. num is 1-4.''' scope_channel = channel(channel_name,read_function=lambda: self._read_scope_channel(scope_channel_number)) scope_channel.set_delegator(self) return self._add_channel(scope_channel)
[docs] def trigger_force(self): '''Creates a trigger event. If TRIGger:STATE is set to READy, the acquisition will complete. Otherwise, this command will be ignored.''' self.get_interface().write('TRIGger FORCe')
def _read_scope_time(self): ''' Data conversion: voltage = [(data value - yreference) * yincrement] + yorigin time = [(data point number - xreference) * xincrement] + xorigin ''' preamble = self.get_interface().ask('WFMOUTPRE?').split(';') preamble[0] = preamble[0].split(':')[-1] #remove junk that doesn't really belong to first field preamble_dict = {} for field in preamble: name, value = field.split(' ', 1) preamble_dict[name] = value preamble_dict['NR_PT'] = int(preamble_dict['NR_PT']) preamble_dict['XINCR'] = float(preamble_dict['XINCR']) preamble_dict['PT_OFF'] = float(preamble_dict['PT_OFF']) preamble_dict['XZERO'] = float(preamble_dict['XZERO']) xpoints = map(lambda x: (x - preamble_dict['PT_OFF'])*preamble_dict['XINCR']+preamble_dict['XZERO'],range(preamble_dict['NR_PT'])) return xpoints def _read_scope_channel(self,scope_channel_number): '''return list of y-axis points for named channel list will be datalogged by logger as a string in a single cell in the table trigger=False can by used to suppress acquisition of new data by the instrument so that data from a single trigger may be retrieved from each of the four channels in turn by read_channels() ''' #trigger / single arm sequence commands need investigation. Forcing trigger here is not correct # if trigger: # self.get_interface().write('TRIGger') # Examples WFMOUTPRE? ? might return the waveform formatting data as: # [0] :WFMOUTPRE:BYT_NR 2; # [1] BIT_NR 16; # [2] ENCDG ASCII; # [3] BN_FMT RI; # [4] BYT_OR MSB; # [5] WFID "Ch1, DC coupling, 100.0mV/div, 4.000us/div, 10000 points, Sample mode"; # [6] NR_PT 10000; # [7] PT_FMT Y; # [8] XUNIT "s"; # [9] XINCR 4.0000E-9; # [10] XZERO - 20.0000E-6; # [11] PT_OFF 0; # [12] YUNIT "V"; # [13] YMULT 15.6250E-6; # [14] YOFF :"6.4000E+3; # [15] YZERO 0.0000 self.get_interface().write('DATA:SOUrce CH{}'.format(scope_channel_number)) preamble = self.get_interface().ask('WFMOUTPRE?').split(';') preamble[0] = preamble[0].split(':')[-1] #remove junk that doesn't really belong to first field preamble_dict = {} for field in preamble: name, value = field.split(' ', 1) preamble_dict[name] = value preamble_dict['YMULT'] = float(preamble_dict['YMULT']) #scale int to volts preamble_dict['YZERO'] = float(preamble_dict['YZERO']) #offset set into scope, subtract from raw_data before scaling if you want data offset preamble_dict['YOFF'] = float(preamble_dict['YOFF']) #waveform position raw_data = self.get_interface().ask('CURVe?') raw_data = raw_data.split(',') #not sure where y_zero goes in eqn! #offset seems to be display only raw_data[0] = raw_data[0].split(' ')[-1] data = map(lambda x: (int(x)-preamble_dict['YOFF'])*preamble_dict['YMULT']+preamble_dict['YZERO'], raw_data) #TODO - implement binary transfer if speed becomes a problem return data def read_delegated_channel_list(self,channels): if self.force_trigger: self.trigger_force() results = {} for channel in channels: results[channel.get_name()] = channel.read_without_delegator() return results
[docs]class fluke_45(scpi_instrument): '''single channel fluke_45 meter defaults to dc voltage, note this instrument currently does not support using multiple measurement types at the same time''' def __init__(self,interface_visa): '''interface_visa''' self._base_name = 'fluke_45' scpi_instrument.__init__(self,"f25 @ {}".format(interface_visa)) self.add_interface_visa(interface_visa) self.config_dc_voltage()
[docs] def config_dc_voltage(self, range="AUTO", rate="S"): '''Configure meter for DC voltage measurement. Optionally set range and rate''' self._config("VDC ", range, rate)
[docs] def config_dc_current(self, range="AUTO", rate="S"): '''Configure meter for DC current measurement. Optionally set range and rate''' self._config("ADC ", range, rate)
[docs] def config_ac_voltage(self, range="AUTO", rate="S"): '''Configure meter for AC voltage measurement. Optionally set range and rate''' self._config("VAC ", range, rate)
[docs] def config_ac_current(self, range="AUTO", rate="S"): '''Configure meter for AC current measurement. Optionally set range and rate''' self._config("AAC ", range, rate)
[docs] def add_channel(self,channel_name): '''Add named channel to instrument without configuring measurement type.''' meter_channel = channel(channel_name,read_function=self.read_meter) return self._add_channel(meter_channel)
[docs] def read_meter(self): '''Return float representing meter measurement. Units are V,A,Ohm, etc depending on meter configuration.''' return float(self.get_interface().ask("MEAS1?"))
def _config(self, command_string, range, rate): RANGE_SETTINGS = ["AUTO", 1, 2, 3, 4, 5, 6, 7] RATE_SETTINGS = ["S", "M", "F"] if range not in RANGE_SETTINGS: raise Exception("Error: Not a valid range setting, valid settings are:" + str(RANGE_SETTINGS)) if rate not in RATE_SETTINGS: raise Exception("Error: Not a valid rate setting, valid settings are:" + str(RATE_SETTINGS)) self.get_interface().write(command_string) self.get_interface().write("AUTO " if range == "AUTO" else "RANGE " + str(range)) self.get_interface().write("RATE " + str(rate))
[docs]class saleae(instrument, delegator): '''analog DAQ intrument using Saleae Logic Pro hardware. Requires https://pypi.python.org/pypi/saleae Also requires Saleae Logic software GUI to be running to listen on TCP remote control port. Digital channels not supported (yet). (Mixed analog/digital capture file binary unsupported.)''' def __init__(self,host=u'localhost', port=10429): import saleae as saleae_lib instrument.__init__(self,"Saleae Logic @ {}:{}".format(host,port)) delegator.__init__(self) self._saleae = saleae_lib.Saleae(host,port) self._channels = [] self.set_num_samples(100) self.set_sample_rate() #max rate
[docs] def set_num_samples(self, num_samples_per_channel): '''set number of samples to average and sample rate because valid sample rates change with ???number of configured channels???, may need to call after adding all channels.''' self._saleae.set_num_samples(num_samples_per_channel)
def set_sample_rate(self, sample_rate=None): sample_rates = [i[1] for i in self._saleae.get_all_sample_rates()] if sample_rate is not None: if sample_rate in sample_rates: self._saleae.set_sample_rate((0,sample_rate)) else: sample_rates = [i[1] for i in self._saleae.get_all_sample_rates()] else: self._saleae.set_sample_rate((0,sample_rates[0])) return sample_rates def get_sample_rates(self): return [i[1] for i in self._saleae.get_all_sample_rates()] def _set_active_channels(self): self._saleae.set_active_channels(digital=[],analog=self._channels)
[docs] def add_channel_scalar(self,channel_name, channel_number, scaling=1.0): '''Add analog scalar (DMM) DAQ channel to instrument. channel_number is 0-7 or 0-15 for Logic Pro 8 and Logic Pro 16 respectively.''' assert isinstance(channel_number,int) assert channel_number < 16 assert channel_number not in self._channels new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_number)) new_channel.set_attribute('channel',channel_number) new_channel.set_attribute('type','scalar') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) self._channels.append(channel_number) self._set_active_channels() return self._add_channel(new_channel)
[docs] def add_channel_trace(self,channel_name, channel_number, scaling=1.0): '''Add analog vector trace (scope) DAQ channel to instrument. channel_number is 0-7 or 0-15 for Logic Pro 8 and Logic Pro 16 respectively.''' assert isinstance(channel_number,int) assert channel_number < 16 assert channel_number not in self._channels new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_number)) new_channel.set_attribute('channel',channel_number) new_channel.set_attribute('type','vector') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) self._channels.append(channel_number) self._set_active_channels() return self._add_channel(new_channel)
def _dummy_read(self, index): raise Exception("Shouldn't be here...") def _read_from_file(self, file, bytes, timeout=5): '''intermittently, data is slow to flush to disk...''' start_time = time.time() str = file.read(bytes) while len(str) < bytes: if (time.time() - start_time) > timeout: raise Exception('Saleae communication failure') str += file.read(bytes-len(str)) return str
[docs] def read_delegated_channel_list(self,channels): '''private''' results_dict = {} temp_dir = tempfile.mkdtemp(prefix='saleae_tmp') temp_file = temp_dir + '/capture.logicdata' self._saleae.capture_start() while not self._saleae.is_processing_complete(): pass ch_list = [ch.get_attribute('channel') for ch in channels] self._saleae.export_data2(temp_file, digital_channels=[], analog_channels=ch_list, format='binary') # while not self._saleae.is_processing_complete(): pass with open(temp_file + '.bin', 'r+b') as logicdata: num_samples_per_channel = struct.unpack('<Q', self._read_from_file(logicdata, 8))[0] num_channels = struct.unpack('<L',self._read_from_file(logicdata, 4))[0] sample_period = struct.unpack('<d',self._read_from_file(logicdata, 8))[0] assert len(channels) == num_channels for channel in channels: if channel.get_attribute('type') == 'scalar': channel_avg = 0 for sample in range(num_samples_per_channel): channel_avg += struct.unpack('<f',self._read_from_file(logicdata, 4))[0] channel_avg /= num_samples_per_channel results_dict[channel.get_name()] = channel_avg elif channel.get_attribute('type') == 'vector': channel_data = [] for sample in range(num_samples_per_channel): channel_data.append(struct.unpack('<f',logicdata.read(4))[0]) results_dict[channel.get_name()] = channel_data else: raise Exception('Bad "type" channel attribute.') logicdata.close() os.remove(temp_file + '.bin') os.rmdir(temp_dir) return results_dict
[docs]class firmata(instrument): ''' Firmata is a protocol for communicating with microcontrollers from software on a host computer. The protocol can be implemented in firmware on any microcontroller architecture as well as software on any host computer software package. The Arduino repository described here is a Firmata library for Arduino and Arduino-compatible devices. If you would like to contribute to Firmata, please see the Contributing section below. Usage The model supported by PyICe is to load a general purpose sketch called StandardFirmata on the Arduino board and then use the host computer exclusively to interact with the Arduino board. *** Arduino/Linduno specific: *** StandardFirmata is located in the Arduino IDE in File -> Examples -> Firmata. *** This sketch must be flashed onto the microcontroller board to use this instrument driver. *** Other microcontrollers: *** Must flash architechture-specific compiled embedded server. https://github.com/firmata/protocol/blob/master/README.md https://github.com/firmata/arduino/blob/master/readme.md https://github.com/MrYsLab/PyMata/blob/master/README.md ''' def __init__(self,serial_port_name): ''' 1) note that this makes its own serial object. Can't use master.get_raw_serial_interface and this will read/write in its own thread. 2) inherit from delegator and aggregate pin states with single query??? ''' try: from PyMata.pymata import PyMata except ImportError as e: print "*** Please install the Pymata module." print "*** https://github.com/MrYsLab/PyMata/blob/master/README.md" print "*** Try typing 'pip install pymata' or 'python PyICe/deps/PyMata_v2.09/setup.py install'." raise e self._base_name = 'Firmata' instrument.__init__(self,"Firmata Client @ {}".format(serial_port_name)) self.ser_name = serial_port_name self.firmata_board = PyMata(self.ser_name, bluetooth=False, verbose=True) #verbose doesn't seem to print much except board capabilities upon init self._configured_pins = {} self._configured_pins[self.firmata_board.ANALOG] = {} self._configured_pins[self.firmata_board.DIGITAL] = {} def _check_configure_pin(self, pin, channel): if pin in self._configured_pins[channel.get_attribute('pin_type')]: raise Exception('Cannot configure {} pin {} as type {}. Pin already configured as type {}.'.format('Analog' if channel.get_attribute('pin_type') == self.firmata_board.ANALOG else 'Digital', pin, channel.get_attribute('channel_type'), self._configured_pins[channel.get_attribute('pin_type')][pin].get_attribute('channel_type'))) else: self._configured_pins[channel.get_attribute('pin_type')][pin] = channel self.firmata_board.set_pin_mode(pin, mode=channel.get_attribute('mode'), pin_type=channel.get_attribute('pin_type'))
[docs] def add_channel_digital_input(self, channel_name, pin, enable_pullup=False): '''Digital input pin. Use higher-numbered digital pin aliasing to use analog pins as digital. For Arduno Uno/Linduino: A0=14 A1=15 A2=16 A3=17 A4=18 A5=19 Set enable_pullup=True to enable on-board uC pullups. (~20k in Arduino Uno/Linduino AtMega328P) ''' new_channel = integer_channel(channel_name, size=1, read_function=lambda: self.firmata_board.digital_read(pin)) new_channel.set_attribute('channel_type','DIGITAL_INPUT') new_channel.set_attribute('pin',pin) new_channel.set_attribute('mode',self.firmata_board.INPUT) new_channel.set_attribute('pin_type',self.firmata_board.DIGITAL) new_channel.set_attribute('pullup_enabled',enable_pullup) self._check_configure_pin(pin, new_channel) self.firmata_board.digital_write(pin, 1 if enable_pullup else 0) new_channel.set_description(self.get_name() + ': ' + self.add_channel_digital_input.__doc__) return self._add_channel(new_channel) # def add_channel_digital_input_bus(self, channel_name, pin_list): # '''Multi-pin digital input bus.''' # raise Exception('Not yet implemented')
[docs] def add_channel_digital_output(self, channel_name, pin): '''Digital output pin. analog_pin argument doesn't function. Use higher-numbered digital pin aliasing to use analog pins as digital. For Arduno Uno/Linduino: A0=14 A1=15 A2=16 A3=17 A4=18 A5=19 ''' new_channel = integer_channel(channel_name, size=1, write_function=lambda value: self.firmata_board.digital_write(pin, value)) new_channel.set_attribute('channel_type','DIGITAL_OUTPUT') new_channel.set_attribute('pin',pin) new_channel.set_attribute('mode',self.firmata_board.OUTPUT) new_channel.set_attribute('pin_type',self.firmata_board.DIGITAL) self._check_configure_pin(pin, new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel_digital_output.__doc__) return self._add_channel(new_channel) # def add_channel_digital_output_bus(self, channel_name, pin_list): # '''Multi-pin digital output bus.''' # raise Exception('Not yet implemented')
[docs] def add_channel_analog_input(self, channel_name, pin): '''Analog input pin.''' new_channel = integer_channel(channel_name, size=10, read_function=lambda: self.firmata_board.analog_read(pin)) #don't really know size, but it doesn't matter. 10-bit answer on Arduino hardware new_channel.set_attribute('channel_type','ANALOG_INPUT') new_channel.set_attribute('pin',pin) new_channel.set_attribute('mode',self.firmata_board.INPUT) new_channel.set_attribute('pin_type',self.firmata_board.ANALOG) self._check_configure_pin(pin, new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel_analog_input.__doc__) new_channel.add_format(format_name='arduino_adc_5v', format_function=lambda code: code*5.0/1024, unformat_function=lambda analog: int(round(analog/5.0*1024)), signed=False, units='V') new_channel.add_format(format_name='arduino_adc_3v3', format_function=lambda code: code*3.3/1024, unformat_function=lambda analog: int(round(analog/3.3*1024)), signed=False, units='V') #Not sure if Firmata protocol allows ADC reference switch.... #new_channel.add_format(format_name='arduino_adc_1v1', format_function=lambda code: code*1.1/1024, unformat_function=lambda analog: int(round(analog/1.1*1024)), signed=False, units='V') #new_channel.add_format(format_name='arduino_adc_2v56', format_function=lambda code: code*2.56/1024, unformat_function=lambda analog: int(round(analog/2.56*1024)), signed=False, units='V') return self._add_channel(new_channel)
[docs] def add_channel_pwm_output(self, channel_name, pin): '''PWM output pin. Arduino UNO (Atmega328) compatible with digital pins 3,5,6,9,10,11. ''' new_channel = integer_channel(channel_name, size=8, write_function=lambda value: self.firmata_board.analog_write(pin, value)) #8-bit hardware is Arduino specific (0-255 valide PWM values) new_channel.set_attribute('channel_type','PWM_OUTPUT') new_channel.set_attribute('pin',pin) new_channel.set_attribute('mode',self.firmata_board.PWM) new_channel.set_attribute('pin_type',self.firmata_board.DIGITAL) self._check_configure_pin(pin, new_channel) new_channel.set_description(self.get_name() + ': ' + self.add_channel_pwm_output.__doc__) new_channel.add_format(format_name='arduino_pwm_dc', format_function=lambda code: code*1.0/255, unformat_function=lambda analog: int(round(analog/1.0*255)), signed=False, units='') new_channel.add_format(format_name='arduino_pwm_5v', format_function=lambda code: code*5.0/255, unformat_function=lambda analog: int(round(analog/5.0*255)), signed=False, units='V') new_channel.add_format(format_name='arduino_pwm_3v3', format_function=lambda code: code*3.3/255, unformat_function=lambda analog: int(round(analog/3.3*255)), signed=False, units='V') return self._add_channel(new_channel)
[docs] def add_channel_servo(self, channel_name, pin): '''RC servo control (544ms-2400ms PWM) output.''' raise Exception('Not yet implemented') #self.firmata_board.servo_config(pin, min_pulse=544, max_pulse=2400)
[docs] def add_channel_digital_latch(self, channel_name, digital_input_channel, threshold_high=True): '''Latch transient signals on a digital input pin. Software logic appears to be edge-triggered. Input pin channel must have been previously configured with firmata.add_channel_digital_input(). Pass channel object instance to digital_input_channel argument. latches rising edge by default. Set threshold_high=False to set latch sensitivity to logic low. this doesn't appear to have access to analog pins (A0-A5) used as digital IO. ''' def read_latch_status(latch_channel): latch_status = self.firmata_board.get_digital_latch_data(latch_channel.get_attribute('pin')) latch_state = True if latch_status[1] == self.firmata_board.LATCH_LATCHED else False if latch_state: #re-arm latch self.firmata_board.set_digital_latch(pin=latch_channel.get_attribute('pin'), threshold_type=latch_channel.get_attribute('threshold_type')) return True return False new_channel = integer_channel(channel_name, size=1, read_function=lambda: None) #dummy read function until channel instance is created new_channel._read = lambda: read_latch_status(new_channel) #get reference back to channel for attribute lookup new_channel.set_attribute('channel_type','DIGITAL_LATCH') new_channel.set_attribute('pin',digital_input_channel.get_attribute('pin')) new_channel.set_attribute('threshold_type',self.firmata_board.DIGITAL_LATCH_HIGH if threshold_high else self.firmata_board.DIGITAL_LATCH_LOW) new_channel.set_attribute('parent_channel', digital_input_channel) assert digital_input_channel.get_attribute('channel_type') == 'DIGITAL_INPUT' self.firmata_board.set_digital_latch(pin=new_channel.get_attribute('pin'), threshold_type=new_channel.get_attribute('threshold_type')) new_channel.set_description(self.get_name() + ': ' + self.add_channel_digital_latch.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_analog_latch(self, channel_name, analog_input_channel, threshold, threshold_type='>'): '''Latch transient signals on an analog (ADC) input pin. Input pin channel must have been previously configured with firmata.add_channel_analog_input(). Pass channel object instance to analog_input_channel argument. threshold is in Volts, assuming 5V Arduino ADC full scale (1023). Change format setting of _thresold channel to use raw (0-1023) or 3.3V ADC scales. latches high signal levels by default. Set threshold_type='>=', '<' or to set latch sensitivity to logic low. ''' def read_latch_status(latch_channel): latch_status = self.firmata_board.get_analog_latch_data(latch_channel.get_attribute('pin')) latch_state = True if latch_status[1] == self.firmata_board.LATCH_LATCHED else False if latch_state: #re-arm latch th_ch = latch_channel.get_attribute('threshold_channel') th_ch.write(th_ch.read()) return True return False assert analog_input_channel.get_attribute('channel_type') == 'ANALOG_INPUT' latch_channel = integer_channel(channel_name, size=1, read_function=lambda: None) #dummy read function until channel instance is created latch_channel._read = lambda: read_latch_status(latch_channel) #get reference back to channel for attribute lookup latch_channel.set_attribute('channel_type','ANALOG_LATCH') latch_channel.set_attribute('pin',analog_input_channel.get_attribute('pin')) latch_channel.set_attribute('parent_channel', analog_input_channel) latch_channel.set_description(self.get_name() + ': ' + self.add_channel_analog_latch.__doc__) if threshold_type == '>': latch_channel.set_attribute('threshold_type',self.firmata_board.ANALOG_LATCH_GT) elif threshold_type == '<': latch_channel.set_attribute('threshold_type',self.firmata_board.ANALOG_LATCH_LT) elif threshold_type == '>=': latch_channel.set_attribute('threshold_type',self.firmata_board.ANALOG_LATCH_GTE) elif threshold_type == '<=': latch_channel.set_attribute('threshold_type',self.firmata_board.ANALOG_LATCH_LTE) else: raise Exception("threshold_type: {} not in ['>', '<', '>=', '<=']".format(threshold_type)) threshold_channel = integer_channel(channel_name + '_threshold', size=10, write_function=lambda threshold: self.firmata_board.set_analog_latch(pin=latch_channel.get_attribute('pin'), threshold_type=latch_channel.get_attribute('threshold_type'), threshold_value=threshold)) threshold_channel.add_format(format_name='arduino_adc_5v', format_function=lambda code: code*5.0/1024, unformat_function=lambda analog: int(round(analog/5.0*1024)), signed=False, units='V') threshold_channel.add_format(format_name='arduino_adc_3v3', format_function=lambda code: code*3.3/1024, unformat_function=lambda analog: int(round(analog/3.3*1024)), signed=False, units='V') threshold_channel.set_format('arduino_adc_5v') threshold_channel.write(threshold) threshold_channel.set_attribute('threshold_type',threshold_type) threshold_channel.set_attribute('parent_channel', analog_input_channel) threshold_channel.set_attribute('latch_channel', latch_channel) latch_channel.set_attribute('threshold_channel', threshold_channel) self._add_channel(threshold_channel) return self._add_channel(latch_channel) ############################################### # Virtual Instruments # ###############################################
[docs]class instrument_humanoid(instrument): '''Notification helper to put human control of a manual instrument into an otherwise automated measurement.''' def __init__(self, notification_function=None): '''Notification will be sent to notification_function when a write occurs to any channel in this instrument. The function should take a single string argument and deliver it to the user as appropriate (sms, email, etc). Hint: Use a lambda function to include a subject line in the email: myemail = lab_utils.email(destination='myemail@linear.com') notification_function=lambda msg: myemail.send(msg,subject="LTC lab requires attention!") If notification_function is None, messages will only be sent to the terminal.''' instrument.__init__(self,"manaul instrument interface notifying via {}".format(notification_function)) self._base_name = 'Human Feedback' self.notification_functions = [] if notification_function is not None: self.add_notification_function(notification_function) self.enable_notifications = True
[docs] def add_notification_function(self, notification_function): '''Add additional notification function to instrument. Ex email and SMS. Notification will be sent to notification_function when a write occurs to any channel in this instrument. The function should take a single string argument and deliver it to the user as appropriate (sms, email, etc). Hint: Use a lambda function to include a subject line in the email: myemail = lab_utils.email(destination='myemail@linear.com') notification_function=lambda msg: myemail.send(msg,subject="LTC lab requires attention!")''' self.notification_functions.append(notification_function)
[docs] def add_channel_notification_enable(self, channel_name): '''Hook to temporarily suspend notifications, ex for initial setup.''' new_channel = channel(channel_name,write_function=self.set_notification_enable) new_channel.set_description(self.get_name() + ': ' + self.add_channel_notification_enable.__doc__) return self._add_channel(new_channel)
[docs] def set_notification_enable(self, enabled): '''non-channel hook to enable/disable notifications''' if enabled: self.enable_notifications = True else: self.enable_notifications = False
[docs] def add_channel(self,channel_name): '''add new channel named channel_name. Writes to channel_name will send a notification using notification_function and will block until the user acknowledges (in the terminal) that they have intervened as appropriate. Useful for including manual instruments in an otherwise automated setup. To set delay after changing channel, use set_write_delay() method of returned channel.''' new_channel = channel(channel_name,write_function=lambda value: self._write(channel_name, value)) new_channel.set_description(self.get_name() + ': ' + self.add_channel.__doc__) return self._add_channel(new_channel)
def _write(self, channel_name, value): if self.enable_notifications: now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") msg = 'Please modify channel: {} to value: {} now. ({})'.format(channel_name, value, now) for notification_function in self.notification_functions: notification_function(msg) print msg raw_input('Then press any key to continue.')
[docs]class delay_loop(lab_utils.delay_loop, instrument): '''instrument wrapper for lab_utils.delay_loop enables logging of delay diagnostic variables''' def __init__(self, strict=False, begin=True, no_drift=True): '''Set strict to True to raise an Exception if loop time is longer than requested delay. Timer will automatically begin when the object is instantiated if begin=True. To start timer only when ready, set begin=False and call begin() method to start timer. If no_drift=True, delay loop will manage loop time over-runs by debiting extra time from next cycle. This insures long-term time stability at the expense of increased jitter. Windows task switching can add multi-mS uncertainty to each delay() call, which can accumulate if not accounted for. Set no_drift=False to ignore time over-runs when computing next delay time. ''' instrument.__init__(self, "delay_loop instrument wrapper") lab_utils.delay_loop.__init__(self, strict, begin, no_drift) self._base_name = 'Precision Delay Loop Virtual Instrument Wrapper'
[docs] def add_channel_count(self, channel_name): '''total number of times delay() method called''' new_channel = channel(channel_name,read_function=self.get_count) new_channel.set_description(self.get_name() + ': ' + self.add_channel_count.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_total_time(self, channel_name): '''total number of seconds since delay() method first called''' new_channel = channel(channel_name,read_function=self.get_total_time) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_time.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delay_margin(self, channel_name): '''time remaining after user's loop tasks completed to sleep before start of next cycle. Negative if user tasks exceed loop time and no time is left to sleep. Includes any make-up contribution if previous iterations over-ran allocated loop time with no_drift attribute set.''' new_channel = channel(channel_name,read_function=self.delay_margin) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delay_margin.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_achieved_loop_time(self, channel_name): '''actual time spent during in last loop iteration possibly longer than requested loop time if user taskes exceeded requested time (overrun).''' new_channel = channel(channel_name,read_function=self.achieved_loop_time) new_channel.set_description(self.get_name() + ': ' + self.add_channel_achieved_loop_time.__doc__) return self._add_channel(new_channel)
[docs]class clipboard(instrument): '''Virtual instrument to exchange data with Windows/Linux clipboard for interactive copy and paste with another application.''' def __init__(self): instrument.__init__(self, 'Clipboard Exchange Virtual Instrument') self._base_name = 'Copy/Past Clipboard Virtual Instrument' try: import pyperclip #cross-platform #https://pyperclip.readthedocs.org/ #https://github.com/asweigart/pyperclip self._lib = pyperclip self._copy = self._pyperclip_copy self._paste = self._pyperclip_paste except ImportError: print 'pyperclip dependency not found. Attempting to use win32clipboard.' try: import win32clipboard #http://docs.activestate.com/activepython/2.4/pywin32/win32clipboard.html self._lib = win32clipboard self._copy = self._win32_copy self._paste = self._win32_paste except ImportError: raise Exception('Clipboard virtual instrument requires either pyperclip or pywin32 module win32clipboard.')
[docs] def add_channel_copy(self,channel_name): '''Place data written to channel_name onto clipboard.''' new_channel = channel(channel_name,write_function=self._copy) new_channel.set_description(self.get_name() + ': ' + self.add_channel_copy.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_paste(self,channel_name): '''Place data from clipboard into channel_name.''' new_channel = channel(channel_name,read_function=self._paste) new_channel.set_description(self.get_name() + ': ' + self.add_channel_paste.__doc__) return self._add_channel(new_channel)
[docs] def register_copy_channel(self, channel_object, write_copy=True): '''Automatically places results on clipboard each time channel_object is read and optionally when channel_object is written.''' channel_object.add_read_callback(lambda channel_object,read_value: self._copy(read_value)) if write_copy: channel_object.add_write_callback(lambda channel_object,write_value: self._copy(write_value))
def _copy(self,clipboard_data): '''place clipboard_data onto OS clipboard''' raise Exception('Overloaded implementation is library specific.') def _paste(self): '''return OS clipboard contents''' raise Exception('Overloaded implementation is library specific.') def _pyperclip_copy(self,clipboard_data): self._lib.copy(str(clipboard_data)) def _pyperclip_paste(self): return self._lib.paste() def _win32_copy(self,clipboard_data): self._lib.OpenClipboard() self._lib.EmptyClipboard() self._lib.SetClipboardText(str(clipboard_data)) self._lib.CloseClipboard() def _win32_paste(self): self._lib.OpenClipboard() data = self._lib.GetClipboardData() self._lib.CloseClipboard() return data
[docs]class accumulator(instrument): '''Virtual accumulator instrument. Writable channel adds value to stored total. Readable channel returns accumulation total. Can be used as a counter by writing accumulation value to +/-1''' def __init__(self, init=0): '''Init sets initial accumulation total. Defaults to 0.''' self._base_name = "Accumulator Virtual Instrument" instrument.__init__(self, self._base_name) self.accumulation = init
[docs] def add_channel_accumulation(self,channel_name): '''Channel reads return total accumulated quantity.''' new_channel = channel(channel_name,read_function=lambda: self.accumulation) new_channel.set_description(self.get_name() + ': ' + self.add_channel_accumulation.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_accumulate(self,channel_name): '''Channel writes accumulate value into total previously accumulated quantity.''' new_channel = channel(channel_name,write_function=self.accumulate) new_channel.set_description(self.get_name() + ': ' + self.add_channel_accumulate.__doc__) return self._add_channel(new_channel)
[docs] def accumulate(self, value): '''Adds value to accumulation total. Use with caution outside channel framework.''' self.accumulation += value
[docs]class timer(instrument, delegator): '''Virtual timer instrument. All channels are read only and return time since either last read or first read, scaled to appropriate time units. All channels operate from a common timebase.''' def __init__(self): self._base_name = 'Timer Virtual Instrument' delegator.__init__(self) instrument.__init__(self, self._base_name) self.divs = { 'seconds' : 1.0, 'minutes' : 60.0, 'hours' : 3600.0, 'days' : 86400.0, #weeks, years? } self.last_time = None self.elapsed = None self._paused = False def _dummy_read(self, channel_name): return self.results_dict[channel_name]
[docs] def add_channel_total_seconds(self,channel_name): '''Channel read reports elapsed time since first read with units of seconds.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['seconds']) new_channel.set_attribute('type','total_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_seconds.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_total_minutes(self,channel_name): '''Channel read reports elapsed time since first read with units of minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['minutes']) new_channel.set_attribute('type','total_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_minutes.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_total_hours(self,channel_name): '''Channel read reports elapsed time since first read with units of hours.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['hours']) new_channel.set_attribute('type','total_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_hours.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_total_days(self,channel_name): '''Channel read reports elapsed time since first read with units of days.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['days']) new_channel.set_attribute('type','total_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_days.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_total_scale(self,channel_name,time_div): '''Channel read reports elapsed time since first read with user supplied time units. time_div is seconds per user-unit, eg 60 for minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',float(time_div)) new_channel.set_attribute('type','total_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_total_scale.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delta_seconds(self,channel_name): '''Channel read reports elapsed time since last read with units of seconds.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['seconds']) new_channel.set_attribute('type','delta_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delta_seconds.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delta_minutes(self,channel_name): '''Channel read reports elapsed time since last read with units of minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['minutes']) new_channel.set_attribute('type','delta_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delta_minutes.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delta_hours(self,channel_name): '''Channel read reports elapsed time since last read with units of hours.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['hours']) new_channel.set_attribute('type','delta_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delta_hours.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delta_days(self,channel_name): '''Channel read reports elapsed time since last read with units of days.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['days']) new_channel.set_attribute('type','delta_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delta_days.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_delta_scale(self,channel_name,time_div): '''Channel read reports elapsed time since last read with user supplied time units. time_div is seconds per user-unit, eg 60 for minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',float(time_div)) new_channel.set_attribute('type','delta_timer') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_delta_scale.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_frequency_hz(self,channel_name): '''Channel read reports read frequency in Hz.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['seconds']) new_channel.set_attribute('type','frequency') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_frequency_hz.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_frequency_scale(self,channel_name,time_div): '''Channel read reports read frequency with user supplied time units. time_div is seconds per user-unit, eg 60 for RPM.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',float(time_div)) new_channel.set_attribute('type','frequency') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_frequency_scale.__doc__) return self._add_channel(new_channel)
def _compute_delta(self): if not self._paused: self.this_time = datetime.datetime.now() if self.last_time is None: self.last_time = self.this_time self.total_time = datetime.timedelta(0) self.elapsed = self.this_time - self.last_time self.total_time += self.elapsed self.last_time = self.this_time
[docs] def reset_timer(self): '''Resets timer to 0. Use with caution outside channel framework.''' self.last_time = None self._compute_delta()
[docs] def stop_and_reset_timer(self): '''Halts and resets timer to 0. Timer will begin running after first read, same behavior as after timer object instantiation. Use with caution outside channel framework.''' self.last_time = None
[docs] def pause_timer(self): '''pause timer . Call resume_timer() to continue counting.''' if self._paused: raise Exception('Attemped to pause already paused timer.') else: #update internal variables because _compute_delta won't while paused self._pause_time = datetime.datetime.now() self.elapsed = self._pause_time - self.last_time #shouldn't ever pause a timer before it starts... self.total_time += self.elapsed self._paused = True
[docs] def resume_timer(self): '''resume timer . Call pause_timer() to stop counting again. Can also call resume_timer() at the beginning of time to start the timer.''' if self._paused: time_paused = datetime.datetime.now() - self._pause_time self.total_time -= self.elapsed #undo temporatry accumulation during pause_timer() self.last_time += time_paused #time warp self._paused = False elif self.last_time is None: #allow unpause of never-started timer self.reset_timer() else: raise Exception('Attemped to resume unpaused timer.')
[docs] def read_delegated_channel_list(self,channels): '''private''' self._compute_delta() self.results_dict = {} for channel in channels: if channel.get_attribute('type') == 'delta_timer': self.results_dict[channel.get_name()] = self.elapsed.total_seconds() / channel.get_attribute('time_div') elif channel.get_attribute('type') == 'total_timer': self.results_dict[channel.get_name()] = self.total_time.total_seconds() / channel.get_attribute('time_div') elif channel.get_attribute('type') == 'frequency': try: self.results_dict[channel.get_name()] = channel.get_attribute('time_div') / self.elapsed.total_seconds() except ZeroDivisionError: self.results_dict[channel.get_name()] = None else: raise Exception('Unknown channel type: {}'.format(channel.get_attribute('type'))) channel.read_without_delegator() return self.results_dict
[docs]class integrator(accumulator, timer): '''Virtual integrator instrument. Integrate channel is writable and accumulates value to internally stored total, multiplied by elapsed time since last integrate channel write. Integration channels are read only and return integration total, scaled to appropriate time units. Timer channels are read-only and return elapsed time used to compute time time differential, scaled to appropriate time units. A readable channel from a different instrument can be registered with this instrument so that any read of that channel causes its value to be integrated automatically without requiring an explicit call to this instrument's integrate method or channel. All channels operate from a common timebase.''' def __init__(self, init=0): '''Init sets initial accumulation total. Defaults to 0.''' accumulator.__init__(self, init) timer.__init__(self) self._base_name = 'Integrator Virtual Instrument' instrument.__init__(self, self._base_name) self.last_value = None
[docs] def add_channel_integration_seconds(self,channel_name): '''Channel read reports integration value with time units of seconds.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['seconds']) new_channel.set_attribute('type','integrator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integration_seconds.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_integration_minutes(self,channel_name): '''Channel read reports integration value with time units of minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['minutes']) new_channel.set_attribute('type','integrator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integration_minutes.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_integration_hours(self,channel_name): '''Channel read reports integration value with time units of hours.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['hours']) new_channel.set_attribute('type','integrator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integration_hours.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_integration_days(self,channel_name): '''Channel read reports integration value with time units of days.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['days']) new_channel.set_attribute('type','integrator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integration_days.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_integration_scale(self,channel_name,time_div): '''Channel read reports integration value with user supplied time units. time_div is seconds per user-unit, eg 60 for minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',float(time_div)) new_channel.set_attribute('type','integrator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integration_scale.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_integrate(self,channel_name): '''Writing to this channel causes written value to be added to accumulator scaled by elapsed time since last write.''' new_channel = channel(channel_name,write_function=self.integrate) new_channel.set_description(self.get_name() + ': ' + self.add_channel_integrate.__doc__) return self._add_channel(new_channel)
[docs] def register_integrand_channel(self, channel_object): '''Automatically calls integrate method each time channel_object is read, for example in logger.log().''' channel_object.add_read_callback(lambda channel_object,read_value: self.integrate(read_value))
[docs] def integrate(self, value): '''Scale value by elapsed time and store to accumulator. Should typically be used through integrate channel above.''' #results stored internally in value*seconds. Read channels scale to other time units on the way out. self._compute_delta() if not self._paused: if self.last_value is not None: interval_val = (value + self.last_value) * 0.5 #interval_val = value self.accumulate(interval_val * self.elapsed.total_seconds()) self.last_value = value #first order hold to record interval average rather than endpoint
[docs] def read_delegated_channel_list(self,channels): '''private''' self.results_dict = {} for channel in channels: if channel.get_attribute('type') == 'integrator': self.results_dict[channel.get_name()] = self.accumulation / channel.get_attribute('time_div') #actually returns 0 first time #elif self.elapsed is None: #first read; can't call total_seconds() method of NoneType # results_dict[channel.get_name()] = None elif channel.get_attribute('type') == 'delta_timer': try: self.results_dict[channel.get_name()] = self.elapsed.total_seconds() / channel.get_attribute('time_div') except AttributeError, TypeError: #first read before call to integrate() self.results_dict[channel.get_name()] = None elif channel.get_attribute('type') == 'total_timer': try: self.results_dict[channel.get_name()] = self.total_time.total_seconds() / channel.get_attribute('time_div') except AttributeError, TypeError: #first read before call to integrate() self.results_dict[channel.get_name()] = None else: raise Exception('Unknown channel type: {}'.format(channel.get_attribute('type'))) channel.read_without_delegator() return self.results_dict
[docs]class differencer(instrument): '''Virtual differencer instrument. Compute_difference channel is writable and causes computation of first difference from last written value. Read_difference channel is read-only and returns computed difference. A readable channel from a different instrument can be registered with this instrument so that any read of that channel causes its value to be differenced automatically without requiring an explicit call to this instrument's difference method.''' def __init__(self,init=None): '''Init sets initial value of previous value used to compute difference. Defaults to None.''' self.last_value = init self.diff = None self._base_name = 'Differencer Virtual Instrument' instrument.__init__(self, self._base_name)
[docs] def add_channel_read_difference(self, channel_name): '''Channel read returns difference between previous two values passed to difference method.''' new_channel = channel(channel_name,read_function=lambda: self.diff) new_channel.set_description(self.get_name() + ': ' + self.add_channel_read_difference.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_compute_difference(self, channel_name): '''Channel write computes difference between previous two values passed to difference method.''' new_channel = channel(channel_name,write_function=self.difference) new_channel.set_description(self.get_name() + ': ' + self.add_channel_compute_difference.__doc__) return self._add_channel(new_channel)
[docs] def difference(self, value): '''Returns difference between value and value passed in last method call.''' if self.last_value is None: self.diff = None else: self.diff = value - self.last_value self.last_value = value return self.diff
[docs] def register_difference_channel(self, channel_object): '''Automatically calls difference method each time channel_object is read, for example in logger.log().''' channel_object.add_read_callback(lambda channel_object,read_value: self.difference(read_value))
[docs]class differentiator(timer,differencer): '''Virtual differentiator instrument. Differentiate channel is writable and causes computation of first time derivative between value and last written value. Differentiation channels are read-only and return previously computed time derivative, scaled to appropriate time units. Timer channels are read-only and return elapsed time used to compute derivative, scaled to appropriate time units. A readable channel from a different instrument can be registered with this instrument so that any read of that channel causes its value to be differentiated automatically without requiring an explicit call to this instrument's differentiate method or channel.''' def __init__(self): timer.__init__(self) differencer.__init__(self) self._base_name = 'Differentiator Virtual Instrument' instrument.__init__(self, self._base_name) self.derivative = None
[docs] def add_channel_differentiation_seconds(self,channel_name): '''Channel read reports derivative value with time units of seconds.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['seconds']) new_channel.set_attribute('type','differentiator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiation_seconds.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_differentiation_minutes(self,channel_name): '''Channel read reports derivative value with time units of minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['minutes']) new_channel.set_attribute('type','differentiator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiation_minutes.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_differentiation_hours(self,channel_name): '''Channel read reports derivative value with time units of hours.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['hours']) new_channel.set_attribute('type','differentiator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiation_hours.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_differentiation_days(self,channel_name): '''Channel read reports derivative value with time units of days.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',self.divs['days']) new_channel.set_attribute('type','differentiator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiation_days.__doc__) return self._add_channel(new_channel)
[docs] def add_channel_differentiation_scale(self,channel_name,time_div): '''Channel read reports derivative value with user supplied time units. time_div is seconds per user-unit, eg 60 for minutes.''' new_channel = channel(channel_name,read_function=lambda: self._dummy_read(channel_name)) new_channel.set_attribute('time_div',float(time_div)) new_channel.set_attribute('type','differentiator') new_channel.set_delegator(self) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiation_scale.__doc__) return self._add_channel(new_channel)
[docs] def differentiate(self, value): '''Scale value by elapsed time and store to accumulator. Should typically be used through integrate channel above.''' #results stored internally in value*seconds. Read channels scale to other time units on the way out. self._compute_delta() if not self._paused: self.difference(value) if self.diff is not None: try: self.derivative = self.diff / self.elapsed.total_seconds() except ZeroDivisionError: self.derivative = None else: self.derivative = None return self.derivative
[docs] def add_channel_differentiate(self, channel_name): '''Channel write causes time derivative between write value and previous write value to be computed and stored.''' new_channel = channel(channel_name,write_function=self.differentiate) new_channel.set_description(self.get_name() + ': ' + self.add_channel_differentiate.__doc__) return self._add_channel(new_channel)
[docs] def register_derivative_channel(self, channel_object): '''Automatically calls difference method each time channel_object is read, for example in logger.log().''' channel_object.add_read_callback(lambda channel_object,read_value: self.differentiate(read_value))
[docs] def read_delegated_channel_list(self,channels): '''private''' self.results_dict = {} for channel in channels: if channel.get_attribute('type') == 'differentiator': if self.derivative is not None: self.results_dict[channel.get_name()] = self.derivative * channel.get_attribute('time_div') else: self.results_dict[channel.get_name()] = None elif channel.get_attribute('type') == 'delta_timer': self.results_dict[channel.get_name()] = self.elapsed.total_seconds() / channel.get_attribute('time_div') elif channel.get_attribute('type') == 'total_timer': self.results_dict[channel.get_name()] = self.total_time.total_seconds() / channel.get_attribute('time_div') else: raise Exception('Unknown channel type: {}'.format(channel.get_attribute('type'))) channel.read_without_delegator() return self.results_dict
class servo(instrument): def __init__(self,fb_channel,output_channel,minimum,maximum,abstol,reltol=.001): '''Single channel virtual servo instrument. Given a forcing channel object and a measurement channel object, modifies the forcing channel value until the measurement channel is within specified tolerance or the number of allowed tries is exceeded. Max number of tries can be varied by modifying the 'tries' property. fb_channel is the measurement channel object output_channel is the forcing channel object minimum is the lowest value that may be forced during the servo attempt maximum is the highest value that may be forced during the servo attempt abstol is the amount that the measurement may differ from the target value to consider the servo complete. units are the same as the measurement channel (window is +/-abstol) reltol is the unitless scale factor that determines when the servo loop is sufficiently settled. (window is target*(1 +/- reltol)) ''' self._base_name = 'servo' self.fb_channel = fb_channel self.output_channel = output_channel instrument.__init__(self,"Servo Instrument forcing {} via {}".format(fb_channel.get_name(),output_channel.get_name())) self.tries = 10 self.verbose = False self.gain_est = 1 self.target = self.output_channel.read() self.reconfigure(minimum,maximum,abstol,reltol) def reconfigure(self, minimum=None, maximum=None, abstol=None, reltol=None): if (minimum is not None): self.minimum = minimum if (maximum is not None): self.maximum = maximum if (abstol is not None): self.abstol = abstol if (reltol is not None): self.reltol = reltol def add_channel_target(self,channel_name): '''Channel write causes output_channel to servo to new target value.''' servo_channel = channel(channel_name,write_function=self.servo) servo_channel.set_description(self.get_name() + ': ' + self.add_channel_target.__doc__) return self._add_channel(servo_channel) def servo(self,target=None): '''Servo fb_channel to target by varying output_channel. If target is omitted, previous target is maintained.''' if target == None: target = self.target else: self.target = target if self.verbose: print "Servoing {} to {}".format(self.get_name(), self.target) print " reltol:{} abstol:{} min:{} max:{}".format(self.reltol,self.abstol,self.minimum,self.maximum) cnt = 0 cnt_break = 0 readback = float(self.fb_channel.read()) self.setting = self.output_channel.read() while(True): cnt += 1 if (self.servo_check(readback)): return cnt if cnt > self.tries: return False error = readback - self.target change_by = error * -1/self.gain_est new_setting = self.setting + change_by if new_setting > self.maximum: new_setting = self.maximum print " Limit Max: {}".format(self.output_channel.get_name()) if new_setting < self.minimum: new_setting = self.minimum print " Limit Min: {}".format(self.output_channel.get_name()) self.output_channel.write(new_setting) new_readback = float(self.fb_channel.read()) change = new_readback - readback if (change != 0) & (new_setting != self.setting): self.gain_est = (change / (new_setting - self.setting)) readback = new_readback if self.verbose: print " {}: TGT:{:3.5f} ERR:{:3.5f} FRC:{:3.5f} RES:{:3.5f} GAIN:{:3.5f}".format(cnt, self.target, error, new_setting, new_readback, self.gain_est) self.setting = new_setting # Added following code to quit if two consecutive tries are within # reltol even though final target has not been achieved. This will # help if available current is lower than the target if abs(change) < self.abstol: cnt_break += 1 if cnt_break > 3: print "{}: abstol limit reached".format(self.get_name()) return False def servo_check(self, readback=None): '''Returns True if servo is within abstol or reltol tolerances. Returns False if servo failed to converge within alotted number of tries.''' if readback is None: readback = float(self.fb_channel.read()) if ((readback < (self.target * (1 + self.reltol))) & (readback > (self.target * (1 - self.reltol)))): return True if ((readback < (self.target + self.abstol)) & (readback > (self.target - self.abstol))): return True return False
[docs]class servo_group(object): '''This is a group of servos. It will servo each servo in that group until all are in regulation or up to servo_group.tries times''' def __init__(self,name): self.servos = [] self.verbose = False self.max_tries = 5 self.tries = 0 self.name = name
[docs] def add_servo(self,servo_inst): '''Add a servo virtual instrument to the servo_group''' assert isinstance(servo_inst, servo) self.servos.append(servo_inst)
[docs] def servo(self): '''run each servo in turn until all are in regulation.''' self.tries = 0 while(True): self.tries += 1 if self.verbose: print("Looping all servos ") for servo in self.servos: servo.servo() #must make it through all servos twice restart = False for servo in self.servos: if not servo.servo_check(): restart = True if self.verbose: print "{} failed servo_check, restarting".format(servo.name) if not restart: return True if self.tries > self.max_tries: print("unable to servo in " + str(self.max_tries) + ", failed, END") return False
[docs]class ramp_to(instrument): '''Virtual instrument that changes channel setting incrementally. Useful to minimize impact of overshoot when trying to use power supply as a precision voltage source. This is a crutch. A better option would be to use an SMU if available. ''' def __init__(self, verbose=False): instrument.__init__(self,"ramp_to virtual instrument") self._base_name = "ramp_to" self.verbose = verbose
[docs] def add_channel_binary(self, channel_name, forcing_channel, abstol=0.001, max_step=None): '''Writes binarily decreasing magnitude steps to forcing_channel until within abstol of final voltage. If specified, max_step will bound the step upper magnitude. Use forcing_channel.set_write_delay(seconds) to control ramp rate.''' assert abstol > 0 assert max_step is None or max_step > abstol new_channel = channel(channel_name,write_function=lambda final_value: self._ramp_binary(forcing_channel, final_value, abstol, max_step)) new_channel.set_description('{}: binary ramping slave channel: {}. {}'.format(self.get_name(),forcing_channel.get_name(),self.add_channel_linear.__doc__)) new_channel.write(forcing_channel.read()) forcing_channel.add_write_callback(lambda forcing_channel, final_value: new_channel._set_value(final_value)) #keep channels sync'd in both directions return self._add_channel(new_channel)
[docs] def add_channel_linear(self, channel_name, forcing_channel, step_size = 0.01): '''Writes constant steps of size step_size (linear_ramp) to forcing_channel until within abstol of final voltage. Use forcing_channel.set_write_delay(seconds) to control ramp rate.''' assert step_size > 0 new_channel = channel(channel_name,write_function=lambda final_value: self._ramp_linear(forcing_channel, final_value, step_size)) new_channel.set_description('{}: linear ramping slave channel: {}. {}'.format(self.get_name(),forcing_channel.get_name(),self.add_channel_linear.__doc__)) new_channel.write(forcing_channel.read()) forcing_channel.add_write_callback(lambda forcing_channel, final_value: new_channel._set_value(final_value)) #keep channels sync'd in both directions return self._add_channel(new_channel)
[docs] def add_channel_overshoot(self, channel_name, forcing_channel, abstol, estimated_overshoot): '''Writes steps to forcing channel_such that peak overshoot magnitude never exceeds written value by more than abstol. estimated_overshoot is specified as a fraction of setting change (peak = final_value + (final_value - previous_value)*estimated_overshoot). For example, to model 10% overshoot (5V to 6V transition hits peak 6.1V), set estimated_overshoot=0.1. ''' assert abstol > 0 assert estimated_overshoot >= 0 estimated_overshoot = float(estimated_overshoot) new_channel = channel(channel_name,write_function=lambda final_value: self._ramp_overshoot(forcing_channel, final_value, abstol, estimated_overshoot)) new_channel.set_description('{}: overshoot controlling slave channel: {}. {}'.format(self.get_name(),forcing_channel.get_name(),self.add_channel_overshoot.__doc__)) new_channel.write(forcing_channel.read()) forcing_channel.add_write_callback(lambda forcing_channel, final_value: new_channel._set_value(final_value)) #keep channels sync'd in both directions return self._add_channel(new_channel)
def _direction(self, delta): try: return delta / abs(delta) except ZeroDivisionError: #no direction if we're already there return 1 def _ramp_binary(self, forcing_channel, final_value, abstol, max_step): present_value = forcing_channel.read() if present_value is None: raise Exception('You must write a value to underlying channel {} before using {}.'.format(forcing_channel.get_name(), self.get_name())) delta = final_value - present_value while abs(delta) > abstol: if max_step is not None and abs(delta) > max_step: forcing_channel.write(present_value + float(max_step) * self._direction(delta)) if self.verbose: print "Slewing channel: {} to: {}".format(forcing_channel.get_name(), forcing_channel.read()) else: forcing_channel.write(present_value + 0.5 * delta) if self.verbose: print "Binary ramping channel: {} to: {}".format(forcing_channel.get_name(), forcing_channel.read()) present_value = forcing_channel.read() delta = final_value - present_value forcing_channel.write(final_value) def _ramp_linear(self, forcing_channel, final_value, step_size): present_value = forcing_channel.read() if present_value is None: raise Exception('Error: must write a value to underlying channel {} before using {}.'.format(forcing_channel.get_name(), self.get_name())) delta = final_value - present_value while abs(delta) > step_size: forcing_channel.write(present_value + float(step_size) * self._direction(delta)) present_value = forcing_channel.read() delta = final_value - present_value if self.verbose: print "Linear ramping channel: {} to: {}".format(forcing_channel.get_name(), forcing_channel.read()) forcing_channel.write(final_value) def _ramp_overshoot(self, forcing_channel, final_value, abstol, estimated_overshoot): present_value = forcing_channel.read() if present_value is None: raise Exception('Error: must write a value to underlying channel {} before using {}.'.format(forcing_channel.get_name(), self.get_name())) delta = final_value - present_value direction = self._direction(delta) #calculate next output such that output+overshoot <= final_value +/- abstol #x + (x-pv)*estimated_overshoot = final_value + sign*abstol next = (final_value + direction*abstol + present_value*estimated_overshoot) / (1+estimated_overshoot) while direction == 1 and next < final_value or direction == -1 and next > final_value: forcing_channel.write(next) if self.verbose: peak = next + (next-present_value)*estimated_overshoot print "Controlled overshoot stepping channel: {} to: {} with estimated overshoot to: {}".format(forcing_channel.get_name(), forcing_channel.read(), peak) present_value = forcing_channel.read() next = (final_value + direction*abstol + present_value*estimated_overshoot) / (1+estimated_overshoot) forcing_channel.write(final_value)
[docs]class threshold_finder(instrument,delegator): '''virtual instrument Does not automatically find threshold unless auto_find is enabled from add_channel. Otherwise, you must call threshold_finder.find(), or .find_linear() The channels used with this instrument may want to be virtual instruments. For example the comparator_input_channel_force could be used to clear a latched output before a new value is set, or the comparator_output_sense_channel could interpret complex comparator outputs that are not direct measurements. ''' def __init__(self,comparator_input_force_channel, comparator_output_sense_channel, minimum, maximum, abstol, comparator_input_sense_channel=None, forcing_overshoot = 0, output_threshold=None, verbose=False): ''' comparator_input_force_channel - DUT comparator input forcing channel object. comparator_output_sense_channel - DUT comparator output measurement channel object. minimum - minimum forced input to the DUT comparator via comparator_input_force_channel. maximum - maximum forced input to the DUT comparator via comparator_input_force_channel. abstol - resolution of search. Relative to comparator_input_channel_force, not comparator_input_sense_channel. comparator_input_sense_channel - optionally specify a channel object to read back actual (Kelvin) input to DUT from comparator_input_force_channel. forcing_overshoot - optionally specify an expected overshoot of forcing channel as a fraction of setting change. Steps toward threshold will be magnitude-controlled to keep peak value within abstol of target. Over-estimation will slow down search and Under-estimation may corrupt search results. output_threshold - optional digitization level threshold for comparator_output_sense_channel. If unspecified, will be calculated from mean of comparator_output_sense_channel reading with comparator_input_channel_force set to minimum and maximum. verbose - print extra information about search progress. cautious - take extra measurements to make sure search is proceeding correctly and has not been corrupted by overshoot, oscillation, etc. ''' self._base_name = 'threshold_finder_{}_{}'.format(comparator_input_force_channel.get_name(), comparator_output_sense_channel.get_name()) instrument.__init__(self,"Threshold_finder virtual instrument forcing:{} reading:{}".format(comparator_input_force_channel.get_name(), comparator_output_sense_channel.get_name() )) delegator.__init__(self) self._output_threshold = None self._output_threshold_calc = None self.verbose = verbose self.reconfigure(comparator_input_force_channel, comparator_output_sense_channel, comparator_input_sense_channel, output_threshold, minimum, maximum, abstol, forcing_overshoot) self.max_tries = 50 self.auto_find = False #default output before first find() self.threshold = None self.rising = None self.falling = None self.tries = None self.hysteresis = None self.forced_rising = None self.forced_falling = None self.rising_uncertainty = None self.falling_uncertainty = None self.rising_relative_uncertainty = None self.falling_relative_uncertainty = None self.search_algorithm = None self.results_dictionary = None
[docs] def reconfigure(self, comparator_input_force_channel, comparator_output_sense_channel, comparator_input_sense_channel, output_threshold=None, minimum = None, maximum = None, abstol=None, forcing_overshoot = None): '''Reconfigure channel settings to use a single threshold finder instrument with multiplexed DUT channels. Required arguments: comparator_input_force_channel - DUT comparator input forcing channel object. comparator_output_sense_channel - DUT comparator output measurement channel object. comparator_input_sense_channel - optionally specify a channel object to read back actual (Kelvin) input to DUT from comparator_input_force_channel. Set to None to disable. Optional argument (set to automatic if unspecified): output_threshold - optional digitization level threshold for comparator_output_sense_channel. If unspecified, will be calculated from mean of comparator_output_sense_channel reading with comparator_input_channel_force set to minimum and maximum. Optional arguments (unchanged if unspecified): minimum - minimum forced input to the DUT comparator via comparator_input_force_channel. maximum - maximum forced input to the DUT comparator via comparator_input_force_channel. abstol - resolution of search. Relative to comparator_input_channel_force, not comparator_input_sense_channel. ''' self._comparator_input_channel = comparator_input_force_channel self._comparator_output_sense_channel = comparator_output_sense_channel self._comparator_input_sense_channel = comparator_input_sense_channel self._output_threshold = output_threshold if minimum is not None: self._minimum = float(minimum) if maximum is not None: self._maximum = float(maximum) if abstol is not None: self._abstol = float(abstol) if forcing_overshoot is not None: self._forcing_overshoot = forcing_overshoot if self._abstol <= 0: raise Exception('abstol must be finite and positive') if self._minimum >= self._maximum: raise Exception('minimum must be less than maximum') self._ramper = ramp_to(self.verbose) if self._forcing_overshoot != 0: self._comparator_input_ramper = self._ramper.add_channel_overshoot('input_ramper', forcing_channel=self._comparator_input_channel, abstol = self._abstol, estimated_overshoot = self._forcing_overshoot) else: self._comparator_input_ramper = self._comparator_input_channel
[docs] def add_channel_all(self,channel_name,auto_find=False): '''shortcut method adds the following channels: threshold (Average of rising and falling thresolds. Relative to comparator_input_sense_channel.) rising threshold (Average of measurments at low and high endpoints of rising threshold uncertainty window. Relative to comparator_input_sense_channel.) falling threshold (Average of measurments at low and high endpoints of falling threshold uncertainty window. Relative to comparator_input_sense_channel.) tries (Number of binary search steps required to reduce uncertainty window to within abstol, or number of abstol-sized steps required to find threshold with linear search.) hysteresis (Difference between rising and falling thresholds. Relative to comparator_input_sense_channel.) abstol (Maximum two-sided uncertainty range (window width) for binary search, or step size for linear search. Relative to comparator_input_force_channel.) rising uncertainty (Achieved one-sided additive rising threshold uncertainty range for binary or linear search. Relative to comparator_input_sense_channel.) falling uncertainty (Achieved one-sided additive falling threshold uncertainty range for binary or linear search. Relative to comparator_input_sense_channel.) rising relative uncertainty (Achieved one-sided multiplicative rising threshold uncertainty range for binary or linear search. Relative to comparator_input_sense_channel.) falling relative uncertainty (Achieved one-sided multiplicative falling threshold uncertainty range for binary or linear search. Relative to comparator_input_sense_channel.) forced rising threshold (Average of low and high forced endpoints of rising threshold uncertainty window. Relative to comparator_input_force_channel.) forced falling threshold (Average of low and high forced endpoints of falling threshold uncertainty window. Relative to comparator_input_force_channel.) output_threshold (Calculated or specified digitization threshold for comparator_output_sense_channel.) if auto_find is 'linear', automatically call find_linear() when channel is read. if auto_find is 'geometric', automatically call find_geometric() when channel is read. if auto_find is any other true value, automatically call find() when channel is read. ''' th = self.add_channel_threshold(channel_name,auto_find) self.add_channel_rising(channel_name+"_rising") self.add_channel_falling(channel_name+"_falling") self.add_channel_tries(channel_name+"_tries") self.add_channel_hysteresis(channel_name+"_hysteresis") self.add_channel_abstol(channel_name+"_abstol") self.add_channel_uncertainty(channel_name+"_uncertainty") self.add_channel_relative_uncertainty(channel_name+"_relative_uncertainty") self.add_channel_forced_rising(channel_name+"_forced_rising") self.add_channel_forced_falling(channel_name+"_forced_falling") self.add_channel_output_threshold(channel_name+"_output_threshold") self.add_channel_algorithm(channel_name+"_search_algorithm") return th
[docs] def add_channel_threshold(self,channel_name,auto_find=False): '''Average of rising and falling thresholds found by last call to find() method. Relative to comparator_input_sense_channel. if auto_find is 'linear', automatically call find_linear() when channel is read. if auto_find is 'geometric', automatically call find_geometric() when channel is read. if auto_find is any other true value, automatically call find() when channel is read.''' new_channel = channel(channel_name,read_function=lambda: self.threshold) self.auto_find = auto_find new_channel.set_description(self.get_name() + ': ' + self.add_channel_threshold.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_rising(self,channel_name): '''Average of measurments at low and high endpoints of rising threshold uncertainty window. Relative to comparator_input_sense_channel.''' new_channel = channel(channel_name,read_function= lambda: self.rising) new_channel.set_description(self.get_name() + ': ' + self.add_channel_rising.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_falling(self,channel_name): '''Average of measurments at low and high endpoints of falling threshold uncertainty window. Relative to comparator_input_sense_channel.''' new_channel = channel(channel_name,read_function= lambda: self.falling) new_channel.set_description(self.get_name() + ': ' + self.add_channel_falling.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_tries(self,channel_name): '''Number of binary search steps required to reduce uncertainty window to within abstol, or number of abstol-sized steps required to find threshold with linear search.''' new_channel = channel(channel_name,read_function= lambda: self.tries) new_channel.set_description(self.get_name() + ': ' + self.add_channel_tries.__doc__) new_channel.set_delegator(self) self._add_channel(new_channel) return new_channel
[docs] def add_channel_hysteresis(self,channel_name): '''Difference between rising and falling thresholds. Relative to comparator_input_sense_channel.''' new_channel = channel(channel_name,read_function= lambda: self.hysteresis) new_channel.set_description(self.get_name() + ': ' + self.add_channel_hysteresis.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_abstol(self,channel_name): '''Maximum two-sided uncertainty range (window width) for binary search, or step size for linear search. Relative to comparator_input_force_channel.''' new_channel = channel(channel_name,read_function= lambda: self._abstol) new_channel.set_description(self.get_name() + ': ' + self.add_channel_abstol.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_uncertainty(self,channel_name): '''Single sided measured threshold uncertainty at termination of search. i.e (threshold_rising - uncertainty_rising) < {true rising threshold} < (threshold_rising + uncertainty_rising) and (threshold_falling - uncertainty_falling) < {true falling threshold} < (threshold_falling + uncertainty_falling). For binary search with comparator_input_sense_channel=None, will be between 0.5*abstol and 0.25*abstol. For linear sweep with comparator_input_sense_channel=None, will be 0.5*abstol. With comparator_input_sense_channel defined, uncertainty will be relative to measured rather than forced inputs and may be scaled differently than forcing (abstol) units. ''' rising_threshold_uncertainty_channel = channel(channel_name+"_rising",read_function= lambda: self.rising_uncertainty) rising_threshold_uncertainty_channel.set_description(self.get_name() + ': ' + self.add_channel_uncertainty.__doc__) rising_threshold_uncertainty_channel.set_delegator(self) self._add_channel(rising_threshold_uncertainty_channel) falling_threshold_uncertainty_channel = channel(channel_name+"_falling",read_function= lambda: self.falling_uncertainty) falling_threshold_uncertainty_channel.set_description(self.get_name() + ': ' + self.add_channel_uncertainty.__doc__) falling_threshold_uncertainty_channel.set_delegator(self) self._add_channel(falling_threshold_uncertainty_channel) return rising_threshold_uncertainty_channel
[docs] def add_channel_relative_uncertainty(self,channel_name): ''' Single sided relative meatured threshold uncertainty at termination of search. i.e threshold_rising * (1 - relative_uncertainty_rising) < {true rising threshold} < threshold_rising * (1 + relative_uncertainty_rising) and threshold_falling * (1 - relative_uncertainty_falling) < {true falling threshold} < threshold_falling * (1 + relative_uncertainty_falling) With comparator_input_sense_channel defined, uncertainty will be relative to measured rather than forced inputs and may be scaled differently than forcing (abstol) units. ''' rising_threshold_relative_uncertainty_channel = channel(channel_name+"_rising",read_function= lambda: self.rising_relative_uncertainty) rising_threshold_relative_uncertainty_channel.set_description(self.get_name() + ': ' + self.add_channel_relative_uncertainty.__doc__) rising_threshold_relative_uncertainty_channel.set_delegator(self) self._add_channel(rising_threshold_relative_uncertainty_channel) falling_threshold_relative_uncertainty_channel = channel(channel_name+"_falling",read_function= lambda: self.falling_relative_uncertainty) falling_threshold_relative_uncertainty_channel.set_description(self.get_name() + ': ' + self.add_channel_relative_uncertainty.__doc__) falling_threshold_relative_uncertainty_channel.set_delegator(self) self._add_channel(falling_threshold_relative_uncertainty_channel) return rising_threshold_relative_uncertainty_channel
[docs] def add_channel_forced_rising(self,channel_name): '''Average of low and high forced endpoints of rising threshold uncertainty window. Relative to comparator_input_force_channel.''' new_channel = channel(channel_name,read_function= lambda: self.forced_rising) new_channel.set_description(self.get_name() + ': ' + self.add_channel_forced_rising.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_forced_falling(self,channel_name): '''Average of low and high forced endpoints of falling threshold uncertainty window. Relative to comparator_input_force_channel.''' new_channel = channel(channel_name,read_function= lambda: self.forced_falling) new_channel.set_description(self.get_name() + ': ' + self.add_channel_forced_falling.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_output_threshold(self,channel_name): '''Digitization threshold for comparator_output_sense_channel.''' new_channel = channel(channel_name,read_function= lambda: self._output_threshold_calc) new_channel.set_description(self.get_name() + ': ' + self.add_channel_output_threshold.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
[docs] def add_channel_algorithm(self,channel_name): '''Search method used to determine threshold.''' new_channel = channel(channel_name,read_function= lambda: self.search_algorithm) new_channel.set_description(self.get_name() + ': ' + self.add_channel_algorithm.__doc__) new_channel.set_delegator(self) return self._add_channel(new_channel)
def _write_comparator_input(self,value, controlled): '''set forced input to DUT comparator''' if controlled: self._comparator_input_ramper.write(value) else: self._comparator_input_channel.write(value) def _read_comparator_output(self): '''measure analog output of DUT comparator''' return self._comparator_output_sense_channel.read() def _read_input_sense(self,input_set): '''measure sensed input to dut comparator. Data is stored internally for future use.''' if self._comparator_input_sense_channel is not None: sensed_input = self._comparator_input_sense_channel.read() self._input_reads[input_set] = sensed_input return sensed_input def _digitize_output(self,value): '''Convert analog output to boolean by polarity-aware comparison with threshold.''' if self.pol > 0: return value > self._output_threshold_calc else: return value < self._output_threshold_calc def _test(self, input_force, measure_input, controlled): '''Private procedure to test each point during sweep. 1) write new value to comparator_input_force_channel 2) read comparator_output_sense_channel and digitize 3) optionally read comparator_input_sense_channel and store for future reference 4) return results dict ''' self._write_comparator_input(input_force, controlled) output_analog = self._read_comparator_output() try: output_digital = self._digitize_output(output_analog) except AttributeError as e: #can't digitize output before threshold and polarity have been determined output_digital = None if measure_input: sensed_input = self._read_input_sense(input_force) else: sensed_input = None self._intermediate_results[input_force] = {'forced_input': input_force, 'output_analog': output_analog, 'output_digital': output_digital, 'sensed_input': sensed_input } return self._intermediate_results[input_force] def _check_polarity(self): #fill the variables self.begin_time = time.time() self._input_reads = {} #TODO: consider merging with _intermediate_results? self._intermediate_results = {} self.debug_print("------------------------------") self.debug_print("Check polarity and output digitizer threshold.".format(self.tries)) #get the polarity low_test = self._test(self._minimum, measure_input=True, controlled=False) self.debug_print("Measured low output: {} at input: {}".format(low_test['output_analog'], self._minimum)) high_test = self._test(self._maximum, measure_input=True, controlled=False) self.debug_print("Measured high output: {} at input: {}".format(high_test['output_analog'], self._maximum)) if high_test['output_analog'] == low_test['output_analog']: raise Exception('{}: Comparator output unchanged at max and min input forcing levels!'.format(self.get_name())) if low_test['output_analog'] < high_test['output_analog']: self.pol = 1 else: self.pol = -1 if self._output_threshold is not None: self._output_threshold_calc = self._output_threshold else: self._output_threshold_calc = (high_test['output_analog'] + low_test['output_analog']) / 2.0 self.debug_print("Threshold: {}".format(self._output_threshold_calc)) self.debug_print("Polarity: {}1".format("+" if self.pol ==1 else "-")) def _compute_outputs(self): ''' Shared math for binary and linear search outputs Takes instance variables self.falling_min, self.falling_max, self.rising_min, self.rising_max as input. Stores output instance variables self.forced_rising, self.forced_falling, self.rising, self.falling, self.hysteresis, self.threshold, self.rising_uncertainty, self.falling_uncertainty, self.rising_relative_uncertainty, self.falling_relative_uncertainty. Passes through self.tries, self._abstol. ''' self.forced_rising = (self.rising_min + self.rising_max) / 2.0 self.forced_falling = (self.falling_min + self.falling_max) / 2.0 if self._comparator_input_sense_channel is not None: #replace comparator_input_channel_force values with comparator_input_sense_channel values. try: self.rising = (self._input_reads[self.rising_max] + self._input_reads[self.rising_min]) / 2.0 self.falling = (self._input_reads[self.falling_max] + self._input_reads[self.falling_min]) / 2.0 self.rising_uncertainty = max(self._input_reads[self.rising_max] - self.rising, self.rising - self._input_reads[self.rising_min]) self.falling_uncertainty = max(self._input_reads[self.falling_max] - self.falling, self.falling - self._input_reads[self.falling_min]) self.rising_relative_uncertainty = max(self._input_reads[self.rising_max] / self.rising - 1, 1 - self._input_reads[self.rising_min] / self.rising) self.falling_relative_uncertainty = max(self._input_reads[self.falling_max] / self.falling - 1, 1 - self._input_reads[self.falling_min] / self.falling) except (KeyError, Exception) as excp: print "Something is wrong with the threshold finder instrument." print excp import pdb pdb.set_trace() else: self.rising = self.forced_rising self.falling = self.forced_falling self.rising_uncertainty = (self.rising_max - self.rising_min) / 2.0 self.falling_uncertainty = (self.falling_max - self.falling_min) / 2.0 self.rising_relative_uncertainty = self.rising_uncertainty / self.rising self.falling_relative_uncertainty = self.falling_uncertainty / self.falling self.hysteresis = self.rising - self.falling self.threshold = (self.rising + self.falling) / 2.0 self.elapsed_time = time.time() - self.begin_time self.results_dictionary = OrderedDict(( ("threshold", self.threshold), ("rising", self.rising), ("falling", self.falling), ("hysteresis", self.hysteresis), ("forced_rising", self.forced_rising), ("forced_falling", self.forced_falling), ("rising_uncertainty", self.rising_uncertainty), ("falling_uncertainty", self.falling_uncertainty), ("rising_relative_uncertainty", self.rising_relative_uncertainty), ("falling_relative_uncertainty", self.falling_relative_uncertainty), ("polarity", self.pol), ("tries", self.tries), ("abstol", self._abstol), ("elapsed_time", self.elapsed_time), ("output_threshold", self._output_threshold_calc), ("search_algorithm", self.search_algorithm) )) self.debug_print("Search results:") for k,v in self.results_dictionary.iteritems(): self.debug_print("\t{}: {}".format(k,v)) return self.results_dictionary
[docs] def measure_input(self, input_sense_channel): '''Measure input sense (Kelvin) channel manually after completion of search algorithm. This may be somewhat less accurate than measuring sense channel during search. It exposes possibly non-ideal hysteresis or gain/offset drift of the forcing instrument by uncorrelating measurements in time. Typically used with comparator_input_sense_channel=None to speed up search at each point. Updates and returns internal results dictionary. ''' self.debug_print("Post-measuring input sense....") if self.results_dictionary is None: raise Exception('ERROR: Must complete a threshold search before calling this method.') begin_time = time.time() self._write_comparator_input(self._minimum, controlled=False) self._write_comparator_input(self.rising_min, controlled=False) rising_min_sense = input_sense_channel.read() self._write_comparator_input(self.rising_max, controlled=False) rising_max_sense = input_sense_channel.read() self.rising = (rising_min_sense + rising_max_sense) / 2.0 self._write_comparator_input(self._maximum, controlled=False) self._write_comparator_input(self.falling_max, controlled=False) falling_max_sense = input_sense_channel.read() self._write_comparator_input(self.falling_min, controlled=False) falling_min_sense = input_sense_channel.read() self.falling = (falling_min_sense + falling_max_sense) / 2.0 self.hysteresis = self.rising - self.falling self.threshold = (self.rising + self.falling) / 2.0 self.elapsed_time += time.time() - begin_time self.rising_uncertainty = max(rising_max_sense - self.rising, self.rising - rising_min_sense) self.falling_uncertainty = max(falling_max_sense - self.falling, self.falling - falling_min_sense) self.rising_relative_uncertainty = max(rising_max_sense / self.rising - 1, 1 - rising_min_sense / self.rising) self.falling_relative_uncertainty = max(falling_max_sense / self.falling - 1, 1 - falling_min_sense / self.falling) self.results_dictionary["rising"] = self.rising self.results_dictionary["falling"] = self.falling self.results_dictionary["threshold"] = self.threshold self.results_dictionary["hysteresis"] = self.hysteresis self.results_dictionary["elapsed_time"] = self.elapsed_time self.results_dictionary["rising_uncertainty"] = self.rising_uncertainty self.results_dictionary["falling_uncertainty"] = self.falling_uncertainty self.results_dictionary["rising_relative_uncertainty"] = self.rising_relative_uncertainty self.results_dictionary["falling_relative_uncertainty"] = self.falling_relative_uncertainty self.debug_print("Updated results with input sense:") for k,v in self.results_dictionary.iteritems(): self.debug_print("\t{}: {}".format(k,v)) return self.results_dictionary
def debug_print(self, msg): if self.verbose: print msg
[docs] def find(self, cautious=False): '''Hysteresis-aware double binary search. Returns dictionary of results. if cautious, perform extra measurment at each step to ensure hsyteresis flips and search region has not been corrupted. ''' self._check_polarity() self.rising_min = self._minimum self.rising_max = self._maximum self.falling_min = self._minimum self.falling_max = self._maximum #Binary searching a given interval to within a given abstol always takes the same number of steps. abstol_divisions = (self._maximum - self._minimum) / self._abstol self.tries = max(int(math.ceil(math.log(abstol_divisions,2))), 0) self.debug_print("------------------------------") self.debug_print("Binary searching with {} iterations".format(self.tries)) assert self.tries <= self.max_tries for cnt in range(self.tries): self.debug_print("-----------------") #work on falling threshold self.debug_print("Falling Min: {} Max: {}".format(self.falling_min,self.falling_max)) #cross high threshold to look for low reset = min(self.rising_max + self._abstol, self._maximum) if cautious and not self._test(reset, measure_input=False, controlled=False)['output_digital']: raise Exception('Failed to change comparator output high at: {}. Check comparator_input_channel overshoot and adjust forcing_overshoot.'.format(reset)) elif not cautious: self._write_comparator_input(reset, controlled=False) new_value = (self.falling_max + self.falling_min) / 2.0 self.debug_print("Try {}".format(new_value)) test_result = self._test(new_value, measure_input=True, controlled=True) self.debug_print("Output Analog: {}".format(test_result['output_analog'])) self.debug_print("Output Digital: {}".format(test_result['output_digital'])) if test_result['output_digital']: self.falling_max = new_value else: self.falling_min = new_value self.debug_print("------") #work on rising threshold self.debug_print("Rising Min: {} Max: {}".format(self.rising_min,self.rising_max)) #cross low threshold to look for high reset = max(self.falling_min - self._abstol, self._minimum) if cautious and self._test(reset, measure_input=False, controlled=False)['output_digital']: raise Exception('Failed to change comparator output low at: {}. Check comparator_input_channel overshoot and adjust forcing_overshoot.'.format(reset)) elif not cautious: self._write_comparator_input(reset, controlled=False) new_value = (self.rising_max + self.rising_min) / 2.0 self.debug_print("Try: {}".format(new_value)) test_result = self._test(new_value, measure_input=True, controlled=True) self.debug_print("Output Analog: {}".format(test_result['output_analog'])) self.debug_print("Output Digital: {}".format(test_result['output_digital'])) if test_result['output_digital']: self.rising_max = new_value else: self.rising_min = new_value self.debug_print("-----------------") self.search_algorithm = "binary search" res = self._compute_outputs() return res
[docs] def find_no_hysteresis(self): '''Hysteresis-unaware single binary search. Returns dictionary of results. ''' self._check_polarity() th_min = self._minimum th_max = self._maximum #Binary searching a given interval to within a given abstol always takes the same number of steps. abstol_divisions = (self._maximum - self._minimum) / self._abstol self.tries = max(int(math.ceil(math.log(abstol_divisions,2))), 0) self.debug_print("------------------------------") self.debug_print("Binary searching with {} iterations, ignoring hysteresis.".format(self.tries)) assert self.tries <= self.max_tries for cnt in range(self.tries): self.debug_print("-----------------") self.debug_print("Min: {} Max: {}".format(th_min,th_max)) new_value = (th_max + th_min) / 2.0 self.debug_print("Try {}".format(new_value)) test_result = self._test(new_value, measure_input=True, controlled=True) self.debug_print("Output Analog: {}".format(test_result['output_analog'])) self.debug_print("Output Digital: {}".format(test_result['output_digital'])) if test_result['output_digital']: th_max = new_value else: th_min = new_value self.debug_print("-----------------") self.search_algorithm = "binary search without hysteresis" #fill the input variables to _compute_outputs self.rising_min = th_min self.rising_max = th_max self.falling_min = th_min self.falling_max = th_max res = self._compute_outputs() return res
[docs] def find_linear(self): '''Hysteresis aware linear sweep. Returns dictionary of results''' self._check_polarity() self.rising_min = self._minimum self.rising_max = self._maximum self.falling_min = self._minimum self.falling_max = self._maximum self.tries = 0 self.debug_print("------------------------------") self.debug_print("Searching for rising threshold") self._find_linear_threshold(self._minimum, self._maximum, self._abstol) # find rising threshold self.debug_print("-------------------------------") self.debug_print("Searching for falling threshold") self._find_linear_threshold(self._minimum, self.rising_max, -1 * self._abstol) # find falling threshold starting just after rising hysteresis flip. self.search_algorithm = "linear search" res = self._compute_outputs() return res
[docs] def find_geometric(self, decades=None): '''Perform repeated linear searches for rising and falling thresholds with 10x increase in resolution each iteration. Final resolution is abstol Optionally specify decades argment to control how many searches are performed. Defaults to as many as possible for given min/max range and abstol. No steps are ever made toward the thresold with magnitude largee than current search's resolution in case of overshoot. ''' self._check_polarity() self.rising_min = self._minimum self.rising_max = self._maximum self.falling_min = self._minimum self.falling_max = self._maximum self.tries = 0 max_decades = int(math.floor(math.log((self._maximum - self._minimum) / self._abstol,10)))+1 if decades is None: decades = max_decades assert decades >= 1 assert isinstance(decades,int) assert decades <= max_decades cisc = self._comparator_input_sense_channel self._comparator_input_sense_channel = None for e in range(decades-1, -1, -1): step_size = self._abstol * 10**e if e == 0: self._comparator_input_sense_channel = cisc #only read back input on last sweep self.debug_print("------------------------------") self.debug_print("Ramping to: {}".format(self.rising_min)) self._write_comparator_input(self.rising_min, controlled=True) #small steps toward threshold self.debug_print("Searching for rising threshold with step size: {} between: {} and: {}".format(step_size,self.rising_min,self.rising_max)) self._find_linear_threshold(self.rising_min, self.rising_max, step_size) # find rising threshold self.debug_print("-------------------------------") self.debug_print("Ramping to: {}".format(self.falling_max)) self._write_comparator_input(self.falling_max, controlled=True) #small steps toward threshold self.debug_print("Searching for falling threshold with step size: {} between: {} and: {}".format(step_size,self.falling_max,self.falling_min)) self._find_linear_threshold(self.falling_min, self.falling_max, -1 * step_size) # find falling threshold starting just after rising hysteresis flip. if e != 0: #account for possible forcing instrument overshoot in travel direction self.rising_max = min(self._maximum, self.rising_max + step_size) self.falling_min = max(self._minimum, self.falling_min - step_size) self.search_algorithm = "geometric linear search" res = self._compute_outputs() return res
[docs] def find_hybrid(self, linear_backtrack=None): '''Perform course binary search, then approach rising and falling thresholds from correct direction with linear search. Both binary and linear searches will be performed to abstol forcing tolerance. The linear search will be started linear_backtrack distance away from expected threshold, with default of 5 * reltol. Each of the two linear sweeps will take approximatley (linear_backtrack / reltol) steps toward threshold. Steps toward threshold are of max magnitude max_step. ''' if linear_backtrack is None: linear_backtrack = 5 * self._abstol assert linear_backtrack >= self._abstol assert linear_backtrack > 0 #First run quick binary search to get rough answers #Don't need to read back inputs for this phase. cisc = self._comparator_input_sense_channel self._comparator_input_sense_channel = None binary_res = self.find(cautious=True) self._comparator_input_sense_channel = cisc min_rising = binary_res['forced_rising'] - linear_backtrack max_rising = binary_res['forced_rising'] + linear_backtrack min_falling = binary_res['forced_falling'] - linear_backtrack max_falling = binary_res['forced_falling'] + linear_backtrack if min_rising < self._minimum or max_rising > self._maximum or min_falling < self._minimum or max_falling > self._maximum: raise Exception('Hybrid sweep abstol: {} and linear_backtrack: {} too large for input range: {} to: {}.'.format(self._abstol, linear_backtrack, self._minimum, self._maximum)) #Then run one-sided linear search for rising threshold self.rising_min = None self.rising_max = None self.debug_print("------------------------------") #set hysteresis up for rising threshold reset = max(min_falling - self._abstol, self._minimum) self.debug_print("Setting hysteresis state low and ramping from: {} to: {}".format(reset, min_rising)) self._write_comparator_input(reset, controlled=False) #start below falling threshold self._write_comparator_input(min_rising, controlled=True) #small steps back self.debug_print("Linear searching for rising threshold between: {} and: {}".format(min_rising, max_rising)) self._find_linear_threshold(min_rising, max_rising, self._abstol) #then start looking if self.rising_min is None or self.rising_max is None: raise Exception('Hybrid threshold finder failed to reset hysteresis before rising threshold linear sweep. Try increasing linear_backtrack.') #Then run one-sided linear search for falling threshold self.falling_min = None self.falling_max = None self.debug_print("------------------------------") #set hysteresis up for falling threshold reset = min(max_rising + self._abstol, self._maximum) self.debug_print("Setting hysteresis state high and ramping from: {} to: {}".format(reset, max_falling)) self._write_comparator_input(reset, controlled=False) #start above rising threshold self._write_comparator_input(max_falling, controlled=True) #take small steps back self.debug_print("Linear searching for falling threshold between: {} and: {}".format(max_falling, min_falling)) self._find_linear_threshold(min_falling, max_falling, -1 * self._abstol) #then start looking if self.falling_min is None or self.falling_max is None: raise Exception('Hybrid threshold finder failed to reset hysteresis before falling threshold linear sweep. Try increasing linear_backtrack.') #Lastly, compute outputs self.search_algorithm = "binary tuned linear search" res = self._compute_outputs() return res
def _find_linear_threshold(self, min, max, step): '''One sided Linear sweep which sweeps up if step is positive or down if step is negative and finds the threshold Pol is the polarity of the comparator output''' if self._forcing_overshoot >= 1: #eventually all linear searches use step size abstol, so might as well detect problems here right away. raise Exception("Can't make linear steps of magnitude abstol with over 100% peak overshoot,") if step > 0: try_threshold = min test = False else: try_threshold = max test = True test_result = {'output_digital': test} tries = 0 while test_result['output_digital'] == test: tries += 1 self.debug_print("-----------------") self.debug_print("Try {}: {}".format(self.tries+tries, try_threshold)) test_result = self._test(try_threshold, measure_input=True, controlled=False) self.debug_print("Output Analog: {}".format(test_result['output_analog'])) self.debug_print("Output Digital: {}".format(test_result['output_digital'])) if step > 0: if test_result['output_digital']: self.rising_max = try_threshold else: self.rising_min = try_threshold if try_threshold == max: break elif try_threshold + step >= max: try_threshold = max else: try_threshold += step else: if not test_result['output_digital']: self.falling_min = try_threshold else: self.falling_max = try_threshold if try_threshold == min: break elif try_threshold + step <= min: try_threshold = min else: try_threshold += step if tries < 2: raise Exception('Linear sweep too few steps. Step: {} too large for input range: {} or threshold: {} not enclosed by interval {}:{}.'.format(step, max - min, try_threshold, min, max)) if test_result['output_digital'] == test: raise Exception('Linear search failed to find transition between: {} and: {}.'.format(min,max)) self.tries += tries def test_repeatability(self, linear_backtrack=None, decades=None): binary_results = self.find(cautious=True) hybrid_results = self.find_hybrid(linear_backtrack=linear_backtrack) linear_results = self.find_linear() geometric_results = self.find_geometric(decades) print "\n\n" str = "" max_key_len = 0 for key in binary_results: #same keys in all 3 results if key == "search_algorithm": continue if len(key) > max_key_len: max_key_len = len(key) str += "{}:\tbinary:{:1.6G} linear:{:1.6G} hybrid:{:1.6G} geometric:{:1.6G}\n".format(key, binary_results[key], linear_results[key], hybrid_results[key], geometric_results[key] #max(binary_results[key], linear_results[key], hybrid_results[key], geometric_results[key]) - min(binary_results[key], linear_results[key], hybrid_results[key], geometric_results[key]) ) print str.expandtabs(max_key_len+2) return {'binary': binary_results, 'linear': linear_results, 'hybrid': hybrid_results }
[docs] def read_delegated_channel_list(self,channels): '''private''' if self.auto_find: if isinstance(self.auto_find, str) and self.auto_find.lower() == 'linear': self.find_linear() elif isinstance(self.auto_find, str) and self.auto_find.lower() == 'geometric': self.find_geometric() else: self.find() results_dict = {} for channel in channels: results_dict[channel.get_name()] = channel.read_without_delegator() return results_dict
class calibrator(instrument): def __init__(self, verbose=False): ''' Calibrator virtual instrument. Corrects channel's read/write values based on either two-point gain/offset correction or full lookup table. Requires numpy for least squares computation during calibration measurement. Can be used without numpy if gain/offset numbers are supplied from elsewhere. ''' self._base_name = 'calibrator' instrument.__init__(self,"calibrator virtual instrument") self.verbose = verbose def calibrate(self, forcing_channel, readback_channel, forcing_values, results_filename=None): '''produce calibration table and gain/offset calculation for later use by 2point and spline calibrators''' import numpy points = {} for force_v in forcing_values: forcing_channel.write(force_v) points[force_v] = readback_channel.read() force_values = [x for x in sorted(points)] readback_values = [points[x] for x in force_values] x_arr = numpy.vstack([readback_values, numpy.ones(len(force_values))]).T results = {} results['force_values'] = force_values results['readback_values'] = readback_values results['gain'], results['offset'] = numpy.linalg.lstsq(x_arr, force_values)[0] if results_filename is not None: import pickle with open(results_filename, 'wb') as f: pickle.dump(results, f) return results def add_channel_calibrated_2point(self, channel_name, forcing_channel, gain=None, offset=None, calibration_filename=None, **kwargs): '''correct channel writes by previously determined 2-point gain/offset trim. Can pass in **calibrate() to get gain/offset measurements. offset and gain are specified in the direction of readback channel to forcing channel. ie forcing channel error. ''' if calibration_filename is None and gain is not None and offset is not None: gain = float(gain) offset = float(offset) elif calibration_filename is not None and gain is None and offset is None: import pickle with open(calibration_filename, 'rb') as f: cal_dict = pickle.load(f) gain = cal_dict['gain'] offset = cal_dict['offset'] else: raise Exception('Specify either calibration_filename or gain and offset arguments, but not both.') new_channel = channel(channel_name,write_function= lambda value: self._correct_2point(value, forcing_channel, gain, offset)) new_channel.set_description(self.get_name() + ': ' + self.add_channel_calibrated_2point.__doc__) forcing_channel.add_write_callback(lambda forcing_channel, raw_value: new_channel._set_value((raw_value - offset) / gain)) #keep channels sync'd in both directions return self._add_channel(new_channel) def _correct_2point(self, value, forcing_channel, gain, offset): raw_value = value * gain + offset if self.verbose: print "Correcting {} to {} to get {}".format(forcing_channel.get_name(), raw_value, value) forcing_channel.write(raw_value) def add_channel_calibrated_spline(self, channel_name, forcing_channel, calibration_filename=None, **kwargs): '''correct channel writes by previously determined mapping (from calibrate()). Can pass in **calibrate() to get cal map, store results dict locally, or use pickle file. Requires scipy.interpolate.UnivariateSpline ''' from scipy.interpolate import UnivariateSpline if kwargs.get('force_values', None) is not None and kwargs.get('readback_values', None) is not None and calibration_filename is None: force_values = kwargs['force_values'] readback_values = kwargs['readback_values'] elif kwargs.get('force_values', None) is None and kwargs.get('readback_values', None) is None and calibration_filename is not None: import pickle with open(calibration_filename, 'rb') as f: cal_dict = pickle.load(f) force_values = cal_dict['force_values'] readback_values = cal_dict['readback_values'] else: raise Exception('Specify either calibration_filename or **dictionary from calibrate(), but not both.') spl_rev = UnivariateSpline(force_values, readback_values, s=0) #change raw output to desired output for callback readback_values_sort, force_values_sort = zip(*sorted(zip(readback_values, force_values), key=lambda tup: tup[0])) #SORT by readback spl = UnivariateSpline(readback_values_sort, force_values_sort, s=0) #change desired output to raw output new_channel = channel(channel_name,write_function= lambda value: self._correct_spline(value, forcing_channel, spl)) new_channel.set_description(self.get_name() + ': ' + self.add_channel_calibrated_spline.__doc__) forcing_channel.add_write_callback(lambda forcing_channel, raw_value: new_channel._set_value(spl_rev(raw_value))) #keep channels sync'd in both directions return self._add_channel(new_channel) def _correct_spline(self, value, forcing_channel, spl): corrected_value = spl(value) if self.verbose: print "Correcting {} to {} to get {}".format(forcing_channel.get_name(), corrected_value, value) forcing_channel.write(corrected_value) class smart_battery_emulator(instrument): def __init__(self, voltage_channel_name, current_channel_name, voltage_interval, current_interval, verbose=False): ''' Smart Battery emulator to kick out voltage and current requests to keep a smart-battery charger alive. ''' self._base_name = 'SB_emulator' instrument.__init__(self,"Smart Battery Emulator") self.verbose = verbose self.writer = lab_utils.threaded_writer(verbose = self.verbose) self.voltage_thread = self.writer.connect_channel(channel_name = voltage_channel_name, time_interval = voltage_interval) self.current_thread = self.writer.connect_channel(channel_name = current_channel_name, time_interval = current_interval) self.initial_voltage_interval = voltage_interval self.initial_current_interval = current_interval def stop_all(self): '''Kills all threaded channels, can't be restarted.''' self.writer.stop_all() def add_channel_voltage_interval(self, channel_name): '''adds a channel that can change the update interval of the smart battery voltage''' new_channel = channel(channel_name, write_function = self.voltage_thread.set_time_interval) new_channel.set_description(self.add_channel_voltage_interval.__doc__) new_channel.write(self.initial_voltage_interval) return self._add_channel(new_channel) def add_channel_current_interval(self, channel_name): '''adds a channel that can change the update interval of the smart battery current''' new_channel = channel(channel_name, write_function = self.current_thread.set_time_interval) new_channel.set_description(self.add_channel_current_interval.__doc__) new_channel.write(self.initial_current_interval) return self._add_channel(new_channel)