write configured parameters to the hardware
writable parameters with a configured value should call write_<param> on initialization. + introduced 'initwrite' parameter property for more fine control over this + minor improvements in metaclass.py, param.py, commandhandler.py + rearranged test_modules.py Change-Id: I2eec45da40947a73d9c180f0f146eb62efbda2b3 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/21986 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
44eeea1159
commit
1521e0a34b
@ -20,12 +20,36 @@
|
||||
# *****************************************************************************
|
||||
"""command handler
|
||||
|
||||
utility class for cases, where multiple parameters are treated with a common command.
|
||||
Utility class for cases, where multiple parameters are treated with a common command.
|
||||
The support for LakeShore and similar protocols is already included.
|
||||
|
||||
For read, instead of the methods read_<parameter> we write one method analyze_<group>
|
||||
for all parameters with the same handler. Before analyze_<group> is called, the
|
||||
reply is parsed and converted to values, which are then given as arguments.
|
||||
|
||||
def analyze_<group>(self, value1, value2, ...):
|
||||
# <here> we have to calculate parameters from the values (value1, value2 ...)
|
||||
# and assign them to self.<parameter>
|
||||
# no return value is expected
|
||||
|
||||
For write, instead of the methods write_<parameter>" we write one method change_<group>
|
||||
for all parameters with the same handler.
|
||||
|
||||
def change_<group>(self, new, value1, value2, ...):
|
||||
# <new> is a wrapper object around the module, containing already the new values.
|
||||
# if READ_BEFORE_WRITE is True (the default), the additional arguments (value1, ...)
|
||||
# must be in the argument list. They contain the values read from the hardware.
|
||||
# If they are not needed, set READ_BEFORE_WRITE to False, or declare them as '*args'.
|
||||
# The expression ('<parameter>' in new) returns a boolean indicating, whether
|
||||
# this parameter is subject to change.
|
||||
# The return value must be either a sequence of values to be written to the hardware,
|
||||
# which will be formatted by the handler, or None. The latter is used only in some
|
||||
# special cases, when nothing has to be written.
|
||||
"""
|
||||
import re
|
||||
|
||||
from secop.metaclass import Done
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
|
||||
@ -113,34 +137,38 @@ class CmdParser:
|
||||
|
||||
|
||||
class ChangeWrapper:
|
||||
"""store parameter changes before they are applied"""
|
||||
"""Wrapper around a module
|
||||
|
||||
def __init__(self, module, pname, value):
|
||||
A ChangeWrapper instance is used as the 'new' argument for the change_<group> message.
|
||||
new.<parameter> is either the new, changed value or the old value from the module.
|
||||
In addition '<parameter>' indicates, whether <parameter> is to be changed.
|
||||
setting new.<parameter> does not yet set the value on the module.
|
||||
"""
|
||||
def __init__(self, module, valuedict):
|
||||
self._module = module
|
||||
setattr(self, pname, value)
|
||||
for pname, value in valuedict.items():
|
||||
setattr(self, pname, value)
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""get values from module for unknown keys"""
|
||||
"""get current values from _module for unchanged parameters"""
|
||||
return getattr(self._module, key)
|
||||
|
||||
def apply(self, module):
|
||||
"""set only changed values"""
|
||||
for k, v in self.__dict__.items():
|
||||
if k != '_module' and v != getattr(module, k):
|
||||
setattr(module, k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return ', '.join('%s=%r' % (k, v) for k, v in self.__dict__.items() if k != '_module')
|
||||
|
||||
def __contains__(self, pname):
|
||||
"""check whether a specific parameter is to be changed"""
|
||||
return pname in self.__dict__
|
||||
|
||||
|
||||
class CmdHandlerBase:
|
||||
"""generic command handler"""
|
||||
READ_BEFORE_WRITE = True
|
||||
# if READ_BEFORE_WRITE is True, a read is performed before a write, and the parsed
|
||||
# additional parameters are added to the argument list of change_<group>.
|
||||
|
||||
def __init__(self, group):
|
||||
# group is used for calling the proper analyze_<group> and change_<group> methods
|
||||
self.group = group
|
||||
self.parameters = {}
|
||||
self.parameters = set()
|
||||
self._module_class = None
|
||||
|
||||
def parse_reply(self, reply):
|
||||
"""return values from a raw reply"""
|
||||
@ -179,11 +207,11 @@ class CmdHandlerBase:
|
||||
|
||||
and registers the parameter in this handler
|
||||
"""
|
||||
if not modclass in self.parameters:
|
||||
self.parameters[modclass] = []
|
||||
# Make sure that parameters from different module classes are not mixed
|
||||
# (not sure if this might happen)
|
||||
self.parameters[modclass].append(pname)
|
||||
self._module_class = self._module_class or modclass
|
||||
if self._module_class != modclass:
|
||||
raise ProgrammingError("the handler '%s' for '%s.%s' is already used in module '%s'"
|
||||
% (self.group, modclass.__name__, pname, self._module_class.__name__))
|
||||
self.parameters.add(pname)
|
||||
return self.read
|
||||
|
||||
def read(self, module):
|
||||
@ -196,14 +224,15 @@ class CmdHandlerBase:
|
||||
reply = self.send_command(module)
|
||||
# convert them to parameters
|
||||
getattr(module, 'analyze_' + self.group)(*reply)
|
||||
for pname in self.parameters[module.__class__]:
|
||||
assert module.__class__ == self._module_class
|
||||
for pname in self.parameters:
|
||||
if module.parameters[pname].readerror:
|
||||
# clear errors on parameters, which were not updated.
|
||||
# this will also inform all activated clients
|
||||
setattr(module, pname, getattr(module, pname))
|
||||
except Exception as e:
|
||||
# set all parameters of this handler to error
|
||||
for pname in self.parameters[module.__class__]:
|
||||
for pname in self.parameters:
|
||||
module.setError(pname, e)
|
||||
raise
|
||||
return Done # parameters should be updated already
|
||||
@ -215,23 +244,35 @@ class CmdHandlerBase:
|
||||
"""
|
||||
|
||||
def wfunc(module, value, cmd=self, pname=pname):
|
||||
# do a read of the current hw values
|
||||
values = cmd.send_command(module)
|
||||
# convert them to parameters
|
||||
analyze = getattr(module, 'analyze_' + cmd.group)
|
||||
analyze(*values)
|
||||
# create wrapper object 'new' with changed parameter 'pname'
|
||||
new = ChangeWrapper(module, pname, value)
|
||||
# call change_* for calculation new hw values
|
||||
values = getattr(module, 'change_' + cmd.group)(new, *values)
|
||||
# send the change command and a query command
|
||||
analyze(*cmd.send_change(module, *values))
|
||||
# update only changed values
|
||||
new.apply(module)
|
||||
return Done # parameter 'pname' should be changed already
|
||||
cmd.write(module, {pname: value})
|
||||
return Done
|
||||
|
||||
return wfunc
|
||||
|
||||
def write(self, module, valuedict, force_read=False):
|
||||
"""write values to the module
|
||||
|
||||
When called from write_<param>, valuedict contains only one item:
|
||||
the parameter to be changed.
|
||||
When called from initialization, valuedict may have more items.
|
||||
"""
|
||||
analyze = getattr(module, 'analyze_' + self.group)
|
||||
if self.READ_BEFORE_WRITE or force_read:
|
||||
# do a read of the current hw values
|
||||
values = self.send_command(module)
|
||||
# convert them to parameters
|
||||
analyze(*values)
|
||||
if not self.READ_BEFORE_WRITE:
|
||||
values = ()
|
||||
# create wrapper object 'new' with changed parameter 'pname'
|
||||
new = ChangeWrapper(module, valuedict)
|
||||
# call change_* for calculation new hw values
|
||||
values = getattr(module, 'change_' + self.group)(new, *values)
|
||||
if values is None: # this indicates that nothing has to be written
|
||||
return
|
||||
# send the change command and a query command
|
||||
analyze(*self.send_change(module, *values))
|
||||
|
||||
|
||||
class CmdHandler(CmdHandlerBase):
|
||||
"""more evolved command handler
|
||||
|
@ -119,7 +119,6 @@ class ModuleMeta(PropertyMeta):
|
||||
"and read_%s" % (pname, pname))
|
||||
rfunc = handler
|
||||
else:
|
||||
rfunc = attrs.get('read_' + pname, None)
|
||||
for base in bases:
|
||||
if rfunc is not None:
|
||||
break
|
||||
|
@ -28,7 +28,7 @@ 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.errors import ConfigError, ProgrammingError, SECoPError, BadValueError
|
||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||
from secop.lib.enum import Enum
|
||||
from secop.metaclass import ModuleMeta
|
||||
@ -178,9 +178,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
(self.name, k, ', '.join(self.parameters.keys())))
|
||||
|
||||
# 4) complain if a Parameter entry has no default value and
|
||||
# is not specified in cfgdict
|
||||
# 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():
|
||||
if pname not in cfgdict:
|
||||
if pname in cfgdict:
|
||||
if not pobj.readonly and not pobj.initwrite is False:
|
||||
# parameters given in cfgdict have to call write_<pname>
|
||||
try:
|
||||
pobj.value = pobj.datatype(cfgdict[pname])
|
||||
except BadValueError as e:
|
||||
raise ConfigError('%s.%s: %s' % (name, pname, e))
|
||||
self.writeDict[pname] = pobj.value
|
||||
else:
|
||||
if pobj.default is None:
|
||||
if not pobj.poll:
|
||||
raise ConfigError('Module %s: Parameter %r has no default '
|
||||
@ -193,7 +202,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# when not all hardware parameters are read because of startup timeout
|
||||
pobj.value = pobj.datatype(pobj.datatype.default)
|
||||
else:
|
||||
cfgdict[pname] = pobj.default
|
||||
try:
|
||||
value = pobj.datatype(pobj.default)
|
||||
except BadValueError as e:
|
||||
raise ProgrammingError('bad default for %s.%s: %s'
|
||||
% (name, pname, e))
|
||||
if pobj.initwrite:
|
||||
# we will need to call write_<pname>
|
||||
# if this is not desired, the default must not be given
|
||||
pobj.value = value
|
||||
self.writeDict[pname] = value
|
||||
else:
|
||||
cfgdict[pname] = value
|
||||
|
||||
# 5) 'apply' config:
|
||||
# pass values through the datatypes and store as attributes
|
||||
@ -258,10 +278,36 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
self.log.debug('empty %s.startModule()' % self.__class__.__name__)
|
||||
started_callback()
|
||||
|
||||
def pollOne(self, pname):
|
||||
"""call read function and handle error logging"""
|
||||
def pollOneParam(self, pname):
|
||||
"""poll parameter <pname> with proper error handling"""
|
||||
try:
|
||||
getattr(self, 'read_' + pname)()
|
||||
return getattr(self, 'read_'+ pname)()
|
||||
except SECoPError as e:
|
||||
self.log.error(str(e))
|
||||
except Exception as e:
|
||||
self.log.error(formatException())
|
||||
|
||||
def writeOrPoll(self, pname):
|
||||
"""write configured value for a parameter, if any, else poll
|
||||
|
||||
with proper error handling
|
||||
"""
|
||||
try:
|
||||
pobj = self.parameters[pname]
|
||||
if pobj.handler:
|
||||
pnames = pobj.handler.parameters
|
||||
valuedict = {n: self.writeDict.pop(n) for n in pnames if n in self.writeDict}
|
||||
if valuedict:
|
||||
self.log.info('write parameters %r', valuedict)
|
||||
pobj.handler.write(self, valuedict, force_read=True)
|
||||
return
|
||||
pobj.handler.read(self)
|
||||
else:
|
||||
if pname in self.writeDict:
|
||||
self.log.info('write parameter %s', pname)
|
||||
getattr(self, 'write_'+ pname)(self.writeDict.pop(pname))
|
||||
else:
|
||||
getattr(self, 'read_'+ pname)()
|
||||
except SECoPError as e:
|
||||
self.log.error(str(e))
|
||||
except Exception as e:
|
||||
@ -285,7 +331,7 @@ class Readable(Module):
|
||||
)
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
default=0., datatype=FloatRange(),
|
||||
datatype=FloatRange(),
|
||||
poll=True,
|
||||
),
|
||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
||||
@ -320,6 +366,8 @@ class Readable(Module):
|
||||
|
||||
def __pollThread_inner(self, started_callback):
|
||||
"""super simple and super stupid per-module polling thread"""
|
||||
for pname in list(self.writeDict):
|
||||
self.writeOrPoll(pname)
|
||||
i = 0
|
||||
fastpoll = self.pollParams(i)
|
||||
started_callback()
|
||||
@ -339,7 +387,7 @@ class Readable(Module):
|
||||
continue
|
||||
if nr % abs(int(pobj.poll)) == 0:
|
||||
# pollParams every 'pobj.pollParams' iteration
|
||||
self.pollOne(pname)
|
||||
self.pollOneParam(pname)
|
||||
return False
|
||||
|
||||
|
||||
@ -350,7 +398,7 @@ class Writable(Readable):
|
||||
"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0., readonly=False, datatype=FloatRange(),
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
),
|
||||
}
|
||||
|
||||
@ -395,7 +443,7 @@ class Drivable(Writable):
|
||||
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)
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
def do_stop(self):
|
||||
|
@ -26,7 +26,7 @@ from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
||||
NoneOr, TextType, IntRange
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.errors import ProgrammingError, BadValueError
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
@ -114,6 +114,8 @@ class Parameter(Accessible):
|
||||
settable=False, default=False),
|
||||
'handler': Property('[internal] overload the standard read and write functions',
|
||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
||||
'initwrite': Property('[internal] write this parameter on initialization (default None: write if given in config)',
|
||||
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
|
||||
}
|
||||
|
||||
def __init__(self, description, datatype, ctr=None, unit=None, **kwds):
|
||||
@ -126,7 +128,7 @@ class Parameter(Accessible):
|
||||
# goodie: make an instance from a class (forgotten ()???)
|
||||
datatype = datatype()
|
||||
else:
|
||||
raise ValueError(
|
||||
raise ProgrammingError(
|
||||
'datatype MUST be derived from class DataType!')
|
||||
|
||||
kwds['description'] = description
|
||||
@ -139,6 +141,9 @@ class Parameter(Accessible):
|
||||
if self.handler and not self.poll:
|
||||
self.properties['poll'] = True
|
||||
|
||||
if self.readonly and self.initwrite:
|
||||
raise ProgrammingError('can not have both readonly and initwrite!')
|
||||
|
||||
if self.constant is not None:
|
||||
self.properties['readonly'] = True
|
||||
# The value of the `constant` property should be the
|
||||
@ -233,6 +238,12 @@ class Override(CountedObj):
|
||||
constant = obj.datatype(self.kwds.pop('constant'))
|
||||
self.kwds['constant'] = obj.datatype.export_value(constant)
|
||||
self.kwds['readonly'] = True
|
||||
if 'datatype' in self.kwds and 'default' not in self.kwds:
|
||||
try:
|
||||
self.kwds['datatype'](obj.default)
|
||||
except BadValueError:
|
||||
# clear default, if it does not match datatype
|
||||
props['default'] = None
|
||||
props.update(self.kwds)
|
||||
|
||||
if self.reorder:
|
||||
|
@ -186,7 +186,7 @@ class Poller(PollerBase):
|
||||
mininterval = interval
|
||||
due = max(lastdue + interval, pobj.timestamp + interval * 0.5)
|
||||
if now >= due:
|
||||
module.pollOne(pname)
|
||||
module.pollOneParam(pname)
|
||||
done = True
|
||||
lastdue = due
|
||||
due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5))
|
||||
@ -214,7 +214,7 @@ class Poller(PollerBase):
|
||||
for _, queue in sorted(self.queues.items()): # do SLOW polls first
|
||||
for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue):
|
||||
lastdue = time.time()
|
||||
module.pollOne(pname)
|
||||
module.writeOrPoll(pname)
|
||||
due = lastdue + min(self.maxwait, module.pollinterval * factor)
|
||||
# in python 3 comparing tuples need some care, as not all objects
|
||||
# are comparable. Inserting a unique idx solves the problem.
|
||||
|
@ -26,6 +26,7 @@ import pytest
|
||||
from secop.commandhandler import CmdParser, CmdHandler
|
||||
from secop.modules import Module, Parameter
|
||||
from secop.datatypes import FloatRange, StringType, IntRange, Property
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
@pytest.mark.parametrize('fmt, text, values, text2', [
|
||||
('%d,%d', '2,3', [2,3], None),
|
||||
@ -99,7 +100,7 @@ def test_CmdHandler():
|
||||
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
|
||||
|
||||
|
||||
class TestModule(Module):
|
||||
class Module1(Module):
|
||||
properties = {
|
||||
'channel': Property('the channel', IntRange(), default=3),
|
||||
'loop': Property('the loop', IntRange(), default=2),
|
||||
@ -130,7 +131,8 @@ def test_CmdHandler():
|
||||
|
||||
data = Data()
|
||||
updates = {}
|
||||
module = TestModule('mymodule', LoggerStub(), {'.description': ''}, ServerStub(updates))
|
||||
module = Module1('mymodule', LoggerStub(), {'.description': ''}, ServerStub(updates))
|
||||
print(updates)
|
||||
updates.clear() # get rid of updates from initialisation
|
||||
|
||||
# for sendRecv
|
||||
@ -182,3 +184,10 @@ def test_CmdHandler():
|
||||
assert module.real == updates.pop('real') == 12.3
|
||||
assert data.empty()
|
||||
assert not updates
|
||||
|
||||
with pytest.raises(ProgrammingError): # can not use a handler for different modules
|
||||
# pylint: disable=unused-variable
|
||||
class Module2(Module):
|
||||
parameters = {
|
||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""test data types."""
|
||||
@ -25,93 +26,119 @@
|
||||
#import pytest
|
||||
|
||||
import threading
|
||||
from secop.datatypes import BoolType, EnumType, FloatRange
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.datatypes import BoolType, FloatRange, StringType
|
||||
from secop.modules import Communicator, Drivable, Module
|
||||
from secop.params import Command, Override, Parameter
|
||||
from secop.poller import BasicPoller
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
def __init__(self, updates):
|
||||
self.updates = updates
|
||||
|
||||
def announce_update(self, moduleobj, pname, pobj):
|
||||
self.updates.setdefault(moduleobj.name, {})
|
||||
self.updates[moduleobj.name][pname] = pobj.value
|
||||
|
||||
def announce_update_error(self, moduleobj, pname, pobj, err):
|
||||
self.updates['error', moduleobj.name, pname] = str(err)
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, *args):
|
||||
print(*args)
|
||||
info = exception = debug
|
||||
|
||||
|
||||
class ServerStub:
|
||||
def __init__(self, updates):
|
||||
self.dispatcher = DispatcherStub(updates)
|
||||
|
||||
|
||||
def test_Communicator():
|
||||
logger = type('LoggerStub', (object,), dict(
|
||||
debug = lambda self, *a: print(*a),
|
||||
info = lambda self, *a: print(*a),
|
||||
))()
|
||||
|
||||
dispatcher = type('DispatcherStub', (object,), dict(
|
||||
announce_update = lambda self, m, pn, pv: print('%s:%s=%r' % (m.name, pn, pv)),
|
||||
))()
|
||||
|
||||
srv = type('ServerStub', (object,), dict(
|
||||
dispatcher = dispatcher,
|
||||
))()
|
||||
|
||||
o = Communicator('communicator',logger, {'.description':''}, srv)
|
||||
o = Communicator('communicator', LoggerStub(), {'.description':''}, ServerStub({}))
|
||||
o.earlyInit()
|
||||
o.initModule()
|
||||
event = threading.Event()
|
||||
o.startModule(event.set)
|
||||
assert event.is_set() # event should be set immediately
|
||||
|
||||
|
||||
def test_ModuleMeta():
|
||||
# pylint: disable=too-many-function-args
|
||||
newclass1 = ModuleMeta.__new__(ModuleMeta, 'TestDrivable', (Drivable,), {
|
||||
"parameters" : {
|
||||
class Newclass1(Drivable):
|
||||
parameters = {
|
||||
'pollinterval': Override(reorder=True),
|
||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
||||
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
||||
},
|
||||
"commands": {
|
||||
}
|
||||
commands = {
|
||||
# intermixing parameters with commands is not recommended,
|
||||
# but acceptable for influencing the order
|
||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
||||
'value': Override(datatype=BoolType(), default=True),
|
||||
'value': Override(datatype=StringType(), default='first'),
|
||||
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
|
||||
},
|
||||
"do_cmd": lambda self, arg: not arg,
|
||||
"do_cmd2": lambda self, arg: not arg,
|
||||
"read_param1": lambda self, *args: True,
|
||||
"read_param2": lambda self, *args: False,
|
||||
"read_a1": lambda self, *args: True,
|
||||
"read_a2": lambda self, *args: False,
|
||||
"read_value": lambda self, *args: True,
|
||||
"init": lambda self, *args: [None for self.accessibles['value'].datatype in [EnumType('value', OK=1, Bad=2)]],
|
||||
})
|
||||
}
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def do_cmd(self, arg):
|
||||
return not arg
|
||||
|
||||
def do_cmd2(self, arg):
|
||||
return not arg
|
||||
|
||||
def read_param1(self):
|
||||
return True
|
||||
|
||||
def read_param2(self):
|
||||
return False
|
||||
|
||||
def read_a1(self):
|
||||
return True
|
||||
|
||||
def read_a2(self):
|
||||
return True
|
||||
|
||||
def read_value(self):
|
||||
return 'second'
|
||||
|
||||
|
||||
# first inherited accessibles, then Overrides with reorder=True and new accessibles
|
||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
||||
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
||||
|
||||
# pylint: disable=too-many-function-args
|
||||
newclass2 = ModuleMeta.__new__(ModuleMeta, 'UpperClass', (newclass1,), {
|
||||
"parameters": {
|
||||
class Newclass2(Newclass1):
|
||||
parameters = {
|
||||
'cmd2': Override('another stuff'),
|
||||
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True),
|
||||
'b2': Parameter('a2', datatype=BoolType(), default=True),
|
||||
},
|
||||
})
|
||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
|
||||
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
|
||||
poll=True, readonly=False, initwrite=True),
|
||||
}
|
||||
|
||||
def write_a1(self, value):
|
||||
self._a1_written = value
|
||||
return value
|
||||
|
||||
def write_b2(self, value):
|
||||
self._b2_written = value
|
||||
return value
|
||||
|
||||
def read_value(self):
|
||||
return 0
|
||||
|
||||
sortcheck2 = ['value', 'status', 'target', 'pollinterval',
|
||||
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2']
|
||||
|
||||
logger = type('LoggerStub', (object,), dict(
|
||||
debug = lambda self, *a: print(*a),
|
||||
info = lambda self, *a: print(*a),
|
||||
exception = lambda self, *a: print(*a),
|
||||
))()
|
||||
|
||||
dispatcher = type('DispatcherStub', (object,), dict(
|
||||
announce_update = lambda self, m, pn, po: print('%s:%s=%r' % (m.name, pn, po.value)),
|
||||
))()
|
||||
|
||||
srv = type('ServerStub', (object,), dict(
|
||||
dispatcher = dispatcher,
|
||||
))()
|
||||
logger = LoggerStub()
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
params_found = set() # set of instance accessibles
|
||||
objects = []
|
||||
|
||||
for newclass, sortcheck in [(newclass1, sortcheck1), (newclass2, sortcheck2)]:
|
||||
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
|
||||
o1 = newclass('o1', logger, {'.description':''}, srv)
|
||||
o2 = newclass('o2', logger, {'.description':''}, srv)
|
||||
for obj in [o1, o2]:
|
||||
@ -127,16 +154,48 @@ def test_ModuleMeta():
|
||||
# HACK: atm. disabled to fix all other problems first.
|
||||
assert check_order + sorted(check_order)
|
||||
|
||||
o1 = newclass1('o1', logger, {'.description':''}, srv)
|
||||
o2 = newclass2('o2', logger, {'.description':''}, srv)
|
||||
# check for inital updates working properly
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
expectedBeforeStart = {'target': 0.0, 'status': [Drivable.Status.IDLE, ''],
|
||||
'param1': False, 'param2': 1.0, 'a1': 0.0, 'a2': True, 'pollinterval': 5.0,
|
||||
'value': 'first'}
|
||||
assert updates.pop('o1') == expectedBeforeStart
|
||||
o1.earlyInit()
|
||||
event = threading.Event()
|
||||
o1.startModule(event.set)
|
||||
event.wait()
|
||||
# should contain polled values
|
||||
expectedAfterStart = {'status': [Drivable.Status.IDLE, ''],
|
||||
'value': 'second'}
|
||||
assert updates.pop('o1') == expectedAfterStart
|
||||
|
||||
# check in addition if parameters are written
|
||||
o2 = Newclass2('o2', logger, {'.description':'', 'a1': 2.7}, srv)
|
||||
# no update for b2, as this has to be written
|
||||
expectedBeforeStart['a1'] = 2.7
|
||||
assert updates.pop('o2') == expectedBeforeStart
|
||||
o2.earlyInit()
|
||||
event = threading.Event()
|
||||
o2.startModule(event.set)
|
||||
event.wait()
|
||||
# value has changed type, b2 and a1 are written
|
||||
expectedAfterStart.update(value=0, b2=True, a1=2.7)
|
||||
assert updates.pop('o2') == expectedAfterStart
|
||||
assert o2._a1_written == 2.7
|
||||
assert o2._b2_written is True
|
||||
|
||||
assert not updates
|
||||
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
o2 = Newclass2('o2', logger, {'.description':''}, srv)
|
||||
assert o2.parameters['a1'].datatype.unit == 'deg/s'
|
||||
o2 = newclass2('o2', logger, {'.description':'', 'value.unit':'mm', 'param2.unit':'mm'}, srv)
|
||||
o2 = Newclass2('o2', logger, {'.description':'', 'value.unit':'mm', 'param2.unit':'mm'}, srv)
|
||||
# check datatype is not shared
|
||||
assert o1.parameters['param2'].datatype.unit == 'Ohm'
|
||||
assert o2.parameters['param2'].datatype.unit == 'mm'
|
||||
# check '$' in unit works properly
|
||||
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||
cfg = newclass2.configurables
|
||||
cfg = Newclass2.configurables
|
||||
assert set(cfg.keys()) == {'export', 'group', 'description',
|
||||
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
|
||||
@ -147,8 +206,8 @@ def test_ModuleMeta():
|
||||
'description'}
|
||||
|
||||
# check on the level of classes
|
||||
# this checks newclass1 too, as it is inherited by newclass2
|
||||
for baseclass in newclass2.__mro__:
|
||||
# this checks Newclass1 too, as it is inherited by Newclass2
|
||||
for baseclass in Newclass2.__mro__:
|
||||
# every cmd/param has to be collected to accessibles
|
||||
acs = getattr(baseclass, 'accessibles', None)
|
||||
if issubclass(baseclass, Module):
|
||||
|
@ -27,6 +27,7 @@ import pytest
|
||||
|
||||
from secop.datatypes import BoolType, IntRange
|
||||
from secop.params import Command, Override, Parameter, Parameters
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
def test_Command():
|
||||
@ -55,7 +56,7 @@ def test_Parameter():
|
||||
p2 = Parameter('description2', datatype=IntRange(), constant=1)
|
||||
assert p1 != p2
|
||||
assert p1.ctr != p2.ctr
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ProgrammingError):
|
||||
Parameter(None, datatype=float)
|
||||
p3 = p1.copy()
|
||||
assert p1.ctr != p3.ctr
|
||||
|
@ -157,9 +157,11 @@ class Module:
|
||||
def isBusy(self):
|
||||
return self.is_busy
|
||||
|
||||
def pollOne(self, pname):
|
||||
def pollOneParam(self, pname):
|
||||
getattr(self, 'read_' + pname)()
|
||||
|
||||
writeOrPoll = pollOneParam
|
||||
|
||||
def __repr__(self):
|
||||
rdict = self.__dict__.copy()
|
||||
rdict.pop('parameters')
|
||||
|
Loading…
x
Reference in New Issue
Block a user