Add lib/sequence support
Change-Id: If9bde0062185a5f13d17d9d2cea091bde7c98db9
This commit is contained in:
parent
eb80b56808
commit
63418fce04
@ -35,9 +35,46 @@ class ProgrammingError(SECoPServerError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommunicationError(SECoPServerError):
|
# for remote operation
|
||||||
pass
|
class SECoPError(SECoPServerError):
|
||||||
|
errorclass = 'InternalError'
|
||||||
|
|
||||||
|
class NoSuchModuleError(SECoPError):
|
||||||
|
errorclass = 'NoSuchModule'
|
||||||
|
|
||||||
|
class NoSuchParameterError(SECoPError):
|
||||||
|
errorclass = 'NoSuchParameter'
|
||||||
|
|
||||||
|
class NoSuchCommandError(SECoPError):
|
||||||
|
errorclass = 'NoSuchCommand'
|
||||||
|
|
||||||
|
class CommandFailedError(SECoPError):
|
||||||
|
errorclass = 'CommandFailed'
|
||||||
|
|
||||||
|
class CommandRunningError(SECoPError):
|
||||||
|
errorclass = 'CommandRunning'
|
||||||
|
|
||||||
|
class ReadOnlyError(SECoPError):
|
||||||
|
errorclass = 'ReadOnly'
|
||||||
|
|
||||||
|
class BadValueError(SECoPError):
|
||||||
|
errorclass = 'BadValue'
|
||||||
|
|
||||||
|
class CommunicationError(SECoPError):
|
||||||
|
errorclass = 'CommunicationFailed'
|
||||||
|
|
||||||
|
class TimeoutError(SECoPError):
|
||||||
|
errorclass = 'CommunicationFailed' # XXX: add to SECop messages
|
||||||
|
|
||||||
|
class HardwareError(SECoPError):
|
||||||
|
errorclass = 'CommunicationFailed' # XXX: Add to SECoP messages
|
||||||
|
|
||||||
|
class IsBusyError(SECoPError):
|
||||||
|
errorclass = 'IsBusy'
|
||||||
|
|
||||||
|
class IsErrorError(SECoPError):
|
||||||
|
errorclass = 'IsError'
|
||||||
|
|
||||||
|
class DisabledError(SECoPError):
|
||||||
|
errorclass = 'Disabled'
|
||||||
|
|
||||||
class HardwareError(SECoPServerError):
|
|
||||||
pass
|
|
||||||
|
181
secop/lib/sequence.py
Normal file
181
secop/lib/sequence.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# -*- 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 devices that require sequenced actions on value change."""
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from secop.lib import mkthread
|
||||||
|
from secop.protocol import status
|
||||||
|
from secop.errors import IsBusyError
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Step(object):
|
||||||
|
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(object):
|
||||||
|
"""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 device 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 device 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 device 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 status.BUSY, 'moving: ' + self._seq_phase
|
||||||
|
elif self._seq_error:
|
||||||
|
if self._seq_fault_on_error:
|
||||||
|
return status.ERROR, self._seq_error
|
||||||
|
return status.WARN, self._seq_error
|
||||||
|
elif self._seq_stopped:
|
||||||
|
if self._seq_fault_on_stop:
|
||||||
|
return status.ERROR, self._seq_stopped
|
||||||
|
return status.WARN, self._seq_stopped
|
||||||
|
if hasattr(self, 'read_hw_status'):
|
||||||
|
return self.read_hw_status()
|
||||||
|
return OK, ''
|
||||||
|
|
||||||
|
def do_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)
|
||||||
|
|
||||||
|
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:
|
||||||
|
while True:
|
||||||
|
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):
|
||||||
|
cleanup_func(store, *step.args)
|
||||||
|
return
|
||||||
|
sleep(step.waittime)
|
||||||
|
if not result:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.log.exception('error in sequence step: %s', e)
|
||||||
|
self._seq_error = 'during %s: %s' % (step.desc, e)
|
||||||
|
break
|
@ -559,7 +559,7 @@ class AnalogOutput(PyTangoDevice, Driveable):
|
|||||||
def write_userlimits(self, value):
|
def write_userlimits(self, value):
|
||||||
return self._checkLimits(value)
|
return self._checkLimits(value)
|
||||||
|
|
||||||
def do_start(self, value=FloatRange()):
|
def write_target(self, value=FloatRange()):
|
||||||
try:
|
try:
|
||||||
self._dev.value = value
|
self._dev.value = value
|
||||||
except HardwareError:
|
except HardwareError:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user