improvements on PPMS and LS370

- PPMS: improved machanism for 10 K waiting
- LS370: fixed an issue with auto range
+ LS370: show test for all status bits

Change-Id: Ia6454141917893f0e5c6c4351df3a864942bb629
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23495
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2020-06-25 12:02:17 +02:00
parent a520e6e1e4
commit aa4c8f1f04
5 changed files with 148 additions and 110 deletions

View File

@ -247,3 +247,14 @@ def getfqdn(name=''):
def getGeneralConfig(): def getGeneralConfig():
return CONFIG return CONFIG
def formatStatusBits(sword, labels, start=0):
"""Return a list of labels according to bit state in `sword` starting
with bit `start` and the first label in `labels`.
"""
result = []
for i, lbl in enumerate(labels, start):
if sword & (1 << i) and lbl:
result.append(lbl)
return result

View File

@ -27,6 +27,7 @@ from secop.metaclass import Done
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.poller import Poller, REGULAR from secop.poller import Poller, REGULAR
from secop.lib import formatStatusBits
import secop.iohandler import secop.iohandler
Status = Drivable.Status Status = Drivable.Status
@ -49,10 +50,7 @@ filterhdl = IOHandler('filter', 'FILTER?%(channel)d', '%d,%d,%d')
scan = IOHandler('scan', 'SCAN?', '%d,%d') scan = IOHandler('scan', 'SCAN?', '%d,%d')
STATUS_TEXT = {0: ''} STATUS_BIT_LABELS = 'CS_OVL VCM_OVL VMIX_OVL VDIF_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()
for bit, text in enumerate('CS_OVL VCM_OVL VMIX_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()):
for i in range(1 << bit, 2 << bit):
STATUS_TEXT[i] = text
class StringIO(secop.stringio.StringIO): class StringIO(secop.stringio.StringIO):
@ -177,13 +175,13 @@ class ResChannel(HasIodev, Readable):
return result return result
def read_status(self): def read_status(self):
if self.channel != self._main.channel:
return Done
if not self.enabled: if not self.enabled:
return [self.Status.DISABLED, 'disabled'] return [self.Status.DISABLED, 'disabled']
if self.channel != self._main.channel:
return Done
result = int(self.sendRecv('RDGST?%d' % self.channel)) result = int(self.sendRecv('RDGST?%d' % self.channel))
result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities) result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
statustext = STATUS_TEXT[result] statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
if statustext: if statustext:
return [self.Status.ERROR, statustext] return [self.Status.ERROR, statustext]
return [self.Status.IDLE, ''] return [self.Status.IDLE, '']
@ -191,10 +189,11 @@ class ResChannel(HasIodev, Readable):
def analyze_rdgrng(self, iscur, exc, rng, autorange, excoff): def analyze_rdgrng(self, iscur, exc, rng, autorange, excoff):
result = dict(range=rng) result = dict(range=rng)
if autorange: if autorange:
result['auotrange'] = 'hard' result['autorange'] = 'hard'
elif self.autorange == 'hard': #elif self.autorange == 'hard':
result['autorange'] = 'soft' # result['autorange'] = 'soft'
# else: do not change autorange # else: do not change autorange
self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
if excoff: if excoff:
result.update(iexc=0, vexc=0) result.update(iexc=0, vexc=0)
elif iscur: elif iscur:
@ -225,6 +224,7 @@ class ResChannel(HasIodev, Readable):
if change.autorange == 'soft': if change.autorange == 'soft':
if rng < self.minrange: if rng < self.minrange:
rng = self.minrange rng = self.minrange
self.autorange = change.autorange
return iscur, exc, rng, autorange, excoff return iscur, exc, rng, autorange, excoff
def analyze_inset(self, on, dwell, pause, curve, tempco): def analyze_inset(self, on, dwell, pause, curve, tempco):

View File

@ -21,7 +21,6 @@
"""a very simple simulator for a LakeShore Model 370""" """a very simple simulator for a LakeShore Model 370"""
from secop.modules import Communicator from secop.modules import Communicator
#from secop.lib import mkthread
class Ls370Sim(Communicator): class Ls370Sim(Communicator):
CHANNEL_COMMANDS = [ CHANNEL_COMMANDS = [
@ -47,7 +46,7 @@ class Ls370Sim(Communicator):
for channel in range(1,17): for channel in range(1,17):
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
if excoff == '1': if excoff == '1':
self._data['RDGST?%d' % channel] = '4' self._data['RDGST?%d' % channel] = '6'
else: else:
self._data['RDGST?%d' % channel] = '0' self._data['RDGST?%d' % channel] = '0'
@ -70,8 +69,3 @@ class Ls370Sim(Communicator):
#if command.startswith('R'): #if command.startswith('R'):
# print('> %s\t< %s' % (command, reply)) # print('> %s\t< %s' % (command, reply))
return ';'.join(reply) return ';'.join(reply)
#def run(self):
# # time dependent simulation
# while True:
# time.sleep(1)

View File

@ -209,8 +209,18 @@ class UserChannel(Channel):
'no': 'no':
Property('channel number', Property('channel number',
datatype=IntRange(0, 0), export=False, default=0), datatype=IntRange(0, 0), export=False, default=0),
'linkenable':
Property('name of linked channel for enabling',
datatype=StringType(), export=False, default=''),
} }
def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None)
if other:
other.enabled = enabled
return enabled
class DriverChannel(Channel): class DriverChannel(Channel):
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
@ -410,8 +420,11 @@ class Temp(PpmsMixin, Drivable):
Parameter('intermediate set point', Parameter('intermediate set point',
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp),
'ramp': 'ramp':
Parameter('ramping speed', readonly=False, handler=temp, default=0, Parameter('ramping speed', readonly=False, default=0,
datatype=FloatRange(0, 20, unit='K/min')), datatype=FloatRange(0, 20, unit='K/min')),
'workingramp':
Parameter('intermediate ramp value',
datatype=FloatRange(0, 20, unit='K/min'), handler=temp),
'approachmode': 'approachmode':
Parameter('how to approach target!', readonly=False, handler=temp, Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode)), datatype=EnumType(ApproachMode)),
@ -458,6 +471,7 @@ class Temp(PpmsMixin, Drivable):
general_stop = False general_stop = False
_cool_deadline = 0 _cool_deadline = 0
_wait_at10 = False _wait_at10 = False
_ramp_at_limit = False
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
"""update value and status""" """update value and status"""
@ -469,10 +483,10 @@ class Temp(PpmsMixin, Drivable):
status = self.STATUS_MAP.get(status_code, (self.Status.ERROR, 'unknown status code %d' % status_code)) status = self.STATUS_MAP.get(status_code, (self.Status.ERROR, 'unknown status code %d' % status_code))
now = time.time() now = time.time()
if value > 11: if value > 11:
# when starting from T > 40, this will be 15 min. # when starting from T > 50, this will be 15 min.
# when starting from lower T, it will be less # when starting from lower T, it will be less
# when ramping with 2 K/min or less, the deadline is now # when ramping with 2 K/min or less, the deadline is now
self._cool_deadline = max(self._cool_deadline, now + min(30, value - 10) * 30) # 30 sec / K self._cool_deadline = max(self._cool_deadline, now + min(40, value - 10) * 30) # 30 sec / K
elif self._wait_at10: elif self._wait_at10:
if now > self._cool_deadline: if now > self._cool_deadline:
self._wait_at10 = False self._wait_at10 = False
@ -506,24 +520,40 @@ class Temp(PpmsMixin, Drivable):
self._expected_target_time = 0 self._expected_target_time = 0
self.status = status self.status = status
def analyze_temp(self, setpoint, ramp, approachmode): def analyze_temp(self, setpoint, workingramp, approachmode):
if (setpoint, workingramp, approachmode) == self._last_settings:
# update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware
return {}
self._last_settings = setpoint, workingramp, approachmode
if setpoint != 10 or not self._wait_at10: if setpoint != 10 or not self._wait_at10:
self.log.debug('read back target %g %r' % (setpoint, self._wait_at10))
self.target = setpoint self.target = setpoint
result = dict(setpoint=setpoint) if workingramp != 2 or not self._ramp_at_limit:
# we update ramp and approachmode only at init self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit))
if self.ramp == 0: self.ramp = workingramp
result['ramp'] = ramp result = dict(setpoint=setpoint, workingramp=workingramp)
result['approachmode'] = approachmode self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp)))
return result return result
def change_temp(self, change): def change_temp(self, change):
if 10 >= self.value > change.setpoint:
ramp = min(2, change.ramp)
print('ramplimit', change.ramp, self.value, ramp)
else:
ramp = change.ramp ramp = change.ramp
self.calc_expected(change.setpoint, ramp) setpoint = change.setpoint
return change.setpoint, ramp, change.approachmode wait_at10 = False
ramp_at_limit = False
if self.value > 11:
if setpoint <= 10:
wait_at10 = True
setpoint = 10
elif self.value > setpoint:
if ramp >= 2:
ramp = 2
ramp_at_limit = True
self._wait_at10 = wait_at10
self._ramp_at_limit = ramp_at_limit
self.calc_expected(setpoint, ramp)
self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit))
return setpoint, ramp, change.approachmode
def write_target(self, target): def write_target(self, target):
self._stopped = False self._stopped = False
@ -532,24 +562,22 @@ class Temp(PpmsMixin, Drivable):
self._status_before_change = self.status self._status_before_change = self.status
self.status = (self.Status.BUSY, 'changed target') self.status = (self.Status.BUSY, 'changed target')
self._last_change = time.time() self._last_change = time.time()
if self.value > 10 > target and self.ramp > 2:
self._wait_at10 = True
self.temp.write(self, 'setpoint', 10)
else:
self._wait_at10 = False
self.temp.write(self, 'setpoint', target) self.temp.write(self, 'setpoint', target)
self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10)))
return target return target
def write_approachmode(self, value): def write_approachmode(self, value):
if self.isDriving(): if self.isDriving():
self.temp.write(self, 'approachmode', value) self.temp.write(self, 'approachmode', value)
return Done return Done
self.approachmode = value
return None # do not execute TEMP command, as this would trigger an unnecessary T change return None # do not execute TEMP command, as this would trigger an unnecessary T change
def write_ramp(self, value): def write_ramp(self, value):
if self.isDriving(): if self.isDriving():
self.temp.write(self, 'ramp', value) self.temp.write(self, 'ramp', value)
return Done return Done
# self.ramp = value
return None # do not execute TEMP command, as this would trigger an unnecessary T change return None # do not execute TEMP command, as this would trigger an unnecessary T change
def calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
@ -656,6 +684,7 @@ class Field(PpmsMixin, Drivable):
self.status = status self.status = status
def analyze_field(self, target, ramp, approachmode, persistentmode): def analyze_field(self, target, ramp, approachmode, persistentmode):
# print('last_settings tt %s' % repr(self._last_settings))
if (target, ramp, approachmode, persistentmode) == self._last_settings: if (target, ramp, approachmode, persistentmode) == self._last_settings:
# we update parameters only on change, as 'ramp' and 'approachmode' are # we update parameters only on change, as 'ramp' and 'approachmode' are
# not always sent to the hardware # not always sent to the hardware
@ -669,6 +698,7 @@ class Field(PpmsMixin, Drivable):
def write_target(self, target): def write_target(self, target):
if abs(self.target - self.value) <= 2e-5 and target == self.target: if abs(self.target - self.value) <= 2e-5 and target == self.target:
self.target = target
return None # avoid ramping leads return None # avoid ramping leads
self._status_before_change = self.status self._status_before_change = self.status
self._stopped = False self._stopped = False
@ -679,6 +709,7 @@ class Field(PpmsMixin, Drivable):
def write_persistentmode(self, mode): def write_persistentmode(self, mode):
if abs(self.target - self.value) <= 2e-5 and mode == self.persistentmode: if abs(self.target - self.value) <= 2e-5 and mode == self.persistentmode:
self.persistentmode = mode
return None # avoid ramping leads return None # avoid ramping leads
self._last_change = time.time() self._last_change = time.time()
self._status_before_change = self.status self._status_before_change = self.status
@ -688,6 +719,7 @@ class Field(PpmsMixin, Drivable):
return Done return Done
def write_ramp(self, value): def write_ramp(self, value):
self.ramp = value
if self.isDriving(): if self.isDriving():
self.field.write(self, 'ramp', value) self.field.write(self, 'ramp', value)
return Done return Done
@ -805,6 +837,7 @@ class Position(PpmsMixin, Drivable):
if self.isDriving(): if self.isDriving():
self.move.write(self, 'speed', value) self.move.write(self, 'speed', value)
return Done return Done
self.speed = value
return None # do not execute MOVE command, as this would trigger an unnecessary move return None # do not execute MOVE command, as this would trigger an unnecessary move
def do_stop(self): def do_stop(self):

View File

@ -53,7 +53,7 @@ class PpmsSim:
def __init__(self): def __init__(self):
self.status = NamedList('t mf ch pos', 1, 1, 1, 1) self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
self.st = 0x1111 self.st = 0x1111
self.t = 200 self.t = 15
self.temp = NamedList('target ramp amode', 200., 1, 0, fast=self.t, delay=10) self.temp = NamedList('target ramp amode', 200., 1, 0, fast=self.t, delay=10)
self.mf = 100 self.mf = 100
self.field = NamedList('target ramp amode pmode', 0, 50, 0, 0) self.field = NamedList('target ramp amode pmode', 0, 50, 0, 0)