in the current implementation, in case polling results in an error, an error_update is generated. But on activate, for this parameter a normal update will be generated, indicating that this value is ok. With this change, parameters have an additonal attribue 'readerror', which save the error generated on the last read, or None, if the read was successful. In addition, subsequent repeated errors are only reported the first time. Change-Id: I39f18d17debadd8aa5b904e59998d2172f3f11b9 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/21974 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
430 lines
17 KiB
Python
430 lines
17 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:
|
|
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
#
|
|
# *****************************************************************************
|
|
"""Define Baseclasses for real Modules implemented in the server"""
|
|
|
|
|
|
import sys
|
|
import time
|
|
from collections import OrderedDict
|
|
|
|
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
|
StringType, TupleOf, get_datatype, ArrayOf, TextType
|
|
from secop.errors import ConfigError, ProgrammingError, SECoPError
|
|
from secop.lib import formatException, formatExtendedStack, mkthread
|
|
from secop.lib.enum import Enum
|
|
from secop.metaclass import ModuleMeta
|
|
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
|
from secop.properties import HasProperties, Property
|
|
from secop.poller import Poller, BasicPoller
|
|
|
|
|
|
# XXX: connect with 'protocol'-Modules.
|
|
# Idea: every Module defined herein is also a 'protocol'-Module,
|
|
# all others MUST derive from those, the 'interface'-class is still derived
|
|
# from these base classes (how to do this?)
|
|
|
|
|
|
class Module(HasProperties, metaclass=ModuleMeta):
|
|
"""Basic Module
|
|
|
|
ALL secop Modules derive from this
|
|
|
|
note: within Modules, parameters should only be addressed as self.<pname>
|
|
i.e. self.value, self.target etc...
|
|
these are accessing the cached version.
|
|
they can also be written to (which auto-calls self.write_<pname> and
|
|
generate an async update)
|
|
|
|
if you want to 'update from the hardware', call self.read_<pname>() instead
|
|
the return value of this method will be used as the new cached value and
|
|
be an async update sent automatically.
|
|
"""
|
|
# static properties, definitions in derived classes should overwrite earlier ones.
|
|
# note: properties don't change after startup and are usually filled
|
|
# with data from a cfg file...
|
|
# note: only the properties predefined here are allowed to be set in the cfg file
|
|
# note: the names map to a [datatype, value] list, value comes from the cfg file,
|
|
# datatype is fixed!
|
|
properties = {
|
|
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False),
|
|
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'),
|
|
'description': Property('Description of the module', TextType(), extname='description', mandatory=True),
|
|
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
|
default=('',0), extname='meaning'),
|
|
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
|
default='user', extname='visibility'),
|
|
'implementation': Property('Internal name of the implementation class of the module', StringType(),
|
|
extname='implementation'),
|
|
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
|
|
extname='interface_classes'),
|
|
# what else?
|
|
}
|
|
|
|
# properties, parameters and commands are auto-merged upon subclassing
|
|
parameters = {}
|
|
commands = {}
|
|
|
|
# reference to the dispatcher (used for sending async updates)
|
|
DISPATCHER = None
|
|
|
|
pollerClass = Poller
|
|
|
|
def __init__(self, name, logger, cfgdict, srv):
|
|
# remember the dispatcher object (for the async callbacks)
|
|
self.DISPATCHER = srv.dispatcher
|
|
self.log = logger
|
|
self.name = name
|
|
|
|
# handle module properties
|
|
# 1) make local copies of properties
|
|
super(Module, self).__init__()
|
|
|
|
# 2) check and apply properties specified in cfgdict
|
|
# specified as '.<propertyname> = <propertyvalue>'
|
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
|
if k[0] == '.':
|
|
if k[1:] in self.__class__.properties:
|
|
self.setProperty(k[1:], cfgdict.pop(k))
|
|
else:
|
|
raise ConfigError('Module %r has no property %r' %
|
|
(self.name, k[1:]))
|
|
|
|
# 4) set automatic properties
|
|
mycls = self.__class__
|
|
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
|
self.properties['implementation'] = myclassname
|
|
# list of all 'secop' modules
|
|
self.properties['interface_classes'] = [
|
|
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
|
# list of only the 'highest' secop module class
|
|
self.properties['interface_classes'] = [[
|
|
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
|
|
|
# handle Features
|
|
# XXX: todo
|
|
|
|
# handle accessibles
|
|
# 1) make local copies of parameter objects
|
|
# they need to be individual per instance since we use them also
|
|
# to cache the current value + qualifiers...
|
|
accessibles = OrderedDict()
|
|
# conversion from exported names to internal attribute names
|
|
accessiblename2attr = {}
|
|
for aname, aobj in self.accessibles.items():
|
|
# make a copy of the Parameter/Command object
|
|
aobj = aobj.copy()
|
|
if aobj.export:
|
|
if aobj.export is True:
|
|
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
|
if predefined_obj:
|
|
if isinstance(aobj, predefined_obj):
|
|
aobj.setProperty('export', aname)
|
|
else:
|
|
raise ProgrammingError("can not use '%s' as name of a %s" %
|
|
(aname, aobj.__class__.__name__))
|
|
else: # create custom parameter
|
|
aobj.setProperty('export', '_' + aname)
|
|
accessiblename2attr[aobj.export] = aname
|
|
accessibles[aname] = aobj
|
|
# do not re-use self.accessibles as this is the same for all instances
|
|
self.accessibles = accessibles
|
|
self.accessiblename2attr = accessiblename2attr
|
|
# provide properties to 'filter' out the parameters/commands
|
|
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
|
|
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command))
|
|
|
|
# 2) check and apply parameter_properties
|
|
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
|
if '.' in k[1:]:
|
|
paramname, propname = k.split('.', 1)
|
|
paramobj = self.accessibles.get(paramname, None)
|
|
# paramobj might also be a command (not sure if this is needed)
|
|
if paramobj:
|
|
if propname == 'datatype':
|
|
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k)))
|
|
elif propname in paramobj.getProperties():
|
|
paramobj.setProperty(propname, cfgdict.pop(k))
|
|
else:
|
|
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
|
(self.name, paramname, propname))
|
|
|
|
# 3) check config for problems:
|
|
# only accept remaining config items specified in parameters
|
|
for k, v in cfgdict.items():
|
|
if k not in self.parameters:
|
|
raise ConfigError(
|
|
'Module %s:config Parameter %r '
|
|
'not understood! (use one of %s)' %
|
|
(self.name, k, ', '.join(self.parameters.keys())))
|
|
|
|
# 4) complain if a Parameter entry has no default value and
|
|
# is not specified in cfgdict
|
|
for pname, pobj in self.parameters.items():
|
|
if pname not in cfgdict:
|
|
if pobj.default is None:
|
|
if not pobj.poll:
|
|
raise ConfigError('Module %s: Parameter %r has no default '
|
|
'value and was not given in config!' %
|
|
(self.name, pname))
|
|
# we do not want to call the setter for this parameter for now,
|
|
# this should happen on the first read
|
|
pobj.readerror = ConfigError('not initialized')
|
|
# above error will be triggered on activate after startup,
|
|
# when not all hardware parameters are read because of startup timeout
|
|
pobj.value = pobj.datatype(pobj.datatype.default)
|
|
else:
|
|
cfgdict[pname] = pobj.default
|
|
|
|
# 5) 'apply' config:
|
|
# pass values through the datatypes and store as attributes
|
|
for k, v in list(cfgdict.items()):
|
|
try:
|
|
# this checks also for the proper datatype
|
|
# note: this will NOT call write_* methods!
|
|
setattr(self, k, v)
|
|
except (ValueError, TypeError):
|
|
self.log.exception(formatExtendedStack())
|
|
raise
|
|
# raise ConfigError('Module %s: config parameter %r:\n%r' %
|
|
# (self.name, k, e))
|
|
cfgdict.pop(k)
|
|
|
|
# Modify units AFTER applying the cfgdict
|
|
for k, v in self.parameters.items():
|
|
dt = v.datatype
|
|
if '$' in dt.unit:
|
|
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
|
|
|
|
# 6) check complete configuration of * properties
|
|
self.checkProperties()
|
|
for p in self.parameters.values():
|
|
p.checkProperties()
|
|
|
|
# helper cfg-editor
|
|
def __iter__(self):
|
|
return self.accessibles.__iter__()
|
|
|
|
def __getitem__(self, item):
|
|
return self.accessibles.__getitem__(item)
|
|
|
|
def setError(self, pname, exception):
|
|
"""sets a parameter to a read error state
|
|
|
|
the error will be cleared when the parameter is set
|
|
"""
|
|
pobj = self.parameters[pname]
|
|
self.DISPATCHER.announce_update_error(self, pname, pobj, exception)
|
|
|
|
def isBusy(self):
|
|
'''helper function for treating substates of BUSY correctly'''
|
|
# defined even for non drivable (used for dynamic polling)
|
|
return False
|
|
|
|
def earlyInit(self):
|
|
# may be overriden in derived classes to init stuff
|
|
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
|
|
|
def initModule(self):
|
|
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
|
|
|
def startModule(self, started_callback):
|
|
'''runs after init of all modules
|
|
|
|
started_callback to be called when thread spawned by late_init
|
|
or, if not implemented, immediately
|
|
might return a timeout value, if different from default
|
|
'''
|
|
|
|
self.log.debug('empty %s.startModule()' % self.__class__.__name__)
|
|
started_callback()
|
|
|
|
def pollOne(self, pname):
|
|
"""call read function and handle error logging"""
|
|
try:
|
|
getattr(self, 'read_' + pname)()
|
|
except SECoPError as e:
|
|
self.log.error(str(e))
|
|
except Exception as e:
|
|
self.log.error(formatException())
|
|
|
|
|
|
class Readable(Module):
|
|
"""Basic readable Module
|
|
|
|
providing the readonly parameter 'value' and 'status'
|
|
|
|
Also allow configurable polling per 'pollinterval' parameter.
|
|
"""
|
|
# pylint: disable=invalid-name
|
|
Status = Enum('Status',
|
|
IDLE = 100,
|
|
WARN = 200,
|
|
UNSTABLE = 270,
|
|
ERROR = 400,
|
|
DISABLED = 0,
|
|
)
|
|
parameters = {
|
|
'value': Parameter('current value of the Module', readonly=True,
|
|
default=0., datatype=FloatRange(),
|
|
poll=True,
|
|
),
|
|
'pollinterval': Parameter('sleeptime between polls', default=5,
|
|
readonly=False,
|
|
datatype=FloatRange(0.1, 120),
|
|
),
|
|
'status': Parameter('current status of the Module',
|
|
default=(Status.IDLE, ''),
|
|
datatype=TupleOf(EnumType(Status), StringType()),
|
|
readonly=True, poll=True,
|
|
),
|
|
}
|
|
|
|
def startModule(self, started_callback):
|
|
'''start basic polling thread'''
|
|
if issubclass(self.pollerClass, BasicPoller):
|
|
# use basic poller for legacy code
|
|
mkthread(self.__pollThread, started_callback)
|
|
else:
|
|
started_callback()
|
|
|
|
def __pollThread(self, started_callback):
|
|
while True:
|
|
try:
|
|
self.__pollThread_inner(started_callback)
|
|
except Exception as e:
|
|
self.log.exception(e)
|
|
self.status = (self.Status.ERROR, 'polling thread could not start')
|
|
started_callback()
|
|
print(formatException(0, sys.exc_info(), verbose=True))
|
|
time.sleep(10)
|
|
|
|
def __pollThread_inner(self, started_callback):
|
|
"""super simple and super stupid per-module polling thread"""
|
|
i = 0
|
|
fastpoll = self.pollParams(i)
|
|
started_callback()
|
|
while True:
|
|
i += 1
|
|
try:
|
|
time.sleep(self.pollinterval * (0.1 if fastpoll else 1))
|
|
except TypeError:
|
|
time.sleep(min(self.pollinterval)
|
|
if fastpoll else max(self.pollinterval))
|
|
fastpoll = self.pollParams(i)
|
|
|
|
def pollParams(self, nr=0):
|
|
# Just poll all parameters regularly where polling is enabled
|
|
for pname, pobj in self.parameters.items():
|
|
if not pobj.poll:
|
|
continue
|
|
if nr % abs(int(pobj.poll)) == 0:
|
|
# pollParams every 'pobj.pollParams' iteration
|
|
self.pollOne(pname)
|
|
return False
|
|
|
|
|
|
class Writable(Readable):
|
|
"""Basic Writable Module
|
|
|
|
providing a settable 'target' parameter to those of a Readable
|
|
"""
|
|
parameters = {
|
|
'target': Parameter('target value of the Module',
|
|
default=0., readonly=False, datatype=FloatRange(),
|
|
),
|
|
}
|
|
|
|
|
|
class Drivable(Writable):
|
|
"""Basic Drivable Module
|
|
|
|
provides a stop command to interrupt actions.
|
|
Also status gets extended with a BUSY state indicating a running action.
|
|
"""
|
|
|
|
Status = Enum(Readable.Status, BUSY=300)
|
|
|
|
commands = {
|
|
'stop': Command(
|
|
'cease driving, go to IDLE state',
|
|
argument=None,
|
|
result=None
|
|
),
|
|
}
|
|
|
|
overrides = {
|
|
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
|
|
}
|
|
|
|
def isBusy(self):
|
|
'''helper function for treating substates of BUSY correctly'''
|
|
return 300 <= self.status[0] < 400
|
|
|
|
# improved polling: may poll faster if module is BUSY
|
|
def pollParams(self, nr=0):
|
|
# poll status first
|
|
self.read_status()
|
|
fastpoll = self.isBusy()
|
|
for pname, pobj in self.parameters.items():
|
|
if not pobj.poll:
|
|
continue
|
|
if pname == 'status':
|
|
# status was already polled above
|
|
continue
|
|
if ((int(pobj.poll) < 0) and fastpoll) or (
|
|
nr % abs(int(pobj.poll))) == 0:
|
|
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
|
|
# otherwise poll every 'pobj.poll' iteration
|
|
self.pollOne(pname)
|
|
return fastpoll
|
|
|
|
def do_stop(self):
|
|
"""default implementation of the stop command
|
|
|
|
by default does nothing."""
|
|
|
|
|
|
class Communicator(Module):
|
|
"""Basic communication Module
|
|
|
|
providing no parameters, but a 'communicate' command.
|
|
"""
|
|
|
|
commands = {
|
|
"communicate": Command("provides the simplest mean to communication",
|
|
argument=StringType(),
|
|
result=StringType()
|
|
),
|
|
}
|
|
|
|
|
|
|
|
class Attached(Property):
|
|
# we can not put this to properties.py, as it needs datatypes
|
|
def __init__(self, attrname=None):
|
|
self.attrname = attrname
|
|
super().__init__('attached module', StringType())
|
|
|
|
def __repr__(self):
|
|
return 'Attached(%r)' % self.description
|