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:
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
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

View File

@ -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

View File

@ -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):

View File

@ -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:

View File

@ -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.