diff --git a/secop/errors.py b/secop/errors.py index 2791090..709f1af 100644 --- a/secop/errors.py +++ b/secop/errors.py @@ -35,9 +35,46 @@ class ProgrammingError(SECoPServerError): pass -class CommunicationError(SECoPServerError): - pass +# for remote operation +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 diff --git a/secop/lib/sequence.py b/secop/lib/sequence.py new file mode 100644 index 0000000..9b48622 --- /dev/null +++ b/secop/lib/sequence.py @@ -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 +# Enrico Faulhaber +# +# ***************************************************************************** + +"""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 diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index 30f362b..b610e8d 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -559,7 +559,7 @@ class AnalogOutput(PyTangoDevice, Driveable): def write_userlimits(self, value): return self._checkLimits(value) - def do_start(self, value=FloatRange()): + def write_target(self, value=FloatRange()): try: self._dev.value = value except HardwareError: