frappy/secop/lib/sequence.py
Markus Zolliker b423235c5d new poll mechanism
- remove secop.poller and basic poller
- regular polls for 'important' parameters done by method doPoll
- all other parameters are polled slower (slowInterval) and
  with lower priority (only one at a time when main poll is due)
- nopoll decorator for read_* to disable poll
- enablePoll attribute (default True) for disabling polling a module
- fast polls may be implemented by means of a statemachine
- configurable slow poll interval
+ allow a Parameter to override a Property (parameter
  Readable.pollinterval overrides Module.pollinterval)

Change-Id: Ib1b3453041a233678b7c4b4add22ac399670e447
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27832
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2022-03-04 09:58:15 +01:00

193 lines
6.9 KiB
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:
# Georg Brandl <g.brandl@fz-juelich.de>
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Utilities for modules that require sequenced actions on value change."""
from time import sleep
from secop.errors import IsBusyError
from secop.lib import mkthread
class Namespace:
pass
class Step:
def __init__(self, desc, waittime, func, *args, **kwds):
self.desc = desc
self.waittime = waittime
self.func = func
self.args = args
self.kwds = kwds
class SequencerMixin:
"""Mixin for worker classes that need to execute a sequence of actions,
including waits, that exceeds the usual Tango timeout (about 3 seconds)
and should be executed asynchronously.
.. automethod:: init_sequencer
.. automethod:: start_sequence
.. automethod:: seq_is_alive
.. method:: _ext_state()
Implement this to return a custom state tuple when the sequence is
not active.
"""
def init_sequencer(self, fault_on_error=True, fault_on_stop=False):
"""Initialize the sequencer. Must be called in the worker's init().
*fault_on_error* and *fault_on_stop* control the behavior when
exceptions are raised, or stop is activated, during the sequence, see
below.
"""
# thread variable init
self._seq_thread = None
self._seq_fault_on_error = fault_on_error
self._seq_fault_on_stop = fault_on_stop
self._seq_stopflag = False
self._seq_phase = ''
self._seq_error = None
self._seq_stopped = None
def start_sequence(self, seq, **store_init):
"""Start the sequence, given the list of steps.
Each step should be a ``Step`` instance:
Step('phase description', waittime, callable)
where the callable should take one argument and execute an atomic step
of the sequence. The description is added to the status string while
the step is active. The waittime is a sleep after the step completes.
As long as the callable returns a true value, the step is repeated.
The argument to the step callable is a featureless "store" object on
which data can be transferred between steps. This is provided so that
steps don't save temporary variables on ``self``. Keyword arguments
given to ``start_sequence`` are added to the store at the beginning.
**Error handling**
If *fault_on_error* in ``init_sequencer`` is true and an exception is
raised during an atomic step, the module goes into an ERROR state
because it cannot be ensured that further actions will be safe to
execute. A manual reset is required.
Otherwise, the module goes into the WARN state and can be started
again normally.
**Stop handling**
Between each atomic step, the "stop" flag for the sequence is checked,
which is set by the mixin's ``Stop`` method.
The *fault_on_stop* argument in ``init_sequencer`` controls which state
the module enters when the sequence is interrupted by a stop. Here,
the default is to only go into ALARM.
"""
if self.seq_is_alive():
raise IsBusyError('move sequence already in progress')
self._seq_stopflag = False
self._seq_error = self._seq_stopped = None
self._seq_thread = mkthread(self._seq_thread_outer, seq, store_init)
def seq_is_alive(self):
"""Can be called to check if a sequence is currently running."""
return self._seq_thread and self._seq_thread.isAlive()
def read_status(self):
if self.seq_is_alive():
return self.Status.BUSY, 'moving: ' + self._seq_phase
if self._seq_error:
if self._seq_fault_on_error:
return self.Status.ERROR, self._seq_error
return self.Status.WARN, self._seq_error
if self._seq_stopped:
if self._seq_fault_on_stop:
return self.Status.ERROR, self._seq_stopped
return self.Status.WARN, self._seq_stopped
if hasattr(self, 'readHwStatus'):
return self.readHwStatus()
return self.Status.IDLE, ''
def stop(self):
if self.seq_is_alive():
self._seq_stopflag = True
def _seq_thread_outer(self, seq, store_init):
try:
self._seq_thread_inner(seq, store_init)
except Exception as e:
self.log.exception('unhandled error in sequence thread: %s', e)
self._seq_error = str(e)
finally:
self._seq_thread = None
self.doPoll()
def _seq_thread_inner(self, seq, store_init):
store = Namespace()
store.__dict__.update(store_init)
self.log.debug('sequence: starting, values %s', store_init)
for step in seq:
self._seq_phase = step.desc
self.log.debug('sequence: entering phase: %s', step.desc)
try:
i = 0
while True:
store.i = i
result = step.func(store, *step.args)
if self._seq_stopflag:
if result:
self._seq_stopped = 'stopped while %s' % step.desc
else:
self._seq_stopped = 'stopped after %s' % step.desc
cleanup_func = step.kwds.get('cleanup', None)
if callable(cleanup_func):
try:
cleanup_func(store, result, *step.args)
except Exception as e:
self.log.exception(e)
raise
return
sleep(step.waittime)
if not result:
break
i += 1
except Exception as e:
self.log.exception(
'error in sequence step %r: %s', step.desc, e)
self._seq_error = 'during %s: %s' % (step.desc, e)
break