fix new statemachine with ips magfield

+ add on_error, on_restart etc. to states.py
+ add n_retry argument to mercury change/multichange
This commit is contained in:
2022-12-19 15:12:00 +01:00
parent 397ca350f8
commit b7a1f17e5e
4 changed files with 119 additions and 67 deletions

View File

@ -118,12 +118,32 @@ class HasStates:
self.setFastPoll(False) self.setFastPoll(False)
self.read_status() self.read_status()
def on_cleanup(self, sm):
if isinstance(sm.cleanup_reason, Exception):
return self.on_error(sm)
if isinstance(sm.cleanup_reason, Start):
return self.on_restart(sm)
if isinstance(sm.cleanup_reason, Stop):
return self.on_stop(sm)
self.log.error('bad cleanup reason %r', sm.cleanup_reason)
def on_error(self, sm):
self.log.error('handle error %r', sm.cleanup_reason)
self.final_status(ERROR, repr(sm.cleanup_reason))
return None
def on_restart(self, sm):
return None
def on_stop(self, sm):
return None
def start_machine(self, statefunc, fast_poll=True, **kwds): def start_machine(self, statefunc, fast_poll=True, **kwds):
sm = self._state_machine sm = self._state_machine
sm.status = self.get_status(statefunc, BUSY) sm.status = self.get_status(statefunc, BUSY)
if sm.statefunc: if sm.statefunc:
sm.status = sm.status[0], 'restarting' sm.status = sm.status[0], 'restarting'
sm.start(statefunc, **kwds) sm.start(statefunc, cleanup=kwds.pop('cleanup', self.on_cleanup), **kwds)
self.read_status() self.read_status()
if fast_poll: if fast_poll:
sm.reset_fast_poll = True sm.reset_fast_poll = True

View File

@ -26,7 +26,7 @@ from secop.lib.enum import Enum
from secop.errors import BadValueError, HardwareError from secop.errors import BadValueError, HardwareError
from secop_psi.magfield import Magfield, SimpleMagfield, Status from secop_psi.magfield import Magfield, SimpleMagfield, Status
from secop_psi.mercury import MercuryChannel, off_on, Mapped from secop_psi.mercury import MercuryChannel, off_on, Mapped
from secop.lib.statemachine import Retry from secop.states import Retry
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3) Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3)
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
@ -65,6 +65,13 @@ class SimpleField(MercuryChannel, SimpleMagfield):
obj = object.__new__(newclass) obj = object.__new__(newclass)
return obj return obj
def initModule(self):
super().initModule()
try:
self.write_action(Action.hold)
except Exception as e:
self.log.error('can not set to hold %r', e)
def read_value(self): def read_value(self):
return self.query('PSU:SIG:FLD') return self.query('PSU:SIG:FLD')
@ -94,12 +101,12 @@ class SimpleField(MercuryChannel, SimpleMagfield):
def set_and_go(self, value): def set_and_go(self, value):
self.setpoint = self.change('PSU:SIG:FSET', value) self.setpoint = self.change('PSU:SIG:FSET', value)
assert self.write_action('hold') == 'hold' assert self.write_action(Action.hold) == Action.hold
assert self.write_action('run_to_set') == 'run_to_set' assert self.write_action(Action.run_to_set) == Action.run_to_set
def start_ramp_to_target(self, sm): def start_ramp_to_target(self, sm):
# if self.action != 'hold': # if self.action != Action.hold:
# assert self.write_action('hold') == 'hold' # assert self.write_action(Action.hold) == Action.hold
# return Retry # return Retry
self.set_and_go(sm.target) self.set_and_go(sm.target)
sm.try_cnt = 5 sm.try_cnt = 5
@ -116,13 +123,11 @@ class SimpleField(MercuryChannel, SimpleMagfield):
return Retry return Retry
def final_status(self, *args, **kwds): def final_status(self, *args, **kwds):
print('FINAL-hold') self.write_action(Action.hold)
self.write_action('hold')
return super().final_status(*args, **kwds) return super().final_status(*args, **kwds)
def on_restart(self, sm): def on_restart(self, sm):
print('ON_RESTART-hold', sm.sm) self.write_action(Action.hold)
self.write_action('hold')
return super().on_restart(sm) return super().on_restart(sm)
@ -136,7 +141,7 @@ class Field(SimpleField, Magfield):
_field_mismatch = None _field_mismatch = None
__init = True __init = True
__switch_heater_fix = 0 __switch_fixed_until = 0
def doPoll(self): def doPoll(self):
super().doPoll() super().doPoll()
@ -208,7 +213,8 @@ class Field(SimpleField, Magfield):
value = self.query('PSU:SIG:SWHT', off_on) value = self.query('PSU:SIG:SWHT', off_on)
now = time.time() now = time.time()
if value != self.switch_heater: if value != self.switch_heater:
if now < self.__switch_heater_fix: if now < self.__switch_fixed_until:
self.log.debug('correct fixed switch time')
# probably switch heater was changed, but IPS reply is not yet updated # probably switch heater was changed, but IPS reply is not yet updated
if self.switch_heater: if self.switch_heater:
self.switch_on_time = time.time() self.switch_on_time = time.time()
@ -228,8 +234,10 @@ class Field(SimpleField, Magfield):
self.log.info('switch heater already %r', value) self.log.info('switch heater already %r', value)
# we do not want to restart the timer # we do not want to restart the timer
return value return value
self.__switch_heater_fix = time.time() + 10 self.__switch_fixed_until = time.time() + 10
return self.change('PSU:SIG:SWHT', value, off_on) self.log.debug('switch time fixed for 10 sec')
result = self.change('PSU:SIG:SWHT', value, off_on, n_retry=0) # no readback check
return result
def start_ramp_to_field(self, sm): def start_ramp_to_field(self, sm):
if abs(self.current - self.persistent_field) <= self.tolerance: if abs(self.current - self.persistent_field) <= self.tolerance:
@ -241,9 +249,7 @@ class Field(SimpleField, Magfield):
if self.switch_heater: if self.switch_heater:
self.log.warn('switch is already on!') self.log.warn('switch is already on!')
return self.ramp_to_field return self.ramp_to_field
self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field) self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.persistent_field, e)
return Retry
self.status = Status.PREPARING, 'wait for switch off'
sm.after_wait = self.ramp_to_field sm.after_wait = self.ramp_to_field
return self.wait_for_switch return self.wait_for_switch
return self.ramp_to_field return self.ramp_to_field
@ -270,7 +276,7 @@ class Field(SimpleField, Magfield):
return Retry return Retry
def wait_for_switch(self, sm): def wait_for_switch(self, sm):
if not self.delay(10): if not sm.delta(10):
return Retry return Retry
try: try:
self.log.warn('try again') self.log.warn('try again')
@ -282,8 +288,8 @@ class Field(SimpleField, Magfield):
def start_ramp_to_zero(self, sm): def start_ramp_to_zero(self, sm):
try: try:
assert self.write_action('hold') == 'hold' assert self.write_action(Action.hold) == Action.hold
assert self.write_action('run_to_zero') == 'run_to_zero' assert self.write_action(Action.run_to_zero) == Action.run_to_zero
except (HardwareError, AssertionError) as e: except (HardwareError, AssertionError) as e:
self.log.warn('switch not yet ready %r', e) self.log.warn('switch not yet ready %r', e)
self.status = Status.PREPARING, 'wait for switch off' self.status = Status.PREPARING, 'wait for switch off'
@ -298,6 +304,6 @@ class Field(SimpleField, Magfield):
sm.try_cnt -= 1 sm.try_cnt -= 1
if sm.try_cnt < 0: if sm.try_cnt < 0:
raise raise
assert self.write_action('hold') == 'hold' assert self.write_action(Action.hold) == Action.hold
assert self.write_action('run_to_zero') == 'run_to_zero' assert self.write_action(Action.run_to_zero) == Action.run_to_zero
return Retry return Retry

View File

@ -23,9 +23,9 @@ import time
from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR
from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
from secop.features import HasLimits from secop.features import HasLimits
from secop.errors import ConfigError, ProgrammingError, HardwareError from secop.errors import ConfigError, ProgrammingError, HardwareError, BadValueError
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.states import Retry, HasStates, status_code from secop.states import Retry, HasStates, status_code, Start
UNLIMITED = FloatRange() UNLIMITED = FloatRange()
@ -157,20 +157,23 @@ class Magfield(SimpleMagfield):
ramp_tmo = Parameter( ramp_tmo = Parameter(
'timeout for field ramp progress', 'timeout for field ramp progress',
FloatRange(0, unit='s'), readonly=False, default=30) FloatRange(0, unit='s'), readonly=False, default=30)
__init = True __init_persistency = True
switch_on_time = None switch_on_time = None
switch_off_time = None switch_off_time = None
def doPoll(self): def doPoll(self):
if self.__init: if self.__init_persistency:
self.__init = False if self.__init_persistency is True:
if self.read_switch_heater() and self.mode == Mode.PERSISTENT:
self.read_value() # check for persistent field mismatch
# switch off heater from previous live or manual intervention
self.write_target(self.persistent_field)
else:
self._last_target = self.persistent_field self._last_target = self.persistent_field
self.__init_persistency = time.time() + 60
self.read_value() # check for persistent field mismatch
elif self.read_switch_heater() and self.mode != Mode.DRIVEN:
if time.time() > self.__init_persistency:
# switch off heater from previous live or manual intervention
self.log.info('fix mode after startup')
self.write_mode(self.mode)
else: else:
self.__init_persistency = False
super().doPoll() super().doPoll()
def initModule(self): def initModule(self):
@ -178,33 +181,49 @@ class Magfield(SimpleMagfield):
self.registerCallbacks(self) # for update_switch_heater self.registerCallbacks(self) # for update_switch_heater
def write_mode(self, value): def write_mode(self, value):
self.start_machine(self.start_field_change, cleanup=self.cleanup, target=self.target, mode=value) self.__init_persistency = False
target = self.persistent_field
func = self.start_field_change
if value == Mode.DISABLED:
target = 0
if abs(self.persistent_field) < self.tolerance:
func = self.start_switch_off
elif value == Mode.PERSISTENT:
func = self.start_switch_off
self.start_machine(func, target=target, mode=value)
return value return value
def write_target(self, target): def write_target(self, target):
self.__init_persistency = False
if self.mode == Mode.DISABLED:
if target == 0:
return 0
self.log.info('raise error %r', target)
raise BadValueError('disabled')
self.check_limits(target) self.check_limits(target)
self.start_machine(self.start_field_change, cleanup=self.cleanup, target=target, mode=self.mode) self.start_machine(self.start_field_change, target=target, mode=self.mode)
return target return target
def cleanup(self, sm): # sm is short for statemachine def on_error(self, sm): # sm is short for statemachine
if self.switch_heater != 0: if self.switch_heater == ON:
self.persistent_field = self.read_value() self.persistent_field = self.read_value()
if sm.mode != Mode.DRIVEN: if sm.mode != Mode.DRIVEN:
self.log.warning('turn switch heater off') self.log.warning('turn switch heater off')
self.write_switch_heater(0) self.write_switch_heater(OFF)
return self.on_error(sm)
@status_code('PREPARING') @status_code(Status.PREPARING)
def start_field_change(self, sm): def start_field_change(self, sm):
self.setFastPoll(True, 1.0) self.setFastPoll(True, 1.0)
if sm.target == self.persistent_field or ( if sm.target == self.persistent_field or (
sm.target == self._last_target and sm.target == self._last_target and
abs(sm.target - self.persistent_field) <= self.tolerance): # short cut abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off return self.check_switch_off
if self.switch_heater: if self.switch_heater == ON:
return self.start_switch_on return self.start_switch_on
return self.start_ramp_to_field return self.start_ramp_to_field
@status_code('PREPARING') @status_code(Status.PREPARING)
def start_ramp_to_field(self, sm): def start_ramp_to_field(self, sm):
"""start ramping current to persistent field """start ramping current to persistent field
@ -213,7 +232,7 @@ class Magfield(SimpleMagfield):
""" """
raise NotImplementedError raise NotImplementedError
@status_code('PREPARING', 'ramp leads to match field') @status_code(Status.PREPARING, 'ramp leads to match field')
def ramp_to_field(self, sm): def ramp_to_field(self, sm):
if sm.init: if sm.init:
sm.stabilize_start = 0 # in case current is already at field sm.stabilize_start = 0 # in case current is already at field
@ -229,7 +248,7 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now sm.stabilize_start = sm.now
return self.stabilize_current return self.stabilize_current
@status_code('PREPARING') @status_code(Status.PREPARING)
def stabilize_current(self, sm): def stabilize_current(self, sm):
if sm.now - sm.stabilize_start < self.wait_stable_leads: if sm.now - sm.stabilize_start < self.wait_stable_leads:
return Retry return Retry
@ -237,7 +256,6 @@ class Magfield(SimpleMagfield):
def update_switch_heater(self, value): def update_switch_heater(self, value):
"""is called whenever switch heater was changed""" """is called whenever switch heater was changed"""
print('SW', value)
if value == 0: if value == 0:
if self.switch_off_time is None: if self.switch_off_time is None:
self.log.info('restart switch_off_time') self.log.info('restart switch_off_time')
@ -249,12 +267,12 @@ class Magfield(SimpleMagfield):
self.switch_on_time = time.time() self.switch_on_time = time.time()
self.switch_off_time = None self.switch_off_time = None
@status_code('PREPARING') @status_code(Status.PREPARING)
def start_switch_on(self, sm): def start_switch_on(self, sm):
if self.read_switch_heater() == 0: if self.read_switch_heater() == OFF:
self.status = Status.PREPARING, 'turn switch heater on' self.status = Status.PREPARING, 'turn switch heater on'
try: try:
self.write_switch_heater(True) self.write_switch_heater(ON)
except Exception as e: except Exception as e:
self.log.warning('write_switch_heater %r', e) self.log.warning('write_switch_heater %r', e)
return Retry return Retry
@ -262,13 +280,15 @@ class Magfield(SimpleMagfield):
self.status = Status.PREPARING, 'wait for heater on' self.status = Status.PREPARING, 'wait for heater on'
return self.wait_for_switch_on return self.wait_for_switch_on
@status_code('PREPARING') @status_code(Status.PREPARING)
def wait_for_switch_on(self, sm): def wait_for_switch_on(self, sm):
if (sm.target == self._last_target and if (sm.target == self._last_target and
abs(sm.target - self.persistent_field) <= self.tolerance): # short cut abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off return self.check_switch_off
self.read_switch_heater() # trigger switch_on/off_time self.read_switch_heater() # trigger switch_on/off_time
if self.switch_heater == 0: if self.switch_heater == OFF:
if sm.init: # avoid too many states chained
return Retry
self.log.warning('switch turned off manually?') self.log.warning('switch turned off manually?')
return self.start_switch_on return self.start_switch_on
if sm.now - self.switch_on_time < self.wait_switch_on: if sm.now - self.switch_on_time < self.wait_switch_on:
@ -278,7 +298,7 @@ class Magfield(SimpleMagfield):
self._last_target = sm.target self._last_target = sm.target
return self.start_ramp_to_target return self.start_ramp_to_target
@status_code('RAMPING') @status_code(Status.RAMPING)
def start_ramp_to_target(self, sm): def start_ramp_to_target(self, sm):
"""start ramping current to target field """start ramping current to target field
@ -287,7 +307,7 @@ class Magfield(SimpleMagfield):
""" """
raise NotImplementedError raise NotImplementedError
@status_code('RAMPING') @status_code(Status.RAMPING)
def ramp_to_target(self, sm): def ramp_to_target(self, sm):
self.persistent_field = self.value self.persistent_field = self.value
dif = abs(self.value - sm.target) dif = abs(self.value - sm.target)
@ -298,6 +318,7 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now sm.stabilize_start = sm.now
tdif = self.get_progress(sm, self.value) tdif = self.get_progress(sm, self.value)
if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo: if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo:
self.log.warn('no progress')
raise HardwareError('no progress') raise HardwareError('no progress')
sm.stabilize_start = None sm.stabilize_start = None
return Retry return Retry
@ -305,10 +326,10 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now sm.stabilize_start = sm.now
return self.stabilize_field return self.stabilize_field
@status_code('STABILIZING') @status_code(Status.STABILIZING)
def stabilize_field(self, sm): def stabilize_field(self, sm):
self.persistent_field = self.value self.persistent_field = self.value
if sm.now > sm.stablize_start + self.wait_stable_field: if sm.now < sm.stabilize_start + self.wait_stable_field:
return Retry return Retry
return self.check_switch_off return self.check_switch_off
@ -317,17 +338,19 @@ class Magfield(SimpleMagfield):
return self.final_status(Status.PREPARED, 'driven') return self.final_status(Status.PREPARED, 'driven')
return self.start_switch_off return self.start_switch_off
@status_code('FINALIZING') @status_code(Status.FINALIZING)
def start_switch_off(self, sm): def start_switch_off(self, sm):
if self.switch_heater == 1: if self.switch_heater == ON:
self.write_switch_heater(False) self.write_switch_heater(OFF)
return self.wait_for_switch_off return self.wait_for_switch_off
@status_code('FINALIZING') @status_code(Status.FINALIZING)
def wait_for_switch_off(self, sm): def wait_for_switch_off(self, sm):
self.persistent_field = self.value self.persistent_field = self.value
self.read_switch_heater() self.read_switch_heater()
if self.switch_off_time is None: if self.switch_heater == ON:
if sm.init: # avoid too many states chained
return Retry
self.log.warning('switch turned on manually?') self.log.warning('switch turned on manually?')
return self.start_switch_off return self.start_switch_off
if sm.now - self.switch_off_time < self.wait_switch_off: if sm.now - self.switch_off_time < self.wait_switch_off:
@ -336,7 +359,7 @@ class Magfield(SimpleMagfield):
return self.final_status(Status.IDLE, 'leads current at field, switch off') return self.final_status(Status.IDLE, 'leads current at field, switch off')
return self.start_ramp_to_zero return self.start_ramp_to_zero
@status_code('FINALIZING') @status_code(Status.FINALIZING)
def start_ramp_to_zero(self, sm): def start_ramp_to_zero(self, sm):
"""start ramping current to zero """start ramping current to zero
@ -345,15 +368,15 @@ class Magfield(SimpleMagfield):
""" """
raise NotImplementedError raise NotImplementedError
@status_code('FINALIZING') @status_code(Status.FINALIZING)
def ramp_to_zero(self, sm): def ramp_to_zero(self, sm):
"""ramp field to zero""" """ramp field to zero"""
if sm.init: if sm.init:
self.init_progress(sm, self.current) self.init_progress(sm, self.current)
if abs(self.current) > self.tolerance: if abs(self.current) > self.tolerance:
if self.get_progress(sm, self.current, self.ramp) > self.leads_ramp_tmo: if self.get_progress(sm, self.value) > self.leads_ramp_tmo:
raise HardwareError('no progress') raise HardwareError('no progress')
return Retry return Retry
if sm.mode == Mode.DISABLED and self.persistent_field == 0: if sm.mode == Mode.DISABLED and abs(self.persistent_field) < self.tolerance:
return self.final_status(Status.DISABLED, 'disabled') return self.final_status(Status.DISABLED, 'disabled')
return self.final_status(Status.IDLE, 'persistent mode') return self.final_status(Status.IDLE, 'persistent mode')

View File

@ -123,13 +123,14 @@ class MercuryChannel(HasIO):
else: else:
raise HardwareError(msg) from None raise HardwareError(msg) from None
def multichange(self, adr, values, convert=as_float, tolerance=0): def multichange(self, adr, values, convert=as_float, tolerance=0, n_retry=3):
"""set parameter(s) in mercury syntax """set parameter(s) in mercury syntax
:param adr: as in see multiquery method :param adr: as in see multiquery method
:param values: [(name1, value1), (name2, value2) ...] :param values: [(name1, value1), (name2, value2) ...]
:param convert: a converter function (converts given value to string and replied string to value) :param convert: a converter function (converts given value to string and replied string to value)
:param tolerance: tolerance for readback check :param tolerance: tolerance for readback check
:param n_retry: number of retries or 0 for no readback check
:return: the values as tuple :return: the values as tuple
Example: Example:
@ -143,7 +144,7 @@ class MercuryChannel(HasIO):
adr = self._complete_adr(adr) adr = self._complete_adr(adr)
params = ['%s:%s' % (k, convert(v)) for k, v in values] params = ['%s:%s' % (k, convert(v)) for k, v in values]
cmd = 'SET:%s:%s' % (adr, ':'.join(params)) cmd = 'SET:%s:%s' % (adr, ':'.join(params))
for _ in range(3): # try 3 times or until readback result matches for _ in range(max(1, n_retry)): # try n_retry times or until readback result matches
t = time.time() t = time.time()
reply = self.communicate(cmd) reply = self.communicate(cmd)
head = 'STAT:SET:%s:' % adr head = 'STAT:SET:%s:' % adr
@ -158,6 +159,8 @@ class MercuryChannel(HasIO):
except (AssertionError, AttributeError, ValueError) as e: except (AssertionError, AttributeError, ValueError) as e:
time.sleep(0.1) # in case of missed replies this might help to skip garbage time.sleep(0.1) # in case of missed replies this might help to skip garbage
raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e
if n_retry == 0:
return [v[1] for v in values] # no readback check
keys = [v[0] for v in values] keys = [v[0] for v in values]
debug = [] debug = []
readback = self.multiquery(adr, keys, convert, debug) readback = self.multiquery(adr, keys, convert, debug)
@ -182,9 +185,9 @@ class MercuryChannel(HasIO):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multiquery(adr, [name], convert)[0] return self.multiquery(adr, [name], convert)[0]
def change(self, adr, value, convert=as_float, tolerance=0): def change(self, adr, value, convert=as_float, tolerance=0, n_retry=3):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multichange(adr, [(name, value)], convert, tolerance)[0] return self.multichange(adr, [(name, value)], convert, tolerance, n_retry)[0]
class TemperatureSensor(MercuryChannel, Readable): class TemperatureSensor(MercuryChannel, Readable):