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:
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user