state as of 01.07.2026

This commit is contained in:
2026-07-01 16:43:48 +02:00
parent 18ee68190f
commit a4ede7cfef
5 changed files with 840 additions and 98 deletions
+164
View File
@@ -0,0 +1,164 @@
Node('ls370test.psi.ch',
'Lsc370 Test',
interface='tcp://5000',
)
### temperature monitoring lakeshore (tmon) ###
IO('io_tmon', 'dil3-ts:3003')
Mod('tmon',
'frappy_psi.lakeshore370.Device',
'monitoring lakeshore 370',
curve_handling = True,
io = 'io_tmon',
)
Mod('switcher',
'frappy_psi.lakeshore370.Switcher',
'',
io = 'io_tmon',
)
Mod('stillt',
'frappy_psi.lakeshore370.Sensor',
'tmon stillt',
io = 'io_tmon',
device = 'tmon',
channel = 10,
switcher = 'switcher',
calcurve = 'ruoxm0',
enabled = True,
)
Mod('sorb',
'frappy_psi.lakeshore370.Sensor',
'tmon sorb',
io = 'io_tmon',
device = 'tmon',
channel = 12,
switcher = 'switcher',
calcurve = 'c270',
enabled = True,
)
Mod('sample_mon',
'frappy_psi.lakeshore370.Sensor',
'tmon sample',
io = 'io_tmon',
device = 'tmon',
channel = 3,
switcher = 'switcher',
calcurve = 'rx078',
enabled = False,
)
Mod('mix',
'frappy_psi.lakeshore370.Sensor',
'tmon mix',
io = 'io_tmon',
device = 'tmon',
channel = 4,
switcher = 'switcher',
calcurve = 'ruoxm0',
enabled = True,
)
Mod('samplehtr',
'frappy_psi.lakeshore370.Sensor',
'tmon samplehtr',
io = 'io_tmon',
device = 'tmon',
channel = 1,
switcher = 'switcher',
calcurve = 'rx078',
enabled = False, # disable when used as heater
)
Mod('onek',
'frappy_psi.lakeshore370.RawSensor',
'tmon onek',
io = 'io_tmon',
device = 'tmon',
channel = 11,
switcher = 'switcher',
# calcurve = 'ruoxm0',
enabled = True,
)
### temperature regulating lakeshore (treg) ###
IO('io_treg', 'dil3-ts:3001')
Mod('treg',
'frappy_psi.lakeshore370.Device',
'regulating lakeshore 370',
io = 'io_treg',
curve_handling = True,
)
Mod('switcher_treg',
'frappy_psi.lakeshore370.Switcher',
'',
io = 'io_treg',
)
Mod('sample',
'frappy_psi.lakeshore370.TemperatureLoop',
'treg sample',
io = 'io_treg',
device = 'treg',
channel = 6,
switcher = 'switcher_treg',
output_module = 'htr',
enabled = True,
calcurve = 'cx078',
)
Mod('htr',
'frappy_psi.lakeshore370.MainOutput',
'treg htr',
io = 'io_treg',
max_heater = '100mA',
max_power = 320e-6,
resistance = 316,
)
Mod('relais',
'frappy_psi.lakeshore370.AnalogOutput',
'relais to switch between mix heater (0%) and sample holder heater (100%)',
io = 'io_treg',
output_no = 2,
)
# Mod('splehtr',
# 'frappy_psi.lakeshore370.TemperatureLoop',
# 'treg heater',
# io = 'io',
# device = 'treg',
# channel = 8,
# switcher = 'switcher',
# calcurve = 'cx078',
# enabled = True,
# )
### lakeshore 370 in lab (ldmse3-ts:3014') ###
# IO('io', 'ldmse3-ts:3014')
# Mod('T1',
# 'frappy_psi.lakeshore370.RawSensor',
# '',
# io = 'io',
# channel = 1,
# switcher = 'switcher',
# )
# Mod('T5',
# 'frappy_psi.lakeshore370.RawSensor',
# '',
# io = 'io',
# channel = 5,
# switcher = 'switcher',
# )
+122 -85
View File
@@ -32,6 +32,7 @@ from frappy.errors import CommunicationFailedError, ConfigError, \
HardwareError, DisabledError, ImpossibleError, secop_error, SECoPError
from frappy.lib.units import NumberWithUnit, format_with_unit
from frappy.lib import formatStatusBits
from frappy.lib.enum import EnumMember
from frappy_psi.calcurve import CalCurve
from frappy_psi.convergence import HasConvergence
from frappy.mixins import HasOutputModule, HasControlledBy
@@ -104,7 +105,7 @@ class HasLscIO(HasIO):
self.communicate(f'{msghead};*OPC?')
return None
# when an argument is given as integer, it might be that this argument might be a float
converters = [string_to_num if isinstance(a, int) else type(a) for a in args]
converters = [string_to_num if isinstance(a, (int, EnumMember)) else type(a) for a in args]
values = [a if isinstance(a, str) else f'{a:g}'
for c, a in zip(converters, args)]
if ' ' in msghead:
@@ -162,7 +163,8 @@ class Device(HasLscIO, Module):
if np.array_equal(prev_request.points, req.points):
# a request is already running and the curve content has not changed
self.log.info('already installing %s', sensor.calcurve)
req.add_sensor(sensor)
prev_request.add_sensor(sensor)
# self.log.info('sensors for req %r', prev_request.sensors)
else:
self.log.info('file has changed, restart treating %s', sensor.calcurve)
self._requests[sensor.calcurve] = req
@@ -367,7 +369,7 @@ class Device(HasLscIO, Module):
# write a temporary header
# (in case loading will not finish, the curve should be marked as empty)
header = list(request.crvhdr)
self.put_header(request.curve_no, 'loading...', '', *header[2:])
self.put_header(request.curve_no, 'loading...', '_', *header[2:])
request.pointer = 0
return self.load_points
@@ -423,7 +425,7 @@ class Device(HasLscIO, Module):
def put_header(self, curve_no, name, sn, fmt, limit, coef):
"""write header"""
self.communicate(f'CRVHDR {curve_no},"{name}","{sn}",{fmt},{limit},{coef};*OPC?')
self.command(f'CRVHDR {curve_no}', name, sn or '_', fmt, limit, coef)
def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-5), significant=6):
"""check whether a returned calibration point is equal within curve point precision"""
@@ -595,19 +597,81 @@ class Base(HasLscIO):
super().initModule()
class Sensor(Base, Readable):
class SensorBase(Base, Readable):
"""base class for sensors"""
value = Parameter(unit='K')
status = Parameter(datatype=StatusType(Readable, 'DISABLED', 'BUSY'))
raw = Parameter('raw sensor value', datatype=FloatRange(unit='Ohm'), default=0)
channel = Property('used channel', StringType()) # input
calcurve = Parameter('calibration curve name', StringType(), readonly=False)
enabled = Parameter('enable flag', BoolType(), readonly=False)
curve_no = None
STATUS_BIT_LABELS = 'invalid_reading old_reading b2 b3 t_under t_over units_zero units_overrange'.split()
_read_error = None
_value_error = None
_raw_error = None
_do_read = True
# TODO: implement alarms
def doPoll(self):
self.read_status() # polls also value and raw
def get_data(self):
"""get reading status, raw and Kelvin value with minimal delay"""
status = IDLE, ''
raw_error = None
value_error = None
if self.enabled:
rdgst, raw, value = self.get_internal_data()
rdgst &= 0xfd # suppress old reading
if rdgst:
statuslist = formatStatusBits(rdgst, self.STATUS_BIT_LABELS)
status = ERROR, statuslist[-1] # show only the most fatal error
value_error = HardwareError(statuslist[-1])
while statuslist and statuslist[-1].startswith('t_'):
statuslist.pop()
if statuslist:
raw_error = HardwareError(statuslist[-1])
elif self._curve_handling:
value_error = self.device.get_calib_state(self)
if value_error:
status = ERROR, str(value_error)
else:
value_error = raw_error = DisabledError('disabled')
value = raw = 0
status = DISABLED, 'disabled'
self.enable = False
self._value_error = value_error
self._raw_error = raw_error
return status, value, raw
class RawSensor(SensorBase):
"""class for sensors using raw value (w/o calibration curve)"""
value = Parameter('raw value', unit='Ohm')
_curve_handling = False
def get_internal_data(self):
ch = self.channel
rdgst, raw = self.query(f'RDGST?{ch};SRDG?{ch}', int, float)
return rdgst, raw, raw
def read_status(self):
status, self.value, _ = self.get_data()
if self._raw_error:
self.announceUpdate('value', err=self._raw_error)
return status
@nopoll
def read_value(self):
self.status, value, _ = self.get_data()
if self._raw_error:
raise self._raw_error
return value
class Sensor(SensorBase):
"""class for sensors using value (incl. calibration curve)"""
value = Parameter('calibrated value', unit='K')
raw = Parameter('raw', datatype=FloatRange(unit='Ohm'))
calcurve = Parameter('calibration curve name', StringType(), readonly=False)
curve_no = None
_curve_handling = False # True when a device is present and device.curve_handling is True
# TODO: implement alarms
@@ -615,62 +679,40 @@ class Sensor(Base, Readable):
super().initModule()
if self.device:
self._curve_handling = self.device.curve_handling
self.log.info('curve handling %r', self._curve_handling)
if not self._curve_handling:
self.parameters['calcurve'].setProperty('export', False)
def doPoll(self):
self.read_status() # polls also value and raw
def get_internal_data(self):
ch = self.channel
rdgst, raw, value = self.query(f'RDGST?{ch};SRDG?{ch};KRDG?{ch}', int, float, float)
return rdgst, raw, value
@nopoll
def read_value(self):
self.status, value, raw = self.get_data()
if isinstance(value, SECoPError):
raise value
if not isinstance(raw, SECoPError):
self.raw = raw
return value
def read_status(self):
status, self.value, self.raw = self.get_data()
if self._raw_error:
self.announceUpdate('raw', err=self._raw_error)
if self._value_error:
self.announceUpdate('value', err=self._value_error)
return status
@nopoll
def read_raw(self):
self.status, value, raw = self.get_data()
if isinstance(raw, SECoPError):
raise raw
self.value = value
self.status, self.value, raw = self.get_data()
if self._value_error:
self.announceUpdate('value', err=self._value_error)
if self._raw_error:
raise self._raw_error
return raw
def read_status(self):
try:
status, self.value, self.raw = self.get_data()
except Exception as e:
self.raw = self.value = secop_error(e)
raise
return status
def get_data(self):
"""get reading status, raw and Kelvin value with minimal delay"""
status = IDLE, ''
if self.enabled:
ch = self.channel
rdgst, raw, value = self.query(f'RDGST?{ch};SRDG?{ch};KRDG?{ch}', int, float, float)
rdgst &= 0xfd # suppress old reading
if rdgst:
statuslist = formatStatusBits(rdgst, self.STATUS_BIT_LABELS)
status = ERROR, statuslist[-1] # show only the most fatal error
value = HardwareError(statuslist[-1])
while statuslist and statuslist[-1].startswith('t_'):
statuslist.pop()
if statuslist:
raw = HardwareError(statuslist[-1])
elif self._curve_handling:
value_error = self.device.get_calib_state(self)
if value_error:
value = value_error
status = ERROR, str(value_error)
else:
raw = value = DisabledError('disabled')
status = DISABLED, 'disabled'
self.enable = False
return status, value, raw
@nopoll
def read_value(self):
self.status, value, self.raw = self.get_data()
if self._raw_error:
self.announceUpdate('raw', err=self._raw_error)
if self._value_error:
raise self._value_error
return value
def write_calcurve(self, calibname):
if not self._curve_handling:
@@ -728,7 +770,7 @@ class Output(Base, HasControlledBy, Writable):
''', FloatRange(0, 100, unit='W'), readonly=False)
# for the case when several outputs are possible:
output_no = Parameter('lakeshore output or loop number', IntRange(1, 4), default=1)
resistance = Parameter('heater resistance', FloatRange(10, 100), readonly=False, default=25)
resistance = Parameter('heater resistance', FloatRange(10, 100, unit='Ohm'), readonly=False, default=25)
Extension = None
AMP_VOLT_WATT = NumberWithUnit('A', 'V', 'W')
imax = None
@@ -751,27 +793,6 @@ class Output(Base, HasControlledBy, Writable):
"""
raise NotImplementedError
def set_closed_loop(self, loop):
"""set to control mode
:param loop: the temperature loop module
"""
if self._control_loop != loop:
self.log.info(f'set output to be controlled on channel {loop.channel}')
self._control_loop = loop
self.configure()
else:
self.fix_heater_range()
def set_open_loop(self):
"""set to open loop"""
if self._control_loop is not None:
self.log.info('put output into manual mode')
self._control_loop = None
self.configure()
else:
self.fix_heater_range()
def fix_heater_range(self):
"""switch on heater range, if off"""
@@ -890,10 +911,10 @@ class MainOutput(Output):
self.put_manual_power(self._control_loop.power_offset)
def read_max_power(self):
curidx, self._user_current = self.query(f'HTRSET? {self.output_no}', int, int, float)[1:3]
curidx, self._user_current = self.query(f'HTRSET? {self.output_no}', int, int, float)[1:3] # ! cmd not found in ls370 manual (HTRRNG?)
if not self._user_current:
self._user_current = 2.0 ** (curidx * 0.5 - 1)
self._htr_range = self.query(f'RANGE?{self.output_no}', int) or self._htr_range
self._htr_range = self.query(f'RANGE?{self.output_no}', int) or self._htr_range # ! cmd not found in ls370 manual (HTRRNG?)
self._power_scale = self.imax ** 2 * self.heater_ranges[self._htr_range] / 1e4
return self.calc_power(100)
@@ -964,13 +985,16 @@ class Loop(HasConvergence, HasOutputModule, Sensor):
}, prefix='pid_', readonly=False)
power_offset = Parameter('power offset\n\n(using LakeShores Manual Output)',
FloatRange(0, 100, unit='W'), readonly=False, default=0)
def loop(self):
return f' {self.output_module.output_no}'
def write_ctrlpars(self, pid):
pid = self.command(f'PID {self.output_module.output_no}', *[pid[k] for k in 'pid'])
pid = self.command(f'PID{self.loop()}', *[pid[k] for k in 'pid'])
return dict(zip('pid', pid))
def read_ctrlpars(self):
pid = self.query(f'PID?{self.output_module.output_no}', float, float, float)
pid = self.query(f'PID?{self.loop()}', float, float, float)
return dict(zip('pid', pid))
def write_target(self, value):
@@ -978,7 +1002,6 @@ class Loop(HasConvergence, HasOutputModule, Sensor):
if sensor_status[0] >= BUSY:
raise ImpossibleError(f'can not control while status is {sensor_status}')
self.activate_control()
self.output_module.set_closed_loop(self)
self.set_target(value)
def write_power_offset(self, value):
@@ -989,10 +1012,24 @@ class Loop(HasConvergence, HasOutputModule, Sensor):
return self.get_target()
def set_target(self, value):
return self.command(f'SETP {self.output_module.output_no}', float(value))
return self.command(f'SETP{self.loop()}', float(value))
def get_target(self):
return self.query(f'SETP?{self.output_module.output_no}', float)
return self.query(f'SETP?{self.loop()}', float)
def set_control_active(self, active):
if active:
controlled_by = self.name
if self.output_module.controlled_by != controlled_by:
self.log.info(f'set output to be controlled on channel {self.channel}')
self.output_module.controlled_by = controlled_by
self.output_module.configure()
else:
self.output_module.fix_heater_range()
else:
self.output_module.write_target(0)
self.log.info('put output into manual loop')
# TODO: or should we keep output value?
class Device2(Device):
+6 -2
View File
@@ -61,7 +61,11 @@ class Device(ls.Device):
return super().is_equal(left, right, fixeps, significant)
class Sensor340(ls.Sensor):
class RawSensor(ls.RawSensor):
model = 340
class Sensor(ls.Sensor):
model = 340
intype = None # arguments for the intype command
@@ -100,7 +104,7 @@ class Sensor340(ls.Sensor):
self.command(f'INSET {self.channel}', 1, 1)
class Loop340(ls.Loop, Sensor340):
class Loop340(ls.Loop, Sensor):
pass
+537
View File
@@ -0,0 +1,537 @@
#!/usr/bin/env python
# *****************************************************************************
# 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
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# Anik Stark <anik.stark@psi.ch>
# *****************************************************************************
"""LakeShore 370"""
import time
from math import sqrt
from frappy.datatypes import BoolType, EnumType, FloatRange, IntRange, StatusType
from frappy.lib import formatStatusBits
from frappy.core import Done, Drivable, Parameter, Property, CommonReadHandler, CommonWriteHandler, Writable, \
IDLE, ERROR, DISABLED, Command
from frappy_psi.channelswitcher import Channel, ChannelSwitcher
import frappy_psi.lakeshore as ls
Status = Drivable.Status
STATUS_BIT_LABELS = 'CS_OVL VCM_OVL VMIX_OVL VDIF_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()
class Ls370:
model = 370
channel = Property('used channel', IntRange(1, 17))
class IO(ls.IO):
identification = [('*IDN?', 'LSCI,MODEL370,.*')]
wait_before = 0.05
class Device(ls.Device):
# ioClass = IO
model = 370
channels = list(range(1, 17))
user_curves = (1, 21) # the last curve is 20
log_formats = True, True # log Ohm / log K is supported
cmds_per_line = 1
_crvsav_deadline = None
def disable_channel(self, channel):
self.command(f'INSET {channel}', 0, 1, 3, 0, 1)
def put_header(self, curve_no, name, sn, fmt, limit, coef):
self.command(f'CRVHDR {curve_no}', f'{name:15s}', f'{sn:10s}', fmt, limit, coef)
def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6):
# for whatever reason, the number of digits after decimal point for the T column is only 4
return super().is_equal(left, right, fixeps, significant)
class Switcher(ls.HasLscIO, ChannelSwitcher):
value = Parameter('channel', datatype=IntRange(1, 16))
target = Parameter('channel', datatype=IntRange(1, 16))
use_common_delays = Parameter('use switch_delay and measure_delay instead of the channels pause and dwell',
BoolType(), readonly=False, default=False)
common_pause = Parameter('pause with common delays', FloatRange(3, 200, unit='s'), readonly=False, default=3)
ioClass = IO
fast_poll = 1
_measure_delay = None
_switch_delay = None
def startModule(self, start_events):
super().startModule(start_events)
# disable unused channels
for ch in range(1, 16):
if ch not in self.channels:
self.command(f'INSET {ch}', 0, 1, 3, 0, 1)
channelno, autoscan = self.query('SCAN?', int, int)
if channelno in self.channels and self.channels[channelno].enabled:
if not autoscan:
return # nothing to do
else:
channelno = self.next_channel(channelno)
if channelno is None:
self.status = 'ERROR', 'no enabled channel'
return
self.command(f'SCAN {channelno}', 0)
def doPoll(self):
"""poll buttons
and check autorange during filter time
"""
super().doPoll()
# self.channels[self.target].get_raw_value() # check range or read # ! no get_raw_value()
channelno, autoscan = self.query('SCAN?', int, int)
if autoscan:
# pressed autoscan button: switch off HW autoscan and toggle soft autoscan
self.autoscan = not self.autoscan
# self.communicate(f'SCAN {self.value},0;SCAN?')
self.command('SCAN', self.value, 0)
if channelno != self.value:
# channel changed by keyboard
if channelno in self.channels:
self.write_target(channelno)
else:
channelno = self.value
chan = self.channels.get(channelno)
if chan is None:
channelno = self.next_channel(channelno)
if channelno is None:
raise ValueError('no channels enabled')
self.write_target(channelno)
chan = self.channels.get(self.value)
chan.read_autorange()
chan.fix_autorange() # check for toggled autorange button
return Done
def write_switch_delay(self, value):
self._switch_delay = value
return super().write_switch_delay(value)
def write_measure_delay(self, value):
self._measure_delay = value
return super().write_measure_delay(value)
def write_use_common_delays(self, value):
if value:
# use values from a previous change, instead of
# the values from the current channel
if self._measure_delay is not None:
self.measure_delay = self._measure_delay
if self._switch_delay is not None:
self.switch_delay = self._switch_delay
return value
def set_delays(self, chan):
if self.use_common_delays:
if chan.dwell != self.measure_delay:
chan.write_dwell(self.measure_delay)
if chan.pause != self.common_pause:
chan.write_pause(self.common_pause)
filter_ = max(0, self.switch_delay - self.common_pause)
if chan.filter != filter_:
chan.write_filter(filter_)
else:
# switch_delay and measure_delay is changing with channel
self.switch_delay = chan.pause + chan.filter
self.measure_delay = chan.dwell
def set_active_channel(self, chan):
self.command(f'SCAN {chan.channel}', 0)
self.value = chan.channel
chan._last_range_change = time.monotonic()
self.set_delays(chan)
class SensorBase(Ls370, ls.SensorBase, Channel):
"""temperature channel on Lakeshore 370"""
RES_RANGE = {key: i+1 for i, key in list(
enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm']
for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]}
CUR_RANGE = {key: i + 1 for i, key in list(
enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA']
for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]}
VOLT_RANGE = {key: i + 1 for i, key in list(
enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))}
RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used
MAX_RNG = len(RES_SCALE) - 1
status = Parameter(datatype=StatusType(Drivable, 'DISABLED'))
pollinterval = Parameter(visibility=3, default=1)
range = Parameter('reading range', readonly=False,
datatype=EnumType(**RES_RANGE))
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
datatype=EnumType(**RES_RANGE))
autorange = Parameter('autorange', datatype=BoolType(),
readonly=False, default=1)
iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False)
vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False)
enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False)
pause = Parameter('pause after channel change', datatype=FloatRange(3, 60, unit='s'), readonly=False)
dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200, unit='s'), readonly=False)
filter = Parameter('filter time', datatype=FloatRange(1, 200, unit='s'), readonly=False)
_toggle_autorange = 'init' # flag to toggle autorange
_prev_rdgrng = (1, 1) # last read values for icur and exc
_last_range_change = 0
rdgrng_params = 'range', 'iexc', 'vexc'
inset_params = 'enabled', 'pause', 'dwell'
_curve = None
tempco = None
def initModule(self):
# take io from switcher
# pylint: disable=unsupported-assignment-operation
self.attachedModules['io'] = self.switcher.io # pylint believes this is None
super().initModule()
def is_switching(self, now, last_switch, switch_delay):
last_switch = max(last_switch, self._last_range_change)
if now + 0.5 > last_switch + self.pause:
self.get_data() # adjust range only
return super().is_switching(now, last_switch, switch_delay)
@CommonReadHandler(rdgrng_params)
def read_rdgrng(self):
iscur, exc, rng, autorange, excoff = self.query(f'RDGRNG?{self.channel}', int, int, int, int, int)
self._prev_rdgrng = iscur, exc
if autorange:
# pressed autorange button: toggle software autorange
# we always disable hardware autorange
if not self._toggle_autorange:
self._toggle_autorange = True
iexc = 0 if excoff or not iscur else exc
vexc = 0 if excoff or iscur else exc
if (rng, iexc, vexc) != (self.range, self.iexc, self.vexc):
self._last_range_change = time.monotonic()
try:
self.range, self.iexc, self.vexc = rng, iexc, vexc
except Exception:
# avoid raising errors on disabled channel
if self.enabled:
raise
@CommonWriteHandler(rdgrng_params)
def write_rdgrng(self, change):
self.read_range() # make sure autorange is handled
if 'vexc' in change: # in case vext is changed, do not consider iexc
change['iexc'] = 0
if change['iexc']:
iscur = 1
exc = change['iexc']
excoff = 0
elif change['vexc']:
iscur = 0
exc = change['vexc']
excoff = 0
else:
iscur, exc = self._prev_rdgrng # set to last read values
excoff = 1
rng = change['range']
if self.autorange:
rng = max(rng, self.minrange)
self.command(f'RDGRNG {self.channel}', iscur, exc, rng, 0, excoff)
self.read_range()
def fix_autorange(self):
if self._toggle_autorange:
if self._toggle_autorange == 'init':
self.write_autorange(True)
else:
self.write_autorange(not self.autorange)
self._toggle_autorange = False
@CommonReadHandler(inset_params)
def read_inset(self):
# ignore curve no and temperature coefficient
self.enabled, self.dwell, self.pause, self.curve_no, self.tempco \
= self.query(f'INSET?{self.channel}', int, int, int, int, int)
@CommonWriteHandler(inset_params)
def write_inset(self, change):
_, _, _, curve_no, tempco = self.query(f'INSET?{self.channel}', int, int, int, int, int)
self.enabled, self.dwell, self.pause, _, _ = self.command(
f'INSET {self.channel}', change['enabled'], change['dwell'], change['pause'],
curve_no, tempco)
if 'enabled' in change and change['enabled']:
# switch to enabled channel
self.switcher.write_target(self.channel)
elif self.switcher.target == self.channel:
self.switcher.set_delays(self)
def read_filter(self):
on, settle = self.query(f'FILTER?{self.channel}', int, int)
return settle if on else 0
def write_filter(self, value):
on = 1 if value else 0
value = max(1, value)
on, settle, _ = self.command(f'FILTER {self.channel}', on, value, self.channel)
if not on:
settle = 0
return settle
class RawSensor(SensorBase, ls.RawSensor):
def get_internal_data(self):
ch = self.channel
rdgst = self.query(f'RDGST?{ch}', int)
raw = self.query(f'RDGR?{ch}', float)
return rdgst, raw, raw
def get_data(self):
if not self.enabled:
self.status = DISABLED, 'disabled'
if not self.channel == self.switcher.value == self.switcher.target:
return self.status, self.value, self.value
return super().get_data()
class Sensor(SensorBase, ls.Sensor):
def get_internal_data(self):
ch = self.channel
rdgst = self.query(f'RDGST?{ch}', int)
raw = self.query(f'RDGR?{ch}', float)
value = self.query(f'RDGK?{ch}', float)
return rdgst, raw, value
def get_data(self):
if not self.enabled:
self.status = DISABLED, 'disabled'
if not self.channel == self.switcher.value == self.switcher.target:
return self.status, self.value, self.raw
return super().get_data()
def install_sensor(self):
if self.query(f'INSET?{self.channel}', int, int, int, int, int)[4] != self.tempco:
self.write_enabled(True)
def install_curve(self):
"""already done in install_sensor()"""
reply = self.query(f'INSET?{self.channel}', int, int, int, int, int)
self.log.info('install curve %r %r', reply, self.curve_no)
if reply[3] != self.curve_no:
self.enabled, self.dwell, self.pause, _, _ = self.command(
f'INSET {self.channel}', self.enabled, self.dwell, self.pause,
self.curve_no, self.tempco)
def get_curve_type(self, calcurve):
unit = calcurve.options.get('unit', 'Ohm')
logformat = True, False
range_limit = None
if unit == 'Ohm':
if calcurve.ptc:
self.tempco = 2 # PTC
else:
self.tempco = 1 # NTC
return logformat, range_limit
class SorbHeater(ls.HasLscIO, Writable):
value = Parameter('heater output', FloatRange(0, unit='W'))
target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False, default=0)
output = Property('output channel', IntRange(0, 2), default=1)
HTRRNG = {n: i for i, n in enumerate(['off', '31.6uA', '100uA', '316uA', '1mA', '3.16mA', '10mA', '31.6mA', '100mA'])}
heater_range = Parameter('heater range', EnumType(HTRRNG), readonly=False)
target_max = Parameter('max power', FloatRange(0, unit='$'), readonly=False, default=5)
fullpower = Parameter('power at 100 %', FloatRange(0, unit='$'), readonly=False, default=5)
def write_target(self, value):
percent = 100 * sqrt(value / self.fullpower)
on = bool(value)
if on != bool(self.heater_range):
self.write_heater_range(on)
self.command(f'MOUT {self.output},{percent}')
# percent = parse1(self.communicate(f'MOUT {ch},{percent};MOUT?{ch}'))
# percent = self.query(f'MOUT?{ch}', float)
# return (percent * 0.01) ** 2 * self.fullpower
return self.read_value()
def read_value(self):
percent = self.query(f'MOUT?{self.output}', float)
# percent = parse1(self.communicate(f'MOUT?{self.output}'))
return (percent * 0.01) ** 2 * self.fullpower
def write_heater_range(self, value):
# ch = self.output
# return parse1(self.communicate(f'RANGE {ch},{value};RANGE?{ch}'))
self.command(f'HTRRNG ', value)
return self.read_heater_range()
def read_heater_range(self):
# return parse1(self.communicate(f'RANGE?{self.output}'))
return self.query(f'HTRRNG?', int)
class TemperatureLoop(ls.Loop, Sensor, Drivable):
loop = Property('lakshore loop', IntRange(0, 1), default=0) # TODO: implemented specific issues of loop 1
target = Parameter('setpoint', FloatRange(0, unit='$'))
control_active = Parameter('we are controlling', BoolType(), default=0)
_control_active = False
def loop(self):
return ''
def doPoll(self):
super().doPoll()
self.read_control_active()
def read_control_active(self):
if self._control_active:
self.output_module.fix_heater_range()
return self._control_active
# def read_target(self):
# if self._control_active:
# return self.query('SETP?', float)
# return 0
def set_target(self, target):
outmode = 1
prev = self.query(f'CMODE?', int)
if outmode != prev:
self.command(f'CMODE', outmode)
for chan in self.switcher.channels.values():
chan._control_active = False
self._control_active = True
self.read_control_active()
self.convergence_start()
# do not return the readback value, as it might not yet be correct
self.command('SETP', target)
return target
HEATER_RANGES = {8 - i: 10 ** -i for i in range(8)}
class MainOutput(ls.MainOutput):
ioClass = IO
model = 370
HTRRNG = {n: i for i, n in enumerate(['off', '31.6uA', '100uA', '316uA', '1mA', '3.16mA', '10mA', '31.6mA', '100mA'])}
# htrrng = Parameter('', EnumType(HTRRNG), readonly=False)
htr = Parameter('heater percentage', FloatRange(unit='%'))
minheater = Parameter('minimal heater current', FloatRange(0, 0.01, unit='A'), readonly=False, default=0)
resistance = Parameter(datatype=FloatRange(1,100000, unit='Ohm'))
heater_ranges = HEATER_RANGES
HTRST_MAP = {
0: (IDLE, ''),
1: (ERROR, 'Open heater load'),
}
_manual_output = 0.0 # TODO: check how to set this
vmax = 50
imax = 2
max_currents = {4 - i: 2 ** (1 - i) for i in range(4)}
SETPOINTLIMS = 1500.0
sorted_factors = sorted([(fhtr * fcur ** 2, (i, h))
for i, fcur in max_currents.items()
for h, fhtr in HEATER_RANGES.items()
])
def get_status(self):
st = self.query(f'HTRST?', int)
return self.HTRST_MAP[st]
def configure(self):
if self._desired_max_power is None:
self.log.info(f'max_heater {self.writeDict} {self.max_heater}')
self.write_max_heater(self.max_heater)
icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1)
self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
self._htr_range = htr_range
if self._control_loop is None:
mode = self.query(f'CMODE?', int)
if mode != 3: # open loop
self.command(f'CSET', '0', 1, 1, 1, 1, self._htr_range, self.resistance) # control off
self.command(f'CMODE', 3) # open loop
self.command('HTRRNG', self._htr_range)
self.put_manual_power(self._manual_output)
else:
self.command(f'CSET', self._control_loop.channel, 1, 1, 1, 1, self._htr_range, self.resistance) # control on
self.command(f'CMODE', 1) # pid
self.command('HTRRNG', self._htr_range)
self.put_manual_power(self._control_loop.power_offset)
def fix_heater_range(self):
# switch heater range on, if needed
irng = self.query('HTRRNG?', int)
if irng != self._htr_range:
if irng:
self.log.info('output range was changed manually')
self._htr_range = irng
else:
self.log.info('output was off - switch on again')
self.command('HTRRNG', self._htr_range)
def read_max_power(self):
htr_range = self.query(f'CSET?', int, int, int, int, int, int, float)[5]
htr_range = self.query('HTRRNG?', int) or htr_range
self._htr_range = htr_range
self._power_scale = 0.1 ** 2 * self.heater_ranges[htr_range] / 1e4
return self.calc_power(100)
def read_htr(self):
"""read heater output in percent or power depending on the heater output selection"""
return self.query('HTR?', float)
@Command
def control_off(self):
"""switch control off"""
self._control_active = False
self.command(f'HTRRNG', 0)
self.read_control_active()
def set_htrrng(self):
if self._control_active:
newhtr = int(self.htrrng) if self._control_active else 0
htrrng = self.query('HTRRNG?', int)
if htrrng != newhtr:
if newhtr:
self.log.info('switched heater on %d', newhtr)
self.command('HTRRNG', newhtr)
def put_manual_power(self, value):
self.command(f'MOUT ', value)
# def write_htrrng(self, value):
# if self._control_active:
# self.command('HTRRNG', int(value))
# return value
class AnalogOutput(ls.HasLscIO, Writable):
target = Parameter('target', datatype=FloatRange(0, 100, unit='%'))
value = Parameter('value', datatype=FloatRange(0, 100, unit='%'))
output_no = Property('output number', datatype=IntRange(1, 2))
def write_target(self, target):
if target > 0:
self.command(f'ANALOG {self.output_no}', 1, 2, 1, 1, 0.0, 0.0, target) # manual (2)
return
self.command(f'ANALOG {self.output_no}', 1, 0, 1, 1, 0.0, 0.0, target) # off (0)
def read_value(self):
return self.query(f'ANALOG? {self.output_no}', int, int, int, int, float, float, float)[6]
+11 -11
View File
@@ -414,11 +414,11 @@ class TemperatureLoop(HasConvergence, TemperatureChannel, Drivable):
minheater = Parameter('minimal heater current', FloatRange(0, 0.01, unit='A'), readonly=False, default=0)
HTRRNG = {n: i for i, n in enumerate(['off', '30uA', '100uA', '300uA', '1mA', '3mA', '10mA', '30mA', '100mA'])}
htrrng = Parameter('', EnumType(HTRRNG), readonly=False)
ctrlpars = StructParam('control parameters struct', {
'p': Parameter('proportional heat parameter', FloatRange()),
'i': Parameter('integral heat parameter', FloatRange()),
'd': Parameter('derivative heat parameter', FloatRange()),
}, readonly=False, export=False)
# ctrlpars = StructParam('control parameters struct', {
# 'p': Parameter('proportional heat parameter', FloatRange()),
# 'i': Parameter('integral heat parameter', FloatRange()),
# 'd': Parameter('derivative heat parameter', FloatRange()),
# }, readonly=False, export=False)
htr = Parameter('heater percentage', FloatRange(unit='%'))
_control_active = False
@@ -481,10 +481,10 @@ class TemperatureLoop(HasConvergence, TemperatureChannel, Drivable):
self.communicate(f'SETP {self.loop},{target};*OPC?')
return target
def write_ctrlpars(self, ctrlpars):
p, i, d = self.change(f'PID {self.loop}', ctrlpars['p'], ctrlpars['i'], ctrlpars['d'])
return {'p': p, 'i': i, 'd': d}
# def write_ctrlpars(self, ctrlpars):
# p, i, d = self.change(f'PID {self.loop}', ctrlpars['p'], ctrlpars['i'], ctrlpars['d'])
# return {'p': p, 'i': i, 'd': d}
def read_ctrlpars(self):
p, i, d = self.query(f'PID?{self.loop}')
return {'p': p, 'i': i, 'd': d}
# def read_ctrlpars(self):
# p, i, d = self.query(f'PID?{self.loop}')
# return {'p': p, 'i': i, 'd': d}