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:
zolliker 2019-12-10 11:36:40 +01:00
parent 44eeea1159
commit 1521e0a34b
9 changed files with 285 additions and 115 deletions

View File

@ -20,12 +20,36 @@
# ***************************************************************************** # *****************************************************************************
"""command handler """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. 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 import re
from secop.metaclass import Done from secop.metaclass import Done
from secop.errors import ProgrammingError
@ -113,34 +137,38 @@ class CmdParser:
class ChangeWrapper: 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 self._module = module
setattr(self, pname, value) for pname, value in valuedict.items():
setattr(self, pname, value)
def __getattr__(self, key): def __getattr__(self, key):
"""get values from module for unknown keys""" """get current values from _module for unchanged parameters"""
return getattr(self._module, key) return getattr(self._module, key)
def apply(self, module): def __contains__(self, pname):
"""set only changed values""" """check whether a specific parameter is to be changed"""
for k, v in self.__dict__.items(): return pname in self.__dict__
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')
class CmdHandlerBase: class CmdHandlerBase:
"""generic command handler""" """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): def __init__(self, group):
# group is used for calling the proper analyze_<group> and change_<group> methods # group is used for calling the proper analyze_<group> and change_<group> methods
self.group = group self.group = group
self.parameters = {} self.parameters = set()
self._module_class = None
def parse_reply(self, reply): def parse_reply(self, reply):
"""return values from a raw reply""" """return values from a raw reply"""
@ -179,11 +207,11 @@ class CmdHandlerBase:
and registers the parameter in this handler and registers the parameter in this handler
""" """
if not modclass in self.parameters: self._module_class = self._module_class or modclass
self.parameters[modclass] = [] if self._module_class != modclass:
# Make sure that parameters from different module classes are not mixed raise ProgrammingError("the handler '%s' for '%s.%s' is already used in module '%s'"
# (not sure if this might happen) % (self.group, modclass.__name__, pname, self._module_class.__name__))
self.parameters[modclass].append(pname) self.parameters.add(pname)
return self.read return self.read
def read(self, module): def read(self, module):
@ -196,14 +224,15 @@ class CmdHandlerBase:
reply = self.send_command(module) reply = self.send_command(module)
# convert them to parameters # convert them to parameters
getattr(module, 'analyze_' + self.group)(*reply) 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: if module.parameters[pname].readerror:
# clear errors on parameters, which were not updated. # clear errors on parameters, which were not updated.
# this will also inform all activated clients # this will also inform all activated clients
setattr(module, pname, getattr(module, pname)) setattr(module, pname, getattr(module, pname))
except Exception as e: except Exception as e:
# set all parameters of this handler to error # set all parameters of this handler to error
for pname in self.parameters[module.__class__]: for pname in self.parameters:
module.setError(pname, e) module.setError(pname, e)
raise raise
return Done # parameters should be updated already return Done # parameters should be updated already
@ -215,23 +244,35 @@ class CmdHandlerBase:
""" """
def wfunc(module, value, cmd=self, pname=pname): def wfunc(module, value, cmd=self, pname=pname):
# do a read of the current hw values cmd.write(module, {pname: value})
values = cmd.send_command(module) return Done
# 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
return wfunc 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): class CmdHandler(CmdHandlerBase):
"""more evolved command handler """more evolved command handler

View File

@ -119,7 +119,6 @@ class ModuleMeta(PropertyMeta):
"and read_%s" % (pname, pname)) "and read_%s" % (pname, pname))
rfunc = handler rfunc = handler
else: else:
rfunc = attrs.get('read_' + pname, None)
for base in bases: for base in bases:
if rfunc is not None: if rfunc is not None:
break break

View File

@ -28,7 +28,7 @@ from collections import OrderedDict
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \ from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
StringType, TupleOf, get_datatype, ArrayOf, TextType 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 import formatException, formatExtendedStack, mkthread
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.metaclass import ModuleMeta from secop.metaclass import ModuleMeta
@ -178,9 +178,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
(self.name, k, ', '.join(self.parameters.keys()))) (self.name, k, ', '.join(self.parameters.keys())))
# 4) complain if a Parameter entry has no default value and # 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(): 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 pobj.default is None:
if not pobj.poll: if not pobj.poll:
raise ConfigError('Module %s: Parameter %r has no default ' 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 # when not all hardware parameters are read because of startup timeout
pobj.value = pobj.datatype(pobj.datatype.default) pobj.value = pobj.datatype(pobj.datatype.default)
else: 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: # 5) 'apply' config:
# pass values through the datatypes and store as attributes # 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__) self.log.debug('empty %s.startModule()' % self.__class__.__name__)
started_callback() started_callback()
def pollOne(self, pname): def pollOneParam(self, pname):
"""call read function and handle error logging""" """poll parameter <pname> with proper error handling"""
try: 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: except SECoPError as e:
self.log.error(str(e)) self.log.error(str(e))
except Exception as e: except Exception as e:
@ -285,7 +331,7 @@ class Readable(Module):
) )
parameters = { parameters = {
'value': Parameter('current value of the Module', readonly=True, 'value': Parameter('current value of the Module', readonly=True,
default=0., datatype=FloatRange(), datatype=FloatRange(),
poll=True, poll=True,
), ),
'pollinterval': Parameter('sleeptime between polls', default=5, 'pollinterval': Parameter('sleeptime between polls', default=5,
@ -320,6 +366,8 @@ class Readable(Module):
def __pollThread_inner(self, started_callback): def __pollThread_inner(self, started_callback):
"""super simple and super stupid per-module polling thread""" """super simple and super stupid per-module polling thread"""
for pname in list(self.writeDict):
self.writeOrPoll(pname)
i = 0 i = 0
fastpoll = self.pollParams(i) fastpoll = self.pollParams(i)
started_callback() started_callback()
@ -339,7 +387,7 @@ class Readable(Module):
continue continue
if nr % abs(int(pobj.poll)) == 0: if nr % abs(int(pobj.poll)) == 0:
# pollParams every 'pobj.pollParams' iteration # pollParams every 'pobj.pollParams' iteration
self.pollOne(pname) self.pollOneParam(pname)
return False return False
@ -350,7 +398,7 @@ class Writable(Readable):
""" """
parameters = { parameters = {
'target': Parameter('target value of the Module', '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: nr % abs(int(pobj.poll))) == 0:
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy) # poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
# otherwise poll every 'pobj.poll' iteration # otherwise poll every 'pobj.poll' iteration
self.pollOne(pname) self.pollOneParam(pname)
return fastpoll return fastpoll
def do_stop(self): def do_stop(self):

View File

@ -26,7 +26,7 @@ from collections import OrderedDict
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \ from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
NoneOr, TextType, IntRange NoneOr, TextType, IntRange
from secop.errors import ProgrammingError from secop.errors import ProgrammingError, BadValueError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
@ -114,6 +114,8 @@ class Parameter(Accessible):
settable=False, default=False), settable=False, default=False),
'handler': Property('[internal] overload the standard read and write functions', 'handler': Property('[internal] overload the standard read and write functions',
ValueType(), export=False, default=None, mandatory=False, settable=False), 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): 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 ()???) # goodie: make an instance from a class (forgotten ()???)
datatype = datatype() datatype = datatype()
else: else:
raise ValueError( raise ProgrammingError(
'datatype MUST be derived from class DataType!') 'datatype MUST be derived from class DataType!')
kwds['description'] = description kwds['description'] = description
@ -139,6 +141,9 @@ class Parameter(Accessible):
if self.handler and not self.poll: if self.handler and not self.poll:
self.properties['poll'] = True 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: if self.constant is not None:
self.properties['readonly'] = True self.properties['readonly'] = True
# The value of the `constant` property should be the # The value of the `constant` property should be the
@ -233,6 +238,12 @@ class Override(CountedObj):
constant = obj.datatype(self.kwds.pop('constant')) constant = obj.datatype(self.kwds.pop('constant'))
self.kwds['constant'] = obj.datatype.export_value(constant) self.kwds['constant'] = obj.datatype.export_value(constant)
self.kwds['readonly'] = True 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) props.update(self.kwds)
if self.reorder: if self.reorder:

View File

@ -186,7 +186,7 @@ class Poller(PollerBase):
mininterval = interval mininterval = interval
due = max(lastdue + interval, pobj.timestamp + interval * 0.5) due = max(lastdue + interval, pobj.timestamp + interval * 0.5)
if now >= due: if now >= due:
module.pollOne(pname) module.pollOneParam(pname)
done = True done = True
lastdue = due lastdue = due
due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5)) 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 _, queue in sorted(self.queues.items()): # do SLOW polls first
for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue): for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue):
lastdue = time.time() lastdue = time.time()
module.pollOne(pname) module.writeOrPoll(pname)
due = lastdue + min(self.maxwait, module.pollinterval * factor) due = lastdue + min(self.maxwait, module.pollinterval * factor)
# in python 3 comparing tuples need some care, as not all objects # in python 3 comparing tuples need some care, as not all objects
# are comparable. Inserting a unique idx solves the problem. # are comparable. Inserting a unique idx solves the problem.

View File

@ -26,6 +26,7 @@ import pytest
from secop.commandhandler import CmdParser, CmdHandler from secop.commandhandler import CmdParser, CmdHandler
from secop.modules import Module, Parameter from secop.modules import Module, Parameter
from secop.datatypes import FloatRange, StringType, IntRange, Property from secop.datatypes import FloatRange, StringType, IntRange, Property
from secop.errors import ProgrammingError
@pytest.mark.parametrize('fmt, text, values, text2', [ @pytest.mark.parametrize('fmt, text, values, text2', [
('%d,%d', '2,3', [2,3], None), ('%d,%d', '2,3', [2,3], None),
@ -99,7 +100,7 @@ def test_CmdHandler():
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d') group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
class TestModule(Module): class Module1(Module):
properties = { properties = {
'channel': Property('the channel', IntRange(), default=3), 'channel': Property('the channel', IntRange(), default=3),
'loop': Property('the loop', IntRange(), default=2), 'loop': Property('the loop', IntRange(), default=2),
@ -130,7 +131,8 @@ def test_CmdHandler():
data = Data() data = Data()
updates = {} 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 updates.clear() # get rid of updates from initialisation
# for sendRecv # for sendRecv
@ -182,3 +184,10 @@ def test_CmdHandler():
assert module.real == updates.pop('real') == 12.3 assert module.real == updates.pop('real') == 12.3
assert data.empty() assert data.empty()
assert not updates 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),
}

View File

@ -17,6 +17,7 @@
# #
# Module authors: # Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> # Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""test data types.""" """test data types."""
@ -25,93 +26,119 @@
#import pytest #import pytest
import threading import threading
from secop.datatypes import BoolType, EnumType, FloatRange from secop.datatypes import BoolType, FloatRange, StringType
from secop.metaclass import ModuleMeta
from secop.modules import Communicator, Drivable, Module from secop.modules import Communicator, Drivable, Module
from secop.params import Command, Override, Parameter 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(): def test_Communicator():
logger = type('LoggerStub', (object,), dict( o = Communicator('communicator', LoggerStub(), {'.description':''}, ServerStub({}))
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.earlyInit() o.earlyInit()
o.initModule() o.initModule()
event = threading.Event() event = threading.Event()
o.startModule(event.set) o.startModule(event.set)
assert event.is_set() # event should be set immediately assert event.is_set() # event should be set immediately
def test_ModuleMeta(): def test_ModuleMeta():
# pylint: disable=too-many-function-args class Newclass1(Drivable):
newclass1 = ModuleMeta.__new__(ModuleMeta, 'TestDrivable', (Drivable,), { parameters = {
"parameters" : {
'pollinterval': Override(reorder=True), 'pollinterval': Override(reorder=True),
'param1' : Parameter('param1', datatype=BoolType(), default=False), 'param1' : Parameter('param1', datatype=BoolType(), default=False),
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True), 'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
"cmd": Command('stuff', argument=BoolType(), result=BoolType()) "cmd": Command('stuff', argument=BoolType(), result=BoolType())
}, }
"commands": { commands = {
# intermixing parameters with commands is not recommended, # intermixing parameters with commands is not recommended,
# but acceptable for influencing the order # but acceptable for influencing the order
'a1': Parameter('a1', datatype=BoolType(), default=False), 'a1': Parameter('a1', datatype=BoolType(), default=False),
'a2': Parameter('a2', datatype=BoolType(), default=True), '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()), 'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
}, }
"do_cmd": lambda self, arg: not arg, pollerClass = BasicPoller
"do_cmd2": lambda self, arg: not arg,
"read_param1": lambda self, *args: True, def do_cmd(self, arg):
"read_param2": lambda self, *args: False, return not arg
"read_a1": lambda self, *args: True,
"read_a2": lambda self, *args: False, def do_cmd2(self, arg):
"read_value": lambda self, *args: True, return not arg
"init": lambda self, *args: [None for self.accessibles['value'].datatype in [EnumType('value', OK=1, Bad=2)]],
}) 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 # first inherited accessibles, then Overrides with reorder=True and new accessibles
sortcheck1 = ['value', 'status', 'target', 'pollinterval', sortcheck1 = ['value', 'status', 'target', 'pollinterval',
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2'] 'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
# pylint: disable=too-many-function-args class Newclass2(Newclass1):
newclass2 = ModuleMeta.__new__(ModuleMeta, 'UpperClass', (newclass1,), { parameters = {
"parameters": {
'cmd2': Override('another stuff'), 'cmd2': Override('another stuff'),
'value': Override(datatype=FloatRange(unit='deg'), reorder=True), 'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True), 'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
'b2': Parameter('a2', datatype=BoolType(), default=True), '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', sortcheck2 = ['value', 'status', 'target', 'pollinterval',
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2'] 'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2']
logger = type('LoggerStub', (object,), dict( logger = LoggerStub()
debug = lambda self, *a: print(*a), updates = {}
info = lambda self, *a: print(*a), srv = ServerStub(updates)
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,
))()
params_found = set() # set of instance accessibles params_found = set() # set of instance accessibles
objects = [] objects = []
for newclass, sortcheck in [(newclass1, sortcheck1), (newclass2, sortcheck2)]: for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
o1 = newclass('o1', logger, {'.description':''}, srv) o1 = newclass('o1', logger, {'.description':''}, srv)
o2 = newclass('o2', logger, {'.description':''}, srv) o2 = newclass('o2', logger, {'.description':''}, srv)
for obj in [o1, o2]: for obj in [o1, o2]:
@ -127,16 +154,48 @@ def test_ModuleMeta():
# HACK: atm. disabled to fix all other problems first. # HACK: atm. disabled to fix all other problems first.
assert check_order + sorted(check_order) assert check_order + sorted(check_order)
o1 = newclass1('o1', logger, {'.description':''}, srv) # check for inital updates working properly
o2 = newclass2('o2', logger, {'.description':''}, srv) 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' 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 # check datatype is not shared
assert o1.parameters['param2'].datatype.unit == 'Ohm' assert o1.parameters['param2'].datatype.unit == 'Ohm'
assert o2.parameters['param2'].datatype.unit == 'mm' assert o2.parameters['param2'].datatype.unit == 'mm'
# check '$' in unit works properly # check '$' in unit works properly
assert o2.parameters['a1'].datatype.unit == 'mm/s' assert o2.parameters['a1'].datatype.unit == 'mm/s'
cfg = newclass2.configurables cfg = Newclass2.configurables
assert set(cfg.keys()) == {'export', 'group', 'description', assert set(cfg.keys()) == {'export', 'group', 'description',
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
@ -147,8 +206,8 @@ def test_ModuleMeta():
'description'} 'description'}
# check on the level of classes # check on the level of classes
# this checks newclass1 too, as it is inherited by newclass2 # this checks Newclass1 too, as it is inherited by Newclass2
for baseclass in newclass2.__mro__: for baseclass in Newclass2.__mro__:
# every cmd/param has to be collected to accessibles # every cmd/param has to be collected to accessibles
acs = getattr(baseclass, 'accessibles', None) acs = getattr(baseclass, 'accessibles', None)
if issubclass(baseclass, Module): if issubclass(baseclass, Module):

View File

@ -27,6 +27,7 @@ import pytest
from secop.datatypes import BoolType, IntRange from secop.datatypes import BoolType, IntRange
from secop.params import Command, Override, Parameter, Parameters from secop.params import Command, Override, Parameter, Parameters
from secop.errors import ProgrammingError
def test_Command(): def test_Command():
@ -55,7 +56,7 @@ def test_Parameter():
p2 = Parameter('description2', datatype=IntRange(), constant=1) p2 = Parameter('description2', datatype=IntRange(), constant=1)
assert p1 != p2 assert p1 != p2
assert p1.ctr != p2.ctr assert p1.ctr != p2.ctr
with pytest.raises(ValueError): with pytest.raises(ProgrammingError):
Parameter(None, datatype=float) Parameter(None, datatype=float)
p3 = p1.copy() p3 = p1.copy()
assert p1.ctr != p3.ctr assert p1.ctr != p3.ctr

View File

@ -157,9 +157,11 @@ class Module:
def isBusy(self): def isBusy(self):
return self.is_busy return self.is_busy
def pollOne(self, pname): def pollOneParam(self, pname):
getattr(self, 'read_' + pname)() getattr(self, 'read_' + pname)()
writeOrPoll = pollOneParam
def __repr__(self): def __repr__(self):
rdict = self.__dict__.copy() rdict = self.__dict__.copy()
rdict.pop('parameters') rdict.pop('parameters')