implement configurable module-properties

+ make parameter-properties configurable
+ better derivation of automatic properties
+ implement 'group' properties (how to display in gui???
+ clean up descriptive data by omitting unset and live properties

Change-Id: Icd2b6e91e09037e9d4a8d6ad88483f8509a2cf5f
This commit is contained in:
Enrico Faulhaber
2017-01-26 17:05:25 +01:00
parent 6ec30e38e8
commit 8123d21897
4 changed files with 105 additions and 73 deletions

View File

@ -11,7 +11,12 @@ encoding=demo
[device cryo] [device cryo]
# some (non-defaut) module properties
.group=very important/stuff
# class of module:
class=devices.cryo.Cryostat class=devices.cryo.Cryostat
# some parameters
jitter=0.1 jitter=0.1
T_start=10.0 T_start=10.0
target=10.0 target=10.0
@ -27,3 +32,6 @@ tolerance=0.1
window=30 window=30
timeout=900 timeout=900
# some (non-default) parameter properties
pollinterval.export=False

View File

@ -52,7 +52,8 @@ class PARAM(object):
default=Ellipsis, default=Ellipsis,
unit=None, unit=None,
readonly=True, readonly=True,
export=True): export=True,
group=''):
if isinstance(description, PARAM): if isinstance(description, PARAM):
# make a copy of a PARAM object # make a copy of a PARAM object
self.__dict__.update(description.__dict__) self.__dict__.update(description.__dict__)
@ -63,6 +64,7 @@ class PARAM(object):
self.unit = unit self.unit = unit
self.readonly = readonly self.readonly = readonly
self.export = export self.export = export
self.group = group
# internal caching: value and timestamp of last change... # internal caching: value and timestamp of last change...
self.value = default self.value = default
self.timestamp = 0 self.timestamp = 0
@ -71,15 +73,22 @@ class PARAM(object):
return '%s(%s)' % (self.__class__.__name__, ', '.join( return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())])) ['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
def as_dict(self): def as_dict(self, static_only=False):
# used for serialisation only # used for serialisation only
return dict( res = dict(
description=self.description, description=self.description,
unit=self.unit, readonly=self.readonly,
readonly=self.readonly, validator=validator_to_str(self.validator),
value=self.value, )
timestamp=format_time(self.timestamp) if self.timestamp else None, if self.unit:
validator=validator_to_str(self.validator), ) res['unit'] = self.unit
if self.group:
res['group'] = self.group
if not static_only:
res['value'] = self.value
if self.timestamp:
res['timestamp'] = format_time(self.timestamp)
return res
# storage for CMDs settings (description + call signature...) # storage for CMDs settings (description + call signature...)
@ -113,8 +122,8 @@ class DeviceMeta(type):
if '__constructed__' in attrs: if '__constructed__' in attrs:
return newtype return newtype
# merge PARAM and CMDS from all sub-classes # merge PROPERTIES, PARAM and CMDS from all sub-classes
for entry in ['PARAMS', 'CMDS']: for entry in ['PROPERTIES', 'PARAMS', 'CMDS']:
newentry = {} newentry = {}
for base in reversed(bases): for base in reversed(bases):
if hasattr(base, entry): if hasattr(base, entry):
@ -181,17 +190,17 @@ class DeviceMeta(type):
# also collect/update information about CMD's # also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {})) setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
for name in attrs: for name in attrs:
if name.startswith('do'): if name.startswith('do_'):
if name[2:] in newtype.CMDS: if name[3:] in newtype.CMDS:
continue continue
value = getattr(newtype, name) value = getattr(newtype, name)
if isinstance(value, types.MethodType): if isinstance(value, types.MethodType):
argspec = inspect.getargspec(value) argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self': if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0] del argspec[0][0]
newtype.CMDS[name[2:]] = CMD( newtype.CMDS[name[3:]] = CMD(
getattr(value, '__doc__'), argspec.args, getattr(value, '__doc__'), argspec.args,
None) # XXX: find resulttype! None) # XXX: how to find resulttype?
attrs['__constructed__'] = True attrs['__constructed__'] = True
return newtype return newtype
@ -209,12 +218,17 @@ class DeviceMeta(type):
class Device(object): class Device(object):
"""Basic Device, doesn't do much""" """Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta __metaclass__ = DeviceMeta
# PARAMS and CMDS are auto-merged upon subclassing # static PROPERTIES, definitions in derived classes should overwrite earlier ones.
PARAMS = { # how to configure some stuff which makes sense to take from configfile???
"interfaceclass": PARAM("protocol defined interface class", PROPERTIES = {
default="Device", 'group' : None, # some Modules may be grouped together
validator=str), 'meaning' : None, # XXX: ???
'priority' : None, # XXX: ???
'visibility' : None, # XXX: ????
# what else?
} }
# PARAMS and CMDS are auto-merged upon subclassing
PARAMS = {}
CMDS = {} CMDS = {}
DISPATCHER = None DISPATCHER = None
@ -225,16 +239,41 @@ class Device(object):
self.name = devname self.name = devname
# make local copies of PARAMS # make local copies of PARAMS
params = {} params = {}
for k, v in self.PARAMS.items(): for k, v in self.PARAMS.items()[:]:
params[k] = PARAM(v) params[k] = PARAM(v)
self.PARAMS = params
# check and apply properties specified in cfgdict
# moduleproperties are to be specified as '.<propertyname>=<propertyvalue>'
for k, v in cfgdict.items():
if k[0] == '.':
if k[1:] in self.PROPERTIES:
self.PROPERTIES[k[1:]] = v
del cfgdict[k]
# derive automatic properties
mycls = self.__class__ mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
params['implementationclass'] = PARAM( self.PROPERTIES['implementation'] = myclassname
'implementation specific class name', self.PROPERTIES['interfaces'] = [b.__name__ for b in mycls.__mro__
default=myclassname, if b.__module__.startswith('secop.devices.core')]
validator=str) self.PROPERTIES['interface'] = self.PROPERTIES['interfaces'][0]
# remove unset (default) module properties
for k,v in self.PROPERTIES.items():
if v == None:
del self.PROPERTIES[k]
# check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>'
for k,v in cfgdict.items()[:]:
if '.' in k[1:]:
paramname, propname = k.split('.', 1)
if paramname in self.PARAMS:
paramobj = self.PARAMS[paramname]
if hasattr(paramobj, propname):
setattr(paramobj, propname, v)
del cfgdict[k]
self.PARAMS = params
# check config for problems # check config for problems
# only accept config items specified in PARAMS # only accept config items specified in PARAMS
for k, v in cfgdict.items(): for k, v in cfgdict.items():
@ -286,31 +325,13 @@ class Readable(Device):
providing the readonly parameter 'value' and 'status' providing the readonly parameter 'value' and 'status'
""" """
PARAMS = { PARAMS = {
'interfaceclass': PARAM( 'value': PARAM('current value of the device', readonly=True, default=0.),
'protocol defined interface class', 'pollinterval': PARAM('sleeptime between polls', default=5,
default="Readable", readonly=False, validator=floatrange(0.1, 120), ),
validator=str), 'status': PARAM('current status of the device', default=(status.OK, ''),
'value': PARAM(
'current value of the device', readonly=True, default=0.),
'pollinterval': PARAM(
'sleeptime between polls',
readonly=False,
default=5,
validator=floatrange(0.1, 120), ),
# 'status': PARAM('current status of the device', default=status.OK,
# validator=enum(**{'idle': status.OK,
# 'BUSY': status.BUSY,
# 'WARN': status.WARN,
# 'UNSTABLE': status.UNSTABLE,
# 'ERROR': status.ERROR,
# 'UNKNOWN': status.UNKNOWN}),
# readonly=True),
'status': PARAM(
'current status of the device',
default=(status.OK, ''),
validator=vector( validator=vector(
enum(**{ enum(**{
'idle': status.OK, 'IDLE': status.OK,
'BUSY': status.BUSY, 'BUSY': status.BUSY,
'WARN': status.WARN, 'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE, 'UNSTABLE': status.UNSTABLE,
@ -343,25 +364,25 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable providing a settable 'target' parameter to those of a Readable
""" """
PARAMS = { PARAMS = {
"interfaceclass": PARAM("protocol defined interface class", 'target': PARAM('target value of the device', default=0., readonly=False,
default="Driveable",
validator=str,
),
'target': PARAM('target value of the device',
default=0.,
readonly=False,
), ),
} }
# XXX: CMDS ???? auto deriving working well enough?
def doStart(self): def do_start(self):
"""normally does nothing, """normally does nothing,
but there may be modules which _start_ the action here but there may be modules which _start_ the action here
""" """
def doStop(self): def do_stop(self):
"""Testing command implementation """Testing command implementation
wait a second""" wait a second"""
time.sleep(1) # for testing ! time.sleep(1) # for testing !
def do_pause(self):
"""if implemented should pause the module
use start to continue movement
"""

View File

@ -30,8 +30,10 @@ from secop.protocol import status
from secop.validators import floatrange, positive, enum, nonnegative, vector from secop.validators import floatrange, positive, enum, nonnegative, vector
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
class CryoBase(Driveable):
pass
class Cryostat(Driveable): class Cryostat(CryoBase):
"""simulated cryostat with: """simulated cryostat with:
- heat capacity of the sample - heat capacity of the sample
@ -61,12 +63,15 @@ class Cryostat(Driveable):
maxpower=PARAM("Maximum heater power", maxpower=PARAM("Maximum heater power",
validator=nonnegative, default=1, unit="W", validator=nonnegative, default=1, unit="W",
readonly=False, readonly=False,
group='heater',
), ),
heater=PARAM("current heater setting", heater=PARAM("current heater setting",
validator=floatrange(0, 100), default=0, unit="%", validator=floatrange(0, 100), default=0, unit="%",
group='heater',
), ),
heaterpower=PARAM("current heater power", heaterpower=PARAM("current heater power",
validator=nonnegative, default=0, unit="W", validator=nonnegative, default=0, unit="W",
group='heater',
), ),
target=PARAM("target temperature", target=PARAM("target temperature",
validator=nonnegative, default=0, unit="K", validator=nonnegative, default=0, unit="K",
@ -78,19 +83,19 @@ class Cryostat(Driveable):
pid=PARAM("regulation coefficients", pid=PARAM("regulation coefficients",
validator=vector(nonnegative, floatrange(0, 100), floatrange(0, 100)), validator=vector(nonnegative, floatrange(0, 100), floatrange(0, 100)),
default=(40, 10, 2), readonly=False, default=(40, 10, 2), readonly=False,
# export=False, group='pid',
), ),
p=PARAM("regulation coefficient 'p'", p=PARAM("regulation coefficient 'p'",
validator=nonnegative, default=40, unit="%/K", readonly=False, validator=nonnegative, default=40, unit="%/K", readonly=False,
export=False, group='pid',
), ),
i=PARAM("regulation coefficient 'i'", i=PARAM("regulation coefficient 'i'",
validator=floatrange(0, 100), default=10, readonly=False, validator=floatrange(0, 100), default=10, readonly=False,
export=False, group='pid',
), ),
d=PARAM("regulation coefficient 'd'", d=PARAM("regulation coefficient 'd'",
validator=floatrange(0, 100), default=2, readonly=False, validator=floatrange(0, 100), default=2, readonly=False,
export=False, group='pid',
), ),
mode=PARAM("mode of regulation", mode=PARAM("mode of regulation",
validator=enum('ramp', 'pid', 'openloop'), default='ramp', validator=enum('ramp', 'pid', 'openloop'), default='ramp',
@ -98,21 +103,21 @@ class Cryostat(Driveable):
), ),
pollinterval=PARAM("polling interval", pollinterval=PARAM("polling interval",
validator=positive, default=5, validator=positive, default=5,
export=False,
), ),
tolerance=PARAM("temperature range for stability checking", tolerance=PARAM("temperature range for stability checking",
validator=floatrange(0, 100), default=0.1, unit='K', validator=floatrange(0, 100), default=0.1, unit='K',
readonly=False, readonly=False,
group='window',
), ),
window=PARAM("time window for stability checking", window=PARAM("time window for stability checking",
validator=floatrange(1, 900), default=30, unit='s', validator=floatrange(1, 900), default=30, unit='s',
readonly=False, readonly=False,
export=False, group='window',
), ),
timeout=PARAM("max waiting time for stabilisation check", timeout=PARAM("max waiting time for stabilisation check",
validator=floatrange(1, 36000), default=900, unit='s', validator=floatrange(1, 36000), default=900, unit='s',
readonly=False, readonly=False,
export=False, group='window',
), ),
) )
CMDS = dict( CMDS = dict(

View File

@ -178,14 +178,14 @@ class Dispatcher(object):
# return a copy of our list # return a copy of our list
return self._export[:] return self._export[:]
def list_module_params(self, modulename): def list_module_params(self, modulename, only_static=False):
self.log.debug('list_module_params(%r)' % modulename) self.log.debug('list_module_params(%r)' % modulename)
if modulename in self._export: if modulename in self._export:
# omit export=False params! # omit export=False params!
res = {} res = {}
for paramname, param in self.get_module(modulename).PARAMS.items(): for paramname, param in self.get_module(modulename).PARAMS.items():
if param.export: if param.export:
res[paramname] = param.as_dict() res[paramname] = param.as_dict(only_static)
self.log.debug('list params for module %s -> %r' % self.log.debug('list params for module %s -> %r' %
(modulename, res)) (modulename, res))
return res return res
@ -211,16 +211,14 @@ class Dispatcher(object):
module = self.get_module(modulename) module = self.get_module(modulename)
# some of these need rework ! # some of these need rework !
dd = { dd = {
'class': module.__class__.__name__, 'parameters': self.list_module_params(modulename, only_static=True),
'bases': [b.__name__ for b in module.__class__.__bases__],
'parameters': self.list_module_params(modulename),
'commands': self.list_module_cmds(modulename), 'commands': self.list_module_cmds(modulename),
'interfaceclass': 'Readable', 'properties' : module.PROPERTIES,
} }
result['modules'][modulename] = dd result['modules'][modulename] = dd
result['equipment_id'] = self.equipment_id result['equipment_id'] = self.equipment_id
result['firmware'] = 'The SECoP playground' result['firmware'] = 'The SECoP playground'
result['version'] = "2016.12" result['version'] = "2017.01"
# XXX: what else? # XXX: what else?
return result return result