frappy/secop/states.py
camea b7a1f17e5e fix new statemachine with ips magfield
+ add on_error, on_restart etc. to states.py
+ add n_retry argument to mercury change/multichange
2022-12-19 15:12:00 +01:00

177 lines
5.9 KiB
Python

#!/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>
#
# *****************************************************************************
"""state machine mixin
handles status depending on statemachine state
"""
from secop.core import BUSY, IDLE, ERROR, Parameter, Command, Done
from secop.errors import ProgrammingError
from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop
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 = owner.statusMap.copy()
owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text
setattr(owner, name, self.func) # replace with original method
def __call__(self, func):
self.func = func
return self
class HasStates:
status = Parameter() # make sure this is a parameter
all_status_changes = False # when true, send also updates for status changes within a cycle
_state_machine = None
_status = IDLE, ''
statusMap = {}
def init_state_machine(self, **kwds):
self._state_machine = StateMachine(
logger=self.log,
idle_status=(IDLE, ''),
transition=self.state_transition,
reset_fast_poll=False,
status=(IDLE, ''),
**kwds)
def initModule(self):
super().initModule()
self.init_state_machine()
def state_transition(self, sm, newstate):
"""handle status updates"""
status = self.get_status(newstate)
if sm.next_task:
if isinstance(sm.next_task, Stop):
if newstate and status is not None:
status = status[0], 'stopping (%s)' % status[1]
elif newstate:
# restart case
if status is not None:
status = sm.status[0], 'restarting (%s)' % status[1]
else:
# start case
status = self.get_status(sm.next_task.newstate, BUSY)
if status:
sm.status = status
if self.all_status_changes:
self.read_status()
def get_status(self, statefunc, default_code=None):
if statefunc is None:
status = self._state_machine.idle_status or (ERROR, 'Finish was returned without final status')
else:
name = statefunc.__name__
status = self.statusMap.get(name)
if status is None and default_code is not None:
status = default_code, name.replace('_', ' ')
return status
def read_status(self):
sm = self._state_machine
if sm.status == self.status:
return Done
return sm.status
def doPoll(self):
super().doPoll()
sm = self._state_machine
sm.cycle()
if sm.statefunc is None:
if sm.reset_fast_poll:
sm.reset_fast_poll = False
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, cleanup=kwds.pop('cleanup', self.on_cleanup), **kwds)
self.read_status()
if fast_poll:
sm.reset_fast_poll = True
self.setFastPoll(True)
self.pollInfo.trigger(True) # trigger poller
def stop_machine(self, stopped_status=(IDLE, 'stopped')):
sm = self._state_machine
if sm.is_active:
sm.idle_status = stopped_status
sm.stop()
sm.status = self.get_status(sm.statefunc, sm.status[0])[0], 'stopping'
self.read_status()
self.pollInfo.trigger(True) # trigger poller
@Command
def stop(self):
self.stop_machine()
def final_status(self, code=IDLE, text=''):
"""final status
Usage:
return self.final_status('IDLE', 'machine idle')
"""
sm = self._state_machine
sm.idle_status = code, text
sm.cleanup = None
return Finish