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]
# some (non-defaut) module properties
.group=very important/stuff
# class of module:
class=devices.cryo.Cryostat
# some parameters
jitter=0.1
T_start=10.0
target=10.0
@ -27,3 +32,6 @@ tolerance=0.1
window=30
timeout=900
# some (non-default) parameter properties
pollinterval.export=False

View File

@ -52,7 +52,8 @@ class PARAM(object):
default=Ellipsis,
unit=None,
readonly=True,
export=True):
export=True,
group=''):
if isinstance(description, PARAM):
# make a copy of a PARAM object
self.__dict__.update(description.__dict__)
@ -63,6 +64,7 @@ class PARAM(object):
self.unit = unit
self.readonly = readonly
self.export = export
self.group = group
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
@ -71,15 +73,22 @@ class PARAM(object):
return '%s(%s)' % (self.__class__.__name__, ', '.join(
['%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
return dict(
description=self.description,
unit=self.unit,
readonly=self.readonly,
value=self.value,
timestamp=format_time(self.timestamp) if self.timestamp else None,
validator=validator_to_str(self.validator), )
res = dict(
description=self.description,
readonly=self.readonly,
validator=validator_to_str(self.validator),
)
if self.unit:
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...)
@ -113,8 +122,8 @@ class DeviceMeta(type):
if '__constructed__' in attrs:
return newtype
# merge PARAM and CMDS from all sub-classes
for entry in ['PARAMS', 'CMDS']:
# merge PROPERTIES, PARAM and CMDS from all sub-classes
for entry in ['PROPERTIES', 'PARAMS', 'CMDS']:
newentry = {}
for base in reversed(bases):
if hasattr(base, entry):
@ -181,17 +190,17 @@ class DeviceMeta(type):
# also collect/update information about CMD's
setattr(newtype, 'CMDS', getattr(newtype, 'CMDS', {}))
for name in attrs:
if name.startswith('do'):
if name[2:] in newtype.CMDS:
if name.startswith('do_'):
if name[3:] in newtype.CMDS:
continue
value = getattr(newtype, name)
if isinstance(value, types.MethodType):
argspec = inspect.getargspec(value)
if argspec[0] and argspec[0][0] == 'self':
del argspec[0][0]
newtype.CMDS[name[2:]] = CMD(
newtype.CMDS[name[3:]] = CMD(
getattr(value, '__doc__'), argspec.args,
None) # XXX: find resulttype!
None) # XXX: how to find resulttype?
attrs['__constructed__'] = True
return newtype
@ -209,12 +218,17 @@ class DeviceMeta(type):
class Device(object):
"""Basic Device, doesn't do much"""
__metaclass__ = DeviceMeta
# PARAMS and CMDS are auto-merged upon subclassing
PARAMS = {
"interfaceclass": PARAM("protocol defined interface class",
default="Device",
validator=str),
# static PROPERTIES, definitions in derived classes should overwrite earlier ones.
# how to configure some stuff which makes sense to take from configfile???
PROPERTIES = {
'group' : None, # some Modules may be grouped together
'meaning' : None, # XXX: ???
'priority' : None, # XXX: ???
'visibility' : None, # XXX: ????
# what else?
}
# PARAMS and CMDS are auto-merged upon subclassing
PARAMS = {}
CMDS = {}
DISPATCHER = None
@ -225,16 +239,41 @@ class Device(object):
self.name = devname
# make local copies of PARAMS
params = {}
for k, v in self.PARAMS.items():
for k, v in self.PARAMS.items()[:]:
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__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
params['implementationclass'] = PARAM(
'implementation specific class name',
default=myclassname,
validator=str)
self.PROPERTIES['implementation'] = myclassname
self.PROPERTIES['interfaces'] = [b.__name__ for b in mycls.__mro__
if b.__module__.startswith('secop.devices.core')]
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
# only accept config items specified in PARAMS
for k, v in cfgdict.items():
@ -286,31 +325,13 @@ class Readable(Device):
providing the readonly parameter 'value' and 'status'
"""
PARAMS = {
'interfaceclass': PARAM(
'protocol defined interface class',
default="Readable",
validator=str),
'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, ''),
'value': PARAM('current value of the device', readonly=True, default=0.),
'pollinterval': PARAM('sleeptime between polls', default=5,
readonly=False, validator=floatrange(0.1, 120), ),
'status': PARAM('current status of the device', default=(status.OK, ''),
validator=vector(
enum(**{
'idle': status.OK,
'IDLE': status.OK,
'BUSY': status.BUSY,
'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE,
@ -343,25 +364,25 @@ class Driveable(Readable):
providing a settable 'target' parameter to those of a Readable
"""
PARAMS = {
"interfaceclass": PARAM("protocol defined interface class",
default="Driveable",
validator=str,
),
'target': PARAM('target value of the device',
default=0.,
readonly=False,
'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,
but there may be modules which _start_ the action here
"""
def doStop(self):
def do_stop(self):
"""Testing command implementation
wait a second"""
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.lib import clamp, mkthread
class CryoBase(Driveable):
pass
class Cryostat(Driveable):
class Cryostat(CryoBase):
"""simulated cryostat with:
- heat capacity of the sample
@ -61,12 +63,15 @@ class Cryostat(Driveable):
maxpower=PARAM("Maximum heater power",
validator=nonnegative, default=1, unit="W",
readonly=False,
group='heater',
),
heater=PARAM("current heater setting",
validator=floatrange(0, 100), default=0, unit="%",
group='heater',
),
heaterpower=PARAM("current heater power",
validator=nonnegative, default=0, unit="W",
group='heater',
),
target=PARAM("target temperature",
validator=nonnegative, default=0, unit="K",
@ -78,19 +83,19 @@ class Cryostat(Driveable):
pid=PARAM("regulation coefficients",
validator=vector(nonnegative, floatrange(0, 100), floatrange(0, 100)),
default=(40, 10, 2), readonly=False,
# export=False,
group='pid',
),
p=PARAM("regulation coefficient 'p'",
validator=nonnegative, default=40, unit="%/K", readonly=False,
export=False,
group='pid',
),
i=PARAM("regulation coefficient 'i'",
validator=floatrange(0, 100), default=10, readonly=False,
export=False,
group='pid',
),
d=PARAM("regulation coefficient 'd'",
validator=floatrange(0, 100), default=2, readonly=False,
export=False,
group='pid',
),
mode=PARAM("mode of regulation",
validator=enum('ramp', 'pid', 'openloop'), default='ramp',
@ -98,21 +103,21 @@ class Cryostat(Driveable):
),
pollinterval=PARAM("polling interval",
validator=positive, default=5,
export=False,
),
tolerance=PARAM("temperature range for stability checking",
validator=floatrange(0, 100), default=0.1, unit='K',
readonly=False,
group='window',
),
window=PARAM("time window for stability checking",
validator=floatrange(1, 900), default=30, unit='s',
readonly=False,
export=False,
group='window',
),
timeout=PARAM("max waiting time for stabilisation check",
validator=floatrange(1, 36000), default=900, unit='s',
readonly=False,
export=False,
group='window',
),
)
CMDS = dict(

View File

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