changed/fixed behaviour of stop commands in PPMS

- make sure new target created by the stop command is between
  old and new target
- fixed bad behaviour of level reading

Change-Id: I484c0902c694c0edb81e2d9238985c05f92e04f4
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22100
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2020-01-08 14:46:45 +01:00
parent d442c5b6bf
commit f0a3306f9c
3 changed files with 98 additions and 84 deletions

View File

@ -81,7 +81,7 @@ def clamp(_min, value, _max):
"""return the median of 3 values, """return the median of 3 values,
i.e. value if min <= value <= max, else min or max depending on which side i.e. value if min <= value <= max, else min or max depending on which side
value lies outside the [min..max] interval value lies outside the [min..max] interval. This works even when min > max!
""" """
# return median, i.e. clamp the the value between min and max # return median, i.e. clamp the the value between min and max
return sorted([_min, value, _max])[1] return sorted([_min, value, _max])[1]

View File

@ -39,6 +39,7 @@ from secop.modules import Module, Readable, Drivable, Parameter, Override,\
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\ from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
BoolType, StatusType BoolType, StatusType
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.lib import clamp
from secop.errors import HardwareError from secop.errors import HardwareError
from secop.poller import Poller from secop.poller import Poller
import secop.iohandler import secop.iohandler
@ -54,7 +55,6 @@ except ImportError:
class IOHandler(secop.iohandler.IOHandler): class IOHandler(secop.iohandler.IOHandler):
CMDARGS = ['no'] CMDARGS = ['no']
CMDSEPARATOR = None # no command chaining CMDSEPARATOR = None # no command chaining
READ_BEFORE_WRITE = False
def __init__(self, name, querycmd, replyfmt): def __init__(self, name, querycmd, replyfmt):
changecmd = querycmd.split('?')[0] + ' ' changecmd = querycmd.split('?')[0] + ' '
@ -127,11 +127,12 @@ class Main(Communicator):
class PpmsMixin(HasIodev, Module): class PpmsMixin(HasIodev, Module):
properties = { properties = {
'iodev': Attached(), 'iodev': Attached(),
'general_stop': Property('respect general stop', datatype=BoolType(),
export=True, default=True)
} }
pollerClass = Poller pollerClass = Poller
enabled = True # default, if no parameter enable is defined enabled = True # default, if no parameter enable is defined
_last_target_change = 0 # used by several modules
_last_settings = None # used by several modules _last_settings = None # used by several modules
slow_pollfactor = 1 slow_pollfactor = 1
@ -303,11 +304,9 @@ class Level(PpmsMixin, Readable):
""" """
def analyze_level(self, level, status): def analyze_level(self, level, status):
if status: # ignore 'old reading' state of the flag, as this happens only for a short time
self.status = [self.Status.IDLE, ''] # during measuring
else: return dict(value=level, status=[self.Status.IDLE, ''])
self.status = [self.Status.ERROR, 'old reading']
return dict(value=level)
class Chamber(PpmsMixin, Drivable): class Chamber(PpmsMixin, Drivable):
@ -416,10 +415,6 @@ class Temp(PpmsMixin, Drivable):
Parameter('drive timeout, in addition to ramp time', readonly=False, Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600), datatype=FloatRange(0, unit='sec'), default=3600),
} }
properties = {
'general_stop': Property('respect general stop', datatype=BoolType(),
export=True, default=True)
}
# pylint: disable=invalid-name # pylint: disable=invalid-name
TempStatus = Enum( TempStatus = Enum(
'TempStatus', 'TempStatus',
@ -447,12 +442,10 @@ class Temp(PpmsMixin, Drivable):
channel = 'temp' channel = 'temp'
_stopped = False _stopped = False
_expected_target = 0 _expected_target_time = 0
_last_change = 0 # 0 means no target change is pending _last_change = 0 # 0 means no target change is pending
_last_target = None # last reached target
def earlyInit(self): general_stop = False
self.setProperty('general_stop', False)
super().earlyInit()
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
"""update value and status""" """update value and status"""
@ -462,13 +455,6 @@ class Temp(PpmsMixin, Drivable):
self.value = value self.value = value
status = self.STATUS_MAP[packed_status & 0xf] status = self.STATUS_MAP[packed_status & 0xf]
now = time.time() now = time.time()
if self._stopped:
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
self._stopped = False
else:
self.status = [self.Status.IDLE, 'stopped(%s)' % status[1]]
return
if self._last_change: # there was a change, which is not yet confirmed by hw if self._last_change: # there was a change, which is not yet confirmed by hw
if now > self._last_change + 5: if now > self._last_change + 5:
self._last_change = 0 # give up waiting for busy self._last_change = 0 # give up waiting for busy
@ -477,13 +463,23 @@ class Temp(PpmsMixin, Drivable):
self._last_change = 0 self._last_change = 0
else: else:
status = [self.Status.BUSY, 'changed target'] status = [self.Status.BUSY, 'changed target']
if self._expected_target: if abs(self.value - self.target) < self.target * 0.01:
self._last_target = self.target
elif self._last_target is None:
self._last_target = self.value
if self._stopped:
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
status = [status[0], 'stopped']
else:
status = [status[0], 'stopping (%s)' % status[1]]
if self._expected_target_time:
# handle timeout # handle timeout
if self.isDriving(status): if self.isDriving(status):
if now > self._expected_target + self.timeout: if now > self._expected_target_time + self.timeout:
status = [self.Status.WARN, 'timeout while %s' % status[1]] status = [self.Status.WARN, 'timeout while %s' % status[1]]
else: else:
self._expected_target = 0 self._expected_target_time = 0
self.status = status self.status = status
def analyze_temp(self, target, ramp, approachmode): def analyze_temp(self, target, ramp, approachmode):
@ -500,7 +496,7 @@ class Temp(PpmsMixin, Drivable):
def write_target(self, target): def write_target(self, target):
self._stopped = False self._stopped = False
if abs(self.target - self.value) < 2e-5 and target == self.target: if abs(self.target - self.value) <= 2e-5 * target and target == self.target:
return None return None
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']
@ -518,19 +514,18 @@ class Temp(PpmsMixin, Drivable):
return None # change_temp will not be called, as this would trigger an unnecessary T change return None # change_temp will not be called, as this would trigger an unnecessary T change
def calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
self._expected_target = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
def do_stop(self): def do_stop(self):
if self.isDriving(): if not self.isDriving():
return return
if self.status[0] == self.Status.STABILIZING: if self.status[0] != self.Status.STABILIZING:
# we are already near target # we are not near target
newtarget = self.target newtarget = clamp(self._last_target, self.value, self.target)
else: if newtarget != self.target:
newtarget = self.value self.log.debug('stop at %s K', newtarget)
self.log.info('stop at %s K', newtarget) self.write_target(newtarget)
self.write_target(newtarget) self.status = [self.status[0], 'stopping (%s)' % self.status[1]]
self.status = [self.Status.IDLE, 'stopped']
self._stopped = True self._stopped = True
@ -583,7 +578,7 @@ class Field(PpmsMixin, Drivable):
channel = 'field' channel = 'field'
_stopped = False _stopped = False
_last_target = 0 _last_target = None # last reached target
_last_change = 0 # means no target change is pending _last_change = 0 # means no target change is pending
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
@ -595,14 +590,7 @@ class Field(PpmsMixin, Drivable):
status_code = (packed_status >> 4) & 0xf status_code = (packed_status >> 4) & 0xf
status = self.STATUS_MAP[status_code] status = self.STATUS_MAP[status_code]
now = time.time() now = time.time()
if self._stopped: if self._last_change: # there was a change, which is not yet confirmed by hw
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
self._stopped = False
else:
self.status = [status[0], 'stopped (%s)' % status[1]]
return
elif self._last_change: # there was a change, which is not yet confirmed by hw
if status_code == 1: # persistent mode if status_code == 1: # persistent mode
# leads are ramping (ppms has no extra status code for this!) # leads are ramping (ppms has no extra status code for this!)
if now < self._last_change + 30: if now < self._last_change + 30:
@ -616,6 +604,16 @@ class Field(PpmsMixin, Drivable):
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
else: else:
status = [self.Status.BUSY, 'changed target'] status = [self.Status.BUSY, 'changed target']
if abs(self.target - self.value) <= 1e-4:
self._last_target = self.target
elif self._last_target is None:
self._last_target = self.value
if self._stopped:
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
status = [status[0], 'stopped']
else:
status = [status[0], 'stopping (%s)' % status[1]]
self.status = status self.status = status
def analyze_field(self, target, ramp, approachmode, persistentmode): def analyze_field(self, target, ramp, approachmode, persistentmode):
@ -624,34 +622,49 @@ class Field(PpmsMixin, Drivable):
# not always sent to the hardware # not always sent to the hardware
return {} return {}
self._last_settings = target, ramp, approachmode, persistentmode self._last_settings = target, ramp, approachmode, persistentmode
return dict(target=target * 1e-4, ramp=ramp * 6e-3, approachmode=approachmode, return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode,
persistentmode=persistentmode) persistentmode=persistentmode)
def change_field(self, change): def change_field(self, change):
if change.doesInclude('target', 'persistentmode'):
if change.doesInclude('target'):
self._last_target = self.target # save for stop command
self._stopped = False
self._last_change = time.time()
self._status_before_change = list(self.status)
else:
# changed ramp or approachmode
if not self.isDriving():
return None # nothing to be written, as this would trigger a ramp up of leads current
return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode
def write_target(self, target):
if abs(self.target - self.value) <= 2e-5 and target == self.target:
return None # avoid ramping leads
self._status_before_change = list(self.status)
self._stopped = False
self._last_change = time.time()
self.status = [self.Status.BUSY, 'changed target']
return target
def write_persistentmode(self, mode):
if abs(self.target - self.value) <= 2e-5 and mode == self.persistentmode:
return None # avoid ramping leads
self._last_change = time.time()
self._status_before_change = list(self.status)
self._stopped = False
self.status = [self.Status.BUSY, 'changed persistent mode']
return mode
def write_ramp(self, value):
if self.isDriving():
return value
return None # change_field will not be called, as this would trigger a ramp up of leads current
def write_approachmode(self, value):
if self.isDriving():
return value
return None # change_temp will not be called, as this would trigger a ramp up of leads current
def do_stop(self): def do_stop(self):
if not self.isDriving(): if not self.isDriving():
return return
self.status = [self.Status.IDLE, '_stopped'] newtarget = clamp(self._last_target, self.value, self.target)
if newtarget != self.target:
self.log.debug('stop at %s T', newtarget)
self.write_target(newtarget)
self.status = [self.status[0], 'stopping (%s)' % self.status[1]]
self._stopped = True self._stopped = True
if abs(self.value - self.target) > 1e-4:
# ramping is not yet at end
if abs(self.value - self._last_target) < 1e-4:
# ramping has not started yet, use more precise last target instead of current value
self.target = self.put_settings(self._last_target, 'target')
else:
self.target = self.put_settings(self.value, 'target')
class Position(PpmsMixin, Drivable): class Position(PpmsMixin, Drivable):
@ -684,7 +697,7 @@ class Position(PpmsMixin, Drivable):
channel = 'position' channel = 'position'
_stopped = False _stopped = False
_last_target = 0 _last_target = None # last reached target
_last_change = 0 # means no target change is pending _last_change = 0 # means no target change is pending
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
@ -697,12 +710,6 @@ class Position(PpmsMixin, Drivable):
return return
self.value = value self.value = value
status = self.STATUS_MAP[(packed_status >> 12) & 0xf] status = self.STATUS_MAP[(packed_status >> 12) & 0xf]
if self._stopped:
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
self._stopped = False
else:
status = [self.Status.IDLE, 'stopped(%s)' % status[1]]
if self._last_change: # there was a change, which is not yet confirmed by hw if self._last_change: # there was a change, which is not yet confirmed by hw
now = time.time() now = time.time()
if now > self._last_change + 5: if now > self._last_change + 5:
@ -712,6 +719,16 @@ class Position(PpmsMixin, Drivable):
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
else: else:
status = [self.Status.BUSY, 'changed target'] status = [self.Status.BUSY, 'changed target']
if abs(self.value - self.target) < 0.1:
self._last_target = self.target
elif self._last_target is None:
self._last_target = self.value
if self._stopped:
# combine 'stopped' with current status text
if status[0] == self.Status.IDLE:
status = [status[0], 'stopped']
else:
status = [status[0], 'stopping (%s)' % status[1]]
self.status = status self.status = status
def analyze_move(self, target, mode, speed): def analyze_move(self, target, mode, speed):
@ -727,7 +744,6 @@ class Position(PpmsMixin, Drivable):
return change.target, 0, speed return change.target, 0, speed
def write_target(self, target): def write_target(self, target):
self._last_target = self.target # save for stop command
self._stopped = False self._stopped = False
self._last_change = 0 self._last_change = 0
self._status_before_change = self.status self._status_before_change = self.status
@ -736,17 +752,14 @@ class Position(PpmsMixin, Drivable):
def write_speed(self, value): def write_speed(self, value):
if self.isDriving(): if self.isDriving():
return value return value
return None # change_move not called: as this would trigger an unnecessary move return None # change_move not called as this would trigger an unnecessary move
def do_stop(self): def do_stop(self):
if not self.isDriving(): if not self.isDriving():
return return
self.status = [self.Status.BUSY, '_stopped'] newtarget = clamp(self._last_target, self.value, self.target)
if newtarget != self.target:
self.log.debug('stop at %s T', newtarget)
self.write_target(newtarget)
self.status = [self.status[0], 'stopping (%s)' % self.status[1]]
self._stopped = True self._stopped = True
if abs(self.value - self.target) > 1e-2:
# moving is not yet at end
if abs(self.value - self._last_target) < 1e-2:
# moving has not started yet, use more precise last target instead of current value
self.target = self.write_target(self._last_target)
else:
self.target = self.write_target(self.value)

View File

@ -54,7 +54,7 @@ class PpmsSim:
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 = 200
self.temp = NamedList('target ramp amode', 295., 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)
self.pos = 0 self.pos = 0
@ -75,6 +75,7 @@ class PpmsSim:
self.start = self.time self.start = self.time
self.mf_start = 0 self.mf_start = 0
self.ch_start = 0 self.ch_start = 0
self.t_start = 0
self.changed = set() self.changed = set()
def progress(self): def progress(self):