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:
zolliker 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.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):
sm = self._state_machine
sm.status = self.get_status(statefunc, BUSY)
if sm.statefunc:
sm.status = sm.status[0], 'restarting'
sm.start(statefunc, **kwds)
sm.start(statefunc, cleanup=kwds.pop('cleanup', self.on_cleanup), **kwds)
self.read_status()
if fast_poll:
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_psi.magfield import Magfield, SimpleMagfield, Status
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)
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)
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):
return self.query('PSU:SIG:FLD')
@ -94,12 +101,12 @@ class SimpleField(MercuryChannel, SimpleMagfield):
def set_and_go(self, value):
self.setpoint = self.change('PSU:SIG:FSET', value)
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_set') == 'run_to_set'
assert self.write_action(Action.hold) == Action.hold
assert self.write_action(Action.run_to_set) == Action.run_to_set
def start_ramp_to_target(self, sm):
# if self.action != 'hold':
# assert self.write_action('hold') == 'hold'
# if self.action != Action.hold:
# assert self.write_action(Action.hold) == Action.hold
# return Retry
self.set_and_go(sm.target)
sm.try_cnt = 5
@ -116,13 +123,11 @@ class SimpleField(MercuryChannel, SimpleMagfield):
return Retry
def final_status(self, *args, **kwds):
print('FINAL-hold')
self.write_action('hold')
self.write_action(Action.hold)
return super().final_status(*args, **kwds)
def on_restart(self, sm):
print('ON_RESTART-hold', sm.sm)
self.write_action('hold')
self.write_action(Action.hold)
return super().on_restart(sm)
@ -136,7 +141,7 @@ class Field(SimpleField, Magfield):
_field_mismatch = None
__init = True
__switch_heater_fix = 0
__switch_fixed_until = 0
def doPoll(self):
super().doPoll()
@ -208,7 +213,8 @@ class Field(SimpleField, Magfield):
value = self.query('PSU:SIG:SWHT', off_on)
now = time.time()
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
if self.switch_heater:
self.switch_on_time = time.time()
@ -228,8 +234,10 @@ class Field(SimpleField, Magfield):
self.log.info('switch heater already %r', value)
# we do not want to restart the timer
return value
self.__switch_heater_fix = time.time() + 10
return self.change('PSU:SIG:SWHT', value, off_on)
self.__switch_fixed_until = time.time() + 10
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):
if abs(self.current - self.persistent_field) <= self.tolerance:
@ -241,9 +249,7 @@ class Field(SimpleField, Magfield):
if self.switch_heater:
self.log.warn('switch is already on!')
return self.ramp_to_field
self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field)
return Retry
self.status = Status.PREPARING, 'wait for switch off'
self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.persistent_field, e)
sm.after_wait = self.ramp_to_field
return self.wait_for_switch
return self.ramp_to_field
@ -270,7 +276,7 @@ class Field(SimpleField, Magfield):
return Retry
def wait_for_switch(self, sm):
if not self.delay(10):
if not sm.delta(10):
return Retry
try:
self.log.warn('try again')
@ -282,8 +288,8 @@ class Field(SimpleField, Magfield):
def start_ramp_to_zero(self, sm):
try:
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_zero') == 'run_to_zero'
assert self.write_action(Action.hold) == Action.hold
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
except (HardwareError, AssertionError) as e:
self.log.warn('switch not yet ready %r', e)
self.status = Status.PREPARING, 'wait for switch off'
@ -298,6 +304,6 @@ class Field(SimpleField, Magfield):
sm.try_cnt -= 1
if sm.try_cnt < 0:
raise
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_zero') == 'run_to_zero'
assert self.write_action(Action.hold) == Action.hold
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
return Retry

View File

@ -23,9 +23,9 @@ import time
from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR
from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
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.states import Retry, HasStates, status_code
from secop.states import Retry, HasStates, status_code, Start
UNLIMITED = FloatRange()
@ -157,20 +157,23 @@ class Magfield(SimpleMagfield):
ramp_tmo = Parameter(
'timeout for field ramp progress',
FloatRange(0, unit='s'), readonly=False, default=30)
__init = True
__init_persistency = True
switch_on_time = None
switch_off_time = None
def doPoll(self):
if self.__init:
self.__init = False
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:
if self.__init_persistency:
if self.__init_persistency is True:
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:
self.__init_persistency = False
super().doPoll()
def initModule(self):
@ -178,33 +181,49 @@ class Magfield(SimpleMagfield):
self.registerCallbacks(self) # for update_switch_heater
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
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.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
def cleanup(self, sm): # sm is short for statemachine
if self.switch_heater != 0:
def on_error(self, sm): # sm is short for statemachine
if self.switch_heater == ON:
self.persistent_field = self.read_value()
if sm.mode != Mode.DRIVEN:
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):
self.setFastPoll(True, 1.0)
if sm.target == self.persistent_field or (
sm.target == self._last_target and
abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off
if self.switch_heater:
if self.switch_heater == ON:
return self.start_switch_on
return self.start_ramp_to_field
@status_code('PREPARING')
@status_code(Status.PREPARING)
def start_ramp_to_field(self, sm):
"""start ramping current to persistent field
@ -213,7 +232,7 @@ class Magfield(SimpleMagfield):
"""
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):
if sm.init:
sm.stabilize_start = 0 # in case current is already at field
@ -229,7 +248,7 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now
return self.stabilize_current
@status_code('PREPARING')
@status_code(Status.PREPARING)
def stabilize_current(self, sm):
if sm.now - sm.stabilize_start < self.wait_stable_leads:
return Retry
@ -237,7 +256,6 @@ class Magfield(SimpleMagfield):
def update_switch_heater(self, value):
"""is called whenever switch heater was changed"""
print('SW', value)
if value == 0:
if self.switch_off_time is None:
self.log.info('restart switch_off_time')
@ -249,12 +267,12 @@ class Magfield(SimpleMagfield):
self.switch_on_time = time.time()
self.switch_off_time = None
@status_code('PREPARING')
@status_code(Status.PREPARING)
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'
try:
self.write_switch_heater(True)
self.write_switch_heater(ON)
except Exception as e:
self.log.warning('write_switch_heater %r', e)
return Retry
@ -262,13 +280,15 @@ class Magfield(SimpleMagfield):
self.status = Status.PREPARING, 'wait for heater on'
return self.wait_for_switch_on
@status_code('PREPARING')
@status_code(Status.PREPARING)
def wait_for_switch_on(self, sm):
if (sm.target == self._last_target and
abs(sm.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off
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?')
return self.start_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
return self.start_ramp_to_target
@status_code('RAMPING')
@status_code(Status.RAMPING)
def start_ramp_to_target(self, sm):
"""start ramping current to target field
@ -287,7 +307,7 @@ class Magfield(SimpleMagfield):
"""
raise NotImplementedError
@status_code('RAMPING')
@status_code(Status.RAMPING)
def ramp_to_target(self, sm):
self.persistent_field = self.value
dif = abs(self.value - sm.target)
@ -298,6 +318,7 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now
tdif = self.get_progress(sm, self.value)
if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo:
self.log.warn('no progress')
raise HardwareError('no progress')
sm.stabilize_start = None
return Retry
@ -305,10 +326,10 @@ class Magfield(SimpleMagfield):
sm.stabilize_start = sm.now
return self.stabilize_field
@status_code('STABILIZING')
@status_code(Status.STABILIZING)
def stabilize_field(self, sm):
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 self.check_switch_off
@ -317,17 +338,19 @@ class Magfield(SimpleMagfield):
return self.final_status(Status.PREPARED, 'driven')
return self.start_switch_off
@status_code('FINALIZING')
@status_code(Status.FINALIZING)
def start_switch_off(self, sm):
if self.switch_heater == 1:
self.write_switch_heater(False)
if self.switch_heater == ON:
self.write_switch_heater(OFF)
return self.wait_for_switch_off
@status_code('FINALIZING')
@status_code(Status.FINALIZING)
def wait_for_switch_off(self, sm):
self.persistent_field = self.value
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?')
return self.start_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.start_ramp_to_zero
@status_code('FINALIZING')
@status_code(Status.FINALIZING)
def start_ramp_to_zero(self, sm):
"""start ramping current to zero
@ -345,15 +368,15 @@ class Magfield(SimpleMagfield):
"""
raise NotImplementedError
@status_code('FINALIZING')
@status_code(Status.FINALIZING)
def ramp_to_zero(self, sm):
"""ramp field to zero"""
if sm.init:
self.init_progress(sm, self.current)
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')
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.IDLE, 'persistent mode')

View File

@ -123,13 +123,14 @@ class MercuryChannel(HasIO):
else:
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
:param adr: as in see multiquery method
:param values: [(name1, value1), (name2, value2) ...]
:param convert: a converter function (converts given value to string and replied string to value)
:param tolerance: tolerance for readback check
:param n_retry: number of retries or 0 for no readback check
:return: the values as tuple
Example:
@ -143,7 +144,7 @@ class MercuryChannel(HasIO):
adr = self._complete_adr(adr)
params = ['%s:%s' % (k, convert(v)) for k, v in values]
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()
reply = self.communicate(cmd)
head = 'STAT:SET:%s:' % adr
@ -158,6 +159,8 @@ class MercuryChannel(HasIO):
except (AssertionError, AttributeError, ValueError) as e:
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
if n_retry == 0:
return [v[1] for v in values] # no readback check
keys = [v[0] for v in values]
debug = []
readback = self.multiquery(adr, keys, convert, debug)
@ -182,9 +185,9 @@ class MercuryChannel(HasIO):
adr, _, name = adr.rpartition(':')
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(':')
return self.multichange(adr, [(name, value)], convert, tolerance)[0]
return self.multichange(adr, [(name, value)], convert, tolerance, n_retry)[0]
class TemperatureSensor(MercuryChannel, Readable):