#!/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_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_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 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]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))
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_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_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 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
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)