diff --git a/cfg/ah2700test.cfg b/cfg/ah2700test.cfg new file mode 100644 index 0000000..34fa6a0 --- /dev/null +++ b/cfg/ah2700test.cfg @@ -0,0 +1,17 @@ +[node AH2700Test.psi.ch] +description = AH2700 capacitance bridge test + +[interface tcp] +type = tcp +bindto = 0.0.0.0 +bindport = 5000 + +[module cap] +class = secop_psi.ah2700.Capacitance +description = capacitance +uri=ldmse3-ts:3015 + +#[module ahcom] +#class = secop_psi.ah2700.StringIO +#uri=ldmse3-ts:3015 +#description = serial communicator to an AH2700 diff --git a/secop_psi/ah2700.py b/secop_psi/ah2700.py new file mode 100755 index 0000000..b0e549e --- /dev/null +++ b/secop_psi/ah2700.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""Andeen Hagerling capacitance bridge""" + +from secop import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done + + +class Ah2700IO(StringIO): + end_of_line = '\r\n' + timeout = 5 + + +class Capacitance(HasIodev, Readable): + parameters = { + 'value': Override('capacitance', FloatRange(unit='pF'), poll=True), + 'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0), + 'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0), + 'loss': Parameter('loss', FloatRange(unit='deg'), readonly=False, default=0), + } + iodevClass = Ah2700IO + + def read_value(self): + reply = self.sendRecv('SI') # single trigger + if reply.startswith('SI'): # this is an echo + self.sendRecv('SERIAL ECHO OFF') + reply = self.sendRecv('SI') + if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH" + self.status = [self.Status.ERROR, reply] + return Done + self.status = [self.Status.IDLE, ''] + # examples of replies: + # 'F= 1000.0 HZ C= 0.000001 PF L> 0.0 DS V= 15.0 V' + # 'F= 1000.0 HZ C= 0.0000059 PF L=-0.4 DS V= 15.0 V OVEN' + # 'LOSS TOO HIGH' + # make sure there is always a space after '=' and '>' + # split() ignores multiple white space + reply = reply.replace('=', '= ').replace('>', '> ').split() + _, freq, _, _, cap, _, _, loss, lossunit, _, volt = reply[:11] + self.freq = freq + self.voltage = volt + self.value = cap + if lossunit == 'DS': + self.loss = loss + else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens + reply = self.sendRecv('UN DS').split() # UN DS returns a reply similar to SI + try: + self.loss = reply[7] + except IndexError: + pass # don't worry, loss will be updated next time + return Done + + def read_freq(self): + self.read_value() + return Done + + def read_loss(self): + self.read_value() + return Done + + def read_volt(self): + self.read_value() + return Done diff --git a/secop_psi/k2601b.py b/secop_psi/k2601b.py new file mode 100755 index 0000000..e9b6600 --- /dev/null +++ b/secop_psi/k2601b.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""Keithley 2601B source meter + +not tested yet""" + +from secop import Writable, Module, Parameter, Override, Attached,\ + BoolType, FloatRange, EnumType, HasIodev, StringIO + + +class K2601bIO(StringIO): + identification = [('print(localnode.description)', 'Keithley Instruments SMU 2601B.*')] + + +SOURCECMDS = { + 1: 'reset()' + 'smua.source.func = smua.OUTPUT_DCVOLTS ' + 'display.smua.measure.func = display.MEASURE_DCAMP ' + 'smua.source.autorangev = 1', + 2: 'reset()' + 'smua.source.func = smua.OUTPUT_DCAMPS ' + 'smua.source.autorangei = 1', +} + + +class SourceMeter(HasIodev, Module): + parameters = { + 'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True), + 'power': Parameter('readback power', FloatRange(unit='W'), poll=True), + 'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), + readonly=False, default=0), + 'active': Parameter('output enable', BoolType(), readonly=False, poll=True), + } + iodevClass = K2601bIO + + def read_resistivity(self): + return self.sendRecv('print(smua.measure.r())') + + def read_power(self): + return self.sendRecv('print(smua.measure.p())') + + def read_active(self): + return self.sendRecv('print(smua.source.output)') + + def write_active(self, value): + return self.sendRecv('smua.source.output = %d print(smua.source.output)' % value) + + # for now, mode will not be read from hardware + + def write_mode(self, value): + if value == 0: + self.write_active(0) + else: + self.sendRecv(SOURCECMDS[value] + ' print(0)') + return value + + +class Current(HasIodev, Writable): + properties = { + 'sourcemeter': Attached(), + } + parameters = { + 'value': Override('measured current', FloatRange(unit='A'), poll=True), + 'target': Override('set current', FloatRange(unit='A'), poll=True), + 'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage + 'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True), + } + + def read_value(self): + return self.sendRecv('print(smua.measure.i())') + + def read_target(self): + return self.sendRecv('print(smua.source.leveli)') + + def write_target(self, value): + if not self.active: + raise ValueError('current source is disabled') + if value > self.limit: + raise ValueError('current exceeds limit') + return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value) + + def read_limit(self): + if self.active: + return self.limit + return self.sendRecv('print(smua.source.limiti)') + + def write_limit(self, value): + if self.active: + return value + return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value) + + def read_active(self): + return self._sourcemeter.mode == 1 and self._sourcemeter.read_active() + + def write_active(self, value): + if self._sourcemeter.mode != 1: + if value: + self._sourcemeter.write_mode(1) # switch to current + else: + return 0 + return self._sourcemeter.write_active(value) + + +class Voltage(HasIodev, Writable): + properties = { + 'sourcemeter': Attached(), + } + parameters = { + 'value': Override('measured voltage', FloatRange(unit='V'), poll=True), + 'target': Override('set voltage', FloatRange(unit='V'), poll=True), + 'active': Parameter('voltage is controlled', BoolType(), poll=True), + 'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True), + } + + def read_value(self): + return self.sendRecv('print(smua.measure.v())') + + def read_target(self): + return self.sendRecv('print(smua.source.levelv)') + + def write_target(self, value): + if not self.active: + raise ValueError('voltage source is disabled') + if value > self.limit: + raise ValueError('voltage exceeds limit') + return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value) + + def read_limit(self): + if self.active: + return self.limit + return self.sendRecv('print(smua.source.limitv)') + + def write_limit(self, value): + if self.active: + return value + return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value) + + def read_active(self): + return self._sourcemeter.mode == 2 and self._sourcemeter.read_active() + + def write_active(self, value): + if self._sourcemeter.mode != 2: + if value: + self._sourcemeter.write_mode(2) # switch to voltage + else: + return 0 + return self._sourcemeter.write_active(value)