diff --git a/cfg/ls336_cfg.py b/cfg/ls336_cfg.py new file mode 100644 index 0000000..cb328a4 --- /dev/null +++ b/cfg/ls336_cfg.py @@ -0,0 +1,18 @@ +Node('example_cryo.psi.ch', # a globally unique identification + 'this is an example cryostat for the Frappy tutorial', # describes the node + interface='tcp://10767') # you might choose any port number > 1024 +Mod('io', # the name of the module + 'frappy_demo.lakeshore.LakeshoreIO', # the class used for communication + 'communication to main controller', # a description + uri='tcp://129.129.138.78:7777', # the serial connection + ) +Mod('T', + 'frappy_demo.lakeshore.TemperatureLoop', + 'Sample Temperature', + io='io', + channel='A', # the channel on the LakeShore for this module + loop=1, # the loop to be used + value=Param(max=470), # set the maximum expected T + target=Param(max=420), # set the maximum allowed target T + heater_range=3, # 5 for model 350 + ) diff --git a/frappy_demo/lakeshore.py b/frappy_demo/lakeshore.py new file mode 100644 index 0000000..ec5293f --- /dev/null +++ b/frappy_demo/lakeshore.py @@ -0,0 +1,108 @@ +#!/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 +# ***************************************************************************** +"""LakeShore demo + +demo example for tutorial +""" + +from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType, \ + IDLE, BUSY, WARN, ERROR, Drivable, IntRange + + +class LakeshoreIO(StringIO): + wait_before = 0.05 # Lakeshore requires a wait time of 50 ms between commands + # Lakeshore commands (see manual) + # '*IDN?' is sent on connect, and the reply is checked to match the regexp 'LSCI,.*' + identification = [('*IDN?', 'LSCI,.*')] + + +class TemperatureSensor(HasIO, Readable): + """a temperature sensor (generic for different models)""" + # internal property to configure the channel + channel = Property('the Lakeshore channel', datatype=StringType()) + # 0, 1500 is the allowed range by the LakeShore controller + # this range should be restricted in the configuration (see below) + value = Parameter(datatype=FloatRange(0, 1500, unit='K')) + + def read_value(self): + # the communicate method sends a command and returns the reply + reply = self.communicate(f'KRDG?{self.channel}') + return float(reply) + + def read_status(self): + code = int(self.communicate(f'RDGST?{self.channel}')) + if code >= 128: + text = 'units overrange' + elif code >= 64: + text = 'units zero' + elif code >= 32: + text = 'temperature overrange' + elif code >= 16: + text = 'temperature underrange' + elif code % 2: + # ignore 'old reading', as this may happen in normal operation + text = 'invalid reading' + else: + return IDLE, '' + return ERROR, text + + +class TemperatureLoop(TemperatureSensor, Drivable): + # lakeshore loop number to be used for this module + loop = Property('lakeshore loop', IntRange(1, 2), default=1) + target = Parameter(datatype=FloatRange(min=0, max=1500, unit='K')) + heater_range = Property('heater power range', IntRange(0, 5)) # max. 3 on LakeShore 336 + tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly = False) + _driving = False + + def write_target(self, target): + # reactivate heater in case it was switched off + # the command has to be changed in case of model 340 to f'RANGE {self.heater_range};RANGE?' + self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}') + reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}') + self._driving = True + self.status = BUSY, 'target changed' + return float(reply) + + def read_target(self): + return float(self.communicate(f'SETP?{self.loop}')) + + def read_status(self): + code = int(self.communicate(f'RDGST?{self.channel}')) + if code >= 128: + text = 'units overrange' + elif code >= 64: + text = 'units zero' + elif code >= 32: + text = 'temperature overrange' + elif code >= 16: + text = 'temperature underrange' + elif code % 2: + # ignore 'old reading', as this may happen in normal operation + text = 'invalid reading' + elif abs(self.target - self.value) > self.tolerance: + if self._driving: + return BUSY, 'approaching setpoint' + return WARN, 'temperature out of tolerance' + else: + self._driving = False + return IDLE, '' + return ERROR, text