state on dilsc as of 2022-10-03
vector field, but no new state machine yet
This commit is contained in:
@@ -39,7 +39,7 @@ class HasOffset(Feature):
|
||||
implementation to be done in the subclass
|
||||
"""
|
||||
offset = PersistentParam('offset (physical value + offset = HW value)',
|
||||
FloatRange(unit='deg'), readonly=False, default=0)
|
||||
FloatRange(unit='$'), readonly=False, default=0)
|
||||
|
||||
def write_offset(self, value):
|
||||
self.offset = value
|
||||
@@ -62,9 +62,9 @@ class HasLimits(Feature):
|
||||
except for the offset
|
||||
"""
|
||||
abslimits = Property('abs limits (raw values)', default=(-9e99, 9e99), extname='abslimits', export=True,
|
||||
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')))
|
||||
limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99),
|
||||
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')))
|
||||
_limits = None
|
||||
|
||||
def apply_offset(self, sign, *values):
|
||||
|
||||
@@ -30,7 +30,7 @@ The created state object may hold variables needed for the state.
|
||||
A state function may return either:
|
||||
- a function for the next state to transition to
|
||||
- Retry(<delay>) to keep the state and call the
|
||||
- or `None` for finishing
|
||||
- or `Finish` for finishing
|
||||
|
||||
|
||||
Initialisation Code
|
||||
@@ -44,33 +44,23 @@ def state_x(stateobj):
|
||||
... further code ...
|
||||
|
||||
|
||||
Cleanup Function
|
||||
----------------
|
||||
Error Handler
|
||||
-------------
|
||||
|
||||
cleanup=<cleanup function> as argument in StateMachine.__init__ or .start
|
||||
defines a cleanup function to be called whenever the machine is stopped or
|
||||
an error is raised in a state function. A cleanup function may return
|
||||
either None for finishing or a further state function for continuing.
|
||||
In case of stop or restart, this return value is ignored.
|
||||
|
||||
|
||||
State Specific Cleanup Code
|
||||
---------------------------
|
||||
|
||||
To execute state specific cleanup, the cleanup may examine the current state
|
||||
(stateobj.state) in order to decide what to be done.
|
||||
|
||||
If a need arises, a future extension to this library may support specific
|
||||
cleanup functions by means of a decorator adding the specific cleanup function
|
||||
as an attribute to the state function.
|
||||
handler=<handler object> as argument in StateMachine.__init__ or .start
|
||||
defines a handler object to be called whenever the machine is stopped,
|
||||
restarted. finished or an error is raised in a state function.
|
||||
<handler>.on_error may return either None for finishing or a further state
|
||||
function for continuing. The other handler methods always return None,
|
||||
as there is no useful follow up state.
|
||||
|
||||
|
||||
Threaded Use
|
||||
------------
|
||||
|
||||
On start, a thread is started, which is waiting for a trigger event when the
|
||||
machine is not active. For test purposes or special needs, the thread creation
|
||||
may be disabled. :meth:`cycle` must be called periodically in this case.
|
||||
machine is not active. In case the thread creation is disabled. :meth:`cycle`
|
||||
must be called periodically for running the state machine.
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -80,6 +70,7 @@ from logging import getLogger
|
||||
from secop.lib import mkthread, UniqueObject
|
||||
|
||||
|
||||
Finish = UniqueObject('Finish')
|
||||
Stop = UniqueObject('Stop')
|
||||
Restart = UniqueObject('Restart')
|
||||
|
||||
@@ -89,6 +80,52 @@ class Retry:
|
||||
self.delay = delay
|
||||
|
||||
|
||||
class StateHandler:
|
||||
"""default handlers
|
||||
|
||||
may be used as base class or mixin for implementing custom handlers
|
||||
"""
|
||||
def on_error(self, statemachine, exc):
|
||||
"""called on error
|
||||
|
||||
:param statemachine: the state machine object
|
||||
:param exc: the exception
|
||||
:return: None or a state function to be executed for handling the error state
|
||||
"""
|
||||
statemachine.log.warning('%r raised in state %r', exc, statemachine.status_string)
|
||||
|
||||
def on_transition(self, statemachine, newstate):
|
||||
"""called when state is changed
|
||||
|
||||
:param statemachine: the statemachine
|
||||
:param newstate: the new state function
|
||||
|
||||
this method will not be called when the state is changed to None,
|
||||
e.g. on finish, restart, stop or when None is returned from the error handler
|
||||
"""
|
||||
|
||||
def on_restart(self, statemachine):
|
||||
"""called on restart
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
def on_stop(self, statemachine):
|
||||
"""called when stopped
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
def on_finish(self, statemachine):
|
||||
"""called on finish
|
||||
|
||||
:param statemachine: the state machine object
|
||||
"""
|
||||
|
||||
|
||||
default_handler = StateHandler()
|
||||
|
||||
|
||||
class StateMachine:
|
||||
"""a simple, but powerful state machine"""
|
||||
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
|
||||
@@ -98,7 +135,7 @@ class StateMachine:
|
||||
now = None
|
||||
init = True
|
||||
stopped = False
|
||||
last_error = None # last exception raised or Stop or Restart
|
||||
restarted = False
|
||||
_last_time = 0
|
||||
|
||||
def __init__(self, state=None, logger=None, threaded=True, **kwds):
|
||||
@@ -109,9 +146,9 @@ class StateMachine:
|
||||
:param threaded: whether a thread should be started (default: True)
|
||||
:param kwds: any attributes for the state object
|
||||
"""
|
||||
self.default_delay = 0.25 # default delay when returning None
|
||||
self.default_delay = 0.25 # default delay when returning Retry(None)
|
||||
self.now = time.time() # avoid calling time.time several times per state
|
||||
self.cleanup = self.default_cleanup # default cleanup: finish on error
|
||||
self.handler = default_handler
|
||||
self.log = logger or getLogger('dummy')
|
||||
self._update_attributes(kwds)
|
||||
self._lock = threading.RLock()
|
||||
@@ -120,23 +157,9 @@ class StateMachine:
|
||||
self._thread_queue = queue.Queue()
|
||||
self._idle_event = threading.Event()
|
||||
self._thread = None
|
||||
self._restart = None
|
||||
if state:
|
||||
self.start(state)
|
||||
|
||||
@staticmethod
|
||||
def default_cleanup(state):
|
||||
"""default cleanup
|
||||
|
||||
:param self: the state object
|
||||
:return: None (for custom cleanup functions this might be a new state)
|
||||
"""
|
||||
if state.stopped: # stop or restart
|
||||
verb = 'stopped' if state.stopped is Stop else 'restarted'
|
||||
state.log.debug('%s in state %r', verb, state.status_string)
|
||||
else:
|
||||
state.log.warning('%r raised in state %r', state.last_error, state.status_string)
|
||||
|
||||
def _update_attributes(self, kwds):
|
||||
"""update allowed attributes"""
|
||||
cls = type(self)
|
||||
@@ -168,10 +191,12 @@ class StateMachine:
|
||||
|
||||
def _new_state(self, state):
|
||||
self.state = state
|
||||
self.init = True
|
||||
self.now = time.time()
|
||||
self.transition_time = self.now
|
||||
self.log.debug('state: %s', self.status_string)
|
||||
if state:
|
||||
self.handler.on_transition(self, state)
|
||||
self.init = True
|
||||
self.now = time.time()
|
||||
self.transition_time = self.now
|
||||
|
||||
def cycle(self):
|
||||
"""do one cycle in the thread loop
|
||||
@@ -187,18 +212,26 @@ class StateMachine:
|
||||
ret = self.state(self)
|
||||
self.init = False
|
||||
if self.stopped:
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self)
|
||||
self.log.debug('stopped in state %r', self.status_string)
|
||||
self.handler.on_stop(self)
|
||||
self.stopped = False
|
||||
ret = None
|
||||
elif self.restarted:
|
||||
self.log.debug('restarted in state %r', self.status_string)
|
||||
self.handler.on_restart(self)
|
||||
self.restarted = False
|
||||
ret = None
|
||||
except Exception as e:
|
||||
self.last_error = e
|
||||
ret = self.cleanup(self)
|
||||
self.log.debug('called %r %sexc=%r', self.cleanup,
|
||||
'ret=%r ' % ret if ret else '', e)
|
||||
if ret is None:
|
||||
self.log.debug('state: None after cleanup')
|
||||
self.state = None
|
||||
try:
|
||||
ret = self.handler.on_error(self, e)
|
||||
self.log.debug('called on_error with exc=%r%s', e,
|
||||
' ret=%r' % ret if ret else '')
|
||||
except Exception as ee:
|
||||
self.log.exception('%r raised in on_error(state, %r)', ee, e)
|
||||
if ret is Finish:
|
||||
self.log.debug('finish in state %r', self.status_string)
|
||||
self.handler.on_finish(self)
|
||||
self._new_state(None)
|
||||
self._idle_event.set()
|
||||
return None
|
||||
if callable(ret):
|
||||
@@ -210,12 +243,13 @@ class StateMachine:
|
||||
if ret.delay is None:
|
||||
return self.default_delay
|
||||
return ret.delay
|
||||
self.last_error = RuntimeError('return value must be callable, Retry(...) or finish')
|
||||
self.handler.on_error(self, RuntimeError(
|
||||
'return value must be callable, Retry(...) or Finish, not %r' % ret))
|
||||
break
|
||||
else:
|
||||
self.last_error = RuntimeError('too many states chained - probably infinite loop')
|
||||
self.cleanup(self)
|
||||
self.state = None
|
||||
self.handler.on_error(self, RuntimeError(
|
||||
'too many states chained - probably infinite loop'))
|
||||
self._new_state(None)
|
||||
return None
|
||||
|
||||
def trigger(self, delay=0):
|
||||
@@ -238,9 +272,8 @@ class StateMachine:
|
||||
delay = self.cycle()
|
||||
|
||||
def _start(self, state, **kwds):
|
||||
self._restart = None
|
||||
self._idle_event.clear()
|
||||
self.last_error = None
|
||||
self.restarted = False
|
||||
self.stopped = False
|
||||
self._update_attributes(kwds)
|
||||
self._new_state(state)
|
||||
@@ -259,19 +292,18 @@ class StateMachine:
|
||||
"""start with a new state
|
||||
|
||||
and interrupt the current state
|
||||
the cleanup function will be called with state.stopped=Restart
|
||||
the cleanup function will be called with state.restarted = True
|
||||
|
||||
:param state: the first state
|
||||
:param kwds: items to put as attributes on the state machine
|
||||
"""
|
||||
self.log.debug('start %r', kwds)
|
||||
if self.state:
|
||||
self.stopped = Restart
|
||||
self.restarted = True
|
||||
with self._lock: # wait for running cycle finished
|
||||
if self.stopped: # cleanup is not yet done
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self) # ignore return state on restart
|
||||
self.stopped = False
|
||||
if self.restarted: # on_restart is not yet called
|
||||
self.handler.on_restart(self)
|
||||
self.restarted = False
|
||||
self._start(state, **kwds)
|
||||
else:
|
||||
self._start(state, **kwds)
|
||||
@@ -279,16 +311,15 @@ class StateMachine:
|
||||
def stop(self):
|
||||
"""stop machine, go to idle state
|
||||
|
||||
the cleanup function will be called with state.stopped=Stop
|
||||
the cleanup function will be called with state.stopped = True
|
||||
"""
|
||||
self.log.debug('stop')
|
||||
self.stopped = Stop
|
||||
self.stopped = True
|
||||
with self._lock:
|
||||
if self.stopped: # cleanup is not yet done
|
||||
self.last_error = self.stopped
|
||||
self.cleanup(self) # ignore return state on restart
|
||||
if self.stopped: # on_stop is not yet called
|
||||
self.handler.on_stop(self)
|
||||
self.stopped = False
|
||||
self.state = None
|
||||
self._new_state(None)
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""wait for state machine being idle"""
|
||||
|
||||
110
secop/states.py
Normal file
110
secop/states.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""mixin for modules with a statemachine"""
|
||||
|
||||
from secop.lib.statemachine import StateMachine, Finish, Retry, StateHandler
|
||||
from secop.core import BUSY, IDLE, ERROR, Parameter
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
class status_code:
|
||||
"""decorator for state methods"""
|
||||
def __init__(self, code, text=None):
|
||||
self.code = code
|
||||
self.text = text
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
if not issubclass(owner, HasStates):
|
||||
raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__)
|
||||
self.cls = owner
|
||||
self.name = name
|
||||
if 'statusMap' not in owner.__dict__:
|
||||
# we need a copy on each inheritance level
|
||||
owner.statusMap = dict(owner.statusMap)
|
||||
owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text
|
||||
setattr(owner, name, self.func)
|
||||
|
||||
def __call__(self, func):
|
||||
self.func = func
|
||||
return self
|
||||
|
||||
|
||||
class HasStates(StateHandler):
|
||||
status = Parameter() # make sure this is a parameter
|
||||
skip_consecutive_status_changes = False
|
||||
_state_machine = None
|
||||
_next_cycle = 0
|
||||
statusMap = {}
|
||||
|
||||
def init_state_machine(self, fullstatus=True, **kwds):
|
||||
self._state_machine = StateMachine(
|
||||
logger=self.log,
|
||||
threaded=False,
|
||||
handler=self,
|
||||
default_delay=1e-3, # small but not zero (avoid infinite loop)
|
||||
**kwds)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.init_state_machine()
|
||||
|
||||
def on_error(self, statemachine, exc):
|
||||
"""called on error"""
|
||||
error = '%r in %s' % (exc, statemachine.status_string)
|
||||
self.log.error('%s', error)
|
||||
return self.final_status(ERROR, error)
|
||||
|
||||
def on_transition(self, statemachine, newstate):
|
||||
if not self.skip_consecutive_status_changes:
|
||||
self.set_status_from_state(newstate)
|
||||
|
||||
def set_status_from_state(self, newstate):
|
||||
name = newstate.__name__
|
||||
status = self.statusMap.get(name)
|
||||
if status is None:
|
||||
status = BUSY, name.replace('_', ' ')
|
||||
if status != self.status:
|
||||
self.status = status
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
now = self.pollInfo.last_main
|
||||
if now > self._next_cycle:
|
||||
delay = self._state_machine.cycle()
|
||||
if delay is None:
|
||||
self._next_cycle = 0
|
||||
else:
|
||||
if self.skip_consecutive_status_changes:
|
||||
self.set_status_from_state(self._state_machine.state)
|
||||
self._next_cycle = now + delay
|
||||
|
||||
def start_state(self, start_state, fast_poll=True, **kwds):
|
||||
self._state_machine.start(start_state, **kwds)
|
||||
if fast_poll is not None:
|
||||
self.setFastPoll(fast_poll)
|
||||
|
||||
def final_status(self, code=IDLE, text='', fast_poll=False):
|
||||
self.status = code, text
|
||||
if fast_poll is not None:
|
||||
self.setFastPoll(fast_poll)
|
||||
return Finish
|
||||
Reference in New Issue
Block a user