removed old style syntax
- removed secop/metaclass.py - moved code from ModuleMeta to modules.HasAccessibles.__init_subclass__ - reworked properties: assignment obj.property = value now always allowed - reworked Parameters and Command to be true descriptors - Command must now be solely used as decorator - renamed 'usercommand' to 'Command' - command methods no longer start with 'do_' - reworked mechanism to determine accessible order: the attribute paramOrder, if given, determines order of accessibles + fixed some issues makeing the IDE more happy + simplified code for StatusType and added a test for it Change-Id: I8045cf38ee6f4d4862428272df0b12a7c8abaca7 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25049 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
ed02131a37
commit
1a8ddbc696
@ -212,13 +212,13 @@ max-locals=50
|
|||||||
max-returns=10
|
max-returns=10
|
||||||
|
|
||||||
# Maximum number of branch for function / method body
|
# Maximum number of branch for function / method body
|
||||||
max-branches=40
|
max-branches=50
|
||||||
|
|
||||||
# Maximum number of statements in function / method body
|
# Maximum number of statements in function / method body
|
||||||
max-statements=150
|
max-statements=150
|
||||||
|
|
||||||
# Maximum number of parents for a class (see R0901).
|
# Maximum number of parents for a class (see R0901).
|
||||||
max-parents=10
|
max-parents=15
|
||||||
|
|
||||||
# Maximum number of attributes for a class (see R0902).
|
# Maximum number of attributes for a class (see R0902).
|
||||||
max-attributes=50
|
max-attributes=50
|
||||||
|
@ -20,13 +20,12 @@ Parameters, Commands and Properties
|
|||||||
...................................
|
...................................
|
||||||
|
|
||||||
.. autoclass:: secop.params.Parameter
|
.. autoclass:: secop.params.Parameter
|
||||||
.. autoclass:: secop.params.usercommand
|
.. autoclass:: secop.params.Command
|
||||||
.. autoclass:: secop.properties.Property
|
.. autoclass:: secop.properties.Property
|
||||||
.. autoclass:: secop.modules.Attached
|
.. autoclass:: secop.modules.Attached
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Datatypes
|
Datatypes
|
||||||
.........
|
.........
|
||||||
|
|
||||||
|
@ -29,11 +29,10 @@
|
|||||||
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
||||||
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
|
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached, Done
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.params import Parameter, Command, Override, usercommand
|
from secop.params import Parameter, Command
|
||||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||||
from secop.metaclass import Done
|
|
||||||
from secop.iohandler import IOHandler, IOHandlerBase
|
from secop.iohandler import IOHandler, IOHandlerBase
|
||||||
from secop.stringio import StringIO, HasIodev
|
from secop.stringio import StringIO, HasIodev
|
||||||
from secop.proxy import SecNode, Proxy, proxy_class
|
from secop.proxy import SecNode, Proxy, proxy_class
|
||||||
|
@ -98,7 +98,7 @@ class DataType(HasProperties):
|
|||||||
def set_properties(self, **kwds):
|
def set_properties(self, **kwds):
|
||||||
"""init datatype properties"""
|
"""init datatype properties"""
|
||||||
try:
|
try:
|
||||||
for k,v in kwds.items():
|
for k, v in kwds.items():
|
||||||
self.setProperty(k, v)
|
self.setProperty(k, v)
|
||||||
self.checkProperties()
|
self.checkProperties()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -151,11 +151,12 @@ class Stub(DataType):
|
|||||||
"""
|
"""
|
||||||
for dtcls in globals().values():
|
for dtcls in globals().values():
|
||||||
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||||
for prop in dtcls.properties.values():
|
for prop in dtcls.propertyDict.values():
|
||||||
stub = prop.datatype
|
stub = prop.datatype
|
||||||
if isinstance(stub, cls):
|
if isinstance(stub, cls):
|
||||||
prop.datatype = globals()[stub.name](*stub.args)
|
prop.datatype = globals()[stub.name](*stub.args)
|
||||||
|
|
||||||
|
|
||||||
# SECoP types:
|
# SECoP types:
|
||||||
|
|
||||||
class FloatRange(DataType):
|
class FloatRange(DataType):
|
||||||
@ -165,16 +166,14 @@ class FloatRange(DataType):
|
|||||||
:param maxval: (property **max**)
|
:param maxval: (property **max**)
|
||||||
:param kwds: any of the properties below
|
:param kwds: any of the properties below
|
||||||
"""
|
"""
|
||||||
properties = {
|
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
|
||||||
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
|
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
|
||||||
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
|
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
|
||||||
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0),
|
extname='absolute_resolution', default=0.0)
|
||||||
extname='absolute_resolution', default=0.0),
|
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
|
||||||
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0),
|
extname='relative_resolution', default=1.2e-7)
|
||||||
extname='relative_resolution', default=1.2e-7),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None, **kwds):
|
def __init__(self, minval=None, maxval=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -212,7 +211,7 @@ class FloatRange(DataType):
|
|||||||
hints['minval'] = hints.pop('min')
|
hints['minval'] = hints.pop('min')
|
||||||
if 'max' in hints:
|
if 'max' in hints:
|
||||||
hints['maxval'] = hints.pop('max')
|
hints['maxval'] = hints.pop('max')
|
||||||
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k,v) for k,v in hints.items()))
|
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items()))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -247,12 +246,10 @@ class IntRange(DataType):
|
|||||||
:param minval: (property **min**)
|
:param minval: (property **min**)
|
||||||
:param maxval: (property **max**)
|
:param maxval: (property **max**)
|
||||||
"""
|
"""
|
||||||
properties = {
|
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
|
||||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
|
max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
|
||||||
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
|
|
||||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
||||||
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
|
# unit = Property('physical unit', StringType(), extname='unit', default='')
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None):
|
def __init__(self, minval=None, maxval=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -278,7 +275,12 @@ class IntRange(DataType):
|
|||||||
raise BadValueError('Can not convert %r to int' % value)
|
raise BadValueError('Can not convert %r to int' % value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'IntRange(%d, %d)' % (self.min, self.max)
|
args = (self.min, self.max)
|
||||||
|
if args[1] == DEFAULT_MAX_INT:
|
||||||
|
args = args[:1]
|
||||||
|
if args[0] == DEFAULT_MIN_INT:
|
||||||
|
args = ()
|
||||||
|
return 'IntRange%s' % repr(args)
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -315,24 +317,23 @@ class ScaledInteger(DataType):
|
|||||||
note: limits are for the scaled float value
|
note: limits are for the scaled float value
|
||||||
the scale is only used for calculating to/from transport serialisation
|
the scale is only used for calculating to/from transport serialisation
|
||||||
"""
|
"""
|
||||||
properties = {
|
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
|
||||||
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
|
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
|
||||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
|
||||||
'max': Property('high limit', FloatRange(), extname='max', mandatory=True),
|
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
absolute_resolution = Property('absolute resolution', FloatRange(0),
|
||||||
'absolute_resolution': Property('absolute resolution', FloatRange(0),
|
extname='absolute_resolution', default=0.0)
|
||||||
extname='absolute_resolution', default=0.0),
|
relative_resolution = Property('relative resolution', FloatRange(0),
|
||||||
'relative_resolution': Property('relative resolution', FloatRange(0),
|
extname='relative_resolution', default=1.2e-7)
|
||||||
extname='relative_resolution', default=1.2e-7),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
scale = float(scale)
|
scale = float(scale)
|
||||||
if absolute_resolution is None:
|
if absolute_resolution is None:
|
||||||
absolute_resolution = scale
|
absolute_resolution = scale
|
||||||
self.set_properties(scale=scale,
|
self.set_properties(
|
||||||
|
scale=scale,
|
||||||
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
||||||
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||||
absolute_resolution=absolute_resolution,
|
absolute_resolution=absolute_resolution,
|
||||||
@ -363,8 +364,8 @@ class ScaledInteger(DataType):
|
|||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='scaled',
|
return self.get_info(type='scaled',
|
||||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
try:
|
try:
|
||||||
@ -384,8 +385,8 @@ class ScaledInteger(DataType):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
hints = self.get_info(scale=float('%g' % self.scale),
|
hints = self.get_info(scale=float('%g' % self.scale),
|
||||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
@ -434,10 +435,11 @@ class EnumType(DataType):
|
|||||||
return EnumType(self._enum)
|
return EnumType(self._enum)
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return {'type': 'enum', 'members':dict((m.name, m.value) for m in self._enum.members)}
|
return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
|
return "EnumType(%r, %s)" % (self._enum.name,
|
||||||
|
', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -460,6 +462,9 @@ class EnumType(DataType):
|
|||||||
def format_value(self, value, unit=None):
|
def format_value(self, value, unit=None):
|
||||||
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
self._enum.name = name
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
for m in self._enum.members:
|
for m in self._enum.members:
|
||||||
other(m)
|
other(m)
|
||||||
@ -471,12 +476,10 @@ class BLOBType(DataType):
|
|||||||
internally treated as bytes
|
internally treated as bytes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
properties = {
|
minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
default=0)
|
||||||
default=0),
|
maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||||
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
mandatory=True)
|
||||||
mandatory=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minbytes=0, maxbytes=None):
|
def __init__(self, minbytes=0, maxbytes=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -538,14 +541,12 @@ class StringType(DataType):
|
|||||||
|
|
||||||
for parameters see properties below
|
for parameters see properties below
|
||||||
"""
|
"""
|
||||||
properties = {
|
minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
extname='minchars', default=0)
|
||||||
extname='minchars', default=0),
|
maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||||
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED),
|
extname='maxchars', default=UNLIMITED)
|
||||||
extname='maxchars', default=UNLIMITED),
|
isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||||
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
Stub('BoolType'), extname='isUTF8', default=False)
|
||||||
Stub('BoolType'), extname='isUTF8', default=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minchars=0, maxchars=None, **kwds):
|
def __init__(self, minchars=0, maxchars=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -611,7 +612,8 @@ class StringType(DataType):
|
|||||||
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
||||||
# whereas StringType is supposed to not contain '\n'
|
# whereas StringType is supposed to not contain '\n'
|
||||||
# unfortunately, SECoP makes no distinction here....
|
# unfortunately, SECoP makes no distinction here....
|
||||||
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation
|
# note: content is supposed to follow the format of a git commit message,
|
||||||
|
# i.e. a line of text, 2 '\n' + a longer explanation
|
||||||
class TextType(StringType):
|
class TextType(StringType):
|
||||||
def __init__(self, maxchars=None):
|
def __init__(self, maxchars=None):
|
||||||
if maxchars is None:
|
if maxchars is None:
|
||||||
@ -621,7 +623,7 @@ class TextType(StringType):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.maxchars == UNLIMITED:
|
if self.maxchars == UNLIMITED:
|
||||||
return 'TextType()'
|
return 'TextType()'
|
||||||
return 'TextType(%d)' % (self.maxchars)
|
return 'TextType(%d)' % self.maxchars
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
# DataType.copy will not work, because it is exported as 'string'
|
# DataType.copy will not work, because it is exported as 'string'
|
||||||
@ -678,12 +680,10 @@ class ArrayOf(DataType):
|
|||||||
|
|
||||||
:param members: the datatype of the elements
|
:param members: the datatype of the elements
|
||||||
"""
|
"""
|
||||||
properties = {
|
minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
default=0)
|
||||||
default=0),
|
maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||||
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
mandatory=True)
|
||||||
mandatory=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, members, minlen=0, maxlen=None):
|
def __init__(self, members, minlen=0, maxlen=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -714,7 +714,7 @@ class ArrayOf(DataType):
|
|||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
"""set also properties of members"""
|
"""set also properties of members"""
|
||||||
if key in self.__class__.properties:
|
if key in self.propertyDict:
|
||||||
super().setProperty(key, value)
|
super().setProperty(key, value)
|
||||||
else:
|
else:
|
||||||
self.members.setProperty(key, value)
|
self.members.setProperty(key, value)
|
||||||
@ -806,8 +806,7 @@ class TupleOf(DataType):
|
|||||||
try:
|
try:
|
||||||
if len(value) != len(self.members):
|
if len(value) != len(self.members):
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Illegal number of Arguments! Need %d arguments.' %
|
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
||||||
(len(self.members)))
|
|
||||||
# validate elements and return as list
|
# validate elements and return as list
|
||||||
return tuple(sub(elem)
|
return tuple(sub(elem)
|
||||||
for sub, elem in zip(self.members, value))
|
for sub, elem in zip(self.members, value))
|
||||||
@ -835,7 +834,7 @@ class TupleOf(DataType):
|
|||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if not isinstance(other, TupleOf):
|
if not isinstance(other, TupleOf):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
if len(self.members) != len(other.members) :
|
if len(self.members) != len(other.members):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
for a, b in zip(self.members, other.members):
|
for a, b in zip(self.members, other.members):
|
||||||
a.compatible(b)
|
a.compatible(b)
|
||||||
@ -867,11 +866,11 @@ class StructOf(DataType):
|
|||||||
if name not in members:
|
if name not in members:
|
||||||
raise ProgrammingError(
|
raise ProgrammingError(
|
||||||
'Only members of StructOf may be declared as optional!')
|
'Only members of StructOf may be declared as optional!')
|
||||||
self.default = dict((k,el.default) for k, el in members.items())
|
self.default = dict((k, el.default) for k, el in members.items())
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""DataType.copy does not work when members contain enums"""
|
"""DataType.copy does not work when members contain enums"""
|
||||||
return StructOf(self.optional, **{k: v.copy() for k,v in self.members.items()})
|
return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()})
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||||
@ -991,8 +990,8 @@ class CommandType(DataType):
|
|||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||||
|
|
||||||
class DataTypeType(DataType):
|
class DataTypeType(DataType):
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""check if given value (a python obj) is a valid datatype
|
"""check if given value (a python obj) is a valid datatype
|
||||||
@ -1091,16 +1090,13 @@ class LimitsType(TupleOf):
|
|||||||
|
|
||||||
|
|
||||||
class StatusType(TupleOf):
|
class StatusType(TupleOf):
|
||||||
# shorten initialisation and allow acces to status enumMembers from status values
|
# shorten initialisation and allow access to status enumMembers from status values
|
||||||
def __init__(self, enum):
|
def __init__(self, enum):
|
||||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||||
self.enum = enum
|
self._enum = enum
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
enum = TupleOf.__getattr__(self, 'enum')
|
return getattr(self._enum, key)
|
||||||
if hasattr(enum, key):
|
|
||||||
return getattr(enum, key)
|
|
||||||
return TupleOf.__getattr__(self, key)
|
|
||||||
|
|
||||||
|
|
||||||
def floatargs(kwds):
|
def floatargs(kwds):
|
||||||
|
@ -24,11 +24,10 @@
|
|||||||
|
|
||||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||||
FloatRange, StringType, StructOf, TupleOf
|
FloatRange, StringType, StructOf, TupleOf
|
||||||
from secop.metaclass import ModuleMeta
|
from secop.modules import Command, Parameter, HasAccessibles
|
||||||
from secop.modules import Command, Parameter
|
|
||||||
|
|
||||||
|
|
||||||
class Feature(metaclass=ModuleMeta):
|
class Feature(HasAccessibles):
|
||||||
"""all things belonging to a small, predefined functionality influencing the working of a module"""
|
"""all things belonging to a small, predefined functionality influencing the working of a module"""
|
||||||
|
|
||||||
|
|
||||||
@ -39,33 +38,37 @@ class HAS_PID(Feature):
|
|||||||
# note: (i would still but them in the same group, though)
|
# note: (i would still but them in the same group, though)
|
||||||
# note: if extra elements are implemented in the pid struct they MUST BE
|
# note: if extra elements are implemented in the pid struct they MUST BE
|
||||||
# properly described in the description of the pid Parameter
|
# properly described in the description of the pid Parameter
|
||||||
parameters = {
|
|
||||||
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ),
|
# parameters
|
||||||
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ),
|
use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
|
||||||
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True),
|
# pylint: disable=invalid-name
|
||||||
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True),
|
p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
|
||||||
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True),
|
i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
|
||||||
'pid': Parameter('(optional) Struct of p,i,d, minimum output value',
|
d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
|
||||||
|
base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
|
||||||
|
pid = Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||||
datatype=StructOf(p=FloatRange(0),
|
datatype=StructOf(p=FloatRange(0),
|
||||||
i=FloatRange(0),
|
i=FloatRange(0),
|
||||||
d=FloatRange(0),
|
d=FloatRange(0),
|
||||||
base_output=FloatRange(0),
|
base_output=FloatRange(0),
|
||||||
), optional=True,
|
), optional=True,
|
||||||
), # note: struct may be extended with custom elements (names should be prefixed with '_')
|
) # note: struct may be extended with custom elements (names should be prefixed with '_')
|
||||||
'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False),
|
output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Has_PIDTable(HAS_PID):
|
class Has_PIDTable(HAS_PID):
|
||||||
parameters = {
|
|
||||||
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)),
|
# parameters
|
||||||
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
|
||||||
|
pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||||
StructOf(p=FloatRange(0),
|
StructOf(p=FloatRange(0),
|
||||||
i=FloatRange(0),
|
i=FloatRange(0),
|
||||||
d=FloatRange(0),
|
d=FloatRange(0),
|
||||||
_heater_range=FloatRange(0),
|
_heater_range=FloatRange(0),
|
||||||
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange'
|
_base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Persistent(Feature):
|
class HAS_Persistent(Feature):
|
||||||
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
|
|||||||
# 'coupled' : Status.BUSY+2, # to be discussed.
|
# 'coupled' : Status.BUSY+2, # to be discussed.
|
||||||
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
||||||
#}
|
#}
|
||||||
parameters = {
|
|
||||||
'persistent_mode': Parameter('Use persistent mode',
|
# parameters
|
||||||
|
persistent_mode = Parameter('Use persistent mode',
|
||||||
datatype=EnumType(off=0,on=1),
|
datatype=EnumType(off=0,on=1),
|
||||||
default=0, readonly=False),
|
default=0, readonly=False)
|
||||||
'is_persistent': Parameter('current state of persistence',
|
is_persistent = Parameter('current state of persistence',
|
||||||
datatype=BoolType(), optional=True),
|
datatype=BoolType(), optional=True)
|
||||||
'stored_value': Parameter('current persistence value, often used as the modules value',
|
stored_value = Parameter('current persistence value, often used as the modules value',
|
||||||
datatype='main', unit='$', optional=True),
|
datatype='main', unit='$', optional=True)
|
||||||
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||||
datatype='main', unit='$' ),
|
datatype='main', unit='$' )
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Tolerance(Feature):
|
class HAS_Tolerance(Feature):
|
||||||
# detects IDLE status by checking if the value lies in a given window:
|
# detects IDLE status by checking if the value lies in a given window:
|
||||||
# tolerance is the maximum allowed deviation from target, value must lie in this interval
|
# tolerance is the maximum allowed deviation from target, value must lie in this interval
|
||||||
# for at least ´timewindow´ seconds.
|
# for at least ´timewindow´ seconds.
|
||||||
parameters = {
|
|
||||||
'tolerance': Parameter('Half height of the Window',
|
# parameters
|
||||||
datatype=FloatRange(0), default=1, unit='$'),
|
tolerance = Parameter('Half height of the Window',
|
||||||
'timewindow': Parameter('Length of the timewindow to check',
|
datatype=FloatRange(0), default=1, unit='$')
|
||||||
|
timewindow = Parameter('Length of the timewindow to check',
|
||||||
datatype=FloatRange(0), default=30, unit='s',
|
datatype=FloatRange(0), default=30, unit='s',
|
||||||
optional=True),
|
optional=True)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Timeout(Feature):
|
class HAS_Timeout(Feature):
|
||||||
parameters = {
|
|
||||||
'timeout': Parameter('timeout for movement',
|
# parameters
|
||||||
datatype=FloatRange(0), default=0, unit='s'),
|
timeout = Parameter('timeout for movement',
|
||||||
}
|
datatype=FloatRange(0), default=0, unit='s')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Pause(Feature):
|
class HAS_Pause(Feature):
|
||||||
# just a proposal, can't agree on it....
|
# just a proposal, can't agree on it....
|
||||||
parameters = {
|
|
||||||
'pause': Command('pauses movement', argument=None, result=None),
|
@Command(argument=None, result=None)
|
||||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
def pause(self):
|
||||||
argument=None, result=None),
|
"""pauses movement"""
|
||||||
}
|
|
||||||
|
@Command(argument=None, result=None)
|
||||||
|
def go(self):
|
||||||
|
"""continues movement or start a new one if target was change since the last pause"""
|
||||||
|
|
||||||
|
|
||||||
class HAS_Ramp(Feature):
|
class HAS_Ramp(Feature):
|
||||||
parameters = {
|
|
||||||
'ramp': Parameter('speed of movement', unit='$/min',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
ramp =Parameter('speed of movement', unit='$/min',
|
||||||
'use_ramp': Parameter('use the ramping of the setpoint, or jump',
|
datatype=FloatRange(0))
|
||||||
|
use_ramp = Parameter('use the ramping of the setpoint, or jump',
|
||||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||||
optional=True),
|
optional=True)
|
||||||
'setpoint': Parameter('currently active setpoint',
|
setpoint = Parameter('currently active setpoint',
|
||||||
datatype=FloatRange(0), unit='$',
|
datatype=FloatRange(0), unit='$',
|
||||||
readonly=True, ),
|
readonly=True, )
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Speed(Feature):
|
class HAS_Speed(Feature):
|
||||||
parameters = {
|
|
||||||
'speed' : Parameter('(maximum) speed of movement (of the main value)',
|
# parameters
|
||||||
unit='$/s', datatype=FloatRange(0)),
|
speed = Parameter('(maximum) speed of movement (of the main value)',
|
||||||
}
|
unit='$/s', datatype=FloatRange(0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Accel(HAS_Speed):
|
class HAS_Accel(HAS_Speed):
|
||||||
parameters = {
|
|
||||||
'accel' : Parameter('acceleration of movement', unit='$/s^2',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
accel = Parameter('acceleration of movement', unit='$/s^2',
|
||||||
'decel' : Parameter('deceleration of movement', unit='$/s^2',
|
datatype=FloatRange(0))
|
||||||
datatype=FloatRange(0), optional=True),
|
decel = Parameter('deceleration of movement', unit='$/s^2',
|
||||||
}
|
datatype=FloatRange(0), optional=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_MotorCurrents(Feature):
|
class HAS_MotorCurrents(Feature):
|
||||||
parameters = {
|
|
||||||
'movecurrent' : Parameter('Current while moving',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
movecurrent = Parameter('Current while moving',
|
||||||
'idlecurrent' : Parameter('Current while idle',
|
datatype=FloatRange(0))
|
||||||
datatype=FloatRange(0), optional=True),
|
idlecurrent = Parameter('Current while idle',
|
||||||
}
|
datatype=FloatRange(0), optional=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Curve(Feature):
|
class HAS_Curve(Feature):
|
||||||
# proposed, not yet agreed upon!
|
# proposed, not yet agreed upon!
|
||||||
parameters = {
|
|
||||||
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'),
|
# parameters
|
||||||
# XXX: tbd. (how to upload/download/select a curve?)
|
curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
|
||||||
}
|
|
||||||
|
@ -54,7 +54,7 @@ method has to be called explicitly int the write_<parameter> method, if needed.
|
|||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from secop.metaclass import Done
|
from secop.modules import Done
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ SIMPLETYPES = {
|
|||||||
'IntRange': 'int',
|
'IntRange': 'int',
|
||||||
'BlobType': 'bytes',
|
'BlobType': 'bytes',
|
||||||
'StringType': 'str',
|
'StringType': 'str',
|
||||||
|
'TextType': 'str',
|
||||||
'BoolType': 'bool',
|
'BoolType': 'bool',
|
||||||
'StructOf': 'dict',
|
'StructOf': 'dict',
|
||||||
}
|
}
|
||||||
@ -179,7 +180,7 @@ def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
|||||||
def class_doc_handler(app, what, name, cls, options, lines):
|
def class_doc_handler(app, what, name, cls, options, lines):
|
||||||
if what == 'class':
|
if what == 'class':
|
||||||
if issubclass(cls, HasProperties):
|
if issubclass(cls, HasProperties):
|
||||||
append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property)
|
append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property)
|
||||||
if issubclass(cls, Module):
|
if issubclass(cls, Module):
|
||||||
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||||
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
||||||
|
@ -141,7 +141,7 @@ class SequencerMixin:
|
|||||||
return self.read_hw_status()
|
return self.read_hw_status()
|
||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if self.seq_is_alive():
|
if self.seq_is_alive():
|
||||||
self._seq_stopflag = True
|
self._seq_stopflag = True
|
||||||
|
|
||||||
|
@ -1,248 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# *****************************************************************************
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License as published by the Free Software
|
|
||||||
# Foundation; either version 2 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
||||||
#
|
|
||||||
# Module authors:
|
|
||||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
||||||
#
|
|
||||||
# *****************************************************************************
|
|
||||||
"""Define Metaclass for Modules/Features"""
|
|
||||||
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from secop.errors import ProgrammingError, BadValueError
|
|
||||||
from secop.params import Command, Override, Parameter, Accessible, usercommand
|
|
||||||
from secop.datatypes import EnumType
|
|
||||||
from secop.properties import PropertyMeta, flatten_dict, Property
|
|
||||||
|
|
||||||
|
|
||||||
class Done:
|
|
||||||
"""a special return value for a read/write function
|
|
||||||
|
|
||||||
indicating that the setter is triggered already"""
|
|
||||||
|
|
||||||
|
|
||||||
# warning: MAGIC!
|
|
||||||
|
|
||||||
class ModuleMeta(PropertyMeta):
|
|
||||||
"""Metaclass
|
|
||||||
|
|
||||||
joining the class's properties, parameters and commands dicts with
|
|
||||||
those of base classes.
|
|
||||||
also creates getters/setter for parameter access
|
|
||||||
and wraps read_*/write_* methods
|
|
||||||
(so the dispatcher will get notfied of changed values)
|
|
||||||
"""
|
|
||||||
def __new__(cls, name, bases, attrs): # pylint: disable=too-many-branches
|
|
||||||
# allow to declare accessibles directly as class attribute
|
|
||||||
# all these attributes are removed
|
|
||||||
flatten_dict('parameters', Parameter, attrs)
|
|
||||||
# do not remove commands from attrs, they are kept as descriptors
|
|
||||||
flatten_dict('commands', usercommand, attrs, remove=False)
|
|
||||||
flatten_dict('properties', Property, attrs)
|
|
||||||
|
|
||||||
commands = attrs.pop('commands', {})
|
|
||||||
parameters = attrs.pop('parameters', {})
|
|
||||||
overrides = attrs.pop('overrides', {})
|
|
||||||
|
|
||||||
newtype = type.__new__(cls, name, bases, attrs)
|
|
||||||
if '__constructed__' in attrs:
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
newtype = PropertyMeta.__join_properties__(newtype, name, bases, attrs)
|
|
||||||
|
|
||||||
# merge accessibles from all sub-classes, treat overrides
|
|
||||||
# for now, allow to use also the old syntax (parameters/commands dict)
|
|
||||||
accessibles_list = []
|
|
||||||
for base in reversed(bases):
|
|
||||||
if hasattr(base, "accessibles"):
|
|
||||||
accessibles_list.append(base.accessibles)
|
|
||||||
for accessibles in [parameters, commands, overrides]:
|
|
||||||
accessibles_list.append(accessibles)
|
|
||||||
accessibles = {} # unordered dict of accessibles, will be sorted later
|
|
||||||
for accessibles_dict in accessibles_list:
|
|
||||||
for key, obj in accessibles_dict.items():
|
|
||||||
if isinstance(obj, Override):
|
|
||||||
if key not in accessibles:
|
|
||||||
raise ProgrammingError("module %s: can not apply Override on %s: no such accessible!"
|
|
||||||
% (name, key))
|
|
||||||
obj = obj.apply(accessibles[key])
|
|
||||||
accessibles[key] = obj
|
|
||||||
else:
|
|
||||||
aobj = accessibles.get(key)
|
|
||||||
if aobj:
|
|
||||||
if obj.kwds is not None: # obj may be used for override
|
|
||||||
if isinstance(obj, Command) != isinstance(obj, Command):
|
|
||||||
raise ProgrammingError("module %s.%s: can not override a %s with a %s!"
|
|
||||||
% (name, key, aobj.__class_.name, obj.__class_.name, ))
|
|
||||||
obj = aobj.override(obj)
|
|
||||||
accessibles[key] = obj
|
|
||||||
setattr(newtype, key, obj)
|
|
||||||
if not isinstance(obj, (Parameter, Command)):
|
|
||||||
raise ProgrammingError('%r: accessibles entry %r should be a '
|
|
||||||
'Parameter or Command object!' % (name, key))
|
|
||||||
accessibles[key] = obj
|
|
||||||
|
|
||||||
# Correct naming of EnumTypes
|
|
||||||
for k, v in accessibles.items():
|
|
||||||
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
|
|
||||||
v.datatype._enum.name = k
|
|
||||||
|
|
||||||
# newtype.accessibles will be used in 2 places only:
|
|
||||||
# 1) for inheritance (see above)
|
|
||||||
# 2) for the describing message
|
|
||||||
newtype.accessibles = OrderedDict(sorted(accessibles.items(), key=lambda item: item[1].ctr))
|
|
||||||
|
|
||||||
# check for attributes overriding parameter values
|
|
||||||
for pname, pobj in newtype.accessibles.items():
|
|
||||||
if pname in attrs:
|
|
||||||
value = attrs[pname]
|
|
||||||
if isinstance(value, (Accessible, Override)):
|
|
||||||
continue
|
|
||||||
if isinstance(pobj, Parameter):
|
|
||||||
try:
|
|
||||||
value = pobj.datatype(attrs[pname])
|
|
||||||
except BadValueError:
|
|
||||||
raise ProgrammingError('parameter %r can not be set to %r'
|
|
||||||
% (pname, attrs[pname]))
|
|
||||||
newtype.accessibles[pname] = pobj.override(default=value)
|
|
||||||
elif isinstance(pobj, usercommand):
|
|
||||||
if not callable(attrs[pname]):
|
|
||||||
raise ProgrammingError('%s.%s overwrites a command'
|
|
||||||
% (newtype.__name__, pname))
|
|
||||||
pobj = pobj.override(func=attrs[name])
|
|
||||||
newtype.accessibles[pname] = pobj
|
|
||||||
|
|
||||||
# check validity of Parameter entries
|
|
||||||
for pname, pobj in newtype.accessibles.items():
|
|
||||||
# XXX: create getters for the units of params ??
|
|
||||||
|
|
||||||
# wrap of reading/writing funcs
|
|
||||||
if isinstance(pobj, Command):
|
|
||||||
if isinstance(pobj, usercommand):
|
|
||||||
do_name = 'do_' + pname
|
|
||||||
# create additional method do_<pname> for backwards compatibility
|
|
||||||
if do_name not in attrs:
|
|
||||||
setattr(newtype, do_name, pobj)
|
|
||||||
continue
|
|
||||||
rfunc = attrs.get('read_' + pname, None)
|
|
||||||
rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None
|
|
||||||
if rfunc_handler:
|
|
||||||
if rfunc:
|
|
||||||
raise ProgrammingError("parameter '%s' can not have a handler "
|
|
||||||
"and read_%s" % (pname, pname))
|
|
||||||
rfunc = rfunc_handler
|
|
||||||
else:
|
|
||||||
for base in bases:
|
|
||||||
if rfunc is not None:
|
|
||||||
break
|
|
||||||
rfunc = getattr(base, 'read_' + pname, None)
|
|
||||||
|
|
||||||
# create wrapper except when read function is already wrapped
|
|
||||||
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
|
|
||||||
|
|
||||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
|
||||||
if rfunc:
|
|
||||||
self.log.debug("calling %r" % rfunc)
|
|
||||||
try:
|
|
||||||
value = rfunc(self)
|
|
||||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
|
||||||
if value is Done: # the setter is already triggered
|
|
||||||
return getattr(self, pname)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
|
||||||
self.announceUpdate(pname, None, e)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# return cached value
|
|
||||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
|
||||||
value = self.accessibles[pname].value
|
|
||||||
setattr(self, pname, value) # important! trigger the setter
|
|
||||||
return value
|
|
||||||
|
|
||||||
if rfunc:
|
|
||||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
|
||||||
setattr(newtype, 'read_' + pname, wrapped_rfunc)
|
|
||||||
wrapped_rfunc.__wrapped__ = True
|
|
||||||
|
|
||||||
if not pobj.readonly:
|
|
||||||
wfunc = attrs.get('write_' + pname, None)
|
|
||||||
if wfunc is None: # ignore the handler, if a write function is present
|
|
||||||
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
|
|
||||||
for base in bases:
|
|
||||||
if wfunc is not None:
|
|
||||||
break
|
|
||||||
wfunc = getattr(base, 'write_' + pname, None)
|
|
||||||
|
|
||||||
# create wrapper except when write function is already wrapped
|
|
||||||
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
|
||||||
|
|
||||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
|
||||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
|
||||||
pobj = self.accessibles[pname]
|
|
||||||
value = pobj.datatype(value)
|
|
||||||
if wfunc:
|
|
||||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
|
||||||
returned_value = wfunc(self, value)
|
|
||||||
if returned_value is Done: # the setter is already triggered
|
|
||||||
return getattr(self, pname)
|
|
||||||
if returned_value is not None: # goodie: accept missing return value
|
|
||||||
value = returned_value
|
|
||||||
setattr(self, pname, value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
if wfunc:
|
|
||||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
|
||||||
setattr(newtype, 'write_' + pname, wrapped_wfunc)
|
|
||||||
wrapped_wfunc.__wrapped__ = True
|
|
||||||
|
|
||||||
def getter(self, pname=pname):
|
|
||||||
return self.accessibles[pname].value
|
|
||||||
|
|
||||||
def setter(self, value, pname=pname):
|
|
||||||
self.announceUpdate(pname, value)
|
|
||||||
|
|
||||||
setattr(newtype, pname, property(getter, setter))
|
|
||||||
|
|
||||||
# check information about Command's
|
|
||||||
for attrname in attrs:
|
|
||||||
if attrname.startswith('do_'):
|
|
||||||
if attrname[3:] not in newtype.accessibles:
|
|
||||||
raise ProgrammingError('%r: command %r has to be specified '
|
|
||||||
'explicitly!' % (name, attrname[3:]))
|
|
||||||
|
|
||||||
attrs['__constructed__'] = True
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
@property
|
|
||||||
def configurables(cls):
|
|
||||||
# note: this ends up as an property of the Module class (not on the instance)!
|
|
||||||
|
|
||||||
# dict of properties with Property and Parameter with dict of properties
|
|
||||||
res = {}
|
|
||||||
# collect info about properties
|
|
||||||
for pn, pv in cls.properties.items():
|
|
||||||
if pv.settable:
|
|
||||||
res[pn] = pv
|
|
||||||
# collect info about parameters and their properties
|
|
||||||
for param, pobj in cls.accessibles.items():
|
|
||||||
res[param] = {}
|
|
||||||
for pn, pv in pobj.getProperties().items():
|
|
||||||
if pv.settable:
|
|
||||||
res[param][pn] = pv
|
|
||||||
return res
|
|
288
secop/modules.py
288
secop/modules.py
@ -20,12 +20,11 @@
|
|||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define Baseclasses for real Modules implemented in the server"""
|
"""Define base classes for real Modules implemented in the server"""
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
||||||
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
||||||
@ -33,19 +32,147 @@ from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueErro
|
|||||||
SilentError, InternalError, secop_error
|
SilentError, InternalError, secop_error
|
||||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.metaclass import ModuleMeta
|
from secop.params import PREDEFINED_ACCESSIBLES, Command, Parameter, Accessible
|
||||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
from secop.poller import Poller, BasicPoller
|
from secop.poller import Poller, BasicPoller
|
||||||
|
|
||||||
|
|
||||||
# XXX: connect with 'protocol'-Modules.
|
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
||||||
# Idea: every Module defined herein is also a 'protocol'-Module,
|
|
||||||
# all others MUST derive from those, the 'interface'-class is still derived
|
|
||||||
# from these base classes (how to do this?)
|
|
||||||
|
|
||||||
|
|
||||||
class Module(HasProperties, metaclass=ModuleMeta):
|
class HasAccessibles(HasProperties):
|
||||||
|
"""base class of Module
|
||||||
|
|
||||||
|
joining the class's properties, parameters and commands dicts with
|
||||||
|
those of base classes.
|
||||||
|
wrap read_*/write_* methods
|
||||||
|
(so the dispatcher will get notified of changed values)
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||||
|
super().__init_subclass__()
|
||||||
|
# merge accessibles from all sub-classes, treat overrides
|
||||||
|
# for now, allow to use also the old syntax (parameters/commands dict)
|
||||||
|
accessibles = {}
|
||||||
|
for base in cls.__bases__:
|
||||||
|
accessibles.update(getattr(base, 'accessibles', {}))
|
||||||
|
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
|
||||||
|
for aname, aobj in accessibles.items():
|
||||||
|
value = getattr(cls, aname, None)
|
||||||
|
if not isinstance(value, Accessible): # else override is already done in __set_name__
|
||||||
|
anew = aobj.override(value)
|
||||||
|
newaccessibles[aname] = anew
|
||||||
|
setattr(cls, aname, anew)
|
||||||
|
anew.__set_name__(cls, aname)
|
||||||
|
ordered = {}
|
||||||
|
for aname in cls.__dict__.get('paramOrder', ()):
|
||||||
|
if aname in accessibles:
|
||||||
|
ordered[aname] = accessibles.pop(aname)
|
||||||
|
elif aname in newaccessibles:
|
||||||
|
ordered[aname] = newaccessibles.pop(aname)
|
||||||
|
# ignore unknown names
|
||||||
|
# starting from old accessibles not mentioned, append items from 'order'
|
||||||
|
accessibles.update(ordered)
|
||||||
|
# then new accessibles not mentioned
|
||||||
|
accessibles.update(newaccessibles)
|
||||||
|
cls.accessibles = accessibles
|
||||||
|
|
||||||
|
# Correct naming of EnumTypes
|
||||||
|
for k, v in accessibles.items():
|
||||||
|
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
|
||||||
|
v.datatype.set_name(k)
|
||||||
|
|
||||||
|
# check validity of Parameter entries
|
||||||
|
for pname, pobj in accessibles.items():
|
||||||
|
# XXX: create getters for the units of params ??
|
||||||
|
|
||||||
|
# wrap of reading/writing funcs
|
||||||
|
if isinstance(pobj, Command):
|
||||||
|
# nothing to do for now
|
||||||
|
continue
|
||||||
|
rfunc = cls.__dict__.get('read_' + pname, None)
|
||||||
|
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||||
|
if rfunc_handler:
|
||||||
|
if rfunc:
|
||||||
|
raise ProgrammingError("parameter '%s' can not have a handler "
|
||||||
|
"and read_%s" % (pname, pname))
|
||||||
|
rfunc = rfunc_handler
|
||||||
|
|
||||||
|
# create wrapper except when read function is already wrapped
|
||||||
|
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
|
||||||
|
|
||||||
|
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||||
|
if rfunc:
|
||||||
|
self.log.debug("calling %r" % rfunc)
|
||||||
|
try:
|
||||||
|
value = rfunc(self)
|
||||||
|
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
||||||
|
if value is Done: # the setter is already triggered
|
||||||
|
return getattr(self, pname)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
||||||
|
self.announceUpdate(pname, None, e)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# return cached value
|
||||||
|
self.log.debug("rfunc(%s): return cached value" % pname)
|
||||||
|
value = self.accessibles[pname].value
|
||||||
|
setattr(self, pname, value) # important! trigger the setter
|
||||||
|
return value
|
||||||
|
|
||||||
|
if rfunc:
|
||||||
|
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||||
|
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
||||||
|
wrapped_rfunc.__wrapped__ = True
|
||||||
|
|
||||||
|
if not pobj.readonly:
|
||||||
|
wfunc = getattr(cls, 'write_' + pname, None)
|
||||||
|
if wfunc is None: # ignore the handler, if a write function is present
|
||||||
|
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
|
||||||
|
|
||||||
|
# create wrapper except when write function is already wrapped
|
||||||
|
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
||||||
|
|
||||||
|
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||||
|
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||||
|
pobj = self.accessibles[pname]
|
||||||
|
value = pobj.datatype(value)
|
||||||
|
if wfunc:
|
||||||
|
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
||||||
|
returned_value = wfunc(self, value)
|
||||||
|
if returned_value is Done: # the setter is already triggered
|
||||||
|
return getattr(self, pname)
|
||||||
|
if returned_value is not None: # goodie: accept missing return value
|
||||||
|
value = returned_value
|
||||||
|
setattr(self, pname, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
if wfunc:
|
||||||
|
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||||
|
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||||
|
wrapped_wfunc.__wrapped__ = True
|
||||||
|
|
||||||
|
# check information about Command's
|
||||||
|
for attrname in cls.__dict__:
|
||||||
|
if attrname.startswith('do_'):
|
||||||
|
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||||
|
% (cls.__name__, attrname))
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
# collect info about properties
|
||||||
|
for pn, pv in cls.propertyDict.items():
|
||||||
|
if pv.settable:
|
||||||
|
res[pn] = pv
|
||||||
|
# collect info about parameters and their properties
|
||||||
|
for param, pobj in cls.accessibles.items():
|
||||||
|
res[param] = {}
|
||||||
|
for pn, pv in pobj.getProperties().items():
|
||||||
|
if pv.settable:
|
||||||
|
res[param][pn] = pv
|
||||||
|
cls.configurables = res
|
||||||
|
|
||||||
|
|
||||||
|
class Module(HasAccessibles):
|
||||||
"""basic module
|
"""basic module
|
||||||
|
|
||||||
all SECoP modules derive from this.
|
all SECoP modules derive from this.
|
||||||
@ -58,7 +185,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- the programmer normally should not need to reimplement :meth:`__init__`
|
- the programmer normally should not need to reimplement :meth:`__init__`
|
||||||
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
|
- within modules, parameters should only be addressed as ``self.<pname>``,
|
||||||
|
i.e. ``self.value``, ``self.target`` etc...
|
||||||
|
|
||||||
- these are accessing the cached version.
|
- these are accessing the cached version.
|
||||||
- they can also be written to, generating an async update
|
- they can also be written to, generating an async update
|
||||||
@ -77,22 +205,17 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
# note: properties don't change after startup and are usually filled
|
# note: properties don't change after startup and are usually filled
|
||||||
# with data from a cfg file...
|
# with data from a cfg file...
|
||||||
# note: only the properties predefined here are allowed to be set in the cfg file
|
# note: only the properties predefined here are allowed to be set in the cfg file
|
||||||
# note: the names map to a [datatype, value] list, value comes from the cfg file,
|
export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
|
||||||
# datatype is fixed!
|
group = Property('optional group the module belongs to', StringType(), default='', extname='group')
|
||||||
properties = {
|
description = Property('description of the module', TextType(), extname='description', mandatory=True)
|
||||||
'export': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False),
|
meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
|
||||||
'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'),
|
default=('', 0), extname='meaning')
|
||||||
'description': Property('Description of the module', TextType(), extname='description', mandatory=True),
|
visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
default='user', extname='visibility')
|
||||||
default=('',0), extname='meaning'),
|
implementation = Property('internal name of the implementation class of the module', StringType(),
|
||||||
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
extname='implementation')
|
||||||
default='user', extname='visibility'),
|
interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||||
'implementation': Property('Internal name of the implementation class of the module', StringType(),
|
extname='interface_classes')
|
||||||
extname='implementation'),
|
|
||||||
'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()),
|
|
||||||
extname='interface_classes'),
|
|
||||||
# what else?
|
|
||||||
}
|
|
||||||
|
|
||||||
# properties, parameters and commands are auto-merged upon subclassing
|
# properties, parameters and commands are auto-merged upon subclassing
|
||||||
parameters = {}
|
parameters = {}
|
||||||
@ -113,14 +236,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
# handle module properties
|
# handle module properties
|
||||||
# 1) make local copies of properties
|
# 1) make local copies of properties
|
||||||
super(Module, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
# 2) check and apply properties specified in cfgdict
|
# 2) check and apply properties specified in cfgdict
|
||||||
# specified as '.<propertyname> = <propertyvalue>'
|
# specified as '.<propertyname> = <propertyvalue>'
|
||||||
# (this is for legacy config files only)
|
# (this is for legacy config files only)
|
||||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||||
if k[0] == '.':
|
if k[0] == '.':
|
||||||
if k[1:] in self.__class__.properties:
|
if k[1:] in self.propertyDict:
|
||||||
self.setProperty(k[1:], cfgdict.pop(k))
|
self.setProperty(k[1:], cfgdict.pop(k))
|
||||||
else:
|
else:
|
||||||
raise ConfigError('Module %r has no property %r' %
|
raise ConfigError('Module %r has no property %r' %
|
||||||
@ -128,20 +251,20 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
# 3) check and apply properties specified in cfgdict as
|
# 3) check and apply properties specified in cfgdict as
|
||||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||||
for k in self.__class__.properties:
|
for k in self.propertyDict:
|
||||||
if k in cfgdict:
|
if k in cfgdict:
|
||||||
self.setProperty(k, cfgdict.pop(k))
|
self.setProperty(k, cfgdict.pop(k))
|
||||||
|
|
||||||
# 4) set automatic properties
|
# 4) set automatic properties
|
||||||
mycls = self.__class__
|
mycls = self.__class__
|
||||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||||
self.properties['implementation'] = myclassname
|
self.implementation = myclassname
|
||||||
# list of all 'secop' modules
|
# list of all 'secop' modules
|
||||||
self.properties['interface_classes'] = [
|
# self.interface_classes = [
|
||||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||||
# list of only the 'highest' secop module class
|
# list of only the 'highest' secop module class
|
||||||
self.properties['interface_classes'] = [[
|
self.interface_classes = [
|
||||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
|
||||||
|
|
||||||
# handle Features
|
# handle Features
|
||||||
# XXX: todo
|
# XXX: todo
|
||||||
@ -150,7 +273,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
# 1) make local copies of parameter objects
|
# 1) make local copies of parameter objects
|
||||||
# they need to be individual per instance since we use them also
|
# they need to be individual per instance since we use them also
|
||||||
# to cache the current value + qualifiers...
|
# to cache the current value + qualifiers...
|
||||||
accessibles = OrderedDict()
|
accessibles = {}
|
||||||
# conversion from exported names to internal attribute names
|
# conversion from exported names to internal attribute names
|
||||||
accessiblename2attr = {}
|
accessiblename2attr = {}
|
||||||
for aname, aobj in self.accessibles.items():
|
for aname, aobj in self.accessibles.items():
|
||||||
@ -159,31 +282,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
if isinstance(aobj, Parameter):
|
if isinstance(aobj, Parameter):
|
||||||
# fix default properties poll and needscfg
|
# fix default properties poll and needscfg
|
||||||
if aobj.poll is None:
|
if aobj.poll is None:
|
||||||
aobj.properties['poll'] = bool(aobj.handler)
|
aobj.poll = bool(aobj.handler)
|
||||||
if aobj.needscfg is None:
|
if aobj.needscfg is None:
|
||||||
aobj.properties['needscfg'] = not aobj.poll
|
aobj.needscfg = not aobj.poll
|
||||||
|
|
||||||
if not self.export: # do not export parameters of a module not exported
|
if not self.export: # do not export parameters of a module not exported
|
||||||
aobj.properties['export'] = False
|
aobj.export = False
|
||||||
if aobj.export:
|
if aobj.export:
|
||||||
if aobj.export is True:
|
if aobj.export is True:
|
||||||
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
||||||
if predefined_obj:
|
if predefined_obj:
|
||||||
if isinstance(aobj, predefined_obj):
|
if isinstance(aobj, predefined_obj):
|
||||||
aobj.setProperty('export', aname)
|
aobj.export = aname
|
||||||
else:
|
else:
|
||||||
raise ProgrammingError("can not use '%s' as name of a %s" %
|
raise ProgrammingError("can not use '%s' as name of a %s" %
|
||||||
(aname, aobj.__class__.__name__))
|
(aname, aobj.__class__.__name__))
|
||||||
else: # create custom parameter
|
else: # create custom parameter
|
||||||
aobj.setProperty('export', '_' + aname)
|
aobj.export = '_' + aname
|
||||||
accessiblename2attr[aobj.export] = aname
|
accessiblename2attr[aobj.export] = aname
|
||||||
accessibles[aname] = aobj
|
accessibles[aname] = aobj
|
||||||
# do not re-use self.accessibles as this is the same for all instances
|
# do not re-use self.accessibles as this is the same for all instances
|
||||||
self.accessibles = accessibles
|
self.accessibles = accessibles
|
||||||
self.accessiblename2attr = accessiblename2attr
|
self.accessiblename2attr = accessiblename2attr
|
||||||
# provide properties to 'filter' out the parameters/commands
|
# provide properties to 'filter' out the parameters/commands
|
||||||
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
|
self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
|
||||||
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command))
|
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
|
||||||
|
|
||||||
# 2) check and apply parameter_properties
|
# 2) check and apply parameter_properties
|
||||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||||
@ -200,6 +323,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
else:
|
else:
|
||||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||||
(self.name, paramname, propname))
|
(self.name, paramname, propname))
|
||||||
|
else:
|
||||||
|
raise ConfigError('Module %s has no Parameter %r!' %
|
||||||
|
(self.name, paramname))
|
||||||
|
|
||||||
# 3) check config for problems:
|
# 3) check config for problems:
|
||||||
# only accept remaining config items specified in parameters
|
# only accept remaining config items specified in parameters
|
||||||
@ -209,7 +335,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
'Module %s:config Parameter %r '
|
'Module %s:config Parameter %r '
|
||||||
'not understood! (use one of %s)' %
|
'not understood! (use one of %s)' %
|
||||||
(self.name, k, ', '.join(list(self.parameters) +
|
(self.name, k, ', '.join(list(self.parameters) +
|
||||||
list(self.__class__.properties))))
|
list(self.propertyDict))))
|
||||||
|
|
||||||
# 4) complain if a Parameter entry has no default value and
|
# 4) complain if a Parameter entry has no default value and
|
||||||
# is not specified in cfgdict and deal with parameters to be written.
|
# is not specified in cfgdict and deal with parameters to be written.
|
||||||
@ -348,7 +474,6 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
modobj.announceUpdate(p, value)
|
modobj.announceUpdate(p, value)
|
||||||
self.valueCallbacks[pname].append(cb)
|
self.valueCallbacks[pname].append(cb)
|
||||||
|
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
"""helper function for treating substates of BUSY correctly"""
|
"""helper function for treating substates of BUSY correctly"""
|
||||||
# defined even for non drivable (used for dynamic polling)
|
# defined even for non drivable (used for dynamic polling)
|
||||||
@ -403,31 +528,22 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
"""basic readable Module"""
|
"""basic readable module"""
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
Status = Enum('Status',
|
Status = Enum('Status',
|
||||||
IDLE = 100,
|
IDLE=100,
|
||||||
WARN = 200,
|
WARN=200,
|
||||||
UNSTABLE = 270,
|
UNSTABLE=270,
|
||||||
ERROR = 400,
|
ERROR=400,
|
||||||
DISABLED = 0,
|
DISABLED=0,
|
||||||
UNKNOWN = 401,
|
UNKNOWN=401,
|
||||||
) #: status codes
|
) #: status codes
|
||||||
parameters = {
|
|
||||||
'value': Parameter('current value of the Module', readonly=True,
|
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||||
datatype=FloatRange(),
|
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||||
poll=True,
|
default=(Status.IDLE, ''), poll=True)
|
||||||
),
|
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
default=5, readonly=False)
|
||||||
readonly=False,
|
|
||||||
datatype=FloatRange(0.1, 120),
|
|
||||||
),
|
|
||||||
'status': Parameter('current status of the Module',
|
|
||||||
default=(Status.IDLE, ''),
|
|
||||||
datatype=TupleOf(EnumType(Status), StringType()),
|
|
||||||
readonly=True, poll=True,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, started_callback):
|
||||||
"""start basic polling thread"""
|
"""start basic polling thread"""
|
||||||
@ -476,11 +592,9 @@ class Readable(Module):
|
|||||||
|
|
||||||
class Writable(Readable):
|
class Writable(Readable):
|
||||||
"""basic writable module"""
|
"""basic writable module"""
|
||||||
parameters = {
|
|
||||||
'target': Parameter('target value of the Module',
|
target = Parameter('target value of the module',
|
||||||
default=0, readonly=False, datatype=FloatRange(),
|
default=0, readonly=False, datatype=FloatRange())
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Drivable(Writable):
|
class Drivable(Writable):
|
||||||
@ -488,17 +602,7 @@ class Drivable(Writable):
|
|||||||
|
|
||||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||||
|
|
||||||
commands = {
|
status = Parameter(datatype=StatusType(Status)) # override Readable.status
|
||||||
'stop': Command(
|
|
||||||
'cease driving, go to IDLE state',
|
|
||||||
argument=None,
|
|
||||||
result=None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
overrides = {
|
|
||||||
'status': Override(datatype=StatusType(Status)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
"""check for busy, treating substates correctly
|
"""check for busy, treating substates correctly
|
||||||
@ -532,23 +636,16 @@ class Drivable(Writable):
|
|||||||
self.pollOneParam(pname)
|
self.pollOneParam(pname)
|
||||||
return fastpoll
|
return fastpoll
|
||||||
|
|
||||||
def do_stop(self):
|
@Command(None, result=None)
|
||||||
"""default implementation of the stop command
|
def stop(self):
|
||||||
|
"""cease driving, go to IDLE state"""
|
||||||
by default does nothing."""
|
|
||||||
|
|
||||||
|
|
||||||
class Communicator(Module):
|
class Communicator(Module):
|
||||||
"""basic abstract communication module"""
|
"""basic abstract communication module"""
|
||||||
|
|
||||||
commands = {
|
@Command(StringType(), result=StringType())
|
||||||
"communicate": Command("provides the simplest mean to communication",
|
def communicate(self, command):
|
||||||
argument=StringType(),
|
|
||||||
result=StringType()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_communicate(self, command):
|
|
||||||
"""communicate command
|
"""communicate command
|
||||||
|
|
||||||
:param command: the command to be sent
|
:param command: the command to be sent
|
||||||
@ -569,7 +666,8 @@ class Attached(Property):
|
|||||||
# we can not put this to properties.py, as it needs datatypes
|
# we can not put this to properties.py, as it needs datatypes
|
||||||
def __init__(self, attrname=None):
|
def __init__(self, attrname=None):
|
||||||
self.attrname = attrname
|
self.attrname = attrname
|
||||||
super().__init__('attached module', StringType())
|
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
|
||||||
|
super().__init__('attached module', StringType(), mandatory=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||||
|
559
secop/params.py
559
secop/params.py
@ -24,62 +24,69 @@
|
|||||||
|
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
||||||
NoneOr, TextType, IntRange, TupleOf
|
NoneOr, TextType, IntRange, TupleOf, StructOf
|
||||||
from secop.errors import ProgrammingError, BadValueError
|
from secop.errors import ProgrammingError, BadValueError
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
|
|
||||||
object_counter = itertools.count(1)
|
UNSET = object() # an argument not given, not even None
|
||||||
|
|
||||||
|
|
||||||
class Accessible(HasProperties):
|
class Accessible(HasProperties):
|
||||||
"""base class for Parameter and Command"""
|
"""base class for Parameter and Command"""
|
||||||
|
|
||||||
properties = {}
|
|
||||||
kwds = None # is a dict if it might be used as Override
|
kwds = None # is a dict if it might be used as Override
|
||||||
|
|
||||||
def __init__(self, ctr, **kwds):
|
def __init__(self, **kwds):
|
||||||
self.ctr = ctr or next(object_counter)
|
super().__init__()
|
||||||
super(Accessible, self).__init__()
|
self.init(kwds)
|
||||||
# do not use self.properties.update here, as no invalid values should be
|
|
||||||
|
def init(self, kwds):
|
||||||
|
# do not use self.propertyValues.update here, as no invalid values should be
|
||||||
# assigned to properties, even not before checkProperties
|
# assigned to properties, even not before checkProperties
|
||||||
for k,v in kwds.items():
|
for k, v in kwds.items():
|
||||||
self.setProperty(k, v)
|
self.setProperty(k, v)
|
||||||
|
|
||||||
def __repr__(self):
|
def inherit(self, cls, owner):
|
||||||
props = []
|
for base in owner.__bases__:
|
||||||
for k, prop in sorted(self.__class__.properties.items()):
|
if hasattr(base, self.name):
|
||||||
v = self.properties.get(k, prop.default)
|
aobj = getattr(base, 'accessibles', {}).get(self.name)
|
||||||
if v != prop.default:
|
if aobj:
|
||||||
props.append('%s=%r' % (k, v))
|
if not isinstance(aobj, cls):
|
||||||
return '%s(%s, ctr=%d)' % (self.__class__.__name__, ', '.join(props), self.ctr)
|
raise ProgrammingError('%s %s.%s can not inherit from a %s' %
|
||||||
|
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
|
||||||
|
# inherit from aobj
|
||||||
|
for pname, value in aobj.propertyValues.items():
|
||||||
|
if pname not in self.propertyValues:
|
||||||
|
self.propertyValues[pname] = value
|
||||||
|
break
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return self.properties
|
return self.propertyValues
|
||||||
|
|
||||||
def override(self, from_object=None, **kwds):
|
def override(self, value=UNSET, **kwds):
|
||||||
"""return a copy of ourselfs, modified by <other>"""
|
"""return a copy, overridden by a bare attribute
|
||||||
props = dict(self.properties, ctr=self.ctr)
|
|
||||||
if from_object:
|
and/or some properties"""
|
||||||
props.update(from_object.kwds)
|
raise NotImplementedError
|
||||||
props.update(kwds)
|
|
||||||
props['datatype'] = props['datatype'].copy()
|
|
||||||
return type(self)(inherit=False, internally_called=True, **props)
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""return a copy of ourselfs"""
|
"""return a (deep) copy of ourselfs"""
|
||||||
props = dict(self.properties, ctr=self.ctr)
|
raise NotImplementedError
|
||||||
# deep copy, as datatype might be altered from config
|
|
||||||
props['datatype'] = props['datatype'].copy()
|
|
||||||
return type(self)(inherit=False, internally_called=True, **props)
|
|
||||||
|
|
||||||
def for_export(self):
|
def for_export(self):
|
||||||
"""prepare for serialisation"""
|
"""prepare for serialisation"""
|
||||||
return self.exportProperties()
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
props = []
|
||||||
|
for k, prop in sorted(self.propertyDict.items()):
|
||||||
|
v = self.propertyValues.get(k, prop.default)
|
||||||
|
if v != prop.default:
|
||||||
|
props.append('%s=%r' % (k, v))
|
||||||
|
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
|
||||||
|
|
||||||
|
|
||||||
class Parameter(Accessible):
|
class Parameter(Accessible):
|
||||||
@ -87,40 +94,43 @@ class Parameter(Accessible):
|
|||||||
|
|
||||||
:param description: description
|
:param description: description
|
||||||
:param datatype: the datatype
|
:param datatype: the datatype
|
||||||
:param inherit: whether properties not given should be inherited.
|
:param inherit: whether properties not given should be inherited
|
||||||
defaults to True when datatype or description is missing, else to False
|
|
||||||
:param reorder: when True, put this parameter after all inherited items in the accessible list
|
|
||||||
:param kwds: optional properties
|
:param kwds: optional properties
|
||||||
:param ctr: (for internal use only)
|
|
||||||
:param internally_used: (for internal use only)
|
|
||||||
"""
|
"""
|
||||||
# storage for Parameter settings + value + qualifiers
|
# storage for Parameter settings + value + qualifiers
|
||||||
|
|
||||||
properties = {
|
description = Property(
|
||||||
'description': Property('mandatory description of the parameter', TextType(),
|
'mandatory description of the parameter', TextType(),
|
||||||
extname='description', mandatory=True),
|
extname='description', mandatory=True)
|
||||||
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
datatype = Property(
|
||||||
extname='datainfo', mandatory=True),
|
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||||
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
|
extname='datainfo', mandatory=True)
|
||||||
extname='readonly', default=True),
|
readonly = Property(
|
||||||
'group': Property('optional parameter group this parameter belongs to', StringType(),
|
'not changeable via SECoP (default True)', BoolType(),
|
||||||
extname='group', default=''),
|
extname='readonly', default=True, export='always')
|
||||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
group = Property(
|
||||||
extname='visibility', default=1),
|
'optional parameter group this parameter belongs to', StringType(),
|
||||||
'constant': Property('optional constant value for constant parameters', ValueType(),
|
extname='group', default='')
|
||||||
extname='constant', default=None, mandatory=False),
|
visibility = Property(
|
||||||
'default': Property('[internal] default (startup) value of this parameter '
|
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
'if it can not be read from the hardware',
|
extname='visibility', default=1)
|
||||||
ValueType(), export=False, default=None, mandatory=False),
|
constant = Property(
|
||||||
'export': Property('''
|
'optional constant value for constant parameters', ValueType(),
|
||||||
[internal] export settings
|
extname='constant', default=None)
|
||||||
|
default = Property(
|
||||||
|
'''[internal] default (startup) value of this parameter
|
||||||
|
|
||||||
|
if it can not be read from the hardware''', ValueType(),
|
||||||
|
export=False, default=None)
|
||||||
|
export = Property(
|
||||||
|
'''[internal] export settings
|
||||||
|
|
||||||
* False: not accessible via SECoP.
|
* False: not accessible via SECoP.
|
||||||
* True: exported, name automatic.
|
* True: exported, name automatic.
|
||||||
* a string: exported with custom name''',
|
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||||
OrType(BoolType(), StringType()), export=False, default=True),
|
export=False, default=True)
|
||||||
'poll': Property('''
|
poll = Property(
|
||||||
[internal] polling indicator
|
'''[internal] polling indicator
|
||||||
|
|
||||||
may be:
|
may be:
|
||||||
|
|
||||||
@ -132,20 +142,30 @@ class Parameter(Accessible):
|
|||||||
* 3 (REGULAR), polled with pollperiod
|
* 3 (REGULAR), polled with pollperiod
|
||||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||||
else polled with pollperiod
|
else polled with pollperiod
|
||||||
''',
|
''', NoneOr(IntRange()),
|
||||||
NoneOr(IntRange()), export=False, default=None),
|
export=False, default=None)
|
||||||
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
|
needscfg = Property(
|
||||||
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
|
'[internal] needs value in config', NoneOr(BoolType()),
|
||||||
settable=False, default=False),
|
export=False, default=None)
|
||||||
'handler': Property('[internal] overload the standard read and write functions',
|
optional = Property(
|
||||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
'[internal] is this parameter optional?', BoolType(),
|
||||||
'initwrite': Property('[internal] write this parameter on initialization'
|
export=False, settable=False, default=False)
|
||||||
' (default None: write if given in config)',
|
handler = Property(
|
||||||
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
|
'[internal] overload the standard read and write functions', ValueType(),
|
||||||
}
|
export=False, default=None, settable=False)
|
||||||
|
initwrite = Property(
|
||||||
|
'''[internal] write this parameter on initialization
|
||||||
|
|
||||||
def __init__(self, description=None, datatype=None, inherit=True, *,
|
default None: write if given in config''', NoneOr(BoolType()),
|
||||||
reorder=False, ctr=None, internally_called=False, **kwds):
|
export=False, default=None, settable=False)
|
||||||
|
|
||||||
|
# used on the instance copy only
|
||||||
|
value = None
|
||||||
|
timestamp = 0
|
||||||
|
readerror = None
|
||||||
|
|
||||||
|
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
|
||||||
|
super().__init__(**kwds)
|
||||||
if datatype is not None:
|
if datatype is not None:
|
||||||
if not isinstance(datatype, DataType):
|
if not isinstance(datatype, DataType):
|
||||||
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
||||||
@ -154,57 +174,92 @@ class Parameter(Accessible):
|
|||||||
else:
|
else:
|
||||||
raise ProgrammingError(
|
raise ProgrammingError(
|
||||||
'datatype MUST be derived from class DataType!')
|
'datatype MUST be derived from class DataType!')
|
||||||
kwds['datatype'] = datatype
|
self.datatype = datatype
|
||||||
|
if 'default' in kwds:
|
||||||
|
self.default = datatype(kwds['default'])
|
||||||
|
|
||||||
if description is not None:
|
if description is not None:
|
||||||
if not internally_called:
|
self.description = inspect.cleandoc(description)
|
||||||
description = inspect.cleandoc(description)
|
|
||||||
kwds['description'] = description
|
|
||||||
|
|
||||||
unit = kwds.pop('unit', None)
|
# save for __set_name__
|
||||||
if unit is not None and datatype: # for legacy code only
|
self._inherit = inherit
|
||||||
datatype.setProperty('unit', unit)
|
self._unit = unit # for legacy code only
|
||||||
|
self._constant = constant
|
||||||
|
|
||||||
constant = kwds.get('constant')
|
def __get__(self, instance, owner):
|
||||||
if constant is not None:
|
# not used yet
|
||||||
constant = datatype(constant)
|
if instance is None:
|
||||||
|
return self
|
||||||
|
return instance.parameters[self.name].value
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
obj.announceUpdate(self.name, value)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
if self._inherit:
|
||||||
|
self.inherit(Parameter, owner)
|
||||||
|
|
||||||
|
# check for completeness
|
||||||
|
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
|
||||||
|
if missing_properties:
|
||||||
|
raise ProgrammingError('Parameter %s.%s needs a %s' %
|
||||||
|
(owner.__name__, name, ' and a '.join(missing_properties)))
|
||||||
|
if self._unit is not None:
|
||||||
|
self.datatype.setProperty('unit', self._unit)
|
||||||
|
|
||||||
|
if self._constant is not None:
|
||||||
|
constant = self.datatype(self._constant)
|
||||||
# The value of the `constant` property should be the
|
# The value of the `constant` property should be the
|
||||||
# serialised version of the constant, or unset
|
# serialised version of the constant, or unset
|
||||||
kwds['constant'] = datatype.export_value(constant)
|
self.constant = self.datatype.export_value(constant)
|
||||||
kwds['readonly'] = True
|
self.readonly = True
|
||||||
if internally_called: # fixes in case datatype has changed
|
|
||||||
default = kwds.get('default')
|
if 'default' in self.propertyValues:
|
||||||
if default is not None:
|
# fixes in case datatype has changed
|
||||||
try:
|
try:
|
||||||
datatype(default)
|
self.datatype(self.default)
|
||||||
except BadValueError:
|
except BadValueError:
|
||||||
# clear default, if it does not match datatype
|
# clear default, if it does not match datatype
|
||||||
kwds['default'] = None
|
self.propertyValues.pop('default')
|
||||||
super().__init__(ctr, **kwds)
|
|
||||||
if inherit:
|
|
||||||
if reorder:
|
|
||||||
kwds['ctr'] = next(object_counter)
|
|
||||||
if unit is not None:
|
|
||||||
kwds['unit'] = unit
|
|
||||||
self.kwds = kwds # contains only the items which must be overwritten
|
|
||||||
|
|
||||||
# internal caching: value and timestamp of last change...
|
if self.export is True:
|
||||||
self.value = self.default
|
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||||
self.timestamp = 0
|
self.export = name
|
||||||
self.readerror = None # if not None, indicates that last read was not successful
|
else:
|
||||||
|
self.export = '_' + name
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
# deep copy, as datatype might be altered from config
|
||||||
|
res = Parameter()
|
||||||
|
res.name = self.name
|
||||||
|
res.init(self.propertyValues)
|
||||||
|
res.datatype = res.datatype.copy()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def override(self, value=UNSET, **kwds):
|
||||||
|
res = self.copy()
|
||||||
|
res.init(kwds)
|
||||||
|
if value is not UNSET:
|
||||||
|
res.value = res.datatype(value)
|
||||||
|
return res
|
||||||
|
|
||||||
def export_value(self):
|
def export_value(self):
|
||||||
return self.datatype.export_value(self.value)
|
return self.datatype.export_value(self.value)
|
||||||
|
|
||||||
|
def for_export(self):
|
||||||
|
return dict(self.exportProperties(), readonly=self.readonly)
|
||||||
|
|
||||||
def getProperties(self):
|
def getProperties(self):
|
||||||
"""get also properties of datatype"""
|
"""get also properties of datatype"""
|
||||||
superProp = super().getProperties().copy()
|
super_prop = super().getProperties().copy()
|
||||||
superProp.update(self.datatype.getProperties())
|
super_prop.update(self.datatype.getProperties())
|
||||||
return superProp
|
return super_prop
|
||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
"""set also properties of datatype"""
|
"""set also properties of datatype"""
|
||||||
if key in self.__class__.properties:
|
if key in self.propertyDict:
|
||||||
super().setProperty(key, value)
|
super().setProperty(key, value)
|
||||||
else:
|
else:
|
||||||
self.datatype.setProperty(key, value)
|
self.datatype.setProperty(key, value)
|
||||||
@ -213,208 +268,168 @@ class Parameter(Accessible):
|
|||||||
super().checkProperties()
|
super().checkProperties()
|
||||||
self.datatype.checkProperties()
|
self.datatype.checkProperties()
|
||||||
|
|
||||||
def for_export(self):
|
|
||||||
"""prepare for serialisation
|
|
||||||
|
|
||||||
readonly is mandatory for serialisation, but not for declaration in classes
|
|
||||||
"""
|
|
||||||
r = super().for_export()
|
|
||||||
if 'readonly' not in r:
|
|
||||||
r['readonly'] = self.__class__.properties['readonly'].default
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
class UnusedClass:
|
|
||||||
# do not derive anything from this!
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Parameters(OrderedDict):
|
|
||||||
"""class storage for Parameters"""
|
|
||||||
def __init__(self, *args, **kwds):
|
|
||||||
self.exported = {} # only for lookups!
|
|
||||||
super(Parameters, self).__init__(*args, **kwds)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if value.export:
|
|
||||||
if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)):
|
|
||||||
value.properties['export'] = key
|
|
||||||
else:
|
|
||||||
value.properties['export'] = '_' + key
|
|
||||||
self.exported[value.export] = key
|
|
||||||
super(Parameters, self).__setitem__(key, value)
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return super(Parameters, self).__getitem__(self.exported.get(item, item))
|
|
||||||
|
|
||||||
|
|
||||||
class Commands(Parameters):
|
|
||||||
"""class storage for Commands"""
|
|
||||||
|
|
||||||
|
|
||||||
class Override:
|
|
||||||
"""Stores the overrides to be applied to a Parameter
|
|
||||||
|
|
||||||
note: overrides are applied by the metaclass during class creating
|
|
||||||
reorder=True: use position of Override instead of inherited for the order
|
|
||||||
"""
|
|
||||||
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
|
|
||||||
self.kwds = kwds
|
|
||||||
# allow to override description and datatype without keyword
|
|
||||||
if description:
|
|
||||||
self.kwds['description'] = description
|
|
||||||
if datatype is not None:
|
|
||||||
self.kwds['datatype'] = datatype
|
|
||||||
if reorder: # result from apply must use new ctr from Override
|
|
||||||
self.kwds['ctr'] = next(object_counter)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%s)' % (self.__class__.__name__, ', '.join(
|
|
||||||
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
|
|
||||||
|
|
||||||
def apply(self, obj):
|
|
||||||
return obj.override(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Accessible):
|
class Command(Accessible):
|
||||||
# to be merged with usercommand
|
|
||||||
properties = {
|
|
||||||
'description': Property('description of the Command', TextType(),
|
|
||||||
extname='description', export=True, mandatory=True),
|
|
||||||
'group': Property('optional command group of the command.', StringType(),
|
|
||||||
extname='group', export=True, default=''),
|
|
||||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
|
||||||
extname='visibility', export=True, default=1),
|
|
||||||
'export': Property('''
|
|
||||||
[internal] export settings
|
|
||||||
|
|
||||||
* False: not accessible via SECoP.
|
|
||||||
* True: exported, name automatic.
|
|
||||||
* a string: exported with custom name''',
|
|
||||||
OrType(BoolType(), StringType()), export=False, default=True),
|
|
||||||
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
|
|
||||||
BoolType(), export=False, default=False, settable=False),
|
|
||||||
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
|
|
||||||
DataTypeType(), extname='datainfo', mandatory=True),
|
|
||||||
'argument': Property('datatype of the argument to the command, or None',
|
|
||||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
|
||||||
'result': Property('datatype of the result from the command, or None',
|
|
||||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, description=None, *, reorder=False, inherit=True,
|
|
||||||
internally_called=False, ctr=None, **kwds):
|
|
||||||
if internally_called:
|
|
||||||
inherit = False
|
|
||||||
# make sure either all or no datatype info is in kwds
|
|
||||||
if 'argument' in kwds or 'result' in kwds:
|
|
||||||
datatype = CommandType(kwds.get('argument'), kwds.get('result'))
|
|
||||||
else:
|
|
||||||
datatype = kwds.get('datatype')
|
|
||||||
datainfo = {}
|
|
||||||
datainfo['datatype'] = datatype or CommandType()
|
|
||||||
datainfo['argument'] = datainfo['datatype'].argument
|
|
||||||
datainfo['result'] = datainfo['datatype'].result
|
|
||||||
if datatype:
|
|
||||||
kwds.update(datainfo)
|
|
||||||
if description is not None:
|
|
||||||
kwds['description'] = description
|
|
||||||
if datatype:
|
|
||||||
datainfo = {}
|
|
||||||
super(Command, self).__init__(ctr, **datainfo, **kwds)
|
|
||||||
if inherit:
|
|
||||||
if reorder:
|
|
||||||
kwds['ctr'] = next(object_counter)
|
|
||||||
self.kwds = kwds
|
|
||||||
|
|
||||||
@property
|
|
||||||
def argument(self):
|
|
||||||
return self.datatype.argument
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result(self):
|
|
||||||
return self.datatype.result
|
|
||||||
|
|
||||||
|
|
||||||
class usercommand(Command):
|
|
||||||
"""decorator to turn a method into a command
|
"""decorator to turn a method into a command
|
||||||
|
|
||||||
:param argument: the datatype of the argument or None
|
:param argument: the datatype of the argument or None
|
||||||
:param result: the datatype of the result or None
|
:param result: the datatype of the result or None
|
||||||
:param inherit: whether properties not given should be inherited.
|
:param inherit: whether properties not given should be inherited
|
||||||
defaults to True when datatype or description is missing, else to False
|
|
||||||
:param reorder: when True, put this command after all inherited items in the accessible list
|
|
||||||
:param kwds: optional properties
|
:param kwds: optional properties
|
||||||
|
|
||||||
{all properties}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
description = Property(
|
||||||
|
'description of the Command', TextType(),
|
||||||
|
extname='description', export=True, mandatory=True)
|
||||||
|
group = Property(
|
||||||
|
'optional command group of the command.', StringType(),
|
||||||
|
extname='group', export=True, default='')
|
||||||
|
visibility = Property(
|
||||||
|
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
|
extname='visibility', export=True, default=1)
|
||||||
|
export = Property(
|
||||||
|
'''[internal] export settings
|
||||||
|
|
||||||
|
* False: not accessible via SECoP.
|
||||||
|
* True: exported, name automatic.
|
||||||
|
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||||
|
export=False, default=True)
|
||||||
|
optional = Property(
|
||||||
|
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||||
|
export=False, default=False, settable=False)
|
||||||
|
datatype = Property(
|
||||||
|
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||||
|
DataTypeType(), extname='datainfo', export='always')
|
||||||
|
argument = Property(
|
||||||
|
'datatype of the argument to the command, or None', NoneOr(DataTypeType()),
|
||||||
|
export=False, mandatory=True)
|
||||||
|
result = Property(
|
||||||
|
'datatype of the result from the command, or None', NoneOr(DataTypeType()),
|
||||||
|
export=False, mandatory=True)
|
||||||
|
|
||||||
func = None
|
func = None
|
||||||
|
|
||||||
def __init__(self, argument=False, result=None, inherit=True, **kwds):
|
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||||
|
super().__init__(**kwds)
|
||||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||||
# normal case
|
# normal case
|
||||||
self.func = None
|
|
||||||
if argument is False and result:
|
if argument is False and result:
|
||||||
argument = None
|
argument = None
|
||||||
if argument is not False:
|
if argument is not False:
|
||||||
if isinstance(argument, (tuple, list)):
|
if isinstance(argument, (tuple, list)):
|
||||||
# goodie: allow declaring multiple arguments as a tuple
|
# goodie: treat as TupleOf
|
||||||
# TODO: check that calling works properly
|
|
||||||
argument = TupleOf(*argument)
|
argument = TupleOf(*argument)
|
||||||
kwds['argument'] = argument
|
self.argument = argument
|
||||||
kwds['result'] = result
|
self.result = result
|
||||||
self.kwds = kwds
|
|
||||||
else:
|
else:
|
||||||
# goodie: allow @usercommand instead of @usercommand()
|
# goodie: allow @Command instead of @Command()
|
||||||
self.func = argument # this is the wrapped method!
|
self.func = argument # this is the wrapped method!
|
||||||
if argument.__doc__ is not None:
|
if argument.__doc__:
|
||||||
kwds['description'] = argument.__doc__
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
self.name = self.func.__name__
|
self.name = self.func.__name__
|
||||||
super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds)
|
self._inherit = inherit # save for __set_name__
|
||||||
|
|
||||||
def override(self, from_object=None, **kwds):
|
|
||||||
result = super().override(from_object, **kwds)
|
|
||||||
func = kwds.pop('func', from_object.func if from_object else None)
|
|
||||||
if func:
|
|
||||||
result(func) # pylint: disable=not-callable
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
def __set_name__(self, owner, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
if self.func is None:
|
||||||
|
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
|
||||||
|
(owner.__name__, name))
|
||||||
|
if self._inherit:
|
||||||
|
self.inherit(Command, owner)
|
||||||
|
|
||||||
|
self.datatype = CommandType(self.argument, self.result)
|
||||||
|
if self.export is True:
|
||||||
|
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||||
|
self.export = name
|
||||||
|
else:
|
||||||
|
self.export = '_' + name
|
||||||
|
|
||||||
def __get__(self, obj, owner=None):
|
def __get__(self, obj, owner=None):
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return self
|
return self
|
||||||
if not self.func:
|
if not self.func:
|
||||||
raise ProgrammingError('usercommand %s not properly configured' % self.name)
|
raise ProgrammingError('Command %s not properly configured' % self.name)
|
||||||
return self.func.__get__(obj, owner)
|
return self.func.__get__(obj, owner)
|
||||||
|
|
||||||
def __call__(self, fun):
|
def __call__(self, func):
|
||||||
description = self.kwds.get('description') or fun.__doc__
|
if 'description' not in self.propertyValues and func.__doc__:
|
||||||
self.properties['description'] = self.kwds['description'] = description
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
self.name = fun.__name__
|
self.func = func
|
||||||
self.func = fun
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
res = Command()
|
||||||
|
res.name = self.name
|
||||||
|
res.func = self.func
|
||||||
|
res.init(self.propertyValues)
|
||||||
|
if res.argument:
|
||||||
|
res.argument = res.argument.copy()
|
||||||
|
if res.result:
|
||||||
|
res.result = res.result.copy()
|
||||||
|
res.datatype = CommandType(res.argument, res.result)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def override(self, value=UNSET, **kwds):
|
||||||
|
res = self.copy()
|
||||||
|
res.init(kwds)
|
||||||
|
if value is not UNSET:
|
||||||
|
res.func = value
|
||||||
|
return res
|
||||||
|
|
||||||
|
def do(self, module_obj, argument):
|
||||||
|
"""perform function call
|
||||||
|
|
||||||
|
:param module_obj: the module on which the command is to be executed
|
||||||
|
:param argument: the argument from the do command
|
||||||
|
:returns: the return value converted to the result type
|
||||||
|
|
||||||
|
- when the argument type is TupleOf, the function is called with multiple arguments
|
||||||
|
- when the argument type is StructOf, the function is called with keyworded arguments
|
||||||
|
- the validity of the argument/s is/are checked
|
||||||
|
"""
|
||||||
|
func = self.__get__(module_obj)
|
||||||
|
if self.argument:
|
||||||
|
# validate
|
||||||
|
argument = self.argument(argument)
|
||||||
|
if isinstance(self.argument, TupleOf):
|
||||||
|
res = func(*argument)
|
||||||
|
elif isinstance(self.argument, StructOf):
|
||||||
|
res = func(**argument)
|
||||||
|
else:
|
||||||
|
res = func(argument)
|
||||||
|
else:
|
||||||
|
if argument is not None:
|
||||||
|
raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name))
|
||||||
|
res = func()
|
||||||
|
if self.result:
|
||||||
|
return self.result(res)
|
||||||
|
return None # silently ignore the result from the method
|
||||||
|
|
||||||
|
def for_export(self):
|
||||||
|
return self.exportProperties()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
result = super().__repr__()
|
||||||
|
return result[:-1] + ', %r)' % self.func if self.func else result
|
||||||
|
|
||||||
|
|
||||||
# list of predefined accessibles with their type
|
# list of predefined accessibles with their type
|
||||||
PREDEFINED_ACCESSIBLES = dict(
|
PREDEFINED_ACCESSIBLES = dict(
|
||||||
value = Parameter,
|
value=Parameter,
|
||||||
status = Parameter,
|
status=Parameter,
|
||||||
target = Parameter,
|
target=Parameter,
|
||||||
pollinterval = Parameter,
|
pollinterval=Parameter,
|
||||||
ramp = Parameter,
|
ramp=Parameter,
|
||||||
user_ramp = Parameter,
|
user_ramp=Parameter,
|
||||||
setpoint = Parameter,
|
setpoint=Parameter,
|
||||||
time_to_target = Parameter,
|
time_to_target=Parameter,
|
||||||
unit = Parameter, # reserved name
|
unit=Parameter, # reserved name
|
||||||
loglevel = Parameter, # reserved name
|
loglevel=Parameter, # reserved name
|
||||||
mode = Parameter, # reserved name
|
mode=Parameter, # reserved name
|
||||||
stop = Command,
|
stop=Command,
|
||||||
reset = Command,
|
reset=Command,
|
||||||
go = Command,
|
go=Command,
|
||||||
abort = Command,
|
abort=Command,
|
||||||
shutdown = Command,
|
shutdown=Command,
|
||||||
communicate = Command,
|
communicate=Command,
|
||||||
)
|
)
|
||||||
|
@ -23,27 +23,44 @@
|
|||||||
"""Define validated data types."""
|
"""Define validated data types."""
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
from secop.errors import ConfigError, ProgrammingError, BadValueError
|
||||||
|
|
||||||
|
|
||||||
def flatten_dict(dictname, itemcls, attrs, remove=True):
|
class HasDescriptorMeta(type):
|
||||||
properties = {}
|
def __new__(cls, name, bases, attrs):
|
||||||
# allow to declare properties directly as class attribute
|
newtype = type.__new__(cls, name, bases, attrs)
|
||||||
# all these attributes are removed
|
if sys.version_info < (3, 6):
|
||||||
for k, v in attrs.items():
|
# support older python versions
|
||||||
if isinstance(v, tuple) and v and isinstance(v[0], itemcls):
|
for key, attr in attrs.items():
|
||||||
# this might happen when migrating from old to new style
|
if hasattr(attr, '__set_name__'):
|
||||||
raise ProgrammingError('declared %r with trailing comma' % k)
|
attr.__set_name__(newtype, key)
|
||||||
if isinstance(v, itemcls):
|
newtype.__init_subclass__()
|
||||||
properties[k] = v
|
return newtype
|
||||||
if remove:
|
|
||||||
for k in properties:
|
|
||||||
attrs.pop(k)
|
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||||
properties.update(attrs.get(dictname, {}))
|
@classmethod
|
||||||
attrs[dictname] = properties
|
def __init_subclass__(cls):
|
||||||
|
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||||
|
bad = [k for k, v in cls.__dict__.items()
|
||||||
|
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||||
|
if bad:
|
||||||
|
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filterDescriptors(cls, filter_type):
|
||||||
|
res = {}
|
||||||
|
for name in dir(cls):
|
||||||
|
desc = getattr(cls, name, None)
|
||||||
|
if isinstance(desc, filter_type):
|
||||||
|
res[name] = desc
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
UNSET = object() # an unset value, not even None
|
||||||
|
|
||||||
|
|
||||||
# storage for 'properties of a property'
|
# storage for 'properties of a property'
|
||||||
@ -56,7 +73,8 @@ class Property:
|
|||||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||||
when they match the default
|
when they match the default
|
||||||
:param extname: external name
|
:param extname: external name
|
||||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
|
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
|
||||||
|
special value 'always': export also when matching the default
|
||||||
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
||||||
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
||||||
:param settable: settable from the cfg file
|
:param settable: settable from the cfg file
|
||||||
@ -64,142 +82,125 @@ class Property:
|
|||||||
|
|
||||||
# note: this is intended to be used on base classes.
|
# note: this is intended to be used on base classes.
|
||||||
# the VALUES of the properties are on the instances!
|
# the VALUES of the properties are on the instances!
|
||||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
|
||||||
|
settable=True, value=UNSET, name=''):
|
||||||
if not callable(datatype):
|
if not callable(datatype):
|
||||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||||
self.description = inspect.cleandoc(description)
|
self.description = inspect.cleandoc(description)
|
||||||
self.default = datatype.default if default is None else datatype(default)
|
self.default = datatype.default if default is UNSET else datatype(default)
|
||||||
self.datatype = datatype
|
self.datatype = datatype
|
||||||
self.extname = extname
|
self.extname = extname
|
||||||
self.export = export or bool(extname)
|
self.export = export or bool(extname)
|
||||||
if mandatory is None:
|
if mandatory is None:
|
||||||
mandatory = default is None
|
mandatory = default is UNSET
|
||||||
self.mandatory = mandatory
|
self.mandatory = mandatory
|
||||||
self.settable = settable or mandatory # settable means settable from the cfg file
|
self.settable = settable or mandatory # settable means settable from the cfg file
|
||||||
|
self.value = UNSET if value is UNSET else datatype(value)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
return instance.propertyValues.get(self.name, self.default)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
instance.propertyValues[self.name] = self.datatype(value)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.name = name
|
||||||
|
if self.export and not self.extname:
|
||||||
|
self.extname = '_' + name
|
||||||
|
if self.description == '_':
|
||||||
|
# the programmer indicates, that the name is already speaking for itself
|
||||||
|
self.description = name.replace('_', ' ')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
extras = ['default=%s' % repr(self.default)]
|
||||||
self.description, self.datatype, self.default, self.extname, self.export,
|
if self.export:
|
||||||
self.mandatory, self.settable)
|
extras.append('extname=%r' % self.extname)
|
||||||
|
extras.append('export=%r' % self.export)
|
||||||
|
if self.mandatory:
|
||||||
|
extras.append('mandatory=True')
|
||||||
|
if not self.settable:
|
||||||
|
extras.append('settable=False')
|
||||||
|
if self.value is not UNSET:
|
||||||
|
extras.append('value=%s' % repr(self.value))
|
||||||
|
if not self.name:
|
||||||
|
extras.append('name=%r' % self.name)
|
||||||
|
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
|
||||||
|
|
||||||
|
|
||||||
class Properties(OrderedDict):
|
class HasProperties(HasDescriptors):
|
||||||
"""a collection of `Property` objects
|
propertyValues = None
|
||||||
|
|
||||||
checks values upon assignment.
|
|
||||||
You can either assign a Property object, or a value
|
|
||||||
(which must pass the validator of the already existing Property)
|
|
||||||
"""
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if not isinstance(value, Property):
|
|
||||||
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
|
||||||
# make sure, extname is valid if export is True
|
|
||||||
if not value.extname and value.export:
|
|
||||||
value.extname = '_%s' % key # generate custom key
|
|
||||||
elif value.extname and not value.export:
|
|
||||||
value.export = True
|
|
||||||
OrderedDict.__setitem__(self, key, value)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
raise ProgrammingError('deleting Properties is not supported!')
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMeta(type):
|
|
||||||
"""Metaclass for HasProperties
|
|
||||||
|
|
||||||
joining the class's properties with those of base classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __new__(cls, name, bases, attrs):
|
|
||||||
newtype = type.__new__(cls, name, bases, attrs)
|
|
||||||
if '__constructed__' in attrs:
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
flatten_dict('properties', Property, attrs)
|
|
||||||
newtype = cls.__join_properties__(newtype, name, bases, attrs)
|
|
||||||
|
|
||||||
attrs['__constructed__'] = True
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __join_properties__(cls, newtype, name, bases, attrs):
|
|
||||||
# merge properties from all sub-classes
|
|
||||||
properties = Properties()
|
|
||||||
for base in reversed(bases):
|
|
||||||
properties.update(getattr(base, "properties", {}))
|
|
||||||
# update with properties from new class
|
|
||||||
properties.update(attrs.get('properties', {}))
|
|
||||||
newtype.properties = properties
|
|
||||||
|
|
||||||
# generate getters
|
|
||||||
for k, po in properties.items():
|
|
||||||
|
|
||||||
def getter(self, pname=k):
|
|
||||||
val = self.__class__.properties[pname].default
|
|
||||||
return self.properties.get(pname, val)
|
|
||||||
|
|
||||||
if k in attrs and not isinstance(attrs[k], (property, Property)):
|
|
||||||
if callable(attrs[k]):
|
|
||||||
raise ProgrammingError('%r: property %r collides with method'
|
|
||||||
% (newtype, k))
|
|
||||||
# store the attribute value for putting on the instance later
|
|
||||||
try:
|
|
||||||
# for inheritance reasons, it seems best to store it as a renamed attribute
|
|
||||||
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
|
|
||||||
except BadValueError:
|
|
||||||
raise ProgrammingError('%r: property %r can not be set to %r'
|
|
||||||
% (newtype, k, attrs[k]))
|
|
||||||
setattr(newtype, k, property(getter))
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
|
|
||||||
class HasProperties(metaclass=PropertyMeta):
|
|
||||||
properties = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HasProperties, self).__init__()
|
super(HasProperties, self).__init__()
|
||||||
self.initProperties()
|
|
||||||
|
|
||||||
def initProperties(self):
|
|
||||||
# store property values in the instance, keep descriptors on the class
|
# store property values in the instance, keep descriptors on the class
|
||||||
self.properties = {}
|
self.propertyValues = {}
|
||||||
# pre-init with properties default value (if any)
|
# pre-init
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
value = getattr(self, '_initProp_' + pn, self)
|
if po.value is not UNSET:
|
||||||
if value is not self: # property value was given as attribute
|
self.setProperty(pn, po.value)
|
||||||
self.properties[pn] = value
|
|
||||||
elif not po.mandatory:
|
@classmethod
|
||||||
self.properties[pn] = po.default
|
def __init_subclass__(cls):
|
||||||
|
super().__init_subclass__()
|
||||||
|
# raise an error when an attribute is a tuple with one single descriptor as element
|
||||||
|
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||||
|
bad = [k for k, v in cls.__dict__.items()
|
||||||
|
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||||
|
if bad:
|
||||||
|
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||||
|
properties = {}
|
||||||
|
for base in cls.__bases__:
|
||||||
|
properties.update(getattr(base, 'propertyDict', {}))
|
||||||
|
properties.update(cls.filterDescriptors(Property))
|
||||||
|
cls.propertyDict = properties
|
||||||
|
# treat overriding properties with bare values
|
||||||
|
for pn, po in properties.items():
|
||||||
|
value = cls.__dict__.get(pn, po)
|
||||||
|
if not isinstance(value, Property): # attribute is a bare value
|
||||||
|
po = Property(**po.__dict__)
|
||||||
|
try:
|
||||||
|
po.value = po.datatype(value)
|
||||||
|
except BadValueError:
|
||||||
|
for base in cls.__bases__:
|
||||||
|
if pn in getattr(base, 'propertyDict', {}):
|
||||||
|
if callable(value):
|
||||||
|
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||||
|
(cls.__name__, pn, base.__name__))
|
||||||
|
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||||
|
(cls.__name__, pn, value))
|
||||||
|
cls.propertyDict[pn] = po
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
"""validates properties and checks for min... <= max..."""
|
"""validates properties and checks for min... <= max..."""
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
if po.export and po.mandatory:
|
if po.mandatory:
|
||||||
if pn not in self.properties:
|
if pn not in self.propertyDict:
|
||||||
name = getattr(self, 'name', self.__class__.__name__)
|
name = getattr(self, 'name', self.__class__.__name__)
|
||||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||||
# apply validator (which may complain further)
|
# apply validator (which may complain further)
|
||||||
self.properties[pn] = po.datatype(self.properties[pn])
|
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
if pn.startswith('min'):
|
if pn.startswith('min'):
|
||||||
maxname = 'max' + pn[3:]
|
maxname = 'max' + pn[3:]
|
||||||
minval = self.properties[pn]
|
minval = self.propertyValues.get(pn, po.default)
|
||||||
maxval = self.properties.get(maxname, minval)
|
maxval = self.propertyValues.get(maxname, minval)
|
||||||
if minval > maxval:
|
if minval > maxval:
|
||||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||||
|
|
||||||
|
|
||||||
def getProperties(self):
|
def getProperties(self):
|
||||||
return self.__class__.properties
|
return self.propertyDict
|
||||||
|
|
||||||
def exportProperties(self):
|
def exportProperties(self):
|
||||||
# export properties which have
|
# export properties which have
|
||||||
# export=True and
|
# export=True and
|
||||||
# mandatory=True or non_default=True
|
# mandatory=True or non_default=True
|
||||||
res = {}
|
res = {}
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
val = self.properties.get(pn, None)
|
val = self.propertyValues.get(pn, po.default)
|
||||||
if po.export and (po.mandatory or val != po.default):
|
if po.export and (po.export == 'always' or val != po.default):
|
||||||
try:
|
try:
|
||||||
val = po.datatype.export_value(val)
|
val = po.datatype.export_value(val)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -208,4 +209,7 @@ class HasProperties(metaclass=PropertyMeta):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
self.properties[key] = self.__class__.properties[key].datatype(value)
|
# this is overwritten by Param.setProperty and DataType.setProperty
|
||||||
|
# in oder to extend setting to inner properties
|
||||||
|
# otherwise direct setting of self.<key> = value is preferred
|
||||||
|
self.propertyValues[key] = self.propertyDict[key].datatype(value)
|
||||||
|
@ -42,7 +42,7 @@ import threading
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from time import time as currenttime
|
from time import time as currenttime
|
||||||
|
|
||||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
||||||
from secop.params import Parameter
|
from secop.params import Parameter
|
||||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||||
@ -109,7 +109,7 @@ class Dispatcher:
|
|||||||
self._subscriptions.setdefault(eventname, set()).add(conn)
|
self._subscriptions.setdefault(eventname, set()).add(conn)
|
||||||
|
|
||||||
def unsubscribe(self, conn, eventname):
|
def unsubscribe(self, conn, eventname):
|
||||||
if not ':' in eventname:
|
if ':' not in eventname:
|
||||||
# also remove 'more specific' subscriptions
|
# also remove 'more specific' subscriptions
|
||||||
for k, v in self._subscriptions.items():
|
for k, v in self._subscriptions.items():
|
||||||
if k.startswith('%s:' % eventname):
|
if k.startswith('%s:' % eventname):
|
||||||
@ -177,7 +177,7 @@ class Dispatcher:
|
|||||||
result = {'modules': OrderedDict()}
|
result = {'modules': OrderedDict()}
|
||||||
for modulename in self._export:
|
for modulename in self._export:
|
||||||
module = self.get_module(modulename)
|
module = self.get_module(modulename)
|
||||||
if not module.properties.get('export', False):
|
if not module.export:
|
||||||
continue
|
continue
|
||||||
# some of these need rework !
|
# some of these need rework !
|
||||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||||
@ -186,7 +186,7 @@ class Dispatcher:
|
|||||||
result['modules'][modulename] = mod_desc
|
result['modules'][modulename] = mod_desc
|
||||||
result['equipment_id'] = self.equipment_id
|
result['equipment_id'] = self.equipment_id
|
||||||
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
||||||
result['version'] = '2019.08'
|
result['version'] = '2021.02'
|
||||||
result.update(self.nodeprops)
|
result.update(self.nodeprops)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -195,40 +195,24 @@ class Dispatcher:
|
|||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
cname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if cmdname is None:
|
cobj = moduleobj.commands.get(cname)
|
||||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
if cobj is None:
|
||||||
cmdspec = moduleobj.commands[cmdname]
|
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||||
if argument is None and cmdspec.datatype.argument is not None:
|
|
||||||
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
|
|
||||||
|
|
||||||
if argument is not None and cmdspec.datatype.argument is None:
|
|
||||||
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
|
|
||||||
|
|
||||||
if cmdspec.datatype.argument:
|
|
||||||
# validate!
|
|
||||||
argument = cmdspec.datatype(argument)
|
|
||||||
|
|
||||||
# now call func
|
# now call func
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
func = getattr(moduleobj, 'do_' + cmdname)
|
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||||
res = func() if argument is None else func(argument)
|
|
||||||
|
|
||||||
# pipe through cmdspec.datatype.result
|
|
||||||
if cmdspec.datatype.result:
|
|
||||||
res = cmdspec.datatype.result(res)
|
|
||||||
|
|
||||||
return res, dict(t=currenttime())
|
|
||||||
|
|
||||||
def _setParameterValue(self, modulename, exportedname, value):
|
def _setParameterValue(self, modulename, exportedname, value):
|
||||||
moduleobj = self.get_module(modulename)
|
moduleobj = self.get_module(modulename)
|
||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if pname is None:
|
pobj = moduleobj.parameters.get(pname)
|
||||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
if pobj is None:
|
||||||
pobj = moduleobj.parameters[pname]
|
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||||
if pobj.constant is not None:
|
if pobj.constant is not None:
|
||||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||||
% (modulename, pname))
|
% (modulename, pname))
|
||||||
@ -252,10 +236,10 @@ class Dispatcher:
|
|||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if pname is None:
|
pobj = moduleobj.parameters.get(pname)
|
||||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
if pobj is None:
|
||||||
pobj = moduleobj.parameters[pname]
|
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||||
if pobj.constant is not None:
|
if pobj.constant is not None:
|
||||||
# really needed? we could just construct a readreply instead....
|
# really needed? we could just construct a readreply instead....
|
||||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||||
@ -321,15 +305,13 @@ class Dispatcher:
|
|||||||
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
||||||
|
|
||||||
def handle_do(self, conn, specifier, data):
|
def handle_do(self, conn, specifier, data):
|
||||||
# XXX: should this be done asyncron? we could just return the reply in
|
|
||||||
# that case
|
|
||||||
modulename, cmd = specifier.split(':', 1)
|
modulename, cmd = specifier.split(':', 1)
|
||||||
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
||||||
|
|
||||||
def handle_ping(self, conn, specifier, data):
|
def handle_ping(self, conn, specifier, data):
|
||||||
if data:
|
if data:
|
||||||
raise ProtocolError('ping requests don\'t take data!')
|
raise ProtocolError('ping requests don\'t take data!')
|
||||||
return (HEARTBEATREPLY, specifier, [None, {'t':currenttime()}])
|
return (HEARTBEATREPLY, specifier, [None, {'t': currenttime()}])
|
||||||
|
|
||||||
def handle_activate(self, conn, specifier, data):
|
def handle_activate(self, conn, specifier, data):
|
||||||
if data:
|
if data:
|
||||||
|
@ -28,14 +28,11 @@ from secop.properties import Property
|
|||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
from secop.lib import get_class
|
from secop.lib import get_class
|
||||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||||
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError
|
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError, BadValueError
|
||||||
|
|
||||||
|
|
||||||
class ProxyModule(HasIodev, Module):
|
class ProxyModule(HasIodev, Module):
|
||||||
properties = {
|
module = Property('remote module name', datatype=StringType(), default='')
|
||||||
'module':
|
|
||||||
Property('remote module name', datatype=StringType(), default=''),
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
_consistency_check_done = False
|
_consistency_check_done = False
|
||||||
@ -55,7 +52,7 @@ class ProxyModule(HasIodev, Module):
|
|||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
if not self.module:
|
if not self.module:
|
||||||
self.properties['module'] = self.name
|
self.module = self.name
|
||||||
self._secnode = self._iodev.secnode
|
self._secnode = self._iodev.secnode
|
||||||
self._secnode.register_callback(self.module, self.updateEvent,
|
self._secnode.register_callback(self.module, self.updateEvent,
|
||||||
self.descriptiveDataChange, self.nodeStateChange)
|
self.descriptiveDataChange, self.nodeStateChange)
|
||||||
@ -103,9 +100,9 @@ class ProxyModule(HasIodev, Module):
|
|||||||
dt = props['datatype']
|
dt = props['datatype']
|
||||||
try:
|
try:
|
||||||
cobj.datatype.compatible(dt)
|
cobj.datatype.compatible(dt)
|
||||||
except Exception:
|
except BadValueError:
|
||||||
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
||||||
% (self.module, pname, pobj.datatype, dt))
|
% (self.module, cname, cobj.datatype, dt))
|
||||||
# what to do if descriptive data does not match?
|
# what to do if descriptive data does not match?
|
||||||
# we might raise an exception, but this would lead to a reconnection,
|
# we might raise an exception, but this would lead to a reconnection,
|
||||||
# which might not help.
|
# which might not help.
|
||||||
@ -141,14 +138,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
|||||||
|
|
||||||
|
|
||||||
class SecNode(Module):
|
class SecNode(Module):
|
||||||
properties = {
|
uri = Property('uri of a SEC node', datatype=StringType())
|
||||||
'uri':
|
|
||||||
Property('uri of a SEC node', datatype=StringType()),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'request':
|
|
||||||
Command('send a request', argument=StringType(), result=StringType())
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
self.secnode = SecopClient(self.uri, self.log)
|
self.secnode = SecopClient(self.uri, self.log)
|
||||||
@ -156,8 +146,9 @@ class SecNode(Module):
|
|||||||
def startModule(self, started_callback):
|
def startModule(self, started_callback):
|
||||||
self.secnode.spawn_connect(started_callback)
|
self.secnode.spawn_connect(started_callback)
|
||||||
|
|
||||||
def do_request(self, msg):
|
@Command(StringType(), result=StringType())
|
||||||
"""for test purposes"""
|
def request(self, msg):
|
||||||
|
"""send a request, for debugging purposes"""
|
||||||
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
||||||
return encode_msg_frame(*reply).decode('utf-8')
|
return encode_msg_frame(*reply).decode('utf-8')
|
||||||
|
|
||||||
@ -184,17 +175,12 @@ def proxy_class(remote_class, name=None):
|
|||||||
else:
|
else:
|
||||||
raise ConfigError('%r is no SECoP module class' % remote_class)
|
raise ConfigError('%r is no SECoP module class' % remote_class)
|
||||||
|
|
||||||
parameters = {}
|
attrs = rcls.propertyDict.copy()
|
||||||
commands = {}
|
|
||||||
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
|
||||||
|
|
||||||
for aname, aobj in rcls.accessibles.items():
|
for aname, aobj in rcls.accessibles.items():
|
||||||
if isinstance(aobj, Parameter):
|
if isinstance(aobj, Parameter):
|
||||||
pobj = aobj.copy()
|
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||||
parameters[aname] = pobj
|
attrs[aname] = pobj
|
||||||
pobj.properties['poll'] = False
|
|
||||||
pobj.properties['handler'] = None
|
|
||||||
pobj.properties['needscfg'] = False
|
|
||||||
|
|
||||||
def rfunc(self, pname=aname):
|
def rfunc(self, pname=aname):
|
||||||
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
||||||
@ -216,12 +202,11 @@ def proxy_class(remote_class, name=None):
|
|||||||
|
|
||||||
elif isinstance(aobj, Command):
|
elif isinstance(aobj, Command):
|
||||||
cobj = aobj.copy()
|
cobj = aobj.copy()
|
||||||
commands[aname] = cobj
|
|
||||||
|
|
||||||
def cfunc(self, arg=None, cname=aname):
|
def cfunc(self, arg=None, cname=aname):
|
||||||
return self._secnode.execCommand(self.name, cname, arg)
|
return self._secnode.execCommand(self.name, cname, arg)
|
||||||
|
|
||||||
attrs['do_' + aname] = cfunc
|
attrs[aname] = cobj(cfunc)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
||||||
|
@ -227,7 +227,7 @@ class Server:
|
|||||||
# all objs created, now start them up and interconnect
|
# all objs created, now start them up and interconnect
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
self.log.info('registering module %r' % modname)
|
self.log.info('registering module %r' % modname)
|
||||||
self.dispatcher.register_module(modobj, modname, modobj.properties['export'])
|
self.dispatcher.register_module(modobj, modname, modobj.export)
|
||||||
if modobj.pollerClass is not None:
|
if modobj.pollerClass is not None:
|
||||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||||
@ -236,10 +236,10 @@ class Server:
|
|||||||
|
|
||||||
# handle attached modules
|
# handle attached modules
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
for propname, propobj in modobj.__class__.properties.items():
|
for propname, propobj in modobj.propertyDict.items():
|
||||||
if isinstance(propobj, Attached):
|
if isinstance(propobj, Attached):
|
||||||
setattr(modobj, propobj.attrname or '_' + propname,
|
setattr(modobj, propobj.attrname or '_' + propname,
|
||||||
self.dispatcher.get_module(modobj.properties[propname]))
|
self.dispatcher.get_module(getattr(modobj, propname)))
|
||||||
# call init on each module after registering all
|
# call init on each module after registering all
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
modobj.initModule()
|
modobj.initModule()
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
"""Define Simulation classes"""
|
"""Define Simulation classes"""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: rework after syntax change!
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
@ -27,11 +27,10 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
import re
|
import re
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached
|
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Done
|
||||||
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
|
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
|
||||||
from secop.errors import CommunicationFailedError, CommunicationSilentError
|
from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError
|
||||||
from secop.poller import REGULAR
|
from secop.poller import REGULAR
|
||||||
from secop.metaclass import Done
|
|
||||||
|
|
||||||
|
|
||||||
class StringIO(Communicator):
|
class StringIO(Communicator):
|
||||||
@ -39,38 +38,22 @@ class StringIO(Communicator):
|
|||||||
|
|
||||||
self healing is assured by polling the parameter 'is_connected'
|
self healing is assured by polling the parameter 'is_connected'
|
||||||
"""
|
"""
|
||||||
properties = {
|
uri = Property('hostname:portnumber', datatype=StringType())
|
||||||
'uri':
|
end_of_line = Property('end_of_line character', datatype=ValueType(),
|
||||||
Property('hostname:portnumber', datatype=StringType()),
|
default='\n', settable=True)
|
||||||
'end_of_line':
|
encoding = Property('used encoding', datatype=StringType(),
|
||||||
Property('end_of_line character', datatype=ValueType(),
|
default='ascii', settable=True)
|
||||||
default='\n', settable=True),
|
identification = Property('''
|
||||||
'encoding':
|
|
||||||
Property('used encoding', datatype=StringType(),
|
|
||||||
default='ascii', settable=True),
|
|
||||||
'identification':
|
|
||||||
Property('''
|
|
||||||
identification
|
identification
|
||||||
|
|
||||||
a list of tuples with commands and expected responses as regexp,
|
a list of tuples with commands and expected responses as regexp,
|
||||||
to be sent on connect''',
|
to be sent on connect''',
|
||||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
||||||
}
|
|
||||||
parameters = {
|
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||||
'timeout':
|
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||||
Parameter('timeout', datatype=FloatRange(0), default=2),
|
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
||||||
'wait_before':
|
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||||
Parameter('wait time before sending', datatype=FloatRange(), default=0),
|
|
||||||
'is_connected':
|
|
||||||
Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR),
|
|
||||||
'pollinterval':
|
|
||||||
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'multicomm':
|
|
||||||
Command('execute multiple commands in one go',
|
|
||||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
|
||||||
}
|
|
||||||
|
|
||||||
_reconnectCallbacks = None
|
_reconnectCallbacks = None
|
||||||
|
|
||||||
@ -105,11 +88,12 @@ class StringIO(Communicator):
|
|||||||
self._conn = AsynConn(uri, self._eol_read)
|
self._conn = AsynConn(uri, self._eol_read)
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
for command, regexp in self.identification:
|
for command, regexp in self.identification:
|
||||||
reply = self.do_communicate(command)
|
reply = self.communicate(command)
|
||||||
if not re.match(regexp, reply):
|
if not re.match(regexp, reply):
|
||||||
self.closeConnection()
|
self.closeConnection()
|
||||||
raise CommunicationFailedError('bad response: %s does not match %s' %
|
raise CommunicationFailedError('bad response: %s does not match %s' %
|
||||||
(reply, regexp))
|
(reply, regexp))
|
||||||
|
|
||||||
def closeConnection(self):
|
def closeConnection(self):
|
||||||
"""close connection
|
"""close connection
|
||||||
|
|
||||||
@ -170,7 +154,7 @@ class StringIO(Communicator):
|
|||||||
if removeme:
|
if removeme:
|
||||||
self._reconnectCallbacks.pop(key)
|
self._reconnectCallbacks.pop(key)
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
"""send a command and receive a reply
|
"""send a command and receive a reply
|
||||||
|
|
||||||
using end_of_line, encoding and self._lock
|
using end_of_line, encoding and self._lock
|
||||||
@ -179,6 +163,8 @@ class StringIO(Communicator):
|
|||||||
"""
|
"""
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
self.read_is_connected() # try to reconnect
|
self.read_is_connected() # try to reconnect
|
||||||
|
if not self._conn:
|
||||||
|
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
# read garbage and wait before send
|
# read garbage and wait before send
|
||||||
@ -210,11 +196,13 @@ class StringIO(Communicator):
|
|||||||
self.log.error(self._last_error)
|
self.log.error(self._last_error)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def do_multicomm(self, commands):
|
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||||
|
def multicomm(self, commands):
|
||||||
|
"""communicate multiple request/replies in one row"""
|
||||||
replies = []
|
replies = []
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
replies.append(self.do_communicate(cmd))
|
replies.append(self.communicate(cmd))
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
|
|
||||||
@ -223,17 +211,15 @@ class HasIodev(Module):
|
|||||||
|
|
||||||
not only StringIO !
|
not only StringIO !
|
||||||
"""
|
"""
|
||||||
properties = {
|
iodev = Attached()
|
||||||
'iodev': Attached(),
|
uri = Property('uri for automatic creation of the attached communication module',
|
||||||
'uri': Property('uri for automatic creation of the attached communication module',
|
StringType(), default='')
|
||||||
StringType(), default=''),
|
|
||||||
}
|
|
||||||
|
|
||||||
iodevDict = {}
|
iodevDict = {}
|
||||||
|
|
||||||
def __init__(self, name, logger, opts, srv):
|
def __init__(self, name, logger, opts, srv):
|
||||||
iodev = opts.get('iodev')
|
iodev = opts.get('iodev')
|
||||||
super().__init__(name, logger, opts, srv)
|
Module.__init__(self, name, logger, opts, srv)
|
||||||
if self.uri:
|
if self.uri:
|
||||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||||
'export': False}
|
'export': False}
|
||||||
@ -243,7 +229,9 @@ class HasIodev(Module):
|
|||||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||||
srv.modules[ioname] = iodev
|
srv.modules[ioname] = iodev
|
||||||
self.iodevDict[self.uri] = ioname
|
self.iodevDict[self.uri] = ioname
|
||||||
self.setProperty('iodev', ioname)
|
self.iodev = ioname
|
||||||
|
elif not self.iodev:
|
||||||
|
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
try:
|
try:
|
||||||
@ -254,4 +242,4 @@ class HasIodev(Module):
|
|||||||
super().initModule()
|
super().initModule()
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def sendRecv(self, command):
|
||||||
return self._iodev.do_communicate(command)
|
return self._iodev.communicate(command)
|
||||||
|
@ -27,18 +27,16 @@ from math import atan
|
|||||||
|
|
||||||
from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType
|
from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType
|
||||||
from secop.lib import clamp, mkthread
|
from secop.lib import clamp, mkthread
|
||||||
from secop.modules import Drivable, Override, Parameter
|
from secop.modules import Drivable, Parameter, Command
|
||||||
|
|
||||||
# test custom property (value.test can be changed in config file)
|
# test custom property (value.test can be changed in config file)
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
|
|
||||||
Parameter.properties['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
|
Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
|
||||||
|
|
||||||
|
|
||||||
class CryoBase(Drivable):
|
class CryoBase(Drivable):
|
||||||
properties = {
|
is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True)
|
||||||
'is_cryo': Property('private Flag if this is a cryostat', BoolType(), default=True, export=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Cryostat(CryoBase):
|
class Cryostat(CryoBase):
|
||||||
@ -49,93 +47,88 @@ class Cryostat(CryoBase):
|
|||||||
- thermal transfer between regulation and samplen
|
- thermal transfer between regulation and samplen
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = dict(
|
jitter = Parameter("amount of random noise on readout values",
|
||||||
jitter=Parameter("amount of random noise on readout values",
|
|
||||||
datatype=FloatRange(0, 1), unit="K",
|
datatype=FloatRange(0, 1), unit="K",
|
||||||
default=0.1, readonly=False, export=False,
|
default=0.1, readonly=False, export=False,
|
||||||
),
|
),
|
||||||
T_start=Parameter("starting temperature for simulation",
|
T_start = Parameter("starting temperature for simulation",
|
||||||
datatype=FloatRange(0), default=10,
|
datatype=FloatRange(0), default=10,
|
||||||
export=False,
|
export=False,
|
||||||
),
|
),
|
||||||
looptime=Parameter("timestep for simulation",
|
looptime = Parameter("timestep for simulation",
|
||||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||||
readonly=False, export=False,
|
readonly=False, export=False,
|
||||||
),
|
),
|
||||||
ramp=Parameter("ramping speed of the setpoint",
|
ramp = Parameter("ramping speed of the setpoint",
|
||||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
),
|
||||||
setpoint=Parameter("current setpoint during ramping else target",
|
setpoint = Parameter("current setpoint during ramping else target",
|
||||||
datatype=FloatRange(), default=1, unit='K',
|
datatype=FloatRange(), default=1, unit='K',
|
||||||
),
|
),
|
||||||
maxpower=Parameter("Maximum heater power",
|
maxpower = Parameter("Maximum heater power",
|
||||||
datatype=FloatRange(0), default=1, unit="W",
|
datatype=FloatRange(0), default=1, unit="W",
|
||||||
readonly=False,
|
readonly=False,
|
||||||
group='heater_settings',
|
group='heater_settings',
|
||||||
),
|
),
|
||||||
heater=Parameter("current heater setting",
|
heater = Parameter("current heater setting",
|
||||||
datatype=FloatRange(0, 100), default=0, unit="%",
|
datatype=FloatRange(0, 100), default=0, unit="%",
|
||||||
group='heater_settings',
|
group='heater_settings',
|
||||||
),
|
),
|
||||||
heaterpower=Parameter("current heater power",
|
heaterpower = Parameter("current heater power",
|
||||||
datatype=FloatRange(0), default=0, unit="W",
|
datatype=FloatRange(0), default=0, unit="W",
|
||||||
group='heater_settings',
|
group='heater_settings',
|
||||||
),
|
),
|
||||||
target=Override("target temperature",
|
target = Parameter("target temperature",
|
||||||
datatype=FloatRange(0), default=0, unit="K",
|
datatype=FloatRange(0), default=0, unit="K",
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
),
|
||||||
value=Override("regulation temperature",
|
value = Parameter("regulation temperature",
|
||||||
datatype=FloatRange(0), default=0, unit="K",
|
datatype=FloatRange(0), default=0, unit="K",
|
||||||
test='TEST',
|
test='TEST',
|
||||||
),
|
),
|
||||||
pid=Parameter("regulation coefficients",
|
pid = Parameter("regulation coefficients",
|
||||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||||
FloatRange(0, 100)),
|
FloatRange(0, 100)),
|
||||||
default=(40, 10, 2), readonly=False,
|
default=(40, 10, 2), readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
p=Parameter("regulation coefficient 'p'",
|
# pylint: disable=invalid-name
|
||||||
|
p = Parameter("regulation coefficient 'p'",
|
||||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
i=Parameter("regulation coefficient 'i'",
|
i = Parameter("regulation coefficient 'i'",
|
||||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
d=Parameter("regulation coefficient 'd'",
|
d = Parameter("regulation coefficient 'd'",
|
||||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
mode=Parameter("mode of regulation",
|
mode = Parameter("mode of regulation",
|
||||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||||
default='ramp',
|
default='ramp',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
),
|
||||||
pollinterval=Override("polling interval",
|
pollinterval = Parameter("polling interval",
|
||||||
datatype=FloatRange(0), default=5,
|
datatype=FloatRange(0), default=5,
|
||||||
),
|
),
|
||||||
tolerance=Parameter("temperature range for stability checking",
|
tolerance = Parameter("temperature range for stability checking",
|
||||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
group='stability',
|
group='stability',
|
||||||
),
|
),
|
||||||
window=Parameter("time window for stability checking",
|
window = Parameter("time window for stability checking",
|
||||||
datatype=FloatRange(1, 900), default=30, unit='s',
|
datatype=FloatRange(1, 900), default=30, unit='s',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
group='stability',
|
group='stability',
|
||||||
),
|
),
|
||||||
timeout=Parameter("max waiting time for stabilisation check",
|
timeout = Parameter("max waiting time for stabilisation check",
|
||||||
datatype=FloatRange(1, 36000), default=900, unit='s',
|
datatype=FloatRange(1, 36000), default=900, unit='s',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
group='stability',
|
group='stability',
|
||||||
),
|
),
|
||||||
)
|
|
||||||
commands = dict(
|
|
||||||
stop=Override(
|
|
||||||
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._stopflag = False
|
self._stopflag = False
|
||||||
@ -180,8 +173,11 @@ class Cryostat(CryoBase):
|
|||||||
def read_pid(self):
|
def read_pid(self):
|
||||||
return (self.p, self.i, self.d)
|
return (self.p, self.i, self.d)
|
||||||
|
|
||||||
def do_stop(self):
|
@Command()
|
||||||
# stop the ramp by setting current setpoint as target
|
def stop(self):
|
||||||
|
"""Stop ramping the setpoint
|
||||||
|
|
||||||
|
by setting the current setpoint as new target"""
|
||||||
# XXX: discussion: take setpoint or current value ???
|
# XXX: discussion: take setpoint or current value ???
|
||||||
self.write_target(self.setpoint)
|
self.write_target(self.setpoint)
|
||||||
|
|
||||||
|
@ -28,42 +28,39 @@ import time
|
|||||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.modules import Drivable, Override, Parameter as SECoP_Parameter, Readable
|
from secop.modules import Drivable, Parameter as SECoP_Parameter, Readable
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
|
|
||||||
|
|
||||||
class Parameter(SECoP_Parameter):
|
class Parameter(SECoP_Parameter):
|
||||||
properties = {
|
test = Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test')
|
||||||
'test' : Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test'),
|
|
||||||
}
|
|
||||||
|
|
||||||
PERSIST = 101
|
PERSIST = 101
|
||||||
|
|
||||||
|
|
||||||
class Switch(Drivable):
|
class Switch(Drivable):
|
||||||
"""switch it on or off....
|
"""switch it on or off....
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('current state (on or off)',
|
value = Parameter('current state (on or off)',
|
||||||
datatype=EnumType(on=1, off=0), default=0,
|
datatype=EnumType(on=1, off=0), default=0,
|
||||||
),
|
)
|
||||||
'target': Override('wanted state (on or off)',
|
target = Parameter('wanted state (on or off)',
|
||||||
datatype=EnumType(on=1, off=0), default=0,
|
datatype=EnumType(on=1, off=0), default=0,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
'switch_on_time': Parameter('seconds to wait after activating the switch',
|
switch_on_time = Parameter('seconds to wait after activating the switch',
|
||||||
datatype=FloatRange(0, 60), unit='s',
|
datatype=FloatRange(0, 60), unit='s',
|
||||||
default=10, export=False,
|
default=10, export=False,
|
||||||
),
|
)
|
||||||
'switch_off_time': Parameter('cool-down time in seconds',
|
switch_off_time = Parameter('cool-down time in seconds',
|
||||||
datatype=FloatRange(0, 60), unit='s',
|
datatype=FloatRange(0, 60), unit='s',
|
||||||
default=10, export=False,
|
default=10, export=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
properties = {
|
description = Property('The description of the Module', StringType(),
|
||||||
'description' : Property('The description of the Module', StringType(),
|
default='no description', mandatory=False, extname='description')
|
||||||
default='no description', mandatory=False, extname='description'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
# could ask HW
|
# could ask HW
|
||||||
@ -109,30 +106,29 @@ class Switch(Drivable):
|
|||||||
class MagneticField(Drivable):
|
class MagneticField(Drivable):
|
||||||
"""a liquid magnet
|
"""a liquid magnet
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('current field in T',
|
value = Parameter('current field in T',
|
||||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||||
),
|
)
|
||||||
'target': Override('target field in T',
|
target = Parameter('target field in T',
|
||||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
'ramp': Parameter('ramping speed',
|
ramp = Parameter('ramping speed',
|
||||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
'mode': Parameter('what to do after changing field',
|
mode = Parameter('what to do after changing field',
|
||||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
'heatswitch': Parameter('name of heat switch device',
|
heatswitch = Parameter('name of heat switch device',
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
||||||
overrides = {
|
|
||||||
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
|
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||||
@ -202,21 +198,20 @@ class MagneticField(Drivable):
|
|||||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||||
self.log.error(self, 'main thread exited unexpectedly!')
|
self.log.error(self, 'main thread exited unexpectedly!')
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
self.write_target(self.read_value())
|
self.write_target(self.read_value())
|
||||||
|
|
||||||
|
|
||||||
class CoilTemp(Readable):
|
class CoilTemp(Readable):
|
||||||
"""a coil temperature
|
"""a coil temperature
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('Coil temperatur',
|
value = Parameter('Coil temperatur',
|
||||||
unit='K', datatype=FloatRange(), default=0,
|
unit='K', datatype=FloatRange(), default=0,
|
||||||
),
|
)
|
||||||
'sensor': Parameter("Sensor number or calibration id",
|
sensor = Parameter("Sensor number or calibration id",
|
||||||
datatype=StringType(), readonly=True,
|
datatype=StringType(), readonly=True,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(2.3 + random.random(), 3)
|
return round(2.3 + random.random(), 3)
|
||||||
@ -225,18 +220,17 @@ class CoilTemp(Readable):
|
|||||||
class SampleTemp(Drivable):
|
class SampleTemp(Drivable):
|
||||||
"""a sample temperature
|
"""a sample temperature
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('Sample temperature',
|
value = Parameter('Sample temperature',
|
||||||
unit='K', datatype=FloatRange(), default=10,
|
unit='K', datatype=FloatRange(), default=10,
|
||||||
),
|
)
|
||||||
'sensor': Parameter("Sensor number or calibration id",
|
sensor = Parameter("Sensor number or calibration id",
|
||||||
datatype=StringType(), readonly=True,
|
datatype=StringType(), readonly=True,
|
||||||
),
|
)
|
||||||
'ramp': Parameter('moving speed in K/min',
|
ramp = Parameter('moving speed in K/min',
|
||||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
_thread = threading.Thread(target=self._thread)
|
_thread = threading.Thread(target=self._thread)
|
||||||
@ -272,20 +266,19 @@ class Label(Readable):
|
|||||||
of several subdevices. used for demoing connections between
|
of several subdevices. used for demoing connections between
|
||||||
modules.
|
modules.
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'system': Parameter("Name of the magnet system",
|
system = Parameter("Name of the magnet system",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'subdev_mf': Parameter("name of subdevice for magnet status",
|
subdev_mf = Parameter("name of subdevice for magnet status",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'subdev_ts': Parameter("name of subdevice for sample temp",
|
subdev_ts = Parameter("name of subdevice for sample temp",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'value': Override("final value of label string", default='',
|
value = Parameter("final value of label string", default='',
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
strings = [self.system]
|
strings = [self.system]
|
||||||
@ -317,29 +310,25 @@ class Label(Readable):
|
|||||||
class DatatypesTest(Readable):
|
class DatatypesTest(Readable):
|
||||||
"""for demoing all datatypes
|
"""for demoing all datatypes
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||||
readonly=False, default=1),
|
readonly=False, default=1)
|
||||||
'tupleof': Parameter('tuple of int, float and str',
|
tupleof = Parameter('tuple of int, float and str',
|
||||||
datatype=TupleOf(IntRange(), FloatRange(),
|
datatype=TupleOf(IntRange(), FloatRange(),
|
||||||
StringType()),
|
StringType()),
|
||||||
readonly=False, default=(1, 2.3, 'a')),
|
readonly=False, default=(1, 2.3, 'a'))
|
||||||
'arrayof': Parameter('array: 2..3 times bool',
|
arrayof = Parameter('array: 2..3 times bool',
|
||||||
datatype=ArrayOf(BoolType(), 2, 3),
|
datatype=ArrayOf(BoolType(), 2, 3),
|
||||||
readonly=False, default=[1, 0, 1]),
|
readonly=False, default=[1, 0, 1])
|
||||||
'intrange': Parameter('intrange', datatype=IntRange(2, 9),
|
intrange = Parameter('intrange', datatype=IntRange(2, 9),
|
||||||
readonly=False, default=4),
|
readonly=False, default=4)
|
||||||
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1),
|
floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||||
readonly=False, default=0, ),
|
readonly=False, default=0)
|
||||||
'struct': Parameter('struct(a=str, b=int, c=bool)',
|
struct = Parameter('struct(a=str, b=int, c=bool)',
|
||||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||||
c=BoolType()),
|
c=BoolType()))
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayTest(Readable):
|
class ArrayTest(Readable):
|
||||||
parameters = {
|
x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
default=100000 * [0])
|
||||||
default = 100000 * [0]),
|
|
||||||
}
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from secop.datatypes import FloatRange, StringType
|
from secop.datatypes import FloatRange, StringType
|
||||||
from secop.modules import Communicator, Drivable, Parameter, Readable, Override
|
from secop.modules import Communicator, Drivable, Parameter, Readable
|
||||||
from secop.params import Command
|
from secop.params import Command
|
||||||
|
|
||||||
|
|
||||||
@ -45,11 +45,10 @@ class Heater(Drivable):
|
|||||||
class name indicates it to be some heating element,
|
class name indicates it to be some heating element,
|
||||||
but the implementation may do anything
|
but the implementation may do anything
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'maxheaterpower': Parameter('maximum allowed heater power',
|
maxheaterpower = Parameter('maximum allowed heater power',
|
||||||
datatype=FloatRange(0, 100), unit='W',
|
datatype=FloatRange(0, 100), unit='W',
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(100 * random.random(), 1)
|
return round(100 * random.random(), 1)
|
||||||
@ -64,22 +63,21 @@ class Temp(Drivable):
|
|||||||
class name indicates it to be some temperature controller,
|
class name indicates it to be some temperature controller,
|
||||||
but the implementation may do anything
|
but the implementation may do anything
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'sensor': Parameter(
|
sensor = Parameter(
|
||||||
"Sensor number or calibration id",
|
"Sensor number or calibration id",
|
||||||
datatype=StringType(
|
datatype=StringType(
|
||||||
8,
|
8,
|
||||||
16),
|
16),
|
||||||
readonly=True,
|
readonly=True,
|
||||||
),
|
)
|
||||||
'target': Override(
|
target = Parameter(
|
||||||
"Target temperature",
|
"Target temperature",
|
||||||
default=300.0,
|
default=300.0,
|
||||||
datatype=FloatRange(0),
|
datatype=FloatRange(0),
|
||||||
readonly=False,
|
readonly=False,
|
||||||
unit='K',
|
unit='K',
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(100 * random.random(), 1)
|
return round(100 * random.random(), 1)
|
||||||
@ -90,8 +88,8 @@ class Temp(Drivable):
|
|||||||
|
|
||||||
class Lower(Communicator):
|
class Lower(Communicator):
|
||||||
"""Communicator returning a lowercase version of the request"""
|
"""Communicator returning a lowercase version of the request"""
|
||||||
command = {
|
|
||||||
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'),
|
@Command(argument=StringType(), result=StringType(), export='communicate')
|
||||||
}
|
def communicate(self, command):
|
||||||
def do_communicate(self, request):
|
"""lowercase a string"""
|
||||||
return str(request).lower()
|
return str(command).lower()
|
||||||
|
@ -58,20 +58,20 @@ except ImportError:
|
|||||||
class EpicsReadable(Readable):
|
class EpicsReadable(Readable):
|
||||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||||
# Commmon parameter for all EPICS devices
|
# Commmon parameter for all EPICS devices
|
||||||
parameters = {
|
|
||||||
'value': Parameter('EPICS generic value',
|
# parameters
|
||||||
|
value = Parameter('EPICS generic value',
|
||||||
datatype=FloatRange(),
|
datatype=FloatRange(),
|
||||||
default=300.0,),
|
default=300.0,)
|
||||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||||
datatype=EnumType(v3=3, v4=4),),
|
datatype=EnumType(v3=3, v4=4),)
|
||||||
# 'private' parameters: not remotely accessible
|
value_pv = Parameter('EPICS pv_name of value',
|
||||||
'value_pv': Parameter('EPICS pv_name of value',
|
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'status_pv': Parameter('EPICS pv_name of status',
|
status_pv = Parameter('EPICS pv_name of status',
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
}
|
|
||||||
|
|
||||||
# Generic read and write functions
|
# Generic read and write functions
|
||||||
def _read_pv(self, pv_name):
|
def _read_pv(self, pv_name):
|
||||||
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
|
|||||||
class EpicsDrivable(Drivable):
|
class EpicsDrivable(Drivable):
|
||||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||||
# Commmon parameter for all EPICS devices
|
# Commmon parameter for all EPICS devices
|
||||||
parameters = {
|
|
||||||
'target': Parameter('EPICS generic target', datatype=FloatRange(),
|
# parameters
|
||||||
default=300.0, readonly=False),
|
target = Parameter('EPICS generic target', datatype=FloatRange(),
|
||||||
'value': Parameter('EPICS generic value', datatype=FloatRange(),
|
default=300.0, readonly=False)
|
||||||
default=300.0,),
|
value = Parameter('EPICS generic value', datatype=FloatRange(),
|
||||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
default=300.0,)
|
||||||
datatype=StringType(),),
|
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||||
# 'private' parameters: not remotely accessible
|
datatype=StringType(),)
|
||||||
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(),
|
target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(),
|
value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(),
|
status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
}
|
|
||||||
|
|
||||||
# Generic read and write functions
|
# Generic read and write functions
|
||||||
def _read_pv(self, pv_name):
|
def _read_pv(self, pv_name):
|
||||||
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
|
|||||||
|
|
||||||
class EpicsTempCtrl(EpicsDrivable):
|
class EpicsTempCtrl(EpicsDrivable):
|
||||||
|
|
||||||
parameters = {
|
|
||||||
# TODO: restrict possible values with oneof datatype
|
# parameters
|
||||||
'heaterrange': Parameter('Heater range', datatype=StringType(),
|
heaterrange = Parameter('Heater range', datatype=StringType(),
|
||||||
default='Off', readonly=False,),
|
default='Off', readonly=False,)
|
||||||
'tolerance': Parameter('allowed deviation between value and target',
|
tolerance = Parameter('allowed deviation between value and target',
|
||||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||||
readonly=False,),
|
readonly=False,)
|
||||||
# 'private' parameters: not remotely accessible
|
heaterrange_pv = Parameter('EPICS pv_name of heater range',
|
||||||
'heaterrange_pv': Parameter('EPICS pv_name of heater range',
|
datatype=StringType(), default="unset", export=False,)
|
||||||
datatype=StringType(), default="unset", export=False,),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self._read_pv(self.target_pv)
|
return self._read_pv(self.target_pv)
|
||||||
|
@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
parameters = {
|
|
||||||
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False),
|
# parameters
|
||||||
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False),
|
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||||
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False),
|
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||||
'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False),
|
subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
|
||||||
'userlimits': Parameter('User defined limits of device value',
|
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||||
|
userlimits = Parameter('User defined limits of device value',
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10),
|
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||||
'abslimits': Parameter('Absolute limits of device value',
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
default=(-0.5, 0.5), poll=True,
|
default=(-0.5, 0.5), poll=True,
|
||||||
),
|
)
|
||||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
precision = Parameter('Precision of the device value (allowed deviation '
|
||||||
'of stable values from target)',
|
'of stable values from target)',
|
||||||
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
||||||
),
|
)
|
||||||
'ramp': Parameter('Target rate of field change per minute', readonly=False,
|
ramp = Parameter('Target rate of field change per minute', readonly=False,
|
||||||
datatype=FloatRange(unit='$/min'), default=1.0),
|
datatype=FloatRange(unit='$/min'), default=1.0)
|
||||||
'calibration': Parameter('Coefficients for calibration '
|
calibration = Parameter('Coefficients for calibration '
|
||||||
'function: [c0, c1, c2, c3, c4] calculates '
|
'function: [c0, c1, c2, c3, c4] calculates '
|
||||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||||
' in T', poll=1,
|
' in T', poll=1,
|
||||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||||
default=(1.0, 0.0, 0.0, 0.0, 0.0)),
|
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||||
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting',
|
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||||
short=ArrayOf(
|
short=ArrayOf(
|
||||||
FloatRange(), 5, 5),
|
FloatRange(), 5, 5),
|
||||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False),
|
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
|
||||||
}
|
|
||||||
|
|
||||||
def _current2field(self, current, *coefficients):
|
def _current2field(self, current, *coefficients):
|
||||||
"""Return field in T for given current in A.
|
"""Return field in T for given current in A.
|
||||||
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
return self._currentsource.read_status()[0] == 'BUSY'
|
return self._currentsource.read_status()[0] == 'BUSY'
|
||||||
if self._currentsource.status[0] != 'BUSY':
|
if self._currentsource.status[0] != 'BUSY':
|
||||||
if self._enable.status[0] == 'ERROR':
|
if self._enable.status[0] == 'ERROR':
|
||||||
self._enable.do_reset()
|
self._enable.reset()
|
||||||
self._enable.read_status()
|
self._enable.read_status()
|
||||||
self._enable.write_target('On')
|
self._enable.write_target('On')
|
||||||
self._enable._hw_wait()
|
self._enable._hw_wait()
|
||||||
|
@ -41,7 +41,7 @@ from secop.errors import CommunicationFailedError, \
|
|||||||
ConfigError, HardwareError, ProgrammingError
|
ConfigError, HardwareError, ProgrammingError
|
||||||
from secop.lib import lazy_property
|
from secop.lib import lazy_property
|
||||||
from secop.modules import Command, Drivable, \
|
from secop.modules import Command, Drivable, \
|
||||||
Module, Override, Parameter, Readable, BasicPoller
|
Module, Parameter, Readable, BasicPoller
|
||||||
|
|
||||||
#####
|
#####
|
||||||
|
|
||||||
@ -160,24 +160,18 @@ class PyTangoDevice(Module):
|
|||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'comtries': Parameter('Maximum retries for communication',
|
comtries = Parameter('Maximum retries for communication',
|
||||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0),
|
comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
|
||||||
unit='s', default=0.1, readonly=False,
|
unit='s', default=0.1, readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
|
tangodevice = Parameter('Tango device name',
|
||||||
'tangodevice': Parameter('Tango device name',
|
|
||||||
datatype=StringType(), readonly=True,
|
datatype=StringType(), readonly=True,
|
||||||
# export=True, # for testing only
|
# export=True, # for testing only
|
||||||
export=False,
|
export=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'reset': Command('Tango reset command', argument=None, result=None),
|
|
||||||
}
|
|
||||||
|
|
||||||
tango_status_mapping = {
|
tango_status_mapping = {
|
||||||
PyTango.DevState.ON: Drivable.Status.IDLE,
|
PyTango.DevState.ON: Drivable.Status.IDLE,
|
||||||
@ -372,7 +366,9 @@ class PyTangoDevice(Module):
|
|||||||
|
|
||||||
return (myState, tangoStatus)
|
return (myState, tangoStatus)
|
||||||
|
|
||||||
def do_reset(self):
|
@Command(argument=None, result=None)
|
||||||
|
def reset(self):
|
||||||
|
"""Tango reset command"""
|
||||||
self._dev.Reset()
|
self._dev.Reset()
|
||||||
|
|
||||||
|
|
||||||
@ -405,13 +401,9 @@ class Sensor(AnalogInput):
|
|||||||
# note: we don't transport the formula to secop....
|
# note: we don't transport the formula to secop....
|
||||||
# we support the adjust method
|
# we support the adjust method
|
||||||
|
|
||||||
commands = {
|
@Command(argument=FloatRange(), result=None)
|
||||||
'setposition': Command('Set the position to the given value.',
|
def setposition(self, value):
|
||||||
argument=FloatRange(), result=None,
|
"""Set the position to the given value."""
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_setposition(self, value):
|
|
||||||
self._dev.Adjust(value)
|
self._dev.Adjust(value)
|
||||||
|
|
||||||
|
|
||||||
@ -427,29 +419,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
controllers, ...
|
controllers, ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'userlimits': Parameter('User defined limits of device value',
|
userlimits = Parameter('User defined limits of device value',
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
default=(float('-Inf'), float('+Inf')),
|
default=(float('-Inf'), float('+Inf')),
|
||||||
readonly=False, poll=10,
|
readonly=False, poll=10,
|
||||||
),
|
)
|
||||||
'abslimits': Parameter('Absolute limits of device value',
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
),
|
)
|
||||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
precision = Parameter('Precision of the device value (allowed deviation '
|
||||||
'of stable values from target)',
|
'of stable values from target)',
|
||||||
datatype=FloatRange(1e-38, unit='$'),
|
datatype=FloatRange(1e-38, unit='$'),
|
||||||
readonly=False, group='stability',
|
readonly=False, group='stability',
|
||||||
),
|
)
|
||||||
'window': Parameter('Time window for checking stabilization if > 0',
|
window = Parameter('Time window for checking stabilization if > 0',
|
||||||
default=60.0, readonly=False,
|
default=60.0, readonly=False,
|
||||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||||
),
|
)
|
||||||
'timeout': Parameter('Timeout for waiting for a stable value (if > 0)',
|
timeout = Parameter('Timeout for waiting for a stable value (if > 0)',
|
||||||
default=60.0, readonly=False,
|
default=60.0, readonly=False,
|
||||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||||
),
|
)
|
||||||
}
|
|
||||||
_history = ()
|
_history = ()
|
||||||
_timeout = None
|
_timeout = None
|
||||||
_moving = False
|
_moving = False
|
||||||
@ -566,7 +558,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
if self.status[0] == self.Status.BUSY:
|
if self.status[0] == self.Status.BUSY:
|
||||||
# changing target value during movement is not allowed by the
|
# changing target value during movement is not allowed by the
|
||||||
# Tango base class state machine. If we are moving, stop first.
|
# Tango base class state machine. If we are moving, stop first.
|
||||||
self.do_stop()
|
self.stop()
|
||||||
self._hw_wait()
|
self._hw_wait()
|
||||||
self._dev.value = value
|
self._dev.value = value
|
||||||
# set meaningful timeout
|
# set meaningful timeout
|
||||||
@ -587,7 +579,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||||
sleep(0.3)
|
sleep(0.3)
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
self._dev.Stop()
|
self._dev.Stop()
|
||||||
|
|
||||||
|
|
||||||
@ -601,21 +593,14 @@ class Actuator(AnalogOutput):
|
|||||||
"""
|
"""
|
||||||
# for secop: support the speed and ramp parameters
|
# for secop: support the speed and ramp parameters
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'speed': Parameter('The speed of changing the value',
|
speed = Parameter('The speed of changing the value',
|
||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||||
),
|
)
|
||||||
'ramp': Parameter('The speed of changing the value',
|
ramp = Parameter('The speed of changing the value',
|
||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||||
poll=30,
|
poll=30,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'setposition': Command('Set the position to the given value.',
|
|
||||||
argument=FloatRange(), result=None,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_speed(self):
|
def read_speed(self):
|
||||||
return self._dev.speed
|
return self._dev.speed
|
||||||
@ -630,7 +615,9 @@ class Actuator(AnalogOutput):
|
|||||||
self.write_speed(value / 60.)
|
self.write_speed(value / 60.)
|
||||||
return self.read_speed() * 60
|
return self.read_speed() * 60
|
||||||
|
|
||||||
def do_setposition(self, value=FloatRange()):
|
@Command(FloatRange(), result=None)
|
||||||
|
def setposition(self, value=FloatRange()):
|
||||||
|
"""Set the position to the given value."""
|
||||||
self._dev.Adjust(value)
|
self._dev.Adjust(value)
|
||||||
|
|
||||||
|
|
||||||
@ -641,21 +628,16 @@ class Motor(Actuator):
|
|||||||
It has the ability to move a real object from one place to another place.
|
It has the ability to move a real object from one place to another place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'refpos': Parameter('Reference position',
|
refpos = Parameter('Reference position',
|
||||||
datatype=FloatRange(unit='$'),
|
datatype=FloatRange(unit='$'),
|
||||||
),
|
)
|
||||||
'accel': Parameter('Acceleration',
|
accel = Parameter('Acceleration',
|
||||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||||
),
|
)
|
||||||
'decel': Parameter('Deceleration',
|
decel = Parameter('Deceleration',
|
||||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'reference': Command('Do a reference run', argument=None, result=None),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_refpos(self):
|
def read_refpos(self):
|
||||||
return float(self._getProperty('refpos'))
|
return float(self._getProperty('refpos'))
|
||||||
@ -672,7 +654,9 @@ class Motor(Actuator):
|
|||||||
def write_decel(self, value):
|
def write_decel(self, value):
|
||||||
self._dev.decel = value
|
self._dev.decel = value
|
||||||
|
|
||||||
def do_reference(self):
|
@Command()
|
||||||
|
def reference(self):
|
||||||
|
"""Do a reference run"""
|
||||||
self._dev.Reference()
|
self._dev.Reference()
|
||||||
return self.read_value()
|
return self.read_value()
|
||||||
|
|
||||||
@ -681,32 +665,29 @@ class TemperatureController(Actuator):
|
|||||||
"""A temperature control loop device.
|
"""A temperature control loop device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'p': Parameter('Proportional control Parameter', datatype=FloatRange(),
|
# pylint: disable=invalid-name
|
||||||
|
p = Parameter('Proportional control Parameter', datatype=FloatRange(),
|
||||||
readonly=False, group='pid',
|
readonly=False, group='pid',
|
||||||
),
|
)
|
||||||
'i': Parameter('Integral control Parameter', datatype=FloatRange(),
|
i = Parameter('Integral control Parameter', datatype=FloatRange(),
|
||||||
readonly=False, group='pid',
|
readonly=False, group='pid',
|
||||||
),
|
)
|
||||||
'd': Parameter('Derivative control Parameter', datatype=FloatRange(),
|
d = Parameter('Derivative control Parameter', datatype=FloatRange(),
|
||||||
readonly=False, group='pid',
|
readonly=False, group='pid',
|
||||||
),
|
)
|
||||||
'pid': Parameter('pid control Parameters',
|
pid = Parameter('pid control Parameters',
|
||||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||||
readonly=False, group='pid', poll=30,
|
readonly=False, group='pid', poll=30,
|
||||||
),
|
)
|
||||||
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||||
),
|
)
|
||||||
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1,
|
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
# We want this to be freely user-settable, and not produce a warning
|
precision = Parameter(default=0.1)
|
||||||
# on startup, so select a usually sensible default.
|
ramp = Parameter(description='Temperature ramp')
|
||||||
'precision': Override(default=0.1),
|
|
||||||
'ramp': Override(description='Temperature ramp'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
@ -755,15 +736,14 @@ class PowerSupply(Actuator):
|
|||||||
"""A power supply (voltage and current) device.
|
"""A power supply (voltage and current) device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'voltage': Parameter('Actual voltage',
|
voltage = Parameter('Actual voltage',
|
||||||
datatype=FloatRange(unit='V'), poll=-5),
|
datatype=FloatRange(unit='V'), poll=-5)
|
||||||
'current': Parameter('Actual current',
|
current = Parameter('Actual current',
|
||||||
datatype=FloatRange(unit='A'), poll=-5),
|
datatype=FloatRange(unit='A'), poll=-5)
|
||||||
}
|
|
||||||
overrides = {
|
# overrides
|
||||||
'ramp': Override(description='Current/voltage ramp'),
|
ramp = Parameter(description='Current/voltage ramp')
|
||||||
}
|
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
@ -782,9 +762,8 @@ class DigitalInput(PyTangoDevice, Readable):
|
|||||||
"""A device reading a bitfield.
|
"""A device reading a bitfield.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
'value': Override(datatype=IntRange()),
|
value = Parameter(datatype=IntRange())
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value
|
return self._dev.value
|
||||||
@ -794,10 +773,9 @@ class NamedDigitalInput(DigitalInput):
|
|||||||
"""A DigitalInput with numeric values mapped to names.
|
"""A DigitalInput with numeric values mapped to names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
mapping = Parameter('A dictionary mapping state names to integers',
|
||||||
datatype=StringType(), export=False), # XXX:!!!
|
datatype=StringType(), export=False) # XXX:!!!
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalInput, self).initModule()
|
super(NamedDigitalInput, self).initModule()
|
||||||
@ -821,12 +799,11 @@ class PartialDigitalInput(NamedDigitalInput):
|
|||||||
bit width accessed.
|
bit width accessed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'startbit': Parameter('Number of the first bit',
|
startbit = Parameter('Number of the first bit',
|
||||||
datatype=IntRange(0), default=0),
|
datatype=IntRange(0), default=0)
|
||||||
'bitwidth': Parameter('Number of bits',
|
bitwidth = Parameter('Number of bits',
|
||||||
datatype=IntRange(0), default=1),
|
datatype=IntRange(0), default=1)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalInput, self).initModule()
|
super(PartialDigitalInput, self).initModule()
|
||||||
@ -844,10 +821,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
|||||||
bitfield.
|
bitfield.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
'value': Override(datatype=IntRange()),
|
value = Parameter(datatype=IntRange())
|
||||||
'target': Override(datatype=IntRange()),
|
target = Parameter(datatype=IntRange())
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value # mapping is done by datatype upon export()
|
return self._dev.value # mapping is done by datatype upon export()
|
||||||
@ -865,10 +841,9 @@ class NamedDigitalOutput(DigitalOutput):
|
|||||||
"""A DigitalOutput with numeric values mapped to names.
|
"""A DigitalOutput with numeric values mapped to names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
mapping = Parameter('A dictionary mapping state names to integers',
|
||||||
datatype=StringType(), export=False),
|
datatype=StringType(), export=False)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalOutput, self).initModule()
|
super(NamedDigitalOutput, self).initModule()
|
||||||
@ -894,12 +869,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
|||||||
bit width accessed.
|
bit width accessed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'startbit': Parameter('Number of the first bit',
|
startbit = Parameter('Number of the first bit',
|
||||||
datatype=IntRange(0), default=0),
|
datatype=IntRange(0), default=0)
|
||||||
'bitwidth': Parameter('Number of bits',
|
bitwidth = Parameter('Number of bits',
|
||||||
datatype=IntRange(0), default=1),
|
datatype=IntRange(0), default=1)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalOutput, self).initModule()
|
super(PartialDigitalOutput, self).initModule()
|
||||||
@ -925,17 +899,16 @@ class StringIO(PyTangoDevice, Module):
|
|||||||
receives strings.
|
receives strings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'bustimeout': Parameter('Communication timeout',
|
bustimeout = Parameter('Communication timeout',
|
||||||
datatype=FloatRange(unit='s'), readonly=False,
|
datatype=FloatRange(unit='s'), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'endofline': Parameter('End of line',
|
endofline = Parameter('End of line',
|
||||||
datatype=StringType(), readonly=False,
|
datatype=StringType(), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'startofline': Parameter('Start of line',
|
startofline = Parameter('Start of line',
|
||||||
datatype=StringType(), readonly=False,
|
datatype=StringType(), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
}
|
|
||||||
|
|
||||||
def read_bustimeout(self):
|
def read_bustimeout(self):
|
||||||
return self._dev.communicationTimeout
|
return self._dev.communicationTimeout
|
||||||
@ -955,53 +928,48 @@ class StringIO(PyTangoDevice, Module):
|
|||||||
def write_startofline(self, value):
|
def write_startofline(self, value):
|
||||||
self._dev.startOfLine = value
|
self._dev.startOfLine = value
|
||||||
|
|
||||||
commands = {
|
@Command(argument=StringType(), result=StringType())
|
||||||
'communicate': Command('Send a string and return the reply',
|
def communicate(self, value=StringType()):
|
||||||
argument=StringType(),
|
"""Send a string and return the reply"""
|
||||||
result=StringType()),
|
|
||||||
'flush': Command('Flush output buffer',
|
|
||||||
argument=None, result=None),
|
|
||||||
'read': Command('read some characters from input buffer',
|
|
||||||
argument=IntRange(0), result=StringType()),
|
|
||||||
'write': Command('write some chars to output',
|
|
||||||
argument=StringType(), result=None),
|
|
||||||
'readLine': Command('Read sol - a whole line - eol',
|
|
||||||
argument=None, result=StringType()),
|
|
||||||
'writeLine': Command('write sol + a whole line + eol',
|
|
||||||
argument=StringType(), result=None),
|
|
||||||
'availableChars': Command('return number of chars in input buffer',
|
|
||||||
argument=None, result=IntRange(0)),
|
|
||||||
'availableLines': Command('return number of lines in input buffer',
|
|
||||||
argument=None, result=IntRange(0)),
|
|
||||||
'multiCommunicate': Command('perform a sequence of communications',
|
|
||||||
argument=ArrayOf(
|
|
||||||
TupleOf(StringType(), IntRange()), 100),
|
|
||||||
result=ArrayOf(StringType(), 100)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_communicate(self, value=StringType()):
|
|
||||||
return self._dev.Communicate(value)
|
return self._dev.Communicate(value)
|
||||||
|
|
||||||
def do_flush(self):
|
@Command(argument=None, result=None)
|
||||||
|
def flush(self):
|
||||||
|
"""Flush output buffer"""
|
||||||
self._dev.Flush()
|
self._dev.Flush()
|
||||||
|
|
||||||
def do_read(self, value):
|
@Command(argument=IntRange(0), result=StringType())
|
||||||
|
def read(self, value):
|
||||||
|
"""read some characters from input buffer"""
|
||||||
return self._dev.Read(value)
|
return self._dev.Read(value)
|
||||||
|
|
||||||
def do_write(self, value):
|
@Command(argument=StringType(), result=None)
|
||||||
|
def write(self, value):
|
||||||
|
"""write some chars to output"""
|
||||||
return self._dev.Write(value)
|
return self._dev.Write(value)
|
||||||
|
|
||||||
def do_readLine(self):
|
@Command(argument=None, result=StringType())
|
||||||
|
def readLine(self):
|
||||||
|
"""Read sol - a whole line - eol"""
|
||||||
return self._dev.ReadLine()
|
return self._dev.ReadLine()
|
||||||
|
|
||||||
def do_writeLine(self, value):
|
@Command(argument=StringType(), result=None)
|
||||||
|
def writeLine(self, value):
|
||||||
|
"""write sol + a whole line + eol"""
|
||||||
return self._dev.WriteLine(value)
|
return self._dev.WriteLine(value)
|
||||||
|
|
||||||
def do_multiCommunicate(self, value):
|
@Command(argument=ArrayOf(TupleOf(StringType(), IntRange()), 100),
|
||||||
|
result=ArrayOf(StringType(), 100))
|
||||||
|
def multiCommunicate(self, value):
|
||||||
|
"""perform a sequence of communications"""
|
||||||
return self._dev.MultiCommunicate(value)
|
return self._dev.MultiCommunicate(value)
|
||||||
|
|
||||||
def do_availableChars(self):
|
@Command(argument=None, result=IntRange(0))
|
||||||
|
def availableChars(self):
|
||||||
|
"""return number of chars in input buffer"""
|
||||||
return self._dev.availableChars
|
return self._dev.availableChars
|
||||||
|
|
||||||
def do_availableLines(self):
|
@Command(argument=None, result=IntRange(0))
|
||||||
|
def availableLines(self):
|
||||||
|
"""return number of lines in input buffer"""
|
||||||
return self._dev.availableLines
|
return self._dev.availableLines
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Andeen Hagerling capacitance bridge"""
|
"""Andeen Hagerling capacitance bridge"""
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done
|
from secop.core import Readable, Parameter, FloatRange, HasIodev, StringIO, Done
|
||||||
|
|
||||||
|
|
||||||
class Ah2700IO(StringIO):
|
class Ah2700IO(StringIO):
|
||||||
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Capacitance(HasIodev, Readable):
|
class Capacitance(HasIodev, Readable):
|
||||||
parameters = {
|
|
||||||
'value': Override('capacitance', FloatRange(unit='pF'), poll=True),
|
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
||||||
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0),
|
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||||
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0),
|
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||||
'loss': Parameter('loss', FloatRange(unit='deg'), default=0),
|
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||||
}
|
|
||||||
iodevClass = Ah2700IO
|
iodevClass = Ah2700IO
|
||||||
|
|
||||||
def parse_reply(self, reply):
|
def parse_reply(self, reply):
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
not tested yet"""
|
not tested yet"""
|
||||||
|
|
||||||
from secop.core import Writable, Module, Parameter, Override, Attached,\
|
from secop.core import Writable, Module, Parameter, Attached,\
|
||||||
BoolType, FloatRange, EnumType, HasIodev, StringIO
|
BoolType, FloatRange, EnumType, HasIodev, StringIO
|
||||||
|
|
||||||
|
|
||||||
@ -42,13 +42,13 @@ SOURCECMDS = {
|
|||||||
|
|
||||||
|
|
||||||
class SourceMeter(HasIodev, Module):
|
class SourceMeter(HasIodev, Module):
|
||||||
parameters = {
|
|
||||||
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True),
|
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
||||||
'power': Parameter('readback power', FloatRange(unit='W'), poll=True),
|
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
||||||
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||||
readonly=False, default=0),
|
readonly=False, default=0)
|
||||||
'active': Parameter('output enable', BoolType(), readonly=False, poll=True),
|
active = Parameter('output enable', BoolType(), readonly=False, poll=True)
|
||||||
}
|
|
||||||
iodevClass = K2601bIO
|
iodevClass = K2601bIO
|
||||||
|
|
||||||
def read_resistivity(self):
|
def read_resistivity(self):
|
||||||
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
|
|||||||
|
|
||||||
|
|
||||||
class Current(HasIodev, Writable):
|
class Current(HasIodev, Writable):
|
||||||
properties = {
|
sourcemeter = Attached()
|
||||||
'sourcemeter': Attached(),
|
|
||||||
}
|
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
||||||
parameters = {
|
target = Parameter('set current', FloatRange(unit='A'), poll=True)
|
||||||
'value': Override('measured current', FloatRange(unit='A'), poll=True),
|
active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage
|
||||||
'target': Override('set current', FloatRange(unit='A'), poll=True),
|
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
|
||||||
'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage
|
|
||||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.i())')
|
return self.sendRecv('print(smua.measure.i())')
|
||||||
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
|
|||||||
|
|
||||||
|
|
||||||
class Voltage(HasIodev, Writable):
|
class Voltage(HasIodev, Writable):
|
||||||
properties = {
|
sourcemeter = Attached()
|
||||||
'sourcemeter': Attached(),
|
|
||||||
}
|
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
||||||
parameters = {
|
target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
|
||||||
'value': Override('measured voltage', FloatRange(unit='V'), poll=True),
|
active = Parameter('voltage is controlled', BoolType(), poll=True)
|
||||||
'target': Override('set voltage', FloatRange(unit='V'), poll=True),
|
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
|
||||||
'active': Parameter('voltage is controlled', BoolType(), poll=True),
|
|
||||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.v())')
|
return self.sendRecv('print(smua.measure.v())')
|
||||||
|
@ -22,8 +22,7 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached
|
from secop.modules import Readable, Drivable, Parameter, Property, Attached, Done
|
||||||
from secop.metaclass import Done
|
|
||||||
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
|
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
|
||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
from secop.poller import Poller, REGULAR
|
from secop.poller import Poller, REGULAR
|
||||||
@ -59,13 +58,11 @@ class StringIO(secop.stringio.StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Main(HasIodev, Drivable):
|
class Main(HasIodev, Drivable):
|
||||||
parameters = {
|
|
||||||
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)),
|
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
||||||
'target': Override('channel to select', datatype=IntRange(0, 17)),
|
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||||
'autoscan':
|
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||||
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False),
|
pollinterval = Parameter('sleeptime between polls', default=1)
|
||||||
'pollinterval': Override('sleeptime between polls', default=1),
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = Poller
|
pollerClass = Poller
|
||||||
iodevClass = StringIO
|
iodevClass = StringIO
|
||||||
@ -142,40 +139,23 @@ class ResChannel(HasIodev, Readable):
|
|||||||
_main = None # main module
|
_main = None # main module
|
||||||
_last_range_change = 0 # time of last range change
|
_last_range_change = 0 # time of last range change
|
||||||
|
|
||||||
properties = {
|
channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
|
||||||
'channel':
|
main = Attached()
|
||||||
Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False),
|
|
||||||
'main':
|
|
||||||
Attached()
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||||
'value':
|
pollinterval = Parameter(visibility=3)
|
||||||
Override(datatype=FloatRange(unit='Ohm')),
|
range = Parameter('reading range', readonly=False,
|
||||||
'pollinterval':
|
datatype=EnumType(**RES_RANGE), handler=rdgrng)
|
||||||
Override(visibility=3),
|
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||||
'range':
|
datatype=EnumType(**RES_RANGE))
|
||||||
Parameter('reading range', readonly=False,
|
autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||||
datatype=EnumType(**RES_RANGE), handler=rdgrng),
|
readonly=False, handler=rdgrng, default=2)
|
||||||
'minrange':
|
iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng)
|
||||||
Parameter('minimum range for software autorange', readonly=False, default=1,
|
vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng)
|
||||||
datatype=EnumType(**RES_RANGE)),
|
enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset)
|
||||||
'autorange':
|
pause = Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset)
|
||||||
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset)
|
||||||
readonly=False, handler=rdgrng, default=2),
|
filter = Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl)
|
||||||
'iexc':
|
|
||||||
Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng),
|
|
||||||
'vexc':
|
|
||||||
Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng),
|
|
||||||
'enabled':
|
|
||||||
Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset),
|
|
||||||
'pause':
|
|
||||||
Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset),
|
|
||||||
'dwell':
|
|
||||||
Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset),
|
|
||||||
'filter':
|
|
||||||
Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl),
|
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._main = self.DISPATCHER.get_module(self.main)
|
self._main = self.DISPATCHER.get_module(self.main)
|
||||||
|
@ -41,7 +41,7 @@ class Ls370Sim(Communicator):
|
|||||||
self._data[fmt % chan] = v
|
self._data[fmt % chan] = v
|
||||||
# mkthread(self.run)
|
# mkthread(self.run)
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
# simulation part, time independent
|
# simulation part, time independent
|
||||||
for channel in range(1,17):
|
for channel in range(1,17):
|
||||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||||
|
@ -34,8 +34,8 @@ Polling of value and status is done commonly for all modules. For each registere
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from secop.modules import Module, Readable, Drivable, Parameter, Override,\
|
from secop.modules import Readable, Drivable, Parameter,\
|
||||||
Communicator, Property, Attached
|
Communicator, Property, Attached, HasAccessibles, Done
|
||||||
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
|
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
|
||||||
BoolType, StatusType
|
BoolType, StatusType
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
@ -44,7 +44,6 @@ from secop.errors import HardwareError
|
|||||||
from secop.poller import Poller
|
from secop.poller import Poller
|
||||||
import secop.iohandler
|
import secop.iohandler
|
||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
from secop.metaclass import Done
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import secop_psi.ppmswindows as ppmshw
|
import secop_psi.ppmswindows as ppmshw
|
||||||
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
|
|||||||
class Main(Communicator):
|
class Main(Communicator):
|
||||||
"""ppms communicator module"""
|
"""ppms communicator module"""
|
||||||
|
|
||||||
parameters = {
|
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||||
'pollinterval': Parameter('poll interval', readonly=False,
|
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
||||||
datatype=FloatRange(), default=2),
|
default="", readonly=True)
|
||||||
'communicate': Override('GBIP command'),
|
|
||||||
'data': Parameter('internal', poll=True, export=True, # export for test only
|
|
||||||
default="", readonly=True, datatype=StringType()),
|
|
||||||
}
|
|
||||||
properties = {
|
|
||||||
'class_id': Property('Quantum Design class id', export=False,
|
|
||||||
datatype=StringType()),
|
|
||||||
}
|
|
||||||
|
|
||||||
_channel_names = ['packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
|
class_id = Property('Quantum Design class id', StringType(), export=False)
|
||||||
|
|
||||||
|
_channel_names = [
|
||||||
|
'packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
|
||||||
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
|
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
|
||||||
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
||||||
assert len(_channel_names) == 30
|
assert len(_channel_names) == 30
|
||||||
@ -102,7 +96,8 @@ class Main(Communicator):
|
|||||||
def register(self, other):
|
def register(self, other):
|
||||||
self.modules[other.channel] = other
|
self.modules[other.channel] = other
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
|
"""GPIB command"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
reply = self._ppms_device.send(command)
|
reply = self._ppms_device.send(command)
|
||||||
self.log.debug("%s|%s", command, reply)
|
self.log.debug("%s|%s", command, reply)
|
||||||
@ -114,7 +109,7 @@ class Main(Communicator):
|
|||||||
if channel.enabled:
|
if channel.enabled:
|
||||||
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
||||||
# send, read and convert to floats and ints
|
# send, read and convert to floats and ints
|
||||||
data = self.do_communicate('GETDAT? %d' % mask)
|
data = self.communicate('GETDAT? %d' % mask)
|
||||||
reply = data.split(',')
|
reply = data.split(',')
|
||||||
mask = int(reply.pop(0))
|
mask = int(reply.pop(0))
|
||||||
reply.pop(0) # pop timestamp
|
reply.pop(0) # pop timestamp
|
||||||
@ -133,11 +128,9 @@ class Main(Communicator):
|
|||||||
return data # return data as string
|
return data # return data as string
|
||||||
|
|
||||||
|
|
||||||
class PpmsMixin(HasIodev, Module):
|
class PpmsMixin(HasIodev, HasAccessibles):
|
||||||
"""common methods for ppms modules"""
|
"""common methods for ppms modules"""
|
||||||
properties = {
|
iodev = Attached()
|
||||||
'iodev': Attached(),
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = Poller
|
pollerClass = Poller
|
||||||
enabled = True # default, if no parameter enable is defined
|
enabled = True # default, if no parameter enable is defined
|
||||||
@ -177,28 +170,21 @@ class PpmsMixin(HasIodev, Module):
|
|||||||
|
|
||||||
class Channel(PpmsMixin, Readable):
|
class Channel(PpmsMixin, Readable):
|
||||||
"""channel base class"""
|
"""channel base class"""
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter('main value of channels', poll=True)
|
||||||
Override('main value of channels', poll=True),
|
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||||
'enabled':
|
datatype=BoolType(), default=False)
|
||||||
Parameter('is this channel used?', readonly=False, poll=False,
|
pollinterval = Parameter(visibility=3)
|
||||||
datatype=BoolType(), default=False),
|
|
||||||
'pollinterval':
|
channel = Property('channel name',
|
||||||
Override(visibility=3),
|
datatype=StringType(), export=False, default='')
|
||||||
}
|
no = Property('channel number',
|
||||||
properties = {
|
datatype=IntRange(1, 4), export=False)
|
||||||
'channel':
|
|
||||||
Property('channel name',
|
|
||||||
datatype=StringType(), export=False, default=''),
|
|
||||||
'no':
|
|
||||||
Property('channel number',
|
|
||||||
datatype=IntRange(1, 4), export=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
Readable.earlyInit(self)
|
Readable.earlyInit(self)
|
||||||
if not self.channel:
|
if not self.channel:
|
||||||
self.properties['channel'] = self.name
|
self.channel = self.name
|
||||||
|
|
||||||
def get_settings(self, pname):
|
def get_settings(self, pname):
|
||||||
return ''
|
return ''
|
||||||
@ -207,19 +193,12 @@ class Channel(PpmsMixin, Readable):
|
|||||||
class UserChannel(Channel):
|
class UserChannel(Channel):
|
||||||
"""user channel"""
|
"""user channel"""
|
||||||
|
|
||||||
parameters = {
|
pollinterval = Parameter(visibility=3)
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
properties = {
|
|
||||||
'no':
|
|
||||||
Property('channel number',
|
|
||||||
datatype=IntRange(0, 0), export=False, default=0),
|
|
||||||
'linkenable':
|
|
||||||
Property('name of linked channel for enabling',
|
|
||||||
datatype=StringType(), export=False, default=''),
|
|
||||||
|
|
||||||
}
|
no = Property('channel number',
|
||||||
|
datatype=IntRange(0, 0), export=False, default=0)
|
||||||
|
linkenable = Property('name of linked channel for enabling',
|
||||||
|
datatype=StringType(), export=False, default='')
|
||||||
|
|
||||||
def write_enabled(self, enabled):
|
def write_enabled(self, enabled):
|
||||||
other = self._iodev.modules.get(self.linkenable, None)
|
other = self._iodev.modules.get(self.linkenable, None)
|
||||||
@ -233,16 +212,11 @@ class DriverChannel(Channel):
|
|||||||
|
|
||||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||||
|
|
||||||
parameters = {
|
current = Parameter('driver current', readonly=False, handler=drvout,
|
||||||
'current':
|
datatype=FloatRange(0., 5000., unit='uA'))
|
||||||
Parameter('driver current', readonly=False, handler=drvout,
|
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
||||||
datatype=FloatRange(0., 5000., unit='uA')),
|
datatype=FloatRange(0., 1000., unit='uW'))
|
||||||
'powerlimit':
|
pollinterval = Parameter(visibility=3)
|
||||||
Parameter('power limit', readonly=False, handler=drvout,
|
|
||||||
datatype=FloatRange(0., 1000., unit='uW')),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
|
|
||||||
def analyze_drvout(self, no, current, powerlimit):
|
def analyze_drvout(self, no, current, powerlimit):
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
@ -260,27 +234,19 @@ class BridgeChannel(Channel):
|
|||||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||||
parameters = {
|
|
||||||
'enabled':
|
enabled = Parameter(handler=bridge)
|
||||||
Override(handler=bridge),
|
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
||||||
'excitation':
|
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||||
Parameter('excitation current', readonly=False, handler=bridge,
|
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
||||||
datatype=FloatRange(0.01, 5000., unit='uA')),
|
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||||
'powerlimit':
|
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||||
Parameter('power limit', readonly=False, handler=bridge,
|
datatype=BoolType())
|
||||||
datatype=FloatRange(0.001, 1000., unit='uW')),
|
readingmode = Parameter('reading mode', readonly=False, handler=bridge,
|
||||||
'dcflag':
|
datatype=EnumType(ReadingMode))
|
||||||
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
|
||||||
datatype=BoolType()),
|
datatype=FloatRange(0.0001, 100., unit='mV'))
|
||||||
'readingmode':
|
pollinterval = Parameter(visibility=3)
|
||||||
Parameter('reading mode', readonly=False, handler=bridge,
|
|
||||||
datatype=EnumType(ReadingMode)),
|
|
||||||
'voltagelimit':
|
|
||||||
Parameter('voltage limit', readonly=False, handler=bridge,
|
|
||||||
datatype=FloatRange(0.0001, 100., unit='mV')),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
|
|
||||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
@ -306,12 +272,9 @@ class Level(PpmsMixin, Readable):
|
|||||||
|
|
||||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
||||||
'value': Override(datatype=FloatRange(unit='%'), handler=level),
|
status = Parameter(handler=level)
|
||||||
'status': Override(handler=level),
|
pollinterval = Parameter(visibility=3)
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = 'level'
|
channel = 'level'
|
||||||
|
|
||||||
@ -360,16 +323,13 @@ class Chamber(PpmsMixin, Drivable):
|
|||||||
venting_continuously=9,
|
venting_continuously=9,
|
||||||
general_failure=15,
|
general_failure=15,
|
||||||
)
|
)
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(description='chamber state', handler=chamber,
|
||||||
Override(description='chamber state', handler=chamber,
|
datatype=EnumType(StatusCode))
|
||||||
datatype=EnumType(StatusCode)),
|
target = Parameter(description='chamber command', handler=chamber,
|
||||||
'target':
|
datatype=EnumType(Operation))
|
||||||
Override(description='chamber command', handler=chamber,
|
pollinterval = Parameter(visibility=3)
|
||||||
datatype=EnumType(Operation)),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
||||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
||||||
@ -409,37 +369,29 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
"""temperature"""
|
"""temperature"""
|
||||||
|
|
||||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||||
Status = Enum(Drivable.Status,
|
Status = Enum(
|
||||||
RAMPING = 370,
|
Drivable.Status,
|
||||||
STABILIZING = 380,
|
RAMPING=370,
|
||||||
|
STABILIZING=380,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
||||||
Override(datatype=FloatRange(unit='K'), poll=True),
|
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||||
'status':
|
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
||||||
Override(datatype=StatusType(Status), poll=True),
|
setpoint = Parameter('intermediate set point',
|
||||||
'target':
|
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
||||||
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False),
|
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||||
'setpoint':
|
datatype=FloatRange(0, 20, unit='K/min'))
|
||||||
Parameter('intermediate set point',
|
workingramp = Parameter('intermediate ramp value',
|
||||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp),
|
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
||||||
'ramp':
|
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
||||||
Parameter('ramping speed', readonly=False, default=0,
|
datatype=EnumType(ApproachMode))
|
||||||
datatype=FloatRange(0, 20, unit='K/min')),
|
pollinterval = Parameter(visibility=3)
|
||||||
'workingramp':
|
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||||
Parameter('intermediate ramp value',
|
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp),
|
|
||||||
'approachmode':
|
|
||||||
Parameter('how to approach target!', readonly=False, handler=temp,
|
|
||||||
datatype=EnumType(ApproachMode)),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
'timeout':
|
|
||||||
Parameter('drive timeout, in addition to ramp time', readonly=False,
|
|
||||||
datatype=FloatRange(0, unit='sec'), default=3600),
|
|
||||||
}
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
TempStatus = Enum(
|
TempStatus = Enum(
|
||||||
'TempStatus',
|
'TempStatus',
|
||||||
@ -464,17 +416,14 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
14: (Status.ERROR, 'can not complete'),
|
14: (Status.ERROR, 'can not complete'),
|
||||||
15: (Status.ERROR, 'general failure'),
|
15: (Status.ERROR, 'general failure'),
|
||||||
}
|
}
|
||||||
properties = {
|
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||||
'general_stop': Property('respect general stop', datatype=BoolType(),
|
default=True, value=False)
|
||||||
export=True, default=True)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = 'temp'
|
channel = 'temp'
|
||||||
_stopped = False
|
_stopped = False
|
||||||
_expected_target_time = 0
|
_expected_target_time = 0
|
||||||
_last_change = 0 # 0 means no target change is pending
|
_last_change = 0 # 0 means no target change is pending
|
||||||
_last_target = None # last reached target
|
_last_target = None # last reached target
|
||||||
general_stop = False
|
|
||||||
_cool_deadline = 0
|
_cool_deadline = 0
|
||||||
_wait_at10 = False
|
_wait_at10 = False
|
||||||
_ramp_at_limit = False
|
_ramp_at_limit = False
|
||||||
@ -588,7 +537,7 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
def calc_expected(self, target, ramp):
|
def calc_expected(self, target, ramp):
|
||||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
if self.status[0] != self.Status.STABILIZING:
|
if self.status[0] != self.Status.STABILIZING:
|
||||||
@ -605,35 +554,27 @@ class Field(PpmsMixin, Drivable):
|
|||||||
"""magnetic field"""
|
"""magnetic field"""
|
||||||
|
|
||||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||||
Status = Enum(Drivable.Status,
|
Status = Enum(
|
||||||
PREPARED = 150,
|
Drivable.Status,
|
||||||
PREPARING = 340,
|
PREPARED=150,
|
||||||
RAMPING = 370,
|
PREPARING=340,
|
||||||
FINALIZING = 390,
|
RAMPING=370,
|
||||||
|
FINALIZING=390,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
||||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
||||||
'value':
|
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||||
Override(datatype=FloatRange(unit='T'), poll=True),
|
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
||||||
'status':
|
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
||||||
Override(datatype=StatusType(Status), poll=True),
|
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
||||||
'target':
|
approachmode = Parameter('how to approach target', readonly=False, handler=field,
|
||||||
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field),
|
datatype=EnumType(ApproachMode))
|
||||||
'ramp':
|
persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
|
||||||
Parameter('ramping speed', readonly=False, handler=field,
|
datatype=EnumType(PersistentMode))
|
||||||
datatype=FloatRange(0.064, 1.19, unit='T/min')),
|
pollinterval = Parameter(visibility=3)
|
||||||
'approachmode':
|
|
||||||
Parameter('how to approach target', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(ApproachMode)),
|
|
||||||
'persistentmode':
|
|
||||||
Parameter('what to do after changing field', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(PersistentMode)),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'persistent mode'),
|
1: (Status.IDLE, 'persistent mode'),
|
||||||
@ -735,7 +676,7 @@ class Field(PpmsMixin, Drivable):
|
|||||||
return Done
|
return Done
|
||||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
@ -751,20 +692,15 @@ class Position(PpmsMixin, Drivable):
|
|||||||
|
|
||||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
||||||
Override(datatype=FloatRange(unit='deg'), poll=True),
|
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
||||||
'target':
|
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||||
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move),
|
datatype=BoolType(), default=True)
|
||||||
'enabled':
|
speed = Parameter('motor speed', readonly=False, handler=move,
|
||||||
Parameter('is this channel used?', readonly=False, poll=False,
|
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||||
datatype=BoolType(), default=True),
|
pollinterval = Parameter(visibility=3)
|
||||||
'speed':
|
|
||||||
Parameter('motor speed', readonly=False, handler=move,
|
|
||||||
datatype=FloatRange(0.8, 12, unit='deg/sec')),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
}
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'at target'),
|
1: (Status.IDLE, 'at target'),
|
||||||
5: (Status.BUSY, 'moving'),
|
5: (Status.BUSY, 'moving'),
|
||||||
@ -843,7 +779,7 @@ class Position(PpmsMixin, Drivable):
|
|||||||
self.speed = value
|
self.speed = value
|
||||||
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
|
@ -26,7 +26,7 @@ import math
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.interpolate import splrep, splev # pylint: disable=import-error
|
from scipy.interpolate import splrep, splev # pylint: disable=import-error
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, Attached, StringType, BoolType
|
from secop.core import Readable, Parameter, Attached, StringType, BoolType
|
||||||
|
|
||||||
|
|
||||||
def linear(x):
|
def linear(x):
|
||||||
@ -102,6 +102,7 @@ class CalCurve:
|
|||||||
sensopt = calibspec.split(',')
|
sensopt = calibspec.split(',')
|
||||||
calibname = sensopt.pop(0)
|
calibname = sensopt.pop(0)
|
||||||
_, dot, ext = basename(calibname).rpartition('.')
|
_, dot, ext = basename(calibname).rpartition('.')
|
||||||
|
kind = None
|
||||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
||||||
# first try without adding kind
|
# first try without adding kind
|
||||||
filename = join(path.strip(), calibname)
|
filename = join(path.strip(), calibname)
|
||||||
@ -150,16 +151,14 @@ class CalCurve:
|
|||||||
|
|
||||||
|
|
||||||
class Sensor(Readable):
|
class Sensor(Readable):
|
||||||
properties = {
|
rawsensor = Attached()
|
||||||
'rawsensor': Attached(),
|
|
||||||
}
|
calib = Parameter('calibration name', datatype=StringType(), readonly=False)
|
||||||
parameters = {
|
abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
|
||||||
'calib': Parameter('calibration name', datatype=StringType(), readonly=False),
|
value = Parameter(unit='K')
|
||||||
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True),
|
pollinterval = Parameter(export=False)
|
||||||
'value': Override(unit='K'),
|
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||||
'pollinterval': Override(export=False),
|
|
||||||
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
|
|
||||||
}
|
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
description = 'a calibrated sensor value'
|
description = 'a calibrated sensor value'
|
||||||
_value_error = None
|
_value_error = None
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
# no fixtures needed
|
# no fixtures needed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, Enum, StatusType, \
|
||||||
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
||||||
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
||||||
|
|
||||||
@ -359,6 +359,7 @@ def test_BoolType():
|
|||||||
# pylint: disable=unexpected-keyword-arg
|
# pylint: disable=unexpected-keyword-arg
|
||||||
BoolType(unit='K')
|
BoolType(unit='K')
|
||||||
|
|
||||||
|
|
||||||
def test_ArrayOf():
|
def test_ArrayOf():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -478,6 +479,14 @@ def test_Command():
|
|||||||
'result':{'type': 'int', 'min':-3, 'max':3}}
|
'result':{'type': 'int', 'min':-3, 'max':3}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_StatusType():
|
||||||
|
status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400)
|
||||||
|
dt = StatusType(status_codes)
|
||||||
|
assert dt.IDLE == status_codes.IDLE
|
||||||
|
assert dt.ERROR == status_codes.ERROR
|
||||||
|
assert dt._enum == status_codes
|
||||||
|
|
||||||
|
|
||||||
def test_get_datatype():
|
def test_get_datatype():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
get_datatype(1)
|
get_datatype(1)
|
||||||
|
@ -107,15 +107,11 @@ def test_IOHandler():
|
|||||||
|
|
||||||
|
|
||||||
class Module1(Module):
|
class Module1(Module):
|
||||||
properties = {
|
channel = Property('the channel', IntRange(), default=3)
|
||||||
'channel': Property('the channel', IntRange(), default=3),
|
loop = Property('the loop', IntRange(), default=2)
|
||||||
'loop': Property('the loop', IntRange(), default=2),
|
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
|
||||||
}
|
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
||||||
parameters = {
|
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
|
||||||
'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False),
|
|
||||||
'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def sendRecv(self, command):
|
||||||
assert data.pop('command') == command
|
assert data.pop('command') == command
|
||||||
@ -196,6 +192,4 @@ def test_IOHandler():
|
|||||||
with pytest.raises(ProgrammingError): # can not use a handler for different modules
|
with pytest.raises(ProgrammingError): # can not use a handler for different modules
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
class Module2(Module):
|
class Module2(Module):
|
||||||
parameters = {
|
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
|
||||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
|
||||||
}
|
|
||||||
|
@ -22,14 +22,14 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""test data types."""
|
"""test data types."""
|
||||||
|
|
||||||
# no fixtures needed
|
|
||||||
#import pytest
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import BoolType, FloatRange, StringType
|
from secop.datatypes import BoolType, FloatRange, StringType
|
||||||
from secop.modules import Communicator, Drivable, Module
|
from secop.modules import Communicator, Drivable, Module
|
||||||
from secop.params import Command, Override, Parameter, usercommand
|
from secop.params import Command, Parameter
|
||||||
from secop.poller import BasicPoller
|
from secop.poller import BasicPoller
|
||||||
|
from secop.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
class DispatcherStub:
|
class DispatcherStub:
|
||||||
@ -64,30 +64,27 @@ def test_Communicator():
|
|||||||
assert event.is_set() # event should be set immediately
|
assert event.is_set() # event should be set immediately
|
||||||
|
|
||||||
|
|
||||||
def test_ModuleMeta():
|
def test_ModuleMagic():
|
||||||
class Newclass1(Drivable):
|
class Newclass1(Drivable):
|
||||||
parameters = {
|
param1 = Parameter('param1', datatype=BoolType(), default=False)
|
||||||
'pollinterval': Override(reorder=True),
|
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
|
||||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
|
||||||
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
@Command(argument=BoolType(), result=BoolType())
|
||||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
def cmd(self, arg):
|
||||||
}
|
"""stuff"""
|
||||||
commands = {
|
return not arg
|
||||||
# intermixing parameters with commands is not recommended,
|
|
||||||
# but acceptable for influencing the order
|
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
value = Parameter(datatype=StringType(), default='first')
|
||||||
'value': Override(datatype=StringType(), default='first'),
|
|
||||||
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
|
@Command(argument=BoolType(), result=BoolType())
|
||||||
}
|
def cmd2(self, arg):
|
||||||
|
"""another stuff"""
|
||||||
|
return not arg
|
||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
def do_cmd(self, arg):
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
def do_cmd2(self, arg):
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
def read_param1(self):
|
def read_param1(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -103,19 +100,31 @@ def test_ModuleMeta():
|
|||||||
def read_value(self):
|
def read_value(self):
|
||||||
return 'second'
|
return 'second'
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class Mod1(Module): # pylint: disable=unused-variable
|
||||||
|
def do_this(self): # old style command
|
||||||
|
pass
|
||||||
|
|
||||||
# first inherited accessibles, then Overrides with reorder=True and new accessibles
|
with pytest.raises(ProgrammingError):
|
||||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
class Mod2(Module): # pylint: disable=unused-variable
|
||||||
|
param = Parameter(), # pylint: disable=trailing-comma-tuple
|
||||||
|
|
||||||
|
|
||||||
|
# first inherited accessibles
|
||||||
|
sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop',
|
||||||
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
||||||
|
|
||||||
class Newclass2(Newclass1):
|
class Newclass2(Newclass1):
|
||||||
parameters = {
|
paramOrder = 'param1', 'param2', 'cmd', 'value'
|
||||||
'cmd2': Override('another stuff'),
|
|
||||||
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
@Command(description='another stuff')
|
||||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
|
def cmd2(self, arg):
|
||||||
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
|
return arg
|
||||||
poll=True, readonly=False, initwrite=True),
|
|
||||||
}
|
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||||
|
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
||||||
|
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
||||||
|
poll=True, readonly=False, initwrite=True)
|
||||||
|
|
||||||
def write_a1(self, value):
|
def write_a1(self, value):
|
||||||
self._a1_written = value
|
self._a1_written = value
|
||||||
@ -128,41 +137,9 @@ def test_ModuleMeta():
|
|||||||
def read_value(self):
|
def read_value(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
sortcheck2 = ['status', 'target', 'pollinterval',
|
# first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
|
||||||
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'value', 'a1', 'b2']
|
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
|
||||||
|
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
|
||||||
# check consistency of new syntax:
|
|
||||||
class Testclass1(Drivable):
|
|
||||||
pollinterval = Parameter(reorder=True)
|
|
||||||
param1 = Parameter('param1', datatype=BoolType(), default=False)
|
|
||||||
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
|
|
||||||
|
|
||||||
@usercommand(BoolType(), BoolType())
|
|
||||||
def cmd(self, arg):
|
|
||||||
"""stuff"""
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
|
||||||
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
|
||||||
value = Parameter(datatype=StringType(), default='first')
|
|
||||||
|
|
||||||
@usercommand(BoolType(), BoolType())
|
|
||||||
def cmd2(self, arg):
|
|
||||||
"""another stuff"""
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
class Testclass2(Testclass1):
|
|
||||||
cmd2 = Command('another stuff')
|
|
||||||
value = Parameter(datatype=FloatRange(unit='deg'), reorder=True)
|
|
||||||
a1 = Parameter(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False)
|
|
||||||
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
|
||||||
poll=True, readonly=False, initwrite=True)
|
|
||||||
|
|
||||||
for old, new in (Newclass1, Testclass1), (Newclass2, Testclass2):
|
|
||||||
assert len(old.accessibles) == len(new.accessibles)
|
|
||||||
for (oname, oobj), (nname, nobj) in zip(old.accessibles.items(), new.accessibles.items()):
|
|
||||||
assert oname == nname
|
|
||||||
assert oobj.for_export() == nobj.for_export()
|
|
||||||
|
|
||||||
logger = LoggerStub()
|
logger = LoggerStub()
|
||||||
updates = {}
|
updates = {}
|
||||||
@ -176,15 +153,11 @@ def test_ModuleMeta():
|
|||||||
o2 = newclass('o2', logger, {'.description':''}, srv)
|
o2 = newclass('o2', logger, {'.description':''}, srv)
|
||||||
for obj in [o1, o2]:
|
for obj in [o1, o2]:
|
||||||
objects.append(obj)
|
objects.append(obj)
|
||||||
ctr_found = set()
|
for o in obj.accessibles.values():
|
||||||
for n, o in obj.accessibles.items():
|
|
||||||
# check that instance accessibles are unique objects
|
# check that instance accessibles are unique objects
|
||||||
assert o not in params_found
|
assert o not in params_found
|
||||||
params_found.add(o)
|
params_found.add(o)
|
||||||
assert o.ctr not in ctr_found
|
assert list(obj.accessibles) == sortcheck
|
||||||
ctr_found.add(o.ctr)
|
|
||||||
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
|
|
||||||
assert check_order == sorted(check_order)
|
|
||||||
|
|
||||||
# check for inital updates working properly
|
# check for inital updates working properly
|
||||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||||
@ -246,7 +219,7 @@ def test_ModuleMeta():
|
|||||||
assert acs is not None
|
assert acs is not None
|
||||||
else: # do not check object or mixin
|
else: # do not check object or mixin
|
||||||
acs = {}
|
acs = {}
|
||||||
for n, o in acs.items():
|
for o in acs.values():
|
||||||
# check that class accessibles are not reused as instance accessibles
|
# check that class accessibles are not reused as instance accessibles
|
||||||
assert o not in params_found
|
assert o not in params_found
|
||||||
|
|
||||||
|
@ -25,68 +25,78 @@
|
|||||||
# no fixtures needed
|
# no fixtures needed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import BoolType, IntRange
|
from secop.datatypes import BoolType, IntRange, FloatRange
|
||||||
from secop.params import Command, Override, Parameter, Parameters
|
from secop.params import Command, Parameter
|
||||||
|
from secop.modules import HasAccessibles
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError
|
||||||
|
|
||||||
|
|
||||||
def test_Command():
|
def test_Command():
|
||||||
cmd = Command('do_something')
|
class Mod(HasAccessibles):
|
||||||
assert cmd.description == 'do_something'
|
@Command()
|
||||||
assert cmd.ctr
|
def cmd(self):
|
||||||
assert cmd.argument is None
|
"""do something"""
|
||||||
assert cmd.result is None
|
@Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
|
||||||
assert cmd.for_export() == {'datainfo': {'type': 'command'},
|
def cmd2(self):
|
||||||
'description': 'do_something'}
|
pass
|
||||||
|
|
||||||
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1))
|
assert Mod.cmd.description == 'do something'
|
||||||
assert cmd.description
|
assert Mod.cmd.argument is None
|
||||||
assert isinstance(cmd.argument, IntRange)
|
assert Mod.cmd.result is None
|
||||||
assert isinstance(cmd.result, IntRange)
|
assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||||
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9},
|
'description': 'do something'}
|
||||||
'result': {'type': 'int', 'min':-1, 'max':1}},
|
|
||||||
'description': 'do_something'}
|
assert Mod.cmd2.description == 'do some other thing'
|
||||||
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
assert isinstance(Mod.cmd2.argument, IntRange)
|
||||||
|
assert isinstance(Mod.cmd2.result, IntRange)
|
||||||
|
assert Mod.cmd2.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min': -9, 'max': 9},
|
||||||
|
'result': {'type': 'int', 'min': -1, 'max': 1}},
|
||||||
|
'description': 'do some other thing'}
|
||||||
|
assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
||||||
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
||||||
'description': 'do_something'}
|
'description': 'do some other thing'}
|
||||||
|
|
||||||
|
|
||||||
def test_Parameter():
|
def test_Parameter():
|
||||||
p1 = Parameter('description1', datatype=IntRange(), default=0)
|
class Mod(HasAccessibles):
|
||||||
p2 = Parameter('description2', datatype=IntRange(), constant=1)
|
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
||||||
assert p1 != p2
|
p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
|
||||||
assert p1.ctr != p2.ctr
|
p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False)
|
||||||
|
p4 = Parameter('desc4', datatype=FloatRange(), constant=1)
|
||||||
|
assert repr(Mod.p1) != repr(Mod.p3)
|
||||||
|
assert id(Mod.p1.datatype) != id(Mod.p2.datatype)
|
||||||
|
assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True}
|
||||||
|
assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True}
|
||||||
|
assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False}
|
||||||
|
assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True,
|
||||||
|
'constant': 1.0}
|
||||||
|
p3 = Mod.p1.copy()
|
||||||
|
assert id(p3) != id(Mod.p1)
|
||||||
|
assert repr(Mod.p1) == repr(p3)
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
Parameter(None, datatype=float, inherit=False)
|
Parameter(None, datatype=float, inherit=False)
|
||||||
p3 = p1.copy()
|
|
||||||
assert p1.ctr == p3.ctr
|
|
||||||
p3.ctr = p1.ctr # manipulate ctr for next line
|
|
||||||
assert repr(p1) == repr(p3)
|
|
||||||
assert p1.datatype != p2.datatype
|
|
||||||
|
|
||||||
|
|
||||||
def test_Override():
|
def test_Override():
|
||||||
p = Parameter('description1', datatype=BoolType, default=False)
|
class Base(HasAccessibles):
|
||||||
|
p1 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
|
p2 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
|
p3 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
|
|
||||||
o = Override(default=True, reorder=True)
|
class Mod(Base):
|
||||||
q = o.apply(p)
|
p1 = Parameter(default=True)
|
||||||
qctr = q.ctr
|
p2 = Parameter() # override without change
|
||||||
assert q.ctr > p.ctr # reorder=True: take ctr from override object
|
|
||||||
assert q != p
|
|
||||||
assert qctr == o.apply(p).ctr # do not create a new ctr when applied again
|
|
||||||
|
|
||||||
o2 = Override(default=True)
|
assert Mod.p1 != Base.p1
|
||||||
q2 = o2.apply(p)
|
assert Mod.p2 != Base.p2
|
||||||
assert q2.ctr == p.ctr # reorder=False: take ctr from inherited param
|
assert Mod.p3 == Base.p3
|
||||||
assert q2 != p
|
|
||||||
assert repr(q2) != repr(p)
|
|
||||||
|
|
||||||
q3 = Override().apply(p) # Override without change
|
assert id(Mod.p2) != id(Base.p2) # must be a new object
|
||||||
assert id(q2) != id(p) # must be a new object
|
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
|
||||||
assert repr(q3) == repr(p) # but must be a clone
|
|
||||||
|
|
||||||
|
|
||||||
def test_Parameters():
|
def test_Export():
|
||||||
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True)))
|
class Mod:
|
||||||
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True)
|
param = Parameter('description1', datatype=BoolType, default=False)
|
||||||
assert ps['_p2'].export == '_p2'
|
assert Mod.param.export == '_param'
|
||||||
|
@ -24,38 +24,58 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
||||||
from secop.errors import ProgrammingError, ConfigError
|
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||||
from secop.properties import Property, Properties, HasProperties
|
from secop.properties import Property, HasProperties
|
||||||
|
|
||||||
# args are: datatype, default, extname, export, mandatory, settable
|
|
||||||
|
def Prop(*args, name=None, **kwds):
|
||||||
|
# collect the args for Property
|
||||||
|
return name, args, kwds
|
||||||
|
|
||||||
|
|
||||||
|
# Property(description, datatype, default, ...)
|
||||||
V_test_Property = [
|
V_test_Property = [
|
||||||
[(StringType(), 'default', 'extname', False, False),
|
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||||
dict(default='default', extname='extname', export=True, mandatory=False)],
|
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||||
[(IntRange(), '42', '_extname', False, True),
|
],
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||||
[(IntRange(), '42', '_extname', True, False),
|
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=False)],
|
],
|
||||||
[(IntRange(), 42, '_extname', True, True),
|
[Prop(IntRange(), '42', export=True, name='name'),
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||||
[(IntRange(), 0, '', True, True),
|
],
|
||||||
dict(default=0, extname='', export=True, mandatory=True)],
|
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||||
[(IntRange(), 0, '', True, False),
|
dict(default=42, extname='_extname', export=True, mandatory=True)
|
||||||
dict(default=0, extname='', export=True, mandatory=False)],
|
],
|
||||||
[(IntRange(), 0, '', False, True),
|
[Prop(IntRange(), 0, export=True, mandatory=True),
|
||||||
dict(default=0, extname='', export=False, mandatory=True)],
|
dict(default=0, extname='', export=True, mandatory=True)
|
||||||
[(IntRange(), 0, '', False, False),
|
],
|
||||||
dict(default=0, extname='', export=False, mandatory=False)],
|
[Prop(IntRange(), 0, export=True, mandatory=False),
|
||||||
[(IntRange(), None, '', None),
|
dict(default=0, extname='', export=True, mandatory=False)
|
||||||
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
|
],
|
||||||
[(ValueType(), 1, '', False),
|
[Prop(IntRange(), 0, export=False, mandatory=True),
|
||||||
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
|
dict(default=0, extname='', export=False, mandatory=True)
|
||||||
|
],
|
||||||
|
[Prop(IntRange(), 0, export=False, mandatory=False),
|
||||||
|
dict(default=0, extname='', export=False, mandatory=False)
|
||||||
|
],
|
||||||
|
[Prop(IntRange()),
|
||||||
|
dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory
|
||||||
|
],
|
||||||
|
[Prop(ValueType(), 1),
|
||||||
|
dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory
|
||||||
|
],
|
||||||
]
|
]
|
||||||
@pytest.mark.parametrize('args, check', V_test_Property)
|
@pytest.mark.parametrize('propargs, check', V_test_Property)
|
||||||
def test_Property(args, check):
|
def test_Property(propargs, check):
|
||||||
p = Property('', *args)
|
name, args, kwds = propargs
|
||||||
|
p = Property('', *args, **kwds)
|
||||||
|
if name:
|
||||||
|
p.__set_name__(None, name)
|
||||||
result = {k: getattr(p, k) for k in check}
|
result = {k: getattr(p, k) for k in check}
|
||||||
assert result == check
|
assert result == check
|
||||||
|
|
||||||
|
|
||||||
def test_Property_basic():
|
def test_Property_basic():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
@ -67,47 +87,47 @@ def test_Property_basic():
|
|||||||
Property('', 1)
|
Property('', 1)
|
||||||
Property('', IntRange(), '42', 'extname', False, False)
|
Property('', IntRange(), '42', 'extname', False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_Properties():
|
def test_Properties():
|
||||||
p = Properties()
|
class Cls(HasProperties):
|
||||||
with pytest.raises(ProgrammingError):
|
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||||
p[1] = 2
|
bb = Property('', IntRange(), 0, export=False)
|
||||||
p['a'] = Property('', IntRange(), '42', export=True)
|
|
||||||
assert p['a'].default == 42
|
assert Cls.aa.default == 42
|
||||||
assert p['a'].export is True
|
assert Cls.aa.export is True
|
||||||
assert p['a'].extname == '_a'
|
assert Cls.aa.extname == '_aa'
|
||||||
with pytest.raises(ProgrammingError):
|
|
||||||
p['a'] = 137
|
cc = Cls()
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(BadValueError):
|
||||||
del p[1]
|
cc.aa = 137
|
||||||
with pytest.raises(ProgrammingError):
|
|
||||||
del p['a']
|
assert Cls.bb.default == 0
|
||||||
p['a'] = Property('', IntRange(), 0, export=False)
|
assert Cls.bb.export is False
|
||||||
assert p['a'].default == 0
|
assert Cls.bb.extname == ''
|
||||||
assert p['a'].export is False
|
|
||||||
assert p['a'].extname == ''
|
|
||||||
|
|
||||||
|
|
||||||
class c(HasProperties):
|
class c(HasProperties):
|
||||||
properties = {
|
# properties
|
||||||
'a' : Property('', IntRange(), 1),
|
a = Property('', IntRange(), 1)
|
||||||
}
|
|
||||||
|
|
||||||
class cl(c):
|
class cl(c):
|
||||||
properties = {
|
# properties
|
||||||
'a' : Property('', IntRange(), 3),
|
a = Property('', IntRange(), 3)
|
||||||
'b' : Property('', FloatRange(), 3.14),
|
b = Property('', FloatRange(), 3.14)
|
||||||
'minabc': Property('', IntRange(), 8),
|
minabc = Property('', IntRange(), 8)
|
||||||
'maxabc': Property('', IntRange(), 9),
|
maxabc = Property('', IntRange(), 9)
|
||||||
'minx': Property('', IntRange(), 2),
|
minx = Property('', IntRange(), 2)
|
||||||
'maxy': Property('', IntRange(), 1),
|
maxy = Property('', IntRange(), 1)
|
||||||
}
|
|
||||||
|
|
||||||
def test_HasProperties():
|
def test_HasProperties():
|
||||||
o = c()
|
o = c()
|
||||||
assert o.properties['a'] == 1
|
assert o.a == 1
|
||||||
o = cl()
|
o = cl()
|
||||||
assert o.properties['a'] == 3
|
assert o.a == 3
|
||||||
assert o.properties['b'] == 3.14
|
assert o.b == 3.14
|
||||||
|
|
||||||
|
|
||||||
def test_Property_checks():
|
def test_Property_checks():
|
||||||
o = c()
|
o = c()
|
||||||
@ -119,6 +139,7 @@ def test_Property_checks():
|
|||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
o.checkProperties()
|
o.checkProperties()
|
||||||
|
|
||||||
|
|
||||||
def test_Property_override():
|
def test_Property_override():
|
||||||
o1 = c()
|
o1 = c()
|
||||||
class co(c):
|
class co(c):
|
||||||
@ -131,10 +152,10 @@ def test_Property_override():
|
|||||||
class cx(c): # pylint: disable=unused-variable
|
class cx(c): # pylint: disable=unused-variable
|
||||||
def a(self):
|
def a(self):
|
||||||
pass
|
pass
|
||||||
assert 'collides with method' in str(e.value)
|
assert 'collides with' in str(e.value)
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError) as e:
|
with pytest.raises(ProgrammingError) as e:
|
||||||
class cz(c): # pylint: disable=unused-variable
|
class cz(c): # pylint: disable=unused-variable
|
||||||
a = 's'
|
a = 's'
|
||||||
|
|
||||||
assert 'can not be set to' in str(e.value)
|
assert 'can not set' in str(e.value)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user