introduce update callbacks

includes a use case:
- a software calibration, to be applied to any Readable.
- calibration could be changed on the fly

+ refactored a little bit update events mechanism

Change-Id: Ifa340770caa9eb2185fe7e912c51bd9ddb411ece
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23093
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2020-05-15 09:30:52 +02:00
parent 7d987b3e42
commit 5c33cbf7a5
10 changed files with 315 additions and 78 deletions

View File

@ -30,6 +30,7 @@ from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
from secop.lib.enum import Enum
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
from secop.properties import Property
from secop.params import Parameter, Command, Override
from secop.metaclass import Done
from secop.iohandler import IOHandler, IOHandlerBase

View File

@ -286,7 +286,7 @@ class IOHandler(IOHandlerBase):
except Exception as e:
# set all parameters of this handler to error
for pname in self.parameters:
module.setError(pname, e)
module.announceUpdate(pname, None, e)
raise
return Done

View File

@ -23,7 +23,6 @@
"""Define Metaclass for Modules/Features"""
import time
from collections import OrderedDict
from secop.errors import ProgrammingError, BadValueError
@ -31,8 +30,6 @@ from secop.params import Command, Override, Parameter
from secop.datatypes import EnumType
from secop.properties import PropertyMeta
EVENT_ONLY_ON_CHANGED_VALUES = False
class Done:
"""a special return value for a read/write function
@ -149,7 +146,7 @@ class ModuleMeta(PropertyMeta):
return getattr(self, pname)
except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e))
self.setError(pname, e)
self.announceUpdate(pname, None, e)
raise
else:
# return cached value
@ -198,15 +195,7 @@ class ModuleMeta(PropertyMeta):
return self.accessibles[pname].value
def setter(self, value, pname=pname):
pobj = self.accessibles[pname]
value = pobj.datatype(value)
pobj.timestamp = time.time()
if (not EVENT_ONLY_ON_CHANGED_VALUES) or (value != pobj.value):
pobj.value = value
# also send notification
if pobj.export:
self.log.debug('%s is now %r' % (pname, value))
self.DISPATCHER.announce_update(self, pname, pobj)
self.announceUpdate(pname, value)
setattr(newtype, pname, property(getter, setter))

View File

@ -29,7 +29,8 @@ from collections import OrderedDict
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError, SilentError
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError,\
SilentError, InternalError, secop_error
from secop.lib import formatException, formatExtendedStack, mkthread
from secop.lib.enum import Enum
from secop.metaclass import ModuleMeta
@ -94,6 +95,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
self.DISPATCHER = srv.dispatcher
self.log = logger
self.name = name
self.valueCallbacks = {}
self.errorCallbacks = {}
# handle module properties
# 1) make local copies of properties
@ -199,6 +202,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
# is not specified in cfgdict and deal with parameters to be written.
self.writeDict = {} # values of parameters to be written
for pname, pobj in self.parameters.items():
self.valueCallbacks[pname] = []
self.errorCallbacks[pname] = []
if pname in cfgdict:
if not pobj.readonly and pobj.initwrite is not False:
# parameters given in cfgdict have to call write_<pname>
@ -265,14 +271,68 @@ class Module(HasProperties, metaclass=ModuleMeta):
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
"""
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
"""announce a changed value or readerror"""
pobj = self.parameters[pname]
if value is not None:
pobj.value = value # store the value even in case of error
if err:
if not isinstance(err, SECoPError):
err = InternalError(err)
if str(err) == str(pobj.readerror):
return # do call updates for repeated errors
else:
try:
pobj.value = pobj.datatype(value)
except Exception as e:
err = secop_error(e)
pobj.timestamp = timestamp or time.time()
pobj.readerror = err
if pobj.export:
self.DISPATCHER.announce_update_error(self, pname, pobj, exception)
self.DISPATCHER.announce_update(self.name, pname, pobj)
if err:
callbacks = self.errorCallbacks
arg = err
else:
callbacks = self.valueCallbacks
arg = value
cblist = callbacks[pname]
for cb in cblist:
try:
cb(arg)
except Exception as e:
# print(formatExtendedTraceback())
pass
def registerCallbacks(self, modobj, autoupdate=()):
for pname in self.parameters:
errfunc = getattr(modobj, 'error_update_' + pname, None)
if errfunc:
def errcb(err, p=pname, efunc=errfunc):
try:
efunc(err)
except Exception as e:
modobj.announceUpdate(p, err=e)
self.errorCallbacks[pname].append(errcb)
else:
def errcb(err, p=pname):
modobj.announceUpdate(p, err=err)
if pname in autoupdate:
self.errorCallbacks[pname].append(errcb)
updfunc = getattr(modobj, 'update_' + pname, None)
if updfunc:
def cb(value, ufunc=updfunc, efunc=errcb):
try:
ufunc(value)
except Exception as e:
efunc(e)
self.valueCallbacks[pname].append(cb)
elif pname in autoupdate:
def cb(value, p=pname):
modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None):
"""helper function for treating substates of BUSY correctly"""

View File

@ -43,8 +43,7 @@ from collections import OrderedDict
from time import time as currenttime
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, InternalError,\
SECoPError
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
from secop.params import Parameter
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
@ -101,26 +100,10 @@ class Dispatcher:
for conn in listeners:
conn.queue_async_reply(msg)
def announce_update(self, moduleobj, pname, pobj):
def announce_update(self, modulename, pname, pobj):
"""called by modules param setters to notify subscribers of new values
"""
# argument pname is no longer used here - should we remove it?
pobj.readerror = None
self.broadcast_event(make_update(moduleobj.name, pobj))
def announce_update_error(self, moduleobj, pname, pobj, err):
"""called by modules param setters/getters to notify subscribers
of problems
"""
# argument pname is no longer used here - should we remove it?
if not isinstance(err, SECoPError):
err = InternalError(err)
if str(err) == str(pobj.readerror):
return # do not send updates for repeated errors
pobj.readerror = err
pobj.timestamp = currenttime() # indicates the first time this error appeared
self.broadcast_event(make_update(moduleobj.name, pobj))
self.broadcast_event(make_update(modulename, pobj))
def subscribe(self, conn, eventname):
self._subscriptions.setdefault(eventname, set()).add(conn)

View File

@ -21,41 +21,37 @@
# *****************************************************************************
"""SECoP proxy modules"""
from secop.lib import get_class
from secop.modules import Module, Writable, Readable, Drivable, Attached
from secop.datatypes import StringType
from secop.protocol.dispatcher import make_update
from secop.properties import Property
from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.params import Parameter, Command
from secop.errors import ConfigError, make_secop_error, secop_error
from secop.modules import Module, Writable, Readable, Drivable
from secop.datatypes import StringType
from secop.properties import Property
from secop.stringio import HasIodev
from secop.lib import get_class
from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError
class ProxyModule(Module):
class ProxyModule(HasIodev, Module):
properties = {
'iodev': Attached(),
'module':
Property('remote module name', datatype=StringType(), default=''),
}
pollerClass = None
_consistency_check_done = False
_secnode = None
def iodevClass(self, name, logger, opts, srv):
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
return SecNode(name, logger, opts, srv)
def updateEvent(self, module, parameter, value, timestamp, readerror):
pobj = self.parameters[parameter]
pobj.timestamp = timestamp
if parameter not in self.parameters:
return # ignore unknown parameters
# should be done here: deal with clock differences
if readerror:
readerror = make_secop_error(*readerror)
if not readerror:
try:
pobj.value = value # store the value even in case of a validation error
pobj.value = pobj.datatype(value)
except Exception as e:
readerror = secop_error(e)
pobj.readerror = readerror
self.DISPATCHER.broadcast_event(make_update(self.name, pobj))
self.announceUpdate(parameter, value, readerror, timestamp)
def initModule(self):
if not self.module:
@ -116,9 +112,17 @@ class ProxyModule(Module):
# for now, the error message must be enough
def nodeStateChange(self, online, state):
if online and not self._consistency_check_done:
self._check_descriptive_data()
self._consistency_check_done = True
if online:
if not self._consistency_check_done:
self._check_descriptive_data()
self._consistency_check_done = True
else:
newstatus = Readable.Status.ERROR, 'disconnected'
readerror = CommunicationFailedError('disconnected')
if self.status != newstatus:
for pname in set(self.parameters) - set(('module', 'status')):
self.announceUpdate(pname, None, readerror)
self.announceUpdate('status', newstatus)
class ProxyReadable(ProxyModule, Readable):
@ -164,7 +168,7 @@ def proxy_class(remote_class, name=None):
remote class is <import path>.<class name> of a class used on the remote node
if name is not given, 'Proxy' + <class name> is used
"""
if issubclass(remote_class, Module):
if isinstance(remote_class, type) and issubclass(remote_class, Module):
rcls = remote_class
remote_class = rcls.__name__
else:
@ -231,4 +235,7 @@ def Proxy(name, logger, cfgdict, srv):
title cased as it acts like a class
"""
remote_class = cfgdict.pop('remote_class')
if 'description' not in cfgdict:
cfgdict['description'] = 'remote module %s on %s' % (
cfgdict.get('module', name), cfgdict.get('iodev', '?'))
return proxy_class(remote_class)(name, logger, cfgdict, srv)