From 8123d2189795ad5ebd1cfa7858202e2f106e0d6b Mon Sep 17 00:00:00 2001 From: Enrico Faulhaber Date: Thu, 26 Jan 2017 17:05:25 +0100 Subject: [PATCH] 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 --- etc/cryo.cfg | 8 ++ secop/devices/core.py | 137 ++++++++++++++++++++--------------- secop/devices/cryo.py | 21 ++++-- secop/protocol/dispatcher.py | 12 ++- 4 files changed, 105 insertions(+), 73 deletions(-) diff --git a/etc/cryo.cfg b/etc/cryo.cfg index 70250a9..7375644 100644 --- a/etc/cryo.cfg +++ b/etc/cryo.cfg @@ -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 + diff --git a/secop/devices/core.py b/secop/devices/core.py index 155df32..f17c7a8 100644 --- a/secop/devices/core.py +++ b/secop/devices/core.py @@ -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 '.=' + 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 '. = ' + 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 + """ + diff --git a/secop/devices/cryo.py b/secop/devices/cryo.py index 263d427..a7f8948 100644 --- a/secop/devices/cryo.py +++ b/secop/devices/cryo.py @@ -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( diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 9dfd8dc..b6ed582 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -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