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:
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
"""
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user